对上篇文章回顾下,上篇讲到了javascript
经过 ctx.cookies
,咱们能够在 controller 中便捷、安全的设置和读取 Cookie。html
class HomeController extends Controller {
async add() {
const ctx = this.ctx;
let count = ctx.cookies.get('count');
count = count ? Number(count) : 0;
ctx.cookies.set('count', ++count);
ctx.body = count;
}
async remove() {
const ctx = this.ctx;
ctx.cookies.set('count', null);
ctx.status = 204;
}
}
复制代码
设置 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 个参数的支持:java
{Boolean} overwrite
:设置 key 相同的键值对如何处理,若是设置为 true
,则后设置的值会覆盖前面设置的,不然将会发送两个 set-cookie 响应头。{Boolean} signed
:设置是否对 Cookie 进行签名,若是设置为 true,则设置键值对的时候会同时对这个键值对的值进行签名,后面取的时候作校验,能够防止前端对这个值进行篡改。默认为 true。{Boolean} encrypt
:设置是否对 Cookie 进行加密,若是设置为 true,则在发送 Cookie 前会对这个键值对的值进行加密,客户端没法读取到 Cookie 的明文值。默认为 false。Cookie 是加签不加密的,浏览器能够看到明文,js 不能访问,不能被客户端(手工)篡改。mysql
若是想要 Cookie 在浏览器端能够被 js 访问并修改:linux
ctx.cookies.set(key, value, {
httpOnly: false,
signed: false,
});
复制代码
上面在设置 Cookie 的时候,咱们能够设置 options.signed 和 options.encrypt 来对 Cookie 进行签名或加密,所以对应的在获取 Cookie 的时候也要传相匹配的选项。git
因为咱们在 Cookie 中须要用到加解密和验签,因此须要配置一个秘钥供加密使用。在 config/config.default.js
中github
module.exports = {
keys: 'key1,key2',
};
复制代码
keys 配置成一个字符串,能够按照逗号分隔配置多个 key。Cookie 在使用这个配置进行加解密时:redis
若是咱们想要更新 Cookie 的秘钥,可是又不但愿以前设置到用户浏览器上的 Cookie 失效,能够将新的秘钥配置到 keys 最前面,等过一段时间以后再删去不须要的秘钥便可。sql
Cookie 在 Web 应用中常常承担标识请求方身份的功能,因此 Web 应用在 Cookie 的基础上封装了 Session 的概念,专门用作用户身份识别。
框架内置了 Session 插件,给咱们提供了 ctx.session
来访问或者修改当前用户 Session 。
class HomeController extends Controller {
async fetchPosts() {
const ctx = this.ctx;
// 获取 Session 上的内容
const userId = ctx.session.userId;
const posts = await ctx.service.post.fetch(userId);
// 修改 Session 的值
ctx.session.visited = ctx.session.visited ? (ctx.session.visited + 1) : 1;
ctx.body = {
success: true,
posts,
};
}
}
复制代码
Session 的使用方法很是直观,直接读取它或者修改它就能够了,若是要删除它,直接将它赋值为 null:
ctx.session = null;
复制代码
须要 特别注意 的是:设置 session 属性时须要避免如下几种状况(会形成字段丢失,详见 koa-session 源码)
_
开头isNew
// ❌ 错误的用法
ctx.session._visited = 1; // --> 该字段会在下一次请求时丢失
ctx.session.isNew = 'HeHe'; // --> 为内部关键字, 不该该去更改
// ✔️ 正确的用法
ctx.session.visited = 1; // --> 此处没有问题
复制代码
Session 的实现是基于 Cookie 的,默认配置下,用户 Session 的内容加密后直接存储在 Cookie 中的一个字段中,用户每次请求咱们网站的时候都会带上这个 Cookie,咱们在服务端解密后使用。Session 的默认配置以下:
exports.session = {
key: 'EGG_SESS',
maxAge: 24 * 3600 * 1000, // 1 天
httpOnly: true,
encrypt: true,
};
复制代码
能够看到这些参数除了 key
都是 Cookie 的参数,key
表明了存储 Session 的 Cookie 键值对的 key 是什么。在默认的配置下,存放 Session 的 Cookie 将会加密存储、不可被前端 js 访问,这样能够保证用户的 Session 是安全的。
Session 默认存放在 Cookie 中,可是若是咱们的 Session 对象过于庞大,就会带来一些额外的问题:
咱们只须要设置 app.sessionStore
便可将 Session 存储到指定的存储中。
// app.js
module.exports = app => {
app.sessionStore = {
// support promise / async
async get (key) {
// return value;
},
async set (key, value, maxAge) {
// set key to store
},
async destroy (key) {
// destroy key
},
};
};
复制代码
sessionStore
的实现咱们也能够封装到插件中,例如 egg-session-redis 就提供了将 Session 存储到 redis 中的能力,在应用层,咱们只须要引入 egg-redis 和 egg-session-redis 插件便可。
// plugin.js
exports.redis = {
enable: true,
package: 'egg-redis',
};
exports.sessionRedis = {
enable: true,
package: 'egg-session-redis',
};
复制代码
一旦选择了将 Session 存入到外部存储中,就意味着系统将强依赖于这个外部存储,当它挂了的时候,咱们就彻底没法使用 Session 相关的功能了。所以咱们更推荐你们只将必要的信息存储在 Session 中,保持 Session 的精简并使用默认的 Cookie 存储,用户级别的缓存不要存储在 Session 中。
虽然在 Session 的配置中有一项是 maxAge,可是它只能全局设置 Session 的有效期,咱们常常能够在一些网站的登录页上看到有 记住我 的选项框,勾选以后可让登录用户的 Session 有效期更长。这种针对特定用户的 Session 有效时间设置咱们能够经过 ctx.session.maxAge=
来实现。
const ms = require('ms');
class UserController extends Controller {
async login() {
const ctx = this.ctx;
const { username, password, rememberMe } = ctx.request.body;
const user = await ctx.loginAndGetUser(username, password);
// 设置 Session
ctx.session.user = user;
// 若是用户勾选了 `记住我`,设置 30 天的过时时间
if (rememberMe) ctx.session.maxAge = ms('30d');
}
}
复制代码
默认状况下,当用户请求没有致使 Session 被修改时,框架都不会延长 Session 的有效期,可是在有些场景下,咱们但愿用户若是长时间都在访问咱们的站点,则延长他们的 Session 有效期,不让用户退出登陆态。
// config/config.default.js
module.exports = {
session: {
renew: true,
},
};
复制代码
onerror 插件的配置中支持 errorPageUrl 属性,当配置了 errorPageUrl 时,一旦用户请求线上应用的 HTML 页面异常,就会重定向到这个地址。
在 config/config.default.js
中 先配置静态文件地址
// config/config.default.js
module.exports = {
static: {
prefix: '/',
dir: path.join(appInfo.baseDir, 'app/public'),
},
};
复制代码
// config/config.default.js
module.exports = {
onerror: {
// 线上页面发生异常时,重定向到这个页面上
errorPageUrl: '/50x.html',
},
};
复制代码
// config/config.default.js
module.exports = {
onerror: {
all(err, ctx) {
// 在此处定义针对全部响应类型的错误处理方法
// 注意,定义了 config.all 以后,其余错误处理方法不会再生效
ctx.body = 'error';
ctx.status = 500;
},
html(err, ctx) {
// html hander
ctx.body = '<h3>error</h3>';
ctx.status = 500;
},
json(err, ctx) {
// json hander
ctx.body = { message: 'error' };
ctx.status = 500;
},
jsonp(err, ctx) {
// 通常来讲,不须要特殊针对 jsonp 进行错误定义,jsonp 的错误处理会自动调用 json 错误处理,并包装成 jsonp 的响应格式
},
},
};
复制代码
框架并不会将服务端返回的 404 状态当作异常来处理,可是框架提供了当响应为 404 且没有返回 body 时的默认响应。
{ "message": "Not Found" }
复制代码
<h1>404 Not Found</h1>
复制代码
框架支持经过配置,将默认的 HTML 请求的 404 响应重定向到指定的页面。
// config/config.default.js
module.exports = {
notfound: {
pageUrl: '/404.html',
},
};
复制代码
在一些场景下,咱们须要自定义服务器 404 时的响应,和自定义异常处理同样,咱们也只须要加入一个中间件便可对 404 作统一处理:
// app/middleware/notfound_handler.js
module.exports = () => {
return async function notFoundHandler(ctx, next) {
await next();
if (ctx.status === 404 && !ctx.body) {
if (ctx.acceptJSON) {
ctx.body = { error: 'Not Found' };
} else {
ctx.body = '<h1>Page Not Found</h1>';
}
}
};
};
复制代码
在配置中引入中间件:
// config/config.default.js
module.exports = {
middleware: [ 'notfoundHandler' ],
};
复制代码
框架提供了 egg-mysql 插件来访问 MySQL 数据库。这个插件既能够访问普通的 MySQL 数据库,也能够访问基于 MySQL 协议的在线数据库服务
npm i --save egg-mysql
复制代码
开启插件:
// config/plugin.js
exports.mysql = {
enable: true,
package: 'egg-mysql',
};
复制代码
在 config/config.${env}.js
配置各个环境的数据库链接信息。
若是咱们的应用只须要访问一个 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 访问
复制代码
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,
};
复制代码
使用方式:
const client1 = app.mysql.get('db1');
await client1.query(sql, values);
const client2 = app.mysql.get('db2');
await client2.query(sql, values);
复制代码
因为对 MySQL 数据库的访问操做属于 Web 层中的数据处理层,所以咱们强烈建议将这部分代码放在 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 };
}
}
// 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;
}
}
复制代码
// 插入
const result = await this.app.mysql.insert('posts', { title: 'Hello World' }); // 在 post 表中,插入 title 为 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`;
复制代码
where
查询条件 { status: 'draft', author: ['author1', 'author2'] }
columns
查询的列名 ['author', 'title']
orders
排序方式 [['created_at','desc'], ['id','desc']]
limit
10
查询条数offset
0
偏移量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;
复制代码
// 修改数据,将会根据主键 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;
复制代码
const result = await this.app.mysql.delete('posts', {
author: 'fengmk2',
});
=> DELETE FROM `posts` WHERE `author` = 'fengmk2';
复制代码
使用 query 能够执行合法的 sql 语句。
咱们极其不建议开发者拼接 sql 语句,这样很容易引发 sql 注入!!
若是必需要本身拼接 sql 语句,请使用 mysql.escape
方法。
const postId = 1;
const results = await this.app.mysql.query('update posts set hits = (hits + ?) where id = ?', [1, postId]);
=> update posts set hits = (hits + 1) where id = 1;
复制代码
通常来讲,事务是必须知足4个条件(ACID): Atomicity(原子性)、Consistency(一致性)、Isolation(隔离性)、Durability(可靠性)
所以,对于一个事务来说,必定伴随着 beginTransaction、commit 或 rollback,分别表明事务的开始,成功和失败回滚。
beginTransaction
, commit
或 rollback
都由开发者来彻底控制,能够作到很是细粒度的控制。const conn = await app.mysql.beginTransaction(); // 初始化事务
try {
await conn.insert(table, row1); // 第一步操做
await conn.update(table, row2); // 第二步操做
await conn.commit(); // 提交事务
} catch (err) {
// error, rollback
await conn.rollback(); // 必定记得捕获异常后回滚事务!!
throw err;
}
复制代码
beginTransactionScope(scope, ctx)
scope
: 一个 generatorFunction,在这个函数里面执行此次事务的全部 sql 语句。ctx
: 当前请求的上下文对象,传入 ctx 能够保证即使在出现事务嵌套的状况下,一次请求中同时只有一个激活状态的事务。const result = await app.mysql.beginTransactionScope(async conn => {
// don't commit or rollback by yourself
await conn.insert(table, row1);
await conn.update(table, row2);
return { success: true };
}, ctx); // ctx 是当前请求的上下文,若是是在 service 文件中,能够从 `this.ctx` 获取到
// if error throw on scope, will auto rollback
复制代码
若是须要调用 MySQL 内置的函数(或表达式),可使用 Literal
。
NOW()
:数据库当前系统时间,经过 app.mysql.literals.now
获取。await this.app.mysql.insert(table, {
create_time: this.app.mysql.literals.now,
});
=> INSERT INTO `$table`(`create_time`) VALUES(NOW())
复制代码
下例展现了如何调用 MySQL 内置的 CONCAT(s1, ...sn)
函数,作字符串拼接。
const Literal = this.app.mysql.literals.Literal;
const first = 'James';
const last = 'Bond';
await this.app.mysql.insert(table, {
id: 123,
fullname: new Literal(`CONCAT("${first}", "${last}"`),
});
=> INSERT INTO `$table`(`id`, `fullname`) VALUES(123, CONCAT("James", "Bond"))
复制代码
启动的时候
windows
set DEBUG=ali-rds* && npm run dev
复制代码
linux、 mac
DEBUG=ali-rds* npm run dev
复制代码
在 Node.js 社区中,sequelize 是一个普遍使用的 ORM 框架,它支持 MySQL、PostgreSQL、SQLite 和 MSSQL 等多个数据源。
安装依赖
npm install --save egg-sequelize mysql2
复制代码
在 config/plugin.js
中引入 egg-sequelize 插件
exports.sequelize = {
enable: true,
package: 'egg-sequelize',
};
复制代码
在 config/config.default.js
中编写 sequelize 配置
config.sequelize = {
dialect: 'mysql',
host: '127.0.0.1',
port: 3306,
database: 'egg-sequelize-doc-default',
};
复制代码
咱们能够在不一样的环境配置中配置不一样的数据源地址,用于区分不一样环境使用的数据库,例如咱们能够新建一个 config/config.unittest.js
配置文件,写入以下配置,将单测时链接的数据库指向 egg-sequelize-doc-unittest。
exports.sequelize = {
dialect: 'mysql',
host: '127.0.0.1',
port: 3306,
database: 'egg-sequelize-doc-unittest',
};
复制代码
在项目的演进过程当中,每个迭代都有可能对数据库数据结构作变动,怎样跟踪每个迭代的数据变动,并在不一样的环境(开发、测试、CI)和迭代切换中,快速变动数据结构呢?这时候咱们就须要 Migrations 来帮咱们管理数据结构的变动了。
sequelize 提供了 sequelize-cli 工具来实现 Migrations,咱们也能够在 egg 项目中引入 sequelize-cli。
npm install --save-dev sequelize-cli
复制代码
在 egg 项目中,咱们但愿将全部数据库 Migrations 相关的内容都放在 database
目录下,因此咱们在项目根目录下新建一个 .sequelizer
配置文件:
'use strict';
const path = require('path');
module.exports = {
config: path.join(__dirname, 'database/config.json'),
'migrations-path': path.join(__dirname, 'database/migrations'),
'seeders-path': path.join(__dirname, 'database/seeders'),
'models-path': path.join(__dirname, 'app/model'),
};
复制代码
npx sequelize init:config
npx sequelize init:migrations
复制代码
执行完后会生成 database/config.json
文件和 database/migrations
目录,咱们修改一下 database/config.json
中的内容,将其改为咱们项目中使用的数据库配置:
{
"development": {
"username": "root",
"password": null,
"database": "egg-sequelize-doc-default",
"host": "127.0.0.1",
"dialect": "mysql"
},
"test": {
"username": "root",
"password": null,
"database": "egg-sequelize-doc-unittest",
"host": "127.0.0.1",
"dialect": "mysql"
}
}
复制代码
时 sequelize-cli 和相关的配置也都初始化好了,咱们能够开始编写项目的第一个 Migration 文件来建立咱们的一个 users 表了。
npx sequelize migration:generate --name=init-users
复制代码
执行完后会在 database/migrations
目录下生成一个 migration 文件(${timestamp}-init-users.js
),咱们修改它来处理初始化 users 表:
'use strict';
module.exports = {
// 在执行数据库升级时调用的函数,建立 users 表
up: async (queryInterface, Sequelize) => {
const { INTEGER, DATE, STRING } = Sequelize;
await queryInterface.createTable('users', {
id: { type: INTEGER, primaryKey: true, autoIncrement: true },
name: STRING(30),
age: INTEGER,
created_at: DATE,
updated_at: DATE,
});
},
// 在执行数据库降级时调用的函数,删除 users 表
down: async queryInterface => {
await queryInterface.dropTable('users');
},
};
复制代码
# 升级数据库
npx sequelize db:migrate
# 若是有问题须要回滚,能够经过 `db:migrate:undo` 回退一个变动
# npx sequelize db:migrate:undo
# 能够经过 `db:migrate:undo:all` 回退到初始状态
# npx sequelize db:migrate:undo:all
复制代码
首先咱们来在 app/model/
目录下编写 user 这个 Model:
'use strict';
module.exports = app => {
const { STRING, INTEGER, DATE } = app.Sequelize;
const User = app.model.define('user', {
id: { type: INTEGER, primaryKey: true, autoIncrement: true },
name: STRING(30),
age: INTEGER,
created_at: DATE,
updated_at: DATE,
});
return User;
};
复制代码
这个 Model 就能够在 Controller 和 Service 中经过 app.model.User
或者 ctx.model.User
访问到了,例如咱们编写 app/controller/users.js
:
// app/controller/users.js
const Controller = require('egg').Controller;
function toInt(str) {
if (typeof str === 'number') return str;
if (!str) return str;
return parseInt(str, 10) || 0;
}
class UserController extends Controller {
async index() {
const ctx = this.ctx;
const query = { limit: toInt(ctx.query.limit), offset: toInt(ctx.query.offset) };
ctx.body = await ctx.model.User.findAll(query);
}
async show() {
const ctx = this.ctx;
ctx.body = await ctx.model.User.findByPk(toInt(ctx.params.id));
}
async create() {
const ctx = this.ctx;
const { name, age } = ctx.request.body;
const user = await ctx.model.User.create({ name, age });
ctx.status = 201;
ctx.body = user;
}
async update() {
const ctx = this.ctx;
const id = toInt(ctx.params.id);
const user = await ctx.model.User.findByPk(id);
if (!user) {
ctx.status = 404;
return;
}
const { name, age } = ctx.request.body;
await user.update({ name, age });
ctx.body = user;
}
async destroy() {
const ctx = this.ctx;
const id = toInt(ctx.params.id);
const user = await ctx.model.User.findByPk(id);
if (!user) {
ctx.status = 404;
return;
}
await user.destroy();
ctx.status = 200;
}
}
module.exports = UserController;
复制代码
工具总结