从0开始微操搞定antd+ts+react-app-rewired+react-router+mobx

3,011 阅读15分钟

由于工作需要,最近搞了一系列后台数据报表类的可视化项目,在技术选型上使用了antd+ts+ehcarts+mobx,脚手架使用了create-react-app,在整合过程中发现有些关于这方面的文档交代的不是很清楚。在闲下来时,特整理了一些关键步骤,对自己来说也是一个复盘过程,同时也希望此文给有些懵逼的兄弟姐妹们作为一个参考,遇事不要慌,干就完了。

项目配置文件package.json先放一波:

{
  "name": "my-app",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@testing-library/jest-dom": "^4.2.4",
    "@testing-library/react": "^9.3.2",
    "@testing-library/user-event": "^7.1.2",
    "@types/jest": "^24.0.0",
    "@types/node": "^12.0.0",
    "@types/react": "^16.9.0",
    "@types/react-dom": "^16.9.0",
    "antd": "^4.6.4",
    "lodash": "^4.17.20",
    "mobx": "^5.15.6",
    "mobx-react": "^6.3.0",
    "react": "^16.13.1",
    "react-document-title": "^2.0.3",
    "react-dom": "^16.13.1",
    "react-router-dom": "^5.2.0",
    "react-scripts": "3.4.3",
    "typescript": "~3.7.2"
  },
  "scripts": {
    "start": "react-app-rewired start --REACT_APP_ENV=local",
    "build:prod": "react-app-rewired build --REACT_APP_ENV=prod",
    "build:test": "react-app-rewired build --REACT_APP_ENV=test",
    "build:dev": "react-app-rewired build --REACT_APP_ENV=dev",
    "test": "react-app-rewired test --env=jsdom"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },
  "devDependencies": {
    "@types/lodash": "^4.14.161",
    "@types/react-document-title": "^2.0.4",
    "@types/react-router-dom": "^5.1.5",
    "babel-plugin-import": "^1.13.0",
    "customize-cra": "^1.0.0",
    "less": "^3.12.2",
    "less-loader": "^7.0.1",
    "progress-bar-webpack-plugin": "^2.1.0",
    "react-app-rewired": "^2.1.6"
  }
}

本人比较喜欢用yarn,所以下面所有操作都以yarn命令执行的,要用npm命令也随你。

整合步骤

一、第一阶段:搞定项目初始化,集成ts

  • 项目初始化

找一个你想把项目放置的目录,然后执行如下命令:

//如果没有安装过create-react-app脚手架,请先安装
//由于要使用ts,所以用以下命令
yarn create react-app  my-app --template typescript

以上命令执行后,生成名称为my-app的项目,项目根目录下会自动生成ts项目的配置文件tsconfig.json: 额外说一句,如果在ts项目中引入第三方依赖的时候报错,如下这种: 这是因为没有ts的类型申明文件导致,可以到对应库的官网看看是否有提供,一般就是图中提示的那种,如果官网有提供安装一下即可,如果官网没有提供,那么在项目下搞一个xxx.d.ts文件,申明一下即可解决。相关资料可以自行Google一下。

  • 添加antd
yarn add antd

以上两个步骤搞完,第一阶段就完毕了,so easy,妈妈再也不用担心我写的bug了。。。。

以上也可以antd参考文档:ant.design/docs/react/…

二、第二阶段:搞定react-app-rewire配置文件,集成antd

如果你初次搞这套整合,如果你又去看了看第一阶段中最后放出的文档,可能会发现前面还好,但是后面越看越迷糊,整到最后就懵逼了,最后好像怎么整也不行。。。反正我是有这种感觉。不过可以不用管,按照以下的步骤继续走下去,保证可以走通。

  • 整合react-app-rewired配置

至于连为啥要整合react-app-rewire都闹不明白的兄弟,建议先Google一下这玩意是干啥的,非常简单,在此不再多说。

整合步骤如下:

1)package.json配置文件中,script配置替换如下:

