手把手带你配置React后台管理项目(typescript)

215 阅读6分钟

说明: 笔者node:v16.15.1, yarn:1.22.18

一、配置安装项

1. 项目初始化

npx create-react-app yunyan-web --template typescript

初始化后的目录结构如下:

image.png

node_modules是安装node后用来存放用包管理工具下载安装的包的文件夹。比如webpack、gulp、grunt这些工具,我们可以在 node_modules 中看到所有依赖的包。

2. 调整目录结构如下

image.png

调整./pages/App.tsx的引用路径:

import React from 'react';
import logo from '../logo.svg'; // 更改此处引入路径
import './App.css';

function App() {
  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 App;

./index.tsx中更改app的引入路径:

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './pages/App'; // 更改此处引入路径
import reportWebVitals from './reportWebVitals';

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

更改完成后,运行yarn start验证文件结构调整后,项目能否成功运行。

React 是一个用于构建用户界面的 JavaScript 库,他只负责UI部分,而需要构建一个完整的react项目,还需要安装router & redux。

3. 安装router & redux

React Router 是完整的 React 路由解决方案。

React Router 保持 UI 与 URL 同步。它拥有简单的 API 与强大的功能例如代码缓冲加载、动态路由匹配、以及建立正确的位置过渡处理。

router官网: reactrouter.com/

Redux 是 JavaScript 状态容器,提供可预测化的状态管理。但是redux要和react结合使用,还需要安装中间件,还有一些比较复杂的规范用法,于是官方推荐了Redux Toolkit。

Redux Toolkit 是官方推荐的编写 Redux 逻辑的方法。它包含了 Redux 核心,并包含我们认为对于构建 Redux 应用必不可少的软件包和功能。Redux Toolkit简化了大多数 Redux 任务,防止了常见错误,使编写 Redux 应用程序更加容易。

安装react-router-dom@6,Redux Toolkit:

 yarn add react-router-dom@6
 yarn add @reduxjs/toolkit react-redux

接下来,我们配置下Redux Toolkit的typescript的用法,typescript和JS的用法不一致。

新建./store/features/dictSlice.ts配置如下:

import { createSlice } from '@reduxjs/toolkit';

const initialState = {
  appName: '',
};

export const dictSlice = createSlice({
  name: 'user',
  initialState,
  reducers: {
    updateAppName: (state, action) => {
      state.appName = action.payload;      
    },
  },
});

// Action creators are generated for each case reducer function
export const { updateAppName } = dictSlice.actions;

export default dictSlice.reducer;

新建./store/index.ts配置如下:

import { configureStore } from '@reduxjs/toolkit';
import dictReducer from './features/dictSlice';

export const store = configureStore({
  reducer: {
    dictInfo: dictReducer,
  },
});

// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>;
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch;

新建./store/hooks.ts配置如下:

import { useDispatch, useSelector } from 'react-redux';
import type { TypedUseSelectorHook } from 'react-redux';
import type { RootState, AppDispatch } from './index';

// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

以上文件编写好了,不用更改,这是通用写法。

./index.tsx改写如下:

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './pages/App'; // 更改此处引入路径
import reportWebVitals from './reportWebVitals';
import { store } from './store'
import { Provider } from 'react-redux'

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <Provider store={store}>
    <React.StrictMode>
      <App />
    </React.StrictMode>
  </Provider>,
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

以上配置完成后,我们来运行一下服务yarn start验证配置是否成功。

验证成功后,我们在文件中使用redux管理的状态,./pages/App.tsx:

import React from 'react';
import { useAppDispatch } from '../store/hooks';
import { useSelector } from 'react-redux';
import { updateAppName } from '../store/features/dictSlice' 
import './App.css';

function App() {
  const { appName } = useSelector((state: any) => state.dictInfo); // 获取store的值

  const dispatch = useAppDispatch();

  dispatch(updateAppName('新的标签名')) // 更新store

  return (
    <div className="App">
      <header className="App-header">
        {appName}
        <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 App;

自此我们的store配置成功。

前面引入store中的action时,引入的路径比较长,

import { updateAppName } from '../store/features/dictSlice'

如果文件层级更深,引入的路径会越来越长,我们可以配置路径别名,减少引入路径。

我们需要对 create-react-app 的默认配置进行自定义,create-react-app官方脚手架默认将webpack配置隐藏起来了,你也可以使用 create-react-app 提供的 yarn run eject 命令将所有内建的配置暴露出来,只是这个操作不可逆。

在这里我们使用 craco。 对create-react-app 的默认配置进行自定义,安装craco

yarn add @craco/craco --save

根目录下创建craco.config.js,代码如下:

const path = require('path');

module.exports = {
  webpack: {
    // webpack配置
    alias: {
      // 设置别名方便文件引用
      '@': path.resolve(__dirname, './src'), // 路径别名
    },
  }  
};

修改 package.json 里的 scripts 属性将package.json的启动改为craco启动: 修改 package.json 里的 scripts 属性将package.json的启动改为craco启动:

"start": "craco start",
"build": "craco build",
"test": "craco test",

如果是JS版的,以上的配置就可以了,TS版的,还需要配置tsconfig.json

{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "baseUrl": "./",
    "paths": {
      "@/*": ["src/*"], // 别名
    },
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx"
  },
  "include": ["src"]
}

配置完成后,启动一下服务,验证配置是否正确。

我们注意对比一下启动信息,这是改成craco之前的

image.png

这是设置了之后的

image.png

设置成功后,可以看到,我们的现在的应用程序是运行craco start命令开始的。

设置成功后,我们更改前面引入store

import React from 'react';
import { useAppDispatch } from '@/store/hooks';
import { useSelector } from 'react-redux';
import { updateAppName } from '@/store/features/dictSlice' 
import './App.css';

function App() {
  const { appName } = useSelector((state: any) => state.dictInfo); // 获取store的值

  const dispatch = useAppDispatch();

  dispatch(updateAppName('新的标签名')) // 更新store

  return (
    <div className="App">
      <header className="App-header">
        {appName}
        <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 App;

基础配置的完成,接下来我们配置下写页面需要装的UI组件库 antd 是基于 Ant Design 设计体系的 React UI 组件库,主要用于研发企业级中后台产品。国内后端项目开发用的最多的UI组件库。

引入antd组件

安装antd和less

yarn add antd
yarn add -D less less-loader craco-less

./index.css更名为App.less, 并引入:@import '~antd/dist/antd.less';

@import '~antd/dist/antd.less';

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
....

./index.tsx调整如下:

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.less'; // 更新CSS扩展名为less
import App from './router'; // 更改此处引入路径为Router
import reportWebVitals from './reportWebVitals';
import { store } from './store';
import { Provider } from 'react-redux';
import { HashRouter } from 'react-router-dom';

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <Provider store={store}>
    <HashRouter>
      <React.StrictMode>
        <App />
      </React.StrictMode>
    </HashRouter>
  </Provider>,
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();


export default App;

craco.config.js调整如下:

const path = require('path');
const CracoLessPlugin = require('craco-less');

module.exports = {
  webpack: {
    // webpack配置
    alias: {
      // 设置别名方便文件引用
      '@': path.resolve(__dirname, './src'), // 路径别名
    },
    module: {
      rules: [
        {
          test: /\.less$/i,
          use: [
            // compiles Less to CSS
            "style-loader",
            "css-loader",
            "less-loader",
          ],
        },
      ],
    }
  },
  plugins: [
    {
      plugin: CracoLessPlugin,
      options: {
        lessLoaderOptions: {
          lessOptions: {
            javascriptEnabled: true,
          },
        },
      },
    },
  ],
};

配置完成后,运行yarn start验证是否配置成功。

image.png

常见的后台管理系统由登录,主界面和子页面组成。 接下来我们写个简单的登录页面,保存数据到后端。

二、UI及交互

调整pages的目录如下所示:

image.png

1. 登录

注:UI组件的代码全来源至ant.design ./pages/login/index.tsx登录界面设置如下:

import { Button, Form, Input, Col, Row } from 'antd';
import React from 'react';

const Index: React.FC = () => {
  const onFinish = (values: any) => {
    console.log('Success:', values);
  };

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

  return (
    <Row>
      <Col span={8}></Col>
      <Col span={8} style={{marginTop:'200px'}}>
        <Form
          name="basic"
          labelCol={{ span: 8 }}
          wrapperCol={{ span: 16 }}
          onFinish={onFinish}
          onFinishFailed={onFinishFailed}
          autoComplete="off"
        >
          <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 wrapperCol={{ offset: 8, span: 16 }}>
            <Button type="primary" htmlType="submit">
              登 录
            </Button>
          </Form.Item>
        </Form>
      </Col>
      <Col span={8}></Col>
    </Row>
    
  );
};

export default Index;

router/router.jsx代码如下:

import { useRoutes } from 'react-router-dom';
import NotFount from '@/pages/error/404';
import Login from '@/pages/login';

export default function AppRouter() {
  let navRouters = [
    // 导航路由
    {
      path: '/',
      element: <Login />,
    },
    {
      path: '*',
      element: <NotFount />,
    },
  ];
  const element = useRoutes(navRouters);
  return element;
}


./index.tsx调整如下:

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.less'; // 更新CSS扩展名为less
import App from './router'; // 更改此处引入路径为Router
import reportWebVitals from './reportWebVitals';
import { store } from './store';
import { Provider } from 'react-redux';
import { HashRouter } from 'react-router-dom';

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <Provider store={store}>
    <HashRouter>
      <React.StrictMode>
        <App />
      </React.StrictMode>
    </HashRouter>
  </Provider>,
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

完成以上代码编写后,运行yarn start,打开url http://localhost:3000/ 显示如下:

image.png

这是登录的UI界面,还未添加登录逻辑,我们再添加主界面的逻辑。

2. 主框架

./pages/main/index.less设置如下:

#components-layout-demo-custom-trigger .trigger {
  padding: 0 24px;
  font-size: 18px;
  line-height: 64px;
  cursor: pointer;
  transition: color 0.3s;
}

#components-layout-demo-custom-trigger .trigger:hover {
  color: #1890ff;
}

#components-layout-demo-custom-trigger .logo {
  height: 32px;
  margin: 16px;
  background: rgba(255, 255, 255, 0.3);
}

.site-layout .site-layout-background {
  background: #fff;
}

.ant-layout-sider{
  height: 100vh;
}

./pages/main/index.tsx主框架界面设置如下:

import {
  MenuFoldOutlined,
  MenuUnfoldOutlined,
  UploadOutlined,
  UserOutlined,
  VideoCameraOutlined,
} from '@ant-design/icons';
import { Layout, Menu } from 'antd';
import React, { useState } from 'react';
import './index.less'; 

const { Header, Sider, Content } = Layout;

const App: React.FC = () => {
  const [collapsed, setCollapsed] = useState(false);

  return (
    <Layout>
      <Sider trigger={null} collapsible collapsed={collapsed}>
        <div className="logo" />
        <Menu
          theme="dark"
          mode="inline"
          defaultSelectedKeys={['1']}
          items={[
            {
              key: '1',
              icon: <UserOutlined />,
              label: 'nav 1',
            },
            {
              key: '2',
              icon: <VideoCameraOutlined />,
              label: 'nav 2',
            },
            {
              key: '3',
              icon: <UploadOutlined />,
              label: 'nav 3',
            },
          ]}
        />
      </Sider>
      <Layout className="site-layout">
        <Header className="site-layout-background" style={{ padding: 0 }}>
          {React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, {
            className: 'trigger',
            onClick: () => setCollapsed(!collapsed),
          })}
        </Header>
        <Content
          className="site-layout-background"
          style={{
            margin: '24px 16px',
            padding: 24,
            minHeight: 280,
          }}
        >
          Content
        </Content>
      </Layout>
    </Layout>
  );
};

