目 录CONTENT

文章目录

EGG框架

Administrator
2020-07-24 / 0 评论 / 0 点赞 / 5606 阅读 / 19816 字

Egg.js框架

Egg.js 为企业级框架和应用而生,我们希望由 Egg.js 孕育出更多上层框架,帮助开发团队和开发人员降低开发和维护成本。

在学习这个框架之前,有几个点要说明一下

  1. 约定大于配置
  2. 完全的模块化低耦合(一个模块或一个插件只干一件事情)

安装Egg.js框架

$ yarn add egg egg-bin

上面的代码安装了两个模块,一个是egg框架 ,一个是egg-bin,这个模块主要是用于启动egg项目

构建项目架构

egg是约定大于配置的,所以要按照约定好的东西来建立目录结构与文件

1571812853396

上面的图是我们配置egg目录的图

接下来在package.json里面配置egg的启动

1571812941907

编写控制器

在刚刚创建的项目时面,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);
}

当所有的东西准备完成以后,我们启动浏览器,仍然会报错

1571814232171

这个错误提示我们要配置一个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 一致

可使用以下模板引擎,更多查看

现在我们还是使用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文件上传

  1. 先设置表单的enctype="multipart/form-data"

  2. 安装egg-multipart模块

    $ yarn add egg-multipart
    
  3. 配置模块

    config.default.js里面进行相关的配置

    //配置文件上传
    exports.multipart={
       fileSize:'50mb',
       mode:"file",                 //配置已文件的形式接收
       //配置文件的默认存放位置
       tmpdir:path.join(__dirname,"../app/uploadImgs")
    }
    
  4. 在控制器接收文件

    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);
}

注意事项

  1. 在开发过程当中要约定好,所有的POST请求我们都要使用do来开头
  2. 这个方法不能做到路径变量

当我们完成这个方法的封装以后,这个时候如果再到控制器时面添加方法的时候,就不用再进行路由文件一映射添加了,它会自动实现映射关系

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=
0

评论区