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

( 第四篇 )仿寫'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庫的編寫文章列表 :鏈接描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章