( 第四篇 )仿寫'Vue生態'系列___"Proxy雙向綁定與封裝請求"
本次任務
- vue3.0使用了Proxy進行數據的劫持, 那當然就有必要研究並實戰一下這方面知識了.
- 對Reflect進行解讀, 並將Object的操作少部分改爲Reflect的形式.
- 異步不能總用定時器模擬, 本次自己封裝一個簡易的'axios'.
- 有了請求當然需要服務器, 用koa啓動一個簡易的服務.
一. Proxy
vue3.0選擇了這個屬性, 雖然也會提供兼容版本, 但基本也算是跟老版ie說再見了, Proxy會解決之前無法監聽數組的修改這個痛點, 也算是我輩前端的福音了.
使用方面會有很大不同, defineProperty是監控一個對象, 而Proxy是返回一個新對象, 這就需要我完全重寫Observer模塊了, 話不多說先把基本功能演示一下.
由下面的代碼可知:
- Proxy可以代理數組.
- 代理並不會改變原數據的類型, Array還是Array.
- 修改length屬性會觸發set, 瀏覽器認爲length當然是屬性, 修改他當然要觸發set.
- 像是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的設計理念.
- 靜態方法 Reflect.defineProperty() 基本等同於 Object.defineProperty() 方法,唯一不同是返回 Boolean 值, 這樣就不用擔心defineProperty時的報錯了.
- 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;
}
}
編寫插件的時候, 先要考慮用戶會怎麼用它
- 用戶指定請求的方法, 本次只做post與get.
- 可以配置請求地址.
- 可以傳參, 當然post與get處理參數肯定不一樣.
- 返回值我們用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;
}
}
邊邊角角的小功能
- 設置超出時間與超出時的回調.
- 請求的取消
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());
既然說到這裏就, 那就總結一下吧
跨域的幾種方式
- jsonp 這個太傳統了, 製作一個script標籤發送請求.
- cors 也就是服務端設置允許什麼來源的請求, 什麼方法的請求等等,纔可以跨域.
- postMessage 兩個頁面之間傳值, 經常出現在一個頁面負責登錄, 另一個頁面獲取用戶的登錄token.
- document.domain 相同的domain可以互相拿數據.
- window.name 這個沒人用, 但是挺好玩, 有三個頁面 a,b,c, a與b 同源, c單獨一個源, a用iframe打開c頁面, c把要傳的值放在 window.name上,監聽加載成功事件, 瞬間改變 iframe 的地址, 爲b, 此時 b 同源, window 會被繼承過來, 偷樑換柱, 利用了換地址 window不變的特點;
- 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', 操控父級;
- http-proxy 就比如說vue的代理請求, 畢竟服務器之間不存在跨域.
- nginx 配置一下就好了, 比前端做好多了
- 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對框架以及數據的操作有更深的理解, 受益匪淺.
下一集:
- 對指令的解析.
- 具體指令的處理方式.
- 篇幅夠的話聊聊事件與生命週期
大家都可以一起交流, 共同學習,共同進步, 早日實現自我價值!!