從零到一自建雲表格系統

語言: CN / TW / HK

從零到一自建雲表格系統

大部分業務系統、管理後臺 70% 的功能是資料表格的增刪改查。這些功能的實現,以複製貼上修改欄位為主。本文將嘗試用一種更優雅的 “複製貼上” 方式編寫此類功能。如果你希望告別枯燥乏味的重複勞動,或者需要自建一套雲表格系統,接下來的內容可以幫到你。

DEMO 演示

通過表格編輯器,線上設計製作表格類功能模組。適用於使用者管理、供應商管理、訂單管理等表格 CURD 操作的功能模組。

image.png

res.png

原始碼地址: https://github.com/YaoApp/demo-widget

**一鍵部署: **https://letsinfra.com/openapp/demo-widget

構建工具

YAO 開源應用引擎

yao-arch-white.png YAO 是一款開源應用引擎,使用 Golang 編寫,以一個命令列工具的形式存在, 下載即用。適合用於搭建業務系統、網站/APP API 介面、管理後臺等應用程式。

YAO 採用 flow-based 的程式設計模式,通過編寫 YAO DSL (JSON 格式邏輯描述) 或 JavaScript 處理器,實現各種功能。 YAO DSL 可以有多種編寫方式:

  1. 純手工編寫
  2. 使用自動化指令碼,根據上下文邏輯生成
  3. 使用視覺化編輯器,通過“拖拉拽”製作

**GitHub 地址: ** https://github.com/yaoapp/yao
Github Stars: 4.3K 開源協議: Apache 2.0 官方文件: https://yaoapps.com/doc

YAO Widget

YAO 將通用功能模組抽象為 Widget,通過 YAO DSL 描述差異,快速複製各種功能。 在開發中,如果開發一個相似的功能模組,需要將已有的功能複製貼上,批量替換差異內容。 YAO 提供一種新方式 ,使用 DSL 描述差異內容,快速“複製貼上”一個功能模組,以此來提升開發效率。

**YAO 內建了一組 Widgets, **涵蓋大部分應用程式開發常見功能。

內建 Widget 功能
model 資料結構建模
flow 資料邏輯編排
api RESTFul API
table 表格 CURD
chart 資料圖表
login 使用者登入
register 使用者註冊
task 併發任務
schedule 計劃任務
socket Socket
websocket WebSocket

Widget 支援開發者自定義,可基於自身業務邏輯特徵,自定義 DSL, 快速複製各種通用功能。本文將通過自定義 Widget 的方式,實現一套雲表格系統。

實戰: 極簡實用的雲表格系統

Part I: 製作 dyform Widget

準備工作: 安裝 YAO 命令

閱讀以下內容,需要具備基礎程式設計能力,熟悉 RESTFul API ,關係資料庫,JavaScript 語言等程式設計常識。 參考文件 入門指南 , 在本地安裝 YAO 命令 ( 版本: v0.10.1),並熟悉基本使用方法。
**在本地建立一個應用: **

# 建立一個專案資料夾
mkdir -p /your/project/root

# 初始化專案
yao init

# 建立資料表 & 初始化選單
yao migrate && yao run flows.setmenu

第一步: 設計 Widge DSL 資料結構

根據業務邏輯特徵,設計 DSL 結構,本例中,不同表格間的差異為 表格欄位、搜尋器以及表格名稱。 **dyform Widget DSL 資料結構: **

{
  "name": "TEST",
  "decription": "A TEST DYFORM",
  "columns": [
    {
      "title": "First Name",
      "name": "name",
      "type": "input",
      "search": true,
      "props": {
        "placeholder": "Please input your first name"
      }
    },
    {
      "title": "Amount",
      "name": "amount",
      "type": "input",
      "search": true,
      "props": {
        "placeholder": "Please input amount"
      }
    },
    {
      "title": "Description",
      "name": "desc",
      "type": "textArea",
      "props": {
        "placeholder": "Please input Description"
      }
    }
  ]
}

欄位描述 DSL :

配置項 說明
title 在表格中顯示的標題
name 欄位名稱, 對應資料庫欄位名
type 填寫表單時,使用的元件
props 元件屬性
search 該欄位是否可以作為篩查項

建立 dyforms 檔案,用於儲存 DSL

cd /your/project/root
mkdir dyforms

將上面 DSL 儲存到 **dyforms/demo.form.json **檔案,用於除錯。

第二步: 建立 dyform Widget

建立 dyform widget:

cd /your/project/root
mkdir -p widgets/dyform

# 建立 widget 檔案
touch widgets/dyform/widget.json  # Widget 基本資訊
touch widgets/dyform/compile.js   # dyform DSL 編譯器
touch widgets/dyform/process.js   # dyform 處理器
touch widgets/dyform/export.js    # dyform 匯出內建 Widget 定義

提示: YAO 自定義 widget 放置在 widgets 目錄,需要手動建立這個目錄。

使用編輯器開啟 ** dyform/widget.json** 填寫 dyform widget 基本資訊

{
  "label": "Dynamic Form",
  "description": "A form widget. users can design forms online",
  "version": "0.1.0",
  "root": "dyforms",
  "extension": ".form.json",
  "modules": ["Models", "Tables"]
}

**欄位說明: **

配置項 說明
label Widget 名稱,在視覺化編輯器顯示。
description Widget 介紹, 在視覺化編輯器顯示。
version 版本號
root DSL 檔案儲存路徑(相對於專案根目錄)
extension DSL 副檔名
modules 需要匯出的模組。 在 export.js 中根據 DSL 描述,轉換的 YAO 內建 widgets。 如 model, table 等。這些 widgets 與儲存在專案目錄中的 DSL 檔案等效。

第三步: 編寫 dyform 處理器

編寫 **dyform/process.js **指令碼,實現 **Model, Table **處理器,將自定義 DSL 轉換為 YAO 資料模型 widget 和 表格 widget。

編寫 Export 函式,宣告要匯出的處理器, 檢視原始碼

function Export() {
  return { Model: "Model", Table: "Table" };
}

實現 Model & Table 處理器邏輯

處理器 說明 原始碼連結
Model 將 dyform DSL 轉換為 model DSL, 等效於在 models 資料夾手動編寫一個 xxx.mod.json DSL 檔案 檢視原始碼
Table 將 dyform DSL 轉換為 table DSL, 等效於在 tables 資料夾手動編寫一個 xxx..json DSL 檔案 檢視原始碼

提示: 建議使用 yao run 命令除錯處理器,參考原始碼註釋文件。

第四步: 匯出為 Model & Table widget

編寫 dyform/export.js 指令碼, 實現 Models, Tables 函式,匯出為 YAO model & table widgets.

匯出函式 說明 原始碼連結
Models 呼叫 Model 處理器,匯出 Yao model widget, 資料結構為 {MODEL_NAME:String: Model_DSL:Object} 檢視原始碼
Tables 呼叫 Table 處理器, 匯出為 Yao table widget , 資料結構為: {TABLE_NAME:String: Table_DSL:Object} 檢視原始碼

效果預覽

在 dyforms 資料夾下,新增 **xxx.form.json **檔案, 編寫 dyform DSL 描述檔案,即可快速建立一組表格管理模組、表格 API、表格處理器和模型處理器。

demo.png

使用 yao migrate 命令建立資料表結構,yao start 命令啟動服務,登入管理介面,即可使用表格管理功能。 路由地址: http://127.0.0.1:5099/xiang/table/dyform.xxx

Part II: 將 dyform DSL 儲存到資料庫,實現動態讀寫

如果希望把表格製作功能開放給使用者,且在通常情況下程式碼目錄應設定為只讀,這就無法動態建立 DSL 檔案。對 dyform widget 稍加改造,將 dyform DSL 儲存在資料庫中即可。

第一步: 建立一個 template model, 儲存 dyform DSL

如何建立使用資料模型 models/template.mod.json 和管理表格 tables/template.tab.json, 參考Model 文件Table 文件 models/template.mod.json 如下

{
  "name": "Template",
  "table": { "name": "template", "comment": "For dyform DSL source" },
  "columns": [
    { "label": "ID", "name": "id", "type": "ID", "comment": "ID" },
    { "label": "Name", "name": "name", "type": "string", "index": true },
    {
      "label": "DSL",
      "name": "dsl",
      "type": "text",
      "comment": "DSL"
    }
  ],
  "values": [],
  "option": { "timestamps": true, "soft_deletes": true }
}

DSL 管理介面 admin.png

第二步: 給 dyform Widget 新增 Save & Delete 處理器

編寫 Export 函式,宣告要匯出的處理器, 檢視原始碼

function Export() {
  return { Model: "Model", Table: "Table", Save: "Save", Delete: "Delete" };
}

實現 Save & Delete 處理器邏輯

