webpack4.x配置指南
简介
鉴于webpack更新太快,总结下基础配置方法,理解有限,仅做抛砖引玉之用。
起步
初始化配置文件 package.json
并安装webpack
mkdir webpack-demo && cd webpack-demo npm init -y //-y 初始化选项默认为 yes npm i webpack webpack-cli -D // -D 即 -save-dev 版本4.x以上需要安装webpack-cli
创建以下目录结构、文件和内容:
webpack-demo |- package.json + |- /src + |- index.js
//index.js document.write("Hello webpack4!");
创建webpack配置文件
编写开发环境和生产环境彼此独立的webpack配置文件
先添加三个文件
webpack-demo |- package.json + |- webpack.common.js + |- webpack.dev.js + |- webpack.prod.js |- /src |- index.js |- /node_modules
1.webpack.common.js
用到两个基本的插件
npm i clean-webpack-plugin html-webpack-plugin -D
clean-webpack-plugin
:打包时自动清除输出文件夹中未用到的文件;html-webpack-plugin
:打包时会自动生成index.html
并替换已有的index.html
,bundle.js也会自行添加到 html 中。
//webpack.common.js const path = require('path'); const CleanWebpackPlugin = require('clean-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { entry: './src/index.js', plugins: [ new CleanWebpackPlugin(['dist']), new HtmlWebpackPlugin({ title: 'index' }) ], output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist') //定义输出文件夹dist路径 } };
2.webpack.dev.js
先安装webpack-merge,用以合并通用配置文件与开发环境配置文件
npm i webpack-merge -D
安装开发服务器devServer
,作用是修改代码后实时重新加载(刷新浏览器)
npm i webpack-dev-server -D
//webpack.dev.js const merge = require('webpack-merge'); const common = require('./webpack.common.js'); const webpack = require('webpack'); module.exports = merge(common,{ devServer: { //启用开发服务器 contentBase: './dist', //告诉服务器从哪提供内容,只有在想要提供静态文件时才需要 compress: true, //一切服务都启用gzip 压缩 host: '0.0.0.0', //指定使用一个host,可用ip地址访问,没有的话如果别人访问会被禁止。默认localhost。 port: '9999', //指定端口号,如省略,默认为”8080“ hot: true, //启用模块热替换特性 inline: true, //启用内联模式,一段处理实时重载的脚本被插入到bundle中,并且构建消息会出现在浏览器控制台 historyApiFallback: true,//开发单页应用时有用,依赖于HTML5 history API,设为true时所有跳转将指向index.html }, plugins: [ new webpack.HotModuleReplacementPlugin(), //webpack内置的热更新插件 ], mode: 'development' });
devServer的更多可选参数-https://www.webpackjs.com/con...
HotModuleReplacementPlugin
模块热替换(Hot Module Replacement)插件,用以在运行时更新发生改变的模块,从而无需进行完全刷新。
3.webpack.prod.js
同样用'webpack-merge'合并通用配置文件与生产环境配置文件
//webpack.prod.js const merge = require('webpack-merge'); const common = require('./webpack.common.js'); module.exports = merge(common,{ mode: "production" });关于
mode
此时你可能会注意到配置文件中有个mode项,webpack4中新加,作用如下:
--mode production 生产环境
不需要像旧版本一样定义node环境变量
new webpack.DefinePlugin({"process.env.NODE_ENV":JSON.stringify("production") })
ps:许多 library 将通过与 process.env.NODE_ENV 环境变量关联,以决定 library 中应该引用哪些内容。当使用 process.env.NODE_ENV === 'production' 时,一些 library 可能针对具体用户的环境进行代码优化,从而删除或添加一些重要代码。
自动开启一些插件,如:
uglifyjs-webpack-plugin
js代码压缩(所以无需再单独使用)NoEmitOnErrorsPlugin
编译出错时跳过输出,以确保输出资源不包含错误ModuleConcatenationPlugin
webpack3 添加的作用域提升(Scope Hoisting)- --mode development 开发环境
自行定义node环境变量为development
new webpack.DefinePlugin({"process.env.NODE_ENV":JSON.stringify("development") })
使用 eval 构建 module, 提升增量构建速度
自动开启一些插件,如NamedModulesPlugin
使用模块热替换(HMR)时会显示模块的相对路径
具体描述:
Option | Description |
---|---|
development | Sets process.env.NODE_ENV to value development. Enables NamedChunksPlugin and NamedModulesPlugin. |
production | Sets process.env.NODE_ENV to value production. Enables FlagDependencyUsagePlugin, FlagIncludedChunksPlugin, ModuleConcatenationPlugin, NoEmitOnErrorsPlugin, OccurrenceOrderPlugin, SideEffectsFlagPlugin and UglifyJsPlugin. |
none | Opts out of any default optimization options |
启动
在package.json
"scripts" 中添加npm脚本,从而快捷运行开发服务器 | 打包生产环境代码
//package.json { "name": "webpack-demo", "version": "1.0.0", "description": "", "private": true, "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "webpack-dev-server --open --config webpack.dev.js", "build": "webpack --config webpack.prod.js" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "clean-webpack-plugin": "^0.1.19", "html-webpack-plugin": "^3.2.0", "webpack": "^4.15.1", "webpack-cli": "^3.0.8", "webpack-dev-server": "^3.1.4", "webpack-merge": "^4.1.3" } }
"start": "webpack-dev-server --open --config webpack.dev.js",
webpack-dev-server 启动开发服务器 --open 打开浏览器 --config webpack.dev.js 设置运行此脚本时执行的配置文件为webpack.dev.js
"build": "webpack --config webpack.prod.js"
webpack 启动webpack --config webpack.prod.js 设置运行此脚本时执行的配置文件为webpack.prod.js
执行 npm start
此时应该可以看到 Hello webpack4!
执行 npm run build
项目文件夹中自动生成打包后的文件目录(输出文件夹dist)
webpack-demo |- package.json |- webpack.common.js |- webpack.dev.js |- webpack.prod.js |- /src |- index.js |- /dist | - index.html | - app.bundle.js |- /node_modules
使用sourcemap
sourcemap 能实现打包后的运行代码与源代码的映射,帮助我们debug到原始开发代码。
///webpack.dev.js module.exports = merge(common,{ devtool: 'cheap-module-eval-source-map', ... });
大多数时候开发环境用'cheap-module-eval-source-map'是最好的选择,想要完整的功能又不介意构建速度的话就直接用'source-map'。具体的配置项很多,可以是eval,source-map,cheap,module,inline的任意组合。
具体每个参数的作用请查阅官方api:https://webpack.js.org/config...
也可参考这篇文章https://segmentfault.com/a/11... 这里不做详述。
代码分离
把代码分离到不同的 bundle 中,可以按需加载或并行加载这些文件。可用于获取更小的 bundle,以及控制资源加载优先级,如果使用合理,会极大影响加载时间。
三种常用的代码分离方法:
1.入口起点:使用 entry 配置手动地分离代码。
先在src文件夹添加一个文件another-module.js
//another-module.js import _ from 'lodash'; console.log( _.join(['Another', 'module', 'loaded!'], ' ') );
修改index.js
//index.js import _ from 'lodash'; console.log( _.join(['index', 'module', 'loaded!'], ' ') );
用到了lodash,安装下依赖
npm i lodash -S
修改webpack.common.js中entry和output配置
//webpack.common.js module.exports = { entry: { index: './src/index.js', another: './src/another-module.js' }, output: { filename: '[name].bundle.js', //根据入口文件名来定义输出文件名 path: path.resolve(__dirname, 'dist') } };
执行 npm run build
将生成如下构建结果:
Hash: 66f57fffc46778f3b145 Version: webpack 4.16.0 Time: 2966ms Asset Size Chunks Chunk Names another.bundle.js 70.4 KiB 0 [emitted] another index.bundle.js 70.4 KiB 1 [emitted] index index.html 251 bytes [emitted] [1] (webpack)/buildin/module.js 497 bytes {0} {1} [built] [2] (webpack)/buildin/global.js 489 bytes {0} {1} [built] [3] ./src/another-module.js 86 bytes {0} [built] [4] ./src/index.js 83 bytes {1} [built] + 1 hidden module Child html-webpack-plugin for "index.html": 1 asset [0] (webpack)/buildin/module.js 497 bytes {0} [built] [1] (webpack)/buildin/global.js 489 bytes {0} [built] + 2 hidden modules
存在的问题:
- 如果入口 chunks 之间包含重复的模块,那些重复模块都会被引入到各个 bundle 中。
- 不够灵活,不能将核心应用程序逻辑动态地拆分代码。
以上两点中,第一点对我们的示例来说无疑是个问题,index.js 和another-module.js中都引入了 lodash,这样就在两个 bundle 中造成重复引用。接着,我们通过使用 SplitChunks 来移除重复的模块。
2.防止重复:使用SplitChunks
去重和分离 chunk。webpack4 之前版本用的是CommonsChunkPlugin
//webpack.common.js const path = require('path'); module.exports = { entry: { index: './src/index.js', another: './src/another-module.js' }, plugins: [ new CleanWebpackPlugin(['dist']), new HtmlWebpackPlugin({ title: 'Production' }) ], output: { filename: '[name].bundle.js', //根据入口文件名来定义输出文件名 path: path.resolve(__dirname, 'dist') }, + optimization: { + splitChunks: { + chunks: 'all' + } + } };
再次执行npm run build查看效果
... vendors~another~index.bundle.js 69.5 KiB 0 [emitted] vendors~another~index another.bundle.js 1.54 KiB 1 [emitted] another index.bundle.js 1.54 KiB 2 [emitted] index ...
观察打包后文件大小,可以看到index.bundle.js
和another.bundle.js
中已经移除了重复的依赖模块。lodash 被分离到单独的vendors~another~index.bundle.js
chunk中。
3.动态导入:通过模块的内联函数调用来分离代码。
略~。~
分离css
需要用到插件mini-css-extract-plugin
,这个插件会将提取css到单独的文件,根据每个包含css的js文件创建一个css文件,因此,你的样式将不再内嵌到 JS bundle 中。如果你的样式文件大小较大,这会做更快提前加载,因为 CSS bundle 会跟 JS bundle 并行加载。同时还支持按需加载css和SourceMaps.
相较于旧版extract-text-webpack-plugin
插件,mini-css-extract-plugin
的优势有
- 异步加载
- 没有重复的编译
- 更容易使用
- Specific to CSS
- 支持热更新
npm i mini-css-extract-plugin -D
//webpack.common.js const MiniCssExtractPlugin = require("mini-css-extract-plugin"); module.exports = { ... plugins: [ new MiniCssExtractPlugin({ filename: "[name].css", }) ], module: { rules: [ { test: /\.css$/, use: [ { loader: MiniCssExtractPlugin.loader, options: { publicPath: '../' //可以配置输出的css文件路径 } }, "css-loader" ] } ] } ... }
注意
,这个插件不兼容style-loader
(用于以<style>标签形式将css-loader内部样式注入到HTML页面)。
如果想在开发环境下使用style-loader
,在生产环境分离css文件,可以这么配置:
//webpack.common.js const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const devMode = process.env.NODE_ENV !== 'production' module.exports = { plugins: [ new MiniCssExtractPlugin({ filename: '[name].css', }) ], module: { rules: [ { test: /\.(sa|sc|c)ss$/, use: [ devMode ? 'style-loader' : MiniCssExtractPlugin.loader, 'css-loader', { loader:"postcss-loader", //本文未用到此loader... options: { // 如果没有options这个选项将会报错 No PostCSS Config found plugins: (loader) => [] } }, 'sass-loader', ], } ] } }
很明显我们需要先安装处理样式文件的各种loader
npm i style-loader css-loader postcss-loader node-sass sass-loader -D
这里有个问题,node环境变量process.env.NODE_ENV在webpack.config中其实是undefined,之前提及的mode配置会自动定义这个环境变量,但只能在打包后的js中取到,如何在webpack的配置文件中获取这个值呢,需要引入cross-env
npm i cross-env -D
然后在package.json
的脚本命令中指定环境变量
"start": "cross-env NODE_ENV=development webpack-dev-server --open --config webpack.dev.js", "build": "cross-env NODE_ENV=production webpack --config webpack.prod.js"
可自行添加css文件,在js中import,执行npm run build查看效果
当然也可以不获取process.env.NODE_ENV
来区分环境,在dev.js和prod.js分别配置处理样式文件的rule就行了,这也是最开始我们分开写开发环境和生产环境的webpack配置文件的原因。这里提及只是方便从低版本webpack迁移到4.x。
在单个文件中提取所有CSS
配合optimization.splitChunks.cacheGroups使用
//webpack.common.js optimization: { splitChunks: { cacheGroups: { styles: { name: 'styles', test: /\.css$/, chunks: 'all', enforce: true } } } }
会额外生成一个styles.bundle.js
按照入口JS来分离css
//webpack.common.js ... function recursiveIssuer(m) { if (m.issuer) { return recursiveIssuer(m.issuer); } else if (m.name) { return m.name; } else { return false; } } module.exports = { entry: { index: './src/index.js', another: './src/another-module.js' }, ... optimization: { splitChunks: { cacheGroups: { vendor: { //分离第三方库 test: /[\\/]node_modules[\\/]/, name: 'lodash', chunks: 'all' }, indexStyles: { name: 'index', test: (m,c,entry = 'index') => m.constructor.name === 'CssModule' && recursiveIssuer(m) === entry, chunks: 'all', enforce: true }, otherStyles: { name: 'another', test: (m,c,entry = 'another') => m.constructor.name === 'CssModule' && recursiveIssuer(m) === entry, chunks: 'all', enforce: true } } } } };
压缩css
webpack5可能会内置CSS压缩,webpack4需要使用像optimize-css-assets-webpack-plugin
这样的插件。有个问题是设置optimization.minimizer后,会覆盖上文提到的mode配置项提供的默认值,因此需要同时使用JS压缩插件UglifyJsPlugin
npm i optimize-css-assets-webpack-plugin uglifyjs-webpack-plugin -D
//webpack.prod.js const merge = require('webpack-merge'); const common = require('./webpack.config.js'); const UglifyJsPlugin = require("uglifyjs-webpack-plugin"); const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin"); module.exports = merge(common,{ mode: "production", optimization: { minimizer: [ new UglifyJsPlugin({ cache: true, parallel: true, sourceMap: true // set to true if you want JS source maps }), new OptimizeCSSAssetsPlugin({}) ], } });
缓存
我们都知道浏览器获取资源是比较耗费时间的,所以它会使用一种名为 缓存 的技术。通过命中缓存,以降低网络流量,使网站加载速度更快。如果我们在部署新版本时不更改资源的文件名,浏览器就可能会认为它没有被更新,就会使用它的缓存版本。因此确保 webpack 编译生成的文件能够被客户端缓存,而在文件内容变化后,能够请求到新的文件是很有必要的。
通过使用 output.filename
进行文件名替换,可以确保浏览器获取到修改后的文件。[hash]
替换可以用于在文件名中包含一个构建相关的hash,但是更好的方式是使用[chunkhash]
替换,在文件名中包含一个 chunk相关的hash。遗憾的是chunkhash和热更新不兼容,所以开发环境和生产环境要分开配置。
//webpack.common.js ... output: { filename: devMode ? '[name].[hash:8].js': '[name].[chunkhash:8].js', //数字8表示取hash标识符的前八位 chunkFilename: devMode ? '[name].[hash:8].js': '[name].[chunkhash:8].js', //异步模块的文件输出名 path: path.resolve(__dirname, 'dist') }, ...
关于[hash]
和[chunkhash]
的区别,简单来说,[hash]
是编译(compilation)后的hash值,compilation
对象代表某个版本的资源对应的编译进程。项目中任何一个文件改动,webpack就会重新创建compilation
对象,然后计算新的compilation的hash值,所有的编译输出文件名都会使用相同的hash指纹,改一个就一起变。而[chunkhash]
是根据具体模块文件的内容计算所得的hash值,某个文件的改动只会影响它本身的hash指纹,不会影响其他文件。
上文代码分离一节中已经提到了如何将第三方库(比如lodash或react)提取到单独的vendor chunk文件中,因为它们很少像本地的源代码那样频繁修改。利用客户端的长效缓存机制,可以消除请求,减少向服务器获取资源,同时还能保证客户端代码和服务器端代码版本一致。
除了第三方库,webpack在入口模块中,包含了某些样板(boilerplate)
,确切来说就是runtime
和 manifest
。即webpack运行时的引导代码,这部分代码我们也将它单独提取出来。
//webpack.common.js ... optimization: { runtimeChunk: 'single', //分离webpack运行时的引导代码 splitChunks: { chunks: 'all', cacheGroups: { vendor: { test: /[\\/]node_modules[\\/]/, name: 'vendors', chunks: 'all' } } } } ...
我们删掉another-module.js
,修改index.js
如下
///index.js import _ from 'lodash'; function component() { var element = document.createElement('div'); // Lodash, now imported by this script element.innerHTML = _.join(['Hello', 'webpack'], ' '); return element; } document.body.appendChild(component());
执行npm run build
产生以下输出:
Hash: 131e8681e4403392cb5d Version: webpack 4.16.1 Time: 744ms Asset Size Chunks Chunk Names index.5bc56cae.js 260 bytes 0 [emitted] index vendors.5d8f5a63.js 69.5 KiB 1 [emitted] vendors runtime.eb6eb2fb.js 1.42 KiB 2 [emitted] runtime index.html 316 bytes [emitted] [1] ./src/index.js 253 bytes {0} [built] [2] (webpack)/buildin/global.js 489 bytes {1} [built] [3] (webpack)/buildin/module.js 497 bytes {1} [built] + 1 hidden module Child html-webpack-plugin for "index.html": 1 asset [2] (webpack)/buildin/global.js 489 bytes {0} [built] [3] (webpack)/buildin/module.js 497 bytes {0} [built] + 2 hidden modules
可以看到编译出的文件名后加上了hash,运行时的引导代码也被单独提取出来了。
接着添加一个print.js
///print.js export default function print(text) { console.log(text); };
修改index.js
///index.js import _ from 'lodash'; + import Print from './print'; function component() { var element = document.createElement('div'); // Lodash, now imported by this script element.innerHTML = _.join(['Hello', 'webpack'], ' '); + element.onclick = Print.bind(null, 'Hello webpack!'); return element; } document.body.appendChild(component());
再执行npm run build
构建结果如下:
Hash: a710a54674ea8d4b3263 Version: webpack 4.16.1 Time: 3328ms Asset Size Chunks Chunk Names index.15466585.js 327 bytes 0 [emitted] index vendors.7bde7828.js 69.5 KiB 1 [emitted] vendors runtime.eb6eb2fb.js 1.42 KiB 2 [emitted] runtime index.html 316 bytes [emitted] [1] (webpack)/buildin/global.js 489 bytes {1} [built] [2] (webpack)/buildin/module.js 497 bytes {1} [built] [3] ./src/index.js + 1 modules 406 bytes {0} [built] | ./src/index.js 337 bytes [built] | ./src/print.js 64 bytes [built] + 1 hidden module Child html-webpack-plugin for "index.html": 1 asset [2] (webpack)/buildin/global.js 489 bytes {0} [built] [3] (webpack)/buildin/module.js 497 bytes {0} [built] + 2 hidden modules
我们期望的是,只有 index bundle 的 hash 发生变化,然而vendors也跟着变了。这是因为每个 module.id 会基于默认的解析顺序(resolve order)进行增量。也就是说,当解析顺序发生变化,ID 也会随之改变。(官方文档里说runtime的hash也发生了变化,这里并未出现)
可以使用两个插件来解决这个问题。一是NamedModulesPlugin
,将使用模块的路径而不是数字标识符。此插件有助于在开发过程中输出结果的可读性,但执行时间会长一些。二是使用webpack内置插件HashedModuleIdsPlugin
,推荐用于生产环境构建:
const webpack = require('webpack'); ... module.exports = { ... plugins: [ ... new webpack.HashedModuleIdsPlugin(), ... ], ... };
接下来,我们可以随意修改index.js的代码或者增删print.js,再进行构建查看hash的变化。
关于css缓存
假如index.js引用了一个index.css文件,它们会共用相同的chunkhash值。这时如果index.js更改了代码,index.css文件就算内容没有任何改变也会重复构建。
我们可以使用MiniCssExtractPlugin
里的contenthash
,保证css文件所处的模块里只要css文件内容不变,其本身就不会重复构建。
new MiniCssExtractPlugin({ filename: "[name].[contenthash:8].css", chunkFilename: "[name].[contenthash:8].css" }),
这样基本就完成了webpack的缓存配置。
有个小问题是,当修改index.css文件代码,重新构建后index.js的hash值也一起改变了。。。
尝试了下安装插件WebpackMd5Hash可以解决
npm i webpack-md5-hash -D
but,这个插件可能会引发别的bug,好吧这里先不用,后续补充,有兴趣可自行搜索
Babel
完成了基本的配置文件编写与代码分离,开发中需要用babel将旧的浏览器或环境中的es 2015+代码转换为es5。
需要安装一些依赖。
babel-core //必备的核心库 babel-loader //webpack loader配置必备 babel-preset-env //支持es2015、2016、2017, babel-preset-stage-0 //默认向后支持 stage-1,stage-2,stage-3, babel-runtime babel-plugin-transform-runtime //转译新的API
npm i babel-runtime -S
npm i babel-core babel-loader babel-preset-env babel-preset-stage-0 babel-plugin-transform-runtime -D
创建.babelrc
文件
///.babelrc { "presets": [ "env", "stage-0" ], "plugins": [ ["transform-runtime", { "helpers": false, //建议为false "polyfill": false, //是否切换新的内置插件(Promise,Set,Map等)为使用非全局污染的 polyfill,根据你的网站兼容性情况来看,开启会增加很多额外的代码 "regenerator": true //是否切换 generator 函数为不污染全局作用域的 regenerator runtime。 }], ] }
关于 babel-polyfill 与 babel-plugin-transform-runtime
babel 可以转译新的 JavaScript 语法,但并不会转化BOM里面不兼容的API比如Promise,Set,Symbol,Array.from,async 等。这时就需要 polyfill(软垫片) 来转化这些API
babel-polyfill
会仿效一个完整的 ES2015+ 环境,这样你就可以使用新的内置对象比如 Promise 或WeakMap, 静态方法比如 Array.from 或者 Object.assign, 实例方法比如 Array.prototype.includes 和生成器函数(提供 regenerator 插件)。babel-polyfill
缺点是它通过改写全局prototype的方式实现,会污染全局对象所以不适合第三方库的开发,且打包后代码冗余量比较大,我们可能不需要用到所有的新API,对于现代浏览器有些也不需要polyfill。
babel-plugin-transform-runtime
依赖babel-runtime
,babel编译es6到es5的过程中,babel-plugin-transform-runtime
会自动polyfill es5不支持的特性,这些polyfill包就是在babel-runtime这个包里(这就是为啥babel-runtime
需要作为生产依赖引入(使用 --save))。transform-runtime优点是不会污染全局变量,多次使用只会打包一次,并且统一按需引入依赖,无重复、多余引入。缺点是例如"foobar".includes("foo")等实例方法将不起作用。
React
以react开发为例,如果是搭建新的项目,可以直接安装官方脚手架create-react-app
或者使用阿里的开源ui框架 Ant Design
这里仅仅提一下如何在webpack中配置react开发环境
npm install react react-dom -S
还需要安装
babel-plugin-transform-decorators-legacy //支持修饰符语法 @connect babel-preset-react //解析react语法 react-hot-loader //react热更新需要在babelrc做配置
///.babelrc { "presets": [ "env", "react", "stage-0" ], "plugins": [ ["transform-runtime", { "helpers": false, //建议为false "polyfill": false, //是否开始polyfill,根据网站兼容性决定是否开启 "regenerator": true }], "react-hot-loader/babel", //react热更新插件 "transform-decorators-legacy" //修饰符语法转换插件 ] }
如果之前webpack-dev-server配置正确,这时只要把你的根组件标记为热导出,就能启用react热更新
///index.js import React from 'react'; import { hot } from 'react-hot-loader'; const App = () => <div>Hello World!</div> export default hot(module)(App);
别忘了配置babel-loader
///webpack.common.js module: { rules: [{ test: /\.jsx?$/, use: 'babel-loader' }] }
未完待续,容老夫喝口水先...