react--随笔3

扩展

Immutable.js



typescript

搭建环境

create-react-app 目录 --template typescript 

统一变化

  • 所有用到jsx语法的文件都需要以tsx后缀命名
  • 使用组件声明时的Component泛型参数声明,来代替PropTypes!
  • 全局变量或者自定义的window对象属性,统一在项目根下的global.d.ts中进行声明定义
  • 对于项目中常用到的接口数据对象,在types/目录下定义好其结构化类型声明

react类型

RouteComponentProps

RouteComponentProps

RouteChildrenProps

HTMLDivElement

match

React.FC

React.FunctionComponent

React.ReactNode

ComponentType

JSX.Element

Dispatch

AxiosRequestConfig

AxiosPromise

类组件

创建组件
export default class 组件名 extends React.Component<IProps, IState>{}
export default class Comp3 extends React.Component<{ //内联类型注解
  value: string; 
  onChange: (value: string ) => void 
}, {}> {}
export default class 组件名 extends React.Component<{}, {}>{} //组件没有props和状态时

IProps,IState接口类型需要定义,可以定义在组件文件内部,或者types目录

类型约束
export interface List {//通用,可以丢到外部,也可以在外部定义,推荐`types/`目录下
  readonly id: number;
  name: string;
}
type IProps = { //未export 不通用
  readonly id: number;
  title: string;
  num?: number;
  arr?: string[]
}
type IState = {
  msg1: string;
  msg2: number;
  list: List[]
}
组件状态
export default class 组件名 extends React.Component<IProps, IState>{
	state: Readonly<IState> = initState;//state不建议通过实例属性修改,作为只读定义
}

initState 可定义到组件外部,也可定义在内部

let initState:IState = {
  msg1: 'xx',
  msg2: 12,
  list: [
    { id: 1, name: 'alex' },
    { id: 2, name: 'alex2' },
    { id: 2, name: 'alex3' },
  ]
};
props属性

类型约束内部确定只读,必传,可选特性,默认值在类属性defaultProps设定

type IProps = { //未export 不通用
  readonly id: number;//只读, props理论都应该是只读
  title: string;//必传
  num?: number;//可选
  arr?: string[]
}
export default class 组件名 extends React.Component<IProps, IState>{
	
  //props默认值
  static defaultProps={
    num:0
  }
}
事件
const initialState = { clicksCount: 0 };//先定义值

//再使用typeof推断类型,并设置只读,限定this.state修改
type TState = Readonly<typeof initialState>;
                      
class Counter extends Component<{}, TState>{
  readonly state: TState = initialState;//因为 React 不推荐直接更新 state 及其属性
  render() {
    const { clicksCount } = this.state;
    return (
      <div>
        <h3>事件</h3>
        <button onClick={this.handlerIncrement}>+</button>
        <button onClick={this.handlerDecrement}>-</button>
        <div>{clicksCount}</div>
      </div>
    )
  }

  // private handlerIncrement=()=>this.setState({clicksCount:this.state.clicksCount+1})
  // private handlerDecrement=()=>this.setState({clicksCount:this.state.clicksCount-1})
  private handlerIncrement = () => this.setState(increment)
  private handlerDecrement = () => this.setState(decrement)
}

//独立纯函数,编译单独测试
const increment = (prevState: State) => ({ clicksCount: prevState.clicksCount + 1 })
const decrement = (prevState: State) => ({ clicksCount: prevState.clicksCount - 1 })

函数式组件

定义组件

使用 React.FunctionComponent 接口定义函数组件

type Props = {
  foo: string;
};

const MyComponent: React.FunctionComponent<Props> = props => {
  return <span>{props.foo}</span>;
};
export {MyComponent}

使用 React.FC 别名定义函数组件

interface IProps {
  readonly id?:number;
  title?:string;
  num?:number;
  arr?:string[]
}

type IProps2=Readonly<IProps>;//类型映射

//函数式组件
const Footer: React.FC<IProps2> = (props) => {
  // props.num=2; //error 类型约束props为只读
  // props.title='2323';//error
  return (
    <div className="Footer">
      footer
    </div>
  )
}
export default Footer
事件,props

事件函数通过props传入

type TProps = {
  onClick(e: MouseEvent<HTMLElement>): void//必传
  text?: string//可选
}

//props默认值在函数接收参数时设定,handleClick为对象别名
const Button: FC<TProps> = ({ onClick: handleClick, text = '按钮' }: TProps) => (
  <>
    <h3>无状态组件</h3>
    <button onClick={handleClick}>按钮</button>
  </>
);

组件注入

通过props或直接嵌套的方式,向组件注入一些可变的元素

jsx节点

注入的元素有:string,number,boolean,ReactElement

举例:<Comp2 header={<h4>Header</h4>} body={12} />

interface Iprops {
  header?: React.ReactNode;//jsx节点类型 string,number,boolean,ReactElement
  body: ReactNode;//类型需要导入,来自react包
}

//! 代表排除null
class Comp2 extends React.Component<Iprops, {}> {
  render() {
    return (
      <>
        <div>---------接收可渲染的内容start--------</div>
        {this.props!.header}
        {this.props.body}
        <div>---------接收可渲染的内容end--------</div>
      </>
    );
  }
}
children嵌套

调用组件时,插入到组件的内容<组件>内容</组件>

type AuthProps = {
  children?: JSX.Element;//设定children类型,可选
  [propName:string]:any//props可以接受其他任何值
}
const authState = { show: false }//先赋值
type AuthState = Readonly<typeof authState>//后推断类型

class Auth extends Component<AuthProps, AuthState>{
  static readonly defaultProps: AuthProps = {title:'bmw'}
  readonly state: AuthState = authState
  render() {
    const { children } = this.props
    return (
    	<>
      	<div>组件自身内容</div>
        {children && children}
        <div>组件自身内容</div>
      </>
    )
  }
}
注入组件

类似于<Auth path="/foo" component={MyView} />

type AuthProps = {
  component?: ComponentType<any>;//设定要接受的组件类型ComponentType
}
const authState = { show: false }
type AuthState = Readonly<typeof authState>

class Auth extends Component<AuthProps, AuthState>{
  static readonly defaultProps: AuthProps = {title:'bmw'}
  readonly state: AuthState = authState
  render() {
    const { component: InjectedComponent } = this.props //InjectedComponent字面量别名
    
    return (
    	<>
      	<h3>组件本身</h3>
      	{/* 调用通过props传入的组件 */}
      	{InjectedComponent && <InjectedComponent/>}
      </>
    )
  }
}
注入render属性

给调用的组件传递render函数,指定被调用组件的渲染内容

举例:

<Auth render={() => (
  <div>render后的内容</div>
)} />

实现:

type AuthProps = {
  render?: () => JSX.Element;
  [propName:string]:any
}
const authState = { show: false }
type AuthState = Readonly<typeof authState>

class Auth extends Component<AuthProps, AuthState>{
  static readonly defaultProps: AuthProps = {title:'bmw'}
  readonly state: AuthState = authState
  render() {
    const { render } = this.props
    if (render) {
      return render()//调用传入渲染函数,按照外部要求渲染
    }
    return (
    	<div>原本内容</div>
    )
  }
}
ref在类组件

引用渲染完成后的元素

方式1
import React from 'react';

export default class Comp3 extends React.Component<{ //内联类型注解
  value: string; 
  onChange: (value: string ) => void 
}, {}> {
	
  //使用 ref 和 null 的联合类型,并且在回调函数中初始化他
  input: HTMLInputElement | null = null;
  
  render() {
    return (
      <>
        <h3>refs</h3>
        <input
          ref={el => this.input = el}
          value={this.props.value}
          onChange={e => this.props.onChange(e.target.value)}
        />
      </>
    );
  }
  componentDidMount(){
    this.input != null && this.input.focus() //获取焦点
  }
}
方式2
import React,{createRef} from 'react';

export default class Comp3 extends React.Component<{ //内联类型注解
  value: string; 
  onChange: (value: string ) => void 
}, {}> {

  // 使用createRef函数,返回ref对象,并指定类型,作为实例
  input = createRef<HTMLInputElement>()
  
	render() {
    return (
      <>
        <h3>refs</h3>
        <input
          ref={this.input}
          value={this.props.value}
          onChange={e => this.props.onChange(e.target.value)}
        />
      </>
    );
  }
  componentDidMount(){
    this.input.current!.focus()
  }
}
数据交互 axios
反向代理

在src目录下创建setupProxy.js

const proxy = require('http-proxy-middleware'); //需要安装中间件

module.exports = function(app) {
  app.use(
    proxy("/api", {
      target: 'http://localhost:3001',
      changeOrigin: true
    })
  );
  app.use(
    proxy("/mock", {
      target: 'http://localhost:3333',
      changeOrigin: true
    })
  )

};
axios 二次封装

在plugins目录创建axios.ts

import axios from 'axios';

export interface IUser {//通用,可在外部定义,或外部使用
  err:number,
  data:any,
  token:string
}
type TUser = Partial<IUser> & string | null //映射 交叉 联合

// 添加一个请求的拦截
axios.interceptors.request.use((config) => {

  //1抓取本地token,携带在请求头里
  let user:TUser = window.localStorage.getItem('user');
  user = user ? JSON.parse(user) : '';
  config.headers={'token': user!.token}

  //显示loading...

  return config;//返回请求

}, function(error) {
  // 请求错误时做点事
  return Promise.reject(error);
});

