随着互联网飞速发展,愈来愈多的传统服务搬到了线上,商家急需一个官网介绍本身的产品,提升知名度。所以 “建站” 成为了一种刚需。本文就如何用 Vue.js 实现一个建站应用提供了解决思路。javascript
做为前端工程师,相信你们都写过很多网站和应用,我把网站简单的分为 “表现型” 和 “操做型”。表现型能够是一个产品介绍网站,而操做型的典型表明是管理后台。不久前有机会参与一个建站项目的设计与开发,它属于操做类型网站但须要更深一层的抽象,它是一个 “建立网站的网站”。本文尝试基于 Vue.js 框架设计实现这样一个应用,用户经过拖拽模块就能够建站。但愿经过本文的介绍,可以带给你们不同的视角。html
获取需求是开始项目的第一步。通常来讲建站工具大多提供如下功能:前端
咱们在开始动手以前能够先分析一下需求。vue
从需求中能够提取到几个关键词:“模板”、“主题色”、“模块”、“页面”、“分栏” 。明确这些关键词的意义将有助于咱们接下来的设计。java
页面:一个网站由一个或多个页面(Page)组成。node
分栏/分区:页面由不一样的功能区组成,好比公司介绍、成功案例、联系咱们等。它们多是纵向排列的,也可能左右分栏排布,咱们取个更恰当的名字 “区块”(Section)。git
模块:每一个区块包含一个或多个组件,这些组件组合起来达到同一个目的。例如公司介绍这个区块能够用一段文字模块介绍公司大致状况,用一个轮播模块展现公司主打产品。这里所说的模块和咱们熟悉的 “组件”(Component)划等号,是咱们要实现的最小功能单元。github
模板:模板能够有不少种定义。此处咱们能够理解为一个页面的布局,相似于 QQ 空间能够选择的分栏布局。模板定义了一个页面包含的区块数量和每一个区块的横向占比。vuex
主题色:每一个网站都应有本身的风格。能够简单认为:风格 = 模板 + 主题色,不一样的模板搭配不一样的主题色造成了不一样风格的网站。数据库
那么问题来了,咱们应该如何存储咱们的网站呢?换句话说,存在数据库里面的是一个怎样的数据结构?是一个包含 HTML 的超大字符串吗?不急,基于上面的关键词定义咱们能够总结一下:
用 JSON 格式能够把它表示成:
{
"id": 1,
"name": "xxx公司"
"type": "site",
"children": [{
"type": "page",
"name": "首页",
"children": [{
"type": "section",
"name": "公司简介",
"children": [{
"type": "paragraph"
}, {
"type": "carousel"
}]
}]
}]
}复制代码
那么模块就是树中的叶子节点,需求中要求模块能够配置,咱们能够把配置分为两部分:包含的内容(content)和设置(config)。举例来讲,轮播模块中 content 存放的是几张图片的 URL,config 能够是轮播切换的动画效果、是否开启自动播放等设置。
与模块同样,site、page、section 都是树中的节点,均可以根据须要在节点上增长 content 和 config。只不过对于这几类节点来讲 content 其实就是 children,只有 config 属性。
主题色是网站节点的配置项,能够在 site.config
中增长 themeColor
属性来表示。
如何支持手持设备呢?前面说到模板定义了板块的横向占比,因此在板块的 config 属性中能够配置该板块在不一样尺寸的横向占比。若采用 Bootstrap 的 12 栏栅格系统的话,能够很方便的经过设置 class 来达到目的。例如某个板块的 config 中 class="col-xs-12 col-sm-6 col-md-3"
表示该板块在手机下横向占 100%、平板占 50%、PC 占 25%。
综上,一个网站能够完整的表示为一个树形 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 树。基本思路是该组件先渲染本身,而后渲染本身的后代,每一个后代也重复此渲染过程,如此渲染整棵树。
这里须要根据节点的 type
属性也就是一个 String 来获取对应的组件定义。幸运的是 Vue.js 中已经有这样的动态组件 Component
,此组件的 is
属性接受一个 String。由此咱们的 render 组件能够这样写:
<!-- 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 中的 createElement
方法本身实现该组件,详见此 gist( gist.github.com/github-libr… )。
至此,咱们已经设计了网站的数据表示,以及从数据到页面的渲染。那么这棵 JSON 树从何而来呢?
要建立一个网站,用户在后台会经历选择样板站、调整色调、拖拽模块、编辑模块内容和配置、保存等操做。值得注意的是,此处用户选择的样板站跟文章开头的模板定义有些差异。若是说模板是网站的骨架的话,那样板站是填充了初始数据(默认色调、板块中包含的模块以及模块的默认内容)的模板。
从数据的角度来看,能够更清楚地看到这些步骤是如何逐步生成这棵树的。
界面操做 | 影响数据 |
---|---|
选择模板(样板站) | 该模板定义的初始树,包含默认的色调和模块 |
选择色调 | 更新 site.config.themeColor |
拖拽模块到区域中 | 在对应的 section.children 的数组中 push 一个组件节点 |
在区域中排序模块 | 在对应的 section.children 的数组中从新设置组件节点的 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,好比统一加一个前缀 edit-
。例如 Paragraph 这个组件的编辑组件能够编写以下:
<!-- 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 这个模块(此处略去 edit-wrapper
的实现,事实上组件的编辑能够经过弹窗加表单实现,也能够是更方便的 inline editing )。
如此一来,每一个模块都有一个对应的编辑模块,在两次渲染的时候便存在一个转换的过程。假设在数据库中存的是只读的树,那么在编辑后台获取到该树时须要转换成可编辑树从而渲染成带有编辑入口的网站,在保存时则须要转换成只读树保存。转换的过程其实很简单,把每一个节点的 type 属性增长或删除前缀便可。
咱们选用拖拽的方式在区域中加入模块以及排序模块。有不少开源的库帮咱们作了拖拽的实现,甚至有 Vue.js 的封装。此处推荐 Vue.Draggable
( github.com/SortableJS/… )这个库,它是基于 Sortable.js 作的一层封装。其典型的应用场景以下:
<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
和 sort
事件调用 store 中对应的方法便可。
如本节表格所述,拖拽只是界面上的操做方式,本质上是对数组中元素的增长和调整 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 框架分担了咱们的工做,提升了开发效率,使得咱们能够专一于业务逻辑设计,这也是框架价值的体现。
做者:唐鹤俊
简介:百姓网前端工程师。本文仅为做者我的观点,不表明百姓网立场。
本文在 “百姓网技术团队” 微信公众号首发,扫码当即订阅: