为什么要有web应用开发框架
原生node 的痛点
- 1:写法繁琐,复用性差
- 2:令人费解的api,官方api都是callback形式的编程模式
- 3:无法描写复杂业务逻辑,如 处理 日志 鉴权、 eggjs.org/zh-cn/intro…
什么是koa
简介
Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。
通过利用 async 函数,Koa 帮你丢弃回调函数,并有力地增强错误处理。 Koa 并没有捆绑任何中间件, 而是提供了一套优雅的方法,帮助您快速而愉快地编写服务端应用程序。
- 新
- 下一代, 超前
- 新的语法, es7
- 优雅
中间件
1.logger
Koa 的最大特色,也是最重要的一个设计,就是中间件(middleware)。为了理解中间件,我们先看一下 Logger (打印日志)功能的实现。
最简单的写法就是在main函数里面增加一行。
const main = ctx => {
console.log(`${Date.now()} ${ctx.request.method} ${ctx.request.url}`);
ctx.response.body = 'Hello World';
};
2. 中间件的概念
上一个例子里面的 Logger 功能,可以拆分成一个独立函数。
const logger = (ctx, next) => {
console.log(`${Date.now()} ${ctx.request.method} ${ctx.request.url}`);
next();
}
app.use(logger);
3.中间件执行顺序
const koa = require('koa')
const app = new koa()
const one = async(ctx, next) => {
console.log('>> one');
await next();
console.log('<< one');
}
const two = async(ctx, next) => {
console.log('>> two');
await next();
console.log('<< two');
}
const three = async(ctx, next) => {
console.log('>> three');
await next();
console.log('<< three');
}
app.use(one);
app.use(two);
app.use(three);
app.listen(3001,()=>{
console.log('3001');
})
*洋葱模型
其实就是想向我们表达,调用next的时候,中间件的代码执行顺序是什么。
4.异步中间件
迄今为止,所有例子的中间件都是同步的,不包含异步操作。如果有异步操作(比如读取数据库),中间件就必须写成 async 函数。请看
const fs = require('fs.promised');
const Koa = require('koa');
const app = new Koa();
const main = async function (ctx, next) {
ctx.response.type = 'html';
ctx.response.body = await fs.readFile('./demos/template.html', 'utf8');
};
app.use(main);
app.listen(3000);
5.koa 原理
一个基于nodejs的入门级http服务,类似下面代码:
const server = http.createServer((req, res)=>{ res.writeHead(200) res.end('hi 666')
})
server.listen(3000,()=>{ console.log('监听端口3000') })
代码分析,先写个大体框架
const http = require('http')
class Kob {
constructor(){
}
use(callback){
this.callback = callback
};
listen(...arg){
const server = http.createServer((req,res)=>{
this.callback(req,res)
}
)
server.listen(...arg)
};
}
module.exports = Kob
const kob = require('./kob')
const app = new kob()
app.use((req,res)=>{
res.writeHead(200)
res.end('hi 666')
})
app.listen(30001,()=>{
console.log('listen...30001');
})
目前为止,Kob只是个马甲,要真正实现目标还需要引入上下文(context)和中间件机制(middleware)
context
koa为了能够简化API,引入上下文context概念,将原始请求对象req和响应对象res封装并挂载到 context上,并且在context上设置getter和setter,从而简化操作
// index.js
app.use(ctx=>{
ctx.body = 'hehe'
})
koa将req,res对象分别进行包装成request和response 然后再合成到context中 知识储备:getter/setter方法 通过对get/set对对象进行处理封装
const test = {
info :{
name:'peiyi'
},
get name(){
return `[${this.info.name}]`
},
set name(val){
this.info.name = val
}
}
整合
//request.js
module.exports = {
get url(){
return this.req.url
},
get method(){
return this.req.method.toLowerCase()
}
}
//Response.js
module.exports = {
get body(){
return this._body
},
set body(val){
return this._body = val
}
}
//Context.js
module.exports = {
get url(){
return this.request.url
},
get body(){
return this.response.body
},
set body(val){
return this.response.body = val
},
get method(){
return this.request.method
},
}
实现洋葱圈:
Compose.js
function compose(middlewarea){
return function (ctx) {
return dispatch(0)
function dispatch(i) {
let fn = middlewarea[i]
if(!fn) return Promise.resolve()
return Promise.resolve(
fn(ctx,()=>{
return dispatch(i+1)
})
)
}
}
}
async function fn1(ctx,next) {
console.log('fn1');
// await next()
console.log('end fn1');
}
async function fn2(ctx,next) {
console.log('fn2');
// await next()
console.log('end fn2');
}
async function fn3(ctx,next) {
console.log('fn3');
// await next()
console.log('end fn3');
}
compose([fn1,fn2,fn3])('context')
最后kob.js
//kob.js
const http = require('http')
const request = require('./request')
const response = require('./response')
const context = require('./context')
const compose = require('./compose')
class Kob {
constructor(){
this.middlewares = []
}
use(middleware){
this.middlewares.push(middleware)
// this.callback = callback
};
listen(...arg){
const server = http.createServer(async(req,res)=>{
const ctx = this.createContext(req,res)
// 合成中间件
const fn = compose(this.middlewares)
// this.callback(ctx)
await fn(ctx)
//响应
res.end(ctx.body)
}
)
server.listen(...arg)
};
createContext(req,res){
const ctx = Object.create(context)
ctx.request = Object.create(request)
ctx.response = Object.create(response)
ctx.req = ctx.request.req = req
ctx.res = ctx.response.res = res
return ctx
}
}
扩展内容 Object.create的理解 juejin.cn/post/684490…
Egg与Koa的关系
Koa 是一个非常优秀的框架,然而对于企业级应用来说,它还比较基础,并不是一个合适的企业级框架。 Egg 选择了 Koa 作为其基础框架,在它的模型基础上,进一步对它进行了一些增强。分层实现问题,约定优于定义
egg
由阿里巴巴团队开源的一套基于koa的应用框架,已经在集团内部服务了大量的nodejs系统。
Egg.js 为企业级框架和应用而生*,我们希望由 Egg.js 孕育出更多上层框架,帮助开发团队和开发人员降低开发和维护成本。*
设计原则
Egg 的插件机制有很高的可扩展性,一个插件只做一件事。
(比如 Nunjucks 模板封装成了 egg-view-nunjucks、MySQL 数据库封装成了 egg-mysql)。Egg 通过框架聚合这些插件,并根据自己的业务场景定制配置,这样应用的开发成本就变得很低。
Egg 奉行『约定优于配置』,按照一套统一的约定进行应用开发,团队内部采用这种方式可以减少开发人员的学习成本,开发人员不再是『钉子』,可以流动起来。
没有约定的团队,沟通成本是非常高的,比如有人会按目录分栈而其他人按目录分功能,开发者认知不一致很容易犯错。但约定不等于扩展性差,相反 Egg 有很高的扩展性,可以按照团队的约定定制框架。使用 Loader 可以让框架根据不同环境定义默认配置,还可以覆盖 Egg 的默认约定。
特点
- 提供基于 Egg 定制上层框架的能力
- 高度可扩展的插件机制
- 内置多进程管理
- 基于 Koa 开发,性能优异
- 框架稳定,测试覆盖率高
- 渐进式开发(通过egg /lib封装一些方法,提供其他项目或发包使用)
扩展
// app/extend/context.js
module.exports = {
get isIOS() {
const iosReg = /iphone|ipad|ipod/i;
return iosReg.test(this.get('user-agent'));
},
};
在 Controller 中,我们就可以使用到刚才定义的这个便捷属性了:
// app/controller/home.js
exports.handler = ctx => {
ctx.body = ctx.isIOS
? 'Your operating system is iOS.'
: 'Your operating system is not iOS.';
};
插件
作用:主要为程序提供一些工具类,当插件稳定后,还可以独立出来作为一个npm包提供其他业务或者社区使用 Koa 中,经常会引入许许多多的中间件来提供各种各样的功能,例如引入 koa-bodyparser 来解析请求 body。而 Egg 提供了一个更加强大的插件机制,让这些独立领域的功能模块可以更加容易编写。
一个插件可以包含
- extend:扩展基础对象的上下文,提供各种工具类、属性。
- middleware:增加一个或多个中间件,提供请求的前置、后置处理逻辑。
- config:配置各个环境下插件自身的默认配置项。 插件机制egg一大特色。它不但可以保证框架核心的足够精简、稳定、高效,还可以促进业务逻辑的复用,生态圈的形成。
- Koa 已经有了中间件的机制,为啥还要插件呢?
- 中间件、插件、应用它们之间是什么关系,有什么区别?
为什么要插件
使用 Koa 中间件过程中发现了下面一些问题:
- 中间件加载其实是有先后顺序的,但是中间件自身却无法管理这种顺序,只能交给使用者。这样其实非常不友好,一旦顺序不对,结果可能有天壤之别。
- 中间件的定位是拦截用户请求,并在它前后做一些事情,例如:鉴权、安全检查、访问日志等等。但实际情况是,有些功能是和请求无关的,例如:定时任务、消息订阅、后台逻辑等等。
- 有些功能包含非常复杂的初始化逻辑,需要在应用启动的时候完成。这显然也不适合放到中间件中去实现。
中间件、插件、应用的关系
一个插件其实就是一个『迷你的应用』,和应用(app)几乎一样:
- 它包含了 Service、中间件、配置、框架扩展等等。
- 它没有独立的 Router 和 Controller。(插件一般不写业务逻辑)
- 它没有 plugin.js,只能声明跟其他插件的依赖,而不能决定其他插件的开启与否。
插件没有独立的 router 和 controller。这主要出于几点考虑:
-
路由一般和应用强绑定的,不具备通用性。
-
一个应用可能依赖很多个插件,如果插件支持路由可能导致路由冲突。
-
如果确实有统一路由的需求,可以考虑在插件里通过中间件来实现。
// 创建项⽬
$ npm i egg-init -g
$ egg-init egg --type=simple
$ cd egg-example
$ npm i
// 启动项⽬
$ npm run dev
$ open localhost:7001
基础功能
目录结构的约定
egg的原则: 约定大于配置
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 (可选)//utils
│ ├── request.js (可选)
│ ├── response.js (可选)
│ ├── context.js (可选)//ctx
│ ├── 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
框架规定的目录
app/router.js用于配置 URL 路由规则。app/controller/**用于解析用户的输入,处理后返回相应的结果。app/service/**用于编写业务逻辑层,可选。app/middleware/**用于编写中间件,可选。app/public/**用于放置静态资源,可选。app/extend/**用于框架的扩展,可选。config/config.{env}.js用于编写配置文件。config/plugin.js用于配置需要加载的插件。test/**用于单元测试。app.js和agent.js用于自定义启动时的初始化工作,可选。
内置插件约定的目录
app/public/**用于放置静态资源,可选。app/schedule/**用于定时任务,可选。
Application
Application 是全局应用对象,在一个应用中,只会实例化一个,它继承自 Koa.Application,在它上面我们可以挂载一些全局的方法和对象。我们可以轻松的在插件或者应用中扩展 Application 对象。
事件
在框架运行时,会在 Application 实例上触发一些事件,应用开发者或者插件开发者可以监听这些事件做一些操作。作为应用开发者,我们一般会在启动自定义脚本中进行监听。
-
server: 该事件一个 worker 进程只会触发一次,在 HTTP 服务完成启动后,会将 HTTP server 通过这个事件暴露出来给开发者。 -
error: 运行时有任何的异常被 onerror 插件捕获后,都会触发error事件,将错误对象和关联的上下文(如果有)暴露给开发者,可以进行自定义的日志记录上报等处理。 -
request和response: 应用收到请求和响应请求时,分别会触发request和response事件,并将当前请求上下文暴露出来,开发者可以监听这两个事件来进行日志记录。
// app.js
module.exports = app => {
app.once('server', server => {
// websocket
});
app.on('error', (err, ctx) => {
// report error
});
app.on('request', ctx => {
// log receive request
});
app.on('response', ctx => {
// ctx.starttime is set by framework
const used = Date.now() - ctx.starttime;
// log total cost
});
};
获取方式
// app.js
module.exports = app => {
app.xxxx = 'xxxx';
};
使用插件
插件一般通过 npm 模块的方式进行复用:
npm i egg-mysql --save
然后需要在应用或框架的 config/plugin.js 中声明:
// config/plugin.js
// 使用 mysql 插件
exports.mysql = {
enable: true,
package: 'egg-mysql',
};
根据环境配置
同时,我们还支持 plugin.{env}.js 这种模式,会根据运行环境加载插件配置。
// config/plugin.local.js
exports.dev = {
enable: true,
package: 'egg-dev',
};
引入
-
package是npm方式引入,也是最常见的引入方式 -
path是绝对路径引入,如应用内部抽了一个插件,但还没达到开源发布独立npm的阶段,或者是应用自己覆盖了框架的一些插件
// config/plugin.js
const path = require('path');
exports.mysql = {
enable: true,
path: path.join(__dirname, '../lib/plugin/egg-mysql'),
};
如何写一个插件
你可以直接使用 egg-boilerplate-plugin 脚手架来快速上手。
$ mkdir egg-hello && cd egg-hello
$ npm init egg --type=plugin
$ npm i
一个插件其实就是一个『迷你的应用』,下面展示的是一个插件的目录结构,和应用(app)几乎一样。
. egg-hello
├── package.json
├── app.js (可选)
├── agent.js (可选)
├── app
│ ├── extend (可选)
│ | ├── helper.js (可选)
│ | ├── request.js (可选)
│ | ├── response.js (可选)
│ | ├── context.js (可选)
│ | ├── application.js (可选)
│ | └── agent.js (可选)
│ ├── service (可选)
│ └── middleware (可选)
│ └── mw.js
├── config
| ├── config.default.js
│ ├── config.prod.js
| ├── config.test.js (可选)
| ├── config.local.js (可选)
| └── config.unittest.js (可选)
└── test
└── middleware
└── mw.test.js
- 插件需要在
package.json中的eggPlugin节点指定插件特有的信息:
-
{String} name- 插件名(必须配置),具有唯一性,配置依赖关系时会指定依赖插件的 name。 -
{Array} dependencies- 当前插件强依赖的插件列表(如果依赖的插件没找到,应用启动失败)。 -
{Array} optionalDependencies- 当前插件的可选依赖插件列表(如果依赖的插件未开启,只会 warning,不会影响应用启动)。 -
{Array} env- 只有在指定运行环境才能开启,具体有哪些环境可以参考运行环境。此配置是可选的,一般情况下都不需要配置。
{
"name": "egg-rpc",
"eggPlugin": {
"name": "rpc",
"dependencies": [ "registry" ],
"optionalDependencies": [ "vip" ],
"env": [ "local", "test", "unittest", "prod" ]
}
}
插件能做什么?
- 扩展内置对象的接口
app/extend/request.js- 扩展 Koa#Request 类app/extend/response.js- 扩展 Koa#Response 类app/extend/context.js- 扩展 Koa#Context 类app/extend/helper.js- 扩展 Helper 类app/extend/application.js- 扩展 Application 类app/extend/agent.js- 扩展 Agent 类- 插入自定义中间件
- 在应用启动时做一些初始化工作
- 设置定时任务