koa list in github : https://github.com/topics/koa
异常处理
并发
async / await
特点
- 让异步逻辑用同步写法实现
- 最底层的await返回需要是Promise对象
- 可以通过多层 async function 的同步写法代替传统的callback嵌套
function getSyncTime() {
return new Promise((resolve, reject) => {
try {
let startTime = new Date().getTime()
setTimeout(() => {
let endTime = new Date().getTime()
let data = endTime - startTime
resolve( data )
}, 500)
} catch ( err ) {
reject( err )
}
})
}
async function getSyncData() {
let time = await getSyncTime()
let data = `endTime - startTime = ${time}`
return data
}
async function getData() {
let data = await getSyncData()
console.log( data )
}
getData()
了解更多异步编程,可以戳鲸鱼之前的笔记Nodejs学习记录:异步编程
现在我们实现异步编程是用 async/await 加上 Promise, 那么我们使用Promise如何兼容以前的回调呢?--> async awarit
const fs = require("fs");
const readFilePromise = filename => {
new Promise((resolve, reject) => {
fs.readFile(filename, (err, data) => {
if(err) {
reject(err)
return
}
resolve(data)
})
})
}
async function main() {
const txt = await readFilePromise("mock.txt")
console.log(txt.toString())
}
main()
cookie
HTTP 请求都是无状态的,但是我们的 Web 应用通常都需要知道发起请求的人是谁。为了解决这个问题,HTTP 协议设计了一个特殊的请求头:Cookie。服务端可以通过响应头(set-cookie)将少量数据响应给客户端,浏览器会遵循协议将数据保存,并在下次请求同一个服务的时候带上(浏览器也会遵循协议,只在访问符合 Cookie 指定规则的网站时带上对应的 Cookie 来保证安全性)。
cookie经常用于做登录信息的储存,当然我们在后端经常喜欢用它,在前端的单页应用一般喜欢用localstorage
通过 ctx.cookies
,我们可以在 controller 中便捷、安全的设置和读取 Cookie。
来个简单的案例,看看如何写入cookie
koa提供了从上下文直接读取、写入cookie的方法
- ctx.cookies.get(name, [options])
读取上下文请求中的cookie
- ctx.cookies.set(name, value, [options])
在上下文中写入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 的值。{Boolean} overwrite
感兴趣的可以看看 cookie的实现源码
koa2 中操作的cookies是使用了npm的cookies模块,所以在读写cookie的使用参数与该模块的使用一致。
源码在:https://github.com/pillarjs/c...
const Koa = require('koa')
const app = new Koa()
app.use(async(ctx) => {
if(ctx.url === '/index'){
ctx.cookies.set(
'cid',
'hello world',
{
domain: 'localhost', //写cookie所在的域名
path: '/index',// 写cookie所在的路径
maxAge:10 * 60 * 1000, // cookie有效时长
expires: new Date('2017-02-15'), // cookie失效时间
httpOnly: false, // 是否只用于http请求中获取
overwrite:false // 是否允许重写
}
)
ctx.body = 'cookie is ok'
} else {
ctx.body = 'hello world'
}
})
app.listen(3000, () => {
console.log('[demo] cookie is starting at port 3000')
})
在设置 Cookie 时我们需要思考清楚这个 Cookie 的作用,它需要被浏览器保存多久?是否可以被 js 获取到?是否可以被前端修改?
访问http://localhost:3000/index
- 可以在控制台的cookie列表中中看到写在页面上的cookie
- 在控制台的console中使用document.cookie可以打印出在页面的所有cookie(需要是httpOnly设置false才能显示)
更改下代码
const Koa = require('koa')
const app = new Koa()
app.use(async(ctx) => {
if(ctx.url === '/index'){
ctx.cookies.set(
'cid',
'hello world',
{
domain: 'localhost', //写cookie所在的域名
path: '/index',// 写cookie所在的路径
maxAge:10 * 60 * 1000, // cookie有效时长
expires: new Date('2017-02-15'), // cookie失效时间
httpOnly: false, // 是否只用于http请求中获取
overwrite:false // 是否允许重写
}
);
ctx.body = 'cookie is ok';
} else {
if(ctx.cookies.get('cid')){
ctx.body= ctx.cookies.get('cid');
}else {
ctx.body = 'cookie is none';
}
}
})
app.listen(3000, () => {
console.log('[demo] cookie is starting at port 3000')
})
重启服务器node cookie.js
,浏览器分别输入http://localhost:3000/index/
http://localhost:3000
http://localhost:3000/index/aa
因为我们配置了
{path:'/index'}
session
Cookie 在 Web 应用中经常承担标识请求方身份的功能,
但是cookie信息会被储存在浏览器本地或硬盘中,这样会有安全问题,如果有人能够访问你的电脑就能分析出你的敏感信息,用户名、密码等等。为了解决这个隐患,所以 Web 应用在 Cookie 的基础上封装了 Session 的概念,专门用做用户身份识别。
既然服务器渲染又需要用户登录功能,那么用session去记录用户登录态是必要的
koa2原生功能只提供了cookie的操作,但是没有提供session操作。session就只能自己实现或者通过第三方中间件实现。
但是基于koa的egg.js框架内置了 Session 插件,给我们提供了 ctx.session 来访问或者修改当前用户 Session 。----> cookie & session
在koa2中实现session的方案有:
- 如果session数据量很小,可以直接存在内存中
- 如果session数据量很大,则需要存储介质存放session数据
数据库存储方案
储存在MySQL
需要用到中间件:
- koa-session
- koa-session-minimal
适用于koa2 的session中间件,提供存储介质的读写接口 。
- koa-mysql-session
为koa-session-minimal中间件提供MySQL数据库的session数据读写操作。
然后,我们将将sessionId和对应的数据存到数据库
将数据库的存储的sessionId存到页面的cookie中
根据cookie的sessionId去获取对于的session信息
在mysql数据库创建 Koa_session_demo数据库
const Koa = require('koa')
const session = require('koa-session-minimal');
const MysqlSession = require('koa-mysql-session')
const app = new Koa()
//配置存储session信息的mysql
let store = new MysqlSession({
user: 'root',
password: 'wyc2016',
database: 'koa_session_demo',
host: '127.0.0.1',
})
//存放sessionId的cookie配置
let cookie = {
maxAge: '',// cookie有效时长
expires: '',// cookie失效时间
path: '', // 写cookie所在的路径
domain: '', // 写cookie所在的域名
httpOnly: '', // 是否只用于http请求中获取
overwrite: '', // 是否允许重写
secure: '',
sameSite: '',
signed: '',
}
// 使用session中间件
app.use(session({
key: 'SESSION_ID',
store: store,
cookie: cookie
}))
app.use(async (ctx) => {
// 设置session
if(ctx.url === '/set') {
ctx.session = {
user_id: Math.random().toString(36).substr(2),
count:0
}
ctx.body = ctx.session
}else if (ctx.url === '/'){
// 读取session信息
ctx.session.count = ctx.session.count + 1
ctx.body = ctx.session
}
})
app.listen(3000, ()=> {
console.log('[demo] session is starting at port 3000');
})
储存在redis
在express中我们用的是express-session,那么在koa2中用的是哪些模块:
注意:一旦选择了将 Session 存入到外部存储中,就意味着系统将强依赖于这个外部存储,当它挂了的时候,我们就完全无法使用 Session 相关的功能了。因此我们更推荐大家只将必要的信息存储在 Session 中,保持 Session 的精简并使用默认的 Cookie 存储,用户级别的缓存不要存储在 Session 中。
设置跨域
示例:https://github.com/WangCao/h5-Dooring/blob/master/server.js
模板引擎
文件上传(upload)
busboy模块
npm install --save busboy
busboy 模块是用来解析POST请求,node原生req中的文件流。
更多详细API可以访问npm官方文档 https://www.npmjs.com/package...
const inspect = require('util').inspect
const path = require('path')
const fs = require('fs')
const Busboy = require('busboy')
// req 为node原生请求
const busboy = new Busboy({ headers: req.headers })
// ...
// 监听文件解析事件
busboy.on('file', function(fieldname, file, filename, encoding, mimetype) {
console.log(`File [${fieldname}]: filename: ${filename}`)
// 文件保存到特定路径
file.pipe(fs.createWriteStream('./upload'))
// 开始解析文件流
file.on('data', function(data) {
console.log(`File [${fieldname}] got ${data.length} bytes`)
})
// 解析文件结束
file.on('end', function() {
console.log(`File [${fieldname}] Finished`)
})
})
// 监听请求中的字段
busboy.on('field', function(fieldname, val, fieldnameTruncated, valTruncated) {
console.log(`Field [${fieldname}]: value: ${inspect(val)}`)
})
// 监听结束事件
busboy.on('finish', function() {
console.log('Done parsing form!')
res.writeHead(303, { Connection: 'close', Location: '/' })
res.end()
})
req.pipe(busboy)
上传文件简单实现
#index.js
const Koa = require('koa');
const path = require('path');
const app = new Koa();
const {uploadFile} = require('./util/upload')
app.use( async (ctx) => {
if(ctx.url === '/' && ctx.method === 'GET') {
//当GET请求时候返回表单页面
let html = `
<h1>koa2 upload demo</h1>
<form method="POST" action="/upload.json" enctype="multipart/form-data">
<p>file upload</p>
<span>picName:</span><input name="picName" type="text" /><br/>
<input name="file" type="file" /><br/><br/>
<button type="submit">submit</button>
</form>
`
ctx.body = html
}else if (ctx.url === '/upload.json' && ctx.method ==='POST'){
// 上传文件请求处理
let result = {success: false}
let serverFilePath = path.join(__dirname, 'upload-files')
//上传文件事件
result = await uploadFile(ctx, {
fileType: 'album',
path: serverFilePath
})
ctx.body = result
}else {
// 其他请求显示404
ctx.body = '<h1>404!!! o(╯□╰)o</h1>'
}
})
app.listen(3000, () => {
console.log('[demo] upload-simple is starting at port 3000');
})
# util/upload.js
const inspect = require('util').inspect
const path = require('path')
const fs = require('fs')
const Busboy = require('busboy')
/**
* 同步创建文件目录
* @param {string} dirname 目录绝对地址
* @return {boolean} 创建目录结果
*/
function mkdirsSync( dirname ) {
if (fs.existsSync( dirname )) {
return true
} else {
if (mkdirsSync( path.dirname(dirname)) ) {
fs.mkdirSync( dirname )
return true
}
}
}
/**
* 获取上传文件的后缀名
* @param {string} fileName 获取上传文件的后缀名
* @return {string} 文件后缀名
*/
function getSuffixName( fileName ) {
let nameList = fileName.split('.')
return nameList[nameList.length - 1]
}
/**
* 上传文件
* @param {object} ctx koa上下文
* @param {object} options 文件上传参数 fileType文件类型, path文件存放路径
* @return {promise}
*/
function uploadFile( ctx, options) {
let req = ctx.req
let res = ctx.res
let busboy = new Busboy({headers: req.headers})
// 获取类型
let fileType = options.fileType || 'common'
let filePath = path.join( options.path, fileType)
let mkdirResult = mkdirsSync( filePath )
return new Promise((resolve, reject) => {
console.log('文件上传中...')
let result = {
success: false,
formData: {},
}
// 解析请求文件事件
busboy.on('file', function(fieldname, file, filename, encoding, mimetype) {
let fileName = Math.random().toString(16).substr(2) + '.' + getSuffixName(filename)
let _uploadFilePath = path.join( filePath, fileName )
let saveTo = path.join(_uploadFilePath)
// 文件保存到制定路径
file.pipe(fs.createWriteStream(saveTo))
// 文件写入事件结束
file.on('end', function() {
result.success = true
result.message = '文件上传成功'
console.log('文件上传成功!')
})
})
// 解析表单中其他字段信息
busboy.on('field', function(fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) {
console.log('表单字段数据 [' + fieldname + ']: value: ' + inspect(val));
result.formData[fieldname] = inspect(val);
});
// 解析结束事件
busboy.on('finish', function( ) {
console.log('文件上结束')
resolve(result)
})
// 解析错误事件
busboy.on('error', function(err) {
console.log('文件上出错')
reject(result)
})
req.pipe(busboy)
})
}
module.exports = {
uploadFile
}
异步上传图片实现
源码:https://github.com/JXtreehous...
通过stream的方式上传文件
参考中间件-> aliyun-oss-upload-stream
为什么使用``stream?
- 使用
stream
的方式上传文件可以很大程度上降低服务器内存开销。Aliyun官方SDK并没有对stream进行一个完美的封装,所以通常上传文件(Put Object)
的流程是客户端上传文件到服务器,服务器把文件数据缓存到内存,等文件全部上传完毕后,一次性上传到Aliyun Oss服务。这样做一旦瞬间上传文件的请求过多,服务器的内存开销会直线上升。而使用stream的方式上传文件的流程是客户端在上传文件数据到服务器的过程中,服务器同时也在把文件数据往Aliyun Oss服务传送,而不需要在服务器上缓存文件数据。 - 可以上传大文件,根据上传数据方式不同而不同, Put Object 方式 文件最大不能超过 5GB,而使用stream的方式,文件大小不能超过 48.8TB
- 更快的速度,由于传统方式(Put Object方式)是客户端上传完毕文件后,统一上传到Aliyun Oss,而stream的方式基本上客户端上传完毕后,服务器已经把一大半的文件数据上传到Aliyun了,所以速度要快很多
- 使用更简单,经过封装后,stream的方式使用起来非常的方便,1分钟就可以学会如何使用
数据库
sequelize 连接池
ORM(Object Relational Mapping)框架,提供了了PostgreSQL, MySQL, SQLite and MSSQL 数据库连接池
import Sequelize from "sequelize";
const sequelize = new Sequelize('mock_server', 'root', '123456', {
host: 'localhost',
dialect: 'mysql',
dialectOptions: {
charset: "utf8mb4",
collate: "utf8mb4_unicode_ci",
supportBigNumbers: true,
bigNumberStrings: true
},
pool: {
max: 5,
min: 0,
idle: 10000
}
});
const sequelize = ne
Sequelize ORM
this.dao = sequelize.define('Api', {
project_id: Sequelize.INTEGER,
url: Sequelize.STRING,
request_body: Sequelize.TEXT,
response_body: Sequelize.TEXT,
user_id: Sequelize.INTEGER,
description: Sequelize.TEXT,
host: Sequelize.STRING,
});
this.dao.sync();
let api = this.dao.findOne({
where: {
user_id: user_id,
id: api_id,
project_id: pid,
}
});
将查询结果封装成Promise
Sequelize 事务
Database Transaction
- 托管事务:根据 Promise 链的结果⾃自动提交或回滚事
务,如果遇到⼀一⾏行行,⾃自动回滚所有操作
- 非托管事务:如果出现异常,需要⼿手动处理理commit或
者执⾏行行rollback操作
mysql
npm install --save mysql
mysql模块是node操作MySQL的引擎,可以在node.js环境下对MySQL数据库进行建表,增、删、改、查等操作。
创建数据库会话
onst mysql = require('mysql')
const connection = mysql.createConnection({
host : '127.0.0.1', // 数据库地址
user : 'root', // 数据库用户
password : '123456' // 数据库密码
database : 'my_database' // 选中数据库
})
// 执行sql脚本对数据库进行读写
connection.query('SELECT * FROM my_table', (error, results, fields) => {
if (error) throw error
// connected!
// 结束会话
connection.release()
});
注意:一个事件就有一个从开始到结束的过程,数据库会话操作执行完后,就需要关闭掉,以免占用连接资源。
创建数据连接池
一般情况下操作数据库是很复杂的读写过程,不只是一个会话,如果直接用会话操作,就需要每次会话都要配置连接参数。所以这时候就需要连接池管理会话。
const mysql = require('mysql')
// 创建数据池
const pool = mysql.createPool({
host : '127.0.0.1', // 数据库地址
user : 'root', // 数据库用户
password : '123456' // 数据库密码
database : 'my_database' // 选中数据库
})
// 在数据池中进行会话操作
pool.getConnection(function(err, connection) {
connection.query('SELECT * FROM my_table', (error, results, fields) => {
// 结束会话
connection.release();
// 如果有错误就抛出
if (error) throw error;
})
})
更多详细API可以访问npm官方文档 https://www.npmjs.com/package...
async/await封装使用mysql
由于mysql模块的操作都是异步操作,每次操作的结果都是在回调函数中执行,现在有了async/await,就可以用同步的写法去操作数据库
Promise封装mysql模块
Promise封装 ./async-db
const mysql = require('mysql')
const pool = mysql.createPool({
host : '127.0.0.1',
user : 'root',
password : '123456',
database : 'my_database'
})
let query = function( sql, values ) {
return new Promise(( resolve, reject ) => {
pool.getConnection(function(err, connection) {
if (err) {
reject( err )
} else {
connection.query(sql, values, ( err, rows) => {
if ( err ) {
reject( err )
} else {
resolve( rows )
}
connection.release()
})
}
})
})
}
module.exports = { query }
async/await使用
const { query } = require('./async-db')
async function selectAllData( ) {
let sql = 'SELECT * FROM my_table'
let dataList = await query( sql )
return dataList
}
async function getData() {
let dataList = await selectAllData()
console.log( dataList )
}
getData()
建表初始化
通常初始化数据库要建立很多表,特别在项目开发的时候表的格式可能会有些变动,这时候就需要封装对数据库建表初始化的方法,保留项目的sql脚本文件,然后每次需要重新建表,则执行建表初始化程序就行
├── index.js # 程序入口文件
├── node_modules/
├── package.json
├── sql # sql脚本文件目录
│ ├── data.sql
│ └── user.sql
└── util # 工具代码
├── db.js # 封装的mysql模块方法
├── get-sql-content-map.js # 获取sql脚本文件内容
├── get-sql-map.js # 获取所有sql脚本文件
└── walk-file.js # 遍历sql脚本文件
https://chenshenhai.github.io...
https://github.com/ChenShenha...
Redis
nodejs调用redis服务
import redis from "redis";
const client = redis.createClient(({
port: "19002",
host: "localhost"
}));
使用场景
- 首⻚页数据列列表,top N列列表
- 日志
- 计数信息(如接⼝口访问次数等)
- mock数据
难点
- 数据同步
- 内存飙升
JSONP
原生koa2实现jsonp
在项目复杂的业务场景,有时候需要在前端跨域获取数据,这时候提供数据的服务就需要提供跨域请求的接口,通常是使用JSONP的方式提供跨域接口。
https://chenshenhai.github.io...
https://github.com/ChenShenha...
const Koa = require('koa')
const app = new Koa()
app.use(async (ctx) => {
// 如果jsonp 的请求为GET
if(ctx.method === 'GET' && ctx.url.split('?')[0] ==='/getData.jsonp'){
// 获取jsonp的callback
let callbackName = ctx.query.callback || 'callback'
let returnData = {
success: true,
data: {
text: 'this is a jsonp api',
time: new Date().getTime(),
}
}
// jsonp的script字符串
let jsonpStr = `;${callbackName}(${JSON.stringify(returnData)})`
// 用text/javascript,让请求支持跨域获取
ctx.type = 'text/javascript'
//输出jsonp字符串
ctx.body = jsonpStr
}else{
ctx.body = 'hello jsonp'
}
})
app.listen(3000, () => {
console.log('[demo] jsonp is starting at port 3000')
})
前端部分
$.ajax({
url: 'http://localhost:3000/getData.jsonp',
type: 'GET',
dataType: 'JSONP',
success: function(res) {
console.log(res)
}
})
koa-jsonp中间件
npm install --save koa-jsonp
https://www.npmjs.com/package...
https://github.com/chenshenha...
markdown
OAuth 2.0
OAuth 2.0深入了解:以微信开放平台统一登录为例
微信开放平台开发——网页微信扫码登录(OAuth2.0)
常用中间件
Koa Static Cache: https://www.npmjs.com/package...
更多中间件
koa-onerror
koa-safe-jsonp
常用脚手架
koa-generator
yi-ge/Koa2-API-Scaffold
- Express-style
- Support koa 1.x(supported)
- Support koa 2.x(koa middleware supported,need Node.js 7.6+ ,babel optional)
restful api
uri:
http://localhost:7001/game/62231163?channel=1323
router:
controller.game.detail
const { gameId, channel } = ctx.query;
const _gameId = ctx.params.id || gameId; // 一级分类
console.log('-------------------1', ctx.query)
console.log('-------------------2', gameId)
console.log('-------------------3', ctx.url)
console.log('---------------------4', ctx.params)
console.log('---------------------5', ctx.querystring)
console.log('--------------------6', ctx.origin)
https://www.jianshu.com/p/d0b...
ssrf
看我如何利用NodeJS SSRF漏洞获得AWS完全控制权限
Web安全漏洞之SSRF
Web 安全漏洞 SSRF 简介及解决方案
welefen/ssrf-agent
sms(短信服务、邮箱服务 )
参考项目
egg-commerce
koajs/examples
johndatserakis/koa-vue-notes-api
使用koa2+wechaty打造个人微信小秘书
https://github.com/jtyjty9999...
常见问题
koa
与express
区别
关于区别详解 戳我这篇文章express中间件 文末
koa原理
- 中间件(Middleware)
- 上下文(Context)
参考
Koa.js 设计模式-学习笔记
koa2进阶学习笔记
koa2 源码分析
npm koa-session-minimal
koa2中的session及redis
egg.js Cookie and Session
《HTTP权威指南》
七天学会NodeJS
Koa2 之文件上传下载
node消息队列
快速搭建可用于实战的koa2+mongodb框架
https://chenshenhai.github.io...
KOA2框架原理解析和实现
koa 介绍 ppt
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。