//自动生成的原来的配置
"scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  }
//替换后的新配置
"scripts": {
        "start": "react-app-rewired start --REACT_APP_ENV=local", //本地开发环境(这个注释实际使用要删掉)
        "build:prod": "react-app-rewired build --REACT_APP_ENV=prod",//打包生产环境(这个注释实际使用要删掉)
        "build:test": "react-app-rewired build --REACT_APP_ENV=test",//打包测试环境(这个注释实际使用要删掉)
        "build:dev": "react-app-rewired build --REACT_APP_ENV=dev",//打包开发环境(这个注释实际使用要删掉)
        "test": "react-app-rewired test --env=jsdom"
    }
 特别说明:一般实际商业项目,都会有对应的不同部署环境,在此项目中使用REACT_APP_ENV环境变量来区分不同的编译环境。
 此处先设置好,后面在config-overrides文件中有处理和使用

2)生成react-app-rewire的配置(其实就是用来修改webpack配置的配置)

首先安装react-app-rewired依赖:yarn add react-app-rewired -D

在my-app根目录下新建文件:config-overrides.js,废话不多说,直接放出完整文件详细配置内容如下:

const {
    override,
    fixBabelImports,
    addLessLoader,
    useEslintRc,
    disableEsLint,
    addDecoratorsLegacy,
    overrideDevServer,
    addWebpackAlias,
} = require('customize-cra');

//1、自定义环境变量REACT_APP_ENV配置
for (let i = 0; i < process.argv.length; i++) {
    if (process.argv[i].indexOf('--') === 0) {
        let item = process.argv[i]
            .substring('--'.length, process.argv[i].length)
            .split('=');
        process.env[item[0]] = item[1];
    }
}
//2、关闭map打包
process.env.GENERATE_SOURCEMAP =
    process.env.REACT_APP_ENV !== 'local' ? 'false' : 'true';

//3、progress 进度条插件
const chalk = require('chalk');
const progressBarPlugin = require('progress-bar-webpack-plugin')({
    width: 60,
    format:
        `${chalk.green('build')} [ ${chalk.cyan(':bar')} ]` +
        ` ${chalk.cyan(':msg')} ${chalk.red('(:percent)')}`,
    clear: true,
});

//4、修改build文件夹路劲需要
const path = require('path');
const paths = require('react-scripts/config/paths');
const staticFile = 'my-app'; //打包出来的资源文件夹(如果不做设置则默认是build文件夹)
paths.appBuild = path.join(path.dirname(paths.appBuild), staticFile);

// 5、本地开发时代理服务器解决跨域问题(仅仅本地开发有效)
const devServerConfig = () => (config) => {
    return {
        ...config,
        // 服务开启gzip
        compress: true,
        proxy: {
            '/': {
                target: 'http://192.168.100.118:8808/api',
                changeOrigin: true,
                pathRewrite: {
                    '^/': '/',
                },
            },
        },
    };
};

module.exports = {
    webpack: override(
        // 6、集成antd的按需加载,新版(好像是4.0以后)的antd也可以不用设置了
        fixBabelImports('import', {
            libraryName: 'antd',
            style: true,
        }),
        //7、样式模块化和antd主题修改等配置
        addLessLoader({
            //localIdentName: '[name]__[local]--[hash:base64:5]',
            lessOptions: {
                javascriptEnabled: true,
                cssModules: {
                    //样式模块化配置(但是这儿似乎不是必须,因为当前的脚手架默认支持)
                    localIdentName: '[path][name]__[local]--[hash:base64:5]',
                },
                //下面这行很特殊,这里是更改主题的关键,这里我只更改了主色,当然还可以更改其他的
                modifyVars: {
                    '@primary-color': '#317CC8',
                    '@modal-confirm-body-padding': '8px',
                    '@table-padding-horizontal': '10px',
                    '@table-padding-vertical': '8px',
                    '@layout-header-background': '#317CC8',
                    '@layout-body-background': '#fafafa',
                    // '@table-header-color': '#F4F4F4',
                },
            },
        }),
        disableEsLint(), //忽略eslint警告
        // 8、设置路径别名,这样就不需要在项目中引用公用组件或一些公用模块的时候写很长很长的路径了,可根据实际需要配置
        addWebpackAlias({
            ['@']: path.join(__dirname, '/src'),
            '@util': path.join(__dirname, '/src/utils'),
            '@static': path.join(__dirname, '/src/static'),
            '@store': path.join(__dirname, '/src/store'),
            '@common': path.join(__dirname, '/src/common'),
        }),
        // addDecoratorsLegacy(),//添加装饰器支持
        // 9、webpack的一些配置项修改,此处修改打包输入目录和编译进度条配置
        (config) => {
            // 本地开发无需要设置打包
            if (process.env.REACT_APP_ENV !== 'local') {
                config.output.path = path.join(
                    path.dirname(config.output.path || '/'),
                    staticFile
                );
            }
            config.plugins.push(progressBarPlugin);
            return config;
        }
    ),
    // 10、本地开发时代理服务器配置
    devServer: overrideDevServer(devServerConfig()),
};

