如何寫出簡潔、優雅、可維護的元件。

如何寫出簡潔、優雅、可維護的元件。

最近開始維護專案,然後我發現每天的時間常常是花在修改幾個小功能上,改了問題有出了另一個問題,思考哪裡不對?,然後這麼一天就過去了。。然後我就開始思考標題好讓我的時間不總是花在修改上,下面的是我最近的一些總結。


功能分離

這個算是物件導向裡的思想,在元件裡,有很多功能是獨立的,比如最常見的傳送驗證碼,確認密碼等。把這些邏輯封裝成一個或幾個函式寫在元件裡的話,這在元件很小的時候沒有什麼影響,但是當元件功能比較複雜的時候,就會有些問題:

元件邏輯區域會變的很大,各種方法混雜很難一眼辨識
因為定義功能需要的變數和方法不在一起,導致修改麻煩

功能分離就是把這些功能抽離出來,寫出一個類,然後在元件裡引入。

下面是一個簡單的彈窗控制的功能的類和這個類的使用:

export class DialogCtrl {
  isVisible = false;

  open () {
    this.isVisible = true;
  }
  close () {
    this.isVisible = false;
  }
}

然後在需要的元件裡引入並例項化:

DialogCtrl = new this.CommonService.DialogCtrl();  // 是否開啟彈窗

在html裡可以直接這樣用:

<nz-modal
    [nzVisible]="DialogCtrl.isVisible" 
    [nzTitle]="'更新密碼'" 
    [nzContent]="modalContent" 
    (nzOnCancel)="DialogCtrl.close()" 
    [nzConfirmLoading]="isSubmiting"
    nzOkText="儲存"
    (nzOnOk)="savePassword()"></nz-modal>

這個nz-modal是一個彈窗,在元件裡我們只有一個變數的宣告,如此簡潔!而在html裡DialogCtrl.isVisible,DialogCtrl.close()的形式也很容易理解它的作用和出處。

這樣做的另一個好處是利於實現複用。對於可以複用的功能,比如上面傳送驗證碼的邏輯,可以建一個全域性的服務來提供。在angular裡,通過angular的服務和依賴注入可以很輕鬆的實現,這裡是我集中功能的common.service.ts服務檔案:

common.servide.ts檔案:

@Injectable()
export class CommonService {
  // 功能類集合
  public DialogCtrl = DialogCtrl;
  public MessageCodeCtrl = MessageCodeCtrl;
  public CheckPasswordCtrl = CheckPasswordCtrl;

  constructor(
    private http: HttpClient
  ) { }

  /* 獲取簡訊驗證碼(這些功能需要用到的方法)
  -------------------------- */
  public getVerificationCode (phoneNum: string): Observable<any> {
    return this.http.get('/account/short_message?phoneNumber='   phoneNum);
  }
}

需要的地方只要注入這個服務就可以獲取想要的功能。相比較直接建立一個元件來實現,我覺得這樣寫有一些優勢:

靈活性更高。寫成元件會有樣式的限制,而這樣寫沒有。
更簡潔。寫成元件,與之溝通只能通過子父元件的傳入變數,監聽子元件事件的方法,你使用的元件不可避免的會多出這些變數和方法。

狀態管理

不知道大夥兒有沒有這樣的感覺,自己寫新專案的時候覺得邏輯清晰,程式碼簡練,功能也都實現了,但是過一段時間去看或者要改自己的程式碼的時候…哇,看不懂。

前端複雜的地方源於數不清的狀態,於是我為那些有複雜狀態的元件建立一個集中管理狀態的物件(這裡我取名為Impure):

  /* 變數定義 -- 狀態
  -------------------------- */
  registerForm: FormGroup;  // 註冊賬號表單
  registerInfoForm: FormGroup; // 公司資訊表單
  isSubmitting = false; // 表單是否正在提交
  nowForm = 'registerForm';  // 當前正在操作的表單
  MessageCodeCtrl = new this.CommonService.MessageCodeCtrl(this.Msg, this.CommonService); // 驗證碼控制

  /* 變數定義 -- 定值
  -------------------------- */
  registerFormSubmitAttr = ['login', 'password', 'shortMessageCode', 'roles', 'langKey'];
  registerInfoFormFormSubmitAttr = ['simName', 'contacter', 'officeTel', 'uid'];

  /* 改變狀態事件
  -------------------------- */
  Impure = {

    // 表單初始化
    RegisterFormInit: () => this.registerForm = this.registerFormInit(),
    RegisterInfoFormInit: () => this.registerInfoForm = this.registerInfoFormInit(),

    // 驗證碼不合法
    MessageCodeInvalid: {
      notSend: () => this.Msg.error('您還未傳送驗證碼'),
      notRight: () => this.Msg.error('驗證碼錯誤')
    },

    // 表單提交
    FormSubmit: {
      invalid: () => this.Msg.error('表單填寫有誤'),
      before: () => this.isSubmitting = true,
      registerOk: () => {
        this.Msg.success('賬號註冊成功');
        this.nowForm = 'registerInfoForm';
      },
      registerInfoOk: () => {
        this.Msg.success('儲存資訊成功!請耐心等待管理員稽核');
        this.Router.navigate(['/login']);
      },
      fail: () => this.Msg.error('提交失敗,請重試'),
      after: () => this.isSubmitting = false
    }
  };

這是一個簡單的有兩個表單的註冊元件,因為兩個表單html耦合度很高,所以寫在了一起。

在元件內將變數分為狀態和定值的兩類,宣告瞭一個Impure物件來集中管理這些狀態,原則上這個元件裡所有狀態的改變都寫在Impure裡,而將事件觸發的判斷條件,資料處理寫在Impure外面。

可以對比下這兩個使用Impure和不使用Impure的表單提交方法:

  /* 註冊賬號表單提交(Impure)
  -------------------------- */
  async register (form) {
    const _ = this.Fp._; // ramda庫,用於資料處理
    const { MessageCodeInvalid, FormSubmit } = this.Impure;

    // 表單不合法
    if (form.invalid) { FormSubmit.invalid(); return; }

    // 驗證碼不合法
    if (!this.MessageCodeCtrl.code) { MessageCodeInvalid.notSend(); return; }
    if (this.MessageCodeCtrl.code !== form.controls.shortMessageCode.value) { MessageCodeInvalid.notRight(); return; }

    // 表單提交
    FormSubmit.before();
    const data = _.compose(_.pick(this.registerFormSubmitAttr), _.map(_.prop('value')))(form.controls); // 資料處理
    const res = await this.AccountService.producerRegisterFirst(data).toPromise();
    if (!res) { FormSubmit.registerOk(); } else { FormSubmit.fail(); }
    FormSubmit.after();
  }

  /* 公司資訊表單提交(非Impure)
  -------------------------- */
  async registerInfo ({ simName, contacter, officeTel }) {
    // 表單不合法
    if (this.registerInfoForm.invalid) { this.Msg.error('表單填寫有誤'); return; }

    // 表單提交
    this.isSubmitting = true;
    const data = { // 資料處理
      simName: simName.value,
      contacter: contacter.value,
      officeTel: officeTel.value,
      uid: this.registerForm.controls.phone.value
    };
    const res = await this.AccountService.producerRegisterSecond(data).toPromise();
    if (!res) {
      this.Msg.success('儲存資訊成功!請耐心等待管理員稽核');
      this.Router.navigate(['/login']);
    } else {
      this.Msg.error('提交失敗,請重試');
    }
    this.isSubmitting = false;
  }

使用Impure管理狀態後,邏輯清晰,在提交表單時你只需要關注事件發生的條件就可以了,而第二個條件和狀態寫在一起會很混亂,不能一眼清楚這個狀態改變發生在什麼時候,特別是你一段時間再來看的時候。

其實這裡的資料處理(這裡面的data)應該單獨拿出來寫一個方法的,我只是來頂一下用純函式來處理資料的優點,這裡的_是用了ramda這個庫。相比較第二個處理方式,第一種方式更加優雅,簡潔,很容易看出資料的源頭是什麼(這裡是form.controls),單獨抽離成資料處理函式也有很高的複用性。

假如某一天你要改下這裡兩個表格的成功後的狀態,不再需要到這兩個長長的提交函式裡找到它們然後一個一個改,只要在Impure裡面改就可以了,你甚至不需要看那兩個提交的方法。

這樣子,一個元件可以大致分為狀態,狀態管理(impure),改變狀態的事件(狀態改變的判斷條件),和資料處理(純函式)四部分,各司其職,很好維護。

結語

這些適合我但不一定適合所有人,每個人都有自己的風格,各位看官感受下就好。以後我有其它方面的總結也會拿出來分享。