04 【优化】
04 【优化】
1.Public Path(公共路径)
webpack 提供一个非常有用的配置,该配置能帮助你为项目中的所有资源指定一个基础路径。它被称为公共路径(publicPath)
。
在开发模式中,我们通常有一个 assets/
文件夹,它往往存放在和首页一个级别的目录下。这样是挺方便;但是如果在生产环境下,你想把这些静态文件统一使用CDN加载,那该怎么办?
publicPath
配置公共路径,所有文件的引用将自动添加公共路径的绝对地址。
module.exports = {
...
output: {
...
publicPath: 'https://localhost:3000/'
}
}
2.环境变量 Environment variable
想要消除 webpack.config.js 在 开发环境 和 生产环境 之间的差异,你可能需要环境变量(environment variable)。
webpack 命令行 环 境配置 的 --env 参数,可以允许你传入任意数量的环境变量。而在 webpack.config.js 中可以访问到这些环境变量。例如, \--env production
或\--env goal=local
。
npx webpack --env goal=local --env production --progress
对于我们的 webpack 配置,有一个必须要修改之处。通常, module.exports 指向配置对象。要使用 env 变量,你必须将 module.exports 转换成一个函数:
module.exports = ( env ) => {
return {
...
// 根据命令行参数 env 来设置不同环境的 mode
mode: env.production ? 'production' : 'development'
}
}
3.配置文件优化
开发环境(/doc/webpack-guides-development)和生产环境(production)的构建目标差异很大。
在开发环境中,我们需要具有强大的、具有实时重新加载(live reloading)或热模块替换(hot module replacement)能力的 source map 和 localhost server。
而在生产环境中,我们的目标则转向于关注更小的 bundle,更轻量的 source map,以及更优化的资源,以改善加载时间。由于要遵循逻辑分离,我们通常建议为每个环境编写彼此独立的 webpack 配置。
虽然,以上我们将生产环境和开发环境做了略微区分,但是,请注意,我们还是会遵循不重复原则(Don't repeat yourself - DRY),保留一个“通用”配置。为了将这些配置合并在一起,我们将使用一个名为 webpack-merge
的工具。通过“通用”配置,我们不必在环境特定(environment-specific)的配置中重复代码。
分别对 development
和 production
两种模式优化。
1、首先新建 webpack-config
文件夹,在文件夹中添加三个文件,分别为通用的配置文件、开发模式的配置文件以及生产模式的配置文件。
webpack.config.common.js
const path = require('path');
// 自定义html文件模板插件
const HtmlWebpackPlugin = require('html-webpack-plugin');
// 将css代码抽离成一个文件插件
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const toml = require('toml');
const yaml = require('yaml');
const json5 = require('json5');
module.exports = {
// 入口文件
entry: {
index: {
import: './src/index.js',
// dependOn: 'shared', // 当前入口所依赖的入口
},
another: {
import: './src/another-module.js',
// dependOn: 'shared', // 当前入口所依赖的入口
},
// shared: 'lodash', // 当上面两个模块有lodash这个模块时,就提取出来并命名为shared chunk
},
// 输出文件
output: {
// 这里要用绝对路径
path: path.join(__dirname, '../dist'),
// 将上一次dist目录的打包文件清除
clean: {
keep: /favicon/,
},
// 自定义输出文件名(根据内容生成hash名称,扩展名用原扩展名)
assetModuleFilename: 'images/[contenthash][ext]',
},
/*
html-webpack-plugin 没有任何配置会生成一个自动引入js文件的index.html
*/
plugins: [
new HtmlWebpackPlugin({
// 可以用于html模板的title标签内容
title: 'dselegent',
template: './index.html', // 打包生成的文件的模板
filename: 'index.html', // 打包生成的文件名称。默认为index.html
// 设置所有资源文件注入模板的位置。可以设置的值
// true|'head'|'body'|false,默认值为 true
inject: 'body',
}),
// 压缩的css代码生成的位置
new MiniCssExtractPlugin({
filename: 'styles/[contenthash].css',
}),
],
// 配置资源文件
module: {
rules: [
// asset/resource
{
test: /\.png$/,
type: 'asset/resource',
// 优先级高于 assetModuleFilename
generator: {
filename: 'images/[contenthash][ext]',
},
},
// asset/inline
{
test: /\.svg$/,
type: 'asset/inline',
},
// asset/source
{
test: /\.txt$/,
type: 'asset/source',
},
// asset
// 默认大于8kb生成一个单独文件引用,否则使用base64
{
test: /\.jpg$/,
type: 'asset',
// 更改成大于4MB生成一个单独文件
parser: {
dataUrlCondition: {
maxSize: 4 * 1024 * 1024,
},
},
},
// 处理css
{
test: /\.(css|less)$/,
/* 从后向前执行,less-loader将less解析成css代码,css-loader解析css代码,
style-loader用js将css代码插入到style标签中 */
// use: ['style-loader', 'css-loader', 'less-loader'],
// css代码生成一个css文件,用link标签引入
use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader'],
},
// 处理fonts字体文件
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
generator: {
filename: 'fonts/[contenthash][ext]',
},
},
// csv或tsv
{
test: /\.(csv|tsv)$/,
use: 'csv-loader',
},
// xml
{
test: /\.xml$/,
use: 'xml-loader',
},
// 自定义parser
// toml
{
test: /\.toml$/,
type: 'json',
parser: {
parse: toml.parse,
},
},
// yaml
{
test: /\.yaml$/,
type: 'json',
parser: {
parse: yaml.parse,
},
},
// json5
{
test: /\.json5$/,
type: 'json',
parser: {
parse: json5.parse,
},
},
// js
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
},
],
},
// 优化配置
optimization: {
// 抽离文件
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
},
},
};
webpack.config.dev.js
const path = require('path');
// 自定义html文件模板插件
const HtmlWebpackPlugin = require('html-webpack-plugin');
// 将css代码抽离成一个文件插件
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const toml = require('toml');
const yaml = require('yaml');
const json5 = require('json5');
module.exports = {
// 输出文件
output: {
filename: 'js/[name].js',
},
// 开发模式
mode: 'development',
// 在开发模式下追踪代码具体位置
devtool: 'inline-source-map',
// 热更新
devServer: {
// 将 dist 目录下的文件作为 web 服务的根目录。
static: './dist',
open: true,
},
// 优化配置
optimization: {
runtimeChunk: 'single',
},
};
webpack.config.prod.js
const path = require('path');
// 自定义html文件模板插件
const HtmlWebpackPlugin = require('html-webpack-plugin');
// 将css代码抽离成一个文件插件
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
// css代码压缩插件
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
// js代码压缩
const TerserPlugin = require('terser-webpack-plugin');
const toml = require('toml');
const yaml = require('yaml');
const json5 = require('json5');
module.exports = {
// 输出文件
output: {
filename: 'js/[name].[contenthash].js',
// 定义公共路径
publicPath: 'http://localhost:8080/',
},
// 开发模式
mode: 'production',
// 优化配置
optimization: {
minimizer: [new CssMinimizerPlugin(), new TerserPlugin({
parallel: true, //多线程压缩
extractComments: false //不要注释-因为默认会对每个压缩的文件生成一个txt的注释文本。没必要
})],
},
// 构建不产生警告
performance: {
hints: false,
},
};
于是可以知道,这三个文件其实就是三个对象。第一个对象存放共有的属性和值,dev的对象则存放开发环境的对象,pro的则存放生产环境的对象。 当我们想打开发的包时,只需要使用公用的对象和开发的对象的并集即可。也就是这两者要进行合并。 并且是深层的合并,取并集。webpack提供了一个插件来帮助我们完成这个步骤。
2、使用 webpack-merge
将文件进行合并。安装 webpack-merge
pnpm i webpack-merge -D
3、添加一个合并文件 webpack.config.js
webpack.config.js
const { merge } = require('webpack-merge');
const commonConfig = require('./webpack.config.common');
const productionConfig = require('./webpack.config.prod');
const developmentConfig = require('./webpack.config.dev');
module.exports = env => {
switch (true) {
case env.development:
return merge(commonConfig, developmentConfig);
case env.production:
return merge(commonConfig, productionConfig);
default:
return new Error('No matching configuration was found');
}
};
4、修改 package.json 文件
// 将自定义的命令分别指向相应的文件以及添加 env 环境变量的参数
{
"scripts": {
"start": "webpack serve -c ./webpack-config/webpack.config.js --env development",
"build": "webpack -c ./webpack-config/webpack.config.js --env production"
},
}
如果是当前目录安装了包,可以在脚本中直接使用webpack命令
5、使用命令运行
pnpm run start的时候,就是执行抽离出来的公共配置和开发配置的并集。
pnpm run build的时候,就是执行抽离出来的公共配置和生产配置的并集。
4.webpack模块与解析原理
4.1 在webpack中何为模块
当我们在代码中引入使用的这些东西,都可以成为webpack的模块。
4.2 模块的引入
resolver
是一个帮助寻找模块绝对路径的库。 一个模块可以作为另一个模块的依赖模块,然后被后者引用,如下:
import foo from 'path/to/module';
// 或者
require('path/to/module');
所依赖的模块可以是来自应用程序的代码或第三方库。 resolver
帮助 webpack
从每个 require
/import
语句中,找到需要引入到 bundle
中的模块代码。 当打包模块时,webpack
使用 enhanced-resolve 来解析文件路径。
webpack能够解析三种常见的模块引入方法。
4.2.1 相对路径
这种情况下,使用 import
或 require
的资源文件所处的目录,被认为是上下文目录。 在import/require
中给定的相对路径,enhanced-resolve
会拼接此上下文路径,来生成模块的绝对路径(path.resolve(__dirname, RelativePath)
) 。 这也是我们在写代码时最常用的方式之一,另一种最常用的方式则是模块路径。
4.2.2 绝对路径
绝对路径就是从项目的根目录开始的。这个"/"就是指代的项目根目录:
由于已经获得文件的绝对路径,因此不需要再做进一步解析。
4.2.3 第三方模块的引入
import _ from "lodash";
console.log("---", _.join(["webpack", "module"], "-"));
webpack会自动从node_module中识别并引入对应的第三方库。
也就是在 resolve.modules
中指定的所有目录检索模块(node_modules
里的模块已经被默认配置了)。 你可以通过配置别名的方式来替换初始模块路径, 具体请参照下面 resolve.alias
配置选项。
4.3 resolve配置
路径别名
当我们在写业务代码的时候,经常需要引入组件,如果一个组件隐藏的太深,就很麻烦。为了简化路径的写法,于是就有了路径别名:
resolve: {
alias: {
"@": path.resolve(__dirname, "./src") //配置路径别名,__dirname是node的一个全局变量,记录当前文件的绝对路径(是这个配置文件在的目录)
}
},
默认识别文件后缀顺序
我们发现,只需要“import add from '@/utils/add'”
, webpack就可以帮我们找到add.js
。 事实上,这与import add from '@/utils/add.js'
的效果是一致的。 为 什么会这样? 原来webpack的内置解析器已经默认定义好了一些 文件/目录 的路径 解析规则。 比如当我们
import utils from './utils';
utils是一个文件目录而不是模块(文件),但webpack在这种情况下默认帮我们添加了 后缀“/index.js”,从而将此相对路径指向到utils里的index.js。 这是webpack解析器 默认内置好的规则。 那么现在有一个问题: 当utils文件夹下同时拥有add.js add.json时,"@utils/add"会指向谁呢?
@/utils/add.json
{
"name": "add"
}
我们发现仍然指向到add.js
。 当我们删掉add.js
,会发现此时的引入的add变成了一个 json对象。 上述现象似乎表明了这是一个默认配置的优先级的问题。 而webpack对 外暴露了配置属性: resolve.extentions
, 它的用法形如:
module.exports = {
//...
resolve: {
extensions: ['.js', '.json', '.wasm'],
},
};
webpack会按照数组顺序去解析这些后缀名,对于同名的文件,webpack总是会先 解析列在数组首位的后缀名的文件。
4.4 外部扩展
怎么理解呢,意思是如果需要引用一个库,但是又不想让webpack打包(减少打包的时间),并且又不影响我们在程序中以CMD、AMD或者window/global全局等方式进行使用(一般都以import方式引用使用),那就可以通过配置externals。
这样做的目的就是将不怎么需要更新的第三方库脱离webpack打包,不被打入bundle中,从而减少打包时间,但又不影响运用第三方库的方式,例如import方式等。
有时候,我们为了减小bundle的体积,会把一些不变的第三方库用cdn的形式引入进来。比如jQuery,我们会在html文件中通过script中引入使用。但是我们更希望webpack帮助我们做这件事情。
externals 配置选项提供了「从输出的 bundle 中排除依赖」的方法。相反,所创建的 bundle 依赖于那些存在于用户环境(consumer’s environment)中的依赖。此功能通常对 library 开发人员来说是最有用的,然而也会有各种各样的应用程序用到它。
例如:
在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',
},
};
其中 jquery
是暴露给你内部代码使用的模块名;jQuery
是外部环境存在的模块名
这样就剥离了那些不需要改动的依赖模块,换句话,下面展示的代码还可以正常运行:
import $ from 'jquery';
$('.my-element').animate(/* ... */);
这样不仅之前对第三方库的用法方式不变,还把第三方库剥离出webpack的打包中,从而加速webpack的打包速度。
但是写在index.html
页面很麻烦
// 插入的方式
externalsType: "script",
externals: {
// key值jquery会作为import from后面的
// 前面为cdn地址,后面的为默认暴露出的对象(写成自己想要的样子)
jquery1: ["https://ajax.aspnetcdn.com/ajax/jquery/jquery-3.5.1.min.js", "$"]
//import $ from "jquery1"
},
这样之后,在代码中引入jquery,会暴露为一个$的对象,可以直接使用。并且它的引入,是首屏资源引入之后,执行到这部分代码时才会引入,这样可以减少首屏的js加载时间。
4.5 依赖图
每当一个文件依赖另一个文件时,webpack
都会将文件视为直接存在 依赖关系。这使得 webpack
可以获取非代码资源,如 images
或 web 字体
等。并会把它们作为 依赖 提供给应用程序。
当 webpack 处理应用程序时,它会根据命令行参数中或配置文件中定义的模块列表开始处理。 从 入口 开始,webpack 会递归的构建一个 依赖关系图,这个依赖图包含着应用程序中所需的每个模块,然后将所有模块打包为少量的 bundle —— 通常只有一个 —— 可由浏览器加载。
单纯讲似乎很抽象,我们更期望能够可视化打包产物的依赖图,下边列示了一些 bundle分析工具。
bundle 分析(bundle analysis) 工具:
官方是一个不错的开始。还有一些其他社区支持的可选项:
webpack-chart
: webpack stats 可交互饼图。
webpack-visualizer
: 可视化并分析你的 bundle,检查哪些模块占用空间,哪些 可能是重复使用的。
webpack-bundle-analyzer
:一个 plugin 和 CLI 工具,它将 bundle 内容展示为 一个便捷的、交互式、可缩放的树状图形式。
webpack bundle optimize helper
:这个工具会分析你的 bundle,并提供可操 作的改进措施,以减少 bundle 的大小。
bundle-stats
:生成一个 bundle 报告(bundle 大小、资源、模块),并比较不 同构建之间的结果。
我们来使用 webpack-bundle-analyzer 实现。
安装
pnpm install --save-dev webpack-bundle-analyzer
然后我们配置它:
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
// ...others
new BundleAnalyzerPlugin()
]
}
这时我们执行打包命令
我们在浏览器中打开http://127.0.0.1:8888,我们成功可视化了打包产物依赖图!
每个文件的占用面积越大则该文件越大,所以找到相关文件优化即可
注意: 对于 HTTP/1.1 的应用程序来说,由 webpack 构建的 bundle 非常强大。当 浏览器发起请求时,它能最大程度的减少应用的等待时间。 而对于 HTTP/2 来说, 我们还可以使用代码分割进行进一步优化。(开发环境观测的话需要在DevServer里 进行配置{http2:true, https:false})。
options
new BundleAnalyzerPlugin({
// 可以是`server`,`static`或`disabled`。
// 在`server`模式下,分析器将启动HTTP服务器来显示软件包报告。
// 在“静态”模式下,会生成带有报告的单个HTML文件。
// 在`disabled`模式下,你可以使用这个插件来将`generateStatsFile`设置为`true`来生成Webpack Stats JSON文件。
analyzerMode: 'server',
// 将在“服务器”模式下使用的主机启动HTTP服务器。
analyzerHost: '127.0.0.1',
// 将在“服务器”模式下使用的端口启动HTTP服务器。
analyzerPort: 8888,
// 路径捆绑,将在`static`模式下生成的报告文件。
// 相对于捆绑输出目录。
reportFilename: 'report.html',
// 模块大小默认显示在报告中。
// 应该是`stat`,`parsed`或者`gzip`中的一个。
// 有关更多信息,请参见“定义”一节。
defaultSizes: 'parsed',
// 在默认浏览器中自动打开报告
openAnalyzer: true,
// 如果为true,则Webpack Stats JSON文件将在bundle输出目录中生成
generateStatsFile: false,
// 如果`generateStatsFile`为`true`,将会生成Webpack Stats JSON文件的名字。
// 相对于捆绑输出目录。
statsFilename: 'stats.json',
// stats.toJson()方法的选项。
// 例如,您可以使用`source:false`选项排除统计文件中模块的来源。
// 在这里查看更多选项:https: //github.com/webpack/webpack/blob/webpack-1/lib/Stats.js#L21
statsOptions: null,
logLevel: 'info' // 日志级别。可以是'信息','警告','错误'或'沉默'。
})
5.postcss处理css3兼容性前缀
5.1 概述
postcss
是一个用js工具和插件转换css代码的工具。比如说可以使用Autoprefixer
插件自动获取浏览器流行度和能够支持的属性,并且根据这些数据帮助我们自动地为css规则添加前缀,将最新的css语法转化成大多数浏览器能够理解的语法。
5.2 具体的应用
5.2.1 安装
pnpm i mini-css-extract-plugin css-loader postcss-loader postcss -D
这里需要解释一下,mini-css-extract-plugin插件是替换style-loader插件的(都是把css-loader解析后的代码添加到html上,不同的是style-loader直接添加到html上,而mini-css-extract-plugin是采用link资源的方式添加。所以这里直接使用mini-css-extract-plugin)
5.2.2 配置
{
test: /\.(scss|css)$/,
use: [
loader: MiniCssExtractPlugin.loader,
"css-loader",
"postcss-loader",
"sass-loader"
]
},
注意这里的use数组中的配置,是有顺序要求的,比如我这里是使用scss预处理器,还安装了sass-loader。 那么写出来的style.scss文件,先是经过sass-loader编译处理成常规的css,然后,再经由postcss-loader添加浏览器兼容前缀。再经过css-loader解析成webpack能够理解的模块。最后经由MiniCssExtractPlugin来通过link链接到index.html上。 写如下的css代码,会发现并没有添加兼容浏览器的 css3前缀,这是因为还需要安装一个插件来实现这一个功能。
5.2.3 配置autoprefixer
插件 autoprefixer 提供自动给样式加前缀去兼容浏览器,
安装
pnpm i autoprefixer -D
然后再在项目根目录下创建一个文件:postcss.config.js
,这个文件是专门用来配置css的插件的。
module.exports = () => {
return {
plugins: {
autoprefixer: {}// 为CSS 规则添加前缀
}
};
};
最后,在 package.json
内增加如下实例内容:
"browserslist": [
"> 1%",
"last 2 versions"
],
last 2 versions
: 每个浏览器中最新的两个版本。1% or >= 1%
: 全球浏览器使用率大于1%或大于等于1%。
尽量不要用browserslist最好放在.browserslistrc或者package.json内 postcss.config.js内 overrideBrowserslist 的优先级高于 package.json的browserslist,且不能为空,否则会不生效。
于是重启项目,则会看到前缀了:
6.使用TypeScript
在前端生态里,TS扮演着越来越重要的角色。 我们直入正题,讲下如何在webpack工程化环境中集成TS。 首先,当然是安装我们的ts和对应的loader。
pnpm install --save-dev typescript ts-loader
接下来我们需要在项目根目录下添加一个ts的配置文件————tsconfig.json,我们 可以用ts自带的工具来自动化生成它。
npx tsc --init
我们发现生成了一个tsconfig.json,里面注释掉了绝大多数配置。 现在,根据我们想要的效果来打开对应的配置。
{
"compilerOptions": {
"module": "commonjs" /* Specify what module code is generated. */,
"rootDir": "./src" /* Specify the root folder within your source files. */,
"sourceMap": true, /* Create source map files for emitted JavaScript files. */
"outDir": "./dist" /* Specify an output folder for all emitted files. */,
"allowSyntheticDefaultImports": true /* Allow 'import x from y' when a module doesn't have a default export. */,
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */,
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
"strict": true /* Enable all strict type-checking options. */,
"skipLibCheck": true
}
}
好了,接下来我们新增一个src/index.ts,内置一些内容。 然后我们别忘了更改我们的entry及配置对应的loder。 当然,还有resolve.extensions,将.ts放在.js之前,这样它会先找.ts。 注意,如果我们使用了sourceMap,一定记得和上面的ts配置一样,设置 sourcemap为true。
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
entry: './src/index.ts',
devtool: 'inline-source-map',
module: {
rules: [
{
test: /\.(ts|tsx)$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
extensions: [ '.tsx', '.ts', '.js' ],
},
};
在从 npm 上安装第三方库时,一定要记得同时安装这个库的类型声明文件(typing definition)。 我们可以从 TypeSearch中找到并安装这些第三方库的类型声明文件(https://www.ty pescriptlang.org/dt/search?search=) 。 举个例子,如果想安装 lodash 类型声明文件,我们可以运行下面的命令:
pnpm install --save-dev @types/lodash
index.ts
import _ from 'lodash'
const age: number = 18;
console.log(age)
console.log(_.join(["hello","dselegent"]," "));
7.多页面应用
尽管单页面应用很流行,但是我们并不总是需要它。 在多页面应用程序中,server 会拉取一个新的 HTML 文档给你的客户端。页面重新加载此新文档,并且资源被重新下载。然而,这给了我们特殊的机会去做很多事,例如使用 optimization.splitChunks 为页面间共享的应用程序代码创建 bundle。由于入口起点数量的增多,多页应用能够复用多个入口起点之间的大量代码/模块,从而可以极大地从这些技术中受益。
7.1 entry 配置
7.1.1 单个入口(简写)语法
用法: entry: string | [string]
webpack.config.js
module.exports = {
entry: './src/main.js',
};
entry 属性的单个入口语法,参考下面的简写:
module.exports = {
entry: {
main: './src/main.js',
},
};
我们也可以将一个文件路径数组传递给 entry 属性,这将创建一个所谓的 "multimain entry"。在你想要一次注入多个依赖文件,并且将它们的依赖关系绘制在一个 "chunk" 中时,这种方式就很有用。
module.exports = {
entry: ['./src/file_1.js', './src/file_2.js','lodash'],
output: {
filename: 'bundle.js',
},
};
【优点】当通过一个入口为应用程序快速配置webpack时,单一入口的语法是很好的选择。
【缺点】使用这种语法方式它的扩展性、配置灵活性不大。
7.1.2 对象语法
用法: entry: { string | [string] } | {}
module.exports = {
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。publicPath
: 当该入口的输出文件在浏览器中被引用时,为它们指定一个公共 URL 地址。请查看 output.publicPath。
“webpack 配置的可扩展” 是指,这些配置可以重复使用,并且可以与其他配置组合使用。这是一种流行的技术,用于将关注点从环境(environment)、构建目标(build target)、运行时(runtime)中分离。然后使用专门的工具(如 webpack-merge)将它们合并起来。
Tip
runtime
和dependOn
不能在同一个入口上同时使用- 确保
runtime
不能指向已存在的入口名称dependOn
不能循环引用
示例:
webpack.config.js:
module.exports = {
entry: {
a2: 'dependingfile.js',
b2: {
dependOn: 'a2',
import: './src/app.js',
}
}
};
runtime
和 dependOn
不应在同一个入口上同时使用,所以如下配置无效,并且会抛出错误:
module.exports = {
entry: {
a2: './a',
b2: {
runtime: 'x2',
dependOn: 'a2',
import: './b'
}
}
};
确保 runtime
不能指向已存在的入口名称,例如下面配置会抛出一个错误:
module.exports = {
entry: {
a1: './a',
b1: {
runtime: 'a1',
import: './b',
}
}
};
另外 dependOn
不能是循环引用的,下面的例子也会出现错误:
module.exports = {
entry: {
a3: {
import: './a',
dependOn: 'b3',
},
b3: {
import: './b',
dependOn: 'a3',
}
}
};
7.1.3 使用场景
1.分离 app(应用程序) 和 vendor(第三方库) 入口
webpack.config.js
module.exports = {
entry: {
main: './src/app.js',
vendor: './src/vendor.js',
}
};
这是告诉 webpack 要配置 2 个单独的入口点。
webpack.prod.js:
module.exports = {
output: {
filename: '[name].[contenthash].bundle.js',
}
};
webpack.dev.js:
module.exports = {
output: {
filename: '[name].bundle.js',
},
};
这样就可以在 vendor.js
中存入未做修改的必要 library 或文件(例如 Bootstrap, jQuery, 图片等),然后将它们打包在一起成为单独的 chunk。
内容保持不变,这使浏览器可以独立地缓存它们,从而减少了加载时间。
在 webpack < 4 的版本中,通常将 vendor 作为一个单独的入口起点添加到 entry 选项中,以将其编译为一个单独的文件(与 CommonsChunkPlugin 结合使用)。
在 webpack 4 中不鼓励这样做。而是使用 optimization.splitChunks选项,将 vendor 和 app(应用程序) 模块分开,并为其创建一个单独的文件。
不要为 vendor 或其他不是执行起点创建 entry。
2.多页面应用程序
module.exports = {
entry: {
pageOne: './src/pageOne/index.js',
pageTwo: './src/pageTwo/index.js',
pageThree: './src/pageThree/index.js',
}
};
这是告诉 webpack 需要三个独立分离的依赖图。
在多页面应用程序中,server 会拉取一个新的 HTML 文档给客户端。
页面重新加载此新文档,并且资源被重新下载。
然而,这提供了特殊的机会去做很多事,例如使用 optimization.splitChunks
为页面间共享的应用程序代码创建 bundle。
由于入口起点数量的增多,多页应用能够复用多个入口起点之间的大量代码/模块,从而可以极大地从这些技术中受益。
7.2 配置 index.html 模板
1.生成多个HTML文件
要生成多个HTML文件,请在plugins数组中多次声明插件
webpack.config.js
{
entry: {
main: {
import: ['./src/app2.ts', './src/app.ts'], // 启动时需加载的模块
dependOn: 'lodash',// 当前入口所依赖的入口
filename: 'chanel1/[name].js',// 指定要输出的文件名称
},
main2: {
import: './src/app3.ts',
dependOn: 'lodash',//这里的名字写的不是模块的名字,而是我们下面定义的名字
filename: 'chanel2/[name].js',
},
lodash: {// 这里是上面定义的公用依赖模块
import: 'lodash',
filename: 'common/[name].js',
},
},,
output: {
path: __dirname + '/dist',
filename: 'index_bundle.js'
},
plugins: [
new HtmlWebpackPlugin({
template: './index.html',
filename: 'chanel1/index.html', // 指定模板
chunks: ['main', 'lodash'],// 要引入的js文件,这里的名字写的不是模块的名字,而是我们上面定义的名字
}),
new HtmlWebpackPlugin({
template: './index2.html',
filename: 'chanel2/index2.html',
chunks: ['main2', 'lodash'],
}),
],
}
2.编写自己的模板
如果默认生成的HTML不能满足您的需要,您可以提供自己的模板。最简单的方法是使用template
选项并传递一个自定义的HTML文件。html-webpack-plugin将自动将所有必需的CSS、JS、manifest和favicon文件注入到标记中。
其他模板加载程序的详细信息在这里进行了记录。
plugins: [
new HtmlWebpackPlugin({
title: 'Custom template',
// Load a custom template (lodash by default)
template: 'index.html'
})
]
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<!-- ejs 语法(JavaScript 模板引擎) -->
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
</body>
</html>
8.tree shaking
什么是treeShaking?
- treeShaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。
treeShaking有什么用?
- 至于说有什么用呢?它的作用就是将程序中没有用到的代码在打包编译的时候都删除掉,这样能减少打包后包的体积大小,减少程序执行的时长
(https://webpack.js.org/guides/tree-shaking/)
注意:Tree Shaking只支持ES Module语法(即通过import引入 export导出).
项目结构
webpack-demo
|- /dist
|- bundle.js
|- index.html
|- /node_modules
|- /src
|- index.js
+ |- math.js
|- package.json
|- webpack.config.js
mian.js
export const add = (x, y) => x + y;
export const subtract = (x, y) => x - y;
需要将 mode 配置设置成development,以确定 bundle 不会被压缩:
index.js
中引入
import { add } from './math';
console.log(add(1, 2));
从上面看到,我们引用并使用了math.add
函数,没有使用 math.subtract
函数
执行 npx webpack
可以看到,打包结果中 math
模块的两个函数都被打包了
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
mode: 'production',
optimization: {
// 使用 ES module 方式引用的模块将被 tree shaking 优化
usedExports: true,
},
};
注意:在mode为development模式下是不起作用的,应该在开发过程中是要不断调试代码的;
执行 npx webpack
可以看到,只有 已经使用的 add
函数被暴露出去
在生产环境看以下效果
执行 npx webpack
可以看到,只有已经使用的 add
函数的执行结果, subtract
函数就是所谓的“未引用代码(dead code)”,也就是说,应该删除掉未被引用的 export
。并且代码已经被 webpack 优化精简了
可以得出结论,tree shaking 会将通过使用 ES module 方式引用的模块中未使用的代码删除掉
tree shaking
两个关键词:1. 使用 ES module 模块方案; 2. 未使用的代码
src/index.js
import { add, subtract } from './math';
console.log(add(1, 2));
从上面看到,我们引用了 add, subtract
但只使用了math.add
函数,没有使用 math.subtract
函数
执行 npx webpack
可以看到,打包结果中依旧只有 add
函数被打包了,未使用过的 subtract
函数被删除了
出于好奇,webpack是如何完美的避开没有使用的代码的呢?
很简单:就是 Webpack 没看到你使用的代码。Webpack 跟踪整个应用程序的 import/export 语句,因此,如果它看到导入的东西最终没有被使用,它会认为那 是未引用代码(或叫做“死代码”—— dead-code ),并会对其进行 tree-shaking 。
死代码并不总是那么明确的。下面是一些死代码和“活”代码的例子:
// 这会被看作“活”代码,不会做 tree-shaking
import { add } from './math'
console.log(add(5, 6))
// 导入并赋值给 JavaScript 对象,但在接下来的代码里没有用到
// 这就会被当做“死”代码,会被 tree-shaking
import { add, minus } from './math'
console.log(add(5, 6))
// 导入但没有赋值给 JavaScript 对象,也没有在代码里用到
// 这会被当做“死”代码,会被 tree-shaking
import { add, minus } from './math'
console.log('hello webpack')
// 导入整个库,但是没有赋值给 JavaScript 对象,也没有在代码里用到
// 非常奇怪,这竟然被当做“活”代码,因为 Webpack 对库的导入和本地代码导入的处理方式不同。
import { add, minus } from './math'
import 'lodash'
console.log('hello webpack')
9.sideEffects
注意 Webpack 不能百分百安全地进行 tree-shaking。有些模块导入,只要被引入,就会对应用程序产生重要的影响。一个很好的例子就是 全局样式
文件,或者 全局JS
文件。
Webpack 认为这样的文件有“副作用”。具有副作用的文件不应该做 tree-shaking,因为这将破坏整个应用程序。Webpack 的设计者清楚地认识到不知道哪些文件有副作用的情况下打包代码的风险,因此webpack4默认地将所有代码视为有副作用。这可以保护你免于删除必要的文件,但这意味着 Webpack 的默认行为实际上是不进行 tree-shaking。值得注意的是webpack5默认会进行 tree-shaking。
src/style.css
body {
background-color: chocolate;
}
src/todo.global.js
console.log('TODO');
src/index.js
import _ from 'lodash';
import { add, subtract } from './math';
import './todo.global';
import './style.css';
console.log(add(1, 2));
webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'production',
plugins: [
new HtmlWebpackPlugin(),
],
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader',
],
},
],
},
optimization: {
usedExports: true,
},
};
执行 npx webpack serve
,你会发现 style.css
、 todo.global.js
都生效了,这是因为这两个文件不是使用的 ESmodules
方式将模块导出(export)的。
Webpack
认为这样的文件有“副作用”。具有副作用的文件不应该做 tree-shaking
,因为这将破坏整个应用程序。
Webpack
的设计者清楚地认识到不知道哪些文件有副作用的情况下打包代码的风险,因此webpack 4
默认地将所有代码视为有副作用。
这可以保护你免于删除必要的文件,但这意味着 Webpack 的默认行为实际上是不进行 tree-shaking
。值得注意的是 webpack 5
默认会进行 tree-shaking
。如何告诉 Webpack 你的代码无副作用,可以通过 package.json 有一个特殊的属性sideEffects
,就是为此而存在的。
如何告诉 Webpack 你的代码无副作用,可以通过 package.json
有一个特殊的属性 sideEffects
,就是为此而存在的。
**sideEffects
**有三个可能的值:
true:(默认值)这意味着所有的文件都有副作用,也就是没有一个文件可以 tree-shaking。
false:告诉 Webpack 没有文件有副作用,所有文件都可以 tree-shaking。
数组:是文件路径数组。它告诉 webpack,除了数组中包含的文件外,你的任何文件都没有副作用。因此,除了指定的文件之外,其他文件都可以安全地进行 tree-shaking。
Tip
“side effect(副作用)” 的定义是,在导入时会执行特殊行为的代码,而不是仅仅暴露一个 export 或多个 export。举例说明,例如 polyfill,它影响全局作用域,并且通常不提供 export。
package.json
{
"sideEffects": true, // 所有的文件都有副作用,也就是没有一个文件可以 `tree-shaking`。
}
执行 npx webpack serve
可以发现并无变化,因为这是默认的
{
"sideEffects": false, // 告诉 Webpack 没有文件有副作用,所有文件都可以 `tree-shaking`。
}
执行 npx webpack serve
可以发现上面示例中的 style.css
、 todo.global.js
都被 tree-shaking
了
显然 sideEffects
设置为 true 或则 false 显得有些鲁莽极端,你可以使用数组的方式配置文件
{
"sideEffects": ['*.css', '*.global.js'], // 告诉 Webpack 扩展名是 .css 或者 .global.js 文件视为有副作用,不要 `tree-shaking`
}
执行 npx webpack serve
可以发现上面示例中的 style.css
、 todo.global.js
都被 tree-shaking
了
webpack 4
曾经不进行对 CommonJs
导出和 require()
调用时的导出使用分析。
webpack 5
增加了对一些 CommonJs
构造的支持,允许消除未使用的 CommonJs
导出,并从 require()
调用中跟踪引用的导出名称。
解释 tree shaking 和 sideEffectssideEffects
和 usedExports
(更多被认为是 tree shaking)是两种不同的优化方式。
sideEffects
更为有效 是因为它允许跳过整个模块/文件和整个文件子树。
usedExports
依赖于 terser 去检测语句中的副作用。它是一个 JavaScript 任务而且没有像 sideEffects
一样简单直接。而且它不能跳转子树/依赖由于细则中说副作用需要被评估。
结论
我们学到为了利用 tree shaking 的优势, 你必须…