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

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

这里不讨论 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));

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

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

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

项目前端打包工具从 NEJ 切换成 webpack的更多相关文章

  1. 前端打包工具之fis3的初级使用

    说到打包工具,大家都会想到webpack,我之前也接触过webpack,说实话个人觉得webpack上手容易,但是对于新手来说里面有太多坑,配置文件也不简单.于是乎,我转入了fis3阵营,发现fis3 ...

  2. 前端打包工具——build release介绍

    前言 对于前端开发者来说,资源打包是日常过程中一个必不可少的过程:目前我们大多数时候使用grunt.gulp.webpack这三个工具来完成这个工作:但是有一个特点就是我们没创建一个项目都要对应的去编 ...

  3. grunt,gulp,webpack前端打包工具的特性

    1.http://www.cnblogs.com/lovesong/p/6413546.html (gulp与webpack的区别) 2.http://blog.csdn.net/qq_3231263 ...

  4. Webpack前端打包工具

    一.安装 安装Webpack之前需要安装nodejs,然后用npm安装: $ npm install webpack -g &nsbp;运行以上命令就将Webpack安装到了全局环境中.  但 ...

  5. js模块化方案以及前端打包工具

    图片来自知乎

  6. 前端项目自动化构建工具——Webpack入门教程

    参考资料:https://www.webpackjs.com/(中文文档)   https://www.webpackjs.com/(官方文档) 首先有必要说明一下,本文侧重讲解webpack基本配置 ...

  7. 好用的打包工具webpack

    <什么是webpack> webpack是一个模块打包器,任何静态资源(js.css.图片等)都可以视作模块,然后模块之间也可以相互依赖,通过webpack对模块进行处理后,可以打包成我们 ...

  8. WPF打包工具

    找到一款相当不错的WPF项目的打包工具:advanced installer 工具简单易用,有破/解版,还可以把项目依赖库一起打到一个包中. 用法参考: https://www.cnblogs.com ...

  9. 细说前端自动化打包工具--webpack

    背景 记得2004年的时候,互联网开发就是做网页,那时也没有前端和后端的区分,有时一个网站就是一些纯静态的html,通过链接组织在一起.用过Dreamweaver的都知道,做网页就像用word编辑文档 ...

随机推荐

  1. 利用redis限制单个时间内某个mac地址的访问次数

    一.思路 用户mac地址唯一,可以作为redis中的key,每次请求进来,利用ttl命令,判断redis中key的剩余时间,如果大于零,则利用incr进行+1操作,然后再与总的限制次数作对比. 二.代 ...

  2. 退出telnet 命令

    很多时候 telnet 完就无法退出了,ctrl+c 有时也无法退出 后来找到了正确的命令 ctrl+]  然后在telnet 命令行输入 quit  就可以退出了

  3. 【304】python专题-读取xml文件

    参考:XML DOM 参考手册(w3school) 参考:python专题-读取xml文件 参考:请问用python怎么修改xml的节点值? 1. 读取标签内的文本(Python) 如下的 xml 文 ...

  4. Render Texture

    [Render Texture] Render Textures are special types of Textures that are created and updated at runti ...

  5. 如何了解一个Web项目

    一:学会如何读一个JavaWeb项目源代码 步骤:表结构->web.xml->mvc->db->spring ioc->log->代码 1.先了解项目数据库的表结构 ...

  6. 虚拟化技术:Xen与KVM的对比

    作为开源的虚拟化技术,对比Xen和KVM可以看到,Xen以6个无与伦比的优势领先:更好的可用资源.平台支持.可管理性.实施.动态迁移和性能基准. 可用资源:Xen的问世要比KVM早4年之久(两者分别是 ...

  7. shiro 权限集成Ehcache 配置 学习记录(二)

    1.加入依赖 <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-eh ...

  8. 40 Questions to test your skill in Python for Data Science

    Comes from: https://www.analyticsvidhya.com/blog/2017/05/questions-python-for-data-science/ Python i ...

  9. jvisualvm远程监控服务器tomcat

    1.在 {服务器tomcat路径}/bin/catalina.sh 中,的[# OS specific support.  $var _must_ be set to either true or f ...

  10. [C++] the pointer array & the array's pointer

    int *p[4]------p是一个指针数组,每一个指向一个int型的int (*q)[4]---------q是一个指针,指向int[4]的数组 --> type: int(*)[4] vo ...