webpack解惑:多入口檔案打包策略

呂大豹發表於2016-10-10

本文是我用webpack進行專案構建的實踐心得,場景是這樣的,專案是大型類cms型,技術選型是vue,只支援chrome,有諸多子功能模組,全部打包在一起的話會有好幾MB,所以最佳方式是進行多入口打包。文章包含我探索的過程以及webpack在使用中的一些技巧,希望能給大家帶來參考價值。

首先,專案打包策略遵循以下幾點原則:

  1. 選擇合適的打包粒度,生成的單檔案大小不要超過500KB
  2. 充分利用瀏覽器的併發請求,同時保證併發數不超過6
  3. 儘可能讓瀏覽器命中304,頻繁改動的業務程式碼不要與公共程式碼打包
  4. 避免載入太多用不到的程式碼,層級較深的頁面進行非同步載入

基於以上原則,我選擇的打包策略如下:

  1. 第三方庫如vue、jquery、bootstrap打包為一個檔案
  2. 公共元件如彈窗、選單等打包為一個檔案
  3. 工具類、專案通用基類打包為一個檔案
  4. 各個功能模組打包出自己的入口檔案
  5. 各功能模組作用一個SPA,子頁面進行非同步載入

 

各入口檔案的打包

由於專案不適宜整體作為一個SPA,所以各子功能都有一個自己的入口檔案,我的原始碼目錄結構如下:

apps目錄下放置各個子功能,如question和paper,下面是各自的子頁面。components目錄放置公共元件,這個後面再說。

由於功能模組是隨時會增加的,我不能在webpack的entry中寫死這些入口檔案,所以用了一個叫做glob的模組,它能夠用萬用字元來取到所有的檔案,就像我們用gulp那樣。動態獲取子功能入口檔案的程式碼如下:

/**
* 動態查詢所有入口檔案
*/
var files = glob.sync('./public/src/apps/*/index.js');
var newEntries = {};

files.forEach(function(f){
   var name = /.*\/(apps\/.*?\/index)\.js/.exec(f)[1];//得到apps/question/index這樣的檔名
   newEntries[name] = f;
});

config.entry = Object.assign({}, config.entry, newEntries);

webpack打包後的目錄是很亂的,如果你入口檔案的名字取為question,那麼會在dist目錄下直接生成一個question.xxxxx.js的檔案。但是如果把名字取為apps/question/index這樣的,則會生成對應的目錄結構。我是比較喜歡構建後的目錄也有清晰的結構的,可能是習慣gulp的後遺症吧。這樣也便於我們在前端路由中進行統一操作。也是一個小技巧吧,我生成的各入口檔案的目錄如下:

 

第三方庫的打包

專案中用到了一些第三方庫,如vue、vue-router、jquery、boostrap等。這些庫我們基本上是不會改動原始碼的,並且專案初期就基本確定了,不會再新增。所以把它們打包在一起。當然這個也是要考慮大小不超過500KB的,如果是用到了像ueditor這樣的大型工具庫,還是要單獨打包的。

配置檔案的寫法是很簡單的,在entry中配一個名為vendor的就好,比如:

entry: {
    vendor: ['vue', 'vue-router', './public/vendor/jquery/jquery']
},

不管是用npm安裝的還是自己放在專案目錄中的庫都是可以的,只要路徑寫對就行。

為了把第三方庫拆分出來(用<script>標籤單獨載入),我們還需要用webpack的CommonsChunkPlugin外掛來把它提取一下,這樣他就不會與業務程式碼打包到一起了。程式碼:

new webpack.optimize.CommonsChunkPlugin('vendor');

 

公共元件的打包

這部分程式碼的處理我是糾結了好久的,因為webpack的打包思想是以模組的依賴樹為標準來進行分析的,如果a模組使用了loading元件,那麼loading元件就會被打包進a模組,除非我們在程式碼中用require.ensure或者AMD式的require加回撥,顯式宣告該元件非同步載入,這樣loading元件會被單獨打包成一個chunk檔案。

以上兩者都不是我想要的,理由參見文章開頭的打包原則,把所有公共元件打包在一起是一個自然合理的選擇,但這又與webpack的精神相悖。

一開始我想到了一招曲線救國,就是在components目錄下建一個main.js檔案,該檔案引用所有的元件,這樣打包main.js的時候所有元件都會被打包進來,main.js的程式碼如下:

import loading from './loading.vue';
import topnav from './topnav.vue';
import centernav from './centernav.vue';

export {loading, topnav, centernav}

有點像sass的main檔案的感覺。使用的時候這樣寫:

let components = require('./components/main');

export default {
    components: {
        loading: (resolve) =>{
            require(['./components/main'],function(components){
                resolve(components.loading);
            })
        }
    }
}

缺點是也得寫成非同步載入的,否則main.js還是會被打包進業務程式碼。

不過後來我又一想,既然vendor可以,為什麼元件不可以用同樣的方式處理呢?於是乎找到了最佳方法。 同樣先用glob動態找到所有的components,然後寫進entry,最後再用CommonsChunkPlugin外掛剝離出來。程式碼如下:

/*動態查詢所有components*/
var comps = glob.sync('./public/src/components/*.vue');
var compsEntry = {components: comps};
config.entry = Object.assign({}, config.entry, compsEntry);

要注意CommonsChunkPlugin是不可以new多個的,要剝離多個需要傳陣列進去,寫法如下:

new webpack.optimize.CommonsChunkPlugin({
    names: ['vendor', 'components']
})

如此一來,components就和vendor一樣可以用<script>標籤引入頁面了,使用的時候就可以隨便引入了,不會再被重複打包進業務程式碼。如:

import loading from './components/loading';
import topnav from './components/topnav';

 

把這些檔案塞進入口頁面

之前說過我們的子功能模組有各自的頁面,所以我們需要把這些檔案都給引入進這些頁面,webpack的HtmlWebpackPlugin可以做這件事情,我們在動態查詢入口檔案的時候順便把它做了就行了,程式碼如下:

/**
 * 動態查詢所有入口檔案
 */
var files = glob.sync('./public/src/apps/*/index.js');
var newEntries = {};

files.forEach(function(f){
    var name = /.*\/(apps\/.*?\/index)\.js/.exec(f)[1]; //得到apps/question/index 這樣的檔名
    newEntries[name] = f;

    var plug =  new HtmlWebpackPlugin({
        filename: path.resolve(__dirname, '../public/dist/'+ name +'.html'),
        chunks: ['vendor', name, 'components'],
        template: path.resolve(__dirname, '../public/src/index.html'),
        inject: true
    });
    config.plugins.push(plug);
});

 

子頁面的非同步載入

每個功能模組是作為一個SPA應用來處理的,這就意味著我們會根據前端路由來動態載入相應子頁面,使用官方的vue-router是很容易實現的,比如我們在question/index.js中可以如下寫:

router.map({
    '/list': {
        component: (resolve) => {
            require(['./list.vue'], resolve);
        }
    },
    '/edit': {
        component: (resolve) => {
            require(['./edit.vue'], resolve);
        }
    }
});

在webpack的配置檔案中就無需再寫什麼了,它會自動打包出對應的chunk檔案,此時我的dist目錄就長這樣了:

有一點讓我疑惑的是,非同步載入的chunk檔案貌似無法輸出檔名稱,儘管我在output引數中這麼配置:chunkFilename: '[name].[chunkhash].js',[name]那裡輸出的還是id,可能和webpack處理非同步chunk的機制有關吧,猜測的。不過也無所謂的,反正能夠正確載入,就是名字難看點。

--------更新於2016.10.11-------

為非同步chunk命名的方法我找到了,需要兩步。首先output中還是應該這麼配置:chunkFilename: '[name].[chunkhash].js'。然後,利用require.ensure的第三個引數,可以為chunk指定名字。上面的程式碼修改為如下:

router.map({
    '/list': {
        component: (resolve) => {
            // require(['./list.vue'], resolve);
            require.ensure([], function(){
                resolve(require('./list.vue'));
            }, 'list');
        }
    },
    '/edit': {
        component: (resolve) => {
            //require(['./edit.vue'], resolve);
            require.ensure([], function(){
                resolve(require('./edit.vue'));
            }, 'edit');
        }
    }
});

這樣list和edit這兩個元件生成的chunk就有名字了,如下:

 

我個人還是偏好生成的chunk能帶上名字,這樣可讀性好一些,便於除錯和儘快發現錯誤。 

 


以上就是一個大概的架子了,由於我也是剛剛開始探索webpack(之前gulp黨),一邊 實踐一邊分享吧,還有很多細節的東西沒法細講,我在本系列文章中慢慢道來吧。

相關文章