第四篇-仿写Vue生态系列Proxy双向绑定与封装请求

8次阅读

共计 6711 个字符,预计需要花费 17 分钟才能阅读完成。

(第四篇)仿写 ’Vue 生态 ’ 系列___”Proxy 双向绑定与封装请求 ”

本次任务

  1. vue3.0 使用了 Proxy 进行数据的劫持, 那当然就有必要研究并实战一下这方面知识了.
  2. 对 Reflect 进行解读, 并将 Object 的操作少部分改为 Reflect 的形式.
  3. 异步不能总用定时器模拟, 本次自己封装一个简易的 ’axios’.
  4. 有了请求当然需要服务器, 用 koa 启动一个简易的服务.

一. Proxy

vue3.0 选择了这个属性, 虽然也会提供兼容版本, 但基本也算是跟老版 ie 说再见了, Proxy 会解决之前无法监听数组的修改这个痛点, 也算是我辈前端的福音了.
使用方面会有很大不同, defineProperty 是监控一个对象, 而 Proxy 是返回一个新对象, 这就需要我完全重写 Observer 模块了, 话不多说先把基本功能演示一下.
由下面的代码可知:

  1. Proxy 可以代理数组.
  2. 代理并不会改变原数据的类型, Array 还是 Array.
  3. 修改 length 属性会触发 set, 浏览器认为 length 当然是属性, 修改他当然要触发 set.
  4. 像是 push, pop 这种操作也是会触发 set 的, 而且不止一次, 可以借此看出这些方法的实现原理.
  let ary = [1, 2, 3, 4];
  let proxy = new Proxy(ary, {get(target, key) {return target[key];
    },
    set(target, key, value) {console.log('我被触发了');
      return value;
    }
  });
  console.log(Array.isArray(proxy)); // true
  proxy.length = 1; // 我被触发了

我之前写的劫持模块就需要彻底改版了
cc_vue/src/Observer.js
改变 $data 指向我选择在这里做, 为了保持主函数的纯净.

// 数据劫持
import {Dep} from './Watch';
let toString = Object.prototype.toString;
class Observer {constructor(vm, data) {
    // 由于 Proxy 的机制是返回一个代理对象, 那我们就需要更改实例上的 $data 的指向了
    vm.$data = this.observer(data);
  }
}

export default Observer;

observer
对象与数组是两种循环的方式, 每次递归的解析里面的元素, 最后整个对象完全由 Proxy 组成.

 observer(data) {let type = toString.call(data),
        $data = this.defineReactive(data);
    if (type === '[object Object]') {for (let item in data) {data[item] = this.defineReactive(data[item]);
      }
    } else if (type === '[object Array]') {
      let len = data.length;
      for (let i; i < len; i++) {data[i] = this.defineReactive(data[i]);
      }
    }
    return $data;
  }

defineReactive
遇到基本类型我会直接 return;
代理基本类型还会报错????;

 defineReactive(data) {let type = toString.call(data);
    if (type !== '[object Object]' && type !== '[object Array]') return data;
    let dep = new Dep(),
        _this = this;
    return new Proxy(data, {get(target, key) {Dep.target && dep.addSub(Dep.target);
        return target[key];
      },
      set(target, key, value) {if (target[key] !== value) {
        // 万一用户付给了一个新的对象, 就需要重新生成监听元素了.
          target[key] = _this.observer(value);
          dep.notify();}
        return value;
      }
    });
  }

Observer 模块改装完毕
现在 vm 上面的 data 已经是 Proxy 代理的 data 了, 也挺费性能的, 所以说用 vue 开发的时候, 尽量不要弄太多数据在 data 身上.

二. Reflect

这个属性也蛮有趣的, 它的出现很符合设计模式, 数据就是要有一套专用的处理方法, 而且函数式处理更符合 js 的设计理念.

  1. 静态方法 Reflect.defineProperty() 基本等同于 Object.defineProperty() 方法,唯一不同是返回 Boolean 值, 这样就不用担心 defineProperty 时的报错了.
  2. Reflect 对象的方法与 Proxy 对象的方法一一对应,只要是 Proxy 对象的方法,就能在 Reflect 对象上找到对应的方法。

下面把常用的方法演示一下
操作成功或失败会返回布尔值

 let obj = {name:'lulu'};
  console.log(Reflect.get(obj,'name')) // name
  console.log(Reflect.has(obj,'name')) // true
  console.log(Reflect.has(obj,'name1')) // false
  console.log(Reflect.set(obj,'age',24)) // true
  console.log(Reflect.get(obj,'age')) // 24

