20、代理模式:原理、实现与应用

代理模式详解与实践

代理模式:原理、实现与应用

1. 封装 Web 服务的通用模式

在处理 Web 服务时,我们可以抽象出一个通用模式来封装服务。此模式虽会因 Web 服务类型不同,实现细节有所差异,但能为创建自定义代理提供通用框架。

由于同源限制,Web 服务代理必须与页面来自同一域名。以下是 WebserviceProxy 类的实现:

/* WebserviceProxy class */
var WebserviceProxy = function() {
    this.xhrHandler = XhrManager.createXhrHandler();
};
WebserviceProxy.prototype = {  
    _xhrFailure: function(statusCode) {
        throw new Error('StatsProxy: Asynchronous request for stats failed.');
    },
    _fetchData: function(url, dataCallback, getVars) {
        var that = this;
        var callback = { 
            success: function(responseText) {
                var obj = eval('(' + responseText + ')');
                dataCallback(obj);
            }, 
            failure: that._xhrFailure
        };
        var getVarArray = [];
        for(varName in getVars) {
            getVarArray.push(varName + '=' + getVars[varName]);
        }
        if(getVarArray.length > 0) {
            url = url + '?' + getVarArray.join('&');
        }
        xhrHandler.request('GET', url, callback);    
    }
};

使用该通用模式时,只需创建自定义类并继承 WebserviceProxy ,再使用 _fetchData 方法实现所需方法。以下是 StatsProxy 类作为 WebserviceProxy 子类的示例:

/* StatsProxy class. */
var StatsProxy = function() {}; // implements PageStats
extend(StatsProxy, WebserviceProxy);

/* Implement the needed methods. */
StatsProxy.prototype.getPageviews = function(callback, startDate, endDate, 
    page) {
    this._fetchData('/stats/getPageviews/', callback, { 
        'startDate': startDate, 
        'endDate': endDate, 
        'page': page 
    });
};
StatsProxy.prototype.getUniques = function(callback, startDate, endDate, 
    page) {
    this._fetchData('/stats/getUniques/', callback, { 
        'startDate': startDate, 
        'endDate': endDate, 
        'page': page 
    });
};
StatsProxy.prototype.getBrowserShare = function(callback, startDate, endDate, 
    page) {
    this._fetchData('/stats/getBrowserShare/', callback, { 
        'startDate': startDate, 
        'endDate': endDate, 
        'page': page 
    });
};
StatsProxy.prototype.getTopSearchTerms = function(callback, startDate, 
    endDate, page) {
    this._fetchData('/stats/getTopSearchTerms/', callback, { 
        'startDate': startDate, 
        'endDate': endDate, 
        'page': page 
    });
};
StatsProxy.prototype.getMostVisitedPages = function(callback, startDate, 
    endDate) {
    this._fetchData('/stats/getMostVisitedPages/', callback, { 
        'startDate': startDate, 
        'endDate': endDate 
    });
};
2. 目录查找示例

假设要在公司网站主页添加可搜索的人员目录,该目录应模拟实体目录页面,按姓氏首字母展示员工信息。由于此页面流量大,需尽可能节省带宽,避免小功能拖慢整个页面。

鉴于页面大小的重要性,决定仅为有需求的用户加载人员数据,这是使用虚拟代理的绝佳场景,它能延迟加载高带宽资源,直到用户需要时再加载。同时,还需告知用户目录正在加载,避免用户误以为网站故障。

首先,创建作为代理真实主题的类,该类负责获取人员数据并生成 HTML 展示数据:

/* Directory interface. */
var Directory = new Interface('Directory', ['showPage']);
/* PersonnelDirectory class, the Real Subject */
var PersonnelDirectory = function(parent) { // implements Directory
    this.xhrHandler = XhrManager.createXhrHandler();
    this.parent = parent;
    this.data = null;
    this.currentPage = null;
    var that = this;
    var callback = {
        success: that._configure,
        failure: function() { 
            throw new Error('PersonnelDirectory: failure in data retrieval.'); 
        }
    }
    xhrHandler.request('GET', 'directoryData.php', callback); 
};
PersonnelDirectory.prototype = {
    _configure: function(responseText) {
        this.data = eval('(' + reponseText + ')');
        ...
        this.currentPage = 'a';
    },
    showPage: function(page) {
        $('page-' + this.currentPage).style.display = 'none';
        $('page-' + page).style.display = 'block';
        this.currentPage = page;
    }
};

