阅读 7624

egg-从入门到上线 (上)

1 环境搭建、创建、运行

1.1 介绍

egg.js是阿里旗下基于node.js和koa是一个node企业级应用开发框架,可以帮助开发团队,和开发人员减少成本。
基于koa2、es6、es7使得node具有更有规范的开发模式,更低的学习成本、更优雅的代码、更少的维护成本。

image.png

image.png


1.2 环境搭建

1、要求nodejs版本必须大于8.0并且要用LTS 版本
2、创建egg的环境   npm i egg-init -g  / cnpm i egg-init -g        (只需要安装一次)
3、创建项目
cd 到目录里面   (注意目录不要用中文  不要有空格)

1.3 创建

$ npm i egg-init -g
$ egg-init egg-example --type=simple   //例如:egg-init 项目名称 --type=simple
$ cd egg-example
$ npm i
复制代码

1.4 运行项目

npm run dev  
open localhost:7001 //一般性来说默认端口是7001
复制代码

2 目录结构介绍

2.1 目录结构

egg-project
├── package.json
├── app.js (可选)
├── agent.js (可选)
├── app(项目开发目录)
|   ├── router.js (用于配置 URL 路由规则)
│   ├── 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
复制代码

image.png

3 访问路由

egg在设计完全符合比较好的mvc的设计模式。

3.1 那么什么是mvc呢?

全名是Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范。

在egg中视图 (view)、控制器(controller) 和数据模型 Model(Service) 和配置文件(config)

3.2 控制器(controller)

  • app/controller 目录下面实现 Controller
// app/controller/home.js

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

class HomeController extends Controller {
  async index() {
    const { ctx } = this;
    ctx.body = 'hi, world';
  }
}

module.exports = HomeController;

复制代码

输入 npm run dev 查看 http://127.0.0.1:7001 输出  hi, world

我认为控制器就是一个接口,他管理输入和输出

*同样你可以在app/controller 目录下 写很多个这样个js的,来代表接口

3.3 路由(Router)

主要用来描述请求 URL 和具体承担执行动作的 Controller 的对应关系, 框架约定了 app/router.js 文件用于统一所有路由规则。

现在很多单页面,都是存在相对于的路由,你写个js,同样就要写一个路由

// app/controller/user.js
class UserController extends Controller {
  async info() {
    const { ctx } = this;
    ctx.body = {
      name: `hello ${ctx.params.id}`,
    };
  }
}
复制代码
// app/router.js
module.exports = app => {
  const { router, controller } = app;
  router.get('/user/:id', controller.user.info);
};
复制代码

3.4 数据模型 Model(Service)

简单来说,Service 就是在复杂业务场景下用于做业务逻辑封装的一个抽象层,提供这个抽象有以下几个好处:

  • 保持 Controller 中的逻辑更加简洁。
  • 保持业务逻辑的独立性,抽象出来的 Service 可以被多个 Controller 重复调用。
  • 将逻辑和展现分离,更容易编写测试用例。
// app/service/user.js
const Service = require('egg').Service;

class UserService extends Service {
  async addName(name) {
    const user = `你好,${name}`;
    return user;
  }
}

module.exports = UserService;
复制代码
// app/controller/user.js
class UserController extends Controller {
  async info() {
    const { ctx } = this;
    const userInfo = await ctx.service.user.addName('wjw');
    ctx.body = userInfo;
  }
}
复制代码

3.5 egg中视图 (view)

egg中的模板渲染,但是我认为前端后端分离的设计,更加有利于作为服务型架构设计,所以这边不描述view的构造

4 get、post请求

4.1 get 请求

4.1.1 query

在 URL 中 ?后面的部分是一个 Query String,这一部分经常用于 GET 类型的请求中传递参数。例如 GET /search?name=egg&age=26name=egg&age=26 就是用户传递过来的参数。我们可以通过 context.query(为一个对象)拿到解析过后的这个参数体

module.exports = app => {

class HomeController extends Controller {
  async getQuery() {
      const queryObj = this.ctx.query;
      console.log(queryObj.age);
      console.log(queryObj);
      //打印结果:{ name: 'egg', age: '26' }
    }
  }
  return SearchController;
};
复制代码


当 Query String 中的 key 重复时,context.query只取 key 第一次出现时的值,后面再出现的都会被忽略。GET /posts?category=egg&category=koa 通过 context.query拿到的值是 { category: 'egg' }

4.1.2 queries

有时候我们的系统会设计成让用户传递相同的 key,例如 GET /posts?category=egg&id=1&id=2&id=3。针对此类情况,框架提供了 context.queries 对象,这个对象也解析了 Query String,但是它不会丢弃任何一个重复的数据,而是将他们都放到一个数组中:

// GET /posts?category=egg&id=1&id=2&id=3
const Controller = require('egg').Controller;

class HomeController extends Controller {
  async getQueries() {
    console.log(this.ctx.queries);
    //result:
    // {
    //   category: [ 'egg' ],
    //   id: [ '1', '2', '3' ],
    // }
  }
};
复制代码

context.queries上所有的 key 如果有值,也一定会是数组类型。

4.2 post 请求

// 获取参数方法 post 请求


module.exports = app => {
class HomeController extends Controller {
  async postObj() {
      const queryObj = ctx.request.body;
      ctx.body = queryObj;
    }
  }
  return SearchController;
};
复制代码

但是我们请求有时是get,有时是post,有时本来应该是post的请求,但是为了测试方便,还是做成get和post请求都支持的请求,于是一个能同时获取get和post请求参数的中间件就很有必要了.

4.3 编写中间层解决get、post请求

4.3.1 在app目录下新建middleware文件夹

4.3.2 在middleware里面新建params.js,内容如下

/**
 * 获取请求参数中间件
 * 可以使用ctx.params获取get或post请求参数
 */
module.exports = options => {
  return async function params(ctx, next) {
    ctx.params = {
      ...ctx.query,
      ...ctx.request.body
    }
    await next();
  };
};
复制代码

本质上就是把get请求的参数和post请求的参数都放到params这个对象里,所以,不管是get还是post都能获取到请求参数

4.3.3 在/config/config.default.js里注入中间件

'use strict';
module.exports = appInfo => {
  const config = exports = {};
// 注入中间件
  config.middleware = [
    'params',
  ];
  return config;
};
复制代码

4.3.4 使用文章获取

/**
 * 添加文章接口
 */
'use strict';
const Service = require('egg').Service;
class ArticleService extends Service {
  async add() {
    const { ctx } = this;
    // 获取请求参数
    const {
      userId,
      title,
      content,
    } = ctx.params;
    const result = await ctx.model.Article.create({
      userId,
      title,
      content,
    });
    return result;
  }
}
module.exports = ArticleService;
复制代码

4.3.5 允许post请求跨域

// config/plugin.js
exports.cors = {
  enable: true,
  package: 'egg-cors',
};
复制代码
 // config/config.default.js
config.security = {
  csrf: {
    enable: false,
    ignoreJSON: true,
  },
  domainWhiteList: [ 'http://www.baidu.com' ], // 配置白名单
};

config.cors = {
  // origin: '*',//允许所有跨域访问,注释掉则允许上面 白名单 访问
  allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH',
};
复制代码

*一般性最好使用白名单,不要使用全部允许跨域,不安全

5 mysql数据库

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

5.1 安装与配置

安装对应的插件 egg-mysql :

npm i --save egg-mysql
复制代码

开启插件:

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

在 config/config.${env}.js 配置各个环境的数据库连接信息。

5.1.1 单数据源

如果我们的应用只需要访问一个 MySQL 数据库实例,可以如下配置:
使用方式:

// config/config.${env}.js
exports.mysql = {
  // 单数据库信息配置
  client: {
    // host
    host: 'mysql.com',
    // 端口号
    port: '3306',
    // 用户名
    user: 'test_user',
    // 密码
    password: 'test_password',
    // 数据库名
    database: 'test',
  },
  // 是否加载到 app 上,默认开启
  app: true,
  // 是否加载到 agent 上,默认关闭
  agent: false,
};
复制代码
await app.mysql.query(sql, values); // 单实例可以直接通过 app.mysql 访问
复制代码

5.1.2 多数据源

如果我们的应用需要访问多个 MySQL 数据源,可以按照如下配置:

exports.mysql = {
  clients: {
    // clientId, 获取client实例,需要通过 app.mysql.get('clientId') 获取
    db1: {
      // host
      host: 'mysql.com',
      // 端口号
      port: '3306',
      // 用户名
      user: 'test_user',
      // 密码
      password: 'test_password',
      // 数据库名
      database: 'test',
    },
    db2: {
      // host
      host: 'mysql2.com',
      // 端口号
      port: '3307',
      // 用户名
      user: 'test_user',
      // 密码
      password: 'test_password',
      // 数据库名
      database: 'test',
    },
    // ...
  },
  // 所有数据库配置的默认值
  default: {
  },
  // 是否加载到 app 上,默认开启
  app: true,
  // 是否加载到 agent 上,默认关闭
  agent: false,
};
复制代码

