前言
在 基础篇 中,我们基于webpack构建了我们的基础工程化环境,将我们认为需要的功能配置了上去。 除开公共基础配置之外,我们意识到两点:
- 开发环境(mode=development),追求强大的开发功能和效率,配置各种方便开发的功能;
- 生产环境(mode=production),追求更小更轻量的bundle(即打包产物);
接下来基于我们的开发需求,完善我们的工程化配置的同时,来介绍一些常用并强大的工具。
第一章:提高开发效率与完善团队开发规范
1.1 source-map
作为一个开发工程师——无论是什么开发,要求开发环境最不可少的一点功能就是——debug功能。 之前我们通过webpack, 将我们的源码打包成了 bundle.js 。 试想:实际上客户端(浏览器)读取的是打包后的 bundle.js ,那么当浏览器执行代码报错的时候,报错的信息自然也是bundle的内容。 我们如何将报错信息(bundle错误的语句及其所在行列)映射到源码上?
是的,souce-map。
webpack已经内置了sourcemap的功能,我们只需要通过简单的配置,将可以开启它。
module.exports = {
//开启 source map
//开发中推荐使用 'source-map'
//生产环境一般不开启 source map
devtool: 'source-map',
}
当我们执行打包命令之后,我们发现bundle的最后一行总是会多出一个注释,指向打包出的bundle.map.js(sourcemap文件)。sourcemap文件用来描述 源码文件和bundle文件的代码位置映射关系。基于它,我们将bundle文件的错误信息映射到源码文件上。
7种模式
除开'source-map'外,还可以基于我们的需求设置其他值,webpack——devtool一共提供了7种SourceMap模式:
| 模式 | 解释 |
|---|---|
| eval | 默认值。每个module会封装到 eval 里包裹起来执行,并且会在末尾追加注释 //@ sourceURL. |
| source-map | 生成一个SourceMap文件. |
| hidden-source-map | 和 source-map 一样,但不会在 bundle 末尾追加注释. |
| inline-source-map | 生成一个 DataUrl 形式的 SourceMap 文件. |
| eval-source-map | 每个module会通过eval()来执行,并且生成一个DataUrl形式的SourceMap. |
| cheap-source-map | 生成一个没有列信息(column-mappings)的SourceMaps文件,不包含loader的 sourcemap(譬如 babel 的sourcemap) |
| cheap-module-source-map | 生成一个没有列信息(column-mappings)的SourceMaps文件,同时 loader 的 sourcemap 也被简化为只包含对应行的。 |
要注意的是,生产环境我们一般不会开启sourcemap功能,主要有两点原因:
-
通过bundle和sourcemap文件,可以反编译出源码————也就是说,线上产物有soucemap文件的话,就意味着有暴漏源码的风险。
-
我们可以观察到,sourcemap文件的体积相对比较巨大,这跟我们生产环境的追求不同(生产环境追求更小更轻量的bundle)。
一道思考题: 有时候我们期望能第一时间通过线上的错误信息,来追踪到源码位置,从而快速解决掉bug以减轻损失。但又不希望sourcemap文件报漏在生产环境,有比较好的方案吗?
示例1:默认值 eval。
先搭建一个最基本的webpack环境。
1、生成package.json文件。
npm init -y
2、搭建一个最基本的webpack环境,安装以下包。
npm install webpack webpack-cli webpack-dev-server html-webpack-plugin -D
3、创建app.js。
console.log('hello,world!')
4、创建webpack.config.js。
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: './app.js',
output: {
clean: true
},
mode: 'development',
plugins: [
new HtmlWebpackPlugin()
]
}
5、打包。
npx webpack
6、打包效果。
7、启动 开发服务器。
npx webpack serve
8、在浏览器中访问 http://localhost:8080/ ,查看执行效果。
9、结论。
默认情况下,即使我们没有配置source map,webpack也会在开发环境下,设置source map,值为eval。相当于手动设置 devtool: false。
效果:eval可以准确定位到源代码。
示例2:关闭source map。
devtool: false
效果:关闭source map后,不可以准确定位到源代码。
示例3:source-map。
1、修改配置。
devtool: 'source-map',
2、打包。
3、效果:
1.生成了一个.map文件。
2.source-map 可以精准定位到源代码,可以定位到行数和列数。
示例4:hidden-source-map。
devtool: 'hidden-source-map'
效果:
1.生成了.map文件。
2.bundle末尾没有添加注释,所以bundle和.map文件无法关联起来。
3.不能定位到源代码。
示例5:inline-source-map。
devtool: 'inline-source-map',
效果:
1.没有生成.map文件,但是会将其内容以dataURL方式嵌入到bundle文件中。
2.可以定位到源代码。
示例6:eval-source-map。
devtool: 'eval-source-map',
效果:
1.没有生成单独的.map文件,但是会将其内容以dataURL方式嵌入到bundle文件中。
2.eval()执行module代码。
3.可以定位到源代码。
示例7:cheap-source-map。
devtool: 'cheap-source-map',
效果:
1.生成了一个.map文件。
2.source-map 可以精准定位到源代码,只能定位到行数,不能定位到列数。
cheap-source-map、source-map的区别:
source-map 会在 .map文件 / mappings 中记录源代码的行数和列数,而cheap-source-map只记录列数。
一般情况下,我们调试代码时,只需要知道代码的行数就可以了,并不需要关注列数。
示例8:cheap-module-source-map。
1、配置devtool。
devtool: 'cheap-module-source-map',
2、安装babel相关的包。
npm install babel-loader @babel/core @babel/preset-env -D
3、配置babel-loader。
babel的作用:将app.js中的类语法转换为es5的语法。
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
},
4、修改入口文件。
class A {
constructor() {
this.str = 'hello webpack'
}
sayHello(){
console.log(this.str)
}
}
const a = new A()
a.sayHello()
3、打包。
npx webpack
4、效果。
1.生成.map文件。
2.可以定位到被babel转换前的源代码,即定位到类语法的代码。
cheap-source-map、cheap-module-source-map的区别。
如果我们使用babel将app.js中的类语法转换为es5语法,我们还能定位到类语法代码吗?
cheap-module-source-map 可以定位到babel转换前的源代码,而cheap-source-map只能定位到babel转换后的代码。
示例:
推荐 cheap-module-source-map
在开发环境中,推荐大家使用 cheap-module-source-map 这个选项。因为它既可以生成单独的.map文件,还可以定位到babel转换前的源代码,能够大大提高调试代码的效率。
1.2 devServer
开发环境下,我们往往需要启动一个web服务,方便我们模拟一个用户从浏览器中访问我们的web服务,读取我们的打包产物,以观测我们的代码在客户端的表现。webpack内置了这样的功能,我们只需要简单的配置就可以开启它。
在此之前,我们需要安装它
npm install -D webpack-dev-server
devServer.proxy基于强大的中间件 http-proxy-middleware 实现的,因此它支持很多的配置项,我们基于此,可以做应对绝大多数开发场景的定制化配置。
webpack-dev-server的最底层实现是源自于node的http模块。
基础使用
通过 devServer选项 来配置webpack-dev-server如何工作。
示例1:
1、生成package.json文件。
npm init -y
2、搭建一个最基本的webpack环境,安装以下包。
npm install webpack webpack-cli webpack-dev-server html-webpack-plugin -D
3、app.js。
console.log('hello,webpack')
4、webpack.config.js。
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: './app.js',
output: {
clean: true
},
mode: 'development',
devtool: 'cheap-module-source-map',
//配置开发服务器
devServer: {
//设置web服务的根目录
static: path.resolve(__dirname, './dist')
},
plugins: [
new HtmlWebpackPlugin()
]
}
5、打包。
npx webpack
6、启动开发服务器。
npx webpack serve
7、浏览器中访问 http://localhost:8080/ 。
8、结论。
开发服务器启动成功。
开启gzips压缩
devServer.compress选项:是否开启服务器的资源压缩功能,可以减小传输的体积大小。
devServer: {
//启动web服务的gzips压缩功能,减小传输体积。
compress: true
},
效果:
结论:启动gzips压缩后,web服务器会在资源的请求响应头中添加一个字段:Content-Encoding: gzip。
设置端口号
devServer.port:设置web服务的端口号,默认为8080。
设置响应头
通过headers选项可以添加响应头。
示例:
1、配置文件。
devServer: {
//设置响应头
headers: {
'X-Access-Token': 'abc123' //token
}
},
2、效果。
启动代理
proxy选项。
我们打包出的 js bundle 里有时会含有一些对特定接口的网络请求(ajax/fetch)。要注意,此时客户端地址是在 http://localhost:8080/ 下,假设我们的接口来自http://localhost:4001/ ,那么毫无疑问,此时控制台里会报错并提示你跨域。
如何解决这个问题? 在开发环境下,我们可以使用devServer自带的proxy功能:
devServer: {
//开启代理
proxy: {
'/api': 'http:localhost:4001'
}
},
示例1:未开启代理。
1、搭建一个简单的后台接口服务器。
//server.js
const http = require('http')
const app = http.createServer((req, res) => {
if(req.url === '/api/hello'){
res.end('hello node')
}
})
app.listen(9000, 'localhost', ()=>{
console.log('后台服务器启动成功,请访问:http://localhost:9000')
})
2、启动后台接口服务器。
node server.js
3、在浏览器中访问 http://localhost:9000 。
说明后台接口服务器运行成功。
4、发起ajax请求。
入口文件中,通过fetch方法来请求后台接口。
fetch('http://localhost:9000/api/hello')
.then(response => response.text())
.then(result => {
console.console(result)
})
5、打包、启动服务器、浏览器执行效果。
6、结论。
未开启代理时,跨域请求会报错。
示例2:开启代理。
1、省略请求路径中的域名。
//app.js
fetch('/api/hello')
.then(response => response.text())
.then(result => {
console.log(result)
})
2、开启代理。
devServer: {
//开启代理
proxy: {
'/api': 'http://localhost:9000'
}
},
3、打包、启动服务器、浏览器执行效果。
请求成功了。
4、结论。
开启代理后,跨域问题就解决了。
现在,对 /api/hello 的请求会将请求代理到 http://localhost:9000/api/hello 。
https
如果想让我们的本地http服务变成https服务,我们只需要这样配置:
devServer: {
https: true
},
注意,此时我们访问 http://localhost:port 是无法访问我们的服务的,我们需要在地址栏里加前缀:https: 注意:由于默认配置使用的是自签名证书,所以有得浏览器会告诉你是不安全的,但我们依然可以继续访问它。 当然我们也可以提供自己的证书——如果有的话:
http2
如果想要配置http2,那么直接设置:
devServer: {
http2: true
},
即可,http2默认自带https自签名证书,当然我们仍然可以通过https配置项来使用自己的证书。
historyApiFallback
如果我们的项目是SPA应用,如Vue项目、React项目等,而且使用的是H5 history 历史路由模式时,我们在刷新页面或者直接在地址栏修改路由路径时,页面有可能会报错。如何解决呢?
如果我们的应用是个SPA(单页面应用),当路由到/some时(可以直接在地址栏里输入),会发现此时刷新页面后,控制台会报错。
GET http://localhost:3000/some 404 (Not Found)
此时打开network,刷新并查看,就会发现问题所在———浏览器把这个路由当作了静态资源地址去请求,然而我们并没有打包出/some这样的资源,所以这个访问无疑是404的。 如何解决它? 这种时候,我们可以通过配置来提供页面代替任何404的静态资源响应:
devServer: {
historyApiFallback: true,
},
[webpack-dev-server] 404s will fallback to '/index.html'
此时重启服务刷新后发现请求变成了index.html。 当然,在多数业务场景下,我们需要根据不同的访问路径定制替代的页面,这种情况下,我们可以使用rewrites这个配置项。 类似这样:
开发服务器主机
如果你在开发环境中起了一个devServer服务,并期望你的同事能访问到它,你只需要配置:
devServer: {
host: '0.0.0.0'
},
这时候,如果你的同事跟你处在同一局域网下,就可以通过局域网ip http://192.168.43.168:8080/ 来访问你的服务啦。
1.3 模块热替换与热加载
热替换
模块热替换(HMR - hot module replacement)功能会在应用程序运行过程中,替换、添加或删除 模块,而无需重新加载整个页面。
启用 webpack 的 热模块替换 特性,需要配置devServer.hot参数:
devServer: {
//开启热替换功能
hot: true
},
此时我们实现了基本的模块热替换功能。webpack默认开启了HMR功能。
HMR 加载样式
如果你配置了style-loader,那么现在已经同样支持样式文件的热替换功能了。
这是因为style-loader的实现使用了module.hot.accept,在CSS依赖模块更新之后,会对 style 标签打补丁。从而实现了这个功能。
热加载
热加载:文件更新时,自动刷新我们的服务和页面。
新版的webpack-dev-server默认已经开启了热加载的功能。 它对应的参数是devServer.liveReload,默认为true。 注意,如果想要关掉它,要将liveReload设置为false的同时,也要关掉hot。
热替换 和 热加载 有什么用处呢?它们可以帮助我们大大提高代码的调试效率。
示例1:css的热替换。
1、搭建一个基础的webpack环境,安装以下包。
npm install webpack webpack-cli webpack-dev-server html-webpack-plugin -D
npm i style-loader css-loader -D
2、设置配置文件。
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: './app.js',
output: {
clean: true
},
mode: 'development',
devtool: 'cheap-module-source-map',
devServer: {
static: path.resolve(__dirname, './dist'),
//开启hmr功能
hot: true
},
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
},
plugins: [
new HtmlWebpackPlugin()
]
}
3、style.css。
.square{
width: 60px;
height: 60px;
background-color: red;
margin: 30px 0;
}
4、app.js。
功能:点击按钮,添加一个正方形框。点击多次,则添加多个正方形框。
import './style.css'
const button = document.createElement('button')
button.textContent = '添加'
button.addEventListener('click', ()=>{
const div = document.createElement('div')
div.classList.add('square')
document.body.appendChild(div)
})
document.body.appendChild(button)
5、打包。
npx webpack
6、启动开发服务器。
npx webpack serve
7、浏览器执行效果。
1.先点击按钮三次,生成三个正方形框,它们的背景色都是red。
2.再修改style.css,将正方形框的背景色修改为green。
3.不需要重新打包和启动服务器。
4.在浏览器中查看执行效果。
8、结论。
修改style.css代码后,页面没有自动刷新,但是正方形框的背景色已经从红色变成了绿色。这就是css的热替换功能。
css的热替换功能:css模块代码发生修改后,页面会局部更新,显示最新的css代码的样式,而不会自动刷新页面,已经执行过的js代码的效果能够保留下来,只是样式更新了。
示例2:js的热替换。
1、input.js。
document.querySelector('#box').innerHTML = '<input type="text" value="test" />'
2、index.html。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<div id="box"></div>
</body>
</html>
3、配置文件。
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: './app.js',
output: {
clean: true
},
mode: 'development',
devtool: 'cheap-module-source-map',
devServer: {
static: path.resolve(__dirname, './dist'),
//开启hmr功能
hot: true
},
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './index.html'
})
]
}
4、app.js。
import './style.css'
import './input.js'
const button = document.createElement('button')
button.textContent = '添加'
button.addEventListener('click', ()=>{
const div = document.createElement('div')
div.classList.add('square')
document.body.appendChild(div)
})
document.body.appendChild(button)
5、打包、启动开发服务器、浏览器执行效果。
1.先点击三次按钮,在页面上生成三个正方形框。
2.再修改input.js代码。
3.不需要重新打包和启动开发服务器。
4.查看浏览执行效果。
6、结论。
修改input.js模块的代码后,页面发生了自动刷新,三个正方形框也消失了。说明input.js模块并没有开启热替换功能。
哪如何开启input.js模块的热替换功能呢?
7、修改app.js文件。
import './style.css'
import './input.js'
const button = document.createElement('button')
button.textContent = '添加'
button.addEventListener('click', ()=>{
const div = document.createElement('div')
div.classList.add('square')
document.body.appendChild(div)
})
document.body.appendChild(button)
//开启input.js模块的hmr功能
if(module.hot){
module.hot.accept('./input.js', ()=>{
})
}
通过 module.hot.accept() 方法来开启某个js模块的热替换功能。
参数1:要开启热替换功能的js模块。
参数2:热替换完成后的回调。
//开启input.js模块的hmr功能
if(module.hot){
module.hot.accept('./input.js', ()=>{})
}
8、重新打包、启动服务器、浏览器执行效果。
1.先点击三次按钮,在页面上生成三个正方形框。
2.再修改input.js代码。
3.不需要重新打包和启动开发服务器。
4.查看浏览执行效果。
9、结论。
通过accept()方法 开启 input.js 模块的热替换功能后,修改input.js代码后,页面不会自动刷新,其它模块的已经执行过的效果会保留,只是重新执行了input.js的代码。
小结
简单理解,热替换:修改某个模块的代码后,页面不会全部刷新,而是刷新发生修改的模块。发生修改的模块会被重新执行,显示最新代码的执行效果。而其它未修改的模块则不会重新执行,已经执行过的效果会保留下来。
热替换功能 实际上使用的是一个插件 HotModuleReplacementPlugin,在webpack 4.x版本中,需要手动安装和配置该插件。在webpack 5.x中,已经内置了,可以开箱即用。
如果是我们自己写的js模块,则需要在入口文件中,使用module.hot.accept()方法来手动开启该模块的热替换功能。
其实 css模块 也需要通过module.hot.accept()方法来开启热替换功能,但是style-loader帮我们完成了。
如果是Vue、React项目,框架会自动帮我们开启了热替换功能,不需要我们手动进行配置。
1.4 eslint
eslint是用来扫描我们所写的代码是否符合规范的工具。 往往我们的项目是多人协作开发的,我们期望统一的代码规范,这时候可以让eslint来对我们进行约束。 严格意义上来说,eslint配置跟webpack无关,但在工程化开发环境中,它往往是不可或缺的。
基本使用
在项目中,安装eslint包:
npm install eslint --save-dev
配置eslint,只需要在根目录下添加一个.eslintrc文件(或者.eslintrc.json, .js等)。 当然,我们可以使用eslint工具来自动生成它:
npx eslint --init
我们可以看到控制台里的展示:
并生成了一个配置文件(.eslintrc.json),这样我们就完成了eslint的基本规则配置。eslint配置文件里的配置项含义如下:
VSCode插件
VSCode中,可以安装一个eslint插件,该插件会直接在编辑区中,使用红色波浪线来标记出不符合规范的代码,而不需要手动执行npx eslint,也不需要在控制台中查看报错信息了,更加方便。
注意:在项目中,必须搭建eslint环境(安装eslint包,创建.eslintrc.js等),VSCode中的eslint插件 才能正常工作,对项目中的js文件进行语法检查。否则不会对项目中的js文件进行检查。
VSCode快捷键:Ctrl + ~ 打开命令行终端。
结合webpack使用
我们期望eslint能够实时提示报错而不必等待执行命令。 这个功能可以通过给自己的IDE(代码编辑器)安装对应的eslint插件来实现。 然而,不是每个IDE都有插件,如果不想使用插件,又想实时提示报错,那么我们可以结合 webpack 的打包编译功能来实现。
module:{
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: ['babel-loader', 'eslint-loader']
}
]
},
需要同时使用derServer。
现在我们就可以实时地看到代码里的不规范报错啦。
示例1:单独使用eslint。
1、安装eslint。
npm install eslint --save-dev
2、生成eslint配置文件。
npx eslint --init
//.eslintrc.js
module.exports = {
"env": {
"browser": true,
"es2021": true
},
"extends": [
"standard"
],
"parserOptions": {
"ecmaVersion": 13,
"sourceType": "module"
},
"rules": {
}
};
3、修改.eslintrc.js中的ecmaVersion为12。
"ecmaVersion": 12,
4、app.js。
console.log('hello, world')
5、执行语法检查指令。
npx eslint ./app.js
6、效果。
7、结论。
eslint 执行成功。
示例2:在webpack中使用eslint。
在示例1的基础上进行以下操作。
1、搭建一个基本的webpack环境,安装一些包。
npm install webpack webpack-cli webpack-dev-server html-webpack-plugin -D
2、安装bable相关的包。
npm install -D babel-loader @babel/core
3、安装eslint-loader。
npm install -D eslint-loader
4、webpack.config.js。
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: './app.js',
output: {
clean: true
},
mode: 'development',
devtool: 'cheap-module-source-map',
devServer: {
static: path.resolve(__dirname, './dist'),
},
module:{
rules: [
{
test: /\.js$/,
exclue: /node_modules/,
use: ['babel-loader', 'eslint-loader']
}
]
},
plugins: [
new HtmlWebpackPlugin()
]
}
对eslint进行配置的关键代码:
module:{
rules: [
{
test: /\.js$/,
exclue: /node_modules/,
use: ['babel-loader', 'eslint-loader']
}
]
},
7、打包,而且启动开发服务器。
npx webpack serve
8、浏览器执行效果。
在浏览中访问 http://localhost:8080/ 。
9、结论。
可以看到,页面上并没有正常显示,而是显示出了eslint的报错信息。 当我们改正了app.js文件中不符合eslint语法规范的代码后,页面就正常显示了。 说明我们成功在webpack中使用了eslint。
关闭覆盖层
如果我们不想在页面上显示 eslint语法检查的报错信息,则可以关闭覆盖层:
devServer: {
client: {
overlay: false
}
},
这样的话,就只在命令行窗口和浏览器控制台中,输出eslint语法检查的报错信息,而页面上就不会显示。
1.5 git-hooks 与 husky (建议先去学git)
为了保证团队里的开发人员提交的代码符合规范,我们可以在开发者上传代码时进行校验。 我们常用 husky 来协助进行代码提交时的 eslint 校验。在使用husky之前,我们先来研究一下 git-hooks 。
注意:
1、git相关的指令 要在git bash命令行中执行,而不是window命令行。 2、如何打开git bash命令行? 右击文件夹,选中Git Bash Here。
husky是一个基于git hooks实现的工具。
一般在开发时,不进行语法检查。提交时才进行语法检查。
git hooks的基本使用
示例1:不成功。
0.搭建一个基本的webpack环境。
1、检查git是否安装成功。
git --version
2、初始化git仓库。在项目根目录下打开命令行窗口,执行:
git init
3、查看.git目录。
1、执行 git init,会在项目根目录中生成一个隐藏的 .git 目录。
2、ls -la 指令:查看当前文件夹中的所有内容。
ls -la
4、创建.gitignore文件。
.gitignore文件用来指定哪些文件不用被提交。如node_modules不用提交。
//.gitignore
**/node_modules
5、查看仓库状态。
git status
6、添加到暂存区。
git add .
7、提交到本地库。
git commit -m 'init'
8、切换到.git文件夹,查看里面的内容。
可以发现,.git文件夹中有一个hooks文件夹,它里面就存放了hooks。
cd .git
ls -la
以字母d开的是文件夹,如 drwxr-xr-x 。
9、切换到hooks文件夹,查看里面的内容。
cd hooks
ls -la
可以发现,hooks文件夹中 有很多 .sample 后缀的文件。 这些.sample文件就是hooks。什么是hooks呢?它就是git 回调函数,git会在执行指令时自动调用它们。
10、查看 pre-commit.sample 文件。
cat pre-commit.sample
实际上,pre-commit.sample文件并不能执行。如果我们想要执行该文件,则需要创建一个pre-commit文件来代替它。
cat 指令:在命令行中输出指定文件的内容。
11、创建 pre-commit 文件。
touch pre-commit
pre-commit文件中可以编写一些脚本,当git进行提交时,就会先去执行这个文件中的代码。
touch 指令 :创建一个文件
12、编辑pre-commit文件内容。
vim pre-commit
//pre-commit
echo pre-commit
vim 指令: 编辑指定文件的内容。
vim常用编辑指令:
i 进入编辑状态
esc 退出编辑状态
:wq 保存退出
不按i键,按d d键两次 删除当前行
vim指令 编辑文件时,需要先切换到文件所在的目录。
12.2、给pre-commit添加可执行权限。
chmod +x ./pre-commit
错误:pre-commit 加不了可执行权限,为什么???
13、返回项目根目录。
cd ..
cd ..
cd .. :返回上一级目录
14、app.js。
随意写一些不符合eslint语法规范的代码,来进行测试。
console.log('hello, eslint');
15、git操作。
git status
git add .
git commit -m '2'
报错:提交失败了,pre-commit也没有被执行。
16、结论。
12步 和 15步报错了,失败了。但是视频中老师却成功了,为什么???
17、
假设我们在12、15步都成功了,继续往后进行。 17步及以后 是视频截屏,记录老师的操作,方便以后学习。
可以看到,提交时,pre-commit文件被执行了,输出了'pre-commit'。我们接下来只需要在pre-commit文件中,写入eslint语法检查的代码即可。
18、编辑pre-commit文件的内容。
npx eslint ./src
19、提交。
git status
git add .
git commit -m '3'
20、结论。
当app.js中有不符合规范的代码时,提交失败,命令行中输出eslint报错信息。这样我们就成功地 在提交前 使用pre-commit来对代码进行语法检查了,如果检查不通过,则提交失败。如果检查通过,则提交成功。
提交hooks
示例2:也没有成功。
说明:从示例1的10步接着往下做。
11、我们回到项目的根目录下,新建一个文件夹,暂时命名为".mygithooks"。然后在此文件夹下,新增一个git-hook文件,命名为"pre-commit",并写入以下内容:
npx eslint ./src
12、好了,我们新建了自己的git-hook,但此时git并不能识别。下面我们执行这行命令:
# 项目根目录下
git config core.hooksPath .mygithooks
上述命令给我们自己的文件,配置了git-hook的执行权限。
12.2 查看.git/config文件内容。
cd .git
ls -la
cat config
可以发现,config文件中有一个hooksPath字段。
13、给.mygithooks/pre-commit文件添加可执行权限。
chmod +x .mygithooks/pre-commit
14、提交。
git status
git add .
git commit -m '3'
15、结论。
提交时,pre-commit执行成功,eslint成功执行。这样,我们就成功使用git hooks来完成提交前对代码进行语法检查。
husky
示例3:husky的基本使用。没有成功。
0、搭建一个基本的webpack环境。
1、安装husky。
npm install husky --save-dev
2、启用 Git hooks。
npx husky install
3、package.json中,添加自定义脚本。
// package.json
{
"scripts": {
"prepare": "husky install"
}
}
4、创建一个hook。
1.在.husky目录中新建一个文件pre-commit。
2.在pre-commit文件中输入以下内容:
npx eslint ./src
4.2、给.husky/pre-commit添加可执行权限。
chmod +x .husky/pre-commit
4.3、解决4.2步中出现的无法设置可执行权限的问题。
注意在window系统中提供chmod指令为文件添加可执行权限时,可能会无效。
需要在文件的第一行添加以下语句:
#!/bin/bash
5、提交。
git status
git add .
git commit -m 'husky-01'
6、结论。
5步 执行提交时,报错了。而视频中老师却成功了。为什么???
小结
最好先去学习git。不然,很多git相关的指令都不会,而且容易报各种错误。
第二章:模块与依赖
在模块化编程中,开发者将程序分解为功能离散的文件,并称之为模块。 每个模块都拥有小于完整程序的体积,使得验证、调试及测试变得轻而易举。 精心编写的模块提供了可靠的抽象和封装界限,使得应用程序中每个模块都具备了条理清晰的设计和明确的目的。
Node.js 从一开始就支持模块化编程。 但,浏览器端的模块化还在缓慢支持中——截止到2021,大多主流浏览器已支持ESM模块化,因此基于ESM的打包工具生态逐渐开始活跃。
在前端工程化圈子里存在多种支持 JavaScript 模块化的工具,这些工具各有优势和限制。 Webpack从这些系统中汲取了经验和教训,并将 模块 的概念应用到项目的任何文件中。
2.1 Webpack 模块与解析原理
在讲webpack模块解析之前,我们先了解下webpack模块的概念,以及简单探究下webpack的具体实现。
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
- Sass
- Less
- JSON
- YAML
总的来讲,这些都可以被认为是webpack模块。
compiler与Resolvers
在我们运行webpack的时候(就是我们执行webpack命令进行打包时),其实就是相当于执行了下面的代码:
const webpack = require('webpack');
const compiler = webpack({
// ...这是我们配置的webpackconfig对象
})
webpack的执行会返回一个描述webpack打包编译整个流程的对象,我们将其称之为compiler。 compiler对象描述整个webpack打包流程———它内置了一个打包状态,随着打包过程的进行,状态也会实时变更,同时触发对应的webpack生命周期钩子。 (简单点讲,我们可以将其类比为一个Promise对象,状态从打包前,打包中到打包完成或者打包失败。) 每一次webpack打包,就是创建一个compiler对象,走完整个生命周期的过程。
而webpack中所有关于模块的解析,都是compiler对象里的内置模块解析器去工作的————简单点讲,你可以理解为这个对象上的一个属性,我们称之为Resolvers。 webpack的Resolvers解析器的主体功能就是模块解析,它是基于 enhanced-resolve 这个包实现的。换句话讲,在webpack中,无论你使用怎样的模块引入语句,本质其实都是在调用这个包的api进行模块路径解析。
2.2 模块解析(resolve)
webpack通过Resolvers实现了模块之间的依赖和引用。举个例子:
import _ from 'lodash';
// 或者
const add = require('./utils/add');
所引用的模块可以是来自应用程序的代码,也可以是第三方库。 resolver 帮助webpack 从每个 require/import 语句中,找到需要引入到 bundle 中的模块代码。当打包模块时,webpack 使用 enhanced-resolve 来解析文件路径。(webpack_resolver的代码实现很有思想,webpack基于此进行treeshaking,这个概念我们后面会讲到)。
1、webpack中的模块路径解析规则
通过内置的enhanced-resolve,webpack 能解析三种文件路径:
● 绝对路径
import '/home/me/file';
import 'C:\\Users\\me\\file';
由于已经获得文件的绝对路径,因此不需要再做进一步解析。
● 相对路径
import '../utils/reqFetch';
import './styles.css';
这种情况下,使用 import 或 require 的资源文件所处的目录,被认为是上下文目录。 在 import/require 中给定的相对路径,enhanced-resolve会拼接此上下文路径,来生成模块的绝对路径path.resolve(__dirname, RelativePath) 。 这也是我们在写代码时最常用的方式之一,另一种最常用的方式则是模块路径。
● 模块路径
import 'module';
import 'module/lib/file';
也就是在resolve.modules中指定的所有目录检索模块(node_modules里的模块已经被默认配置了)。 你可以通过配置别名的方式来替换初始模块路径, 具体请参照下面resolve.alias 配置选项。
绝对路径:可以在最前面使用一个 斜杆/ 表示当前项目的根目录路径。
模块路径:会去node_modules中去找。
相对路径:是相当于当前文件的路径。
2、resolve
alias 设置目录别名
我们可以通过 resolve.alias 来自定义配置模块路径。
alias 英 [ˈeɪliəs] n.别名;
为什么要使用别名呢?因为项目的目录结构如果非常深,使用相对路径来查找文件时,会非常麻烦。此时,可以为某个目录起一个别名,来简化路径,方便查找。
现在我们来是实现一下:首先,我们src目录下新建一个utils文件夹,并新建一个add.js文件,对外暴露出一个add函数。
// src/utils/add.js
export default function add(a, b){
return a + b;
}
然后我们在src/index.js中基于相对路径引用并使用它:
import add from './utils/add';
console.log(add);
很好,代码跑起来了并且没有报错。 这时我们期望能用@utils/add的方式去引用它,于是我们这样写了:
import add from '@utils/add';
console.log(add(a,b));
很明显它会报错,因为webpack会将其当做一个模块路径来识别———所以无法找到@utils这个模块。 这时,我们配置下resolve:
// webpack.config.js
const path = require('path');
module.exports = {
//...
resolve: {
alias: {
"@utils": path.resolve(__dirname, 'src/utils/')
},
},
};
如代码所示,我们将utils文件夹的绝对路径配置为一个模块路径,起一个别名为“@utils”。 重启服务发现,代码跑起来了。模块识别成功了。
省略后缀名
引入模块时,可以省略.js后缀名,只写文件名。webpack会自动优先查找.js后缀的同名文件。
extentions 设置查找优先级
resolve.extentions选项 设置webpack查找文件时的优先级。
上述代码中我们发现,只需要“import add from '@utils/add'”, webpack就可以帮我们找到add.js。 事实上,这与import add from '@utils/add.js' 的效果是一致的。 为什么会这样? 原来webpack的内置解析器已经默认定义好了一些 文件/目录 的路径解析规则。 比如当我们
import utils from './utils';
utils是一个文件目录而不是模块(文件),但webpack在这种情况下默认帮我们添加了后缀“/index.js”,从而将此相对路径指向到utils里的index.js。 这是webpack解析器默认内置好的规则。 那么现在有一个问题: 当utils文件夹下同时拥有add.js、add.json时,"@utils/add"会指向谁呢? @utils/add.json
{
"name": "add"
}
我们发现仍然指向到add.js。 当我们删掉add.js,会发现此时的引入的add变成了一个json对象。 上述现象似乎表明了这是一个默认配置的优先级的问题。 而webpack对外暴露了配置属性: resolve.extentions , 它的用法形如:
module.exports = {
//...
resolve: {
extensions: ['.js', '.json', '.wasm'],
},
};
webpack会按照数组顺序去解析这些后缀名,对于同名的文件,webpack总是会先解析列在数组首位的后缀名的文件。
一些例子
◎ 示例1:自定义模块 和 第三方模块。
1、在项目中搭建一个基本的webpack环境。
npm init -y
npm install webpack webpack-cli --save-dev
npm install lodash --save-dev
//webpack.config.js
module.exports = {
entry: './src/index.js',
output: {
clean: true
},
mode: 'development',
devtool: 'cheap-module-source-map',
}
3、编辑应用代码。
在入口文件中,分别使用ESModule、Commonjs来引入第三方模块lodash和自定义math.js。
//math.js
const add = (x, y) => {
return x + y
}
module.exports = {
add
}
//index.js
const math = require('./utils/math.js') //引入自定义模块
import _ from 'lodash' //引入第三方模块
console.log(math.add(5, 6))
console.log(_.join(['hellow', 'webpack']), ' ')
4、打包。
npx webpack
5、执行。
node main.js
6、结论。
可以看到webpack打包成功,自定义模块和第三方模块的代码也成功执行了。 这个例子说明了webpack天生就支持打包自定义模块和第三方模块。
◎ 示例2:webpack解析绝对路径。
在示例1的基础上,在index.js中,将自定义模块math.js的引用路径修改为绝对路径:
const math = require('/src/utils/math.js')
重新打包,执行打包后的bundle文件。 可以发现,main.js正常执行。 结论:webpack能够解析以绝对路径来引入的模块。
◎ 示例3:使用路径别名。
1.在webpack.config.js文件中,添加一个 resolve.alias 选项,来为某个目录设置别名。
const path = require('pah')
module.exports = {
entry: './src/index.js',
output: {
clean: true
},
mode: 'development',
devtool: 'cheap-module-source-map',
resolve: {
//设置目录别名
alias: {
'@': path.resolve(__dirname, './src')
}
}
}
2.使用目录别名。
//使用别名:@ 表示src目录
const math = require('@/utils/math.js')
import _ from 'lodash'
console.log(math.add(5, 6))
console.log(_.join(['hellow', 'webpack']), ' ')
3.重新打包,执行。
执行成功。说明我们配置目录别名成功了。
2.3 外部扩展(Externals)
有时候我们为了减小bundle的体积,从而把一些不变的第三方库用cdn的形式引入进来,比如jQuery: index.html
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js" ></script>
这个时候我们想在我们的代码里使用引入的jquery———但似乎三种模块引入方式都不行,这时候怎么办呢? webpack给我们提供了Externals的配置属性,让我们可以配置外部扩展模块:
module.exports = {
//...
externals: {
jquery: 'jQuery',
},
};
我们不想本地安装jquery,也不想直接放在window对象上,希望类似于已经安装好的方式来载入这个包,该怎么办呢?
我们尝试在代码中使用jQuery:
// index.js
import $ from 'jquery';
console.log($);
发现打印成功,这说明我们已经在代码中使用它。 注意:我们如何得知 { jquery:'jQuery'} 中的 'jQuery'? 其实就是cdn里打入到window中的变量名,比如jQuery不仅有jQuery变量名,还有$,那么我们也可以写成这样子:
module.exports = {
//...
externals: {
jquery: '$',
},
};
重启服务后发现,效果是一样的。
自动引入cdn链接
在html文件中,添加script标签和cdn链接,可以引入第三方模块,但是手动的效率比较低,能不能让webpack自动帮助我们来做呢?
修改externals选项,而且添加一个 externalsType 选项。
module.exports = {
//...
externalsType: 'script',
externals: {
jquery: [
'https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js',
'jquery'
]
}
}
value改为一个数组,第一个元素:第三方包的cdn链接,第二个元素:第三方包在window上暴露的对象。
externalsType:指定用script标签来引入cdn链接。
2.4 依赖图(dependency graph)
每当一个文件依赖另一个文件时,webpack 会直接将文件视为存在依赖关系。 这使得 webpack 可以获取非代码资源,如 images 或 web 字体等。并会把它们作为 依赖 提供给应用程序。 当 webpack 开始工作时,它会根据我们写好的配置,从 入口(entry) 开始,webpack 会递归的构建一个 依赖关系图,这个依赖图包含着应用程序中所需的每个模块,然后将所有模块打包为bundle(也就是output的配置项)。
单纯讲似乎很抽象,我们更期望能够可视化打包产物的依赖图,下边列示了一些bundle分析工具。
bundle 分析(bundle analysis) 工具:
官方分析工具 是一个不错的开始。还有一些其他社区支持的可选项:
- webpack-chart: webpack stats 可交互饼图。
- webpack-visualizer: 可视化并分析你的 bundle,检查哪些模块占用空间,哪些可能是重复使用的。
- webpack-bundle-analyzer:一个 plugin 和 CLI 工具,它将 bundle 内容展示为一个便捷的、交互式、可缩放的树状图形式。
- webpack bundle optimize helper:这个工具会分析你的 bundle,并提供可操作的改进措施,以减少 bundle 的大小。
- bundle-stats:生成一个 bundle 报告(bundle 大小、资源、模块),并比较不同构建之间的结果。
我们来使用 webpack-bundle-analyzer 实现。
# 首先安装这个插件作为开发依赖
# NPM
npm install --save-dev webpack-bundle-analyzer
# Yarn
yarn add -D webpack-bundle-analyzer
然后我们配置它:
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
// ...others
new BundleAnalyzerPlugin()
]
}
这时我们执行打包命令,发现控制台里打印出下面这样的日志:
Webpack Bundle Analyzer is started at http://127.0.0.1:8888
Use Ctrl+C to close it
asset bundle.js 5.46 KiB [emitted] [minimized] (name: main) 1
related asset
asset index.html 352 bytes [emitted]
orphan modules 2.35 KiB [orphan] 3 modules
...
我们在浏览器中打开 http://127.0.0.1:8888 ,我们成功可视化了打包产物依赖图!
注意: 对于 HTTP/1.1 的应用程序来说,由 webpack 构建的 bundle 非常强大。当浏览器发起请求时,它能最大程度的减少应用的等待时间。 而对于 HTTP/2 来说,我们还可以使用代码分割进行进一步优化。(开发环境观测的话需要在DevServer里进行配置{http2:true, https:false})。这个我们会在之后的课程里讲。
◎ 示例
1.搭建测试项目。
npm init -y
npm install --save-dev webpack webpack-cli webpack-dev-server html-webpack-plugin
npm install --save lodash
//安装依赖图插件
npm install --save-dev webpack-bundle-analyzer
2.编辑测试代码。
//math.js
export function add(x, y){
return x + y
}
//app.js
import { add } from './math.js'
console.log(add(6, 7))
//app2.js
import(/* webpackChunkName: 'lodash' */ 'lodash')
.then(({ default: _ })=>{
console.log(_.join(['hello', 'webpack'], ' '))
})
1、
/* webpackChunkName: 'lodash' */是一个魔法注释,告诉webpack在打包时,将当前导入的模块单独打包成一个文件,文件名为lodash。此时,lodash是动态加载的。
2、import() 方法返回的是一个promise,可以链式调用then()方法。then()方法中可以拿到模块导出的数据。
3、default:_表示将lodash默认导出的对象重命名为下划线。default表示默认导出的对象。
4、设置了两个入口文件:app.js、app2.js。 app.js中引用了自定义模块math.js。 app2.js中引用了第三方模块lodash。
3.配置文件。
//webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
//导入依赖图插件
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
module.exports = {
entry: {
app: './src/app.js',
app2: './src/app2.js
},
output: {
clean: true
},
mode: 'development',
devtool: 'cheap-module-source-map',
devServer: {
static: path.resolve(__dirname, './dist'),
},
plugins: [
new HtmlWebpackPlugin(),
new BundleAnalyzerPlugin()
]
}
4.打包,启动开发服务器,浏览器执行效果。
npx webpack
npx webpack serve
5.结论。
1、执行打包,webpack-bundle-analyzer插件会自动打开一个浏览器页面 http://127.0.0.1:8888/ ,显示当前项目的模块依赖图。
2、左侧是工具栏。工具栏中,有三种统计方式: Stat、Parsed、Gzipped,分别表示统计、解析、gzip,还列出了各个bundle的体积大小。
3、右侧是图形化的模块信息,列出了当前项目打包输出了哪些模块。每个矩形框都是一个输出的bundle,它们有不同的背景色,其中偏蓝色的是静态加载的模块,即以script标签引入的。偏白色的是动态加载的模块。
4、通过使用webpack-bundle-analyzer插件,可以很方便地看出项目打包输出了多少个bundle,每个bundle的体积,以及它们之间的依赖关系。
第三章:扩展功能
3.1 PostCSS与CSS模块
PostCSS 是一个用 JavaScript 工具和插件转换 CSS 代码的工具。比如可以使用 Autoprefixer 插件自动获取浏览器的流行度和能够支持的属性,并根据这些数据帮我们自动的 为 CSS 规则添加前缀,将最新的 CSS 语法转换成大多数浏览器都能理解的语法。
CSS 模块 能让你永远不用担心命名太大众化而造成冲突,只要用最有意义的名字就行了。
PostCSS
PosetCSS 与 Webpack 结合,需要安装 style-loader , css-loader , postcss-loader 三个loader:
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader',
'postcss-loader'
]
}
]
},
然后在项目根目录下创建 postcss.config.js :
module.exports = {
plugins: [
require('autoprefixer'),
require('postcss-nested')
]
}
插件 autoprefixer 提供自动给样式加前缀去兼容浏览器, postcss-nested 提供编写嵌套的样式语法。
最后,在 package.json 内增加如下实例内容:
"browserslist": [
"> 1%",
"last 2 versions"
]
数组内的值对应的含义:
1.last 2 versions : 每个浏览器中最新的两个版本。
2.> 1% or >= 1% : 全球浏览器使用率大于1%或大于等于1%。
autoprefixer、postcss-nested都是postcss的插件。
autoprefixer:为css样式添加兼容前缀。
postcss-nested:允许我们在.css文件中使用嵌套语法,来简化选择器的编写。
CSS 模块
目前还有一个问题,就是多人编写的样式可能会冲突,开启 CSS 模块可以解决这个问题。
webpack.config.js 配置:
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
// 开启css模块
modules: true
}
},
'postcss-loader'
]
}
]
},
样式文件 style.css :
body {
display: flex;
background-color: pink;
//定义一个类样式 .box
.box{
width: 100px;
height: 100px;
background-color: green;
}
}
在 js 文件里导入 css 文件:
// 开启 css 模块后,可以导入模块
import style from './style.css'
const div = document.createElement('div')
div.textContent = 'hello webpack'
// style 里可以识别 class 样式
div.classList.add(style.box)
document.body.appendChild(div)
也可以部分开启 CSS 模块模式,比如全局样式可以冠以 .global 前缀,如:
- *.global.css 普通模式
- *.css css module模式
如何理解 部分开启 CSS 模块模式?某些样式需要使用 css模块 来进行唯一性标识,避免被同名覆盖,而另一些样式是全局的,需要覆盖其它同名的样式,则不需要使用css模块。
如 全局样式 是统一的,需要覆盖同名的自定义样式,所以不使用 css 模块,而我们自己的样式则不希望被其它人的同名样式覆盖,则 使用css 模块。
这里统一用 global 关键词进行识别。用正则表达式匹配文件:
1、new RegExp(`^(?!.*\\.global).*\\.css`) 这个正则表达式用来匹配后缀名中包 .global 的.css文件,即全局样式文件,则不需要开启css module。
2、new RegExp(`^(.*\\.global).*\\.css`) 这个正则表达式用来匹配后缀名中不包 .global 的.css文件,即普通样式文件,则需要开启css module。
3、exclude选项:排除node_modules目录下的css文件,即不需要为第三方包的css文件为来设置css module。
◎ 示例:autoprefixer插件的基本使用。
1、搭建测试项目。
npm init -y
npm install --save-dev webpack webpack-cli webpack-dev-server html-webpack-plugin
npm install --save-dev style-loader css-loader
npm install --save-dev postcss-loader
npm install --save-dev autoprefixer
npm install --save-dev postcss-nested
2、编辑测试代码。
//style.css
body {
display: flex;
background-color: pink;
}
//app.js
import './style.css'
3、webpack.config.js 。
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: './src/app.js',
output: {
clean: true
},
mode: 'development',
devtool: 'cheap-module-source-map',
devServer: {
static: path.resolve(__dirname, './dist'),
},
//关键代码
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader',
'postcss-loader'
]
}
]
},
plugins: [
new HtmlWebpackPlugin(),
]
}
4、postcss.config.js 。
在项目根目录新建一个postcss.config.js文件,写入以下内容:
module.exports = {
plugins: [
require('autoprefixer')
]
}
5、package.json 。
在package.json中添加一个browserslist选项。
"browserslist": [
"> 1%",
"last 2 versions"
]
6、打包、启动服务器、浏览器执行效果。
npx webpack
npx webpack serve
7、结论。
autoprefixer插件成功为flex属性添加了兼容性前缀 -webkit 和 -ms。
◎ 示例:postcss-nested插件的基本使用。
在示例1的基础上,继续练习。
1、postcss.config.js
module.exports = {
plugins: [
require('autoprefixer'),
//配置postcss-nested插件
require('postcss-nested')
]
}
2、style.css
body {
display: flex;
background-color: pink;
/* 嵌套 */
div{
width: 100px;
height: 100px;
background-color: green;
}
}
3、ap.js
import './style.css'
let div = document.createElement('div')
document.body.appendChild(div)
4、打包、启动服务器、浏览器执行效果。
5、结论。
可以发现,postcss插件 能够解析.css文件中的嵌套代码,转换成浏览器能够识别的正常的样式代码。
◎ 示例:css模块的基本使用。
在示例2的基础上,继续练习。
1、编辑测试代码。
//webpack.config.js
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
// 开启css模块
modules: true
}
},
'postcss-loader'
]
}
]
},
//app.js
import style from './style.css'
console.log(style)
let div = document.createElement('div')
div.textContent = 'hello css module'
div.classList.add(style.box)
document.body.appendChild(div)
//style.css
body {
display: flex;
background-color: pink;
.box{
width: 100px;
height: 100px;
background-color: green;
}
}
2、打包、启动服务器、浏览器执行效果。
3、结论。
1、css module 会将.css文件中的类名 变成一个个具有唯一性的哈希值。
2、Vue、React框架也使用了css module机制,来避免样式的冲突。
3.2 Web Works
有时我们需要在客户端进行大量的运算,但又不想让它阻塞我们的js主线程。你可能第一时间考虑到的是异步。
但事实上,运算量过大(执行时间过长)的异步也会阻塞js事件循环,甚至会导致浏览器假死状态。
这时候,HTML5的新特性 WebWorker就派上了用场。
在此之前,我们简单的了解下这个特性。
html5之前,打开一个常规的网页,浏览器会启用几个线程?
一般而言,至少存在三个线程(公用线程不计入在内):
分别是js引擎线程(处理js)、GUI渲染线程(渲染页面)、浏览器事件触发线程(控制交互)。
当一段JS脚本长时间占用着处理机,就会挂起浏览器的GUI更新,而后面的事件响应也被排在队列中得不到处理,从而造成了浏览器被锁定进入假死状态。
现在如果遇到了这种情况,我们可以做的不仅仅是优化代码————html5提供了解决方案,webworker。
webWorkers提供了js的后台处理线程的API,它允许将复杂耗时的单纯js逻辑处理放在浏览器后台线程中进行处理,让js线程不阻塞UI线程的渲染。
多个线程间也是可以通过相同的方法进行数据传递。
它的使用方式如下:
//new Worker(scriptURL: string | URL, options?: WorkerOptions)
new Worker("someWorker.js");
也就是说,需要单独写一个js脚本,然后使用new Worker来创建一个Work线程实例。
这意味着并不是将这个脚本当做一个模块引入进来,而是单独开一个线程去执行这个脚本。
我们知道,常规模式下,我们的webpack工程化环境只会打包出一个bundle.js,那我们的worker脚本怎么办?也许你会想到设置多入口(Entry)多出口(ouotput)的方式。事实上不需要那么麻烦,webpack4的时候就提供了worker-loader专门配置webWorker。令人开心的是,webpack5之后就不需要用loader啦,因为webpack5内置了这个功能。
我们来试验一下:
● 第一步
创建一个work脚本 work.js,我们甚至不需要写任何内容,我们的重点不是webWorker的使用,而是在webpack环境中使用这个特性。当然,也可以写点什么,比如:
● 在 index.js 中使用它
(import.meta.url这个参数能够锁定我们当前的这个模块——注意,它不能在commonjs中使用。)
这时候我们执行打包命令,会发现,dist目录下除了bundle.js之外,还有另外一个xxx.bundle.js!
这说明我们的webpack5自动的将被new Work使用的脚本单独打出了一个bundle。
我们加上刚才的问答代码,执行npm run dev,发现它是能够正常工作。并且在network里也可以发现多了一个src_worker_js.bundle.js。
总结: webpack5以来内置了很多功能,让我们不需要过多的配置,比如之前讲过的hot模式,和现在的web workder。
◎ 示例
1、搭建webpack环境。
npm init -y
npm install --save-dev webpack webpack-cli webpack-dev-server html-webpack-plugin
2、搭建测试项目。
//work.js
self.onmessage = (message) => {
self.postMessage({
answer: 666
})
}
//app.js
const worker = new Worker(new URL('./work.js', import.meta.url))
worker.postMessage({
question: 'hi,那边的worker线程,请告诉我今天的幸运数字是多少?'
})
worker.onmessage = (message) => {
console.log(message)
console.log(message.data.answer)
}
//webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: './src/app.js',
output: {
clean: true
},
mode: 'development',
devtool: 'cheap-module-source-map',
devServer: {
static: path.resolve(__dirname, './dist'),
},
plugins: [
new HtmlWebpackPlugin(),
]
}
3、打包、启动服务器、浏览器执行效果。
npx webpack
npx webpack serve --open
4、结论。
可以看到,work.js被单独打包输出成一个文件,而且它在浏览器中正常执行了。
3.3 TypeScript
在前端生态里,TS扮演着越来越重要的角色。 我们直入正题,讲下如何在webpack工程化环境中集成TS。
首先,当然是安装我们的ts和对应的loader。
npm install --save-dev typescript ts-loader
接下来我们需要在项目根目录下添加一个ts的配置文件————tsconfig.json,我们可以用ts自带的工具来自动化生成它。
npx tsc --init
我们发现生成了一个tsconfig.json,里面注释掉了绝大多数配置。 现在,根据我们想要的效果来打开对应的配置。
{
"compilerOptions": {
"outDir": "./dist/",
"noImplicitAny": true,
"sourceMap": true,
"module": "es6",
"target": "es5",
"jsx": "react",
"allowJs": true,
"moduleResolution": "node"
}
}
好了,接下来我们新增一个src/index.ts,内置一些内容。
然后我们别忘了更改我们的entry及配置对应的loder。
当然,还有resolve.extensions,将.ts放在.js之前,这样它会先找.ts。
注意,如果我们使用了sourceMap,一定记得和上面的ts配置一样,设置sourcemap为true。
也别忘记在我们的webpack.config.js里,添加sourcemap,就像我们之前课程里讲的那样。
更改如下:
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: './src/app.ts',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, './dist'),
clean: true
},
mode: 'development',
devtool: 'inline-source-map',
devServer: {
static: path.resolve(__dirname, './dist'),
},
resolve: {
extensions: ['.ts', '.js']
},
module: {
rules: [
{
test: /\.ts$/,
use: 'ts-loader',
exclude: /node_modules/
}
]
},
plugins: [
new HtmlWebpackPlugin(),
]
}
运行我们的项目,我们发现完全没有问题呢!
使用第三方类库
在从 npm 上安装第三方库时,一定要记得同时安装这个库的类型声明文件(typing definition)。
我们可以从 TypeSearch中找到并安装这些第三方库的类型声明文件(www.typescriptlang.org/dt/search?s…) 。
举个例子,如果想安装 lodash 类型声明文件,我们可以运行下面的命令:
npm install --save-dev @types/lodash
eslint & ts
注意,如果要使用eslint,使用初始化命令的时候,记得选择“使用了typesctipt”。
npx eslint --init
# 往下选择的时候选择使用了typesctipt
如果已经配置了eslint,但没有配置ts相关的配置,那么我们需要先安装对应的plugin:
yarn add -D @typescript-eslint/eslint-plugin@latest @typescript-eslint/parser@latest
注意如果需要用到react的话,记得也要安装
yarn add -D eslint-plugin-react@latest
vue或者其他常用框架同样如此,一般都会有专门的plugin。
然后我们对.esilntrc进行更改~
执行npm run eslint试一下!
大功告成!
一些示例
◎ 示例:webpack打包.ts文件。
1、搭建webpack环境。
npm init -y
npm install --save-dev webpack webpack-cli webpack-dev-server html-webpack-plugin
npm install --save-dev typescript ts-loader
2、搭建测试项目。
//app.ts
const age: number = 18
console.log(age)
//webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: './src/app.ts',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, './dist'),
clean: true
},
mode: 'development',
devtool: 'inline-source-map',
devServer: {
static: path.resolve(__dirname, './dist'),
},
resolve: {
extensions: ['.ts', '.js']
},
module: {
rules: [
{
test: /\.ts$/,
use: 'ts-loader',
exclude: /node_modules/
}
]
},
plugins: [
new HtmlWebpackPlugin(),
]
}
3、创建tsconfig.json文件。
npx tsc --init
3.2、修改tsconfig.json文件
4、打包、启动服务器、效果。
npx webpack
npx webpack serve --open
5、结论。
可以看到,.ts文件被转换为了.js文件,而且在浏览器中正常执行了。
这说明,我们成功地让webpack来解析和打包.ts文件。
◎ 示例:.ts中使用第三方包。
在示例1的基础上,继续练习。
1、安装lodash。
npm install lodash
2、安装lodash的类型文件。
npm i @types/lodash --save-dev
3、app.ts
import _ from 'lodash'
const age: number = 18
console.log(age)
console.log(_.join(['hello', 'world'], ' '))
4、打包、启动服务器、效果。
npx webpack
npx webpack serve --open
5、结论。
可以发现,lodash正常执行,说明我们成功在ts文件中使用了第三方包。
小结
1、
/* webpackChunkName: 'lodash' */是一个魔法注释,告诉webpack在打包时,将当前要导入的模块单独打包成一个文件,文件名为lodash。
2、import() 方法返回的是一个promise,可以链式调用then()方法。
官方链接
视频教程
千锋最新前端webpack5全套教程,全网最完整的webpack教程(基础+高级)
配套资料
老师笔记:H:\学习课程\ediary日记\学习课程\webpack5_千峰\资料\笔记-webpack5学习指南-V1.0-去水印.pdf
资料目录:H:\学习课程\ediary日记\学习课程\webpack5_千峰\资料
练习代码目录:H:\学习课程\ediary日记\学习课程\webpack5_千峰\wepack5_千峰_练习代码目录
其它
进度:220104-