使用egg搭建egg腳手架

安裝項目依賴

$ npm init
$ npm i egg --save
$ npm i egg-bin --save-dev
複製代碼

添加 npm scripts 到 package.json:css

{
  "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;
}
複製代碼

啓動html

$ npm run dev
$ open localhost:7001
複製代碼

靜態文件

Egg 內置了 static 插件,線上環境建議部署到 CDN,無需該插件。chrome

static 插件默認映射 /public/* -> app/public/* 目錄npm

此處,咱們把靜態資源都放到 app/public 目錄便可:json

app/public
├── css
│   └── news.css
└── js
    ├── lib.js
    └── news.js
複製代碼

靜態文件中間件:用來攔截對靜態文件的請求,若是是靜態文件的話,直接把文件從硬盤上讀出來,返回給客戶端。bootstrap

模板渲染

安裝模版引擎插件promise

$ 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);
    })
})
複製代碼
相關文章
相關標籤/搜索