Compare commits

...

33 Commits

Author SHA1 Message Date
bbf826b765 feat: remove web-env 2025-06-12 15:01:59 +08:00
9d22faa8ba bump 2025-05-24 17:18:10 +08:00
e4768b6360 merge 2025-05-24 17:10:13 +08:00
598e29cf5a temp 2025-05-24 17:03:32 +08:00
4152133951 temp 2025-05-24 17:00:37 +08:00
02446fd60f temp 2025-05-24 16:58:59 +08:00
351b2e3366 temp 2025-04-03 19:33:16 +08:00
d70118ad3d temp 2025-04-03 15:34:57 +08:00
69784e8ed4 add history 2025-03-29 17:48:38 +08:00
415f008209 update 2025-03-28 09:39:27 +08:00
8d4505eb24 fix: change to version 3 2025-03-27 19:58:01 +08:00
3d7879c79d .gitignore change no turbo 2025-03-27 19:53:52 +08:00
4d7af80d15 fix: types 2025-03-26 14:06:10 +08:00
152fda350e store init 2025-03-26 11:52:56 +08:00
3d50b64543 fix 2025-03-06 22:45:17 +08:00
077e99cbc8 perf: app and page 修改 2025-03-03 23:31:05 +08:00
abdc5c7923 bump 2025-03-01 23:14:44 +08:00
3ee69a4f81 perf: for useConfigKey 2025-03-01 17:35:43 +08:00
86ae2c7f70 fix: basename 修改,后缀为/的去掉/ 2025-02-27 18:26:56 +08:00
3a027efb79 feat: add Load 2025-02-27 00:57:05 +08:00
d8d5073542 store 添加Load模块 2025-02-19 06:52:18 +08:00
b39c950ef7 temp 2025-01-07 03:01:09 +08:00
01db9c9ea2 doc 2025-01-07 02:58:02 +08:00
c855c7d3d5 feat: add page 2025-01-02 23:04:01 +08:00
7cb42edc5f fix: add iifi for Page 2024-12-22 21:41:23 +08:00
9b3de5eba3 feat: add app fof iife 2024-12-22 14:49:48 +08:00
3b19cd8581 temp 2024-12-18 11:10:48 +08:00
78d581c98c fix: add env 2024-12-09 12:48:13 +08:00
b5dde4e823 fix: page 2024-12-08 12:54:57 +08:00
a4f03c76eb fix: update 2024-12-01 23:58:08 +08:00
4148d162e4 fix: add page perfect 2024-11-30 02:21:33 +08:00
ebf0b230b1 feat: add config and page 2024-11-30 00:10:51 +08:00
f62254af26 fix: change build dts 2024-11-02 22:27:03 +08:00
20 changed files with 977 additions and 88 deletions

4
.gitignore vendored
View File

@@ -3,4 +3,6 @@ dist
build
.cache
.DS_Store
*.log
*.log
.turbo
dist-react

3
.npmrc Normal file
View File

@@ -0,0 +1,3 @@
//npm.xiongxiao.me/:_authToken=${ME_NPM_TOKEN}
@abearxiong:registry=https://npm.pkg.github.com
//registry.npmjs.org/:_authToken=${NPM_TOKEN}

99
demo/index.html Normal file
View File

@@ -0,0 +1,99 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Demo Page</title>
</head>
<body>
<h1>Welcome to the Demo Page</h1>
<p>This is a basic HTML template.</p>
<script src="./src/index.ts" type="module"></script>
<form action="/submit" method="POST">
<label for="browser">选择浏览器:</label>
<input list="browsers" id="browser" name="browser" />
<datalist id="browsers">
<option value="Chrome">
<option value="Firefox">
<option value="Safari">
<option value="Edge">
</datalist>
<button type="submit">提交</button>
</form>
<style>
.custom-datalist-wrapper {
position: relative;
width: 200px;
}
.custom-datalist-input {
width: 100%;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
}
.custom-datalist-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
border: 1px solid #ccc;
border-radius: 4px;
max-height: 150px;
overflow-y: auto;
display: none;
z-index: 1000;
}
.custom-datalist-dropdown.active {
display: block;
}
.custom-datalist-item {
padding: 8px;
cursor: pointer;
}
.custom-datalist-item:hover {
background-color: #f0f0f0;
}
</style>
<div class="custom-datalist-wrapper">
<input type="text" class="custom-datalist-input" placeholder="选择或输入..." oninput="filterDatalist(this)" />
<div class="custom-datalist-dropdown" id="datalist-dropdown">
<div class="custom-datalist-item" onclick="selectDatalistItem(this)">Apple</div>
<div class="custom-datalist-item" onclick="selectDatalistItem(this)">Banana</div>
<div class="custom-datalist-item" onclick="selectDatalistItem(this)">Cherry</div>
<div class="custom-datalist-item" onclick="selectDatalistItem(this)">Date</div>
</div>
</div>
<script>
function filterDatalist(input) {
const filter = input.value.toLowerCase();
const dropdown = document.getElementById('datalist-dropdown');
const items = dropdown.querySelectorAll('.custom-datalist-item');
dropdown.classList.add('active');
items.forEach(item => {
item.style.display = item.textContent.toLowerCase().includes(filter) ? 'block' : 'none';
});
}
function selectDatalistItem(item) {
const input = document.querySelector('.custom-datalist-input');
input.value = item.textContent;
const dropdown = document.getElementById('datalist-dropdown');
dropdown.classList.remove('active');
}
</script>
</body>
</html>

16
demo/package.json Normal file
View File

@@ -0,0 +1,16 @@
{
"name": "demo",
"version": "0.0.1",
"description": "",
"main": "index.js",
"scripts": {
"dev": "vite"
},
"keywords": [],
"author": "abearxiong <xiongxiao@xiongxiao.me>",
"license": "MIT",
"type": "module",
"dependencies": {
"@kevisual/store": "link:.."
}
}

28
demo/src/index.ts Normal file
View File

@@ -0,0 +1,28 @@
// console.log('index.js');
// //@ts-ignore
import { Page } from '@kevisual/store/page';
const page = new Page({
isListen: true,
});
page.basename = '';
page.addPage('/', 'home');
page.addPage('/:id', 'user');
page.subscribe(
'home',
(params, state) => {
console.log('home', params, 'state', state);
return;
},
);
page.subscribe('user', (params, state) => {
console.log('user', params, 'state', state);
return;
});
// page.navigate('/c', { id: 3 });
// page.navigate('/c', { id: 2 });
// page.refresh();
// @ts-ignore
window.page = page;

View File

