在工作过程中,我们常常会遇到一些项目,随着时间的推移变得越来越难以维护的情况。经过分析,可以总结出以下几点原因:功能的持续迭代导致代码冗余增多、缺乏统一规范或者团队成员未严格执行规范、项目成员的变动等。
那么,如何避免或减少这些问题的出现呢?我整理了几个常用的优化技巧:
1、项目结构:
在项目结构方面,我们可以借鉴单一职责的设计思想。在基于umi架构的目录结构基础上,可以增加一些文件夹和文件,例如common、hooks、types等。当然,这并不是一种固定的规范,在开发过程中,可以根据实际项目情况进行增删和调整。为什么要增加这些内容呢?用专业术语来说,是为了对代码进行精细化管理,避免组件代码无限堆叠。根据不同的职责,将之前写在组件内部的代码拆分到独立的模块中,这样有利于代码的阅读和维护。
├── config ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── 配置文件 路由配置
│ └── routes.ts
├── mock ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── mock数据
│ └── api.ts
├── src
│ ├── assets ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── 静态文件 图标 图片 等
│ ├── components ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ─ 公共组件
│ ├── layouts ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── layout布局
│ ├── pages ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── 页面集合
│ │ ├── pageA ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ─ ─ 页面
│ │ │ ├── common
│ │ │ │ ├── constant.ts ── ── ── ── ── ── ── ── ── ── ── ── ── 常量
│ │ │ │ ├── context.ts ── ── ── ── ── ── ── ── ── ── ── ── ── context
│ │ │ │ ├── enum.ts ── ── ── ── ── ── ── ── ── ── ── ── ── ── ─ 枚举
│ │ │ │ ├── validator.ts ── ── ── ── ── ── ── ── ── ── ── ── ── 正则表达式、表单校验方法
│ │ │ │ └── utils.ts ── ── ── ── ── ── ── ── ── ── ── ── ── ── util方法
│ │ │ ├── components ── ── ── ── ── ── ── ── ── ── ── ── ── ── ─ 组件
│ │ │ ├── hooks ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── 自定义hooks
│ │ │ │ ├── useA.ts
│ │ │ │ └── useB.ts
│ │ │ ├── index.less ── ── ── ── ── ── ── ── ── ── ── ── ── ── ─ 页面样式
│ │ │ ├── index.tsx ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── 页面入口文件
│ │ │ └── types.ts ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ts声明
│ ├── services ── ── ── ── ── ── ── ── ── ── ── ── ── ── ──── ── ──── 接口
│ │ └── index.ts
│ ├── types.ts ── ── ── ── ── ── ── ── ── ── ── ── ── ── ──── ── ──── 全局ts声明
│ └── utils ── ── ── ── ── ── ── ── ── ── ── ── ── ── ──── ── ── ──── utils
│ │ ├── validator.ts ── ── ── ── ── ── ── ── ── ── ── ── ── ── ──── 全局表单校验
│ │ ├── request.ts ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── 请求拦截器
│ │ └── utils.ts ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── ── 通用utils方法
│ └── app.ts
├── package.json
└── tsconfig.json
2、全局封装:
全局封装在前端架构中扮演着重要的角色。它不仅可以帮助我们减少代码冗余,还可以让开发人员更专注于核心业务逻辑的实现,而不必重复编写基础功能的代码。当需要修改或更新功能时,只需在封装的模块中进行修改,而不需要逐个查找和修改所有使用该功能的地方,极大的提高了开发效率。
// 拦截器
import eventEmit from '@/utils/eventEmit';
// 请求拦截器
const requestInterceptors = (url, options) => {
if (options.loading) {
eventEmit.emit('triggerLoading', true);
}
return {
url,
options: options,
};
};
// 响应拦截器
const responseInterceptors = async (response, options) => {
if (options.loading) {
eventEmit.emit('triggerLoading', false);
}
const data = await response.clone().json();
// 未登录 跳转到登录页面
if (data?.code === 1000) {
history.push(LOGIN_PATH);
return;
}
// 在拦截器里面统一校验code
if (data.code === 0 ) {
return data;
}
message.error(data.message);
return Promise.reject(data.message);
};
// 触发loading效果:接口请求的时候增加loading参数
const getDataSource = (params,loading=true) => {
return request(url ,{ params,loading })
}
// Loading组件
import React, { useEffect, useRef, useState } from 'react';
import { Spin } from 'antd';
import eventEmit from '@/utils/eventEmit';
import './index.less';
const Loading: React.FC = () => {
const refCount = useRef(0);
const [loading, setLoading] = useState<boolean>(false);
useEffect(() => {
eventEmit.addListener('triggerLoading', onLoading);
return () => {
eventEmit.removeListener('triggerLoading', onLoading);
};
}, []);
const onLoading = (isLoading: boolean) => {
if (isLoading) {
refCount.current += 1;
} else {
refCount.current -= 1;
}
setLoading(!!refCount.current);
};
return (
<Spin className="global-loading" spinning={loading} />
);
};
export default Loading;
上面的代码我们实现了两个功能的封装:全局响应code校验和全局loading。 大部分项目都做了全局响应校验,这块就不展开讲了。唯一要注意的就是不要重复校验(做了全局校验,实际的接口调用时,就不用再做校验)。关于全局loading,实现方式有很多种,以上代码是结合发布订阅模式来实现的。主要有两个优点,一是不用在每个组件内部实现loading逻辑,二是Loading使用了固定定位,所以组件loading状态的改变,不会导致整个页面的重新render和文档流的重绘与重排。使用时只需在全局layout组件导入Loading组件。
3、代码排版:
良好的代码排版对于代码的可读性和可维护性非常重要。通过对不同职责的代码进行归类和分层,让代码看起来更整洁,视觉体验更好。下面是我个人比较喜欢的一种代码排版方式:
// 1 react
import { FC, useState, useEffect } from 'react';
// 2 第三方npm包 antd umi moment等
import { xxx } from 'antd';
import { history } from 'umi';
import moment from 'moment';
// 3 组件
import { xxx } from '@/components/xxxx'
import { xxx } from '@/components/xxxx'
import { xxxx } from '../../components/xxxxx'
import { abc } from '../../components/abc'
// 4 其他 接口 常量 util方法 等
import { xxx } from '../constant';
import { DetailInfo } from './types';
import { xxx } from '@/utils/utils';
// 5 样式
import styles from './index.less';
// 6 组件ts声明 每个组件只保留一个基础的ts声明 其余的放到types.ts文件下
interface PropsType {
}
const OrderDetail:FC<PropsType>=(props)=>{
// 7 props
const {abc} = props;
// 8 状态
const [detailInfo,setDetailInfo] = useState<DetailInfo>(xxx);
const [xxx,xxx] = useState(xxx);
// 9 hooks
const xxx = useMemo(()=>{return xxxx},[abc]);
useEffect(()=>{},[a]);
useEffect(()=>{},[b]);
// 10 方法
const onChange=()=>{};
const onClick=()=>{};
return(
<div></div>
)
}
export default OrderDetail;
4、代码聚合:
代码聚合指的是实现高内聚低耦合的原则,这也是评判软件设计优劣的一个标准。许多设计模式和框架都基于高内聚低耦合的原则。例如,我们可以拆分原本有1000行代码的组件A,虽然拆分后的代码量可能会增加,但是通过拆分,我们可以更快的定位我们要改动的模块,并且可以更准确的评估我们的改动所影响的范围。与代码的灵活性和可维护性相比,这些额外的代码量可以忽略不计。关于代码聚合的案例,可以参考React性能优化之状态内聚。

总结
本文主要从代码层面的设计和实现角度,探讨了如何使前端项目更易于维护。然而,要构建一个优秀的前端项目,我们还需从更广泛的角度去思考。本文的目的是希望开发者不要忽略一些细节的问题,把简单的事情做到极致,然后再去思考一些更深层次的东西。只有在不断地优化和改进中,我们才能打造出高质量、易于维护的前端项目。