From 9620367366e55b037269dedf11e3f89f49a2b9b1 Mon Sep 17 00:00:00 2001 From: wuduoyi Date: Mon, 8 Jun 2020 18:11:26 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=B8=80=E4=B8=8B=E6=96=87?= =?UTF-8?q?=E6=A1=A3=EF=BC=8C=E5=AF=B9=E6=96=B0=E4=BA=BA=E6=9B=B4=E5=8F=8B?= =?UTF-8?q?=E5=A5=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../english/README.md => README-en.md | 36 +- README.md | 21 +- build/md-parser.js | 306 +++++++------ docs/api.md | 121 +++-- docs/basic.md | 22 +- docs/custom.md | 433 ++++++++++++++++++ docs/dev.md | 189 -------- docs/getting_started.md | 207 +++++---- docs/intro.md | 198 ++++++++ docs/renderers.md | 4 +- docs/renderers/Action.md | 4 +- docs/sdk.md | 336 -------------- docs/style.md | 6 +- examples/components/App.jsx | 9 +- examples/components/Doc.jsx | 30 +- examples/components/SchemaRender.jsx | 1 + examples/index.html | 111 +++-- examples/sdk-placeholder.html | 37 +- package.json | 2 +- 19 files changed, 1132 insertions(+), 941 deletions(-) rename docs/languages/english/README.md => README-en.md (50%) create mode 100644 docs/custom.md delete mode 100644 docs/dev.md create mode 100644 docs/intro.md delete mode 100644 docs/sdk.md diff --git a/docs/languages/english/README.md b/README-en.md similarity index 50% rename from docs/languages/english/README.md rename to README-en.md index a040ae7ed..87d613ad4 100644 --- a/docs/languages/english/README.md +++ b/README-en.md @@ -1,8 +1,8 @@ # amis -A page renderer that renders pages directly based on a JSON configuration in a specific format. Combined with the business-side API, you can quickly develop various management pages. +A Low-Code frontend UI Framework. You can quickly develop various management pages by only using JSON configuration. Frontend skill is not required. -Currently used in Baidu's internal [AMIS] (http://amis.baidu.com) platform, 100+ departments have access to use, create 1.2w+ pages, welcome to use and make suggestions. +Currently used in Baidu's internal infrastructure, created more than 40000 pages. To build your own backend system with amis, you can refer to this: https://github.com/fex-team/amis-admin @@ -10,39 +10,39 @@ To build your own backend system with amis, you can refer to this: https://githu ``` # Install project npm dependencies. -Npm i +npm i # Start compiling and output the code to the webroot directory of the service you just opened. -Npm run dev +npm run dev # Open the fis3 service, please visit http://127.0.0.1:8888/examples/pages/simple. -Npm start +npm start ``` ## Testing ```bash #Installation dependency -Npm i +npm i #Executing test cases -Npm test +npm test # View test case coverage -Npm run coverage +npm run coverage ``` ## Working with documents For a better reading experience, it is recommended to read https://baidu.github.io/amis/ directly in gh-pages. -* [Quick Start] (/docs/getting_started.md) -* [Basic Usage] (/docs/basic.md) -* [Advanced Usage] (/docs/advanced.md) -* [Render Manual] (/docs/renderers.md) -* [How to customize] (/docs/sdk.md) -* [custom component] (/docs/dev.md) -* [Auxiliary Style] (/docs/style.md) +- [Quick Start](/docs/getting_started.md) +- [Basic Usage](/docs/basic.md) +- [Advanced Usage](/docs/advanced.md) +- [Render Manual](/docs/renderers.md) +- [How to customize](/docs/sdk.md) +- [custom component](/docs/dev.md) +- [Auxiliary Style](/docs/style.md) ## How to contribute @@ -50,9 +50,9 @@ Please write in typescript, all reasonable changes, new public renderers, use ca ## Maintainer -* [2betop](https://github.com/2betop) -* [RickCole21] (https://github.com/RickCole21) -* [catchonme](https://github.com/catchonme) +- [2betop](https://github.com/2betop) +- [RickCole21](https://github.com/RickCole21) +- [catchonme](https://github.com/catchonme) ## Discussion diff --git a/README.md b/README.md index 45c2a04e4..b8190d244 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,28 @@ # amis -前端低代码框架,通过 JSON 配置就能生成各种后台页面。 +前端低代码框架,通过 JSON 配置就能生成各种后台页面,极大减少开发成本,甚至可以不需要了解前端。 -目前在百度大量用于内部平台的前端开发,已有 100+ 部门使用,创建了 2.5w+ 页面。 +目前在百度广泛用于内部平台的前端开发,已有 100+ 部门使用,创建了 3w+ 页面。 -通过 amis 搭建自己的后台系统,可以参考这: https://github.com/fex-team/amis-admin -包含:fis3 版本、webpack 版本和 jssdk 版本。 +## 入门介绍 -## 快速开始 +请阅读 -请参考 +## 相关工具及平台 + +- 通过 amis 搭建自己的后台系统:https://github.com/fex-team/amis-admin +- 可视化编辑器:http://fex-team.github.io/amis-editor ## 开发指南 +以下是参与开发 amis 才需要看的,使用请看前面的入门文档。 + > 如果 github 下载慢可以使用 [gitee](https://gitee.com/baidu/amis) 上的镜像。 -推荐使用 node 10,node 6 和 node 8 也可以。node 12 未测试,可能 fis3 还有些插件不支持。 +推荐使用 node 8/10/12。 ```bash -# 安装项目 npm 依赖。 +# 安装项目 npm 依赖,在 node 12 下会有报错但不影响正常使用。 npm i # 开始编译,把代码产出到刚开启的服务的 webroot 目录。 @@ -51,6 +55,7 @@ npm run coverage - [2betop](https://github.com/2betop) - [RickCole21](https://github.com/RickCole21) - [catchonme](https://github.com/catchonme) +- [nwind](https://github.com/nwind) ## 讨论 diff --git a/build/md-parser.js b/build/md-parser.js index e943207f5..72f5a998c 100644 --- a/build/md-parser.js +++ b/build/md-parser.js @@ -1,25 +1,32 @@ /* eslint-disable */ -var marked = require("marked"); -var yaml = (yaml = require("js-yaml")); +var marked = require('marked'); +let prism = require('prismjs'); +let loadLanguages = require('prismjs/components/'); +loadLanguages(['bash', 'javascript', 'jsx', 'tsx', 'css', 'markup', 'json']); +var yaml = (yaml = require('js-yaml')); var rYml = /^\s*---([\s\S]*?)---\s/; var renderer = new marked.Renderer(); marked.setOptions({ - renderer: renderer, - gfm: true, - tables: true, - breaks: false, - pedantic: false, - sanitize: true, - smartLists: true, - smartypants: false + renderer: renderer, + gfm: true, + tables: true, + breaks: false, + pedantic: false, + sanitize: true, + smartLists: true, + smartypants: false }); -// Synchronous highlighting with highlight.js +// Synchronous highlighting with prism.js marked.setOptions({ - highlight: function(code) { - return require("highlight.js").highlightAuto(code).value; + highlight: function (code, lang) { + if (lang) { + return prism.highlight(code, prism.languages[lang], lang); + } else { + return code; } + } }); // renderer.table = function(header, body) { @@ -33,155 +40,150 @@ marked.setOptions({ // + '\n'; // }; -renderer.link = function(href, title, text) { - if (this.options.sanitize) { - try { - var prot = decodeURIComponent(unescape(href)) - .replace(/[^\w:]/g, "") - .toLowerCase(); - } catch (e) { - return ""; - } - if ( - prot.indexOf("javascript:") === 0 || - prot.indexOf("vbscript:") === 0 - ) { - return ""; - } +renderer.link = function (href, title, text) { + if (this.options.sanitize) { + try { + var prot = decodeURIComponent(unescape(href)) + .replace(/[^\w:]/g, '') + .toLowerCase(); + } catch (e) { + return ''; } - if (href && href[0] === "#") { - href = "#" + - encodeURIComponent( - href - .substring(1) - .toLowerCase() - .replace(/[^\u4e00-\u9fa5_a-zA-Z0-9]+/g, "-") - ); + if (prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0) { + return ''; } + } + if (href && href[0] === '#') { + href = + '#' + + encodeURIComponent( + href + .substring(1) + .toLowerCase() + .replace(/[^\u4e00-\u9fa5_a-zA-Z0-9]+/g, '-') + ); + } - var out = '"; - return out; + var out = ''; + return out; }; -module.exports = function(content, file) { - var m = rYml.exec(content); - var info = {}; - if (m && m[1]) { - info = yaml.safeLoad(m[1]); - content = content.substring(m[0].length); - } +module.exports = function (content, file) { + var m = rYml.exec(content); + var info = {}; + if (m && m[1]) { + info = yaml.safeLoad(m[1]); + content = content.substring(m[0].length); + } - var toc = { - label: "目录", - type: "toc", - children: [], - level: 0 - }; - var stack = [toc]; + var toc = { + label: '目录', + type: 'toc', + children: [], + level: 0 + }; + var stack = [toc]; - renderer.heading = function(text, level) { - var escapedText = encodeURIComponent( - text.toLowerCase().replace(/[^\u4e00-\u9fa5_a-zA-Z0-9]+/g, "-") - ); - - if (level < 5) { - var menu = { - label: text, - fragment: escapedText, - fullPath: "#" + escapedText, - level: level - }; - - while (stack.length && stack[0].level >= level) { - stack.shift(); - } - - stack[0].children = stack[0].children || []; - stack[0].children.push(menu); - - stack.unshift(menu); - } - - var anchor = - ''; - - return "" + anchor + text + ""; - - // return '' + - // text + ''; - }; - - const placeholder = {}; - let index = 1; - - content = content.replace( - /```(schema|html)(?::(.*?))?\n([\s\S]*?)```/g, - function(_, lang, attr, code) { - const setting = {}; - attr && attr.split(/\s+/).forEach(function(item) { - var parts = item.split("="); - - if (parts[1] && /^('|").*\1/.test(parts[1])) { - parts[1] = parts[1].substring(1, parts[1].length - 1); - } - - setting[parts[0]] = parts[1] ? decodeURIComponent(parts[1]) : ""; - - if (parts[0] === 'height') { - setting.height = parseInt(setting.height, 10) + 200/*编辑器的高度*/; - attr = attr.replace(item, `height="${setting.height}"`); - - } - }); - - - // placeholder[index] = ``; - if (lang === "html") { - if (~code.indexOf('
${code}
${
-                    require("highlight.js").highlightAuto(code).value
-                }
`; - } else { - placeholder[ - index - ] = `
`; - } - - return `[[${index++}]]`; - } + renderer.heading = function (text, level) { + var escapedText = encodeURIComponent( + text.toLowerCase().replace(/[^\u4e00-\u9fa5_a-zA-Z0-9]+/g, '-') ); - content = marked(content).replace(/

\[\[(\d+)\]\]<\/p>/g, function( - _, - id - ) { - return placeholder[id] || ""; - }); + if (level < 5) { + var menu = { + label: text, + fragment: escapedText, + fullPath: '#' + escapedText, + level: level + }; - content = fis.compile.partial(content, file, "html") + `\n\n

