一、创建项目并启动
第一步:全局安装:npm install -g create-react-app
第二步:切换到想创建项目的目录,使用命令create-react-app hello-react
第三步:进入项目目录,cd hello-react
第四步:启动项目,npm start
二、目录结构
1、目录结构
其中,public/index.htm,src/App.js,src/index.js 三个是最重要的文件。
+ node_module ------ 第三方资源
+ public ------ 静态资源文件夹
+ favicon.ico ------ 网站页面图标
+ index.html ------ 主页面
+ logo192.png ------ logo 图
+ logo512.png ------ logo 图
+ manifest.json ------ 应用加壳的配置文件
+ robots.txt ------ 爬虫协议文件
+ src ------ 源码文件夹
+ App.css ------ App 组件的样式
+ App.js ------ App 组件
+ App.test.js ------ 用于给 App 组件做测试
+ index.css ------ 全局样式
+ index.js ------ 入口文件
+ logo.svg ------ logo 图
+ reportWebVitals.js ------ 页面性能分析文件(需要 web-vitals 库的支持)
+ setupTests.js ------ 组件单元测试的文件(需要 jest-dom 库的支持)
2、文件内容说明
public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<!-- 开启理想窗口,用于做移动端网页的适配 -->
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- 用于配置浏览器页签+地址栏的颜色(仅支持安卓手机浏览器,兼容性较差) -->
<meta name="theme-color" content="#000000" />
<meta name="description" content="Web site created using create-react-app"/>
<!-- 用于指定网页添加到手机主屏幕后到图标(仅支持 apple 手机) -->
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!-- 应用加壳时到配置 -->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>
src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(document.getElementById('root'));
// React.StrictMode 标签会自动校验 react 语法,遇到一些将要遗弃或不推荐使用的语法,会提示
// 不加 React.StrictMode 标签,直接使用 App 组件也没啥影响
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
reportWebVitals();
注意:旧版本的 src/index.js 中,渲染组件是通过方法ReactDOM.render(<App/>,el)
实现的:
import React from "react";
import App from "./App"
// 旧版本引入 ReactDOM ,然后 执行 ReactDOM.render()
import ReactDOM from "react-dom"
ReactDOM.render(<App />, document.getElementById("root"))
而新版本18.0.2
是通过ReactDOM.createRoot(el).render(<App/>)
实现的:
import React from "react";
import App from "./App"
// 新版本引入方式,利用 ReactDOM.createRoot() 创建节点,然后执行 render 函数
import ReactDOM from "react-dom/client"
ReactDOM.createRoot(document.getElementById("root"))
.render(
<App/>
)
三、开发注意事项
1、组件命名
组件可以以js
为后缀,也可以以 jsx 为后缀,以 jsx 为后缀可以明显区别于其他功能性的 js 文件。
2、引入 React 和 Component
1)只引 React,定义类组件时使用 React.Component
import React from "react";
// 定义类式组件
export default class Hello extends React.Component {
render() {
return (
<h1 className={hello.title}>Hello 组件</h1>
)
}
}
2)解构引入 Component,定义类组件时直接使用 Component
// React 中使用了默认暴露和分别暴露,所以可以使用下面的引入方式
// import React, { Component } from "react";
import { Component } from "react";
// 定义类式组件
export default class Welcome extends Component {
render() {
return (
<h1 className="title">Welcome组件</h1>
)
}
}
能使用以上引用方式是因为 React 中使用了 默认暴露 和 分别暴露
class React {
}
// 分别暴露 Component
export class Component {
}
React.Component = Component
// 默认暴露 React
export default React
--------------------------------------
// 其他文件引用时可以这样:
import React, { Component } from "react";
import { Component } from "react";
3、引入 ReactDOM
1)新版本18.0.2
中,要从 react-dom/client
中引入 ReactDOM
,用法如下:
import React from "react";
// 新版本引入 ReactDOM,渲染节点时使用 ReactDOM.createRoo(el).render(<App/>)
import ReactDOM from "react-dom/client"
import App from "./App";
ReactDOM.createRoot(document.getElementById("root"))
.render(<App/>)
2)旧版本中,要从 react-dom
中引入 ReactDOM
,用法如下:
import React from "react";
import ReactDOM from "react-dom"
import App from "./App";
ReactDOM.render(<App />, document.getElementById("root"))
4、css 模块化
为什么
按照截图方式在同一个组件中引用多个组件,如果 Hello 和 Welcome 组件存在相同类的不同样式时,后者会覆盖前者,所以需要模块化样式,使其互不影响。
怎么做
- 将 .css 文件改为 .module.css 文件
- 引入 css 文件时,使用
import hello from "./Hello.module.css"
代替import "./Hello.module.css"
- 组件标签中使用 hello.title,
<h1 className={hello.title}>Hello 组件</h1>
编译出来是如下效果:
5、组件通信
父子通信:直接通过组件标签的属性进行传值,子组件中通过 props 可以接受
祖孙通信:遵循状态在哪里,操作状态的方法就在哪里的原则,将所有修改 state 数据的方法都定义在 state 所在的组件中,给子组件标签添加(方法)属性funcName={funcName}
-》孙组件标签添加(方法)属性funcName={funcName}
-》孙组件内部根据需要,调用传过来的方法this.props.funcName()
6、跨域
前提:本地前端项目地址:http://localhost:3000
1)法一:配置在 packge.json 中
- 在
package.json
中配置"proxy": "http://localhost:5000"
- 组件中使用
axios.get("http://localhost:3000/students").then()
接口请求时会先在 3000 端口服务上找 /students 接口,找不到就去配置好的 5000 端口上找
说明:
1、优点:配置简单,前端请求资源时可以不加任何前缀
2、缺点:不能配置多个代理
3、工作方式:上述方式配置代理,当前请求了3000不存在的资源时,那么该请求会转发给 5000(优先匹配前端资源)
axios.get("http://192.168.31.229:3000/students").then(
(res) => {console.log("学生接口调用成功",res)},
(err) => {console.log("学生接口调用失败", err)}
)
2)法二:配置在 setupProxy.js 中
1)第一步:创建代理配置文件:在 src 下创建配置文件:src/setupProxy.js
2)编写 setupProxy.js 配置具体代理规则:
const { createProxyMiddleware } = require("http-proxy-middleware")
module.exports = function (app) {
app.use(
createProxyMiddleware("/api1", { // api1 是需要转发的请求(所有带有 /api1 前缀的请求都会转发给5000)
target: "http://localhost:5000", // 配置转发目标地址(能返回数据的服务器地址)
changeOrigin: true, // 控制服务器接收到的请求头中 host 字段的值
/*
changeOrigin 为true时,服务器收到的请求头中的 host 为 http://localhost:5000
changeOrigin 为false时,服务收到的请求头中的 host 为前端工程的服务器的host(http://localhost:3000)
changeOrigin 默认为false,但我们一般将changeOrigin的值设为true
*/
pathRewrite: {"^/api1":""} // 去除请求前缀,保证交给后台服务器的是正常请求地址(必须配置)
}),
createProxyMiddleware("/api2", {
target: "http://localhost:5001",
changeOrigin: true,
pathRewrite: {"^/api2":""}
})
)
}
使用:
axios.get("http://192.168.31.229:3000/api1/students").then(
(res) => {console.log("学生接口调用成功",res)},
(err) => {console.log("学生接口调用失败", err)}
)
说明:
1.优点:可以配置多个代理,可以灵活控制是否走代理
2.缺点:配置繁琐,前端请求资源时必须加前缀
7、组件通信
1、动态初始化列表,如何确定将数据放在哪个组件的state中?
- 某个组件使用:放在自身的state中
- 某些组件使用:放在他们共同的父组件的state中(官方称此操作为:状态提升)
2、关于父子组件通信:
- 【父组件】给【子组件】传递数据:通过props传递
- 【子组件】给【父组件】传递数据:通过props传递,要求父组件提前给子组件传递一个函数func,子组件通过this.props.func调用
3、状态在哪里,操作状态的方法就在哪里
8、消息订阅与发布(个组件间进行通信)
1)下载 pubsub-js
npm i pubsub-js
2)消息订阅
componentDidMount() {
// 消息订阅
// 消息订阅,回调里面接收两个参数,第一个是消息名,这里也就是 updateState,第二个是消息发布时携带的参数
this.token = PubSub.subscribe("updateState",(_, stateObj)=> {
this.setState(stateObj)
})
}
3)消息发布
PubSub.publish("updateState", {users: res.data.items, isLoadding: false})
4)取消订阅
componentWillUnmount() {
// 取消订阅
PubSub.unsubscribe(this.token)
}
9、路由 react-router-dom
基本使用:
- a 标签改为 Link 标签或者 NavLink
<Link className='menu-item' to="/home">home</Link>
<NavLink className='menu-item' to="/home">home</NavLink>
NavLink标签与Link相比,当前页面匹配的菜单会自动增加一个 active 类,或者可以使用 activeClassName 修改为需要的类名 - 展示区用 Route 标签进行路径的匹配
<Route path="/home" component={Home}></Route>
- 的最外侧包裹一个
<BrowserRouter>
或<HashRouter>
10、路由组件与一般组件
- 写法不同
一般组件:
路由组件: - 存放位置不同:
一般组件:components
路由组件:pages - 接收到的props不同:
一般组件:写组件标签时传递了什么,就能接收到什么
路由组件:接收到三个固定属性
- history:
go, goBack, goForward, push, replace (方法) - location:
pathname:“”, search:“”, state: “” - match:
params: {}, path: “”, url: “”
11、路由
路由渲染时,若有多个相同 path,不同 component 路由,取并集,也就是如下代码,没有 Switch 标签包裹时,/home 路由同时显示 Home 组件和 Test 组件。
Switch 包裹时,/home 路由显示第一个匹配到的组件,也就是 Home 组件。
<Switch>
<Route path="/about" component={About}></Route>
<Route path="/home" component={Home}></Route>
<Route path="/home" component={Test}></Route>
</Switch>
react 封装组件:
- 标签体内容是一个特殊的标签属性
- 子组件可以通过 this.props.children 可以获取标签体内容,可以直接通过属性方式写入标签体内容。
<!-- 这里的 to="/about" 和 内容 about 会以 props 传入到组件 MyNavLink 中,形式是:{to:"/about", children:"about"} -->
<MyNavLink to="/about">about</MyNavLink>
<MyNavLink to="/home">home</MyNavLink>
import React from "react";
import { NavLink } from "react-router-dom";
export default class MyNavLink extends React.Component {
render() {
console.log(this.props) //{to: '/about', children: 'about'}
return (
<NavLink activeClassName="active-menu" className='menu-item' {...this.props}/>
)
}
}
1)多层路由时,样式丢失
<link rel="stylesheet" href="./css/base.css">
场景:例如路由前要加前缀,为 /qiao/about,如果页面通过相对路径,引入了 public 目录中的样式 base.css,页面刷新时,base.css 样式会丢失。
原因:开发模式默认打开 http://192.168.31.229:3000/,这时候 public/css/base.css 从 public 中能正常拿到,此时样式是正常的。点击路由跳转到 http://192.168.31.229:3000/qiao/about,此时刷新页面,因为从 public 下找不到 qiao/about,会默认渲染 index.html 页面,而且样式文件因为使用相对路口,路径转化为 public/qiao/css/base.css,样式路径有误,所以样式获取不到。
解决方法:
- index.html 中引入样式使用绝对路径,将
./XXX
改为/XXX
- index.html 中引入样式使用
%PUBLIC_URL%
,将./XXX
改为%PUBLIC_URL%/XXX
- 使用 hash
HashRouter
模式代替 history 模式BrowserRouter
2)路由的严格匹配和模糊匹配
- 默认使用的是模糊匹配(简单记:输入的路径 必须要包含 匹配的路径,且顺序要一致)
- 开启严格匹配
- 严格匹配不要随便开启,需要时再开,有些时候开启会导致无法继续匹配二级路由
<MyNavLink to="/home/a/b">home</MyNavLink>
<Route exact path="/home" component={Home}></Route>
3)Redirect
- 一般写在所有路由注册的最下方,当所有路由都无法匹配是,跳转到 Redirect 指定的路由。
<Switch>
<Route path="/about" component={About}></Route>
<Route path="/home" component={Home}></Route>
<Redirect to="/about"/>
</Switch>
4)嵌套路由
- 注册子路由时要写上父路由的 path 的值(/home/news)
- 路由的匹配是按照注册路由的顺序进行的 (先匹配home路由的组件,在匹配news路由的组件)
5)路由传参
- params 参数
路由链接(携带参数):<Link to={
/home/message/detail/tom/18}>详情</Link>
注册路由(声明接收):<Route path="/home/message/detail/:name/:age" component={Detail}/>
接收参数:this.props.match.params
- search 参数
路由链接(携带参数):<Link to={
/home/message/detail?name=tom&age=18}>详情</Link>
注册路由(无需声明,正常注册即可):<Route path="/home/message/detail" component={Detail}/>
接收参数:this.props.location.search
备注:获取到的 search 是 urlencoded 编码字符串,需要借助 qs 解析 - state 参数
路由链接(携带参数):<Link to={{pathname: "/home/message/detail", state: {name: "tom", age: 18}}}>{item.title}</Link>
注册路由(无需声明,正常注册即可):<Route path="/home/message/detail" component={Detail}/>
接收参数:this.props.location.state
备注:刷新也可以保留参数,但是清除缓存,history所有数据就会被清空,刷新无法获取参数
6)push和replace
Link 标签默认是 push 默认,要打开 replace 模式,需要在标签中增加属性 replace={true},或者简写 replace
<Link replace={true} to={{pathname: "/home/message/detail", state: {id: item.id, title: item.title}}}>{item.title}</Link>
<Link replace to={{pathname: "/home/message/detail", state: {id: item.id, title: item.title}}}>{item.title}</Link>
<!-- 默认是push、 -->
<Link to={{pathname: "/home/message/detail", state: {id: item.id, title: item.title}}}>{item.title}</Link>