1. 程式人生 > >專案前端打包工具從 NEJ 切換成 webpack

專案前端打包工具從 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));


免費體驗雲安全(易盾)內容安全、驗證碼等服務

更多網易技術、產品、運營經驗分享請點選


相關文章:
【推薦】 HBase最佳實踐-管好你的作業系統
【推薦】 資料庫路由中介軟體MyCat - 原始碼篇(16)