《Ant Design 实战教程》学习笔记

1 前端开发的演变

静态页面阶段

  1. 后端收到浏览器的请求
  2. 生成静态页面
  3. 发送到浏览器

后端 MVC 模式:

  • Model(模型层):提供/保存数据
  • Controller(控制层):数据处理,实现业务逻辑
  • View(视图层):展示数据,提供用户界面

AJAX 阶段

  • AJAX 技术:脚本独立向服务器请求数据,拿到数据以后,进行处理并更新网页。整个过程中,后端只是负责提供数据,其他事情都由前端处理。
  • 前端不再是后端的模板,而是实现了从“获取数据 -> 处理数据 -> 展示数据”的完整业务逻辑。

前端 MVC 阶段

基本上是把 MVC 模式搬到了前端。后来提出了 MVVM 模式:

  • Model:读写数据
  • View:展示数据
  • View-Model:数据处理

View Model 是简化的 Controller。它的唯一作用就是为 View 提供处理好的数据,不含其他逻辑。也就是说,Model 拿到数据以后,View Model 将数据处理成视图层(View)需要的格式,在视图层展示出来。

这个模型的特点是 View 绑定 View Model。如果 View Model 的数据变了,View(视图层)也跟着变了;反之亦然,如果用户在视图层修改了数据,也立刻反映在 View Model。整个过程完全不需要手工处理。

SPA 阶段

前端可以做到读写数据、切换视图、用户交互,这意味着,网页其实是一个应用程序,而不是信息的纯展示。这种单张网页的应用程序称为 SPA(single-page application)。

所谓 SPA,就是指在一张网页(single page)上,通过良好的体验,模拟出多页面应用程序(application)。用户的浏览器只需要将网页载入一次,然后所有操作都可以在这张页面上完成,带有迅速的响应和虚拟的页面切换。

2 笔记

React

核心理念是将网页应用看成一个组件构成的状态机(state machine),状态的变化导致了 UI 的变化。React 本身的 API 并不多,是一个较为简单的框架。但是,要用好它,必须使用其他的配套工具,所以人们常说学习 React 并不是学习一个框架,而是学习一整套 React 技术栈。

React 本身的定位很单纯,它是一个网页组件的解决方案。也就是说,它只解决怎么把复杂的页面拆分成一个个组件,然后一个个独立的组件又怎么拼装成可以互相协同的网页。组件是中性的,任何一种应用架构都可以采用。因此,React 可以用于 MVC 架构,也可以用于 MVVM 架构,或者别的架构。

React 为了方便开发者自创了 JSX 语法。JSX 可以被 Babel 转码器转为正常的 JavaScript 语法。JSX 语法的特点就是,凡是使用 JavaScript 的值的地方,都可以插入这种类似 HTML 的语法。任何 JSX 表达式,顶层只能有一个标签,也就是说只能有一个根元素。一般来说,HTML 原生标签都使用小写,开发者自定义的组件标签首字母大写。

在我们的技术栈中 React 负责的是将数据渲染成页面,但是并不负责管理数据本身。

生命周期

组件有函数式组件和类组件两种,函数式组件没有生命周期函数,类组件必须继承React.Component这个基类。

组件内部,所有参数都放在this.props属性中。
this.props对象有一个非常特殊的参数this.props.children,表示当前组件“包裹”的所有内容。这个属性在 React 里面有很大的作用,它意味着组件内部可以拿到,用户在组件里面放置的内容。除了接受外部参数,组件内部也有不同的状态。React 规定,组件的内部状态记录在this.state这个对象上面。

组件的运行过程中,存在不同的阶段。React 为这些阶段提供了钩子方法,允许开发者自定义每个阶段自动执行的函数。这些方法统称为生命周期方法(lifecycle methods)componentDidMount()componentWillUnmount()componentDidUpdate()就是三个最常用的生命周期方法。

  • componentDidMount()会在组件挂载后自动调用。
  • componentWillUnmount()会在组件卸载前自动调用。
  • componentDidUpdate()会在 UI 每次更新后调用(即组件挂载成功以后,每次调用 render 方法,都会触发这个方法)。

还有三个不常用的:

  • shouldComponentUpdate(nextProps, nextState):每当this.propsthis.state有变化,在render方法执行之前,就会调用这个方法。该方法返回一个布尔值,表示是否应该继续执行render方法,即如果返回false,UI 就不会更新,默认返回true。组件挂载时,render方法的第一次执行,不会调用这个方法。
  • static getDerivedStateFromProps(props, state):该方法在render方法执行之前调用,包括组件的第一次记载。它应该返回一个新的 state 对象,通常用在组件状态依赖外部输入的参数的情况。
  • getSnapshotBeforeUpdate():该方法在每次 DOM 更新之前调用,用来收集 DOM 信息。它返回的值,将作为参数传入componentDidUpdate()方法。

