一般情况下,前后端合作模式是这样:

  • 前端负责把设计稿实现成静态的页面demo,并且把一些资源部署到CDN;
  • 后端同学将静态demo修改成后端模板(如vmxtpljadeejs等)。
    这里有一个很明显的耦合问题:
    前后端使用的模板不一致了,导致后期维护成本巨大。由于在模板中可能嵌入了很多业务逻辑,当其他同学来接手项目时,往往容易在不完全理解业务逻辑的情况改出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
// xtpl-loader模块部分代码
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
// xtpl-loader返回的数据result字串,此时的require语法是webpack里的require函数
// ...code
buffer = root.includeModule(scope, { params: [ require("./content.xtpl") ] }, buffer,tpl);
// ...code

当我们使用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({
// ...option
template: 'xtpl-compiler-loader!' + filePath + '?data={"title": "title"}',
// ...option
})

express服务器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// webpack编译器
var compiler = webpack(webpackConfig);
// webpack-dev-server中间件
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; // loader输出的内存文件目录
var fileContent = devMiddleware.fileSystem.readFileSync(filePath).toString();
res.set('content-type', 'text/html');
res.send(fileContent);
res.end();
});

经过上面的配置后,webpack能正确解析模板的includeextend等语法,但是有2个槽点:
第一:无法实时监听当子模板的修改;
第二:在template参数传入data数据很奇怪,容易让人困惑。
因为槽点一,放弃了这个方案。

2、从内存中获取入口xtpl文件(已经过html-webpack-plugin注入处理),再通过其映射关系从硬盘中读取其他模板文件。
这种方式在逻辑上完全没有问题(而且不用自己写loader),因为只有一个文件是存储在内存中,而其他文件都是通过映射关系实时从硬盘中读取。

html-webpack-plugin配置:

1
2
3
4
5
6
// 字符串方式读取文件
new htmlWebpackPlugin({
// ...option
template: filePath + '?variable=data',
// ...option
})

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
// webpack编译器
var compiler = webpack(webpackConfig);
// webpack-dev-server中间件
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();
// truePath是指filePath内存文件路径匹配的硬盘路径
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里没有显式的给出loaderloader是模板的加载器,见这里

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的功能。

当前的开发模式采用的是第二种模式,因为实现起来更容易,逻辑也更清晰,功能也完善。

欢迎补充!~~~