手动开发与本地运行
这一页是不用后台页面的那条路:全程命令行为,把「clone 一个已发布的应用仓库 → 本地改后端 + 改前端 → 跑起来 → 打包 → 上架」 每一步的确切命令写清楚。看得懂这页,就能不打开开发台也把开发做完。
它对照的是 Halo 的两种插件开发方式:
| Halo | Wujies | 特点 |
|---|---|---|
./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.gitwujies-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 -DskipTestsoverview 里每个应用的 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。装卸各做了什么见
应用生命周期。
速查:改了什么,跑什么
| 改了什么 | 手动做法 | 是否要宿主重启 |
|---|---|---|
| 后端 Java | mvn ... compile -DskipTests →(开着监听就等自动 reload,否则)POST /app/dev/reload | 否 |
| 前端 Vue / TS | npm 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 了源码就允许加载未签名产物。
绝不要在生产环境开启开发模式:它绕过签名、白名单与校验和。