7天开发一个点餐系统-采用Typescript开发一个全栈应用

ui
Food Truck 是一个基于 Javascript(Typescript) 开发的餐车点餐系统, 该系统包括了用户短信注册, 点餐, 下单支付的完整流程.
将开发过程分为7个步骤, 每个步骤在 Github 的代码仓库中都有对应的分支(从 phase/1phase/7), 方便参考.
Food Truck is an online food ordering app based on Javascript(Typescript), this system covers the flow from registering, menu navigating and placing orders. I split this flow into 7 steps, each step corresponds to a branch of this repository.

虽然说7天从头到尾开发一个全栈的应用有些夸张, 但是本文展示了如何运用成熟的云原生技术, 高效的搭建一个应用可能性. 这是一个针对有一定开发经验的初级教程, 所以并不会深入讨论每一个技术点, 但是会较全面的覆盖开发一个全栈应用的所有方面, 也包括持续集成(CI/CD) 方面的内容.
Although it is an exaggeration to develop a full-stack application in 7 days, but this article shows how to use mature cloud technology to build an application possibility. This is a primary tutorial for new bees with a little development experience, so it will not deep dive every technical point, but it will cover all aspects of developing a full-stack application, including continuous integration (CI/ CD).

服务端采用了 Node.jsKoa2 框架. 用户鉴权是基于 AWSCognito, 注册登入涉及的后台 Lambda Function 是在前端的工程里, 由 AWS Amplify 提供支持. 支付采用了 Stripe 提供的服务.
The server uses the Node.js and Koa2 frameworks. User authentication is based on Cognito of AWS, the backend Lambda Function involved in registration and login is in the front-end project and is supported by AWS Amplify. Payment uses the service provided by Stripe.

前端采用 Vue3, 考虑第三方库的兼容性问题, 没有采用 Vite 作为开发和打包工具.
The front-end uses Vue3

作为抛砖引玉, 这篇文章提供一个大家可以学习交流的起点, 有兴趣的同学欢迎加微信和笔者互相交流.
As an introduction, this article provides a starting point for everyone to learn and communicate. It is welcome to add WeChat to communicate with the author.
IMG_3482
相关的代码仓库如下:
The relevant code repositories are as follows:
后端代码仓库 Back-end
前端代码仓库 Front-end
共享库 Shared library

代码仓库暂时不公开, 有需要交流学习的同学可以添加联系方式。

Day 1 搭建前后端项目脚手架

”工欲善其事,必先利其器“

If a worker wants to do his job well, he must first sharpen his tools

  • 高效的开发环境和顺畅的持续集成流程决定了开发的效率. 在 MacOS 下建议安装 iTerm2, Oh-My-Zsh 并配置 Powerlevel10K 的 theme. 这里不详细展开这些工具的安装了, 有兴趣的同学可以上网搜一下. 这是我的开发环境.

An efficient development environment and a smooth continuous integration process guarantees the efficiency of development. Under MacOS, it is recommended to install iTerm2, Oh-My-Zsh and then configure the theme of Powerlevel10K. The installation of these tools will not be discussed here, if you are interested in these tools, you can google it. This is my development environment.
oh-my-zsh-powerlevel10k

  • 如果是一个团队参与一个项目的开发, 在开发时统一代码规范也是很重要的, 这个章节会重点描述 ESLintPrettier 的配置来规范团队的代码.

It is very important to unify the code style during development if a team is involved in a project. This section will focus on the configuration of ESLint and Prettier to standardize the team's code style.

从零到一创建一个后端的工程(create backend project from scratch)

  1. 先在 Github 上创建一个仓库 ft-node

create ft-node repository on Github first

  • 初始化本地 git 仓库, 并推送到 Github 仓库中

initialize the local repository, and push it onto the remote repository under Github

mkdir backend && cd backend
echo "# ft-node" >> README.md
git init
git add README.md
git commit -m "first commit"
git remote add origin git@github.com:quboqin/ft-node.git
git push -u origin main
复制代码
  • 添加 .gitignore

add .gitignore file

# .gitignore
# Dependency directory
node_modules

# Ignore built ts files
dist
复制代码
  1. 初始化 npmTypescript 项目

Intialize the node.js project with npm, and configure Typescript

# 创建代码目录
mkdir src
# 初始化 npm 项目
npm init -y
# 在初始化 Typescript 项目前先在本地安装 typescript
npm install typescript --save-dev
# 初始化 typescript 项目
npx tsc --init --rootDir src --outDir dist \
--esModuleInterop --target esnext --lib esnext \
--module commonjs --allowJs true --noImplicitAny true \
--resolveJsonModule true --experimentalDecorators true --emitDecoratorMetadata true \
--sourceMap true --allowSyntheticDefaultImports true
复制代码
  1. 安装 Koa, Koa Router@Koa/cors(支持跨域), 以及对应的类型依赖
npm i koa koa-router @koa/cors
npm i @types/koa @types/koa-router types/koa__cors --save-dev
复制代码
  1. 创建一个简单 Koa 服务
  • 在 src 目录下创建 server.ts 文件
// server.ts
import Koa from 'koa'
import Router from 'koa-router'
import cors from '@koa/cors'

const app = new Koa()
const router = new Router()

app.use(cors())

router.get('/checkHealth', async (ctx) => {
  ctx.body = {
    code: 0,
    data: 'Hello World!',
  }
})

app.use(router.routes())

app.listen(3000)

console.log('Server running on port 3000')
复制代码
  • 测试
npx tsc
node dist/server.js
Server running on port 3000
复制代码
  1. 安装 ts-nodenodemon, Running the server with Nodemon and TS-Node, 并修改 package.json 中的脚本
npm i ts-node nodemon --save-dev
# 如果 typescript 安装在全局环境下
# npm link typescript
复制代码

修改 package.json

  "scripts": {
    "dev": "nodemon --watch 'src/**/*' -e ts,tsx --exec ts-node ./src/server.ts",
    "build": "rm -rf dist && tsc",
    "start": "node dist/server.js"
  },
复制代码
  1. 配置 ESLintPrettier

Lint

  • 安装 eslint 和相关依赖
npm i eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin --save-dev
复制代码
  • eslint: The core ESLint linting library
  • @typescript-eslint/parser: The parser that will allow ESLint to lint TypeScript code
  • @typescript-eslint/eslint-plugin: A plugin that contains a bunch of ESLint rules that are TypeScript specific
  • 添加 .eslintrc.js 配置文件

