第二十一篇 离线应用与客户端存储

发布时间 2023-04-05 22:48:48作者: caix-1987
by caix in 深圳

离线应用

支持离线 Web 应用开发是 HTML5 的另一个重点。所谓离线 Web 应用,就是在设备不能上网的情况下仍然可以运行的应用

开发离线Web 应用需要几个步骤。首先是确保应用知道设备是否能上网,以便下一步执行正确的操作。然后,应用还必须能访问一定的资源(图像、Javascript、CSS等),只有这样才能正常工作。最好,必须有一块本地空间用户保存数据,无论能否上网都不妨碍读写

HTML5 及其相关的 API让开发离线应用成为现实

离线检测

要知道设备是否在线还是离线,HTML5 定义了一个 navigator.onLine 属性,这个属性值为 true 表示设备能上网,值为 false 表示设备离线

if (navigator.onLine) {
    // 正常工作
} else {
    // 执行离线状态时的任务
}

由于 navigator.onLine 存在一定的兼容性问题,因此除了 navigator.onLine 属性之外,为了更好地确定网络是否可用,HTML5 还定义了两个事件 online 和 offline

当网络在离线和在线之间切换时在 window 对象上触发这两个事件

window.addEventListener('online', function() {
    // 正常工作
});

window.addEventListener('offline', function() {
    // 执行离线状态时的任务
});

在实际应用中,最好在页面加载后,最好先通过 navigator.onLine 取得初始的状态。然后通过上述两个事件来确定网络连接状态是否变化。当上述事件触发时,navigator.onLine 属性的值也会改变,不过必须要手工轮询这个属性才能检测到网络状态的变化

应用缓存

HTML5 的应用缓存(application cache),或者简称为 appcache,是专门为开发离线 Web 应用而设计的。Appcache 就是从浏览器的缓存中分出来的一块缓存区。要想在这个缓存中保存数据,可以使用一个描述文件(manifest file),列出要下载和缓存的资源

描述文件示例

CACHE MANIFEST
# Comment

file.js
file.css

然后在 html 中引用:

<html manifest="./xxx.manifest">

xxx.manifest 文件的 MIME 类型必须是 text/cache-manifest

该 API 的核心是 applicationCache 对象,这个对象有一个 status 属性,属性的值是常量,表示应用缓存的如下当前状态

0: 无缓存,即没有与页面相关的应用缓存
1: 闲置,即应用缓存未得到更新
2: 检查中,即正在下载描述文件并检查更新
3: 下载中,即应用缓存正在下载描述文件中指定的资源
4: 更新完成,即应用缓存已经更新了资源,而且所有资源都已下载完毕,可以通过 swapCache() 来使用了
5: 废弃,即应用缓存的描述文件已经不存在了,因此页面无法再访问应用缓存

相关事件

checking: 在浏览器为应用缓存查找更新时触发
error: 在检查更新或者下载资源期间发生错误时触发
noupdate: 在检查描述文件发现文件无变化时触发
downloading: 在开始下载应用缓存资源时触发
progress: 在文件下载应用缓存的过程中持续不断地触发
updateready: 在页面新的应用缓存下载完毕且可以通过 swapCache() 使用时触发
cached: 在应用缓存完整可用时触发

一般来讲,这些事件会随着页面加载按上述顺序依次触发。也可以通过调用 update() 方法手动触发上述事件

数据存储

基础知识

HTTP Cookie,通常直接叫做 cookie,是在客户端用于存储会话信息的。该标准要求服务器对任意 HTTP 请求发送 Set-Cookie HTTP 头信息作为响应的一部分,其中包含会话信息。服务器响应头示例

HTTP/1.1 200 OK
Content-type: text/html
Set-Cookie: name=value
Other-header: other-header-value

发送回服务器的额外信息可以用于唯一验证客户来自于发送的哪个请求

完整的 cookie 包括:

名称: 一个唯一确定 cookie 的名称。必须被 URL 编码。
值: 存储在 cookie 中的字符串值。必须被 URL 编码。
域: cookie 对于哪个域是有效的。
路径: 对于指定域中的那个路径,应该向服务器发送 cookie。
失效时间: 表示 cookie 何时应该被删除的时间戳。
安全标志: 指定后,cookie 只有在使用 SSL 连接的时候才发送到服务器。

Set-Cookie:name=value; domain=www.laixiangran.cn; path=/; expires=Mon, 29 Oct 2018 03:53:10 GMT; secure;

大小限制

每个域的 cookie 总数是有限,不同浏览器之间所有不同,IE6 以下是最多 20 个,IE7 以上最多 50 个,Firefox最多 50 个,Opera 最多 30 个,Safari 和 Chrome 不限制

cookie 的尺寸也有限制,大多数浏览器有大约 4096B。
基本用法

当用来获取属性值时,document.cookie 返回当前页面可用的所有 cookie 字符串,一系列由分号隔开的键值对,如下所示:

document.cookie
// name1=value1;name2=value2;name3=value3;

当用来设置值时,document.cookie 属性会设置一个新的 cookie 字符串添加到现有的 cookie 集合中,并不会像普通对象设置属性一样覆盖原 cookie 的值,除非设置的 cookie 的名称已经存在,如下所示:

// cookie 的名称不存在
document.cookie = 'name4=value4'
// name1=value1;name2=value2;name3=value3;name4=value4;
// 而不是 name4=value4;

// cookie 的名称存在
document.cookie = 'name3=value4'
// name1=value1;name2=value2;name3=value4;
我们可以封装一些方法,方便我们对 cookie 的操作

var CookieUtil = {

    get: function (name) {
        var cookieName = encodeURIComponent(name) + "=",
            cookieStart = document.cookie.indexOf(cookieName),
            cookieValue = null,
            cookieEnd;

        if (cookieStart > -1) {
            cookieEnd = document.cookie.indexOf(";", cookieStart);
            if (cookieEnd == -1) {
                cookieEnd = document.cookie.length;
            }
            cookieValue = decodeURIComponent(document.cookie.substring(cookieStart + cookieName.length, cookieEnd));
        }

        return cookieValue;
    },

    set: function (name, value, expires, path, domain, secure) {
        var cookieText = encodeURIComponent(name) + "=" + encodeURIComponent(value);

        if (expires instanceof Date) {
            cookieText += "; expires=" + expires.toGMTString();
        }

        if (path) {
            cookieText += "; path=" + path;
        }

        if (domain) {
            cookieText += "; domain=" + domain;
        }

        if (secure) {
            cookieText += "; secure";
        }

        document.cookie = cookieText;
    },

    unset: function (name, path, domain, secure) {
        this.set(name, "", new Date(0), path, domain, secure);
    }

};

使用方法:

// 设置 cookie
CookieUtil.set('name', 'lai');
CookieUtil.set('sex', 'man');

// 读取 cookie
CookieUtil.get('name'); // 'lai'
CookieUtil.get('sex'); // 'man'

// 删除 cookie
CookieUtil.unset('name');
CookieUtil.unset('sex');

// 设置 cookie,包括它的路径、域、失效日期
CookieUtil.set('name', 'lai', '/', 'www.laixiangran.cn', new Date());

Web Storyage

Web Storage 的目的是克服由 cookie 带来的一些限制,当数据需要被严格控制在客户端上时,无须持续地将数据发回服务器

Web Storage 的两个主要目标是:

提供一种在 cookie 之外存储会话数据的路径。
提供一种存储大量可以跨会话存在的数据的机制。

Web Storage 主要定义了两种对象:

sessionStorage 

localStorage

这两个对象区别如下

sessionStorage: 存储特定于某个会话的数据,也就是该数据只保持到浏览器关闭。存储数据大小大多数限制在 2.5M,少数浏览器限制在 5M 或者不限制。

localStorage: 数据保存到通过 JavaScript 删除或者是用户清除浏览器缓存。存储数据大小大多数限制在 5M,少数浏览器限制在 2.5M 或者不限制。

都是 Storage 对象的实例
Storage 类型有如下方法

clear(): 删除所有值。
getItem(name): 根据指定的名字 name 获取对应的值。
key(index): 获取 index 位置处的值的名字。
removeItem(name): 删除由 name 指定的键值对。
setItem(name, value): 为指定的 name 设置一个对应的值,值为字符串。

对 sessionStorage 和 localStorage 进行操作都会触发 storage 事件,该事件对象有以下属性:

domain: 发生变化的存储空间的域名。
key: 设置或者删除的键名。
newValue: 如果是设置值,则是新值;如果是删除键,则是null。
oldValue: 键被更改之前的值。

IndexedDB

Indexed Database API,简称为 IndexedDB,是在浏览器中保存结构化数据的一种数据库。其思想是创建一套 API,方便保存和读取 JavaScript 对象,同时还支持查询和搜索

