$ npm init
$ npm i egg --save
$ npm i egg-bin --save-dev
複製代碼
添加 npm scripts 到 package.json:css
{
"scripts": {
"dev": "egg-bin dev"
}
}
複製代碼
// 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
複製代碼
防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');
複製代碼
在實際應用中,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()-->控制器, 因此,當有在控制器以前執行邏輯的需求時,咱們使用中間件
兩種指定方式:
支持config.prod.js / config.local.js寫法,加載順序,先加載config.default.js,再根據env讀取對應的config文件
"scripts": {
"dev": "SET EGG_SERVER_ENV=local egg-bin dev"
},
複製代碼
單元測試的優勢:
測試框架 官方推薦 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);
});
});
複製代碼
正常來講,若是要完整手寫一個 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')
})
})
複製代碼
異步測試有三種方式
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 插件,給咱們提供了 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時,注意兩點:
Session 默認存放在 Cookie 中,可是若是咱們的 Session 對象過於龐大,就會帶來一些額外的問題:
框架提供了將 Session 存儲到除了 Cookie 以外的其餘存儲的擴展方案,咱們只須要設置 app.sessionStore 便可將 Session 存儲到指定的存儲中。
// 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);
})
})
複製代碼
// 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);
})
})
複製代碼