Egg.js框架
Egg.js 为企业级框架和应用而生,我们希望由 Egg.js 孕育出更多上层框架,帮助开发团队和开发人员降低开发和维护成本。
在学习这个框架之前,有几个点要说明一下
- 约定大于配置
- 完全的模块化低耦合(一个模块或一个插件只干一件事情)
安装Egg.js框架
$ yarn add egg egg-bin
上面的代码安装了两个模块,一个是egg框架 ,一个是egg-bin,这个模块主要是用于启动egg项目
构建项目架构
egg是约定大于配置的,所以要按照约定好的东西来建立目录结构与文件
上面的图是我们配置egg目录的图
接下来在package.json里面配置egg的启动
编写控制器
在刚刚创建的项目时面,controller就是用于存放我们的控制器的
// const Controller = require("egg").Controller;
const {Controller} =require("egg");
//这里的命名有约束
class DepartmentInfoController extends Controller{
//驼峰命名
async list(){
this.ctx.response.body="hello egg";
}
}
module.exports=DepartmentInfoController;
配置路由映射
在创建egg.js的项目的时候,我们在app的目录下面创建了一个router.js
文件,这文件的主要目的就是实现路由与控制器的映射关系
module.exports = app => {
let {router,controller}=app;
router.get("/departmentInfo/list",controller.departmentInfo.list);
}
当所有的东西准备完成以后,我们启动浏览器,仍然会报错
这个错误提示我们要配置一个keys
配置egg的keys
在config的目录下面,找到config.default.js
文件,进行keys的配置
exports.keys="abcsnsfi23123";
这个地方的keys可以随便
配置egg的监听地址与端口
egg在配置启动的时候使用的是7001端口号,主机地址使用的是127.0.0.1
,现在,我们可以进行更改
在config.default.js
里面,添加如下配置
//配置egg的启动
exports.cluster={
listen:{
//配置监听的主机地址,0.0.0.0代表当前电脑上面所有的IP地埴
hostname:"0.0.0.0",
port:80,
path:"" //启动路径 ,设置为空
}
}
这个时候,当我们重新启动项目以后,项目就可以使用新地址与新端口来访问了
配置egg的视图
框架内置 egg-view 作为模板解决方案,并支持多模板渲染,每个模板引擎都以插件的方式引入,但保持渲染的 API 一致
可使用以下模板引擎,更多查看
- egg-view-nunjucks
- egg-view-react
- egg-view-vue
- egg-view-ejs
- egg-view-handlebars
- egg-view-pug
- egg-view-xtpl
现在我们还是使用art-tempalte
第一步:安装模块
$ yarn add egg-view-arttemplate
第二步:配置插件
在config目录下面找到plugin.js
,去添加如下配置
//配置视图模板
exports.arttemplate={
enable:true,
package:"egg-view-arttemplate"
}
第三步:配置项目的默认模板
//配置默认模板引擎
exports.view={
//配置模板的后缀名映射关系
mapping:{
".html":"arttemplate",
}
}
第四步:添加视图文件
在view的文件夹下面,创建一个departmentInfo
文件夹,然后在里面创建发前模块的视图
第五步:渲染模板
// const Controller = require("egg").Controller;
const {Controller} =require("egg");
//这里的命名有约束
class DepartmentInfoController extends Controller{
//驼峰命名
async list(){
await this.ctx.render("departmentInfo/list");
}
}
module.exports=DepartmentInfoController;
配置公开文件夹(静态文件夹)
egg与express都是MVC的规范,它们的所有的请求都要经过控制器,在express里面,我们要配置公开文件夹(静态文件夹),在egg里面,egg默认把app
目录下面的public
文件夹公开了
如果我们希望自定义的文件夹也要公开,则需要加载第三方插件egg-static
第一下:下载模块
$ yarn add egg-static
第二步:配置模块
//配置程序的静态目录
exports.static = {
prefix: "/public/",
dir: [
path.join(__dirname, "../app/public"),
{
prefix:"/uploadImgs/",
dir:path.join(__dirname,"../app/uploadImgs")
}
]
}
上面我们配置了两个静态目录 ,一个是public的静态目录,一个是uploadImgs的静态目录
http://127.0.0.1/uploadImgs/part2.png
当我们打开上面的网址的时候,就可以直接找到这张图生,不需要经过路由与控制器了
egg-mysql操作
egg框架针对不同的数据库有不同的操作模块,我们以mysql为例
第一步:安装模块
$ yarn add egg-mysql
第二步:配置插件
//配置mysql的用户名与密码
exports.mysql = {
//客户端
client: {
host: "127.0.0.1",
port: 3306,
user: "root",
password: "aaabbb",
database: "sms",
multipleStatements: true
},
//相当于把DBUtils当成一个属性挂载到了app上面
app: true,
agent: false //默认是false
}
egg的service层操作
第一步:创建service对象
在service目录下面创建一个service文件departmentInfo.js
/**
* departmentinfo的数据表service
*/
// const Service = require("egg").Service;
const {Service} = require("egg");
//构造函数 大写开头
class DepartmentInfoService extends Service{
//因为数据库的操作是异步的,所以需要等待
//查询所有
async getAllList(){
//departmentinfo代表的是表或视图
let result = await this.app.mysql.select("departmentinfo",{
where:{
isDel:false
// did:[1,2,3]
},
limit:5,
offset:5,
orders:["dname"]
});
return result;
}
//查询一个
async findById(){
}
}
module.exports=DepartmentInfoService;
在这一层里面我们直接通过app就拿到了数据库操作对象mysql
第三步:controller控制器调用
//这里的命名有约束
class DepartmentInfoController extends Controller{
//驼峰命名
async list(){
//如果是属性赋值,没事
//this.ctx.response.body="123123";
//通过service 查询所有数据
let result = await this.ctx.service.departmentInfo.getAllList();
await this.ctx.render("departmentInfo/list",{
departmentInfoList:result
});
}
}
在这一步里面,我们直接通过控制器对象里同的ctx拿到了service层
egg-mysql使用的是一种特殊的操作方式
BaseService的编写
与express框架里面一样,我们可以把公共的数据库操作方法封装成一个对象,然后让所有的service再继承这个对象
/*baseService.js*/
const {
Service
} = require("egg");
class BaseService extends Service {
// 因为这个baseService继承了egg里面的service
constructor() {
//把egg框架注入到server里面的参数app以及其它的都传递给父类
super(...arguments);
this.tableName = "";
}
/**
* 获取所有数据
*/
async getAllList(tableName) {
let result = await this.app.mysql.select(tableName||this.tableName, {
where: {
isDel: false
}
});
return result;
}
}
module.exports = BaseService;
departmentInfo.js代码
const BaseService = require("./baseService");
//构造函数 大写开头
class DepartmentInfoService extends BaseService{
constructor(){
super(...arguments);
this.tableName="departmentinfo";
}
}
module.exports= BaseService;
egg-mysql详细操作
查询
可以直接使用 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;
直接执行 sql 语句
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;
新增操作
/**
* service新增系别信息
*/
async addDepartmentInfo({dname,manager,manager_tel}){
let result = await this.app.mysql.insert(this.tableName,{
dname,manager,manager_tel
});
return result;
}
insert into tableName (dname,manager,manager_tel) values (dname,manager,manager_tel)
Egg的middleware编写
在express里面,我们使用app.use
来加载中间件,但是在egg里面它的中间件全部放在middleware这个文件夹下面,在固定的编写格式
module.exports = (options, app) => {
//在这里,它要返回一个方法
return (ctx, next) => {
}
}
options代表这个中间件在调用的时候的参数
app代表egg调用的时候自动注入的对象
ctx代表控制器的上下文,可以通过这个对像拿到
request
,response
next它是一个方法,代表继续拦截以后的操作
现在我们就来写一个egg.js框架里面必备的一个中间件csrf.js.。这个中间件是为了防止跨域攻击的
/**
* cstf的中间件(拦截器)
*/
module.exports = (options, app) => {
//在这里,它要返回一个方法
return async (ctx, next) => {
//如果要配置csrf
ctx.state.csrf = ctx.csrf;
//它个ctx.state它会自动根据你的想要的引擎生成一个变量叫_csrf
await next();
}
}
写了好中间件以后,一定要配置中间件, 我们要在config.deafult.js
里面启用这个中间件
//配置需要启用的中间件
exports.middleware=["csrf"];
egg.js文件上传
-
先设置表单的
enctype="multipart/form-data"
-
安装
egg-multipart
模块$ yarn add egg-multipart
-
配置模块
在
config.default.js
里面进行相关的配置//配置文件上传 exports.multipart={ fileSize:'50mb', mode:"file", //配置已文件的形式接收 //配置文件的默认存放位置 tmpdir:path.join(__dirname,"../app/uploadImgs") }
-
在控制器接收文件
this.ctx.request.files
接收到的是一个数组,数据格式如下
[ { field: 'manager_photo', filename: 'specs_black.png', encoding: '7bit', mime: 'image/png', fieldname: 'manager_photo', transferEncoding: '7bit', mimeType: 'image/png', filepath: 'D:\\H1904\\1023\\code\\egg_demo\\app\\uploadImgs\\2019\\10\\24\\10\\182940b2-ab6b-4061-8a62-2bf6d7a8e5bb.png' } ]
egg.js的路由方法封装
在使用egg.js的过程当中,我们经常发现,我们需要在书写完控制器以后再进行路由的相关操作,这个时候,我们可以在路由文件router.js
的这个文件里面写一个通用方法,实现路由的加载
/**
* 路由文件
*/
const fs = require("fs");
const path = require("path");
module.exports = app => {
let {
router,
controller
} = app;
//读控制器文件夹
let controllerList = fs.readdirSync(path.join(__dirname, "./controller"));
//遍历所有的控制器文件
controllerList.forEach(item => {
//拿到控制器的名子
let controllerName = item.replace(".js", "");
//接下来,去拿控制器时面所有的方法
let _keys = Reflect.ownKeys(controller[controllerName]);
//这个属性名不能是sysbol类型,并且是一个方法
_keys.forEach(m => {
//m就是每一个方法名
if(typeof m!="symbol"&&typeof controller[controllerName][m]=="function"){
//do开头的是POST,其它的是GET
if(m.startsWith("do")){
router.post(`/${controllerName}/${m}`,controller[controllerName][m]);
}
else{
router.get(`/${controllerName}/${m}`,controller[controllerName][m]);
}
}
});
});
router.get("/departmentInfo/query/:did", controller.departmentInfo.query);
}
注意事项
- 在开发过程当中要约定好,所有的
POST
请求我们都要使用do
来开头 - 这个方法不能做到路径变量
当我们完成这个方法的封装以后,这个时候如果再到控制器时面添加方法的时候,就不用再进行路由文件一映射添加了,它会自动实现映射关系
egg.js中的session
第一步:安装模块
$ yarn add egg-session
第二步:配置session
//plugin.js
exports.session = true;
第三步:使用session
this.ctx.session.userInfo=userName; //session的赋值
//------
userName=this.ctx.session.userInfo //session的取值
egg-session实现登陆验证
在平时的开发当中,我们可以实现session做登陆验证,现在我们来编写一个中间件
第一步:创建中间件
//middleware/loginAuth.js
/**
* 登陆验证的中间件
*/
const appConfig=require("../../config/appConfig");
module.exports = (options, app) => {
return async (ctx, next) => {
if(appConfig.excludePath.includes(ctx.path)){
//如果是需要放行的页面,我直接放行
await next();
}
else{
if(ctx.session.userInfo){
//说明别人已经登陆了
await next();
}
else{
await ctx.redirect("/main/login");
}
}
}
}
第二步:启用中间件
//config/config.default.js
//配置需要启用的中间件
exports.middleware=["csrf","loginAuth"];
这样,当用户登陆以后就可以实现登陆验证了
egg.js的生命周期
/**
* 从你启动,到你停止都在这里
*/
class AppBootHook {
constructor(app) {
this.app = app;
}
//配置文件即将加载
configWillLoad() {
//在这个步骤,我们还可以更改配置文件
console.log("我在configWillLoad里面执行了");
// 此时 config 文件已经被读取并合并,但是还并未生效
// 这是应用层修改配置的最后时机
// 注意:此函数只支持同步调用
// 例如:参数中的密码是加密的,在此处进行解密
this.app.config.mysql.password = decrypt(this.app.config.mysql.password);
// 例如:插入一个中间件到框架的 coreMiddleware 之间
const statusIdx = this.app.config.coreMiddleware.indexOf('status');
this.app.config.coreMiddleware.splice(statusIdx + 1, 0, 'limit');
}
//配置文件已经加载
configDidLoad() {
}
//文件加载完成
didLoad() {
// 所有的配置已经加载完毕
// 可以用来加载应用自定义的文件,启动自定义的服务
// 例如:创建自定义应用的示例
this.app.queue = new Queue(this.app.config.queue);
await this.app.queue.init();
// 例如:加载自定义的目录
this.app.loader.loadToContext(path.join(__dirname, 'app/tasks'), 'tasks', {
fieldClass: 'tasksClasses',
});
}
//插件启动完毕
willReady() {
// 所有的插件都已启动完毕,但是应用整体还未 ready
// 可以做一些数据初始化等操作,这些操作成功才会启动应用
// 例如:从数据库加载数据到内存缓存
this.app.cacheData = await this.app.model.query(QUERY_CACHE_SQL);
}
//worker 准备就绪
didReady() {
// 应用已经启动完毕
const ctx = await this.app.createAnonymousContext();
await ctx.service.Biz.request();
}
//应用启动完成
serverDidReady() {
// http / https server 已启动,开始接受外部请求
// 此时可以从 app.server 拿到 server 的实例
this.app.server.on('timeout', socket => {
// handle socket timeout
});
}
//应用即将关闭
beforeClose() {
console.log("我要关闭了");
}
}
module.exports = AppBootHook;
框架提供了统一的入口文件(app.js
)进行启动过程自定义,这个文件返回一个 Boot 类,我们可以通过定义 Boot 类中的生命周期方法来执行启动应用过程中的初始化工作。
框架提供了这些 生命周期函数供开发人员处理:
- 配置文件即将加载,这是最后动态修改配置的时机(
configWillLoad
) - 配置文件加载完成(
configDidLoad
) - 文件加载完成(
didLoad
) - 插件启动完毕(
willReady
) - worker 准备就绪(
didReady
) - 应用启动完成(
serverDidReady
) - 应用即将关闭(
beforeClose
)
我们可以在 app.js
中定义这个 Boot 类,下面我们抽取几个在应用开发中常用的生命周期函数来举例:
Request 别名
以下访问器和 Request 别名等效:
ctx.header
ctx.headers
ctx.method
ctx.method=
ctx.url
ctx.url=
ctx.originalUrl
ctx.origin
ctx.href
ctx.path
ctx.path=
ctx.query
ctx.query=
ctx.querystring
ctx.querystring=
ctx.host
ctx.hostname
ctx.fresh
ctx.stale
ctx.socket
ctx.protocol
ctx.secure
ctx.ip
ctx.ips
ctx.subdomains
ctx.is()
ctx.accepts()
ctx.acceptsEncodings()
ctx.acceptsCharsets()
ctx.acceptsLanguages()
ctx.get()
ctx.session
Response 别名
以下访问器和 Response 别名等效:
ctx.body
ctx.body=
ctx.status
ctx.status=
ctx.message
ctx.message=
ctx.length=
ctx.length
ctx.type=
ctx.type
ctx.headerSent
ctx.redirect()
ctx.attachment()
ctx.set()
ctx.append()
ctx.remove()
ctx.lastModified=
ctx.etag=