1. source-map
我们在开发中需要经常用到debug功能。之前我们通过webpack将我们的源码打包成了bundle.js,实际上客户端(浏览器)读取的是打包后的bundle.js ,那么当浏览器执行代码报错的时候,报错的信息自然也是bundle的内容。
我们如何将报错信息(bundle错误的语句及其所在行列)映射到源码上?答案就是通过source-map。
webpack已经内置了source-map,我们通过简单的配置就能够开启它:
devtool: 'source-map'
其中source-map有以下前缀可以选择:
-
inline:不生成映射关系文件,打包进main.js
-
cheap: 1.只精确到行,不精确到列,打包速度快 2.只管业务代码,不管第三方模块
-
module:不仅管业务代码,而且管第三方代码
-
eval:执行效率最快,性能最好
不过需要注意,生产环境下一般不需要开启source-map ,主要有两点原因:
- 通过bundle和sourcemap文件可以反编译出源码,有暴露源码的风险
- sourcemap文件的体积相对比较巨大,这跟我们生产环境的追求不同。(生产环境追求更小更轻量的bundle)
2. devServer
开发环境下,我们往往需要启动一个web服务,方便我们模拟一个用户从浏览器中访问我们的web服务,读取我们的打包产物,以观测我们的代码在客户端的表现。Webpack内置了这个功能:
devServer: {
// 路径
static: path.resolve(__dirname, './dist'),
// 是否在服务端开启gzip压缩代码
compress: false,
// 启动端口
port: 3000,
// 使得自己的devServer服务能被同一局域网内的人通过ip访问到
host: '0.0.0.0',
// 响应头
headers: {
'X-Access-Token': 'abc123'
},
// 代理跨域 如果路径包含/api则代理到9000端口
proxy: {
'/api': 'http://localhost:9000'
},
// 是否开启https 两种方法
// https: true
http2: true,
// 404时跳回指定的地址 设置为true即跳回根页面 可以根据需求作更多配置
historyApiFallback: true,
// 模块热替换 默认为true
// 实现在应用程序运行过程中,增删改模块而无需重新加载整个页面
hot: false,
// 模块热加载 默认为true
// 实现文件更新时,自动刷新我们的服务和页面
liveReload: false
},
重点掌握通过proxy代理跨域,通过historyApiFallback跳转地址,模块热替换和热加载等内容。
proxy实现跨域的原理是什么?
proxy工作原理实质上是利用http-proxy-middleware这个http代理中间件,实现请求转发给其他服务器。
在开发阶段, webpack-dev-server 会启动一个本地开发服务器,所以我们的应用在开发阶段是独立运行在 localhost的一个端口上,而后端服务又是运行在另外一个地址上
所以在开发阶段中,由于浏览器同源策略的原因,当本地访问后端就会出现跨域请求的问题
通过设置webpack proxy实现代理请求后,相当于浏览器与服务端中添加一个代理者
当本地发送请求的时候,代理服务器响应该请求,并将请求转发到目标服务器,目标服务器响应数据后再将数据返回给代理服务器,最终再由代理服务器将数据响应给本地。
注意:服务器与服务器之间请求数据并不会存在跨域行为,跨域行为是浏览器安全策略限制
2.1 模块热加载热替换js
上面的config配置可以实现CSS的热加载热替换,如果要实现js中的热更新还要在js文件中写入对应热更新代码,比如:
if (module.hot) {
module.hot.accept(’./library.js’, function() {
// Do something with the updated library module…
});
}
library.js
document.querySelector('#box').innerHTML = '<input type="text" value="test23456" />'
3. eslint
eslint是用来扫描我们所写的代码是否符合规范的工具。 往往我们的项目是多人协作开发的,我们期望统一的代码规范,这时候可以让eslint来对我们进行约束。 严格意义上来说,eslint配置跟webpack无关,但在工程化开发环境中,它往往是不可或缺的。
npm i eslint -D
//安装好后执行
npx eslint --init
会生成这么一个文件:
{
"env": {
"browser": true,
"es2021": true
},
"extends": "airbnb-base",
"overrides": [
],
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"rules": {
}
}
我们现在创建文件夹src,随便写一个文件app.js:
console.log('hello eslint');
然后在控制台中执行检察:
npx eslint ./src
可以在配置文件中忽略对no-console的提示:
"rules": {
"no-console": 0
}
但是这样检查太麻烦了,我们可以直接通过vscode的扩展下载eslint:
此时如果犯语句后不写分号之类的错误vscode就会自动报错了
如果即不想用命令行这么麻烦,又不想安装插件,那么还可以通过webpack来实现:
在webpack.config.js中这么写:
module: {
rules: [
{
test: /\.js$/,
use: ['babel-loader', 'eslint-loader'],
},
],
},
4. 外部扩展
通过externals实现通过外部cdn加载第三方库
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode: 'development',
entry: './app.js',
plugins: [
new HtmlWebpackPlugin()
],
// 类型
externalsType: 'script',
externals: {
// 链接和别名
jquery: [
'https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js',
'$'
]
}
}
5. webpack + web worker
有时我们需要在客户端进行大量的运算,但又不想让它阻塞我们的js主线程。你可能第一时间考虑到的是异步。但事实上,运算量过大(执行时间过长)的异步也会阻塞js事件循环,甚至会导致浏览器假死状态。这时候,HTML5的新特性WebWorker就派上了用场。
html5之前,打开一个常规的网页,浏览器会启用几个线程?
一般而言,至少存在三个线程(公用线程不计入在内):
分别是js引擎线程(处理js)、GUI渲染线程(渲染页面)、浏览器事件触发线程(控制交互)。
webWorkers提供了js的后台处理线程的API,它允许将复杂耗时的单纯js逻辑处理放在浏览器后台线程中进行处理,让js线程不阻塞UI线程的渲染。
new Worker("someWorker.js");
但是常规模式下,webpack工程化环境只会打包出一个bundle.js,那我们的worker脚本怎么办?
webpack5内置了处理这种情况的功能:
创建一个脚本worker.js
self.onmessage = ({ data: { question } }) => {
self.postMessage({
answer: 42,
})
}
在index.js中使用
const worker = new Worker(new URL('./work.js', import.meta.url));
worker.postMessage({
question:
'hi,那边的worker线程,请告诉我今天的幸运数字是多少?',
});
worker.onmessage = ({ data: { answer } }) => {
console.log(answer);
};
6. 多页面应用
6.1 entry
entry: {
main: {
import: ['./src/app2.js', './src/app.js'],
dependOn: 'lodash',
filename: 'chanel1/[name].js'
},
main2: {
import: './src/app3.js',
dependOn: 'lodash',
filename: 'chanel2/[name].js'
},
lodash: {
import: 'lodash',
filename: 'common/[name].js'
}
}
6.2 plugins
plugins: [
new HtmlWebpackPlugin({
title: '多页面应用',
template: './index.html',
inject: 'body',
filename: 'chanel1/index.html',
chunks: ['main', 'lodash'],
publicPath: 'http://www.b.com/'
}),
new HtmlWebpackPlugin({
template: './index2.html',
inject: 'body',
filename: 'chanel2/index2.html',
chunks: ['main2', 'lodash'],
publicPath: 'http://www.a.com/'
})
]
7. tree-shaking
7.1 tree-shaking
Tree Shaking 指的就是当我引入一个模块的时候,我不引入这个模块的所有代码,我只引入我需要的代码,这就需要借助 webpack 里面自带的 Tree Shaking 这个功能来帮我们实现。它依赖于 ES2015 模块语法的静态结构特性,例如import和export。
现在写一个math.js
export const add = (x, y) => {
return x + y
}
export const minus = (x, y) => {
return x - y
}
和一个app.js
import { add, minus } from './math'
console.log(add(5, 6))
会发现app.js中没用到minus方法,然后我们来写webpack.config.js,引入一个optimization属性称为usedExports:
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode: 'development',
entry: './src/app.js',
plugins: [
new HtmlWebpackPlugin()
],
optimization: {
// 该属性实现tree-shaking
usedExports: true
},
}
假设不加该属性,则add和没被使用的minus都会被暴露:
假如添加该属性,则只有add会被暴露。且minus被提示未被使用:
但是如果导入某个第三方库比如axios而不去使用,webpack还是会正常打包,无法识别到该库被导入后未被使用。
7.2 sideEffects
注意 Webpack 不能百分百安全地进行 tree-shaking。有些模块被引入后不希望进行tree-shaking,比如css模块和全局js配置模块。因为这些模块没有导出任何模块,那么可能会导致被webpack摇掉,导致bug。
可以在package.json中设置sideEffects的值:
- true:所有文件都有副作用,都不能被tree-shaking
- false:所有文件都没有副作用,都能被tree-shaking
- 数组:数组中的文件不能被tree-shaking,别的都可以
比如现在我们不想css模块被摇掉,我们就可以写:
"sideEffects": [
"*.css"
]
8. 常见问题
8.1 loader和plugin的概念和区别?
Loader本质就是一个函数,在该函数中对接收到的内容进行转换,返回转换后的结果。因为 Webpack 只认识 JavaScript,所以 Loader 就成了翻译官,对其他类型的资源进行转译的预处理工作,运行在打包之前。
默认情况下,在加载模块的时候,webpack只支持对js文件打包。像遇到css、sass、png等这些类型的文件的时候,webpack则无能为力,这时候就需要配置对应的loader进行文件内容的解析。因此,loader运行在NodeJS中。仅仅只是为了打包。
常见配置方式:
module: {
rules: [
{
test: /\.(css|less)$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader']
},
{
test: /\.(csv|tsv)$/,
use: 'csv-loader'
},
{
test: /\.xml$/,
use: 'xml-loader'
},
]
},
Plugin就是插件,可以扩展 Webpack 的功能。在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。plugin不仅只局限在打包,资源的加载上,它的功能要更加丰富。从打包优化和压缩,到重新定义环境变量,功能强大到可以用来处理各种各样的任务。
常见用法:
plugins: [
new HtmlWebpackPlugin({
// html模板
template: './index.html',
// 输出文件名
filename: 'app.html',
// script标签位置(默认在head中)
inject: 'body'
}),
new MiniCssExtractPlugin({
filename: 'styles/[contenthash].css'
})
],
optimization: {
minimizer: [
new CssMinimizerPlugin()
]
}
8.2 常见的loader和plugin有哪些?
loader
- style-loader:将css添加到DOM的内联样式标签style里,然后通过 dom 操作去加载 css
- css-loader/less-loader:前者允许导入css,后者允许导入less并将less转换为css
- file-loader:分发文件到output目录并返回相对路径
- html-minify-loader:压缩HTML
- babel-loader:用babel来转换ES6到ES5
- awesome-typescript-loader:将 TypeScript 转换成 JavaScript
- eslint-loader:通过 ESLint 检查 JavaScript 代码
- cache-loader:可以在一些性能开销较大的 Loader 之前添加,目的是将结果缓存到磁盘里
plugin
- html-webpack-plugin:打包后自动创建html
- web-webpack-plugin:可方便地为单页应用输出 HTML,比 html-webpack-plugin 好用
- terser-webpack-plugin:压缩js,支持es6
- css-minimizer-webpack-plugin:压缩css
- compression-webpack-plugin:压缩文件
- mini-css-extract-plugin: 分离样式文件,CSS 提取为独立文件,支持按需加载
- serviceworker-webpack-plugin:为网页应用增加离线缓存功能
- clean-webpack-plugin: 目录清理
8.3 五个核心概念
1、Entry(入口):指示webpack以哪个文件为入口起点打包。分析构建内部依赖图。
2、Output(出口):指示webpack打包后的资源bundles输出到哪里去,以及如何命名。
3、Loader:Loader让webpack能够去处理那些非javaScript文件【图片文件、html文件、样式文件】(webpack自身只理解javascript)
4、Plugins(插件):可以用于执行范围更广的任务。插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。
5、Mode(模式):指示webpack使用相应模式的配置。
8.4 webpack常见性能优化
(1) 缩小loader打包范围
通过include和exclude来缩小loader的处理范围
module.exports = {
mode: 'development',
mdule: {
rules: [
{
test: /\.js$/,
include: /\.\src/,//应用于src目录下
exclude: /node_modules/, //排除某些文件或目录
use: ["xx-loader"]
}
],
}
}
(2) 缓存loader结果
当文件内容不变时,经过相同的loader解析后,解析结果并没有改变,所以这个时候将loader的解析结果保存下来,让后续的解析直接使用保存的结果:
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: [{
loader: 'cache-loader',
cacheDirectory: './cache'
}, ...loaders]
}
]
}
}
(3) 多线程打包
通过thread-loader开启一个线程池,后续会把loader放进线程池的线程中运行以提高运行效率。
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: [{
loader: 'cache-loader',
cacheDirectory: './cache'
}, 'thread-loader']
}
]
}
}
(4) 代码分离
默认情况下,所有的JavaScript代码(业务代码、第三方依赖、暂时没有用到的模块)在首页全部都加载,就会影响首页的加载速度。
将代码分离到不同的bundle中,之后我们可以按需加载,或者并行加载这些文件。
这个在之前笔记里写了
(5) 缓存第三方库
笔记写了
(6) tree shaking
笔记写了
(7) 懒加载
笔记写了
(8) 代码压缩 分离CSS 等等...
- terser-webpack-plugin:压缩js,支持es6
- css-minimizer-webpack-plugin:压缩css
- compression-webpack-plugin:压缩文件
- mini-css-extract-plugin: 分离样式文件,CSS 提取为独立文件,支持按需加载
8.5 webpack HMR
启动Webpack Dev Server:
开发者启动Webpack Dev Server,该服务器集成了webpack-dev-middleware和webpack-hot-middleware等中间件,用于处理静态文件服务和热更新逻辑。
在启动命令中,通常会包含--hot或hot: true等选项来启用HMR功能。
监听文件变化:
Webpack Dev Server通过其内部机制监听项目文件的变化。这种监听是独立于Webpack的watch mode的,因此即使Webpack没有处于watch模式,WDS也能正常工作。
重新编译:
当检测到文件变化时,Webpack Dev Server会触发Webpack重新编译这些变化的文件。编译结果通常不会直接写入磁盘,而是保存在内存中,以减少磁盘I/O操作。
建立WebSocket连接:
Webpack Dev Server与浏览器之间通过WebSocket建立了一个长连接。这个连接用于实时传递编译过程中的状态信息和模块更新信息。
推送更新信息:
Webpack Dev Server通过WebSocket将新模块的hash值等信息推送给浏览器。这些信息包括哪些模块发生了变化,以及这些模块的更新内容。
浏览器端处理:
浏览器端接收到WebSocket传递的信息后,会解析出需要更新的模块信息。
浏览器通过Ajax请求获取包含更新模块信息的JSON文件(manifest文件),该文件列出了所有需要更新的模块及其hash值。
接着,浏览器再通过JSONP请求获取具体的模块更新代码(update chunk)。
模块热替换:
浏览器端的HotModuleReplacement.runtime(HMR运行时)接收到更新代码后,会对比新旧模块的hash值,决定是否进行替换。
如果决定替换,HMR运行时会检查模块间的依赖关系,并更新模块及其依赖引用。