可以通过, 以交互的方式创建

npx eslint --init
复制代码

但是建议手动在项目的 root 目录下创建这个文件

// .eslintrc.js
module.exports = {
  parser: '@typescript-eslint/parser',  // Specifies the ESLint parser
  parserOptions: {
    ecmaVersion: 2020,  // Allows for the parsing of modern ECMAScript features
    sourceType: 'module', // Allows for the use of imports
  },
  extends: [
    'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin
  ],
  rules: {
    // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
    // e.g. "@typescript-eslint/explicit-function-return-type": "off",
  },
}
复制代码
  • 添加 Prettier
npm i prettier eslint-config-prettier eslint-plugin-prettier --save-dev
复制代码
  • prettier: The core prettier library
  • eslint-config-prettier: Disables ESLint rules that might conflict with prettier
  • eslint-plugin-prettier: Runs prettier as an ESLint rule

In order to configure prettier, a .prettierrc.js file is required at the root project directory. Here is a sample .prettierrc.js file:

// .prettierrc.js
module.exports = {
  tabWidth: 2,
  semi: false,
  singleQuote: true,
  trailingComma: 'all',
}
复制代码

Next, the .eslintrc.js file needs to be updated:

// eslintrc.js
extends: [
  "plugin:@typescript-eslint/recommended", // Uses the recommended rules from the @typescript-eslint/eslint-plugin
  "plugin:prettier/recommended" // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array.
],
复制代码
  • Automatically Fix Code in VS Code
    • 安装 eslint 扩展, 这里不需要安装 prettier 扩展

并点击右下角激活 eslint 扩展
eslint-vscode-extension-enable

  • 在 VSCode 中创建 Workspace 的配置
{
  "editor.formatOnSave": false,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
}
复制代码
  • Run ESLint with the CLI
    • A useful command to add to the package.json scripts is a lint command that will run ESLint.
{
  "scripts": {
    "lint": "eslint '*/**/*.{js,ts,tsx}' --quiet --fix"
  }
}
复制代码
  • 添加 .eslintignore
# don't ever lint node_modules
node_modules
# don't lint build output (make sure it's set to your correct build folder name)
dist
复制代码
  • Preventing ESLint and formatting errors from being committed
npm install husky lint-staged --save-dev
复制代码

To ensure all files committed to git don't have any linting or formatting errors, there is a tool called lint-staged that can be used. lint-staged allows to run linting commands on files that are staged to be committed. When lint-staged is used in combination with husky, the linting commands specified with lint-staged can be executed to staged files on pre-commit (if unfamiliar with git hooks, read about them here).
To configure lint-staged and husky, add the following configuration to the package.json file:

{
  "husky": {
      "hooks": {
          "pre-commit": "lint-staged"
      }
  },
  "lint-staged": {
      "*.{js,ts,tsx}": [
          "eslint --fix"
      ]
  }
}
复制代码

初始化 Vue3 前端

  1. 通过 Vue Cli 创建前端项目
# 参看 vue cli 的版本
vue --version
# 更新一下最新版本
npm update -g @vue/cli
# 创建项目
vue create frontend
复制代码

vue-cli-5

  1. Vue Cli 创建的脚手架已经配置好大部分 ESLintPrettier, 这里只要统一一下前后端的 Prettier 设置
  • 添加 .prettierrc.js 文件
// .prettierrc.js
module.exports = {
  tabWidth: 2,
  semi: false,
  singleQuote: true,
  trailingComma: 'all',
}
复制代码
  • 同样修改 workspace 的配置
{
  "editor.formatOnSave": false,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  }
}
复制代码
  1. 运行 npm run lint

前端引入 Axois, 访问接口(health interface)

  1. 安装 axios 模块
npm i axios --save
复制代码
  1. 封装 axios 模块, 设置默认 URL
// src/utils/axios.ts
axios.defaults.baseURL = 'http://localhost:3000'
复制代码
  1. 封装 src/apis/health.ts 文件
// src/apis/health.ts
import { result } from '@/utils/axios'

export function checkHealth<U>(): Promise<U | void> {
  return result('get', '/checkHealth')
}
复制代码
  1. 在 App.vue 中调用 checkHealth 接口
<script lang="ts">
import { defineComponent, onMounted } from 'vue'

import { checkHealth } from '@/apis/health'

export default defineComponent({
  name: 'App',
  setup() {
    const init = async () => {
      checkHealth()
    }
    onMounted(init)
  },
})
</script>
复制代码

这个阶段完成了一个最简单 APP 的前后端工程的搭建. 我这里规范了前后端的代码, 同学可以根据自己团队的代码规范添加不同的规则.

Javascript & Typescript Essential

Day 2 重构后端项目 前端引入状态管理

重构服务端

引入服务端日志库 loggerwinston

# logger 作为记录请求的中间件
npm install koa-logger --save
npm install @types/koa-logger --save-dev
# winston 用于后端其他地方的日志记录
npm install winston --save
复制代码

效果如下
koa-logger-winston

引入 dotenv , 从环境变量和环境变量配置文件读取配置

  1. node 的后端服务是在运行时动态加载这些环境变量的,代码里我采用了 dotenv 模块来加载环境变量
npm install dotenv --save
复制代码
  1. 读取环境变量
// src/config/index.ts
import { config as dotenv } from 'dotenv'

const env = dotenv({ path: `.env.${process.env.NODE_ENV}` })
if (env.error) throw env.error
复制代码
  1. ** 两个注意点 **
  • 如果 NODE_ENV 设置为 production, npm ci 不会安装 devDependencies 中的依赖包,如果在运行的 EC2 上编译打包,编译会报错。所以打包编译我放在了 Github Actions 的容器中了,所以避免了这个问题

We have installed all our packages using the --save-dev flag. In production, if we use npm install --production these packages will be skipped.

  • 操作系统下设置的环境变量会覆盖 .env 文件中定义的环境变量, 一般会把敏感的信息, 例如密码和密钥直接定义在系统的环境变量里, 而不是放在 .env 的文件下, 这样避免将代码提交到公开的代码仓库带来安全的隐患.
  1. 配置文件的加载逻辑都放到 config 目录下

dotenv-config

服务端拆解 App 和 Server

  1. index.ts 只保留了启动 http 服务和其他连接数据库的服务
// src/index.ts
import * as http from 'http'
import app from './app'

import wsLogger from './utils/ws-logger'

async function initializeDB(): Promise<void> {
  wsLogger.info(`Connect database...`)

  return
}

async function bootstrap(port: number) {
  const HOST = '0.0.0.0'

  return http.createServer(app.callback()).listen(port, HOST)
}

try {
  ;(async function (): Promise<void> {
    await initializeDB()

    const HTTP_PORT = 3000
    await bootstrap(HTTP_PORT)

    wsLogger.info(`🚀 Server listening on port ${HTTP_PORT}!`)
  })()
} catch (error) {
  setImmediate(() => {
    wsLogger.error(
      `Unable to run the server because of the following error: ${error}`,
    )
    process.exit()
  })
}
复制代码
  1. app.ts 负责各种中间件的加载, 并安装 bodyParser
npm install koa-bodyparser --save
npm install @types/koa-bodyparser --save-dev
复制代码
// src/app.ts
import Koa from 'koa'
import cors from '@koa/cors'
import logger from 'koa-logger'
import bodyParser from 'koa-bodyparser'

import wrapperRouter from './apis'

const app = new Koa()

app.use(cors())
app.use(logger())
app.use(bodyParser())

const wpRouter = wrapperRouter()
app.use(wpRouter.routes()).use(wpRouter.allowedMethods())

export default app
复制代码

搭建 RCS 模型, 将 server.ts 拆成 http 服务, Koa2 中间件和 koa-router 三个模块, 符合单一职责的设计原理

the logic should be divided into these directories and files.

  • Models - The schema definition of the Model
  • Routes - The API routes maps to the Controllers
  • Controllers - The controllers handles all the logic behind validating request parameters, query, Sending Responses with correct codes.
  • Services - The services contains the database queries and returning objects or throwing errors
  1. router 拆解到 apis 目录下, 按模块提供不同的接口, 并通过动态的方式加载
// src/apis/index.ts
import * as fs from 'fs'
import * as path from 'path'
import Router from 'koa-router'

import { serverConfig } from '../config/server'
const { apiVersion } = { apiVersion: serverConfig.apiVersion }

export default function wrapperRouter(): Router {
  const router = new Router({
    prefix: `/api/${apiVersion}`,
  })

  const baseName = path.basename(__filename)

  fs.readdirSync(__dirname)
    // 过滤掉 apis 根目录下的 index.js 和 index.js.map 文件
    .filter(
      (file) =>
        file.indexOf('.') !== 0 && file !== baseName && !file.endsWith('.map'),
    )
    .forEach((file) => {
      import(path.join(__dirname, file)).then((module) => {
        router.use(module.router().routes())
      })
    })

  return router
}
复制代码
  1. apis 下各个子模块的拆解如下图

rcs

构建前端路由, 引入 Vue 3.0 的状态管理 Provide 和 Inject

用 Vue3 的 Provide 和 Inject 替代 Vuex
参考 Using provide/inject in Vue.js 3 with the Composition API

用 npm 搭建共享的 Lib, 共享数据对象(Building and publishing an npm typescript package)

参考 Step by step: Building and publishing an NPM Typescript package.

  1. 创建目录
mkdir lib && cd lib
复制代码
  1. 初始化 git 仓库
git init
git checkout -b main
echo "# Building and publishing an npm typescript package" >> README.md
git add . && git commit -m "Initial commit"
复制代码
  1. 初始化 npm 项目
npm init -y
echo "node_modules" >> .gitignore
npm install --save-dev typescript
复制代码
  1. 配置 tsconfig.json
{
  "compilerOptions": {
    "target": "es2017",
    "module": "commonjs",
    "outDir": "./lib",
    "sourceMap": true,
    "strict": true,
    "noImplicitAny": true,
    "declaration": true,
    "strictPropertyInitialization": false,
    "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */,
    "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */
  },
  "include": ["src"],
  "exclude": ["node_modules", "**/__tests__/*"]
}
复制代码
  1. ignore /lib
echo "/lib" >> .gitignore
复制代码
  1. 配置 eslint
  • 安装 eslint 和 typescript 相关依赖
npm i eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin --save-dev
复制代码
  • 创建 .eslintrc.js
// .eslintrc.js
module.exports = {
  parser: '@typescript-eslint/parser',
  extends: ['plugin:@typescript-eslint/recommended', 'prettier/@typescript-eslint', 'plugin:prettier/recommended'],
  parserOptions: {
    ecmaVersion: 2018,
    sourceType: 'module',
  },
  rules: {},
}
复制代码
  • 安装 prettier
npm i prettier eslint-config-prettier eslint-plugin-prettier -D
复制代码
  • 创建 .prettierrc.js
// .prettierrc.js
module.exports = {
  semi: false,
  trailingComma: 'all',
  singleQuote: true,
  printWidth: 120,
  tabWidth: 2,
}
复制代码
  • 配置 vscode workspace
{
  "editor.formatOnSave": false,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
}
复制代码
  • 添加 .eslintignore
node_modules
/lib
复制代码
  1. 配置 npm 库的输出文件

However, blacklisting files is not a good practice. Every new file/folder added to the root, needs to be added to the .npmignore file as well! Instead, you should whitelist the files /folders you want to publish. This can be done by adding the files property in package.json:

  "files": [
    "lib/**/*"
  ],
复制代码
  1. Setup Testing with Jest
  • 安装 Jest
npm install --save-dev jest ts-jest @types/jest
复制代码
  • Create a new file in the root and name it jestconfig.json:
{
  "transform": {
    "^.+\\.(t|j)sx?$": "ts-jest"
  },
  "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
  "moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"]
}
复制代码
  • Remove the old test script in package.json and change it to:
"test": "jest --config jestconfig.json",
复制代码
  • 准备一个待测试的函数 index.ts
// index.ts
export const Greeter = (name: string): string => `Hello ${name}`
复制代码
  • Write a basic test

In the src folder, add a new folder called tests and inside, add a new file with a name you like, but it has to end with test.ts, for example greeter.test.ts

// greeter.test.ts
import { Greeter } from '../index';
test('My Greeter', () => {
  expect(Greeter('Carl')).toBe('Hello Carl');
});
复制代码

then try to run

npm test
复制代码
  1. Use the magic scripts in NPM
  • 修改脚本 package.json
    "prepare": "npm run build",
    "prepublishOnly": "npm test && npm run lint",
    "preversion": "npm run lint",
    "version": "git add -A src",
    "postversion": "git push && git push --tags",
    "patch": "npm version patch && npm publish"
复制代码
  • Updating your published package version number

To change the version number in package.json, on the command line, in the package root directory, run the following command, replacing <update_type> with one of the semantic versioning release types (patch, major, or minor):

