其他章節請看:
中後臺整合低程式碼預研
背景
筆者目前維護一個 react 中後臺系統(以 spug 為例),每次來了新的需求都需要前端人員重新開發。
前面我們已經對低程式碼
有了一定的認識,如果能透過一個視覺化的配置頁面
就能完成前端開發,將極大的提高前端(或後端)的效率。甚至能加快企業內部數字化(資訊化)建設。
低程式碼介紹
低程式碼
這一概念由 Forrester 在 2014 年正式提出。低程式碼,顧名思義,就是指開發者寫很少的程式碼,透過低程式碼平臺提供的介面、邏輯、物件、流程等視覺化編排工具
來完成大量的開發工作,降低
軟體開發中的不確定性和複雜性。實現軟體的高效構建,無需重複傳統的手動程式設計,同時兼顧業務人員和專業開發人員的更多參與。
零程式碼
屬於低程式碼平臺的一種,不提供或者僅支援有限的程式設計擴充套件能力,技術門檻低,應用場景有限。
目標
最優 | 稍次 |
---|---|
透過低程式碼平臺配置系統所有 前端頁面 |
透過低程式碼平臺配置系統大部分 前端頁面 |
預研產品
amis
amis 是百度的一個低程式碼前端框架,它使用 JSON
配置來生成頁面,可以減少頁面開發工作量,極大提升效率。開源免費
,github Star 13.4k。
包含兩個專案:amis 和 amis-editor(前些天已開源)。amis-editor 透過視覺化形式生成頁面,畫原型的功夫就將前端頁面給開發好了,最後生成該頁面的配置(一個json),該配置放入 amis 解析出來就是一個前端頁面。
amis 線上編輯器如下:
愛速搭
愛速搭是百度智慧雲推出的低程式碼開發平臺,它靈活性強,對開發者友好,在百度內部大規模使用,有超過 4w 內部頁面是基於它製作的,是百度內部中臺系統的核心基礎設施。支援私有部署,收費
。
Tip: amis 是愛速搭團隊開源的前端低程式碼框架,愛速搭應用中的頁面都是基於 amis 渲染的,同時愛速搭平臺自身的絕大部分頁面也是基於 amis 開發 —— 愛速搭與amis
lowcode-engine
阿里低程式碼引擎(lowcode-engine)是一款為低程式碼平臺開發者提供的,具備強大定製擴充套件能力的低程式碼設計器研發框架。免費
。
Tip: 前面筆者也稍微實現了一個簡單的視覺化編輯器,有些麻煩,也有很多不足。真實場景傾向使用成熟的編輯器
。
釘釘宜搭
釘釘宜搭是阿里巴巴自研的低程式碼應用開發平臺,基於阿里雲的雲基礎設施和釘釘的企業數字化作業系統,為每個組織提供低門檻、高效率的數字化業務應用生產新模式。在宜搭上生產的每個應用天然具備互聯互通、資料驅動、安全可控的特點。收費
。
在宜搭模版市場提供了一些免費應用模版,只需選擇一個模版修改個別文案,一分鐘就能搭建一款專屬應用
方案概要
市面上確實存在幫助企業加快數字化建設的低程式碼平臺,透過該平臺能較快的搭建各種系統,但通常是收費
的。
透過各類低程式碼產品的預研,也結合筆者當前工作需求:免費
、內網部署
、靈活
)。目標求其次:透過低程式碼平臺配置系統大部分
前端頁面。
方案篩選:
- amis vs 愛速搭:amis 免費
- amis vs lowcode-engine: amis 更全,包含編輯器和渲染器(解析 json 成前端頁面),而 lowcode-engine 只是一個編輯器。倘若需要自建一個低程式碼平臺, lowcode-engine 是一個不錯的選擇
最終方案:中後臺系統(spug) + amis + amis-editor
(開源、免費)。就像這樣:
- amis-editor 配置頁面,生成 json
- amis 透過 json 渲染出頁面
- spug 整合 amis
方案可行性
透過 amis-editor 視覺化操作快速建立頁面,然後將配置放入 amis 中解析,實現大部分前端頁面的視覺化生成。
介面資料
本地啟動一個 node 服務,用於模擬
介面資料。
使用其他服務也可以,只要傳送請求能返回資料(資料參考 amis 官網,直接使用 amis 官網的介面報跨域失敗)。就像這樣:
node 服務
初始化專案 local-mock:
$ mkdir local-mock
$ cd local-mock
// 初始化專案
$ npm init -y
修改如下 package.json:
{
"name": "local-mock",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"body-parser": "^1.20.2",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"express": "^4.18.2"
},
"devDependencies": {
"nodemon": "^2.0.22"
}
}
新建 node.js(注:允許來自 http://localhost
的跨域請求):
// node.js
var express = require('express');
var app = express();
// 跨域參考:https://blog.csdn.net/gdutRex/article/details/103636581
var allowCors = function (req, res, next) {
// 注:筆者使用 `*` 仍報跨域問題,修改為請求地址(`http://localhost`)即可。
res.header('Access-Control-Allow-Origin', 'http://localhost');
res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type,lang,sfopenreferer ');
res.header('Access-Control-Allow-Credentials', 'true');
next();
};
//使用跨域中介軟體
app.use(allowCors);
// mock資料來自 amis 官網示例:https://aisuda.bce.baidu.com/amis/zh-CN/components/crud
const data1 = {"status":0,"msg":"ok","data":{"count":171,"rows":[{"engine":"Gecko - tuhzbk","browser":"Camino 1.0","platform":"OSX.2+","version":"1.8","grade":"A","id":11},{"engine":"Gecko - aias9l","browser":"Camino 1.5","platform":"OSX.3+","version":"1.8","grade":"A","id":12},{"engine":"Gecko - s72lo","browser":"Netscape 7.2","platform":"Win 95+ / Mac OS 8.6-9.2","version":"1.7","grade":"A","id":13},{"engine":"Gecko - 1uegwbc","browser":"Netscape Browser 8","platform":"Win 98SE+","version":"1.7","grade":"A","id":14},{"engine":"Gecko - tjtajk","browser":"Netscape Navigator 9","platform":"Win 98+ / OSX.2+","version":"1.8","grade":"A","id":15},{"engine":"Gecko - ux0rsf","browser":"Mozilla 1.0","platform":"Win 95+ / OSX.1+","version":"1","grade":"A","id":16},{"engine":"Gecko - a3ae5r","browser":"Mozilla 1.1","platform":"Win 95+ / OSX.1+","version":"1.1","grade":"A","id":17},{"engine":"Gecko - 55daeh","browser":"Mozilla 1.2","platform":"Win 95+ / OSX.1+","version":"1.2","grade":"A","id":18},{"engine":"Gecko - eh2p99","browser":"Mozilla 1.3","platform":"Win 95+ / OSX.1+","version":"1.3","grade":"A","id":19},{"engine":"Gecko - f6yo9k","browser":"Mozilla 1.4","platform":"Win 95+ / OSX.1+","version":"1.4","grade":"A","id":20}]}}
const data2 = { "status": 0, "msg": "ok" }
// 查詢
app.get('/amis/api/mock2/', function (req, res) {
res.end(JSON.stringify(data1));
})
// 新增
app.post('/amis/api/mock2/', function (req, res) {
res.end(JSON.stringify(data2));
})
// 監聽3000埠
var server = app.listen(3020, function () {
console.log('listening at =====> http://127.0.0.1:3020...');
});
編輯器
下載 amis-editor-demo,透過 npm i
安裝依賴,然後 npm run dev
本地啟動編輯器:
Administrator@3L-WK-10 MINGW64 /e/amis-editor-demo-master (master)
$ npm run dev
> amis-editor-demo@1.0.0 dev E:\amis-editor-demo-master
> amis dev
_/
_/_/_/ _/_/_/ _/_/ _/_/_/
_/ _/ _/ _/ _/ _/ _/_/
_/ _/ _/ _/ _/ _/ _/_/
_/_/_/ _/ _/ _/ _/ _/_/_/
當前版本:v3.1.8.
- [amis]開啟除錯模式...
<i> [webpack-dev-middleware] wait until bundle finished
assets by path *.js 66.8 MiB
assets by chunk 28.6 MiB (id hint: vendors) 53 assets
+ 61 assets
assets by info 4.13 MiB [immutable]
assets by chunk 2.92 MiB (auxiliary name: editor) 17 assets
+ 5 assets
assets by path *.css 5.07 MiB
assets by chunk 224 KiB (id hint: vendors) 2 assets
asset index.css 2.98 MiB [emitted] (name: index)
asset editor.css 1.87 MiB [emitted] (name: editor)
assets by path *.html 1.31 KiB
asset editor.html 674 bytes [emitted]
asset index.html 672 bytes [emitted]
Entrypoint index 13.7 MiB (4.06 MiB) = index.css 2.98 MiB index.js 10.7 MiB 21 auxiliary assets
Entrypoint editor 12.6 MiB (2.92 MiB) = editor.css 1.87 MiB editor.js 10.8 MiB 17 auxiliary assets
runtime modules 1.06 MiB 652 modules
orphan modules 3.34 MiB (javascript) 4.13 MiB (asset) [orphan] 106 modules
javascript modules 31.1 MiB
modules by path ./node_modules/ 31.1 MiB 3639 modules
modules by path ./src/ 38.4 KiB 22 modules
css modules 3.15 MiB
modules by path ./node_modules/monaco-editor/esm/vs/ 138 KiB 68 modules
modules by path ./node_modules/amis/ 2.65 MiB 3 modules
modules by path ./node_modules/@fortawesome/fontawesome-free/css/*.css 113 KiB 2 modules
+ 3 modules
json modules 1.93 MiB
./node_modules/amis/schema.json 1.9 MiB [built] [code generated]
./node_modules/entities/lib/maps/entities.json 28.4 KiB [built] [code generated]
webpack 5.76.3 compiled successfully in 37081 ms
√ [amis]除錯模式已開啟!
> Listening at http://localhost:80
當前執行指令碼:
http://localhost:80/index.js
當前執行樣式[可能不存在]:
http://localhost:80/index.css
(node:10788) UnhandledPromiseRejectionWarning: Error: Exited with code 4294967295
...
輸出有點錯誤,不管它,瀏覽器訪問 http://localhost:80
即可進入編輯器頁面。
下面筆者快速演示
配置一個有增加
、刪除
、編輯
、查詢
的頁面。就像這樣:
配置過程如下:
最後生成的配置在這裡(也可直接修改):
Tip:除了編輯
的url需要修改 json,其他的都可以在編輯器右側中配置。
注:目前筆者將 amis-editor 作為一個單獨的專案執行,透過 npm run build
打包,打包後的 html 中出現以 https://aisuda.github.io/amis-editor-demo/demo/
開頭的資源,引入內網是不行的,於是更改 assetsPublicPath: './'
即可指向引入打包出來的資源。目前還是有點小圖示
沒有出來,可能需要更改某些地方。
// amis.config.js
build: {
entry: {
index: './src/index.tsx',
editor: './src/mobile.tsx',
},
NODE_ENV: 'production',
assetsRoot: resolve('./demo'), // 打包後的檔案絕對路徑(物理路徑)
// assetsPublicPath: 'https://aisuda.github.io/amis-editor-demo/demo/', // 設定靜態資源的引用路徑(根域名+路徑)
assetsPublicPath: './', // 設定靜態資源的引用路徑(根域名+路徑)
...
}
中後臺整合 amis
引入 amis
下載sdk.tar.gz 解壓後把 skd 資料夾放入 spug 的 public 中,然後在單頁面中引入。就像這樣:
// spug/public.index.html
<!-- 引入 amis 的包 >
<link rel="stylesheet" href="sdk/sdk.css" />
<link rel="stylesheet" href="sdk/helper.css" />
<link rel="stylesheet" href="sdk/iconfont.css" />
<script src="sdk/sdk.js"></script>
...
<title>Spug</title>
Tip:amis 還提供了 react 的方式,也就是透過 npm 來使用,但公司分配的機器不太好,構建專案報記憶體不夠(JavaScript heap out of memory
),多次嘗試(比如:在 linux(node18)中執行、調整記憶體大小 --max-old-space-size、win7 中安裝 node14/16)也未解決,只能選用 sdk 的方式。
新建頁面
新建頁面 amis A
,用於將 amis-editor 的配置 json 渲染出前端頁面。相關程式碼有:
- store.js - 取得 amis-editor 的配置。這裡使用 mockjs 模擬,資料就是上文編輯器建立的頁面。
// spug\src\pages\amis\store.js
import { observable,} from 'mobx';
import http from 'libs/http';
class Store {
// 表格資料
@observable config = {};
@observable isFetching = false;
fetchRecords = (path) => {
http.get(`/api${path}/`)
.then(res => {
// {type: "page", title: "Hello world", body: Array(2), id: "u:7b55b5793e16", asideResizor: false, …}
console.log('res', res)
this.config = res;
})
.finally(() => this.isFetching = false)
};
}
export default new Store()
- index.js - 根據配置渲染頁面,也就是 amis 解析 json:
// spug\src\pages\amis\index.js
import React from 'react';
import { observer } from 'mobx-react';
import axios from 'axios';
import store from './store';
import _ from 'lodash';
window.enableAMISDebug = true
// amis 環境配置
const env = {
theme: 'cxd',
// 下面三個介面必須實現
fetcher: ({
url, // 介面地址
method, // 請求方法 get、post、put、delete
data, // 請求資料
responseType,
config, // 其他配置
headers // 請求頭
}) => {
config = config || {};
config.withCredentials = true;
responseType && (config.responseType = responseType);
if (config.cancelExecutor) {
config.cancelToken = new (axios).CancelToken(
config.cancelExecutor
);
}
config.headers = headers || {};
if (method !== 'post' && method !== 'put' && method !== 'patch') {
if (data) {
config.params = data;
}
return (axios)[method](url, config);
} else if (data && data instanceof FormData) {
config.headers = config.headers || {};
config.headers['Content-Type'] = 'multipart/form-data';
} else if (
data &&
typeof data !== 'string' &&
!(data instanceof Blob) &&
!(data instanceof ArrayBuffer)
) {
data = JSON.stringify(data);
config.headers = config.headers || {};
config.headers['Content-Type'] = 'application/json';
}
return (axios)[method](url, data, config);
},
isCancel: (value) => {
console.log('isCancel')
},
copy: (content) => {
console.log('copy')
}
};
function updateAmis(...options) {
if (!document.getElementById('amisbox')) {
return
}
let amis = window.amisRequire('amis/embed');
let amisScoped = amis.embed('#amisbox', ...options);
}
@observer
class AMISComponent extends React.Component {
componentDidMount() {
const path = this.props.page
// 請求對應頁面的配置
store.fetchRecords(path)
}
render() {
// 根據配置檔案重新渲染頁面
// 注:使用 lodash 的深複製不起作用
updateAmis(JSON.parse(JSON.stringify(store.config)), {}, env)
return <div id="amisbox"></div>
}
}
class APP extends React.Component {
componentWillUnmount() {
// 解除安裝時需要清空,否則切換頁面還會顯示上個頁面
store.config = {};
}
render() {
return (
<>
<AMISComponent page={this.props.location.pathname} />
</>
);
}
}
export default APP;
注:為了讓配置變更時 amis 能重新渲染頁面,筆者使用了一個 hack 的方式:JSON.parse(JSON.stringify(store.config))
。其他方法都不行(curd 存在顯示問題):Object.assign(store.config)、_.cloneDeep(store.config)、{...store.config}
Tip: 這段程式碼參考 react 引入方式的 https://github.com/aisuda/amis-react-starter/blob/main/src/App.tsx。例子中是 typescript 寫法,對於暫時不支援 ts 的專案,直接將型別去除即可使用。就像這樣:
config.cancelToken = new (axios as any).CancelToken(
config.cancelExecutor
);
// 去除 ts 的型別
config.cancelToken = new (axios).CancelToken(
config.cancelExecutor
);
import React from 'react';
import { observer } from 'mobx-react';
import axios from 'axios';
import { render as renderAmis, ToastComponent, AlertComponent } from 'amis';
import store from './store';
import _ from 'lodash';
// amis 環境配置
const env = {
theme: 'cxd',
// 下面三個介面必須實現
fetcher: ({
url, // 介面地址
method, // 請求方法 get、post、put、delete
data, // 請求資料
responseType,
config, // 其他配置
headers // 請求頭
}) => {
console.log('fetcher', method)
config = config || {};
config.withCredentials = true;
responseType && (config.responseType = responseType);
if (config.cancelExecutor) {
config.cancelToken = new (axios).CancelToken(
config.cancelExecutor
);
}
config.headers = headers || {};
if (method !== 'post' && method !== 'put' && method !== 'patch') {
if (data) {
config.params = data;
}
return (axios)[method](url, config);
} else if (data && data instanceof FormData) {
config.headers = config.headers || {};
config.headers['Content-Type'] = 'multipart/form-data';
} else if (
data &&
typeof data !== 'string' &&
!(data instanceof Blob) &&
!(data instanceof ArrayBuffer)
) {
data = JSON.stringify(data);
config.headers = config.headers || {};
config.headers['Content-Type'] = 'application/json';
}
return (axios)[method](url, data, config);
},
isCancel: (value) => {
console.log('isCancel')
},
copy: (content) => {
console.log('copy')
}
};
@observer
class AMISComponent extends React.Component {
componentDidMount() {
const path = this.props.page
// 請求對應頁面的配置
store.fetchRecords(path)
}
render() {
return renderAmis(
// store.config,
// 使用 _.cloneDeep() 報錯
JSON.parse(JSON.stringify(store.config)),
{
// props...
},
env
);
}
}
class APP extends React.Component {
componentWillUnmount() {
// 解除安裝時需要清空,否則切換頁面還會顯示上個頁面
store.config = {};
}
render() {
return (
<>
<ToastComponent key="toast" position={'top-right'} />
<AlertComponent key="alert" />
<AMISComponent page={this.props.location.pathname} />
</>
);
}
}
export default APP;
放行 amis 介面
注:spug 中 axios 被封裝到 http.js 中,雖然在 amis 中透過 axios 傳送請求,還是會走 http.js 中的攔截器(handleResponse
)
由於 amis 的資料格式和 spug 的不同,這裡暫時約定有 flag:amis
的是 amis 的介面,資料不做處理,直接放行。
// http.js
import http from 'axios'
...
// response處理
function handleResponse(response) {
// 由於 amis 的資料格式和 spug 的不同,這裡暫時約定有 flag:amis(例如:{"flag": "amis", "status": 0, "msg": "ok" }) 的是amis 的介面,資料不做處理,直接放行
const isAmis = response.data.flag === 'amis'
if(isAmis){
return Promise.resolve(response.data)
}
Tip:amis 所需的資料格式和 spug 的不同,一種方法是前端來調整。建議讓後端返回 amis 所需的資料格式,因為這是 amis 的頁面,是新功能。
node 服務
現在spug的服務介面是 3010,修改 node 服務允許其跨域:
var allowCors = function (req, res, next) {
// 注:筆者使用 `*` 仍報跨域問題,修改為請求地址(`http://localhost`)即可。
res.header('Access-Control-Allow-Origin', 'http://localhost:3010');
...
next();
};
Tip:使用 yapi(docker 方式安裝即可) 非常方便模擬資料,也無需處理跨域。
效果
amis-editor 整合到 spug 的效果如下:
自定義元件
amis-editor 也提供了自定義元件,筆者參考自定義元件-a
新建一個自定義元件-b
:
實現很簡單,就是複製一份以下檔案:
- ./renderer/MyRenderer.tsx
- ./editor/MyRenderer.tsx
spug 中的 amis 整合該自定義元件。可以這樣做:
// 自定義元件,props 中可以拿到配置中的所有引數,比如 props.label 是 'Name'
function CustomComponent(props) {
const { target } = props;
let dom = React.useRef(null);
React.useEffect(function () {
// 從這裡開始寫自定義程式碼,dom.current 就是新建立的 dom 節點
// 可以基於這個 dom 節點對接任意 JavaScript 框架,比如 jQuery/Vue 等
dom.current.innerHTML = `<p>Hello {target}! @amis-editor</p>`
// 而 props 中能拿到這個
});
return React.createElement('div', {
ref: dom
});
}
方案總結
中臺系統 + amis + amis-editor
此方案能透過視覺化的配置
實現前端頁面的開發,實現所見即所得的效果。
對於常用的頁面只需要透過視覺化配置介面就能完成前端開發。
其他章節請看: