SPA

单页应用设计指南

包勇明 / @huntbao

自我介绍

    * 80 后 * 2015.8 加入网易 * 曾混迹于阿里巴巴、盛大创新院(麦库记事)等公司 * 翻译书籍: * [《jQuery UI开发指南》](http://item.jd.com/11127008.html) * [《jQuery、jQuery UI及jQuery Mobile技巧与示例》](http://item.jd.com/11298722.html) * [《单页Web应用 JavaScript从前端到后端》](http://item.jd.com/11545544.html) * 目前在前端框架组开发和维护[《有范云协作》](http://youfan.netease.com/)

今天要分享的内容

  • SPA 的基本概念
  • SPA 的架构设计

认识 SPA

SPA 的读法

英文:[es] - [pi:] - [ei] vs [spɑ:]

中文:单页应用 vs 单页面应用

SPA 的定义

From Wikipedia: > A single-page application (SPA) is a web application or web site that fits on a single web page with the goal of providing a more fluid user experience similar to a desktop application.
In a SPA, either all necessary code – HTML, JavaScript, and CSS – is retrieved with a single page load, or the appropriate resources are dynamically loaded and added to the page as necessary, usually in response to user actions. The page does not reload at any point in the process, nor does control transfer to another page, although the location hash can be used to provide the perception and navigability of separate logical pages in the application, as can the HTML5 pushState() API . Interaction with the single page application often involves dynamic communication with the web server behind the scenes.

实现 SPA 的技术

  • 无需插件(JavaScript):

    • Ajax
    • Websocket
    • Server Sent Event (SSE)
  • 需要插件:

    • Silverlight
    • Flash
    • Java Applet

JavaScript 开发 SPA 的优势

  • 借助浏览器的优势:

    • 使用最广泛的应用
    • 跨平台
    • 引擎性能在不断提升
  • 自身优势:

    • 语言在不断完善
    • Node.js 的发布为前端工程师打开了一扇大门

话题:Compile to JavaScript

>浏览器只支持 JavaScript,是基于浏览器编程的无可争辩的标准

一些理由:

  • 熟悉度
  • 框架
  • 多目标
  • 成熟度

话题:Compile to JavaScript

常见的有:

>褒贬不一,但对 JavaScript 语言的发展无疑是有积极的推进作用的

SPA 前后端通信的数据格式

  • JSON
  • HTML 片段

SPA 面临的挑战

SPA 面临的挑战

  • 工程师的分工
  • 前端工程师的专业素质
  • SEO

SPA 面临的挑战:SEO

  • 受登录保护的应用没有 SEO 的问题
  • 不受登录保护的应用:

    • 搜索引擎自身的解决方案
    • 专门为搜索引擎生成页面
>相信总有那么一天,SPA 的 SEO 不会再是一个问题

SPA 面临的挑战:前端工程师的专业素质

  • 不断涌现的新技术、框架、开发思想

    • 模块加载技术
    • DOM --> Data Model
  • 心态:

    • 开放
    • 学习
    • 创新

SPA 面临的挑战:工程师的分工

  • 前后端工程师的分工:前后端分离,并行开发

    • 后端工程师专注 API 开发:RESTful API

    • 前端工程师负责界面和用户交互逻辑
  • 前端工程师团队内部的分工

    • 模块化

SPA 的架构设计

一种不使用框架的设计思路

SPA 的架构设计

  • 核心:

    • 主控制模块
    • 路由模块
    • 功能模块
    • 数据模块
  • 其他:

    • 第三方模块

主控制模块

  • 渲染和管理功能模块
  • 管理应用状态
  • 协调功能模块
>主控制模块是功能模块和业务逻辑以及通用浏览器接口(像 URI 或者 Cookie)之间的协调者,是架构的中枢

主控制模块:在架构中的位置

路由:为什么要使用?

  • 前进、后退
  • 收藏 URL
  • 发送 URL
  • 应用内部跳转
>建议为每个模块分配单独的路由

路由:实现原理

  • location hash

    • hash change event
    • iframe hack
  • HTML5 PushState
>SPA 框架一般都会提供路由功能,比如 Backbone 的 Router, NEJ 的 UMI 等

路由:职责

>简单来说,路由模块的职责是在路由变化时,通知应用的主控制模块,主控制模块根据路由模块提供的路由信息做相应的处理

路由:全面接管页面状态变化

>功能模块可能会引起应用状态的变化,此时功能模块不用做任何处理,只要调用路由模块提供的更改路由的 API 即可

路由:全面接管页面状态变化

>比如,功能模块 A 中有一个链接需要跳转到功能模块 B,跳转后左边菜单栏的高亮选项需要变化。菜单高亮在两个时刻需要考虑,一是应用初始化时,二是用户使用过程中。此时,A 不需要去调用菜单模块提供的高亮方法,A 只需要调用路由模块提供的更改路由的方法,接下来的过程等同于应用的初始化逻辑

路由:全面接管页面状态变化

>提示:可以为所有会引起应用状态变化的元素增加特殊的 class 类名,然后使用事件代理的方法在文档结点上监听这些元素的点击事件

一件很重要的事情

创建文件和目录

创建文件和目录:方式一

```shell +-- css | +-- reset.css | +-- commom.css | `-- modules | +-- module.xxx.css | `-- module.zzz.css +-- js | +-- libs | +-- jquery.js | `-- backbone.js | +-- modules | +-- module.xxx.js | +-- module.yyy.js | `-- module.zzz.js | +-- common.js | `-- spa.shell.js `-- res ```

创建文件和目录:方式一

  • 有单独的 css 和 js 目录
  • css 和 js 保持平行结构
  • css 选择器前缀为模块名,以免全局样式冲突,不使用 id 选择器
  • js 以模块名为名字空间,使用立即执行表达式,以免污染全局名字空间

不足之处:

  • 开发时有切换成本
  • css 和 js 不一定完全平行,有些模块没有自己的 css
  • 模块移植性差

话题:CSS 预处理器

话题:CSS 预处理器

话题:CSS 代码的复用

>除了一些全局的基础样式、button、form control、layout 等,不推荐其他样式的刻意复用。适当的代码冗余可以避免令人头痛的样式冲突问题。

创建文件和目录:方式二

```shell +-- common | +-- reset.css | +-- commom.css | `-- commom.js +-- lib | +-- jquery.js | `-- backbone.js +-- module | +-- module.xxx | +-- module.xxx.css | `-- module.xxx.js | +-- module.yyy | `-- module.yyy.js | `-- module.zzz | +-- module.zzz.css | `-- module.zzz.js `-- pages ```

创建文件和目录:方式二

  • common 目录中放置应用中会统一引入的资源
  • lib 目录中放置第三方的库和框架
  • module 目录中放置具体的模块

    • 模块有自己的文件夹,里面放置自己的 css 和 js 文件
    • 模块的 css 和 js 文件名和模块名保持一致

创建文件和目录:方式二

不足之处:

>由于所有的功能模块有一定的相似性,在新建模块时,很有可能是拷贝其他已创建好的模块,然后再更改文件的名称以及代码中相应的名字空间,有一定的体力成本,整个过程略显枯燥

话题:模块 vs 组件

  • 模块

    • 单例
    • 负责具体的业务逻辑
  • 组件

    • 可以被多次实例化
    • 是从业务模块中抽象出来的可复用功能

话题:组件继承

>继承会产生耦合。JavaScript 是动态类型语言,在运行时可以赋于其他类型的值,所以 IDE 无法通过词法就能分析出对象间是否有继承关系,加之在 ES6 之前又没有原生 Class 对象,导致在 IDE 中跟踪代码变得非常困难。不推荐多层级的继承。

话题:组件继承

>根据以往的经验,一个前端团队需要有一些基础组件的沉淀,包括但不限于详细的开发规范和编码规范。这有利于提升团队的工作效率,在团队新增成员时,效果尤其明显。还有一个额外的好处是,会显著增加团队成员间的沟通机会和共同语言,有助于团队氛围的营造。

创建文件和目录:方式二

>方式二中并没有严格区分模块还是组件,统一放在 module 目录下面,在实际开发中,可以根据情况再做划分

创建文件和目录:方式三

```shell +-- common | +-- reset.css | +-- commom.css | `-- commom.js +-- libs | +-- jquery.js | `-- backbone.js +-- modules | +-- module.xxx | +-- index.css | `-- index.js | +-- module.yyy | `-- index.js | `-- module.zzz | +-- index.css | `-- index.js `-- pages ```

创建文件和目录:方式三

  • 模块的 css 和 js 文件使用相同的名称

不足之处:

  • 搜索文件时相当痛苦
  • IDE tab 中文件名的显示问题

创建文件和目录:方式三

创建文件和目录:方式三

创建文件和目录:方式三

HTML 模板放在哪儿?

  • 放在模块文件夹中,名称为 module.xxx.html

    • 开发时设置跨域可加载
    • 上线时使用工具将 html 字符串化,和 JS 打包在一起
  • 以字符串的形式放置在模块的 js 中

    • 字符串拼接,有点麻烦。可以增强编辑器的功能,比如一键将 html 转换成字符串拼接的格式
    • ES6 中的字符串模板技术

HTML 模板放在哪儿?

话题:JSX

>使用 JSX,可以在 JavaScript 中编写 HTML 模板,还集成了常见的模板引擎功能
>很多人表示难以接受 JSX。一种新技术的推出,最难以接受的部分往往也是它创新最大的部分。

功能模块

  • 设计良好的 API
  • 强隔离性
>精力放在创建能增值的核心模块上,次要的模块可以交给第三方
>只要时间和资源允许,就可以有选择性地用更好的模块来替换第三方模块
>可以在多个项目之间重用模块

话题:第三方模块

代表应用:

  • GA
  • DisQus
  • AddThis、ShareThis、JiaThis

话题:第三方模块

一些共同特征:

  • 在自己的容器内渲染,容器要么由别人提供,要么由它们自己添加到文档上
  • 提供了精心设计的 API,以便控制它们的行为
  • 通过将自己的 JavaScript、数据和 CSS 精心地隔离,以免污染主页面

话题:第三方模块

一些缺陷:

  • 依赖于第三方的代码和服务
  • 功能过剩,经常要比自己的模块要慢
  • 隐私问题
  • 缺乏灵活性,经常不能无缝地集成
  • 跨功能通信很难或者是不可能的
  • 定制化的模块很难或者是不可能的

话题:第三方模块

>把功能模块当作第三方模块一样来开发,能使我们从中获益
  • 团队更加高效,因为开发人员可以根据模块来划分职责
  • 不会有未使用的或者是不想要的功能
  • 代码维护和重用变得更加容易

话题:第三方模块

>应用的非核心功能使用第三方模块,然后在时间和资源允许时,有选择性地使用自己的功能模块来替换它们,这样就能更好地集成、运行更快、侵入性更小,或者是以上全部的好处

功能模块:在架构中的位置

功能模块:在架构中的位置

  • 主控制模块可以调用任何功能模块
  • 功能模块只调用共享的公用模块
  • 功能模块之间不允许相互调用
  • 功能模块的唯一数据源或者功能只能来自主控制模块,在配置和初始化期间以参数的形式传给模块的公开方法

功能模块:不允许相互调用

假设可以互相调用:

  • 当模块 A 出错或者崩溃的时候,则所有调用 A 的模块都有可能出问题
  • 就算系统运行良好,重构或者替换某个模块也会很困难
  • 随着系统中模块的不断增加,系统的耦合性会越来越强

功能模块:通过中介者通信

功能模块:通过中介者通信

  • 功能模块只发布事件,通知中介者发生了什么事情或者需要做什么操作,接下来的工作就全部交给中介者
  • 该功能模块不需要知道其他模块的存在
  • 中介者订阅事件、调用相应的功能模块完成任务
>本架构设计中,中介者由主控制模块担当

功能模块:通过中介者通信

缺点:

  • 由于模块之间是间接通信,所以有些许性能损耗
  • 对于模块很少的简单系统,可能会显得多此一举

数据模块

>让人纠结的模块
>如果按照第三方模块的设计思路,模块需要维护自己的数据,也就是系统没有统一的数据模块

数据模块

没有数据模块的时候:

  • 移植很容易
  • 前端工程师的工作更加独立

带来的问题:

  • 代码会有冗余,浪费工程师资源,是劳动密集性的做法
  • 无法对数据进行统一的管理和自动化测试
  • 对系统的整体理解困难

数据模块

>根据近几年的实践来看,在不同系统之间移植模块,大多数时候没有这样的需求,就算有也很少能无缝移植,还是需要一定的成本,这是假想出来的美好设计。可以这么说,收益不如带来的损失。
>推荐使用统一的数据模块,并建议对它进行全面的自动化测试

数据模块:在构架中的位置

数据模块:职责

>负责业务逻辑和数据管理
>虽然所有的业务逻辑和数据都是通过数据模块访问的,但并不意味着必须只能使用一个(可能非常大)JavaScript 文件来存放数据模型。可以使用名字空间,把数据模型分成多个容易管理的小文件

数据模块:实例

>React 中的 Store
>NEJ 中的 Cache

话题:Flux

话题:NEJ Cache

话题:Mock 数据服务

>在 Node.js 出现之前,在数据模块和服务器之间可能还会加一层功能,实现真实数据和模拟数据的切换。得益于 Node.js,前端工程师也可以很轻松地开发出本地模拟容器,使得本地代码和线上代码完全一致。

话题:UI 自动化测试

>这一块的技术正在摸索中,目前自己写了一个 Chrome 浏览器插件,[Columba](https://github.com/huntbao/columba),基本流程可以走通,接下来需要实践和优化。

结语

>SPA 极大地提升了用户体验,同时也给工程师带来了极大的挑战,但这难不倒懒惰又聪明的工程师。复杂的 SPA 不断涌现,浏览器技术不断增强,相信 SPA 的未来会更美好!

Thank You

Q & A

包勇明 / @huntbao