一直觉得,个人Webpack
就是复制粘贴的水平,而对Webpack
的知识真的很模糊,甚至是纯小白。因此前段时间开始对Webpack
进行比较系统的学习。javascript
学习完成后,我抽空整理了笔记,前先后后也花了一周多。最后以为能够分享出来,让对Webpack
还很模糊的朋友,能够学习一下。css
固然,读完本文,你会发现Webpack
还有更多更深的东西值得咱们去学习,所以这只是一个开始,从零开始。html
在学习webpack
以前,咱们须要先来捋一捋三个术语——module
、chunk
和bundle
。java
先看看webpack
官方对module
的解读:node
Module
是离散功能块,相比于完整程序提供了更小的接触面。精心编写的模块提供了可靠的抽象和封装界限,使得应用程序中每一个模块都具备条理清楚的设计和明确的目的。jquery
其实简单来讲,module
模块就是咱们编写的代码文件,好比JavaScript
文件、CSS
文件、Image
文件、Font
文件等等,它们都是属于module
模块。而module
模块的一个特色,就是能够被引入使用。webpack
一样的先看看官方解读:git
此
webpack
特定术语在内部用于管理捆绑过程。输出束(bundle)由块组成,其中有几种类型(例如entry
和child
)。一般,块直接与输出束 (bundle
)相对应,可是,有些配置不会产生一对一的关系github
其实chunk
是webpack
打包过程的中间产物,webpack
会根据文件的引入关系生成chunk
,也就是说一个chunk
是由一个module
或多个module
组成的,这取决于有没有引入其余的module
。web
先看看官方解读:
bundle
由许多不一样的模块生成,包含已经通过加载和编译过程的源文件的最终版本。
bundle
实际上是webpack
的最终产物,一般来讲,一个bundle
对应这一个chunk
。
其实module
、chunk
和bundle
能够说是同一份代码在不一样转换场景的不一样名称:
module
webpack
处理时时chunk
bundle
咱们经过一个小demo
来过一下,如今有一个项目,路径以下:
src/
├── index.css
├── index.js
├── common.js
└── utils.js
复制代码
而后咱们有两个入口文件,一个是index.js
,一个是utils.js
,在index.js
中引入了index.css
和common.js
。而后经过webpack
打包出来了index.bundle.css
、index.bundle.js
和utils.bundle.js
。
好,介绍完背景后,咱们就能够来分析一下module
、chunk
和bundle
。
首先,咱们编写的代码,就是module
,也就是说index.css
、common.js
、index.js
和utils.js
共四个module
文件。
其次,咱们有两个入口文件,分别为index.js
和utils.js
,而且它们最后是独立打包成bundle
的,从而在webpack
打包过程当中就会造成两个chunk
文件,而由index.js
造成chunk
还包含着index.js
引入的module
——common.js
和index.css
。
最后,咱们打包出来了index.bundle.css
、index.bundle.js
和uitls.bundle.js
,这三个也就是bundle
文件。
最后,咱们能够总结一下三者之间的关系:一个budnle
对应着一个chunk
,一个chunk
对应着一个或多个module
。
接下来,咱们经过一步步实践,来慢慢学习webpack
,这篇文章使用的是webpack5
。
首先,新建一个项目文件夹,而后初始化项目。
yarn init -y
复制代码
而后安装一下webpack
。当咱们使用webpack
时,还须要安装webpack-cli
。
由于webpack
只是在开发环境才会使用到,因此咱们只须要添加到devDependencies
便可。
# webpack -> 5.47.0, webpack-cli-> 4.7.2
yarn add webpack webpack-cli -D
复制代码
而后再项目中新建src
路径,再新建一个index.js
:
console.log("Hello OUDUIDUI");
复制代码
而后执行npx webpack
,则执行webpack
打包。这时你的项目就会多一个dist
文件夹,而且在dist
文件夹中会看到一个main.js
,里面的代码跟index.js
同样。
固然,咱们能够在package.json
中编辑script
命令:
"scripts": {
"dev": "webpack"
}
复制代码
而后执行yarn dev
,也能够成功打包。
若是使用过webpack
的朋友应该知道,webpack
其实有一个配置文件——webpack.config.js
。
但为何前面的初始化测试时,咱们没有编辑配置文件却能够成功打包?这是由于webpack
会有一个默认配置,当它检测到咱们没有配置文件的时候,它默认会使用本身的默认配置。
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'main.js',
},
};
复制代码
首先,咱们简单来过一下这些默认配置叭。
entry
选项是用来配置入口文件的,它能够是字符串、数组或者对象类型。webpack
默认只支持js
和json
文件做为入口文件,所以若是引入其余类型文件会保存。
output
选项是设置输出配置,该选项必须是对象类型,不能是其它类型格式。在output
对象中,必填的两个选项就是导出路径path
和导出bundle
文件名称filename
。其中path
选项必须为绝对路径。
entry
和output
的配置,对于不一样的应用场景的配置也会有所不一样。
咱们最广泛的就是单个入口文件,而后打包成单个bundle
文件。这种应用场景下,entry
可使用字符串的形式,则跟默认配置文件相似:
entry: './src/index.js'
复制代码
当咱们的项目须要有多个入口文件,但只须要一个输出bundle
的时候,这时候entry
可使用数组的形式:
entry: ['./src/index_1.js', './src/index_2.js']
复制代码
注意:此时其实只有一个chunk
当咱们的项目同时多个入口文件,而且它们须要单独打包,也就是意味着会有多个bundle
文件输出,此时咱们的entry
须要使用对象形式,而且对象key
对应的对应chunk
的名称。
entry: {
index: "./src/index.js", // chunkName为index
main: "./src/main.js" // chunkName为main
}
复制代码
此时,咱们的output.filename
也不能写死了,这时候webpack
提供了一个占位符[name]
给咱们使用,它会自动替换为对应的chunkName
。
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js' // [name]占位符会自动替换为chunkName
},
复制代码
根据上面的配置,最后会打包出index.js
和main.js
。
在单入口单输出的应用场景下,entry
也可使用对象的形式,从而来自定义chunkName
,而后output.filename
也使用[name]
占位符来自动匹配。固然也可使用数组,可是不太大必要。
当entry
使用数组或字符串的时候,chunkName
默认为main
,所以若是output.filename
使用[name]
占位符的时候,会自动替换为main
。
在前面的打包测试的时候,命令行都会报一个警告:
WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value.
Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
复制代码
这是由于webpack
须要咱们配置mode
选项。
wepack给咱们提供了三个选项,即none
、development
和production
,而默认就是production
。
三者的区别呢,在于webpack
自带的代码压缩和优化插件使用。
none
:不使用任何默认优化选项;
development
:指的是开发环境,会默认开启一些有利于开发调试的选项,好比NamedModulesPlugin
和NamedChunksPlugin
,分别是给module
和chunk
命名的,而默认是一个数组,对应的chunkName
也只是下标,不利于开发调试;
production
:指的是生产环境,则会开启代码压缩和代码性能优化的插件,从而打包出来的文件也相对none
和development
小不少。
当咱们设置mode以后,咱们能够在
process.env.NODE_ENV
获取到当前的环境
所以咱们能够在配置文件上文件上配置mode
:
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js',
},
// 开启source-map
devtool: "source-map"
};
复制代码
webpack
也给咱们提供了另外一种方式,就是在命令行中配置,也就是加上--mode
:
// package.json
"scripts": {
"dev": "webpack --mode development",
"build": "webpack --mode production"
}
复制代码
聊完mode
后,说到开发调试,不难想起的就是sourceMap
。而咱们能够在配置文件中,使用devtool
开启它。
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js',
},
// 开启source-map
devtool: "source-map"
};
复制代码
打包后,你的dist
中就会多了一个main.js.map
文件。
固然,官方不止提供这么一个选项,具体的能够去官网看看,这里就说其余几个比较经常使用的选项。
none
:不会生成sourceMap
;
eval
:每一个模块都会使用eval()
执行,不建议生成环境中使用;
cheap-source-map
:生成sourceMap
,可是没有列映射,则只会提醒是在代码的第几行,不会提示到第几列;
inline-source-map
:会生成sourceMap
,但不会生成map
文件,而是将sourceMap
放在打包文件中。
前面咱们有提到过,就是webpack
的入口文件只能接收JavaScript
文件和JSON
文件。
但咱们一般项目还会有其余类型的文件,好比html
、css
、图片、字体等等,这时候咱们就须要用到第三方loader
来帮助webpack
来解析这些文件。理论上只要有相应的loader
,就能够处理任何类型的文件。
在
webpack
官网其实提供了不少loader
,已经能知足咱们平常使用,固然咱们也能够去github
找找别人写的loader
或者本身手写loader
来使用。
而对于loader
的配置,是写着module
选项里面的。module
选项是一个对象,它里面有一个rules
属性,是一个数组,在里面咱们能够配置多个匹配规则。
而匹配规则是一个对象,会有test
属性和use
属性,test
属性通常是正则表达式,用来识别文件类型,而use
属性是一个数组,里面用来存放对该文件类型使用的loader
。
module: {
rules: [
{
test: /\.css$/, // 识别css文件
use: ['style-loader', 'css-loader'] // 对css文件使用的三个loader
}
]
}
复制代码
对于use
数组的顺序是有要求的,webpack
会根据自后向前的规则去执行loader
。也就是说,上面的例子webpack
会先执行css-loader
,再执行style-loader
。
其次,当咱们须要对对应loader
提供配置的时候,咱们能够选用对象写法:
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
{
// loader名称
loader: 'css-loader',
// loader选项
options: {
...
}
}
]
}
]
}
复制代码
在后面咱们根据实际应用场景再讲讲module
的使用。
webpack
还提供了一个plugins
选项,让咱们可使用一些第三方插件,所以咱们可使用第三方插件来实现打包优化、资源管理、注入环境变量等任务。
一样的,
webpack
官方也提供了不少plugin
。
plugins
选项是一个数组,里面能够放入多个plugin
插件。
plugins: [
new htmlWebpackPlugin(),
new CleanWebpackPlugin(),
new miniCssExtractPlugin(),
new TxtWebpackPlugin()
]
复制代码
而对于plugins
数组对排序位置是没有要求,由于在plugin
的实现中,webpack
会经过打包过程的生命周期钩子,所以在插件逻辑中就已经设置好须要在哪一个生命周期执行哪些任务。
当咱们是Web
项目的时候,咱们必然会存在html
文件去实现页面。
而对于其余类型的文件,好比css
、图片、文件等等,咱们是能够经过引入入口js
文件,而后经过loader
进行解析打包。而对于html
文件,咱们不可能将其引入到入口文件而后解析打包,反而咱们还须要将打包出来的bundle
文件引入html
文件去使用,
所以,其实咱们须要实现的操做只有两个,一个是复制一份html
文件到打包路径下,另外一个就是将打包出来的bundle
文件自动引入到html
文件中去。
这时候咱们须要使用一个插件来实现这些功能——html-webpack-plugin
。
# 5.3.2
yarn add html-webpack-plugin -D
复制代码
安装插件后,咱们先在src
文件下新建一下index.html
。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Webpack Demo</title>
</head>
<body>
<div>Hello World</div>
</body>
</html>
复制代码
这里面咱们暂时不须要引入任何模块。
接下来配置一下webpack
。通常plugin
插件都是一个类,而咱们须要在plugins
选项中须要建立一个插件实例。
对于htmlWebpackPlugin
插件,咱们须要传入一些配置:html
模板地址template
和打包出来的文件名filename
。
const path = require('path');
// 引入htmlWebpackPlugin
const htmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js',
},
plugins: [
// 使用htmlWebpackPlugin插件
new htmlWebpackPlugin({
// 指定html模板
template: './src/index.html',
// 自定义打包的文件名
filename: 'index.html'
})
]
};
复制代码
接下来执行一下打包,就会发现dist
文件下会生成一个index.html
。打开会发现,webpack
会自动将bundle
文件引入:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Webpack Demo</title>
<script defer src="main.js"></script></head>
<body>
<div>Hello World</div>
</body>
</html>
复制代码
若是咱们有多个chunk
的时候,咱们能够指定该html
要引入哪些chunk
。在htmlWebpackPlugin
配置中有一个chunks
选项,是一个数组,你只须要加入你想引入的chunkName
便可。
const path = require('path');
// 引入htmlWebpackPlugin
const htmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
index: './src/index.js',
main: './src/main.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js',
},
plugins: [
new htmlWebpackPlugin({
template: './src/index.html',
filename: 'index.html',
chunks: ["index"] // 只引入index chunk
})
]
};
复制代码
打包完成后,dist
文件下会出现index.html
、index.js
和main.js
,可是index.html
只会引入index.js
。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script defer src="index.js"></script></head>
<body>
HelloWorld!
</body>
</html>
复制代码
若是咱们须要实现多页面的话,只须要再new
一个htmlWebpackPlugin
实例便可,这里就再也不多说。
在每次打包前,咱们其实都须要去清空一下打包路径的文件。
若是文件重名的话,webpack
还会自动覆盖,可是实际中咱们都会在打包文件名称中加入哈希值,所以清空的操做不得不实现。
这时候咱们须要使用一个插件——clean-webpack-plugin
。
yarn add clean-webpack-plugin -D
复制代码
而后只需引入到配置文件且在plugins
配置就可使用了。
const path = require('path');
// 引入CleanWebpackPlugin
const {CleanWebpackPlugin} = require('clean-webpack-plugin');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist/'),
filename: '[name].js',
publicPath: ''
},
plugins: [
// 使用CleanWebpackPlugin
new CleanWebpackPlugin(),
]
};
复制代码
有些状况下,咱们不须要彻底清空打包路径,这时候咱们可使用到一个选项,叫cleanOnceBeforeBuildPatterns
,它是一个数组,默认是[**/*]
,也就是清理output.path
路径下全部东西。而你能够在里面输入只想删除的文件,同时咱们能够输入不想删除的文件,只须要在前面加上一个!
。
须要注意的是,
cleanOnceBeforeBuildPatterns
这个选项是能够删除打包路径下以外的文件,只须要你配上绝对路径的话。所以CleanWebpackPlugin
还提供了一个选项供测试——dry
,默认是为false
,当你设置为true
后,它就不会真正的执行删除,而是只会在命令行打印出被删除的文件,这样子更好的避免测试过程当中误删。
const path = require('path');
const {CleanWebpackPlugin} = require('clean-webpack-plugin');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist/'),
filename: '[name].js',
publicPath: ''
},
plugins: [
new CleanWebpackPlugin({
// dry: true // 打开可测试,不会真正执行删除动做
cleanOnceBeforeBuildPatterns: [
'**/*', // 删除dist路径下全部文件
`!package.json`, // 不删除dist/package.json文件
],
}),
]
};
复制代码
当咱们使用webpack
的时候,不只仅只是用于打包文件,大部分咱们还会依赖webpack
来搭建本地服务,同时利用其热更新的功能,让咱们更好的开发和调试代码。
接下来咱们来安装一下webpack-dev-server
:
# 版本为3.11.2
yarn add webpack-dev-server -D
复制代码
而后执行下列代码开启服务:
npx webpack serve
复制代码
或者在package.json配置一下:
"scripts": {
"serve": "webpack serve --mode development"
}
复制代码
而后经过yarn serve
运行。
这时,webpack会默认开启http://localhost:8080/
服务(具体看大家运行返回的代码),而该服务指向的是dist/index.html
。
但你会发现,你的dist
中实际上是没有任何文件的,这是由于webpack
将实时编译后的文件都保存到了内存当中。
其实webpack
自带提供了--watch
命令,能够实现动态监听文件的改变并实时打包,输出新的打包文件。
但这种方案存在着几个缺点,一就是每次你一修改代码,webpack就会所有文件进行从新打包,这时候每次更新打包的速度就会慢了不少;其次,这样的监听方式作不到热更新,即每次你修改代码后,webpack从新编译打包后,你就得手动刷新浏览器,才能看到最新的页面结果。
而webpack-dev-server
,却有效了弥补这两个问题。它的实现,是使用了express
启动了一个http
服务器,来伺候资源文件。而后这个http
服务器和客户端使用了websocket
通信协议,当原始文件做出改动后,webpack-dev-server
就会实时编译,而后将最后编译文件实时渲染到页面上。
在webpack.config.js
中,有一个devServer
选项是用来配置webpack-dev-server
,这里简单讲几个比较经常使用的配置。
咱们能够经过port来设置服务器端口号。
module.exports = {
...
// 配置webpack-dev-server
devServer: {
port: 8888, // 自定义端口号
},
};
复制代码
在devServer
中有一个open
选项,默认是为false
,当你设置为true
的时候,你每次运行webpack-dev-server
就会自动帮你打开浏览器。
module.exports = {
...
// 配置webpack-dev-server
devServer: {
open: true, // 自动打开浏览器窗口
},
};
复制代码
这个选项是用来设置本地开发的跨域代理的,关于跨域的知识就很少在这说了,这里就说说如何去配置。
proxy
的值必须是一个对象,在里面咱们能够配置一个或多个跨域代理。最简单的配置写法就是地址配上api
地址。
module.exports = {
...
devServer: {
// 跨域代理
proxy: {
'/api': 'http://localhost:3000'
},
},
};
复制代码
这时候,当你请求/api/users
的时候,就会代理到http://localhost:3000/api/users
。
若是你不须要传递/api
的话,你就须要使用对象的写法,从而增长一些配置选项:
module.exports = {
//...
devServer: {
// 跨域代理
proxy: {
'/api': {
target: 'http://localhost:3000', // 代理地址
pathRewrite: { '^/api': '' }, // 重写路径
},
},
},
};
复制代码
这时候,当你请求/api/users
的时候,就会代理到http://localhost:3000/users
。
在proxy中的选项,还有两个比较经常使用的,一个就是changeOrigin
,默认状况下,代理时会保留主机头的来源,当咱们将其设置为true
能够覆盖这种行为;还有一个是secure
选项,当你的接口使用了https
的时候,须要将其设置为false
。
module.exports = {
//...
devServer: {
// 跨域代理
proxy: {
'/api': {
target: 'http://localhost:3000', // 代理地址
pathRewrite: { '^/api': '' }, // 重写路径
secure: false, // 使用https
changeOrigin: true // 覆盖主机源
},
},
},
};
复制代码
接下来说讲关于webpack
对css
的解析处理叭。
在前面的例子也能看到,咱们解析css
须要用到的loader
有css-loader
和style-loader
。css-loader
主要用来解析css
文件,而style-loader
是将css
渲染到DOM
节点上。
首先咱们来安装一下:
# css-loader -> 6.2.0; style-loader -> 3.2.1
yarn add css-loader style-loader -D
复制代码
而后咱们新建一个css
文件。
/* style.css */
body {
background: #222;
color: #fff;
}
复制代码
而后在index.js
引入一下:
import "./style.css";
复制代码
紧接着咱们配置一下webpack
:
module.exports = {
...
module: {
rules: [
{
test: /\.css$/, // 识别css文件
use: ['style-loader', 'css-loader'] // 先使用css-loader,再使用style-loader
}
]
},
...
};
复制代码
这时候咱们打包一下,会发现dist
路径下只有main.js
和index.html
。但打开一下index.html
会发现css
是生效的。
这是由于style-loader
是将css
代码插入到了main.js
当中去了。
若是咱们不想将css
代码放进js
中,而是直接导出一份css
文件的话,就得使用另外一个插件——mini-css-extract-plugin
。
# 2.1.0
yarn add mini-css-extract-plugin -D
复制代码
而后将其引入到配置文件,而且在plugins
引入。
const miniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
...
plugins: [
// 使用miniCssExtractPlugin插件
new miniCssExtractPlugin({
filename: "[name].css" // 设置导出css名称,[name]占位符对应chunkName
})
]
};
复制代码
紧接着,咱们还须要更改一下loader
,咱们再也不使用style-loader
,而是使用miniCssExtractPlugin
提供的loader
。
const miniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
...
module: {
rules: [
{
test: /\.css$/,
// 使用miniCssExtractPlugin.loader替换style-loader
use: [miniCssExtractPlugin.loader,'css-loader']
}
]
},
plugins: [
new miniCssExtractPlugin({
filename: "[name].css"
})
]
};
复制代码
接下来打包一下,dist
路径下就会多出一个main.css
文件,而且在index.html
中也会自动引入。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script defer src="main.js"></script><link href="main.css" rel="stylesheet"></head>
<body>
HelloWorld!
</body>
</html>
复制代码
当咱们使用一下css
新特性的时候,可能须要考虑到浏览器兼容的问题,这时候可能须要对一些css
属性添加浏览器前缀。而这类工做,其实能够交给webpack
去实现。准确来讲,是交给postcss
去实现。
postcss
对于css
犹如babel
对于JavaScript
,它专一于对转换css
,好比添加前缀兼容、压缩css
代码等等。
首先咱们须要先安装一下postcss
和post-css-loader
。
# postcss -> 8.3.6,postcss-loader -> 6.1.1
yarn add postcss postcss-loader -D
复制代码
接下来,咱们在webpack
配置文件先引入postcss-loader
,它的顺序是在css-loader
以前执行的。
rules: [
{
test: /\.css$/,
// 引入postcss-loader
use: [miniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
}
]
复制代码
接下来配置postcss
的工做,就不在webpack
的配置文件里面了。postcss
自身也是有配置文件的,咱们须要在项目根路径下新建一个postcss,config.js
。而后里面也有一个配置项,为plugins
。
module.exports = {
plugins: []
}
复制代码
这也意味着,postcss
自身也支持不少第三方插件使用。
如今咱们想实现的添加前缀的功能,须要安装的插件叫autoprefixer
。
# 1.22.10
yarn add autoprefixer -D
复制代码
而后咱们只须要引入到postcss
的配置文件中,而且它里面会有一个配置选项,叫overrideBrowserslist
,是用来填写适用浏览器的版本。
module.exports = {
plugins: [
// 将css编译为适应于多版本浏览器
require('autoprefixer')({
// 覆盖浏览器版本
// last 2 versions: 兼容各个浏览器最新的两个版本
// > 1%: 浏览器全球使用占有率大于1%
overrideBrowserslist: ['last 2 versions', '> 1%']
})
]
}
复制代码
关于overrideBrowserslist
的选项填写,咱们能够去参考一下browserslist,这里就很少讲。
固然,咱们其实能够在package.json
中填写兼容浏览器信息,或者使用browserslist
配置文件.browserslistrc
来填写,这样子若是咱们之后使用其余插件也须要考虑到兼容浏览器的时候,就能够统一用到,好比说babel
。
// package.json 文件
{
...
"browserslist": ['last 2 versions', '> 1%']
}
复制代码
# .browserslsetrc 文件
last 2 versions
> 1%
复制代码
但若是你多个地方都配置的话,overrideBrowserslist
的优先级是最高的。
接下来,咱们修改一下style.css
,使用一下比较新的特性。
body {
display: flex;
}
复制代码
而后打包一下,看看打包出来后的main.css
。
body {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
}
复制代码
当咱们须要压缩css
代码的时候,可使用postcss
另外一个插件——cssnano
。
# 5.0.7
yarn add cssnano -D
复制代码
而后仍是在postcss
配置文件中引入:
module.exports = {
plugins: [
... ,
require('cssnano')
]
}
复制代码
打包一下,看看main.css
。
body{display:-webkit-box;display:-ms-flexbox;display:flex}
复制代码
在如今咱们实际开发中,咱们会更多使用Sass
、Less
或者stylus
这类css
预处理器。而其实html
是没法直接解析这类文件的,所以咱们须要使用对应的loader
将其转换成css
。
接下来,我就以sass
为例,来说一下如何使用webpack
解析sass
。
首先咱们须要安装一下sass
和sass-loader
。
# sass -> 1.36.0, sass-loader -> 12.1.0
yarn add sass sass-loader -D
复制代码
而后咱们在module
加上sass
的匹配规则,sass-loader
的执行顺序应该是排第一,咱们须要先将其转换成css
,而后才能执行后续的操做。
rules: [
...
{
test: /\.(scss|sass)$/,
use: [miniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'sass-loader']
}
]
复制代码
而后咱们在项目中新建一个style.scss
。
$color-white: #fff;
$color-black: #222;
body {
background: $color-black;
div {
color: $color-white;
}
}
复制代码
而后在index.js
引入。
import "./style.css";
import "./style.scss";
复制代码
而后执行打包,再看看打包出来的main.css
,scss
文件内容被解析到里面,同时若是咱们引入多个css
或css
预处理器文件的话,miniCssExtractPlugin
也会将其打包成一个bundle
文件里面。
body{display:-webkit-box;display:-ms-flexbox;display:flex}
body{background:#222}body div{color:#fff}
复制代码
当咱们使用了图片、视频或字体等等其余静态资源的话,咱们须要用到url-loader
和file-loader
。
# url-loader -> 4.1.1; file-loader -> 6.2.0
yarn add url-loader file-loader -D
复制代码
首先咱们在项目中引入一张图片,而后在引入到index.js
中。
import pic from "./image.png";
const img = new Image();
img.src= pic;
document.querySelector('body').append(img);
复制代码
而后我先使用url-loader
。
module.exports = {
...
module: {
rules: [
{
test: /\.(png|je?pg|gif|webp)$/,
use: ['url-loader']
}
]
}
};
复制代码
而后执行一下打包。
你会发现,dist
路径下没有图片文件,可是你打开页面是能够看到图片的,且经过调试工具,咱们能够看到其实url-loader
默认会将静态资源转成base64
。
固然,url-loader
选项有提供一个属性,叫limit
,就是咱们能够设置一个文件大小阈值,当文件大小超过这个值的时候,url-loader
就不会转成base64
,而是直接打包成文件。
module.exports = {
...
module: {
rules: [
{
test: /\.(png|je?pg|gif|webp)$/,
use: [{
loader: 'url-loader',
options: {
name: '[name].[ext]', // 使用占位符设置导出名称
limit: 1024 * 10 // 设置转成base64阈值,大于10k不转成base64
}
}]
}
]
}
};
复制代码
这时候咱们再打包一下,dist
文件夹下就会出现了图片文件。
而file-loader
其实跟url-loader
差很少,但它默认就是导出文件,而不会导出base64
的。
module.exports = {
...
module: {
rules: [
{
test: /\.(png|je?pg|gif|webp)$/,
use: ['file-loader']
}
]
}
};
复制代码
打包一下,会发现dist
文件夹下依旧会打包成一个图片文件,可是它的名称会被改为哈希值,咱们能够经过options
选项来设置导出的名称。
module.exports = {
...
module: {
rules: [
{
test: /\.(png|je?pg|gif|webp)$/,
use: [{
loader: 'file-loader',
options: {
name: '[name].[ext]', // 使用占位符设置导出名称
}
}]
}
]
}
};
复制代码
而对于视频文件、字体文件,也是用相同的方法,只不过是修改test
。
module.exports = {
...
module: {
rules: [
// 图片
{
test: /\.(png|je?pg|gif|webp)$/,
use: {
loader: 'url-loader',
options: {
esModule: false,
name: '[name].[ext]',
limit: 1024 * 10
}
}
},
// 字体
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
use: {
loader: 'url-loader',
options: {
name: '[name].[ext]',
limit: 1024 * 10
}
}
},
// 媒体文件
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
use: {
loader: 'url-loader',
options: {
name: '[name].[ext]',
limit: 1024 * 10
}
}
}
]
}
};
复制代码
但如今有个问题,就是若是直接在index.html
引入图片的话,能够顺利打包吗?
答案是不会的,咱们能够测试一下。首先将图片引入index.html
。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<img src="./image.png">
</body>
</html>
复制代码
而后执行打包后,打包出来的index.html
照样是<img src="./image.png">
,可是咱们并无解析和打包出来image.png
出来。
这时候咱们须要借助另外一个插件——html-withimg-loader
。
# 0.1.16
yarn add html-withimg-loader -D
复制代码
而后咱们再添加一条rules
。
{ test: /\.html$/,loader: 'html-withimg-loader' }
复制代码
这时候打包成功后,dist
文件成功将图片打包出来了,可是打开页面的时候,图片仍是展现不出来。而后经过调试工具看的话,会发现
<img src="{"default":"image.png"}">
复制代码
这是由于html-loader
使用的是commonjs
进行解析的,而url-loader
默认是使用esmodule
解析的。所以咱们须要设置一下url-loader
。
{
test: /\.(png|je?pg|gif|webp)$/,
use: {
loader: 'url-loader',
options: {
esModule: false, // 不适用esmodule解析
name: '[name].[ext]',
limit: 1024 * 10
}
}
}
复制代码
这时候从新打包一下,页面就能成功展现图片了。
在webpack5
中,新添了一个资源模块,它容许使用资源文件(字体,图标等)而无需配置额外 loader
,具体的内容你们能够看看文档,这里简单讲一下如何操做。
前面的例子,咱们对静态资源都使用了url-loader
或者file-loader
,而在webpack5
,咱们甚至能够须要手动去安装和使用这两个loader
,而直接设置一个type
属性。
{
test: /\.(png|jpe?g|gif|svg|eot|ttf|woff|woff2)$/i,
type: "asset/resource",
}
复制代码
而后打包测试后,静态文件都会直接打包成文件并自动引入,效果跟file-loader
一致。
type
值提供了四个选项:
asset/resource
: 发送一个单独的文件并导出 URL。以前经过使用 file-loader
实现。asset/inline
: 导出一个资源的 data URI。以前经过使用 url-loader
实现。asset/source
:**导出资源的源代码。以前经过使用 raw-loader
实现。asset
: 在导出一个 data URI 和发送一个单独的文件之间自动选择。以前经过使用 url-loader
,而且配置资源体积限制实现。同时,咱们能够在output
设置输出bundle
静态文件的名称:
output: {
path: path.resolve(__dirname, 'dist/'),
filename: '[name].js',
// 设置静态bundle文件的名称
assetModuleFilename: '[name][ext]'
}
复制代码
不只仅css
须要转义,JavaScript
也要为了兼容多浏览器进行转义,所以咱们须要用到babel
。
# 8.2.2
yarn add babel-loader -D
复制代码
同时,咱们须要使用babel
中用于JavaScript
兼容的插件:
# @babel/preset-env -> 7.14.9; @babel/core -> 7.14.8; @core-js -> 3.16.0
yarn add @babel/preset-env @babel/core core-js -D
复制代码
接下来,咱们须要配置一下webpack
的配置文件。
{
test: /\.js$/,
use: ['babel-loader']
}
复制代码
而后咱们须要配置一下babel
。固然咱们能够直接在webpack.config.js
里面配置,可是babel
一样也提供了配置文件.babelrc
,所以咱们就直接在这边进行配置。
在根路径新建一个.babelrc
。
{
"presets": [
[
"@babel/preset-env",
{
// 浏览器版本
"targets": {
"edge": "17",
"chrome": "67"
},
// 配置corejs版本,但须要额外安装corejs
"corejs": 3,
// 加载状况
// entry: 须要在入口文件进入@babel/polyfill,而后babel根据使用状况按需载入
// usage: 无需引入,自动按需加载
// false: 入口文件引入,所有载入
"useBuiltIns": "usage"
}
]
]
}
复制代码
接下来,咱们来测试一下,先修改一下index.js
。
new Promise(resolve => {
resolve('HelloWorld')
}).then(res => {
console.log(res);
})
复制代码
而后执行yarn build
进行打包。
在使用babel
以前,打包出来的main.js
以下。
!function(){"use strict";new Promise((o=>{o("HelloWorld")})).then((o=>{console.log(o)}))}();
复制代码
上面打包代码是直接使用了Promise
,而没有考虑到低版本浏览器的兼容。而后咱们打开babel
后,执行一下打包命令,会发现代码多出了不少。
而在打包代码中,能够看到webpack
使用了polyfill
实现promise
类,而后再去调用,从而兼容了低版本浏览器没有promise
属性问题。
在目前咱们的测试代码中,咱们的src
文件夹以下:
├── src
│ ├── Alata-Regular.ttf
│ ├── image.png
│ ├── index.html
│ ├── index.js
│ ├── style.css
│ └── style.scss
复制代码
而正常项目的话,咱们会使用文件夹将其分好类,这并不难,咱们先简单归类一下。
├── src
│ ├── index.html
│ ├── js
│ │ └── index.js
│ ├── static
│ │ └── image.png
│ │ └── Alata-Regular.ttf
│ └── style
│ ├── style.css
│ └── style.scss
复制代码
接下来,咱们须要打包出来的文件也是归类好的,这里就不太复杂,直接用一个assets
文件夹将全部静态文件放进去,而后index.html
放外面。
├── dist
│ ├── assets
│ │ ├── Alata-Regular.ttf
│ │ ├── image.png
│ │ ├── main.css
│ │ └── main.js
│ └── index.html
复制代码
这里先补充一下style.css
引入字体的代码:
@font-face {
font-family: "test-font";
src: url("../static/Alata-Regular.ttf") format('truetype')
}
body {
display: flex;
font-family: "test-font";
}
复制代码
首先,咱们先将打包出来的JavaScript
文件放入assets
文件夹下,咱们只须要修改output.filename
便可。
output: {
path: path.resolve(__dirname, 'dist/'),
filename: 'assets/[name].js'
}
复制代码
其次,咱们将打包出来的css
文件也放入assets
路径下,由于咱们打包css
是使用miniCssExtractPlugin
,所以咱们只须要配置一下miniCssExtractPlugin
的filename
便可:
plugins: [
...
new miniCssExtractPlugin({
filename: "assets/[name].css"
})
]
复制代码
最后就是静态资源了,这里咱们使用静态模块方案,因此直接修改output.assetModuleFilename
便可:
output: {
path: path.resolve(__dirname, 'dist/'),
filename: 'assets/[name].js',
assetModuleFilename: 'assets/[name][ext]'
},
复制代码
这时候打包一下,预览一下页面,发现都正常引入和使用。
一般,咱们打包文件的文件名都须要带上一个哈希值,这会给咱们的好处就是避免缓存。
webpack
也提供了三种哈希值的策略,接下来咱们一一来看看:
为了更好的比较三者之间的区别,这边先调整一下项目和配置。
// index.js
import pic from "../static/image.png";
const img = new Image();
img.src = pic;
document.querySelector('body').append(img);
// main.js
import "../style/style.scss";
import "../style/style.css";
console.log('Hello World')
// webpack.config.js
entry: {
index: './src/js/index.js',
main: './src/js/main.js'
},
复制代码
hash
策略,是以项目为单位的,也就是说,只要项目一个文件发生改动,首先打包后该文件对应的bundle
文件名会改变,其次全部js
文件和css
文件的文件名也会改变。
咱们先经过一个例子来看看:
首先咱们须要在全部设置filename
的地方加入[hash]
占位符,同时咱们也能够设置哈希值的长度,只需加上冒号和长度值便可,好比[hash:6]
。
module.exports = {
entry: {
index: './src/js/index.js',
main: './src/js/main.js'
},
output: {
path: path.resolve(__dirname, 'dist/'),
filename: 'assets/[name]-[hash:6].js',
assetModuleFilename: 'assets/[name]-[hash:6][ext]'
},
module: {
...
},
plugins: [
...
new miniCssExtractPlugin({
filename: "assets/[name]-[hash:6].css"
}),
]
};
复制代码
这时候打包一下,看看打包文件:
├── assets
│ ├── Alata-Regular-e83420.ttf
│ ├── image-7503bc.png
│ ├── index-7fa71a.js
│ ├── main-7fa71a.css
│ └── main-7fa71a.js
└── index.html
复制代码
而后我随便改一下style.css
,再从新打包一下。
这时候会发现index.js
、main.js
、main.css
的文件名都会发生改变,但静态文件并不会发生变化。
├── assets
│ ├── Alata-Regular-e83420.ttf
│ ├── image-7503bc.png
│ ├── index-4b2329.js
│ ├── main-4b2329.css
│ └── main-4b2329.js
└── index.html
复制代码
而后咱们从新找一张图片,覆盖一下image.png
,而后从新打包。
这时候,index.js
、main.js
、main.css
的文件名依旧会发生改变,同时image.png
也发生了变化。
├── assets
│ ├── Alata-Regular-e83420.ttf
│ ├── image-f3f2ec.png
│ ├── index-46acaa.js
│ ├── main-46acaa.css
│ └── main-46acaa.js
└── index.html
复制代码
经过上面的例子,咱们能够简单总结出:
js
、css
打包文件的文件名都会发生变化,尽管来自多个chunk
。js
、css
打包文件的文件名也都会发生变化。而chunkhash
策略的话,是以chunk
为单位的,若是一个文件发生变化,只有那条chunk
相关的文件的打包文件文件名才会发生变化。
咱们依旧经过例子看看:
首先咱们先将配置文件都改为chunkhash
。这里注意的是chunkhash
不适用于静态文件,所以静态文件依旧使用hash
。
module.exports = {
entry: {
index: './src/js/index.js',
main: './src/js/main.js'
},
output: {
path: path.resolve(__dirname, 'dist/'),
filename: 'assets/[name]-[chunkhash:6].js',
assetModuleFilename: 'assets/[name]-[hash:6][ext]'
},
module: {
...
},
plugins: [
...
new miniCssExtractPlugin({
filename: "assets/[name]-[chunkhash:6].css"
}),
]
};
复制代码
先打包一次:
├── assets
│ ├── Alata-Regular-e83420.ttf
│ ├── image-f3f2ec.png
│ ├── index-6be98e.js
│ ├── main-a15a74.css
│ └── main-a15a74.js
└── index.html
复制代码
而后咱们首先修改一下style.css
,打包一下,会发现main.css
和main.js
都发生了变化,而index.js
不是一个chunk
的,所以不会发生变化。
├── assets
│ ├── Alata-Regular-e83420.ttf
│ ├── image-f3f2ec.png
│ ├── index-6be98e.js
│ ├── main-88f8ea.css
│ └── main-88f8ea.js
└── index.html
复制代码
一样,咱们再覆盖一下image.png
,再打包一下。
这时候image.png
当然会发生变化,而后index.js
也发生了变化,由于它们是一个chunk
的,而main.css
和main.js
就不会发生变化。
├── assets
│ ├── Alata-Regular-e83420.ttf
│ ├── image-7503bc.png
│ ├── index-89dfd4.js
│ ├── main-88f8ea.css
│ └── main-88f8ea.js
└── index.html
复制代码
简单总结一下:
chunk
的js
、css
打包文件的文件名都会发生变化。chunk
的js
、css
打包文件的文件名也都会发生变化。最后一个就是contenthash
策略, 它是以自身内容为单位的,所以当一个文件发生变化的时候,首先它自己的打包文件的名称会发生变化,其次,引入它的文件的打包文件也会发生变化。
惯例来个实验:
咱们将因此哈希占位符改为contenthash
。
module.exports = {
entry: {
index: './src/js/index.js',
main: './src/js/main.js'
},
output: {
path: path.resolve(__dirname, 'dist/'),
filename: 'assets/[name]-[contenthash:6].js',
assetModuleFilename: 'assets/[name]-[contenthash:6][ext]'
},
module: {
...
},
plugins: [
...
new miniCssExtractPlugin({
filename: "assets/[name]-[contenthash:6].css"
}),
]
};
复制代码
而后先打包一下。
├── assets
│ ├── Alata-Regular-e83420.ttf
│ ├── image-7503bc.png
│ ├── index-1e2b37.js
│ ├── main-02a4b4.css
│ └── main-c437b0.js
└── index.html
复制代码
首先咱们先修改一下图片吧,找一张新图覆盖一下image.png
,而后打包一下。
首先image.png
的名称必定会发生变化,由于它改动了。其次index.js
也会发生变化,这是由于它引入了image.png
,而image.png
的名称发生变化,所以它代码中引入的名称也得发生变化,所以index.js
的名称也会发生变化。
而main.js
和main.css
由于没有引用image.png
,所以不会发生变化。
├── assets
│ ├── Alata-Regular-e83420.ttf
│ ├── image-f3f2ec.png
│ ├── index-e241d6.js
│ ├── main-02a4b4.css
│ └── main-c437b0.js
└── index.html
复制代码
接下来,咱们来修改一下main.js
,而后打包一下。
咱们会发现,只有main.js
的打包文件会发生变化,而处于同个chunk
的main.css
却不会发生变化,这是由于main.css
没有引用main.js
。
├── assets
│ ├── Alata-Regular-e83420.ttf
│ ├── image-f3f2ec.png
│ ├── index-e241d6.js
│ ├── main-02a4b4.css
│ └── main-d1f8ed.js
└── index.html
复制代码
如今能够简单总结一下:
一般咱们项目都会有开发环境和生产环境。
前面咱们也看到了webpack
提供了一个mode
选项,但咱们开发中不太可能说开发的时候mode
设置为development
,而后等到要打包才设置为production
。固然,前面咱们也说了,咱们能够经过命令--mode
来对应匹配mode
选项。
但若是开发环境和生产环境的webpack
配置差别不只仅只有mode
选项的话,咱们可能须要考虑多份打包配置了。
咱们默认的webpack
配置文件名为webpack.config.js
,而webpack
执行的时候,也默认会找该配置文件。
但若是咱们不使用该文件名,而改为webpack.conf.js
的话,webpack
正常执行是会使用默认配置的,所以咱们须要使用一个--config
选项,来指定配置文件。
webpack --config webpack.conf.js
复制代码
所以,咱们就能够分别配置一个开发环境的配置webpack.dev.js
和生成环境的配置webpack.prod.js
,而后经过指令进行执行不一样配置文件:
// package.json
"scripts": {
"dev": "webpack --config webpack.dev.js",
"build": "webpack --config webpack.prod.js",
}
复制代码
若是说,你不想建立那么多配置文件的话,咱们也能够只只用webpack.config.js
来实现多份打包配置。
按照前面说的使用--mode
配置mode
选项,其实咱们能够在webpack.config.js
中拿到这个变量,所以咱们就能够经过这个变量去返回不一样的配置文件。
// argv.mode能够获取到配置的mode选项
module.exports = (env, argv) => {
if (argv.mode === 'development') {
// 返回开发环境的配置选项
return { ... }
}else if (argv.mode === 'production') {
// 返回生产环境的配置选项
return { ... }
}
};
复制代码
mode
选项和devtool
选项前面已经有讲到关于mode
选项和devtool
选项,而不一样选项打包的速度也会有所不一样,所以按照你的实际需求进行配置,有须要用到才生成,没须要用到就能省就省。
在配置文件中,其实有一个resovle.alias
选项,它能够建立import
和reuquire
别名,来确保模块引入变得更简单,同时webpack
在打包的时候也能更快的找到引入文件。
// webpack.config.js
const path = require('path');
module.exports = {
...
resolve: {
alias: {
// 配置style路径的别名
style: path.resolve(__dirname, 'src/style/')
},
}
};
复制代码
// 使用
import "style/style.scss";
import "style/style.css";
复制代码
当咱们使用loader
的时候,咱们能够配置include
来指定只解析该路径下的对应文件,同时咱们能够配置exclude
来指定不解析该路径下的对应文件。
const path = require('path');
module.exports = {
...
module: {
rules: [
{
test: /\.css$/,
use: [miniCssExtractPlugin.loader, 'css-loader', 'postcss-loader'],
include: [path.resolve(__dirname, 'src')] // 只解析src路径下的css
}
{
test: /\.js$/,
use: 'babel-loader',
exclude: /node_modules/ // 不解析node_modules路径下的js
}
]
}
};
复制代码
咱们能够在module.noParse
选项中,只配置不须要解析的文件。一般咱们会忽略一些大型插件从而来提升构建性能。
module.exports = {
...
module: {
noParse: /jquery|lodash/,
},
};
复制代码
在webpack
构建过程当中,其实大部分消耗时间都是用到loader
解析上面,一方面是由于转换文件数据量很大,另外一方面是由于JavaScript
单线程特性的缘由,所以须要一个个去处理,而不能并发操做。
而咱们可使用HappyPack
,将这部分任务分解到多个子进程中去进行并行处理,子进程处理完成后把结果发送到主进程中去,从而减小总的构建时间。
# 5.0.1
yarn add happypack -D
复制代码
// webpack.config.js
const HappyPack = require("happypack");
const os = require("os");
const HappyThreadPool = HappyPack.ThreadPool({size: os.cpus().length});
module.exports = {
...
module: {
rules: [
{
test: /\.js$/,
use: [{
loader: 'happypack/loader?id=happyBabelLoader'
}]
}
]
},
plugins: [
new HappyPack({
id: 'happyBabelLoader', // 与loader对应的id标识
// 用法跟loader配置同样
loaders: [
{loader: 'babel-loader', options: {}}
],
threadPool: HappyThreadPool // 共享进程池
})
]
};
复制代码
起码有聊到,当mode
为production
的时候,webpack
打包会开启代码压缩插件,同时webpack
也有提供一个optimization
选项,让咱们可使用本身喜欢的插件去覆盖原生插件。
所以,咱们可使用webpack-parallel-uglify-plugin
来覆盖原生代码压缩插件,它的一个优势就是能够并行执行。
# 2.0.0
yarn add webpack-parallel-uglify-plugin -D
复制代码
// webpack.config.js
const ParallelUglifyPlugin = require("webpack-parallel-uglify-plugin")
module.exports = {
...
optimization: {
minimizer: [
new ParallelUglifyPlugin({
// 缓存路径
cacheDir: '.cache/',
// 压缩配置
uglifyJS: {
output: {
comments: false,
beautify: false
},
compress: {
drop_console: true,
collapse_vars: true,
reduce_vars: true
}
}
})
]
}
};
复制代码
咱们每次执行构建都会把全部的文件都从新编译一边,若是咱们能够将这些重复动做缓存下来的话,对下一步的构建速度会有很大的帮助。
如今大部分的loader
都提供了缓存选项,但并不是全部的loader
都有,所以咱们最好本身去配置一下全局的缓存动做。
在Webpack5
以前,咱们都使用了cache-loader
,而在webpack5
中,官方提供了一个cache
选项给咱们带来持久性缓存。
// 开发环境
module.exports = {
cache: {
type: 'memory' // 默认配置
}
}
// 生产环境
module.exports = {
cache: {
type: 'filesystem',
buildDependencies: {
config: [__filename]
}
}
}
复制代码
咱们可使用webpack-bundle-analyzer
插件来帮助咱们分析打包文件,它会将打包后的内容束展现为方便交互的直观树状图,让咱们知道咱们所构建包中真正引入的内容。
# 4.4.2
yarn add webpack-bundle-analyzer -D
复制代码
// webpack.config.js
const {BundleAnalyzerPlugin} = require('webpack-bundle-analyzer');
module.exports = {
...
plugins: [
new BundleAnalyzerPlugin()
]
};
复制代码
而后咱们打包后,webpack
会自动打开一个页面,显示咱们打包文件的状况,经过打包报告能够很直观的知道哪些依赖包大,则能够作作针对性的修改。
若是不想每次运行都打开网页的话,咱们能够先将数据保存起来,而后要看的时候再执行新的命令去查看。
// webpack.config.js
new BundleAnalyzerPlugin({
analyzerMode: 'disabled',
generateStatsFile: true
})
复制代码
// package.json
"scripts": {
"analyzer": "webpack-bundle-analyzer --port 3000 ./dist/stats.json"
},
复制代码
在webpack
官网,它给提出了几个loader
的编写原则:
loader
只作一件事情;webpack
会按照顺序链式去调用每一个loader
;webpack
定制的设计规则和结构,输入和输入均为字符串,每一个loader
彻底独立,即插即用。同时webpack
还给咱们提供了loader API
,所以咱们可使用this
去获取须要用到的API
,但也是由于如此,咱们loader
的实现就不能使用箭头函数了。
今天,咱们来简单手写一下sass-loader
、css-loader
和style-loader
,而它们也有各自的单一功能:
sass-loader
:用来解析sass
和scss
代码;css-loader
:用来解析css
代码;style-loader
:将css
代码插入到js
中。首先,咱们先建立一个myLoders
文件夹,而后建立三个loader
文件。
├── myLoaders
│ ├── ou-css-loader.js
│ ├── ou-sass-loader.js
│ └── ou-style-loader.js
复制代码
而后咱们须要在webpack
引入,而且须要配置一下resolveLoader
选项,由于webpack
默认只会去node_modules
搜索loader
。
module.exports = {
...
resolveLoader: {
// 添加loader查询路径
modules: ['node_modules', './myLoaders']
},
module: {
rules: [{
test: /\.(scss|sass)$/,
// 使用本身的loader
use: ['ou-style-loader','ou-css-loader','ou-sass-loader']
}]
}
};
复制代码
首先咱们先来实现ou-sass-loader
。
loader
的本质就是一个函数,而咱们能够在函数的第一个参数获取到对应文件的代码,咱们能够先打印一下来看看。
// ou-sass-loader.js
module.exports = function(source) {
console.log(source);
}
复制代码
而后执行打包后,咱们能够看到咱们的scss
文件中的代码。
所以,咱们可使用sass
插件来进行解析scss
代码,sass
有一个render
函数能够去解析。
// ou-sass-loader.js
const sass = require('sass');
module.exports = function(source) {
// 使用render函数进行解析scss代码
sass.render({data: source}, (err, result) => {
console.log(result);
});
}
复制代码
咱们在执行一下打包,会发现result
是一个对象,而里面的css
就是咱们所须要的,所以咱们须要将其返回出去。
这里
css
是Buffer
,咱们须要去解析它,可是解析它是css-loader
的工做,而不是sass-loader
的工做。
{
css: <Buffer 62 6f 64 79 20 7b 0a 20 20 62 61 63 6b 67 72 6f 75 6e 64 3a 20 23 32 32 32 3b 0a 7d 0a 62 6f 64 79 20 64 69 76 20 7b 0a 20 20 63 6f 6c 6f 72 3a 20 23 ... 6 more bytes>,
map: null,
stats: {
entry: 'data',
start: 1628131813793,
end: 1628131813830,
duration: 37,
includedFiles: [ [Symbol($ti)]: [Rti] ]
}
}
复制代码
但这里是一个异步操做,咱们不能直接return
回去,而是须要使用到webpack
提供的一个API
——this.async
,它自己是一个函数,而后会返回一个callback()
让咱们能够返回异步的结果。
// ou-sass-loader.js
const sass = require('sass');
module.exports = function(source) {
// 获取callback函数
const callback = this.async();
sass.render({data: source}, (err, result) => {
// 将结果返回
if (err) return callback(err);
callback(null, result.css);
});
}
复制代码
这时候,咱们ou-sass-loader
就实现了,接下来咱们来实现ou-css-loader
。
它其实任务很简单,就是将ou-sass-loader
返回的css
解析为字符串就能够了。
// ou-css-loader.js
module.exports = function(source) {
return JSON.stringify(source)
}
复制代码
最后就是ou-style-loader
,它的任务就是建立一个style
标签,而后将ou-css-loader
返回的数据插进去,而且将style
标签放置到head
标签里面去。
// ou-style-loader.js
module.exports = function(sources) {
return ` const tag = document.createElement("style"); tag.innerHTML = ${sources}; document.head.appendChild(tag) `
}
复制代码
这时咱们简易版的sass-loader
、css-laoder
和style-laoder
就实现了,咱们能够执行一下打包命令,检验页面是否是有对应的样式效果。
在webpack
运行过程当中,会存在一个生命周期,而在生命周期中webpack
会广播出许多事情,而在plugin
中是能够监听到这些事件,所以plugin
是能够实如今合适的时机经过Webpack
提供的API
去实现一些动做。
正常状况下,一个plugin
是一个类,而且里面会有一个apply
函数,而在apply
函数中会接收到一个compiler
参数,里面包含了关于webpack
环境全部的配置信息。
module.exports = class MyPlugin {
apply (compiler) {}
}
复制代码
在compiler
中会暴露不少生命周期钩子函数,具体的能够查看文档。咱们能够经过如下方式去访问钩子函数。
compiler.hooks.someHook.tap(...)
复制代码
在tap
方法中,接收两个参数,一个是该plugin
的名称,一个是回调函数,而在回调函数中,又会接收到一个compilation
参数。
module.exports = class MyPlugin {
apply (compiler) {
compiler.hooks.compile.tap("MyPlugin", (compilation) => {
console.log(compilation)
})
}
}
复制代码
compilation
对象包含了当前的模块资源、编译生成资源、变化的文件等。当运行webpack
开发环境中间件时,每当检测到一个文件变化,就会建立一个新的 compilation
,从而生成一组新的编译资源。compilation
对象也提供了不少关键时机的回调,以供插件作自定义处理时选择使用。
compliation
也暴露了许多的钩子,具体的话能够去看看文档。
接下来,简单实现一下一个plugin
,打包后生成一个txt
文件,里面会打印出每一个bundle
的大小。
module.exports = class MyPlugin {
apply(compiler) {
// 生成资源到 output 目录以前
compiler.hooks.emit.tap("MyPlugin", (compilation) => {
let str = ''
for (let filename in compilation.assets) {
// 获取文件名称和文件大小
str += `${filename} -> ${compilation.assets[filename]['size']() / 1000}KB\n`
}
// 新建fileSize.txt
compilation.assets['fileSize.txt'] = {
// 内容
source: function () {
return str
}
}
})
}
}
复制代码
紧接着,咱们将其引入到webpack.config.js
,并在plugins
中建立实例。
const MyPlugin = require("./myPlugins/my-plugin")
module.exports = {
...
plugins: [
new MyPlugin()
]
};
复制代码
而后打包后,dist
文件中会生成一个fileSize.txt
文件。
assets/Alata-Regular-e83420.ttf -> 96.208KB
assets/image-f3f2ec.png -> 207.392KB
index.html -> 0.364KB
assets/index-41f0e2.css -> 0.177KB
assets/index-acc2f5.js -> 1.298KB
复制代码
喜欢的朋友能够点个
Star
哦~
首先咱们先初始化咱们的项目文件。
先新建一个src
路径,而后建立三个js
文件——index.js
、a.js
、b.js
。
// index.js
import {msg} from "./a.js";
console.log(msg);
// a.js
import {something} from "./b.js";
export const msg = `Hello ${something}`;
// b.js
export const something = 'World';
复制代码
而后咱们能够先安装webpack
,而后测试一下打包出来的bundle
文件有什么特色。
这里就很少说了,直接看
bundle
文件(默认配置,mode
为development
)
打包后,咱们能够看到bundle
文件有不少内容,但也有一大半注释。
其实咱们只须要看两个地方,一个是__webpack_modules__
变量。咱们能够看到它是一个对象,而后key
值为module
路径,而value
值是执行module
代码的函数。
var __webpack_modules__ = ({
"./src/a.js": (() => eval( ... )),
"./src/b.js": (() => eval( ... )),
"./src/index.js": (() => eval( ... ))
})
复制代码
其次,咱们能看到一个函数,叫__webpack_require__
,它接收一个moduleId
的参数。然而咱们能够在最后看到了这个函数的调用,就会发现其实moduleId
就是__webpack_modules__
的key
值,也就是module
的路径。
var __webpack_exports__ = __webpack_require__("./src/index.js");
复制代码
到这里,咱们就能够大概捋清楚webpack
打包的一个逻辑了。
webpack
是直接拿到js
文件的代码,即字符串。而后经过eval()
函数执行代码;webpack
会从入口文件开始,不断递归遍历引入模块,而后保持在一个对象里面,key
值为moduleId
,即模块路径,而value
是模块的相关代码。webpack
会将代码转换为commonJS
,即便用require
去引入模块,同时它自身会去封装一个require
函数,去执行入口文件代码。话很少说,咱们开始来手写代码。
首先咱们能够先初始化webpack
配置文件——webpack.config.js
。
const path = require("path");
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, "./dist"),
filename: 'index.js'
}
}
复制代码
其次,咱们新建一个lib
文件夹,而后建立一个webpack.js
,用来手写咱们的mini-webpack
。
咱们能够先初始化一下,Webpack
是一个类,其次构建函数会接受配置文件,其次会有一个run
函数,是webpack
的运行函数。
module.exports = class Webpack {
/** * 构造函数,获取webpack配置 * @param {*} options */
constructor(options) {}
/** * webpack运行函数 */
run() {
console.log('开始执行Webpack!')
}
}
复制代码
而后咱们须要一个执行文件,即在根路径建立一个debugger.js
。
const webpack = require('./lib/webpack');
const options = require('./webpack.config');
new webpack(options).run();
复制代码
紧接着咱们执行一下该文件。
node debugger.js
复制代码
这时候命令行就会打印出开始执行Webpack!
。
咱们能够开始手写mini-webpack
了。
首先,在构造函数中,咱们须要保存一下配置信息。
constructor(options) {
const {entry, output} = options;
this.entry = entry; // 入口文件
this.output = output; // 导出配置
}
复制代码
在执行的第一步,咱们须要来解析一下入口文件,所以咱们用一个parseModules
来实现这个功能。
module.exports = class Webpack {
constructor(options) {
...
}
run() {
// 解析模块
this.parseModules(this.entry);
}
/** * 模块解析 * @param {*} file */
parseModules(file) {}
}
复制代码
在parseModules
中,咱们须要作两件事情:分析模块信息、递归遍历引入模块。
咱们一步一步来实现。首先,封装一个getModuleInfo
函数,来分析模块信息。
parseModules(file) {
// 分析模块
this.getModuleInfo(file);
}
/** * 分析模块 * @param {*} file * @returns Object */
getModuleInfo(file) {}
复制代码
首先,咱们接收到的file
其实就是入口文件的相对路径,即./src/index.js
。所以咱们能够先用node
自带的fs
模块来读取文件内容。
getModuleInfo(file) {
// 读取文件
const body = fs.readFileSync(file, "utf-8");
}
复制代码
读取到内容后,咱们就要来分析一下文件内容了,这时候就须要用到了AST语法树
了。
抽象语法树 (
Abstract Syntax Tree
),简称AST
,它是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每一个节点都表示源代码中的一种结构。演示地址:astexplorer.net/
这里咱们用到的时候babel
的parse
插件,经过它来将JavaScript
转成AST
。
# 7.14.8
yarn add @babel/parser -D
复制代码
const fs = require("fs");
const parser = require("@babel/parser");
module.exports = class Webpack {
...
getModuleInfo(file) {
// 读取文件
const body = fs.readFileSync(file, "utf-8");
// 转化为AST语法树
const ast = parser.parse(body, {
sourceType: 'module' // 表示咱们解析的是ES模块
})
}
}
复制代码
紧接着,咱们还须要使用@babel/traverse
来遍历AST
,从而来识别该文件有没有引入其余模块,有的话就将其记录下来。
# 7.14.8
yarn add @babel/traverse -D
复制代码
const traverse = require("@babel/traverse").default;
复制代码
traverse
接受两个参数,第一个是ast
语法树,第二个是一个对象,在对象中咱们能够设置观察者函数,而且能够针对语法树中的特定节点类型。
好比咱们此次只须要找到引入模块的语句,对应的节点类型为ImportDeclaration
,咱们就能够设置对应的ImportDeclaration
函数,并在参数值获取到节点信息。
const fs = require("fs");
const path = require("path");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
module.exports = class Webpack {
...
getModuleInfo(file) {
// 读取文件
const body = fs.readFileSync(file, "utf-8");
// 转化为AST语法树
const ast = parser.parse(body, {
sourceType: 'module' // 表示咱们解析的是ES模块
})
traverse(ast, {
// visitor函数
ImportDeclaration({node}) {
console.log(node);
}
})
}
}
复制代码
咱们执行一下,能够打印出import {msg} from "./a.js"
的语法树。
所以,咱们须要将其路径收集起来。
// 依赖收集
const deps = {};
traverse(ast, {
// visitor函数
ImportDeclaration({node}) {
// 入口文件路径
const dirname = path.dirname(file);
// 引入文件路径
const absPath = "./" + path.join(dirname, node.source.value);
deps[node.source.value] = absPath;
}
})
复制代码
此时的deps
就是{ './a.js': './src/a.js' }
,之因此要保存它相对项目根路径的相对路径,是为了后面更好的去拿到它的文件内容。
收集完依赖后,咱们须要将AST
转回JavaScript
代码,而且将其转成ES5
语法。这时候咱们就会用到@babel/core
和@babel/preset-env
。
# @babel/core -> 7.14.8, @babel/preset-env -> 7.14.8
yarn add @babel/core @babel/preset-env -D
复制代码
const fs = require("fs");
const path = require("path");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const babel = require("@babel/core");
module.exports = class Webpack {
...
getModuleInfo(file) {
// 读取文件
const body = fs.readFileSync(file, "utf-8");
// 转化为AST语法树
const ast = parser.parse(body, {
sourceType: 'module' // 表示咱们解析的是ES模块
})
// 依赖收集
const deps = {};
traverse(ast, {
// visitor函数
ImportDeclaration({node}) {
// 入口文件路径
const dirname = path.dirname(file);
// 引入文件路径
const absPath = "./" + path.join(dirname, node.source.value);
deps[node.source.value] = absPath;
}
})
// ES6转成ES5
const {code} = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"],
})
}
}
复制代码
这时候咱们能够打印一下code
,会发现它再也不是ESModule
的引入方式了,而是使用了CommonJS
引入方式。
"use strict";
var _a = require("./a.js");
console.log(_a.msg);
复制代码
最终,getModuleInfo
会返回一个对象,对象里面包含着解析文件的路径,该文件的依赖对象以及文件代码。
parseModules(file) {
// 分析模块
const entry = this.getModuleInfo(file);
}
getModuleInfo(file) {
...
return {
file, // 文件路径
deps, // 依赖对象
code // 代码
};
}
复制代码
但咱们分析完入口文件后,咱们就须要进行递归遍历,去分析引入模块。
首先,咱们须要新建一个数组,保存一下全部的分析结果。其次,咱们来实现一下getDeps
函数,来递归遍历引入模块。
parseModules(file) {
// 分析模块
const entry = this.getModuleInfo(file);
const temp = [entry];
// 递归遍历,获取引入模块代码
this.getDeps(temp, entry)
}
/** * 获取依赖 * @param {*} temp * @param {*} module */
getDeps(temp, {deps}) {}
复制代码
在getDeps
中,咱们能够经过第二个参数获取到依赖对象,其次经过遍历这个对象,一一执行一下getModuleInfo
函数,获取各个依赖模块的解析内容,并保存到temp
。
最后,再自调用一下getDeps
,传入引入模块内容,继续递归遍历。
getDeps(temp, {deps}) {
// 遍历依赖
Object.keys(deps).forEach(key => {
// 获取依赖模块代码
const child = this.getModuleInfo(deps[key]);
temp.push(child);
// 递归遍历
this.getDeps(temp, child);
})
}
复制代码
这里还须要进行查重,好比在多个文件都引入了b.js
的话,temp
数组就会保存多个b.js
的内容对象,所以咱们能够先查重一下,若是temp
对象没有该模块,咱们再执行后面的操做。
getDeps(temp, {deps}) {
Object.keys(deps).forEach(key => {
// 去重
if (!temp.some(m => m.file === deps[key])) {
const child = this.getModuleInfo(deps[key]);
temp.push(child);
this.getDeps(temp, child);
}
})
}
复制代码
这时候,咱们模块解析的操做已经完成了差很少了。
最后咱们最须要将temp
数组,转换成对象,即跟__webpack_modules__
相似,以路径为key
名,而后value
为对应的内容信息。
parseModules(file) {
const entry = this.getModuleInfo(file);
const temp = [entry];
this.getDeps(temp, entry)
// 将temp转成对象
const depsGraph = {};
temp.forEach(moduleInfo => {
depsGraph[moduleInfo.file] = {
deps: moduleInfo.deps,
code: moduleInfo.code
}
})
return depsGraph;
}
复制代码
这时候,咱们在run()
函数保存一下解析结果,就完成了第一步操做了。
run() {
// 解析模块
this.depsGraph = this.parseModules(this.entry);
}
复制代码
下一步就是执行打包操做了,咱们先封装一个bundle
函数。
run() {
// 解析模块
this.depsGraph = this.parseModules(this.entry);
// 打包
this.bundle()
}
/** * 生成bundle文件 */
bundle() { }
复制代码
首先咱们先把简单的部分完成了,就是生成打包文件。
咱们要用到fs
模块,先识别打包路径存不存在,不存在的话新建一个目录,其次就写入bundle
文件。
bundle() {
const content = `console.log('Hello World')`;
// 生成bundle文件
!fs.existsSync(this.output.path) && fs.mkdirSync(this.output.path);
const filePath = path.join(this.output.path, this.output.filename);
fs.writeFileSync(filePath, content);
}
复制代码
这时运行一下打包命令,项目里就会出现一个dist
文件夹,里面会有一个index.js
。
console.log('Hello World')
复制代码
接下来咱们就得来实现bundle
文件的内容。
首先它是一个匿名函数只执行的方式,而后它接收一个参数__webpack_modules__
,即咱们前面解析文件的结果。
(function(__webpack_modules__){
...
})(this.depsGraph)
复制代码
其次,咱们须要是实现一下__webpack_require__
函数,它接收一个moduleId
参数,即路径参数。
而后咱们还须要去调用一下__webpack_require__
,并传入入口文件路径。
(function(__webpack_modules__){
function __webpack_require__(moduleId) {
...
}
__webpack_require__(this.entry)
})(this.depsGraph)
复制代码
前面咱们又看到,babel
将代码转义成commonJS
,所以咱们须要来实现一下require
函数,由于JavaScript
自己不具有。
require
函数的实质就是返回引入文件的内容。
同时,咱们还须要新建一个exports
对象,这样子模块导出的内容就能够保存到里面去了,最后也须要将其返回出去。
(function(__webpack_modules__){
function __webpack_require__(moduleId) {
// 实现require方法
function require(relPath) {
return __webpack_require__(__webpack_modules__[moduleId].deps[relPath])
}
// 保存导出模块
var exports = {};
return exports
}
__webpack_require__(this.entry)
})(this.depsGraph)
复制代码
最后,就只须要来执行一下入口文件的代码便可。
这里仍是使用一个匿名函数并自调用。
(function(__webpack_modules__){
function __webpack_require__(moduleId) {
// 实现require方法
function require(relPath) {
return __webpack_require__(__webpack_modules__[moduleId].deps[relPath])
}
// 保存导出模块
var exports = {};
// 调用函数
(function (require,exports,code) {
eval(code)
})(require,exports,__webpack_modules__[moduleId].code)
return exports
}
__webpack_require__(this.entry)
})(this.depsGraph)
复制代码
这时候咱们再将这段代码,换到content
变量中去。
bundle() {
const content = ` (function (__webpack_modules__) { function __webpack_require__(moduleId) { function require(relPath) { return __webpack_require__(__webpack_modules__[moduleId].deps[relPath]) } var exports = {}; (function (require,exports,code) { eval(code) })(require,exports,__webpack_modules__[moduleId].code) return exports } __webpack_require__('${this.entry}') })(${JSON.stringify(this.depsGraph)}) `;
// 生成bundle文件
!fs.existsSync(this.output.path) && fs.mkdirSync(this.output.path);
const filePath = path.join(this.output.path, this.output.filename);
fs.writeFileSync(filePath, content);
}
复制代码
而后执行打包,就能够看到完整的打包内容了。
(function (__webpack_modules__) {
function __webpack_require__(moduleId) {
function require(relPath) {
return __webpack_require__(__webpack_modules__[moduleId].deps[relPath])
}
var exports = {};
(function (require,exports,code) {
eval(code)
})(require,exports,__webpack_modules__[moduleId].code)
return exports
}
__webpack_require__('./src/index.js')
})({"./src/index.js":{"deps":{"./a.js":"./src/a.js"},"code":"\"use strict\";\n\nvar _a = require(\"./a.js\");\n\nconsole.log(_a.msg);"},"./src/a.js":{"deps":{"./b.js":"./src/b.js"},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.msg = void 0;\n\nvar _b = require(\"./b.js\");\n\nvar msg = \"Hello \".concat(_b.something);\nexports.msg = msg;"},"./src/b.js":{"deps":{},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.something = void 0;\nvar something = 'World';\nexports.something = something;"}})
复制代码
最后,咱们执行一下,看看能不能打印出Hello World
。
node ./dist/index.js
复制代码