webpack | 进阶
SourceMap 配置选择
之前我们通过webpack, 将我们的源码打包成了 bundle.js 。 试想:实际上客户端(浏览器)读取的是打包后的 bundle.js ,那么当浏览器执行代码报错的时候,报错的信息自然也是bundle的内容。
我们如何将报错信息(bundle错误的语句及其所在行列)映射到源码上?
devtool 配置
const config = {
entry: './src/index.js', // 打包入口地址
output: {
filename: 'bundle.js', // 输出文件名
path: path.join(__dirname, 'dist'), // 输出文件目录
},
devtool: 'source-map'//开发环境中默认为eval,生产环境中默认不开启
module: {
// ...
}
// ...
复制代码
执行打包后,dist 目录下会生成以 .map
结尾的 SourceMap 文件
dist
├─ avatard4d42d52.png
├─ bundle.js
├─ bundle.js.map
└─ index.html
复制代码
除了 source-map
这种类型之外,还有很多种类型可以用,例如:
了
dist
├─ js
│ ├─ cheap-module-source-map.js #............ 有对应的 .map 文件
│ ├─ cheap-module-source-map.js.map
│ ├─ cheap-source-map.js #................... 有
│ ├─ cheap-source-map.js.map
│ ├─ eval-cheap-module-source-map.js #....... 无
│ ├─ eval-cheap-source-map.js #.............. 无
│ ├─ eval-source-map.js #.................... 无
│ ├─ eval.js #............................... 无
│ ├─ hidden-source-map.js #.................. 有
│ ├─ hidden-source-map.js.map
│ ├─ inline-cheap-module-source-map.js #..... 无
│ ├─ inline-cheap-source-map.js #............ 无
│ ├─ inline-source-map.js #.................. 无
│ ├─ nosources-source-map.js #............... 有
│ ├─ nosources-source-map.js.map
│ ├─ source-map.js #......................... 有
│ └─ source-map.js.map
├─ cheap-module-source-map.html
├─ cheap-source-map.html
├─ eval-cheap-module-source-map.html
├─ eval-cheap-source-map.html
├─ eval-source-map.html
├─ eval.html
├─ hidden-source-map.html
├─ inline-cheap-module-source-map.html
├─ inline-cheap-source-map.html
├─ inline-source-map.html
├─ nosources-source-map.html
└─ source-map.html
复制代码
从目录结构我们可以很容易看出来,含 eval
和 inline
模式的都没有对应的.map
文件
eval
模式:
- 生成代码通过
eval
执行 👇🏻
- 源代码位置通过
@sourceURL
注明 👇🏻
-
无法定位到错误位置,只能定位到某个文件
-
不用生成 SourceMap 文件,打包速度快
source-map
模式:
- 生成了对应的 SourceMap 文件,打包速度慢
- 在源代码中定位到错误所在行列信息 👇🏻
eval-source-map
模式:
- 生成代码通过
eval
执行 👇🏻
2. 包含 dataUrl 形式的 SourceMap 文件
- 可以在编译后的代码中定位到错误所在行列信息
- 生成 dataUrl 形式的 SourceMap,打包速度慢
eval-cheap-source-map
模式:
- 生成代码通过
eval
执行 - 包含 dataUrl 形式的 SourceMap 文件
- 可以在编译后的代码中定位到错误所在行信息
- 不需要定位列信息,打包速度较快
eval-cheap-module-source-map
模式:
- 生成代码通过
eval
执行 - 包含 dataUrl 形式的 SourceMap 文件
- 可以在编译后的代码中定位到错误所在行信息
- 不需要定位列信息,打包速度较快
- 在源代码中定位到错误所在行信息 👇🏻
inline-source-map
模式:
- 通过 dataUrl 的形式引入 SourceMap 文件 👇🏻
devtool | build | rebuild | 显示代码 | SourceMap 文件 | 描述 |
---|---|---|---|---|---|
(none) | 很快 | 很快 | 无 | 无 | 无法定位错误 |
eval | 快 | 很快(cache) | 编译后 | 无 | 定位到文件 |
source-map | 很慢 | 很慢 | 源代码 | 有 | 定位到行列 |
eval-source-map | 很慢 | 一般(cache) | 编译后 | 有(dataUrl) | 定位到行列 |
eval-cheap-source-map | 一般 | 快(cache) | 编译后 | 有(dataUrl) | 定位到行 |
eval-cheap-module-source-map | 慢 | 快(cache) | 源代码 | 有(dataUrl) | 定位到行 |
inline-source-map | 很慢 | 很慢 | 源代码 | 有(dataUrl) | 定位到行列 |
hidden-source-map | 很慢 | 很慢 | 源代码 | 有 | 无法定位错误 |
nosource-source-map | 很慢 | 很慢 | 源代码 | 无 | 定位到文件 |
对照一下校验规则 ^(inline-|hidden-|eval-)?(nosources-)?(cheap-(module-)?)?source-map$
分析一下关键字
关键字 | 描述 |
---|---|
inline | 代码内通过 dataUrl 形式引入 SourceMap |
hidden | 生成 SourceMap 文件,但不使用 |
eval | eval(...) 形式执行代码,通过 dataUrl 形式引入 SourceMap |
nosources | 不生成 SourceMap |
cheap | 只需要定位到行信息,不需要列信息 |
module | 展示源代码中的错误位置 |
- 本地开发:
推荐:eval-cheap-module-source-map
理由:
- 本地开发首次打包慢点没关系,因为
eval
缓存的原因,rebuild 会很快 - 开发中,我们每行代码不会写的太长,只需要定位到行就行,所以加上
cheap
- 我们希望能够找到源代码的错误,而不是打包后的,所以需要加上
module
- 生产环境:
推荐:(none)
理由:
- 通过bundle和sourcemap文件,可以反编译出源码————也就是说,线上产
物有soucemap文件的话,就意味着有暴漏源码的风险。
- 我们可以观察到,sourcemap文件的体积相对比较巨大,这跟我们生产环境的追
求不同(生产环境追求更小更轻量的bundle)。
devServer
1.安装 webpack-dev-server
npm intall webpack-dev-server -D
2.配置本地服务
// webpack.config.js
const config = {
// ...
devServer: {
static: path.resolve(__dirname, 'dist'), // 输出静态文件目录,默认是输出到public
compress: true, //是否启动压缩 gzip
port: 8080, // 端口号
// open:true // 是否自动打开浏览器
},
// ...
}
module.exports = (env, argv) => {
console.log('argv.mode=',argv.mode) // 打印 mode(模式) 值
// 这里可以通过不同的模式修改 config 配置
return config;
}
为了方便,我们配置一下工程的脚本命令,在package.json的scripts里
{ //... "scripts": { //... "dev": "webpack serve --mode development" } }
3.添加响应头
headers: { 'X-Access-Token': 'abc123' },
在浏览器中右键检查(或者使用f12快捷键),在Network一栏查看任意资源访 问,我们发现响应头里成功打入了一个'X-Access-Token': 'abc123' 。
4.开启代理
我们打包出的 js bundle 里有时会含有一些对特定接口的网络请求(ajax/fetch). 要注意,此时客户端地址是在 http://localhost:3000/ 下,假设我们的接口来自 http://localhost:4001/ ,那么毫无疑问,此时控制台里会报错并提示你跨域。
module.exports = {
//... devServer: {
proxy: { '/api': 'http://localhost:4001', },
},
}
对 /api/users 的请求会将请求代理到 http://localhost:4001/api/users 。 如 果不希望传递/api,则需要重写路径:
proxy: {
'/api': {
target: 'http://localhost:4001',
pathRewrite: { '^/api': '' },
},
}
5.historyApiFallback
此时打开network,刷新并查看,就会发现问题所在———浏览器把这个路由当作了 静态资源地址去请求,然而我们并没有打包出/some这样的资源,所以这个访问无疑 是404的。 如何解决它? 这种时候,我们可以通过配置来提供页面代替任何404的静态资源响应:
historyApiFallback:true
在多数业务场景下,我们 需要根据不同的访问路径定制替代的页面,这种情况下,我们可以使用rewrites
module.exports = {
//... devServer: {
historyApiFallback: {
rewrites: [ { from: /^\/$/, to: '/views/landing.html' },
{ from: /^\/subpage/, to: '/views/subpage.html' },
{ from: /./, to: '/views/404.html' },
],
},
},
};
其余配置,如http2、https、主机号
module.exports = {
mode: 'development',
entry: './app.js',
output: {
publicPath: '/' //打包好的文件的公共路径
},
devServer: {
static: path.resolve(__dirname, './dist'),//静态文件路径
compress: false,//gzip
port: 3000,//端口
host: '0.0.0.0',//主机号,设置0.0.0.0后同一个局域网的可以访问
headers: {
'X-Access-Token': 'abc123'//请求头
},
proxy: {
'/api': 'http://localhost:9000'//代理,可rewrites
},
// https: true
http2: true,
historyApiFallback: true
},
plugins: [
new HtmlWebpackPlugin()
]
}
模块热替换与热加载
模块热替换(HMR - hot module replacement)功能会在应用程序运行过程中, 替换、添加或删除 模块,而无需重新加载整个页面
热加载(文件更新时,自动刷新我们的服务和页面,不用手动去F5刷新), 新版的webpack-dev-server 默认已经开启了热加载的功能。 它对应的参数是devServer.liveReload,默认为 true。
devServer: {
hot:false,
liveReload: false, //默认为true,即开启热更新功能。
},
注意,如果想要关掉它,要将liveReload设置为false的同时,也要关掉 hot
eslint
npm install eslint --save-dev
npm install eslint-webpack-plugin --save-dev
后把插件添加到你的 webpack 配置。例如:
const ESLintPlugin = require('eslint-webpack-plugin');
module.exports = {
// ...
plugins: [new ESLintPlugin(options)],
// ...
};
Webpack 模块
1、webpack 模块
何为 webpack 模块 能在webpack工程化环境里成功导入的模块,都可以视作webpack模块。 与 Node.js 模块相比,webpack 模块 能以各种方式表达它们的依赖关系。下面是 一些示例:
- ES2015 import 语句
- CommonJS require()
- 语句 AMD define 和 require
- 语句 css/sass/less 文件中的 @import 语句
- stylesheet url(...) 或者 HTML <img src=...> 文件中的图片链接
支持的模块类型 Webpack 天生支持如下模块类型:
- ECMAScript 模块
- CommonJS 模块
- AMD 模块
- Assets WebAssembly 模块
而我们早就发现——通过 loader 可以使 webpack 支持多种语言格式和预处理器语法编写的模块。loader 向 webpack 描述了如何处理非原生模块,并将相关依赖引入到你 的 bundles中。包括且不限于:
TypeScript(.ts) Sass(.sass) Less(.less) JSON(.json)
2、compiler与Resolvers
在我们运行webpack的时候(就是我们执行webpack命令进行打包时),其实就是相当 于执行了下面的代码:
const webpack = require('webpack');
const compiler = webpack({
// ...这是我们配置的webpackconfig对象
})
webpack的Resolvers解析器的主体功能就是模块解析,它是基于 enhanced-resolve 这个包实现的。换句话讲,在webpack中,无论你使用怎样的 模块引入语句,本质其实都是在调用这个包的api进行模块路径解析。
模块解析(resolve)
webpack通过Resolvers实现了模块之间的依赖和引用。举个例子:
import _ from 'lodash'; // 或者 const add = require('./utils/add');
所引用的模块可以是来自应用程序的代码,也可以是第三方库。 resolver 帮助 webpack 从每个 require/import 语句中,找到需要引入到 bundle 中的模块代码。 当打包模块时,webpack 使用 enhanced-resolve 来解析文件路径。
1、webpack中的模块路径解析规则
通过内置的enhanced-resolve,webpack 能解析三种文件路径:
绝对路径
import '/home/me/file'; import 'C:\\Users\\me\\file'; 注意:/指向根目录
相对路径
import '../utils/reqFetch'; import './styles.css';
模块路径
import 'module'; import 'module/lib/file';
(node_modules里的模块已经 被默认配置了)。 你可以通过配置别名的方式来替换初始模块路径, 具体请参照下面 resolve.alias 配置选项
resolve解析优化
alias:自定义配置模块路径
extentions:定义 文件/目录 的路径 解析规则。
const path = require('path')
module.exports = {
mode: 'development',
entry: './src/app.js',
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'components': path.resolve(__dirname,'src/components'),
"@utils": path.resolve(__dirname, 'src/utils/')
},
// extensions: ['.js', '.json', '.wasm'] webpack 默认配置
extensions: ['.json', '.js', '.vue']
}
}
配置完成之后,我们在项目中就可以
// 使用 src 别名 ~
import '~/fonts/iconfont.css'
// 使用 src 别名 @
import '@/fonts/iconfont.css'
// 使用 components 别名
import footer from "components/footer";
如果用户引入模块时不带扩展名,例如
import file from '../path/to/file';
复制代码
那么 webpack 就会按照 extensions 配置的数组从左到右的顺序去尝试解析模块
externals
externals
配置选项提供了「从输出的 bundle 中排除依赖」的方法。减小bundle的体积,从而把一些不变的第三方库用cdn的形式引入进 来,比如jQuery:
例如,从 CDN 引入 jQuery(本地node_modules中不含这个包),而不是把它打包:
- 引入链接
<script
src="https://code.jquery.com/jquery-3.1.0.js"
integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk="
crossorigin="anonymous"
></script>
复制代码
- 配置 externals
const config = {
//...
externals: {
jquery: 'jQuery',
},
};
复制代码
- 使用 jQuery
import $ from 'jquery';
$('.my-element').animate(/* ... */);
复制代码
构建结果分析
借助插件 webpack-bundle-analyzer 我们可以直观的看到打包结果中,文件的体积大小、各模块依赖关系、文件是够重复等问题,极大的方便我们在进行项目优化的时候,进行问题诊断。
- 安装
$ npm i -D webpack-bundle-analyzer
复制代码
- 配置插件
// 引入插件
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
const config = {
// ...
plugins:[
// ...
// 配置插件
new BundleAnalyzerPlugin({
// analyzerMode: 'disabled', // 不启动展示打包报告的http服务器
// generateStatsFile: true, // 是否生成stats.json文件
})
],
};
复制代码
- 修改启动命令
"scripts": {
// ...
"analyzer": "cross-env NODE_ENV=prod webpack --progress --mode production"
},
复制代码
- 执行编译命令
npm run analyzer
打包结束后,会自行启动地址为 http://127.0.0.1:8888
的 web 服务,访问地址就可以看到
Tree-shaking
tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码 (dead-code)。
Webpack 没看到你使用的代码。Webpack 跟踪整个应用程序的 import/export 语句,因此,如果它看到导入的东西最终没有被使用,它会认为那 是未引用代码(或叫做“死代码”—— dead-code ),并会对其进行 tree-shaking 。
webpack 的生产环境默认支持Tree-shaking
缩小范围
在配置 loader 的时候,我们需要更精确的去指定 loader 的作用目录或者需要排除的目录,通过使用 include
和 exclude
两个配置项,可以实现这个功能,常见的例如:
include
:符合条件的模块进行解析exclude
:排除符合条件的模块,不解析
例如在配置 babel 的时候
const path = require('path');
// 路径处理方法
function resolve(dir){
return path.join(__dirname, dir);
}
const config = {
//...
module: {
noParse: /jquery|lodash/,
rules: [
{
test: /\.js$/i,
include: resolve('src'),
exclude: /node_modules/,
use: [
'babel-loader',
]
},
// ...
]
}
};
多进程配置
thread-loader
配置在 thread-loader 之后的 loader 都会在一个单独的 worker 池(worker pool)中运行
- 安装
$ npm i -D thread-loader
复制代码
- 配置
const path = require('path');
// 路径处理方法
function resolve(dir){
return path.join(__dirname, dir);
}
const config = {
//...
module: {
noParse: /jquery|lodash/,
rules: [
{
test: /\.js$/i,
include: resolve('src'),
exclude: /node_modules/,
use: [
{
loader: 'thread-loader', // 开启多进程打包
options: {
worker: 3,
}
},
'babel-loader',
]
},
// ...
]
}
};
压缩 JS
在生成环境下打包默认会开启 js 压缩,但是当我们手动配置
optimization
选项之后,就不再默认对 js 进行压缩,需要我们手动去配置。
因为 webpack5 内置了terser-webpack-plugin 插件,所以我们不需重复安装,直接引用就可以了,具体配置如下
const TerserPlugin = require('terser-webpack-plugin');
const config = {
// ...
optimization: {
minimize: true, // 开启最小化
minimizer: [
// ...
new TerserPlugin({})
]
},
// ...
}