https://www.yuque.com/books/share/6eb0a508-d745-4e75-8631-8eb127b7b7ca?# 《Egg.js 教程》
快速入门
Egg.js 介绍
快速入门
目录结构
内置对象
Egg.js 综合案例
介绍
- 模仿实现 YouTube Clone 项目
- 采用前后端分离架构
- 先做服务端接口,然后做客户端应用
- 后端技术选型
- Web 框架:Egg.js
- 数据库:MongoDB
- ORM 框架:mongoose
- 身份认证:JWT
- 客户端选型
- Vue.js 3 系列技术栈
使用 Yapi 管理接口
YApi 是高效、易用、功能强大的 api 管理平台,旨在为开发、产品、测试人员提供更优雅的接口管理服务。可以帮助开发者轻松创建、发布、维护 API,YApi 还为用户提供了优秀的交互体验,开发人员只需利用平台提供的接口数据写入工具以及简单的点击操作就可以实现接口的管理。
- GitHub 仓库:https://github.com/YMFE/yapi
- 体验地址:https://yapi.baidu.com/
- 文档:https://hellosean1025.github.io/yapi
特性:
- 基于 Json5 和 Mockjs 定义接口返回数据的结构和文档,效率提升多倍
- 扁平化权限设计,即保证了大型企业级项目的管理,又保证了易用性
- 类似 postman 的接口调试
- 自动化测试, 支持对 Response 断言
- MockServer 除支持普通的随机 mock 外,还增加了 Mock 期望功能,根据设置的请求过滤规则,返回期望数据
- 支持 postman, har, swagger 数据导入
- 免费开源,内网部署,信息再也不怕泄露了
内网部署:
项目初始化
创建项目
npm i create-egg
create-egg youtube-clone-eggjs
cd youtube-clone-eggjs
npm install
npm run dev
目录结构
egg-project
├── package.json
├── app.js (可选)
├── agent.js (可选)
├── app
| ├── router.js
│ ├── controller
│ | └── home.js
│ ├── service (可选)
│ | └── user.js
│ ├── middleware (可选)
│ | └── response_time.js
│ ├── schedule (可选)
│ | └── my_task.js
│ ├── public (可选)
│ | └── reset.css
│ ├── view (可选)
│ | └── home.tpl
│ └── extend (可选)
│ ├── helper.js (可选)
│ ├── request.js (可选)
│ ├── response.js (可选)
│ ├── context.js (可选)
│ ├── application.js (可选)
│ └── agent.js (可选)
├── config
1. List item
| ├── plugin.js
| ├── config.default.js
│ ├── config.prod.js
| ├── config.test.js (可选)
| ├── config.local.js (可选)
| └── config.unittest.js (可选)
└── test
├── middleware
| └── response_time.test.js
└── controller
└── home.test.js
配置 ESLint
- Egg.js simple 模板集成了 ESLint 配置
- 默认使用的校验规则是 Egg 自己定制的 eslint-config-egg
- 也可以根据需要自定义 ESLint 的校验规则,比如 eslint-config-standard
- 建议给 git commit 增加代码校验的 hook,更有利于代码规范的把控
配置 ESLint + Standard
nox eslint --init
在 vscode 中配置 ESLint
配置 git commit hook
在提交代码之前运行时,linting 更有意义。这样,您可以确保没有错误进入存储库并强制执行代码规范。但是,在整个项目上运行 lint 过程的速度很慢,lint 的结果可能无关紧要。最终,您只希望处理将提交的文件。
初始化 mongoose 配置
https://github.com/eggjs/egg-mongoose
注册-登录
用户注册
表单验证
- egg-validate 验证插件:https://github.com/eggjs/egg-validate
- parameter 验证规则文档:https://github.com/node-modules/parameter
配置异统一异常处理
// app/middleware/error_handler.js
module.exports = () => {
return async function errorHandler(ctx, next) {
try {
await next();
} catch (err) {
// 所有的异常都在 app 上触发一个 error 事件,框架会记录一条错误日志
ctx.app.emit('error', err, ctx);
const status = err.status || 500;
// 生产环境时 500 错误的详细错误内容不返回给客户端,因为可能包含敏感信息
const error = status === 500 && ctx.app.config.env === 'prod'
? 'Internal Server Error'
: err.message;
// 从 error 对象上读出各个属性,设置到响应中
ctx.body = { error };
if (status === 422) {
ctx.body.detail = err.errors;
}
ctx.status = status;
}
};
};
JWT 身份认证
// 创建 token
createToken (data) {
const token = jwt.sign(data, this.app.config.jwt.secret, {
expiresIn: this.app.config.jwt.expiresIn
})
return token
}
// 校验 token
verifyToken (token) {
return jwt.verify(token, this.app.config.jwt.secret)
}
中间件
app/middleware/auth.js
module.exports = (options = { required: true }) => {
return async (ctx, next) => {
// 1. 获取请求头中的 token 数据
let token = ctx.headers.authorization
console.log(token)
token = token
? token.split('Bearer ')[1] // Bearer空格token数据
: null
if (token) {
try {
// 3. token 有效,根据 userId 获取用户数据挂载到 ctx 对象中给后续中间件使用
const data = ctx.service.user.verifyToken(token)
ctx.user = await ctx.model.User.findById(data.userId)
} catch (err) {
ctx.throw(401)
}
} else if (options.required) {
ctx.throw(401)
}
// 4. next 执行后续中间件
await next()
}
}
app/router.js
module.exports = app => {
const { router, controller } = app
const auth = app.middleware.auth()
router.prefix('/api/v1') // 设置基础路径
router.get('/', controller.home.index)
router
.get('/user', auth, controller.user.getCurrentUser) // 获取当前登录用户
}
总结:
- 整体应用时定义在 /app/middleware
- 在 /config/config.default.js 中添加
config.middleware = ['errorHandler']
扩展
/app/extend
扩展 Egg.js 应用实例 Application
/app/extend/application.js
/**
* 扩展 Egg.js 应用实例 Application
*/
const RPCClient = require('@alicloud/pop-core').RPCClient
function initVodClient (accessKeyId, accessKeySecret) {
const regionId = 'cn-shanghai' // 点播服务接入区域
const client = new RPCClient({
accessKeyId: accessKeyId,
accessKeySecret: accessKeySecret,
endpoint: 'http://vod.' + regionId + '.aliyuncs.com',
apiVersion: '2017-03-21'
})
return client
}
let vodClient = null
module.exports = {
get vodClient () {
if (!vodClient) {
const { accessKeyId, accessKeySecret } = this.config.vod
vodClient = initVodClient(accessKeyId, accessKeySecret)
}
return vodClient
}
}
工具方法
/app/extend/helper.js
const crypto = require('crypto')
const _ = require('lodash')
exports.md5 = str => {
return crypto.createHash('md5').update(str).digest('hex')
}
exports._ = _
service 扩展
/app/service
/app/service/user.js
const Serive = require('egg').Service
class UserService extends Serive {
get User () {
return this.app.model.User
}
findByUsername (username) {
return this.User.findOne({
username
})
}
}
module.exports = UserService
发布上线
服务器环境配置
- Node.js
- MongoDB
- nginx
这里以 Ubuntu 20.04 为例。
在进行下面的操作之前先更新软件包:
apt update
apt upgrade
建议将 Ubuntu 镜像源切换为国内镜像地址,比如清华大学 Ubuntu 镜像源。
安装 Node
建议使用 nvm 安装和管理 Node 服务。
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.37.2/install.sh | bash
修改 nvm 安装 node 的镜像源:
# .bashrc
export NVM_NODEJS_ORG_MIRROR=http://npm.taobao.org/mirrors/node
安装 Node:
nvm install 14.15.4
nvm alias default 14.15.4
node --version
把 npm 安装地址修改为淘宝镜像源:
npm i -g nrm --registry=https://registry.npm.taobao.org
nrm ls
nrm use taobao
安装 MongoDB
建议参考 MongoDB 官方推荐的安装方式。
安装 MongoDB:
wget -qO - https://www.mongodb.org/static/pgp/server-4.4.asc | sudo apt-key add -
sudo apt-get install gnupg
wget -qO - https://www.mongodb.org/static/pgp/server-4.4.asc | sudo apt-key add -
echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu focal/mongodb-org/4.4 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-4.4.list
sudo apt-get update
sudo apt-get install -y mongodb-org
管理 MongoDB:
# 启动 MongoDB
sudo systemctl start mongod
# 查看 MongoDB 启动状态
sudo systemctl status mongod
# 将 MongoDB 设置为开机启动
sudo systemctl enable mongod
# 停止 MongoDB
sudo systemctl stop mongod
# 重启 MongoDB
sudo systemctl restart mongod
# 使用自带的命令行客户端连接 MongoDB
mongo
安装 nginx
例如 Ubuntu 20.04 的安装方式建议参考:https://www.digitalocean.com/community/tutorials/how-to-install-nginx-on-ubuntu-20-04。
sudo apt install nginx
管理 nginx 服务:
# 启动 nginx
sudo systemctl start nginx
# 查看 nginx 运行状态
systemctl status nginx
# 停止 nginx
sudo systemctl stop nginx
# 重启 nginx
sudo systemctl restart nginx
# 热重启:如果仅更改配置,Nginx通常可以重新加载而不断开连接
sudo systemctl reload nginx
# 添加开机启动 nginx
sudo systemctl enable nginx
# 禁止开机启动 nginx
sudo systemctl disable nginx
安装 Git
apt install git
git --version
手动部署
关于 Egg.js 应用部署:https://eggjs.org/zh-cn/core/deployment.html。
1、将代码提交到 GitHub 远程仓库
2、在远程服务器下载 GitHub 远程仓库
3、启动运行
cd 项目目录
npm i --production
npm start
启动成功之后可以通过 ip 地址访问测试一下:http://服务器ip地址:7001。
注意:云服务器防火墙需要开发 7001 端口的访问权限。
如果需要通过域名访问:
1、添加域名解析记录
2、配置 nginx 代理
# /etc/nginx/conf.d/youtubeclone
server {
# 监听端口
listen 80;
# 域名可以有多个,用空格隔开
# server_name www.w3cschool.cn w3cschool.cn;
server_name test.lipengzhou.com;
# 对 / 启用反向代理
location / {
proxy_set_header X-Real-IP $remote_addr;
# 后端的Web服务器可以通过 X-Forwarded-For 获取用户真实 IP
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 获取真实的请求主机名
proxy_set_header Host $http_host;
# 标识该请求由 nginx 转发
proxy_set_header X-Nginx-Proxy true;
# 代理到本地的 3000 端口服务
proxy_pass http://127.0.0.1:7001;
}
}
3、重启 nginx 服务
4、测试通过域名访问
如果需要更新:
- 本地开发
- 提交更新到远程仓库
- 在远程服务器拉取变更,重新启动服务
自动部署
1、如果是私有 Git 仓库需要配置服务器通过 SSH 连接 github 的权限
# 生成 SSH key
ssh-keygen -o
# 查看并复制公钥
cat ~/.ssh/id_rsa.pub
2、配置 GitHub 仓库 secrets
- USERNAME 服务器用户
- PASSWORD 服务器用户密码
- HOST 服务器主机地址
- PORT 服务器主机端口号
- ACCESSKEYID 阿里云视频点播服务 access id
- ACCESSKEYSECRET 阿里云视频点播服务 access key secret
3、编写 GitHub Actions 脚本
# .github/workflows/nodejs.yml
name: Node.js CI
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
schedule:
- cron: '0 2 * * *'
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
node-version: [10]
os: [ubuntu-latest]
steps:
- name: deploy
uses: appleboy/ssh-action@master
env:
ACCESSKEYID: ${{ secrets.ACCESSKEYID }}
ACCESSKEYSECRET: ${{ secrets.ACCESSKEYSECRET }}
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
# key: ${{ secrets.KEY }}
password: ${{ secrets.PASSWORD }}
port: ${{ secrets.PORT }}
envs: ACCESSKEYID,ACCESSKEYSECRET
script: |
export ACCESSKEYID=$ACCESSKEYID
export ACCESSKEYSECRET=$ACCESSKEYSECRET
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
cd /root/youtubeclone-backend
git pull origin master
npm install --production
npm run stop
npm run start
4、将源码提交到 GitHub 远程仓库
5、等待执行自动部署
6、查看部署结果
配置 HTTPS
确保服务器防火墙允许访问 443
1、申请证书
2、在 nginx 服务器上安装证书
#以下属性中,以ssl开头的属性表示与证书配置有关。
server {
listen 443 ssl;
#配置HTTPS的默认访问端口为443。
#如果未在此处配置HTTPS的默认访问端口,可能会造成Nginx无法启动。
#如果您使用Nginx 1.15.0及以上版本,请使用listen 443 ssl代替listen 443和ssl on。
server_name youtubeclone.lipengzhou.com; #需要将yourdomain.com替换成证书绑定的域名。
ssl_certificate cert/5166245_youtubeclone.lipengzhou.com.pem; #需要将cert-file-name.pem替换成已上传的证书文件的名称。
ssl_certificate_key cert/5166245_youtubeclone.lipengzhou.com.key; #需要将cert-file-name.key替换成已上传的证书密钥文件的名称。
ssl_session_timeout 5m;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
#表示使用的加密套件的类型。
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; #表示使用的TLS协议的类型。
ssl_prefer_server_ciphers on;
location / {
proxy_set_header X-Real-IP $remote_addr;
# 后端的Web服务器可以通过 X-Forwarded-For 获取用户真实 IP
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 获取真实的请求主机名
proxy_set_header Host $http_host;
# 标识该请求由 nginx 转发
proxy_set_header X-Nginx-Proxy true;
# 代理到本地的 3000 端口服务
proxy_pass http://127.0.0.1:7001;
}
}
server {
listen 80;
server_name youtubeclone.lipengzhou.com; #需要将yourdomain.com替换成证书绑定的域名。
rewrite ^(.*)$ https://$host$1; #将所有HTTP请求通过rewrite指令重定向到HTTPS。
}
客户端案例
客户端案例介绍
- 模仿实现 YouTube Clone 客户端应用
- 技术栈
- Vue.js 3
- Vue Router
- Vuex
- TypeScript
- axios
项目初始化
使用 Vue CLI 创建项目
vue create realworld-vue3
Vue CLI v4.5.11
? Please pick a preset: Manually select features
? Check the features needed for your project: Choose Vue version, Babel, TS, Router, Vuex, Linter
? Choose a version of Vue.js that you want to start the project with 3.x (Preview)
? Use class-style component syntax? No
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? Yes
? Use history mode for router? (Requires proper server setup for index fallback in production) No
? Pick a linter / formatter config: Standard
? Pick additional lint features: Lint on save, Lint and fix on commit
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? No
cd realworld-vue3
npm run serve
封装请求接口
/src/utils/request.ts
import axios from 'axios'
import { store } from '@/store'
export const request = axios.create({
baseURL: process.env.VUE_APP_API_BASE_URL
})
// 请求拦截器
request.interceptors.request.use(config => {
const { user } = store.state
if (user) {
config.headers.Authorization = `Bearer ${user.token}`
}
return config
}, err => {
return Promise.reject(err)
})
// 响应拦截器
校验页面访问权限
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
import AppLayout from '@/layout/AppLayout.vue'
import { store } from '@/store'
// 路由规则表
const routes: Array<RouteRecordRaw> = [
{
path: '/',
component: AppLayout,
children: [
{
path: '', // 默认子路由
name: 'home',
component: () => import(/* webpackChunkName: "home" */ '@/views/home/index.vue')
},
{
path: 'profile',
name: 'profile',
component: () => import(/* webpackChunkName: "profile" */ '@/views/profile/index.vue'),
meta: { requiresAuth: true }
},
{
path: 'watch/:videoId',
name: 'watch',
component: () => import(/* webpackChunkName: "video" */ '@/views/watch/index.vue')
}
]
},
{
path: '/login',
name: 'login',
component: () => import(/* webpackChunkName: "login" */ '@/views/login/index.vue')
}
]
// 创建路由实例
const router = createRouter({
history: createWebHashHistory(),
routes
})
router.beforeEach((to, from, next) => {
const { user } = store.state
if (to.matched.some(record => record.meta.requiresAuth)) {
// this route requires auth, check if logged in
// if not, redirect to login page.
if (!user) {
next({
path: '/login',
query: { redirect: to.fullPath }
})
} else {
next()
}
} else {
next() // 确保一定要调用 next()
}
})
export default router
store
src/store/index.js
import { User } from '@/api/user'
import { InjectionKey } from 'vue'
import { createStore, Store, useStore as baseUseStore } from 'vuex'
// 声明 State 类型
export interface State {
count: number
user: User | null
}
// define injection key
export const key: InjectionKey<Store<State>> = Symbol()
export const store = createStore<State>({
state: {
count: 123,
user: JSON.parse(window.localStorage.getItem('user') || 'null')
},
mutations: {
setUser (state, user: User) {
state.user = user
window.localStorage.setItem('user', JSON.stringify(state.user))
}
}
})
// define your own `useStore` composition function
export function useStore () {
return baseUseStore(key)
}
登录页示例
<template>
<div class="gspRov">
<h2>Login to your account</h2>
<form @submit.prevent="handleSubmit">
<ul v-if="errors" class="errors">
<li v-for="(error, index) in errors" :key="index">
{{ `${error.field} ${error.message}` }}
</li>
</ul>
<input v-model="user.email" type="email" placeholder="email" />
<input v-model="user.password" type="password" placeholder="password" />
<div class="action input-group">
<span class="pointer">Signup instead</span>
<button :disabled="isLoading">Login</button>
</div>
</form>
</div>
</template>
<script lang="ts">
import { login } from '@/api/user'
import { defineComponent, reactive, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useStore } from '@/store'
const useLogin = () => {
const router = useRouter()
const store = useStore()
const route = useRoute()
const user = reactive({
email: 'lpzmail@163.com',
password: '123456'
})
const errors = ref([])
const isLoading = ref(false)
const handleSubmit = async () => {
isLoading.value = true
errors.value = []
try {
const { data } = await login(user)
store.commit('setUser', data.user)
const redirect = (route.query.redirect || '/') as string
router.push(redirect)
} catch (err) {
if (err.response.status === 422) {
errors.value = err.response.data.detail
}
}
isLoading.value = false
}
return {
user,
handleSubmit,
errors,
isLoading
}
}
export default defineComponent({
name: 'LoginIndex',
setup () {
return {
...useLogin()
}
}
})
</script>