webpack5:高级篇(2)

505 阅读34分钟

第四章:多页面应用

为什么需要多页面应用?

在实际开发中,一个完整的系统不会将所有的功能都放在一个页面上,因为这样容易导致性能不佳。实际的做法是:按照 功能模块 划分成多个单页应用,每个单页应用都生成一个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.png

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。

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

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

3、打包生成了三个目录: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.png

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

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.png

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

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.png

现在你可以看到,生成了两个额外的文件: 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.png

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.png

image.png

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

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

image.png

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.png

还可以使用 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.png

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.png

image.png

image.png

image.png

6、结论。

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

8.2 构建小轮子

image.png

image.png

image.png

image.png

image.png

然而它只能通过被 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.png

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

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.png

3、添加用户。

npm adduser

image.png

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

4、发布。

image.png

npm publish

image.png

注意:

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

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

5、检查是否发布成功。

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

image.png

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.png

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.png

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

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

● UMD 方式共享模块

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

image.png

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

● 微前端方式共享模块

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

image.png

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

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

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

● 模块联邦方式

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

image.png

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

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.png

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

npx webpack serve --port 3003

image.png

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、Search

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.png

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.png

image.png

2.4 页面效果

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

image.png

image.png

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.png

image.png

3.4 浏览器效果。

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

image.png

3.5 结论。

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

结论

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

官方链接

百度脑图

optimization.splitChunks

http-server

webpack DevServer

devserverdevmiddleware.writeToDisk

ProvidePlugin

imports-loader

@babel/polyfill

output.library

视频教程

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

学习资料

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

上一篇

webpack5:高级篇(1)