Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >如何将Node.js库转换到Deno

如何将Node.js库转换到Deno

作者头像
ACK
发布于 2022-05-29 02:54:08
发布于 2022-05-29 02:54:08
2.4K00
代码可运行
举报
运行总次数:0
代码可运行

由于Node和Deno的一些差异,一个库要想同时支持Node和Deno是需要一些改造的

本文翻译自EdgeDb博客:https://www.edgedb.com/blog/how-we-converted-our-node-js-library-to-deno-using-deno 如果需要将Deno项目直接迁移为Node项目可参考笔者另一篇文章deno 初体验,实战记录一个node项目迁移到deno需要做什么

Deno是一个新的JavaScript运行时,无需编译即原生支持TypeScript。它是由Node.js作者Ryan Dahl创建的,为了解决Node的一些基本设计、安全漏洞问题并集成了当前的一些开发实践如ES Module和TypeScript

在EdgeDb中,我们建立和维护了一个官方的npm上的Node.js客户端。然而,Deno使用了一套完全不同的实践来处理依赖,即直接从公共包库(如deno.land/x)import路径。我们将寻找一种简单的方法来Deno化我们的代码库。也就是用最简单的重构从现有的Node.js实现中生成一个Deno兼容的模块。这解决维护和同步两个几乎相同的代码库的重复工作带来的问题

我们采用了一种“运行时适配器”模式。这是一种通用的解决方法对其他希望支持Deno库的作者也会有用

Node.js vs Deno

Node.js和Deno有一些重要的区别

  1. TypeScript支持: Deno可以直接执行TypeScript而Node.js只能运行JavaScript代码
  2. 模块解析: 默认情况下,Node.js使用CommonJS导入模块并使用require/module.exports语法。它也有一个复杂的解析算法,会从node_modules加载像react这样的普通模块名,并在无额外扩展名导入时尝试添加.js.json。如果导入路径是一个目录,则导入index.js文件

Deno模块解析逻辑简化了很多。它使用了ECMAScript模块语法进行导入和导出。该语法也被TypeScript使用。所有导入必须是有显式文件扩展名的相对路径或者是一个URL

这意味着不存在像npm或yarn那样有node_module包管理器。外部模块可以通过URL直接从公开代码库导入,比如deno.land/xGitHub

  1. 标准库: Node.js有一些内置的标准模块fscryptohttp。这些包名由Node.js保留。相比之下Deno标准库是通过https://deno.land/std/URL导入的。Node和Deno标准库的功能也不同,Deno放弃了一些旧的或过时的Node.js api,引入了一个新的标准库(受Go的启发),并统一支持现代JavaScript特性如Promise(而许多Node.js api仍然使用老的回调风格)
  2. 内置全局变量: Deno所有的核心api都在全局变量Deno中,其它全局变量则只有标准的web api。和Node.js不同的是,Deno没有Bufferprocess这些全局变量

所以需要如何做才能让我们的Node.js库尽可能容易地在Deno中运行呢?下面将一步一步进行改造

TypeScript和模块语法

幸运的是,我们无需考虑将CommonJS的require/module.exports语法转换到到ESMimport/export。我们使用用TypeScript编写edgedb-js,它已经使用了ESM语法。在编译过程中,tsc将我们的文件转换成普通的=CommonJS语法的JavaScript文件。Node.js可以直接运行编译后的文件

本文下面将讨论如何将TypeScript源文件修改为Deno可以直接使用的格式

依赖

edgedb-js没有任何第三方依赖,所以这里不必担心任何三方库的Deno兼容性问题。但仍需要将所有从Node.js标准库中导入(例如pathfs等)替换为等价的Deno文件

注意:如果你的包确实依赖于外部包,可在deno.land/x中查看是否有Deno版本

由于Deno标准库提供了Node.js兼容模块,这个改造比较简单。Deno的标准库上提供了一个包装器并尽可能和Node的api保持一致

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
- import * as crypto from "crypto";
+ import * as crypto from "https://deno.land/std@0.114.0/node/crypto.ts";

为了简化问题,将所有Node.js api导入移到一个名为adapter.node.ts的文件中,并只重新导出我们需要的功能

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// adapter.node.ts
import * as path from "path";
import * as util from "util";
import * as crypto from "crypto";

export {path, net, crypto};

然后在一个名为adapter.deno.ts的文件中为Deno实现相同的适配器

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// adapter.deno.ts
import * as crypto from "https://deno.land/std@0.114.0/node/crypto.ts";
import path from "https://deno.land/std@0.114.0/node/path.ts";
import util from "https://deno.land/std@0.114.0/node/util.ts";

export {path, util, crypto};

当需要使用Node.js的特定功能时,直接从adapter.node.ts导入这些功能。通过这种方式,可以通过简单地将所有adapter.node.ts导入重写为adapter.deno.ts即可使edgedb-js兼容Deno。只要确保这些文件重新导出相同的功能就能符合预期

但实际上应该如何重写这些导入呢。这里我们需要开发一个简单的codemod脚本。下面将使用Deno来开发这个脚本

开发Deno-ifier

在开发之前,列举下需要做的事情:

  • 将Node.js风格的导入重写为更显式的Deno风格。包括添加.ts扩展名和目录导入添加/index.ts
  • adapter.node.ts的导入替换成从adapter.deno.ts的导入
  • 注入Node.js全局变量(如processBuffer)到Deno的代码中。虽然可以简单地从适配器导出这些变量,但我们必须重构Node.js文件以显式地导入它们。为了简化处理,将检测代码中使用了Node.js全局变量的时候注入一个导入
  • src目录重命名为_src,表示它只被edgedb-js内部使用不应该被外部直接导入使用
  • 将主入口文件src/index.ts移动到项目根目录并重命名为mod.ts。这是Deno中的习惯用法(这里index.node.ts的命名并不表明它是只能给Node.js使用而是用来区别于index.browser.tsindex.browser.ts导出的是edgedb-js中浏览器兼容的部分代码)
获取所有文件列表

第一步先获取出源文件。Deno原生fs模块提供了walk函数可以实现:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import {walk} from "https://deno.land/std@0.114.0/fs/mod.ts";

const sourceDir = "./src";
for await (const entry of walk(sourceDir, {includeDirs: false})) {
  // 遍历源文件
}

注意:这里使用的是Deno原生的std/fs模块而不是Node兼容版本的std/node/fs

声明一个重写规则集合并初始化一个Map对象表示源文件路径到需要重写的目标文件的路径

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const sourceDir = "./src";
const destDir = "./edgedb-deno";
const pathRewriteRules = [
  {match: /^src\/index.node.ts$/, replace: "mod.ts"},
  {match: /^src\//, replace: "_src/"},
];

const sourceFilePathMap = new Map<string, string>();

for await (const entry of walk(sourceDir, {includeDirs: false})) {
  const sourcePath = entry.path;
  sourceFilePathMap.set(sourcePath, resolveDestPath(sourcePath));
}

function resolveDestPath(sourcePath: string) {
  let destPath = sourcePath;
  // 使用重写规则
  for (const rule of pathRewriteRules) {
    destPath = destPath.replace(rule.match, rule.replace);
  }
  return join(destDir, destPath);
}

这非常简单,下面开始开发修改源代码部分

重写import和export

要重写导入路径需要知道它们在文件中的位置。我们将使用TypeScript的Compiler API来将源文件解析为抽象语法树并找到导入语句

为了实现这个功能我们需要用到typescript NPM包的compile API。Deno的兼容模块提供了一个直接从CommonJS模块导入的方式。需要在执行Deno代码的时候使用--unstable标识,对于构建阶段这不是什么问题

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import {createRequire} from "https://deno.land/std@0.114.0/node/module.ts";

const require = createRequire(import.meta.url);
const ts = require("typescript");

下面遍历文件并依次解析

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import {walk, ensureDir} from "https://deno.land/std@0.114.0/fs/mod.ts";
import {createRequire} from "https://deno.land/std@0.114.0/node/module.ts";

const require = createRequire(import.meta.url);
const ts = require("typescript");

for (const [sourcePath, destPath] of sourceFilePathMap) {
  compileFileForDeno(sourcePath, destPath);
}

async function compileFileForDeno(sourcePath: string, destPath: string) {
  const file = await Deno.readTextFile(sourcePath);
  await ensureDir(dirname(destPath));

  // 如果文件以'.deno.ts'结尾则直接复制无需修改
  if (destPath.endsWith(".deno.ts")) return Deno.writeTextFile(destPath, file);
  // 如果文件以`.node.ts`结尾则跳过
  if (destPath.endsWith(".node.ts")) return;

  // 使用typescript Compiler API解析源文件
  const parsedSource = ts.createSourceFile(
    basename(sourcePath),
    file,
    ts.ScriptTarget.Latest,
    false,
    ts.ScriptKind.TS
  );
}

对于每个AST,我们通过遍历顶层节点找出importexport语句。这里无需深层查找,因为import/export只会出现在顶级作用域(也无需处理动态import(),因为edgedb-js中也没有使用)

从这些节点中,获取源文件中export/import路径的开始和结束偏移量。然后可以通过切片取代路径内容并插入修改后的路径来重写导入

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const parsedSource = ts.createSourceFile(/*...*/);

const rewrittenFile: string[] = [];
let cursor = 0;
parsedSource.forEachChild((node: any) => {
  if (
    (node.kind === ts.SyntaxKind.ImportDeclaration ||
      node.kind === ts.SyntaxKind.ExportDeclaration) &&
    node.moduleSpecifier
  ) {
    const pos = node.moduleSpecifier.pos + 2;
    const end = node.moduleSpecifier.end - 1;
    const importPath = file.slice(pos, end);

    rewrittenFile.push(file.slice(cursor, pos));
    cursor = end;

    // 使用Deno版本的导入替换导入适配文件
    let resolvedImportPath = resolveImportPath(importPath, sourcePath);
    if (resolvedImportPath.endsWith("/adapter.node.ts")) {
      resolvedImportPath = resolvedImportPath.replace(
        "/adapter.node.ts",
        "/adapter.deno.ts"
      );
    }

    rewrittenFile.push(resolvedImportPath);
  }
});

rewrittenFile.push(file.slice(cursor));

这里的关键部分是resolveImportPath函数。它通过试错查找的方式实现将Node.js风格的引入转化为Deno风格的导入。首先检查路径是否对应于实际文件;如果失败了会尝试添加.ts;如果再失败则尝试添加/index.ts;如果再失败则抛出一个错误。

注入Node.js全局变量

最后一步是处理Node.js全局变量。首先在创建一个global.deno.ts文件。这个文件应该导出包中使用的所有Node.js全局变量的Deno兼容版本

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// global.deno.ts
export {Buffer} from "https://deno.land/std@0.114.0/node/buffer.ts";
export {process} from "https://deno.land/std@0.114.0/node/process.ts";

通过编译后的AST可以拿到源文件中所有全局变量的集合。将使用它在任何引用这些全局变量的文件中注入import语句

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const sourceDir = "./src";
const destDir = "./edgedb-deno";
const pathRewriteRules = [
  {match: /^src\/index.node.ts$/, replace: "mod.ts"},
  {match: /^src\//, replace: "_src/"},
];
const injectImports = {
  imports: ["Buffer", "process"],
  from: "src/globals.deno.ts",
};

// ...

const rewrittenFile: string[] = [];
let cursor = 0;
let isFirstNode = true;
parsedSource.forEachChild((node: any) => {
  if (isFirstNode) {  // 每个文件只执行一次
    isFirstNode = false;

    const neededImports = injectImports.imports.filter((importName) =>
      parsedSource.identifiers?.has(importName)
    );

    if (neededImports.length) {
      const imports = neededImports.join(", ");
      const importPath = resolveImportPath(
        relative(dirname(sourcePath), injectImports.from),
        sourcePath
      );
      const importDecl = `import {${imports}} from "${importPath}";\n\n`;

      const injectPos = node.getLeadingTriviaWidth?.(parsedSource) ?? node.pos;
      rewrittenFile.push(file.slice(cursor, injectPos));
      rewrittenFile.push(importDecl);
      cursor = injectPos;
    }
  }
写入文件

删除老文件的内容并依次写入每个文件

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
try {
  await Deno.remove(destDir, {recursive: true});
} catch {}

const sourceFilePathMap = new Map<string, string>();
for (const [sourcePath, destPath] of sourceFilePathMap) {
  // 重写文件
  await Deno.writeTextFile(destPath, rewrittenFile.join(""));
}
持续集成

一个常见的做法是为包的Deno版本维护一个单独的自动生成的仓库。在我们的例子中,每当一个新的提交合并到master时,将在GitHub Actions中生成edgedb-js的Deno版本。然后生成的文件被发布到edgedb-deno仓库。下面是工作流的简化版本

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
# .github/workflows/deno-release.yml
name: Deno Release
on:
  push:
    branches:
      - master
jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout edgedb-js
        uses: actions/checkout@v2
      - name: Checkout edgedb-deno
        uses: actions/checkout@v2
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          repository: edgedb/edgedb-deno
          path: edgedb-deno
      - uses: actions/setup-node@v2
      - uses: denoland/setup-deno@v1
      - name: Install deps
        run: yarn install
      - name: Get version from package.json
        id: package-version
        uses: martinbeentjes/npm-get-version-action@v1.1.0
      - name: Write version to file
        run: echo "${{ steps.package-version.outputs.current-version}}" > edgedb-deno/version.txt
      - name: Compile for Deno
        run: deno run --unstable --allow-env --allow-read --allow-write tools/compileForDeno.ts
      - name: Push to edgedb-deno
        run: cd edgedb-deno && git add . -f && git commit -m "Build from $GITHUB_SHA" && git push

edgedb-deno内部的另一个工作流则会创建一个GitHub release并发布到deno.land/x。可参考

封装

这就是将现存Node.js模块转换到Deno的通常方法。具体可参考Deno编译脚本workflow

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-05-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
(译)为容器提供更好的隔离:沙箱容器技术概览
既然主流 IT 工业都在采用基于容器的基础设施(云原生方案),那么了解这一技术的短板就很重要了。Docker、LXC 以及 RKT 等传统容器都是共享主机操作系统核心的,因此不能称之为真正的沙箱。这些技术的资源利用率很高,但是受攻击面积和潜在的攻击影响都很大,在多租户的云环境中,不同客户的容器会被同样的进行编排,这种威胁就尤其明显。主机操作系统在为每个容器创建虚拟的用户空间时,不同容器之间的隔离是很薄弱的,这是造成上述问题的根本原因。基于这样的现状,真正的沙箱式容器,成为很多研发工作的焦点。多数方案都对容器之间的边界进行了重新架构,以增强隔离。本文覆盖了四个项目,分别来自于 IBM、Google、Amazon 以及 OpenStack,几个方案的目标是一致的:为容器提供更强的隔离。IBM Nabla 在 Unikernel 的基础上构建容器;Google 的 gVisor 为运行的容器创建一个特定的内核;Amazon 的 Firecracker 是一个超轻量级的沙箱应用管理程序;OpenStack 将容器置入特定的为容器编排平台优化的虚拟机之中。下面对几个方案的概述,有助于读者应对即将到来的转型机会。
崔秀龙
2019/07/24
3.1K0
(译)为容器提供更好的隔离:沙箱容器技术概览
Kata Containers及相关vmm介绍「建议收藏」
Kata Containers 是轻量级虚拟机的一种新颖实现,可无缝集成到容器生态系统中。 Kata Containers 与容器一样轻巧快速,并与容器管理层集成,同时还提供 VM 的安全优势。
全栈程序员站长
2022/09/30
2.5K0
Kata Containers及相关vmm介绍「建议收藏」
干货|浅析 k8s 容器运行时演进
在docker/k8s时代,经常听到CRI, OCI,containerd和各种shim等名词,看完本篇博文,您会有个彻底的理解。
CloudBest
2021/03/12
1.2K0
干货|浅析 k8s 容器运行时演进
容器安全和安全运行时的重要性
容器彻底改变了我们开发和部署应用程序的方式,提供了封装应用程序及其依赖项的轻量级和可移植环境。但我们如何保证它们的安全呢?
我是阳明
2023/10/27
6190
容器安全和安全运行时的重要性
K8S Runtime 种类多,使用复杂?那是你没明白其中的门道
近年来,Runtime(容器运行时)发展迅速,种类也日渐丰富:Docker、rkt、containerd、cri-o、Kata、gVisor……面对这么多的选择,如果你正打算部署一个容器系统或 Kubernetes 集群,你会如何选择呢?在这篇文章中,来自 PingCAP 的工程师吴叶磊将从典型的 Runtime 架构、OCI、CRI 与被滥用的名词“Runtime”等方向,生动阐述什么是 Runtime 以及它们的关系和特点。
马哥linux运维
2019/05/15
2.9K0
K8S Runtime 种类多,使用复杂?那是你没明白其中的门道
一文搞懂 Container
设想一下,在我们的日常项目开发过程中,存在一个应用服务,其使用一些基础库函数并具有某些依赖项。如果我们在不支持这些依赖项的环境平台上运行此应用程序,那么,我们可能会遇到意外错误。随着 DevOps 及云原生理念的注入,我们希望我们所开发的应用程序能够可以跨多个操作系统及平台正常运行。
Luga Lee
2021/12/03
2.1K1
一文搞懂 Container
大话 Kubernetes Runtime
回想最开始接触 k8s 的时候, 经常搞不懂 CRI 和 OCI 的联系和区别, 也不知道为啥要垫那么多的 “shim”(尤其是 containerd-shim 和 dockershim 这两个完全没啥关联的东西还恰好都叫 shim). 所以嘛, 这篇就写一写 k8s 的 runtime 部分, 争取一篇文章把下面这张 Landscape 里的核心项目给白话明白:
iMike
2019/06/04
1.3K0
大话 Kubernetes Runtime
容器安全与安全运行环境的重要性
了解容器运行环境的工作机制,缘何若攻击者突破容器的限制,过度耦合的运行环境可能造成主机被接管,以及gVisor和Kata Containers等安全容器运行环境的好处。
云云众生s
2024/03/28
2110
关于容器和容器运行时的那些事
容器,容器编排,微服务,云原生,这些无疑都是当下软件开发领域里面最热门的术语。容器技术的出现并迅速的广泛应用于软件开发的各个领域里,主要的原因是容器技术革命性的改变了软件开发和部署的基本方式。作为一个架构师,了解容器技术是非常重要的一个话题,我们今天就来聊聊它。
yuanyi928
2020/08/12
1.7K0
关于容器和容器运行时的那些事
运维锅总详解容器OCI规范
OCI是什么?OCI的镜像规范和运行时规范有哪些具体内容?Docker实现了OCI规范了吗?实现OCI规范的开源项目有哪些?OCI诞生背景及历史演进又有哪些内容?希望读完本文,能帮您解答这些疑惑!
锅总
2024/07/20
2570
运维锅总详解容器OCI规范
真正运行容器的工具:深入了解 runc 和 OCI 规范
我们谈谈位于 Docker、Podman、CRI-O 和 Containerd 核心的工具:runc。
没有故事的陈师傅
2021/11/02
3.7K0
真正运行容器的工具:深入了解 runc 和 OCI 规范
基于Rust-vmm实现Kubernetes运行时
周亮宇,腾讯云容器技术专家,负责腾讯云容器服务及EKS弹性容器服务,在云计算领域有着丰富的经验。
腾讯云原生
2020/08/18
3.2K1
Containerd深入浅出-安全容器篇
Containerd 是一个高度模块化的高级运行时,所有模块均可插拔,模块均以 RPC service 形式注册并调用(gRPC 或者 TTRPC)。不同插件通过声明互相依赖,由 Containerd 核心实现统一加载,使用方可以自行实现插件以实现定制化的功能。当然这种设计虽然使得 Containerd 拥有强大的跨平台、可插拔的能力,同时也带来一些缺点,模块之间功能互调必须通过 RPC 调用。
zouyee
2022/11/07
1.1K0
Containerd深入浅出-安全容器篇
Docker、Containerd、RunC分别是什么
上一篇文章《真正运行容器的工具:深入了解 runc 和 OCI 规范》已经讲清楚了Runc与OCI。这里再讲解一下概念。
没有故事的陈师傅
2021/11/02
4.3K0
Docker、Containerd、RunC分别是什么
云原生之容器安全实践
随着越来越多的企业开始上“云”,开始容器化,云安全问题已经成为企业防护的重中之重。
美团技术团队
2020/03/25
1.5K0
Docker 架构解析:理解 Docker 引擎和容器运行时
本篇博客《Docker 架构解析:理解 Docker 引擎和容器运行时》深入探讨了 Docker 技术的核心概念和关键组件。在引言部分,简要介绍了 Docker 的重要性和普及程度,并提出了本文的目的:帮助读者深入理解 Docker 架构、引擎和容器运行时。
猫头虎
2024/04/08
8540
Docker 架构解析:理解 Docker 引擎和容器运行时
容器技术创新漫谈
Kubernetes在2017年赢得了容器编排之战,使得基于容器+Kubernetes来构建PaaS平台成为了云计算的主流方式。在人们把关注的目光都聚焦在Kubernetes上时,容器技术领域在2018年也发生了很多创新,包括amazon最近开源的轻量级虚拟机管理器 Firecracker,Google在今年5月份开源的基于用户态操作系统内核的 gVisor 容器,还有更早开源的虚拟化容器项目 KataContainers,可谓百花齐放。一般的开发者可能认为容器就等于Docker,没想到容器领域还在发生着这么多创新。我在了解这些项目时,发现如果没有一些背景知识,很难get到它们的创新点。我试着通过这篇文章进行一次背景知识的梳理。让我们先从最基本的问题开始:操作系统是怎么工作的?
mazhen
2023/11/24
3830
容器技术创新漫谈
Containerd深度剖析-runtime篇
虽然容器领域的创业随着CoreOS、Docker的卖身,而逐渐归于平寂,但随着Rust语言的兴起,Firecracker、youki项目在容器领域泛起涟漪,对于云原生从业者来说,面试等场景中或多或少都会谈论到容器一些的历史与技术背景。
zouyee
2022/11/07
1.4K0
Containerd深度剖析-runtime篇
AWS的“炮仗”与Serverless
Serverless Computing,即”无服务器计算”,这一概念在刚刚提出的时候并没有获得太多的关注,直到2014年AWS Lambda这一里程碑式的产品出现。通过将无服务器计算的概念嵌入到整个云计算服务的整体产品框架中,无服务器计算正式走进了云计算的舞台。2017年,AWS发布了Fargate产品以充实自己的无服务器计算产品线。
Linux阅码场
2019/07/12
1.5K0
AWS的“炮仗”与Serverless
容器运行时
要把进程运行在容器中,还需要有便捷的SDK或命令来调用Linux的系统功能,从而创建出容器。容器的运行时(runtime)就是运行和管理容器进程、镜像的工具。
CNCF
2022/11/28
1.6K0
容器运行时
推荐阅读
相关推荐
(译)为容器提供更好的隔离:沙箱容器技术概览
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验