共计 40481 个字符,预计需要花费 102 分钟才能阅读完成。
十五、Mobx
npm i mobx@5
1. Mobx 介绍
- Mobx 是一个功能强大,上手非常容易的状态管理工具
- Mobx 背后的哲学很简单: 任何源自应用状态的东西都应该自动地获得
- Mobx 利用 getter 和 setter 来收集组件的数据依赖关系,从而在数据发生变化的时候精确知道哪些组件需要重绘,在界面的规模变大的时候,往往会有很多细粒度更新
(vue类似)
2. Mobx 与 redux 的区别
- Mobx 写法上更偏向于 OOP
- 对一份数据直接进行修改操作,不需要始终返回一个新的数据
- 并非单一 store,可以多 store
- Redux 默认以 JavaScript 原生对象形式存储数据,而 Mobx 使用可观察对象
优点:
- 学习成本小
- 面向对象编程,而且对 TS 友好
缺点:
- 过于自由:Mobx 提供的约定及模版代码很少,代码编写很自由,如果不做一些约定,比较容易导致团队代码风格不统一
- 相关的中间件很少,逻辑层业务整合是问题
3. 支持装饰器
npm i @babel/core @babel/plugin-proposal-decorators @babel/preset-env
创建 .babelrc
文件
{
"presets": [
"@babel/preset-env"
],
"plugins": [
[
"@babel/plugin-proposal-decorators",
{
"legacy": true
}
]
]
}
创建 config-overrides.js
文件
const path = require('path')
const { override, addDecoratorsLegacy } = require('customize-cra')
function resolve(dir) {
return path.join(__dirname, dir)
}
const customize = () => (config, env) => {
config.resolve.alias['@'] = resolve('src')
if (env === 'production') {
config.externals = {
'react': 'React',
'react-dom': 'ReactDOM'
}
}
return config
};
module.exports = override(addDecoratorsLegacy(), customize())
安装依赖
npm i customize-cra react-app-rewired
修改 package.json
文件
...
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-app-rewired eject"
},
...
4. Mobx 的使用
(1)observable 和 autorun
import { observable, autorun } from 'mobx';
const value = observable.box(0);
const number = observable.box(100);
autorun(() => {
console.log(value.get());
});
value.set(1);
value.set(2);
number.set(101);
// 0,1,2。 // autorun 使用到才能被执行
// 只能是同步,异步需要处理
// 观察对象,通过 map
const map = observable.map({ key: "value"});
// map.set("key", "new value");
// map.get("key")
// 观察对象,不通过 map
const map = observable({ key: "value"});
// map.key map.key="xiaoming"
//观察数组
const list = observable([1, 2, 4]);
list[2] = 3;
(2) action,runInAction 和严格模式
import {observable, action, configure,runInAction} from 'mobx';
configure({enforceActions:'always'})
// 严格模式, 必须写 action,
// 如果是 never,可以不写 action,
// 最好设置 always, 防止任意地方修改值, 降低不确定性。
class Store {
@observable number = 0;
@observable name = "Joe";
@action add = () => {
this.number++;
} // action只能影响正在运行的函数,而无法影响当前函数调用的异步操作
@action load = async () => {
const data = await getData();
runInAction(() => {
this.name = data.name;
});
} // runInAction 解决异步问题
}
const newStore = new Store();
newStore.add();
// 如果在组件监听
componentDidMount() {
this.unsubscribe = autorun(()=>{
console.log(newStore.number);
})
}
// 取消监听
componentWillUnmount() {
this.unsubscribe()
}
5. mobx-react 的使用
npm i mobx-react@5
or yarn add mobx-react@5
- react 组件里使用 @observer、observer 函数/装饰器可以用来将 React 组件转变成响应式组件
- 可观察的局部组件状态 @observable 装饰器在 React 组件上引入可观察属性。而不需要通过 React 的冗长和强制性的 setState 机制来管理
import {observer} from "mobx-react"
import {observable} from "mobx"
@observer class Timer extends React.Component {
@observable secondsPassed = 0
componentWillMount() {
setInterval(() => {
this.secondsPassed++
}, 1000)
} // 如果是严格模式需要加上 @action 和 runInAction
// 一个新的生命周期钩子函数 componentWillReact
// 当组件因为它观察的数据发生了改变,它会安排重新渲染,
// 这个时候 componentWillReact 会被触发
componentWillReact() {
console.log("I will re-render, since the todo has changed!");
}
render() {
return (<span>Seconds passed: { this.secondsPassed } </span> )
}
}
ReactDOM.render(<Timer />, document.body)
(3)Provider 组件
它使用了 React 的上下文(context)机制,可以用来向下传递 stores。 要连接到这些 stores,需要传递一个 stores 名称的列表给 inject,这使得 stores 可以作为组件的 props 使用、this.props
// mobx/store.js
class Store {
@observable number = 0;
@action add = () => {
this.number++;
}
}
export default new Store() // 导出 Store 实例
import { inject, observer } from "mobx-react"
@inject("store")
@observer // 需要转换为响应式组件
class Child extends Component{
render(){
return (
<div>
Child --{this.props.store.number}
</div>
)
}
}
@inject("store")
class Middle extends Component{
render(){
return (
<div>
Middle-<button onClick={()=>{
this.props.store.add();
}}>test</button>
<Child/>
</div>
)
}
}
// 通过 provider 传 store 进去
<Provider store={store}>
<Middle/>
</Provider>
// 函数式组件
// 方法一
import React from 'react'
import { Observer } from 'mobx-react'
import store from '../mobx/store'
export default function Cinemas (props) {
return (
<div>
<Observer>
{() => {
return (
store.list.map(item =>
<dl key={item.cinemaId}>
<dt>{item.name}</dt>
<dd>{item.address}</dd>
</dl>
)
)
}}
</Observer>
</div>
)
}
// 方法二
// 使用 mobx 的 observable
import React from 'react'
import { observable } from 'mobx'
import store from '../mobx/store'
function Cinemas (props) {
return (
<div>
{store.list.map(item =>
<dl key={item.cinemaId}>
<dt>{item.name}</dt>
<dd>{item.address}</dd>
</dl>
)}
</div>
)
}
export default observable(Cinemas)
十六、TS
1. typescript
(手册 · TypeScript Handbook(中文版) (gitbooks.io))
- TypeScript 的定位是静态类型语言,在写代码阶段就能检查错误,而非运行阶段
- 类型系统是最好的文档,增加了代码的可读性和可维护性
- 有一定的学习成本,需要理解接口(Interfaces)、泛型(Generics)、类(Classes)等
- ts 最后被编译成 js
2. 创建 react+ts 项目
create-react-app my-app --template typescript
提示以下内容说明 create-react-app
版本低
3. 声明
- 可以在当前文件加上
declare const $: any;
- 安装
npm i @types/jquery
@types 是 npm 的一个分支,用来存放*.d.ts
文件
npm i --save react-router-dom
# 编译器需要通过这个声明文件,进行类型检查工作
npm i --save @types/react-router-dom
4. 变量声明
// String(原生的构造函数) vs string (ts 中的类型)
var myname:string = "字符"
var mybool:boolean = false
var mynumber:number = 100
var mylist:Array<string> = ["111","222","3333"]
var myname2:string | number | boolean = 100
var myname3:string | number = "Joe"
var mylist2:Array<string| number> = [1,2,"Joe"]
var mylist3:(string| number)[] = [1,2,"Joe"]
5. 定义普通函数
接口描述形状
interface SearchFunc {
(source: string, subString: string): boolean;
}
// 对于函数类型的类型检查来说,函数的参数名不需要与接口里定义的名字相匹配
let mySearch: SearchFunc;
mySearch = function(src: string, sub: string): boolean {
let result = src.search(sub);
return result > -1;
}
传参
function Test(list:String[],text?:String,...args:String[]):void{
console.log(list,text,args)
}
Test(["1111","2222"])
// list:["1111","2222"] text: undefined args: []
Test(["0","1"],"a","b","c")
// list:["0","1"] text: "a" args: ["b","c"]
类型断言 as
function Test( mytext:string|number ){
console.log((mytext as string).length) // 对
console.log((mytext as any).length) // 对
console.log((mytext as string[]).length) // 错,原声明没有这个类型,无法断言
}
6. 定义普通类
interface MyInter {
name:String, // 必选属性
readonly country:String, //只读属性
getName():void // 定义方法
}
class MyObj implements MyInter{
name="Joe"
country="China"
private age = 100 // 私有属性, 不能在接口定义
getName(){
// ...
}
private getAge(){
// ...
} // 私有方法, 不能在接口定义
}
7. 定义类组件
interface PropInter {
name: string | number;
firstName?: string; // 可选属性
lastName?: string; // 可选属性
// [propName: string]: any 任意属性
}
interface StateInter {
count: number
}
// 根组件 ,第一个参数可以传 any
class HelloClass extends React.Component<PropInter, StateInter> {
state: State = {
count: 0,
}; // setState 时候也才会检查
static defaultProps = { // 属性默认值
name: "default name"
firstName: "",
lastName: "",
};
}
8. 定义函数式组件
// 根组件
const App:React.FC = (props)=>{
console.log(props)
const [name, setname] = useState<string>("Joe")
return (
<div>
app
</div>
)
}
// 子组件接受属性 -1
interface iprops {
count:number
}
const Child:React.FC<iprops> = (props)=>{
return (
<div>
child-{props.count}
</div>
)
}
// 子组件接受属性 -2
const Child = (props:iprops)=>{
return (
<div>
child-{props.count}
</div>
)
}
useRef
const mytext = useRef<HTMLInputElement>(null)
<input type="text" ref={mytext}/>
useEffect(() => {
console.log(mytext.current && mytext.current.value)
}, [])
useContext
interface IContext{
call:string
}
const GlobalContext = React.createContext<IContext>({
call:"" // 定义初始值,按照接口规则
})
<GlobalContext.Provider value={{
call:"电话"
}}>
// ...
</GlobalContext.Provider>
const {call} = useContext(GlobalContext)
useReducer
interface IPrevState{
count:number
}
interface IAction{
type:string,
payload:any
}
function reducer (prevState:IPrevState,action:IAction){
// ...
return prevState
}
const [state,dispatch]= useReducer(reducer,{
count:1
})
dispatch({
type:"Action1",
payload:[]
})
9. 父子通信
// 父组件调用
<Child key={index} item={item} index={index} cb={(index)=>{
var newlist= [...list]
newlist.splice(index,1)
setList(newlist)
}}/>
// 子组件
interface ItemType{
item:string,
index:number, // 定义接口
cb:(param:number)=>void // 定义接口
}
const Child = (props:ItemType)=>{
let {index,item,cb} = props
return (
<div >{item}
<button onClick={()=>cb(index)}>del-{index}</button>
</div>
)
}
10. 路由
npm i react-router-dom@5
/ npm i --save-dev @types/react-router-dom
编程式导航
// 使用编程式导航,需要引入接口配置
import { RouteComponentProps } from "react-router-dom";
interface IProps {自己定义的接口}
type HomeProps = IProps & RouteComponentProps; // 两个接口属性都支持
interface IState {}
class Home extends React.Component<HomeProps, IState> {
private handleSubmit = async () => {
// code for API calls
this.props.history.push("/home");
};
public render(): any {
return <div>Hello</div>;
}
}
动态路由
interface IParams{
id:string
}
// RouteComponentProps 是一个泛型接口
class Detail extends Component<RouteComponentProps<IParams>, any>{
componentDidMount() {
console.log(this.props.match.params.id)
}
render(){
return (
<div>
detail
</div>
)
}
}
11. redux
import {createStore} from 'redux'
interface ActionInter{
type:string,
payload:any
}
interface IState {
isShow: boolean
}
const reducer = (prevState: IState = {
isShow: true
}, action: ActionInter)=>{
const { type } = action
const newState = { ...prevState }
switch (type) {
case 'show':
newState.isShow = true
return newState
case 'hide':
newState.isShow = false
return prevState
default:
return prevState
}
}
const store = createStore(reducer, /* enhancer */)
export default store
十七、styled-components
npm i styled-components
or yarn add styled-components
它是通过 JavaScript 改变 CSS 编写方式的解决方案之一,从根本上解决常规 CSS 编写的一些弊端。通过 JavaScript 来为 CSS 赋能,我们能达到常规 CSS 所不好处理的逻辑复杂、函数方法、复用、避免干扰。样式书写将直接依附在 JSX 上面,HTML、CSS、JS 三者再次内聚。all in js的思想
基本
import React, { Component } from 'react'
import styled from 'styled-components'
export default function App () {
const StyledFooter = styled.footer`
position:fixed;
bottom:0;
width:100%;
background: pink;
line-height:30px;
ul{
display:flex;
margin:0;
padding:0;
list-style:none;
justify-content:space-evenly;
li:hover{
// PC 测试
background:skyblue;
}
}
`
return (
<div>
函数组件
<StyledFooter>
<ul>
<li>首页</li>
<li>列表</li>
<li>我的</li>
</ul>
</StyledFooter>
</div>
)
}
export default class App extends Component {
render () {
const StyledFooter = styled.footer`
position:fixed;
bottom:0;
width:100%;
background: pink;
line-height:30px;
ul{
display:flex;
margin:0;
padding:0;
list-style:none;
justify-content:space-evenly;
li{
&:hover {
// PC 测试
background:skyblue;
}
}
}
`
return (
<div>
类组件
<StyledFooter>
<ul>
<li>首页</li>
<li>列表</li>
<li>我的</li>
</ul>
</StyledFooter>
</div>
)
}
}
透传 props
import React, { Component } from 'react'
import styled from 'styled-components'
export default class App extends Component {
render () {
const StyledInput = styled.input`
outline:none;
border-bottom:1px solid red;
border-radius:10px;
`
return (
<div>
<StyledInput type='text' placeholder='输入' />
</div>
)
}
}
基于 props 做样式判断
import React, { Component } from 'react'
import styled from 'styled-components'
export default class App extends Component {
render () {
const StyledDiv = styled.div`
width:100px;
height:100px;
background:${props => props.bg || 'hotpink'};
`
return (
<div>
<StyledDiv bg="blue"></StyledDiv>
<StyledDiv></StyledDiv>
</div>
)
}
}
样式化任意组件(一定要写 className )
import React, { Component } from 'react'
import styled from 'styled-components'
export default class App extends Component {
render () {
const StyledChild = styled(Child)`
background:pink;
`
return (
<div>
App
<StyledChild></StyledChild>
</div>
)
}
}
const Child = (props) => <div className={props.className}>Child</div>
原理:高阶函数/组件
扩展样式
import React, { Component } from 'react'
import styled from 'styled-components'
export default class App extends Component {
render () {
const StyledButton1 = styled.button`
width:100px;
height:100px;
background:skyblue;
`
const StyledButton2 = styled(StyledButton1)`
background:red;
`
const StyledButton3 = styled(StyledButton1)`
background:blue;
`
return (
<div>
App
<StyledButton1>按钮1</StyledButton1>
<StyledButton2>按钮2</StyledButton2>
<StyledButton3>按钮3</StyledButton3>
</div>
)
}
}
加动画
import React, { Component } from 'react'
import styled, { keyframes } from 'styled-components'
export default class App extends Component {
render () {
const rotate360 = keyframes`
from {
transform:rotate(0deg);
}
to {
transform:rotate(360deg);
}
`
const StyledDiv = styled.div`
width:100px;
height:100px;
background:red;
animation: ${rotate360} 3s linear infinite;
`
return (
<div>
App
<StyledDiv></StyledDiv>
</div>
)
}
}
十八、单元测试
react-test-renderer
npm i react-test-renderer
or yarn add react-test-renderer
测试文件要以 .test.js
结尾
// ./App.js 要测试的文件
import React, { Component } from 'react'
export default class App extends Component {
state = {
txt: '',
list: ['111', '222', '333']
}
render () {
return (
<div>
<h1>Joe-TodoList</h1>
<input type='text' value={this.state.txt} onChange={(e) => {
this.setState({
txt: e.target.value
})
}} /> <button className='add' onClick={() => {
this.setState({
list: [...this.state.list, this.state.txt],
txt: ''
})
}}>add</button>
<ul>
{this.state.list.map((item, index) => (
<li key={index}>
{item}
<button className='del' onClick={() => {
let newList = [...this.state.list]
newList.splice(index, 1)
this.setState({
list: newList
})
}}>del</button>
</li>
))}
</ul>
</div>
)
}
}
// ./test/react-test.render.test.js 文件
import ShallowRenderer from 'react-test-renderer/shallow'
import ReactTestUtil from 'react-dom/test-utils'
import App from '../App'
describe('react-test-render', function () {
it('App 的名字是 Joe-TodoList', function () {
// 渲染到虚拟 DOM
const render = new ShallowRenderer()
render.render(<App />)
// console.log(render.getRenderOutput().props.children[0].type)
// 断言这个值是 h1
expect(render.getRenderOutput().props.children[0].type).toBe('h1')
expect(render.getRenderOutput().props.children[0].props.children).toBe('Joe-TodoList')
})
it('删除功能', function () {
// 渲染到真实 DOM
const app = ReactTestUtil.renderIntoDocument(<App />)
// 查找 li 标签
let todoItems = ReactTestUtil.scryRenderedDOMComponentsWithTag(app, 'li')
console.log(todoItems.length)
// 获取删除按钮
let delButton = todoItems[1].querySelector('button')
// 模拟点击
ReactTestUtil.Simulate.click(delButton)
// 获取删除后的 li 标签
let todoItemsAfterClick = ReactTestUtil.scryRenderedDOMComponentsWithTag(app, 'li')
// 判断是否删除,长度是否减 1
expect(todoItems.length - 1).toBe(todoItemsAfterClick.length)
})
it('添加功能', function () {
// 渲染到真实 DOM
const app = ReactTestUtil.renderIntoDocument(<App />)
// 获取 li 标签
let todoItems = ReactTestUtil.scryRenderedDOMComponentsWithTag(app, 'li')
// 获取 input输入框 标签
let addInput = ReactTestUtil.scryRenderedDOMComponentsWithTag(app, 'input')
// 输入框放入 value
addInput.value = 'Joe'
// 获取添加按钮 拿到的是数组
// let addButton = ReactTestUtil.scryRenderedDOMComponentsWithClass(app, 'add')
// 拿出一个 返回多个会报错
let addButton = ReactTestUtil.findRenderedDOMComponentWithClass(app, 'add')
// 模拟点击
ReactTestUtil.Simulate.click(addButton)
// 获取添加后的 li 标签
let todoItemsAfterClick = ReactTestUtil.scryRenderedDOMComponentsWithTag(app, 'li')
// 判断是否添加,长度是否加 1
expect(todoItemsAfterClick.length).toBe(todoItems.length + 1)
})
})
npm run test
执行测试
enzyme
npm i enzyme
or yarn add enzyme
安装适配器 npm i enzyme-adapter-react-版本
or yarn add enzyme-adapter-react-版本
@wojtekmaj/enzyme-adapter-react-17
/ @cfaester/enzyme-adapter-react-18
// ./test/enzyme.test.js 文件
import Enzyme, { shallow, mount } from 'enzyme' // 需要适配器enzyme-adapter-react-版本
import adapter from '@cfaester/enzyme-adapter-react-18'
import App from '../App'
Enzyme.configure({ adapter: new adapter() })
describe('react-test-render', function () {
it('App 的名字是 Joe-TodoList', function () {
// 渲染到虚拟 DOM
let app = shallow(<App />)
// 查找 app 组件的 h1 标签里面的内容是不是 Joe-TodoList
expect(app.find('h1').text()).toEqual('Joe-TodoList')
})
it('删除功能', function () {
// 渲染到真实 DOM
let app = mount(<App />)
// 获取 app 组件里面所有的 li 标签
let todoLength = app.find('li').length
// 查找 app 组件里面的 button 标签类名是 del 的第 0 个,模拟点击
app.find('button.del').at(0).simulate('click')
// 期望 app 组件里的 li 标签 删掉了一个
expect(app.find('li').length).toEqual(todoLength - 1)
})
it('添加功能', function () {
// 渲染到真实 DOM
let app = mount(<App />)
// 获取 app 组件里面所有的 li 标签
let todoLength = app.find('li').length
// 获取 app 组件里的 input 标签
let addInput = app.find('input')
// 设置 input 标签的 value 值
addInput.value = 'Joe'
// 获取
// 查找 app 组件里面的 .add 类名的标签,模拟点击
app.find('.add').simulate('click')
// 期望 app 组件里的 li 标签 增加了一个
expect(app.find('li').length).toEqual(todoLength + 1)
})
})
挂载组件
import Enzyme,{mount} from 'enzyme';
import Adapter from '@wojtekmaj/enzyme-adapter-react-17'
// 在使用 Enzyme 前需要先适配 React 对应的版本
Enzyme.configure({ adapter: new Adapter() })
it('挂载拿到状态', () => {
const app = mount(<App />);
expect(app.state().name).toEqual('Joe');
expect(app.state().age).toEqual(100);
})
/*
.text():返回当前组件的文本内容
.html():返回当前组件的 HTML 代码形式
.props():返回根组件的所有属性
.prop(key):返回根组件的指定属性
.state([key]):返回根组件的状态
.setState(nextState):设置根组件的状态
.setProps(nextProps):设置根组件的属性
*/
测试组件渲染出来的 HTML
it('组件渲染出来的 HTML', () => {
const app = mount(<App />);
expect(app.find('#myid').text()).toEqual('Joe');
})
模拟用户交互
it('模拟用户交互', () => {
const app = mount(<App />);
app.find('#mybtn').simulate('click')
expect(app.state().name).toEqual('xiaoming');
})
十九、redux-saga
redux-saga 它是来处理异步流程的npm i redux-saga
or yarn add redux-saga
在 saga 中,全局监听器和接收器使用 Generator 函数和 saga 自身的一些辅助函数实现对整个流程的管控
代码实现
// index.js
import {createStore,applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga/core';
import {reducer} from './reducer'
import mySagas from './saga'
const sagaMiddleware = createSagaMiddleware(); // 创建中间件
const store = createStore(reducer,{list:[]},applyMiddleware(sagaMiddleware))
// 注意运行的时机是在 store 创建好了之后
sagaMiddleware.run(mySagas);
export default store
// saga.js
import {takeEvery,put} from 'redux-saga/effects'
import {changeList} from './action'
function *mySagas(){
// 监听 GET_LIST
// 在每个 `监听 GET_LIST` action 被 dispatch 时调用 getList
yield takeEvery("GET_LIST", getList);
// yield takeEvery("DELETE_LIST", deleteList);
}
function *getList(){
// 异步处理
let res = yield new Promise(resolve=>{
setTimeout(()=>{
resolve(["1111","2222","3333"])
},2000)
})
yield put(changeList(res)) // 发出新的 action
}
export default mySagas
// action.js
export const changeList = (value)=>{
return {
type:"CHANGE_LIST",
payload:value
}
}
export const getSaAction = ()=>{
// GET_LIST 被 saga 监听
return {
type:"GET_LIST"
}
}
// reducer.js
export const reducer = (prevState,action)=>{
let {type,payload} = action;
switch(type){
case "CHANGE_LIST":
let newstate = {...prevState}
newstate.list = [...newstate.list,...payload]
return newstate
default :
return prevState
}
}
// App.js
class App extends Component {
componentDidMount() {
store.subscribe(()=>{
console.log(store.getState())
})
}
handleClick = ()=>{
store.dispatch(getSaAction())
}
render() {
return (
<div >
<button onClick={this.handleClick}>获取异步</button>
</div>
)
}
}
二十、React 补充
1. Portal
Portals 提供了一个最好的在父组件包含的DOM结构层级外的DOM节点渲染组件的方法。
import { createPortal } from 'react-dom'
ReactDOM.createPortal(child,container);
第一个参数 child 是可渲染的 react 子项,比如元素,字符串或者片段等。第二个参数 container 是一个 DOM 元素。
import React, { Component } from 'react'
import { createPortal } from 'react-dom'
export default class PortalDialog extends Component {
render () {
return (
createPortal(
<div style={{
width: '100%',
height: '100%',
position: 'fixed',
top: '0',
left: '0',
backgroundColor: 'rgba(0,0,0,.3)',
zIndex: '99999'
}}>
PortalDialog
<div>loading...</div>
{this.props.children}
<button onClick={this.props.close}>close</button>
</div>
,
document.body
)
)
}
}
(1) 用法
普通的组件,子组件的元素将挂载到父组件的 DOM 节点中。
render() {
// React 挂载一个 div 节点,并将子元素渲染在节点中
return (
<div>
{this.props.children}
</div>
);
}
有时需要将元素渲染到 DOM 中的不同位置上去,这是就用到的 portal 的方法。
render(){
// 此时 React 不再创建 div 节点,而是将子元素渲染到 dom 节点上。domNode,是一个有效的任意位置的 dom 节点。
return ReactDOM.createPortal(
this.props.children,
domNode
)
}
一个典型的用法就是当父组件的 dom 元素有 overflow:hidden 或者 z-inde 样式,而你又需要显示的子元素超出父元素的盒子。举例来说,如对话框,悬浮框,和小提示。
(2) 在 protal 中的事件冒泡
虽然通过 portal 渲染的元素在父组件的盒子之外,但是渲染的 dom 节点仍在 React 的元素树上,在那个 dom 元素上的点击事件仍然能 dom 树中监听到。
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
const getDiv = () => {
const div = document.createElement('div');
document.body.appendChild(div);
return div;
};
const withPortal = (WrappedComponent) => {
class AddPortal extends Component {
constructor(props) {
super(props);
this.el = getDiv();
}
componentWillUnmount() {
document.body.removeChild(this.el);
}
render(props) {
return ReactDOM.createPortal(<WrappedComponent {...props} />, this.el);
}
}
return AddPortal;
};
class Modal extends Component {
render() {
return (
<div>
<div>amodal content</div>
<button type="button">Click</button>
</div>
);
}
}
const PortalModal = withPortal(Modal);
class Page extends Component {
constructor(props) {
super(props);
this.state = { clicks: 0 };
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState(state => ({
clicks: state.clicks + 1
}));
}
render() {
return (
<div onClick={this.handleClick}>
<h3>ppppppppp</h3>
<h3>num: {this.state.clicks}</h3>
<PortalModal />
</div>
);
}
}
export default Page;
2. Lazy 和 Suspense
(1) React.lazy 定义
React.lazy
函数能让你像渲染常规组件一样处理动态引入(的组件)。
什么意思呢?其实就是懒加载。
- 为什么代码要分割
当你的程序越来越大,代码量越来越多。一个页面上堆积了很多功能,也许有些功能很可能都用不到,但是一样下载加载到页面上,所以这里面肯定有优化空间。就如图片懒加载的理论。 - 实现原理
当 Webpack 解析到该语法时,它会自动地开始进行代码分割(Code Splitting),分割成一个文件,当使用到这个文件的时候会这段代码才会被异步加载。 - 解决方案
在React.lazy
和常用的三方包 react-loadable ,都是使用了这个原理,然后配合 webpack 进行代码打包拆分达到异步加载,这样首屏渲染的速度将大大的提高。
由于React.lazy
不支持服务端渲染,所以这时候 react-loadable 就是不错的选择。
import React, { Component, Suspense } from 'react'
// import NowPlaying from './components/NowPlaying'
// import ComingSoon from './components/ComingSoon'
const NowPlaying = React.lazy(() => import('./components/NowPlaying'))
const ComingSoon = React.lazy(() => import('./components/ComingSoon'))
export default class App extends Component {
state = {
type: 1
}
render () {
return (
<div>
App
<button onClick={() => {
this.setState({
type: 1
})
}}>正在热映</button>
<button onClick={() => {
this.setState({
type: 2
})
}}>即将上映</button>
<Suspense fallback={<div>加载中...</div>}>
{
this.state.type === 1 ?
<NowPlaying></NowPlaying> :
<ComingSoon></ComingSoon>
}
</Suspense>
</div>
)
}
}
(2) 如何使用 React.lazy
下面示例代码使用 create-react-app 脚手架搭建:
// OtherComponent.js 文件内容
import React from 'react'
const OtherComponent = ()=>{
return (
<div>
我已加载
</div>
)
}
export default OtherComponent
// App.js 文件内容
import React from 'react';
import './App.css';
// 使用 React.lazy 导入 OtherComponent 组件
const OtherComponent = React.lazy(() => import('./OtherComponent'));
function App() {
return (
<div className="App">
<OtherComponent/>
</div>
);
}
export default App;
这是最简单的 React.lazy
,但是这样页面会报错。这个报错提示我们,在 React 使用了 lazy
之后,会存在一个加载中的空档期,React 不知道在这个空档期中该显示什么内容,所以需要我们指定。接下来就要使用到 Suspense
。
Suspense
如果在 App 渲染完成后,包含 OtherComponent
的模块还没有被加载完成,我们可以使用加载指示器为此组件做优雅降级。这里我们使用 Suspense
组件来解决。
这里将 App 组件改一改
import React, { Suspense, Component } from 'react';
import './App.css';
// 使用 React.lazy 导入 OtherComponent 组件
const OtherComponent = React.lazy(() => import('./OtherComponent'));
export default class App extends Component {
state = {
visible: false
}
render() {
return (
<div className="App">
<button onClick={() => {
this.setState({ visible: true })
}}>加载 OtherComponent 组件</button>
<Suspense fallback={<div>Loading...</div>}>
{ this.state.visible ? <OtherComponent /> : null }
</Suspense>
</div>
)
}
}
我们指定了空档期使用 Loading 展示在界面上面,等 OtherComponent
组件异步加载完毕,把 OtherComponent
组件的内容替换掉 Loading 上。
为了演示把 chrome 网络调到 lower-end mobile,不然看不到 loading 出现。
可以从上面图片看出,当点击加载的时候,页面的 head 会插入 这段代码,发出一个 get 请求,页面开始显示 loading,去请求 2.chunk.js
文件。
请求结束返回内容就是 OtherComponent
组件的内容,只是文件名称和文件内容经过 webpack 处理过。
注意:
Suspense
使用的时候,fallback
一定是存在且有内容的, 否则会报错。
3. forwordRef
引用传递(Ref forwading)是一种通过组件向子组件自动传递 引用 ref 的技术。对于应用者的大多数组件来说没什么作用。但是对于有些重复使用的组件,可能有用。例如某些 input 组件,需要控制其 focus,本来是可以使用 ref 来控制,但是因为该 input 已被包裹在组件中,这时就需要使用 Ref forward 来透过组件获得该 input 的引用。可以透传多层
import React, { Component, forwardRef } from 'react'
export default class App extends Component {
myRef = React.createRef()
render () {
return (
<div>
App
<button onClick={() => {
this.myRef.current.focus()
this.myRef.current.value = ''
}}>获取焦点</button>
<Child ref={this.myRef}></Child>
</div>
)
}
}
const Child = forwardRef((props, ref) => {
return (
<div style={{ backgroundColor: 'skyblue' }}>
Child
<input type='text' defaultValue='初始值' ref={ref}></input>
</div>
)
})
未使用 forwordRef
// 子组件
class Child extends Component{
componentDidMount() {
this.props.callback(this.refs.myinput)
}
render(){
return (
<div>
<input type="text" ref="myinput"/>
</div>
)
}
}
// 父组件
class App extends Component {
render() {
return (
<div>
<Child callback={(el)=>{
el.focus()
}}/>
</div>
)
}
}
使用 forwardRef
// 子组件
const Child = forwardRef((props,ref)=>{
return (
<div>
<input type="text" ref={ref}/>
</div>
)
})
// 父组件
class App extends Component {
myref = createRef()
componentDidMount() {
this.myref.current.focus()
}
render() {
return (
<div>
<Child ref={this.myref}/>
</div>
)
}
}
4. Functional Component 缓存
为什么起 memo 这个名字?
在计算机领域,记忆化是一种主要用来提升计算机程序速度的优化技术方案。它将开销较大的函数调用的返回结果存储起来,当同样的输入再次发生时,则返回缓存好的数据,以此提升运算效率。
作用
组件仅在它的 props 发生改变的时候进行重新渲染。通常来说,在组件树中 React 组件,只要有变化就会走一遍渲染流程。但是 React.memo()
,我们可以仅仅让某些组件进行渲染。
与 PureComponent 区别
PureComponent 只能用于 class 组件,memo 用于 functional 组件
import React, { Component, memo } from 'react'
export default class App extends Component {
state = {
name: 'Joe',
age: 18
}
render () {
return (
<div>
App --- {this.state.name}
<button onClick={() => {
this.setState({
name: 'qiaofugui'
})
}}>Click name</button>
<button onClick={() => {
this.setState({
age: 999
})
}}>Click age</button>
<Child age={this.state.age}></Child>
</div>
)
}
}
const Child = memo((props) => {
console.log('Child')
return (
<div>Child --- {props.age}</div>
)
})
用法
import {memo} from 'react'
const Child = memo(()=>{
return (
<div>
<input type="text" />
</div>
)
})
// 或者
const Child = ()=>{
return (
<div>
<input type="text" />
</div>
)
})
const MemoChild = memo(Child)
二十一、React 扩展
1. GraphQL
(1)介绍与 hello
- GraphQL 是 Facebook 开发的一种数据查询语言,并于2015年公开发布。它是 REST API 的替代品
- GraphQL既是一种用于 API 的查询语言也是一个满足你数据查询的运行时。GraphQL 对你的 API 中的数据提供了一套易于理解的完整描述,使得客户能够准确地获得它需要的数据,而且没有任何冗余,也让 API 更容易地随着时间推进而演进
- 官网:https://graphql.org/
- 中文网:https://graphql.cn/
特点
- 请求需要的数据,不多不少 例如:account 中有 name、age、sex、departmen 等。可以取得需要的字段
- 获取多个资源,只用一个请求
- 描述所有可能类型的系统。便于维护,根据需求平滑演进,添加或者隐藏字段
- reslful 一个接口只能返回页资源,graphql 一次可以读取多个资源
- reslful 用不同的 url 来区分资源,graphql 用类型区分资源
query {
user (id : "1") {
name
gender
employee (first: 20) {
name
email
}
father {
telephone
}
son {
school
}
}
}
(2)参数类型与传递
- 基本类型:
String
/Int
/Float
/Boolean
和 ID,可以在 shema 声明的时候直接使用 [类型]
代表数组,例如:[Int]
代表整型数组- 和 js 传递参数一样,小括号内定义形参,但是注意:参数需要定义类型
!
(叹号)表示参数不能为空
const express = require('express')
const { buildSchema } = require('graphql')
const graphqlHttp = require('express-graphql')
var Schema = buildSchema(`
type Account{
name: String,
age: Int,
location: String
}
type Film{
id: Int,
name: String,
poster: String,
price: Int
}
type Query{
hello: String,
getName: String,
getAge: Int,
getAllName: [String],
getAllAge: [Int],
getAccountInfo: Account,
getNowPlayingList: [Film],
getFilmDetail(id: Int!): Film
}
`)
// 假数据
var fakeDb = [
{ id: 1, name: 'aaa', poster: 'https://111', price: 100 },
{ id: 2, name: 'bbb', poster: 'https://222', price: 200 },
{ id: 3, name: 'ccc', poster: 'https://333', price: 300 }
]
// 处理器
const root = {
// 通过数据库查询
hello: () => {
var str = 'hello world'
return str
},
getName: () => {
return 'Joe'
},
getAge: () => {
return 18
},
getAllName: () => {
return ['aaa', 'bbb', 'ccc']
},
getAllAge: () => {
return [111, 222, 333]
},
getAccountInfo () {
return { name: 'Joe', age: 18, location: 'chinese' }
},
getNowPlayingList () {
return fakeDb
},
getFilmDetail ({ id }) {
return fakeDb.filter(item => item.id === id)[0]
}
}
var app = express()
app.use('/home', function (req, res) {
res.send('home data')
})
app.use('/list', function (req, res) {
res.send('list data')
})
app.use('/graphql', graphqlHttp({
schema: Schema,
rootValue: root,
graphiql: true
}))
app.listen(3000)
// http://localhost:3000/graphql?query=
查数据
// 查数据
query {
hello
getName
getAge
getAllName
getAllAge
getAccountInfo {
name
age
location
}
getNowPlayingList {
id
name
poster
price
}
getFilmDetail(id: 3) {
id
name
poster
price
}
}
(3)mutation
查询数据用 query
,修改数据用 Mutation
const express = require('express')
const { buildSchema } = require('graphql')
const graphqlHttp = require('express-graphql')
var Schema = buildSchema(`
type Film{
id: Int,
name: String,
poster: String,
price: Int
}
input FilmInput{
name: String,
poster: String,
price: Int
}
type Query{
getNowPlayingList: [Film],
}
type Mutation{
createFilm(input: FilmInput): Film,
updateFilm(id: Int!, input: FilmInput): Film,
deleteFilm(id: Int!): Int
}
`)
// 假数据
var fakeDb = [
{ id: 1, name: 'aaa', poster: 'https://111', price: 100 },
{ id: 2, name: 'bbb', poster: 'https://222', price: 200 },
{ id: 3, name: 'ccc', poster: 'https://333', price: 300 }
]
// 处理器
const root = {
// 通过数据库查询
getNowPlayingList () {
return fakeDb
},
createFilm ({ input }) {
var obj = { ...input, id: fakeDb.length + 1 }
fakeDb.push(obj)
return obj
},
updateFilm ({ id, input }) {
var current = null
fakeDb = fakeDb.map(item => {
if (item.id === id) {
current = { ...item, ...input }
return { ...item, ...input }
}
return item
})
return current
},
deleteFilm ({ id }) {
fakeDb = fakeDb.filter(item => item.id !== id)
return 1
}
}
var app = express()
app.use('/home', function (req, res) {
res.send('home data')
})
app.use('/list', function (req, res) {
res.send('list data')
})
app.use('/graphql', graphqlHttp({
schema: Schema,
rootValue: root,
graphiql: true
}))
app.listen(3000)
// http://localhost:3000/graphql
改数据
// 改数据
mutation {
createFilm(input:{
name:"ddd",
poster:"https://444",
price:400
}) {
id,
name,
poster,
price
}
updateFilm(id: 1, input: {
name: "aaa-修改",
poster: "https://111-修改",
price: 111
}) {
id,
name,
poster,
price
}
deleteFilm(id: 1)
}
(4)结合数据库
const express = require('express')
const { buildSchema } = require('graphql')
const graphqlHttp = require('express-graphql')
// ----------链接数据库服务----------
var mongoose = require("mongoose")
mongoose.connect("mongodb://localhost:27017/maizuo", { useNewUrlParser: true, useUnifiedTopology: true })
// 限制 数据库这个films(集合表) 只能存三个字段
var FilmModel = mongoose.model("film", new mongoose.Schema({
name: String,
poster: String,
price: Number
}))
// FilmModel.create
// FilmModel.find
// FilmModel.update
// FilmModel.delete
// --------------------------------
var Schema = buildSchema(`
type Film{
id: String,
name: String,
poster: String,
price: Int
}
input FilmInput{
name: String,
poster: String,
price: Int
}
type Query{
getNowPlayingList: [Film],
}
type Mutation{
createFilm(input: FilmInput): Film,
updateFilm(id: String!, input: FilmInput): Film,
deleteFilm(id: String!): Int
}
`)
// 处理器
const root = {
// 通过数据库查询
getNowPlayingList () {
return FilmModel.find()
},
createFilm ({ input }) {
/*
1. 创建模型
2. 操作数据库
*/
return FilmModel.create({
...input
})
},
updateFilm ({ id, input }) {
return FilmModel.updateOne({
_id: id
}, {
...input
}).then(res => FilmModel.find({ _id: id })).then(res => res[0])
},
deleteFilm ({ id }) {
return FilmModel.deleteOne({ _id: id }).then(res => 1)
}
}
var app = express()
app.use('/home', function (req, res) {
res.send('home data')
})
app.use('/list', function (req, res) {
res.send('list data')
})
app.use('/graphql', graphqlHttp({
schema: Schema,
rootValue: root,
graphiql: true
}))
app.listen(3000)
(5)客户端访问
(6) 结合React
query
npm i react-apollo apollo-boost graphql graphql-tag
import React, { Component } from 'react'
import { ApolloProvider, Query } from 'react-apollo'
import ApolloClient from 'apollo-boost'
import gql from 'graphql-tag'
const client = new ApolloClient({
uri: "/graphql"
})
export default class App extends Component {
render () {
return (
<ApolloProvider client={client}>
<div>
<JoeQuery></JoeQuery>
</div>
</ApolloProvider>
)
}
}
class JoeQuery extends Component {
query = gql`
query {
getNowPlayingList {
id,
name,
price
}
}
`
render () {
return <Query query={this.query}>
{
({ loading, data }) => {
console.log(loading)
return loading ? <div>loading....</div> :
<div>
{
data.getNowPlayingList.map(item =>
<div key={item.id}>
<div>名字:{item.name}</div>
<div>价格:{item.price}</div>
</div>
)
}
</div>
}
}
</Query>
}
}
带参数
import React, { Component } from 'react'
import { ApolloProvider, Query } from 'react-apollo'
import ApolloClient from 'apollo-boost'
import gql from 'graphql-tag'
const client = new ApolloClient({
uri: "/graphql"
})
export default class App extends Component {
render () {
return (
<ApolloProvider client={client}>
<div>
<JoeQuery></JoeQuery>
</div>
</ApolloProvider>
)
}
}
class JoeQuery extends Component {
query = gql`
query getNowPlayingList($id:String!){
getNowPlayingList(id:$id) {
id,
name,
price
}
}
`
state = {
id: "61e66f60dd8ae3c99074ac53"
}
render () {
return <div>
<input type="text" onChange={(evt) => {
this.setState({
id: evt.target.value
})
}} />
<Query query={this.query} variables={{ id: this.state.id }}>
{
({ loading, data }) => {
console.log(loading)
return loading ? <div>loading....</div> :
<div>
{
data.getNowPlayingList.map(item =>
<div key={item.id}>
<div>名字:{item.name}</div>
<div>价格:{item.price}</div>
</div>
)
}
</div>
}
}
</Query>
</div>
}
}
mutation
import React, { Component } from 'react'
import { ApolloProvider, Mutation } from 'react-apollo'
import ApolloClient from 'apollo-boost'
import gql from 'graphql-tag'
const client = new ApolloClient({
uri: "/graphql"
})
export default class App extends Component {
render () {
return (
<ApolloProvider client={client}>
<div>
<JoeCreate></JoeCreate>
</div>
</ApolloProvider>
)
}
}
class JoeCreate extends Component {
createFilm = gql`
mutation createFilm($input: FilmInput){
createFilm(input:$input) {
id,
name,
price
}
}
`
render () {
return <div>
<Mutation mutation={this.createFilm}>
{
(createFilm, { data }) => {
console.log(data)
return <div>
<button onClick={() => {
createFilm({
variables: {
input: {
name: "777",
poster: "http://777",
price: 70
}
}
})
}}>add</button>
</div>
}
}
</Mutation>
</div>
}
}
更新
import React, { Component } from 'react'
import { ApolloProvider, Mutation } from 'react-apollo'
import ApolloClient from 'apollo-boost'
import gql from 'graphql-tag'
const client = new ApolloClient({
uri: "/graphql"
})
export default class App extends Component {
render () {
return (
<ApolloProvider client={client}>
<div>
<JoeUpdate></JoeUpdate>
</div>
</ApolloProvider>
)
}
}
class JoeUpdate extends Component {
createFilm = gql`
mutation updateFilm($id:String!,$input: FilmInput){
updateFilm(id:$id,input:$input) {
id,
name,
price
}
}
`
render () {
return <div>
<Mutation mutation={this.createFilm}>
{
(updateFilm, { data }) => {
console.log(data)
return <div>
<button onClick={() => {
updateFilm({
variables: {
id: "61e67c0031bf52b53c9245c7",
input: {
name: "777-修改",
poster: "http://777-修改",
price: 700
}
}
})
}}>update</button>
</div>
}
}
</Mutation>
</div>
}
}
删除
import React, { Component } from 'react'
import { ApolloProvider, Mutation } from 'react-apollo'
import ApolloClient from 'apollo-boost'
import gql from 'graphql-tag'
const client = new ApolloClient({
uri: "/graphql"
})
export default class App extends Component {
render () {
return (
<ApolloProvider client={client}>
<div>
<JoeDelete></JoeDelete>
</div>
</ApolloProvider>
)
}
}
class JoeDelete extends Component {
createFilm = gql`
mutation deleteFilm($id:String!){
deleteFilm(id:$id)
}
`
render () {
return <div>
<Mutation mutation={this.createFilm}>
{
(deleteFilm, { data }) => {
console.log(data)
return <div>
<button onClick={() => {
deleteFilm({
variables: {
id: "61e67c0031bf52b53c9245c7"
}
})
}}>delete</button>
</div>
}
}
</Mutation>
</div>
}
}
2. dva
npm install dva-cli -g
https://dvajs.com/guide
dva 首先是一个基于 redux 和 redux-saga 的数据流方案,然后为了简化开发体验,dva 还额外内置了 react-router 和 fetch,所以也可以理解为一个轻量级的应用框架
dva = react-router + redux + redux-saga
dva-cli is deprecated, please use create-umi instead, checkout https://umijs.org/guide.create-umi-app.html for detail.
dva 应用的最简洁结构
import dva from 'dva'
const App = () => <div>Hello Dva</div>
// 创建应用
const app = dva()
// 注册视图
app.router(() => <App />)
// 启动应用
app.start('#root')
数据流图
3. umi
umi,中文发音为乌米,是一个可插拔的企业级 react 应用框架。umi 以路由为基础的,支持类 next.js 的约定式路由,以及各种进阶的路由功能,并以此进行功能扩展,比如支持路由级的按需加载。umi 在约定式路由的功能层面会更像 nuxt.js 一些。
开箱即用,省去了搭框架的的时间
安装脚手架
在一个空目录里执行
# npm
npx @umijs/create-umi-app
npx create-umi@latest
# yarn
yarn create umi
# pnpm
pnpm dlx create-umi@latest
目录
一个基础的 Umi 项目大致是这样的
路由
配置式路由:在 .umirc.ts
文件内 routes
选项配置
约定式路由:umi 会根据 pages 目录自动生成路由配置。需要注释 .umirc.ts
文件内 routes
相关,否则自动配置不生效
1. 基础路由
2. 重定向
// pages/index.tsx
import React from 'react'
import { Redirect } from 'umi'
export default function index() {
return (
<Redirect to='/film' />
)
}
3. 嵌套路由
有时候不好用,尝试重启一下
// pages/film/_layout.tsx
import React, { ReactNode } from 'react'
import { IRouteComponentProps, Redirect, useLocation } from 'umi'
interface IProps {
children: ReactNode
}
type myProps = IProps & RouteComponentProps; // 两个接口属性都支持
export default function Film(props: myProps) {
if (props.location.pathname === '/film' || props.location.pathname === '/film/') {
// '/film' 重定向到 '/film/nowplaying'
return <Redirect to='/film/nowplaying' />
}
return (
<div>
Film
<div style={{ width: '100%', height: '200px', backgroundColor: 'pink' }}>大轮播</div>
{props.children}
</div>
)
}
4. 动态路由
// pages/detail/[id].tsx
import React from 'react'
import { IRouteComponentProps, useParams } from 'umi'
interface IDetailId {
id: string
}
export default function Detail(props: IRouteComponentProps) {
const params = useParams<IDetailId>()
return (
// <div>Detail --- {props.match.params.id}</div>
<div>Detail --- {params.id}</div>
)
}
// pages/film/NowPlaying.tsx
import React, { useEffect, useState } from 'react'
import { IRouteComponentProps, useHistory } from 'umi'
interface IItem {
filmId: number,
name: string
}
export default function NowPlaying(props: IRouteComponentProps) {
const [nowPlayingList, setNowPlayingList] = useState([])
const history = useHistory()
useEffect(() => {
fetch('https://m.maizuo.com/gateway?cityId=110100&pageNum=1&pageSize=10&type=1&k=3898483', {
headers: {
'X-Client-Info': '{"a":"3000","ch":"1002","v":"5.2.1","e":"16717661461034803650494465","bc":"110100"}',
'X-Host': 'mall.film-ticket.film.list'
}
}).then(res => res.json())
.then(res => {
console.log(res.data.films)
setNowPlayingList(res.data.films)
})
return () => {
}
}, [])
return (
<div>
NowPlaying
<ul>
{
nowPlayingList.length === 0
? '加载中...' :
nowPlayingList.map((item: IItem) => <li key={item.filmId} onClick={() => {
// props.history.push(`/detail/${item.filmId}`)
history.push(`/detail/${item.filmId}`)
}}>{item.name}</li>)
}
</ul>
</div>
)
}
5. 路由拦截
// src/pages/film/Center.tsx
import React from 'react'
function Center() {
return (
<div>Center</div>
)
}
Center.wrappers = ['@/wrappers/Auth']
export default Center
// src/wrapper/Auth.tsx
import React, { ReactNode } from 'react'
import { Redirect } from 'umi'
interface IProps {
children: ReactNode
}
export default (props: IProps) => {
const isLogin = localStorage.getItem("token")
console.log(isLogin);
if (isLogin) {
return <div>{props.children}</div>
} else {
return <Redirect to="/login" />
}
}
6. hash 模式
// 在.umirc.ts
export default {
history:{ type: 'hash' }
}
// 在 .umirc.ts
import { defineConfig } from 'umi';
export default defineConfig({
nodeModulesTransform: {
type: 'none',
},
history: { type: 'hash' }, // 默认 browser 模式
// routes: [
// { path: '/', component: '@/pages/index' },
// ],
fastRefresh: {},
});
7. 声明式导航
import React, { ReactNode } from 'react'
import { NavLink } from 'umi'
import './index.less'
interface IPRops {
children: ReactNode
}
export default function IndexLayout(props: IPRops) {
return (
<div>
IndexLayout
{props.children}
<ul style={{ display: 'flex', position: 'fixed', left: '0', bottom: '0', justifyContent: 'space-evenly', width: '100%', margin: '0', padding: '0', backgroundColor: 'skyblue', listStyle: 'none', textAlign: 'center' }}>
<li>
<NavLink to='/film' activeClassName='active'>Film</NavLink>
</li>
<li>
<NavLink to='/cinema' activeClassName='active'>Cinema</NavLink>
</li>
<li>
<NavLink to='/center' activeClassName='active'>Center</NavLink>
</li>
</ul>
</div>
)
}
8. 编程式导航
import { history } from 'umi';
history.push(`/detail/${item}`)
mock 功能
umi 里约定 mock 文件夹下的文件或者 page(s) 文件夹下的 _mock 文件即 mock 文件,文件导出接口定义,支持基于 require 动态分析的实时刷新,支持 ES6 语法,以及友好的出错提示
// mock/api.js
export default {
// 支持值为 Object 和 Array
'GET /api/users': { users: [1, 2] },
// GET POST 可省略
'/api/users/1': { id: 1 },
// 支持自定义函数,API 参考 express@4
'POST /api/users/create': (req, res) => { res.end('OK'); },
}
export default {
'GET /users': { name: 'joe', password: '111' },
'POST /users/login': (req, res) => {
console.log(req.body)
const { username, userpwd } = req.body
if (username === 'joe' && userpwd === '111') {
res.send({
ok: 1
})
} else {
res.send({
ok: 0
})
}
}
}
反向代理
// 在.umirc.ts
proxy: {
'/ajax': {
target: 'https://m.maoyan.com',
// pathRewrite: { '^/api': '' },
changeOrigin: true
}
},
import { defineConfig } from 'umi';
export default defineConfig({
nodeModulesTransform: {
type: 'none',
},
history: { type: 'browser' },
// routes: [
// { path: '/', component: '@/pages/index' },
// ],
fastRefresh: {},
proxy: {
'/api': {
target: 'https://i.maoyan.com',
changeOrigin: true
}
}
});
antd
// .umirc.ts
import { defineConfig } from 'umi';
export default defineConfig({
nodeModulesTransform: {
type: 'none',
},
history: { type: 'browser' },
// routes: [
// { path: '/', component: '@/pages/index' },
// ],
fastRefresh: {},
proxy: {
'/api': {
target: 'https://i.maoyan.com',
changeOrigin: true
}
},
antd: {
mobile: false
}
});
// 组件页面中使用
import {Button} from 'antd-mobile'
<Button type="primary">add</Button>
dva 集成
默认开启了 redux 调试工具
- 按目录约定注册 model,无需手动 app.model
- 文件名即命名空间 namespace,可以省去 model 导出的 namespace key
- 无需手写 router.js,交给 umi 处理,支持 model 和 component 的按需加载
- 内置 query-string 处理,无需再手动解码和编码
- 内置 dva-loading 和 dva-immer,其中 dva-immer 需通过配置开启(简化 reducer 编写)
// .umirc.ts
dva:{
// 自定义配置
}
同步
// models/joe.js
export default {
// 命名空间
namespace:'joe',
state:{
isShow:true,
list:[]
},
// 处理 state --同步
reducers:{
// reducer简写, type 类型是 show 的时候自动处理
show(state,{payload}){
return {...state,...payload}
},
hide(state,{payload}){
return {...state,...payload}
}
},
// yield 表示后面的方法执行完以后 call 表示调用一个 api 接口
// put表示一个派发
effects:{
*showEffect(payload,{ put }){
yield put({
type:'show',
payload:{
isShow:true
}
})
},
*hideEffect(payload,{put}){
yield put({
type:'hide',
payload:{
isShow:false
}
})
}
}
}
// 根组件
import { connect } from 'umi';
function BasicLayout(props) {
return (
<div >
{ props.isShow ?... :null }
{props.children}
</div>
);
}
// state.joe 命名空间
export default connect(state=>state.kerwin)(BasicLayout);
// detail.js
import { connect, useDispatch } from 'umi';
function Detail(props) {
const dispatch = useDispatch()
useEffect(() => {
dispatch({
type:"joe/hideEffect" // 命名空间 joe
})
return () => {
dispatch({
type:"joe/showEffect"//命名空间 joe
})
};
}, [])
return (
<div>
Detail
</div>
)
}
export default connect(state=>state.joe)(Detail)
异步
// models/joe.js
import {getNowplaying} from '../util/getNowplaying'; // 封装的 fetch 调用接口
export default{
// ...
reducers:{
// ...
changeList(state,{payload}){
return {...state,...payload}
}
},
// 异步
// yield 表示后面的方法执行完以后 call 表示调用一个 api 接口
effects:{
// ...
*getListEffect(payload,{put,call}){
let res = yield call(getNowplaying,"test-by-joe")
yield put({
type:"changeList",
payload:{
list:res
}
})
}
}
}
// util/getNowplaying
export async function getNowplaying(value){
console.log(value) // value 是 call 的第二个参数
var res = await fetch("/ajax/comingList?ci=65&token=&limit=10&optimus_uuid=43388C403C4911EABDC9998C784A573A4F64A16AA5A34184BADE807E506D749E&optimus_risk_level=71&optimus_code=10").then(res=>res.json())
return res.coming
}
// nowplaying.js
import React,{useEffect} from 'react';
import { connect,useDispatch } from 'umi';
function Nowplaying(props) {
let {list,loading} = props
let dispatch = useDispatch()
useEffect(() => {
if(list.length===0){
dispatch({
type:"joe/getListEffect" // 命名空间 joe
})
}
}, [list])
return (
<div>
nowplaying--{loading.global?'正在加载数据...':''}
{ 遍历list }
</div>
)
}
export default connect(({joe,loading})=>({
...joe,
loading
}))(Nowplaying)
// models/cinemaModel.ts
export default {
namespace: 'cinema',
state: {
list: []
},
reducers: {
clearList(prevState: any) {
return {
...prevState,
list: []
}
},
changeList(prevState: any, action: any) {
return {
...prevState,
list: action.payload
}
}
},
effects: {
*getList(action: any, obj: any): any {
const { call, put } = obj
const res = yield call(getListForList, action.payload.cityId)
console.log(res);
yield put({
type: 'changeList',
payload: res
})
}
}
}
async function getListForList(id: string) {
let res = await fetch(`https://m.maizuo.com/gateway?cityId=${id}&ticketFlag=1&k=5990609`, {
headers: {
'X-Client-Info': '{"a":"3000","ch":"1002","v":"5.2.1","e":"16717661461034803650494465"}',
'X-Host': 'mall.film-ticket.cinema.list'
}
}).then(res => res.json())
return res.data.cinemas
}
// pages/Cinema.tsx
import React, { useEffect } from 'react'
import { DotLoading, NavBar } from 'antd-mobile'
import { SearchOutline } from 'antd-mobile-icons'
import { connect } from 'umi'
function Cinema(props: any) {
useEffect(() => {
if (props.list.length === 0) {
props.dispatch({
type: 'cinema/getList',
payload: {
cityId: props.cityId
}
})
} else {
console.log('缓存');
}
}, [])
return (
<div>
<NavBar back={
<div onClick={() => {
props.dispatch({
type: 'cinema/clearList',
})
props.history.push('/city')
}}>
{props.cityName}
</div>
} backArrow={null} right={
<div style={{ fontSize: '24px' }}><SearchOutline /></div>
}>标题
</NavBar>
{
props.loading &&
<div style={{ fontSize: 24, textAlign: 'center' }}>
<DotLoading></DotLoading>
</div>
}
Cinema
<ul>
{
props.list.map((item: any) =>
<li key={item.cinemaId}>{item.name}</li>
)
}
</ul>
</div>
)
}
const mapStateToProps = (state: any) => {
console.log(state);
return {
loading: state.loading.global,
cityName: state.city.cityName,
cityId: state.city.cityId,
list: state.cinema.list,
}
}
export default connect(mapStateToProps)(Cinema)