整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

Proxy 来代理 JavaScript 里的类

Proxy 来代理 JavaScript 里的类

roxy 对象(Proxy)是 ES6 的一个非常酷却鲜为人知的特性。虽然这个特性存在已久,但是我还是想在本文中对其稍作解释,并用一个例子说明一下它的用法。

什么是 Proxy

正如 MDN 上简单而枯燥的定义:

Proxy 对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。

虽然这是一个不错的总结,但是我却并没有从中搞清楚 Proxy 能做什么,以及它能帮我们实现什么。

首先,Proxy 的概念来源于元编程。简单的说,元编程是允许我们运行我们编写的应用程序(或核心)代码的代码。例如,臭名昭著的 eval 函数允许我们将字符串代码当做可执行代码来执行,它是就属于元编程领域。

Proxy API 允许我们在对象和其消费实体中创建中间层,这种特性为我们提供了控制该对象的能力,比如可以决定怎样去进行它的 get 和 set,甚至可以自定义当访问这个对象上不存在的属性的时候我们可以做些什么。

Proxy 的 API

var p=newProxy(target, handler);

Proxy 构造函数获取一个 target 对象,和一个用来拦截 target 对象不同行为的 handler 对象。你可以设置下面这些拦截项:

  • has —?拦截 in 操作。比如,你可以用它来隐藏对象上某些属性。
  • get —?用来拦截读取操作。比如当试图读取不存在的属性时,你可以用它来返回默认值。
  • set — 用来拦截赋值操作。比如给属性赋值的时候你可以增加验证的逻辑,如果验证不通过可以抛出错误。
  • apply — 用来拦截函数调用操作。比如,你可以把所有的函数调用都包裹在 try/catch 语句块中。

这只是一部分拦截项,你可以在 MDN 上找到完整的列表。

下面是将 Proxy 用在验证上的一个简单的例子:

constCar={ maker: 'BMW', year: 2018,};
const proxyCar=newProxy(Car, {set(obj, prop, value) {if(prop==='maker'&& value.length < 1) {thrownewError('Invalid maker');}
if(prop==='year'&& typeof value !=='number') {thrownewError('Invalid year');} obj[prop]=value;returntrue;}
});
proxyCar.maker=''; // throw exceptionproxyCar.year='1999'; // throw exception

可以看到,我们可以用 Proxy 来验证赋给被代理对象的值。

使用 Proxy 来调试

为了在实践中展示 Proxy 的能力,我创建了一个简单的监测库,用来监测给定的对象或类,监测项如下:

  • 函数执行时间
  • 函数的调用者或属性的访问者
  • 统计每个函数或属性的被访问次数。

这是通过在访问任意对象、类、甚至是函数时,调用一个名为 proxyTrack 的函数来完成的。

如果你希望监测是谁给一个对象的属性赋的值,或者一个函数执行了多久、执行了多少次、谁执行的,这个库将非常有用。我知道可能还有其他更好的工具来实现上面的功能,但是在这里我创建这个库就是为了用一用这个 API。

使用 proxyTrack

首先,我们看看怎么用:

functionMyClass() {}
MyClass.prototype={ isPrime: function() {const num=this.num;for(var i=2; i < num; i++)if(num % i===0) returnfalse;return num !==1&& num !==0;},
 num: null,};
MyClass.prototype.constructor=MyClass;
const trackedClass=proxyTrack(MyClass);
function start() {const my=new trackedClass(); my.num=573723653;if(!my.isPrime()) {return`${my.num} is not prime`;}}
function main() { start();}
main();

如果我们运行这段代码,控制台将会输出:

MyClass.num is being set by start for the 1 timeMyClass.num is being get by isPrime for the 1 timeMyClass.isPrime was called by start for the 1 time and took 0 mils.MyClass.num is being get by start for the 2 time

proxyTrack 接受 2 个参数:第一个是要监测的对象/类,第二个是一个配置项对象,如果没传递的话将被置为默认值。我们看看这个配置项默认值长啥样:

const defaultOptions={ trackFunctions: true, trackProps: true, trackTime: true, trackCaller: true, trackCount: true, stdout: null, filter: null,};

可以看到,你可以通过配置你关心的监测项来监测你的目标。比如你希望将结果输出出来,那么你可以将 console.log 赋给 stdout。

还可以通过赋给 filter 的回调函数来自定义地控制输出哪些信息。你将会得到一个包括有监测信息的对象,并且如果你希望保留这个信息就返回 true,反之返回 false。

在 React 中使用 proxyTrack

因为 React 的组件实际上也是类,所以你可以通过 proxyTrack 来实时监控它。比如:

classMyComponent extends Component{...}
exportdefault connect(mapStateToProps)(proxyTrack(MyComponent, { trackFunctions: true, trackProps: true, trackTime: true, trackCaller: true, trackCount: true, filter: (data)=> {if( data.type==='get'&& data.prop==='componentDidUpdate') returnfalse;returntrue;}}));

可以看到,你可以将你不关心的信息过滤掉,否则输出将会变得杂乱无章。

实现 proxyTrack

我们来看看 proxyTrack 的实现。

首先是这个函数本身:

exportfunction proxyTrack(entity, options=defaultOptions) {if(typeof entity==='function') return trackClass(entity, options);return trackObject(entity, options);}

没什么特别的嘛,这里只是调用相关函数。

再看看 trackObject:

function trackObject(obj, options={}) {const{ trackFunctions, trackProps }=options;
 let resultObj=obj;if(trackFunctions) { proxyFunctions(resultObj, options);}if(trackProps) { resultObj=newProxy(resultObj, {get: trackPropertyGet(options),set: trackPropertySet(options),});}return resultObj;}function proxyFunctions(trackedEntity, options) {if(typeof trackedEntity==='function') return;Object.getOwnPropertyNames(trackedEntity).forEach((name)=> {if(typeof trackedEntity[name]==='function') { trackedEntity[name]=newProxy(trackedEntity[name], { apply: trackFunctionCall(options),});}});}

可以看到,假如我们希望监测对象的属性,我们创建了一个带有 get 和 set 拦截器的被监测对象。下面是 set 拦截器的实现:

function trackPropertySet(options={}) {returnfunctionset(target, prop, value, receiver) {const{ trackCaller, trackCount, stdout, filter }=options;const error=trackCaller && newError();const caller=getCaller(error);const contextName=target.constructor.name==='Object'? '': `${target.constructor.name}.`;const name=`${contextName}${prop}`;const hashKey=`set_${name}`;if(trackCount) {if(!callerMap[hashKey]) { callerMap[hashKey]=1;} else{ callerMap[hashKey]++;}} let output=`${name} is being set`;if(trackCaller) { output +=` by ${caller.name}`;}if(trackCount) { output +=` for the ${callerMap[hashKey]} time`;} let canReport=true;if(filter) { canReport=filter({ type: 'get', prop, name, caller, count: callerMap[hashKey], value,});}if(canReport) {if(stdout) { stdout(output);} else{ console.log(output);}}returnReflect.set(target, prop, value, receiver);};}

更有趣的是 trackClass 函数(至少对我来说是这样):

function trackClass(cls, options={}) { cls.prototype=trackObject(cls.prototype, options); cls.prototype.constructor=cls;
returnnewProxy(cls, { construct(target, args) {const obj=new target(...args);returnnewProxy(obj, {get: trackPropertyGet(options),set: trackPropertySet(options),});}, apply: trackFunctionCall(options),});}

在这个案例中,因为我们希望拦截这个类上不属于原型上的属性,所以我们给这个类的原型创建了个代理,并且创建了个构造函数拦截器。

别忘了,即使你在原型上定义了一个属性,但如果你再给这个对象赋值一个同名属性,JavaScript 将会创建一个这个属性的本地副本,所以赋值的改动并不会改变这个类其他实例的行为。这就是为何只对原型做代理并不能满足要求的原因。

程内容

1.1、函数的概念

函数是由事件驱动的或者当它被调用时执行的可重复使用的代码块。

函数过程中的这些语句用于完成某些有意义的工作——通常是处理文本,控制输入或计算数值。

通过在程序代码中引入函数名称和所需的参数,可在该程序中执行(或称调用)该函数。

