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 name
是ngType
。
开启调试之旅
#进入刚创建的项目目录 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
,分为features
和fw
两大部分。这个内容略多,我后面详述吧。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
命令会用这个配置,生成的结果都会给文件名加hash
,javascript
文件也会压缩。
可用工具介绍
npm start
,启动调试服务器,使用webpack.config.dev.js
作为webpack
配置,不直接生成物理文件,直接内存级别响应调试服务器资源请求。而且内置hot reload
,不用重启服务,修改源码,浏览器即可刷新看到新效果npm run release
,使用webpack.config.prod.js
作为webpack
配置,生成压缩、去缓存化的bundle
文件到ngType/build
目录下。也就是说,如果你要发布到生产环境或者其它什么测试环境,你应该提供的是ngType/build
目录下生成的那堆东西,而不是源码。
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;