news 2026/10/8 8:10:05

Midway 标签组件 @midwayjs/tags 使用指南:从内存到 MySQL 的通用标签系统实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Midway 标签组件 @midwayjs/tags 使用指南:从内存到 MySQL 的通用标签系统实战
  • 后端
  • 微服务
  • 云原生

【免费下载链接】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. 🌈

项目地址:https://gitcode.com/gh_mirrors/mi/midway
点击查看免费下载

导读

本文围绕 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 --save

2. 在 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)。

内存存储配置

配置值类型默认值配置描述
dialectTypestringmemory-配置为memory,则启用内存存储

内存模式零依赖、零启动成本,适合本地开发、单机部署或数据量小的场景;但数据仅存在于进程内,重启即丢失,生产环境建议使用 MySQL。

MySQL 存储配置

如果要使用 MySQL 数据库作为数据存储,需要将 MySQL 的"数据库连接对象"传入 tags 的配置中:

配置值类型默认值配置描述
dialectTypestringmysql-配置为mysql,则启用 MySQL 存储
syncbooleanfalse自动同步 Tags 的表结构,Tags 组件会创建两张数据表,详见下方的数据表信息
instance{ query: (sql: string, placeholder?: any[])}: Promise<[]>-MySQL 连接的实例,需要提供一个 query 方法,可以查看下面的示例
tablePrefixstring-数据表前缀
tableSeparatorstring_数据表的拼接分隔符

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. 🌈

项目地址:https://gitcode.com/gh_mirrors/mi/midway
点击查看免费下载
上一篇:强力指南:如何用genshin-wish-export实现原神抽卡数据的精准分析与智能管理
下一篇:终极iOS设备降级指南:让老旧iPhone/iPad重获新生的完整教程

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/8 8:08:14

Claude的“外置大脑”:用claude-mem实现跨会话持久记忆

Claude 用久了&#xff0c;大家应该都有同一个磨人的体验&#xff1a;开一个新对话窗口&#xff0c;它就“失忆”了。明明上一个 session 里已经把技术方案、命名规范、部署链路聊得清清楚楚&#xff0c;换一个窗口全部归零&#xff0c;又得从“我们之前讨论过的那件事……”开…

作者头像 李华
网站建设 2026/10/8 8:07:00

编辑器上下文模式实战:用符号树与LSP终结长文件迷失

前阵子我接手一个维护了快七年的老服务&#xff0c;业务逻辑倒不算难&#xff0c;真正的敌人是文件长度。一个核心类 800 多行&#xff0c;方法之间相互调用&#xff0c;我经常滚到屏幕中间就忘了自己是在类里还是已经在某个私有方法里&#xff0c;debug 到一半才发现改动放错了…

作者头像 李华
网站建设 2026/10/8 8:06:37

微信小程序+Java互助学习系统毕设:从技术选型到部署答辩完整指南

简介&#xff1a;这份资源是面向高校计算机相关专业学生与Java初学者的一套互助学习平台毕业设计完整方案&#xff0c;采用微信小程序前端搭配Java后端与MySQL数据库&#xff0c;适合作为毕业设计、课程设计或全栈入门练手项目。压缩包共1306个文件&#xff0c;约25.37MB&#…

作者头像 李华
网站建设 2026/10/8 8:05:41

计算机网络实验A1/A3:自研TCP协议栈与HTTP服务器源码解析

简介&#xff1a;中南大学计算机网络实验源代码是一份面向高校网络专业学生的实践资源&#xff0c;聚焦2022年A1、A3两个实验的源码实现&#xff0c;适合作为计算机网络课程配套练习。A1覆盖Socket编程与TCP/UDP通信&#xff0c;包括连接建立、收发数据、异常处理及IP寻址等传输…

作者头像 李华