egg.js学习之旅

1. 初始化项目

npm i egg-init -g
egg-init 项目名 --type=simple
npm i
npm run dev
open http://localhost:7001

目录结构

egg-project
├── package.json
├── app.js (可选)	//用于自定义启动时的初始化工作,可选。
├── agent.js (可选)	//用于自定义启动时的初始化工作,可选。
├── app
|   ├── router.js	//用于配置 URL 路由规则。
│   ├── controller	//用于解析用户的输入,处理后返回相应的结果。
│   |   └── home.js
│   ├── service (可选)	//用于编写业务逻辑层,可选,建议使用
│   |   └── user.js
│   ├── middleware (可选)	//用于编写中间件,可选。
│   |   └── response_time.js
│   ├── schedule (可选)	//用于定时任务,可选。
│   |   └── my_task.js
│   ├── public (可选)	//用于放置静态资源,可选。
│   |   └── reset.css
│   ├── view (可选)	//用于放置模板文件,可选。
│   |   └── home.tpl
│   └── extend (可选)	//用于框架的扩展,可选。
│       ├── helper.js (可选)
│       ├── request.js (可选)
│       ├── response.js (可选)
│       ├── context.js (可选)
│       ├── 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

2.初始Egg.js中的路由

  1. 创建 product 控制器
//app/controller/test.js
"use strict";

const Controller = require("egg").Controller;

class TestController extends Controller {
  async index() {
    const { ctx } = this; //ctx 主要用来存储用户请求的信息,每次请求时都会实例化
    ctx.body = {
      code: 0,
      data: "测试数据!",
      message: "success",
    };
  }
}
module.exports = TestController;
  1. 添加路由
"use strict";

/**
 * @param {Egg.Application} app - egg application
 */
module.exports = (app) => {
  const { router, controller } = app;
  router.get("/", controller.home.index);
  router.get("/test", controller.test.index);
};

3.GET方式请求中的两种传参方式

  1. 方式一:query
    获取方式
async detail() {
  // 获取上下文
  const { ctx } = this;
  console.log('方式一:query', ctx.query);
  ctx.body = `id==${ctx.query.id}`;
}

路由配置

router.get('/product/detail', controller.product.detail);
  1. 方式二:params
    获取方式
async detail2() {
  const { ctx } = this;
  console.log('方式二:params', ctx.params);
  ctx.body = `id==${ctx.params.id}`;
}

路由配置

router.get('/product/detail2/:id', controller.product.detail2);

4.Egg.js 中的请求方法

  1. GET 方法
// 商品详情
async detail() {
  const { ctx } = this;
  console.log(ctx.query);
  ctx.body = `id==${ctx.query.id}`;
}
 
async detail2() {
  const { ctx } = this;
  console.log(ctx.params);
  ctx.body = `id==${ctx.params.id}`;
}

//app/router.js 
router.get('/product/detail', controller.product.detail);
router.get('/product/detail2/:id', controller.product.detail2);
  1. POST 方法
    在config.default.js需要先关闭 csrf j检查
//config/config.default.js

config.security = {
  csrf: {
    enable: false,
  }
};
// 新增
async create() {
  const { ctx } = this;
  console.log(ctx.request.body);
  const { name, weight } = ctx.request.body;
  ctx.body = {
    name,
    weight
  };
}
 //app/router.js 
router.post('/product/create', controller.product.create);
  1. PUT 方法
// 更新
async update() {
  const { ctx } = this;
  console.log(ctx.params);
  ctx.body = {
    id: ctx.params.id
  }
}
//app/router.js  
router.put('/product/update/:id', controller.product.update);
  1. DELETE 方法
// 删除
async delete() {
  const { ctx } = this;
  console.log(ctx.params);
  ctx.body = {
    id: ctx.params.id
  }
}
 
router.delete('/product/delete/:id', controller.product.delete);

5.Egg.js 中的Service服务

  1. 创建service文件
    app/service/test.js
