易腾

易腾 查看完整档案

青岛编辑德州学院  |  电子信息科学与技术 编辑青岛地铁大数据  |  程序员 编辑 api.ytclub.top 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

易腾 收藏了文章 · 10月16日

如何实现label长度固定,文字分散分布的效果

我们经常会看到表单填写时前面的label,都是左右对齐的,即使字数不一样。就像下面的效果:

fensan

解析

这种效果主要通过text-align-last属性来实现。

  • text-align-last:规定如何对齐文本的最后一行

在菜鸟教程上对于这个属性有这样的描述:

text-align-last 属性只有在 text-align 属性设置为 "justify" 时才起作用。

但我尝试了一下,当文本只有一行的时候,text-align属性不设置,text-align-last也是有用的。当然这只是针对非IE和Safiri而言。

text-align属性对多行文本的最后一行文本无效。

/*css*/
.label {
      width: 200px;
      text-align-last: justify; 
}
/*html*/
<div class="label">产 品</div>

这样的写法,对IE浏览器和safari浏览器就不起作用了。因为text-align-last不支持这两个浏览器。

针对这个问题,我们可以用伪类来解决。

/*css*/
.label {
      width: 200px;
      height: 30px; /*高度需要添加,不然文字下面会多出一些空隙*/
      text-align: justify; 
}
 .label:after{
        content: '';
        display: inline-block;
        width: 100%;
      }
/*html*/
<div class="label">产 品</div>

有三点需要注意:

  1. 因为加了伪类之后,相当于在标签的文本后又加了一行,所以text-align-last就不起作用了,需要加上text-align
  2. 因为加了伪类之后,文本后面会多出一行,即使给伪类设置高度为0也不行。所以必须给.label属性加上一个高度。区别如下:

height

  1. 加了伪类之后,标签的文字之间必须要存在一个空格(一个就行,不用太多),否则的话,没有效果。

两种方案对比

第二种方法相对于第一种方法来说,兼容性更好,我在多个浏览器测试了一下,都没有问题。

第一种方案更简洁,但是不支持ie和safari,所以建议大家还是用第二种方案更省心。

查看原文

易腾 关注了用户 · 10月12日

阿宝哥 @angular4

http://www.semlinker.com/
聚焦全栈,专注分享 Angular、TypeScript、Node.js/Java 、Spring 技术栈等全栈干货

欢迎各位小伙伴关注本人公众号全栈修仙之路

关注 2194

易腾 收藏了文章 · 7月24日

你TM管这玩意儿叫H5编辑器?????

作为一名程序员,产品经理、设计师等身兼数职的我,推荐一款年度最佳的H5编辑器给大家,真心牛逼,业界良心,看的我热血沸腾,回家一脚踢飞正在熟睡的哈士奇!

H5DS编辑器,软件截图:

592956246-5f0edf4a2a0a5.gif

推荐理由1:时间轴

支持动画时间轴,调试动画和音频

image.png

给图层添加动画后可以通过时间轴设置动画,真的很直观和方便。效率提升N倍。其中紫色块表示进入动画,蓝色是离开动画,黄色块是强调动画,绿色块表示音频,另外时间轴开可以设置音乐播放的开始时间。

推荐理由2:批量操作

批量选中后,还可以一键布局,批量粘贴动画,批量设置动画间隔,真是强大的不要不要呀~

image.png

推荐理由3:逐帧动画

逐帧动画哇~ 真的优秀!

image.png

推荐理由4:路径动画

还可以自定义路径动画,贝塞尔曲线,钢笔工具????!!!这就叫专业

image.png

推荐理由5:黑暗模式

黑暗模式,传说中只有IOS才有的黑暗模式,居然在H5DS编辑器中也有,喜欢黑色的编辑器么?那就开启黑暗模式吧!

image.png

推荐理由6:下载代码

尼玛,还可以把做好的作品下载代码到本地。一次只要10个积分,积分不够可以关注官方的公众号,直接赠送500个积分,每天签到可以赠送10个积分。真的很良心~ 积分用完还可以找作者要积分,基本上可以免费使用,之所以为什么不直接免费?因为是怕有人恶意下载导致服务器负载过高挂了大家都没得玩了。

image.png

推荐理由7:强大音频

语音合成,在线录音,上传音频,你需要的都有!

image.png

image.png

推荐理由8:浮动层

唯一一款支持多浮动层的编辑器,给浮动层添加元素后每个页面都会全部加上元素,这个操作比较骚,真的~

image.png

图解浮动层:

微信截图_20200723190133.png

推荐理由9:吸附定位

吸附定位,这个概念真的比较新,通过名字可以了解到这个功能可以让元素吸附到窗口指定的位置,对这个研究了一下发现这个功能真的很酷,特别是解决各种机型兼容性的问题。

推荐理由10:自定义脚本

额,还可以自定义JS脚本,编辑器实现不了的功能,脚本帮你实现。老板:你TM管着玩意儿叫H5编辑器?????可以写代码的编辑器我还是第一次见。

image.png

推荐理由11:丰富的自定义交互功能

编辑器提供了各种交互功能,比如切换页面、拖动、超链接、打电话、隐藏显示图层、弹窗控制、音频控制等丰富的交互功能。真香~

image

推荐理由12:插件可扩展

编辑器自带了很多插件,这款叫H5DS的编辑器更牛逼了,插件还可以自己开发,为了让插件开发者更爽,居然自己开发了一套UI库,哦,你没看错,真的是一套UI库。【h5ds-ui】

woc~ 能不能好好的做编辑器!不得不说真的奥利给呀~

image.png

image.png

推荐理由13:H5DS-JSSDK

额,居然自己开发了一套JSSDK,让React开发的H5DS在vue, angular中大放异彩。传说中的混合开发来了????

image.png

推荐理由14:

推荐H5DS的理由很简单,我TM就是作者!【H5DS网址】

非要逼我跪下来求你才会给我点个赞吗?????老子堂堂七尺男儿!

image.png

大爷!赏个赞吧!

查看原文

易腾 回答了问题 · 7月17日

webview里如何支持PWA

app的webview,为内置浏览器,关闭即销毁,无法保存缓存pwa

关注 2 回答 1

易腾 赞了文章 · 6月12日

移动前端开发和 Web 前端开发的区别是什么?

简介: 前端这门技术,从诞生发展至今不过寥寥十余年。如果说前十年是 PC 前端的时代,那后十年一定是属于移动前端的时代。特别是随着网络制式的发展,移动设备在全球范围内得到了空前的普及,在前端领域,Hybird Web、React Native、Weex、Flutter 等等一系列新的移动前端技术也如同雨后春笋般冒出来,今天来和大家分享一下我对「移动前端开发和 Web 前端开发」的理解。

滚动.gif

前端这门技术,从诞生发展至今不过寥寥十余年。如果说前十年是 PC 前端的时代,那后十年一定是属于移动前端的时代。特别是随着网络制式的发展,移动设备在全球范围内得到了空前的普及,在前端领域,Hybird Web、React Native、Weex、Flutter 等等一系列新的移动前端技术也如同雨后春笋般冒出来,今天来和大家分享一下我对「移动前端开发和 Web 前端开发」的理解。

回顾:前端发展史

▐ 阶段一:刀耕火种

十多年前的前端,开发者还在为兼容 IE6 而头疼,框架上 jQuery 是老大,有追求的前端开发可能会使用 Zepto.js 以减少网页体积。这个时候,前端页面主要还是以 PC 为主,这个时候根本没有移动前端的概念,在小小的手机屏幕上流量的页面则是以纯文本为主。

image.png

▐ 阶段二:工程化

image.png

在 2011 ~ 2014 年之间的历史阶段里,模块化的思路占为主导。当时为了进行 Assets 资源加载器的设计,就制定了模块化的协议规范。当时比较流行的模块化协议就是 AMD(RequireJS)、CMD(Seajs 为代表)、KMD(Kissy 为代表)。在淘宝、天猫,Kissy 应用的很火,所以 KMD 主导天下;在支付宝及外部社区,Seajs 应用的很火,所以 CMD 主导天下,玉伯大大的名气和威望也在前端圈里特别高;而 AMD 在国外比较流行,但渐渐也被后来出现的 CommonJS 规范削弱了气势。

▐ 阶段三:移动化

随着 3G、4G 的发展和 iOS 和 Android 手机在市场的普及量大增,PC 业务主战场也逐渐过渡到移动端。前端的思维模式由 PC 转向了移动端,并向 App 的用户体验看齐。移动端的 HTML5 协议支持不完善,前端的生产配套不全,Android 的屏幕碎片化,所以那个时候的前端开发移动端页面适配的痛苦要远远超过 PC 时代。

image.png

▐ 阶段四:框架化

在前端社区,Angular、React、Vue、RN (React Native) 这样的 MV* 框架一个接着一个出现,让前端接受了数据驱动思想的洗礼之外,还借助 RN 完成了移动端的体验升级,包括后来的 Weex、Flutter。

在这个阶段,前端开始有了终端的底层架构组,开始构思前端页面在移动终端上的加载性能和用户体验表现。在阿里巴巴内部,为了解决多端复用的问题,Rax 借助 VDOM 打通 WebView 和 Weex 两端,一套代码跑天下。

image.png

