做者 DBCdouble
項目源碼demo:點擊這裏javascript
隨着2018年2月15號webpack4.0.0出來已經有一段時間了,webpack依靠着「零配置」,「最高可提高98%的速度」成功吸粉無數,對於飽受項目打包時間過長的我,無疑是看到了曙光,因而決定開始試水。
css
升級後:html
隨着項目的不斷迭代,樣式文件和js文件的數量愈來愈多,形成webpack的打包花費的時間愈來愈多,在開發環境下,常常須要頻繁調試某一段代碼ctrl+s會出現長時間等待的現象(等得好煩),日積月累,浪費了太多的時間在等待打包上。生產環境就更不用說了,平均時長100s~120s左右,一般狀況狀況下,輸入npm run deploy打包以後,我會選擇出去抽根菸。而若是狀況是要解決線上的bug,則是分秒必爭,因此優化打包時間勢在必行java
webpack2.x生產環境花費時間: 104.145snode
webpack2.x開發環境花費時間: 68099msreact
雖然能直觀得看到webpack2打包所花費的時間,但咱們並不知道webpack打包通過了哪些步驟,在哪一個環節花費了大量時間。這裏可使用speed-measure-webpack-plugin來檢測webpack打包過程當中各個部分所花費的時間,在終端輸入如下命令進行安裝。webpack
npm install speed-measure-webpack-plugin -D複製代碼
安裝完成以後,咱們再webpack的配置文件中配置它git
webpack.config.jses6
參考speed-measure-webpack-plugin的使用方式,查看這裏github
配置好以後,啓動項目(這裏只對開發環境進行分析了)後,以下圖
從上圖能夠看出,webpack打包過程當中絕大部分時間花在了loader上,也就是webpack構建流程的第二個環節,編譯階段。注意上面還能看到ProgressPlugin花費了28.87s,因此在咱們不須要分析webpack打包流程花費的時間後,可在webpack.config.js中註釋掉
先刪除以前的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複製代碼
刪除以前的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/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格式,須要將屬性名加雙引號)
在項目的根目錄下,安裝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文件下的代碼格式了
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複製代碼
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,因此不用安裝)
happypack: 多線程編譯,加快編譯速度(加快loader的編譯速度),注意,thread-loader不能夠和 mini-css-extract-plugin 結合使用
如下文件直接在你的項目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
使用異步加載組件的分割代碼的方式進行體積優化見《Webpack按需加載秒開應用》(最重要的一步)