使用egg搭建egg脚手架

安装项目依赖
$ npm init
$ npm i egg --save
$ npm i egg-bin --save-dev
复制代码

添加 npm scripts 到 package.json:

{
  "scripts": {
    "dev": "egg-bin dev"
  }
}
复制代码
编写Controller
// app/controller/home.js
const { Controller } = require('egg');

class HomeConstroller extends Controller {
    async index() {
        let { ctx } = this;
        ctx.body = 'home';
    }
}

module.exports = HomeConstroller;
复制代码
配置路由映射
// app/router.js
module.exports = app => {
  const { router, controller } = app;
  router.get('/', controller.home.index);
};
复制代码
配置文件
// config/config.default.js
exports.keys = '<此处改为你自己的 Cookie 安全字符串>';
// 写法2
module.exports = app => {
    let config = {};
    config.keys = '<此处改为你自己的 Cookie 安全字符串>';
    return config;
}
复制代码

启动

$ npm run dev
$ open localhost:7001
复制代码
静态文件

Egg 内置了 static 插件,线上环境建议部署到 CDN,无需该插件。

static 插件默认映射 /public/* -> app/public/* 目录

此处,我们把静态资源都放到 app/public 目录即可:

app/public
├── css
│   └── news.css
└── js
    ├── lib.js
    └── news.js
复制代码

静态文件中间件:用来拦截对静态文件的请求,如果是静态文件的话,直接把文件从硬盘上读出来,返回给客户端。

模板渲染

安装模版引擎插件

$ yarn add egg-view-nunjucks --save
复制代码

启用插件


// config/plugin.js
exports.nunjucks = {
  enable: true,
  package: 'egg-view-nunjucks'
};
复制代码

添加 view 配置

// config/config.default.js
exports.keys = '<此处改为你自己的 Cookie 安全字符串>';
// 添加 view 配置
exports.view = {
  defaultExtension: '.html',     // 注意是 .html 要记得point
  defaultViewEngine: 'nunjucks', // 和plugin配置对应
  mapping: {
    '.tpl': 'nunjucks',
  },
};
复制代码

异步render 因为读文件readfile是异步的

await ctx.render('news', { list });
// 查找文件路径 读取文件内容 把模版和数据混合为html
复制代码
  • 在egg中,默认支持防csrf, 在客户端请求服务器的时候,服务器会向客户端发送一个csrfToken
  • 下次客户端再次访问服务端的时候,服务器会校验这个token

防csrf的token是如何下发的

场景:银行转账

csrf流程--生成链接诱导合法用户点击,点击后会发送请求

登陆--返回cookie放在客户端--客户端再次发送请求时携带cookie--客户端进行校验token

防止措施:登陆--返回cookie放在客户端--客户端再次发送请求获取token,再次发送请求时携带token进行转账--客户端进行校验token--token失效

cookie加密

let count = ctx.cookies.get('count');
count = count ? Number(count) : 0;
++count;
let res = crypto.createHmac('sha256', this.config.keys).update(count + '1');
ctx.cookies.set('count', res.digest('hex'));
ctx.body = ctx.cookies.get('count');

复制代码
读取远程接口服务 service

在实际应用中,Controller 一般不会自己产出数据,也不会包含复杂的逻辑,复杂的过程应抽象为业务逻辑层 Service。 超时问题:curl超时,网上找到修改httpAgent.timeout的方法,但是试了还是不可以,后http://localhost换成 http://127.0.0.1解决

helper使用:app/extend/helper.js 文件名字是规定好的,不能随便写 可以在controler里通过ctx.helper调用,也可以在html模版里直接调用helper

中间件编写
// app/middleware/中间件文件名 注意规避关键字
module.exports = (options, app) => {    // options 为本中间件的配置对象
    // 判断是否是chrome访问,如果是则返回403
    return async function (ctx, next) { // next 表示调用下一个中间件
        let userAgent = ctx.get('user-agent') || '';
        let mached = options.ua.some(ua => {
            return ua.test(userAgent);
        });
        if (mached) {
            ctx.body = '403';
        } else {
            await next();
        }
    }
}
复制代码

配置开启的中间件 并配置对应的options

什么时候用中间件?

客户端-->中间件-->next()-->控制器, 所以,当有在控制器之前执行逻辑的需求时,我们使用中间件

运行环境

两种指定方式:

  1. config/env文件 local
  2. 通过 EGG_SERVER_ENV 环境变量来指定,代码里通过app.config.env来读取该环境变量

支持config.prod.js / config.local.js写法,加载顺序,先加载config.default.js,再根据env读取对应的config文件

"scripts": {
    "dev": "SET EGG_SERVER_ENV=local egg-bin dev"
  },
复制代码
单元测试

单元测试的优点:

  1. 代码质量持续有保障
  2. 重构正确性保障
  3. 增强自信心
  4. 自动化运行

测试框架 官方推荐 mochajs mocha教程请参考阮一峰老师的文章:www.ruanyifeng.com/blog/2015/1… power-assert

{
  "scripts": {
    "test": "egg-bin test"
  }
}
复制代码
// test/app/controller/home/home.test.js
const assert = require('assert');

describe('加法函数的测试', function () {
    it('1 加 1 应该等于 2', function () {
        assert(1 + 1 == 2);
    });
});
复制代码
mock

正常来说,如果要完整手写一个 app 创建和启动代码,还是需要写一段初始化脚本的, 并且还需要在测试跑完之后做一些清理工作,如删除临时文件,销毁 app。 常常还有模拟各种网络异常,服务访问异常等特殊情况。也就是快速编写一个单元测试; egg单独为框架抽取了一个测试 mock 辅助模块:egg-mock, 有了它我们就可以非常快速地编写一个 app 的单元测试,并且还能快速创建一个 ctx 来测试它的属性、方法和 Service 等。

const mock = require('egg-mock');
const assert = require('assert');
describe('加法函数的测试', function () {
    it('1 加 1 应该等于 2', function () {
        assert(1 + 1 == 2);
    });
});

复制代码

钩子 Mocha 使用 before/after/beforeEach/afterEach 来处理前置后置任务,基本能处理所有问题。 每个用例会按 before -> beforeEach -> it -> afterEach -> after 的顺序执行,而且可以定义多个。

describe('test/app/controller/home.test.js', () => {
    // 全部开始前
    before(() => {
        console.log('this is before')
    })
    // 每个开始前
    beforeEach(() => {
        console.log('this is beforeEach')
    })
    // 全部结束后
    after(() => {
        console.log('this is after')
    })
    // 每个结束后
    afterEach(() => {
        console.log('this is afterEach')
    })
    it('test1', () => {
        console.log('this is test1')
    })
    it('test2', () => {
        console.log('this is test2')
    })
    it('test3', () => {
        console.log('this is test2')
    })
    it('test4', () => {
        console.log('this is test2')
    })
})
复制代码
异步测试

异步测试有三种方式

  1. promise
  2. callback
  3. async await
const { app, mock, assert } = require('egg-mock/bootstrap');

describe('test/app/controller/home.test.js', () => {
    it('promise', () => {
        return app.httpRequest().get('/home').expect(200).expect('home');
    })
    it('callback', (done) => {
        app.httpRequest().get('/home').expect(200, done);
    })
    it('async&await', async function () {
        await app.httpRequest().get('/home').expect(200).expect('home');;
    })
})
复制代码
describe('test/app/controller/home.test.js', () => {
    it('async&await', async function () {
        let result = await app.httpRequest().get('/home');
        assert(result.status == 200);
        assert(result.text == 'home');
    })
})
复制代码

如何测试控制器ctx

describe('test/app/controller/home.test.js', () => {
    it('test ctx', async function () {
        // 通过app模拟创建出ctx  
        let ctx = await app.mockContext({
            session: { name: 'mmm' }
        })
        assert(ctx.method == 'GET')
        assert(ctx.url == '/')
        assert(ctx.session.name == 'mmm')
    })
})
复制代码
session 和 cookie

框架内置了 Session 插件,给我们提供了 ctx.session 来访问或者修改当前用户 Session 。 如果要删除它,直接将它赋值为 null。 以下是防csrf的手动实现

 //app/controller/user.js
 // 打开添加用户页面
    async add() {
        let { ctx } = this;
        let csrf = Date.now() + Math.random() + '';
        ctx.session.csrf = csrf;
        await ctx.render('user/add', { csrf });
    }
    // 确定添加用户
    async doAdd() {
        let { ctx } = this;
        const user = ctx.request.body; // 得到请求体对象
        if (user.csrf !== ctx.session.csrf) {
            ctx.body = 'csrf error';
            return;
        }
        delete user.csrf;
        ctx.session.csrf = null;
        user.id = users.length > 0 ? users[users.length - 1].id + 1 : 1;
        users.push(user);
        ctx.body = user;
    }
复制代码
<!-- app/view/user/add.html -->
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>add</title>
</head>

<body>
    <form action="/user/doAdd" method="POST">
        用户名:<input type="text" name="username" />
        <input value="提交" type="submit">
        <input name="csrf" type="hidden" value="{{csrf}}" />
    </form>
</body>

</html>
复制代码
 config.session = {
        renew: true, // 每次请求服务器,是否重新生成session
    };
复制代码

写入session时,注意两点:

  • 不要以 _ 开头
  • 不能为 isNew

Session 默认存放在 Cookie 中,但是如果我们的 Session 对象过于庞大,就会带来一些额外的问题:

  1. 浏览器通常都有限制最大的 Cookie 长度,当设置的 Session 过大时,浏览器可能拒绝保存。
  2. Cookie 在每次请求时都会带上,当 Session 过大时,每次请求都要额外带上庞大的 Cookie 信息。

框架提供了将 Session 存储到除了 Cookie 之外的其他存储的扩展方案,我们只需要设置 app.sessionStore 即可将 Session 存储到指定的存储中。

测试controller user
// test/app/controller/user.test.js
const { app, mock, assert } = require('egg-mock/bootstrap');

describe('app/controller/user.js', () => {
    it('test get /user/add', async () => {
        let result = await app.httpRequest().get('/user/add');
        assert(result.status === 200);
        assert(result.text.indexOf('username') !== -1);
    })
    it('test get /user/list', async () => {
        let result = await app.httpRequest().get('/user');
        assert(result.status === 200);
    })
    it('test post /user/doAdd', async () => {
        let result = await app.httpRequest().post('/user/doAdd').send(`username=mmmm`);
        assert(result.status === 200);
        assert(result.body.id === 1);
    })
})
复制代码
测试service
// test/app/service/news.test.js
const { app, mock, assert } = require('egg-mock/bootstrap');
describe('test app/service/news.js', () => {
    it('test news service', async () => {
        let ctx = app.mockContext();
        let { code, data } = await ctx.service.news.list();
        assert(code === 0);
        assert(data.length === 7);
    })
})
复制代码
测试扩展

application中exports 上挂载的变量可以直接通过app访问到

// app/extend/application.js
// 实现一个全局缓存

let cacheData = {};
exports.cache = {
    get(key) {
        return cacheData[key];
    },
    set(key, value) {
        cacheData[key] = value;
    }
}
// app/extend/application.test.js
const { app, assert, mock } = require('egg-mock/bootstrap');

describe('app/extend/application.js', () => {
    it('test application/cache', async () => {
        app.cache.set('name', 'mmm');
        assert(app.cache.get('name') === 'mmm');
    })
})
复制代码

context 中 exports 上挂载的变量可以直接通过ctx访问到

// app/extend/context.js
// 向context添加一个方法,用来获取accept-language请求头
//  这里不要用箭头函数
exports.language = function () {
    return this.get('accept-language');
}
// app/extend/context.test.js
const { app, assert, mock } = require('egg-mock/bootstrap');

describe('app/extend/context.js', () => {
    it('test acceptlanguage', async () => {
        let cxt = app.mockContext({
            headers: { 'accept-language': 'zh-cn' }
        })
        assert(cxt.language() === 'zh-cn');
    })
})
复制代码

request 中 exports 上挂载的变量可以直接通过ctx.request访问到

// app/extend/request.js
module.exports = {
    get isChrome() {        // 前加get可以通过直接访问属性的方式取值
        let userAgent = this.get('User-Agent').toLowerCase();
        return userAgent.includes('chrome');
    }
}
// app/extend/request.test.js
const { app, assert, mock } = require('egg-mock/bootstrap');

describe('app/extend/request.js', () => {
    it('test isChrome', async () => {
        let cxt = app.mockContext({
            headers: { 'User-Agent': 'chrome' }
        })
        assert(cxt.request.isChrome === true);
    })
})
复制代码
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值