1

什么是共享对象

被多次使用到的同一个对象即为共享对象

比如我们用标准的es模块来写一个导出单位转换的模块

//converter module
export default {
    cmToIn(){
        //convert logic
    }
}

当我们在其它模块中使用该模块时,converter即是共享对象,内存中只有一份,不管它被import了多少次。

同理,上面展示的是通用方法的对象集合,在前端项目里,我们也会把一些所谓写死的数据集中封装在某个模块里,方便后期的修改,比如我们实现一个constant常量模块,我们把一些项目中使用的,后期可能会修改的数据放进去

//constant
export default {
    dateFormatter:'YYYY-MM-DD',
    reports:{
        productId:'123',
        productName:'456'
    }
}

这里仅示意一下

为什么要保护共享对象

防止共享的对象被意外修改导致线上故障

原则上这些通用的模块,我们不会,也不会有意的在我们业务代码中去修改里面的数据,尤其像常量这样的模块,如果要修改的话,我们肯定修改常量这个模块。

但是,凡事总有意外,比如说我们有这样一个场景:根据后端返回的用信息,以及前端写死的一些常量,来判断某个用户能不能展示某个报表,我们期望的代码可能是这样的

import Constant from './constant';//引入我们前面定义的constant模块
//...其它略
export default View.extend({
    render(){
        //...其它逻辑略
        if(Constant.reports.productId==user.reportProductId){
            //....
        }
    }
});

注意上述代码中的if语句,如果错写成:if(Constant.reports.productId=user.reportProductId),两个等号的比较写成了一个等号的赋值。

如果自测的时候,用户接口里user.reportProductId返回的正好也是123,那么先赋值,再做if判断,成立,做为开发者会错误的以为这里的逻辑没问题。当然,正常情况下也要测试下用户接口里user.reportProductId返回不是123的情况,这时候或许就能发现问题。

如果上述问题没有测试出来,阴差阳错的上线之后,这个问题对于大型单页应用是致命的,如果某个用户的reportProductId456,访问了写错的页面后,因为被意外的修改了constant中的reports.productId,会导致后续其它模块在读取时不再是最初的123而出问题

如何保护共享对象

const

const关键字声明的仅防止变量被重新赋值,无法防止对象修改

Object.freeze

可以防止被修改,但是如果对象嵌套时,被嵌套的对象依然可以被修改,需要开发者对要freeze的对象递归遍历进行freeze。最重要的一点是,当我修改一个freeze对象时,虽然修改不成功,但也没有任务失败的提示,在前述场景中,我们还是希望开发者在修改一个不允许的被修改的对象时能及时给出相应的提示。

Proxy

es6新增的代理操作对象的方法

Proxy相关的文章非常多,这里就不再详细说,我们借助Proxy来实现一个Safeguard方法来保护我们的共享对象

const Safeguard = o => {
    let build = o => {
        let entity = new Proxy(o, {
            set() {
                throw new Error('readonly');
            },
            get(target, property) {
                let out = target[property];
                if (target.hasOwnProperty(property) &&
                    (Array.isArray(out) ||
                        Object.prototype.toString.call(out) == '[object Object]')) {
                    return build(out);
                }
                return out;
            }
        });
        return entity;
    };
    return build(o);
}

这里简化了代码,你可以根据自己的需要去调整相应的实现逻辑

使用

const user=Safeguard({
    name:'行列',
    address:{
        city:'hz'
    }
});

这个user对象只能读,不能写,当开发者尝试写入新数据时,会抛出错误提示开发者

使用场景

地址栏解析对象

在单页应用中,我们需要把地址栏中的字符串地址解析成对象,方便我们使用。

比如/path/name?a=b&c=d,我们可能解析成这样的对象

{
    path:'/path/name',
    params:{
        a:'b',
        c:'d'
    }
}

如果你统计过你的单页应用,会发现固定的用户总是只访问某些页面,我们可以在用户访问某个页面时,临时的把地址栏中的这个地址字符串解析一遍,也可以把解析结果存起来,当用户再访问这个页面时,不需要解析,把存起来的结果拿出来使用即可

关于这一块我曾经写过Magix.Cache,详细的来说明该如何智能的缓存哪些需要的信息

对于缓存后的地址栏信息对象,它就是一个共享对象,要确保它不能被开发者写入新的值,就可以使用前面我们定义的Safeguard方法来进行保护

缓存的接口数据

在单页应用开发中,有些数据需要后端提供,但是后端提供的这些数据可能在很长一段时间内都不会被修改,比如省市数据,前端没必要在每次需要使用这种数据时都请求一次,所以前端可以把该接口的数据缓存下来,来节省请求

对于这样的数据对象,也需要保护,简言之,只要是共用的对象,均需要防止它被意外的修改

关于上线

前面我们聊到的Safeguard方法,在我看来是没必要发布到线上的,只要开发阶段存在即可。只要保证在开发中没有对共享对象的写入操作,那么发布到线上时肯定也没有写入操作,这时候这个保护Safeguard方法就是多余的。

如何在开发时保护,而发布到线上时去掉呢?

我们可以使用uglify这个代码压缩工具的global_defs配置。比如在开发阶段这样定义

if (typeof DEBUG == 'undefined') window.DEBUG = true;
//...

const user={
    name:'行列',
    address:{
        city:'hz'
    }
}

if(DEBUG){
    user=Safeguard(user);
}

然后在压缩时:

uglify({
    compress: {
        global_defs: {
            DEBUG: false
        }
    },
    output: {
        ascii_only: true
    }
});

那么这样压缩出来的代码就不包含DEBUG相关的语句了

当然,Safeguard跟随上线也没有什么大问题,最后这个“关于上线”这块只是想做更深入的探讨,如果Safeguard要上到线上,注意Proxy的兼容即可

行列
888 声望183 粉丝

专注于web可视化框架的开发与应用