怎么着是单页面应用?

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

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

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

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

今天给大家写一个页面布局的方式,帮助大家更清晰的认识页面布局,方式仅供参考。

就是指一个系统只加载一次资源,之后的操作交互、数据交互是通过路由、ajax来进行,页面并没有刷新。

java单页面应用和多页面应用的区别是什么?哪些网站做成单页面应用好,又有哪些网站做成多页面应用好?
我现在工作做的网站是给企业和政府用的办公系统,
后台主要用了MySQL、SpringMVC、公司自研的jdbc框架;
前端用的是jquery、angularjs,requirejs;
UI部门负责页面的设计及样式。
我们这做出来的都是一个只有index.html的单页面应用,不像我刚开始学的那样是由很多jsp页面相互之间跳转组成的应用。
这导致了一个问题:项目用了RESTful风格,后台只负责提供数据,和前端的耦合性很低,完全不关心页面跳转问题。所以我不清楚的是,以前在后台通过ModelAndView来处理数据和页面视图,现在这些View都由谁来进行管理了?
所以,我想问一下,我们做的这种单页面应用和多页面的应用有什么区别,如何决定使用单页面开发还是多页面?如果是单页面,那视图跳转的职责转到了哪里,如果是多页面应用,除了ModelAndView,现在主流是怎么处理的?

1: 实现如下图Tab切换的功能

图片 1

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>tab切换</title>
  <style>
    * {
      box-sizing: border-box;
    }

    ul,
    li {
      margin: 0;
      padding: 0;
      list-style: none;
    }

    .tab-ct .header>li {
      width: 50px;
      height: 30px;
      line-height: 30px;
      text-align: center;
      border: 1px solid #ccc;
      display: inline-block;
      margin-right: -5px;
      border-right: none;
      cursor: pointer;
    }

    .tab-ct .header>li:last-child {
      border-right: 1px solid #ccc;
    }

    .tab-ct .header>li.active {
      background: #a5daa6;
    }

    .tab-ct .content>li {
      width: 149px;
      height: 100px;
      border: 1px solid #ccc;
      background: #f5daa6;
      border-top: none;
      padding: 10px;
      display: none;
    }

    .tab-ct .content>li.active {
      display: block;
    }
  </style>
</head>

<body>
  <div class="tab-ct">
    <ul class="header">
      <li class="active">tab1</li>
      <li>tab2</li>
      <li>tab3</li>
    </ul>
    <ul class="content">
      <li class="active">我是tab1内容</li>
      <li>我是tab2内容</li>
      <li>我是tab3内容</li>
    </ul>
  </div>

  <script>
    var tabs = document.querySelectorAll('.tab-ct .header>li')
    var panels = document.querySelectorAll('.tab-ct .content>li')

    // for(var i = 0; i < tabs.length; i++){       // 使用for循环去遍历也可以
    //   tabs[i].addEventListener('click', function(){
    //     console.log(this)
    //   })
    // }

    tabs.forEach(function(tab){ //使用遍历,给tabs 里的 每一个tab (li) 绑定事件
      tab.addEventListener('click', function(){
        tabs.forEach(function(node){ //遍历的时候,每一次,先再来个遍历,去除所有的active class
          node.classList.remove('active')
        }) // this之外, 还可以是e.target  window.event.target
        this.classList.add('active') // 然后给当前的tab添加active

        // 接下来,点击header 里面的li,如何对应上 content里面的li
        // 这就需要知道点击的是第几个li,有两种方法:
        // this 看索引号 , 获得数组循环对比
        var index = [].indexOf.call(tabs, this) //找到当前li的index
        panels.forEach(function(node){ //同上,先去除content里面所有li的active class
          node.classList.remove('active')
        })
        panels[index].classList.add('active') //给与tab对应的content li添加active
      })
    })

    // 注意点
    // 1. 数组遍历要熟练, 2. classList操作要熟练 3. 绑定事件
  </script>
</body>
</html>

完成效果

挑战一:前端组件化

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

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

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

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

首先给大家看一下我的文件夹及文件

特点是加载次数少,加载以后性能较高, 不利于seo
如果页面支持h5可以用h5模式+服务器路由rewrite+h5 history
api去掉路由的锚点,和搜索软件优化lib进行seo优化。

2. 实现下图的模态框功能,点击模态框不隐藏,点击关闭以及模态框以外的区域模态框隐藏