以上配置文件为项目开发的完整配置,可直接放心食用。按照代码中注释的顺序阅读即可,关键点都做了说明,很好懂的。

只说一点,走到这步后,你就可以启动项目了,执行如下命令:

yarn start

但是启动过程中可能会报一些npm包没有安装之类的,依次缺啥装啥,直至可以正常start启动就好了,不再赘述。(不会有人不会装npm依赖包把)

//在此步中安装的一般如下的这条命令,-D参数是因为此步中的依赖一般装在开发依赖devDependencies里面就可以了
yarn add 缺少的依赖 -D

如下图: 看到这个,我知道customize-cra依赖没有安装,所以需要执行:

yarn add customize-cra -D

而后再启动,依次类推,其他同理。

如果你启动之后,一切正常,但是引入antd的组件后编译报错找不到less,就是下面这样的: 那么可以装一下less:

yarn add less -D

3)验证antd是否集成成功

按照上面的步骤都搞完了后,正常来说,启动项目应该没有问题了,执行如下命令启动项目:

yarn start

启动成功: 浏览器上打开:http://localhost:3000/,可以看到如下界面:

走到这步,那么我们项目所有的配置ok了,此时如果不放心antd是否集成成功了,可以验证一下,我的验证方式是直接在APP.tsx入口文件引入一个antd的组件(比如:Button),看看是否能够正常显示,如果可以,则证明集成成功了,如下图: 此时在浏览器中看到如下页面: 到这儿,证明antd框架的集成已经OK了。。。。

三、第三阶段:搞定react-router配置

对于一个react技术栈的spa应用,基本上都会用到react-router的路由管理方案。关于react-router的使用可以大概瞄一下官方文档:reactrouter.com/web/example… 但是说实话,如果这块搞得不多的话,想要真正在真实项目中集成使用,还是需要结合各路网文来对照看。特别是react-router5.0以后版本跟之前的使用方式可能有些不同,具体可以自己看看文档,在此不再赘述。

这块的实现套路可能各家有各家的不同,总之能够满足自己的项目需求就是ok的,所以我认为这块没有啥最好的实现方式之说。其实在真实场景中,我们遇到的路由场景大概可以总结为这么几个核心问题:安装哪个版本的依赖?(版本选择问题),项目中路由文件中怎么写?(路由配置问题),用户权限控制怎么搞?(鉴权问题)。以下就这个问题说说我的微操。

  • 安装react-router-dom

react-router-dom和react-router的区别,可以简单的看看这个文章:juejin.cn/post/684490…

一般情况下,安装react-router-dom就好:

yarn add react-router-dom
yarn add @types/react-router-dom -D   //ts项目需要

验证安装一下,先搞两个页面:Login.tsx和Home.tsx 代码如下:

//Login.tsx
import { Button, Checkbox, Form, Input } from 'antd';
import React, { ReactElement } from 'react';
import { RouteComponentProps } from 'react-router-dom';

interface Props extends RouteComponentProps {}

const layout = {
    labelCol: { span: 8 },
    wrapperCol: { span: 16 },
};
const tailLayout = {
    wrapperCol: { offset: 8, span: 16 },
};

