簡單5分鐘,將lowcode低代碼融入到你的中後台管理系統

語言: CN / TW / HK

背景

你是否在做中後台項目中經常要重複做crud的業務邏輯,花費大量時間還時常有bug發生,但是現在只要幾分鐘就能讓你快速連通前後端,拖拉拽實現後台業務邏輯。你就問香不香!

技術選型

🚀 選用百度出品的amis低代碼開源框架, 基於amis-editor(React + TS),我們做的事情就是通過封裝json數據上報、配置、自定義組件等,實現低代碼管理後台實時更新,無需手動寫json配置。如果你要在Vue中使用當然也可以。

👍 簡單一句話: 你不用敲代碼了!!

先附上github地址 https://github.com/ccj-007/lowcode-editor-send

amis官方文檔 https://github.com/baidu/amis

amis-editor編輯器 https://github.com/aisuda/amis-editor-demo

在原框架上實現了哪些功能

  1. 支持url路由跳轉對應的配置頁面
  2. 支持歷史記錄修改
  3. 支持預覽
  4. 支持重置
  5. 支持配置更新前端lowcode頁面(不用敲代碼嘍!!!)
  6. 通過路由及項目名配置查詢
  7. 支持切換環境

如何使用

npm i //安裝依賴 npm run start //通過devserve啟動前端頁面 npm run server //啟動node服務,默認3001端口

注意

1. 本地調試請在server文件夾下定義好文件名,本地調用通過文件名對應路由名。如果需要數據庫連接,請定義好項目名和路由名。json配置在原來基礎上,已經做了一個包裹, 核心數據配置在json屬性內,為了方便定位以及後期維護擴展。

{ "json": { "type": "page", "title": "Hello world", "body": [ ] }, "routeName": "test2.json", "itemName": "cms2" }

核心

