概述
-
传统 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
-
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 中,并返回给客户端
- 引入客户端 React 根组件,调用
-
分别为客户端、服务端版本编写 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
出具体配置信息之后手动修改,实现一些定制化需求,此时就需要用上上面介绍的这些知识点