@axewo/fin-micro 0.0.0-stage → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +223 -2
- package/cli/index.mjs +240 -0
- package/dist/browser.cjs +1 -0
- package/dist/browser.mjs +169 -0
- package/dist/core.cjs +1 -0
- package/dist/core.mjs +654 -0
- package/dist/iframe.cjs +1 -0
- package/dist/iframe.mjs +146 -0
- package/dist/index.cjs +1 -0
- package/dist/index.mjs +35 -0
- package/dist/router-view-impl-B1NzCGKV.mjs +76 -0
- package/dist/router-view-impl-BvOToV_Z.cjs +1 -0
- package/dist/runtime.cjs +1 -0
- package/dist/runtime.mjs +149 -0
- package/dist/sdk.cjs +1 -0
- package/dist/sdk.mjs +118 -0
- package/dist/types/browser/index.d.ts +46 -0
- package/dist/types/core/index.d.ts +63 -0
- package/dist/types/iframe/index.d.ts +77 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/runtime/index.d.ts +21 -0
- package/dist/types/runtime/lifecycle.d.ts +23 -0
- package/dist/types/sdk/index.d.ts +397 -0
- package/dist/types/vue/index.d.ts +102 -0
- package/dist/types/vue/router-view-impl.d.ts +41 -0
- package/dist/types/vue/router-view.d.ts +42 -0
- package/dist/types/vue/tab-context.d.ts +6 -0
- package/dist/vue.cjs +1 -0
- package/dist/vue.mjs +180 -0
- package/package.json +91 -3
- package/scaffold/base/index.html +12 -0
- package/scaffold/base/package.json.template +33 -0
- package/scaffold/base/scripts/check-runtime.mjs +43 -0
- package/scaffold/base/src/components/.gitkeep +1 -0
- package/scaffold/base/src/components/AppProviders.vue +25 -0
- package/scaffold/base/src/hooks/.gitkeep +1 -0
- package/scaffold/base/src/layout/.gitkeep +1 -0
- package/scaffold/base/src/locales/.gitkeep +1 -0
- package/scaffold/base/src/locales/en.ts +12 -0
- package/scaffold/base/src/locales/index.ts +13 -0
- package/scaffold/base/src/locales/zh-CN.ts +12 -0
- package/scaffold/base/src/main.ts +22 -0
- package/scaffold/base/src/router/.gitkeep +1 -0
- package/scaffold/base/src/router/demo.ts +6 -0
- package/scaffold/base/src/router/index.ts +13 -0
- package/scaffold/base/src/stores/.gitkeep +1 -0
- package/scaffold/base/src/stores/demo.ts +12 -0
- package/scaffold/base/src/stores/index.ts +11 -0
- package/scaffold/base/src/styles/index.css +18 -0
- package/scaffold/base/src/types/.gitkeep +1 -0
- package/scaffold/base/src/utils/.gitkeep +1 -0
- package/scaffold/base/src/utils/appConfig.ts +49 -0
- package/scaffold/base/src/utils/errorUtils.ts +92 -0
- package/scaffold/base/src/utils/requestUtils.ts +144 -0
- package/scaffold/base/src/views/.gitkeep +1 -0
- package/scaffold/base/src/views/DemoView.vue +26 -0
- package/scaffold/base/tsconfig.json +16 -0
- package/scaffold/base/vite.config.ts +8 -0
- package/scaffold/host/.env.example +13 -0
- package/scaffold/host/src/App.vue +4 -0
- package/scaffold/host/src/bootstrap.ts +50 -0
- package/scaffold/host/src/hooks/usePlatformSession.ts +91 -0
- package/scaffold/host/src/layout/HostHeader.vue +69 -0
- package/scaffold/host/src/layout/HostLayout.vue +168 -0
- package/scaffold/host/src/layout/HostMenu.vue +15 -0
- package/scaffold/host/src/layout/HostTabs.vue +47 -0
- package/scaffold/host/src/layout/HostWorkspace.vue +26 -0
- package/scaffold/host/src/localPages.ts +45 -0
- package/scaffold/host/src/platform.ts +239 -0
- package/scaffold/host/src/router/guards.ts +128 -0
- package/scaffold/host/src/router/index.ts +25 -0
- package/scaffold/host/src/router/targets.ts +30 -0
- package/scaffold/host/src/router/workspace.ts +63 -0
- package/scaffold/host/src/styles/shell.css +761 -0
- package/scaffold/host/src/types/platform.ts +12 -0
- package/scaffold/host/src/utils/request.ts +42 -0
- package/scaffold/host/src/views/HomeView.vue +19 -0
- package/scaffold/host/src/views/LoginRoute.vue +16 -0
- package/scaffold/host/src/views/LoginView.vue +88 -0
- package/scaffold/host/src/views/NotFoundView.vue +1 -0
- package/scaffold/host/src/views/ProfileView.vue +24 -0
- package/scaffold/host/src/views/WorkspaceRoute.vue +11 -0
- package/scaffold/vue/src/App.vue +24 -0
- package/scaffold/vue/src/bootstrap.ts +67 -0
- package/scaffold/vue/src/bridge/auth.ts +23 -0
- package/scaffold/vue/src/bridge/session.ts +16 -0
- package/scaffold/vue/src/main.ts +13 -0
- package/scaffold/vue/src/router/index.ts +16 -0
- package/scaffold/vue/src/views/DetailView.vue +57 -0
- package/scaffold/vue/src/views/ListView.vue +55 -0
- package/scaffold/vue/src/views/NotFoundView.vue +1 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { watch } from 'vue'
|
|
2
|
+
import type { NavigationGuardReturn, RouteLocationNormalized, Router } from 'vue-router'
|
|
3
|
+
import type { PlatformSessionController } from '../hooks/usePlatformSession'
|
|
4
|
+
import type { PlatformSession } from '../types/platform'
|
|
5
|
+
import { canOpenRoute } from '../platform'
|
|
6
|
+
import { authorizedRedirect, routeTarget } from './targets'
|
|
7
|
+
|
|
8
|
+
/** 扩展只接收本次公开身份及导航快照;signal 在新导航、注销或守卫移除时协作取消。 */
|
|
9
|
+
export interface HostGuardContext {
|
|
10
|
+
to: RouteLocationNormalized
|
|
11
|
+
from: RouteLocationNormalized
|
|
12
|
+
session: PlatformSession
|
|
13
|
+
signal: AbortSignal
|
|
14
|
+
}
|
|
15
|
+
export interface HostGuardOptions {
|
|
16
|
+
/** 已通过内置认证及路由白名单后执行;false 取消,void/true 继续,不得替代服务端鉴权。 */
|
|
17
|
+
beforeEnter?: (context: HostGuardContext) => boolean | void | Promise<boolean | void>
|
|
18
|
+
/** 接收恢复、扩展或离页异常;调用方必须脱敏展示,回调自身异常不会放行导航。 */
|
|
19
|
+
onError?: (error: unknown) => void
|
|
20
|
+
}
|
|
21
|
+
/** 工作区只返回核心事务对应的放行/取消结果,不重复实现认证或重定向策略。 */
|
|
22
|
+
export type WorkspaceGuard = (context: HostGuardContext) => NavigationGuardReturn | Promise<NavigationGuardReturn>
|
|
23
|
+
const installations = new WeakMap<Router, {
|
|
24
|
+
workspace?: WorkspaceGuard
|
|
25
|
+
invalidate: () => void
|
|
26
|
+
dispose: () => void
|
|
27
|
+
}>()
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 每个 router 只安装一次统一前置守卫:等待恢复、认证、授权、业务扩展,最后委托核心离页事务。
|
|
31
|
+
* @param router 当前 App 独享的 Vue Router。
|
|
32
|
+
* @param controller 当前 App 的认证控制器;ready 只等待首次恢复,之后每次读取最新 session。
|
|
33
|
+
* @param options 可选前置扩展与脱敏错误接收器;不额外注册 beforeEach。
|
|
34
|
+
* @returns 幂等撤销函数;重挂载及启动失败必须调用,取消所有待决导航并移除工作区委托。
|
|
35
|
+
* @throws 重复安装时抛错;异步恢复、扩展或委托异常会报告并取消导航,不形成重定向循环。
|
|
36
|
+
*/
|
|
37
|
+
export function installHostGuards(router: Router, controller: PlatformSessionController, options: HostGuardOptions = {}): () => void {
|
|
38
|
+
if (installations.has(router)) throw new Error('当前路由器已安装宿主守卫')
|
|
39
|
+
let disposed = false
|
|
40
|
+
let generation = 0
|
|
41
|
+
let restored = false
|
|
42
|
+
let pending: AbortController | undefined
|
|
43
|
+
const invalidate = () => { generation++; pending?.abort() }
|
|
44
|
+
// 首次恢复提交身份是 ready 的一部分,不取消首次导航;之后的身份替换立即撤销旧事务。
|
|
45
|
+
const offSession = watch(controller.session, () => { if (restored) invalidate() }, { flush: 'sync' })
|
|
46
|
+
const installation = { invalidate, dispose: () => {}, workspace: undefined as WorkspaceGuard | undefined }
|
|
47
|
+
const off = router.beforeEach(async (to, from) => {
|
|
48
|
+
invalidate()
|
|
49
|
+
const current = generation
|
|
50
|
+
const navigation = new AbortController()
|
|
51
|
+
pending = navigation
|
|
52
|
+
const valid = () => !disposed && current === generation && !navigation.signal.aborted
|
|
53
|
+
// 取消只终止导航等待,不假定能终止后台副作用;迟到成功和异常均由 race 消费。
|
|
54
|
+
const wait = async <T>(value: T | Promise<T>): Promise<T | undefined> => {
|
|
55
|
+
let cancel = () => {}
|
|
56
|
+
try {
|
|
57
|
+
return await Promise.race([value, new Promise<undefined>(resolve => {
|
|
58
|
+
cancel = () => resolve(undefined)
|
|
59
|
+
navigation.signal.addEventListener('abort', cancel, { once: true })
|
|
60
|
+
if (navigation.signal.aborted) cancel()
|
|
61
|
+
})])
|
|
62
|
+
} finally { navigation.signal.removeEventListener('abort', cancel) }
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
await wait(controller.ready)
|
|
66
|
+
if (!valid()) return false
|
|
67
|
+
restored = true
|
|
68
|
+
// 恢复在控制器内有代次保护;这里绝不缓存 await 之前的身份,避免旧恢复放行。
|
|
69
|
+
const session = controller.session.value
|
|
70
|
+
if (to.meta.authenticated && !session) return { path: '/login', query: { redirect: to.fullPath }, replace: true }
|
|
71
|
+
if (to.path === '/login' && session) {
|
|
72
|
+
const target = router.resolve(authorizedRedirect(router, session, to.query.redirect))
|
|
73
|
+
return { path: target.path, query: target.query, replace: true }
|
|
74
|
+
}
|
|
75
|
+
// 注销和真实失效优先移除身份;此时不能再调用旧工作区的未保存守卫。
|
|
76
|
+
if (!session) return
|
|
77
|
+
const context = { to, from, session, signal: navigation.signal }
|
|
78
|
+
if (to.meta.authenticated) {
|
|
79
|
+
const target = routeTarget(to)
|
|
80
|
+
const permissions = to.meta.permissions
|
|
81
|
+
const forbidden = (to.path !== '/workspace' || to.query.tab !== undefined) && (!target || !canOpenRoute(session, target))
|
|
82
|
+
const denied = permissions !== undefined && (!Array.isArray(permissions) || !permissions.every(value => typeof value === 'string' && session.permissions.includes(value)))
|
|
83
|
+
if (forbidden || denied) return to.path === '/home' ? false : { path: '/home', replace: true }
|
|
84
|
+
const accepted = await wait(options.beforeEnter?.(context))
|
|
85
|
+
if (!valid() || controller.session.value !== session) return false
|
|
86
|
+
if (accepted === false) return false
|
|
87
|
+
}
|
|
88
|
+
const result = await wait(installation.workspace?.(context))
|
|
89
|
+
if (!valid() || controller.session.value !== session) return false
|
|
90
|
+
return result
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (valid()) {
|
|
93
|
+
try { options.onError?.(error) } catch { /* 展示异常不能成为放行依据。 */ }
|
|
94
|
+
}
|
|
95
|
+
return false
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
installation.dispose = () => {
|
|
99
|
+
if (disposed) return
|
|
100
|
+
disposed = true
|
|
101
|
+
invalidate()
|
|
102
|
+
off()
|
|
103
|
+
offSession()
|
|
104
|
+
installation.workspace = undefined
|
|
105
|
+
installations.delete(router)
|
|
106
|
+
}
|
|
107
|
+
installations.set(router, installation)
|
|
108
|
+
return installation.dispose
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 将当前工作区的核心离页事务挂到统一守卫,不注册第二个 beforeEach。
|
|
113
|
+
* @param router 已调用 installHostGuards 的路由器。
|
|
114
|
+
* @param guard 工作区事务委托;正常离页必须等待其结果,认证失效时不调用。
|
|
115
|
+
* @returns 幂等撤销函数;工作区卸载时调用,旧撤销函数不会删除新工作区的委托。
|
|
116
|
+
* @throws 未安装统一守卫或已有存活工作区时抛错,防止悄悄生成重复导航状态。
|
|
117
|
+
*/
|
|
118
|
+
export function attachWorkspaceGuard(router: Router, guard: WorkspaceGuard): () => void {
|
|
119
|
+
const installation = installations.get(router)
|
|
120
|
+
if (!installation) throw new Error('请先安装宿主路由守卫')
|
|
121
|
+
if (installation.workspace) throw new Error('当前路由器已连接工作区')
|
|
122
|
+
installation.workspace = guard
|
|
123
|
+
return () => {
|
|
124
|
+
if (installation.workspace !== guard) return
|
|
125
|
+
installation.invalidate()
|
|
126
|
+
installation.workspace = undefined
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createRouter, createWebHistory, type RouterHistory } from 'vue-router'
|
|
2
|
+
import LoginRoute from '../views/LoginRoute.vue'
|
|
3
|
+
import WorkspaceRoute from '../views/WorkspaceRoute.vue'
|
|
4
|
+
import NotFoundView from '../views/NotFoundView.vue'
|
|
5
|
+
import type { PlatformSessionController } from '../hooks/usePlatformSession'
|
|
6
|
+
import { installHostGuards, type HostGuardOptions } from './guards'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 创建独占顶级 URL 的宿主路由器,生产服务器需配置 SPA 回退。
|
|
10
|
+
* @param controller 本轮 App 的认证控制器;统一守卫等待其首次恢复。
|
|
11
|
+
* @param history 本轮 history,可在测试中传入 memory history。
|
|
12
|
+
* @param guards 可选业务准入扩展,例如按 to.meta.permissions 声明权限,或通过 beforeEnter 校验业务条件。
|
|
13
|
+
* @returns 带 disposeGuards 的 router;卸载须先撤销守卫,再销毁 history。
|
|
14
|
+
* @throws 路由构造或守卫安装失败时抛错,由 bootstrap 失败清理处理。
|
|
15
|
+
*/
|
|
16
|
+
export function createAppRouter(controller: PlatformSessionController, history: RouterHistory = createWebHistory(import.meta.env.BASE_URL), guards: HostGuardOptions = {}) {
|
|
17
|
+
const router = createRouter({ history, routes: [
|
|
18
|
+
{ path: '/', redirect: '/home' },
|
|
19
|
+
{ path: '/login', component: LoginRoute },
|
|
20
|
+
{ path: '/:page(home|profile|workspace)', component: WorkspaceRoute, meta: { authenticated: true } },
|
|
21
|
+
{ path: '/:pathMatch(.*)*', component: NotFoundView },
|
|
22
|
+
] })
|
|
23
|
+
const disposeGuards = installHostGuards(router, controller, guards)
|
|
24
|
+
return Object.assign(router, { disposeGuards })
|
|
25
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { RouteLocationNormalized, Router } from 'vue-router'
|
|
2
|
+
import { decodeMicroLocation, encodeMicroLocation, type OpenTab } from '@axewo/fin-micro'
|
|
3
|
+
import { localTarget, LOCAL_APP_ID } from '../localPages'
|
|
4
|
+
import { canOpenRoute } from '../platform'
|
|
5
|
+
import type { PlatformSession } from '../types/platform'
|
|
6
|
+
|
|
7
|
+
/** URL 只携带公开导航元信息;授权仍由会话及 core 双重检查。 */
|
|
8
|
+
export function routeTarget(route: Pick<RouteLocationNormalized, 'path' | 'query'>): OpenTab | null {
|
|
9
|
+
if (route.path === '/home') return localTarget('home')
|
|
10
|
+
if (route.path === '/profile') return localTarget('profile')
|
|
11
|
+
if (route.path !== '/workspace' || typeof route.query.tab !== 'string') return null
|
|
12
|
+
try {
|
|
13
|
+
const target = decodeMicroLocation(route.query.tab)
|
|
14
|
+
return target && target.appId !== LOCAL_APP_ID ? { ...target, title: '业务页面' } : null
|
|
15
|
+
} catch { return null }
|
|
16
|
+
}
|
|
17
|
+
export function tabLocation(tab?: OpenTab) {
|
|
18
|
+
if (!tab) return { path: '/workspace', query: {} }
|
|
19
|
+
if (tab.appId === LOCAL_APP_ID) return { path: tab.route, query: {} }
|
|
20
|
+
return { path: '/workspace', query: { tab: encodeMicroLocation(tab) } }
|
|
21
|
+
}
|
|
22
|
+
/** 只接受可解析、同站且当前会话已授权的固定宿主路由,不回显任意输入。 */
|
|
23
|
+
export function authorizedRedirect(router: Router, session: PlatformSession, value: unknown): string {
|
|
24
|
+
if (typeof value !== 'string' || !value.startsWith('/') || value.startsWith('//') || /[\\\u0000-\u0020\u007f]/.test(value)) return '/home'
|
|
25
|
+
try {
|
|
26
|
+
const resolved = router.resolve(value)
|
|
27
|
+
const target = routeTarget(resolved)
|
|
28
|
+
return target && !resolved.hash && canOpenRoute(session, target) ? router.resolve(tabLocation(target)).fullPath : '/home'
|
|
29
|
+
} catch { return '/home' }
|
|
30
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { connectMicroBrowser, type MicroHost, type MicroBrowserOptions } from '@axewo/fin-micro'
|
|
2
|
+
import type { Router } from 'vue-router'
|
|
3
|
+
import { routeTarget, tabLocation } from './targets'
|
|
4
|
+
import { localTarget, workspaceOperation } from '../localPages'
|
|
5
|
+
import { attachWorkspaceGuard } from './guards'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 渲染前先登记主页,旧存储与深链恢复复用同一核心状态,主页始终首位且不重复。
|
|
9
|
+
* @param host 已认证且尚未挂载业务组件的宿主。
|
|
10
|
+
* @param router 已完成首次导航的路由器,提供授权后的初始深链。
|
|
11
|
+
* @param options 用户隔离存储及错误回调;URL 始终交给 Vue Router。
|
|
12
|
+
* @returns 浏览器存储连接器,卸载时须调用 dispose。
|
|
13
|
+
* @throws 主页准入失败时抛错;存储或恢复错误由 options.onError 接收。
|
|
14
|
+
*/
|
|
15
|
+
export function connectWorkspaceBrowser(host: MicroHost, router: Router, options: MicroBrowserOptions) {
|
|
16
|
+
host.openTab(localTarget('home'))
|
|
17
|
+
return connectMicroBrowser(host, { ...options, url: false, initialLocation: routeTarget(router.currentRoute.value) })
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 统一 URL 与核心状态;工作区只挂接离页事务,不另建 beforeEach。
|
|
22
|
+
* @param host 当前工作区唯一宿主,所有离页仍委托核心 beforeLeave。
|
|
23
|
+
* @param router 已安装 installHostGuards 的路由器。
|
|
24
|
+
* @param authenticated 当前身份仍有效且未进入退出确认时返回 true。
|
|
25
|
+
* @param onError 脱敏错误接收器;事务异常会取消导航,不修改固定主页策略。
|
|
26
|
+
* @returns 带 dispose 的连接,卸载时撤销委托、取消待决事务并释放状态订阅。
|
|
27
|
+
* @throws 统一守卫未安装或工作区重复连接时抛错。
|
|
28
|
+
*/
|
|
29
|
+
export function connectWorkspaceRouter(host: MicroHost, router: Router, authenticated: () => boolean, onError: (error: unknown) => void = () => {}) {
|
|
30
|
+
let navigating = false
|
|
31
|
+
let disposed = false
|
|
32
|
+
let generation = 0
|
|
33
|
+
const currentLocation = () => tabLocation(host.getState().tabs.find(tab => tab.tabId === host.getState().activeTabId))
|
|
34
|
+
function sync(replace = false): void {
|
|
35
|
+
if (disposed || navigating || !authenticated()) return
|
|
36
|
+
const target = currentLocation()
|
|
37
|
+
if (router.resolve(target).fullPath === router.currentRoute.value.fullPath) return
|
|
38
|
+
void (replace ? router.replace(target) : router.push(target)).catch(onError)
|
|
39
|
+
}
|
|
40
|
+
const offGuard = attachWorkspaceGuard(router, async ({ to, signal }) => {
|
|
41
|
+
const current = ++generation
|
|
42
|
+
navigating = false
|
|
43
|
+
// 认证失效由统一守卫优先放行清理;仍有身份但正在退出确认时,拒绝新的工作区导航。
|
|
44
|
+
if (!authenticated()) return false
|
|
45
|
+
const target = routeTarget(to)
|
|
46
|
+
const active = host.getState().tabs.find(tab => tab.tabId === host.getState().activeTabId)
|
|
47
|
+
if (target && active?.appId === target.appId && active.tabId === target.tabId && active.route === target.route) return
|
|
48
|
+
// 空工作区路径保留当前页签;外部页面或登录页离开前统一确认全部页签。
|
|
49
|
+
if (to.path === '/workspace' && !target) return
|
|
50
|
+
navigating = true
|
|
51
|
+
try {
|
|
52
|
+
const result = await host.operate(workspaceOperation(target ? { type: 'open', input: target, signal } : { type: 'closeMany', scope: 'all', signal }, host.getState().tabs))
|
|
53
|
+
if (disposed || current !== generation) return false
|
|
54
|
+
return result.status === 'committed' || result.status === 'noop'
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (!disposed && current === generation) onError(error)
|
|
57
|
+
return false
|
|
58
|
+
} finally { if (current === generation) navigating = false }
|
|
59
|
+
})
|
|
60
|
+
const off = host.subscribe(() => sync())
|
|
61
|
+
sync(true)
|
|
62
|
+
return { dispose() { disposed = true; generation++; off(); offGuard() } }
|
|
63
|
+
}
|