让TypeScript代码和类型的分享变得快速而简单

587 阅读19分钟

在我以前的生活中,我用C++和后来的C#写作,并使用这两种语言来分享代码,将其编译成 "库文件",然后我和我的团队可以将其复制到其他项目。像这样共享代码库是很重要的,这样我们就不会为新的项目反复重写或重复普通代码。

在TypeScript中这样做似乎也应该很简单。我们只需编译成JavaScript(我们可以把它看作是我们新版本的 "库文件"),然后把JavaScript代码复制到其他项目。

不过,在实践中,这并不那么简单。根据定义,复杂的应用程序--像我们这些天在TypeScript中构建的现代应用程序--是由多个相互作用的部分组成的。

你是否曾经在TypeScript中编码时看到过这样的错误信息?

File 'X’ is not under 'rootDir' 'Y'. 'rootDir' is expected to contain all source files. ts(6059)

这意味着你正试图从当前项目之外导入代码。最有可能的是,你只是试图将代码从一个项目分享到另一个项目!但是,我们如何真正分享代码?

但是,我们实际上如何在项目之间共享代码库呢?

一种方法是创建独立的代码库,按功能划分我们的应用程序,使我们能够在项目之间重复使用我们最常用和有用的代码。请看图1中的表述。

Figure 1: Sharing code libraries between projects

图1:项目间共享代码库。文件夹图片来自OpenClipArt

事实上,在TypeScript v3.0之前,一直没有优雅的方式来做到这一点--但我们很快就会提到这一点。

这篇博文展示了我寻找TypeScript中共享代码库的类似轻量级方式的最新成果。在这篇文章中,我们会先解决这个问题,然后再讨论TypeScript代码共享的更高级方法,比如在微服务和Docker镜像之间共享,或者在后台和前台之间共享。

我们可以从中获得很多价值,即使是在一个单一的应用程序中。当应用程序的结构的性质是被分离成组件(如微服务、后端/前端、微前端等)时,我们可以很容易地看到在单个应用程序中共享公共函数和数据结构的需要。

如果你想在不同的应用组件中实现代码的干涸,这是你必须知道的。

审查标准方法

在我们处理示例代码之前,让我们简单回顾一下JavaScript社区中共享代码的标准方法。

最常见的方法是将我们的代码发布到npm注册表上,这样它就可以作为一个依赖项被安装到我们想使用它的任何其他项目中。

另一种方法是将我们的代码发布到Git仓库(不一定是GitHub,但通常是),然后用npm直接从那里安装它

请记住,无论是发布到npm注册表还是发布到GitHub,这两种方法都可以公开或私下进行。当然,公开发布是默认的,但当你在一个非开源的代码库上工作时,也可以私下发布。

这些最常见的JavaScript发布方式是相关的,因为它们也是发布TypeScript代码最常见的方式。(如果你需要快速复习一下,可以看一下这个发布指南)。

我们通常很少会直接发布TypeScript代码,而不是将其编译为JavaScript并发布。不过对于很多项目来说,发布一个共享代码库似乎是过犹不及。

首先,这对快速开发来说是一个巨大的障碍;其次,发布一个我只用于少数微服务的库,或者当我想在REST API的两边重复使用一个数据模型时,这似乎是一种浪费。真的,除非你想与世界分享你的数据模型,否则通常不值得发布。

标准方法的局限性

在常规的编码过程中,我在整个代码库中进行频繁的、反复的修改。(当你确实需要为npm包进行这种工作时,请确保你使用npm-link)。其中一些修改会被证明是不必要的或不好的(例如,它们会破坏一些东西),但我们需要自由来实验我们的代码,然后在它不工作的时候退出。我们可以实验和测试我们的工作,而不需要先提交和发布,这是根本。

在npm上发布我的代码库之前,我通常会问自己以下几个问题。

  • 这个库是为在其他项目中独立使用而设计的吗?
  • 我想和其他人分享它吗?
  • 这个库的代码是否或是否应该开放源代码?

不过,在其他一些情况下,发布到npm上可能会拖累高效的开发--这时我们就需要找到其他分享代码的方法。

探索共享TypeScript代码的其他方法

因此,在上面,我们为开发共享代码的替代方法建立了以下准则。

  • 在复杂的项目中,我们希望利用共享代码库,这样我们就可以在应用程序的各个组件中重复使用代码了
  • 将代码发布到Git仓库或npm上可能是不必要的,甚至会阻碍我们工作流程的速度,这要视情况而定

考虑到这一点,让我们进入重头戏:看看在我们的项目之间共享TypeScript代码库的其他手段。

开始吧

在这篇博文中,我们将通过几个例子的代码进行讲解。

第一个例子是一个基本但必要的起点。你需要安装Node.js来运行它。

第二个例子更高级,告诉你如何将一个代码库共享到一个基于Docker的微服务中。你需要安装Docker来运行它。

第三个也是最后一个例子展示了如何在一个基于Docker的微服务和一个用React、TypeScript和Parcel构建的前端之间共享代码。你需要同时安装Docker和Node.js来完全构建和运行这个例子,但如果你想只构建前端,你只需要Node.js。

代码可以在我的GitHub上找到,如果你想跟随,你可以自己克隆代码。

git clone git@github.com:ashleydavis/sharing-typescript-code-libraries.git

或者下载压缩文件并解压到你的本地电脑。

我在下面介绍的方法允许一个灵活的项目结构。你可以使用这些技术来处理单发布(mono-repo),或者在使用元工具时使用元发布(meta-repo)。如果出于某种奇怪的原因,你不使用版本控制,你就不必使用任何repo--相反,你可以在你的本地计算机上使用这些技术和一个临时的目录结构。

这些例子确实遵循一个特定的布局,但这其实并不重要。你可以根据自己的喜好和需要来安排文件和文件夹。

最后,这里介绍的方法也是可扩展的。你可以在每个例子中添加更多的共享代码库,你也可以添加更多的主项目(例如,每个微服务都有一个项目)。

示例1:TypeScript项目参考的入门知识

如果你已经了解如何使用TypeScript项目引用,请随意跳过本节

TypeScript项目引用实际上是非常新的!它们从TypeScript开始就可以使用。它们从2018年发布的TypeScript v3.0开始才有。根据文档,项目引用允许你将TypeScript程序结构化为更小的片段,这有助于改善构建时间,强制组件之间的逻辑分离,并以新的和更好的方式组织你的代码。

让我们从最基本的例子开始:一个Node.js "Hello, World!"程序,它显示了如何将一个共享的TypeScript代码库包含到一个Node.js项目中。这是相当简单的,但它是更高级的例子的必要构件。

这个例子只使用了一个共享库,但你可以通过在你的tsconfig.json 文件中添加references 。每个被引用的项目必须是一个有效的TypeScript项目,并且有自己的tsconfig.json 文件。

下面的图2解释了这个例子项目的结构。如果你克隆了这个项目,可以在GitHub或你的本地电脑上自己探索

Figure 2: Sharing TypeScript libraries, a simple Node.js example

图2:在Node.js项目中使用共享TypeScript库

共享库的代码如下清单1所示。它将 "Hello, World!"打印到控制台。

清单1:共享库的代码

export function showMessage(): void {
console.log("Hello world!\n");
}

主项目的代码从共享库中导入showMessage 函数并调用它,如下清单2所示。

清单2:主项目使用共享库中的代码

import { showMessage } from "../../libs/my-library";
showMessage();

就像我前面说的,这个例子简单得不能再简单了。

有趣的部分是如何在主项目的tsconfig.json 文件中配置项目引用。下面的清单3显示了一个摘录。

清单3:摘录自主项目的文件。tsconfig.json

"references": [
{
"path": "../libs/my-library"
}
]

在清单3中,我们定义了一个指向其他TypeScript项目的references 数组。这将我们的主项目与它的共享库连接起来。

