egg

538 阅读8分钟

简介

Egg.js 为企业级框架和应用而生,我们希望由 Egg.js 孕育出更多上层框架,帮助开发团队和开发人员降低开发和维护成本。Egg 继承于 Koa。

设计原则:一个插件制作一件事情,奉行『约定优于配置』

特性:

  • 提供基于 Egg 定制上层框架的能力
  • 高度可扩展的插件机制
  • 内置多进程管理
  • 基于 Koa 开发,性能优异
  • 框架稳定,测试覆盖率高
  • 渐进式开发

下包

$ mkdir egg-example && cd egg-example  新建一个文件夹
$ npm init egg --type=simple    搭建egg项目
$ npm i     下载项目需要的所有的包

启动项目:
$ npm run dev || yarn dev
$ open http://localhost:7001

简单配置、让后台先起来

proxy

想要前后端练习起来的话,需要配置 proxy

react

react 的项目在 package.jsonwen 文件里面配置

    proxy:'http://127.0.0.1:7001';

vue

vue 项目的话在 vue.config.js 文件里面配置

    module.exports = {
        devServer: {
            proxy: "http://127.0.0.1:7001"
        }
    }

安全协议

CSRF(Cross-site request forgery 跨站请求伪造,也被称为 One Click Attack 或者 Session Riding,通常缩写为 CSRF 或者 XSRF,是一种对网站的恶意利用。 CSRF 攻击会对网站发起恶意伪造的请求,严重影响网站的安全。因此框架内置了 CSRF 防范方案。


    // config.default.js 文件
    config.security = {
        // 安全协议  关闭
        csrf: false,
    };

mysql 库

安装对应的插件 egg-mysql

npm i --save egg-mysql

开启插件

    // config/plugin.js
        exports.mysql = {
            enable: true,
            package: 'egg-mysql',
        };

配置 mysql

    // config.default.js 文件
    // 配置mysql
    config.mysql = {
        // 单数据库信息配置
    client: {
      host: 'localhost',  // host
      port: '3306',  // 端口号
      user: 'root',  // 用户名
      password: '1234',  // 密码
      database: 'abei',  // 数据库名
    },
    // 是否加载到 app 上,默认开启
    app: true,
    // 是否加载到 agent 上,默认关闭
    agent: false,
  };

    使用方式:
    // 单实例可以直接通过 app.mysql 访问
    await app.mysql.query(sql, values);

Cookie 与 Session

Cookie

HTTP 请求都是无状态的,但是我们的 Web 应用通常都需要知道发起请求的人是谁。为了解决这个问题,HTTP 协议设计了一个特殊的请求头:Cookie。服务端可以通过响应头(set-cookie)将少量数据响应给客户端,浏览器会遵循协议将数据保存,并在下次请求同一个服务的时候带上(浏览器也会遵循协议,只在访问符合 Cookie 指定规则的网站时带上对应的 Cookie 来保证安全性)。

设置 Cookie 其实是通过在 HTTP 响应中设置 set-cookie 头完成的,每一个 set-cookie 都会让浏览器在 Cookie 中存一个键值对。在设置 Cookie 值的同时,协议还支持许多参数来配置这个 Cookie 的传输、存储和权限。

参数:

  • {Number} maxAge: 设置这个键值对在浏览器的最长保存时间。是一个从服务器当前时刻开始的毫秒数。
  • {Date} expires: 设置这个键值对的失效时间,如果设置了 maxAge,expires 将会被覆盖。如果 maxAge 和 expires 都没设置,Cookie 将会在浏览器的会话失效(一般是关闭浏览器时)的时候失效。
  • {String} path: 设置键值对生效的 URL 路径,默认设置在根路径上(/),也就是当前域名下的所有 URL 都可以访问这个 Cookie。
  • {String} domain: 设置键值对生效的域名,默认没有配置,可以配置成只在指定域名才能访问。
  • {Boolean} httpOnly: 设置键值对是否可以被 js 访问,默认为 true,不允许被 js 访问。
  • {Boolean} secure: 设置键值对只在 HTTPS 连接上传输,框架会帮我们判断当前是否在 HTTPS 连接上自动设置 secure 的值。

除了这些属性之外,框架另外扩展了 3 个参数的支持:

  • {Boolean} overwrite:设置 key 相同的键值对如何处理,如果设置为 true,则后设置的值会覆盖前面设置的,否则将会发送两个 set-cookie 响应头。
  • {Boolean} signed:设置是否对 Cookie 进行签名,如果设置为 true,则设置键值对的时候会同时对这个键值对的值进行签名,后面取的时候做校验,可以防止前端对这个值进行篡改。默认为 true。
  • {Boolean} encrypt:设置是否对 Cookie 进行加密,如果设置为 true,则在发送 Cookie 前会对这个键值对的值进行加密,客户端无法读取到 Cookie 的明文值。默认为 false。
    // 如果想要 Cookie 在浏览器端可以被 js 访问并修改:
    ctx.cookies.set(key, value, {
    httpOnly: false,
    signed: false,
    });
    // 如果想要 Cookie 在浏览器端不能被修改,不能看到明文:
    ctx.cookies.set(key, value, {
    httpOnly: true, // 默认就是 true
    encrypt: true, // 加密传输
    });

Session

Cookie 在 Web 应用中经常承担标识请求方身份的功能,所以 Web 应用在 Cookie 的基础上封装了 Session 的概念,专门用做用户身份识别。

    // 设置的时候要避免:
      //  不要以 _ 开头
      //   不能为 isNew


    // 默认配置
    exports.session = {
        key: 'EGG_SESS',  //密钥
        maxAge: 24 * 3600 * 1000, // 1 天  失效时间
        httpOnly: true, // 是否允许前端访问
        encrypt: true, //是否加密传输
    };

拦截器

axios

Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。

特性

  • 从浏览器中创建 XMLHttpRequests
  • 从 node.js 创建 http 请求
  • 支持 Promise API
  • 拦截请求和响应
  • 转换请求数据和响应数据
  • 取消请求
  • 自动转换 JSON 数据
  • 客户端支持防御 XSRF

二次封装 axios

    import axios from 'axios'

    const request = axios.create({
        oittime: 5000  // 响应时间  过时不响应
    })
        // 添加请求拦截器
    request.interceptors.request.use(function (config) {
        // 在发送请求之前做些什么
        return config;
    }, function (error) {
        // 对请求错误做些什么
        return Promise.reject(error);
    });

    // 添加响应拦截器
    request.interceptors.response.use(function (response) {
        // 对响应数据做点什么
        return response;
    }, function (error) {
        // 对响应错误做点什么
        return Promise.reject(error);
    });

    export default request;

api

接口处理

    import request from '../utils/request'
    // post请求方式
    export function Login({ phone, code }) {
        return request.post('/api/login', {  //后台路径
            phone,
            code
        })
    }
    //  get请求方式
    export function GetCode(phone) {
        return request.get('/api/getcode', {
            params: {
            phone
            }
        })
    }

后台接口

    module.exports = app => {
        const { router, controller } = app;
        router.get('/api/getcode', controller.user.getcode);
        router.post('/api/login', controller.user.login);
        router.post('/api/search', controller.products.search);
        router.post('/api/detail', controller.products.detail);

        //controller文件夹里面的products文件里面的detail方法
    };

中间件(middleware)

    // 配饰插件
    config.middleware = [ 'errorHandler', 'authortication' ];

    // 统一处理错误的中间件
        'use strict';

    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'
                ? '服务端异常,请稍后重试'
                : err.message;

            // 从 error 对象上读出各个属性,设置到响应中
            ctx.response.status = status;
            ctx.body = {
                code: 0,
                message: error,
            };
        }
    };
    };


    //登录权限的中间件

        'use strict';
        //token鉴权
        const jwt = require('jsonwebtoken');
        //白名单
        const url = [
        '/api/userinfo',
        ];
    module.exports = () => {
        return async function errorHandler(ctx, next) {
            const token = ctx.cookies.get('token');
            try {
                if (url.includes(ctx.request.path)) {
                    const user = jwt.verify(token, 'abei');
                    ctx.userinfo = user;
                    await next();
                } else {
                    await next();
                }
            } catch (error) {
                if (token) {
                    ctx.throw(422, '用户登录失效');
                } else {
                    ctx.throw(422, '用户未登录');
                }
                }
            };
    };

