项目前端打包工具从 NEJ 切换成 webpack

此文已由作者张磊授权网易云社区发布。

欢迎访问网易云社区,了解更多网易技术产品运营经验。


这里不讨论 NEJ 和 webpack 的优劣,仅从技术角度来探寻一下能否实现,以及实现的代价。

前言

上一篇文章 问题有提到 方案1 如何打包的问题,有一种方案就是把打包方案切成 webpack 的。这篇文章就是讲如何实现的。

想法缘由:

  1. NEJ 打包无 watch 模式,导致无法在开发时查看打包后产生的影响,有时候部署到开发环境才发现代码有问题,又是一轮重新部署(部署耗时 7-8min)。

  2. 使用 NEJ 在本地打包,则会对源码产生影响(使用了 ES6 语法,babel 转换后的结果会覆盖源码,导致每次打包后源码都会被覆盖掉,如果源码忘记加入版本控制,基本上还原不回来),同时打包过程耗时长,几乎没人愿意在本地打包。当然也在于本地的开发体验也很好,文件修改了,刷新页面即可。但这里有一个前提,需要浏览器支持最新语法。

  3. NEJ 对静态资源的版本控制不支持 js 文件内的资源,就导致写在 js 代码里面的静态资源路径无法加上合适的时间戳,同时有人会忘记处理该部分代码,导致线上显示有问题。

  4. NEJ 路由按需加载的处理方式,是以 ajax 加载一个 html 去控制 js 文件的加载,这样想完成一个正常的路由按需加载功能,需要发出两个请求,实际上这个功能本应该由一个请求完成即可。在实际使用中,加载的那个 html 文件内容,往往只有一段代码 <textarea name="js" data-src="/pub/xxxx.js?hash"></textarea>。同时 NEJ 会在最终的 html 文件内吐出项目使用的 html 对应的 hash 值,这个解决方案的出发是好的,但是实际上它吐出了所有的 html 对应的 hash 值,无论源码里是否引用该文件,这样就导致这个吐出的结果非常巨大了(目前项目吐出的项有741个,但路由的条目远远低于这个数)。

  5. 由于问题2,导致使用新技术很困难,尤其是基于预编译运行的,基本上寸步难行。

  6. NEJ 应该如何自定义扩展

  7. NEJ 是一种以 html 为主的打包方案,一切由 html 驱动

分析:

方案

问题在上面摆着,解决方案,要么是深入 NEJ 打包工具查看实现,实现出来 watch 模式,要么试着采用新的打包方案,来统一解决该问题。后面采用了 webpack。原因有:

  1. 改造 NEJ 过于困难,需要改造两大点,一个是 watch 模式,一个是 按需加载

  2. webpack 有 watch 模式,有按需加载,支持 amd commonjs es6module 等等,js 文件内的静态资源也有办法设置 hash, js 驱动一切的想法更吸引人

改造成 webpack 打包遇到的问题

首先知道 NEJ 是怎么运行的,由于在开发环境下能正常运行,打包后也能,那么摆在面前的有两条路,查看开发时引用的 nej 的 define.js 文件,查看打包工具 toolkit2。实际上两条路都需要涉猎一番。NEJ 写法和 amd 很类似,这里首先介绍一下 NEJ 和一般的 amd 的不同点:

  1. NEJ 对于 _p _o _f _r 这四个变量没有传入,就可以使用。查看 define.js,原来在每次调用的时候都.apply(window, [{} , {}, function(){return !1}, []]),注意这里的特殊地方, this 指向 window。

  2. NEJ 独特的 platform,通过阅读 toolkit2 的相关代码,发现会被转换成(仅仅演示)