▐ 阶段五:垂直化

随着初代 iPhone 的发布,大屏幕手机逐渐变成了主流,移动端的需求开始爆发。在淘宝天猫,每年双十一的移动端成交额逐年攀升,并逐渐占领绝对领先地位。前端的领域也随着这种趋势逐渐细分,按照场景可以简单分为移动(无线)前端开发和中后台前端开发。

移动前端开发面向的是消费者端的 Web 与 轻 App 业务场景,在这个场景下,淘系经过多年的营销活动沉淀,逐渐形成了移动端独特的、轻量级的解决方案,以及模块维度的、面向运营的页面搭建系统。

中后台前端则是面向企业 ERP、CRM 、OA 等偏后的业务场景,如商家后台等系统。在这个场景下,借助飞冰、Fusion Design 等中后台物料,形成可视化的中后台搭建解决方案,为业务的前端、开发或产品角色提供一站式中后台生产解决方案。

移动前端:混合技术的前世今生

曾几何时,移动客户端开发和前端开发是两条没有交集的平行线,但现在我们越来越拥抱两者的合作产物:混合式(Hybird)应用开发,或者用最近比较火的一个概念 -- 大前端技术。

从技术的表现形式思考,本质上客户端开发与前端开发都是终端技术,它的特点是直接面向用户 UI 编程。

▐ 同是 UI 编程,我们面对的痛点是什么?

传统 Web 前端技术的技术局限性

1、资源加载:HTML、JS、CSS、图片等静态资源存放于远端的服务器,需要动态的异步拉取,再拉取数据进行展示,初始化效率上比 Native 慢的多

2、渲染机制:在浏览器的设计中,JS 的执行和页面的布局、Paint 都在同一个主线程,无法并行化,再加上 JS 的性能赶不上 AOT 语言,执行复杂逻辑时导致的卡顿通常会阻塞 UI,再加上冗长的渲染管线,导致浏览器的渲染体验在等量对比 Native 时并不占优势。

3、页面切换:在浏览器中并不存在路由的概念,这导致页面间的切换体验完全依赖于浏览器 Shell 提供的能力,在页面切换的时候会反复加载。当然前端社区中也出现了单页面应用的概念,但是多个页面的资源也显著增加了 JSBundle 的体积,也使页面的开发更加复杂化了。

4、API 能力:浏览器的安全机制是基于同源策略的沙箱机制,这套沙箱机制阻止了前端开发者使用原生系统能力,你只能使用 W3C 标准定义的功能,而且考虑到终端碎片化的问题,这些接口往往不能直接使用。这在 PC 端的场景中是没有什么问题的,但是在移动端则相反,开发者希望有能力调用系统接口实现一些更富交互的场景。

5、交互性能:浏览器的实时交互性能体验差,在复杂交互场景中大规模的重排限制了 UI 帧率,这种限制在中低端移动设备中尤为严重。

6、脚本语言,动态解析执行
JS 是一门 JIT 语言,也就是需要动态解析执行,对比预先编译机器码的 AOT 语言的执行性能就差得多了。

传统客户端技术的局限性?

1、动态性:客户端开发通常是有固定的版本发布计划,而且受制于 Apple 的 App Store 审核规则,版本发布的不确定性还会受到政策影响,Android 在国内的渠道众多,每次发版都要反复检查渠道,一旦发现线上问题需要依赖再次发版,容错成本非常高,这也大大增加了对业务的局限性。

2、开发成本:客户端的开发成本高,然而生态还不如 Web 丰富,npm 社区的几万开源包,加上更活跃的开发者社区,导致对企业来讲客户端的研发成本是高于 Web 开发的。

3、跨端一致性:传统客户端开发一套业务,是需要实现 Android + iOS 两套代码的,而且由于 Android 和 iOS 的操作系统能力差异,同样的需求往往会用不同的视觉和交互来实现,这也导致了业务成本居高不下。

▐ 混合式前端开发

为什么会出现混合式前端开发?

随着 iOS + Android 确立了移动操作系统的统治地位,前端开发者也在寻找使用操作系统提供的能力进行业务开发的模式。Web 的开发方式远比 iOS 和 Android 更加方便和高效,Web 上层出不穷的各种库和框架也远比 Android 和 iOS 的各种库和框架方便的多。Web 上我们可以方便的使用各种前端框架,及时预览效果(想想大型 Android/iOS 工程的编译时间)。

从阿里巴巴的角度来看,随着中台化的理念逐渐被接受:业务需要追求快速发展,前台的 UI 和需求会随着商业决策快速迭代,前端和客户端不同的岗位也形成了分工化的研发模式。

前端向上,前置作为业务方的唯一接口,逐渐演变为大前端的业务层。在这一层,它的职责是负责定义规范,通过框架规范业务的开发过程,同时封装统一的解决方案和工程化能力,将重复的工作抽离。

客户端向下扎,解耦业务需求,转为大前端的架构层,给上层的业务开发者提供能力支持。通过将客户端的系统级 API 以及宿主应用的能力暴露给上层前端,提高前端页面对更多富交互场景的承载能力。

image.png

在这样的大背景下,各种混合开发技术层出不穷,在这里我们简单的把混合式应用的发展定义为三个阶段:

▐ 阶段一:JSBridge

在这个阶段,主要还是以 WebView 为主,并配合 JSBridge 提供了 Naive 与 JS 之间的通信链路,基于这个通信基础,Native可以暴露出一些标准服务 API 提供给 JS 调用,同样的 JS 也可以以此封装一些基础API给 Native 调用。前端开发者使用传统的 JS + HTML + CSS 进行页面的开发,并且调用 JSBridge API 驱动客户端能力。在这个阶段,Apache Cordova 是业内的佼佼者,大多互联网公司内部也有自己的 JSBridge 框架实现,可以说 JSBridge 第一次给了前端开发者调用 Native 的能力。

image.png

但是 JSBridge 方案的主要缺点在于性能方面及高级组件扩展能力缺失,流畅性始终无法与 Native 媲美。

▐ 阶段二:原生 UI

虽然 Web 的动态性和高效的开发效率,是原生开发望尘莫及的,但是浏览器技术的瓶颈也非常明显:

1、W3C 作为开放的技术标准,历史悠久,包袱多,显著拖慢了浏览器的性能。

2、WebView 渲染引擎设计的上的缺陷,渲染流水线非常长,导致浏览器对合成器动画和非合成器动画区别对待,非合成动画性能不好。

3、单线程模型,无法发挥现代硬件架构特别是 ARM 架构多核心的性能。

4、异步光栅化的设计,在进行长列表滚动时,不可避免出现白屏的现象。

**有没有一种两全其美的方式?
React Native/Weex 的出现给前端开发者带来了一道曙光。**

React Native/Weex 利用 JS 引擎来调用 Native 端的组件,从而实现相应的功能。React Native 和 Weex 都允许前端开发者使用 JS 进行业务逻辑开发,使用 VDOM 来描述文档结构,并配合 CSS 的子集来定制样式,样式和模板分离。

Weex 体系中, JS 的执行在 JS Thread,Layout 执行在独立的 Layout Thread ,渲染执行在系统的 MainThread ,三个线程相互独立,并行执行。

在 WebView 的体系中 JS 的执行、 Layout 、 Paint 都在 MainThread ,相互影响,在进行复杂任务时会导致界面卡顿。

这种方案的优势在于最大化的复用前端的生态和 Native 的生态体系。

在阿里巴巴,Weex 的大规模应用,甚至支撑起了双十一亿级的流量。

image.png

▐ 阶段三:自绘引擎

什么是自绘引擎?

所谓自绘引擎,就是不依赖操作系统提供的布局、原生组件能力,直接调用 GPU 或者底层抽象层进行绘制的渲染引擎。

在上一个阶段,前端开发者已经可以使用 JS 引擎驱动原生 UI 了,为什么还需要自绘引擎?

React Native/Weex 充分将 Native 的 View 体系输出到前端体系中,在进行 Android/iOS Native View 的封装过程中,存在不少难以逾越的障碍。如:难以抹平的双端一致性问题、复杂样式能力难以实现、 Layout 动画需要执行两次(WeexCore Layout 和 Android Native 本身的 Layout )。组件的封装成本随着复杂度增加也越来越高,难以逾越 Native View 限制提供更细致的 W3C 标准能力。

2018 年 Flutter 诞生,通过 Dart 语言构建一套跨平台的开发组件,所有组件基于 Skia 引擎自绘,在性能上和 Native 平台的 View 相媲美,同时解决了上一代架构难以解决的双端一致性等问题。引起大家广泛关注,充分验证了通过绘制构建组件做到 Native View 媲美的 UI 渲染引擎的可行性。

但是 Flutter 本身是缺乏动态更新特性的,社区上也有一些项目在探索 Flutter 的动态化特性,我们团队内部也在实现面向前端的动态化 Flutter 引擎:Kraken,与其它方案不同的是 Kraken 并没有基于 Flutter 自带的 Widgets 框架进行扩展,而是从底层扩展了 W3C 标准的 API,这使得它更像一个浏览器,也让 Flutter 面向 Web 开发者的使用门槛大大降低。

未来:回归本源

天下大势,分久必合,合久必分。移动前端开发本质上还是终端开发的一种形态,不管容器、框架、语言怎么变,在前端开发者中只有 W3C 的标准是永远不变的。笔者认为,随着 Web 的发展,在解决一系列性能、体验问题之后,浏览器技术会成为更通用的 UI 编程标准。

▐ PWA

早先年,Google 就已经在这一领域内努力,推出了 PWA (Progress Web Application) 的概念。

PWA 通过在移动端浏览器提供标准化框架,在 Web App 中实现和 Native App 接近的用户体验。它的特性其实是一系列 W3C 标准和私有标准集合,简单的说 PWA 支持:

  • 离线加载:通过 Service Worker 等提供的缓存机制,允许用户在断网或者弱网的情况下直接读取离线资源。
  • 后台驻留进程:正常情况下,浏览器的页面关闭后它的整个生命周期就结束了,内存也得到了释放。Service Worker 允许页面在关闭的情况下继续运行,这保证了类似于离线缓存、主动推送等。
  • 消息通知:允许 Web 开发者实现类似 App 的主动推送机制。
  • 其它移动 App 的功能特性,如直接保存图标到桌面,允许用户像正常使用 App 一样打开 PWA 应用;可以隐藏 UI 中的默认浏览器元素,让 Web 内容全屏展示,从视觉上看让 Web 应用更像一个原生应用,有时候你根本无法分辨到底是 Web 应用还是原生应用。

▐ PHA

当然在标准能力不完善,业务又需要定制化能力的当下,混合式应用还会继续发展。

PHA (Progress Hybird Application) 的概念应用而生,PHA 是一种渐进式的混合应用增强策略, 提供端测的辅助能力,提升 WebView 的渲染性能与体验。广义地说,当下比较火的小程序、快应用都是 PHA 的一种实现。

在淘系内部,我们也在实现一套轻量级的 PHA 方案,并且在大促中也取得了不错的效果,我想后面单独出一篇关于 PHA 的文章来阐述。

关于未来,随着技术方案的多样化、以及端边界的扩展,我们认为最重要的就是效率与性能的问题。

image.png

基于大数据的机器学习能力,移动端上会拥有更高效的 UI 编排能力,最终能让 UI 渲染实现实时个性化。

image.png

基于最近比较热的 WebAssembly 能力,让浏览器突破 JavaScript 的限制,能拥有更大的想象空间。

作者|卓凌
编辑|橙子君
出品|阿里巴巴新零售淘系技术部

One More Thing

我们是 Rax、飞冰(ICE)、GCanvas 等炙手可热开源项目的开山鼻祖,是你在阿里写前端时绕不开的爱恨纠缠。想不想为集团的前端架构夯实地基让百米高楼更加坚若磐石?想不想让自己的代码造福社区引人膜拜?想不想窥探窥探下一代渲染引擎(Kraken)和下一代手淘应用容器(PHA)的花容月貌?那么,你现在看到的就是唯一的正确答案。

无聊的代码千篇一律,有趣的岗位万里挑一,终端架构的未来如此广阔,需要各位鼎力相助,欢迎加入阿里淘系前端架构团队!

我们是阿里巴巴淘系技术部 - 大前端技术架构团队,诚邀各路小伙伴的加入,一起做大事!

简历可邮件投递至zhuoling.lcl@alibaba-inc.com ,期待你的联系。

1、大比拼 | 下一代高性能跨平台UI渲染引擎
2、Flutter 动态化方案探索
3、最火移动端跨平台方案盘点:React Native、weex、Flutter
4、浅谈 Hybrid App
5、其它一些无法被引用的内外部资源

查看原文

赞 34 收藏 22 评论 1

易腾 赞了文章 · 6月9日

了不起的 TypeScript 入门教程(1.2W字)

想学习 TypeScript 的小伙伴看过来,本文将带你一步步学习 TypeScript 入门相关的十四个知识点,详细的内容大纲请看下图:

typescript-quickstart-directory.png

一、TypeScript 是什么

TypeScript 是一种由微软开发的自由和开源的编程语言。它是 JavaScript 的一个超集,而且本质上向这个语言添加了可选的静态类型和基于类的面向对象编程。

TypeScript 提供最新的和不断发展的 JavaScript 特性,包括那些来自 2015 年的 ECMAScript 和未来的提案中的特性,比如异步功能和 Decorators,以帮助建立健壮的组件。下图显示了 TypeScript 与 ES5、ES2015 和 ES2016 之间的关系:

typescript-scope-new.png

1.1 TypeScript 与 JavaScript 的区别

TypeScriptJavaScript
JavaScript 的超集用于解决大型项目的代码复杂性一种脚本语言,用于创建动态网页。
可以在编译期间发现并纠正错误作为一种解释型语言,只能在运行时发现错误
强类型,支持静态和动态类型弱类型,没有静态类型选项
最终被编译成 JavaScript 代码,使浏览器可以理解可以直接在浏览器中使用
支持模块、泛型和接口不支持模块,泛型或接口
支持 ES3,ES4,ES5 和 ES6 等不支持编译其他 ES3,ES4,ES5 或 ES6 功能
社区的支持仍在增长,而且还不是很大大量的社区支持以及大量文档和解决问题的支持

1.2 获取 TypeScript

命令行的 TypeScript 编译器可以使用 Node.js 包来安装。

1.安装 TypeScript

$ npm install -g typescript

2.编译 TypeScript 文件

$ tsc helloworld.ts
# helloworld.ts => helloworld.js

当然,对于刚入门 TypeScript 的小伙伴,也可以不用安装 typescript,而是直接使用线上的 TypeScript Playground 来学习新的语法或新特性。

TypeScript Playground:https://www.typescriptlang.or...

二、TypeScript 基础类型

2.1 Boolean 类型

let isDone: boolean = false;
// ES5:var isDone = false;

2.2 Number 类型

let count: number = 10;
// ES5:var count = 10;

String 类型

let name: string = "Semliker";
// ES5:var name = 'Semlinker';

2.4 Array 类型

let list: number[] = [1, 2, 3];
// ES5:var list = [1,2,3];

let list: Array<number> = [1, 2, 3]; // Array<number>泛型语法
// ES5:var list = [1,2,3];

2.5 Enum 类型

使用枚举我们可以定义一些带名字的常量。 使用枚举可以清晰地表达意图或创建一组有区别的用例。 TypeScript 支持数字的和基于字符串的枚举。

1.数字枚举

enum Direction {
  NORTH,
  SOUTH,
  EAST,
  WEST,
}

let dir: Direction = Direction.NORTH;

默认情况下,NORTH 的初始值为 0,其余的成员会从 1 开始自动增长。换句话说,Direction.SOUTH 的值为 1,Direction.EAST 的值为 2,Direction.WEST 的值为 3。上面的枚举示例代码经过编译后会生成以下代码:

"use strict";
var Direction;
(function (Direction) {
  Direction[(Direction["NORTH"] = 0)] = "NORTH";
  Direction[(Direction["SOUTH"] = 1)] = "SOUTH";
  Direction[(Direction["EAST"] = 2)] = "EAST";
  Direction[(Direction["WEST"] = 3)] = "WEST";
})(Direction || (Direction = {}));
var dir = Direction.NORTH;

当然我们也可以设置 NORTH 的初始值,比如:

enum Direction {
  NORTH = 3,
  SOUTH,
  EAST,
  WEST,
}

2.字符串枚举

在 TypeScript 2.4 版本,允许我们使用字符串枚举。在一个字符串枚举里,每个成员都必须用字符串字面量,或另外一个字符串枚举成员进行初始化。

enum Direction {
  NORTH = "NORTH",
  SOUTH = "SOUTH",
  EAST = "EAST",
  WEST = "WEST",
}

以上代码对于的 ES5 代码如下:

"use strict";
var Direction;
(function (Direction) {
    Direction["NORTH"] = "NORTH";
    Direction["SOUTH"] = "SOUTH";
    Direction["EAST"] = "EAST";
    Direction["WEST"] = "WEST";
})(Direction || (Direction = {}));

3.异构枚举

异构枚举的成员值是数字和字符串的混合:

enum Enum {
  A,
  B,
  C = "C",
  D = "D",
  E = 8,
  F,
}

以上代码对于的 ES5 代码如下:

"use strict";
var Enum;
(function (Enum) {
    Enum[Enum["A"] = 0] = "A";
    Enum[Enum["B"] = 1] = "B";
    Enum["C"] = "C";
    Enum["D"] = "D";
    Enum[Enum["E"] = 8] = "E";
    Enum[Enum["F"] = 9] = "F";
})(Enum || (Enum = {}));

通过观察上述生成的 ES5 代码,我们可以发现数字枚举相对字符串枚举多了 “反向映射”:

console.log(Enum.A) //输出:0
console.log(Enum[0]) // 输出:A

2.6 Any 类型

在 TypeScript 中,任何类型都可以被归为 any 类型。这让 any 类型成为了类型系统的顶级类型(也被称作全局超级类型)。

let notSure: any = 666;
notSure = "Semlinker";
notSure = false;

any 类型本质上是类型系统的一个逃逸舱。作为开发者,这给了我们很大的自由:TypeScript 允许我们对 any 类型的值执行任何操作,而无需事先执行任何形式的检查。比如:

let value: any;

value.foo.bar; // OK
value.trim(); // OK
value(); // OK
new value(); // OK
value[0][1]; // OK

在许多场景下,这太宽松了。使用 any 类型,可以很容易地编写类型正确但在运行时有问题的代码。如果我们使用 any 类型,就无法使用 TypeScript 提供的大量的保护机制。为了解决 any 带来的问题,TypeScript 3.0 引入了 unknown 类型。

2.7 Unknown 类型

就像所有类型都可以赋值给 any,所有类型也都可以赋值给 unknown。这使得 unknown 成为 TypeScript 类型系统的另一种顶级类型(另一种是 any)。下面我们来看一下 unknown 类型的使用示例:

let value: unknown;

value = true; // OK
value = 42; // OK
value = "Hello World"; // OK
value = []; // OK
value = {}; // OK
value = Math.random; // OK
value = null; // OK
value = undefined; // OK
value = new TypeError(); // OK
value = Symbol("type"); // OK

value 变量的所有赋值都被认为是类型正确的。但是,当我们尝试将类型为 unknown 的值赋值给其他类型的变量时会发生什么?

let value: unknown;

let value1: unknown = value; // OK
let value2: any = value; // OK
let value3: boolean = value; // Error
let value4: number = value; // Error
let value5: string = value; // Error
let value6: object = value; // Error
let value7: any[] = value; // Error
let value8: Function = value; // Error

unknown 类型只能被赋值给 any 类型和 unknown 类型本身。直观地说,这是有道理的:只有能够保存任意类型值的容器才能保存 unknown 类型的值。毕竟我们不知道变量 value 中存储了什么类型的值。

现在让我们看看当我们尝试对类型为 unknown 的值执行操作时会发生什么。以下是我们在之前 any 章节看过的相同操作:

let value: unknown;

value.foo.bar; // Error
value.trim(); // Error
value(); // Error
new value(); // Error
value[0][1]; // Error

value 变量类型设置为 unknown 后,这些操作都不再被认为是类型正确的。通过将 any 类型改变为 unknown 类型,我们已将允许所有更改的默认设置,更改为禁止任何更改。

2.8 Tuple 类型

众所周知,数组一般由同种类型的值组成,但有时我们需要在单个变量中存储不同类型的值,这时候我们就可以使用元组。在 JavaScript 中是没有元组的,元组是 TypeScript 中特有的类型,其工作方式类似于数组。

元组可用于定义具有有限数量的未命名属性的类型。每个属性都有一个关联的类型。使用元组时,必须提供每个属性的值。为了更直观地理解元组的概念,我们来看一个具体的例子:

let tupleType: [string, boolean];
tupleType = ["Semlinker", true];

在上面代码中,我们定义了一个名为 tupleType 的变量,它的类型是一个类型数组 [string, boolean],然后我们按照正确的类型依次初始化 tupleType 变量。与数组一样,我们可以通过下标来访问元组中的元素:

console.log(tupleType[0]); // Semlinker
console.log(tupleType[1]); // true

在元组初始化的时候,如果出现类型不匹配的话,比如:

tupleType = [true, "Semlinker"];

此时,TypeScript 编译器会提示以下错误信息:

[0]: Type 'true' is not assignable to type 'string'.
[1]: Type 'string' is not assignable to type 'boolean'.

很明显是因为类型不匹配导致的。在元组初始化的时候,我们还必须提供每个属性的值,不然也会出现错误,比如:

tupleType = ["Semlinker"];

此时,TypeScript 编译器会提示以下错误信息:

Property '1' is missing in type '[string]' but required in type '[string, boolean]'.

2.9 Void 类型

某种程度上来说,void 类型像是与 any 类型相反,它表示没有任何类型。当一个函数没有返回值时,你通常会见到其返回值类型是 void:

// 声明函数返回值为void
function warnUser(): void {
  console.log("This is my warning message");
}

以上代码编译生成的 ES5 代码如下:

"use strict";
function warnUser() {
  console.log("This is my warning message");
}

需要注意的是,声明一个 void 类型的变量没有什么作用,因为它的值只能为 undefinednull

let unusable: void = undefined;

2.10 Null 和 Undefined 类型

TypeScript 里,undefinednull 两者有各自的类型分别为 undefinednull

let u: undefined = undefined;
let n: null = null;

默认情况下 nullundefined 是所有类型的子类型。 就是说你可以把 nullundefined 赋值给 number 类型的变量。然而,如果你指定了--strictNullChecks 标记,nullundefined 只能赋值给 void 和它们各自的类型。

2.11 Never 类型

never 类型表示的是那些永不存在的值的类型。 例如,never 类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型。

// 返回never的函数必须存在无法达到的终点
function error(message: string): never {
  throw new Error(message);
}

function infiniteLoop(): never {
  while (true) {}
}

在 TypeScript 中,可以利用 never 类型的特性来实现全面性检查,具体示例如下:

type Foo = string | number;

function controlFlowAnalysisWithNever(foo: Foo) {
  if (typeof foo === "string") {
    // 这里 foo 被收窄为 string 类型
  } else if (typeof foo === "number") {
    // 这里 foo 被收窄为 number 类型
  } else {
    // foo 在这里是 never
    const check: never = foo;
  }
}

注意在 else 分支里面,我们把收窄为 never 的 foo 赋值给一个显示声明的 never 变量。如果一切逻辑正确,那么这里应该能够编译通过。但是假如后来有一天你的同事修改了 Foo 的类型:

type Foo = string | number | boolean;

然而他忘记同时修改 controlFlowAnalysisWithNever 方法中的控制流程,这时候 else 分支的 foo 类型会被收窄为 boolean 类型,导致无法赋值给 never 类型,这时就会产生一个编译错误。通过这个方式,我们可以确保

controlFlowAnalysisWithNever 方法总是穷尽了 Foo 的所有可能类型。 通过这个示例,我们可以得出一个结论:使用 never 避免出现新增了联合类型没有对应的实现,目的就是写出类型绝对安全的代码。

三、TypeScript 断言

有时候你会遇到这样的情况,你会比 TypeScript 更了解某个值的详细信息。通常这会发生在你清楚地知道一个实体具有比它现有类型更确切的类型。

通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”。类型断言好比其他语言里的类型转换,但是不进行特殊的数据检查和解构。它没有运行时的影响,只是在编译阶段起作用。

类型断言有两种形式:

3.1 “尖括号” 语法

let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;

3.2 as 语法

let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;

四、类型守卫

A type guard is some expression that performs a runtime check that guarantees the type in some scope. —— TypeScript 官方文档

类型保护是可执行运行时检查的一种表达式,用于确保该类型在一定的范围内。换句话说,类型保护可以保证一个字符串是一个字符串,尽管它的值也可以是一个数值。类型保护与特性检测并不是完全不同,其主要思想是尝试检测属性、方法或原型,以确定如何处理值。目前主要有四种的方式来实现类型保护:

4.1 in 关键字

interface Admin {
  name: string;
  privileges: string[];
}

interface Employee {
  name: string;
  startDate: Date;
}

type UnknownEmployee = Employee | Admin;

function printEmployeeInformation(emp: UnknownEmployee) {
  console.log("Name: " + emp.name);
  if ("privileges" in emp) {
    console.log("Privileges: " + emp.privileges);
  }
  if ("startDate" in emp) {
    console.log("Start Date: " + emp.startDate);
  }
}

4.2 typeof 关键字

function padLeft(value: string, padding: string | number) {
  if (typeof padding === "number") {
      return Array(padding + 1).join(" ") + value;
  }
  if (typeof padding === "string") {
      return padding + value;
  }
  throw new Error(`Expected string or number, got '${padding}'.`);
}

typeof 类型保护只支持两种形式:typeof v === "typename"typeof v !== typename"typename" 必须是 "number""string""boolean""symbol"。 但是 TypeScript 并不会阻止你与其它字符串比较,语言不会把那些表达式识别为类型保护。

4.3 instanceof 关键字

interface Padder {
  getPaddingString(): string;
}

class SpaceRepeatingPadder implements Padder {
  constructor(private numSpaces: number) {}
  getPaddingString() {
    return Array(this.numSpaces + 1).join(" ");
  }
}

class StringPadder implements Padder {
  constructor(private value: string) {}
  getPaddingString() {
    return this.value;
  }
}

let padder: Padder = new SpaceRepeatingPadder(6);

if (padder instanceof SpaceRepeatingPadder) {
  // padder的类型收窄为 'SpaceRepeatingPadder'
}

4.4 自定义类型保护的类型谓词

function isNumber(x: any): x is number {
  return typeof x === "number";
}

function isString(x: any): x is string {
  return typeof x === "string";
}

五、联合类型和类型别名

5.1 联合类型

联合类型通常与 nullundefined 一起使用:

const sayHello = (name: string | undefined) => {
  /* ... */
};

例如,这里 name 的类型是 string | undefined 意味着可以将 stringundefined 的值传递给sayHello 函数。

sayHello("Semlinker");
sayHello(undefined);

通过这个示例,你可以凭直觉知道类型 A 和类型 B 联合后的类型是同时接受 A 和 B 值的类型。

5.2 可辨识联合

TypeScript 可辨识联合(Discriminated Unions)类型,也称为代数数据类型或标签联合类型。它包含 3 个要点:可辨识、联合类型和类型守卫。

