第九章 前后端框架
9.1 前端框架
Typescript特别适合用于开发前端应用。Typescript对JSX有很好的支持,而且能安全地建模不可变性,从而提升应用的结构和安全性,写出的代码正确性高,便于维护。
9.1.1 React
JSX/TSX内容等
详情请到React官方学习
9.1.2 Angular
详情请到Angular学习
依赖注入,控制反转
9.1.3 Vue
详情请到Vue官方学习
Vue3全面支持Typescript
9.2 类型安全的API
- 针对REST式API的swagger
- 针对GraphQI的Apollo
- 针对RPC的gRPC
9.3 后端框架
Nest.js完全支持Typescript
通过Typescript访问数据库,TypeORM超好用
第十章 命名空间和模块
在一个程序中,我们可以在不同的层级上进行封装。在最底层,函数封装行为,对象和列表等数据结构封装数据。函数和数据还可以放在类中,还可以把函数放在单独的数据库或数据存储器中,把函数和数据放入独立的命名空间中。
通常一个类或一系列使用函数放在一个文件中。再向上一层,我们可能会把多个类或多组使用函数组织在一起,构建成包(package),发布到NPM中。
模块(module)是一个重要概念,
我们要知道编译器(TSC)是如何解析模块的,
要知道构建系统(Webpack,Gulp,Vite等)是如何解析模块的,
还要知道模块在运行时是如何加载 到应用的(使用<script/>
标签,SystemJS等)。
对JavaScript来说,这些操作往往都由单独的程序执行,有点混乱。CommonJS和ES2015模块标准简化了这三个程序之间的互操作,Webpack等强大的打包程序进一步抽象了这三个解析过程背后涉及的操作。
本章着重讨论这这三种工具中的第一种,即Typescript解析和编译模块所用的工具,第12章再讨论构建系统和运行时加载程序。
- 实现命名空间和模块的不同方式
- 导入和导出代码的不同方式
- 在代码增长的过程中弹性伸缩这些方法。
- 模块模式与脚本模式的比较
- 声明合并的概念及其作用
10.1 JavaScript模块简史
由于Typescript最终编译成JavaScript,并与JavaScript互操作,因此Typescript要支持JavaScript程序使用的各种模块标准。
起初(1995)JavaScript并不支持任何模块系统。由于没有模块,一切都在全局命名空间中声明,导致应用非常难以构建和弹性伸缩。而且,我们无法显式指明各个模块对外开放哪些API。
为了解析问题,人们通过对象或理解调用函数(IIFE)表达式模拟模块,把对象或IIFE附加到全局对象window上,供当前应用
window.emailListmModule = {
renderList(){}
}
window.emailComposerModule = {
renderComposer(){}
}
window.appModule = {
renderApp(){
window.emailListmModule.renderList()
window.emailComposerModule.renderComposer()
}
}
可是加载和运行JavaScript会阻塞浏览器UI,浏览器会变得越来越慢。
于是有了新方案:在页面加载完成之后动态加载JavaScript,而不同时加载。
在页面加载完毕之后惰性(通常是异步)加载JavaScript代码,为了实现异步惰性,需要做到以下三点:
- 模块要适当的封装,否则,不断流入的依赖会导致页面崩溃
- 模块之间的依赖要显示声明,否则,不知道需要加载什么模块,以及以什么顺序加载。
- 每个模块在应用中都要有一个唯一标识符。否则,无法指定需要加载哪个模块。
nodejs大哥采用用的是Commonjs模块标准
// emailBaseModule.js
let emailList = require('emailListModule')
let emailComposer = require("emailComposerModule")
module.exports.renderBase = function(){}
可是Commjs处理事情的方式有几个问题,包括:require必须同步调用,Commonjs模块解析算法不适合在web使用。Commjs的代码在某些情况下不会做静态分析。
因为module.exports可能出现在任何位置(甚至可能在不会执行的分支),require调用可以出现任何位置,而且可以包含任意字符串和表达式,就导致静态链接JavaScript,不能确认被 引用的文件真实存在,也无法确保被引用的文件真正的到处了声称的代码。
然后,ECMAScript语言第六版,即ES2015为导入和导出引入了一个新标准,句法更简洁,而且支持静态分析。
// emailBaseModule.js
import emailList from 'emialListModule'
import emailComposer from 'emailCpmposerModule'
export function rederBase(){
}
如今,我们在JavaScript和Typescript中使用的就是这个标准。不是每个JavaScript引擎都原生支持这个标准,所以要把代码编译成被支持的格式(在nodejs中编译成commonjs格式,在浏览器环境中,编译成全局或单个模块可加载的格式)
TSC的构建系统还支持针对不同的目标环境编译模块,包括:全局,ES2015,Commonjs,AMD,Systemjs,UMD。
10.2 import,export
除非迫不得已,不要在Typescript中使用Commjs,全局或命名空间下的模块。
// a.ts
export function foo(){}
export function bar(){}
import {foo,bar} from './a'
export let result = bar()
// ./c.ts
export default function meow(){}
import meow form './c'
meow()
import * as a from './a'
a.foo()
export * from './a'
export {result} from './b'
export meow from './'
由于我们编写的是Typescript,而不是JavaScript代码,所以,除了值外,还可以导出类型和接口。因为类型和值位于不同的命名空间中,所以可以导出两个同名的内容,一个在值层面,一个在类型层面。Typescript将推导出你值得是类型还是值
// g.ts
export let X = 3;
export type X = {y:string}
// h.ts
import {X} from './g'
let a = X + 1
let b:X = {y:'z'}
10.2.1 动态导入
应用变大后,初次渲染的时间将不断增加。这对前端应用来说是不可忽视的问题。
进一步的优化措施是惰性加载分块,在真正使用的时候才加载。
LABjs及其衍生库引入了真正需要时才惰性加载的概念,这个概念正式的名称是"动态导入"(dynamic imports),用法
let locale = awat import('locale_cn-en')
import可以作为一个语句使用,作用是静态获取代码,另外,也可以作为一个函数调用,此时返回结果是一个Promise
动态导入应该使用下面两种做法中的其中一种:
-
直接把字符串字面量传给import,不要事先赋值给变量
-
把表达式传给import,但要手动注解模块的签名
import {locale} form ''
async function main(){
let userlocal = awat getuserlocal()
let path = "./loca-${userlocal}"
let local:typeof locale = await import(path)
}
导入locale不过只作为类型,这样写出代码不仅具有十足的类型安全,而且能动态计算导入那个包。
10.2.2使用Commonjs和AMD模块
使用Commjs或AMD标准的JavaScript模块时,可以直接从模块中导入名称,就像使用es2015模块一样
import {something} from './commonjs/module'
默认情况下,commonjs的默认导出不能与es2015的默认导出互操作:使用默认 导出,要借助通配符:
import * as fs from 'fs'
fs.readFile('some/file.txt')
// import { readFile } from "fs";
// readFile
// import * as fs from 'fs'
// fs.readFile
10.2.3 模块模式与脚本模式
Typescript采用两种模式编译Typescript文件:模块模式和脚本模式,
文件中有没有import或export语句,如果有使用模块模式,如果没有,使用脚本模式
目前为止我们使用的都是模块模式,
在脚本模式下,声明的顶层变量子啊项目中的任何文件都可以使用,无需导入,可以放心使用UMD模块中的全局导出,不用事先导入,下述情况使用脚本模式:
- 快速验证不打算编译成任何模块系统的浏览器代码({"module":"none"}),在html
文件中直接使用<script/>
标签引入 - 创建类型声明(11.1节)
其实,在使用Typescript编写代码时,基本上应该始终使用模块模式,通过import导入代码,通过export导出代码,供其他文件使用。
10.3 命名空间
Typescript还提供了另一种封装代码的方式:namespace关键字。很多java,C#,C++,PHP和python都支持。
要注意一点:虽然Typescript支持命名空间,但这并不是封装代码的首选方式。如果你不确定使用命名空间还是模块,首选模块!
命名空间所做的抽象摒弃了文件在文件系统中的目录结构,我们无需知道.mine函数保存在a/b/c/d中,若想使用这个函数,使用简洁的命名空间A.B.C.D.mine即可
假设有两个文件,一个文件是发起http get请求的模块,另一个文件使用该模块发起请求:
// Get.ts
namespace Network {
export function get<T>(url: string): Promise<T> {
return new Promise(()=>{})
}
}
// App.ts
namespace App {
Network.get<GitRepo>("api.github.com/repos/")
}
命名空间必须有名称,命名空间可以导出函数,变量,类型,接口和其他命名空间。namespacekuai中没有显示导出的代码为所在块的私有代码。由于命名空间可以导出命名空间,因此命名空间可以嵌套。
namespace Netwok {
export namespace HTTP {
export function get<T>(url:string):Promise<T>{
return new Promise(()=>{})
}
}
export namespace TCP{
export function listenerCount(port:number){
}
function test(){
}
}
export namespace UDP{
}
}
Netwok.HTTP.get
Netwok.TCP.listenerCount
Netwok.TCP.test // 找不到
命名空间也可以分散在多个文件中。Typescript将递归合并名称相同的命名空间
为了使用方便,可以创建别名
namespace A {
export namespace B{
export namespace C {
export let d = 3
}
}
}
import d = A.B.C.d
let e = d*3
console.log(e);//9
10.3.1 冲突
导出相同名称的代码会导致冲突:
// 报错
namespace Network {
export function request(){}
}
namespace Network {
export function request(){}
}
特例,改进函数类型时对外参(ambient)函数声明的重载不导致冲突
10.3.2 编译输出
与导入和导出不一样,命名空间不遵循tsconfig.json中的module设置,始终编译成全局变量。
模块优于命名空间
为了更符合JavaScript标准,为了更明确地指定依赖,较之命名空间,应该更多的使用常规的模块
明确指明依赖的好处很多,可以提示代码可读性,可以强制模块隔离,还可以做静态分析。
在大型项目中,建议全部使用模块,不要用命名空间
10.4 声明合并
目前,我们接触到了Typescript所做的三种合并:
- 合并值和类型,根据使用情况区分一个名称是引用值还是类型
- 把多个命名空间合并成一个
- 把多个接口合并成一个
Typescript支持很多类型的合并,让一些难以表达的模式变为可能。