Deploy docs to pages (#104)

* chore: update

* chore: update

* chore: remove website
This commit is contained in:
Wells
2024-02-19 20:08:36 +08:00
committed by GitHub
parent b2293ba9f4
commit 7470190e0b
102 changed files with 3 additions and 90902 deletions
@@ -1,5 +1,5 @@
# Simple workflow for deploying static content to GitHub Pages
name: Deploy static content to Pages
name: Deploy Github Pages
on:
# Runs on pushes targeting the default branch
-34
View File
@@ -1,34 +0,0 @@
name: 'typedoc'
on:
push:
branches: [main]
permissions:
contents: read
pages: write
id-token: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
# Generate your TypeDoc documentation
- run: npx typedoc
# https://github.com/actions/upload-pages-artifact
- uses: actions/upload-pages-artifact@v2
with:
path: ./docs # This should be your TypeDoc "out" path.
deploy:
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
# https://github.com/actions/deploy-pages
uses: actions/deploy-pages@v2
+1 -1
View File
@@ -20,7 +20,7 @@ English | [简体中文](/README.zh-CN.md)
You can view the detailed usage guide through the following links:
- Document site: <https://netease.github.io/tango/>
- Document site: <https://netease.github.io/tango-site/>
- Playground application: <https://tango-demo.musicfe.com/designer/>
## ✨ Features
+1 -1
View File
@@ -20,7 +20,7 @@
可以通过下面的链接查看详细的使用指南:
- 官方文档站点: <https://netease.github.io/tango/>
- 官方文档站点: <https://netease.github.io/tango-site/>
- 演示应用: <https://tango-demo.musicfe.com/designer/>
## ✨ 特性
-20
View File
@@ -1,20 +0,0 @@
# Dependencies
/node_modules
# Production
/build
# Generated files
.docusaurus
.cache-loader
# Misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
-41
View File
@@ -1,41 +0,0 @@
# Website
This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator.
### Installation
```
$ yarn
```
### Local Development
```
$ yarn start
```
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
### Build
```
$ yarn build
```
This command generates static content into the `build` directory and can be served using any static contents hosting service.
### Deployment
Using SSH:
```
$ USE_SSH=true yarn deploy
```
Not using SSH:
```
$ GIT_USER=<Your GitHub username> yarn deploy
```
If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
-3
View File
@@ -1,3 +0,0 @@
module.exports = {
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
};
Binary file not shown.

Before

Width:  |  Height:  |  Size: 297 KiB

@@ -1,76 +0,0 @@
---
title: Tango LowCode Builder Released!
authors: [wwsun]
tags: [netease, tango, open-source]
---
## 📝 Tango 简介
Tango 是一个用于快速构建低代码平台的低代码设计器框架,借助 Tango 只需要数行代码就可以完成一个基本的低代码平台前端系统的搭建。Tango 低代码设计器直接读取前端项目的源代码,并以源代码为中心,执行和渲染前端视图,并为用户提供低代码可视化搭建能力,用户的搭建操作会转为对源代码的修改。借助于 Tango 构建的低代码工具或平台,可以实现 源码进,源码出的效果,无缝与企业内部现有的研发体系进行集成。
### ✨ 核心特性
- 经历网易云音乐内网生产环境的实际检验,可灵活集成应用于低代码平台,本地开发工具等
- 基于源码 AST 驱动,无私有 DSL 和协议
- 提供实时出码能力,支持源码进,源码出
- 开箱即用的前端低代码设计器,提供灵活易用的设计器 React 组件
- 使用 TypeScript 开发,提供完整的类型定义文件
### 🏗️ 基于源码的低代码搭建方案
Tango 低代码引擎不依赖私有搭建协议和 DSL,而是直接使用源代码驱动,引擎内部将源码转为 AST,用户的所有的搭建操作转为对 AST 的遍历和修改,进而将 AST 重新生成为代码,将代码同步给在线沙箱执行。与传统的 [基于 Schema 驱动的低代码方案](https://mp.weixin.qq.com/s/yqYey76qLGYPfDtpGkVFfA) 相比,不受私有 DSL 和协议的限制,能够完美的实现低代码搭建与源码开发的无缝集成。
<img src="https://p5.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/13140534982/ee2e/f42c/cc9a/184e2918a011b57d46e6c64a2722fa44.png" />
### 📄 源码进,源码出
由于引擎内核完全基于源代码驱动实现,Tango 低代码引擎能够实现源代码进,源代码出的可视化搭建能力,不提供任何私有的中间产物。如果公司内部已经有了一套完善的研发体系(代码托管、构建、部署、CDN),那么可以直接使用 Tango 低代码引擎与现有的服务集成构建低代码开发平台。
![code in, code out](./codein-codeout.png)
### 🏆 产品优势
与基于私有 Schema 的低代码搭建方案相比,Tango 低代码引擎具有如下优势:
| 对比项 | 基于 Schema 的低代码搭建方案 | Tango(基于源码 AST 转换) |
| ------------ | -------------------------------------------------- | --------------------------------------------------------------------- |
| 适用场景 | 面向特定的垂直搭建场景,例如表单,营销页面等 | 🔥 面面向以源码为中心的应用搭建场景 |
| 语言能力 | 依赖私有协议扩展,不灵活,且难以与编程语言能力对齐 | 🔥 直接基于 JavaScript 语言,可以使用所有的语言特性,不存在扩展性问题 |
| 开发能力 | LowCode | 🔥 LowCode + ProCode |
| 源码导出 | 以 Schema 为中心,单向出码,不可逆 | 🔥 以源码为中心,双向转码 |
| 自定义依赖 | 需要根据私有协议扩展封装,定制成本高 | 🔥 原有组件可以无缝低成本接入 |
| 集成研发设施 | 定制成本高,需要额外定制 | 🔥 低成本接入,可以直接复用原有的部署发布能力 |
## 📐 技术架构
Tango 低代码引擎在实现上进行了分层解藕,使得上层的低代码平台与底层的低代码引擎可以独立开发和维护,快速集成部署。此外,Tango 低代码引擎定义了一套开放的物料生态体系,开发者可以自由的贡献扩展组件配置能力的属性设置器,以及扩展低代码物料的二方三方业务组件。
具体的技术架构如下图所示:
![low-code engine](./lowocode-engine.png)
## ⏰ 开源里程碑
Tango 低代码引擎是网易云音乐内部低代码平台的核心构件,开源涉及到大量的核心逻辑解藕的工作,这将给我们正常的工作带来大量的额外工作,因此我们计划分阶段推进 Tango 低代码引擎的开源事项。
1. 今天我们正式发布 Tango 低代码引擎的第一个社区版本,该版本将会包括 Tango 低代码引擎的核心代码库,TangoBoot 应用框架,以及基于 antd v4 适配的低代码组件库。
2. 我们计划在今年的 **9 月 30 日** 发布低代码引擎的 **1.0 Beta** 版本,该版本将会对核心的实现面向社区场景重构,移除掉我们在云音乐内部的一些兼容代码,并将核心的实现进行重构和优化。
3. 我们计划在今年的 **10 月 30 日** 发布低代码引擎的 **1.0 RC** 版本,该版本将会保证核心 API 基本稳定,不再发生 BREAKING CHANGE,同时我们将会提供完善翔实的开发指南、部署文档、和演示应用。
4. **正式版**本我们将在 **2023 年 Q4 结束前** 发布,届时我们会进一步完善我们的开源社区运营机制。
![milestones](./tango-milestones.png)
## 🤝 社区建设
我们的开源工作正在积极推进中,可以通过如下的信息了解到我们的最新进展:
- Github 仓库:<https://github.com/NetEase/tango>
- 文档站点:<https://netease.github.io/tango/>
欢迎大家加入到我们的社区中来,一起参与到 Tango 低代码引擎的开源建设中来。有任何问题都可以通过 [Github Issues](https://github.com/NetEase/tango/issues) 反馈给我们,我们会及时跟进处理。
## 💗 致谢
感谢网易云音乐公共技术团队,大前端团队,直播技术团队,以及所有参与过 Tango 项目的同学们。
感谢 CodeSandbox 提供的 [Sandpack](https://sandpack.codesandbox.io/) 项目,为 Tango 提供了强大的基于浏览器的代码构建与执行能力。
Binary file not shown.

Before

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

-5
View File
@@ -1,5 +0,0 @@
wwsun:
name: Wells SUN
title: Core maintainer
url: https://github.com/wwsun
image_url: https://github.com/wwsun.png
-38
View File
@@ -1,38 +0,0 @@
# 目录结构
## 目录结构规范
在目录结构上,我们约定了一套通用的目录结构
```md
── src
│ +── assets
│ +── pages
│ +── components
│ +── services/index.js
│ +── stores
│ ├── routes.js // 路由配置
│ ├── global.less // 全局样式
| |-- index.js 应用启动配置
└── package.json
|---tango.config.json 设计器和 external 配置
```
## `src/components` 自定义组件
该目录放置本地开发的组件,例如
```jsx
// src/components/sample.jsx
import React from 'react';
import { Box } from '@music163/antd';
// 如果需要被搭建,使用 Box 代替 div,并且支持透出属性
export function Sample(props) {
return <Box {...props}>sample component</Box>;
}
```
:::info
注意,如果需要 Sample 组件能够被设计器解析和搭建,则一定要使用 MusicOne 的 `Box` 组件代替普遍的 `div` 元素,你也可以使用 `<Box as="span" />` 来改变渲染的 html 元素。
:::
-134
View File
@@ -1,134 +0,0 @@
# 应用入口
框架通过 `runApp` 创建渲染整个应用,在创建应用时可以传入应用的全局配置。
## 基本配置
通过 `src/index.js` 对应用进行全局配置,设置路由、运行时环境、服务函数、状态模型等。
```js
import { runApp, tangoBootConfig } from '@music163/tango-boot';
import { message } from '@music163/antd';
// 配置请求错误发生时消息默认的展现方式,此处使用 message 组件进行消息浮层的展示
tangoBootConfig.toast = message;
runApp({
// 启动项配置
boot: {},
// 应用容器注入
providers: [],
// 状态模型
stores: {},
// 服务函数
services: {},
// 路由配置
router: {},
});
```
## 启动项配置
`boot` 配置项用于应用的启动项配置:
```js
runApp({
boot: {
// 应用挂载的 dom 结点
mountElement: document.querySelector('#root'),
// 是否开启 qiankun,如开启则对应为 `qiankun: { appName: string }`
qiankun: false,
},
});
```
## 路由配置
`router` 配置项用于配置应用的前端路由,底层基于 `react-router` 实现:
```js
import routes from './routes';
runApp({
router: {
// 路由类型: hash | browser
type: 'hash',
// 路由配置信息
config: routes,
// basename,
},
});
```
TangoBoot 使用的是 ReactRouter 的[静态路由配置方案](https://github.com/remix-run/react-router/tree/v5/packages/react-router-config)。具体的路由配置文件如下:
```js
// routes.js
import Index from './pages/index';
import About from './pages/about';
const routes = [
{
path: '/',
exact: true,
component: Index,
},
{
path: '/about',
component: About,
},
{
path: '/user/:id',
component: About,
},
];
export default routes;
```
## 应用容器配置
由于 `runApp` 封装了根组件的渲染逻辑,某些时候,你可能想要为根组件包裹特定的容器类组件,例如多语言配置,状态容器等等,可以借助 `providers` 配置项进行:
```js
runApp({
// 传入 providers 组件实例,按照传入顺序进行包裹
providers: [<ConfigProvider />, <LocaleProvider />],
});
```
## 设置环境变量 {#env}
某些时候你可能需要自定义一些环境变量,以便在应用运行在不同的环境中的时候进行快速的判断。你可以借助 `tango.env` 来快速的获取到应用的环境信息。需要注意的是,默认情况下不会有任何的环境信息,你可以在需要的时候通过在 `runApp` 中传入 `getEnv` 设置的方式来设置你想要的环境变量。
例如:
```js
runApp({
// 自定义应用的环境信息,你可以返回对象
getEnv() {
if (location.origin.includes('tango')) {
// tango 设计器中
return 'development';
}
if (location.origin.includes('localhost')) {
// 本地开发
return 'local';
}
return 'production';
},
});
```
在任意地方消费环境变量,例如:
```jsx
<Box isRender={tango.env === 'development'}>一个仅在开发态显示的区域</Box>
```
-87
View File
@@ -1,87 +0,0 @@
# 全局单例
TangoBoot 默认提供全局单例模式,意味着,您可以在项目中始终使用该全局单例来进行必要的应用运行时信息的访问。
:::tip
单例意味着单一实例,也叫单例模式,是一种比较常见的设计模式。限制类实例化次数只能一次,一个类只有一个实例,并提供一个访问它的全局访问点。TangoBoot 单例意味着您的应用各种始终只有一个 TangoBoot 实例,并且可以借助该单一实例,一致的获取到应用到运行时信息。
:::
## 如何获取全局单例
```js
// 从模块包引入
import tango from '@music163/tango-boot';
// 或直接从 window 获取
window.tango;
// 两者是对等的
console.log(tango === window.tango); // true
```
### 可以从 `tango` 单例中获取的信息
```js
// tango
{
// 自定义配置信息
config: {},
// 自定义辅助函数
helpers: {},
// History
history: History;
// 服务函数集合
services: {},
// 状态模型集合
stores: {},
// builtin helpers
closeModal: Function;
openModal: Function;
setStoreValue: Function;
getStoreValue: Function;
// ...
}
```
## 如何使用
### 在模型定义中使用
```jsx
import tango, { defineModel } from '@music163/tango-boot';
export default defineModel(
{
title: 'hello',
async listUsers() {
const data = await tango.services.listUsers();
this.title = data;
},
},
'app'
);
```
### 在视图中使用
```jsx
import tango, { definePage } from '@music163/tango-boot';
function App() {
return (
<Box>
{tango.stores.app.title}
<Button onClick={tango.services.list}>request</Button>
</Box>
);
}
export default definePage(App);
```
-130
View File
@@ -1,130 +0,0 @@
# 常用的工具函数
tango-boot 提供了一组便捷的工具函数,用于应对日常开发过程中的典型操作逻辑。
### `getUser()`
> 从 v1.15.0 开始提供
获取当前的登录用户
```js
const user = tango.getUser();
```
### `checkPrivilege({ appCode, privilegeCode })`
> 从 v1.15.0 开始提供
检查 PMS 权限
```js
const test = await tango.checkPrivilege({ appCode, privilegeCode });
```
### `copyToClipboard(text: string)`
复制文本到剪贴板
```js
tango.copyToClipboard('hello world');
```
### `navigateTo(link: string, data?: object)`
导航到其他前端路由
```js
// 基本用法
tango.navigateTo('/about');
// 带路由参数
tango.navigateTo('/about', { the: 'query' }); // /about?the=query
```
### `showToast(text: string, type: string, time: number)`
- text 消息正文
- type 消息类型 `success | error | warn`
- time 消息展示的时间,单位秒,默认为 3 秒
消息提示
```js
tango.showToast('hello world');
tango.showToast('something is error', 'error', 3);
```
### `openModal(modalId: string)`
根据弹层 ID 唤起目标弹层
```js
tango.openModal('modal123');
```
### `closeModal(modalId: string)`
根据弹层 ID 关闭目标弹层
```js
tango.closeModal('modal123');
```
### `formatDate(date, format)`
- date: `number | string | Moment` 日期值
- format: `string` 格式化,取值参考 https://momentjs.com/docs/#/displaying/format/
格式化日期值
```js
tango.formatDate(1659496225323, 'YYYY-MM-DD HH:mm:ss'); // 2022-08-03 11:10:25
tango.formatDate('2022-11-11', 'X'); // 1668096000
```
### `formatNumber(number, locale, options)`
- number: `number`
- locale: `string`
- options: `Intl.NumberFormatOptions`,参考取值 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat
格式化数字
```js
tango.formatNumber(99999); // 99,999
```
### `getCurrentLanguage`
获取当前语种
```js
tango.getCurrentLanguage(); // en-US
```
### `setLanguage(localeCode)`
设置当前语种
```js
tango.setLanguage('en-US');
```
### `getLanguageList`
获取当前应用可用语种列表
```js
tango.getLanguageList();
```
### `trans(module:key)`
根据多语言 key 获取当前语种文案
```js
tango.trans('common:add'); // 新增
```
-24
View File
@@ -1,24 +0,0 @@
# 国际化
TangoBoot 本身不提供国际化功能,国际化和您的 UI 组件库相关,在云音乐,我们采用的是基于 `@music163/antd` 的 UI 层解决方案。可以借助应用入口的容器配置功能进行国际化配置。
:::tip
如果您还不了解应用入口配置,可以先访问 [应用入口](./app) 一节进行了解。
:::
## 配置示例
```js
import { runApp } from '@music163/tango-boot';
import { ConfigProvider } from '@music163/antd';
import locale from '@music163/antd/es/locale/zh_CN';
import 'moment/dist/locale/zh-cn';
// 设置 moment 的文案
moment.locale('zh-cn');
runApp({
// 设置 UI 组件库的文案配置
providers: [<ConfigProvider locale={locale} />],
});
```
-139
View File
@@ -1,139 +0,0 @@
# TangoBoot
TangoBoot 是一个服务于 Tango 低代码应用开发体系的前端应用框架,提供了标准的数据请求,状态管理,路由方案,并提供了通用的运行时工具函数,让开发者通过简单的配置即可生成单页应用。
## 应用架构
TangoBoot 的应用架构使用的是 View-Model-Service 三层模型,其中模型层定义了 Observable States,视图层观察 Model 的变化而进行自动更新,服务层用来创建一组服务函数,供视图层和模型层消费。图如下图所示:
<img
alt="image"
width="600px"
src="https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/13760951704/985c/9706/7f18/be713816a143d3a054f51c9d1fc59b36.png"
/>
## 核心 API
- `runApp` 创建应用入口
- `definePage` 定义 Reactive 视图
- `defineStore` 定义可观察的状态模型
- `defineServices` 定义服务函数
### 创建应用入口
其中`index.js` 为应用的入口文件,一个基本的实现为
```jsx
import { runApp } from '@music163/tango-boot';
import routes from './routes';
import services from './services';
import home from './stores/home';
import counter from './stores/counter';
// 全局样式
import './global.less';
const { mount, unmount, bootstrap } = runApp({
boot: {
mountElement: document.querySelector('#root'),
qiankun: false,
},
stores: {
home,
counter,
},
services,
router: {
type: 'browser',
config: routes,
},
});
export { mount, unmount, bootstrap };
```
### 创建视图模型
通过 defineStore 来进行视图模型的定义非常简单,只需简单的声明状态和动作即可
```jsx
import { defineStore } from '@music163/tango-boot';
const counter = defineStore({
num: 0,
get() {},
decrement: function () {
counter.num--;
},
increment: () => counter.num++,
});
export default counter;
```
### 创建 Reactive 视图
视图层如果要监听状态的变化,只需要 `definePage` 对视图组件进行一层包裹即可。
```jsx
import React from 'react';
import tango, { definePage } from '@music163/tango-boot';
class App extends React.Component {
increment = () => {
tango.stores.counter.increment();
};
render() {
return (
<div>
<h1>Counter: {tango.stores.counter.num}</h1>
<button type="primary" onClick={this.increment}>
+1
</button>
</div>
);
}
}
export default definePage(App);
```
### 创建数据服务函数
`defineServices` 提供了低成本声明异步服务函数的能力。
```jsx
import { defineServices } from '@music163/tango-boot';
export default defineServices({
list: {
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users',
formatter: (res) => {
const { data, message } = res;
return {
code: 200,
list: data,
total: data.length,
message,
};
},
},
add: {
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users',
method: 'post',
},
update: {
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users',
method: 'post',
},
delete: {
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users?id=1',
},
});
```
-66
View File
@@ -1,66 +0,0 @@
# 双向绑定
TangoBoot 支持快捷实现组件与视图双向绑定。
## 借助 `withModel` 实现双向绑定
`withModel` 是 TangoBoot 内置的模型绑定 HOC,支持将组件的内部状态同步到某个 store 定义的状态中。基本的用法如下:
```jsx
class Input extends React.Component {
foo() {}
render() {
return <input {...this.props} />;
}
}
const ModelInput = withModel({
// 设置从组件同步到 store 中的状态值
getValueFromEvent(e: any) {
return e.target.value;
},
})(Input);
```
现在 `ModelInput` 获取了双向绑定的能力,其 `value` 变化后的值将会通过 `onChange` 事件同步给绑定的模型变量
```jsx
const Store = defineStore(
{
name: 'alice',
},
'user'
);
const ModelApp = defineView((props) => {
return (
<div>
<ModelInput model="user.name" />
</div>
);
});
```
## 配置选项
`withModel` 的定义如下:
```jsx
withModel(options)(Component);
```
其中 options 的配置如下:
| 属性名 | 类型 | 默认值 | 说明 |
| ----------------- | ------------------ | ---------- | -------------------------- |
| name | string | | 组件的 displayName |
| valuePropName | string | "value" | 组件的受控值 |
| trigger | string | "onChange" | 组件值变化时的回调函数 |
| getValueFromEvent | `(...args) => any` | | 从回调函数参数中的取值方法 |
借助 `withModel` 增强后,Component 组件将会获得如下属性:
| 属性名 | 类型 | 默认值 | 说明 |
| ------ | ------ | ------ | ------------------------ |
| model | string | | 绑定的模型变量的变量路径 |
-45
View File
@@ -1,45 +0,0 @@
---
sidebar_position: 4
---
# 创建页面
TangoBoot 内置提供了一个创建 reactive 视图的模型,用户无需关注复杂的依赖关系,只需要将视图组件使用 `definePage` 包裹即可。
## definePage
基于传递进来的组件创建一个 reactive 视图。reactive 视图会根据 store 数据的变化来自动触发视图重新渲染。例如
```jsx
import React from 'react';
import { definePage, defineStore } from '@music163/tango-boot';
const user = defineStore({ name: 'Bob' });
export default definePage(() => <div>Hello {user.name}!</div>);
```
`definePage` 内部为被包裹的组件实现了一个最佳的 `shouldComponentUpdate` 或 `memo`。
使用 `definePage` 包裹后的组件还会默认接收一些额外的属性,以便于开发者便捷的获取部分状态信息。具体包括:
- `routerData` 路由数据:包括 `params` 路由参数,`query` 查询参数,`pathname` 路径名
- `stores` 模型索引
- `services` 服务索引
## defineView
`defineView` 是一个更加轻量的实现 reactive 视图的包裹器,推荐所有在视图中使用的本地组件都使用它来进行包裹。与 `definePage` 不同的是,他没有额外的属性透传进来。
```jsx
export default defineView(() => (
<div>
<div>Hello {user.name}!</div>
<div>Your first post is: {timeline.posts[0]}</div>
</div>
));
```
:::info
`defineView` 的实现依赖了 [react-easy-state](https://github.com/RisingStack/react-easy-state#creating-reactive-views),感兴趣的同学可以访问对应的链接了解更详细的内容。
:::
-33
View File
@@ -1,33 +0,0 @@
# 组件实例获取
TangoBoot 支持快捷设置和获取组件的实例。
## 借助 `withRef` 注册组件实例
`withRef` 是 TangoBoot 内置的实例注册 HOC,可以快捷的进行组件实例的注册和获取。基本的用法如下:
```tsx
class Input extends React.Component {
foo() {}
render() {
return <input {...this.props} />;
}
}
const ClassInput = withRef()(Input);
```
借助 `withRef` 包裹组件后,当新的组件实例被设置 `id` 属性时,会自动将对应的实例注册到 `tango.refs` 中。例如:
```tsx
export function Basic() {
const inputRef = useRef();
useEffect(() => {
console.log(tango.refs.classInput, inputRef.current);
}, []);
// 设置 id 属性后,会自动组件实例到 `tango.refs` 中
return <ClassInput id="classInput" ref={inputRef} />;
}
```
-90
View File
@@ -1,90 +0,0 @@
# 消费异步数据服务
TangoBoot 提供一个配置式的创建数据服务函数的方法,通过 `defineServices` 可以非常轻松的声明一组异步服务函数,供视图层或模型层消费。
## 简单用法
```js
import { defineServices } from '@music163/tango-boot';
const services = defineServices({
list: {
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users',
},
add: {
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users',
method: 'post',
},
});
```
调用异步服务函数
```js
async () => {
// 最简单的调用方法
await services.list();
// 用可以快捷传递参数,参数1为数据载荷,参数2为请求配置
await services.add(payload, config);
};
```
## 请求配置
默认情况下,`defineServices` 底层采用 [axios](https://github.com/axios/axios) 发起请求。
```js
import { defineServices } from '@music163/tango-boot';
export default defineServices({
list: {
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users',
formatter: (res) => {
const { data, message } = res;
return {
code: 200,
list: data,
total: data.length,
message,
};
},
// 这里你也可以传入 axios 的其他配置
},
});
```
常用的配置如下:
- url 请求地址
- method 请求方法,默认为 `GET`
- `GET`
- `POST`
- `PUT`
- `DELETE`
- formatter 格式化响应数据
- requestType 请求体编码类型(content-type),默认值为 `json`
- `json` 默认将 JavaScript 对象序列化为 JSON
- `x-www-form-urlencoded` 对应为 `application/x-www-form-urlencoded`
- headers 自定义请求头
- timeout 超时的毫秒数
更多的配置选项可以参考 [axios 请求配置文档](https://axios-http.com/zh/docs/req_config)。
:::tip 请求体编码
默认情况下 TangoBoot 将 JavaScript 对象序列化为 JSON。如果要以 `application/x-www-form-urlencoded` 格式发送数据,可以将 `requestType` 设置为 `x-www-form-urlencoded`。
`application/x-www-form-urlencoded` 数据被编码成以 '&' 分隔的键 - 值对,同时以 '=' 分隔键和值。非字母或数字的字符会被 [percent-encoding](https://developer.mozilla.org/zh-CN/docs/Glossary/percent-encoding): 这也就是为什么这种类型不支持二进制数据 (应使用 multipart/form-data 代替).
:::
## 配置错误消息展示
配置请求错误发生时消息默认的展现方式,此处使用 `message` 组件进行消息浮层的展示。推荐在应用的入口文件进行配置:
```js
import { tangoBootConfig } from '@music163/tango-boot';
import { message } from '@music163/antd';
tangoBootConfig.toast = message;
```
-214
View File
@@ -1,214 +0,0 @@
# 管理前端应用状态
TangoBoot 采用了一个轻量级的使用 Reactive 的状态管理方案,开发者可以非常轻松的进行页面状态的管理。一个最基本的示例如下所示:
```js
import React from 'react';
import { defineStore, definePage } from '@music163/tango-boot';
const counter = defineStore({
num: 0,
increment: () => counter.num++,
});
// 当状态变化的时候,视图会自动刷新
export default definePage(() => <button onClick={counter.increment}>{counter.num}</button>);
```
值得注意的是,开发者必须借助 `defineStore` 来定义状态,它会帮助你创建一个 observable 的状态对象。借助 `definePage` 来定义视图,它会帮你订一个 reactive 的视图组件,并且在自动监听状态的变化,按需进行 UI 更新。
## 创建状态模型 Stores
基本语法为 `defineStore(storeObject: object, namespace?: string)`
- `storeObject` 为原始的状态对象
- `namespace` 为该 Store 对应的命名空间,如果你定义了,可以借助 `tango.stores[namespace]` 来获取该引用
一个基本的例子如下:
```js
import { defineStore } from '@music163/tango-boot';
const user = defineStore({ name: 'Rick' });
// stores behave like normal JS objects
user.name = 'Bob';
```
### 复杂的数据结构
```js
// stores can include any valid JS structure
// including nested data, arrays, Maps, Sets, getters, setters, inheritance, ...
const user = defineStore({
profile: {
firstName: 'Bob',
lastName: 'Smith',
get name() {
return `${user.profile.firstName} ${user.profile.lastName}`;
},
},
hobbies: ['programming', 'sports'],
friends: new Map(),
});
// stores may be mutated in any syntactically valid way
user.profile.firstName = 'Bob';
delete user.profile.lastName;
user.hobbies.push('reading');
user.friends.set('id', otherUser);
```
### 异步方法
```js
const userStore = defineStore({
user: {},
async fetchUser() {
userStore.user = await fetch('/user');
// or use this
// this.user = await fetch('/user');
},
});
```
### 多个 Store
useStore.js
```js
import { store } from '@music163/tango-boot';
const userStore = defineStore(
{
user: {},
async fetchUser() {
userStore.user = await fetch('/user');
},
},
'userStore',
);
export default userStore;
```
recipesStore.js
```js
import tango, { store } from '@music163/tango-boot';
import userStore from './userStore';
const recipesStore = defineStore(
{
recipes: [],
// 方式1:直接用调用
async fetchRecipes() {
recipesStore.recipes = await fetch(`/recipes?user=${userStore.user.id}`);
},
// 方法2:使用 this 调用,仅支持 method property,不支持箭头函数
async fetchRecipes2() {
this.recipes = await fetch(`/recipes?user=${userStore.user.id}`);
},
// 方法3:使用 tango 全局变量调用,你也可以使用这种方式引入其他的 store
async fetchRecipes3() {
tango.stores.recipesStore.recipes = await fetch(
`/recipes?user=${tango.stores.userStore.user.id}`,
);
},
},
'recipesStore',
);
export default recipesStore;
```
## 创建 Reactive 视图
你可以借助 `definePage` 创建视图,也可以直接包裹您的原始组件。基本用法为 `definePage(Component)`,借助 `definePage` 可以帮你自动监听状态的变化,并在变化时自动触发视图的重新渲染。
一个简单的例子如下:
```js
import React from 'react';
import { view, store } from '@music163/tango-boot';
// this is a global state store
const user = defineStore({ name: 'Bob' });
// this is re-rendered whenever user.name changes
export default definePage(() => (
<div>
<input value={user.name} onChange={(ev) => (user.name = ev.target.value)} />
<div>Hello {user.name}!</div>
</div>
));
```
## 实现视图与状态双向绑定
TangoBoot 提供了一个名为 `withModel` 的 [HOC](https://zh-hans.reactjs.org/docs/higher-order-components.html),可以借助它来实现视图与状态的双向绑定。
使用方法如下:
```js
withModel(options)(BaseComponent);
```
### 参数配置
其中可配置的 options 参数包括:
- `name` 用于配置包裹后组件的 displayName,可选
- `valuePropName` 绑定的 value 属性名,用于实现双向绑定的受控逻辑,默认为 `value`
- `trigger` 设置收集字段值变更的时机,默认为 `onChange`
- `getValueFromEvent` 设置从事件回调中获取 value 值的方法,默认为 `val => val`,直接返回 trigger 的第一个参数
嵌套 `whitModel` 后,组件将会获得两个新增的属性:
- `model` 用于绑定 Store 中的状态
- `innerRef` 用于获取内部组件的 [ref](https://zh-hans.reactjs.org/docs/refs-and-the-dom.html) 引用
### 双向绑定示例
基本用法如下:
```jsx
import tango, { withModel, defineView, defineStore } from '@music163/tango-boot';
// 定义一个基本的 Input 组件
class Input extends React.Component {
foo() {}
render() {
return <input {...this.props} />;
}
}
// 实现双向绑定
const ModelInput = withModel({
getValueFromEvent(e) {
return e.target.value;
},
})(Input);
// 定义一个 Store
defineStore(
{
name: 'alice',
},
'user',
);
// 双向绑定验证
const ModelApp = defineView((props) => {
return (
<div>
<ModelInput model="user.name" />
<div>{tango.stores.user.name}</div>
</div>
);
});
```
-96
View File
@@ -1,96 +0,0 @@
# tango.config.json
`tango.config.json` 是 Tango 项目的核心配置文件,当您的项目使用 Tango 设计器在线开发时会读取该文件进行线上开发环境的初始化。
## entry
非必填。`entry` 是项目的入口文件,用于在线设计器的初始化,默认取值为 `/src/index.js`。
## packages
项目使用的 npm 包依赖信息,用于定义依赖的类型以及外置依赖资源。该配置项为一个 map,key 为对应的 npm 包名,value 为该依赖的配置信息。
- version 包的版本号,必选
- library UMD 包中的全局变量名,必选
- type 依赖的类型
- baseDependency 基础依赖包
- bizDependency 业务组件依赖包
- dependency 其他依赖包
- resources 外置依赖资源列表
- designerResources 在设计器视图载入的外置资源列表,用来需要区别加载时使用
- description 描述信息
依赖的类型 `type` 可以是 `baseDependency` 即项目的基础依赖,用于获取项目基础物料列表的信息;也可以是 `bizDependency` 即业务组件依赖,用于获取合并到物料面板业务组件菜单中的业务组件列表;也可以是 `depenency` 即项目的其他依赖,只用于提供 external 信息与版本信息等,不会和物料面板产生关系。
当配置了外置依赖资源 `resources` 与其对应的全局变量 `library` 时,当这些依赖被 import 时,相关资源不会被打包到 bundle 中,而是在运行时再去外部获取这些扩展依赖,其等效于之前的 `sandbox.externalResources` 与 `sandbox.externals` 配置项。此外,可以将 `resource` 中的版本号替换为 `{{version}}`,其会在平台上构建时替换为版本号。特殊的,可以定义 `designerResources` 用于适配平台的设计器视图。
```json
{
"react": {
"version": "17.0.2",
"library": "React",
"type": "dependency",
"resources": ["https://unpkg.com/react@{{version}}/umd/react.development.js"]
},
"react-dom": {
"version": "17.0.2",
"library": "ReactDOM",
"type": "dependency",
"resources": ["https://unpkg.com/react-dom@{{version}}/umd/react-dom.development.js"]
},
"@music163/tango-boot": {
"version": "0.1.3",
"library": "TangoBoot",
"type": "baseDependency",
"resources": ["https://unpkg.com/@music163/tango-boot@{{version}}/dist/boot.js"],
"description": "云音乐低代码运行时框架"
},
"@music163/antd": {
"version": "0.1.2",
"library": "TangoAntd",
"type": "baseDependency",
"resources": [
"https://unpkg.com/@music163/antd/{{version}}/dist/index.js",
"https://unpkg.com/@music163/antd/{{version}}/dist/index.css"
],
"description": "云音乐低代码中后台应用基础物料",
"designerResources": [
"https://unpkg.com/@music163/antd/{{version}}/dist/designer.js",
"https://unpkg.com/@music163/antd/{{version}}/dist/index.css"
]
},
"@music163/biz-comp": {
"type": "bizDependency",
"version": "0.0.2"
}
}
```
:::tip 注意依赖的先后顺序
由于依赖是按照定义的先后顺序注入至沙箱内,因此需要确保 `packages` 内依赖的先后顺序,否则可能会导致应用加载依赖失败(例如将 `react` 放置在组件包之后,会导致组件包无法正常加载)。
:::
## externalResources
除了上述 `packages` 定义的外置依赖资源外,在这里可以定义沙箱启动时引入的非 npm 包的外置依赖资源列表,可以是 `js` 或 `css` 等静态文件。例如:
```json
{
"externalResources": ["https://tango.netease.io/script.js"]
}
```
## proxy
非必填。项目的代理规则。
```json
{
"proxy": {
"/api/pms": {
"target": "http://cms.qa.igame.163.com",
"changeOrigin": true
}
}
}
```
@@ -1,135 +0,0 @@
# 组件库自定义
Tango 提供了基于源码的低代码开发能力,默认不提供私有的内置组件库,仅提供了一个简单的示例用于说明如何将只有组件库接入到 Tango 中。
:::tip
**组件库接入**意味着开发者可以将自己的组件库接入到 Tango 中,以便于在设计器中使用自己的组件,包括拖拽、配置、生成等行为。
:::
## 基本的目录结构
基本的组件库目录结构如下:
```text
+ src
+ button
- view.tsx // 默认视图文件
- index.ts // 渲染视图入口文件
- designer.ts // 设计器视图入口文件
- prototype.ts // 【新增】组件描述文件
+ date-picker
- index.ts // 组件包默认入口文件
- designer.ts // 【新增】组件包设计器视图入口文件
```
可以发现,相比正常的组件代码,Tango 对于接入的组件库要求提供 2 个全新的文件 `designer.ts` 和 `prototype.ts`。其中 `designer.ts` 为设计视图文件,用来实现在 Tango 设计器中的辅助搭建行为,如果无需定制,可以直接保持 `designer.ts` 文件和 `index.ts` 文件一致。而 `prototype.ts` 文件则是用来描述组件的属性和行为的,用于在设计器中渲染组件的配置项,和控制组件的拖拽行为等。
一个可供参考的示例代码是 <https://github.com/NetEase/tango-components/tree/main/packages/antd/src>
### designer.ts
在 `designer.ts` 文件中,相比原有的组件库入口文件,还需要导出 `menuData` 和 `prototypes` 两个模块。
```jsx
export * from './button';
export * from './card';
//...
// 组件的配置描述列表
export const prototypes = [
{
title: '按钮',
name: 'Button',
props: [
{
name: 'size',
setter: 'textSetter',
},
//...
],
},
//...
];
export const menuData = {
// 常用组件
common: [
{
title: '基本',
items: ['Button'],
},
],
};
```
其中 `menuData` 的 `key` 可选列表如下:
| key | 说明 |
| ------- | -------- |
| common | 常用组件 |
| atom | 原子组件 |
| snippet | 代码片段 |
### prototype.ts
组件的配置描述文件。
## 组件配置描述
### ComponentPrototypeType
组件的配置描述。
| 属性 | 说明 | 类型 | 默认值 |
| -------------- | ----------------------------------------------- | ---------------------------------------------------------------------------- | ------ |
| childrenName | 子组件的名称 | `string` | - |
| docs | 组件的文档地址 | `string` | - |
| exportType | 组件的导出类型 `defaultExport` \| `namedExport` | `string` | - |
| hasChildren | 是否有子组件 | `boolean` | - |
| help | 组件的帮助文档 | `string` | - |
| icon | 组件的图标,图片地址或 iconfont 图标名 | string | - |
| name | 组件的名称 | `string` | - |
| package | 组件的包名,或引入路径 | `string` | - |
| props | 组件的属性描述 | `ComponentPropType[]` | - |
| relatedImports | 组件的相关引入 | `string[]` | - |
| rules | 组件的规则 | `ComponentDndRulesType` | - |
| title | 组件的标题 | `string` | - |
| type | 组件的类型 | "page" \| "container" \| "placeholder" \| "element" \| "snippet" \| "block"` | - |
| usage | 组件的使用说明 | `string` | - |
### ComponentPropType
组件的属性描述。
| 属性 | 说明 | 类型 | 默认值 |
| --------------------- | -------------------------------------------- | ------------------------------------------- | ------ |
| autoCompleteOptions | 自动补全的提示值,仅对 ExpressionSetter 有效 | `string[]` | - |
| autoInitValue | 如果没提供 initValue, 是否自动初始化值 | `boolean` | - |
| defaultValue | 组件的内置默认值 | `any` | - |
| disableVariableSetter | 是否禁用变量设置器 | `boolean` | - |
| docs | 属性的文档地址 | `string` | - |
| getProp | 动态设置属性,覆盖已有的 prop 对象 | `(form) => any` | - |
| getSetterProps | 动态设置属性,覆盖已有的 setterProps 对象 | `(form) => any` | - |
| getVisible | 动态设置表单项是否展示 | `(form) => boolean` | - |
| group | 属性的分组 | `basic` \| `event` \| `style` \| `advanced` | - |
| initValue | 首次拖拽后用来初始化组件的属性值 | `any` | - |
| name | 属性的名称 | `string` | - |
| options | 属性的选项 | `any[]` | - |
| placeholder | 属性的占位符 | `string` | - |
| props | 如果是对象属性,这里声明子属性列表 | `ComponentPropType[]` | - |
| setter | 属性的设置器 | `string` | - |
| setterProps | 设置器的属性设置 | `any` | - |
| tips | 属性的提示 | `string` | - |
| title | 属性的标题 | `string` | - |
### ComponentDndRulesType
组件拖拽规则类型
| 属性 | 说明 | 类型 | 默认值 |
| ------------------------- | ---------------------------------------------------------------------------- | --------------------------- | ------ |
| canDrag | 当前组件是否可以被拖拽 | `() => boolean` | - |
| canDrop | 当前节点是否可以拖拽到目标节点中 | `(targetName) => boolean` | - |
| canMoveIn | 进来的节点是否可以落进来,仅适用于容器节点 | `(incomingName) => boolean` | - |
| canMoveOut | 被拖拽的节点是否可以被拖离当前节点,仅适用于容器节点 | `(outgoingName) => boolean` | - |
| childrenContainerSelector | 子节点的容器选择器,用于快速定位子节点容器,适合组件存在多个可搭建区域时使用 | `string` | - |
@@ -1,5 +0,0 @@
# 编辑器自定义
:::tip
正在编写中,敬请期待。
:::
@@ -1,72 +0,0 @@
# 面板自定义
Tango 设计器由多个可自定义的面板组成,默认提供了一个标准的低代码设计器布局,可以根据实际需求进行自定义。如下图所示,可以对设计器的多个区域进行自定义,包括工具栏、侧边栏、配置面板、选择工具栏等等。
![panels](https://p5.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/33769559515/5564/0630/b437/02cd440a1789b8138ee64d058f86db20.png)
- 工具栏(Toolbar):设计器的顶部区域,通常用来放置一些全局信息和操作按钮,例如项目信息,顶部工具栏、发布按钮等等。
- 侧边栏(Sidebar):设计器的左侧区域,通常用来放置一些核心功能的入口,例如k组件库、页面列表、变量配置、数据源配置等。
- 配置面板(Properties):设计器的右侧区域,通常用来放置一些配置项,例如组件属性配置、页面属性配置等。
- 选择工具栏(SelectionMenu):设计器的底部区域,通常用来放置一些选择工具,例如复制、删除、定位等。
除了使用预定义的设计器布局组件,Tango 支持完全使用开发者自行开发的组件进行替换。
## 内置设计器布局组件介绍
内置的设计器布局组件包括:
- Designer:设计器的状态容器,用来提供设计器核心状态的上下文容器,包括主题、引擎状态、沙箱状态等。
- DesignerPanel:设计器的主框架,提供了核心的布局容器,便于开发者进行后续的自定义。
- Toolbar:工具栏容器组件,提供了默认的工具栏的布局设置,可以通过 `Toolbar.Item` 进行快捷的定义工具栏的子项。
- Sidebar:侧边栏容器组件,提供了默认的侧边栏的布局设置,可以通过 `Sidebar.Item` 进行快捷的定义侧边栏的子项。
- WorkspacePanel:工作区容器组件,设计器中央的区域,包括画布和编辑等用户核心工作区,提供了默认的工作区的布局配置。
- WorkspaceView:工作区的视图组件,用于快捷定制工作区的多种视图模式,可以实例化多个工作区视图,但同一时间只能由一个工作区视图处于激活状态。
- SettingPanel:配置面板组件,提供了默认的属性配置能力。
## 基本的设计器布局
一个基本的的设计器布局示例如下图所示:
```jsx
export default function App() {
return (
<Designer theme={themeLight} engine={engine} sandboxQuery={sandboxQuery}>
<DesignerPanel
logo={<Logo />}
description={<ProjectDetail />}
actions={
<Box px="l">
<Toolbar>
<Toolbar.Item key="routeSwitch" placement="left" />
<Toolbar.Item key="history" placement="left" />
<Toolbar.Item key="preview" placement="left" />
<Toolbar.Item key="modeSwitch" placement="right" />
<Toolbar.Item key="togglePanel" placement="right" />
<Toolbar.Separator />
<Toolbar.Item placement="right">
<Space>
<Button type="primary">发布</Button>
</Space>
</Toolbar.Item>
</Toolbar>
</Box>
}
>
<Sidebar>
<Sidebar.Item key="components" label="组件" icon={<AppstoreAddOutlined />} />
<Sidebar.Item key="outline" label="结构" icon={<BuildOutlined />} />
</Sidebar>
<WorkspacePanel>
<WorkspaceView mode="design">
<CustomDesignView />
</WorkspaceView>
<WorkspaceView mode="code">
<CustomSourceCodeView />
</WorkspaceView>
</WorkspacePanel>
<SettingPanel />
</DesignerPanel>
</Designer>
);
}
```
@@ -1,4 +0,0 @@
# 选择器自定义
提供设计器视图中用户选中某个区域后展示的快捷工具。
@@ -1,95 +0,0 @@
# 设置器自定义
属性设置器用于在配置面板中展示特定配置项的配置逻辑。Tango 内置了多种标准的属性设置器,对于一些特殊场景,内置的属性设置器可能无法满足你的需要,此时开发者可以扩展自己的属性设置。
## 设置器组件
### SettingPanel
| 属性 | 说明 | 类型 | 默认值 |
| ---------------- | -------------------------------------- | ---------------------------- | ------ |
| title | 面板标题 | string | - |
| defaultValue | 默认值 | object | - |
| groupOptions | 分组选项 | object | - |
| model | 表单状态管理实例 | FormModel | - |
| onChange | 值变化回调 | (name, value, field) => void | - |
| prototype | 组件的可配置描述 | ComponentPrototype | - |
| renderItemExtra | 自定义渲染表单项的额外内容(标签右侧) | (props) => ReactNode | - |
| showGroups | 是否展示分组 | boolean | - |
| showItemSubtitle | 是否展示表单项的副标题 | boolean | - |
| showSearch | 是否展示搜索框 | boolean | - |
## 内置属性设置器
| 设置器名 | 接收值类型 | 设置器说明 | 可配置项 |
| ----------------- | ------------------------------------ | -------------------------- | ------------------ |
| eventSetter | expression | 动作函数或事件监听器设置器 | |
| actionListSetter | | ActionList 的 value 设置器 | |
| boolSetter | boolean | 布尔值设置器 | |
| choiceSetter | `{ label: string, value: string }[]` | RadioGroup 设置器 | - options 选项列表 |
| pickerSetter | `{ label: string, value: string }[]` | 下拉值设置器 | - options 选项列表 |
| colorSetter | string | 色彩 | |
| dateSetter | string | 日期设置器 | |
| dateRangeSetter | `string[]` | 日期范围设置器 | |
| timeSetter | `string` | 时间设置器 | |
| timeRangeSetter | `string[]` | 时间范围设置器 | |
| expressionSetter | expression | 表达式设置器 | |
| jsonSetter | json expression | JSON 表达式设置器 | |
| jsxSetter | jsx expression | JSX 设置器 | |
| numberSetter | number | 数字类型设置器 | |
| textSetter | string | 文本设置器 | |
| textAreaSetter | string | 文本域设置器 | |
| modelSetter | string | 变量绑定设置器 | |
| enumSetter | enum | 枚举值设置器 | |
| sliderSetter | number | 滑块设置器 | |
| listSetter | `object[]` | 列表值设置器 | |
| renderPropsSetter | Function | render props 设置器 | |
## 注册自定义属性设置器
### 属性设置器的实现标准
属性设置器组件是一个标准的受控型表单组件,开发者所开发的组件只需要暴露 `value` 和 `onChange(value)` 两个属性让属性面板的表单容器进行控制即可。
例如,我们可以实现一个简单的字符串设置器 StringSetter,其实现如下:
```jsx
function StringSetter({ value, onChange, ...rest }) {
const handleChange = (e) => {
onChange(e.target.value);
};
return <input value={value} onChange={handleChange} />;
}
```
### 注册属性设置器
注册属性设置器非常简单,只需要借助设置器暴露出来的 `registerSetter` 即可。
```jsx
import { registerSetter } from '@music163/tango-designer';
// 注册自定义 setter
registerSetter({
name: 'stringSetter',
component: StringSetter,
});
```
### 使用自定义的属性设置器
在组件的物料协议中直接声明对应的属性设置器名称即可。例如:
```jsx
const prototype = {
name: 'PageHeader',
title: 'PageHeader',
props: [
{
name: 'title',
title: '标题',
setter: 'stringSetter',
},
],
};
```
@@ -1,53 +0,0 @@
# 侧边栏自定义
提供了默认的设计器左侧边栏的布局设置。
## 侧边栏组件
### Sidebar
侧边栏容器组件。
### Sidebar.Item
侧边栏子项
| 属性 | 说明 | 类型 | 默认值 |
| ----------- | ------------------------------------------------ | ------------------------------------------------- | ------ |
| key | 子项的唯一标识 | `string` | - |
| label | 子项的描述文本,推荐不超过2个字 | `string` | - |
| icon | 图标 | `ReactNode` | - |
| showBadge | 是否显示角标 | `boolean` \| `{ count?: number; dot?: boolean; }` | - |
| title | 展开面板的标题 | `string` | - |
| width | 展开面板的宽度 | `number` | - |
| isFloat | 展开面板是否为浮动面板,浮动面板不压缩工作区宽度 | `boolean` | - |
| widgetProps | 子项的属性 | `object` | - |
| children | 子项的展开面板内容 | `ReactNode` | - |
## 内置的工具栏组件
设计器内置了一些基本的侧边栏组件,当子项的 `key` 使用了特定的值时,会自动的进行渲染。
| key | 组件 |
| ---------- | ---------- |
| components | 组件列表 |
| outline | 结构 |
| dependency | 依赖管理 |
| variables | 变量管理 |
| dataSource | 数据源管理 |
例如,下面的代码会自动渲染一个结构面板:
```jsx
<Sidebar.Item key="outline" label="结构" icon={<BuildOutlined />} />
```
## 自定义侧边栏子项
可以直接在 `Sidebar.Item` 的子节点传入自定义的内容来渲染需要的结果,例如:
```jsx
<Sidebar.Item key="custom" label="自定义" icon={<SmileOutlined />}>
<div>展开的内容部分</div>
</Sidebar.Item>
```
@@ -1,53 +0,0 @@
# 工具栏自定义
提供了默认的设计器工具栏的布局设置。
## 工具栏组件
### Toolbar
工具栏列表容器
### Toolbar.Item
工具栏子项
| 属性 | 说明 | 类型 | 默认值 |
| ----------- | -------------- | --------------------------- | ------- |
| key | 子项的唯一标识 | `string` | - |
| placement | 放置的位置 | `left` \| `right` \| `left` | `right` |
| widgetProps | 子项的属性 | `object` | - |
### Toolbar.Separator
工具栏分隔线,用来对工具栏子项进行分组展示。
## 内置的工具栏组件
设计器内置了一些基本的工具栏组件,当工具栏子项使用了特定的 `key` 值时,会自动的进行渲染。
| key | 组件 |
| ----------- | ------------------------------------ |
| routeSwitch | 路由切换 |
| history | 历史记录 |
| preview | 沙箱预览 |
| modeSwitch | 工作区模式切换,在源码和设计模式切换 |
| togglePanel | 切换布局面板的显示和隐藏 |
例如,下面的代码会自动渲染为一个路由切换的工具项。
```jsx
<Toolbar.Item key="routeSwitch" />
```
## 自定义工具栏项
可以直接在 `Toolbar.Item` 的子节点传入自定义的工具栏项,例如:
```jsx
<Toolbar.Item placement="right">
<Space>
<Button type="primary">发布</Button>
</Space>
</Toolbar.Item>
```
@@ -1,18 +0,0 @@
# 设计器接入
设计器为用户提供应用搭建的可视化界面。有两种方式初始化低代码设计器:
1. clone 官方示例代码,按照文档说明直接启动项目。
2. 手工引入设计器的 npm 包,自定义配置、启动、运行。
## 方法1: 通过示例代码启动设计器
WIP
:::tip
官方示例是一个包含了低代码设计器前后端低完整项目,可以直接启动。对于后端部分,作为示例而言,仅提供了最基本的逻辑,用户需要按照需求自行扩展。
:::
## 方法2: 手工引入设计器的 npm 包
WIP
@@ -1,19 +0,0 @@
# 低代码沙箱接入
沙箱是搭建产物(对于 Tango 主要是源码)的运行环境,它是一个独立的环境,可以在其中运行搭建产物,以便于开发者可以在不影响生产环境的情况下进行调试和测试。
Tango 沙箱由三个部分构成,包括低代码沙箱前端组件、在线打包器、沙箱后端服务,如下图所示。
![tango sandbox](https://p5.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/30579143007/ab5d/3611/950e/5ae276b6131a4a479d6fb10e50ebbfcb.png)
- 沙箱前端组件:一个开箱即用的沙箱组件,只需要传入代码和配置就可以完成应用的渲染。
- 在线打包器:提供搭建产物的浏览器端构建能力,类似于一个浏览器版本的 webpack,此部分逻辑主要来自于 [sandpack](https://sandpack.codesandbox.io/) 项目。
- 沙箱后端服务:对依赖的资源进行预构建,以及提供资源合并等服务,用来加速沙箱内部的构建打包过程。
## 沙箱的前端组件接入
WIP
## 沙箱的后端服务接入
WIP
@@ -1,7 +0,0 @@
# 服务端接入
介绍一个基本的服务端实现,以及如何接入。
:::tip
正在编写中,敬请期待。
:::
@@ -1,5 +0,0 @@
# 文件系统
:::tip
正在编写中,敬请期待。
:::
@@ -1,112 +0,0 @@
# 技术架构概览
我们在 2023年8月底[正式开源了 Tango 低代码引擎](https://juejin.cn/post/7273051203562749971)。Tango 是一个基于源码的低代码设计器框架,支持直接基于项目源码提供低代码可视化开发能力,可以无缝的与既有的本地开发工作流进行集成,从而提供渐进式的低代码开发能力。
![Tango 低代码引擎使用演示](https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/30108735057/7ba9/dced/9ac3/420f6e04b371dd47de06e7d71142560d.gif)
按照计划,我们在 2023年9月底[发布了 1.0 alpha 版本](https://github.com/NetEase/tango/releases),在此版本中我们遵循 **“最小内核”** 的原则对 Tango 的核心实现进行了大幅的重构,剥离了大量冗余的代码实现。
为了帮助大家更近一步的了解 Tango 开源版本的核心构成与代码实现,本文将会详细揭秘 Tango 低代码引擎的设计思考与实现过程。
- Github 仓库:<https://github.com/NetEase/tango>
- 发行历史:<https://github.com/NetEase/tango/releases>
- 文档站点:<https://netease.github.io/tango/>
## 低代码可视化搭建之殇
从实现上看,低代码搭建能力的核心是 UI 可视化编程。借助 UI 可视化编程,可以大大的弱化使用者对于代码编程的感知,但在真实的业务需求场景中,我们面临着大量的复杂的应用逻辑,使用者很难借助 UI 操作表达功能逻辑。例如下图中的合同管理,资金结算等页面。如果借助于传统的低代码方案,通常会发现,很容易一条路走到黑,没有回头路。所以,经常会有开发者抱怨,稍微复杂的场景下,低代码的效率甚至不如写代码。
![在实际业务场景中面临大量难以低代码开发的前端应用](https://p5.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/30577891541/dac5/e050/986b/9466633e32518be2685e882618343251.png)
## 传统低代码方案的问题
我们不妨先简单分析一下传统的低代码方案的问题。传统的低代码搭建方案往往采用定义私有 Schema 协议来可视化表达视图逻辑,也就是将代码逻辑转换为私有的描述,大致的原理可以参考下面这张图。
![基于 Schema 的低代码可视化搭建方案](https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/30577932595/1456/2196/aeee/a10fbe99c3f6d050629b140ecfbbc257.png)
这类方案很容易面临不断膨胀的私有 JSON 协议。并且,私有协议扩展性和灵活性差,难以达到图灵完备状态。例如在我们的实际开发过程中,传统的低代码方案会面临各种各样的扩展性卡点。此外,开发能力往往受限于内置的组件和模板。且难以复用现有的前端资产,例如组件和代码等等。对于开发者而言,私有协议也导致问题定位难,调试难。
借助于私有协议的搭建方案通常适合于轻业务逻辑的简单类表单,营销类的活动页面等等,很难用于复杂的业务逻辑搭建场景,因为私有协议难以有效的应对这类场景的复杂性和灵活性需求。虽然,有些方案提供了协议转代码的能力,但通常只实现了单向转码,可视化开发和代码开发是两条完全割裂的路径。
**在此基础上,我们就需要重新思考低代码搭建协议的设计问题。**
## 从私有搭建协议到公有协议
那么,我们能否不使用私有协议,而是采用公有协议?
答案是,可以的![ESTree](https://github.com/estree/estree) 规范作为主流的处理 JavaScript 源代码的标准社区协议,被广泛用于浏览器 JavaScript Parser 的实现。借助于 ESTree 协议,可以完美的实现对源码逻辑的描述,并且社区有大量的工具可以帮助我们完成这个过程。
![基于ESTree规范,实现双向互转的低代码搭建能力](https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/30578051842/b7b4/f625/9458/3ead74325547a45f501ae99c7270cffa.png)
因此,我们尝试使用 ESTree 规范来实现低代码搭建过程。借助于 ESTree 规范,我们无需定义私有的渲染描述协议,并且可以低成本的实现代码到协议,协议到代码到互转。借助于双向转码的能力,我们获得全新的低代码开发体验。
## Tango 低代码引擎实现原理
基于这个思路,我们设计了基于 ESTree 规范的低代码引擎方案 -- Tango。可以通过下面这张图来简单的描述下实现逻辑:
![Tango 低代码引擎实现分析](https://p5.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/30578073085/61cd/b2db/e103/9ed9dd334a6679c6ec18a02270efe446.png)
首先将源代码解析为 AST。用户的拖拉拽等操作则映射为对 AST 的遍历和修改。最后将新的 AST 重新生成代码,交给设计器沙箱去渲染执行。而对 AST 的解析、遍历、修改、生成,则可以借助大量的社区工具,这里我们选择的是 babel!
> AST 的全称是抽象语法树,是一种分层的程序表达,根据编程语言的语法呈现源代码的结构。
![大量的工具基于 AST 实现](https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/30578121562/578f/1bac/9dd3/69b5b4e5c1171babf4db427f41981b4d.png)
其实,数量众多的前端工具库都是基于 AST 操纵实现的。我们可以发现,在任意的前端项目中的 package.json 里的 devDependencies 里的很多工具包是基于 AST 解析操纵实现的,例如 JS 的转译,代码压缩,ESLint 等等,我们可以阅读这些工具的源码来进一步的学习。
![将源码转为 AST 描述的基本过程](https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/30578132788/1f4b/d7d7/56b8/feb4220a611afae0629d76758479118a.png)
如图所示,将源代码转为 AST 描述的基本过程包括词法分析和句法分析两个阶段:
- 词法分析:借助词法分析器将代码字符串分割为标记列表。
- 句法分析:借助句法分析器将标记数据转为 AST 描述。
最后,我们可以获得源代码的结构化描述树。有很多工具可以帮我们来实现这个过程,例如 babel -- 它可以帮助我们轻松的实现代码到 ast,ast 遍历修改,ast 到代码的过程。
## 基于 AST 实现搭建的基本过程
我们来看一下使用 ast 实现搭建逻辑的基本过程。
看一个具体的例子:通过修改 AST,在 Page 中插入一个 Section 节点。
![基于 AST 实现搭建逻辑](https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/30579119959/aea0/6e5a/6aba/979804c4270f5ad05b84da2220624afd.png)
中间这段代码,展示了核心的逻辑,通过遍历整个 AST 中的所有 JSXElement 节点,找到第一个 Page 元素,然后在 Page 元素的 children 里插入新的 Section 节点。这只是一段演示代码,具体的过程比这个要复杂的多,因为有很多的边际逻辑要处理。最后,我们可以将 ast 重新生成为代码,得到我们想要的结果。
## Tango 的数据变更流程设计
了解了基本的实现原理后,我们来看一下低代码引擎的数据变更流程设计。
![数据变更流程设计](https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/30579135078/e381/579c/61ed/91e110abd1d17c742e8aa7d407d0327b.png)
首先是引擎初始化。源码文件会被引擎内核解析进行状态初始化。接下来,对于用户的操作,会触发浏览器事件,引擎接收到相应的事件,触发内核中的状态变更,更新 AST。
然后,内核会基于新的 AST 的同步生成代码,由引擎将代码同步给渲染沙箱。渲染沙箱感知到代码变化后,会触发页面重新渲染,也就是沙箱的 HMR 过程。
## 基于源码的在线渲染沙箱设计
接下来,我们需要考虑的是如何在浏览器中执行 JavaScript 源码工程?有很多方案可以选择,我们选择的方案是 [sandpack](https://sandpack.codesandbox.io/),它是由 CodeSandbox 开源的可以在浏览器中实时运行 JavaScript 项目的的工具库。在具体实现上,[我们对 sandpack 进行了一系列的改造](https://juejin.cn/post/7102243774985666596),以满足低代码生产环境的需要。
基于 sandpack 的在线渲染沙箱方案如下图图所示。
![Tango 沙箱设计](https://p5.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/30579143007/ab5d/3611/950e/5ae276b6131a4a479d6fb10e50ebbfcb.png)
在实现上,主要包括 3 个部分,分别是:​
- 低代码沙箱:它是一个开箱即用的前端组件,只需要传入源代码和构建配置信息即可完成前端项目的构建和执行。
- 在线 Bundler:是低代码沙箱的核心,用来在浏览器上构建和执行源代码,本质上是一个在浏览器端运行的简化版 webpack。
- 打包服务:是一个 node 服务,用来对 npm 包执行预构建和资源合并。
从沙箱执行流程来看,首先 Sandbox 组件将项目的源代码和 compile 指令使用 postMessage 传递给在线 Bundler,在线 Bundler 在接收到 compile 指令后,bundler 会从 packager 打包服务加载项目的 npm 依赖,然后编译和执行代码,最后发送 success 消息给低代码沙箱。
## Tango 低代码引擎的构成
结合上面的介绍,在构成上,Tango 低代码引擎主要包括 3 个核心组成部分,分别是:
- 引擎内核:扶额建立文件,节点模型,提供输入输出能力。
- 拖拽引擎和可视化面板:提供可视化开发能力
- 渲染沙箱:提供源码在浏览器上的编译执行能力。
![引擎构成](https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/30579167082/1404/27e2/b8e5/0c719ca82494a282080d73adeff7196e.png)
借助于 Tango 低代码引擎,我们可以为开发者提供全新的在线开发体验,支持源码级的自定义能力。对可视化开发而言,可视化配置会触发 AST 的修改,进而会重新生成对应的源码。而对源码开发而言,修改源码后会同步更新 AST。
@@ -1,5 +0,0 @@
# 沙箱实现
:::tip
正在编写中,敬请期待。
:::
@@ -1,75 +0,0 @@
import Link from '@docusaurus/Link';
# 快速开始
Tango 是一个用于快速构建低代码平台的低代码设计器框架,借助 Tango 只需要数行代码就可以完成一个基本的低代码平台前端系统的搭建。
:::warning 测试版免责声明
当前开源版本为低代码引擎的 beta 演示版本,API 可能不稳定,不建议在生产环境中使用。为了保障社区版本能够有效与内部版本解藕,并且项目开源涉及到大量的额外的代码分离工作,我们预计在 2023 年 Q4 发布可以运行在生产环境的正式版。
:::
## 环境要求
- 安装 node 16.0.0 以上版本: <https://nodejs.org/en/download/>
- 安装 npm 8.x 或 yarn 1.x: <https://classic.yarnpkg.com/en/docs/getting-started>
## 启动 Tango Playground
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> 并允许不安全的证书
## 启动本地沙箱
本地开发除了需要启动 Tango 的 playground 外,还需要一个可用的沙箱环境来承载设计器。
:::tip
沙箱部分来源于 codesandbox,不属于 tango 的核心代码库,我们在原有的 codesandbox 基础上进行了一系列的改造
:::
如果没有可用的在线沙箱,可以通过本地启动一个沙箱来完成开发;如果已有一个在线沙箱,且沙箱与本地的 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
```
你也可以从 [这里](https://github.com/NetEase/codesandbox-client/releases) 下载预构建的沙箱产物,创建 `www` 子文件夹并将压缩包解压到该文件夹内
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
```
-31
View File
@@ -1,31 +0,0 @@
import Link from '@docusaurus/Link';
# 简介
Tango 是一个用于快速构建低代码平台的低代码设计器框架,借助 Tango 只需要数行代码就可以完成一个基本的低代码平台前端系统的搭建。Tango 低代码设计器直接读取前端项目的源代码,并以源代码为中心,执行和渲染前端视图,并为用户提供低代码可视化搭建能力,用户的搭建操作会转为对源代码的修改。借助于 Tango 构建的低代码工具或平台,可以实现 源码进,源码出的效果,无缝与企业内部现有的研发体系进行集成。
<img src="https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/30108735057/7ba9/dced/9ac3/420f6e04b371dd47de06e7d71142560d.gif" alt="preview" />
## 核心特性
- 经历网易云音乐内网生产环境的实际检验,可灵活集成应用于低代码平台,本地开发工具等
- 基于源码 AST 驱动,无私有 DSL 和协议
- 提供实时出码能力,支持源码进,源码出
- 开箱即用的前端低代码设计器,提供灵活易用的设计器 React 组件
- 使用 TypeScript 开发,提供完整的类型定义文件
## 实现原理
Tango 低代码引擎不依赖私有搭建协议和 DSL,而是直接使用源代码驱动,引擎内部将源码转为 AST,用户的所有的搭建操作转为对 AST 的遍历和修改,进而将 AST 重新生成为代码,将代码同步给在线沙箱执行。具体的过程如下图所示,首先将源代码解析为 AST。用户的拖拉拽等操作则映射为对 AST 的遍历和修改。最后将新的 AST 重新生成代码,交给设计器沙箱去渲染执行。而对 AST 的解析、遍历、修改、生成,则可以借助大量的社区工具,这里我们选择的是 babel!
![Tango 低代码引擎实现分析](https://p5.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/30578073085/61cd/b2db/e103/9ed9dd334a6679c6ec18a02270efe446.png)
## 技术架构
Tango 低代码引擎主要包括 3 个核心组成部分,分别是:
- 引擎内核:扶额建立文件,节点模型,提供输入输出能力。
- 拖拽引擎和可视化面板:提供可视化开发能力
- 渲染沙箱:提供源码在浏览器上的编译执行能力。
![引擎构成](https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/30579167082/1404/27e2/b8e5/0c719ca82494a282080d73adeff7196e.png)
@@ -1,136 +0,0 @@
# 基础组件包规范
## 目录结构
```txt
+ src
+ button
- view.tsx // 默认视图文件
- index.ts // 渲染视图入口文件
- designer.ts // 设计器视图入口文件
- prototype.ts // 组件描述文件
+ date-picker
- index.ts // 组件包默认入口文件
- designer.ts // 组件包设计器视图入口文件
```
## 组件包入口
### 默认入口
`src/index.ts` 是组件包的默认出口文件。
### 设计视图入口
`src/designer.ts` 是组件包的设计视图出口文件。您至少应该导出 `menuData` 和 `prototypes` 两个模块:
```ts
export { Button } from './button';
export { Card } from './card';
//...
// 组件的配置描述列表
export const prototypes = [
{
title: '按钮',
name: 'Button',
props: [
{
name: 'size',
setter: 'textSetter',
},
//...
],
},
//...
];
export const menuData = {
// 常用组件
common: [
{
title: '基本',
items: ['Button'],
},
],
};
```
其中 `menuData` 的 key 可选列表如下:
| key | 分类标题 |
| ------- | -------- |
| common | 常用组件 |
| atom | 原子组件 |
| snippet | 组合 |
| block | 区块 |
## 组件入口
### 组件默认视图文件
`src/button/view.tsx`
```tsx
export function Button({ children, ...rest }) {
return <button {...rest}>{children}</button>;
}
```
需要注意的是,你需要将组件多余的属性 `...rest` 透传给跟结点 `<button {...rest}>`,以便于在设计器中时组件能够接收到跟踪信息。
### 组件默认入口文件
`src/button/index.ts`
```ts
export * from './view';
```
### 组件设计器视图文件
设计器视图是您的组件在 tango 设计器中进行展示的视图,大部分情况下,您无需关注此逻辑,特殊情况下,您的组件可能需要在设计器下有不一样的展现和数据透出,此时可以定义该视图文件实现。
`src/button/designer.tsx`
```tsx
import { withDnd } from '@music/tango-apps-shared';
import { Button as ButtonBase } from './view';
export const Button = withDnd({
// 组件名
name: 'Button',
// 是否在设计器中有包裹层,用于设置 dnd 的追踪信息
hasWrapper: false,
})(ButtonBase);
```
### 组件描述文件
`src/button/prototype.ts`
```ts
import { ComponentPrototypeType } from '@music/tango-apps-shared';
export const Button: ComponentPrototypeType = {
title: '按钮',
name: 'Button',
exportType: 'namedExport',
icon: 'icon-anniu',
type: 'element',
package: '@music163/antd',
hasChildren: false,
props: [
{
name: 'children',
title: '文案',
setter: 'textSetter',
initValue: '按钮',
},
],
};
```
## 依赖说明
如果您的组件会视图组件,推荐您的组件基于 `@music163/antd` 或 `@music163/antd` 的最新版本进行开发,且不要依赖其他 UI 组件包,例如 `antd`, `fusion` 等。
@@ -1,148 +0,0 @@
# 基础物料协议
import TypesTable from '@site/src/components/TypesTable';
物料协议定义了一个组件的基本信息,包括组件的名称、属性、拖拽规则等。物料协议是为组件附加的额外描述文件,不会侵入到你的组件代码中,因此你可以在不改动组件代码的情况下,为组件添加物料协议。
## 组件原型 prototype
<TypesTable name="ComponentPrototypeType" />
## 组件属性 ComponentPropType
<TypesTable name="ComponentPropType" />
### 嵌套属性
当某个组件属性为嵌套属性时,例如 `<Table scroll={{ x: 800, y: 400 }} />`,此时可以描述如下:
```jsx
{
name: 'Table',
props: [
{
name: 'scroll',
props: [
{
name: 'x',
setter: 'numberSetter',
},
{
name: 'y',
setter: 'numberSetter',
}
],
}
],
}
```
### 属性的关联展示
当某个属性依赖某个特定的其他属性值时,可以借助 `getVisible` 实现关联展示控制:
```js
{
name: 'Button',
props: [
{
name: 'shape',
},
{
name: 'buttonType',
getVisible: (form) => {
// 配置项 buttonType 仅在配置项 shape 的值为 button 时才展示
return form.getValue('shape') === 'button';
},
},
];
}
```
### 动态属性设置
当组件需要依据某个属性值进行动态设置其他属性时,可以借助 `getProp` 实现控制:
```js
{
name: 'FormItem',
props: [
{
name: 'component',
title: '控件类型',
},
{
name: 'componentProps',
title: '子组件属性',
getProp(form) {
const type = form.getValue('component');
const proto = { ...componentMap[type] };
const props = omitProps(proto.props, [
'placeholder',
'options',
'onChange',
'defaultValue',
'value',
]);
return {
title: proto.title + '属性',
props,
};
},
}
],
}
```
### 属性的输入提示
当某个属性在输入时需要进行输入提示时,例如某个函数属性需要提示其签名的模版,则可以借助属性输入提示实现:
```js
{
name: 'TableColumn',
props: [
{
name: 'render',
setter: 'expressionSetter',
autoCompleteOptions: ['(value, record, index) => { return null; }'],
}
],
}
```
![img](https://p5.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/18917075100/9569/09fa/bc62/c418d3de8694ce93977450fb725bb524.gif)
## 组件拖拽规则 ComponentDndRulesType
<TypesTable name="ComponentDndRulesType" />
### canDrag/canDrop
在 onDragStart 的时候执行。
```js
export const Page = {
name: 'Page',
rules: {
canDrag() {
return false;
},
},
};
```
### canMoveIn/canMoveOut
在 onDragEnter 的时候执行。
```js
export const Modal = {
name: 'Modal',
rules: {
canMoveIn(incomingName) {
return !(incomingName === Modal.name);
},
},
};
```
-174
View File
@@ -1,174 +0,0 @@
const lightCodeTheme = require('prism-react-renderer/themes/github');
const darkCodeTheme = require('prism-react-renderer/themes/dracula');
/** @type {import('@docusaurus/types').Config} */
const config = {
title: '使用 Tango 构建你的低代码生产力工具',
tagline: '基于源代码 AST 实现可视化搭建操作,支持实时出码,不受私有 DSL 和协议限制',
favicon: 'img/favicon.ico',
// Set the production url of your site here
url: 'https://netease.github.io',
// Set the /<baseUrl>/ pathname under which your site is served
// For GitHub pages deployment, it is often '/<projectName>/'
baseUrl: '/tango',
// GitHub pages deployment config.
// If you aren't using GitHub pages, you don't need these.
organizationName: 'netease', // Usually your GitHub org/user name.
projectName: 'tango', // Usually your repo name.
onBrokenLinks: 'throw',
onBrokenMarkdownLinks: 'warn',
// Even if you don't use internalization, you can use this field to set useful
// metadata like html lang. For example, if your site is Chinese, you may want
// to replace "en" with "zh-Hans".
i18n: {
defaultLocale: 'zh-CN',
locales: ['zh-CN', 'en'],
},
// scripts: [{ src: 'https://buttons.github.io/buttons.js', async: true, defer: true }],
presets: [
[
'classic',
/** @type {import('@docusaurus/preset-classic').Options} */
({
docs: {
sidebarPath: require.resolve('./sidebars.js'),
// Please change this to your repo.
// Remove this to remove the "edit this page" links.
editUrl:
'https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/',
},
blog: {
showReadingTime: true,
// Please change this to your repo.
// Remove this to remove the "edit this page" links.
editUrl:
'https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/',
},
theme: {
customCss: require.resolve('./src/css/custom.css'),
},
}),
],
],
themeConfig:
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
({
// Replace with your project's social card
image: 'img/social-card.png',
announcementBar: {
id: 'notion_alert',
content: '🏗 当前版本为 alpha 版本,相关文档正在编写之中,敬请期待!',
backgroundColor: 'var(--ifm-color-primary-contrast-background)',
textColor: 'var(--ifm-color-primary-contrast-foreground)',
isCloseable: false,
},
navbar: {
title: '',
logo: {
alt: 'Tango Logo',
src: 'img/logo.png',
},
items: [
{
type: 'docSidebar',
sidebarId: 'designer',
position: 'left',
label: '文档',
},
{
type: 'docSidebar',
sidebarId: 'boot',
position: 'left',
label: '应用框架',
},
{ to: '/blog', label: '博客', position: 'left' },
{
type: 'html',
position: 'right',
value: `<a class="navbar__link github-button" href="https://github.com/netease/tango" data-color-scheme="no-preference: light; light: light; dark: dark;" data-size="large" data-show-count="true" aria-label="Star netease/tango on GitHub">Github</a><script async defer src="https://buttons.github.io/buttons.js"></script>`,
},
{
href: 'https://tango-demo.musicfe.com/designer/',
label: '演示应用',
position: 'right',
},
{
type: 'localeDropdown',
position: 'right',
},
],
},
footer: {
style: 'dark',
links: [
{
title: '文档',
items: [
{
label: '快速开始',
to: '/docs/designer/quick-start',
},
],
},
{
title: '社区',
items: [
{
label: 'Discussions',
href: 'https://github.com/NetEase/tango/discussions',
},
{
label: 'Discord',
href: 'https://discordapp.com/invite/docusaurus',
},
],
},
{
title: '相关资源',
items: [
{
label: '博客',
to: '/blog',
},
{
label: 'GitHub',
href: 'https://github.com/netease/tango',
},
],
},
{
title: '更多产品',
items: [
{
label: '海豹 D2C - Figma 插件',
to: 'https://www.figma.com/community/plugin/1174548852019950797/seal-figma-to-code-d2c',
},
{
label: '海豹 D2C - MasterGo 插件',
to: 'https://mastergo.com/community/plugin/98956774428196/',
},
],
},
],
logo: {
alt: 'NetEase Cloud Music',
src: 'https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/15400855445/6c8b/10d4/a8a8/452c8518b0c0a660549996d366cdff77.png',
width: 320,
},
copyright: `Copyright © ${new Date().getFullYear()} NetEase Cloud Music, Inc.`,
},
prism: {
theme: lightCodeTheme,
darkTheme: darkCodeTheme,
},
}),
};
module.exports = config;
-318
View File
@@ -1,318 +0,0 @@
{
"homepage.hero.alert": {
"message": "🏗 The current version is 0.x alpha version. Please do not use it in a production environment. The 1.0 version will be released in 2023Q4."
},
"homepage.hero.title": {
"message": "Develop your low-code tools based on Tango"
},
"homepage.hero.tagline": {
"message": "Source code based low-code engine, support realtime code generation, without private schema and DSL"
},
"homepage.hero.button.document": {
"message": "Document"
},
"homepage.timeline.alpha": {
"message": "Core codebase published, documentation site released"
},
"homepage.timeline.beta": {
"message": "Core API refactoring and optimization for community scenarios"
},
"homepage.timeline.rc": {
"message": "The core API is basically stable and no longer Breaking Changes."
},
"homepage.timeline.stable": {
"message": "API is completely stable, provides good community support, and can be used in production environments."
},
"homepage.features.low-code": {
"message": "LowCode"
},
"homepage.features.low-code-content": {
"message": "Provide a general low-code engine that supports quickly creating and modifying applications through drag-and-drop and configuration, making pattern-based application development easy."
},
"homepage.features.code-driven": {
"message": "Code Driven"
},
"homepage.features.code-driven-content": {
"message": "The underlying low-code engine is driven by source code, without proprietary building protocols and DSL, supporting online low-code and source code dual-mode synchronous development."
},
"homepage.features.outbox": {
"message": "Outbox"
},
"homepage.features.outbox-content": {
"message": "Provide out-of-the-box low-code designer components, support easy gateways, and quickly build low-code productivity tools based on low-code engines."
},
"theme.ErrorPageContent.title": {
"message": "This page crashed.",
"description": "The title of the fallback page when the page crashed"
},
"theme.NotFound.title": {
"message": "Page Not Found",
"description": "The title of the 404 page"
},
"theme.NotFound.p1": {
"message": "We could not find what you were looking for.",
"description": "The first paragraph of the 404 page"
},
"theme.NotFound.p2": {
"message": "Please contact the owner of the site that linked you to the original URL and let them know their link is broken.",
"description": "The 2nd paragraph of the 404 page"
},
"theme.admonition.note": {
"message": "note",
"description": "The default label used for the Note admonition (:::note)"
},
"theme.admonition.tip": {
"message": "tip",
"description": "The default label used for the Tip admonition (:::tip)"
},
"theme.admonition.danger": {
"message": "danger",
"description": "The default label used for the Danger admonition (:::danger)"
},
"theme.admonition.info": {
"message": "info",
"description": "The default label used for the Info admonition (:::info)"
},
"theme.admonition.caution": {
"message": "caution",
"description": "The default label used for the Caution admonition (:::caution)"
},
"theme.blog.archive.title": {
"message": "Archive",
"description": "The page & hero title of the blog archive page"
},
"theme.blog.archive.description": {
"message": "Archive",
"description": "The page & hero description of the blog archive page"
},
"theme.BackToTopButton.buttonAriaLabel": {
"message": "Scroll back to top",
"description": "The ARIA label for the back to top button"
},
"theme.blog.paginator.navAriaLabel": {
"message": "Blog list page navigation",
"description": "The ARIA label for the blog pagination"
},
"theme.blog.paginator.newerEntries": {
"message": "Newer Entries",
"description": "The label used to navigate to the newer blog posts page (previous page)"
},
"theme.blog.paginator.olderEntries": {
"message": "Older Entries",
"description": "The label used to navigate to the older blog posts page (next page)"
},
"theme.blog.post.paginator.navAriaLabel": {
"message": "Blog post page navigation",
"description": "The ARIA label for the blog posts pagination"
},
"theme.blog.post.paginator.newerPost": {
"message": "Newer Post",
"description": "The blog post button label to navigate to the newer/previous post"
},
"theme.blog.post.paginator.olderPost": {
"message": "Older Post",
"description": "The blog post button label to navigate to the older/next post"
},
"theme.blog.post.plurals": {
"message": "One post|{count} posts",
"description": "Pluralized label for \"{count} posts\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)"
},
"theme.blog.tagTitle": {
"message": "{nPosts} tagged with \"{tagName}\"",
"description": "The title of the page for a blog tag"
},
"theme.tags.tagsPageLink": {
"message": "View All Tags",
"description": "The label of the link targeting the tag list page"
},
"theme.colorToggle.ariaLabel": {
"message": "Switch between dark and light mode (currently {mode})",
"description": "The ARIA label for the navbar color mode toggle"
},
"theme.colorToggle.ariaLabel.mode.dark": {
"message": "dark mode",
"description": "The name for the dark color mode"
},
"theme.colorToggle.ariaLabel.mode.light": {
"message": "light mode",
"description": "The name for the light color mode"
},
"theme.docs.breadcrumbs.navAriaLabel": {
"message": "Breadcrumbs",
"description": "The ARIA label for the breadcrumbs"
},
"theme.docs.DocCard.categoryDescription": {
"message": "{count} items",
"description": "The default description for a category card in the generated index about how many items this category includes"
},
"theme.docs.paginator.navAriaLabel": {
"message": "Docs pages",
"description": "The ARIA label for the docs pagination"
},
"theme.docs.paginator.previous": {
"message": "Previous",
"description": "The label used to navigate to the previous doc"
},
"theme.docs.paginator.next": {
"message": "Next",
"description": "The label used to navigate to the next doc"
},
"theme.docs.tagDocListPageTitle.nDocsTagged": {
"message": "One doc tagged|{count} docs tagged",
"description": "Pluralized label for \"{count} docs tagged\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)"
},
"theme.docs.tagDocListPageTitle": {
"message": "{nDocsTagged} with \"{tagName}\"",
"description": "The title of the page for a docs tag"
},
"theme.docs.versionBadge.label": {
"message": "Version: {versionLabel}"
},
"theme.docs.versions.unreleasedVersionLabel": {
"message": "This is unreleased documentation for {siteTitle} {versionLabel} version.",
"description": "The label used to tell the user that he's browsing an unreleased doc version"
},
"theme.docs.versions.unmaintainedVersionLabel": {
"message": "This is documentation for {siteTitle} {versionLabel}, which is no longer actively maintained.",
"description": "The label used to tell the user that he's browsing an unmaintained doc version"
},
"theme.docs.versions.latestVersionSuggestionLabel": {
"message": "For up-to-date documentation, see the {latestVersionLink} ({versionLabel}).",
"description": "The label used to tell the user to check the latest version"
},
"theme.docs.versions.latestVersionLinkLabel": {
"message": "latest version",
"description": "The label used for the latest version suggestion link label"
},
"theme.common.editThisPage": {
"message": "Edit this page",
"description": "The link label to edit the current page"
},
"theme.common.headingLinkTitle": {
"message": "Direct link to {heading}",
"description": "Title for link to heading"
},
"theme.lastUpdated.atDate": {
"message": " on {date}",
"description": "The words used to describe on which date a page has been last updated"
},
"theme.lastUpdated.byUser": {
"message": " by {user}",
"description": "The words used to describe by who the page has been last updated"
},
"theme.lastUpdated.lastUpdatedAtBy": {
"message": "Last updated{atDate}{byUser}",
"description": "The sentence used to display when a page has been last updated, and by who"
},
"theme.navbar.mobileVersionsDropdown.label": {
"message": "Versions",
"description": "The label for the navbar versions dropdown on mobile view"
},
"theme.tags.tagsListLabel": {
"message": "Tags:",
"description": "The label alongside a tag list"
},
"theme.AnnouncementBar.closeButtonAriaLabel": {
"message": "Close",
"description": "The ARIA label for close button of announcement bar"
},
"theme.blog.sidebar.navAriaLabel": {
"message": "Blog recent posts navigation",
"description": "The ARIA label for recent posts in the blog sidebar"
},
"theme.CodeBlock.copied": {
"message": "Copied",
"description": "The copied button label on code blocks"
},
"theme.CodeBlock.copyButtonAriaLabel": {
"message": "Copy code to clipboard",
"description": "The ARIA label for copy code blocks button"
},
"theme.CodeBlock.copy": {
"message": "Copy",
"description": "The copy button label on code blocks"
},
"theme.CodeBlock.wordWrapToggle": {
"message": "Toggle word wrap",
"description": "The title attribute for toggle word wrapping button of code block lines"
},
"theme.DocSidebarItem.toggleCollapsedCategoryAriaLabel": {
"message": "Toggle the collapsible sidebar category '{label}'",
"description": "The ARIA label to toggle the collapsible sidebar category"
},
"theme.NavBar.navAriaLabel": {
"message": "Main",
"description": "The ARIA label for the main navigation"
},
"theme.navbar.mobileLanguageDropdown.label": {
"message": "Languages",
"description": "The label for the mobile language switcher dropdown"
},
"theme.TOCCollapsible.toggleButtonLabel": {
"message": "On this page",
"description": "The label used by the button on the collapsible TOC component"
},
"theme.blog.post.readMore": {
"message": "Read More",
"description": "The label used in blog post item excerpts to link to full blog posts"
},
"theme.blog.post.readMoreLabel": {
"message": "Read more about {title}",
"description": "The ARIA label for the link to full blog posts from excerpts"
},
"theme.blog.post.readingTime.plurals": {
"message": "One min read|{readingTime} min read",
"description": "Pluralized label for \"{readingTime} min read\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)"
},
"theme.docs.breadcrumbs.home": {
"message": "Home page",
"description": "The ARIA label for the home page in the breadcrumbs"
},
"theme.docs.sidebar.collapseButtonTitle": {
"message": "Collapse sidebar",
"description": "The title attribute for collapse button of doc sidebar"
},
"theme.docs.sidebar.collapseButtonAriaLabel": {
"message": "Collapse sidebar",
"description": "The title attribute for collapse button of doc sidebar"
},
"theme.docs.sidebar.navAriaLabel": {
"message": "Docs sidebar",
"description": "The ARIA label for the sidebar navigation"
},
"theme.docs.sidebar.closeSidebarButtonAriaLabel": {
"message": "Close navigation bar",
"description": "The ARIA label for close button of mobile sidebar"
},
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": {
"message": "← Back to main menu",
"description": "The label of the back button to return to main menu, inside the mobile navbar sidebar secondary menu (notably used to display the docs sidebar)"
},
"theme.docs.sidebar.toggleSidebarButtonAriaLabel": {
"message": "Toggle navigation bar",
"description": "The ARIA label for hamburger menu button of mobile navigation"
},
"theme.docs.sidebar.expandButtonTitle": {
"message": "Expand sidebar",
"description": "The ARIA label and title attribute for expand button of doc sidebar"
},
"theme.docs.sidebar.expandButtonAriaLabel": {
"message": "Expand sidebar",
"description": "The ARIA label and title attribute for expand button of doc sidebar"
},
"theme.ErrorPageContent.tryAgain": {
"message": "Try again",
"description": "The label of the button to try again rendering when the React error boundary captures an error"
},
"theme.common.skipToMainContent": {
"message": "Skip to main content",
"description": "The skip to content label used for accessibility, allowing to rapidly navigate to main content with keyboard tab/enter navigation"
},
"theme.tags.tagsPageTitle": {
"message": "Tags",
"description": "The title of the tag list page"
},
"homepage.hero.button.playground": {
"message": "Playground"
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

@@ -1,80 +0,0 @@
---
title: Tango LowCode Builder Released!
authors: [wwsun]
tags: [netease, tango, open-source]
---
## 📝 Introduction
Tango is a low-code builder framework for quickly building low-code platforms or tools. With Tango, you only need a few lines of code to build a basic low-code builder. The Tango low-code builder reads the source code of the frontend project directly, executes and renders the frontend view based on the source code, and provides users with low-code visual building capabilities. User's building operations will be translated into modifications to the source code. With low-code tools or platforms built using Tango, it is possible to achieve the effect of "source code in, source code out" and seamlessly integrate with the existing development flow within the enterprise.
### ✨ Features
- After actual testing in the production environment of NetEase Cloud Music, it can be flexibly integrated and applied to low-code platforms, local development tools, and more.
- Driven by source code AST, with no private DSL and protocols.
- Provides real-time code output capability, supports source code input and output.
- Out-of-the-box front-end low-code designer, providing flexible and easy-to-use designer React components.
- Developed using TypeScript, providing complete type definition files.
### 🏗️ Code-based Low-Code Solution
Due to the fact that the engine kernel is completely based on the source code-driven implementation, Tango low-code engine can achieve the visualization building capability of source code input and source code output, without providing any private intermediate products. If the company already has a complete R&D system (code hosting, building, deployment, CDN), Tango low-code engine can be directly used to integrate with existing services to build a low-code development platform.
![engine flow](./engine-flow.png)
### 📄 Code in, Code out
Due to the engine kernel being completely driven by source code, the Tango low-code engine can achieve the visual construction capability of source code in and source code out, without providing any private intermediate products. If the company already has a complete development system (code hosting, building, deployment, CDN), then it can directly use the Tango low-code engine to integrate with existing services to build a low-code development platform.
![code in, code out](./codein-codeout.png)
### 🏆 Advantages
Compared to the low-code building solution based on private schema, Tango low-code engine has the following advantages:
| Comparison Items | Schema-base low-code solution | Tango (Code-based) |
| ------------------------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| Applicable Scenarios | Targeted vertical building scenarios, such as forms, marketing pages, etc. | 🔥 Applicable to application building scenarios centered on source code |
| Language Capabilities | Relies on private protocols for extension, inflexible, and difficult to align with programming language capabilities | 🔥 Based directly on JavaScript language, can use all language features, no extensibility issues |
| Development Capability | LowCode | 🔥 LowCode + ProCode |
| Source Code Export | Centered on schema, one-way code output, irreversible | 🔥 Source code-centered, two-way transcoding |
| Custom Dependencies | Need to be encapsulated according to private protocol extension, high customization cost | 🔥 Existing components can be seamlessly and low-cost integrated |
| Integrated R&D Facilities | High customization cost, additional customization required | 🔥 Low-cost integration, can directly reuse existing deployment and publishing capabilities |
The specific technical architecture is shown in the following figure:
## 📐 Architecture
The Tango low-code engine has been layered and decoupled in implementation, enabling independent development and maintenance of the upper-level low-code platform and the lower-level low-code engine, allowing for fast integration and deployment. In addition, Tango's low-code engine defines an open material ecosystem, allowing developers to freely contribute to extension components and configure property setters that enhance low-code materials.
The specific technical architecture is shown in the following diagram.
![low-code engine](./engine-components.png)
## ⏰ Milestones
Tango low-code engine is the core component of NetEase Cloud Music's internal low-code platform. Open sourcing involves a lot of decoupling work of core logic, which will bring us a lot of additional work in our normal work. Therefore, we plan to promote the open source of Tango low-code engine in stages.
1. Today we officially released the first community version of Tango low-code engine, which will include the core code library of Tango low-code engine, TangoBoot application framework, and low-code component library adapted to antd v4.
2. We plan to release the 1.0 Beta version of the low-code engine on September 30th this year. This version will refactor the core implementation for community scenarios, remove some compatibility code we have internally in Cloud Music, and optimize the implementation of the core.
3. We plan to release the 1.0 RC version of the low-code engine on October 30th this year. This version will ensure that the core API is basically stable and no longer has BREAKING CHANGE. At the same time, we will provide comprehensive and detailed development guides, deployment documents, and demonstration applications.
4. The official version will be released before the end of Q4 2023, and we will further improve our open source community operation mechanism.
![milestones](./tango-milestones.png)
## 🤝 Community
Our open source work is actively advancing, and you can learn about our latest progress through the following information:
- Github Repository: <https://github.com/NetEase/tango>
- Documentation Site: <https://netease.github.io/tango/>
Welcome everyone to join our community and participate in the open source construction of the Tango low-code engine. Any questions can be reported to us through [Github Issues](https://github.com/NetEase/tango/issues), and we will follow up and deal with them promptly.
## 💗 Acknowledgements
Thanks to the Netease Cloud Music Public Technical Team, the Front-End Team, the Live Broadcasting Technical Team, and all the colleagues who participated in the Tango project.
Thanks to the Sandpack project provided by CodeSandbox, which provides powerful browser-based code building and execution capabilities for Tango.
Tanks to the Babel project, which provides powerful javascript ast parsing and traversing capabilities for Tango.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

@@ -1,5 +0,0 @@
wwsun:
name: Wells SUN
title: Core maintainer
url: https://github.com/wwsun
image_url: https://github.com/wwsun.png
@@ -1,14 +0,0 @@
{
"title": {
"message": "Blog",
"description": "The title for the blog used in SEO"
},
"description": {
"message": "Blog",
"description": "The description for the blog used in SEO"
},
"sidebar.title": {
"message": "Recent posts",
"description": "The label for the left sidebar"
}
}
@@ -1,6 +0,0 @@
{
"version.label": {
"message": "Next",
"description": "The label for version current"
}
}
@@ -1,37 +0,0 @@
# Directory Structure
## Specification
在目录结构上,我们约定了一套通用的目录结构
```md
── src
│ +── assets
│ +── pages
│ +── components
│ +── services/index.js
│ +── stores
│ ├── routes.js // route config
│ ├── global.less // global styles
| |-- index.js // app entry
└── package.json
|---tango.config.json // config for low-code engine
```
## Custom Components
This directory is used to store locally developed components, such as:
```jsx
// src/components/sample.jsx
import React from 'react';
import { Box } from '@music163/antd';
export function Sample(props) {
return <Box {...props}>sample component</Box>;
}
```
:::info
Note that if you want the Sample component to be parsed and built by the designer, you must use MusicOne's `Box` component instead of the common `div` element. You can also use `<Box as="span" />` to change the rendered HTML element.
:::
@@ -1,134 +0,0 @@
# App Entry
The framework creates and renders the entire application through `runApp`, and global configurations of the application can be passed in during the creation of the application.
## Configuration
Configure the application globally through `src/index.js`, set up routes, runtime environment, service functions, state models, and more.
```js
import { runApp, tangoBootConfig } from '@music163/tango-boot';
import { message } from '@music163/antd';
// Configure the default display method for error messages that occur during requests, using the message component to display the message overlay.
tangoBootConfig.toast = message;
runApp({
// boot config
boot: {},
// container components
providers: [],
// store instances
stores: {},
// service functions
services: {},
// route config
router: {},
});
```
## App Boot
The `boot` configuration item is used for the startup configuration of the application.
```js
runApp({
boot: {
// react-dom mount element
mountElement: document.querySelector('#root'),
// enable qiankun micro-app support, `qiankun: { appName: string }`
qiankun: false,
},
});
```
## Route Config
`router` configuration item is used to configure the front-end routing of the application, which is implemented based on `react-router`.
```js
import routes from './routes';
runApp({
router: {
// 路由类型: hash | browser
type: 'hash',
// 路由配置信息
config: routes,
// basename,
},
});
```
TangoBoot uses the [Static Routing Configuration Scheme](https://github.com/remix-run/react-router/tree/v5/packages/react-router-config) of ReactRouter. The specific routing configuration file is as follows:
```js
// routes.js
import Index from './pages/index';
import About from './pages/about';
const routes = [
{
path: '/',
exact: true,
component: Index,
},
{
path: '/about',
component: About,
},
{
path: '/user/:id',
component: About,
},
];
export default routes;
```
## Container Components
Sometimes, you may want to wrap the root component with specific container components such as multi-language configuration, state container, etc. Since `runApp` encapsulates the rendering logic of the root component, you can use the `providers` configuration option.
```js
runApp({
// Wrap the instance of the providers component in the order in which it is passed in.
providers: [<ConfigProvider />, <LocaleProvider />],
});
```
## Environment Variables {#env}
Sometimes you may need to customize some environment variables to quickly determine when the application is running in different environments. You can use `tango.env` to quickly obtain the application's environment information. It should be noted that by default there will be no environment information, and you can set the desired environment variables by passing `getEnv` in `runApp` when needed.
For example:
```js
runApp({
// You can return an object with the environment information of the custom application.
getEnv() {
if (location.origin.includes('tango')) {
// tango designer
return 'development';
}
if (location.origin.includes('localhost')) {
// local environment
return 'local';
}
return 'production';
},
});
```
Consume environment variables anywhere, for example:
```jsx
<Box isRender={tango.env === 'development'}>A box only render in development environment</Box>
```
@@ -1,87 +0,0 @@
# Global Variable
TangoBoot provides a global singleton mode by default, which means that you can always use this global singleton to access necessary application runtime information in your project.
:::tip
Singleton means a single instance, also called "singleton pattern," which is a common design pattern. It limits the number of times a class can be instantiated to only once, so that a class has only one instance and provides a global access point to it. In the context of TangoBoot, "singleton" means that your application will always have only one instance of TangoBoot, and you can consistently obtain application-to-runtime information through this single instance.
:::
## Get the global variable
```js
// case1: get from package
import tango from '@music163/tango-boot';
// case2: get from window
window.tango;
// both are the same
console.log(tango === window.tango); // true
```
### Get data from the global `tango` variable
```js
// tango
{
// custom configs
config: {},
// custom helper functions
helpers: {},
// History
history: History;
// service functions
services: {},
// store instances
stores: {},
// builtin helpers
closeModal: Function;
openModal: Function;
setStoreValue: Function;
getStoreValue: Function;
// ...
}
```
## Usage Guide
### Use in store
```jsx
import tango, { defineModel } from '@music163/tango-boot';
export default defineModel(
{
title: 'hello',
async listUsers() {
const data = await tango.services.listUsers();
this.title = data;
},
},
'app'
);
```
### Use in page runtime
```jsx
import tango, { definePage } from '@music163/tango-boot';
function App() {
return (
<Box>
{tango.stores.app.title}
<Button onClick={tango.services.list}>request</Button>
</Box>
);
}
export default definePage(App);
```
@@ -1,130 +0,0 @@
# 常用的工具函数
tango-boot 提供了一组便捷的工具函数,用于应对日常开发过程中的典型操作逻辑。
### `getUser()`
> 从 v1.15.0 开始提供
获取当前的登录用户
```js
const user = tango.getUser();
```
### `checkPrivilege({ appCode, privilegeCode })`
> 从 v1.15.0 开始提供
检查 PMS 权限
```js
const test = await tango.checkPrivilege({ appCode, privilegeCode });
```
### `copyToClipboard(text: string)`
复制文本到剪贴板
```js
tango.copyToClipboard('hello world');
```
### `navigateTo(link: string, data?: object)`
导航到其他前端路由
```js
// 基本用法
tango.navigateTo('/about');
// 带路由参数
tango.navigateTo('/about', { the: 'query' }); // /about?the=query
```
### `showToast(text: string, type: string, time: number)`
- text 消息正文
- type 消息类型 `success | error | warn`
- time 消息展示的时间,单位秒,默认为 3 秒
消息提示
```js
tango.showToast('hello world');
tango.showToast('something is error', 'error', 3);
```
### `openModal(modalId: string)`
根据弹层 ID 唤起目标弹层
```js
tango.openModal('modal123');
```
### `closeModal(modalId: string)`
根据弹层 ID 关闭目标弹层
```js
tango.closeModal('modal123');
```
### `formatDate(date, format)`
- date: `number | string | Moment` 日期值
- format: `string` 格式化,取值参考 https://momentjs.com/docs/#/displaying/format/
格式化日期值
```js
tango.formatDate(1659496225323, 'YYYY-MM-DD HH:mm:ss'); // 2022-08-03 11:10:25
tango.formatDate('2022-11-11', 'X'); // 1668096000
```
### `formatNumber(number, locale, options)`
- number: `number`
- locale: `string`
- options: `Intl.NumberFormatOptions`,参考取值 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat
格式化数字
```js
tango.formatNumber(99999); // 99,999
```
### `getCurrentLanguage`
获取当前语种
```js
tango.getCurrentLanguage(); // en-US
```
### `setLanguage(localeCode)`
设置当前语种
```js
tango.setLanguage('en-US');
```
### `getLanguageList`
获取当前应用可用语种列表
```js
tango.getLanguageList();
```
### `trans(module:key)`
根据多语言 key 获取当前语种文案
```js
tango.trans('common:add'); // 新增
```
@@ -1,24 +0,0 @@
# 国际化
TangoBoot 本身不提供国际化功能,国际化和您的 UI 组件库相关,在云音乐,我们采用的是基于 `@music163/antd` 的 UI 层解决方案。可以借助应用入口的容器配置功能进行国际化配置。
:::tip
如果您还不了解应用入口配置,可以先访问 [应用入口](./app) 一节进行了解。
:::
## 配置示例
```js
import { runApp } from '@music163/tango-boot';
import { ConfigProvider } from '@music163/antd';
import locale from '@music163/antd/es/locale/zh_CN';
import 'moment/dist/locale/zh-cn';
// 设置 moment 的文案
moment.locale('zh-cn');
runApp({
// 设置 UI 组件库的文案配置
providers: [<ConfigProvider locale={locale} />],
});
```
@@ -1,138 +0,0 @@
# TangoBoot
TangoBoot is a frontend application framework that serves the Tango low-code application development system. It provides standard data requests, state management, and routing solutions, as well as generic runtime utility functions, allowing developers to generate single-page applications through simple configuration.
## Application Architecture
The application architecture of TangoBoot uses the View-Model-Service three-layer model. The model layer defines Observable States, the view layer observes the changes of the model and updates automatically, and the service layer is used to create a set of service functions for the consumption of the view layer and the model layer. The diagram is shown in the figure below:
<img
alt="image"
width="600px"
src="https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/13760951704/985c/9706/7f18/be713816a143d3a054f51c9d1fc59b36.png"
/>
## Core API
- `runApp` create the app entry
- `definePage` define reactive views
- `defineStore` define observable states
- `defineServices` define async service functions
### Create app entry
The `index.js` is the app entry filea simple example is:
```jsx
import { runApp } from '@music163/tango-boot';
import routes from './routes';
import services from './services';
import home from './stores/home';
import counter from './stores/counter';
import './global.less';
const { mount, unmount, bootstrap } = runApp({
boot: {
mountElement: document.querySelector('#root'),
qiankun: false,
},
stores: {
home,
counter,
},
services,
router: {
type: 'browser',
config: routes,
},
});
export { mount, unmount, bootstrap };
```
### Create Observable States
Defining a view model through defineStore is very simple, simply declare the state and actions.
```jsx
import { defineStore } from '@music163/tango-boot';
const counter = defineStore({
num: 0,
get() {},
decrement: function () {
counter.num--;
},
increment: () => counter.num++,
});
export default counter;
```
### Create Reactive Views
If the view layer needs to listen for state changes, it only needs to wrap the view component with `definePage`.
```jsx
import React from 'react';
import tango, { definePage } from '@music163/tango-boot';
class App extends React.Component {
increment = () => {
tango.stores.counter.increment();
};
render() {
return (
<div>
<h1>Counter: {tango.stores.counter.num}</h1>
<button type="primary" onClick={this.increment}>
+1
</button>
</div>
);
}
}
export default definePage(App);
```
### Create Service Functions
Use `defineServices` to define your remote apis as service functions.
```jsx
import { defineServices } from '@music163/tango-boot';
export default defineServices({
list: {
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users',
formatter: (res) => {
const { data, message } = res;
return {
code: 200,
list: data,
total: data.length,
message,
};
},
},
add: {
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users',
method: 'post',
},
update: {
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users',
method: 'post',
},
delete: {
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users?id=1',
},
});
```
@@ -1,66 +0,0 @@
# 双向绑定
TangoBoot 支持快捷实现组件与视图双向绑定。
## 借助 `withModel` 实现双向绑定
`withModel` 是 TangoBoot 内置的模型绑定 HOC,支持将组件的内部状态同步到某个 store 定义的状态中。基本的用法如下:
```jsx
class Input extends React.Component {
foo() {}
render() {
return <input {...this.props} />;
}
}
const ModelInput = withModel({
// 设置从组件同步到 store 中的状态值
getValueFromEvent(e: any) {
return e.target.value;
},
})(Input);
```
现在 `ModelInput` 获取了双向绑定的能力,其 `value` 变化后的值将会通过 `onChange` 事件同步给绑定的模型变量
```jsx
const Store = defineStore(
{
name: 'alice',
},
'user'
);
const ModelApp = defineView((props) => {
return (
<div>
<ModelInput model="user.name" />
</div>
);
});
```
## 配置选项
`withModel` 的定义如下:
```jsx
withModel(options)(Component);
```
其中 options 的配置如下:
| 属性名 | 类型 | 默认值 | 说明 |
| ----------------- | ------------------ | ---------- | -------------------------- |
| name | string | | 组件的 displayName |
| valuePropName | string | "value" | 组件的受控值 |
| trigger | string | "onChange" | 组件值变化时的回调函数 |
| getValueFromEvent | `(...args) => any` | | 从回调函数参数中的取值方法 |
借助 `withModel` 增强后,Component 组件将会获得如下属性:
| 属性名 | 类型 | 默认值 | 说明 |
| ------ | ------ | ------ | ------------------------ |
| model | string | | 绑定的模型变量的变量路径 |
@@ -1,41 +0,0 @@
# Pages
TangoBoot 内置提供了一个创建 reactive 视图的模型,用户无需关注复杂的依赖关系,只需要将视图组件使用 `definePage` 包裹即可。
## definePage
基于传递进来的组件创建一个 reactive 视图。reactive 视图会根据 store 数据的变化来自动触发视图重新渲染。例如
```jsx
import React from 'react';
import { definePage, defineStore } from '@music163/tango-boot';
const user = defineStore({ name: 'Bob' });
export default definePage(() => <div>Hello {user.name}!</div>);
```
`definePage` 内部为被包裹的组件实现了一个最佳的 `shouldComponentUpdate` 或 `memo`。
使用 `definePage` 包裹后的组件还会默认接收一些额外的属性,以便于开发者便捷的获取部分状态信息。具体包括:
- `routerData` 路由数据:包括 `params` 路由参数,`query` 查询参数,`pathname` 路径名
- `stores` 模型索引
- `services` 服务索引
## defineView
`defineView` 是一个更加轻量的实现 reactive 视图的包裹器,推荐所有在视图中使用的本地组件都使用它来进行包裹。与 `definePage` 不同的是,他没有额外的属性透传进来。
```jsx
export default defineView(() => (
<div>
<div>Hello {user.name}!</div>
<div>Your first post is: {timeline.posts[0]}</div>
</div>
));
```
:::info
`defineView` 的实现依赖了 [react-easy-state](https://github.com/RisingStack/react-easy-state#creating-reactive-views),感兴趣的同学可以访问对应的链接了解更详细的内容。
:::
@@ -1,33 +0,0 @@
# 组件实例获取
TangoBoot 支持快捷设置和获取组件的实例。
## 借助 `withRef` 注册组件实例
`withRef` 是 TangoBoot 内置的实例注册 HOC,可以快捷的进行组件实例的注册和获取。基本的用法如下:
```tsx
class Input extends React.Component {
foo() {}
render() {
return <input {...this.props} />;
}
}
const ClassInput = withRef()(Input);
```
借助 `withRef` 包裹组件后,当新的组件实例被设置 `id` 属性时,会自动将对应的实例注册到 `tango.refs` 中。例如:
```tsx
export function Basic() {
const inputRef = useRef();
useEffect(() => {
console.log(tango.refs.classInput, inputRef.current);
}, []);
// 设置 id 属性后,会自动组件实例到 `tango.refs` 中
return <ClassInput id="classInput" ref={inputRef} />;
}
```
@@ -1,90 +0,0 @@
# Async Services
TangoBoot provides a configuration-based method for creating data service functions. By using `defineServices`, it is very easy to declare a set of asynchronous service functions for consumption by the view layer or model layer.
## Basic Usage
```js
import { defineServices } from '@music163/tango-boot';
const services = defineServices({
list: {
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users',
},
add: {
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users',
method: 'post',
},
});
```
call the service function:
```js
async () => {
// simple usage
await services.list();
// call with params, payload the the data passed to request body(for 'get' method, will be query parameters), config the axios request config
await services.add(payload, config);
};
```
## Request Config
`defineServices` use [axios](https://github.com/axios/axios) to send requests, so you can pass in the axios request config as the second parameter of the service function.
```js
import { defineServices } from '@music163/tango-boot';
export default defineServices({
list: {
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users',
formatter: (res) => {
const { data, message } = res;
return {
code: 200,
list: data,
total: data.length,
message,
};
},
// you can pass other axios config here
},
});
```
Following is the common configs
- `url` request url
- `method` request methoddefault is `GET`
- `GET`
- `POST`
- `PUT`
- `DELETE`
- `formatter` format the response data
- `requestType` encoding type of request body (content-type), default value is `json`
- `json` By default, serialize JavaScript objects to JSON.
- `x-www-form-urlencoded` equals to `application/x-www-form-urlencoded`
- `headers` request header
- `timeout` timeout of request
more configs should find in [axios documentation](https://axios-http.com/zh/docs/req_config)。
:::tip Encoding Type
By default, TangoBoot serializes JavaScript objects to JSON. To send data in `application/x-www-form-urlencoded` format, you can set the `requestType` to `x-www-form-urlencoded`.
`application/x-www-form-urlencoded` data is encoded as key-value pairs separated by '&', with keys and values separated by '='. Non-alphanumeric characters are [percent-encoded](https://developer.mozilla.org/zh-CN/docs/Glossary/percent-encoding), which is why this type does not support binary data (use multipart/form-data instead).
:::
## Error Messages
The default presentation method of the message when a configuration request error occurs is to use the `message` component for message overlay display. It is recommended to configure this in the application's entry file.
```js
import { tangoBootConfig } from '@music163/tango-boot';
import { message } from '@music163/antd';
tangoBootConfig.toast = message;
```
@@ -1,214 +0,0 @@
# States
TangoBoot 采用了一个轻量级的使用 Reactive 的状态管理方案,开发者可以非常轻松的进行页面状态的管理。一个最基本的示例如下所示:
```js
import React from 'react';
import { defineStore, definePage } from '@music163/tango-boot';
const counter = defineStore({
num: 0,
increment: () => counter.num++,
});
// 当状态变化的时候,视图会自动刷新
export default definePage(() => <button onClick={counter.increment}>{counter.num}</button>);
```
值得注意的是,开发者必须借助 `defineStore` 来定义状态,它会帮助你创建一个 observable 的状态对象。借助 `definePage` 来定义视图,它会帮你订一个 reactive 的视图组件,并且在自动监听状态的变化,按需进行 UI 更新。
## 创建状态模型 Stores
基本语法为 `defineStore(storeObject: object, namespace?: string)`
- `storeObject` 为原始的状态对象
- `namespace` 为该 Store 对应的命名空间,如果你定义了,可以借助 `tango.stores[namespace]` 来获取该引用
一个基本的例子如下:
```js
import { defineStore } from '@music163/tango-boot';
const user = defineStore({ name: 'Rick' });
// stores behave like normal JS objects
user.name = 'Bob';
```
### 复杂的数据结构
```js
// stores can include any valid JS structure
// including nested data, arrays, Maps, Sets, getters, setters, inheritance, ...
const user = defineStore({
profile: {
firstName: 'Bob',
lastName: 'Smith',
get name() {
return `${user.profile.firstName} ${user.profile.lastName}`;
},
},
hobbies: ['programming', 'sports'],
friends: new Map(),
});
// stores may be mutated in any syntactically valid way
user.profile.firstName = 'Bob';
delete user.profile.lastName;
user.hobbies.push('reading');
user.friends.set('id', otherUser);
```
### 异步方法
```js
const userStore = defineStore({
user: {},
async fetchUser() {
userStore.user = await fetch('/user');
// or use this
// this.user = await fetch('/user');
},
});
```
### 多个 Store
useStore.js
```js
import { store } from '@music163/tango-boot';
const userStore = defineStore(
{
user: {},
async fetchUser() {
userStore.user = await fetch('/user');
},
},
'userStore',
);
export default userStore;
```
recipesStore.js
```js
import tango, { store } from '@music163/tango-boot';
import userStore from './userStore';
const recipesStore = defineStore(
{
recipes: [],
// 方式1:直接用调用
async fetchRecipes() {
recipesStore.recipes = await fetch(`/recipes?user=${userStore.user.id}`);
},
// 方法2:使用 this 调用,仅支持 method property,不支持箭头函数
async fetchRecipes2() {
this.recipes = await fetch(`/recipes?user=${userStore.user.id}`);
},
// 方法3:使用 tango 全局变量调用,你也可以使用这种方式引入其他的 store
async fetchRecipes3() {
tango.stores.recipesStore.recipes = await fetch(
`/recipes?user=${tango.stores.userStore.user.id}`,
);
},
},
'recipesStore',
);
export default recipesStore;
```
## 创建 Reactive 视图
你可以借助 `definePage` 创建视图,也可以直接包裹您的原始组件。基本用法为 `definePage(Component)`,借助 `definePage` 可以帮你自动监听状态的变化,并在变化时自动触发视图的重新渲染。
一个简单的例子如下:
```js
import React from 'react';
import { view, store } from '@music163/tango-boot';
// this is a global state store
const user = defineStore({ name: 'Bob' });
// this is re-rendered whenever user.name changes
export default definePage(() => (
<div>
<input value={user.name} onChange={(ev) => (user.name = ev.target.value)} />
<div>Hello {user.name}!</div>
</div>
));
```
## 实现视图与状态双向绑定
TangoBoot 提供了一个名为 `withModel` 的 [HOC](https://zh-hans.reactjs.org/docs/higher-order-components.html),可以借助它来实现视图与状态的双向绑定。
使用方法如下:
```js
withModel(options)(BaseComponent);
```
### 参数配置
其中可配置的 options 参数包括:
- `name` 用于配置包裹后组件的 displayName,可选
- `valuePropName` 绑定的 value 属性名,用于实现双向绑定的受控逻辑,默认为 `value`
- `trigger` 设置收集字段值变更的时机,默认为 `onChange`
- `getValueFromEvent` 设置从事件回调中获取 value 值的方法,默认为 `val => val`,直接返回 trigger 的第一个参数
嵌套 `whitModel` 后,组件将会获得两个新增的属性:
- `model` 用于绑定 Store 中的状态
- `innerRef` 用于获取内部组件的 [ref](https://zh-hans.reactjs.org/docs/refs-and-the-dom.html) 引用
### 双向绑定示例
基本用法如下:
```jsx
import tango, { withModel, defineView, defineStore } from '@music163/tango-boot';
// 定义一个基本的 Input 组件
class Input extends React.Component {
foo() {}
render() {
return <input {...this.props} />;
}
}
// 实现双向绑定
const ModelInput = withModel({
getValueFromEvent(e) {
return e.target.value;
},
})(Input);
// 定义一个 Store
defineStore(
{
name: 'alice',
},
'user',
);
// 双向绑定验证
const ModelApp = defineView((props) => {
return (
<div>
<ModelInput model="user.name" />
<div>{tango.stores.user.name}</div>
</div>
);
});
```
@@ -1,86 +0,0 @@
# tango.config.json
`tango.config.json` is the core configuration file of the Tango project. When your project uses Tango designer for online development, it will read this file to initialize the online development environment.
## entry
Not required. `entry` is the entry file of the project, used for the initialization of the online designer. The default value is `/src/index.js`.
## packages
The npm package dependency information used in the project is used to define the type of dependency and external dependency resources. This configuration item is a map, where the key is the corresponding npm package name, and the value is the configuration information for the dependency.
The dependency type `type` can be `baseDependency`, which is the basic dependency of the project used to obtain the project's basic material list information; it can also be `bizDependency`, which is the business component dependency used to obtain the business component list merged into the material panel business component menu; it can also be `dependency`, which is other dependencies of the project, used only to provide external information and version information, and does not have any relationship with the material panel.
When the external dependency resource `resources` and its corresponding global variable `library` are configured, when these dependencies are imported, the relevant resources will not be packaged into the bundle, but the extension dependencies will be obtained externally at runtime. This is equivalent to the previous `sandbox.externalResources` and `sandbox.externals` configuration items. In addition, the version number in `resource` can be replaced with `{{version}}`, which will be replaced with the version number at the time of platform construction. Specifically, `designerResources` can be defined for adapting to the platform's designer view.
```json
{
"react": {
"version": "17.0.2",
"library": "React",
"type": "dependency",
"resources": ["https://unpkg.com/react@{{version}}/umd/react.development.js"]
},
"react-dom": {
"version": "17.0.2",
"library": "ReactDOM",
"type": "dependency",
"resources": ["https://unpkg.com/react-dom@{{version}}/umd/react-dom.development.js"]
},
"@music163/tango-boot": {
"version": "0.1.3",
"library": "TangoBoot",
"type": "baseDependency",
"resources": ["https://unpkg.com/@music163/tango-boot@{{version}}/dist/boot.js"],
"description": "tango app runtime framework"
},
"@music163/antd": {
"version": "0.1.2",
"library": "TangoAntd",
"type": "baseDependency",
"resources": [
"https://unpkg.com/@music163/antd/{{version}}/dist/index.js",
"https://unpkg.com/@music163/antd/{{version}}/dist/index.css"
],
"description": "component library for tango apps",
"designerResources": [
"https://unpkg.com/@music163/antd/{{version}}/dist/designer.js",
"https://unpkg.com/@music163/antd/{{version}}/dist/index.css"
]
},
"@music163/biz-comp": {
"type": "bizDependency",
"version": "0.0.2"
}
}
```
:::tip
Since the dependencies are injected into the sandbox according to the order defined, it is necessary to ensure the order of dependencies in `packages`. Otherwise, it may cause the application to fail to load dependencies (for example, placing `react` after the component package will cause the component package to fail to load normally).
:::
## externalResources
In addition to the external dependency resources defined by the `packages` mentioned above, you can define a list of external dependency resources that are not npm packages and are introduced when the sandbox is started. These can be static files such as `js` or `css`. For example:
```json
{
"externalResources": ["https://tango.netease.io/script.js"]
}
```
## proxy
Not required. Proxy rules for the project.
```json
{
"proxy": {
"/api/pms": {
"target": "http://cms.qa.igame.163.com",
"changeOrigin": true
}
}
}
```
@@ -1,58 +0,0 @@
# 设计器扩展概览
主要介绍如何扩展 tango 设计器。tango 设计器提供了三个部位的自定义扩展能力,分别是标题栏,工具栏,和侧边栏。可以通过下面这张图进行简要的了解:
<img src="https://p5.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/13359732115/c5ca/c5ae/bbd4/852daf849ac39162baf3bc3a985de6de.png" />
## 标题栏扩展
`DesignerPanel` 提供了 logo, description, actions 三个扩展点,分别对应于 平台标识、应用描述、行动点区域,开发者可以按照需求进行扩展。例如:
```jsx
<DesignerPanel logo={<Logo />} description={<ProjectDetail />} actions={<Box>新版沙箱</Box>}>
</SidebarPanel>
```
## 工具栏扩展
工具栏面板支持任意组合和设置渲染的位置,开发者可以通过 `ToolbarPanel.Item` 添加自定义的工具栏选项,例如下面的代码扩展了一个 newPage 按钮,用于支持在设计器中创建新页面的需求。
```jsx
<ToolbarPanel>
<ToolbarPanel.Item key="history" />
<ToolbarPanel.Item key="viewportRefresh" />
<ToolbarPanel.Item key="preview" />
<ToolbarPanel.Item key="routeSwitch" />
<ToolbarPanel.Item key="newPage">
<Button
onClick={() => {
const { name, code } = genDefaultPage(index);
engine.workspace.addViewPage(`${name}${index++}`, code);
message.success('页面新建成功');
}}
>
添加新页面
</Button>
</ToolbarPanel.Item>
<ToolbarPanel.Item key="modeSwitch" placement="center" />
<ToolbarPanel.Item key="viewportSwitch" placement="right" />
</ToolbarPanel>
```
## 侧边栏扩展
侧边栏面板支持任意组合和调换顺序,用户可以通过 `SidebarPanel.Item` 组件进行自定义扩展,默认情况下根据一些内置的 key 标识,设计器会自动渲染对应的面板,用户也可以根据自己的需求采用自定义渲染子节点的方案进行扩展。
例如,下面的示例代码中扩展了一个 “自定义面板”。
```jsx
<SidebarPanel>
<SidebarPanel.Item key="components" />
<SidebarPanel.Item key="outline" />
<SidebarPanel.Item key="dataSource" />
<SidebarPanel.Item key="dependency" />
<SidebarPanel.Item key="history" />
<SidebarPanel.Item key="custom" title="自定义面板" icon={<BulbOutlined />}>
<Box p="m">这里是一个自定义的面板,你可以任意添加</Box>
</SidebarPanel.Item>
</SidebarPanel>
```
@@ -1,67 +0,0 @@
# 外部数据源
如果您的扩展部件需要使用外部数据源,可以参考本文的做法。
## 存在部件共享
如果数据源需要在多个扩展部件中共享,我们推荐您借助 tango 的 `remoteServices` 规范进行接口的实现。
### 外部数据源定义
例如,我们需要调用一组图片素材的接口,此时我们可以定义一个 `ImageService`,如下:
```tsx
import { createServices } from '@music/request';
export const remoteServices = {
ImageService: createServices(
{
listMy: {
url: '/my/upload/list',
},
listFav: {
url: '/my/star/list',
},
listPub: {
url: '/list',
},
},
{
baseURL: 'https://febase-openapi.fn.netease.com/deer/api/deer/pic',
withCredentials: false, // 解决跨域时必须非*问题
}
),
};
```
### 外部数据源引入
我们在设计器初始化的时候可以通过 `remoteServices` 属性进行外部数据源的传入。
```tsx
<Designer remoteServices={remoteServices}></Designer>
```
### 在设计器扩展中使用定义的外部数据源
```tsx
import { useRemoteServices } from '@music163/tango-designer';
export function CustomWidget() {
const remoteServices = useRemoteServices();
}
```
## 仅在单个组件
如果您的外部数据服务没有额外的共享需求,您也可以直接在组件内部进行数据服务的发起。
```tsx
import request from '@music/request';
export function CustomWidget() {
useEffect(() => {
request('//some.domain/get');
}, []);
}
```
@@ -1,141 +0,0 @@
---
sidebar_position: 3
---
# 物料接入
Tango 提供了低成本的物料接入方式,支持直接复用现有的组件体系。只需要在现有组件包基础上简单的提供配置说明问题,即可轻松接入到 Tango 低代码生态体系中。
import Link from '@docusaurus/Link';
## 预览视图和设计器视图
- 预览视图:是组件默认的渲染模式,通常我们只需要关注此视图行为。
- 设计器视图:为低代码平台定制的渲染视图,当我们需要自定义组件在 Tango 设计器中的部分渲染行为时,可以通过修改设计器视图的渲染逻辑实现。
### 设计器视图和 `withDnd`
通常情况下我们不需要太关注于设计器视图,只需通过 `withDnd` 简单的进行包装即可。例如:
```jsx
// 将 antd Button 包装一层提供设计器视图
export Button = withDnd({
name: 'Button', // 必须,组件的名字,用来正确设置组件的 displayName
isFunctionComponent: true, // 可选,如果包裹的组件为函数组件,可以在此设置
overrideProps: {}, // 可选,覆盖掉包裹组件的默认属性值
})(AntButton);
```
默认情况下 withDnd 会在组件外层包裹一层 dnd 容器,以便于组件能够在设计器中被拖拽:
- draggable 属性表示该区域可以被拖拽
- data-dnd 用来追踪渲染的 dom 元素
```jsx
<div className="dnd-wrapper" draggable data-dnd="button:123">
<button>hello</button>
</div>
```
### 自定义拖拽容器
由于默认情况下拖拽容器是一个 `div` 节点,为[块级元素](https://www.w3schools.com/cssref/pr_class_display.asp)。部分情况下,你可能期望它渲染为一个[行内容器](https://www.w3schools.com/cssref/pr_class_display.asp),此时可以通过如下方式配置:
```jsx
export Button = withDnd({
name: 'Button',
display: 'inline-block', // 将 dnd 容器渲染为行内元素
wrapperStyle: {}, // 传入 dnd 容器的自定义样式
})(AntButton);
```
### 禁用拖拽容器
某些情况下,你的组件可能会检查子节点的有效性,此时你可能不期望组件被包装额外的容器节点,此时你可以关闭拖拽容器,但你需要保证你的组件能够接收父级元素传入的属性信息并附加到组件的跟节点上,否则将无法正确的设置组件的 dnd 信息。
```jsx
export Button = withDnd({
name: 'Button',
hasWrapper: false, // 禁用拖拽容器
})(MyButton);
const MyButton = (props) => {
const { children, ...rest } = props;
// 此时,需要保证你的组件能够正确的将多余的属性透传到组件的根节点上
return <button {...rest}>{children}</button>
}
```
## 组件包接入
组件包一般包含多个组件导出,例如 `@music163/antd` 就是典型的组件包,它面向中后台场景提供统一的物料层解决方案。组件包接入 Tango 体系,无需修改现有的代码实现,只需要在此基础上提供额外的配置文件即可。如下所示:
```
- src
- button
- designer.tsx # 设计器视图,可以不提供
- index.ts # 默认视图出口
- prototype.ts # 组件可配置描述协议
- index.ts
- prototypes.ts
- designer.ts
```
参考示例:https://g.hz.netease.com/NeteaseMusicUI/music-one/-/tree/master/packages/components/src/action
### 设计器视图的实现
对大部分组件而言,都不需要提供特定的设计器视图,只需要使用 tango 提供的 dnd hoc 进行简单的包裹导出即可。
```tsx
import { Button as Base, ButtonProps } from 'antd';
import { withDnd } from '@music/tango-apps-shared';
export const Button = withDnd<HTMLDivElement, ButtonProps<any>>({
name: 'Button',
})(Base);
```
### 编写 prototypes 文件
可以参考 <Link to="/docs/protocol/material-protocol">物料协议</Link> 编写该文档
### 属性设置器选择
参考属性设置器文档进行选择。
### 可配置项的验证
TangoApps 提供了 SettingFormPlayground 组件用来测试组件的可配置能力,可以直接在组件文档中加入可配置测试示例,例如在 storybook 中进行测试。
```jsx
import React from 'react';
import { SettingFormPlayground } from '@music/tango-apps-setting-form';
import { Input, prototypes } from '@music163/antd';
export default {
title: 'Prototype/Input',
};
export function Basic() {
return (
<SettingFormPlayground prototype={prototypes.Input}>
<Input />
</SettingFormPlayground>
);
}
```
SettingFormPlayground 组件支持实时预览配置结果,预览效果如下:
<img src="https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/13140796879/edbb/5dad/a330/013a98940e487956b605e1e533f54371.png" />
## 单个业务组件接入
单个组件接入可以参考组件包的接入方案。
:::info
考虑到单组件形态的特点,在云音乐业务场景中,TangoStudio 将会与红石物料中心对接,提供业务组件的自动化接入能力,目前该能力正在开发过程中。
:::
@@ -1,9 +0,0 @@
# DesignerPanel 设计器布局容器
设计器的主框架提供了一个基本的低代码设计器的布局容器,你可以很轻松的通过此布局容器来实现自己的设计器布局。
## 属性列表
import TypesTable from '@site/src/components/TypesTable';
<TypesTable name="DesignerPanelProps" />
@@ -1,56 +0,0 @@
# Designer 设计器容器
设计器根节点,注入全局状态。
## 属性列表
import TypesTable from '@site/src/components/TypesTable';
<TypesTable name="DesignerProps" />
## 设计器初始化
```jsx
<Designer engine={engine} sandboxQuery={sandboxQuery} remoteServices={remoteServices}></Designer>
```
## engine
设计器引擎实例,用于管理设计器的核心状态。
### 基本的初始化方式
```js
const engine = createEngine({
entry: '/src/index.js',
files: sampleFiles,
componentPrototypes: prototypes as any,
});
```
### 自定义 workspace 的初始化方式
默认情况下,引擎采用的是源码解析模式,即将源码解析为 ast 树,后续的搭建逻辑转为对 ast 树的操作。如果你想自定义搭建逻辑,可以通过自定义 workspace 的方式来实现。
引擎在 2.0 本本中提供了新的 JsonWorkspace 来支持自定义搭建逻辑,JsonWorkspace 采用的是 json 格式的数据结构,你可以通过自定义 json 来实现自定义搭建逻辑。
```js
const engine = createEngine({
workspace: new JsonWorkspace({
prototypes: prototypes as any,
files: schemaFiles,
}),
});
```
:::tip
按照这种方式,你可以自定义自己的 Workspace 实现。具体可以参考 Workspace 的实现标准。
:::
## sandboxQuery
沙箱的查询实例,用于向沙箱注册 dom 查询能力。
## remoteServices
远程服务实例,用于注册全局共享的数据服务实例。
@@ -1,27 +0,0 @@
# Hooks 钩子方法
设计器提供了一组 Hooks 用于快速获取设计器的各种状态。
import TypesTable from '@site/src/components/TypesTable';
## useWorkspace
获取工作区状态。
```js
// App 需要放置在 Designer 容器中
function App() {
const workspace = useWorkspace();
// do what you want
}
```
## useDesigner
```js
// App 需要放置在 Designer 容器中
function App() {
const designer = useDesigner();
// do what you want
}
```
@@ -1,9 +0,0 @@
# Sandbox 沙箱
设计器的运行时沙箱,用来执行代码,渲染页面。
import TypesTable from '@site/src/components/TypesTable';
默认沙箱是一个基于 CodeSandbox 的沙箱实例,可以直接在浏览器端执行代码。
<TypesTable name="SandboxProps" />
@@ -1,9 +0,0 @@
# SettingPanel 属性设置面板
属性设置器用于在配置面板中展示特定配置项的配置逻辑。
## 属性列表
import TypesTable from '@site/src/components/TypesTable';
<TypesTable name="SettingPanelProps" />
@@ -1,38 +0,0 @@
# SidebarPanel 侧边栏面板
侧边栏面板提供了一个简单易用的主操作面板,可以将一些高频操作和核心部件放置到侧边栏面板中,方便用户快速操作。
## 属性列表
import TypesTable from '@site/src/components/TypesTable';
<TypesTable name="SidebarPanelProps" />
## 创建浮动面板
借助 `isFloat` 和 `width` 属性可以创建浮动自定义宽度面板,脱离框架对侧边栏的宽度限制。
```jsx
<SidebarPanel.Item key="history" isFloat width="40vw" />
```
## 徽标提示
`showBadge` 属性可以设置工具栏是否显示徽标提示,用于某些面板需要对外展现需要被用户注意的时机。
<img
alt="img"
src="https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/18224398190/2847/b86d/df55/c817e86a0b1b3a6b08f7cc98362caec5.png"
width="300px"
/>
```jsx
<SidebarPanel.Item
key="info"
label="消息"
title="消息列表"
icon={<NotificationOutlined />}
showBadge={{ count: 2 }}>
<Box>info panel</Box>
</SidebarPanel.Item>
```
@@ -1,10 +0,0 @@
# ViewPanel 主视图面板
设计器的主视图面板,用来放置设计器的主视图,包括:沙箱,编辑器,预览等。主视图可以放置多个,但只有一个会在激活状态。
## 属性列表
import TypesTable from '@site/src/components/TypesTable';
<TypesTable name="ViewPanelProps" />
@@ -1,3 +0,0 @@
# WorkspacePanel 工作区面板
主工作区的布局容器,用于放置工作区的多重视图。
@@ -1,71 +0,0 @@
import Link from '@docusaurus/Link';
# Quick Start
Tango is a front-end framework for quickly building low-code tools. With Tango, you can quickly build a basic low-code designer with just a small amount of code. This article will help you use Tango to implement a basic low-code visual designer.
:::warning Disclaimer
The low-code platform based on the Tango low-code engine has already served many internal business development scenarios within NetEase Cloud Music. Due to the large amount of code separation work involved in open source work, the current open source version is an alpha demo version of the low-code engine, and the API may be unstable. It is not recommended to use it in a production environment. We expect to release a production-ready version in Q4 2023.
:::
## Environment
- Install node >= 16.0.0 : <https://nodejs.org/en/download/>
- Install npm 8.x or yarn 1.x: <https://classic.yarnpkg.com/en/docs/getting-started>
## Start Tango Playground
1. clone the repository
```sh
git clone https://github.com/NetEase/tango.git
```
2. add the local development domain to the hosts file.
```hosts
127.0.0.1 local.netease.com
```
3. install dependencies and run the Tango Playground
```sh
yarn install
yarn start
```
4. open your browser to run <https://local.netease.com:6006> , should allow insecure certificates.
## Run the local sandbox
In addition to starting the playground of Tango, local development also requires a usable sandbox environment to host the designer.
If there is no available online sandbox, you can complete the development by starting a sandbox locally; if there is already an online sandbox and the sandbox shares the top-level domain name with the local playground, please directly jump to step 7 and modify `bundlerURL` to the address of the online sandbox.
1. clone the repository
```sh
git clone https://github.com/NetEase/codesandbox-client.git
```
2. update `Caddyfile`change `:8080` to the domain name of playground appe.g. `local.netease.com`
3. add the domain names to the `hosts` file
```hosts
127.0.0.1 local.netease.com
```
4. install dependencies and build the sandbox
```sh
yarn install
yarn build:deps
yarn build:sandpack
```
Or you can download prebuild assets from [releases](https://github.com/NetEase/codesandbox-client/releases). Create a folder `www` next to `Caddyfile`, and extract compressed tarball into `www/`.
5. If you do not have Caddy locally, you can go to <https://caddyserver.com/download> to download it.
```sh
caddy run
```
6. Open your browser to visit <https://local.netease.com:8443> , also allow insecure certificates
7. update `apps/playground/src/pages/index.tsx`, find `<Sandbox />` component, change `bundlerURL` to `https://local.netease.com:8443`
```jsx
<Sandbox bundlerURL="https://local.netease.com:8443" />
```
8. Go back to the playground page and refresh. Now, the playground will use the sandbox you specified to run the designer.
If you have Docker locally, you can modify steps 4-5 above to use Docker to build and launch the image.
```sh
docker build -t tango-codesandbox .
docker run -p 8443:8443 tango-codesandbox
```
@@ -1,81 +0,0 @@
# 属性设置器
属性设置器用于在配置面板中展示特定配置项的配置逻辑。Tango 内置了多种标准的属性设置器,对于一些特殊场景,内置的属性设置器可能无法满足你的需要,此时开发者可以扩展自己的属性设置。
## 内置属性设置器
| 设置器名 | 接收值类型 | 设置器说明 | 可配置项 |
| ----------------- | ------------------------------------ | -------------------------- | ------------------ |
| eventSetter | expression | 动作函数或事件监听器设置器 | |
| actionListSetter | | ActionList 的 value 设置器 | |
| boolSetter | boolean | 布尔值设置器 | |
| choiceSetter | `{ label: string, value: string }[]` | RadioGroup 设置器 | - options 选项列表 |
| pickerSetter | `{ label: string, value: string }[]` | 下拉值设置器 | - options 选项列表 |
| colorSetter | string | 色彩 | |
| dateSetter | string | 日期设置器 | |
| dateRangeSetter | `string[]` | 日期范围设置器 | |
| timeSetter | `string` | 时间设置器 | |
| timeRangeSetter | `string[]` | 时间范围设置器 | |
| expressionSetter | expression | 表达式设置器 | |
| jsonSetter | json expression | JSON 表达式设置器 | |
| jsxSetter | jsx expression | JSX 设置器 | |
| iconSetter | string | Icon 组件设置器 | |
| iconTypeSetter | string | Icon 组件类型设置器 | |
| numberSetter | number | 数字类型设置器 | |
| textSetter | string | 文本设置器 | |
| textAreaSetter | string | 文本域设置器 | |
| modelSetter | string | 变量绑定设置器 | |
| enumSetter | enum | 枚举值设置器 | |
| sliderSetter | number | 滑块设置器 | |
| listSetter | `object[]` | 列表值设置器 | |
| renderPropsSetter | Function | render props 设置器 | |
| imageSetter | string | 云鹿图片设置器 | |
## 注册自定义属性设置器
### 属性设置器的实现标准
属性设置器组件是一个标准的受控型表单组件,开发者所开发的组件只需要暴露 `value` 和 `onChange(value)` 两个属性让属性面板的表单容器进行控制即可。
例如,我们可以实现一个简单的字符串设置器 StringSetter,其实现如下:
```jsx
function StringSetter({ value, onChange, ...rest }) {
const handleChange = (e) => {
onChange(e.target.value);
};
return <input value={value} onChange={handleChange} />;
}
```
### 注册属性设置器
注册属性设置器非常简单,只需要借助设置器暴露出来的 `registerSetter` 即可。
```jsx
import { registerSetter } from '@music163/tango-designer';
// 注册自定义 setter
registerSetter({
name: 'stringSetter',
component: StringSetter,
});
```
### 使用自定义的属性设置器
在组件的物料协议中直接声明对应的属性设置器名称即可。例如:
```jsx
const prototype = {
name: 'PageHeader',
title: 'PageHeader',
props: [
{
name: 'title',
title: '标题',
setter: 'stringSetter',
},
],
};
```
@@ -1,35 +0,0 @@
import Link from '@docusaurus/Link';
# Introduction
Tango is a low-code designer framework for quickly building low-code platforms. With Tango, you only need a few lines of code to build a basic low-code platform frontend system. The Tango low-code designer **directly reads the source code of the frontend project, executes and renders the frontend view based on the source code, and provides users with low-code visual building capabilities. User building operations will be converted into modifications to the code**. With Tango, **source code goes in, source code comes out**.
## Architecture
The Tango low-code engine has been layered and decoupled in implementation, allowing the upper-level low-code platform and the lower-level low-code engine to be developed and maintained independently for rapid integration and deployment. On the other hand, the Tango low-code engine defines an open material ecosystem, allowing developers to freely contribute extension component configuration attribute setters and expand low-code materials to second and third-party business components.
<img src="https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/18236990116/aabf/8a16/28ee/f0fd29a84c7aa40bb26383cdef12b88c.png" />
## Builder Components
Tango low-code engine designer is used to low-cost initialization of a basic low-code platform. Its front-end mainly includes the following parts:
- Designer main framework: for external framework initialization, state management, drag-and-drop engine binding, and other core logic.
- Sidebar panel: provides expandable sidebar panels, in addition to built-in universal panels, users can also add custom panels.
- Property setting panel: property setter panel that supports user property configuration.
- Runtime sandbox: used for rendering the designer runtime view.
- Web IDE: used for online source code editing.
<img src="https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/30108642346/b8cf/e86d/ef5a/514d90b722b5d8dc0e18516ed594a07b.png" />
## Source Code based Low-Code Kernel
The Tango low-code engine does not rely on private setup protocols and DSL, but directly uses source code to drive it. The engine internally converts the source code to AST, and all the user's setup operations are converted to traversal and modification of AST, and then the AST is regenerated as code and synchronized to the online sandbox for execution. Compared with [traditional schema-based low-code solutions](https://mp.weixin.qq.com/s/yqYey76qLGYPfDtpGkVFfA), which are limited by private DSL and protocols, it can perfectly integrate low-code setup with source code development.
<img src="https://p5.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/13140534982/ee2e/f42c/cc9a/184e2918a011b57d46e6c64a2722fa44.png" />
## Code in, Code out
Due to the engine kernel being completely based on source code-driven implementation, Tango's low-code engine is able to achieve visual building capabilities with source code input and output, without providing any proprietary intermediate products. As a result, the online development capabilities built by Tango can seamlessly connect and integrate with the team's existing development services (code hosting, building, deployment, CDN).
<img src="https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/13208022400/b809/b82e/77b0/5e4fe78a8f11c8ed89c9ec9ced43e845.png" />
@@ -1,136 +0,0 @@
# Package Specification
## 目录结构
```txt
+ src
+ button
- view.tsx // 默认视图文件
- index.ts // 渲染视图入口文件
- designer.ts // 设计器视图入口文件
- prototype.ts // 组件描述文件
+ date-picker
- index.ts // 组件包默认入口文件
- designer.ts // 组件包设计器视图入口文件
```
## 组件包入口
### 默认入口
`src/index.ts` 是组件包的默认出口文件。
### 设计视图入口
`src/designer.ts` 是组件包的设计视图出口文件。您至少应该导出 `menuData` 和 `prototypes` 两个模块:
```ts
export { Button } from './button';
export { Card } from './card';
//...
// 组件的配置描述列表
export const prototypes = [
{
title: '按钮',
name: 'Button',
props: [
{
name: 'size',
setter: 'textSetter',
},
//...
],
},
//...
];
export const menuData = {
// 常用组件
common: [
{
title: '基本',
items: ['Button'],
},
],
};
```
其中 `menuData` 的 key 可选列表如下:
| key | 分类标题 |
| ------- | -------- |
| common | 常用组件 |
| atom | 原子组件 |
| snippet | 组合 |
| block | 区块 |
## 组件入口
### 组件默认视图文件
`src/button/view.tsx`
```tsx
export function Button({ children, ...rest }) {
return <button {...rest}>{children}</button>;
}
```
需要注意的是,你需要将组件多余的属性 `...rest` 透传给跟结点 `<button {...rest}>`,以便于在设计器中时组件能够接收到跟踪信息。
### 组件默认入口文件
`src/button/index.ts`
```ts
export * from './view';
```
### 组件设计器视图文件
设计器视图是您的组件在 tango 设计器中进行展示的视图,大部分情况下,您无需关注此逻辑,特殊情况下,您的组件可能需要在设计器下有不一样的展现和数据透出,此时可以定义该视图文件实现。
`src/button/designer.tsx`
```tsx
import { withDnd } from '@music/tango-apps-shared';
import { Button as ButtonBase } from './view';
export const Button = withDnd({
// 组件名
name: 'Button',
// 是否在设计器中有包裹层,用于设置 dnd 的追踪信息
hasWrapper: false,
})(ButtonBase);
```
### 组件描述文件
`src/button/prototype.ts`
```ts
import { ComponentPrototypeType } from '@music/tango-apps-shared';
export const Button: ComponentPrototypeType = {
title: '按钮',
name: 'Button',
exportType: 'namedExport',
icon: 'icon-anniu',
type: 'element',
package: '@music163/antd',
hasChildren: false,
props: [
{
name: 'children',
title: '文案',
setter: 'textSetter',
initValue: '按钮',
},
],
};
```
## 依赖说明
如果您的组件会视图组件,推荐您的组件基于 `@music163/antd` 或 `@music163/antd` 的最新版本进行开发,且不要依赖其他 UI 组件包,例如 `antd`, `fusion` 等。
@@ -1,148 +0,0 @@
# Component Specification
import TypesTable from '@site/src/components/TypesTable';
The material agreement defines the basic information of a component, including the name, properties, drag and drop rules, etc. The material agreement is an additional description file attached to the component, which will not invade your component code. Therefore, you can add a material agreement to the component without changing the component code.
## 组件原型 prototype
<TypesTable name="ComponentPrototypeType" />
## 组件属性 ComponentPropType
<TypesTable name="ComponentPropType" />
### 嵌套属性
当某个组件属性为嵌套属性时,例如 `<Table scroll={{ x: 800, y: 400 }} />`,此时可以描述如下:
```jsx
{
name: 'Table',
props: [
{
name: 'scroll',
props: [
{
name: 'x',
setter: 'numberSetter',
},
{
name: 'y',
setter: 'numberSetter',
}
],
}
],
}
```
### 属性的关联展示
当某个属性依赖某个特定的其他属性值时,可以借助 `getVisible` 实现关联展示控制:
```js
{
name: 'Button',
props: [
{
name: 'shape',
},
{
name: 'buttonType',
getVisible: (form) => {
// 配置项 buttonType 仅在配置项 shape 的值为 button 时才展示
return form.getValue('shape') === 'button';
},
},
];
}
```
### 动态属性设置
当组件需要依据某个属性值进行动态设置其他属性时,可以借助 `getProp` 实现控制:
```js
{
name: 'FormItem',
props: [
{
name: 'component',
title: '控件类型',
},
{
name: 'componentProps',
title: '子组件属性',
getProp(form) {
const type = form.getValue('component');
const proto = { ...componentMap[type] };
const props = omitProps(proto.props, [
'placeholder',
'options',
'onChange',
'defaultValue',
'value',
]);
return {
title: proto.title + '属性',
props,
};
},
}
],
}
```
### 属性的输入提示
当某个属性在输入时需要进行输入提示时,例如某个函数属性需要提示其签名的模版,则可以借助属性输入提示实现:
```js
{
name: 'TableColumn',
props: [
{
name: 'render',
setter: 'expressionSetter',
autoCompleteOptions: ['(value, record, index) => { return null; }'],
}
],
}
```
![img](https://p5.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/18917075100/9569/09fa/bc62/c418d3de8694ce93977450fb725bb524.gif)
## 组件拖拽规则 ComponentDndRulesType
<TypesTable name="ComponentDndRulesType" />
### canDrag/canDrop
在 onDragStart 的时候执行。
```js
export const Page = {
name: 'Page',
rules: {
canDrag() {
return false;
},
},
};
```
### canMoveIn/canMoveOut
在 onDragEnter 的时候执行。
```js
export const Modal = {
name: 'Modal',
rules: {
canMoveIn(incomingName) {
return !(incomingName === Modal.name);
},
},
};
```
@@ -1,42 +0,0 @@
{
"link.title.文档": {
"message": "Documentation",
"description": "The title of the footer links column with title=文档 in the footer"
},
"link.title.社区": {
"message": "Community",
"description": "The title of the footer links column with title=社区 in the footer"
},
"link.title.更多": {
"message": "More",
"description": "The title of the footer links column with title=更多 in the footer"
},
"link.item.label.快速开始": {
"message": "Quick Start",
"description": "The label of footer link with label=快速开始 linking to /docs/designer/quick-start"
},
"link.item.label.Stack Overflow": {
"message": "Stack Overflow",
"description": "The label of footer link with label=Stack Overflow linking to https://stackoverflow.com/questions/tagged/netease-tango"
},
"link.item.label.Discord": {
"message": "Discord",
"description": "The label of footer link with label=Discord linking to https://discordapp.com/invite/docusaurus"
},
"link.item.label.博客": {
"message": "Blog",
"description": "The label of footer link with label=博客 linking to /blog"
},
"link.item.label.GitHub": {
"message": "GitHub",
"description": "The label of footer link with label=GitHub linking to https://github.com/netease/tango"
},
"copyright": {
"message": "Copyright © 2023 NetEase Cloud Music, Inc.",
"description": "The footer copyright"
},
"logo.alt": {
"message": "NetEase Cloud Music",
"description": "The alt text of footer logo"
}
}
@@ -1,30 +0,0 @@
{
"logo.alt": {
"message": "Tango Logo",
"description": "The alt text of navbar logo"
},
"item.label.引擎文档": {
"message": "Builder",
"description": "Navbar item with label builder"
},
"item.label.应用框架": {
"message": "Boot",
"description": "Navbar item with label framework"
},
"item.label.协议": {
"message": "Protocol",
"description": "Navbar item with label protocol"
},
"item.label.博客": {
"message": "Blog",
"description": "Navbar item with label blog"
},
"item.label.演示应用": {
"message": "Playground",
"description": "Navbar item with label playground"
},
"item.label.GitHub": {
"message": "GitHub",
"description": "Navbar item with label GitHub"
}
}
-45
View File
@@ -1,45 +0,0 @@
{
"name": "website",
"version": "0.0.0",
"private": true,
"scripts": {
"docusaurus": "docusaurus",
"start": "docusaurus start",
"build": "docusaurus build",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
"clear": "docusaurus clear",
"serve": "docusaurus serve",
"write-translations": "docusaurus write-translations",
"write-heading-ids": "docusaurus write-heading-ids",
"typecheck": "tsc"
},
"dependencies": {
"@docusaurus/core": "2.4.1",
"@docusaurus/preset-classic": "2.4.1",
"@mdx-js/react": "^1.6.22",
"clsx": "^1.2.1",
"prism-react-renderer": "^1.3.5",
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "2.4.1",
"@tsconfig/docusaurus": "^1.0.5"
},
"browserslist": {
"production": [
">0.5%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"engines": {
"node": ">=16.14"
}
}
-68
View File
@@ -1,68 +0,0 @@
/**
* Creating a sidebar enables you to:
- create an ordered group of docs
- render a sidebar for each doc of that group
- provide next/previous navigation
The sidebars can be generated from the filesystem, or explicitly defined here.
Create as many sidebars as you want.
*/
// @ts-check
/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
const sidebars = {
// By default, Docusaurus generates a sidebar from the docs folder structure
// tutorialSidebar: [{ type: 'autogenerated', dirName: '.' }],
// But you can create a sidebar manually
designer: [
'intro',
'designer/quick-start',
{
type: 'category',
label: '接入指南',
items: ['designer/deploy/designer', 'designer/deploy/sandbox', 'designer/deploy/server'],
collapsed: false,
},
{
type: 'category',
label: '设计器自定义',
items: [
'designer/customize/panels',
'designer/customize/tools',
'designer/customize/sidebar',
'designer/customize/setters',
'designer/customize/editor',
'designer/customize/components',
],
collapsed: false,
},
{
type: 'category',
label: '设计原理',
items: ['designer/design/overview', 'designer/design/filesystem', 'designer/design/sandbox'],
collapsed: false,
},
],
boot: [
'boot/intro',
'boot/app-spec',
'boot/tango-config',
'boot/app',
'boot/service',
'boot/store',
'boot/page',
'boot/global',
// 'boot/helpers',
// 'boot/model',
// 'boot/ref',
// 'boot/i18n',
],
// protocol: ['protocol/material-protocol', 'protocol/material-package-spec'],
};
module.exports = sidebars;
-29
View File
@@ -1,29 +0,0 @@
import React, { useState } from 'react';
export interface AlertProps {
type?: 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info';
closeable?: boolean;
children: React.ReactNode;
}
export function Alert({ type = 'primary', closeable, children }: AlertProps) {
const [visible, setVisible] = useState(true);
if (!visible) {
return <div className="alert" role="alert" />;
}
return (
<div className={`alert alert--${type}`} role="alert">
{closeable && (
<button
aria-label="Close"
className="clean-btn close"
type="button"
onClick={() => setVisible(false)}
>
<span aria-hidden="true">&times;</span>
</button>
)}
{children}
</div>
);
}
@@ -1,68 +0,0 @@
import React from 'react';
import clsx from 'clsx';
import styles from './styles.module.css';
import Translate, { translate } from '@docusaurus/Translate';
interface FeatureItem {
title: string;
Svg: React.ComponentType<React.ComponentProps<'svg'>>;
description: JSX.Element;
}
const FeatureList: FeatureItem[] = [
{
title: translate({ id: 'homepage.features.code-driven', message: '源码驱动' }),
Svg: require('@site/static/img/html-d.svg').default,
description: (
<Translate id="homepage.features.code-driven-content">
</Translate>
),
},
{
title: translate({ id: 'homepage.features.low-code', message: '实时出码' }),
Svg: require('@site/static/img/digital-content-4.svg').default,
description: (
<Translate id="homepage.features.low-code-content">
</Translate>
),
},
{
title: translate({ id: 'homepage.features.outbox', message: '开箱即用' }),
Svg: require('@site/static/img/cpu-f.svg').default,
description: (
<Translate id="homepage.features.outbox-content">
</Translate>
),
},
];
function Feature({ title, Svg, description }: FeatureItem) {
return (
<div className={clsx('col col--4')}>
<div className="text--center">
<Svg className={styles.featureSvg} role="img" />
</div>
<div className="text--center padding-horiz--md">
<h3>{title}</h3>
<p>{description}</p>
</div>
</div>
);
}
export default function HomepageFeatures(): JSX.Element {
return (
<section className={styles.features}>
<div className="container">
<div className="row">
{FeatureList.map((props, idx) => (
<Feature key={idx} {...props} />
))}
</div>
</div>
</section>
);
}
@@ -1,12 +0,0 @@
.features {
border-top: 1px solid var(--ifm-color-emphasis-300);
display: flex;
align-items: center;
padding: 5rem 0;
width: 100%;
}
.featureSvg {
height: 200px;
width: 200px;
}
-79
View File
@@ -1,79 +0,0 @@
import React from 'react';
import styles from './index.module.css';
import { translate } from '@docusaurus/Translate';
const timelines = [
{
icon: null,
title: 'Alpha',
date: '2024.01.31',
description: translate({
id: 'homepage.timeline.alpha',
message: '核心 API 重构完成,文档内容优化',
}),
},
{
icon: null,
title: '1.0 RC',
date: '2024.04.30',
description: translate({
id: 'homepage.timeline.rc',
message: '核心 API 基本稳定,能力完善。',
}),
},
{
icon: null,
title: '1.0',
date: 'Before 2024.12.31',
description: translate({
id: 'homepage.timeline.stable',
message: '1.0 正式版常规迭代',
}),
},
];
export default function HomepageTimeline() {
return <Timeline items={timelines} />;
}
export interface TimelineProps {
items: TimelineItemProps[];
}
export function Timeline({ items = [] }: TimelineProps) {
return (
<div className={styles.homepageSection}>
<div className="container">
<div className={styles.homepageSectionTitle}>Timelines</div>
<div className="row">
{items.map((item) => (
<TimelineItem key={item.date} {...item} />
))}
</div>
</div>
</div>
);
}
interface TimelineItemProps {
icon: React.ReactNode;
title: string;
date: string;
description?: string;
}
function TimelineItem({ icon, title, date, description }: TimelineItemProps) {
return (
<div className="col">
<div className={styles.timelineItemStyle}>
<div className={styles.timelineDot}>{icon}</div>
<div className={styles.timelineLine} />
</div>
<div className="margin-top--md">
<h3>{title}</h3>
<time>{date}</time>
<p>{description}</p>
</div>
</div>
);
}
-181
View File
@@ -1,181 +0,0 @@
import React from 'react';
// import data from './tango-types.json';
const data: any = {};
const typeMap = {};
if (data && data.children) {
data.children.forEach((group) => {
group.children.forEach((item) => {
if (item.name) {
typeMap[item.name] = item;
}
});
});
}
const getType = (type: any = {}) => {
let ret;
switch (type.type) {
case 'union':
ret = 'union';
break;
case 'array':
ret = `${getType(type.elementType)}[]`;
break;
default:
ret = type.name;
break;
}
return ret;
};
// function normalizeText(text) {
// return text?.replaceAll('\n', '<br />');
// }
export function Line({ html }: any) {
return <div dangerouslySetInnerHTML={{ __html: html }} />;
}
const typeTableColumns = [
{ dataIndex: 'name', title: '属性名' },
{
dataIndex: 'type',
title: '类型',
render(val, record) {
if (record.kindString === 'Method') {
return 'function';
}
return getType(record.type || {});
},
},
{
dataIndex: 'flags.isOptional',
title: '是否必选',
render(val, record) {
return record.flags.isOptional ? null : '必填';
},
},
{
dataIndex: 'comment',
title: '说明',
width: '40%',
render(val) {
if (!val) {
return null;
}
return (
<div>
<Line
html={
val.summary
? val.summary
.map((item) => {
if (item.kind === 'text') {
return item.text.replaceAll('\n', '<br />');
}
return '';
})
.join('<br />')
: undefined
}
/>
{/* <Line
html={
val.blockTags
? val.blockTags
.map((item) =>
item.content?.map((tag) => tag?.text).join('<br />')
)
.join('<br />')
: null
}
/> */}
</div>
);
},
},
];
export default function TypeTable(props: any) {
const { name } = props;
const detail = typeMap[name];
if (!detail) {
return <div style={{ border: '1px solid red', color: 'red' }}>can not find {name}</div>;
}
let dataSource = [];
if (detail.type?.type === 'reflection') {
dataSource = detail.type.declaration.children;
} else {
dataSource = detail.children;
}
return (
<SimpleTable title={name} dataSource={dataSource} columns={typeTableColumns} rowKey="id" />
);
}
function SimpleTable({ title, dataSource, columns, rowKey }: any) {
return (
<table className="SimpleTable" style={{ width: '100%' }}>
{title && <caption>{title}</caption>}
{renderTableHead(columns)}
{renderTableBody(dataSource, columns, rowKey)}
</table>
);
}
function renderTableHead(columns: any[] = []) {
return (
<thead>
<tr>
{columns.map((column) => (
<th key={column.dataIndex} style={{ width: column.width }}>
{column.title}
</th>
))}
</tr>
</thead>
);
}
function renderTableBody(dataSource, columns, rowKey) {
return (
<tbody>
{dataSource.map((rowData) => {
if (!rowData) {
return null;
}
return (
<tr key={rowData[rowKey]}>
{columns.map((column, rowIndex) => (
<td key={column.dataIndex}>{renderTableCell(column, rowData, rowIndex)}</td>
))}
</tr>
);
})}
</tbody>
);
}
function renderTableCell({ dataIndex, render }, rowData, rowIndex) {
const val = getValueByPath(rowData, dataIndex);
if (typeof render === 'function') {
return render(val, rowData, rowIndex);
}
return val;
}
function getValueByPath(obj, path) {
if (!path) {
return;
}
const paths = path.split('.');
return paths.reduce((prev, cur) => {
return prev[cur];
}, obj);
}
@@ -1,30 +0,0 @@
.homepageSection {
border-top: 1px solid var(--ifm-color-emphasis-300);
background-color: var(--ifm-color-emphasis-100);
padding: 5rem 0;
}
.homepageSectionTitle {
font-size: 2rem;
text-align: center;
margin-bottom: 2rem;
}
.timelineItemStyle {
display: flex;
align-items: center;
column-gap: 0.5rem;
}
.timelineDot {
background-color: var(--ifm-color-emphasis-300);
width: 24px;
height: 24px;
border-radius: 24px;
}
.timelineLine {
background-color: var(--ifm-color-emphasis-300);
width: 100%;
height: 2px;
}
File diff suppressed because it is too large Load Diff
-30
View File
@@ -1,30 +0,0 @@
/**
* Any CSS included here will be global. The classic template
* bundles Infima by default. Infima is a CSS framework designed to
* work well for content-centric websites.
*/
/* You can override the default Infima variables here. */
:root {
--ifm-color-primary: #2e8555;
--ifm-color-primary-dark: #29784c;
--ifm-color-primary-darker: #277148;
--ifm-color-primary-darkest: #205d3b;
--ifm-color-primary-light: #33925d;
--ifm-color-primary-lighter: #359962;
--ifm-color-primary-lightest: #3cad6e;
--ifm-code-font-size: 95%;
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
}
/* For readability concerns, you should choose a lighter palette in dark mode. */
[data-theme='dark'] {
--ifm-color-primary: #25c2a0;
--ifm-color-primary-dark: #21af90;
--ifm-color-primary-darker: #1fa588;
--ifm-color-primary-darkest: #1a8870;
--ifm-color-primary-light: #29d5b0;
--ifm-color-primary-lighter: #32d8b4;
--ifm-color-primary-lightest: #4fddbf;
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
}
-35
View File
@@ -1,35 +0,0 @@
/**
* CSS files with the .module.css suffix will be treated as CSS modules
* and scoped locally.
*/
.heroBanner {
padding: 4rem 0;
text-align: center;
position: relative;
overflow: hidden;
}
.heroImageBox {
margin-top: 4rem;
}
.heroImage {
width: 100%;
border-radius: var(--ifm-global-radius);
border: 1px solid var(--ifm-color-emphasis-300);
box-shadow: var(--ifm-global-shadow-md);
}
@media screen and (max-width: 996px) {
.heroBanner {
padding: 2rem;
}
}
.buttons {
display: flex;
align-items: center;
justify-content: center;
column-gap: 12px;
}
-64
View File
@@ -1,64 +0,0 @@
import React from 'react';
import clsx from 'clsx';
import Link from '@docusaurus/Link';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import Layout from '@theme/Layout';
import HomepageFeatures from '@site/src/components/HomepageFeatures';
import HomepageTimeline from '@site/src/components/Timeline';
import { translate } from '@docusaurus/Translate';
import styles from './index.module.css';
function HomepageHeader() {
const { siteConfig } = useDocusaurusContext();
return (
<header className={clsx('hero', styles.heroBanner)}>
<div className="container">
<h1 className="hero__title">
{translate({
id: 'homepage.hero.title',
message: siteConfig.title,
})}
</h1>
<p className="hero__subtitle">
{translate({ id: 'homepage.hero.tagline', message: siteConfig.tagline })}
</p>
<div className={styles.buttons}>
<Link className="button button--primary button--lg" to="/docs/intro">
{translate({
id: 'homepage.hero.button.document',
message: '快速开始',
})}
</Link>
<Link
className="button button--secondary button--lg"
to="https://tango-demo.musicfe.com/designer/"
>
{translate({ id: 'homepage.hero.button.playground', message: '演示应用' })}
</Link>
</div>
<div className={styles.heroImageBox}>
<img
className={styles.heroImage}
src="https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/30108642346/b8cf/e86d/ef5a/514d90b722b5d8dc0e18516ed594a07b.png"
alt="preview"
/>
d
</div>
</div>
</header>
);
}
export default function Home(): JSX.Element {
const { siteConfig } = useDocusaurusContext();
return (
<Layout title={siteConfig.title} description="Description will go into a meta tag in <head />">
<HomepageHeader />
<main>
<HomepageTimeline />
<HomepageFeatures />
</main>
</Layout>
);
}
View File
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 19 KiB

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 300" data-imageid="digital-content-4" imageName="Coding" class="illustrations_image" style="width: 261px;"><g id="_87_coding_outline" data-name="#87_coding_outline"><path d="M289.32,90.26V223.54a3.65,3.65,0,0,1-3.65,3.65H130.85a3.65,3.65,0,0,1-3.65-3.65V90.26a3.65,3.65,0,0,1,3.65-3.66H285.67A3.65,3.65,0,0,1,289.32,90.26Z" fill="#2e8555" class="target-color"/><path d="M285.67,228.19H130.85a4.66,4.66,0,0,1-4.65-4.65V90.26a4.66,4.66,0,0,1,4.65-4.66H285.67a4.66,4.66,0,0,1,4.65,4.66V223.54A4.65,4.65,0,0,1,285.67,228.19ZM130.85,87.6a2.66,2.66,0,0,0-2.65,2.66V223.54a2.65,2.65,0,0,0,2.65,2.65H285.67a2.65,2.65,0,0,0,2.65-2.65V90.26a2.66,2.66,0,0,0-2.65-2.66Z" fill="#093f68"/><path d="M272.8,76.47V209.75a3.65,3.65,0,0,1-3.65,3.65H114.33a3.65,3.65,0,0,1-3.65-3.65V76.47a3.65,3.65,0,0,1,3.65-3.66H269.15A3.65,3.65,0,0,1,272.8,76.47Z" fill="#fff"/><path d="M269.15,214.4H114.33a4.65,4.65,0,0,1-4.65-4.65V76.47a4.66,4.66,0,0,1,4.65-4.66H269.15a4.66,4.66,0,0,1,4.65,4.66V209.75A4.65,4.65,0,0,1,269.15,214.4ZM114.33,73.81a2.66,2.66,0,0,0-2.65,2.66V209.75a2.65,2.65,0,0,0,2.65,2.65H269.15a2.65,2.65,0,0,0,2.65-2.65V76.47a2.66,2.66,0,0,0-2.65-2.66Z" fill="#093f68"/><path d="M272.8,76.47V209.75a3.65,3.65,0,0,1-3.65,3.65h-103V72.81h103A3.65,3.65,0,0,1,272.8,76.47Z" fill="#093f68"/><path d="M269.15,214.4h-103a1,1,0,0,1-1-1V72.81a1,1,0,0,1,1-1h103a4.66,4.66,0,0,1,4.65,4.66V209.75A4.65,4.65,0,0,1,269.15,214.4Zm-102-2h102a2.65,2.65,0,0,0,2.65-2.65V76.47a2.66,2.66,0,0,0-2.65-2.66h-102Z" fill="#093f68"/><path d="M272.8,76.47v15H110.68v-15a3.65,3.65,0,0,1,3.65-3.66H269.15A3.65,3.65,0,0,1,272.8,76.47Z" fill="#fff"/><path d="M272.8,92.48H110.68a1,1,0,0,1-1-1v-15a4.66,4.66,0,0,1,4.65-4.66H269.15a4.66,4.66,0,0,1,4.65,4.66v15A1,1,0,0,1,272.8,92.48Zm-161.12-2H271.8v-14a2.66,2.66,0,0,0-2.65-2.66H114.33a2.66,2.66,0,0,0-2.65,2.66Z" fill="#093f68"/><circle cx="123.19" cy="81.63" r="3.38" fill="#f56132"/><path d="M123.19,86a4.38,4.38,0,1,1,4.38-4.38A4.39,4.39,0,0,1,123.19,86Zm0-6.76a2.38,2.38,0,1,0,2.38,2.38A2.39,2.39,0,0,0,123.19,79.25Z" fill="#093f68"/><circle cx="136.84" cy="81.63" r="3.38" fill="#ffbc0e"/><path d="M136.84,86a4.38,4.38,0,1,1,4.37-4.38A4.39,4.39,0,0,1,136.84,86Zm0-6.76a2.38,2.38,0,1,0,2.37,2.38A2.38,2.38,0,0,0,136.84,79.25Z" fill="#093f68"/><circle cx="150.48" cy="81.63" r="3.38" fill="#70cc40"/><path d="M150.48,86a4.38,4.38,0,1,1,4.38-4.38A4.39,4.39,0,0,1,150.48,86Zm0-6.76a2.38,2.38,0,1,0,2.38,2.38A2.39,2.39,0,0,0,150.48,79.25Z" fill="#093f68"/><path d="M148.3,104.19H120.73a1,1,0,0,1,0-2H148.3a1,1,0,0,1,0,2Z" fill="#dfeaef"/><path d="M153,116.59H129.79a1,1,0,0,1,0-2H153a1,1,0,0,1,0,2Z" fill="#dfeaef"/><path d="M156.18,129h-19.3a1,1,0,0,1,0-2h19.3a1,1,0,0,1,0,2Z" fill="#dfeaef"/><path d="M149.88,141.41H125.46a1,1,0,0,1,0-2h24.42a1,1,0,0,1,0,2Z" fill="#dfeaef"/><path d="M143.31,153.81H120.73a1,1,0,0,1,0-2h22.58a1,1,0,0,1,0,2Z" fill="#dfeaef"/><path d="M148.3,166.22h-20a1,1,0,0,1,0-2h20a1,1,0,0,1,0,2Z" fill="#dfeaef"/><path d="M141.41,178.62H120.73a1,1,0,0,1,0-2h20.68a1,1,0,0,1,0,2Z" fill="#dfeaef"/><path d="M148.3,191h-20a1,1,0,0,1,0-2h20a1,1,0,0,1,0,2Z" fill="#dfeaef"/><path d="M193.62,104.19h-16.8a1,1,0,0,1,0-2h16.8a1,1,0,0,1,0,2Z" fill="#fff"/><path d="M216.82,116.59H182.34a1,1,0,0,1,0-2h34.48a1,1,0,0,1,0,2Z" fill="#ffbc0e"/><path d="M208.26,129H182.34a1,1,0,0,1,0-2h25.92a1,1,0,0,1,0,2Z" fill="#ffbc0e"/><path d="M194.58,141.41H179.7a1,1,0,0,1,0-2h14.88a1,1,0,0,1,0,2Z" fill="#fff"/><path d="M200.5,153.81H183.7a1,1,0,0,1,0-2h16.8a1,1,0,0,1,0,2Z" fill="#f56132"/><path d="M209.06,166.22H183.12a1,1,0,0,1,0-2h25.94a1,1,0,0,1,0,2Z" fill="#fff"/><path d="M189.42,178.62h-12.6a1,1,0,0,1,0-2h12.6a1,1,0,0,1,0,2Z" fill="#fff"/><path d="M209.06,191h-27.6a1,1,0,0,1,0-2h27.6a1,1,0,0,1,0,2Z" fill="#70cc40"/><path d="M235.5,191H217.38a1,1,0,0,1,0-2H235.5a1,1,0,0,1,0,2Z" fill="#ffbc0e"/><path d="M229.62,129h-12.8a1,1,0,0,1,0-2h12.8a1,1,0,0,1,0,2Z" fill="#fff"/><path d="M247.87,116.59H223.22a1,1,0,0,1,0-2h24.65a1,1,0,0,1,0,2Z" fill="#2e8555" class="target-color"/><path d="M243.79,129h-7.21a1,1,0,0,1,0-2h7.21a1,1,0,0,1,0,2Z" fill="#f56132"/><path d="M212.66,153.81h-7.2a1,1,0,1,1,0-2h7.2a1,1,0,1,1,0,2Z" fill="#f56132"/><path d="M247.87,153.81H218.58a1,1,0,1,1,0-2h29.29a1,1,0,0,1,0,2Z" fill="#ffbc0e"/><path d="M223.22,166.22h-6.4a1,1,0,0,1,0-2h6.4a1,1,0,0,1,0,2Z" fill="#2e8555" class="target-color"/><path d="M247.87,191h-4.08a1,1,0,0,1,0-2h4.08a1,1,0,0,1,0,2Z" fill="#fff"/></g></svg>

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 300" data-imageid="html-d" imageName="Javascript" class="illustrations_image" style="width: 261px;"><g id="_181_javascript_outline" data-name="#181_javascript_outline"><rect x="144.72" y="69.25" width="130.1" height="153.64" rx="10.96" fill="#2e8555" class="target-color"/><path d="M263.86,223.89H155.67a12,12,0,0,1-11.95-11.95V80.21a12,12,0,0,1,11.95-12H263.86a12,12,0,0,1,12,12V211.94A12,12,0,0,1,263.86,223.89ZM155.67,70.25a10,10,0,0,0-9.95,10V211.94a10,10,0,0,0,9.95,9.95H263.86a10,10,0,0,0,10-9.95V80.21a10,10,0,0,0-10-10Z" fill="#093f68"/><path d="M257.86,230.75H141.93a7.08,7.08,0,0,1-7.08-7.08V84.75a7.08,7.08,0,0,1,7.08-7.08h93.81l29.2,27.19V223.67A7.08,7.08,0,0,1,257.86,230.75Z" fill="#fff"/><path d="M257.86,231.75H141.93a8.09,8.09,0,0,1-8.08-8.08V84.75a8.09,8.09,0,0,1,8.08-8.08h93.81a1,1,0,0,1,.68.27l29.21,27.19a1,1,0,0,1,.31.73V223.67A8.09,8.09,0,0,1,257.86,231.75ZM141.93,78.67a6.09,6.09,0,0,0-6.08,6.08V223.67a6.09,6.09,0,0,0,6.08,6.08H257.86a6.08,6.08,0,0,0,6.08-6.08V105.3l-28.6-26.63Z" fill="#093f68"/><path d="M235.74,77.67V99.06a5.46,5.46,0,0,0,5.39,5.47l23.81.33Z" fill="#2e8555" class="target-color"/><path d="M264.94,105.86h0l-23.81-.33a6.49,6.49,0,0,1-6.38-6.47V77.67a1,1,0,0,1,1.68-.73l29.21,27.19a1,1,0,0,1,.24,1.1A1,1,0,0,1,264.94,105.86ZM236.74,80V99.06a4.47,4.47,0,0,0,4.4,4.47l21.22.29Z" fill="#093f68"/><path d="M182.76,164.08,164,172.64v.14l18.78,8.56v9.39l-30-13.67v-8.7l30-13.68Z" fill="#093f68"/><path d="M182.76,191.73a.85.85,0,0,1-.41-.09l-30-13.67a1,1,0,0,1-.58-.91v-8.7a1,1,0,0,1,.58-.91l30-13.68a1,1,0,0,1,1.41.91v9.4a1,1,0,0,1-.58.91l-17,7.72,17,7.72a1,1,0,0,1,.58.91v9.39a1,1,0,0,1-.46.85A1,1,0,0,1,182.76,191.73Zm-29-15.31,28,12.76V182l-18.2-8.29a1,1,0,0,1-.58-.91v-.14a1,1,0,0,1,.58-.91l18.2-8.3v-7.19l-28,12.76Zm29-12.34h0Z" fill="#093f68"/><path d="M205,141.08l6.76,2.41L194,197.3l-6.7-2.42Z" fill="#093f68"/><path d="M194,198.3a1,1,0,0,1-.34-.06l-6.7-2.42a1,1,0,0,1-.61-1.25l17.72-53.81a1,1,0,0,1,.51-.58,1,1,0,0,1,.78,0l6.75,2.41a1,1,0,0,1,.62,1.26L195,197.61a1,1,0,0,1-.51.59A1.13,1.13,0,0,1,194,198.3Zm-5.44-4,4.81,1.74,17.15-51.9-4.87-1.75Z" fill="#093f68"/><path d="M216.36,181.34l18.78-8.56v-.14l-18.78-8.56v-9.4l30,13.68v8.7l-30,13.67Z" fill="#093f68"/><path d="M216.36,191.73a1,1,0,0,1-.54-.15,1,1,0,0,1-.46-.85v-9.39a1,1,0,0,1,.58-.91l16.94-7.72L215.94,165a1,1,0,0,1-.58-.91v-9.4a1,1,0,0,1,1.41-.91l30,13.68a1,1,0,0,1,.58.91v8.7a1,1,0,0,1-.58.91l-30,13.67A.85.85,0,0,1,216.36,191.73Zm1-9.75v7.2l28-12.76V169l-28-12.76v7.19l18.2,8.3a1,1,0,0,1,.58.91v.14a1,1,0,0,1-.58.91Z" fill="#093f68"/><rect x="120.98" y="92.53" width="71.35" height="44.25" rx="2.13" fill="#2e8555" class="target-color"/><path d="M190.2,137.78H123.11a3.13,3.13,0,0,1-3.13-3.12v-40a3.14,3.14,0,0,1,3.13-3.13H190.2a3.13,3.13,0,0,1,3.13,3.13v40A3.13,3.13,0,0,1,190.2,137.78ZM123.11,93.53A1.14,1.14,0,0,0,122,94.66v40a1.13,1.13,0,0,0,1.13,1.12H190.2a1.12,1.12,0,0,0,1.13-1.12v-40a1.13,1.13,0,0,0-1.13-1.13Z" fill="#093f68"/><path d="M151.53,122.88a7.29,7.29,0,0,1-1.07,2.59,6,6,0,0,1-2.18,2,7.46,7.46,0,0,1-3.61.77,7.3,7.3,0,0,1-4.6-1.43,6.78,6.78,0,0,1-2.46-3.67l4.64-1.56a2.7,2.7,0,0,0,.78,1.38,2,2,0,0,0,1.4.52,1.69,1.69,0,0,0,1.7-.89,5.49,5.49,0,0,0,.42-2.33V105.87h5.28v14.3A13.07,13.07,0,0,1,151.53,122.88Z" fill="#093f68"/><path d="M167.67,111.52a4.69,4.69,0,0,0-1.77-1.37,5,5,0,0,0-2.1-.51,6.44,6.44,0,0,0-1.06.1,2.81,2.81,0,0,0-1,.35,2.31,2.31,0,0,0-.72.67,1.84,1.84,0,0,0-.29,1.06,1.78,1.78,0,0,0,.23.93,2.06,2.06,0,0,0,.67.64,7,7,0,0,0,1.06.51c.41.15.87.31,1.39.47.73.25,1.5.52,2.3.82a8.49,8.49,0,0,1,2.18,1.18,6.3,6.3,0,0,1,1.62,1.82,5.29,5.29,0,0,1,.65,2.72,7.1,7.1,0,0,1-.69,3.24,6.51,6.51,0,0,1-1.86,2.26,8.14,8.14,0,0,1-2.67,1.32,11.49,11.49,0,0,1-3.1.43,12.59,12.59,0,0,1-4.51-.82A9.59,9.59,0,0,1,154.4,125l3.44-3.5a6.32,6.32,0,0,0,2.11,1.64,5.66,5.66,0,0,0,2.59.66,5.38,5.38,0,0,0,1.14-.12,3.08,3.08,0,0,0,1-.4,2,2,0,0,0,.66-.74,2.28,2.28,0,0,0,.25-1.11,1.71,1.71,0,0,0-.31-1,2.9,2.9,0,0,0-.87-.78A7,7,0,0,0,163,119l-1.92-.63a19.51,19.51,0,0,1-2-.79,7,7,0,0,1-1.78-1.19,5.49,5.49,0,0,1-1.26-1.75,6,6,0,0,1-.47-2.5,6.16,6.16,0,0,1,.73-3.1,6.57,6.57,0,0,1,1.94-2.12,8.49,8.49,0,0,1,2.7-1.21,12.06,12.06,0,0,1,3-.38,11.27,11.27,0,0,1,3.76.67,9.66,9.66,0,0,1,3.36,2Z" fill="#093f68"/><path d="M191.53,101.63H121a1,1,0,0,1,0-2h70.55a1,1,0,0,1,0,2Z" fill="#093f68"/><path d="M131.06,98.11h-5.12a1,1,0,0,1,0-2h5.12a1,1,0,0,1,0,2Z" fill="#093f68"/><path d="M257.68,212.94h-113a1,1,0,1,1,0-2h113a1,1,0,0,1,0,2Z" fill="#dfeaef"/></g></svg>

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Some files were not shown because too many files have changed in this diff Show More