一次失败的HTML模块化尝试

前端最基本的工作就是写页面模板,在完成webpackTpl的环境搭建之后逐渐尝试使用HtmlWebpackPlugin来生成模板。在多页面的项目中,往往存在相同的布局,因此开始了一次HTML模块化的尝试。PS:结局惨不忍睹。

<!--more-->

在伟大的DRY原则指导下,为了实现HTML模板的复用,必须实现某种类似于includeimport的机制,将页面结构拆分成多个独立的文件组件(比如header, footer等),然后按需引入即可。由于最初就是使用HtmlWebpackPlugin来开发模板的,因此就从它开始入手。

(再次提醒,这次的内容真的是瞎折腾,大家看看就好了,不要被我带到坑里面去了~真是一把心酸泪。)

1. HtmlWebpackPlugin

在前面的webpack折腾记(二)中,总结出了使用HtmlWebpackPlugin的几个好处:

  • 配置通用的模板变量及资源路径
  • 支持循环和条件分支,方便模拟数据
  • 样式脚本内联

在编写单页面的时候,由于只需要一个模板,因此上面的功能基本上就能满足开发需求了。

不幸的是,HtmlWebpackPlugin虽然是ejs的语法,但是,它并不支持ejs的<% include common/header %>这样的语法,在多页面的情况下肯定是不行的。查了半天资料,网上推荐的html-loaderejs-loader也不能解决(可能是我的打开方式不对)。

转念一想,既然HtmlWebpackPlugin不支持,那我们可以单独编写ejs模板,然后再将模板输入给HtmlWebpackPlugin进行打包不就可以了吗?

2. ejs

既然是完全采用ejs来进行开发,那么“将结构拆分成组件然后按需引入”的目的就能够轻松使用include完成。

2.1. 模板传参

有时候需要为模板传入参数,然后渲染出对应的测试数据,文档中给的示例是

<ul>
  <% users.forEach(function(user){ %>
    <%- include('user/show', {user: user}) %>
  <% }); %>
</ul>

文档中还指出<% include user/show %>这样的方式已经停止更新了,但是我还是比较喜欢这种写法,因此还有一个折衷的解决办法:在ejs配置中传入一个全局对象,然后通过对象上的属性来进行参数传递

// index.ejs
<? var opt.msg = "heallo "?>
<? include common/header ?>

// header.ejs
<h1><? opt.msg ?></h1>

(PS:是不是很无语~)

2.2. 热加载

要在开发时使用ejs并实现热加载,我采用的解决办法是gulp + gulp-ejs,即单独开启一个gulp任务用来监听开发模板的变化,并将编译后的模板输入到HtmlWebpackPlugin的模板目录,最后由webpack实现热更新。

gulp.task('ejs', function(){
    let { SRC_DIR } = config;
    gulp.watch(`${SRC_DIR}/ejs/*.ejs`, function (e) {
        let file = e.path;
        gulp.src(file)
            .pipe(ejs({},{
                delimiter: "?"
            }).on("error", function (e) {
                console.log(e)
            }))
            .pipe(gulp.dest(`${SRC_DIR}/tpl`));
    })
});

这里就有点绕了,由于之前的webpackTpl搭建的环境有一个任务是输出生成环境后台需要的PHP模板,因此现在我们的模板生成路径就变成了:

ejs > htmlplugin template > html/php

这里强行把模板拆分然后再整合在一起,就感觉像是吃饱了撑的(/捂眼笑)。

2.3. 界定符

由于ejs的默认界定符是<% %>,这跟HtmlWebpackPlugin的一模一样。如果不做任何操作,则HtmlWebpackPlugin得到的就是纯粹的HTML文本,为了使用HtmlWebpackPlugin的某些特性(向webpackTpl兼容),我的处理是直接修改了gulp-ejs的界定符参数配置,然而文档上面并没有说明如何进行配置,查看源码可以发现

module.exports = function (data, options, settings) {
      // ...
    file.contents = new Buffer(
        ejs.render(file.contents.toString(), data, options)
    )
}

data参数为传入模板的变量,options为ejs相关的配置,所以直接将界定符设置为<? ?>形式。(PS:这完全不是在倒腾HTML模块了...)

.pipe(ejs({},{
    delimiter: "?"
 })

万万没想到漏了一点:php采用的也是<?php ?>界定符,也就是说如果想要在模板中直接输出原始的php代码,编译肯定会报错的(为什么要在ejs模板上写php代码呢?因为我们后台是php~~),然后我的解决办法是使用函数输出php代码字符串

pipe(ejs({
    opt: {},
    // 输出php字符串变量
    php(str){
        return '<?php ' + str + ' ?>';
    },
},{
    delimiter: "?"
})

然后在模板中调用<?- php("echo 1"); ?>即可。现在博客写到这里,我真想知道当时的我到底是犯了什么浑~~弱智啊!!

2.4. 动态引入

本以为搞完上面的事情,整个活就可以告一段落了:

  • 采用原生的ejs开发模板,畅快地使用模板变了,循环分支,组件引入等功能
  • 采用gulp-ejs动态编译模板,配合webpack实现热加载

后来突然想到,这特么输出的还是一个完整的html页面啊,最后切换环境输出给后台的时候,还是需要把模板的组件提取出来~这完全是重复的工作嘛(现在除了前端页面,后台的路由和模板数据也得我负责)。所以就想着干脆把ejs的include替换成动态的加载吧:

  • 在开发时使用正常的ejs引入
  • 在打包时直接输出php框架的组件引入

为了实现这个目的,又写了一个全局的ejs方法

load: function(tplPath){
    if (isDev){
        if (!~file.indexOf("*")) {
            tplPath += ".ejs";
            let absPath = path.resolve(path.dirname(file), tplPath);
            return ejs.render(`<% include ${tplPath} %>`, {}, {filename: `${path.dirname(absPath)}`});
        }else {

        }
    }else {
        tplPath += ".php";
        return `<?php $this->load->view("${tplPath}"); ?>`
    }
},

上面代码的具体细节就不深究了(反正现在我已经放弃了),主要就是为了实现上面的在不同编译环境的模板输出。在模板源文件上调用<?- load("commomn/head")?>即可

3. 反思

上面的工作大概折腾了半天时间,整个流程总算是能跑起来了。当我把之前十来个模板页面上的公共组件提取出来之后,脑海中出现了一个声音:卧槽,有病吧?

静下来想一想,最初的目的只是为了在模板中加载一个外部的组件,谁知道却围绕着ejs折腾了这么多无关的事情,简直就是本末倒置啊。先理一理:

  • 为了实现模块,复用公共组件,采用了ejs进行开发
  • 为了打包,使用了webpack
  • 为了区分开发环境和线上环境,为整个流程进行了一系列的适配处理
  • 最后输出了后台需要的PHP模板文件

突然发现,做的这些事,无非是将ejs的组件编译成了PHP的组件(还得费时费力的去适配和调整模板),后台语言天生就支持文件导入,像Laravel这样的框架提供的blade模板更是强大,既然最后的生产目的都是为了对接数据,一开始直接用PHP写模板不就得了吗?为什么要钻进“通过webpack然后再转成PHP的模板的死胡同“呢?瞬间醒悟。

另外,维护的方面来讲,以前只需要调试本身的模板即可,现在呢?需要深入好几层,才能发现最初的源代码,虽然开发看起来高大上了(开发效率有没有提高我就不确定了),但是无形之中却增加了维护的难度,后面如果有同事不明白你的工作流程,直接在编译的模板上面进行更改,肯定会骂死你的。

现在的前端环境发展十分迅速,像我这种半路杀进来的人感觉也十分明显,各种工具,各种框架层出不穷,圈子也比较浮躁(感觉我自己也受到了感染,这次HTML模块化的尝试就是一个例子)。

然而,术业有专攻,传统也并不是一无是处,找到适合业务的技术框架和开发环境才是最主要的,脱离生产环境高谈技术的都是耍流氓。另外,一定不要浮躁!切记。

PS:貌似把blade的模板用JS实现一遍也不错哦,这样模板就可以完全通用了~打住打住....