常见问题 & 排错指南

这里整理了开发客户端插件时比较容易踩的坑。如果你遇到了"明明写对了但就是不生效"的情况,可以先来这里找找看。

插件相关

插件创建后在管理器里看不到

确认执行了 yarn pm create 而不是手动创建目录。yarn pm create 除了生成文件,还会把插件注册到数据库的 applicationPlugins 表里。如果手动创建了目录,可以执行 yarn nocobase upgrade 重新扫描。

插件启用后页面没有变化

按以下顺序排查:

  1. 确认执行了 yarn pm enable <pluginName>
  2. 刷新浏览器(有时候需要强制刷新 Ctrl+Shift+R
  3. 检查浏览器控制台有没有报错

修改代码后页面没有更新

不同类型的文件,热更新行为不一样:

文件类型修改后需要
src/client-v2/ 下的 tsx/ts自动热更新,不需要操作
src/locale/ 下的翻译文件重启应用
src/server/collections/ 下新增或修改 collection执行 yarn nocobase upgrade

如果客户端代码改了但没热更新,先试试刷新浏览器。

路由相关

注册的页面路由访问不到

NocoBase v2 的路由会默认加上 /v2 前缀。比如你注册了 path: '/hello',实际访问地址是 /v2/hello

this.router.add('hello', {
  path: '/hello', // 实际访问 -> /v2/hello
  componentLoader: () => import('./pages/HelloPage'),
});

详见 Router 路由

插件设置页点进去是空白

如果设置页菜单出现了但内容为空,通常是两个原因之一:

原因一:v1 client 用了 componentLoader

componentLoader 是 client-v2 的写法,v1 client 要用 Component 直接传组件:

// ❌ v1 client 不支持 componentLoader
this.pluginSettingsManager.addPageTabItem({
  menuKey: 'my-settings',
  key: 'index',
  componentLoader: () => import('./pages/MyPage'),
});

// ✅ v1 client 用 Component
import MyPage from './pages/MyPage';
this.pluginSettingsManager.addPageTabItem({
  menuKey: 'my-settings',
  key: 'index',
  Component: MyPage,
});

原因二:页面组件没有用 export default 导出

componentLoader 需要模块有默认导出,漏了 default 就加载不到。

区块相关

自定义区块在「添加区块」菜单里看不到

确认在 load() 里注册了模型:

this.flowEngine.registerModelLoaders({
  MyBlockModel: {
    loader: () => import('./models/MyBlockModel'),
  },
});

如果用的是 registerModels(非按需加载写法),确认 models/index.ts 里正确导出了模型。

添加区块后,数据表选择列表里没有我的表

通过 defineCollection 定义的表是服务端内部表,默认不会出现在 UI 的数据表列表中。

推荐做法:在 NocoBase 界面的「数据源管理」中添加对应的数据表,配置好字段和接口类型后,表就会自动出现在区块的数据表选择列表里。

如果确实需要在插件代码里注册(比如示例插件里的演示场景),可以通过 addCollection 手动注册,详见 做一个前后端联动的数据管理插件。注意必须通过 eventBus 模式注册,不能直接在 load() 里调用——ensureLoaded() 会在 load() 之后清空并重新设置所有 collection。

自定义区块只想绑定特定数据表

在模型上覆盖 static filterCollection,返回 true 的 collection 才会出现在选择列表里:

export class MyBlockModel extends TableBlockModel {
  static filterCollection(collection: Collection) {
    return collection.name === 'myTable';
  }
}

字段相关

自定义字段组件在「字段组件」下拉菜单里看不到

按以下顺序排查:

  1. 确认调用了 DisplayItemModel.bindModelToInterface('ModelName', ['input']),并且 interface 类型匹配——比如 input 对应单行文本字段,checkbox 对应复选框
  2. 确认模型在 load() 里注册了(registerModelsregisterModelLoaders
  3. 确认字段模型调用了 define({ label })

字段组件下拉菜单里显示的是类名

忘了在字段模型上调用 define({ label }),加上就行:

MyFieldModel.define({
  label: tExpr('My field'),
});

同时确保 src/locale/ 下的翻译文件里有对应的 key,不然中文环境下还是显示英文原文。

操作相关

自定义操作按钮在「配置操作」里看不到

确认模型上设置了正确的 static scene

出现位置
ActionSceneEnum.collection区块顶部操作栏(比如「新建」按钮旁边)
ActionSceneEnum.record表格每行的操作列(比如「编辑」「删除」旁边)
ActionSceneEnum.both两种场景都出现

点击操作按钮没有反应

确认 registerFlowon 设置为 'click'

MyActionModel.registerFlow({
  key: 'myFlow',
  on: 'click', // 监听按钮点击
  steps: {
    doSomething: {
      async handler(ctx) {
        // 你的逻辑
      },
    },
  },
});
注意

registerFlow 里的 uiSchema 是配置面板(设置态)的 UI,不是运行时弹窗。如果你想在点击按钮后弹出一个表单,应该在 handler 里用 ctx.viewer.dialog() 来打开弹窗。

国际化相关

翻译不生效

最常见的原因:

  • 初次添加 src/locale/ 目录或文件——需要重启应用才能生效
  • 翻译 key 不一致——确认 key 和代码里的字符串完全一致,注意空格和大小写
  • 组件里直接用了 ctx.t()——ctx.t() 不会自动注入插件的 namespace,在组件里应该用 useT() hook(从 locale.ts 导入)

tExpr()useT()this.t() 用错场景

这三个翻译方法的使用场景不同,用错了要么报错要么翻译不生效:

方法用在哪里说明
tExpr()define()registerFlow() 等静态定义模块加载时 i18n 还没初始化,用延迟翻译
useT()React 组件内部返回绑定了插件 namespace 的翻译函数
this.t()Plugin 的 load()自动注入插件包名作为 namespace

详见 i18n 国际化

API 请求相关

请求返回 403 Forbidden

通常是服务端的 ACL 没有配置。比如你的 collection 叫 todoItems,需要在服务端插件的 load() 里允许对应的操作:

// 只允许查询
this.app.acl.allow('todoItems', ['list', 'get'], 'loggedIn');

// 允许完整增删改查
this.app.acl.allow('todoItems', ['list', 'get', 'create', 'update', 'destroy'], 'loggedIn');

'loggedIn' 表示登录用户即可访问。如果不设置 acl.allow,默认只有管理员能操作。

请求返回 404 Not Found

按以下顺序排查:

  • 如果用的是 defineCollection,确认 collection 名称拼写正确
  • 如果用的是 resourceManager.define,确认 resource 名称和 action 名称都对
  • 检查请求 URL 格式——NocoBase 的 API 格式是 resourceName:actionName,比如 todoItems:listexternalApi:get

构建和部署相关

yarn build --tar 报错 "no paths specified to add to archive"

执行 yarn build <pluginName> --tar 时报错:

TypeError: no paths specified to add to archive

不过单独执行 yarn build <pluginName>(不带 --tar)是正常的。

这个问题通常是因为插件的 .npmignore用了取反语法(npm 的 ! 前缀)。--tar 打包时,NocoBase 会读取 .npmignore 的每一行并在前面加上 ! 转成 fast-glob 的排除模式。如果你的 .npmignore 已经用了取反语法,比如:

*
!dist
!package.json

处理后会变成 ['!*', '!!dist', '!!package.json', '**/*']。其中 !* 会排除所有根级文件(包括 package.json),而 !!dist 并不会被 fast-glob 识别为"重新包含 dist"——取反失效了。如果 dist/ 目录恰好为空或者构建没有产出文件,最终收集到的文件列表就是空的,tar 就会抛出这个错误。

解决办法: .npmignore 里不要用取反语法,改成只列出需要排除的目录:

/node_modules
/src

打包逻辑会把这些转换成排除模式(!./node_modules!./src),再加上 **/* 匹配所有其他文件。这样写既简单又不会遇到取反处理的问题。

插件上传到生产环境后启用失败(本地正常)

插件在本地开发时一切正常,但通过「插件管理器」上传到生产环境后启用失败,日志里出现类似这样的报错:

TypeError: Cannot assign to read only property 'constructor' of object '[object Object]'

这个问题通常是因为插件把 NocoBase 内置的依赖打包进了自己的 node_modules/。NocoBase 的构建系统维护了一份 external 列表,里面的包(比如 reactantdaxioslodash 等)由 NocoBase 宿主提供,不应该被打包进插件。如果插件带了一份私有的副本,运行时可能会和宿主已经加载的版本冲突,引发各种奇怪的错误。

为什么本地没问题: 本地开发时插件在 packages/plugins/ 目录下,没有私有 node_modules/,依赖会解析到项目根目录下已经加载好的版本,不会产生冲突。

解决办法: 把插件 package.json 里的 dependencies 都移到 devDependencies——NocoBase 的构建系统会自动处理插件的依赖:

{
- "dependencies": {
-   "axios": "1.7.7"
- },
+ "devDependencies": {
+   "axios": "1.7.7"
+ },
}

然后重新构建并打包。这样插件的 dist/node_modules/ 就不会包含这些包,运行时会使用 NocoBase 宿主提供的版本。

通用原则

NocoBase 的构建系统维护了一份 external 列表,里面的包(比如 reactantdaxioslodash 等)由 NocoBase 宿主提供,插件不应该自己打包。插件的所有依赖都应该放在 devDependencies 里,构建系统会自动判断哪些需要打包进 dist/node_modules/、哪些由宿主提供。

相关链接