主页

十五、Mobx

https://cn.mobx.js.org/
npm i mobx@5

1. Mobx 介绍

  1. Mobx 是一个功能强大,上手非常容易的状态管理工具
  2. Mobx 背后的哲学很简单: 任何源自应用状态的东西都应该自动地获得
  3. Mobx 利用 getter 和 setter 来收集组件的数据依赖关系,从而在数据发生变化的时候精确知道哪些组件需要重绘,在界面的规模变大的时候,往往会有很多细粒度更新

(vue类似)

2. Mobx 与 redux 的区别

  1. Mobx 写法上更偏向于 OOP
  2. 对一份数据直接进行修改操作,不需要始终返回一个新的数据
  3. 并非单一 store,可以多 store
  4. 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

  1. react 组件里使用 @observer、observer 函数/装饰器可以用来将 React 组件转变成响应式组件
  2. 可观察的局部组件状态 @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

文档地址
  1. TypeScript 的定位是静态类型语言,在写代码阶段就能检查错误,而非运行阶段
  2. 类型系统是最好的文档,增加了代码的可读性和可维护性
  3. 有一定的学习成本,需要理解接口(Interfaces)、泛型(Generics)、类(Classes)等
  4. ts 最后被编译成 js

2. 创建 react+ts 项目

create-react-app my-app --template typescript
提示以下内容说明 create-react-app 版本低

3. 声明

  1. 可以在当前文件加上 declare const $: any;
  2. 安装 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}&nbsp;
              <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 函数能让你像渲染常规组件一样处理动态引入(的组件)。
什么意思呢?其实就是懒加载。

  1. 为什么代码要分割
    当你的程序越来越大,代码量越来越多。一个页面上堆积了很多功能,也许有些功能很可能都用不到,但是一样下载加载到页面上,所以这里面肯定有优化空间。就如图片懒加载的理论。
  2. 实现原理
    当 Webpack 解析到该语法时,它会自动地开始进行代码分割(Code Splitting),分割成一个文件,当使用到这个文件的时候会这段代码才会被异步加载。
  3. 解决方案
    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/

特点

  1. 请求需要的数据,不多不少
    例如:account 中有 name、age、sex、departmen 等。可以取得需要的字段
  2. 获取多个资源,只用一个请求
  3. 描述所有可能类型的系统。便于维护,根据需求平滑演进,添加或者隐藏字段
  • 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)

React

版权属于:Joe
作品采用:本作品采用 知识共享署名-相同方式共享 4.0 国际许可协议 进行许可。
0

目录

来自 《React17-全家桶(3)》
评论

qiaofugui

博主很懒,啥都没有
188 文章数
14 评论量
3 分类数
191 页面数
已在风雨中度过 2年138天17小时15分