之前写了了篇文章介绍了如何基于 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";
写在最后,如果按照文章的操作没有达到预期效果,多半是库的版本不对!