实习的我有幸参与重构,从中学到了什么?

241 阅读9分钟

背景与主要场景

​ 本人目前在某短视频 广告平台实习,在此期间有幸参加了内部xx系统B端的某模块重构,特此总结记录下,从中学习到的知识与自己的理解,可能有部分理解不到位,或者未得其精髓,欢迎各位大佬批评指出。

​ 因为是内部平台。所以在借助 Antd design Pro上找了一些相似场景的管理系统图片。方便大家理解。主要场景如下图所示(本文示例基于 antd4.x 完成)。

​ 第一个是列表页。主要提供数据的展示,筛选,查询

截屏2021-11-13 下午3.26.57.png

第二是一个表单的场景。但是我们的场景要相对复杂很多。基本上可以说是我们系统中最主要的场景

截屏2021-11-13 下午3.27.39.png

老系统中痛点

针对我们之前的分层 可以简单分为两部分

// index.tsx
import xx from 'xxxx'

interface Iprops{
  
}
// 常量定义
const xx = []

export default function(props:Iprops){
  
  const showTarget = ()=>{
    const target = (
    	<div>
        展示内容
      </div>
    )
    
    if( xxx == xxx.xx){
      if(xxx === xx){
        return target
      }
    }
  }
  
  return (
  	<div>
    	<div>
    		展示内容
     	</div>
      {
        xxxx.xx && (
        <div>
    			判断内容展示
     		</div>
        )
      }
      {
        showTarget()
      }
    </div>
  )
}
// model.ts 这部分主要定义数据  于api 的交互 不做介绍

因为我们场景会存在各式各样的业务逻辑判断。这样的分层。会导致一个问题。我们的index.tsx 文件会变的非常庞大(即使把一些大组件提出去 做Common Compoents,但依旧抵不住上千的代码)。逻辑错综复杂,且不利于维护。

重构后设计

在旧系统中。我们可以从上述的简单描述中了解到,我们目前主要的痛点是

  1. 动辄上千行的代码。对开发造成及其不友好的体验
  2. 视图与逻辑紧密结合。代码臃肿于不利于维护
  3. 数据各自定义问题(没有描述)

基于上面的问题。 我们重新设计了我们的分层结构。如下图所示

截屏2021-11-13 下午5.41.43.png

具体实现,我们可以针对具体的场景来进行定义

如何定义结构?

List场景

//目录结构   
// xxxList
//   -List    
//.  -SearchForm
//.  -application.ts
//.  -index.tsx
//xxxModel.ts

​ 按照上图的第一个场景来看。就是一个简单的 搜索+ 列表。我们把它分为数据 与 UI两部分。

截屏2021-11-13 下午3.26.57.png

为什么把 List 的展示也看作UI状态呢?在我们的理解中。List 只用于展示数据,至于一些操作也是建立在本条数据之上,上面的searchForm 并不需要关心这个状态。在代码中的表达为

UI

// List/index.tsx
import xx from 'xxx'
export default function(){
  const { state, bizAction } = useContext(xxxListModel);
  const tableCol = [] // 定义列
  const onChange  = (currentPage: number, pageSize?: number)=>{
     bizAction.querySubmit({
      pageInfo: {
        pageSize,
        currentPage,
      },
    });
  }
  return (
    <div>
      {
        权限控制。&& Button 展示
      }
      <Table
        dataSource={state.uiState.list}
        loading={state.uiState.listLoading}
        columns={tableCol}
        pagination={{
          current: state.uiState.pageInfo.currentPage,
          pageSize: state.uiState.pageInfo.pageSize,
          total: state.uiState.pageInfo.totalCount,
          onChange
        }}
      />
    </div>
  );
}
// SearchForm/index.tsx
import xx from 'xxx'
export default function(){
 const { from, bizAction } = useContext(xxxListModel);
  
 const onSearch = (values: any) => {
    bizAction.querySubmit({
      condition: {
        ...values,
      },
    });
  };
  
  // 参考代码
  // https://ant.design/components/form-cn/ 高级搜索部分
  return (
    <Form
      onFinish={onSearch}
    >
      <!-- 吧 formItem 展现。这里不做展示 有需要参考上述地址-->
      <Row gutter={24}>{getFields()}</Row> 
      <Row>
        <Col span={24} style={{ textAlign: 'right' }}>
          <Button type="primary" htmlType="submit">
            查询
          </Button>
          <Button
            style={{ margin: '0 8px' }}
            onClick={() => {
              form.resetFields();
            }}
          >
            清空
          </Button>
        </Col>
      </Row>
    </Form>
  );
}

Application

// Applications.ts
export const xxxListModel = () => {
  const { xxxEntity: xxxState, xxxAction } = useXxxEntityModel();
  const { uiState, dispatchUi } = useListUiState(); // 参考后面独立UI状态
  
  // 其他自定义状态或者逻辑
 
  class bizAction{
    
    static changeState(value: any) {
      xxxAction.dispatchXxx({
        type: xxxAction.SetXxx,
        payload: value,
      });
    }
    
    // 利用class. 使用装饰器做统一处理。 不用再写tryCatch 简单高效
    @Catch(Error, () => {})
     static async querySubmit(params: any) {
      dispatchUi({ type: ListActionType.LodingStart });
      let condition: any = {
        pageInfo: params.pageInfo || uiState.pageInfo,
      };
      const conditionParams = params.condition

      if (params.condition) {
        condition.pageInfo = {
          pageSize: 10,
          currentPage: 1,
        };
      }
       // 整合数据
      const queryCondition = hanldeYourDate(conditionParams)
      
      // 调用底层能力
      const { list, pageInfo } = await xxxAction.queryXXXList(queryCondition);

       // 改变UI 状态 展示数据
      dispatchUi({
        type: ListActionType.LoadingEnd,
        payload: {
          pageInfo,
          list,
        },
      });
    }
  }
  
  return {
    state: {
      uiState,
    },
    bizAction,
  };
}

export const xxxListModel = createContext({});


// xxxModel.ts   定义原子化数据
import xx from 'xxx'
export interface xxxStateType {
  // 定义物品属性。搜索页和searchFrom 详情页等都基于这里面属性
  xxxName:string;
}

export enum XxxAction {
  ResetXxx,
  SetXxx,
}

export const useXxxEntityModel = () => {
  
  const xxxReducer = (state: xxxStateType, action: IAction<XxxAction>) => {
    // 数据之间的联动 类型转化等
    switch (action.type) {
      case XxxAction.ResetXxx:
        return {
         xxxName:'achen'
        } as xxxStateType;
      case XxxAction.SetXxx:
        state.xxxName = action.payload.xxxName ?? state.xxxName;
        // ......
        break;
      default:
    }
  };
    
  const [xxxEntity, dispatchXxx] = useImmerReducer<
    xxxStateType,
    IAction<XxxAction>
  >(xxxReducer, {} as xxxStateType);
  
    
    class action {

      // 状态改变
      static dispatchXxx = dispatchXxx;

      @Catch(Error, () => {})
      static resetXxx() {
        dispatchXxx({
          type: XxxAction.ResetXxx,
        });
      }
      
      @Catch(Error, (err) => {
        message.error(err.errorMsg);
      })
      static async submitXXXX(params: XxxParams) { // XxxParams为后台数据返回定义
        // 数据转换、组装、数据校验、接口请求、返回处理
        const result = await apis.xxxT({
          XxxParams: params,
        });
        return result;
      }
    }
   
    return {
      xxxEntity,
      dispatchXxx: action,
    };
}

PageEdit 场景

通过最开始的介绍,已经说明过我们的场景为 多表单提交,联动等。所以基于list的设计上。我们利用抽象类定义一些属性(类似抽离的Vue?)方便我们开发

export function Control() {
  abstract class ControlClass {
    // UI 状态
    public static uiState?: any;

    // form 
    public static form?: any;

    // 计算属性
    public static computedState?: Record<string, any>;

    // 监听  自定义hooks
    public static sideEffect?: Record<string, any>;

    // 表单确定
    public static onFormConfirm?: Function;

  }
  return ControlClass;
}

form

针对form,我们想做的事在application统一管理form表单属性

const { formInstances, formFieldsName, formsName } = getForm({ // 传入 formName和字段名称 返回form实例 名称 属性值
    x: ['1', '2', '3', '4'],
    xx: ['5', '6'],
    xxx: ['7'],
  }); 

export {
  form:{
      formInstances, 
      formFieldsName, 
      formsName
    }
}

// 使用的过程中 
<Form
        form={formInstances.xxxBase}
        name={formsName.xxxBase}
      >
        <FormItem
          label="姓名"
          name={formFieldsName.name}
          rules={[
            { max: 20, message: '你是名字是不是长的有点离谱?' },
          ]}
        >
          <Input placeholder="请输入姓名"  />
        </FormItem>
</Form>

至于 这个getForm 其实就是包了Form.useForm 在导出。

// 用来给对象赋值key
export function strEnum<T extends string>(o: Array<T>): { [K in T]: K } {
  return o.reduce((res, key) => {
    res[key] = key;
    return res;
  }, Object.create(null));
}

export function getForm<T extends string, U extends string>(opt: Record<T, U[]>) {
  const formsName = strEnum(Object.keys(opt) as T[]);
  const formInstances = {} as Record<T, FormInstance>;
  let formFieldsName = {} as { [K in U]: K };

  Object.keys(opt).forEach((k) => {
    [formInstances[k as T]] = useForm();
    formFieldsName = {
      ...formFieldsName,
      ...strEnum(opt[k as T]),
    };
  });

  return {
    formInstances, 
    formsName,
    formFieldsName,// 所有formFields name
  };
}

computedState

用来处理一些复杂计算。把这部分抽离出来 减轻index.tsx的逻辑

// 
class a extends Control(){
   static get computedState(){
    return {
      xxxList(){
        const xxlist = formInstances.xxx.getFieldValue(formFieldsName.xxxlist)
        // ....
          let res  
        // 进行处理比较 获得一个值
          return res
      }
    } 
   }
}

sideEffect

这个简单理解,就是自定义hooks,都写在一起方便管理。用的时候调用即可

state Or reducer

​ 我们在平时使用的定义状态的时候,通常都会使用useState来进行状态定义。针对简单场景来看,这是比较灵活。但是就是因为太过于灵活。导致了我们在开发过程中,在任何一个有需要的地方都可以调用setState。导致了我们逻辑分散在各处。对开发和维护的效率都有一定的降低。 ​ 而使用useReducer ,通过dispatch传递对应的action。在一处地方定义 监管所有的状态变化。更方便的维护。逻辑也更加清楚

例子就不再提供了,建议自行写一下,感受一下两者差别。

独立UI状态

​ 为什么要把UI状态独立?我相信,有一部分的开发者的model的层中,都会有一个Loading的状态。不同的page 有不同的 loading。既然都有,为什么不独立出来呢?既然打算独立了。为什么不做一个更通用的呢?针对我们的B端的场景,我们做了如下两种

列表的独立UI

​ 这部分感觉还是可以做一些事情。只是在重构时考虑到尽量的简化,就保证了单一性。

enum ListActionType {
  LodingStart,
  LoadingEnd,
  UpdatePage,
}


const useListUiState = <T>()=>{
  // 定义 list 基本的数据类型,可根据需要进行添加
  const defalutUiState = {
    list:T[],
    listLoading:false,
    pageInfo:{
      current:1,
      pageSize:10,
    }
  }
  
  // 定义 reducer。至于为什么是reducer 而不是 useState。上文已经简单介绍了
  const uiReducer = (state: typeof defalutUiState, action: ListActionType)=>{
    if (!action) return;
    switch (action.type) {
      case ListActionType.LodingStart: {
        state.listLoading = true;
        break;
      }
      case ListActionType.LoadingEnd: {
        state.listLoading = false;
        state.list = action.payload.list;
        state.pageInfo = action.payload.pageInfo;
        break;
      }
      case ListActionType.UpdatePage: {
        if (action.payload) {
          state.pageInfo = action.payload;
        }
        break;
      }
      default:
    }
  } 
  
  // useImmerReducer 其实就是 借助immer 做一个数据持久化
  const [uiState, dispatchUi] = useImmerReducer<typeof defalutUiState, IAction<ListActionType>>(
    uiReducer,
    defalutUiState,
  );
  
  return {
    uiState,
    disPatchUi
  }
}

表单的独立UI

​ 相对于list,表单的场景更多会在page整体考虑。因为我们有很多的 新建,编辑,更新等场景,所以在一定程度上我们会吧一些无关的状态尽量的整合,达到外部无感知的目的

