# webpack5:高级篇

# 前言

基础篇 中,我们基于 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-20220124015021846

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

  • 生成.map 文件。
  • 可以定位到被 babel 转换前的源代码,即定位到类语法的代码。

# cheap-source-map、cheap-module-source-map 的区别。

如果我们使用 babel 将 app.js 中的类语法转换为 es5 语法,我们还能定位到类语法代码吗?

cheap-module-source-map 可以定位到 babel 转换前的源代码,而 cheap-source-map 只能定位到 babel 转换后的代码。

示例: image.png

image-20220124015405188

image-20220124015355471

# 推荐 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-20220124015538925

# 启动代理

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')
})
  1. 启动后台接口服务器。
node server.js
  1. 在浏览器中访问 http://localhost:9000

image.png

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

  1. 发起 ajax 请求。

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

fetch('http://localhost:9000/api/hello')
.then(response => response.text())
.then(result => {
    console.console(result)
})
  1. 打包、启动服务器、浏览器执行效果。

image.png

  1. 结论。

未开启代理时,跨域请求会报错。

示例 2:开启代理。

  1. 省略请求路径中的域名。
//app.js

fetch('/api/hello')
.then(response => response.text())
.then(result => {
	console.log(result)
})
  1. 开启代理。
devServer: {
    //开启代理
    proxy: {
        '/api': 'http://localhost:9000'
    }
},
  1. 打包、启动服务器、浏览器执行效果。

image.png

请求成功了。

  1. 结论。

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

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

# https

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

devServer: {
    https: true
},

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

image-20220124015743899

# 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-20220124015803735

# 开发服务器主机

如果你在开发环境中起了一个 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-20220124015951395

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

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

6、启动开发服务器。

npx webpack serve

7、浏览器执行效果。

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

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

  • 先点击三次按钮,在页面上生成三个正方形框。
  • 再修改 input.js 代码。
  • 不需要重新打包和启动开发服务器。
  • 查看浏览执行效果。

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

  • 先点击三次按钮,在页面上生成三个正方形框。
  • 再修改 input.js 代码。
  • 不需要重新打包和启动开发服务器。
  • 查看浏览执行效果。

image.png

image-20220124020200550

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

# 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-20220124020342895

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

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

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

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

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

cd hooks
ls -la

image-20220124020836560

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

20、结论。

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

# 提交 hooks

image-20220124020956907

image-20220124021028466

image-20220124021042465

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

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。

  • 在.husky 目录中新建一个文件 pre-commit。
  • 在 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-20220124021235321

image-20220124021243586

6、结论。

5 步 执行提交时,报错了。而视频中老师却成功了。为什么???


# 第二章:模块与依赖

在模块化编程中,开发者将程序分解为功能离散的文件,并称之为模块。 每个模块都拥有小于完整程序的体积,使得验证、调试及测试变得轻而易举。 精心编写的模块提供了可靠的抽象和封装界限,使得应用程序中每个模块都具备了条理清晰的设计和明确的目的。

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

支持的模块类型

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

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

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

  • TypeScript
  • Sass
  • Less
  • JSON
  • YAML

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

image-20220124021358840

# compiler 与 Resolvers

image-20220124021409852

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

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

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

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

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 module。
2new 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

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

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

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

image-20220124022358643

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

(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-20220124022506246

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

执行 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-20220124022836718

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

5、结论。

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

# 小结

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

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


# 第四章:多页面应用

为什么需要多页面应用?

在实际开发中,一个完整的系统不会将所有的功能都放在一个页面上,因为这样容易导致性能不佳。实际的做法是:按照 功能模块 划分成多个单页应用,每个单页应用都生成一个 html 文件。而且随着业务的发展,还可以在项目中逐渐加入更多的单页应用。

# 4.1 entry 配置

# ● 单个入口(简写)语法

用法: entry: string | [string]

module.exports = {
    entry: './path/to/my/entry/file.js',
};

entry 属性的单个入口语法,参考下面的简写:

module.exports = {
    entry: {
        main: './path/to/my/entry/file.js',
    },
};

我们也可以将一个文件路径数组传递给 entry 属性,这将创建一个所谓的 “multi-main entry”。在你想要一次注入多个依赖文件,并且将它们的依赖关系绘制在一个 "chunk" 中时,这种方式就很有用。

module.exports = {
    entry: ['./src/file_1.js', './src/file_2.js'],
    output: {
        filename: 'bundle.js',
    },
};

当你希望通过一个入口(例如一个库)为应用程序或工具快速设置 webpack 配置时,单一入口的语法方式是不错的选择。然而,使用这种语法方式来扩展或调整配置的灵活性不大。

# ● 对象语法

用法: entry: { <entryChunkName> string | [string] } | {}

module.exports = {
    entry: {
        app: './src/app.js',
        adminApp: './src/adminApp.js',
    },
};

对象语法会比较繁琐。然而,这是应用程序中定义入口的最可扩展的方式。

描述入口的对象:
用于描述入口的对象。你可以使用如下属性:

  • dependOn : 当前入口所依赖的入口。它们必须在该入口被加载前被加载。
  • filename : 指定要输出的文件名称。
  • import : 启动时需加载的模块。
  • library : 指定 library 选项,为当前 entry 构建一个 library。
  • runtime : 运行时 chunk 的名字。如果设置了,就会创建一个新的运行时 chunk。在 webpack 5.43.0 之后可将其设为 false 以避免一个新的运行时 chunk。
  • publicPath : 当该入口的输出文件在浏览器中被引用时,为它们指定一个公共 URL 地址。请查看 output.publicPath。
module.exports = {
    entry: {
        a2: 'dependingfile.js',
        b2: {
            dependOn: 'a2',
            import: './src/app.js',
        },
    },
};

runtime 和 dependOn 不应在同一个入口上同时使用,所以如下配置无效,并且会抛出错误:

module.exports = {
    entry: {
        a2: './a',
        b2: {
            runtime: 'x2',
            dependOn: 'a2',
            import: './b',
        },
    },
};

确保 runtime 不能指向已存在的入口名称,例如下面配置会抛出一个错误:

module.exports = {
    entry: {
        a1: './a',
        b1: {
            runtime: 'a1',
            import: './b',
        },
    },
};

另外 dependOn 不能是循环引用的,下面的例子也会出现错误:

module.exports = {
    entry: {
        a3: {
            import: './a',
            dependOn: 'b3',
        },
        b3: {
            import: './b',
            dependOn: 'a3',
        },
    },
};

# 一些示例

# ◎ 示例 1:entry 是一个数组。

entry 数组中可以列出多个入口文件,它们会被打包到同一个 bundle 中。

1、搭建 webpack 环境。

npm init -y
npm i -D webpack webpack-cli webpack-dev-server html-webpack-plugin

2、编写测试代码。

image.png

//app.js
console.log('app.js')

//app2.js
console.log('app2.js')

3、配置文件。

//webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
    //entry是一个数组
    entry: ['./src/app.js', './src/app2.js'],
    output: {
        clean: true
    },
    mode: 'development',
    devtool: 'cheap-module-source-map',
    devServer: {
        static: path.resolve(__dirname, './dist'),
    },
    plugins: [
        new HtmlWebpackPlugin(),
    ]
}

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

npx webpack
npx webpack serve --open

image.png

image.png

5、结论。

1.app.js 和 app2.js 之间可以没有任何关系。
2.entry 是一个数组时,里面列出的模块会被打包到同一个 bundle 中。
3.entry 数组中的多个模块的执行顺序:从头部到尾部。

# ◎ 示例 2:entry 数组中包含第三方包。

entry 数组中不仅可以写自定义的模块,还可以写第三方包。
效果:自定义模块 和 第三方包 就会被打包到同一个 bundle 中。

1、安装 lodash。

npm i lodash

2、修改配置文件。

entry: ['./src/app2.js','./src/app.js', 'lodash'],

3、打包、启动服务器,效果。

npx webpack
npx webpack serve --open

4、结论。

1. 示例 1 中,输出的 bundle(main.js)中只包含我们自定义的模块的代码,而示例 2 中还包含了第三方包的代码。

# ◎ 示例 3:单独输出第三方包。

我们希望将 自定义模块 和 第三方模块 分开打包,并输出到不同的 bundle 中,该怎么办呢?

1、修改配置文件。

entry: {
    main: ['./src/app.js','./src/app2.js'],
    lodash: 'lodash'
},

2、打包、启动服务器,效果。

npx webpack
npx webpack serve --open

image.png

image.png

3、结论。
可以发现,输出了两个 bundle:main.js 和 lodash.js。

# ◎ 示例 4:重复打包。

示例 3 中,app.js 和 app2.js 中都没有使用 lodash。打包后,main.js 的大小为 219kb,lodash 为 751kb。如果 app.js 中使用了 lodash,打包后 bundle 的体积会如何变化呢?

1、app.js

import _ from 'lodash'

console.log('app.js')

console.log(_)

2、打包、启动服务器,效果。

npx webpack
npx webpack serve --open

image.png

image.png

3、结论。

可以发现,lodash.js 体积没有变化,而 main.js 体积却变大了很多,几乎和 lodash.js 一样大。这说明 lodash 也被打包到 main.js 中了。

# ◎ 示例 5:避免重复打包。

示例 4 存在一个问题:打包后的代码存在两份 lodash,导致代码重复冗余。如何解决呢?

可以通过 dependOn选项 将自定义模块中所依赖的第三方包 提取出来,单独打包。

import选项 :指定当前 bundle 的入口文件,可以有多个。

1、修改 entry。

entry: {
    main: {
        import: ['./src/app.js','./src/app2.js'],
        dependOn: 'lodash'
    },
    lodash: 'lodash'
},

2、打包、启动服务器,效果。

npx webpack
npx webpack serve --open

image.png

image.png

3、结论。

打包输出了两个 bundle:main.js,lodash.js,它们的体积分别是 1.37kb、754kb,这说明打包后只存在一份 lodash,成功解决了重复打包的问题。

