7.Page与History的综合应用

在传统页面中,每个url都是和页面绑定的,即使是单页面,也应该有这种习惯。因此我们把Page对象作为不同的url,通过页面的切换来更改url。url的变换代表着用户在该应用中的探索路径,用户可以随时的后退到上一个页面,或者前进到后一个页面。从上面的history篇章中,使用浏览器的提供的api可以很好的解决这个问题。然而在实际的使用中存在一个问题:比如用户从一个页面跳转到另外一个页面,然后后退到前一个页面,发现上一个页面并不是他之前访问的页面了,原因如下:

  1. 后退到上一个页面,页面重新渲染时得到的数据和上一次的数据不一样;
  2. 后退到上一个页面,无法让页面后退到之前浏览的那个位置;
  3. 渲染的页面有问题。

一般第三点都不会发生,第二点也不是很重要,如果在切换页面的时候存储跳转时的滚动地址,也可以很好的解决,第一点确实是个值得思考的问题,原因如下:

  1. 第一次页面进入请求了,第二次是否还需要去请求数据;
  2. 如果第二次不去请求,直接渲染第一次显示的数据,那怎么保证页面的数据是最新的呢;
  3. 按照2的做法,意味着页面不是重新渲染,恢复的页面以及dom绑定的事件和最后离开页面的时候应该是保持一致的;
  4. 按照3的做法,我们必须要存储历史记录中的页面相关数据,比如缓存dom,缓存data等;
  5. 关于缓存data,是否可以把data完全交给history.state,它不就是为了缓存页面数据而存在的么?不过这里并非采用了history.state,因为当我们要任意更改历史记录的时候,之前没有保存在历史记录的data就无法被还原,通过内存存储就能增加灵活性,当然history.state可以作为备案,要求缓存的数据的可序列化的。

合理的历史记录一般不会有太多级,而且把历史记录中的页面储存起来,可以加快页面的渲染速度,增强用户体验。

需求

通过上面的讨论,我们需要实现如下的目标:

  1. 针对历史记录创建一层数据缓存,用来实现页面导航的时候可以快速切换页面,并将其还原;
  2. 数据缓存中主要存储Page对象中的DOM,事件和data;
  3. 要保证缓存数据和历史记录的数据保持高度一致,当新的页面添加到历史记录的时候,缓存记录就会新增一条记录,删除历史记录时,就删除缓存中对应的页面数据;
  4. 为了保证页面数据和后台数据的一致性,提供了一个可选的方法,主要是为了进行数据更新以及页面的局部更新。这个可选方法只有在快速切换的时候才会调用。

实现思路

之前的Page对象实现中, 每次渲染一个新页面的时候,都会new一个指定的页面,现在我们只要在切换页面的时候把它缓存起来,等到需要的时候,再把它还原回去。只有重新创建一个页面的时候或者历史记录中没有缓存页面的时候,才会去new一个新Page对象。现在我们在Page的原型对象中新增两个方法:

  1. save(), 将数据缓存起来;
  2. restore(dom), 将数据还原到页面上。

同时页面的生命周期也产生了变化,因为从历史记录中还原页面不再进行render,getDomObj和beforeInit流程了,通过restore方法,就快速的将页面还原到浏览器了,为了保证数据一致性,添加了一个afterRestore可选方法,进行局部更新,保证前后端数据更新交互。

save方法是对Page对象的浅删除操作,将剩余的必要信息缓存起来。之前的destroy是深度删除操作,是为了删除所有的引用。相对应的,要进行缓存的Page对象的生命周期是这样的:

  1. restore(dom):将Page还原到页面上;
  2. 如果存在afterRestore方法,调用之后,通过局部更新从而保证页面一致;
  3. 调用init方法,初始化该页面需要引入的插件;
  4. 日常的业务处理,等待用户切换页面;
  5. 首先调用dispose方法,这个方法主要是处理引入的插件的销毁;
  6. save(),将数据缓存起来, 如果存在beforeSave方法,先调用beforeSave。

如下代码

save: function () {
   this.isSave = true; // 设置状态
   this.destroy(false); // 浅删除
   this.parent.history.save(this); // 保存页面于历史缓存中
   if (typeof this.beforeSave === "function") this.beforeSave();
   this.nodes = []; // 把dom全部缓存起来
   for (var i = 0; i < this.parentDom.childNodes.length; i++) {
       this.nodes.push(this.parentDom.childNodes[i]);
   }
},
restore: function (dom) {
    dom.innerHTML = ''; // 清除
    // 保证dom是原来的dom
    var fragment = this.template.content;
    for (var i = 0; i < this.nodes.length; i++) {
        fragment.appendChild(this.nodes[i]);
    }
    dom.appendChild(fragment);
    this.nodes.length = 0;
    if (typeof this.afterRestore === "function") this.afterRestore();
    this._beforeInit();
    // 这方法后紧接着就是绑定事件。
}// 调用destroy(true)仅在历史记录没有该页面的时候调用,否则都是浅删除
destroy: function (isClean) {
    this.dispatchEvent("_dispose");
    if (isClean) {
        this.eventDispatcher.destroy();
        this._removeDom();
        this.template = null;
        this.nodes.length = 0;
        this.parent = null;
        this.data = {};
        this.parentDom = null;
    }
},
// _dispose方法放在_init里面定义,仅能被调用一次
_init: function () {
    this.isSave = false;
    this.eventDispatcher.clearListenerByType("_dispose"); // 清除这个事件,然后绑定新的
    this.attachDiyEvent("_dispose", function () {
        if (typeof this.dispose === "function") this.dispose();
        this._removeEventListener();
        this.http.destroy();
    }, true) // 调用后销毁
    if (typeof this.init === "function") this.init.apply(this, arguments);
},

