From 1a6aa1782bc59d96a5e66738be7f4362c7390fcf Mon Sep 17 00:00:00 2001
From: ccloli <8115912+ccloli@users.noreply.github.com>
Date: Tue, 6 Feb 2024 15:45:36 +0800
Subject: [PATCH] docs: deploy designer & sandbox
---
.../website/docs/designer/deploy/designer.mdx | 455 +++++++++++++++++-
apps/website/docs/designer/deploy/sandbox.mdx | 132 ++++-
2 files changed, 583 insertions(+), 4 deletions(-)
diff --git a/apps/website/docs/designer/deploy/designer.mdx b/apps/website/docs/designer/deploy/designer.mdx
index 197b56dd..ae572483 100644
--- a/apps/website/docs/designer/deploy/designer.mdx
+++ b/apps/website/docs/designer/deploy/designer.mdx
@@ -1,5 +1,456 @@
# 设计器接入
-:::tip
-正在编写中,敬请期待。
+Tango 是使用 React 编写的低代码引擎,因此你需要一个基于 React 的工程来接入 Tango 的设计器。
+
+:::tip
+你也可以直接使用本项目内的 `/apps/playground` 作为基础应用并改造,只需要移除 `.umirc.ts` 中与 `resolvePackageIndex()` 相关的配置并手动安装 `@music163/tango-designer` 即可。
:::
+
+
+## 初始化本地项目
+
+1. 首先请确认本地已经安装了 Node.js 与 npm/yarn,且 Node.js 的版本大于 16。若本地没有安装 Node.js 或版本不满足需求,推荐使用 [nvm](https://github.com/nvm-sh/nvm) 来安装并实现多版本管理。
+
+ ```sh
+ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
+ nvm install 16
+ nvm use 16
+ ```
+
+2. Tango 是基于 React 开发的,如果你本地没有 React 项目,可以使用 [`umi`](https://umijs.org/) 或者 [`create-react-app`](https://create-react-app.dev/) + [`craco`](https://craco.js.org/) 等脚手架创建一个新的 React 项目。下面的例子将以 umi 作为示例。
+
+ ```sh
+ mkdir a-tango-demo
+ cd a-tango-demo
+ npx create-umi@latest
+ ```
+
+3. 在本地的 hosts 文件中将沙箱的同级域名指向本地,以实现沙箱与本地通信。如果你还没有部署过沙箱,可参考 [沙箱接入](./sandbox) 文档。
+
+ ```hosts
+ # 假设沙箱已经部署在了 sandbox.example.com 下,hosts 可以将任意 example.com 的子域名指向本地
+ 127.0.0.1 local.example.com
+ ```
+
+4. 与沙箱联调需要开启 HTTPS,因此你需要准备一个 HTTPS 证书并开启本地 HTTPS,你可以使用 Let's Encrypt 或者如下的指令自签证书,并配置到项目中。
+
+ ```sh
+ openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes \
+ -subj "/CN=example.com" -addext "subjectAltName=DNS:example.com,DNS:*.example.com" \
+ -keyout example.com.key -out example.com.crt
+ ```
+
+ ```ts
+ // .umirc.ts
+ import { defineConfig } from 'umi';
+ import path from 'path';
+
+ export default defineConfig({
+ routes: [
+ { path: '/', component: 'index' },
+ ],
+ npmClient: 'npm',
+ https: {
+ key: path.resolve(__dirname, 'example.com.key'),
+ cert: path.resolve(__dirname, 'example.com.crt'),
+ http2: false,
+ },
+ });
+ ```
+
+5. 由于 Google Chrome 的安全策略,你需要让项目在本地开发时能正常返回 HTTP 响应头 `Origin-Agent-Cluster: ?0`,否则会导致 Tango 无法正常工作。
+
+ ```ts
+ // plugin.ts
+ import type { IApi } from 'umi';
+
+ export default (api: IApi) => {
+ api.addMiddlewares(() => {
+ return (req, res, next) => {
+ res.setHeader('Origin-Agent-Cluster', '?0');
+ next();
+ };
+ });
+ };
+ ```
+
+
+## 引入设计器
+
+1. 在项目中安装如下依赖:
+
+ ```sh
+ npm install @music163/tango-designer
+ # 如果本地没有 antd 等依赖的话,需要一并安装
+ npm install antd@4 @ant-design/icons
+ ```
+
+2. 初始化设计器时,你需要预先准备好符合 Tango 代码规范的项目代码,并将其组装为 `{ filename: string; code: string; }[]` 的数组,具体可参 [项目规范](#TODO) 文档。你也可以先使用如下代码用于测试:
+
+
+ 点击查看代码
+
+ ```ts
+ // files.ts
+ const packageJson = `{
+ "name": "demo",
+ "private": true,
+ "dependencies": {
+ "@music163/antd": "0.1.6",
+ "@music163/tango-boot": "0.1.3",
+ "react": "17.0.2",
+ "react-dom": "17.0.2",
+ "prop-types": "15.7.2",
+ "tslib": "2.5.0"
+ }
+ }`;
+
+ const tangoConfigJson = `{
+ "packages": {
+ "react": {
+ "version": "17.0.2",
+ "library": "React",
+ "type": "dependency",
+ "resources": [
+ "https://cdn.jsdelivr.net/npm/react@{{version}}/umd/react.development.js"
+ ]
+ },
+ "react-dom": {
+ "version": "17.0.2",
+ "library": "ReactDOM",
+ "type": "dependency",
+ "resources": [
+ "https://cdn.jsdelivr.net/npm/react-dom@{{version}}/umd/react-dom.development.js"
+ ]
+ },
+ "react-is": {
+ "version": "16.13.1",
+ "library": "ReactIs",
+ "type": "dependency",
+ "resources": [
+ "https://cdn.jsdelivr.net/npm/react-is@{{version}}/umd/react-is.production.min.js"
+ ]
+ },
+ "styled-components": {
+ "version": "5.3.5",
+ "library": "styled",
+ "type": "dependency",
+ "resources": [
+ "https://cdn.jsdelivr.net/npm/styled-components@{{version}}/dist/styled-components.min.js"
+ ]
+ },
+ "moment": {
+ "version": "2.29.4",
+ "library": "moment",
+ "type": "dependency",
+ "resources": [
+ "https://cdn.jsdelivr.net/npm/moment@{{version}}/moment.js"
+ ]
+ },
+ "@music163/tango-boot": {
+ "version": "0.2.1",
+ "library": "TangoBoot",
+ "type": "baseDependency",
+ "resources": [
+ "https://cdn.jsdelivr.net/npm/@music163/tango-boot@{{version}}/dist/boot.js"
+ ],
+ "description": "云音乐低代码运行时框架"
+ },
+ "@music163/antd": {
+ "version": "0.1.7",
+ "library": "TangoAntd",
+ "type": "baseDependency",
+ "resources": [
+ "https://cdn.jsdelivr.net/npm/@music163/antd@{{version}}/dist/index.js",
+ "https://cdn.jsdelivr.net/npm/antd@4.24.13/dist/antd.css"
+ ],
+ "description": "云音乐低代码中后台应用基础物料",
+ "designerResources": [
+ "https://cdn.jsdelivr.net/npm/@music163/antd@{{version}}/dist/designer.js",
+ "https://cdn.jsdelivr.net/npm/antd@4.24.13/dist/antd.css"
+ ]
+ }
+ }
+ }`;
+
+ const routesCode = `
+ import Index from "./pages/index";
+
+ const routes = [
+ {
+ path: '/',
+ exact: true,
+ component: Index
+ },
+ ];
+
+ export default routes;
+ `;
+
+ const storeIndexCode = `
+ export { default as app } from './app';
+ `;
+
+ const entryCode = `
+ import { runApp } from '@music163/tango-boot';
+ import routes from './routes';
+ import './services';
+ import './stores';
+ import './index.less';
+
+ runApp({
+ boot: {
+ mountElement: document.querySelector('#root'),
+ qiankun: false,
+ },
+
+ router: {
+ type: 'browser',
+ config: routes,
+ },
+ });
+ `;
+
+ const viewHomeCode = `
+ import React from "react";
+ import { definePage } from "@music163/tango-boot";
+ import {
+ Page,
+ Section,
+ Button,
+ } from "@music163/antd";
+
+ class App extends React.Component {
+ render() {
+ return (
+
+
+
+ );
+ }
+ }
+
+ export default definePage(App);
+ `;
+
+
+ const storeApp = `
+ import { defineStore } from '@music163/tango-boot';
+
+ export default defineStore({
+ title: 'Page Title',
+ }, 'app');
+ `;
+
+ const serviceCode = `
+ import { defineServices } from '@music163/tango-boot';
+
+ export default defineServices({
+ test: {
+ url: "http://example.com/api/test",
+ method: "get"
+ }
+ });
+ `;
+
+ const lessCode = `
+ body {
+ font-size: 12px;
+ }
+ `;
+
+ export const sampleFiles = [
+ { filename: '/package.json', code: packageJson },
+ { filename: '/tango.config.json', code: tangoConfigJson },
+ { filename: '/src/index.js', code: entryCode },
+ { filename: '/src/pages/index.js', code: viewHomeCode },
+ { filename: '/src/routes.js', code: routesCode },
+ { filename: '/src/stores/index.js', code: storeIndexCode },
+ { filename: '/src/stores/app.js', code: storeApp },
+ { filename: '/src/services/index.js', code: serviceCode },
+ { filename: '/src/index.less', code: lessCode },
+ ];
+ ```
+
+
+
+
+3. 在项目的代码中引入设计器,并传入业务项目的代码至 workspace,具体可参考如下代码:
+
+
+ 点击查看代码
+
+ ```ts
+ import React, { useState, useMemo } from 'react';
+ import { Box } from 'coral-system';
+ import { Button, Space } from 'antd';
+ import {
+ Designer,
+ DesignerPanel,
+ SettingPanel,
+ Sidebar,
+ Toolbar,
+ WorkspacePanel,
+ WorkspaceView,
+ CodeEditor,
+ Sandbox,
+ DndQuery,
+ themeLight,
+ } from '@music163/tango-designer';
+ import { createEngine, Workspace } from '@music163/tango-core';
+ import {
+ ApiOutlined,
+ AppstoreAddOutlined,
+ BuildOutlined,
+ ClusterOutlined,
+ FunctionOutlined,
+ createFromIconfontCN,
+ } from '@ant-design/icons';
+ import { sampleFiles } from './files';
+
+ // 沙箱初始化
+ const sandboxQuery = new DndQuery({
+ context: 'iframe',
+ });
+
+ // 图标库初始化(物料面板和组件树使用了 iconfont 里的图标)
+ createFromIconfontCN({
+ scriptUrl: '//at.alicdn.com/t/c/font_2891794_lzc7rtwuzf.js',
+ });
+
+ /**
+ * 平台初始化
+ */
+ export default function App() {
+ const [menuLoading, setMenuLoading] = useState(true);
+ const [menuData, setMenuData] = useState(false);
+
+ const { workspace, engine } = useMemo(() => {
+ // 实例化工作区
+ const workspace = new Workspace({
+ entry: '/src/index.js',
+ files: sampleFiles,
+ });
+
+ // 引擎初始化
+ const engine = createEngine({
+ workspace,
+ });
+
+ return { workspace, engine };
+ }, [sampleFiles]);
+
+ return (
+
+
+ Logo
+
+ )}
+ description="Description"
+ actions={
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ >
+
+ }
+ widgetProps={{
+ menuData: menuData as any,
+ loading: menuLoading,
+ }}
+ />
+ } />
+ }
+ isFloat
+ width={800}
+ />
+ } isFloat width={800} />
+ }
+ isFloat
+ width={800}
+ />
+
+
+
+ {
+ if (e.type === 'done') {
+ const sandboxWindow: any = sandboxQuery.window;
+ if (sandboxWindow.TangoAntd) {
+ if (sandboxWindow.TangoAntd.menuData) {
+ setMenuData(sandboxWindow.TangoAntd.menuData);
+ }
+ if (sandboxWindow.TangoAntd.prototypes) {
+ workspace.setComponentPrototypes(sandboxWindow.TangoAntd.prototypes);
+ }
+ }
+ if (sandboxWindow.localTangoComponentPrototypes) {
+ workspace.setComponentPrototypes(sandboxWindow.localTangoComponentPrototypes);
+ }
+ setMenuLoading(false);
+ }
+ }}
+ />
+
+
+
+
+
+
+
+
+ );
+ }
+ ```
+
+
+
+4. 编写完成后,使用脚手架的指令启动项目开始开发,启动后需要通过之前 hosts 配置的域名打开本地项目,并信任自签发的证书。
+
+ ```sh
+ # 在传入 HOST 与 PORT 环境变量启动后,可通过 https://local.example.com:8001 访问项目
+ PORT=8001 HOST=local.example.com npm start
+ ```
+
+5. 如果一切正常,现在你应该可以正常看到设计器的效果了。
+
+ ![](https://p6.music.126.net/obj/wo3DlcOGw6DClTvDisK1/33392683897/c72b/f6f2/2944/a481233c752a9e74213fa15aa516b6f0.png)
+
+
+## 配置低代码设计器
+
+请参考 [设计器自定义](../customize/panels) 部分。
diff --git a/apps/website/docs/designer/deploy/sandbox.mdx b/apps/website/docs/designer/deploy/sandbox.mdx
index 0cd02fa0..fd6d7e62 100644
--- a/apps/website/docs/designer/deploy/sandbox.mdx
+++ b/apps/website/docs/designer/deploy/sandbox.mdx
@@ -1,5 +1,133 @@
# 低代码沙箱接入
-:::tip
-正在编写中,敬请期待。
+Tango 预置的沙箱方案是基于 [CodeSandbox](https://github.com/codesandbox/codesandbox-client) 实现的,并在之上做了适配 Tango 的修改,你可以在 [这里](https://github.com/NetEase/codesandbox-client) 查看我们修改后的实现。由于沙箱的能力并不属于 Tango 的核心部分,因此你需要单独部署 CodeSandbox 的沙箱服务。
+
+
+## 准备沙箱产物
+
+### 使用预构建的资源
+
+我们的代码仓库配置了 GitHub Actions,它会在代码变更时自动构建沙箱,然后将最终的产物上传至 GitHub Releases。
+
+你可以直接在 [这里](https://github.com/NetEase/codesandbox-client/releases) 下载最新版本的产物,下载后将其解压至 `./www` 目录下即可。
+
+### 手动构建
+
+如你需要对沙箱做一些修改,你可以通过 clone 代码仓库到本地,然后执行指令构建产物。
+
+1. 首先请确认本地已经安装了 Node.js 与 npm/yarn,且 Node.js 的版本大于 16。若本地没有安装 Node.js 或版本不满足需求,推荐使用 [nvm](https://github.com/nvm-sh/nvm) 来安装并实现多版本管理。目前 workflow 里配置的 Node.js 版本为 16。
+
+ ```sh
+ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
+ nvm install 16
+ nvm use 16
+ ```
+
+2. 克隆代码仓库到本地:
+
+ ```sh
+ git clone https://github.com/NetEase/codesandbox-client.git
+ ```
+
+3. 安装依赖:
+
+ ```sh
+ cd codesandbox-client
+ yarn
+ ```
+
+4. 构建产物:
+
+ ```sh
+ yarn build:deps
+ yarn build:sandpack
+ ```
+
+5. 构建完成后,你可以在 `./www` 下找到最终构建的产物。
+
+
+## 部署沙箱
+
+:::tip
+CodeSandbox 的沙箱服务需要确保部署环境支持 HTTPS 访问。你需要一个可自主控制的域名,并为其获取有效的 SSL 证书。你可以使用 Let's Encrypt 或 ZeroSSL 等方法获取证书,或者使用主机服务提供商提供的 HTTPS 方案。
+
+对于本地开发等无法准备有效 SSL 证书的情况,你可以将域名在 hosts 文件里指向 `127.0.0.1`,然后使用 Caddy 提供的自签发证书功能(该配置已预置在仓库的 `Caddyfile` 中),或者使用下面的命令手动生成证书并添加信任,然后将其写入配置文件:
+
+```sh
+# 为 example.com 及其子域名生成一个有效期 10 年的自签发证书
+openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes \
+ -subj "/CN=example.com" -addext "subjectAltName=DNS:example.com,DNS:*.example.com" \
+ -keyout example.com.key -out example.com.crt
+```
+
:::
+
+### 使用 Caddy 部署
+
+我们在项目内部提供了一个 Caddy 配置文件,你可以在这个配置文件中调整参数,然后使用 Caddy 启动服务器。
+
+1. 从 [Caddy 的下载页面](https://caddyserver.com/download) 下载可执行文件,或参考 [Caddy 的文档](https://caddyserver.com/docs/install) 了解其他安装方式。
+
+2. 确保构建产物位于 `./www` 目录下,且当前目录下存在 `Caddyfile` 文件。如果你是直接从 Releases 页面下载的沙箱产物,可以从 [这里](https://github.com/NetEase/codesandbox-client/raw/master/Caddyfile) 下载最新的 `Caddyfile` 文件。
+
+3. 将沙箱运行的域名的 DNS 记录解析到你的服务器的 IP 地址;如果你是在本地开发,你可以在 hosts 文件里将测试的域名指向 `127.0.0.1`。
+
+4. 如果你需要使用 Caddy 的自动 HTTPS 功能(包括本地开发),请将 `Caddyfile` 里的 `:8080` 替换为你自己的域名,Caddy 会为该域名自动签发 SSL 证书;如果你在服务上游有其他代理服务器负责处理 HTTPS,则可以不做修改,但请确认上游服务器代理的端口与 `Caddyfile` 里的端口保持一致。
+
+5. 在 4 开启了 HTTPS 功能的基础上,如果你需要 Caddy [自动签发合法的 HTTPS 证书](https://caddyserver.com/docs/automatic-https) (而不是本地开发用的自签证书),请将 `Caddyfile` 里的 `local_certs` 一行删除;如果需要手动指定证书,请参考 [Caddy 文档](https://caddyserver.com/docs/caddyfile/options#tls-options)。
+
+6. Caddy 的配置文件里默认监听的 HTTP 端口是 `8080` 端口,HTTPS 则是 `8443` 端口,如果需要修改为默认的 `80` 与 `443` 端口,请修改 `Caddyfile` 里的 `http_port` 与 `https_port`
+
+7. 如果一切准备就绪,你可以使用下面的指令启动 Caddy:
+
+ ```sh
+ caddy run
+ ```
+
+8. 部署完成后,在浏览器上访问你配置的域名端口,例如若你在 4 里将域名改为 `local.example.com`,则访问 `https://local.example.com:8443` 即可。若浏览器的标题栏显示为 _Sandbox - CodeSandbox_ 则表示部署成功。
+
+### 使用 Docker 部署
+
+我们在项目内部提供了一个 `Dockerfile`,它会自动从源码构建产物,并自动使用 Caddy 的 Docker 镜像部署。你可以在部署前按照上面的步骤按需修改 `Caddyfile` 文件,然后使用下面的指令构建镜像并启动容器:
+
+```sh
+docker build -t tango-codesandbox .
+docker run -p 8443:8443 tango-codesandbox
+```
+
+### 使用 nginx 部署
+
+如果你更熟悉 nginx,可以参考 [这里](https://github.com/NetEase/tango/issues/84#issuecomment-1878229696) 的配置修改,然后将 SSL 证书更新至配置中,或是通过 Certbot 或 acme.sh 等工具签发证书并自动修改 nginx 配置。
+
+
+## 常见问题
+
+- **沙箱部署后显示白屏,并在控制台提示 `Error: Can't detect sandbox ID from the current URL`**
+
+ 这是正常情况,沙箱需要引擎传入代码后才能正常执行,只需确认浏览器的标题栏显示为 _Sandbox - CodeSandbox_ 即可。
+
+- **沙箱部署必须要启用 HTTPS 吗?**
+
+ 是的,因为 CodeSandbox 会注册 Service Worker 来处理请求缓存。此外由于 Chrome 的 [安全策略](https://developer.chrome.com/blog/document-domain-setter-deprecation) 限制,若需要与不同域名的 iframe 通过修改 `document.domain` 通信,必须在 top 与 iframe 的 HTTP 响应头中添加 `Origin-Agent-Cluster: ?0` 响应头,而且该响应头仅在 HTTPS 下生效。
+
+- **本地开发没有合法的 HTTPS 证书该怎么办?**
+
+ 你可以在操作系统的 hosts 文件里将任意域名指向 `127.0.0.1`,并为该域名签发自签证书。Caddy 会在配置文件中指定了访问域名的情况下自动签发证书,并自动将 CA 证书加入当前设备的可信根证书,因此你不需要再做其他操作。不过如果你使用了其他的部署方案,或者有自己的理由自行签发证书,可以使用下面的指令创建证书:
+
+ ```sh
+ # 为 example.com 及其子域名生成一个有效期 10 年的自签发证书
+ openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes \
+ -subj "/CN=example.com" -addext "subjectAltName=DNS:example.com,DNS:*.example.com" \
+ -keyout example.com.key -out example.com.crt
+ ```
+
+- **我无法在 Tango 设计器里选中或拖拽沙箱里的组件**
+
+ 这是因为 Tango 设计器的沙箱是通过 iframe 嵌入的,而 iframe 的安全策略是不允许跨域访问的,所以 Tango 与沙箱会通过修改 `document.domain` 为同级域名的方式来实现跨域访问。
+
+ 首先请确认 Tango 的设计器与沙箱所属的域名都是同级域名的子域名。例如你可以将设计器部署在 `tango.example.com` 下,然后将沙箱部署在 `sandbox.example.com` 下,由于他们都是 `example.com` 的子域名,所以两者可以通过修改 `document.domain` 实现跨域访问。
+
+ 如果他们已经位于同级域名下,请确认 Tango 设计器与沙箱 iframe 的页面请求的 HTTP 响应头中均包含 `Origin-Agent-Cluster: ?0`,只有当服务器返回该响应头时,Chrome 才能正常修改 `document.domain`。
+
+ 如果上述步骤均已确认无误,这可能是浏览器的缓存问题,可尝试重启浏览器后重新访问。
+