feat: 更新container版本,修改新的引入方式
This commit is contained in:
parent
6c8fd040da
commit
7327a134b1
2
.gitignore
vendored
2
.gitignore
vendored
@ -4,3 +4,5 @@ build
|
|||||||
.cache
|
.cache
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
|
.turbo
|
3
.npmrc
Normal file
3
.npmrc
Normal 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}
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@kevisual/container",
|
"name": "@kevisual/container",
|
||||||
"version": "0.0.3",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "dist/container.js",
|
"main": "dist/container.js",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
@ -37,6 +37,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-commonjs": "^28.0.0",
|
"@rollup/plugin-commonjs": "^28.0.0",
|
||||||
"@rollup/plugin-node-resolve": "^15.3.0",
|
"@rollup/plugin-node-resolve": "^15.3.0",
|
||||||
|
"@rollup/plugin-terser": "^0.4.4",
|
||||||
"@rollup/plugin-typescript": "^12.1.0",
|
"@rollup/plugin-typescript": "^12.1.0",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
"@types/react": "^18.3.4",
|
"@types/react": "^18.3.4",
|
||||||
@ -53,6 +54,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^5.0.7",
|
"nanoid": "^5.0.7",
|
||||||
"rollup-plugin-dts": "^6.1.1",
|
"rollup-plugin-dts": "^6.1.1",
|
||||||
|
"scheduler": "^0.23.2",
|
||||||
"zustand": "^4.5.5"
|
"zustand": "^4.5.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -5,6 +5,7 @@ import resolve from '@rollup/plugin-node-resolve';
|
|||||||
import commonjs from '@rollup/plugin-commonjs';
|
import commonjs from '@rollup/plugin-commonjs';
|
||||||
import copy from 'rollup-plugin-copy';
|
import copy from 'rollup-plugin-copy';
|
||||||
import { dts } from 'rollup-plugin-dts';
|
import { dts } from 'rollup-plugin-dts';
|
||||||
|
import terser from '@rollup/plugin-terser';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {import('rollup').RollupOptions}
|
* @type {import('rollup').RollupOptions}
|
||||||
@ -18,6 +19,15 @@ const config1 = {
|
|||||||
plugins: [
|
plugins: [
|
||||||
resolve(), // 使用 @rollup/plugin-node-resolve 解析 node_modules 中的模块
|
resolve(), // 使用 @rollup/plugin-node-resolve 解析 node_modules 中的模块
|
||||||
commonjs(), //
|
commonjs(), //
|
||||||
|
terser({
|
||||||
|
format: {
|
||||||
|
comments: false, // 移除注释
|
||||||
|
},
|
||||||
|
compress: {
|
||||||
|
drop_console: true, // 移除 console.log
|
||||||
|
drop_debugger: true, // 移除 debugger
|
||||||
|
},
|
||||||
|
}),
|
||||||
typescript({
|
typescript({
|
||||||
allowImportingTsExtensions: true,
|
allowImportingTsExtensions: true,
|
||||||
noEmit: true,
|
noEmit: true,
|
||||||
@ -50,9 +60,9 @@ const config2 = {
|
|||||||
declaration: false,
|
declaration: false,
|
||||||
}), // 使用 @rollup/plugin-typescript 处理 TypeScript 文件
|
}), // 使用 @rollup/plugin-typescript 处理 TypeScript 文件
|
||||||
// 复制/src/container.css 到dist/container.css
|
// 复制/src/container.css 到dist/container.css
|
||||||
copy({
|
// copy({
|
||||||
targets: [{ src: 'src/container.css', dest: 'dist' }],
|
// targets: [{ src: 'src/container.css', dest: 'dist' }],
|
||||||
}),
|
// }),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
const config2Dts = {
|
const config2Dts = {
|
||||||
|
78
src/code-url.ts
Normal file
78
src/code-url.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { MD5 } from 'crypto-js';
|
||||||
|
|
||||||
|
type CodeUrl = {
|
||||||
|
url: string; // 地址
|
||||||
|
code?: string; // 代码
|
||||||
|
id?: string; // codeId
|
||||||
|
hash?: string;
|
||||||
|
};
|
||||||
|
export class CodeUrlManager {
|
||||||
|
urls: CodeUrl[] = [];
|
||||||
|
urlMap: { [key: string]: CodeUrl } = {};
|
||||||
|
constructor() {
|
||||||
|
this.urls = [];
|
||||||
|
}
|
||||||
|
addCode(code: string, id: string, hash?: string) {
|
||||||
|
const urlMap = this.urlMap;
|
||||||
|
const resultContent = (content: CodeUrl) => {
|
||||||
|
const index = this.urls.findIndex((item) => item.id === id);
|
||||||
|
if (index === -1) {
|
||||||
|
this.urls.push(content);
|
||||||
|
}
|
||||||
|
return content;
|
||||||
|
};
|
||||||
|
if (urlMap[hash]) {
|
||||||
|
// 可能id不同,但是hash相同。已经加载过了。
|
||||||
|
const content = {
|
||||||
|
...urlMap[hash],
|
||||||
|
id,
|
||||||
|
};
|
||||||
|
return resultContent(content);
|
||||||
|
}
|
||||||
|
const _hash = hash || MD5(code).toString();
|
||||||
|
if (urlMap[_hash]) {
|
||||||
|
const content = {
|
||||||
|
...urlMap[_hash],
|
||||||
|
id,
|
||||||
|
};
|
||||||
|
return resultContent(content);
|
||||||
|
}
|
||||||
|
const index = this.urls.findIndex((item) => item.id === id);
|
||||||
|
if (index > -1) {
|
||||||
|
// 没有共同hash的值了,但是还是找了id,这个时候应该移除之前
|
||||||
|
const url = this.urls[index];
|
||||||
|
const list = this.urls.filter((item) => item.hash === url.hash);
|
||||||
|
if (list.length <= 1) {
|
||||||
|
// 这个hash的值没有人用了
|
||||||
|
URL.revokeObjectURL(url.url);
|
||||||
|
this.urlMap[url.hash] = null;
|
||||||
|
}
|
||||||
|
this.urls.splice(index, 1);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 全新代码
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
const url = URL.createObjectURL(new Blob([code], { type: 'application/javascript' }));
|
||||||
|
const content = {
|
||||||
|
id,
|
||||||
|
url,
|
||||||
|
code,
|
||||||
|
hash: _hash,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.urls.push(content);
|
||||||
|
urlMap[_hash] = { ...content, id: null };
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
removeUrl(id?: string, url?: string) {
|
||||||
|
const index = this.urls.findIndex((item) => item.url === url || item.id === id);
|
||||||
|
if (index > -1) {
|
||||||
|
const url = this.urls[index];
|
||||||
|
URL.revokeObjectURL(url.url);
|
||||||
|
this.urlMap[url.hash] = null;
|
||||||
|
this.urls.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export const codeUrlManager = new CodeUrlManager();
|
@ -1,8 +1,10 @@
|
|||||||
import { Container, ContainerOpts } from './container';
|
import { Container, ContainerOpts, createEmotion } from './container';
|
||||||
import { addListener } from './listener/dom';
|
import { addListener } from './listener/dom';
|
||||||
|
|
||||||
export type ContainerEditOpts = {
|
export type ContainerEditOpts = {
|
||||||
edit?: boolean;
|
edit?: boolean;
|
||||||
mask?: boolean;
|
mask?: boolean;
|
||||||
|
initCss?: string;
|
||||||
} & ContainerOpts;
|
} & ContainerOpts;
|
||||||
let isListener = false;
|
let isListener = false;
|
||||||
export class ContainerEdit extends Container {
|
export class ContainerEdit extends Container {
|
||||||
@ -28,9 +30,12 @@ export class ContainerEdit extends Container {
|
|||||||
addListener(this.root);
|
addListener(this.root);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (opts.initCss && this.root) {
|
||||||
|
this.root.classList.add(kvContainerStyle);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
renderChildren(cid: string, parentElement?: any, pid?: any) {
|
renderChildren(cid: string, parentElement?: any, pid?: any, isNew = true) {
|
||||||
const el = super.renderChildren(cid, parentElement, pid);
|
const el = super.renderChildren(cid, parentElement, pid, isNew);
|
||||||
|
|
||||||
if (el) {
|
if (el) {
|
||||||
const computedStyle = window.getComputedStyle(el);
|
const computedStyle = window.getComputedStyle(el);
|
||||||
@ -72,3 +77,28 @@ export class ContainerEdit extends Container {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const css = `.kv-container.active {
|
||||||
|
background: #000 ;
|
||||||
|
border: 2px solid #195ca9;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
.kv-container.dragging {
|
||||||
|
opacity: 0.5; /* 拖动时降低透明度 */
|
||||||
|
box-sizing: content-box;
|
||||||
|
border: 2px dashed blue;
|
||||||
|
}
|
||||||
|
.kv-container.hover {
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
|
.kv-container > .resizer, .kv-container > .drag-title {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.kv-container.active > .resizer, .kv-container.active > .drag-title {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const emotion = createEmotion({ key: 'kv-container-style', speedy: true });
|
||||||
|
|
||||||
|
const kvContainerStyle = emotion.css(css);
|
||||||
|
351
src/container-old.ts
Normal file
351
src/container-old.ts
Normal file
@ -0,0 +1,351 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import createEmotion from '@emotion/css/create-instance';
|
||||||
|
import EventEmitter from 'eventemitter3';
|
||||||
|
import { ContainerEvent } from './event/continer';
|
||||||
|
import { RenderCode } from './render';
|
||||||
|
import { codeUrlManager } from './code-url';
|
||||||
|
|
||||||
|
const handleOneCode = async (data: RenderData) => {
|
||||||
|
const { code } = data;
|
||||||
|
const urlsBegin = ['http', 'https', 'blob', '//:'];
|
||||||
|
if (typeof code === 'string' && code && urlsBegin.find((item) => code.startsWith(item))) {
|
||||||
|
const importContent = await import(/* @vite-ignore */ code);
|
||||||
|
const { render, unmount, ...rest } = importContent || {};
|
||||||
|
const _data = {
|
||||||
|
...data,
|
||||||
|
code: {
|
||||||
|
render,
|
||||||
|
unmount,
|
||||||
|
...rest,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return _data;
|
||||||
|
}
|
||||||
|
if (typeof code === 'string' && data.codeId) {
|
||||||
|
const codeId = data.codeId;
|
||||||
|
const { url, hash } = codeUrlManager.addCode(code, codeId, data.hash);
|
||||||
|
const importContent = await import(/* @vite-ignore */ url);
|
||||||
|
const { render, unmount, ...rest } = importContent || {};
|
||||||
|
const _data = {
|
||||||
|
...data,
|
||||||
|
codeId,
|
||||||
|
hash,
|
||||||
|
code: {
|
||||||
|
render,
|
||||||
|
unmount,
|
||||||
|
...rest,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return _data;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCode = async (data: RenderData[]) => {
|
||||||
|
const handleData = [];
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
const item = data[i];
|
||||||
|
const _data = await handleOneCode(item);
|
||||||
|
handleData.push(_data);
|
||||||
|
}
|
||||||
|
return handleData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RenderData = {
|
||||||
|
id: string; // id不会重复
|
||||||
|
children?: string[];
|
||||||
|
parents?: string[];
|
||||||
|
className?: string;
|
||||||
|
style?: React.CSSProperties | { [key: string]: string };
|
||||||
|
code: RenderCode | string;
|
||||||
|
codeId?: string;
|
||||||
|
shadowRoot?: boolean;
|
||||||
|
hash?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ContainerOpts = {
|
||||||
|
root?: HTMLDivElement | string;
|
||||||
|
shadowRoot?: ShadowRoot;
|
||||||
|
destroy?: () => void;
|
||||||
|
data?: RenderData[];
|
||||||
|
showChild?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class Container {
|
||||||
|
data: RenderData[];
|
||||||
|
root: HTMLDivElement;
|
||||||
|
globalCss: any;
|
||||||
|
showChild: boolean;
|
||||||
|
event: EventEmitter;
|
||||||
|
entryId: string = '';
|
||||||
|
loading: boolean = false;
|
||||||
|
loaded: boolean = false;
|
||||||
|
constructor(opts: ContainerOpts) {
|
||||||
|
const data = opts.data || [];
|
||||||
|
this.loadData.apply(this, [data]);
|
||||||
|
|
||||||
|
const rootElement = typeof opts.root === 'string' ? document.querySelector(opts.root) : opts.root;
|
||||||
|
this.root = rootElement || (document.body as any);
|
||||||
|
this.globalCss = createEmotion({ key: 'css-global', speedy: true }).css;
|
||||||
|
this.showChild = opts.showChild ?? true;
|
||||||
|
const event = new EventEmitter();
|
||||||
|
this.event = event;
|
||||||
|
const listening = this.root.dataset.listening;
|
||||||
|
if (listening !== 'true') {
|
||||||
|
this.root.addEventListener('onContainer', (e: ContainerEvent) => {
|
||||||
|
const { type, data } = e.data;
|
||||||
|
this.event.emit(type, data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async loadData(data: RenderData[], key: string = '') {
|
||||||
|
if (data.length === 0) {
|
||||||
|
this.data = data;
|
||||||
|
this.loaded = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.loading = true;
|
||||||
|
this.data = await handleCode(data);
|
||||||
|
this.loading = false;
|
||||||
|
this.event.emit('loadedData' + key);
|
||||||
|
this.loaded = true;
|
||||||
|
}
|
||||||
|
async getData({ id, codeId }: { id?: string; codeId?: string }) {
|
||||||
|
if (id && codeId) {
|
||||||
|
return this.data.find((item) => item.id === id && item.codeId === codeId);
|
||||||
|
}
|
||||||
|
if (id) {
|
||||||
|
return this.data.find((item) => item.id === id);
|
||||||
|
}
|
||||||
|
if (codeId) {
|
||||||
|
return this.data.find((item) => item.codeId === codeId);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
async updateDataCode(data: RenderData[]) {
|
||||||
|
if (this.loading) {
|
||||||
|
console.warn('loading');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const _data = this.data.map((item) => {
|
||||||
|
const node = data.find((node) => node.codeId && node.codeId === item.codeId);
|
||||||
|
if (node) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
...node,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
await this.loadData(_data);
|
||||||
|
}
|
||||||
|
async updateData(data: RenderData[]) {
|
||||||
|
if (this.loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const _data = this.data.map((item) => {
|
||||||
|
const node = data.find((node) => node.id === item.id);
|
||||||
|
if (node) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
...node,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
console.log('updateData', _data.length, data.length);
|
||||||
|
await this.loadData(_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderChildren(cid: string, parentElement?: any, pid?: any): void | HTMLDivElement {
|
||||||
|
const data = this.data;
|
||||||
|
console.log('renderChildren', cid, data);
|
||||||
|
const globalCss = this.globalCss;
|
||||||
|
if (!parentElement) {
|
||||||
|
parentElement = this.root;
|
||||||
|
this.root.dataset.entryId = cid;
|
||||||
|
this.entryId = cid;
|
||||||
|
}
|
||||||
|
const renderChildren = this.renderChildren.bind(this);
|
||||||
|
const node = data.find((node: RenderData) => node.id === cid);
|
||||||
|
const event = this.event;
|
||||||
|
if (!node) {
|
||||||
|
console.warn('node not found', cid);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { style, code } = node;
|
||||||
|
const el = document.createElement('div');
|
||||||
|
const root = parentElement.appendChild(el);
|
||||||
|
const parentIds = parentElement.dataset.parentIds || '';
|
||||||
|
const shadowRoot = node.shadowRoot ? el.attachShadow({ mode: 'open' }) : null;
|
||||||
|
const { css, sheet, cache } = createEmotion({ key: 'css' });
|
||||||
|
el.dataset.cid = cid;
|
||||||
|
el.dataset.pid = pid;
|
||||||
|
el.dataset.parentIds = parentIds ? parentIds + ',' + cid : cid;
|
||||||
|
if (shadowRoot) {
|
||||||
|
cache.insert = (selector: string, serialized: any, sheet: any, shouldCache: boolean) => {
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = selector;
|
||||||
|
shadowRoot.appendChild(style);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (el.style) {
|
||||||
|
el.className = !shadowRoot ? globalCss(style as any) : css(style as any);
|
||||||
|
}
|
||||||
|
el.classList.add(cid, 'kv-container');
|
||||||
|
const { render, unmount } = (code as RenderCode) || {};
|
||||||
|
let renderRoot = node.shadowRoot ? shadowRoot : root;
|
||||||
|
const ctx = {
|
||||||
|
root: root,
|
||||||
|
shadowRoot,
|
||||||
|
renderRoot,
|
||||||
|
event,
|
||||||
|
container: this,
|
||||||
|
code: code as RenderCode,
|
||||||
|
data: node,
|
||||||
|
css: shadowRoot ? css : globalCss,
|
||||||
|
};
|
||||||
|
if (render) {
|
||||||
|
render(ctx);
|
||||||
|
} else {
|
||||||
|
// no render, so no insert
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event) {
|
||||||
|
const destroy = (id: string) => {
|
||||||
|
if (id) {
|
||||||
|
if (id === cid || node?.parents?.find?.((item) => item === id)) {
|
||||||
|
unmount?.(ctx); // 销毁父亲元素有这个元素的
|
||||||
|
event.off('destroy', destroy); // 移除监听
|
||||||
|
} else {
|
||||||
|
// console.warn('destroy id not found, and not find parentIds', id, 'currentid', cid);
|
||||||
|
}
|
||||||
|
} else if (!id) {
|
||||||
|
unmount?.(ctx); // 所有的都销毁
|
||||||
|
event.off('destroy', destroy);
|
||||||
|
}
|
||||||
|
// 不需要销毁子元素
|
||||||
|
};
|
||||||
|
event.on('destroy', destroy);
|
||||||
|
}
|
||||||
|
if (shadowRoot) {
|
||||||
|
const slot = document.createElement('slot');
|
||||||
|
shadowRoot.appendChild(slot);
|
||||||
|
}
|
||||||
|
if (!this.showChild) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const childrenIds = node.children || [];
|
||||||
|
childrenIds.forEach((childId) => {
|
||||||
|
renderChildren(childId, root, cid);
|
||||||
|
});
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
async destroy(id?: string) {
|
||||||
|
if (!id) {
|
||||||
|
this.root.innerHTML = '';
|
||||||
|
} else {
|
||||||
|
const elements = this.root.querySelectorAll(`[data-cid="${id}"]`);
|
||||||
|
elements.forEach((element) => element.remove());
|
||||||
|
}
|
||||||
|
this.event.emit('destroy', id);
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve(null);
|
||||||
|
}, 200);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 只能用一次
|
||||||
|
* @param entryId
|
||||||
|
* @param data
|
||||||
|
* @param opts
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async render(entryId: string, data?: RenderData[], opts: { reload?: boolean } = {}) {
|
||||||
|
if (this.entryId && this.entryId !== entryId) {
|
||||||
|
await this.destroy();
|
||||||
|
} else if (this.entryId) {
|
||||||
|
this.destroy(entryId);
|
||||||
|
}
|
||||||
|
const _data = data || this.data;
|
||||||
|
this.entryId = entryId;
|
||||||
|
if (opts?.reload) {
|
||||||
|
this.loading = true;
|
||||||
|
await this.loadData.apply(this, [_data]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.loading || !this.loaded) {
|
||||||
|
this.event.once('loadedData', () => {
|
||||||
|
this.renderChildren(entryId);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
this.renderChildren(entryId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async hotReload(id: string) {
|
||||||
|
await this.destroy(id);
|
||||||
|
// const node = document.querySelector(`[data-cid="${id}"]`);
|
||||||
|
if (this.loading) {
|
||||||
|
this.event.once('loadedData', () => {
|
||||||
|
this.renderChildren(id);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
this.renderChildren(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async reRender() {
|
||||||
|
await this.destroy();
|
||||||
|
this.renderChildren(this.entryId);
|
||||||
|
}
|
||||||
|
async renderId(id: string) {
|
||||||
|
if (!this.entryId) {
|
||||||
|
this.render(id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (id === this.entryId) {
|
||||||
|
this.reRender();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const node = this.data.find((item) => item.id === id);
|
||||||
|
if (node?.parents && node.parents.length > 0) {
|
||||||
|
const parent = node.parents[node.parents.length - 1];
|
||||||
|
const parentElement = this.root.querySelector(`[data-cid="${parent}"]`);
|
||||||
|
await this.destroy(id);
|
||||||
|
this.renderChildren(id, parentElement, parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close() {
|
||||||
|
this.destroy();
|
||||||
|
const event = this.event;
|
||||||
|
event && event.removeAllListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class ContainerOne extends Container {
|
||||||
|
constructor(opts: ContainerOpts) {
|
||||||
|
super(opts);
|
||||||
|
}
|
||||||
|
async renderOne({ code: RenderCode }) {
|
||||||
|
const newData = { codeId: 'test-reneder-one-code-id', id: 'test-render-one', code: RenderCode };
|
||||||
|
console.log('loading', this.loading);
|
||||||
|
if (this.loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('renderOne', newData);
|
||||||
|
if (this.data.length === 0) {
|
||||||
|
await this.loadData([newData]);
|
||||||
|
} else {
|
||||||
|
await this.updateData([newData]);
|
||||||
|
}
|
||||||
|
this.hotReload('test-render-one');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export const mount = ({ render, unmount }, root: string | HTMLDivElement) => {
|
||||||
|
let _root = typeof root === 'string' ? document.querySelector(root) : root;
|
||||||
|
_root = _root || (document.body as any);
|
||||||
|
render({ root: _root });
|
||||||
|
};
|
@ -1,5 +1,5 @@
|
|||||||
.kv-container.active {
|
.kv-container.active {
|
||||||
background: #000 !important;
|
background: #000;
|
||||||
border: 2px solid #195ca9;
|
border: 2px solid #195ca9;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
@ -11,10 +11,11 @@
|
|||||||
.kv-container.hover {
|
.kv-container.hover {
|
||||||
cursor: move;
|
cursor: move;
|
||||||
}
|
}
|
||||||
.kv-container > .resizer, .kv-container > .drag-title {
|
.kv-container > .resizer,
|
||||||
|
.kv-container > .drag-title {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.kv-container.active > .resizer, .kv-container.active > .drag-title {
|
.kv-container.active > .resizer,
|
||||||
|
.kv-container.active > .drag-title {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
480
src/container.ts
480
src/container.ts
@ -1,130 +1,12 @@
|
|||||||
import createEmotion from '@emotion/css/create-instance';
|
import createEmotion from '@emotion/css/create-instance';
|
||||||
import EventEmitter from 'eventemitter3';
|
import EventEmitter from 'eventemitter3';
|
||||||
import { MD5 } from 'crypto-js';
|
|
||||||
import { ContainerEvent } from './event/continer';
|
import { ContainerEvent } from './event/continer';
|
||||||
import { RenderCode } from './render';
|
import { RenderCode } from './render';
|
||||||
|
import * as schedule from './scheduler';
|
||||||
|
import { CodeUrlAdapter } from './handle/adapter';
|
||||||
|
import { codeUrlManager } from './code-url';
|
||||||
|
|
||||||
type CodeUrl = {
|
export { createEmotion };
|
||||||
url: string; // 地址
|
|
||||||
code?: string; // 代码
|
|
||||||
id?: string; // codeId
|
|
||||||
hash?: string;
|
|
||||||
};
|
|
||||||
export class CodeUrlManager {
|
|
||||||
urls: CodeUrl[] = [];
|
|
||||||
urlMap: { [key: string]: CodeUrl } = {};
|
|
||||||
constructor() {
|
|
||||||
this.urls = [];
|
|
||||||
}
|
|
||||||
addCode(code: string, id: string, hash?: string) {
|
|
||||||
const urlMap = this.urlMap;
|
|
||||||
const resultContent = (content: CodeUrl) => {
|
|
||||||
const index = this.urls.findIndex((item) => item.id === id);
|
|
||||||
if (index === -1) {
|
|
||||||
this.urls.push(content);
|
|
||||||
}
|
|
||||||
return content;
|
|
||||||
};
|
|
||||||
if (urlMap[hash]) {
|
|
||||||
// 可能id不同,但是hash相同。已经加载过了。
|
|
||||||
const content = {
|
|
||||||
...urlMap[hash],
|
|
||||||
id,
|
|
||||||
};
|
|
||||||
return resultContent(content);
|
|
||||||
}
|
|
||||||
const _hash = hash || MD5(code).toString();
|
|
||||||
if (urlMap[_hash]) {
|
|
||||||
const content = {
|
|
||||||
...urlMap[_hash],
|
|
||||||
id,
|
|
||||||
};
|
|
||||||
return resultContent(content);
|
|
||||||
}
|
|
||||||
const index = this.urls.findIndex((item) => item.id === id);
|
|
||||||
if (index > -1) {
|
|
||||||
// 没有共同hash的值了,但是还是找了id,这个时候应该移除之前
|
|
||||||
const url = this.urls[index];
|
|
||||||
const list = this.urls.filter((item) => item.hash === url.hash);
|
|
||||||
if (list.length <= 1) {
|
|
||||||
// 这个hash的值没有人用了
|
|
||||||
URL.revokeObjectURL(url.url);
|
|
||||||
this.urlMap[url.hash] = null;
|
|
||||||
}
|
|
||||||
this.urls.splice(index, 1);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* 全新代码
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
const url = URL.createObjectURL(new Blob([code], { type: 'application/javascript' }));
|
|
||||||
const content = {
|
|
||||||
id,
|
|
||||||
url,
|
|
||||||
code,
|
|
||||||
hash: _hash,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.urls.push(content);
|
|
||||||
urlMap[_hash] = { ...content, id: null };
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
removeUrl(id?: string, url?: string) {
|
|
||||||
const index = this.urls.findIndex((item) => item.url === url || item.id === id);
|
|
||||||
if (index > -1) {
|
|
||||||
const url = this.urls[index];
|
|
||||||
URL.revokeObjectURL(url.url);
|
|
||||||
this.urlMap[url.hash] = null;
|
|
||||||
this.urls.splice(index, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export const codeUrlManager = new CodeUrlManager();
|
|
||||||
const handleOneCode = async (data: RenderData) => {
|
|
||||||
const { code } = data;
|
|
||||||
const urlsBegin = ['http', 'https', 'blob', '//:'];
|
|
||||||
if (typeof code === 'string' && code && urlsBegin.find((item) => code.startsWith(item))) {
|
|
||||||
const importContent = await import(/* @vite-ignore */ code);
|
|
||||||
const { render, unmount, ...rest } = importContent || {};
|
|
||||||
const _data = {
|
|
||||||
...data,
|
|
||||||
code: {
|
|
||||||
render,
|
|
||||||
unmount,
|
|
||||||
...rest,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return _data;
|
|
||||||
}
|
|
||||||
if (typeof code === 'string' && data.codeId) {
|
|
||||||
const codeId = data.codeId;
|
|
||||||
const { url, hash } = codeUrlManager.addCode(code, codeId, data.hash);
|
|
||||||
const importContent = await import(/* @vite-ignore */ url);
|
|
||||||
const { render, unmount, ...rest } = importContent || {};
|
|
||||||
const _data = {
|
|
||||||
...data,
|
|
||||||
codeId,
|
|
||||||
hash,
|
|
||||||
code: {
|
|
||||||
render,
|
|
||||||
unmount,
|
|
||||||
...rest,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return _data;
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
const handleCode = async (data: RenderData[]) => {
|
|
||||||
const handleData = [];
|
|
||||||
for (let i = 0; i < data.length; i++) {
|
|
||||||
const item = data[i];
|
|
||||||
const _data = await handleOneCode(item);
|
|
||||||
handleData.push(_data);
|
|
||||||
}
|
|
||||||
return handleData;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RenderData = {
|
export type RenderData = {
|
||||||
id: string; // id不会重复
|
id: string; // id不会重复
|
||||||
children?: string[];
|
children?: string[];
|
||||||
@ -141,31 +23,27 @@ export type RenderData = {
|
|||||||
export type ContainerOpts = {
|
export type ContainerOpts = {
|
||||||
root?: HTMLDivElement | string;
|
root?: HTMLDivElement | string;
|
||||||
shadowRoot?: ShadowRoot;
|
shadowRoot?: ShadowRoot;
|
||||||
destroy?: () => void;
|
|
||||||
data?: RenderData[];
|
data?: RenderData[];
|
||||||
showChild?: boolean;
|
adapter?: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class Container {
|
export class Container {
|
||||||
data: RenderData[];
|
data: RenderData[];
|
||||||
root: HTMLDivElement;
|
root: HTMLDivElement;
|
||||||
globalCss: any;
|
globalCss: any;
|
||||||
showChild: boolean;
|
|
||||||
event: EventEmitter;
|
event: EventEmitter;
|
||||||
entryId: string = '';
|
entryId: string = '';
|
||||||
loading: boolean = false;
|
adapter: CodeUrlAdapter;
|
||||||
loaded: boolean = false;
|
|
||||||
constructor(opts: ContainerOpts) {
|
constructor(opts: ContainerOpts) {
|
||||||
const data = opts.data || [];
|
const data = opts.data || [];
|
||||||
this.loadData.apply(this, [data]);
|
this.data = data;
|
||||||
|
|
||||||
const rootElement = typeof opts.root === 'string' ? document.querySelector(opts.root) : opts.root;
|
const rootElement = typeof opts.root === 'string' ? document.querySelector(opts.root) : opts.root;
|
||||||
this.root = rootElement || (document.body as any);
|
this.root = rootElement || (document.body as any);
|
||||||
this.globalCss = createEmotion({ key: 'css-global', speedy: true }).css;
|
this.globalCss = createEmotion({ key: 'css-global', speedy: true }).css;
|
||||||
this.showChild = opts.showChild ?? true;
|
|
||||||
const event = new EventEmitter();
|
const event = new EventEmitter();
|
||||||
this.event = event;
|
this.event = event;
|
||||||
const listening = this.root.dataset.listening;
|
const listening = this.root.dataset.listening;
|
||||||
|
this.adapter = opts.adapter || this.createAdapter();
|
||||||
if (listening !== 'true') {
|
if (listening !== 'true') {
|
||||||
this.root.addEventListener('onContainer', (e: ContainerEvent) => {
|
this.root.addEventListener('onContainer', (e: ContainerEvent) => {
|
||||||
const { type, data } = e.data;
|
const { type, data } = e.data;
|
||||||
@ -173,68 +51,31 @@ export class Container {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async loadData(data: RenderData[], key: string = '') {
|
protected createAdapter() {
|
||||||
if (data.length === 0) {
|
const adapter = new CodeUrlAdapter({ codeUrlManager });
|
||||||
this.data = data;
|
this.adapter = adapter;
|
||||||
this.loaded = true;
|
return adapter;
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.loading = true;
|
|
||||||
this.data = await handleCode(data);
|
|
||||||
this.loading = false;
|
|
||||||
this.event.emit('loadedData' + key);
|
|
||||||
this.loaded = true;
|
|
||||||
}
|
}
|
||||||
async getData({ id, codeId }: { id?: string; codeId?: string }) {
|
getData({ id }: { id: string }) {
|
||||||
if (id && codeId) {
|
const data = this.data;
|
||||||
return this.data.find((item) => item.id === id && item.codeId === codeId);
|
const node = data.find((node) => node.id === id);
|
||||||
}
|
return node;
|
||||||
if (id) {
|
}
|
||||||
return this.data.find((item) => item.id === id);
|
updateData(data: RenderData[]) {
|
||||||
}
|
this.data = this.data.map((node) => {
|
||||||
if (codeId) {
|
const find = data.find((item) => item.id === node.id);
|
||||||
return this.data.find((item) => item.codeId === codeId);
|
return find || node;
|
||||||
}
|
});
|
||||||
return null;
|
}
|
||||||
}
|
/**
|
||||||
async updateDataCode(data: RenderData[]) {
|
* 渲染模块
|
||||||
if (this.loading) {
|
* @param cid
|
||||||
console.warn('loading');
|
* @param parentElement
|
||||||
return;
|
* @param pid
|
||||||
}
|
* @returns
|
||||||
const _data = this.data.map((item) => {
|
*/
|
||||||
const node = data.find((node) => node.codeId && node.codeId === item.codeId);
|
renderChildren(cid: string, parentElement?: any, pid?: any, isNew = true): void | HTMLDivElement {
|
||||||
if (node) {
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
...node,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return item;
|
|
||||||
});
|
|
||||||
await this.loadData(_data);
|
|
||||||
}
|
|
||||||
async updateData(data: RenderData[]) {
|
|
||||||
if (this.loading) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const _data = this.data.map((item) => {
|
|
||||||
const node = data.find((node) => node.id === item.id);
|
|
||||||
if (node) {
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
...node,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return item;
|
|
||||||
});
|
|
||||||
console.log('updateData', _data.length, data.length);
|
|
||||||
await this.loadData(_data);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderChildren(cid: string, parentElement?: any, pid?: any): void | HTMLDivElement {
|
|
||||||
const data = this.data;
|
const data = this.data;
|
||||||
console.log('renderChildren', cid, data);
|
|
||||||
const globalCss = this.globalCss;
|
const globalCss = this.globalCss;
|
||||||
if (!parentElement) {
|
if (!parentElement) {
|
||||||
parentElement = this.root;
|
parentElement = this.root;
|
||||||
@ -243,20 +84,64 @@ export class Container {
|
|||||||
}
|
}
|
||||||
const renderChildren = this.renderChildren.bind(this);
|
const renderChildren = this.renderChildren.bind(this);
|
||||||
const node = data.find((node: RenderData) => node.id === cid);
|
const node = data.find((node: RenderData) => node.id === cid);
|
||||||
const event = this.event;
|
|
||||||
if (!node) {
|
if (!node) {
|
||||||
console.warn('node not found', cid);
|
console.warn('node not found', cid);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
let el;
|
||||||
|
if (isNew) {
|
||||||
|
el = document.createElement('div');
|
||||||
|
parentElement.appendChild(el);
|
||||||
|
const parentIds = parentElement.dataset.parentIds || '';
|
||||||
|
|
||||||
|
el.dataset.cid = cid;
|
||||||
|
el.dataset.pid = pid;
|
||||||
|
el.dataset.parentIds = parentIds ? parentIds + ',' + cid : cid;
|
||||||
|
// 代码渲染
|
||||||
|
const { renderFn } = this.getContext({ renderEl: el, entryId: cid });
|
||||||
|
schedule.addTask(renderFn, 'high');
|
||||||
|
// 代码渲染结束
|
||||||
|
} else {
|
||||||
|
el = parentElement.querySelector(`[data-cid="${cid}"]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!el) {
|
||||||
|
console.warn('when no createNewElement el not found', cid);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const childrenIds = node.children || [];
|
||||||
|
childrenIds.forEach((childId) => {
|
||||||
|
renderChildren(childId, el, cid);
|
||||||
|
});
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 渲染根节点
|
||||||
|
* @param entryId
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async renderRoot(entryId: string) {
|
||||||
|
if (this.entryId && this.entryId !== entryId) {
|
||||||
|
await this.destroy();
|
||||||
|
} else if (this.entryId) {
|
||||||
|
await this.destroy(entryId);
|
||||||
|
}
|
||||||
|
this.renderChildren(entryId);
|
||||||
|
}
|
||||||
|
async reRender() {
|
||||||
|
await this.destroy();
|
||||||
|
this.renderChildren(this.entryId);
|
||||||
|
}
|
||||||
|
protected getContext({ renderEl, entryId }: { renderEl: HTMLDivElement; entryId: string }) {
|
||||||
|
const node = this.data.find((node) => node.id === entryId);
|
||||||
const { style, code } = node;
|
const { style, code } = node;
|
||||||
const el = document.createElement('div');
|
const event = this.event;
|
||||||
const root = parentElement.appendChild(el);
|
const globalCss = this.globalCss;
|
||||||
const parentIds = parentElement.dataset.parentIds || '';
|
const root = renderEl;
|
||||||
const shadowRoot = node.shadowRoot ? el.attachShadow({ mode: 'open' }) : null;
|
const adapter = this.adapter;
|
||||||
|
const containerThis = this;
|
||||||
|
const shadowRoot = node.shadowRoot ? renderEl.attachShadow({ mode: 'open' }) : null;
|
||||||
const { css, sheet, cache } = createEmotion({ key: 'css' });
|
const { css, sheet, cache } = createEmotion({ key: 'css' });
|
||||||
el.dataset.cid = cid;
|
|
||||||
el.dataset.pid = pid;
|
|
||||||
el.dataset.parentIds = parentIds ? parentIds + ',' + cid : cid;
|
|
||||||
if (shadowRoot) {
|
if (shadowRoot) {
|
||||||
cache.insert = (selector: string, serialized: any, sheet: any, shouldCache: boolean) => {
|
cache.insert = (selector: string, serialized: any, sheet: any, shouldCache: boolean) => {
|
||||||
const style = document.createElement('style');
|
const style = document.createElement('style');
|
||||||
@ -264,64 +149,94 @@ export class Container {
|
|||||||
shadowRoot.appendChild(style);
|
shadowRoot.appendChild(style);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (el.style) {
|
const cid = entryId;
|
||||||
el.className = !shadowRoot ? globalCss(style as any) : css(style as any);
|
renderEl.className = '';
|
||||||
}
|
if (renderEl.style) {
|
||||||
el.classList.add(cid, 'kv-container');
|
renderEl.className = !shadowRoot ? globalCss(style as any) : css(style as any);
|
||||||
const { render, unmount } = (code as RenderCode) || {};
|
|
||||||
let renderRoot = node.shadowRoot ? shadowRoot : root;
|
|
||||||
const ctx = {
|
|
||||||
root: root,
|
|
||||||
shadowRoot,
|
|
||||||
renderRoot,
|
|
||||||
event,
|
|
||||||
container: this,
|
|
||||||
code: code as RenderCode,
|
|
||||||
data: node,
|
|
||||||
css: shadowRoot ? css : globalCss,
|
|
||||||
};
|
|
||||||
if (render) {
|
|
||||||
render(ctx);
|
|
||||||
} else {
|
|
||||||
// no render, so no insert
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event) {
|
|
||||||
const destroy = (id: string) => {
|
|
||||||
if (id) {
|
|
||||||
if (id === cid || node?.parents?.find?.((item) => item === id)) {
|
|
||||||
unmount?.(ctx); // 销毁父亲元素有这个元素的
|
|
||||||
event.off('destroy', destroy); // 移除监听
|
|
||||||
} else {
|
|
||||||
// console.warn('destroy id not found, and not find parentIds', id, 'currentid', cid);
|
|
||||||
}
|
|
||||||
} else if (!id) {
|
|
||||||
unmount?.(ctx); // 所有的都销毁
|
|
||||||
event.off('destroy', destroy);
|
|
||||||
}
|
|
||||||
// 不需要销毁子元素
|
|
||||||
};
|
|
||||||
event.on('destroy', destroy);
|
|
||||||
}
|
}
|
||||||
|
renderEl.classList.add(cid, 'kv-container');
|
||||||
if (shadowRoot) {
|
if (shadowRoot) {
|
||||||
const slot = document.createElement('slot');
|
const slot = document.createElement('slot');
|
||||||
shadowRoot.appendChild(slot);
|
shadowRoot.appendChild(slot);
|
||||||
}
|
}
|
||||||
if (!this.showChild) {
|
const renderFn = async () => {
|
||||||
|
const { code: newCode } = await adapter.handle(node as any);
|
||||||
|
const { render, unmount } = newCode || {};
|
||||||
|
let renderRoot = node.shadowRoot ? shadowRoot : root;
|
||||||
|
const ctx = {
|
||||||
|
root: root,
|
||||||
|
shadowRoot,
|
||||||
|
renderRoot,
|
||||||
|
event,
|
||||||
|
container: containerThis,
|
||||||
|
code: newCode as RenderCode,
|
||||||
|
data: node,
|
||||||
|
css: shadowRoot ? css : globalCss,
|
||||||
|
};
|
||||||
|
if (render) {
|
||||||
|
render(ctx);
|
||||||
|
} else {
|
||||||
|
// no render, so no insert
|
||||||
|
}
|
||||||
|
if (event) {
|
||||||
|
const destroy = (id: string) => {
|
||||||
|
if (id) {
|
||||||
|
if (id === cid || node?.parents?.find?.((item) => item === id)) {
|
||||||
|
unmount?.(ctx); // 销毁父亲元素有这个元素的
|
||||||
|
event.off('destroy', destroy); // 移除监听
|
||||||
|
} else {
|
||||||
|
// console.warn('destroy id not found, and not find parentIds', id, 'currentid', cid);
|
||||||
|
}
|
||||||
|
} else if (!id) {
|
||||||
|
unmount?.(ctx); // 所有的都销毁
|
||||||
|
event.off('destroy', destroy);
|
||||||
|
}
|
||||||
|
// 不需要销毁子元素
|
||||||
|
};
|
||||||
|
event.on('destroy', destroy);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return { renderFn, shadowRoot };
|
||||||
|
}
|
||||||
|
// 更新渲染
|
||||||
|
async render(entryId: string, el?: HTMLDivElement) {
|
||||||
|
if (schedule.getLoading()) {
|
||||||
|
console.log('schedule is loading');
|
||||||
|
}
|
||||||
|
if (!this.entryId) {
|
||||||
|
console.warn('root is use renderRoot method, not render');
|
||||||
|
return this.renderRoot(entryId);
|
||||||
|
}
|
||||||
|
const isRoot = this.entryId === entryId;
|
||||||
|
if (isRoot) {
|
||||||
|
await this.reRender();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const childrenIds = node.children || [];
|
let renderEl = el;
|
||||||
childrenIds.forEach((childId) => {
|
if (!el) {
|
||||||
renderChildren(childId, root, cid);
|
renderEl = this.root.querySelector(`[data-cid="${entryId}"]`);
|
||||||
});
|
}
|
||||||
return el;
|
if (!renderEl) {
|
||||||
|
console.warn('render element not found', entryId);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
await this.destroy(entryId);
|
||||||
|
}
|
||||||
|
const parentElement = renderEl.parentElement;
|
||||||
|
const pid = renderEl.dataset.pid;
|
||||||
|
const { renderFn } = this.getContext({ renderEl, entryId });
|
||||||
|
schedule.addTask(renderFn, 'high');
|
||||||
|
this.renderChildren(entryId, parentElement, pid, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async destroy(id?: string) {
|
async destroy(id?: string) {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
this.root.innerHTML = '';
|
this.root.innerHTML = '';
|
||||||
} else {
|
} else {
|
||||||
const elements = this.root.querySelectorAll(`[data-cid="${id}"]`);
|
const elements = this.root.querySelectorAll(`[data-cid="${id}"]`);
|
||||||
elements.forEach((element) => element.remove());
|
elements.forEach((element) => {
|
||||||
|
element.innerHTML = '';
|
||||||
|
});
|
||||||
}
|
}
|
||||||
this.event.emit('destroy', id);
|
this.event.emit('destroy', id);
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
@ -330,92 +245,23 @@ export class Container {
|
|||||||
}, 200);
|
}, 200);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* 只能用一次
|
|
||||||
* @param entryId
|
|
||||||
* @param data
|
|
||||||
* @param opts
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
async render(entryId: string, data?: RenderData[], opts: { reload?: boolean } = {}) {
|
|
||||||
if (this.entryId && this.entryId !== entryId) {
|
|
||||||
await this.destroy();
|
|
||||||
} else if (this.entryId) {
|
|
||||||
this.destroy(entryId);
|
|
||||||
}
|
|
||||||
const _data = data || this.data;
|
|
||||||
this.entryId = entryId;
|
|
||||||
if (opts?.reload) {
|
|
||||||
this.loading = true;
|
|
||||||
await this.loadData.apply(this, [_data]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.loading || !this.loaded) {
|
|
||||||
this.event.once('loadedData', () => {
|
|
||||||
this.renderChildren(entryId);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
this.renderChildren(entryId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async hotReload(id: string) {
|
|
||||||
await this.destroy(id);
|
|
||||||
// const node = document.querySelector(`[data-cid="${id}"]`);
|
|
||||||
if (this.loading) {
|
|
||||||
this.event.once('loadedData', () => {
|
|
||||||
this.renderChildren(id);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
this.renderChildren(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async reRender() {
|
|
||||||
await this.destroy();
|
|
||||||
this.renderChildren(this.entryId);
|
|
||||||
}
|
|
||||||
async renderId(id: string) {
|
|
||||||
if (!this.entryId) {
|
|
||||||
this.render(id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (id === this.entryId) {
|
|
||||||
this.reRender();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const node = this.data.find((item) => item.id === id);
|
|
||||||
if (node?.parents && node.parents.length > 0) {
|
|
||||||
const parent = node.parents[node.parents.length - 1];
|
|
||||||
const parentElement = this.root.querySelector(`[data-cid="${parent}"]`);
|
|
||||||
await this.destroy(id);
|
|
||||||
this.renderChildren(id, parentElement, parent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
close() {
|
close() {
|
||||||
this.destroy();
|
this.destroy();
|
||||||
const event = this.event;
|
const event = this.event;
|
||||||
event && event.removeAllListeners();
|
event && event.removeAllListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ContainerOne extends Container {
|
export class ContainerOne extends Container {
|
||||||
constructor(opts: ContainerOpts) {
|
constructor(opts: ContainerOpts) {
|
||||||
super(opts);
|
super(opts);
|
||||||
}
|
}
|
||||||
async renderOne({ code: RenderCode }) {
|
async renderOne({ code: RenderCode }) {
|
||||||
const newData = { codeId: 'test-reneder-one-code-id', id: 'test-render-one', code: RenderCode };
|
const newData = { codeId: 'test-reneder-one-code-id', id: 'test-render-one', code: RenderCode };
|
||||||
console.log('loading', this.loading);
|
this.data = [newData];
|
||||||
if (this.loading) {
|
setTimeout(() => {
|
||||||
return;
|
this.renderRoot('test-render-one');
|
||||||
}
|
}, 1000);
|
||||||
console.log('renderOne', newData);
|
|
||||||
if (this.data.length === 0) {
|
|
||||||
await this.loadData([newData]);
|
|
||||||
} else {
|
|
||||||
await this.updateData([newData]);
|
|
||||||
}
|
|
||||||
this.hotReload('test-render-one');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export const mount = ({ render, unmount }, root: string | HTMLDivElement) => {
|
export const mount = ({ render, unmount }, root: string | HTMLDivElement) => {
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
export * from './container-edit';
|
export * from './container-edit';
|
||||||
|
|
||||||
export * from './container';
|
|
||||||
|
|
||||||
export * from './render';
|
export * from './render';
|
||||||
|
|
||||||
export * from './event/unload';
|
export * from './event/unload';
|
||||||
@ -9,3 +7,5 @@ export * from './event/unload';
|
|||||||
export * from './event/emitter';
|
export * from './event/emitter';
|
||||||
|
|
||||||
export * from './event/continer';
|
export * from './event/continer';
|
||||||
|
|
||||||
|
export * from './code-url';
|
85
src/handle/adapter.ts
Normal file
85
src/handle/adapter.ts
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import type { CodeUrlManager } from '../code-url';
|
||||||
|
type Fn = (args: any) => any;
|
||||||
|
export type RenderCode = {
|
||||||
|
render: Fn;
|
||||||
|
unmount?: Fn;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AdapterData = {
|
||||||
|
codeId?: string;
|
||||||
|
code?: string;
|
||||||
|
hash?: string;
|
||||||
|
codeUrl?: string;
|
||||||
|
};
|
||||||
|
type CodeAdapterOpts<T = any> = {
|
||||||
|
handle?: any;
|
||||||
|
} & T;
|
||||||
|
export class CodeAdapter {
|
||||||
|
handle: (data: AdapterData) => Promise<any>;
|
||||||
|
constructor(opts: CodeAdapterOpts) {
|
||||||
|
this.handle = opts.handle;
|
||||||
|
}
|
||||||
|
setHandle(handle: (data: AdapterData) => Promise<any>) {
|
||||||
|
this.handle = handle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type Opts = {
|
||||||
|
codeUrlManager: CodeUrlManager;
|
||||||
|
};
|
||||||
|
export class CodeUrlAdapter extends CodeAdapter {
|
||||||
|
codeUrlManager: CodeUrlManager;
|
||||||
|
constructor(opts: CodeAdapterOpts<Opts>) {
|
||||||
|
super(opts);
|
||||||
|
this.handle = (data) => handleOneCode(data, this.codeUrlManager);
|
||||||
|
this.codeUrlManager = opts.codeUrlManager;
|
||||||
|
}
|
||||||
|
setCodeUrlManager(codeUrlManager: CodeUrlManager) {
|
||||||
|
this.codeUrlManager = codeUrlManager;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export const handleOneCode = async (data: AdapterData, codeUrlManager: CodeUrlManager) => {
|
||||||
|
const { code, codeId } = data;
|
||||||
|
const urlsBegin = ['http', 'https', 'blob'];
|
||||||
|
|
||||||
|
let importUrl = '';
|
||||||
|
// codeUrl优先级最高
|
||||||
|
if (data?.codeUrl) {
|
||||||
|
importUrl = data.codeUrl;
|
||||||
|
} else if (typeof code === 'string' && code && urlsBegin.find((item) => code.startsWith(item))) {
|
||||||
|
importUrl = code;
|
||||||
|
}
|
||||||
|
if (importUrl) {
|
||||||
|
const importContent = await import(/* @vite-ignore */ importUrl);
|
||||||
|
const { render, unmount, ...rest } = importContent || {};
|
||||||
|
const _data = {
|
||||||
|
...data,
|
||||||
|
code: {
|
||||||
|
render,
|
||||||
|
unmount,
|
||||||
|
...rest,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return _data;
|
||||||
|
}
|
||||||
|
if (typeof code === 'string' && codeId) {
|
||||||
|
const { url, hash } = codeUrlManager.addCode(code, codeId, data.hash);
|
||||||
|
const importContent = await import(/* @vite-ignore */ url);
|
||||||
|
const { render, unmount, ...rest } = importContent || {};
|
||||||
|
const _data = {
|
||||||
|
...data,
|
||||||
|
codeId,
|
||||||
|
hash,
|
||||||
|
code: {
|
||||||
|
render,
|
||||||
|
unmount,
|
||||||
|
...rest,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return _data;
|
||||||
|
} else if (typeof code === 'string' && !codeId) {
|
||||||
|
console.error('codeId is required', data);
|
||||||
|
}
|
||||||
|
// 无需处理
|
||||||
|
return data;
|
||||||
|
};
|
@ -7,3 +7,5 @@ export * from './event/unload';
|
|||||||
export * from './event/emitter';
|
export * from './event/emitter';
|
||||||
|
|
||||||
export * from './event/continer';
|
export * from './event/continer';
|
||||||
|
|
||||||
|
export * from './code-url';
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import EventEmitter from 'eventemitter3';
|
import EventEmitter from 'eventemitter3';
|
||||||
import type { Container } from './container';
|
import type { Container } from './container';
|
||||||
import type { ContainerEdit } from './container-edit';
|
// import type { ContainerEdit } from './container-edit';
|
||||||
import { Emotion } from '@emotion/css/create-instance';
|
import { Emotion } from '@emotion/css/create-instance';
|
||||||
|
|
||||||
export type RenderContext<T extends { [key: string]: any } = {}> = {
|
export type RenderContext<T extends { [key: string]: any } = {}> = {
|
||||||
root?: HTMLDivElement;
|
root?: HTMLDivElement;
|
||||||
shadowRoot?: ShadowRoot;
|
shadowRoot?: ShadowRoot;
|
||||||
container?: Container | ContainerEdit;
|
// container?: Container | ContainerEdit;
|
||||||
|
container?: Container;
|
||||||
event?: EventEmitter;
|
event?: EventEmitter;
|
||||||
code?: {
|
code?: {
|
||||||
render: Render;
|
render: Render;
|
||||||
|
1
src/scheduler/index.ts
Normal file
1
src/scheduler/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './scheduler';
|
55
src/scheduler/scheduler.ts
Normal file
55
src/scheduler/scheduler.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
// 定义任务队列
|
||||||
|
const highPriorityTasks = [];
|
||||||
|
const lowPriorityTasks = [];
|
||||||
|
let isSchedulerRunning = false; // 调度器状态
|
||||||
|
|
||||||
|
export const fnvoid = () => {};
|
||||||
|
export type Priority = 'high' | 'low';
|
||||||
|
// 添加任务的辅助函数
|
||||||
|
export const addTask = (callback: any, priority: Priority = 'low') => {
|
||||||
|
if (priority === 'high') {
|
||||||
|
highPriorityTasks.push(callback);
|
||||||
|
} else {
|
||||||
|
lowPriorityTasks.push(callback);
|
||||||
|
}
|
||||||
|
// 如果调度器未运行且有任务,启动调度
|
||||||
|
if (!isSchedulerRunning) {
|
||||||
|
isSchedulerRunning = true;
|
||||||
|
requestIdleCallback(processTasks);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 调度函数
|
||||||
|
const processTasks: IdleRequestCallback = (deadline) => {
|
||||||
|
// 优先处理高优先级任务
|
||||||
|
while (highPriorityTasks.length && deadline.timeRemaining() > 0) {
|
||||||
|
const task = highPriorityTasks.shift();
|
||||||
|
task();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有剩余时间,再处理低优先级任务
|
||||||
|
while (lowPriorityTasks.length && deadline.timeRemaining() > 0) {
|
||||||
|
const task = lowPriorityTasks.shift();
|
||||||
|
task();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否还有任务
|
||||||
|
if (highPriorityTasks.length || lowPriorityTasks.length) {
|
||||||
|
requestIdleCallback(processTasks);
|
||||||
|
} else {
|
||||||
|
// 没有任务,停止调度器
|
||||||
|
isSchedulerRunning = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// 开始调度
|
||||||
|
requestIdleCallback(processTasks);
|
||||||
|
|
||||||
|
// 添加一些测试任务
|
||||||
|
// addTask(() => console.log('Low priority task 1'), 'low');
|
||||||
|
// addTask(() => console.log('High priority task 1'), 'high');
|
||||||
|
// addTask(() => console.log('Low priority task 2'), 'low');
|
||||||
|
// addTask(() => console.log('High priority task 2'), 'high');
|
||||||
|
|
||||||
|
export const getLoading = () => {
|
||||||
|
return isSchedulerRunning;
|
||||||
|
};
|
@ -1,4 +1,4 @@
|
|||||||
import { handleCode, RenderData } from '../container';
|
import { handleCode, RenderData } from '../container-old';
|
||||||
import { createStore } from 'zustand/vanilla';
|
import { createStore } from 'zustand/vanilla';
|
||||||
export type ContainerStore = {
|
export type ContainerStore = {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { RenderData } from '../container';
|
import { RenderData } from '../container-old';
|
||||||
|
|
||||||
export const getTree = (data: RenderData[], id: string) => {
|
export const getTree = (data: RenderData[], id: string) => {
|
||||||
const node = data.find((node) => node.id === id);
|
const node = data.find((node) => node.id === id);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user