
本文将指导您使用 K8S
,Docker,Yarn workspace ,TypeScript,esbuild,Express 和 React 来设置构建一个基本的云原生 Web 应用程序。 在本教程的最后,您将拥有一个可完全构建和部署在 K8S 上的 Web 应用程序。
该项目将被构造为 monorepo。 monorepo 的目标是提高模块之间共享的代码量,并更好地预测这些模块如何一起通信(例如在微服务架构中)。出于本练习的目的,我们将使结构保持简单:
设置项目之前的唯一要求是在机器上安装 yarn。 Yarn 与 npm ?一样,是一个程序包管理器,但性能更好,功能也略多。 您可以在官方文档中阅读有关如何安装它的更多信息。
进入到要初始化项目的文件夹,然后通过您喜欢的终端执行以下步骤:
{
??"name":?"my-app",
??"version":?"1.0.0",
??"license":?"UNLICENSED",
??"private":?true?//?Required?for?yarn?workspace?to?work
}现在,已经创建了 package.json 文件,我们需要为我们的模块app,common 和 server 创建文件夹。 为了方便 yarn workspace 发现模块并提高项目的可读性(readability),我们将模块嵌套在 packages 文件夹下:
my-app/ ├─?packages/?//?我们当前和将来的所有模块都将存在的地方 │??├─?app/ │??├─?common/ │??├─?server/ ├─?package.json
我们的每个模块都将充当一个小型且独立的项目,并且需要其自己的 package.json 来管理依赖项。要设置它们中的每一个,我们既可以使用 yarn init(在每个文件夹中),也可以手动创建文件(例如,通过 IDE)。
软件包名称使用的命名约定是在每个软件包之前都使用 @my-app/* 作为前缀。这在 NPM 领域中称为作用域(您可以在此处阅读更多内容)。您不必像这样给自己加上前缀,但以后会有所帮助。
一旦创建并初始化了所有三个软件包,您将具有如下所示的相似之处。
app 包:
{
??"name":?"@my-app/app",
??"version":?"0.1.0",
??"license":?"UNLICENSED",
??"private":?true
}common 包:
{
??"name":?"@my-app/common",
??"version":?"0.1.0",
??"license":?"UNLICENSED",
??"private":?true
}server 包:
{
??"name":?"@my-app/server",
??"version":?"0.1.0",
??"license":?"UNLICENSED",
??"private":?true
}最后,我们需要告诉 yarn 在哪里寻找模块,所以回去编辑项目的 package.json 文件并添加以下 workspaces 属性(如果您想了解更多有关详细信息,请查看 Yarn 的 workspaces 文档)。
{
??"name":?"my-app",
??"version":?"1.0",
??"license":?"UNLICENSED",
??"private":?true,
??"workspaces":?["packages/*"]?//?在这里添加
}您的最终文件夹结构应如下所示:
my-app/ ├─?packages/ │??├─?app/ │??│??├─?package.json │??├─?common/ │??│??├─?package.json │??├─?server/ │??│??├─?package.json ├─?package.json
现在,您已经完成了项目的基础设置。
现在,我们将第一个依赖项添加到我们的项目:TypeScript。TypeScript 是 JavaScript 的超集,可在构建时实现类型检查。
通过终端进入项目的根目录,运行 yarn add -D -W typescript。
您的 package.json 应该如下所示:
{
??"name":?"my-app",
??"version":?"1.0",
??"license":?"UNLICENSED",
??"private":?true,
??"workspaces":?["packages/*"],
??"devDependencies":?{
????"typescript":?"^4.2.3"
??}
}这还将创建一个 yarn.lock 文件(该文件确保在项目的整个生命周期中依赖项的预期版本保持不变)和一个 node_modules 文件夹,该文件夹保存依赖项的 binaries。
现在我们已经安装了 TypeScript,一个好习惯是告诉它如何运行。为此,我们将添加一个配置文件,该文件应由您的 IDE 拾取(如果使用 VSCode,则会自动获取)。
在项目的根目录下创建一个 tsconfig.json 文件,并将以下内容复制到其中:
{
??"compilerOptions":?{
????/*?Basic?*/
????"target":?"es2017",
????"module":?"CommonJS",
????"lib":?["ESNext",?"DOM"],
????/*?Modules?Resolution?*/
????"moduleResolution":?"node",
????"esModuleInterop":?true,
????/*?Paths?Resolution?*/
????"baseUrl":?"./",
????"paths":?{
??????"@flipcards/*":?["packages/*"]
????},
????/*?Advanced?*/
????"jsx":?"react",
????"experimentalDecorators":?true,
????"resolveJsonModule":?true
??},
??"exclude":?["node_modules",?"**/node_modules/*",?"dist"]
}您可以轻松地搜索每个 compileoptions 属性及其操作,但对我们最有用的是 paths 属性。例如,这告诉 TypeScript 在 @my-app/server 或 @my-app/app 包中使用 @my-app/common 导入时在哪里查找代码和 typings。
您当前的项目结构现在应如下所示:
my-app/ ├─?node_modules/ ├─?packages/ │??├─?app/ │??│??├─?package.json │??├─?common/ │??│??├─?package.json │??├─?server/ │??│??├─?package.json ├─?package.json ├─?tsconfig.json ├─?yarn.lock
Yarn workspace 允许我们通过 yarn workspace @my-app/* 命令模式访问任何子包,但是每次键入完整的命令将变得非常多余。为此,我们可以创建一些 helper script 方法来提升开发体验。打开项目根目录下的 package.json,并向其添加以下 scripts 属性。
{
??"name":?"my-app",
??"version":?"1.0",
??"license":?"UNLICENSED",
??"private":?true,
??"workspaces":?["packages/*"],
??"devDependencies":?{
????"typescript":?"^4.2.3"
??},
??"scripts":?{
????"app":?"yarn?workspace?@my-app/app",
????"common":?"yarn?workspace?@my-app/common",
????"server":?"yarn?workspace?@my-app/server"
??}
}现在可以像在子包中一样执行任何命令。例如,您可以通过键入 yarn server add express 来添加一些新的依赖项。这将直接向 server 包添加新的依赖项。
在后续部分中,我们将开始构建前端和后端应用程序。
如果计划使用 Git 作为版本控制工具,强烈建议忽略生成的文件,例如二进制文件或日志。
为此,请在项目的根目录下创建一个名为 .gitignore 的新文件,并将以下内容复制到其中。这将忽略本教程稍后将生成的一些文件,并避免提交大量不必要的数据。
#?Logs yarn-debug.log* yarn-error.log* #?Binaries node_modules/ #?Builds dist/ **/public/script.js
文件夹结构应如下所示:
my-app/ ├─?packages/ ├─?.gitignore ├─?package.json
这部分将着重于将代码添加到我们的 common、app 和 server 包中。
我们将从 common 开始,因为此包将由 app 和 server 使用。它的目标是提供共享的逻辑(shared logic)和变量(variables)。
在本教程中,common 软件包将非常简单。首先,从添加新文件夹开始:
创建此文件夹后,将以下文件添加到其中:
src/index.ts
export?const?APP_TITLE?=?‘my-app‘;
现在我们有一些要导出的代码,我们想告诉 TypeScript 从其他包中导入它时在哪里寻找它。为此,我们将需要更新 package.json 文件:
package.json
{
??"name":?"@my-app/common",
??"version":?"0.1.0",
??"license":?"UNLICENSED",
??"private":?true,
??"main":?"./src/index.ts"?//?添加这一行来为?TS?提供入口点
}我们现在已经完成了 common 包!
结构提醒:
common/ ├─?src/ │??├─?index.ts ├─?package.json
该 app 包将需要以下依赖项:
从项目的根目录运行:
package.json
{
??"name":?"@my-app/app",
??"version":?"0.1.0",
??"license":?"UNLICENSED",
??"private":?true,
??"dependencies":?{
????"@my-app/common":?"^0.1.0",?//?Notice?that?we‘ve?added?this?import?manually
????"react":?"^17.0.1",
????"react-dom":?"^17.0.1"
??},
??"devDependencies":?{
????"@types/react":?"^17.0.3",
????"@types/react-dom":?"^17.0.2"
??}
}要创建我们的 React 应用程序,我们将需要添加两个新文件夹:
一旦创建了这两个文件夹,我们就可以开始添加 HTML 文件,该文件将成为我们应用程序的宿主。
public/index.html
my-appYou?need?to?enable?JavaScript?to?run?this?app.
现在我们有了要渲染的页面,我们可以通过添加下面的两个文件来实现非常基本但功能齐全的 React 应用程序。
src/index.tsx
import?*?as?React?from?‘react‘;
import?*?as?ReactDOM?from?‘react-dom‘;
import?{?App?}?from?‘./App‘;
ReactDOM.render(,?document.getElementById(‘root‘));此代码从我们的 HTML 文件挂接到 root div 中,并将 React组件树 注入其中。
src/App.tsx
import?{?APP_TITLE?}?from?‘@flipcards/common‘;
import?*?as?React?from?‘react‘;
export?function?App():?React.ReactElement?{
??const?[count,?setCount]?=?React.useState(0);
??return?(Welcome?on?{APP_TITLE}!This?is?the?main?page?of?our?application?where?you?can?confirm?that?it
????????is?dynamic?by?clicking?the?button?below.Current?count:?{count}setCount((prev)?=>?prev?+?1)}>Increment);
}这个简单的 App 组件将呈现我们的应用标题和动态计数器。这将是我们的 React tree 的入口点。随意添加您想要的任何代码。
就是这样!我们已经完成了非常基本的 React 应用程序。目前它并没有太大的作用,但是我们总是可以稍后再使用它并添加更多功能。
结构提醒:
app/ ├─?public/ │??├─?index.html ├─?src/ │??├─?App.tsx │??├─?index.tsx ├─?package.json
server 软件包将需要以下依赖项:
从项目的根目录运行:
package.json
{
??"name":?"@my-app/server",
??"version":?"0.1.0",
??"license":?"UNLICENSED",
??"private":?true,
??"dependencies":?{
????"@my-app/common":?"^0.1.0",?//?请注意,我们已手动添加了此导入
????"cors":?"^2.8.5",
????"express":?"^4.17.1"
??},
??"devDependencies":?{
????"@types/cors":?"^2.8.10",
????"@types/express":?"^4.17.11"
??}
}现在我们的 React 应用程序已经准备就绪,我们需要的最后一部分是服务器来为其提供服务。首先为其创建以下文件夹:
接下来,添加 server 的主文件:
src/index.ts
import?{?APP_TITLE?}?from?‘@flipcards/common‘;
import?cors?from?‘cors‘;
import?express?from?‘express‘;
import?{?join?}?from?‘path‘;
const?PORT?=?3000;
const?app?=?express();
app.use(cors());
//?服务来自?"public"?文件夹的静态资源(例如:当有图像要显示时)
app.use(express.static(join(__dirname,?‘../../app/public‘)));
//?为?HTML?页面提供服务
app.get(‘*‘,?(req:?any,?res:?any)?=>?{
??res.sendFile(join(__dirname,?‘../../app/public‘,?‘index.html‘));
});
app.listen(PORT,?()?=>?{
??console.log(`${APP_TITLE}‘s?server?listening?at?http://localhost:${PORT}`);
});这是一个非常基本的 Express 应用程序,但如果除了单页应用程序之外我们没有任何其他服务,那么这就足够了。
结构提醒:
server/ ├─?src/ │??├─?index.ts ├─?package.json
为了将 TypeScript 代码转换为可解释的 JavaScript 代码,并将所有外部库打包到单个文件中,我们将使用打包工具。JS/TS 生态系统中有许多捆绑器,如 WebPack、Parcel 或 Rollup,但我们将选择 esbuild。与其他捆绑器相比,esbuild 自带了许多默认加载的特性(TypeScript, React),并有巨大的性能提升(快了 100 倍)。如果你有兴趣了解更多,请花时间阅读作者的常见问题解答。
这些脚本将需要以下依赖项:
从项目的根目录运行:yarn add -D -W esbuild ts-node。
package.json
{
??"name":?"my-app",
??"version":?"1.0",
??"license":?"UNLICENSED",
??"private":?true,
??"workspaces":?["packages/*"],
??"devDependencies":?{
????"esbuild":?"^0.9.6",
????"ts-node":?"^9.1.1",
????"typescript":?"^4.2.3"
??},
??"scripts":?{
????"app":?"yarn?workspace?@my-app/app",
????"common":?"yarn?workspace?@my-app/common",
????"server":?"yarn?workspace?@my-app/server"
??}
}现在,我们拥有构建应用程序所需的所有工具,因此让我们创建第一个脚本。
首先在项目的根目录下创建一个名为 scripts/ 的新文件夹。
我们的脚本将用 TypeScript 编写,并从命令行使用 ts-node 执行。尽管存在用于 esbuild 的 CLI,但是如果您要传递更复杂的参数或将多个工作流组合在一起,则可以通过 JS 或 TS 使用该库,这更加方便。
在 scripts/ 文件夹中创建一个 build.ts 文件,并在下面添加代码(我将通过注释解释代码的作用):
scripts/build.ts
import?{?build?}?from?‘esbuild‘;
/**
?*?在构建期间传递的通用选项。
?*/
interface?BuildOptions?{
??env:?‘production‘?|?‘development‘;
}
/**
?*?app?包的一个构建器函数。
?*/
export?async?function?buildApp(options:?BuildOptions)?{
??const?{?env?}?=?options;
??await?build({
????entryPoints:?[‘packages/app/src/index.tsx‘],?//?我们从这个入口点读?React?应用程序
????outfile:?‘packages/app/public/script.js‘,?//?我们在?public/?文件夹中输出一个文件(请记住,在?HTML?页面中使用了?"script.js")
????define:?{
??????‘process.env.NODE_ENV‘:?`"${env}"`,?//?我们需要定义构建应用程序的?Node.js?环境
????},
????bundle:?true,
????minify:?env?===?‘production‘,
????sourcemap:?env?===?‘development‘,
??});
}
/**
?*?server?软件包的构建器功能。
?*/
export?async?function?buildServer(options:?BuildOptions)?{
??const?{?env?}?=?options;
??await?build({
????entryPoints:?[‘packages/server/src/index.ts‘],
????outfile:?‘packages/server/dist/index.js‘,
????define:?{
??????‘process.env.NODE_ENV‘:?`"${env}"`,
????},
????external:?[‘express‘],?//?有些库必须标记为外部库
????platform:?‘node‘,?//?为?Node?构建时,我们需要为其设置环境
????target:?‘node14.15.5‘,
????bundle:?true,
????minify:?env?===?‘production‘,
????sourcemap:?env?===?‘development‘,
??});
}
/**
?*?所有软件包的构建器功能。
?*/
async?function?buildAll()?{
??await?Promise.all([
????buildApp({
??????env:?‘production‘,
????}),
????buildServer({
??????env:?‘production‘,
????}),
??]);
}
//?当我们从终端使用?ts-node?运行脚本时,将执行此方法
buildAll();该代码很容易解释,但是如果您觉得遗漏了部分,可以查看 esbuild 的 API文档 以获取完整的关键字列表。
我们的构建脚本现已完成! 我们需要做的最后一件事是在我们的 package.json 中添加一个新命令,以方便地运行构建操作。
{
??"name":?"my-app",
??"version":?"1.0",
??"license":?"UNLICENSED",
??"private":?true,
??"workspaces":?["packages/*"],
??"devDependencies":?{
????"esbuild":?"^0.9.6",
????"ts-node":?"^9.1.1",
????"typescript":?"^4.2.3"
??},
??"scripts":?{
????"app":?"yarn?workspace?@my-app/app",
????"common":?"yarn?workspace?@my-app/common",
????"server":?"yarn?workspace?@my-app/server",
????"build":?"ts-node?./scripts/build.ts"?//?Add?this?line?here
??}
}现在,您可以在每次对项目进行更改时从项目的根文件夹运行 yarn build 来启动构建过程(如何添加hot-reloading,稍后讨论)。
结构提醒:
my-app/ ├─?packages/ ├─?scripts/ │??├─?build.ts ├─?package.json ├─?tsconfig.json
我们的应用程序已经构建好并可以提供给全世界使用,我们只需要向 package.json 添加最后一个命令即可:
{
??"name":?"my-app",
??"version":?"1.0",
??"license":?"UNLICENSED",
??"private":?true,
??"workspaces":?["packages/*"],
??"devDependencies":?{
????"esbuild":?"^0.9.6",
????"ts-node":?"^9.1.1",
????"typescript":?"^4.2.3"
??},
??"scripts":?{
????"app":?"yarn?workspace?@my-app/app",
????"common":?"yarn?workspace?@my-app/common",
????"server":?"yarn?workspace?@my-app/server",
????"build":?"ts-node?./scripts/build.ts",
????"serve":?"node?./packages/server/dist/index.js"?//?Add?this?line?here
??}
}由于我们现在正在处理纯 JavaScript,因此可以使用 node 二进制文件启动服务器。因此,继续运行 yarn serve。
如果您查看控制台,您将看到服务器正在成功侦听。你也可以打开一个浏览器,导航到 http://localhost:3000 来显示你的 React 应用????!
如果你想在运行时改变端口,你可以用一个环境变量作为前缀来启动 serve 命令: PORT=4000 yarn serve。
本节将假定您已经熟悉容器的概念。
为了能够根据我们的代码创建镜像,我们需要在计算机上安装 Docker。要了解如何基于 OS 进行安装,请花一点时间查看官方文档
。
要生成 Docker 镜像,第一步是在我们项目的根目录下创建一个 Dockerfile(这些步骤可以完全通过 CLI 来完成,但是使用配置文件是定义构建步骤的默认方式)。
FROM?node:14.15.5-alpine WORKDIR?/usr/src/app #?尽早安装依赖项,以便如果我们应用程序中的 #?某些文件发生更改,Docker无需再次下载依赖项, #?而是从下一步(“?COPY?..”)开始。 COPY?./package.json?. COPY?./yarn.lock?. COPY?./packages/app/package.json?./packages/app/ COPY?./packages/common/package.json?./packages/common/ COPY?./packages/server/package.json?./packages/server/ RUN?yarn #?复制我们应用程序的所有文件(.gitignore?中指定的文件除外) COPY?.?. #?编译?app RUN?yarn?build #?Port EXPOSE?3000 #?Serve CMD?[?"yarn",?"serve"?]
我将尝试尽可能详细地说明这里发生的事情以及这些步骤的顺序为什么很重要:
如果您想了解更多有关这些关键字的信息,可以查看 Dockerfile参考。
使用 .dockerignore 文件不是强制性的,但强烈建议您使用以下文件:
如果您已经熟悉它,它的工作原理就像 .gitignore 文件一样。您可以将以下内容复制到与 Dockerfile 相同级别的 .dockerignore 文件中,该文件将被自动提取。
README.md #?Git .gitignore #?Logs yarn-debug.log yarn-error.log #?Binaries node_modules */*/node_modules #?Builds */*/build */*/dist */*/script.js
随意添加任何您想忽略的文件,以减轻您的最终镜像。
现在我们的应用程序已经为 Docker 准备好了,我们需要一种从 Docker 生成实际镜像的方法。为此,我们将向根 package.json添加一个新命令:
{
??"name":?"my-app",
??"version":?"1.0.0",
??"license":?"MIT",
??"private":?true,
??"workspaces":?["packages/*"],
??"devDependencies":?{
????"esbuild":?"^0.9.6",
????"ts-node":?"^9.1.1",
????"typescript":?"^4.2.3"
??},
??"scripts":?{
????"app":?"yarn?workspace?@my-app/app",
????"common":?"yarn?workspace?@my-app/common",
????"server":?"yarn?workspace?@my-app/server",
????"build":?"ts-node?./scripts/build.ts",
????"serve":?"node?./packages/server/dist/index.js",
????"docker":?"docker?build?.?-t?my-app"?//?Add?this?line
??}
}docker build . -t my-app 命令告诉 docker 使用当前目录(.)查找 Dockerfile,并将生成的镜像(-t)命名为 my-app。
确保运行了 Docker 守护进程,以便在终端中使用 docker 命令。
现在该命令已经在我们项目的脚本中,您可以使用 yarn docker 运行它。
在运行该命令后,您应该期望看到以下终端输出:
Sending?build?context?to?Docker?daemon??76.16MB Step?1/12?:?FROM?node:14.15.5-alpine ?--->?c1babb15a629 Step?2/12?:?WORKDIR?/usr/src/app ?--->?b593905aaca7 Step?3/12?:?COPY?./package.json?. ?--->?e0046408059c Step?4/12?:?COPY?./yarn.lock?. ?--->?a91db028a6f9 Step?5/12?:?COPY?./packages/app/package.json?./packages/app/ ?--->?6430ae95a2f8 Step?6/12?:?COPY?./packages/common/package.json?./packages/common/ ?--->?75edad061864 Step?7/12?:?COPY?./packages/server/package.json?./packages/server/ ?--->?e8afa17a7645 Step?8/12?:?RUN?yarn ?--->?2ca50e44a11a Step?9/12?:?COPY?.?. ?--->?0642049120cf Step?10/12?:?RUN?yarn?build ?--->?Running?in?15b224066078 yarn?run?v1.22.5 $?ts-node?./scripts/build.ts Done?in?3.51s. Removing?intermediate?container?15b224066078 ?--->?9dce2d505c62 Step?11/12?:?EXPOSE?3000 ?--->?Running?in?f363ce55486b Removing?intermediate?container?f363ce55486b ?--->?961cd1512fcf Step?12/12?:?CMD?[?"yarn",?"serve"?] ?--->?Running?in?7debd7a72538 Removing?intermediate?container?7debd7a72538 ?--->?df3884d6b3d6 Successfully?built?df3884d6b3d6 Successfully?tagged?my-app:latest
就是这样!现在,我们的镜像已创建并注册在您的机器上,供 Docker 使用。 如果您希望列出可用的 Docker 镜像,则可以运行 docker image ls 命令:
→?docker?image?ls REPOSITORY????TAG???????IMAGE?ID????????CREATED??????????SIZE my-app????????latest????df3884d6b3d6????4?minutes?ago????360MB
像这样运行命令
通过命令行运行一个可用的 Docker 镜像非常简单:docker run -d -p 3000:3000 my-app
你可以确认你的容器正在运行 docker ps。这将列出所有正在运行的容器:
如果您对启动容器有其他要求和疑问,请在此处找到更多信息。
→?docker?ps CONTAINER?ID????IMAGE?????COMMAND??????????????????CREATED??????????STATUS??????????PORTS????????????????????NAMES 71465a89b58b????my-app????"docker-entrypoint.s…"???7?seconds?ago????Up?6?seconds????0.0.0.0:3000->3000/tcp???determined_shockley
现在,打开浏览器并导航到以下URL http://localhost:3000,查看您正在运行的应用程序!
使用 Yarn workspace,TypeScript,esbuild,React 和 Express 构建 K8S 云原生应用(一)
原文:https://blog.51cto.com/u_15168528/2824635