1.2、函数详解

函数是由关键字 function、函数名、一组参数,以及置于括号中的待执行代码所组成的。

当调用该函数时,会创建一个临时空间(闭包),并且执行函数内的代码。

可以在某事件发生时直接调用函数(比如当用户点击按钮时),并且可由 JavaScript 在任何位置进行调用。

调用带参数的函数

在调用函数时,您可以向其传递值,这些值被称为参数。

这些参数可以在函数中使用。

您可以发送任意多的参数,由逗号 (,) 分隔。

当您声明函数时,请把参数作为变量来声明。

变量和参数必须以一致的顺序出现。第一个变量就是第一个被传递的参数的给定的值,以此类推。

带有返回值的函数

有时,我们会希望函数将值返回调用它的地方。

通过使用 return 语句就可以实现。

在使用 return 语句时,函数会停止执行,并返回指定的值。

整个 JavaScript 并不会停止执行,仅仅是函数。JavaScript 将继续执行代码,从调用函数的地方。

js函数调用的参数就是 this 和 arguments 。arguments是参数组,他并不是一个真实的数组,但是可以使用.length方法获得长度。

函数有四种调用模式

一、函数调用形式

所谓函数形式就是一般声明函数后直接调用

二、方法调用模式

同样的是函数,将其赋值给一个对象的成员以后就不一样了. 将函数赋值给对象的成员后,那么这个就不在称为函数,而应该叫做方法.

三、构造器调用模式

同样是函数,在单纯的函数模式下,this 表示 window;在对象方法模式下,this 指 的是当前对象. 除了这两种情况,JavaScript 中函数还可以是构造器. 将函数作为构造器来使用的语法就是在函数调用前面加上一个 new 关键字. (关于this关键字下面即将讲到)

四、apply调用模式

除了上述三种调用模式以外,函数作为对象还有 apply 方法与 call 方法可以使用, apply 模式既可以像函数一样使用,也可以像方法一样使用,是一个灵活的使用方法。

JavaScript 对大小写敏感。关键词 function 必须是小写的,并且必须以与函数名称相同的大小写来调用函数。

传值和传引用(传地址)

1.3、闭包的概念

闭包就是能够读取其他函数内部变量的函数。

在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"。

在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

1.4、基于函数执行的内存解析(闭包使用)

由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

闭包是通过函数实现的一块动态的、临时的内存空间,在函数调用时动态产生,调用完即销毁,在这个过程中里面定义的变量和函数都是临时的(局部的)

1.5、对象的概念

万事万物皆对象,每一个对象可以有自己的属性、方法、标识,这意味着每一个对象都可以拥有自己的内部数据(这些数据通过对象的属性来表示)和行为(行为通过对象的方法来体现),内存中的每一个对象都可以通过一个标识(这个标识指的就是对象在内存中的地址)唯一的和其他对象区分开来.

1.6、对象所在内存区域

对象所在的内存区域就是堆内存

1.7、javascript中对象的语法

创建 JavaScript 对象

通过 JavaScript,您能够定义并创建自己的对象。

创建新对象有两种不同的方法:

1.定义并创建对象的实例

2.使用函数来定义对象,然后创建新的对象实例

javascript操作对象的特色写法

var obj=new Object(); 以及 var obj={}; 这两种创建对象的方式结果是一样的

而通过后者var obj={name:”aaa”,say:function(){}};可以实现对对象属性和功能的初始化定义

访问对象的属性

属性是与对象相关的值。

访问对象属性的语法是:

objectName.propertyName

访问对象的方法

方法是能够在对象上执行的动作。

您可以通过以下语法来调用方法:

objectName.methodName()

1.8、封装的概念

生活中的封装是指:

将某些东西包装在一起,然后以新的完整形式呈现出来;

程序中的封装是指:

隐藏对象的属性和实现细节,仅对外提供公共的访问方式

封装其实就是有选择性地公开或隐藏某些信息,它解决了数据的安全性问题,便于维护和扩展。

在javascript中通过构造器进行封装的实现

1.9、Function构造器函数和new关键字

JavaScript中function定义函数有两种作用:

1. 作为普通的函数声明使用。

之前我们所有针对函数的讲解都是这种使用方式

2. 作为类构造器函数声明使用。

构造器函数结合new关键字来调用,返回对象的内存地址,之后可以通过操作这个对象(内存地址)的属性和方法来实现业务逻辑

1.10、this关键字

this代表的是当前内存空间的引用。

通过this显示的去使用当前对象的成员(属性、方法)

1.11、prototype原型

Prototype原型模式是一种创建型设计模式,Prototype模式允许一个对象再创建另外一个可定制的对象,根本无需知道任何如何创建的细节

工作原理是:通过将一个原型对象传给那个要发动创建的对象,这个要发动创建的对象通过请求原型对象拷贝它们自己来实施创建。

1.12、for循环迭代对象属性

1.14、typeof关键字的使用

JS中的变量是松散类型(即弱类型)的,可以用来保存任何类型的数据。

typeof 可以用来检测给定变量的数据类型。

1.15、instanceof关键字的使用

instanceof关键字用于判断一个引用类型变量所指向的对象是否是一个类(或接口、抽象类、父类)的实例。

1.16、普通函数和构造器函数的区别

共同点:

只要是函数,都需要被调用,只要被调用都会创建闭包。

异同点:

普通函数调用直接在函数名字后面加上一对括号;构造器函数使用new关键字来调用。

普通函数调用关注的是执行函数的过程和返回结果;构造器函数调用关注的返回的内存空间(闭包)。.

1.17、作用域

作用域是指对某一变量和方法具有访问权限的代码空间

在Javascript中, 作用域是在函数中维护的。

1.18、继承的概念

继承是指一个对象直接使用另一对象的属性和方法。

继承可以使得子类具有父类的各种属性和方法,而不需要再次编写相同的代码。

在令子类继承父类的同时,可以重新定义某些属性,并重写某些方法,即覆盖父类的原有属性和方法,使其获得与父类不同的功能。

喜欢请关注!点赞 评论!

今日微语:

坐在秋天的旷野里,把一地凄清,守候成满山淡黄的野菊。生命若歌,起伏跌宕,声起声落,我们每个人都是歌者;浮华尘世,生命如茶,或浓或淡,或苦或甜,需要我们用心去品尝。记住该记住的,忘记该忘记的,改变能改变的,接受不能接受的,也许,我们无法把握未来,但我们起码可以左右现在,不是吗?喜欢与你默默对视,喜欢在心的旷野里,与你缠绵相依。时光荏苒,无关风月,只是,愿意在你的注视下,轻执墨痕,为你写一段文字。一缕清风,一朵小花,一个微笑,一句轻声的问候,就够了。爱,无需刻意的装饰。



在前面

今年国庆假期终于可以憋在家里了不用出门了,不用出去看后脑了,真的是一种享受。这么好的光阴怎么浪费,睡觉、吃饭、打豆豆这怎么可能(耍多了也烦),完全不符合我们程序员的作风,赶紧起来把文章写完。

这篇文章比较基础,在国庆期间的业余时间写的,这几天又完善了下,力求把更多的前端所涉及到的关于文件上传的各种场景和应用都涵盖了,若有疏漏和问题还请留言斧正和补充。

自测读不读

以下是本文所涉及到的知识点,break or continue ?

  • 文件上传原理
  • 最原始的文件上传
  • 使用 koa2 作为服务端写一个文件上传接口
  • 单文件上传和上传进度
  • 多文件上传和上传进度
  • 拖拽上传
  • 剪贴板上传
  • 大文件上传之分片上传
  • 大文件上传之断点续传
  • node 端文件上传

原理概述

原理很简单,就是根据 http 协议的规范和定义,完成请求消息体的封装和消息体的解析,然后将二进制内容保存到文件。

我们都知道如果要上传一个文件,需要把 form 标签的enctype设置为multipart/form-data,同时method必须为post方法。

那么multipart/form-data表示什么呢?

multipart互联网上的混合资源,就是资源由多种元素组成,form-data表示可以使用HTML Forms 和 POST 方法上传文件,具体的定义可以参考RFC 7578。

multipart/form-data 结构