把我的代码稍微改装一下
cc_vue/src/index.js

 proxyVm(data = {}, target = this) {for (let key in data) {
      Reflect.defineProperty(target, key, {enumerable: true, // 描述属性是否会出现在 for in 或者 Object.keys()的遍历中
        configurable: true, // 描述属性是否配置,以及可否删除
        get() {return Reflect.get(data,key)
        },
        set(newVal) {if (newVal !== data[key]) {Reflect.set(data,key,newVal)
          }
        }
      });
    }
  }

三. 封装简易的 ”axios”

我见过很多人离开 axios 或者 jq 中的 ajax 就没法做项目了, 其实完全可以自己封装一个, 原理都差不多, 而且现在也可以用 ’feach’ 弄, 条件允许的情况下真的不一定非要依赖插件.
独立的文件夹负责网络相关的事宜;
cc_vue/use/http

class C_http {constructor() {
    // 请求可能很多, 并且需要互不干涉, 所以决定每个类生成一个独立的请求
    let request = new XMLHttpRequest();
    request.responseType = 'json';
    this.request = request;
  }
}

编写插件的时候, 先要考虑用户会怎么用它

  1. 用户指定请求的方法, 本次只做 post 与 get.
  2. 可以配置请求地址.
  3. 可以传参, 当然 post 与 get 处理参数肯定不一样.
  4. 返回值我们用 Promise 的形式返回给用户.
  http.get('http:xxx.com', { name: 'lulu'}).then(data => {});
  http.post('http:xxx.com', { name: 'lulu'}).then(data => {});

get 与 post 方法其实不用每次都初始化, 我们直接写在外面
处理好参数直接调用 open 方法, 进入 open 状态某些参数才能设置;
在有参数的情况下为链接添加 ’?’;
参数品在链接后面, 我之前遇到一个 bug, 拼接参数的时候如果结尾是 ’&’ 部分手机出现跳转错误, 所以为了防止特殊情况的发生, 我们要判断一下干掉结尾的 ’&’;

function get(path, data) {let c_http = new C_http();
  let str = '?';
  for (let i in data) {str += `${i}=${data[i]}&`;
  }
  if (str.charAt(str.length - 1) === '&') {str = str.slice(0, -1);
  }
  path = str === '?' ? path : `${path}${str}`;
  c_http.request.open('GET', path);
  return c_http.handleReadyStateChange();}

post
这个就很好说了, .data 是请求自带的.

function post(path, data) {let c_http = new C_http();
  c_http.request.open('POST', path);
  c_http.data = data;
  return c_http.handleReadyStateChange();}

handleReadyStateChange

handleReadyStateChange() { 
    // 这个需要在 open 之后写
    // 设置数据类型
    this.request.setRequestHeader(
      'content-type',
      'application/json;charset=utf-8'
    );
    // 现在前端所有返回都是 Promise 化;
    return new Promise((resolve) => {this.request.onreadystatechange = () => {// 0    UNSENT    代理被创建,但尚未调用 open() 方法。// 1    OPENED    open() 方法已经被调用。// 2    HEADERS_RECEIVED    send() 方法已经被调用,并且头部和状态已经可获得。// 3    LOADING    下载中;responseText 属性已经包含部分数据。// 4    DONE    下载操作已完成。if (this.request.readyState === 4) {
        // 这里因为是独立开发, 就直接写 200 了, 具体项目里面会比较复杂
          if (this.request.status === 200) {
           // 返回值都在 response 变量里面
            resolve(this.request.response);
          }
        }
      };
      // 真正的发送事件.
      this.send();});
  }

send

send() {
// 数据一定要 JSON 处理一下
    this.request.send(JSON.stringify(this.data));
  }

很多人提到 “ 拦截器 ” 会感觉很高大上, 其实真的没啥
简易的拦截器 ”interceptors”????

// 1: 使用对象不使用 [] 是因为可以高效的删除拦截器
const interceptorsList = {};
// 2: 每次发送数据之前执行所有拦截器, 别忘了把请求源传进去.
  send() {for (let i in interceptorsList) {interceptorsList[i](this);
    }
    this.request.send(JSON.stringify(this.data));
  }
// 3: 添加与删除拦截器的方法, 没啥东西所以直接协议期了.
function interceptors(cb, type) {if (type === 'remove') {delete interceptorsList[cb];
  } else if (typeof cb === 'function') {interceptorsList[cb] = cb;
  }
}