``` //src/App.tsx
import React from 'react'; import { Editor } from 'amis-editor'; import './App.css' import axios from 'axios'

interface StateType { json: any routeName: string itemName: string preview: boolean historyList: any[] step: number maxHistoryNum: number baseURL: string useTestBaseURL: string isLocalTest: boolean }

class App extends React.Component { constructor(props: any) { super(props) this.state = { json: { type: "page" //確保是頁面層級 }, routeName: 'client-admin', //默認為'' itemName: "cms2", //默認為'' preview: false, historyList: [], step: 0, maxHistoryNum: 10, baseURL: window.localStorage.getItem('baseURL') || 'https://dev.zzss.com', //正式開發使用 useTestBaseURL: 'http://localhost:3001', //本地調試環境切換使用 isLocalTest: true, //用於本地調試環境,正式開發請設置為false } } componentDidMount() { //獲取url query this.checkQuery() setTimeout(() => { this.getJSON() }, 0) }

getJSON = () => { let { routeName, itemName, isLocalTest, baseURL, useTestBaseURL, } = this.state

if (!routeName || !itemName) {
  alert('請傳入必要參數')
  return
}
let url = isLocalTest ? useTestBaseURL : baseURL
//這裏要請求對應的路由數據
axios.post(url + '/api/getJSON',
  {
    routeName: this.state.routeName,
    itemName: this.state.itemName
  },
).then((res: any) => {
  if (res.data.success === false) {
    alert(res.data.msg)
    return
  }

  let obj = res.data
  this.clearJSON()
  let newObj = this.changeBaseURLtoDomain(obj)

  this.setState({
    json: newObj,
    historyList: [...this.state.historyList, newObj],
  }, () => {
    console.log("獲取到最新的JSON", this.state.json);
  })
}).catch((e) => {
  alert("獲取後端json失敗" + JSON.stringify(e))
})

}

sendJSON = () => { let { routeName, itemName, isLocalTest, baseURL, useTestBaseURL } = this.state if (this.state.json.type !== 'page') { alert('請確保在頁面層級更新json') return } if (!routeName || !itemName) { alert('請傳入必要參數') return } let obj = this.chengeDomaintoBaseURL(this.state.json)

let url = isLocalTest ? useTestBaseURL : baseURL
axios.post(url + '/api/setJSON', {
  json: obj,
  routeName: this.state.routeName,
  itemName: this.state.itemName
},
  {
    headers: {
      'Content-Type': 'application/json'
    }
  }
).then((res) => {
  if (res.data.success === false) {
    alert(res.data.msg)
    return
  }

  if (res && res.data && res.data.json) {
    alert("配置成功")
    let obj = res.data.json
    this.setState({
      json: obj
    })
  }
}).catch((e) => {
  alert("存入配置失敗" + JSON.stringify(e))
})

} //監聽lowcode的json改變 handleChange = (e: any) => { if (!e) return this.setState({ json: e, historyList: [...this.state.historyList, e], step: this.state.step + 1 }, () => { let { historyList, maxHistoryNum } = this.state if (historyList.length > maxHistoryNum) { let limitObj = [...historyList].splice(-maxHistoryNum) this.setState({ historyList: limitObj, step: this.state.step - 1 }) } console.log("change", this.state.historyList); }) } //獲取query checkQuery = () => { let itemName = this.getQueryString('itemName') let routeName = this.getQueryString('routeName') if (itemName && routeName) { this.setState({ itemName, routeName }) } } // 獲取查詢字符串 getQueryString = (name: string) => { var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)"); var r = window.location.search.substr(1).match(reg); if (r != null) { return unescape(r[2]); } else { return null }; } //監聽項目名輸入 inputItemName = () => { //@ts-ignore let val = this.refs.itemName.value; this.setState({ itemName: val }) } //監聽路由輸入 inputRouteName = () => { //@ts-ignore let val = this.refs.routeName.value; this.setState({ routeName: val }) } //開始預覽 startPreview = () => { this.setState({ preview: !this.state.preview }) } //重置 clearJSON = () => { this.setState({ json: {} }) } //上一步 backHistoryJSON = () => { let { step, historyList } = this.state if (step - 1 >= 0) { this.setState({ step: step - 1, }, () => { this.setState({ json: historyList[this.state.step] }) }) } else { alert('您當前沒有歷史記錄') } } //下一步 goHistoryJSON = () => { let { step, historyList } = this.state let curStep = historyList.length - 1 if (step < curStep) { this.setState({ step: step + 1, }, () => { this.setState({ json: historyList[this.state.step] }) }) } else { alert('已經是最新!') } }

//根路徑 inputUrlName = () => { //@ts-ignore let val = this.refs.baseURL.value; this.setState({ baseURL: val, }, () => { window.localStorage.setItem('baseURL', this.state.baseURL) }) }

//轉為domain changeBaseURLtoDomain = (obj: any) => { let { baseURL } = this.state if (!baseURL) return let str = JSON.stringify(obj) let res = str.replace(/${baseURL}/g, baseURL) return JSON.parse(res) } //轉為${baseURL} chengeDomaintoBaseURL = (obj: any) => { let { baseURL } = this.state if (!baseURL) return let str = JSON.stringify(obj) let urlReg = new RegExp(baseURL, 'g') let res = str.replace(urlReg, '${baseURL}') return JSON.parse(res) }

render() { return ( <>

項目名: this.inputItemName()} /> 路由名: this.inputRouteName()} /> 設置baseURL: this.inputUrlName()} />

) } }

export default App; ```

後端服務

