前端基本工程搭建

68 阅读21分钟

搭建项目

初始化

安装脚手架

使用 npm npm i @ant-design/pro-cli -g

创建项目

pro create sc-safety-plantform-front

目录结构

├── config # umi 配置,包含路由,构建等配置 
├── mock # 本地模拟数据 
├── public 
│ └── favicon.png # Favicon 
├── src 
│ ├── assets # 本地静态资源 
│ ├── components # 业务通用组件 
│ ├── e2e # 集成测试用例 
│ ├── layouts # 通用布局 
│ ├── models # 全局 dva model 
│ ├── pages # 业务页面入口和常用模板 
│ ├── services # 后台接口服务 
│ ├── utils # 工具库 
│ ├── locales # 国际化资源 
│ ├── global.less # 全局样式 
│ └── global.ts # 全局 JS 
├── tests # 测试工具 
├── README.md 
└── package.json

页面代码结构推荐

为了让项目代码组织更加规范,让开发能够更方便的定位到相关页面组件代码,我们定义了一套规范,该规范当前只作为推荐的指导,并非强制。

src
├── components
└── pages
    ├── Welcome        // 路由组件下不应该再包含其他路由组件,基于这个约定就能清楚的区分路由组件和非路由组件了
    |   ├── components // 对于复杂的页面可以再自己做更深层次的组织,但建议不要超过三层
    |   ├── Form.tsx
    |   ├── index.tsx  // 页面组件的代码
    |   └── index.less // 页面样式
    ├── Order          // 路由组件下不应该再包含其他路由组件,基于这个约定就能清楚的区分路由组件和非路由组件了
    |   ├── index.tsx
    |   └── index.less
    ├── User
    |   ├── components // group 下公用的组件集合
    |   ├── Login      // group 下的页面 Login
    |   ├── Register   // group 下的页面 Register
    |   └── util.ts    // 这里可以有一些共用方法之类,不做推荐和约束,看业务场景自行做组织
    └── *              // 其它页面组件代码

所有路由组件(会配置在路由配置中的组件)我们推荐以大驼峰命名打平到 pages 下面第一级(复杂的项目可以增加 group 层级,在 group 下放置 pages)。不建议在路由组件内部再嵌套路由组件 - 不方便分辨一个组件是否是路由组件,而且不方便快速从全局定位到路由组件。

我们推荐尽可能的拆分路由组件为更细粒度的组件,对于多个页面可能会用到的组件我们推荐放到 src/components 中,对于只是被单个页面依赖的(区块)组件,我们推荐就近维护到路由组件文件夹下即可。

配置git相关

git init git add . git commit -m'init project'

新增页面

这里的『页面』指配置了路由,能够通过链接直接访问的模块,要新建一个页面,通常可以在脚手架的基础上进行简单的配置。

新增 ts、less 文件

在 src / pages 下创建新的 js,less 文件。 如果有多个相关页面,您可以创建一个新文件夹来放置相关文件。

config
src
  models
  pages
+   NewPage.js
+   NewPage.less
  ...
...
package.json

为了更好的演示,我们初始化NewPage.js的内容如下:

export default () => {
  return <div>New Page</div>;
};

暂时不向本文档中的样式文件添加内容,您也可以尝试自己添加内容。

样式文件默认使用CSS Modules,如果需要,可以导入antd less 变量 在文件的头部:

@import '\~antd/lib/style/themes/default.less';

这样可以轻松获取 antd 样式变量并在文件中使用它们,这可以保持保持页面的一致性,并有助于实现自定义主题。

新增布局

在脚手架中我们通过嵌套路由来实现布局模板。routes.ts 是一个数组,其中第一级数据就是我们的布局,如果你需要新增布局可以再直接增加一个新的一级数据。

src/pages/NewPage

export default [
 // user
 {
  path: '/user',
  component: '../layouts/UserLayout',
  routes:[...]
 },
 // app
 {
  path: '/',
  component: '../layouts/BasicLayout',
  routes:[...]
 },
 // new
 {
  path: '/new',
  component: '../layouts/new_page',
  routes:[...]
 },
]

src/layouts/new_page.tsx布局的实例如下:

import React from 'react';

