Typescript第九/十章 前后端框架,命名空间和模块

第九章 前后端框架

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代码,为了实现异步惰性,需要做到以下三点:

  1. 模块要适当的封装,否则,不断流入的依赖会导致页面崩溃
  2. 模块之间的依赖要显示声明,否则,不知道需要加载什么模块,以及以什么顺序加载。
  3. 每个模块在应用中都要有一个唯一标识符。否则,无法指定需要加载哪个模块。

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

动态导入应该使用下面两种做法中的其中一种:

  1. 直接把字符串字面量传给import,不要事先赋值给变量

  2. 把表达式传给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模块中的全局导出,不用事先导入,下述情况使用脚本模式:

  1. 快速验证不打算编译成任何模块系统的浏览器代码({"module":"none"}),在html文件中直接使用<script/>标签引入
  2. 创建类型声明(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支持很多类型的合并,让一些难以表达的模式变为可能。

相关推荐
却尘14 小时前
Next.js 请求最佳实践 - vercel 2026一月发布指南
前端·react.js·next.js
ccnocare14 小时前
浅浅看一下设计模式
前端
Lee川14 小时前
🎬 从标签到屏幕:揭秘现代网页构建与适配之道
前端·面试
Ticnix15 小时前
ECharts初始化、销毁、resize 适配组件封装(含完整封装代码)
前端·echarts
纯爱掌门人15 小时前
终焉轮回里,藏着 AI 与人类的答案
前端·人工智能·aigc
twl15 小时前
OpenClaw 深度技术解析
前端
崔庆才丨静觅15 小时前
比官方便宜一半以上!Grok API 申请及使用
前端
星光不问赶路人15 小时前
vue3使用jsx语法详解
前端·vue.js
天蓝色的鱼鱼15 小时前
shadcn/ui,给你一个真正可控的UI组件库
前端
布列瑟农的星空15 小时前
前端都能看懂的Rust入门教程(三)——控制流语句
前端·后端·rust