webpack解惑:多入口文件打包策略

本文是我用webpack進行項目構建的實踐心得,場景是這樣的,項目是大型類cms型,技術選型是vue,只支持chrome,有諸多子功能模塊,所有打包在一塊兒的話會有好幾MB,因此最佳方式是進行多入口打包。文章包含我探索的過程以及webpack在使用中的一些技巧,但願能給你們帶來參考價值。html

首先,項目打包策略遵循如下幾點原則:前端

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

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

  1. 第三方庫如vue、jquery、bootstrap打包爲一個文件
  2. 公共組件如彈窗、菜單等打包爲一個文件
  3. 工具類、項目通用基類打包爲一個文件
  4. 各個功能模塊打包出本身的入口文件
  5. 各功能模塊做用一個SPA,子頁面進行異步加載

 

各入口文件的打包

因爲項目不適宜總體做爲一個SPA,因此各子功能都有一個本身的入口文件,個人源碼目錄結構以下:jquery

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

因爲功能模塊是隨時會增長的,我不能在webpack的entry中寫死這些入口文件,因此用了一個叫作glob的模塊,它可以用通配符來取到全部的文件,就像咱們用gulp那樣。動態獲取子功能入口文件的代碼以下:web

/**
* 動態查找全部入口文件
*/
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-router

 

第三方庫的打包

項目中用到了一些第三方庫,如vue、vue-router、jquery、boostrap等。這些庫咱們基本上是不會改動源代碼的,而且項目初期就基本肯定了,不會再添加。因此把它們打包在一塊兒。固然這個也是要考慮大小不超過500KB的,若是是用到了像ueditor這樣的大型工具庫,仍是要單獨打包的。chrome

配置文件的寫法是很簡單的,在entry中配一個名爲vendor的就好,好比:npm

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

不論是用npm安裝的仍是本身放在項目目錄中的庫都是能夠的,只要路徑寫對就行。gulp

爲了把第三方庫拆分出來(用<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黨),一邊 實踐一邊分享吧,還有不少細節的東西無法細講,我在本系列文章中慢慢道來吧。

相關文章
相關標籤/搜索