( 第四篇 )仿写’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对框架以及数据的操作有更深的理解, 受益匪浅.
下一集:
- 对指令的解析.
- 具体指令的处理方式.
- 篇幅够的话聊聊事件与生命周期
大家都可以一起交流, 共同学习,共同进步, 早日实现自我价值!!