( 第四篇 )仿寫'Vue生態'系列___"Proxy雙向綁定與封裝請求"

( 第四篇 )仿寫'Vue生態'系列___"Proxy雙向綁定與封裝請求"

本次任務 前端

  1. vue3.0使用了Proxy進行數據的劫持, 那固然就有必要研究並實戰一下這方面知識了.
  2. 對Reflect進行解讀, 並將Object的操做少部分改成Reflect的形式.
  3. 異步不能總用定時器模擬, 本次本身封裝一個簡易的'axios'.
  4. 有了請求固然須要服務器, 用koa啓動一個簡易的服務.

一. Proxy

vue3.0選擇了這個屬性, 雖然也會提供兼容版本, 但基本也算是跟老版ie說再見了, Proxy會解決以前沒法監聽數組的修改這個痛點, 也算是我輩前端的福音了.
使用方面會有很大不一樣, defineProperty是監控一個對象, 而Proxy是返回一個新對象, 這就須要我徹底重寫Observer模塊了, 話很少說先把基本功能演示一下.
由下面的代碼可知:vue

  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指向我選擇在這裏作, 爲了保持主函數的純淨.ios

// 數據劫持
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組成.nginx

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;
代理基本類型還會報錯😯;git

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身上.github

二. Reflect

這個屬性也蠻有趣的, 它的出現很符合設計模式, 數據就是要有一套專用的處理方法, 並且函數式處理更符合js的設計理念.web

  1. 靜態方法 Reflect.defineProperty() 基本等同於 Object.defineProperty() 方法,惟一不一樣是返回 Boolean 值, 這樣就不用擔憂defineProperty時的報錯了.
  2. Reflect對象的方法與Proxy對象的方法一一對應,只要是Proxy對象的方法,就能在Reflect對象上找到對應的方法。

下面把經常使用的方法演示一下
操做成功或失敗會返回布爾值ajax

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/httpnpm

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庫的編寫文章列表 :連接描述

相關文章
相關標籤/搜索