const NewpageLayout: React.FC = ({ children }) => {
  return (
    <div>
      <div>i am new_page layout content</div>
      <div>{children}</div>
    </div>
  );
};

export default NewpageLayout;

配置路由

我们需要在 routes.ts 中使用 component 配置我们页面到路由中。

export default [
  {
    path: '/user',
    component: '../layouts/UserLayout',
    routes: [
      {
        // path 支持为一个 url,必须要以 http 开头
        path: 'https://pro.ant.design/docs/getting-started-cn',
        target: '_blank', // 点击新窗口打开
        name: '文档',
      },
      {
        // 访问路由,以 / 开头为绝对路径
        path: '/user/login',
        // ./Page ->src/pages/Login
        component: './NewPage',
      },
      {
        // 访问路由,如果不是以 / 开头会拼接父路由
        // reg -> /user/reg
        path: 'reg',
        // ./Page ->src/pages/Reg
        component: '../layouts/NewPage2',
      },
    ],
  },
  {
    path: '/new',
    layout: false,
    component: '../layouts/new_page',
    routes: [
      {
        name: 'new',
        icon: 'new',
        path: '/new',
        component: './NewPage',
      },
    ],
  },
];

路由配置完成后,访问页面即可看到效果,如果需要在菜单中显示,需要配置 name,icon,hideChildrenInMenu等来辅助生成菜单。

具体值如下:

  • name:string 配置菜单的 name,如果配置了国际化,name 为国际化的 key。
  • icon:string 配置菜单的图标,默认使用 antd 的 icon 名,默认不适用二级菜单的 icon。
  • access:string 权限配置,需要预先配置权限
  • hideChildrenInMenu:true 用于隐藏不需要在菜单中展示的子路由。
  • hideInMenu:true 可以在菜单中不展示这个路由,包括子路由。
  • hideInBreadcrumb:true 可以在面包屑中不展示这个路由,包括子路由。
  • headerRender:false 当前路由不展示顶栏
  • footerRender:false 当前路由不展示页脚
  • menuRender: false 当前路由不展示菜单
  • menuHeaderRender: false 当前路由不展示菜单顶栏
  • parentKeys: string[] 当此节点被选中的时候也会选中 parentKeys 的节点
  • flatMenu 子项往上提,只是不展示父菜单

新页面效果

多环境

目前系统共配置了dev和prod两个环境变量。环境配置通过环境变量UMI_ENV来识别,config/config.dev.ts和config/config.prod.ts的配置会和config/config.ts做 deep merge 后形成最终配置。

多环境配置

config/config.ts

默认配置

config/config.dev.ts

当环境变量UMI_ENV为dev时的配置。

config/config.prod.ts

当环境变量UMI_ENV为prod时的配置。

多环境启动/打包

命令window.UMI_ENV环境变量说明代理使用说明
yarn startdev以测试环境为基本环境进行启动使用测试环境的代理
yarn start:prodprod以生产环境为基本环境进行启动使用生产环境的代理
yarn builddev以测试环境为基本环境进行打包打包时代理无效
yarn build:prodprod以生产环境为基本环境进行打包打包时代理无效

多环境proxy代理

代理配置通过变量UMI_ENV来区分,前后端代理配置在config/config.[UMI_ENV].ts文件内。不同环境的代理使用说明请参考【多环境-多环境启动/打包】章节。

export default {
  proxy: {
    // localhost:8000/api/** -> https://preview.pro.ant.design/api/**
    '/api/': {
      // 要代理的地址
      target: 'https://preview.pro.ant.design',
      // 配置了这个可以从 http 代理到 https
      // 依赖 origin 的功能可能需要这个,比如 cookie
      changeOrigin: true,
    },
  }
};

request

使用 request

通过 import { request } from 'umi'; 你可以使用内置的请求方法。 request 接收两个参数,第一个参数是 url,第二个参数是请求的 options。options 具体格式参考 umi-request。

request 的大部分用法等同于 umi-request,一个不同的是 options 扩展了一个配置 skipErrorHandler,该配置为 true 是会跳过默认的错误处理,用于项目中部分特殊的接口。

示例代码如下:

request('/api/user', {
  params: {
    name: 1,
  },
  skipErrorHandler: true,
});