图片 2

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
  <style>
    .modal-dialog {
      display: none;
      position: fixed;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      background:rgba(0,0,0,0.7);
    }

    .modal-dialog .bt {
      display: inline-block;
      padding: 3px 6px;
      border: 1px solid #ccc;
      border-radius: 3px;
      font-size: 14px;
    }

    .modal-dialog a {
      text-decoration: none;
      color:deepskyblue;
    }
    /* 下面的cover,是为了展示整个modal,所添加的cover,上面的区别在于,没有display: none; 
    这里提供参考,在css最末尾用了个较为简单明了的方法,可以满足目前的简单需求 */
    /* .modal-dialog .cover {
      position: fixed;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      background-color: #000;
      opacity: 0.5;
      z-index: 99;
    } */
    .modal-dialog .modal-ct {
      position: fixed;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      width: 300px;
      border: 1px solid #ccc;
      border-radius: 5px;
      background: #fff;
      z-index: 100;
    }
    .modal-dialog .modal-ct .header {
      position: relative;
      height: 36px;
      line-height: 36px;
      border-bottom: 1px solid #ccc;
    }
    .modal-dialog .modal-ct .header h3 {
      margin: 0;
      padding-left: 10px;
      font-size: 16px;
    }
    .modal-dialog .modal-ct .header .close {
      position: absolute;
      right: 10px;
      top: 10px;
      line-height: 1;
    }
    .modal-dialog .modal-ct .content {
      padding: 10px;
    }
    .modal-dialog .modal-ct .footer {
      padding: 10px;
      border-top: 1px solid #eee;
    }
    .modal-dialog .modal-ct .footer:after {
      content: '';
      display: table;
      clear: both;
    }
    .modal-dialog .modal-ct .footer .btn {
      float: right;
      margin-left: 10px;
    }

    .open {
      display: block;
    }
  </style>
</head>
<body>
  <div class="btn-group">
    <button id="btn-modal">点我1</button>
  </div>

  <div id="modal-1" class="modal-dialog">
    <div class="modal-ct">
      <div class="header">
        <h3>我是标题1</h3>
        <a href="#" class="close">x</a>
      </div>
      <div class="content">
        <p>我是第一段</p>
        <p>我是第二段</p>
      </div>
      <div class="footer">
        <a href="#" class="btn btn-confirm">确定</a>
        <a href="#" class="btn btn-cancel">取消</a>
      </div>
    </div>
  </div>

  <script>
    var btn = document.querySelector('#btn-modal'),
      modal = document.querySelector('#modal-1'),
      modalCt = document.querySelector('#modal-1 .modal-ct'),
      close = document.querySelector('#modal-1 .close'),
      btnConfirm = document.querySelector('.btn-confirm'),
      btnCancel = document.querySelector('.btn-cancel')

    btn.addEventListener('click', function(){ //默认模态框隐藏,现在要展示 加clas或操作style(最好前者 比如加个open)
      modal.classList.add('open')
    })

    // 声明一个事件处理函数,以便 x  确定 取消, 都能绑定相同的操作
    function handler(){
      modal.classList.remove('open')
    }
    close.addEventListener('click', handler)
    btnConfirm.addEventListener('click', handler)
    btnCancel.addEventListener('click', handler)

    // 现在希望点击模态框以外的,也能消失
    modal.addEventListener('click', function(){
      modal.classList.remove('open')
    })
    // 但是点击到里面的 ct容器,会向上冒泡,从而也会隐藏modal,所以需要取消ct的事件冒泡
    modalCt.addEventListener('click',function(e){
      e.stopPropagation()
    })
  </script>

</body>
</html>

完成效果

挑战二:路由去中心化

基于我们所说的前提,中心化的路由维护起来很坑爹(如果做一两个页面 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
这样一个路由加载器,按照我们约定的资源目录结构去加载相应的页面,我们就不需要去干声明路由并且中心化路由这种蠢事了。具体来看代码。咱也懒得去解析了,里面有注释。

图片 3

挑战三:领域数据中心化

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

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

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

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

图片 4

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

var bannerStore = new fruitStore();

var appleStore = new fruitStore();

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

那么 store 具体弄啥勒?

图片 5

  • 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
还有另一个作用就是数据的输入输出格式化,简单举个栗子:图片 6

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

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

文件

挑战四: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 文件,方便离线使用。

这里有一个base.css文件,这个css文件主要用来存放一些可以重复使用的样式。

挑战五:性能优化

@前端农民工 在 别处 已经说得很清楚了,直接传送门过去看吧,这里不罗嗦了。

 

1 赞 2 收藏
评论

图片 7

图片 8

base.css内容

从图片中我们可以看到很多的样式都是通过class定义的,在需要用到这些样式的时候可以直接在html文件的class中添加class名。拿取很方便,这个文件也可以在以后不断的添加内容作为一个通用的库来使用。

接着就是页面布局,下面是html文件的内容。

图片 9

html

从这张图就可以清晰的看到base.css文件的运用。充分展现了样式与结构的分离,且提高了html文件的理解,保留了html文件的整洁。

再看看这个页面的css文件内容。

图片 10

css

结合html和css,可以看到整个页面的结构逐渐清晰,并且以某种颜色的区域占据网页,直观的展现了网页的初步轮廓。

这样做的好处不言而喻,它整个结构非常清晰,且环环相扣,每一个模块都是独立的,且紧密的拼构成一个完整的页面,避免了模块之间的相互影响。对页面的修改也很方便,利于后期的维护。对程序员编写代码也有一个清晰的思路。

最后就是这个页面布局的效果图。

图片 11

效果图

发表评论

电子邮件地址不会被公开。 必填项已用*标注