//添加一个响应拦截
axios.interceptors.response.use(function(response) {

  // res.data ~~ {err:1,msg:xx,data:{}} ~~ response.data

  //token过期: 返回值2,当前路由不是login时跳转,并传递当前路径,登录后可以有参考原路跳回
  if (response.data.err === 2 && !window.location.href.includes('/login')) {
    window.location.href='http://localhost:3000/login?path='+window.location.pathname
  }

  return response;

}, function(error) {
  return Promise.reject(error);
});

declare global { //定义到全局  也可以定义到src/global.d.ts
  interface Window {//给window接口添加axios方法函数
    axios(config: AxiosRequestConfig): AxiosPromise<any>
  }
}

window.axios = axios;  //希望全局使用axios ,

export default axios;

数据交互
import React from 'react';
// import axios from '../plugins/axios';

export interface IListItem {//推荐定义到src/types目录下
  _id: string, title: string, des: string, time: number
}


//数据交互
export default class Comp4 extends React.Component<{},{}> {
  readonly state:{
    list:Array<IListItem>
  }={
    list:[]
  }
  render() {
    let {list}=this.state
    return (
      <>
        <h3>comp4-数据交互</h3>
        {
          list.map((item:IListItem)=>(
            <li key={item._id}>
              {item.title}/{item.time}
            </li>
          ))
        }
      </>
    );
  }
  componentDidMount(){
    //window.axios({
    axios({//需要引入plugins/axios
      url:'/api/home'
    }).then(
      ({data:{data:list}})=>this.setState({list})
    )
  }
}

路由

主入口.tsx

import {BrowserRouter as Router,Route} from 'react-router-dom'

ReactDOM.render(
    <Router>
      <Route component={App} />
    </Router>
  , 
  // document.getElementById('root') as HTMLElement
  document.getElementById('root')! //移除null和undefined
);

根组件.tsx

<Header />
<Switch >
  <Route path="/home" component={Home} />
  <Route path="/goods" component={Goods} />
  <Route path="/login" component={Login} />
  <Route path="/reg" component={Reg} />
  <Route path="/detail/:_id" component={Detail} />
  <Redirect exact from="/" to="/home" />
</Switch>

<Footer />

列表页.tsx

 <li key={item._id}>
    <Link to={`/detail/${item._id}?dataName=home`}>
      {item.title}/{item.time}
    </Link>
  </li>

详情页.tsx,解决params._id 不存在的问题

import React from 'react'
import {RouteComponentProps,match} from 'react-router-dom';
import qs from 'qs';//类似query-string

export interface IDetail {
  title: string;
  des: string;
  time: number;
  detail: {
    auth: string;
    content: string;
    auth_icon: string;
  }
}

type TDetail = {
  err: number;
  msg: string;
  data: Partial<IDetail>;//可选映射
};

//params._id 不存在

//解决方案1
type TProps={
  match: match<{_id?: string}>//交叉一个属性到RouteComponentProps类型
} & RouteComponentProps

//解决方案2
type ParamsInfo = {//作为 RouteComponentProps的泛型传入,定义params的内容
  _id:string
}
type TProps2=RouteComponentProps<ParamsInfo>;

export default class Detail extends React.Component<TProps, TDetail>{
  readonly state: TDetail = {
    err: 1,
    msg: '失败',
    data: {}
  }
  componentDidMount() {
    let dataName=qs.parse(this.props.location.search,{ignoreQueryPrefix:true}).dataName;
    let _id=this.props.match.params._id||null;
    window.axios({
      url:`/api/${dataName}/${_id}`
    }).then(
      ({data:{err,msg,data}})=>this.setState({err,msg,data})
    )
  }
  render() {
    let { err, data } = this.state;

    return (
      <>
        {
          err === 0 ? (
            <div>
              <h3>detail</h3>
              {data.title}/{data.detail?.auth}
            </div>
          ) : (
            <div>骨架屏</div>
          )
        }
      </>
    )
  }
}

函数式组件,接受路由上下文的类型约束

const Login: React.FC<RouteChildrenProps> = ({ history, match, location }) => {}

需要规定FC别名的泛型约束RouteChildrenProps, 来自react-router-dom包

状态管理

安装:

yarn add redux react-redux @types/react-redux -S

定义类型:src/types

//public type
export interface IListItem {
  _id: string, title: string, des: string, time: number
}

//store type
export interface IStoreState {
  bNav: boolean;
  bFoot: boolean;
  bLoading: false;
  home: IListItem[];
  follow: Array<IListItem>;
  user: {
    err: number;
    msg: string;
  },

  count:number;
  test:number;
}

export type StoreState = Partial<Readonly<IStoreState>>


//action type
export type TActionCount = {
  type: string;
  payload?: number
}

定义提交类型:src/store/const.ts

// 定义增加 state 类型常量
export const INCREMENT = "INCREMENT";
// 定义减少 state 类型常量
export const DECREMENT = "DECREMENT";


定义action: src/store/actions

import { DECREMENT, INCREMENT } from '../const'
import { TActionCount } from '../../types'
import { Dispatch } from 'redux';

// 增加 state 次数的方法  同步
export const increment = (): TActionCount => ({
  type: INCREMENT,
})

// 减少 state 次数的方法 异步
export const decrement = (arg: any): any => (dispatch: Dispatch): Promise<any> => new Promise((resolve, reject) => {
  setTimeout(() => {//axios走起
    dispatch({ type: DECREMENT })
    resolve('异步actions发回来的回执')
  }, 1000)
})

定义reducers: src/store/reducers

import { combineReducers } from 'redux'

import {count} from './count'
import {other} from './other'

// combineReducers 可以吧store变成一个对象来组合reducer
//combineReducers(对象)  对象{key:value} key=state.key value=reducer函数
const rootReducer = combineReducers({
 count,//state.count: count的reducer函数
 other
})

export default rootReducer;

定义count: src/store/reducers/count

import { DECREMENT, INCREMENT } from '../const';
import { TActionCount } from '../../types';

//参数state 只代表state.count的值 , 一定要有初始值
export function count(state:number = 0, {type,payload}: TActionCount): number {
  switch (type) {
    case INCREMENT:
      return payload ? state + payload : state + 1;
    case DECREMENT:
      return state - 1
    default:
      return state
  }
}

定义store实例: src/plugins/redux

import { createStore,applyMiddleware} from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension'//开启调试工具
import thunk from 'redux-thunk'//改装dispatch接受函数
import reducer from '../store/reducers'; 

// 1、创建 store  initState是可选参数
const store = createStore(reducer,composeWithDevTools(applyMiddleware(thunk)));

export default store;

主入口引入store:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './layouts/App';

import {BrowserRouter as Router,Route} from 'react-router-dom'


import { Provider } from 'react-redux';
import store from './plugins/redux'
ReactDOM.render(
  <Provider store={store}>
    <Router>
      <Route component={App} />
    </Router>
  </Provider>
  , 
  // document.getElementById('root') as HTMLElement
  document.getElementById('root')! //移除null和undefined
);

组件接入redux使用:

import React from 'react';
import { connect } from 'react-redux';
import { Dispatch } from 'redux';

import { StoreState } from '../types';
import { INCREMENT } from '../store/const';
import { decrement } from '../store/actions';


// 创建类型接口
export interface IProps {
  count?: number;
  test?: number;
  onIncrement: () => void,
  onDecrement: () => void
}

// 使用接口代替 PropTypes 进行类型校验
class Counter extends React.PureComponent<IProps> {
  public render() {
    const { count, onIncrement, onDecrement } = this.props;
    return (
      <>
        <h3>redux+react-redux+react-thunk</h3>
        <p>
          {count}
        <br />
        <button onClick={onIncrement}> +  </button>
        <button onClick={onDecrement}> - </button>
      </p>
      </>
      
    )
  }
}

// 将 reducer 中的状态插入到组件的 props 中
// 下面是单个reducer的时候,多个的时候需要选传入哪个reducer
// const { test, count } = state
// const mapStateToProps = (state: StoreState): StoreState => ({
const mapStateToProps = ({count,other}: StoreState): StoreState => ({
  // count:state.count
  count
})

// 将 对应action 插入到组件的 props 中
const mapDispatchToProps = (dispatch: Dispatch) => ({
  onDecrement: () => dispatch(decrement('组件发出的参数')).then((res:string)=>console.log(res)),
  onIncrement: () => dispatch({type:INCREMENT,payload:2})
})

// 使用 connect 高阶组件对 Counter 进行包裹
export default connect(mapStateToProps, mapDispatchToProps)(Counter);

hooks
import React, { useState, useEffect, useRef } from 'react'

interface Item{
  id?:number;
  title?:string;
}
const Reg: React.FC = () => {
  //只有在没有初始值的情况下才需要加入类型限制,因为有初始值时可以推断出实际状态的类型。
  const [msg, setMsg] = useState('数据1');//有初始值,会类型推断 √
  const [msg2, setMsg2] = useState();//没有初始值,推断为any
  const [msg3, setMsg3] = useState<number>(0);//手动指定类型和初始值
  const [msg4, setMsg4] = useState<Item|null>({id:1});//手动指定类型
  const [msg5, setMsg5] = useState<any[]>([]);//手动指定类型

  // const box = useRef(null)//有时设置引用可能会在稍后的时间点发生
  const box = useRef<HTMLDivElement>(null)//使用 useRef 时需要更加明确 被引用的类型

  useEffect(() => {
    console.log('didMount');
    box.current!.style.background='red';
    return () => {
      console.log('unmount');
    };
  }, []);

  return (
    <>
      <h3>hooks</h3>
      <div>msg:{msg}</div>
      <div>msg2:{msg2}</div>
      <div>msg3:{msg3+1}</div>
      {/* ? 代表对象存在,才去访问子key */}
      <div>msg4:{msg4?.id}/{msg4?.title}</div>
      <div>
        msg5:
          {
            msg5?.map((val,index)=>(
              <li key={index}>{val}</li>
            ))
          }
      </div>
      <div ref={box}>box</div>
      <button onClick={
       ()=>{
        setMsg('更新后的msg1')
        setMsg2('更新后的msg2')//简单类型直接修改
        setMsg3(msg3+1)
        // setMsg4({id:2})//符合类型,这样做会丢掉一些东东
        setMsg4(msg4=>({...msg4,id:22}))//可以这样
        // setMsg5(msg5=>([...msg5,'bmw']))
        setMsg5([...msg5,'bmw'])//或者这样
       } 
      }>更新状态</button>
    </>
  )

}

