更新 @kevisual/api 版本至 0.0.21,添加 router-api-proxy.ts 和 proxy.ts 文件,重构 QueryProxy 类以优化 API 查询初始化
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@kevisual/api",
|
"name": "@kevisual/api",
|
||||||
"version": "0.0.20",
|
"version": "0.0.21",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "mod.ts",
|
"main": "mod.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,490 +1,3 @@
|
|||||||
import { QueryClient as Query, Result } from '@kevisual/query';
|
export * from './proxy.ts';
|
||||||
import { QueryRouterServer, Route } from '@kevisual/router/src/route.ts';
|
|
||||||
import { filter } from '@kevisual/js-filter'
|
|
||||||
import { EventEmitter } from 'eventemitter3';
|
|
||||||
|
|
||||||
export const RouteTypeList = ['api', 'context', 'worker', 'page'] as const;
|
export { initApi } from './router-api-proxy.ts';
|
||||||
export type RouterViewItemInfo = RouterViewApi | RouterViewContext | RouterViewWorker | RouteViewPage;
|
|
||||||
export type RouterViewItem<T = {}> = RouterViewItemInfo & T;
|
|
||||||
|
|
||||||
type RouteViewBase = {
|
|
||||||
/**
|
|
||||||
* _id 用于纯本地存储标识
|
|
||||||
*/
|
|
||||||
_id?: string;
|
|
||||||
id?: string;
|
|
||||||
title?: string;
|
|
||||||
description?: string;
|
|
||||||
enabled?: boolean;
|
|
||||||
/**
|
|
||||||
* 响应数据
|
|
||||||
*/
|
|
||||||
response?: any;
|
|
||||||
/**
|
|
||||||
* 默认动作配置
|
|
||||||
*/
|
|
||||||
action?: { path?: string; key?: string; id?: string; payload?: any;[key: string]: any };
|
|
||||||
}
|
|
||||||
export type RouterViewApi = {
|
|
||||||
type: 'api',
|
|
||||||
api: {
|
|
||||||
url: string,
|
|
||||||
// 已初始化的query实例,不需要编辑配置
|
|
||||||
query?: Query
|
|
||||||
}
|
|
||||||
} & RouteViewBase;
|
|
||||||
|
|
||||||
export type RouterViewContext = {
|
|
||||||
type: 'context',
|
|
||||||
context: {
|
|
||||||
key: string,
|
|
||||||
// 从context中获取router,不需要编辑配置
|
|
||||||
router?: QueryRouterServer
|
|
||||||
}
|
|
||||||
} & RouteViewBase;
|
|
||||||
export type RouterViewWorker = {
|
|
||||||
type: 'worker',
|
|
||||||
worker: {
|
|
||||||
type: 'Worker' | 'SharedWorker' | 'serviceWorker',
|
|
||||||
url: string,
|
|
||||||
// 已初始化的worker实例,不需要编辑配置
|
|
||||||
worker?: Worker | SharedWorker | ServiceWorker,
|
|
||||||
/**
|
|
||||||
* worker选项
|
|
||||||
* default: { type: 'module' }
|
|
||||||
*/
|
|
||||||
workerOptions?: {
|
|
||||||
type: 'module' | 'classic'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} & RouteViewBase;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 去掉不需要保存都服务器的数据
|
|
||||||
* @param item
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const pickRouterViewData = (item: RouterViewItem) => {
|
|
||||||
const { action, response, _id, ...rest } = item;
|
|
||||||
if (rest.type === 'api') {
|
|
||||||
if (rest.api) {
|
|
||||||
delete rest.api.query;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (rest.type === 'worker') {
|
|
||||||
if (rest.worker) {
|
|
||||||
delete rest.worker.worker;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (rest.type === 'context') {
|
|
||||||
if (rest.context) {
|
|
||||||
delete rest.context.router;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return rest
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* 注入 js 的url地址,使用importScripts加载
|
|
||||||
*/
|
|
||||||
export type RouteViewPage = {
|
|
||||||
type: 'page',
|
|
||||||
page: {
|
|
||||||
url: string,
|
|
||||||
}
|
|
||||||
} & RouteViewBase;
|
|
||||||
|
|
||||||
export type RouterViewQuery = {
|
|
||||||
id: string,
|
|
||||||
query: string,
|
|
||||||
title: string
|
|
||||||
}
|
|
||||||
export type RouterViewData = {
|
|
||||||
data: { items: RouterViewItem[]; }
|
|
||||||
views: RouterViewQuery[];
|
|
||||||
viewId?: string;
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class QueryProxy {
|
|
||||||
router: QueryRouterServer;
|
|
||||||
token?: string;
|
|
||||||
routerViewItems: RouterViewItem[];
|
|
||||||
views: RouterViewQuery[];
|
|
||||||
emitter: EventEmitter;
|
|
||||||
constructor(opts?: { router?: QueryRouterServer, token?: string, routerViewData?: RouterViewData }) {
|
|
||||||
this.router = opts?.router || new QueryRouterServer();
|
|
||||||
this.token = opts?.token || this.getDefulatToken();
|
|
||||||
this.routerViewItems = opts?.routerViewData?.data?.items || [];
|
|
||||||
this.views = opts?.routerViewData?.views || [];
|
|
||||||
this.initRouterViewQuery();
|
|
||||||
this.emitter = new EventEmitter();
|
|
||||||
}
|
|
||||||
getDefulatToken() {
|
|
||||||
try {
|
|
||||||
if (typeof window !== 'undefined' && typeof window.localStorage !== 'undefined') {
|
|
||||||
return localStorage.getItem('token') || undefined;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
initRouterViewQuery() {
|
|
||||||
this.routerViewItems = this.routerViewItems.map(item => {
|
|
||||||
return this.initRouterView(item);
|
|
||||||
}).filter(item => {
|
|
||||||
const enabled = item.enabled ?? true;
|
|
||||||
return enabled;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
initRouterView(item: RouterViewItem) {
|
|
||||||
if (item.type === 'api' && item.api?.url) {
|
|
||||||
const url = item.api.url;
|
|
||||||
if (item?.api?.query) return item;
|
|
||||||
const query = new Query({ url: url });
|
|
||||||
item['api'] = { url: url, query: query };
|
|
||||||
}
|
|
||||||
if (item.type === 'worker' && item.worker?.url) {
|
|
||||||
let viewItem = item as RouterViewWorker;
|
|
||||||
if (!item.worker?.workerOptions?.type) {
|
|
||||||
item.worker.workerOptions = { ...item.worker.workerOptions, type: 'module' };
|
|
||||||
}
|
|
||||||
if (item.worker.worker) {
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
let worker: Worker | SharedWorker | ServiceWorker | undefined = undefined;
|
|
||||||
if (item.worker.type === 'SharedWorker') {
|
|
||||||
worker = new SharedWorker(item.worker.url, item.worker.workerOptions);
|
|
||||||
worker.port.start();
|
|
||||||
} else if (viewItem.worker.type === 'serviceWorker') {
|
|
||||||
if ('serviceWorker' in navigator) {
|
|
||||||
navigator.serviceWorker.register(viewItem.worker.url, item.worker.workerOptions).then(function (registration) {
|
|
||||||
console.debug('注册serviceWorker成功 ', registration.scope);
|
|
||||||
}, function (err) {
|
|
||||||
console.debug('注册 serviceWorker 失败: ', err);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.warn('当前浏览器不支持serviceWorker');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
worker = new Worker(viewItem.worker.url, item.worker.workerOptions);
|
|
||||||
}
|
|
||||||
viewItem['worker']['worker'] = worker;
|
|
||||||
}
|
|
||||||
if (item.type === 'context' && item.context?.key) {
|
|
||||||
if (item.context?.router) {
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
// @ts-ignore
|
|
||||||
const context = globalThis['context'] || {}
|
|
||||||
const router = context[item.context.key] as QueryRouterServer;
|
|
||||||
if (router) {
|
|
||||||
item['context']['router'] = router;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return item;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化路由
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
async init() {
|
|
||||||
const routerViewItems = this.routerViewItems || [];
|
|
||||||
if (routerViewItems.length === 0) {
|
|
||||||
// 默认初始化api类型路由
|
|
||||||
await this.initApi();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const item of routerViewItems) {
|
|
||||||
switch (item.type) {
|
|
||||||
case 'api':
|
|
||||||
await this.initApi(item);
|
|
||||||
break;
|
|
||||||
case 'context':
|
|
||||||
await this.initContext(item);
|
|
||||||
break;
|
|
||||||
case 'worker':
|
|
||||||
await this.initWorker(item);
|
|
||||||
break;
|
|
||||||
case 'page':
|
|
||||||
await this.initPage(item);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.emitter.emit('initComplete');
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* 监听初始化完成
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
async listenInitComplete(): Promise<boolean> {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
this.emitter.removeAllListeners('initComplete');
|
|
||||||
resolve(false);
|
|
||||||
}, 3 * 60000); // 3分钟超时
|
|
||||||
const func = () => {
|
|
||||||
clearTimeout(timer);
|
|
||||||
resolve(true);
|
|
||||||
}
|
|
||||||
this.emitter.once('initComplete', func);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
async initApi(item?: RouterViewApi) {
|
|
||||||
const that = this;
|
|
||||||
const query = item?.api?.query || new Query({ url: item?.api?.url || '/api/router' })
|
|
||||||
const res = await query.post<{ list: RouterItem[] }>({ path: "router", key: 'list', token: this.token });
|
|
||||||
if (res.code !== 200) {
|
|
||||||
console.error('Failed to init query proxy router:', res.message);
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const _list = res.data?.list || []
|
|
||||||
for (const r of _list) {
|
|
||||||
if (r.path || r.id) {
|
|
||||||
console.debug(`注册路由: [${r.path}] ${r?.key}`, 'API');
|
|
||||||
let metadata = r.metadata || {};
|
|
||||||
metadata.viewItem = item;
|
|
||||||
this.router.route({
|
|
||||||
path: r.path,
|
|
||||||
key: r.key || '',
|
|
||||||
id: r.id,
|
|
||||||
description: r.description,
|
|
||||||
metadata: metadata,
|
|
||||||
}).define(async (ctx) => {
|
|
||||||
const msg = { ...ctx.query };
|
|
||||||
if (msg.token === undefined && that.token !== undefined) {
|
|
||||||
msg.token = that.token;
|
|
||||||
}
|
|
||||||
const res = await query.post<any>({ path: r.path, key: r.key, ...msg });
|
|
||||||
ctx.forward(res)
|
|
||||||
}).addTo(that.router);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async initContext(item?: RouterViewContext) {
|
|
||||||
// @ts-ignore
|
|
||||||
const context = globalThis['context'] || {}
|
|
||||||
const router = item?.context?.router || context[item?.context?.key] as QueryRouterServer;
|
|
||||||
if (!router) {
|
|
||||||
console.warn(`未发现Context router ${item?.context?.key}`);
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const routes = router.getList();
|
|
||||||
for (const r of routes) {
|
|
||||||
console.debug(`注册路由: [${r.path}] ${r?.key}`, 'Context');
|
|
||||||
let metadata = r.metadata || {};
|
|
||||||
metadata.viewItem = item;
|
|
||||||
this.router.route({
|
|
||||||
path: r.path,
|
|
||||||
key: r.key || '',
|
|
||||||
id: r.id,
|
|
||||||
description: r.description,
|
|
||||||
metadata: metadata,
|
|
||||||
}).define(async (ctx) => {
|
|
||||||
const res = await router.run({ path: r.path, key: r.key, ...ctx.query });
|
|
||||||
ctx.forward(res)
|
|
||||||
}).addTo(this.router);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
generateId() {
|
|
||||||
return 'route_' + Math.random().toString(36).substring(2, 9);
|
|
||||||
}
|
|
||||||
async callWorker(msg: any, viewItem: RouterViewWorker['worker']): Promise<Result> {
|
|
||||||
const that = this;
|
|
||||||
const requestId = this.generateId();
|
|
||||||
const worker = viewItem?.worker;
|
|
||||||
if (!worker) {
|
|
||||||
return { code: 500, message: 'Worker未初始化' };
|
|
||||||
}
|
|
||||||
let port: MessagePort | Worker | ServiceWorker;
|
|
||||||
if (viewItem.type === 'SharedWorker') {
|
|
||||||
port = (worker as SharedWorker).port;
|
|
||||||
} else {
|
|
||||||
port = worker as Worker | ServiceWorker;
|
|
||||||
}
|
|
||||||
port.postMessage({
|
|
||||||
...msg,
|
|
||||||
requestId: requestId,
|
|
||||||
})
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
that.emitter.removeAllListeners(requestId);
|
|
||||||
resolve({ code: 500, message: '请求超时' });
|
|
||||||
}, 3 * 60 * 1000); // 3分钟超时
|
|
||||||
that.emitter.once(requestId, (res: any) => {
|
|
||||||
clearTimeout(timer);
|
|
||||||
resolve(res);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
async initWorker(item?: RouterViewWorker, initRoutes: boolean = true) {
|
|
||||||
const that = this;
|
|
||||||
if (!item?.worker?.url) {
|
|
||||||
console.warn('Worker URL not provided');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const viewItem = item.worker;
|
|
||||||
const worker = viewItem?.worker;
|
|
||||||
if (!worker) {
|
|
||||||
console.warn('Worker not initialized');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const callResponse = (e: MessageEvent) => {
|
|
||||||
const msg = e.data;
|
|
||||||
if (msg.requestId) {
|
|
||||||
const requestId = msg.requestId;
|
|
||||||
that.emitter.emit(requestId, msg);
|
|
||||||
} else {
|
|
||||||
that.router.run(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (item.worker.type === 'SharedWorker') {
|
|
||||||
const port = (worker as SharedWorker).port;
|
|
||||||
port.onmessage = callResponse;
|
|
||||||
port.start();
|
|
||||||
} else if (item.worker.type === 'serviceWorker') {
|
|
||||||
navigator.serviceWorker.addEventListener('message', callResponse);
|
|
||||||
} else {
|
|
||||||
(worker as Worker).onmessage = callResponse;
|
|
||||||
}
|
|
||||||
if (!initRoutes) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const callWorker = this.callWorker.bind(this);
|
|
||||||
|
|
||||||
const res = await callWorker({
|
|
||||||
path: "router",
|
|
||||||
key: 'list',
|
|
||||||
token: this.token,
|
|
||||||
}, viewItem);
|
|
||||||
|
|
||||||
if (res.code !== 200) {
|
|
||||||
console.error('Failed to init query proxy router:', res.message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const _list = res.data?.list || []
|
|
||||||
for (const r of _list) {
|
|
||||||
if (r.path || r.id) {
|
|
||||||
console.debug(`注册路由: [${r.path}] ${r?.key}`, 'API');
|
|
||||||
let metadata = r.metadata || {};
|
|
||||||
metadata.viewItem = item;
|
|
||||||
this.router.route({
|
|
||||||
path: r.path,
|
|
||||||
key: r.key || '',
|
|
||||||
id: r.id,
|
|
||||||
description: r.description,
|
|
||||||
metadata: metadata,
|
|
||||||
}).define(async (ctx) => {
|
|
||||||
const msg = { ...ctx.query };
|
|
||||||
if (msg.token === undefined && that.token !== undefined) {
|
|
||||||
msg.token = that.token;
|
|
||||||
}
|
|
||||||
const res = await callWorker({ path: r.path, key: r.key, ...msg }, viewItem);
|
|
||||||
ctx.forward(res)
|
|
||||||
}).addTo(that.router);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async initPage(item?: RouteViewPage) {
|
|
||||||
if (!item?.page?.url) {
|
|
||||||
console.warn('Page地址未提供');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const url = item.page.url;
|
|
||||||
try {
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
await import(url).then((module) => { }).catch((err) => {
|
|
||||||
console.error('引入Page脚本失败:', url, err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('引入Page脚本失败:', url, e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* 列出路由
|
|
||||||
* @param filter
|
|
||||||
* @param query WHERE metadata.tags CONTAINS 'premium'
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
async listRoutes(filterFn?: (item: Route) => boolean, opts?: { viewId?: string, query?: string }) {
|
|
||||||
let query = opts?.query;
|
|
||||||
if (opts?.viewId) {
|
|
||||||
const view = this.views.find(v => v.id === opts.viewId);
|
|
||||||
if (view) {
|
|
||||||
query = view.query;
|
|
||||||
}
|
|
||||||
} if (opts?.query) {
|
|
||||||
query = opts.query;
|
|
||||||
}
|
|
||||||
const routes = this.router.routes.filter(filterFn || (() => true));
|
|
||||||
if (query) {
|
|
||||||
return filter(routes, query);
|
|
||||||
}
|
|
||||||
return routes;
|
|
||||||
}
|
|
||||||
async getViewQuery(viewId: string) {
|
|
||||||
const view = this.views.find(v => v.id === viewId);
|
|
||||||
if (view) {
|
|
||||||
return view.query;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* 运行路由
|
|
||||||
* @param msg
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
async run(msg: { id?: string, path?: string, key?: string }) {
|
|
||||||
return await this.router.run(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
async runByRouteView(routeView: RouterViewItem) {
|
|
||||||
if (routeView.response) {
|
|
||||||
return routeView;
|
|
||||||
}
|
|
||||||
const item = this.initRouterView(routeView);
|
|
||||||
if (item.type === 'api' && item.api?.url) {
|
|
||||||
const query = item.api.query!;
|
|
||||||
const res = await query.post<any>(item.action || {});
|
|
||||||
item.response = res;
|
|
||||||
return item;
|
|
||||||
} else if (item.type === 'api') {
|
|
||||||
item.response = { code: 500, message: 'API URL未配置' };
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
if (item.type === 'context' && item.context?.router) {
|
|
||||||
const router = item.context.router;
|
|
||||||
const res = await router.run(item.action || {});
|
|
||||||
item.response = res;
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
if (item.type === 'page') {
|
|
||||||
await this.initPage(item);
|
|
||||||
const res = await this.router.run(item.action || {});
|
|
||||||
item.response = res;
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
if (item.type === 'worker' && item.worker?.worker) {
|
|
||||||
await this.initWorker(item, false);
|
|
||||||
const callWorker = this.callWorker.bind(this);
|
|
||||||
const res = await callWorker(item.action || {}, item.worker);
|
|
||||||
item.response = res;
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
item.response = { code: 500, message: '无法处理的路由类型' };
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type RouterItem = {
|
|
||||||
id?: string;
|
|
||||||
path?: string;
|
|
||||||
key?: string;
|
|
||||||
description?: string;
|
|
||||||
middleware?: string[];
|
|
||||||
metadata?: Record<string, any>;
|
|
||||||
}
|
|
||||||
463
query/query-proxy/proxy.ts
Normal file
463
query/query-proxy/proxy.ts
Normal file
@@ -0,0 +1,463 @@
|
|||||||
|
import { QueryClient as Query, Result } from '@kevisual/query';
|
||||||
|
import { QueryRouterServer, App, Route } from '@kevisual/router';
|
||||||
|
import { filter } from '@kevisual/js-filter'
|
||||||
|
import { EventEmitter } from 'eventemitter3';
|
||||||
|
import { initApi } from './router-api-proxy';
|
||||||
|
|
||||||
|
export const RouteTypeList = ['api', 'context', 'worker', 'page'] as const;
|
||||||
|
export type RouterViewItemInfo = RouterViewApi | RouterViewContext | RouterViewWorker | RouteViewPage;
|
||||||
|
export type RouterViewItem<T = {}> = RouterViewItemInfo & T;
|
||||||
|
|
||||||
|
type RouteViewBase = {
|
||||||
|
/**
|
||||||
|
* _id 用于纯本地存储标识
|
||||||
|
*/
|
||||||
|
_id?: string;
|
||||||
|
id?: string;
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
enabled?: boolean;
|
||||||
|
/**
|
||||||
|
* 响应数据
|
||||||
|
*/
|
||||||
|
response?: any;
|
||||||
|
/**
|
||||||
|
* 默认动作配置
|
||||||
|
*/
|
||||||
|
action?: { path?: string; key?: string; id?: string; payload?: any;[key: string]: any };
|
||||||
|
}
|
||||||
|
export type RouterViewApi = {
|
||||||
|
type: 'api',
|
||||||
|
api: {
|
||||||
|
url: string,
|
||||||
|
// 已初始化的query实例,不需要编辑配置
|
||||||
|
query?: Query
|
||||||
|
}
|
||||||
|
} & RouteViewBase;
|
||||||
|
|
||||||
|
export type RouterViewContext = {
|
||||||
|
type: 'context',
|
||||||
|
context: {
|
||||||
|
key: string,
|
||||||
|
// 从context中获取router,不需要编辑配置
|
||||||
|
router?: QueryRouterServer
|
||||||
|
}
|
||||||
|
} & RouteViewBase;
|
||||||
|
export type RouterViewWorker = {
|
||||||
|
type: 'worker',
|
||||||
|
worker: {
|
||||||
|
type: 'Worker' | 'SharedWorker' | 'serviceWorker',
|
||||||
|
url: string,
|
||||||
|
// 已初始化的worker实例,不需要编辑配置
|
||||||
|
worker?: Worker | SharedWorker | ServiceWorker,
|
||||||
|
/**
|
||||||
|
* worker选项
|
||||||
|
* default: { type: 'module' }
|
||||||
|
*/
|
||||||
|
workerOptions?: {
|
||||||
|
type: 'module' | 'classic'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} & RouteViewBase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 去掉不需要保存都服务器的数据
|
||||||
|
* @param item
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const pickRouterViewData = (item: RouterViewItem) => {
|
||||||
|
const { action, response, _id, ...rest } = item;
|
||||||
|
if (rest.type === 'api') {
|
||||||
|
if (rest.api) {
|
||||||
|
delete rest.api.query;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (rest.type === 'worker') {
|
||||||
|
if (rest.worker) {
|
||||||
|
delete rest.worker.worker;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (rest.type === 'context') {
|
||||||
|
if (rest.context) {
|
||||||
|
delete rest.context.router;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rest
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 注入 js 的url地址,使用importScripts加载
|
||||||
|
*/
|
||||||
|
export type RouteViewPage = {
|
||||||
|
type: 'page',
|
||||||
|
page: {
|
||||||
|
url: string,
|
||||||
|
}
|
||||||
|
} & RouteViewBase;
|
||||||
|
|
||||||
|
export type RouterViewQuery = {
|
||||||
|
id: string,
|
||||||
|
query: string,
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
export type RouterViewData = {
|
||||||
|
data: { items: RouterViewItem[]; }
|
||||||
|
views: RouterViewQuery[];
|
||||||
|
viewId?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class QueryProxy {
|
||||||
|
router: QueryRouterServer;
|
||||||
|
token?: string;
|
||||||
|
routerViewItems: RouterViewItem[];
|
||||||
|
views: RouterViewQuery[];
|
||||||
|
emitter: EventEmitter;
|
||||||
|
constructor(opts?: { router?: QueryRouterServer, token?: string, routerViewData?: RouterViewData }) {
|
||||||
|
this.router = opts?.router || new QueryRouterServer();
|
||||||
|
this.token = opts?.token || this.getDefulatToken();
|
||||||
|
this.routerViewItems = opts?.routerViewData?.data?.items || [];
|
||||||
|
this.views = opts?.routerViewData?.views || [];
|
||||||
|
this.initRouterViewQuery();
|
||||||
|
this.emitter = new EventEmitter();
|
||||||
|
}
|
||||||
|
getDefulatToken() {
|
||||||
|
try {
|
||||||
|
if (typeof window !== 'undefined' && typeof window.localStorage !== 'undefined') {
|
||||||
|
return localStorage.getItem('token') || undefined;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
initRouterViewQuery() {
|
||||||
|
this.routerViewItems = this.routerViewItems.map(item => {
|
||||||
|
return this.initRouterView(item);
|
||||||
|
}).filter(item => {
|
||||||
|
const enabled = item.enabled ?? true;
|
||||||
|
return enabled;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initRouterView(item: RouterViewItem) {
|
||||||
|
if (item.type === 'api' && item.api?.url) {
|
||||||
|
const url = item.api.url;
|
||||||
|
if (item?.api?.query) return item;
|
||||||
|
const query = new Query({ url: url });
|
||||||
|
item['api'] = { url: url, query: query };
|
||||||
|
}
|
||||||
|
if (item.type === 'worker' && item.worker?.url) {
|
||||||
|
let viewItem = item as RouterViewWorker;
|
||||||
|
if (!item.worker?.workerOptions?.type) {
|
||||||
|
item.worker.workerOptions = { ...item.worker.workerOptions, type: 'module' };
|
||||||
|
}
|
||||||
|
if (item.worker.worker) {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
let worker: Worker | SharedWorker | ServiceWorker | undefined = undefined;
|
||||||
|
if (item.worker.type === 'SharedWorker') {
|
||||||
|
worker = new SharedWorker(item.worker.url, item.worker.workerOptions);
|
||||||
|
worker.port.start();
|
||||||
|
} else if (viewItem.worker.type === 'serviceWorker') {
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
navigator.serviceWorker.register(viewItem.worker.url, item.worker.workerOptions).then(function (registration) {
|
||||||
|
console.debug('注册serviceWorker成功 ', registration.scope);
|
||||||
|
}, function (err) {
|
||||||
|
console.debug('注册 serviceWorker 失败: ', err);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.warn('当前浏览器不支持serviceWorker');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
worker = new Worker(viewItem.worker.url, item.worker.workerOptions);
|
||||||
|
}
|
||||||
|
viewItem['worker']['worker'] = worker;
|
||||||
|
}
|
||||||
|
if (item.type === 'context' && item.context?.key) {
|
||||||
|
if (item.context?.router) {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
|
const context = globalThis['context'] || {}
|
||||||
|
const router = context[item.context.key] as QueryRouterServer;
|
||||||
|
if (router) {
|
||||||
|
item['context']['router'] = router;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化路由
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async init() {
|
||||||
|
const routerViewItems = this.routerViewItems || [];
|
||||||
|
if (routerViewItems.length === 0) {
|
||||||
|
// 默认初始化api类型路由
|
||||||
|
await this.initApi();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const item of routerViewItems) {
|
||||||
|
switch (item.type) {
|
||||||
|
case 'api':
|
||||||
|
await this.initApi(item);
|
||||||
|
break;
|
||||||
|
case 'context':
|
||||||
|
await this.initContext(item);
|
||||||
|
break;
|
||||||
|
case 'worker':
|
||||||
|
await this.initWorker(item);
|
||||||
|
break;
|
||||||
|
case 'page':
|
||||||
|
await this.initPage(item);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.emitter.emit('initComplete');
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 监听初始化完成
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async listenInitComplete(): Promise<boolean> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
this.emitter.removeAllListeners('initComplete');
|
||||||
|
resolve(false);
|
||||||
|
}, 3 * 60000); // 3分钟超时
|
||||||
|
const func = () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
resolve(true);
|
||||||
|
}
|
||||||
|
this.emitter.once('initComplete', func);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async initApi(item?: RouterViewApi) {
|
||||||
|
initApi({ item: item, router: this.router, token: this.token });
|
||||||
|
}
|
||||||
|
async initContext(item?: RouterViewContext) {
|
||||||
|
// @ts-ignore
|
||||||
|
const context = globalThis['context'] || {}
|
||||||
|
const router = item?.context?.router || context[item?.context?.key] as QueryRouterServer;
|
||||||
|
if (!router) {
|
||||||
|
console.warn(`未发现Context router ${item?.context?.key}`);
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const routes = router.getList();
|
||||||
|
for (const r of routes) {
|
||||||
|
console.debug(`注册路由: [${r.path}] ${r?.key}`, 'Context');
|
||||||
|
let metadata = r.metadata || {};
|
||||||
|
metadata.viewItem = item;
|
||||||
|
this.router.route({
|
||||||
|
path: r.path,
|
||||||
|
key: r.key || '',
|
||||||
|
id: r.id,
|
||||||
|
description: r.description,
|
||||||
|
metadata: metadata,
|
||||||
|
}).define(async (ctx) => {
|
||||||
|
const res = await router.run({ path: r.path, key: r.key, ...ctx.query });
|
||||||
|
ctx.forward(res)
|
||||||
|
}).addTo(this.router);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
generateId() {
|
||||||
|
return 'route_' + Math.random().toString(36).substring(2, 9);
|
||||||
|
}
|
||||||
|
async callWorker(msg: any, viewItem: RouterViewWorker['worker']): Promise<Result> {
|
||||||
|
const that = this;
|
||||||
|
const requestId = this.generateId();
|
||||||
|
const worker = viewItem?.worker;
|
||||||
|
if (!worker) {
|
||||||
|
return { code: 500, message: 'Worker未初始化' };
|
||||||
|
}
|
||||||
|
let port: MessagePort | Worker | ServiceWorker;
|
||||||
|
if (viewItem.type === 'SharedWorker') {
|
||||||
|
port = (worker as SharedWorker).port;
|
||||||
|
} else {
|
||||||
|
port = worker as Worker | ServiceWorker;
|
||||||
|
}
|
||||||
|
port.postMessage({
|
||||||
|
...msg,
|
||||||
|
requestId: requestId,
|
||||||
|
})
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
that.emitter.removeAllListeners(requestId);
|
||||||
|
resolve({ code: 500, message: '请求超时' });
|
||||||
|
}, 3 * 60 * 1000); // 3分钟超时
|
||||||
|
that.emitter.once(requestId, (res: any) => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
resolve(res);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async initWorker(item?: RouterViewWorker, initRoutes: boolean = true) {
|
||||||
|
const that = this;
|
||||||
|
if (!item?.worker?.url) {
|
||||||
|
console.warn('Worker URL not provided');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const viewItem = item.worker;
|
||||||
|
const worker = viewItem?.worker;
|
||||||
|
if (!worker) {
|
||||||
|
console.warn('Worker not initialized');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const callResponse = (e: MessageEvent) => {
|
||||||
|
const msg = e.data;
|
||||||
|
if (msg.requestId) {
|
||||||
|
const requestId = msg.requestId;
|
||||||
|
that.emitter.emit(requestId, msg);
|
||||||
|
} else {
|
||||||
|
that.router.run(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (item.worker.type === 'SharedWorker') {
|
||||||
|
const port = (worker as SharedWorker).port;
|
||||||
|
port.onmessage = callResponse;
|
||||||
|
port.start();
|
||||||
|
} else if (item.worker.type === 'serviceWorker') {
|
||||||
|
navigator.serviceWorker.addEventListener('message', callResponse);
|
||||||
|
} else {
|
||||||
|
(worker as Worker).onmessage = callResponse;
|
||||||
|
}
|
||||||
|
if (!initRoutes) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const callWorker = this.callWorker.bind(this);
|
||||||
|
|
||||||
|
const res = await callWorker({
|
||||||
|
path: "router",
|
||||||
|
key: 'list',
|
||||||
|
token: this.token,
|
||||||
|
}, viewItem);
|
||||||
|
|
||||||
|
if (res.code !== 200) {
|
||||||
|
console.error('Failed to init query proxy router:', res.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const _list = res.data?.list || []
|
||||||
|
for (const r of _list) {
|
||||||
|
if (r.path || r.id) {
|
||||||
|
console.debug(`注册路由: [${r.path}] ${r?.key}`, 'API');
|
||||||
|
let metadata = r.metadata || {};
|
||||||
|
metadata.viewItem = item;
|
||||||
|
this.router.route({
|
||||||
|
path: r.path,
|
||||||
|
key: r.key || '',
|
||||||
|
id: r.id,
|
||||||
|
description: r.description,
|
||||||
|
metadata: metadata,
|
||||||
|
}).define(async (ctx) => {
|
||||||
|
const msg = { ...ctx.query };
|
||||||
|
if (msg.token === undefined && that.token !== undefined) {
|
||||||
|
msg.token = that.token;
|
||||||
|
}
|
||||||
|
const res = await callWorker({ path: r.path, key: r.key, ...msg }, viewItem);
|
||||||
|
ctx.forward(res)
|
||||||
|
}).addTo(that.router);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async initPage(item?: RouteViewPage) {
|
||||||
|
if (!item?.page?.url) {
|
||||||
|
console.warn('Page地址未提供');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const url = item.page.url;
|
||||||
|
try {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
await import(url).then((module) => { }).catch((err) => {
|
||||||
|
console.error('引入Page脚本失败:', url, err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('引入Page脚本失败:', url, e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 列出路由
|
||||||
|
* @param filter
|
||||||
|
* @param query WHERE metadata.tags CONTAINS 'premium'
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async listRoutes(filterFn?: (item: Route) => boolean, opts?: { viewId?: string, query?: string }) {
|
||||||
|
let query = opts?.query;
|
||||||
|
if (opts?.viewId) {
|
||||||
|
const view = this.views.find(v => v.id === opts.viewId);
|
||||||
|
if (view) {
|
||||||
|
query = view.query;
|
||||||
|
}
|
||||||
|
} if (opts?.query) {
|
||||||
|
query = opts.query;
|
||||||
|
}
|
||||||
|
const routes = this.router.routes.filter(filterFn || (() => true));
|
||||||
|
if (query) {
|
||||||
|
return filter(routes, query);
|
||||||
|
}
|
||||||
|
return routes;
|
||||||
|
}
|
||||||
|
async getViewQuery(viewId: string) {
|
||||||
|
const view = this.views.find(v => v.id === viewId);
|
||||||
|
if (view) {
|
||||||
|
return view.query;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 运行路由
|
||||||
|
* @param msg
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async run(msg: { id?: string, path?: string, key?: string }) {
|
||||||
|
return await this.router.run(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
async runByRouteView(routeView: RouterViewItem) {
|
||||||
|
if (routeView.response) {
|
||||||
|
return routeView;
|
||||||
|
}
|
||||||
|
const item = this.initRouterView(routeView);
|
||||||
|
if (item.type === 'api' && item.api?.url) {
|
||||||
|
const query = item.api.query!;
|
||||||
|
const res = await query.post<any>(item.action || {});
|
||||||
|
item.response = res;
|
||||||
|
return item;
|
||||||
|
} else if (item.type === 'api') {
|
||||||
|
item.response = { code: 500, message: 'API URL未配置' };
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
if (item.type === 'context' && item.context?.router) {
|
||||||
|
const router = item.context.router;
|
||||||
|
const res = await router.run(item.action || {});
|
||||||
|
item.response = res;
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
if (item.type === 'page') {
|
||||||
|
await this.initPage(item);
|
||||||
|
const res = await this.router.run(item.action || {});
|
||||||
|
item.response = res;
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
if (item.type === 'worker' && item.worker?.worker) {
|
||||||
|
await this.initWorker(item, false);
|
||||||
|
const callWorker = this.callWorker.bind(this);
|
||||||
|
const res = await callWorker(item.action || {}, item.worker);
|
||||||
|
item.response = res;
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
item.response = { code: 500, message: '无法处理的路由类型' };
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RouterItem = {
|
||||||
|
id?: string;
|
||||||
|
path?: string;
|
||||||
|
key?: string;
|
||||||
|
description?: string;
|
||||||
|
middleware?: string[];
|
||||||
|
metadata?: Record<string, any>;
|
||||||
|
}
|
||||||
48
query/query-proxy/router-api-proxy.ts
Normal file
48
query/query-proxy/router-api-proxy.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { Query } from "@kevisual/query";
|
||||||
|
import { RouterViewApi, RouterItem } from ".";
|
||||||
|
import { App, type QueryRouterServer } from "@kevisual/router";
|
||||||
|
import { filter } from "@kevisual/js-filter";
|
||||||
|
export const initApi = async (opts: {
|
||||||
|
item?: RouterViewApi,
|
||||||
|
router: QueryRouterServer | App,
|
||||||
|
token?: string,
|
||||||
|
/**
|
||||||
|
* WHERE path = 'auth' OR path = 'router'
|
||||||
|
*/
|
||||||
|
exclude?: string;
|
||||||
|
}) => {
|
||||||
|
const router = opts?.router! as QueryRouterServer;
|
||||||
|
const item = opts?.item;
|
||||||
|
const token = opts?.token;
|
||||||
|
const query = item?.api?.query || new Query({ url: item?.api?.url || '/api/router' })
|
||||||
|
const res = await query.post<{ list: RouterItem[] }>({ path: "router", key: 'list', token: token });
|
||||||
|
if (res.code !== 200) {
|
||||||
|
console.error('初始化路由失败:', query.url, res.message);
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let _list = res.data?.list || []
|
||||||
|
if (opts?.exclude) {
|
||||||
|
_list = filter(_list, opts.exclude);
|
||||||
|
}
|
||||||
|
for (const r of _list) {
|
||||||
|
if (r.path || r.id) {
|
||||||
|
console.debug(`注册路由: [${r.path}] ${r?.key}`, 'API');
|
||||||
|
let metadata = r.metadata || {};
|
||||||
|
metadata.viewItem = item;
|
||||||
|
router.route({
|
||||||
|
path: r.path,
|
||||||
|
key: r.key || '',
|
||||||
|
id: r.id,
|
||||||
|
description: r.description,
|
||||||
|
metadata: metadata,
|
||||||
|
}).define(async (ctx) => {
|
||||||
|
const msg = { ...ctx.query };
|
||||||
|
if (msg.token === undefined && token !== undefined) {
|
||||||
|
msg.token = token;
|
||||||
|
}
|
||||||
|
const res = await query.post<any>({ path: r.path, key: r.key, ...msg });
|
||||||
|
ctx.forward(res)
|
||||||
|
}).addTo(router);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user