大而全的 react 框架 umijs 试用

根据介绍,umijs 是围绕 react 搭建的企业级开发框架。它有诸多优点:可扩展、开箱即用……

在看了基础的介绍之后,可以感觉到 umi 和 next 很相似。实际上官网也说了,umi 有参考 next。好的的设计相互借鉴可以少走很多弯路,这还是很好的。

umi 自动集成了很多插件,比如 antd、dva 等等。确实是开箱即用的。但对于一个新事物(我是指我新学习),我不太敢大规模应用。不过此次项目只有几个页面,试试 umi 未尝不可,谁让公司自己的组件库不更新呢。

因为之前公司项目用的是 react15.x、antd2.x,也没用 ts,所以直接面对 react16.x、antd4.x、ts 还是有点磕磕碰碰。

请求封装

umi 自带的 request 就是 promise 形式的,直接使用也很方便,但我还是习惯于再封装一层,也是为了把之前项目的习惯搬过来。封装主要有两点,一个是 token 等验证信息的填充。还有一个是请求结果的判定。如果没有权限,需要自动退出。所有结果都是 reject——为了方便业务代码种的 yield,其余判定也交给具体的业务请求逻辑。

另外,使用中发现接口错误会有两次报错提示。其中一次是我业务代码里写的,还有一次找了很久才发现是 umi 自带的😔。可能我文档看得不仔细,但是不管文档里有没有写,我觉得框架默认打开自带的接口报错提示不太好。既然它默认打开,我也只能手动关闭了。

// app.ts
import { RequestConfig, ErrorShowType } from 'umi'

// 关闭统一错误处理
export const request: RequestConfig = {
  errorConfig: {
    adaptor: resData => {
      return {
        ...resData,
        showType: ErrorShowType.SILENT
      }
    }
  }
}

路由拦截

根据文档的介绍——可以在 app.ts 里写个 render 函数进行拦截。但在实际使用后发现这个函数好像只执行了一次(应用第一次渲染),并且我不知道如何在此判断路由是否存在。这跟我认识中的路由拦截不太一样。

所以,最好还是在 layout 里进行拦截。就像在路由组件外面再包裹一层 PrivateRouter 组件一样(之前项目是这样处理的)。不过现在这个项目不大,权限要求也不高,将就着 app.ts 里拦截也可以。

antd 默认英文

毕竟是要进军全球的企业,组件库默认英文。但好歹提供了配置。

// .umirc.ts
import { defineConfig } from 'umi'

export default defineConfig({
  locale: {
 // 写全,否则不生效
    default: 'zh-CN',
    antd: true,
    baseNavigator: false
  },
})

// src/locales/zh-CN.ts
export default {}

用 updeep 代替 immer

immer 的使用方式并不是很合我心,还是习惯用 updeep。但是,updeep 和 immer 有冲突,所以需要在配置文件里关闭 immer。

// .umirc.ts
export default defineConfig({
  dva: {
    immer: false, // updeep 和 immer 冲突
  },
})

然后,在 dva 里就可以愉快地使用 updeep 了。

import { Dispatch, History, Effect } from 'umi'
import { EffectsCommandMap } from 'dva'
import { AnyAction } from 'redux'
import { message } from 'antd'
import u from 'updeep'

export interface YsAccountsState {
  dataSource: Array<object>
}

const namespace = 'ysAccounts'

export default {
  //……
  effects: {
    // 获取列表
    *_page({ payload }: AnyAction, { call, put, select }: EffectsCommandMap) {

      //……
      if (res && res.result === 0) {
        yield put({
          type: 'updateState',
          payload: {
            dataSource: res.data.rows || [],
          },
        })
      } else {
        message.error(res?.msg || '获取列表失败')
      }
    },
  }
  reducers: {
    updateState(state: YsAccountsState, action: AnyAction) {
      return u(action.payload, state)
    },
  },
}

SvgIcon

