本文是我用webpack進行項目構建的實踐心得,場景是這樣的,項目是大型類cms型,技術選型是vue,只支持chrome,有諸多子功能模塊,全部打包在一起的話會有好幾MB,所以最佳方式是進行多入口打包。文章包含我探索的過程以及webpack在使用中的一些技巧,希望能給大家帶來參考價值。
首先,項目打包策略遵循以下幾點原則:
- 選擇合適的打包粒度,生成的單文件大小不要超過500KB
- 充分利用瀏覽器的并發請求,同時保證并發數不超過6
- 盡可能讓瀏覽器命中304,頻繁改動的業務代碼不要與公共代碼打包
- 避免加載太多用不到的代碼,層級較深的頁面進行異步加載
基于以上原則,我選擇的打包策略如下:
- 第三方庫如vue、jquery、bootstrap打包為一個文件
- 公共組件如彈窗、菜單等打包為一個文件
- 工具類、項目通用基類打包為一個文件
- 各個功能模塊打包出自己的入口文件
- 各功能模塊作用一個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黨),一邊 實踐一邊分享吧,還有很多細節的東西沒法細講,我在本系列文章中慢慢道來吧。
文章列表