//登录页面
export default function Login({ history }: Props): ReactElement {
    const onFinish = (values: any) => {
        history.push('/');
    };

    const onFinishFailed = (errorInfo: any) => {
        console.log('Failed:', errorInfo);
    };
    return (
        <div style={{ width: 300, margin: '100px auto' }}>
            <Form
                {...layout}
                name="basic"
                initialValues={{ remember: true }}
                onFinish={onFinish}
                onFinishFailed={onFinishFailed}
            >
                <Form.Item
                    label="用户名"
                    name="username"
                    rules={[
                        {
                            required: true,
                            message: 'Please input your username!',
                        },
                    ]}
                >
                    <Input />
                </Form.Item>

                <Form.Item
                    label="密码"
                    name="password"
                    rules={[
                        {
                            required: true,
                            message: 'Please input your password!',
                        },
                    ]}
                >
                    <Input.Password />
                </Form.Item>

                <Form.Item
                    {...tailLayout}
                    name="remember"
                    valuePropName="checked"
                >
                    <Checkbox>记住登录</Checkbox>
                </Form.Item>

                <Form.Item {...tailLayout}>
                    <Button type="primary" htmlType="submit">
                        登录
                    </Button>
                </Form.Item>
            </Form>
        </div>
    );
}
//Home.tsx
import { Button } from 'antd';
import React, { ReactElement } from 'react';
import { RouteComponentProps } from 'react-router-dom';

interface Props extends RouteComponentProps {}

//登录系统进入的首页
export default function Home({ history }: Props): ReactElement {
    return (
        <>
            <div style={{ textAlign: 'right' }}>
                <Button
                    onClick={() => {
                        history.push('/login');
                    }}
                >
                    退出
                </Button>
            </div>
            <div style={{ textAlign: 'center', paddingTop: 30, fontSize: 24 }}>
                欢迎使用系统
            </div>
        </>
    );
}

改造一下App.tsx文件:

import React from 'react';
import { HashRouter, Route, Switch } from 'react-router-dom';
import './App.css';
import Home from './page/home';
import Login from './page/login';

function App() {
    return (
        <HashRouter>
            <Switch>
                <Route exact={true} path={'/'} component={Home} />
                <Route path={'/login'} component={Login} />
            </Switch>
        </HashRouter>
    );
}

export default App;

此时启动项目,不出意外的话可以看到home页面: 点击右上角的退出按钮,退出到登录页面: 经过以上步骤,证明react路由集成到项目中ok了,但是真实的项目中开发,还需考虑其他东西(比如:登录后才能跳到home页面,404页面,可维护性等),那接下来就搞一下真实开发的路由配置。

  • 正式开发时的路由配置

上面的验证中,我们发现我们的页面配置是写在App.tsx文件中的,每次增减页面,都要去修改这个文件,这其实不太利于维护。在正式开发的时候,一种比较好的方式就是根据把路由的管理抽离出一个配置文件出来,App.tsx文件作为路由的配置文件写好之后就不用频繁去变动它了,在此我经过以下几个微操来实现它:

1)新建一个router文件夹,在其中创建一个index.ts文件,以后所有增删页面的配置在此文件操作:

import Home from '../page/home';
import Login from '../page/login';

export interface IRouterProps {
    name?: string;//名称(实际没用)
    path: string;//路径
    component: any;//对应的组件
    needLogin?: boolean;//页面是否需要登录才能看
}

//写一个页面就在此数组里面加一个
const routers: IRouterProps[] = [
    { name: '首页', path: '/', component: Home, needLogin: true },
    { name: '登录', path: '/login', component: Login, needLogin: false },
];

export default routers;

2)生成进入系统后的页面公用布局页面

一般的系统页面布局都是固定的,类似下面这种: 对于这种,实际开发中可以抽出一个一个公用的层来做这件事,我们创建一个在src下创建layout目录,在其中搞一个BasicLayout.tsx文件: 代码如下:

import { Button, Layout, Menu } from 'antd';
import React, { Component } from 'react';
import DocumentTitle from 'react-document-title';
import { RouteComponentProps } from 'react-router-dom';
import styles from './BasicStyle.module.less';

interface IBasicLayoutProps extends RouteComponentProps {
    title?: string;
    loading?: boolean;
}

interface IBasicLayoutState {}

const { Header, Content, Footer } = Layout;
const { SubMenu } = Menu;

/**
 * 基础布局组件
 */
export default class BasicLayout extends Component<
    IBasicLayoutProps,
    IBasicLayoutState
> {
    constructor(props: IBasicLayoutProps) {
        super(props);
    }

    componentDidMount() {}

    render() {
        let layout = (
            <DocumentTitle title={'系统'}>
                <Layout>
                    <Header className={styles.header}>
                        <div>
                            菜单(实际情况可能由后端返回,这样做到用户路由的权限控制)
                        </div>
                        <Button
                            onClick={() => {
                                this.props.history.push('/login');
                            }}
                        >
                            退出
                        </Button>
                    </Header>

                    <Content className={styles.main} id="mainContainer">
                        {this.props.children}
                    </Content>
                </Layout>
            </DocumentTitle>
        );
        return layout;
    }
}

3)改造App.tsx配置文件:

import React, { FC } from 'react';
import { HashRouter, Redirect, Route, Switch } from 'react-router-dom';
import './App.less'; // 必须要放在最后位置
import BasicLayout from './layout/BasicLayout';
import NoMatchPage from './page/404';
import routers, { IRouterProps } from './router';

const App: FC = () => {
    //这个变量用来表示是否登录过,实际项目中根据登录过后缓存的用户或token是否存在来判断
    const isLogin = false;
    console.log('是否登录==>' + isLogin);
    return (
        <HashRouter>
            <Switch>
                {routers.map((item: IRouterProps, key: number) => {
                    return (
                        <Route
                            key={key}
                            exact
                            path={item.path}
                            render={(props: any) => {
                                if (!item.needLogin || isLogin) {
                                    if (item.needLogin) {
                                        //需要登录的页面使用BasicLayout布局
                                        return (
                                            <BasicLayout {...props}>
                                                <item.component {...props} />
                                            </BasicLayout>
                                        );
                                    } else {
                                        //这是不需要登录的页面的布局
                                        return <item.component {...props} />;
                                    }
                                } else {
                                    return <Redirect exact to={'/login'} />;
                                }
                            }}
                        />
                    );
                })}
                {/* <Route exact={true} path={'/'} component={Home}/>
                <Route path={'/login'} component={Login}/>*/}
                {/*找不到页面的时候404页面*/}
                <Route component={NoMatchPage} />
            </Switch>
        </HashRouter>
    );
};

export default App;

经过以上改造,基本可以实现一个实际项目中路由配置的改造了。现在启动项目,浏览器访问http://localhost:3000/#/,会自动跳转到登录页面了,手动修改App.tsx中的isLogin变量,为true,在访问,则可以跳到home页面了。所以,只要我们用代码控制,比如:登录成功后修改isLogin为true,否则为false,就能简单实现是否登录来控制登录路由了。

在实际开发中,如果要根据登录用户的角色来控制不同的用户看到不同的菜单,那么请看BasicLayout.tsx中菜单区域,只需控制这个区域的数据是登录之后从后端返回过来的菜单数据渲染生成,然后不同的菜单再对应一个前端router.ts配置里面的一个前端页面即可:

import Home from '../page/home';
import Login from '../page/login';

export interface IRouterProps {
    name?: string;//名称(实际没用)
    path: string;//路径
    component: any;//对应的组件
    needLogin?: boolean;//页面是否需要登录才能看
}

//写一个页面就在此数组里面加一个
const routers: IRouterProps[] = [
    { name: '首页', path: '/', component: Home, needLogin: true },
    { name: '登录', path: '/login', component: Login, needLogin: false },
    //演示加一个页面,此页面的path跟后端的返回的菜单对应
    {name:'新页面',path:'/xxxx',component: xxxx, needLogin: true }
];

export default routers;

这块的实现具体可以灵活处理,在此不再赘述了。

四、第四阶段:搞定mobx状态管理

搞到第三阶段后,其实基本上一个可以开发一个真实项目了,这步其实是非必须的集成,因为有dva,hooks,页面state等方案可以选择,具体可以根据实际需要来搞。因为项目中有些状态需要全局管理,所以在此本人集成了mobx解决方案,以下简单阐述:

  • 安装mobx
yarn add mobx mobx-react
  • 创建store入口配置文件

在项目根目录下新建store目录,在store中创建index.tsx文件,作为store配置的入口文件: 代码如下:

import { useLocalStore } from 'mobx-react';
import 'mobx-react-lite/batchingForReactDom';
import React, { createContext, FC, useContext } from 'react';
import AppStore from './AppStore';
import UserStore from './UserStore';

//生成store的统一方法
const createStores = () => {
    //新增加一个store,在此创建一个实例即可
    return {
        appStore: new AppStore(),
        userStore: new UserStore(),
    };
};

//类组件使用的store
const stores = createStores(); 
 // Context的封装,创建 Provider,通过 React.Context来注入,包裹函数组件,将hookStores注入到函数组件中
