markdown CMS自述文件

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了markdown CMS自述文件相关的知识,希望对你有一定的参考价值。

% Content Manage System
% Klook Admin Sys
% Wed Aug 23 2017 08:06:22 GMT+0800 (CST)




# What is CMS:

## Concept 概念



## 如何使用cms系统

本文主要是给开发/测试/产品同事介绍:

1. 什么是CMS系统
1. CMS适用于什么场景
1. CMS的优势与限制
1. CMS系统的大概架构(for admin)
1. 如何通过需求构建一套CMS模型
1. 如何在ADMIN系统根据需求添加一套CMS配置并可以在前端取到数据
1. web端如何获得CMS数据
1. API & DOC

## Contents 

### 什么是CMS系统

CMS - Content Manage System(内容管理系统): 通过数据配置的方式模块化开发网站静态资源, 解决网站信息管理的常见问题和需求, 比如配置主题/广告/推广活动/宣传文章等静态内容.

### CMS适用于什么场景

CMS适用于当网站有静态资源需要运营的时候, 快速给AM/EM提供一个运营工具.

### CMS的优势与限制

优势: 

- 开发快速(admin)
- 高度可配置
- 不需要后端, admin可以和各平台前端直接配合, 节约开发人力
- 因此, 前端可以同时定义和使用符合自己需求的数据格式.
- 统一的操作界面
- 最小化维护成本

限制: 

- 数据不敏感: CMS不关注其中存储的具体内容, 它只是提供了一个便捷的运营工具, 所以难以对其中的内容进行归纳或检索. 遇到内部数据需要同时在别处使用的情况会不好处理.
- 难以迁移: 由于CMS对内部数据的无力性,数据迁移也会相对而言有点复杂. 开发的时候最好基于之前的数据结构进行添加, 尽量不要改动之前的数据结构, 尽量避免数据迁移.
- 牺牲功能灵活性: CMS作为一套便捷的运营工具, 可能同时用于多个静态资源项目的管理. 为了降低维护成本. 一般情况下尽量避免对于特殊需求的支持, 为了系统可维护性会牺牲一部分灵活性, 可能遇到特殊需求的时候需要牺牲产品设计的复杂功能.


### CMS系统的大概架构(for admin)


> CMS 主要是为了解决经常重复使用UI时,同一件事情做很多次效率低下的问题.

> 通过UI(schema)与数据(source)双向绑定, 从而在获得数据结构的同时就可以自动生成UI, 在UI填写完毕自动生成对应数据。再加上预先配置好的对应数据处理方法,这样就可以在遇到新的需求的时候只需要写出最基本的数据结构就可以基本实现整个后端流程。


1. CMS分为创建,编辑,列表三个部分
1. 配置部分和内容部分分开
1. 内容部分用id取代具体多语言资源, 把通用内容和多语言内容分开.
1. 发布的时候区分通用内容的发布与多语言资源的发布.
1. 所以,当需要去做通用内容的多语言区分的时候, 需要建立多个条目
1. 组件有默认的常用组件, 也可以在配置中自定义组件UI


### 如何通过需求构建一套CMS模型

从需求中抽象出需要的数据结构和UI, 找到需要的配置项, 确认需不需要多语言配置, 需不需要配置列表.

比如客路join us可以配置为:

1. 配置项: join us是通用信息, 不需要区分显示, 可以配置为全语言, 全平台显示的信息. 
1. 编辑页面为: 最基本的单元是一个区分语言的title(数据)的input(UI), 和一个区分语言的description的markdown(UI), 存在一个object里面. 
1. join us有多条信息, 可以配置为多个generic只存单个信息或在一个generic里面配置为数组类型的结构存多条信息, 为了方便可以取后者.
1. 又因为不同语言的join us的内容长度(数组长度)不同, 所以需要区分语言去创建, 这样就需要有列表和创建页面.

###  如何在ADMIN系统根据需求添加一套CMS配置并可以在前端取到数据

CMS的配置分为:

- 配置创建页面
- 配置编辑页面
- 配置列表页面
- 路由/权限