npm version <update_type>
复制代码
  • Run npm publish.
  1. Finishing up package.json
  "description": "share library between backend and frontend",
  "main": "lib/index.js",
  "types": "lib/index.d.ts",
复制代码
  1. Publish you package to NPM!
  • run npm login
╰─ npm login                                                                                                                                  ─╯
npm notice Log in on https://registry.npmjs.org/
Username: quboqin
Password: 
Email: (this IS public) qubo.qin.2018@gmail.com
Logged in as quboqin on https://registry.npmjs.org/.
复制代码
  1. install quboqin-lib on both sides(frontend and backend), and add test code
npm i quboqin-lib
复制代码

Day 3 前端的界面设计和开发

创建数据模型, 更新 quboqin-lib

前端引入 Tailwind CSS UI 库

  1. Figma 设计一个 UI 的原型

figma
2. 安装 Tailwind
这是一个基于 utility-first 思想的 UI 库, 在它基础上上可以方便搭建 UI 界面, 基本不用在代码里写一行 css. 也方便后期维护
安装中要注意 PostCSS 7 的兼容性

  • 安装
npm install -D tailwindcss@npm:@tailwindcss/postcss7-compat postcss@^7 autoprefixer@^9
复制代码
  • Add Tailwind as a PostCSS plugin

Add tailwindcss and autoprefixer to your PostCSS configuration.

// postcss.config.js
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  }
}
复制代码
  • Customize your Tailwind installation
npx tailwindcss init
复制代码
  • Include Tailwind in your CSS
/* index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
复制代码

then import this file in main.ts

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

import './index.css'

createApp(App).use(router).mount('#app')
复制代码
  • When building for production, be sure to configure the purge option to remove any unused classes for the smallest file size:
// tailwind.config.js
  module.exports = {
   purge: [
     './src/**/*.html',
     './src/**/*.js',
   ],
    darkMode: false, // or 'media' or 'class'
    theme: {
      extend: {},
    },
    variants: {},
    plugins: [],
  }
复制代码
  1. 其他组件库

可以用 Tailwind 开发 UI 的组件, 这个工程里为了快速开发, 这里还引入了 Vant, 这是一个较早支持 Vue 3 的 UI 库

# Install Vant 3 for Vue 3 project
npm i vant@next -S
复制代码
  1. 用到了 materialdesignicons , 在 html 中添加
<link href="https://cdn.jsdelivr.net/npm/@mdi/font@5.x/css/materialdesignicons.min.css" rel="stylesheet">
复制代码

封装 Axios 和接口, 提供 Mock 数据

// src/apis/goods.ts
import goods from '@/mock/goods.json'

export function getAllGoods(): Promise<Good[] | void> {
  return result('get', '/goods', null, goods as Good[])
}
复制代码

本地支持HTTPS[Server]

  1. 在项目的 root 目录下创建 local-ssl 子目录, 在 local-ssl 子目录下,新建一个配置文件 req.cnf
[req]
distinguished_name = req_distinguished_name
x509_extensions = v3_req
prompt = no
[req_distinguished_name]
C = US
ST = California
L = Folsom
O = MyCompany
OU = Dev Department
CN = www.localhost.com
[v3_req]
keyUsage = critical, digitalSignature, keyAgreement
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = www.localhost.com
DNS.2 = localhost.com
DNS.3 = localhost
复制代码
  1. local-ssl 目录下创建本地证书和私钥
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout cert.key -out cert.pem -config req.cnf -sha256
复制代码
  1. 修改 server 端代码支持本地 https
// src/index.ts
  interface CERT_FILE {
    key: string
    cert: string
    ca?: string
  }

  const certPaths: Record<string, CERT_FILE> = {}

  certPaths[`${SERVER.LOCALHOST}`] = {
    key: './local-ssl/cert.key',
    cert: './local-ssl/cert.pem',
  }

  const httpsOptions = {
    key: fs.readFileSync(certPaths[config.serverConfig.server].key),
    cert: fs.readFileSync(certPaths[config.serverConfig.server].cert),
  }

  return https.createServer(httpsOptions, app.callback()).listen(port, HOST)
复制代码
  1. 启动 https 服务后,仍然报错
curl https://localhost:3000/api/v1/users
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.haxx.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above
复制代码

在浏览器链接该 https 服务,也报错误,下载证书,双击导入密钥管理器,手动让证书受信
always-trust

  1. 前端接口也改成 https 访问
// src/utils/axios.ts
axios.defaults.baseURL = 'https://localhost:3000/api/v1'
复制代码

Day 4 手机登录和注册

用户鉴权和手机登录用到了 AWSCognitoAmplify 等服务

在前端工程里初始化 AmplifyCognito

  1. 初始化 Amplify
amplify init
复制代码

amplify-init
默认创建了 dev 后端环境

  1. 添加 Auth 服务
amplify add auth
复制代码

**一定要选择 Manual Configuration **, 具体参数如下
cognito-manual
否则 Cognito 后台配置会默认为 Username 方式鉴权, 而且无法修改

  1. 添加 PreSignup 的函数[选择 Manual Configuration 可以在命令行里添加, 这步可以跳过]
amplify fucntion add
复制代码

amplify-function add

  1. 更新这个四个 Lambda Functions 的逻辑

  2. 推送 dev 环境

amplify push
复制代码
  1. 进入 AWS ConsoleCognito, 手动绑定 PreSignup 函数[选择 Manual Configuration 在命令行里已经绑定, 这步可以跳过]

cognito-presignup

  1. CreateAuthChallenge 添加发送短信权限

进入 Lambda 的 后台页面,找到对应环境下 devfoodtruck4d6aa6e5CreateAuthChallenge 函数
找到对应的 Role,在Role的后台添加 SNS 权限
sns-role

  1. 配置 SNS

aws-sns-profile

  1. 前端添加 Amplify 模块
  • 安装依赖包
npm i aws-amplify aws-amplify-vue --save
复制代码
  • 初始化 Amplify 配置
// main.ts
import Amplify from 'aws-amplify'
import awsconfig from './aws-exports'
Amplify.configure(awsconfig)
复制代码

** 在 tsconfig.json 中添加

"allowJs": true,
复制代码

添加 aws-exports.d.ts

// eslint-disable-next-line @typescript-eslint/ban-types
declare const awsmobile: {}
export default awsmobile
复制代码

这两步都要, 否则打包会报以下错误

TS7016: Could not find a declaration file for module './aws-exports'. '/Users/qinqubo/magic/projects/full-stack/food-truck/frontend/src/aws-exports.js' implicitly has an 'any' type.
    4 |
    5 | import Amplify from 'aws-amplify'
  > 6 | import awsconfig from './aws-exports'
      |                       ^^^^^^^^^^^^^^^
    7 | Amplify.configure(awsconfig)
    8 |
    9 | import './index.css'
复制代码
  1. 团队其他成员如果在新的环境下开发

clone 代码仓库后, 在项目根目录下

amplify init
复制代码

amplify-clone-backend
Do you want to use an existing environmentYes

后端工程添加 JWT 模块

  1. 添加 JWT 的中间件的依赖
npm i jsonwebtoken jwk-to-pem --save
npm i @types/jsonwebtoken @types/jwk-to-pem --save-dev
复制代码
  1. 添加 JWT 中间件, 检查 cognito 的有效性
// src/jwt/cognito.ts
复制代码
  1. 修改 app.ts
// src/app.ts
const unprotectedRouter = wrapperRouter(false)
const protectedRouter = wrapperRouter(true)

app.use(unprotectedRouter.routes()).use(unprotectedRouter.allowedMethods())
app.use(jwtCognito(config.awsCognitoConfig))
app.use(protectedRouter.routes()).use(protectedRouter.allowedMethods())

export default app
复制代码
  1. apis 目录下分受保护的和不受保护的两类目录, 并修改路由加载逻辑
// src/apis/index.ts
export default function wrapperRouter(isProtected: boolean): Router {
  const router = new Router({
    prefix: `/api/${apiVersion}`,
  })

  const subFolder = path.resolve(
    __dirname,
    isProtected ? './protected' : './unprotected',
  )

  // Require all the folders and create a sub-router for each feature api
  fs.readdirSync(subFolder).forEach((file) => {
    import(path.join(subFolder, file)).then((module) => {
      router.use(module.router().routes())
    })
  })

  return router
}
复制代码

用 Postman 测试

  1. 设置Content-Type为application/json

postman-content-type

  1. 设置Token

postman-token

  1. Query parameters
https://localhost:3000/api/v1/goods
https://localhost:3000/api/v1/users
复制代码

Day 5 数据库的支持

Postgres 和数据模型

  1. 在后端项目安装 postgres 相关 package
npm i typeorm reflect-metadata pg --save
复制代码
  1. 改造共享库
  • 在共享库安装 typeorm
npm i typeorm --save
复制代码

之后更新前后端的依赖

  1. 在后端工程里安装 uuid
npm i uuid --save
npm i @types/uuid --save-dev
复制代码
  1. 通过 docker 连接数据库
  • 添加 docker-compose.yml
version: '3.1'
services:
  db:
    image: postgres:10-alpine
    ports:
      - '5432:5432'
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: apidb
  admin:
    image: adminer
    restart: always
    depends_on:
      - db
    ports:
      - 8081:8080
复制代码
  • 添加 postgres 配置和连接代码
// src/config/index.ts
export async function initializePostgres(): Promise<Connection | void> {
  if (postgresConfig) {
    try {
      return createConnection({
        type: 'postgres',
        url: postgresConfig.databaseUrl,
        ssl: postgresConfig.dbsslconn
          ? { rejectUnauthorized: false }
          : postgresConfig.dbsslconn,
        synchronize: true,
        logging: false,
        entities: postgresConfig.dbEntitiesPath,
      })
    } catch (error) {
      logger.error(error)
    }
  }
}
复制代码
  • 导入 goods 数据
npm run import-goods
复制代码

Jest 测试服务层和数据库

  1. 安装 Jest
npm i jest @types/jest ts-jest --save-dev
复制代码
  1. 配置 Jest
// jest.config.js
/* eslint-disable max-len */
module.exports = {
  clearMocks: true,
  maxWorkers: 1,
  preset: 'ts-jest',
  testEnvironment: 'node',
  testMatch: [
    '**/__tests__/**/*.[jt]s?(x)',
    '!**/__tests__/coverage/**',
    '!**/__tests__/utils/**',
    '!**/__tests__/images/**',
    //   "**/?(*.)+(spec|test).[tj]s?(x)"
  ],
  globalSetup: './src/utils/jest.setup.ts',
}
复制代码
// ormconfig.js
module.exports = {
  name: 'default',
  type: 'postgres',
  host: 'localhost',
  port: 5432,
  username: 'user',
  password: 'pass',
  database: 'test',
  logging: false,
  synchronize: true,
  entities: ['node_modules/quboqin-lib/lib/**/*.js'],
}
复制代码
  1. 在 src/tests 下添加两个测试用例

axois 不直接支持 URL的 Path Variable, .get('/:phone', controller.getUser) 路由无法通过 axois 直接访问, 修改 getUsers, 通过 Query Parameter 提取 phone

  1. Path parameters

** axois发出的请求不支持这种方式 **,可以在浏览器和Postman里测试这类接口

https://localhost:3000/api/v1/addresses/24e583d5-c0ff-4170-8131-4c40c8b1e474
复制代码

对应的route是

  router
    .get('/:id', controller.getAddress)
复制代码

下面的控制器里演示是如何取到参数的

  public static async getAddress(ctx: Context): Promise<void> {
    const { id } = ctx.params
    const address: Address = await postgre.getAddressById(id)

    const body = new Body()
    body.code = ERROR_CODE.NO_ERROR

    if (address) {
      body.data = address
    } else {
      body.code = ERROR_CODE.UNKNOW_ERROR
      body.message = `the card you are trying to retrieve doesn't exist in the db`
    }

    ctx.status = 200
    ctx.body = body
  }
复制代码
  • 如何现在Postman里测试接口

设置Content-Type为application/json
postman-content-type

设置Token
postman-token

  1. Query parameters
https://localhost:3000/api/v1/addresses?phone=%2B13233013227&id=24e583d5-c0ff-4170-8131-4c40c8b1e474
复制代码
  1. Koa2 对应 Axios 返回的值, 以下是伪代码
// Koa ctx: Context
interface ctx {
  status,
  body: {
    code,
    data,
    message
  }
}

// Axois response
interface response {
  status,
  data
}
复制代码

response.data 对应 ctx.body, 所以在 Axios 获取 response 后, 是从 response.data.data 得到最终的结果