目录结构

    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
    | ├── 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

controller

用于解析用户的输入,处理后返回相应的结果

    /app/controller/user文件

    'use strict';

    const Controller = require('egg').Controller;

    class UserController extends Controller {
        async home() {
            this.ctx.body = "hi,egg"
        }
    }

service

用于处理业务逻辑

下面是一个 Service 中访问 MySQL 数据库的例子。

更多 Service 层的介绍,可以参考 Service

// app/service/user.js
class UserService extends Service {
  async find(uid) {
    // 假如 我们拿到用户 id 从数据库获取用户详细信息
    const user = await this.app.mysql.get('users', { id: 11 });
    return { user };
  }
}
之后可以通过 Controller 获取 Service 层拿到的数据。

// app/controller/user.js
class UserController extends Controller {
  async info() {
    const ctx = this.ctx;
    const userId = ctx.params.id;
    const user = await ctx.service.user.find(userId);
    ctx.body = user;
  }
}

mysql

在 Web 应用方面 MySQL 是最常见,最好的关系型数据库之一。非常多网站都选择 MySQL 作为网站数据库。

egg-mysql

框架提供了 egg-mysql 插件来访问 MySQL 数据库。这个插件既可以访问普通的 MySQL 数据库,也可以访问基于 MySQL 协议的在线数据库服务。

