项目前端打包工具从 NEJ 切换成 webpack
此文已由作者张磊授权网易云社区发布。
欢迎访问网易云社区,了解更多网易技术产品运营经验。
这里不讨论 NEJ 和 webpack 的优劣,仅从技术角度来探寻一下能否实现,以及实现的代价。
前言
上一篇文章 问题有提到 方案1 如何打包的问题,有一种方案就是把打包方案切成 webpack 的。这篇文章就是讲如何实现的。
想法缘由:
- NEJ 打包无 watch 模式,导致无法在开发时查看打包后产生的影响,有时候部署到开发环境才发现代码有问题,又是一轮重新部署(部署耗时 7-8min)。 
- 使用 NEJ 在本地打包,则会对源码产生影响(使用了 ES6 语法,babel 转换后的结果会覆盖源码,导致每次打包后源码都会被覆盖掉,如果源码忘记加入版本控制,基本上还原不回来),同时打包过程耗时长,几乎没人愿意在本地打包。当然也在于本地的开发体验也很好,文件修改了,刷新页面即可。但这里有一个前提,需要浏览器支持最新语法。 
- NEJ 对静态资源的版本控制不支持 js 文件内的资源,就导致写在 js 代码里面的静态资源路径无法加上合适的时间戳,同时有人会忘记处理该部分代码,导致线上显示有问题。 
- NEJ 路由按需加载的处理方式,是以 ajax 加载一个 html 去控制 js 文件的加载,这样想完成一个正常的路由按需加载功能,需要发出两个请求,实际上这个功能本应该由一个请求完成即可。在实际使用中,加载的那个 html 文件内容,往往只有一段代码 <textarea name="js" data-src="/pub/xxxx.js?hash"></textarea>。同时 NEJ 会在最终的 html 文件内吐出项目使用的 html 对应的 hash 值,这个解决方案的出发是好的,但是实际上它吐出了所有的 html 对应的 hash 值,无论源码里是否引用该文件,这样就导致这个吐出的结果非常巨大了(目前项目吐出的项有741个,但路由的条目远远低于这个数)。 
- 由于问题2,导致使用新技术很困难,尤其是基于预编译运行的,基本上寸步难行。 
- NEJ 应该如何自定义扩展 
- NEJ 是一种以 html 为主的打包方案,一切由 html 驱动 
分析:
方案
问题在上面摆着,解决方案,要么是深入 NEJ 打包工具查看实现,实现出来 watch 模式,要么试着采用新的打包方案,来统一解决该问题。后面采用了 webpack。原因有:
- 改造 NEJ 过于困难,需要改造两大点,一个是 watch 模式,一个是 按需加载 
- webpack 有 watch 模式,有按需加载,支持 amd commonjs es6module 等等,js 文件内的静态资源也有办法设置 hash, js 驱动一切的想法更吸引人 
改造成 webpack 打包遇到的问题
首先知道 NEJ 是怎么运行的,由于在开发环境下能正常运行,打包后也能,那么摆在面前的有两条路,查看开发时引用的 nej 的 define.js 文件,查看打包工具 toolkit2。实际上两条路都需要涉猎一番。NEJ 写法和 amd 很类似,这里首先介绍一下 NEJ 和一般的 amd 的不同点:
- NEJ 对于 _p _o _f _r 这四个变量没有传入,就可以使用。查看 define.js,原来在每次调用的时候都.apply(window, [{} , {}, function(){return !1}, []]),注意这里的特殊地方, this 指向 window。 
- 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) {
    });
}- NEJ 对没有返回的模块会让其返回 _p (即 {}) 
- NEJ 文件间存在环 (这个问题有点坑) 
- NEJ 有部分特殊的 js 文件,是别的源码中获取,在这里需要作调整 
- NEJ.define 的写法不太适用于打包,调整成 define 
- define(['{pro}'] 文件路径前缀 {pro} 这种开头的特殊处理 
- NEJ 独特的 patch, patch 一般和 platform 配合 
- 如何改造 NEJ 的按需加载,通过 js 来加载 html 模版,这个 debugger 多次后,也有了解决方案 
- 后端 html 模版(这里使用的是 ftl)路径的处理 
解决问题
- 很多问题可以通过正则表达式解决,譬如 问题 1、6,一开始也是通过正则解决,后面考虑到要解决的问题越来越多,难道写越来越复杂的正则?接着想到了 babel,通过使用 babel 插件完美解决了绝大多数的问题。只剩下问题 4、9、5、10。解决问题一共写了两个 babel 插件 babel-plugin-transform-nej(解决 js 问题) babel-plugin-transform-nej-template(解决按需加载问题) 
- 问题4 是通过 webpack 的 CircularDependencyPlugin 找到所有的环,然后手动解环搞定的。解环步骤:移出顶部 js 上的引用,然后在使用到该模块的方法内部,手动再次引用 require('xxx') 即可 
- 问题10,通过 beyond compare 软件解决的,因为解决方案横跨周期长,html 文件存在被修改的可能,为了防止合并冲突,所以存放在两个目录,每次比对文件修改解决。同时对于 ftl 使用了 html-webpack-plugin,使用过程中无语法兼容问题。 
- 问题5,在构建使用过程中发现,一一解决的 
- 问题9,阅读源码,简单实现了依靠 js 直接解析 html 模版的方法。 
- NEJ模版 加载使用的是 html-loader 表现良好 
- regularjs模版 使用的 loader,是重写的, github 上的两款,一款无静态资源的处理,另一款是模仿 vue-loader 写的,均不适合。这里主要是要实现对静态资源自动添加 hash 的功能 
带来的新问题
- 开发体验(每次启动时间长) 
- 性能(单独测试) 
- 对已有的源码产生的影响(一旦应用转换后,没回来的可能) 
- 是否解决了之前困扰的问题(解决了) 
本地实践后的数据
- webpack dev server 启动 60s+,每次 rebuild, 3-6s 
- webpack 生产模式 5min+ 
- 打包后大小对比,基本保持一致,大 2M 左右,按需加载还有优化的可能(目前一个路由一个 js 文件,比如 /a 加载a.js,/a/b 加载 b.js,这时候可能更希望在 /a 时就加载 (a+b).js,同时理论上来说a.js+b.js>=(a+b).js) 
- 绝大部分的源码均通过 babel 转换完成,特殊修改的仅仅是少数,均已改造完成 
- 代码运行基本无问题,具体要仔细测试 
- 切换成 webpack,再想切换回去,是回不去了,因为涉及到核心代码的改动,除非重新设计一些辅助函数 
- 这是一个较为通用的解决方案,开发期间经过了N个迭代,核心代码依然可以通过 babel 正常转换 
实践后的再次优化
对开发分支 fork 出一个 shadow 分支,在 shadow 分支上更改必须修改的核心代码,同时同步每次的开发分支的修改,由于仅修改了核心代码,同步的时候就极少冲突。这样就得到了了两个版本,一个是 nej 打包的,所有的代码均未变动;shadow 是 webpack 打包的,仅修改了核心代码,业务代码的修改放到打包过程中去做。在测试环境测试了几个迭代,使用 webpack 打包的分支表现稳定,未收到相关错误。同时 nej 打包的也可以正常上线,直到 shadow 版本测试充分后,即可启用 babel 转换,将 nej 写法转换成正规的 amd 写法。当然不满意 amd,这时也可以轻易切成其他写法。
未来可以做什么
- 理论上可以随意使用最新的 esNext 实现 
- 基于预编译的 typescript 也可以实施 
- prebuild 可以选择运行 test、eslint,不通过 test、eslint不允许编译通过, 之前虽然有 test、eslint 开发流程,但是使用上是体现在提交阶段 
- 完全把控静态资源,需要修改静态资源的引入方式 
- 优化加载方案 
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));更多网易技术、产品、运营经验分享请点击。
相关文章:
【推荐】 他们要消失了吗?探访人工智能浪潮下的鉴黄师
 
                    
                     
                    
                 
                    
                
 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号