history.go(-1) 页面跳转,表单却会自动保存检索信息溯源(autocomplete 属性)

背景:SPA 应用,大体是一个有检索表单列表条目展示的页面。每个条目有链接,点击会切换路由跳转到条目的详情页面。详情页面有返回按钮,可以返回列表检索的界面。

问题:此前的应用从详情页面返回列表,没有实现保持检索后状态的功能,但是现在准备去实现的时候发现,返回(通过标题中的history.go(-1)实现)之后竟然会自动保存检索的表单信息,这是为什么?

最终geogle 在 Stack Overflow 中找到了答案(baidu什么的一点儿都没用)本文会将已被接受的回答翻译过来,希望能给其他也有这个疑问的人提供借鉴。


  • autocomplete 属性
  • 本地存储介绍
  • 路由 state

autocomplete 属性(HTML5下)

“罪魁祸首”就是这个 autocomplete 属性。

autocompleteHTML5下的属性,它会告诉浏览器将用户早些输入并submit的值填写在表单field中。但是在我的测试中我却看到:如果把 autocomplete 属性设置为 on,在填写表单后,尽管没有 submit ,当我操作浏览器history前进后退时,表单也会自动填充(之前输入的值),如果设置off,表单内容会被清除(看到这里我都已经恍然大悟了,哈哈哈)。

因此,(针对 HTML5 用户),你可以使用这个属性来缓存你的 form data(在大多数浏览器下都管用,除了 Opera

<form action="/update" method="post" 
autocomplete="on">
    Email:    <input type="text" id="email" /><br />
    Username: <input type="text" id="uname" /><br />
    Password: <input type="password" id="pwd" 
    autocomplete="off"/><br />
    <input type="submit" />
</form> 

小技巧:你可以为某个特定表单项关闭 auto-complete 属性(off),而外边 form 元素仍然保持 on,那么其他除了设置 off 的表单项都会保持 on
例如:上边代码的 password input 输入框的 auto-complete 为 off

MSDN 评论

  • 如果autocomplete属性缺省,那么表单域会默认设置为 on,不过前提是这个表单元素没有父级 form 元素,或者 form 元素已经将autocomplete属性设置为 on
  • Information provided by the AutoComplete feature is not exposed to the object model, and is not visible to a Web page until the user selects one of the suggestions as a value for the text field.(这部分没有看明白,链接

接下来就到了本地存储的 show time 了,本地存储还是稳!


没有提交的表单数据保存在本地

你可以在页面重新跳转之前或者任何表单消失(focus-out )的事件之前,使用下边的本地存储技术来保存这些输入数据。

cookies

good-old的cookies (说实话这个形容真的贴切,好用因为兼容性不错,老因为存在一些安全性的问题)在这种情景下可以拿来一用,但是你要仔细斟酌这样做的弊端:

尽管你能使用代码将这些(cookie的)值加密,但毕竟我们(数据)在客户端,cookies 也没有真的那么安全。Http-OnlySecure 字段标志 cookies 不会帮助我们,因为这些可选项(Http-only、secure)是用来实施 SSL(Secure Socket Link,可以认为 Https = Http + SSL)的,secure表示cookie只能在 Https 协议下传递,Http-Only 表示 cookies 不能通过 JavaScript 读取。

译者总结:如果你要使用 js 将 cookie 保存本地数据,那么你就可能要放弃安全的 Https 来保证数据安全。

浏览器有 cookie 的大小限制。所以如果你没留意大小限制(当你写 cookie 或者通过 maxLength 属性限制控件值得长度),那么这将会成为一个问题,因此,在这种情况下, 整理这些数据(满足大小限制)是最烦人的事情。除此之外,浏览器还有对每一个 domain 下所能设置 cookies 数量的限制,所以:当你准备将表单数据保存在 cookies 中时,应该先将他们合并为一个或者少数几个 cookies,而不是为每一个表单域的值都设置 cookies,否则,你的站点会超出限制。

引自 MSDN:所有浏览器最大支持 cookie 大小到 4096 Bytes,因为容量限制,cookies 最好用来存储小数量的数据。

当然,好处就是所有浏览器都支持 cookies,如果你不打算借助 cookies “缓存”敏感、太长的数据,那么你大可去用 cookies,如果不是这样,你最好采用下一条方案localStorage

// 下面只是一个示例,在某种程度上没有在产品级应用下完全测试,
// 但是它应该可以给你一些 idea 

/** 
 * 从目标表单中缓存用户输入表单数据,存储到 cookies 中
 *并且在请求或者需要时回填到表单中
 */
var formCache = (function () {
    var _form = null, 
        _formData = [],
        _strFormElements = "input[type='text'],"
                + "input[type='checkbox']," 
                + "input[type='radio']," 
                // + "input[type='password'],"  // leave password field out 
                + "input[type='hidden'],"
                // + "input[type='image'],"
                + "input[type='file'],"
                // more input types...
                + "input[type='email'],"
                + "input[type='tel'],"
                + "input[type='url'],"
                + "select,"
                + "textarea";

    function _warn() {
        console.log('formCache is not initialized.');
    }

    return {

        /**
         * Initializes the formCache with a target form (id). 
         * You can pass any container id for the formId parameter, formCache will 
         * still look for form elements inside the given container. If no form id 
         * is passed, it will target the first <form> element in the DOM. 
         * 使用一个目标form的id初始化formCache,为闭包内的 _form 赋值,
         */
        init: function (formId) {
            var f = (typeof formId === 'undefined' || formId === null || $.trim(formId) === '') 
                    ? $('form').first() 
                    : $('#' + formId);
            _form = f.length > 0 ? f : null;
            console.log(_form);
            return formCache; // make it chainable
        },

        /** 
         * Stores the form data in the cookies.
         * 将form data 存储在 cookies 中
         */
        save: function () {
            if (_form === null) return _warn();

            _form
                .find(_strFormElements)
                .each(function() {
                    var f = $(this).attr('id') + ':' + formCache.getFieldValue($(this));
                    _formData.push(f);
                });
            docCookies.setItem('formData', _formData.join(), 31536e3); // 1 year expiration (persistent)
            console.log('Cached form data:', _formData);
            return formCache;
        },

        /** 
         * Fills out the form elements from the data previously stored in the cookies.
         * 使用先前保存在 cookies 中的数据,填充进 form elements
         */
        fetch: function () {
            if (_form === null) return _warn();

            if (!docCookies.hasItem('formData')) return;
            var fd = _formData.length < 1 ? docCookies.getItem('formData').split(',') : _formData;
            $.each(fd, function (i, item) {
                var s = item.split(':');
                var elem = $('#' + s[0]);
                formCache.setFieldValue(elem, s[1]);
            });
            return formCache;
        },

        /** 
         * Sets the value of the specified form field from previously stored data.
         * 设置特定表单域的值
         */
        setFieldValue: function (elem, value) {
            if (_form === null) return _warn();

            if (elem.is('input:text') || elem.is('input:hidden') || elem.is('input:image') ||
                    elem.is('input:file') || elem.is('textarea')) {
                elem.val(value);
            } else if (elem.is('input:checkbox') || elem.is('input:radio')) {
                elem.prop('checked', value);
            } else if (elem.is('select')) {
                elem.prop('selectedIndex', value);
            }
            return formCache;
        },

        /**
         * Gets the previously stored value of the specified form field.
         * 获取先前存储的特定表单域值
         */
        getFieldValue: function (elem) {
            if (_form === null) return _warn();

            if (elem.is('input:text') || elem.is('input:hidden') || elem.is('input:image') ||
                elem.is('input:file') || elem.is('textarea')) {
                    return elem.val();
                } else if (elem.is('input:checkbox') || elem.is('input:radio')) {
                    return elem.prop('checked');
                } else if (elem.is('select')) {
                    return elem.prop('selectedIndex');
                }
            else return null;
        },

        /**
         * Clears the cache and removes the previously stored form data from cookies.
         * 清空 cache 和之前存储的 cookies
         */
        clear: function () {
            _formData = [];
            docCookies.removeItem('formData');
            return formCache;
        },

        /**
         * Clears all the form fields. 
         * This is different from form.reset() which only re-sets the fields 
         * to their initial values.
         * 清空所有表单,不同于 form.reset() ,form.reset() 只是将表单域值重新设置为最初的 value
         */
        clearForm: function () {
            _form
                .find(_strFormElements)
                .each(function() {
                    var elem = $(this);
                    if (elem.is('input:text') || elem.is('input:password') || elem.is('input:hidden') || 
                        elem.is('input:image') || elem.is('input:file') || elem.is('textarea')) {
                        elem.val('');
                    } else if (elem.is('input:checkbox') || elem.is('input:radio')) {
                        elem.prop('checked', false);
                    } else if (elem.is('select')) {
                        elem.prop('selectedIndex', -1);
                    }
                });
            return formCache;
        }
    };
})();

// Save form data right before we unload the form-page
// 在页面卸载之前保存表单域值进 cookies
$(window).on('beforeunload', function (event) {
    formCache.save();
    return false;
});

// Initialize and fetch form data (if exists) when we load 
//the form-page back
// 在页面重新加载的时候,初始化并获取(如果存在的话)表单数据
$(document).on('ready', function (event) {
    formCache.init().fetch();
});

这是demo在 jsFiddle 中的示例;

注意:上边代码需要引来自developer.mozilla.org cookies reader/writer 代码。除此之外,你可以使用 Yahoos YUI2:Cookie Utility(它有一个非常有用的方法 setSub()来设置单独 cookie 的sub-cookies),从而解决我上边提的浏览器 cookies 限制(上边提到的大小和数量的限制)。

localStorage

你可以使用更多诸如localStorage(HTML5中的技术)的现代技术。它更加安全快捷。包含IE8所有浏览器都支持这个 api(而且,ios和安卓也同样支持)。

 // We have local storage support
if (typeof Storage !== 'undefined') {
 // to save to local storage
    localStorage.username = 'Onur'; 
   // to fetch from local storage
    document.getElementById('uname').value = localStorage.username; 
}

所以,类似上边cookies的例子,在页面卸载之前保存,加载之前获取:

$(window).on('beforeunload', function (event) {
    saveFormToLocalStorage();
    return false;
});

$(document).on('ready', function (event) {
    fillFormFromLocalStorage()
});

因为面试被问到,这里译者补充以下localStorage的应用场景:

  • localStorage 可以用来统计页面访问次数
  • localStorage 安全性也不是很好,不能用来保存敏感和重要数据参照
  • 待补充。。。

sessionStorage

这个技术有点类似。“The sessionStorage object is equal to the localStorage object, except that it stores the data for only one session.”摘自 W3C,意思是 sessionStorage对象等同于localStorage对象,除了前者只保存一个会话的数据。


通过无声的AJAX请求将Form数据保存在Server/DB(服务器/数据库)

这不是一个行之有效的途径,但是当其他方法都行不通时,你可能会想使用这个。你可以在windowbeforeunload事件之前发送请求,并给用户一个提示。
注意

$(window).on('beforeunload', function (event) {
    // 检查是否至少有一个表单域填了内容,没有填就不用请求了,如果填了就发请求
    return "You are leaving the page without submitting the form...";
});

页面加载时从Server取出先前存储的数据

小提示:例如用户正在填写一个update(更新)表单,你可以总是从Server端获取之前保存的数据,并自动填写在表单中(当然不包括敏感信息的表单域)。


总结

如果你真的需要这样(保存表单数据)而且尽管有麻烦也值得你这么做,你应该考虑一种跨浏览器的解决方案,从而实现这种fallback(后备)机制。例如:

IF你的开发环境(或者用户环境)已经支持HTML5的特性:使用HTML5autocomplete属性(你可以预先在HTML中嵌入这个属性,或者当你测试浏览器是否支持时,借助JavaScript/jQuery来设置这个属性)。

ELSEIF你(开发环境)支持Storage对象:使用localStorage

ELSEIF[当前会话存储的cookies]+[你的表单数据所需要的 cookies 大小]<4096Bytes:就使用cookies

ELESIF你有一个Server-sideweb-app:发送AJAX请求,把数据保存在Server端。

ELSE 什么都不要做(黔驴技穷)。

注意:想了解更多HTML5的新特性,去看下这个 page和这个 page,或者你可以使用 Modernizr

HTTPS 问题
当使用HTTPS时,所有表单变更都会消失的原因:它是一个安全协议。表单大多时候是让用户输入数据而且(很有可能)包含有敏感数据(密码手机号之类的),所以这种做法其实是天然并且符合人们预期的。我上班提出的解决方案会同样像他们在HTTP下一样奏效,所以我所提出的可以解决你的顾虑。

延伸阅读:


补充:history API

history 也是HTML5 的API,它本来是用来控制前端页面路由的,但是每个路由也可以携带一定数据,所以也可以通过它来保存表单数据。
具体是使用window.history.pushState() ,这使得我们即可以修改 url 也可以不刷新页面,·pushState`是改变路由的一种方法。

 	history.pushState(stateObject, title, url);

其中个参数的含义:

  • 状态对象(stateObject)–stateObject是一个JavaScript对象,通过pushState方法可以将stateObject内容传递到新页面中(这个就是用来保存数据的对象)。
  • 标题(title)–几乎没有浏览器支持该参数,但是传一个空字符串会比较安全。
  • 地址(url)–新的历史记录条目的地址(可选,不指定的话则为文档当前URL);浏览器在调用pushState()方法后不会加载该地址;传入的URL与当前URL应该是同源的,否则,pushState()会抛出异常。

对于SPA跳转页面而且保存表单数据的应用场景,这种方法显得很合“时宜”。

获取当前页面保存的状态数据也非常简单:

let currentState = history.state;

当然可以通过 replaceState 来替换路由:

history.replaceState(stateObj, "page 3", "bar2.html");

详细文档

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值