什么是共享对象
被多次使用到的同一个对象即为共享对象
比如我们用标准的es
模块来写一个导出单位转换的模块
//converter moduleexport default { cmToIn(){ //convert logic }}
当我们在其它模块中使用该模块时,converter
即是共享对象,内存中只有一份,不管它被import
了多少次。
同理,上面展示的是通用方法的对象集合,在前端项目里,我们也会把一些所谓写死
的数据集中封装在某个模块里,方便后期的修改,比如我们实现一个constant
常量模块,我们把一些项目中使用的,后期可能会修改的数据放进去
//constantexport 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
的情况,这时候或许就能发现问题。
如果上述问题没有测试出来,阴差阳错的上线之后,这个问题对于大型单页应用是致命的,如果某个用户的reportProductId
是456
,访问了写错的页面后,因为被意外的修改了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' }}
如果你统计过你的单页应用,会发现固定的用户总是只访问某些页面,我们可以在用户访问某个页面时,临时的把地址栏中的这个地址字符串解析一遍,也可以把解析结果存起来,当用户再访问这个页面时,不需要解析,把存起来的结果拿出来使用即可
关于这一块我曾经写过,详细的来说明该如何智能的缓存哪些需要的信息
对于缓存后的地址栏信息对象,它就是一个共享对象,要确保它不能被开发者写入新的值,就可以使用前面我们定义的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
的兼容即可