从零开始,构建复杂 React 中后台项目 系列一(基础篇)

·  阅读 606
从零开始,构建复杂 React 中后台项目 系列一(基础篇)

完整项目地址:ice-fusion-admin

前言

为什么我要做这个项目呢,简单来说 2 个目的。一是为了沉淀。目前自己还是做了很多 React 的中后台管理项目,也发现了这些项目的一些共通性,但缺少一个沉淀的项目和机会。因此最近打算从零搭建一个 React 中后台管理项目启动模板,总结目前自己在已有项目中学到的优秀方法。另外可能会有盆友发现项目名称是曾相识,没错,是借鉴了vue-element-admin这个项目,可以说这个项目是 Vue 技术栈里最好的中后台管理模板了,当初这个项目极大的提高了我 Vue 的开发水平,这也就是我做这个项目的第二个原因,帮助新学习 React 技术栈的同学们,希望在这个项目中大家可以相互交流学习。

初始化一个 Ice 基础项目

首先使用 ice cli 初始化一个简单的 Ts 项目 mkdir ice-fusion-admin && cd ice-fusion-admin npm init ice 选择简单模板 image.png 执行 npm install && npm start ,随后便可在浏览器中看到我们初始化的项目了 image.png 随后在 Vscode 中打开项目文件,可以看到如下的文件结构,如果对项目的工程配置需要有其他的定制,可以在 build.json 文件 中编写。所有配置项文档:ice.work/docs/config…

├── build/ # 构建产物
├── mock/ # 本地模拟数据
│ ├── index.[j,t]s
├── public/
│ ├── index.html # 应用入口 HTML
│ └── favicon.png # Favicon
├── src/ # 源码路径
│ ├── components/ # 项目公共组件,局部页面组件请放在对应 page 目录下的 components
│ │ └── Guide/
│ │ ├── index.[j,t]sx
│ │ └── index.module.scss
│ ├── pages/ # 页面
│ │ └── index.tsx/
│ ├── global.scss # 全局样式
│ └── app.[j,t]s[x] # 应用入口脚本
├── README.md
├── package.json
├── .editorconfig
├── .eslintignore
├── .eslintrc.[j,t]s
├── .prettierignore # prettier 忽略文件
├── .prettierrc.js # prettier 插件的配置文件,prettier 是一个美化代码的插件
├── .gitignore
├── .stylelintignore
├── .stylelintrc.[j,t]s
├── .gitignore
└── [j,t]sconfig.json # js/ts 配置文件,配置 js 版本、编译选项等
复制代码

推荐安装 prettier 插件,并和团队协商共同的 prettier 插件的配置文件,可以让编码风格统一 image.png 如果项目中没有生成.prettierrc.js 配置文件的话,可以安装完插件后手动进行配置

// .prettierrc.js
const { getPrettierConfig } = require('@iceworks/spec');
// 直接引入ice提供的react对应prettier配置,我们在项目中也是这样使用
module.exports = getPrettierConfig('react');
复制代码
// .prettierignore
build/
tests/
demo/
.ice/
coverage/
**/*-min.js
**/*.min.js
package-lock.json
yarn.lock
复制代码

引入 Fusion Next 组件库

接下来就是在项目中引入 fusion 的 next 组件库了 首先安装 next 组件库。 npm install @alifd/next --save 随后安装 ice 提供的 fusion 插件 npm install build-plugin-fusion --save-dev 我们可以基于定制 fusion 主题等操作,所有支持的配置项可以查看 ice.work/docs/plugin… 安装完成后,在 build.json 文件中对插件进行配置

{
  "plugins": [
    [
      "build-plugin-fusion",
      {
        "themePackage": "@alifd/theme-design-pro"
      }
    ]
  ]
}
复制代码

随后在项目中引入 next 组件库的 button 检查是否配置成功

//src/components/Guide
import * as React from 'react';
import { Button, Box } from '@alifd/next';
import styles from './index.module.scss';

const Guide = () => {
  return (
    <div className={styles.container}>
      <h2 className={styles.title}>Welcome to icejs!</h2>
      <Box direction="row" spacing={20} justify="center">
        <Button type="normal">Normal</Button>
        <Button type="primary">Prirmary</Button>
        <Button type="secondary">Secondary</Button>
      </Box>
      <p className={styles.description}>This is a awesome project, enjoy it!</p>
    </div>
  );
};

export default Guide;
复制代码

可以看到已经成功引入了 next 组件库 image.png

为应用添加 Layout

layout 也就是布局,页面整体布局是一个产品最外层的框架结构,往往会包含导航、侧边栏、面包屑以及内容等。想要了解一个后台项目,先要了解它的基础布局。从一般定义上来说,非业务的“应用内容”部分,也就是导航菜单、用户信息、页面头部、面包屑导航等部分就是属于 layout。

image.png

