用Typescript編寫AngularJS應用是怎樣一種感覺

Typescript是一門擁有可選靜態類型系統、基於類的編譯型語言。這話若是你覺着怪,那嘗試這麼理解一下,她是JavaScript的超集,也就是說,理論上她支持JavaScript的全部特性,而後又提供了額外的優點。javascript

舉幾個小栗子說明其優點:css

範型支持html

圖片描述

錯誤提示前端

圖片描述

這類快速提示,在原生的JavaScript中幾乎是沒法想象的,若是你的眼力不夠好,那問題只能等到運行時再發現了,浪費時間、浪費精力。html5

那既然Typescript這麼屌,並且angular2就推薦使用Typescript編寫應用,那AngularJS能不能也用Typescript編寫應用?編寫的過程又是怎樣一種感覺!?答案是確定的,不然我我在這幹什麼^^。但使用起來是怎樣的感覺,請容許我先上一張動圖表達個人心情:java

圖片描述

到處有提示,糾錯不是夢!有沒有很風騷?jquery

圖片描述

但在一切開始以前,先讓我強烈建議各位安裝由Github主持開發的超強編輯器atom,再配合她的atom-typescript插件,絕對亮瞎曾經身爲JSer的你!!!webpack

OK,和以前用ES6編寫AngularJS程序是怎樣一種體驗同樣,咱們也代碼未動,工具先行,誰讓咱們前端界又被稱爲「帶薪搭環境界」,環境,如今成了每個入行前端朋友的夢魘!那麼一個快速、簡潔的、跟得上時代的腳手架就顯得尤其重要,因而我要介紹的是generator-ts-angulargit

安裝yo(若是你還沒安過的話)

npm install -g yo

請注意前綴sudo,若是你使用的是unix like操做系統的話angularjs

安裝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也何嘗不可呢

相關文章
相關標籤/搜索