这种类型的本质是结合联合类型和字面量类型的一种类型保护方法。如果一个类型是多个类型的联合类型,且多个类型含有一个公共属性,那么就可以利用这个公共属性,来创建不同的类型保护区块。

1.可辨识

可辨识要求联合类型中的每个元素都含有一个单例类型属性,比如:

enum CarTransmission {
  Automatic = 200,
  Manual = 300
}

interface Motorcycle {
  vType: "motorcycle"; // discriminant
  make: number; // year
}

interface Car {
  vType: "car"; // discriminant
  transmission: CarTransmission
}

interface Truck {
  vType: "truck"; // discriminant
  capacity: number; // in tons
}

在上述代码中,我们分别定义了 MotorcycleCarTruck 三个接口,在这些接口中都包含一个 vType 属性,该属性被称为可辨识的属性,而其它的属性只跟特性的接口相关。

2.联合类型

基于前面定义了三个接口,我们可以创建一个 Vehicle 联合类型:

type Vehicle = Motorcycle | Car | Truck;

现在我们就可以开始使用 Vehicle 联合类型,对于 Vehicle 类型的变量,它可以表示不同类型的车辆。

3.类型守卫

下面我们来定义一个 evaluatePrice 方法,该方法用于根据车辆的类型、容量和评估因子来计算价格,具体实现如下:

const EVALUATION_FACTOR = Math.PI; 
function evaluatePrice(vehicle: Vehicle) {
  return vehicle.capacity * EVALUATION_FACTOR;
}

const myTruck: Truck = { vType: "truck", capacity: 9.5 };
evaluatePrice(myTruck);

对于以上代码,TypeScript 编译器将会提示以下错误信息:

Property 'capacity' does not exist on type 'Vehicle'.
Property 'capacity' does not exist on type 'Motorcycle'.

原因是在 Motorcycle 接口中,并不存在 capacity 属性,而对于 Car 接口来说,它也不存在 capacity 属性。那么,现在我们应该如何解决以上问题呢?这时,我们可以使用类型守卫。下面我们来重构一下前面定义的 evaluatePrice 方法,重构后的代码如下:

function evaluatePrice(vehicle: Vehicle) {
  switch(vehicle.vType) {
    case "car":
      return vehicle.transmission * EVALUATION_FACTOR;
    case "truck":
      return vehicle.capacity * EVALUATION_FACTOR;
    case "motorcycle":
      return vehicle.make * EVALUATION_FACTOR;
  }
}

在以上代码中,我们使用 switchcase 运算符来实现类型守卫,从而确保在 evaluatePrice 方法中,我们可以安全地访问 vehicle 对象中的所包含的属性,来正确的计算该车辆类型所对应的价格。

5.3 类型别名

类型别名用来给一个类型起个新名字。

type Message = string | string[];

let greet = (message: Message) => {
  // ...
};

六、交叉类型

TypeScript 交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。

interface IPerson {
  id: string;
  age: number;
}

interface IWorker {
  companyId: string;
}

type IStaff = IPerson & IWorker;

const staff: IStaff = {
  id: 'E1006',
  age: 33,
  companyId: 'EFT'
};

console.dir(staff)

在上面示例中,我们首先为 IPerson 和 IWorker 类型定义了不同的成员,然后通过 & 运算符定义了 IStaff 交叉类型,所以该类型同时拥有 IPerson 和 IWorker 这两种类型的成员。

七、TypeScript 函数

7.1 TypeScript 函数与 JavaScript 函数的区别

TypeScriptJavaScript
含有类型无类型
箭头函数箭头函数(ES2015)
函数类型无函数类型
必填和可选参数所有参数都是可选的
默认参数默认参数
剩余参数剩余参数
函数重载无函数重载

7.2 箭头函数

1.常见语法

myBooks.forEach(() => console.log('reading'));

myBooks.forEach(title => console.log(title));

myBooks.forEach((title, idx, arr) =>
  console.log(idx + '-' + title);
);

myBooks.forEach((title, idx, arr) => {
  console.log(idx + '-' + title);
});

2.使用示例

// 未使用箭头函数
function Book() {
  let self = this;
  self.publishDate = 2016;
  setInterval(function () {
    console.log(self.publishDate);
  }, 1000);
}

// 使用箭头函数
function Book() {
  this.publishDate = 2016;
  setInterval(() => {
    console.log(this.publishDate);
  }, 1000);
}

7.3 参数类型和返回类型

function createUserId(name: string, id: number): string {
  return name + id;
}

7.4 函数类型

let IdGenerator: (chars: string, nums: number) => string;

function createUserId(name: string, id: number): string {
  return name + id;
}

IdGenerator = createUserId;

7.5 可选参数及默认参数

// 可选参数
function createUserId(name: string, id: number, age?: number): string {
  return name + id;
}

// 默认参数
function createUserId(
  name: string = "Semlinker",
  id: number,
  age?: number
): string {
  return name + id;
}

在声明函数时,可以通过 ? 号来定义可选参数,比如 age?: number 这种形式。在实际使用时,需要注意的是可选参数要放在普通参数的后面,不然会导致编译错误。

7.6 剩余参数

function push(array, ...items) {
  items.forEach(function (item) {
    array.push(item);
  });
}

let a = [];
push(a, 1, 2, 3);

7.7 函数重载

函数重载或方法重载是使用相同名称和不同参数数量或类型创建多个方法的一种能力。要解决前面遇到的问题,方法就是为同一个函数提供多个函数类型定义来进行函数重载,编译器会根据这个列表去处理函数的调用。

function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: string, b: number): string;
function add(a: number, b: string): string;
function add(a: Combinable, b: Combinable) {
  if (typeof a === "string" || typeof b === "string") {
    return a.toString() + b.toString();
  }
  return a + b;
}

在以上代码中,我们为 add 函数提供了多个函数类型定义,从而实现函数的重载。之后,可恶的错误消息又消失了,因为这时 result 变量的类型是 string 类型。在 TypeScript 中除了可以重载普通函数之外,我们还可以重载类中的成员方法。

方法重载是指在同一个类中方法同名,参数不同(参数类型不同、参数个数不同或参数个数相同时参数的先后顺序不同),调用时根据实参的形式,选择与它匹配的方法执行操作的一种技术。所以类中成员方法满足重载的条件是:在同一个类中,方法名相同且参数列表不同。下面我们来举一个成员方法重载的例子:

class Calculator {
  add(a: number, b: number): number;
  add(a: string, b: string): string;
  add(a: string, b: number): string;
  add(a: number, b: string): string;
  add(a: Combinable, b: Combinable) {
    if (typeof a === "string" || typeof b === "string") {
      return a.toString() + b.toString();
    }
    return a + b;
  }
}

const calculator = new Calculator();
const result = calculator.add("Semlinker", " Kakuqo");

这里需要注意的是,当 TypeScript 编译器处理函数重载时,它会查找重载列表,尝试使用第一个重载定义。 如果匹配的话就使用这个。 因此,在定义重载的时候,一定要把最精确的定义放在最前面。另外在 Calculator 类中,add(a: Combinable, b: Combinable){ } 并不是重载列表的一部分,因此对于 add 成员方法来说,我们只定义了四个重载方法。

八、TypeScript 数组

8.1 数组解构

let x: number; let y: number; let z: number;
let five_array = [0,1,2,3,4];
[x,y,z] = five_array;

8.2 数组展开运算符

let two_array = [0, 1];
let five_array = [...two_array, 2, 3, 4];

8.3 数组遍历

let colors: string[] = ["red", "green", "blue"];
for (let i of colors) {
  console.log(i);
}

九、TypeScript 对象

9.1 对象解构

let person = {
  name: "Semlinker",
  gender: "Male",
};

let { name, gender } = person;

9.2 对象展开运算符

let person = {
  name: "Semlinker",
  gender: "Male",
  address: "Xiamen",
};

// 组装对象
let personWithAge = { ...person, age: 33 };

// 获取除了某些项外的其它项
let { name, ...rest } = person;

十、TypeScript 接口

在面向对象语言中,接口是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类去实现。

TypeScript 中的接口是一个非常灵活的概念,除了可用于对类的一部分行为进行抽象以外,也常用于对「对象的形状(Shape)」进行描述。

10.1 对象的形状

interface Person {
  name: string;
  age: number;
}

let Semlinker: Person = {
  name: "Semlinker",
  age: 33,
};

10.2 可选 | 只读属性

interface Person {
  readonly name: string;
  age?: number;
}

只读属性用于限制只能在对象刚刚创建的时候修改其值。此外 TypeScript 还提供了 ReadonlyArray<T> 类型,它与 Array<T> 相似,只是把所有可变方法去掉了,因此可以确保数组创建后再也不能被修改。

let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error!

十一、TypeScript 类

11.1 类的属性与方法

在面向对象语言中,类是一种面向对象计算机编程语言的构造,是创建对象的蓝图,描述了所创建的对象共同的属性和方法。

在 TypeScript 中,我们可以通过 Class 关键字来定义一个类:

class Greeter {
  // 静态属性
  static cname: string = "Greeter";
  // 成员属性
  greeting: string;

