React 学习总结

本贴最后更新于 2074 天前,其中的信息可能已经东海扬尘

第 1 章 课程导学

学习流程
相关的知识点
学习前提
要有一些 js、es6、webpack、npm 等基础知识

第 2 章 React 初探

2-1 React 简介

React
FaceBook 推出
2013 年开源
函数式编程
使用人数最多的前端框架之一
健全的文档与完善的社区

2-2 React 开发环境准备

1)、确定自己电脑已经安装了 node 环境
根据自己电脑来安装属于自己电脑版本的 node,推荐使用稳定版
node 官网下载地址
2)、安装 react 脚手架 create-react-app

npx create-react-app my-app cd my-app npm start
2-3 工程目录文件简介

因为通过脚手架下载的程序,有很多文件是需要我们剔除出去的。
例如没用的样式文件、测试文件、pwa 相关的一些文件等等

  • 项目结构
  • ├── node_modules # 第三方的依赖库文件
  • ├── public
  • │ ├── favicon.ico # Favicon
  • │ ├── index.html # 首页
  • ├── src
  • │ ├── app.js # 业务代码文件
  • │ ├── index.js # 应用入口
  • ├── README.md
  • └── .gitignore #git 忽略提交的文件配置
  • └── package.json #启动、编译命令设置;依赖包管理配置信息
  • └── yarn.lock #yarn 依赖包管理配置信息
2-4 React 中的组件

下面就是定义了一个 App 的组件

//App.js import React, {Component} from 'react' class App extends Component { render() { //JSX 语法 return ( <div>hello world!</div> ) } } export default App

App 组件引用和调用

//index.js import React from 'react' import ReactDOM from 'react-dom' import App from 'App.js' ReactDOM.render(<App />,document.getElementById('root'))

JSX 语法在使用的时候,组件的定义和使用的时候要大写
JSX 语法中,我们要求一个组件 render 函数返回的内容,外层必须要有一个大的元素包裹(div 或者 Fragment)
Fragment 渲染到页面的时候是不占位的
eg:

//index.js import App from 'App.js' //大写 ReactDOM.render(<App />,document.getElementById('root'))

第 3 章 React 基础精讲

3-1 使用 React 编写 TodoList 功能
//TodoItem.js import React, {Component} from 'react'; import PropTypes from 'prop-types'; //组件类型进行校验 class TodoItem extends Component { constructor(props) { super(props); this.handleDelete = this.handleDelete.bind(this) } render() { const {content, test} = this.props; return ( <div onClick={this.handleDelete}>{test}-{content}</div> ) } handleDelete() { const {index, deleteItem} = this.props; deleteItem(index) } } TodoItem.propTypes = { test: PropTypes.string.isRequired, //父组件没有给子组件传递test 但是又是必填项,所以可以通过defaultProps给一个默认值 content: PropTypes.string, deleteItem: PropTypes.func, index: PropTypes.number }; TodoItem.defaultProps = { test: 'hello' }; export default TodoItem //TodoList.js import React, {Component, Fragment} from 'react'; import TodoItem from './TodoItem'; class TodoList extends Component { constructor(props) { super(props); this.state = { inputValue: '', list: [] }; this.handleInputChange = this.handleInputChange.bind(this); this.handleAdd = this.handleAdd.bind(this); this.handleDelete = this.handleDelete.bind(this); } render() { return ( <Fragment> <div> <label htmlFor="inputArea">输入内容:</label> <input id="inputArea" type="text" value={this.state.inputValue} onChange={this.handleInputChange} /> <button onClick={this.handleAdd}>提交</button> </div> <ul> {this.getTodoItem()} </ul> </Fragment> ) } getTodoItem() { return this.state.list.map((item, index) => { return ( <TodoItem content={item} key={index} index={index} deleteItem={this.handleDelete} /> ) }) } handleInputChange = (e) => { const value = e.target.value; this.setState(() => { return { inputValue: value } }) }; handleAdd = () => { this.setState((prevState) => { return { list: [...prevState.list, prevState.inputValue], inputValue: '' } }); }; handleDelete = (index) => { this.setState((prevState) => { const list = [...prevState.list]; list.splice(index, 1); return {list} }); } } export default TodoList;
3-2 JSX 语法细节补充

1.注释:必须要用花括号括起来
单行注释

{ //这里是单行注释 }

多行注释

{/*这是多行注释*/}

2.样式名称引入:将 class 改为 className

因为 class 这个和定义组件“类”有冲突

<input className="inputClass" />

3.表单 label for 属性的引用,要将 for 改为 htmlFor
防止 for 和 js 中循环 for 引起冲突

<label htmlFor="inputArea">用户名</label> <input id="inputArea" />

4.将 input 标签内容直接进行转义呈现在页面 dangerouslySetInnerHTML

<li key={index} onClick={this.handleDeleteItem.bind(this, index)} dangerouslySetInnerHTML={{__html: item}} > </li>
3-3 组件拆分与组件之间的传值

1.组件拆分
当一个页面很大的时候,那么我们就会对这个页面进行组件的拆分,拆分的结构就像一棵树一样
组件之间的结构

2.组件之间的传值
父组件向子组件传值是通过“子组件的属性”
子组件接受父组件传过来的值:this.props.属性名称
子组件向父组件传值是通过 子组件调用父组件的方法,从来改变父组件的数据,但是要对这个方法调用的时候,父组件要对这个方法进行 this 的绑定

3-4 TodoList 代码优化
//解构优化 this.props.content ,this.props.index 改为 const {content,index} = this.props 调用的时候,直接使用content,index即可 //this的绑定优化 都可以写在constructor里面 onChange={this.handleChange.bind(this)} constructor(props){ super(props); .... .... this.handleChange = this.handleChange.bind(this) //优化地方 } 调用的时候直接写: this.handleChange //Ui优化 当一段的UI太过于庞大的是时候,可以把这段UI放到一个函数方法里面,return 返回出去,同时在使用的地方调用一下即可 //setState键值对优化为异步函数 this.setState({ inputValue: e.target.value }) 改为 this.setState(()=>{ const value = e.target.value //在异步里面必须要提出来,如果直接须赋值会报错的 return { inputValue:value //如果将value 改为e.target.value 会报错 } }) //setState中this.state 改为 参数prevState handleAdd = () => { this.setState((prevState) => { return { list: [...prevState.list, prevState.inputValue], inputValue: '' } }); }; //if else判断改变为 switch case也可以提高性能
3-5 围绕 React 衍生出的思考

声明式开发
可以与以其他框架并存
组件化
单项数据流(父组件可以向子组件传值,但是子组件一定不能直接去改变这个值)
视图层框架 (负责视图渲染,但是针对一些大数据和复杂数据的时候,需要交由 redux 等数据框架来处理)
函数式编程 (便于自动化测试)

第 4 章 React 高级内容

4-1 React developer tools 安装及使用

打开 chrome 浏览器,并且打开 chrome 应用商店,搜索 React Developer Tools 添加即可
React Developer Tools 地址
百度网盘下载地址

4-2 PropTypes 与 DefaultProps 的应用

创建好的组件,最好给组件定义属性类型(string、number、boolean 等等) 和创建默认值。就拿上面 todoList 这个例子来说吧。
PropTypes
父组件(TodoList)向子组件(TodoItem)传递了 content(字符串)、index(数字)、deleteItem(方法),那么如果我们在子组件中声明了这些类型,就可以避免一些不别要的麻烦,如果父组件传递过来的属性值不是我们想要的,那么我就可以告诉浏览器我需要什么类型的值
DefaultProps
可以当父组件没有给子组件传递任何值的时候,通过 DefaultProps 给子组件初始化一些数据(即默认值)
详细代码见 3-1 使用 React 编写 TodoList 功能
了解更多 PropTypes 知识链接地址

4-3 props,state 与 render 函数的关系

1.当组件的 state 或者 props 发生改变时,render 函数就会重新执行
2.当父组件的 render 函数被重新执行时,它的子组件的 render 函数都将被重新执行
eg:
在 render 函数的后面进行打印
打印父组件 console.log('parent-render')
打印子组件 console.log('child1-render')
更改父组件的 state 值
你会发现 console 里面 parent-render 和 child1-render 都被打印出来了

4-4 React 中的虚拟 DOM

1.state 数据
2.JSX 模板
3.数据 + 模板 结合,生成真实 DOM,来显示
4.state 发生改变
5.数据 + 模板 结合,生成真实 DOM,替换原始 DOM

缺陷:
第一次生成一个完整的 DOM 片段
第二次生成一个完整的 DOM 片段
第二次的 DOM 替换第一次的 DOM,非常消耗性能

解决??
1.state 数据
2.JSX 模板
3.数据 + 模板 结合,生成真实 DOM,来显示
4.state 发生改变
5.数据 + 模板 结合,生成真实 DOM,并不直接替换原始 DOM
6.新的 DOM(documentFragment)和原始的 DOM,做对比,找差异
7.找出 input 框发生改变的部分
8.只用新的 DOM 中的 input 元素,替换老的 DOM 中 input 的元素

缺陷:
性能的提升并不明显

接着解决:
1.state 数据
2.JSX 模板
3.生成虚拟 DOM(其实就是一个 js 对象,用它来描述真实 DOM)(损耗了性能但是极小)

['div',{id:'abc'},['span',{},'hello world']]

4.用虚拟 DOM 的结构生成真实 DOM,来显示

<div id="abc"><span>hello world</span></div>

5.state 发生改变
6.数据 + 模板 结合,生成新的虚拟 DOM (极大的提升了性能)

['div',{id:'abc'},['span',{},'bye bye']]

7.比较原始虚拟 DOM 和生成新的虚拟 DOM,找到区别是 span 的内容(极大的提升了性能)
8.直接操作 DOM,改变 span 的内容

总结如下:
虚拟 DOM 其实就是 js 对象,那为什么采用虚拟 dom 会提高性能呢?因为他们比对的是 js 对象,而不是真的 DOM,从而极大的提升了性能

4-5 深入了解虚拟 DOM
JSX(模板) => 虚拟DOM(js对象) =>真实的DOM render() { return <div>item</div> } 等价于 render() { return React.createElement('div',{},'item') }

虚拟 DOM 有点:
1.性能提升了
2.它是的跨度应用得以实现 React Native
因为数据 + 模板生成的是虚拟 DOM,
1)、如果是在网页中,那么再有虚拟 DOM(JS 对象)生成真实的 DOM,浏览器可以得以识别,所以网页中可以得到应用
2)、如果在 App 中,那么再用虚拟 DOM(JS 对象)不去生成真实的 DOM,而去生成原生的组件,那么在 App 中也就可以得到应用

4-6 虚拟 DOM 中的 Diff 算法

虚拟 DOM 什么时候回发生比对呢?
答:那就是当 state 值发生改变的时候,虚拟 DOM 才会开始进行比对

为什么 setState 设计成为异步的?
答:我们知道,当 state 发生改变或者 props(即父组件中的 state 发生改变)时,也就是当我们调用 setState 方法的时候,state 值发生改变,虚拟 DOM 开始进行比较。
那么如果连续 3 次调用 setState 方法的时候,变更 3 组数据(我们此时会想 react 会进行三次虚拟 DOM 比对,三次渲染页面),其实 react 会把短时间内连续调用 setState 方法合并为一个 setState,只去做一次虚拟 DOM 的比对,然后更新一次 DOM,这样就可以省去额外两次 DOM 比对带来的性能消耗,所以把 setState 设计成为异步的

react 中虚拟 DOM 比对采用的是 diff 算法:其实同层比对,key 值比对
同层比对

key 值比对

所以列表循环中不要用 index 作为 key 值的,在 diff 算法进行比较时候,会导致 key 变更,而产生一些性能上的问题
因为 index(索引值有时候会变更)会导致 key 值不稳定,
eg:
a 0 b 1 c 2
当删除 a
b 0 c 1
使用一个稳定的值作为 key 才是我们首先要考虑的
eg:
a a b b c c
当输出 a
b b c c

4-7 React 中 ref 的使用

ref 是帮助我们 react 直接获得 dom 元素,一般情况下我们尽量不要使用 ref
eg:

<input id="inputArea" type="text" value={this.state.inputValue} onChange={this.handleInputChange} ref={(input) => this.input = input} //ref 使用 /> handleInputChange = (e) => { //const value = e.target.value; const value = this.input.value; //通过ref 来获取input的value值 this.setState(() => { return { inputValue: value } }) };

