mirror of
https://github.com/NetEase/tango.git
synced 2026-08-29 02:01:34 +08:00
fix: refactor codes
This commit is contained in:
@@ -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
@@ -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 提供了强大的基于浏览器的代码构建与执行能力。
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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': {
|
||||
|
||||
@@ -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 的方式来实现。
|
||||
|
||||
@@ -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>
|
||||
```
|
||||
|
||||
## 运行结果
|
||||
|
||||
运行结果如下:
|
||||
|
||||

|
||||
|
||||
## Playground 示例
|
||||
|
||||
实际的代码,可以参考 <https://github.com/NetEase/tango/tree/main/apps/playground> 源码。
|
||||
|
||||
@@ -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',
|
||||
],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from './sandbox';
|
||||
// export * from './standalone';
|
||||
// export * from './frame';
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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} />;
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user