  // 构造函数 - 执行初始化操作
  constructor(message: string) {
    this.greeting = message;
  }

  // 静态方法
  static getClassName() {
    return "Class name is Greeter";
  }

  // 成员方法
  greet() {
    return "Hello, " + this.greeting;
  }
}

let greeter = new Greeter("world");

那么成员属性与静态属性,成员方法与静态方法有什么区别呢?这里无需过多解释,我们直接看一下以下编译生成的 ES5 代码:

"use strict";
var Greeter = /** @class */ (function () {
    // 构造函数 - 执行初始化操作
    function Greeter(message) {
        this.greeting = message;
    }
    // 静态方法
    Greeter.getClassName = function () {
        return "Class name is Greeter";
    };
    // 成员方法
    Greeter.prototype.greet = function () {
        return "Hello, " + this.greeting;
    };
    // 静态属性
    Greeter.cname = "Greeter";
    return Greeter;
}());
var greeter = new Greeter("world");

11.2 访问器

在 TypeScript 中,我们可以通过 gettersetter 方法来实现数据的封装和有效性校验,防止出现异常数据。

let passcode = "Hello TypeScript";

class Employee {
  private _fullName: string;

  get fullName(): string {
    return this._fullName;
  }

  set fullName(newName: string) {
    if (passcode && passcode == "Hello TypeScript") {
      this._fullName = newName;
    } else {
      console.log("Error: Unauthorized update of employee!");
    }
  }
}

let employee = new Employee();
employee.fullName = "Semlinker";
if (employee.fullName) {
  console.log(employee.fullName);
}

11.3 类的继承

继承 (Inheritance) 是一种联结类与类的层次模型。指的是一个类(称为子类、子接口)继承另外的一个类(称为父类、父接口)的功能,并可以增加它自己的新功能的能力,继承是类与类或者接口与接口之间最常见的关系。

继承是一种 is-a 关系:

ts-is-a.jpeg

在 TypeScript 中,我们可以通过 extends 关键字来实现继承:

class Animal {
  name: string;
  
  constructor(theName: string) {
    this.name = theName;
  }
  
  move(distanceInMeters: number = 0) {
    console.log(`${this.name} moved ${distanceInMeters}m.`);
  }
}

class Snake extends Animal {
  constructor(name: string) {
    super(name);
  }
  
  move(distanceInMeters = 5) {
    console.log("Slithering...");
    super.move(distanceInMeters);
  }
}

let sam = new Snake("Sammy the Python");
sam.move();

11.4 ECMAScript 私有字段

在 TypeScript 3.8 版本就开始支持ECMAScript 私有字段,使用方式如下:

class Person {
  #name: string;

  constructor(name: string) {
    this.#name = name;
  }

  greet() {
    console.log(`Hello, my name is ${this.#name}!`);
  }
}

let semlinker = new Person("Semlinker");

semlinker.#name;
//     ~~~~~
// Property '#name' is not accessible outside class 'Person'
// because it has a private identifier.

与常规属性(甚至使用 private 修饰符声明的属性)不同,私有字段要牢记以下规则:

  • 私有字段以 # 字符开头,有时我们称之为私有名称;
  • 每个私有字段名称都唯一地限定于其包含的类;
  • 不能在私有字段上使用 TypeScript 可访问性修饰符(如 public 或 private);
  • 私有字段不能在包含的类之外访问,甚至不能被检测到。

十二、TypeScript 泛型

软件工程中,我们不仅要创建一致的定义良好的 API,同时也要考虑可重用性。 组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。

在像 C# 和 Java 这样的语言中,可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。 这样用户就可以以自己的数据类型来使用组件。

设计泛型的关键目的是在成员之间提供有意义的约束,这些成员可以是:类的实例成员、类的方法、函数参数和函数返回值。

泛型(Generics)是允许同一个函数接受不同类型参数的一种模板。相比于使用 any 类型,使用泛型来创建可复用的组件要更好,因为泛型会保留参数类型。

12.1 泛型接口

interface GenericIdentityFn<T> {
  (arg: T): T;
}

12.2 泛型类

class GenericNumber<T> {
  zeroValue: T;
  add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) {
  return x + y;
};

12.3 泛型变量

对刚接触 TypeScript 泛型的小伙伴来说,看到 T 和 E,还有 K 和 V 这些泛型变量时,估计会一脸懵逼。其实这些大写字母并没有什么本质的区别,只不过是一个约定好的规范而已。也就是说使用大写字母 A-Z 定义的类型变量都属于泛型,把 T 换成 A,也是一样的。下面我们介绍一下一些常见泛型变量代表的意思:

  • T(Type):表示一个 TypeScript 类型
  • K(Key):表示对象中的键类型
  • V(Value):表示对象中的值类型
  • E(Element):表示元素类型

12.4 泛型工具类型

为了方便开发者 TypeScript 内置了一些常用的工具类型,比如 Partial、Required、Readonly、Record 和 ReturnType 等。出于篇幅考虑,这里我们只简单介绍 Partial 工具类型。不过在具体介绍之前,我们得先介绍一些相关的基础知识,方便读者自行学习其它的工具类型。

1.typeof

在 TypeScript 中,typeof 操作符可以用来获取一个变量声明或对象的类型。

interface Person {
  name: string;
  age: number;
}

const sem: Person = { name: 'semlinker', age: 30 };
type Sem= typeof sem; // -> Person

function toArray(x: number): Array<number> {
  return [x];
}

type Func = typeof toArray; // -> (x: number) => number[]

2.keyof

keyof 操作符可以用来一个对象中的所有 key 值:

interface Person {
    name: string;
    age: number;
}

type K1 = keyof Person; // "name" | "age"
type K2 = keyof Person[]; // "length" | "toString" | "pop" | "push" | "concat" | "join" 
type K3 = keyof { [x: string]: Person };  // string | number

3.in

in 用来遍历枚举类型:

type Keys = "a" | "b" | "c"

type Obj =  {
  [p in Keys]: any
} // -> { a: any, b: any, c: any }

4.infer

在条件类型语句中,可以用 infer 声明一个类型变量并且对它进行使用。

type ReturnType<T> = T extends (
  ...args: any[]
) => infer R ? R : any;

以上代码中 infer R 就是声明一个变量来承载传入函数签名的返回值类型,简单说就是用它取到函数返回值的类型方便之后使用。

5.extends

有时候我们定义的泛型不想过于灵活或者说想继承某些类等,可以通过 extends 关键字添加泛型约束。

interface ILengthwise {
  length: number;
}

function loggingIdentity<T extends ILengthwise>(arg: T): T {
  console.log(arg.length);
  return arg;
}

现在这个泛型函数被定义了约束,因此它不再是适用于任意类型:

loggingIdentity(3);  // Error, number doesn't have a .length property

这时我们需要传入符合约束类型的值,必须包含必须的属性:

loggingIdentity({length: 10, value: 3});

6.Partial

Partial<T> 的作用就是将某个类型里的属性全部变为可选项 ?

定义:

/**
 * node_modules/typescript/lib/lib.es5.d.ts
 * Make all properties in T optional
 */
type Partial<T> = {
  [P in keyof T]?: T[P];
};

在以上代码中,首先通过 keyof T 拿到 T 的所有属性名,然后使用 in 进行遍历,将值赋给 P,最后通过 T[P] 取得相应的属性值。中间的 ? 号,用于将所有属性变为可选。

示例:

interface Todo {
  title: string;
  description: string;
}

function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
  return { ...todo, ...fieldsToUpdate };
}

const todo1 = {
  title: "organize desk",
  description: "clear clutter",
};

const todo2 = updateTodo(todo1, {
  description: "throw out trash",
});

在上面的 updateTodo 方法中,我们利用 Partial<T> 工具类型,定义 fieldsToUpdate 的类型为 Partial<Todo>,即:

{
   title?: string | undefined;
   description?: string | undefined;
}

十三、TypeScript 装饰器

13.1 装饰器是什么

  • 它是一个表达式
  • 该表达式被执行后,返回一个函数
  • 函数的入参分别为 target、name 和 descriptor
  • 执行该函数后,可能返回 descriptor 对象,用于配置 target 对象

13.2 装饰器的分类

  • 类装饰器(Class decorators)
  • 属性装饰器(Property decorators)
  • 方法装饰器(Method decorators)
  • 参数装饰器(Parameter decorators)

13.3 类装饰器

类装饰器声明:

declare type ClassDecorator = <TFunction extends Function>(
  target: TFunction
) => TFunction | void;

类装饰器顾名思义,就是用来装饰类的。它接收一个参数:

  • target: TFunction - 被装饰的类

看完第一眼后,是不是感觉都不好了。没事,我们马上来个例子:

function Greeter(target: Function): void {
  target.prototype.greet = function (): void {
    console.log("Hello Semlinker!");
  };
}

@Greeter
class Greeting {
  constructor() {
    // 内部实现
  }
}

let myGreeting = new Greeting();
myGreeting.greet(); // console output: 'Hello Semlinker!';

上面的例子中,我们定义了 Greeter 类装饰器,同时我们使用了 @Greeter 语法糖,来使用装饰器。

友情提示:读者可以直接复制上面的代码,在 TypeScript Playground 中运行查看结果。

