webpack 是前端项目常用的模块打包器,但不少开发者对 webpack 通常一年只接触两次,剩下的时间就 "只管用"了。接下来作者将带着大家重温下webpack
webpack简介及作用
本质上, webpack 是一个用于现代 JavaScript 应用程序的 静态模块打包工具。当 webpack 处理应用程序时,它会在内部从一个或多个入口点构建一个 依赖图(dependency graph),然后将你项目中所需的每一个模块组合成一个或多个 bundles,它们均为静态资源,用于展示你的内容。
webpack的安装与基本配置
安装
npm install webpack webpack-cli --save-dev
不推荐 全局安装 webpack。这会将你项目中的 webpack 锁定到指定版本,并且在使用不同的 webpack 版本的项目中, 可能会导致构建失败。
基本webpack.config.js文件配置
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
};
通过配置文件执行构建
npx webpack --config webpack.config.js
常用的webpack配置项
入口(entry)
入口起点(entry point)用来告诉 webpack 使用哪个模块来作为内部依赖图的构建开始。进入入口起点后,webpack 将会找出入口起点(直接和间接)依赖的模块和库。 入口起点可以只有一个也可以有多个
module.exports = {
// (简写)语法 单个入口
entry: './path/to/my/entry/file.js', //entry 默认值是./src/index.js
// 也可以将一个文件路径数组传递给 entry 属性
// entry: ['./src/file_1.js', './src/file_2.js'],
// 也可以采用对象语法。 对象语法会比较繁琐。然而,这是应用程序中定义入口的最可扩展的方式。
entry: {
app: './src/app.js',
adminApp: './src/adminApp.js',
},
};
用于描述入口的对象。你可以使用如下属性:
-
dependOn: 当前入口所依赖的入口。它们必须在该入口被加载前被加载。(不能是循环引用的) -
filename: 指定要输出的文件名称。 -
import: 启动时需加载的模块。 -
library: 指定 library 选项,为当前 entry 构建一个 library。 -
runtime: 运行时 chunk 的名字。如果设置了,就会创建一个新的运行时 chunk。在 webpack 5.43.0 之后可将其设为 false 以避免一个新的运行时 chunk。(runtime 不能指向已存在的入口名称) -
publicPath: 当该入口的输出文件在浏览器中被引用时,为它们指定一个公共 URL 地址。
输出(output)
output 指定 webpack 构建后所创建的 bundle 位置,以及命名规则。主要输出文件的默认值是 ./dist/main.js ,其他生成文件默认放置在 ./dist 文件夹中。(只能指定一个 output 配置)
const path = require('path');
module.exports = {
entry: './path/to/my/entry/file.js',
output: {
path: path.resolve(__dirname, 'dist'), // 生成的文件位置
filename: 'my-first-webpack.bundle.js', // 生成的文件名
// 如果配置中创建出多于一个 "chunk"(例如,使用多个入口起点或使用像 CommonsChunkPlugin 这样的插件),则应该使用 占位符(substitutions) 来确保每个文 唯一 的名称。
// filename: '[name].js',
},
};
加载器(loader)
loader 用于对模块的源代码进行转换。 webpack 只能理解 JavaScript 和 JSON 文件,loader 使 webpack 可以处理其他类型的文件,并将它们转换为有效模块,以供应用程序使用,以及被添加到依赖图中。 例如,你可以使用 loader 告诉 webpack 加载 CSS 文件,或者将 TypeScript 转为 JavaScript。为此,首先安装相对应的 loader:
npm install --save-dev css-loader ts-loader
然后指示 webpack 对每个 .css 使用 css-loader,以及对所有 .ts 文件使用 ts-loader。module.rules 允许你在 webpack 配置中指定多个 loader。 这种方式是展示 loader 的一种简明方式,并且有助于使代码变得简洁和易于维护。
loader 总是从右到左(或从下到上)地取值(evaluate)/执行(execute)。在实际(从右到左)执行 loader 之前,会先 从左到右 调用 loader 上的 pitch 方法。
module.exports = {
module: {
rules: ['a-loader', 'b-loader', 'c-loader'],
},
};
实际执行顺序
|- a-loader `pitch`
|- b-loader `pitch`
|- c-loader `pitch`
|- requested module is picked up as a dependency
|- c-loader normal execution
|- b-loader normal execution
|- a-loader normal execution
如果某个 loader 在 pitch 方法中给出一个结果,那么这个过程会回过身来,并跳过剩下的 loader。在我们上面的例子中,如果 b-loader 的 pitch 方法返回了一些东西:
module.exports = function (content) {
return someSyncOperation(content);
};
module.exports.pitch = function (remainingRequest, precedingRequest, data) {
if (someCondition()) {
return (
'module.exports = require(' +
JSON.stringify('-!' + remainingRequest) +
');'
);
}
};
上面的步骤将被缩短为:
|- a-loader `pitch`
|- b-loader `pitch` returns a module
|- a-loader normal execution
插件(plugin)
loader 用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。包括:打包优化,资源管理,注入环境变量。webpack 插件是一个具有 apply 方法的 JavaScript 对象。apply 方法会被 webpack compiler 调用,并且在 整个 编译生命周期都可以访问 compiler 对象。
想要使用一个插件,你只需要 require() 它,然后把它添加到 plugins 数组中。多数插件可以通过选项(option)自定义。你也可以在一个配置文件中因为不同目的而多次使用同一个插件,这时需要通过使用 new 操作符来创建一个插件实例。
-
配置方式
webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack'); // 用于访问内置插件
module.exports = {
module: {
rules: [{ test: /\.txt$/, use: 'raw-loader' }],
},
plugins: [new HtmlWebpackPlugin({ template: './src/index.html' })],
};
-
NODE API方式
在使用 Node API 时,还可以通过配置中的
plugins属性传入插件。
const webpack = require('webpack'); // 访问 webpack 运行时(runtime)
const configuration = require('./webpack.config.js');
let compiler = webpack(configuration);
new webpack.ProgressPlugin().apply(compiler);
compiler.run(function (err, stats) {
// ...
});
模式(mode)
通过选择 development, production 或 none 其中的一个,来设置 mode 参数,使之可以在开发和生产环境采用不同的配置。你可以启用 webpack 内置在相应环境下的优化。其默认值为 production。
module.exports = {
mode: 'production',
};
生产环境下将会默认打开一些性能优化配置,如:代码压缩与tree shaking。
解析(Resolve)
配置模块如何解析。例如,当在 ES2015 中调用 import 'lodash',resolve 选项能够对 webpack 查找 'lodash' 的方式去做修改。
module.exports = {
//...
resolve: {
// configuration options
},
};
-
resolve.alias创建
import或require的别名,来确保模块引入变得更简单。例如,一些位于src/文件夹下的常用模块:
const path = require('path');
module.exports = {
//...
resolve: {
alias: {
Utilities: path.resolve(__dirname, 'src/utilities/'),
Templates: path.resolve(__dirname, 'src/templates/'),
},
},
};
现在,替换“在导入时使用相对路径”这种方式,就像这样:
import Utility from '../../utilities/utility';
//也可以这样使用别名:
import Utility from 'Utilities/utility';
-
resolve.fallback当正常解析失败时,重定向模块请求。
module.exports = { //... resolve: { fallback: { crypto: require.resolve('crypto-browserify'), stream: require.resolve('stream-browserify'), }, }, };
devServer
webpack-dev-server 够实现代码修改后自动打包,自动刷新浏览器,从而提高我们的开发效率。
devServer.allowedHosts: 该选项允许将允许访问开发服务器的服务列入白名单。module.exports = { //... devServer: { allowedHosts: [ 'host.com', 'subdomain.host.com', 'subdomain2.host.com', 'host2.com', ], }, };用
.作为子域通配符。.host.com 会与 host.com,www.host.com 以及 host.com 等其他任何其他子域匹配。当设置为 'all' 时会跳过 host 检查。并不推荐这样做,因为不检查 host 的应用程序容易受到 DNS 重绑定攻击。当设置为 'auto' 时,此配置项总是允许 localhost、 host 和 client.webSocketURL.hostnamedevServer.clientlogging允许在浏览器中设置日志级别,例如在重载之前,在一个错误之前或者 热模块替换 启用时。
module.exports = { //... devServer: { client: { // 'log' | 'info' | 'warn' | 'error' | 'none' | 'verbose' logging: 'info', }, }, };overlayboolean = true object: { errors boolean = true, warnings boolean = true }当出现编译错误或警告时,在浏览器中显示全屏覆盖。progressboolean在浏览器中以百分比显示编译进度。
devServer.compressboolean = true启用gzip压缩devServer.hot'only'boolean = true启用 webpack 的 热模块替换 特性。启用热模块替换功能,在构建失败时不刷新页面作为回退,使用hot: 'only'devServer.openbooleanstringobject[string, object]告诉 dev-server 在服务器已经启动后打开浏览器。设置其为 true 以打开你的默认浏览器。devServer.port'auto'stringnumber指定监听请求的端口号。devServer.proxyobject[object, function]如果你有单独的后端开发服务器 API,并且希望在同域名下发送 API 请求 ,那么代理某些 URL 会很有用。可以解决开发环境的跨域问题。现在,对module.exports = { //... devServer: { proxy: { '/api': 'http://localhost:3000', }, }, };/api/users的请求会将请求代理到http://localhost:3000/api/users。- 如果不希望传递
/api,则需要重写路径:module.exports = { //... devServer: { proxy: { '/api': { target: 'http://localhost:3000', pathRewrite: { '^/api': '' }, }, }, }, }; - 默认情况下,将不接受在 HTTPS 上运行且证书无效的后端服务器。 如果需要,可以这样修改配置
module.exports = { //... devServer: { proxy: { '/api': { target: 'https://other-server.example.com', secure: false, }, }, }, }; - 有时不想代理所有内容。 可以基于函数的返回值绕过代理。
在该功能中,可以访问请求,响应和代理选项。
- 返回
null或undefined以继续使用代理处理请求。 - 返回
false会为请求产生 404 错误。 - 返回提供服务的路径,而不是继续代理请求。 例如。 对于浏览器请求,想要提供 HTML 页面,但是对于 API 请求,想要代理它。 可以执行以下操作:
module.exports = { //... devServer: { proxy: { '/api': { target: 'http://localhost:3000', bypass: function (req, res, proxyOptions) { if (req.headers.accept.indexOf('html') !== -1) { console.log('Skipping proxy for browser request.'); return '/index.html'; } }, }, }, }, }; - 返回
- 如果想将多个特定路径代理到同一目标,则可以使用一个或多个带有
context属性的对象的数组:module.exports = { //... devServer: { proxy: [ { context: ['/auth', '/api'], target: 'http://localhost:3000', }, ], }, }; - 默认情况下,代理时会保留主机头的来源,可以将
changeOrigin设置为true以覆盖此行为。module.exports = { //... devServer: { proxy: { '/api': { target: 'http://localhost:3000', changeOrigin: true, }, }, }, };
- 如果不希望传递
cache
缓存生成的 webpack 模块和 chunk,来改善构建速度。cache 会在开发 模式被设置成 type: 'memory' 而且在 生产 模式 中被禁用。 cache: true 与 cache: { type: 'memory' } 配置作用一致。 传入 false 会禁用缓存:
module.exports = {
//...
cache: false,
};
当将 cache.type 设置为 'filesystem' 是会开放更多的可配置项。
-
cache.typestring: 'memory' | 'filesystem'将
cache类型设置为内存或者文件系统。memory选项很简单,它告诉 webpack 在内存中存储缓存,不允许额外的配置。 -
cache.cacheDirectory缓存文件存放的路径,默认为
node_modules/.cache/webpack。(当cache.type被设置成'filesystem'可用)。const path = require('path'); module.exports = { //... cache: { type: 'filesystem', cacheDirectory: path.resolve(__dirname, '.temp_cache'), }, }; -
cache.buildDependencies额外的依赖文件,当这些文件内容发生变化时,缓存会完全失效而执行完整的编译构建,通常可设置为项目配置文件。 默认是
webpack/lib来获取 webpack 的所有依赖项。module.exports = { cache: { buildDependencies: { // This makes all dependencies of this file - build dependencies config: [__filename], // 默认情况下 webpack 与 loader 是构建依赖。 }, }, }; -
cache.managedPaths受控目录,Webpack 构建时会跳过新旧代码哈希值与时间戳的对比,直接使用缓存副本,默认值为
['./node_modules'] -
cache.profile跟踪并记录各个
'filesystem'缓存项的详细时间信息。默认值为false -
cache.maxAge缓存失效时间(以毫秒为单位),默认值为
一个月(5184000000) -
cache.allowCollectingMemory收集在反序列化期间分配的未使用的内存,仅当
cache.type设置为'filesystem'时生效。这需要将数据复制到更小的缓冲区中,并有性能成本。module.exports = { cache: { type: 'filesystem', allowCollectingMemory: true, }, };
devtool
此选项控制是否生成,以及如何生成 source map。source map 是用于在浏览器的开发者工具中跳转到源代码的映射文件,可以方便地进行代码调试和性能分析。选择一种 source map 风格来增强调试过程。不同的值会明显影响到构建(build)和重新构建(rebuild)的速度。
你可以直接使用
SourceMapDevToolPlugin/EvalSourceMapDevToolPlugin来替代使用devtool选项,因为它有更多的选项。切勿同时使用devtool选项和SourceMapDevToolPlugin/EvalSourceMapDevToolPlugin插件。devtool选项在内部添加过这些插件,所以你最终将应用两次插件。
一些常见配置
| devtool | performance | production | quality |
|---|---|---|---|
| (none) | build: fastest rebuild: fastest | yes | bundle |
eval | build: fast rebuild: fastest | no | generated |
eval-cheap-source-map | build: ok rebuild: fast | no | transformed |
eval-source-map | build: slowest rebuild: ok | no | original |
eval-cheap-module-source-map | build: slow rebuild: fast | no | original lines |
source-map | build: slowest rebuild: slowest | yes | original |
hidden-source-map | build: slowest rebuild: slowest | yes | original |
nosources-source-map | build: slowest rebuild: slowest | yes | original |
验证 devtool 名称时, 我们期望使用某种模式, 注意不要混淆 devtool 字符串的顺序, 模式是:
[inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map
上述模式中有三类关键词:
inline、hidden、evalinline:Source Map内容通过base64放在js文件中引入。hidden:代码中没有sourceMappingURL,浏览器不自动引入Source Map。eval:生成代码和Source Map内容混淆在一起,通过eval输出。
nosources使用这个关键字的Source Map不包含sourcesContent,调试时只能看到文件信息和行信息,无法看到源码。
品质说明(quality)
quality 决定我们调试时能看到的源码内容。
bundled:将所有生成的代码视为一大块代码。你看不到相互分离的模块。generated:每个模块相互分离,并用模块名称进行注释。可以看到 webpack 生成的代码。示例:你会看到类似var module__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(42); module__WEBPACK_IMPORTED_MODULE_1__.a();,而不是import {test} from "module"; test();。transformed:每个模块相互分离,并用模块名称进行注释。可以看到 webpack 转换前、loader 转译后的代码。示例:你会看到类似import {test} from "module"; var A = function(_test) { ... }(test);,而不是import {test} from "module"; class A extends test {};。original:每个模块相互分离,并用模块名称进行注释。你会看到转译之前的代码,正如编写它时。这取决于 loader 支持。(lines only):source map 被简化为每行一个映射。这通常意味着每个语句只有一个映射(假设你使用这种方式)。这会妨碍你在语句级别上调试执行,也会妨碍你在每行的一些列上设置断点。与压缩后的代码组合后,映射关系是不可能实现的,因为压缩工具通常只会输出一行。
外部扩展(Externals)
externals 配置选项提供了「从输出的 bundle 中排除依赖」的方法。相反,所创建的 bundle 依赖于那些存在于用户环境(consumer's environment)中的依赖。此功能通常对 library 开发人员来说是最有用的,然而也会有各种各样的应用程序用到它。
externals 防止将某些 import 的包(package)打包到 bundle 中,而是在运行时(runtime)再去从外部获取这些扩展依赖(external dependencies)。
例如,从 CDN 引入 jQuery,而不是把它打包:
<!-- index.html -->
<script
src="https://code.jquery.com/jquery-3.1.0.js"
integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk="
crossorigin="anonymous"
></script>
// webpack.config.js
module.exports = {
//...
externals: {
jquery: 'jQuery',
},
};
这样就剥离了那些不需要改动的依赖模块,换句话,下面展示的代码还可以正常运行:
import $ from 'jquery';
$('.my-element').animate(/* ... */);
上面展示了一个使用外部全局变量的示例,但实际上可以以以下任何形式使用外部变量:全局变量、CommonJS、AMD、ES2015 模块。
- externals的几种写法
module.exports = {
// 1.字符串
externals: 'jquery',
// 2.[string]
externals: {
subtract: ['./math', 'subtract'],
},
// 3.对象
externals: {
react: 'react',
},
// 或者
externals: {
lodash: {
commonjs: 'lodash',
amd: 'lodash',
root: '_', // 指向全局变量
},
},
// 或者
externals: {
subtract: {
root: ['math', 'subtract'],
},
},
// 4.函数
/**
* 1) function ({ context, request, contextInfo, getResolve }, callback)
* 2) function ({ context, request, contextInfo, getResolve }) => promise
*
* 函数接收两个入参:
* - ctx (object):包含文件详情的对象。
ctx.context (string): 包含引用的文件目录。
ctc.request (string): 被请求引入的路径。
ctx.contextInfo (object): 包含 issuer 的信息(如,layer 和 compiler)
ctx.getResolve 5.15.0+: 获取当前解析器选项的解析函数。
- callback (function (err, result, type)): 用于指明模块如何被外部化的回调函数
回调函数接收三个入参:
- err (Error): 被用于表明在外部外引用的时候是否会产生错误。如果有错误,这将会是唯一被用到的参数。
- result (string [string] object): 描述外部化的模块。可以接受其它标准化外部化模块格式,(string, [string],或 object)。
- type (string): 可选的参数,用于指明模块的 external type(如果它没在 result 参数中被指明)。
**/
externals: [
function ({ context, request }, callback) {
if (/^yourregex$/.test(request)) {
// 使用 request 路径,将一个 commonjs 模块外部化
return callback(null, 'commonjs ' + request);
}
// 继续下一步且不外部化引用
callback();
},
],
// RegExp 匹配给定正则表达式的每个依赖,都将从输出 bundle 中排除。
externals: /^(jquery|\$)$/i,
// 5.混用语法
externals: [
{
// 字符串
react: 'react',
// 对象
lodash: {
commonjs: 'lodash',
amd: 'lodash',
root: '_', // indicates global variable
},
// 字符串数组
subtract: ['./math', 'subtract'],
},
// 函数
function ({ context, request }, callback) {
if (/^yourregex$/.test(request)) {
return callback(null, 'commonjs ' + request);
}
callback();
},
// 正则表达式
/^(jquery|\$)$/i,
],
};
补充
import()中的表达式
不能使用完全动态的 import 语句,例如 import(foo)。是因为 foo 可能是系统或项目中任何文件的任何路径。
import() 必须至少包含一些关于模块的路径信息。打包可以限定于一个特定的目录或文件集,以便于在使用动态表达式时 - 包括可能在 import() 调用中请求的每个模块。例如, import(./locale/${language}.json) 会把 .locale 目录中的每个 .json 文件打包到新的 chunk 中。在运行时,计算完变量 language 后,就可以使用像 english.json 或 german.json 的任何文件。
Magic Comments(魔法注释)
内联注释使 'import() 必须至少包含一些关于模块的路径信息' 这一特性得以实现。通过在 import 中添加注释,我们可以进行诸如给 chunk 命名或选择不同模式的操作。
// 单个目标
import(
/* webpackChunkName: "my-chunk-name" */
/* webpackMode: "lazy" */
/* webpackExports: ["default", "named"] */
'module'
);
// 多个可能的目标
import(
/* webpackInclude: /\.json$/ */
/* webpackExclude: /\.noimport\.json$/ */
/* webpackChunkName: "my-chunk-name" */
/* webpackMode: "lazy" */
/* webpackPrefetch: true */
/* webpackPreload: true */
`./locale/${language}`
);
// webpackIgnore:设置为 true 时,禁用动态导入解析。
// 将 webpackIgnore 设置为 true 则不进行代码分割。
import(/* webpackIgnore: true */ 'ignored-module.js');
webpackChunkName: 新 chunk 的名称。 从 webpack 2.6.0 开始,占位符[index]和[request]分别支持递增的数字或实际的解析文件名。 添加此注释后,将单独的给我们的 chunk 命名为 [my-chunk-name].js 而不是 [id].js。webpackMode:从 webpack 2.6.0 开始,可以指定以不同的模式解析动态导入。支持以下选项:'lazy'(默认值):为每个import()导入的模块生成一个可延迟加载(lazy-loadable)的 chunk。'lazy-once':生成一个可以满足所有import()调用的单个可延迟加载(lazy-loadable)的 chunk。此 chunk 将在第一次import()时调用时获取,随后的import()则使用相同的网络响应。注意,这种模式仅在部分动态语句中有意义,例如 import(./locales/${language}.json),其中可能含有多个被请求的模块路径。'eager':不会生成额外的 chunk。所有的模块都被当前的 chunk 引入,并且没有额外的网络请求。但是仍会返回一个 resolved 状态的Promise。与静态导入相比,在调用import()完成之前,该模块不会被执行。'weak':尝试加载模块,如果该模块函数已经以其他方式加载,(即另一个 chunk 导入过此模块,或包含模块的脚本被加载)。仍会返回Promise,但是只有在客户端上已经有该 chunk 时才会成功解析。如果该模块不可用,则返回 rejected 状态的Promise,且网络请求永远都不会执行。当需要的 chunks 始终在(嵌入在页面中的)初始请求中手动提供,而不是在应用程序导航在最初没有提供的模块导入的情况下触发,这对于通用渲染(SSR)是非常有用的。
webpackPrefetch:告诉浏览器将来可能需要该资源来进行某些导航跳转.webpackPreload:预加载,告诉浏览器在当前导航期间可能需要该资源。4.6+才支持,如果是老版本 webpack,可以使用preload-webpack-plugin这种插件来实现预加载。webpackInclude:在导入解析(import resolution)过程中,用于匹配的正则表达式。只有匹配到的模块才会被打包。webpackExclude:在导入解析(import resolution)过程中,用于匹配的正则表达式。所有匹配到的模块都不会被打包。