# ◎ 示例 6: 多 bundle 引用相同的第三方包。

如果在多个 bundle 中都引用了同一个第三方包,会引起重复打包问题吗?

1、测试代码。

//app3.js

import _ from 'lodash'
console.log(_)

2、配置文件。

entry: {
    main: {
        import: ['./src/app.js','./src/app2.js'],
        dependOn: 'lodash'
    },
    main2: {
        import: './src/app3.js',
        dependOn: 'lodash'
    },
    lodash: 'lodash'
},

3、打包、启动服务器,效果。

npx webpack
npx webpack serve --open

image.png

image.png

image.png

image.png

4、结论。

打包输出了 3 个 bundle:main.js、main2.js、lodash.js,它们的体积分别是:1.37kb、1.17kb、754kb,说明第三方包 lodash 只打包了一份,不存在重复打包问题。


# 4.2 配置 index html 模板

# 生成多个 HTML 文件

要生成多个 HTML 文件,请在插件数组中多次声明插件。

{
    entry: 'index.js',
    output: {
        path: __dirname + '/dist',
        filename: 'index_bundle.js'
    },
    plugins: [
        new HtmlWebpackPlugin(), // Generates default index.html
        new HtmlWebpackPlugin({ // Also generate a test.html
            filename: 'test.html',
            template: 'src/assets/test.html'
        })
    ]
}

# 编写自己的模板

如果默认生成的 HTML 不能满足您的需要,您可以提供自己的模板。最简单的方法是使用 template 选项并传递自定义 HTML 文件。html 网页包插件将自动将所有必要的 CSS、JS、manifest 和 favicon 文件注入标记中。

plugins: [
    new HtmlWebpackPlugin({
        title: 'Custom template',
        // Load a custom template (lodash by default)
        template: 'index.html'
    })
]

//index.html
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8"/>
        <title><%= htmlWebpackPlugin.options.title %></title>
    </head>
    <body>
    </body>
</html>

# HtmlWebpackPlugin 插件 常用配置项

1、自定义属性
new HtmlWebpackPlugin() 时,可传入一个配置对象,在配置对象中,不仅可以设置内置的属性,还可以自定义属性,在 html 模板中可以通过模板语法 <%= htmlWebpackPlugin.options.xxx %> 来使用这些自定义的属性。

如下图,title 属性 就是一个自定义属性,在 html 模板中可以通过模板语法 <%= htmlWebpackPlugin.options.title %> 来使用它的值,来设置页面标签栏的标题文字。

2、template
指定 html 模板。

3、inject
指定在哪个位置引入打包后的文件。默认值是’head’,表示在 head 标签中引入打包后的文件。> 我们也可以修改为’body’,表示在 body 标签末尾引入打包后的文件。

4、chunks
指定引入哪些 chunks。 默认情况下,htmlwebpackplugin 插件会自动引入打包后所有的 chunks。

plugins: [
    new HtmlWebpackPlugin({
        title: '多页面应用',
        template: './src/index.html',
        inject: 'body',
        chunks: ['main', 'lodash']
    }),
]

# ◎ 示例: HtmlWebpackPlugin 插件 常用配置项。

1、配置文件。

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
    entry: {
        main: {
            import: ['./src/app.js','./src/app2.js'],
            dependOn: 'lodash'
        },
        main2: {
            import: './src/app3.js',
            dependOn: 'lodash'
        },
        lodash: 'lodash'
    },
    output: {
        clean: true
    },
    mode: 'development',
    devtool: 'cheap-module-source-map',
    devServer: {
        static: path.resolve(__dirname, './dist'),
    },
    plugins: [
        //关键代码
        new HtmlWebpackPlugin({
            title: '多页面应用',
            template: './src/index.html',
            inject: 'body',
            chunks: ['main', 'lodash']
        }),
    ]
}

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

npx webpack
npx webpack serve --open

image.png

image.png

image.png

image.png

3、结论。

可以发现,打包后的 index.html 确实使用了 template选项 指定的 html 模板,title 也设置成功,也只引入了 chunks选项 中指定的的 chunk,而且是在 body 标签中引入。


# 4.3 多页面应用

搭建多页面应用,需要进行哪些操作?
1、打包出多个 html 文件,每个 html 文件对应一个单页面应用。
2、将每个 html 文件和它里面独有引入的 chunk 存放在同一个目录中,公共的 chunk 也可以放在一个单独的目录中。
3、每个 html 文件引入 chunk 不完全相同。

配置项说明:

1、 entry.Chunk.import 选项: 指定当前 chunk 的入口文件,如果有多个,则使用一个数组。
2、 entry.Chunk.dependOn 选项:列出当前 chunk 依赖的第三方包。它会被单独打包,避免将自己的代码和第三方包代码打包到一个 chunk 中,导致体积过大。
3、 entry.Chunk.filename 选项:指定当前 chunk 的输出名字。还可以指定存放目录。
4、可以创建多个 HtmlWebpackPlugin 实例 new HtmlWebpackPlugin() ,来生成多个 html 文件,对应多个单页面应用的主页面。
5、 plugins.HtmlWebpackPlugin.filename 选项:指定生成的 html 的名字。还可以指定存放目录。
6、 plugins.HtmlWebpackPlugin.publicPath 选项:指定域名。如果要将打包输出的 chunk 部署到 cdn 上,就可以使用该 publicPath 选项来指定 cdn 域名。

webpack.config.js

module.exports = {
    entry: {
        pageOne: './src/pageOne/index.js',
        pageTwo: './src/pageTwo/index.js',
        pageThree: './src/pageThree/index.js',
    },
};

这是什么? 我们告诉 webpack 需要三个独立分离的依赖图(如上面的示例)。

为什么? 在多页面应用程序中,server 会拉取一个新的 HTML 文档给你的客户端。页面重新加载此新文档,并且资源被重新下载。然而,这给了我们特殊的机会去做很多事,例如使用 optimization.splitChunks 为页面间共享的应用程序代码创建 bundle。由于入口起点数量的增多,多页应用能够复用多个入口起点之间的大量代码 / 模块,从而可以极大地从这些技术中受益。

# ◎ 示例:搭建多页面应用 环境。

1、测试代码。

image.png

//app.js 
import _ from 'lodash'

console.log('app.js')
console.log(_)

//app2.js
console.log('app2.js')

//app3.js
import _ from 'lodash'

console.log(_)

//index.html
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title><%= htmlWebpackPlugin.options.title %></title>
    </head>
    <body>
        <p>我是html模板</p>
    </body>
</html>

//index2.html
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title><%= htmlWebpackPlugin.options.title %></title>
    </head>
    <body>
        <p>我是html模板2</p>
    </body>
</html>

2、配置文件。

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
    //关键代码
    entry: {
        //打包输出三个chunk:main、main2、lodash
        main: {
            import: ['./src/app.js','./src/app2.js'],
            dependOn: 'lodash',
            filename: 'page1/[name].js'
        },
        main2: {
            import: './src/app3.js',
            dependOn: 'lodash',
            filename: 'page2/[name].js'
        },
        lodash: {
            import: 'lodash',
            filename: 'common/[name].js'
        }
    },
    output: {
        clean: true
    },
    mode: 'development',
    devtool: 'cheap-module-source-map',
    devServer: {
        static: path.resolve(__dirname, './dist'),
    },
    //关键代码
    plugins: [
        new HtmlWebpackPlugin({
            title: '多页面应用-页面1',
            template: './src/index.html',
            inject: 'body',
            filename: 'page1/index.html',
            chunks: ['main', 'lodash'],
            publicPath: 'http://www.a.com/'
        }),
        new HtmlWebpackPlugin({
            title: '多页面应用-页面2',
            template: './src/index2.html',
            inject: 'body',
            filename: 'page2/index2.html',
            chunks: ['main2', 'lodash'],
            publicPath: 'http://www.b.com/'
        }),
    ]
}

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

npx webpack 
npx webpack serve --open

image.png

image.png

image.png

image-20220124023945553

4、结论。

  1. 打包输出了三个 chunk:main.js,main2.js,lodash.js。

​ main.js 的入口文件有:app.js、app2.js。

​ main2.js 的入口文件有:app3.js。

​ lodash.js 中打包了第三方包 lodash。

​ app.js、app2.js 中都引用了第三方包 lodash。

  1. 打包生成了两个 html 文件:index.html、index2.html。

​ index.html 中引入了 main.js、lodash.js。

​ index2.html 中引入了 main2.js、lodash.js。

  1. 打包生成了三个目录:page1、page2、common。

​ 打包输出的 chunk 都存放在不同的目录中。

​ page1 目录和 page2 目录 有 html 文件,而 common 目录没有。


# 第五章: Tree shaking

tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码 (dead-code)。它依赖于 ES2015 模块语法的 静态结构 特性,例如 importexport 。这个术语和概念实际上是由 ES2015 模块打包工具 rollup 普及起来的。

webpack 2 正式版本内置支持 ES2015 模块(也叫做 harmony modules)和未使用模块检测能力。新的 webpack 4 正式版本扩展了此检测能力,通过 package.json"sideEffects" 属性作为标记,向 compiler 提供提示,表明项目中的哪些文件是 “pure (纯正 ES2015 模块)”,由此可以安全地删除文件中未使用的部分。

# 5.1 tree-shaking 实险

● src/math.js

export function square(x) {
    return x * x;
}

export function cube(x) {
    return x * x * x;
}

需要将 mode 配置设置成 development,以确定 bundle 不会被压缩:

● webpack.config.js

const path = require('path');

module.exports = {
    entry: './src/index.js',
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'dist'),
    },
    mode: 'development',
    optimization: {
        usedExports: true,
    },
};

配置完这些后,更新入口脚本,使用其中一个新方法:

import { cube } from './math.js';

function component() {
    const element = document.createElement('pre');
    element.innerHTML = [
        'Hello webpack!',
        '5 cubed is equal to ' + cube(5)
    ].join('\n\n');
    
    return element;
}

document.body.appendChild(component());

