写在前面的话

蚂蚁金服体验团队为社区贡献了Ant.Design这样优秀的UI框架,官方主要以React实现,同时社区也涌现了很以React、Vue等框架为基础的实现,唯独缺少Angular的实现。那么开始尝试造轮子,把Ant.Design移植到Angular上来。本文从零开始,把踩过的坑都记录下来。

本文不是教程,主要记录学习的过程,如有错误或者更好的实现,烦请慷慨赐教。

本文目前用到的主要技术框架:

下文所提及的Angular均以最新版本为准

准备工作

QuickStart这个项目种子包含了Angular、Typescript,转译工具是tsc,构建工具是Systemjs。

项目以官方文档里面推荐的QuickStart开始,先从GitHub上Clone下来:

git clone https://github.com/angular/quickstart.git quickstart
cd quickstart
npm install
npm start

项目已经可以正常运行了,但是因为Clone下来的版本有许多不需要的东西,我们先按照文档里的操作删除原有的git版本库:

rm -rf .git  # OS/X (bash)
rd .git /S/Q # windows

然后删除项目里的不必要文件(non-essential files )

似乎删除了测试文件

OS/X (bash)

xargs rm -rf < non-essential-files.osx.txt
rm src/app/*.spec*.ts
rm non-essential-files.osx.txt

Windows

for /f %i in (non-essential-files.txt) do del %i /F /S /Q
rd .git /s /q
rd e2e /s /q

这里可以把项目备份一份,以便后面的项目继续使用。

接着修改项目名字和相应的package信息,我们改成button相关的。
之后初始化我们自己的git版本:

git init

由于是写模块,所以我们在项目根目录下新建一个example的文件夹,将src下的文件统统移到example上,同时需要修改package上的脚本信息,将所有指向src文件的命令统统指向example,然后运行命令:

npm start

文件正常编译并启动。

第一个模块 Button

Angular中UI组件一般以特性模块的方式出现,

我们在src文件下新建几个文件:

- index.ts  //用于编写Button Module
- button.ts  //用于编写Button Component
- button.spec.ts //用于编写Button 测试用例(karam + jasmine),之后完善
- button.html //用于编写模板
- style/button.css //用于编写样式

编写Module

src/index.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AsButton } from './button'

@NgModule({
    imports: [ CommonModule ],
    exports: [ AsButton],
    declarations: [ AsButton ]
})

export class AsButtonModule {}

这里将模块命名为AsButtonModule,从button文件引入AsButton组件并在Module文件声明和导出。
核心和通用模块作用和用法参考官方文档。

编写Component文件

src/button.ts
import { Component, Input, Output, EventEmitter, SimpleChange, ViewChild, ElementRef } from '@angular/core'
import * as classNames  from 'classnames'

export type ButtonType = 'primary' | 'ghost' | 'dashed' | 'danger';
export type ButtonShape = 'circle' | 'circle-outline';
export type ButtonSize = 'small' | 'large';

@Component({
    moduleId: module.id,
    selector: 'as-button',
    templateUrl: 'button.html',
    styleUrls: ['style/button.css']
})
export class AsButton {
    private classes: any;
    private _loading: boolean;
    private clicked: boolean;
    private oldClicked: boolean;
    timeout: any;
    delayTimeout: any;
    // 接口声明
    @Input()
    type: string

    @Input()
    htmlType: string

    @Input()
    icon: string

    @Input()
    shape: ButtonShape

    @Input()
    prefixCls: string

    @Input()
    size: ButtonSize

    @Input()
    loading: boolean

    @Input()
    ghost: boolean

    @Output()
    onClick = new EventEmitter<Event>();

    @Output()
    onMouseUp = new EventEmitter<Event>();

    @ViewChild('AsButton') 
    button: ElementRef;

    constructor(){
        this.prefixCls = "as-btn";
        this.clicked = false;
        this.ghost = false;
        this._loading = false;
    }
    // 初始化class样式
    ngOnInit(){
        this.updateClass()
    }
    // loading状态更新
    ngOnChange(changes: {[propKey: string]: SimpleChange}){
        const currentLoading = this.loading
        const loading = changes["loading"];

        if (currentLoading) {
            clearTimeout(this.delayTimeout);
        }

        if (loading){
            this.delayTimeout = setTimeout(() => {
                this._loading = !!loading
            })
        } else {
            this._loading = !!loading
        }
    }

    ngDoCheck() {
        // 检测如果this.clicked的状态改变,则触发class更新
        if (this.clicked !== this.oldClicked) {
            this.updateClass()
            this.oldClicked = this.clicked
        }
    }
    
    /**
     * 绑定点击事件
     */
    handleClick = (e: Event) => {
        this.clicked = true;
        clearTimeout(this.timeout);
        // 防止点击过快,延迟500毫秒
        this.timeout = setTimeout(() => this.clicked = false, 500);
        // 如果父级组件绑定了点击事件,则执行
        const onClick = this.onClick;
        if (onClick) {
            onClick.emit(e)
        }
    }
    handleMouseUp = (e: Event) => {
        this.button.nativeElement.blur();
        if (this.onMouseUp) {
            this.onMouseUp.emit(e)
        }
    }
    // 更新Class的方法
    updateClass = () =>{
        const { 
            type,
            htmlType,
            icon,
            shape,
            prefixCls,
            size,
            ghost
        } = this

        const sizeCls =  ({
            large: 'lg',
            small: 'sm',
        })[size] || '';
        
        this.classes = classNames(prefixCls, {
            [`${prefixCls}-${type}`]: !!type,
            [`${prefixCls}-${shape}`]: !!shape,
            [`${prefixCls}-${sizeCls}`]: !!sizeCls,
            // [`${prefixCls}-icon-only`]: !children && icon, 
            [`${prefixCls}-loading`]: !!this._loading,
            [`${prefixCls}-clicked`]: !!this.clicked,
            [`${prefixCls}-background-ghost`]: !!ghost,
        })
    }
}

