基于koa+fis3+swig前后端isomorphic类同构实现
(认真把这篇文章看完,保证你会学到很多,如果没学到请联系作者或直接报警)
随着前端技术的不断变革,前端从后台吐页面 -> 前端MVC -> mvvm、react -> node直出 ->同构跨端的大致发展方向(后面我也讲下前端的跨终端实现),前端正在向着模块化、高效性、跨端性扩展。过渡完mvvm、react和node直出的阶段,前端工程师们又开始在通往后台同构的道路上探索。而且也出现了一些列可能的方案,例如使用mvc、mvvm或react在服务端做实现,无论如何。但是我们不得面临一系列问题。同构的目的?同构的核心?同构的代价?同构的优势?
一、同构的三个问题
我们先来看下这三个问题。
- 同构的目的
首先说下目前比较主流的web前后端分离方案:这种方案将web站点分为前端和后台,前端通过拉取后台数据的到页面再通过js模板渲染到页面上。几个主要问题,一是页面显示需要等后台请求(cgi等)返回来才能渲染,二是seo怎么办,同时也承受开发联调效率的考研。
所以node直出的方案在阿里、腾讯等大的前端团队里陆续被使用起来,思路是通过node直出首屏内容和关键性seo信息解决了上述问题。
但是问题是我们无法全部直出页面所有的内容,通常因为太大,所以前端仍然需要维护原有的前端代码。更多情况下,同一个站点中,我们更希望的是在某些场景下使用前端渲染,另一些情况下使用后端直出(例如,希望hybridapp在没有离线包的第一次直出,后面不需要下载静态文件时使用前端拉取渲染的方式,或者在高级的浏览器下使用http2前端渲染,低端浏览器上则使用直出),结果我们不得不维护两套不同的前后台代码,尽管可能都是用js写的。所以同构希望解决的是维护前后端维护两套代码的问题。
- 同构的核心
同构希望做的事情是只开发一套项目代码,既可以实现前端的渲染也可以做后台的直出。为什么可以这样做呢?因为后台直出页面在后端生成,实现的方式也是通过数据加上模板编译的方式生成,前端渲染和后台直出的模式生成dom的区别只在于数据和模板的渲染发生在什么时候。
- 同构的代价
同构的目的是为了统一前后台的方案,自然也会牵扯到前后端的适应性修改。例如前端的数据渲染如何与后台直出保持一致、后台如何处理异步的问题、在原有业务上的实现代价、和原有前端框架的的冲突性等等。如果考虑到这些问题,同构改造实现的代价就会很大,毕竟它是综合了两个开发人员的工作量。
- 同构的优势
同构的网站应该具有一些优势:1、可以根据用户需要方便的选择前端渲染还是后台支持;2、开发者真的只需要维护一套代码,当然这个是不严谨的,后台多出的工作是配置路由和数据接口编写,但和前端dom相关的指维护一套;3、前端的模块化开发和后台所有模块是共享的;4、可以避免前后端工程师的联调沟通成本,但整体成本会比单个人开发大;4、开发构建调试系统完善。
二、可能的解决方案
- reactjs
这个被讨论的比较火。但事实上,目前直接使用react都比较困难,而react的应用现在就少,而且也没见过大型应用使用,所以同构的价值不大,其实对于此实现方案在开发时的配置依然比较多。不过技术上react同构是一个可行度很高的方案。
想多了解的话可以看下《isomorphic-reactjs》,其核心思路是使用renderToString将virtual dom直接转化成为html,因为virtual dom在前后端都可用。这样就实现了直出的转换。然而我想说的是,reactjs项目里面html和js混淆,模板语言生硬,渲染和事件绑定在一起,行为和结构层不分离只使用js来管理,本来对一般人来说技术学习成本高,项目大了很难管理,有可能带给我们至少两倍以上的成本。
- mv*在后台直出方案
相比来说这个可行性稍大些。例如你只需要在服务器端实现一个mvvm的核心,通过自己实现dom分析器其来解析后端模板中的directives、filter、和事件bind就可以了,但是你要去做这个mvvm核心,而且如果你抛开主流的mvvm框架去做,而且要和前端使用的mvvm框架解析一样,除非如果保证你写的框架足够优秀能,或者被别人接受。当然一个可行的方案是完全根据现有某个主流mvvm框架(例如vuejs、avalon等)的语法来在后端实现一个功能相同的解析插件,因为现有主流的mvvm是不能在node上解析模板的。
这样的话开发的代码就必须是使用mvvm的模板来写了。当然根据mvc框架来实现的原理类似,不过可以灵活些。之前我们做过类似的事情,实现了vuejs的一些基础的directive和filter解析,但是我们业务前端都没有很多人使用vuejs,做同构很像后端上的一厢情愿。最后放弃了。
- 核心的问题
无论是react还是mvvm来实现,其实我们要弄清楚同构需要做的最核心的一件事情是保证一个数据渲染机制(react是virtual to html、mvvm是view模板)在前后端都能正确解析。所以保证这件事情实现了问题是不是就简单了。使用react或mvvm只是说我们可以更好的做前端模块化管理。
大多情况下,我们更多推荐使用mvvm管理vm模板,因为这样就可以模板重用了,但是我们也不得不考虑实现后端mvvm解析模板的代价。但其实在tpl模板层面只是需要一个通用模板,一个能同时支持后台和前端模块化开发的模板。mustache、handlebar、jade、ejs、artTemplate、kissy?似乎有很多选择,但是他们要么都只能在一端 工作,要么功能较简单,都不能直接解决问题。但是这个问题必须得到解决。
三、实现同构要解决的问题
通过分析,所以整理下我们需要解决的几个问题:
- 前后端框架选择。主要包括前端主要框架和后端的web框架的选择。
- 模板渲染机制。实现前后端dom渲染的统一,这个比较重要,刚刚重点讨论过了。
- 构建打包。同一套代码,基于前后端场景打包完成后应该是不一样的,并且对于开发者来说需要有完整的模块化机制、打包体系、不同输出调试目录
- 渲染和直出区分。怎样决定是选择前端渲染的方式还是后台直出的方式
四、最终解决方案
4.1 前后端框架选择
- 后端框架 koa
基于现在的的node端发展趋势,koa相比经典express(其它的一些不主流框架这里就不比较了,大家也可以去了解)有了很多优势,koa自实现的generator特性能用来写没有callback的异步处理,而使用express需要配合bluebrid使用promise特性;koa 不在内核方法中绑定任何中间件,它除了提供了一个轻量优雅的函数库,还包含错误处理机制,并相对于express某些功能的语法糖使代码更简洁,另外generators 实现了的级联中间件,控制权在中间件之间传递很清晰,而后面这些express都不能直接做到。
当然后端除了这些,还需要关注数据库层的问题,一般使用mongodb、mysql或redis都OK,具体根据业务场景决定,而且同时也可以搭配pm2来进行进程管理。
- 前端框架灵活
为了兼容旧的框架,我们还是需要兼容到jquery/zepto,当然也希望能够兼容到mvvm的框架。这方面处理的方案可以比较灵活,而是用后端mvvm直出的方案可能就兼容不了jquery的程序了。所以使用通用模板后你要用react都不会有任何问题。
4.2 模板渲染机制
swigjs是node端的一个优秀简洁的模板引擎,能够根据路径渲染页面、支持面向对象的模板继承、页面复用、支持动态页面、并且使用简洁能快速上手,目前不仅在node端较为通用,相对于jade、ejs优秀,而且在浏览器端也可以很好的运行。但问题是,即使直出后的情况下浏览器上仍然有异步渲染的情况,是不是直出后也需要引入这个swigjs模板库呢?况且就算是浏览器渲染的方式,需要加载这个模板库,而且模板是动态编译的,一定会慢。 对于这些问题,我们希望是前端渲染情况下在构建阶段就能够完成模板编译,当然swig在后端的解决方案是没有任何问题的。
为此我们还是必须一个和swig类似的前端静态编译插件。幸运的是作者已经为你做了这件事情:
npm install fis3-parser-swig
,使用时配置如下(暂不支持浏览器端模板继承):
.match(/.+//(.+)//.+/.tpl$/, { // js 模版一律用 .tpl,可以使用[模块名.tpl]作为模板 isMod: true, rExt: 'js', id: '$1.tpl', moduleId: '$1.tpl', release: '$1.tpl', // 发布的后的文件名,避免和同目录下的 js 冲突 parser: fis.plugin('swig') })
它的优势是配置后可以在构建阶段就将前端的模板文件编译成js文件,就不用在页面打开是加载swig模板引擎做渲染了。这样具体的项目代码请参见最后面的github地址。
4.3 构建打包
构建上还是基于fis3上开发,当然也可以去自由选择gulp或其它的,个人习惯fis3的一些优势,这里就不展开讨论了,如果想使用es6也可以使用下作者之前做的插件 fis3-parser-babel
,具体这里就不展开介绍了。这里先看下整个项目开发目录的设计:
|--asyncWigdet #前端异步模块的存放目录,这里渲染和直出加载的是相同的 |--testMod #前端异步加载的模块 |--mock |--indexPage.json #mock的开发调试数据目录 |--modules #前端扩展库,例如angular或jquery的插件等 |--libs #前端基础库,例如angular、jquery等 |--pages #页面入口模块 |--index |--index.html #模块html |--index.tpl #模块模板 |--main.js #模块对应的js |--index.scss #模块对应的css |--server #服务器端内容 |--lib #服务器端基础库,例如db连接服务基础模块等 |--mock #mock的开发调试数据目录 |--models #数据库model文件 |--pages #前端编译后的view模板 |--routes #koa route |--views #后台若不使用模板,也可以直接使用这里无编译的view |--index.js #koa入口任务脚本 |--pm2.json #pm2管理配置 |--widget #页面内部业务模块 |--search-bar |--index.html #模块html |--index.tpl #模块模板 |--main.js #模块对应的js |--index.scss #模块对应的css |--- ... |--fis-config.js #fis打包配置文件 |--dev #前端渲染页面输出目录,当然这里和可以和编译出的node端模板使用同一个目录 |--index.html #入口页面
目录具体可以参考github地址: koa-fis3-isomorphic
根据这个目录总结一下: - 这里编译出前端页面和后端的pages模板(和views相同功能,但避免和原生views命名冲突)是通过fis3配置的不同任务来实现的,例如调试时可以使用 fis3 release dev
和 fis3 release server
来完成不同构建,暂时使用的一个目录
-
这里只需要维护一套代码目录规范,前后端打包进行两次(当然你可以合并下两个命令变为一条)
- 前端自动打包都是fis3完成的,这里我们不需要去关心
- 前端的基础框架可以任意选择,这里只是用zepto做范例
4.4 渲染和直出区分
受平时项目经验的启发,在koa + swig + fis3 + fis3-parser-swig的条件下,我们配置url地址中的一个特定r参数来判断使用前端渲染还是后台支持,例如: http://localhost:3000/index.html?r=1
使用前端渲染,不带 r=1
则使用后台支持。服务端判断带有 r=1
则转到前端的html服务器上,前端判断 r=1
则调用数据render方法;否则后台直接渲染模板,前端不做数据render,只做事件绑定。这样简单可靠的解决了这个问题。
var getUrlParam = function(name) { var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)"); //构造一个含有目标参数的正则表达式对象 var r = window.location.search.substr(1).match(reg); //匹配目标参数 if (r != null) { return unescape(r[2]); } return null; //返回参数值 }; // ... //前端摸个模块的处理 init: function(data) { // url中带有r,才做前端模板渲染 if (window.r) { this._renderData(data); } this._bindEvent(); }, _renderData: function(data) { this.$el.html(panelTpl({panel3: data})) },
总结一下完整的方案和思路:
在fis3构建和koa后端框架的前提下,使用swig来实现后端模板渲染,使用fis3-parser-swig来做前端页面的模板编译,最终能够在前后端支持统一个模板的解析。
五、 需要注意的问题
同构的优势前面讲到了,这里说下几个需要注意的问题
-
这里的实现只代表个人思路,实现的具体组合方案有很多种,但是思想是一致的,例如使用gulp,或后台解析mvvm模板或mvc模板等。
-
模板
fis3-parser-swig
不支持继承,因为是前端组件化环境编译的,不需要使用模板继承;node端可以任意使用 -
开发调试时文件watcch变化不生效问题。如果开发时fis3 watch文件变化和nodemon wactch文件变化冲突,可能导致前端代码不能自动生效。原因是fis3要进行的文件改变的目录被nodemon进程占用,这是适当重启下server就好了,这里两个watch目的也是为了提升调试的效率。
-
使用同一套构建也是可以的。 不过需要注意的问题,静态编译后前端的模板在数据没返回是会显示模板的语法乱码(这个大家都不陌生了),通常解决思路是先让模板隐藏,数据渲染完后显示。这里建议是两次打包不同的配置生成不同的html:前端渲染方式静态编译时,模块主容器的内容就不要用模板了,动态引入的tpl模板会被编译成js去填充渲染模块的主容器。
-
如果使用后台直出后,需要加载的前端js仍然是和前端渲染时一样的,这里可以去做分离减小直出后加载的js文件大小。现在为了便于处理放在一起
github地址