export default Reg;

umi2

官网 项目

创建项目

mkdir project
cd project
yarn create umi

项目结构

|-public 本地数据资源
|-mock umi支持mock数据,无需代理
|-src
  |-assets 开发资源
  |-compoennts 通用组件
  |-layouts 为根布局,根据不同的路由选择return不同的布局,可以嵌套有一些和布局相关的组件
  |-pages 页面 约定式路由的页面
  |-plugins 子有的插件配置 如axios
  |-routes 授权路由
  |-app.js 运行时的配置文件  对等 react的index.js
  |- global.css 全局样式
|-umirc 编译时配置

资源指向

  • 支持@指向src别名
  • 相对指向 src
  • 绝对指向 public
  • img标签 指向public或者服务器
  • 静态图片: import | require |
  • 数据图片: 绝对指向public | 服务器

路由

路由使用约定式,对齐nuxt,也可配置umirc后使用配置型路由,路由组件同react-router解构来自umiimport {NavLink} from 'umi'

约定式路由
|-pages
  |-index.js   "/" 路由 页面
  |-index/index.js "/" 路由 页面
  
  |-goods.js  // /goods路由
  |-goods/index.js  // /goods路由页
    //goods 路由的默认页 return null 代表没有默认页
  |-goods/$id.js  // /goods/1 路由页 
  |-goods/$id$.js  // /goods 路由 goods下没有index 时 展示了 /goods 或 /goods/id,代表id可选
  |-goods/_layout.js  // /goods 路由页 有自己的展示区 {props.children} 不然会在父的展示区展示

  |-goods/category.js   // /goods/category 路由
  |-goods/category/index.js // /goods/category 路由

  |-404.js 生产模式下的404页,开发模式默认umi的
  //document.ejs 浏览器模板页 没有umi自动生成,有取当前模板结构
路由跳转
声明式
<Link to={{pathname:'/goods/2',query:{a:11,b:22}}}>商品 002</Link>
编程式
import router from 'umi/router';
					
router.push('/login')

router.push({
  pathname:'/goods/1',
  query:{a:11,b:22}
})
// search:'a=111&b=222' 如果传递了search 会覆盖query,query只是对search的封装引用了search的值

props.history.xx() //可用
传接参
props.match.params
props.location.query 返回对象

授权路由
前置
//组件内部守卫, 在组件文件最上方

/*
* title: reg Page
* Routes:
*   - ./src/routes/Auth.js
* */
后置
//同react
<Prompt
  when={true}
  message={(location) => {
    return window.confirm(`确认要去向 ${location.pathname}?`);
  }}
/>

数据交互

自带mock,无需代理,其他数据需要代理,本地数据放在public

app.js 配置

在出错时显示个 message 提示用户,在加载和路由切换时显示个 loading,页面载入完成时请求后端,根据响应动态修改路由,引入一些插件配置模块,在运行时带入这些配置

配置思想

对外导出一堆umi内部可识别的函数 来完成配置

export function render(oldRender) {
    渲染应用之前做权限校验,不通过则跳转到登录页
    oldRender() 渲染应用的函数
}

export function onRouteChange({ location, routes, action }) {
  初始加载和路由切换时的逻辑,用于路由监听, action 是路由切换方式如:push
}

export function rootContainer(container) {
  封装 root container  外层有个 Provider 要包裹 的场景 必须要有返回值,无需是可以不写这个函数
  // const DvaContainer = require('@tmp/DvaContainer').default;
  return React.createElement(DvaContainer, null, container);
}

export function modifyRouteProps(props, { route }) {
  修改传给路由组件的 props , 所有组件都中
    props,Object,原始 props
    route,Object,当前路由配置
    return { ...props, 混入后的key: 值 };
}

umirc 配置

配置编译环境 umirc 无需重启

export default {

  history:'hash' 路由模式 默认历史记录

  publicPath: "/public/" 数据资源在非根目录或cdn时使用, 必须 绝对路径 + /结尾 影响打包后的位置会指向public

  disableCSSModules: false 	关闭css模块化 默认开启,推荐开启

  cssModulesExcludes:['index.css','login.css'] 指定项目目录下的文件不走 css modules 不支持scss

  sass: {}	支持scss 需要安装 sass-loader node-sass

  mountElementId:'app' 	指定 react app 渲染到的 HTML 元素 id。

  proxy: {
    '/api': {
      target: 'http://localhost:3001',
      "changeOrigin": true,
      // pathRewrite: {'^/api' : ''}
    },
    '/douban': {
      target: 'https://douban.uieee.com',
      "changeOrigin": true,
      pathRewrite: {'^/douban' : ''},
      secure: false //接受https的代理
    }
  },

  routes: [ //使用手动配置路由,约定式路由失效
    {
      path: '/',
      component: '../layouts/index',
      routes: [
        { path: '/', component: '../pages/index.js' },
        { path: '/users/', component: '../pages/users/index.js' },
        { path: '/users/list', component: '../pages/users/list.js' },
        { path: '/users/:id', component: '../pages/users/$id.js' },
      ]
    }
  ],

  plugins: [ 插件配置
    ['umi-plugin-react', {
      antd: false, 是否开启antd 需要安装依赖
      dva: false, 是否器dva支持 需要安装依赖

      dynamicImport: {//按需加载 生产环境下有效果
        webpackChunkName: true,//实现有意义的异步文件名
        loadingComponent: './components/Loading.js',//指定加载时的loading组件路径
      },

      title: 'umitest', 开启 title 插件

      routes: {
        exclude: [	用于忽略某些路由,比如使用 dva 后,通常需要忽略 models、components、services 等目录
          /components\//,
        ],
      },
    }],
  ],
}

umi3

官网

使用react开发,可扩展的企业级前端应用框架,让react开发高度可配置(合并式,无需触碰webpack底层),支持各种功能扩展和业务需求,可替换next完成服务端渲染

Umi 如何工作

把大家常用的技术栈进行整理,收敛到一起,让大家只用 Umi 就可以完整 80% 的日常工作。CRA不支持配置(合并式),不是框架,但umi是,且支持配置,并内置插件的方式整合开发者遇到的一些常规业务问题(如 antd、dva 的深度整合,比如国际化、权限、数据流、配置式路由、补丁方案、自动化 external 方面)

技术收敛

img

创建项目

//首先得有 node,并确保 node 版本是 10.13 或以上
mkdir project
cd project
yarn create @umijs/umi-app
yarn 安装依赖
yarn start 启动开发
yarn build 打包构建

项目结构

.
├── package.json //插件和插件集 @umijs/ 开头的依赖会被自动注册为插件或插件集。
├── .umirc.ts //配置文件,包含 umi 内置功能和插件的配置
├── .env //环境变量
├── dist //打包后
├── mock //mock 文件,此目录下所有 js 和 ts 文件会被解析为 mock 
├── public //静态资源
└── src
    ├── .umi //临时文件目录 忽视
    ├── layouts/index.tsx //布局组件
    ├── pages // 页面级别路由组件
        ├── index.less
        └── index.tsx
    └── app.ts //运行时配置文件 扩展运行时的能力
		├── global.css // 全局样式,如果存在此文件,会被自动引入到入口文件最前面 无效

不希望类型检查,可以更名为jsx或者js

样式和资源指向

  • 支持@别名指向src
  • css 里面~@执向src
  • 相对 根在 src
  • 绝对根在 public
  • 静态图片引入: import | require | 指向src
  • 数据图片引入: 指向public | 服务器
  • src/global.css 为全局样式
  • 当做 CSS Modules 用时,Umi 会自动识别 如: import styles from './foo.css'

路由

路由使用约定式,对齐nuxt,也可配置umirc后使用配置型路由,路由组件同react-router解构来自umi import {NavLink} from 'umi'

约定式路由

也叫文件路由,不需要手写配置,没有 routes 配置,Umi 会进入约定式路由模式,然后分析 src/pages 目录

|-pages
  |-index.tsx   "/" 路由 页面
  |-index/index.tsx "/" 路由 页面
  
  |-goods.tsx  // /goods路由
  |-goods/index.tsx  // /goods路由页
    //goods 路由的默认页 return null 代表没有默认页
  |-goods/[id].tsx  // /goods/1 路由页 
  |-goods/_layout.tsx  // /goods 路由页 有自己的展示区 {props.children} 不然会在全局路由展示

  |-goods/category.tsx   // /goods/category 路由
  |-goods/[uid]/comment.tsx // /goods/23/comment 路由


  |-404.js 生产模式下的404页,//目前有问题 https://github.com/umijs/umi/issues/4437
  //document.ejs 浏览器模板页 没有umi自动生成,有取当前模板结构 ***
