Vue1.0+Webpack1+Gulp專案升級構建方案的踩坑路
最近半年在維護公司的一個管理後臺專案,搭建之初的技術棧比較混亂,構建方案採用了Gulp
中呼叫Webpack
的方式,Gulp
負責處理.html
檔案,Webpack
負責載入.vue
、.js
等。而在這一套構建方案中,主要有這些問題:
http-proxy-middleware
因此,在熟悉這個專案之後,打算對其構建方案進行升級,主要為了解決上述的問題。
1. 原有構建方案描述
原有構建速度
npm run build npm run dev
原有構建結果
./build/development ./build/html ./build/rev
打包程式碼解析
/** * 使用gulp-clean外掛刪除build目錄下的檔案 */ gulp.task('clean', function () { if (!stopClean) { return gulp.src('build/' + directory, { read: false }).pipe(clean()) } }) /** * 使用webpack打包vue與js檔案,在clean之後進行 */ gulp.task('webpack', ['clean'], function (callback) { deCompiler.run(function (err, stats) { if (err) throw new gutil.PluginError('webpack', err) gutil.log('[webpack]', stats.toString({})) callback() }) }) /** * 使用gulp-uglify外掛對js檔案進行醜化,在webpack之後進行 */ gulp.task('minify', ['webpack'], function () { if (environment) { return } else { return gulp.src('build/' + directory + '/*.js').pipe(uglify()) } }) /** * 使用gulp-rev外掛為打包後的檔案增加hash,在minify之後執行 * * gulp-rev會做什麼: * 根據靜態資源內容,生成md5簽名,打包出來的檔名會加上md5簽名,同時生成一個json用來儲存檔名路徑對應關係。 * 替換html裡靜態資源的路徑為帶有md5值的檔案路徑,這樣html才能找到資源路徑。 * 有些人可能會做:靜態伺服器配置靜態資源的過期時間為永不過期。 * 達到什麼效果: * 靜態資源只需請求一次,永久快取,不會發送協商請求304 * 版本更新只會更新修改的靜態資源內容 * 不刪除舊版本的靜態資源,版本回滾的時候只需要更新html,同樣不會增加http請求次數 */ gulp.task('hashJS', ['minify'], function () { var dest = gulp.src(['一串入口檔案...']) .pipe(rev()) // 設定檔案的hash key .pipe(gulp.dest('build/' + directory)) // 將經過管道處理的檔案寫出到目錄 .pipe(rev.manifest({})) // 生成對映hash key的json .pipe(gulp.dest('build/rev')) // 將經過管道處理的檔案寫出到目錄 !environment && gulp.src(['一串入口檔案...']).pipe(clean()) return dest }) /** * 使用gulp-rev-replace外掛為html中引用的js和css替換新的hash * 使用gulp-livereload外掛在所有檔案重新打包完成後區域性更新頁面 */ gulp.task('revReplace', ['hashJS'], function () { return gulp.src(['html/*.html']) .pipe(revReplace({ ... })) // 給html中的js引用提供新的hash .pipe(gulp.dest('build/html')) // 輸出檔案 .pipe(livereload()) // 區域性更新頁面 }) /** * 使用gulp.watch,當應用程式目錄下有任何檔案發生改變,則重新執行一遍打包命令 * gulp.watch:監視檔案,並且可以在檔案發生改動時候做一些事情。 */ gulp.task('watch', ['revReplace'], function () { stopClean = true livereload.listen() gulp.watch('app/**/*', ['clean', 'webpack', 'minify', 'hashJS', 'revReplace']) }) /** * 輸出dev和build的工作流 */ gulp.task('default', ['clean', 'webpack', 'minify', 'hashJS', 'revReplace', 'watch']) // dev gulp.task('build', ['clean', 'webpack', 'minify', 'hashJS', 'revReplace']) // build /** * webpack配置 */ var devCompiler = webpack({ entry: { ... // 一眾入口檔案 vendor: ['vue', 'vue-router', 'lodash', 'echarts'] // 公共模組 }, output: { path: ..., // 所有輸出檔案的目標路徑 publicPath: ..., // 輸出解析檔案的目錄 filename: ..., // 輸出檔案 chunkFilename: ... // 通過非同步請求的檔案 }, // 排除以下內容打包到 bundle,減小檔案大小 external: { jquery: 'jQuery', dialog: 'dialog' }, plugins: [ /** * 通過將公共模組拆出來,最終合成的檔案能夠在最開始的時候載入一次,便存到快取中供後續使用。 * 這個帶來頁面速度上的提升,因為瀏覽器會迅速將公共的程式碼從快取中取出來,而不是每次訪問一個新頁面時,再去載入一個更大的檔案。 */ new webpack.optimize.CommonsChunkPlugin({ name: ['vendor'] }), /** * DefinePlugin 允許建立一個在編譯時可以配置的全域性常量。這可能會對開發模式和生產模式的構建允許不同的行為非常有用。 */ new webpack.DefinePlugin({ __VERSION__: new Date().getTime() }) ], resolve: { root: __dirname, extensions: ['', '.js', '.vue', '.json'], // 解析元件的檔案字尾白名單 alias: { ... } // 配置路徑別名 }, module: { // 各個檔案的loaders loaders: [ { test: /\.vue$/, loader: 'vue-loader' }, { test: /\.css$/, loader: 'style-loader!css-loader' }, { test: /\.jsx$/, loader: 'babel-loader', include: [path.join(__dirname, 'app')], exclude: /core/ }, { test: /\.json$/, loader: 'json' } ] }, vue: { loaders: { js: 'babel-loader' } } }) 複製程式碼
2. 將Gulp
的功能移到Webpack1
上執行
使用html-webpack-plugin
外掛構建專案的主.html
檔案
module.exports = { plugins: [ new HtmlWebpackPlugin({ filename: '...', // 輸出的路徑 template: '...', // 提取源html的路徑 chunks: ['...'], // 需要匯入的模組 inject: true // 是否附加到body底部 }) ] } 複製程式碼
使用webpack.optimize.UglifyJsPlugin
外掛進行JS
壓縮
module.exports = { plugins: [ new webpack.optimize.UglifyJsPlugin({ compress: { warnings: false } }) ] } 複製程式碼
使用webpack-dev-server
模組,提供node
搭建的開發環境
module.exports = { devServer: { clientLogLevel: 'warning', // 輸出日誌的級別,配置為警告級別以上才輸出 inline: true, // 啟動 live reload hot: true, // 允許啟用熱過載 compress: true, // 對所有靜態資源進行gzip壓縮 open: true, // 預設在啟動本地服務時開啟瀏覽器 quiet: true, // 禁止輸出繁雜的構建日誌 host: ..., // 服務啟動的域名 port: ..., // 服務啟動的埠 proxy: { ... }, // http代理配置 /** * 這個配置常用於解決spa應用h5路由模式下將所有404路由匹配回index.html的問題 * 由於生產環境為主頁匹配了一個比較簡單的別名,因此開發環境也照搬後端服務的配置 */ historyApiFallback: { rewrites: [{ from: '/^\/admin/', to: '...' }] } } } 複製程式碼
踩坑
-
[email protected] requires a peer of webpack^@4.0.0 but none is installed.
:這兩個模組版本不相容,回退到webpack-dev-server@2
成功執行。 -
Cannot resolve module 'fsevents' ...
:將全域性的webpack
呼叫改為直接從node_modules/webpack
下直接呼叫,解決了問題,node node_modules/webpack/bin/webpack.js --config webpack.config.js
。 -
Cannot resolve module 'fs' ...
:配置config.node.fs = 'empty'
,為Webpack
提供node
原生模組,使其能載入到這個物件。 -
熱過載只對
.js
和.css
及.vue
中的<style>
內樣式生效,對.vue
檔案中的html
模板及js
內容都不生效,會列印“模組程式碼已發生改變並重新編譯,但熱過載不生效,可能會啟用全域性重新整理的策略”之類的資訊,暫時沒有解決,初步判斷是低版本的vue-hot-reload-api
對這些部分的處理有問題,有大神瞭解原理可以在評論區科普一哈=.=。
3. 從Webpack1
升級到Webpack3
由於Webpack2
與Webpack3
幾乎完全相容,只是涉及到一些增量的功能,因此選擇直接從Webpack1
遷移到Webapck3
,先在專案中安裝Webpack3
,然後根據Webpack2
文件中《從Webpack1
遷移》的章節,對配置項進行更改,參考的文件戳這個:www.html.cn/doc/webpack…
這次升級沒有遇到什麼問題,根據文件配置稍作更改就跑通了。梳理一下目前為止實現的功能:
-
新的
Webpack
構建程式碼已經實現了原有的所有功能,下面列舉新增的功能。 -
使用
webpack-dev-server
作為開發伺服器,實現了儲存時live reload
的功能。 -
使用
http-proxy-middleware
外掛,將請求直接代理到測試服,讓開發環境脫離了本地部署的後端服務,大大降低了開發環境部署的時間成本。 -
新增
friendly-errors-webpack-plugin
,輸出友好的構建日誌,列印幾個重要模組的開發環境地址,配置方面完全參考了vue-cli@2
的預設配置。 -
新增
postcss-loader
,對css新增相容處理,配置方面完全參考了vue-cli@2
的預設配置。 -
使用
webpack.optimize.UglifyJsPlugin
壓縮js程式碼。
嘗試進行構建,輸出構建時間記錄:
-
npm run build
:約135s
-
npm run dev
:初次構建約58s
,持續構建約30s
專案構建時間過長(第一次打包把自己嚇了一跳...),只能繼續尋求構建速度上的優化
4. 在Webpack3
下進行構建速度的優化
使用webpack-jarvis
監測構建效能
webpack-jarvis
是一個圖形化的webpack效能監測工具,它配置簡便,對構建過程的時間佔比、構建結果的詳細記錄都有具體的輸出
// 經過簡單的配置就可以在本地3001埠輸出構建結果記錄 const Jarvis = require('webpack-jarvis') module.exports = { plugins: [ new Jarvis({ watchOnly: false, port: 3001 }) ] } 複製程式碼
使用happypack
先根據網上搜到的文章,做一些簡單的優化,如使用happypack
,這個模組通過多程序模型,來加速程式碼構建,但是使用之後貌似沒有太明顯的結果,構建時間大概減少了幾秒吧...暫時還不太懂這個模組對優化什麼場景的效果比較明顯,之前有看到一篇講解happypack
原理的文章,但還沒細看,有興趣小夥伴可以研究一下,要是能在評論裡簡潔明瞭的給渣渣樓主解釋一下就更好了TUT:taobaofed.org/blog/2016/1…
const HappyPack = require('happypack') const os = require('os') const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length }) module.exports = { plugins: [ new HappyPack({ // happypack的id,在呼叫時需要宣告,若需要編譯其他型別的檔案需要再宣告一個happypack id: 'js', // cacheDirectory:設定後,將盡量在babel編譯時使用快取載入器的結果,避免重新走一遍babel的高昂代價 use: [{ loader: 'babel-loader', cacheDirectory: true }], // 根據cpu的核心數判斷需要拆分多少個程序池 threadPool: happyThreadPool, // 是否輸出編譯過程的日誌 verbose: true }) ] } 複製程式碼
做完這一步後,輸出構建時間記錄:
-
npm run build
:約130s
-
npm run dev
:初次構建約60s
,持續構建約30s
devtool
配置為cheap-module-eval-source-map
devtool
選項啟用cheap-module-eval-source-map
模式:vue-cli@2
預設配置為這種模式,cheap
代表在輸出source-map
時省略列資訊;module
表示在編譯過程中啟用如babel-loader
這樣的預編譯器,使得除錯時可以直接看到未經編譯的原始碼;eval
表示啟用eval
模式編譯,該模式直接使用eval
函式執行編譯後模組的字串,減少了將字串轉化為可執行的程式碼檔案這個步驟,加快了專案開發中重建的速度;source-map
表示輸出原始碼的對映表,使得開發時可以直接把錯誤定位到原始碼,提高開發效率。
做完這一步後,效果並不明顯=.=(相比原來的source-map
),大概減少了幾秒,輸出構建時間記錄:
-
npm run build
:約130s
-
npm run dev
:初次構建約58s
,持續構建約30s
使用html-webpack-plugin-for-multihtml
提升多入口專案重建速度
重建一次竟然需要30s
!各種搜尋找到了html-webpack-plugin
的一條issue
,發現html-webpack-plugin@2
在構建多入口應用時速度確實有明顯變慢的情況,作者給出的解決方案是使用這個模組的一個分支專案(是由作者本人fork
原專案並針對這個問題進行修復的專案)html-webpack-plugin-for-multihtml
,用法與html-webpack-plugin
完全相同,使用之後重建僅需1s
左右。
做完這一步後,輸出構建時間記錄:
-
npm run build
:約130s
-
npm run dev
:初次構建約58s
,持續構建約1s
使用webpack.DllPlugin
提取公共模組
在輸出結果中找到了不少較大的依賴包,如Vue
的核心庫、lodash
、echarts
等等,還有一些不希望被打包的靜態資源,想辦法避免每次都編譯這些內容,提升編譯速度,所以找到了這個外掛。
webpack.DllPlugin
這個外掛是來源於Windows
系統的.dll
檔案(動態連結庫)的用法:首先通過DllPlugin
模組構建出一個包含公共模組的包和一個對映表,再通過DllReferencePlugin
模組通過對映表給每個模組關聯對應的依賴,這樣可以對這些公共模組進行預先打包,以後構建的時候就不需要處理這些模組,減少打包時間。
// webpack.dll.conf.js const webpack = require('webpack') module.exports = { entry: { vendor: [...] }, output: { path: resolve('build/development'), filename: '[name].dll.js', library: '[name]_library' }, plugins: [ new webpack.optimize.UglifyJsPlugin(), new webpack.DllPlugin({ path: resolve('build/development/[name]-manifest.json'), // 生成manifest檔案輸出的位置和檔名稱 name: '[name]-library', // 與output.library是一樣的,對應manifest.json檔案中name欄位的值,防止全域性變數衝突 context: __dirname }) ] } // webpack.base.conf.js const webpack = require('webpack') module.exports = { plugins: [ new webpack.DllReferencePlugin({ context: __dirname, manifest: require('../build/development/vendor-manifest.json') // 讓webpack從對映表獲取使用的依賴 }) ] } 複製程式碼
打包出來之後還需要在html檔案中引入公共庫vendor.dll.js
檔案
<html> <head></head> <body> <div id="app"></div> <script src="/build/development/vendor.dll.js"></script> <!-- 其他JS應該注入到dll的後面,確保能夠引用到公共庫的內容 --> </body> </html> 複製程式碼
做完這一步後,輸出構建時間記錄,發現構建效率有了明顯的提高:
-
npm run dll
:約25s
-
npm run build
:約70s
-
npm run dev
:初次構建約55s
,持續構建約1s
5. 後記
優化到這裡就差不多結束,這次的優化為舊專案提供了新一代spa
專案應有的一些功能,搭建了更現代的本地開發環境。由於本文篇幅有點太長,完整的配置就丟在另一篇文章裡,下面丟連結:
6. Q&A
Q: 為什麼不直接升級到Webpack4
?
A:Webpack4
只支援vue-loader@15
以上版本,而這個版本已經無法解析Vue1
的檔案。