传统 Web 开发强调样式、结构、逻辑分离,以此降低技术复杂度。但 React 认为渲染逻辑本质上与其它 UI 逻辑存在内在耦合关系,所以提倡将结构、逻辑与样式共同存放在同一文件中,以“组件”这种松散耦合结构实现关注点分离,并为此设计实现了一套 JavaScript-XML(JSX) 技术,以支持在 JavaScript 中编写 Template 代码,如:
import React from 'react';
const Component = () => {
return <div className="hello">hello world</div>
}
为支持这一特性,我们需要搭建一套使用的工程化环境,将 JSX 及 React 组件转换为能够在浏览器上运行的 JavaScript 代码。本文将递进介绍使用 Webpack 搭建 React 应用开发环境的主要方法,包括:
- 如何使用
Babel
处理JSX文件? - 如何使用
html-webpack-plugin
、webpack-dev-server
运行 React 应用? - 如何在 React 中复用 TypeScript、Less 等编译工具?
- 如何搭建 React SSR 环境?
- 如何使用 Create React App?
使用 Babel 加载 JSX 文件
绝大多数情况下,我们都会使用 JSX 方式编写 React 组件,但问题在于浏览器并不支持这种代码,为此我们首先需要借助构建工具将 JSX 等价转化为标准 JavaScript 代码。
在 Webpack 中可以借助 babel-loader
,并使用 React 预设规则集 @babel/preset-react
,完成 JSX 到 JavaScript 的转换,具体步骤:
- 安装依赖:
yarn add -D webpack webpack-cli babel-loader @babel/core @babel/preset-react
- 修改 Webpack 配置,加入
babel-loader
相关声明:
module.exports = {
mode: 'none',
module: {
rules: [
{
test: /\.jsx$/,
loader: "babel-loader",
options: {
presets: ["@babel/preset-react"],
}
},
],
},
};
- 执行构建命令,如
npx webpack
。
经过 babel-loader
处理后,JSX 将被编译为 JavaScript 格式的 React.createElement
函数调用,如:
此外,JSX 支持新旧两种转换模式,一是上图这种 React.createElement
函数,这种模式要求我们在代码中引入 React,如上图的 import React from "react"
;二是自动帮我们注入运行时代码,此时需要设置 runtime:automatic
,如:
{
test: /\.jsx$/,
loader: 'babel-loader',
options: {
"presets": [
["@babel/preset-react", {
"runtime": "automatic"
}]
]
}
}
这种模式会自动导入 react/jsx-runtime
,不必开发者手动管理 React 依赖。
加载 CSS 文件
注意,上例 Webpack 配置还无法处理 CSS 代码:
为此需要添加 CSS 加载器,如 css-loader/style-loader
,如:
module.exports = {
mode: 'none',
module: {
rules: [
{
test: /\.jsx$/,
loader: 'babel-loader',
options: {
'presets': [["@babel/preset-react", {
"runtime": "automatic"
}]]
}
},
{
test: /\.css$/,
use: ["style-loader", "css-loader"],
}
],
},
};
相关用法已在其它章节有详细介绍,此处不再赘述。
运行页面
上例接入的 babel-loader
使得 Webpack 能够正确理解、翻译 JSX 文件的内容,接下来我们还需要用 html-webpack-plugin
和 webpack-dev-server
让页面真正运行起来,配置如下:
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
module: {/*...*/},
devServer: {
hot: true,
open: true
},
plugins: [
new HtmlWebpackPlugin({
templateContent: `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Webpack App</title>
</head>
<body>
<div id="app" />
</body>
</html>
`
})
]
};
之后,运行 npx webpack serve
命令,即可自动打开带热更功能的页面:
复用其它编译工具
与 Vue 类似,在 React 开发环境中我们也可以搭配其它工程化工具提升开发效率、质量,包括:
- 使用
babel-loader
、ts-loader
加载 TSX 代码; - 使用
less-loader
、sass-loader
预处理样式代码。
使用 TSX
社区有两种主流的 TSX 加载方案,一是使用 Babel 的 @babel/preset-typescript
规则集;二是直接使用 ts-loader
。先从 Babel 规则集方案说起:
- 安装依赖,核心有:
yarn add -D typescript @babel/preset-typescript
- 修改 Webpack 配置,添加用于处理 TypeScript 代码的规则:
module.exports = {
module: {
rules: [
{
test: /\.tsx$/,
loader: 'babel-loader',
options: {
'presets': [["@babel/preset-react", {
"runtime": "automatic"
}],
'@babel/preset-typescript']
}
},
],
},
}
之后,将组件文件后缀修改 .tsx
,Babel 就会帮我们完成 TypeScript 代码编译。ts-loader
用法也很相似:
- 安装依赖:
yarn add -D typescript ts-loader
- 修改 Webpack 配置,添加
ts-loader
规则:
module.exports = {
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
module: {
rules: [
{
test: /\.tsx$/,
use: 'ts-loader',
},
],
}
};
- 修改
tsconfig.json
文件,添加jsx
配置属性:
{
"compilerOptions": {
//...
"jsx": "react-jsx"
}
}
完毕。两种方式功能效果相似,相对而言我个人更倾向于 babel-loader
,因为 Babel 是一种通用的代码编译工具,配置适当 Preset 后能做的事情更多,相关经验更容易复用到其它场景。
使用 CSS 预处理器
类似的,我们还可以使用 Less/Sass/Stylus 等语言开发 CSS 代码,接入过程与上述 TypeScript 相似,以 Less 为例,首先安装依赖:
yarn add -D less less-loader css-loader style-loader
其次,修改 Webpack 配置,添加 Less 文件相关处理规则:
module.exports = {
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
module: {
rules: [
{
test: /\.tsx$/,
use: 'ts-loader',
},
{
test: /\.less$/,
use: ["style-loader", "css-loader", "less-loader"],
},
],
},
};
之后,引入相关样式文件.less
,然后,Webpack 就会使用 less-loader
加载这一模块内容。
提示:其它 CSS 相关工具,如 Sass、Stylus、PostCSS 均遵循同样规则。
实现 Server Side Render
在上一章节介绍如何搭建 Vue 开发环境时,已经就 SSR 的基本概念与各项优缺点做了详细阐述,这里我们就直接进入主题吧。React 有许多实现 SSR 的方案,例如:Next.js、egg-react-ssr、ssr(基于egg-react-ssr) 等,接下来我们尝试使用 Webpack、React、Express 搭建一套 React SSR 应用环境,一步步剖析关键技术点。示例代码目录结构(示例代码已上传到小册仓库):
├─ react-ssr-example
│ ├─ package.json
│ ├─ server.js
│ ├─ src
│ │ ├─ App.css
│ │ ├─ App.jsx
│ │ ├─ entry-client.jsx
│ │ ├─ entry-server.jsx
│ ├─ webpack.base.js
│ ├─ webpack.client.js
│ └─ webpack.server.js
- 首先,需要为客户端环境准备项目入口文件 ——
entry-client.js
,内容:
import { createRoot } from 'react-dom/client';
import App from './App';
const container = document.getElementById('app');
const root = createRoot(container);
root.render(<App />);
- 为服务端环境准备入口文件 ——
server-client.js
,内容:
import React from 'react'
import express from 'express';
import App from './App'
import { renderToString } from 'react-dom/server';
// 通过 manifest 文件,找到正确的产物路径
const clientManifest = require("../dist/manifest-client.json");
const server = express();
server.get("/", (req, res) => {
const html = renderToString(<App/>);
const clientCss = clientManifest["client.css"];
const clientBundle = clientManifest["client.js"];
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>React SSR Example</title>
<link rel="stylesheet" href="${clientCss}"></link>
</head>
<body>
<!-- 注入组件运行结果 -->
<div id="app">${html}</div>
<!-- 注入客户端代码产物路径 -->
<!-- 实现 Hydrate 效果 -->
<script src="${clientBundle}"></script>
</body>
</html>
`);
});
server.use(express.static("./dist"));
server.listen(3000, () => {
console.log("ready");
});
上例代码核心逻辑:
- 引入客户端 React 根组件,调用
renderToString
将其渲染为 HTML 字符串; - 获取客户端打包产物映射文件
manifest
文件,然后将组件 HTML 字符串与entry-client.js
产物路径注入到 HTML 中,并返回给客户端。
- 分别为客户端、服务端版本编写 Webpack 配置文件,即上述目录中的三个
webpack.*.js
文件。其中:base
用于设定基本规则;webpack.client.js
用于定义构建客户端资源的配置:
const Merge = require("webpack-merge");
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const { WebpackManifestPlugin } = require("webpack-manifest-plugin");
const base = require("./webpack.base");
// 继承自 `webpack.base.js`
module.exports = Merge.merge(base, {
entry: {
// 入口指向 `entry-client.js` 文件
client: path.join(__dirname, "./src/entry-client.jsx"),
},
output: {
filename: 'index.js',
publicPath: "/",
},
module: {
rules: [{ test: /\.css$/, use: [MiniCssExtractPlugin.loader, "css-loader"] }],
},
plugins: [
// 这里使用 webpack-manifest-plugin 记录产物分布情况
// 方面后续在 `server.js` 中使用
new WebpackManifestPlugin({ fileName: "manifest-client.json" }),
// 生成CSS文件
new MiniCssExtractPlugin({
filename: 'index.[contenthash].css'
}),
// 自动生成 HTML 文件内容
new HtmlWebpackPlugin({
templateContent: `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Webpack App</title>
</head>
<body>
<div id="app" />
</body>
</html>
`,
}),
],
});
注意:
- 这里我们需要使用
webpack-manifest-plugin
插件记录产物构建路径,之后才能在server.js
中动态注入 HTML 代码中;- 示例代码还用到
mini-css-extract-plugin
,将 CSS 从 JS 文件中抽离出来,成为一个单独的文件。
- 在
webpack.server.js
定义构建服务端资源的配置:
const Merge = require("webpack-merge");
const path = require("path");
const { WebpackManifestPlugin } = require("webpack-manifest-plugin");
const base = require("./webpack.base");
module.exports = Merge.merge(base, {
entry: {
server: path.join(__dirname, "./src/entry-server.jsx"),
},
target: "node",
output: {
// 打包后的结果会在 node 环境使用
// 因此此处将模块化语句转译为 commonjs 形式
libraryTarget: "commonjs2",
filename: 'server.js'
},
module: {
rules: [{
test: /.css$/,
loader: './loader/removeCssLoader'
}]
},
});
大部分配置与普通 Node 应用相似,唯一需要注意的是:在 SSR 中,通常由客户端代码提前做好 CSS 资源编译,对服务端而言只需要支持输出构建后的 CSS 文件路径即可,不需要关注 CSS 具体内容,因此通常会用一个简单的自定义 Loader 跳过 CSS 资源,如:
module.exports = () => {
return 'module.exports = null';
};
接下来,我们只需要调用适当命令即可分别生成客户端、服务端版本代码:
# 客户端版本:
npx webpack --config ./webpack.client.js
# 服务端版本:
npx webpack --config ./webpack.server.js
- 至此,SSR 的工程化框架搭建完毕,接下来可以开始编写任何 React 代码,例如:
import React, { useState } from 'react';
import './App.css';
const App = () => {
const [isActivity, setIsActivity] = useState(false);
const handleClick = () => {
setIsActivity(!isActivity);
};
return (
<div>
<h3 className={`main ${isActivity ? 'activate' : 'deactivate'}`}>Hello World</h3>
<button onClick={handleClick}>Toggle</button>
</div>
);
};
export default App;
之后,编译并执行 node ./dist/server.js
启动 Node 应用,访问页面时服务端将首先返回如下 HTML 内容:
页面也能正常运行 App.jsx
交互效果:
提示:
- 实际项目中建议使用更成熟、完备的技术方案,如 Next.js;
- 建议大家拉取示例代码,阅读学习。
总的来说,React 的 SSR 实现逻辑与 Vue 极为相似,都需要搭建对应的 Client、Server 端构建环境,之后在 Server 端引入组件代码并将其渲染为 HTML 字符串,配合 manifest
记录的产物信息组装出完整的 Web 页面代码,从而实现服务端渲染能力。
使用 Create React App
综上,手动配置 React 开发环境的过程复杂且繁琐的,如果每次构建项目都需要从零开始使用 Webpack、Babel、TypeScript、Less、Mocha 等工具搭建项目环境,那对新手、老手来说都是极高的门槛和心智负担。
好在社区已经将大量重复、被验证有效的模式封装成开箱即用的脚手架工具,包括:
- Create React App:是官方支持的创建 React 应用程序的方式,提供免配置的现代构建开发环境;
- Modern JS:字节跳动开源的现代 Web 工程体系。
这些工具能够快速生成一套健壮的 React 开发环境,以 Create React App 为例,只需执行一条简单命令:
npx create-react-app my-app
之后,Create React App 会自动安装项目依赖,项目环境就算是搭建完毕了。
Create React App 提供的默认配置已经能够满足许多场景下的开发需求,必要时开发者还可以通过customize-cra 和 react-app-rewired 修改工程化配置,例如:
const { override, addLessLoader } = require("customize-cra");
module.exports = override(
addLessLoader({
strictMath: true,
noIeCompat: true,
cssLoaderOptions: {},
cssModules: {
localIdentName: "[path][name]__[local]--[hash:base64:5]",
},
})
));
然后修改 Script 运行脚本:
"scripts": {
- "start": "react-scripts start",
+ "start": "react-app-rewired start",
- "build": "react-scripts build",
+ "build": "react-app-rewired build",
- "test": "react-scripts test",
+ "test": "react-app-rewired test",
"eject": "react-scripts eject"
}
提示:更多信息可参考 Create React App 官网 Working with Webpack 一节。
总结
本文介绍如何使用 Webpack 开发 React 应用,从最基础的 JSX 代码编译;到如何使用 TypeScript、Less 等基础编译工具;再到如何搭建 React SSR 应用;最后介绍如何使用 Create React App 迅速搭建开发环境。
就我个人而言,多数情况下我都会选择使用 Create React App 或其它脚手架工具快速搭建开发框架,但多数时候又必须 eject
出具体配置信息之后手动修改,实现一些定制化需求,此时就需要用上上面介绍的这些知识点。
思考题
React JSX 经过 Webpack 转换后的结果与 Vue SFC 转换结果极为相似,为何 Vue 不能复用 Babel 而选择开发一个独立的 vue-loader
插件?