const StoresContext = createContext(stores);
// hooks 使用笔记看这里 -> https://github.com/olivewind/blog/issues/1
const useStores = () => useContext(StoresContext); //hooks组件使用的store

type TTodoStore = ReturnType<typeof createStores>;
const TodoStoreContext = createContext<TTodoStore | null>(null);

// 创建 Provider,通过 React.Context 来注入
const TodoStoreProvider: FC = ({ children }) => {
    const hookStores = useLocalStore(createStores); //函数组件中hooks使用的store
    return (
        <TodoStoreContext.Provider value={hookStores}>
            {children}
        </TodoStoreContext.Provider>
    );
};

export { stores, TodoStoreProvider, useStores };

以上代码可以同时配置达到在react的function组件和class组件中使用的效果,可以放心食用。

  • 改造项目index.tsx入口文件 主要配置store集成:
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'mobx-react';
import { stores, TodoStoreProvider } from './store';
import './index.css';
import App from './App';
import zhCN from 'antd/es/locale/zh_CN'; // 引入中文包
import * as serviceWorker from './serviceWorker';
import { ConfigProvider, message } from 'antd';
import 'moment/locale/zh-cn';

//message全局配置
message.config({
    duration: 2,
    maxCount: 1,
});

const appStart = () => {
    return ReactDOM.render(
        <ConfigProvider locale={zhCN}>
        	{/*集成store在function和class组件中可用的方式*/}
            <Provider {...stores}>
                <TodoStoreProvider>
                    <App />
                </TodoStoreProvider>
            </Provider>
        </ConfigProvider>,
        document.getElementById('root')
    );
};

appStart();

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
  • 使用store

最后,来搞一波store的使用,看看是否集成成功和怎么去用他,我用登录退出的这个功能来做实验。 在store目录下生成UserStore.ts:

import { action, observable } from 'mobx';
export default class UserStore {
    @observable public loading: boolean = false;
    @observable public loginSuccess: boolean = false;

    //登陆
    @action
    public loginAction() {
        this.loading = true;
        setTimeout(() => {
            this.loginSuccess = true;
            this.loading = false;
            sessionStorage.setItem('token', 'logingedddd');
        }, 1000);
    }

    //退出
    @action
    public loginOutAction() {
        this.loginSuccess = false;
        sessionStorage.setItem('token', '');
    }
}

改造Login.tsx:

import { Button, Checkbox, Form, Input } from 'antd';
import { observer } from 'mobx-react';
import React, { ReactElement } from 'react';
import { Redirect, RouteComponentProps } from 'react-router-dom';
import { useStores } from '../../store';

interface Props extends RouteComponentProps {}

const layout = {
    labelCol: { span: 8 },
    wrapperCol: { span: 16 },
};
const tailLayout = {
    wrapperCol: { offset: 8, span: 16 },
};

//登录页面
function Login({ history }: Props): ReactElement {
    const { userStore } = useStores();
    const { loading, loginSuccess } = userStore;

    const onFinish = (values: any) => {
        userStore.loginAction();
    };

    const onFinishFailed = (errorInfo: any) => {
        console.log('Failed:', errorInfo);
    };

    //登录了之后要跳转到home页面
    const token: string | null = sessionStorage.getItem('token');
    if (loginSuccess || (token && token.length > 0)) {
        return <Redirect to={'/'} />;
    }
    return (
        <div style={{ width: 300, margin: '100px auto' }}>
            <Form
                {...layout}
                name="basic"
                initialValues={{ remember: true }}
                onFinish={onFinish}
                onFinishFailed={onFinishFailed}
            >
                <Form.Item
                    label="用户名"
                    name="username"
                    rules={[
                        {
                            required: true,
                            message: 'Please input your username!',
                        },
                    ]}
                >
                    <Input />
                </Form.Item>

                <Form.Item
                    label="密码"
                    name="password"
                    rules={[
                        {
                            required: true,
                            message: 'Please input your password!',
                        },
                    ]}
                >
                    <Input.Password />
                </Form.Item>

                <Form.Item
                    {...tailLayout}
                    name="remember"
                    valuePropName="checked"
                >
                    <Checkbox>记住登录</Checkbox>
                </Form.Item>

                <Form.Item {...tailLayout}>
                    <Button type="primary" htmlType="submit" loading={loading}>
                        登录
                    </Button>
                </Form.Item>
            </Form>
        </div>
    );
}

