用Typescript编写AngularJS应用是怎样一种感受

更新于 2016-05-14  约 25 分钟

Typescript是一门拥有可选静态类型系统、基于类的编译型语言。这话如果你觉着怪,那尝试这么理解一下,她是JavaScript的超集,也就是说,理论上她支持JavaScript的所有特性,然后又提供了额外的优势。

举几个小栗子说明其优势:

范型支持

图片描述

错误提示

图片描述

这类快速提示,在原生的JavaScript中几乎是无法想象的,如果你的眼力不够好,那问题只能等到运行时再发现了,浪费时间、浪费精力。

那既然Typescript这么屌,而且angular2就推荐使用Typescript编写应用,那AngularJS能不能也用Typescript编写应用?编写的过程又是怎样一种感受!?答案是肯定的,否则我我在这干什么^^。但使用起来是怎样的感受,请允许我先上一张动图表达我的心情:

图片描述

处处有提示,纠错不是梦!有没有很风骚?

图片描述

但在一切开始之前,先让我强烈建议各位安装由Github主持开发的超强编辑器atom,再配合她的atom-typescript插件,绝对亮瞎曾经身为JSer的你!!!

OK,和之前用ES6编写AngularJS程序是怎样一种体验一样,我们也代码未动,工具先行,谁让我们前端界又被称为“带薪搭环境界”,环境,现在成了每一个入行前端朋友的梦魇!那么一个快速、简洁的、跟得上时代的脚手架就显得尤为重要,于是我要介绍的是generator-ts-angular

安装yo(如果你还没安过的话)

npm install -g yo

请注意前缀sudo,如果你使用的是unix like操作系统的话

安装generator-ts-angular

npm install -g generator-ts-angular

请注意前缀sudo,如果你使用的是unix like操作系统的话

使用generator-ts-angular创建项目

先找个你喜欢的目录,然后运行下面的命令,因为一会新项目会直接创建在该目录下。

yo ts-angular

上面命令回车后,生成器会问你如下问题,老实作答即可(注意: 对单页应用没经验的孩纸,在Use html5 model这个问题上,请选择No; 当问你Which registry would you use?时,国内用户选择第一个淘宝镜像安装速度会快很多)

图片描述

当命令执行完毕后,你就能在当前目录下看到刚才创建的项目了,本例中我使用的project namengType

开启调试之旅

#进入刚创建的项目目录
cd ngType
#启动调试服务
npm start

然后你就可以在http://localhost:8080下,看到刚创建的项目的运行效果了:

图片描述

项目简介

骨架结构

ngType
├── css
├── etc
├── img
├── ts
│   ├── features
│   │   ├──common
│   │   │   ├── directive
│   │   │   └── listener
│   │   └── todos
│   │       ├── controller
│   │       ├── model
│   │       ├── partials
│   │       └── service
│   ├── fw
│   │   ├── config
│   │   ├── ext
│   │   ├── init
│   │   ├── lib
│   │   └── service
│   │
│   └── typings
│       ├── angularjs
│       ├── es6-collections
│       ├── es6-promise
│       └── jquery
│
├── index.html_vm
├── package.json
├── require.d.ts
├── tsconfig.json
├── tsd.json
├── webpack.config.dev.js
├── webpack.config.prod.js
  • css, 这个不用多说吧,里面有个main.css,自己随便改改看嘛。我这里没有默认引入less或者sass理由非常简单,留给开发人员选择自己喜爱的工具

  • etc, 一些公共配置性内容,可以放在这里,方便查找、通用

  • img, 用我多说么?放图片的啦

  • ts, 分为featuresfw两大部分。这个内容略多,我后面详述吧。

  • typings, 这里放着那些非Typescript编写的库、原始类型的Declaration Files,没有这个,Typescript的静态分析工具就没办法提供那些强大提示和检查了。

  • index.html_vm, 单页应用html模版,最终的html会由webpack根据这个模版生成

  • package.json, 项目的npm描述文件,那些具体的工具命令(譬如刚才用过的npm start,都在这里面定义好了)

  • require.d.ts, 这个Declaration File还是由于webpack的缘故,详情看这里

  • tsconfig.json, 这个是Typescript需要的配置文件,里面包含的编译后的ECMAScript版本,已经模块规范...

  • tsd.json, 这里定义了本项目需要哪些额外的Declaration Files,根据这个文件下载的定义文件,就放在前面提到的typings目录里

  • webpack.config.dev.js, 开发、调试环境使用的webpack配置

  • webpack.config.prod.js, 正式运行环境使用的webpack配置。npm run release命令会用这个配置,生成的结果都会给文件名加hashjavascript文件也会压缩。

可用工具介绍

  • npm start, 启动调试服务器,使用webpack.config.dev.js作为webpack配置,不直接生成物理文件,直接内存级别响应调试服务器资源请求。而且内置hot reload,不用重启服务,修改源码,浏览器即可刷新看到新效果

  • npm run release, 使用webpack.config.prod.js作为webpack配置,生成压缩、去缓存化的bundle文件到ngType/build目录下。也就是说,如果你要发布到生产环境或者其它什么测试环境,你应该提供的是ngType/build目录下生成的那堆东西,而不是源码。

  • npm run dev, 使用webpack.config.dev.js作为webpack配置,生成物理文件。

ts目录介绍

features

common

那些通用的逻辑、UI组件可以通通放在这里,譬如为了演示方便,我已经在features/common/directive里写了一个Autofocus.ts的指令。

//引入基类
import FeatureBase from '../../../fw/lib/FeatureBase';

class Autofocus extends FeatureBase {

    constructor() {
       //设置名称,会在ts/main.ts里的findDependencies中用到
        super('AutofocusModule');
    }

    _autoFocus() {
        return {
            restrict: 'A',
            //注意看,有了类型,当你"点"的时候,有提示出现哦
            link: function($scope: angular.IScope, element: angular.IAugmentedJQuery) {
                element[0].focus();
            }
        };
    }

    execute() {
        //注册该指令到当前feature
        this.directive('autofocus', this._autoFocus);
    }
}

//默认导出即可
export default Autofocus;

todos

这是一个单纯的演示feature,里面的内容我们后面详解

fw

这里面都是些所谓"框架"级别的设置,有兴趣的话挨个儿打开瞧瞧嘛,没什么大不了的。

特别注意,大部分时候,你的开发都应该围绕features目录展开,之所以叫fw,就是和具体业务无关,除非你需要修改框架启动逻辑,路由控制系统。。。,否则不需要动这里的内容

源码介绍

ts/index.ts

入口文件

/**
 * 
 * 这里连用两个ensure,是webpack的特性,可以强制在bundle时将内容拆成两个部分
 * 然后两个部分还并行加载
 *
 */

//第一个部分是一个很小的spinner,在并行加载两个chunk时,这个非常小的部分90%会竞速成功
//于是你就看到了传说中的loading动画
require.ensure(['splash-screen/dist/splash.min.css', 'splash-screen'], function(require) {
    //这里的强转any类型,是因为我使用的功能是webpack的特性,所以Typescript并不知道
    //所以要强制忽略Typescript的提示
    (<any>require('splash-screen/dist/splash.min.css')).use();
    (<any>require('splash-screen')).Splash.enable('circular');
});

//由于这里是真正的业务,代码多了太多,所以体积也更大,加载也更慢,于是在这个chunk加载完成前
//有个美好的loading动画,要比生硬的白屏更优雅。
//放心,这个chunk加载完后,loading动画也会被销毁
require.ensure(['../css/main.css', './main'], function(require) {

    (<any>require('../css/main.css')).use();
    //这里启动了真正的“框架”
    var App = (<any>require('./main')).default;
    (new App()).run();
});

ts/main.ts

“框架”启动器

//引入依赖部分
import * as ng from 'angular';
import Initializers from './fw/init/main';
import Extensions from './fw/ext/main';
import Configurators from './fw/config/main';
import Services from './fw/service/main';
import Features from './features/main';
import {Splash} from 'splash-screen';

import FeatureBase from './fw/lib/FeatureBase';

class App {

    //声明成员变量及其类型
    appName: string;
    features: Array<FeatureBase>;
    depends: Array<string>;
    app: angular.IModule;

    constructor() {
        //这里相当于ng-app的名字
        this.appName = 'ngType';
        //实例化所有features
        Features.forEach(function(Feature) {
            this.push(new Feature());
        }, this.features = []);
    }

    //从features实例中提取AngularJS module name
    //并将这些name作为ngType的依赖
    //会在下面createApp时用到
    findDependencies() {
        this.depends = Extensions.slice(0);

        var featureNames = this.features
            .filter(feature => !!feature.export)
            .map(feature => feature.export);

        this.depends.push(...featureNames);
    }

    //激活初始化器,个别操作希望在AngularJS app启动前完成
    beforeStart() {
        Initializers.forEach((Initializer) => (new Initializer(this.features)).execute());

        this.features.forEach(feature => feature.beforeStart());
    }

    //创建ngType应用实例
    createApp() {
        this.features.forEach(feature => feature.execute());

        this.app = ng.module(this.appName, this.depends);
    }

    //配置ngType
    configApp() {
        Configurators.forEach((Configurator) => (new Configurator(this.features, this.app)).execute());
    }

    //注册fw下的“框架”级service
    registerService() {
        Services.forEach((Service) => (new Service(this.features, this.app)).execute());
    }

