fix: refactor codes

This commit is contained in:
wwsun
2023-08-28 16:18:29 +08:00
parent aa8472ac82
commit 9392231414
27 changed files with 91 additions and 1391 deletions
+1 -1
View File
@@ -63,7 +63,7 @@ const sandboxQuery = new DndQuery({
Initialize the designer layout (WIP)
Find details from [Documentation Site](./#)。
Find details from [Documentation Site](https://netease.github.io/tango/)。
## 💻 Development
+3 -3
View File
@@ -63,7 +63,7 @@ const sandboxQuery = new DndQuery({
设计器框架初始化(WIP)
详细使用方法请参考 [使用文档](./#)。
详细使用方法请参考 [使用文档](https://netease.github.io/tango/)。
## 💻 开发
@@ -100,6 +100,6 @@ yarn start
## 💗 致谢
感谢网易云音乐大前端团队,公共技术团队,直播技术团队,以及所有参与过 Tango 项目的同学们。
感谢网易云音乐公共技术团队,大前端团队,直播技术团队,以及所有参与过 Tango 项目的同学们。
感谢 CodeSandbox 提供的 [Sandpack](https://sandpack.codesandbox.io/) 项目,为 Tango 提供了强大的在线代码执行能力。
感谢 CodeSandbox 提供的 [Sandpack](https://sandpack.codesandbox.io/) 项目,为 Tango 提供了强大的基于浏览器的代码构建与执行能力。
+1 -1
View File
@@ -14,7 +14,7 @@
},
"dependencies": {
"@ant-design/icons": "^4.8.0",
"@music163/antd": "^0.1.1",
"@music163/antd": "^0.1.2",
"@music163/tango-designer": "*",
"@music163/tango-helpers": "*",
"antd": "^4.24.2",
+3 -3
View File
@@ -44,11 +44,11 @@ const tangoJson = {
resources: ['https://unpkg.com/moment@{{version}}/moment.js'],
},
'@music163/tango-boot': {
version: '0.1.2',
version: '0.1.3',
library: 'TangoBoot',
type: 'baseDependency',
// resources: ['https://unpkg.com/@music163/tango-boot@{{version}}/dist/boot.js'],
resources: ['http://localhost:9001/boot.js'],
resources: ['https://unpkg.com/@music163/tango-boot@{{version}}/dist/boot.js'],
// resources: ['http://localhost:9001/boot.js'],
description: '云音乐低代码运行时框架',
},
'@music163/antd': {
+10 -11
View File
@@ -1,6 +1,5 @@
import React from 'react';
import {
createEngine,
Designer,
DesignerPanel,
SidebarPanel,
@@ -13,26 +12,26 @@ import {
DndQuery,
ComponentsView,
} from '@music163/tango-designer';
import { createEngine, Workspace } from '@music163/tango-core';
import { prototypes, menuData } from '@music163/antd/lib/esm/designer';
import { Logo, ProjectDetail } from './share';
import { sampleFiles } from '../mock/project';
import './index.less';
/**
* 1. 如果要支持代码格式化,需要提前在 html 模板中引入
* <script src="https://unpkg.com/prettier@2.6.0/standalone.js"></script>
* <scrip src="https://unpkg.com/prettier@2.6.0/parser-graphql.js"></script>
*/
console.log(prototypes, menuData);
// 1. 实例化工作区
const workspace = new Workspace({
entry: '/src/index.js',
files: sampleFiles,
prototypes: prototypes as any,
});
// 2. 引擎初始化
const engine = createEngine({
entry: '/src/index.js',
files: sampleFiles,
componentPrototypes: prototypes,
workspace,
});
const workspace = engine.workspace;
// @ts-ignore
window.__workspace__ = workspace;
@@ -11,10 +11,7 @@ import TypeTable from '@site/src/components/TypeTable';
## 设计器初始化
```jsx
<Designer
engine={engine}
sandboxQuery={sandboxQuery}
remoteServices={remoteServices}></Designer>
<Designer engine={engine} sandboxQuery={sandboxQuery} remoteServices={remoteServices}></Designer>
```
## engine
@@ -31,22 +28,6 @@ const engine = createEngine({
});
```
### 使用内置的 workspace 实
:::warning
只适用于配合 LegacySandbox 使用。
:::
```js
// 引擎初始化
const engine = createEngine({
entry: '/src/index.js',
// files: sampleFilesForLegacy,
componentPrototypes: prototypes as any,
useBuiltinWorkspace: true,
});
```
### 自定义 workspace 的初始化方式
默认情况下,引擎采用的是源码解析模式,即将源码解析为 ast 树,后续的搭建逻辑转为对 ast 树的操作。如果你想自定义搭建逻辑,可以通过自定义 workspace 的方式来实现。
+52 -196
View File
@@ -2,209 +2,65 @@ import Link from '@docusaurus/Link';
# 快速开始
tango 是一个用于快速构建低代码平台的 JavaScript 框架。借助 tango 只需要少量代码就可以快速的搭建一个基本的低代码设计器。本文将会帮助你借助 tango-apps 实现一个基本的低代码可视化设计器。
Tango 是一个用于快速构建低代码工具的前端框架。借助 Tango 只需要少量代码就可以快速的搭建一个基本的低代码设计器。本文将会帮助你借助 Tango 实现一个基本的低代码可视化设计器。
## 环境设置
## 环境要求
- 安装 node 10.13 以上版本
- 安装 npm 或 yarn
### 初始化
## 启动 Tango Playground
推荐使用 [umi 来初始化项目](https://v3.umijs.org/zh-CN/docs/getting-started)。
1. 克隆仓库至本地
```sh
git clone https://github.com/NetEase/tango.git
```
2. 在 hosts 文件下添加本地开发的域名
```hosts
127.0.0.1 local.netease.com
```
3. 安装依赖并启动 Tango Playground
```sh
yarn install
yarn start
```
4. 打开浏览器访问 <https://local.netease.com:6006> 并允许不安全的证书
:::tip
本文使用的是 umi3,如果使用 umi4 可以参考官方文档进行升级改造。
:::
## 启动本地沙箱
创建空目录
本地开发除了需要启动 Tango 的 playground 外,还需要一个可用的沙箱环境来承载设计器。
```bash
mkdir myapp && cd myapp
如果没有可用的在线沙箱,可以通过本地启动一个沙箱来完成开发;如果已有一个在线沙箱,且沙箱与本地的 playground 共用一级域名,请直接跳至第 7 步修改 `bundlerURL` 为在线沙箱的地址。
1. 克隆仓库至本地
```sh
git clone https://github.com/NetEase/codesandbox-client.git
```
2. 修改 `Caddyfile`,将 `:8080` 修改为 playground 的域名,例如 `local.netease.com`
3. 将上述域名添加至 hosts 文件
```hosts
127.0.0.1 local.netease.com
```
4. 安装依赖并构建沙箱产物
```sh
yarn install
yarn build:deps
yarn build:sandpack
```
5. 使用 Caddy 托管构建沙箱后的静态文件,若本地没有 Caddy 可前往 <https://caddyserver.com/download> 下载
```sh
caddy run
```
6. 打开浏览器访问 <https://local.netease.com:8443> 并允许不安全的证书
7. 修改 `apps/playground/src/pages/index.tsx`,找到 `<Sandbox />` 组件,将 `bundlerURL` 修改为 `https://local.netease.com:8443`
```jsx
<Sandbox bundlerURL="https://local.netease.com:8443" />
```
8. 回到 playground 页面并刷新,现在 playground 将使用你指定的沙箱来运行设计器
若你本地有 Docker 也可将上述的第 4-5 步改为使用 Docker 构建镜像并启动:
```sh
docker build -t tango-codesandbox .
docker run -p 8443:8443 tango-codesandbox
```
通过 umi 工具初始化工程
```bash
$ yarn create @umijs/umi-app
# 或 npx @umijs/create-umi-app
```
安装依赖
```bash
yarn
```
启动项目
```bash
yarn start
```
### Host 配置
Host 配置如下:
```text
127.0.0.1 local.netease.com
```
在浏览器中打开 <https://local.netease.com:8000>
:::tip
内部渲染沙箱默认走 https ,所以外层应用也需要为 https 应用。为避免跨域,需要配置 netease.com 域名。可以参考 [mkcert](https://github.com/FiloSottile/mkcert) 文档进行 https 证书签发。
:::
## 安装依赖
### 使用 npm 包
```bash
# 设计器主框架
yarn add @music163/tango-designer
# UI 组件包
yarn add @music163/antd
```
### 浏览器引入
在浏览器中使用 script 和 link 标签直接引入文件,并使用全局变量 `TangoDesigner`。
:::info
tango-designer 构建包依赖 react, react-dom, styled-components, antd 等,请确保提前引入这些文件。
:::
例如
```html
<script src="https://unpkg.com/react@16.14.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.14.0/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/react-is@16.13.1/umd/react-is.production.min.js"></script>
<script src="https://unpkg.com/moment/min/moment-with-locales.js"></script>
<script src="https://unpkg.com/styled-components@5.3.11/dist/styled-components.js"></script>
<script src="https://unpkg.com/antd@4.24.13/dist/antd-with-locales.min.js"></script>
```
## 引入 antd 样式
设计器的部分组件使用 antd 组件,因此需要引入 antd 对应的样式文件,也可以使用 CDN 引入。
```less
// src/pages/index.less
@import '~antd/dist/antd.less';
```
## 引擎内核的初始化
```jsx
// src/pages/index.tsx
import { createEngine } form '@music163/tango-designer';
// 引擎初始化
const engine = createEngine({
entry: '/src/index.js',
files: sampleFiles,
componentPrototypes: prototypes as any,
});
```
## 拖拽引擎的初始化
```jsx
// src/pages/index.tsx
import { DndQuery } form '@music163/tango-designer';
const sandboxQuery = new DndQuery({
context: 'iframe',
});
```
## 设计器框架的初始化
```jsx
// 平台初始化
export default function App() {
useEffect(() => {
engine.workspace.selectSource.clear();
}, []);
return (
<Designer engine={engine} sandboxQuery={sandboxQuery}>
<DesignerPanel logo="Tango">
<SidebarPanel>
<SidebarPanel.Item key="components">
<ComponentsView
menuData={
{
common: [
{
title: '基本',
items: [
'Button',
'ButtonGroup',
'ActionList',
'Action',
'Image',
'Text',
'Icon',
],
},
],
} as any
}
/>
</SidebarPanel.Item>
<SidebarPanel.Item key="outline" />
<SidebarPanel.Item key="model" isFloat width="40vw" />
<SidebarPanel.Item key="dataSource" isFloat width="40vw" />
<SidebarPanel.Item key="history" />
</SidebarPanel>
<WorkspacePanel>
<ToolbarPanel>
<ToolbarPanel.Item key="history" />
<ToolbarPanel.Item key="viewportRefresh" />
<ToolbarPanel.Item key="preview" />
<ToolbarPanel.Item key="routeSwitch" />
<ToolbarPanel.Item key="modeSwitch" placement="center" />
<ToolbarPanel.Item key="viewportSwitch" placement="right" />
</ToolbarPanel>
<ViewPanel mode="design">
<Sandbox
bundlerURL={`${window.location.protocol}//codesandbox.fn.${
window.location.hostname.endsWith('netease.com')
? 'netease'
: 'igame.163'
}.com/`}
/>
</ViewPanel>
<ViewPanel mode="code">
<CodeEditor />
</ViewPanel>
</WorkspacePanel>
<SettingPanel />
</DesignerPanel>
</Designer>
);
}
```
## 代码格式化
如果需要自动格式化生成的代码,则需要在浏览器中提前引入 standalone prettier,可以使用如下标签:
```html
<script src="https://unpkg.com/prettier@2.6.0/standalone.js"></script>
<script src="https://unpkg.com/prettier@2.6.0/parser-babel.js"></script>
```
## 运行结果
运行结果如下:
![img](https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/14410831217/8cdc/ec54/b137/71877a803f62a37496954a02e00af615.png)
## Playground 示例
实际的代码,可以参考 <https://github.com/NetEase/tango/tree/main/apps/playground> 源码。
+2
View File
@@ -33,11 +33,13 @@ const sidebars = {
'designer/modules/sandbox',
'designer/modules/hooks',
],
collapsible: false,
},
{
type: 'category',
label: '扩展',
items: ['designer/extend/overview', 'designer/extend/remote-service'],
collapsible: false,
},
'designer/setters',
],
+7 -57
View File
@@ -1,40 +1,16 @@
import { ComponentPrototypeType } from '@music163/tango-helpers';
import { Designer, Engine, SimulatorNameType, Workspace } from './models';
import { FileItemType } from './types';
import { Designer, Engine, SimulatorNameType } from './models';
import { IWorkspace } from './models/interfaces';
/**
* 这里提前实例化 workspace,先保证全局唯一
* TIP: builtinWorkspace 暂时只是给老版的 sandbox 消费,其他场景不需要
*/
export const builtinWorkspace = new Workspace();
type CreateEngineOptionsType = {
interface ICreateEngineOptions {
/**
* 自定义工作区
*/
workspace?: IWorkspace;
/**
* 是否使用老版全局 workspace,目前仅 LegacySandbox 需要开启
*/
useBuiltinWorkspace?: boolean;
/**
* 文件入口
*/
entry?: string;
/**
* 初始化的文件列表
*/
files?: FileItemType[];
/**
* 组件的原型信息
*/
componentPrototypes?: Record<string, ComponentPrototypeType>;
/**
* 默认的模拟器模式
*/
defaultSimulatorMode?: SimulatorNameType;
};
}
/**
* Designer 实例化工厂函数
@@ -42,37 +18,11 @@ type CreateEngineOptionsType = {
* @returns
*/
export function createEngine({
workspace: workspaceProp,
useBuiltinWorkspace,
defaultSimulatorMode,
files,
entry,
componentPrototypes,
}: CreateEngineOptionsType) {
let workspace: IWorkspace = workspaceProp;
if (!workspace) {
// 如果用户没有提供,则使用内置的初始化策略
if (useBuiltinWorkspace) {
workspace = builtinWorkspace;
workspace.entry = entry;
if (files && files.length) {
workspace.addFiles(files);
}
if (componentPrototypes) {
workspace.setComponentPrototypes(componentPrototypes);
}
} else {
workspace = new Workspace({
entry,
files,
prototypes: componentPrototypes,
});
}
}
workspace,
defaultSimulatorMode = 'desktop',
}: ICreateEngineOptions) {
const engine = new Engine({
workspace: workspaceProp || (workspace as any),
workspace,
designer: new Designer({
workspace,
simulator: defaultSimulatorMode,
-1
View File
@@ -6,6 +6,5 @@ export * from './context';
export { INTERNAL_SELECTION_TOOLS } from './framework/simulator/selection';
export { createEngine, TangoServiceModule } from '@music163/tango-core';
export { useWorkspace, useDesigner, observer } from '@music163/tango-context';
export { register as registerSetter } from '@music163/tango-setting-form';
@@ -88,7 +88,6 @@ export const ComponentsView = observer(
menuData = emptyMenuData,
showBizComps = true,
showBlocks = true,
onBizDependencyAdd,
getBizCompName = upperCamelCase,
loading = false,
}: ComponentsViewProps) => {
-3
View File
@@ -1,5 +1,2 @@
export * from './legacy';
export * from './json-sandbox';
export * from './sandbox';
export * from './code-sandbox';
export type { CodeSandboxProps } from './types';
@@ -1 +0,0 @@
export * from './sandbox';
@@ -1,143 +0,0 @@
import React, { useRef } from 'react';
export interface JsonSandboxProps {
/**
* 文件列表
*/
files?: Record<string, string>;
/**
* 事件监听器
*/
eventHandlers?: Record<string, Function>;
/**
* 组件包的 umd 全局变量名
*/
componentsPackageGlobalName?: string;
/**
* Json 渲染器的地址
*/
jsonRendererUrl: string;
}
export function JsonSandbox({
files,
eventHandlers,
componentsPackageGlobalName,
jsonRendererUrl,
}: JsonSandboxProps) {
const rendererRef = useRef<any>();
const schemaData = JSON.parse(files['/src/pages/index.schema.json']);
const tangoConfig = JSON.parse(files['/tango.config.json']);
const mountContent = async (frame: HTMLIFrameElement) => {
if (!frame) {
return;
}
if (!rendererRef.current) {
const renderer = await buildHtml(frame, {
schemaData,
eventHandlers,
externalResources: tangoConfig?.sandbox?.externalResources || [],
jsonRendererUrl,
});
/**
* 避免在 await 期间 rendererRef.current 发生了变化
* @see https://eslint.org/docs/latest/rules/require-atomic-updates#properties
*/
if (!rendererRef.current) {
rendererRef.current = renderer;
}
}
rendererRef.current.run('root', (window: Window) => {
return {
tree: schemaData,
components: window[componentsPackageGlobalName],
};
});
};
return (
<iframe
style={{
width: '100%',
height: '100%',
border: 0,
}}
ref={async (frame) => await mountContent(frame)}
/>
);
}
type BuildHtmlOptions = {
/**
* 渲染数据
*/
schemaData: any;
/**
* 外置的资源包
*/
externalResources?: string[];
} & Pick<JsonSandboxProps, 'eventHandlers' | 'jsonRendererUrl'>;
async function buildHtml(
iframe: HTMLIFrameElement,
{ schemaData, eventHandlers, externalResources, jsonRendererUrl }: BuildHtmlOptions,
) {
const doc = iframe.contentDocument;
const win = iframe.contentWindow;
win['schemaData'] = schemaData;
const styles: string[] = [];
const scripts: string[] = [];
externalResources.forEach((resource) => {
if (resource.endsWith('.css')) {
styles.push(resource);
} else if (resource.endsWith('.js')) {
scripts.push(resource);
}
});
const styleTags = styles.reduce((acc, style) => {
return `${acc}<link rel="stylesheet" href="${style}" />`;
}, '');
const scriptTags = scripts.reduce((acc, script) => {
return `${acc}<script src="${script}"></script>`;
}, '');
doc.open();
doc.write(`
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Json Sandbox</title>
${styleTags}
</head>
<body>
<div id="root"></div>
${scriptTags}
<script src="${jsonRendererUrl}"></script>
</body>
</html>`);
doc.close();
Object.keys(eventHandlers || {}).forEach((key) => {
doc[key.toLowerCase()] = eventHandlers[key];
});
return new Promise((resolve) => {
const renderer = win['jsonSandboxRenderer'];
if (renderer) {
return resolve(renderer);
}
const loaded = () => {
resolve(win['jsonSandboxRenderer']);
win.removeEventListener('load', loaded);
};
win.addEventListener('load', loaded);
});
}
-47
View File
@@ -1,47 +0,0 @@
import React, { useContext } from 'react';
import ReactFrame, { FrameContext } from 'react-frame-component';
import { StyleSheetManager } from 'styled-components';
const template = `<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Tango Sandbox</title>
<link rel="stylesheet" href="https://unpkg.com/antd/dist/antd.css" />
<script src="https://unpkg.com/react@16.14.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.14.0/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/moment/min/moment-with-locales.js"></script>
<script src="https://unpkg.com/react-is@16/umd/react-is.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone@7.15.6/babel.js"></script>
<script src="https://unpkg.com/styled-components@5.3.3/dist/styled-components.js"></script>
<script src="https://unpkg.com/antd/dist/antd-with-locales.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/netease-sea@1.0.0/dist/index.min.js"></script>
<script src="http://127.0.0.1:8081/assets/loader.js"></script>
</head>
<body>
<div id="root"></div>
</body>
</html>
`;
export function Frame({ children, ...rest }: React.ComponentPropsWithoutRef<'iframe'>) {
return (
<ReactFrame initialContent={template} mountTarget="#root" {...rest}>
<InjectFrameStyles>{children}</InjectFrameStyles>
</ReactFrame>
);
}
export function useFrame() {
const frame = useContext(FrameContext);
return frame;
}
const InjectFrameStyles = (props: any) => {
const { document } = useContext(FrameContext);
return (
<StyleSheetManager target={document.head}>
<>{props.children}</>
</StyleSheetManager>
);
};
-59
View File
@@ -1,59 +0,0 @@
/**
* @deprecated
* @param code
* @param options
* @returns
*/
export function transformCodes(code: string, options?: any) {
// @ts-ignore
const ret = Babel.transform(code, {
presets: [
[
'env',
{
modules: 'systemjs',
},
],
'react',
],
...options,
});
return {
code: ret.code,
// uri: `data:application/javascript;charset=utf-8;base64,${btoa(ret.code)}`
};
}
/**
* @deprecated
* @param url
* @returns
*/
export function ignoreNoVersonPackage(url: string) {
if (
!/(([0-9]|([1-9]([0-9]*))).){2}([0-9]|([1-9]([0-9]*)))/.exec(url) &&
url.indexOf('api/v1/readHistoryFile') === -1
) {
console.warn('找不到npm包版本号:', url);
const tpl = `define("${url}", [], {})`;
eval(tpl);
return true;
}
return false;
}
export function code2browser(code: string, options: any = {}) {
const { fileName, beautify = false } = options;
// code = beautify ? format(code) : code;
// @ts-ignore
return Babel.transform(code, {
presets: ['es2015', 'react'],
plugins: [
['proposal-decorators', { legacy: true }],
['proposal-class-properties', { loose: true }],
],
ast: false,
sourceMaps: false,
filename: fileName,
}).code;
}
-3
View File
@@ -1,3 +0,0 @@
export * from './sandbox';
// export * from './standalone';
// export * from './frame';
-143
View File
@@ -1,143 +0,0 @@
import { builtinWorkspace as workspace } from '@music163/tango-core';
import { logger, noop } from '@music163/tango-helpers';
import { code2browser } from './helpers';
// @ts-ignore
const seajs = window.seajs || {
on(event: string, callback: any) {},
};
const resolve = seajs.resolve;
seajs.resolve = (id: string, refUri: string) => {
const importMap = seajs.customImportMap || {};
if (id in importMap) {
return id;
}
// custom resolve
if (seajs.customResolve) {
const ret = seajs.customResolve(id, refUri);
if (ret) {
return ret;
}
}
// 为了解决类似 xxx/xxxx/variables 这类问题,还需判断是否 lib 目录下的
// let index: number;
// const ret = Object.keys(resolved).some((key, i) => {
// if (id.indexOf(key) === 0 && id.replace(key, '').charAt(0) === '/' && id.replace(key, '').split('/')[1] === 'lib') {
// index = i;
// return true;
// }
// return false;
// });
// if (ret) {
// return Object.keys(resolved)[index];
// }
return resolve(id, refUri);
};
const request = seajs.request;
seajs.parseFilename = (url: string): string => {
const uri = new URL(url);
const filename = uri.pathname;
return filename;
};
seajs.customFetch = (url: string) => {
return fetch(url).then((res) => res.text());
};
seajs.request = (url: string, callback: () => any = noop) => {
// url = url.replace(/\.jsx\.js$/, '.jsx');
// url = url.replace(/\.json\.js$/, '.json');
// 先忽略掉样式
if (/\.scss\.js$|\.sass\.js$|\.less\.js$|\.css\.js$/.exec(url)) return callback();
// 先忽略掉没有版本号的包,这个一般是node包
// if (ignoreNoVersonPackage(url)) return callback();
const fileName = seajs.parseFilename(url);
let ret;
// TODO: 这里混入了 loader 的逻辑中,需要一个更好的方式去判断,能否在往外放
if (workspace.getFile(fileName)) {
ret = new Promise((resolve, reject) => {
resolve(workspace.getFile(fileName).code);
});
} else {
if (url.indexOf('materiels') > -1) {
if (/\.json/.test(url)) {
ret = fetch(`${url}?${Date.now()}`)
.then((res) => {
return res.text();
})
.then((content) => {
const tpl = `seajsDefine("${url}", [], function (require, exports, module) {
module.exports = ${content}
})`;
eval(tpl);
callback();
});
} else {
request(url, callback);
}
return;
}
ret = seajs.customFetch(url);
}
ret
.then((source: string) => {
logger.group(`load from remote: ${fileName}`, source);
return source as string;
})
.then((source: string) => {
workspace.addFile(fileName, source);
return workspace.getFile(fileName).code;
})
// 正式的渲染流程
.then((source: string) => {
return code2browser(source, { fileName });
})
.then((source: string) => {
return `
seajsDefine("${url}", function (require, exports, module) {
${source}
})
`.trim();
})
.then((source: string) => {
// TODO: 渲染错误时,应该提供 Error Placeholder,或者直接做在沙箱里
try {
eval(source);
callback();
} catch (err) {
logger.error('渲染出错啦,请检查代码语法', err);
}
});
};
seajs.on('request', (data: any) => {
const importMap = seajs.customImportMap || {};
const uri = data.uri;
if (importMap[uri]) {
const def = new Function(
'uri',
'ret',
`
seajsDefine(uri, function(require, exports, module) {
module.exports = ret;
})
`,
);
def(uri, importMap[uri]);
data.onRequest();
data.requested = true;
}
});
-42
View File
@@ -1,42 +0,0 @@
import './loader';
export interface RenderOptions {
/**
* 应用的入口文件地址
* @example /src/index.js
*/
entry?: string;
/**
* 需要更新的目标文件路径
*/
filenames?: any;
/**
* 模块调用后的回调
*/
callback?: () => void;
}
// @ts-ignore
const seajs = window.seajs;
export function render({ entry, callback }: RenderOptions) {
if (!entry) {
return;
}
// 这里主要用于支持开发态 HMR,seajs 有缓存的话,就需要去清空掉
if (seajs.get(entry)) {
seajs.clear();
}
seajs.use([entry], callback);
}
export function refresh({ entry, callback }: RenderOptions) {
if (!entry) {
return;
}
seajs.clear();
seajs.use([entry], callback);
}
-95
View File
@@ -1,95 +0,0 @@
import React, { useEffect, useMemo } from 'react';
import styled from 'styled-components';
import { builtinWorkspace as workspace } from '@music163/tango-core';
import { noop } from '@music163/tango-helpers';
import { render, refresh } from './render';
const SandboxContainer = styled.div`
position: relative;
min-height: 90%;
`;
type UrlResolverOptions = {
baseUrl: string;
url: string;
};
export interface LegacySandboxProps extends React.ComponentPropsWithoutRef<'div'> {
/**
* url 地址拼装函数
*/
urlResolver?: (options: UrlResolverOptions) => string;
/**
* 文件服务地址,用来追加到文件名的相对路径前
*/
baseUrl: string;
/**
* 入口文件地址
*/
entry: string;
/**
* 渲染后的回调
*/
afterRender?: () => void;
/**
* MouduleReload 后的回调
*/
afterReload?: () => void;
/**
* MoudleReload 后的执行 callback 的延迟的毫秒数
*/
reloadTimeout?: number;
}
function defaultUrlResolver({ baseUrl, url }: UrlResolverOptions): string {
let ret = url;
if (url && url.indexOf(baseUrl) === -1) {
ret = `${baseUrl}${url}`;
}
return ret;
}
/**
* 基本的沙箱实现
* TODO: 实现独立的基于 iframe 的沙箱
*/
export function LegacySandbox({
urlResolver = defaultUrlResolver,
baseUrl,
entry: entryProps,
afterRender = noop,
afterReload = noop,
reloadTimeout = 0,
...rest
}: LegacySandboxProps) {
const entry = useMemo(() => {
return urlResolver({
url: entryProps,
baseUrl,
});
}, [baseUrl, entryProps, urlResolver]);
useEffect(() => {
render({
entry,
callback: afterRender,
});
}, [entry, afterRender]);
useEffect(() => {
let timeout: any;
workspace.on('refresh', (e: any) => {
refresh({
entry: urlResolver({ url: e.detail.entry, baseUrl }),
callback: () => {
timeout = setTimeout(afterReload, reloadTimeout);
},
});
});
return () => {
timeout && clearTimeout(timeout);
};
}, [baseUrl, urlResolver, afterReload, reloadTimeout]);
return <SandboxContainer id="sandbox-container" className="Sandbox" {...rest} />;
}
@@ -1,46 +0,0 @@
import React, { useRef, useEffect } from 'react';
const style = {
width: '100%',
height: '100%',
};
export function StandaloneLegacySandbox({ workspace, customImportMap }: any) {
const ref = useRef<HTMLIFrameElement>();
useEffect(() => {
if (ref.current) {
ref.current.contentWindow['workspace'] = workspace;
ref.current.contentWindow['customImportMap'] = customImportMap;
ref.current.contentDocument.open();
ref.current.contentDocument.write(`
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Tango Sandbox</title>
<link rel="stylesheet" href="https://unpkg.com/antd/dist/antd.css" />
<script src="https://unpkg.com/react@16.14.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.14.0/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/moment/min/moment-with-locales.js"></script>
<script src="https://unpkg.com/react-is@16/umd/react-is.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone@7.15.6/babel.js"></script>
<script src="https://unpkg.com/styled-components@5.1.1/dist/styled-components.js"></script>
<script src="https://unpkg.com/antd/dist/antd-with-locales.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/netease-sea@1.0.0/dist/index.min.js"></script>
<script src="http://127.0.0.1:8081/assets/loader.js"></script>
<script>
seajs.customImportMap = window.customImportMap;
seajs.use(['http://127.0.0.1:8081/src/index.js']);
</script>
</head>
<body>
<div id="sandbox-container"></div>
`);
ref.current.contentDocument.close();
}
}, [workspace, customImportMap]);
return <iframe style={style} ref={ref} />;
}
-34
View File
@@ -1,34 +0,0 @@
import { IFiles } from '../types';
// 兼容 tango.config.json,转成 sandbox.config.json
export const enhanceFiles = (files: IFiles, entry = '/src/index.js') => {
if (files['/tango.config.json']) {
const tangConfigJsonStr = files['/tango.config.json'].code;
const tangConfigJson = JSON.parse(tangConfigJsonStr);
files['/sandbox.config.json'] = {
code: JSON.stringify(tangConfigJson.sandbox, null, 2),
};
}
if (!files['/index.html']) {
files['/index.html'] = {
code: `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="data:image/ico;base64,aWNv">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="root" />
<script type="module" src="${entry}"></script>
</body>
</html>
`,
};
}
return files;
};
-30
View File
@@ -1,30 +0,0 @@
import React from 'react';
import ViteSandbox from '../vite-sandbox';
import { CodeSandbox } from '../code-sandbox';
import { CodeSandboxProps as ProSandboxProps } from '../types';
import { enhanceFiles } from './helper';
/**
* @deprecated 已废弃,请直接使用 CodeSandbox
*/
export function ProSandbox({ moduleType = 'cjs', ...props }: ProSandboxProps) {
if (moduleType === 'esm') {
return (
<ViteSandbox
bundlerURL="https://vitesandbox.fn.netease.com"
files={enhanceFiles(props.files, props.entry)}
{...props}
/>
);
} else {
return (
<CodeSandbox
bundlerURL="https://codesandbox.fn.netease.com"
files={enhanceFiles(props.files, props.entry)}
{...props}
/>
);
}
}
export type { ProSandboxProps };
@@ -1,55 +0,0 @@
import MD5 from 'crypto-js/md5';
function isSameOrigin(url1: string, url2: string) {
try {
const u1 = new URL(url1);
const u2 = new URL(url2);
return (
u1.protocol === u2.protocol &&
u1.hostname.split('.').slice(-2).join('.') === u2.hostname.split('.').slice(-2).join('.')
);
} catch (e) {
console.log(e, 'from isSameOrigin');
return false;
}
}
export const getBaseName = () => {
const urlParams = new URLSearchParams(window.location.search);
const busid = MD5(JSON.stringify(urlParams.get('sprintId') || '')).toString();
const wcid = MD5(JSON.stringify(urlParams.get('appId') || '')).toString();
return `/${busid}/vite/${wcid}`;
};
export function changeRoute(iframeEl: HTMLIFrameElement, path = '/', routerMode = 'history') {
const basename = getBaseName();
const _isSameOrigin = isSameOrigin(window.location.href, iframeEl.src);
if (!_isSameOrigin) {
console.log('无法跨域设置 iframe 中的路由');
return;
}
if (iframeEl.contentDocument) {
const innerIframe = iframeEl.contentDocument.querySelector(
'#inner-sandbox-container',
) as HTMLIFrameElement;
if (innerIframe) {
try {
if (routerMode === 'hash') {
innerIframe.contentWindow.location.hash = basename + path;
} else {
innerIframe.contentWindow.history.pushState(
{ key: '642dmeli', state: null },
null,
basename + path,
);
const popevent = new PopStateEvent('popstate', { state: { key: '642dmeli' } });
innerIframe.contentWindow.dispatchEvent(popevent);
}
} catch (e) {
console.log(e, 'from changeStartRoute');
}
}
}
}
-121
View File
@@ -1,121 +0,0 @@
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { TangoEventName } from '@music163/tango-helpers';
import Loading from './loading';
import { changeRoute } from './helper';
import { ViteSandboxProps, IFiles } from '../types';
const urlParams = new URLSearchParams(window.location.search);
const ViteSandbox = ({
files = {},
bundlerURL = '',
eventHandlers = {},
startRoute,
routerMode,
onLoad,
}: ViteSandboxProps) => {
const [loading, setLoading] = useState(true);
const iframeEl = useRef(null);
const sendProjectInfo = useCallback((files: IFiles) => {
iframeEl.current.contentWindow.postMessage(
{
type: 'compile-esm',
payload: {
files,
busid: urlParams.get('sprintId') || '',
wcid: urlParams.get('appId') || '',
},
},
'*',
);
}, []);
const sandboxOnLoad = useCallback(() => {
// 修改 domain,以便让外部页面和 iframe 页面在同一个域名下,主要目的是为了直接监听 iframe 页面的事件
document.domain = window.location.hostname.split('.').slice(-2).join('.');
sendProjectInfo(files);
}, [files, sendProjectInfo]);
// 监听事件
const attachEvent = useCallback(
(document: Document) => {
Object.keys(eventHandlers).forEach((eventType) => {
document[eventType.toLowerCase()] = eventHandlers[eventType];
if (eventType === 'onTango') {
document?.addEventListener(TangoEventName.DesignerAction, eventHandlers[eventType]);
}
});
},
[eventHandlers],
);
const handleMessage = useCallback(
({ data }: any) => {
switch (data.type) {
case 'inner-sandbox-container-created':
if (iframeEl.current.contentDocument) {
const innerIframe = iframeEl.current.contentDocument.querySelector(
'#inner-sandbox-container',
) as HTMLIFrameElement;
if (innerIframe && innerIframe.contentDocument) {
innerIframe.onload = () => {
attachEvent(innerIframe.contentDocument);
// 被构建应用的页面渲染成功后,设置初始路由
changeRoute(iframeEl.current, startRoute, routerMode);
// 被构建应用的页面渲染成功后,关闭 loading
setLoading(false);
// 被构建应用的页面渲染成功后,执行传入的回调函数
onLoad && onLoad();
};
}
}
break;
default:
break;
}
},
[attachEvent, startRoute, routerMode, onLoad],
);
useEffect(() => {
window.addEventListener('message', handleMessage);
return () => {
window.removeEventListener('message', handleMessage);
};
}, [handleMessage]);
useEffect(() => {
sendProjectInfo(files);
}, [files, sendProjectInfo]);
useEffect(() => {
changeRoute(iframeEl.current, startRoute, routerMode);
}, [startRoute, routerMode]);
return (
<>
{loading ? <Loading /> : null}
<iframe
ref={iframeEl}
id="sandbox-container"
style={{
width: '100%',
height: '100%',
border: 0,
outline: 0,
}}
src={bundlerURL}
onLoad={sandboxOnLoad}
/>
</>
);
};
export default ViteSandbox;
@@ -1,266 +0,0 @@
import React from 'react';
import styled from 'styled-components';
const LoadingWrapper = styled.div`
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #151515;
color: #fff;
z-index: 9;
.container {
position: absolute;
top: 0;
left: 0;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: row;
}
.text {
margin-top: 22rem;
font-size: 1rem;
width: 100%;
font-weight: 200;
text-align: center;
text-transform: capitalize;
}
.cube,
.cube * {
position: absolute;
width: 6rem;
height: 6rem;
}
.cube .sides * {
box-sizing: border-box;
border: 0.12rem solid white;
border-radius: 0.25rem;
background: rgba(255, 255, 255, 0.1);
}
@keyframes rotate {
0% {
transform: rotateX(-37.5deg) rotateY(45deg);
}
50% {
transform: rotateX(-37.5deg) rotateY(405deg);
}
100% {
transform: rotateX(-37.5deg) rotateY(405deg);
}
}
.sides {
animation: rotate 3s ease infinite;
animation-delay: 0.8s;
transform-style: preserve-3d;
transform: rotateX(-37.5deg) rotateY(45deg);
}
.cube .sides .top {
animation: top-animation 3s ease infinite;
animation-delay: 0ms;
transform: rotateX(90deg) translateZ(96px);
animation-fill-mode: forwards;
transform-origin: 50% 50%;
}
@keyframes top-animation {
0% {
opacity: 1;
transform: rotateX(90deg) translateZ(100px);
}
20% {
opacity: 1;
transform: rotateX(90deg) translateZ(48px);
}
70% {
opacity: 1;
transform: rotateX(90deg) translateZ(48px);
}
90% {
opacity: 1;
transform: rotateX(90deg) translateZ(100px);
}
100% {
opacity: 1;
transform: rotateX(90deg) translateZ(100px);
}
}
.cube .sides .bottom {
animation: bottom-animation 3s ease infinite;
animation-delay: 0ms;
transform: rotateX(-90deg) translateZ(96px);
animation-fill-mode: forwards;
transform-origin: 50% 50%;
}
@keyframes bottom-animation {
0% {
opacity: 1;
transform: rotateX(-90deg) translateZ(100px);
}
20% {
opacity: 1;
transform: rotateX(-90deg) translateZ(48px);
}
70% {
opacity: 1;
transform: rotateX(-90deg) translateZ(48px);
}
90% {
opacity: 1;
transform: rotateX(-90deg) translateZ(100px);
}
100% {
opacity: 1;
transform: rotateX(-90deg) translateZ(100px);
}
}
.cube .sides .front {
animation: front-animation 3s ease infinite;
animation-delay: 100ms;
transform: rotateY(0deg) translateZ(96px);
animation-fill-mode: forwards;
transform-origin: 50% 50%;
}
@keyframes front-animation {
0% {
opacity: 1;
transform: rotateY(0deg) translateZ(96px);
}
20% {
opacity: 1;
transform: rotateY(0deg) translateZ(48px);
}
70% {
opacity: 1;
transform: rotateY(0deg) translateZ(48px);
}
90% {
opacity: 1;
transform: rotateY(0deg) translateZ(96px);
}
100% {
opacity: 1;
transform: rotateY(0deg) translateZ(96px);
}
}
.cube .sides .back {
animation: back-animation 3s ease infinite;
animation-delay: 100ms;
transform: rotateY(-180deg) translateZ(96px);
animation-fill-mode: forwards;
transform-origin: 50% 50%;
}
@keyframes back-animation {
0% {
opacity: 1;
transform: rotateY(-180deg) translateZ(96px);
}
20% {
opacity: 1;
transform: rotateY(-180deg) translateZ(48px);
}
70% {
opacity: 1;
transform: rotateY(-180deg) translateZ(48px);
}
90% {
opacity: 1;
transform: rotateY(-180deg) translateZ(96px);
}
100% {
opacity: 1;
transform: rotateY(-180deg) translateZ(96px);
}
}
.cube .sides .left {
animation: left-animation 3s ease infinite;
animation-delay: 100ms;
transform: rotateY(-90deg) translateZ(96px);
animation-fill-mode: forwards;
transform-origin: 50% 50%;
}
@keyframes left-animation {
0% {
opacity: 1;
transform: rotateY(-90deg) translateZ(96px);
}
20% {
opacity: 1;
transform: rotateY(-90deg) translateZ(48px);
}
70% {
opacity: 1;
transform: rotateY(-90deg) translateZ(48px);
}
90% {
opacity: 1;
transform: rotateY(-90deg) translateZ(96px);
}
100% {
opacity: 1;
transform: rotateY(-90deg) translateZ(96px);
}
}
.cube .sides .right {
animation: right-animation 3s ease infinite;
animation-delay: 100ms;
transform: rotateY(90deg) translateZ(96px);
animation-fill-mode: forwards;
transform-origin: 50% 50%;
}
@keyframes right-animation {
0% {
opacity: 1;
transform: rotateY(90deg) translateZ(96px);
}
20% {
opacity: 1;
transform: rotateY(90deg) translateZ(48px);
}
70% {
opacity: 1;
transform: rotateY(90deg) translateZ(48px);
}
90% {
opacity: 1;
transform: rotateY(90deg) translateZ(96px);
}
100% {
opacity: 1;
transform: rotateY(90deg) translateZ(96px);
}
}
`;
const Loading = () => (
<LoadingWrapper>
<div className="container">
<div className="cube">
<div className="sides">
<div className="top" />
<div className="right" />
<div className="bottom" />
<div className="left" />
<div className="front" />
<div className="back" />
</div>
</div>
<div className="text">{'ViteSandbox Compiling...'}</div>
</div>
</LoadingWrapper>
);
export default Loading;
+11 -9
View File
@@ -2741,20 +2741,22 @@
call-me-maybe "^1.0.1"
glob-to-regexp "^0.3.0"
"@music163/antd@^0.1.1":
version "0.1.1"
resolved "https://registry.npmjs.org/@music163/antd/-/antd-0.1.1.tgz"
integrity sha512-yUD42g1pAMK5MxhBqXUXsgapGGPAY6VcOXLEhjJVcYuR835mC6LvHFsBEvqgjUIFMnI3aol/VohOwPRIqqaqDw==
"@music163/antd@^0.1.2":
version "0.1.2"
resolved "https://registry.npmjs.org/@music163/antd/-/antd-0.1.2.tgz#15d0dd41839649d1294bb89a9c8c78eb370e06c9"
integrity sha512-4S7Bi3f0VWmvEdQFjPQQWlwgMrTPX1uwTIKnLMB8a4LT617kJrW748f/tZwY32kuDdooZ5tpMZCM93rmu5hEKQ==
dependencies:
"@music163/foundation" "^0.1.1"
"@music163/foundation" "^0.1.2"
"@music163/tango-boot" "^0.1.2"
"@music163/tango-helpers" "^0.1.1"
antd "4"
classnames "^2.3.2"
coral-system "^1.0.6"
"@music163/foundation@^0.1.1":
version "0.1.1"
resolved "https://registry.npmjs.org/@music163/foundation/-/foundation-0.1.1.tgz"
integrity sha512-opkEWIGcVhKznRMnptz04qhUcphMUl0mPEgH1QovIpj738veSiQExqCs0C2pqZThNwVMYZlzbweSjOlAT9D+Sw==
"@music163/foundation@^0.1.2":
version "0.1.2"
resolved "https://registry.npmjs.org/@music163/foundation/-/foundation-0.1.2.tgz#e541bee3bbfc223ca94b9e15f9ec97da4e88b495"
integrity sha512-vy1RFFvklwY4F0/WpSKw4XUCjVrrl9lQtKiwNaz/iUfxg01MAYAX4dUziQFNHKrdPP6ROAPmMNkAZGYxbhAhIA==
dependencies:
"@music163/tango-boot" "^0.1.2"
coral-system "^1.0.6"