React项目(toB)一期总结

516 阅读5分钟

项目介绍

一个机房监控项目,分为数据采集端和管理端。在机房内安装摄像头后,由各种模型分析产生各种类型报警(携带大件设备、动火监管、重要设备监管、堆放杂物和抽烟操作)。管理端能处理临时审批(由机房或者局站负责人审批是否能够进入机房),各种报警报表的展示,负责人管理等。

模版地址:github.com/AlienGao/re…

目录结构

\

Src

api

该目录存放项目的接口地址,每个模块对应一个接口文件,以下是审批模块的部分接口。

trial.js

import request from "@/utils/request";
import { CancelToken } from 'axios';
import cancels from "./cancels";
const trial = {  
    trialForm: (data) => request.post("/approval/approval", data),  // 列表    
    getTrialRecord: (data) => {   
        cancels.getTrialRecord()    
        return request.get('/approval/approval', {      
        params: data,      
        cancelToken: new CancelToken(cancel => {        
        cancels.getTrialRecord = cancel;      
        })    
    })  
}};export default trial;

cancelToken的作用是发生多次请求的情况下取消上次还没结束的请求

components

该目录存放公用组件

由于该项目滚动区域不是body,会导致使用antd组件时,菜单有滚动定位问题。所以针对此问题对antd组件做了一个简单的封装。

Antdd/BackTop.js

import { BackTop } from "antd";
function backTop(props) {  
    return <BackTop {...props} />;
}
backTop.propTypes = BackTop.propTypes;
backTop.defaultProps = {  
    ...BackTop.defaultProps,  
    target: () => document.getElementById("BackTopMark"),
};
export default backTop;

FilterComponent.js\

import { useImperativeHandle, forwardRef } from "react";
import { Form, Row, Col, Button } from "antd";
import { Select } from "@/components/Antdd";
import SeverRoomChoose from "./SeverRoomChoose";
import { useSelector } from "react-redux";
import RangeTime from "@/components/RangeTime";
const FormItem = Form.Item;
const FilterComponent = (props, ref) => {  const dict = useSelector(({ dict }) => dict);  // 重置表单值  
const locationIfm = useSelector(({ location }) => location);  
const [form] = Form.useForm();  
useImperativeHandle(ref, () => ({    formFields: form,  }));  
const initialValues = props.initialValues ? props.initialValues : null;  return (    
    <Form layout="inline" onFinish={props.onFinish} form={form} initialValues={initialValues}>      
        <Row gutter={[20, 20]} style={{ width: "100%" }}>{props.hiddenPlacePick ? null : 
        (<Col span={24}>
            <SeverRoomChoose hiddenRoom={props.hiddenRoom} form={form} locationIfm={locationIfm}/>
        </Col>)} {props.hiddenStatus ? null : (<Col span={3}>
            <FormItem name="approval_status">
                <Select options={dict.approval_status} placeholder="审批状态"></Select>            </FormItem>
        </Col>)}
        {props.extraFilters ? props.extraFilters.map((item) => item) : null}        
        <RangeTime form={form} /> 
            <Col span={4}>
                <FormItem>
                    <Button type="primary" htmlType="submit">查询</Button>         
                </FormItem>        
            </Col>      
        </Row>    
    </Form>);
};
const WrappedForm = forwardRef(FilterComponent);export default WrappedForm;

几乎所有的查询页面的顶部的筛选项都类似,由此封装了一个筛选组件,支持扩张其他筛选条件。只需要在父组件中传入extraFilters数组。该筛选组件中还包含一个地市,区域,局站,机房的级连组件(SeverRoomChoose)。可以发现这个筛选组件还向父组件暴露了实例formFields,必要的时候在父组件中可以通过绑定的ref获取该实例。

const getFormValue = useRef();

const { formFields } = getFormValue.current;
<FilterComponent  ref={getFormValue}/>

layout

定义该项目的布局

LayoutMain.js

<Layout className="layout">  
    <Sider style={{overflow: "auto", height: "100vh", position: "fixed",left: 0,}}>    
        <SubMenu />  
    </Sider>  
    <Layout className="site-layout" style={{ marginLeft: 200 }}>    
        <Header />    
        <Layout.Content className="content-container">      
            <Content />    
        </Layout.Content>  
    </Layout>
</Layout>

由左边侧边栏,头部导航栏和右侧内容组成。

Content.js

<Layout.Content  
    style={{    
        margin: "30px 16px 0",    
        overflow: "auto",    
        background: "#fff",    
        position: "relative",  
    }}  
    id="BackTopMark"
>  
    <BackTop visibilityHeight={100} />  
    <Switch>    
        <Redirect exact from="/" to={routerConfig[0].path}></Redirect>   
        <Redirect exact path="/guide" to={routerConfig[0].path}></Redirect>    {/* 增加视频路由 同时改变侧边栏 */}    
        <Redirect exact path="/videos" to="/404"></Redirect>    
        {renderRouteList(routerConfig)}    
        <Redirect to="/404"></Redirect>  
    </Switch>
</Layout.Content>

renderRouteList根据路由配置生成路由\

  const renderRouteItem = (item) => (    
      <Route      
          key={item.path}      
          path={item.path}      
          component={item.component ? item.component : null}    
      ></Route>  
  );  
  const renderRouteList = (list) =>    
      list.reduce((acc, item) => {      
          if (item.children === void 0) {        
              acc.push(renderRouteItem(item));        
              return acc;      
          }      
      item.children.forEach((subItem) => {        
          acc.push(renderRouteItem(subItem));      
      });      
  return acc;    
}, []);

pages

这里的存放的都是每个模块,跟react-router对应的路由需要一一对应。每个模块内有多个页面, 每个页面都是一个文件夹,文件名就是页面名称,每个页面都要包含如下几个文件:

  • index.jsx ---- 页面的入口文件
  • index.module.scss ---- 页面所需要的样式 使用css module避免样式污染

routes

路由配置文件

import TemporaryTrial from "@/pages/Trial/TemporaryTrial/index";
import TrialPage from "@/pages/Trial/TrialPage/index";
const routes = [  
    {    
        path: "/guide/trial",    
        name: "临时审批",    
        children: [      
            {        
                path: "/guide/trial/temporary-trial",        
                name: "审批申请",        
                component: TemporaryTrial,        
                showStationListTitle: true,      
            },  
        ]
    }
];
export default routes;

可以在路由中配置showStationListTitle是否展示局站信息,在LayoutMain中可以遍历路由,在需要有特殊展示的路由中加入需要展示的组件。

Store

存放全局状态的文夹

我们知道每个页面都有自己的actions.jsactionTypes.jsreducer.js,但是这里是全局的,另外index.js会向外暴露store,然后在src根目录下的index.js中引入使用。

index.js

import { createStore, applyMiddleware, combineReducers } from "redux";
import thunkMiddleware from "redux-thunk";
import { persistStore, persistReducer } from "redux-persist";
import storage from "redux-persist/lib/storage";/** * 这是一个 reducer,形式为 (state, action) => state 的纯函数。 
* 描述了 action 如何把 state 转变成下一个 state。 * state 的形式取决于你,可以是基本类型、数组、对象、 * 惟一的要点是  当 state 变化时需要返回全新的对象,而不是修改传入的参数。 */
import loginReducer from "./login/reducer";
const rootReducer = combineReducers({  login: loginReducer,});
const persistConfig = {  key: "root",  storage,};
const persistedReducer = persistReducer(persistConfig, rootReducer);
export default () => {  
    let store = createStore(persistedReducer, applyMiddleware(thunkMiddleware));  
    let persistor = persistStore(store);  
    return { store, persistor };
};

