基于 umi 构建中后台前端项目入门

Table of Contents

1. 目标

相比于后端(甚至是 App、小程序开发)而言,前端发展到现在还没有一个相对成熟稳定的姿态。 Web 前端开发并不难,JS/HTML/CSS 也都很好理解,但是从学习到可以开始写生产代码,曲线比较陡峭。根源在于组件、框架、工具等比较混乱,变更又很快。 通常对于初学者有了基础知识之后就不知道怎么选择、怎么再进一步。

本文的目标在于帮助那些有一定的程序设计经验的后端开发或者运维,利用 umi 可以快速上手写一些前端项目。

虽然陆陆续续写前端有四五年了,但都是迫于产品化前端资源匮乏的情况下才写的(大部分在写后端)。所以,我不是一个专职的前端。 从 2018 年开始用 React 全家桶写,本文的很多思路也是基于这个经历。所以目标更在于「启发」而非面面俱到的「教程」。

2. 需要了解的前置技术

一点前端技术都不想学就开始写是不可能的。所以一些前端基础还是要有的。放心,基础技术都很简单。

2.1. [必须]要了解的

  • 前端三剑客:
  • React:Facebook 开源的、时下最热的前端框架,官方文档,一定要通读一遍,但是例子没必要全部实现。跟学 JavaScript 一样,有那些东西,基本的语法是要有的
  • LESS:LESS 是对 CSS 的一层抽象,使用的时候最终还是会转换成 CSS,官方文档,建议了解其基本语法
  • npm/yarn:包管理和构建工具

说明:

  • ECMAScript 和 JavaScript 的关系:ES 一种规范,而 JS 是实现(其他实现还有 ActionScript),很多时候 ES 和 JS 是混用的,等价成一个东西。 ES6 和 ES2015 也是同一个东西,只是叫法不一样。
  • HTML/CSS 是前端基础中的基础,*必须* 要了解其基本语法,但是他们真的简单,不要有心理压力。HTML 没有太多可说的,对于 CSS 要掌握的有:
    • 基础:颜色、字体、文字样式、对齐、阴影 简单,1 个小时足够
    • 选择器:类选择器、ID 选择器、属性选择器、伪类和伪元素 简单,两个小时学习足够
    • HTML 结构和样式层叠关系 会有点绕,但都是些语法
    • 盒模型: margin padding border 这是布局的基础 简单,一个小时足够
    • 布局:常用的几种定位方式, float 布局难,flex 布局很好理解很好操作。/整体有点难度,但是可以跳过/
  • 了解 CSS 之后,LESS 只要看一遍就不用再看了,给你一个例子立马就明白了:

    .main-menu {
      padding: 20px 0;
      background: #e2e2e2;
    }
    
    .main-menu > .navigation {
      display: inline-block;
      padding: 20px 0;
      width: 30%;
      font-style: 15px;
      color: #333;
    }
    
    .main-menu > .navigation:hover {
      text-decoration: none;
      color: #0a6c9f;
      background: #e2e2e2;
    }
    

    这是 CSS 写法,下面 LESS 的写法:

    @background-color: #e2e2e2;
    @padding: 20px 0;
    
    .main-menu {
      & {
        padding: @padding;
        background: @background-color;
      }
    
      > .navigation {
        display: inline-block;
        padding: @padding;
        width: 30%;
        font-style: 15px;
        color: #333;
        &:hover {
          text-decoration: none;
          color: #0a6c9f;
          background: @background-color;
        }
      }
    }
    

    可以理解成 LESS 是 CSS 的预编译语言,最后都会转成 CSS 来运行,而且在 LESS 中写 CSS 的语法也一点问题都没有。

2.2. [可选]要了解的

这前提是 React 技术栈,不然那可就多了。

  • node.js 是一个基于 Chrome V8 引擎 的 JavaScript 运行时
  • react-router URL 路由器:负责把一个 URL 路径绑定到一个 React 组件上 好理解
  • redux JavaScript 可预测的状态容器,react-redux 是 React 的实现 难理解
  • redux-saga 他是 redux 的中间件 难理解