function get<T, U>(path: string, params: T): Promise<U> {
  return new Promise((resolve, reject) => {
    axios
      .get(path, {
        params: params,
      })
      .then((response) => {
        resolve(response.data.data)
      })
      .catch((error) => {
        reject(error)
      })
  })
}
复制代码

Day 6 手机支付

服务端的实现

  1. 引入 stripe
npm i stripe --save
复制代码
  1. 配置 stripe
// src/utils/stripe.ts
import Stripe from 'stripe'
import { config } from '../config'
import { PAYMENT_TYPE } from 'quboqin-lib/lib/card'

export const stripe = new Stripe(
  config.paymentType === PAYMENT_TYPE.STRIPE_ONLINE
    ? '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'
    : 'sk_test_DU1VhfGGYrEv5EvJS2my4yhD',
  { apiVersion: '2020-08-27' },
)
复制代码
  1. 在订单接口中发起支付调用

前端的实现

  1. html 文件里添加 stripe 全局模块
    <script src="https://js.stripe.com/v3/"></script>
复制代码
  1. 添加 stripe 的封装
// scr/utils/stripe.js

// eslint-disable-next-line no-undef
export const stripe = Stripe(
  process.env.VUE_APP_ONLINE_PAYMENT === '1'
    ? 'pk_live_51HZCSNLZTTlHwkSOdYTRnFdh0AxF7JNwXShbMrKfEPzxnXLPzGz0hXJJzKxybnWngvF89FKJRXxnr2fo8zpNlZ5700Nm864NNM'
    : 'pk_test_GrY8g9brTmOaRZ6XKks0woG0',
)

export const elements = stripe.elements()

const elementStyles = {
  base: {
    color: '#32325D',
    fontWeight: 500,
    fontFamily: 'Source Code Pro, Consolas, Menlo, monospace',
    fontSize: '16px',
    fontSmoothing: 'antialiased',

    '::placeholder': {
      color: '#CFD7DF',
    },
    ':-webkit-autofill': {
      color: '#e39f48',
    },
  },
  invalid: {
    color: '#E25950',

    '::placeholder': {
      color: '#FFCCA5',
    },
  },
}

const elementClasses = {
  focus: 'focused',
  empty: 'empty',
  invalid: 'invalid',
}

// export const cardIBan = elements.create('iban', {
//   ...{
//     style: elementStyles,
//     classes: elementClasses,
//   },
//   ...{
//     supportedCountries: ['SEPA'],
//     placeholderCountry: 'US',
//     hideIcon: true,
//   },
// })

export const cardNumber = elements.create('cardNumber', {
  style: elementStyles,
  classes: elementClasses,
})

export const cardExpiry = elements.create('cardExpiry', {
  style: elementStyles,
  classes: elementClasses,
})

export const cardCvc = elements.create('cardCvc', {
  style: elementStyles,
  classes: elementClasses,
})
复制代码
  1. 添加开发环境配置

.env
.env.development
改造 axois 读取配置

// src/utils/axios.ts
const port = process.env.VUE_APP_PORT
const url = process.env.VUE_APP_BASE_URL
axios.defaults.baseURL = port ? `${url}:${process.env.VUE_APP_PORT}` : `${url}`
axios.defaults.timeout = process.env.VUE_APP_TIMEOUT
  ? +process.env.VUE_APP_TIMEOUT
  : 5000
axios.defaults.headers.post['Content-Type'] =
  'application/x-www-form-urlencoded;charset=UTF-8'
复制代码
  1. 修改 CreditCardDetail.vue

Day 7 持续集成

前端编译环境和 Heroku, Amplify, 自建 Oracle VPS 的部署

部署到 Heroku

  1. 创建 stagingproduction 两个独立的 APP
    与服务端部署不一样,两个环境都是在 Webpack 打包时注入了环境变量,两个环境都需要走 CI 的编译打包流程,所以通过 PipelinePromote 是没有意义的。我们这里创建两个独立的 APP:[staging-ft, production-ft]
  2. 为每个 APP 添加编辑插件, 因为是静态部署,比 Node Server 多了一个插件
heroku buildpacks:add heroku/nodejs
heroku buildpacks:add https://github.com/heroku/heroku-buildpack-static
复制代码

并且要添加 static.json 文件在项目的根目录下

{
  "root": "dist",
  "clean_urls": true,
  "routes": {
    "/**": "index.html"
  }
}
复制代码
  1. 在两个 APP 的设置中绑定关联的 Github 仓库和分支

这里的 VUE_APP_URL 是在 编译 时候,覆盖 axios 默认的 BASE_URL,指向对应的 Node Server,不同的分支也可以有不同的 Value. 与 Amplify 不同, 这里要设置 NODE_ENV=production,设置了这个后 npm ci 不会影响 install devDependencies 下的模块

通过 Amplify 部署

创建 amplify.yaml 文件,修改 build 的脚本
version: 0.2
backend:
  phases:
    build:
      commands:
        - '# Execute Amplify CLI with the helper script'
        - amplifyPush --simple
frontend:
  phases:
    preBuild:
      commands:
        - npm ci
    build:
      commands:
        - nvm use $VERSION_NODE_10
        - node -v
        - if [ "${AWS_BRANCH}" = "aws-staging" ]; then echo "staging branch" && npm run build:staging; elif [ "${AWS_BRANCH}" = "aws-production" ]; then echo "production branch" && npm run build:production; else echo "test branch" && npm run build:mytest; fi
  artifacts:
    baseDirectory: dist
    files:
      - '**/*'
  cache:
    paths:
      - node_modules/**/*
复制代码

当推送到对应的分支后,Amplify 会调用这个脚本执行编译,打包和部署

设置环境相关的变量

amplify-env
这里的 VUE_APP_URL 是在 编译 时候,覆盖 axios 默认的 BASE_URL,指向对应的 Node Server,不同的分支也可以有不同的 Value

  • 注意不要添加 NODE_ENV=production,设置了这个后 npm ci 不会 install devDependencies 下的模块,会导致 npm run build 报错无法找到 vue-cli-service
  • VueWebpack 会根据 --mode [staging | production ] 找到对应的 .env.\* 文件, 在这些中再声明 NODE_ENV=production
创建 Amplify 角色

创建 Amplify APP 时,好像没有自动创建关联的 Role, 我手动创建了一个
aws-role-amplify

部署到 Oracle CentOS 8 服务器中的 Nginx

  1. 配置 Nginx 支持单个端口对应多个二级域名的静态服务
  • 编辑 /etc/nginx/nginx.conf 支持同一个端口,不同的静态服务器
server {
    listen  80;
    server_name     test.magicefire.com;
    root            /usr/share/nginx/html/test;
    location / {}
}

server {
    listen  80;
    server_name     staging.magicefire.com;
    root            /usr/share/nginx/html/staging;
    location / {}
}

server {
    listen  80;
    server_name     production.magicefire.com;
    root            /usr/share/nginx/html/production;
    location / {}
}
复制代码

建立对应的目录,在目录下放测试 html

  • 修改 Cloudflare,添加三条 A 记录,支持 VPSIP

cloudflare-static

  • 通过 Let's Encrypt 修改 nginxhttps 支持

安装 certbotNode Server 的部署

certbot -nginx
复制代码

certbot-static

  1. 在 .github/workflows 下添加 Github Actions, 编写 Github Actions 部署脚本

注意不要添加 NODE_ENV=production,设置了这个后 npm ci 不会 install devDependencies 下的模块,会导致 npm run build 报错无法找到 vue-cli-service
Vue 的 Webpack 会根据 --mode [staging | production ] 找到对应的 .env.* 文件, 在这些中再声明 NODE_ENV=production

  1. 在 Github 的仓库设置中,给 Actions 用到的添加加密的 Secrets

github-secrets
DEPLOY_ORACLE=/usr/share/nginx/html

后端运行环境和 Heroku/EC2 部署

运行时根据环境加载配置项

  1. 在 package.json 的脚本中通过 NODE_ENV 定义环境 [development, test, staging, production]
  2. 安装 dotenv 模块, 该模块通过 NODE_ENV 读取工程根目录下对应的 .env.[${NODE_ENV}] 文件, 加载该环境下的环境变量
  3. 按 topic[cognito, postgres, server, etc] 定义不同的配置, 并把 process.env.IS_ONLINE_PAYMENT 之类字符串形式的转成不同的变量类型
  4. 可以把环境变量配置在三个不同的level
  • 运行机器的 SHELL 环境变量中, 主要是定义环境的 NODE_ENV 和其他敏感的密码和密钥
  • 在以 .${NODE_ENV} 结尾的 .env 文件中
  • 最后汇总到 src/config 目录下按 topic 区分的配置信息
  • 代码的其他地方读取的都是 src/config 下的配置信息
  1. 在后端环境下设置 NODE_ENV 有一个副作用,在 Typescript 编译打包前

** 如果 NODE_ENV 设置为 production, npm ci 不会安装 devDependencies 中的依赖包,如果在运行的 EC2 上编译打包,编译会报错。所以打包编译我放在了Github Actions 的容器中了,所以避免了这个问题 **
We have installed all our packages using the --save-dev flag. In production, if we use npm install --production these packages will be skipped.

Amplify 创建线上用户库

  1. 在前端,通过 Amplify 添加线上环境
amplify env add prod
复制代码
  1. 检查状态, 并部署到 Amplify 后台
amplify status
复制代码

amplify-env-add-prod

amplify push
复制代码

进入 AWS Cognito 后台, 会增加一个线上的 UserPool
aws-cognito-user-pool

  1. 添加 foodtruck2826b8ad2826b8adCreateAuthChallenge-prod 的短信权限

  2. 修改线上配置文件中 AWS_COGNITO_USER_POOL_ID

// .env.oracle
// .env.staging
// .env.production
AWS_COGNITO_USER_POOL_ID=ap-northeast-1_2ae9TN9FH
复制代码

主要区分三个模块在不同环境的部署

  • Postgres 数据库
  • 支付
  • 用户鉴权

修改 HTTPS 的证书加载

Heroku 部署

Heroku 是我们介绍的三种 CI/CD 流程中最简单的方式

  1. 创建一条 Pipeline, 在 Pipeline 下创建 staging 和 production 两个应用

heroku-pipeline
2. 在 APP 的设置里关联 Github 上对应的仓库和分支
heroku-github

  • APP staging 选择 heroku-staging 分支
  • APP production 选择 heroku-production 分支
  1. 为每个 APP 添加 heroku/nodejs 编译插件
heroku login -i
heroku buildpacks:add heroku/nodejs -a staging-api-food-truck
复制代码
  1. 设置运行时的环境变量

heroku-vars-buildpack
这里通过 SERVER 这个运行时的环境变量,告诉 index.ts 不要加载 https 的服务器, 而是用 http 的服务器。
** Heroku 的 API 网关自己已支持 https,后端起的 node server 在内网里是 http, 所以要修改代码 换成 http server,否者会报 503 错误**
5. 修改 index.ts 文件,在 Heroku 下改成 HTTP
6. APP production 一般不需要走 CI/CD 的流程,只要设置 NODE_ENV=production,然后在 APP staging 验证通过后, promote 就可以完成快速部署。
7. 查看 heroku 上的日志

heroku logs --tail -a staging-api-food-truck
复制代码

AWS EC2 部署

aws-cicd

在 AWS 上搭建环境和创建用户和角色
CodeDeploy

We'll be using CodeDeploy for this setup so we have to create a CodeDeploy application for our project and two deployment groups for the application. One for staging and the other for production.

  1. To create the api-server CodeDeploy application using AWS CLI, we run this on our terminal:
aws deploy create-application \
--application-name api-server \
--compute-platform Server
复制代码
  1. Before we run the cli command to create the service role, we need to create a file with IAM specifications for the role, copy the content below into it and name it code-deploy-trust.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "Service": [
          "codedeploy.amazonaws.com"
        ]
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
复制代码
  1. We can now create the role by running:
aws iam create-role \
--role-name CodeDeployServiceRole \
--assume-role-policy-document file://code-deploy-trust.json
复制代码
  1. After the role is created we attach the AWSCodeDeployRole policy to the role
aws iam attach-role-policy \
--role-name CodeDeployServiceRole \
--policy-arn arn:aws:iam::aws:policy/service-role/AWSCodeDeployRole
复制代码
  1. To create a deployment group we would be needing the service role ARN.
aws iam get-role \
--role-name CodeDeployServiceRole \
--query "Role.Arn" \
--output text
复制代码

The ARN should look something like arn:aws:iam::403593870368:role/CodeDeployServiceRole
6. Let's go on to create a deployment group for the staging and production environments.