这里我们采用@alifd/fusion-design-pro-js 这款模板中提供的 Layout 组件 在 src 目录下新建 layouts 目录存放布局文件

image.png

这里我们使用了 2 个布局,一个是 BasicLayout,也就是包含了导航、侧边栏、面包屑等组件等应用内容布局,另一个是 UserLayout,用于用户登陆、注册等页面。

路由管理

在 ice.js 中可以使用约定式路由、配置式路由 2 种方式,官方推荐使用配置式路由,应用的路由信息统一在 src/routes.ts 中配置我个人也觉得这种配置方式比较好,尤其是在复杂的项目中,而且也方便进行个性化配置。 配置式路由:ice.work/docs/guide/…

约定式路由:ice.work/docs/guide/…

我们新建登陆、项目主页(dashboard),并配合上面导入的 layout 组件,配置完成的路由配置文件如下:

import React from 'react';
import { IRouterConfig } from 'ice';
import UserLayout from '@/layouts/UserLayout';
import BasicLayout from '@/layouts/BasicLayout';
import Login from '@/pages/Login';
import Workplace from '@/pages/Workplace';
import FeedbackNotFound from '@/pages/FeedbackNotFound';

// 懒加载路由
const Register = React.lazy(() => import('@/pages/Register'));

const routerConfig: IRouterConfig[] = [
  {
    path: '/user',
    component: UserLayout,
    children: [
      {
        path: '/login/:username',
        component: Login,
      },
      {
        path: '/register',
        component: Register,
      },
      {
        path: '/',
        redirect: '/user/login',
      },
    ],
  },
  {
    path: '/',
    component: BasicLayout,
    children: [
      {
        path: '/dashboard/workplace',
        component: Workplace,
      },
      {
        // 404 没有匹配到的路由
        component: FeedbackNotFound,
      },
    ],
  },
];

export default routerConfig;
复制代码

但其实在实际项目中,我们还会对路由从不同子应用/子业务层面进行拆分,统一整合后再向外导出:

image.png image.png 最后在 src/app.ts 中,我们可以配置路由的类型和基础路径等信息

import React from 'react';
import { runApp, IAppConfig } from 'ice';

const appConfig: IAppConfig = {
  router: {
    type: 'browser', // 路由类型browser是BrowserHistory模式,hash是HashHistory模式
    basename: '/seller', //统一路径前置路径
    fallback: <div>loading...</div>, //配合路由懒加载的加载组件
    modifyRoutes: (routes) => {
      // 动态路由配置
      return routes;
    },
  },
  app: {
    rootId: 'ice-container',
  },
};

runApp(appConfig);
复制代码

在应用中跳转不同路由和获取参数的方法和使用 react-router 基本一样,其实 Ice 的路由功能也是基于 react-router 的封装

HashHistory 与 BrowserHistory

前端路由通常有两种实现方式:HashHistory 和 BrowserHistory,路由都带着 # 说明使用的是 HashHistory。这两种方式优缺点:

特点\方案HashHistoryBrowserHistory
美观度不好,有 # 号
易用性简单中等,需要 server 配合
依赖 server 端配置不依赖依赖
跟锚点功能冲突冲突不冲突
兼容性IE8IE10
state 传递参数不支持支持

多环境

ice 默认默认情况下支持 start/build 两个环境,对应的即 icejs start/build 两个命令,也就是所谓的本地开发/打包发布代码模式,但是实际开发中会需要多套模式的支持,比如我部门目前一个项目有就通常有日常、预发、线上等 3 套环境,在不同编译环境下会需要启动不同等插件,在 ice 项目中,我们可以通过 --mode 参数来设置不同环境,比如我这里新增加 pre 、daily 模式,在并且配置在这 2 个模式下关闭 mock 服务

image.png

封装一个请求方法

之前有写过一篇详细的文章介绍封装过程,有兴趣的朋友可以看看 👀,基于 Axios 封装一个带缓存功能的请求方法 在 src 目录下建立 utils 文件夹,存放项目中需要的公共方法

image.png

管理所有的接口模块

一般中后台项目都会涉及到数量众多的接口,合理的管理这些接口有助于更好的维护项目,这是我目前项目中 Api 模块等一部分

image.png

如图可见模块有很多,而且随着业务的迭代,模块还会会越来越多。 所以这里建议根据业务模块来划分 pages,在 src 目录下建立 service 文件夹放置 api 模块,并且将 pages 和 service 两个模块一一对应,从而方便维护。如下图:

image.pngimage.png 在此项目中我们先建立一个简单的登陆接口模块

image.png

跨域问题

前后端交互最长遇到的就是跨域问题,我们团队都是通过访问后端地址代理前端资源的方式来解决,也就是直接访问后端服务提供的页面 url 地址,然后将页面中加载的资源代理成本地调试的资源 image.png 这种方案 ice 也提供了对应的支持

  1. 通过 icejs 插件 build-plguin-smart-debug
  2. chrome 插件 xswitch

