本文以react项目为例,从零到一配置webpack,涉及分chunk优化等内容。
项目初始化
在新建的文件夹中打开shell
,输入npm init -y
进行项目初始化,生成package.json
。
在package.json
中的scripts
当中添加如下指令,限制安装依赖使用pnpm
:
"scripts": {
"preinstall": "npx only-allow pnpm"
},
接着在decDependencies
中添加webpack
和webpack-cli
,一定记得webpack
是在开发依赖当中。
"devDependencies": {
"webpack": "5.89.0",
"webpack-cli": "5.1.4"
}
这里推荐使用vscode插件Version Lens
进行版本提示,安装后点击在屏幕右上角的V字按钮使用:
接着pnpm i
安装依赖,然后创建src
目录,在src
目录下创建index.js
,输入以下代码:
const a = 1;
console.log(a);
然后在package.json
下的scripts
中添加build
指令,并且在终端执行pnpm build
:
"scripts": {
"preinstall": "npx only-allow pnpm",
"build": "webpack"
},
可以看到在dist目录下生成了main.js,并且其中代码如下:
console.log(1);
这是因为webpack
默认src
下的index.js
作为入口,去生成dist
下的main.js
,并且在没有指定mode
的情况下,webpack
默认打开生产环境,会对代码执行 Tree-shaking。
配置
我们要真正给 webpack 指明入口、出口,以及相关的配置,就需要创建webpack.config.js
进行配置。
const path = require('path')
module.exports = {
mode: "production",
entry: path.resolve(__dirname, "src", "index.js"),
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "build")
}
}
其中,mode
指明了我们现在使用生产环境,entry
和output
分别对应入口和出口,并且我们推荐使用path来对路径进行处理。
我们也可以在打包产物中加入哈希,以便更好地控制缓存。
output: {
filename: "bundle.[hash].js",
path: path.resolve(__dirname, "build")
}
生成的产物如下,其中一个用了hash
,另一个用了内容哈希contenthash
,当然也可以用chunkhash
:
我们可以在output
中指定clean: true
使得每次打包都能清除之前的产物。
接下去要介绍 webpack 中的几大概念:
-
entry 入口
-
output 输出
-
module 模块
-
asset 资源
-
chunk 代码块
- bundle 产物
babel
我们接下来要做的就是配置模块,简单来说就是处理什么样的文件,使用什么样的 loader,这里以 babel-loader 为例,在webpack.config.js
中添加module
配置,同时也要记得去安装 babel-loader 和 @babel/core。
module: {
rules: [
{
test: /\.js$/,
use: "babel-loader"
}
]
},
babel 的配置我们建议在独立的文件babel.config.json
中配置,而不是在webpack.config.js
中。
我们测试下面这个例子,在src/index.js
中有如下代码:
const sum = (a, b) => {
return a + b;
}
const result = sum(20, 30);
console.log(result, sum)
打包后的产物如下:
!function(){const o=(o,n)=>o+n,n=o(20,30);console.log(n,o)}();
可见代码经过了丑化压缩,并且最外层使用了立即执行函数(IIFE)做作用域的隔离。
而我们如果要在输出时对代码进行降级,也就是打polyfill
,因为有些低版本浏览器不支持高阶语法,比如箭头函数,我们就要借助编译工具,也就是 babel 的插件集,这里用以上的例子测试下箭头函数转 function。
首先安装@babel/plugin-transform-arrow-functions
,然后在babel.config.json
中做如下配置:
{
"plugins": ["@babel/plugin-transform-arrow-functions"]
}
接着pnpm build
打包,产物如下:
!function(){const n=function(n,o){return n+o},o=n(20,30);console.log(o,n)}();
可以看到箭头函数被编译成了普通函数。
那么我们要转换一种语法就要用一种插件,以后项目就会非常复杂,所以 babel 把插件打包成了 preset。
我们将刚刚的插件换成@babel/preset-env
,在babel.config.json
中作如下配置:
{
"presets": ["@babel/preset-env"]
}
最终打包结果和使用箭头函数插件的结果一样。
Typescript
再往后我们要去做 typescript 编译相关的配置。
首先我们需要安装 babel 中 ts 的预设@babel/preset-typescript
。
然后要在babel.config.json
中作如下配置:
{
"presets": ["@babel/preset-env", "@babel/preset-typescript"]
}
并且webpack.config.js
中,babel-loader
处理的文件类型也要进行相应的改变:
module.exports = {
mode: "production",
entry: path.resolve(__dirname, "src", "index.ts"),
module: {
rules: [
{
test: /\.(j|t)s$/,
use: {
loader: "babel-loader"
}
}
]
},
output: {
filename: "bundle.[hash].js",
path: path.resolve(__dirname, "build"),
clean: true
}
}
最后我们打包如下index.ts
:
const myName: string = "musio";
const age: number = 18;
console.log(myName, age);
产物如下:
console.log("musio",18);
如果这个过程中有哪里报错就去检查有没有什么地方没有改成 ts。
React
接下去我们基于 webpack 自己配置一个 react 项目。
我们首先要安装@babel/preset-react
、react
,其中react
要安装到dependencies
当中。
"dependencies": {
"react": "18.2.0"
},
"devDependencies": {
"@babel/core": "^7.23.7",
"@babel/preset-env": "^7.23.8",
"@babel/preset-react": "^7.23.3",
"@babel/preset-typescript": "^7.23.3",
"babel-loader": "^9.1.3",
"webpack": "5.89.0",
"webpack-cli": "5.1.4"
}
其次我们要在babel.config.json
的preset中加入@babel/preset-react
:
{
"presets": [
"@babel/preset-env",
"@babel/preset-typescript",
"@babel/preset-react"
]
}
接下去要修改webpack.config.js
中识别文件的类型:
module: {
rules: [
{
test: /\.(js|ts|tsx)$/,
use: {
loader: "babel-loader"
}
}
]
},
此外,为了简化导入时的名称,无需'xxx.tsx'
而是'xxx'
,我们要在webpack.config.js
中指定后缀名。
resolve: {
extensions: [".js", ".ts", ".tsx"]
}
接下去我们创建基本的组件示例Button.tsx
:
import React from 'react'
const Button = () => {
return <div>I am Button</div>
}
export default Button;
运行pnpm build
,最终产物包括了 react 的一些 hooks 以及最关键的:
return e.createElement("div",null,"I am Button")
devServer
这里是 webpack 很重要的一个能力,也就是 devServer 和热更新的能力。
我们在webpack.config.js
中添加如下配置:
devServer: {
// 项目构建后的路径
static: path.resolve(__dirname, "build"),
// 启动 gzip 压缩
compress: true,
// 端口号
port: 3000,
// 自动打开浏览器
open: true,
// 开启 HMR 功能
hot: true
}
同时我们要安装webpack-dev-server
,并且此时,再叫build就不合理了,应该改成:
"scripts": {
"preinstall": "npx only-allow pnpm",
"dev": "webpack-dev-server",
"build": "webpack --mode production"
},
或者也可以用webpack --config webpack.dev.js
,通过独立的文件来配置开发环境。
运行pnpm dev
,可以看到自动打开了网页,并且更改 src 中的代码,项目实时更新。
我们更改 App 和 Button 组件,来写点 react 基本语法:
import React from "react"
import { useState } from "react"
import Button from './Button'
const App = () => {
const [count, setCount] = useState(0)
return (
<>
<div>App {count}</div>
<Button onClick={() => setCount((c) => c + 1)}>+</Button>
</>
)
}
export default App
import React from 'react'
interface ButtonProps {
onClick: () => void;
children: React.ReactNode;
}
const Button: React.FC<ButtonProps> = ({ onClick, children }) => {
return <button onClick={onClick}>{children}</button>
}
export default Button;
可以看到点击按钮,可以使 count 值增加。
但是代码会有报错提示,这是因为我们没有 tsconfig 导致的,所以我们接下去去配置tsconfig.json
。
tsconfig.json
tsconfig.json
配置如下:
{
"compilerOptions": {
"target": "ESNext",
"module": "ES6",
"jsx": "react",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"strict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
}
}
我们发现配置了tsconfig.json
之后还是会有许多报错,这是因为我们要安装@types/react
和@types/react-dom
这两个依赖。
安装后发现现在只有createRoot报错,我们改成以下的方式:
import React from "react";
import ReactDOM from "react-dom"
import App from './App'
ReactDOM.render(<App />, document.getElementById("root"));
优化
优化以前是通过 webpack 插件去实现的,现在是在 webpack 中通过配置optimization
去实现,要记得优化一定是在生产模式中。
我们使用webpack官网推荐的配置:
optimization: {
usedExports: false,
splitChunks: {
chunks: 'async',
minSize: 20000,
minRemainingSize: 0,
minChunks: 1,
maxAsyncRequests: 30,
maxInitialRequests: 30,
enforceSizeThreshold: 50000,
cacheGroups: {
defaultVendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
reuseExistingChunk: true,
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
},
},
},
},
我们也可以测试一下 react 的 lazyComponent,在App.tsx
中:
import React from "react"
import { Suspense, useState } from "react"
// import Button from './Button'
const AsyncButton = React.lazy(() =>
import('./Button')
)
const App = () => {
const [count, setCount] = useState(0)
return (
<>
<div>App {count}</div>
<Suspense fallback={<div>loading...</div>}>
<AsyncButton onClick={() => setCount((c) => c + 1)}>+</AsyncButton>
</Suspense>
</>
)
}
export default App
可以看到生成了另一部分代码,内容就是 button。
为什么要分chunk?
因为首屏加载优化,我们要让第一次加载的代码量足够小。
我们可以安装webpack-bundle-analyzer
来分析代码体积,然后在webpack.config.js
中使用:
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
module.exports = {
...
plugins: [
...
new BundleAnalyzerPlugin()
],
...
}
此时重新运行 build: