代理模式: 编程处处有代理

495 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第18天,点击查看活动详情

什么是代理

为其他对象提供一种代理以控制对这个对象的访问。

为什么需要代理呢?因为直接访问对象时会带来很多的问题,比如说:要访问的对象在远程的机器上。在面向对象系统中,有些对象由于某些原因(比如对象创建开销很大,或者某些操作需要安全控制,或者需要进程外的访问),直接访问会给使用者或者系统结构带来很多麻烦,我们可以在访问此对象时加上一个对此对象的访问层。

image.png

例如,你通过房产中介买房子,中介就是一个代理。你接触到的是中介这个代理,而非真正的房主。

再例如,明星都有经纪人,某活动想请明星演出,需要对接经纪人。艺术家不方便谈钱,但可以和经纪人谈。经纪人就是一个代理。

代码演示

class RealImg {
  fileName: string
  constructor(fileName: string) {
    this.fileName = fileName
  }
  private loadFromDist() {
    console.log('load from dist', this.fileName)
  }
  display() {
    this.loadFromDist()
    console.log('display', this.fileName)
  }
}

class ProxyImg {
  realImg: RealImg
  constructor(fileName: string) {
    this.realImg = new RealImg(fileName)
  }
  display() {
    // 处理一些逻辑
    this.realImg.display()
  }
}

image.png

代理模式和装饰器模式最大的区别是代理模式可以改变原始对象的行为,而装饰器模式不能改变。在上面的代码中,我可以特点条件下选择不显示图片,但是装饰器模式必须要显示。

应用场景

DOM 事件代理

<div id="div1">
    <a href="#">a1</a>
    <a href="#">a2</a>
    <a href="#">a3</a>
    <a href="#">a4</a>
</div>
<button>点击增加一个 a 标签</button>

<script>
    var div1 = document.getElementById('div1')
    div1.addEventListener('click', function (e) {
        var target = e.target
        if (e.nodeName === 'A') {
            alert(target.innerHTML)
        }
    })
</script>

webpack devServer

  • 配置 webpack
// webpack.config.js
module.exports = {
  // 其他配置...
  devServer: {
    proxy: {
      '/api': 'http://localhost:8081',
    },
  },
};
  • 启动 nodejs 服务,监听 8081 端口
  • 借用 axios 发送请求
import axios from 'axios'

document.getElementById('btn1')?.addEventListener('click', () => {
    axios.get('/api/info')
        .then(res => {
            console.log(res)
        })
})

nginx 反向代理

server {
    listen   8000;
    location / {
        proxy_pass http://localhost:8001;
    }
    location /api/ {
        proxy_pass http://localhost:8002;
        proxy_set_header Host $host;
    }
}

正向代理与反向代理

webpack devServer 属于正向代理,代理的配置都在客户端,比如我们配置了devServer等。nginx 属于反向代理,代理的配置都在服务端,客户端不需要进行任何配置。

Proxy

Proxy基本语法

// 明星
const star = {
    name: '张三',
    age: 25,
    2',phone: '1861111222
    price: 0 // 艺术物价,明星不谈钱
}

// 经纪人
const agent = new Proxy(star, {
    get(target, key) {
        if (key === 'phone') {
            return '13900001111' // 返回经纪人的的电话
        }
        if (key === 'price') {
            return 100 * 1000  // 报价
        }
        return Reflect.get(target, key) // 返回原来的属性值
    },
    set(target, key, val): boolean {
        if (key === 'price') {
            if (val < 100 * 1000) {
                throw new Error('价格太低了...')
            } else {
                console.log('报价成功,合作愉快!', val)
                return Reflect.set(target, key, val)
            }
        }
        // 其他属性不可设置
        return false
    }
})
  • Reflect 字面意思是反射的意思,把 target 的信息反射出去。
  • 当设置的时候,返回一个布尔值。return false表示不可设置。

Proxy 的使用场景

跟踪属性访问

Vue3 就是通过这个特性实现数据响应式。

const user = {
    name: '张三'
}
const proxy = new Proxy(user, {
    get(target, key) {
        console.log('get...')
        return Reflect.get(target, key)
    },
    // get(...args) {
    //     return Reflect.get(...args)
    // },
    set(target, key, val) {
        console.log('set...', val)
        return Reflect.set(target, key, val)
    }
})

proxy.name = '李四'
console.log(proxy.name)

get 里面的参数可以一股脑的传入到 Reflect中,因此可以这么写:

get(...args) {
   return Reflect.get(...args)
}

隐藏属性

const hiddenProps = ['girlfriend'] // 要隐藏的属性 key
const user = {
    name: '张三',
    age: 25,
    girlfriend: '小红'
}
const proxy = new Proxy(user, {
    get(target, key) {
        if (hiddenProps.includes(key as string)) return undefined
        return Reflect.get(target, key)
    },
    has(target, key) {
        if (hiddenProps.includes(key as string)) return false
        return Reflect.has(target, key)
    },
    set(target, key, val) {
        if (hiddenProps.includes(key as string)) return false
        console.log('set...', val)
        return Reflect.set(target, key, val)
    }
})

console.log('age', proxy.age)
console.log('girlfriend', proxy.girlfriend) // undefined

验证属性

如果用 TS ,会有静态类型检查,用不到这个验证。用 JS 的话会有效果。

const user = {
    name: '张三',
    age: 25,
}
const proxy = new Proxy(user, {
    get(target, key) {
        return Reflect.get(target, key)
    },
    set(target, key, val) {
        if (key === 'age') {
            if (typeof val !== 'number') return false // 验证 age 类型
        }
        return Reflect.set(target, key, val)
    }
})

proxy.age = 'a'
console.log(proxy.age) // 25

记录实例

const userList = new WeakSet() // 每次初始化 user ,都记录到这里

class User {
    name: string
    constructor(name: string) {
        this.name = name
    }
}
const user1 = new User()
userList.add(user1)
const user2 = new User()
userList.add(user2)

每次初始化一个实例的时候,需要记录到userList,因此每次都要执行userList.add(user1),这样比较麻烦,可以使用代理来简化。

const ProxyUser = new Proxy(User, {
    construct(...args) {
        const user = Reflect.construct(...args)
        userList.add(user) // 记录 user 对象
        return user
    }
})

const user1 = new ProxyUser('张三')
const user2 = new ProxyUser('李四')
console.log('userList', userList)

Proxy 不仅可以代理对象,还能代理类。上面的代码就是代理了类上面的construct函数,写法如上。这样当实例化的时候,就会自动的记录实例。

Proxy 的注意事项

捕获器不变式

这是"红宝书"里的叫法。捕获器即 get ,不变式即不能因为 Proxy 而改变对象本身的描述符特性。

const obj = { x: 100, y: 0 }
Object.defineProperty(obj, 'y', {
    value: 200,
    writable: false,
    configurable: false,
})
// proxy 不能改变对象本身的描述符特性
const proxy = new Proxy(obj, {
    get() {
        return 'abc'
    }
})

console.log(proxy.x)
console.log(proxy.y) // y 属性描述符被修改,proxy 不能修改它的值