其他方案还有通过cros、以及本地代理的方式,但 cros 方案需要后端的支持,所以还是看团队协商怎样方便,在这个项目中,我们就使用本地代理的方式来解决跨域问题。其他跨域解决方案可以参考:ice.work/docs/guide/…

我们可以在 build.json 文件中配置 proxy 字段控制跨域,这种方式比较适合简单配置,但并不能完全满足需求,ice 的跨域解决方案本质还是基于http-proxy-middleware实现的,所以支持的配置项和http-proxy-middleware一致。但 json 文件中并不能支持函数配置项(当然也可以开启 JS 类型的配置文件,同时需要在 npm scripts 中指定配置文件),这里我们可以用插件的形式实现功能,使用单独的配置文件来控制跨域配置。 在根目录新建 setupProxy.ts 配置文件,写入配置内容:

const merge = require('lodash/merge');

module.exports = ({ context, onGetWebpackConfig }) => {
  onGetWebpackConfig((config) => {
    // 这里配置需要开启代理的mode,何为mode?see:https://ice.work/docs/guide/basic/config
    // 根据配置的不同启动mode,自动使用不同的代理
    const proxyModes = {
      // 所有支持的参数,详细配置教程 https://github.com/chimurai/http-proxy-middleware
      pre: {
        '/api': {
          target: 'http://www.preServer.org',
        },
        '/preOnly': {
          target: 'http://www.preOnlyServer.org',
        },
      },
      daily: {
        '/api': {
          target: 'http://www.dailyServer.org',
        },
      },
    };
    const { mode } = context.commandArgs;
    if (!proxyModes[mode]) {
      return;
    }
    const proxyRules = Object.entries(proxyModes[mode]);
    const originalDevServeProxy = config.devServer.get('proxy') || [];
    if (proxyRules.length) {
      const proxy = proxyRules
        .map(([match, opts]) => {
          const { target, ...proxyRule } = opts;
          return merge(
            {
              target,
              changeOrigin: true,
              logLevel: 'warn',
              onProxyRes: function onProxyReq(proxyRes, req) {
                proxyRes.headers['x-proxy-by'] = 'ice-proxy';
                proxyRes.headers['x-proxy-match'] = match;
                proxyRes.headers['x-proxy-target'] = target;

                let distTarget = target;
                if (target && target.endsWith('/')) {
                  distTarget = target.replace(/\/$/, '');
                }
                proxyRes.headers['x-proxy-target-path'] = distTarget + req.url;
              },
              onError: function onError(err, req, res) {
                // proxy server error can't trigger onProxyRes
                res.writeHead(500, {
                  'x-proxy-by': 'ice-proxy',
                  'x-proxy-match': match,
                  'x-proxy-target': target,
                });
                res.end(`proxy server error: ${err.message}`);
              },
            },
            proxyRule,
            { context: match }
          );
        })
        .filter((v) => v);
      config.devServer.proxy([...originalDevServeProxy, ...proxy]);
    }
    // http://localhost:3000/api/foo/bar -> http://www.example.org/api/foo/bar
  });
};
复制代码

最后在 build.json 中将 setupProxy.ts 配置为一个插件进行引入 image.png 这样就能在项目编译之前自动执行代理脚本了。这个配置文件本质上就是一个 ice.js 的插件,如果有其他的工程配置需求也可以像这样做个定制插件,并直接在 build.json 中使用,详情 🔎 查看 ice 插件开发指南

前后端的交互和 Mock 数据

从我自己的开发经历来看,平时的开发中交流成本占据了我们很大一部分时间,但前后端如果有一个好的协作方式的话能解决很多时间。我们开发流程都是前后端和产品一起开会讨论项目,之后后端根据需求,首先定义数据格式和 api,有些复杂的接口会和我们前端一起讨论。定好接口规范的后,一般前端会会先对一些页面 UI 进行开发,后端 Swagger 文档写好后我们通过Pont(一个数据调试平台) 生成接口都 mock 数据,进行数据交互开发。有一说一,Pont 是真的好用,强烈推荐,尤其是使用 ts 开发项目。节省了非常多的前后端沟通成本。但也有少数时候后端没写 swwager 的,这时候一般就直接使用本地 Mock 服务了,ice 也提供了开箱即用的 Mock 服务。参见文档:ice.work/docs/guide/… 我们首先在项目根目录下建立 mock 文件夹,mock/ 文件夹下面的所有 js/ts 都会被框架自动加载,因此你可以把接口合理的拆分到多个 mock 文件中,推荐是和在 service 目录下的业务接口文件一一对应,然后在 mock/ 文件夹下面建立 index.ts 存放所有公共的 mock 接口配置。

image.png

占坑

从零开始,构建复杂 React 中后台项目,完整项目地址:ice-fusion-admin

后续内容持续更新中,欢迎关注💕~

分类:
前端
标签:
分类:
前端
标签: