如何写出简洁、优雅、可维护的组件。

功能分离

这个算是面向对象里的思想,在组件里,有很多功能是独立的,比如最常见的发送验证码,确认密码等。把这些逻辑封装成一个或几个函数写在组件里的话,这在组件很小的时候没有什么影响,但是当组件功能比较复杂的时候,就会有些问题:

  1. 组件逻辑区域会变的很大,各种方法混杂很难一眼辨识
  2. 因为定义功能需要的变量和方法不在一起,导致修改麻烦

功能分离就是把这些功能抽离出来,写出一个类,然后在组件里引入。
下面是一个简单的弹窗控制的功能的类和这个类的使用:

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服务文件:

Paste_Image.png

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

需要的地方只要注入这个服务就可以获取想要的功能。相比较直接建立一个组件来实现,我觉得这样写有一些优势:
灵活性更高。写成组件会有样式的限制,而这样写没有。
更简洁。写成组件,与之沟通只能通过子父组件的传入变量,监听子组件事件的方法,你使用的组件不可避免的会多出这些变量和方法。

状态管理

不知道大伙儿有没有这样的感觉,自己写新项目的时候觉得逻辑清晰,代码简练,功能也都实现了,但是过一段时间去看或者要改自己的代码的时候...哇,这是什么玩意儿。至少我有过:flushed:
前端复杂的地方源于数不清的状态,于是我为那些有复杂状态的组件建立一个集中管理状态的对象(这里我取名为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),改变状态的事件(状态改变的判断条件),和数据处理(纯函数)四部分,各司其职,很好维护。

结语

这些适合我但不一定适合所有人,每个人都有自己的风格,各位看官感受下就好。以后我有其它方面的总结也会拿出来分享。

学习前端的同学注意了!!!
学习过程中遇到什么问题或者想获取学习资源的话,欢迎加入前端学习交流群461593224,我们一起学前端!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容