TypeScript在node项目中的实践
TypeScript在node项目中的实践
TypeScript可以理解为是JavaScript的一个超集,也就是说涵盖了所有JavaScript的功能,并在之上有着自己独特的语法。
最近的一个新项目开始了TS的踩坑之旅,现分享一些可以借鉴的套路给大家。
为什么选择TS
作为巨硬公司出品的一个静态强类型编译型语言,该语言已经出现了几年的时间了,相信在社区的维护下,已经是一门很稳定的语言。
我们知道,JavaScript是一门动态弱类型解释型脚本语言,动态带来了很多的便利,我们可以在代码运行中随意的修改变量类型以达到预期目的。
但同时,这是一把双刃剑,当一个庞大的项目出现在你的面前,面对无比复杂的逻辑,你很难通过代码看出某个变量是什么类型,这个变量要做什么,很可能一不小心就会踩到坑。
而静态强类型编译能够带来很多的好处,其中最重要的一点就是可以帮助开发人员杜绝一些马虎大意的问题:
图为rollbar统计的数千个项目中数量最多的前十个异常
不难看出,因为类型不匹配、变量为空导致的异常比你敢承认的次数要多。
譬如
而这一点在TS中得到了很好的改善,任何一个变量的引用,都需要指定自己的类型,而你下边在代码中可以用什么,支持什么方法,都需要在上边进行定义:
这个提示会在开发、编译期来提示给开发者,避免了上线以后发现有问题,再去修改。
另外一个由静态编译类型带来的好处,就是函数签名。
还是就像上边所说的,因为是一个动态的脚本语言,所以很难有编辑器能够在开发期间正确地告诉你所要调用的一个函数需要传递什么参数,函数会返回什么类型的返回值。
而在TS中,对于一个函数,首先你需要定义所有参数的类型,以及返回值的类型。
这样在函数被调用时,我们就可以很清晰的看到这个函数的效果:
这是最基础的、能够让程序更加稳定的两个特性,当然,还有更多的功能在TS中的:TypeScript | Handbook
TypeScript在node中的应用
在TS的官网中,有着大量的示例,其中就找到了Express版本的例子,针对这个稍作修饰,应用在了一个 koa 项目中。
环境依赖
在使用TS之前,需要先准备这些东西:
- VS code,同为巨硬公司出品,本身就是TS开发的,遂该编辑器是目前对TS支持度最高的一个
- Node.js 推荐8.11版本以上
-
npm i -g typescript
,全局安装TS,编译所使用的tsc命令在这里 -
npm i -g nodemon
,全局安装nodemon,在tsc编译后自动刷新服务器程序
以项目中使用的一些核心依赖:
-
reflect-metadata
: 大量装饰器的包都会依赖的一个基础包,用于注入数据 -
routing-controllers
: 使用装饰器的方式来进行koa-router的开发 -
sequelize
: 抽象化的数据库操作 -
sequelize-typescript
: 上述插件的装饰器版本,定义实体时使用
项目结构
首先,放出目前项目的结构:
. ├── README.md ├── copy-static-assets.ts ├── nodemon.json ├── package-lock.json ├── package.json ├── dist ├── src │ ├── config │ ├── controllers │ ├── entity │ ├── models │ ├── middleware │ ├── public │ ├── app.ts │ ├── server.ts │ ├── types │ └── utils ├── tsconfig.json └── tslint.json
src
为主要开发目录,所有的TS代码都在这里边,在经过编译过后,会生成一个与src
同级的dist
文件夹,这个文件夹是node
引擎实际运行的代码。
在src
下,主要代码分为了如下结构(依据自己项目的实际情况进行增删):
# | folder | desc |
---|---|---|
1 | controllers | 用于处理接口请求,原apps 、routes 文件夹。 |
2 | middleware | 存放了各种中间件、全局 or 自定义的中间件 |
3 | config | 各种配置项的位置,包括端口、log 路径、各种巴拉巴拉的常量定义。 |
4 | entity | 这里存放的是所有的实体定义(使用了sequelize进行数据库操作)。 |
5 | models | 使用来自entity 中的实体进行sequelize 来完成初始化的操作,并将sequelize 对象抛出。 |
6 | utils | 存放的各种日常开发中提炼出来的公共函数 |
7 | types | 存放了各种客制化的复合类型的定义,各种结构、属性、方法返回值的定义(目前包括常用的Promise版redis与qconf) |
controllers
controllers只负责处理逻辑,通过操作model对象,而不是数据库来进行数据的增删改查
鉴于公司绝大部分的Node项目版本都已经升级到了Node 8.11
,理所应当的,我们会尝试新的语法。
也就是说我们会抛弃Generator
,拥抱async
/await
。
使用Koa
、Express
写过接口的童鞋应该都知道,当一个项目变得庞大,实际上会产生很多重复的非逻辑代码:
router.get('/', ctx => {}) router.get('/page1', ctx => {}) router.get('/page2', ctx => {}) router.get('/page3', ctx => {}) router.get('/pageN', ctx => {})
而在每个路由监听中,又做着大量重复的工作:
router.get('/', ctx => { let uid = Number(ctx.cookies.get('uid')) let device = ctx.headers['device'] || 'ios' let { tel, name } = ctx.query })
几乎每一个路由的头部都是在做着获取参数的工作,而参数很可能来自header
、body
甚至是cookie
及query
。
所以,我们对原来koa的使用方法进行了一个较大的改动,并使用routing-controllers大量的应用装饰器来帮助我们处理大部分的非逻辑代码。
原有router的定义:
module.exports = function (router) { router.get('/', function* (next) { let uid = Number(this.cookies.get('uid')) let device = this.headers['device'] this.body = { code: 200 } }) }
使用了TypeScript与装饰器的定义:
@Controller export default class { @Get('/') async index ( @CookieParam('uid') uid: number, @HeaderParam('device') device: string ) { return { code: 200 } } }
为了使接口更易于检索、更清晰,所以我们抛弃了原有的bd-router
的功能(依据文件路径作为接口路径、TS中的文件路径仅用于文件分层)。
直接在controllers
下的文件中声明对应的接口进行监听。
middleware
如果是全局的中间件,则直接在class上添加@Middleware
装饰器,并设置type: 'after|before'
即可。
如果是特定的一些中间件,则创建一个普通的class即可,然后在需要使用的controller
对象上指定@UseBefore
/@UseAfter
(可以写在class上,也可以写在method上)。
所有的中间件都需要继承对应的MiddlewareInterface接口,并需要实现use
方法
// middleware/xxx.ts import {ExpressMiddlewareInterface} from "../../src/driver/express/ExpressMiddlewareInterface" export class CompressionMiddleware implements KoaMiddlewareInterface { use(request: any, response: any, next?: Function): any { console.log("hello compression ...") next() } } // controllers/xxx.ts @UseBefore(CompressionMiddleware) export default class { }
entity
文件只负责定义数据模型,不做任何逻辑操作
同样的使用了sequelize+装饰器的方式,entity只是用来建立与数据库之间通讯的数据模型。
import { Model, Table, Column } from 'sequelize-typescript' @Table({ tableName: 'user_info_test' }) export default class UserInfo extends Model<UserInfo> { @Column({ comment: '自增ID', autoIncrement: true, primaryKey: true }) uid: number @Column({ comment: '姓名' }) name: string @Column({ comment: '年龄', defaultValue: 0 }) age: number @Column({ comment: '性别' }) gender: number }
因为sequelize建立连接也是需要对应的数据库地址、账户、密码、database等信息、所以推荐将同一个数据库的所有实体放在一个目录下,方便sequelize加载对应的模型
同步的推荐在config下创建对应的配置信息,并添加一列用于存放实体的key。
这样在建立数据库链接,加载数据模型时就可以动态的导入该路径下的所有实体:
// config.ts export const config = { // ... mysql1: { // ... config + entity: 'entity1' // 添加一列用来标识是什么实体的key }, mysql2: { // ... config + entity: 'entity2' // 添加一列用来标识是什么实体的key } // ... } // utils/mysql.ts new Sequelize({ // ... modelPath: [path.reolve(__dirname, `../entity/${config.mysql1.entity}`)] // ... })
model
model的定位在于根据对应的实体创建抽象化的数据库对象,因为使用了sequelize,所以该目录下的文件会变得非常简洁。
基本就是初始化sequelize对象,并在加载模型后将其抛出。
export default new Sequelize({ host: '127.0.0.1', database: 'database', username: 'user', password: 'password', dialect: 'mysql', // 或者一些其他的数据库 modelPaths: [path.resolve(__dirname, `../entity/${configs.mysql1.entity}`)], // 加载我们的实体 pool: { // 连接池的一些相关配置 max: 5, min: 0, acquire: 30000, idle: 10000 }, operatorsAliases: false, logging: true // true会在控制台打印每次sequelize操作时对应的SQL命令 })
utils
所有的公共函数,都放在这里。
同时推荐编写对应的索引文件(index.ts),大致的格式如下:
// utils/get-uid.ts export default function (): number { return 123 } // utils/number-comma.ts export default function(): string { return '1,234' } // utils/index.ts export {default as getUid} from './get-uid' export {default as numberComma} from './number-comma'
每添加一个新的util
,就去index
中添加对应的索引,这样带来的好处就是可以通过一行来引入所有想引入的utils
:
import {getUid, numberComma} from './utils'
configs
configs下边存储的就是各种配置信息了,包括一些第三方接口URL、数据库配置、日志路径。
各种balabala的静态数据。
如果配置文件多的话,建议拆分为多个文件,然后按照utils
的方式编写索引文件。
types
这里存放的是所有的自定义的类型定义,一些开源社区没有提供的,但是我们用到的第三方插件,需要在这里进行定义,一般来说常用的都会有,但是一些小众的包可能确实没有TS的支持,例如我们有使用的一个node-qconf
:
// types/node-qconf.d.ts export function getConf(path: string): string | null export function getBatchKeys(path: string): string[] | null export function getBatchConf(path: string): string | null export function getAllHost(path: string): string[] | null export function getHost(path: string): string | null
类型定义的文件规定后缀为 .d.ts
types下边的所有文件可以直接引用,而不用关心相对路径的问题(其他普通的model则需要写相对路径,这是一个很尴尬的问题)。
目前使用TS中的一些问题
当前GitHub仓库中,有2600+的开启状态的issues,筛选bug标签后,依然有900+的存在。
所以很难保证在使用的过程中不会踩坑,但是一个项目拥有这么多活跃的issues,也能从侧面说明这个项目的受欢迎程度。
目前遇到的唯一一个比较尴尬的问题就是:
引用文件路径一定要写全。。
import module from '../../../../f**k-module'
小结
初次尝试TypeScript,深深的喜欢上了这个语言,虽说也会有一些小小的问题,但还是能克服的:)。
使用一门静态强类型编译语言,能够将很多bug都消灭在开发期间。
基于上述描述的一个简单示例:代码仓库
希望大家玩得开心,如有任何TS相关的问题,欢迎来骚扰。NPM loves U.
。
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
探索分布式服务框架Dubbo9:Dubbo整体架构与实现原理
Dubbo剖析-整体架构分析 一、前言 工欲善其事,必先利其器,前面通过几篇文章简单的介绍了如何使用Dubbo搭建一个简单的分布式系统,在接下来的的一段时间就来研究Dubbo原理设计,本文作为原理设计的开篇先整体介绍下dubbo的架构。 二、整体架构 image.png dubbo官方的这个图很复杂,但是一开始没有必要深入细节,下面我们简单讲解主要模块。 其中Service 和 Config 层为 API,对应服务提供方来说是使用ServiceConfig来代表一个要发布的服务配置对象,对应服务消费方来说ReferenceConfig代表了一个要消费的服务的配置对象。可以直接初始化配置类,也可以通过 spring 解析配置生成配置类。 其它各层均为 SPI,SPI意味着下面各层都是组件化可以被替换的,这也是dubbo比较好的一点,主要功能组件都可以插件化替换。dubbo增强了JDK中的SPI功能,在dubbo中其它各层都是使用扩展点进行提供服务的,dubbo增强的SPI增加了对扩展点 IoC 和 AOP 的支持,一个扩展点可以直接 setter 注入其它扩展点;并且不会一次性实例化扩展...
- 下一篇
这些趋势正对关键基础设施的创新研发带来深刻革新
云计算、物联网、5G、智能终端、AI智能、VR虚拟现实…… 这些耳熟能详的名字,为我们构建了庞大的万物互联智能时代的蓝图。 而在这背后,面向未来构建新一代的关键基础设施架构,正成为各个行业用户实现业务持续发展最基本的保障手段。 保障应用系统的永远在线,关键基础设施成为不可或缺的重要应用。 当前,信息应用、数据中心、移动互联、数字化转型、高端制造、智能生产等等带来的巨大需求,使得关键基础设施设备的应用,成为崭新时代的重要角色。 比如,快速发展的数据中心,其效能的发挥就离不开供电、制冷等关键基础设施设备的保障。作为一切信息技术实现安全高效应用的支撑基础,数据中心需要稳固的物理基础架构,为其运行提供高可靠保障。 在工业领域,其特殊的生产制造场景,更需要配备能够适应恶劣环境的工业级基础设施设备,保障工业生产过程安全、持续、可靠运行。 不仅如此,在通信、交通、医疗、政府、教育、互联网、金融等几乎所有行业中,关键基础设施应用也非常关键,行业用户必须为大量IT负载配置供电、制冷等设备,确保各种核心应用系统时时在线,保障业务的持续、稳定开展。 应用变革对产品提出更高要求。 对于绝大多数用户而言,全新的...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Hadoop3单机部署,实现最简伪集群
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- Linux系统CentOS6、CentOS7手动修改IP地址
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- CentOS8安装Docker,最新的服务器搭配容器使用
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- Windows10,CentOS7,CentOS8安装Nodejs环境
- SpringBoot2全家桶,快速入门学习开发网站教程
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库
- CentOS7安装Docker,走上虚拟化容器引擎之路