受控组件

(非)受控组件:(不)能直接控制状态的组件。“受控”与“非受控”两个概念,区别在于这个组件的状态是否可以被外部修改。一个设计得当的组件应该同时支持“受控”与“非受控”两种形式,即当开发者不控制组件属性时,组件自己管理状态,而当开发者控制组件属性时,组件该由属性控制。而开发一个复杂组件更需要注意这点,以避免只有部分属性受控,使其变成一个半受控组件。一个典型的组件例子,可以参考 antd 中的 tabs 组件

布局

典型布局
在这里插入图片描述
侧边导航
在这里插入图片描述

Umi.js

  1. 封装了编译步骤,包括了很多开发时的有用工具。只要你写好 React 代码,接下来 umi 就会把它处理为生产代码。
  2. 配置文件被约定为config/config.js。也可以使用 .umirc.js 来作为配置文件。它和 config/config.js 是二选一的。
  3. src 目录,它用来存放项目的除了配置以及单测以外的主要代码。
  4. 约定的存放页面代码的文件夹是 pages。在配置项中添加 singulartrue 可以让 page 变为约定的文件夹。
  5. 在 umi 中,你可以使用约定式的路由,在 page 下面的 JS 文件都会按照文件名映射到一个路由。
  6. 除了约定式的路由,你也可以使用配置式的路由。其中 component 是一个字符串,它是相对于 page 目录的相对路径。
  7. 当有了 routes 的配置之后 umi 就不会再执行约定式对应的路由逻辑了。

脚手架

  • 我们写的代码其实并不是原生的 JS,HTML 和 CSS,而是基于它们扩展出来的更上层的语法。
  • 我们将会大量编写 React 组件(实际上 antd 就是 Ant Design 的 React 组件的实现),这些组件需要通过编译为最终的 JS 和 CSS,然后引入到 HTML 网页中才能够被浏览器正确地执行。
  • 由于存在一个编译过程,这就需要基于编译工具搭建一个项目的脚手架,使得我们可以通过工具实现代码的编译。通过编译后的代码才是浏览器能够执行的代码,这样我们才能进行项目的开发和最终的部署。本项目使用 umi。

路由

在 umi 中,应用都是单页应用,页面地址的跳转都是在浏览器端实现的,不会去重新请求服务端获取 html。html 只是在应用初始化的时候加载一次。所有的页面都是由不同的组件构成,页面的切换其实就是不同组件的切换,你只需要在配置中把不同的路由路径和对应的组件关联上即可。

在 umi 应用中,路由的配置是在/config/config.jsexports.routes中配置。
exports.routes 需要的是一个数组,数组中的每一个对象是一个路由信息。

配置代理

配置代理只需要在配置文件 config/config.js 中与 routes 同级处增加 proxy 字段,代码如下,

proxy: {
 '/dev': {
    target: 'https://08ad1pao69.execute-api.us-east-1.amazonaws.com',
    changeOrigin: true,
  },
},

配置的含义是:去往本地服务器 localhost:8000 的 ajax 调用中,如果是以 /dev 开头的,那么就转发到远端的 https://08ad1pao69.execute-api.us-east-1.amazonaws.com 服务器当中,/dev 也会保留在转发地址中。

比如:

/dev/random_joke 就会被转发到 https://08ad1pao69.execute-api.us-east-1.amazonaws.com/dev/random_joke

在这里插入图片描述

模拟服务端数据

在前面的章节中,我们设置了代理,于是所有的 HTTP 请求都可以先到达本地开发服务器,再被转发。在实际的开发中,后端的服务不一定马上可用,这就需要本地服务器另外一个能力:模拟数据(mock)。设置代理是 mock 的前提。

一个 ajax 请求发送到本地开发服务器后,我们可以设置:如果请求满足某个规则,则不转发这个请求,而是直接返回一个「假」结果给浏览器。在实际的开发中,我们常常先和服务端的同学商定 http 请求的接口接受什么参数,返回什么结果,然后先用 mock 数据来模拟,自己和自己「联调」。等待服务端同学开发好了,再解除 mock,用真实数据「联调」。

设置模拟数据时需要在工程根目录下的 mock 子目录中的建立文件。首先在工程中增加 mock 目录,并在其中创建文件 puzzlecards.js(取其他名字也可以,名字这里不需要)。如果想 mock 掉我们在上一个章节中的向 /dev/random_joke 的 ajax 调用,需要写入以下内容到文件,

const random_jokes = [
  {
    setup: "What is the object oriented way to get wealthy ?",
    punchline: "Inheritance"
  },
  {
    setup: "To understand what recursion is...",
    punchline: "You must first understand what recursion is"
  },
  {
    setup: "What do you call a factory that sells passable products?",
    punchline: "A satisfactory"
  }
];

let random_joke_call_count = 0;

export default {
  "get /dev/random_joke": function(req, res) {
    const responseObj =
      random_jokes[random_joke_call_count % random_jokes.length];
    random_joke_call_count += 1;
    setTimeout(() => {
      res.json(responseObj);
    }, 3000);
  }
};

模拟出错:

export default {
  "get /dev/random_joke": function(req, res) {
    res.status(500);
    res.json({});
  }
};

在 dva model 中我们加入简单的错误捕获:

import { message } from 'antd';

// ... 原有逻辑不修改

  try { // 加入 try catch 捕获抛错
    const puzzle = yield call(request, endPointURI);
    yield put({ type: 'addNewCard', payload: puzzle });

    yield call(delay, 3000);

    const puzzle2 = yield call(request, endPointURI);
    yield put({ type: 'addNewCard', payload: puzzle2 });
  } catch (e) {
    message.error('数据获取失败'); // 打印错误信息
  }

在每一个调用点做打印错误信息很麻烦,这里只是为了展示 mock 出错场景。在实际的开发中,一般会统一处理 http 请求错误时的信息提示。
刚才的模拟中,mock 具备动态改变、延时返回等能力,如果你不需要这个能力,也可以简单地使用对象。

export default {
  "get /dev/random_joke": {
    setup: "What is the object oriented way to get wealthy ?",
    punchline: "Inheritance"
  }
};

umi-plugin-react 插件

umi 官方的 umi-plugin-react 插件集成了常用的一些进阶的功能。

在插件集 umi-plugin-react 中配置 antd 打开 antd 插件,antd 插件会帮你引入 antd 并实现按需编译。也可以配置 dva 打开 dva 插件。

export default {
  plugins: [
    [
      "umi-plugin-react",
      {
        antd: true,
        dva: true
      }
    ]
  ]
  // ...
};

修改 antd 主题

修改配置文件:

// 加入 theme 定义
  theme: {
    "@primary-color": "#30b767", // 绿色
  }

更改全局样式

CSS modules 的 global 语法允许我们声明一个 class 名称不可被改写。

如果使用 umi 的话,有一个专门的文件 global.less 来让我们书写全局样式。这个文件并不会被 CSS modules 处理。

一个用途是全局性地定义 HTML 标签的样式,比如写入:

p {
  margin: 0;
}

另外一个用途是全局性地覆盖第三方库的样式,比如 antd 中的样式 。
我们全局覆盖 ant-btn 的样式,增加下面的定义:

.ant-btn {
  box-shadow: 0 3px 7px rgba(0, 0, 0, 0.5);
}

dva.js

DVA 是基于 redux、redux-saga 和 react-router 的轻量级前端框架及最佳实践沉淀。

model

软件架构的分层

在这里插入图片描述
服务端:

  1. Controller 层负责与用户直接打交道,渲染页面、提供接口等,侧重于展示型逻辑
  2. Service 层负责处理业务逻辑,供 Controller 层调用。
  3. Data Access 层顾名思义,负责与数据源对接,进行纯粹的数据读写,供 Service 层调用。
    前端:
  4. Page 负责与用户直接打交道:渲染页面、接受用户的操作输入,侧重于展示型交互性逻辑
  5. Model 负责处理业务逻辑,为 Page 做数据、状态的读写、变换、暂存等。
  6. Service 负责与 HTTP 接口对接,进行纯粹的数据读写。
dva 的 model 对象
app.model({
  namespace: "todoList",

  state: [],

  effects: {
    *query({ _ }, { put, call }) {
      const rsp = yield call(queryTodoListFromServer);
      const todoList = rsp.data;
      yield put({ type: "save", payload: todoList });
    }
  },

  reducers: {
    save(state, { payload: todoList }) {
      return [...state, todoList];
    }
  }
});
  1. namespace:model 的命名空间,只能用字符串。一个大型应用可能包含多个 model,通过namespace区分。
  2. state:当前 model 状态的初始值,表示当前状态。
  3. reducers:用于处理同步操作,可以修改 state,由 action 触发。reducer 是一个纯函数,它接受当前的 state 及一个 action 对象。action 对象里面可以包含数据体(payload)作为入参,需要返回一个新的 state。
  4. effects:用于处理异步操作(例如:与服务端交互)和业务逻辑,也是由 action 触发。但是,它不可以修改 state,要通过触发 action 调用 reducer 实现对 state 的间接操作。
  5. action:是 reducers 及 effects 的触发器,一般是一个对象,形如{ type: 'add', payload: todo },通过 type 属性可以匹配到具体某个 reducer 或者 effect,payload 属性则是数据体,用于传送给 reducer 或 effect。
dva 的作用
  • 通过把状态上提到 dva model 中,我们把数据逻辑从页面中抽离出来。
  • 通过 effect 优雅地处理数据生成过程中的副作用,副作用中最常见的就是异步逻辑。
  • dva model 中的数据可以注入给任意组件。
  • 另外,dva 允许把数据逻辑再拆分(「页面」常常就是分隔的标志),以 namespace 区分。当你觉得有必要时,不同的 namespace 之间的 state 是可以互相访问的。

如果你熟悉 React 中最基本的两个概念 props 和 state,一定知道 props 和 state 对于一个组件来讲都是数据的来源,而 state 又可以通过 props 传递给子组件,这像是一个鸡生蛋蛋生鸡的问题:到底谁是数据的源头 ?答案是 state,而且是广义的 state:它可以是 react 组件树中各级组件的 state,也可以是 react 组件树外部由其他 js 数据结构表示的 state,而 dva 管理的就是 react 组件树之外的 state: Redux。归根结底,props 是用来传导数据的,而 state 是数据改变的源泉。

如果你已经对 React 开发比较熟悉,就会知道子组件的 state 可以上提 (state hoisting),由父组件来管理:

  • 子组件间接回调到父组件的 setState 的方法来改变父组件的 state;
  • 新的 state 通过 props 的形式把再次被子组件获悉。

而 dva 可以帮助我们把 state 上提到 所有 React 组件之上,过程是相似的:

  • 页面通过调用 dispatch 函数来驱动 dva model state 的改变;
  • 改变后的 dva model state 通过 connect 方法注入页面。

所谓「注入」从本质上是 控制反转 的一种实现,这种思想在许多的语言框架中都有体现,最著名的莫过于基于 Java 语言的 Spring。组件不再负责管理数据,组件只是通过 connect 向 dva 声明所需数据。

connect

  • connect 让组件获取到两样东西:1. model 中的数据;2. 驱动 model 改变的方法。
  • connect 本质上只是一个 javascript 函数,通过 @ 装饰器语法使用,放置在组件定义的上方;
  • connect 既然是函数,就可以接受入参,第一个入参是最常用的,它需要是一个函数,我们习惯给它命名叫做 mapStateToProps,顾名思义就是把 dva model 中的 state 通过组件的 props 注入给组件。通过实现这个函数,我们就能实现把 dva model 的 state 注入给组件。
const namespace = "puzzlecards";

const mapStateToProps = state => {
  const cardList = state[namespace];
  return {
    cardList
  };
};
@connect(mapStateToProps, mapDispatchToProps)
export default class PuzzleCardsPage extends Component {
  render() {
    return (
      <div>
        {this.props.cardList.map(card => {
          return (
            <Card key={card.id}>
              <div>Q: {card.setup}</div>
              <div>
                <strong>A: {card.punchline}</strong>
              </div>
            </Card>
          );
        })}
        <div>
          <Button
            onClick={() =>
              this.props.onClickAdd({
                setup:
                  "Lorem ipsum dolor sit amet, consectetur adipiscing elit",
                punchline: "here we use dva"
              })
            }
          >
            {" "}
            添加卡片{" "}
          </Button>
        </div>
      </div>
    );
  }
}

mapStateToProps 这个函数的入参 state 其实是 dva 中所有 state 的总合。对于初学 js 的人可能会很疑惑:这个入参是谁给传入的呢?其实你不用关心,你只需知道 dva 框架会适时调用 mapStateToProps,并传入 dva model state 作为入参,我们再次提醒:传入的 state 是个 “完全体”,包含了 所有 namespace 下的 state!我们自己定义的 dva model state 就是以 namespace 为 key 的 state 成员。所以 const namespace = 'puzzlecards' 中的 puzzlecards 必须和 model 中的定义完全一致。dva 期待 mapStateToProps 函数返回一个 对象,这个对象会被 dva 并入到 props 中,在上面的例子中我们取到数据后,把它改名为 cardList 并返回( 注意返回的不是 cardList 本身,而是一个包含了 cardList 的对象! ),cardList 就可以在子组件中通过 props 被访问到了。