@@ -1,18 +1,22 @@
{
"name": "@kevisual/store",
"version": "0.0.1-alpha.1",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"version": "0.0.8",
"main": "dist/store.js",
"module": "dist/store.js",
"types": "dist/store.d.ts",
"private": false,
"type": "module",
"scripts": {
"dev": "rollup -c -w",
"dev:lib": "rollup -c -w",
"build": "npm run clean && rollup -c",
"test": "NODE_ENV=development node --experimental-vm-modules node_modules/jest/bin/jest.js --detectOpenHandles",
"build:app": "npm run build && rsync dist/* ../deploy/dist",
"build:lib": "npm run build",
"clean": "rm -rf dist"
},
"files": [
"dist"
"dist",
"dist-react"
],
"keywords": [
"kevisual",
@@ -22,19 +26,27 @@
"license": "ISC",
"description": "",
"devDependencies": {
"@rollup/plugin-commonjs": "^28.0.1",
"@rollup/plugin-node-resolve": "^15.3.0",
"@rollup/plugin-typescript": "^12.1.1",
"@kevisual/context": "^0.0.3",
"@kevisual/load": "^0.0.6",
"@kevisual/router": "^0.0.21",
"@kevisual/types": "link:../types",
"@rollup/plugin-commonjs": "^28.0.3",
"@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^12.1.2",
"@types/lodash-es": "^4.17.12",
"eventemitter3": "^5.0.1",
"fast-deep-equal": "^3.1.3",
"immer": "^10.1.1",
"lodash-es": "^4.17.21",
"nanoid": "^5.0.7",
"rollup": "^4.24.0",
"nanoid": "^5.1.5",
"path-to-regexp": "^8.2.0",
"rollup": "^4.41.1",
"rollup-plugin-dts": "^6.2.1",
"ts-node": "^10.9.2",
"tslib": "^2.8.0",
"typescript": "^5.6.3",
"zustand": "^5.0.0"
"tslib": "^2.8.1",
"typescript": "^5.8.3",
"zustand": "^5.0.5"
},
"publishConfig": {
"access": "public"
@@ -47,6 +59,22 @@
".": {
"import": "./dist/store.js",
"require": "./dist/store.js"
},
"./config": {
"import": "./dist/web-config.js",
"require": "./dist/web-config.js"
},
"./context": {
"import": "./dist/web-config.js",
"require": "./dist/web-config.js"
},
"./web-page.js": {
"import": "./dist/web-page.js",
"require": "./dist/web-page.js"
},
"./react": {
"import": "./dist-react/store-react.js",
"types": "./dist-react/index.d.ts"
}
}
}

24
react/package.json Normal file
View File

@@ -0,0 +1,24 @@
{
"name": "@kevisual/store-react",
"version": "0.0.1",
"description": "",
"main": "index.js",
"scripts": {
"dev:lib": "vite build --watch",
"build:lib": "vite build",
"build": "vite build"
},
"keywords": [],
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
"license": "MIT",
"packageManager": "pnpm@10.6.5",
"type": "module",
"peerDependencies": {
"react": "^19.0.0"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.3.4",
"vite": "^6.2.3",
"vite-plugin-dts": "^4.5.3"
}
}

91
react/src/Store.tsx Normal file
View File

@@ -0,0 +1,91 @@
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { StateCreator } from '../../src/store';
import { shallow, useShallow } from 'zustand/shallow';
import { useContextKey } from '../../src/web-context';
import { UseBoundStore, StoreApi } from 'zustand';
export const StoreContext = createContext<any>(null);
export const initStoreFn: StateCreator<any, [], [], any> = (set, get, store) => {
return {
mark: '123',
setMark: (mark: string) => set({ mark }),
info: 'info',
setInfo: (info) => set({ info }),
};
};
export const StoreContextProvider = ({
children,
id,
stateCreator,
}: {
children: React.ReactNode;
id: string;
stateCreator?: StateCreator<any, [], [], any>;
}) => {
const store = useContextKey<any>('store');
if (!store) {
console.error('store not found');
return null;
}
const smStore = useMemo(() => {
return store.createIfNotExists(stateCreator || initStoreFn, id);
}, [id]);
const [state, setState] = useState(smStore);
useEffect(() => {
setState(smStore);
}, [smStore]);
return <StoreContext.Provider value={state}>{children}</StoreContext.Provider>;
};
type SimpleObject = Record<string, any>;
export const useStore = function (selector: any): any {
const store = useContext(StoreContext);
const allState = store.getState();
const selectedState = selector ? selector(allState) : allState;
const [state, setState] = useState(selectedState);
useEffect(() => {
const unsubscribe = store.subscribe((newState: any) => {
const newSelectedState = selector ? selector(newState) : newState;
if (!shallow(state, newSelectedState)) {
setState(newSelectedState);
}
});
return () => unsubscribe();
}, [store, useShallow, state]);
return state;
};
useStore.getState = function (id: string) {
const store = useContextKey<any>('store');
if (!store) {
console.error('store not found');
return null;
}
return store.getStore(id).getState();
};
useStore.setState = function (id: string, state: any) {
const store = useContextKey<any>('store');
if (!store) {
console.error('store not found');
return null;
}
store.getStore(id).setState(state);
};
useStore.subscribe = function (fn: any) {
const store = useContextKey<any>('store');
if (!store) {
console.error('store not found');
return null;
}
return store.subscribe(fn);
};
export type BoundStore<T> = UseBoundStore<StoreApi<T>> & {
getState: (id: string) => T;
setState: (id: string, state: T) => void;
subscribe: (fn: (state: T) => void) => () => void;
createStore: (stateCreator: StateCreator<any, [], [], any>) => void;
createIfNotExists: (stateCreator: StateCreator<any, [], [], any>) => void;
};

View File

@@ -0,0 +1,83 @@
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { StateCreator } from '../../src/store';
import { shallow, useShallow } from 'zustand/shallow';
import { useContextKey } from '../../src/web-context';
export const initStoreFn: StateCreator<any, [], [], any> = (set, get, store) => {
return {
description: 'this is a blank store',
};
};
export const useStoreContext = (id: string, stateCreator?: StateCreator<any, [], [], any>) => {
const StoreContext = createContext<any>(null);
const store = useContextKey<any>('store');
if (!store) {
console.error('store not found');
return null;
}
if (!stateCreator) {
console.error('stateCreator not found');
return null;
}
const StoreContextProvider = ({ children, id, stateCreator }: { children: React.ReactNode; id: string; stateCreator?: StateCreator<any, [], [], any> }) => {
const smStore = useMemo(() => {
console.log('stateCreator', stateCreator);
return store.createIfNotExists(stateCreator || initStoreFn, id);
}, [id]);
const [state, setState] = useState(smStore);
useEffect(() => {
setState(smStore);
}, [smStore]);
console.log('value', smStore.getState());
// console.log('value', state);
return <StoreContext.Provider value={state}>{children}</StoreContext.Provider>;
};
const useStore = (selector?: any) => {
const store = useContext(StoreContext);
const allState = store.getState();
const selectedState = selector ? selector(allState) : allState;
const [state, setState] = useState(selectedState);
useEffect(() => {
const unsubscribe = store.subscribe((newState: any) => {
const newSelectedState = selector ? selector(newState) : newState;
setState(newSelectedState);
});
return () => unsubscribe();
}, [store, useShallow, state]);
};
useEffect(() => {
// console.log('store', store);
// @ts-ignore
window.storeContext = {
// @ts-ignore
...window.storeContext,
[id]: {
StoreContext,
Provider: ({ children }: { children: React.ReactNode }) => {
return (
<StoreContextProvider id={id} stateCreator={stateCreator}>
{children}
</StoreContextProvider>
);
},
},
};
return () => {
// @ts-ignore
delete window.storeContext[id];
};
}, [id]);
return {
StoreContext,
Provider: ({ children }: { children: React.ReactNode }) => {
return (
<StoreContextProvider id={id} stateCreator={stateCreator}>
{children}
</StoreContextProvider>
);
},
useStore,
};
};

1
react/src/index.ts Normal file
View File

@@ -0,0 +1 @@
export * from './Store';

43
react/tsconfig.json Normal file
View File

@@ -0,0 +1,43 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": [
"ES2020",
"DOM",
"DOM.Iterable"
],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
// "jsx": "react",
// "jsxFragmentFactory": "Fragment",
// "jsxFactory": "h",
"jsx": "react-jsx",
"baseUrl": "./",
"typeRoots": [
"node_modules/@types",
"node_modules/@kevisual/types",
],
"paths": {
"@/*": [
"src/*"
]
},
/* Linting */
"strict": true,
"noImplicitAny": false,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true
},
"include": [
"src",
"typings.d.ts"
]
}