有的读者可能想问,例子中总是输出 Hello Semlinker! ,能自定义输出的问候语么 ?这个问题很好,答案是可以的。

具体实现如下:

function Greeter(greeting: string) {
  return function (target: Function) {
    target.prototype.greet = function (): void {
      console.log(greeting);
    };
  };
}

@Greeter("Hello TS!")
class Greeting {
  constructor() {
    // 内部实现
  }
}

let myGreeting = new Greeting();
myGreeting.greet(); // console output: 'Hello TS!';

13.4 属性装饰器

属性装饰器声明:

declare type PropertyDecorator = (target:Object, 
  propertyKey: string | symbol ) => void;

属性装饰器顾名思义,用来装饰类的属性。它接收两个参数:

  • target: Object - 被装饰的类
  • propertyKey: string | symbol - 被装饰类的属性名

趁热打铁,马上来个例子热热身:

function logProperty(target: any, key: string) {
  delete target[key];

  const backingField = "_" + key;

  Object.defineProperty(target, backingField, {
    writable: true,
    enumerable: true,
    configurable: true
  });

  // property getter
  const getter = function (this: any) {
    const currVal = this[backingField];
    console.log(`Get: ${key} => ${currVal}`);
    return currVal;
  };

  // property setter
  const setter = function (this: any, newVal: any) {
    console.log(`Set: ${key} => ${newVal}`);
    this[backingField] = newVal;
  };

  // Create new property with getter and setter
  Object.defineProperty(target, key, {
    get: getter,
    set: setter,
    enumerable: true,
    configurable: true
  });
}

class Person { 
  @logProperty
  public name: string;

  constructor(name : string) { 
    this.name = name;
  }
}

const p1 = new Person("semlinker");
p1.name = "kakuqo";

以上代码我们定义了一个 logProperty 函数,来跟踪用户对属性的操作,当代码成功运行后,在控制台会输出以下结果:

Set: name => semlinker
Set: name => kakuqo

13.5 方法装饰器

方法装饰器声明:

declare type MethodDecorator = <T>(target:Object, propertyKey: string | symbol,          
  descriptor: TypePropertyDescript<T>) => TypedPropertyDescriptor<T> | void;

方法装饰器顾名思义,用来装饰类的方法。它接收三个参数:

  • target: Object - 被装饰的类
  • propertyKey: string | symbol - 方法名
  • descriptor: TypePropertyDescript - 属性描述符

废话不多说,直接上例子:

function LogOutput(tarage: Function, key: string, descriptor: any) {
  let originalMethod = descriptor.value;
  let newMethod = function(...args: any[]): any {
    let result: any = originalMethod.apply(this, args);
    if(!this.loggedOutput) {
      this.loggedOutput = new Array<any>();
    }
    this.loggedOutput.push({
      method: key,
      parameters: args,
      output: result,
      timestamp: new Date()
    });
    return result;
  };
  descriptor.value = newMethod;
}

class Calculator {
  @LogOutput
  double (num: number): number {
    return num * 2;
  }
}

let calc = new Calculator();
calc.double(11);
// console ouput: [{method: "double", output: 22, ...}]
console.log(calc.loggedOutput); 

下面我们来介绍一下参数装饰器。

13.6 参数装饰器

参数装饰器声明:

declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, 
  parameterIndex: number ) => void

参数装饰器顾名思义,是用来装饰函数参数,它接收三个参数:

  • target: Object - 被装饰的类
  • propertyKey: string | symbol - 方法名
  • parameterIndex: number - 方法中参数的索引值
function Log(target: Function, key: string, parameterIndex: number) {
  let functionLogged = key || target.prototype.constructor.name;
  console.log(`The parameter in position ${parameterIndex} at ${functionLogged} has
    been decorated`);
}

class Greeter {
  greeting: string;
  constructor(@Log phrase: string) {
    this.greeting = phrase; 
  }
}

// console output: The parameter in position 0 
// at Greeter has been decorated

介绍完 TypeScript 入门相关的基础知识,猜测很多刚入门的小伙伴已有 “从入门到放弃” 的想法,最后我们来简单介绍一下编译上下文。

十四、编译上下文

14.1 tsconfig.json 的作用

  • 用于标识 TypeScript 项目的根路径;
  • 用于配置 TypeScript 编译器;
  • 用于指定编译的文件。

14.2 tsconfig.json 重要字段

  • files - 设置要编译的文件的名称;
  • include - 设置需要进行编译的文件,支持路径模式匹配;
  • exclude - 设置无需进行编译的文件,支持路径模式匹配;
  • compilerOptions - 设置与编译流程相关的选项。

14.3 compilerOptions 选项

compilerOptions 支持很多选项,常见的有 baseUrltargetbaseUrlmoduleResolutionlib 等。

compilerOptions 每个选项的详细说明如下:

{
  "compilerOptions": {

    /* 基本选项 */
    "target": "es5",                       // 指定 ECMAScript 目标版本: 'ES3' (default), 'ES5', 'ES6'/'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'
    "module": "commonjs",                  // 指定使用模块: 'commonjs', 'amd', 'system', 'umd' or 'es2015'
    "lib": [],                             // 指定要包含在编译中的库文件
    "allowJs": true,                       // 允许编译 javascript 文件
    "checkJs": true,                       // 报告 javascript 文件中的错误
    "jsx": "preserve",                     // 指定 jsx 代码的生成: 'preserve', 'react-native', or 'react'
    "declaration": true,                   // 生成相应的 '.d.ts' 文件
    "sourceMap": true,                     // 生成相应的 '.map' 文件
    "outFile": "./",                       // 将输出文件合并为一个文件
    "outDir": "./",                        // 指定输出目录
    "rootDir": "./",                       // 用来控制输出目录结构 --outDir.
    "removeComments": true,                // 删除编译后的所有的注释
    "noEmit": true,                        // 不生成输出文件
    "importHelpers": true,                 // 从 tslib 导入辅助工具函数
    "isolatedModules": true,               // 将每个文件做为单独的模块 (与 'ts.transpileModule' 类似).

    /* 严格的类型检查选项 */
    "strict": true,                        // 启用所有严格类型检查选项
    "noImplicitAny": true,                 // 在表达式和声明上有隐含的 any类型时报错
    "strictNullChecks": true,              // 启用严格的 null 检查
    "noImplicitThis": true,                // 当 this 表达式值为 any 类型的时候,生成一个错误
    "alwaysStrict": true,                  // 以严格模式检查每个模块,并在每个文件里加入 'use strict'

    /* 额外的检查 */
    "noUnusedLocals": true,                // 有未使用的变量时,抛出错误
    "noUnusedParameters": true,            // 有未使用的参数时,抛出错误
    "noImplicitReturns": true,             // 并不是所有函数里的代码都有返回值时,抛出错误
    "noFallthroughCasesInSwitch": true,    // 报告 switch 语句的 fallthrough 错误。(即,不允许 switch 的 case 语句贯穿)

    /* 模块解析选项 */
    "moduleResolution": "node",            // 选择模块解析策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6)
    "baseUrl": "./",                       // 用于解析非相对模块名称的基目录
    "paths": {},                           // 模块名到基于 baseUrl 的路径映射的列表
    "rootDirs": [],                        // 根文件夹列表,其组合内容表示项目运行时的结构内容
    "typeRoots": [],                       // 包含类型声明的文件列表
    "types": [],                           // 需要包含的类型声明文件名列表
    "allowSyntheticDefaultImports": true,  // 允许从没有设置默认导出的模块中默认导入。

    /* Source Map Options */
    "sourceRoot": "./",                    // 指定调试器应该找到 TypeScript 文件而不是源文件的位置
    "mapRoot": "./",                       // 指定调试器应该找到映射文件而不是生成文件的位置
    "inlineSourceMap": true,               // 生成单个 soucemaps 文件,而不是将 sourcemaps 生成不同的文件
    "inlineSources": true,                 // 将代码与 sourcemaps 生成到一个文件中,要求同时设置了 --inlineSourceMap 或 --sourceMap 属性

    /* 其他选项 */
    "experimentalDecorators": true,        // 启用装饰器
    "emitDecoratorMetadata": true          // 为装饰器提供元数据的支持
  }
}

看到这里的读者都是“真爱”,如果你还意犹未尽,那就来看看本人整理的 Github 上 1.5K+ 的开源项目:awesome-typescript

https://github.com/semlinker/...

十五、参考资源

十六、推荐阅读

查看原文

赞 93 收藏 69 评论 2

易腾 关注了专栏 · 6月9日

全栈修仙之路

聚焦全栈,专注分享 Angular、TypeScript、Node.js/Java 、Spring 技术栈等全栈干货。 欢迎小伙伴们关注公众号全栈修仙之路,一起升级打怪。

关注 5786

易腾 发布了文章 · 6月9日

vue内input框限制只能输入手机号

前端开发中经常遇到手机号输入的情况,而产品往往会要求只能输入手机号,禁止混入其他内容;
最近整理了下之前几个项目的方法,找到一个在vue项目中的一个实用方法,分享一下;

这个方法使用的是 vue 自定义指令,之前一直没咋用,最近研究了下发现很实用;

