1. 核心概念
webpack把一切静态资源视为模块,所以又叫做静态模块打包器。通过入口文件递归构建依赖图,借助不同的loader处理相应的文件源码,最终输出标环境可执行的代码。
通常我们使用其构建项目时,维护的是一份配置文件,如果把整个webpack功能视为一个复杂的函数,那么这份配置就是函数的参数,我们通过修改参数来控制输出的结果。
在开发环境中,为了提升开发效率和体验,我们希望源码的修改能实时且无刷新地反馈在浏览器中,这种技术就是HRM(Hot Module Replacement)。
借助于loader/plugin,我们可以差异化处理不同的文件类型。如果有个性化需求,还可以实现自定的loader/plugin。
2. 安装webpack
创建文件夹如 webpack,并在终端内执行
$ yarn init 一路回车
$ yarn add webpack webpack-cli -D
本节课安装的版本
webpack ^5.58.1
webpack-cli ^4.9.0
webpack从零到一
创建基础文件目录如下:
webpack/
src/
index.js
index.html
package.json
index.js
function test(content) {
document.querySelector('#app').innerHTML = content;
}
test('something');
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>webpack</title>
</head>
<body>
<div id="app"></div>
</body>
</html>
<script type="text/javascript" src="./output/main.js"></script>
// output/main.js是打包后的js代码路径
package.json
{
"name": "webpack-study",
"version": "1.0.0",
"description": "",
"main": "src/index.js",
"scripts": {
"test": "..."
},
"author": "",
"license": "ISC",
"devDependencies": {
"webpack": "^5.58.1",
"webpack-cli": "^4.9.0"
}
}
补充打包命令,即对 ./src/index.js 文件进行编译,输出目录为output,指定开发环境参数和devtool类型可以不对产物进行压缩,便于后续的产物分析。
"scripts": {
"build": "webpack ./src/index.js -o ./output --mode=development --devtool=cheap-module-source-map"
}
执行命令 yarn build,可见打包出的文件output/main.js 如下
这时候浏览器访问index.html 就可以看到something了!
webpack从一到二
- 新建webpack.config.js,将build命令执行的参数转移到该文件:
const path = require('path');
module.exports = {
mode: 'development',
devtool: 'cheap-module-source-map',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'output'),
filename: 'main.js'
},
}
// 修改package.json
"scripts": {
"build": "webpack"
}
执行命令 yarn build,效果一模一样。
- 增加ES6的转换能力
创建 src/es6.js文件
export default class CountChange {
count = 1
increment = () => {
this.count++
}
decrease = () => {
this.count--;
}
}
// index.js 中引入
import CountChange from './es6';
const instance = new CountChange();
function test(content) {
document.querySelector('#app').innerHTML = content;
}
test(instance.count)
这时候打包产物(这是局部):
可见class CountChange还是老样子,虽然chrome已经支持了类的原生运行,但有些浏览器还是只能使用ES5的代码。再如我们使用装饰器的话,chrome也无能为力。
3.babel出场
·安装
$ yarn add @babel/core @babel/preset-env babel-loader -D
·增加配置
// webpack.config.js
{
...
module: {
rules: [{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env']
],
}
}
}]
}
}
·再次打包,查看output/main.js
perfect!再接再厉,补上装饰器:
es6.js
const decorator = (target, key, descriptor) => {
target[key] = function (...args) {
console.log(this.count);
return descriptor.value.apply(this, args);
};
return target[key];
}
export default class CountChange {
count = 1
@decorator
increment() {
this.count++;
}
...
}
现在打包肯定是报错的,因为对于“@”这样的符号babel预设并不能直接识别。
·安装相应的插件
yarn add @babel/plugin-proposal-decorators -D
·配置
// webpack.config.js
{
...
module: {
rules: [{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env']
],
plugins: [
["@babel/plugin-proposal-decorators", { "legacy": true }],
]
}
}
...
}
·重新执行打包命令,得到main.js
注意,源码的target是类的原型对象,本示例中方法的装饰等于重写。
可用于生产的react脚手架
$ yarn add react react-dom @babel/preset-react -S
创建文件 src/react.js
import React from 'react';
import { render } from 'react-dom';
const App = () => <div>App</div>;
render(<App />, document.querySelector('#app'));
webpack.config.js
{
...
entry: './src/react.js', // 修改文件入口
// module.rules[0].use.options.presets
presets: [
...
'@babel/preset-react'
],
}
打包查看HTML文件。
·缓存包提取
optimization: {
splitChunks: {
cacheGroups: {
vendor: {
filename: 'vendor.js',
chunks: 'all',
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/
},
}
}
}
记得把产物引入 html文件。
· css loader
$ yarn add style-loader css-loader -D
·样式抽离
$ yarn add mini-css-extract-plugin -D
webpack.config.js
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
...
plugins: [
new MiniCssExtractPlugin({
filename: "[name].css",
chunkFilename: "[id].css"
})
],
...
// module.rules
// MiniCssExtractPlugin.loader 代替 style-loader
别忘记我们一直使用的开发环境模式,所以没有压缩。
·自动引入脚本和样式表
虽然上面的样式抽离成功了,但是并没有出现在 HTML文件中,我们依然要像之前的 js 文件一样手动引入。对于动态生成的文件,这显然是不现实的。于是:
$ yarn add html-webpack-plugin -D
const HtmlWebpackPlugin = require('html-webpack-plugin')
...
plugins:[
new HtmlWebpackPlugin()
],
效果是打包文件多了一个index.html:
这里的路径可以通过output 参数配置
问题是缺少 div#app 元素,所以我们使用模板:
plugins:[
new HtmlWebpackPlugin({
template: './template.html'
})
]
// template.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>webpack</title>
</head>
<body>
<div id="app"></div>
</body>
</html>
·HMR
到目前为止,我们一直在使用打包+刷新的模式查看代码效果,显然十分繁琐低效。借助本地开发服务器来解决这个问题:
$ yarn add webpack-cli webpack-dev-server -D
增加命令
“scripts”: {
“start”: “webpack serve”
}
启动效果:
这时候,我们修改App组件,可以看到页面已经实时更新了,只不过是【刷新】!继续:
webpack.config.js
{
devServer: {
port: 8000, // 顺便更改一下端口
hot: true
}
}
入口文件
…
render(, document.querySelector(’#app’));
if (module.hot) {
module.hot.accept(App, () => {
render(, document.querySelector(’#app’));
});
}
// 如果 App 组件是外部文件创建的,通常写作(与import导入的路径一致):
if (module.hot) {
module.hot.accept(’./App’, () => {
render(, document.querySelector(’#app’));
});
}
至此,一个功能相对完善的脚手架就搭建完成了!
异步组件
我们就下面的手动实现极简的异步(高阶)组件开始:
const lazy = fn => class extends React.Component {
state = {
Component: () => null
}
async componentDidMount() {
const { default: Component } = await fn();
this.setState({ Component });
}
render() {
const Component = this.state.Component;
return <Component {...this.props} />;
}
}
const Async = lazy(() => import('./Async'));
// 指定产出的模块名称(注意这里的注释是有用的):
const Async = lazy(() => import(/* webpackChunkName: "Async" */ './Async'));
webpack 将以import 函数为分割点,import(’./Async’) 返回 promise,等待组件加载完成后,展示真正的组件。Component: () => null为默认内容。
在项目中使用了async/await,如果报错regeneratorRuntime is not defined,因为babel默认只转换新的JavaScript语法(syntax),如箭头函数等,而不转换新的API,比如Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全局对象,此时需要一些辅助函数(babel 6.x以下版本借助polyfill,需要在entry之前或根文件头部引入,本课程均以babel 7之后的标准讲解):
$ yarn add @babel/plugin-transform-runtime -D
.babelrc
plugins: [
“@babel/plugin-transform-runtime”,
…
]
思考runtime做什么的?
我们要知道,异步组件从本质上,解决的还是SPA用户体验的问题。它为webpack提供了代码分割的依据,使得使用率高或者加载时间长的组件代码独立出去,同时通过低成本的过渡交互,保证了网站的体验。
require.ensure
在此不久前,require.ensure也是非常流行的一个代码异步加载的方式。
require.ensure(
dependencies: String[], // 依赖项
callback: function(require), // 加载组件
errorCallback: function(error), // 加载失败
chunkName: String // 指定产出块名称
)
// eg
require.ensure([], function () {
const ensure = require('./requireEnsure');
ensure.default();
},
() => null, 'require-ensure')
// 最后两个参数均可缺省
打包可见生成了 output/require-ensu