#### 页面逻辑



如果配置项只需要配置单个静态资源也无须区分多语言, 例如首页banner, 则不需要列表和创建页面. 只需要一个编辑页面编辑唯一的条目.
否则, 需要配置创建/列表页面来适应批量管理.




#### 编辑页面

- CMS 最基本的模块是由一个数据结构(source)去配置一个编辑页面. 这个数据结构是从需求中抽象的内容,并且由这个数据结构自动生成对应的admin UI
- 每个需求对应CMS的一个type, 所以创建CMS内容的第一步是在config中指定一个新的type并写出这个type对应的从需求中抽象出的数据结构以及UI配置, 称为schema
- schema的key既数据结构(source)的key, schema是source的一种扩展, 在source上标记了UI的形式和对应参数, 用户最终会把内容填写道schema上面, 然后自动转为对应的数据(source)储存起来.

i.e.: 

最简化的版本, klook 加入我们页面的配置, 只需要title & description. 添加过程为设置type为403, 并添加schema:


    403: configJoinUs, // About页面加入我们

    function configJoinUs(type) {
        var schema = {
            schema: {
                type: 'object',
                title: "Join Us",
                object_schema: {
                    items: {
                        title: "Join Us",
                        type: 'array',
                        add_by_unshift: true,
                        array_item_schema: {
                            type: "object",
                            object_schema: {
                                "title": {
                                    "type": "text",
                                    "title": "Title",
                                    mandatory: true,
                                },
                                "desc": {
                                    "type": "markdown",
                                    "title": "Description",
                                    mandatory: true,
                                },
                            }
                        }
                    },
                }
            }
        };
        CmsConfigBuilder(type)
            .setCreateOption(create_options) // 创建页面需要
            .setSchema(schema) // 编辑页面需要
            .setListOption(list_options, list_schema) // 列表页面需要
    }


schema中的type为常用的admin UI组件,具体定义及配置需参考代码, 更复杂的需求也只是把schema改成更复杂的结构.



#### 创建页面

- CMS的配置包括自定义字段和固定字段

固定配置:每个都需要, 可以使用默认值

- language/语言, 语言如果为 "ALL" 则表示此条目不区分语言, 否则应选择特定语言进行创建.
- platform/平台
- region/地区
- type/CMS type
- title/当前条目的检索名

自定义配置: 这些字端可以根据需要使用及赋予含义

- type_id(int): 比如可以指代城市id, 活动id, ...
- type_key(string): 比如可以指代一个promo code/campaign type
- type_extra(string): 也是一个判断标志 比如 (`login` / `not_login`)
- etc... 可以在不能满足需求的时候适当添加自定义配置字段

将需要配置的项列出, i.e.:

    

    var create_options = {
        platform: true,
        platform_type: 'multiple',
        title: true,
        language: true,
        mandatory: ['platform', 'language']
    }


表示区分平台和语言并且必填, 同时支持添加一个检索标题, 也可以自定义UI. 



#### 列表页面

同理,列表页面也需要声明配置项, 其中包括筛选的配置项和列表显示的配置项. 字段同样基于CMS固有的配置字段, 除上述字段外还包括:

- create_editor
- create_time
- id
- last_modify_time
- modify_editor
- priority

可以根据需求选择需要展示的筛选项/列表展示项.

i.e.:

    var list_options = {
        platform: true,
        published_language: true,
        title: true,
    }

    var list_schema = {
        data: [{
            prop: 'id',
            name: __("global_id"),
            width: "50"
        }, {
            prop: 'create_editor',
            name: __("cms_created_by"),
            formatter(row) {
                return getUser(row.create_editor)
            }
        }, {
            prop: 'title',
            name: __('cms_title')
        }, {
            prop: 'platform',
            name: __('global_platform'),
            formatter(row, index) {
                return getPlatform(row.platform)
            }
        }, {
            prop: 'published_language',
            name: __('cms_published_lang'),
            formatter: commonLangFormattter
        }, ],
        options: {}
    }


#### 路由 && 权限 