export default observer(Login);

改造App.tsx:

import { observer } from 'mobx-react';
import React, { FC } from 'react';
import { HashRouter, Redirect, Route, Switch } from 'react-router-dom';
import './App.less'; // 必须要放在最后位置
import BasicLayout from './layout/BasicLayout';
import NoMatchPage from './page/404';
import routers, { IRouterProps } from './router';
import { useStores } from './store';

const App: FC = () => {
    const { userStore } = useStores();
    const { loginSuccess } = userStore;
    //这个变量用来表示是否登录过,实际项目中根据登录过后缓存的用户或token是否存在来判断
    const isLogin = loginSuccess || sessionStorage.getItem('token');
    console.log('是否登录==>' + isLogin);

    return (
        <HashRouter>
            <Switch>
                {routers.map((item: IRouterProps, key: number) => {
                    return (
                        <Route
                            key={key}
                            exact
                            path={item.path}
                            render={(props: any) => {
                                if (!item.needLogin || isLogin) {
                                    if (item.needLogin) {
                                        //需要登录的页面布局
                                        return (
                                            <BasicLayout {...props}>
                                                <item.component {...props} />
                                            </BasicLayout>
                                        );
                                    } else {
                                        //这是不需要登录的页面的布局
                                        return <item.component {...props} />;
                                    }
                                } else {
                                    return <Redirect exact to={'/login'} />;
                                }
                            }}
                        />
                    );
                })}
                {/* <Route exact={true} path={'/'} component={Home}/>
                <Route path={'/login'} component={Login}/>*/}
                <Route component={NoMatchPage} />
            </Switch>
        </HashRouter>
    );
};

export default observer(App);

改造BasicLayout.tsx:

import { Button, Layout, Menu } from 'antd';
import { inject, observer } from 'mobx-react';
import React, { Component } from 'react';
import DocumentTitle from 'react-document-title';
import { RouteComponentProps } from 'react-router-dom';
import AppStore from '../store/AppStore';
import UserStore from '../store/UserStore';
import styles from './BasicStyle.module.less';

interface IBasicLayoutProps extends RouteComponentProps {
    title?: string;
    loading?: boolean;
    appStore?: AppStore;
    userStore?: UserStore;
}

interface IBasicLayoutState {}

const { Header, Content, Footer } = Layout;
const { SubMenu } = Menu;

/**
 * 基础布局组件
 */

@inject('appStore', 'userStore')
@observer
export default class BasicLayout extends Component<
    IBasicLayoutProps,
    IBasicLayoutState
> {
    constructor(props: IBasicLayoutProps) {
        super(props);
    }

    componentDidMount() {}

    render() {
        const { userStore } = this.props;
        let layout = (
            <DocumentTitle title={'系统'}>
                <Layout>
                    <Header className={styles.header}>
                        <div>菜单</div>
                        <Button
                            onClick={() => {
                                //this.props.history.push('/login');
                                userStore?.loginOutAction();
                            }}
                        >
                            退出
                        </Button>
                    </Header>

                    <Content className={styles.main} id="mainContainer">
                        {this.props.children}
                    </Content>
                </Layout>
            </DocumentTitle>
        );
        return layout;
    }
}

好了,经过以上几个改造,再起重新启动项目,你可以实现登录和退出功能,mobx集成成功。

以上改造的几个文件有class组件,也有function组件,不同的组件中使用mobx的使用方法具体请阅读代码。

五、第五阶段:总结

这套操作基本涵盖了从0开始,搭建一个目前较流行的react开发技术架构,可以作为新手学习或实际项目框架的搭建的一个参考,大神请绕道而行。最后,如果你要是觉得我何必那么蛋疼的要从0开始搞呢?有没有拿下来就直接开搞的框架呢?那也是有的,比如umi这种,直接拉下来跟着写就好了,不过有时候成熟的解决方案用多了,反而会把一些基本的东西搞丢了,从这个角度来说,有时从0开始会给我们一种更加感性的理解,这或许也是一种成长吧。好了,终于搞完了,项目源码地址:github.com/phonet/antd…

发现写文档好累😫😫😫😫😫😫😫😫