本文将指导您使用 K8S ,Docker,Yarn workspace ,TypeScript,esbuild,Express 和 React 来设置构建一个基本的云原生 Web 应用程序。在本教程的最后,您将拥有一个可完全构建和部署在 K8S 上的 Web 应用程序。
设置项目
该项目将被构造为 monorepo。 monorepo 的目标是提高模块之间共享的代码量,并更好地预测这些模块如何一起通信(例如在微服务架构中)。出于本练习的目的,我们将使结构保持简单:
- app,它将代表我们的 React website。
- server,它将使用 Express 服务我们的 app。
- common,其中一些代码将在 app 和 server 之间共享。
设置项目之前的唯一要求是在机器上安装 yarn。 Yarn 与 npm 一样,是一个程序包管理器,但性能更好,功能也略多。您可以在官方文档中阅读有关如何安装它的更多信息。
Workspaces(工作区)
进入到要初始化项目的文件夹,然后通过您喜欢的终端执行以下步骤:
- 使用 mkdir my-app 创建项目的文件夹(可以自由选择所需的名称)。
- 使用 cd my-app 进入文件夹。
- 使用 yarn init 初始化它。这将提示您创建初始 package.json 文件的相关问题(不用担心,一旦创建文件,您可以随时对其进行修改)。如果您不想使用 yarn init 命令,则始终可以手动创建文件,并将以下内容复制到其中:
- {
- "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
文件夹结构应如下所示:
- my-app/
- ├─ packages/
- ├─ .gitignore
- ├─ package.json
添加代码
这部分将着重于将代码添加到我们的 common、app 和 server 包中。
Common
我们将从 common 开始,因为此包将由 app 和 server 使用。它的目标是提供共享的逻辑(shared logic)和变量(variables)。
文件
在本教程中,common 软件包将非常简单。首先,从添加新文件夹开始:
src/ 文件夹,包含包的代码。
创建此文件夹后,将以下文件添加到其中:
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
依赖项
该 app 包将需要以下依赖项:
- react
- react-dom
从项目的根目录运行:
- yarn app add react react-dom
- yarn app add -D @types/react @types/react-dom (为 TypeScript 添加类型typings)
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 应用程序,我们将需要添加两个新文件夹:
- 一个 public/ 文件夹,它将保存基本 HTML 页面和我们的 assets。
- 一个 src/ 文件夹,其中包含我们应用程序的代码。
一旦创建了这两个文件夹,我们就可以开始添加 HTML 文件,该文件将成为我们应用程序的宿主。
public/index.html
-
-
-
-
my-app - name="description" content="Welcome on my application!" />
-
-
-
- -- 这个 div 是我们将注入 React 应用程序的地方 -->
- "root">
- -- 这是包含我们的应用程序的脚本的路径 -->
-
-
-
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 (
-
-
-
- This is the main page of our application where you can confirm that it
- is dynamic by clicking the button below.
-
-
-
Current count: {count}
-
-
- );
- }
这个简单的 App 组件将呈现我们的应用和动态计数器。这将是我们的 React tree 的入口点。随意添加您想要的任何代码。
就是这样!我们已经完成了非常基本的 React 应用程序。目前它并没有太大的作用,但是我们总是可以稍后再使用它并添加更多功能。
结构提醒:
- app/
- ├─ public/
- │ ├─ index.html
- ├─ src/
- │ ├─ App.tsx
- │ ├─ index.tsx
- ├─ package.json
Server
依赖项
server 软件包将需要以下依赖项:
- cors
- express
从项目的根目录运行:
- yarn server add cors express
- yarn server add -D @types/cors @types/express(为 TypeScript 添加类型typings)
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 应用程序已经准备就绪,我们需要的最后一部分是服务器来为其提供服务。首先为其创建以下文件夹:
- 一个 src/ 文件夹,包含我们服务器的代码。
接下来,添加 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
构建应用
Bundlers(打包构建捆绑器)
为了将 TypeScript 代码转换为可解释的 JavaScript 代码,并将所有外部库打包到单个文件中,我们将使用打包工具。JS/TS 生态系统中有许多捆绑器,如 WebPack、Parcel 或 Rollup,但我们将选择 esbuild。与其他捆绑器相比,esbuild 自带了许多默认加载的特性(TypeScript, React),并有巨大的性能提升(快了 100 倍)。如果你有兴趣了解更多,请花时间阅读作者的常见问题解答。
这些脚本将需要以下依赖项:
- esbuild 是我们的捆绑器
- ts-node 是 TypeScript 的 REPL,我们将使用它来执行脚本
从项目的根目录运行:yarn add -D -W esbuild ts-node。
package.json
- {
- "name": "my-app",
- "version": "1.0",
- "license": "UNLICENSED",
- "private": true,
- "workspaces": ["packages
- interface BuildOptions {
- env: 'production' | 'development';
- }
-
-
- 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',
- });
- }
-
-
- 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*/node_modules
-
- # Builds
- **/dist
- */*/script.js
随意添加任何您想忽略的文件,以减轻您的最终镜像。
构建 Docker Image
现在我们的应用程序已经为 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
- -d 以分离模式运行容器(在后台)。
- -p 设置暴露容器的端口(格式为[host port]:[container port])。因此,如果我们想将容器内部的端口 3000(还记得 Dockerfile 中的 EXPOSE 参数)暴露到容器外部的端口 8000,我们将把 8000:3000 传递给 -p 标志。
你可以确认你的容器正在运行 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,查看您正在运行的应用程序🚀!