看下 http 请求的消息体



  • 请求头:

Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryDCntfiXcSkPhS4PN 表示本次请求要上传文件,其中boundary表示分隔符,如果要上传多个表单项,就要使用boundary分割,每个表单项由———XXX开始,以———XXX结尾。

  • 消息体- Form Data 部分

每一个表单项又由Content-Type和Content-Disposition组成。

Content-Disposition: form-data 为固定值,表示一个表单元素,name 表示表单元素的 名称,回车换行后面就是name的值,如果是上传文件就是文件的二进制内容。

Content-Type:表示当前的内容的 MIME 类型,是图片还是文本还是二进制数据。

解析

客户端发送请求到服务器后,服务器会收到请求的消息体,然后对消息体进行解析,解析出哪是普通表单哪些是附件。

可能大家马上能想到通过正则或者字符串处理分割出内容,不过这样是行不通的,二进制buffer转化为string,对字符串进行截取后,其索引和字符串是不一致的,所以结果就不会正确,除非上传的就是字符串。

不过一般情况下不需要自行解析,目前已经有很成熟的三方库可以使用。

至于如何解析,这个也会占用很大篇幅,后面的文章在详细说。

最原始的文件上传

使用 form 表单上传文件

在 ie时代,如果实现一个无刷新的文件上传那可是费老劲了,大部分都是用 iframe 来实现局部刷新或者使用 flash 插件来搞定,在那个时代 ie 就是最好用的浏览器(别无选择)。

DEMO



这种方式上传文件,不需要 js ,而且没有兼容问题,所有浏览器都支持,就是体验很差,导致页面刷新,页面其他数据丢失。

HTML

 <form method="post" action="http://localhost:8100" enctype="multipart/form-data">

        选择文件:
            <input type="file" name="f1"/> input 必须设置 name 属性,否则数据无法发送<br/>
<br/>
            标题:<input type="text" name="title"/><br/><br/><br/>

        <button type="submit" id="btn-0">上 传</button>

</form>

复制代码

文件上传接口

服务端文件的保存基于现有的库koa-body结合 koa2实现服务端文件的保存和数据的返回。

在项目开发中,文件上传本身和业务无关,代码基本上都可通用。

在这里我们使用koa-body库来实现解析和文件的保存。

koa-body 会自动保存文件到系统临时目录下,也可以指定保存的文件路径。



然后在后续中间件内得到已保存的文件的信息,再做二次处理。

  • ctx.request.files.f1 得到文件信息,f1为input file 标签的 name
  • 获得文件的扩展名,重命名文件

NODE

/**
 * 服务入口
 */
var http=require('http');
var koaStatic=require('koa-static');
var path=require('path');
var koaBody=require('koa-body');//文件保存库
var fs=require('fs');
var Koa=require('koa2');

var app=new Koa();
var port=process.env.PORT || '8100';

var uploadHost=`http://localhost:${port}/uploads/`;

app.use(koaBody({
    formidable: {
        //设置文件的默认保存目录,不设置则保存在系统临时目录下  os
        uploadDir: path.resolve(__dirname, '../static/uploads')
    },
    multipart: true // 开启文件上传,默认是关闭
}));

//开启静态文件访问
app.use(koaStatic(
    path.resolve(__dirname, '../static') 
));

//文件二次处理,修改名称
app.use((ctx)=> {
    var file=ctx.request.files.f1;//得道文件对象
    var path=file.path;
    var fname=file.name;//原文件名称
    var nextPath=path+fname;
    if(file.size>0 && path){
        //得到扩展名
        var extArr=fname.split('.');
        var ext=extArr[extArr.length-1];
        var nextPath=path+'.'+ext;
        //重命名文件
        fs.renameSync(path, nextPath);
    }
    //以 json 形式输出上传文件地址
    ctx.body=`{
        "fileUrl":"${uploadHost}${nextPath.slice(nextPath.lastIndexOf('/')+1)}"
    }`;
});

/**
 * http server
 */
var server=http.createServer(app.callback());
server.listen(port);
console.log('demo1 server start ......   ');
复制代码

CODE

https://github.com/Bigerfe/fe-learn-code/