基于 Webpack 从 0 到 1 启动一个 React 项目
之前写了了篇文章介绍了如何基于 Webpack 从 0 到 1 启动一个 Vue 项目,那么就很有必要介绍一下 Vue 的竞品 React 是如何基于 Webpack 从 0 到 1 启动
下面是这个项目运行效果,以及完整的示例源代码

普通启动
如果你是刚开始接触 HTML/CSS/JavaScript 三件套开始接触的前端,那么你可能比较熟悉或者比较能接受的引入 React 的方式可能是使用 CDN 的方式,大概如下(下面这个是我要介绍的例子)
<head>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
</head>
<body>
<div id="root"></div>
</body>
<script>
const useState = React.useState;
const createElement = React.createElement;
function App() {
let [count, setCount] = useState(0);
const handleClick = () => {
setCount(++count);
};
return createElement("div", {
children: [
"count:" + count,
createElement(
"button",
{
key: "2",
onClick: handleClick,
},
"click + 1"
),
],
});
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
createElement(React.StrictMode, {
children: [createElement(App, { key: "1" })],
})
);
</script>
但这种方式是基于 React.createElement 去编写 DOM 的代码的,React 中比较常用的是 JSX 语法,它们的文档认为 JSX
它有助于编写UI代码 - 无论是使用 React 还是其他库。
而 React 的文档推荐使用 JSX 的方式是使用 babel 转换,其实使用 babel 这已经涉及到前端工程化的初步阶段了,因为使用 babel 需要使用 node.js,毕竟你知道了 node.js 后就会知道 npm 就会知道 webpack 就会知道 create-react-app
而对于 Vue 来说,你可以用下面的模板写好一段时间
// template
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<body>
<!-- DOM -->
</body>
<script>
// js
</script>
<style>
/** css **/
</style>
Webpack 启动
真正要上手 React 还是需要使用 Webpack,Webpack 主要使用来对项目中模块进行打包,并且它的生态链中还具有热更新和一系列转换的功能,大概是下面这样

接下来就是 React 项目启动的初步配置
先找个位置并在终端(或者命令行)初始化一个项目
npm init
初始化后会有一些选项,可以直接回车全部忽略,也可以根据自己意向填写

选择完成之后

这个时候系统会创建一个 package.json 文件

接下来就是开始配置 Webpack 系列套件
Webpack 系列套件
npm install -D webpack webpack-dev-server webpack-cli
-D, --save-dev 代表打包时该部分依赖不会被打包进去
创建 webapck.config.js,它是 Webpack 的配置文件,文件内容如下
// webpack.config.js
const path = require("path");
module.exports = {
mode: "development",
devServer: {
static: {
// 配置提供静态资源服务的目录
directory: path.join(__dirname, "public"),
},
// 静态资源服务端口
// React 预览将在此端口
port: 8080,
},
};
配置脚本命令,修改 package.json 的 "script" 字段如下
{
// ...
"scripts": {
"start": "webpack server"
},
// ...
}
同时创建 public 文件夹,后期将在这个文件上创建静态资源文件,例如 .html
此时的文件目录结构
├── node_modules
├── public
├── package-lock.json
├── package.json
└── webpack.config.js
React 系列套件
接下来就是 React 系列套件的配置,根据上面的 html 例子分别下载 react 和 react-dom
npm install react react-dom
react 大家都清楚,那么 react-dom 这个包是干嘛的呢?简单来说,react-dom 是 react 剥离出的涉及 DOM 操作的部分
react 的核心思想是虚拟 DOM,react 包含了生成虚拟 DOM 的函数 react.createElement,及 Component 类。当我们自己封装组件时,就需要继承 Component 类,才能使用生命周期函数等。而 react-dom 包的核心功能就是把这些虚拟 DOM 渲染到文档中变成实际 DOM
不关注 Vue 的同学可以跳过下面这个段落
与 Vue 编译时的区别
比较熟悉 Vue 的同学可能会有疑问,react-dom 是 react 的编译时吗?因为在 Vue2 中有运行时(runtime)和编译时,在 Vue3 中也是单独剥离出一个包给编译时(@vue/complier-sfc)
其实严格来说 react-dom 是负责处理将各自框架的虚拟 DOM 渲染到浏览器中,算是一个运行时的东西,但是 Vue 把它集成在了运行时中而不是和 React 一样独立出来
Vue 的编译时严格意义上来说是将开发自定义的模板编译成虚拟 DOM,比如
<template>
<div>
{{ "count:" + count }}
<button @click="handleClick">click + 1</button>
</div>
</template>
// 模拟虚拟 DOM
// 可以将 h 函数中的参数结合为一个 option 对象
// option 对象就是一个简易的 vnode
// vnode 通常会有更多内置的属性
h("div", [
"count:" + this.count,
h(
"button",
{
on: {
click: this.handleClick,
},
},
"click + 1"
),
]);
而 react 本身是通过 babel-loader 实现 JSX 和 React.createElement 的互转的,在下面的 JSX 配置 中有说明
所以结论就是 react-dom 是 Vue 运行时中将虚拟 DOM 渲染到浏览器真实 DOM 的功能部分,是运行时而不是编译时
解构
下载完 React 系列套件的依赖后,就需要将上面的 html 例子进行解构,创建 src/index.js 、 App.js 和 public/index.html
将上面的 html 的例子转换成下面的结构
// App.js
import { useState, createElement } from "react";
function App() {
let [count, setCount] = useState(0);
const handleClick = () => {
setCount(++count);
};
return createElement("div", {
children: [
"count:" + count,
createElement(
"button",
{
key: "uniqueId",
onClick: handleClick,
},
"click + 1"
),
],
});
}
export default App;
// index.js
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
React.createElement(React.StrictMode, {
children: [React.createElement(App, { key: "1" })],
})
);
<!-- index.html -->
<body>
<div id="root"></div>
</body>
<script src="./bundle.js"></script>
同时修改 webpack.config.js 添加输入输出
module.exports = {
// ...
entry: path.join(__dirname, "./src/index.js"),
output: {
publicPath: "",
filename: "bundle.js",
},
};
关于 entry 和 output 两个字段配置不熟悉的可以参考这篇文章 基于 Webpack 从 0 到 1 启动一个 Vue 项目
此时在终端/命令行运行脚本 npm run start 即可得到文章开头提到的效果

