UNPKG

lupine.web

Version:

lupine.web is a extremely fast, small size and lightweight frontend framework, using React TSX syntax.

181 lines (164 loc) 6.62 kB
import { VNode } from '../jsx'; import { isFrontEnd } from '../lib/is-frontend'; import { PageProps } from './export-lupine'; import { mountInnerComponent } from './mount-component'; import { Logger } from '../lib/logger'; export type PageRouterCallback = (props: PageProps) => Promise<VNode<any> | null>; export type PageRouterData = { path: string; handler: (PageRouterCallback | PageRouter)[]; parameterVariables: string[]; parameterLength: number; }; export type FramePageProps = { component: (placeholderClassname: string, vnode: VNode<any>) => Promise<VNode<any>>; placeholderClassname: string; }; export class PageRouter { logger = new Logger('page-router'); private routerData: PageRouterData[] = []; private filter: PageRouterCallback | undefined; private framePage: FramePageProps | undefined; private subDir: string = ''; // if the filter returns null (passed filter), the router will continue. // it works in the same way as in use method setFilter(filter: PageRouterCallback) { this.filter = filter; } // if the script is under a sub-dir (without last /), then findRoute needs to remove it from the url setSubDir(subDir: string) { this.subDir = subDir; } setFramePage(framePage: FramePageProps) { this.framePage = framePage; } // the path should start with / and end without /, and it can be // /aaa/:bbb/ccc/:ddd (ccc is a fixed section) // /aaa/:bbb/ccc/?ddd/?eee (from ddd, all sections are optional) // /aaa/:?bbb/ccc/ (from bbb, all sections are optional) private storeRouter(path: string, handler: (PageRouterCallback | PageRouter)[]) { let fixedPath; if (path === '*' || path === '' || path === '/*') { // removed path === '/' || fixedPath = '*'; } else { fixedPath = path; if (!fixedPath.startsWith('/')) { fixedPath = '/' + fixedPath; } if (fixedPath.endsWith('/') && fixedPath.length > 1) { fixedPath = fixedPath.substring(0, fixedPath.length - 1); } } let parameterLength = 0; let parameterVariables: string[] = []; const ind = fixedPath.indexOf('/:'); if (ind >= 0) { parameterVariables = fixedPath.substring(ind + 1).split('/'); fixedPath = fixedPath.substring(0, ind); // from optionInd, all will be optional const optionInd = parameterVariables.findIndex((item) => item.startsWith('?')); parameterLength = optionInd >= 0 ? optionInd : parameterVariables.length; } this.routerData.push({ path: fixedPath, handler, parameterVariables, parameterLength, }); } use(path: string, ...handler: (PageRouterCallback | PageRouter)[]) { this.storeRouter(path, handler); } private async callHandle(handle: PageRouterCallback, path: string, props: PageProps): Promise<VNode<any> | null> { try { const vNode = await handle(props); // logger.debug(`Processed path: ${path}`); return vNode; } catch (e: any) { this.logger.error(`Processed path: ${path}, error: ${e.message}`); // res.write(JSON.stringify({ status: 'error', message: `Processed path: ${path}, error: ${e.message}` })); } return null; } async findRoute(url: string, props: PageProps, renderPartPage: boolean): Promise<VNode<any> | null> { for (let i = 0, routerList; (routerList = this.routerData[i]); i++) { if (routerList.path === '*' || url === routerList.path || url.startsWith(routerList.path + '/')) { const parameters: { [key: string]: string } = {}; let meet = true; if (routerList.parameterVariables.length > 0) { meet = false; let newUrl = url.substring(routerList.path.length + 1); if (newUrl.endsWith('/')) { newUrl = newUrl.substring(0, newUrl.length - 1); } const restPath = newUrl.split('/'); // the path must have mandatory parameters but some parameters can be optional if ( restPath.length >= routerList.parameterLength && restPath.length <= routerList.parameterVariables.length ) { meet = true; for (const [index, item] of routerList.parameterVariables.entries()) { if (!item.startsWith(':') && !item.startsWith('?') && item !== restPath[index]) { meet = false; break; } else if ((item.startsWith(':') || item.startsWith('?')) && index < restPath.length) { parameters[item.replace(/[:?]/g, '')] = restPath[index]; } } props.urlParameters = parameters; } } if (meet) { for (let j = 0, router; (router = routerList.handler[j]); j++) { if (router instanceof PageRouter) { // it's a sub-level router const nextPath = routerList.path === '*' || (url === '/' && routerList.path === '/') ? url : url.substring(routerList.path.length); // TODO: sub-level? const vNode = await router.handleRoute(nextPath, props, renderPartPage); if (vNode) { return vNode; } } else { // it should be a function // the query's url should match the api's path const dom = await this.callHandle(router, url, props); if (dom) { return dom; } } } // stop process for this path if no page is found return null; } } } return null; } async handleRoute(url: string, props: PageProps, renderPartPage: boolean): Promise<VNode<any> | null> { if (url.startsWith(this.subDir)) { url = url.substring(this.subDir.length); } let vNode = null; if (this.filter) { vNode = await this.callHandle(this.filter, url, props); } if (!vNode) { vNode = await this.findRoute(url, props, renderPartPage); } if (vNode && this.framePage) { const selector = '.' + this.framePage.placeholderClassname; if (renderPartPage && isFrontEnd() && document.querySelector(selector)) { await mountInnerComponent(selector, vNode); return null; } else { return this.framePage.component(this.framePage.placeholderClassname, vNode); } } return vNode; } }