export default {
  namespace: "puzzlecards",
  state: {
    data: [
      {
        id: 1,
        setup: "Did you hear about the two silk worms in a race?",
        punchline: "It ended in a tie"
      },
      {
        id: 2,
        setup: "What happens to a frog's car when it breaks down?",
        punchline: "It gets toad away"
      }
    ],
    counter: 100
  },
  reducers: {
    addNewCard(state, { payload: newCard }) {
      const nextCounter = state.counter + 1;
      const newCardWithId = { ...newCard, id: nextCounter };
      const nextData = state.data.concat(newCardWithId);
      return {
        data: nextData,
        counter: nextCounter
      };
    }
  }
};
import React, { Component } from "react";
import { Card, Button } from "antd";
import { connect } from "dva";

const namespace = "puzzlecards";

const mapStateToProps = state => {
  const cardList = state[namespace].data;
  return {
    cardList
  };
};

const mapDispatchToProps = dispatch => {
  return {
    onClickAdd: newCard => {
      const action = {
        type: `${namespace}/addNewCard`,
        payload: newCard
      };
      dispatch(action);
    }
  };
};

@connect(mapStateToProps, mapDispatchToProps)
export default class PuzzleCardsPage extends Component {
  render() {
    return (
      <div>
        {this.props.cardList.map(card => {
          return (
            <Card key={card.id}>
              <div>Q: {card.setup}</div>
              <div>
                <strong>A: {card.punchline}</strong>
              </div>
            </Card>
          );
        })}
        <div>
          <Button
            onClick={() =>
              this.props.onClickAdd({
                setup:
                  "Lorem ipsum dolor sit amet, consectetur adipiscing elit",
                punchline: "here we use dva"
              })
            }
          >
            {" "}
            添加卡片{" "}
          </Button>
        </div>
      </div>
    );
  }
}

onClickAdd 是怎么被注入的呢 ?答案就在于我们给 connect 传入了第二个函数:mapDispatchToProps。我们习惯用这个名字是因为它精炼地说明了这个函数的作用:以 dispatch 为入参,返回一个挂着函数的对象,这个对象上的函数会被 dva 并入 props,注入给组件使用。

我们在 onClickAdd 函数中调用 dispatch 派发了一个 action,action 包含 onClickAdd 传递过来的内容 { setup, punchline } 作为 payload,action 的 type 是 puzzlecards/addNewCardaddNewCard 在这个例子中是 reducer 的名字,这个我们下面会讲到。dispatch 函数就是和 dva model 打交道的唯一途径。 dispatch 函数接受一个 对象 作为入参,在概念上我们称它为 action,唯一强制要包含的是 type 字段,string 类型,用来告诉 dva 我们想要干什么。我们可以选择给 action 附着其他字段,这里约定用 payload 字段表示额外信息。

我们把想做的事情通过 action 描述出来,并通过 dispatch 告诉 dva model,而对这个消息的处理就是 dva 的事情了。如果深入了解 React 的读者,一定觉得这句话似曾相识。是的,dva 和 React 哲学一脉相承,React 也是遵循这个原理工作的,在组件中总要写一个 render 函数,这个函数就是向 React 描述你想要渲染出的内容,作为开发者你并不会去直接操作 DOM,而 React 负责把 render 函数的返回值转化成 DOM 元素,并由 React 最终决定渲染 DOM 的时机和流程(React 渲染引擎的执行是个异步的过程)。

接下来的问题是:派发出的 action 怎样被 dva 识别并执行 "添加卡片" 的逻辑呢 ?

reducer

dva model 中可以定义一个叫做 reducers 的成员用来响应 action 并修改 state。每一个 reducer 都是一个 function,action 派发后,通过 action.type 被唯一地匹配到,随后执行函数体逻辑,返回值被 dva 使用作为新的 state。state 的改变随后会被 connect 注入到组件中,触发视图改变。

reducer 的样子大概是:

someReducer(state /* old state */, { payload }) {
  // ... do calculation
  return {
    // ... build a new object as next state and return it
  };
}

reducer 应该是一个 “纯函数”,它的返回值作为新的 state。dva 会注入旧的 state 和 action 中的 payload,是否使用完全根据需要决定;返回值必须是一个新构造对象,绝不能把旧 state 的引用返回!

reducer 干的事情和 React 中 setState(prevState => { ... }) 很像,都要返回一个新构造的对象,但区别是:reducer 的返回值会 整个取代 (Replace) 老的 state,而 setState 中回调函数的返回值是会 融合(Merge) 到老的 state 中去。