request options 参数

参数说明类型可选值默认值
method请求方式stringget , post , put ...get
paramsurl 请求参数object 或 URLSearchParams 对象----
data提交的数据any----
headersfetch 原有参数object--{}
timeout超时时长, 默认毫秒, 写操作慎用number--
timeoutMessage超时可自定义提示文案, 需先定义 timeoutstring----
prefix前缀, 一般用于覆盖统一设置的 prefixstring----
suffix后缀, 比如某些场景 api 需要统一加 .jsonstring----
credentialsfetch 请求包含 cookies 信息string--credentials: 'same-origin'
useCache是否使用缓存(仅支持浏览器客户端)boolean--false
validateCache缓存策略函数(url, options) => boolean--默认 get 请求做缓存
ttl缓存时长, 0 为不过期number--60000
maxCache最大缓存数number--无限
requestTypepost 请求时数据类型stringjson , formjson
parseResponse是否对 response 做处理简化boolean--true
charset字符集stringutf8 , gbkutf8
responseType如何解析返回的数据stringjson , text , blob , formData ...json , text
throwErrIfParseFail当 responseType 为 'json', 对请求结果做 JSON.parse 出错时是否抛出异常boolean--false
getResponse是否获取源 response, 返回结果将包裹一层boolean--fasle
errorHandler异常处理, 或者覆盖统一的异常处理function(error)--
cancelToken取消请求的 TokenCancelToken.token----
{
  // 'method' 是创建请求时使用的方法
  method: 'get', // default

  // 'params' 是即将于请求一起发送的 URL 参数,参数会自动 encode 后添加到 URL 中
  // 类型需为 Object 对象或者 URLSearchParams 对象
  params: { id: 1 },

  // 'paramsSerializer' 开发者可通过该函数对 params 做序列化(注意:此时传入的 params 为合并了 extends 中 params 参数的对象,如果传入的是 URLSearchParams 对象会转化为 Object 对象
  paramsSerializer: function (params) {
    return Qs.stringify(params, { arrayFormat: 'brackets' })
  },

  // 'data' 作为请求主体被发送的数据
  // 适用于这些请求方法 'PUT', 'POST', 和 'PATCH'
  // 必须是以下类型之一:
  // - string, plain object, ArrayBuffer, ArrayBufferView, URLSearchParams
  // - 浏览器专属:FormData, File, Blob
  // - Node 专属: Stream
  data: { name: 'Mike' },

  // 'headers' 请求头
  headers: { 'Content-Type': 'multipart/form-data' },

  // 'timeout' 指定请求超时的毫秒数(0 表示无超时时间)
  // 如果请求超过了 'timeout' 时间,请求将被中断并抛出请求异常
  timeout: 1000,

  // ’prefix‘ 前缀,统一设置 url 前缀
  // ( e.g. request('/user/save', { prefix: '/api/v1' }) => request('/api/v1/user/save') )
  prefix: '',

  // ’suffix‘ 后缀,统一设置 url 后缀
  // ( e.g. request('/api/v1/user/save', { suffix: '.json'}) => request('/api/v1/user/save.json') )
  suffix: '',

  // 'credentials' 发送带凭据的请求
  // 为了让浏览器发送包含凭据的请求(即使是跨域源),需要设置 credentials: 'include'
  // 如果只想在请求URL与调用脚本位于同一起源处时发送凭据,请添加credentials: 'same-origin'
  // 要改为确保浏览器不在请求中包含凭据,请使用credentials: 'omit'
  credentials: 'same-origin', // default

  // ’useCache‘ 是否使用缓存,当值为 true 时,GET 请求在 ttl 毫秒内将被缓存,缓存策略唯一 key 为 url + params + method 组合
  useCache: false, // default

  // ’ttl‘ 缓存时长(毫秒), 0 为不过期
  ttl: 60000,

  // 'maxCache' 最大缓存数, 0 为无限制
  maxCache: 0,

  // 根据协议规范, GET 请求用于获取、查询服务端数据,在数据更新频率不频繁的情况下做必要的缓存能减少服务端的压力,因为缓存策略是默认对 GET 请求做缓存,但对于一些特殊场景需要缓存其他类型请求的响应数据时,我们提供 validateCache 供用户自定义何时需要进行缓存, key 依旧为 url + params + method
  validateCache: (url, options) => { return options.method.toLowerCase() === 'get' },

  // 'requestType' 当 data 为对象或者数组时, umi-request 会根据 requestType 动态添加 headers 和设置 body(可传入 headers 覆盖 Accept 和 Content-Type 头部属性):
  // 1. requestType === 'json' 时, (默认为 json )
  // options.headers = {
  //   Accept: 'application/json',
  //   'Content-Type': 'application/json;charset=UTF-8',
  //   ...options.headers,
  // }
  // options.body = JSON.stringify(data)
  // 2. requestType === 'form' 时,
  // options.headers = {
  //   Accept: 'application/json',
  //   'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
  //   ...options.headers,
  // };
  // options.body = query-string.stringify(data);
  // 3. 其他 requestType
  // options.headers = {
  //   Accept: 'application/json',
  //   ...options.headers,
  // };
  // options.body = data;
  requestType: 'json', // default

  // ’parseResponse‘ 是否对请求返回的 Response 对象做格式、状态码解析
  parseResponse: true, // default

  // ’charset‘ 当服务端返回的数据编码类型为 gbk 时可使用该参数,umi-request 会按 gbk 编码做解析,避免得到乱码, 默认为 utf8
  // 当 parseResponse 值为 false 时该参数无效
  charset: 'gbk',

  // 'responseType': 如何解析返回的数据,当 parseResponse 值为 false 时该参数无效
  // 默认为 'json', 对返回结果进行 Response.text().then( d => JSON.parse(d) ) 解析
  // 其他(text, blob, arrayBuffer, formData), 做 Response[responseType]() 解析
  responseType: 'json', // default

  // 'throwErrIfParseFail': 当 responseType 为 json 但 JSON.parse(data) fail 时,是否抛出异常。默认不抛出异常而返回 Response.text() 后的结果,如需要抛出异常,可设置 throwErrIfParseFail 为 true
  throwErrIfParseFail: false, // default

  // 'getResponse': 是否获取源 Response, 返回结果将包含一层: { data, response }
  getResponse: false,// default

  // 'errorHandler' 统一的异常处理,供开发者对请求发生的异常做统一处理,详细使用请参考下方的错误处理文档
  errorHandler: function(error) { /* 异常处理 */ },

  // 'cancelToken' 取消请求的 Token,详细使用请参考下方取消请求文档
  cancelToken: null,
}

