系统内有一堆对实体的基本的增删改查的操作,所以想抽象一个基类
'use strict';
const Service = require('egg').Service;
class LogService extends Service {
// index======================================================================================================>
async index(payload) {
const { ctx } = this;
const { helper } = ctx;
let { page, limit, isPaging, where, sortby } = payload;
if (where) {
where = JSON.parse(where);
}
const selectObj = { where };
if (sortby) {
sortby = sortby.replace(', ', ',').replace(' ,', ',');
const order = sortby.split(',');
selectObj.order = order.map(n => n.split(' '));
} else {
selectObj.order = [[ 'id', 'DESC' ]];
}
if (!isPaging) {
limit = helper.toInt(limit) || ctx.app.config.default_limit;
page = helper.toInt(page) || ctx.app.config.default_page;
selectObj.offset = (page - 1) * limit;
selectObj.limit = limit;
} else {
page = ctx.app.config.default_page;
}
const res = await ctx.model.Log.findAndCountAll(selectObj);
return { count: res.count, list: res.rows, limit, currentPage: page };
}
// show======================================================================================================>
async show(id) {
const { ctx } = this;
const log = await ctx.model.Log.findById(id);
if (!log) {
ctx.throw(404, ctx.__('Data not found'));
}
return log;
}
// create======================================================================================================>
async create(payload) {
const { ctx } = this;
return ctx.model.Log.create(payload);
}
// destroy======================================================================================================>
async destroy(id) {
const { ctx } = this;
const { Log } = ctx.model;
const log = await Log.findById(id);
if (!log) {
ctx.throw(404, ctx.__('Data not found'));
}
return log.destroy();
}
// update======================================================================================================>
async update(id, payload) {
const { ctx } = this;
const { Log } = ctx.model;
const log = await Log.findById(id);
let canSave = true;
if (!log) {
canSave = false;
ctx.throw(404, ctx.__('Data not found'));
}
if (canSave) {
const keyArr = [ 'remark' ];
const willUpdate = ctx.helper.getKeyObj({ obj: payload, keyArr });
await log.update(willUpdate);
return log;
}
}
}
module.exports = LogService;
对应的单元测试
'use strict';
const { app } = require('egg-mock/bootstrap');
const assert = require('assert-extends');
const baseModel = 'log';
describe.only('LogService test', () => {
describe('index(payload)', () => {
it('should limit 2 data and order id DESC', async () => {
// 通过 factory-girl 快速创建 数据 对象到数据库中
const list = await app.factory.createMany(baseModel, 50);
const returnObj = await app.mockContext().service[baseModel].index({ limit: 2 });
assert(returnObj.count === 50);
assert(returnObj.list.length === 2);
assert(returnObj.list[0].creatorName === list[49].creatorName);
assert(returnObj.list[0].remark === list[49].remark);
assert(returnObj.list[0].id > returnObj.list[1].id);
assert(returnObj.limit === 2);
assert(returnObj.currentPage === 1);
});
it('should just one data', async () => {
// 通过 factory-girl 快速创建 数据 对象到数据库中
const list = await app.factory.createMany(baseModel, 50);
const remark = list[0].remark;
const returnObj = await app.mockContext().service[baseModel].index({ where: JSON.stringify({
remark,
}) });
assert(returnObj.count === 1);
assert(returnObj.list.length === 1);
assert(returnObj.list[0].remark === remark);
});
it('should limit 20 data', async () => {
// 通过 factory-girl 快速创建 数据 对象到数据库中
const list = await app.factory.createMany(baseModel, 50);
const returnObj = await app.mockContext().service[baseModel].index({});
assert(returnObj.count === 50);
assert(returnObj.list.length === 20);
assert(returnObj.list[0].creatorName === list[49].creatorName);
assert(returnObj.list[0].remark === list[49].remark);
assert(returnObj.limit === 20);
assert(returnObj.currentPage === 1);
});
it('should in page 3', async () => {
// 通过 factory-girl 快速创建 数据 对象到数据库中
const list = await app.factory.createMany(baseModel, 50);
const returnObj = await app.mockContext().service[baseModel].index({ page: 3 });
assert(returnObj.count === 50);
assert(returnObj.list.length === 10);
assert(returnObj.list[0].creatorName === list[9].creatorName);
assert(returnObj.list[0].remark === list[9].remark);
assert(returnObj.limit === 20);
assert(returnObj.currentPage === 3);
});
it('should paging', async () => {
// 通过 factory-girl 快速创建 数据 对象到数据库中
await app.factory.createMany(baseModel, 50);
const returnObj = await app.mockContext().service[baseModel].index({ page: 4, isPaging: true });
assert(returnObj.count === 50);
assert(returnObj.list.length === 50);
assert(!returnObj.limit);
assert(returnObj.currentPage === 1);
});
it('should order creatorName asc and remark desc', async () => {
// 通过 factory-girl 快速创建 数据 对象到数据库中
const list = await app.factory.createMany(baseModel, 50);
const returnObj = await app.mockContext().service[baseModel].index({ sortby: '`creatorName` asc, remark desc' });
assert(returnObj.count === 50);
assert(returnObj.list.length === 20);
assert(returnObj.limit === 20);
assert(returnObj.currentPage === 1);
assert(returnObj.list[0].creatorName === list[0].creatorName);
assert(returnObj.list[0].remark === list[0].remark);
});
});
describe('show(id)', () => {
it('should get exists data', async () => {
const log = await app.factory.create(baseModel);
const returnObj = await app.mockContext().service[baseModel].show(log.id);
assert(returnObj.remark === log.remark);
});
it('should not get no exists data', async () => {
const ctx = app.mockContext();
return assert.asyncThrows(
async () => {
await ctx.service[baseModel].show(10000);
},
/^NotFoundError: Data not found$/
);
});
});
describe('create(payload)', () => {
it('Should can create', async () => {
const willSave = {
remark: 'this is 这是',
creatorName: 'creatorName',
userId: 1,
creatorId: 1,
};
const returnObj = await app.mockContext().service[baseModel].create(willSave);
assert(returnObj.id);
assert(returnObj.creatorName === willSave.creatorName);
assert(returnObj.remark === willSave.remark);
assert(returnObj.userId === willSave.userId);
assert(returnObj.creatorId === willSave.creatorId);
});
it('Validate should can‘t create', async () => {
const willSaveValidate = {
creatorName: 'creatorName',
};
return assert.asyncThrows(
async () => {
await app.mockContext().service[baseModel].create(willSaveValidate);
},
/^SequelizeValidationError:(?:.|[\r\n])*$/
);
});
});
describe('update(id, payload)', () => {
it('should update succeed', async () => {
const log1 = await app.factory.create(baseModel);
const willUpdate1 = {
remark: 'update 更新',
};
const willUpdate2 = {
creatorName: 'creatorName 更新',
};
// 创建 ctx
const ctx = app.mockContext();
const logService = ctx.service[baseModel];
let returnObj = await logService.update(log1.id, willUpdate1);
assert(returnObj.remark === willUpdate1.remark);
assert(returnObj.userId === log1.userId);
assert(returnObj.creatorName === log1.creatorName);
assert(returnObj.creatorId === log1.creatorId);
returnObj = await logService.show(log1.id);
assert(returnObj.remark === willUpdate1.remark);
assert(returnObj.userId === log1.userId);
assert(returnObj.creatorName === log1.creatorName);
assert(returnObj.creatorId === log1.creatorId);
returnObj = await logService.update(log1.id, willUpdate2);
assert(returnObj.remark === willUpdate1.remark);
assert(returnObj.userId === log1.userId);
assert(returnObj.creatorName === log1.creatorName);
assert(returnObj.creatorId === log1.creatorId);
assert(returnObj.creatorName !== willUpdate2.creatorName);
returnObj = await logService.show(log1.id);
assert(returnObj.remark === willUpdate1.remark);
assert(returnObj.userId === log1.userId);
assert(returnObj.creatorName === log1.creatorName);
assert(returnObj.creatorId === log1.creatorId);
});
it('should not update succeed not exist data', async () => {
return assert.asyncThrows(
async () => {
await app.mockContext().service[baseModel].update(10000, {});
},
/^NotFoundError: Data not found$/
);
});
});
describe('destroy(id)', () => {
it('should destroy succeed', async () => {
const log = await app.factory.create(baseModel);
// 创建 ctx
const ctx = app.mockContext();
const logService = ctx.service[baseModel];
const returnObj = await logService.show(log.id);
assert(returnObj);
await logService.destroy(log.id);
return assert.asyncThrows(
async () => {
await logService.show(log.id);
},
/^NotFoundError: Data not found$/
);
});
it('should not destroy succeed', async () => {
return assert.asyncThrows(
async () => {
await app.mockContext().service[baseModel].destroy(10000);
},
/^NotFoundError: Data not found$/
);
});
});
});
然后应该有一堆的基本也是这样的实体,所以想抽象一个基类
'use strict';
const Service = require('egg').Service;
class BaseService extends Service {
constructor({ model, updateKeyArr }) {
super();
this.model = model;
this.updateKeyArr = updateKeyArr;
}
// index======================================================================================================>
async index(payload) {
const { ctx } = this;
const { helper } = ctx;
let { page, limit, isPaging, where, sortby } = payload;
if (where) {
where = JSON.parse(where);
}
const selectObj = { where };
if (sortby) {
sortby = sortby.replace(', ', ',').replace(' ,', ',');
const order = sortby.split(',');
selectObj.order = order.map(n => n.split(' '));
} else {
selectObj.order = [[ 'id', 'DESC' ]];
}
if (!isPaging) {
limit = helper.toInt(limit) || ctx.app.config.default_limit;
page = helper.toInt(page) || ctx.app.config.default_page;
selectObj.offset = (page - 1) * limit;
selectObj.limit = limit;
} else {
page = ctx.app.config.default_page;
}
const res = await ctx.model[this.model].findAndCountAll(selectObj);
return { count: res.count, list: res.rows, limit, currentPage: page };
}
// show======================================================================================================>
async show(id) {
const { ctx } = this;
const entity = await ctx.model[this.model].findById(id);
if (!entity) {
ctx.throw(404, ctx.__('Data not found'));
}
return entity;
}
// create======================================================================================================>
async create(payload) {
const { ctx } = this;
return ctx.model[this.model].create(payload);
}
// destroy======================================================================================================>
async destroy(id) {
const { ctx } = this;
const entity = await ctx.model[this.model].findById(id);
if (!entity) {
ctx.throw(404, ctx.__('Data not found'));
}
return entity.destroy();
}
// update======================================================================================================>
async update(id, payload) {
const { ctx } = this;
const entity = await ctx.model[this.model].findById(id);
let canSave = true;
if (!entity) {
canSave = false;
ctx.throw(404, ctx.__('Data not found'));
}
if (canSave) {
const willUpdate = ctx.helper.getKeyObj({ obj: payload, keyArr: this.updateKeyArr });
await entity.update(willUpdate);
return entity;
}
}
}
module.exports = BaseService;
LogService 就可以变成
'use strict';
const BaseService = require('../core/base_service');
class LogService extends BaseService {
constructor() {
super({
model: 'Log',
updateKeyArr: [ 'remark' ],
});
}
}
module.exports = LogService;
但是运行单元测试就全部变成这样。
14) LogService test
destroy(id)
should not destroy succeed:
TypeError: Cannot read property 'app' of undefined
at new BaseContextClass (node_modules/egg-core/lib/utils/base_context_class.js:25:20)
at new BaseContextClass (node_modules/egg/lib/core/base_context_class.js:13:1)
at new BaseService (app/core/base_service.js:8:5)
at new LogService (app/service/log.js:8:5)
at getInstance (node_modules/egg-core/lib/loader/context_loader.js:92:18)
at ClassLoader.get (node_modules/egg-core/lib/loader/context_loader.js:27:22)
at assert.asyncThrows (test/app/service/log.test.js:209:17)
at /Users/thomas/Documents/projects/hc_user/node_modules/assert-extends/index.js:10:13
at new Promise (<anonymous>)
at Function.assert.asyncThrows (node_modules/assert-extends/index.js:7:10)
at Context.it (test/app/service/log.test.js:207:21)
[use `--full-trace` to display the full stack trace]
看会Egg.js 的教程 https://eggjs.org/zh-cn/basics/controller.html 也有对controller 有类似的操作。想Service应该也是可以的吧? 也有找到 https://cnodejs.org/topic/5ae7c43202591040485ba997 这样一个类似的。 为什么 这里不可以?请问大神们,哪里出错啦?
require('egg').Service
因为这个基类的 super 入参是要求传递 app 的,而你直接 super()
了。
这类问题,建议提炼一个最小可复现仓库,来反馈问题。 譬如你这个抽象 Service 基类,其实就一个简单的示例就够了,不用贴那么多代码,会导致其他人看的效率不高的。
其实这段报错表述的挺清楚的,点击过去看看源码一下就知道了。
TypeError: Cannot read property 'app' of undefined
at new BaseContextClass (node_modules/egg-core/lib/utils/base_context_class.js:25:20)
at new BaseContextClass (node_modules/egg/lib/core/base_context_class.js:13:1)
at new BaseService (app/core/base_service.js:8:5)
at new LogService (app/service/log.js:8:5)
谢谢 @atian25
这个app 是在哪里传进去呢? 有一些可以参考的项目吗?
module.exports = app => {
return class BaseService extends app.Service {
// implement
constructor({ a, b }) {
super(app);
this.a = a;
this.b = b;
}
};
};
是这样吗? 那如果我想用下面这种方式的话,是没有办法获取吗?看文档的介绍,好似只有从this中获取的方式
const Service = require('egg').Service;
class BaseService extends Service {
constructor({ a, b }) {
super();
this.a = a;
this.b = b;
}
Service 是 egg loader 实例化的,它只会传递 ctx 进去的,你没办法自己去实例化并传递你自己的入参的。
class BaseService extends Service {
constructor({ ctx, model }) {
super(ctx);
this.model = model;
}
}
class LogService extends BaseService {
constructor(ctx) {
super({
ctx,
model: 'Log',
})
}
}
或者:
class BaseService extends Service {
init({ model }) {
this.model = model;
}
}
class LogService extends BaseService {
constructor(app) {
super(app);
this.init({ model: 'Log' });
}
}
看 base_context_class 的源码,貌似是要传context呢
'use strict';
/**
* BaseContextClass is a base class that can be extended,
* it's instantiated in context level,
* {@link Helper}, {@link Service} is extending it.
*/
class BaseContextClass {
/**
* @constructor
* @param {Context} ctx - context instance
* @since 1.0.0
*/
constructor(ctx) {
/**
* @member {Context} BaseContextClass#ctx
* @since 1.0.0
*/
this.ctx = ctx;
/**
* @member {Application} BaseContextClass#app
* @since 1.0.0
*/
this.app = ctx.app;
/**
* @member {Config} BaseContextClass#config
* @since 1.0.0
*/
this.config = ctx.app.config;
/**
* @member {Service} BaseContextClass#service
* @since 1.0.0
*/
this.service = ctx.service;
}
}
module.exports = BaseContextClass;
但是在 请求时的 Context 实例和 Application.createAnonymousContext() 获取的 context 实例应该是不一样的吧? 那这个参考项目内的 在Service 层用 ctx.throw 这些方法 会否有 问题?
嗯,说错,是 ctx
createAnonymousContext() 是不带用户信息的,因为是非请求的。你啥情况下会用到它?
@atian25 原来是这样,受教了,非常感谢
@atian25 我这边也遇到了同样的问题,我目前使用的getter解决的
class BaseService extends Service {
get model() {
return 'Model';
}
index(where) {
return this.ctx.model[ this.model ].find(where);
}
}
class BusinessClass extends BaseService {
// 通过getter方法来实现传入model
get model() {
return 'Demo';
}
}
请教下大神,这样用可能会造成什么其他问题么
没啥问题,