做者 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按需加载秒开应用》(最重要的一步)