const Service = require('egg').Service;
 
class TestService extends Service {
  async index() {
    return {
      id: 100,
      name: '测试'
    }
  }
}
 
module.exports = TestService;
  1. 调用
async index() {
  const { ctx } = this;
  const res = await ctx.service.product.index();
  ctx.body = res;
}
  1. 服务的命名规则
/***
 * 服务的命名规则
 * Service 文件必须放在 app/service 目录,可以支持多级目录,访问的时候可以通过目录名级联访问。
 * app/service/biz/user.js => ctx.service.biz.user (推荐)
 * app/service/sync_user.js => ctx.service.syncUser
 * app/service/HackerNews.js => ctx.service.hackerNews
 */

6.Egg.js 中模板引擎的使用

  1. 安装egg-view-ejs
cnpm i --save egg-view-ejs
  1. 修改配置
    config/plugin.js
'use strict';
 
exports.ejs = {
  enable: true,
  package: 'egg-view-ejs'
};

config/config.default.js

config.view = {
  mapping: {
    '.html': 'ejs'
  }
};
  1. 创建 html 文件
    app/view/test.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>首页</title>
</head>
<body>
    <h1>这是首页内容</h1>
 
    <p>id:<%=res.id%></p>
    <p>name:<%=res.name%></p>
 
    <ul>
        <%for(var i=0; i<lists.length; i++) {%>
        <li><%=lists[i]%></li>
        <%}%>
    </ul>
</body>
</html>
  1. 传值
    app/controller/test.js
'use strict';
 
const Controller = require('egg').Controller;
 
class HomeController extends Controller {
  async index() {
    const { ctx } = this;
    const res = await ctx.service.product.index();
    // ctx.body = res;
    await ctx.render('index.html', {
      res,
      lists: ['a', 'b', 'c']
    });
  }
}
 
module.exports = HomeController;
  1. 效果
    在这里插入图片描述

7.Egg.js 连接 mysql 数据库

  1. 安装 egg-mysql
npm i --save egg-mysql
  1. 配置 plugin.js
exports.mysql = {
  enable: true,
  package: 'egg-mysql',
};
  1. 配置 config.default.js
  config.mysql = {
  // 单数据库信息配置
	  client: {
	    // host
	    host: 'localhost',
	    // 端口号
	    port: '3306',
	    // 用户名
	    user: 'root',
	    // 密码
	    password: 'root',
	    // 数据库名
	    database: 'egg_article',   
	  },
	  // 是否加载到 app 上,默认开启
	  app: true,
	  // 是否加载到 agent 上,默认关闭
	  agent: false,
	};
  };
  return {
    ...config,
    ...userConfig,
  };
  1. egg-mysql 的使用
//app/service/user.js
"use strict";

const Service = require("egg").Service;
class UserService extends Service {
  async find() {
    // "users" 为test数据库数据表名
    const user = this.app.mysql.get("users", { id: 1 });
    return { user };
  }
}

module.exports = UserService;
//app/controller/home.js
"use strict";

const Controller = require("egg").Controller;
class HomeController extends Controller {
  async index() {
    const { ctx } = this;
    // ctx.body = "hi, egg";
    const user = await ctx.service.user.find();
    ctx.body = user;
  }
}

module.exports = HomeController;
//app/router.js
module.exports = app => {
  const { router, controller } = app;
  router.get('/', controller.home.index);
};
egg.js常用方法:
  1. get 查找一条
let result = await this.app.mysql.get("user",{id:1})
  1. 查找数据的另一种方式
let result = await this.app.mysql.select("user",{
where:{id:1}
})
  1. 增加数据
let result = await this.app.mysql.insert("user",{username:"lisi",password:"1234"})
  1. 修改数据的第一种方式:根据主键修改
let result = await this.app.mysql.update('user',{ id:2, username:'赵四' });
//修改数据的第二种方式:通过 sql 来修改数据
let results=await this.app.mysql.query('update user set username = ? where id = ?',["王五",2]);
  1. 删除数据
let result= await this.app.mysql.delete('user',{ id:3 });
  1. 执行 sql
this.app.mysql.query(sql,values);
  1. mysql 事务
const conn=await this.app.mysql.beginTransaction();
try{
    await conn.insert('user',{'username':'xiao1','password':'1111'});
    await conn.update('user',{id:2,username:'黑子'});
    await conn.commit();  //提交事务
}catch(err){
    await conn.rollback();//回滚事务
    throw err;
}

8.Egg middleware 中间件

中间件:匹配路由前、匹配路由完成做的一系列操作。
一般来说中间件也会有自己的配置。在框架中,一个完整的中间件是包含了配置处理的。我们约定一个中间件是一个放置在 app/middleware 目录下的单独文件,它需要 exports 一个普通的 function,接受两个参数:

options: 中间件的配置项,框架会将 app.config[${middlewareName}] 传递进来。
app: 当前应用 Application 的实例。

  1. app/middleware 下面新建 forbidip.js 内容如下:
/**
 * 拦截特殊ip,防止爬虫
 * options: 中间件的配置项,框架会将 app.config[${middlewareName}] 传递进来
 * app: 当前应用 Application 的实例
 * 每次路由变化都会触发 中间件
 */
module.exports = (options, app) => {
  return async function forbidipMiddleware(ctx, next) {
    /**
     * 要屏蔽的ip
     * 1、从数据库获取
     * 2、从参数传入
     */
    console.log(options);
    console.log(ctx.request.ip);
    // 获取客户端的ip
    var sourceIp = ctx.request.ip;
    const match = options.ip.some(val => {
      if(val == sourceIp){
        return true;
      }
    });
 
    if(match){
      ctx.status = 403;
      // message 接受的字符不能有中文,否则会报错
      // ctx.message = 'Your IP has been blocked';
      ctx.body = '您的ip已经被屏蔽';
    }else{
      await next();
    }
  }
}
  1. 找到 config.default.js 配置当前项目需要使用的中间件以及中间件的参数
// 增加配置中间件
config.middleware = ['forbidip'];
 
// 给pforbidip中间件传入的参数
config.forbidip = {
  ip: ['192.168.0.10']
}
  1. 注意 传入的中间件名称 需要与 中间件文件名 相同
    在这里插入图片描述

9.Egg 安全机制 CSRF 的防范

方式一:
在 controller 中通过参数传入模板

/**
 * 方式一:在 controller 中通过参数传入模板
 * this.ctx.csrf 用户访问这个页面的时候生成一个密钥
 */
await ctx.render('home', {
  csrf: this.ctx.csrf
});

方式二:
通过创建中间件,设置模板全局变量
app/middleware/auth.js

/**
 * 同步表单的 CSRF 校验
 */
module.exports = (options, app) => {
  return async function auth(ctx, next) {
    // 设置模板全局变量
    ctx.state.csrf = ctx.csrf;
 
    await next();
  }
}

config/config.default.js

// 增加配置中间件
config.middleware = ['auth'];

controller 中使用

/**
 * 方式二:通过创建中间件,设置模板全局变量
 * config.middleware = [auth'];
 */
await ctx.render('home');

模板:
app/view/home.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>首页</title>
</head>
<body>
    <form action="/add?_csrf=<%=csrf%>" method="POST">
        <!-- 通过隐藏的表单域传值 -->
        <!-- <input type="hidden" name="_csrf" value="<%=csrf%>" /> -->
        用户名:<input type="text" name="username" /><br />
        密  码:<input type="password" name="password" /><br />
        <button type="submit">提交</button>
    </form>
</body>
</html>

注:配置 域白名单,跳过 crsf 检测
config/config.default.js

// 安全配置
config.security = {
  csrf: {
    enable: false,
    ignoreJSON: true
  },
  // 允许访问接口的白名单
  domainWhiteList: ['*'] // ['http://localhost:8080']
}

