【React系列】手把手带你撸后台系统(架构篇)

11,218 阅读7分钟

一、前言

本系列文章计划撰文3篇:

项目地址:github react-admin

系统预览:react-admin system

本系列文章将介绍从零开始搭建一个高可复用的后台架构系统,让每一个人都能轻松搭出自己的后台。系统功能包含登录授权路由鉴权组件化,涉及react-routerreact-redux的应用。系统最终实现的效果:

本篇介绍的内容包含:

  • 脚手架环境搭建与构建配置说明
  • 基础架构设计
  • 侧边栏实现
  • 初探路由

二、React环境搭建

2.1 脚手架环境

创建一个React App,目测有3到4种方法,这是官网文档的说明,可以移步查看。我们这里主要介绍npxnpm两种方式。

2.1.1 什么是npx?

npxnpm@5.2版本新增的命令,如果你的npm版本是高于这个版本的,那么你可以直接使用npx命令了;否则就需要全局安装:npm install -g npx。但个人建议是升级npmnpm install -g npm@next

npx的愿景就是:让天下没有难调用的模块。譬如我们项目中集成了Eslint模块,我们想要在命令行调用该模块必须像这样:./node_modules/.bin/eslint --init;有了npx,我们可以直接:npx eslint --init

同时,**npx还能避免全局安装,真正的属于用完即走。**早先以前,我们使用create-react-app创建react应用,必须要全局安装该模块,react script才可用。现在,通过npx执行npx create-react-app my-appnpx会将create-react-app下载在一个临时目录,并让react script全局可用,用完之后再删除,待下次需要时再重新下载。

使用npx创建本后台应用:npx create-react-app my-admin

2.1.2 npm init经历了什么?

在@5.2版本之前,执行npm init会在当前目录创建一个package.json文件,而在@5.2及之后的版本,我们可以通过npm init <initializer>命令创建一个应用,譬如:npm init react-app my-app。其本质也是内部调用npx命令,会自动将react-app补全为create-react-app,继而下载并创建应用。

使用npm init创建本后台应用:npm init react-app my-admin

2.2 构建配置

在上一步骤创建应用之后,默认的目录架构像下面这样:

执行npm eject命令会生成configscript两个目录,目录下是与项目相关的webpack配置文件,我们可以根据需要自定义构建配置。运行命令后的目录架构如下:
image

其次,我们还可以在一些特殊文件中定义配置信息:新建jsconfig.json文件,并填充如下内容:

{
    "compilerOptions": {
      "baseUrl": "src"  // 编译根路径
    }
}

2.3 目录说明

最终我们系统的目录架构如下:

├── config   // 配置相关   
├── script   // 构建脚本 
├── public   // 应用对外目录 
├── src      // 源代码 
│   ├── components  // 公共组件
│   ├── font        // 字体相关
│   ├── js          // js库
│   ├── router      // 路由相关
│   ├── scss        // 样式表
│   ├── store       // redux相关
│   ├── views       // 页面应用
│   ├── index.js    // 入口文件
│   ├── Page.js     // 页面路由入口
│   ├── App.js      // 主应用入口

三、基础架构

从主应用入口文件·App.js·开始,根据我们系统后台的布局,用以下代码替换原有代码:

import React, { Component } from 'react'
class App extends Component {
  render () {
    return (
      <div className="container">
        <section className="sidebar">
          侧边导航栏
        </section>
        <section className="main">
          <header className="header">
            <span className="username">Hi, 安歌</span>
          </header>
          <div className="wrapper">
            主体内容
          </div>
          <footer className="footer">
            <span className="copyright">Copyright@2019 安歌</span>
          </footer>
        </section>
      </div>
    )
  }
}
export default App

加上样式:在scss目录新建index.scss,同时删除根目录的App.css文件(根据个人习惯选择scss或其他的预编译语言),并在index.js中添加样式表的引用。可以得到如下效果:

四、侧边导航栏

4.1 在components目录新建SideBar.js组件,同时在router目录下新建路由配置文件config.js,这份配置文件由侧边栏跟路由共用:

// Sidebar.js: 侧栏导航组件,侧栏菜单在router/config.js配置
import React, { Component } from 'react'
class SideBar extends Component {
    constructor (props) {
        super(props)
        this.state = {
            routes: []  // 路由列表
        }
    }
    render () {
        return (
            <ul className="sidebar-wrapper">
                侧边栏
            </ul>
        )
    }
}
export default SideBar

4.2 定义路由配置文件:支持多级嵌套,routescomponent不能同级共存,如果存在子菜单,则用routes字段,否则使用component字段。

// router/config.js
export default [
    {
        title: '我的事务', // 页面标题&一级nav标题
        icon: 'icon-home',
        routes: [{
            name: '待审批',  // 次级nav标题
            path: '/front/approval/undo', // 路由url
            component: 'ApprovalUndo'  // 路由组件
        }, {
            name: '已处理',
            path: '/front/approval/done',
            auth: 'add',  // 访问所需权限
            component: 'ApprovalDone'
        }]
    },
    // ...
]

