不知不觉,从上次我宣布回来已经过了两个月了,目前网站仍然没有更新,因为我还在做一些更基础的工作。
现在这个站点,是我高中学习PHP搭建的,在当时来说这是一个非常实在的选择。然而,时过境迁,Nodejs和js生态的繁荣,尤其是打包的技术,我认为js也已经非常适合香我这样自己达着玩的系统,甚至可以支撑一些大型系统的需求。当然PHP也一直在更新迭代,例如更新了强类型和反射,这可比TS那套编译时反射要好用太多了。不过,如果我希望用上这些新特性,那我一定需要大规模重构整个系统,一方面,由于工作原因,我现在显然对js会更加熟悉,另外就是前后端同构对ssr来说还是要友好很多(尽管理论上PHP也是SSR,但是它毕竟不是通过前端框架实现的,因此操作起来会有一些麻烦,对于spa来说更是如此,在我现在这一版的博客中,就是在页面切换时将内容部分的html通过json返回然后塞入到dom中这种粗暴方式实现的,但是也会导致需要手动执行一些脚本等各种问题,更重要的是无法使用前端的框架,甚至为了解析markdown也是费力做了一套js版本和一套PHP版本,并且两个版本之间显然也会有差异。),因此尽管PHP仍然很香,但是我还是得在js森阿提重新开始(但是希望什么时候js也能有一个真正的反射的机制吧)
我香做的,是一个前后端和ssr完全一体的框架,在现有市场中,尽管有nextjs这样的ssr+前后端框架,但是,一来其前后端开发本身是割裂的,其前后端拥有各自独立的路由,并且后端的开发体验也不好,本质上仍然还是一个前端框架。然而我希望的是一个真正的前后端一体的框架,至少,二者应当共享同一套路由,同时,我希望能和nestjs那样有方便的一来注入、类型声明既是TS类型也是参数校验,同时还能作为文档。因此我决定自己搓一个。
我需要解决的第一个问题就是js本身仍然是一个弱类型语言,尽管TS可以提供类型定义,但由于TS本身不是运行时,其类型在实际执行前会被擦除,因此实际上它并不具备多少反射的能力,即便是nestjs,使用起来仍然处处受限,一个简单类型只能通过定义class,切某人情况下还获取不到这个class有什么属性,必须添加修饰器才会有对应的元数据生成。我这里使用了一些第三方的方案,在编译阶段将类型信息写入到产物js中,但是它的确不是那么稳定,毕竟TS的类型实在太灵活了。但总之我还是实现了DI和参数类型解析。接下来的问题是如何让前后端使用同一套路由的问题。
当然,这个问题本身并不难,如果我们站在后端的角度来说,页面只是一种请求方式,甚至他只是一个get请求的返回。所以实际操作上,我为一个普通的后端加上了一个view模块,他本质会注册一个get路由,但是在返回内容时,会被view模块将其翻译成真实的HTML(后端处理时)或JS挂在指令(前端处理时),当然这个具体的转换过程需要对应的适配器(我把它叫做Render),通过不同的render我们就可以实现不同前端框架的内容渲染和挂载,同时,整个应用和前端框架无关。当然我们还需要一个Layout,不过这也不难,我们只需要允许每一级的路由能支持这样一个元数据,同时我们能定位每个请求中所有经过的路由对象就可以遍历出所有的layout,然后同样layout本身也是一个render,他们渲染时还接收一个child render渲染点即可。
但是前面说到的事理想情况,实际上我们的页面需要跳转,我希望它能像正常spa那样能够避免页面切换,同时还要避免layout重新挂载导致状态丢失,因此我需要精确控制layout的渲染范围。所以我对缘由Render做了一次代理,首先它可以接受一个新的树来进行对比,精确找到需要更新参数和子节点需要重新渲染的位置,如果原有的Render声明了对应的更新方法,我们让对应的Render自行更新,如果没有,我们因为已经记录了挂载的dom,我们可以在这个代理render中执行解除挂载和重新挂载,同时,Render也不是必须同时拥有mount和renderToString两个方法,拥有renderToString时,前端切换页面也能直接使用这个html添加到页面上并找到route标签来实现子组件的渲染,只是这样的组件几乎不会有js的执行能力。当然如果自定义了mount方法那框架就不会去干涉子组件的mount了,毕竟这时候路由的dom全可以让render自行决定,框架试试检测router节点的创建还是有点费勒不讨好(也许未来可以尝试下有没有更加友好的方式),同时render还能决定是直接当组件渲染还是调用挂载(例如layout和page都是vue编写的,我们当然更希望二者是同一个实例和上下文,当然如果render判断为不同技术栈或者没这个必要,仍然可以使用mount方法独立挂载)。
接下来的问题就是重新将前端路由分开,避免前端打包到后端代码,毕竟轻的话,后端代码通常没有也不会做模块拆分会导,着会导致到爆的路由部分异常庞大,同时数据库配置、密钥等信息被分发到前端也有非常大的安全隐患、node或其他后端特有代码被打包到前端甚至会直接因为找不到模块而无法加载。这里Minecraft的mod开发给我了一些思路,我们只需要将只能在特定的端执行的方法使用SideOnly修饰器标注即可。只要我们有处理ast语法树的处理能力,我们就可以很轻易的实现这个功能。我可不希望每次都要写SideOnly,另外由于TS本身没有反射能力,实际上修饰器的使用也仅限于class,因此我实现了文件内基于ts的ast静态追踪一个变量的定义(当然我做不到追踪动态赋值,也追踪不到跨文件的重新导出,或者合并后的调用),再给予自定义配置,实现特定修饰器的裁剪,同时也添加类型版的SideOnly解决元数据可能被污染的问题、SideSwitch便于在代码中分类执行代码、注释版SideOnly等。
对于项目产出,我希望我各个子站点项目上彼此独立,就和现在一样,但是只有一个node进程来加载这些项目,甚至可以考虑打成包,甚至进行签名。当然,目前还是处理测试阶段,只有文件夹形式的测试
整个项目目流程还是已经走通,思路上目前想到的基本都完成了验证,尽管存在很多限制且因为运行时类型有时候会出现一些纯类型的导出触发类型找不到(实际上是它写入的类型信息找不到,因为奔雷就没倒出这个),但已经初步满足我的需求了,接下来,还有一些需要处理的或者欠缺的东西:
- SideOnly模块需要进一步添加对特定调用的抹除,和一些特殊抹除方式,以便适配更多范围;
- 后端目前没有做请求体的解析,虽然这个解析的工作本身不难,但是一方面,我希望能将参数定义直接引入body解析环节,更早的阻止非法请求,对于文件,丢弃、内存存储或硬盘存储也有一个确定归宿。另一方面是我在想跨平台兼容等问题和性能问题的平衡,如果要抛弃node的数据类型就意味着我们需要先将node的Readable转换成包准格式,但是着会造成一些性能损失,这是我不希望看到的;
- 正如前面所说,我希望每个项目是个独立的模块,因此还需要探索模块隔离、热更新、签名和打包成单文件等;4、数据库选型,尽管mysql很好,但是SQLlite无需启动服务可以更方便的跨平台调试、PG者比Mysql更加强大和规范,可以肯定的是Mysql肯定是要退休了,但是具体什么形式我还无法确定。5、模块间调用,我希望有更好的用户中心调用方式,而不是在同一台服务器还需要http调用,当然,着也不一定是必须的,可以之后再看。
整体而言,我想可能再过一段时间就会有一测试版的blog或者sucs上线了,当然数据不一定会保持同步,首次测试期间,原则上正式版的站点永远使用主数据库。