diff --git a/package.json b/package.json index 77b2a47..2d433cf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kevisual/store", - "version": "0.0.4", + "version": "0.0.6", "main": "dist/store.js", "module": "dist/store.js", "types": "dist/store.d.ts", @@ -26,6 +26,8 @@ "license": "ISC", "description": "", "devDependencies": { + "@kevisual/load": "workspace:*", + "@kevisual/types": "link:../types", "@rollup/plugin-commonjs": "^28.0.3", "@rollup/plugin-node-resolve": "^16.0.1", "@rollup/plugin-typescript": "^12.1.2", @@ -34,14 +36,16 @@ "immer": "^10.1.1", "lodash-es": "^4.17.21", "nanoid": "^5.1.5", - "rollup": "^4.37.0", + "rollup": "^4.41.1", "rollup-plugin-dts": "^6.2.1", "ts-node": "^10.9.2", "tslib": "^2.8.1", - "tsup": "^8.4.0", - "typescript": "^5.8.2", - "vite-plugin-dts": "^4.5.3", - "zustand": "^5.0.3" + "typescript": "^5.8.3", + "zustand": "^5.0.5", + "@kevisual/router": "^0.0.21", + "@rollup/plugin-terser": "^0.4.4", + "eventemitter3": "^5.0.1", + "path-to-regexp": "^8.2.0" }, "publishConfig": { "access": "public" @@ -60,8 +64,8 @@ "require": "./dist/web-config.js" }, "./context": { - "import": "./dist/web-context.js", - "require": "./dist/web-context.js" + "import": "./dist/web-config.js", + "require": "./dist/web-config.js" }, "./web-page.js": { "import": "./dist/web-page.js", @@ -76,8 +80,5 @@ "types": "./dist-react/index.d.ts" } }, - "dependencies": { - "eventemitter3": "^5.0.1", - "path-to-regexp": "^8.2.0" - } + "dependencies": {} } \ No newline at end of file diff --git a/rollup.config.js b/rollup.config.js index 7443441..6a3536b 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -4,6 +4,7 @@ import typescript from '@rollup/plugin-typescript'; import resolve from '@rollup/plugin-node-resolve'; import commonjs from '@rollup/plugin-commonjs'; import { dts } from 'rollup-plugin-dts'; +import terser from '@rollup/plugin-terser'; /** * @type {import('rollup').RollupOptions} @@ -77,4 +78,20 @@ export default [ }, plugins: [dts()], }, + { + input: 'src/app.ts', + output: { + file: 'dist/app.js', + format: 'iife', + }, + plugins: [resolve({ browser: true }), commonjs(), typescript(), terser()], + }, + { + input: 'src/app.ts', + output: { + file: 'dist/app.d.ts', + format: 'es', + }, + plugins: [dts()], + }, ]; diff --git a/src/app.ts b/src/app.ts new file mode 100644 index 0000000..9b9c98a --- /dev/null +++ b/src/app.ts @@ -0,0 +1,28 @@ +// 当前的功能,把所有的模块注入到windows对象当中 +import * as WebEnv from './web-env.ts'; +import * as Load from '@kevisual/load/browser'; +import { Page } from './page.ts'; + +export class PageInit { + static isInit = false; + static init(opts?: { load?: boolean; page?: boolean }) { + if (PageInit.isInit) { + return; + } + const { load = true, page = false } = opts || {}; + PageInit.isInit = true; + // bind to window, 必须要的获取全局的环境变量 + const { useConfigKey, useContextKey } = WebEnv; + window.useConfigKey = useConfigKey; + window.useContextKey = useContextKey; + // @ts-ignore + window.webEnv = WebEnv; + // @ts-ignore + load && (window.Load = Load); + page && + useContextKey('page', () => { + return new Page(); + }); + } +} +PageInit.init(); diff --git a/src/page.ts b/src/page.ts index 7fc11d1..4a02144 100644 --- a/src/page.ts +++ b/src/page.ts @@ -1,5 +1,5 @@ import { getPathKey } from '@/utils/path-key.ts'; -import { match } from 'path-to-regexp'; +import * as pathToRegexp from 'path-to-regexp'; import deepEqual from 'fast-deep-equal'; const generateRandom = () => { @@ -7,8 +7,17 @@ const generateRandom = () => { return crypto.randomUUID(); }; type PageOptions = { + /** + * 路径 + */ path?: string; + /** + * key + */ key?: string; + /** + * basename + */ basename?: string; isListen?: boolean; }; @@ -23,7 +32,9 @@ type CallbackInfo = { fn: CallFn; id: string; } & PageModule; -let currentUrl = location.href; +/** + * 根据basename,其中的path和key,来判断一个应用的模块。 + */ export class Page { pageModule = new Map(); // pathname的第一个路径 @@ -33,19 +44,51 @@ export class Page { basename: string; isListen: boolean; callbacks = [] as CallbackInfo[]; + ok = false; constructor(opts?: PageOptions) { const pathKey = getPathKey(); this.path = opts?.path ?? pathKey.path; this.key = opts?.key ?? pathKey.key; - this.basename = opts?.basename ?? `/${this.path}/${this.key}`; + if (opts?.basename) { + this.basename = opts?.basename; + } else { + if (this.key) { + this.basename = `/${this.path}/${this.key}`; + } else { + const location = window.location; + this.basename = location.pathname; + } + } + this.clearEndSlash(); const isListen = opts?.isListen ?? true; if (isListen) { this.listen(); } + this.ok = !!this.key; } - popstate(event?: PopStateEvent, manualOpts?: { id?: string; type: 'singal' | 'all' }) { - const pathname = window.location.pathname; - console.log('popstate', event); + /** + * 清除路径的结尾斜杠,所有的最后的斜杠都删除 + */ + clearEndSlash() { + this.basename = this.basename.replace(/\/+$/, ''); + return this; + } + /** + * 检查路径 + */ + checkPath() { + const pathKey = getPathKey(); + const { path, key } = pathKey; + this.path = path || ''; + this.key = key || ''; + this.ok = !!this.key; + this.basename = `/${this.path}/${this.key}`; + this.clearEndSlash(); + return this; + } + + popstate(event?: PopStateEvent, manualOpts?: { id?: string; type: 'singal' | 'all'; pathname?: string }) { + const pathname = manualOpts?.pathname ?? window.location.pathname; if (manualOpts) { if (manualOpts.type === 'singal') { const item = this.callbacks.find((item) => item.id === manualOpts.id); @@ -62,7 +105,17 @@ export class Page { result && item.fn?.(result.params, event.state, { event, manualOpts }); }); } + /** + * 调用callback中id或者pathname的函数, 其中id优先级高于pathname,若都没有,则从location.pathname中获取 + * @param opts + */ + call(opts?: { id?: string; pathname?: string }) { + this.popstate({ state: window.history.state } as any, { ...opts, type: 'all' }); + } listen() { + if (this.isListen) { + return; + } this.isListen = true; window.addEventListener('popstate', this.popstate.bind(this), false); } @@ -89,7 +142,7 @@ export class Page { }); } /** - * 检查当前路径是否匹配 + * 检查当前路径是否匹配, 如何提交pathname,则检查pathname * @param key * @param pathname * @returns @@ -106,8 +159,12 @@ export class Page { } const location = window.location; const _pathname = pathname || location.pathname; + if (!_pathname.includes(this.basename)) { + // console.error(`PageModule ${key} not found`); + return false; + } const cur = _pathname.replace(this.basename, ''); - const routeMatch = match(pageModule.path, { decode: decodeURIComponent }); + const routeMatch = pathToRegexp.match(pageModule.path, { decode: decodeURIComponent }); const result = routeMatch(cur); let params = {}; if (result) { @@ -124,20 +181,22 @@ export class Page { * @param opts * @returns */ - subscribe(key: string, fn?: CallFn, opts?: { pathname?: string; runImmediately?: boolean; id?: string }) { + async subscribe(key: string, fn?: CallFn, opts?: { pathname?: string; runImmediately?: boolean; id?: string }) { const runImmediately = opts?.runImmediately ?? true; // 默认立即执行 const id = opts?.id ?? generateRandom(); - const path = this.pageModule.get(key)?.path; - if (!path) { + const pageModule = this.pageModule.get(key); + if (!pageModule) { console.error(`PageModule ${key} not found`); return () => {}; } + const path = pageModule?.path || ''; this.callbacks.push({ key, fn, id: id, path }); if (runImmediately) { const location = window.location; const pathname = opts?.pathname ?? location.pathname; const result = this.check(key, pathname); if (result) { + // 如果是手动调用,则不需要检查是否相等,直接执行,而且是执行当前的subscribe的函数 this.popstate({ state: window.history.state } as any, { id, type: 'singal' }); } } @@ -145,6 +204,9 @@ export class Page { this.callbacks = this.callbacks.filter((item) => item.id !== id); }; } + getPathKey() { + return getPathKey(); + } /** * 返回当前路径,不包含basename * @returns @@ -157,12 +219,26 @@ export class Page { getAppPath() { return this.path; } + /** + * 返回当前key + * @returns + */ getAppKey() { return this.key; } + /** + * 解码路径 + * @param path + * @returns + */ decodePath(path: string) { return decodeURIComponent(path); } + /** + * 编码路径 + * @param path + * @returns + */ encodePath(path: string) { return encodeURIComponent(path); } @@ -170,7 +246,7 @@ export class Page { * 如果state 和 pathname都相等,则不执行popstate * @param path * @param state - * @param check + * @param check 是否检查, 默认检查 * @returns */ navigate(path: string | number, state?: any, check?: boolean) { @@ -193,6 +269,12 @@ export class Page { this.popstate({ state } as any, { type: 'all' }); } } + /** + * 替换当前路径 + * @param path + * @param state + * @param check + */ replace(path: string, state?: any, check?: boolean) { let _check = check ?? true; window.history.replaceState(state, '', this.basename + path); @@ -200,10 +282,26 @@ export class Page { this.popstate({ state } as any, { type: 'all' }); } } + /** + * 刷新当前页面 + */ refresh() { const state = window.history.state; this.popstate({ state } as any, { type: 'all' }); } + /** + * 检查路径是否匹配 + * @param path + * @param checkPath + * @returns + */ + pathMatch(regexpPath: string, checkPath: string) { + return pathToRegexp.match(regexpPath, { decode: decodeURIComponent })(checkPath); + } + pathToRegexp = pathToRegexp; + static match(regexpPath: string, checkPath: string) { + return pathToRegexp.match(regexpPath, { decode: decodeURIComponent })(checkPath); + } } /** * 获取history state diff --git a/src/utils/path-key.ts b/src/utils/path-key.ts index 76b87bc..8063da2 100644 --- a/src/utils/path-key.ts +++ b/src/utils/path-key.ts @@ -2,6 +2,8 @@ export const getPathKey = () => { // 从localtion.href的路径中,/a/b 中 a为path,b为key const pathname = location.pathname; const paths = pathname.split('/'); - const [path, key] = paths.slice(1); + let [path, key] = paths.slice(1); + path = path || ''; + key = key || ''; return { path, key, id: path + '---' + key, prefix: `/${path}/${key}` }; }; diff --git a/src/web-env.ts b/src/web-env.ts index fcb5c0a..0b46bd7 100644 --- a/src/web-env.ts +++ b/src/web-env.ts @@ -1,52 +1,80 @@ import { getPathKey } from './utils/path-key.ts'; +import { BaseLoad } from '@kevisual/load'; +const gt = (globalThis as any) || window || self; type GlobalEnv = { name?: string; [key: string]: any; }; +// 从window对象中获取全局的环境变量,如果没有则初始化一个 export const useEnv = (initEnv?: GlobalEnv, initKey = 'config') => { - const env: GlobalEnv = (window as any)[initKey]; + const env: GlobalEnv = gt[initKey]; const _env = env || initEnv; if (!env) { if (_env) { - (window as any)[initKey] = _env; + gt[initKey] = _env; } else { - (window as any)[initKey] = {}; + gt[initKey] = {}; } } - return window[initKey] as GlobalEnv; + return gt[initKey] as GlobalEnv; }; +// 从全局环境变量中获取指定的key值,如果没有则初始化一个, key不存在,返回Env对象 export const useEnvKey = (key: string, init?: () => T | null, initKey = 'config'): T => { const _env = useEnv({}, initKey); - if (key && _env[key]) { + // 已经存在,直接返回 + if (key && typeof _env[key] !== 'undefined') { return _env[key]; } + // 不存在,但是有初始化函数,初始化的返回,同步函数,删除了重新加载? if (key && init) { _env[key] = init(); return _env[key]; } - - return _env as any; -}; - -export const useEnvKeySync = async (key: string, init?: () => Promise | null, initKey = 'conifg'): Promise => { - const _env = useEnv({}, initKey); - if (key && init) { - _env[key] = await init(); - return _env[key]; - } if (key) { - return _env[key]; + // 加载 + const baseLoad = new BaseLoad(); + const voidFn = async () => { + return _env[key]; + }; + const checkFn = async () => { + const loadRes = await baseLoad.load(voidFn, { + key, + isReRun: true, + checkSuccess: () => _env[key], + timeout: 5 * 60 * 1000, + interval: 1000, + // + }); + if (loadRes.code !== 200) { + console.error('load key error'); + return null; + } + return _env[key]; + }; + return checkFn() as T; } - return _env as any; + // 不存在,没有初始化函数 + console.error('key is empty '); + return null; }; export const usePageEnv = (init?: () => {}, initKey = 'conifg') => { const { id } = getPathKey(); return useEnvKey(id, init, initKey); }; - +export const useEnvKeyNew = (key: string, initKey = 'conifg', opts?: { getNew?: boolean; init?: () => {} }) => { + const _env = useEnv({}, initKey); + if (key) { + delete _env[key]; + } + if (opts?.getNew && opts.init) { + return useEnvKey(key, opts.init, initKey); + } else if (opts?.getNew) { + return useEnvKey(key, null, initKey); + } +}; type GlobalContext = { name?: string; [key: string]: any; @@ -55,14 +83,13 @@ export const useContext = (initContext?: GlobalContext) => { return useEnv(initContext, 'context'); }; -export const useContextKey = (key: string, init?: () => T): T => { +export const useContextKey = (key: string, init?: () => T, isNew?: boolean): T => { + if (isNew) { + return useEnvKeyNew(key, 'context', { getNew: true, init }); + } return useEnvKey(key, init, 'context'); }; -export const useContextKeySync = async (key: string, init?: () => Promise): Promise => { - return useEnvKeySync(key, init, 'context'); -}; - export const usePageContext = (init?: () => {}) => { const { id } = getPathKey(); return useContextKey(id, init); @@ -76,15 +103,36 @@ export const useConfig = (initConfig?: GlobalConfig) => { return useEnv(initConfig, 'config'); }; -export const useConfigKey = (key: string, init?: () => T): T => { +export const useConfigKey = (key: string, init?: () => T, isNew?: boolean): T => { + if (isNew) { + return useEnvKeyNew(key, 'config', { getNew: true, init }); + } return useEnvKey(key, init, 'config'); }; -export const useConfigKeySync = async (key: string, init?: () => Promise): Promise => { - return useEnvKeySync(key, init, 'config'); -}; - export const usePageConfig = (init?: () => {}) => { const { id } = getPathKey(); return useConfigKey(id, init); }; + +class InitEnv { + static isInit = false; + + static init(opts?: { load?: boolean; page?: boolean }) { + if (InitEnv.isInit) { + return; + } + const { load = true, page = false } = opts || {}; + InitEnv.isInit = true; + // bind to window, 必须要的获取全局的环境变量 + // @ts-ignore + gt.useConfigKey = useConfigKey; + // @ts-ignore + gt.useContextKey = useContextKey; + // @ts-ignore + gt.webEnv = { useConfigKey, useContextKey }; + // @ts-ignore + load && (gt.Load = BaseLoad); + } +} +InitEnv.init(); diff --git a/src/web.ts b/src/web.ts index b17642b..b659f00 100644 --- a/src/web.ts +++ b/src/web.ts @@ -1,7 +1,14 @@ -export * from './page.ts'; -export * from './web-env.ts'; +import { Page } from './page.ts'; +import * as WebEnv from './web-env.ts'; -export * from 'nanoid'; -export * from 'path-to-regexp'; +import { nanoid } from 'nanoid'; +import * as PathToREgexp from 'path-to-regexp'; +import * as Load from '@kevisual/load/browser'; -export * from 'eventemitter3'; +export const WebModule = { + Page, + WebEnv, + nanoid, + PathToREgexp, + Load, +};