边边角角的小功能

  1. 设置超出时间与超出时的回调.
  2. 请求的取消
class C_http {constructor() {let request = new XMLHttpRequest();
    request.timeout = 5000;
    request.responseType = 'json';
    request.ontimeout = this.ontimeout;
    this.request = request;
  }
 ontimeout() {throw new Error('超时了, 快检查一下');
  }
abort() {this.request.abort();
  }
}

简易的 ’axios’ 就做好, 普通的请求都没问题的

四. 服务器

请求做好了, 当然要启动服务了, 本次就不连接数据库了, 要不然就跑题了.

koa2
不了解 koa 的同学跟着做也没问题

npm install koa-generator -g
Koa2 项目名

cc_vue/use/server 是本次工程的服务相关存放处.

cc_vue/use/server/bin/www
端口号可以随意更改, 当时 9999 被占了我就设了 9998;

const pros = '9998';
var port = normalizePort(process.env.PORT || pros);

cc_vue/use/server/routes/index.js

这个页面就是专门处理路由相关, koa 很贴心, router.get 就是处理 get 请求.
每个函数必须写 async 也是为了著名的 ’ 洋葱圈 ’.
想了解更多相关知识可以去看 koa 教程, 我也是用到的时候才会去看一眼.
写代码的时候遇到需要测试延迟相关的时候, 不要总用定时器, 要多自己启动服务.

const router = require('koa-router')();

router.get('/', async (ctx, next) => {
  ctx.body = {data: '我是数据'};
});

router.post('/', async (ctx, next) => {ctx.body = ctx.request.body;});

module.exports = router;

写到现在可以开始跑起来试试了

五. 跨域

???? 一个很传统的问题出现了 ’ 跨域 ’.
这里我们简单的选择插件来解决, 十分粗暴.
cc_vue/use/server/app.js

npm install --save koa2-cors
var cors = require('koa2-cors');
app.use(cors());

既然说到这里就, 那就总结一下吧
跨域的几种方式

  1. jsonp 这个太传统了, 制作一个 script 标签发送请求.
  2. cors 也就是服务端设置允许什么来源的请求, 什么方法的请求等等, 才可以跨域.
  3. postMessage 两个页面之间传值, 经常出现在一个页面负责登录, 另一个页面获取用户的登录 token.
  4. document.domain 相同的 domain 可以互相拿数据.
  5. window.name 这个没人用, 但是挺好玩, 有三个页面 a,b,c, a 与 b 同源, c 单独一个源, a 用 iframe 打开 c 页面, c 把要传的值放在 window.name 上, 监听加载成功事件, 瞬间改变 iframe 的地址, 为 b, 此时 b 同源, window 会被继承过来, 偷梁换柱, 利用了换地址 window 不变的特点;
  6. location.hash 这个也好玩, 是很聪明的人想出来的, 有三个页面 a,b,c, a 与 b 同源, c 单独一个源,a 给 c 传一个 hash 值(因为一个网址而已, 不会跨域), c 把 hash 解析好, 把结果 用 iframe 传递给 b,b 使用 window.parent.parent 找到父级的父级, window.parent.parent.location.hash = ‘xxxxx’, 操控父级;
  7. http-proxy 就比如说 vue 的代理请求, 毕竟服务器之间不存在跨域.
  8. nginx 配置一下就好了, 比前端做好多了
  9. websocket 人家天生就不跨域.

本次测试的 dom 结构

<div id="app">
      <p>n: {{n.length}}</p>
      <p>m: {{m}}</p>
      <p>n+m: {{n.length + m}}</p>
      <p>{{http}}</p>
    </div>
 let vm = new C({
    el: '#app',
    data: {n: [1, 2, 3],
      m: 2,
      http: '等待中'
    }
  });
  http.get('http://localhost:9998/', { name: 'lulu', age: '23'}).then(data => {
    vm.http = data.data;
    vm.n.length = 1
    vm.n.push('22')
  });

具体效果请在工程里面查看

end

做这个工程能让自己对 vue 对框架以及数据的操作有更深的理解, 受益匪浅.
下一集:

  1. 对指令的解析.
  2. 具体指令的处理方式.
  3. 篇幅够的话聊聊事件与生命周期

大家都可以一起交流, 共同学习, 共同进步, 早日实现自我价值!!

github:github
个人技术博客: 链接描述
更多文章,ui 库的编写文章列表 : 链接描述

正文完
 0