注意,我们没有从 src/math.js 模块中 import 另外一个 square 方法。这个函数就是所谓的 “未引用代码 (dead code)”,也就是说,应该删除掉未被引用的 export 。

现在运行 npm script npm run build ,并查看输出的 bundle:

● dist/bundle.js

image.png

注意,上面的 unused harmony export square 注释。如果你观察它下面的代码,你会注意到虽然我们没有引用 square ,但它仍然被包含在 bundle 中。

● mode: production

如果此时修改配置:

image.png

打包后发现无用的代码全部都消失了。

处于好奇,webpack 是如何完美的避开没有使用的代码的呢?

很简单:就是 Webpack 没看到你使用的代码。Webpack 跟踪整个应用程序的 import/export 语句,因此,如果它看到导入的东西最终没有被使用,它会认为那是未引用代码 (或叫做 “死代码”—— dead-code ),并会对其进行 tree-shaking 。

死代码并不总是那么明确的。下面是一些死代码和 “活” 代码的例子:

image.png

1、默认情况下,被 import 导入的模块中的代码 不管是否被使用到,都会被打包到当前模块。

2、如果我们想去掉 未使用到的代码,该怎么办呢?
1. 设置生产模式。 mode: 'production'
2. 设置 optimization.usedExports 选项。

3、webpack5 tree-shaking 功能的特点:
1 . 对于自定义模块,如果只导入,而没有调用过它,则也会被 “摇” 掉。
2 . 对于第三方包,如果只导入,而没有调用过它,则也会被打包。
3 . 对于.css 文件,如果只导入,而没有调用过它,则也会被打包。这是 webpack 智能 tree-shaking 的一个表现。因为样式文件一般只需要导入就能够生效,而不需要调用。

# ◎ 示例:tree-shaking 功能的基本使用。

1、搭建 webpack 环境。

npm init -y
npm install --save-dev webpack webpack-cli webpack-dev-server html-webpack-plugin

2、配置文件。

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
    entry: './src/app.js',
    output: {
        clean: true
    },
    mode: 'production',
    //devtool: 'cheap-module-source-map',
    devServer: {
        static: path.resolve(__dirname, './dist'),
    },
    plugins: [
        new HtmlWebpackPlugin(),
    ],
    optimization: {
        usedExports: true
    }
}

3、测试代码。

image-20220124024127202

//app.js
import { add } from './math.js'

console.log(add(5, 6))

//math.js
export function add(x, y){
    return x + y
}
export function minus(x, y){
    return x - y
}

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

npx webpack 
npx webpack serve --open

image.png

image-20220124024144533

5、结论。

main.js 中只打印了 11 这个数字,add 函数都没有,这是因为 webpack 内部做了最极致的优化。也没有打包未使用到的 minus 函数。说明 tree-shaking 功能成功启动了。


# 5.2 sideEffects

注意 Webpack 不能百分百安全地进行 tree-shaking。有些模块导入,只要被引入,就会对应用程序产生重要的影响。一个很好的例子就是全局样式表,或者设置全局配置的 JavaScript 文件。

Webpack 认为这样的文件有 “副作用”。具有副作用的文件不应该做 tree-shaking,因为这将破坏整个应用程序。

Webpack 的设计者清楚地认识到不知道哪些文件有副作用的情况下打包代码的风险,因此 webpack4 默认地将所有代码视为有副作用。这可以保护你免于删除必要的文件,但这意味着 Webpack 的默认行为实际上是不进行 tree-shaking。值得注意的是 webpack5 默认会进行 tree-shaking。

如何告诉 Webpack 你的代码无副作用,可以通过 package.json 有一个特殊的属性 sideEffects,就是为此而存在的。

它有三个可能的值:

  • true

    如果不指定其他值的话。这意味着所有的文件都有副作用,也就是没有一个文件可以 tree-shaking。

  • false

    告诉 Webpack 没有文件有副作用,所有文件都可以 tree-shaking。

  • 数组 […]

    是文件路径数组。它告诉 webpack,除了数组中包含的文件外,你的任何文件都没有副作用。因此,除了指定的文件之外,其他文件都可以安全地进行 tree-shaking。

webpack4 曾经不进行对 CommonJs 导出和 require () 调用时的导出使用分析。webpack 5 增加了对一些 CommonJs 构造的支持,允许消除未使用的 CommonJs 导出,并从 require () 调用中跟踪引用的导出名称。

1、sideEffects 选项 是 package.json 的配置项,而不是 webpack.config.js。
2、sideEffects 选项 用来告诉 webpack 哪些文件是有 “副作用” 的,不要对它们进行 tree-shaking,一律全部打包。没有在该选项中列出的文件,则被认为没有 “副作用”,可以进行 tree-shaking。
3、 webpack 默认认为样式文件是有 “副作用的”,不会对它们进行 tree-shaking,一律全部打包。 相当于: "sideEffects": ["*.css"]

# ◎ 示例:sideEffects 的基本使用。

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

1、安装 style-loader、css-loader。

npm install --save-dev style-loader css-loader

2、配置文件。

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
    entry: './src/app.js',
    output: {
        clean: true
    },
    mode: 'production',
    //devtool: 'cheap-module-source-map',
    devServer: {
        static: path.resolve(__dirname, './dist'),
    },
    module: {
        rules: [
            {
                test: /\.css$/,
                use: ['style-loader', 'css-loader']
            }
        ]
    },
    plugins: [
        new HtmlWebpackPlugin(),
    ],
    optimization: {
        usedExports: true
    }
}

3、测试代码。

image-20220124024210518

//style.css 
body{
    background-color: pink;
}

//m.global.js 
//全局js文件,即使只导入而未调用,也不希望被tree-shaking。
console.log('global code')

//app.js
import './style.css'
import './m.global.js'

4、sideEffects 选项。

//package.json

"sideEffects": ["*.css", "*.global.js"],

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

npx webpack 
npx webpack serve --open

image.png

6、结论。

1 . 在入口文件 app.js 中,只导入而没有进行调用 全局 js 文件 m.global.js,默认情况下,它会被 webpack tree-shaking 掉。

可以通过 sideEffects 选项,将全局 js 文件 m.global.js 标记为有 “副作用” 的,让 webpack 不要对其进行 tree-shaking。

2 . style.css 文件 导入后,不需要调用,webpack 默认认为样式文件是有 “副作用” 的,不会对其进行 “树摇”。


# 第六章:渐进式网络应用程序 PWA

渐进式网络应用程序 (progressive web application - PWA),是一种可以提供类似于 native app (原生应用程序) 体验的 web app (网络应用程序)。PWA 可以用来做很多事。其中最重要的是,在 离线 (offline) 时应用程序能够继续运行功能。这是通过使用名为 Service Workers 的 web 技术来实现的。

# 6.1 非离线环境下运行

到目前为止,我们一直是直接查看本地文件系统的输出结果。通常情况下,真正的用户是通过网络访问 web app;用户的浏览器会与一个提供所需资源(例如 .html、.js、.css 文件)的 server 通讯。

我们通过搭建一个拥有更多基础特性的 server 来测试下这种离线体验。这里使用 http-server package: npm install http-server --save-dev 。还要修改 package.jsonscripts 部分,来添加一个 start script:

package.json

{
    ...
    "scripts": {
        "start": "http-server dist"
    },
    ...
}

注意:默认情况下,webpack DevServer 会写入到内存。我们需要启用 devserverdevmiddleware.writeToDisk 配置项,来让 http-server 处理 ./dist 目录中的文件。

devServer: {
    devMiddleware: {
        index: true,
        writeToDisk: true,
    },
},

如果你之前没有操作过,先得运行命令 npm run build 来构建你的项目。然后运行命令 npm start 。应该产生以下输出:

image.png

如果你打开浏览器访问 http://localhost:8080 (即 http://127.0.0.1 ),你应该会看到 webpack 应用程序被 serve 到 dist 目录。如果停止 server 然后刷新,则 webpack 应用程序不再可访问。

这就是我们为实现离线体验所需要的改变。在本章结束时,我们应该要实现的是,停止 server 然后刷新,仍然可以看到应用程序正常运行。

1、执行 npx webpack serve 指令时,会重新进行打包,而且启动一个服务器。
2、执行 npx webpack serve 打包时,默认不会将打包后的资源输出到目录中,而是存放在内存中。

如果我们希望将重新打包后的资源写入目录,则可以设置 devServer.devMiddleware.writeToDisk 选项。
3、 http-server 是一个 npm 包,可以启动一个服务器,来管理打包后的 web 资源,让用户能够从浏览器来访问 web 应用。作用类似于 webpack-dev-server

# ◎ 示例:http-server 的基本使用。

1、搭建 webpack 环境。

npm init -y
npm install --save-dev webpack webpack-cli webpack-dev-server html-webpack-plugin

//安装 http-server
npm install --save-dev http-server

2、测试代码。

image.png

//index.js
console.log('hello, world')

3、配置文件。

//webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
    entry: './src/index.js',
    output: {
        clean: true
    },
    mode: 'development',
    devtool: 'cheap-module-source-map',
    devServer: {
        static: path.resolve(__dirname, './dist'),
    },
    plugins: [
        new HtmlWebpackPlugin(),
    ],
}

4、package.json

"scripts": {
    "start": "http-server dist",
},

5、打包,启动 web 服务。

npx webpack
npm start

6、效果。

image-20220124024258216

7、结论。

http server 可以启动一个服务器,管理打包后的 web 资源,用户可以通过浏览器来访问它。但只要 http server 停止,浏览器就无法访问了。这说明我们的 web 应用是非离线的。


# 6.2 添加 Workbox

添加 workbox-webpack-plugin 插件,然后调整 webpack.config.js 文件:

npm install workbox-webpack-plugin --save-dev

webpack.config.js

const WorkboxPlugin = require('workbox-webpack-plugin')

plugins: [
    new WorkboxPlugin.GenerateSW({
        // 快速启用 ServiceWorkers 
        clientsClaim: true, 
        // 不允许遗留任何“旧的” ServiceWorkers
        skipWaiting: true,
    })
]