tip:
当我们在试用 ref 获取 dom 元素的时候,有时候会出现数据不对,或者少一步操作现象,那么因为 setState 是异步函数,解决这个问题就是把 ref 相关操作的内容放到 setState 回调函数中进行操作

<ul ref={(ul)=>this.ul = ul}> <div>hello</div> </ul> this.setState(() => { return { inputValue: value } },() => { //回调成功后,在这进行ref相关操作 this.ul.querySelectorAll('div').length })

在 React v16.3 版本中引入的 React.createRef() 方法

//使用方法 import React, {Component} from 'react'; class Products extends Component { constructor(props) { super(props); this.myRef = React.createRef(); //这个地方 } handleChangeInputValue = () => { console.log(this.myRef.current.value) //取值的时候借助current属性 }; render() { return ( <div> <input type="text" ref={this.myRef} onChange={this.handleChangeInputValue}/> </div> ) } } export default Products

Ref 的适合使用的几个场景:

  • 处理焦点、文本选择或媒体控制。
this.textInput.current.focus();
  • 触发强制动画。
  • 集成第三方 DOM 库
4-8 React 的生命周期函数

生命周期函数:
某一时刻自动调用执行的函数。

//虽然不是react的生命周期函数,但是有这个功能,初始化数据就放在constructor里面进行操作的 1.初始化 constructor(props){ super(props); } 2.挂载的生命周期函数(Mounting) //当组件即将要被挂载到页面的时候,会被执行 componentWillMount(){} //render render(){} //当组件被挂载到页面后,会被执行 componentDidMount(){} 3.数据发生改变更新的生命周期函数(Updating) //当组件被更新之前,会被执行 shouldComponentUpdate(nextProps,nextState){ const {list} = this.props; if(nextProps.list !== list){ return true }else{ return false } } //当组件被更新之前,会被执行,但是在shouldComponentUpdate后面 //如果shouldComponentUpdate 返回true 它才执行 //如果返回 false 它不会被执行 componentWillUpdate(){} //当组件被更新之后,会被执行 componentDidUpdate(){} //一个组件要从父组件接受参数 //如果这个组件第一次存在于父组件中,不会被执行 //如果这个组件之前已经存在父组件中,才会被执行 componentWillReceiveProps(nextProps){} 4.组件从页面卸载的生命周期函数 //当这个组件将要被从页面剔除出去的时候,会被执行 componentWillUnmount(){}

react 生命周期函数

4-9 React 生命周期函数的使用场景

1.生命周期使用场景

//针对组件的优化的时候,可以使用shouldComponentUpdate,当组件当中的属性值发生改变的是去做更新,没有改变则不去更新,减少组件被重复多次渲染的频率,减少性能的消耗 shouldComponentUpdate(nextProps, nextState) { const {content} = this.props; if (nextProps.content !== content) { return true } else { return false } } //ajax接口请求 放在componentDidMount()里面 import axios from 'axios' componentDidMount(){ axios.get('api') .then((res)=>{ console.log('success') }) .catch(()=>{ console.log('error') }) }

2.性能优化
1)、shouldComponentUpdate
2)、setState 采用的是异步回调的方式,提高了性能
3)、虚拟 DOM 中的同层比对和 key 值比对,也极大的提高了性能

4-10 使用 Charles 实现本地数据 mock

charles 下载安装

//桌面创建一个todoList.json文件 ["react","vue","angular"] //设置charles 打开 charles ---> 菜单栏tools ---> Map Local Setting //请求接口 componentDidMount() { axios.get('/api/todoList') .then((res) => { this.setState(() => { return { list: [...res.data] } }) }) .catch(() => { console.log('error') }) }

Map Local Setting 设置

4-11 React 中实现 CSS 过渡动画

css 过度动画主要还是 css3 知识点的理解和认识,比如:transition(all 1s ease-in)、animation(show-item 1s ease-in forwards)、@keyframes 等等
还就就是借助第三方库 react-transition-group 等等

//state constructor(props) { super(props); this.state = { show: true }; this.handleToggle = this.handleToggle.bind(this); } //JSX <h3>动画部分</h3> <div className={this.state.show ? 'show' : 'hide'}>hello</div> <button onClick={this.handleToggle}>toggle</button> //绑定事件 handleToggle() { this.setState((prevState) => { return { show: prevState.show === true ? false : true } }) } //style .show { /*opacity: 1;*/ /*transition: all 1s ease-in;*/ animation: show-item 2s ease-in forwards; } .hide { /*opacity: 0;*/ /*transition: all 1s ease-in;*/ animation: hide-item 2s ease-in forwards; } @keyframes show-item { 0% { opacity: 0; color: red; } 50% { opacity: 0.5; color: green; } 100% { opacity: 1; color: blue; } } @keyframes hide-item { 0% { opacity: 1; color: red; } 50% { opacity: 0.5; color: green; } 100% { opacity: 0; color: blue; } }
4-12 使用 react-transition-group 实现动画
import { CSSTransition } from 'react-transition-group'; <CSSTransition in={this.state.show} timeout={1000} classNames="fade" unmountOnExit onEntered={(el)=>{el.style.color='blue'}} appear={true} > <div>hello</div> </CSSTransition> //css /*CSSTransition*/ .fade-enter,fade-appear{ opacity: 0; } .fade-enter-active,fade-appear-active{ opacity: 1; transition: opacity 1s ease-in; } .fade-enter-done{ opacity: 1; } .fade-exit{ opacity: 1; } .fade-exit-active{ opacity: 1; transition: opacity 1s ease-in; } .fade-exit-done{ opacity: 0; }

当我们想一个数组都拥有动画的时候,我们会用 react-transition-group 里面的 TransitionGroup
了解更多链接地址

第 5 章 Redux 入门

5-1 Redux 概念简述

react 是一个视图层框架
那么,redux 就是一个数据层框架
Redux = Reducer + Flux
image.png

5-2 Redux 的工作流程

Redux 的工作流程

React Component:借书者
Action Creators: 我要借本三国演义这句话
Store: 图书馆管理员
Reducer:记录本

