Feathers.js 核心概念详解 - Services、Hooks、Channels
发布时间:2024-05-10
作者:一介布衣
标签:Feathers.js, Services, Hooks, Channels, 核心概念
前言
上一篇文章我们快速搭建了第一个 Feathers.js 应用,体验了它的强大功能。但是要真正掌握 Feathers.js,我们必须深入理解它的三个核心概念:Services(服务)、Hooks(钩子)和 Channels(频道)。
说实话,刚开始学 Feathers.js 的时候,我对这些概念也是一知半解。特别是 Hooks,总觉得和其他框架的中间件差不多。后来深入使用才发现,Feathers.js 的设计真的很巧妙,这三个概念相互配合,形成了一个非常优雅的架构。
今天我就来详细解析这三个核心概念,让大家真正理解 Feathers.js 的设计哲学。
Services(服务)- 数据操作的统一抽象
什么是 Service?
Service 是 Feathers.js 的核心概念,它代表一个数据资源的抽象。无论数据存储在内存、文件、数据库,还是来自第三方 API,Service 都提供统一的接口。
Service 的标准方法
每个 Service 都实现以下标准方法:
javascript
// 标准的 Service 接口
class MyService {
async find(params) {
// 查询多条记录
// 返回: { total: number, limit: number, skip: number, data: [] }
}
async get(id, params) {
// 根据 ID 获取单条记录
// 返回: object
}
async create(data, params) {
// 创建新记录
// 返回: object 或 array
}
async update(id, data, params) {
// 完全替换记录
// 返回: object
}
async patch(id, data, params) {
// 部分更新记录
// 返回: object
}
async remove(id, params) {
// 删除记录
// 返回: object
}
}
内存 Service 示例
让我们创建一个简单的内存 Service:
javascript
// src/services/todos/todos.class.js
class TodosService {
constructor() {
this.todos = [];
this.currentId = 0;
}
async find(params) {
const { query = {} } = params;
let result = [...this.todos];
// 简单的过滤
if (query.completed !== undefined) {
result = result.filter(todo => todo.completed === query.completed);
}
// 简单的搜索
if (query.text) {
result = result.filter(todo =>
todo.text.toLowerCase().includes(query.text.toLowerCase())
);
}
return {
total: result.length,
limit: params.query.$limit || result.length,
skip: params.query.$skip || 0,
data: result
};
}
async get(id, params) {
const todo = this.todos.find(t => t.id === parseInt(id));
if (!todo) {
throw new Error(`Todo with id ${id} not found`);
}
return todo;
}
async create(data, params) {
const todo = {
id: ++this.currentId,
text: data.text,
completed: data.completed || false,
createdAt: new Date(),
updatedAt: new Date()
};
this.todos.push(todo);
return todo;
}
async patch(id, data, params) {
const todo = await this.get(id, params);
Object.assign(todo, data, {
updatedAt: new Date()
});
return todo;
}
async remove(id, params) {
const todo = await this.get(id, params);
const index = this.todos.findIndex(t => t.id === parseInt(id));
this.todos.splice(index, 1);
return todo;
}
}
module.exports = TodosService;
注册 Service
javascript
// src/services/todos/todos.service.js
const TodosService = require('./todos.class');
const hooks = require('./todos.hooks');
module.exports = function (app) {
const options = {
paginate: app.get('paginate')
};
// 注册服务
app.use('/todos', new TodosService(options));
// 获取服务实例并配置钩子
const service = app.service('todos');
service.hooks(hooks);
};
Service 的查询语法
Feathers.js 提供了强大的查询语法:
javascript
// 基础查询
await app.service('todos').find({
query: {
completed: true,
text: 'Learn Feathers'
}
});
// 比较操作符
await app.service('todos').find({
query: {
createdAt: {
$gte: '2024-01-01',
$lt: '2024-12-31'
}
}
});
// 逻辑操作符
await app.service('todos').find({
query: {
$or: [
{ completed: true },
{ priority: 'high' }
]
}
});
// 分页和排序
await app.service('todos').find({
query: {
$limit: 10,
$skip: 20,
$sort: {
createdAt: -1
}
}
});
// 字段选择
await app.service('todos').find({
query: {
$select: ['id', 'text', 'completed']
}
});
Hooks(钩子)- 业务逻辑的组织方式
什么是 Hook?
Hook 是在 Service 方法执行前后运行的函数,用于处理验证、权限、数据转换、日志记录等横切关注点。
Hook 的类型和执行时机
javascript
// Hook 的执行顺序
app.service('todos').hooks({
before: {
all: [hook1, hook2], // 所有方法执行前
find: [hook3], // find 方法执行前
get: [hook4], // get 方法执行前
create: [hook5, hook6], // create 方法执行前
update: [hook7], // update 方法执行前
patch: [hook8], // patch 方法执行前
remove: [hook9] // remove 方法执行前
},
after: {
all: [hook10], // 所有方法执行后
find: [hook11], // find 方法执行后
// ... 其他方法
},
error: {
all: [hook12], // 所有方法出错时
create: [hook13] // create 方法出错时
}
});
Hook Context 对象
每个 Hook 都会接收一个 context 对象:
javascript
const myHook = (context) => {
console.log('Hook 类型:', context.type); // 'before', 'after', 'error'
console.log('方法名:', context.method); // 'find', 'get', 'create', etc.
console.log('服务路径:', context.path); // '/todos'
console.log('请求参数:', context.params); // 包含 query, user 等
console.log('请求数据:', context.data); // create/update/patch 的数据
console.log('响应结果:', context.result); // after hook 中的结果
console.log('错误信息:', context.error); // error hook 中的错误
return context;
};
常用 Hook 示例
1. 认证 Hook
javascript
const { authenticate } = require('@feathersjs/authentication').hooks;
// 要求用户登录
app.service('todos').hooks({
before: {
all: [authenticate('jwt')]
}
});
2. 数据验证 Hook
javascript
const validateTodo = (context) => {
const { data } = context;
if (!data.text || data.text.trim().length === 0) {
throw new Error('Todo 内容不能为空');
}
if (data.text.length > 200) {
throw new Error('Todo 内容不能超过200个字符');
}
// 清理数据
data.text = data.text.trim();
return context;
};
app.service('todos').hooks({
before: {
create: [validateTodo],
update: [validateTodo],
patch: [validateTodo]
}
});
3. 权限控制 Hook
javascript
const restrictToOwner = (context) => {
const { params } = context;
if (!params.user) {
throw new Error('用户未登录');
}
// 只能操作自己的 Todo
if (context.method === 'find') {
context.params.query.userId = params.user.id;
} else {
// get, patch, remove 等方法
context.params.query = {
...context.params.query,
userId: params.user.id
};
}
return context;
};
app.service('todos').hooks({
before: {
all: [authenticate('jwt'), restrictToOwner]
}
});
4. 数据关联 Hook
javascript
const populateUser = (context) => {
return context.app.service('users').get(context.result.userId)
.then(user => {
context.result.user = user;
return context;
});
};
app.service('todos').hooks({
after: {
get: [populateUser],
find: [
(context) => {
// 批量关联用户信息
const userIds = [...new Set(context.result.data.map(todo => todo.userId))];
return context.app.service('users').find({
query: { id: { $in: userIds } }
}).then(users => {
const userMap = new Map(users.data.map(user => [user.id, user]));
context.result.data.forEach(todo => {
todo.user = userMap.get(todo.userId);
});
return context;
});
}
]
}
});
5. 日志记录 Hook
javascript
const logActivity = (context) => {
const { method, path, params, data } = context;
console.log(`[${new Date().toISOString()}] ${method.toUpperCase()} ${path}`, {
user: params.user?.id,
data: data,
query: params.query
});
return context;
};
app.service('todos').hooks({
before: {
all: [logActivity]
}
});
Hook 的组合和复用
javascript
// hooks/common.js
const { authenticate } = require('@feathersjs/authentication').hooks;
const requireAuth = authenticate('jwt');
const addTimestamp = (field = 'createdAt') => (context) => {
if (context.data) {
context.data[field] = new Date();
}
return context;
};
const addUserId = (context) => {
if (context.data && context.params.user) {
context.data.userId = context.params.user.id;
}
return context;
};
module.exports = {
requireAuth,
addTimestamp,
addUserId
};
// 在服务中使用
const { requireAuth, addTimestamp, addUserId } = require('../hooks/common');
app.service('todos').hooks({
before: {
all: [requireAuth],
create: [addUserId, addTimestamp('createdAt')],
update: [addTimestamp('updatedAt')],
patch: [addTimestamp('updatedAt')]
}
});
Channels(频道)- 实时事件的路由系统
什么是 Channel?
Channel 决定实时事件发送给哪些客户端。当 Service 的数据发生变化时,Feathers.js 会自动发送事件,Channel 负责路由这些事件。
基础 Channel 概念
javascript
// src/channels.js
module.exports = function(app) {
if(typeof app.channel !== 'function') {
// 如果没有实时功能,直接返回
return;
}
app.on('connection', (connection) => {
// 新连接建立时的处理
console.log('新用户连接:', connection);
});
app.on('disconnect', (connection) => {
// 连接断开时的处理
console.log('用户断开连接:', connection);
});
// 发布事件到频道
app.publish((data, hook) => {
// 返回要发送事件的频道
return app.channel('everybody');
});
};
用户认证后的 Channel 管理
javascript
module.exports = function(app) {
app.on('connection', (connection) => {
// 未认证用户加入匿名频道
app.channel('anonymous').join(connection);
});
app.on('login', (authResult, { connection }) => {
const { user } = authResult;
if (connection) {
// 认证成功后,从匿名频道移除,加入认证频道
app.channel('anonymous').leave(connection);
app.channel('authenticated').join(connection);
// 加入用户专属频道
app.channel(`user-${user.id}`).join(connection);
// 根据用户角色加入不同频道
if (user.role === 'admin') {
app.channel('admins').join(connection);
}
}
});
app.on('disconnect', (connection) => {
// 连接断开时自动从所有频道移除
});
};
服务级别的事件发布
javascript
// 为不同服务配置不同的发布策略
module.exports = function(app) {
// ... 连接管理代码
// 全局发布策略
app.publish((data, hook) => {
const { service, method } = hook;
// 根据服务和方法决定发布策略
switch (service) {
case 'todos':
return publishTodoEvents(data, hook, app);
case 'messages':
return publishMessageEvents(data, hook, app);
default:
return app.channel('authenticated');
}
});
// Todo 事件发布策略
function publishTodoEvents(data, hook, app) {
const { method, params } = hook;
switch (method) {
case 'created':
case 'updated':
case 'patched':
case 'removed':
// 只发送给 Todo 的所有者
return app.channel(`user-${data.userId}`);
default:
return [];
}
}
// 消息事件发布策略
function publishMessageEvents(data, hook, app) {
const { method } = hook;
switch (method) {
case 'created':
// 新消息发送给所有在线用户
return app.channel('authenticated');
case 'removed':
// 删除消息只通知管理员
return app.channel('admins');
default:
return app.channel('authenticated');
}
}
};
高级 Channel 功能
1. 房间系统
javascript
// 聊天室频道管理
app.on('login', (authResult, { connection }) => {
const { user } = authResult;
if (connection) {
app.channel('authenticated').join(connection);
// 用户可能在多个聊天室
if (user.rooms) {
user.rooms.forEach(roomId => {
app.channel(`room-${roomId}`).join(connection);
});
}
}
});
// 加入/离开房间的服务方法
app.use('/room-actions', {
async create(data, params) {
const { action, roomId } = data;
const { connection, user } = params;
if (action === 'join') {
app.channel(`room-${roomId}`).join(connection);
// 通知房间内其他用户
app.channel(`room-${roomId}`).send({
type: 'user-joined',
user: user,
roomId: roomId
});
} else if (action === 'leave') {
app.channel(`room-${roomId}`).leave(connection);
// 通知房间内其他用户
app.channel(`room-${roomId}`).send({
type: 'user-left',
user: user,
roomId: roomId
});
}
return { success: true };
}
});
// 消息发布到特定房间
app.service('messages').publish('created', (data, hook) => {
return app.channel(`room-${data.roomId}`);
});
2. 条件发布
javascript
app.service('todos').publish('created', (data, hook) => {
// 根据 Todo 的可见性决定发布范围
if (data.isPublic) {
return app.channel('authenticated');
} else {
return app.channel(`user-${data.userId}`);
}
});
app.service('notifications').publish('created', (data, hook) => {
// 根据通知类型发布到不同频道
switch (data.type) {
case 'system':
return app.channel('authenticated');
case 'admin':
return app.channel('admins');
case 'personal':
return app.channel(`user-${data.userId}`);
default:
return [];
}
});
3. 数据过滤
javascript
app.service('users').publish('patched', (data, hook) => {
// 更新用户信息时,过滤敏感数据
const safeData = {
id: data.id,
username: data.username,
avatar: data.avatar,
lastSeen: data.lastSeen
};
return app.channel('authenticated').send(safeData);
});
三大概念的协作
让我们看一个完整的例子,展示 Services、Hooks 和 Channels 如何协作:
javascript
// 聊天消息服务
class MessagesService {
async create(data, params) {
const message = {
id: generateId(),
text: data.text,
userId: params.user.id,
roomId: data.roomId,
createdAt: new Date()
};
// 保存到数据库
await this.Model.create(message);
return message;
}
}
// Hooks 配置
app.service('messages').hooks({
before: {
create: [
authenticate('jwt'),
// 验证用户是否在房间中
async (context) => {
const { roomId } = context.data;
const { user } = context.params;
const room = await context.app.service('rooms').get(roomId);
if (!room.members.includes(user.id)) {
throw new Error('您不在此房间中');
}
return context;
},
// 内容过滤
(context) => {
context.data.text = filterBadWords(context.data.text);
return context;
}
]
},
after: {
create: [
// 更新房间最后消息时间
async (context) => {
await context.app.service('rooms').patch(context.result.roomId, {
lastMessageAt: new Date()
});
return context;
}
]
}
});
// Channels 配置
app.service('messages').publish('created', (data, hook) => {
// 消息只发送给房间内的用户
return app.channel(`room-${data.roomId}`);
});
总结
通过深入学习 Services、Hooks 和 Channels,我们可以看到 Feathers.js 的设计哲学:
Services 提供了统一的数据操作接口,让我们可以:
- 抽象不同的数据源
- 保持 API 的一致性
- 简化客户端开发
Hooks 提供了强大的业务逻辑组织方式,让我们可以:
- 分离横切关注点
- 复用通用逻辑
- 保持代码的清洁
Channels 提供了灵活的实时事件路由,让我们可以:
- 精确控制事件发送
- 实现复杂的实时交互
- 优化网络性能
这三个概念相互配合,形成了一个强大而优雅的架构,让我们能够快速构建复杂的实时应用。
下一篇文章,我们将通过构建一个待办事项应用,把这些概念应用到实际项目中。
相关文章推荐:
有问题欢迎留言讨论,我会及时回复大家!