新增一个HistoryStorage对象,用于存储页面缓存,并且修改原先的History对象, 让History专注于交互,HistoryStorage专注于存储,并将之前的popstate事件转到History对象下, 修改如下

function History(app) {
    this.app = app; 
    this.appStorage = new HistoryStorage();
    this.skipPop = false; // 是否要过滤popstate事件
    this.popBack = null; // 过滤popstate事件时执行的可变方法
    // 代表监听popstate实现
    window.addEventListener("popstate", this._popHandler.bind(this)); 
}
History.prototype = {
    constructor: History,
    // 设置锁屏与否
    setLock: function (isLock) {
        this.appStorage.setLock(isLock);
    },
    // 获取是否锁屏状态
    getLock: function () {
        return this.appStorage.isLock
    },
    // 恢复页面
    restore: function () {
        this.appStorage.restore();
    },
    pushState: function (page, option) {
        this.appStorage.pushState(page, option);
    },
    replaceState: function (page, option) {
        this.appStorage.replaceState(page, option);
    },
    _popHandler: function (ev) {
        var app = this.app, that = this;
        if (this.getLock()) return this.restore();
        if (this.skipPop) 
          if (typeof this.popBack === "function") return this.popBack();
        // 改变hash也会触发popstate事件
        if (location.pathname == app.currentPage.url) return; 
        // 传入三个方法,分别代表当前位置是否在首页,如果是首页执行第二个方法,不是首页则执行第三个方法
        this.appStorage.popOperation(function (name, nameList) {
            return nameList.indexOf(name) === 0;
        }, function (component) {
            if (typeof app.outofHistory === "function") app.outofHistory();
            setTimeout(function () {
                that.pushState(component);
                app._renderPage(component, true); // 渲染页面
            }, 2000);
        }, function (component, config, str) {
            that.renderBackComponent(app.component, config, str); 
        });
    }
};

History的实际操作转向StorageHistory, 为了后期能更好的扩展。

function HistoryStorage() {
    this.history = []; // 存放url数组,对应历史记录
    this.components = []; // 存放Page对象
    this.datas = []; // 存放数据,这里的数据和Page对象,还有包含其它数据
    this.index = null; // 指向当前的位置
    this.isLock = false;
}
HistoryStorage.prototype = {
    constructor: HistoryStorage,
    popOperation: function (filter, elseFn, operationFn) {
        var name = this._getCurrentHistoryName();
        if (filter(name, this.history)) {
            elseFn(this.components[this.index]);
        } else {
            var urlObj = this._getSurroundUrl(),
                str, config,
                component = this.components[this.index];

            component.save(); // 保存页面
            if (urlObj.prev === name) {
                config = this.datas[--this.index];
                str = "out";
            } else {
                config = this.datas[++this.index];
                str = "in";
            }
            operationFn(this.components[this.index], config, str);
        }
    },
    // 指向pushState和replaceState的时候,把数据和page对象保存起来
    replaceState: function (component, option) {
        option = option || {};
        var url = option.url || component.url;
        // 销毁页面
        if (this.components[this.index]) this.components[this.index].destroy(true);
        if (location.protocol !== "file:") history.replaceState(option, "", url);
        this.datas[this.index] = option;
        this.components[this.index] = component;
        this.history[this.index] = url;
    },
    pushState: function (component, option) {
        option = option || {};
        var url = this.type == "app" ? 
            option.url || component.url : 
        	location.pathname + "?popup=" + component.name,
            components = this.components,
            datas = this.datas;

        if (components[this.index] && !components[this.index].isSave) 
            components[this.index].save();
        if (location.protocol !== "file:")  history.pushState(option, "", url);
        if (typeof this.index === "number") {
            for (var i = components.length - 1; i >= this.index + 1; i--) {
                // 移除页面的时候销毁页面
                if (components[i]) components[i].destroy(true);
            }
            var nextIndex = ++this.index,
                len = components.length;
            components.splice(nextIndex, len - nextIndex, component);
            datas.splice(nextIndex, len - nextIndex, option);
            this.history.splice(nextIndex, len - nextIndex, url);
        } else {
            this.index = 0;
            this.components.push(component);
            this.history.push(url);
            this.datas.push(option);
        }
    },
    // 其它操作和之前的类似,这里省略
}
  

[案例地址]http://www.renxuan.tech:2006

结语

通过history和Page配合,让页面能够快速的切换,极大的提升了页面的切换速度,让webapp更像原生app。

推广

底层框架开源地址:https://gitee.com/string-for-100w/string
演示网站: https://www.renxuan.tech/

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值