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