配合实际场景来理清客户端存储的几种方法

681 阅读12分钟

知识点1:学会cookie在服务端的使用方式

  • 实际场景:七天内免登录功能

知识点2:学会cookie在客户端的使用方式

  • 实际场景:页面换肤功能

知识点3:学会使用localStorage及sessionStorage做数据的持久化

  • 实际场景:页面换肤功能、添加歌曲列表功能

项目初始化搭建

  • 本文以koa来搭建项目的服务器端,借助了koa-setup第三方库(点击进入官网)来快速搭建起项目目录和安装必要的依赖,该项目最终的package.json如下:
{
  "dependencies": {
    "koa": "^2.7.0",
    "koa-bodyparser": "^4.2.1",
    "koa-router": "^7.4.0",
    "koa-static": "^5.0.0",
    "koa-views": "^6.2.0",
    "md5": "^2.2.1",
    "pug": "^2.0.3"
  }
}
  • koa-stup自动构建的帮助下得到该项目的初始化模板后,便可以着手书写配置入口文件,让项目成功运行起来,下面是配置的基础代码:
const Koa = require("koa")
//用来处理动态路由
const Router = require("koa-router")
//静态资源托管
const serve = require("koa-static")
const views = require("koa-views")
//该中间件对从客户端post请求到服务器的数据进行了封装
const bodyParser = require("koa-bodyparser");
//加密数据
const md5 = require("md5");
const app = new Koa()

//托管静态资源
app.use(serve(__dirname+"/static"))


//选择模板引擎映射视图
app.use(views(__dirname + "/views"), {
    map: {
        html: "pug"
    }
})

app.use(bodyParser());

const router = new Router()

router.get("/",(ctx)=>{
    ctx.body = "hello test"
})

router.get("/login",(ctx)=>{
    ctx.body = "hello login"
})

app.use(router.routes())


app.listen(8080,()=>{
    console.log("open server localhost:8080")
})
  • 在配置完基础代码后,通过nodemon第三方库来启动该文件,启动后通过端口号打开页面,可以观察到服务器搭建成功了,效果图如下:

实现七天内免登录功能

  • 流程图如下:
  • 项目目录如下:

第一步:构造登录、错误、展示页面的视图和样式

  • views/login.pug文件的视图:
>>>这里是views/login.pug文件的代码
>>>注意点:这里的表单是以POST的方式往/checkUser地址发送数据

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')
    link(rel='stylesheet', href='css/login.css')
    title Document
  body
    .loginContainer
      h1 登录
      form(action='/checkUser', method='post')
        | 姓名:
        input.inputStyle(type='text', name='username')
        br
        | 密码:
        input.inputStyle(type='password', name='pwd')
        br
        input.loginStyle(type='submit', value='登录')
        |  |
        input(type='checkbox', name='memberMe')
        | 记住我

  • views/error.pug文件的视图:
<!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
    body
        | 用户名或者密码错误
        br
        span 4
        | 秒之后自动跳转回登录页面...
        br
        a(href="/login") 不等了点击直接跳转回登录页面
script.
    let timer = document.querySelector("span").innerText;
    //字符串类型转为数字类型
    timer = parseInt(timer);
    setInterval(()=>{
        timer--
        document.querySelector("span").innerText = timer;
        if(timer<1){
            window.location.href = "/login";
        }
    },1000)

  • views/list.pug文件的视图:
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')
    link(rel='stylesheet', type='text/css', href='css/list.css')
    script(src="js/list.js" type="text/javascript")
    title Document
  body
    button.changeSkin 点击换肤

第二步:服务器页面的实现逻辑部署

  • ./index.js文件的服务器页面部署
router.get("/login", async (ctx, next) => {
    //1.在每次访问/login页面时先通过服务器端获取是否存在该cookie
    let cookieInfo = ctx.cookies.get("isLogin")
    //2.如果存在证明用户可能已经登录成功
    //需在判断该cookie的合法性因为cookie是会被伪造的
    if (cookieInfo) {
        //3.这里为了演示将用户数据写死并做简单的md5加密 
        //实际场景中会去检索数据库里是否有匹配的用户
        let serverInfo = md5("张三" + "123");
        //4.在判定该cookie不仅存在并且与实际的用户信息相匹配的情况下
        //即判定用户已经登录过了且cookie是合法的未过期的
        if (serverInfo == cookieInfo) {
           //直接重定向至展示页面
            ctx.redirect("/list");
        }
    }
    //5.其他情况下访问/login时呈现给用户的是登录页即可
    await ctx.render("login.pug");
})


//1.借助koa-bodyparser来拿到用户填写的登录表单的数据
//使用该库后可以通过ctx.request.body来查看表单提交过来的数据
router.post("/checkUser", (ctx, next) => {
    console.log(ctx.request.body);
    //2.为了演示这里假定用户名是张三 密码是123
    //只有用户输入用户名张三 密码123才可以判定为登录成功
    if (ctx.request.body.username == "张三" && ctx.request.body.pwd == "123") {
        //console.log(ctx.request.body);
        
        //3.只有当用户勾选了 记住我 这个按钮
        //ctx.request.body.memberMe的结果才为true
        if (ctx.request.body.memberMe) {
            //4.当用户勾选记住我按钮后
            //服务端需要使用cookie来告知客户端储存用户登录成功的相关信息
            
            //为了演示这里只做了简单的md5加密
            //实例场景中不应该在客户端存储敏感信息特别是用户信息
            let loginStatus = md5("张三" + "123");
            
            //5.ctx.cookies.set(name, value, [options])
            //服务端使用cookie来告知客户端储存用户信息并设置过期时间
            //这里过期时间设置为 7天
            ctx.cookies.set("isLogin", loginStatus, {
                maxAge: 3600 * 1000 * 24 * 7
            })
        }
        
        //6.用户登录成功后重定向跳转到/list页面;
        ctx.redirect("/list");
    } else {
        //7.用户名或者密码错误跳转到/error页面;
        //这里是只要不满足用户名张三 密码123都会跳转到/error页面
        ctx.redirect("/error");
    }
})

router.get("/list", async (ctx, next) => {
    // await ctx.body='我是list页面'
        
    await ctx.render("list.pug");
})


router.get("/error", async (ctx, next) => {
    // await ctx.body='我是error页面'
    
    await ctx.render("error.pug");

})

实现页面换肤功能

  • 效果图:

实现方法一:通过构建一个对象池子来存储换肤效果从而控制页面的换肤效果

  • views/list.pug文件的视图:(这里引入了js/list.js文件)
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')
    link(rel='stylesheet', type='text/css', href='css/list.css')
    script(src="js/list.js" type="text/javascript")
    title Document
  body
    button.changeSkin 点击换肤
  • 封装实现了换肤效果的js/list.js文件:
/*
缺点是在页面刷新之后换肤的效果没有记忆性
因为key值会重新变为0

注意该js文件是在客户端运行的
如何做到即使是页面重载后依旧能记忆用户的换肤效果呢???
*/
window.onload = function(){
        //定义页面的颜色库
        let colorArr = ["white","rgb(204,232,207)", "rgb(200,200,169)", "rgb(114,111,128)"];
        //颜色库的默认索引为0  默认肤色
        let key = 0;
        //当点击换肤的按钮后
    	document.querySelector(".changeSkin").onclick = function(){
            key++;
            //索引递增到超出颜色库的索引值时置空
            key = key>3?0:key;
            //基于颜色库实现的换肤功能
            document.body.style.background = colorArr[key];
   	 }
}

实现方式二:通过构建一个对象池子来存储换肤效果,并在客户端设置与当前页面的肤色对应的cookie来做到记忆的功能

  • 封装实现了换肤效果的js/list.js文件:
/*
缺点:
浏览器会在每次请求的时候主动组织
所有域下的cookie到请求头 cookie 中
发送给服务器端

单单针对换肤功能来说存储换肤的cookie信息有必要在每次的浏览器请求中
将此cookie信息发送给服务端吗???
*/

window.onload = function(){

        //定义页面的颜色库
        let colorArr = ["white","rgb(204,232,207)", "rgb(200,200,169)", "rgb(114,111,128)"];
               
        //颜色库的默认索引为0  默认肤色
        //影响肤色的是key值 
        //在每一次点击换肤的时候可以保存key值到cookie中 同步记忆效果
        let key = 0;
        if(getCookie('key')){
            key=getCookie('key')
        }
        document.body.style.background = colorArr[key];
        //当点击换肤的按钮后
    document.querySelector(".changeSkin").onclick = function(){
        key++;
        //索引递增 超出颜色库的索引值时置空
        key = key>3?0:key;
        //在每一次点击换肤的时候可以将key值同步保存在cookie中
        setCookie('key',key,{
            "Max-Age":3600
        })
        //基于颜色库实现的换肤功能
        document.body.style.background = colorArr[key];
    }
}


//封装一个设置cookie的方法
function setCookie(name,value,options={}){
    //存储cookie的用法:document.cookie='test=hello;Max-Age=3600;'
    //获取cookie的用法:console.log(document.cookie)
    //document.cookie的返回值是当前域名下的所有cookie
    
    //在客户端获取cookie时数据结构类比test=hello; test2=hello2 test=hello;
    
    //注意点:末尾的分号一定不能忘记!!
    let cookieData = `${name}=${value};`;
    //注意点:如果options是空对象不会产生结果
    for(let key in options){
        let str = `${key}=${options[key]};`;
        cookieData += str;
    }
    document.cookie = cookieData;
}

