
本文将带你使用 Kubernetes、Docker、Yarn Workspace、TypeScript、esbuild、Express 和 React,搭建一个基础的云原生 Web 应用。完成本系列后,你将拥有一个可以构建并部署到 Kubernetes 上的完整项目。
设置项目
这个项目会采用 MonoRepo 结构。使用 MonoRepo 的好处在于,不同模块之间更容易共享代码,同时也更方便统一管理它们之间的协作方式,尤其适合多模块或微服务风格的应用。为了便于理解,这里我们先保持结构简洁。
项目将包含三个模块:app,表示 React 前端;server,用于通过 Express 提供服务;common,存放前后端都可以复用的共享代码。
开始之前,只需要确保本机已经安装 Yarn。它和 npm 一样都是包管理工具,但在性能和工作区支持方面更适合这里的场景。
Workspaces
进入你准备创建项目的目录后,在终端中依次完成以下步骤。
先使用 mkdir my-app 创建项目目录,名称可以按需修改。然后执行 cd my-app 进入目录。接着运行 yarn init 初始化项目,这一步会帮助你生成最初的 package.json 文件。如果你不想通过命令交互生成,也可以手动创建该文件,并写入以下内容:
{ “name”: “my-app”, “version”: “1.0.0”, “license”: “UNLICENSED”, “private”: true }
根目录的 package.json 创建完成后,就可以为 app、common 和 server 三个模块建立目录。为了让 Yarn Workspace 更容易识别这些模块,同时让项目结构更清晰,我们统一把它们放在 packages 目录下。
my-app/ ├─ packages/ │ ├─ app/ │ ├─ common/ │ ├─ server/ ├─ package.json
每个模块都可以看作一个独立的小项目,因此都需要自己的 package.json 来维护依赖。你可以进入各自目录执行 yarn init,也可以直接手动创建这些文件。
包名建议统一使用 @my-app/* 这种形式。在 npm 生态里,这种写法叫作 scope。虽然不是强制要求,但后续管理依赖和导入路径时会更清晰。
完成三个包的初始化后,内容大致如下。
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 配置:
{ “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。
-D 表示把 TypeScript 安装到 devDependencies,因为它只会在开发和构建阶段使用。-W 表示把依赖安装到 Workspace 根目录,使整个工作区都能共享这份配置。
安装后,根目录 package.json 大致如下:
{ “name”: “my-app”, “version”: “1.0”, “license”: “UNLICENSED”, “private”: true, “workspaces”: [“packages/*”], “devDependencies”: { “typescript”: “^4.2.3” } }
执行完成后,还会生成 yarn.lock 文件,用于锁定依赖版本,确保团队成员和部署环境安装到一致的版本。同时会创建 node_modules 目录,用来存放依赖内容。
既然已经安装了 TypeScript,接下来最好补上配置文件。这样 IDE 就可以识别编译规则并提供更准确的提示。
在项目根目录新建 tsconfig.json,并写入如下内容:
{ “compilerOptions”: { “target”: “es2017”, “module”: “CommonJS”, “lib”: [“ESNext”, “DOM”], “moduleResolution”: “node”, “esModuleInterop”: true, “baseUrl”: “./”, “paths”: { “@my-app/*”: [“packages/*”] }, “jsx”: “react”, “experimentalDecorators”: true, “resolveJsonModule”: true }, “exclude”: [“node_modules”, “**/node_modules/*”, “dist”] }
这些配置项都可以单独查阅,但在这个项目里最重要的是 paths。它让 TypeScript 知道,当我们在 app 或 server 中通过 @my-app/common 这样的方式导入时,应该去哪里寻找源码和类型定义。
现在项目结构会变成这样:
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/* 的形式访问任意子包,但每次都写完整命令会比较繁琐。为了提高开发效率,我们可以在根目录中定义一些简化脚本。
打开根目录 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
如果你打算使用 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
先从 common 包开始,因为它会同时被前端和后端使用。它的职责是提供可复用的共享逻辑和常量。
文件
在这个示例里,common 包保持尽量简单。先创建一个 src/ 目录,用于存放源码。
然后在其中添加文件:
src/index.ts
export const APP_TITLE = ‘my-app’;
有了导出的内容后,还需要告诉 TypeScript 和其他包,从哪里可以找到这个入口文件。因此需要更新 common 的 package.json:
package.json
{ “name”: “@my-app/common”, “version”: “0.1.0”, “license”: “UNLICENSED”, “private”: true, “main”: “./src/index.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
安装完成后,app 的 package.json 可以写成如下形式:
{ “name”: “@my-app/app”, “version”: “0.1.0”, “license”: “UNLICENSED”, “private”: true, “dependencies”: { “@my-app/common”: “^0.1.0”, “react”: “^17.0.1”, “react-dom”: “^17.0.1” }, “devDependencies”: { “@types/react”: “^17.0.3”, “@types/react-dom”: “^17.0.2” } }
为了搭建 React 应用,还需要再添加两个目录:
public/,用于放置基础 HTML 页面和静态资源;src/,用于放置应用源码。
目录创建完成后,先添加作为宿主页面的 HTML 文件。
public/index.html
My-app
需要启用 JavaScript 才能运行此应用。
接下来添加两个核心文件,构建一个最基本但可运行的 React 应用。
src/index.tsx
import * as React from ‘react’; import * as ReactDOM from ‘react-dom’; import { App } from ‘./app’; ReactDOM.render(<App />, document.getElementById(‘root’));
这段代码会找到 HTML 中的 root 节点,并把 React 组件树挂载进去。
src/app.tsx
import { APP_TITLE } from ‘@my-app/common’; import * as React from ‘react’; export function App(): React.ReactElement { const [count, setCount] = React.useState(0); return ( <> <h1>Welcome to {APP_TITLE}!</h1> <p>This is the main page of our application. You can confirm that it is dynamic by clicking the button below.</p> <p>Current count: {count}</p> <button onClick={() => setCount((prev) => prev + 1)}>Increment</button> </> ); }
这个简单的 App 组件会显示应用标题和一个动态计数器,它也是整个 React 树的入口。后续你可以在这里继续扩展更多功能。
至此,一个最基础的 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
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”:
