webpack 学习笔记
最近把webpack文档里的指南
过了一遍,有一部分跟着跑了demo。看过可能会忘记,所以再整理一遍加深记忆。在我写下这篇笔记的时候,我觉得自己只能进行基础的配置,别人做的复杂的脚手架中的webpack配置还是要再去学习的。内容和官网几乎一样。
起步
注意node版本
基本安装
- 初始化一个项目
// 初始化npm
npm init -y
//安装webpack-cli,用于在命令行中运行webpack
npm install webpack webpack-cli --save-dev
- 调整package.json的内容保证安装包是私有的;
- 确保安装包时私有的,移除main入口,可以防止意外发布代码。
- "main": "index.js",
+ "private": true,
在文档的示例中,用script标签引入存在隐式的依赖关系(关系是在index.js引入前要先引入ladash)。
这种方式管理js项目会出现一些问题:
- 无法直接体现脚本的执行依赖于外部库
- 如果依赖不存在或引入顺序错误,应用程序将无法正常运行
- 如果依赖被引入但并未使用,浏览器被迫下载无用代码
创建bundle
./dist
叫分发代码./src
叫源代码
安装package时,要打包到生产环境bundle中时使用
npm install --save
用于开发环境
npm install --save-dev
- 执行
npx webpack
会将src/index.js
作为入口起点,也会生成dist/main.js
作为输出
配置文件
- 文件名
webpack.config.js
- 最基础的配置
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
};
比起CLI这种简单的方法,配置文件具有灵活性。可以通过配置方式指定loader规则、plugin、resolve选项、以及其他许多增强功能。
npm scripts
在package.json中设置快捷方式
"scripts": {
"build": "webpack"
},
执行npm run build
即可打包文件
管理资源
- webpack会动态的打包所有依赖(创建所谓的依赖图);现在每个模块都可以明确的表述它自身的依赖,避免打包未使用的模块。
- webpack除了可以引入js,还可以通过loader或内置的
AssetModules
引入其他任何类型的文件
加载css
需要安装style-loader
和css-loader
rules: [
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
}
]
- 模块loader可以链式调用。
- 链中的每个loader都将对资源进行转换。
- 链会逆序执行;第一个loader将其结果(被转换后的资源)传递给下一个loader。
- 最后,链中最后的loader返回JavaScript。
在本例中,由于loader是链式调用的,所以style-loader和css-loader的顺序不能调换。
webpack根据正则表达式,来确定应该查找哪些文件,并将其提供给指定的loader。
加载image图像
可以使用webpack内置的AssetModules
rules: [
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',
}
]
图像将会被添加到output目录中
加载fonts字体
找不到woff/woff2文件,无法实践。
加载数据
可以加载的有用资源还有数据。JSON支持是内置的。
要导入CSV、TSV和XML,可以使用csv-loader
和xml-loader
rules: [
{
test: /\.(csv|tsv)$/i,
use: ['csv-loader'],
}, {
test: /\.xml$/i,
use: ['xml-loader'],
}
]
此外,文档还备注了一个tips: 在使用 d3 等工具实现某些数据可视化时,这个功能极其有用。可以不用在运行时再去发送一个 ajax 请求获取和解析数据,而是在构建过程中将其提前加载到模块中,以便浏览器加载模块后,直接就可以访问解析过的数据。
自定义JSON模块parser
使用自定义parser替代特定的webpack loader。俺也不知道这些后缀(.toml
,.yaml
,.json5
)的文件是干嘛的。
const toml = require('toml');
const yaml = require('yamljs');
const json5 = require('json5');
module: {
rules: [
{
test: /\.toml$/i,
type: 'json',
parser: {
parse: toml.parse,
},
}, {
test: /\.yaml$/i,
type: 'json',
parser: {
parse: yaml.parse,
},
}, {
test: /\.json5$/i,
type: 'json',
parser: {
parse: json5.parse,
},
}
]
},
全局资源
上面这种方式可以将文件和资源结合起来(放在一个文件夹里,这种集中放置的方式会让所有资源紧密耦合)。要使用资源的话,可以以文件夹为单位操作。
管理输出
在文件名中使用hash并输出多个bundle
预先准备
- 给entry添加新的入口起点,再修改output,配置文件如下。
const path = require('path');
module.exports = {
entry: {
index: './src/index.js',
print: './src/print.js',
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};
这时候打包好的js文件一个叫index.bundle.js
一个叫print.bundle.js
dist文件夹里的index.html
也是自己生成的
设置 HtmlWebpackPlugin
不安装这个插件,dist文件夹下的文件很乱。每次更改入口名称或者添加新的入口,构建的时候会生成新的文件,index.html引用的还是旧文件名,还得手动改,很麻烦。实践中还发现用了这个插件之后,除第一次外,打包速度会加快。 安装这个插件会自动生成index.html的内容。
const HtmlWebpackPlugin = require('html-webpack-plugin');
plugins: [
new HtmlWebpackPlugin({
title: '管理输出',
}),
],
清理./dist
文件夹
每次构建前清理./dist文件夹
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
manifest
不会
开发环境
source map
- 解决很难追踪到error和waring在源代码中的原始位置的问题。
- source map可以将编译后的代码映射回原始代码(控制台中可看) 示例中用到inline-source-map选项 在webpack.config.js中新增
module.export = {
devtool: 'inline-source-map'
}
现在如果出现错误的话,浏览器控制台中的报错会指出发生错误的文件和内容。
选择开发工具
每次编译输入npm run build
很麻烦,在代码发生变化后自动编译代码。
watch mode观察者模式
在package.json.scripts中添加
"watch": "webpack --watch"
效果:保存文件就可以看到实时编译结果
缺点是:为了看到修改效果,需要刷新浏览器。所以一般的解决方案是webpack-dev-server
webpack-dev-server
webpack-dev-server提供一个基本的web server,并且具有live loading(实时重加载)的功能。
module.export = {
devServer: {
static: {
directory: path.join(__dirname, 'dist'),
},
},
}
以上配置告知webpack-dev-server,将dist目录下的文件serve到localhost:8080(默认)下。(serve,将资源作为server的可访问文件)
webpack-dev-server会从output.path中定义的目录为服务提供bundle文件,文件通过http://[devServer.host]:[devServer.port]/[output.publicPath]/[output.filename]
进行访问
配置的时候注意node版本问题。
webpack-dev-server v4.0.0+ 要求 node >= v12.13.0、webpack >= v4.37.0(但是我们推荐使用 webpack >= v5.0.0)和 webpack-cli >= v4.7.0。webpack-dev-middleware
webpack-dev-middleware
是一个封装器(wrapper),它可以把 webpack 处理过的文件发送到一个server。(根据名字middleware,是个中间件)
示例给出了一个与express server配合使用的示例
在webpack.config.js
output: {
publicPath: '/',
},
// server.js
const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const app = express();
const config = require('./webpack.config.js');
const compiler = webpack(config);
// 告知 express 使用 webpack-dev-middleware,
// 以及将 webpack.config.js 配置文件作为基础配置。
app.use(
webpackDevMiddleware(compiler, {
publicPath: config.output.publicPath,
})
);
// 将文件 serve 到 port 3000。
app.listen(3000, function () {
console.log('Example app listening on port 3000!\n');
});
添加npm script
{
"scripts": {
"server": "node server.js",
}
}
我的输出和官网给的例子不一样,但是控制台打印的内容一样,页面上也能正常展示。我猜测应该是配置成功了。
终于明白了。。这个东西就是把文件serve到3000端口,之前的示例都是8080端口。
代码分离
- 代码分离这个特性,可以把代码分离到不同的bundle中,然后按需加载或并行加载这些文件。
- 代码分离可以用于获取更小的bundle,以控制资源加载优先级,使用合理,会极大影响加载时间
常用的三种方法
- 入口起点: 使用entry配置手动分离代码
- 防止重复:使用
EntryDependencies
或者SplitChunksPlugin
去重和分离chunk - 动态导入:通过模块的内联函数调用来分离代码
入口起点
-
优点:简单直观
-
缺点:手动配置,存在隐患
-
做法: 新建文件,配置多个入口
entry: {
index: './src/index.js',
another: './src/another-module.js',
},
打包时的输出信息能看出index.bundle.js和another.bundle.js的体积都很大,因为重复模块都被引入了。
隐患:
- 如果入口之间包含一些重复的模块,那些重复的模块都会被引入各个bundle中;
- 不够灵活,不能动态地将核心应用程序逻辑中的代码拆分出来
防止重复
入口依赖
配置dependOn option选项,可以在多个chunk之间共享模块 webpack.js
entry: {
index: {
import: './src/index.js',
dependOn: 'shared',
},
another: {
import: './src/another-module.js',
dependOn: 'shared',
},
shared: 'lodash',
},
还需要增加optimization配置项
optimization: {
runtimeChunk: 'single',
},
这时候生成四个bundle文件,除shared.bundle.js,index.bundle.js 和 another.bundle.js 之外,还生成了一个 runtime.bundle.js
避免使用多入口的入口:`entry: { page: ['./analytics', './app'] }`,在使用async脚本标签时,会有更好的优化以及一致的执行顺序。
SplitChunksPlugin
该插件可以将公共的依赖模块提取到已有的入口chunk,或者提取到一个新生成的chunk。 把上一步的配置删除。在基础配置中增加:
optimization: {
splitChunks: {
chunks: 'all',
},
},
这时候打包出来的有三个bundle,在index.bundle.js和another.bundle.js的基础上还有一个以vendors-node_modules
开头的文件
社区还提供了一个对于代码分离很有帮助的plugin和loader
- mini-css-extract-plugin: 用于将 CSS 从主应用程序中分离。
动态导入
涉及动态导入时,webpack提供了两个类似的技术:
- 第一种:import
- 第二种:require.ensure
不需要another.bundle.js了,修改index.js
function getComponent() {
const element = document.createElement('div');
return import('lodash')
.then(({ default: _ }) => {
const element = document.createElement('div');
element.innerHTML = _.join(['Hello', 'webpack'], '');
return element;
}).catch((error => 'An error occurred while loading the component'));
};
getComponent().then((component) => {
document.body.appendChild(component);
});
需要default的原因: webpack 4 在导入 CommonJS 模块时,将不再解析为 module.exports 的值,而是为 CommonJS 模块创建一个 artificial namespace 对象。
打包后,lodash会被分离到一个单独的bundle。
也可以和async函数一起使用
async function getComponent() {
const element = document.createElement('div');
const { default: _ } = await import('lodash');
element.innerHTML = _.join(['Hello', 'webpack'], '');
return element;
};
getComponent().then((component) => {
document.body.appendChild(component);
});
import中还可以传入动态表达式(动态表达式是什么啊?嘤嘤嘤)
预获取/预加载模块
没看呢
bundle分析
也没看呢
缓存
webpack在打包我们模块化的引用程序时,会生成一个可部署的/dist
目录,然后把打包的内容放置在此目录中。
如果我们在部署新版本中不更改资源的文件名,浏览器就会认为他没有更新,就会使用他的缓存版本。
这一章节的重点在于通过必要的配置,以确保
- webpack编译生成的文件都够被客户端缓存
- 在文件内容变化后,能够请求到新的文件
输出文件的文件名
- 通过替换output.filename中的substitutions设置,来定义输出文件的名称。
- webpack 提供了一种称为 substitution(可替换模板字符串) 的方式,通过带括号字符串来模板化文件名。
- [contenthash] substitution 将根据资源内容创建出唯一 hash。当资源内容发生变化时,[contenthash] 也会发生变化
修改output配置中
filename: '[name].[contenthash].js',
然后输出的文件名就是xxx.一串hash.js
这样了。
这里还有一个问题,webpack在入口chunk中,包含了某些boilerplate,特别是runtime和manifest。重复构建时会导致文件名改变。
提取引导模版
- SplitChunksPlugin 可以用于将模块分离到单独的 bundle 中
- optimization.runtimeChunk 选项将 runtime 代码拆分为一个单独的 chunk
optimization: {
runtimeChunk: 'single',
},
推荐将第三方库提取到单独的vendor chunk文件中。(第三方库的文件很少频繁修改)。
以上的步骤,利用client的长效缓存机制,命中缓存来取消请求,并减少向server获取资源,同时保证client代码和server代码版本一致。
可以通过使用SplitChunksPlugin 插件的 cacheGroups 选项实现。
webpack.config.js
optimization: {
moduleIds: 'deterministic',
runtimeChunk: 'single',
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
}
}
}
}
这时候main中不包含node_modules的代码,node_modules的代码被单独打包到vendors中。(我记得上一章节用了splitchunks就会把引入的包拆分开)
模块标识符
场景:新增模块引入,只希望main的hash变化,但其实main、vendor、runtime都会变化。(main和runtime的变化都是在预期内的,vendor不该变化) 原因:每个 module.id 会默认地基于解析顺序(resolve order)进行增量。也就是说,当解析顺序发生变化,ID 也会随之改变。
- main bundle 会随着自身的新增内容的修改,而发生变化。
- vendor bundle 会随着自身的 module.id 的变化,而发生变化。
- manifest runtime 会因为现在包含一个新模块的引用,而发生变化。
main和manifest发生变化是在预期内的,vendor发生变化是不符合预期的。修复这个问题, 在optimization.moduleIds设置为deterministic
optimization: {
moduleIds: 'deterministic',
}
这时候添加新的本地依赖,vendors都不会再变
问题
我自己实践的过程中,
- 在输出文件名那一步,重复构建并文件名并不会改变。
- 模块标识符,新版本好像不设置也可以。新增本地依赖,打包的输出里只打印变化的文件,没变化的文件直接不打印了。(我用的是webpack版本是5,指南应该是4)
创建Library
webpack除了可以打包应用程序,还可以用于打包js library。
创建一个library
和创建应用程序步骤一样
Expose the Library
通过 output.library 配置项暴露从入口导出的内容
output: {
library: "webpackNumbers",
},
这样子通过script标签被引用,如果要运行在CommonJS、AMD、Node.js等环境 更新配置项为('umd'好像是个通用的配置项)
library: {
name: 'webpackNumbers',
type: 'umd',
}
- tips:不推荐使用array作为库的entry
问题
创建好的库,不知道怎么引用。。。只会script的方式
外部化lodash
在一些场景中,我们更倾向于把lodash当作peerDependency。consumer(使用者)应该已经安装过ladash了。因此,放弃外部控制此外部Library,而是将控制权让给使用Library的consumer。通过externals配置来完成。 webpack.config.js新增
externals: {
lodash: {
commonjs: 'lodash',
commonjs2: 'lodash',
amd: 'lodash',
root: '_',
},
},
这意味着你的library需要一个名为lodash的依赖,这个依赖在consumer环境中必须存在且可用。
外部化的限制
对于想要实现从一个依赖中调用多个文件的那些Library。
import A from 'library/A';
import B from 'library/B';
无法通过在externals中指定整个library的方式,将他们从bundle中排除。而是需要逐个或使用一个正则表达式来排除他们。
modules.exports = {
externals: [
'library/one',
'library/two',
// 正则
// /^library\/.+$/,
]
}
最终步骤
与生产环境指南中提到的步骤相结合,来优化输出结果。 将生成 bundle 的文件路径,添加到 package.json 中的 main 字段中。
{
...
"main": "dist/webpack-numbers.js",
...
}
或者将其添加为标准模块
{
...
"module": "src/index.js",
...
}
这里的 key(键) main 是参照 package.json 标准,而 module 是参照 一个提案,此提案允许 JavaScript 生态系统升级使用 ES2015 模块,而不会破坏向后兼容性。
- 以后可以学习一下从库的创建到发布过程
环境变量
- webpack命令行环境配置的 --env参数,允许传入任意数量的环境变量;
npx webpack --env goal=local --env production --progress
- webpack.config.js中可以访问到这些变量。
- 使用env变量,必须将module.exports转换成一个函数;
const path = require('path');
module.exports = (env) => {
// Use env.<YOUR VARIABLE> here:
console.log('Goal: ', env.goal); // 'local'
console.log('Production: ', env.production); // true
return {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};
};
- 如果设置 env 变量,却没有赋值,--env production 默认表示将 env.production 设置为 true
问题
翻看了客服系统的webpack配置,webpack的配置被写入文件,然后node该文件启动。配置也不是直接写的,而是new各种对象出来的。。。
构建性能
通用环境
使用最新版本(webpack、node)
将loader应用于最少数量的必要模块
通过include字段,将loader应用在实际需要的模块。
const path = require('path');
module.exports = {
//...
module: {
rules: [
{
test: /\.js$/,
include: path.resolve(__dirname, 'src'),
loader: 'babel-loader',
},
],
},
};
- 使用babel-loader的时候要安装三个模块
npm install babel-loader @babel/core @babel/preset-env --save-dev
- 不写include构建时间 1397ms
- 加上include构建时间 700ms
每个额外的loader/plugin都有其启动时间,尽量减少使用工具
不加loader:398ms
提高解析速度
- 减少 resolve.modules, resolve.extensions, resolve.mainFiles, resolve.descriptionFiles 中条目数量,因为他们会增加文件系统调用的次数。(现在还没用resolve呢。。。)
- 如果你不使用 symlinks(例如 npm link 或者 yarn link),可以设置 resolve.symlinks: false。(也没用过symlinks)
- 如果你使用自定义 resolve plugin 规则,并且没有指定 context 上下文,可以设置 resolve.cacheWithContext: false
使用dllPlugin为改变不频繁的代码生成单独编译的结果;提高程序的编译速度,增加了构建过程的复杂度
减少编译结果的大小,提高构建性能,保持chunk体积小
- 使用数量更少、体积更小的library
- 多页面程序用SplitChunksPlugin
- 多页面程序用SplitChunksPlugin,开启async模式
- 移除未引用的代码
- 只编译当前正在开发的那些代码
thread-loader 可以将非常消耗资源的 loader 分流给一个 worker pool。
- 不要使用太多worker,nodejs的runtime和loader都有启动开销。最小化worker和main process之间的模块传输。进程间通讯非常消耗资源。
在webpack配置中使用cache选项,使用package.json中的postinstall清除缓存目录
好像cache: true
是比cache: false
在demo中快那么十几ms。。。
自定义plugin/loader
没用过自定义plugin/loader
将ProgressPlugin从webpack中删除,可以缩短构建时间.但是它也可能不会为快速构建提供太多价值。
开发环境
增量编译
- 增量编译我的理解是根据模块依赖图,只对变化的部分进行编译,不变化的就用之前编译好的在内存里的。
- 使用内置的watch mode;内置的watch mode会记录时间戳并将此信息传递给compilation以使缓存失效。
- 在某些环境配置中,watch mode会回退到poll mode(轮询模式)。监听许多文件会导致CPU大量负载。可以通过watchOptions.poll开增加轮询时间。
- watchOptions.poll通过传递 true 开启 polling,或者指定毫秒为单位进行轮询。
- 把poll的值设置的足够大,更新内容在这段时间里就监听不到了
在内存中编译(在内存中编译、serve资源)
- webpack-dev-server
- webpack-hot-middleware
- webpack-dev-middleware
stats.toJson加速
- 避免获取stats对象的部分内容。webpack4默认使用stats.toJson()输出大量数据;webpack-dev-server在v3.1.3以后的版本,最小化每次增量构建从stats对象中获取的数据量。
devtool
- "eval" 具有最好的性能,但并不能帮助你转译代码。
- 使用
cheap-source-map
变体配置来提高性能 - 使用
eval-source-map
变体配置进行增量编译 - 最佳选择:
eval-cheap-module-source-map
使用eval-cheap-module-source-map
用了320ms/340ms,用eval-source-map用了385ms/390ms
避免在生产环境下才会用到的工具
最小化entry chunk
- webpack只会在文件系统中输出已经更新的chunk,对于某些配置项(HMR,ouput.chunkFilename的[name]/[contenthash],[fullhash])来说,除了对已经更新的chunk无效外,对entry chunk也无效。
- 确保生产entry chunk时,尽量减少其体积以提高性能。
- (eg:为运行时代码创建runtime chunk)
加上runtimeChunk: true
比不加好像快了那么十几ms
webpack执行额外的算法任务,来优化输出结果的体积和加载性能,这些优化用于小型代码库,但是在大型代码库中十分消耗性能。
webpack会在输出的bundle中生成路径信息,打包数千个模块的项目中,会造成垃圾回收性能压力。
- 在 options.output.pathinfo 设置中关闭。
- 这个东西会在bundle中新增一点点东西
设置为false的时候设置为true的时候
Node.js v8.9.10 - v9.11.1中的ES2015Map和Set实现,存在性能回退。
ts-loader
- 为ts-loader传入transpileOnly 选项,缩短ts-loader的构建时间。
- 使用此选项会关闭类型检查,如果要再次开启类型检查,使用ForkTsCheckerWebpackPlugin插件,使用此插件会将检查过程移至单独的进程,加快ts类型检查和ESlint插入的速度。
生产环境
优化代码质量比构建性能更重要
创建多个compilation
为什么需要多个compilation?
- parallel-wepack
source maps
相当消耗资源
工具相关问题
以下工具存在会降低构建性能
babel
减少preset/babel的数量
ts
- 使用fork-ts-checker-webpack-plugin进行类型检查
- 配置loader跳过类型检查
- 使用 ts-loader 时,设置 happyPackMode: true / transpileOnly: true。
sass
- node-sass 中有个来自 Node.js 线程池的阻塞线程的 bug。 当使用 thread-loader 时,需要设置 workerParallelJobs: 2。
内容安全策略
没学
开发-vagrant
没学
依赖管理
没学
安装
就是咋安装webpack,根据项目需要安装就行,全局安装很不方便。
模块热替换
就是那里改变更新哪里。虽然示例给的很简单,但是正式的项目中关于热替换的配置还是挺多的。 提供入口的方法始终没有走通。(直接配置项、node API都没有走通)
启用HMR
在devServer配置项中新增hot: true
在index.js中新增
if (module.hot) {
module.hot.accept('./print.js', function() {
console.log('Accepting the updated printMe module!');
printMe();
})
}
这时候修改print.js的内容,控制台中printMe的打印内容就会变
通过Node.js API
指南中给的例子好像不太行,还有个配置项拼错了。dev-server.js的内容酱紫就可以正常serve出来。
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const DevServer = require('webpack-dev-server');
const config = {
mode: 'development',
entry: [
// Runtime code for hot module replacement
'webpack/hot/dev-server.js',
// Dev server client for web socket transport, hot and live reload logic
// 'webpack-dev-server/client?http://localhost:8080/?hot=true&live-reload=true',
'webpack-dev-server/client?http://localhost:8080/',
// 'webpack-dev-server/client/index.js?hot=true&live-reload=true',
// Your entry
'./src/index.js',
],
plugins: [
new HtmlWebpackPlugin({
title: 'development',
}),
// Plugin for hot module replacement
new webpack.HotModuleReplacementPlugin(),
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
}
};
const compiler = webpack(config);
// `hot` and `client` options are disabled because we added them manually
const devServerConfig = {
static: {
directory: path.join(__dirname, 'dist'),
},
hot: true,
client: false,
};
const server = new DevServer(devServerConfig, compiler);
server.listen(8080);
问题
刚才的情况,虽然printMe函数更新了,但是点击按钮还是打印旧的函数。这是因为onClick事件处理函数仍然绑定在旧printMe上。重新绑定一下:
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();
document.body.appendChild(element);
})
}
HMR加载样式
把loader配置上就行了
tree shaking
tree shaking是一个术语,用于描述移除JavaScript上下文中的未引用代码(dead-code)。它依赖于ES2015模块与法的静态结构特性,例如import和export。这个术语的概念实践是由ES2015模块打包工具rollup普及起来的。
将文件标记为side-effect-free
- package.json的"sideEffects"属性。
- 如果所有代码都不含负副作用,就设置为false
- 如果确实有副作用就写进数组,数组里支持正则
- 还可以在module.rule配置选项中设置"sideEffects"
treeShaking和sideEffects
- sideEffects 和 usedExports(更多被认为是 tree shaking)是两种不同的优化方式。
- sideEffects 更为有效 是因为它允许跳过整个模块/文件和整个文件子树。
- usedExports 依赖于 terser 去检测语句中的副作用。它是一个 JavaScript 任务而且没有像 sideEffects 一样简单直接。而且它不能跳转子树/依赖由于细则中说副作用需要被评估。尽管导出函数能运作如常,但 React 框架的高阶函数(HOC)在这种情况下是会出问题的。
将函数标记为无副作用
/*#__PURE__*/
压缩输出结果
mode: 'production'
结论
用到treeShaking的优势
- 使用ES2015模块语法(即import和export)
- 确保没有编译器将ES2015模块语法转换为CommonJS(@@babel/preset-env的默认行为)
- 在package.json中添加"sideEffects"属性
- 使用mode为"production"的配置项(启用更多优化项,包括代码压缩和tree shaking)
问题
感觉自己只是知道了tree shaking是啥意思,并没有真正的理解在不同的场景下它的配置。
生产环境
配置
- 在开发环境下,一般需要强大的source map和一个有着live reloading或hot module replacement能力的localhost server。
- 在生产环境下,关注点在于压缩bundle、更轻量的source map、资源优化。
- 为每个环境编写彼此独立的webpack配置,遵循不重复原则(Don't repeat yourself),保留一个common配置。
NPM scripts
修改一个scripts
指定mode
许多 library 通过与 process.env.NODE_ENV
- NODE_ENV 是一个由 Node.js 暴露给执行脚本的系统环境变量
- 在构建脚本webpack.config.js中process.env.NODE_ENV并没有被设置为'production'
- 任何位于 /src 的本地代码都可以关联到 process.env.NODE_ENV 环境变量
压缩
- 生产环境下默认使用TerserPlugin
- 还有个选项ClosureWebpackPlugin
源码映射
- 对于本指南,我们将在生产环境中使用 source-map 选项
- 避免在生产中使用 inline-*** 和 eval-***,因为它们会增加 bundle 体积大小,并降低整体性能。
压缩CSS
没看呢,用的是MiniCssExtractPlugin
这个插件
cli替代选项
懒加载
- 懒加载或者按需加载。
- 把你的代码在一些逻辑断点处分离开,然后在一些代码块中完成某些操作后,立即引用或即将引用一些新的代码块。
- 加快了应用的初始加载速度,减轻了它的总体体积。
示例
示例好像只是换了点击事件的写法,并没有在设置上有什么变化
button.onclick = e => import(/*webpackChunkName: "print"*/'./print').then(module => {
const print = module.default;
print();
});
可以实现,刚进入页面不加载print文件,点击按钮后才加载print文件的内容。但是真是的项目里,没见过谁这么用啊。
框架
俺只会react,还没看呢。应该去看一下的。。。
ECMAScript模块
ECMAScript模块(ESM)
将模块标记为ESM
在package.json设置
/*强制package.json下的文件使用ECMAScript*/
{
"type": "module"
}
/*强制package.json下的文件使用CommonJS*/
{
"type": "commonjs"
}
- 文件还可以通过
.mjs
或.cjs
扩展名来设置模块类型。.mjs
将它们强制置为ESM,.cjs
将它们强制置为CommonJS。 - 除了模块格式外,将模式标记为ESM还会影响解析逻辑、操作逻辑和模块中的可用符号。
- 导入模块在ESM中更为严格,导入相对路径的模块必须包含文件名和扩展名;支持导入包。
- non-ESM仅能导入default导出的模块,不支持命名导出的模块。 CommonJS语法不可用。
- HMR使用import.meta.webpackHot代替module.hot
shimming预置依赖
- 一些第三方的库会用到一些全局依赖,这些 library 也可能会创建一些需要导出的全局变量。这些不符合规范的模块就是shimming发挥作用的地方。
- 另外一个地方:当你希望polyfill扩展浏览器能力,来支持到更多用户时。在这种情况下,将这些polyfills提供给需要修补的浏览器(也就是按需加载)。
预置变量依赖
新增plugins
new webpack.ProvidePlugin({
_: 'lodash',
}),
在index.js就可以直接用_了。
还可以使用 ProvidePlugin 暴露出某个模块中单个导出,通过配置一个“数组路径”。
new webpack.ProvidePlugin({
join: ['lodash', 'join'],
}),
这是在文件里直接用join就行
细粒度shimming
一些遗留模块依赖的this指向的是window对象。
在本例中,当模块运行在CommonJS上下文中时,this指向的是module.exports
。
使用imports-loader覆盖this指向:
module: {
rules: [
{
test: require.resolve('./src/index.js'),
use: 'imports-loader?wrapper=window',
},
],
},
全局Exports
在src目录下新建global.js
const file = 'blah.txt';
const helpers = {
test: function () {
console.log('test something');
},
parse: function () {
console.log('parse something');
},
};
使用exports-loader
module: {
rules: [
{
test: require.resolve('./src/global.js'),
use: 'exports-loader?type=commonjs&exports=file,multiple|helpers.parse|parse',
},
],
},
这样可以将一个全局变量作为一个普通模块来导出。(file导出为file,helpers.parse导出为parse)。在 entry 入口文件中(即 src/index.js),可以使用 const { file, parse } = require('./globals.js');,可以保证一切将顺利运行。
加载polyfill
- polyfill是一块代码,用来为旧浏览器提供它没有原生支持的较新的功能。
- polyfill基于自身执行,并且是在基础代码执行之前。
- 这种方式优先考虑正确性,而不考虑bundle体积大小。
- 为了安全和可靠,polyfill/shim必须运行于所有其他代码之前,而且需要同步加载。或者说,需要在所有polyfill/shim加载之后,再去加载所有应用程序代码。
- 社区中存在误解,即现代浏览器“不需要”polyfill,或者polyfill/shim仅用于添加缺失功能。实际上,它们通常用于修复损坏实现(repair broken implementaion)。
- 即使是最现代的浏览器中,也会出现这种情况。因此最佳实践仍旧是,不加选择地和同步地加载所有polyfill/shim,尽管这回导致bundle体积成本。
场景:fetch数据,有的老浏览器没有fetch 在index.html的head中写入
<script>
const modernBrowser = 'fetch' in window && 'assign' in Object;
if (!modernBrowser) {
const scriptElement = document.createElement('script');
scriptElement.async = false;
scriptElement.src = '/polyfills.bundle.js';
document.head.appendChild(scriptElement);
}
</script>
entry入口文件中写入
fetch('https://jsonplaceholder.typicode.com/users')
.then((response) => response.json())
.then((json) => {
console.log("We retrieved some data! AND we're confident it will work on a variety of browser distributions.");
console.log(json);
})
.catch((error) =>
console.error('Something went wrong when fetching this data: ', error)
);
这个东西我没法验证。。。chrome浏览器是有fetch的。。。
进一步优化
babel-preset-env package 通过 browserslist 来转译那些你浏览器中不支持的特性。这个 preset 使用 useBuiltIns 选项,默认值是 false,这种方式可以将全局 babel-polyfill 导入,改进为更细粒度的 import 格式/。
Node内置
像process这种Node内置模块,能直接根据配置文件进行正确的polyfill,而不需要任何特定的loader或者plugin。
其他工具
我觉得这种情况就还是找个别的库用吧。。。
有些遗留模块没有AMD/CommonJS版本,但你也想把他们加进dist,使用noParse来标出这个模块。这样webpack可以引入这些模块,但是不进行转化(parse),以及不解析(resolve)require()和import语句。这种语法还会提高构建性能。
noParse设置
module.exports = {
module: {
noParse: /jquery|lodash/,
}
}
或者
module.exports = {
module: {
noParse: (content) => /jquery|lodash/.test(content),
}
}
任何需要AST的功能(ProvidePlugin)都不起作用
一些模块支持多种模块格式,例如一个混合有AMD、CommonJS和legacy的模块。在这种大多数这种模块下,会首先检查define,然后使用一些怪异代码导出一些属性。
解决方法:通过imports-loader设置additionalCode=var%20define%20=%20false;
来强制CommonJS路径
Tips
shimming是一个库,他将新的API引入到一个旧的环境中,而且仅靠旧的环境中已有的手段实现。polyfill就是用在浏览器API上的shimming。 我们通常的做法是先检查当前浏览器是否支持某个API,如果不支持的话就按需加载对应的polyfill,然后旧浏览器就都可以用这个API了。
Typescript
基础配置
执行以下命令安装Typescript compiler和loader
npm install --save-dev typescript ts-loader
在项目中添加tsconfig.json
{
"compilerOptions": {
"outDir": "./dist/",
"noImplicitAny": true,
"module": "es6",
"target": "es5",
"jsx": "react",
"allowJs": true,
"moduleResolution": "node"
}
}
修改webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.ts',
module: {
rules: [
{
test: /\.(tsx|ts)?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};
webpack会直接从./index.ts
进入,通过ts-loader加载所有的.ts
和.tsx
文件,并在当前目录输出bundle.js文件。
引入lodash文件的时候,要多安装一个类型声明文件
npm install --save-dev @types/lodash
引入的时候
import * as _ from 'lodash';
Loader
- ts-loader依赖于tsconfig.json配置。确保没有将module设置成CommonJS模式,否则webpack将不会tree-shake你的代码。
- 如果你已经使用bable-loader转译你的代码,你可以不使用额外的loader,使用
@babel/preset-typescript
然后让babel同时处理js和ts。 - 与ts-loader,底层的@babel/plugin-transform-typescript不执行任何类型检查。
source maps
在tsconfig.json里添加
"sourceMap": true,
webpack.config.js里也要加
module.exports = {
devtool: 'inline-source-map',
}
第三方库
从npm安装第三方库时,一定要记得同时安装此library的类型声明文件。就和刚才lodash文件一样
导入其他资源
没看呢,还不会写ts呢,学会了ts在写。
Web Worker
没看懂这是干吗呢。。。。
渐进式应用程序
现在,我们并没有运行在离线环境下
就是现在起的服务,停了之后程序就不能用了。。。
要实现的是停了服务程序还能用
添加wordbox
这一部分的具体实现还是和文档里有很大差别的 首先安装插件
npm install workbox-webpack-plugin --save-dev
在webpack.config.js中使用
const WorkboxPlugin = require('workbox-webpack-plugin');
new WorkboxPlugin.GenerateSW({
clientsClaim: true,
skipWaiting: true,
}),
或者
const { GenerateSW } = require('workbox-webpack-plugin');
new GenerateSW({
clientsClaim: true,
skipWaiting: true,
}),
然后执行npm run build
输出如下
注册Service Worker
修改index.js代码
import _ from 'lodash';
import printMe from "./print";
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js').then(registration => {
console.log('sw registered: ', registration);
}).catch(registrationError => {
console.log('SW registration failed: ', registrationError);
});
});
}
这时候先构建npm run build
在启动npm run start
。就可以看控制台如下
有时候workerbox也会输出信息
这样就实现了停掉server以后,再刷新页面,程序还在正常运行。
在实践的过程中还遇到了个问题:
index.js请求的路径写错了,修改了之后重新打包然后起服务,发现报错信息中路径还是错的。这时候清空缓存并硬性重新加载,就可以获取到最新的数据了。所以我觉得service worker就是把编译好的文件缓存在浏览器里,你不强制刷新他他都不会自己更新。
公共路径
这一章学的迷迷糊糊的。。。关键是还没跑通。。。
示例
- output.publicPath 指定应用程序中所有资源的基础路径。
- 发送到output.path目录的每个文件,都将从output.publicPath位置引用。(就是path是保持文件的路径,publicPath是引用文件的路径)
基于环境设置
import webpack from 'webpack';
// 尝试使用环境变量,否则使用根路径
const ASSET_PATH = process.env.ASSET_PATH || '/';
export default {
output: {
publicPath: ASSET_PATH,
},
plugins: [
// 这可以帮助我们在代码中安全地使用环境变量
new webpack.DefinePlugin({
'process.env.ASSET_PATH': JSON.stringify(ASSET_PATH),
}),
],
};
- 加上DefinePlugin中的设置才能在src的文件中获取到process.env.ASSET_PATH。否则是获取不到的。但是这样也说不通。
- 但是process.env.ASSET_PATH这个变量。。。应该是不需要设置就能获取到的吧。。。
在运行时设置
- 在运营时设置publicPath.
__webpack_public_path__
是webpack暴露的全局变量。
__webpack_public_path__ = process.env.ASSET_PATH;
一个问题
如果在entry中用的是ES2015 module import,会在import之后进行__webpack_public_path__
的赋值,这种情况下,必须将public path赋值移至一个专用的文件,然后放在entry.js的上面
// entry.js
import './public-path';
import './app';
Automatic publicPath
- 有可能你事先不知道 publicPath 是什么,webpack 会自动根据
import.meta.url
、document.currentScript
、script.src
或者self.location
变量设置publicPath
。你需要做的是将output.publicPath
设为 'auto': - 不支持document.currentScript的浏览器引入polyfill
集成
大概就是webpack如何跟任务运行工具结合使用。
资源模块
允许使用资源文件,无需配置loader
Resource资源
module: {
rules: [
{
test: /\.png/,
type: 'asset/resource'
}
]
},
配置完以后就可以import了
自定义文件输出名
默认情况下,asset/resource 模块以 [hash][ext][query] 文件名发送到输出目录。
自己写的demo里生成的是酱紫的。·ac19192cb83207a2522b.png
,但这其中哪个是hash哪个是ext哪个是query我也不知道。
两种方式自定义文件生成名
- output.assetModuleFilename修改模版字符串
output: {
assetModuleFilename: 'images/[hash][ext][query]',
}
- 将某些资源发送到指定目录
module: {
rules: [
{
test: /\.html/,
type: 'asset/resource',
generator: {
filename: 'static/[hash][ext][query]',
}
}
]
},
结果就是src下的.html后缀的文件都被发送到dist/static文件下。
Rule.generator.filename 与 output.assetModuleFilename 相同,并且仅适用于 asset 和 asset/resource 模块类型。
inline资源
酱紫设置
module: {
rules: [
{
test: /\.png/,
type: 'asset/inline',
}
]
},
import的资源回变成data URIs。
data URIs由四部分组成
data:[<mediaType>][;base64],<data>
自定义data URI生成器
const svgToMiniDataURI = require('mini-svg-data-uri');
module: {
rules: [
{
test: /\.svg/,
type: 'asset/inline',
generator: {
dataUrl: content => {
content = content.toString();
return svgToMiniDataURI(content);
}
}
}
]
},
source资源
module: {
rules: [
{
test: /\.txt/,
type: 'asset/source',
}
]
},
正常import就可以了
URL资源
通用类型资源
当type:'asset'
时,webpack按照默认条件,自动在resource和inline之间进行选择。小于8kb的文件,将会视为inline模块类型,否则被视为resource模块类型。
配置项Rule.parser.dataUrlCondition
maxSize 如果一个模块源码大小小于maxSize,那么模块会被当作Base64编码的字符串注入包中,否则模块文件会被生成到输出的目标目录。
变更内联loader语法
现在建议去掉内联loader的语法。 oneof规则,只要能匹配到一个即可退出匹配。
entry高级用法
- 不使用import样式文件的应用程序中,使用值数组结构的entry。并且传入不同类型的文件,可以实现将CSS和JS(其他)文件分离在不同bundle。
- 在生产(prodution)模式中使用
MiniCssExtractPlugin
mode.exports = {
mode: 'production',
entry: {
home: ['./src/home.js', './src/home.scss'],
account: ['./src/account.js', './src/account.scss'],
},
module: {
rules: [
{
test: /\.scss$/,
use: [
// fallback to style-loader in development
process.env.NODE_ENV !== 'production'
? 'style-loader'
: MiniCssExtractPlugin.loader,
'css-loader',
'sass-loader',
],
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css',
}),
],
}
这样能dist文件夹里生成四个文件
- home.js
- home.css
- account.js
- account.css