├── .gitignore ├── .prettierrc ├── README.md ├── assets ├── step1.jpg └── step2.jpg ├── index.js ├── output └── 30410212 │ ├── 52459895.html │ ├── 52459896.html │ ├── 52459897.html │ ├── 52459898.html │ ├── 52459899.html │ ├── 52459900.html │ ├── 52459901.html │ ├── 52459902.html │ ├── 52459903.html │ ├── 52459904.html │ ├── 52459905.html │ ├── 52459906.html │ ├── 52459907.html │ ├── 52459908.html │ ├── 52459909.html │ ├── 52459910.html │ ├── 52459911.html │ ├── 52459912.html │ ├── 52459913.html │ ├── 52459914.html │ ├── 52459915.html │ ├── 52459916.html │ ├── 52459917.html │ ├── 52459918.html │ ├── 52459919.html │ ├── 52459920.html │ ├── 52459921.html │ ├── 52459922.html │ ├── 52459923.html │ ├── 52459924.html │ ├── 52459925.html │ ├── 52459926.html │ ├── 52459927.html │ ├── 52459928.html │ ├── 52459929.html │ ├── 52459930.html │ ├── 52459931.html │ ├── 52459932.html │ ├── 52459933.html │ ├── 52459934.html │ ├── 52459935.html │ ├── 52459936.html │ ├── 52459937.html │ ├── 52459938.html │ ├── 52459939.html │ ├── 52459940.html │ ├── 52459941.html │ ├── 52459942.html │ ├── 52459943.html │ ├── 52459944.html │ ├── 52459945.html │ ├── 52459946.html │ ├── 52459947.html │ ├── 52459948.html │ ├── 52459949.html │ ├── 52459950.html │ ├── 52459951.html │ ├── 52459952.html │ ├── 52459953.html │ ├── 52459954.html │ ├── 52459955.html │ ├── 52459956.html │ ├── 52459957.html │ ├── 52459958.html │ ├── 52459959.html │ ├── 52459960.html │ ├── 52459961.html │ ├── 52459962.html │ ├── 52459963.html │ ├── 52459964.html │ ├── 52459965.html │ ├── 52459966.html │ ├── 52459967.html │ ├── 52459968.html │ ├── 52459969.html │ ├── 52459970.html │ ├── 52459971.html │ ├── 52459972.html │ ├── 52459973.html │ ├── 52459974.html │ ├── 52459975.html │ ├── 52459976.html │ ├── 52459977.html │ ├── 52459978.html │ ├── 52459979.html │ ├── 52459980.html │ ├── 52459981.html │ ├── 52459982.html │ ├── 52459983.html │ ├── 52459984.html │ ├── 52459985.html │ ├── 52459986.html │ ├── 52459987.html │ ├── 52459988.html │ ├── 52459989.html │ ├── 52459990.html │ ├── 52459991.html │ ├── 52459992.html │ ├── 52459993.html │ ├── 52459994.html │ ├── 52459995.html │ ├── 52459996.html │ ├── 52459997.html │ ├── 52459998.html │ ├── 52459999.html │ ├── 52460000.html │ ├── 52460001.html │ ├── 52460002.html │ ├── 52460003.html │ ├── 52460004.html │ ├── 52460005.html │ ├── 52460006.html │ ├── 52460007.html │ ├── 52460008.html │ ├── 52460009.html │ ├── 52460010.html │ ├── 52460011.html │ ├── 52460012.html │ ├── 52460013.html │ ├── 52460014.html │ ├── 52460015.html │ ├── 52460016.html │ ├── 52460017.html │ ├── 52460018.html │ ├── 52460019.html │ ├── 52460020.html │ ├── 52460021.html │ ├── 52460022.html │ ├── 52460023.html │ ├── 52460024.html │ ├── 52460025.html │ ├── 52460026.html │ ├── 52460027.html │ ├── 52460028.html │ ├── 52460029.html │ ├── 52460030.html │ ├── 52460031.html │ ├── 52460032.html │ ├── 52460033.html │ ├── 52460034.html │ ├── 52460035.html │ ├── 52460036.html │ ├── 52460037.html │ ├── 52460038.html │ ├── 52460039.html │ ├── 52460040.html │ ├── 52460041.html │ ├── 52460042.html │ ├── 52460043.html │ ├── 52460044.html │ ├── 52460045.html │ ├── 52460046.html │ ├── 52460047.html │ ├── 52460048.html │ ├── 52460049.html │ ├── 52460050.html │ ├── 52460051.html │ ├── 52460052.html │ ├── 52460053.html │ ├── 52460054.html │ ├── 52460055.html │ ├── 52460056.html │ ├── 52460057.html │ ├── 52460058.html │ ├── 52460059.html │ ├── 52460060.html │ ├── 52460061.html │ ├── 52460062.html │ ├── 52460063.html │ └── index.html ├── package-lock.json ├── package.json └── util.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 一键导出京东阅读已购买的电子书, [导出格式示例](./output/30410212/index.html), [相关博文](http://poppython.com/blog/export_jd_read.html) 2 | 3 | ## 如何使用 4 | step0: 下载代码, 并安装依赖 5 | 6 | ```bash 7 | git clone https://github.com/rmlzy/export_jd_read.git 8 | cd hack_jd_read 9 | npm install 10 | ``` 11 | 12 | step1: 13 | 登录[京东读书](https://e.jd.com/), 拷贝页面 cookie 中的 thor 值, 到 `index.js` 的 `thor` 变量里 14 | 15 |  16 | 17 | step2: 在我的已购电子书页面, 点击 "在线阅读" 按钮 18 |  19 | 20 | step3: 记住页面 URL 地址中的 `bookId` 和 `readType` 参数, 例如: `https://cread.jd.com/read/startRead.action?bookId=30506710&readType=3` 21 | 22 | step4: 执行 `npm run start`, 并输入 step3 的 `bookId` 和 `readType` 值即可 23 | 24 | 程序会自动将导出的章节存储到 `output` 目录. 25 | 26 | ## 使用说明 27 | + 此脚本不会保存或者上传你的 Cookie; 28 | + 如有侵权请联系我删除! 29 | 30 | ## LICENSE 31 | WTFPL 32 | -------------------------------------------------------------------------------- /assets/step1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goodwjf/export_jd_read/bf62d8e66c165ced6592cf97d8b2943e3fadac0a/assets/step1.jpg -------------------------------------------------------------------------------- /assets/step2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goodwjf/export_jd_read/bf62d8e66c165ced6592cf97d8b2943e3fadac0a/assets/step2.jpg -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const fetch = require("node-fetch"); 2 | const path = require("path"); 3 | const fs = require("fs-extra"); 4 | const inquirer = require("inquirer"); 5 | const FormatJson = require("./util").FormatJson; 6 | const genEpubUrl = require("./util").genEpubUrl; 7 | const genCatalogUrl = require("./util").genCatalogUrl; 8 | 9 | // !!! thor 的值粘贴到下边的变量里 !!! 10 | let thor = ""; 11 | // !!!!!! 12 | let bookId = ""; 13 | let readType = ""; 14 | 15 | const fetchAndSave = async (chapterId) => { 16 | const url = genEpubUrl("/read/gC.action", bookId, chapterId, readType); 17 | const res = await fetch(`https://cread.jd.com${url}`, { 18 | headers: { 19 | accept: "*/*", 20 | "accept-language": "zh-CN,zh;q=0.9,en;q=0.8", 21 | "cache-control": "no-cache", 22 | pragma: "no-cache", 23 | "sec-fetch-dest": "empty", 24 | "sec-fetch-mode": "cors", 25 | "sec-fetch-site": "same-origin", 26 | "x-requested-with": "XMLHttpRequest", 27 | cookie: `thor=${thor};`, 28 | }, 29 | referrer: 30 | "https://cread.jd.com/read/startRead.action?bookId=30334444&readType=1", 31 | referrerPolicy: "strict-origin-when-cross-origin", 32 | body: null, 33 | method: "GET", 34 | mode: "cors", 35 | }); 36 | const json = await res.json(); 37 | if (json.code !== "0") { 38 | console.log("❌ 抓取出错: ", json.msg); 39 | return; 40 | } 41 | console.log("✅ : chapterId", chapterId); 42 | const real = FormatJson.formatContent(json.content); 43 | const html = real.contentList[0].content; 44 | await fs.outputFile( 45 | path.join(__dirname, "output", bookId, `${chapterId}.html`), 46 | html 47 | ); 48 | }; 49 | 50 | const fetchCatalog = async () => { 51 | const url = genCatalogUrl("/read/lC.action", bookId, readType); 52 | const res = await fetch(`https://cread.jd.com${url}`, { 53 | headers: { 54 | accept: "*/*", 55 | "accept-language": "zh-CN,zh;q=0.9,en;q=0.8", 56 | "cache-control": "no-cache", 57 | pragma: "no-cache", 58 | "sec-fetch-dest": "empty", 59 | "sec-fetch-mode": "cors", 60 | "sec-fetch-site": "same-origin", 61 | "x-requested-with": "XMLHttpRequest", 62 | cookie: `thor=${thor};`, 63 | }, 64 | referrer: 65 | "https://cread.jd.com/read/startRead.action?bookId=30410212&readType=3", 66 | referrerPolicy: "strict-origin-when-cross-origin", 67 | body: null, 68 | method: "GET", 69 | mode: "cors", 70 | }); 71 | const json = await res.json(); 72 | if (json.code !== "0") { 73 | console.log("❌ 抓取出错: ", json.msg); 74 | return; 75 | } 76 | const real = FormatJson.formatContent(json.content); 77 | return real.catalogList; 78 | }; 79 | 80 | const genIndexHtml = async (catalog) => { 81 | const lis = []; 82 | catalog.forEach((item) => { 83 | if (item.level === 0) { 84 | lis.push( 85 | `
版权信息
书名:Angular权威教程
作者:(美)Ari Lerner等
译者:Nice Angular社区
ISBN:9787115451583
本书由人民邮电出版社授权京东读书电子版制作与发行
版权所有 · 侵权必究
-------------------------------------------------------------------------------- /output/30410212/52459897.html: -------------------------------------------------------------------------------- 1 |推荐序
很高兴这本《Angular权威教程》成为Angular中文资源的一部分,希望它能广受欢迎,给中国的Angular社区提供一份令人愉悦的学习资源,也希望它帮助更多工程师开始使用下一代Angular框架来开发应用。
我认识雪狼和他所属的Nice Angular社区是在2016年。那时候,他们开始了对Angular官方网站卓越的本地化工作。现在,这份中文官网已经部署在了angular.cn上。
本书及其翻译工作充分体现了中国开源软件开发者的热情和共享精神。感谢雪狼等来自Nice Angular社区的志愿者们对此作出的贡献。愿本书帮助你开始试用Angular!祝你成功!
Naomi Black,Google Angular项目经理兼主管
作为一项开源技术和前沿Web开发框架,Angular持续吸引着中国区开发人员的关注。作为雪狼及其所属Nice Angular社区的集体工作成果,这本书是开源力量的又一次证明,证明这种热情、这种志愿精神确实可以帮助业界享受到全球最新的开发技术。我谨代表Google开发技术推广部向这本书的出版表示祝贺。
栾跃,Google开发技术推广部大中华区主管
-------------------------------------------------------------------------------- /output/30410212/52459898.html: -------------------------------------------------------------------------------- 1 |译者序
简介
以笔者之所见,《Angular权威教程》大概是目前除了Angular官方文档之外最全面的学习资料了,这从其英文版多达600多页的篇幅就可见一斑。相应地,它面对的对象涵盖了从入门级到中高级的读者,是一本可以陪伴你成长的好书。
在内容安排上,本书具有大量的例子以保障其足够浅显,但也穿插着一些原理分析以保障其足够深入。除此之外,本书还给出了很多外部参考资料,让富有探险精神的你可以向专家级进发。
翻译说明
未来的版本号及发布计划
Angular就要出4.0了!是的,过一阵子还有Angular 5/6/7/8……这本书会很快过时吗?答案是“不会”。Angular开发组对于未来的版本号及发布计划有一个正式的说明,大意是:
我们要兼顾向后兼容和向前演进,因此以后我们将严格遵循SemVer语义化版本规范,并力求让版本升级变得可预测,以便使用者可以提前安排。在大版本号之间会出现少量破坏性变更,但是不用担心,相邻的大版本号之间只会把一些API标记为废弃的。也就是说,理想情况下,4的程序是可以直接迁移到5的,只是会收到一些API废弃提示,到6中才会彻底移除。同时,官方会在文档中给出详细的升级指南,帮助开发者升级。
因此,尽请放心,Angular以后绝不会出现像从1升级到2这么大的变化。事实上,NodeJS现在采用的就是类似的版本策略,提高发布的可预测性对于工程化开发是很有价值的。
另外,这里为什么没有3?简单点说就是因为路由模块比其他模块多发布过一次,因此当你使用core模块的2.0时,和它配套的router模块却是3.0的,这容易让开发人员困惑,跳过3,可以让所有模块的编号重新对齐。
对框架名称的说明
Angular开发组正式确定了新的命名策略:用AngularJS来代表1.x版本,而Angular代表2.x、4.x、5.x等很多后续版本,因为Angular 2+将支持TypeScript/JavaScript/Dart,而不再是JavaScript。这些变化已经在官方文档中体现出来了,而本书也将同样遵循这样的命名策略。
名词:装饰器与注解
@Component等语法元素在TypeScript中被称为装饰器(decorator),但在本书中,作者统一称其为注解(annotation)。这两种提法都是正确的。在语法层面,@Component确实是装饰器,这是TypeScript的标准叫法;但是在语义层面,Angular中是把它作为注解使用的。两者的区别是,装饰器直接改变被装饰者的行为,而注解则提供元数据,供框架去根据这些元数据做不同的处理。在Angular目前的版本中,@Component确实只是提供了元数据。
我们在跟原作者讨论之后,决定还是跟随作者的提法来翻译。不过在日常工作中,还是建议你遵循TypeScript的提法,将其称为装饰器。
支持与勘误
如果对本书中的一些概念不太理解,请参阅Angular官方中文站angular.cn。这里有来自官方开发组的权威资料。
如果对本书有任何疑问或发现问题,请到https://github.com/nice-angular/ng-book-2提交issue。
同时,对于一些经过确认的issue,我们也会更新在勘误区。
关于我们
参与本次翻译的一共有7位成员,都是AngularJS领域的专家和Angular领域的先行者。稍后会有我们的简短介绍。
本书各章的译者和校对者如下:
除此之外,雪狼还承担了项目管理和中文统稿工作;破狼负责全书的技术准确性把关;叶志敏负责与作者沟通,并在英文理解方面进行把关。
我们的感恩
本书得以发行,首先要感谢Angular开发组及其项目经理Naomi Black。正是由于她的支持和牵线搭桥,才有了我们和图灵的这次合作。
我们还要感谢Google开发技术推广部及其大中华区主管栾跃和项目经理程路,正是由于他们的努力,让Angular在中国的推广普及工作有了正规军的加入,而本书的出版正是推广计划中的一小部分。
我们还要感谢图灵的编辑朱巍和杨琳,在整个翻译过程中,她们给了我们许多专业的指导和帮助。本书得以在迅速出版的同时保证高质量,她们的经验和把关居功甚伟。
最后,要感谢Angular中文社区。我所指的并不是由我们几个创建并管理的这些QQ群、微信群等,而是指广义的中文社区。无论你在北京还是上海,也无论你在国内还是海外;无论你是高手还是新兵,也无论你是否像我们一样是Angular的忠实粉丝,你们都是广义Angular中文社区中的一员。在我们的心中,只有一个Angular中文社区,她不被任何人拥有,也被每一个人拥有,因为她就是我们每个人。
固然,我们这几位译者都是推广Angular的志愿者与先行者,但我们真正希望看到的是一个繁荣、开放、互通的中文社区,是全球Angular社区的一部分,我们希望看到Angular的技术社区遍地开花。因此,如果你有自己的组织或影响力,请联系我们,我们愿与你携手共进,分享各种知识、渠道与资源,共同制定与推进社区发展计划。要知道,无论你将来是求职还是创业,一个繁荣的社区都会给你带来强力的支持。
一旦有了共同的愿景和开放、包容的文化,我们就能无视时空的阻隔,在天南海北守望相助,共同面对新技术的挑战与机遇。纷繁的世界、冰冷的技术与温暖的社区,共同构成了本书的出版背景。
雪狼的感恩
汪志成,网名雪狼。ThoughtWorker & Google开发者专家(GDE),拥有18年软件开发经验,崇尚简单、专业、分享,“好为人师,好为人师”;合著有《AngularJS深度剖析与最佳实践》。
首先,我要感谢我的家人,特别是我的妻子春娜。为了翻译官方文档和这本书,我失去了很多陪伴他们的时间,没有他们的支持,故事将无从开始。
其次,我要感谢ThoughtWorks,没有这样一个平台,我就无法安心钻研技术,更没有大量把新技术应用于工程实践中的机会。
最后,要特别感谢我刚刚出生的女儿,你是激励我前进的动力。闺女,看到了吗?这是老爹给你的迎新礼物。
破狼的感恩
格茸扎西,网名破狼。ThoughtWorks一线码农、架构师、咨询师;爱好读书和旅游,也常涂鸦一些技术博文;合著有《AngularJS深度剖析与最佳实践》;国内Angular最早布道者,Nice Angular社区“狼主”。
首先,要感谢我的妻子和父亲。因为他们的鼓励,我才能顺利完成本书相应章节的翻译。
其次,要感谢ThoughtWorks这个大家庭。因为在这个自组织和黑客文化环境的熏陶下,我才能潜心钻研这些技术。
最后,要感谢图灵出版社的朱巍编辑、本书的作者以及其他译者们。
叶志敏的感恩
叶志敏,虽留英多年、远漂他乡、四处奔波,一颗热爱软件开发的心却依旧如初。多年前曾与雪狼共事,合作愉快,因此成为好朋友。由雪狼推荐进入Angular世界,使用Angular和.NET平台开发软件多年。从Alpha阶段开始使用Angular。与雪狼合作,翻译Angular官方文档站,并经过Angular团队的推荐,承接翻译本书的重任。
首先感谢我的妻子。从怀孕到照顾女儿健康成长,她一直对我的工作非常理解和支持,从无怨言。其次,感谢我母亲和岳母的慈爱与帮助。最后,希望女儿能健康成长,平安一生。
Hantsy的感恩
Hantsy,拥有15年软件工程经验。2012年曾受JBoss(RedHat子公司)邀请前往波士顿参加JBoss用户和开发人员年度大会,并获得JBoss Community Recognition Awards。现为自由职业者,远程工作多年。
感谢Angular中文团队和图灵的支持,非常荣幸参与本书中文版的翻译。感谢Angular团队的努力,为我们带来如此优秀的工具框架。
张旋的感恩
张旋,PMP、ACP、NPDP,中科院计算所烟台分所集成应用中心主任。1982年生人,1996年起接触编程。正式从事软件工作行业11年。擅长项目管理、团队管理、技术体系建设。非常喜欢研究和对比各种新技术,生成适合工程使用的技术栈,并灌输到整个团队中去。
十分荣幸能成为Nice Angular社区的一员,感谢雪狼和破狼。感谢本书原作者为我们提供了一本这么好的Angular教程,也感谢本书的所有翻译者,从你们身上我确实学到了很多。感谢我的老婆莉莉,照看乐乐辛苦了,谢谢你给我时间让我做自己喜欢的事。最后感谢图灵出版社的朱巍编辑,本次合作非常愉快,期待下次更好的机会!
郑丰彧的感恩
郑丰彧,网名Z,现就职于大商集团天狗网,Angular爱好者,喜欢函数式编程、WebGL。
首先,我要感谢雪狼,一次很偶然的机会受到雪狼的邀请,让我受宠若惊,也为我开启了这次Angular翻译之旅。
其次,我要感谢我的家人,尤其是有孕在身的老婆和孕育中的宝宝。为了翻译这本书,我牺牲了很多原本用来陪伴你们的时间。
最后,我想对即将出世的女儿柚柚说句话:我们全家人对你的期待正如我们Nice Angular社区对此书的期待。所以,赶快“问世”吧!
王子实的感恩
王子实,现任光辉城市全栈工程师。1992年生,自学生时代便喜好编程,一直以来对各种新技术非常着迷,乐于对其进行研究与探索,并将成果在团队中进行推广,以提升整体效率。
非常感谢雪狼能够给我这次机会参与到本书的翻译中来,能够让我对Angular社区尽一点点自己的绵薄之力。
同时也要感谢其他参与翻译的译者们,让我有了这次非常宝贵的经验。尤其是在翻译过程中遇到一些技术问题以及对原书内容有一些疑惑时,大家探究与实践的精神让我印象深刻。
还要感谢我的妻子默默给予我支持与理解。
最后,就是要感谢Google带给我们Angular这个强大而又好用的框架。希望它也能越来越好,不断进步!
-------------------------------------------------------------------------------- /output/30410212/52459914.html: -------------------------------------------------------------------------------- 1 |Angular是用一种类似于JavaScript的语言——TypeScript——构建的。
或许你会对用新语言来开发Angular心存疑虑,但事实上,在开发Angular应用时,我们有充分的理由用TypeScript代替普通的JavaScript。
TypeScript并不是一门全新的语言,而是ES6的超集。所有的ES6代码都是完全有效且可编译的TypeScript代码。图2-1展示了它们之间的关系。
图2-1 ES5、ES6和TypeScript
什么是ES5?什么是ES6?ES5是ECMAScript 5的缩写,也被称为“普通的JavaScript”。ES5就是大家熟知的JavaScript,它能够运行在大部分浏览器上。ES6则是下一个版本的JavaScript,在后续章节中我们还会深入讨论它。
在本书出版的时候,支持ES6的浏览器还很少,更不用说TypeScript了。我们用转译器来解决这个问题。TypeScript转译器能把TypeScript代码转换为几乎所有浏览器都支持的ES5代码。
从TypeScript代码到ES5代码的唯一转换器是由TypeScript核心团队编写的。然而,将ES6代码(不是TypeScript代码)转换到ES5代码则有两个主要的转换器:Google开发的Traceur与JavaScript社区创建的Babel
。在本书中我们并不会直接使用它们,但它们也是值得了解的不错项目。
我们在上一章安装了TypeScript环境,如果你是从本章开始学习的,那么可以这样安装TypeScript环境:npm install -g typescript。
TypeScript是Microsoft和Google之间的官方合作项目。有这两家强有力的科技巨头在背后支撑,对于我们来说是个好消息,因为这表示TypeScript将会得到长期的支持。这两家公司都承诺全力推动Web技术的发展,我们这些开发人员显然会获益匪浅。
另外,转译器的好处还在于:它允许小型团队对语言进行改善,而不必要求所有人都去升级他们的浏览器。
需要指出的是:TypeScript并不是开发Angular应用的必选语言。我们同样可以使用ES5代码(即“普通”JavaScript)来开发Angular应用。Angular也为全部功能提供了ES5 API。那么为什么我们还要使用TypeScript呢?这是因为TypeScript有不少强大的功能,能极大地简化开发。
TypeScript相对于ES5有五大改善:
●类型
●类
●注解
●模块导入
●语言工具包(比如,解构)
接下来我们逐个介绍。
顾名思义,相对于ES6,TypeScript最大的改善是增加了类型系统。
有些人可能会觉得,缺乏类型检查正是JavaScript这些弱类型语言的优点。也许你对类型检查心存疑虑,但我仍然鼓励你试一试。类型检查的好处有:
(1)有助于代码的编写,因为它可以在编译期预防bug;
(2)有助于代码的阅读,因为它能清晰地表明你的意图。
另外值得一提的是,TypeScript中的类型是可选的。如果希望写一些快速代码或功能原型,可以首先省略类型,然后再随着代码日趋成熟逐渐加上类型。
TypeScript的基本类型与我们平时所写JavaScript代码中用的隐式类型一样,包括字符串、数字、布尔值等。
直到ES5,我们都在用var关键字定义变量,比如var name;。
TypeScript的新语法是从ES5自然演化而来的,仍沿用var来定义变量,但现在可以同时为变量名提供可选的变量类型了:
var name:string;
在声明函数时,也可以为函数参数和返回值指定类型:
function greetText(name:string):string {
return "Hello " + name;
}
这个例子中,我们定义了一个名为greetText的新函数,它接收一个名为name的参数。name:string语法表示函数想要的name参数是string类型。如果给该函数传一个string以外的参数,代码将无法编译通过。对我们来说,这是好事,否则这段代码将会引入bug。
或许你还注意到了,greetText函数在括号后面还有一个新语法:string {。冒号之后指定的是该函数的返回值类型,在本例中为string。这很有用,原因有二:如果不小心让函数返回了一个非string型的返回值,编译器就会告诉我们这里有错误;使用该函数的开发人员也能很清晰地知道自己将会拿到什么类型的数据。
我们来看看如果写了不符合类型声明的代码会怎样:
function hello(name:string):string {
return 12;
}
当尝试编译代码时,将会得到下列错误:
$ tsc compile-error.ts
compile-error.ts(2,12):error TS2322:Type 'number' is not assignable to type 'string'.
这是怎么回事?我们尝试返回一个number类型的12,但hello函数期望的返回值类型为string(它是在参数声明的后面以):string {的形式声明的)。
要纠正它,可以把函数的返回值类型改为number:
function hello(name:string):number {
return 12;
}
虽然这只是一个小例子,但足以证明类型检查能为我们节省大量调试bug的时间。
现在知道了如何使用类型,但怎么才能知道有哪些可用类型呢?接下来我们就会罗列出这些内置的类型,并教你如何创建自己的类型。
尝试REPL
为了运行本章中的例子,我们要先安装一个小工具,名为TSUN(TypeScript Upgraded Node,支持TypeScript的升级版Node):
$ npm install -g tsun
接着启动它:
$ tsun
TSUN:TypeScript Upgraded Node
type in TypeScript expression to evaluate
type:help for commands in repl
>
这个小小的>是一个命令提示符,表示TSUN已经准备好接收命令了。
对于本章后面的大部分例子,你都可以复制粘贴到这个终端窗口中运行。
2.4.1 字符串
字符串包含文本,声明为string类型:
var name:string = 'Felipe';
2.4.2 数字
无论整数还是浮点,任何类型的数字都属于number类型。在TypeScript中,所有的数字都是用浮点数表示的,这些数字的类型就是number:
var age:number = 36;
2.4.3 布尔类型
布尔类型(boolean)以true(真)和false(假)为值。
var married:boolean = true;
2.4.4 数组
数组用Array类型表示。然而,因为数组是一组相同数据类型的集合,所以我们还需要为数组中的条目指定一个类型。
我们可以用Array<type>或者type[]语法来为数组条目指定元素类型:
var jobs:Array<string> = ['IBM', 'Microsoft', 'Google'];
var jobs:string[] = ['Apple', 'Dell', 'HP'];
数字型数组的声明与之类似:
var jobs:Array<number> = [1, 2, 3];
var jobs:number[] = [4, 5, 6];
2.4.5 枚举
枚举是一组可命名数值的集合。比如,如果我们想拿到某人的一系列角色,可以这么写:
enum Role {Employee, Manager, Admin};
var role:Role = Role.Employee;
默认情况下,枚举类型的初始值是0。我们也可以调整初始化值的范围:
enum Role {Employee = 3, Manager, Admin};
var role:Role = Role.Employee;
在上面的代码中,Employee的初始值被设置为3而不是0。枚举中其他项的值是依次递增的,意味着Manager的值为4,Admin的值为5。同样,我们也可以单独为枚举中的每一项指定值:
enum Role {Employee = 3, Manager = 5, Admin = 7};
var role:Role = Role.Employee;
还可以从枚举的值来反查它的名称:
enum Role {Employee, Manager, Admin};
console.log('Roles:', Role[0], ',', Role[1], 'and', Role[2]);
2.4.6 任意类型
如果我们没有为变量指定类型,那它的默认类型就是any。在TypeScript中,any类型的变量能够接收任意类型的数据:
var something:any = 'as string';
something = 1;
something = [1, 2, 3];
2.4.7 “无”类型
void意味着我们不期望那里有类型。它通常用作函数的返回值,表示没有任何返回值:
function setName(name:string):void {
this.name = name;
}
JavaScript ES5采用的是基于原型的面向对象设计。这种设计模型不使用类,而是依赖于原型。
JavaScript社区采纳了大量最佳实践,以弥补JavaScript缺少类的问题。这些最佳实践已经被总结在Mozilla的开发指南中了,你还可以找到一篇关于JavaScript面向对象设计的优秀概述
。
不过,在ES6中,我们终于有内置的类了。
用class关键字来定义一个类,紧随其后的是类名和类的代码块:
class Vehicle {
}
类可以包含属性、方法以及构造函数。
2.5.1 属性
属性定义了类实例对象的数据。比如名叫Person的类可能有first_name、last_name和age属性。
类中的每个属性都可以包含一个可选的类型。比如,我们可以把first_name和last_name声明为字符串类型(string),把age声明为数字类型(number)。
Person类的声明是这样的:
class Person {
first_name:string;
last_name:string;
age:number;
}
2.5.2 方法
方法是运行在类对象实例上下文中的函数。在调用对象的方法之前,必须要有这个对象的实例。
要实例化一个类,我们使用new关键字。比如new Person()会创建一个Person类的实例对象。
如果我们希望问候某个Person,就可以这样写:
class Person {
first_name:string;
last_name:string;
age:number;
greet(){
console.log("Hello", this.first_name);
}
}
注意,借助this关键字,我们能用this.first_name表达式来访问Person类的first_name属性。
如果没有显式声明过方法的返回类型和返回值,就会假定它可能返回任何东西(即any类型)。然而,因为这里没有任何显式的return语句,所以实际返回的类型是void。
注意,void类型也是一种合法的any类型。
调用greet方法之前,我们要有一个Person类的实例对象。代码如下:
// declare a variable of type Person
var p:Person;
// instantiate a new Person instance
p = new Person();
// give it a first_name
p.first_name = 'Felipe';
// call the greet method
p.greet();
我们还可以将对象的声明和实例化缩写为一行代码:
var p:Person = new Person();
假设我们希望Person类有一个带返回值的方法。比如,要获取某个Person在数年后的年龄,我们可以这样写:
class Person {
first_name:string;
last_name:string;
age:number;
greet(){
console.log("Hello", this.first_name);
}
ageInYears(years:number):number {
return this.age + years;
}
}
// instantiate a new Person instance
var p:Person = new Person();
// set initial age
p.age = 6;
// how old will he be in 12 years?
p.ageInYears(12);
// -> 18
2.5.3 构造函数
构造函数是当类进行实例化时执行的特殊函数。通常会在构造函数中对新对象进行初始化工作。
构造函数必须命名为constructor。因为构造函数是在类被实例化时调用的,所以它们可以有输入参数,但不能有任何返回值。
我们要通过调用new ClassName()来执行构造函数,以完成类的实例化。
当类没有显式地定义构造函数时,将自动创建一个无参构造函数:
class Vehicle {
}
var v = new Vehicle();
它等价于:
class Vehicle {
constructor(){
}
}
var v = new Vehicle();
在TypeScript中,每个类只能有一个构造函数。
这是违背ES6标准的。在ES6中,一个类可以拥有不同参数数量的多个构造函数重载实现。
我们可以使用带参数的构造函数来将对象的创建工作参数化。
比如,我们可以对Person类使用构造函数来初始化它的数据:
class Person {
first_name:string;
last_name:string;
age:number;
constructor(first_name:string, last_name:string, age:number){
this.first_name = first_name;
this.last_name = last_name;
this.age = age;
}
greet(){
console.log("Hello", this.first_name);
}
ageInYears(years:number):number {
return this.age + years;
}
}
用下面这种方法重写前面的例子要容易些:
var p:Person = new Person('Felipe', 'Coury', 36);
p.greet();
当创建这个对象的时候,其姓名、年龄都会被初始化。
2.5.4 继承
面向对象的另一个重要特性就是继承。继承表明子类能够从父类得到它的行为。然后,我们就可以在这个子类中重写、修改以及添加行为。
如果要深入了解ES5的继承是如何工作的,可以参考Mozilla开发文档中的文章“Inheritance and the prototype chain”
。
TypeScript是完全支持继承特性的,并不像ES5那样要靠原型链实现。继承是TypeScript的核心语法,用extends关键字实现。
要说明这一点,我们来创建一个Report类:
class Report {
data:Array<string>;
constructor(data:Array<string>){
this.data = data;
}
run(){
this.data.forEach(function(line){ console.log(line); });
}
}
这个Report类有一个字符串数组类型的data的属性。当我们调用run方法时,它会循环这个data数组中的每一项数据,然后用console.log打印出来。
.forEach是Array中的一个方法,它接收一个函数作为参数,并对数组中的每一个条目逐个调用该函数。
给Report增加几行数据,并调用run把这些数据打印到控制台:
var r:Report = new Report(['First line', 'Second line']);
r.run();
运行结果如下:
First line
Second line
现在,假设我们希望有第二个报表,它需要增加一些头信息和数据,但我们仍想复用现有Report类的run方法来向用户展示数据。
为了复用Report类的行为,要使用extends关键字来继承它:
class TabbedReport extends Report {
headers:Array<string>;
constructor(headers:string[], values:string[]){
super(values)
this.headers = headers;
}
run(){
console.log(this.headers);
super.run();
}
}
var headers:string[] = ['Name'];
var data:string[] = ['Alice Green', 'Paul Pfifer', 'Louis Blakenship'];
var r:TabbedReport = new TabbedReport(headers, data)
r.run();
ES6和TypeScript提供了许多语法特性,让编码成为一种享受。其中最重要的两点是:
●胖箭头函数语法
●模板字符串
2.6.1 胖箭头函数
胖箭头(=>)函数是一种快速书写函数的简洁语法。
在ES5中,每当我们要用函数作为方法参数时,都必须用function关键字和紧随其后的花括号({})表示。就像这样:
// ES5-like example
var data = ['Alice Green', 'Paul Pfifer', 'Louis Blakenship'];
data.forEach(function(line){ console.log(line); });
现在我们可以用=>语法来重写它了:
// Typescript example
var data:string[] = ['Alice Green', 'Paul Pfifer', 'Louis Blakenship'];
data.forEach((line)=> console.log(line));
当只有一个参数时,圆括号可以省略。箭头(=>)语法可以用作表达式:
var evens = [2,4,6,8];
var odds = evens.map(v => v + 1);
也可以用作语句:
data.forEach(line => {
console.log(line.toUpperCase())
});
=>语法还有一个重要的特性,就是它和环绕它的外部代码共享同一个this。这是它和普通function写法最重要的不同点。通常,我们用function声明的函数有它自己的this。有时在JavaScript中能看见如下代码:
var nate = {
name:"Nate",
guitars:["Gibson", "Martin", "Taylor"],
printGuitars:function(){
var self = this;
this.guitars.forEach(function(g){
// this.name is undefined so we have to use self.name
console.log(self.name + " plays a " + g);
});
}
};
由于胖箭头会共享环绕它的外部代码的this,我们可以这样改写:
var nate = {
name:"Nate",
guitars:["Gibson", "Martin", "Taylor"],
printGuitars:function(){
this.guitars.forEach((g)=> {
console.log(this.name + " plays a " + g);
});
}
};
可见,箭头函数是处理内联函数的好办法。这也让我们在JavaScript中更容易使用高阶函数。
2.6.2 模板字符串
ES6引入了新的模板字符串语法,它有两大优势:
(1)可以在模板字符串中使用变量(不必被迫使用+来拼接字符串);
(2)支持多行字符串。
●字符串中的变量
这种特性也叫字符串插值(string interpolation)。你可以在字符串中插入变量,做法如下:
var firstName = "Nate";
var lastName = "Murray";
// interpolate a string
var greeting = `Hello ${firstName} ${lastName}`;
console.log(greeting);
注意,字符串插值必须使用反引号,不能用单引号或双引号。
●多行字符串
反引号字符串的另一个优点是允许多行文本:
var template = `
<div>
<h1>Hello</h1>
<p>This is a great website</p>
</div>
`
// do something with `template`
当我们要插入模板这样的长文本字符串时,多行字符串会非常有帮助。
在TypeScript和ES6中还有很多其他的优秀语法特性,如:
●接口
●泛型
●模块的导入、导出
●标注
●解构
我们会在本书的后续章节中讲到这些概念并使用它们。目前,本章的这些基本知识已经足够你开始学习Angular了。
言归正传,让我们回到Angular吧!
Angular是用一种类似于JavaScript的语言——TypeScript——构建的。
或许你会对用新语言来开发Angular心存疑虑,但事实上,在开发Angular应用时,我们有充分的理由用TypeScript代替普通的JavaScript。
TypeScript并不是一门全新的语言,而是ES6的超集。所有的ES6代码都是完全有效且可编译的TypeScript代码。图2-1展示了它们之间的关系。
图2-1 ES5、ES6和TypeScript
什么是ES5?什么是ES6?ES5是ECMAScript 5的缩写,也被称为“普通的JavaScript”。ES5就是大家熟知的JavaScript,它能够运行在大部分浏览器上。ES6则是下一个版本的JavaScript,在后续章节中我们还会深入讨论它。
在本书出版的时候,支持ES6的浏览器还很少,更不用说TypeScript了。我们用转译器来解决这个问题。TypeScript转译器能把TypeScript代码转换为几乎所有浏览器都支持的ES5代码。
从TypeScript代码到ES5代码的唯一转换器是由TypeScript核心团队编写的。然而,将ES6代码(不是TypeScript代码)转换到ES5代码则有两个主要的转换器:Google开发的Traceur与JavaScript社区创建的Babel
。在本书中我们并不会直接使用它们,但它们也是值得了解的不错项目。
我们在上一章安装了TypeScript环境,如果你是从本章开始学习的,那么可以这样安装TypeScript环境:npm install -g typescript。
TypeScript是Microsoft和Google之间的官方合作项目。有这两家强有力的科技巨头在背后支撑,对于我们来说是个好消息,因为这表示TypeScript将会得到长期的支持。这两家公司都承诺全力推动Web技术的发展,我们这些开发人员显然会获益匪浅。
另外,转译器的好处还在于:它允许小型团队对语言进行改善,而不必要求所有人都去升级他们的浏览器。
需要指出的是:TypeScript并不是开发Angular应用的必选语言。我们同样可以使用ES5代码(即“普通”JavaScript)来开发Angular应用。Angular也为全部功能提供了ES5 API。那么为什么我们还要使用TypeScript呢?这是因为TypeScript有不少强大的功能,能极大地简化开发。
TypeScript相对于ES5有五大改善:
●类型
●类
●注解
●模块导入
●语言工具包(比如,解构)
接下来我们逐个介绍。
顾名思义,相对于ES6,TypeScript最大的改善是增加了类型系统。
有些人可能会觉得,缺乏类型检查正是JavaScript这些弱类型语言的优点。也许你对类型检查心存疑虑,但我仍然鼓励你试一试。类型检查的好处有:
(1)有助于代码的编写,因为它可以在编译期预防bug;
(2)有助于代码的阅读,因为它能清晰地表明你的意图。
另外值得一提的是,TypeScript中的类型是可选的。如果希望写一些快速代码或功能原型,可以首先省略类型,然后再随着代码日趋成熟逐渐加上类型。
TypeScript的基本类型与我们平时所写JavaScript代码中用的隐式类型一样,包括字符串、数字、布尔值等。
直到ES5,我们都在用var关键字定义变量,比如var name;。
TypeScript的新语法是从ES5自然演化而来的,仍沿用var来定义变量,但现在可以同时为变量名提供可选的变量类型了:
var name:string;
在声明函数时,也可以为函数参数和返回值指定类型:
function greetText(name:string):string {
return "Hello " + name;
}
这个例子中,我们定义了一个名为greetText的新函数,它接收一个名为name的参数。name:string语法表示函数想要的name参数是string类型。如果给该函数传一个string以外的参数,代码将无法编译通过。对我们来说,这是好事,否则这段代码将会引入bug。
或许你还注意到了,greetText函数在括号后面还有一个新语法:string {。冒号之后指定的是该函数的返回值类型,在本例中为string。这很有用,原因有二:如果不小心让函数返回了一个非string型的返回值,编译器就会告诉我们这里有错误;使用该函数的开发人员也能很清晰地知道自己将会拿到什么类型的数据。
我们来看看如果写了不符合类型声明的代码会怎样:
function hello(name:string):string {
return 12;
}
当尝试编译代码时,将会得到下列错误:
$ tsc compile-error.ts
compile-error.ts(2,12):error TS2322:Type 'number' is not assignable to type 'string'.
这是怎么回事?我们尝试返回一个number类型的12,但hello函数期望的返回值类型为string(它是在参数声明的后面以):string {的形式声明的)。
要纠正它,可以把函数的返回值类型改为number:
function hello(name:string):number {
return 12;
}
虽然这只是一个小例子,但足以证明类型检查能为我们节省大量调试bug的时间。
现在知道了如何使用类型,但怎么才能知道有哪些可用类型呢?接下来我们就会罗列出这些内置的类型,并教你如何创建自己的类型。
尝试REPL
为了运行本章中的例子,我们要先安装一个小工具,名为TSUN(TypeScript Upgraded Node,支持TypeScript的升级版Node):
$ npm install -g tsun
接着启动它:
$ tsun
TSUN:TypeScript Upgraded Node
type in TypeScript expression to evaluate
type:help for commands in repl
>
这个小小的>是一个命令提示符,表示TSUN已经准备好接收命令了。
-------------------------------------------------------------------------------- /output/30410212/52459916.html: -------------------------------------------------------------------------------- 1 |Angular是用一种类似于JavaScript的语言——TypeScript——构建的。
或许你会对用新语言来开发Angular心存疑虑,但事实上,在开发Angular应用时,我们有充分的理由用TypeScript代替普通的JavaScript。
TypeScript并不是一门全新的语言,而是ES6的超集。所有的ES6代码都是完全有效且可编译的TypeScript代码。图2-1展示了它们之间的关系。
图2-1 ES5、ES6和TypeScript
什么是ES5?什么是ES6?ES5是ECMAScript 5的缩写,也被称为“普通的JavaScript”。ES5就是大家熟知的JavaScript,它能够运行在大部分浏览器上。ES6则是下一个版本的JavaScript,在后续章节中我们还会深入讨论它。
在本书出版的时候,支持ES6的浏览器还很少,更不用说TypeScript了。我们用转译器来解决这个问题。TypeScript转译器能把TypeScript代码转换为几乎所有浏览器都支持的ES5代码。
从TypeScript代码到ES5代码的唯一转换器是由TypeScript核心团队编写的。然而,将ES6代码(不是TypeScript代码)转换到ES5代码则有两个主要的转换器:Google开发的Traceur与JavaScript社区创建的Babel
。在本书中我们并不会直接使用它们,但它们也是值得了解的不错项目。
我们在上一章安装了TypeScript环境,如果你是从本章开始学习的,那么可以这样安装TypeScript环境:npm install -g typescript。
TypeScript是Microsoft和Google之间的官方合作项目。有这两家强有力的科技巨头在背后支撑,对于我们来说是个好消息,因为这表示TypeScript将会得到长期的支持。这两家公司都承诺全力推动Web技术的发展,我们这些开发人员显然会获益匪浅。
另外,转译器的好处还在于:它允许小型团队对语言进行改善,而不必要求所有人都去升级他们的浏览器。
需要指出的是:TypeScript并不是开发Angular应用的必选语言。我们同样可以使用ES5代码(即“普通”JavaScript)来开发Angular应用。Angular也为全部功能提供了ES5 API。那么为什么我们还要使用TypeScript呢?这是因为TypeScript有不少强大的功能,能极大地简化开发。
TypeScript相对于ES5有五大改善:
●类型
●类
●注解
●模块导入
●语言工具包(比如,解构)
接下来我们逐个介绍。
顾名思义,相对于ES6,TypeScript最大的改善是增加了类型系统。
有些人可能会觉得,缺乏类型检查正是JavaScript这些弱类型语言的优点。也许你对类型检查心存疑虑,但我仍然鼓励你试一试。类型检查的好处有:
(1)有助于代码的编写,因为它可以在编译期预防bug;
(2)有助于代码的阅读,因为它能清晰地表明你的意图。
另外值得一提的是,TypeScript中的类型是可选的。如果希望写一些快速代码或功能原型,可以首先省略类型,然后再随着代码日趋成熟逐渐加上类型。
TypeScript的基本类型与我们平时所写JavaScript代码中用的隐式类型一样,包括字符串、数字、布尔值等。
直到ES5,我们都在用var关键字定义变量,比如var name;。
TypeScript的新语法是从ES5自然演化而来的,仍沿用var来定义变量,但现在可以同时为变量名提供可选的变量类型了:
var name:string;
在声明函数时,也可以为函数参数和返回值指定类型:
function greetText(name:string):string {
return "Hello " + name;
}
这个例子中,我们定义了一个名为greetText的新函数,它接收一个名为name的参数。name:string语法表示函数想要的name参数是string类型。如果给该函数传一个string以外的参数,代码将无法编译通过。对我们来说,这是好事,否则这段代码将会引入bug。
或许你还注意到了,greetText函数在括号后面还有一个新语法:string {。冒号之后指定的是该函数的返回值类型,在本例中为string。这很有用,原因有二:如果不小心让函数返回了一个非string型的返回值,编译器就会告诉我们这里有错误;使用该函数的开发人员也能很清晰地知道自己将会拿到什么类型的数据。
我们来看看如果写了不符合类型声明的代码会怎样:
function hello(name:string):string {
return 12;
}
当尝试编译代码时,将会得到下列错误:
$ tsc compile-error.ts
compile-error.ts(2,12):error TS2322:Type 'number' is not assignable to type 'string'.
这是怎么回事?我们尝试返回一个number类型的12,但hello函数期望的返回值类型为string(它是在参数声明的后面以):string {的形式声明的)。
要纠正它,可以把函数的返回值类型改为number:
function hello(name:string):number {
return 12;
}
虽然这只是一个小例子,但足以证明类型检查能为我们节省大量调试bug的时间。
现在知道了如何使用类型,但怎么才能知道有哪些可用类型呢?接下来我们就会罗列出这些内置的类型,并教你如何创建自己的类型。
尝试REPL
为了运行本章中的例子,我们要先安装一个小工具,名为TSUN(TypeScript Upgraded Node,支持TypeScript的升级版Node):
$ npm install -g tsun
接着启动它:
$ tsun
TSUN:TypeScript Upgraded Node
type in TypeScript expression to evaluate
type:help for commands in repl
>
这个小小的>是一个命令提示符,表示TSUN已经准备好接收命令了。
-------------------------------------------------------------------------------- /output/30410212/52459917.html: -------------------------------------------------------------------------------- 1 |Angular是用一种类似于JavaScript的语言——TypeScript——构建的。
或许你会对用新语言来开发Angular心存疑虑,但事实上,在开发Angular应用时,我们有充分的理由用TypeScript代替普通的JavaScript。
TypeScript并不是一门全新的语言,而是ES6的超集。所有的ES6代码都是完全有效且可编译的TypeScript代码。图2-1展示了它们之间的关系。
图2-1 ES5、ES6和TypeScript
什么是ES5?什么是ES6?ES5是ECMAScript 5的缩写,也被称为“普通的JavaScript”。ES5就是大家熟知的JavaScript,它能够运行在大部分浏览器上。ES6则是下一个版本的JavaScript,在后续章节中我们还会深入讨论它。
在本书出版的时候,支持ES6的浏览器还很少,更不用说TypeScript了。我们用转译器来解决这个问题。TypeScript转译器能把TypeScript代码转换为几乎所有浏览器都支持的ES5代码。
从TypeScript代码到ES5代码的唯一转换器是由TypeScript核心团队编写的。然而,将ES6代码(不是TypeScript代码)转换到ES5代码则有两个主要的转换器:Google开发的Traceur与JavaScript社区创建的Babel
。在本书中我们并不会直接使用它们,但它们也是值得了解的不错项目。
我们在上一章安装了TypeScript环境,如果你是从本章开始学习的,那么可以这样安装TypeScript环境:npm install -g typescript。
TypeScript是Microsoft和Google之间的官方合作项目。有这两家强有力的科技巨头在背后支撑,对于我们来说是个好消息,因为这表示TypeScript将会得到长期的支持。这两家公司都承诺全力推动Web技术的发展,我们这些开发人员显然会获益匪浅。
另外,转译器的好处还在于:它允许小型团队对语言进行改善,而不必要求所有人都去升级他们的浏览器。
需要指出的是:TypeScript并不是开发Angular应用的必选语言。我们同样可以使用ES5代码(即“普通”JavaScript)来开发Angular应用。Angular也为全部功能提供了ES5 API。那么为什么我们还要使用TypeScript呢?这是因为TypeScript有不少强大的功能,能极大地简化开发。
TypeScript相对于ES5有五大改善:
●类型
●类
●注解
●模块导入
●语言工具包(比如,解构)
接下来我们逐个介绍。
顾名思义,相对于ES6,TypeScript最大的改善是增加了类型系统。
有些人可能会觉得,缺乏类型检查正是JavaScript这些弱类型语言的优点。也许你对类型检查心存疑虑,但我仍然鼓励你试一试。类型检查的好处有:
(1)有助于代码的编写,因为它可以在编译期预防bug;
(2)有助于代码的阅读,因为它能清晰地表明你的意图。
另外值得一提的是,TypeScript中的类型是可选的。如果希望写一些快速代码或功能原型,可以首先省略类型,然后再随着代码日趋成熟逐渐加上类型。
TypeScript的基本类型与我们平时所写JavaScript代码中用的隐式类型一样,包括字符串、数字、布尔值等。
直到ES5,我们都在用var关键字定义变量,比如var name;。
TypeScript的新语法是从ES5自然演化而来的,仍沿用var来定义变量,但现在可以同时为变量名提供可选的变量类型了:
var name:string;
在声明函数时,也可以为函数参数和返回值指定类型:
function greetText(name:string):string {
return "Hello " + name;
}
这个例子中,我们定义了一个名为greetText的新函数,它接收一个名为name的参数。name:string语法表示函数想要的name参数是string类型。如果给该函数传一个string以外的参数,代码将无法编译通过。对我们来说,这是好事,否则这段代码将会引入bug。
或许你还注意到了,greetText函数在括号后面还有一个新语法:string {。冒号之后指定的是该函数的返回值类型,在本例中为string。这很有用,原因有二:如果不小心让函数返回了一个非string型的返回值,编译器就会告诉我们这里有错误;使用该函数的开发人员也能很清晰地知道自己将会拿到什么类型的数据。
我们来看看如果写了不符合类型声明的代码会怎样:
function hello(name:string):string {
return 12;
}
当尝试编译代码时,将会得到下列错误:
$ tsc compile-error.ts
compile-error.ts(2,12):error TS2322:Type 'number' is not assignable to type 'string'.
这是怎么回事?我们尝试返回一个number类型的12,但hello函数期望的返回值类型为string(它是在参数声明的后面以):string {的形式声明的)。
要纠正它,可以把函数的返回值类型改为number:
function hello(name:string):number {
return 12;
}
虽然这只是一个小例子,但足以证明类型检查能为我们节省大量调试bug的时间。
现在知道了如何使用类型,但怎么才能知道有哪些可用类型呢?接下来我们就会罗列出这些内置的类型,并教你如何创建自己的类型。
尝试REPL
为了运行本章中的例子,我们要先安装一个小工具,名为TSUN(TypeScript Upgraded Node,支持TypeScript的升级版Node):
$ npm install -g tsun
接着启动它:
$ tsun
TSUN:TypeScript Upgraded Node
type in TypeScript expression to evaluate
type:help for commands in repl
>
这个小小的>是一个命令提示符,表示TSUN已经准备好接收命令了。
-------------------------------------------------------------------------------- /output/30410212/52459918.html: -------------------------------------------------------------------------------- 1 |Angular是用一种类似于JavaScript的语言——TypeScript——构建的。
或许你会对用新语言来开发Angular心存疑虑,但事实上,在开发Angular应用时,我们有充分的理由用TypeScript代替普通的JavaScript。
TypeScript并不是一门全新的语言,而是ES6的超集。所有的ES6代码都是完全有效且可编译的TypeScript代码。图2-1展示了它们之间的关系。
图2-1 ES5、ES6和TypeScript
什么是ES5?什么是ES6?ES5是ECMAScript 5的缩写,也被称为“普通的JavaScript”。ES5就是大家熟知的JavaScript,它能够运行在大部分浏览器上。ES6则是下一个版本的JavaScript,在后续章节中我们还会深入讨论它。
在本书出版的时候,支持ES6的浏览器还很少,更不用说TypeScript了。我们用转译器来解决这个问题。TypeScript转译器能把TypeScript代码转换为几乎所有浏览器都支持的ES5代码。
从TypeScript代码到ES5代码的唯一转换器是由TypeScript核心团队编写的。然而,将ES6代码(不是TypeScript代码)转换到ES5代码则有两个主要的转换器:Google开发的Traceur与JavaScript社区创建的Babel
。在本书中我们并不会直接使用它们,但它们也是值得了解的不错项目。
我们在上一章安装了TypeScript环境,如果你是从本章开始学习的,那么可以这样安装TypeScript环境:npm install -g typescript。
TypeScript是Microsoft和Google之间的官方合作项目。有这两家强有力的科技巨头在背后支撑,对于我们来说是个好消息,因为这表示TypeScript将会得到长期的支持。这两家公司都承诺全力推动Web技术的发展,我们这些开发人员显然会获益匪浅。
另外,转译器的好处还在于:它允许小型团队对语言进行改善,而不必要求所有人都去升级他们的浏览器。
需要指出的是:TypeScript并不是开发Angular应用的必选语言。我们同样可以使用ES5代码(即“普通”JavaScript)来开发Angular应用。Angular也为全部功能提供了ES5 API。那么为什么我们还要使用TypeScript呢?这是因为TypeScript有不少强大的功能,能极大地简化开发。
TypeScript相对于ES5有五大改善:
●类型
●类
●注解
●模块导入
●语言工具包(比如,解构)
接下来我们逐个介绍。
顾名思义,相对于ES6,TypeScript最大的改善是增加了类型系统。
有些人可能会觉得,缺乏类型检查正是JavaScript这些弱类型语言的优点。也许你对类型检查心存疑虑,但我仍然鼓励你试一试。类型检查的好处有:
(1)有助于代码的编写,因为它可以在编译期预防bug;
(2)有助于代码的阅读,因为它能清晰地表明你的意图。
另外值得一提的是,TypeScript中的类型是可选的。如果希望写一些快速代码或功能原型,可以首先省略类型,然后再随着代码日趋成熟逐渐加上类型。
TypeScript的基本类型与我们平时所写JavaScript代码中用的隐式类型一样,包括字符串、数字、布尔值等。
直到ES5,我们都在用var关键字定义变量,比如var name;。
TypeScript的新语法是从ES5自然演化而来的,仍沿用var来定义变量,但现在可以同时为变量名提供可选的变量类型了:
var name:string;
在声明函数时,也可以为函数参数和返回值指定类型:
function greetText(name:string):string {
return "Hello " + name;
}
这个例子中,我们定义了一个名为greetText的新函数,它接收一个名为name的参数。name:string语法表示函数想要的name参数是string类型。如果给该函数传一个string以外的参数,代码将无法编译通过。对我们来说,这是好事,否则这段代码将会引入bug。
或许你还注意到了,greetText函数在括号后面还有一个新语法:string {。冒号之后指定的是该函数的返回值类型,在本例中为string。这很有用,原因有二:如果不小心让函数返回了一个非string型的返回值,编译器就会告诉我们这里有错误;使用该函数的开发人员也能很清晰地知道自己将会拿到什么类型的数据。
我们来看看如果写了不符合类型声明的代码会怎样:
function hello(name:string):string {
return 12;
}
当尝试编译代码时,将会得到下列错误:
$ tsc compile-error.ts
compile-error.ts(2,12):error TS2322:Type 'number' is not assignable to type 'string'.
这是怎么回事?我们尝试返回一个number类型的12,但hello函数期望的返回值类型为string(它是在参数声明的后面以):string {的形式声明的)。
要纠正它,可以把函数的返回值类型改为number:
function hello(name:string):number {
return 12;
}
虽然这只是一个小例子,但足以证明类型检查能为我们节省大量调试bug的时间。
现在知道了如何使用类型,但怎么才能知道有哪些可用类型呢?接下来我们就会罗列出这些内置的类型,并教你如何创建自己的类型。
尝试REPL
为了运行本章中的例子,我们要先安装一个小工具,名为TSUN(TypeScript Upgraded Node,支持TypeScript的升级版Node):
$ npm install -g tsun
接着启动它:
$ tsun
TSUN:TypeScript Upgraded Node
type in TypeScript expression to evaluate
type:help for commands in repl
>
这个小小的>是一个命令提示符,表示TSUN已经准备好接收命令了。
对于本章后面的大部分例子,你都可以复制粘贴到这个终端窗口中运行。
2.4.1 字符串
字符串包含文本,声明为string类型:
var name:string = 'Felipe';
2.4.2 数字
无论整数还是浮点,任何类型的数字都属于number类型。在TypeScript中,所有的数字都是用浮点数表示的,这些数字的类型就是number:
var age:number = 36;
2.4.3 布尔类型
布尔类型(boolean)以true(真)和false(假)为值。
var married:boolean = true;
2.4.4 数组
数组用Array类型表示。然而,因为数组是一组相同数据类型的集合,所以我们还需要为数组中的条目指定一个类型。
我们可以用Array<type>或者type[]语法来为数组条目指定元素类型:
var jobs:Array<string> = ['IBM', 'Microsoft', 'Google'];
var jobs:string[] = ['Apple', 'Dell', 'HP'];
数字型数组的声明与之类似:
var jobs:Array<number> = [1, 2, 3];
var jobs:number[] = [4, 5, 6];
2.4.5 枚举
枚举是一组可命名数值的集合。比如,如果我们想拿到某人的一系列角色,可以这么写:
enum Role {Employee, Manager, Admin};
var role:Role = Role.Employee;
默认情况下,枚举类型的初始值是0。我们也可以调整初始化值的范围:
enum Role {Employee = 3, Manager, Admin};
var role:Role = Role.Employee;
在上面的代码中,Employee的初始值被设置为3而不是0。枚举中其他项的值是依次递增的,意味着Manager的值为4,Admin的值为5。同样,我们也可以单独为枚举中的每一项指定值:
enum Role {Employee = 3, Manager = 5, Admin = 7};
var role:Role = Role.Employee;
还可以从枚举的值来反查它的名称:
enum Role {Employee, Manager, Admin};
console.log('Roles:', Role[0], ',', Role[1], 'and', Role[2]);
2.4.6 任意类型
如果我们没有为变量指定类型,那它的默认类型就是any。在TypeScript中,any类型的变量能够接收任意类型的数据:
var something:any = 'as string';
something = 1;
something = [1, 2, 3];
2.4.7 “无”类型
void意味着我们不期望那里有类型。它通常用作函数的返回值,表示没有任何返回值:
function setName(name:string):void {
this.name = name;
}
JavaScript ES5采用的是基于原型的面向对象设计。这种设计模型不使用类,而是依赖于原型。
JavaScript社区采纳了大量最佳实践,以弥补JavaScript缺少类的问题。这些最佳实践已经被总结在Mozilla的开发指南中了,你还可以找到一篇关于JavaScript面向对象设计的优秀概述
。
不过,在ES6中,我们终于有内置的类了。
用class关键字来定义一个类,紧随其后的是类名和类的代码块:
class Vehicle {
}
类可以包含属性、方法以及构造函数。
2.5.1 属性
属性定义了类实例对象的数据。比如名叫Person的类可能有first_name、last_name和age属性。
类中的每个属性都可以包含一个可选的类型。比如,我们可以把first_name和last_name声明为字符串类型(string),把age声明为数字类型(number)。
Person类的声明是这样的:
class Person {
first_name:string;
last_name:string;
age:number;
}
2.5.2 方法
方法是运行在类对象实例上下文中的函数。在调用对象的方法之前,必须要有这个对象的实例。
要实例化一个类,我们使用new关键字。比如new Person()会创建一个Person类的实例对象。
如果我们希望问候某个Person,就可以这样写:
class Person {
first_name:string;
last_name:string;
age:number;
greet(){
console.log("Hello", this.first_name);
}
}
注意,借助this关键字,我们能用this.first_name表达式来访问Person类的first_name属性。
如果没有显式声明过方法的返回类型和返回值,就会假定它可能返回任何东西(即any类型)。然而,因为这里没有任何显式的return语句,所以实际返回的类型是void。
注意,void类型也是一种合法的any类型。
调用greet方法之前,我们要有一个Person类的实例对象。代码如下:
// declare a variable of type Person
var p:Person;
// instantiate a new Person instance
p = new Person();
// give it a first_name
p.first_name = 'Felipe';
// call the greet method
p.greet();
我们还可以将对象的声明和实例化缩写为一行代码:
var p:Person = new Person();
假设我们希望Person类有一个带返回值的方法。比如,要获取某个Person在数年后的年龄,我们可以这样写:
class Person {
first_name:string;
last_name:string;
age:number;
greet(){
console.log("Hello", this.first_name);
}
ageInYears(years:number):number {
return this.age + years;
}
}
// instantiate a new Person instance
var p:Person = new Person();
// set initial age
p.age = 6;
// how old will he be in 12 years?
p.ageInYears(12);
// -> 18
2.5.3 构造函数
构造函数是当类进行实例化时执行的特殊函数。通常会在构造函数中对新对象进行初始化工作。
构造函数必须命名为constructor。因为构造函数是在类被实例化时调用的,所以它们可以有输入参数,但不能有任何返回值。
我们要通过调用new ClassName()来执行构造函数,以完成类的实例化。
当类没有显式地定义构造函数时,将自动创建一个无参构造函数:
class Vehicle {
}
var v = new Vehicle();
它等价于:
class Vehicle {
constructor(){
}
}
var v = new Vehicle();
在TypeScript中,每个类只能有一个构造函数。
这是违背ES6标准的。在ES6中,一个类可以拥有不同参数数量的多个构造函数重载实现。
我们可以使用带参数的构造函数来将对象的创建工作参数化。
比如,我们可以对Person类使用构造函数来初始化它的数据:
class Person {
first_name:string;
last_name:string;
age:number;
constructor(first_name:string, last_name:string, age:number){
this.first_name = first_name;
this.last_name = last_name;
this.age = age;
}
greet(){
console.log("Hello", this.first_name);
}
ageInYears(years:number):number {
return this.age + years;
}
}
用下面这种方法重写前面的例子要容易些:
var p:Person = new Person('Felipe', 'Coury', 36);
p.greet();
当创建这个对象的时候,其姓名、年龄都会被初始化。
2.5.4 继承
面向对象的另一个重要特性就是继承。继承表明子类能够从父类得到它的行为。然后,我们就可以在这个子类中重写、修改以及添加行为。
如果要深入了解ES5的继承是如何工作的,可以参考Mozilla开发文档中的文章“Inheritance and the prototype chain”
。
TypeScript是完全支持继承特性的,并不像ES5那样要靠原型链实现。继承是TypeScript的核心语法,用extends关键字实现。
要说明这一点,我们来创建一个Report类:
class Report {
data:Array<string>;
constructor(data:Array<string>){
this.data = data;
}
run(){
this.data.forEach(function(line){ console.log(line); });
}
}
这个Report类有一个字符串数组类型的data的属性。当我们调用run方法时,它会循环这个data数组中的每一项数据,然后用console.log打印出来。
.forEach是Array中的一个方法,它接收一个函数作为参数,并对数组中的每一个条目逐个调用该函数。
给Report增加几行数据,并调用run把这些数据打印到控制台:
var r:Report = new Report(['First line', 'Second line']);
r.run();
运行结果如下:
First line
Second line
现在,假设我们希望有第二个报表,它需要增加一些头信息和数据,但我们仍想复用现有Report类的run方法来向用户展示数据。
为了复用Report类的行为,要使用extends关键字来继承它:
class TabbedReport extends Report {
headers:Array<string>;
constructor(headers:string[], values:string[]){
super(values)
this.headers = headers;
}
run(){
console.log(this.headers);
super.run();
}
}
var headers:string[] = ['Name'];
var data:string[] = ['Alice Green', 'Paul Pfifer', 'Louis Blakenship'];
var r:TabbedReport = new TabbedReport(headers, data)
r.run();
ES6和TypeScript提供了许多语法特性,让编码成为一种享受。其中最重要的两点是:
●胖箭头函数语法
●模板字符串
2.6.1 胖箭头函数
胖箭头(=>)函数是一种快速书写函数的简洁语法。
在ES5中,每当我们要用函数作为方法参数时,都必须用function关键字和紧随其后的花括号({})表示。就像这样:
// ES5-like example
var data = ['Alice Green', 'Paul Pfifer', 'Louis Blakenship'];
data.forEach(function(line){ console.log(line); });
现在我们可以用=>语法来重写它了:
// Typescript example
var data:string[] = ['Alice Green', 'Paul Pfifer', 'Louis Blakenship'];
data.forEach((line)=> console.log(line));
当只有一个参数时,圆括号可以省略。箭头(=>)语法可以用作表达式:
var evens = [2,4,6,8];
var odds = evens.map(v => v + 1);
也可以用作语句:
data.forEach(line => {
console.log(line.toUpperCase())
});
=>语法还有一个重要的特性,就是它和环绕它的外部代码共享同一个this。这是它和普通function写法最重要的不同点。通常,我们用function声明的函数有它自己的this。有时在JavaScript中能看见如下代码:
var nate = {
name:"Nate",
guitars:["Gibson", "Martin", "Taylor"],
printGuitars:function(){
var self = this;
this.guitars.forEach(function(g){
// this.name is undefined so we have to use self.name
console.log(self.name + " plays a " + g);
});
}
};
由于胖箭头会共享环绕它的外部代码的this,我们可以这样改写:
var nate = {
name:"Nate",
guitars:["Gibson", "Martin", "Taylor"],
printGuitars:function(){
this.guitars.forEach((g)=> {
console.log(this.name + " plays a " + g);
});
}
};
可见,箭头函数是处理内联函数的好办法。这也让我们在JavaScript中更容易使用高阶函数。
2.6.2 模板字符串
ES6引入了新的模板字符串语法,它有两大优势:
(1)可以在模板字符串中使用变量(不必被迫使用+来拼接字符串);
(2)支持多行字符串。
●字符串中的变量
这种特性也叫字符串插值(string interpolation)。你可以在字符串中插入变量,做法如下:
var firstName = "Nate";
var lastName = "Murray";
// interpolate a string
var greeting = `Hello ${firstName} ${lastName}`;
console.log(greeting);
注意,字符串插值必须使用反引号,不能用单引号或双引号。
●多行字符串
反引号字符串的另一个优点是允许多行文本:
var template = `
<div>
<h1>Hello</h1>
<p>This is a great website</p>
</div>
`
// do something with `template`
当我们要插入模板这样的长文本字符串时,多行字符串会非常有帮助。
在TypeScript和ES6中还有很多其他的优秀语法特性,如:
●接口
●泛型
●模块的导入、导出
●标注
●解构
我们会在本书的后续章节中讲到这些概念并使用它们。目前,本章的这些基本知识已经足够你开始学习Angular了。
言归正传,让我们回到Angular吧!
Angular是用一种类似于JavaScript的语言——TypeScript——构建的。
或许你会对用新语言来开发Angular心存疑虑,但事实上,在开发Angular应用时,我们有充分的理由用TypeScript代替普通的JavaScript。
TypeScript并不是一门全新的语言,而是ES6的超集。所有的ES6代码都是完全有效且可编译的TypeScript代码。图2-1展示了它们之间的关系。
图2-1 ES5、ES6和TypeScript
什么是ES5?什么是ES6?ES5是ECMAScript 5的缩写,也被称为“普通的JavaScript”。ES5就是大家熟知的JavaScript,它能够运行在大部分浏览器上。ES6则是下一个版本的JavaScript,在后续章节中我们还会深入讨论它。
在本书出版的时候,支持ES6的浏览器还很少,更不用说TypeScript了。我们用转译器来解决这个问题。TypeScript转译器能把TypeScript代码转换为几乎所有浏览器都支持的ES5代码。
从TypeScript代码到ES5代码的唯一转换器是由TypeScript核心团队编写的。然而,将ES6代码(不是TypeScript代码)转换到ES5代码则有两个主要的转换器:Google开发的Traceur与JavaScript社区创建的Babel
。在本书中我们并不会直接使用它们,但它们也是值得了解的不错项目。
我们在上一章安装了TypeScript环境,如果你是从本章开始学习的,那么可以这样安装TypeScript环境:npm install -g typescript。
TypeScript是Microsoft和Google之间的官方合作项目。有这两家强有力的科技巨头在背后支撑,对于我们来说是个好消息,因为这表示TypeScript将会得到长期的支持。这两家公司都承诺全力推动Web技术的发展,我们这些开发人员显然会获益匪浅。
另外,转译器的好处还在于:它允许小型团队对语言进行改善,而不必要求所有人都去升级他们的浏览器。
需要指出的是:TypeScript并不是开发Angular应用的必选语言。我们同样可以使用ES5代码(即“普通”JavaScript)来开发Angular应用。Angular也为全部功能提供了ES5 API。那么为什么我们还要使用TypeScript呢?这是因为TypeScript有不少强大的功能,能极大地简化开发。
TypeScript相对于ES5有五大改善:
●类型
●类
●注解
●模块导入
●语言工具包(比如,解构)
接下来我们逐个介绍。
顾名思义,相对于ES6,TypeScript最大的改善是增加了类型系统。
有些人可能会觉得,缺乏类型检查正是JavaScript这些弱类型语言的优点。也许你对类型检查心存疑虑,但我仍然鼓励你试一试。类型检查的好处有:
(1)有助于代码的编写,因为它可以在编译期预防bug;
(2)有助于代码的阅读,因为它能清晰地表明你的意图。
另外值得一提的是,TypeScript中的类型是可选的。如果希望写一些快速代码或功能原型,可以首先省略类型,然后再随着代码日趋成熟逐渐加上类型。
TypeScript的基本类型与我们平时所写JavaScript代码中用的隐式类型一样,包括字符串、数字、布尔值等。
直到ES5,我们都在用var关键字定义变量,比如var name;。
TypeScript的新语法是从ES5自然演化而来的,仍沿用var来定义变量,但现在可以同时为变量名提供可选的变量类型了:
var name:string;
在声明函数时,也可以为函数参数和返回值指定类型:
function greetText(name:string):string {
return "Hello " + name;
}
这个例子中,我们定义了一个名为greetText的新函数,它接收一个名为name的参数。name:string语法表示函数想要的name参数是string类型。如果给该函数传一个string以外的参数,代码将无法编译通过。对我们来说,这是好事,否则这段代码将会引入bug。
或许你还注意到了,greetText函数在括号后面还有一个新语法:string {。冒号之后指定的是该函数的返回值类型,在本例中为string。这很有用,原因有二:如果不小心让函数返回了一个非string型的返回值,编译器就会告诉我们这里有错误;使用该函数的开发人员也能很清晰地知道自己将会拿到什么类型的数据。
我们来看看如果写了不符合类型声明的代码会怎样:
function hello(name:string):string {
return 12;
}
当尝试编译代码时,将会得到下列错误:
$ tsc compile-error.ts
compile-error.ts(2,12):error TS2322:Type 'number' is not assignable to type 'string'.
这是怎么回事?我们尝试返回一个number类型的12,但hello函数期望的返回值类型为string(它是在参数声明的后面以):string {的形式声明的)。
要纠正它,可以把函数的返回值类型改为number:
function hello(name:string):number {
return 12;
}
虽然这只是一个小例子,但足以证明类型检查能为我们节省大量调试bug的时间。
现在知道了如何使用类型,但怎么才能知道有哪些可用类型呢?接下来我们就会罗列出这些内置的类型,并教你如何创建自己的类型。
尝试REPL
为了运行本章中的例子,我们要先安装一个小工具,名为TSUN(TypeScript Upgraded Node,支持TypeScript的升级版Node):
$ npm install -g tsun
接着启动它:
$ tsun
TSUN:TypeScript Upgraded Node
type in TypeScript expression to evaluate
type:help for commands in repl
>
这个小小的>是一个命令提示符,表示TSUN已经准备好接收命令了。
对于本章后面的大部分例子,你都可以复制粘贴到这个终端窗口中运行。
2.4.1 字符串
字符串包含文本,声明为string类型:
var name:string = 'Felipe';
2.4.2 数字
无论整数还是浮点,任何类型的数字都属于number类型。在TypeScript中,所有的数字都是用浮点数表示的,这些数字的类型就是number:
var age:number = 36;
2.4.3 布尔类型
布尔类型(boolean)以true(真)和false(假)为值。
var married:boolean = true;
2.4.4 数组
数组用Array类型表示。然而,因为数组是一组相同数据类型的集合,所以我们还需要为数组中的条目指定一个类型。
我们可以用Array<type>或者type[]语法来为数组条目指定元素类型:
var jobs:Array<string> = ['IBM', 'Microsoft', 'Google'];
var jobs:string[] = ['Apple', 'Dell', 'HP'];
数字型数组的声明与之类似:
var jobs:Array<number> = [1, 2, 3];
var jobs:number[] = [4, 5, 6];
2.4.5 枚举
枚举是一组可命名数值的集合。比如,如果我们想拿到某人的一系列角色,可以这么写:
enum Role {Employee, Manager, Admin};
var role:Role = Role.Employee;
默认情况下,枚举类型的初始值是0。我们也可以调整初始化值的范围:
enum Role {Employee = 3, Manager, Admin};
var role:Role = Role.Employee;
在上面的代码中,Employee的初始值被设置为3而不是0。枚举中其他项的值是依次递增的,意味着Manager的值为4,Admin的值为5。同样,我们也可以单独为枚举中的每一项指定值:
enum Role {Employee = 3, Manager = 5, Admin = 7};
var role:Role = Role.Employee;
还可以从枚举的值来反查它的名称:
enum Role {Employee, Manager, Admin};
console.log('Roles:', Role[0], ',', Role[1], 'and', Role[2]);
2.4.6 任意类型
如果我们没有为变量指定类型,那它的默认类型就是any。在TypeScript中,any类型的变量能够接收任意类型的数据:
var something:any = 'as string';
something = 1;
something = [1, 2, 3];
2.4.7 “无”类型
void意味着我们不期望那里有类型。它通常用作函数的返回值,表示没有任何返回值:
function setName(name:string):void {
this.name = name;
}
JavaScript ES5采用的是基于原型的面向对象设计。这种设计模型不使用类,而是依赖于原型。
JavaScript社区采纳了大量最佳实践,以弥补JavaScript缺少类的问题。这些最佳实践已经被总结在Mozilla的开发指南中了,你还可以找到一篇关于JavaScript面向对象设计的优秀概述
。
不过,在ES6中,我们终于有内置的类了。
用class关键字来定义一个类,紧随其后的是类名和类的代码块:
class Vehicle {
}
类可以包含属性、方法以及构造函数。
2.5.1 属性
属性定义了类实例对象的数据。比如名叫Person的类可能有first_name、last_name和age属性。
类中的每个属性都可以包含一个可选的类型。比如,我们可以把first_name和last_name声明为字符串类型(string),把age声明为数字类型(number)。
Person类的声明是这样的:
class Person {
first_name:string;
last_name:string;
age:number;
}
2.5.2 方法
方法是运行在类对象实例上下文中的函数。在调用对象的方法之前,必须要有这个对象的实例。
要实例化一个类,我们使用new关键字。比如new Person()会创建一个Person类的实例对象。
如果我们希望问候某个Person,就可以这样写:
class Person {
first_name:string;
last_name:string;
age:number;
greet(){
console.log("Hello", this.first_name);
}
}
注意,借助this关键字,我们能用this.first_name表达式来访问Person类的first_name属性。
如果没有显式声明过方法的返回类型和返回值,就会假定它可能返回任何东西(即any类型)。然而,因为这里没有任何显式的return语句,所以实际返回的类型是void。
注意,void类型也是一种合法的any类型。
调用greet方法之前,我们要有一个Person类的实例对象。代码如下:
// declare a variable of type Person
var p:Person;
// instantiate a new Person instance
p = new Person();
// give it a first_name
p.first_name = 'Felipe';
// call the greet method
p.greet();
我们还可以将对象的声明和实例化缩写为一行代码:
var p:Person = new Person();
假设我们希望Person类有一个带返回值的方法。比如,要获取某个Person在数年后的年龄,我们可以这样写:
class Person {
first_name:string;
last_name:string;
age:number;
greet(){
console.log("Hello", this.first_name);
}
ageInYears(years:number):number {
return this.age + years;
}
}
// instantiate a new Person instance
var p:Person = new Person();
// set initial age
p.age = 6;
// how old will he be in 12 years?
p.ageInYears(12);
// -> 18
2.5.3 构造函数
构造函数是当类进行实例化时执行的特殊函数。通常会在构造函数中对新对象进行初始化工作。
构造函数必须命名为constructor。因为构造函数是在类被实例化时调用的,所以它们可以有输入参数,但不能有任何返回值。
我们要通过调用new ClassName()来执行构造函数,以完成类的实例化。
当类没有显式地定义构造函数时,将自动创建一个无参构造函数:
class Vehicle {
}
var v = new Vehicle();
它等价于:
class Vehicle {
constructor(){
}
}
var v = new Vehicle();
在TypeScript中,每个类只能有一个构造函数。
这是违背ES6标准的。在ES6中,一个类可以拥有不同参数数量的多个构造函数重载实现。
我们可以使用带参数的构造函数来将对象的创建工作参数化。
比如,我们可以对Person类使用构造函数来初始化它的数据:
class Person {
first_name:string;
last_name:string;
age:number;
constructor(first_name:string, last_name:string, age:number){
this.first_name = first_name;
this.last_name = last_name;
this.age = age;
}
greet(){
console.log("Hello", this.first_name);
}
ageInYears(years:number):number {
return this.age + years;
}
}
用下面这种方法重写前面的例子要容易些:
var p:Person = new Person('Felipe', 'Coury', 36);
p.greet();
当创建这个对象的时候,其姓名、年龄都会被初始化。
2.5.4 继承
面向对象的另一个重要特性就是继承。继承表明子类能够从父类得到它的行为。然后,我们就可以在这个子类中重写、修改以及添加行为。
如果要深入了解ES5的继承是如何工作的,可以参考Mozilla开发文档中的文章“Inheritance and the prototype chain”
。
TypeScript是完全支持继承特性的,并不像ES5那样要靠原型链实现。继承是TypeScript的核心语法,用extends关键字实现。
要说明这一点,我们来创建一个Report类:
class Report {
data:Array<string>;
constructor(data:Array<string>){
this.data = data;
}
run(){
this.data.forEach(function(line){ console.log(line); });
}
}
这个Report类有一个字符串数组类型的data的属性。当我们调用run方法时,它会循环这个data数组中的每一项数据,然后用console.log打印出来。
.forEach是Array中的一个方法,它接收一个函数作为参数,并对数组中的每一个条目逐个调用该函数。
给Report增加几行数据,并调用run把这些数据打印到控制台:
var r:Report = new Report(['First line', 'Second line']);
r.run();
运行结果如下:
First line
Second line
现在,假设我们希望有第二个报表,它需要增加一些头信息和数据,但我们仍想复用现有Report类的run方法来向用户展示数据。
为了复用Report类的行为,要使用extends关键字来继承它:
class TabbedReport extends Report {
headers:Array<string>;
constructor(headers:string[], values:string[]){
super(values)
this.headers = headers;
}
run(){
console.log(this.headers);
super.run();
}
}
var headers:string[] = ['Name'];
var data:string[] = ['Alice Green', 'Paul Pfifer', 'Louis Blakenship'];
var r:TabbedReport = new TabbedReport(headers, data)
r.run();
ES6和TypeScript提供了许多语法特性,让编码成为一种享受。其中最重要的两点是:
●胖箭头函数语法
●模板字符串
2.6.1 胖箭头函数
胖箭头(=>)函数是一种快速书写函数的简洁语法。
在ES5中,每当我们要用函数作为方法参数时,都必须用function关键字和紧随其后的花括号({})表示。就像这样:
// ES5-like example
var data = ['Alice Green', 'Paul Pfifer', 'Louis Blakenship'];
data.forEach(function(line){ console.log(line); });
现在我们可以用=>语法来重写它了:
// Typescript example
var data:string[] = ['Alice Green', 'Paul Pfifer', 'Louis Blakenship'];
data.forEach((line)=> console.log(line));
当只有一个参数时,圆括号可以省略。箭头(=>)语法可以用作表达式:
var evens = [2,4,6,8];
var odds = evens.map(v => v + 1);
也可以用作语句:
data.forEach(line => {
console.log(line.toUpperCase())
});
=>语法还有一个重要的特性,就是它和环绕它的外部代码共享同一个this。这是它和普通function写法最重要的不同点。通常,我们用function声明的函数有它自己的this。有时在JavaScript中能看见如下代码:
var nate = {
name:"Nate",
guitars:["Gibson", "Martin", "Taylor"],
printGuitars:function(){
var self = this;
this.guitars.forEach(function(g){
// this.name is undefined so we have to use self.name
console.log(self.name + " plays a " + g);
});
}
};
由于胖箭头会共享环绕它的外部代码的this,我们可以这样改写:
var nate = {
name:"Nate",
guitars:["Gibson", "Martin", "Taylor"],
printGuitars:function(){
this.guitars.forEach((g)=> {
console.log(this.name + " plays a " + g);
});
}
};
可见,箭头函数是处理内联函数的好办法。这也让我们在JavaScript中更容易使用高阶函数。
2.6.2 模板字符串
ES6引入了新的模板字符串语法,它有两大优势:
(1)可以在模板字符串中使用变量(不必被迫使用+来拼接字符串);
(2)支持多行字符串。
●字符串中的变量
这种特性也叫字符串插值(string interpolation)。你可以在字符串中插入变量,做法如下:
var firstName = "Nate";
var lastName = "Murray";
// interpolate a string
var greeting = `Hello ${firstName} ${lastName}`;
console.log(greeting);
注意,字符串插值必须使用反引号,不能用单引号或双引号。
●多行字符串
反引号字符串的另一个优点是允许多行文本:
var template = `
<div>
<h1>Hello</h1>
<p>This is a great website</p>
</div>
`
// do something with `template`
当我们要插入模板这样的长文本字符串时,多行字符串会非常有帮助。
在TypeScript和ES6中还有很多其他的优秀语法特性,如:
●接口
●泛型
●模块的导入、导出
●标注
●解构
我们会在本书的后续章节中讲到这些概念并使用它们。目前,本章的这些基本知识已经足够你开始学习Angular了。
言归正传,让我们回到Angular吧!
Angular是用一种类似于JavaScript的语言——TypeScript——构建的。
或许你会对用新语言来开发Angular心存疑虑,但事实上,在开发Angular应用时,我们有充分的理由用TypeScript代替普通的JavaScript。
TypeScript并不是一门全新的语言,而是ES6的超集。所有的ES6代码都是完全有效且可编译的TypeScript代码。图2-1展示了它们之间的关系。
图2-1 ES5、ES6和TypeScript
什么是ES5?什么是ES6?ES5是ECMAScript 5的缩写,也被称为“普通的JavaScript”。ES5就是大家熟知的JavaScript,它能够运行在大部分浏览器上。ES6则是下一个版本的JavaScript,在后续章节中我们还会深入讨论它。
在本书出版的时候,支持ES6的浏览器还很少,更不用说TypeScript了。我们用转译器来解决这个问题。TypeScript转译器能把TypeScript代码转换为几乎所有浏览器都支持的ES5代码。
从TypeScript代码到ES5代码的唯一转换器是由TypeScript核心团队编写的。然而,将ES6代码(不是TypeScript代码)转换到ES5代码则有两个主要的转换器:Google开发的Traceur与JavaScript社区创建的Babel
。在本书中我们并不会直接使用它们,但它们也是值得了解的不错项目。
我们在上一章安装了TypeScript环境,如果你是从本章开始学习的,那么可以这样安装TypeScript环境:npm install -g typescript。
TypeScript是Microsoft和Google之间的官方合作项目。有这两家强有力的科技巨头在背后支撑,对于我们来说是个好消息,因为这表示TypeScript将会得到长期的支持。这两家公司都承诺全力推动Web技术的发展,我们这些开发人员显然会获益匪浅。
另外,转译器的好处还在于:它允许小型团队对语言进行改善,而不必要求所有人都去升级他们的浏览器。
需要指出的是:TypeScript并不是开发Angular应用的必选语言。我们同样可以使用ES5代码(即“普通”JavaScript)来开发Angular应用。Angular也为全部功能提供了ES5 API。那么为什么我们还要使用TypeScript呢?这是因为TypeScript有不少强大的功能,能极大地简化开发。
TypeScript相对于ES5有五大改善:
●类型
●类
●注解
●模块导入
●语言工具包(比如,解构)
接下来我们逐个介绍。
顾名思义,相对于ES6,TypeScript最大的改善是增加了类型系统。
有些人可能会觉得,缺乏类型检查正是JavaScript这些弱类型语言的优点。也许你对类型检查心存疑虑,但我仍然鼓励你试一试。类型检查的好处有:
(1)有助于代码的编写,因为它可以在编译期预防bug;
(2)有助于代码的阅读,因为它能清晰地表明你的意图。
另外值得一提的是,TypeScript中的类型是可选的。如果希望写一些快速代码或功能原型,可以首先省略类型,然后再随着代码日趋成熟逐渐加上类型。
TypeScript的基本类型与我们平时所写JavaScript代码中用的隐式类型一样,包括字符串、数字、布尔值等。
直到ES5,我们都在用var关键字定义变量,比如var name;。
TypeScript的新语法是从ES5自然演化而来的,仍沿用var来定义变量,但现在可以同时为变量名提供可选的变量类型了:
var name:string;
在声明函数时,也可以为函数参数和返回值指定类型:
function greetText(name:string):string {
return "Hello " + name;
}
这个例子中,我们定义了一个名为greetText的新函数,它接收一个名为name的参数。name:string语法表示函数想要的name参数是string类型。如果给该函数传一个string以外的参数,代码将无法编译通过。对我们来说,这是好事,否则这段代码将会引入bug。
或许你还注意到了,greetText函数在括号后面还有一个新语法:string {。冒号之后指定的是该函数的返回值类型,在本例中为string。这很有用,原因有二:如果不小心让函数返回了一个非string型的返回值,编译器就会告诉我们这里有错误;使用该函数的开发人员也能很清晰地知道自己将会拿到什么类型的数据。
我们来看看如果写了不符合类型声明的代码会怎样:
function hello(name:string):string {
return 12;
}
当尝试编译代码时,将会得到下列错误:
$ tsc compile-error.ts
compile-error.ts(2,12):error TS2322:Type 'number' is not assignable to type 'string'.
这是怎么回事?我们尝试返回一个number类型的12,但hello函数期望的返回值类型为string(它是在参数声明的后面以):string {的形式声明的)。
要纠正它,可以把函数的返回值类型改为number:
function hello(name:string):number {
return 12;
}
虽然这只是一个小例子,但足以证明类型检查能为我们节省大量调试bug的时间。
现在知道了如何使用类型,但怎么才能知道有哪些可用类型呢?接下来我们就会罗列出这些内置的类型,并教你如何创建自己的类型。
尝试REPL
为了运行本章中的例子,我们要先安装一个小工具,名为TSUN(TypeScript Upgraded Node,支持TypeScript的升级版Node):
$ npm install -g tsun
接着启动它:
$ tsun
TSUN:TypeScript Upgraded Node
type in TypeScript expression to evaluate
type:help for commands in repl
>
这个小小的>是一个命令提示符,表示TSUN已经准备好接收命令了。
对于本章后面的大部分例子,你都可以复制粘贴到这个终端窗口中运行。
2.4.1 字符串
字符串包含文本,声明为string类型:
var name:string = 'Felipe';
2.4.2 数字
无论整数还是浮点,任何类型的数字都属于number类型。在TypeScript中,所有的数字都是用浮点数表示的,这些数字的类型就是number:
var age:number = 36;
2.4.3 布尔类型
布尔类型(boolean)以true(真)和false(假)为值。
var married:boolean = true;
2.4.4 数组
数组用Array类型表示。然而,因为数组是一组相同数据类型的集合,所以我们还需要为数组中的条目指定一个类型。
我们可以用Array<type>或者type[]语法来为数组条目指定元素类型:
var jobs:Array<string> = ['IBM', 'Microsoft', 'Google'];
var jobs:string[] = ['Apple', 'Dell', 'HP'];
数字型数组的声明与之类似:
var jobs:Array<number> = [1, 2, 3];
var jobs:number[] = [4, 5, 6];
2.4.5 枚举
枚举是一组可命名数值的集合。比如,如果我们想拿到某人的一系列角色,可以这么写:
enum Role {Employee, Manager, Admin};
var role:Role = Role.Employee;
默认情况下,枚举类型的初始值是0。我们也可以调整初始化值的范围:
enum Role {Employee = 3, Manager, Admin};
var role:Role = Role.Employee;
在上面的代码中,Employee的初始值被设置为3而不是0。枚举中其他项的值是依次递增的,意味着Manager的值为4,Admin的值为5。同样,我们也可以单独为枚举中的每一项指定值:
enum Role {Employee = 3, Manager = 5, Admin = 7};
var role:Role = Role.Employee;
还可以从枚举的值来反查它的名称:
enum Role {Employee, Manager, Admin};
console.log('Roles:', Role[0], ',', Role[1], 'and', Role[2]);
2.4.6 任意类型
如果我们没有为变量指定类型,那它的默认类型就是any。在TypeScript中,any类型的变量能够接收任意类型的数据:
var something:any = 'as string';
something = 1;
something = [1, 2, 3];
2.4.7 “无”类型
void意味着我们不期望那里有类型。它通常用作函数的返回值,表示没有任何返回值:
function setName(name:string):void {
this.name = name;
}
JavaScript ES5采用的是基于原型的面向对象设计。这种设计模型不使用类,而是依赖于原型。
JavaScript社区采纳了大量最佳实践,以弥补JavaScript缺少类的问题。这些最佳实践已经被总结在Mozilla的开发指南中了,你还可以找到一篇关于JavaScript面向对象设计的优秀概述
。
不过,在ES6中,我们终于有内置的类了。
用class关键字来定义一个类,紧随其后的是类名和类的代码块:
class Vehicle {
}
类可以包含属性、方法以及构造函数。
2.5.1 属性
属性定义了类实例对象的数据。比如名叫Person的类可能有first_name、last_name和age属性。
类中的每个属性都可以包含一个可选的类型。比如,我们可以把first_name和last_name声明为字符串类型(string),把age声明为数字类型(number)。
Person类的声明是这样的:
class Person {
first_name:string;
last_name:string;
age:number;
}
2.5.2 方法
方法是运行在类对象实例上下文中的函数。在调用对象的方法之前,必须要有这个对象的实例。
要实例化一个类,我们使用new关键字。比如new Person()会创建一个Person类的实例对象。
如果我们希望问候某个Person,就可以这样写:
class Person {
first_name:string;
last_name:string;
age:number;
greet(){
console.log("Hello", this.first_name);
}
}
注意,借助this关键字,我们能用this.first_name表达式来访问Person类的first_name属性。
如果没有显式声明过方法的返回类型和返回值,就会假定它可能返回任何东西(即any类型)。然而,因为这里没有任何显式的return语句,所以实际返回的类型是void。
注意,void类型也是一种合法的any类型。
调用greet方法之前,我们要有一个Person类的实例对象。代码如下:
// declare a variable of type Person
var p:Person;
// instantiate a new Person instance
p = new Person();
// give it a first_name
p.first_name = 'Felipe';
// call the greet method
p.greet();
我们还可以将对象的声明和实例化缩写为一行代码:
var p:Person = new Person();
假设我们希望Person类有一个带返回值的方法。比如,要获取某个Person在数年后的年龄,我们可以这样写:
class Person {
first_name:string;
last_name:string;
age:number;
greet(){
console.log("Hello", this.first_name);
}
ageInYears(years:number):number {
return this.age + years;
}
}
// instantiate a new Person instance
var p:Person = new Person();
// set initial age
p.age = 6;
// how old will he be in 12 years?
p.ageInYears(12);
// -> 18
2.5.3 构造函数
构造函数是当类进行实例化时执行的特殊函数。通常会在构造函数中对新对象进行初始化工作。
构造函数必须命名为constructor。因为构造函数是在类被实例化时调用的,所以它们可以有输入参数,但不能有任何返回值。
我们要通过调用new ClassName()来执行构造函数,以完成类的实例化。
当类没有显式地定义构造函数时,将自动创建一个无参构造函数:
class Vehicle {
}
var v = new Vehicle();
它等价于:
class Vehicle {
constructor(){
}
}
var v = new Vehicle();
在TypeScript中,每个类只能有一个构造函数。
这是违背ES6标准的。在ES6中,一个类可以拥有不同参数数量的多个构造函数重载实现。
我们可以使用带参数的构造函数来将对象的创建工作参数化。
比如,我们可以对Person类使用构造函数来初始化它的数据:
class Person {
first_name:string;
last_name:string;
age:number;
constructor(first_name:string, last_name:string, age:number){
this.first_name = first_name;
this.last_name = last_name;
this.age = age;
}
greet(){
console.log("Hello", this.first_name);
}
ageInYears(years:number):number {
return this.age + years;
}
}
用下面这种方法重写前面的例子要容易些:
var p:Person = new Person('Felipe', 'Coury', 36);
p.greet();
当创建这个对象的时候,其姓名、年龄都会被初始化。
2.5.4 继承
面向对象的另一个重要特性就是继承。继承表明子类能够从父类得到它的行为。然后,我们就可以在这个子类中重写、修改以及添加行为。
如果要深入了解ES5的继承是如何工作的,可以参考Mozilla开发文档中的文章“Inheritance and the prototype chain”
。
TypeScript是完全支持继承特性的,并不像ES5那样要靠原型链实现。继承是TypeScript的核心语法,用extends关键字实现。
要说明这一点,我们来创建一个Report类:
class Report {
data:Array<string>;
constructor(data:Array<string>){
this.data = data;
}
run(){
this.data.forEach(function(line){ console.log(line); });
}
}
这个Report类有一个字符串数组类型的data的属性。当我们调用run方法时,它会循环这个data数组中的每一项数据,然后用console.log打印出来。
.forEach是Array中的一个方法,它接收一个函数作为参数,并对数组中的每一个条目逐个调用该函数。
给Report增加几行数据,并调用run把这些数据打印到控制台:
var r:Report = new Report(['First line', 'Second line']);
r.run();
运行结果如下:
First line
Second line
现在,假设我们希望有第二个报表,它需要增加一些头信息和数据,但我们仍想复用现有Report类的run方法来向用户展示数据。
为了复用Report类的行为,要使用extends关键字来继承它:
class TabbedReport extends Report {
headers:Array<string>;
constructor(headers:string[], values:string[]){
super(values)
this.headers = headers;
}
run(){
console.log(this.headers);
super.run();
}
}
var headers:string[] = ['Name'];
var data:string[] = ['Alice Green', 'Paul Pfifer', 'Louis Blakenship'];
var r:TabbedReport = new TabbedReport(headers, data)
r.run();
ES6和TypeScript提供了许多语法特性,让编码成为一种享受。其中最重要的两点是:
●胖箭头函数语法
●模板字符串
2.6.1 胖箭头函数
胖箭头(=>)函数是一种快速书写函数的简洁语法。
在ES5中,每当我们要用函数作为方法参数时,都必须用function关键字和紧随其后的花括号({})表示。就像这样:
// ES5-like example
var data = ['Alice Green', 'Paul Pfifer', 'Louis Blakenship'];
data.forEach(function(line){ console.log(line); });
现在我们可以用=>语法来重写它了:
// Typescript example
var data:string[] = ['Alice Green', 'Paul Pfifer', 'Louis Blakenship'];
data.forEach((line)=> console.log(line));
当只有一个参数时,圆括号可以省略。箭头(=>)语法可以用作表达式:
var evens = [2,4,6,8];
var odds = evens.map(v => v + 1);
也可以用作语句:
data.forEach(line => {
console.log(line.toUpperCase())
});
=>语法还有一个重要的特性,就是它和环绕它的外部代码共享同一个this。这是它和普通function写法最重要的不同点。通常,我们用function声明的函数有它自己的this。有时在JavaScript中能看见如下代码:
var nate = {
name:"Nate",
guitars:["Gibson", "Martin", "Taylor"],
printGuitars:function(){
var self = this;
this.guitars.forEach(function(g){
// this.name is undefined so we have to use self.name
console.log(self.name + " plays a " + g);
});
}
};
由于胖箭头会共享环绕它的外部代码的this,我们可以这样改写:
var nate = {
name:"Nate",
guitars:["Gibson", "Martin", "Taylor"],
printGuitars:function(){
this.guitars.forEach((g)=> {
console.log(this.name + " plays a " + g);
});
}
};
可见,箭头函数是处理内联函数的好办法。这也让我们在JavaScript中更容易使用高阶函数。
2.6.2 模板字符串
ES6引入了新的模板字符串语法,它有两大优势:
(1)可以在模板字符串中使用变量(不必被迫使用+来拼接字符串);
(2)支持多行字符串。
●字符串中的变量
这种特性也叫字符串插值(string interpolation)。你可以在字符串中插入变量,做法如下:
var firstName = "Nate";
var lastName = "Murray";
// interpolate a string
var greeting = `Hello ${firstName} ${lastName}`;
console.log(greeting);
注意,字符串插值必须使用反引号,不能用单引号或双引号。
●多行字符串
反引号字符串的另一个优点是允许多行文本:
var template = `
<div>
<h1>Hello</h1>
<p>This is a great website</p>
</div>
`
// do something with `template`
当我们要插入模板这样的长文本字符串时,多行字符串会非常有帮助。
在TypeScript和ES6中还有很多其他的优秀语法特性,如:
●接口
●泛型
●模块的导入、导出
●标注
●解构
我们会在本书的后续章节中讲到这些概念并使用它们。目前,本章的这些基本知识已经足够你开始学习Angular了。
言归正传,让我们回到Angular吧!
Angular是用一种类似于JavaScript的语言——TypeScript——构建的。
或许你会对用新语言来开发Angular心存疑虑,但事实上,在开发Angular应用时,我们有充分的理由用TypeScript代替普通的JavaScript。
TypeScript并不是一门全新的语言,而是ES6的超集。所有的ES6代码都是完全有效且可编译的TypeScript代码。图2-1展示了它们之间的关系。
图2-1 ES5、ES6和TypeScript
什么是ES5?什么是ES6?ES5是ECMAScript 5的缩写,也被称为“普通的JavaScript”。ES5就是大家熟知的JavaScript,它能够运行在大部分浏览器上。ES6则是下一个版本的JavaScript,在后续章节中我们还会深入讨论它。
在本书出版的时候,支持ES6的浏览器还很少,更不用说TypeScript了。我们用转译器来解决这个问题。TypeScript转译器能把TypeScript代码转换为几乎所有浏览器都支持的ES5代码。
从TypeScript代码到ES5代码的唯一转换器是由TypeScript核心团队编写的。然而,将ES6代码(不是TypeScript代码)转换到ES5代码则有两个主要的转换器:Google开发的Traceur与JavaScript社区创建的Babel
。在本书中我们并不会直接使用它们,但它们也是值得了解的不错项目。
我们在上一章安装了TypeScript环境,如果你是从本章开始学习的,那么可以这样安装TypeScript环境:npm install -g typescript。
TypeScript是Microsoft和Google之间的官方合作项目。有这两家强有力的科技巨头在背后支撑,对于我们来说是个好消息,因为这表示TypeScript将会得到长期的支持。这两家公司都承诺全力推动Web技术的发展,我们这些开发人员显然会获益匪浅。
另外,转译器的好处还在于:它允许小型团队对语言进行改善,而不必要求所有人都去升级他们的浏览器。
需要指出的是:TypeScript并不是开发Angular应用的必选语言。我们同样可以使用ES5代码(即“普通”JavaScript)来开发Angular应用。Angular也为全部功能提供了ES5 API。那么为什么我们还要使用TypeScript呢?这是因为TypeScript有不少强大的功能,能极大地简化开发。
TypeScript相对于ES5有五大改善:
●类型
●类
●注解
●模块导入
●语言工具包(比如,解构)
接下来我们逐个介绍。
顾名思义,相对于ES6,TypeScript最大的改善是增加了类型系统。
有些人可能会觉得,缺乏类型检查正是JavaScript这些弱类型语言的优点。也许你对类型检查心存疑虑,但我仍然鼓励你试一试。类型检查的好处有:
(1)有助于代码的编写,因为它可以在编译期预防bug;
(2)有助于代码的阅读,因为它能清晰地表明你的意图。
另外值得一提的是,TypeScript中的类型是可选的。如果希望写一些快速代码或功能原型,可以首先省略类型,然后再随着代码日趋成熟逐渐加上类型。
TypeScript的基本类型与我们平时所写JavaScript代码中用的隐式类型一样,包括字符串、数字、布尔值等。
直到ES5,我们都在用var关键字定义变量,比如var name;。
TypeScript的新语法是从ES5自然演化而来的,仍沿用var来定义变量,但现在可以同时为变量名提供可选的变量类型了:
var name:string;
在声明函数时,也可以为函数参数和返回值指定类型:
function greetText(name:string):string {
return "Hello " + name;
}
这个例子中,我们定义了一个名为greetText的新函数,它接收一个名为name的参数。name:string语法表示函数想要的name参数是string类型。如果给该函数传一个string以外的参数,代码将无法编译通过。对我们来说,这是好事,否则这段代码将会引入bug。
或许你还注意到了,greetText函数在括号后面还有一个新语法:string {。冒号之后指定的是该函数的返回值类型,在本例中为string。这很有用,原因有二:如果不小心让函数返回了一个非string型的返回值,编译器就会告诉我们这里有错误;使用该函数的开发人员也能很清晰地知道自己将会拿到什么类型的数据。
我们来看看如果写了不符合类型声明的代码会怎样:
function hello(name:string):string {
return 12;
}
当尝试编译代码时,将会得到下列错误:
$ tsc compile-error.ts
compile-error.ts(2,12):error TS2322:Type 'number' is not assignable to type 'string'.
这是怎么回事?我们尝试返回一个number类型的12,但hello函数期望的返回值类型为string(它是在参数声明的后面以):string {的形式声明的)。
要纠正它,可以把函数的返回值类型改为number:
function hello(name:string):number {
return 12;
}
虽然这只是一个小例子,但足以证明类型检查能为我们节省大量调试bug的时间。
现在知道了如何使用类型,但怎么才能知道有哪些可用类型呢?接下来我们就会罗列出这些内置的类型,并教你如何创建自己的类型。
尝试REPL
为了运行本章中的例子,我们要先安装一个小工具,名为TSUN(TypeScript Upgraded Node,支持TypeScript的升级版Node):
$ npm install -g tsun
接着启动它:
$ tsun
TSUN:TypeScript Upgraded Node
type in TypeScript expression to evaluate
type:help for commands in repl
>
这个小小的>是一个命令提示符,表示TSUN已经准备好接收命令了。
对于本章后面的大部分例子,你都可以复制粘贴到这个终端窗口中运行。
2.4.1 字符串
字符串包含文本,声明为string类型:
var name:string = 'Felipe';
2.4.2 数字
无论整数还是浮点,任何类型的数字都属于number类型。在TypeScript中,所有的数字都是用浮点数表示的,这些数字的类型就是number:
var age:number = 36;
2.4.3 布尔类型
布尔类型(boolean)以true(真)和false(假)为值。
var married:boolean = true;
2.4.4 数组
数组用Array类型表示。然而,因为数组是一组相同数据类型的集合,所以我们还需要为数组中的条目指定一个类型。
我们可以用Array<type>或者type[]语法来为数组条目指定元素类型:
var jobs:Array<string> = ['IBM', 'Microsoft', 'Google'];
var jobs:string[] = ['Apple', 'Dell', 'HP'];
数字型数组的声明与之类似:
var jobs:Array<number> = [1, 2, 3];
var jobs:number[] = [4, 5, 6];
2.4.5 枚举
枚举是一组可命名数值的集合。比如,如果我们想拿到某人的一系列角色,可以这么写:
enum Role {Employee, Manager, Admin};
var role:Role = Role.Employee;
默认情况下,枚举类型的初始值是0。我们也可以调整初始化值的范围:
enum Role {Employee = 3, Manager, Admin};
var role:Role = Role.Employee;
在上面的代码中,Employee的初始值被设置为3而不是0。枚举中其他项的值是依次递增的,意味着Manager的值为4,Admin的值为5。同样,我们也可以单独为枚举中的每一项指定值:
enum Role {Employee = 3, Manager = 5, Admin = 7};
var role:Role = Role.Employee;
还可以从枚举的值来反查它的名称:
enum Role {Employee, Manager, Admin};
console.log('Roles:', Role[0], ',', Role[1], 'and', Role[2]);
2.4.6 任意类型
如果我们没有为变量指定类型,那它的默认类型就是any。在TypeScript中,any类型的变量能够接收任意类型的数据:
var something:any = 'as string';
something = 1;
something = [1, 2, 3];
2.4.7 “无”类型
void意味着我们不期望那里有类型。它通常用作函数的返回值,表示没有任何返回值:
function setName(name:string):void {
this.name = name;
}
JavaScript ES5采用的是基于原型的面向对象设计。这种设计模型不使用类,而是依赖于原型。
JavaScript社区采纳了大量最佳实践,以弥补JavaScript缺少类的问题。这些最佳实践已经被总结在Mozilla的开发指南中了,你还可以找到一篇关于JavaScript面向对象设计的优秀概述
。
不过,在ES6中,我们终于有内置的类了。
用class关键字来定义一个类,紧随其后的是类名和类的代码块:
class Vehicle {
}
类可以包含属性、方法以及构造函数。
2.5.1 属性
属性定义了类实例对象的数据。比如名叫Person的类可能有first_name、last_name和age属性。
类中的每个属性都可以包含一个可选的类型。比如,我们可以把first_name和last_name声明为字符串类型(string),把age声明为数字类型(number)。
Person类的声明是这样的:
class Person {
first_name:string;
last_name:string;
age:number;
}
2.5.2 方法
方法是运行在类对象实例上下文中的函数。在调用对象的方法之前,必须要有这个对象的实例。
要实例化一个类,我们使用new关键字。比如new Person()会创建一个Person类的实例对象。
如果我们希望问候某个Person,就可以这样写:
class Person {
first_name:string;
last_name:string;
age:number;
greet(){
console.log("Hello", this.first_name);
}
}
注意,借助this关键字,我们能用this.first_name表达式来访问Person类的first_name属性。
如果没有显式声明过方法的返回类型和返回值,就会假定它可能返回任何东西(即any类型)。然而,因为这里没有任何显式的return语句,所以实际返回的类型是void。
注意,void类型也是一种合法的any类型。
调用greet方法之前,我们要有一个Person类的实例对象。代码如下:
// declare a variable of type Person
var p:Person;
// instantiate a new Person instance
p = new Person();
// give it a first_name
p.first_name = 'Felipe';
// call the greet method
p.greet();
我们还可以将对象的声明和实例化缩写为一行代码:
var p:Person = new Person();
假设我们希望Person类有一个带返回值的方法。比如,要获取某个Person在数年后的年龄,我们可以这样写:
class Person {
first_name:string;
last_name:string;
age:number;
greet(){
console.log("Hello", this.first_name);
}
ageInYears(years:number):number {
return this.age + years;
}
}
// instantiate a new Person instance
var p:Person = new Person();
// set initial age
p.age = 6;
// how old will he be in 12 years?
p.ageInYears(12);
// -> 18
2.5.3 构造函数
构造函数是当类进行实例化时执行的特殊函数。通常会在构造函数中对新对象进行初始化工作。
构造函数必须命名为constructor。因为构造函数是在类被实例化时调用的,所以它们可以有输入参数,但不能有任何返回值。
我们要通过调用new ClassName()来执行构造函数,以完成类的实例化。
当类没有显式地定义构造函数时,将自动创建一个无参构造函数:
class Vehicle {
}
var v = new Vehicle();
它等价于:
class Vehicle {
constructor(){
}
}
var v = new Vehicle();
在TypeScript中,每个类只能有一个构造函数。
这是违背ES6标准的。在ES6中,一个类可以拥有不同参数数量的多个构造函数重载实现。
我们可以使用带参数的构造函数来将对象的创建工作参数化。
比如,我们可以对Person类使用构造函数来初始化它的数据:
class Person {
first_name:string;
last_name:string;
age:number;
constructor(first_name:string, last_name:string, age:number){
this.first_name = first_name;
this.last_name = last_name;
this.age = age;
}
greet(){
console.log("Hello", this.first_name);
}
ageInYears(years:number):number {
return this.age + years;
}
}
用下面这种方法重写前面的例子要容易些:
var p:Person = new Person('Felipe', 'Coury', 36);
p.greet();
当创建这个对象的时候,其姓名、年龄都会被初始化。
2.5.4 继承
面向对象的另一个重要特性就是继承。继承表明子类能够从父类得到它的行为。然后,我们就可以在这个子类中重写、修改以及添加行为。
如果要深入了解ES5的继承是如何工作的,可以参考Mozilla开发文档中的文章“Inheritance and the prototype chain”
。
TypeScript是完全支持继承特性的,并不像ES5那样要靠原型链实现。继承是TypeScript的核心语法,用extends关键字实现。
要说明这一点,我们来创建一个Report类:
class Report {
data:Array<string>;
constructor(data:Array<string>){
this.data = data;
}
run(){
this.data.forEach(function(line){ console.log(line); });
}
}
这个Report类有一个字符串数组类型的data的属性。当我们调用run方法时,它会循环这个data数组中的每一项数据,然后用console.log打印出来。
.forEach是Array中的一个方法,它接收一个函数作为参数,并对数组中的每一个条目逐个调用该函数。
给Report增加几行数据,并调用run把这些数据打印到控制台:
var r:Report = new Report(['First line', 'Second line']);
r.run();
运行结果如下:
First line
Second line
现在,假设我们希望有第二个报表,它需要增加一些头信息和数据,但我们仍想复用现有Report类的run方法来向用户展示数据。
为了复用Report类的行为,要使用extends关键字来继承它:
class TabbedReport extends Report {
headers:Array<string>;
constructor(headers:string[], values:string[]){
super(values)
this.headers = headers;
}
run(){
console.log(this.headers);
super.run();
}
}
var headers:string[] = ['Name'];
var data:string[] = ['Alice Green', 'Paul Pfifer', 'Louis Blakenship'];
var r:TabbedReport = new TabbedReport(headers, data)
r.run();
ES6和TypeScript提供了许多语法特性,让编码成为一种享受。其中最重要的两点是:
●胖箭头函数语法
●模板字符串
2.6.1 胖箭头函数
胖箭头(=>)函数是一种快速书写函数的简洁语法。
在ES5中,每当我们要用函数作为方法参数时,都必须用function关键字和紧随其后的花括号({})表示。就像这样:
// ES5-like example
var data = ['Alice Green', 'Paul Pfifer', 'Louis Blakenship'];
data.forEach(function(line){ console.log(line); });
现在我们可以用=>语法来重写它了:
// Typescript example
var data:string[] = ['Alice Green', 'Paul Pfifer', 'Louis Blakenship'];
data.forEach((line)=> console.log(line));
当只有一个参数时,圆括号可以省略。箭头(=>)语法可以用作表达式:
var evens = [2,4,6,8];
var odds = evens.map(v => v + 1);
也可以用作语句:
data.forEach(line => {
console.log(line.toUpperCase())
});
=>语法还有一个重要的特性,就是它和环绕它的外部代码共享同一个this。这是它和普通function写法最重要的不同点。通常,我们用function声明的函数有它自己的this。有时在JavaScript中能看见如下代码:
var nate = {
name:"Nate",
guitars:["Gibson", "Martin", "Taylor"],
printGuitars:function(){
var self = this;
this.guitars.forEach(function(g){
// this.name is undefined so we have to use self.name
console.log(self.name + " plays a " + g);
});
}
};
由于胖箭头会共享环绕它的外部代码的this,我们可以这样改写:
var nate = {
name:"Nate",
guitars:["Gibson", "Martin", "Taylor"],
printGuitars:function(){
this.guitars.forEach((g)=> {
console.log(this.name + " plays a " + g);
});
}
};
可见,箭头函数是处理内联函数的好办法。这也让我们在JavaScript中更容易使用高阶函数。
2.6.2 模板字符串
ES6引入了新的模板字符串语法,它有两大优势:
(1)可以在模板字符串中使用变量(不必被迫使用+来拼接字符串);
(2)支持多行字符串。
●字符串中的变量
这种特性也叫字符串插值(string interpolation)。你可以在字符串中插入变量,做法如下:
var firstName = "Nate";
var lastName = "Murray";
// interpolate a string
var greeting = `Hello ${firstName} ${lastName}`;
console.log(greeting);
注意,字符串插值必须使用反引号,不能用单引号或双引号。
●多行字符串
反引号字符串的另一个优点是允许多行文本:
var template = `
<div>
<h1>Hello</h1>
<p>This is a great website</p>
</div>
`
// do something with `template`
当我们要插入模板这样的长文本字符串时,多行字符串会非常有帮助。
在TypeScript和ES6中还有很多其他的优秀语法特性,如:
●接口
●泛型
●模块的导入、导出
●标注
●解构
我们会在本书的后续章节中讲到这些概念并使用它们。目前,本章的这些基本知识已经足够你开始学习Angular了。
言归正传,让我们回到Angular吧!
Angular提供了若干内置指令。在本章中,我们将探讨每一个内置指令并通过示例教会你如何使用它们。
内置指令是已经导入过的,你的组件可以直接使用它们。因此,不用像你自己的组件一样把它们作为指令导入进来。
如果你希望根据一个条件来决定显示或隐藏一个元素,可以使用ngIf指令。这个条件是由你传给指令的表达式的结果决定的。
如果表达式的结果返回的是一个假值,那么元素会从DOM上被移除。
下面是一些例子:
<div *ngIf="false"></div> <!—— never displayed ——>
<div *ngIf="a > b"></div> <!—— displayed if a is more than b ——>
<div *ngIf="str == 'yes'"></div> <!—— displayed if str holds the string "yes" ——>
<div *ngIf="myFunc()"></div> <!—— displayed if myFunc returns a true value ——>
如果你有AngularJS的经验,那么大概以前已经用过ngIf指令了。你可以把它当作AngularJS中ng-if的替代品。但另一方面,Angular并没有为AngularJS中的ng-show指令提供内置的替代品。那么,如果你只是想改变一个元素的CSS可见性,就应该使用ngStyle或class指令。本章稍后会介绍它们。
有时候你需要根据一个给定的条件来渲染不同的元素。
遇到这种情况时,你可能会像下面这样多次使用ngIf:
<div class="container">
<div *ngIf="myVar == 'A'">Var is A</div>
<div *ngIf="myVar == 'B'">Var is B</div>
<div *ngIf="myVar!= 'A' && myVar!= 'B'">Var is something else</div>
</div>
如你所见,当myVar的值既不是A也不是B时,代码将变得相当繁琐,其实我们真正想表达的只是一个else而已。随着我们添加的值越来越多,ngIf条件也会变得越来越复杂。
为了说明这种增长的复杂性,假设我们想要处理一个新的值C。
为了达到目的,我们不仅要添加一个使用ngIf的新元素,而且要修改最后一种情况:
<div class="container">
<div *ngIf="myVar == 'A'">Var is A</div>
<div *ngIf="myVar == 'B'">Var is B</div>
<div *ngIf="myVar == 'C'">Var is C</div>
<div *ngIf="myVar!= 'A' && myVar!= 'B' && myVar!= 'C'">Var is something else</div>
</div>
对于这种情况,Angular引入了ngSwitch指令。
如果你熟悉switch语句的话,应该会觉得似曾相识。
指令背后的思想也是一样的:对表达式进行一次求值,然后根据其结果来决定如何显示指令内的嵌套元素。
一旦有了结果,我们就可以:
●使用ngSwitchCase指令描述已知结果;
●使用ngSwitchDefault指令处理所有其他未知情况。
让我们使用这组新的指令来重写之前的例子:
<div class="container" [ngSwitch]="myVar">
<div *ngSwitchCase="'A'">Var is A</div>
<div *ngSwitchCase="'B'">Var is B</div>
<div *ngSwitchDefault>Var is something else</div>
</div>
如果想要处理新值C,只需要插入一行:
<div class="container" [ngSwitch]="myVar">
<div *ngSwitchCase="'A'">Var is A</div>
<div *ngSwitchCase="'B'">Var is B</div>
<div *ngSwitchCase="'C'">Var is C</div>
<div *ngSwitchDefault>Var is something else</div>
</div>
不需要修改默认(即备用)条件。
ngSwitchDefault元素是可选的。如果我们不用它,那么当myVar没有匹配到任何期望的值时就不会渲染任何东西。
你也可以为不同的元素声明同样的*ngSwitchCase值,这样就可以多次匹配同一个值了。例子如下:
code/built_in_directives/app/ts/ng_switch/ng_switch.ts
template:`
<h4 class="ui horizontal divider header">
Current choice is {{ choice }}
</h4>
<div class="ui raised segment">
<ul [ngSwitch]="choice">
<li *ngSwitchCase="1">First choice</li>
<li *ngSwitchCase="2">Second choice</li>
<li *ngSwitchCase="3">Third choice</li>
<li *ngSwitchCase="4">Fourth choice</li>
<li *ngSwitchCase="2">Second choice, again</li>
<li *ngSwitchDefault>Default choice</li>
</ul>
</div>
<div style="margin-top:20px;">
<button class="ui primary button"(click)="nextChoice()">
Next choice
</button>
</div>
`
在上面的例子中,当choice的值是2的时候,第2个和第5个li都会被渲染。
使用ngStyle指令,可以通过Angular表达式给特定的DOM元素设定CSS属性。
该指令最简单的用法就是[style.<cssproperty>]="value"的形式,下面是一个例子。
code/built_in_directives/app/ts/ng_style/ng_style.ts
<div [style.background-color]="'yellow'">
Uses fixed yellow background
</div>
这个代码片段就是使用ngStyle指令把CSS的background-color属性设置为字符串字面量yellow。
另一种设置固定值的方式就是使用ngStyle属性,使用键值对来设置每个属性。
code/built_in_directives/app/ts/ng_style/ng_style.ts
<div [ngStyle]="{color:'white', 'background-color':'blue'}">
Uses fixed white text on blue background
</div>
注意,在ngStyle的说明中,我们对background-color使用了单引号,但却没有对color使用。这是为什么呢?因为ngStyle的参数是一个JavaScript对象,而color是一个合法的键,不需要引号。但是在background-color中,连字符是不允许出现在对象的键名当中的,除非它是一个字符串,因此使用了引号。
通常情况下,我尽量不会对对象的键使用引号,除非不得不用。
我们在这里同时设置了color和background-color属性。
但ngStyle指令真正的能力在于使用动态值。
在这个例子中,我们定义了两个输入框。
code/built_in_directives/app/ts/ng_style/ng_style.ts
<div class="ui input">
<input type="text" name="color" value="{{color}}" #colorinput>
</div>
<div class="ui input">
<input type="text" name="fontSize" value="{{fontSize}}" #fontinput>
</div>
<button class="ui primary button"(click)="apply(colorinput.value, fontinput\
.value)">
Apply settings
</button>
然后使用它们的值来设置三个元素的CSS属性。
在第一个元素中,我们基于输入框的值来设定字体大小。
code/built_in_directives/app/ts/ng_style/ng_style.ts
<div>
<span [ngStyle]="{color:'red'}" [style.font-size.px]="fontSize">
red text
</span>
</div>
注意,我们在某些情况下必须指定单位。例如,把font-size设置为12不是合法的CSS,必须指定一个单位,比如12px或者1.2em。Angular提供了一个便捷语法用来指定单位:这里我们使用的格式是[style.fontSize.px]。
后缀.px表明我们设置font-size属性值以像素为单位。你完全可以把它替换为[style.font-size.em],以相对长度为单位来表示字体大小;还可以使用[style.fontSize.%],以百分比为单位。
另外两个元素使用#colorinput的值来设置文字颜色和背景颜色。
code/built_in_directives/app/ts/ng_style/ng_style.ts
<h4 class="ui horizontal divider header">
ngStyle with object property from variable
</h4>
<div>
<span [ngStyle]="{color:color}">
{{ color }} text
</span>
</div>
<h4 class="ui horizontal divider header">
style from variable
</h4>
<div [style.background-color]="color"
style="color:white;">
{{ color }} background
</div>
这样,当我们点击Apply settings按钮时,就会调用方法来设置新的值。
code/built_in_directives/app/ts/ng_style/ng_style.ts
apply(color:string, fontSize:number){
this.color = color;
this.fontSize = fontSize;
}
与此同时,文本颜色和字体大小都通过NgStyle指令作用在元素上了。
ngClass指令在HTML模板中用ngClass属性来表示,让你能动态设置和改变一个给定DOM元素的CSS类。
如果你用过AngularJS,会发现ngClass指令和过去在AngularJS中的ngClass所做的事是非常相似的。
使用这个指令的第一种方式是传入一个对象字面量。该对象希望以类名作为键,而值应该是一个用来表明是否应该应用该类的真/假值。
假设我们有一个叫作bordered的CSS类,用来给元素添加一个黑色虚线边框。
code/built_in_directives/app/css/styles.scss
.bordered {
border:1px dashed black;
background-color:#eee;
}
我们来添加两个div元素:一个一直都有bordered类(因此一直有边框),而另一个永远都不会有。
code/built_in_directives/app/ts/ng_class/ng_class.ts
<div [ngClass]="ordered:false">This is never bordered</div>
<div [ngClass]="ordered:true">This is always bordered</div>
如预期一样,两个div应该是如图4-1这样渲染的。
图4-1 ngClass指令的简单用法
当然,使用ngClass指令来动态分配类会有用得多。
为了动态使用它,我们添加了一个变量作为对象的值:
code/built_in_directives/app/ts/ng_class/ng_class.ts
<div [ngClass]="ordered:isBordered">
Using object literal.Border {{ isBordered ? "ON":"OFF" }}
</div>
或者在组件中定义该对象:
code/built_in_directives/app/ts/ng_class/ng_class.ts
export class NgClassSampleApp {
isBordered:boolean;
classesObj:Object;
classList:string[];
并直接使用它:
code/built_in_directives/app/ts/ng_class/ng_class.ts
<div [ngClass]="classesObj">
Using object var.Border {{ classesObj.bordered ? "ON":"OFF" }}
</div>
再次强调,当你使用像bordered-box这种包含连字符的类名时需要小心。JavaScript对象不允许字面量的键出现连字符。如果确实需要,那就必须像这样用字符串作为键:
<div [ngClass]="{'bordered-box':false}">……</div>
我们也可以使用一个类名列表来指定哪些类名会被添加到元素上。为此,我们可以传入一个数组型字面量:
code/built_in_directives/app/ts/ng_class/ng_class.ts
<div class="base" [ngClass]="['blue', 'round']">
This will always have a blue background and
round corners
</div>
或者在组件中声明一个数组对象:
this.classList = ['blue', 'round'];
并把它传进来:
code/built_in_directives/app/ts/ng_class/ng_class.ts
<div class="base" [ngClass]="classList">
This is {{ classList.indexOf('blue')> -1 ? "":"NOT" }} blue
and {{ classList.indexOf('round')> -1 ? "":"NOT" }} round
</div>
在上个例子中,[ngClass]分配的类名和通过HTML的class属性分配的已存在类名都是生效的。
最后添加到元素的类总会是HTML属性class中的类和[ngClass]指令求值结果得到的类的集合。
在这个例子中:
code/built_in_directives/app/ts/ng_class/ng_class.ts
<div class="base" [ngClass]="['blue', 'round']">
This will always have a blue background and
round corners
</div>
元素有全部的三个类:HTML的class属性提供的base,以及通过[ngClass]分配的blue和round(如图4-2所示)。
图4-2 来自属性和指令的CSS类
这个指令的任务是重复一个给定的DOM元素(或一组DOM元素),每次重复都会从数组中取一个不同的值。
这个指令是AngularJS中ng-repeat的继任者。
它的语法是*ngFor="let item of items"。
●let item语法指定一个用来接收items数组中每个元素的(模板)变量。
●items是来自组件控制器的一组项的集合。
要阐明这一点,我们来看一下代码示例。我们在组件控制器中声明了一个城市的数组:
this.cities = ['Miami', 'Sao Paulo', 'New York'];
然后在模板中有如下的HTML片段。
code/built_in_directives/app/ts/ng_for/ng_for.ts
<h4 class="ui horizontal divider header">
Simple list of strings
</h4>
<div class="ui list" *ngFor="let c of cities">
<div class="item">{{ c }}</div>
</div>
它会如你期望的那样在div中渲染每一个城市,如图4-3所示。
图4-3 使用ngFor指令的结果
我们还可以这样迭代一个对象数组。
code/built_in_directives/app/ts/ng_for/ng_for.ts
this.people = [
{ name:'Anderson', age:35, city:'Sao Paulo' },
{ name:'John', age:12, city:'Miami' },
{ name:'Peter', age:22, city:'New York' }
];
然后根据每一行数据渲染出一个表格。
code/built_in_directives/app/ts/ng_for/ng_for.ts
<h4 class="ui horizontal divider header">
List of objects
</h4>
<table class="ui celled table">
<thead>
<tr>
<th>Name</th>
<th>Age</th>
<th>City</th>
</tr>
</thead>
<tr *ngFor="let p of people">
<td>{{ p.name }}</td>
<td>{{ p.age }}</td>
<td>{{ p.city }}</td>
</tr>
</table>
结果如图4-4所示。
图4-4 渲染对象数组
我们还可以使用嵌套数组。如果想根据城市进行分组,可以定义一个新对象数组。
code/built_in_directives/app/ts/ng_for/ng_for.ts
this.peopleByCity = [
{
city:'Miami',
people:[
{ name:'John', age:12 },
{ name:'Angel', age:22 }
]
},
{
city:'Sao Paulo',
people:[
{ name:'Anderson', age:35 },
{ name:'Felipe', age:36 }
]
}
];
};
然后可以使用ngFor为每个城市渲染一个h2标签。
code/built_in_directives/app/ts/ng_for/ng_for.ts
<div *ngFor="let item of peopleByCity">
<h2 class="ui header">{{ item.city }}</h2>
并且使用一个嵌套指令对这个城市中的人们进行迭代。
code/built_in_directives/app/ts/ng_for/ng_for.ts
<table class="ui celled table">
<thead>
<tr>
<th>Name</th>
<th>Age</th>
</tr>
</thead>
<tr *ngFor="let p of item.people">
<td>{{ p.name }}</td>
<td>{{ p.age }}</td>
</tr>
</table>
下面是模板代码的最终结果。
code/built_in_directives/app/ts/ng_for/ng_for.ts
<h4 class="ui horizontal divider header">
Nested data
</h4>
<div *ngFor="let item of peopleByCity">
<h2 class="ui header">{{ item.city }}</h2>
<table class="ui celled table">
<thead>
<tr>
<th>Name</th>
<th>Age</th>
</tr>
</thead>
<tr *ngFor="let p of item.people">
<td>{{ p.name }}</td>
<td>{{ p.age }}</td>
</tr>
</table>
</div>
它会为每个城市渲染一个表格,如图4-5所示。
图4-5 渲染嵌套数组
获取索引
在迭代数组时,我们可能也要获取每一项的索引。
我们可以在ngFor指令的值中插入语法let idx = index并用分号分隔开,这样就可以获取索引了。这时候,Angular会把当前的索引分配给我们提供的变量(在这里是变量idx)。
注意,和JavaScript一样,索引都是从0开始的。因此第一个元素的索引是0,第二个是1,以此类推。
对我们的第一个例子稍加改动,添加代码段let num = index。
code/built_in_directives/app/ts/ng_for/ng_for.ts
<div class="ui list" *ngFor="let c of cities; let num = index">
<div class="item">{{ num+1 }} - {{ c }}</div>
</div>
它会在城市的名称前面添加序号,如图4-6所示。
图4-6 使用索引
当我们想告诉Angular不要编译或者绑定页面中的某个特殊部分时,要使用ngNodBindable指令。
假设我们想在模板中渲染纯文本{{ content }}。通常情况下,这段文本会被绑定到变量content的值,因为我们使用了{{ }}模板语法。
那该如何渲染出纯文本{{ content }}呢?可以使用ngNonBindable指令。
假设我们想要用一个div来渲染变量content的内容,紧接着输出文本<- this is what {{ content }} rendered来指向变量实际的值。
为了做到这一点,要使用下面的模板。
code/built_in_directives/app/ts/ng_non_bindable/ng_non_bindable.ts
template:`
<div class='ngNonBindableDemo'>
<span class="bordered">{{ content }}</span>
<span class="pre" ngNonBindable>
← This is what {{ content }} rendered
</span>
</div>
`
有了ngNonBindable属性,Angular不会编译第二个span里的内容,而是原封不动地将其显示出来(如图4-7所示)。
图4-7 使用ngNonBindable的结果
Angular的核心指令数量很少,但我们却能通过组合这些简单的指令来创建五花八门的应用。
-------------------------------------------------------------------------------- /output/30410212/52459936.html: -------------------------------------------------------------------------------- 1 |Angular提供了若干内置指令。在本章中,我们将探讨每一个内置指令并通过示例教会你如何使用它们。
内置指令是已经导入过的,你的组件可以直接使用它们。因此,不用像你自己的组件一样把它们作为指令导入进来。
如果你希望根据一个条件来决定显示或隐藏一个元素,可以使用ngIf指令。这个条件是由你传给指令的表达式的结果决定的。
如果表达式的结果返回的是一个假值,那么元素会从DOM上被移除。
下面是一些例子:
<div *ngIf="false"></div> <!—— never displayed ——>
<div *ngIf="a > b"></div> <!—— displayed if a is more than b ——>
<div *ngIf="str == 'yes'"></div> <!—— displayed if str holds the string "yes" ——>
<div *ngIf="myFunc()"></div> <!—— displayed if myFunc returns a true value ——>
如果你有AngularJS的经验,那么大概以前已经用过ngIf指令了。你可以把它当作AngularJS中ng-if的替代品。但另一方面,Angular并没有为AngularJS中的ng-show指令提供内置的替代品。那么,如果你只是想改变一个元素的CSS可见性,就应该使用ngStyle或class指令。本章稍后会介绍它们。
有时候你需要根据一个给定的条件来渲染不同的元素。
遇到这种情况时,你可能会像下面这样多次使用ngIf:
<div class="container">
<div *ngIf="myVar == 'A'">Var is A</div>
<div *ngIf="myVar == 'B'">Var is B</div>
<div *ngIf="myVar!= 'A' && myVar!= 'B'">Var is something else</div>
</div>
如你所见,当myVar的值既不是A也不是B时,代码将变得相当繁琐,其实我们真正想表达的只是一个else而已。随着我们添加的值越来越多,ngIf条件也会变得越来越复杂。
为了说明这种增长的复杂性,假设我们想要处理一个新的值C。
为了达到目的,我们不仅要添加一个使用ngIf的新元素,而且要修改最后一种情况:
<div class="container">
<div *ngIf="myVar == 'A'">Var is A</div>
<div *ngIf="myVar == 'B'">Var is B</div>
<div *ngIf="myVar == 'C'">Var is C</div>
<div *ngIf="myVar!= 'A' && myVar!= 'B' && myVar!= 'C'">Var is something else</div>
</div>
对于这种情况,Angular引入了ngSwitch指令。
如果你熟悉switch语句的话,应该会觉得似曾相识。
指令背后的思想也是一样的:对表达式进行一次求值,然后根据其结果来决定如何显示指令内的嵌套元素。
一旦有了结果,我们就可以:
●使用ngSwitchCase指令描述已知结果;
●使用ngSwitchDefault指令处理所有其他未知情况。
让我们使用这组新的指令来重写之前的例子:
<div class="container" [ngSwitch]="myVar">
<div *ngSwitchCase="'A'">Var is A</div>
<div *ngSwitchCase="'B'">Var is B</div>
<div *ngSwitchDefault>Var is something else</div>
</div>
如果想要处理新值C,只需要插入一行:
<div class="container" [ngSwitch]="myVar">
<div *ngSwitchCase="'A'">Var is A</div>
<div *ngSwitchCase="'B'">Var is B</div>
<div *ngSwitchCase="'C'">Var is C</div>
<div *ngSwitchDefault>Var is something else</div>
</div>
不需要修改默认(即备用)条件。
ngSwitchDefault元素是可选的。如果我们不用它,那么当myVar没有匹配到任何期望的值时就不会渲染任何东西。
你也可以为不同的元素声明同样的*ngSwitchCase值,这样就可以多次匹配同一个值了。例子如下:
code/built_in_directives/app/ts/ng_switch/ng_switch.ts
template:`
<h4 class="ui horizontal divider header">
Current choice is {{ choice }}
</h4>
<div class="ui raised segment">
<ul [ngSwitch]="choice">
<li *ngSwitchCase="1">First choice</li>
<li *ngSwitchCase="2">Second choice</li>
<li *ngSwitchCase="3">Third choice</li>
<li *ngSwitchCase="4">Fourth choice</li>
<li *ngSwitchCase="2">Second choice, again</li>
<li *ngSwitchDefault>Default choice</li>
</ul>
</div>
<div style="margin-top:20px;">
<button class="ui primary button"(click)="nextChoice()">
Next choice
</button>
</div>
`
在上面的例子中,当choice的值是2的时候,第2个和第5个li都会被渲染。
使用ngStyle指令,可以通过Angular表达式给特定的DOM元素设定CSS属性。
该指令最简单的用法就是[style.<cssproperty>]="value"的形式,下面是一个例子。
code/built_in_directives/app/ts/ng_style/ng_style.ts
<div [style.background-color]="'yellow'">
Uses fixed yellow background
</div>
这个代码片段就是使用ngStyle指令把CSS的background-color属性设置为字符串字面量yellow。
另一种设置固定值的方式就是使用ngStyle属性,使用键值对来设置每个属性。
code/built_in_directives/app/ts/ng_style/ng_style.ts
<div [ngStyle]="{color:'white', 'background-color':'blue'}">
Uses fixed white text on blue background
</div>
注意,在ngStyle的说明中,我们对background-color使用了单引号,但却没有对color使用。这是为什么呢?因为ngStyle的参数是一个JavaScript对象,而color是一个合法的键,不需要引号。但是在background-color中,连字符是不允许出现在对象的键名当中的,除非它是一个字符串,因此使用了引号。
通常情况下,我尽量不会对对象的键使用引号,除非不得不用。
我们在这里同时设置了color和background-color属性。
但ngStyle指令真正的能力在于使用动态值。
在这个例子中,我们定义了两个输入框。
code/built_in_directives/app/ts/ng_style/ng_style.ts
<div class="ui input">
<input type="text" name="color" value="{{color}}" #colorinput>
</div>
<div class="ui input">
<input type="text" name="fontSize" value="{{fontSize}}" #fontinput>
</div>
<button class="ui primary button"(click)="apply(colorinput.value, fontinput\
.value)">
Apply settings
</button>
然后使用它们的值来设置三个元素的CSS属性。
在第一个元素中,我们基于输入框的值来设定字体大小。
code/built_in_directives/app/ts/ng_style/ng_style.ts
<div>
<span [ngStyle]="{color:'red'}" [style.font-size.px]="fontSize">
red text
</span>
</div>
注意,我们在某些情况下必须指定单位。例如,把font-size设置为12不是合法的CSS,必须指定一个单位,比如12px或者1.2em。Angular提供了一个便捷语法用来指定单位:这里我们使用的格式是[style.fontSize.px]。
后缀.px表明我们设置font-size属性值以像素为单位。你完全可以把它替换为[style.font-size.em],以相对长度为单位来表示字体大小;还可以使用[style.fontSize.%],以百分比为单位。
另外两个元素使用#colorinput的值来设置文字颜色和背景颜色。
code/built_in_directives/app/ts/ng_style/ng_style.ts
<h4 class="ui horizontal divider header">
ngStyle with object property from variable
</h4>
<div>
<span [ngStyle]="{color:color}">
{{ color }} text
</span>
</div>
<h4 class="ui horizontal divider header">
style from variable
</h4>
<div [style.background-color]="color"
style="color:white;">
{{ color }} background
</div>
这样,当我们点击Apply settings按钮时,就会调用方法来设置新的值。
code/built_in_directives/app/ts/ng_style/ng_style.ts
apply(color:string, fontSize:number){
this.color = color;
this.fontSize = fontSize;
}
与此同时,文本颜色和字体大小都通过NgStyle指令作用在元素上了。
ngClass指令在HTML模板中用ngClass属性来表示,让你能动态设置和改变一个给定DOM元素的CSS类。
如果你用过AngularJS,会发现ngClass指令和过去在AngularJS中的ngClass所做的事是非常相似的。
使用这个指令的第一种方式是传入一个对象字面量。该对象希望以类名作为键,而值应该是一个用来表明是否应该应用该类的真/假值。
假设我们有一个叫作bordered的CSS类,用来给元素添加一个黑色虚线边框。
code/built_in_directives/app/css/styles.scss
.bordered {
border:1px dashed black;
background-color:#eee;
}
我们来添加两个div元素:一个一直都有bordered类(因此一直有边框),而另一个永远都不会有。
code/built_in_directives/app/ts/ng_class/ng_class.ts
<div [ngClass]="ordered:false">This is never bordered</div>
<div [ngClass]="ordered:true">This is always bordered</div>
如预期一样,两个div应该是如图4-1这样渲染的。
图4-1 ngClass指令的简单用法
当然,使用ngClass指令来动态分配类会有用得多。
为了动态使用它,我们添加了一个变量作为对象的值:
code/built_in_directives/app/ts/ng_class/ng_class.ts
<div [ngClass]="ordered:isBordered">
Using object literal.Border {{ isBordered ? "ON":"OFF" }}
</div>
或者在组件中定义该对象:
code/built_in_directives/app/ts/ng_class/ng_class.ts
export class NgClassSampleApp {
isBordered:boolean;
classesObj:Object;
classList:string[];
并直接使用它:
code/built_in_directives/app/ts/ng_class/ng_class.ts
<div [ngClass]="classesObj">
Using object var.Border {{ classesObj.bordered ? "ON":"OFF" }}
</div>
再次强调,当你使用像bordered-box这种包含连字符的类名时需要小心。JavaScript对象不允许字面量的键出现连字符。如果确实需要,那就必须像这样用字符串作为键:
<div [ngClass]="{'bordered-box':false}">……</div>
我们也可以使用一个类名列表来指定哪些类名会被添加到元素上。为此,我们可以传入一个数组型字面量:
code/built_in_directives/app/ts/ng_class/ng_class.ts
<div class="base" [ngClass]="['blue', 'round']">
This will always have a blue background and
round corners
</div>
或者在组件中声明一个数组对象:
this.classList = ['blue', 'round'];
并把它传进来:
code/built_in_directives/app/ts/ng_class/ng_class.ts
<div class="base" [ngClass]="classList">
This is {{ classList.indexOf('blue')> -1 ? "":"NOT" }} blue
and {{ classList.indexOf('round')> -1 ? "":"NOT" }} round
</div>
在上个例子中,[ngClass]分配的类名和通过HTML的class属性分配的已存在类名都是生效的。
最后添加到元素的类总会是HTML属性class中的类和[ngClass]指令求值结果得到的类的集合。
在这个例子中:
code/built_in_directives/app/ts/ng_class/ng_class.ts
<div class="base" [ngClass]="['blue', 'round']">
This will always have a blue background and
round corners
</div>
元素有全部的三个类:HTML的class属性提供的base,以及通过[ngClass]分配的blue和round(如图4-2所示)。
图4-2 来自属性和指令的CSS类
这个指令的任务是重复一个给定的DOM元素(或一组DOM元素),每次重复都会从数组中取一个不同的值。
这个指令是AngularJS中ng-repeat的继任者。
它的语法是*ngFor="let item of items"。
●let item语法指定一个用来接收items数组中每个元素的(模板)变量。
●items是来自组件控制器的一组项的集合。
要阐明这一点,我们来看一下代码示例。我们在组件控制器中声明了一个城市的数组:
this.cities = ['Miami', 'Sao Paulo', 'New York'];
然后在模板中有如下的HTML片段。
code/built_in_directives/app/ts/ng_for/ng_for.ts
<h4 class="ui horizontal divider header">
Simple list of strings
</h4>
<div class="ui list" *ngFor="let c of cities">
<div class="item">{{ c }}</div>
</div>
它会如你期望的那样在div中渲染每一个城市,如图4-3所示。
图4-3 使用ngFor指令的结果
我们还可以这样迭代一个对象数组。
code/built_in_directives/app/ts/ng_for/ng_for.ts
this.people = [
{ name:'Anderson', age:35, city:'Sao Paulo' },
{ name:'John', age:12, city:'Miami' },
{ name:'Peter', age:22, city:'New York' }
];
然后根据每一行数据渲染出一个表格。
code/built_in_directives/app/ts/ng_for/ng_for.ts
<h4 class="ui horizontal divider header">
List of objects
</h4>
<table class="ui celled table">
<thead>
<tr>
<th>Name</th>
<th>Age</th>
<th>City</th>
</tr>
</thead>
<tr *ngFor="let p of people">
<td>{{ p.name }}</td>
<td>{{ p.age }}</td>
<td>{{ p.city }}</td>
</tr>
</table>
结果如图4-4所示。
图4-4 渲染对象数组
我们还可以使用嵌套数组。如果想根据城市进行分组,可以定义一个新对象数组。
code/built_in_directives/app/ts/ng_for/ng_for.ts
this.peopleByCity = [
{
city:'Miami',
people:[
{ name:'John', age:12 },
{ name:'Angel', age:22 }
]
},
{
city:'Sao Paulo',
people:[
{ name:'Anderson', age:35 },
{ name:'Felipe', age:36 }
]
}
];
};
然后可以使用ngFor为每个城市渲染一个h2标签。
code/built_in_directives/app/ts/ng_for/ng_for.ts
<div *ngFor="let item of peopleByCity">
<h2 class="ui header">{{ item.city }}</h2>
并且使用一个嵌套指令对这个城市中的人们进行迭代。
code/built_in_directives/app/ts/ng_for/ng_for.ts
<table class="ui celled table">
<thead>
<tr>
<th>Name</th>
<th>Age</th>
</tr>
</thead>
<tr *ngFor="let p of item.people">
<td>{{ p.name }}</td>
<td>{{ p.age }}</td>
</tr>
</table>
下面是模板代码的最终结果。
code/built_in_directives/app/ts/ng_for/ng_for.ts
<h4 class="ui horizontal divider header">
Nested data
</h4>
<div *ngFor="let item of peopleByCity">
<h2 class="ui header">{{ item.city }}</h2>
<table class="ui celled table">
<thead>
<tr>
<th>Name</th>
<th>Age</th>
</tr>
</thead>
<tr *ngFor="let p of item.people">
<td>{{ p.name }}</td>
<td>{{ p.age }}</td>
</tr>
</table>
</div>
它会为每个城市渲染一个表格,如图4-5所示。
图4-5 渲染嵌套数组
获取索引
在迭代数组时,我们可能也要获取每一项的索引。
我们可以在ngFor指令的值中插入语法let idx = index并用分号分隔开,这样就可以获取索引了。这时候,Angular会把当前的索引分配给我们提供的变量(在这里是变量idx)。
注意,和JavaScript一样,索引都是从0开始的。因此第一个元素的索引是0,第二个是1,以此类推。
对我们的第一个例子稍加改动,添加代码段let num = index。
code/built_in_directives/app/ts/ng_for/ng_for.ts
<div class="ui list" *ngFor="let c of cities; let num = index">
<div class="item">{{ num+1 }} - {{ c }}</div>
</div>
它会在城市的名称前面添加序号,如图4-6所示。
图4-6 使用索引
当我们想告诉Angular不要编译或者绑定页面中的某个特殊部分时,要使用ngNodBindable指令。
假设我们想在模板中渲染纯文本{{ content }}。通常情况下,这段文本会被绑定到变量content的值,因为我们使用了{{ }}模板语法。
那该如何渲染出纯文本{{ content }}呢?可以使用ngNonBindable指令。
假设我们想要用一个div来渲染变量content的内容,紧接着输出文本<- this is what {{ content }} rendered来指向变量实际的值。
为了做到这一点,要使用下面的模板。
code/built_in_directives/app/ts/ng_non_bindable/ng_non_bindable.ts
template:`
<div class='ngNonBindableDemo'>
<span class="bordered">{{ content }}</span>
<span class="pre" ngNonBindable>
← This is what {{ content }} rendered
</span>
</div>
`
有了ngNonBindable属性,Angular不会编译第二个span里的内容,而是原封不动地将其显示出来(如图4-7所示)。
图4-7 使用ngNonBindable的结果
Angular的核心指令数量很少,但我们却能通过组合这些简单的指令来创建五花八门的应用。
-------------------------------------------------------------------------------- /output/30410212/52459987.html: -------------------------------------------------------------------------------- 1 |管理数据可以说是编写可维护应用最棘手的方面之一。有很多种方法可以将数据应用到你的应用之中:
●AJAX HTTP请求
●Websocket
●Indexdb
●LocalStorage
●LocalStorage
●Service Worker
●等等
数据架构涉及的问题如下。
●如何将所有不同的数据源聚合成一个完整的体系?
●如何防止意想不到的副作用导致bug?
●如何更好地构建代码以使其更容易维护并让新来的团队成员更容易上手?
●当数据发生变化时,如何让应用尽快作出反应?
多年以来,MVC一直是构建数据应用的标准模式:模型包含业务逻辑,视图负责显示数据,控制器将所有一切联系在一起。不过问题是,我们知道MVC模式并不能很好地直接转化到客户端的网络应用中。
目前,数据架构领域出现了复兴并有许多新理念涌现出来。
●MVW/双向数据绑定:Model-View-Whatever是用来形容AngularJS中默认架构的一个术语。$scope提供数据双向绑定,整个应用都共用同样的数据结构,某个区域的一个变化会传达至该应用的其余部分。
●Flux:它使用单向数据流。在Flux中,Store负责存储数据,View负责渲染Store中的数据,Action负责改变Store中的数据。虽然设置Flux有一点繁琐,但是因为数据只在一个方向上流动,所以很容易推断。
●可观察对象:observable给我们提供了数据流。我们订阅数据流然后执行操作对变化作出反应。RxJS是当下最流行的响应式JavaScript库,给我们提供了强有力的操作符,用来在数据流上组合一系列操作。
还有很多关于这些理念的变种,例如:
Flux作为一种模式而并非具体实现,它有许多不同的实现方案(就像MVC有许多的实现方案一样);
●Immutability是以上所有数据架构的一个常见变种;
●Falcor是一个强大的框架,可以帮你将客户端模型和服务端数据进行绑定。
●Falcor通常使用可观察对象类型的数据架构。
Angular数据架构
Angular在数据架构的选择上极其灵活。一种数据策略在一个项目中可行并不代表在另一个项目中也可行,所以Angular并未规定具体的技术栈,而是力图让你无论选择何种数据架构都能很容易使用(同时保持高性能)。
这样的好处是,你可以拥有足够的灵活性来让Angular适应几乎任何情况。只是有一点不太好:你将不得不自己选择适合项目的数据架构。
别担心,我们不会让你自己去作出这个艰难的决定!在接下来的几章里,我们将教你如何使用这里提到的某些模式来构建应用。
管理数据可以说是编写可维护应用最棘手的方面之一。有很多种方法可以将数据应用到你的应用之中:
●AJAX HTTP请求
●Websocket
●Indexdb
●LocalStorage
●LocalStorage
●Service Worker
●等等
数据架构涉及的问题如下。
●如何将所有不同的数据源聚合成一个完整的体系?
●如何防止意想不到的副作用导致bug?
●如何更好地构建代码以使其更容易维护并让新来的团队成员更容易上手?
●当数据发生变化时,如何让应用尽快作出反应?
多年以来,MVC一直是构建数据应用的标准模式:模型包含业务逻辑,视图负责显示数据,控制器将所有一切联系在一起。不过问题是,我们知道MVC模式并不能很好地直接转化到客户端的网络应用中。
目前,数据架构领域出现了复兴并有许多新理念涌现出来。
●MVW/双向数据绑定:Model-View-Whatever是用来形容AngularJS中默认架构的一个术语。$scope提供数据双向绑定,整个应用都共用同样的数据结构,某个区域的一个变化会传达至该应用的其余部分。
●Flux:它使用单向数据流。在Flux中,Store负责存储数据,View负责渲染Store中的数据,Action负责改变Store中的数据。虽然设置Flux有一点繁琐,但是因为数据只在一个方向上流动,所以很容易推断。
●可观察对象:observable给我们提供了数据流。我们订阅数据流然后执行操作对变化作出反应。RxJS是当下最流行的响应式JavaScript库,给我们提供了强有力的操作符,用来在数据流上组合一系列操作。
还有很多关于这些理念的变种,例如:
Flux作为一种模式而并非具体实现,它有许多不同的实现方案(就像MVC有许多的实现方案一样);
●Immutability是以上所有数据架构的一个常见变种;
●Falcor是一个强大的框架,可以帮你将客户端模型和服务端数据进行绑定。
●Falcor通常使用可观察对象类型的数据架构。
Angular数据架构
Angular在数据架构的选择上极其灵活。一种数据策略在一个项目中可行并不代表在另一个项目中也可行,所以Angular并未规定具体的技术栈,而是力图让你无论选择何种数据架构都能很容易使用(同时保持高性能)。
这样的好处是,你可以拥有足够的灵活性来让Angular适应几乎任何情况。只是有一点不太好:你将不得不自己选择适合项目的数据架构。
别担心,我们不会让你自己去作出这个艰难的决定!在接下来的几章里,我们将教你如何使用这里提到的某些模式来构建应用。