乐趣区

expressmongodbreacttypescriptantd搭建管理后台系统后端前端下

react+typescript+antd 搭建有登录验证的管理后台

在上一篇文章中,我介绍了如何使用 express+mongodb 构建后端项目,现在我再来跟大家介绍如何使用 react+typescript+antd 来构建前端项目

使用 create-react-app 生成 react 的 typescript 项目框架

  • 技术栈

react
typescript
antd
react-router-dom
react-redux
canvas
ES6
cookie

  • 目录结构

assets——存放静态资源
components——存放公共组件
pages——存放页面
store——存放状态管理数据流文件
utils——存放函数工具
api.ts——定义 api 接口文件
App.css——定义全局 css
App.ts——定义项目入口组件
global.d.ts——定义全局的声明文件
index.tsx——入口文件
router.ts——路由配置表

  • 全局安装 create-react-app
npm install -g create-react-app
  • 我们将创建一个名为 react-antd-ts 的新项目:
npx create-react-app antd-demo-ts --typescript
cd react-antd-ts
npm start

此时浏览器会访问 http://localhost:3000/,看到 Welcome to React 的界面就算成功了

  • 引入按需加载 antd
npm install antd --save

此时我们需要对 create-react-app 的默认配置进行自定义,这里我们使用 react-app-rewired(一个对 create-react-app 进行自定义配置的社区解决方案,而不需要 eject 暴露 react 的 webpack 配置文件)。

引入 react-app-rewired 并修改 package.json 里的启动配置。由于新的 react-app-rewired@2.x 版本的关系,你还需要安装 customize-cra。

npm install react-app-rewired customize-cra --save

修改 package.json

"scripts": {
-   "start": "react-scripts start",
+   "start": "react-app-rewired start",
-   "build": "react-scripts build",
+   "build": "react-app-rewired build",
-   "test": "react-scripts test",
+   "test": "react-app-rewired test",
}
  • 使用 babel-plugin-import

babel-plugin-import 是一个用于按需加载组件代码和样式的 babel 插件(原理),现在我们尝试安装它并修改 config-overrides.js 文件。

npm install babel-plugin-import --save

然后在项目根目录创建一个 config-overrides.js 用于修改默认配置
config-overrides.js

const {override, fixBabelImports} = require('customize-cra');
module.exports = override(
  fixBabelImports('import', {
    libraryName: 'antd',
    libraryDirectory: 'es',
    style: 'css',
  }),
);
  • 使用 react-hot-loader 实现代码修改热更新

如果你试一下在现有项目上修改代码,发现整个页面都会刷新,这并不是我们想要的,如果想局部更新,我们可以使用 react-hot-loader 来实现

npm install --save react-hot-loader

修改 App.tsx 文件:
App.tsx

import React from 'react';
import logo from './logo.svg';
import {hot} from 'react-hot-loader/root'
import './App.css';