fetch 原其他参数有效, 详见fetch 文档

使用 useRequest

useRequest 是最佳实践中内置的一个 Hook。

import { useRequest } from 'umi';
export default () => {
  const { data, error, loading } = useRequest(() => {
    return services.getUserList('/api/test');
  });
  if (loading) {
    return <div>loading...</div>;
  }
  if (error) {
    return <div>{error.message}</div>;
  }
  return <div>{data.name}</div>;
};

其中 useRequest 的第一个参数接收一个 function,该 function 需要返回一个 Promise,如果你接入了 OneAPI 那么 OneAPI 自动生成的 services 就是一个个这样的 function。

该 Hook 的返回中暴露了各项值,然后你就可以消费它们了,该 Hook 返回的 data 是后端实际返回 JSON 数据中的 data 字段,方便使用(当然你也可以通过配置修改)。更多关于 useRequest 的用法参考它的 API 文档

更多配置你可以参考 @ahooksjs/use-request 的文档,相比 @umijs/use-request 本身, import { useRequest } from 'umi'; 有如下两点差异:

  • 按照接口请求规范内置了 formatResult: res => res?.data 让你可以更方便的使用数据,当然你也可以自己配置 formatResult 来覆盖内置的这个逻辑。
  • 按照接口错误规范统一了错误处理逻辑。

需要注意的是,useRequest 的全局配置,之前为 import { useAPIProvider } from '@umijs/use-request',现在修正为 import { useRequestProvider } from 'umi';

你也可以查看知乎专栏文章《useRequest- 蚂蚁中台标准请求 Hooks》了解 useRequest。

请求前拦截:requestInterceptors