// NEJ.patch('TR<3.0', ['./xx.js'], function (a) {})if (plarform_base.xx < 3.0) {
    define(['base/platform', './xx.js'], function (plarform_base, a) {

    });
}
  1. NEJ 对没有返回的模块会让其返回 _p (即 {})

  2. NEJ 文件间存在环 (这个问题有点坑)

  3. NEJ 有部分特殊的 js 文件,是别的源码中获取,在这里需要作调整

  4. NEJ.define 的写法不太适用于打包,调整成 define

  5. define(['{pro}'] 文件路径前缀 {pro} 这种开头的特殊处理

  6. NEJ 独特的 patch, patch 一般和 platform 配合

  7. 如何改造 NEJ 的按需加载,通过 js 来加载 html 模版,这个 debugger 多次后,也有了解决方案

  8. 后端 html 模版(这里使用的是 ftl)路径的处理

解决问题

  1. 很多问题可以通过正则表达式解决,譬如 问题 1、6,一开始也是通过正则解决,后面考虑到要解决的问题越来越多,难道写越来越复杂的正则?接着想到了 babel,通过使用 babel 插件完美解决了绝大多数的问题。只剩下问题 4、9、5、10。解决问题一共写了两个 babel 插件 babel-plugin-transform-nej(解决 js 问题) babel-plugin-transform-nej-template(解决按需加载问题)

  2. 问题4 是通过 webpack 的 CircularDependencyPlugin 找到所有的环,然后手动解环搞定的。解环步骤:移出顶部 js 上的引用,然后在使用到该模块的方法内部,手动再次引用 require('xxx') 即可

  3. 问题10,通过 beyond compare 软件解决的,因为解决方案横跨周期长,html 文件存在被修改的可能,为了防止合并冲突,所以存放在两个目录,每次比对文件修改解决。同时对于 ftl 使用了 html-webpack-plugin,使用过程中无语法兼容问题。

  4. 问题5,在构建使用过程中发现,一一解决的

  5. 问题9,阅读源码,简单实现了依靠 js 直接解析 html 模版的方法。

  6. NEJ模版 加载使用的是 html-loader 表现良好

  7. regularjs模版 使用的 loader,是重写的, github 上的两款,一款无静态资源的处理,另一款是模仿 vue-loader 写的,均不适合。这里主要是要实现对静态资源自动添加 hash 的功能

带来的新问题

  1. 开发体验(每次启动时间长)

  2. 性能(单独测试)

  3. 对已有的源码产生的影响(一旦应用转换后,没回来的可能)

  4. 是否解决了之前困扰的问题(解决了)

本地实践后的数据

  1. webpack dev server 启动 60s+,每次 rebuild, 3-6s

  2. webpack 生产模式 5min+

  3. 打包后大小对比,基本保持一致,大 2M 左右,按需加载还有优化的可能(目前一个路由一个 js 文件,比如 /a 加载a.js,/a/b 加载 b.js,这时候可能更希望在 /a 时就加载 (a+b).js,同时理论上来说a.js+b.js>=(a+b).js)

  4. 绝大部分的源码均通过 babel 转换完成,特殊修改的仅仅是少数,均已改造完成

  5. 代码运行基本无问题,具体要仔细测试

  6. 切换成 webpack,再想切换回去,是回不去了,因为涉及到核心代码的改动,除非重新设计一些辅助函数

  7. 这是一个较为通用的解决方案,开发期间经过了N个迭代,核心代码依然可以通过 babel 正常转换

实践后的再次优化

对开发分支 fork 出一个 shadow 分支,在 shadow 分支上更改必须修改的核心代码,同时同步每次的开发分支的修改,由于仅修改了核心代码,同步的时候就极少冲突。这样就得到了了两个版本,一个是 nej 打包的,所有的代码均未变动;shadow 是 webpack 打包的,仅修改了核心代码,业务代码的修改放到打包过程中去做。在测试环境测试了几个迭代,使用 webpack 打包的分支表现稳定,未收到相关错误。同时 nej 打包的也可以正常上线,直到 shadow 版本测试充分后,即可启用 babel 转换,将 nej 写法转换成正规的 amd 写法。当然不满意 amd,这时也可以轻易切成其他写法。

未来可以做什么

  1. 理论上可以随意使用最新的 esNext 实现

  2. 基于预编译的 typescript 也可以实施

  3. prebuild 可以选择运行 test、eslint,不通过 test、eslint不允许编译通过, 之前虽然有 test、eslint 开发流程,但是使用上是体现在提交阶段

  4. 完全把控静态资源,需要修改静态资源的引入方式

  5. 优化加载方案

babel 解析效果

// 源码NEJ.define([], function() {
     NEJ.patch('TR>=6.0', [], function () {

     });
     NEJ.patch('TR>=6.0', [], function () {

     });    return;
});
NEJ.define(['./a.js'], function(b) {
     NEJ.patch('4.0<=TR<=5.0', [], function () {

     });    return;
});//解析结果define(['base/platform'], function (plarform_base) {    if (plarform_base._$KERNEL.engine === 'trident' && plarform_base._$KERNEL.release >= '6.0') {
        define([], function () {}.bind(window));
    }    if (plarform_base._$KERNEL.engine === 'trident' && plarform_base._$KERNEL.release >= '6.0') {
        define([], function () {}.bind(window));
    }    return {};
}.bind(window));
define(['base/platform', './a.js'], function (plarform_base, b) {    if (plarform_base._$KERNEL.engine === 'trident' && plarform_base._$KERNEL.release >= '4.0' && plarform_base._$KERNEL.release <= '5.0') {
        define([], function () {}.bind(window));
    }    return {};
}.bind(window));
// 源码
define(['text!./index.css'], () => {    return {};
});
define(['text!./index.html'], () => {    return {};
});
define(['regular!./index.html'], () => {    return {};
});// 解析结果
define(['text!./index.css'], (() => {    return {};
}).bind(window));
define(['html-loader!./index.html'], (() => {    return {};
}).bind(window));
define(['regular-template-loader!./index.html'], (() => {    return {};
}).bind(window));
// 源码a();
NEJ.define(function() {    return;
});
NEJ.define(function(_p){    return _p;
});
NEJ.define([    'require',    '{pro}/index.js'], function(require, factory, _p) {    function test() {}    return;
});
NEJ.define([    'require',    '{pro}index.js'], function(require, factory) {    function test() {}    return;
});
NEJ.define([    '{platform}a.js',    'dependency'], function(require, factory, _p, _o) {    function test() {}    if (true) {        return {};
    }
});// 解析结果a();
define([], function () {    return {};
}.bind(window));
define([], function () {    var _p = {};    return _p;
}.bind(window));
define(['require', './../../../../../../javascript/index.js'], function (require, factory) {    var _p = {};    function test() {}    return _p;
}.bind(window));
define(['require', './../../../../../../javascript/index.js'], function (require, factory) {    function test() {}    return {};
}.bind(window));
define(['require', './../../../../../../javascript/index.js'], function (require, factory) {    function test() {}    return {};
}.bind(window));
define(['./platform/a.patch.js', 'dependency'], function (require, factory) {    var _p = {};    var _o = {};    function test() {}    if (true) {        return {};
    }    return _p;
}.bind(window));
// 源码// 空// 解析结果define([], function () {  return {};
}.bind(window));
// 源码
NEJ.define(() => {    return;
});
NEJ.define((_p) => {    return _p;
});
NEJ.define(() => ({}));
NEJ.define(['a.js'], (a, _p, _o) => ({}));
NEJ.define(['a.js'], (a, _p, _o) => {    return a;
});
NEJ.define(['a.js'], (a, _p, _o) => {    return ;
});// 解析结果
define([], (() => {    return {};
}).bind(window));
define([], (() => {    var _p = {};    return _p;
}).bind(window));
define([], (() => ({})).bind(window));
define(['a.js'], (a => ({})).bind(window));
define(['a.js'], (a => {    var _p = {};    var _o = {};    return a;
}).bind(window));
define(['a.js'], (a => {    var _p = {};    var _o = {};    return _p;
}).bind(window));


免费体验云安全(易盾)内容安全、验证码等服务

更多网易技术、产品、运营经验分享请点击


相关文章:
【推荐】 深情留不住,套路得人心- -聊聊套路那些事儿

posted @ 2018-11-06 10:56  网易数帆  阅读(511)  评论(0编辑  收藏  举报