- 后端
- 微服务
- 云原生
【免费下载链接】midway
🍔 A Node.js Serverless Framework for front-end/full-stack developers. Build the application for next decade. Works on AWS, Alibaba Cloud, Tencent Cloud and traditional VM/Container. Super easy integrate with React and Vue. 🌈
导读
本文围绕 Midway 官方提供的通用标签组件@midwayjs/tags(对应仓库 site/docs/extensions/tags.md),完整讲解如何在@midwayjs/faas、@midwayjs/web、@midwayjs/koa、@midwayjs/express等框架中接入标签能力:包括安装与组件注册、clients多分组配置、TagClient的八个核心 API(标签增删改查、实体绑定/解绑、按标签查实体、按实体查标签)、内存与 MySQL 两种存储方言的配置差异,以及数据表结构设计。读完本文,你将掌握一套可直接复用的服务端标签系统集成方案,并理解其底层实现原理。
标签(Tag)是服务端一种高度抽象的通用系统化能力,适用于资源分类、权限控制、状态流转等大量业务场景。Midway 将其沉淀为独立组件,屏蔽底层存储差异,让业务侧只需关心"标签"与"实体"两个概念。
适用框架与支持情况
该组件适用于@midwayjs/faas、@midwayjs/web、@midwayjs/koa和@midwayjs/express多种框架,均为通用实现:
| web 支持情况 | 支持 |
|---|---|
| @midwayjs/koa | ✅ |
| @midwayjs/faas | ✅ |
| @midwayjs/web | ✅ |
| @midwayjs/express | ✅ |
从源码看,组件本身只依赖@midwayjs/core的Configuration、ServiceFactory、InjectClient等通用能力(见 packages/tags/src/manager.ts),因此可以无缝嵌入上述任意框架,其 package.json 中的engines.node >= 20约束了运行环境(见 packages/tags/package.json)。
使用场景:标签系统的典型业务价值
标签是一种抽象化的服务端常用系统化能力,可用于多种用途:
- 组织管理资源
- 实现分类系统(面向内容、人群等);
- 资源管理系统:例如给图片添加各种颜色标签、物体和场景标签,通过标签筛选图片;视频等素材打标签。
- 访问控制:权限系统(管理员、编辑、游客等角色标签)。
- 状态系统:编辑中、已发布等状态标签。
基于标签系统提供的增删改查,以及通过标签对绑定了标签的"实体"进行增删改查,能够很方便地实现更多高级业务逻辑。标签系统正是为这类业务场景而设计,让服务端基于标签能力实现更高效、便捷的开发。
这里需要理解组件中"实体"(Object)的抽象:实体可以是图片、文件、用户、文章等任何业务对象,实体的objectId由调用方自行控制,组件只负责维护"标签 ↔ 实体"之间的多对多关系。
快速接入:安装、注册与首个调用
1. 安装依赖
$ npm i @midwayjs/tags --save2. 在 configuration 中引入组件
// src/configuration.ts import { Configuration } from '@midwayjs/core'; import * as tags from '@midwayjs/tags'; @Configuration({ imports: [ // ... tags ], }) export class MainConfiguration {}组件内部通过 packages/tags/src/configuration.ts 的TagsConfiguration注册,其namespace为tags,并内置了默认配置:
tags: { default: { dialectType: 'memory', // 未显式配置时默认使用内存存储 } as ITagDialectOption, },也就是说,即使你不写任何tags配置,组件也会以"内存存储"模式就绪。
3. 添加配置(内存模式)
// src/config/config.local.ts export default { tags: { clients: { 'tagGroup1': { // 使用 本机内存 作为数据存储 dialectType: 'memory', }, }, } }4. 在代码中调用
// src/testTags.ts import { Provide, Inject, InjectClient } from '@midwayjs/core'; import { TagServiceFactory, TagClient } from '@midwayjs/tags'; @Provide() export class TestTagsService { @Inject() tags: TagServiceFactory; // 相当于 this.tags.get('tagGroup1') @InjectClient(TagServiceFactory, 'tagGroup1') tagClient: TagClient; @ServerlessTrigger(ServerlessTriggerType.HTTP, { path: '/tags/list', method: 'get'}) async listTags() { // 也可以直接使用 this.tagClient const tagClient: TagClient = this.tags.get('tagGroup1'); // add new tag const tagInfo = await tagClient.new({ name: 'test-tag-name', desc: 'tag desc', }); /* tagInfo = { success: true, id: 1, } */ // list top 20 tags const tags = await tagClient.list({ count: true }); /* tags: { list: [ { id: 1, name: 'test-tag-name', desc: 'tag desc' } ], total: 1 } */ return tags; } }这里的注入方式体现了 Midway 多实例组件的标准用法:TagServiceFactory继承自核心的ServiceFactory<TagClient>,在@Init()阶段通过initClients(this.tags)按clients配置批量创建客户端(见 packages/tags/src/manager.ts);@InjectClient(TagServiceFactory, 'tagGroup1')则相当于this.tags.get('tagGroup1')的快捷注入。每个TagClient内部持有一个按分组(group)隔离的ITagDialectInstance实例(见 packages/tags/src/service.ts)。
八个核心方法详解
所有方法都返回Promise,操作类方法统一返回{ success: boolean; message: string; id?: number }结构,便于业务侧判断成败。下面逐一说明签名与行为。
新增标签 new
new(tagDefine: { // 标签名,在同一个 group 里面不能重复 name: string; // 标签描述 desc?: string; }): Promise<{ success: boolean; message: string; // 标签id id?: number; }>;同名标签在同一个 group 内不可重复创建:内存实现通过tagStore.get(tagDefine.name)判重(见 packages/tags/src/dialect/memory.ts),MySQL 实现则先按name查询再插入(见 packages/tags/src/dialect/mysql.ts),重复时返回错误码tag already exists(枚举定义见 packages/tags/src/error.ts)。
删除标签 remove
删除标签也会删除和这个标签绑定的实体关系:
remove(tagIdOrName: number | string): Promise<{ success: boolean; message: string; // 标签id id?: number; }>;注意入参既支持标签id也支持标签name。内存实现会先通过listObjects找出该标签绑定的所有实体并逐个删除关系记录,再移除标签本身;MySQL 实现则先执行delete from relationship where tid = ?再删除tag记录,保证级联清理(见 packages/tags/src/dialect/mysql.ts)。
更新标签 update
更新一个标签的基础信息:
update(tagIdOrName: number | string, params: Partial< { name: string; desc?: string; }>): Promise<{ success: boolean; message: string; // 标签id id?: number; }>;MySQL 实现会将参数desc映射为数据库字段descri,并跳过group、id等不可更新字段(见 packages/tags/src/dialect/mysql.ts)。
列举标签 list
搜索标签,支持分页:
list(listOptions?: { // 搜索的标签,支持传入标签 id 和标签名 tags?: Array<number | string>; // 检索的时候标签是采用交集还是并集,取值为 and 和 or type?: MATCH_TYPE; count?: boolean; pageSize?: number; page?: number; }): Promise<{ // 标签列表 list: { id: number; name: string; desc: string; createAt: number; updateAt: number; }[]; // 标签总数 total?: number; }>;关于默认值与匹配规则,从实现中可以提炼出以下事实(见 packages/tags/src/service.ts 与 packages/tags/src/utils.ts):
- 默认分页:
page默认1,pageSize默认20;传入count: true时返回total,否则只返回list。 - 字符串模糊匹配:标签名支持通配符风格搜索,规则为
%前缀表示"以 xxx 结尾"(endsWith)、%后缀表示"以 xxx 开头"(startsWith)、xxx%/%xxx同时出现时等价于精确匹配;直接传name则完全匹配(formatMatchLike的实现见 packages/tags/src/utils.ts)。 type: MATCH_TYPE取值and(交集)与or(并集),枚举定义见 packages/tags/src/interface.ts。
上述行为在 packages/tags/test/memory.test.ts 中有完整的用例验证,例如向分组内写入 100 个标签后,list({ count: true })默认返回 20 条且total === 100;list({ page: 2, pageSize: 17 })返回第 18~34 条;混合传[2, 4, '%t67', 'test78', 'test9%']可精确命中对应匹配规则。
绑定实体 bind
绑定实体的意思就是将其他的任何东西绑定到一个标签上,这里的实体可以是一张图片、也可以是一个文件,实体的 id 由用户自己控制:
bind(bindOptions: { // 标签列表 tags: Array<number | string>; // 不存在标签的话自动创建标签,并绑定,默认为false autoCreateTag?: boolean; // 实体id objectId: number, }): Promise<{ success: boolean; message: string; }>两个值得注意的细节:
- 若
tags中引用了不存在的标签,默认返回tag does not exist错误(bind方法入口还会对空tags数组做参数校验,见 packages/tags/src/service.ts); - 当
autoCreateTag: true且传入的是字符串标签名时,会自动创建该标签(描述默认为auto create)并完成绑定。内存与 MySQL 两种方言均实现了这一逻辑,测试用例bind一节也验证了自动创建行为(见 packages/tags/test/memory.test.ts)。
解绑实体 unbind
unbind(unbindOptions: { // 解绑的多个标签,标签id或者是标签 name tags: Array<number | string>, // 实体id objectId: number, }): Promise<{ success: boolean; message: string; }>根据标签列举实体 listObjects
listObjects(listOptions?: { // 标签id或者是标签 name tags?: Array<string|number>; count?: boolean; // 检索的时候标签是采用交集还是并集,取值为 and 和 or type?: MATCH_TYPE; pageSize?: number; page?: number; }): Promise<{ // 实体的 id 列表 list: number[]; // 实体总数 total?: number; }>;这是"通过标签反查资源"的核心能力,比如筛选同时拥有"风景"和"日落"两个标签的图片。type默认or(并集),当type: 'and'(交集)时,返回同时绑定了所有指定标签的实体。MySQL 方言针对"单标签查询"做了性能优化(直接WHERE tid = ?),多标签交集则用GROUP BY oid HAVING COUNT(*) = N实现(见 packages/tags/src/dialect/mysql.ts);测试用例中也验证了 And/Or 两种模式下的结果差异(见 packages/tags/test/memory.test.ts)。
根据实体获取标签 listObjectTags
listObjectTags(listOptions?: { // 实体id objectId: number; count?: boolean; pageSize?: number; page?: number; }): Promise<{ list: { // 标签列表 name: string; desc?: string; id: number; createAt: number; updateAt: number; }[]; // 标签总数 total?: number; }>;即"反查某个实体被打上了哪些标签"。MySQL 实现先查relationship表拿到tid列表,再回查tag表组装完整标签信息(见 packages/tags/src/dialect/mysql.ts)。
存储配置:内存与 MySQL
Tags 支持内存存储(默认)和 MySQL 数据库存储两种方式,下面是一个配置的示例:
// src/config/config.local.ts export default { tags: { clients: { 'tagGroup1': { // 使用 本机内存 作为数据存储 dialectType: 'memory', }, 'tagGroup2': { // 使用 mysql 作为数据存储 dialectType: 'mysql', // 自动同步表结构 sync: true, // mysql 连接实例 instance: mysqlConnection.promise(), }, }, } }clients下的每个 key 即一个独立的分组(group),分组之间数据完全隔离:内存模式下每个 group 拥有独立的tagStore与tagRelationStore(见 packages/tags/src/dialect/memory.ts),MySQL 模式下则通过group字段区分(见 packages/tags/src/dialect/mysql.ts)。
内存存储配置
| 配置 | 值类型 | 默认值 | 配置描述 |
|---|---|---|---|
| dialectType | stringmemory | - | 配置为memory,则启用内存存储 |
内存模式零依赖、零启动成本,适合本地开发、单机部署或数据量小的场景;但数据仅存在于进程内,重启即丢失,生产环境建议使用 MySQL。
MySQL 存储配置
如果要使用 MySQL 数据库作为数据存储,需要将 MySQL 的"数据库连接对象"传入 tags 的配置中:
| 配置 | 值类型 | 默认值 | 配置描述 |
|---|---|---|---|
| dialectType | stringmysql | - | 配置为mysql,则启用 MySQL 存储 |
| sync | boolean | false | 自动同步 Tags 的表结构,Tags 组件会创建两张数据表,详见下方的数据表信息 |
| instance | { query: (sql: string, placeholder?: any[])}: Promise<[]> | - | MySQL 连接的实例,需要提供一个 query 方法,可以查看下面的示例 |
| tablePrefix | string | - | 数据表前缀 |
| tableSeparator | string | _ | 数据表的拼接分隔符 |
instance是组件与数据库交互的唯一入口,其类型为IMysqlQuery = (sql: string, placeholder?: any[]) => [any, any](见 packages/tags/src/interface.ts),组件内部所有 SQL 都通过它执行,因此任何"暴露query方法"的连接对象(mysql2、sequelize 等)理论上都可适配。
下面是使用mysql2这个 npm 包进行数据库连接的示例:
// src/config/config.local.ts const mysql = require('mysql2'); export default () => { const connection = mysql.createConnection({ host: 'db4free.net', user: 'tag***', password: 'tag***', database: 'tag***', charset: 'utf8', }); return { tags: { clients: { 'tagGroup': { dialectType: 'mysql', sync: true, instance: { // 包含 query 的mysql连接实例 query: (...args) => { return connection.promise().query(...args); } }, }, }, } } }在生命周期中管理数据库连接
你也可以考虑在configuration.ts的onConfigLoad生命周期中进行数据库连接,这样的好处是在关闭时,可以关闭数据库连接:
// src/configuration.ts import { Config, Configuration } from '@midwayjs/core'; import { join } from 'path'; import * as tags from '@midwayjs/tags'; import { ITagMysqlDialectOption } from '@midwayjs/tags'; const mysql = require('mysql2'); @Configuration({ imports: [ tags ], }) export class MainConfiguration { connection; @Config() tags; async onConfigLoad(container) { // 创建 mysql 连接 this.connection = mysql.createConnection({ host: 'db4free.net', user: 'tag***', password: 'tag***', database: 'tag***', charset: 'utf8', }); let dialect: ITagMysqlDialectOption = { dialectType: 'mysql', sync: true, instance: { query: (...args) => { return this.connection.promise().query(...args); } } }; return { tags: dialect } } async onStop() { // 关闭 mysql 连接 this.connection.close(); } }这种做法的优势是连接的生命周期与应用生命周期对齐:在onConfigLoad中创建连接并注入配置,在onStop中关闭连接,避免连接泄漏。
数据表信息
Tags 组件需要两种数据表来存储数据,分别是tag和relationship。这两张表在数据库中真实的表名,是通过配置中的"表名前缀"、"表名分隔符"和"客户端名/分组名"进行拼接的,例如:
const clientName = 'local-test'; const { tablePrefix = 'a', tableSeparator = '_' } = tagOptions; const tagTableName = `${tablePrefix}${tableSeparator}${clientName}${tableSeparator}tag`; // tagTableName: a_local-test_tag const relationshipTableName = `${tablePrefix}${tableSeparator}${clientName}${tableSeparator}relationship` // relationshipTableName: a_local-test-relationship表名拼接逻辑在 packages/tags/src/dialect/mysql.ts 的buildTableName中实现:未配置tablePrefix时直接使用tags_${tableName}形式,配置前缀后为${tablePrefix}_tags_${tableName},分隔符默认_。这意味着每个分组(client)独占两张表,多租户场景下可通过前缀天然隔离。
当你在配置中启用sync的自动表结构同步时,如果没有这两张表,就会根据下述的表结构创建对应的数据表(checkOrCreateTable会先执行SHOW TABLES LIKE探测,已存在则跳过,见 packages/tags/src/dialect/mysql.ts):
tag表结构:
CREATE TABLE `tag` ( `id` BIGINT unsigned NOT NULL AUTO_INCREMENT, `group` varchar(32) NULL, `name` varchar(32) NULL, `descri` varchar(128) NULL, `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, `update_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL, PRIMARY KEY (id) )relationship表结构:
CREATE TABLE `relationship` ( `id` BIGINT unsigned NOT NULL AUTO_INCREMENT, `tid` BIGINT unsigned NOT NULL, `oid` BIGINT unsigned NOT NULL, `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, `update_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL, PRIMARY KEY (id) )其中tag.group记录分组名、name为标签名(同一分组内唯一)、descri为描述;relationship.tid关联tag.id,oid为业务侧的实体 id,两者构成"标签 ↔ 实体"的多对多关系表。
底层实现与扩展机制
理解组件内核有助于排查问题与二次扩展:
- 工厂 + 方言模式:
TagServiceFactory(packages/tags/src/manager.ts)根据配置决定方言实现——优先使用用户自定义dialect,其次按dialectType选择MysqlDialect,其余情况回退到MemoryDialect。你可以通过ITagUserDialect传入自定义ITagDialect实现,挂载其他存储(如 Redis、PostgreSQL),这也是接口设计预留的扩展点(见 packages/tags/src/interface.ts)。 - 统一返回结构:
success/error工具函数(packages/tags/src/utils.ts)保证所有方言的返回值形态一致,业务代码无需关心底层存储差异。 - 分页计算:
getPageOpions(page, pageSize)统一换算limit/offset,并支持pageSize: Infinity的全量拉取(内存删除标签时即用此特性获取全部关联实体,见 packages/tags/src/dialect/memory.ts)。 - 错误语义:组件内置
EXISTS、NOT_EXISTS、MISSING_PARAMETERS、OPER_ERROR四类错误枚举(packages/tags/src/error.ts),调用方可据此做精确的错误分支处理。
小结
@midwayjs/tags通过"分组隔离 + 方言可插拔 + 统一 API"的设计,把标签这类高频系统能力从业务代码中彻底剥离:开发阶段用内存模式零成本起步,生产环境切换 MySQL 存储并开启sync自动建表即可平滑升级;八个 API 覆盖了标签生命周期与"标签 ↔ 实体"关系的全部常见操作,足以支撑资源分类、权限、状态等绝大多数打标场景。完整的测试用例(packages/tags/test/memory.test.ts、packages/tags/test/mysql.test.ts)可供你在集成时对照验证各方法的分页、匹配、级联删除与交集并集语义。
- 后端
- 微服务
- 云原生
【免费下载链接】midway
🍔 A Node.js Serverless Framework for front-end/full-stack developers. Build the application for next decade. Works on AWS, Alibaba Cloud, Tencent Cloud and traditional VM/Container. Super easy integrate with React and Vue. 🌈
相关推荐
Midway 中的 JWT 组件 @midwayjs/jwt 使用指南:配置、注入与 Token 签发校验实战
Midway 中的 JWT 组件 @midwayjs/jwt 使用指南:配置、注入与 Token 签发校验实战 本指南介绍 Midway 框架内置的 JWT 组
后端微服务云原生Minimal Mistakes 标签体系实战:从 Many Tags 边界用例看 Jekyll 标签归档的完整实现
Minimal Mistakes 标签体系实战:从 Many Tags 边界用例看 Jekyll 标签归档的完整实现 本文以 Minimal Mistakes
前端静态站点AWS CLI 实战:使用 `aws autoscaling delete-tags` 删除 Auto Scaling 组标签
AWS CLI 实战:使用 aws autoscaling delete tags 删除 Auto Scaling 组标签 本文以 AWS CLI 官方示例文档
开发工具云原生运维
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考