@[toc]

一、什么是react

React12 起源于 Facebook 的内部项目,因为该公司对市场上所有 JavaScript MVC框架,都不满意,就决定自己写一套,用来架设Instagram 的网站。做出来以后,发现这套东西很好用,就在2013年5月开源了。

由于 React的设计思想极其独特,属于革命性创新,性能出众,代码逻辑却非常简单。所以,越来越多的人开始关注和使用,认为它可能是将来 Web 开发的主流工具。 这个项目本身也越滚越大,从最早的UI引擎变成了一整套前后端通吃的 Web App 解决方案。衍生的 React Native 项目,目标更是宏伟,希望用写 Web App 的方式去写 Native App。如果能够实现,整个互联网行业都会被颠覆,因为同一组人只需要写一次 UI ,就能同时运行在服务器、浏览器和手机。 React主要用于构建UI。你可以在React里传递多种类型的参数,如声明代码,帮助你渲染出UI、也可以是静态的HTML DOM元素、也可以传递动态变量、甚至是可交互的应用组件。 特点: 1.声明式设计:React采用声明范式,可以轻松描述应用。 2.高效:React通过对DOM的模拟,最大限度地减少与DOM的交互。 3.灵活:React可以与已知的库或框架很好地配合。

二、什么是dva

dva 3首先是一个基于 reduxredux-saga 的数据流方案,然后为了简化开发体验,dva 还额外内置了 react-routerfetch,所以也可以理解为一个轻量级的应用框架。 特性

  • 易学易用,仅有 6 个 api,对 redux 用户尤其友好,配合 umi 使用后更是降低为 0 API
  • elm 概念,通过 reducers, effectssubscriptions 组织 model
  • 插件机制,比如 dva-loading 可以自动处理 loading 状态,不用一遍遍地写 showLoading 和 hideLoading
  • 支持 HMR(热加载),基于 babel-plugin-dva-hmr实现 components、routes 和 models 的 HMR

简单来说dva就是这个团队觉得redux+redux-sage这一套数据流太麻烦,将这些整合封装成了一个最佳体验模式(也就是最方便开发使用)提供开发者使用

三、什么是umi

Umi 4,中文可发音为乌米,是可扩展的企业级前端应用框架。Umi 以路由为基础的,同时支持配置式路由和约定式路由,保证路由的功能完备,并以此进行功能扩展。然后配以生命周期完善的插件体系,覆盖从源码到构建产物的每个生命周期,支持各种功能扩展和业务需求。

Umi 是蚂蚁金服的底层前端框架,已直接或间接地服务了 3000+ 应用,包括 java、node、H5 无线、离线(Hybrid)应用、纯前端 assets 应用、CMS 应用等。他已经很好地服务了我们的内部用户,同时希望他也能服务好外部用户。

它主要具备以下功能:

  • 🎉 可扩展,Umi 实现了完整的生命周期,并使其插件化,Umi 内部功能也全由插件完成。此外还支持插件和插件集,以满足功能和垂直域的分层需求。
  • 📦 开箱即用,Umi 内置了路由、构建、部署、测试等,仅需一个依赖即可上手开发。并且还提供针对 React 的集成插件集,内涵丰富的功能,可满足日常 80% 的开发需求。
  • 🐠 企业级,经蚂蚁内部 3000+ 项目以及阿里、优酷、网易、飞猪、口碑等公司项目的验证,值得信赖。
  • 🚀 大量自研,包含微前端、组件打包、文档工具、请求库、hooks 库、数据流等,满足日常项目的周边需求。
  • 🌴 完备路由,同时支持配置式路由和约定式路由,同时保持功能的完备性,比如动态路由、嵌套路由、权限路由等等。
  • 🚄 面向未来,在满足需求的同时,我们也不会停止对新技术的探索。比如 dll 提速、modern mode、webpack@5、自动化化 external、bundler less 等等。

什么时候不用 umi?

如果你,

  • 需要支持 IE 8 或更低版本的浏览器
  • 需要支持 React 16.8.0 以下的 React
  • 需要跑在 Node 10 以下的环境中
  • 有很强的 webpack 自定义需求和主观意愿
  • 需要选择不同的路由方案

Umi 可能不适合你。

四、什么是antd

antd [5]是基于 Ant Design 设计体系的 React UI 组件库,主要用于研发企业级中后台产品。 特性

  • 🌈 提炼自企业级中后台产品的交互语言和视觉风格。

  • 📦 开箱即用的高质量 React 组件。

  • 🛡 使用 TypeScript 开发,提供完整的类型定义文件。

  • ⚙️ 全链路开发和设计工具体系。

  • 🌍 数十个国际化语言支持。

  • 🎨 深入每个细节的主题定制能力。

支持环境

  • 现代浏览器和 IE11 及以上(需要 polyfills)。
  • 支持服务端渲染。
  • Electron

六、开始使用脚手架创建项目

首先保证电脑上有node环境,这个就不说了

本教程使用yarn也是umi推荐的依赖管理工具

1. 第一步安装umi全局依赖

$yarn global add umi
$umi -v
umi@3.0.9

成功安装umi依赖

umi升级成3.0了,手脚架创建项目与以前不一样了 2. 新建一个文件夹,并在终端打开,并创建项目

$ mkdir myapp && cd myapp
$ yarn create @umijs/umi-app

此时手脚架在该文件夹下会创建这些文件

Copy: .editorconfig Write: .gitignore Copy: .prettierignore Copy: .prettierrc Write: .umirc.ts Copy: mock/.gitkeep Write: package.json Copy: README.md Copy: src/pages/index.less Copy: src/pages/index.tsx Copy: tsconfig.json Copy: typings.d.ts

需要查看隐藏文件才能看到”.”开头的文件,ubuntu查看隐藏文件快捷键为ctrl+h,macOs好像是command+shift+.忘记了。

3.使用yarn安装依赖

$ yarn
yarn install v1.21.1
[1/4] 🔍  Resolving packages...
success Already up-to-date.
✨  Done in 0.71s.

启动项目

$ yarn start

Starting the development server…

✔ Webpack Compiled successfully in 17.84s

DONE Compiled successfully in 17842ms
8:06:31 PM

App running at:

4.在浏览器里打开 http://localhost:8000/,能看到以下界面:

success 此时,你的umi项目的目录是这样的:

在这里插入图片描述

  • mock文件夹是用来写前端mock数据的,此目录下所有 js 和 ts 文件会被解析为 mock 文件。
  • node_modules是项目所有依赖的包
  • src
    • .umi是临时文件目录,比如入口文件、路由等,都会被临时生成到这里。不要提交 .umi 目录到 git 仓库,他们会在 umi dev 和 umi build 时被删除并重新生成。
    • pages目录所有路由组件存放在这里。
    • .editorconfig 编辑器的设置
    • .gitignore 使用git时不需要git维护的内容设置
    • prettierignore package.json中有插件prettier,这是prettier代码美化工具不需要关系的文件设置
    • prettierrc prettier代码美化规则
    • .umirc.ts umi框架的配置
    • package.json 包含插件和插件集,以 @umijs/preset-、@umijs/plugin-、umi-preset- 和 umi-plugin- 开头的依赖会被自动注册为插件或插件集。
    • readme不说了
    • tsconfig.json 指定ts编译的一些参数信息
    • typings.d.ts 变量一系列的申明文件

umi的配置项:

打开**.umirc.ts**文件 在这里插入图片描述 看到的配置是这样的,defineConfig是umi的一个函数,应该是预留的目前没什么用,直接return了这个传入的对象,我们来改造一下,跟踪到definConfig函数中我们看到: 在这里插入图片描述 ts定义传入的对象类型为Iconfig,返回类型为Iconfig,在源码中是直接return了。所以我们为了更好的理解,我们将.umirc.ts改写为这样

import { defineConfig } from 'umi';
import { IConfig } from '@umijs/types';
const config: IConfig = {
  routes: [{ path: '/', component: '@/pages/index' }],
};
export default defineConfig(config);
 
 

这个和原来的没有区别,更好理解而已。 接下来我们跟踪到IConfig中,看配置属性:

export interface IConfig extends IConfigCore {
  alias?: {
    [key: string]: string;
  };
  analyze?: BundleAnalyzerPlugin.Options;
  autoprefixer?: object;
  base?: string;
  chainWebpack?: {
    (
      memo: WebpackChain,
      args: {
        webpack: typeof webpack;
        env: env;
        createCSSRule: ICreateCSSRule;
      },
    ): void;
  };
  chunks?: string[];
  cssLoader?: object;
  cssnano?: object;
  copy?: string[];
  define?: {
    [key: string]: any;
  };
  devServer?: IServerOpts;
  devtool?: webpack.Options.Devtool;
  dynamicImport?: {
    loading?: string;
  };
  exportStatic?: {
    htmlSuffix?: boolean;
    dynamicRoot?: boolean;
  };
  externals?: any;
  extraBabelPlugins?: IPresetOrPlugin[];
  extraBabelPresets?: IPresetOrPlugin[];
  extraPostCSSPlugins?: any[];
  favicon?: string;
  forkTSCheker?: object;
  hash?: boolean;
  headScripts?: IScriptConfig;
  history?: {
    type: 'browser' | 'hash' | 'memory';
    options?: object;
  };
  ignoreMomentLocale?: boolean;
  inlineLimit?: number;
  lessLoader?: object;
  links?: Partial<HTMLLinkElement>[];
  manifest?: Partial<IManifest>;
  metas?: Partial<HTMLMetaElement>[];
  mock?:
    | {
        exclude?: string[];
      }
    | false;
  mountElementId?: string;
  outputPath?: string;
  plugins?: IPresetOrPlugin[];
  postcssLoader?: object;
  presets?: IPresetOrPlugin[];
  proxy?: any;
  publicPath?: string;
  runtimePublicPath?: boolean;
  scripts?: IScriptConfig;
  singular?: boolean;
  ssr?: object;
  styleLoader?: object;
  styles?: IStyleConfig;
  targets?: ITargets;
  terserOptions?: object;
  theme?: object;
  title?: string;
  [key: string]: any;
}

这些属性自己去根据umi官网上的配置项说明,根据自己需求自由添加,地址是https://umijs.org/zh-CN/config,不多说了。

七、开始编写页面

router提取

在根目录创建config文件夹,创建router.comfig.ts文件 插入代码:

import { IRoute } from 'umi-types';
 
const routes: IRoute[] = [{ path: '/', component: '@/pages/index' }];
 
export default routes;
 

发现没有umi-types依赖包,yarn在开发环境中,生产环境不需要的

$yarn add umi-types --dev 

将.umirc.ts中改成引用router

import { defineConfig } from 'umi';
import pageRoutes from './config/router.config';
import { IConfig } from 'umi-types';
const config: IConfig = {
  routes: pageRoutes,
};
 
export default defineConfig(config);

这么做防止以后router 过多,拆封成一个文件单独管理

项目的拓展工作

SRC文件夹下创建多个文件夹,拆分组件、model、工具类等,再添加global.less文件全局使用的样式。 创建完后项目目录结构是这样的: 在这里插入图片描述

编写页面demo

1.我们先将umi中的页面的demo复制粘贴进来看看效果 打开pages下的index.tsx文件 将代码替换,我将我个人理解的注释都写在代码里面了

import React, { FC } from 'react';
import { IndexModelState, ConnectProps, Loading, connect } from 'umi';
/*
  connect就是react-redux中的connect,umi直接import,export出来的
  IndexModelState是在model中export的数据类型,在下方数据绑定到组件才能取到
  ConnectProps是页面传入的类型包含dispatch,match,location,history,route熟悉react的人都知道不解释了
  Loading是dva封装的model异步方法调用状态,执行中为true,否则为false,为界面加载中的状态使用
*/
 
//定义当前页面的props,继承ConnectProps,再添加两个属性,model中的数据以及loading状态
interface PageProps extends ConnectProps {
  index: IndexModelState;
  loading: boolean;
}
 
/*
  页面组件,FC是在react中定义的type FC<P = {}> = FunctionComponent<P>,表示IndexPage是一个方法组件。
  FunctionComponent又是什么?
 
  interface FunctionComponent<P = {}> {
        (props: PropsWithChildren<P>, context?: any): ReactElement<any, any> | null;
        propTypes?: WeakValidationMap<P>;
        contextTypes?: ValidationMap<any>;
        defaultProps?: Partial<P>;
        displayName?: string;
    }
    文件跟踪下去太多了,就到这吧
  泛型为PageProps表示页面传入的参数类型
  return 页面展示的内容
*/
const IndexPage: FC<PageProps> = ({ index, dispatch }) => {
  const { name } = index;
  return <div>Hello {name}</div>;
};
 
/*
  使用redux中的connect将model数据绑定到组件
  connect传入一个箭头函数,获得model的数据,采用解构的方式{index,loading}:{index: IndexModelState; loading: Loading}(冒号后面ts数据类型)
  然后return了index和loading.models.index
  loading是dva全部model的loading状态,其中.index是逻辑上当前页面绑定的model的loading,model.ts中命名空间是index
  最后export default IndexPage也就是这个组件
*/
export default connect(
  ({ index, loading }: { index: IndexModelState; loading: Loading }) => ({
    index,
    loading: loading.models.index,
  }),
)(IndexPage);
 

现在代码是报错的,别着急,再在index.ts同级目录下创建model.ts数据流控制文件,将代码复制进去,我这种CV工程师常干的事情,model自己去看dva官网的教程,写的很清楚了,通常一个页面一个model

import { Effect, ImmerReducer, Reducer, Subscription } from 'umi';
 
//定义的State数据类型
export interface IndexModelState {
  name: string;
}
 
//定义model的类型
export interface IndexModelType {
  namespace: 'index';
  state: IndexModelState;
  effects: {
    query: Effect;
  };
  reducers: {
    save: Reducer<IndexModelState>;
    // 启用 immer 之后
    // save: ImmerReducer<IndexModelState>;
  };
  subscriptions: { setup: Subscription };
}
 
//实际的model
const IndexModel: IndexModelType = {
  namespace: 'index',
  //model的数据
  state: {
    name: 'skedush in model',
  },
  //model副作用也是异步方法,相同的输入可能得到不同的输出,一般用它做网络请求
  effects: {
    *query({ payload }, { call, put }) {},
  },
  /*
  reducers 聚合积累的结果是当前 model 的 state 对象。通过 actions 中传入的值,与当前 reducers 中的值进行运算获得新的值
  type Reducer<S, A> = (state: S, action: A) => S
  需要注意的是 Reducer 必须是纯函数(纯函数:同样的输入必然得到同样的输出,它们不应该产生任何副作用(函数执行的过程中对外部产生了可观察的变化,我们就说函数产生了副作用。)。)
  */
  reducers: {
    save(state, action) {
      return {
        ...state,
        ...action.payload,
      };
    },
    // 启用 immer 之后可以这么写,不过这个我们暂时不用,主要解决了引用对象出现的问题
    // save(state, action) {
    //   state.name = action.payload;
    // },
  },
  /**
   * Subscription 语义是订阅,用于订阅一个数据源,然后根据条件 dispatch 需要的 action。数据源可以是当前的时间、
   * 服务器的 websocket 连接、keyboard 输入、geolocation 变化、history 路由变化等等。
   */
  subscriptions: {
    setup({ dispatch, history }) {
      return history.listen(({ pathname }) => {
        if (pathname === '/') {
          dispatch({
            type: 'query',
          });
        }
      });
    },
  },
 
  /**
   * model中还是action和dispatch的概念
   * Action
    type AsyncAction = any
    Action 是一个普通 javascript 对象,它是改变 State 的唯一途径。无论是从 UI 事件、网络回调,还是 WebSocket
    等数据源所获得的数据,最终都会通过 dispatch 函数调用一个 action,从而改变对应的数据。action 必须带有 type
    属性指明具体的行为,其它字段可以自定义,如果要发起一个 action 需要使用 dispatch 函数;需要注意的是 dispatch
    是在组件 connect Models以后,通过 props 传入的。
 
    dispatch 函数
    type dispatch = (a: Action) => Action
    dispatching function 是一个用于触发 action 的函数,action 是改变 State 的唯一途径,但是它只描述了一个行为,
    而 dipatch 可以看作是触发这个行为的方式,而 Reducer 则是描述如何改变数据的。
 
    在 dva 中,connect Model 的组件通过 props 可以访问到 dispatch,可以调用 Model 中的 Reducer 或者 Effects,
    常见的形式如:
    dispatch({
      type: 'index/query', // 如果在 model 外调用,需要添加 namespace,比如当前的model命名空间是index,这就是调用effect中的query函数
      payload: {}, // 需要传递的信息
    });
   */
};
 