```

//server/app.js 用於調試服務端 const http = require("http"); const fs = require('fs'); const path = require('path');

/ * 失敗數據模型 * @param {} msg 消息 / function errModel (msg) { let obj = { success: false, msg } return JSON.stringify(obj) }

http.createServer(function (req, res) { res.setHeader('Access-Control-Allow-Origin', ''); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); res.setHeader('Content-Type', 'application/json;'); res.setHeader("Access-Control-Allow-Methods", "DELETE,PUT,POST,GET,OPTIONS"); console.log(req.url); console.log(req.method); if (req.method == 'OPTIONS') { res.writeHead(200, { 'Content-Type': 'text/plain', 'Access-Control-Allow-Origin': '', 'Access-Control-Allow-Headers': 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild, sessionToken', 'Access-Control-Allow-Methods': 'PUT, POST, GET, DELETE, OPTIONS' }); res.end(''); }

if (req.method === 'POST' && req.url === '/api/setJSON') { let item = ''; // 讀取每次發送的數據 req.on('data', function (chunk) { item += chunk.toString(); }); // 數據發送完成 req.on('end', function () { let items = JSON.parse(item) if (items.routeName && items.itemName) { let file = path.join(__dirname, ${items.routeName}.json) // json文件需要存入路徑 fs.writeFileSync(file, item) //將數據返回到客户端 res.write(item); res.end(); } else { res.write(errModel('文件配置失敗, 檢查路由或項目名是否正確')); res.end(); } }); }

if (req.method === 'POST' && req.url === '/api/getJSON') { let item = ''; // 讀取每次發送的數據 req.on('data', function (chunk) { item += chunk.toString(); }); // 數據發送完成 req.on('end', function () { let items = JSON.parse(item)

  if (items.routeName && items.itemName) {
    let file = path.join(__dirname, `${items.routeName}.json`)

    fs.readFile(file, 'utf-8', function (err, data) {
      if (err) {
        console.log(err);
        res.write(errModel('請檢查路由是否正確'));
        res.end();
      } else {
        let obj = JSON.parse(data)
        res.write(JSON.stringify(obj.json));
        res.end();
      }
    });
  } else {
    res.write(errModel('請檢查路由或項目名是否正確'));
    res.end();
  }
});

}

}).listen(3001); // 監聽的端口 ```

如何在Vue的前端項目中使用 ?

1. 在靜態目錄public中的index.html引入對應的sdk,sdk官網有可以自行下載

```

```

2. 在路由允許的情況下調用封裝的方法,即可渲染lowcode頁面

``` import Vue from 'vue' import defaultConfig from "./config"; import axios from 'axios'

var timer = null

let defaultOptions = { method: 'local', // 'http' | 'local' 通過接口返回或者本地靜態文件夾獲取 routeName: '', //輸入路由名(必填) itemName: '', //項目名(必填) } let newOptions //修改後的配置 /* * 在路由允許的情況下調用可生成對應lowcode頁面 * @param {DOM} DOM * @param {Object} options / export const getLowcodePage = (DOM, options = {}) => { newOptions = Object.assign(defaultOptions, options) let { routeName } = newOptions if (!DOM || !routeName) { throw new Error('DOM or routeName is no exist') }

//handle first render error
const check = (routeName) => {
  let dom = document.querySelector(DOM)
  if (dom) {
    getJsonFs(routeName)
    if (!timer) {
      clearTimeout(timer)
    }
  } else {
    timer = setTimeout(() => {
      check(routeName)
    }, 0)
  }
}

//get json
const getJsonFs = (routeName) => {
  if (newOptions.method === 'local') {
    Vue.http.get(`lowcode/pages/${routeName}.json`, {}, { emulateJSON: true }).then((res) => {
      let obj = JSON.parse(res.bodyText)
      if (obj) {
        startAmis(obj)
      }
    }).catch((error) => {
      console.log("error", error);
    })
  }


  if (newOptions.method === 'http') {
    //正式項目需要通過post請求傳入對象{routeName, itemName}
    //目前調試使用,注意某些跨域情況在vue.config.js中做跨域代理
    axios.get('/api/getJSON').then((res) => {
      let { data } = res
      startAmis(data)
      console.log('http', data);
    }).catch((e) => {
      alert("獲取後端json失敗" + JSON.stringify(e))
    })
  }
}

//amis render
const startAmis = (jsonObj) => {
  console.log("jsonObj", jsonObj);
  let amis = window.amisRequire('amis/embed');
  amis.embed(DOM, jsonObj, {
    data: {
      baseURL: process.env.VUE_APP_API_BASE_URL
    }
  }, defaultConfig
  )
}

//entrance
check(routeName)

} ```

3. 做跨域代理

//vue.config.js devServer: { proxy: { //測試lowcode使用 '/api': { target: 'http://localhost:3001', changeOrigin: true, }, } },

4. 開始調用方法

```

```