在网络请求的 .then 或 catch 处理前拦截,你可以在 src/app.tsx 网络请求配置内增加如下配置:

export const request: RequestConfig = {
  // 新增自动添加AccessToken的请求前拦截器和自动添加验签的拦截器
  requestInterceptors: [authHeaderInterceptor, securityInterceptor],
};

requestInterceptors 接收一个数组,数组的每一项为一个 request 拦截器。等同于 umi-request 的 request.interceptors.request.use() 。

拦截器示例代码如下:

// src/app.tsx
const authHeaderInterceptor = (url: string, options: RequestConfig) => {
  const authHeader = { Authorization: 'Bearer xxxxxx' };
  return {
    url: `${url}`,
    options: { ...options, interceptors: true, headers: authHeader },
  };
};

更具体内容见 umi-request 的 拦截器文档。

响应后拦截:responseInterceptors

在网络请求响应的 .then 或 catch 处理前拦截处理,使用方法基本和 requestInterceptors 相同。

具体示例代码如下:

// src/app.tsx
const demoResponseInterceptors = (response: Response, options: RequestConfig) => {
  response.headers.append('interceptors', 'yes yo');
  return response;
};

export const request: RequestConfig = {
  responseInterceptors: [demoResponseInterceptors],
};

中间件:middlewares

中间件和拦截器一样,同样可以让开发者优雅地做网络请求前后的增强处理。但是用起来稍复杂,推荐优先使用拦截器。

示例代码如下:

// src/app.tsx
const demo1Middleware = async (ctx: Context, next: () => void) => {
  console.log('request1');
  await next();
  console.log('response1');
};

const demo2Middleware = async (ctx: Context, next: () => void) => {
  console.log('request2');
  await next();
  console.log('response2');
};

export const request: RequestConfig = {
  middlewares: [demo1Middleware, demo2Middleware],
};

执行顺序如下:

request1 -> request2 -> response -> response2 -> response1

强烈建议你再细看一下 umi-request 关于 中间件的文档

统一错误处理

接口请求并不一定是 100% 成功的,但是正常情况下我们预期接口都是成功的,只有网络异常或者权限等问题的情况下才会出现接口请求失败。所以我们通常期望的是代码逻辑只需要考虑成功的情况,对于异常情况只要在一个地方统一处理即可。

在最佳实践中,我们定义了一套接口格式和错误处理的规范,当失败时会统一提示错误,代码只需要考虑成功即可。你可以使用 import { request } from 'umi'; 来使用最佳实践内置的请求方法来获得该能力。

默认的接口格式为:

export interface response {
  success: boolean; // if request is success
  data?: any; // response data
  errorCode?: string; // code for errorType
  errorMessage?: string; // message display to user
  showType?: number; // error display type: 0 silent; 1 message.warn; 2 message.error; 4 notification; 9 page
  traceId?: string; // Convenient for back-end Troubleshooting: unique request ID
  host?: string; // onvenient for backend Troubleshooting: host of current access server
}

当然你也可以通过 app.tsx 中暴露的 request 的运行时配置来修改或者自定义自己项目的一些逻辑,具体参考 @umijs/plugin-request 的文档

当出现 HTTP 错误或者返回的数据中 success 为 false 的情况下 request 会抛出一个异常,当你使用 useRequest 的时候该异常会被 useRequest 捕获,大部分情况下你不需要关心异常的情况,统一的错误处理会做统一的错误提示。对于部分场景需要手动处理错误的时候你可以通过 useRequest 暴露的 onError 方法或者 error 对象来做自定义处理。

后端接口规范不满足的情况下你可以通过配置 errorConfig.adaptor 来做适配。当 success 返回是 false 的情况我们会按照 showType 和 errorMessage 来做统一的错误提示,同时抛出一个异常。

抛出的异常的格式为:

interface RequestError extends Error {
  data?: any; // 这里是后端返回的原始数据
  info?: ErrorInfoStructure;
}

另外你可以通过 Error.name 是否是 BizError 来判断是否是因为 success 为 false 抛出的错误。

统一接口规范

除了上面错误处理所定义的最外层的规范以外,对于 data 内的数据格式我们也提供了一套规范。对于分页场景来说我们推荐后端采用如下的格式,这样前端可以很简单的和 antd 的 Table 组件对接,当然,如果后端不是这个格式也可以使用 useRequest Hook 的 formatResult 配置来做转换。

{
   list: [],
   current?: number,
   pageSize?: number,
   total?: number,
}

参考:后端接口规范建议

为了最后部署的时候能够区分页面和接口,同时也是为了方便前端调试做接口的转发,我们推荐后端接口路径统一添加 /api 的前缀。

另外接口的返回格式建议参考统一的接口规范,方便做统一的错误处理,示例如下:

{
  "success": true,
  "data": {},
  "errorCode": "1001",
  "errorMessage": "error message",
  "showType": 2,
  "traceId": "someid",
  "host": "10.1.1.1"
}

对于简单的可以如下:

{
  "success": true,
  "data": {},
  "errorMessage": "error message"
}

具体参考上面的统一错误处理和统一接口规范。

如果后端返回格式不符合规范的可以参考 @umijs/plugin-request 的文档,配置运行时配置中的 errorConfig.adaptor 兼容。

构建时配置

当前支持的构建时配置如下:

export default {
  request: {
    dataField: 'data',
  },
};

dataField 对应接口统一格式中的数据字段,比如接口如果统一的规范是 { success: boolean, data: any} ,那么就不需要配置,这样你通过 useRequest 消费的时候会生成一个默认的 formatResult,直接返回 data 中的数据,方便使用。如果你的后端接口不符合这个规范,可以自行配置 dataField 。配置为 '' (空字符串)的时候不做处理。

相关文档

Mock

脚手架中约定了两种 mock 的定义方式。

  • 在根目录的 mock 中接入
  • 在 src/page 中的 mock.ts 的文件中配置

一个标准的 mock 由三部分组成,以 List 配置为例。

export default {
  'GET /api/rule': [{ name: '12' }],
  'POST /api/rule': (req: Request, res: Response, u: string) => {
    res.send({
      success: true,
    });
  },
};

第一部分是 网络请求的 Method 配置,完整的列表可以看这里。一般我们都会使用 GET 和 POST。

第二部分是 URL 也就是我们发起网络请求的地址。一般我们会使用统一的前缀方便代理的使用。

第三部分是 数据处理,我们可以配置一个 JSON, JSON 数据会直接返回。或者是配置一个 function,function 有三个参数 req, res,url 。具体使用方式与 express 相同。数据必须要通过 res.send 来返回。

数据流

推荐优先使用 @umijs/plugin-model 插件,它是一种基于 hooks 范式的简易数据管理方案(部分场景可以取代 dva),通常用于中台项目的全局共享数据。

我们都知道自定义 hooks 是逻辑复用的利器,但我们也知道它不能复用状态,就和 react 内置的 hooks 一样,每次调用产生的状态都是相互隔离、无关的。那么,在业务开发中,如果我们需要提取的逻辑和状态都希望能够在多个组件中『共享』,就像其他数据流管理工具(dva, mobx)一样,@umijs/plugin-model 就是一个不错的选择。

新建 Model

在 src/models 目录下新建文件,文件名会成为 model 的 namespace. 允许使用 ts, js, tsx(推荐), jsx(不推荐) 四种后缀。 eg. demo.ts 的 namespace 是 demo

一个 model 的内容需要是一个标准的 JavaScript function,并被默认导出,可以在 function 中使用 hooks.例如下面的例子就是一个合法的 model:

// demo.ts
export default () => 'Hello World';

在实际使用场景中,model 可以包含其他 hooks,例如下面的计数器的例子:

// counter.ts
import { useState, useCallback } from 'react';

export default () => {
  const [counter, setCounter] = useState(0);
  const increment = useCallback(() => setCounter((c) => c + 1), []);
  const decrement = useCallback(() => setCounter((c) => c - 1), []);
  return { counter, increment, decrement };
};

使用 Model

在代码中使用 model,需要从 umi 中导出 useModel。useModel 是一个 React Custom Hook,传入 namespace 即可获取对应 model 的返回值。

仍以NewPage为例:

import { useModel } from 'umi';

export default () => {
  const message = useModel('demo');
  const { counter, increment, decrement } = useModel('counter');
  return (
    <div>
      <div>New Page</div>
      <div>useModel(`demo`)示例:{message}</div>
      <div>
        useModel(`counter`)示例:<button onClick={() => decrement()}>-</button>
        {counter}
        <button onClick={() => increment()}>+</button>
      </div>
    </div>
  );
};

Model 性能优化

useModel 可以接受一个可选的第二个参数,可以用于性能优化。当组件只需要消费 model 中的部分参数,而对其他参数的变化并不关心时,可以传入一个函数用于过滤。函数的返回值将取代 model 的返回值,成为 useModel 的最终返回值。例如:

import { useModel } from 'umi';

export default () => {
  const { add, minus } = useModel('counter', (ret) => ({
    add: ret.increment,
    minus: ret.decrement,
  }));

  return (
    <div>
      <button onClick={add}>add by 1</button>
      <button onClick={minus}>minus by 1</button>
    </div>
  );
};

上面的组件对 counter 的值并不关心,只需要使用 increment 和 decrement 两个方法,因此使用第二个参数,过滤掉了 counter 这一频繁变化的值。

相关 umi 插件

  • plugin-initial-state 配合 plugin-initial-state 可以快速在组件内获取到全局初始状态
  • @umijs/plugin-model 配合 vscode 插件,启用后 command + 鼠标左键点击(转到定义) useModel('namespace') 中 namespace 的字符串,即可跳转到对应的 model 文件。
  • plugin-qiankun 配合 plugin-qiankun 可以在子应用中获取到主应用传递的 props

全局初始化数据

全局初始化数据包括用户的角色权限信息或者其他一些页面间共享的数据。

const { initialState, setInitialState } = useModel('@@initialState');

图标

iconfont使用(推荐)

建议使用iconfont图标Symbol方式引入。首先将iconfont文件内容拷贝到public/iconfont/文件夹下。并按照以下步骤进行操作:

第一步:封装IconFont公共组件:

在 document.ejs 中的配置:

import { createFromIconfontCN } from '@ant-design/icons';

const IconFont = createFromIconfontCN({
  scriptUrl: '/iconfont/iconfont.js',
});

export default IconFont;

第二步:挑选相应图标并获取类名,应用于页面:

import IconFont from '@/components/IconFont'

const App: React.FC = () => (
  <Space>
    <IconFont type="icon-tuichu" />
    <IconFont type="icon-facebook" />
    <IconFont type="icon-twitter" />
  </Space>
);

export default App;

后续每次iconfont更新只需要将iconfont/文件拷贝到public下即可。

svg组件使用

在React中可以使用组件式或者url式任一方式引入:

组件式引入

import { ReactComponent as Logo } from './logo.svg'

function Analysis() {
  return <Logo width={90} height={120} />
}

url方式引入

import logoSrc from './logo.svg'

function Analysis() {
  return <img src={logoSrc} alt="logo" />
}

自定义Icon图标使用

import Icon, { HomeOutlined } from '@ant-design/icons';
import type { CustomIconComponentProps } from '@ant-design/icons/lib/components/Icon';
import { Space } from 'antd';
import React from 'react';

const HeartSvg = () => (
  <svg width="1em" height="1em" fill="currentColor" viewBox="0 0 1024 1024">
    <path d="M923 283.6c-13.4-31.1-32.6-58.9-56.9-82.8-24.3-23.8-52.5-42.4-84-55.5-32.5-13.5-66.9-20.3-102.4-20.3-49.3 0-97.4 13.5-139.2 39-10 6.1-19.5 12.8-28.5 20.1-9-7.3-18.5-14-28.5-20.1-41.8-25.5-89.9-39-139.2-39-35.5 0-69.9 6.8-102.4 20.3-31.4 13-59.7 31.7-84 55.5-24.4 23.9-43.5 51.7-56.9 82.8-13.9 32.3-21 66.6-21 101.9 0 33.3 6.8 68 20.3 103.3 11.3 29.5 27.5 60.1 48.2 91 32.8 48.9 77.9 99.9 133.9 151.6 92.8 85.7 184.7 144.9 188.6 147.3l23.7 15.2c10.5 6.7 24 6.7 34.5 0l23.7-15.2c3.9-2.5 95.7-61.6 188.6-147.3 56-51.7 101.1-102.7 133.9-151.6 20.7-30.9 37-61.5 48.2-91 13.5-35.3 20.3-70 20.3-103.3 0.1-35.3-7-69.6-20.9-101.9z" />
  </svg>
);

const HeartIcon = (props: Partial<CustomIconComponentProps>) => (
  <Icon component={HeartSvg} {...props} />
);

const App: React.FC = () => (
  <Space>
    <HeartIcon style={{ fontSize: '32px', color: 'hotpink' }} />
    <Icon component={HomeOutlined as React.ForwardRefExoticComponent<any>} />
  </Space>
);

export default App;

使用 less(完成)

less 作为 css 的超集,提供了很多 css 没有功能,其中最方便的就是变量。antd 中提供了丰富的变量来帮助我们进行开发。同时也让我的自定义主题变得更加简单,以下是 antd 提供的一些常用变量。所有样式变量可以在 这里 找到。

@primary-color: #1890ff; // 全局主色
@link-color: #1890ff; // 链接色
@success-color: #52c41a; // 成功色
@warning-color: #faad14; // 警告色
@error-color: #f5222d; // 错误色
@font-size-base: 14px; // 主字号
@heading-color: rgba(0, 0, 0, 0.85); // 标题色
@text-color: rgba(0, 0, 0, 0.65); // 主文本色
@text-color-secondary: rgba(0, 0, 0, 0.45); // 次文本色
@disabled-color: rgba(0, 0, 0, 0.25); // 失效色
@border-radius-base: 2px; // 组件/浮层圆角
@border-color-base: #d9d9d9; // 边框色
@box-shadow-base: 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08),
  0 9px 28px 8px rgba(0, 0, 0, 0.05); // 浮层阴影

使用这些变量:

@import '~antd/es/style/themes/default.less';

.page {
  height: 100vh;
}

.myLink {
  color: @primary-color;
  font-size: @font-size-base;
}

SuperMap iClient for OpenLayers

开发时需要引入 OpenLayers 和 SuperMap iClient for OpenLayers。其中,OpenLayers 具体包括 CSS 文件和 JS 文件。你可以通过以下几种方式获取这些文件:

官网

iclient.supermap.io/web/introdu…

openlayers.org/

安装

iclient.supermap.io/download/do…

npm install @supermap/iclient-ol

引入css


配置为按需引入

首先,安装 @supermap/babel-plugin-import:

npm install @supermap/babel-plugin-import -D

然后,在config.tx中添加如下配置:

import { defineConfig } from 'umi';

export default defineConfig({
  extraBabelPlugins: [
    [
      '@supermap/babel-plugin-import',
      {
        libraryName: '@supermap/iclient-ol',
      },
    ],
  ],
});

接下来,如果你只希望引入部分组件,比如 TileSuperMapRest,那么需要写入以下内容:

import React, { useEffect } from 'react';
import Map from 'ol/Map';
import View from 'ol/View';
import TileLayer from 'ol/layer/Tile';
import { TileSuperMapRest } from '@supermap/iclient-ol';
import styles from './index.less';

const url = 'https://iserver.supermap.io/iserver/services/map-world/rest/maps/World';

const MapDemo: React.FC = () => {
  useEffect(() => {
    const map = new Map({
      target: 'map',
      view: new View({
        center: [0, 0],
        zoom: 2,
        projection: 'EPSG:4326',
      }),
    });
    const layer = new TileLayer({
      source: new TileSuperMapRest({
        url: url,
        wrapX: true,
      }),
      projection: 'EPSG:4326',
    });
    map.addLayer(layer);
  }, []);
  return (
    <div className={styles.container}>
      <div id="map" className={styles.map} />
    </div>
  );
};

export default MapDemo;

基础工程搭建参考Demo

iclient.supermap.io/web/introdu…

iclient.supermap.io/examples/op…

iclient.supermap.io/web/introdu…

openlayers.org/

TypeScript参考文档

www.typescriptlang.org/docs/

typescript.bootcss.com/tutorials/r…

github.com/typescript-…

pro.ant.design/zh-CN/docs/…