在这里插入图片描述

Effect 和 Generator function

在实际的开发中,计算新 state 时常常需要异步操作配合,比如说强制延时、异步网络请求数据(比如 ajax)等等。但是 reducer 需要是个纯函数,我们不能在 reducer 中写这些逻辑,破坏了这个机制后 dva 将无法工作。在 dva 框架下,effect 就是专门处理这些具有 “副作用” 的操作的执行单元。

export default {
  namespace: "some_namespace",
  state: {},
  effects: {
    // 定义 effects 成员
    someEffect: function*() {},
    someOtherEffect: function*() {}
    // ...
  },
  reducers: {
    // ...
  }
};
effect 和 middleware

**局部上看 effect 就是一个一个的 generator function。宏观上看,effect 是一层中间件。**中间件是一种程序架构和分布式系统架构上的思想。

" Middleware is some code you can put between the framework receiving a request, and the framework generating a response. "

在上一章中 action 被 dispatch 之后就能够 直接 到达 reducer。为了保证 reducer 的纯粹性,但同时又能够处理副作用,就需要打破「直接」这个特性。effect 充当了这么一个中间层,当 action 被 dispatch 之后,会先到达 effect 处理副作用,然后该 effect 最终会促使新的 action 发送出去,这个新的 action 可能被其他的 effect 再捕获继续处理,也可能被 reducer 捕获并结束,无论怎样,最终处理逻辑的终点都将是 reducer。

在上一章节中,我们知道 action.type 的构造是 namespace 名称 + / + reducer 名称,事实上 action.type 也可以是 namespace 名称 + / + effect 名称。对于视图层来讲,其实并不会感知 effect 和 reducer 的区别。视图层只是通过 action 描述想做什么,至于这个 action 之后是直接被 reducer 处理还是通过 effect 再到 reducer,视图层并不感知,也不应该关心。这样我们就做到了数据逻辑和视图逻辑的分离处理。

Generator function
getData: function* ({ payload }, { call, put }) {
  const data = yield call(SomeService.getEndpointData, payload, 'maybeSomeOtherParams');
  yield put({ type: 'getData_success', payload: data });
}
  1. 当这个 generator function 被执行时,执行的流程看上去会是同步的!
  2. 入参有两个对象,第一个对象就是匹配这个 effectaction 对象,因此可以取到约定的 payload 这个字段,第二个对象是 effect 原语集,其中 call, put 最为常用。generator function 入参中的两个对象都是在运行时由 dva 注入到 generator function 中的。
  3. call 其实是一个函数,和 yield 关键字配合使用处理异步逻辑,call 第一个参数是一个函数,要求函数返回 Promise,之后的参数是该函数调用时的入参。yield call 调用后就阻塞了,Promise 被解析后,得到异步调用的结果,存储到 data 中,然后程序才能继续进行。
  4. put 也是一个函数,putyield 配合使用,用来派发一个 action,和 dispatch 的功能**一模一样!**只不过是在 effect 函数中使用而已。
  5. yield put 派发的 action 如果是为了触发 同 model 中的其他 effect/reducer 执行,不需要指定 namespace 名称。

异步的实质是事件发生促使程序的执行点来回跳转。我们使用 callback 本质上是描述跳转的一种手段。generator function 并没有改变异步的本质,只是改变了描述的方式,使得程序看起来像是同步一样。

一个 generator function 在执行时有 两方。一方是 generator function 本身,另一方是 generator function 的句柄持有者,而这一般都是框架所持有。我们姑且称这个句柄为 genStub。当框架调用 genStub.next() 时,generator function 会执行到下一个 yield 然后暂停,并把 yield 后面表达式的计算值返还给框架,同时把程序执行权交给框架。框架拿到值后做处理,比如就是异步处理,处理结束拿到结果,再次调用 genStub.next(),返还值给 generator function 同时驱动它恢复执行。当恢复执行时,你可以认为 返回的处理结果会整体替换 yield <expression>,然后程序继续执行到下一个 yield

yield 这个单词用在这里特别形象:yield 本身有「让步」的意思,也有「产出」的意思。

「generator function yield 到外部的值」和「外部返还给 generator function 的值」不是一回事!!!

generator function 定义了流程,并在每次 yield 节点上报想做的事情。而异步的真正执行逻辑由 generator function 句柄的持有者代为执行。对应到 dva 上也是一样的。拿 call 做例子,call 其实是一个特别简单的函数。call 的返回值只是一个 plain javascript object:

{
  CALL: {
    fn: SomeService.getEndpointData,
    args: [payload, 'maybeSomeOtherParams']
  }
}

我们通过 call 向 dva 描述了想做的事情:请帮我执行这个函数,Promise 解析后通知我继续执行,并把 Promise 的解析值返回给我。
在这里插入图片描述

在 React 16 中,页面初始化时的异步请求必须只能在 componentDidMount 中做,不能在 constructor, UNSAFE_componentWillMount, UNSAFE_componentWillReceiveProps, getDerivedStateFromProps 中做。

请重新审视我们的请求:

const endPointURI =
  "https://08ad1pao69.execute-api.us-east-1.amazonaws.com/dev/random_joke";

这里我们直接调用了一个「非本地」地址。在实际开发中是比较罕见的。这里能够成功,是因为被调用的 API 做了额外的人为设置,允许一个「非同域」的 ajax 请求。

我们在浏览器中看到的页面是从一个本地开发服务器所伺服的。这个本地开发服务器的地址就是 http://localhost:8000/ 。当我们调用 getRandomPuzzle 时,此时发送 ajax 请求页面的域就是 http://localhost:8000,但是请求的数据在另外一台服务器 https://08ad1pao69.execute-api.us-east-1.amazonaws.com。一个是 http 一个是 https,路径也不同,端口也不同(https 是 443)。任意这三个东西有一个不同,就认为是资源请求「跨域」了。http 的 默认 安全规则是不允许「跨域」请求。

值得注意的是,发送 ajax 请求的是你的浏览器,所谓 User Agent,而「跨域」的限制 仅仅在浏览器和服务器之间。我们不能强制远程服务器都像例子中那样允许「跨域」请求,所以我们能做的就是不要使用浏览器发送请求。所以在前端开发中,一种常见的规避跨域的方法就是:把 ajax 请求发送到你的本地开发服务器,然后本地开发服务器再把 ajax 请求转发到远端去,从网络拓扑上看本地开发服务器起着「反向代理」的作用。本地服务器和远端服务器是「服务器和服务器间的通信」,就不存在跨域问题了。

package.json

scripts 中定义的命令,可以在项目根目录中通过 cnpm run [scriptname] 来运行。

  "scripts": {
    "dev": "umi dev",
    "build": "umi build",
    "test": "echo \"Error: no test specified\" && exit 1"
  }

你可以通过 cnpm run build 来构建出最终的产物,执行该命令后会生成最终的 HTML、CSS 和 JS 到 dist 目录下。它们是浏览器可以直接识别并运行的代码,这样你就可以将它们部署到你想要的服务器上了。

如果你直接用浏览器打开 HTML 那是无法正确展示的,因为直接打开无法识别出 HTML 引入的 JS 和 CSS 的路径。你需要确保的的 HTML 在一个 HTTP 的 web 容器中,并保证对应的页面的访问路径正确。比如使用 serve:

cnpm install serve -g
serve ./dist

Ant Design

表单

Form.create()(List);

这段代码的作用是创建一个高阶组件,为页面组件 List 提供表单所需要的内容(this.props.form)。

表单组件是通过 FormForm.Item 配合使用,其中每一个 Form.Item 都是一个表单域。而 getFieldDecorator 是用于将包裹的组件与表单进行双向绑定使用。
此外,我们还可以设置改表单域是否是必填项(required: true)或者是否需要类型检查(type: url)。
通过 validateFields 方法验证表单是否完成填写,通过便提交添加操作。

在 antd 中,我们提供了诸如 Input, Select 之类的用于接收用户输入的组件。但是如果这些不满足你的需要时该怎么办呢?比如你如果需要一个富文本输入框,又或者是一个复杂的多行内容输入的表格。我们怎么可以添加这些复杂的组件,让它们可以和 antd 的 Form 组件一起使用呢?

其实很简单,在上面我们介绍了 getFieldDecorator 这个方法,它执行后会返回一个函数,那个函数接收一个参数,那个参数就是一个输入组件。比如在上面的例子中它可能是 <Input />也有可能是 <Select />。但是它并没有被局限在 antd 支持的组件内,你完全可以传入你自己的一个组件,比如下面的示例:

<FormItem label="自定义输入">
  {getFieldDecorator("custom", {
    rules: [{ required: true }]
  })(<YourInput />)}
</FormItem>

其中,只要 YourInput 这个组件满足如下三个条件即可:

  • 提供受控属性 value 或其它与 valuePropName 的值同名的属性。
  • 提供 onChange 事件或 trigger 的值同名的事件。
  • 不能是函数式组件。

