React进阶-项目构建

512 阅读9分钟

本片文章主要是如何搭建一个企业级项目需要的大致的配置,跟着这篇文章练完手你可以学会配置如下库:

  • 集成 antd
  • 路径别名 @
  • react-router
  • styled-components
  • redux
  • 配置全局 axios
  • eslint
  • prettier
  • 自动风格校验
  • router 鉴权和守卫
  • lodash
  • react-loadable

我希望屏幕前的你不要收藏一下就完事了,如果您没有搭建项目的经验请跟着本片文章练一遍,这是初中级前端走向高级前端必经之路!!!看完这篇文章的您要是有什么好的建议请评论!咱们共同成长!

脚手架搭建项目

create-react-app 项目名称 --typescript

项目搭建成功后删除一些不必要的文件,这里我删除的少,这里没啥规范,根据情况自己删除。

  • App.css
  • App.test.tsx
  • index.css
  • log.svg

删除文件后的目录:看看就行!

新建目录

 /src
   ├─ /common            <-- 全局公用目录
   |  ├─ /fonts          <-- 字体文件目录
   |  ├─ /images         <-- 图片文件目录
   |  └─ /style          <-- 公用样式文件目录
   |  |  ├─ frame.css    <-- 全部公用样式(引入其他css)
   |  |  ├─ reset.css    <-- 清零样式
   |  |  └─ global.css   <-- 全局公用样式
   ├─ /components        <-- 公共模块组件目录
   ├─ /pages             <-- 页面组件目录
   ├─ /store			       <-- 存放根store目录
   ├─ /utils			       <-- 存放工具目录
   ├─ App.js             <-- 项目主模块
   └─ index.js           <-- 项目入口文件
  • frame.css
/* 全部公用样式,主要是引用其他的样式 */
@import './global.css';
@import './reset.css';
  • reset.css
/* 全局清除的样式,可以在此文件中重置antd的一些样式 */
  • global.css
/* 全局公用的样式 */
/* 全局公用的样式 */
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

*::before,
*::after {
    box-sizing: border-box;
}

a {
    color: inherit;
    text-decoration: none;
}

a:active,
a:hover {
    outline: 0;
}

img {
    display: block;
}

input,
button {
    font-family: inherit;
}

ol,
ul {
    list-style: none;
}

table {
    border-collapse: collapse;
    border-spacing: 0;
}
  • index.tsx
...
// 引入全局样式
import "./common/style/frame.css";
...

在浏览器中F12检查元素就可以看到我们的全局的样式代码,就证明全局样式已经生效。由于操作过于简单请自行查看!!!

集成 antd

  • 首先下载 antd
npm install antd babel-plugin-import
  • 高级配置
npm install react-app-rewired customize-cra

有了cra我们就不需要将webpack的配置文件释放出来了,毕竟这个过程是不可逆的。

  • 修改 package.json 文件
/* 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",
-   "eject": "react-scripts eject",
+   "eject": "react-app-rewired eject"
}
  • 根目录创建 config-overrides.js
/** config-overrides.js */
const {
  override,
  fixBabelImports
} = require("customize-cra");

module.exports = override(
  // 按需加载组件代码和样式
  fixBabelImports("import", {
    libraryName: "antd",
    libraryDirectory: "es",
    style: true,
  }),
);

  • 定制主题

自定义主题需要用到 less 变量覆盖功能

npm install less less-loader

继续配置config-overrides.js文件

/** config-overrides.js */
const {
  override,
  fixBabelImports,
  addLessLoader,
} = require("customize-cra");

module.exports = override(
  // 按需加载组件代码和样式
  fixBabelImports("import", {
    libraryName: "antd",
    libraryDirectory: "es",
    style: true,
  }),
  // 加载 less 样式
  addLessLoader({
    lessOptions: {
      javascriptEnabled: true,
      modifyVars: { "@primary-color": "#1DA57A" },
    },
  })
);

  • 配置路径别名
const {
  override,
  fixBabelImports,
  addWebpackAlias,
  addLessLoader,
  addDecoratorsLegacy
} = require("customize-cra");
const path = require("path");

module.exports = override(
  // 按需加载组件代码和样式
  fixBabelImports("import", {
    libraryName: "antd",
    libraryDirectory: "es",
    style: true,
  }),
  // 将提供的别名信息添加到webpack的别名部分。传递一个包含任意数量的对象文字,整个对象将被合并进来。
  addWebpackAlias({ "@": path.resolve(__dirname, "src") }),
  // 加载 less 样式
  addLessLoader({
    lessOptions: {
      javascriptEnabled: true,
      modifyVars: { "@primary-color": "#1DA57A" },
    },
  }),
  // 在传统模式下添加装饰器。
  addDecoratorsLegacy()
);

在根目录新建paths.json文件

/**paths.json*/
{
  "compilerOptions": {
  + "baseUrl": ".", // 设置baseUrl来告诉编译器到哪里去查找模块。 所有非相对模块导入都会被当做相对于 baseUrl。
  + "paths": {
        "@/*": ["src/*"] // 请注意"paths"是相对于"baseUrl"进行解析。
      }
  }
}

修改tsconfig.json文件

/**tsconfig.json*/
...
+ "extends": "./paths.json"

集成antd差不多就结束了,现在赶紧试一试组件和路径别名@吧!!!

  • 修改 index.tsx
// 引入全局样式
- import "./common/style/frame.css";
+ import "@/common/style/frame.css";
  • 修改 App.tsx
+ import { Button } from "antd";
...
+ <Button>按钮</Button>

操作完毕,npm start 试一试!操作太简单,自行查看!!!

添加 styled-components

npm install styled-components @types/styled-components

在components文件夹下我们创建Navigate文件夹,开始书写我们的全局导航栏

├─ /components      <-- 公共模块组件目录
|	└─ /Navigate   	  <-- 全局公用导航栏
|   |	├─ style.ts   <-- 导航样式
|  	|	└─ index.tsx  <-- 导航栏组件
/** style.ts */
import styled from "styled-components";

export const NavContainer = styled.div`
  background: linear-gradient(90deg, #53a0fd, #5bd7d8);
  height: 50px;
  color: #fff;
  display: flex;
  justify-content: flex-start;
  line-height: 50px;
  padding: 0 20px;
  a + a {
    margin-left: 25px;
  }
`;

书写全局导航栏组件

/** index.tsx */
import * as React from "react";

// 样式
import { NavContainer } from "./style";

export interface NavigateProps {}

export interface NavigateState {}

class Navigate extends React.Component<NavigateProps, NavigateState> {
  constructor(props: NavigateProps) {
    super(props);
    this.state = {};
  }
  render() {
    return <NavContainer>导航菜单</NavContainer>;
  }
}

export default Navigate;

调用导航栏组件

/**App.tsx*/
...
// 组件
+ import Navigate from "@/components/Navigate/index";
...
render() {
    return (
      <div>
        <Navigate />
        <Button>按钮</Button>
      </div>
    );
  }

添加 react-router

npm install react-router-dom @types/react-router-dom

目录结构:

├─ /pages        	  <-- 页面文件
|	├─ /Home 	  	    <-- Home文件
|  	|	└─ index.tsx  <-- Home页面
|	└─ /User   	  	  <-- User文件
|  	|	└─ index.tsx  <-- User页面

书写Home页面需要的代码:

/** Home index.tsx*/
import * as React from "react";

export interface HomeProps {}

export interface HomeState {}

class Home extends React.Component<HomeProps, HomeState> {
  constructor(props: HomeProps) {
    super(props);
    this.state = {};
  }
  render() {
    return (
      <div>
        <h1>Home页面</h1>
      </div>
    );
  }
}

export default Home;

书写User页面代码:

/** User index.tsx*/
import * as React from "react";

export interface UserProps {}

export interface UserState {}

class User extends React.Component<UserProps, UserState> {
  constructor(props: UserProps) {
    super(props);
    this.state = {};
  }
  render() {
    return (
      <div>
        <h1>User页面</h1>
      </div>
    );
  }
}

export default User;

修改导航栏组件Navigate:

/** Navigate  index.tsx*/
...
+ import { Link } from "react-router-dom";
...

<NavContainer>
    <Link to="/home">Home</Link>
    <Link to="/user">User</Link>
</NavContainer>

修改App.tsx:

...
+ import {
  BrowserRouter as Router,
  Route,
  Switch,
  Redirect,
} from "react-router-dom";
// 组件
...
+ import Home from "@/pages/Home";
+ import User from "@/pages/User";
...
render() {
    return (
      <Router>
        <div>
          <Navigate />
          <Switch>
            <Route path="/home" component={Home} />
            <Route path="/user" component={User} />
            <Redirect from="/" to="/home" />
          </Switch>
        </div>
      </Router>
    );
  }

添加了路由和样式,我们npm start 看看!

添加 js-cookie

这个没啥讲的,就下载就Ok了,主要是用它判断登录状态之类的!

npm install js-cookie @types/js-cookie

配置全局的 Axios

npm install axios

在utils目录下添加api.ts文件,配置全局的Axios

├─ /utils        	  <-- 公共工具目录
|  	└─ api.ts 	  	  <-- axios请求文件
/**utils api.ts */
import axios, { AxiosRequestConfig, AxiosError, AxiosResponse } from "axios";
import Cookies from "js-cookie";
import { message } from "antd";

//错误代码的枚举
const enum HTTP_ERROR_CODE {
  CODE_400 = "请求错误(400)",
  CODE_401 = "未授权,请重新登陆(401)",
  CODE_403 = "拒绝访问(403)",
  CODE_404 = "请求出错(404)",
  CODE_406 = "请求参数错误(406)",
  CODE_408 = "请求超时(408)",
  CODE_500 = "服务器错误(500)",
  CODE_501 = "服务器未实现(501)",
  CODE_502 = "网络错误(502)",
  CODE_503 = "服务不可用(503)",
  CODE_504 = "网络超时(504)",
}
// 服务器错误代码
const enum SERVER_ERROR_CODE {
  CODE_50000 = "服务器错误!",
  CODE_51000 = "未授权,请重新登录",
}

// 全局axios配置
axios.defaults.baseURL = "";
axios.defaults.withCredentials = true; //表示跨域请求时是否需要使用凭证
axios.defaults.headers.common["Content-Type"] = "application/json";
axios.defaults.timeout = 60 * 1000; // 接口响应时间为1分钟

// http request 拦截器
axios.interceptors.request.use(
  (config: AxiosRequestConfig) => {
    // 可以再此进行全局的请求接口Loading展示
    // TODO--> Loading

    // 验证toKen
    const token = Cookies.get("");
    if (!token) config.headers.common.token = token;
    return config;
  },
  (error: AxiosError) => {
    message.error(error);
    return Promise.reject(error);
  }
);

// http response 拦截器
axios.interceptors.response.use(
  (response: AxiosResponse<any>) => {
    // 清除全局的Loading状态
    // TODO
    return response;
  },
  (error: AxiosError) => {
    // 清除全局的Loading状态
    // TODO
    // 在这里输出响应的错误信息,这个只是例子
    message.error(HTTP_ERROR_CODE.CODE_400);
    return Promise.reject(error);
  }
);

// 全局的接口的权限解析
const permissionParser = (res: any): Promise<any> => {
  return new Promise(async (resole, reject) => {
    if (res && res.data && res.data.code === 51000) {
      message.warning(SERVER_ERROR_CODE["CODE_51000"]);
      Cookies.remove("");
      window.location.href = "/login";
      reject(SERVER_ERROR_CODE["CODE_51000"]);
    } else {
      resole(res);
    }
  });
};

// 默认解析
const defaultParser = (res: any): Promise<any> => {
  return new Promise((resolve, reject) => {
    if (res && res.data && res.data.code) {
      const { code, data } = res.data;
      if (code === 20000) {
        resolve(data);
      } else if (code === 20001) {
        resolve(res.data.message);
      } else {
        message.error(res.data.message);
        reject(res.data.message);
      }
    } else {
      message.error("接口错误!");
      reject("接口错误!");
    }
  });
};

// get
export const get = (url: string, query?: object): Promise<any> => {
  return new Promise((resolve) => {
    axios(url, {
      params: query,
    })
      .then(permissionParser)
      .then(defaultParser)
      .then((res) => resolve(res))
      .catch((e) => console.error(e));
  });
};

// post
export const post = (url: string, data?: object, query?: object) => {
  return new Promise((resolve) => {
    axios(url, {
      method: "POST",
      params: query,
      data,
    })
      .then(permissionParser)
      .then(defaultParser)
      .then((res) => resolve(res))
      .catch((e) => console.error(e));
  });
};

// put

export const put = (url: string, data?: object, query?: object) => {
  return new Promise((resolve) => {
    axios(url, {
      method: "PUT",
      params: query,
      data,
    })
      .then(permissionParser)
      .then(defaultParser)
      .then((res) => resolve(res))
      .catch((e) => console.error(e));
  });
};

// delete
export const del = (url: string, data?: object, query?: object) => {
  return new Promise((resolve) => {
    axios(url, {
      method: "DELETE",
      params: query,
      data,
    })
      .then(permissionParser)
      .then(defaultParser)
      .then((res) => resolve(res))
      .catch((e) => console.error(e));
  });
};

// callAPI
export const callApi = (url: string, options: object) => {
  return new Promise((resolve) => {
    axios(url, options)
      .then(permissionParser)
      .then(defaultParser)
      .then((res) => resolve(res))
      .catch((e) => console.error(e));
  });
};

全局Axios配置完毕

ESLint+Prettier 统一代码风格

npm install -D eslint@6.6.0 typescript @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-react eslint-config-alloy

在你的项目的根目录下创建一个 .eslintrc.js 文件

module.exports = {
  extends: ["alloy", "alloy/react", "alloy/typescript"],
  env: {
    // 你的环境变量(包含多个预定义的全局变量)
    //
    browser: true,
    node: true,
    // mocha: true,
    jest: true,
    // jquery: true
  },
  globals: {
    // 你的全局变量(设置为 false 表示它不允许被重新赋值)
    //
    // myGlobal: false
  },
  rules: {
    // 自定义你的规则
    'react/jsx-uses-react': 'error',
    'react/jsx-uses-vars': 'error',
    'no-dupe-keys': 'off',
    'no-var': 'error',
    complexity: 'off',
    indent: 'off',
    '@typescript-eslint/no-empty-interface': 'off',
    '@typescript-eslint/explicit-member-accessibility': 'off',
    'prefer-promise-reject-errors': 'off',
    'no-eq-null': 'off',
    eqeqeq: 'off',
    '@typescript-eslint/prefer-optional-chain': 'off',
    'no-async-promise-executor': 'off',
    '@typescript-eslint/no-loss-of-precision': 'off',
    'default-case-last': 'off',
    'grouped-accessor-pairs': 'off',
    'no-constructor-return': 'off',
    'no-dupe-else-if': 'off',
    'no-promise-executor-return': 'off',
    'no-setter-return': 'off',
    'no-unreachable-loop': 'off',
    'no-useless-backreference': 'off',
  },
};

下载Prettier

npm install prettier -D

在你的项目的根目录下创建一个 .prettierrc.js 文件

module.exports = {
  // 一行最多 120 字符
  printWidth: 120,
  // 使用 2 个空格缩进
  tabWidth: 2,
  // 不使用缩进符,而使用空格
  useTabs: false,
  // 行尾需要有分号
  semi: true,
  // 使用单引号
  singleQuote: true,
  // 对象的 key 仅在必要时用引号
  quoteProps: 'as-needed',
  // jsx 不使用单引号,而使用双引号
  jsxSingleQuote: false,
  // 末尾需要有逗号
  trailingComma: 'all',
  // 大括号内的首尾需要空格
  bracketSpacing: true,
  // jsx 标签的反尖括号需要换行
  jsxBracketSameLine: false,
  // 箭头函数,只有一个参数的时候,也需要括号
  arrowParens: 'always',
  // 每个文件格式化的范围是文件的全部内容
  rangeStart: 0,
  rangeEnd: Infinity,
  // 不需要写文件开头的 @prettier
  requirePragma: false,
  // 不需要自动在文件开头插入 @prettier
  insertPragma: false,
  // 使用默认的折行标准
  proseWrap: 'preserve',
  // 根据显示样式决定 html 要不要折行
  htmlWhitespaceSensitivity: 'css',
  // vue 文件中的 script 和 style 内不用缩进
  vueIndentScriptAndStyle: false,
  // 换行符使用 lf
  endOfLine: 'lf',
  // 格式化嵌入的内容
  embeddedLanguageFormatting: 'auto',
};

更改package.json

/**package.json*/
...
"scripts": {
...
+ "format": "prettier src --write",
+ "lint": "eslint --ext .tsx,.ts --fix -f table ./src"
}
...

npm run format 和 npm run lint 试试!

npm run format
npm run lint

成功的样子:

当你lint的时候估计会遇到一些格式的报错,一定不要慌,我们可以先把eslint报出来的校验规则先off掉,就像 'no-dupe-keys': 'off' 一样,在.eslintrc.js中的rules中将对应的校验规则off掉,千万不要慌,我们配置的绝对没错,坑我已经踩过了,跟着我的配置绝对可以跑通!

自动化风格校验

npm install -D husky lint-staged glob

这里为什么要下载 glob呢? 点击这里查看原因

修改package.json

/**package.json*/
...
"husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },
  "lint-staged": {
    "src/**/*.{ts,tsx}": [
      "eslint",
      "prettier --write",
      "git add"
    ]
  },
...

要想使用自动化检验,你的项目中必须要有git!! 使用风格校验:

git add . 
git commit -m "commit:..."

成功的样子:

redux

说实话,redux的类型定义比Mobx的类型定义麻烦好多

npm install redux react-redux @types/react-redux

在store文件下添加一下文件

├─ /store        	  		  <-- 根store文件夹
|	├─ actions.ts 	  	  	<-- 根action文件
|	├─ index.ts 	  	  	  <-- 根store文件
|	├─ reducer.ts 	  	  	<-- 根reducer文件
|	└─ storeInterface.ts   	<-- 根store接口文件

书写接口文件storeInterface.ts

/**storeInterface.ts*/
// 这个文件主要是定义根store的类型

// type类型

export const ADDITION = 'ADDITION';
// eslint-disable-next-line
export type ADDITION = typeof ADDITION;

export const REDUCE = 'REDUCE';
// eslint-disable-next-line
export type REDUCE = typeof REDUCE;

// action 类型

export interface AdditionAction {
  type: ADDITION;
}
export interface ReduceAction {
  type: REDUCE;
}

// action 类型归一

export type TodoAction = AdditionAction | ReduceAction;

书写根reducer文件reducer.ts

/**reducer.ts*/
import { TodoAction, ADDITION, REDUCE } from '@/store/storeInterface';

// 根store
const defaultStore = {
  count: 0,
};

// 推测根store类型
type rootData = Readonly<typeof defaultStore>;

export default (store: rootData = defaultStore, action: TodoAction): rootData => {
  switch (action.type) {
    case ADDITION: {
      let { count } = store;
      count++;
      return {
        ...store,
        count,
      };
    }
    case REDUCE: {
      let { count } = store;
      count--;
      return {
        ...store,
        count,
      };
    }

    default: {
      return { ...store };
    }
  }
};

书写根store文件index.ts

/**index.ts*/
import { createStore, combineReducers } from 'redux';
import { devToolsEnhancer } from 'redux-devtools-extension/logOnlyInProduction';
// 根store
import Reducer from '@/store/reducer';

// 将子 Reducer 合并成一个大的函数

const staticStore = combineReducers({
  init: Reducer,
});

// 创建store
const store = createStore(staticStore, devToolsEnhancer({}));

export type RootState = ReturnType<typeof staticStore>;
export default store;

// 动态注入的类型我不会定义,会的大佬请赐教!!!!

// const createReducer = (asyncReducers) => {
//   return combineReducers({
//     ...staticReducers,
//     ...asyncReducers,
//   });
// };

// const store = createStore(createReducer(), devToolsEnhancer());

// store.asyncReducers = {};

// export const injectReducer = (key, asyncReducer) => {
//   store.asyncReducers[key] = asyncReducer;
//   store.replaceReducer(createReducer(store.asyncReducers));
// };

书写action文件actions.ts

/**actions.ts*/
import { AdditionAction, ReduceAction, ADDITION, REDUCE } from '@/store/storeInterface';

export const additionAction = (): AdditionAction => ({
  type: ADDITION,
});

export const reduceAction = (): ReduceAction => ({
  type: REDUCE,
});

添加根store在index.tsx

/**index.ts*/
import React from 'react';
import ReactDOM from 'react-dom';
import * as serviceWorker from './serviceWorker';
// 组件
import App from './App';
// 引入全局样式
import '@/common/style/frame.css';

+ // 引入store
+ import { Provider } from 'react-redux';
+ import store from '@/store';

ReactDOM.render(
  <Provider store={store}>
    <React.StrictMode>
      <App />
    </React.StrictMode>
  </Provider>,
  document.getElementById('root'),
);
serviceWorker.unregister();

在Home页面引入根store

/**Home index.tsx*/
import * as React from 'react';
import { Button } from 'antd';
// 引入根store----说实话下面这些代码直接丑哭了,不知道哪个大佬能够斧正一下
import { connect, ConnectedProps } from 'react-redux';
import { RootState } from '@/store';
import { additionAction, reduceAction } from '@/store/actions';

const mapStateToProps = (state: RootState) => {
  const {
    init: { count },
  } = state;
  return {
    count,
  };
};
const mapDispatchToProps = { additionAction, reduceAction };
const connector = connect(mapStateToProps, mapDispatchToProps);
// 下面的代码主要是定义props的类型
type PropsFromRedux = ConnectedProps<typeof connector>;
type HomeProps = {} & PropsFromRedux;

export interface HomeState {}

class Home extends React.Component<HomeProps, HomeState> {
  constructor(props: HomeProps) {
    super(props);
    this.state = {};
  }
  render() {
    return (
      <div>
        <h1>Home页面</h1>
        <h2>根store中的count-- {this.props.count}</h2>
        <br />
        <Button type="primary" onClick={this.props.additionAction}>
          增加
        </Button>
        <Button type="primary" onClick={this.props.reduceAction}>
          减少
        </Button>
      </div>
    );
  }
}

export default connector(Home);

redux添加完毕。

路由鉴权和守卫

其实路由鉴权很简单,它本质就是判断,满足权限渲染Route,Link。不满足权限就不渲染。只是写法不同而已,思想就是这么个思想。

在src目录下新建router目录

/src
|  ├─ /router        	  				    <-- 根router文件夹
|  |	├─ routerHOC.tsx	  			    <-- 存放router高阶组件文件
|  |	└─ routePrivilegeConfig.ts   	<-- 权限路由配置文件

在pages目录下新建两个目录,用来新建两个页面。

├─ /pages             <-- 页面文件
|   ├─ /Home          <-- Home文件
|   |   └─ index.tsx  <-- Home页面
|   ├─ /User          <-- User文件
|   |   └─ index.tsx  <-- User页面
|   ├─ /Admin         <-- Admin文件
|   |   └─ index.tsx  <-- Admin页面
|   └─ /Login         <-- Login文件
|   |   └─ index.tsx  <-- Login页面

Admin页面代码:

/**Admin index.tsx*/
import React from 'react';

export default function Admin() {
  return (
    <div>
      <h1>Admin 页面</h1>
    </div>
  );
}

Login页面代码:

/**Login index.tsx*/
import React from 'react';

export default function Login() {
  return (
    <div>
      <h1>Login页面</h1>
    </div>
  );
}

路由配置代码:

/**router  routePrivilegeConfig.ts*/

import React from 'react';

// 组件
import Home from '@/pages/Home';
import User from '@/pages/User';
import Admin from '@/pages/Admin';
import Login from '@/pages/Login';

export interface Router {
  path: string | string[];
  component: React.ComponentType<any>;
  exact: boolean;
  role: string; // 需要的权限
  backUrl: string; // 不满足权限跳回的页面
  lintTitle: string; // link的名称
}

const routerConfig: Router[] = [
  {
    path: '/login',
    component: Login,
    exact: false,
    role: 'all',
    backUrl: '/login',
    lintTitle: 'Login',
  },
  {
    path: '/user',
    component: User,
    exact: false,
    role: 'User',
    backUrl: '/login',
    lintTitle: 'User',
  },
  {
    path: '/admin',
    component: Admin,
    exact: false,
    role: 'Admin',
    backUrl: '/login',
    lintTitle: 'Admin',
  },
  {
    path: '/home',
    component: Home,
    exact: false,
    role: 'User',
    backUrl: '/login',
    lintTitle: 'Home',
  },
];
export default routerConfig;

Link,Route的高阶组件:

/**router  routerHOC.ts*/
import React from 'react';
import { Redirect, Link, Route, RouteProps } from 'react-router-dom';
import Cookies from 'js-cookie';

// 用来判断用户现在是否登陆
// eslint-disable-next-line
const judgmentLogin = () => {
  const token = Cookies.get('键名');
  return token ? true : false;
};

type RProps = {
  backUrl: string;
  role: string;
} & RouteProps;

export function RouteHOC(props: RProps) {
  const { backUrl, role, ...otherProps } = props;
  // 如果用户的登陆失效,则跳转对应的页面
  // if (['all', 'User'].includes(role) && judgmentLogin())
  // ['User'] 这个数组里面存储的就是后端返回的个人权限,然后和路由配置中的权限比较。
  if (['User'].includes(role)) {
    return <Route {...otherProps} />;
  } else if (['all'].includes(role)) {
    return <Route {...otherProps} />;
  } else {
    return <Redirect to={backUrl} />;
  }
}
interface LProps {
  role: string;
  lintTitle: string;
  path: string | string[];
  component?: React.ComponentType<any>;
  replace?: boolean;
  innerRef?: React.Ref<HTMLAnchorElement>;
}
export function LinkHOC(props: LProps) {
  const { role, lintTitle, path } = props;
  return ['all', 'User'].includes(role) ? <Link to={path as string}>{lintTitle}</Link> : null;
}

修改App.tsx文件

/**App.tsx*/
import React from 'react';
import { BrowserRouter as Router, Switch, Redirect } from 'react-router-dom';
// 组件
import Navigate from '@/components/Navigate';

// 路由权限配置
import { RouteHOC } from '@/router/routerHOC';
import rPC from '@/router/routePrivilegeConfig';

export interface AppProps {}

export interface AppState {}

class App extends React.Component<AppProps, AppState> {
  constructor(props: AppProps) {
    super(props);
    this.state = {};
  }
  render() {
    return (
      <Router>
        <div>
          <Navigate />
          <Switch>
            {rPC.map((item, index) => (
              <RouteHOC key={index} {...item} />
            ))}
            <Redirect from="/" to="/home" />
          </Switch>
        </div>
      </Router>
    );
  }
}

export default App;

修改Navigate.tsx文件

/**Navigate.tsx  导航栏组件*/
import * as React from 'react';
// 路由权限配置
import { LinkHOC } from '@/router/routerHOC';
import rPC from '@/router/routePrivilegeConfig';
// 样式
import { NavContainer } from './style';

export interface NavigateProps {}

export interface NavigateState {}

class Navigate extends React.Component<NavigateProps, NavigateState> {
  constructor(props: NavigateProps) {
    super(props);
    this.state = {};
  }
  render() {
    return (
      <NavContainer>
        {rPC.map((item, index) => (
          <LinkHOC key={index} {...item} />
        ))}
      </NavContainer>
    );
  }
}

export default Navigate;

下载 lodash

npm install @types/lodash lodash

react-loadable 下载

npm install @types/react-loadable react-loadable

在components目录下创建Loading目录,书写全局的loading状态

├─ /components      <-- 公共模块组件目录
|	├─ /Loading   	  <-- 全局公用loading
|  	|	└─ index.tsx  <-- loading组件
|	└─ /Navigate   	  <-- 全局公用导航栏
|   |	├─ style.ts   <-- 导航样式
|  	|	└─ index.tsx  <-- 导航栏组件

书写全局的loading组件:

/**/Loading index.tsx */
import React from "react";
import styled, { keyframes } from "styled-components";

const StyledLoader = styled.div`
  display: block;
  background-color: rgba(255, 255, 255, 0.2);
  width: 100%;
  height: 100%;
  position: fixed;
  top: 0;
  bottom: 0;
  left: 0;
  z-index: 10000;
  display: flex;
  justify-content: center;
  align-items: center;
  opacity: 1;
  text-align: center;
`;

const Wrapper = styled.div`
  width: 200px;
  height: 100px;
  display: inline-flex;
  flex-direction: column;
  justify-content: space-around;
`;

const spinner = keyframes`
  0% {
    transform: rotate(0deg);
  }

  100% {
    transform: rotate(360deg);
  }
`;

const Inner = styled.div`
  width: 48px;
  height: 48px;
  margin: 0 auto;
  text-indent: -12345px;
  border-top: 1px solid rgba(0, 0, 0, 0.08);
  border-right: 1px solid rgba(0, 0, 0, 0.08);
  border-bottom: 1px solid rgba(0, 0, 0, 0.08);
  border-left: 1px solid rgba(0, 0, 0, 0.7);
  border-radius: 50%;
  z-index: 100001;
  animation: ${spinner} 600ms infinite linear;
`;

const Text = styled.div`
  width: 200px;
  height: 20px;
  text-align: center;
  font-size: 12px;
  letter-spacing: 4px;
  color: #000;
  margin-top: 10px;
`;

export default function Loading() {
  return (
    <StyledLoader>
      <Wrapper>
        <Inner />
        <Text>LOADING</Text>
      </Wrapper>
    </StyledLoader>
  );
}

接下来就是在每个组件的目录下创建Loadable.ts文件:例如在Admin页面

├─ /pages             	<-- 页面文件
|   ├─ /Home          	<-- Home文件
|  	|	├─ Loadable.ts	<-- react-loadable文件
|   |   └─ index.tsx  	<-- Home页面
|   ├─ /User          	<-- User文件
|  	|	├─ Loadable.ts	<-- react-loadable文件
|   |   └─ index.tsx  	<-- User页面
|   ├─ /Admin         	<-- Admin文件
|  	|	├─ Loadable.ts	<-- react-loadable文件
|   |   └─ index.tsx  	<-- Admin页面
|   └─ /Login         	<-- Login文件
|  	|	├─ Loadable.ts	<-- react-loadable文件
|   |   └─ index.tsx  	<-- Login页面

Loadable.ts 每个文件都一样,对组件使用react-loadable:

/**Loadable.ts  react-loadable文件*/
import Loadable from "react-loadable";
import Loading from "@/components/Loading";

export default Loadable({
  loader: () => import("./index"),
  loading: Loading,
});

修改组件的引入方式:修改routePrivilegeConfig.ts

...
// 组件
import Home from '@/pages/Home/Loadable';
import User from '@/pages/User/Loadable';
import Admin from '@/pages/Admin/Loadable';
import Login from '@/pages/Login/Loadable';
...

ok,npm start试试吧!!!

如果您看完之后收获了很多,请不要吝啬您的点赞!您的点赞是我前进最大的动力!!如果您觉得文章中有不正确的地方请指正!