Wujies 无界平台
平台功能(自研)插件开发

手动开发与本地运行

这一页是不用后台页面的那条路:全程命令行为,把「clone 一个已发布的应用仓库 → 本地改后端 + 改前端 → 跑起来 → 打包 → 上架」 每一步的确切命令写清楚。看得懂这页,就能不打开开发台也把开发做完。

它对照的是 Halo 的两种插件开发方式:

HaloWujies特点
./gradlew haloServer(DevTools,推荐)可视化开发台 的「启动后端」+「启动前端」编译、加载、监听都由宿主代劳
runtime-mode: development + --halo.plugin.fixed-plugin-path本页:写 fixed-plugin-path + POST /app/dev/reload自己敲命令,但要几个接口都能用

两条路用的是同一套开发模式接口,区别只在于「谁按下按钮」。命令行的好处是不依赖页面、能写进脚本、 CI 里也能跑;代价是每次改完要自己触发编译和热加载。

应用不能单独启动

一个应用(插件)跑不起来属于自己的进程:后端代码装载进宿主的 JVM,在宿主的子 Spring 上下文里; 前端是宿主浏览器里 import() 的 ESM 产物,共享宿主那份 Vue / Element Plus 实例。 所以本地开发的对象永远是「一个跑着的宿主 + 一份被它加载的插件产物」,Halo 同理。

想完全不碰真实宿主,可以用沙箱(H2 内存库,不连 MySQL / Redis)挂起单个插件 jar。⚠️ sandbox 模块只在早期那棵单体仓库 wujies-plugins 里,那棵仓库已下线、不再作为开发入口,下面仅当本机还留着历史检出时才可用:

# 先在应用仓库打出 jar
cd <插件仓库>
mvn -B package -DskipTests
# 再在单体仓库的历史检出里起沙箱
cd ..\wujies\wujies-plugins
mvn -B -pl sandbox spring-boot:run `
  "-Dspring-boot.run.arguments=--sandbox.plugin-jar=<插件仓库绝对路径>\target\<artifactId>-<版本>.jar"

沙箱只验后端接口,菜单和前端页面看不到 —— 那部分必须走真实宿主。

先决条件

需要说明
JDK 17+宿主与插件同一个 java.version;JAVA_HOME 要指到它
Maven命令行有 mvn,或用 IDE 自带的 mvn.cmd
Node 18+ 与 npm只有要构建前端时才需要
一个跑起来的宿主默认 http://127.0.0.1:8086,context-path 为 /
一个能用的令牌后面所有 curl 都要带 Authorization: Bearer <token> 与 clientid: <VITE_APP_CLIENT_ID> 两个头
本机的 ~/.m2 里有平台构件见下面「第一次在本机编译插件」

取令牌最省事的办法:浏览器登录管理端,从任意请求的 Request Headers 里把 Authorization 与 clientid 抄出来。 下面统一写成 $TOKEN / $CLIENT。

第一次在本机编译插件

独立插件仓库的 pom.xml 里父构件是 cn.wujies.plugins:wujies-plugins-parent:1.0.0,<relativePath/> 留空 ⇒ Maven 直接去仓库找它,不再依赖「它在单体仓库里的相对位置」。父 POM、wujies-common-*、兄弟插件构件目前只发布到本机的 ~/.m2(还没进 Maven 中央仓库),所以陌生机器 clone 下来会卡在「找不到 wujies-plugins-parent」。本机准备一次即可:

# 1) 平台构件:在宿主仓库安装 admin 及其依赖模块
#    ⚠️ 必须先停掉正在跑的宿主 JVM,否则会出现 NoClassDefFoundError
cd <...>\wujies-platform
mvn -pl wujies-admin -am -B install -DskipTests

# 2) 插件父 POM:只装根 POM 本身,不构建子模块
#    ⚠️ 母本 POM 在已下线的单体仓库 wujies-plugins(Gitee 取不到),仅本机有历史检出时才装得了
cd <...>\wujies\wujies-plugins
mvn -B -N install

这两步做过之后,独立插件仓库在本机就能 mvn compile / package 通过。构件发布进度见 插件 SDK 与依赖包。

第 1 步:开启开发模式(一次性)

宿主 application.yml:

app-market:
  plugin-runtime:
    # 开发模式总开关:false 时下面这些接口全部 404
    dev-mode: true
    # 留空 = 默认扫描项目根的 plugins/,把其中每个插件工程都加载(Halo 的 fixed-plugin-path)
    # 要加载别处的工程时,在这里写绝对路径(可多行)
    fixed-plugin-path: []
    # 可选:显式指定 mvn 可执行文件;留空由宿主自行定位
    maven-command: ""

改完重启宿主。启动时宿主会加载 fixed-plugin-path 里列出的每个工程;留空则加载项目根 plugins/ 里的全部插件工程(前提是已有 target/classes 或 target/*.jar; 没有只告警,不会替你跑 Maven)。配置项可以写插件工程、插件容器(如 plugins/), 也可以写含多个 wujies-* 的单体仓库根 —— 单体仓库根只用于定位,不会批量加载。

空配置时宿主按约定位置找插件容器(相对宿主进程工作目录):

../plugins
plugins
../../plugins
wujies/plugins

容器判定:目录下至少有一个 wujies-*/app.json。单体仓库的命中条件是目录里同时存在 package.ps1 与 wujies-hello/。

独立插件仓库本身就是插件工程(仓库根有 app.json):放到 plugins/wujies-<code>-plugin/ 就进了默认扫描范围;放在别处则写进 fixed-plugin-path。

定位优先级恒为:fixed-plugin-path > 约定自动探测,同名应用只认前面那一份。

第 2 步:取源码

已发布的应用都在 Gitee 组织 wujies 下,一个应用一个仓库,仓库名 wujies-<编码>-plugin (与 Maven artifactId 一致);后端在仓库根,前端在 frontend/。

git clone git@gitee.com:wujies/wujies-enterprise-plugin.git
wujies-enterprise-plugin/
├── app.json          清单:编码、版本、产物、菜单与权限(唯一权威描述)
├── pom.xml           后端 Maven 工程,parent = wujies-plugins-parent
├── src/main/java/    Controller / Service / Mapper / domain
├── migrations/       建表与种子数据,按文件名顺序执行
└── frontend/         前端远程应用(vite.config.ts + src/index.ts + src/views)

早期 11 个应用全挤在一棵单体仓库 wujies-plugins 里;那棵仓库已下线(Gitee 取不到), 不再是取源码的入口 —— 只有本机还留着它的历史检出时,才用于打开发包、起沙箱这些母本操作。 后面每一步都按「一个应用一个仓库」写。

第 3 步:确认宿主找到了这份代码

fixed-plugin-path 里写对了,宿主启动时就已经加载并定位好;这一步只是核对:

curl -H "Authorization: Bearer $TOKEN" -H "clientid: $CLIENT" `
  http://127.0.0.1:8086/app/dev/overview

看 fixedPluginPath(配置值)、searchRoots(本次实际扫描的根)、 plugins(扫出来的应用列表,每项带 code / layout / artifactPath / buildCommand)。 plugins 里有你要的编码就够了 —— 后续 reload / watch 的 path 都可以省略。

第 4 步:编译后端

开发模式加载的是编译输出目录,不需要打 jar,所以只跑 compile:

# 独立插件仓库(仓库根就是插件工程)
mvn -B -f C:/Users/you/code/wujies-enterprise-plugin/pom.xml compile -DskipTests

overview 里每个应用的 buildCommand 就是这条,可以直接复制。

第 5 步:热加载,或开监听

把新产物换进宿主 —— 行为是「先卸载上一代类加载器,再装载新产物」,重复调用幂等,宿主不重启:

curl -X POST http://127.0.0.1:8086/app/dev/reload `
  -H "Content-Type: application/json" `
  -H "Authorization: Bearer $TOKEN" -H "clientid: $CLIENT" `
  -d '{ "appCode": "enterprise" }'

path 省略时按 appCode 在 fixed-plugin-path(及约定位置)里定位;也可以显式指向工程目录、target/、target/classes 或一个 jar(解析规则见可视化开发台 · 热加载)。 响应里的 artifactPath / artifactType 说明宿主真正加载了哪个目录,接口数、组件数、Mapper 数是装载结果。

不想每次手动 reload,让宿主盯着产物:

curl -X POST http://127.0.0.1:8086/app/dev/watch `
  -H "Content-Type: application/json" `
  -H "Authorization: Bearer $TOKEN" -H "clientid: $CLIENT" `
  -d '{ "appCode": "enterprise" }'

开启后每秒比对一次工程 target/ 的指纹(文件数 + 总字节 + 最大修改时间), 连续 2 秒不再变化才 reload —— 为了避开 Maven 编译中途的半个工程。于是日常回路只剩两件事:

改 Java → mvn compile → (产物稳定后自动热加载)→ 刷新页面

关闭监听(不卸载插件):DELETE /app/dev/watch/enterprise。监听是进程内状态,重启宿主后要重新开。

热加载只换后端代码:不建表、不同步菜单、不部署前端。首次安装这三件事走第 7 步。

第 6 步:改前端

独立插件仓库的前端都在本仓库直接构建,依赖来自已发布的 @wujies/*(ui-shared / components / api-client / ui-plugin-bundler-kit),vite.config.ts 只有一句 defineRemoteApp({ appCode, root }), 不需要单体仓库参与。

# 在应用仓库自己的 frontend/ 里构建,依赖来自已发布的 @wujies/*
cd C:/Users/you/code/wujies-hello-plugin/frontend
npm install
npm run dev       # vite build --watch:保存即增量重建 frontend/dist
npm run build     # 只构建一次

产物写在前端工程自己的 dist/,宿主前端(plus-ui)的 dev server 对 /remote-apps/<编码>/** 本来就优先读插件工程的产物,所以保存 → 等一句 built in ... → 刷新浏览器就能看到效果。 这里没有 HMR,也不能起第二个独立 dev server:远程应用与宿主共享同一份 Vue / Element Plus 实例。

宿主 dev server 要能找到你的插件仓库。它默认扫与宿主同级的 wujies-*-plugin 目录和几个单体仓库候选; clone 在别处时用环境变量显式告诉它(分号或逗号分隔多个):

$env:VITE_PLUGIN_REPOS = "C:/Users/you/code/wujies-enterprise-plugin;C:/Users/you/code/wujies-hello-plugin"
pnpm dev

改前端不需要调任何后端接口:/remote-apps/** 是静态读取,刷新页面即可。

第 7 步:建表与菜单(本地安装)

菜单、表结构、前端落位都不在热加载的职责里,由「本地安装」补齐。命令行直接调同一个接口:

curl -X POST http://127.0.0.1:8086/app/dev/install `
  -H "Content-Type: application/json" `
  -H "Authorization: Bearer $TOKEN" -H "clientid: $CLIENT" `
  -d '{ "code": "enterprise", "watch": false }'

zipPath 省略时,服务端按当前已构建产物现场组装一个开发包再装。它依次做六件事: 落位(后端 jar / 前端产物 / 迁移脚本)→ 执行未跑过的迁移 → 登记 <清单版本>-dev 开发版本(草稿态,不进应用市场) → 按 app.json 幂等同步菜单并授予当前租户管理员 → 加载后端 target/classes → 可选开启监听。

响应里 devVersion、deployment、migrations、menus、artifact 分别对应这几步。同一套产物重复安装是幂等的。

zipPath 只能位于插件工程目录、仓库上一级的 .plugin-dist 内。 完整字段与限制见可视化开发台 · 本地安装。

第 8 步:打开发包

开发包 = 未签名的 zip,内容与正式包一致,用于联调和把产物搬到另一台宿主。

每个独立插件仓库根都带完整的 package.ps1 / package.cmd(母本是单体仓库根那份 wujies-plugins/package.ps1,各仓用 node wujies-plugins/scripts/sync-package-script.mjs 同步,字节级一致),clone 下来就能在本仓库根体检、构建、签名、出包, 不需要旁边的单体仓库,也不用手工传前端产物路径:

# ① 首次装前端依赖(之后不用再来)
cd C:/Users/you/code/wujies-enterprise-plugin/frontend
npm install

# ② 打包前体检:几秒返回,不构建
cd C:/Users/you/code/wujies-enterprise-plugin
powershell -NoProfile -ExecutionPolicy Bypass -File .\package.ps1 -CheckOnly

# ③ 开发包:不签名,前端跑 npm run build、后端跑 mvn,落 ..\.plugin-dist\enterprise\dev
powershell -NoProfile -ExecutionPolicy Bypass -File .\package.ps1 -Dev

# ③’ 正式包:去掉 -Dev,用本仓库 keys\private.pem 签名(没有密钥先 -Keygen,见「密钥与签名」)
powershell -NoProfile -ExecutionPolicy Bypass -File .\package.ps1

双击 package.cmd 等价于③(Windows 默认拒绝直接运行 .ps1)。命令在独立仓库里不要传 -Plugin (它按「相对仓库根的子目录」找清单,传了报「找不到插件目录」)。早期单体仓库那种「一个仓库装多个应用」的 布局才需要 -Plugin <子目录> 并走 pnpm --filter;那棵仓库已下线、不再是入口,详见打包应用。

只想复用现成产物(改清单、重打 zip)加 -SkipBuild:

# 前提:frontend/dist/index.js 与 target/<artifactId>-<版本>.jar 都已存在
.\package.ps1 -Dev -SkipBuild

# 纯前端应用(如 finance)没有后端,一条命令直接出包:脚本自己跳过 JDK/Maven 探测
cd C:/Users/you/code/wujies-finance-plugin
.\package.ps1 -Dev

⚠️ 带前端的仓库不要用 -SkipFrontend:它连「复制前端产物」这一步一起跳过, 实测这样出的包只有 app.json(0.7 KB),装上去没有界面。-SkipFrontend 只用于没有前端的纯后端应用。

⚠️ 独立仓库里不用传 -FrontendProject(传了会被忽略并打一行灰字提示)—— 那种布局的前端工程固定就是仓库根 frontend/、产物 frontend/dist。 -FrontendProject 只在单体布局有意义,而单体 frontend/ 下现在只剩模板 hello-app(拆仓前那 10 个旧副本已删除)。 -FrontendDist 也一般不用传,只有想从别处取现成产物时才给。

历史写法(已不推荐):早期各仓的 package.ps1 只是薄包装,要先自己 npm run build, 再调单体仓库那份通用脚本并补 -Plugin <该仓库绝对路径> -FrontendDist <那份 dist> -SkipBuild。 通用脚本 + 这三个参数至今仍能跑通,但正常打包请直接在仓库根执行上面的命令。

实测(-Dev 出包内容,各应用的产物都在自己仓库的 frontend/dist / target):quote 出 app.json + route-index.json + backend/wujies-quote-plugin-1.0.1.jar + frontend/index.js(99.9 KB)+ frontend/assets/quote-app.css + migrations/01-create-quote-tables.sql;enterprise 同结构;finance(纯前端)只有 app.json + frontend/; gallery 出 gallery-1.0.5.zip(含 frontend/index.js 344 KB 与 assets/gallery-app.css)。

不带 -SkipBuild 时脚本会先跑 Maven 与前端构建。产物落在 <仓库上一级>/.plugin-dist/<编码>/<编码>-<版本>-dev.zip,结构是 app.json + backend/<artifactId>-<版本>.jar + frontend/** + migrations/*.sql + route-index.json。

这里的「版本」一律取 app.json 的 version,不是 Maven 的 <version>:脚本会按清单版本重命名包内的 jar (所以 pom.xml 继承父 POM 的 1.0.0、清单写 1.0.1 是正常的,包内文件名与 assets.backend 始终对得上)。 只想临时出另一个版本,传 -Version 1.0.2 —— 它只改写包内那份 app.json,不会动源文件。

也可以不碰命令行,让宿主替你在该应用仓库里执行 package.ps1:POST /app/dev/package { "code": "enterprise" } 返回 zipPath,GET /app/dev/package/enterprise/download 下载最近一次产出的 zip(不重新构建) —— 这两步正是开发台「打包」抽屉背后做的事,参数(版本、开发包、只用现有产物)与实时进度见 可视化开发台 · 打包。

开发包不签名:security.mode=warn/off 的部署可以用,enforce 模式不能上传安装。 正式上架要签名,见密钥与签名:

# 在本应用仓库根;私钥就是该仓库的 keys\private.pem,没传 -SigningKey 时脚本会自动找到它
.\package.ps1

第 9 步:上架

应用上架 → 应用管理 → 选中应用 → 上传新版本(zip)→ 发布 → 部署

部署前会做白名单、摘要与签名校验,并按 app.json 同步菜单、执行 migrations/。 接口路径是 POST /app/version/{versionId}/deploy。装卸各做了什么见 应用生命周期。

速查:改了什么,跑什么

改了什么手动做法是否要宿主重启
后端 Javamvn ... compile -DskipTests →(开着监听就等自动 reload,否则)POST /app/dev/reload否
前端 Vue / TSnpm run dev(或 build 一次)→ 刷新浏览器否
表结构新增 migrations/0N-<描述>.sql → POST /app/dev/install否
菜单 / 按钮权限改 app.json 的 menus → POST /app/dev/install否
版本号 / 产物路径改 app.json → 重新构建并打包否
clone 了新仓库放进 plugins/wujies-<code>-plugin/,或写进 fixed-plugin-path是(启动时扫描)
切换 dev-mode / fixed-plugin-path / maven-command改 yml是

已知前提与坑

坑现状绕法
平台构件没进公共 Maven 仓库父 POM 与 wujies-common-* 只有本机 ~/.m2 版本先按「第一次在本机编译插件」装一次
打包脚本有 11 份副本每个仓库带的是完整脚本(母本 wujies-plugins/package.ps1),改了母本不单独生效只改母本,然后 node wujies-plugins/scripts/sync-package-script.mjs 同步;提交前用 --check 确认没有漂移(有差异退出码 1,它同时会校对 package.cmd 入口)
存量工程的 npm run typecheck 还没修旧业务代码里的类型报错,构建不受影响以 npm run build + 宿主 npm run tools:check 为准
cd … && npm run build 在 Windows PowerShell 5.1 里报错&& 是 PowerShell 7 / cmd 的写法;开发台的「启动前端」由宿主后端用 cmd.exe /c npm run dev 执行(工作目录设成该仓库的 frontend/),所以界面上那条 cd … && … 只是给人看的等价写法,程序并没有整条丢给 shell自己贴进 PowerShell 5.1 时拆成两行(先 cd,再 npm run build),或用分号 ; 分隔
启动时没加载新写的插件工程还没有编译产物先 mvn compile,再重启宿主或点「一键开发」
正在跑的宿主 JVM 时执行 mvn install会 NoClassDefFoundError先停 JVM 再安装构件

签名校验绕过、不写 active-plugins.txt、监听不落库等限制,是开发模式刻意为之, 逐条列在可视化开发台 · 热加载 / 监听不做什么。

这套接口只在开发模式存在

dev-mode=false 时这些接口一律 404,enforce 部署也不会因为你 clone 了源码就允许加载未签名产物。 绝不要在生产环境开启开发模式:它绕过签名、白名单与校验和。

相关阅读

目录