一直都在用使用 webpack 打包vue应用,但是从来没有详细了解过 webpack 是什么,所以用了4天的时间看完了一套 Webpack 视频教程,通过一步步的代码实现,我现在对 webpack 有了比较全面而清晰的了解,遂将自己学到的知识整理成这篇文章,希望能帮到也在学习 webpack 的你
概念
webpack 是什么
Webpack 是一个静态模块打包工具
在实际的项目开发过程中,会产生很多文件,比如 js 文件、css 文件、html文件,还有 ttf 文件、png 文件等等,这些文件都是项目要用到的文件而且它们之间存在依赖关系;比如一个 html 文件引入了一个 css 样式文件、一个外部的 js 文件,这些依赖关系往往很复杂,造成的结果就是,整个项目运行之前,要不断地去查找这些松散的文件,然后把它们整合到一起然后才能运行。
我们可以把这些松散的文件看成一个个静态模块,为了减少项目运行过程中耗费在聚合这些文件上的时间,我们要想个办法把**该合并的合并(减少 http 请求的次数),该压缩的压缩(去除代码中无用的字符,减少体积),该转换的转换(比如把 sass 转为 css)**等等,这一系列优化工作我们可以手动完成,无非就是一个一个文件的去处理,最后可以得到想要的项目,但是工作量将会非常大,而且非常低效
webpack 就提供了优化项目的能力,它可以基于项目中每个文件之间的关系,构建出一个依赖关系图,据此就能找到所有的静态资源;然后,通过调用 loader 、plugin 等自带的处理工具,就能执行上述所有的优化操作,最后将处理好的项目输出到一个文件中,最重要的是——这一切全是自动化的,具体细节我们不需要关心,我们只要给 webapck 指定一个入口(entry)(从哪里开始处理文件)和出口(output)(在哪里输出处理好的文件),再配置好 loader 或 plugin就可以了!
webpack 术语解释
module
在模块化编程中,开发者将程序分解成离散功能块(discrete chunks of functionality),并称之为模块。
每个模块具有比完整程序更小的接触面,使得校验、调试、测试轻而易举。 精心编写的模块提供了可靠的抽象和封装界限,使得应用程序中每个模块都具有条理清楚的设计和明确的目的。
Node.js 从最一开始就支持模块化编程。然而,在web 中模块化的支持正缓慢到来。在 web 中存在多种支持 JavaScript 模块化的工具,这些工具各有优势和限制。webpack 基于从这些系统获得的经验教训,并将模块的概念应用于项目中的任何文件。
—— Webpack 官方文档
我个人的理解,在 webpack 中 module 的概念与 ES6 中的 module 、CommonJS 中的 module 都是相似的,它指的就是模块化
chunk
这是 webpack 特定的术语,它被用在 webpack 内部,来管理构建(building)的过程。bundle 由 chunk 组成,其中有几种类型(例如,入口 chunk(entry chunk) 和子 chunk(child chunk))。通常 chunk 会直接对应所输出的 bundle,但是有一些配置并不会产生一对一的关系
—— Webpack 官方文档
webpack会从你定义的入口开始,一个个的找到 module 然后构建依赖树,最终输出一个 chunk。它就像一个装着很多文件的文件袋,里面的文件就是各个模块,Webpack在外面加了一层包裹,从而形成了chunk。
bundle
由多个不同的模块生成,bundles 包含了已经过加载和编译的最终源文件版本
bundle 其实就是一个chunk打包后的输出产物,每个工程里都可以定义多个 entry ,那么就有多个 bundle。
安装
安装 webpack 有两种方式:全局安装和本地安装(这些都是 npm 的知识,可以参考这篇文章) ,我的结论是——本地安装
进入项目文件夹,然后运行下面的命令
npm i webpack webpack-cli -D
它安装了两个包,webpack 是核心包,webpack-cli 是通过命令行操作 webpack 执行的包
核心
Entry
entry的作用是告诉 webpack 从哪个文件开始处理,从而分析构建出依赖关系图
Output
output 的作用是告诉 webpack 构建并打包成功之后,往哪里输出打包好的资源文件
Loader
webpack 本身只能理解 Javascript,所以 loader 的作用是赋予 webpack 处理非 Javascript 文件的能力,也就是“翻译能力”,比如处理 less、sass、stylus、ts等等
Plugins
执行一些 loader 无法完成的更复杂的任务,比如代码的压缩,静态资源的打包操作等等
Mode
使 webpack 启动不同的的模式:
- development——能让代码本地调试并运行的环境(不压缩)
- production——能让代码优化上线运行的环境(压缩)
开发环境用法
打包命令
首先,新建一个目录作为你的项目的根目录,再创建src目录,然后在其中新建 index.js 文件
webpack ./src/index.js -o ./build/built.js --mode=development
在开发环境下,使用这条命令来告诉 webpack :
- 路径 ./src/index.js 被定义为入口文件,webpack 就从这个 index.js 文件开始,构建依赖关系图
- 参数
-o表示的就是output出口的意思 - 路径 ./build/built.js 被定义为出口文件,webpack 打包完成后,就会输出到这个 built.js 文件
- 选项 --mode=development 定义当前环境为开发环境,也就是代码只会被打包,而不会被压缩和优化,另一个选项是 --mode=production 定义环境为生产环境,也就是代码会被打包、压缩、优化,准备发布到线上运行了
处理 js、json
webpack 本身只能理解 Javascript,所以,不需要任何额外插件或loader 就能处理 js 文件
// data.json
{
"name":"jack",
"age": 18
}
// index.js
import data from './data.json'
console.log(data)
function add(x, y) {
return x+y
}
console.log(add(1,2))
对上面的文件执行打包后,浏览器控制台就会打印出 add函数的返回值和 json 中的数据,这说明 webpack 成功打包了 js、json 文件。在 出口文件 built.js 中也可以看到打包后的代码,所有的代码都被打包在了一个 eval 函数中,包括注释与缩进
webpack.config.js
我们每次都使用这样的上面的命令就会太繁琐了,因为还要带上路径参数,还要声明使用的 loader 参数,为了方便,我们在根目录创建一个配置文件来存储我们的配置,然后在终端使用webpack命令就能执行打包
// webpack.config.js
const { resolve } = require('path')
module.exports = {
entry: './src/index.js',
output: {
// 输出文件名
filename: 'built.js',
/*
* 输出路径
* 为了保证不出错,需要使用绝对路径,就需要导入 node.js 的 path 模块
* 使用其中的 resolve 来拼接路径
*/
path: resolve(__dirname, 'build') // __dirname 是 node.js 内置变量,代表当前文件的绝对路径
},
// loader 的配置,定义了使用哪些 loader 来处理的不同类型的文件
module:[
// 多个loader的写法
{
// 处理规则
test: '',
// 声明要使用的loader
use:['loader1', 'loader2',.....]
},
//单个loader的写法
{
test:''
loader:''
}
],
// 插件,用于处理更为复杂的打包操作,比如压缩、优化
plugins:[]
// 模式,此为必填项,可选值:production,代表生产环境
mode: 'development'
}
处理 css 文件
css-loader 和 style-loader 用于处理 css 文件。首先要安装它们
npm i css-loader style-loader -D
然后在 module 的 rules中定义如何去使用它们:
// webpack.config.js
const { resolve } = require('path')
module.exports = {
// loader 的配置,用于处理非 js 文件,比如 css、img
module: {
rules: [
{
// 使用正则表达式来指定要匹配文件的类型
test: /\.css$/,
// 使用哪些 loader
use: [
// 执行顺序是 从下到上(从右到左)
'style-loader', //生成 style 标签,将样式插入其中
'css-loader' // 将 css 文件编译成 commonJS 模块加载到 js 中,里面的内容是 样式字符串
]
}
]
}
}
然后使用命令 webpack 打包运行,查看出口文件 built.js,其中的 eval 函数记录了 css 代码被打包成了字符串形式
- css-loader 的作用是将 css代码转写成样式字符串
- style-loader 的作用是创建
<style>标签,将样式字符串插入到HTML页面 head 标签中
处理 图片
url-loader 用于处理 CSS 文件中引入的图片,html-loader 用于处理HTML中引入的图片
要先安装 file-loader,因为 url-loader 依赖于它
npm i url-loader file-loader html-loader -D
同样,在 module 的 rules中定义如何去使用它们:
// webpack.config.js
const { resolve } = require('path')
module.exports = {
module: {
rules: [
// 处理 图片 的loader(url-loader 和 file-loader)
{
test: /\.(jpg|png|gif)$/,
// url-loader 只能处理 CSS 中引入的图片,默认以 commonJS 模块处理
loader: 'url-loader',
options: {
/**图片小于 8kb 就会被转为 base64 格式
* 优点:减少http请求,减轻服务器压力
* 缺点:图片体积会变大
* */
limit: 8 * 1024
}
},
{
test: /\.html$/,
// 只能处理 HTML中的图片,处理成commonJS 模块,然后交给 url-loader 处理
loader: 'html-loader'
}
]
}
}
打包之后的图片,会被重命名,新的命名是由 webpack 生成的唯一的32位hash值,例如:
830bf3c820562c180c8975c2a0d00557.jpg
为了方便,我们不需要给图片这么长的命名,所以我们修改下配置文件,在 options中的limit 后面增加一个 name 选项
// hash:10 表示只取前10位值,ext 表示源文件的后缀名
name:'[hash:10].[ext]'
再次打包以后,新的图片的名字就会更短:
830bf3c820.jpg
处理 html 文件
html-webpack-plugin 是一个插件,专门用于处理 HTML 文件
npm i html-webpack-plugin -D
// webpack.config.js
const { resolve } = require('path')
// HtmlWebpackPlugin是一个构造函数
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
plugins: [
/**
* html-webpack-plugin
* 功能:默认会创建一个空的HTML,自动引入打包输出的所有资源(js/css)
* 需求:需要有一个已经写好的 html 作为模板
*/
new HtmlWebpackPlugin({
// 复制一个 './src/index.html' 文件,保持原有的结构,自动写入外部引用
template: './src/index.html'
})
],
mode: 'development'
}
使用命令 webpack 执行打包后,会在出口 build 文件夹下面生成一个新的HTML文件,其中自动引入了 built.js
处理其他文件
其他文件就是指:除了 css、html、js 以外的文件,比如 font(字体)、SVG图片、icon(图标)等等
这些文件,统一用 file-loader 来处理
module.exports = {
module: {
rules: [{
// 使用 exclude 排除不需要打包的资源
exclude: /\.(css|html|js)$/,
loader: 'file-loader',
options: {
name: '[hash:10].[ext]'
}
}]
}
}
webpack-dev-server
用于在开发时,自动编译、自动打包项目、自动打开并显示结果到浏览器(省去了我们手动打包的麻烦),它会在内存中进行编译,所以速度非常快
// 安装 dev derver 功能
npm i webpack-dev-server -D
dev server 不属于 webpack 的五大核心之一,它要单独配置,代码如下:
module.exports = {
entry: ...,
output: ...,
module: [],
plugin: ...,
mode: ...,
devServer: {
contentBase: resolve(__dirname, 'build'), // 项目构建后的输出路径
compress: true, // 是否压缩代码
port: 3000, // 服务器的端口号
open: true // 是否自动打开页面到浏览器中
}
}
使用指令来启动 devServer 服务
npx webpack-dev-server
最后,由于 devServer 只在内存中编译你的代码,所以它在本地不会有任何输出,你可以删除 /built 文件夹,然后再次运行 devServer,你会发现一切正常,只是本地没有任何文件会被创建
生产环境用法
打包CSS为单独文件
使用 style-loader 处理CSS文件,会将CSS转为字符串写入Javascript代码中,如果CSS的体积过大,那么页面就会产生闪屏现象,而且也会导致Javascript文件体积增大,也增加了浏览器解析js的时间
mini-css-extract-plugin 是一个Plugin,它可以将CSS打包成单独的文件
npm i mini-css-extract-plugin -D
使用 mini-css-extract-plugin 以及 MiniCssExtractPlugin.loader
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
module: {
rules: [
{
test: /\.css$/,
// 不再需要style-loader将css字符串注入Javascript,取代 style-loader
use: [MiniCssExtractPlugin.loader, 'css-loader']
}
]
},
plugins: [
new MiniCssExtractPlugin({
// 给打包出的CSS文件重命名
filename: 'css/built.css'
})
]
}
运行打包命令后,查看 build 文件夹,就能看到输出的 css 文件
CSS兼容性处理
我们知道,有些浏览器是不完全支持所有 css 属性的,所以我们需要写上适配不同浏览器的 css 属性,比如加上前缀moz-,就表示适配 FireFox 浏览器;但是浏览器的种类很多,我们不可能每一个属性都手动去写,因为工作量将会巨大。
webpack 提供了 postcss-loader 和 postcss-preset-env 来处理 css 的兼容性,它们能在输出的 css 中帮我们加上这些兼容的属性。首先安装它们:
npm i postcss-loader postcss-preset-env -D
现在,我们要去package.json 文件中定义一个属性 browserlist ,它规定了如何去兼容浏览器,下面是一段示例代码:
// package.json
"browserslist": {
// 开发环境
"development": [
"last 1 chrome version", // 适配最新版本的 chrome 浏览器
"last 1 firefox version", // 适配最新版本的 firefox 浏览器
"last 1 safari version" // 适配最新版本的 safari 浏览器
],
// 生产环境
"production": [
">0.2%", // 适配市面上99.8%以上的浏览器
"not dead", // 排除以及淘汰的浏览器,比如 IE5
"not op_mini all"
]
},
接下来我们还要设置一下 node.js 环境变量,因为默认情况下会应用的browserlist配置是生产环境配置,也就是上面的 development,我们先来看看开发环境下,postcss会如何处理 css 兼容性
// webpack.config.js
process.env.NODE_ENV = 'development'
开发环境下经过 css 兼容性处理的输出文件如下:
接着,我们再来看看生产环境下,css 兼容性处理的情况,我们将 package.json 中的process.env.NODE_ENV = 'development'注释掉,然后再次打包,就会输出如下的样式:
通过开发环境和生产环境对比,我们可以发现,使用生产环境的 browserlist 配置,处理 css 兼容性的时候会更加的全面和严格
压缩CSS
有时候 css 样式可以多达几百行,其中有不少空白字符,为了精简 css 文件的体积,我们需要将其压缩,删去空白字符。optimize-css-assets-webpack-plugin 插件就提供了压缩 css 的功能
npm i optimize-css-assets-webpack-plugin -D
直接调用这个插件就可以了:
// webpack.config.js
plugins: [
new OptimizeCssAssetsWebpackPlugin()
]
这时,你可以打开输出的 CSS 文件,里面的代码非常紧凑,只有一行,没有任何多余的字符
JS语法检查
Javascript 语法检查的作用就是规范化我们的源代码,检查出其中的语法错误,统一变成风格,比如字符串,要么就全部使用双引号,要么就全部使用单引号,不要一会儿用这个一会儿用那个,导致代码看起来非常不美观、不协调。
webpack 中运行 JS 语法检查用主要靠 ESLint
npm i eslint-loader eslint eslint-config-airbnb-base eslint-plugin-import -D
为了获得统一的代码风格,我们就需要一个通用的 模板 规范化我们的代码,通常呢,我们会使用airbnb-base这个代码风格,具体细节,可以查看它的文档
安装完成后,我们还需要在 package.json 中配置 eslint :
"eslintConfig": {
"extends": "airbnb-base", // 继承于 airbnb-base
"env": {
"browser": true
}
}
接下来在 webpack.config.js 文件中,使用 eslint-loader
module: {
rules: [
{
test: /\.js$/, // 只检查 js 文件
exclude: /node_modules/, // 排除掉 node 包,因为它们都是第三方js库
loader: 'eslint-loader'
}
]
}
现在我们来写一段Javascript代码,故意写成风格混乱,看起来像这样:
然后运行 webpack 打包,我们可以控制台会报错,然后输出如下信息:
上面展示了部分输出,它们都来自于 ESLint 语法检查器,提示你,哪里不该换行却换行了,哪里使用了多余的空格;但手动的一行行的去修复太麻烦了,所以我们可以让 ESlint 自动修复它,要在webpack.config.js 文件中加上 options 配置:
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'eslint-loader',
options: {
fix: true // 启用自动修复
}
}
]
}
这时,打包以后,我们再打开 index.js 文件,就可以看到,里面的代码已经被自动的规范化了
JS兼容性处理
基本 js 兼容性处理
我们知道,ES6 语法目前只有主流的浏览器(Chrome、FireFox)才支持,而且支持的还不是特别全面。比如下面这段代码,在 Chrome 中可以正常运行,但是在 IE 11 中就会报错
// 使用 ES6 中的箭头函数语法
const message = () => {
console.log('Hello World')
}
message()
上面这种情况就是 js 不兼容了,为了使我们写的 js 代码兼容大部分浏览器,我们还得借助于专门的第三方库——Babel,它可以将我们写的 ES6 代码,转译成同等意义的 ES5 代码,这样就能让那些老旧的浏览器也能正常运行我们的 js 代码。
在 webpack 中使用 babel 很方便,只需要安装 babel-loader 以及其附带的包
npm i babel-loader @babel/core @babel/preset-env -D
然后配置 babel
// webpack.config.js
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'] // 使用预定义的兼容性方案
}
}
]
},
这次,将输出的 index.html 用 IE 运行,就不会再有报错了
全部 js 兼容性处理
上面的 babel 配置只能处理基本的 Javascript 语法,如果遇到更高级的语法(比如 Promsie ),是无法处理的。我们使用 ES6 中的 Promise 来写一段代码:
const promise = Promise.resolve("Hello World")
promise.then(function (p) {
console.log(p)
})
在 IE 中运行就会报错,因为 IE 根本不支持 Promise
为了让 IE 也兼容 Promise ,所以我们要安装并使用**@babel/polyfill**
npm i @babel/polyfill -D
它的使用非常简单,直接在 js 文件的头部引入就可以了
// index.js
import '@/babel/polyfill'
const promise = Promise.resolve("Hello World")
promise.then(function (p) {
console.log(p)
})
然后我们再次打包,运行到 IE,这次就不会再有报错了。@babel/polyfill 这个库所做的工作就是:接管了代码中所有的高级语法,然后转译为 ES5 及以下的实现,这样 IE 就能运用 Promise 了
值得注意的是:
-
引入 @babel/polyfill 之前打包的文件大小
-
引入 @babel/polyfill 之后打包的文件大小
前后对比发现,居然变大了许多,513KB;这是因为引入的 @babel/polyfill 包含了所有的高级语法兼容性处理的实现,所以体积才会这么大
部分 js 兼容性处理
上面的例子中,我们只是使用了一个 Promise ,就引入了体积500多K的库文件,这样未免有点浪费空间。如果能按需引入就好了,这样将不会引入多余的库文件,占用空间。
corejs 就可以提供这样的能力:
npm i core-js -D
在使用 core-js 之前,记得先要注释掉 index.js 中的 @babel/polyfill
// webpack.config.js
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
{
useBuiltIns: 'usage', // 启用按需引入
corejs: {
version: 3 // 指定 core-js 的版本
},
targets: {
chrome: '60',
firefox: '60',
safari: '10',
ie: '9',
edge: '17'
}
}
]
]
}
}
]
}
配置完 core-js 以后,我们再来打包运行,就会发现体积相比于使用 @/babel/polyfill 要少了很多
js和html压缩
要压缩 js 文件,只需要将 mode 的值设置为 production,这样 webpack 就会在打包的时候自动压缩 js 文件。html 文件的压缩则需要用到 html-webpack-plugin 这个插件,在其中配置之后,就能实现 html 的压缩
// webpack.config.js
mode: 'production',
plugins:[
new HtmlWebpackPlugin({
// 配置 html 压缩
minify: {
collapseWhitespace: true, // 去掉多余的空格
removeComments: true // 移除注释
}
})
]
html-webpack-plugin 的用法远不止文章提到的这几种,更具体详细的用法,请参考文档
webpack性能优化
HMR
通常一个项目中会有很多个CSS文件和JS文件,还有很多其他的文件,但唯独HTML文件只有一个(通常情况下),当我们启动了webpack-dev-server的服务以后,我们修改任何文件,它都会将所有存在于依赖关系图中的文件,自动编译并刷新到页面上,这非常的方便
但是,你是否想过一个问题——当你的项目中有10个JS文件的时候,你修改其中一个,那么这10个文件都会被重新编译;当你的项目中有10000个JS文件的时候,你只修改其中一个,那么这10000个文件还是都会被重新编译;这就造成了性能上的浪费,我只修改了一个,何必要重新编译所有的文件呢?
能不能只是重新编译我们修改的那个文件,而没修改的就原封不动不用管它?
HMR(Hot Module Replacement)模块热替换,是 webpack-dev-server 提供的功能,它就是用来实现上述需求的。它可以只编译我们修改过的文件,而其余文件则不会编译,这样就不必去做那些重复的工作!要使用它,只需要在 devServer中配置:
// package.json
devServer: {
contentBase: resolve(__dirname, 'build'),
compress: true,
port: 3000,
open: true,
hot: true // 启用 HMR 功能
}
比如,我们的项目中有一个 a.css 文件,在启用HMR之后,我们修改它,然后 webpack-dev-server 就会只编译它而不会编译没有改动的模块,在控制台中可以看到 HMR 的输出信息,它显示了热替换的是哪个模块:
注意:
-
样式文件默认支持 HMR,因为
style-loader中实现了 -
JS 文件默认情况下是不支持 HMR 的,当你开启了 HMR,就算你只修改了一个样式文件,所有的JS文件还是会被全部编译
-
HTML则没有必要使用HMR,因为它只有一个。但是,开启HMR之后HTML页面将不能自动刷新,除非手动刷新页面。
下面,我们就来解决这些问题:
-
为了使HTML页面在我们修改后自动刷新,我们需要在 entry 中加入 index.html
entry: ['./src/index.js', './src/index.html'] -
为了使 JS 文件也支持 HMR,我们需要在 index.js 中配置:
// print.js export default function print() { console.log('print.js 文件运行了') }// index.js import print from './print' // 如果 module.hot 为 true,说明启用了 HMR if (module.hot) { // 让 JS 也支持 HMR module.hot.accept('./print.js', function () { print() // 执行一个函数,来体现当前的 JS 文件被重新编译了,而其他的文件则没有 }) }
现在控制台就会显示出 HMR 的输出信息,当我们只修改 print.js 文件,那么就只有 print.js 文件会被重新编译
source-map
我们使用 webpack 构建并打包项目之后,运行的都是构建后的代码;要知道,构建后代码将非常不适合程序员阅读(为了适合机器去阅读),所以代码错误调试就是个大问题。假如构建后代码运行出错了,我们将很难定位到出错位置
souce-map 提供了构建后代码到源代码的映射,这样就能准确的显示代码错误以及提示信息,极大的方便我们调式错误,它也是在 webpack.config.js 中配置的,它有着很多种用法,下面一一列举:
devtool: 'source-map'
// devtool: 'inline-source-map'
// devtool: 'hidden-source-map'
// devtool: 'eval-source-map'
// devtool: 'nosources-source-map'
// devtool: 'cheap-source-map'
// devtool: 'cheap-module-source-map'
运行 webpack 打包之后,就可以看到在输出目录中,多了一个built.js.map 文件,其中记录源代码于构建后代码的映射关系
source-map 有很多种用法:
-
source-map & inline-source-map
source-map会将生成的映射代码集中在一起,输出到
built.js.map中;inline-source-map 则是内联到built.js中,优点是构建速度快。它们都能够准确的指示出源代码中错误发生的位置 -
hidden-source-map & nosources-source-map
能够提示错误信息,但是无法追踪到源代码;它们两个的作用就是隐藏源代码
-
eval-source-map
能够提示错误,以及定位到精准的源代码位置。不同点在于:它分别为每个文件生成对应的内联 source-map ,都包含在 eval 函数中
-
cheap-source-map & cheap-module-source-map
都能提示错误信息,但是只能定位到错误发生的代码行,而不能精确到错误语句(以上几种都能精确到语句)
之所以 source-map 提供了这么多的用法,就是为了适应不同的环境;有的方式速度快,有的方式对调试友好...我们在开发的时候可以按需使用,这样才能最方便我们的开发。在开发环境下,我们需要的可能是对调试友好,在生产环境下我们需要的则可能是速度快、隐藏源代码
从构建速度来看,source-map 的速度从快到慢依次是:eval-source-map > inline-source-map > cheap-source-map;从提示错误信息的精度(是否对调试友好)来看,从高到低依次是 source -map> cheap-module-source-map > cheap-source-map。
所以,兼顾到速度快与易于调试两个方面,建议使用 eval-source-map
oneOf
webpack中loader的处理机制是:将当前要处理的文件匹配列表中所有的 loader,比如有一个js文件,rules中定义了10个loader,第一个是处理js文件的loader,当第一个loader处理完成后,webpack不会自动跳出,而是会继续拿着这个js文件去尝试匹配剩下的9个loader,相当于 switch 语句没有break。
而 oneOf 的作用就相当于break,它在匹配到第一个正确的 loader 以后,就不再继续去匹配 rules 中其余的 loader ,这样就能节省一部分开销。示例如下:
rules: [
{
oneOf: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.(jpg|png|gif)$/,
loader: 'url-loader',
options: {
limit: 8 * 1024,
name: '[hash:10].[ext]',
esModule: false
}
},
{
test: /\.html$/,
loader: 'html-loader',
},
{
exclude: /\.(html|css|js|png|gif)$/,
loader: 'file-loader',
options: {
name: '[hash:10].[ext]',
}
}
]
}
]
**注意:**在 oneOf 中,不能有两个 loader 都用来处理同一类型的文件,比如 eslint-loader 和 babel-loader 都处理 js 文件,但是 oneOf 中,只会匹配一个(只有一个能生效);如果要使两个 loader 都生效,就要拿出一个 loader 放到 oneOf 外面,就像下面这样:
rules: [
// eslint-loader 在 oneOf 外面,会被第一个匹配到
{
test: /\.js$/,
exclude: /node_modules/,
enforce: 'pre', // enforce 选项表示,优先匹配
loader: 'eslint-loader',
options: {
fix: true
}
},
{
oneOf: [
// babel-loader 同样也能匹配到,因为 oneOf 中只有它这一个 loader 用于处理js
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {.....}
},
{
exclude: /\.(html|css|js|png
loader: 'file-loader',
options: {
name: '[hash:10].[ext]',
}
}
]
}
]
缓存
babel缓存
babel-loader 在处理项目中 js 文件时存在着一个问题:无论文件有没有被修改,都会被 babel 编译;也就是说,100个 js 文件中,只修改了1个,但是其余99个 js 文件还是会被 babel 处理;这与 webpack-dev-server 中产生的问题非常相似。
babel-laoder 没有 HMR 功能,但是它有缓存的功能:将上一次处理好的文件全部缓存起来,下一次调用时只处理那些修改过的文件,而没有修改过的文件直接调用缓存中的结果
在 babel-loader 中使用缓存功能,只要一个配置——cacheDirectory:
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
presets: [......],
cacheDirectory: true // 开启缓存功能
}
文件资源缓存
使用 webpack 来打包项目, webpack 会把打包后的内容放置在 /dist 目录中,只要把 /dist 目录中的内容部署到服务器 server 上,浏览器就能够访问它。而在载入项目的过程中,获取资源是比较耗费时间的,所以浏览器使用了 缓存 。浏览器可以通过使用缓存,以降低网络流量,使网站加载速度更快。
打开浏览器控制台的 Network 选项,就可以看到当前网站资源的缓存情况
通常,浏览器会将当前版本的资源文件缓存起来,以供下次使用(缓存起来就不用再去请求),然而,如果在部署新版本的项目时不更改资源的文件名,浏览器会认为它没有被更新,就会直接使用它的缓存版本,我们看到的结果就是:虽然更新了代码,但是页面上并没有发生变化。
为了确保 webpack 编译生成的文件能被浏览器缓存,而且在文件内容变化后,还能够请求到最新的文件从而在页面上显示,我们就要通过一些手段来实现文件资源缓存的更新,比如每次 webpack 打包后都给文件重命名,这样只要浏览器发现文件名被修改了,就不会再去使用缓存,而是请求最新的文件。
我们可以使用 hash 值来给文件重命名
hash
在输出的 built.js 文件名中植入一个 hash 值,达到修改资源文件名的目的
output: {
filename: 'built.[hash:10].js',
path: resolve(__dirname, 'build')
}
然后再次打包,你就会看到built.js的文件名发生了变化,比如built.2a310a49ad.js。其实这个 hash 值就是每次 webpack 打包成功后生成的那个 hash 值,也就是说:任何一个文件发生了修改,那么所有的文件命名中都会植入一个新的 hash 值
chunkhash
根据 chunk 来生成 hash 值
output: {
filename: 'built.[chunkhash:10].js',
path: resolve(__dirname, 'build')
}
chunkhash 与上面的不同点在于:hash 是以整个项目文件资源为范围,而 chunkhash 则是以同一个 chunk 为范围来命名文件的。例如一个 a.js 引入了 a.css ,那么它们在被打包时,就属于同一个 chunk。chunkhash 只会在属于同一个 chunk 的文件上生效,也就是说:在同一个 chunk 下的许多个文件,只要有一个修改了,那么所有的文件名都会被植入一个新的 hash 值
contenthash
根据文件内容来生成 hash 值
output: {
filename: 'built.[contenthash:10].js',
path: resolve(__dirname, 'build')
}
不同的文件,生成的 hash 值一定不同,也就是说:只要你修改了一个文件,那么它的命名就会包含新的 hash 值,其余的任何文件,只要没修改则命名不变
结合上面三种 hash 命名的方法来看,我们当然要选用 contenthash 啦,因为它的开销最小,粒度最小,所以是最精确、性能最好的!
tree shaking
假如,你从 a.js 文件中引入了两个函数 a 和 b,然后你只是使用了 a 函数,并没有使用 b 函数;这时,使用 webpack 打包,b 函数的代码也会被打包构建,即使它没有被使用——去除掉项目中无用的代码,对提高性能具有很大的帮助,不仅可以减小体积,还能节省编译这些代码的开销
想象一下秋天里,你站在一棵小树下,然后你摇晃这棵树会怎样?树上的枯枝败叶会掉落下来...
tree shaking 就是这个形象的比喻中的“摇树”的行为,我们的项目就好比一颗大树,那些无用的代码就是树上的枯枝败叶,通过“摇晃”项目——这颗大树,就能把那些无用的代码都去除掉——这就是 tree shaking 的作用
webpack 中如何启用 tree shaking 呢?很简单,只需要设置 mode = "production"
启用了 tree shaking 以后,webpack 打包的代码中,就不会再有那些无用(或引入了但是没有使用)的代码;同时,也有着一个问题,因为 webpack 并没有独立思考的能力,它默认情况下会把引入的 CSS 文件也“从树上摇下来”,所以,我们要通过配置,来告诉 webpack 那些文件是有用的,这样 tree shaking 的时候就不会把它们误删
在 package.json 中使用一个 sideEffects属性来定义
"sideEffect": false
// 或者
"sideEffect": ['*.css']
上面配置的含义是:
- sideEffects 为 false 时,webpack 会认为所有文件都可以进行 tree hshaking ,这样那些显式使用的代码会保留,但显式引入,隐式调用的代码却会被清除;比如,外部引入的 @babel/polyfill 可能会被清除
- sideEffects 为一个数组,其中写入要匹配的文件后缀名,比如上面代码中的
*.css就表示所有的 CSS 文件都是有用代码(不需要 tree shaking处理),所以就不会被清除
最后,很重要的一个前提:tree shaking 只识别 ES6 Module,其他类型的模块它不能识别。更详细的用法在这里
代码分离
多入口
前面所使用的 webpack 打包后输出的 bundle 中,只有一个 built.js 文件,毫无疑问,它就是我们项目中所有 js 代码的集成,所以,它会很臃肿,因为所有的 js (可能还有 css 等文件)都在其中;我们需要将这个臃肿的文件分割开来,这样就能减小 bundle 的体积,便于加载,可以通过多入口来实现 bundle 的分离
entry: {
// key是指定输出的JS文件的名称,value是入口文件
main: './src/js/index.js',
test: './src/js/test.js'
},
设置多入口打包之后,查看 webpack 的输出信息:分别输出了 main.js 和 test.js
不过,多入口虽然可以将最终打包输出的文件分解为多个小文件,但是它不能解决重复引用的问题,假如 a.js 和 b.js中都引入了 jQuery,那么最后打包输出的两个文件中,每个都会包含 jQuery——这就是重复引用
SplitChunksPlugin
为了解决重复引用的问题,我们通过 SplitChunksPlugin来将文件之间公共引用的库文件,打包成单独的 bundle
-
index.js
import $ from 'jQuery' console.log($) -
test.js
import $ from 'jQuery' console.log($)
通过 optimization属性来配置 SplitChunksPlugin:
optimization: {
splitChunks: {
// all 表示所有的 chunk 都要被SplitChunksPlugin处理
chunks: "all"
}
}
执行打包:
我们可以看到,main.js 和 test.js 都引入了 jQuery ,但是 jQuery 被单独打包成一个 bundle 文件,从文件大小也可以看出来。SplitChunksPlugin 更高级的用法在这里
import()
还有一种方法可以让 webpack 在打包的时候将一个 js 打包成单独的 bundle ——使用 ES6 的 import() 方法来导入一个外部 js 文件
-
index.js
// 因为使用了 import() 方法,所以就不用再使用下面的语句 // import { msg } from './test' import('./test').then((result) => { console.log(result.msg) }).catch((error) => { console.log(error) })test.js -
test.js
import $ from 'jQuery' console.log($) export const msg = 'Hello World'
现在执行打包:
可以看到,结果如我们所愿,每个 js 文件都被打包成了单独的 bundle;只是文件名发生了变化——都是以 chunk 的 id 来命名的,这样命名的坏处就是 id 仅仅是一个数字,不便于我们分辨
现在我们来让 webpack 给这些文件一个规范的命名,通过添加注释的方式
import(/*webpackChunkName: 'my_test'*/'./test').then((result) => {
console.log(result.msg)
}).catch((error) => {
console.log(error)
})test.js
再次打包:
个人觉得这种在代码中执行注释的方式不太“正常”,很奇怪
懒加载
所谓懒加载,就是用到的时候才被加载,没用到就不加载;就像妈妈喊你拖地,你才去拖地;妈妈没喊你拖地,你就懒着,不主动的去帮忙拖地。
下面用两个文件来演示,没使用懒加载的情况:
-
index.js
console.log('index.js 被加载了') import { msg } from './test' -
test.js
console.log('test 被加载了') export const msg = 'Hello World'执行打包
我们来分析一下:index.js 虽然引入了 test.js ,但是并没有调用 test.js 中的变量,然而 test.js 中的 console 语句还是执行了!这说明——test.js 无论有没有被调用,它都被加载并执行了
显然,一下子把所有文件都加载(无论那个文件是否会被用到)不是个好的方式,至少对性能来说不是件好事情,所以这里,我们要想办法优化——通过 懒加载
还是用 ES6 的 import() 方法,它的返回值是一个 promise 对象,通过 then() 就能得到 import 导入的文件,我们改写 index.js 如下:
console.log('index.js 被加载了')
document.getElementById('btn').onclick = function () {
import('./test').then((result) => {
console.log(result.msg)
}).catch((error) => {
console.log(error)
})
}
再次执行打包
可以看到,这次 test.js 没有被加载,它只有在点击了按钮以后才会被加载并运行,这就是懒加载
上面仅仅是一个简单的示例,懒加载是一个很好的性能优化手段,可以深入了解其文档