處理器 說明 原始碼連結
Save 根據 dyform DSL 轉換為 model DSL, 更新對應資料模型結構,同時重新載入對應例項。 檢視原始碼
Delete 刪除資料模型對應資料表 檢視原始碼

編輯 tables/template.tab.json,新增 hooks 指令碼,在表格儲存和刪除時,更新/刪除資料表,建立/刪除選單。

**修改 tables/template.tab.json 新增 Hook **檢視原始碼** **

  "hooks": {
    "after:save": "scripts.template.AfterSave",
    "after:delete": "scripts.template.AfterDelete"
  },

**新增 hooks 指令碼 scripts/template.js **檢視原始碼

Hook 說明
AfterSave 根據 DSL ,更新 dyform 資料表,並新增選單
Delete 刪除 dyform 資料表,並移除選單

第三步: 給 dyform Widget 新增內容源

編輯 widgets/dyform/compile.js , 從資料庫中讀取 DSL

/**
 * Source
 * Where to get the source of DSL
 */
function Source() {
  var sources = {};
  tpls = Process("models.template.Get", { select: ["id", "dsl"], limit: 1000 });
  if (tpls.code && tpls.message) {
    log.Error("Load dyform sources: %s", tpls.message);
    return sources;
  }

  tpls.forEach((tpl) => {
    tpl = tpl || {};
    try {
      instance = `instance_${tpl.id}`;
      dsl = JSON.parse(tpl.dsl);
      sources[instance] = dsl;
    } catch (e) {
      log.Error("Source %v DSL: %s", tpl.id, e.message);
      return;
    }
  });

  return sources;
}

**Compile.js 方法說明 **檢視文件

Hook 說明
Source 自定義 DSL 資料來源讀取邏輯
Compile 根據上下文或環境資訊,調整 DSL 結構或準備相關資源
OnLoad 當 DSL 載入完成,呼叫此方法。

效果預覽

登入管理後臺,動態新增 dyform DSL ,即可動態建立刪除對應表格。 admin2.png res.png

Part III: 視覺化表單編輯器

可以使用表單製作工具來生成 form DSL。有很多優秀的開源表單編輯器可以選擇,比如 XXXX , 為了演示效果,XGEN 快速實現了一個 DEMO 級的編輯器元件,產品級的實現將在 XGEN-NEXT (正式版) 釋出時提供。 原始碼地址: https://github.com/YaoApp/xgen/tree/main/src/cloud/components/form/FormPrinter image.png

第一步: 適配編輯器元件輸入輸出

為 template 表格新增 Hook, 用來轉換 DSL 結構。 編輯 tables/template.tab.json 檔案、 scripts/template.js檔案

**新增 **before:save** hook ** 將編輯器元件輸出,轉換為 form DSL
tables/template.tab.json

{
  ...
	"hooks": {
    "before:save": "scripts.template.BeforeSave",
    "after:save": "scripts.template.AfterSave",
    "after:delete": "scripts.template.AfterDelete"
  }
  ...
}

scripts/template.js

/**
 * BeforSave Hook: transform the form editor data to the DSL
 * @param {*} payload
 */
function BeforeSave(payload) {
  payload = payload || {};
  columns = payload.dsl || [];
  payload["dsl"] = { columns: [], name: payload.name || "UNTITLE" };
  columns.forEach((column) => {
    let type = column.id || "";
    payload["dsl"].columns.push({
      title: column.title || "UNTITLE",
      name: column.bind,
      type: type.toLowerCase(),
      props: column.props || {},
    });
  });
  payload["dsl"] = JSON.stringify(payload["dsl"]);
  return [payload];
}

tips : 可是使用 yao run命令除錯例如:

yao run scripts.template.BeforeSave '::{"dsl":...}'

新增 **after:find** hook 將 form DSL 轉換為元件輸入結構

tables/template.tab.json

{
  ...
  "hooks": {
    "after:find": "scripts.template.AfterFind",
    "before:save": "scripts.template.BeforeSave",
    "after:save": "scripts.template.AfterSave",
    "after:delete": "scripts.template.AfterDelete"
  }
  ...
}

scripts/template.js

/**
 * AfterFind Hook: transform the DSL format to the form editor needs
 * @param {*} id
 * @param {*} template
 */
