webpack5:高级篇(1)

791 阅读33分钟

前言

基础篇 中,我们基于webpack构建了我们的基础工程化环境,将我们认为需要的功能配置了上去。 除开公共基础配置之外,我们意识到两点:

  1. 开发环境(mode=development),追求强大的开发功能和效率,配置各种方便开发的功能;
  2. 生产环境(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功能,主要有两点原因:

  1. 通过bundle和sourcemap文件,可以反编译出源码————也就是说,线上产物有soucemap文件的话,就意味着有暴漏源码的风险。

  2. 我们可以观察到,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、打包效果。

image.png

7、启动 开发服务器。

npx webpack serve

8、在浏览器中访问 http://localhost:8080/ ,查看执行效果。

image.png

image.png

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、打包。

image.png

image.png

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',

image.png

效果:
1.没有生成.map文件,但是会将其内容以dataURL方式嵌入到bundle文件中。
2.可以定位到源代码。

示例6:eval-source-map。

devtool: 'eval-source-map',

image.png

效果:
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转换后的代码。

示例: image.png

image.png

image.png

推荐 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/

image.png

8、结论。

开发服务器启动成功。

开启gzips压缩

devServer.compress选项:是否开启服务器的资源压缩功能,可以减小传输的体积大小。

devServer: {
    //启动web服务的gzips压缩功能,减小传输体积。
    compress: true
},

效果: image.png

结论:启动gzips压缩后,web服务器会在资源的请求响应头中添加一个字段:Content-Encoding: gzip。

设置端口号

devServer.port:设置web服务的端口号,默认为8080。

设置响应头

通过headers选项可以添加响应头。

示例:
1、配置文件。

devServer: {
    //设置响应头
    headers: {
        'X-Access-Token': 'abc123' //token
    }
},

2、效果。

image.png

启动代理

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

image.png

说明后台接口服务器运行成功。

4、发起ajax请求。

入口文件中,通过fetch方法来请求后台接口。

fetch('http://localhost:9000/api/hello')
.then(response => response.text())
.then(result => {
    console.console(result)
})

5、打包、启动服务器、浏览器执行效果。

image.png

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、打包、启动服务器、浏览器执行效果。

image.png

请求成功了。

4、结论。

开启代理后,跨域问题就解决了。

现在,对 /api/hello 的请求会将请求代理到 http://localhost:9000/api/hello

https

如果想让我们的本地http服务变成https服务,我们只需要这样配置:

devServer: {
    https: true
},

注意,此时我们访问 http://localhost:port 是无法访问我们的服务的,我们需要在地址栏里加前缀:https: 注意:由于默认配置使用的是自签名证书,所以有得浏览器会告诉你是不安全的,但我们依然可以继续访问它。 当然我们也可以提供自己的证书——如果有的话:

image.png

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这个配置项。 类似这样:

image.png

开发服务器主机

如果你在开发环境中起了一个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,那么现在已经同样支持样式文件的热替换功能了。

image.png

这是因为style-loader的实现使用了module.hot.accept,在CSS依赖模块更新之后,会对 style 标签打补丁。从而实现了这个功能。

热加载

热加载:文件更新时,自动刷新我们的服务和页面。

新版的webpack-dev-server默认已经开启了热加载的功能。 它对应的参数是devServer.liveReload,默认为true。 注意,如果想要关掉它,要将liveReload设置为false的同时,也要关掉hot。

image.png

热替换 和 热加载 有什么用处呢?它们可以帮助我们大大提高代码的调试效率。

示例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

image.png

6、启动开发服务器。

npx webpack serve

7、浏览器执行效果。

1.先点击按钮三次,生成三个正方形框,它们的背景色都是red。
2.再修改style.css,将正方形框的背景色修改为green。
3.不需要重新打包和启动服务器。
4.在浏览器中查看执行效果。

image.png

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.查看浏览执行效果。

image.png

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.查看浏览执行效果。

image.png

image.png

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

我们可以看到控制台里的展示:

image.png

并生成了一个配置文件(.eslintrc.json),这样我们就完成了eslint的基本规则配置。eslint配置文件里的配置项含义如下:

image.png

image.png

image.png

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

image.png

//.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、效果。

image.png

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/

image.png

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环境。

image.png

1、检查git是否安装成功。

git --version

2、初始化git仓库。在项目根目录下打开命令行窗口,执行:

git init

3、查看.git目录。

1、执行 git init,会在项目根目录中生成一个隐藏的 .git 目录。
2、ls -la 指令:查看当前文件夹中的所有内容。

image.png

ls -la

image.png

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

image.png

以字母d开的是文件夹,如 drwxr-xr-x 。

9、切换到hooks文件夹,查看里面的内容。

cd hooks
ls -la

image.png

可以发现,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

image.png

错误:pre-commit 加不了可执行权限,为什么???

13、返回项目根目录。

cd ..
cd ..

cd .. :返回上一级目录

14、app.js。

随意写一些不符合eslint语法规范的代码,来进行测试。

console.log('hello, eslint');

15、git操作。

git status
git add .
git commit -m '2'

image.png

报错:提交失败了,pre-commit也没有被执行。

16、结论。

12步 和 15步报错了,失败了。但是视频中老师却成功了,为什么???

17、

假设我们在12、15步都成功了,继续往后进行。 17步及以后 是视频截屏,记录老师的操作,方便以后学习。

image.png

可以看到,提交时,pre-commit文件被执行了,输出了'pre-commit'。我们接下来只需要在pre-commit文件中,写入eslint语法检查的代码即可。

18、编辑pre-commit文件的内容。

npx eslint ./src

19、提交。

git status
git add .
git commit -m '3'

image.png

20、结论。

当app.js中有不符合规范的代码时,提交失败,命令行中输出eslint报错信息。这样我们就成功地 在提交前 使用pre-commit来对代码进行语法检查了,如果检查不通过,则提交失败。如果检查通过,则提交成功。

提交hooks

image.png

image.png

示例2:也没有成功。

说明:从示例1的10步接着往下做。

11、我们回到项目的根目录下,新建一个文件夹,暂时命名为".mygithooks"。然后在此文件夹下,新增一个git-hook文件,命名为"pre-commit",并写入以下内容:

npx eslint ./src

image.png

12、好了,我们新建了自己的git-hook,但此时git并不能识别。下面我们执行这行命令:

# 项目根目录下

git config core.hooksPath .mygithooks

上述命令给我们自己的文件,配置了git-hook的执行权限。

12.2 查看.git/config文件内容。

cd .git 
ls -la
cat config

image.png

可以发现,config文件中有一个hooksPath字段。

13、给.mygithooks/pre-commit文件添加可执行权限。

chmod +x .mygithooks/pre-commit

14、提交。

git status
git add .
git commit -m '3'

image.png

15、结论。

提交时,pre-commit执行成功,eslint成功执行。这样,我们就成功使用git hooks来完成提交前对代码进行语法检查。

husky

image.png

示例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

image.png

4.2、给.husky/pre-commit添加可执行权限。

chmod +x .husky/pre-commit

image.png

4.3、解决4.2步中出现的无法设置可执行权限的问题。

注意在window系统中提供chmod指令为文件添加可执行权限时,可能会无效。

需要在文件的第一行添加以下语句:

#!/bin/bash

5、提交。

git status
git add .
git commit -m 'husky-01'

image.png

image.png

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=...> 文件中的图片链接

image.png

支持的模块类型

Webpack 天生支持如下模块类型:

  • ECMAScript 模块
  • CommonJS 模块
  • AMD 模块
  • Assets
  • WebAssembly 模块

而我们早就发现——通过 loader 可以使 webpack 支持多种语言和预处理器语法编写的模块。loader 向 webpack 描述了如何处理非原生模块,并将相关依赖引入到你的 bundles中。包括且不限于:

  • TypeScript
  • Sass
  • Less
  • JSON
  • YAML

总的来讲,这些都可以被认为是webpack模块。

image.png

compiler与Resolvers

image.png

在我们运行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。

image.png

//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

image.png

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 选项,来为某个目录设置别名。

image.png

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

image.png

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

image.png

image.png

image.png

image.png

5.结论。

image.png

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 前缀,如:

  1. *.global.css 普通模式
  2. *.css css module模式

如何理解 部分开启 CSS 模块模式?

某些样式需要使用 css模块 来进行唯一性标识,避免被同名覆盖,而另一些样式是全局的,需要覆盖其它同名的样式,则不需要使用css模块。

如 全局样式 是统一的,需要覆盖同名的自定义样式,所以不使用 css 模块,而我们自己的样式则不希望被其它人的同名样式覆盖,则 使用css 模块。

这里统一用 global 关键词进行识别。用正则表达式匹配文件:

image.png

1new RegExp(`^(?!.*\\.global).*\\.css`) 这个正则表达式用来匹配后缀名中包 .global 的.css文件,即全局样式文件,则不需要开启css module2new RegExp(`^(.*\\.global).*\\.css`) 这个正则表达式用来匹配后缀名中不包 .global 的.css文件,即普通样式文件,则需要开启css module3、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

image.png

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

image.png

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、打包、启动服务器、浏览器执行效果。

image.png

image.png

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、打包、启动服务器、浏览器执行效果。

image.png

image.png

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环境中使用这个特性。当然,也可以写点什么,比如:

image.png ● 在 index.js 中使用它

image.png

(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、搭建测试项目。

image.png

//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

image.png

image.png

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进行更改~

image.png

执行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、搭建测试项目。

image.png

//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文件

image.png

4、打包、启动服务器、效果。

npx webpack 
npx webpack serve --open

image.png

5、结论。

可以看到,.ts文件被转换为了.js文件,而且在浏览器中正常执行了。
这说明,我们成功地让webpack来解析和打包.ts文件。

◎ 示例:.ts中使用第三方包。

在示例1的基础上,继续练习。

1、安装lodash。

npm install lodash

2、安装lodash的类型文件。

image.png

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

image.png

5、结论。

可以发现,lodash正常执行,说明我们成功在ts文件中使用了第三方包。

小结

1、/* webpackChunkName: 'lodash' */ 是一个魔法注释,告诉webpack在打包时,将当前要导入的模块单独打包成一个文件,文件名为lodash。

2、import() 方法返回的是一个promise,可以链式调用then()方法。

官方链接

百度脑图

webpack
模块

webpack-bundle-analyzer

husky npm
husky 官网

PostCSS
Autoprefixer

视频教程

千锋最新前端webpack5全套教程,全网最完整的webpack教程(基础+高级)

配套资料

老师笔记:H:\学习课程\ediary日记\学习课程\webpack5_千峰\资料\笔记-webpack5学习指南-V1.0-去水印.pdf

资料目录:H:\学习课程\ediary日记\学习课程\webpack5_千峰\资料

练习代码目录:H:\学习课程\ediary日记\学习课程\webpack5_千峰\wepack5_千峰_练习代码目录

其它

进度:220104-

上一篇

webpack5:基础篇