export default IndexModel;
 
 

以上代码都是umi官网的demo中复制来的不是本人写的

再看看页面,左上角是不是出现了hello skedush in model的字样,其中skedush in model是在model的state中的name传入当前组件展示的 在这里插入图片描述

修改组件使用class形式

打开index.tsx 这组件已经不是function component了,所以在react导入PureComponent,我直接在代码里注释,看的更清楚一些

import React, { PureComponent } from 'react';
/**
 * 将FC换成了PureComponent,PureComponent和Component的区别是PureComponent自动处理了页面渲染的一些优化
 * 一般的页面需求我们不需要自己控制页面渲染,使用PureComponent就行
 */
import { IndexModelState, ConnectProps, Loading, connect } from 'umi';
 
interface PageProps extends ConnectProps {
  index: IndexModelState;
  loading: boolean;
}
//定义当前页面state数据的类型
interface IndexPageState {
  name: string;
}
 
class IndexPage extends PureComponent<PageProps, IndexPageState> {
  /**
   * 当前页面继承了PureComponent类,后面是泛型,第一个是组件传入参数的类型,第二个是当前页面数据的类型
   */
 
  //class的构造方法,参数props数据类型是PageProps
  constructor(props: Readonly<PageProps>) {
    super(props);
    //state中的数据类型必须是IndexPageState
    this.state = {
      name: 'skedush in this state',
    };
  }
 
  //render函数渲染当前页面组件
  render() {
    //index会在this.props中,也就是当前组件传入的参数,其中还有dispatch函数等,可自行输出查看
    const { index } = this.props;
    //从命名空间为index的model中获取state:name
    const { name } = index;
    //return也就是render的渲染对象
 
    const thisStateName = this.state.name;
    return (
      <div>
        Hello {name} and {thisStateName}
      </div>
    );
  }
}
 
export default connect(
  ({ index, loading }: { index: IndexModelState; loading: Loading }) => ({
    index,
    loading: loading.models.index,
  }),
)(IndexPage);
 

页面展示 在这里插入图片描述 两个name一个是在model的state的,一个是在当前组件的state中的。

引用antd组件并操作state与model

在终端执行命令安装antd
$ yarn add antd

修改index.tsx,以及在model中添加Effect index.tsx: 在头部引用antd中的button组件,在render将组件渲染,并绑定onclick事件,事件使用箭头函数,默认bind了当前的组件。

import React, { PureComponent } from 'react';
import { IndexModelState, ConnectProps, Loading, connect } from 'umi';
import { Button } from 'antd';
 
interface PageProps extends ConnectProps {
  index: IndexModelState;
  loading: boolean;
}
//定义当前页面state数据的类型
interface IndexPageState {
  name: string;
}
 
class IndexPage extends PureComponent<PageProps, IndexPageState> {
  /**
   * 当前页面继承了PureComponent类,后面是泛型,第一个是组件传入参数的类型,第二个是当前页面数据的类型
   */
 
  //class的构造方法,参数props数据类型是PageProps
  constructor(props: Readonly<PageProps>) {
    super(props);
    //state中的数据类型必须是IndexPageState
    this.state = {
      name: 'skedush in this state',
    };
  }
 
  //render函数渲染当前页面组件
  render() {
    //index会在this.props中,也就是当前组件传入的参数,其中还有dispatch函数等,可自行输出查看
    const { index } = this.props;
    //从命名空间为index的model中获取state:name
    const { name } = index;
    //return也就是render的渲染对象
 
    const thisStateName = this.state.name;
    return (
      <div>
        Hello {name} and {thisStateName}
        <div>
          <Button type={'primary'} onClick={this.onClick}>
            点我改变当前页面state中的name
          </Button>
        </div>
        <br />
        <div>
          <Button type={'primary'} onClick={this.onClick2}>
            点我改变model中state的name
          </Button>
        </div>
      </div>
    );
  }
 
  onClick = () => {
    const { name } = this.state;
    this.setState({
      name: name + '1',
    });
  };
 