export default App;

我们再调整一下路由文件./router/index.tsx

import { useRoutes } from 'react-router-dom';
import NotFount from '@/pages/error/404';
import Login from '@/pages/login';
import Main from '@/pages/main';
import App from '@/pages/main/App';

export default function AppRouter() {
  let navRouters = [
    // 导航路由
    {
      path: '/',
      element: <Main />,
      children: [
        {
          index: true,
          element: <App />,
        },
      ],
    }, {
      path: 'login',
      element: <Login />,
    }, {
      path: '*',
      element: <NotFount />,
    },
  ];
  const element = useRoutes(navRouters);
  return element;
}

重启服务yarn start,打开http://localhost:3000/ ,这个时候我们看到主界面引入的组件变成了Main组件,

image.png

那我们如何访问login的路由呢,有浏览器中输入 http://localhost:3000/#/login ,即可访问登录界面,为什么路由中有#,这是因为我们前面在./index.tsx配置路由时,配置的是 HashRouter

image.png

HashRouter 在路径中包含了#,相当于HTML的锚点定位。(# 符号的英文叫hash,所以叫HashRouter)

BrowserRouter 使用的是HTML5的新特性History,没有HashRouter(锚点定位)那样通用,低版本浏览器可能不支持。

更多关于两者的区别参照:blog.csdn.net/m0_45382009…

