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`。 + + 如果上述步骤均已确认无误,这可能是浏览器的缓存问题,可尝试重启浏览器后重新访问。 +