Egg.js -- 及案列介绍

12 篇文章 1 订阅

https://www.yuque.com/books/share/6eb0a508-d745-4e75-8631-8eb127b7b7ca?# 《Egg.js 教程》

快速入门

Egg.js 介绍

快速入门

目录结构

内置对象

Egg.js 综合案例

介绍

使用 Yapi 管理接口

YApi 是高效、易用、功能强大的 api 管理平台,旨在为开发、产品、测试人员提供更优雅的接口管理服务。可以帮助开发者轻松创建、发布、维护 API,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

注册-登录

用户注册
表单验证

配置异统一异常处理

// 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

将 SSH 公钥添加到 GitHub 账户中。

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}

客户端案例

客户端案例介绍

项目初始化

使用 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>

资源链接

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值