随着互联网飞速发展,越来越多的传统服务搬到了线上,商家急需一个官网介绍自己的产品,提高知名度。因此 “建站” 成为了一种刚需。本文就如何用 vue.js 实现一个建站应用提供了解决思路。
作为前端工程师,相信大家都写过不少网站和应用,我把网站简单的分为 “表现型” 和 “操作型”。表现型可以是一个产品介绍网站,而操作型的典型代表是管理后台。不久前有机会参与一个建站项目的设计与开发,它属于操作类型网站但需要更深一层的抽象,它是一个 “创建网站的网站”。本文尝试基于 vue.js 框架设计实现这样一个应用,用户通过拖拽模块就可以建站。希望通过本文的介绍,能够带给大家不一样的视角。
获取需求是开始项目的第一步。一般来说建站工具大多提供以下功能:
我们在开始动手之前可以先分析一下需求。
从需求中可以提取到几个关键词:“模板”、“主题色”、“模块”、“页面”、“分栏” 。明确这些关键词的意义将有助于我们接下来的设计。
那么问题来了,我们应该如何存储我们的网站呢?换句话说,存在数据库里面的是一个怎样的数据结构?是一个包含 html 的超大字符串吗?不急,基于上面的关键词定义我们可以总结一下:
用 JSON 格式可以把它表示成:
- {
- "id": 1,
- "name": "xxx公司""type": "site",
- "children": [{
- "type": "page",
- "name": "首页",
- "children": [{
- "type": "section",
- "name": "公司简介",
- "children": [{
- "type": "paragraph"
- },
- {
- "type": "carousel"
- }]
- }]
- }]
- }
中增加
- site.config
属性来表示。
- themeColor
表示该板块在手机下横向占 100%、平板占 50%、PC 占 25%。
- class="col-xs-12 col-sm-6 col-md-3"
综上,一个网站可以完整的表示为一个树形 JSON。该树中包含了所有页面、板块、模块的内容和配置。
我们已经有了网站的数据表示,那么下一个问题是如何从数据中渲染出网站呈现给用户呢?其实我们只要想办法渲染这棵 JSON 树就行了。
两步走:
属性和
- node
- themeColor
第一步是个 “体力活”,此处以单段文字模块为例:
- <!-- Paragraph.vue -->
- <template>
- <div>
- <h1 :style="{color: themeColor}">{{node.content.title}}</h1>
- <small v-if="node.config.showSubTitle">{{node.content.subTitle}}</small>
- <p>{{node.content.detail}}</p>
- </div>
- </template>
- <script>
- export default {
- name: 'paragraph',
- props: ['node', 'themeColor']
- }
- </script>
完成所有节点代码编写之后,第二步,我们需要写一个类似于 “renderer” 的组件来递归的渲染 JSON 树。基本思路是该组件先渲染自己,然后渲染自己的后代,每个后代也重复此渲染过程,如此渲染整棵树。
这里需要根据节点的
属性也就是一个 String 来获取对应的组件定义。幸运的是 Vue.js 中已经有这样的动态组件
- type
,此组件的
- Component
属性接受一个 String。由此我们的 render 组件可以这样写:
- is
- <!-- render.vue -->
- <tempplate>
- <
- component
- :is
- =
- "node.type"
- :node
- =
- "node"
- :theme
- =
- "themeColor"
- >
- <
- render
- v-for
- =
- "child in node.children"
- :key
- =
- "child.id"
- :node
- =
- "child"
- :theme
- =
- "themeColor"
- />
- </component>
- </tempplate>
- <script>
- // 导入JSON 树中所涉及的所有节点
- import Page from './Page.vue'
- import Section from './Section.vue'
- import Paragraph from './Paragraph.vue'
- export default {
- name: 'render',
- props: ['node', 'themeColor'],
- components: {
- Page,
- Section,
- Paragraph
- }
- }
- </script>
注:若 Vue.js 没有提供动态 Component 组件,我们也可以利用 Vue.js 中的
方法自己实现该组件,详见此 gist( gist.github.com/github-libr… )。
- createElement
至此,我们已经设计了网站的数据表示,以及从数据到页面的渲染。那么这棵 JSON 树从何而来呢?
要创建一个网站,用户在后台会经历选择样板站、调整色调、拖拽模块、编辑模块内容和配置、保存等操作。值得注意的是,此处用户选择的样板站跟文章开头的模板定义有些差别。如果说模板是网站的骨架的话,那样板站是填充了初始数据(默认色调、板块中包含的模块以及模块的默认内容)的模板。
从数据的角度来看,可以更清楚地看到这些步骤是如何逐步生成这棵树的。
界面操作 | 影响数据 |
---|---|
选择模板(样板站) | 该模板定义的初始树,包含默认的色调和模块 |
选择色调 | 更新
|
拖拽模块到区域中 | 在对应的的数组中 push 一个组件节点 |
在区域中排序模块 | 在对应的的数组中重新设置组件节点的 index |
编辑模块内容和配置 | 更新对应模块的 content 和 config 中的属性 |
保存网站 | 把 JSON 树存入数据库持久化 |
既然选择了 Vue.js,我们可以选择官方推荐的 Vuex( vuex.vuejs.org/en/ )来管理状态。
首先创建一个 Vuex 实例,该实例包含一个 site 对象和一些对节点的操作:
- // store.js
- import Vue from 'vue'
- import Vuex from 'vuex'
- Vue.use(Vuex)
- const store = new Vuex.Store({
- state: {
- site: {}
- },
- mutations: {
- changeThemeColor () {},
- addModule () {},
- sortModule () {},
- removeModule () {},
- updateModule () {}
- },
- actions: {
- getSite () {},
- saveSite () {}
- }
- })
理论上有了这些方法我们已经可以从通过程序更新这棵树了,但作为编辑后台,我们还要提供一些界面上的入口让用户来编辑这棵树。
也就是说,我们需要在编辑后台渲染一个可以编辑的网站,在网站上线后渲染一个只读的网站,即同一个 JSON 树需要渲染两次。因为 renderer 是根据节点的 type 来渲染对应的组件的,所以对于编辑后台我们需要给每个节点取另一个 type,比如统一加一个前缀
。例如 Paragraph 这个组件的编辑组件可以编写如下:
- edit-
- <!-- EditParagraph.vue -->
- <template>
- <edit-wrapper>
- <
- paragraph
- :node
- =
- "node"
- :themeColor
- =
- "themeColor"
- />
- </edit-wrapper>
- </template>
- <script>
- // EditWrapper提供统一的编辑入口,内部仍需要渲染一次 Paragraph 便于实时预览编辑结果
- import EditWrapper from './EditWrapper.vue'
- import Paragraph from './Paragraph.vue'
- import { mapMutations } from 'vuex'
- export default {
- name: 'edit-paragraph',
- props: ['node', 'themeColor'],
- methods: { ...mapMutations(['updateModule']) }
- }
- </script>
用户可以通过这个组件的界面入口编辑 Paragraph 这个模块(此处略去
的实现,事实上组件的编辑可以通过弹窗加表单实现,也可以是更方便的 inline editing )。
- edit-wrapper
如此一来,每个模块都有一个对应的编辑模块,在两次渲染的时候便存在一个转换的过程。假设在数据库中存的是只读的树,那么在编辑后台获取到该树时需要转换成可编辑树从而渲染成带有编辑入口的网站,在保存时则需要转换成只读树保存。转换的过程其实很简单,把每个节点的 type 属性增加或删除前缀即可。
我们选用拖拽的方式在区域中加入模块以及排序模块。有很多开源的库帮我们做了拖拽的实现,甚至有 Vue.js 的封装。此处推荐
( github.com/SortableJS/… )这个库,它是基于 Sortable.js 做的一层封装。其典型的应用场景如下:
- Vue.Draggable
- <
- draggable
- v-model
- =
- "myArray"
- :options
- =
- "{group:'people'}"
- @
- start
- =
- "drag=true"
- @
- end
- =
- "drag=false"
- >
- <div v-for="element in myArray">{{element.name}}</div>
- </draggable>
在我们的场景中,只需在
组件上监听
- draggable
和
- add
事件调用 store 中对应的方法即可。
- sort
如本节表格所述,拖拽只是界面上的操作方式,本质上是对数组中元素的增加和调整 index 的操作。
为了及时保存用户的编辑,我们可以在用户修改主题色、编辑模块、删除模块等操作时自动保存。本质上是需要检测 store 中 site 这个状态的变更。Vuex 中的插件(plugin)可以针对改动做一些类如记录日志和持久化的操作,我们可以写一个 autoSave 的插件来实现。
- // store.js
- const autoSave = (store) = >{
- store.watch(state = >state.site, (newV, oldV) = >{
- store.dispatch('saveSite')
- },
- {
- deep: true
- })
- }
- const store = new Vuex.Store({
- state: {
- site: {}
- },
- plugins: [autoSave]
- })
至此,我们已经用 Vue.js 和相关技术实现了文章开头列出的建站应用的需求。
本文从需求分析、方案设计、编码实现分别介绍了用 Vue.js 写一个建站应用可能会遇到的问题以及大致的思路。可以看出,文章很大篇幅都在讲数据,包括数据格式的设计、数据的操作、数据变更的检测。这是因为 Vue.js 框架帮我们做了从数据到界面的渲染以及数据变更后界面的更新的工作,我们要做的是管理好这些数据。Vue.js 框架分担了我们的工作,提高了开发效率,使得我们可以专注于业务逻辑设计,这也是框架价值的体现。
作者:唐鹤俊
简介:百姓网前端工程师。本文仅为作者个人观点,不代表百姓网立场。
本文在 “百姓网技术团队” 微信公众号首发,扫码立即订阅:
来源: https://juejin.im/post/5a04045f5188251c306d78c7