所以,如果你需要在表单中添加一个富文本组件,那么你可以在 Ant Design 官网推荐的社区精选组件中找到合适你项目的富文本编辑器组件。然后你可以自定义一个组件,该组件应该封装你找到的编辑器子组件并满足上面说的三个条件,这样你就可以在你的表单中使用它了。

自定义样式

CSS modules

在这里插入图片描述

我们看到它实际的 class 是 style__hello__<hash数值>,并非在源文件中声明的 hello。这就是 CSS modules 起了作用。这个 hash 值是全局唯一的,比如通过文件路径来获得,这样 class 名称就做到了全局唯一。通过全局唯一的 CSS 命名,我们变相地获得了局部作用域的 CSS(scoped CSS)。如果一个 CSS 文件仅仅是作用在某个局部的话,我们称这样一个 CSS 文件为 CSS module。

CSS modules 不是一个可以安装的 npm 包,也不是 w3c 中的某个标准,只是一项流行的社区规范(an opinionated proposal)。webpack browserify 等打包工具(module bundler)的能力让工具生成局部 CSS 成为可能,CSS modules 规范也应运而生。webpack 实现了这套规范。umi 依赖 webpack,默认开启了 CSS modules 特性。

注意:很多 CSS 选择器是不会被 CSS Modules 处理的,比如 body、div 、a 这样的 HTML 标签名就不会。我们推荐如果要定义局部 css 样式/动画, 只使用 class 或 @keyframe。

webpack 实现 CSS module 的原理
在现代 web 开发中,服务器并不能直接使用我们写的 JS CSS HMTL 文件。事实上,我们按照规范写出代码,输入给编译工具 (transpiler) ,它最终把代码转换/打包,输出成浏览器可以使用的 JS CSS HTML。在多年的社区沉淀后,脱颖而出的是诸如 webpack 这样的工具,这类编译工具又称为 module bundler。webpack 允许我们用 import/export (ES6) 或者 require/module.exports (CommonJs) 这样的语法来书写我们的 JS 代码,它甚至允许我们在 js 里面 import 一个 CSS 文件。注意:如果脱离了 webpack 的语境,这么写当然是会引起语法错误的。
在现代 web 开发中,我们的运行时代码强耦合了编译时工具,强耦合换来的是传统 web 开发所不可企及的新能力。对于 webpack,当我们每次写了 import A from B 的时候,我们其实是声明了一个 A 对于 B 的依赖。当在 a.js 中写入 import styles from a.css 后,webpack 就可以把这个依赖翻译成:每当 a.js 被使用时,保证生成一个 style 标签,里面嵌入 a.css 的内容。同时 webpack 给予我们另一个能力:不同类型文件间可以信息传递。webpack 把 a.css 中的类名根据规则编译成为全局唯一的字符串,传递给 a.js 使用,于是手工维持的命名规则就可以自动生成。

CSS modules 与 Less 语法一起使用

虽然被称作 CSS modules,但是它完全可以和 Less 一起无缝使用,与使用普通 CSS 没有什么区别,只需要使用 Less 的语法写样式就可以了。鉴于 antd 的样式也使用了 Less 作为开发语言。所以我们的实战教程强烈推荐 CSS module 和 Less 文件一起使用。

Less 介绍
Less 是一个 CSS 的超集,Less 允许我们定义变量,使用嵌套式声明,定义函数等。严格说 Less 包含两部分:1. Less 的语法。2. Less 预处理器(Less preprocessor)。浏览器终究只认识 CSS,所以 Less 文件需要经过 Less 预处理器编译成为 CSS。
在工具的支持下,一个 Less 文件首先会经过 CSS modules 的编译,把类名全局唯一化,然后才被 Less preprocessor 编译成为 CSS 文件。正因此,Less 文件可以和 CSS modules 无缝联合使用。

在 CSS modules 中覆盖 antd 样式

CSS modules 的 global 语法 派上了用场。它允许我们声明一个 class 名称不可被改写。

global 不应该被滥用,特别地我们建议:若想在某个文件中覆盖 antd 样式,请加上一个类似 .override-ant-btn 的类包裹住 global 修饰的名称,以避免全局样式声明分散在项目各处。

更换 antd 主题

前面章节我们讲过如何覆盖某个页面中 antd 的样式,有时候我们想要「批量修改」 antd 的样式,这就需要利用 less 提供的一个能力:modifyVars。简单地讲,antd 在使用 less 定义样式时,使用了大量的变量声明。这些变量的定义在编译期是可以被工具识别并修改的。

发布了184 篇原创文章 · 获赞 19 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/Exupery_/article/details/103949499