使用 Yarn workspace,TypeScript,esbuild,React 和 Express 构建 K8S 云原生应用(一)
本文将指导您使用
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应用🎉!
如果你想在运行时改变端口,你可以用一个环境变量作为前缀来启动
serve命令:
PORT=4000 yarn serve。
Docker 🐳
本节将假定您已经熟悉容器的概念。
为了能够根据我们的代码创建镜像,我们需要在计算机上安装
Docker。要了解如何基于
OS进行安装,请花一点时间查看官方文档 。
Dockerfile
要生成
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" ]
我将尝试尽可能详细地说明这里发生的事情以及这些步骤的顺序为什么很重要:
FROM
告诉Docker
将指定的基础镜像用于当前上下文。在我们的案例中,我们希望有一个可以运行Node.js
应用程序的环境。WORKDIR
设置容器中的当前工作目录。COPY
将文件或文件夹从当前本地目录(项目的根目录)复制到容器中的工作目录。如您所见,在此步骤中,我们仅复制与依赖项相关的文件。这是因为Docker
将每个构建中的命令的每个结果缓存为一层。因为我们要优化构建时间和带宽,所以我们只想在依赖项发生更改(通常比文件更改发生的频率小)时重新安装它们。RUN
在shell
中执行命令。EXPOSE
是用于容器的内部端口(与我们的应用程序的PORT env
无关)。 这里的任何值都应该很好,但是如果您想了解更多信息,可以查看官方文档。CMD
的目的是提供执行容器的默认值。
如果您想了解更多有关这些关键字的信息,可以查看 Dockerfile参考。
添加 .dockerignore
使用
.dockerignore文件不是强制性的,但强烈建议您使用以下文件:
- 确保您没有将垃圾文件复制到容器中。
- 使
COPY
命令的使用更加容易。
如果您已经熟悉它,它的工作原理就像
.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 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,查看您正在运行的应用程序🚀!
我是为少 微信:uuhells123 公众号:黑客下午茶 加我微信(互相学习交流),关注公众号(获取更多学习资料~)
- React Native:使用 JavaScript 构建原生应用 详细剖析
- 深入浅出 React Native:使用 JavaScript 构建原生应用
- 使用gulp+Browserify构建React应用
- 深入浅出 React Native:使用 JavaScript 构建原生应用
- 使用typescript构建Vue应用的实现
- 深入浅出 React Native:使用 JavaScript 构建原生应用
- 使用typescript构建react项目环境搭建
- 使用Node和React构建一个基本的CRUD应用
- 深入浅出 React Native:使用 JavaScript 构建原生应用
- 使用express.js框架一步步实现基本应用以及构建可扩展的web应用
- 挨踢部落直播课堂第七期:如何使用React构建同构(isomorphic)应用
- 【react】使用 create-react-app 构建基于TypeScript的React前端架构----上
- erui _ eruie 003使用React,GraphQL和用户身份验证构建运行状况跟踪应用
- 使用Beetle.Express简单构建高吞吐的TCP&UDP应用
- 使用Webpack构建React应用
- 深入浅出 React Native:使用 JavaScript 构建原生应用
- 使用Angular与TypeScript构建Electron应用
- 基于ES6,使用React、Webpack、Babel构建模块化JavaScript应用
- 解决:使用create-react-app构建react应用很慢
- 使用React + Redux + React-router构建可扩展的前端应用