因为是移植的关系,组件的API和代码实现基本参考原版的Ant.Design。

编写HTML模板

src/button.html
<button #AsButton 
        [class]="classes"
        [type]="htmlType || 'button'"
        (click)="handleClick($event)" 
        (mouseup)="handleMouseUp()">
    <span>
        <ng-content></ng-content>
    </span>
</button>

编写CSS样式文件

src/style/button.less to .css

参考Ant.Design的Button样式

导入到Example

我们需要看看组件效果是否达到了预期。

修改App Module

example/app/app.module.ts
import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent }  from './app.component';
import { AsButtonModule } from '../../src/index';


@NgModule({
  imports:      [ BrowserModule, AsButtonModule ],
  declarations: [ AppComponent ],
  bootstrap:    [ AppComponent ]
})
export class AppModule { }

修改App Component

example/app/app.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
})
export class AppComponent  {
    loading = false;
    handleOK(){
      console.log("click")
    };
}

修改App Component Html

这里的html文件原版是没有的,我们来新建一个:

App.component.html
<div>
    <as-button type="primary" >Primary</as-button>
    <as-button>Default</as-button>
    <as-button type="dashed">Dashed</as-button>
    <as-button type="danger" (onClick)="handleOK($event)">Danger</as-button>
</div>

如果此时运行了npm start,命令行应该提示无法找到src下面的文件,报404。
这里使用了Systemjs和Browsersync来同步项目,Systemjs没有正取识别我们的引用文件,我们找到项目根目录下的ts-config.json文件进行修改,添加一行:

{
  "server": {
    "baseDir": "examples",
    "routes": {
      "/node_modules": "node_modules",
+     "/src": "src"
    }
  }
}

这样,Systemjs就能正确的将src下面编译好的js文件导入到浏览器中了。

预期中的效果

图片描述

本文涉及代码:https://github.com/ng-compone...


猫切
390 声望23 粉丝

这是很长、很好的一生