15

( 第四篇 )仿写'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库的编写文章列表 :链接描述


lulu_up
5.7k 声望6.9k 粉丝

自信自律, 终身学习, 创业者