5.2 封装增删改查

5.2.1、插入,向users表内插入一条数据

const result = await this.app.mysql.insert('users', {
    name: 'wjw',
    age: 18
  })
// 判断:result.affectedRows === 1
复制代码

5.2.2、查询,查询users表name=Jack的数据

const result = await this.app.mysql.select('users', {
    columns: ['id', 'name'], //查询字段,全部查询则不写,相当于查询*
    where: {
        name: 'wjw'
    }, //查询条件
    orders: [
        ['id', 'desc'] //降序desc,升序asc
    ],
    limit: 10, //查询条数
    offset: 0 //数据偏移量(分页查询使用)
  })
//判断:result.length > 0
复制代码

5.2.3、修改,修改users表id=1的数据age为20

const result = await this.app.mysql.update('users', {
      age: 20 //需要修改的数据
  }, {
      where: {
        id: 1
      } //修改查询条件
  });
//判断:result.affectedRows === 1
复制代码

5.2.4、删除,删除users表name=wjw的数据

const result = await this.app.mysql.delete('users', {
    name: 'wjw'
})
复制代码

6 Cookie 的使用

6.1 Cookie 简介

  • cookie 是存储于访问者的计算机中的变量。可以让我们用同一个浏览器访问同一个域名的时候共享数据。
  • HTTP 是无状态协议。简单地说,当你浏览了一个页面,然后转到同一个网站的另一个页面,服务器无法认识到这是同一个浏览器在访问同一个网站。每一次的访问,都是没有任何关系的。

6.2 Cookie 的设置和获取

6.2.1 Cookie 设置语法

ctx.cookies.set(key, value, options)

this.ctx.cookies.set('name','zhangsan');
复制代码

6.2.2 Cookie 获取语法

ctx.cookies.get(key, options)

this.ctx.cookies.get('name')
复制代码

6.2.3 清除 Cookie

this.ctx.cookies.set('name',null);
复制代码

或者设置 maxAge 过期时间为 0

6.3 Cookie 参数 options

eggjs.org/en/core/coo…

