大型单页面应用的进阶挑战

2015/09/30 · HTML5,
JavaScript ·
单页应用

原文出处: 林子杰(@Zack__lin)   

阅读须知:这里的大型单页面应用(SPA Web
App)是指页面和功能组件在一个某个量级以上,举个栗子,比如
30+个页面100+个组件,同时伴随着大量的数据交互操作和多个页面的数据同步操作。并且这里提到的页面,均属于
hash 页面,而多页面概念的页面,是一个独立的 html
文档。基于这个前提,我们再来讨论,否则我怕我们 Get 不到同一个 G 点上去。

前端发展与现状

大家都知道前端是由HTML、CSS、Js组成的,一开始这样写出来的页面,不能局部加载,复用性比较差,重复工作比较多。微软就推出了ifram标签,就是相当于在网页中嵌套一个网页,切换目录只是切换ifram中的网页,还是直接加载某个完整的html界面。接着ajax的出现,实现了局部刷新,优化了用户体验。后来进入了jQuery时代,jQuery封装了很多原生方法,减少了代码量。现在我们前端进入了前后端分离时代,流行
MV* 框架(MVC、MVP、MVVM),MVVM框架有Angular、Vue、React。

威尼斯人线上娱乐 1

MVVM框架

现在我们后台管理系统是基于Vue开发的单页面应用(SPA)。

挑战一:前端组件化

基于我们所说的前提,第一个面对的挑战是组件化。这里还是要强调的是组件化根本目的不是为了复用,很多人根本没想明白这点,总是觉得造的轮子别的业务可以用,说不定以后也可以用。

其实前端发展迭代这么快,交互变化也快,各种适配更新层出不穷。今天造的轮子,过阵子别人造了个高级轮子,大家都会选更高档的轮子,所以现在前端界有一个现象就是为了让别人用自己的轮子,自己使劲不停地造。

在前端工业化生产趋势下,如果要提高生产效率,就必须让组件规范化标准化,达到怎样的程度呢?一辆车除了底盘和车身框架需要自己设计打造之外,其他标准化零件都可以采购组装(专业学得差,有啥谬误请指正)。也就是说,除了
UI
和前端架构需要自己解决之外,其他的组件都是可以奉行拿来主义的,如果打算让车子跑得更稳更安全,可以对组件进行打磨优化完善。

说了这么说,倒不如看看徐飞的文章《2015前端组件化框架之路》 里面写的内容都是经过一定实践得出的想法,所以大部分内容我是赞成而且深有体会的。

Vue简单介绍

1、Vue.js是一个构建数据驱动的web框架
2、Vue.js实现了数据的双向绑定和组件化
3、Vue.js只需要关系数据的变化,无需繁琐的获取和操作dom
比如给一个元素绑定事件并赋值,jQuery的做法是:

<input class="ipt" type="text">
<button class="btn"></button>
<script type="text/javascript">
$.ready(function () {
        $('.ipt').value();
        $('.btn').click(function() {    
        })
 })
</script>

vue的写法是:

<input class="ipt" v-model="value" type="text">
<button class="btn" @click="click"></button>

.vue文件的写法

<template>
    这里写HTML
</template>
<script type="text/ecmascript-6">
    这里写数据和方法
</script>
<style lang="stylus" rel="stylesheet/stylus">
    这里写css
</style>

挑战二:路由去中心化

基于我们所说的前提,中心化的路由维护起来很坑爹(如果做一两个页面 DEMO
的就没必要出来现眼了)。MV*
架构就是存在这么个坑爹的问题,需要声明中心化 route(angular 和 react
等都需要先声明页面路由结构),针对不同的路由加载哪些组件模块。一旦页面多起来,甚至假如有人偷懒直接在某个路由写了一些业务耦合的逻辑,这个
route 的可维护性就变得有些糟糕了。而且用户访问的第一个页面,都需要加载
route,即使其他路由的代码跟当前页面无关。

我们再回过头来思考静态页面简单的加载方式。我们只要把 nginx 搭起来,把
html 页面放在对应的静态资源目录下,启动 nginx 服务后,在浏览器地址栏输入
127.0.0.1:8888/index.html
就可以访问到这个页面。再复杂一点,我们把目录整成下面的形式:

/post/201509151800.html /post/201509151905.html /post/201509152001.html
/category/js_base_knowledge.html /category/css_junior_use.html
/category/life_is_beautiful.html

1
2
3
4
5
6
/post/201509151800.html
/post/201509151905.html
/post/201509152001.html
/category/js_base_knowledge.html
/category/css_junior_use.html
/category/life_is_beautiful.html

这种目录结构很熟吧,对 SEO
很友好吧,当然这是后话了,跟我们今天说的不是一回事。这种目录结果,不用我们去给
Web Server 定义一堆路由规则,页面存在即返回,否则返回
404,完全不需要多余的声明逻辑。

基于这种目录结构,我们可以抽象成这样子:

/{page_type}/{page_name}.html

1
/{page_type}/{page_name}.html

其实还可以更简单:

/p/{name}.html

1
/p/{name}.html

从组件化的角度出发,还可以这样子:

/p/{name}/name.js /p/{name}/name.tpl /p/{name}/name.css

1
2
3
/p/{name}/name.js
/p/{name}/name.tpl
/p/{name}/name.css

所以,按照我们简化后的逻辑,我们只需要一个 page.js
这样一个路由加载器,按照我们约定的资源目录结构去加载相应的页面,我们就不需要去干声明路由并且中心化路由这种蠢事了。具体来看代码。咱也懒得去解析了,里面有注释。

项目结构

├── build // 构建相关
│ ├── build.js
│ ├── check-versions.js
│ ├── dev-client.js
│ ├── dev-server.js
│ ├── utils.js
│ ├── vue-loader.conf.js
│ ├── webpack.base.conf.js
│ ├── webpack.dev.conf.js
│ └── webpack.prod.conf.js
├── comm// 打包后生成的目录
│ ├── favicon.ico
│ ├── index.html
│ └── static
├── config // 配置相关
│ ├── dev.env.js
│ ├── index.js
│ └── prod.env.js
├── package.json //开发生产依赖
├── server //服务及mock数据
│ ├── controller
│ └── mock
├── src //源代码
│ ├── App.vue // 入口页面
│ ├── api // 所有请求
│ ├── assets // 字体等静态资源
│ ├── common // 全局公用方法
│ ├── components // 全局公用组件
│ ├── favicon.ico
│ ├── index.html // html模板
│ ├── main.js // 入口js 加载组件 初始化等
│ ├── pages // 所有页面
│ ├── plugins // 全局工具
│ ├── router // 路由
威尼斯人线上娱乐,│ └── store // 全局store管理
└── static // 第三方不打包资源
├── async.js
├── css
├── img
├── jquery-1.8.3.min.js
├── jquery.ztree.js
├── md5.js
├── paopao.js
├── spark-md5.js
├── tinymce
├── upload.js
├── upyun-mu.js
└── vue-style-loader

挑战三:领域数据中心化

对于单向数据流循环和数据双向绑定谁优谁劣是永远也讨论没结果的问题,要看是什么业务场景什么业务逻辑,如果这个前提没统一好说啥都是白搭。当然,这个挑战的前提是非后台的单页面应用,后台的前端根本就不需要考虑前端内存缓存数据的处理,直接跟接口数据库交互就行了。明确了这个前提,我们接着讨论什么叫领域数据中心化。

前面讨论到两种数据绑定的方式,但是如果频繁跟接口交互:

  • 内存数据销毁了,重新请求数据耗时浪费流量
  • 如果两个接口字段部分不一样但是使用场景一样
  • 多个页面直接有部分的数据相同,但是先来后到导致某些计数字段不一致
  • 多个页面的数据相同,其中某些数据发生用户操作行为导致数据发生变动

因此,我们需要在业务视图逻辑层和数据接口层中间增加一个
store(领域模型),而这个 store 需要有一个统一的 内存缓存 cache,这个
cache 就是中心化的数据缓存。那这个 store 究竟是用来弄啥勒?

威尼斯人线上娱乐 2

Store 具有多形态,每个 store
好比某一类物品的仓储(领域,换个词容易理解),如蔬果店 fruit-store,
服装店
clothes-store,蔬果店可以放苹果香蕉黑木耳,服装店可以放背心底裤人字拖。如果品种过于繁多,我们可以把蔬果店精细化运营变成香蕉专卖店,苹果专卖店(!==
appstore),甚至是黑木耳专卖店…( _
_)ノ|,蔬果种类不一样,但是也都是称重按斤卖嘛。

