【前端架构】多页面应用设计

1,578 阅读5分钟


一、单页面应用分析

  1. 构建成本
    • 本地构建流程处理
    • 持续集成服务器
    • 当框架提供的构建流程不符合需求的时候,需要重写构建脚本
  2. 学习成本
    • 前端技术更新快
    • 旧框架维护学习成本
    • 新框架学习成本
  3. 后台渲染成本
    • 预渲染,向搜索引擎提供一份可以被索引的HTML代码
    • 同构应用,由后端运行JavaScript代码生成对应的HTML代码
    • 混合式后台渲染,由后端解析前端模板,生成对应的HTML代码
  4. 应用架构的复杂性
    • 前端需要处理更加复杂的应用交互
    • 后台的数据通过API接口直接暴露,需要更严谨考虑数据安全的问题
    • 复杂的应用,需要考虑增加BFF层(服务于前端的后端)
    • 代码逻辑需要重复校验,比如权限和敏感的表单数据提交,需要同时在前端和后端进行校验
    • 为了提供可供前端测试的API,需要搭建Mock Server和造Mock数据

二、简单多页面应用分析

  1. 选择UI库及框架
    • 在多页面时代,由于前端应用的简单化,几乎打部分前端团队都拥有自己的前端框架和UI库
    • 随着复杂度的提升及开源社区的活跃,大部分中小公司逐步采用外部框架解决方案
  2. jQuery和Bootstrap仍然好用
    • jQuery大大地简化了HTML与JavaScript的操作
    • 因为大量旧网站的存在,目前jQuery仍然是使用最广泛的前端框架
    • Bootstrap由于其响应式的处理能力,使得开发人员可以更关注于实现,而不是CSS样式,也因此成了一些开源CMS、框架所提供的默认UI框架
    • 如果想快速使用多页面技术来开发应用,那么使用jQuery的生态能感受到显著的开发优势,再将Bootstrap作为UI框架来提升开发体验
  3. 不使用框架
    • 对于只是简单地显示、隐藏DOM等操作的应用来说,不需要使用框架
    • HTML新标准形成后,DOM的操作进一步简化,比如原生的querySelectorAll等属性,为了提升兼容性,还可以配置一个pollyfill

三、复杂多页面应用设计

  1. 模板与模板引擎原理
    • 简单的HTML可以通过字符串拼接或者模板字符串的方式来生成,如:

      html += 
          '<button type="button"' +
          ' i-id="' + id + '"' + style +
          (val.disabled ? 'disabled' : '') +
          (val.autofocus ? 'autofocus class="ui-dialog-autofocus"' : '') + '>' +
          val.value + '</button>';

    • 生成复杂的HTML使用模板引擎
  2. 基于字符串的模板引擎设计
    • 代表框架:Mustache、Handlebars.js
    • 在更新DOM的时候会更新整个DOM节点
    • 模板引擎实现

      /**
      * 模板解析器
      */
      function simple(template, data) {
          return template.replace(/\{\{([\w\.]*)\}\}/g,
          function(str, key) {
              var keys = key.split(".");
              var value = data[keys.shift()];
              for (var i = 0; i < keys.length; i++) {
                  if (value) {
                      value = value[keys[i]];
                  }
              }
              return (typeof value !== undefined && value !== null) ? value: "";
          });
      }
      
      /**
      * 使用示例
      */
      var data = {
          user: {
              name: 'Phodal',
              info: {
                  address: {
                      city: '漳州'
                  }
              }
          }
      }
      var temp = simple("<p>用户:{{user.name}},地址:{{user.info.address.city}}<p>", data);
      document.getElementById("result").innerHTML = temp;

  3. 基于JavaScript的模板引擎设计
    • 将模板编译为某种SDL(领域特别语言),比如HyperScript或者Javascript对象(代码+数据)
    • 在使用时,调用JavaScript来渲染出DOM节点
    • 当发生变更时,通过DOM Diff算法来替换对应的修改节点
  4. 双向绑定原理及实践
    • 视图变化实时地让数据模型变化

      /**
      * 在输入模型中输入数据时,监听数据的变化,再将这些值传递到模型中进行处理
      */
      var inputData = '';
      var inputText = document.querySelector('#inputText');
      inputText.addEventListener('input', function (event) {
        inputData = event.target.value;
      });

    • 数据变化时,更新视图

      var button = document.querySelector('#change-button');
      button.addEventListener('click', function (event) {
        inputText.value = 'change';
      });

    • 双向绑定几种实现方式:
      • 手动绑定:两个单向绑定的结合,通过手动set和get数据来触发UI或数据变化
      • 脏检查机制:在发生指定的事件(如HTTP请求、DOM事件)时,遍历数据相应的元素,然后进行数据比较,对变化的数据进行操作
      • 数据劫持:通过hack的方式(Object.defineProperty())对数据的getter和setter进行劫持,在数据变化时,通知相应的数据订阅者,以触发相应的监听回调

        var o = {};
        var bValue;
        Object.defineProperty(o, "b", {
          get: function () {
            return bValue;
          },
          set: function (newValue) {
            bValue = newValue;
          },
          enumerable: true,
          configurable: true
        });
        o.b = 38;
  5. 前端路由原理及实践:监听路由的变化,调用函数来处理对应的逻辑
  6. 两种路由类型
    • History模式
      • back:返回前一页
      • forward:在浏览器记录中前往下一页
      • go:在当前页面相对位置从浏览器历史记录记载页面
      • pushState:按指定的名称和URL将数据push进会话历史栈
      • replaceState:指定的数据、名称和URL,更新历史栈上最新的入口
    • Hash模式:
      • 监听hashchange事件:window.addEventListener('hashchange', functionRef, false)
      • Hash值的改变不会导致页面重新加载
      • Hash值由浏览器控制,不会发送到服务器端
      • Hash值的改变会记录在浏览器的访问历史中,因此可以在浏览器中前进和后退
  7. 自造Hash路由管理器
    • add:创建理由集、添加路由的key及其对应的函数

      function Router () {
        this.routes = {};
        this.currentUrl = '';
      }
      
      Router.prototype.add = function (path, callback) {
        this.routes[path] = callback || function () {};
      }

    • refresh:解析出当前路由的key,再根据key从路由集中找到并调用对应的路由处理函数

      Router.prototype.refresh = function () {
        this.currentUrl = location.hash.replace(/^#*/, '');
        this.routes[this.currentUrl]();
      }

    • load:初始化路由相应的监听事件

      Router.prototype.load = function () {
        window.addEventListener('load', this.refresh.bind(this), false);
        window.addEventListener('hashchange', this.refresh.bind(this), false);
      }

    • navigate:跳转到对应的路由

      Router.prototype.navigate = function (path) {
        path = path ? path : '';
        location.href = location.href.replace(/#(.*)$/, '#' + path);
      }

四、避免散弹式架构

  1. 散弹式架构应用
    • Backbone轻量级MVC
    • jQuery使用户能更加方便地处理HTML、events、实现动画效果
    • Mustache模板引擎
    • Require.js依赖模块管理
  2. 如何降低散弹性架构的出现频率
    • 统一交互处理
    • 按页面拆分脚本
    • 用ID而非class
    • 其它唯一选择器


推荐阅读:

  1. 上篇:【前端架构】构建流设计

参考资料:

  1. 《前端架构:从入门到微前端》

微信公众号“前端那些事儿”: