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

最近開始維護項目,而後我發現天天的時間經常是花在修改幾個小功能上,改了問題有出了另外一個問題,思考哪裏不對?,而後這麼一天就過去了。。而後我就開始思考標題好讓個人時間不老是花在修改上,下面的是我最近的一些總結。html


功能分離

這個算是面向對象裏的思想,在組件裏,有不少功能是獨立的,好比最多見的發送驗證碼,確認密碼等。把這些邏輯封裝成一個或幾個函數寫在組件裏的話,這在組件很小的時候沒有什麼影響,可是當組件功能比較複雜的時候,就會有些問題:前端

  1. 組件邏輯區域會變的很大,各類方法混雜很難一眼辨識
  2. 由於定義功能須要的變量和方法不在一塊兒,致使修改麻煩

功能分離就是把這些功能抽離出來,寫出一個類,而後在組件裏引入。async

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

export class DialogCtrl {
  isVisible = false;

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

而後在須要的組件裏引入並實例化:函數

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

在html裏能夠直接這樣用:ui

<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()的形式也很容易理解它的做用和出處。this

這樣作的另外一個好處是利於實現複用。對於能夠複用的功能,好比上面發送驗證碼的邏輯,能夠建一個全局的服務來提供。在angular裏,經過angular的服務和依賴注入能夠很輕鬆的實現,這裏是我集中功能的common.service.ts服務文件:
common.service.tsspa

common.servide.ts文件:code

@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);
  }
}

須要的地方只要注入這個服務就能夠獲取想要的功能。相比較直接創建一個組件來實現,我以爲這樣寫有一些優點:orm

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

狀態管理

不知道大夥兒有沒有這樣的感受,本身寫新項目的時候以爲邏輯清晰,代碼簡練,功能也都實現了,可是過一段時間去看或者要改本身的代碼的時候...哇,看不懂。

前端複雜的地方源於數不清的狀態,因而我爲那些有複雜狀態的組件創建一個集中管理狀態的對象(這裏我取名爲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),改變狀態的事件(狀態改變的判斷條件),和數據處理(純函數)四部分,各司其職,很好維護。

結語

這些適合我但不必定適合全部人,每一個人都有本身的風格,各位看官感覺下就好。之後我有其它方面的總結也會拿出來分享。

相關文章
相關標籤/搜索