执行: npx webpack

image-20220124024314261

现在你可以看到,生成了两个额外的文件: service-worker.js 和名称冗长的 workbox-718aa5be.jsservice-worker.js 是 Service Worker 文件, workbox-718aa5be.jsservice-worker.js 引用的文件,所以它也可以运行。你本地生成的文件可能会有所不同;但是应该会有一个 service-worker.js 文件。

所以,值得高兴的是,我们现在已经创建出一个 Service Worker。接下来该做什么?

# ◎ 示例:workbox-webpack-plugin 插件的基本使用。

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

1、安装 workbox-webpack-plugin 插件。

npm install workbox-webpack-plugin --save-dev

2、配置 workbox-webpack-plugin 插件。

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const WorkboxPlugin = require('workbox-webpack-plugin')

module.exports = {
    entry: './src/index.js',
    output: {
        clean: true
    },
    mode: 'development',
    devtool: 'cheap-module-source-map',
    devServer: {
        static: path.resolve(__dirname, './dist'),
        devMiddleware: {
            writeToDisk: true
        }
    },
    plugins: [
        new HtmlWebpackPlugin(),
        new WorkboxPlugin.GenerateSW({
            // 这些选项帮助快速启用 ServiceWorkers 
            clientsClaim: true, 
            // 不允许遗留任何“旧的” ServiceWorkers
            skipWaiting: true,
        })
    ]
}

3、打包、启动服务器,效果。

npx webpack

image-20220124024331991

4、结论。

打包后生成了两个新的文件:workbox-37481be9.js、service-worker.js。它们就是 workbox-webpack-plugin 插件创建出来的。service-worker.js 是主文件,会引用 workbox-37481be9.js 文件。


# 6.3 注册 Service Worker

接下来我们注册 Service Worker,使其出场并开始表演。通过添加以下注册代码来完成此操作:

index.js

再次运行 npx webpack 来构建包含注册代码版本的应用程序。然后用 npm start 启动服务。访问 http://localhost:8080 并查看 console 控制台。在那里你应该看到:

SW registered

现在来进行测试。停止 server 并刷新页面。如果浏览器能够支持 Service Worker,应该可以看到你的应用程序还在正常运行。然而,server 已经停止 serve 整个 dist 文件夹,此刻是 Service Worker 在进行 serve。

image.png

# ◎ 示例

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

1、index.js。

编写 Service Worker 相关的代码。

console.log('hello, world 666')

if('serviceWorker' in navigator){
    window.addEventListener('load', ()=>{
        navigator.serviceWorker.register('/service-worker.js')
        .then(registration => {
            console.log('SW registered:', registration)
        })
        .catch(registrationError => {
            console.log('SW registered failed:', registrationError)
        })
    })
}

2、打包,启动 http-server 服务器,效果。

npx webpack
npm start

image-20220124024442783

image-20220124024456909

3、关闭 http server,刷新浏览器页面,查看是否还可以访问 web 应用。

//终止命令行终端中的http server
Ctrl + C 

image-20220124024512978

4、结论。

1 . 关闭 http server 后,浏览器仍然能显示 web 应用。这说明当服务器挂掉或者用户网络断开后,刷新页面,在浏览器上仍然可以显示我们的 web 应用。

2 . 这是如何实现的呢?这是因为浏览器将 web 应用缓存起来了。

3 . 在 chrome 浏览器 地址栏中打开 chrome://serviceworker-internals ,可以看到 有哪些 web 应用 在当前浏览器中注册了 Service Worker。

image.png

点击 image.png 按钮,可以注销 Service Worker,这样就无法在离线状态下访问对应的 web 应用了。

# 小结

Service Worker 需要配合 webpack、浏览器 一起使用,来创建一个 PWA。


# 第七章: shimming 预置依赖

webpack compiler 能够识别遵循 ES2015 模块语法、CommonJS 或 AMD 规范编写的模块。然而,一些 third party (第三方库) 可能会引用一些全局依赖(例如 jQuery 中的 $ )。因此这些 library 也可能会创建一些需要导出的全局变量。这些 “broken modules (不符合规范的模块)” 就是 shimming (预置依赖) 发挥作用的地方。

shim 另外一个极其有用的使用场景就是:当你希望 polyfill 扩展浏览器能力,来支持到更多用户时。在这种情况下,你可能只是想要将这些 polyfills 提供给需要修补 (patch) 的浏览器(也就是实现按需加载)。

shimming 英 [ˈʃɪmɪŋ] 匀场;匀场技术;垫补;填隙;补偿
shim 英 [ʃɪm] n. 垫片;(木、橡胶、金属等) 楔子;填隙片 vt. 用垫片填

# 7.1 Shimming 预设全局变量

让我们开始第一个 shimming 全局变量的用例。还记得我们之前用过的 lodash 吗?出于演示目的,例如把这个应用程序中的模块依赖,改为一个全局变量依赖。要实现这些,我们需要使用 ProvidePlugin 插件。

ProvidePlugin 插件 不需要安装,是 webpack 内置的。

使用 ProvidePlugin 后,能够在 webpack 编译的每个模块中,通过访问一个变量来获取一个 package。如果 webpack 看到模块中用到这个变量,它将在最终 bundle 中引入给定的 package。

让我们先移除 lodashimport 语句,改为通过插件提供它:

src/index.js

console.log(_.join(['hello', 'webpack'], ' '))

webpack.config.js

const webpack = require('webpack')

module.exports = {
    mode: 'development',
    entry: './src/index.js',
    plugins: [
        new webpack.ProvidePlugin({
            _: 'lodash'
        })
    ]
}

我们本质上所做的,就是告诉 webpack:

如果你遇到了至少一处用到 _ 变量的模块实例,那请你将 lodash package 引入进来,并将其提供给需要用到它的模块。

运行我们的构建脚本,将会看到同样的输出:

image-20220124024612849

还可以使用 ProvidePlugin 暴露出某个模块中单个导出,通过配置一个 “数组路径”(例如 [module, child, ...children?] )实现此功能。所以,我们假想如下,无论 join 方法在何处调用,我们都只会获取到 lodash 中提供的 join 方法。

src/index.js

console.log(join(['hello', 'webpack'], ' '))

webpack.config.js

const webpack = require('webpack')

module.exports = {
    mode: 'development',
    entry: './src/index.js',
    plugins: [
        new webpack.ProvidePlugin({
            // _: 'lodash'
            join: ['lodash', 'join'],
        })
    ]
}

这样就能很好的与 tree shaking 配合,将 lodash library 中的其余没有用到的导出去除。

# ◎ 示例:第三方包 shimming

一般情况下,我们会在一个模块中先导入 lodash,然后使用它。

那能不能将 lodash 变成一个全局变量呢?这样在任何模块中,都可以直接使用 lodash,而不需要导入了。

可以通过将第三方包预设成全局变量来实现。即第三方包的 shimming。

1、搭建 webpack 环境

npm init -y
npm install --save-dev webpack webpack-cli webpack-dev-server html-webpack-plugin

2、测试代码。

//index.js
console.log(_.join(['hello', 'world']), ' ')

3、webpack.config.js。

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const webpack = require('webpack')

module.exports = {
    entry: './src/index.js',
    output: {
        clean: true
    },
    mode: 'development',
    devtool: 'cheap-module-source-map',
    devServer: {
        static: path.resolve(__dirname, './dist'),
    },
    plugins: [
        new HtmlWebpackPlugin(),
        new webpack.ProvidePlugin({
            _: 'lodash'
        })
    ]
}

4、注意:不需要手动安装 lodash。执行 npx webpack 打包时,好像 ProvidePlugin 插件会自动帮我们下载 lodash。

5、打包、效果。

npx webpack
cd dist
node main.js

image.png

image.png

image.png

6、结论。

main.js 的体积竟然有 534kb,这说明 lodash 库被打包进去了。通过 nodejs 来执行 main.js,正常输出了。这说明我们成功地将 第三方包 预设成 全局变量 了。

我们没有 安装第三方包 lodash,也没有在入口文件 index.js 中 import 导入 lodash,只是设置了 ProvidePlugin 选项。

我们没有 安装第三方包 lodash,但是 main.js 的体积却有 534kb 这么大,这说明 webpack ProviedPlugin 会帮助我们下载 lodash 并进行打包。


# 7.2 细粒度 Shimming

一些遗留模块依赖的 this 指向的是 window 对象。在接下来的用例中,调整我们的 index.js :

this.alert('hello webpack')

当模块运行在 CommonJS 上下文中,这将会变成一个问题,也就是说此时的 this 指向的是 module.exports 。在这种情况下,你可以通过使用 imports-loader 覆盖 this 指向:

module: {
    rules: [
        {
            test: require.resolve('./src/index.js'),
            use: 'imports-loader?wrapper=window'
        }
    ]
},

# ◎ 示例:imports-loader 基本使用

如果我们想在模块中使用 this 这个变量,并希望它是指向浏览器的 window 对象的,该怎么办呢?

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

1、安装 imports-loader

npm install --save-dev imports-loader

2、index.js

this.alert('hello, webpack')

3、webpack.config.js。

module: {
    rules: [
        {
            test: require.resolve('./src/index.js'),
            use: 'imports-loader?wrapper=window'
        }
    ]
},

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

npx webpack
npx webpack serve --open

image.png

5、结论。

webpack 打包时,模块中的 this 变量(如果有)会指向 module.exports ,而不是浏览器的 window 对象。但是,有些情况下,我们就希望 this 指向浏览器的 window 对象,来使用 window 对象上的一些 api,这时候该怎么办呢?

可以通过 imports-loader 来修改 this 的指向。


# 7.3 全局 Exports

让我们假设,某个 library 创建出一个全局变量,它期望 consumer (使用者) 使用这个变量。为此,我们可以在项目配置中,添加一个小模块来演示说明:

src/globals.js

const file = 'example.txt';
const helpers = {
    test: function () {
        console.log('test something')
    },

    parse: function () {
        console.log('parse something')
    },
}

