本文将指导您使用 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/*
作为前缀。这在 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
。TypeScript
是 JavaScript
的超集,可在构建时实现类型检查。
通过终端进入项目的根目录,运行 yarn add -D -W typescript
。
- 参数
-D
将TypeScript
添加到devDependencies
,因为我们仅在开发和构建期间使用它。 - 参数
-W
允许在工作空间根目录中安装一个包,使其在app
、common
和server
上全局可用。
您的 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
添加第一个 script
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
如果计划使用 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
开始,因为此包将由 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
包将需要以下依赖项:
从项目的根目录运行:
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
<!DOCTYPE html>
<html>
<head>
<title>my-app</title>
<meta name="description" content="Welcome on my application!" />
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<!-- 这个 div 是我们将注入 React 应用程序的地方 -->
<div id="root"></div>
<!-- 这是包含我们的应用程序的脚本的路径 -->
<script src="script.js"></script>
</body>
</html>
现在我们有了要渲染的页面,我们可以通过添加下面的两个文件来实现非常基本但功能齐全的 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 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 (
<div>
<h1>Welcome on {APP_TITLE}!</h1>
<p>
This is the main page of our application where 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>
</div>
);
}
这个简单的 App
组件将呈现我们的应用标题和动态计数器。这将是我们的 React tree
的入口点。随意添加您想要的任何代码。
就是这样!我们已经完成了非常基本的 React
应用程序。目前它并没有太大的作用,但是我们总是可以稍后再使用它并添加更多功能。
结构提醒:
app/
├─ public/
│ ├─ index.html
├─ src/
│ ├─ App.tsx
│ ├─ index.tsx
├─ package.json
Server
依赖项
server
软件包将需要以下依赖项:
从项目的根目录运行:
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
倍)。如果你有兴趣了解更多,请花时间阅读作者的常见问题解答。
这些脚本将需要以下依赖项:
从项目的根目录运行: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"
}
}
Build(编译构建)
现在,我们拥有构建应用程序所需的所有工具,因此让我们创建第一个脚本。
首先在项目的根目录下创建一个名为 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
Serve(提供服务)
我们的应用程序已经构建好并可以提供给全世界使用,我们只需要向 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
应用