const App: React.FC = () => {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.tsx</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default process.env.NODE_ENV === "development" ? hot(App) : App
  • 使用 less
npm install --save less-loader less

修改 config-overrides.js:

const {override, fixBabelImports,addLessLoader,addPostcssPlugins} = require('customize-cra');
const rewireReactHotLoader = require('react-app-rewire-hot-loader-for-customize-cra')
module.exports = override(
  fixBabelImports('import', {
    libraryName: 'antd',
    libraryDirectory: 'es',
    style: 'css',
  }),
  addLessLoader({
    strictMath: true,
    noIeCompat: true,
    localIdentName: '[local]--[hash:base64:5]' // if you use CSS Modules, and custom `localIdentName`, default is '[local]--[hash:base64:5]'.
  }),
  addPostcssPlugins([require('autoprefixer')]),
  rewireReactHotLoader());

致此,可以引用 antd 的组件了;注意:如果在构建的时候出现 import React from ‘react’ 的报错,可以 npm install @types/react 进行解决;这里给大家科普一下 typescript 里面的声明文件知识,如果引入的模块提示没有找到声明文件,一般可以用 npm install @types/xxxx(模块名称) 来解决。关于引入模块声明文件的相关知识,可以看看以下两篇文章:
https://www.tslang.cn/docs/ha…
https://www.tslang.cn/docs/ha…

(为节省篇幅,以下只讲一些重点以及一些注意事项,所以会将一些已经写好的组件和页面放到项目中,不再叙述这些页面跟组件的构建,最后会附上项目的 git 地址,可以自行参考)

  • 路由配置

单页应用中的重要部分,就是路由系统。由于不同普通的页面跳转刷新,因此单页应用会有一套自己的路由系统需要维护。

我们当然可以手写一个路由系统,但是,为了快速有效地创建于管理我们的应用,我们可以选择一个好用的路由系统。本文选择了 react-router 4。这里需要注意,在 v4 版本里,react-router 将 WEB 部分的路由系统拆分至了 react-router-dom,因此需要 npmreact-router-dom

npm i --save react-router-dom

本例中我们使用 react-router 中的 BrowserRouter 组件包裹整个 App 应用,在其中使用 Route 组件用于匹配不同的路由时加载不同的页面组件。(也可以使用 HashRouter,顾名思义,是使用 hash 来作为路径)react-router 推荐使用 BrowserRouter,BrowserRouter 需要 history 相关的 API 支持。

首先,需要在 App.tsx 中添加 BrowserRouter 组件,并将 Route 组件放在 BrowserRouter 内。其中 Route 组件接收两个属性:path 和 component,分别是匹配的路径与加载渲染的组件, 把 App.tsx 修改如下:
App.tsx

import React from 'react';
import {BrowserRouter,Route,Switch} from 'react-router-dom';
import {store} from './store';
import {Provider} from 'react-redux';
import Index from './components/Index';
import Login from './pages/login';
import Err from './pages/err';
import './App.css';
import {hot} from 'react-hot-loader/root'
import moment from 'moment';
import 'moment/locale/zh-cn';
moment.locale('zh-cn');
const App: React.FC = () => {
  return (<Provider store={store()}>
      <BrowserRouter>
        <Switch>
          <Route path="/login" exact component={Login}></Route>
          <Route path="/err" exact component={Err}></Route>
          <Route path='/' component={Index}/>
        </Switch>
      </BrowserRouter>
    </Provider>

  );
}

export default process.env.NODE_ENV === "development" ? hot(App) : App
  • 使用 redux 来管理数据流

redux 是 flux 架构的一种实现,redux 并不是完全依附于 react 的框架,实际上 redux 是可以和任何 UI 层框架相结合的。因此,为了更好得结合 redux 与 react,对 redux-flow 中的 store 有一个更好的全局性管理,我们还需要使用 react-redux

npm install --save redux
npm install --save react-redux

同时,为了更好地创建 action 和 reducer,我们还会在项目中引入 redux-actions:一个针对 redux 的一个 FSA 工具箱,可以相应简化与标准化 action 与 reducer 部分。当然,这是可选的

npm install --save redux-actions

注意:如果出现以下情况,上面有提到,需要 npm install @types/react-redux 来解决

1、创建对应的 action。

action 是一个 object 类型,对于 action 的结构有 Flux 有相关的标准化建议 FSA
一个 action 必须要包含 type 属性,同时它还有三个可选属性 error、payload 和 meta。

(1) type 属性相当于是 action 的标识,通过它可以区分不同的 action,其类型只能是字符串常量或 Symbol。
(2) payload 属性是可选的,可以使任何类型。payload 可以用来装载数据;在 error 为 true 的时候,payload 一般是用来装载错误信息。
(3) error 属性是可选的,一般当出现错误时其值为 true;如果是其他值,不被理解为出现错误。
(4) meta 属性可以使任何类型,它一般会包括一些不适合在 payload 中放置的数据。

使用 redux-actions 对 actions 进行创建与管理

(1) createAction(type, payloadCreator = Identity, ?metaCreator)
(2) createAction 相当于对 action 创建器的一个包装,会返回一个 FSA,使用这个返回的 FSA 可以创建具体的 action。
(3) payloadCreator 是一个 function,处理并返回需要的 payload;如果空缺,会使用默认方法。如果传入一个 Error 对象则会自动将 action 的 error 属性设为 true

以下以全局控制侧边栏的收开为例,创建 action 和 reducer
在根目录下新建 store 文件夹,创建 types.ts 文件,用于存放 action 的唯一标识符
store/types.ts

export const TOGGLE_SIDE_BAR = 'TOGGLE_SIDE_BAR';

创建 models.ts,用于存放 store 中初始状态的接口声明
store/models.ts

export interface AppModel {collapsed:boolean;}

创建 actions.ts,用于对所有 action 进行管理
store/actions.ts

import {createAction} from 'redux-actions';

import {AppModel} from './models';

import {TOGGLE_SIDE_BAR} from './types';

const toggleSideBar = createAction<AppModel, boolean>(
  TOGGLE_SIDE_BAR,
  (collapsed: boolean) => {return {collapsed}}
);

export {toggleSideBar} 

创建 reducers.ts
store/reducers.ts

import {handleActions} from 'redux-actions';
import {TOGGLE_SIDE_BAR} from './types';
import {AppModel} from './models'

// 初始的状态, 就像 react 中组件内的初始状态,只不过这个是全局的。const initialState: AppModel = {collapsed:false};

export const toggleSideBarReducer = handleActions<AppModel>({[TOGGLE_SIDE_BAR]: (state:any, action:any) => {
    return {
      ...state,
      collapsed: !state.collapsed
    }
  }
}, initialState);

创建 index.ts,用于组合 store
store/index.ts

import {AppModel} from './models';
import {combineReducers} from 'redux';
import {toggleSideBarReducer} from './reducers';
import {Store, createStore, applyMiddleware} from 'redux';
import thunkMiddleware from 'redux-thunk';
// import {History} from 'history';
interface RootState {app: AppModel}
const rootReducer = combineReducers({app: toggleSideBarReducer});
export function store(initialState?:any): Store<RootState> {

  // store 中间件,根据个人需求添加
  const middleware = applyMiddleware(thunkMiddleware)

  return createStore(
    rootReducer,
    initialState,
    middleware
  ) as Store<RootState>;
}
  • api 请求

1、在 package.json 里面加入

  "homepage": ".",
  "proxy": "http://localhost:3001",

2、创建 api.ts,对全局接口请求进行定义

import http from './utils/http';
const getUserList=()=>{return http.get('/getUserList')
}
const register=(params:{name:string,password:string,phone:number,type:number})=>{return http.post('/register',params)
}
const login=(params:{password:string,phone:number})=>{return http.post('/login',params)
}
const logout=()=>{return http.post('/logout')
}
const userInfo=()=>{return http.post('/userInfo')
}
export {
  getUserList,
  register,
  login,
  logout,
  userInfo
}

3、创建 utils 文件夹,新建 http.ts 文件,对 axios 进行全局设置

npm install --save axios

utils/http.ts

import qs from 'qs'
import {message} from 'antd';
import axios,{AxiosResponse, AxiosRequestConfig} from 'axios'
import {Modal} from 'antd';
import {createBrowserHistory} from 'history';
const axiosConfig: AxiosRequestConfig = {
  // 请求后的数据处理
  transformResponse: [(data: AxiosResponse) => {return data}],
  transformRequest: [(data: any) => {return qs.stringify(data)
  }],
  // 超时设置 s
  timeout: 30000,
  // 跨域是否带 Token
  withCredentials: true,
  responseType: 'json',
  headers: {'Content-Type': 'application/x-www-form-urlencoded'}
}
// 取消重复请求
const pending: Array<{
  url: string,
  cancel: any
}> = []
const cancelToken = axios.CancelToken
const removePending = (config: any) => {

  // tslint:disable-next-line:forin
  for (const p in pending) {
    const item: any = p
    const list: any = pending[p]
    // 当前请求在数组中存在时执行函数体
    if (list.url === config.url + '&request_type=' + config.method) {
      // 执行取消操作
      list.cancel()
      // 从数组中移除记录
      pending.splice(item, 1)
    }
  }
}

const service = axios.create(axiosConfig)

// 添加请求拦截器
service.interceptors.request.use((config: any) => {removePending(config)
    config.cancelToken = new cancelToken((c: any) => {pending.push({ url: config.url + '&request_type=' + config.method, cancel: c})
    })
    return config
  },
  (error: any) => {return Promise.reject(error)
  }
)

// 返回状态判断 (添加响应拦截器)
service.interceptors.response.use((res: any) => {removePending(res.config)
    if (res.data) {// LoadingInterface.close();
      if (res.status === 200) {if (res.data.status === 200) {return res.data.data} else if (res.data.status === 403) {
          // 未登录或者 token 过期,重定向到登录页面
          Modal.info({
            title: '通知',
            content:'登录信息已过期,请重新登录',
            onOk(){const history = createBrowserHistory()
              history.push('/login')
              window.location.reload()}
          })
        } else {message.error(res.data.message)
          return Promise.reject(res.data.message)
        }
      } else {message.error( res.statusText)
        return Promise.reject(res.statusText)
      }

    }
  },
  (error: any) => {message.error('请求失败,请稍后再试')
    return Promise.reject(error)
  }
)

export default service

好嘞,基本的东西就介绍到这里了,完整的项目可以到我的 git 上去下载;
后端 git 地址:https://github.com/SuperMrBea…
前端 git 地址:https://github.com/SuperMrBea…
项目在线预览地址:http://www.wxdriver.com
上一篇文章的地址:https://segmentfault.com/a/11…

退出移动版