4.3 使用递归渲染侧边栏

// SideBar.js
import React, { Component, Fragment } from 'react'
class SideBar extends Component {
    constructor (props) {
        // ...
        this.generateSidebar = this.generateSidebar.bind(this)
    }
    render () {
        return ( // 渲染侧边栏
            <ul className="sidebar-wrapper">
                { map(this.generateSidebar, this.state.routes) }
            </ul>
        )
    }
    generateSidebar (item) { // 一级nav
        return <li className="sidebar-item" key={item.title}>
            <div className={ className({
                'sidebar-item-name': true,
                'on': item.active  /* 当前菜单展开/收起标识 */
            }) }>
                <span>
                    { item.title }
                </span>
            </div>
            <ul className="sidebar-sub">
                { this.generateSubMenu(item.routes) }
            </ul>
        </li>
    }
    generateSubMenu (routes) { // 子级nav
        return map(each => <li className="sidebar-sub-item" key={ each.name }>
            { each.component ? <a href={ each.path }>{ each.name }</a> : (
                <Fragment>
                    <div className={ className({
                        'sidebar-item-name': true,
                        'on': each.active // 当前菜单展开/收起标识
                    }) }>
                        { each.name }
                    </div>
                    <ul className="sidebar-sub">
                        { this.generateSubMenu(each.routes) }
                    </ul>
                </Fragment>
            ) }
        </li>, routes)
    }
}

加上样式之后如下图:

SideBar.js中预留两个API留待下篇解说:

  • checkActive: 根据当前访问路由检测菜单项的收合状态;
  • hasPer: 检测当前用户是否有菜单项的访问权限,有则渲染,无则跳过渲染;

五、基础路由

我们需要借助react-router实现几个页面:登录、404以及权限错误。这些属于页面级路由,可以归在views目录下,这里我们在views目录新建login目录作为登录应用、components目录下新建404.jsAuthError.js,将其视作组件:

├── components   // 公共组件 
│   ├── 404.js          // not found
│   ├── AuthError.js    // permission error
├── views       // 公共组件 
│   ├── login           // 登录应用
│   │   ├── index.js    // 登录页面入口

Page.js注册路由:

// Page.js
import React, { Component } from 'react'
import { BrowserRouter as Router, Switch, Route, Redirect } from 'react-router-dom'
import App from 'App'
import AsyncComponent from 'components/AsyncComponent'
const Login = AsyncComponent(() => import(/* webpackChunkName: "login" */ 'views/login'))
const NotFound = AsyncComponent(() => import(/* webpackChunkName: "404" */ 'components/404'))
const AuthError = AsyncComponent(() => import(/* webpackChunkName: "autherror" */ 'components/AuthError'))
class Page extends Component {
    render () {
        return (
            <Router>
                <Switch>
                    <Route exact path="/" render={ () => <Redirect to="/front/approval/undo" push /> } />
                    <Route path="/front/404" component={ NotFound }/>
                    <Route path="/front/autherror" component={ AuthError } />
                    <Route path="/front/login" render={ () => {
                        const isLogin = false // 登录状态从redux获取
                        return isLogin ?  <Redirect to="/front/approval/undo" /> : <Login />
                    } } />
                    <Route render={ () => <App /> } />
                </Switch>
            </Router>
        )
    }
}
export default Page

Route接口用于注册路由并定义渲染逻辑,Page.js引入了主应用App.js文件,因此主入口文件index.js需要引入Page.js并渲染(将原来的引入App.js改成Page.js即可)。

六、异步组件

在上一章节基础路由部分我们用到了一个组件函数AsyncComponent,它是一个工厂函数,用于异步解析我们的组件定义。在大型应用中,我们会使用很多的异步组件,譬如() => import(/* webpackChunkName: "login" */ 'views/login')是一个异步组件,它返回一个promise,在React里面,我们需要自己写一个工厂函数来解析,让Promise变为React可用的组件。AsyncComponent的逻辑很简单:异步解析和渲染:

// AsyncComponent.js
import React, { Component } from 'react'
export default function asyncComponent (importComp) {
    class AsyncComponent extends Component {
        constructor (props) {
            super(props)
            this.state = {
                component: null
            }
        }
        async componentDidMount () {
            const { default: component } = await importComp()
            this.setState({
                component: component
            })
        }
        render () {
            const C = this.state.component
            return C ? <C {...this.props} /> : null
        }
    }
    return AsyncComponent
}

如上一顿操作之后,我们访问/front/login/front/404/front/autherror就能分别访问到登录、Page Not Found和Permission Error页面了,访问根路径会被重定向到/front/approval/undo

以上几个路由属于页面级路由,访问无需授权,在下一篇中我们介绍应用级路由,并实现路由鉴权功能。

其他系列文章

项目地址:github react-admin

系统预览:react-admin system