webpack.config.js

此时,在我们的 entry 入口文件中(即 src/index.js ),可以使用 const { file, parse } = require('./globals.js'); ,可以保证一切将顺利运行。

为什么要使用 exports-loader

一般情况下,我们不知道第三方包内部是如何导出的,此时,我们可以在 webpack.config.js 中对第三方包里面的内容作一个自定义的导出,这样就可以方便的使用它里面的一些 api 了。

# ◎ 示例

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

1、安装 exports-loader

npm i -D exports-loader

2、测试代码。

//index.js

const { file, parse } = require('./globals')

console.log(file) 
parse()

//globals.js 

const file = 'example.txt';
const helpers = {
    test: function() {
        console.log('test something')
    },
    parse: function() {
        console.log('parse something')
    },
}

image.png

3、webpack.config.js

module: {
    rules: [
        {
            test: require.resolve('./src/globals.js'),
            use: 'exports-loader?type=commonjs&exports=file,multiple|helpers.parse|parse',
        },
    ]
},

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

npx webpack 
npx webpack serve --open

image.png

5、结论。

globals.js 没有使用任何模块化语法来导出内容,所以它既不是 ES module ,也不是 CommonJS module 。为什么在入口文件 index.js 中,能够通过 require() 方法 来导入它里面定义的变量和函数呢?

这是因为我们在 webpack.config.js 中,使用 exports-loader 来对 globals.js 做了一个 自定义导出。它会覆盖 globals.js 内部的导出,我们自定义导出什么内容,在其它模块中就可以导入这些内容。所以此时我们不需要关心 globals.js 内部是如何导出的,只要使用我们自定义导出的内容即可。


# 7.4 加载 Polyfills

目前为止,我们讨论的所有内容 都是处理那些遗留的 package,让我们进入到第二个话题:polyfill

有很多方法来加载 polyfill。例如,想要引入 @babel/polyfill 我们只需如下操作:

npm install --save @babel/polyfill

然后,使用 import 将其引入到我们的主 bundle 文件:

import '@babel/polyfill'

console.log(Array.from([1, 2, 3], x => x + x))

注意,这种方式优先考虑正确性,而不考虑 bundle 体积大小。为了安全和可靠,polyfill/shim 必须运行于所有其他代码之前,而且需要同步加载,或者说,需要在所有 polyfill/shim 加载之后,再去加载所有应用程序代码。 社区中存在许多误解,即现代浏览器 “不需要” polyfill,或者 polyfill/shim 仅用于添加缺失功能 - 实际上,它们通常用于修复损坏实现 (repair broken implementation),即使是在最现代的浏览器中,也会出现这种情况。 因此,最佳实践仍然是,不加选择地和同步地加载所有 polyfill/shim,尽管这会导致额外的 bundle 体积成本。

为什么要使用 polyfill ?

有些最新的 javascript api( 如 Array.from () 方法)是低版本浏览器不支持的,通过引入 polyfill ,可以将这些 api 的实现代码打包到 web 应用中,这样就可以正常使用了。

# ◎ 示例:@babel/polyfill 基本使用。

1、搭建 webpack 基本环境。

npm init -y
npm install --save-dev webpack webpack-cli webpack-dev-server html-webpack-plugin

2、安装 @babel/polyfill

npm i -D @babel/polyfill

3、测试代码。

//index.js
import '@babel/polyfill'

console.log(Array.from([1,2,3], x => x + x))

4、打包,效果。

npx webpack

image.png

5、结论。

可见, main.js 体积竟然达到 399kb,这是因为 @babel/polyfill 也被打包进去了。 @babel/polyfill 这个包中就包含了 Array.from () 的底层实现代码,这样在任何版本浏览器中都可以正常使用了 Array.from () 方法了。


# 7.5 进一步优化 Polyfills

不建议使用 import @babel/polyfilll 。因为这样做的缺点是会全局引入整个 polyfill 包,比如 Array.from 会全局引入,不但包的体积大,而且还会污染全局环境。

babel-preset-env package 通过 browserslist 来转译那些你浏览器中不支持的特性。这个 preset 使用 useBuiltIns 选项,默认值是 false ,这种方式可以将全局 babel-polyfill 导入,改进为更细粒度的 import 格式:

import 'core-js/modules/es7.string.pad-start';
import 'core-js/modules/es7.string.pad-end';
import 'core-js/modules/web.timers';
import 'core-js/modules/web.immediate';
import 'core-js/modules/web.dom.iterable';

● 安装 @babel/preset-env 及 相关的包

npm i babel-loader @babel/core @babel/preset-env -D

● webpack.config.js

module: {
    rules: [
        {
            test: /\.js$/,
            exclude: /node_modules/,
            use: {
                loader: 'babel-loader',
                options: {
                    presets: [
                        [
                            '@babel/preset-env',
                            {
                                targets: [
                                    'last 1 version',
                                    '> 1%'
                                ],
                                useBuiltIns: 'usage',
                            }
                        ]
                    ]
                }
            }
        }
    ]
},

useBuiltIns 参数有三个取值: “entry”、”usage”、false

默认值是 false ,此参数决定了 babel 打包时如何处理 @babel/polyfill 语句。

“entry”: 会将文件中 import '@babel/polyfill' 语句 结合 targets ,转换为一系列引入语句,去掉目标浏览器已支持的 polyfill 模块,不管代码里有没有用到,只要目标浏览器不支持都会引入对应的 polyfill 模块。

“usage”: 不需要手动在代码里写 import '@babel/polyfill' ,打包时会自动根据实际代码的使用情况,结合 targets 引入代码里实际用到部分 polyfilll 模块。

false: 对 import '@babel/polyfill' 不作任何处理,也不会自动引入 polyfilll 模块。

需要注意的是在 webpack 打包文件配置的 entry 中引入的 @babel/polyfill 不会根据 useBuiltIns 配置任何转换处理。

由于 @babel/polyfill 在 7.4.0 中被弃用,我们建议直接添加 corejs 并通过 corejs 选项设置版本。

● 执行编译 npx webpack

image.png

提示我们需要安装 core-js

npm i core-js@3 -S

此时还需要 添加一个配置:

module: {
    rules: [
        {
            test: /\.js$/,
            exclude: /node_modules/,
            use: {
                loader: 'babel-loader',
                options: {
                    presets: [
                        [
                            '@babel/preset-env',
                            {
                                targets: [
                                    'last 1 version',
                                    '> 1%'
                                ],
                                useBuiltIns: 'usage',
                                //配置core-js
                                corejs: 3
                            }
                        ]
                    ]
                }
            }
        }
    ]
},

成功优化!

# ◎ 示例:优化 polyfill

在上一节 示例 的基础上继续练习。

1、安装 babel

npm i -D babel-loader @babel/core @babel/preset-env 

2.2 安装 core-js

npm i -D core-js@3

2、webpack.config.js

module: {
    rules: [
        {
            test: /\.js$/,
            exclude: /node_modules/,
            use: {
                loader: 'babel-loader',
                options: {
                    presets: [
                        [
                            '@babel/preset-env',
                            {
                                targets: [
                                    'last 1 version',
                                    '> 1%'
                                ],
                                useBuiltIns: 'usage',
                                corejs: 3
                            }
                        ]
                    ]
                }
            }
        }
    ]
},

3、不需要手动导入 @babel/polyfill

//index.js

//注释掉,不需要手动导入了。
//import '@babel/polyfill' 

console.log(Array.from([1,2,3], x => x + x))

4、打包,效果。

npx webpack

image.png

image-20220124024805945

5、结论。

可见,打包后的 main.js 体积为 110kb,比上一个节示例的打包结果 399kb 小了很多。这说明成功优化了 polyfill。


# 第八章:创建 library

除了打包应用程序,webpack 还可以用于打包 JavaScript library。

# 8.1 如何构建 library

在前面的章节中,打包出的都是一个 web 应用。

思考一下:能否使用 webpack 打包出一个 library,类似于 lodash,而不是一个 web 应用,提供给其它人使用呢?

output.library 选项

library 选项 告诉 webpack 我们要打包成一个库,而不是一个 web 应用。此时,入口文件中,定义而未调用的代码不会被 tree shaking 掉。因为库里面定义的 api 是提供给其它人使用的,库本身不需要调用。

# ◎ 示例:使用 webpack 打包出一个 library 的基本步骤。

1、搭建 webpack 基本环境。

npm init -y 
npm install --save-dev webpack webpack-cli webpack-dev-server html-webpack-plugin

2、webpack.config.js

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'mylib.js',
        clean: true,
        //关键代码
        library: 'mylib'
    },
    mode: 'production',
    devtool: 'cheap-module-source-map',
    devServer: {
        static: path.resolve(__dirname, './dist'),
    },
    plugins: [
        new HtmlWebpackPlugin(),
    ]
}

3、测试代码。

在入口文件 index.js 中,定义并导出了一个 add() 方法,我们希望将它打包成一个库,提供给其它人使用。

export function add(x, y){
    return x + y
}

4、demo/index.html

为了测试打包后的库 mylib.js 是否能正常执行,我们在 demo 页面中,通过 script 标签来引入打包后的库 mylib.js ,并调用了它里面的 add() 方法。

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title></title>
    </head>
    <body>
        <script src="../dist/mylib.js" type="text/javascript" charset="utf-8"></script>
        <script type="text/javascript">
            console.log(mylib)
            console.log(mylib.add(5, 6))
        </script>
    </body>
</html>

5、打包,启动一个服务器,浏览器效果。

npx webpack
npx http-server

image-20220124024829887

image-20220124024838941

image.png

image.png

6、结论。

可见,在浏览器中访问 deom 页面, mylib.js 中的 add() 方法成功执行了。这说明我们已经能够成功地使用 webpack 打包出一个库了。


# 8.2 构建小轮子

image-20220124024913902

image.png

image.png

image-20220124024933891

image-20220124024948590

然而它只能通过被 script 标签引用而发挥作用,它不能运行在 CommonJS、AMD、Node.js 等环境中。

作为一个库作者,我们希望它能够兼容不同的环境,也就是说,用户应该能够通过以下方式使用打包后的库:

● CommonJS module require:

const webpackNumbers = require('webpack-numbers');
// ...
webpackNumbers.wordToNum('Two');

● AMD module require:

require(['webpackNumbers'], function (webpackNumbers) {
    // ...
    webpackNumbers.wordToNum('Two');
});

● script tag:

<!DOCTYPE html>
<html>
    ...
    <script src="https://example.org/webpack-numbers.js">
    </script>
    <script>
        // ...
        // Global variable
        webpackNumbers.wordToNum('Five');
        // Property in the window object
        window.webpackNumbers.wordToNum('Five');
        // ...
    </script>
</html>

我们更新 output.library 配置项,将其 type 设置为 ‘umd’ :

const path = require('path');
module.exports = {
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'webpack-numbers.js',
        library: {
            name: 'webpackNumbers',
            type: 'umd',
        },
    },
};

现在 webpack 将打包一个库,其可以与 CommonJS、AMD 以及 script 标签使用。

image.png

image.png

image.png

image.png

# output.library.type 属性

output.library.type 属性:设置 打包出来的库 可以使用哪种引入方式。

可选值:

● window
以 script 标签来引入。

<script src="../dist/mylib.js" type="text/javascript" charset="utf-8"></script>
<script type="text/javascript">
    console.log(mylib)
    console.log(mylib.add(5, 6))
</script>

● commonjs
以 require () 方法引入。

const { mylib } = require('../dist/mylib')
console.log(mylib.add(5, 6))

● module
即 ES module,目前该功能还不够成熟,需要添加一个选项 experiments.outputModule: true ,此时不能设置 library.name 属性。

引入方式:

<script type="module">
    import { add } from '../dist/mylib.js'
    console.log( add(5, 6) )
</script>

● umd

同时支持多种引入方式,包括 window、commonjs、amd,但 es modulde 支持还存在问题。

# ◎ 示例:处理依赖。

如果自己的库中引入了第三方包,应该如何进行打包呢?

library 选项
globalObject 选项
externals 选项

1、搭建 webpack 基本环境。

npm init -y 
npm install --save-dev webpack webpack-cli 

npm i -D lodash

注意:lodash 不能安装成 运行依赖,避免将 lodash 打包进我们的库中,导致体积过大。

2、测试代码。

//index.js

import _ from 'lodash' 
import numRef from './ref.json'

export function numToWord(num){
    return _.reduce(
        numRef,
        (accum, ref) => {
            return ref.num === num ? ref.word : accum
        },
        ''
    )
}

export function wordToNum(word){
    return _.reduce(
        numRef,
        (accum, ref) => {
            return ref.word === word && word.toLowerCase() ? ref.num : accum
        },
        -1
    )
}

3、webpack.config.js

const path = require('path')

module.exports = {
    entry: './src/index.js',
    output: {
        clean: true,
        path: path.resolve(__dirname, 'dist'),
        filename: 'webpack-numbers.js',
         
        //关键代码
        library: {
            name: 'webpackNumbers',
            type: 'umd'
        },
        globalObject: 'globalThis'
    },
    //externals选项:将lodash单独打包,不合并到我们自己的代码中。
    externals: {
        lodash: {
            commonjs: 'lodash',
            commonjs2: 'lodash',
            amd: 'lodash',
            root: '_'
        }
    },
    //生产环境
    mode: 'production',
}

3.2 修改 package.json main 选项。

name 选项:设置包名。

version 选项:设置包的版本。

main 选项: 设置为打包后的 bundle 主文件的路径。
因为别人在通过 require () 来导入你的包的时候,会去读取 main 选项中指定的文件。

//package.json

{
  "name": "ygl-numbers",
  "version": "1.0.0",
  "description": "",
  "main": "dist/webpack-numbers.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "lodash": "^4.17.21",
    "webpack": "^5.66.0",
    "webpack-cli": "^4.9.1"
  }
}

注意:打包后 bundle 文件的名字和库的名字可以不一样。

如:示例中,打包出来的 bundle 名字是 webpadck-numbers.js ,它是在 webpack.config.js output.filename 选项 中设置的。而发布时包的名字是 ygl-numbers ,它是在 package.json name 选项 中设置的。

4、打包。

npx webpack

image.png

5、测试打包后的库是否能够正常使用。

app.js 中以 CommonJS 语法 来使用 我们打包出来的库 webpack-numbers.js。

image-20220124025052312

/demo/app.js

const webpackNumbers = require('../dist/webpack-numbers')

console.log(webpackNumbers)
console.log(webpackNumbers.numToWord(3))
console.log(webpackNumbers.wordToNum('Four'))

node demo/app.js

image-20220124025101053

6、结论。

1 . 打包后的库 webpack-numbers.js 体积只有 1.24kb,这说明 lodash 包没有被打包进去(或者是只打包了 lodash 中的 reduce() 方法,也就是 tree shaking 掉了 lodash 中那些未使用到的代码。)。

2 . 通过 CommonJS 语法 来引入 webpack-numbers.js ,调用它里面的两个方法: numToWord()wordToNum() ,都执行成功了。这说明打包后的的库支持 CommonJS 语法的引入方式。


# 8.3 发布为 npm-package

如何将前面打包好的库发布到 npm 上,提供给其它人使用呢?

# ◎ 示例:发布 npm 包的基本步骤。

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

1、注册 npm 账号。

访问 npm 官网,注册一个账号。

image.png

2、检查 npm 源。

执行下面的命令,查看当前 npm 源,必须是官方的源:registry.npmjs.org/ 。如果是淘宝的源,则会发布不上去。

npm config get registry

image-20220124025130740

3、添加用户。

npm adduser

image.png

注意:输入时速度要快,最好是复制粘贴,因为网速慢容易导致报错。多输几次就可以了。

4、发布。

image-20220124025143410

npm publish

image.png

注意:

1、包名必须是全球唯一的,如果重名了则发布不了。
如果重名了,只需要修改 package.json 中 name 选项,也可以修改版本号 version 。

2、同一个包 可以发布多次,但要修改版本号。
同一个包,修改源代码,打包后,修改 package.json 中的 version 版本,就可以再次发布了。它会被发布到在同一个包的不同版本中。

5、检查是否发布成功。

登录 npm 官网 / 点击用户头像 / Packages

image-20220124025209895

6、测试发布成功的包 ygl-numbers 是否能够使用。

创建一个项目,下载使用 我们自己刚发布出去的包 ygl-numbers ,测试它能否正常使用。

image.png

npm init -y
npm i ygl-numbers 

//app.js

const yglNumbers = require('ygl-numbers')

console.log(yglNumbers)
console.log(yglNumbers.numToWord(2))
console.log(yglNumbers.wordToNum('Four'))

image-20220124025223444

7、结论。

我们成功地发布了自己的包 ygl-numbers ,而且它可以被其它人正常下载和使用。


# 第九章:模块联邦

# 9.1 什么是模块联邦

federation 英 [ˌfedəˈreɪʃn] n. 联邦;联盟

多个独立的构建可以组成一个应用程序,这些独立的构建之间不应该存在依赖关系, 因此可以单独开发和部署它们。

这通常被称作微前端,但并不仅限于此。

Webpack5 模块联邦让 Webpack 达到了线上 Runtime 的效果,让代码直接在项目间利用 CDN 直接共享,不再需要本地安装 Npm 包、构建再发布了!

我们知道 Webpack 可以通过 DLL 或者 Externals 做代码共享时 Common Chunk,但不同应用和项目间这个任务就变得困难了,我们几乎无法在项目之间做到按需热插拔。

# ● NPM 方式共享模块

想象一下正常的共享模块方式,对,就是 NPM。

如下图所示,正常的代码共享需要将依赖作为 Lib 安装到项目,进行 Webpack 打包构建再上线,如下图:

image-20220124025245483

对于项目 Home 与 Search,需要共享一个模块时,最常见的办法就是将其抽成通用依赖并分别安装在各自项目中。

虽然 Monorepo 可以一定程度解决重复安装和修改困难的问题,但依然需要走本地编译。

# ● UMD 方式共享模块

真正 Runtime 的方式可能是 UMD 方式共享代码模块,即将模块用 Webpack UMD 模式打包,并输出到其他项目中。这是非常普遍的模块共享方式:

image.png

对于项目 Home 与 Search,直接利用 UMD 包复用一个模块。但这种技术方案问题也很明显,就是包体积无法达到本地编译时的优化效果,且库之间容易冲突。

# ● 微前端方式共享模块

微前端:micro-frontends (MFE) 也是最近比较火的模块共享管理方式,微前端就是要解决多项目并存问题,多项目并存的最大问题就是模块共享,不能有冲突。

image.png

由于微前端还要考虑样式冲突、生命周期管理,所以本文只聚焦在资源加载方式上。

微前端一般有两种打包方式:

  1. 子应用独立打包,模块更解耦,但无法抽取公共依赖等。
  2. 整体应用一起打包,很好解决上面的问题,但打包速度实在是太慢了,不具备水平扩展能力。

# ● 模块联邦方式

终于提到本文的主角了,作为 Webpack5 内置核心特性之一的 Federated Module:

image-20220124025305381

从图中可以看到,这个方案是直接将一个应用的包应用于另一个应用,同时具备整体应用一起打包的公共依赖抽取能力。


# 9.2 搭建三个应用

本案例模拟三个应用: NavSearchHome 。每个应用都是独立的,又通过模块联邦联系到了一起。

省略,见示例。


# 9.3 使用模块联邦

在 webpack.config.js 中 配置启用 模块联邦 功能。基本语法:

image.png

注意:
1 . 如何暴露模块?
2 . 如何导入模块?
3 . 各个配置项的作用和使用方式。如图:

image.png


# ◎ 示例

Nav 应用中有一个公共组件 Header.js ,表示页面的公共头部。
Home 应用中有一个公共组件 HomeList.js ,表示一个列表。

因为这里不使用 Vue 等框架,所以 Header.js、HomeList.js 不是真正的组件,只是模拟组件的样子,里面只写原生 js 代码,忽略样式等。

# 单独开发阶段

分别开发三个相互独立的应用: Nav、Home、Search 。

image.png

# 1、Nav

1.0、搭建 webpack 基本环境。

npm init -y
npm install --save-dev webpack webpack-cli webpack-dev-server html-webpack-plugin

1.1 测试代码。

//Header.js

const Header = () => {
    const header = document.createElement('h1') 
    header.textContent = '公共头部'
    return header
}

export default Header

//index.js

import Header from './Header.js'

const div = document.createElement('div')
div.appendChild(Header()) 
document.body.appendChild(div)

1.2、webpack.config.js

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
    entry: './src/index.js',
    output: {
        clean: true
    },
    mode: 'production',
    //devServer项可以设置,也可以不设置
    devServer: {
        static: path.resolve(__dirname, './dist'),
    },
    plugins: [
        new HtmlWebpackPlugin(),
    ]
}

1.3 打包。

npx webpack

image-20220124025435673

1.4 启动服务器,查看效果。

npx webpack serve --port 3003

image-20220124025446022

1.5 结论。

第一个应用 Nav 开发完成。

# 2、Home

2.0、搭建 webpack 基本环境。

npm init -y
npm install --save-dev webpack webpack-cli webpack-dev-server html-webpack-plugin

2.1 测试代码。

//HomeList.js

const HomeList = (num) => {
    let str = '<ul>' 
    for(let i=0; i<num; i++){
        str += '<li> item ' + i + '</li>'
    }
    str += '</ul>'
    return str 
}

export default HomeList

//index.js

import HomeList from './HomeList.js'

const div = document.createElement('div') 
div.innerHTML = HomeList(5)
document.body.appendChild(div)

2.2、webpack.config.js

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
    entry: './src/index.js',
    output: {
        clean: true
    },
    mode: 'production',
    plugins: [
        new HtmlWebpackPlugin(),
    ]
}

2.3 打包,启动服务器,查看效果。

npx webpack
npx webpack serve --port 3004

image.png

image.png

2.4 结论。

第二个应用 Home 开发完成了。

3.0、搭建 webpack 基本环境。

npm init -y
npm install --save-dev webpack webpack-cli webpack-dev-server html-webpack-plugin

3.1 测试代码。

Search 应用 要使用 Nav 应用中的公共组件 Header.js 和 Home 应用中的公共组件 HomeList.js 。

//index.js

3.2、webpack.config.js

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
    entry: './src/index.js',
    output: {
        clean: true
    },
    mode: 'production',
    plugins: [
        new HtmlWebpackPlugin(),
    ]
}
# 使用模块联邦

假设 Nav、Home、Search 这三个相互独立的应用 都已经开发完成,而且都已经发布上线了。在线上环境中,不同应用之间 如果想访问 彼此的公共模块,如 Nav 应用 想要访问 Home 应用 中的公共组件 HomeList,应该怎么办呢?

模块联邦

模块联邦 是 webpack 的一个内置插件。

# 1、Nav

使用 模块联邦插件 ModuleFederationPlugin 向外暴露了一个公共模块 Header

1.1 修改 webpack.config.js:

const { ModuleFederationPlugin } = require('webpack').container
 
plugins: [
    new ModuleFederationPlugin({
        name: 'nav',
        filename: 'remoteEntry.js',
        remotes: {},
        exposes: {
            './Header': './src/Header.js'
        },
        shared: {}
    })
]

1.2 打包,启动服务器

npx webpack
npx webpack serve --port 3003

image.png

image-20220124025531398

# 2、Home

1、使用 模块联邦插件 ModuleFederationPlugin 向外暴露了一个公共模块 HomeList

2、在 Home 应用 中,想要使用 Nav 应用 中提供的公共模块 Header。

2.1 修改 webpack.config.js:

const { ModuleFederationPlugin } = require('webpack').container

plugins: [
    new ModuleFederationPlugin({
        name: 'home',
        filename: 'remoteEntry.js',
        remotes: {
            nav: 'nav@http://localhost:3003/remoteEntry.js'
        },
        exposes: {
            './HomeList': './src/HomeList.js'
        },
        shared: {}
    })
]

2.2 修改 index.js

import HomeList from './HomeList.js'

import('nav/Header') 
.then((Header) => {
	const div = document.createElement('div')
	
	div.appendChild(Header.default())
	
	div.innerHTML += HomeList(5)
	document.body.appendChild(div)
})

2.3 打包,启动服务器。

npx webpack
npx webpack serve --port 3004

image-20220124025554373

image.png

2.4 页面效果

在浏览器中分别访问 两个应用: Nav 应用 http://localhost:3003/ ,Home 应用 http://localhost:3004/

image.png

image-20220124025609169

2.5 结论。

可见,Home 应用 成功地引入了 Nav 应用 中的 Header 模块。

# 3、Search

同理,Search 应用 如果想使用 Nav 应用中的 Header 模块 和 Home 应用中的 HomeList 模块,则依照上面的步骤进行即可。

3.1 webpack.config.js

const { ModuleFederationPlugin } = require('webpack').container

plugins: [
    new ModuleFederationPlugin({
        name: 'search',
        filename: 'remoteEntry.js',
        remotes: {
            nav: 'nav@http://localhost:3003/remoteEntry.js',
            home: 'home@http://localhost:3004/remoteEntry.js'
        },
        exposes: {},
        shared: {}
    })
]

3.2 index.js

/* 
import('nav/Header')
.then((Header) => {
    document.body.appendChild(Header.default())
})

import('home/HomeList')
.then((HomeList) => {
    document.body.innerHTML +=  HomeList.default(8) 
}) */

//使用 Promise.all() 优化上面的代码
Promise.all([import('nav/Header'), import('home/HomeList')])
.then(([
    { default: Header},
    { default: HomeList}
]) => {
    document.body.appendChild(Header())
    document.body.innerHTML += HomeList(8)
})

3.3 打包,启动服务器。

npx webpack 
npx webpack serve --port 3005

image-20220124025626687

image-20220124025647202

3.4 浏览器效果。

在浏览器中访问 Search 应用 页面 http://localhost:3005/

image.png

3.5 结论。

在线上环境中, Search 应用 成功地使用到了 Nav 应用 向外提供的公共模块 HeaderHome 应用 向外提供的公共模块 HomeList

# 结论

至此,我们就掌握了 webpack 模块联邦 的基本使用。


# 第十章:提升构建性能

webpack 的性能提升分为两类:
1、提升项目的性能。如减少首屏时间。受益者是用户。
2、提升 webpack 的构建编译性能。如提高编译速度,减少编译时间。受益者是开发人员。

本章讲的是第二种。

不同版本的 webpack 有不同的优化点,建议参考官网进行操作。

# 10.1 八个通用构建优化

无论你是在 开发环境 还是在 生产环境 下运行构建脚本,以下最佳实践都会有所帮助。

# 1、更新到最新版本

使用最新的 webpack 版本。我们会经常进行性能优化。

Node.js 更新到最新版本,也有助于提高性能。除此之外,将你的 package 管理工具(例如 npm 或者 yarn )更新到最新版本,也有助于提高性能。较新的版本能够建立更高效的模块树以及提高解析速度。

# 2、loader

将 loader 应用于最少数量的必要模块。而非如下:

module.exports = {
    //...
    module: {
        rules: [
            {
                test: /\.js$/,
                loader: 'babel-loader',
            },
        ],
    },
};

通过使用 include 字段,仅将 loader 应用在实际需要将其转换的模块:

const path = require('path');

module.exports = {
    //...
    module: {
        rules: [
            {
                test: /\.js$/,
                include: path.resolve(__dirname, 'src'),
                loader: 'babel-loader',
            },
        ],
    },
};

# 3、引导 (bootstrap)

每个额外的 loader/plugin 都有其启动时间。尽量少地使用工具。

# 4、解析

以下步骤可以提高解析速度:

  • 减少 resolve.modules , resolve.extensions , resolve.mainFiles , resolve.descriptionFiles 中条目数量,因为他们会增加文件系统调用的次数。
  • 如果你不使用 symlinks (例如 npm link 或者 yarn link ),可以设置 resolve.symlinks: false
  • 如果你使用自定义 resolve plugin 规则,并且没有指定 context 上下文,可以设置 resolve.cacheWithContext: false

# 5、小即是快 (smaller = faster)

  • 使用数量更少 / 体积更小的 library。
  • 在多页面应用程序中使用 SplitChunksPlugin 。
  • 在多页面应用程序中使用 SplitChunksPlugin ,并开启 async 模式。
  • 移除未引用代码。
  • 只编译你当前正在开发的那些代码。

# 6、持久化缓存

在 webpack 配置中使用 cache 选项。使用 package.json 中的 "postinstall" 清除缓存目录。

cache 类型设置为内存或者文件系统。 memory 选项很简单,它告诉 webpack 在内存中存储缓存,不允许额外的配置:

module.exports = {
    //...
    cache: {
        type: 'memory',
    },
};

# 7、自定义 plugin/loader

对它们进行概要分析,以免在此处引入性能问题。

# 8、dll

使用 DllPlugin 为更改不频繁的代码生成单独的编译结果。这可以提高应用程序的编译速度,尽管它增加了构建过程的复杂度。

# 9、worker 池 (worker pool)

thread-loader 可以将非常消耗资源的 loader 分流给一个 worker pool。

不要使用太多的 worker,因为 Node.js 的 runtime 和 loader 都有启动开销。最小化 worker 和 main process (主进程) 之间的模块传输。进程间通讯 (IPC,inter process communication) 是非常消耗资源的。

# 10、Progress plugin

将 ProgressPlugin 从 webpack 中删除,可以缩短构建时间。请注意,ProgressPlugin 可能不会为快速构建提供太多价值,因此,请权衡利弊再使用。