ctx.cookies.set(key, value, {
  maxAge:24 * 3600 * 1000,
  httpOnly: true, // 默认情况下是正确的
  encrypt: true, // cookie在网络传输期间加密
  ctx.cookies.get('frontend-cookie', {
  encrypt: true
});
复制代码

6.4 设置中文 Cookie

6.4.1 第一种解决方案

console.log(new Buffer('hello, world!').toString('base64'));
// 转换成 base64字符串:aGVsbG8sIHdvcmxkIQ==
console.log(new Buffer('aGVsbG8sIHdvcmxkIQ==', 'base64').toString()); // 还原 base64字符串:hello, world!
复制代码

6.4.2 第二种解决方案

ctx.cookies.set(key, value, {
	maxAge:24 * 3600 * 1000,
	httpOnly: true, // 默认情况下是正确的
	encrypt: true, // cookie在网络传输期间进行加密
});
复制代码

7 Session的使用

7.1 Session 简单介绍

session 是另一种记录客户状态的机制,不同的是 Cookie 保存在客户端浏览器中,而session 保存在服务器上。

7.2 Session 的工作流程

当浏览器访问服务器并发送第一次请求时,服务器端会创建一个 session 对象,生成一个类似于 key,value 的键值对, 然后将 key(cookie)返回到浏览器(客户)端,浏览器下次再访问时,携带 key(cookie),找到对应的 session(value)。

7.3 Egg.js 中 session 的使用

egg.js 中 session 基于 egg-session 内置了对 session 的操作

7.3.1 设置

this.ctx.session.userinfo={
	name:'张三', 
  age:'20'
}
复制代码

7.3.2 获取

var userinfo=this.ctx.session
复制代码

7.3.3 Session 的默认设置

exports.session = {
  key: 'EGG_SESS',
  maxAge: 24 * 3600 * 1000, // 1 day httpOnly: true,
  encrypt: true
};
复制代码

7.4 Session 在 config.default.js 中的配置

config.session={
  key:'SESSION_ID',
  maxAge:864000,
  renew: true //延长会话有效期
}
复制代码

7.5 cookie 和session 区别

  • cookie 数据存放在客户的浏览器上,session 数据放在服务器上。
  • cookie 相比 session 没有 session 安全,别人可以分析存放在本地的 COOKIE 并进行 COOKIE欺骗。
  • session 会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能考虑到减轻服务器性能方面,应当使用 COOKIE。
  • 单个 cookie 保存的数据不能超过 4K,很多浏览器都限制一个站点最多保存 20 个 cookie。

8 定时任务&定点任务

egg提供了强大的定时任务系统。通过定时任务,可以系统修改服务的缓存数据,以便处理需要定时更新的数据。

在app/schedule目录下新建一个js文件,每一个js文件就是一个定时任务


### 8.1 定时任务
// app/schedule
module.exports = {
  schedule: {
    interval: '1m', // 1 分钟间隔
    type: 'all', // 指定所有的 worker 都需要执行
  },
  async task(ctx) {
    i++
    console.log(i)
  },
};

/* 注释:
	1ms -> 1毫秒
	1s -> 1秒
	1m -> 1分钟
*/
复制代码

8.2 定点任务

定点任务(以每周一的5点30分0秒更新排行榜为例)


1、使用cron参数设定时间,cron参数分为6个部分,*表示所有都满足

*    *    *    *    *    *
┬    ┬    ┬    ┬    ┬    ┬
│    │    │    │    │    |
│    │    │    │    │    └ 星期 (0 - 7) (0或7都是星期日)
│    │    │    │    └───── 月份 (1 - 12)
│    │    │    └────────── 日期 (1 - 31)
│    │    └─────────────── 小时 (0 - 23)
│    └──────────────────── 分钟 (0 - 59)
└───────────────────────── 秒 (0 - 59, optional)
复制代码
// app/schedule
module.exports = {
  schedule: {
    cron: '0 30 5 * * 1', //每周一的5点30分0秒更新
    type: 'all', // 指定所有的 worker 都需要执行
  },
  async task(ctx) {
    i++
    console.log(i)
  },
};
复制代码

8.3 只执行一次定时任务

设置immediate参数为true时,该定时任务会在项目启动时,立即执行一次定时任务

module.exports = {
  schedule: {
    interval: '1m', // 1 分钟间隔
    type: 'all', // 指定所有的 worker 都需要执行
    immediate: true, //项目启动就执行一次定时任务
  },
  async task(ctx) {
    i++
    console.log(i)
  },
};
复制代码

8.4 关闭任务

配置disable参数为true时,该定时任务即关闭

8.5 指定定时任务执行环境env

env: ["dev", "debug"] //该定时任务在开发环境和debug模式下才执行
复制代码

9 部署

9.1 部署服务器

首先当然是在你的服务器上部署好node服务,然后安装好。

服务器需要预装 Node.js,框架支持的 Node 版本为 >= 8.0.0。
框架内置了 egg-cluster 来启动 Master 进程,Master 有足够的稳定性,不再需要使用 pm2 等进程守护模块。
同时,框架也提供了 egg-scripts 来支持线上环境的运行和停止。

egg-scripts start --port=7001 --daemon --title=egg-server-showcase
复制代码
  • --port=7001 端口号,默认会读取环境变量 process.env.PORT,如未传递将使用框架内置端口 7001
  • --daemon 是否允许在后台模式,无需 nohup。若使用 Docker 建议直接前台运行。
  • --env=prod 框架运行环境,默认会读取环境变量 process.env.EGG_SERVER_ENV, 如未传递将使用框架内置环境 prod
  • --workers=2 框架 worker 线程数,默认会创建和 CPU 核数相当的 app worker 数,可以充分的利用 CPU 资源。
  • --title=egg-server-showcase 用于方便 ps 进程时 grep 用,默认为 egg-server-${appname}
  • --framework=yadan 如果应用使用了可以配置 package.json 的 egg.framework 或指定该参数。
  • --ignore-stderr 忽略启动期的报错。

9.1.1 启动配置项

你也可以在 config.{env}.js 中配置指定启动配置。

// config/config.default.js

exports.cluster = {
  listen: {
    port: 7001,
    hostname: '127.0.0.1',
    // path: '/var/run/egg.sock',
  }
}
复制代码

pathporthostname 均为 server.listen 的参数,egg-scripts 和 egg.startCluster 方法传入的 port 优先级高于此配置。
s

9.1.2 停止命令

该命令将杀死 master 进程,并通知 worker 和 agent 优雅退出。
支持以下参数:

  • --title=egg-server 用于杀死指定的 egg 应用,未传递则会终止所有的 Egg 应用。
"start": "egg-scripts start --daemon --title=${进程名称}",
"stop": "egg-scripts stop --title=${进程名称}"
复制代码
  • 你也可以直接通过
ps -eo "pid,command" | grep -- "--title=egg-server"
复制代码

来找到 master 进程,并 kill 掉,无需 kill -9

因为egg的知识点太多,故分上下两章

导读

egg-从入门到上线 (下)

egg-mongoose专题