|- layouts
	|-index.tsx //全局路由。返回一个 React 组件,并通过 props.children 渲染子组件
不同的全局 layout

你可能需要针对不同路由输出不同的全局 layout,但你仍可以在 src/layouts/index.tsx 中对 location.path 做区分,渲染不同的 layout 。

比如想要针对 `/user输出简单布局,

//layouts/user.jsx
import React from 'react';

export default (props) => {
  return (
    <div>
      <h1>user layouts</h1>
      {props.children}
    </div>
  );
}

//layouts/index.jsx

import user from './user'
export default function(props) {
  if (props.location.pathname === '/user') {
    return <User>{ props.children }</User>
  }

  return (
    <>
    	...默认全局路由
      { props.children }
    </>
  );
}
扩展路由属性

支持在代码层通过导出静态属性的方式扩展路由。

import React from 'react';
import './user.css';

function User(){
  return (
    <div>
      <h1 className={'box'}>Page user</h1>
    </div>
  );
}

User.title = 'user Page';//修改路由页面标题

export default User;

其中的 title 会附加到路由配置中。

路由跳转
声明式
import {NavLink} from 'umi'
<NavLink activeStyle={{激活样式}} to="/goods/2?a=1&b=2">商品 002</NavLink>
<NavLink activeStyle={{激活样式}} to={{pathname:'/goods/2',query:{a:11,b:22}}}>商品 002</NavLink>
编程式
import {history} from 'umi';
					
history.push('/login')

history.push('/goods/1?a=1&b=2');

history.push({
  pathname:'/goods/1',
  query:{a:11,b:22},
  search:'a=111&b=222' //如果传递了search 会覆盖query,query只是对search的封装引用了search的值
})

props.history.push({
  pathname:'/goods/1',
  query:{a:111,b:222},
})
传接参
props.match.params
props.location.query 返回对象


import {useLocation,useParams} from 'umi'
let locationHooks = useLocation();
let params = useParams();

{locationHooks.pathname}|{params.uid}
授权路由
前置
//app.ts  全局
import { history } from 'umi';

export function render(oldRender) {
  fetch('/api/auth').then(auth => {
    if (auth.isLogin) { oldRender() }
    else { history.push('/login'); }
  });
}

//路由级别  路由独享
//umirc
routes: [
  { path: '/user', component: 'user',
    wrappers: [
      '@/wrappers/auth',
    ],
  },
  { path: '/login', component: 'login' },
]

//auth
export default (props) => {
  const { isLogin } = useAuth();
  if (isLogin) {
    return <div>{ props.children }</div>;
  } else {
    redirectTo('/login');
  }
}
后置
//同react
<Prompt
  when={true}
  message={(location) => {
    return window.confirm(`确认要去向 ${location.pathname}?`);
  }}
/>
异步路由

组件体积太大,不适合直接计入 bundle 中,以免影响首屏加载速度

//启用按需加载  p__index__index.chunk.css	p__index__index.js
// umirc
export default {
  dynamicImport: {},
}
配置型路由

umirc 中通过 routes 进行配置,格式为路由信息的数组

routes: [
  { path: '/login', component: 'login' },// 不写路径从 src/pages找组件
  { path: '/reg', component: 'reg' },
	
  //不使用全局layout的配置可以写在 / 的上面
  
  {
    path: '/',
    component: '@/layouts/index',
    routes: [//通常在需要为多个路径增加 layout 组件时使用
      { path: '/index', component: 'index' },
      // { exact: true, path: '/goods', component: '@/pages/goods' },
      { path: '/goods', component: '@/pages/goods/index' },
      {
        path: '/goods/:uid',
        component: '@/layouts/goods-detail',//为uid层级页面指定layout
        routes: [
          { path: '/goods/:uid', component: '@/pages/goods/[uid]' },
          { path: '/goods/:uid/comment', component: '@/pages/goods/[uid]/comment' },
          { component: '@/pages/404' }//子集的404,每一级都可以设定404
        ],
      },
      {
        path: '/user',
        component: '@/layouts/user',
        routes: [
          { path: '/user', component: '@/pages/user/index' },
          { path: '/user/:id', component: '@/pages/user/[id]' },
          { component: '@/pages/404' },
        ],
      },
      { path:'/', redirect: '/index' }, //跳转
      { component: '@/pages/404' },
    ],
  }

]

数据交互

mock

Umi 约定 /mock 文件夹下所有文件为 mock 文件无需代理,其他服务器数据需要代理

import mockjs from 'mockjs'
export default {
  // 支持值为 Object 和 Array
  
  'GET /umi/goods': [{id:1,name:'韭菜'},{id:2,name:'西红柿'}],

  // 支持自定义函数,API 参考 express@4
  'POST /umi/login': (req, res) => {
    // 添加跨域请求头
    // res.setHeader('Access-Control-Allow-Origin', '*');
    console.log('req',req.body);//完成业务
    res.send({
      err: 404,
      msg:'登录失败1'
    });
  },

  //引入mockjs

  'GET /umi/goods/home': mockjs.mock({
    'list|100': [{ name: '@city', 'value|1-100': 50, 'type|0-2': 1 }],
  }),

}

//模拟延时
import { delay } from 'roadhog-api-doc'
export default delay({同上},1000)

貌似不支持resFulApi 风格请求,jsonserver支持,值得考虑

代理
//umirc 
proxy: {
  '/api': {
    target: 'http://localhost:9001', //node服务
    "changeOrigin": true,
    // pathRewrite: {'^/api' : ''}
  },
  '/mock': {
    target: 'http://localhost:3333', //自建的jsonserver服务
    "changeOrigin": true,
    // pathRewrite: {'^/mock' : ''},
    // secure: false //接受https的代理
  }
}
axios
//  plugins/axios.js 
import axios from 'axios';

//添加一个请求的拦截
axios.interceptors.request.use(function (config) {

  config.headers={
    'token':'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFsZXgiLCJfaWQiOiI1ZThhMGQ2MzczNDg2MDIzYTRmZDY4ZGYiLCJpYXQiOjE1ODkwMDIzMDUsImV4cCI6MTU4OTA4ODcwNX0.sStWoKBk2mYwa_1-AJOQobL7LBR82DnOseCeTds5ECs'
  };

  console.log('axios拦截器');

  return config;
}, function (error) {
  return Promise.reject(error);
});

// 添加一个响应的拦截
axios.interceptors.response.use(function (response) {
  return response;
}, function (error) {
  return Promise.reject(error);
});


// React.axios = axios;//实例属性 无效
// window.axios = axios; //全局API 无效

export default axios;//需要引入plugins下的axios 才有拦截

插件

umi3提供的插件(内置),基本上无需安装,无需配置,直接使用即可

scss

umi 默认支持 less,要使用scss需要添加插件@umijs/plugin-sass,默认已安装,支持 Dart Sass 切换到 Node Sass,需安装 node-sass 依赖

yarn add @umijs/plugin-sass -D //无需配置就可以支持dart scss
yarn add node-sass -D // 使用node-sass时 需要的依赖

//使用node-sass时的配置 umirc
sass: {
  implementation: require('node-sass'),
}
antd

内置插件,默认开启,直接使用,对齐antd使用

import { Button,message } from 'antd';
<Button 
	type="primary" 
	onClick={()=>message.info('message')}
>Primary</Button>
request
介绍

网络请求库,基于 fetch 封装, 兼具 fetch 与 axios 的特点, 旨在为开发者提供一个统一的api调用方式, 简化使用

特性requestfetchaxios
实现浏览器原生支持浏览器原生支持XMLHttpRequest
大小9k4k (polyfill)14k
query 简化
post 简化
超时
缓存
错误检查
错误处理
拦截器
前缀
后缀
处理 gbk
中间件
取消请求
基本用法

request内置插件,默认开启,直接使用,使用对齐 umi-request@umijs/hooksuseRequest

import {request} from 'umi'
// useRequest 接收了一个异步函数 getUsername ,在组件初次加载时, 自动触发该函数执行。同时 useRequest 会自动管理异步请求的 loading , data , error 等状态
//request 对齐 axios

export default (props) => {
  //data ~~ axios的res.data.data
  //如果数据里面没有data 返回undefined
  //通过配置 umirc request.dataField 可以指定
  console.log('data',data)

  useEffect(()=>{
		//request ~~ axios
    request('/api/goods/home',{params:{_limit:1}}).then(
    	//res ~~ axios的res.data
    ).catch()

  },[]);

  return (
    <div>

    </div>
  );
}
拦截器
//app.ts
export const request = {
  // timeout: 1000,
  // errorConfig: {},
  // middlewares: [],
  requestInterceptors: [
    (url, options)=>{// 请求地址 配置项
      options.headers={token:''}
      return {url,options}
    }
  ],
  responseInterceptors: [
    (response, options) => {//响应体 请求时的配置项
      console.log(response,options)
      return response;
    }
  ],
};
useRequest钩子
介绍

文档

一个强大的管理异步数据请求的 Hook.

核心特性

  • 自动请求/手动请求
  • SWR(stale-while-revalidate)
  • 缓存/预加载
  • 屏幕聚焦重新请求
  • 轮询
  • 防抖
  • 节流
  • 并行请求
  • loading delay
  • 分页
  • 加载更多,数据恢复 + 滚动位置恢复
  • 错误重试
  • 请求超时管理
  • suspense
用法

在组件初次加载时, 自动触发该函数执行。同时 useRequest 会自动管理异步请求的 loading , data , error 等状态。

import {useRequest} from 'umi'

export default function  RequestHooks(){

  // 用法 1
  const { data, error, loading } = useRequest('/mock/home');

	// 用法 2
  const { data, error, loading } = useRequest({
    url: '/mock/home',
    params:{_limit:1}
  });

	// 用法 3
	const { data, error, loading } = useRequest((id)=> `/api/home/${id}`); //?

	// 用法 4
  const { data, loading, run } = useRequest((_limit) => ({
    url: '/mock/home',
    params: { _limit }
  }), {
    manual: true,//手动通过运行run触发
  });
  
  // 轮询
  const { data, loading, run } = useRequest((_limit) => ({
    url: '/mock/home',
    params: { _limit }
  }), {
    manual: true,//手动通过运行run触发
    pollingInterval:1000,//轮询 一秒读一次
    pollingWhenHidden:false,//屏幕不可见时,暂停轮询
  });
  
  if (error) {
    return <div>failed to load</div>
  }

  if (loading) {
    return <div>loading...</div>
  }

  return (
    <div>{JSON.stringify(data)}</div>
    <button onClick={()=>run(1)}>手动</button>
  );
}
并行请求

通过 options.fetchKey ,可以将请求进行分类,每一类的请求都有独立的状态

import { useRequest } from 'umi';
import { Button } from 'antd';

export default () => {
  const { run, fetches } = useRequest((userId)=>({
    url: '/mock/home',
    params:{_limit:1,_page:userId-0}
  }), {
    manual: true,
    fetchKey: id => id,
    onSuccess:(res,params)=>{
      console.log(res, params)
    }
  });

  const users = [{ id: '1', username: 'A' }, { id: '2', username: 'B' }, { id: '3', username: 'C' }];

  return (
    <div>
      <h3>并行请求:单击所有按钮,每个请求都有自己的状态</h3>
      <ul>
        {users.map((user => (
          <li key={user.id} style={{ marginTop: 8 }}>
            <Button loading={fetches[user.id].loading} onClick={() => { run(user.id) }}>读取</Button>
          </li>
        )))}
      </ul>
    </div>
  );
};
防抖-解流-缓存
import { useRequest, request } from 'umi';
import { Input  } from 'antd';
import React from 'react';

let {Search}=Input;

function getHome(search) {
  console.log(1,search)
  return request('/mock/home',{params:{_page:search-0, _limit:1}})
}

export default () => {
  const { data, loading, run, cancel } = useRequest(getHome, {
    // debounceInterval: 500, //频繁调用 run 以防抖策略进行请求
    // throttleInterval: 500,//频繁触发 run ,则会以节流策略进行请求
    // cacheKey:'homepage', //缓存 回退路由,在进入数据data还在
    manual: true
  });

  return (
    <div>
      <h3>防抖</h3>
      <Search
        placeholder="输入页数"
        onChange={e=>run(e.target.value)}
        onBlur={cancel}
        style={{ width: 300 }}
        loading={loading}
      />
      <br/>
      {data && JSON.stringify(data)}
    </div>
  );
};
激活聚焦重求-loading防闪烁
import { useRequest } from 'umi';
import { Spin } from 'antd';
import React from 'react';

export default () => {
  const { data, loading } = useRequest({
    url:'/mock/home',
    params:{_limit:1,_page:1}
  }, {
    refreshOnWindowFocus: true,//浏览器窗口 refocus 和 revisible 时,会重新发起请求
    focusTimespan: 1000,//请求间隔,默认为 5000ms 。
    // loadingDelay:1500 // loading防闪烁 可以延迟 loading 变成 true 的时间,有效防止闪烁
  })

  return (
    <div>
      <p>屏幕聚焦重新请求</p>
      <Spin spinning={loading}>
        <div>{data && JSON.stringify(data)}</div>
      </Spin>
    </div>
  )
}
状态变化重请求
import { useRequest } from '@umijs/hooks';
import { Spin, Select } from 'antd';
import React, { useState } from 'react';

export default () => {
  const [pageNum, setPageNum] = useState('1');

  const { data, loading } = useRequest(() => ({
    url:'/mock/home',
    params:{_limit:1,_page:pageNum}
  }), {
    refreshDeps: [pageNum]//pageNum变化时,会使用之前的 params 重新执行
  });

  return (
    <div>
      <Select onChange={setPageNum} value={pageNum} style={{ marginBottom: 16, width: 120 }}>
        <Select.Option value="1">page 1</Select.Option>
        <Select.Option value="2">page 2</Select.Option>
        <Select.Option value="3">page 3</Select.Option>
      </Select>
      <Spin spinning={loading}>
        home: {data && JSON.stringify(data)}
      </Spin>
    </div>
  );
};

app.ts 配置

运行时配置,跑在浏览器端, 如:在出错时显示个 message 提示用户,在加载和路由切换时显示个 loading,页面载入完成时请求后端,根据响应动态修改路由,引入一些插件配置模块,在运行时带入这些配置

配置思想

对外导出一堆umi内部可识别的函数 来完成配置

//修改路由
export function patchRoutes({ routes }) {
  //比如在最前面添加一个 /foo 路由
  routes.unshift({
    path: '/foo',
    exact: true,
    component: require('@/extraRoutes/foo').default,
  });
}

//权限校验
import { history } from 'umi';

export function render(oldRender) {
  fetch('/api/auth').then(auth => {
    if (auth.isLogin) { oldRender() }
    else { history.push('/login'); }
  });
}

//动态更新路由
let extraRoutes;
export function patchRoutes({ routes }) {
  merge(routes, extraRoutes);
}
export function render() {
  //请求服务端根据响应
  fetch('/api/routes').then((res) => { extraRoutes = res.routes })
}

export function onRouteChange({matchedRoutes, location, routes, action }) {
  //初始加载和路由切换时的逻辑,用于路由监听, action 是路由切换方式如:push
  //用于做埋点统计
  //动态设置标题
  document.title = matchedRoutes[matchedRoutes.length - 1].route.title || ''
}

export function rootContainer(container,args) {
  //封装 root container  外层有个 Provider 要包裹 的场景 必须要有返回值,无需是可以不写这个函数
  // const DvaContainer = require('@tmp/DvaContainer').default;
  return React.createElement(DvaContainer, null, container);
  
  //args 包含:

    //routes,全量路由配置
    //plugin,运行时插件机制
    //history,history 实例
}

export function modifyRouteProps(props, { route }) { // ***
  	//修改传给路由组件的 props , 所有组件都中
    //props,Object,原始 props
    //route,Object,当前路由配置
    return { ...props, 混入后的key: 值 };
}

umirc 配置

配置编译环境 umirc 无需重启, 推荐在 .umirc.ts 中写配置。如果配置比较复杂需要拆分,可以放到 config/config.ts 中,并把配置的一部分拆出去,比如路由。

两者二选一,.umirc.ts 优先级更高。 文档

import { defineConfig } from 'umi';

export default defineConfig({
	
  //设置 node_modules 目录下依赖文件的编译方式
  nodeModulesTransform: {
    type: 'none',
  },


  // 配置型路由 权重高于约定式 且不可合并
  // routes: [
  //   { component: '@/pages/404' },
  // ],

  history: { type: 'hash' }, //哈希路由模式,解决强刷,或者通过后端解决

  //关闭mock
  // mock: false,

  //多个代理 mock数据无需代理
  proxy: {
    '/api': {
      target: 'http://localhost:9001',
      "changeOrigin": true,
      // pathRewrite: {'^/api' : ''}
    },
    '/mock': {
      target: 'http://localhost:333',
      "changeOrigin": true,
      // pathRewrite: {'^/mock' : ''},
      // secure: false //接受https的代理
    }
  },

  //按需加载功能默认是关闭的
  dynamicImport: {
    loading: '@/loading', //定义按需加载时的loading组件
  },

  title: 'hi',//配置应用统一标题

  mountElementId:'app',//指定 react app 渲染到的 HTML 元素 id。

  devServer:{
    port:8082
  },

  favicon: '/favicon.ico',//使用本地的图片,图片请放到 public 目录

  //配置 <head> 里的额外脚本,数组项为字符串或对象
  headScripts: [
    `alert(1);`,
    `http://code.jquery.com/jquery-2.1.1.min.js`,
  ],

  //配置 <head> 额外 link 和style
  styles: [
    `body { color: red; }`,
    `https://a.com/b.css`,
  ],

  
});

SSR


参考

dva

dva = React-Router + Redux + Redux-saga 项目

核心

  • View:React 组件构成的视图层

  • Action:一个对象,描述事件

  • connect 方法:一个函数,绑定 State 到 View

  • dispatch 方法:一个函数,发送 Action 到 State

  • model 数据管理模块

    • State:一个对象,保存整个应用状态
    • reducers: 一个对象 同步业务
    • effects: 一个对象 处理异步
    • subscriptions: 订阅数据源

    img

umi2下使用

umirc配置
antd: true, 开启antd
dva: true,  开启dva数据流
routes: {//路由
  exclude: [//排除
    /models\//,
    /services\//,
    /model\.(t|j)sx?$/,
    /service\.(t|j)sx?$/,
    /components\//,
  ],
},
app配置
export const dva = {
  config: { 
    onError(err) {//监听错误
      err.preventDefault();
      console.error('dva config',err.message);
    },

    //初始数据 不给就取根models的state,给了就不取
    initialState: {
      namespace: {
        key:value
      },
    },
  },
  plugins: [
    // require('dva-logger')(),
  ],
}
model
// src/models 全局
// src/pages/models 页面

export default {
  namespace: 'global',//所有models里面的namespace不能重名
  state: { //存放数据
    stateName: stateValue,
  },
  reducers: { //处理同步 左key 等于dispatch({type:key
    reducersKey(state,{type,payload}) {
      return {
        ...state,
        stateName: newValue,
      };
    }
  },
  effects: {	//处理异步 左key 等于dispatch({type:key
    *login(action, { call, put, select }) {

      //	call:执行异步函数  如: const result = yield call(fetch, '/todos');
      //	put:发出一个 Action,类似于 dispatch
      //	select: 从state里获取数据 如: const todos = yield select(state => state.todos);

      const res = yield call(fetch,'/mock/home')
      const data = yield res.json();

      yield put({
        type: 'reducerKey'|'effectsKey',
      });

    },
  },

  subscriptions: {
    //场景: 时间、服务器的 websocket 连接、keyboard 输入、geolocation 变化、history 路由变化
    //订阅一个数据源 根据条件 dispatch 需要的 action
    //subsription中的方法名是随意定的,每次变化都会一次去调用里面的所有方法

    随意的key({ dispatch, history }) {

      //路由监听
      history.listen(({ pathname, query }) => {});

      //需要导入import key from 'keymaster' 监听键盘
      key('⌘+i, ctrl+i', () => { dispatch({type:reducersKey|effectsKey}) });

      //窗口变化
      window.onresize|onscroll = function(){
        console.log('onresize')
      }

    }
  }
  
}
页面

layouts、组件接入 dva

import {connect} from 'dva'
function 组件(props){
  props.propname
  props.dispatch({type,payload})
    //type:'namespace/reducersKey|effectsKey'
    //type:'namespace/effectsKey'
}
function mapStateToProps(state) {
  return {
    propname: state.namespace.stateKey,
    propname: state.namespace
  };
}
export default connect(mapStateToProps)(组件);

//layouts
import withRouter from 'umi/withRouter';
import {connect} from 'dva'
export default withRouter(connect(mapStateToProps)(组件));

umi3下使用

umi3以插件的形式整合 dva 数据流,配置默认开启

文档

model
// src/models/modelname.js 全局


import { history } from 'umi';
import key from 'keymaster';

export default {
  namespace: 'global',//所有models里面的namespace不能重名
  state: {
    title:'UMI+DVA',
    text: '我是全局text',
    login: false,
    a:'全局models aaaa',
  },
  reducers: {//处理同步 左key 等于dispatch({type:key
    setText(state) {
      return {
        ...state,
        text: '全局设置 后的text'+Math.random().toFixed(2),
      };
    },
    setTitle(state,action) {
      return {
        ...state,
        text: `全局设置 后的title${action.payload.a}/${action.payload.b}`,
      };
    },
    signin:(state)=>({
      ...state,
      login: true,
    }),
  },
  effects: {
    //处理异步 左key 等于dispatch({type:key
    //call:执行异步函数
      // const result = yield call(fetch, '/todos');
    //put:发出一个 Action,类似于 dispatch
    //select: 从state里获取数据
      //const todos = yield select(state => state.todos);
    *login(action, { call, put, select }) {
      const res = yield call(fetch,'/umi/goods/home')
      const data = yield res.json();
      console.log('*login',data);

      yield put({
        type: 'signin',
      });

      yield put(history.push('/'));
      // yield put(routerRedux.push('/'));
    },


    *throwError(action, effects) {
      console.log(effects);
      throw new Error('全局effects 抛出的 error');
    },
  },
  subscriptions: {
    //场景: 时间、服务器的 websocket 连接、keyboard 输入、geolocation 变化、history 路由变化
    //订阅一个数据源 根据条件 dispatch 需要的 action
    //subsription中的方法名是随意定的,每次变化都会一次去调用里面的所有方法
    listenRoute({ dispatch, history }) {
      history.listen(({ pathname, query }) => {
        console.log('global subscriptions',pathname,query);//根据不同pathname加载不同数据发actions给reducers组件绑定state就好
      });
    },
    listenKeyboard({dispatch}) {//监听键盘
      key('⌘+i, ctrl+i', () => { dispatch({type:'setText'}) });
    },
    listenResize({dispatch}) {//监听窗口变化
      window.onresize = function(){
        console.log('onresize')
      }
    },
    listenScroll({dispatch,history,done}){
      window.οnscrοll=function () {
        console.log('onscroll')
      }


    }
  },
};


// src/pages/pagename/model.js 页面
//model下面如果 没有多个state key 可以不用出现models目录
export default {
  namespace: 'count',
  state: 0,//count:0
  reducers: {
    increase(state) {
      return state + 1;
    },
    decrease(state) {
      return state - 1;
    },
  }
};

// src/pages/pagename/models/modelname.js 页面
export default {
  namespace: 'a',  //组件 通过state.a 得到 'goods page data a'
  state: 'goods page data a',
  reducers: {},
};
组件

layouts、组件接入 dva

import {connect} from 'umi'
function 组件(props){
  props.propname
  props.dispatch({type,payload})
    //type:'namespace/reducersKey|effectsKey'
    //type:'namespace/effectsKey'
}
function mapStateToProps(state) {
  return {
    propname: state.namespace.stateKey,
    propname: state.namespace
  };
}
export default connect(mapStateToProps)(组件);

//layouts
import {withRouter, connect} from 'umi'
export default withRouter(connect(mapStateToProps)(组件));

@umijs/hooks

文档

useDrop & useDrag

一对帮助你处理在拖拽中进行数据转移的 hooks

useDrop 可以单独使用来接收文件、文字和网址的拖拽。

useDrag 允许一个 dom 节点被拖拽,需要配合 useDrop 使用。

向节点内触发粘贴时也会被视为拖拽的内容

使用
import React from 'react';
import { useDrop, useDrag } from '@umijs/hooks';

export default () => {
  const getDragProps = useDrag();
  //props  需要透传给接受拖拽区域 dom 节点的 props
  //isHovering 是否是拖拽中,且光标处于释放区域内
  const [props, { isHovering }] = useDrop({
    onText: (text, e) => {
      console.log(text, e);
    },
    onFiles: (files, e) => {
      console.log(e, files);
    },
    onUri: (uri, e) => {
      console.log(uri, e);
    },
    onDom: (content, e) => {
      console.log(content, e);
    },
  });

  return (
    <div>
      <div>useDrop 可以单独使用来接收文件、文字和网址的释放</div>
      <div>useDrag 允许一个 dom 节点被拖拽,需要配合 useDrop 使用</div>
      <div>向节点内触发粘贴时也会被视为拖拽的内容</div>

      <div style={{ border: '1px dashed #e8e8e8', padding: 16, textAlign: 'center' }} {...props}>
        {isHovering ? '撒手' : '拖到这'}
      </div>

      <div style={{ display: 'flex', marginTop: 8 }}>
        <div
          {...getDragProps('box')}
          style={{ border: '1px solid #e8e8e8', padding: 16, width: 80, textAlign: 'center', marginRight: 16 }}
        >
          box
        </div>
      </div>
    </div>
  );
};
useDrag Result
参数说明类型
getDragProps一个接收拖拽的值,并返回需要透传给被拖拽节点 props 的方法(content: any) => props
useDrop Result
参数说明类型
props需要透传给接受拖拽区域 dom 节点的 props-
isHovering是否是拖拽中,且光标处于释放区域内boolean
useDrop Params
参数说明类型默认值
onText拖拽文字的回调(text: string, e: Event) => void-
onFiles拖拽文件的回调(files: File[], e: Event) => void-
onUri拖拽链接的回调(text: string, e: Event) => void-
onDom拖拽自定义 dom 节点的回调(content: any, e: Event) => void-

useVirtualList

解决展示海量数据渲染时首屏渲染缓慢和滚动卡顿问题

使用
import React from 'react';
import { useVirtualList } from '@umijs/hooks';

export default () => {
  //list 当前需要展示的列表内容  {data: T, index: number}[]
  // containerProps  滚动容器的 props  {}
  // wrapperProps  children 外层包裹器 props {}
  const { list, containerProps, wrapperProps } = useVirtualList(Array.from(Array(99999).keys()), {
    overscan: 30,//视区上、下额外展示的 dom 节点数量
    itemHeight: 60,//行高度,静态高度可以直接写入像素值,动态高度可传入函数
  });
  return (
    <>
      <div {...containerProps} style={{ height: '300px', overflow: 'auto' }}>
        <div {...wrapperProps}>
          {list.map((ele, index) => (
            <div
              style={{
                height: 52,
                display: 'flex',
                justifyContent: 'center',
                alignItems: 'center',
                border: '1px solid #e8e8e8',
                marginBottom: 8,
              }}
              key={ele.index}
            >
              Row: {ele.data}
            </div>
          ))}
        </div>
      </div>
    </>
  );
};
API
const result:Result = useVirtualList(originalList: any[], Options);
Result
参数说明类型
list当前需要展示的列表内容{data: T, index: number}[]
containerProps滚动容器的 props{}
wrapperPropschildren 外层包裹器 props{}
scrollTo快速滚动到指定 index(index: number) => void
Params
参数说明类型默认值
originalList包含大量数据的列表T[][]
options可选配置项,见 Options--
Options
参数说明类型默认值
itemHeight行高度,静态高度可以直接写入像素值,动态高度可传入函数number | ((index: number) => number)-
overscan视区上、下额外展示的 dom 节点数量number10

useEventTarget

