作者 DBCdouble
專案原始碼demo:點選這裡
一、前言
隨著2018年2月15號webpack4.0.0出來已經有一段時間了,webpack依靠著“零配置”,“最高可提升98%的速度”成功吸粉無數,對於飽受專案打包時間過長的我,無疑是看到了曙光,於是決定開始試水。
二、專案框架與環境
升級前:- Node: v8.11.4
- webpack: ^1.12.9
- babel相關: ^6.x
- react: ^0.14.8(第一次看到react版本的時候,我有點懵,再看一下是真的哈哈?,不禁讚歎最初架構這個專案的人一定是個react大佬,後續會更新文章升級到react16.x)
- react-router: ^2.6.1(後續會更新文章升級到react-router4.x)
- 相關loaders
- 路由元件(頁面): 130個(專案採用SPA應用,目前有130個路由頁面,所以,如果在足夠大的應用上能成功提升構建速度或減小檔案大小,那麼webpack4.0的版本更新才顯得有意義)
升級後:
- Node: v8.11.4
- webpack: ^4.29.5
- babel相關: ^7.x
- react: ^0.14.8
- react-router: ^2.6.1
- 相關loaders(在後面會詳細說明升級的loaders)
- 路由元件(頁面)數量不變
三、背景
隨著專案的不斷迭代,樣式檔案和js檔案的數量越來越多,造成webpack的打包花費的時間越來越多,在開發環境下,經常需要頻繁除錯某一段程式碼ctrl+s會出現長時間等待的現象(等得好煩),日積月累,浪費了太多的時間在等待打包上。生產環境就更不用說了,平均時長100s~120s左右,通常情況情況下,輸入npm run deploy打包之後,我會選擇出去抽根菸。而如果情況是要解決線上的bug,則是分秒必爭,所以優化打包時間勢在必行
四、分析
webpack的構建流程
- 初始化:啟動構建,讀取與合併配置引數,載入 Plugin,例項化 Compiler。
- 編譯:從 Entry 發出,針對每個 Module 序列呼叫對應的 Loader 去翻譯檔案內容,再找到該 Module 依賴的 Module,遞迴地進行編譯處理。
- 輸出:對編譯後的 Module 組合成 Chunk,把 Chunk 轉換成檔案,輸出到本地。
打包分析
webpack2.x生產環境花費時間: 104.145s
webpack2.x開發環境花費時間: 68099ms
雖然能直觀得看到webpack2打包所花費的時間,但我們並不知道webpack打包經過了哪些步驟,在哪個環節花費了大量時間。這裡可以使用speed-measure-webpack-plugin來檢測webpack打包過程中各個部分所花費的時間,在終端輸入以下命令進行安裝。
npm install speed-measure-webpack-plugin -D複製程式碼
安裝完成之後,我們再webpack的配置檔案中配置它
webpack.config.js
參考speed-measure-webpack-plugin的使用方式,檢視這裡
配置好之後,啟動專案(這裡只對開發環境進行分析了)後,如下圖
從上圖可以看出,webpack打包過程中絕大部分時間花在了loader上,也就是webpack構建流程的第二個環節,編譯階段。注意上面還能看到ProgressPlugin花費了28.87s,所以在我們不需要分析webpack打包流程花費的時間後,可在webpack.config.js中註釋掉
五、安裝和配置
1、webpack
先刪除之前的webpack、webpack-cli、webpack-dev-server
npm uninstall webpack webpack-dev-server webpack-cli && npm uninstall webpacl-cli -g複製程式碼
安裝最新版本的webpack、webpack-cli(webpack4把腳手架webpack-cli從webpack中抽離出來的,所以必須安裝webpack-cli)、webpack-dev-server
npm install webpack webpack-dev-server webpack-cli -D複製程式碼
我這裡順便再把webpack的相關外掛更新到最新版本,因為webpack做了很大的改動相對webpakc2,以防之前老版本的外掛不相容webpack4,所以我這邊將專案中的webpack相關外掛的模組都先刪除掉,以便更新的時候分析錯誤
npm uninstall extract-text-webpack-plugin html-webpack-plugin webpack-dev-middleware webpack-hot-middleware複製程式碼
2、升級babel7
刪除之前的babel相關模組
npm uninstall babel-core babel-loader babel-cli babel-eslint babel-plugin-react-transform babel-plugin-transform-runtime babel-preset-es2015 babel-preset-react babel-preset-stage-0 babel-runtime複製程式碼
安裝babel7
npm install @babel/cli @babel/core babel-loader @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators @babel/plugin-proposal-export-default-from @babel/plugin-transform-runtime @babel/preset-env @babel/preset-react複製程式碼
- @babel/cli: 為babel的腳手架工具
- @babel/core: babel-core是作為babel的核心存在,babel的核心api都在這個模組裡面,比如:transform,用於字串轉碼得到AST
- babel-loader: 就是用於編譯JavaScript程式碼
- @babel/preset-env : 官方解釋“用於編寫下一代JavaScript的編譯器”,編譯成瀏覽器認識的JavaScript標準
- @babel/preset-react: 用於編譯react的jsx,開發react應用必備
@babel/plugin-proposal-class-properties: 解析class類的屬性
@babel/plugin-proposal-decorators: 解析裝飾器模式語法,如使用react-redux的@connect
@babel/plugin-proposal-export-default-from: 解析export xxx from 'xxx'語法
.babelrc 檔案為babel的配置檔案(我這邊是直接在webpack.config.js的babel-loader的options下配置的,.babelrc檔案中注意需要轉換為json格式,需要將屬性名加雙引號)
3、安裝ESlint
在專案的根目錄下,安裝eslint
和eslint-loader
npm install eslint eslint-loader -D複製程式碼
.eslintrc
是ESlint的配置檔案,我們需要在專案的根目錄下增加.eslintrc
檔案。
{
"parser": "babel-eslint",
"env": {
"browser": true,
"es6": true,
"node": true
},
"globals" : {
"Action" : false,
"__DEV__" : false,
"__PROD__" : false,
"__DEBUG__" : false,
"__DEBUG_NEW_WINDOW__" : false,
"__BASENAME__" : false
},
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module"
},
"extends": "airbnb",
"rules": {
"semi": [0],
"react/jsx-filename-extension": [0]
}}
複製程式碼
在webpack.config.js
中,為需要檢測的檔案新增eslint-loader
載入器。一般我們是在程式碼編譯前進行檢測。
webpack.config.js
注意,這裡的isEslint是通過npm scripts傳的引數eslint來判斷當前環境是否需要進行程式碼格式檢查,以便開發者有更多選擇,並且eslint-loader必須配置在babel-loader之前,所以這裡用unshift來新增eslint-loader
packack.json
在package.json檔案中新增如下命令
{
"scripts": {
"eslint": "eslint --ext .js --ext .jsx src/"
}
}複製程式碼
到這裡,就可以通過執行 npm run eslint來檢測src檔案下的程式碼格式了
4、安裝打包需要外掛
npm install webpack-merge yargs-parser clean-webpack-plugin progress-bar-webpack-plugin webpack-build-notifier html-webpack-plugin mini-css-extract-plugin add-asset-html-webpack-plugin uglifyjs-webpack-plugin optimize-css-assets-webpack-plugin friendly-errors-webpack-plugin happypack複製程式碼
- webpack-merge: 用於合併webpack的公共配置和環境配置(合併webpack.config.js和webpack.development.js或者webpack.production.js)
- yargs-parser: 用於將我們的npm scripts中的命令列引數轉換成鍵值對的形式如 --mode development會被解析成鍵值對的形式mode: "development",便於在配置檔案中獲取引數
- clean-webpack-plugin: 用於清除本地檔案,在進行生產環境打包的時候,如果不清除dist資料夾,那麼每次打包都會生成不同的js檔案或者css檔案堆積在資料夾中,因為每次打包都會生成不同的hash值導致每次打包生成的檔名與上次打包不一樣不會覆蓋上次打包留下來的檔案
- progress-bar-webpack-plugin: 打包編譯的時候以進度條的形式反饋打包進度
- webpack-build-notifier: 當你打包之後切換到別的頁面的時候,完成時會在本地系統彈出一個提示框告知你打包結果(成功或失敗或警告)
- html-webpack-plugin: 自動生成html,並預設將打包生成的js、css引入到html檔案中
mini-css-extract-plugin: webpack打包樣式檔案中的預設會把樣式檔案程式碼打包到bundle.js中,mini-css-extract-plugin這個外掛可以將樣式檔案從bundle.js抽離出來一個檔案,並且支援chunk css
add-asset-html-webpack-plugin: 從命名可以看出,它的作用是可以將靜態資源css或者js引入到html-webpack-plugin生成的html檔案中
uglifyjs-webpack-plugin: 程式碼醜化,用於js壓縮(可以呼叫系統的執行緒進行多執行緒壓縮,優化webpack的壓縮速度)
optimize-css-assets-webpack-plugin: css壓縮,主要使用 cssnano 壓縮器(webpack4的執行環境內建了cssnano,所以不用安裝)
- friendly-errors-webpack-plugin: 能夠更好在終端看到webapck執行的警告和錯誤
happypack: 多執行緒編譯,加快編譯速度(加快loader的編譯速度),注意,thread-loader不可以和 mini-css-extract-plugin 結合使用
- splitChunks: CommonChunkPlugin 的後世,用於對bundle.js進行chunk切割(webpack的內建外掛)
- DllPlugin: 將模組預先編譯,它會在第一次編譯的時候將配置好的需要預先編譯的模組編譯在快取中,第二次編譯的時候,解析到這些模組就直接使用快取,而不是去編譯這些模組(webpack的內建外掛)
- DllReferencePlugin: 將預先編譯好的模組關聯到當前編譯中,當 webpack 解析到這些模組時,會直接使用預先編譯好的模組(webpack的內建外掛)
- HotModuleReplacementPlugin: 實現區域性熱載入(重新整理),區別與在webpack-dev-server的全域性重新整理(webpack的內建外掛)
5、webpack相關檔案配置
以下檔案直接在你的專案copy就能使用
webpack.config.js
const path = require('path')
const webpack = require('webpack')
const os = require('os')
const merge = require('webpack-merge')
const argv = require('yargs-parser')(process.argv.slice(2))
const mode = argv.mode || 'development'
const interface = argv.interface || 'development'
const isEslint = !!argv.eslint
const isDev = mode === 'development'
const mergeConfig = require(`./config/webpack.${mode}.js`)
const CleanWebpackPlugin = require('clean-webpack-plugin')
const ProgressBarPlugin = require('progress-bar-webpack-plugin')
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin")
const WebpackBuildNotifierPlugin = require('webpack-build-notifier')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin')
const FirendlyErrorePlugin = require('friendly-errors-webpack-plugin')
const HappyPack = require('happypack')
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length })
const smp = new SpeedMeasurePlugin()
const loading = { html:"載入中..."}
const apiConfig = {
development: 'http://xxxxx/a',
production: 'http://xxx/b'
}
let commonConfig = {
module: {
rules: [{
test: /\.js$/,
loaders: ['happypack/loader?id=babel'],
include: path.resolve(__dirname, 'src'),
exclude: /node_modules/
},{
test: /\.css$/,
loaders: [
MiniCssExtractPlugin.loader,
'css-loader'
]
},{
test: /\.less$/,
loaders: [
isDev ? 'style-loader' : MiniCssExtractPlugin.loader,
'css-loader',
{
loader:'less-loader?sourceMap=true',
options:{
javascriptEnabled: true
},
}
// include: path.resolve(__dirname, 'src')
]
},{
test: /\.(png|svg|jpg|gif)$/,
use: [
'url-loader'
]
},{
test: /\.(woff|woff2|eot|ttf|otf|ico)$/,
use: [
'file-loader'
]
},{
test: /\.(csv|tsv)$/,
use: [
'csv-loader'
]
},{
test: /\.xml$/,
use: [
'xml-loader'
]
},{
test: /\.md$/,
use: [
"html-loader",
"markdown-loader"
]
}]
},
//解析 resolve: {
extensions: ['.js', '.jsx'], // 自動解析確定的擴充套件
},
plugins: [
new HappyPack({
id: 'babel',
loaders: [{
loader: 'babel-loader',
options: {
cacheDirectory: true,
presets: ['@babel/preset-env', '@babel/preset-react'],
plugins: [
['@babel/plugin-proposal-decorators', { "legacy": true }],
'@babel/plugin-proposal-class-properties',
'@babel/plugin-proposal-export-default-from',
'@babel/plugin-transform-runtime',
// 'react-hot-loader/babel',
// 'dynamic-import-webpack',
['import',{
libraryName:'antd',
libraryDirectory: 'es',
style:true
}]
]
}
}],
//共享程式池
threadPool: happyThreadPool,
//允許 HappyPack 輸出日誌
verbose: true,
}),
new CleanWebpackPlugin(['dist']),
new ProgressBarPlugin(),
new WebpackBuildNotifierPlugin({
title: "xxx後臺管理系統?",
logo: path.resolve(__dirname, "src/static/favicon.ico"),
suppressSuccess: true
}),
new webpack.DefinePlugin({
'process.env' : {
'NODE_ENV' : JSON.stringify(mode)
},
'NODE_ENV' : JSON.stringify(mode),
'baseUrl': JSON.stringify(apiConfig[interface]),
'__DEV__' : mode === 'development',
'__PROD__' : mode === 'production',
'__TEST__' : mode === 'test',
'__DEBUG__' : mode === 'development' && !argv.no_debug,
'__DEBUG_NEW_WINDOW__' : !!argv.nw,
'__BASENAME__' : JSON.stringify(process.env.BASENAME || '')
}),
new FirendlyErrorePlugin(),
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'public/index.html'),
favicon: path.resolve(__dirname, 'public/favicon.ico'),
filename: 'index.html',
loading
}),
new MiniCssExtractPlugin({
filename: isDev ? 'styles/[name].[hash:4].css' : 'styles/[name].[hash:8].css',
chunkFilename:isDev ? 'styles/[name].[hash:4].css' : 'styles/[name].[hash:8].css'
}),
// 告訴 Webpack 使用了哪些動態連結庫
new webpack.DllReferencePlugin({
// 描述 vendor 動態連結庫的檔案內容
manifest: require('./public/vendor/vendor.manifest.json')
}),
// 該外掛將把給定的 JS 或 CSS 檔案新增到 webpack 配置的檔案中,並將其放入資源列表 html webpack外掛注入到生成的 html 中。
new AddAssetHtmlPlugin([
{
// 要新增到編譯中的檔案的絕對路徑,以及生成的HTML檔案。支援 globby 字串
filepath: require.resolve(path.resolve(__dirname, 'public/vendor/vendor.dll.js')),
// 檔案輸出目錄
outputPath: 'vendor',
// 指令碼或連結標記的公共路徑
publicPath: 'vendor'
}
]),
new webpack.HotModuleReplacementPlugin()
],
devServer: {
host: 'localhost',
port: 8080,
historyApiFallback: true,
overlay: {//當出現編譯器錯誤或警告時,就在網頁上顯示一層黑色的背景層和錯誤資訊
errors: true
},
inline: true,
open: true,
hot: true
},
performance: {
// false | "error" | "warning" // 不顯示效能提示 | 以錯誤形式提示 | 以警告...
hints: false, // 開發環境設定較大防止警告
// 根據入口起點的最大體積,控制webpack何時生成效能提示,整數型別,以位元組為單位
maxEntrypointSize: 50000000,
// 最大單個資源體積,預設250000 (bytes)
maxAssetSize: 30000000
}
}
if (isEslint) {
commonConfig.module.rules.unshift[{
//前置(在執行編譯之前去執行eslint-loader檢查程式碼規範,有報錯就不執行編譯)
enforce: 'pre',
test: /.(js|jsx)$/,
loaders: ['eslint-loader'],
exclude: /node_modules/
}]
}
module.exports = merge(commonConfig, mergeConfig)複製程式碼
注意:這裡在最後匯出配置的時候並沒有使用speed-measure-webpack-plugin,因為會報錯,不知道是不是因為跟happypack不相容的原因。interface用來判斷當前打包js網路請求的地址,isEslint判斷是否需要執行程式碼檢測,isDev用來判斷當前執行環境是development還是production,具體問題看程式碼
webpack.config.dll.js
const path = require('path');
const webpack = require('webpack');
const CleanWebpaclPlugin = require('clean-webpack-plugin');
const FirendlyErrorePlugin = require('friendly-errors-webpack-plugin');
module.exports = {
mode: 'production',
entry: {
// 將 lodash 模組作為入口編譯成動態連結庫
vendor: ['react', 'react-dom', 'react-router', 'react-redux', 'react-router-redux']
},
output: {
// 指定生成檔案所在目錄
// 由於每次打包生產環境時會清空 dist 資料夾,因此這裡我將它們存放在了 public 資料夾下
path: path.resolve(__dirname, 'public/vendor'),
// 指定檔名
filename: '[name].dll.js',
// 存放動態連結庫的全域性變數名稱,例如對應 vendor 來說就是 vendor_dll_lib // 這個名稱需要與 DllPlugin 外掛中的 name 屬性值對應起來
// 之所以在前面 _dll_lib 是為了防止全域性變數衝突
library: '[name]_dll_lib'
},
plugins: [
new CleanWebpaclPlugin(['vendor'], {
root: path.resolve(__dirname, 'public')
}),
new FirendlyErrorePlugin(), // 接入 DllPlugin
new webpack.DllPlugin({
// 描述動態連結庫的 manifest.json 檔案輸出時的檔名稱
// 由於每次打包生產環境時會清空 dist 資料夾,因此這裡我將它們存放在了 public 資料夾下
path: path.join(__dirname, 'public', 'vendor', '[name].manifest.json'),
// 動態連結庫的全域性變數名稱,需要和 output.library 中保持一致
// 該欄位的值也就是輸出的 manifest.json 檔案 中 name 欄位的值
// 例如 vendor.manifest.json 中就有 "name": "vendor_dll_lib" name: '[name]_dll_lib'
})
],
performance: {
// false | "error" | "warning" // 不顯示效能提示 | 以錯誤形式提示 | 以警告...
hints: "warning", // 開發環境設定較大防止警告
// 根據入口起點的最大體積,控制webpack何時生成效能提示,整數型別,以位元組為單位
maxEntrypointSize: 5000000, // 最大單個資源體積,預設250000 (bytes)
maxAssetSize: 3000000
}}複製程式碼
執行 npm run dll
指令之後,可以看到專案中 public 目錄下多出了一個 vendor 的資料夾,可以看到其中包含兩個檔案:
vendor.dll.js
裡面包含react react-dom react-router react-redux react-router-redux
的基礎執行環境,將這些基礎模組打到一個包裡,只要這些包的包的版本沒升級,以後每次打包就不需要再編譯這些模組,提高打包的速率vendor.manifest.json
也是由 DllPlugin 生成出,用於描述動態連結庫檔案中包含哪些模組
config/webpack.development.js
module.exports = {
mode: 'development',
//devtool: 'cheap-module-source-map',
devtool: 'eval',
output: {
filename: 'scripts/[name].bundle.[hash:4].js'
}
}複製程式碼
在開發環境下,我們不做js壓縮和css壓縮,來提高開發環境下除錯儲存頁面打包的速度
config/webpack.production.js
const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); //開啟多核壓縮
const OptmizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const os = require('os');
module.exports = {
mode: 'production',
devtool: 'hidden-source-map',
output: {
filename: 'scripts/[name].bundle.[hash:8].js'
},
optimization: {
splitChunks: {
chunks: 'all', // initial、async和all
minSize: 30000, // 形成一個新程式碼塊最小的體積
maxAsyncRequests: 5, // 按需載入時候最大的並行請求數
maxInitialRequests: 3, // 最大初始化請求數
automaticNameDelimiter: '~', // 打包分割符
name: true,
cacheGroups: {
vendors: { // 專案基本框架等
chunks: 'all',
test: /antd/,
priority: 100,
name: 'vendors',
}
}
},
minimizer: [
new UglifyJsPlugin({
parallel: os.cpus().length,
cache:true,
sourceMap:true,
uglifyOptions: {
compress: {
// 在UglifyJs刪除沒有用到的程式碼時不輸出警告
warnings: false,
// 刪除所有的 `console` 語句,可以相容ie瀏覽器
drop_console: true,
// 內嵌定義了但是隻用到一次的變數
collapse_vars: true,
// 提取出出現多次但是沒有定義成變數去引用的靜態值
reduce_vars: true,
},
output: {
// 最緊湊的輸出
beautify: false,
// 刪除所有的註釋
comments: false,
}
}
}),
new OptmizeCssAssetsWebpackPlugin({
assetNameRegExp: /\.css$/g,
cssProcessor: require('cssnano'),
cssProcessorOptions: {
safe: true,
discardComments: {
removeAll: true
}
}
})
],
}}複製程式碼
在生產環境的配置中,做了js的壓縮和css壓縮,還有從打包的入口檔案中使用splitChunks分離出來了antd來減小bundle.js的大小
public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
</body>
</html>複製程式碼
package.json
六、打包應用
1、執行 npm run dll 生成public/vendor(之後打包不再需要執行此命令,除非vendor中的包版本有變更)
2、執行 npm run start:dev 本地自動開啟webpack-dev-server
3、執行 npm run deploy 打包生產環境
4、打包時長比對分析
使用非同步載入元件的分割程式碼的方式進行體積優化見《Webpack按需載入秒開應用》(最重要的一步)