那么,我们如何构建Node.js示例项目?

首先,打开一个终端窗口,导航到主 repo 的目录,然后进入 Node.js 示例。

cd sharing-typescript-code-libraries/nodejs-example

现在,向下导航到主项目。

cd my-project

我们现在可以构建项目了。

对于一个普通的TypeScript项目,我们会调用TypeScript编译器,像这样。

npx tsc

但是,现在我们正在使用TypeScript项目的引用,我们必须在最后添加--build 参数。

npx tsc --build

--build 参数使TypeScript编译器从tsconfig.json 读取references 字段。然后,它在构建主项目之前构建每个引用的项目。

这就是了。在最基本的层面上,没有更多的东西了。

这是一个巨大的节省时间的方法!这意味着我们不需要再做任何事情。这意味着我们不必为每个共享的代码库分别调用npx tsc ,重要的是,这意味着我们在改变代码库的代码后永远不会忘记构建它--这将节省我们大量的时间,让我们知道为什么我们的代码改变没有传到主项目中。

作为我个人惯例的一部分,我在一个名为build 的npm脚本中包装了这一点。访问该 [package.json](https://github.com/ashleydavis/sharing-typescript-code-libraries/blob/main/nodejs-example/my-project/package.json)文件,如果你想看看它是什么样子的。这样做意味着我可以为任何项目调用npm run build ,而对于具有共享库的TypeScript项目,它将自动翻译为npx tsc --build 。我觉得这是一个很好的提示,因为它意味着我不必每次都记得添加--build

这种共享方法的好处是,它具有高度的可扩展性。我们可以将其扩展到更多的共享代码库,我们需要做的只是编译主项目。

例2:在微服务和Docker镜像之间共享代码库

让我们考虑一个更高级的例子。想象一下,我们正在创建一个微服务应用,每个微服务都是从一个Docker镜像中部署的。我们有编译好的代码,我们想在微服务之间共享,我们需要把它烘烤到每个Docker镜像中。

虽然这将是更高级的,但这里的示例代码与之前的例子基本相同,只是这次我们将在Docker构建过程中编译我们的主项目和共享库。然后,我们将捆绑并复制编译后的代码到我们的生产Docker镜像中。

图3解释了这个项目的结构。

Figure 3: Sharing TypeScript libraries, the microservices example

图3:微服务之间共享代码

这个例子项目的重要部分是我们用来构建Docker镜像的Docker文件

图4是Dockerfile的注释版本,强调了最重要的部分。

Figure 4: Annotated Docker file that can share TypeScript code libraries

图4:有注释的Dockerfile,可以共享TypeScript代码库

注意我们是如何在这里复制整个根项目的。这就把我们的共享库和主项目的代码复制到了Docker镜像的构建阶段。

接下来,我们调用npm run build ,将TypeScript编译成JavaScript,并将结果捆绑起来。然后,我们使用递归安装工具来安装共享库和主项目的生产专用依赖项。

最终,为了生成一个生产用的Docker镜像,我们必须将编译后的代码包从构建阶段复制到最终的镜像中。最终的镜像应该省略TypeScript源代码和开发依赖项,这些在生产中根本没有必要。只有编译过的JavaScript代码和生产依赖被复制过去。

因此,我们可以说,生产镜像是精简的,没有被开发、调试和测试的不必要的碎片所膨胀。

还有一个问题。我们究竟如何捆绑编译的JavaScript代码?

你没有错过这个问题。它实际上是隐藏在npm run build 。你可以在清单4中看到它是如何工作的,它是从主项目的package.json 文件中提取的。

清单4:从package.json中摘录

"scripts": {
    "build": "tsc --build && ts-project-bundle --out=build",
  },

我们的问题是,编译后的JavaScript代码被嵌入到每个独立的TypeScript项目中。我们必须将编译后的代码分开,留下我们在生产中不需要的原始TypeScript源代码。

我们有几个解决方案的选择。我们可以通过在Docker文件中添加一堆复制命令,轻松地将代码复制到我们的生产Docker镜像中,但这样我们就需要为任何新添加的库添加新的复制命令,这不是很有可扩展性。另外,如果我们能有一个东西来代替我们现在所拥有的和未来可能添加的东西,那就更好了。

这就是ts-project-bundle 的作用,你可以在上面的清单4中看到。这是我创建的一个简单的命令行工具(当然,通过npm共享,因为我希望大家都能使用它)。

它首先读取主项目的tsconfig.json 文件,然后读取每个被引用项目的文件。然后,它将编译后的代码复制到你选择的输出目录中。这就是编译后的JavaScript代码被放置在build 目录中,并准备被复制到最终的生产Docker镜像中。

令人难以置信的是,这种代码提取和捆绑并没有包含在TypeScript编译器中--我希望他们在未来加入这个功能,让ts-project-bundle成为多余。

现在,我们要构建Docker镜像。

导航到主项目的目录,然后像这样调用Docker build命令。

docker build .. -f ./Dockerfile -t hello-world

注意我们是如何使用父目录作为构建环境的。这是因为Docker构建阶段需要访问父目录,其中包括主项目和共享库的代码。

Docker文件与主项目在同一目录下,以保持它与该微服务的相关性和联系。其他微服务也应该有自己的Dockerfile,你可能想分享一个模板化的Dockerfile--但这是另一篇博文。

因为Dockerfile在与构建上下文不同的目录中,我们必须手动指定它,这就是为什么我们使用上面的-f 参数。

在构建Docker镜像后,你可以像这样运行一个容器。

docker run hello-world

这种共享代码的方法是可扩展的。再一次,这里的例子相对简单,但我们可以很容易地在其中添加更多的代码库和微服务。回顾一下。

  • 我们正在使用TypeScript项目引用,以确保构建任何主项目也能构建其共享库
  • 我们正在使用ts-project-bundle 来捆绑微服务的代码,包括它所依赖的任何和所有库
  • 我们使用recursive-install ,为每个微服务和它的所有共享库安装npm依赖项。

如果你想知道捆绑后的代码是什么样子的,请看下图5。你可能也想自己构建代码并检查它。你甚至不需要安装Docker就可以做到这一点--只要导航到主项目,调用npm run build ,然后在生成的build 子目录中探寻,就可以看到编译和捆绑的代码。

Figure 5: Output of the build process

图5:构建过程的输出

例3:在后端和前端之间共享代码库

这个例子也有一个Docker微服务,但它的工作方式和前面的例子一样,所以我不会再解释。

现在我们将重点讨论向前端共享代码库的问题。这个例子的UI是用TypeScript和React创建的,并与Parcel捆绑在一起。

图6解释了这个项目的结构。

Figure 6: Sharing code between backend and frontend

图6:在后端和前端之间共享代码

这个例子有目录backendfrontend ,和libs 。同样,这个项目的布局是可扩展的:你可以在libs 目录下添加更多的库,在backend 目录下添加更多的微服务。

这个结构也很灵活:你可以把它全部放在一个单一的repo中,也可以把它完全分离出来,作为一个元repo,为每个微服务、每个库和前端建立单独的代码库。

清单5显示了我们的例子前台的简单HTML代码

清单5:前台的简单HTML文件

<!DOCTYPE html>
<html lang="en">
<head>
<title>A simple React frontend using Parcel</title>
</head>
<body>
<div id="root"></div>
<script src="./src/index.tsx"></script>
</body>
</html>

这个例子使用了React框架,在清单5中,你可以看到root 元素,我们的React UI将被渲染。随后的script 标签导入了TypeScript的代码文件index.tsx 。这是我们React用户界面的主要代码文件。它也是主项目中唯一的TypeScript代码文件,因为这是一个简单的例子。

清单6显示了index.tsx ,使用React来渲染前端的用户界面。注意我们是如何从我们的共享库中导入函数并使用它来渲染前端的 "Hello, World!"信息的。

清单6:前台的React代码

import React from "react";
import ReactDOM from "react-dom";
import { showMessage } from "../../libs/my-library";
class App extends React.Component {
render() {
return <div>{showMessage()}</div>;
}
}
ReactDOM.render(<App />, document.getElementById("root"));

为了链接到共享库,我们再次使用TypeScript项目引用。你可以自己在前端的tsconfig.json文件中看到这一点,但它与你在前面的例子中看到的并无不同。

为了使这个前端能够在网络浏览器中使用,我们现在必须把它编译成一个静态网页。Parcel让这一切变得简单。它是一个零配置的捆绑器,能自动理解TypeScript。它能理解大多数常见的资产类型,对于其他的东西,有一个插件。

我目前正在使用Parcel v1,但当它更成熟时将转换为v2。如果你想了解更多,Parcel的文档中有一个关于使用TypeScript和React的部分

为了编译我们的前端,我们将调用parcel build 。我会提醒你我的个人惯例,我用npm run build ,记住这很有帮助,因为无论我在什么类型的项目中,我只需要记住这一个命令,而不是试图记住不同命令行工具的变化无常。

你可以在下面的清单7中看到它的样子,这是一个从前端的package.json

清单7:从package.json中提取,显示npm脚本

"scripts": {
"build": "tsc --build && parcel build index.html --out-dir=out",
},

清单7首先使用--build 参数调用TypeScript编译器来构建前端项目和所有共享库。然后我们调用parcel build ,并将其指向index.html ,后者又指向index.tsx ,并指定了输出目录。这就把我们的TypeScript代码编译成JavaScript,并把它捆绑到一个单一的JavaScript文件中,包含在编译的HTML文件中。

你应该尝试运行这个,并确认生成的静态网页确实包含了我们共享库的代码。导航到frontend 目录并运行这个。

npx tsc
parcel build index.html --out-dir=out

或者,更简单,运行这个。

npm run build

现在,导航到out 子目录,查看编译后的HTML和JavaScript文件。搜索 "Hello, World!",你会发现共享库中的代码已被捆绑到静态网页中。

这就是它的全部内容了。我告诉过你,这个例子没有上一个例子那么复杂

现在,你可能会说,这对Parcel来说很好,但是bundler X呢?我的第一选择其实是不使用Parcel的--我试图用Create React App来构建这个例子,但不幸的是,React还不支持项目引用

在我自己的开发中,我正在远离webpack,因为Parcel要简单得多,所以我还没有用webpack试过这个。如果它不能工作,我会感到惊讶,因为在引擎盖下,webpack将使用TypeScript编译器--或者可能是Babel--所以项目引用应该可以工作。

如果你尝试用webpack或其他捆绑程序来实现这一点,请让我知道你的工作情况。

结语

在这篇文章中,我们探讨了分享TypeScript代码库的三种不同方式。在简单了解了共享代码的标准方式(通过npm注册表或GitHub)后,我们探讨了其他轻量级的方法,以便在更大的应用程序中的组件之间共享库,如微服务之间或后端和前端之间。

我在这里介绍的例子是可扩展的。我们可以将它们扩展到包括更多的代码库、更多的微服务等,而且这种方法在单版本或元版本中都能很好地工作。它甚至可以在你的电脑上的特设文件夹中很好地工作--虽然,在这种情况下,我不知道你为什么不使用版本控制,但是,嘿。

我为自己构建的拼图中缺少的那一块是ts-project-bundle,你可以在npmGitHub上找到它。我希望有一天ts-project-bundle会成为历史的遗迹,而TypeScript编译器本身会支持捆绑或导出编译的JavaScript代码的有效手段。让我们希望他们纠正这一点,因为对我来说,这似乎是一个明显的遗漏

如果你喜欢我在这里写的东西,也请观看我关于这个话题的视频。你也可以在Twitter上关注我

The postMake sharing TypeScript code and types quick and easyappeared first onLogRocket Blog.