最近新项目过多,在新项目中每次使用 webpack 都是拷贝以前的项目的配置文件过来,改改直接使用,不少配置仍是只知其一;不知其二,一直想用心的从头配置一次 webpack,加深对 webpack 的理解,因此,有了本文,先献上如下内容github地址。javascript
首先,配置entrycss
const base = { entry: ['./src/index'] }
自 webpack4 起,webpack 提供了默认 entry,也就是咱们上面使用的 './src/index'
,这里咱们用数组包裹一下,方便动态增删,往下html
配置 output:java
const base = { entry: ['./src/index'], output: { publicPath: '/', // 项目根目录 path: path.resolve(__dirname, './dist'), chunkFilename: '[name].[chunkhash].chunk.js' } }
配置 resolve.extensions, require的时候省略文件后缀node
const base = { resolve: { extensions: [".js", ".json"], } }
配置 devServer,开发环境 webpack-dev-server 配置使用react
const host = 'localhost'; const port = 8080; const base = { devServer: { contentBase: [path.join(process.cwd(), './vendor-dev/'), path.join(process.cwd(), './vendor/')], // dllPlugin使用,下文有讲 hot: true, // 热加载 compress: false, open: true, // host: host, port: port, disableHostCheck: true, // 跳过host检测 stats: { colors: true }, filename: '[name].chunk.js', headers: { 'Access-Control-Allow-Origin': '*' } } }
根据不一样的环境,咱们须要对默认的 entry 进行处理,以下webpack
const CleanWebpackPlugin = require('clean-webpack-plugin'); const isDebug = process.env.NODE_ENV !== 'production'; if (isDebug) { base.entry.unshift(`webpack-dev-server/client?http://${host}:${port}`, 'webpack/hot/dev-server'); // 添加devServer入口 base.plugins.unshift(new webpack.HotModuleReplacementPlugin()); // 添加热加载 base.devtool = 'source-map'; } else { base.entry.unshift('babel-polyfill'); // 加入 polyfill base.plugins.push(new CleanWebpackPlugin( // 清理目标目录文件 "*", { root: base.output.path, //根目录 verbose: true, //开启在控制台输出信息 dry: false //启用删除文件 } )) }
添加图片、字体文件处理:git
const base = { module: { rules: [{ test: /\.(woff|woff2|ttf|eot|png|jpg|jpeg|gif|svg)(\?v=\d+\.\d+\.\d+)?$/i, // 图片加载 loader: 'url-loader', query: { limit: 10000 } }] } }
production 生成环境对编译进行 optimizationgithub
const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); const base = { optimization: { minimize: !isDebug, // 是否压缩 minimizer: !isDebug ? [new UglifyJsPlugin({ cache: true, // 使用缓存 parallel: true, // 多线程并行处理 sourceMap: true, // 使用sourceMap uglifyOptions: { comments: false, warnings: false, compress: { unused: true, dead_code: true, collapse_vars: true, reduce_vars: true }, output: { comments: false } } })] : [], splitChunks: { // 自行切割全部chunk chunks: 'all' } }, }
splitChunks 配置的 chunks: 'all'
会改变html的引进的脚本,加了chunksHash后每次编译的结果会不一致,须要结合html-webpack-plugin 使用。web
下面添加 plugins
const ProgressBarPlugin = require('progress-bar-webpack-plugin'); const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; const HtmlWebpackPlugin = require('html-webpack-plugin'); const base = { plugins: [ new ProgressBarPlugin(), // 为编译添加进度条 new webpack.DefinePlugin({ // 为项目注入环境变量 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), '__DEV__': isDebug }), new BundleAnalyzerPlugin({ // 生成编译结果分析报告 analyzerMode: 'server', analyzerHost: '127.0.0.1', analyzerPort: 8889, reportFilename: 'report.html', defaultSizes: 'parsed', generateStatsFile: false, statsFilename: 'stats.json', statsOptions: null, logLevel: 'info' }), new HtmlWebpackPlugin({ // 使用html模板,编译结束后会根据 entry 注入 script脚本 和 css样式表 filename: 'index.html', template: path.resolve(__dirname, './index.html') }) ] }
导出配置
module.exports = base;
webpack 的 react 配置,只要是针对 babel-loader 进行配置,首先声明一个 bable-loader:
const path = require('path'); const babelLoader = { test: /\.jsx?$/, loader: 'babel-loader', include: [path.resolve(process.cwd(), 'src')], query: { babelrc: false, // 禁止使用.babelrc文件 presets: [ // 配置 presets 'react', 'stage-0', [ 'env', { targets: { browsers: ["last 2 versions", "safari >= 7", "ie >= 9", 'chrome >= 52'] }, useBuiltIns: true, debug: false } ] ], plugins: [ 'transform-decorators-legacy', 'transform-class-properties' ] } }
首先对preset进行理解,就是bable的一个套餐,里面包含了各类plugin
另外,添加另外的 plugins
另外,咱们针对开发环境,为react组件添加热替换 preset,babel-preset-react-hmre
if (isDebug) { babelLoader.query.presets = ['react-hmre'].concat(babelLoader.query.presets) }
另外,为了加快编译速度,咱们使用happypack进行多线程编译
const HappyPack = require('happypack'); const os = require('os'); const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length }); // cpus核数 const happyLoaderId = 'happypack-for-react-babel-loader'; const reactConfig = { module: { rules: [{ test: babelLoader.test, loader: 'happypack/loader', query: { id: happyLoaderId }, include: babelLoader.include }] }, plugins: [new HappyPack({ id: happyLoaderId, threadPool: happyThreadPool, loaders: [babelLoader] })] } delete babelLoader.test; delete babelLoader.include; module.exports = reactConfig;
首先,配置 css-loader
const isDebug = process.env.NODE_ENV !== 'production'; const cssLoader = { loader: `css-loader`, options: { sourceMap: isDebug, // 是否添加source-map modules: true, // 是否使用css-module localIdentName: '[local]', // 使用class自己名字,不添加任何hash } }
配置 postcss-loader
const postcssLoader = { loader: 'postcss-loader', options: { config: { path: __dirname } } }
这里咱们使用配置文件 postcss.config.js 路径指向当前文件夹,而后新建配置文件 postcss.config.js,以下
module.exports = { plugins: () => { return [ require('postcss-nested')(), // 用于解开 @media, @supports, @font-face 和 @document 等css规则 require('pixrem')(), // 为 rem 单位添加像素转化 require('autoprefixer')({ // 添加内核前缀 browsers: ['last 2 versions', 'Firefox ESR', '> 1%', 'ie >= 8'] }), require('postcss-flexibility')(), // 添加 flex 布局 polyfill require('postcss-discard-duplicates')() // 去除css中的重复规则 ] } }
配置 less-loader
const lessLoader = { loader: 'less-loader', options: { sourceMap: isDebug, javascriptEnabled: true // 支持内联JavaScript } }
接下来,咱们针对不一样的环境,为webpack添加不一样的 module.rules 和 plugins,首先是开发环境,咱们使用 style-loader 将css进行内联(我的认为内联css对热部署比较友好),另外,同react配置,为了加快编译,咱们使用 happypack 对 loader 进行包裹
const HappyPack = require('happypack'); const os = require('os'); const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length }); const lessHappyLoaderId = 'happypack-for-less-loader'; const cssHappyLoaderId = 'happypack-for-css-loader'; let loaders = []; let plugins = []; if (isDebug) { loaders = [{ test: /\.less$/, loader: 'happypack/loader', query: {id: lessHappyLoaderId} }, { test: /\.css$/, loader: 'happypack/loader', query: {id: cssHappyLoaderId} }] plugins = [new HappyPack({ id: lessHappyLoaderId, threadPool: happyThreadPool, loaders: ['style-loader', cssLoader, postcssLoader, lessLoader ] }), new HappyPack({ id: cssHappyLoaderId, threadPool: happyThreadPool, loaders: ['style-loader', cssLoader, postcssLoader ] })] }
而后,对于生产环境,咱们使用 mini-css-extract-plugin 将 css 文件分离出来,并打包成 chunks,以便减小线上的首屏加载时间。
if (!isDebug) { loaders = [{ test: /\.less$/, use: [MiniCssExtractPlugin.loader, { loader: 'happypack/loader', query: {id: lessHappyLoaderId} }] }, { test: /\.css/, use: [MiniCssExtractPlugin.loader, { loader: 'happypack/loader', query: {id: cssHappyLoaderId} }] }] plugins = [new MiniCssExtractPlugin({ filename: '[name].css', // chunkFilename: "[id].css" }), new HappyPack({ id: lessHappyLoaderId, loaders: [ cssLoader, postcssLoader, lessLoader ], threadPool: happyThreadPool }), new HappyPack({ id: cssHappyLoaderId, loaders: [ cssLoader, postcssLoader ], threadPool: happyThreadPool })] }
最后,导出配置
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin'); const lessConfig = { module: { rules: loaders }, plugins, optimization: { minimizer: [new OptimizeCssAssetsPlugin({ // 使用 OptimizeCssAssetsPlugin 对css进行压缩 cssProcessor: require('cssnano'), // css 压缩优化器 cssProcessorOptions: { discardComments: { removeAll: true } } // 去除全部注释 })] } }; module.exports = lessConfig;
最后,咱们将全部配置 merge 在一块儿
const merge = require('webpack-merge'); const baseConfig = require('./webpack.base.config'); const reactConfig = require('./webpack.react.config'); const lessConfig = require('./webpack.less.config'); const config = merge(baseConfig, reactConfig, lessConfig); module.exports = config;
而后咱们配置 package.json 的 sctipts,这里咱们使用better-npm-run
导出环境变量
{ "scripts": { "start": "better-npm-run start", "build": "better-npm-run build" }, "betterScripts": { "start": { "command": "webpack-dev-server --config ./build/webpack.config.js", "env": { "NODE_ENV": "development" } }, "build": { "command": "webpack --config ./build/webpack.config.js", "env": { "NODE_ENV": "production" } } }, }
好的,配置到这里已经完成,咱们能够肆无忌惮的执行 npm run start
了。
针对 React 项目,对于开发过程,咱们只关心业务代码的增量编译,对于一些第三方 module 咱们不须要对齐进行更改,对于生产环境,这些第三方包也能够利用缓存将其缓存起来,优化线上用户体验,因此咱们可使用DllPlugin
或者SplitChunksPlugin
对这些第三方包进行分离。
DllPlugin 能够将指定的module提早编译好,而后在每次解析到这些指定的module时,webpack可直接使用这些module,而不用从新编译,这样能够大大的增长咱们的编译速度。
SplitChunksPlugin,可使用test对module进行正则匹配,对指定的模块打包成chunk,而后每次编译的时候直接使用这些chunk的缓存,而不用每次解析组装这些module。固然,使用SplitChunksPlugin生成的chunk在生成环境可能由于咱们指定了chunkHash
每次文件名不同,致使咱们不能好好利用浏览器缓存这些第三方库,也会所以影响到咱们html中每次引入的script,必须结合html-webpack-plugin进行使用,但对于一些没有彻底先后端分离的业务项目来讲(如路由由后端来控制,html渲染是后端控制),这很明显是一个麻烦。
dllPlugin的原理就是预先编译模块,而后在html中最早引进这些打包完的包,这样 webpack 就能够从全局变量里面去找这些预先编译好的模块。
下面咱们使用配置使用 dllPlugin,新建配置文件 webpack.dll.config.js,这个文件为 webpack 须要事先编译的配置文件
首先声明输出 output
const path = require('path'); const isDebug = process.env.NODE_ENV !== 'production'; const output = { filename: '[name].js', library: '[name]_library', path: path.resolve(process.cwd(), isDebug ? './vendor-dev/' : './vendor/') // 编译打包后的目录 }
而后声明整体配置
const dllConfig = { entry: { vendor: ['react', 'react-dom'] // 咱们须要事先编译的模块,用entry表示 }, output: output, plugins: [ new webpack.DllPlugin({ // 使用dllPlugin path: path.join(output.path, `${output.filename}.json`), name: output.library // 全局变量名, 也就是 window 下 的 [output.library] }), new ProgressBarPlugin(), new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), __DEV__: isDebug }) ], optimization: {} }
而后,咱们根据不一样的环境,添加配置
if (!isDebug) { dllConfig.mode = 'production'; dllConfig.optimization.minimize = true; dllConfig.optimization.minimizer = [new UglifyJsPlugin({ cache: true, parallel: true, sourceMap: true, uglifyOptions: { comments: false, warnings: false, compress: { unused: true, dead_code: true, collapse_vars: true, reduce_vars: true }, output: { comments: false } } })]; } else { dllConfig.mode = 'development'; } module.exports = dllConfig;
须要注意的是,当咱们使用dllPlugin对react进行编译时,咱们须要使用isDebug对react进行生产环境和开发环境的区分,由于当咱们在生成环境使用开发环境的react的时候,react会报错,因此,咱们这里须要对不一样环境的库进行打包。
编译打包,最后生成了一个 vendor.js 和 vendor.js.json,而后,咱们能够在咱们编译的配置中使用 dllReferencePlugin 引进这个json
下面咱们新建配置文件 webpack.dll.reference.config.js
const path = require('path'); const dllConfig = require('./webpack.dll.config'); const baseConfig = require('./webpack.base.config'); const webpack = require('webpack'); const isDebug = process.env.NODE_ENV !== 'production'; const CopyWebpackPlugin = require('copy-webpack-plugin'); const dllPath = dllConfig.output.path; const dllEntry = dllConfig.entry; const plugins = [ new CopyWebpackPlugin([{ from: path.join(process.cwd(), isDebug ? './vendor-dev/' : './vendor/'), to: baseConfig.output.path, ignore: ['*.json']}]) // 将dll文件拷贝到编译目录 ]; Object.keys(dllEntry).forEach((key) => { const manifest = path.join(dllPath, `${key}.js.json`); plugins.push(new webpack.DllReferencePlugin({ manifest: require(manifest), // 引进dllPlugin编译的json文件 name: `${key}_library` // 全局变量名,与dllPlugin声明的一直 })) }) module.exports = { plugins }
最后,咱们把这个配置在 webpack.config.js 里 merge 进来
const merge = require('webpack-merge'); const baseConfig = require('./webpack.base.config'); const reactConfig = require('./webpack.react.config'); const lessConfig = require('./webpack.less.config'); const dllReferenceConfig = require('./webpack.dll.reference.config'); const config = merge(baseConfig, reactConfig, lessConfig, dllReferenceConfig); module.exports = config;
而后在package.json添加预编译脚本
{ "scripts": { "start:dll": "better-npm-run start:dll", "build:dll": "better-npm-run build:dll" }, "betterScripts": { "start:dll": { "command": "webpack --config ./build/webpack.dll.config.js", "env": { "NODE_ENV": "development" } }, "build:dll": { "command": "webpack --config ./build/webpack.dll.config.js", "env": { "NODE_ENV": "production" } } } }
打完收工,最后,在npm run start
以前,咱们得先执行npm run start:dll
,并在html中引进这个vendor.js
,否则会报错,找不到library,html以下
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>app</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <script src="/vendor.js"></script> <!-- 根据根目录设置 --> </head> <body> </body> </html>
针对 SplitChunksPlugin,其实就是打包 chunks,如咱们把node_modules下的全部模块打到一个chunk中
const splitChunkConfig = { optimization: { splitChunks: { cacheGroups: { vendor: { name: 'vendor', chunks: 'initial', priority: -10, reuseExistingChunk: false, test: /node_modules\/(.*)\.js/ } } } } }
使用 test 匹配 node_modules,最后会生成一个 vendor.chunk.js,若是设置有 chunkHash,文件名会带hash,而后在html中引进便可。
以上,基本搞了一套 webpack 相对编译较快的配置,嗯呢~,该沉淀一下,献上以上github地址,以上配置,已整理成cli,项目根目录一键生成,详情见 README