值得一提的是,针对PC端浏览器刷新后Store中的数据被清空,因此有必要在localStorage中保存Store。这里引入了redux-persist插件,在index.js中加入persistStore, persistReducer就能在浏览器刷新后仍能取到Store中的登录,地址信息等。

Style

存放一些全局样式\

utils

这里会存放一些自己的封装的js工具文件,比如我在项目基于axios封装了一个request.js,简化了axios的操作。\

request.js

import axios from "axios";
import { message } from "antd";
import config from "@/config";
// import qs from "qs";
const TIMEOUT = 5000;
const _axios = axios.create({  
    baseURL: config.url,  
    timeout: TIMEOUT,  
    headers: {    
        "Access-Control-Allow-Origin": "*",  
    },
});
_axios.interceptors.request.use(  
    (config) => {    
        // if (config.method === 'get') {    
        //   config.paramsSerializer = function(params) {    
        //     return qs.stringify(params, { arrayFormat: 'repeat' })    
        //   }    
    // }     
        if (localStorage.getItem("token")) {      
            config.headers.Authorization = localStorage.getItem("token");      
            _axios.defaults.headers.Authorization = localStorage.getItem("token");    
        }    
        return config;  
    },  (err) => {    
        return Promise.reject(err);  
});
_axios.interceptors.response.use(  
    (response) => {    
        if (response.status !== 200) {      
            message.error("接口出错了");      
            return Promise.reject(response.data);    
        }    
        return response.data;  
    },  (err) => {    
        if (err.message === `timeout of ${TIMEOUT}ms exceeded`) {      
            message.error("请求超时");    
        } else if (err.toString() === 'Cancel') {    
        } else {      
            message.error("接口出错了");    
        }    
        return Promise.reject(err);  
    }
);
export default _axios;

config.js

配置代理地址

index.js

webpack入口文件,主要一些全局js或者scss的导入,并执行react-dom下的render方法,代码如下:\

import React from "react";
import ReactDOM from "react-dom";import "@/style/index.css";
import "antd/dist/antd.css";import LayoutMain from "./layout";
import { history } from "@/routers";
import { Router } from "react-router-dom";
import { Provider } from "react-redux";
import storeConfig from "@/store";
import reportWebVitals from "./reportWebVitals";
import { PersistGate } from "redux-persist/integration/react";
import zhCN from "antd/es/locale/zh_CN";
import moment from "moment";
import "moment/locale/zh-cn";
import { ConfigProvider } from "antd";
const { persistor, store } = storeConfig();
moment.locale("zh-cn");
ReactDOM.render(  
    <ConfigProvider    // 全局设置弹窗容器    
        getPopupContainer={(node) => {      
            // if (node) {      
                //   return node      
            // }      
            return document.getElementById('BackTopMark');    
        }}    
    locale={zhCN}  
>    
    <Router history={history}>      
        <Provider store={store}>        
            <PersistGate loading={null} persistor={persistor}>          
                <LayoutMain />        
            </PersistGate>      
        </Provider>    
    </Router>  
</ConfigProvider>,  
document.getElementById("root"));

config-overrides.js

修改webpack配置文件

const path = require('path')
module.exports = {  
    webpack: (config) => {    
        config.output.library = 'server-room-guard'    
        config.output.libraryTarget = 'umd'    // 添加别名    
        config.resolve.alias = {      
            ...config.resolve.alias,      
            '@': path.resolve(__dirname, 'src'),    
        }    
    return config  
},  
    devServer: (configFunction) => {    
        return function (proxy, allowHost) {      
            const config = configFunction(proxy, allowHost)      
            config.headers = {        
                'Access-Control-Allow-Origin': '*',      
            }      
            return config    
        }  
    },  

总结

该项目使用react hooks开发所有组件,作为react的初学者,觉得hooks使用起来十分友好,上手快。开发过程中也发现有许多可以优化的地方, 并且都有做相关的组件封装(比如筛选,表格,级联选择,导出等等),在简化代码的同时极大的提升了开发效率。