创建完配置之后还需要在 desktop.js && nav_confg.js 里面添加路由&&权限

#### 如何添加一个没有创建配置的CMS数据

> 这种场景一般为数据必须存在且只存在一条, 比如首页/特定垂直页的banner数据、热门城市、活动数据。整个系统只会有一条

type999用于全局创建任意type的数据,url为{{path}}/content/cms/create/999

因为是通用创建窗口, type_id, type_key 都是虚拟字段,需要根据具体需求判断是否需要填写

## web端如何获得CMS数据

可以通过type id或者generic id来获得对应cms数据

参数包括: 

- type_id: 参数, number类型, 比如城市id/活动id
- type_key: 参数, string类型, 比如utm_campaign
- type_extra: 参数, string类型, 当需要另一个参数的时候使用比如login状态
- region: header, 区域, 这个是由前端传ip给后端, 后端判断返回与否
- start_time, end_time: 后端判断不需要传, 这个是在有时间限制的CMS数据中使用, 比如banner ad,
- time_need: boolean参数 `true`, 判断取CMS数据是否需要有时间限制
- platform: 不需要传,这个是后端判断的
- publish_language: 不需要传,这个是由前端传的accept-language给后端判断


## API && DOC

### type 

type表示一个配置类(模块)。每种配置对应一个type(比如首页banner, 首页推荐城市, 首页热门活动),正常每个type会有相对应的创建/编辑/列表页。 也可以一个页面里可以同时存在多个type(比如admin首页主题页)

> 添加一个新模块配置就是添加一个新type

### generic

一个generic就是一个最基本的数据对象.

> 编辑/保存等操作只能对一个generic来进行,所以之前有些分散的接口可能会被合到一起
 

```javascript
var TYPES = {
    //通用配置, 特殊处理类型
    TYPE_GENERAL: 999,

    //当季优选主题
    TYPE_THEME_SEASONAL: 111,

    //首页Banner
    TYPE_HOME_BANNER: 101,

    //首页热门城市设置
    TYPE_HOME_DESTINATION: 102,

    //首页的热卖活动
    TYPE_HOME_ACTIVITIES: 103,

    //活动页帮助内容
    TYPE_ACTIVITY_HELP_CONTENT: 104,

    //brand页面配置数据
    TYPE_BRAND: 105,

    //brand页面 theme 配置数据
    TYPE_CAMPAIGN_THEME: 1051,

    //城市页面的主题
    TYPE_CITY_THEME: 121,

    // destination page template theme
    TYPE_CITY_TEMPLATE_THEME: 122,
}
```


### type_id && type_key && type_extra

针对generic增加的项, 用来应对不同的需求可能会有的新的字段(比如一个type下面要针对城市/活动建立generic,就要用type_id表示城市/活动)。type_key 同理(比如campaign page reference url)

### conf_json && conf_json_draft

conf_json 就是generic里面存数据的字段,即vue里面的source。以JSON stringify 字符串的形式存储。保存会保存到conf_json_draft, 发布会才会出现在conf_json里面。

### title

即reference title, 用于在列表里面查generic数据

> 这个在创建页面填写, 和编辑页面的title没有关系。

### platform

支持的平台。 二进制的形成存储。 全选是 `Ob1111`javascript

```javascript
platformOptions: [{
    prop: 0b1000,
    name: __('global_platform_web')
}, {
    prop: 0b0100,
    name: __('global_platform_mobile')
}, {
    prop: 0b0011, // 0b0010: ios; 0b0001: android
    name: __('global_platform_app')
}],
```
不填/不传的话表示支持全平台

```javascript
var platform = _.reduce(data.platform, (sum, val) => sum |= val, 0b0) || 0b1111;
```

### language

支持的语言,创建的时候选择。 若支持所有语言, 此字段为空(不选/不传)。

```javascript
var language = data.language ? [].concat(data.language) : [];
```

### region

传地区的字段,  二进制,

```javascript
region: data.region || 0b11111,
```

### source

source 就是接口的结构,和普通的接口结构没有区别, 也是schema的*source-of-truth*

### schema 

schema就是UI的结构,结构的层级需要和source里面的每个字段一一对应,然后添加相关的ui信息进去。

### example


#### schema

```javascript
var schema = {
    schema: {
        type: 'object',
        title: __("citypage_articles_setting"),
        object_schema: {
            articles: {
                type: 'array',
                title: 'Articles',
                array_item_schema: {
                    type: 'object',
                    object_schema: {
                        title: {
                            type: 'text',
                            title: 'Article Title',
                            madatory: true,
                        },
                        subtitle: {
                            type: 'text',
                            title: 'Article Subtitle',
                            madatory: true,
                        },
                        article_url: {
                            type: 'text',
                            title: 'Article URL',
                            madatory: true,
                        },
                        article_image: {
                            title: 'Image',
                            type: 'imageUpload',
                            madatory: true,
                        }
                    }
                }
            },
        }
    },
    methdos: {},
}
```
#### conf_json(source):

```javascript
{
    "articles": [
        {
            "title": "title",
            "subtitle": "sub",
            "article_url": "url",
            "article_image": "adsf"
        },
        {
            "title": "2",
            "subtitle": "2",
            "article_url": "2",
            "article_image": "2"
        },
        {
            "title": "11",
            "subtitle": "1",
            "article_url": "",
            "article_image": ""
        }
    ]
}
```

#### admin http response

```javascript
{
    "id": 291,
    "platform": 15,
    "region": 31,
    "conf_json": "{\"articles\":[{\"title\":\"title\",\"subtitle\":\"sub\",\"article_url\":\"url\",\"article_image\":\"adsf\"},{\"title\":\"2\",\"subtitle\":\"2\",\"article_url\":\"2\",\"article_image\":\"2\"},{\"title\":\"11\",\"subtitle\":\"1\",\"article_url\":\"\",\"article_image\":\"\"}]}",
    "conf_json_draft": "{\"articles\":[{\"title\":\"title\",\"subtitle\":\"sub\",\"article_url\":\"url\",\"article_image\":\"adsf\"},{\"title\":\"2\",\"subtitle\":\"2\",\"article_url\":\"2\",\"article_image\":\"2\"},{\"title\":\"11\",\"subtitle\":\"1\",\"article_url\":\"\",\"article_image\":\"\"}]}",
    "type": 123,
    "type_id": 76,
    "type_key": "",
    "priority": 182,
    "language": [
        "en_US"
    ],
    "title": "qwer",
    "create_editor": "5",
    "modify_editor": "5",
    "invalid": 1,
    "last_modify_time": "2017-08-21 07:02:56",
    "create_time": "2017-08-15 03:05:17",
    "flag": 0
},
```

#### web http response

```javascript
{
    "error": {
        "code": "",
        "message": ""
    },
    "result": [
        {
            "content": {
                "articles": [
                    {
                        "article_image": "adsf",
                        "article_url": "url",
                        "subtitle": "sub",
                        "title": "title"
                    },
                    {
                        "article_image": "2",
                        "article_url": "2",
                        "subtitle": "2",
                        "title": "2"
                    },
                    {
                        "article_image": "",
                        "article_url": "",
                        "subtitle": "1",
                        "title": "11"
                    }
                ]
            },
            "generic_id": 291,
            "platform": 15,
            "priority": 182,
            "region": 31,
            "type": 123,
            "type_id": 76,
            "type_key": ""
        }
    ],
    "success": true
}
```



## Pages 目录结构

### HTML

```
web/views/desktop
├── pages
│   ├── cms
│   │   ├── create.html     // 创建
│   │   ├── edit.html       // 编辑
│   │   ├── list.html       // 列表
│   │   └── resource.html   // 多语言资源编辑
├── partials
│   ├── components.html     // vue 组件
```


### JS

```
web/views/desktop/pages/cms
├── cms_create.js   // 创建
├── cms_edit.js     // 编辑
├── cms_list.js     // 列表
├── cms_resource.js // 资源
├── config.js       // 统一配置
└── methods.js      // 方法库(cmslib)
```

### create

默认default配置

```javascript
var defaults = {
    platform: false,
    platform_type: 'single', // 'multiple'
    region: false,
    language: false,
    type_id: false,
    type_id_schema: '',
    title: false,
    type_key: false,
    madatory: [], // 必填字段
    methods:{
        next(){ ... }
    },
}
```

最后生成: 

```javascript
    window.vvv = new Vue({
        el: '#app',
        data: {
            source: {
                "source": cmslib.getDefaultItem(schema)
            },
            schema: schema,
        },
        methods,
    })
```

example: 

```javascript
    var theme_citypage_options = {
        platform: true,
        platform_type: 'multiple',
        title: true,
        language: true,
        type_id: true,
        type_id_schema: {
            type: 'city',
            hideunpub: false,
            title: __('cms_theme_destination')
        },
        mandatory: ['platform', 'language', 'type_id'],
        methods:{},
    }
```

### edit

根据 config.js 里面设置的 schema/methods, 和获取到的conf_json, 最后生成:

```javascript
Vue.component(`conf${id}`, {
    template: '#conf_template',
    props: ['platform', 'id', 'language', 'title', 'type_key'],
    data() {
        return Object.assign({
            schema: config.schema,
            source: {
                conf_json,
            },
        }, config.data)
    },
    methods: config.methods
})
```

methods 里面default了一个save方法。