文档内容有误?欢迎大家一起来编写,文档地址:${file.subpath}
`; - info.html = content; - info.toc = toc; + while (stack.length && stack[0].level >= level) { + stack.shift(); + } - + stack[0].children = stack[0].children || []; + stack[0].children.push(menu); - return "module.exports = " + JSON.stringify(info, null, 2) + ";"; + stack.unshift(menu); + } + + var anchor = + ''; + + return '' + anchor + text + ''; + + // return '' + + // text + ''; + }; + + const placeholder = {}; + let index = 1; + + content = content.replace( + /```(schema|html)(?::(.*?))?\n([\s\S]*?)```/g, + function (_, lang, attr, code) { + const setting = {}; + attr && + attr.split(/\s+/).forEach(function (item) { + var parts = item.split('='); + + if (parts[1] && /^('|").*\1/.test(parts[1])) { + parts[1] = parts[1].substring(1, parts[1].length - 1); + } + + setting[parts[0]] = parts[1] ? decodeURIComponent(parts[1]) : ''; + + if (parts[0] === 'height') { + setting.height = + parseInt(setting.height, 10) + 200 /*编辑器的高度*/; + attr = attr.replace(item, `height="${setting.height}"`); + } + }); + + // placeholder[index] = ``; + if (lang === 'html') { + if (~code.indexOf('
${code}
${prism.highlight(
+          code,
+          prism.languages[lang],
+          lang
+        )}
`; + } else { + placeholder[ + index + ] = `
`; + } + + return `[[${index++}]]`; + } + ); + + content = marked(content).replace(/

\[\[(\d+)\]\]<\/p>/g, function (_, id) { + return placeholder[id] || ''; + }); + + content = + fis.compile.partial(content, file, 'html') + + `\n\n

文档内容有误?欢迎大家一起来编写,文档地址:${file.subpath}
`; + info.html = content; + info.toc = toc; + + return 'module.exports = ' + JSON.stringify(info, null, 2) + ';'; }; diff --git a/docs/api.md b/docs/api.md index 956915d55..c77a7da7f 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1,60 +1,111 @@ --- -title: API 说明 +title: 动态数据 --- -amis 渲染器的数据都来源于 api,有一定的格式要求。 - -### 整体要求 - -要求每个接口都返回 `status` 字段用来表示成功还是失败,如果失败了,通过 `msg` 字段来说明失败原因。当然如果成功 `msg` 也可以用来设置提示信息。 - +除了渲染静态页面及表单,amis 还能渲染动态数据,比如下面这个表格数据是来自 api 这个接口的请求 ```json { - "status": 0, // 0 表示成功,非0 表示失败 - "msg": "", // 提示信息 包括失败和成功 - "data": { - // ... - // 具体的数据 + "type": "crud", + "api": " http://xxx/api/sample", + "columns": [ + { + "name": "engine", + "label": "引擎" + }, + { + "name": "browser", + "label": "浏览器" } + ] } ``` -如果你的系统有自己的规范,也没关系,fetcher 整体入口那加个适配器就行了如: +amis 期望这个 api 接口返回的是如下的格式: + +```json +{ + "status": 0, + "msg": "", + "data": { + "items": [ + { + "engine": "Trident", + "browser": "IE 9" + }, + { + "engine": "Gecko", + "browser": "Firefox 70" + } + ] + } +} +``` + +下面是具体介绍 + +### 整体格式 + +要求每个接口都返回 `status` 字段用来表示成功还是失败,如果失败了,通过 `msg` 字段来说明失败原因。当然如果成功 `msg` 也可以用来设置提示信息。 + +```json +{ + "status": 0, // 0 表示成功,非0 表示失败 + "msg": "", // 提示信息 包括失败和成功 + "data": { + // ... + // 具体的数据 + } +} +``` + +如果你的系统有自己的规范,可以在 fetcher 统一进行适配,如: ```js { - fetcher: function(api) { - + renderAmis( + { + // 这里是 amis 的 Json 配置。 + type: 'page', + title: '简单页面', + body: '内容' + }, + { + // props + }, + { + // 忽略别的设置项 + fetcher: function (api) { // 适配这种格式 {"code": 0, "message": "", "result": {}} return axios(config).then(response => { - let payload = { - status: response.data.code, - msg: response.data.message, - data: response.data.result - }; + let payload = { + status: response.data.code, + msg: response.data.message, + data: response.data.result + }; - return { - ...response, - data: payload - } - }) + return { + ...response, + data: payload + }; + }); + } } + ); } ``` -### 具体要求 +### 具体要求 每个渲染的接口返回都有自己的格式要求,主要体现在 data 字段内部,具体请参考每个渲染的接口说明。 -* [Page](./renderers/Page.md#接口说明) -* [CRUD](./renderers/CRUD.md#接口说明) -* [Form](./renderers/Form/Form.md#接口说明) - * [Select](./renderers/Form/Select.md#接口说明) - * [Checkboxes](./renderers/Form/Checkboxes.md#接口说明) - * [Radios](./renderers/Form/Radios.md#接口说明) - * [List](./renderers/Form/List.md#接口说明) -* [Wizard](./renderers/Wizard.md#接口说明) +- [Page](./renderers/Page.md#接口说明) +- [CRUD](./renderers/CRUD.md#接口说明) +- [Form](./renderers/Form/Form.md#接口说明) + - [Select](./renderers/Form/Select.md#接口说明) + - [Checkboxes](./renderers/Form/Checkboxes.md#接口说明) + - [Radios](./renderers/Form/Radios.md#接口说明) + - [List](./renderers/Form/List.md#接口说明) +- [Wizard](./renderers/Wizard.md#接口说明) `TBD` - diff --git a/docs/basic.md b/docs/basic.md index e793e229c..b4cfccbc3 100644 --- a/docs/basic.md +++ b/docs/basic.md @@ -2,8 +2,6 @@ title: 基本用法 --- -为了简化前端开发,amis Renderer 能够直接用配置就能将页面渲染出来。 - 先来看个简单的例子。 ```schema:height="300" @@ -19,20 +17,20 @@ title: 基本用法 } ``` -> PS: 可以通过编辑器实时修改预览 +> 可以通过编辑器实时修改预览 -从上面的内容可以看出,一个简单页面框架已经基本出来了,这是 amis 渲染器配置的入口。从 `page` 渲染器开始出发,通过在容器中放置不同的渲染器来配置不同性质的页面。 +通过使用上面的例子就能配出一个基本页面框架,这是 amis 渲染器配置的入口。从 `page` 渲染器开始出发,通过在容器中放置不同的渲染器来配置不同性质的页面。 简单说明以上配置信息。 -- `$schema` 这个字段可以忽略,他是指定当前 JSON 配置是符合指定路径 https://houtai.baidu.com/v2/schemas/page.json 的 JSON SCHEMA 文件描述的。PS: 编辑器就是靠这个描述文件提示的,可以 hover 到字段上看效果。 -- `type` 指定渲染器类型,这里指定的类型为 `page`。 更多渲染器类型可以去[这里面查看](./renderers.md)。 -- `title` 从 title 开始就是对应的渲染模型上的属性了。这里用来指定标题内容。 -- `subTitle` 副标题. -- `remark` 标题上面的提示信息 -- `aside` 边栏区域内容 -- `body` 内容区域的内容 -- `toolbar` 工具栏部分的内容 +- `$schema` 这个字段可以忽略,他是指定当前 JSON 配置是符合指定路径 https://houtai.baidu.com/v2/schemas/page.json 的 JSON SCHEMA 文件描述的。PS: 编辑器就是靠这个描述文件提示的,可以 hover 到字段上看效果。 +- `type` 指定渲染器类型,这里指定的类型为 `page`。 更多渲染器类型可以去[这里面查看](./renderers.md)。 +- `title` 从 title 开始就是对应的渲染模型上的属性了。这里用来指定标题内容。 +- `subTitle` 副标题. +- `remark` 标题上面的提示信息 +- `aside` 边栏区域内容 +- `body` 内容区域的内容 +- `toolbar` 工具栏部分的内容 这里有三个配置都是容器类型的。`aside`、`body` 和 `toolbar`。什么是容器类型?容器类型表示,他能够把其他渲染类型放进来。以上的例子为了简单,直接放了个字符串。字符串类型内部是把他当成了 [tpl](./renderers/Tpl.md) 渲染器来处理,在这里也可以通过对象的形式指定,如以下的例子的 body 区域是完全等价的。 diff --git a/docs/custom.md b/docs/custom.md new file mode 100644 index 000000000..67ea5fb2f --- /dev/null +++ b/docs/custom.md @@ -0,0 +1,433 @@ +--- +title: 定制功能 +--- + +如果默认的组件不能满足需求,可以通过定制组件来进行扩展,在 amis 中有两种方法: + +1. 临时扩展,适合无需复用的组件。 +2. 注册自定义类型,适合需要在很多地方复用的组件。 + +> 注意,扩展只支持使用 React 组件方式引入的 amis,使用 JSSDK 无法支持 + +## 临时扩展 + +amis 的 JSON 配置最终会转成 React 组件来执行,所以如果只是想在某个配置中加入定制功能,可以直接在这个 JSON 配置里写 React 代码,比如下面这个例子: + +```jsx +{ + "type": "page", + "title": "自定义组件示例", + "body": { + "type": "form", + "controls": [ + { + "type": "text", + "label": "用户名", + "name": "usename" + }, + { + "name": "mycustom", + "children": ({ + value, + onChange + }) => ( +
+