你也可以把HashRouter替换成BrowserRouter,查看两者的区别,有个直观的认识再看前面说到的两者区别的参照,会了解得更深一些。

好,接下来,我们要实现login登录成功后跳转到主框架/main界面

3. 登录login跳转至main

安装axios

Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。

yarn add axios --save-dev

创建文件./src/services/request.ts,代码编写如下:

import axios from 'axios';

const service = axios.create({
  // baseURL: '',
  timeout: 3000,
  headers: {'Content-Type': 'application/json;charset=utf-8'}
});

// 添加请求拦截器
service.interceptors.request.use(function (config) {
  // 在发送请求之前做些什么
  return config;
}, function (error) {
  // 对请求错误做些什么
  return Promise.reject(error);
});

// 添加响应拦截器
service.interceptors.response.use(function (response) {
  // 对响应数据做点什么
  return response;
}, function (error) {
  // 对响应错误做点什么
  return Promise.reject(error);
});

export default service;

创建文件./src/services/api/user.ts,配置如下:

import request from '../request';
// 登录接口

const basUrl = (url: string) => `/api/${url}`; // 这个地址需要你和后端对接确认

export const login = (params: object) => {
  return request({
    url: basUrl('sign'),
    method: 'post',
    data: params,
  });
};

为了方便文件引入,配置api别名./craco.config.js更改如下:

alias: {
  // 设置别名方便文件引用
  '@': path.resolve(__dirname, './src'), // 路径别名
  '@api': path.resolve(__dirname, './src/services/api'),
},

别忘了,TS版的需要配置tsconfig.json,./tsconfig.json配置如下:

"paths": {
  "@/*": ["src/*"],
  "@api/*": ["src/services/api/*"],
},

./src/pages/login/index.tsx引入login

import { Button, Form, Input, Col, Row } from 'antd';
import React from 'react';
import { login } from '@api/user'; // 引入login

引入完成后,重新启动服务,因为本次我们更改了webpack的设置,所以我们要重启服务,验证配置是否成功。 启动服务后,在浏览器中打开http://localhost:3000/#/login, 验证配置。

验证成功后,我们将./src/pages/login/index.tsx文件onFinish函数更改如下:

const onFinish = (values: any) => {
    login(values)
      .then((response: any) => {
        // 登录成功
        console.log('response', response)
      })
      .catch(error => {
        console.log('error', error)
      });
  };

未完,待續,后面有空再寫吧==!

image.png