function AfterFind(template, id) {
  let dsl = JSON.parse(template["dsl"]);
  let columns = dsl.columns || [];
  let types = { input: "Input", select: "Select" };

  template["dsl"] = [];
  columns.forEach((column) => {
    // let props = column.props || {};
    // let search = props.showSearch ? true : false;
    template["dsl"].push({
      title: column.title,
      bind: column.name,
      id: types[column.type],
      props: column.props || {},
      search: true,
      width: 6,
      chosen: false,
      selected: false,
    });
  });

  return template;
}

第二步: 更新 DSL 繫結的元件

編輯 tables/template.tab.json 檔案, 將 DSL 欄位編輯元件設定為 FormPrinter。 原始碼地址: https://github.com/YaoApp/demo-widget/blob/main/tables/template.tab.json

{
  ....
  "Form Editor": {
      "label": "Form Editor",
      "edit": { "type": "FormPrinter", "props": { "value": ":dsl" } }
  }
  ...
}

效果預覽

開啟 templates 表格,點選編輯一個模板,即可使用視覺化編輯器製作表單。 image.png

Part IV: 製作 C 端表單頁面

對於活動報名、預約註冊等 C 端頁面,需要個性化設計來提升使用者體驗。對於此類場景,可以有針對性的設計一組模板,使用任意前端技術棧實現。這裡提供一個 VUE3 實現的 DEMO。 **原始碼地址: **https://github.com/YaoApp/demo-widget-front

第一步: 新增一個 API,用來讀取表格配置資訊

新增一個 GET /page/form/setting/:name API , 在 apis 資料夾下,新增一個 page.http.json 檔案, 新增一個介面,第一個引數為 表格名稱.

{
  "name": "Table Setting",
  "version": "1.0.0",
  "description": "Table Setting",
  "group": "page",
  "guard": "-",
  "paths": [
    {
      "path": "/form/setting/:name",
      "method": "GET",
      "guard": "-",
      "process": "xiang.table.Setting",
      "in": ["$param.name", ":query"],
      "out": { "status": 200, "type": "application/json" }
    }
  ]
}

原始碼地址: https://github.com/YaoApp/demo-widget/blob/main/apis/page.http.json

第二步: 在 SETUP 時,請求配置介面,獲取配置資訊。

const url = `${window.location.protocol}//${window.location.host}/api/page/form/setting/${formName}`;
const response = fetch(url);
response
  .then((res) => {
    return res.json();
  })
  .then((data) => {
    formTitle.value = data.name;
    const fieldset = data?.edit?.layout?.fieldset || [];
    const columns = fieldset.length > 0 ? fieldset[0]?.columns : [];
    const mapping = data?.columns || {};
    columns.forEach((col: any) => {
      let column = mapping[col.name] || false;
      if (column?.edit) {
        let name = column.edit.props?.value || "";
        column.edit.label = col.name;
        column.edit.field = name.replace(":", "");
        column.edit.type = components[column.edit.type];
        formItems.value.push(column.edit);
      }
    });

    loading.value = true;
  })
  .catch((err) => {
    console.log("ERR", err);
    failure.value = err.message;
  });

原始碼地址: https://github.com/YaoApp/demo-widget-front/blob/main/vue3/src/App.vue

第三步: 動態渲染表單

使用 component 元件,根據配置渲染表單

<template>
  <div v-if="loading">
    <div class="header">{{ formTitle }}</div>
    <form>
      <div class="form-item" v-for="item of formItems" :key="item.label">
        <div class="label">{{ item.label }}</div>
        <component :is="item.type" :name="item.field"></component>
      </div>
      <div class="form-item" v-if="formItems">
        <button class="button" type="button">Submit</button>
      </div>
    </form>
  </div>
  <div v-else-if="failure" class="failure">{{ failure }}</div>
  <div v-else>
    <div>Loading...</div>
  </div>
</template>

原始碼地址: https://github.com/YaoApp/demo-widget-front/blob/main/vue3/src/App.vue

第四步: 將製品複製到 UI 目錄

YAO 內建了一個 HTTP server,將製品複製到 ui 目錄即可訪問

npm run build
cp -r dist ../demo-widget/ui/vue3

原始碼地址: https://github.com/YaoApp/demo-widget/tree/main/ui/vue3

效果預覽

開啟瀏覽器,輸入 http://your-host:port/xiang/login/admin 進入後臺,錄入並儲存一個表單。 預設使用者名稱: [email protected] 預設密碼: A123456p+

image.png

開啟 http://your-host:port/vue3/?form=dyform.instance_1 即可訪問自定義表單頁面 image.png