ndexedDB 设计的操作完全是异步进行的。因此,大多数操作会以请求方式进行

限制

和 Web Storage类似,只能由同源(相同协议、域名和端口)页面操作,因此不能跨域共享信息。

Firefox 大小上限为 50M,移动端的 Firefox 大小上限为 5M,不允许本地文件访问。

Chrome 大小上限为 5M,允许本地文件访问。
var indexedDB = window.indexedDB || window.msIndexedDB || window.mozIndexedDB || window.webkitIndexedDB, // 获取 indexedDB
    request,
    store,
    database,
    users = [
        {
            username: "007",
            firstName: "James",
            lastName: "Bond",
            password: "foo"
        },
        {
            username: "ace",
            firstName: "John",
            lastName: "Smith",
            password: "bar"
        }
    ];

// 打开数据库
request = indexedDB.open("example");

// 注册 onerror 及 onsuccess 事件
request.onerror = function (event) {
    alert("Something bad happened while trying to open: " + event.target.errorCode);
};
request.onsuccess = function (event) {
    database = event.target.result;
    
    // 操作数据库
    initializeDatabase();
};

function initializeDatabase() {
    if (database.version != "1.0") {
    
        // 设置数据库版本号
        request = database.setVersion("1.0");
        request.onerror = function (event) {
            alert("Something bad happened while trying to set version: " + event.target.errorCode);
        };
        request.onsuccess = function (event) {
        
            // 使用 users 创建对象存储空间
            store = database.createObjectStore("users", {keyPath: "username"});
            var i = 0,
                len = users.length;

            while (i < len) {
            
                // 插入新值
                store.add(users[i++]);
            }

            alert("Database initialized for first time. Database name: " + database.name + ", Version: " + database.version);
        };
    } else {
        alert("Database already initialized. Database name: " + database.name + ", Version: " + database.version);
        
        // transaction() 创建事务,objectStore() 将存储空间传入事务
        request = database.transaction("users").objectStore("users").get("007");
        request.onsuccess = function (event) {
            alert(event.target.result.firstName);
        };
    }
}

会话机制

我们通常说的会话就是客户端和服务端进行交互的这个过程,会话机制就是交互过程遵从的一些规则和工作原理

HTTP无状态无连接。其中无连接是指,早期的服务端处理完客户端的请求,受到客户端的应答后立即断开。也就是说客户端每请求一次都会重新建立一个TCP连接。请求完成后服务端就会断开连接释放资源。但是当页面有很多图片等静态资源时,客户端每次请求都建立一次TCP连接就会显得很低效

因此,Keep-Alive被提出来解决这个问题。当HTTP请求头中包含content:Keep-Alive时。客户端与赋予段的连接将不会被断开,当客户端发送另一个请求时就是用这个已经建立好的连接,直到超过Keep-Alive规定的时间连接才会断开。

无状态是指HTTP协议是一个无状态的协议,每个请求都是独立的。这样一来,服务器不知道客户端是什么状态,这严重阻碍了交互式应用程序的开发和使用。

因此就出现了两种用于保持HTTP状态的技术。也就是cookie机制和session机制。

具体来说cookie机制采用的是在客户端保持状态的方案,而session机制采用的是在服务器端保持状态的方案。同时我们也看到,由于采用服务器端保持状态的方案在客户端也需要保存一个标识,所以session机制可能需要借助于cookie机制来达到保存标识的目的,但实际上它还有其他选择
cookie怎样生成

正统的cookie是通过扩展HTTP协议来实现的。服务器通过在GTTP响应头中加上一行特殊的指示,浏览器根据只是生成相应的cookie。当然也可以通过客户端代码来生成cookie。例如使用js操作cookie

cookie有哪些内容

cookie包括键名,键值,失效日期,路径和域。键名和键值不做赘述。失效日期如果不设置,则这个cookie的有效期就是浏览器会话期间内,一旦浏览器关闭则此cookie失效。域和路径合起来就是cookie的作用范围。

cookie怎样使用

是由浏览器按照一定的原则在后台自动发送给服务器的。浏览器检查所有存储的cookie,如果某个cookie所声明的作用范围大于等于将要请求的资源所在的位置,则把该cookie附在请求资源的HTTP请求头上发送给服务器。
cookie工作流程:

servlet创建cookie,保存少量数据,发送浏览器。

浏览器获得服务器发送的cookie数据,将自动的保存到浏览器端。

下次访问时,浏览器将自动携带cookie数据发送给服务器。
Session怎样生成

当客户端请求服务端的时候,服务器的servelet会为当前客户端创建一个session并生成一个sessionId指向这个session。ssionId使用种cookie的方法发送给客户端。大部分session机制都使用会话cookie(即有效期为一个浏览器的会话期间的cookie)来保存sessionId,而关闭浏览器后这个sessionId就消失了,就会导致找不到原来的session。但是由于cookie可以被人为的禁止,必须有其他机制以便在cookie被禁止时仍然能够把sessionid传递回服务器。经常被使用的一种技术叫做URL重写,就是把session id直接附加在URL路径的后面。

由于服务端无法判断客户端是否已经结束了当前会话所以会在session中设置一个失效时间,一般是30分钟,当距离用户上一次使用session的时间超过这个失效时间,服务器就认为客户端已将停止了此次会话,才会删除session节省存储空间。

session的工作原理
当客户端需要和服务端进行会话的时候,请求发送到服务端。服务端会先检查这个请求里是否包含了一个sessionId。如果已经包含,服务端可以根据此sessionId检索出session来使用。如果检索不出来(session已经失效被删除了)服务端会针对这个sessionId新建一个session。如果客户端请求不包括sessionId这个值,则为此客户端创建一个session并且生成一个与此session相关联的sessionid。写入cookie.
Token(令牌)
Acesss Token

访问资源接口(API)时所需要的资源凭证

简单 token 的组成: uid(用户唯一的身份标识)、time(当前时间的时间戳)、sign(签名,token 的前几位以哈希算法压缩成的一定长度的十六进制字符串)

特点:

服务端无状态化、可扩展性好
支持移动端设备
安全
支持跨程序调用

token 的身份验证流程:

客户端使用用户名跟密码请求登录

服务端收到请求,去验证用户名与密码

验证成功后,服务端会签发一个 token 并把这个 token 发送给客户端
客户端收到 token 以后,会把它存储起来,比如放在 cookie 里或者 localStorage 里

客户端每次向服务端请求资源的时候需要带着服务端签发的 token

服务端收到请求,然后去验证客户端请求里面带着的 token ,如果验证成功,就向客户端返回请求的数据

每一次请求都需要携带 token,需要把 token 放到 HTTP 的 Header 里

基于 token 的用户认证是一种服务端无状态的认证方式,服务端不用存放 token 数据。用解析 token 的计算时间换取 session 的存储空间,从而减轻服务器的压力,减少频繁的查询数据库

token 完全由应用管理,所以它可以避开同源策略
Token 和 Session 的区别

Session 是一种记录服务器和客户端会话状态的机制,使服务端有状态化,可以记录会话信息。而 Token 是令牌,访问资源接口(API)时所需要的资源凭证。Token 使服务端无状态化,不会存储会话信息。

Session 和 Token 并不矛盾,作为身份认证 Token 安全性比 Session 好,因为每一个请求都有签名还能防止监听以及重放攻击,而 Session 就必须依赖链路层来保障通讯安全了。如果你需要实现有状态的会话,仍然可以增加 Session 来在服务器端保存一些状态。

所谓 Session 认证只是简单的把 User 信息存储到 Session 里,因为 SessionID 的不可预测性,暂且认为是安全的。而 Token ,如果指的是 OAuth Token 或类似的机制的话,提供的是 认证 和 授权 ,认证是针对用户,授权是针对 App 。其目的是让某 App 有权利访问某用户的信息。这里的 Token 是唯一的。不可以转移到其它 App上,也不可以转到其它用户上。

Session 只提供一种简单的认证,即只要有此 SessionID ,即认为有此 User 的全部权利。是需要严格保密的,这个数据应该只保存在站方,不应该共享给其它网站或者第三方 App。所以简单来说:如果你的用户数据可能需要和第三方共享,或者允许第三方调用 API 接口,用 Token 。如果永远只是自己的网站,自己的 App,用什么就无所谓了。
JWT
JSON Web Token(简称 JWT)是目前最流行的跨域认证解决方案。
是一种认证授权机制。

JWT 是为了在网络应用环境间传递声明而执行的一种基于 JSON 的开放标准(RFC 7519)。JWT 的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。比如用在用户登录上。

可以使用 HMAC 算法或者是 RSA 的公/私秘钥对 JWT 进行签名。因为数字签名的存在,这些传递的信息是可信的。