PersonnelDirectory 类的构造函数会发起 XHR 请求获取人员数据,数据返回后调用 _configure 方法创建 HTML 并填充数据。但该类实例化时会立即获取大量数据,若在页面加载时实例化,所有用户都需加载这些数据。因此,需要使用代理延迟实例化。

创建虚拟代理的步骤如下:
1. 创建类的大纲

/* DirectoryProxy class, just the outline. */
var DirectoryProxy = function(parent) { // implements Directory
};
DirectoryProxy.prototype = {
    showPage: function(page) {
    }
};
  1. 实现无用代理
/* DirectoryProxy class, as a useless proxy. */
var DirectoryProxy = function(parent) { // implements Directory
    this.directory = new PersonnelDirectory(parent);
};
DirectoryProxy.prototype = {
    showPage: function(page) {
        return this.directory.showPage(page);
    }
};
  1. 实现虚拟代理
/* DirectoryProxy class, as a virtual proxy. */
var DirectoryProxy = function(parent) { // implements Directory
    this.parent = parent;
    this.directory = null;
    var that = this;
    addEvent(parent, 'mouseover', that._initialize); // Initialization trigger.
};
DirectoryProxy.prototype = {
    _initialize: function() {
        this.directory = new PersonnelDirectory(this.parent);
    },
    showPage: function(page) {
        return this.directory.showPage(page);
    }
};
  1. 添加加载消息并阻塞方法调用
/* DirectoryProxy class, with loading message. */
var DirectoryProxy = function(parent) { // implements Directory
    this.parent = parent;
    this.directory = null;
    this.warning = null;
    this.interval = null;
    this.initialized = false;
    var that = this;
    addEvent(parent, 'mouseover', that._initialize); // Initialization trigger.
};
DirectoryProxy.prototype = {
    _initialize: function() {
        this.warning = document.createElement('div');
        this.parent.appendChild(this.warning);
        this.warning.innerHTML = 'The company directory is loading...';
        this.directory = new PersonnelDirectory(this.parent);
        var that = this;
        this.interval = setInterval(that._checkInitialization, 100);
    },
    _checkInitialization: function() {
        if(this.directory.currentPage != null) {
            clearInterval(this.interval);
            this.initialized = true;
            this.parent.removeChild(this.warning);
        }
    },
    showPage: function(page) {
        if(!this.initialized) {
            return;
        }
        return this.directory.showPage(page);
    }
};

以下是创建虚拟代理的流程图:

graph TD;
    A[创建类大纲] --> B[实现无用代理];
    B --> C[实现虚拟代理];
    C --> D[添加加载消息并阻塞方法调用];
3. 创建虚拟代理的通用模式

JavaScript 灵活性高,可创建动态虚拟代理。该代理会检查给定类的接口,创建对应方法,并在满足特定条件时延迟类的实例化。

首先创建抽象类 DynamicProxy 的不完整版本:

/* DynamicProxy abstract class, incomplete. */
var DynamicProxy = function() {
    this.args = arguments;
    this.initialized = false;
};
DynamicProxy.prototype = {
    _initialize: function() {
        this.subject = {}; // Instantiate the class.
        this.class.apply(this.subject, this.args);
        this.subject.__proto__ = this.class.prototype;
        var that = this;
        this.interval = setInterval(function() { that._checkInitialization(); }, 100);
    },
    _checkInitialization: function() {
        if(this._isInitialized()) {
            clearInterval(this.interval);
            this.initialized = true;
        }
    },
    _isInitialized: function() { // Must be implemented in the subclass.
        throw new Error('Unsupported operation on an abstract class.');
    }
};