常见表单控件(通过 e.target.value获取表单值) 的 onChange 跟 value 逻辑封装,支持 自定义值转换 跟 重置 功能

使用
import React  from 'react';
import { Input, Button } from 'antd';
import { useEventTarget } from '@umijs/hooks'

export default () => {
  //value  表单控件的值 T
  // onChange  表单控件值发生变化时候的回调 (e: { target: { value: T }}) => void
  // reset 重置函数 () => void
  const [valueProps, reset] = useEventTarget('初始值');

  return (<>
      <Input {...valueProps} style={{ width: 200, marginRight: 20 }}/>
      <Button type="primary" onClick={reset}>重置</Button>
    </>
  );
};
API
const [ { value, onChange }, reset ] = useEventTarget<T, U>(initialValue?: T, transformer?: (value: U) => T );
Result
参数说明类型
value表单控件的值T
onChange表单控件值发生变化时候的回调(e: { target: { value: T }}) => void
reset重置函数() => void
Params
参数说明类型默认值
initialValue?可选项, 初始值T-
transformer?可选项,可自定义回调值的转化(value: U) => T-

umi+dva项目

开发

部署

项目1

项目2

umi+dva+ts

项目3

项目4

React-Native

React-Native介绍

React Native (简称RN)是Facebook于2015年4月开源的跨平台移动应用开发框架,是Facebook早先开源的UI框架 React 在原生移动应用平台的衍生产物,目前支持iOS和安卓两大平台。RN使用Javascript语言,类似于HTML的JSX,以及CSS来开发移动应用,因此熟悉Web前端开发的技术人员只需很少的学习就可以进入移动应用开发领域。
  React Native使你能够在Javascript和React的基础上获得完全一致的开发体验,构建世界一流的原生APP。React Native着力于提高多平台开发的开发效率 —— 仅需学习一次,编写任何平台。(Learn once, write anywhere),官网

Native App

	即原生开发模式,开发出来的是原生程序,不同平台上,Android和iOS的开发方法不同,开发出来的是一个独立的APP,能发布应用商店,有如下优点和缺点。

优点:

  • 直接依托于操作系统,交互性最强,性能最好
  • 功能最为强大,特别是在与系统交互中,几乎所有功能都能实现

缺点:

  • 开发成本高,无法跨平台
  • 升级困难
  • 维护成本高

Web App

	即移动端的网站,将页面部署在服务器上,然后用户使用各大浏览器访问,不是独立APP,无法安装和发布Web网站一般分两种,MPA(Multi-page Application)和SPA(Single-page Application)。而Web App一般泛指后面的SPA形式开发出的网站(因为可以模仿一些APP的特性),有如下优点和缺点。

优点:

  • 开发成本低,可以跨平台,调试方便
  • 版本升级容易
  • 维护成本低
  • 无需安装 App,不占用手机内存(通过浏览器即可访问)

缺点:

  • 性能低,用户体验差
  • 依赖于网络,页面访问速度慢,耗费流量
  • 功能受限,大量功能无法实现(无法调用原生 API)
  • 临时性入口,用户留存率低

Hybrid App

即混合开发,也就是半原生半Web的开发模式,有跨平台效果,实质最终发布的仍然是独立的原生APP(各种的平台有各种的SDK),这是一种 Native App 和 Web App 折中的方案,保留了 Native App 和 Web App 的优点。
优点:

  • 开发成本较低,可以跨平台,调试方便
  • 维护成本低,功能可复用
  • 更新较为自由(只下载资源不更新 apk )
  • 学习成本较低(前端开发人员不用学习底层 api)
  • 功能更加完善,性能和体验要比起web app 好

缺点:

  • 相比原生,性能仍然有较大损耗
  • 不适用于交互性较强的app(主要适用于新闻阅读类与信息展示类的 APP)

React Native App

	Facebook发起的开源的一套新的APP开发方案,Facebook在当初深入研究Hybrid开发后,觉得这种模式有先天的缺陷,所以果断放弃,转而自行研究,后来推出了自己的“React Native”方案,不同于H5,也不同于原生,更像是用JS写出原生应用,有如下优点和缺点

优点:

  • 开发成本在 Hybrid 和 Native 开发之间 ,大部分代码还是可复用的,
  • 性能体验高于Hybrid,性能相比原生差别不大
  • 技术日益成熟,发展迅猛

缺点:

  • 门槛相对 Web App 与 Hybrid App 来说相对高一点(也需要了解 Native 层)

开发模式的对比

img

环境搭建

使用expo-cli开发react应用程序需要两种工具:本地开发工具和用于打开应用程序的移动客户端,需要在计算机上安装Node.js(版本10或更新版本)

安装Expo CLI 到pc开发机器
npm install -g expo-cli

手机上安装 expo client

从Play商店下载Android版从App Store 下载iOS版

  • 真机测试 更真实
  • 打开手机expo client->profile->options->sigin in 注册一个账号 并 登陆
  • 安卓真机需要 打开usb调试
pc上装模拟器
  • pc机模拟手机来测试 速度快
  • for windows: 夜游,逍遥… 推荐: mumu 网易模拟器,百度一下
    • 在mumu模拟器上安装 apk -> expo client安装到模拟器 -> 允许expo在其他应用上层显示->登陆expo账号
  • for ios: Xcode, 目前只有安卓模拟器,没有好用的苹果模拟器

创建react-native项目

expo init 目录

选择模板: (Use arrow keys)
  ----- Managed workflow -----
> blank         空模板 
  tabs          带路由

cd 目录
npm start | yarn start  看你init时用的是什么工具安装,这里就用什么工具

? 回车   查看expo帮助
s 回车   登录expo账号   输入在手机expo注册的账号, 登录只需要做一次
shift + r 重启
npm start | yarn start 

注意

  • 保持 手机上expo的账号 和项目创建时 s 的账号一致
  • 保持pc和手机在同一网段,后期热刷新
  • 卡顿时,摇手机 refresh -> pc 机 npm | yarn start + 手机expo client重开

打包apk

npm install -g expo-cli  已安装侧跳过

配置app.json

{
  "expo": {
    "name": "应用名称",
    "slug": "expo-test-win10", //上传到expo时的目录名
    "icon": "./assets/icon.png", //桌面图标
    "splash": {
      "image": "./assets/splash.png",//欢迎图片
      "resizeMode": "contain",
      "backgroundColor": "#ffffff"
    },
    "updates": {
      "fallbackToCacheTimeout": 0
    },
    "assetBundlePatterns": [
      "**/*"
    ],
    "ios": {
      "supportsTablet": true
    },
    "android": {
      "package": "top.uncle9.expo"  //添加安卓的package属性,域名倒放,有无域名无所谓	
    }
  }
}

expo build:android  打包到安装apk,ios需要开发id

1) Let Expo handle the process!		√  让Expo为您生成密钥库
2) I want to upload my own keystore! 上传自己的

Published完成后有个在线地址,或者登陆expo账号去下载apk,之后安卓到模拟器,或真机官网参考

样式

	所有的核心组件都接受名为`style`的属性,使用了驼峰命名法,例如将`background-color`改为`backgroundColor`,可以传入一个数组——在数组中位置居后的样式对象比居前的优先级更高,这样你可以间接实现样式的继承,尺寸都是无单位的,表示的是与设备像素密度无关的逻辑像素点

用法1: <Text style={{key:value,key:value}}>xx</Text>

用法2: <Text style={[{key:value}],[{key:value}]}>xx</Text>

用法3: <Text style={styles.red}>xx</Text>

const styles = StyleSheet.create({
  red: {
    color: 'red',
  },
});

布局

React Native 中使用 flexbox 规则来指定某个组件的子元素的布局,flexDirection的默认值是column而不是row,而flex也只能指定一个数字值。布局图解

相对定位

React Native 中,position 默认值为 relative,即相对布局。

  • 如果父组件没有设置 flex 值,则子组件不论是否设置 flex ,子组件都不会显示在界面上

如下示例代码中,子组件会等比例的占满屏幕

return (
      <View style={{}}>
        <View style={{ flex: 1, backgroundColor: 'yellow' }}></View>
        <View style={{ flex: 1, backgroundColor: 'skyblue' }}></View>
        <View style={{ flex: 1, backgroundColor: 'lightgray' }}></View>
      </View >
    );

  • 如果父组件设置了 flex 值,子组件设置了 flex 值的同时,也设置了高度值,则高度无效

如下示例代码中,子组件会等比例的占满屏幕,设置高度并不影响所占的比例

return (
      <View style={{
              flex: 1,
      }}>
        <View style={{ flex: 1, height: 90, backgroundColor: 'yellow' }}></View>
        <View style={{ flex: 1, backgroundColor: 'skyblue' }}></View>
        <View style={{ flex: 1, backgroundColor: 'lightgray' }}></View>
      </View >
    );

  • 如果父组件设置了 flex 值,子组件没有设置 flex 值,只设置了高度值,则高度有效

如下示例代码中,子组件在界面所占的比例受高度控制,最后一个子组件则自动占满剩余空间

return (
      <View style={{
              flex: 1,
              backgroundColor: 'yellow',
      }}>
        <View style={{ height: 90, backgroundColor: 'yellow' }}></View>
        <View style={{ height: 90, backgroundColor: 'skyblue' }}></View>
        <View style={{ flex: 1, backgroundColor: 'lightgray' }}></View>
      </View >
    );

绝对布局

样式设置 position: ‘absolute’

  • 绝对布局情况下,设置 flex 不能达到页面整屏占满的效果,需要同时设置组件的宽高,否则父组件与子组件都将无法显示