说明:

  • 早期 JavaScript 离开浏览器是不能运行的,node 使得在他可以像 Python 一样在命令行也可以运行,可以写 server 端;这使得 JavaScript 更加的工程化
  • 可预测的状态容器不好理解,但是我理解的是他解决的是组件之间数据通信、状态的问题:

    • store 用来存储状态(数据)
    • action 改变状态的动作(向后端请求、或者 A 组件的变化影响 B 组件)
    • reducer action 的结果处理,修改 store ,生成新的 store

    redux 语法很晦涩,redux-saga 目的是更优雅的管理 Side Effects (比如异步请求)

  • redux + redux-saga 还是很难理解,dva 整合了 redux 和 redux-saga 用起来就很舒服了

刚开始学不用深入了解每一个关联技术的工作原理,只要知道他的存在目的是什么,是为了解决什么问题。

3. React 简介

官方提供了丰富的文档,还有配套的案例,比我写的好多了,所以不再赘述。这里只说一下我所理解的 React 到底干了什么事情。

下面是一个点击按钮,数值自增的例子,用 jQuery 实现是这样的:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
    <script>
      $(document).ready(function() {
        $("button").click(function() {
          var count = $("#count").text();
          $("#count").text(parseInt(count) + 1);
        });
      });
    </script>
  </head>
  <body>
    <div>
      init value is: <span style="color: red" id="count">0</span>
      <br />
      <button>点我</button>
    </div>
  </body>
</html>

demo: https://codesandbox.io/s/click-one-jquery-guim0

如果用 React 实现是这样的:

import React, { Component } from "react";
import ReactDOM from "react-dom";

class ClickPlusOne extends Component {
  state = {
    count: 0
  };
  click = () => {
    this.setState({
      count: this.state + 1,
    })
  };

  render() {
    const { count } = this.state;
    return (
      <div>
        init value is: <span style={{ color: "red" }}>{count}</span>
        <br />
        <button onClick={this.click}>请点我</button>
      </div>
    );
  }
}

function App() {
  return (
    <div className="App">
      <ClickPlusOne />
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

demo: https://codesandbox.io/s/click-plus-react-uh290

在上面的基础上再加一个功能,根据数量渲染一个列表出来。对于 jQuery:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
    <script>
      $(document).ready(function() {
        $("button").click(function() {
          var count = $("#count").text();
          $("#count").text(parseInt(count) + 1);

          var listHtml = [];
          for (var i = 0; i < count; i++) {
            listHtml += `<li>item: ${i + 1}</li>`;
          }
          $("#list").html(listHtml);
        });
      });
    </script>
  </head>
  <body>
    <div>
      init value is: <span style="color: red" id="count">0</span> <br />
      <button>点我</button>
      <ul id="list"></ul>
    </div>
  </body>
</html>

https://codesandbox.io/s/click-jquery-list-fww7j

对于 React:

import React, { Component } from "react";
import ReactDOM from "react-dom";

class ClickPlusOne extends Component {
  state = {
    count: 0
  };

  click = () => {
    this.setState({
      count: this.state.count + 1
    });
  };

  createList = () => {
    const { count } = this.state;
    const children = [];
    for (let i = 0; i < count; i++) {
      children.push(<li key={i}>item: {i + 1}</li>);
    }
    return children;
  };

  render() {
    const { count } = this.state;
    return (
      <div>
        init value is: <span style={{ color: "red" }}>{count}</span>
        <br />
        <button onClick={this.click}>请点我</button>
        <ul>{this.createList()}</ul>
      </div>
    );
  }
}

function App() {
  return (
    <div className="App">
      <ClickPlusOne />
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

demo: https://codesandbox.io/s/click-plus-react-list-ugws4

上面这两个例子,可以看到:

  • 不管是 jQuery 还是 React 的实现方式,他们都干的事情是:当数据变更时,修改 DOM 结构(内容),这个过程可以称之为「渲染」
  • React 代码并不比 jQuery 的代码少,只不过写起来更加优雅一些。尤其是业务逻辑复杂之后,React 代码结构更清晰 我个人以为:React 更加符合后端程序思维方式

这两个例子反映不到的是:

  • React 是 单向数据 流的,当 state(组件内部) 和 props(传入)发生变更时,触发渲染
  • 在 React 中一切都是组件,每个组件都有自己的生命周期 react-lifecycle-methods-diagram,在每个生命周期函数中都可以做一些事情,比如:
    • componentDidMount 时,异步向后端请求数据
    • render 时,返回渲染之后的 DOM 结构
    • componentDidUpdate 时,判断数据变更,重新请求数据
    • componentWillUnmount 时,卸载数据

React 并不是 完整的前端框架解决方案,他只是提供了高效的 DOM 渲染(同样 vue.js 也是如此),脚手架才是解决方案。 React 官方的脚手架是 create-react-app

4. 脚手架(scaffold)

经常写后端人知道后端有 Web 框架,比如:Python 的 Django,flask;Java 的 Sprint Boot;Go 的 Gin 等。一个后端的框架一般会包含那些呢?

接入层(网络层) 负责与外部通信协议命令字转换等
路由层(Router) API 映射到具体的业务逻辑 handler
逻辑处理层(Viewer) 处理业务逻辑
缓存层(Cache) 页面数据缓存
模型层(Model) 定义数据库表,对数据库基本操作 CRUD 提供易用的接口
构建部署工具  
其他 统一异常处理,日志系统,监控,调优工具等

(如果是微服务框架,还会提供服务注册,服务发现等能力)。

不同的语言的框架,或者说不同业务场景产生的框架包含的内容不一样。但其目的都是:*为了减少重复工作而提供的模块、工具抽象*。

对于前端也是类似,只不过前端一般叫 Scaffold,即脚手架。 这个神秘的词汇,如果你查一下维基百科,Scaffold (programming) 就会发现他和 framework 基本上是一个东西。所以… 别想那么多了。就是前端框架。

(我自己 yy 过为什么前端叫脚手架而不叫框架,大约是因为前端的概念用的太乱了,组件,框架乱飞,但是干的事情又不太一样,比如 Bootstrap 也叫框架,React 也叫框架 …)

一个前端脚手架一般会包含这些部分:

网络请求层(request) 与后端异步通信(一般是 HTTP 请求),比较出名的有 fetchaxios;通常会对基础库再封装一层,然后提供统一的错误处理;
路由层(Router) 浏览器中访问的每一个 URL 对应的逻辑处理组件,比如 react-router;还有一种称之为约定式路由,即 URL 路径与目录结构一一对应;
逻辑处理层(Viewer) 具体的页面逻辑和渲染
各种浏览器兼容转换 TypeScript 转换成 JavaScript
  将 ES6/ES7 转换成 ES5,一般使用 babel
  将 LESS/SASS 转换成 CSS
构建打包、性能优化 一般是对 webpack 的二次封装;代码分割;代码打包压缩,代码混淆

其实可以看出前端所干的事情比后端要少很多,而且远没有后端那么成熟,类似运行兼容这种应该是语言层面应该解决的问题,不应该框架来解决。但其目标也是为了减少重复工作。

5. umi

umi 是蚂蚁金服开源的企业级的基于 React 技术栈的应用框架。他的定位类似与 create-react-app,除了上面说的一般脚手架所包含的部分之外,他还集成了:

  • antd:基于 React 的 UI 组件库(类似 Bootstrap,只不过 Bootstrap 是用 JS 实现的)
  • dva:基于 redux 和 redux-saga 的数据流方案,简化使用 redux 成本; umi 融合了 dva,并且提供了 model 自动加载等,又简化了 dva 的使用成本;

5.1. ant design pro 与 umi

其实我一开始是不知道 umi 的,只知道 Ant Design Pro。从 Pro 0.x/1.0 到 Pro 4.0(umi) 只不过短短的两年多时间,只能感叹发展太快了,我恰好在挖财做云平台完整的经历了从 Pro 到 umi 的蜕变。

这期间躺了很多坑,也在 issue 上吐槽了。然而,最终的结果还是比较令人满意的。大体的发展图是这样的:

from-pro-umi.png

5.2. umi 安装

安装脚手架:

npm install umi -g

新建项目:

=> mkdir umi-demo && cd umi-demo
=> npm create umi
? Select the boilerplate type
  ant-design-pro  - Create project with a layout-only ant-design-pro boilerplate, use together with umi block.
> app             - Create project with a simple boilerplate, support typescript.
  block           - Create a umi block.
  library         - Create a library with umi.
  plugin          - Create a umi plugin.

这里选择 app 不要选择 ant-design-pro ,否则会增加很多的 pro template 代码,不够纯粹(不方便演示);实际中后台项目如果为了省事可是使用 pro。

? Do you want to use typescript? (y/N)

N ,如果想使用 typescript,可以输入 y

? What functionality do you want to enable? (Press <space> to select, <a> to toggle all, <i> to invert selection)
❯◯ antd
 ◯ dva
 ◯ code splitting
 ◯ dll
 ◯ internationalization

a 全选即可,如果你想使用其他的 react UI 组件库,这里可以不用 antd 换成别的。

安装依赖包:

npm install

默认情况下 umi 使用 配置式路由,即 URL 对应的相应组件由配置文件中指定。你也可以 约定式路由,即 URL 由 pages 目录下的路径所对应的文件自动处理, 根 / 自动找 index.js 文件。对于初学者理解起来有点困难,建议用 配置式路由。 配置式路由文件在 .umirc.js

运行 npm start

5.3. 目录结构说明

.
├── .umirc.js
├── mock
│   └── .gitkeep
├── package.json
├── src
│   ├── app.js
│   ├── assets
│   │   └── yay.jpg
│   ├── global.css
│   ├── layouts
│   │   ├── index.css
│   │   └── index.js
│   ├── locales
│   │   └── en-US.js
│   ├── models
│   └── pages
│       ├── index.css
│       └── index.js
└── webpack.config.js
  • mock API mock
  • src 业务源代码都在这里,其中:
    • global.css 全局 css 文件,约定每个 pages 下面的 css 文件只能作用于对应的 js 文件,避免 css 污染
    • layout 页面布局组件
    • models 全局 models
    • pages 所有的 URL handler 都在这里,一个 pages 中一般会包含属于自己的 jsx,样式 css,model.js,组件等;
    • locales 用于国际化,可在 .umirc.js 中开启关闭,使用时 import { formatMessage } from 'umi-plugin-locale'; 然后使用 formatMessage 转换
    • components 用于存放全局组件
  • dist npm build 之后,会将静态文件输出到 dist

5.4. 示例:从路由到数据展示

1. 页面路由

本示例将说明,从 URL 访问到组件生命周期开始,请求数据,然后渲染。

所有的路由配置都在 .umirc.jsroutes 下面,在 routes/routes 中新增:

{ path: '/hello', component: './hello' }

将 hello 映射到 hello 组件中,默认会在 pages 下寻找 hello/index.js 文件。所以需要在 pages 下面新建 hello/index.js 文件:

export default () => "hello, world";

然后访问 http://localhost:8000/hello 即可看到 hello, world 页面。

2. 异步请求数据

React 中的每一个组件,都有生命周期。 /hello 这个页面对应的组件也是;在 React 组件生命周期 中有说明,里面列举了常用的和不常用的部分,生命周期是 React 的组件基础,最好认真看看文档。这里只说实现:

假设 hello 页面需要从后端获取一批用户信息,然后展示出来。从哪里发出请求呢?一般是从 componentDidMount ,他是页面首次渲染之后,第一次调用的方法,而且只会调用一次。

import axios from 'axios';
import { Component } from 'react';
import { message } from 'antd';

export default class Hello extends Component {
  state = {
    userList: [],
  }

  componentDidMount() {
    this.queryUsers();
  }

  queryUsers = async () => {
    const resp = await axios.get('/api/users');
    if (resp.status === 200) {
      this.setState({
        userList: resp.data,
      })
    } else {
      message.error(`请求出错:code={resp.statusText}`);
    }
  }

  render() {
    console.log(this.state.userList);
    return "Hello, World";
  }
}

页面逻辑复杂了,要加生命周期函数,所以改成了 class 组件(之前是函数组件),另外,需要后端请求,所以安装了 axios npm install axios --save ,他是一个 http 请求库,类似的还有 fetch,umi 官方提供的是 umi-request,都可以。

代码里我们干了这几个事情,在 componentDidMount 中发起后端调用 queryUsers ,他是异步的(async…await 了解一下),最终正确返回时将数据设置到 state 中,错误提示(message 是 antd 提供的一个消息提示组件)。

因为我们没有实际的后端服务,所以需要 mock 一个。在 mock 目录下新建 user.js 文件,然后写入:

export default {
  'GET /api/users': [
    {
      name: '张三',
      gender: '男',
      age: '22',
      addr: '中国,北京',
    },
    {
      name: '李四',
      gender: '男',
      age: '23',
      addr: '中国,上海',
    },
    {
      name: '王五',
      gender: '女',
      age: '23',
      addr: '中国,广州',
    },
  ],
};

它 mock 了一个 /api/usersGET 请求,并返回了一些数据。

最后,刷新页面即可看到 console 显示了我们获得到的数据。

3. 数据渲染

得到后端返回的数据之后,需要将数据显示到渲染, render

render() {
  const { userList } = this.state;
  return (
    <table>
      <tbody>
        <tr>
          <th>姓名</th>
          <th>性别</th>
          <th>年龄</th>
          <th>地址</th>
        </tr>
        {userList.map(item => (
          <tr key={item.name}>
            <td>{item.name}</td>
            <td>{item.gender}</td>
            <td>{item.age}</td>
            <td>{item.addr}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

刷新页面即可看到数据已经显示在页面上了。

4. 美化一下

原生的表格样式比较「丑陋」,可以手动改 css 文件。umi 引入了 antd ,antd 提供了 表格组件

render() {
  const { userList } = this.state;

  const columns = [
    { title: '姓名', dataIndex: 'name' },
    { title: '性别', dataIndex: 'gender' },
    { title: '年龄', dataIndex: 'age' },
    { title: '地址', dataIndex: 'addr' },
  ];

  return <Table dataSource={userList} columns={columns} bordered />;
}

再刷新,就发现表格显示就美观多了。

【可选】使用 dva 请求数据

大部分时候请求数据直接 request 即可,但有组件需要通信的情况用 redux 能方便很多。所以我们将上面的例子改写成 dva 的方式。 流程如下图:

dva.png

需要 hello 目录下新建两个文件:

  • model.js (子页面的 model 只能被子页面引用)
  • api.js 异步发起 API 调用

看起来有些麻烦(实际上已经比原生的 Redux 写法简单多了),逻辑还是很清晰的。 这么做的好处是,model 可以被多个组件绑定一份数据变更会触发多个组件渲染,也就达到了组件数据通信的目的。

5.5. 使用 Pro template

为了演示更加纯粹,所以 umi 创建项目的时候,选择了 app ,实际中后台项目建议勾选 ant design pro 。使用 pro 之后自动增加了几个刚需的模板:

  • 登录范例
  • 菜单自动生成
  • 菜单前端鉴权
  • 常用的 layout

5.6. 部署

npm build 打包之后,在 dist 目录下生成了类似如下的文件(我希望文件带 hash 值,所以配置中添加了 hash: true ):

.
├── index.html
├── layouts__index.993844f1.chunk.css
├── layouts__index.d5b2fbe6.async.js
├── p__hello.aaf350ae.async.js
├── p__hello__model.js.bc91bd32.async.js
├── p__index.29672b36.async.js
├── p__index.fc500c15.chunk.css
├── static
│   └── yay.44dd3333.jpg
├── umi.5e4f0268.js
├── umi.efbd33bc.css
├── vendors.4391d77a.async.js
└── vendors.5f8d95ca.chunk.css

1 directory, 12 files

一个前端渲染的(CSR)的前端应用,只需要一个 nginx 服务,将 root 指向静态文件所在目录即可。

6. 结语

本文主要面向有一点后端或者脚本开发经验程序员,使用 umi 搭建中台前端项目入门,主要布道的入门思路是:刚开始写前端没必要深入了解每个组件的原理,先照猫画虎,知道怎么做实现可以达到什么目的就行了。 等可以写日常业务的时候,再慢慢的深入。


前端框架原没有后端框架那么稳定,各种酸苦,慢慢体会吧。

First created: 2019-12-15 11:04:29
Last updated: 2022-12-11 Sun 12:49
Power by Emacs 27.1 (Org mode 9.4.4)