一般情况下,前后端合作模式是这样:
- 前端负责把设计稿实现成静态的页面demo,并且把一些资源部署到CDN;
- 后端同学将静态demo修改成后端模板(如
vm
、xtpl
、jade
、ejs
等)。
这里有一个很明显的耦合问题:
前后端使用的模板不一致了,导致后期维护成本巨大。由于在模板中可能嵌入了很多业务逻辑,当其他同学来接手项目时,往往容易在不完全理解业务逻辑的情况改出bug
。这个在我接触的项目中有活生生的例子。
为了提高项目后期可维护性,应该降低这种耦合性,后端同学应该直接使用模板,而不需要通过修改html
文件来创建模板。
当然这样又会有两个问题:
这两个问题对于前期来说是比较痛苦的,但是随着这种模式的推进,后面会变得越来越容易实现。
当然你可能会说,前后端分离的项目就不存在这种情况。简单的前后端分离项目,因为view层
渲染还是由前端来做,并不会出现这种情况。但对于复杂的前后端分离项目,所有引用资源都是在CDN
上的,CDN
部署是页面完成时直接部署,此外如果中间层数据量大的话(进而导致工作量大),静态页面也可能不是同一人来写。这样问题又回到了一般的前后端合作模式。
于是,使用前后端模板渲染保持一致就是一种解决方案了,这里利用webpack
来搭建这样的开发模式。
目前想到的方案大致分为两种(使用xtpl
模板引擎):
- 结合
html-webpack-plugin
插件的template配置参数,编写符合需求的xtpl模板loader;
- 从内存中获取入口
xtpl
文件(已经过html-webpack-plugin
处理),再通过其映射关系从硬盘中读取其他模板文件。
对于第一种方案,你也许会有疑问,template
参数不是支持loader
加载吗?先来看看这种情况:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| // ./extend.xtpl <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div>extend</div> {{{block ('body')}}} </body> </html> // ./index.xtpl {{extend ('./extend.xtpl')}} {{#block ('body')}} title: {{title}} {{test}}jlk {{include ('./content.xtpl')}} {{/block}} // ./content.xtpl <div>content</div>
|
当html-webpack-plugin
载入index.xtpl
文件时,xtpl-loader
并没有解析./content.xtpl
这个文件,而是返回一个含有require
语法的function
模板函数:
1 2 3 4 5 6 7 8 9 10 11 12 13
| module.exports = function(source) { var webpackRemainingChain = loaderUtils.getRemainingRequest(this).split('!'); var filename = webpackRemainingChain[webpackRemainingChain.length - 1]; var result = xtemplateCompiler.compileToStr({ name: filename, isModule: true, withSuffix:'xtpl', content: source, }); this.cacheable(); this.callback(null, `module.exports = ${result}`); };
|
1 2 3 4
| buffer = root.includeModule(scope, { params: [ require("./content.xtpl") ] }, buffer,tpl);
|
当我们使用express
服务器渲染这个模板时,并不能正确解析./contetn.xtpl
这个文件。当然,正是这个问题我们才有了方案一。
现在让我们来开始尝试
1、结合html-webpack-plugin插件的template配置参数,编写符合需求的xtpl模板loader
这里又分为2中方式:
- 写loader,解析include、extend等模板拼接语法
这个工作量和困难度都很大,并且对于不同模板还需要不同实现,所以直接舍弃。
- 在loader里将所有数据与模板渲染完成,由loader直接返回一个渲染好的字符串,再由express服务器把字符串直接输出到浏览器。
xtpl-compiler-loader
自己编写的laoder
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| var xtpl = require('xtpl'); var loaderUtils = require('loader-utils'); module.exports = function (source) { this.cacheable(); var callback = this.async(); var webpackRemainingChain = loaderUtils.getRemainingRequest(this).split('!'); var temp = webpackRemainingChain[webpackRemainingChain.length - 1].split('?'); var filename = temp[0]; var data = JSON.parse(temp[1].split('=')[1]); xtpl.renderFile(filename, {title: 'fdsa'}, function(e, content) { console.log(e,content); callback(null, content); }); };
|
html-webpack-plugin
配置:
1 2 3 4 5
| new htmlWebpackPlugin({ template: 'xtpl-compiler-loader!' + filePath + '?data={"title": "title"}', })
|
express
服务器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| var compiler = webpack(webpackConfig); var devMiddleware = WebpackDevMiddleware(compiler, { publicPath: webpackConfig.output.publicPath, stats: { colors: true, chunks: false } }); var app = express(); app.use(devMiddleware); app.use(webpackHotMiddleware(compiler)); app.get('/', function (req, res) { var filePath = compiler.outputPath; var fileContent = devMiddleware.fileSystem.readFileSync(filePath).toString(); res.set('content-type', 'text/html'); res.send(fileContent); res.end(); });
|
经过上面的配置后,webpack能正确解析模板的include
、extend
等语法,但是有2个槽点:
第一:无法实时监听当子模板的修改;
第二:在template
参数传入data数据很奇怪,容易让人困惑。
因为槽点一,放弃了这个方案。
2、从内存中获取入口xtpl文件
(已经过html-webpack-plugin
注入处理),再通过其映射关系从硬盘中读取其他模板文件。
这种方式在逻辑上完全没有问题(而且不用自己写loader),因为只有一个文件是存储在内存中,而其他文件都是通过映射关系实时从硬盘中读取。
html-webpack-plugin
配置:
1 2 3 4 5 6
| new htmlWebpackPlugin({ template: filePath + '?variable=data', })
|
express
服务器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| var compiler = webpack(webpackConfig); var devMiddleware = WebpackDevMiddleware(compiler, { publicPath: webpackConfig.output.publicPath, stats: { colors: true, chunks: false } }); app.get('/', function (req, res) { var filePath = compiler.outputPath; var fileContent = devMiddleware.fileSystem.readFileSync(filePath).toString(); var result = new xtemplate(fileContent, { name: truePath, loader: xtpl.loader, extname: 'xtpl', encoding: 'utf-8' }).render(fse.readJsonSync(config.jsonUrl)); res.set('content-type', 'text/html'); res.send(result); res.end(); });
|
存在问题:
因为直接拿内存中的字符串进行模板渲染,如果模板中含有include、extend等语法,这些包含与继承的相对路径并没有一个路径基准,所以必须手动指定一个路径基准,在vm
模板引擎中与有参数可以设置:
1 2
| var Engine = require('velocity').Engine; new Engine({template: content, root: rootPath);
|
但在使用xtpl
模板存在的问题:
第一:xtpl
提供的配置参数中并没有类似root
参数;
第二:xtpl.loader
函数并没有被模块xtpl
暴露出来,点这里看源码。
最后阅读了xtpl
源码,从代码调用栈中发现,子模板路径都是相对于父模板路径,而且指定name
参数就是父模板的路径。这里解决了第一个问题。
第二个问题是由于XTemplate
里没有显式的给出loader
,loader
是模板的加载器,见这里
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| var loader = { load: function (tpl, callback) { var template = tpl.root; var path = tpl.name; var rootConfig = template.config; var extname = rootConfig.extname; var pathExtName; if (endsWith(path, extname)) { pathExtName = extname; } else { pathExtName = Path.extname(path); if (!pathExtName) { pathExtName = extname; path += pathExtName; } } if (pathExtName !== extname) { readFile(path, rootConfig, callback); } else { getTplFn(template, path, rootConfig, callback); } } };
|
因为模块中没有暴露出loader
接口,所以要解决第二个问题只能自己写一个npm
包来实现loader
的功能。
当前的开发模式采用的是第二种模式,因为实现起来更容易,逻辑也更清晰,功能也完善。
欢迎补充!~~~