大話APP配置功能的設計和落地

在參與各類app業務開發的過程當中,大部分都會遇到須要對某些功能/界面/數據能夠靈活的管理後臺控制,客戶端根據配置變化而變化,不須要發版本就能夠解決這些需求,大體功能需求就是須要提供一個後臺功能,可以給產品/運營童鞋進行配置管理,而後經過服務端接口輸出給客戶端進行邏輯/渲染使用,這裏針對這種場景,分享一個相對通用的解決方案php

項目背景

當前項目中針對這種配置的需求,每次都須要開發人員從新開發後臺表單,而後修改配置接口針對配置進行輸出,由於這個功能的開發要歸宿到很早之前,也不知道當初爲啥要這麼作,如今存在的問題就是不容易維護和拓展,以及重複開發的成本html

整理需求

  • 配置管理後臺
    • 支持版本控制
    • 支持客戶端類型(安卓/IOS/全部)
    • 表單可配置
  • 配置輸出接口
    • 增量下發
    • 保證高可用,高穩定,高性能
  • 客戶端
    • 接口下發配置數據進行緩存

技術背景

  • 管理後臺:php服務端+jquery+bootstrap
  • 接口項目:php服務端

技術過程

  • 前端技術選型:前端

    • vuejs
    • element ui
  • 核心問題,如何後臺配置生成表單(開發人員來配置)?vue

初步計劃是經過配置表單的JSON生成element ui的表單,進行了一些調研,也找到能夠經過配置JSON生成element ui表單的js庫,感受靈活性差了些,並且當時還不支持富文本,感受後續拓展也是大問題,因此棄用,後面嘗試本身來實現,經過vuejs+element ui組件相對簡單的方式實現了這個配置表單的功能,可以支持基本需求,具體看後面代碼(簡單粗暴)mysql

  • 接口數據增量下發,以及客戶端獲取配置時機和緩存策略

客戶端每次啓動的時候去獲取一次配置,緩存【配置數據】,新增配置添加到緩存,已經存在進行替換
接口輸出【配置數據】的同時在響應頭上【timestamp】= 帶上當前請求的服務器時間戳
客戶端獲取數據,緩存【配置數據】&【timestamp】
客戶端下次請求的頭上帶上【timestamp】= 緩存的時間戳,第一次請求能夠不用
服務端接收到請求的時候獲取客戶端的【timestamp】,過濾配置的時候校驗最後更新時間>=【timestamp】進行輸出【配置數據】jquery

  • 保障高可用,高穩定,高性能,容錯

配置數據進行多級緩存,第一級緩存【redis】,第二級緩存【服務器內存】(php apcu)
接口優先從【服務器內存】中獲取,若是不存在從【redis】 並同步到【服務器內存】,不存在從【mysql】 並同步到【redis】,正常後臺編輯完就同步到redis,【服務器內存】就進行短暫性的緩存(3s),保障在高併發的狀況下能夠快速下發,弊端就是數據變化的時候會延遲N/s後更新git

客戶端在獲取緩存配置的時候若是不存在須要本身有個默認配置,極端狀況下沒法獲取配置的容錯機制,保障功能的正常運行github

解決方案

配置管理列表界面: web

配置管理

配置添加和表單JSON配置界面(開發人員操做): redis

配置管理

配置數據表單界面(產品/運營童鞋操做):

配置管理

前端框架/庫:

主要的代碼內容,以下:

表設計:

-- 配置中心表
CREATE TABLE `config_center` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增ID',
  `title` varchar(100) NOT NULL DEFAULT '' COMMENT '標題',
  `code` varchar(60) NOT NULL DEFAULT '' COMMENT '標識',
  `platform` tinyint(4) NOT NULL DEFAULT '0' COMMENT '0=全部,1=IOS,2=安卓',
  `template` tinyint(4) NOT NULL DEFAULT '1' COMMENT '模板標識',
  `form_json` text NOT NULL COMMENT '表單JSON',
  `form_data` text NOT NULL COMMENT '表單數據',
  `description` varchar(255) NOT NULL DEFAULT '' COMMENT '描述',
  `app_version` varchar(15) NOT NULL DEFAULT '' COMMENT 'app版本',
  `app_version_compare` varchar(10) NOT NULL DEFAULT '' COMMENT 'app版本比較符號',
  `operator` varchar(20) NOT NULL DEFAULT '' COMMENT '編輯人',
  `create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
  `update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新時間',
  `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '狀態,1=有效,-1=刪除',
  PRIMARY KEY (`id`),
  KEY `index_code` (`code`),
  KEY `index_update_at_platform` (`update_at`,`platform`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

複製代碼

表單配置JOSN內容:

[
    {
        el: "input",
        type: "textarea",
        name: "名字",
        field: "name",
        value: "6666",
        rule: [
            {
                required: true,
                message: "請輸入活動名稱",
                trigger: "blur"
            }
        ]
    },
    {
        el: "input-number",
        type: "",
        name: "數字",
        field: "number",
        value: 1,
        min:1,
        max:1000,
        rule: [
            {
                required: true,
                message: "數字",
                trigger: "blur"
            }
        ]
    },
    {
        el: "input",
        type: "text",
        name: "描述",
        field: "desc",
        value: "",
        rule: [
            {
                required: true,
                message: "請輸入活動名稱",
                trigger: "blur"
            }
        ]
    },
    {
        el: "editor",
        type: "",
        name: "富文本",
        field: "editor",
        value: "",
        rule: [
            {
                required: true,
                message: "請輸入內容",
                trigger: "blur"
            }
        ]
    },
    {
        el: "date",
        type: "datetimerange",
        name: "日期範圍",
        field: "datetime",
        value: ["2019-01-01 10:00:00", "2019-03-01 08:00:00"],
        rule: [
            {
                required: true,
                message: "必須",
                trigger: "blur"
            }
        ]
    },
    {
        el: "switch",
        type: "",
        name: "開關",
        field: "open",
        value: false,
        rule: [
            {
                required: true,
                message: "必須",
                trigger: "blur"
            }
        ]
    },
    {
        el: "date",
        type: "datetime",
        name: "活動時間",
        field: "datet",
        value: "2019-01-01"
    },
    {
        el: "slider",
        type: "",
        name: "範圍",
        field: "fw",
        value: 0,
        max: 500
    },
    {
        el: "color",
        type: "",
        name: "顏色",
        field: "color",
        value: ""
    },
    {
        el: "radio",
        type: "",
        name: "類型",
        field: "type",
        value: 0,
        options: [
            {
                label: "類型1",
                value: 1
            },
            {
                label: "類型2",
                value: 2
            },
            {
                label: "類型3",
                value: 3
            }
        ]
    },
    {
        el: "select",
        type: "",
        name: "食品",
        field: "foods",
        value: "黃金糕",
        options: [
            {
                value: 1,
                label: "黃金糕"
            },
            {
                value: 2,
                label: "雙皮奶"
            },
            {
                value: 3,
                label: "蚵仔煎"
            },
            {
                value: 4,
                label: "龍鬚麪"
            },
            {
                value: 5,
                label: "北京烤鴨"
            }
        ]
    },
    {
        el: "checkbox",
        type: "",
        name: "城市",
        field: "city",
        value: [0],
        options: [
            {
                value: 1,
                label: "上海"
            },
            {
                value: 2,
                label: "深圳"
            },
            {
                value: 3,
                label: "北京"
            }
        ]
    }
];
複製代碼

vuejs + element ui 表單模板主要代碼(簡單粗暴)

<el-form size="small" :rules="rules" ref="form" :model="form" label-width="80px">
    <el-form-item v-for="(item,index) in formData" :key="item.key" :label="item.name" :prop="item.field">
        <!--input 輸入框-->
        <el-input v-if="item.el==='input'" :type="item.type" style="width:400px" v-model="form[item.field]"></el-input>
    
        <!--input 數字輸入框-->
        <el-input-number v-if="item.el==='input-number'" :min="item.min" :max="item.max" v-model="form[item.field]"></el-input-number>
    
        <!--datetime 時間-->
        <el-date-picker v-if="item.el==='date'" :type="item.type" v-model="form[item.field]" placeholder="選擇日期時間">
        </el-date-picker>
    
        <!--switch 開關-->
        <el-switch v-if="item.el==='switch'" v-model="form[item.field]" active-text="" inactive-text="">
        </el-switch>
    
        <!--滑塊-->
        <el-slider v-if="item.el==='slider'" v-model="form[item.field]" :max="item.max?item.max:100"></el-slider>
    
        <!--顏色選擇-->
        <el-color-picker v-if="item.el==='color'" v-model="form[item.field]"></el-color-picker>
    
        <!--單選-->
        <el-radio v-if="item.el==='radio'" v-for="(option,index) in item.options" :key="option.key" v-model="form[item.field]" :label="option.value">
            {{ option.label }}
        </el-radio>
    
        <!--多選-->
        <el-checkbox-group v-if="item.el==='checkbox'" v-model="form[item.field]">
            <el-checkbox v-for="(option,index) in item.options" :key="option.value" :label="option.value">
                {{ option.label }}
            </el-checkbox>
        </el-checkbox-group>
    
        <!--選擇器-->
        <el-select v-if="item.el==='select'" v-model="form[item.field]" placeholder="請選擇">
            <el-option v-for="option in item.options" :key="option.value" :label="option.label" :value="option.value">
            </el-option>
        </el-select>
    
        <!-- 富文本-->
        <quill-editor v-if="item.el==='editor'" v-model="form[item.field]"></quill-editor>
    </el-form-item>                
</el-form>

複製代碼

js代碼:

//富文本組件
Vue.use(VueQuillEditor);
$vm = new Vue({
    el: "#app",
    data: {
        template: "1",
        form: {},
        rules: {},
        formData: {},
    },
    methods: {
        useTemplate: function () {
            switch (this.template) {
                case "1": {
                    var formJson = [];
                    if (this.config['form_json']) {
                        formJson = this.config['form_json'];
                    } else if (templateOneJson) {
                        formJson = templateOneJson;
                    }
                    editorJson(formJson);
                    this.createForm(formJson);
                    return
                }
            }
        },
        //預覽
        review: function () {
            var jsonData = editor.get();
            this.createForm(jsonData);
        },
        //根據配置的JSON,解析出構造表單須要的Vue數據
        getData: function (json) {
            var data = {
                //表單數據
                form: {},
                //表單驗證規則
                rules: {},
                //表單控件配置
                formData: {}
            };
            //構造數據
            for (var index in json) {
                var item = json[index];
                data.form[item.field] = item.value;
                if (item.rule) data.rules[item.field] = item.rule;
            }
            data.formData = json;
            return data;
        },
        //建立表單Vue對象
        formVue: function (data) {
            Vue.set($vm, "form", data.form);
            Vue.set($vm, "rules", data.rules);
            Vue.set($vm, "formData", data.formData);
            // $vm.$forceUpdate();
        },
        //根據配置JSON生成Form表單
        createForm: function (json) {
            var data = this.getData(json);
            console.log(data);
            this.formVue(data);
        }
    }
});

複製代碼

前端部分由於基於原有項目技術背景拓展,用最原始的link引入方式,並且沒有拉到前端同窗參與,前端部分若是能夠把後臺功能進行先後端分離,而後基於組件化封裝那就最好不過了,存後端童鞋折騰想一想就好,low了點,能用哈,不過不影響基本實現思路可借鑑參考

總結

當你在開發產品需求時候,除了要解決眼前的問題,是否有思考過以前或者未來也會遇到不少相似的問題。把你的解決方案從解決一個問題擴展到解決一類問題是一項很是重要的能力,也每每是區分新人與資深技術人員的一條分界線


首發於Github🌈大話WEB開發,歡迎Star 🥰

相關文章
相關標籤/搜索