aws deploy create-deployment-group \
--application-name api-server \
--deployment-group-name staging \
--service-role-arn arn:aws:iam::403593870368:role/CodeDeployServiceRole \
--ec2-tag-filters Key=Name,Value=staging,Type=KEY_AND_VALUE
复制代码
aws deploy create-deployment-group \
--application-name api-server \
--deployment-group-name production \
--service-role-arn arn:aws:iam::403593870368:role/CodeDeployServiceRole \
--ec2-tag-filters Key=Name,Value=production,Type=KEY_AND_VALUE
复制代码

进入 Console -> Code Deploy 确认
aws-code-deploy-api-server
aws-code-deploy-api-server-staging

创建 S3 Bucket

创建一个名为 node-koa2-typescript 的 S3 Bucket

aws s3api create-bucket --bucket node-koa2-typescript --region ap-northeast-1
复制代码

aws-s3-node-koa2-typescript

Create and Launch EC2 instance

完整的演示,应该创建 staging 和 production 两个 EC2 实例,为了节省资源,这里只创建一个实例

  1. 创建一个具有访问 S3 权限的角色 EC2RoleFetchS3

aws-role-s3
2. In this article, we will be selecting the Amazon Linux 2 AMI (HVM), SSD Volume Type.
aws-ec2-amazon-linux-2-ami-ssd

  • 绑定上面创建的角色,并确认开启80/22/3001/3002几个端口

aws-ec2-role-inbound

  • 添加 tag,key 是 Name,Value 是 production

aws-ec2-tag

  • 导入用于 ssh 远程登入的公钥

aws-ec2-public-key

  • 通过 ssh 远程登入 EC2 实例,安装 CodeDeploy Agent

安装步骤详见 CodeDeploy Agent
3. 通过 ssh 安装 Node.js 的运行环境

  • 通过 NVM 安装 Node.js
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash
source ~/.bash_profile
nvm list-remote
nvm install 14
复制代码
  • 安装 PM2 管理 node 的进程
npm i pm2 -g

复制代码
  • 在项目的根目录下创建 ecosystem.config.js 文件
module.exports = {
  apps: [
    {
      script: './dist/index.js',
    },
  ],
}
复制代码
  1. 在 EC2 的实例 ec2-user 账号下设置环境变量, 编辑 ~/.bash_profile
export NODE_ENV=production
export SERVER=AWS
复制代码

这里放置 NODE_ENV 和具有敏感信息的环境变量, 这里 SERVER=AWS 只是演示

创建用于 Github Actions 部署脚本的用户组和权限
  1. 在 IAM 中创建以一个 CodeDeployGroup 用户组,并赋予 AmazonS3FullAccess and AWSCodeDeployFullAccess 权限
  2. 在 CodeDeployGroup 添加一个 dev 用户,记录下 Access key ID 和 Secret access key

aws-iam-dev

编写 Github Actions 脚本
  1. 在工程的根目录下创建 .github/workflows/deploy-ec2.yaml 文件

deploy-ec2.yaml 的作用是,当修改的代码提交到 aws-staging 或 aws-production,触发编译,打包,并上传到 S3 的 node-koa2-typescript bucket, 然后再触发 CodeDeploy 完成后续的部署。所以这个 Github Action 是属于 CI 的角色,后面的 CodeDeploy 是 CD 的角色。
2. 在 Github 该项目的设置中添加 Environment secrets, 将刚才 dev 用户的 Access key ID 和 Secret access key 添加进Environment secrets
github-environment-secrets

添加 appspec.yml 及相关脚本

CodeDeploy 从 S3 node-koa2-typescript bucket 中获取最新的打包产物后,上传到 EC2 实例,解压到对应的目录下,这里我们指定的是 /home/ec2-user/api-server。CodeDeploy Agent 会找到该目录下的 appspec.yml 文件执行不同阶段的 Hook 脚本

version: 0.0
os: linux
files:
  - source: .
    destination: /home/ec2-user/api-server
hooks:
  AfterInstall:
    - location: aws-ec2-deploy-scripts/after-install.sh
      timeout: 300
      runas: ec2-user
  ApplicationStart:
    - location: aws-ec2-deploy-scripts/application-start.sh
      timeout: 300
      runas: ec2-user
复制代码

aws-ec2-deploy-scripts/application-start.sh 启动了 Node.js 的服务

#!/usr/bin/env bash
source /home/ec2-user/.bash_profile
cd /home/ec2-user/api-server/
pm2 delete $NODE_ENV
pm2 start ecosystem.config.js --name $NODE_ENV
复制代码
在 EC2 实例下安装免费的域名证书,步骤详见Certificate automation: Let's Encrypt with Certbot on Amazon Linux 2
  1. 去 Cloudflare 添加 A 记录指向这台 EC2 实例,指定二级域名是 aws-api

cloudflare-aws-api
2. 安装配置 Apache 服务器,用于证书认证
3. Install and run Certbot

sudo certbot -d aws-api.magicefire.com
复制代码

根据提示操作,最后证书生成在 /etc/letsencrypt/live/aws-api.magicefire.com/ 目录下
4. 因为我们启动 Node 服务的账号是 ec2-user, 而证书是 root 的权限创建的,所以去 /etc/letsencrypt 给 live 和 archive 两个目录添加其他用户的读取权限

sudo -i
cd /etc/letsencrypt
chmod -R 755 live/
chmod -R 755 archive/
复制代码
  1. Configure automated certificate renewal
部署和验证
  1. 如果没有 aws-production 分支,先创建该分支,并切换到该分支下,合并修改的代码,推送到Github
git checkout -b aws-production
git merge main
git push orign 
复制代码
  1. 触发 Github Actions

github-actions-build-node-api-server

  1. 编译,打包并上传到 S3 后,触发 CodeDeploy

aws-code-deploy-ec2

  1. 完成后在浏览器里检查

chrome-check-api-server
或用 curl 在命令行下确认

curl https://aws-api.magicefire.com:3002/api/v1/health
复制代码

curl-check-api-server

  1. 因为我们没有创建 EC2 的 staging 实例,如果推送到 aws-staging 分支,CodeDeploy 会提示以下错误

aws-code-deploy-failed

过程回顾

aws-code-deploy-cicd

Postgres VPS 和 Heroku 部署[Server]

在 Heroku 上部署 Postgres

  1. Provisioning Heroku Postgres
heroku addons
heroku addons:create heroku-postgresql:hobby-dev
复制代码
  1. Sharing Heroku Postgres between applications
heroku addons:attach my-originating-app::DATABASE --app staging-api-node-server
复制代码
  1. 导入商品到线上 table
npm run import-goods-heroku-postgre
复制代码

在 VPS 上部署 Postgres

编外

支持 Swagger