一、基础知识
以组件方式考虑 UI 的构建
理解 React 组件
props + state = view
- 属性和参数决定试图
- React 组件一般不提供方法,而是某种状态机
受控组件 vs 非受控组件
- 受控组件,需要传入
onChange
的方法,由外部组件控制其数据 - 非受控组件,由自身维护数据
原则
- 创建组件的原则 – 单一职责原则:拆分出的每个组件,职责单一
- 数据状态管理原则 – DRY 原则:
- 能计算得到的状态不单独存储
- 组件尽量无状态,由 props 读取
JSX(本质不是模板引擎,而是语法糖)
在 JSX 中使用表达式
- JSX 本身就是表达式
- 在属性中使用表达式:
{1+2}
- 延展属性:
{...props}
- 表达式作为子元素(必须是可
render
的节点,子组件、字符串等)
优点
- 用声明式的方式创建节点
- 代码动态创建节点,灵活
- 无需学习新的模板语言
约定
- 大写字母开头的是自定义组件
- 小写字母开头的是原生 DOM 节点
- 可以直接使用属性语法(此时不需要大写):
<menu.item />
React 的生命周期
创建时
- Render 阶段:constructor -> getDerivedStateFormProps -> render
- Commit 阶段: 更新 DOM 和 refs -> componentDidMount
更新时
- Render 阶段:getDerivedStateFormProps -> shouldComponentUpdate -> render
- Pre-commit 阶段:getSnapshotBeforeUpdate
- Commit 阶段: 更新 DOM 和 refs -> componentDidUpdate
卸载时
- Commit 阶段:componentWillUnmount
方法
- constructor:用于初始化内部状态,唯一可以直接修改 state 的地方
- getDerivedStateFormProps
- 当 state 需要从 props 初始化时使用
- 不推荐使用:维护两者状态一致性会增加复杂度
- 每次 render 都会调用
- 典型场景:表单控件获取默认值
- componentDidMount
- getSnapshotBeforeUpdate
- 在页面 render 之前调用,state 已更新
- 典型场景:获取 render 之前的 DOM 状态
- componentDidUpdate
- shouldComponentUpdate
- 决定 Visual DOM 是否需要重绘
- 一般由 PureComponent 自动实现
Visual DOM
diff 算法
两个假设
- DOM 结构是相对稳定的
- 类型相同的兄弟节点可以被唯一标识(key)
组件复用
高阶组件(HOC)
- 定义一个函数接收组件作为参数,返回新的组件作为高阶组件:
const EnhancedCompenent = higherOrderComponent(WrappedComponent)
- 高阶组件自身不包含UI的展现,而是给他封装的组件提供一些功能和数据
函数作为子组件
Context API
- 用于使用全局的属性值,根组件是 Provide,子组件 Consumer
```js
const ThemeContext = React.createContext('light') // 初始值为 light
class App extends React.Component {
render() {
return {
<ThemeContext.Provide value="dark">
<ThemedButton />
</ThemeContext.Provide>
}
}
}
function ThemedButton(props) {
return {
<ThemeContext.Consumer>
// theme 即父组件传入的 value="dark"
{theme => <Button {..props} theme={theme}} />}
</ThemeContext.Consumer>
}
}
```
- Context API 产生的变化会被 React 监听到,然后自动刷新相关的组件,不需要通过 forceUpdate
- 使用场景:主要用于全局性的某个数据
脚手架工具
- create-react-app
- 整合了 babel、webpack、test: jest、eslint
- 特点:入门级,最简策略
- Rekit
- 增加了 Redux、React Router、Less/Scss、Feature Oriented Architecture、Dedicated IDE
- Codesandbox.io
二、Redux
JS的状态管理框架
store
```js
const store = createStore(reducer)
store.getState()
store.dispatch(action)
store.subscribe(listener)
```
action
```js
// 描述操作的行为
function addTodo() {
return {type: ADD_TODO, text: 'Build my app'}
}
store.dispatch(addTodo())
store.subscribe(() => console.log(store.getState()))
```
reducer
```js
// 更新 store 实际上是返回了一个新的 store
function todoApp(state, action) {
switch (action.type) {
case ADD_TODO:
return Object.assign({}, state, {
todos: state.todos.concat(...)
})
default:
return state
}
}
```
工具函数
- combineReducers: 多个 reducer 组合
- bindActionCreators: 封装 action 函数
在 React 中使用 Redux
```js
import { connect } from 'react-redux'
class TodoList extends Compenent {
render() {
const { todos, addTodo } = this.props;
return {
...
}
}
}
function mapStateToProps(state) {
// 需要啥绑定啥,不要返回整个 state,会影响性能
return {
todos: state.todos
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({addTodo}, dispatch);
}
const ConnectedTodoList = connect(mapStateToProps, mapDispatchToProps)(TodoList);
export default class App extend React.Compenent {
render() {
return {
<Provider store={store}>
<ConnectedTodoList />
</Provider>
}
}
}
```
- connect 的工作原理:高阶组件
异步 action
- view 事件触发 action,dispatcher 中的 middlewares 截获这个 action,发送异步请求,等待结果后 dispatch
- 本质上 action 都是同步的,异步 action 不是一种特殊的 action,而是由几个同步 action 和 middleware 组成达到异步的目的
- middleware 负责截获 action、发出 action
三、React-router
定义
<Router>
<Link to="/home"></Link>
<Route path="/home" component={Home} />
</Router>
特性
- 声明式定义(tag)
- 动态路由:render 的时候才会被解析
实现方式
API
- : 普通链接,不会触发浏览器刷新,跟 的效果一样
- : 类型 Link,但是会添加当前选中状态
- : 满足条件时提示用户是否离开当前页面
- : 重定向当前页面
- : 路径匹配时显示对应组件,默认情况下多个 Route 都匹配时每个组件都会显示
-
: 只显示第一个匹配的路由
传参
- 通过 URL 传递参数:
- 获取参数:this.props.match.params
嵌套路由
<Router>
<Route path="/:id" component={SubRoute} />
</Router>
<div>
<Route path="/:id/:subId" component={..} />
</div>
四、工程化
UI 组件库
- Ant Design
- Meterial UI
- Semantic UI
使用 Next.js 创建 React 同构应用
- 同构应用,由服务端渲染 APP 页面
- 打包优化
- 使用 next/link (prefetch 预加载)
- 使用 next/dynamic 加载组件(lazyload)
Jest 单元测试
- 不需要浏览器环境,visual DOM 可以在 node.js 环境中运行
- Redux 状态管理,纯数据层测试
工具
- Jest 零配置,很好的支持 React
- JS DOM:浏览器环境的 NodeJS 模拟
- Enzyme:React 组件渲染和测试
- nock:模拟 HTTP 请求
- sinon:函数模拟和调用跟踪
- Istanbul:单元测试覆盖率
jsdom
const JSDOM = require('jsdom').JSDOM
global.window = new JSDOM('<!DOCTYPE html><div id="react-root"></div>').window
global.document = window.document
global.navigator = window.navigator
global.HTMLElement = window.HTMLElement
Enzyme
- shallow rendering:只 render 当前组件
- full rendering:会 render 它的子组件
- static rendering:会 render 所有内容
Nock
nock('url').get('json').reply(200, responseData)
Sinon
const props = {
examples: {},
actions: {
add: jest.fn(),
},
};
const renderedComponent = shallow(
<Component {...props} />
);
renderedComponent.find('.btn').simulate('click');
expect(props.actions.add.mock.calls.length).to(1);
Istanbul
npm i -g istanbul
cd /path
istanbul cover test.js
调试工具
ESLint
- 使用 .eslintrc 配置
- 使用 airbnb 的 JS 代码风格(预定义)
Prettier
- 自动格式化,确保代码风格统一
- VScode 插件
- 使用 .prettierrc
React Dev Tools
- 定位组件,查看 props 和 state
- highlightUpdates,标明哪些组件发生了 update render
Redux Dev Tools
- Diff 查看每个 action 引起的变化
- TimeLine 回到每个 store 的状态点
- Test 自动生成一些测试点
前端项目的理解架构