10.解决跨域问题

  1. 安装 egg-cors
npm i egg-cors --save
  1. 配置plugin.js
exports.cors = {
  enable: true,
  package: 'egg-cors',
};
  1. 配置config.default.js
 config.security = {
    csrf: {
      enable: false,
      ignoreJSON: true
    },
    domainWhiteList: ['*']//[]中放放出的白名单,*代表所有
  };
  config.cors = {
    origin:'*',
    allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH',
  };

11.生成token

  1. 安装egg-jwt
npm i --save egg-jwt
  1. 配置plugin.js
import { EggPlugin } from 'egg';

const plugin: EggPlugin = {
  jwt: {
    enable: true,
    package: "egg-jwt"
  }
};

export default plugin;
  1. 配置config.default.js
config.jwt = {
  secret: "123456"//自定义 token 的加密条件字符串
};
  1. 创建访问路由
import { Application } from 'egg';

export default (app: Application) => {
  const { controller, router, jwt } = app;
  
  //正常路由
  router.post('/admin/login', controller.admin.login);
  
  /* 
  * 这里的第二个对象不再是控制器,而是 jwt 验证对象,第三个地方才是控制器
  * 只有在需要验证 token 的路由才需要第二个 是 jwt 否则第二个对象为控制器
  **/
  router.post('/admin',jwt, controller.admin.index); 
};
  1. 编写控制器,在根目录下的 app/controller/home.ts 编写内容:
//登录 根据用户名查询
  async getUserByUsername() {
    const { ctx, app } = this;
    const { username, password } = ctx.query;
    let user = await ctx.service.user.getUserByUsername(username);
    if (user.user.length) {
      //数据库中有该用户
      let service_password = user.user[0].password;
      if (password === service_password) {
        //生成 token 的方式
        const token = app.jwt.sign({ username }, app.config.jwt.secret);
        // 生成的token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE1NjAzNDY5MDN9.B95GqH-fdRpyZIE5g_T0l8RgzNyWOyXepkLiynWqrJg

        // 返回 token 到前端
        ctx.body = {
          code: 0,
          data: token,
          message: "登录成功",
        };
      } else {
        ctx.throw(500, "密码错误,请重新输入");
        ctx.body = ctx.request.body;
      }
    } else {
      //数据库中没有该用户
      ctx.throw(500, "用户名或密码错误");
      ctx.body = ctx.request.body;
    }
  }

// 验证token,请求时在header配置 Authorization=`Bearer ${token}`
// 特别注意:token不能直接发送,要在前面加上Bearer字符串和一个空格

//访问admin数据时进行验证token,并且解析 token 的数据
  public async index() {
  
    const { ctx ,app} = this;
    
    console.log(ctx.state.user);
    /* 
	* 打印内容为:{ username : 'admin', iat: 1560346903 }
	* iat 为过期时间,可以单独写中间件验证,这里不做细究
	* 除了 iat 之后,其余的为当时存储的数据
	**/
    
    ctx.body = {code:0,msg:'验证成功'};
  }
  1. 前端请求的时候需要在 headers 里面上默认的验证字断 Authorization 就可以了

12.egg-mysql做分页查询

//获取users表中所有用户
  async getUserList(username, page, pageSize) {
    let params = {
      limit: Number(pageSize), // 返回数据量
      offset: (page - 1) * pageSize, // 数据偏移量
    };
    if (username) params.where = { username };
    const user = await this.app.mysql.select("users", params);//数据
    const totalCount = await this.app.mysql.count("users");//数量
    return { data: user, count: totalCount };
  }

13.Egg.js 实现向服务器上传图片

  1. 安装时间处理 及 压缩 模块
npm i --save silly-datetime pump formidable
  1. 文件保存路径.
    config/config.default.js
config.uploadDir = 'app/public/avatar/upload';
  1. app/service/user.js
'use strict';
 
const Service = require("egg").Service;
const path = require("path");
const sd = require("silly-datetime");
const mkdirp = require("mkdirp");
 
class UserService extends Service {
  /**
   * 获取文件上传目录
   * @param {*} filename
   */
  async getUploadFile(id, filename) {
    // 1.获取当前日期
    let day = sd.format(new Date(), "YYYYMMDD");
    // 2.创建图片保存的路径
    let dir = path.join(this.config.uploadDir, day);
    await mkdirp(dir); // 不存在就创建目录
    let date = Date.now(); // 毫秒数
    // 返回图片保存的路径
    let uploadDir = path.join(dir, date + path.extname(filename));
    // app\public\avatar\upload\20200312\1536895331666.png
    let headerImg = this.ctx.origin + uploadDir.slice(3).replace(/\\/g, "/");
 
    const user = await this.app.mysql.update("users", {
      id,
      headerImg,
    });
    return {
      uploadDir,
      saveDir: headerImg,
    };
  }
}
 
module.exports = UserService;
  1. 调用 app/controller/user.js
"use strict";

const Controller = require("egg").Controller;
const fs = require("fs");
const pump = require("pump");
const formidable = require("formidable");

  async parse(req) {
    const form = new formidable.IncomingForm();
    return new Promise((resolve, reject) => {
      form.parse(req, (err, fields, files) => {
        resolve({ fields, files });
      });
    });
  }

  // 保存头像/封面
  async saveAvatar() {
    const { ctx } = this;
    const parts = ctx.multipart({ autoFields: true });

    const extraParams = await this.parse(ctx.req);
    const id = extraParams.fields.id;

    let files = {};
    let stream;
    while ((stream = await parts()) != null) {
      if (!stream.filename) {
        break;
      }
      const fieldname = stream.fieldname; // file表单的名字
      // 上传图片的目录
      const dir = await this.service.user.getUploadFile(id, stream.filename);
      const target = dir.uploadDir;
      const writeStream = fs.createWriteStream(target);

      await pump(stream, writeStream);

      files = Object.assign(files, {
        [fieldname]: dir.saveDir,
      });
    }

    if (Object.keys(files).length > 0) {
      ctx.body = {
        code: 200,
        message: "图片上传成功",
        data: files,
      };
    } else {
      ctx.body = {
        code: 500,
        message: "图片上传失败",
        data: {},
      };
    }
  }
  1. 配置路由
router.post("/user/saveavatar", controller.user.saveAvatar);
  1. vue UI组件
<el-form-item label="头像">
 <el-upload
    class="avatar-uploader"
    :headers="uploadHeader"
    :action="uploadAction"
    :data="{ id: formData.id }"
    :show-file-list="false"
    :on-success="handleAvatarSuccess"
    :before-upload="beforeAvatarUpload"
  >
    <img v-if="imageUrl" :src="imageUrl" class="avatar" />
    <i v-else class="el-icon-plus avatar-uploader-icon"></i>
  </el-upload>
</el-form-item>

<script>
export default {
  data() {
    return {
      uploadAction: 'http://127.0.0.1:7001/user/saveavatar',
      uploadHeader: { Authorization: localStorage.getItem('Authorization') || '' },
      imageUrl: '',
    };
  },
  methods: {
    handleAvatarSuccess(res, file) {
      this.imageUrl = URL.createObjectURL(file.raw);
      this.$message.success('头像上传成功');
      this.$router.push('/mycenter');
    },
    beforeAvatarUpload(file) {
      const isJPG = file.type === 'image/jpeg';
      const isLt2M = file.size / 1024 / 1024 < 2;

      if (!isJPG) {
        this.$message.error('上传头像图片只能是 JPG 格式!');
      }
      if (!isLt2M) {
        this.$message.error('上传头像图片大小不能超过 2MB!');
      }
      return isJPG && isLt2M;
    },
  },
};
</script>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值