文章出處

本文是我用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黨),一邊 實踐一邊分享吧,還有很多細節的東西沒法細講,我在本系列文章中慢慢道來吧。


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()