var bannerStore = new fruitStore();

var appleStore = new fruitStore();

有了这些仓储之后,我们可以放心的把数据丢给视图逻辑层大胆去用。想修改数据?直接让
store 去改就行了,其他页面的 DOM
文本内容也得修改吧?那是其他页面的业务逻辑做的事,我们把事件抛出去就好了,他们处不处理那是他们的事,咱别瞎操心(业务隔离)。

那么 store 具体弄啥勒?

威尼斯人线上娱乐 3

  • 32
    个赞位置可点赞或者取消,三个页面的赞数需要同步,按钮点赞与取消的状态也要同步。
  • 条目是否已收藏,取消收藏后 Page B 需要删除数据,Page A+C
    需要同步状态,如果在 Page C 又有收藏操作,Page B
    需要相应增减数据,Page A 状态需要同步。
  • 发评论,Page C 需要更新评论列表和评论数,Page A+B
    需要更新评论数。如果 Page B 没有被加载过,这时候 Page B
    拿到的数据应该是最新的,需要同步给 A+C 页面对应的数据进行更新。

所以,store
干的活就是数据状态读写和同步,如果把数据状态的操作放到各个页面自己去处理,页面一旦多了或者复杂起来,就会产生各个页面数据和状态可能不一致,页面之前双向引用(业务耦合严重)。store
还有另一个作用就是数据的输入输出格式化,简单举个栗子:威尼斯人线上娱乐 4

  • 任何接口 API 返回的数据,都需要经过 input format
    进行统一格式化,然后再写入
    cache,因为读取的数据已按照我们约定的规范进行的处理,所以我们使用的时候也不需要理会接口是返回怎样的数据类型。
  • 某些组件需要的数据字段格式可能不同,如果把数据处理放在模板进行处理,会导致模板无法更加简洁通用(业务耦合),所以需要
    output format 进行处理。

所以,store
就是扮演着这样的角色——是数据状态读写和同步,以及数据输入输出的格式化处理。

这里来简单讲一下src文件

挑战四:Hybrid App 化

现在 Hybrid App 架构应用很火啊 _
(:3」∠)_,不搞一下都不好意思说自己是做 H5的。这里所说的 Hybrid App
可不是那种内置打包的 html 源码那种,而是直接去服务端请求 html
文档那种,可能会使用离线缓存。有的人以为如果要使用 Hybrid
架构,就不能使用 SPA 的方式,其实 Hybrid 架构更应该使用 SPA。

遇到的几个问题,我简单列举一下:

  • 客户端通过 url 传参

    如果通过 http get 请求的 query 参数进行传参,会导致命中不到 html
    文档缓存,所以通过 SPA 的 hash query 传参,可以规避这个问题。

  • 与其他 html 页面进行跳转

    这种场景下,进入新页面和返回旧页面导致 webview 会重新加载本地的
    html 文档缓存,视觉体验很不爽,即使页面使用了离线缓存,而 SPA
    可以规避这个问题。

  • 使用了离线缓存的页面需要支持代码多版本化

    由于采用了非覆盖性资源发布方式,所以需要仍然保留旧的代码一段时间,以防止用户使用旧的
    html
    文档访问某些按需加载功能或清除了本地缓存数据而拿不到旧版本代码。

  • js 和 css 资源 离线化

    由于离线缓存的资源需要先在 manifest
    文件声明,你也不可能总是手动去维护需要引用的 js 和 css
    资源,并且那些按需加载的功能也会因此失去按需加载的意义。所以需要将
    js 和 css 缓存到
    localstorage,直接省去这一步维护操作。至于用户清除
    localstorage,参考第三点解决方案。

  • 图标资源离线化

    将图标文件进行 base64 编码后存入 css 文件,方便离线使用。

api 和 views

咱们公司后台项目目前大概有十几个api模块。随着业务的迭代,模块会越来越多。所以这里根据业务模块来划分pages,并且将pages
和 api 两个模块一一对应,方便维护,如下图

威尼斯人线上娱乐 5

aip和pages.png

这样不管项目怎么累加,api和pages比较好维护。