Loading…

感觉博客又长草了,逮着空隙来更一篇。

扳着手头算算,正儿八经学JavaScript也已经有三年时间了,还记得刚毕业的时候曾经给自己定下过一个目标:给公司或者自己写一套UI library,免得每次到做项目的时候都要为UI而费很多心思,可惜的是在创图时间不长,临到最后离职也未能如愿,而且现在回头想想,当初也算是空有想法而力有未逮。而到了新公司之后,因为需要,又花了一部分心思点了一些后端的技能树,而主要的时间也是在解决数据和内部工具的问题,这个愿景也就一直搁置了。不过好在,最后还是找机会完成了一个库 - Fermi, 给了自己一个交代。

简单的来说,Fermi是一个基于AngularJS实现的UI库,封装一些开发中常见的元素,e.g. Tab、Progress、Tooltip等等,而得益于Angular Directive提供的Custom Element特性,这些基础组件可以像原生控件一样以声明式的形式被应用在HTML模板中,由AngularJS在客户端的运行时将这些元素自动展开并替换原来的DOM节点。

一个典型的进度条:

<Fermi:Line value="Progress.line" default="0"></Fermi:Line>  

一些设计

秉着「eat your own dog food」的精神,我用Fermi构建了一个它自己的介绍页,同时写了一份还算详尽的文档挂在Github page上,因而博文里也就不多介绍用法了,来更多的谈谈一部分项目的设计和考虑。

这个项目脱胎于Pearson内部的一个Class Scheduler的prototype,实现了一些可重用的组件,但其实Angular1的组件化方案并不如React、Vue提供的方案清晰,因此在实践的过程中做了一部分自己的探索,所以这个项目在某些地方上可能算是anti-pattern的。

模块

在项目的早期,我做过一次文件结构的重构,由原先单文件维护转换为基于webpack的module方案,对比之下,ng自带的module机制就显得冗余,同时存在命名污染的问题,相比module反而更像是namespace。而基于webpack的方案,在babel的加持下模块语法拥抱ES6 module,而且组件的所有依赖(template,scss,asset)可以被就近管理,有更好的维护性。

依赖注入

ng有一个非常特别的IoC机制,很大程度上可以提高模块的可测试性,作模块和模块的解耦,不过同样,也带来了副作用——语法噪音。

app.controller('Home', ['$http', function($http){  
    // code
}])

或者用ES6 class改写,但仍然需要显式声明$inject这个内部属性。

export default class Home{  
     constructor($http){
         this.$http = $http
     }
}

Home.$inject = ['$http']  

好在ES7有一个decorator提案,Babel5也为之做了实验性的支持(但这个特性在Babel6中因为提案的变更暂时取消了,如需使用,需要legacy decorator plugin),得益于此,允许我在项目中实现了一个dependencies的装饰器,算是减少了一部分噪音。

@dependencies('$http')
export default class{  
    constructor($http){
        this.$http = $http
    }
}

出于一些别的考量,这个装饰器实现并没有把语法噪音降低到最小:比如构造函数上的形参声明也是噪音,这里的定义其实是可以由装饰器自动resolve的。

更好的依赖注入装饰器的实现请看这里

放弃$element

除此以外,ng为了方便开发者特地提供了jqLite,因为directive的实现必然免不了对DOM节点的遍历和操作,在$element service调用中返回值就是一个jqLite对象,但不得不说jqLite有些令人失望,并不像jQuery,jqLite的DOM方法只会返回裸对象,以至于如果要沿着某种路径对一系列的DOM操作时,则不得不一次次将元素包裹成jqLite对象。为了提高自己的开发体验,在复杂场景下还是选择了弃用了$element服务,手动实现了部分常用的DOM方法,然后使用ES7的bind operator(::)草案,将方法调用级联化,获得类似jQuery的开发体验。

sample:

domNode::prepend(tmpl1)::last(tmpl2)  

Classify Directive

开头之所以说ng提供组件化方案是不够清晰的原因主要就在于此,由于ng引入了太多的concept,比如一个custom element有compile、controller、link等多个成员函数在不同的时间被初始化。而且ng的directive设计中,构建函数需要返回一个单例对象,custom element在运行时不断调用这个对象上的方法来计算自身的状态,然而每个元素的产生的状态不会属于对象或者custom element实例本身而是属于它的scope。这一设计直接导致了Directive没办法很好的和ES6 class结合,组件类的成员函数和成员属性在定义时是聚合的,但最终在实例化时是离散的。所以再一次为了提高开发体验,我对组件的构建过程做了一些调整 —— 设计了一个directive build的方法保存中间函数产生的状态,并且追踪组件和状态的对应关系,保证这些中间状态可以伴随组件被实例化的过程,可以在不同阶段被访问到。同时,又额外为组件类设计了一个passing的方法,用于解决classify之后父组件和子组件通过controller通讯的问题。(controller方法this的指向改变)

大致如下:

export default class Component{  
    constructor(){
        this.replace  = true
        this.restrict = 'EA'
        this.scope = {}
    }

    compile(){ // 1
        //由于compile阶段的DOM并不稳定,因此建议只在这里缓存非dom节点的状态
        this.status= false
        return this.link
    }

    controller(){ //2
        console.log(this.status)
    }

    passing(exports){ //3
        exports.output= () => console.log('hello Angular') //子组件可以通过link的第四个参数访问到这个method函数
    }

    link($scope, $elem, attrs, ctrl){ //4
        ctrl.output()
    }
}

写在最后

老实说,这个项目花费了我不少的业余时间和精力(也可能是我手残),但这个项目还是让我学习深入了不少知识。e.g.

  • 组件的结构设计
  • 全局服务类组件的API设计
  • ng和ES6工作流的整合
  • ES next(7)的新特性,such as: bind operator
  • Angular的最佳实践

同时也对ng的优缺点有了更深的了解,在横向对比、做技术选型时候有了更多的参照,当然也更加期待Angular2可以早日脱离Beta,投入生产。(然而我是一个React党,不得不说在html上,React JSX的表达力要远超ng,不过ng2里解决了非常多的ng1的槽点,对于我来说依然非常期待 :)

至于其他感想么 —— UI项目真是难写,终于可以写点别的玩了。 :)

另外,如果哪位有幸读到这篇博客,对这个库又看得过眼,烦请不要吝啬star,在此先行谢过了。Fermi-UI

About the author
comments powered by Disqus