本文是我用webpack進行項目構建的實踐心得,場景是這樣的,項目是大型類cms型,技術選型是vue,只支持chrome,有諸多子功能模塊,所有打包在一塊兒的話會有好幾MB,因此最佳方式是進行多入口打包。文章包含我探索的過程以及webpack在使用中的一些技巧,但願能給你們帶來參考價值。html
首先,項目打包策略遵循如下幾點原則:前端
基於以上原則,我選擇的打包策略以下:vue
因爲項目不適宜總體做爲一個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黨),一邊 實踐一邊分享吧,還有不少細節的東西無法細講,我在本系列文章中慢慢道來吧。