直接运行TypeScript,Deno能NodeJS也能

3,761 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第2天,点击查看活动详情 >>


本文是azuo和萌妹俩技术创作之旅的第9篇原创文章,内容创作@azuo😄,精神支持@大头萌妹😂


Deno号称要来替代是Node.js。但是真的学不动了。

image.png

Deno 相比 Node.js 新增的功能特性,Node.js作为语言平台是可以通过代码实现也是扩展更多的新功能。本文将手把手带你用 Node.js 如何实现 Deno 直接运行TypeScript 模块能力。

一、了解Deno

Deno 原生支持 TypeScript 语言,可以直接运行,不必显式转码。 它的内部其实会根据文件后缀名判断,如果是.ts后缀名,就先调用 TyepScript 编译器,将其编译成 JavaScript;如果是.js后缀名,就直接传入 V8 引擎运行。

其实Deno也不是直接运行TypeScript代码,而是把TyepScript转成Javascript后再执行。

具体流程如下:

流程.drawio.png

今天文章内容将详细讲解上面流程图绿色部分运行原理和代码实现。

二、转译TS

Node.js本身无法直接运行TypeScript,需要转译成JavaScript后再运行。TypeScript转译成Javascript ,需用到 ts-node(详细使用文档:typestrong.org/ts-node/api…

转译.drawio.png

接下来,将详细讲解 ts-node 将 ts 转译 js。

2.1 转译流程

ts-node转译 TypeScript,流程很简单,一共就两步,具体如下:

  • 第一步: 使用register详细文档点击跳转)模块创建一个 TypeScript 编译器实例【tsCompiler】:

  • 第二步:调用编译器的实例【tsCompiler】将 TypeScript 转译 JavaScript。

const { register } = require('ts-node')
const tsCompiler = register()
const code = tsCompiler.compile('ts代码字符串', '文件名')

2.2 代码实现

第一步:内容准备:

  1. 创建目录gan-deno,并npm初始化;
  2. 然后新建一个typeScript文件./input/ts-modeule.ts,当成内容输入;
  • npm初始化
# 初始化项目
npm init -y
  • 准备一个ts文件,内容如下:
// ts-modeule.ts文件内容

export function add(a: number, b: number): number {
    return a + b
}

export function subtract(a: number, b: number): number {
    return a - b
}

export interface iXxxObj {
    a: number
    b: string
}

export const xxxObj = {
    a: 1,
    b: 2
}

第二步: 安装ts-node及其依赖

# 安装 typescript
npm install --save typescript
# 安装 ts-node
npm install --save ts-node

第三步:使用fs模块读取ts模块内容并执行

const fs = require('fs')
const path = require('path')
const { register } = require('ts-node')

// ts 转 js
function compile(modulePath) {
    const { base } = path.parse(modulePath)
    // 读取ts模块文件内容
    const tsContet = fs.readFileSync(modulePath, { encoding: 'utf8' })
    // 将ts代码转译成js代码
    const code = register().compile(tsContet, base)
    return code
}
// 转译后js代码
const code = compile(path.resolve(__dirname, './input/ts-modeule.ts'))

console.info('js code:', code)

输出结果:

image.png

ts-node 转译后的JS内容是符合 CommonJS 模块规范滴。

三、运行时处理

上面已经可以完成 TypeScript转译成 javascript过程, 接下来,就要执行并返回TS模块。这个过程还需要两步:

  • 第一步:要执行javascript的代码字符串即可。

在运行时运行JavaScript代码的字符串,有eval new Function两种方式。

  • 第二步:模块挂载后导入。

那么如何获取获取执行后的模块,观察TypeScript转译JavaScript内容:

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.xxxObj = exports.subtract = exports.add = void 0;
function add(a, b) {
    return a + b;
}
exports.add = add;
function subtract(a, b) {
    return a - b;
}
exports.subtract = subtract;
exports.xxxObj = {
    a: 1,
    b: 'bbb'
};

转译后的JS,是符合CommonJS模块,导出模块内容都挂在 exports。关键在于 ——— 想办法读取 exports

3.1 eval方式

按照eval执行特性,只需在eval执行前定义一个 exports变量来进行挂载即可,实现代码如下:

const fs = require('fs')
const path = require('path')
const { register } = require('ts-node')

function loadTSMoudle(modulePath) {
    const { base } = path.parse(modulePath)
    // 读取ts模块文件内容
    const tsContet = fs.readFileSync(modulePath, { encoding: 'utf8' })
    // 将ts代码转译成js代码
    const code = register().compile(tsContet, base)
    
    // 定义一个变量exports来接受
    const exports = {}
    
    // 执行转译后js模块代码
    eval(code)
    
    return exports.default || exports
}

// 获取ts 模块
const tsModule = loadTSMoudle(path.resolve(__dirname, './input/ts-modeule.ts'))

// 执行模块
console.info(
    'xxxObj:', tsModule.xxxObj,
    '\nadd:', tsModule.add(2, 4),
    '\nsubtract:', tsModule.subtract(2, 4),
)

输出结果:

image.png

3.2 new Function 方式

遵循CommonJS模块实现原理,写一个简单版的导出函数,内容如下:

const fs = require('fs')
const path = require('path')
const { register } = require('ts-node')

function loadTSMoudle(modulePath) {
    const { base } = path.parse(modulePath)
    // 读取ts模块文件内容
    const tsContet = fs.readFileSync(modulePath, { encoding: 'utf8' })
    
    // 将ts代码转译成js代码
    const code = register().compile(tsContet, base)
    
    // 构造一个极简版 CommonJS的导出函数
    const loadFunc = new Function(
      'exports', 'require', 'module', '__filename', '__dirname',
      code
    )
    const exports = {}
    const module = { exports }
    loadFunc(exports, require, module, __filename, __dirname)
    return exports.default || exports
}

// 获取ts 模块
const tsModule = loadTSMoudle(path.resolve(__dirname, './input/ts-modeule.ts'))

// 执行模块
console.info(
    'xxxObj:', tsModule.xxxObj,
    '\nadd:', tsModule.add(2, 4),
    '\nsubtract:', tsModule.subtract(2, 4),
)

执行结果如下:

image.png

四、扩展 require

扩展 require方法,来直接加载和运行 ts模块。给予ts模块和JS模块一样的排面!

重写 Module._extensions['.ts'] 方法,在里面读取文件内容,然后调用 ts-node 来把 ts 转译成 js,之后调用 Module._compile 来处理编译后的 js模块。

代码实现如下:

const fs = require('fs')
const path = require('path')
const { register } = require('ts-node')

// 注册一个ts处理方式
require.extensions['.ts'] = function (module, filename) {
    // 获取包路径
    const modulePath = require.resolve(filename)
    
    const { base } = path.parse(modulePath)
    // 读取ts模块文件内容
    const tsContet = fs.readFileSync(modulePath, { encoding: 'utf8' })
    // 将ts代码转译成js代码
    const code = register().compile(tsContet, base)
    module._compile(code, base);
}

// 直接用require获取ts 模块
const tsModule = require(path.resolve(__dirname, './input/ts-modeule.ts'))

// 执行模块
console.info(
    'xxxObj:', tsModule.xxxObj,
    '\nadd:', tsModule.add(2, 4),
    '\nsubtract:', tsModule.subtract(2, 4),
)

输出结果:

image.png

五、总结

Node.js 是一个语言平台,除了语言除本身语法和功能特性外,更重要的是围绕其自身平台建立起来的生态。截至2020年3月17日,npm为大约1200万开发人员提供了130万个软件包,这些开发人员每月下载这些软件包达750亿次。如此繁荣的生态,已经为这个语言平台打下了坚实的基础。 Deno之于 Node.js 新特性和功能,在笔者看来,有不少只具有“次要特征”,完成可以在 Node.js这个平台上通过代码实现来扩展。

本文就演示如何支持加载一个TypeScript模块。有兴趣的同学,可以参考本文的示例,来尝试使用 babel 来扩展扩展JSX模块。