  onClick2 = () => {
    const { dispatch, index } = this.props;
    const { name } = index;
    // props中的dispatch可能为undefault,ts语法?.表示存在dispatch时才调用该方法
    dispatch?.({ type: 'index/changeState', payload: { name: name + '1' } });
  };
}
 
export default connect(
  ({ index, loading }: { index: IndexModelState; loading: Loading }) => ({
    index,
    loading: loading.models.index,
  }),
)(IndexPage);
 

注意修改了model之后需要刷新页面,model才会生效,有时候会自动刷新,有时候并不会自动刷新,修改之后model.ts为:

import { Effect, ImmerReducer, Reducer, Subscription } from 'umi';
 
//定义的State数据类型
export interface IndexModelState {
  name: string;
}
 
//定义model的类型
export interface IndexModelType {
  namespace: 'index';
  state: IndexModelState;
  effects: {
    query: Effect;
    changeState: Effect;
  };
  reducers: {
    save: Reducer<IndexModelState>;
    // 启用 immer 之后
    // save: ImmerReducer<IndexModelState>;
  };
  subscriptions: { setup: Subscription };
}
 
//实际的model
const IndexModel: IndexModelType = {
  namespace: 'index',
  //model的数据
  state: {
    name: 'skedush in model',
  },
  effects: {
    *query({ payload }, { call, put }) {},
    *changeState({ payload }, { call, put }) {
      yield put({ type: 'save', payload: { name: payload.name } });
    },
  },
 
  reducers: {
    save(state, { payload }) {
      return {
        ...state,
        ...payload,
      };
    },
  
  },
 
  subscriptions: {
    setup({ dispatch, history }) {
      return history.listen(({ pathname }) => {
        if (pathname === '/') {
          dispatch({
            type: 'query',
          });
        }
      });
    },
  },
 
 
};
 
export default IndexModel;
 
此时页面展示是这样,点击按钮会产生相应的变化
在这里插入图片描述

修改antd组件的样式

使用cssModule方式修改

1.在index.tsx中引入index.less,首先保证index.tsx同级目录中有index.less这个文件,没有自行创建;Ï

import styles from './index.less';

2.在给button增加className属性,cssmodule方式使用如下

<Button
	className={styles.btn}
	type={'primary'}
 	onClick={this.onClick}
>
  点我改变当前页面state中的name
</Button>

3.在index.less中增加样式

.btn {
  background: blue;
  &:hover {
    background: yellow;
    color: #000;
  }
}
 

在这里插入图片描述

鼠标悬浮:

在这里插入图片描述

注意是是要是引用了antd pro,这么修改button的样式无效,因为antd pro的样式有毒的。

得这么改,不过大多antd的组件样式基本都得这么改:

index.tsx:

//将样式绑定到组件外的className中
 <div className={styles.btnDiv}>
          <Button
            type={'primary'}
            onClick={this.onClick}
          >
            点我改变当前页面state中的name
          </Button>
        </div>

index.less

.btnDiv {
	 //global是覆盖到btnDiv下的全局样式,当然还要考虑less的机制,不说了自己去google查,
  //要是外边没有btnDiv直接用global,会覆盖了整个项目antd的button样式,所有的按钮都变成这样了,不建议使用,造成样式污染
  :global {
    //这里的样式名称是通过浏览器自带的开发者工具下元素捕捉到的
    .ant-btn-primary {
      background: blue;
      &:hover {
        background: yellow;
        color: #000;
      }
    }
  }
}

现在再看页面效果是一样的

捕捉antd组件样式名

防止一些同学这个都不知道,我还是说一下吧,Chrome浏览器为例子,不用Chrome的前端就别学了,按f12 mac和windows都一样。或者之间ctrl+shift+c (command+shift+c) ubuntu的话就不说了,我不配讲ubuntu大佬。

在这里插入图片描述

使用global.less

我们之前创建了global.less这个文件,这个是全局的样式,umi会默认将该文件的样式覆盖到全局,利用global.less修改第二个按钮的样式,woc这里改了半天,因为umi升级到3.0之后global.less有点区别了

编辑global.less:

.antdBtn {
  //umi3.0在这里面用:global反而无效了,不使用才有效
  // :global {
  .ant-btn-primary {
    background: red;
    &:hover {
      background: green;
      color: #000;
    }
  }
  // }
}
//原来umi2.0按照下面用就覆盖了全局的样式,但是在umi3.0好像不行了
// :global {
//   .ant-btn-primary {
//     background: blue;
//     &:hover {
//       background: yellow;
//       color: #000;
//     }
//   }
// }

编辑index.tsx的第二个button:

 <div className={'antdBtn'}>
          <Button type={'primary'} onClick={this.onClick2}>
            点我改变model中state的name
          </Button>
        </div>

查看页面 在这里插入图片描述

使用axios发起网络请求

编写mock

在mock中创建文件 mockDemo.ts:

import { Request, Response } from 'express';
 
export default {
  // 支持值为 Object 和 Array
  'GET /api/users': { name: 'Skedush' },
 
  // GET 可忽略
  '/api/users/1': { id: 1 },
 
  // 支持自定义函数,API 参考 express@4
  'POST /api/users/create': (req: Request, res: Response) => {
    // 添加跨域请求头
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.end('ok');
  },
};
 

保存完之后mock会自动加载,在终端会出现,失败会有失败提醒 在这里插入图片描述

封装request

简单封装request,在utils文件夹中创建request.ts,这里要用到axios和lodash的库,先yarn add一下
$yarn add axios lodash

request.ts:

import axios, { AxiosError, AxiosRequestConfig } from 'axios';
import { cloneDeep, isEmpty } from 'lodash';
import qs from 'qs';
 
//定义返回的类型
export interface ResponseData {
  success: boolean;
  message?: string;
  data?: any;
}
 
//定义axios参数,继承AxiosRequestConfig
export interface RequestConfig extends AxiosRequestConfig {}
 
/**
 * axios的请求封装,地址判断、错误处理
 *
 * @export
 * @param {object} options 请求选项
 * @returns {Promise} 请求结果
 */
export default function request(
  options: RequestConfig,
): Promise<ResponseData | undefined> {
  const { data, url, method = 'get' } = options;
  if (!url) {
    throw new Error('request url none');
  }
 
  //深拷贝data数据,以免造成引用产生的问题
  const cloneData = cloneDeep(data);
 
  //get请求在url中添加请求参数
  options.url =
    method.toLocaleLowerCase() === 'get'
      ? `${url}${isEmpty(cloneData) ? '' : '?'}${qs.stringify(cloneData)}`
      : url;
 
  // session
  options.withCredentials = true;
  // 设置请求头
  options.headers = {
    'X-Request-Type': 'ajax',
    'Content-Type': 'application/json;charset=UTF-8',
  };
 
  //发起请求
  return axios(options)
    .then(response => {
      const { data } = response;
 
      return Promise.resolve({
        //请求成功的返回
        success: true,
        message: '请求成功',
        data: data || {},
      });
    })
    .catch((error: AxiosError) => {
      //请求错误的返回
      return {
        success: false,
        message: error.toString(),
      };
    });
}
 

在model中使用网络请求

打开model.ts 引入编写的request.js 再修改model中的changeState方法

import request from '@/utils/request';
//@代表src目录
*changeState({ payload }, { call, put }) {
      /**
       * call用来调用异步函数,将异步函数和函数参数作为call函数的参数传入,返回一个js对象。saga引入他的主要作用是方便测试,同时也能让我们的代码更加规范化。
同js原生的call一样,call函数也可以指定this对象,只要把this对象当第一个参数传入call方法就好了
 
put是saga对Redux中dispatch方法的一个封装,调用put方法后,saga内部会分发action通知Store更新state。
       */
 
       //yield 等待异步请求结束 ,res请求的返回值,注意这里如果没有yield的话res是一个promise对象
      const res = yield call(() => {
        return request({
          url: '/api/users',
          data: payload,
          method: 'GET',
        });
      });
      //在调用reducers中的save方法,将res存到当前model的state中
      yield put({ type: 'save', payload: { name: res.data.name } });
    },
将以上代码添加替换掉model.ts中对应的地方

修改index.tsx的按钮onclick的事件,发起请求,以及将model中的state再次渲染到页面中 index.tsx

修改onClick2这个方法:

onClick2 = () => {
    //页面用connect绑定了model会传入dispatch
    const { dispatch, index } = this.props;
    //model中state中的name
    const { name } = index;
    //调用model的changeState,并传入参数,注意model的作用域,可以去umi约定式路由看,不做解释
    dispatch?.({ type: 'index/changeState', payload: { name: name + '1' } });
  };

页面效果: 在这里插入图片描述 前面的skedush in model是在model的state中取得的,请求改变了model的数据,传入的props改变页面自动会渲染

点击按钮发起的请求 在这里插入图片描述 网络请求的response 在这里插入图片描述 model中res的输出,因为我们在request中封装了请求的返回值 在这里插入图片描述 开发到这里,单个页面基本的流程已经完成了,写下来快速开发第二个页面。

八、快速开发第二个页面

  1. 在pages下创建Dashboard文件夹,接着再Dashboard中创建Home与NewPage文件夹
  2. 将原本在pages下的index.tsx,index.less,model.ts移动到Home文件夹下,在NewPage中创建index.tsx,index.less,model.ts三个文件创建完后你的目录是这样的:

在这里插入图片描述

  1. 修改config中router.config.ts路由配置文件

import { IRoute } from 'umi-types';
const routes: IRoute[] = [
  { path: '/', component: '@/pages/Dashboard/Home' },
  { path: '/new', component: '@/pages/Dashboard/NewPage' },
];
export default routes;
  1. 现在进入http://localhost:8000/new路由下是空白的,因为我们没有写任何东西,打开NewPage下的index.tsx添加如下代码:
import React, { PureComponent } from 'react';
import { NewPageModelState, ConnectProps, Loading, connect } from 'umi';
 
import { Button } from 'antd';
 
interface NewPageProps extends ConnectProps, NewPageModelState {}
interface NewPageState {}
class NewPage extends PureComponent<NewPageProps, NewPageState> {
  constructor(props: Readonly<NewPageProps>) {
    super(props);
    this.state = {};
  }
 
  render() {
    return (
      <div>
        <div>I am NewPage,model count is {this.props.count}</div>
        <div>
          <Button type={'primary'} onClick={this.clickBtn}>
            NewPage
          </Button>
        </div>
      </div>
    );
  }
  clickBtn = () => {
    const { dispatch, count } = this.props;
    dispatch?.({ type: 'newPage/changeCount', payload: { count } });
  };
}
 
export default connect(
  ({ newPage, loading }: { newPage: NewPageModelState; loading: Loading }) => ({
    count: newPage.count,
    loading: loading.models.newPage,
  }),
)(NewPage);
  1. 在mock文件夹下创建newPage.ts模拟数据请求数据:
import { Request, Response } from 'express';
 
export default {
  ['POST /api/count/post'](req: Request, res: Response) {
    const count = req.body.count;
    res.json({ count: count + 1 });
  },
};
  1. NewPage下的model.ts:
import { Effect, ImmerReducer, Reducer, Subscription } from 'umi';
import request from '@/utils/request';
export interface NewPageModelState {
  count: number;
}
 
export interface NewPageModelType {
  namespace: 'newPage';
  state: NewPageModelState;
  effects: {
    changeCount: Effect;
  };
  reducers: {
    save: Reducer<NewPageModelState>;
  };
  subscriptions: {};
}
 
const NewPageModel: NewPageModelType = {
  namespace: 'newPage',
  state: {
    count: 0,
  },
  effects: {
    *changeCount({ payload }, { call, put }) {
      const res = yield call(() => {
        return request({
          url: '/api/count/post',
          data: payload,
          method: 'POST',
        });
      });
      yield put({ type: 'save', payload: { count: res.data.count } });
    },
  },
 
  reducers: {
    save(state, { payload }) {
      return {
        ...state,
        ...payload,
      };
    },
  },
  subscriptions: {},
};
export default NewPageModel;
  1. 现在让我们看看页面的效果

在这里插入图片描述 在这里插入图片描述

在这里插入图片描述 本场chat结束,这个只是一个简单使用umi框架的教程,真正应用到开发中还需要封装很多通用的工具类、api、装饰器等,还有最关键通用组件的封装。下一篇我将介绍如何封装一些通用的库,与通用组件的封装,以及页面级组件的封装,令开发变的更加简单,打造一款属于自己的企业级框架。