正确设置父组件的样式方式

return (
      <View style={{
              position: 'absolute',
              backgroundColor: 'yellow',
              width: Dimensions.get('window').width,
              height: Dimensions.get('window').height
      }}>
        <View style={{ flex: 1, backgroundColor: 'yellow' }}></View>
        <View style={{ flex: 1, backgroundColor: 'skyblue' }}></View>
        <View style={{ flex: 1, backgroundColor: 'lightgray' }}></View>
      </View >
    );

  • 只设置父组件的宽,设置子组件的高度有效,子组件的flex无效

如下示例代码中,只有第一个设置了高度的view会显示在界面上,而设置了flex的view不会显示

return (
      <View style={{
              position: 'absolute',
              backgroundColor: 'yellow',
              width: Dimensions.get('window').width,
      }}>
        <View style={{ height: 90, backgroundColor: 'yellow' }}></View>
        <View style={{ flex: 1, backgroundColor: 'skyblue' }}></View>
        <View style={{ flex: 1,  backgroundColor: 'lightgray' }}></View>
      </View >
    );

  • 只设置父组件的高,则设置子组件的高度和flex均无效,父组件与子组件都无法显示

如下示例代码中,父组件与子组件都不会显示

return (
      <View style={{
              position: 'absolute',
              backgroundColor: 'yellow',
              height: Dimensions.get('window').height
      }}>
        <View style={{ height: 90, backgroundColor: 'yellow' }}></View>
        <View style={{ flex: 1, backgroundColor: 'skyblue' }}></View>
        <View style={{ flex: 1,  backgroundColor: 'lightgray' }}></View>
      </View >
    );

  • 同时设置了父组件的宽和高的前提下,再设置子组件的高度和flex均有效

如下示例代码中,分别设置了高度和flex的view均会显示在界面上

return (
      <View style={{
              position: 'absolute',
              backgroundColor: 'yellow',
              width: Dimensions.get('window').width,
      }}>
        <View style={{ height: 90, backgroundColor: 'yellow' }}></View>
        <View style={{ flex: 1, backgroundColor: 'skyblue' }}></View>
        <View style={{ flex: 1, backgroundColor: 'lightgray' }}></View>
      </View >
    );

  • 父组件与子组件同时绝对布局的情况下,且设置绝对布局的子组件写在最后一个时,保持原则一样,则父组件与子组件可正常显示

如下示例代码中,子组件均能正常显示,且设置了flex的view会将剩余的屏幕占满显示,不会被绝对布局的同级子view阻挡

return (
      <View style={{
              position: 'absolute',
              backgroundColor: 'yellow',
              width: Dimensions.get('window').width,
              height: Dimensions.get('window').height
      }}>
        <View style={{ height: 90, backgroundColor: 'yellow' }}></View>
        <View style={{ flex: 1, backgroundColor: 'skyblue' }}></View>
        <View style={{ position: 'absolute', width: Dimensions.get('window').width, height: 90, bottom: 90, backgroundColor: 'lightgray' }}></View>
      </View >

  • 父组件与子组件同时绝对布局的情况下,且设置绝对布局的子组件没有写在最后一个时,保持原则一样,则绝对布局的子组件不会显示在父组件中,它会被前一个子组件覆盖

如下示例代码中,除相对布局的子组件能正常显示外,绝对布局的子组件会被同级的子view覆盖

return (
      <View style={{
              position: 'absolute',
              backgroundColor: 'yellow',
              width: Dimensions.get('window').width,
              height: Dimensions.get('window').height
      }}>
        <View style={{ height: 90, backgroundColor: 'yellow' }}></View>
        <View style={{ position: 'absolute', width: Dimensions.get('window').width, height: 90, bottom: 90, backgroundColor: 'lightgray' }}></View>
        <View style={{ flex: 1, backgroundColor: 'skyblue' }}></View>
      </View >
    );

另外,可以给子组件设置不同的flex值,如flex:2, flex: 0.5,则子组件在界面上所占的比例会随之变化。

文本输入

注意 react 中的 onChange 对应的是 rn 中的 onChangeText

触摸事件

TouchableHighlight按下时变暗

TouchableOpacity降低按钮的透明度 可选

滚动视图

  • 放置在ScrollView中的所有组件都会被渲染

长列表

  • FlatList优先渲染屏幕上可见的元素
  • SectionList 带有分组标签

网络请求

  • 不存在跨域,安全机制与网页环境有所不同
  • 为了安全默认只能访问https,对http做了限制
  • 建议你增加HTTPS支持,而不是关闭http限制,会把 苹果提供的安全保障也被关闭了
  • 可以使用所有数据交互库,不含jq

平台判断

针对不同平台编写不同代码的需求

组件与API

组件规定视图表现,api实现编程式组件操作

react native 组件
Picker
图片
  • 本地图片,可获取实际宽高,require里面必须是字符
  • 网络图片,获取不到宽高,要给设定尺寸
  • 背景图片必须指定宽高样式
expo组件
音频 Audio
视频 Video
相机 Camera

路由

	推荐React Navigation 提供了简单易用的跨平台导航方案,在 iOS 和 Android 上都可以进行翻页式、tab 选项卡式和抽屉式的导航布局

安装:

yarn add react-navigation --save  其他都不用做

  • navigation prop传递给每个在stack navigator中定义的路由
  • this.props.navigation.navigate(‘Details’) 无栈,添加栈
  • this.props.navigation.goBack() 回退
  • this.props.navigation.push() 强制添加栈
  • this.props.navigation.popToTop() 后退到 第一个栈
  • 跳转到新路由,老路由组件不会卸载,返回时前组件会卸载

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: React-saga和React-thunk都是用于处理异步操作的中间件。 React-thunk是Redux官方推荐的中间件之一,它允许我们在Redux中编写异步操作,使得我们可以在Redux中处理异步操作,而不需要在组件中处理异步操作。Thunk是一个函数,它接收dispatch和getState作为参数,并返回一个函数,这个函数接收dispatch作为参数,并在异步操作完成后调用dispatch。 React-saga是另一个处理异步操作的中间件,它使用了ES6的Generator函数来处理异步操作。Saga使用了一种称为Effect的概念,它是一个简单的JavaScript对象,用于描述异步操作。Saga使用了一种称为yield的语法,它允许我们在Generator函数中暂停异步操作,并在异步操作完成后继续执行。 总的来说,React-thunk和React-saga都是用于处理异步操作的中间件,它们的实现方式不同,但都可以让我们在Redux中处理异步操作。选择哪种中间件取决于个人的喜好和项目的需求。 ### 回答2: React-Saga和React-Thunk都是React应用中用于处理异步操作的中间件。它们的主要目的是在Redux应用中,帮助我们管理异步操作。这两个中间件都可以让React应用更加的灵活、健壮和易于维护。 React-Saga的核心理念是利用生成器函数来处理异步操作,Saga通过使用生成器来让异步操作变得像同步操作一样,其中每个异步操作都会被转化为一个迭代器函数,这些函数可以被Saga调用和暂停。 Saga主要有以下几个特点: 1. Saga可以使异步操作更加同步和简单,让异步调用变得更容易。Saga使用了轻量级、高效的生成器函数,从而有效地减少了异步调用中的代码复杂度。 2. Saga可以很好地管理和协调多个异步操作。Saga可以在任意阶段暂停异步操作,等待其他异步操作完成之后再继续执行。 3. Saga可以捕获和控制异步操作的错误、超时和状态。当出现问题时,Saga可以修复错误或者更改异步操作的状态,保证应用程序的稳定性和可靠性。 React-Thunk的核心概念是利用闭包函数来处理异步操作,Thunk将异步操作转化为一个闭包函数,然后通过回调函数将其传递到Redux的异步流中。 Thunk的主要特点有以下几个: 1. Thunk可以轻松处理异步操作,没有复杂的代码逻辑或者概念。 2. Thunk主要使用了闭包函数来捕捉当前异步操作的上下文,使得处理异步操作更加的简单、方便和自然。 3. Thunk可以轻松控制异步操作的状态、结果和错误处理,保证应用程序的稳定性和可靠性。 总之,React-Saga和React-Thunk都是帮助我们管理和处理应用程序的异步操作的中间件。它们都有自己独特的实现方式和特点。我们可以根据自己的项目需求和开发团队的技能水平来选择适合我们的中间件。 ### 回答3: React-saga 和 React-thunk 都是针对 React 应用中异步操作的中间件。它们两个都可以用来控制异步流程,使得我们可以更好的管理 React 应用程序中异步操作的数据和状态。 相较于 react-thunk, react-saga 是一个更加强大的中间件,它基于 generator 函数的概念,可以用来控制非常复杂的异步流程,使得我们可以在操作时更加精细地掌控多个异步操作的执行顺序和状态。 如果说 react-thunk 的核心概念是将异步操作封装进一个函数里,而在需要时调用这个函数即可,那么 redux-saga 的核心概念则是分离出一个独立的 Generator 函数来代表所有的异步业务逻辑。 redux-saga 可以让你从另一个角度处理异步流程,使你能够同步处理异步操作,不同的 Saga 可以用一种集中且易于理解的方式组合起来,组成它们自己的执行序列。 总而言之,React-saga和React-thunk 都是 React 应用程序开发中非常实用的工具,对于管理异步操作和数据状态非常有帮助。但是针对不同的开发需求,我们需要选择相应的中间件,来实现我们最好的业务逻辑。所以我们在使用的时候需要根据实际情况选择适合的中间件进行操作,以达到最好的效果。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值