此时的文件目录结构
├── node_modules
├── public
| └── index.html
├── src
| ├── App.js
| └── index.js
├── package-lock.json
├── package.json
└── webpack.config.js
JSX 配置
前面说过 React.createElement 可以用 JSX 代替,因为
从本质上讲,JSX 只是为
React.createElement(component, props, ...children)函数提供的语法糖
强行使用 JSX
如果强行使用 JSX 连 run 都做不到,直接报错
ERROR in ./src/index.js 7:2
Module parse failed: Unexpected token (7:2)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
| const root = ReactDOM.createRoot(document.getElementById("root"));
| root.render(
> <React.StrictMode>
| <App />
| </React.StrictMode>
替换 createElement
而 JSX 代替 React.createElement 的步骤如下
下载 babel-loader 和 @babel/preset-react
npm instal -D babel-loader @babel/preset-react
在根目录下创建 .babelrc
// .babelrc
{
"presets": [
[
"@babel/preset-react",
{
"runtime": "automatic"
}
]
]
}
修改 webpack.config.js
const path = require("path");
module.exports = {
// ...
module: {
rules: [
{
test: /[\.js|\.jsx]$/,
loader: "babel-loader",
exclude: /node_modules/,
},
],
},
};
修改 App.js 和 index.js 中关于 React.createElement 的部分
// index.js
// ...
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// App.js
import { useState } from "react";
function App() {
// ...
return (
<div>
{"count:" + count}
<button onClick={handleClick}>click + 1</button>
</div>
);
}
export default App;
再次运行脚本 npm run start,可以达到同样预期
以下几个问题可以关注一下
注意问题
@babel/preset-react
React 预设(preset)因为 Babel 的版本问题,在 Babel 7 中预设被换成了 @babel/preset-react 而不是 babel-preset-react
通常 Babel X 是指 @babel/core 的版本,比如当前的最新版本 @babel/core@7.20.5,虽然文章例子中并没有使用直接使用 @babel/core 但 babel-loader v8/v9 对应 Babel v7
This README is for babel-loader v8/v9 with Babel v7 If you are using legacy Babel v6, see the 7.x branch docs
可以参考这个讨论 Got error: Plugin/Preset files are not allowed to export objects, only functions
automatic 配置
你可能注意到了,在 .babelrc 的 @babel/preset-react 有选项配置
// .babelrc
{
"presets": [
[
"@babel/preset-react",
{
"runtime": "automatic"
}
]
]
}
首先这个 runtime 选项是用于配置需不需要自动导入 React 的 JSX 转换函数,因为 @babel/preset-react 的 "rumtime" 默认值 classic 默认不导入,只是将 JSX 转换成渲染函数版本大概如下
return /*#__PURE__*/React.createElement("div", null, "count:" + count, /*#__PURE__*/React.createElement("button", {
onClick: handleClick
}, "click + 1"));
而且会报错
Uncaught ReferenceError: React is not defined
解决方法
- 修改
rumtime的值 - 或者主动导入
React,比如
import React from "react";
import { useState } from "react";
写在最后,如果按照文章的操作没有达到预期效果,多半是库的版本不对!
参考资料
转载自:https://juejin.cn/post/7172581396144193573