然后在构造函数中添加代码,为真实主题的每个方法动态创建方法:

/* DynamicProxy abstract class, complete. */
var DynamicProxy = function() {
    this.args = arguments;
    this.initialized = false;
    if(typeof this.class != 'function') {
        throw new Error('DynamicProxy: the class attribute must be set before ' + 
            'calling the super-class constructor.');
    }
    // Create the methods needed to implement the same interface.
    for(var key in this.class.prototype) {
        // Ensure that the property is a function.
        if(typeof this.class.prototype[key] !== 'function') {
            continue;
        }
        // Add the method.
        var that = this;
        (function(methodName) {
            that[methodName] = function() {
                if(!that.initialized) {
                    return
                }
                return that.subject[methodName].apply(that.subject, arguments);
            };
        })(key);
    }
};
DynamicProxy.prototype = {
    _initialize: function() {
        this.subject = {}; // Instantiate the class.
        this.class.apply(this.subject, this.args);
        this.subject.__proto__ = this.class.prototype;
        var that = this;
        this.interval = setInterval(function() { that._checkInitialization(); }, 100);
    },
    _checkInitialization: function() {
        if(this._isInitialized()) {
            clearInterval(this.interval);
            this.initialized = true;
        }
    },
    _isInitialized: function() { // Must be implemented in the subclass.
        throw new Error('Unsupported operation on an abstract class.');
    }
};

使用该类时,需先创建子类。以下是 TestProxy 类作为 DynamicProxy 子类的示例:

/* TestProxy class. */
var TestProxy = function() {
    this.class = TestClass;
    var that = this;
    addEvent($('test-link'), 'click', function() { that._initialize(); }); 
    // Initialization trigger.
    TestProxy.superclass.constructor.apply(this, arguments);
};
extend(TestProxy, DynamicProxy);
TestProxy.prototype._isInitialized = function() {
    ... // Initialization condition goes here.
};

子类需满足以下四个要求:
1. this.class 必须设置为真实主题的类。
2. 必须创建某种初始化触发器。
3. 必须调用超类的构造函数。
4. 必须实现 _isInitialized 方法,根据真实主题是否初始化返回 true false

4. 代理模式的优缺点
类型 优点 缺点
远程代理 - 可将远程资源视为本地 JavaScript 对象,减少访问远程资源的胶水代码,提供统一接口。
- 若远程资源 API 更改,只需在一处修改代码。
- 将与资源相关的数据存储在一处。
- 掩盖复杂行为,访问速度比本地资源慢很多。
- 需要使用回调函数,增加代码复杂度。
- 与远程资源通信可能存在可靠性问题。
虚拟代理 - 提供效率,延迟高成本资源的加载。
- 可在真实主题加载时提供加载消息或虚拟 UI。
- 可避免不必要的资源加载。
- 掩盖延迟实例化的逻辑,程序员不清楚对象创建的触发条件。
- 增加项目复杂度和代码量。

代理模式有多种形式,包括保护代理、远程代理和虚拟代理。保护代理在 JavaScript 中无法实现,远程代理可封装其他语言编写的 Web 服务,虚拟代理可控制高成本类或对象的访问。使用代理模式时,需确保其能使代码更简洁、模块化或高效,否则直接访问真实主题会更简单。

代理模式:原理、实现与应用

5. 代理模式的类型及工作原理

代理模式主要有以下几种类型,下面详细介绍它们的工作原理:
- 保护代理 :用于控制哪些客户端可以访问真实主题的方法。不过在 JavaScript 中,由于语言特性的限制,无法实现保护代理,所以在实际应用中较少涉及。
- 远程代理 :它的核心作用是控制对远程资源的访问。在像 Java 这样的语言中,远程代理可以直接连接到持久的 Java 虚拟机,并传递方法调用。但在客户端 JavaScript 里,虽然无法实现这种方式,但远程代理在封装其他语言编写的 Web 服务时非常有用。它能让开发者将远程资源当作本地对象来处理,极大地简化了对远程资源的访问。例如,当需要访问多个不同的 Web 服务时,可以创建一个抽象的通用远程代理类,然后为每个需要访问的 Web 服务创建子类。
- 虚拟代理 :主要用于控制对创建或维护成本较高的类或对象的访问。在 JavaScript 中,由于终端用户的浏览器内存有限,虚拟代理能很好地解决资源加载的问题。当真实主题加载缓慢时,虚拟代理可以提供加载消息或虚拟用户界面(UI),让用户在真实 UI 加载完成前进行交互。

6. 代理模式的实际应用场景

代理模式在不同的场景下有广泛的应用,以下是一些常见的例子:
- 远程代理的应用 :在开发跨域的 Web 应用时,需要访问其他服务器上的资源。此时可以使用远程代理来封装对这些远程资源的访问,将复杂的跨域请求和数据处理逻辑隐藏在代理中,让开发者可以像操作本地对象一样操作远程资源。例如,一个电商网站需要从不同的供应商服务器获取商品信息,就可以使用远程代理来统一管理这些请求。
- 虚拟代理的应用 :在网页加载大量图片或数据时,使用虚拟代理可以延迟这些资源的加载,直到用户需要查看时再进行加载。比如,一个图片库网站,用户在浏览图片列表时,只加载图片的缩略图,当用户点击某张图片时,再使用虚拟代理加载高清大图,这样可以提高页面的加载速度,减少不必要的带宽消耗。

7. 代理模式的使用建议

在使用代理模式时,需要根据具体的需求和场景来选择合适的代理类型,并注意以下几点:
- 明确需求 :在决定使用代理模式之前,要仔细评估是否真的需要代理提供的功能。如果直接访问真实主题就能满足需求,那么使用代理只会增加项目的复杂度和代码量。
- 合理选择代理类型 :根据资源的性质和访问特点,选择合适的代理类型。例如,如果是访问远程资源,就选择远程代理;如果是处理高成本资源的加载,就选择虚拟代理。
- 做好文档记录 :由于代理模式会掩盖一些复杂的行为,为了让其他开发者能够理解和使用代理,需要做好详细的文档记录,说明代理的功能、使用方法和注意事项。

8. 总结

代理模式是一种非常有用的设计模式,它通过不同的代理类型控制对资源的访问。虽然代理模式有很多优点,如简化远程资源访问、提高资源加载效率等,但也存在一些缺点,如增加代码复杂度、掩盖复杂行为等。在实际应用中,要根据具体情况合理使用代理模式,确保它能使代码更加简洁、模块化和高效。通过合理运用代理模式,可以更轻松地访问原本难以处理的资源,提升项目的性能和用户体验。

以下是代理模式类型及特点的总结表格:
| 代理类型 | 能否在 JavaScript 实现 | 作用 | 优点 | 缺点 |
| ---- | ---- | ---- | ---- | ---- |
| 保护代理 | 否 | 控制客户端对真实主题方法的访问 | 无 | 无 |
| 远程代理 | 是 | 封装远程 Web 服务,将远程资源视为本地对象 | - 减少胶水代码,提供统一接口
- API 更改时只需一处修改代码
- 集中存储资源相关数据 | - 访问速度慢
- 使用回调增加复杂度
- 通信有可靠性问题 |
| 虚拟代理 | 是 | 控制高成本类或对象的访问 | - 延迟高成本资源加载
- 提供加载消息或虚拟 UI
- 避免不必要的资源加载 | - 掩盖延迟实例化逻辑
- 增加项目复杂度和代码量 |

以下是代理模式使用流程的 mermaid 流程图:

graph LR;
    A[明确需求] --> B{是否需要代理};
    B -- 是 --> C{选择代理类型};
    C -- 远程代理 --> D[创建远程代理类];
    C -- 虚拟代理 --> E[创建虚拟代理类];
    B -- 否 --> F[直接访问真实主题];
    D --> G[使用远程代理访问资源];
    E --> H[使用虚拟代理控制资源加载];

通过以上内容,我们对代理模式有了更深入的了解,希望能帮助你在实际开发中更好地运用代理模式。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值