想要消除 webpack.config.js 在 开发环境 和 生产环境 之间的差异,你可能需要环境变量(environment variable)。
webpack 命令行 环境配置 的 --env 参数,可以允许你传入任意数量的环境变量。而在 webpack.config.js 中可以访问到这些环境变量。例如,--env production 或 --env NODE_ENV=local(NODE_ENV 通常约定用于定义环境类型,查看 这里(link:https://dzone.com/articles/what-you-should-know-about-node-env))。
- npx webpack --env NODE_ENV=local --env production --progress
-
Tip
如果设置 env 变量,却没有赋值,--env production 默认表示将 env.production 设置为 true。还有许多其他可以使用的语法。更多详细信息,请查看 webpack CLI 文档。
对于我们的 webpack 配置,有一个必须要修改之处。通常,module.exports 指向配置对象。要使用 env 变量,你必须将 module.exports 转换成一个函数:
webpack.config.js
- const path = require('path');
-
- module.exports = env => {
- // Use env.<YOUR VARIABLE> here:
- console.log('NODE_ENV: ', env.NODE_ENV); // 'local'
- console.log('Production: ', env.production); // true
-
- return {
- entry: './src/index.js',
- output: {
- filename: 'bundle.js',
- path: path.resolve(__dirname, 'dist'),
- },
- };
- };
-
本指南介绍了安装 webpack 的各种方法。
在开始之前,请确保安装了 Node.js 的最新版本。使用 Node.js 最新的长期支持版本(LTS - Long Term Support),是理想的起步。 使用旧版本,你可能遇到各种问题,因为它们可能缺少 webpack 功能, 或者缺少相关 package。
要安装最新版本或特定版本,请运行以下命令之一:
- npm install --save-dev webpack
- # 或指定版本
- npm install --save-dev webpack@<version>
-
Tip
是否使用 --save-dev 取决于你的应用场景。假设你仅使用 webpack 进行构建操作,那么建议你在安装时使用 --save-dev 选项,因为可能你不需要在生产环境上使用 webpack。如果需要应用于生产环境,请忽略 --save-dev 选项。
如果你使用 webpack v4+ 版本,你还需要安装 CLI。
- npm install --save-dev webpack-cli
-
对于大多数项目,我们建议本地安装。这可以在引入重大更新(breaking change)版本时,更容易分别升级项目。 通常会通过运行一个或多个 npm scripts 以在本地 node_modules 目录中查找安装的 webpack, 来运行 webpack:
- "scripts": {
- "build": "webpack --config webpack.config.js"
- }
-
Tip
想要运行本地安装的 webpack,你可以通过 node_modules/.bin/webpack 来访问它的二进制版本。另外,如果你使用的是 npm v5.2.0 或更高版本,则可以运行 npx webpack 来执行。
通过以下 NPM 安装方式,可以使 webpack 在全局环境下可用:
- npm install --global webpack
-
Warning
不推荐 全局安装 webpack。这会将你项目中的 webpack 锁定到指定版本,并且在使用不同的 webpack 版本的项目中, 可能会导致构建失败。
如果你热衷于使用最新版本的 webpack,你可以使用以下命令安装 beta 版本, 或者直接从 webpack 的仓库中安装:
- npm install --save-dev webpack@next
- # 或特定的 tag/分支
- npm install --save-dev webpack/webpack#<tagname/branchname>
-
Warning
安装这些最新体验版本时要小心!它们可能仍然包含 bug,因此不应该用于生产环境。
模块热替换(hot module replacement 或 HMR)是 webpack 提供的最有用的功能之一。它允许在运行时更新所有类型的模块, 而无需完全刷新。本页面重点介绍其 实现,而 概念 页面提供了更多关于 它的工作原理以及为什么它有用的细节。
Warning
HMR 不适用于生产环境,这意味着它应当用于开发环境。更多详细信息, 请查看 生产环境 指南。
此功能可以很大程度提高生产效率。我们要做的就是更新 webpack-dev-server 配置, 然后使用 webpack 内置的 HMR 插件。我们还要删除掉 print.js 的入口起点, 因为现在已经在 index.js 模块中引用了它。
Tip
如果你在技术选型中使用了 webpack-dev-middleware 而没有使用 webpack-dev-server,请使用 webpack-hot-middleware 依赖包,以在你的自定义服务器或应用程序上启用 HMR。
webpack.config.js
- const path = require('path');
- const HtmlWebpackPlugin = require('html-webpack-plugin');
- const { CleanWebpackPlugin } = require('clean-webpack-plugin');
-
- module.exports = {
- entry: {
- app: './src/index.js',
- - print: './src/print.js',
- },
- devtool: 'inline-source-map',
- devServer: {
- contentBase: './dist',
- + hot: true,
- },
- plugins: [
- // new CleanWebpackPlugin(['dist/*']) for < v2 versions of CleanWebpackPlugin
- new CleanWebpackPlugin(),
- new HtmlWebpackPlugin({
- title: 'Hot Module Replacement',
- }),
- ],
- output: {
- filename: '[name].bundle.js',
- path: path.resolve(__dirname, 'dist'),
- },
- };
-
现在,我们来修改 index.js 文件,以便当 print.js 内部发生变更时可以告诉 webpack 接受更新的模块。
index.js
- import _ from 'lodash';
- import printMe from './print.js';
-
- function component() {
- const element = document.createElement('div');
- const btn = document.createElement('button');
-
- element.innerHTML = _.join(['Hello', 'webpack'], ' ');
-
- btn.innerHTML = 'Click me and check the console!';
- btn.onclick = printMe;
-
- element.appendChild(btn);
-
- return element;
- }
-
- document.body.appendChild(component());
- +
- + if (module.hot) {
- + module.hot.accept('./print.js', function() {
- + console.log('Accepting the updated printMe module!');
- + printMe();
- + })
- + }
-
更改 print.js 中 console.log 的输出内容,你将会在浏览器中看到如下的输出 (不要担心现在 button.onclick = printMe() 的输出,我们稍后也会更新该部分)。
print.js
- export default function printMe() {
- - console.log('I get called from print.js!');
- + console.log('Updating print.js...');
- }
-
console
- [HMR] Waiting for update signal from WDS...
- main.js:4395 [WDS] Hot Module Replacement enabled.
- + 2main.js:4395 [WDS] App updated. Recompiling...
- + main.js:4395 [WDS] App hot update...
- + main.js:4330 [HMR] Checking for updates on the server...
- + main.js:10024 Accepting the updated printMe module!
- + 0.4b8ee77….hot-update.js:10 Updating print.js...
- + main.js:4330 [HMR] Updated modules:
- + main.js:4330 [HMR] - 20
-
在 Node.js API 中使用 webpack dev server 时,不要将 dev server 选项放在 webpack 配置对象中。而是在创建时, 将其作为第二个参数传递。例如:
- new WebpackDevServer(compiler, options)
-
想要启用 HMR,还需要修改 webpack 配置对象,使其包含 HMR 入口起点。webpack-dev-server 依赖包中具有一个叫做 addDevServerEntrypoints 的方法,你可以通过使用这个方法来实现。这是关于如何使用的一个基本示例:
dev-server.js
- const webpackDevServer = require('webpack-dev-server');
- const webpack = require('webpack');
-
- const config = require('./webpack.config.js');
- const options = {
- contentBase: './dist',
- hot: true,
- host: 'localhost',
- };
-
- webpackDevServer.addDevServerEntrypoints(config, options);
- const compiler = webpack(config);
- const server = new webpackDevServer(compiler, options);
-
- server.listen(8080, 'localhost', () => {
- console.log('dev server listening on port 8080');
- });
-
Tip
如果你正在使用 webpack-dev-middleware,可以通过 webpack-hot-middleware 依赖包,在自定义 dev server 中启用 HMR。
模块热替换可能比较难以掌握。为了说明这一点,我们回到刚才的示例中。如果你继续点击示例页面上的按钮, 你会发现控制台仍在打印旧的 printMe 函数。
这是因为按钮的 onclick 事件处理函数仍然绑定在旧的 printMe 函数上。
为了让 HMR 正常工作,我们需要更新代码,使用 module.hot.accept 将其绑定到新的 printMe 函数上:
index.js
- import _ from 'lodash';
- import printMe from './print.js';
-
- function component() {
- const element = document.createElement('div');
- const btn = document.createElement('button');
-
- element.innerHTML = _.join(['Hello', 'webpack'], ' ');
-
- btn.innerHTML = 'Click me and check the console!';
- btn.onclick = printMe; // onclick event is bind to the original printMe function
-
- element.appendChild(btn);
-
- return element;
- }
-
- - document.body.appendChild(component());
- + let element = component(); // 存储 element,以在 print.js 修改时重新渲染
- + document.body.appendChild(element);
-
- if (module.hot) {
- module.hot.accept('./print.js', function() {
- console.log('Accepting the updated printMe module!');
- - printMe();
- + document.body.removeChild(element);
- + element = component(); // 重新渲染 "component",以便更新 click 事件处理函数
- + document.body.appendChild(element);
- })
- }
-
借助于 style-loader,使用模块热替换来加载 CSS 实际上极其简单。此 loader 在幕后使用了 module.hot.accept,在 CSS 依赖模块更新之后,会将其 patch(修补) 到 <style> 标签中。
首先使用以下命令安装两个 loader :
- npm install --save-dev style-loader css-loader
-
然后更新配置文件,使用这两个 loader。
webpack.config.js
- const path = require('path');
- const HtmlWebpackPlugin = require('html-webpack-plugin');
- const { CleanWebpackPlugin } = require('clean-webpack-plugin');
-
- module.exports = {
- entry: {
- app: './src/index.js',
- },
- devtool: 'inline-source-map',
- devServer: {
- contentBase: './dist',
- hot: true,
- },
- + module: {
- + rules: [
- + {
- + test: /\.css$/,
- + use: ['style-loader', 'css-loader'],
- + },
- + ],
- + },
- plugins: [
- // 对于 CleanWebpackPlugin 的 v2 versions 以下版本,使用 new CleanWebpackPlugin(['dist/*'])
- new CleanWebpackPlugin(),
- new HtmlWebpackPlugin({
- title: 'Hot Module Replacement',
- }),
- ],
- output: {
- filename: '[name].bundle.js',
- path: path.resolve(__dirname, 'dist'),
- },
- };
-
如同 import 模块,热加载样式表同样很简单:
project
- webpack-demo
- | - package.json
- | - webpack.config.js
- | - /dist
- | - bundle.js
- | - /src
- | - index.js
- | - print.js
- + | - styles.css
-
styles.css
- body {
- background: blue;
- }
-
index.js
- import _ from 'lodash';
- import printMe from './print.js';
- + import './styles.css';
-
- function component() {
- const element = document.createElement('div');
- const btn = document.createElement('button');
-
- element.innerHTML = _.join(['Hello', 'webpack'], ' ');
-
- btn.innerHTML = 'Click me and check the console!';
- btn.onclick = printMe; // onclick event is bind to the original printMe function
-
- element.appendChild(btn);
-
- return element;
- }
-
- let element = component();
- document.body.appendChild(element);
-
- if (module.hot) {
- module.hot.accept('./print.js', function() {
- console.log('Accepting the updated printMe module!');
- document.body.removeChild(element);
- element = component(); // Re-render the "component" to update the click handler
- document.body.appendChild(element);
- })
- }
-
将 body 的 style 改为 background: red;,你应该可以立即看到页面的背景颜色随之更改,而无需完全刷新。
styles.css
- body {
- - background: blue;
- + background: red;
- }
-
tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块语法的 静态结构 特性,例如 import 和 export。这个术语和概念实际上是由 ES2015 模块打包工具 rollup 普及起来的。
webpack 2 正式版本内置支持 ES2015 模块(也叫做 harmony modules)和未使用模块检测能力。新的 webpack 4 正式版本扩展了此检测能力,通过 package.json 的 "sideEffects" 属性作为标记,向 compiler 提供提示,表明项目中的哪些文件是 “pure(纯正 ES2015 模块)”,由此可以安全地删除文件中未使用的部分。
在我们的项目中添加一个新的通用模块文件 src/math.js,并导出两个函数:
project
- webpack-demo
- |- package.json
- |- webpack.config.js
- |- /dist
- |- bundle.js
- |- index.html
- |- /src
- |- index.js
- + |- math.js
- |- /node_modules
-
src/math.js
- export function square(x) {
- return x * x;
- }
-
- export function cube(x) {
- return x * x * x;
- }
-
需要将 mode 配置设置成development,以确定 bundle 不会被压缩:
webpack.config.js
- const path = require('path');
-
- module.exports = {
- entry: './src/index.js',
- output: {
- filename: 'bundle.js',
- path: path.resolve(__dirname, 'dist'),
- },
- + devtool: 'source-map',
- + mode: 'development',
- + optimization: {
- + usedExports: true,
- + },
- };
-
配置完这些后,更新入口脚本,使用其中一个新方法,并且为了简化示例,我们先将 lodash 删除:
src/index.js
- - import _ from 'lodash';
- + import { cube } from './math.js';
-
- function component() {
- - const element = document.createElement('div');
- + const element = document.createElement('pre');
-
- - // Lodash, now imported by this script
- - element.innerHTML = _.join(['Hello', 'webpack'], ' ');
- + element.innerHTML = [
- + 'Hello webpack!',
- + '5 cubed is equal to ' + cube(5)
- + ].join('\n\n');
-
- return element;
- }
-
- document.body.appendChild(component());
-
注意,我们没有从 src/math.js 模块中 import 另外一个 square 方法。这个函数就是所谓的“未引用代码(dead code)”,也就是说,应该删除掉未被引用的 export。现在运行 npm script npm run build,并查看输出的 bundle:
dist/bundle.js (around lines 90 - 100)
- /* 1 */
- /***/ (function (module, __webpack_exports__, __webpack_require__) {
- 'use strict';
- /* unused harmony export square */
- /* harmony export (immutable) */ __webpack_exports__['a'] = cube;
- function square(x) {
- return x * x;
- }
-
- function cube(x) {
- return x * x * x;
- }
- });
-
注意,上面的 unused harmony export square 注释。如果你观察它下面的代码,你会注意到虽然我们没有引用 square,但它仍然被包含在 bundle 中。我们将在下一节解决这个问题。
在一个纯粹的 ESM 模块世界中,很容易识别出哪些文件有 side effect。然而,我们的项目无法达到这种纯度,所以,此时有必要提示 webpack compiler 哪些代码是“纯粹部分”。
通过 package.json 的 "sideEffects" 属性,来实现这种方式。
- {
- "name": "your-project",
- "sideEffects": false
- }
-
如果所有代码都不包含 side effect,我们就可以简单地将该属性标记为 false,来告知 webpack,它可以安全地删除未用到的 export。
Tip
“side effect(副作用)” 的定义是,在导入时会执行特殊行为的代码,而不是仅仅暴露一个 export 或多个 export。举例说明,例如 polyfill,它影响全局作用域,并且通常不提供 export。
如果你的代码确实有一些副作用,可以改为提供一个数组:
- {
- "name": "your-project",
- "sideEffects": ["./src/some-side-effectful-file.js"]
- }
-
此数组支持简单的 glob 模式匹配相关文件。其内部使用了 glob-to-regexp(支持:*,**,{a,b},[a-z])。如果匹配模式为 *.css,且不包含 /,将被视为 **/*.css。
Tip
注意,所有导入文件都会受到 tree shaking 的影响。这意味着,如果在项目中使用类似 css-loader 并 import 一个 CSS 文件,则需要将其添加到 side effect 列表中,以免在生产模式中无意中将它删除:
- {
- "name": "your-project",
- "sideEffects": ["./src/some-side-effectful-file.js", "*.css"]
- }
-
最后,还可以在 module.rules 配置选项 中设置 "sideEffects"。
具体操作入下:
- yarn add webpack-cli webpack-dev-server css-loader style-loader -D
- // 注意webpack-cli 与 webpack-dev-server的版本匹配
-
- webpack-demo
- |- package.json
- |- webpack.config.js
- |- /dist
- |- bundle.js
- |- index.html
- |- /src
- |- index.js
- |- math.js
- + |- some-side-effectful-file.js
- + |- style.css
- |- /node_modules
-
- export default 'side effectful'
-
- body {
- background-color: blueviolet;
- }
-
- import { cube } from './math.js'
-
- +import Greeting from './some-side-effectful-file'
- +import './style.css'
-
- function component() {
- const element = document.createElement('pre')
- element.innerHTML = [
- 'Hello webpack!',
- '5 cubed is equal to ' + cube(5)
- ].join('\n\n')
-
- return element
- }
-
- document.body.appendChild(component())
-
- const path = require('path')
-
- module.exports = {
- mode: 'development',
- entry: {
- index: './src/index.js',
- },
- output: {
- filename: 'bundle.js',
- path: path.resolve(__dirname, 'dist'),
- },
- optimization: {
- usedExports: true,
- },
- + module: {
- + rules: [
- + {
- + test: /\.css$/,
- + use: ['style-loader', 'css-loader']
- + }
- + ]
- + },
- + devServer: {
- + contentBase: path.join(__dirname, 'dist')
- + }
- };
-
- {
- "name": "webpack-demo",
- "version": "1.0.0",
- "description": "",
- "private": true,
- + "sideEffects": false,
- "scripts": {
- "test": "echo \"Error: no test specified\" && exit 1",
- "build": "webpack",
- "start": "webpack-dev-server"
- },
- "keywords": [],
- "author": "",
- "license": "ISC",
- "devDependencies": {
- "css-loader": "^5.0.2",
- "style-loader": "^2.0.0",
- "webpack": "^5.21.0",
- "webpack-cli": "3.3.12",
- "webpack-dev-server": "^3.11.2"
- }
- }
-
- npm run build
-
结果发现,style.css和some-side-effectful-file.js都被打包了。
修改webpack.config.js
- const path = require('path')
-
- module.exports = {
- - mode: 'development',
- + mode: 'production',
- entry: {
- index: './src/index.js',
- },
- output: {
- filename: 'bundle.js',
- path: path.resolve(__dirname, 'dist'),
- },
- optimization: {
- usedExports: true,
- },
- module: {
- rules: [
- {
- test: /\.css$/,
- use: ['style-loader', 'css-loader']
- }
- ]
- },
- devServer: {
- contentBase: path.join(__dirname, 'dist')
- }
- };
-
结果发现,style.css和some-side-effectful-file.js不在打包的目标文件了。
- {
- "name": "webpack-demo",
- "version": "1.0.0",
- "description": "",
- "private": true,
- - "sideEffects": false,
- + "sideEffects": ["./src/some-side-effectful-file.js", "*.css"],
- "scripts": {
- "test": "echo \"Error: no test specified\" && exit 1",
- "build": "webpack",
- "start": "webpack-dev-server"
- },
- "keywords": [],
- "author": "",
- "license": "ISC",
- "devDependencies": {
- "css-loader": "^5.0.2",
- "style-loader": "^2.0.0",
- "webpack": "^5.21.0",
- "webpack-cli": "3.3.12",
- "webpack-dev-server": "^3.11.2"
- }
- }
-