feat: 更新container版本,修改新的引入方式

This commit is contained in:
xion 2024-11-16 19:16:10 +08:00
parent 6c8fd040da
commit 7327a134b1
17 changed files with 802 additions and 335 deletions

2
.gitignore vendored
View File

@ -4,3 +4,5 @@ build
.cache
.DS_Store
*.log
.turbo

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}

View File

@ -1,6 +1,6 @@
{
"name": "@kevisual/container",
"version": "0.0.3",
"version": "1.0.0",
"description": "",
"main": "dist/container.js",
"publishConfig": {
@ -37,6 +37,7 @@
"devDependencies": {
"@rollup/plugin-commonjs": "^28.0.0",
"@rollup/plugin-node-resolve": "^15.3.0",
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^12.1.0",
"@types/crypto-js": "^4.2.2",
"@types/react": "^18.3.4",
@ -53,6 +54,7 @@
"dependencies": {
"nanoid": "^5.0.7",
"rollup-plugin-dts": "^6.1.1",
"scheduler": "^0.23.2",
"zustand": "^4.5.5"
}
}

View File

@ -5,6 +5,7 @@ import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import copy from 'rollup-plugin-copy';
import { dts } from 'rollup-plugin-dts';
import terser from '@rollup/plugin-terser';
/**
* @type {import('rollup').RollupOptions}
@ -18,6 +19,15 @@ const config1 = {
plugins: [
resolve(), // 使用 @rollup/plugin-node-resolve 解析 node_modules 中的模块
commonjs(), //
terser({
format: {
comments: false, // 移除注释
},
compress: {
drop_console: true, // 移除 console.log
drop_debugger: true, // 移除 debugger
},
}),
typescript({
allowImportingTsExtensions: true,
noEmit: true,
@ -50,9 +60,9 @@ const config2 = {
declaration: false,
}), // 使用 @rollup/plugin-typescript 处理 TypeScript 文件
// 复制/src/container.css 到dist/container.css
copy({
targets: [{ src: 'src/container.css', dest: 'dist' }],
}),
// copy({
// targets: [{ src: 'src/container.css', dest: 'dist' }],
// }),
],
};
const config2Dts = {

78
src/code-url.ts Normal file
View 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();

View File

@ -1,8 +1,10 @@
import { Container, ContainerOpts } from './container';
import { Container, ContainerOpts, createEmotion } from './container';
import { addListener } from './listener/dom';
export type ContainerEditOpts = {
edit?: boolean;
mask?: boolean;
initCss?: string;
} & ContainerOpts;
let isListener = false;
export class ContainerEdit extends Container {
@ -28,9 +30,12 @@ export class ContainerEdit extends Container {
addListener(this.root);
}
}
if (opts.initCss && this.root) {
this.root.classList.add(kvContainerStyle);
}
renderChildren(cid: string, parentElement?: any, pid?: any) {
const el = super.renderChildren(cid, parentElement, pid);
}
renderChildren(cid: string, parentElement?: any, pid?: any, isNew = true) {
const el = super.renderChildren(cid, parentElement, pid, isNew);
if (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
View 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 });
};

View File

@ -1,5 +1,5 @@
.kv-container.active {
background: #000 !important;
background: #000;
border: 2px solid #195ca9;
z-index: 100;
}
@ -11,10 +11,11 @@
.kv-container.hover {
cursor: move;
}
.kv-container > .resizer, .kv-container > .drag-title {
.kv-container > .resizer,
.kv-container > .drag-title {
display: none;
}
.kv-container.active > .resizer, .kv-container.active > .drag-title {
.kv-container.active > .resizer,
.kv-container.active > .drag-title {
display: block;
}

View File

@ -1,130 +1,12 @@
import createEmotion from '@emotion/css/create-instance';
import EventEmitter from 'eventemitter3';
import { MD5 } from 'crypto-js';
import { ContainerEvent } from './event/continer';
import { RenderCode } from './render';
import * as schedule from './scheduler';
import { CodeUrlAdapter } from './handle/adapter';
import { codeUrlManager } from './code-url';
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();
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 { createEmotion };
export type RenderData = {
id: string; // id不会重复
children?: string[];
@ -141,31 +23,27 @@ export type RenderData = {
export type ContainerOpts = {
root?: HTMLDivElement | string;
shadowRoot?: ShadowRoot;
destroy?: () => void;
data?: RenderData[];
showChild?: boolean;
adapter?: any;
};
export class Container {
data: RenderData[];
root: HTMLDivElement;
globalCss: any;
showChild: boolean;
event: EventEmitter;
entryId: string = '';
loading: boolean = false;
loaded: boolean = false;
adapter: CodeUrlAdapter;
constructor(opts: ContainerOpts) {
const data = opts.data || [];
this.loadData.apply(this, [data]);
this.data = 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;
this.adapter = opts.adapter || this.createAdapter();
if (listening !== 'true') {
this.root.addEventListener('onContainer', (e: ContainerEvent) => {
const { type, data } = e.data;
@ -173,68 +51,31 @@ export class Container {
});
}
}
async loadData(data: RenderData[], key: string = '') {
if (data.length === 0) {
this.data = data;
this.loaded = true;
return;
protected createAdapter() {
const adapter = new CodeUrlAdapter({ codeUrlManager });
this.adapter = adapter;
return adapter;
}
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 {
getData({ id }: { id: string }) {
const data = this.data;
const node = data.find((node) => node.id === id);
return node;
}
updateData(data: RenderData[]) {
this.data = this.data.map((node) => {
const find = data.find((item) => item.id === node.id);
return find || node;
});
}
/**
*
* @param cid
* @param parentElement
* @param pid
* @returns
*/
renderChildren(cid: string, parentElement?: any, pid?: any, isNew = true): void | HTMLDivElement {
const data = this.data;
console.log('renderChildren', cid, data);
const globalCss = this.globalCss;
if (!parentElement) {
parentElement = this.root;
@ -243,20 +84,64 @@ export class Container {
}
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);
let el;
if (isNew) {
el = document.createElement('div');
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;
// 代码渲染
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 event = this.event;
const globalCss = this.globalCss;
const root = renderEl;
const adapter = this.adapter;
const containerThis = this;
const shadowRoot = node.shadowRoot ? renderEl.attachShadow({ mode: 'open' }) : null;
const { css, sheet, cache } = createEmotion({ key: 'css' });
if (shadowRoot) {
cache.insert = (selector: string, serialized: any, sheet: any, shouldCache: boolean) => {
const style = document.createElement('style');
@ -264,19 +149,27 @@ export class Container {
shadowRoot.appendChild(style);
};
}
if (el.style) {
el.className = !shadowRoot ? globalCss(style as any) : css(style as any);
const cid = entryId;
renderEl.className = '';
if (renderEl.style) {
renderEl.className = !shadowRoot ? globalCss(style as any) : css(style as any);
}
el.classList.add(cid, 'kv-container');
const { render, unmount } = (code as RenderCode) || {};
renderEl.classList.add(cid, 'kv-container');
if (shadowRoot) {
const slot = document.createElement('slot');
shadowRoot.appendChild(slot);
}
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: this,
code: code as RenderCode,
container: containerThis,
code: newCode as RenderCode,
data: node,
css: shadowRoot ? css : globalCss,
};
@ -285,7 +178,6 @@ export class Container {
} else {
// no render, so no insert
}
if (event) {
const destroy = (id: string) => {
if (id) {
@ -303,25 +195,48 @@ export class Container {
};
event.on('destroy', destroy);
}
if (shadowRoot) {
const slot = document.createElement('slot');
shadowRoot.appendChild(slot);
};
return { renderFn, shadowRoot };
}
if (!this.showChild) {
// 更新渲染
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;
}
const childrenIds = node.children || [];
childrenIds.forEach((childId) => {
renderChildren(childId, root, cid);
});
return el;
let renderEl = el;
if (!el) {
renderEl = this.root.querySelector(`[data-cid="${entryId}"]`);
}
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) {
if (!id) {
this.root.innerHTML = '';
} else {
const elements = this.root.querySelectorAll(`[data-cid="${id}"]`);
elements.forEach((element) => element.remove());
elements.forEach((element) => {
element.innerHTML = '';
});
}
this.event.emit('destroy', id);
await new Promise((resolve) => {
@ -330,92 +245,23 @@ export class Container {
}, 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');
this.data = [newData];
setTimeout(() => {
this.renderRoot('test-render-one');
}, 1000);
}
}
export const mount = ({ render, unmount }, root: string | HTMLDivElement) => {

View File

@ -1,7 +1,5 @@
export * from './container-edit';
export * from './container';
export * from './render';
export * from './event/unload';
@ -9,3 +7,5 @@ export * from './event/unload';
export * from './event/emitter';
export * from './event/continer';
export * from './code-url';

85
src/handle/adapter.ts Normal file
View 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;
};

View File

@ -7,3 +7,5 @@ export * from './event/unload';
export * from './event/emitter';
export * from './event/continer';
export * from './code-url';

View File

@ -1,12 +1,13 @@
import EventEmitter from 'eventemitter3';
import type { Container } from './container';
import type { ContainerEdit } from './container-edit';
// import type { ContainerEdit } from './container-edit';
import { Emotion } from '@emotion/css/create-instance';
export type RenderContext<T extends { [key: string]: any } = {}> = {
root?: HTMLDivElement;
shadowRoot?: ShadowRoot;
container?: Container | ContainerEdit;
// container?: Container | ContainerEdit;
container?: Container;
event?: EventEmitter;
code?: {
render: Render;

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

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

View 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;
};

View File

@ -1,4 +1,4 @@
import { handleCode, RenderData } from '../container';
import { handleCode, RenderData } from '../container-old';
import { createStore } from 'zustand/vanilla';
export type ContainerStore = {
loading: boolean;

View File

@ -1,4 +1,4 @@
import { RenderData } from '../container';
import { RenderData } from '../container-old';
export const getTree = (data: RenderData[], id: string) => {
const node = data.find((node) => node.id === id);