插入 insert

// 插入
const result = await this.app.mysql.insert('posts', { title: 'Hello World' }); // 在 post 表中,插入 title 为 Hello World 的记录

 //也可以写成这样
 const result = await this.app.mysql.query('INSERT INTO `posts`(`title`) VALUES('Hello World')')
=> INSERT INTO `posts`(`title`) VALUES('Hello World');

    //返回的值
    console.log(result);
    =>
    {
        fieldCount: 0,
        affectedRows: 1, // 有一条发生变化
        insertId: 3710,
        serverStatus: 2,
        warningCount: 2,
        message: '',
        protocol41: true,
        changedRows: 0
    }

// 判断插入成功
const insertSuccess = result.affectedRows === 1;

查询

可以直接使用 get 方法或 select 方法获取一条或多条记录。select 方法支持条件查询与结果的定制。

  • 查询一条记录
    const post = await this.app.mysql.get('posts', { id: 12 });

    => SELECT * FROM `posts` WHERE `id` = 12 LIMIT 0, 1;
  • 查询全表
    const results = await this.app.mysql.select('posts');

    => SELECT * FROM `posts`;
  • 条件查询和结果定制

    const results = await this.app.mysql.select('posts', { // 搜索 post 表
    where: { status: 'draft', author: ['author1', 'author2'] }, // WHERE 条件
    columns: ['author', 'title'], // 要查询的表字段
    orders: [['created_at','desc'], ['id','desc']], // 排序方式
    limit: 10, // 返回数据量
    offset: 0, // 数据偏移量
    });

    => SELECT `author`, `title` FROM `posts`
    WHERE `status` = 'draft' AND `author` IN('author1','author2')
    ORDER BY `created_at` DESC, `id` DESC LIMIT 0, 10;

删除 delete

    const result = await this.app.mysql.delete('posts', {
    author: 'fengmk2',
    });

    => DELETE FROM `posts` WHERE `author` = 'fengmk2';

修改 upadate

// 修改数据,将会根据主键 ID 查找,并更新
const row = {
  id: 123,
  name: 'fengmk2',
  otherField: 'other field value',    // any other fields u want to update
  modifiedAt: this.app.mysql.literals.now, // `now()` on db server
};
const result = await this.app.mysql.update('posts', row); // 更新 posts 表中的记录

=> UPDATE `posts` SET `name` = 'fengmk2', `modifiedAt` = NOW() WHERE id = 123 ;

// 判断更新成功
const updateSuccess = result.affectedRows === 1;

// 如果主键是自定义的 ID 名称,如 custom_id,则需要在 `where` 里面配置
const row = {
  name: 'fengmk2',
  otherField: 'other field value',    // any other fields u want to update
  modifiedAt: this.app.mysql.literals.now, // `now()` on db server
};

const options = {
  where: {
    custom_id: 456
  }
};
const result = await this.app.mysql.update('posts', row, options); // 更新 posts 表中的记录

=> UPDATE `posts` SET `name` = 'fengmk2', `modifiedAt` = NOW() WHERE custom_id = 456 ;

// 判断更新成功
const updateSuccess = result.affectedRows === 1;