上手React:一个案例,两个环境
目前,前端两大框架React和Vue分庭抗礼。React语法更接近JS,生态更加繁荣,成熟的企业项目大多由React实现。Vue在中文社区的热度只增不减,封装了许多复杂操作,被认为上手更简单。
两者在响应式系统、模版语法等方面有一些设计上的关键区别,但对于新手来说,不足以成为选择依据。构建第一个Web应用时,个人认为如果有JS基础,可直接尝试React,省去记忆新语法的心智负担。
第一个react项目
初始结构
最常见、简单的React项目,一般会从这三个文件开始
├── index.html
└── src
├── App.js
└── index.js
在 index.html
中,我们设置网页基本信息、引入模块,并创建一个id为 root
的元素,后续会将React的 App
组件挂载到该元素上
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<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>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel" src="src/App.js"></script>
<script type="text/babel" src="src/index.js"></script>
</body>
</html>
一般本地引入的 <script>
,放到 <body>
最后,保证脚本执行前DOM元素已存在。CDN引入的放在 <head>
内部,使其优先加载,因为后续代码都依赖它
react.development.js
是React模块本体,react-dom.development.js
负责将组件挂载到DOM节点、更新组件,babel.min.js
的作用是将React的JSX语法翻译为JS
模块导入都没有使用ES6语法,所以其内部对象会暴露为公共变量,这也是为何 ReactDOM
对象能被直接调用
src/index.js
习惯作为一个入口文件,不会写复杂的逻辑,只是将 App
组件挂载、渲染到DOM上。由于 index.html
中提前全局引入了 src/App.js
,此处可以直接调用组件 <App />
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
src/App.js
习惯使用JSX语法编写应用主体,JSX支持JS与HTML混写。
const App = () => {
return (
<div>
<h1>宝可梦</h1>
</div>
);
};
该文件定义了一个名为 App
的箭头函数,返回了一个h1标签,这种形式称为函数组件。另一种形式是类组件,写法更复杂、逐渐被淘汰了,此处略过。
这时,通过本地HTTP服务器(比如Vscode提供的Live Server),一个极简的React项目就跑起来了。
state的创建与更新
我们希望从远程api获取宝可梦列表,并展示在页面中。App.js
可以这样写
const App = () => {
const [pokemons, setPokemons] = React.useState([]);
React.useEffect(() => {
fetch("https://pokeapi.co/api/v2/pokemon")
.then(res => res.json())
.then(json => {
json.results.map((result, index) => {
result.id = index + 1;
});
setPokemons(json.results);
});
}, []);
return (
<div>
<h1>宝可梦</h1>
<ul>
{ pokemons.map( pokemon => <li key={pokemon.id}>{pokemon.name}</li> ) }
</ul>
</div>
);
};
因为 fetch
函数返回 Promise
对象而不是解析后的值,对数据源的所有操作都要使用异步编程,而不能做简单的赋值操作
React是全局引入的,可以直接调用 React.useState()
。该方法返回一个状态变量、一个更新函数,并通过解构赋值分配给 pokemons
和 setPokemons
。pokemons
的初始值被设为 []
React.useEffect
方法的第一个参数指定了获取宝可梦列表的函数,第二个参数指定了触发条件。该案例第二个参数为 []
,代表仅在组件挂载后执行一次;如果不指定第二个参数,则组件的每次更新都会执行;如果给定了带值的Array,则仅当任一值改变时,才触发操作
在返回值中,我们使用JSX语法创建了 <h1>
标签和循环的无序列表 <ul>
标签。只能return单个页面元素,所以需要最外层的 <div>
。在HTML中加入JS逻辑时,需要用 {}
包裹。return的HTML片段,则必须用 ()
包裹
组件化
为了项目的可维护性,我们要尽量避免在 App.js
中写逻辑。当前返回值中,我们用JS逻辑创建了列表,这部分可被拆分为子组件 components/List.jsx
。首字母大写、jsx
后缀名是React组件的命名习惯
const List = ({pokemons}) => {
return (
<ul>
{ pokemons.map( pokemon => <li key={pokemon.id}>{pokemon.name}</li> ) }
</ul>
)
}
这也是个函数组件,需要父组件在调用时指定 pokemons
参数。参数使用解构赋值,实现类似其他语言关键字参数的效果
该组件需要在 index.html
中被全局引入,才能暴露给 App.js
...
<div id="root"></div>
<script type="text/babel" src="src/components/List.jsx"></script>
<script type="text/babel" src="src/App.js"></script>
...
在 App.js
中,以如下方式引用。props传参时需要引用JS变量 pokemons
,依据JSX的规则加了 {}
return (
<div>
<h1>宝可梦</h1>
<List pokemons={pokemons}/>
</div>
);
这种从父组件单向传递到子组件的变量,被称为props(properties),props在子组件中不可修改。这些特性都是为了代码行为可预测、提高性能
事件触发
接下来我们加入输入框,允许筛选宝可梦列表。在 components/Input.jsx
写下
const Input = ({pokemons, setFunc}) => {
const onChangeHandler = event => {
const filtered = pokemons.filter(pokemon => {
return pokemon.name.includes(event.target.value);
});
setFunc(filtered);
};
return (
<input type="search" onChange={ onChangeHandler } />
);
};
这段代码会返回一个 <input>
标签,在检测到输入变化的时候调用 onChangeHandler
函数。onChangeHandler
内部通过 event.target.value
获取输入框的值,并据此过滤列表
按照常理,我们需要将过滤后的列表回传到父组件,再传给 List.jsx
组件调用。然而,React不允许子组件修改父组件的值,只能另寻它法。恰好,此前 React.useState
方法返回的第二个值是更新函数,我们将过滤后的列表设为state,把更新函数传递给 Input.jsx
,就可以在 Input.jsx
更新全局的state了
在 App.js
中,也需要加上定义state、调用 Input.jsx
组件的逻辑。调用组件时,多个props用空格分隔
...
const [pokemons, setPokemons] = React.useState([]);
const [filteredPokemons, setFilteredPokemons] = React.useState([]);
...
<h1>宝可梦</h1>
<Input pokemons={pokemons} setFunc={setFilteredPokemons}/>
<List pokemons={filteredPokemons}/>
...
自然,index.html
中也需要全局引入 Input.jsx
<div id="root"></div>
<script type="text/babel" src="src/components/Input.jsx"></script>
<script type="text/babel" src="src/components/List.jsx"></script>
<script type="text/babel" src="src/App.js"></script>
<script type="text/babel" src="src/index.js"></script>
最终,我们的目录结构是
├── index.html
└── src
├── App.js
├── components
│ ├── Input.jsx
│ └── Lists.jsx
└── index.js
筛选功能即可实现
nodejs环境下的开发
项目结构
在上例中,我们用CDN在线引入所有必需的模块,使用传统的全局模块导入,没有用CSS设置网页样式,为的是将注意力集中在React语法本身
实际的Web开发常使用npm/yarn/pnpm等管理器,将第三方模块下载在本地,引用时采用ES6模块语法。依赖本地的nodejs环境,可以开启HTTP服务器,也免去了对Vscode插件Live Server的需要
不使用在线模块,就要求最终的网页文件内含第三方模块的代码,这个操作被跑在nodejs环境下的构建工具完成。传统的构建工具是Webpack,但Vite速度更快、效率更高,目前越来越流行
使用该命令创建Vite项目,根据提示选择React,并安装依赖项。如果你使用WSL2,pnpm可能会遇到ipv6导致的Timeout,可参考这里解决
pnpm create vite
pnpm i
这会产生一个最简项目,以下都是开发环境用到的文件,开发完成后会被Vite构建为浏览器可识别的HTML+JS+CSS(生产环境),并存放在 dist
目录(此时还未创建)
├── node_modules
├── public
├── src
│ ├── assets
│ ├── App.css
│ ├── App.jsx
│ ├── index.css
│ └── main.jsx
├── README.md
├── index.html
├── package.json
├── pnpm-lock.yaml
└── vite.config.js
src
存放主要的代码、媒体资源。assets
存放需要处理的媒体资源,如需要被压缩的图片。main.jsx
是入口文件,相当于上个案例的 index.js
。App.jsx
是应用主体,相当于上个案例的 App.js
public
存放不需要处理、直接拷贝到 dist
的文件,如矢量图标。vite.config.js
存储了Vite的配置,可以设置JSX解析、HTTP服务器端口、网络代理、路径别名、
node_modules
存放本地第三方包,而包的具体版本、依赖关系,被存储在 package.json
和 pnpm-lock.yaml
中
package.json
的 script
对象,指定了可以使用的快捷命令。为方便调试,可在 dev, preview
命令后添加 --host
参数,允许局域网内其他设备访问
"scripts": {
"dev": "vite --host",
"build": "vite build",
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview --host"
}
使用快捷命令调用 vite --host
,即可开启本地HTTP服务器
pnpm dev
此时网站处于开发模式,所有的代码修改都实时更新
Vite下的案例
在Vite下跑起上述案例,只需对代码稍作修改。index.html
不再需要CDN导入,引用 main.jsx
也使用ES6语法
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
</head>
<body>
<div id="root"></div>
<script type="module" src="src/main.jsx"></script>
</body>
</html>
main.jsx
通过 import
导入模块和组件。<React.StrictMode>
的作用是给出更多的警告信息
import React from 'react'
import ReactDOM from 'react-dom/client'
import { App } from './App.jsx'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
App.jsx
需要导出 App
,导入 Input.jsx
和 List.jsx
组件时 .jsx
可省略
import { useState, useEffect } from 'react'
import { Input } from './components/Input';
import { List } from './components/List';
export const App = () => {
const [pokemons, setPokemons] = useState([]);
const [filteredPokemons, setFilteredPokemons] = useState([]);
useEffect(() => {
fetch("https://pokeapi.co/api/v2/pokemon")
.then(res => res.json())
.then(json => {
json.results.map((result, index) => {
result.id = index + 1;
});
setPokemons(json.results);
setFilteredPokemons(json.results);
});
}, []);
return (
<div>
<h1>宝可梦</h1>
<Input pokemons={pokemons} setFunc={setFilteredPokemons}/>
<List pokemons={filteredPokemons}/>
</div>
);
};
两个子组件的代码,只需加上 export
导出即可
// src/components/Input.jsx
export const Input = ({pokemons, setFunc}) => {
const onChangeHandler = event => {
const filtered = pokemons.filter(pokemon => {
return pokemon.name.includes(event.target.value);
});
setFunc(filtered);
};
return (
<input type="search" onChange={ onChangeHandler } />
);
};
// src/components/List.jsx
export const List = ({pokemons}) => {
return (
<ul>
{ pokemons.map( pokemon => <li key={pokemon.id}>{pokemon.name}</li> ) }
</ul>
)
}
添加样式
在 Input.jsx
组件中,可以直接用内联样式。CSS设置要提供一个JS对象,以 style={cssObj}
的形式
export const Input = ({pokemons, setFunc}) => {
const onChangeHandler = event => {
const filtered = pokemons.filter(pokemon => {
return pokemon.name.includes(event.target.value);
});
setFunc(filtered);
};
const inputCSS = {
padding: '10px 15px',
margin: '10px 0',
border: '1px solid #ccc',
borderRadius: '4px',
outline: 'none',
};
return (
<input type="search" style={ inputCSS } onChange={ onChangeHandler } />
);
};
也可以将样式写入CSS文件,并作为模块导入。在 src/css/Input.css
里
.input-style {
padding: 10px 15px;
margin: 10px 0;
border: 1px solid #ccc;
border-radius: 4px;
outline: none;
}
.input-style:focus {
border-color: #66afe9;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(102, 175, 233, 0.6);
}
在 Input.jsx
中 import
导入,然后用 className
指定类名
import '../css/Input.css'
export const Input = ({pokemons, setFunc}) => {
const onChangeHandler = event => {
const filtered = pokemons.filter(pokemon => {
return pokemon.name.includes(event.target.value);
});
setFunc(filtered);
};
return (
<input type="search" className='input-style' onChange={ onChangeHandler } />
);
};
生产环境
确认开发完成后,可调用快捷命令构建最终的网页文件,并开启HTTP服务器预览
pnpm build
pnpm preview
此时 dist
目录下,已经打包好的HTML+JS+CSS便生成了,包含网站所需的全部代码
├── dist
│ ├── assets
│ │ ├── index-0xvw5vP_.js
│ │ └── index-DWT6LCHw.css
│ └── index.html
查看这些文件,会发现它们格式非常紧凑,这都是为了减少文件体积、加速网页载入
如果你只想创建一个静态网页,这时就可以把 dist
目录打包带走,架个HTTP服务器(如Nginx)向外提供服务
结语
以上,我们使用React在两种开发环境下创建了简易的Web应用,梳理了React的基本概念和用法。
如果在初始化时,你想要使用更多模版,可以在这里找找。
如果你要构建复杂的应用,React原生的state管理很可能无法胜任,可以引入额外的状态管理模块如Redux,或新秀zustand
当你需要构建单页面应用程序(SPA)以提升页面切换速度,你会需要路由管理模块如React router
如果你需要加快页面加载速率、做搜索引擎优化(Search Engine Optimizatio,SEO)时,可以考虑采用服务端渲染(Server Side Rendering,SSR)和静态站点生成(Static Site Generation,SSG)技术,Next.js是一个很好的实现方案
更多React生态的流行模块,可以在这里找到。祝玩得开心!
2024年React初学者入门路线指南 - 知乎 (zhihu.com)
React入门应该是这样的!!!_哔哩哔哩_bilibili