/* 自定义指令 */
    directives: {
      numberOnly: {
        bind: function(e) {
          e.handler = function() {
            e.value = e.value.replace(/\D+/, '')
          }
          e.addEventListener('input', e.handler)
        },
        unbind: function(e) {
          e.removeEventListener('input', e.handler)
        }
      }
    },
 <input 
     v-model="phone" 
     name="phone" 
     pattern="[0-9]*"  
     v-number-only 
     class="y_input" 
     type="tel" 
     maxlength="11" 
     placeholder="请输入手机号">

分享一下,也算是个笔记

查看原文

赞 0 收藏 0 评论 0

易腾 回答了问题 · 6月5日

vue-cli3.0设置.env.development无法获取内容?

这是我的配置方法
package.json配置

//package.json
  "scripts": {
    "serve": "vue-cli-service serve",// 默认本地运行配置 .env.development
    "pro-serve": "vue-cli-service serve --mode production",// 本地运行生产配置.env.production
    "build": "vue-cli-service build",// 默认打包 .env.production
    "dev-test-build": "vue-cli-service build --mode test",// 我单独新建.env.test文件 打测试环境包
    "lint": "vue-cli-service lint"
  },

.env.test

//.env.test
NODE_ENV=production
VUE_APP_BASE_API=http://cs.com
// env文件内变量默认字符串,不需要加"" ''这类的

{E0398F16-523D-410A-8E3A-EDC1F96861E1}_20200605100349.jpg

图片.env.development内这地方把 ‘’ 号去掉

  1. env文件内不需要'',
  2. 而且.env.development文件不需要配置NODE_ENV,这文件本身默认NODE_ENV=development

关注 3 回答 2

易腾 收藏了文章 · 6月2日

Springboot 系列(一)Spring Boot 入门篇

注意:本 Spring Boot 系列文章基于 Spring Boot 版本 v2.1.1.RELEASE 进行学习分析,版本不同可能会有细微差别。

前言

由于 J2EE 的开发变得笨重,繁多的配置,错乱的依赖管理,低下的开发效率,复杂的部署流程,第三方技术的集成难度较大等。同时随着复杂项目的演进,微服务分布式架构思想逐渐进入开发者的视野。

1. Spring Boot 介绍

Spring Boot 提供了一组工具只需要极少的配置就可以快速的构建并启动基于 Spring 的应用程序。解决了传统 Spring 开发需要配置大量配置文件的痛点,同时 Spring Boot 对于第三方库设置了合理的默认值,可以快速的构建起应用程序。当然 Spring Boot 也可以轻松的自定义各种配置,无论是在开发的初始阶段还是投入生成的后期阶段。

<!-- more -->

2. Spring Boot 优点

  • 快速的创建可以独立运行的 Spring 项目以及与主流框架的集成。
  • 使用嵌入式的 Servlet 容器,用于不需要打成war包。
  • 使用很多的启动器(Starters)自动依赖与版本控制。
  • 大量的自动化配置,简化了开发,当然,我们也可以修改默认值。
  • 不需要配置 XML 文件,无代码生成,开箱即用。
  • 准生产环境的运行时应用监控。
  • 与云计算的天然集成。

3. Spring Boot 前置

说了那么多的 Spring Boot 的好处,那么使用 Spring Boot 需要哪些前置知识呢?我简单列举了一下。

  • Spring 框架的使用。
  • Maven 构建工具的使用。
  • IDEA 或其他开发工具的使用。

4. Spring Boot 体验

现在我们已经了解了 Spring Boot 是什么,下面我们将使用 Spring Boot 开发一个入门案例,来体验 Spring Boot 开发姿势是如何的优雅与迅速。
Spring Boot 官方已经为我们如何快速启动 Spring Boot 应用程序提供了多种方式。

你可以在 Spring 官方网站直接生成项目下载导入IDE进行开发。

https://start.spring.io/

也可以直接克隆 GitHub 上的初始项目进行体验。

git clone https://github.com/spring-guides/gs-spring-boot.git
cd gs-spring-boot/initial

这里我们选择后者,直接克隆进入到 initial 文件夹使用 maven 进行编译启动。

 mvn package && java -jar target/gs-spring-boot-0.1.0.jar

第一次编译需要下载需要的依赖,耗时会比较长,编译完成之后紧接着可以看到 Spring 的启动标志。这时 Spring Boot 的 web程序已经运行在8080端口了。

$ curl -s localhost:8080
Greetings from Spring Boot!

5. Spring Boot 开发

下面手动编写一个 Spring Boot 入门案例,快速的开发一个 web mvc 应用。
项目结构如下:

5.1 依赖项


    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

spring-boot-starter-parent 是Spring Boot 的核心依赖,它里面定义了各种在开发中会用到的第三方 jar 的版本信息,因此我们在引入其他的 Spring Boot 为我们封装的启动器的时候都不在需要指定版本信息。如果我们需要自定义版本信息,可以直接覆盖版本属性值即可。

spring-boot-starter-web 提供 web 以及 MVC 和 validator 等web开发框架的支持。

spring-boot-starter-test 提供测试模块的支持。如 Junit,Mockito。

需要说明的是,Spring Boot 为我们提供了很多的已经封装好的称为启动器(starter)的依赖项。让我们在使用的时候不需要再进行复杂的配置就可以迅速的进行应用集成。所有的官方启动器依赖可以在这里查看。

所有官方发布的启动器都遵循类似的命名模式; spring-boot-starter-*,这里*是指特定类型的应用程序。此命名结构旨在帮助您寻找启动器。

注意:编写自己的启动器的时候不应该使用这种命名方式。

5.2 启动类

@SpringBootApplication
public class HelloApplication {

    public static void main(String[] args) {
        SpringApplication.run(HelloApplication.class, args);
    }

    @Bean
    public CommandLineRunner commandLineRunner(ApplicationContext ctx) {
        return args -> {
            // 开始检查spring boot 提供的 beans
            System.out.println("Let's inspect the beans provided by Spring Boot:");
            String[] beanNames = ctx.getBeanDefinitionNames();
            Arrays.sort(beanNames);
            for (String beanName : beanNames) {
                System.out.println(beanName);
            }
        };
    }
}

@SpringBootApplication 注解是一个便利的注解,它包含了以下几个注解。

  1. @Configuration 定义配置类。
  2. @EnableAutoConfiguration 开启自动配置。
  3. @EnableWebMvc 标记为 web应用程序。
  4. @ComponentScan 组件扫描。

5.3 控制器

@RestController
public class HelloController {
    @RequestMapping("/")
    public String index() {
        return "Greetings from Spring Boot!";
    }
}

@RestController@Controller@ResponseBody 的结合体。

5.4 访问测试

直接启动 HelloApplication.java 类就可以在控制台看到启动输出,然后访问8080端口查看启动是否正常。

经过上面的例子,已经使用 Spring Boot 快速的创建了一个 web 应用并进行了简单的访问测试。

6. Spring Boot 单元测试

结合上面提到的 Spring Boot 启动器知识,Spring Boot 已经为我们提供了丰富的第三方框架,测试框架也不例外。

导入单元测试依赖。

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>    

6.1 模拟请求测试

编写单元测试

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;

import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

/**
 * 单元测试
 */
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class HelloApplicationTests {

    @Autowired
    private MockMvc mvc;

    @Test
    public void contextLoads() throws Exception {
        mvc.perform(MockMvcRequestBuilders.get("/").accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(content().string("Greetings from Spring Boot!"));
    }

}

关于上面代码的一些说明。

  • MockMvc 允许我们方便的发送 HTTP 请求。
  • SpringBootTest 方便的创建一个 Spring Boot 项目的测试程序。

运行没有任何异常说明程序测试通过。

6.2 Spring Boot 集成测试


import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;

import java.net.URL;

/**
 * <p>
 * 嵌入式服务器由随机端口启动webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
 * 并且在运行时发现实际端口@LocalServerPort
 *
 * @Author niujinpeng
 * @Date 2018/12/4 15:02
 */
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class HelloApplicationTestBySpringBoot {

    @LocalServerPort
    private int port;

    private URL base;

    @Autowired
    private TestRestTemplate template;

    @Before
    public void setup() throws Exception {
        this.base = new URL("http://localhost:" + port + "/");
    }

    @Test
    public void getHello() throws Exception {
        ResponseEntity<String> response = template.getForEntity(base.toString(), String.class);
        assert (response.getBody().equals("Greetings from Spring Boot!"));
    }

}

嵌入式服务器由随机端口启动 webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT

并且在运行时使用注解 @LocalServerPort 发现实际端口。

运行测试类通过输出。

2018-12-06 22:28:01.914  INFO 14320 --- [o-auto-1-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2018-12-06 22:28:01.914  INFO 14320 --- [o-auto-1-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2018-12-06 22:28:01.937  INFO 14320 --- [o-auto-1-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 23 ms

文章代码已经上传到 GitHub Spring Boot 入门案例

<完>
个人网站:https://www.codingme.net
如果你喜欢这篇文章,可以关注公众号,一起成长。
关注公众号回复资源可以没有套路的获取全网最火的的 Java 核心知识整理&面试资料。
公众号

查看原文

认证与成就

  • 获得 5 次点赞
  • 获得 9 枚徽章 获得 0 枚金徽章, 获得 2 枚银徽章, 获得 7 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-09-04
个人主页被 376 人浏览