// 定义
import { useHistory } from 'react-router';

export enum EditorActionType {
  LoadingStart,
  LoadingEnd,
}

const usePageUiReducer = () => {
  let status = '';
  const history = useHistory();

  if (history.location.pathname.includes('create')) {
    status = 'create';
  } else if (history.location.pathname.includes('update')) {
    status = 'update';
  } else if (history.location.pathname.includes('copy')) {
    status = 'copy';
  }

  const [id] = window.location.hash.split('/').slice(-1);
  const defaultUiState = {
    isUpdated: status === 'update',
    isCreated: status === 'create',
    isCopied: status === 'copy',
    loading: false,
    id: Number.isNaN(parseInt(id, 10)) ? undefined : id,
  };

  const uiReducer = (state: typeof defaultUiState, action: EditorActionType) => {
    if (!action) return;
    switch (action) {
      case EditorActionType.LoadingStart:
        state.loading = true;
        break;
      case EditorActionType.LoadingEnd:
        state.loading = false;
        break;
      default:
    }
  };
  const [uiState, dispatchUi] = useImmerReducer(uiReducer, defaultUiState);

  return {
    uiState,
    dispatchUi,
  };
};

这样外部在使用的过程中,只需要取出对应的状态进行判断即可。无需关心状态的获取。

使用场景就不列举了。跟list的使用差不多,根据各位的需要来增删。

发现的问题

在实际的开发过程中,我们的xxxModel在某种意义上变成了api的调用层,目前我们没出现在 前后在字段不一致等状态。也可能我学艺不精,还没理解到这层的作用。

枚举,什么才是最好的形式

前后各一套

即前端有一套自己定义的枚举,后台也有一套自己定义的枚举。如果有一需求需要新增加枚举值,这样前后太都需要增加对应的枚举,即把一个逻辑分散在了两处地方。

好处

  1. 前后端 可能会根据枚举的定义,整合一些特殊逻辑,新增一些枚举做判断。在接口返回/提交的过程中多一层转化,转化为前后统一规定的枚举。

不太好的地方

上述已经说了,相当于一个逻辑的定义分散在两处。

使用时接口拉取

利于接口文档生成的对应ts文件,在需要用到的时候去接口拉去枚举定义,

const select = ()=>{
  const[enum,setEnum] = useState([])
  
  useEffect(()=>{
    const enum = await  apis.XxxEnumGet() // 获取到所需的enum
      setEnum(enum)
  },[])
    return (
    <Select
      placeholder="请选择"
    >
      {enum.map((item) => (
        <Select.Option
          title={item.label}
          key={item.value}
          value={item.value}
        >
          {item.label}
        </Select.Option>
      ))}
    </Select>
  );
}

上述时一个简答的 demo,但是也会存在一个问题。如果我们需要利用枚举进行判断的时候(比如在提交的时候做校验等),需要在各处需要判断的地方都要发一次请求。 虽然可以放到全局state中。但是感觉这个放在全局中,不太合适

生成对应的文件存放

写了一个js脚本,服务启动时 执行命令。自动去后台拉去对应的接口。再生成对应的文件存放在对应的const下。这样我们只需要在需要的时候执行此命令即可。另外写了一些EnumToOption,EnumMap等,做一些 枚举转换map Options等共用函数,进一步提高效率

// 类似这种 比较简单。就不展示其他了
interface IItem {
  value: number;
  name: string;
}

const enumToOptions = (enums: IItem[] = []) => {
    return enums?.map((val) => ({
      label: val.name,
      value: val.value,
    }));
  };

其他

如果还有其他方式欢迎安利~

收获

我不过是一个普通的实习生,以上很多都是学习组内各位大佬的代码 最后获得自己的提炼。对我来讲,这已经是打开了新世界大门,组内大佬还给我推荐《重构,改善既有的代码设计》,《JavaScript设计模式与开发实践》等书籍,奈何道行尚浅,现在还只是看看,未能实战。 ​

组内还在进行其他项目,有技术,有业务。等到学习完他们的代码,可能会有其他感想或体会,到时候再做记录。 ​

再次感谢组内大佬对我各种xx问题的耐心回答。

安利

《React 学习之道》The Road to React (简体中文版)