[example](#example)


### list

list页面设置一个筛选的schema和一个列表的schema,筛选schema类似创建页面的schema

列表操作default为:
```javascript
    var default_list_options = {
        has_manage_lang: true,
        has_edit: true,
        has_delete: true,
        has_edit_title: true,
        has_edit_priority: true,
    }
```

example:

```javascript
    var theme_citypage_list_options = {
        platform: true,
        title: true,
        published_language: true,
        type_id: true,
        type_id_schema: {
            type: 'city',
            title: __("act_destination")
        },
    }

    var theme_citypage_list_schema = {
        data: [{
            prop: 'id',
            name: __("global_id")
        }, {
            prop: 'create_editor',
            name: __("cms_created_by"),
            formatter(row) {
                return getUser(row.create_editor)
            }
        }, {
            prop: 'type_id',
            name: __("global_city"),
            formatter(row, index) {
                return getCity(row.type_id)
            }
        }, {
            prop: 'title',
            name: __('cms_title')
        }, {
            prop: 'platform',
            name: __('global_platform'),
            formatter(row, index) {
                return getPlatform(row.platform)
            }
        }, {
            prop: 'priority',
            name: __('cms_priority')
        }, {
            prop: 'published_language',
            name: __('cms_published_lang'),
            formatter: commonLangFormattter
        }, ],
        options: {}
    }
```

### config 
 
配置页面config.js添加一个新的type:

```javascript
CmsConfigBuilder(TYPES.TYPE_CITY_TEMPLATE_THEME)
    .setCreateOption(theme_citypage_template_create) // 创建页面
    .setSchema(theme_citypage_template_edit) // 编辑页面
    .setListOption(theme_citypage_template_list_options, theme_citypage_template_list_schema) // 列表页面筛选options,列表table
```

然后添加对应的schema即可。

## Usage

### ui-components

结合components.html 以及 methods.js 生成。\
基本类型的ui-template都有`type,title,mandatory`字段 \
每个类型独有的字段如下:

```javascript
{
    type: 'array',
    array_item_schema: {
        type: ...
    }
}, {
    type: 'object',
    object_schema: ...
}, {
    type: 'text/reference',
    input: 'input/textarea'
}, {
    type: 'radio/checkbox',
border    options: [{
            prop: 'foo',
            name: 'bar'
        },
        {
            prop: 'foo',
            name: 'bar'
        },
    ]
}, {
    type: 'switch/toggle',
    text: 'foo'
}, {
    type: 'dropdown',
    multiple: true,
    group: false,
    options: [
        {
            value: 'foo',
            label: 'bar'
        }
    ]
}

```

## TODO

* unify i18n resource handle logic.
* build config with type. 
* /cms/resource generate form with item's original type instead of just input

## Readme 文档

管理端获取conf_json, admin: <https://dash.readme.io/legacy/project/activity/v1.0/docs/prosrvcontentconfgenenic_idsquery>

web端获取数据,new-web: <https://dash.readme.io/legacy/project/activity/v1.0/docs/v1usrcsrvgenericgeneric_id0-9>

API 1.

```
  /v1/usrcsrv/generic/{generic_id}

  {
  is_parser: Boolean,
  type: Number
  }
```

API 2.

```

/v1/usrcsrv/generic/types/{type}



{
type_keys string (code,des) 可以是多个 
platform int
 
region int
 
page int (1)
 
limit int (15)
 
type_id int (0)
 
is_parser boolean (true)
 
time_need string
}

```

/v1/usrcsrv/generic/types/{types}
/v1/usrcsrv/generic/{generic_id:[0-9]+}?type={type_id}


后端文档

https://phabricator.klook.com/w/cms%E5%86%85%E5%AE%B9%E5%8F%91%E5%B8%83%E7%B3%BB%E7%BB%9F/

数据格式文档

`{admin url}/readme/cms`, i.e. admin1.klook.io/readme/cms


## TODO 

fj:

根据今天的cms问题, 我想了一下, 我梳理了一下cms配置系统的使用场景

1. 前端提交上来用户的各种请求筛选条件(国家地区/语言/平台)
2. cms根据请求的条件去generic的列里面做字段匹配, 然后筛选出来一系列记录
3. 条件匹配在语言, 国家, 平台等字段,因为值可以穷举, 所以我们目前是通过位运算实现了一条Generic记录可以匹配到多个值的情况
4. 但是对于一个Generic可以关联到多个type_key和type_id的时候, 目前的这个架构是不能处理的, 目前只能做相等处理

也就是如果有这种多个搜索的key匹配一个generic的情况下, 我们是不能做的, 但是这种需求总归来说还是以后还会发生, 比如之前的活动faq, 我们是在另外一个cms 类型里面做了一个映射表, 虽然也是达到了效果, 但是是用一个多一次查询操作的方式来达到的.

再配合今天通知模块的这个问题, 虽然我从需求上是可以说服产品对于要关联多个内容的地方进行多次冗余配置, 但是这也是因为目前cms不支持这种多个type_key和type_id映射到一个generic的情况

所以我在想我们是否可以在generic的外面再加一层key和id的映射表, 这个表结构如下:

1. map_key:     varchar
2. map_id:      int
3. generic_id:  int

然后其中map_id, 使用的场景是在一个generic需要关联到多个数字id的时候, map_key的场景是在一个generic需要关联到多个字符串id的时候. 

假设某个需求我们的一个genericid 为1的数据, 需要关联两个活动  39, 10

那么我们就有记录:

cms_map_key, cms_map_id, cms_generic_id
'',         39,         1
'',         10,         1

这种情况下如果有请求进来, 如果用户指定了map_id为39或者map_id为10的情况下, 都可以关联到generic_id 为1的generic

需要处理的情况:

map_id和map_key只用一种(上面的表是否需要分开, 后端来决定), map_id和map_key逻辑类似

1. 新增一个generic的时候, 如果有map_id匹配, 在表中插入多条map_id的匹配记录
2. 更新一个generic的时候, 如果有map_id的匹配关系修改, 应该是删掉没有的map_id, 然后新增新的map_id
3. 删除generic的时候是否要map这个后端来决定

不知道大家觉得是否有必要实现这个内容, 你们也可以看看之间是否有一些需求如果有这个是否就也可以用cms来做




以上是关于markdown CMS自述文件的主要内容,如果未能解决你的问题,请参考以下文章

markdown Gist嵌入 - 隐藏自述文件

markdown 自述文件FR,iOS / Xcode

markdown 在Github自述文件中并排显示图像

markdown 管理员自述

markdown 个人自述

GitHub 与 Markdown 搞混了 - 将 666 更改为 DCLXVI