React Component:我想要借本三国演义
Action Creators:那我给你写封信(dispatch 发号了一个 action 指令),我把这个指令告诉了 Store
Store:我知道了,但是我不知道这本书放到哪了,我帮你问问 Reducers
Reducer:我给你查查,查到了,在这呢。带着三国演义这本书(newState)给了 Store,Store 记录下了借书的内容信息,并这本书最终给了 React Components

5-3 使用 Antd 实现 TodoList 页面布局
//1.安装antd并且引入antd样式文件 npm install antd -S //2.todoList.js import React, {Component} from 'react'; import {Input, Button, List} from 'antd'; import 'antd/dist/antd.css'; import store from './store' class TodoList extends Component { constructor(props) { super(props); this.handleInputValue = this.handleInputValue.bind(this); this.state = store.getState(); } render() { return ( <div style={{marginTop: '10px', marginLeft: '10px'}}> <Input value={this.state.inputValue} placeholder="todo info" style={{width: '300px', marginRight: '10px'}} /> <Button type="primary" >提交</Button> <List style={{marginTop: '10px', width: '300px'}} bordered dataSource={this.state.list} renderItem={item => (<List.Item>{item}</List.Item>)} ></List> </div> ) } handleInputValue(e) { const action = { type: 'change_input_value', value: e.target.value }; store.dispatch(action) } } export default TodoList;
5-4 创建 redux 中的 store
//在src目录下创建store目录,并在store目录下创建index.js(图书馆管理员)和reducer.js(记录本) //index.js import {createStore} from 'redux'; import reducer from './reducer'; const store = createStore(reducer); export default store //reducer.js const defaultState = { inputValue: '123', list: [1, 2] }; export default (state = defaultState, action) => { return state } //TodoList.js import store from './store' constructor(props) { super(props); this.state = store.getState(); console.log(store.getState()) //打印出来的数据是{inputValue: "123",Array(2)} }

创建图书馆 store 并打通 store 和 reducer 之间的桥梁,
然后 react component 通过 store.getState()方法拿到 reducer 里面的数据了

5-5 Action 和 Reducer 的编写

1.Redux DevTools 安装和配置
去 chrome 应用商店 安装 Redux DevTools 这个浏览器插件
Redux DevTools 下载链接地址
安装完了以后,发现用 chrome 浏览器打开控制台,点击 redux 标签,并没有显示任何内容,那是因为需要我们在 store 文件中写一段代码。
配置相关信息的地址,打开 github,搜索 redux-devtools-extension,就能查看相关的配置信息了

import {createStore} from 'redux'; import reducer from './reducer'; const store = createStore( reducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() //这段代码,为了方便浏览器能捕获到redux数据 ); export default store

2.redux 的工作流程原理演示(todoList 这个例子来说)
根据上面的 redux 工作流,咱们来说。
如果我们想要更改 React Component(input 里面的值)
那么我们需要创建一个 Action creators,并且定义一个 action 对象,通过 store.dispatch 方法传递给 store

handleInputValue(e) { //定义一个action const action = { type: 'change_input_value', value: e.target.value }; // 通过store.dispatch方法,把action传递给store store.dispatch(action) }

当 store 接受到 Action creators 发送过来的 action 的时候,它说我需要我的秘书 reducer 帮我查询一下,并且帮我处理一下。
于是乎 reducer 就就收了两个参数,第一个是 state(定义初始化的旧数据),第二个就是传递过来的 action(一个 type 名称,一个是传递过来新的 inputValue 值),
reducer 说:我不能直接去更改 state 里面的值,我需要把 state 值通过 JSON 的 parse 和 stringify 进行数据深层拷贝生成 newState。那么在对这个 newState 进行数据的处理,最后把处理好的数据再 return 回去
store 拿到 reducer 处理好的新数据后,
再通过自己的 store.getState()方法,去拿到 reducer 的最新数据
再通过自己的 store.subscribe()方法,去监测 store 里面的数据变化,
最后通过 this.setState()方法,把最新的数据渲染到页面上去

通过第一方法专门是获得最新数据的 store.subscribe(this.handleStore);

//reducer.js const defaultState = { inputValue: '123', list: [1, 2] }; export default (state = defaultState, action) => { console.log(state, action); if (action.type === 'change_input_value') { const newState = JSON.parse(JSON.stringify(state)); newState.inputValue = action.value; return newState } if (action.type === 'add_list_item') { const newState = JSON.parse(JSON.stringify(state)); newState.list.push(newState.inputValue); newState.inputValue = ''; return newState } return state } //TodoList.js import React, {Component} from 'react'; import {Input, Button, List} from 'antd'; import 'antd/dist/antd.css'; import store from './store' class TodoList extends Component { constructor(props) { super(props); this.handleInputValue = this.handleInputValue.bind(this); this.handleAdd = this.handleAdd.bind(this); this.handleStore = this.handleStore.bind(this); this.state = store.getState(); store.subscribe(this.handleStore);//检测handleStore方面里面数据的变化 } render() { return ( <div style={{marginTop: '10px', marginLeft: '10px'}}> <Input value={this.state.inputValue} placeholder="todo info" style={{width: '300px', marginRight: '10px'}} onChange={this.handleInputValue} /> <Button type="primary" onClick={this.handleAdd}>提交</Button> <List style={{marginTop: '10px', width: '300px'}} bordered dataSource={this.state.list} renderItem={item => (<List.Item>{item}</List.Item>)} ></List> </div> ) } handleInputValue(e) { //定义一个action 把这个type:干什么事情的名字传递过去,并且发生更改的信息传递过去 const action = { type: 'change_input_value', value: e.target.value }; store.dispatch(action) } handleAdd() { const action = { type: 'add_list_item' }; store.dispatch(action) } handleStore() { //把变更后新的数据,重新放入到state中,然后去渲染页面 this.setState(store.getState()); } } export default TodoList;
5-6 ActionTypes 的拆分

为什么要把 action 里面的 type 拆分到一个文件里面呢?
第一当我们把 type 值拼写错误的时候,不好找错
第二我们需要调用相同的内容,写两次
所以我们在 store 文件下面创建了一个 actionType.js

5-7 使用 actionCreator 统一创建 action

为什么要把把组件中的 action 通过方法的形式拆分出去呢?
第一为了方便 action 的统一管理
第二为了减少 React Component 代码的繁琐

//在store文件下创建actionCreators.js import {CHANGE_INPUT_VALUE, ADD_LIST_ITEM, DELETE_LIST_ITEM} from './actionTypes'; export const getChangeInputValue = (value) => ({ type: CHANGE_INPUT_VALUE, value }); export const getAddListItem = () => ({ type: ADD_LIST_ITEM }); export const getDeleteListItem = (index) => ({ type: DELETE_LIST_ITEM, index }); //TodoList.js import {getChangeInputValue, getAddListItem, getDeleteListItem} from './store/actionCreators'; handleInputValue(e) { const action = getChangeInputValue(e.target.value); store.dispatch(action) } handleAdd() { const action = getAddListItem(); store.dispatch(action) } handleDeleteItem(index) { const action = getDeleteListItem(index); store.dispatch(action); }
5-8 Redux 知识点复习补充

1.Redux 在设计和使用的三个原则
1).store 是唯一的
2).只有 store 能改变自己的内容
3).reducer 必须是个纯函数(给个固定的输入,就一定会有固定的输出,且不会有任何副作用)
所以里面不能有异步操作(ajax),不能有时间的操作 new Date()
2.redux 中核心的 API
1).createStore 帮助我们创建一个 store
2).store.dispatch() 帮助我们派发一个 action
3).store.getState() 帮助我们获得 store 当中所有的数据
1).store.subscribe() 帮助我们订阅(监测)store 当中数据的改变

第 6 章 Redux 进阶

6-1 UI 组件和容器组件

1.UI 组件
UI 组件负责页面的渲染
eg:

import React, {Component} from 'react' import {Button, Input,List} from "antd"; class TodoListUi extends Component{ render(){ return ( <div style={{marginTop: '10px', marginLeft: '10px'}}> <Input value={this.props.inputValue} placeholder="todo info" style={{width: '300px', marginRight: '10px'}} onChange={this.props.handleInputValue} /> <Button type="primary" onClick={this.props.handleAdd}>提交</Button> <List style={{marginTop: '10px', width: '300px'}} bordered dataSource={this.props.list} renderItem={(item, index) => ( <List.Item onClick={() => this.props.handleDeleteItem(index)}>{item}</List.Item>)} ></List> </div> ) } } export default TodoListUi;

2.容器组件
它不管你的 UI 长成什么样子,它只负责页面的业务逻辑

import React, {Component} from 'react'; import TodoListUi from './TodoListUi'; import 'antd/dist/antd.css'; import store from './store' import {getChangeInputValue, getAddListItem, getDeleteListItem} from './store/actionCreators'; class TodoList extends Component { constructor(props) { super(props); this.handleInputValue = this.handleInputValue.bind(this); this.handleAdd = this.handleAdd.bind(this); this.handleDeleteItem = this.handleDeleteItem.bind(this); this.handleStore = this.handleStore.bind(this); this.state = store.getState(); store.subscribe(this.handleStore); } render() { return ( <TodoListUi inputValue={this.state.inputValue} list={this.state.list} handleInputValue={this.handleInputValue} handleAdd={this.handleAdd} handleDeleteItem={this.handleDeleteItem} /> ) } handleInputValue(e) { const action = getChangeInputValue(e.target.value); store.dispatch(action) } handleAdd() { const action = getAddListItem(); store.dispatch(action) } handleDeleteItem(index) { const action = getDeleteListItem(index); store.dispatch(action); } handleStore() { this.setState(store.getState()); } } export default TodoList;
6-2 无状态组件

当一个组件只有 render 函数的时候,那么就可以把这个组件改写为无状态组件
优势:性能比较高
无状态组件是一个函数
而一般组件是声明的一个类,这个类里面还有一些生命周期函数,所以执行起来不仅需=要执行类还要执行 render
那么什么时候去用无状态组件呢?
当我们定义 UI 组件的时候,因为没有业务逻辑,只有一个 render,所以一般在情况下,在 UI 组件中我们使用无状态组件比较多一些

//无状态组件 import React from 'react' import {Button, Input, List} from "antd"; const TodoListUi = (props) => { return ( <div style={{marginTop: '10px', marginLeft: '10px'}}> <Input value={props.inputValue} placeholder="todo info" style={{width: '300px', marginRight: '10px'}} onChange={props.handleInputValue} /> <Button type="primary" onClick={props.handleAdd}>提交</Button> <List style={{marginTop: '10px', width: '300px'}} bordered dataSource={props.list} renderItem={(item, index) => ( <List.Item onClick={() => props.handleDeleteItem(index)}>{item}</List.Item>)} ></List> </div> ) }; export default TodoListUi;
6-3 Redux 中发送异步请求获取数据
//在actionTypes.js中创建一个变量 export const INIT_LIST_DATA = 'init_list_data'; //在actionCreators.js中创建一个action export const getTodoListData = (data) => ({ type: INIT_LIST_DATA, data }); /TodoList.js import axios from 'axios'; import {getTodoListData} from './store/actionCreators'; componentDidMount() { axios.get('/api/todoList') .then((res) => { const data = res.data; const action = getTodoListData(data); store.dispatch(action) }) } //reducer.js import {INIT_LIST_DATA} from './actionTypes.js' const defaultState = { inputValue:'', list:[] } export default (state = defaultState, action) =>{ if(action.type === INIT_LIST_DATA){ const newState = JSON.parse(JSON.stringify(state)) newState.list = action.data return newState } return state }
6-4 使用 Redux-thunk 中间件实现 ajax 数据请求

1.Redux-thunk 安装 以及 redux-Devtools 的配置

//安装 npm install redux-thunk -S //redux-Devtools的配置 store文件下的index.js import {createStore, applyMiddleware, compose} from 'redux'; import reducer from './reducer'; import thunk from 'redux-thunk'; const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose; const enhancer = composeEnhancers( applyMiddleware(thunk), ); const store = createStore(reducer, enhancer); export default store

配置参考地址链接:redux-devtools-extension

2.redux-thunk 在程序中的应用
为什么使用 redux-thunk 这个 redux 中间件?
第一:可以把数据操作和数据请求操作从 React Component 中搬到 ActionCreators.js 里面,不会是的组件显得那么拥堵
第二:便于后期的单元测试