这个是个自定义组件

+

当前值:{value}

+ onChange(Math.round(Math.random() * 10000)) + }>随机修改 +
+ ) + } + ] + } +} +``` + +其中的 `mycustom` 就是一个临时扩展,它的 `children` 属性是一个函数,它的返回内容和 React 的 Render 方法一样,即 jsx,在这个方法里你可以写任意 JavaScript 来实现自己的定制需求,这个函数有两个参数 `value` 和 `onChange`,`value` 就是组件的值,`onChange` 方法用来改变这个值,比如上面的例子中,点击链接后就会修改 `mycustom` 为一个随机数,在提交表单的时候就变成了这个随机数。 + +与之类似的还有个 `component` 属性,这个属性可以传入 React Component,如果想用 React Hooks,请通过 `component` 传递,而不是 `children`。 + +这种扩展方式既简单又灵活,但它是写在配置中的,如果需要在很多地方,可以使用下面的「注册自定义类型」方式: + +## 注册自定义类型 + +注册自定义类型需要了解 amis 的工作原理。 + +### 工作原理 + +amis 的渲染过程是将 `json` 转成对应的 React 组件。先通过 `json` 的 type 找到对应的 `Component` 然后,然后把其他属性作为 `props` 传递过去完成渲染。 + +拿一个表单页面来说,如果用 React 组件开发一般长这样。 + +```jsx + +
+ +``` + +把以上配置方式换成 amis JSON, 则是: + +```json +{ + "type": "page", + "title": "页面标题", + "subTitle": "副标题", + "body": { + "type": "form", + "title": "用户登录", + "controls": [ + { + "type": "text", + "name": "username", + "label": "用户名" + } + ] + } +} +``` + +那么,amis 是如何将 JSON 转成组件的呢?直接根据节点的 type 去跟组件一一对应?这样会重名,比如在表格里面展示的类型 `text` 跟表单里面的 `text` 是完全不一样的,一个负责展示,一个却负责输入。所以说一个节点要被什么组件渲染,还需要携带上下文(context)信息。 + +如何携带上下文(context)信息?amis 中是用节点的路径(path)来作为上下文信息。从上面的例子来看,一共有三个节点,path 信息分别是。 + +- `page` 页面节点 +- `page/body/form` 表单节点 +- `page/body/form/controls/0/text` 文本框节点。 + +根据 path 的信息就能很容易注册组件跟节点对应了。 + +Page 组件的示例代码 + +```jsx +@Renderer({ + test: /^page$/ + // ... 其他信息隐藏了 +}) +export class PageRenderer extends React.Component { + // ... 其他信息隐藏了 + render() { + const { + title, + body, + render // 用来渲染孩子节点,如果当前是叶子节点则可以忽略。 + } = this.props; + return ( +
+

{title}

+
+ {render('body', body) /*渲染孩子节点*/} +
+
+ ); + } +} +``` + +Form 组件的示例代码 + +```jsx +@Renderer({ + test: /(^|\/)form$/ + // ... 其他信息隐藏了 +}) +export class FormRenderer extends React.Component { + // ... 其他信息隐藏了 + render() { + const { + title, + controls, + render // 用来渲染孩子节点,如果当前是叶子节点则可以忽略。 + } = this.props; + return ( + + {controls.map((control, index) => ( +
+ {render(`${index}/control`, control)} +
+ ))} +
+ ); + } +} +``` + +Text 组件的示例代码 + +```jsx +@Renderer({ + test: /(^|\/)form(?:\/\d+)?\/control(?\/\d+)?\/text$/ + // ... 其他信息隐藏了 +}) +export class FormItemTextRenderer extends React.Component { + // ... 其他信息隐藏了 + render() { + const { + label, + name, + onChange + } = this.props; + return ( +
+
+ ); + } +} +``` + +那么渲染过程就是根据节点 path 信息,跟组件池中的组件 `test` (检测) 信息做匹配,如果命中,则把当前节点转给对应组件渲染,节点中其他属性将作为目标组件的 props。需要注意的是,如果是容器组件,比如以上例子中的 `page` 组件,从 props 中拿到的 `body` 是一个子节点,由于节点类型是不固定,由使用者决定,所以不能直接完成渲染,所以交给属性中下发的 `render` 方法去完成渲染,`{render('body', body)}`,他的工作就是拿子节点的 path 信息去组件池里面找到对应的渲染器,然后交给对应组件去完成渲染。 + +### 编写自定义组件 + +了解了基本原理后,来看个简单的例子: + +```jsx +import * as React from 'react'; +import {Renderer} from 'amis'; + +@Renderer({ + test: /(^|\/)my\-renderer$/ +}) +class CustomRenderer extends React.Component { + render() { + const {tip} = this.props; + return
这是自定义组件:{tip}
; + } +} +``` + +有了以上这段代码后,就可以这样使用了。 + +```json +{ + "type": "page", + "title": "自定义组件示例", + "body": { + "type": "my-renderer", + "tip": "简单示例" + } +} +``` + +看了前面[amis 工作原理](#工作原理)应该不难理解,这里注册一个 React 组件,当节点的 path 信息是 `my-renderer` 结尾时,交给当前组件来完成渲染。 + +如果这个组件还能通过 `children` 属性添加子节点,则需要使用下面这种写法: + +```jsx +import * as React from 'react'; +import {Renderer} from 'amis'; + +@Renderer({ + test: /(^|\/)my\-renderer2$/ +}) +class CustomRenderer extends React.Component { + render() { + const {tip, body, render} = this.props; + return ( +
+

这是自定义组件:{tip}

+ {body ? ( +
+ {render('body', body, { + // 这里的信息会作为 props 传递给子组件,一般情况下都不需要这个 + })} +
+ ) : null} +
+ ); + } +} +``` + +有了以上这段代码后,就可以这样使用了。 + +```json +{ + "type": "page", + "title": "自定义组件示例", + "body": { + "type": "my-renderer2", + "tip": "简单示例", + "body": { + "type": "form", + "controls": [ + { + "type": "text", + "label": "用户名", + "name": "usename" + } + ] + } + } +} +``` + +跟第一个列子不同的地方是,这里多了个 `render` 方法,这个方法就是专门用来渲染子节点的。来看下参数说明: + +- `region` 区域名称,你有可能有多个区域可以作为容器,请不要重复。 +- `node` 子节点。 +- `props` 可选,可以通过此对象跟子节点通信等。 + +### 表单项的扩展 + +以上是普通渲染器的注册方式,如果是表单项,为了更简单的扩充,请使用 `FormItem` 注解,而不是 `Renderer`。 原因是如果用 `FormItem` 是不用关心:label 怎么摆,表单验证器怎么实现,如何适配表单的 3 中展现方式(水平、上下和内联模式),而只用关心:有了值后如何回显,响应用户交互设置新值。 + +```jsx +import * as React from 'react'; +import {FormItem} from 'amis'; + +@FormItem({ + type: 'custom' +}) +class MyFormItem extends React.Component { + render() { + const {value, onChange} = this.props; + + return ( +
+

这个是个自定义组件

+

当前值:{value}

+ onChange(Math.round(Math.random() * 10000))} + > + 随机修改 + +
+ ); + } +} +``` + +有了以上这段代码后,就可以这样使用了。 + +```json +{ + "type": "page", + "title": "自定义组件示例", + "body": { + "type": "form", + "controls": [ + { + "type": "text", + "label": "用户名", + "name": "usename" + }, + + { + "type": "custom", + "label": "随机值", + "name": "random" + } + ] + } +} +``` + +> 注意: 使用 FormItem 默认是严格模式,即只有必要的属性变化才会重新渲染,有可能满足不了你的需求,如果忽略性能问题,可以传入 `strictMode`: `false` 来关闭。 + +表单项开发主要关心两件事。 + +1. 呈现当前值。如以上例子,通过 `this.props.value` 判定如果勾选了则显示`已勾选`,否则显示`请勾选`。 +2. 接收用户交互,通过 `this.props.onChange` 修改表单项值。如以上例子,当用户点击按钮时,切换当前选中的值。 + +至于其他功能如:label/description 的展示、表单验证功能、表单布局(常规、左右或者内联)等等,只要是通过 FormItem 注册进去的都无需自己实现。 + +需要注意,获取或者修改的是什么值跟配置中 `type` 并列的 `name` 属性有关,也就是说直接关联某个变量,自定义中直接通过 props 下发了某个指定变量的值和修改的方法。如果你想获取其他数据,或者设置其他数据可以看下以下说明: + +- `获取其他数据` 可以通过 `this.props.data` 查看,作用域中所有的数据都在这了。 +- `设置其他数据` 可以通过 `this.props.onBulkChange`, 比如: `this.props.onBulkChange({a: 1, b: 2})` 等于同时设置了两个值。当做数据填充的时候,这个方法很有用。 + +### 其它高级定制 + +下面是一些不太常用的 amis 扩展方式及技巧。 + +#### 自定义验证器 + +如果 amis [自带的验证](./renderers/Form/FormItem.md#)能满足需求了,则不需要关心。组件可以有自己的验证逻辑。 + +```jsx +import * as React from 'react'; +import {FormItem} from 'amis'; +import * as cx from 'classnames'; + +@FormItem({ + type: 'custom-checkbox' +}) +export default class CustomCheckbox extends React.Component { + validate() { + // 通过 this.props.value 可以知道当前值。 + + return isValid ? '' : '不合法,说明不合法原因。'; + } + // ... 其他省略了 +} +``` + +上面的栗子只是简单说明,另外可以做`异步验证`,validate 方法可以返回一个 promise。 + +#### OptionsControl + +如果你的表单组件性质和 amis 的 Select、Checkboxes、List 差不多,用户配置配置 source 可通过 API 拉取选项,你可以用 OptionsControl 取代 FormItem 这个注解。 + +用法是一样,功能方面主要多了以下功能。 + +- 可以配置 options,options 支持配置 visibleOn hiddenOn 等表达式 +- 可以配置 `source` 换成动态拉取 options 的功能,source 中有变量依赖会自动重新拉取。 +- 下发了这些 props,可以更方便选项。 + - `options` 不管是用户配置的静态 options 还是配置 source 拉取的,下发到组件已经是最终的选项了。 + - `selectedOptions` 数组类型,当前用户选中的选项。 + - `loading` 当前选项是否在加载 + - `onToggle` 切换一个选项的值 + - `onToggleAll` 切换所有选项的值,类似于全选。 + +#### 组件间通信 + +关于组件间通信,amis 中有个机制就是,把需要被引用的组件设置一个 name 值,然后其他组件就可以通过这个 name 与其通信,比如这个[栗子](./advanced.md#组件间通信)。其实内部是依赖于内部的一个 Scoped Context。你的组件希望可以被别的组件引用,你需要把自己注册进去,默认自定义的非表单类组件并没有把自己注册进去,可以参考以下代码做添加。 + +```js +import * as React from 'react'; +import {Renderer, ScopedContext} from 'amis'; +@Renderer({ + test: /(?:^|\/)my\-renderer$/ +}) +export class CustomRenderer extends React.Component { + static contextType = ScopedContext; + + componentWillMount() { + const scoped = this.context; + scoped.registerComponent(this); + } + + componentWillUnmount() { + const scoped = this.context; + scoped.unRegisterComponent(this); + } + + // 其他部分省略了。 +} +``` + +把自己注册进去了,其他组件就能引用到了。同时,如果你想找别的组件,也同样是通过 scoped 这个 context,如: `scoped.getComponentByName("xxxName")` 这样就能拿到目标组件的实例了(前提是目标组件已经配置了 name 为 `xxxName`)。 + +#### 其他功能方法 + +自定义的渲染器 props 会下发一个非常有用的 env 对象。这个 env 有以下功能方法。 + +- `env.fetcher` 可以用来做 ajax 请求如: `this.props.env.fetcher('xxxAPi', this.props.data).then((result) => console.log(result))` +- `env.confirm` 确认框,返回一个 promise 等待用户确认如: `this.props.env.confirm('你确定要这么做?').then((confirmed) => console.log(confirmed))` +- `env.alert` 用 Modal 实现的弹框,个人觉得更美观。 +- `env.notify` toast 某个消息 如: `this.props.env.notify("error", "出错了")` +- `env.jumpTo` 页面跳转。 diff --git a/docs/dev.md b/docs/dev.md deleted file mode 100644 index 34f25a231..000000000 --- a/docs/dev.md +++ /dev/null @@ -1,189 +0,0 @@ ---- -title: 自定义组件 -shortname: dev ---- - -自定义组件主要分两类。表单类和非表单类。 - -### FormItem - -即表单类,它主要用来扩充表单项。先看个例子。 - -```jsx -import * as React from 'react'; -import {FormItem} from 'amis'; -import * as cx from 'classnames'; - -@FormItem({ - type: 'custom-checkbox', -}) -export default class CustomCheckbox extends React.Component { - toggle = () => { - const {value, onChange} = this.props; - - onChange(!value); - }; - - render() { - const {value} = this.props; - const checked = !!value; - - return ( -
- - {checked ? '已勾选' : '请勾选'} - -
{checked ? '已勾选' : '请勾选'}
-
- ); - } -} -``` - -有了这个代码后,页面配置 form 的 controls 里面就可以通过这样的配置启动了。 - -```js -{ - // 其他信息省略了。。 - type: 'form', - controls: [ - { - type: 'custom-checkbox', - name: '变量名', - label: '自定义组件。' - } - ] -} -``` - -表单项开发主要关心两件事。 - -1. 呈现当前值。如以上例子,通过 `this.props.value` 判定如果勾选了则显示`已勾选`,否则显示`请勾选`。 -2. 接收用户交互,通过 `this.props.onChange` 修改表单项值。如以上例子,当用户点击按钮时,切换当前选中的值。 - -至于其他功能如:label/description 的展示、表单验证功能、表单布局(常规、左右或者内联)等等,只要是通过 FormItem 注册进去的都无需自己实现。 - -需要注意,获取或者修改的是什么值跟配置中 `type` 并列的 `name` 属性有关,也就是说直接关联某个变量,自定义中直接通过 props 下发了某个指定变量的值和修改的方法。如果你想获取其他数据,或者设置其他数据可以看下以下说明: - -* `获取其他数据` 可以通过 `this.props.data` 查看,作用域中所有的数据都在这了。 -* `设置其他数据` 可以通过 `this.props.onBulkChange`, 比如: `this.props.onBulkChange({a: 1, b: 2})` 等于同时设置了两个值。当做数据填充的时候,这个方法很有用。 - - - -#### 自定义验证器 - -如果 amis [自带的验证](./renderers/Form/FormItem.md#)能满足需求了,则不需要关心。组件可以有自己的验证逻辑。 - -```jsx -import * as React from 'react'; -import {FormItem} from 'amis'; -import * as cx from 'classnames'; - -@FormItem({ - type: 'custom-checkbox', -}) -export default class CustomCheckbox extends React.Component { - validate() { - // 通过 this.props.value 可以知道当前值。 - - return isValid ? '' : '不合法,说明不合法原因。' - } - // ... 其他省略了 -} -``` - -上面的栗子只是简单说明,另外可以做`异步验证`,validate 方法可以返回一个 promise。 - -#### OptionsControl - -如果你的表单组件性质和 amis 的 Select、Checkboxes、List 差不多,用户配置配置 source 可通过 API 拉取选项,你可以用 OptionsControl 取代 FormItem 这个注解。 - -用法是一样,功能方面主要多了以下功能。 - -* 可以配置 options,options 支持配置 visibleOn hiddenOn 等表达式 -* 可以配置 `source` 换成动态拉取 options 的功能,source 中有变量依赖会自动重新拉取。 -* 下发了这些 props,可以更方便选项。 - - `options` 不管是用户配置的静态 options 还是配置 source 拉取的,下发到组件已经是最终的选项了。 - - `selectedOptions` 数组类型,当前用户选中的选项。 - - `loading` 当前选项是否在加载 - - `onToggle` 切换一个选项的值 - - `onToggleAll` 切换所有选项的值,类似于全选。 - -### Renderer - -非表单类的组件自定义,主要通过 `Renderer` 实现。在开始阅读之前,请先阅读 [amis 工作原理](./sdk.md#工作原理)。 - -```jsx -import * as React from 'react'; -import {Renderer} from 'amis'; - -@Renderer({ - test: /(^|\/)my\-renderer$/, -}) -class CustomRenderer extends React.Component { - render() { - const {tip, body, render} = this.props; - - return ( -
-

这是自定义组件:{tip}

- {body ? ( -
- {render('body', body, { - // 这里的信息会作为 props 传递给子组件,一般情况下都不需要这个 - })} -
- ) : null} -
- ); - } -} -``` - -这里注册一个 React 组件,当节点的 path 信息是 `my-renderer` 结尾时,交给当前组件来完成渲染。 - -请注意 `this.props` 中的 `render` 方法,它用来实现容器功能,通过它可以让使用者动态的配置其他渲染模型。 - -### 组件间通信 - -关于组件间通信,amis 中有个机制就是,把需要被引用的组件设置一个 name 值,然后其他组件就可以通过这个 name 与其通信,比如这个[栗子](./advanced.md#组件间通信)。其实内部是依赖于内部的一个 Scoped Context。你的组件希望可以被别的组件引用,你需要把自己注册进去,默认自定义的非表单类组件并没有把自己注册进去,可以参考以下代码做添加。 - -```js -import * as React from 'react'; -import {Renderer, ScopedContext} from 'amis'; -@Renderer({ - test: /(?:^|\/)my\-renderer$/, -}) -export class CustomRenderer extends React.Component { - static contextType = ScopedContext; - - componentWillMount() { - const scoped = this.context; - scoped.registerComponent(this); - } - - componentWillUnmount() { - const scoped = this.context; - scoped.unRegisterComponent(this); - } - - // 其他部分省略了。 -} -``` - -把自己注册进去了,其他组件就能引用到了。同时,如果你想找别的组件,也同样是通过 scoped 这个 context,如: `scoped.getComponentByName("xxxName")` 这样就能拿到目标组件的实例了(前提是目标组件已经配置了 name 为 `xxxName`)。 - -### 其他功能方法 - -自定义的渲染器 props 会下发一个非常有用的 env 对象。这个 env 有以下功能方法。 - -* `env.fetcher` 可以用来做 ajax 请求如: `this.props.env.fetcher('xxxAPi', this.props.data).then((result) => console.log(result))` -* `env.confirm` 确认框,返回一个 promise 等待用户确认如: `this.props.env.confirm('你确定要这么做?').then((confirmed) => console.log(confirmed))` -* `env.alert` 用 Modal 实现的弹框,个人觉得更美观。 -* `env.notify` toast 某个消息 如: `this.props.env.notify("error", "出错了")` -* `env.jumpTo` 页面跳转。 \ No newline at end of file diff --git a/docs/getting_started.md b/docs/getting_started.md index d41ad49c5..2fd40f7b6 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -2,11 +2,14 @@ title: 快速开始 --- -这是一个基于 React 框架的页面渲染器,有配置就能生成页面,配置是什么样的?请前往[基本用法](./basic.md)阅读。知道怎么配置后,就可以用以下方式用于自己的项目了。 +有两种方式使用 amis: -如果你不会 React 也没关系可以看看 [JSSDK 用法](#JSSDK)。 +1. [React 组件](#React 组件),可以整合到 React 项目中,适合熟悉 React 的开发者,可以[开发自定义组件进行扩展](../custom)。 +2. [JSSDK](#JSSDK),可以放到任意页面中使用,能使用 amis 内置的渲染组件,但无法开发自定义组件,适合不使用 React 的项目或不熟悉前端的开发者。 -## 安装依赖 +## React 组件 + +### 安装依赖 直接通过 npm 安装即可。 @@ -14,9 +17,9 @@ title: 快速开始 npm i amis ``` -## 如何使用? +### 整合到 React 组件中 -可以在 React Component 这么使用。 +可以在 React Component 这么使用(TypeScript)。 ```tsx import * as React from 'react'; @@ -83,21 +86,24 @@ class MyComponent extends React.Component { 参数说明: -* `schema` 即页面配置,请前往[基本用法](./basic.md)了解. -* `props` 一般都用不上,如果你想传递一些数据给渲染器内部使用,可以传递 data 数据进去。如: +- `schema` 即页面配置,请前往[基本用法](./basic.md)了解. +- `props` 一般都用不上,如果你想传递一些数据给渲染器内部使用,可以传递 data 数据进去。如: ```jsx - () => renderAmis(schema, { - data: { - username: 'amis' - } - }) + () => + renderAmis(schema, { + data: { + username: 'amis' + } + }); ``` 这样,内部所有组件都能拿到 `username` 这个变量的值。 -* `env` 环境变量,可以理解为这个渲染器工具的配置项,需要调用者实现部分接口。 - * `session: string` 默认为 'global',决定 store 是否为全局共用的,如果想单占一个 store,请设置不同的值。 - * `fetcher: (config: fetcherConfig) => Promise` 用来实现 ajax 发送。 + +- `env` 环境变量,可以理解为这个渲染器工具的配置项,需要调用者实现部分接口。 + + - `session: string` 默认为 'global',决定 store 是否为全局共用的,如果想单占一个 store,请设置不同的值。 + - `fetcher: (config: fetcherConfig) => Promise` 用来实现 ajax 发送。 示例 @@ -142,126 +148,135 @@ class MyComponent extends React.Component { return (axios as any)[method](url, data, config); } ``` - * `isCancel: (e:error) => boolean` 判断 ajax 异常是否为一个 cancel 请求。 + + - `isCancel: (e:error) => boolean` 判断 ajax 异常是否为一个 cancel 请求。 示例 ```js isCancel: (value: any) => (axios as any).isCancel(value) ``` - * `notify: (type:string, msg: string) => void` 用来实现消息提示。 - * `alert: (msg:string) => void` 用来实现警告提示。 - * `confirm: (msg:string) => boolean | Promise` 用来实现确认框。 - * `jumpTo: (to:string, action?: Action, ctx?: object) => void` 用来实现页面跳转,因为不清楚所在环境中是否使用了 spa 模式,所以用户自己实现吧。 - * `updateLocation: (location:any, replace?:boolean) => void` 地址替换,跟 jumpTo 类似。 - * `isCurrentUrl: (link:string) => boolean` 判断目标地址是否为当前页面。 - * `theme: 'default' | 'cxd'` 目前支持两种主题。 - * `copy: (contents:string, options?: {shutup: boolean}) => void` 用来实现,内容复制。 - * `getModalContainer: () => HTMLElement` 用来决定弹框容器。 - * `loadRenderer: (chema:any, path:string) => Promise` 可以通过它懒加载自定义组件,比如: https://github.com/baidu/amis/blob/master/__tests__/factory.test.tsx#L64-L91。 - * `affixOffsetTop: number` 固顶间距,当你的有其他固顶元素时,需要设置一定的偏移量,否则会重叠。 - * `affixOffsetBottom: number` 固底间距,当你的有其他固底元素时,需要设置一定的偏移量,否则会重叠。 - * `richTextToken: string` 内置 rich-text 为 frolaEditor,想要使用,请自行购买,或者自己实现 rich-text 渲染器。 + + - `notify: (type:string, msg: string) => void` 用来实现消息提示。 + - `alert: (msg:string) => void` 用来实现警告提示。 + - `confirm: (msg:string) => boolean | Promise` 用来实现确认框。 + - `jumpTo: (to:string, action?: Action, ctx?: object) => void` 用来实现页面跳转,因为不清楚所在环境中是否使用了 spa 模式,所以用户自己实现吧。 + - `updateLocation: (location:any, replace?:boolean) => void` 地址替换,跟 jumpTo 类似。 + - `isCurrentUrl: (link:string) => boolean` 判断目标地址是否为当前页面。 + - `theme: 'default' | 'cxd'` 目前支持两种主题。 + - `copy: (contents:string, options?: {shutup: boolean}) => void` 用来实现,内容复制。 + - `getModalContainer: () => HTMLElement` 用来决定弹框容器。 + - `loadRenderer: (chema:any, path:string) => Promise` 可以通过它懒加载自定义组件,比如: https://github.com/baidu/amis/blob/master/__tests__/factory.test.tsx#L64-L91。 + - `affixOffsetTop: number` 固顶间距,当你的有其他固顶元素时,需要设置一定的偏移量,否则会重叠。 + - `affixOffsetBottom: number` 固底间距,当你的有其他固底元素时,需要设置一定的偏移量,否则会重叠。 + - `richTextToken: string` 内置 rich-text 为 frolaEditor,想要使用,请自行购买,或者自己实现 rich-text 渲染器。 ## JSSDK -如果你没有组件定制需求直接使用,而且不想折腾 React 相关的,我建议你直接用这种方式。 +JSSDK 适合对前端或 React 不了解的开发者,它不依赖 npm 及 webpack,直接引入代码就能使用,但需要注意这种方式不支持[定制组件](../sdk),只能使用 amis 内置的组件。 -首先请引用一下 CSS 和 JS。 +JSSDK 的代码从以下地址获取: -* JS 地址: https://houtai.baidu.com/v2/jssdk -* CSS 地址: https://houtai.baidu.com/v2/csssdk +- JS: https://houtai.baidu.com/v2/jssdk +- CSS: https://houtai.baidu.com/v2/csssdk -然后执行以下代码就能渲染了。 +然后在页面中插入下面的代码就能渲染出来了: ```js -(function() { - var amis = amisRequire('amis/embed'); - amis.embed('#container', { - type: 'page', - title: 'AMIS Demo', - body: 'This is a simple amis page.' - }, { - // props 一般不用传。 - }, { - // env - fetcher: () => { - // 可以不传,用来实现 ajax 请求 - }, +(function () { + var amis = amisRequire('amis/embed'); + amis.embed( + '#container', + { + type: 'page', + title: 'AMIS Demo', + body: 'This is a simple amis page.' + }, + { + // props 一般不用传。 + }, + { + // env + fetcher: () => { + // 可以不传,用来实现 ajax 请求 + }, - jumpTo: () => { - // 可以不传,用来实现页面跳转 - }, + jumpTo: () => { + // 可以不传,用来实现页面跳转 + }, - updateLocation: () => { - // 可以不传,用来实现地址栏更新 - }, + updateLocation: () => { + // 可以不传,用来实现地址栏更新 + }, - isCurrentUrl: () => { - // 可以不传,用来判断是否目标地址当前地址。 - }, + isCurrentUrl: () => { + // 可以不传,用来判断是否目标地址当前地址。 + }, - copy: () => { - // 可以不传,用来实现复制到剪切板 - }, + copy: () => { + // 可以不传,用来实现复制到剪切板 + }, - notify: () => { - // 可以不传,用来实现通知 - }, + notify: () => { + // 可以不传,用来实现通知 + }, - alert: () => { - // 可以不传,用来实现提示 - }, + alert: () => { + // 可以不传,用来实现提示 + }, - confirm: () => { - // 可以不传,用来实现确认框。 - } - }); + confirm: () => { + // 可以不传,用来实现确认框。 + } + } + ); })(); ``` 注意:以上的 SDK 地址是一个页面跳转,会跳转到一个 CDN 地址,而且每次跳转都是最新的版本,随着 amis 的升级这个地址会一直变动,如果你的页面已经完成功能回归,请直接使用某个固定地址,这样才不会因为 amis 升级而导致你的页面不可用。 -另外,sdk 代码也伴随 npm 一起发布了,不使用 CDN 版本,直接替换成npm包里面的 `amis/sdk/sdk.js` 和 `amis/sdk/sdk.css` 即可。 +另外,sdk 代码也伴随 npm 一起发布了,不使用 CDN 版本,直接替换成 npm 包里面的 `amis/sdk.js` 和 `amis/sdk.css` 即可。 -示例: +完整示例: ```html - - + + AMIS Demo - - - - + + + + - - + +
- + - + ``` - diff --git a/docs/intro.md b/docs/intro.md new file mode 100644 index 000000000..8efc34cd5 --- /dev/null +++ b/docs/intro.md @@ -0,0 +1,198 @@ +--- +title: AMIS 是什么? +shortname: intro +--- + +amis 是一个前端低代码框架,它使用 JSON 配置来生成页面,可以极大节省页面开发工作量,极大提升开发前端界面的效率。 + +## 为什么要做 amis? + +在经历了十几年的发展后,前端开发变得越来越复杂,门槛也越来越高,要使用当下流行的 UI 组件库,你必须懂 npm、webpack、react/vue,必须熟悉 ES 6 语法,最好还了解状态管理,比如 Redux,如果没接触过函数式编程,一开始入门就很困难,而它还有巨大的[生态](https://github.com/markerikson/redux-ecosystem-links),相关的库有 2347 个,然而前端技术的发展不会停滞,等学完这些后可能会发现大家都用 Hooks 了、某个打包工具取代 WebPack 了。。。 + +而有时候你只是为了做个普通的增删改查界面,用于系统管理,类似下面这种: + +```schema:height="500" +{ + "$schema": "http://amis.baidu.com/v2/schemas/page.json#", + "title": "浏览器内核对 CSS 的支持情况", + "remark": "嘿,不保证数据准确性", + "type": "page", + "body": { + "type": "crud", + "draggable": true, + "api": "/api/sample", + "keepItemSelectionOnPageChange": true, + "filter": { + "title": "筛选", + "submitText": "", + "controls": [ + { + "type": "text", + "name": "keywords", + "placeholder": "关键字", + "addOn": { + "label": "搜索", + "type": "submit" + } + } + ] + }, + "bulkActions": [ + { + "label": "批量删除", + "actionType": "ajax", + "api": "delete:/api/sample/${ids|raw}", + "confirmText": "确定要批量删除?" + }, + { + "label": "批量修改", + "actionType": "dialog", + "dialog": { + "title": "批量编辑", + "name": "sample-bulk-edit", + "body": { + "type": "form", + "api": "/api/sample/bulkUpdate2", + "controls": [ + { + "type": "hidden", + "name": "ids" + }, + { + "type": "text", + "name": "engine", + "label": "Engine" + } + ] + } + } + } + ], + "quickSaveApi": "/api/sample/bulkUpdate", + "quickSaveItemApi": "/api/sample/$id", + "filterTogglable": true, + "headerToolbar": [ + "filter-toggler", + "bulkActions", + { + "type": "tpl", + "tpl": "一共有 ${count} 行数据。", + "className": "v-middle" + }, + { + "type": "columns-toggler", + "align": "right" + }, + { + "type": "drag-toggler", + "align": "right" + }, + { + "type": "pagination", + "align": "right" + } + ], + "footerToolbar": [ + "statistics", + "switch-per-page", + "pagination" + ], + "columns": [ + { + "name": "id", + "label": "ID", + "width": 20, + "sortable": true, + "type": "text" + }, + { + "name": "engine", + "label": "Rendering engine", + "sortable": true, + "searchable": true, + "type": "text", + "remark": "Trident 就是 IE,Gecko 就是 Firefox" + }, + { + "name": "platform", + "label": "Platform(s)", + "popOver": { + "body": { + "type": "tpl", + "tpl": "就是为了演示有个叫 popOver 的功能" + }, + "offset": { + "y": 50 + } + }, + "sortable": true, + "type": "text" + }, + { + "name": "grade", + "label": "CSS grade", + "quickEdit": { + "mode": "inline", + "type": "select", + "options": [ + "A", + "B", + "C", + "D", + "X" + ] + }, + "type": "text" + }, + { + "type": "operation", + "label": "操作", + "width": 100, + "buttons": [ + { + "type": "button", + "icon": "fa fa-times text-danger", + "actionType": "ajax", + "tooltip": "删除", + "confirmText": "您确认要删除?", + "api": "delete:/api/sample/$id" + } + ] + } + ] + } +} +``` + +这个界面虽然用 Bootstrap 也能快速搭起来,但要想体验好就需要加很多细节功能,比如数据动态加载、编辑单行数据、批量删除和修改、查询某列、按某列排序、隐藏某列、开启整页内容拖拽排序、表格有分页(页数还会同步到地址栏,刷新页面试试)、如果往下拖动还有首行冻结来方便查看表头等,全部实现这些需要大量的代码。 + +然而上面也看到了,在 amis 里只需要 150 行 JSON 配置(嘿,其中 40 多行只有一个括号),你不需要了解 React/Vue、Webpack,甚至不需要了解 JavaScript,即便没学过 amis 也能猜到大部分配置的作用,只需要简单配置就能完成所有页面开发,这正是建立 amis 的初衷,我们认为**对于大部分常用页面,应该使用最简单的方法来实现**,而不是越来越复杂。 + +## 用 JSON 写页面有什么好处? + +为了实现用最简单方式来生成大部分页面,amis 的解决方案是基于 JSON 来配置,它的独特好处是: + +- **不需要懂前端**就能做出专业且复杂的后台界面,这是所有其他前端 UI 库都无法做到的。在百度内部,大部分 amis 用户之前从来没写过前端页面,也不会 JavaScript。 +- **不受前端技术更新的影响**,同时还能享受 amis 升级带来的界面改进,百度内部最老的 amis 页面是 4 年多前创建的,至今还在使用,而当年的 Angular/Vue/React 版本现在都废弃了,当年流行的 Gulp 也被 Webpack 取代了,如果这些页面不是用 amis,现在的维护成本会很高。 +- 可以**完全**使用[可视化页面编辑器](https://fex-team.github.io/amis-editor/#/edit/0)来制作页面,一般前端可视化编辑器只能用来做静态原型,而 amis 可视化编辑器做出的页面是可以直接上线的。 + +## amis 的其它亮点 + +- **提供完整的界面解决方案**,其它 UI 框架必须使用 JavaScript 来组装业务逻辑,而 amis 只需 JSON 配置就能完整完整功能开发,包括数据获取、表单提交及验证等功能。 +- 内置 **92** 种 UI 组件,包括其它 UI 框架都会不提供的富文本编辑器、代码编辑器等,能满足各种页面组件展现的需求,而且对于特殊的展现形式还可以通过[自定义组件](../dev)来扩充。 +- 容器组件支持**无限层级嵌套**,可以通过组合来满足各种布局需求。 +- 经历了长时间的实战考验,amis 在百度内部得到了广泛使用,在 4 年多的时间里创建了 3w 多页面,从内容审核到机器管理,从数据分析到模型训练,amis 满足了各种各样的页面需求。 + +## amis 不适合做什么? + +使用 JSON 有优点但也有明显缺点,在以下场合并不适合 amis: + +- 大量定制 UI,尤其是面向普通客户(toC)的产品页面 + - JSON 配置使得 amis 更适合做有大量常见 UI 组件的页面,但对于面向普通客户的页面,往往追求个性化的视觉效果,这种情况下用 amis 就不合适,实际上绝大部分前端 UI 组件库也都不适合,只能定制开发。 +- 有极为复杂的交互,或者对交互有很特殊的要求 + - 有些复杂的前端功能,比如可视化编辑器,其中有大量定制的拖拽操作,这种需要依赖原生 DOM 实现的功能无法使用 amis。 + - 但对于某些交互固定的领域,比如图连线,amis 后续会有专门的组件来实现。 + +## 接下来 + +请阅读[快速开始](../getting-started)来学习如何使用 amis。 diff --git a/docs/renderers.md b/docs/renderers.md index d34d26086..55079272f 100644 --- a/docs/renderers.md +++ b/docs/renderers.md @@ -2,7 +2,7 @@ title: 渲染器手册 --- -amis 页面是通过 JSON 配置出来的,是由一个一个渲染模型组成的,掌握他们规则,就能灵活配置出各种页面。 +amis 页面是由一个个渲染模型组成的,并且支持无限层级嵌套,掌握他们规则,就能灵活配置出各种页面。 开始之前,请您一定要先阅读[基本用法](./basic.md)。 @@ -95,6 +95,6 @@ amis 页面是通过 JSON 配置出来的,是由一个一个渲染模型组成 - [Button-Group](./renderers/Button-Group.md): 按钮集合 - [iFrame](./renderers/iFrame.md): 如果需要内嵌外部站点,可用 iframe 来实现 - [Nav](./renderers/Nav.md): 菜单栏 -- [Tasks](./renderers/Tasks.md): 任务操作集合,类似于 orp 上线 +- [Tasks](./renderers/Tasks.md): 任务操作集合,适用于一步步操作 - [QRCode](./renderers/QRCode.md): 二维码显示组件 - [Types](./renderers/Types.md): 类型说明文档 diff --git a/docs/renderers/Action.md b/docs/renderers/Action.md index e773752f5..e29578db3 100644 --- a/docs/renderers/Action.md +++ b/docs/renderers/Action.md @@ -5,7 +5,7 @@ Action 是一种特殊的渲染器,它本身是一个按钮,同时它能触 ```schema:height="100" scope="body" { "label": "弹个框", - "type": "button", + "type": "action", "level": "dark", "actionType": "dialog", "dialog": { @@ -23,7 +23,7 @@ Action 是一种特殊的渲染器,它本身是一个按钮,同时它能触 | 属性名 | 类型 | 默认值 | 说明 | | ---------------- | --------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| type | `string` | `action` | 指定为 Page 渲染器。 | +| type | `string` | `action` | 指定为 Action 渲染器,也可以是 `button`、`submit`、`reset`。 | | actionType | `string` | - | 【必填】这是 action 最核心的配置,来指定该 action 的作用类型,支持:`ajax`、`link`、`url`、`drawer`、`dialog`、`confirm`、`cancel`、`prev`、`next`、`copy`、`close`。 | | label | `string` | - | 按钮文本。可用 `${xxx}` 取值。 | | level | `string` | `default` | 按钮样式,支持:`link`、`primary`、`secondary`、`info`、`success`、`warning`、`danger`、`light`、`dark`、`default`。 | diff --git a/docs/sdk.md b/docs/sdk.md deleted file mode 100644 index db965826b..000000000 --- a/docs/sdk.md +++ /dev/null @@ -1,336 +0,0 @@ ---- -title: 如何定制 ---- - -开始定制之前,请先仔细阅读工作原理。 - -## 工作原理 - -amis 的渲染过程就是将 `json` 转成对应的 React 组件。先通过 `json` 的 type 找到对应的 `Component` 然后,然后把其他属性作为 `props` 传递过去完成渲染。 - -拿一个表单页面来说,如果用React组件调用大概是这样。 - -```jsx - -
- -``` - -把以上配置方式换成 amis JSON, 则是: - -```json -{ - "type": "page", - "title": "页面标题", - "subTitle": "副标题", - "body": { - "type": "form", - "title": "用户登录", - "controls": [ - { - "type": "text", - "name": "username", - "label": "用户名" - } - ] - } -} -``` - -那么,amis 是如何将 JSON 转成组件的呢?直接根据节点的 type 去跟组件一一对应?似乎很可能会重名比如在表格里面展示的类型 `text` 跟表单里面的 `text`是完全不一样的,一个负责展示,一个却负责输入。所以说一个节点要被什么组件渲染,还需要携带上下文(context)信息。 - -如何去携带上下文(context)信息?amis 中直接是用节点的路径(path)来作为上下文信息。从上面的例子来看,一共有三个节点,path 信息分别是。 - -* `page` 页面节点 -* `page/body/form` 表单节点 -* `page/body/form/controls/0/text` 文本框节点。 - -根据 path 的信息就能很容易注册组件跟节点对应了。 - -Page 组件的示例代码 - -```jsx -@Renderer({ - test: /^page$/, - // ... 其他信息隐藏了 -}) -export class PageRenderer extends React.Component { - // ... 其他信息隐藏了 - render() { - const { - title, - body, - render // 用来渲染孩子节点,如果当前是叶子节点则可以忽略。 - } = this.props; - return ( -
-

{title}

-
- {render('body', body)/*渲染孩子节点*/} -
-
- ); - } -} -``` - -Form 组件的示例代码 - -```jsx -@Renderer({ - test: /(^|\/)form$/, - // ... 其他信息隐藏了 -}) -export class FormRenderer extends React.Component { - // ... 其他信息隐藏了 - render() { - const { - title, - controls, - render // 用来渲染孩子节点,如果当前是叶子节点则可以忽略。 - } = this.props; - return ( - - {controls.map((control, index) => ( -
- {render(`${index}/control`, control)} -
- ))} -
- ); - } -} -``` - -Text 组件的示例代码 - -```jsx -@Renderer({ - test: /(^|\/)form(?:\/\d+)?\/control(?\/\d+)?\/text$/ - // ... 其他信息隐藏了 -}) -export class FormItemTextRenderer extends React.Component { - // ... 其他信息隐藏了 - render() { - const { - label, - name, - onChange - } = this.props; - return ( -
-
- ); - } -} -``` - -那么渲染过程就是根据节点 path 信息,跟组件池中的组件 `test` (检测) 信息做匹配,如果命中,则把当前节点转给对应组件渲染,节点中其他属性将作为目标组件的 props。需要注意的是,如果是容器组件,比如以上例子中的 `page` 组件,从 props 中拿到的 `body` 是一个子节点,由于节点类型是不固定,由使用者决定,所以不能直接完成渲染,所以交给属性中下发的 `render` 方法去完成渲染,`{render('body', body)}`,他的工作就是拿子节点的 path 信息去组件池里面找到对应的渲染器,然后交给对应组件去完成渲染。 - -## 自定义组件 - -如果 amis 中组件不能满足你的需求,同时你又会 React 组件开发,那么就自己定制一个吧。 - -先来看个简单的例子 - -```jsx -import * as React from 'react'; -import { - Renderer -} from 'amis'; - -@Renderer({ - test: /(^|\/)my\-renderer$/, -}) -class CustomRenderer extends React.Component { - render() { - const {tip} = this.props; - return ( -
这是自定义组件:{tip}
- ); - } -} -``` - -有了以上这段代码后,就可以这样使用了。 - -```json -{ - "type": "page", - "title": "自定义组件示例", - "body": { - "type": "my-renderer", - "tip": "简单示例" - } -} -``` - -如果你看了[amis工作原理](#工作原理)应该不难理解,这里注册一个 React 组件,当节点的 path 信息是 `my-renderer` 结尾时,交给当前组件来完成渲染。 -如果你只写叶子节点的渲染器,已经可以不用看了,如果你的渲染器中有容器需要可以放置其他节点,那么接着看以下这段代码。 - -```jsx -import * as React from 'react'; -import { - Renderer -} from 'amis'; - -@Renderer({ - test: /(^|\/)my\-renderer2$/, -}) -class CustomRenderer extends React.Component { - render() { - const { - tip, - body, - render - } = this.props; - return ( -
-

这是自定义组件:{tip}

- {body ? ( -
- {render('body', body, { - // 这里的信息会作为 props 传递给子组件,一般情况下都不需要这个 - })} -
- ) : null} -
- ); - } -} -``` - -有了以上这段代码后,就可以这样使用了。 - -```json -{ - "type": "page", - "title": "自定义组件示例", - "body": { - "type": "my-renderer2", - "tip": "简单示例", - "body": { - "type": "form", - "controls": [ - { - "type": "text", - "label": "用户名", - "name": "usename" - } - ] - } - } -} -``` - -跟第一个列子不同的地方是,这里多了个 `render` 方法,这个方法就是专门用来渲染子节点的。来看下参数说明: - -* `region` 区域名称,你有可能有多个区域可以作为容器,请不要重复。 -* `node` 子节点。 -* `props` 可选,可以通过此对象跟子节点通信等。 - -以上是普通渲染器的注册方式,如果是表单项,为了更简单的扩充,请使用 `FormItem` 注解,而不是 `Renderer`。 原因是如果用 `FormItem` 是不用关心:label怎么摆,表单验证器怎么实现,如何适配表单的3中展现方式(水平、上下和内联模式),而只用关心:有了值后如何回显,响应用户交互设置新值。 - -```jsx -import * as React from 'react'; -import { - FormItem -} from 'amis'; - -@FormItem({ - type: 'custom' -}) -class MyFormItem extends React.Component { - render() { - const { - value, - onChange - } = this.props; - - return ( -
-

这个是个自定义组件

-

当前值:{value}

- onChange(Math.round(Math.random() * 10000))}>随机修改 -
- ); - } -} -``` - -有了以上这段代码后,就可以这样使用了。 - -```json -{ - "type": "page", - "title": "自定义组件示例", - "body": { - "type": "form", - "controls": [ - { - "type": "text", - "label": "用户名", - "name": "usename" - }, - - { - "type": "custom", - "label": "随机值", - "name": "random" - } - ] - } -} -``` - -> 注意: 使用 FormItem 默认是严格模式,即只有必要的属性变化才会重新渲染,有可能满足不了你的需求,如果忽略性能问题,可以传入 `strictMode`: `false` 来关闭。 - -以上的例子都是需要先注册组件,然后再使用的,如果你在自己项目中使用,还有更简单的用法,以下示例直接无需注册。 - -```jsx -{ - "type": "page", - "title": "自定义组件示例", - "body": { - "type": "form", - "controls": [ - { - "type": "text", - "label": "用户名", - "name": "usename" - }, - - { - "name": "a", - "children": ({ - value, - onChange - }) => ( -
-

这个是个自定义组件

-

当前值:{value}

- onChange(Math.round(Math.random() * 10000))}>随机修改 -
- ) - } - ] - } -} -``` - -即:通过 `children` 实现一个自定义渲染方法,返回 React.ReactNode 节点。 -任何节点如果包含 `children` 这个属性,则都会把当前节点交给 `children` 来处理,跳过了从 amis 渲染器池子中选择渲染器的过程。`children` 属性其实更应该叫 `render` 属性,但是历史原因不能改了。与之类似的还有个 `component` 属性,这个属性可以传入 React Component,如果想用 React.hooks,请通过 `component` 传递,而不是 `children`。 diff --git a/docs/style.md b/docs/style.md index 6c5e1828c..1154ff8b3 100644 --- a/docs/style.md +++ b/docs/style.md @@ -1,11 +1,9 @@ --- -title: 样式表说明 +title: 定制样式 shortname: style --- -amis 中有大量的功能类 class 可以使用,即可以用在 schema 中,也可以用在自定义组件开发中,掌握这些 class, 几乎可以不用写样式。 - -amis 中的样式基于 [BootStrap V3](http://getbootstrap.com/css/), 这里主要讲 Bootstrap 中没有涉及到的。 +绝大部分 amis 组件里都有个 `className` 配置项,设置后就会给对应的组件添加 css class,而 amis 内置了大量的功能类 class,通过这些 class 的组合就能满足大部分展现调整的需求。 ## 图标 diff --git a/examples/components/App.jsx b/examples/components/App.jsx index fada1e56d..6674b7b1b 100644 --- a/examples/components/App.jsx +++ b/examples/components/App.jsx @@ -102,6 +102,7 @@ if (process.env.NODE_ENV === 'production') { } const navigations = [ + Docs, { label: '示例', children: [ @@ -533,9 +534,9 @@ const navigations = [ component: TestComponent } ] - }, + } + - Docs ]; function isActive(link, location) { @@ -898,9 +899,9 @@ export default function entry({pathPrefix}) { - + {navigations2route(PathPrefix)} diff --git a/examples/components/Doc.jsx b/examples/components/Doc.jsx index 71ae3de06..84b171c1c 100644 --- a/examples/components/Doc.jsx +++ b/examples/components/Doc.jsx @@ -5,6 +5,16 @@ export default { prefix: ({classnames: cx}) =>
  • , label: '文档', children: [ + { + label: 'AMIS 是什么?', + icon: 'fa fa-home', + path: '/docs/intro', + getComponent: (location, cb) => + require(['../../docs/intro.md'], doc => { + cb(null, makeMarkdownRenderer(doc)); + }) + }, + { label: '快速开始', icon: 'fa fa-flash', @@ -38,7 +48,7 @@ export default { // {{renderer-docs}} { - label: 'API 说明', + label: '动态数据', path: '/docs/api', icon: 'fa fa-cloud', getComponent: (location, cb) => @@ -48,27 +58,17 @@ export default { }, { - label: '如何定制', - path: '/docs/sdk', + label: '定制功能', + path: '/docs/custom', icon: 'fa fa-cubes', getComponent: (location, cb) => - require(['../../docs/sdk.md'], doc => { + require(['../../docs/custom.md'], doc => { cb(null, makeMarkdownRenderer(doc)); }) }, { - label: '自定义组件', - path: '/docs/dev', - icon: 'fa fa-code', - getComponent: (location, cb) => - require(['../../docs/dev.md'], doc => { - cb(null, makeMarkdownRenderer(doc)); - }) - }, - - { - label: '样式说明', + label: '定制样式', path: '/docs/style', icon: 'fa fa-laptop', getComponent: (location, cb) => diff --git a/examples/components/SchemaRender.jsx b/examples/components/SchemaRender.jsx index 5509f1b2a..7a222ab90 100644 --- a/examples/components/SchemaRender.jsx +++ b/examples/components/SchemaRender.jsx @@ -205,6 +205,7 @@ export default function (schema) { > + ←点击这里查看源码 ) : null} diff --git a/examples/index.html b/examples/index.html index be6b5b5b0..f6432c53e 100644 --- a/examples/index.html +++ b/examples/index.html @@ -1,52 +1,71 @@ - - - AMis Renderer - - - - - - - - - - - - - - - - - + + + AMis Renderer + + + + + + + + + + + + + + + + + - -
    - - - + + - + /* @require ./index.jsx 标记为同步依赖,提前加载 */ + require(['./index.jsx'], function (app) { + var initialState = {}; + app.bootstrap(document.getElementById('root'), initialState); + }); + + diff --git a/examples/sdk-placeholder.html b/examples/sdk-placeholder.html index ebf678ae9..760bd2a57 100644 --- a/examples/sdk-placeholder.html +++ b/examples/sdk-placeholder.html @@ -1,33 +1,28 @@ - - - - - - - + + + + + + - - + + - + diff --git a/package.json b/package.json index f7b86436b..b4e0aa5f4 100644 --- a/package.json +++ b/package.json @@ -124,7 +124,6 @@ "fis3-preprocessor-js-require-css": "^0.1.3", "font-awesome": "4.7.0", "fs-walk": "0.0.2", - "highlight.js": "^9.12.0", "husky": "^2.2.0", "jest": "^24.5.0", "jest-canvas-mock": "^2.1.0", @@ -133,6 +132,7 @@ "marked": "^0.3.7", "mobx-wiretap": "^0.12.0", "prettier": "^2.0.5", + "prismjs": "^1.20.0", "react-frame-component": "^2.0.0", "react-router": "3.2.0", "react-test-renderer": "^16.8.6",