    //看到了么,这里我会销毁loading动画,并做了容错
    //也就是说,即便你遇到了那微乎其微的状况,loading动画比业务的chunk加载还慢
    //我也会默默的把它收拾掉的
    destroySplash(): void {
        var _this = this;
        Splash.destroy();
        (<any>require('splash-screen/dist/splash.min.css')).unuse();
        setTimeout(function() {
            if (Splash.isRunning()) {
                _this.destroySplash();
            }
        }, 100);
    }

    //启动AngularJS app
    launch() {
        ng.bootstrap(document, [this.appName], { strictDi: true });
    }

    run(): void {
        this.findDependencies();
        this.beforeStart();
        this.createApp();
        this.configApp();
        this.registerService();
        this.destroySplash();
        this.launch();
    }
}

export default App;

用Typescript写Feature

ts/features/todos/main.ts

//一个feature的main.ts负责管理该feature所用到的所有模块
import FeatureBase from '../../fw/lib/FeatureBase';
//引入路由定义
import Routes from './Routes';
//引入controller定义,和service定义
import TodosController from './controller/TodosController';
import TodosService from './service/TodosService';

class Feature extends FeatureBase {

    constructor() {
        //指定feature名字
        super('todos');
        //设置路由
        this.routes = Routes;
    }

    execute() {
        //注册controler到本feature
        this.controller('TodosController', TodosController);
        //注册service到本feature
        this.service('TodosService', TodosService);
    }
}

//导出本feature,别担心,再上一级调用方会正确处理的
export default Feature;

用Typescript写路由

简单到没朋友

//引入路由对应的模版,还是因为webpack,将模版
//作为字符串引入,就是这么easy
//还是因为webpack特性的缘故,这里只能通过强转的形式让IDE忽略检查
var tpl = (<string>require('./partials/todos.html'));

import Route from '../../fw/lib/Route';

const routes: Route[] = [{
    id: 'todos',
    isDefault: true,
    when: '/todos',
    controller: 'TodosController',
    controllerAs: 'todos',
    template: tpl
}];

export default routes;

这里路由的内容你可以写错试试看,会有错误提示哦!

用Typescript写Controller

import * as angular from 'angular';
import InternalService from '../../../fw/service/InternalService';
import TodosService from '../service/TodosService';
import Todo from '../model/Todo';

//定义一个表示状态的类型,可以约束输入哦!
interface IStatusFilter {
    completed?: boolean
}

//自定义TodosScope,因为Scope本身没有todolist属性
//需要自定义添加
interface ITodosScope extends angular.IScope {
    todolist?: Array<Todo>;
}

class TodosController {

    //声明成员,并赋予初始值
    todolist: Array<Todo> = [];
    statusFilter: IStatusFilter = {};
    remainingCount: number = 0;
    filter: string = '';
    editedTodo: Todo;
    newTodo: string = '';

    //屌炸天的ng-annotate插件,妈妈再也不用担心我手写什么inline annotation, 或者$inject属性了
    
    /*@ngInject*/
    constructor(public $scope: ITodosScope, public TodosService: TodosService, public utils: InternalService) {
        this._init_();
        this._destroy_();
    }

    _init_() {
        this.$scope.todolist = this.todolist;
        //从service中获取初始值,并放入this.todolist
        this.TodosService
            .getInitTodos()
            .then(data => { this.todolist.push(...data); });

        //监视todolist的变化
        this.$scope.$watch('todolist', this.onTodosChanged.bind(this), true);
    }

    onTodosChanged() {
        this.remainingCount = this.todolist.filter((todo) => !todo.completed).length;
    }

    addTodo() {
        this.todolist.push({
            title: this.newTodo,
            completed: false
        });
        this.newTodo = '';
    }

    editTodo(todo) {
        this.editedTodo = todo;
    }

    doneEditing(todo: Todo) {
        this.editedTodo = undefined;
        if (!todo.title.trim()) {
            this.removeTodo(todo);
        }
    }

    removeTodo(todo: Todo) {
        this.$scope.todolist = this.todolist = this.todolist.filter((t) => t !== todo);
    }

    markAll(checked: boolean) {
        this.todolist.forEach(todo => todo.completed = checked);
    }

    toggleFilter(e: MouseEvent, filter: string) {
        this.utils.stopEvent(e);
        this.filter = filter;
        this.statusFilter = !filter ? {} : filter === 'active' ? { completed: false } : { completed: true };
    }

    clearDoneTodos() {
        this.$scope.todolist = this.todolist = this.todolist.filter((todo) => !todo.completed);
    }

    _destroy_() {
        this.$scope.$on('$destroy', () => { });
    }
}

export default TodosController;

最后,你可能还有其它问题,直接来看看这里,或者Github上给我提issue也未尝不可呢

阅读 11k更新于 2016-05-14

推荐阅读
leftstick
用户专栏

沙滩一卧两年半,今日浪打我翻身

82 人关注
27 篇文章
专栏主页
目录