# 10.2 通用构建优化 - dll

本节,演示如何使用 dll 来提高构建速度。

# ◎ 示例

示例项目中使用了第三方包 lodash,它的代码是不变的,只需要打包一次,而我们自己的代码则因为频繁修改需要打包多次,所以可以对 lodash 使用 dll 技术,来避免不必要的重复打包,提高打包速度。

1、搭建 webpack 基本环境。

npm init -y
npm install --save-dev webpack webpack-cli webpack-dev-server html-webpack-plugin

npm i -S lodash

2、测试代码。

//index.js

import _ from 'lodash' 
console.log(_.join(['hello', 'webpack'], ' '))

//webpack.config.js

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
 
module.exports = {
    mode: 'production',
    entry: './src/index.js',
    output: {
        clean: true
    },
    plugins: [
        new HtmlWebpackPlugin(),
    ]
}

3、打包。

npx webpack

image.png

4、结论。

在没有使用 dll 技术时,打包耗时 3693ms,main.js 的体积为 69.5kb。

5、webpack.dll.config.js

新建 webpack.dll.config.js 文件,写入以下内容:

const path = require('path')
const webpack = require('webpack')

module.exports = {
    mode: 'production',
    entry: {
        lodash: ['lodash']
    },
    output: {
        filename: '[name].js',
        path: path.resolve(__dirname, 'dll'),
        library: '[name]_[hash]'
    },
    plugins: [
        new webpack.DllPlugin({
            name: '[name]_[hash]',
            path: path.resolve(__dirname, 'dll/manifest.json')
        })
    ]
}

6、package.json

"scripts": {
    "dll": "webpack --config ./webpack.dll.config.js",
},

7、dll 打包

dll 打包,生成 manifest 文件。

npm run dll

image.png

image.png

7.2、结论。

可见,打包输出了一个新的目录 dll,里面有:

  • lodash.js 第三方包被单独打包出来了
  • manifest.json 这个文件就是用来进行构建缓存的。
  • xxx.txt

8、webpack.config.js

使用 dll 打包后生成的文件。

...
const webpack = require('webpack')

...
plugins: [
    ...
    new webpack.DllReferencePlugin({
        manifest: path.resolve(__dirname, './dll/manifest.json')
    })
]

9、应用打包。

npx webpack

image-20220124025907051

10、结论。

可见,使用 dll 后,打包耗时为 608ms,main.js 体积为 556bytes,构建速度快了 6 倍。

注意:此时,main.js 中并不包含 lodash 代码,index.html 中也只引入了 main.js。所以还不能部署上线。

11、安装 AddAssetHtmlWebpackPlugin 插件。

npm i -D add-asset-html-webpack-plugin

12、配置 AddAssetHtmlWebpackPlugin 插件。

//webpack.config.js

...
const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin')

plugins: [
    ...
    new AddAssetHtmlPlugin({
        filepath: path.resolve(__dirname, './dll/lodash.js'),
        publicPath: './'
    })
]

13、打包,启动开发服务器。

npx webpack
npx webpack serve

image.png

image.png

image.png

image.png

14、结论。

开发时,先对第三方包进行 dll 打包,可以提高打包速度。最后打包时,需要使用 AddAssetHtmlWebpackPlugin 插件 来将 dll 打包好的第三方包 合并到最终的打包结果中,才能部署上线。


# 10.3 通用构建优化 - worker-pool

worker 池 (worker pool):
thread-loader 可以将非常消耗资源的 loader 分流给一个 worker pool 。

thread-loader 的使用方法: 哪个 loader 需要放在 worker pool 中运行,就在哪个 loader 的前面添加 thread-loader 。

# ◎ 示例

本示例中,使用 babel-loader 对应用中的 class 语法进行转换。我们想将 babel-loader 放在一个 worker pool 中运行,来提高打包速度。

也可以简单理解为:利用电脑多核的特点,将 babel-loader 单独放在一个 CPU 中运行。

1、搭建 webpack 基本环境。

npm init -y
npm install --save-dev webpack webpack-cli webpack-dev-server html-webpack-plugin

npm i -D babel-loader @babel/core @babel/preset-env

npm i -D thread-loader

2、webpack.config.js

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
    mode: 'development',
    entry: './src/index.js',
    output: {
        clean: true
    },
    devtool: 'cheap-module-source-map',
    devServer: {
        static: path.resolve(__dirname, './dist'),
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: [
                    {
                        loader: 'babel-loader',
                        options: {
                            presets: ['@babel/preset-env']
                        }
                    }
                ]
            }
        ]
    },
    plugins: [
        new HtmlWebpackPlugin(),
    ]
}

3、index.js

class Dog {
    constructor() {
        this.name = '汪汪'
    }

    sayHi(){
        console.log(this.name)
    }
}

const dog = new Dog() 
dog.sayHi() 

4、打包

npx webpack

image-20220124025949780

4.2 结论

可见,没有使用 worker pool 时,打包耗时 1171ms,main.js 体积为 1.22kb。

5、配置 thread-loader。

module: {
    rules: [
        {
            test: /\.js$/,
            exclude: /node_modules/,
            use: [
                {
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env']
                    }
                },
                //添加thread-loader
                {
                    loader: 'thread-loader',
                    options:{
                        workers: 2
                    }
                }
            ]
        }
    ]
},

6、打包

npx webpack

image-20220124030007602

6.2 结论

可见,使用 worker pool 时,打包耗时 1324ms,竟然比没有使用时还长,这是为什么呢?

这是因为启动 thread-loader 是需要时间的。另外一个原因是:worker pool 只对那些耗时很长的 loader 才会有明显的提升效果,而本示例中 babel-loader 只是简单转换了 class 语法,耗时并不长,所以效果不明显。


# 10.3 开发环境

以下步骤对于 开发环境 特别有帮助。

# 1、增量编译

使用 webpack 的 watch mode (监听模式)。而不使用其他工具来 watch 文件和调用 webpack 。内置的 watch mode 会记录时间戳并将此信息传递给 compilation 以使缓存失效。

在某些配置环境中,watch mode 会回退到 poll mode (轮询模式)。监听许多文件会导致 CPU 大量负载。在这些情况下,可以使用 watchOptions.poll 来增加轮询的间隔时间。

# 2、在内存中编译

下面几个工具通过在内存中(而不是写入磁盘)编译和 serve 资源来提高性能:

  • webpack-dev-server
  • webpack-hot-middleware
  • webpack-dev-middleware

# 3、stats.toJson 加速

webpack 4 默认使用 stats.toJson() 输出大量数据。除非在增量步骤中做必要的统计,否则请避免获取 stats 对象的部分内容。 webpack-dev-server 在 v3.1.3 以后的版本,包含一个重要的性能修复,即最小化每个增量构建步骤中,从 stats 对象获取的数据量。

# 4、Devtool

需要注意的是不同的 devtool 设置,会导致性能差异。

  • “eval” 具有最好的性能,但并不能帮助你转译代码。
  • 如果你能接受稍差一些的 map 质量,可以使用 cheap-source-map 变体配置来提高性能
  • 使用 eval-source-map 变体配置进行增量编译。

在大多数情况下,最佳选择是 eval-cheap-module-source-map 。

# 5、避免在生产环境下才会用到的工具

某些 utility, plugin 和 loader 都只用于生产环境。例如,在开发环境下使用 TerserPlugin 来 minify (压缩) 和 mangle (混淆破坏) 代码是没有意义的。通常在开发环境下,应该排除以下这些工具:

  • TerserPlugin
  • [fullhash] / [chunkhash] / [contenthash]
  • AggressiveSplittingPlugin
  • AggressiveMergingPlugin
  • ModuleConcatenationPlugin

# 6、最小化 entry chunk

Webpack 只会在文件系统中输出已经更新的 chunk。某些配置选项(HMR, output.chunkFilename 的 [name] / [chunkhash]/[contenthash] , [fullhash] )来说,除了对已经更新的 chunk 无效之外,对于 entry chunk 也不会生效。

确保在生成 entry chunk 时,尽量减少其体积以提高性能。下面的配置为运行时代码创建了一个额外的 chunk,所以它的生成代价较低:

module.exports = {
    // ...
    optimization: {
        runtimeChunk: true,
    },
};

# 7、避免额外的优化步骤

Webpack 通过执行额外的算法任务,来优化输出结果的体积和加载性能。这些优化适用于小型代码库,但是在大型代码库中却非常耗费性能:

module.exports = {
    // ...
    optimization: {
        removeAvailableModules: false,
        removeEmptyChunks: false,
        splitChunks: false,
    },
};

# 8、输出结果不携带路径信息

Webpack 会在输出的 bundle 中生成路径信息。然而,在打包数千个模块的项目中,这会导致造成垃圾回收性能压力。在 options.output.pathinfo 设置中关闭:

module.exports = {
    // ...
    output: {
        pathinfo: false,
    },
};

# 9、Node.js 版本 8.9.10-9.11.1

Node.js v8.9.10 - v9.11.1 中的 ES2015 Map 和 Set 实现,存在 性能回退。Webpack 大量地使用这些数据结构,因此这次回退也会影响编译时间。之前和之后的 Node.js 版本不受影响。

# 10、TypeScript loader

你可以为 loader 传入 transpileOnly 选项,以缩短使用 ts-loader 时的构建时间。使用此选项,会关闭类型检查。如果要再次开启类型检查,请使用 ForkTsCheckerWebpackPlugin 。使用此插件会将检查过程移至单独的进程,可以加快 TypeScript 的类型检查和 ESLint 插入的速度。

module.exports = {
    // ...
    test: /\.tsx?$/,
    use: [
        {
            loader: 'ts-loader',
            options: {
                transpileOnly: true,
            },
        },
    ],
};

# 10.4 生产环境

以下步骤对于 生产环境 特别有帮助。

# 不启用 SourcMap

source map 相当消耗资源,开发环境模式不要设置 source map

# 官方链接

百度脑图

# 视频教程

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

# 学习资料

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