2025年6月7日 星期六 乙巳(蛇)年 三月十一 设为首页 加入收藏
rss
您当前的位置:首页 > 计算机 > 编程开发 > JavaScript

webpack进阶(三)

时间:02-20来源:作者:点击数:14

九、环境变量

想要消除 webpack.config.js 在 开发环境 和 生产环境 之间的差异,你可能需要环境变量(environment variable)。

webpack 命令行 环境配置 的 --env 参数,可以允许你传入任意数量的环境变量。而在 webpack.config.js 中可以访问到这些环境变量。例如,--env production 或 --env NODE_ENV=localNODE_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 的各种方法。

1、前提条件

在开始之前,请确保安装了 Node.js 的最新版本。使用 Node.js 最新的长期支持版本(LTS - Long Term Support),是理想的起步。 使用旧版本,你可能遇到各种问题,因为它们可能缺少 webpack 功能, 或者缺少相关 package。

2、本地安装

要安装最新版本或特定版本,请运行以下命令之一:

  • 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 来执行。

3、全局安装

通过以下 NPM 安装方式,可以使 webpack 在全局环境下可用:

  • npm install --global webpack

Warning

不推荐 全局安装 webpack。这会将你项目中的 webpack 锁定到指定版本,并且在使用不同的 webpack 版本的项目中, 可能会导致构建失败。

4、最新体验版本

如果你热衷于使用最新版本的 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 不适用于生产环境,这意味着它应当用于开发环境。更多详细信息, 请查看 生产环境 指南。

1、启用 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

2、通过 Node.js API

在 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。

3、问题

模块热替换可能比较难以掌握。为了说明这一点,我们回到刚才的示例中。如果你继续点击示例页面上的按钮, 你会发现控制台仍在打印旧的 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);
  • })
  • }

4、HMR 加载样式

借助于 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

tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块语法的 静态结构 特性,例如 import 和 export。这个术语和概念实际上是由 ES2015 模块打包工具 rollup 普及起来的。

webpack 2 正式版本内置支持 ES2015 模块(也叫做 harmony modules)和未使用模块检测能力。新的 webpack 4 正式版本扩展了此检测能力,通过 package.json 的 "sideEffects" 属性作为标记,向 compiler 提供提示,表明项目中的哪些文件是 “pure(纯正 ES2015 模块)”,由此可以安全地删除文件中未使用的部分。

1、添加一个通用模块

在我们的项目中添加一个新的通用模块文件 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 中。我们将在下一节解决这个问题。

2、将文件标记为 side-effect-free(无副作用)

在一个纯粹的 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的版本匹配
  • project
  • 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
  • src/some-side-effectful-file.js
  • export default 'side effectful'
  • src/style.css
  • body {
  • background-color: blueviolet;
  • }
  • src/index.js
  • 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())
  • webpack.config.js
  • 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')
  • + }
  • };
  • package.json
  • {
  • "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.csssome-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.csssome-side-effectful-file.js不在打包的目标文件了。

  • 修改 package.json
  • {
  • "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"
  • }
  • }
方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门
本栏推荐