//actionCreators.js中的修改 import axios from 'axios'; export const getTodoListDataAction = (data) => ({ type: INIT_LIST_DATA, data }); export const getListData = () => { //redux-thunk 返回action是一个函数的时候,且放到了action里面进行操作的 return (dispatch) => { axios.get('/api/todoList') .then((res) => { const data = res.data; const action = getTodoListDataAction(data); dispatch(action) }) } }; //TodoList.js中的修改 import { getListData } from './store/actionCreators'; componentDidMount() { const action = getListData(); store.dispatch(action); }
6-5 什么是 Redux 的中间件

redux 数据工作流

redux-thunk 其实是对 store.dispatch(action)方法的一个封装和升级,是把异步请求的操作放到了 action 当中进行操作。
在没有使用 redux-thunk 的时候,定义的 action 是一个对象
使用 redux-thunk 之后,定义的 action 不仅可以是对象,而且还可以可以是一个函数
######其他 redux 中间件:
redux-logger:可以记录 action 每次派发的日志
redux-saga:也是解决 react 中异步的一个中间件,单独的把异步的操作放到一个文件中进行操作

6-8 Redux-saga 中间件入门

1.redux-sage 的安装和配置

//安装 npm install redux-saga -S //配置是在 store文件下面的index.js中 import {createStore, applyMiddleware, compose} from 'redux'; import reducer from './reducer'; // import thunk from 'redux-thunk'; import createSagaMiddleware from 'redux-saga'; import todoSaga from './sagas' const sagaMiddleware = createSagaMiddleware(); const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose; const enhancer = composeEnhancers( applyMiddleware(sagaMiddleware), ); const store = createStore(reducer, enhancer); sagaMiddleware.run(todoSaga); export default store //sagas.js

配置参考地址链接:redux-saga

2.redux-saga 在程序中的应用

//TodoList.js componentDidMount() { const action = getTodoListData(); store.dispatch(action); } //actionTypes.js export const INIT_LIST_DATA = 'init_list_data'; //actionCreators.js import {INIT_LIST_DATA} from './actionTypes'; export const getTodoListData = (data) => ({ type: INIT_LIST_DATA, data }); //reducer.js import {INIT_LIST_DATA} from './actionTypes'; const defaultState = { inputValue: '', list: [] }; export default (state = defaultState, action) => { // console.log(state, action); if (action.type === INIT_LIST_DATA) { const newState = JSON.parse(JSON.stringify(state)); newState.list = action.data; return newState } return state } //sagas.js import {takeEvery, put} from 'redux-saga/effects'; import {INIT_LIST_DATA} from './actionTypes'; import {getTodoListData} from './actionCreators'; import axios from 'axios'; function* getListData() { try { const res = yield axios.get('/api/todoList'); const action = getTodoListData(res.data); yield put(action) } catch(e) { console.log('todoList 网络异常') } } function* todoSaga() { yield takeEvery(INIT_LIST_DATA, getListData); } export default todoSaga;

总结:
1).ajax 请求
不采用 Promise 那种形式了(.then),而是通过 yield 来等待返回的结果
2).接受或者监听 Action 的
通过的 takeEvery,检测到变量名称,触发一个 generator 函数
3).派发请求
不采用 store.dispatch(), 而是通过的是 put()
4).出了 takeEvery、put 方法还有(takeLatest、call 等等多种 API)

6-9 如何使用 React-redux

react-redux 核心 API 有哪些?
1).Provider:就是一个连接器的组件,因为 Provider 和 store 做了关联,所以 Provider 这些内部的组件都可以获取到 store 里面的数据内容了

//安装react-redux npm install react-redux -S //使用 在src文件下面的index.js文件进行编写 import React from 'react'; import ReactDOM from 'react-dom'; import { Provider } from 'react-redux' import TodoList from './TodoList' import store from './store' const App = ( <Provider store={store}> <TodoList /> </Provider> ) ReactDOM.render(App,document.getElementById('root'))

2).connect: 是 React Component 调用 react-redux 的 connect 方法,使得组件和 store 关联起来,并且能对 state 进行设置和修改

import React,{ Component } from 'react' import {connect} from 'react-redux; class TodoList extends Component { render() { return ( <div> <div> <input value={this.props.inputValue} onChange={this.props.handleInputChange} /> <button>提交</button> </div> <div> <ul><li>hello</li></ul> </div> </div> ) } } const mapStateToProps = (state) => { return { inputValue: state.inputValue } } //store.dispatch,props mapDispatchToProps = (dispatch) => { return { handleInputChange(e) { const action = { type:'change_input_value', value: e.target.value } dispatch(action) } } } export default connect(mapStateToProps,mapDispatchToProps)(TodoList)
6-12 最终 TodoList 功能

通过 react 官网提供的脚手架工具(creat-react-app)来搭建项目
1).采用了 react 全家桶:
react
react-dom
react-redux
redux
redux-thunk
2).ajax 请求
axios
3).项目目录
项目目录
4).代码展示

//src文件下的 index.js import React from 'react' import ReactDOM from 'react-dom'; import {Provider} from 'react-redux'; import TodoList from './TodoList' import store from './store' const App = ( <Provider store={store}> <TodoList/> </Provider> ) ReactDOM.render(App, document.getElementById('root')); //TodoList.js import React, {Component} from 'react' import {connect} from "react-redux"; import {getInputValueAction, getHandleClickAction, getDeleteItemAction, getListDataApi} from './store/actionCreators' class TodoList extends Component { render() { const {inputValue, list, handleInputChange, handleClick, handleDelete} = this.props; return ( <div> <div> <input type="text" value={inputValue} onChange={handleInputChange} /> <button onClick={handleClick}>提交</button> </div> <div> <ul> { list.map((item, index) => ( <li key={index} onClick={() => handleDelete(index)}>{item}</li> )) } </ul> </div> </div> ) } componentDidMount() { this.props.getListData() } } const mapStateToProps = (state) => { return { inputValue: state.inputValue, list: state.list } }; const mapDispatchToProps = (dispatch) => { return { handleInputChange(e) { const action = getInputValueAction(e.target.value); dispatch(action) }, handleClick() { const action = getHandleClickAction(); dispatch(action) }, handleDelete(index) { const action = getDeleteItemAction(index); dispatch(action) }, getListData() { const action = getListDataApi(); dispatch(action); } } }; export default connect(mapStateToProps, mapDispatchToProps)(TodoList); //store 文件下的index.js import {createStore, applyMiddleware, compose} from 'redux'; import thunk from 'redux-thunk'; import reducer from './reducer' const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose; const enhancer = composeEnhancers(applyMiddleware(thunk)); const store = createStore(reducer, enhancer); export default store; //store 文件下的reducer.js import {CHANGE_INPUT_VALUE, ADD_ITEM, DELETE_ITEM, GET_LIST_DATA} from './actionTypes'; const defaultState = { inputValue: '', list: [] }; export default (state = defaultState, action) => { if (action.type === CHANGE_INPUT_VALUE) { const newState = JSON.parse(JSON.stringify(state)); newState.inputValue = action.value; return newState } if (action.type === ADD_ITEM) { const newState = JSON.parse(JSON.stringify(state)); newState.list.push(newState.inputValue); newState.inputValue = ''; return newState } if (action.type === DELETE_ITEM) { const newState = JSON.parse(JSON.stringify(state)); newState.list.splice(action.index, 1); return newState } if (action.type === GET_LIST_DATA) { const newState = JSON.parse(JSON.stringify(state)); newState.list = action.data; return newState } return state } //store 文件下的actionTypes.js export const CHANGE_INPUT_VALUE = 'change_input_value'; export const ADD_ITEM = 'add_item'; export const DELETE_ITEM = 'delete_item'; export const GET_LIST_DATA = 'get_list_data'; //store 文件下的actionCreators.js import axios from 'axios'; import {CHANGE_INPUT_VALUE, ADD_ITEM, DELETE_ITEM, GET_LIST_DATA} from './actionTypes'; export const getInputValueAction = (value) => ({ type: CHANGE_INPUT_VALUE, value }); export const getHandleClickAction = () => ({ type: ADD_ITEM }); export const getDeleteItemAction = (index) => ({ type: DELETE_ITEM, index }); export const getListDataAction = (data) => ({ type: GET_LIST_DATA, data }); export const getListDataApi = () => { return (dispatch) => { axios.get('/api/todoList') .then(res => { const data = res.data; const action = getListDataAction(data); dispatch(action) }) .catch((e) => { console.log('/api/todoList 网络异常') }) } };

第 7 章 项目实战中的一些技巧

7-1 styled-components 的应用

在写 react 组件的时候,为了防止样式被污染到,我们可以通过 styled-components 自定义标签以及样式。

//1.安装 styled-components npm install styled-components -S //2.初步使用方法,创建一个style.js文件 import styled from 'styled-components'; export const Nav = styled.div` width:1000px; margin: 0 auto; height: 50px; line-height: 50px; &.txtColor{ color:red } ` 组件中引用 import {Nav} from './style.js' <Nav className="txtColor"> //3.attrs属性 export const NavItem = styled.a.attrs({ href: '/' })` //样式 ` export const NavItem = styled.input.attrs({ placeholder: '搜索' })` //样式 ` //4.嵌套的应用 import { Nav,NavItem} from './style.js' <Nav> <NavItem className="bg">首页</NavItem> </Nav> export const Nav = styled.div` width:1000px; margin: 0 auto; height: 50px; line-height: 50px; &.txtColor{ color:red } //嵌套写法 .bg{ background: red } ` //5.全局样式的使用(createGlobalStyle),比如reset.css、iconfont.css等等 export const GlobalStyle = createGlobalStyle` //reset.css内容 或者 iconfont.css 内容等等 `; import React from 'react'; import ReactDOM from 'react-dom'; import {Provider} from 'react-redux'; import Main from './Main'; import store from './store'; import {GlobalStyle} from './style'; import {GlobalStyle2} from './statics/font/iconfont' const App = ( <Provider store={store}> <GlobalStyle/> //这个地方就可以设置为全局样式了 <GlobalStyle2/>//这个地方就可以设置为全局样式了 <Main/> </Provider> ); ReactDOM.render(App, document.getElementById('root')); //6.参数传递和获取 应用场景,当我们都在循环一个列表的数据的时候,需要传递这个img作为它的背景图片 <Toppic imgUrl = '......png'></Topic> <Toppic imgUrl = '......png'></Topic> import styled from 'styled-components' export const Topic = styled.div` background:url(${(props)=>props.imgUrl}) `;

上面是 styled-components 的一些常用的使用方法,要是想学习和了解更多。
styled-components 更多学习和了解地址

7-2 redux 中 combinReducers 的使用

在开发过程中,我们不可能把所有的 reducer 放到一个文件里面,那么肯定需要对 reducer 进行拆分的,但是拆分后的 reducer 最后我们肯定需要在合并到一起呢,因为在 redux 在创建 store 的时候,需要 reducer 的集合作为入参的。所有合并 reducer 就诞生了 combinReducers

import { combinReducers } from 'reducer' import {reducer as headerReducer} from '../common/header/store' import {reducer as footerReducer} from '../common/footer/store' ... const reducer = combinReducers({ header: headerReducer, footer: footerReducer ... }) export default reducer

ps: 调用的时候注意了

const mapState = (state) => { return { focused: state.header.focused //调用时候,你加上你header或者footer } };
7-3 store 的拆分

store 的拆分

上面我们写 TodoList 的 demo 的是,因为只涉及两个页面,所以不会考虑到 store 的拆分,但是在我们制作项目的时候,我们就的考虑 store 的拆分了

1.最外层的 store 文件(store 总中心):(index.js 和 reducer.js)
index.js:创建 store,并且把 store 和 reducer 链接起来,而且配置了 redux-devtools 可以让我们在 chrome 里面看到 redux 的变化
reducer.js: 把项目中各个地方的 reducer 通过 combinReducers 方便合并起来,把合并的最终结果 reducer,暴露出去

//index.js import {createStore, applyMiddleware, compose} from "redux"; import reducer from './reducer'; import thunk from 'redux-thunk'; const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose; const enhancer = composeEnhancers( applyMiddleware(thunk), ); const store = createStore(reducer, enhancer); export default store; //reducer.js import {combineReducers} from "redux"; import {reducer as headerReducer} from '../common/header/store' const reducer = combineReducers({ header: headerReducer }); export default reducer;

2.组件中的 store 文件(碎片 store,那公共 header 为例)的拆分:
index.js: 把下面三个文件整合到一起,并且暴露出去,为了更方便的引用
constants.js: 定义了一些大写的常量,且暴露出去这些常量
actionCreators.js: header 组件中 action 的一些方法都放在这个里面,且暴露出去
reducer.js: header 组件中的 reducer 操作放在这个里面,且暴露出去

//index.js: import reducer from './reducer'; import * as constants from './constants'; import * as actionCreators from './actionCreators'; export { reducer, //为了总store中reducer更方便的引入 constants,//为了actionCreator更方便的引入 actionCreators//为了组件中更方便的引入 } //总store中reducer引入的时候:import {reducer as headerReducer} from '../common/header/store' //actionCreator引入的时候:import {constants} from './index'; //header组件引入的时候:import {actionCreators} from './store'; //constants.js export const INPUT_FOCUSED = 'header/input_focused'; export const INPUT_BLUR = 'header/input_blur'; //actionCreators.js import {constants} from './index'; export const getInputFocusedAction = () => ({ type: constants.INPUT_FOCUSED }); export const getInputBlurAction = () => ({ type: constants.INPUT_BLUR });
7-4 使用 Immutable.js 来管理 store 中的数据

为了确保原始 state 不会被修改,导致的一些问题。所以我们引入了 Immutable.js 来更好维护我们的原始 state 数据

//1.安装 immutable npm install immutable -S //2.使用 immutable中的fromJS 可以把 js对象转变为 immutable对象 import {constants} from './index'; import { fromJS } from 'immutable'; const defaultState = fromJS({ focused:false }) //3.设置 更改state里面的 immutable数据那么就需要.set()方法 //immutable对象的set方法,会结合之前immutable对象的值和设置的值,返回一个全新的对象 export default (state = defaultState,action) =>{ if(action.type === constants.INPUT_FOCUSED) { return state.set('focused',true) } if(action.type === constants.GET_HEADER_LIST) { //return state.set('list', ).set('totalPage', ); //改为 state.merge()方法的 return state.merge({ list: action.data, totalPage:action.totalPage }); } return state } //4.获取 要使用immutable数据 要通过.get方法 const mapState = (state) =>{ return { focused: state.header.get('focused') } } //5.获取 当需要把immutable对象转化为 普通的js对象时候 const {list} = this.props const newList = list.toJS() //toJS方法的使用
7-5 使用 redux-immutable 统一数据格式

上一小节说到,我们将 state 初始化的数据通过 immutable 这个库变成了 immutable 对象,确保了 state 里面数据的稳定性,但是呢,在我们组件去获得 immutable 的时候:
focused: state.header.get('focused')中
state.header 是“js 对象”
而后面的.get('focused')则是“immutable 对象”
这样看的有些不统一,那么如何把 state.header 也变成 immutable 对象呢?那么我们就去看那个地方设置 state.header

//安装redux-immutable npm install redux-immutable -S //在最外层的reducer.js 文件对跟reducer进行设置 将 import {combineReducers} from "redux"; 改为 import {combineReducers} from "redux-immutable";//redux 改为 redux-immutable import {reducer as headerReducer} from '../common/header/store' const reducer = combineReducers({ header: headerReducer }); export default reducer; //优化代码 const mapState = (state) => { return { focused: state.get(header).get('focused') } }; 改为 连续调用两次get方法通过getIn方法一次实现 const mapState = (state) => { return { focused: state.getIn(['header','focused']) } };

ps:学习了解更多 immutable 的知识链接

7-6 避免无意义的请求发送,提升组件性能

有些数据并不是我们每次点击就去请求接口,需要我们初次点击的时候,请求一次接口,随后点击就不请求了,那么就要加一些判断限制一下

const {list} = this.props //当去请求一个列表的时候,如果初始的数据为0,那么我去请求一次接口 (list.length === 0) && dispatch(action) ps:这种只是项目中的一种情况,我们在开发过程中呢,要结合项目开发功能,来写不同的判断来减少或者没必要的接口请求
7-7 什么是路由,如何在 React 中使用路由功能

我们使用的是 react-router-dom 来做路由的

//安装 react-router-dom npm install react-router-dom -S //使用 import React, {Component} from 'react'; import {Provider} from 'react-redux'; import {BrowserRouter, Route} from 'react-router-dom'; import store from './store'; import {GlobalStyle} from './style'; import {GlobalStyle2} from './statics/font/iconfont' import Header from './common/header'; import Home from './pages/Home'; import Detail from './pages/Detail'; class App extends Component { render() { return ( <Provider store={store}> <GlobalStyle/> <GlobalStyle2/> <Header/> <BrowserRouter> <div> <Route path='/' exact component={Home}></Route> <Route path='/detail' exact component={Detail}></Route> </div> </BrowserRouter> </Provider> ) } } export default App;

2.单页路由的跳转,通过 Link 的 to 属性进行页面的跳转
单页跳转的好处,减少 http 请求,请求速度会很快

import {Link} from 'react-router-dom'; <Link to='/'> <Logo /> </Link>

3.页面路由参数的传递
1)、动态路由获取参数
http://localhost:3000/detail/1

//配置路由 <Route path='/detail/:id' exact component={Detail}></Route> //获取Url上面的1这个参数值 this.props.match.params.id

2)、非动态路由获取参数
http://localhost:3000/detail?id=1

//配置路由 <Route path='/detail' exact component={Detail}></Route> //获取Url上面的1这个参数值 const id = this.props.location.search; //search: ?id=2 再对id进行数据的处理才能拿到最终我们想要的值
7-8 PureComponent 的应用
import React,{Component} from 'react'; 改为 import React,{PureComponent} from 'react';

为了考虑 react 的性能优化,需要我们对变更的数据的进行监测,采用 shouldComponentUpdate 对组件进行优化,如果这样的话,就需要写很多 shouldComponentUpdate 的代码,那么 react 中这个 PureComponent 组件 就是自动帮助我们做这部分优化功能的。
注释:如果采用 PureComponent 这个组件那么,必须保证你的数据是 immutable 的

  • React

    React 是 Facebook 开源的一个用于构建 UI 的 JavaScript 库。

    192 引用 • 291 回帖 • 373 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...