如何結合整潔架構和MVP模式提升前端開發體驗(三) – 項目工程化配置、規範篇

工程化配置

還是開發體驗的問題,跟開發體驗有關的項目配置無非就是使用 eslint、prettier、stylelint 統一程式碼風格。

formatting and lint

eslint、prettier、stylelint 怎麼配這裡就不說了,網上文章太多了。想說的是eslint rule 'prettier/prettier': 'error'一定要開啟,以及 stylelint rule 'prettier/prettier': true 也一定要開啟。

雖然配置了eslint、prettier、stylelint,但是可能你隊友的編輯器並沒有裝相應的插件,格式化用的也不是 prettier,然後他修改一行程式碼順便把整個文件格式化了一遍。所以還得配置 husky + lint-staged,提交程式碼的時候按規範格式化回去,不符合規範的程式碼不允許提交。

如果公司的電腦配置還行的話,可以開發階段就做相應的 lint, 把錯誤拋出來,中斷編譯。webpack 可以使用 eslint-loader,stylelint-webpack-plugin;vite 可以使用 vite-plugin-eslint,vite-plugin-stylelint;vue-cli 配置幾個參數就可以開啟,具體看文檔。

ts-check

什麼是 ts-check?舉個例子,有一個後端介面的某個欄位名稱變了,由 user_name 改為了 userName,如果沒有配置開發階段進行 ts-check 並把錯誤拋出來,那麼只能全局查找調用介面的地方去修改,如果改漏了,那就喜提一個 BUG。

ts-check 可以開發階段就做,也可以提交程式碼的時候做。開發階段 webpack 安裝 fork-ts-checker-webpack-plugin ,vite 也是找相應的插件(暫時沒找到用的比較多的)。提交程式碼的時候,結合 husky 做一次全量的 check (比較耗時),react 項目執行 tsc –noEmit –skipLibCheck,vue 項目執行 vue-tsc –noEmit –skipLibCheck

ts-check 能好用的前提是你的項目是 TS 寫的,介面返回值有具體的類型定義,而不是 any。

程式碼規範

主要講講 model,service,presenter,view 這幾層的程式碼規範,之前的文章也有簡單提到過,這裡做個歸納。

model

import { reactive, ref } from "vue";
import { IFetchUserListResult } from "./api";

export const useModel = () => {
  const userList = reactive<{ value: IFetchUserListResult["result"]["rows"] }>({
    value: [],
  });
 
  return {
    userList,
  };
};

export type Model = ReturnType<typeof useModel>;

  1. 每一個欄位都要聲明類型,不要因為欄位多就用 Object[k: string]: string | number | booleanRecord<string, string> 之類的來偷懶。
  2. 可以包含一些簡單邏輯的方法,比如重置 state。
  3. vue 中欄位聲明可以移到 useModel 外面,達到狀態共享的作用,在 useModel 中 return 出去使用。

service

  1. react 技術棧,presenter 層調用的時候使用單例方法,避免每次re-render 都生成新的實例。
  2. service 要盡量保持「整潔」,不要直接調用特定環境,端的 API,盡量遵循 依賴倒置原則。比如 fetch,WebSocket,cookie,localStorage 等 web 端原生 API 以及 APP 端 JSbridge,不建議直接調用,而是抽象,封裝成單獨的庫或者工具函數,保證是可替換,容易 mock 的。Taro,uni-app 等框架的 API 也不要直接調用,可以放到 presenter 層。組件庫提供的命令式調用的組件,也不要使用。
  3. service 方法的入參要合理,不要為了適配組件庫而聲明不合理的參數,比如某個組件返回 string[] 類型的數據,實際只需要數組第一個元素,參數聲明為 string 類型即可。2個以上參數改為使用對象。
  4. 業務不複雜可以省略 service 層。

service 保證足夠的「整潔」,model 和 service 是可以直接進行單元測試的,不需要去關心是 web 環境還是小程式環境。

import { Model } from './model';

export default class Service {
  private static _indstance: Service | null = null;

  private model: Model;

  static single(model: Model) {
    if (!Service._indstance) {
      Service._indstance = new Service(model);
    }
    return Service._indstance;
  }

  constructor(model: Model) {
    this.model = model;
  }
}

presenter

import { message, Modal } from 'antd';
import { useModel } from './model';
import Service from './service';

const usePresenter = () => {
  const model = useModel();
  const service = Service.single(model);

  const handlePageChange = (page: number, pageSize: number) => {
    service.changePage(page, pageSize);
  };

  return {
    model,
    handlePageChange,
  };
};

export default usePresenter;

  1. 處理 view 事件的方法以 handle 或 on 開頭。
  2. 不要出現過多的邏輯。
  3. 生成 jsx 片段的方法以 render 開頭,比如 renderXXX。
  4. 不管是 react 還是 vue 不要解構 model,直接 model.xxxx 的方式使用。

view

  1. 組件 props 寫完整類型。
  2. jsx 不要出現嵌套的三元運算。
  3. 盡量所有的邏輯都放到 presenter 中。
  4. 不要解構 presenter 以及 model,以 presenter.xxx,model.xxxx 方式調用。

store

  1. 不要在外層去使用內層的 store。

介面請求方法

  1. 封裝的介面請求方法支援泛型
import axios, { AxiosRequestConfig } from "axios";
import { message } from "ant-design-vue";

const instance = axios.create({
  timeout: 30 * 1000,
});

// 請求攔截
instance.interceptors.request.use(
  (config) => {
    return config;
  },
  (error) => {
    return Promise.reject(error);
  },
);

// 響應攔截
instance.interceptors.response.use(
  (res) => {
    return Promise.resolve(res.data);
  },
  (error) => {
    message.error(error.message || "網路異常");
    return Promise.reject(error);
  },
);

type Request = <T = unknown>(config: AxiosRequestConfig) => Promise<T>;

export const request = instance.request as Request;

  1. 具體介面的請求方法,入參及返回值都要聲明類型,參數量最多兩個,body 數據命名為 data,非 body 數據命名為 params,都是對象類型。
  2. 參數類型及返回值類型都聲明放在一起,不需要用單獨的文件夾去放,覺得程式碼太多不好看可以用 region 注釋塊摺疊起來(vscode 支援)。
  3. 介面請求方法以 fetch,del,submit,post 等單詞開頭。
  4. 建議介面請求方法直接放在組件同級目錄里,建一個 api.ts 的文件。很多人都習慣把介面請求統一放到一個 servcies 的文件夾里,但是復用的介面又有幾個呢,維護程式碼的時候在編輯器上跨一大段距離來回切換文件夾真的是很糟糕的開發體驗。
// #region 編輯用戶
export interface IEditUserResult {
  code: number;
  msg: string;
  result: boolean;
}

export interface IEditUserParams {
  id: number;
}

export interface IEditUserData {
  name: string;
  age: number;
  mobile: string;
  address?: string;
  tags?: string[];
}

/**
 * 編輯用戶
 * //yapi.smart-xwork.cn/project/129987/interface/api/1796964
 * @author 划水摸魚糊屎工程師
 *
 * @param {IEditUserParams} params
 * @param {IEditUserData} data
 * @returns
 */
export function editUser(params: IEditUserParams, data: IEditUserData) {
  return request<IEditUserResult>(`${env.API_HOST}/api/user/edit`, {
    method: 'POST',
    data,
    params,
  });
}

// #endregion

上面程式碼是工具生成的,下篇說說提升開發效率及體驗的工具。