//封装一个获取cookie的方法
function getCookie(name){
    //注意点:这里是以'; '来分隔cookie字符串 
    //一定不要漏写空格!!!
    let arr = document.cookie.split("; ");
    for(let i =0;i<arr.length;i++){
        //再次分割 循环对比得到匹配值
        let arr2 = arr[i].split("=");
        if(arr2[0]==name){
            return arr2[1];
        }
    }
    //如果对比失败则表示获取不到该name对应的值
    //返回一个空字符串
    return "";
}

实现方式三:通过构建一个对象池子来存储换肤效果,并在客户端设置与当前页面的肤色对应的localStorage来做到记忆的功能

  • 封装实现了换肤效果的js/list.js文件:
/*
localStorage的基本使用
1.localStorage.setItem('local','测试文字')
2.console.log(localStorage.getItem('local'));
3.localStorage.removeItem('local')
4.localStorage.clear()

sessionStorage的api使用与localStorage一致

换肤功能的需求分析:
1.换肤功能需要有时间限制吗?  
1.不需要(localStorage如果不清除是没有过期时间的)

2.换肤的信息需要发送给服务器端吗?
2.不需要(localStorage的信息不会主动发给服务器端)

综上:换肤功能使用localStorage来实现是比较正确的
*/


window.onload = function(){


        //定义页面的颜色库
        let colorArr = ["white","rgb(204,232,207)", "rgb(200,200,169)", "rgb(114,111,128)"];
        
        
        //颜色库的默认索引为0  默认肤色
        //影响肤色的是key值 在每一次点击换肤的时候可以保存key值在cookie中
        let key = 0;
        if(localStorage.getItem('key')){
            key=localStorage.getItem('key')   
        }
        document.body.style.background = colorArr[key];
        
        //当点击换肤的按钮后
    document.querySelector(".changeSkin").onclick = function(){
        key++;
        //索引递增 超出颜色库的索引值时置空
        key = key>3?0:key;
        
        localStorage.setItem('key',key)

        //基于颜色库实现的换肤功能
        document.body.style.background = colorArr[key];
    }
}

实现页面数据通信

  • 用localStorage同域下页面共享的特性可以实现页面数据通信,并基于数据来完成跨页面的视图更新
  • 流程图为:
  • 效果图如下:
  • 构造目录如下:
  • ./index.js文件的代码
//引入歌单信息的json文件
//通过require可以直接把json数据转换为js对象
const musicData = require("./data/music.json");


router.get("/list", async (ctx, next) => {
    //把歌单数据传递给views/list.pug页面上
    /*这里是介绍views/list.pug页面如何使用传递过去的musicData数据
    each val,key in musicData
    	ul(class="listContainer "+(key%2==0?"grayBg":"") )
         span(class="btnController" onclick="showDetail("+JSON.stringify(val)+")")
    */
    await ctx.render("list.pug", {
        musicData
    });
})
  • views/list.pug文件的代码:
  • 注意点:关注歌曲数据musicData的使用方式,点击+号按钮触发的是showDetail()方法
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')
    link(rel='stylesheet', type='text/css', href='css/list.css')
    script(src="js/list.js" type="text/javascript")
    title Document
  body
    button.changeSkin 换肤
    each val,key in musicData
      ul(class="listContainer "+(key%2==0?"grayBg":"") )
        li.grayWord #{key+1}
        li #{val.songName}
        li #{val.album}
        li.grayWord #{val.time}
        span(class="btnController" onclick="showDetail("+JSON.stringify(val)+")")
          a(href='#')
            img(src='img/play.png')
          img(src='img/add.png')
  • views/detail.pug文件的代码:
  • 注意点:关注如何基于localStorage同域下跨页面数据共享的特性实现更新视图
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')
    link(rel='stylesheet', type='text/css', href='css/detail.css')
    script(src="js/detail.js" type="text/javascript")
    title Document
  body
    .headContainer
      a.btnStyle(href='/list') 返回上一页
      a.btnStyle.deleteItem 删除
      a.btnStyle.deleteAll 清空列表
    .listContainer
      ul.myul
        input(type='checkbox')
        li 歌曲
        li 歌手
        li 时长
      .exchange
  • static/js/list.js文件的代码:
//该函数会接收传递过来的要添加的单个歌曲信息
function showDetail(musicData){
    //console.log(musicData,'我是表示一条歌曲信息的对象:但此时还是字符串状态');
    if(localStorage.getItem("musicData")){
        //去重 防止单个歌曲信息被重复添加进localStorage中
        let localData = JSON.parse(localStorage.getItem("musicData"));
        //该音乐信息是不是被添加过了   有的话不需要再做处理了
        if(!localData.find(v=>v.id==musicData.id)){
            localData.push(musicData);
            localStorage.setItem("musicData",JSON.stringify(localData));
        }
    }else{
        //localStorage的key和value都要是字符串类型
        //如果从来没存过的话就初始化存储一次
        localStorage.setItem("musicData",JSON.stringify([musicData]));
    }
    
    
    //通过isOpen变量来记录/detail页面是否是已经打开的状态
    //如果是打开的状态在点击打开的按钮时不应该再去打开一个新页面
    //不需要一直在打开详细页面  用isOpen变量存储是该页面是否是打开的状态
    if(!localStorage.getItem("isOpen")){
        window.open("/detail");
    }
}
  • static/js/detail.js文件的代码:
//储存/detail.js的开启状态;
localStorage.setItem("isOpen",true);

//当页面关闭时候清除开启状态;  
//也就是再次点击/list中的加号按钮时可以在打开一个新窗口了
window.addEventListener("beforeunload",function(){
    localStorage.removeItem("isOpen");
})

//当localStorage的值变更时会触发该条件  
//否则每一次都需要手动刷新页面
//监控localStorage是否有变化
window.addEventListener("storage",function(){
    //更新视图;
    updateView();
})

window.onload = function(){
    updateView();

    //点击清空所有歌曲的按钮的逻辑
    document.querySelector(".deleteAll").onclick = function(){
        localStorage.removeItem("musicData");
        updateView();
    }

    //清点击清空空勾选的歌曲的按钮的逻辑
    document.querySelector(".deleteItem").onclick = function(){
        //拿到所有 check 的复选框  
        //当该复选框的checked时true代表其勾选了
        let inputs  =document.querySelectorAll(".exchange input");
        let musicData = localStorage.getItem("musicData");

        //有可能获取不到  就表示没有可清除的
        musicData  = JSON.parse(localStorage.getItem("musicData")) || [];
        inputs.forEach((v,k)=>{
            if(v.checked){
                //这里的数据中 input的索引值是和musicData的数据索引是一一对应的
                //所以勾选那个就删除相对应的musicData的索引即可
                musicData.splice(k,1);
            }
        })

        //基于删除后的数据再次重新写入localStorage
        localStorage.setItem("musicData",JSON.stringify(musicData));
        //重新写入localStorage后更新视图
        updateView();
    }
}


function updateView(){
    let musicData = localStorage.getItem("musicData");
    if(musicData){
        //根据localStorage的歌曲数据组装视图
        musicData = JSON.parse(musicData);
        let innerContent = "";

        musicData.forEach(v=>{
            let str = `<ul class="myul">
                        <input type="checkbox" />
                        <li>${v.songName}</li>    
                        <li>${v.singer}</li>  
                        <li>${v.time}</li>          
                       </ul>`;
            innerContent+=  str;
        })
        //替换的节点
        document.querySelector(".exchange").innerHTML = innerContent;
    }else{
        document.querySelector(".exchange").innerHTML = "";
    }
}

总结客户端存储的几种方法

  • cookie是http协议下,服务端或者脚本可以维护客户端信息的一种方式。
  • 客户端操作cookie特点:
    • 浏览器会主动存储接收到的 set-cookie 头信息的值;
    • 有时效性;
    • 可以设置 http-only 属性为 true 来禁止客户端代码(js)修改该值

  • localStorage和sessionStorage和cookie共同点:
    • 同域(同源策略)限制:同源策略:请求与响应的 协议、域名、端口都相同 则时同源,否则为 跨源/跨域
    • 存储的内容都会转为字符串格式
    • 都有存储大小限制

  • localStorage和sessionStorage共同点:
    • API相同
    • 存储大小限制一样基本类似
    • 无个数限制

  • localStorage和sessionStorage和cookie不同点:
    • localStorage没有有效期,除非删除,否则一直存在,支持同域下页面共享,支持 storage 事件

    • sessionStorage浏览器关闭,自动销毁,页面数据私有(/list下设置的sessionStorage数据在 /detail下拿不到的),不支持 storage 事件

    • cookie
    • 浏览器也会在每次请求的时候主动组织所有域下的cookie到请求头 cookie 中,发送给服务器端;
    • 浏览器会主动存储接收到的 set-cookie 头信息的值;
    • 可以设置 http-only 属性为 true 来禁止客户端代码(js)修改该值;
    • 可以设置有效期 (默认浏览器关闭自动销毁)(不同浏览器有所不同);
    • 同域下个数有限制,最好不要超过50个(不同浏览器有所不同);
    • 单个cookie内容大小有限制,最好不要超过4000字节(不同浏览器有所不同)