25
react/vite.config.mjs Normal file
View File

@@ -0,0 +1,25 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import dts from 'vite-plugin-dts';
export default defineConfig({
build: {
lib: {
entry: './src/index.ts',
formats: ['es'],
},
outDir: '../dist-react',
emptyOutDir: true,
sourcemap: true,
rollupOptions: {
external: ['react', 'react-jsx-runtime', 'zustand'],
},
},
plugins: [
react(),
dts({
insertTypesEntry: true,
outputDir: '../dist-react/types',
}),
],
});

1
readme.md Normal file
View File

@@ -0,0 +1 @@
# cdn 版 store services

View File

@@ -3,25 +3,63 @@
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}
*/
export default [
{
input: 'src/index.ts', // TypeScript 入口文件
input: 'src/store.ts', // TypeScript 入口文件
output: {
file: 'dist/store.js', // 输出文件
format: 'es', // 输出格式设置为 ES 模块
},
plugins: [
resolve(), // 使用 @rollup/plugin-node-resolve 解析 node_modules 中的模块
resolve({ browser: true }), // 使用 @rollup/plugin-node-resolve 解析 node_modules 中的模块
commonjs(), // 使用 @rollup/plugin-commonjs 解析 CommonJS 模块
typescript({
allowImportingTsExtensions: true,
noEmit: true,
exclude: [],
}), // 使用 @rollup/plugin-typescript 处理 TypeScript 文件
typescript(), // 使用 @rollup/plugin-typescript 处理 TypeScript 文件
],
},
{
input: 'src/store.ts',
output: {
file: 'dist/store.d.ts',
format: 'es',
},
plugins: [dts()],
},
// {
// input: 'src/web-env.ts',
// output: {
// file: 'dist/web-config.js',
// format: 'es',
// },
// plugins: [resolve({ browser: true }), commonjs(), typescript()],
// },
// {
// input: 'src/web-env.ts',
// output: {
// file: 'dist/web-config.d.ts',
// format: 'es',
// },
// plugins: [dts()],
// },
{
input: 'src/page.ts',
output: {
file: 'dist/web-page.js',
format: 'es',
},
plugins: [resolve({ browser: true }), commonjs(), typescript()],
},
{
input: 'src/page.ts',
output: {
file: 'dist/web-page.d.ts',
format: 'es',
},
plugins: [dts()],
},
];

View File

@@ -1,65 +0,0 @@
import { createStore as createZutandStore, StateCreator, StoreApi } from 'zustand/vanilla';
import { shallow } from 'zustand/shallow';
import { get } from 'lodash-es';
import deepEqual from 'fast-deep-equal';
type Listener = (state: any, prevState: any) => void;
export class StoreManager {
stores: Record<string, any>;
constructor() {
this.stores = {};
}
createStore<T = any, U extends T = any>(initialStore: StateCreator<T, [], [], U>, key: string) {
if (this.stores[key]) {
return this.stores[key];
}
this.stores[key] = createZutandStore(initialStore);
return this.stores[key];
}
create<T = any, U extends T = any>(initialStore: StateCreator<T, [], [], U>, key: string) {
return this.createStore(initialStore, key);
}
getStore(key: string) {
return this.stores[key];
}
removeStore(key: string) {
delete this.stores[key];
}
clearStores() {
this.stores = {};
}
shallow(objA: any, objB: any) {
return shallow(objA, objB);
}
subscribe(fn: Listener, { key, path, deep }: { key: string; path: string; deep?: boolean }) {
const _store = this.stores[key] as StoreApi<any>;
if (!_store) {
console.error('no store', key);
return;
}
return _store.subscribe((newState, oldState) => {
try {
const newPath = get(newState, path);
const oldPath = get(oldState, path);
if (!newPath && !oldPath) {
return;
}
if (deep) {
if (!deepEqual(newPath, oldPath)) {
fn?.(newState, oldState);
}
return;
}
if (!shallow(newPath, oldPath)) {
fn?.(newState, oldState);
}
} catch (e) {
console.error('subscribe error', e);
}
});
}
}
export const store = new StoreManager();
export { shallow, deepEqual };

338
src/page.ts Normal file
View File

@@ -0,0 +1,338 @@
import { getPathKey } from '@/utils/path-key.ts';
import * as pathToRegexp from 'path-to-regexp';
import deepEqual from 'fast-deep-equal';
const generateRandom = () => {
// return Math.random().toString(36).substring(8);
return crypto.randomUUID();
};
type PageOptions = {
/**
* 路径
*/
path?: string;
/**
* key
*/
key?: string;
/**
* basename
*/
basename?: string;
isListen?: boolean;
};
type PageModule = {
// 模块的key如 user定义模块的唯一标识
key: string;
// 模块的path路径如 /user/:idmatch的时候会用到
path: string;
};
type CallFn = (params: Record<string, any>, state?: any, e?: any) => any;
type CallbackInfo = {
fn: CallFn;
id: string;
} & PageModule;
/**
* 根据basename,其中的path和key来判断一个应用的模块。
*/
export class Page {
pageModule = new Map<string, PageModule>();
// pathname的第一个路径
path: string;
// pathname的第二个路径
key: string;
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;
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;
}
/**
* 清除路径的结尾斜杠,所有的最后的斜杠都删除
*/
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);
if (item) {
const result = this.check(item.key, pathname);
result && item.fn?.(result.params, event.state, { event });
}
return;
}
}
// manual 为true时表示手动调用不需要检查是否相等
this.callbacks.forEach((item) => {
const result = this.check(item.key, pathname);
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);
}
unlisten() {
this.isListen = false;
window.removeEventListener('popstate', this.popstate.bind(this), false);
}
/**
*
* @param path 路径
* @param key
*/
addPage(path: string, key?: string) {
this.pageModule.set(key, { path, key });
}
addObjectPages(pages: Record<string, string>) {
Object.keys(pages).forEach((key) => {
this.addPage(pages[key], key);
});
}
addPages(pages: { path: string; key?: string }[]) {
pages.forEach((item) => {
this.addPage(item.path, item.key);
});
}
/**
* 检查当前路径是否匹配, 如何提交pathname则检查pathname
* @param key
* @param pathname
* @returns
*/
check(key: string, pathname?: string): false | { pathname: string; params: Record<string, any> } {
const has = this.pageModule.has(key);
if (!has) {
console.error(`PageModule ${key} not found`);
return;
}
const pageModule = this.pageModule.get(key);
if (!pageModule) {
return false;
}
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 = pathToRegexp.match(pageModule.path, { decode: decodeURIComponent });
const result = routeMatch(cur);
let params = {};
if (result) {
result.path;
params = result.params;
return { pathname: _pathname, params };
}
return false;
}
/**
* 订阅path监听事件
* @param key
* @param fn
* @param opts
* @returns
*/
async subscribe(key: string, fn?: CallFn, opts?: { pathname?: string; runImmediately?: boolean; id?: string }) {
const runImmediately = opts?.runImmediately ?? true; // 默认立即执行
const id = opts?.id ?? generateRandom();
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' });
}
}
return () => {
this.callbacks = this.callbacks.filter((item) => item.id !== id);
};
}
getPathKey() {
return getPathKey();
}
/**
* 返回当前路径不包含basename
* @returns
*/
getPath() {
const location = window.location;
const pathname = location.pathname;
return pathname.replace(this.basename, '');
}
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);
}
/**
* 如果state 和 pathname都相等则不执行popstate
* @param path
* @param state
* @param check 是否检查, 默认检查
* @returns
*/
navigate(path: string | number, state?: any, check?: boolean) {
if (typeof path === 'number') {
window.history.go(path);
return;
}
const location = window.location;
const pathname = location.pathname;
const newPathname = new URL(this.basename + path, location.href).pathname;
if (pathname === newPathname) {
if (deepEqual(state, window.history.state)) {
return;
}
}
window.history.pushState(state, '', this.basename + path);
let _check = check ?? true;
if (_check) {
this.popstate({ state } as any, { type: 'all' });
}
}
/**
* 替换当前路径
* @param path
* @param state
* @param check
*/
replace(path: string, state?: any, check?: boolean) {
let _check = check ?? true;
let newPath = this.basename + path;
if (path.startsWith('http')) {
const url = new URL(path);
const origin = url.origin;
newPath = url.toString().replace(origin, '');
}
window.history.replaceState(state, '', newPath);
if (_check) {
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
* @returns
*/
export const getHistoryState = <T = Record<string, any>>() => {
const history = window.history;
const state = history.state || {};
return state as T;
};
/**
* 设置history state
* @param state
*/
export const setHistoryState = (state: any, url?: string) => {
const history = window.history;
const oldState = getHistoryState();
history.replaceState({ ...oldState, ...state }, '', url || window.location.href);
};
/**
* 清除history state
*/
export const clearHistoryState = (url?: string) => {
const history = window.history;
history.replaceState({}, '', url || window.location.href);
};

123
src/store.ts Normal file
View File

@@ -0,0 +1,123 @@
import { createStore as createZutandStore, StateCreator, StoreApi } from 'zustand/vanilla';
import { shallow } from 'zustand/shallow';
import { get } from 'lodash-es';
import deepEqual from 'fast-deep-equal';
export const create = createZutandStore;
export type { StateCreator, StoreApi };
type Listener = (state: any, prevState: any) => void;
export class StoreManager {
stores: Record<string, any>;
constructor() {
this.stores = {};
}
createStore<T = any, U extends T = any>(initialStore: StateCreator<T, [], [], U>, key: string) {
if (this.stores[key]) {
return this.stores[key];
}
this.stores[key] = createZutandStore(initialStore);
return this.stores[key] as StoreApi<T>;
}
create<T extends Record<string, any>, U extends T = T>(initialStore: StateCreator<T, [], [], U>, key: string) {
return this.createStore(initialStore, key) as StoreApi<T>;
}
createIfNotExists<T extends Record<string, any>, U extends T = T>(
initialStore: StateCreator<T, [], [], U>,
key: string,
opts?: {
force?: boolean;
},
): StoreApi<T> {
if (this.stores[key] && !opts?.force) {
return this.stores[key];
}
if (this.stores[key] && opts?.force) {
this.removeStore(key);
}
return this.createStore(initialStore, key);
}
getStore(key: string) {
return this.stores[key];
}
removeStore(key: string) {
delete this.stores[key];
}
clearStores() {
this.stores = {};
}
shallow(objA: any, objB: any) {
return shallow(objA, objB);
}
/**
* path 为可以是 '.a.b.c'的形式
* @param fn
* @param param1
* @returns
*/
subscribe(fn: Listener, { key, path, deep, store }: { key: string; path: string; deep?: boolean; store?: StoreApi<any> }) {
const _store = store || (this.stores[key] as StoreApi<any>);
if (!_store) {
console.error('no store', key);
return;
}
return _store.subscribe((newState, oldState) => {
try {
const newPath = get(newState, path);
const oldPath = get(oldState, path);
if (!newPath && !oldPath) {
return;
}
if (deep) {
if (!deepEqual(newPath, oldPath)) {
fn?.(newState, oldState);
}
return;
}
if (!shallow(newPath, oldPath)) {
fn?.(newState, oldState);
}
} catch (e) {
console.error('subscribe error', e);
}
});
}
}
// export const store = new StoreManager();
type FnListener<T = any> = (state: T, prevState: T) => void;
type SubOptions = {
path?: string;
deep?: boolean;
store?: StoreApi<any>;
};
export const sub = <T = any>(fn: FnListener<T>, { path, deep, store }: SubOptions) => {
if (!store) {
console.error('no store');
return;
}
return store.subscribe((newState: T, oldState: T) => {
try {
const newPath = get(newState, path as string);
const oldPath = get(oldState, path as string);
if (!newPath && !oldPath) {
// 都不存在
return;
}
if (deep) {
if (!deepEqual(newPath, oldPath)) {
fn?.(newState, oldState);
}
return;
}
if (!shallow(newPath, oldPath)) {
fn?.(newState, oldState);
}
} catch (e) {
console.error('subscribe error', e);
}
});
};
export { shallow, deepEqual };

9
src/utils/path-key.ts Normal file
View File

@@ -0,0 +1,9 @@
export const getPathKey = () => {
// 从localtion.href的路径中/a/b 中 a为pathb为key
const pathname = location.pathname;
const paths = pathname.split('/');
let [path, key] = paths.slice(1);
path = path || '';
key = key || '';
return { path, key, id: path + '---' + key, prefix: `/${path}/${key}` };
};

1
src/web-env.ts Normal file
View File

@@ -0,0 +1 @@
export * from '@kevisual/context';

View File

@@ -10,8 +10,9 @@
"baseUrl": "./",
"typeRoots": [
"node_modules/@types",
"node_modules/@kevisual/types",
],
"declaration": true,
"declaration": false,
"noEmit": true,
"allowImportingTsExtensions": true,
"moduleResolution": "NodeNext",