react16 支持导入 svg 图标作为组件使用,这也是我使用 umi 的原因之一。不过,图标不能使用中文名称。

import React, { Component } from 'react'
import { ReactComponent as Block1Svg } from '@/icon/svg/block-1.svg'
import styles from './styles.less'

class RightContent extends Component<PropTypes, StateProps> {
  constructor(props: PropTypes) {
    super(props)
    this.state = {
      controlList: [
        {
          component: <Block1Svg />,
          key: 'block1',
        }
      ],
    }
  }

  render() {
    let { controlList } = this.state

    return (
      controlList.map((item, index) => {
        return <div className={styles.controlItem} key={index}>{item.component}</div>
      }
    )
  }
}
// styles.less
.controlItem {
  color: red;
  path {
    fill: currentColor;
  }
}

修改 HTML 模板

在使用 webpack 之类打包工具工程化开发的情况下,不建议修改 HTML 模板。但是有实际情况又需要修改。很多框架也提供了修改方法。umi 也不例外

<!doctype html>
<html>
<head>
  <meta charSet="utf-8" />
  <title>视频监管</title>
  <script src="./js/jquery.min.js" ></script>
</head>
<body>
  <div id="root"></div>
</body>
</html>

TypeScript

虽然有部分人(包括我)不喜欢 TypeScript,但胳膊拧不过大腿,不紧跟潮流以后很难发展。初次使用,想严格遵守规则有点难,毕竟项目逾期就是我的问题了。所以,有些地方还是用了 any。比如 window 对象不存在某个属性的问题。

(<any>window)?.staffInfo?.tenantId

window 问题除了使用 any 大法,还可以修改根目录下 typings.d.ts 解决。

declare module '*.css';
declare module '*.less';
declare module "*.png";
declare module '*.svg' {
  export function ReactComponent(props: React.SVGProps<SVGSVGElement>): React.ReactElement
  const url: string
  export default url
}
interface Window {
  staffInfo?: object
  adminCompany?: object,
  mapCenter?: Array<number>
}

ps:把灵活简约的 JavaScript 变成臃肿蹩脚的 TypeScript,为什么不直接写 c# 呢?

部署

按照文档,部署到非根目录需要配置 base。但是,这个配置只是解决 history 模式路径前缀的。比如 base 是 /foo/,本地路由是 /bar,那么线上路径就是 /foo/bar。对于打包后的文件引用路径,需要配置 publicPath。否则文件引用还是指向根目录。

我的项目不需要使用 history 模式,直接 hash 就行。所以,我不需要配置 base。

// .umirc.js
export default defineConfig({
  publicPath: './',
  history: {
    type: 'hash'
  }
}

打包分析优化

开启 analyze

按照官方的说法,使用 ANALYZE umi build 就可以了。但我把 build 命令改了之后报错了。在 issue 查了一下,发现因为是 umi3.x 的关系,需要先安装 cross-env,命令改成这样:cross-env ANALYZE=1 umi build。

优化 moment

moment 包默认自带所有语言,所以体积很大。所以修改了下 .umirc.js 配置:

// .umirc.js
import { defineConfig } from 'umi'

export default defineConfig({
  // ……
  chainWebpack(memo, { env, webpack, createCSSRule}) {
    memo
      .plugin('ContextReplacementPlugin')
      .use(webpack.ContextReplacementPlugin, [/moment[\/\\]locale$/, /zh-cn/])
      .end()
  },
  // ……
}

总结

简单使用的体验并不是很好,封装了太多,也比较重量级(也许是 typescript 编译太慢的错觉),有时想做些修改会感觉无所适从。但如果是想获得保姆级的开发体验,或许可以使用一下。但还是建议慎重,适用于阿里的不一定适用于你,毕竟出了问题他们可以内部解决,但你不能。

为什么就没有功能强大、轻量好用的框架呢?就像小刀,几乎不需要额外学习成本就可以使用。不过小刀好像不强大😕。