SegmentFault 最新的文章 |
- 基于拉链式和线性探测式散列表实现Map
- Vue3+TypeScript封装axios并进行请求调用
- 【操作系统—虚拟化】内存分段
- Vue 3 组件开发:搭建基于SpreadJS的表格编辑系统(环境搭建)
- 许式伟:相比 Python,我们可能更需要 Go+
- 美团图数据库平台建设及业务实践
- 为了让 Android 更安全,谷歌推荐开发者使用 Rust 编写系统代码
- 浅探 Web Worker 与 JavaScript 沙箱
Posted: 12 Apr 2021 05:10 PM PDT 前言前几篇我们一起学习了基于数组、链表、二叉树、红黑树来实现Map的操作,本篇我们将会一起来学习基于散列表来实现Map,这种方式对应着java里面的HashMap,这也是使用最多的一种方式 散列表实现Map主要分为了两个步骤:
散列函数实现散列表的第一步就是需要考虑如何把一个键转换为数组的下标,这时候就需要使用到散列函数,先把键值转换成一个整数然后在使用除留余数法计算出数组的下标。每种类型的键我们都需要一个不同的散列函数。 在java中对于常用的数据类型已经实现了hashCode,我们只需要根据hashCode的值使用除留余数法即可转换成数组的下标 java中的约定:如果两个对象的equals相等,那么hashCode一定相同;如果hashCode相同,equals不一定相同。对于自定义类型的键我们通常需要自定义实现hashCode和equals;默认的hashCode返回的是对象的内存地址,这种散列值不会太好。 下面我来看看Java中常用类型的hashCode计算方式 Integer由于hashCode需要返回的值就是一个int值,所以Integer的hashCode实现很简单,直接返回当前的value值
LongJava中Long类型的hashCode计算是先把值无符号右移32位,之后再与值相异或,保证每一位都用用到,最后强制转换成int值
Double、Float浮点类型的键java中hashCode的实现是将键表示为二进制
Stringjava中每个char都可以表示成一个int值,所以字符串转换成一个int值
软缓存如果计算一个散列值比较耗时,那么我可以这个计算好的值缓存起来,即使用一个变量把这个值保存起来,在下一次调用时直接返回。Java中的String就是采用的这种方式。 拉链式的散列表散列函数能够将键值转换成数组的下标;第二步就是需要处理碰撞冲突,也就是需要处理遇到了两个散列值相同的对象应该如何存储。有一种方式是数组中的每一个元素都指向一个链表用来存放散列值相同的对象,这种方式被称为拉链法 拉链法可以使用原始的链表保存键,也可以采用我们之前实现的红黑树来表示,在java8中采用的这两种方式的混合,在节点数超过了8之后变为红黑树。 这里我们就采用简单的链表来实现拉链式散列表,数据结构使用在前几篇中已经实现的
这的hash函数使用key的hashCode与上0x7fffffff得到一个非负的整数,然后再使用除留余数法计算出数组的下标。其中0x7FFFFFFF 的二进制表示就是除了首位是 0,其余都是1,也就是说,这是最大的整型数 int(因为第一位是符号位,0 表示他是正数),可以使用Integer.MAX_VALUE代替 散列表的主要目的在于把键值均匀的分布到数组中,所以散列表是无序的,如果你需要的Map需要支持取出最大值,最小值,那么散列表都不适合。 这里我们实现的拉链式散列表的数组大小是固定的,但是通常在随着数据量的增大,越短的数组出现碰撞的几率会增大,所以需要数组支持动态的扩容,扩容之后需要把原来的值重新插入到扩容之后的数组中。数组的扩容可以参考之前的文章《老哥是时候来复习下数据结构与算法了》 线性探测式散列表实现散列表的另一种方式就是用大小为M来保存N个键值,其中M>N;解决碰撞冲突就需要利用数组中的空位;这种方式中最简单的实现就是线性探测。 线性探测的主要思路:当插入一个键,发生碰撞冲突之后直接把索引加一来检查下一个位置,这时候会出现三种情况:
初始化线性探测式散列表使用两个数组来存放keys和values,
插入
动态调整数组的大小我们可以参考之前在文章《老哥是时候来复习下数据结构与算法了》中实现的动态调整数据的大小;在线性探测中需要把原来的数据重新插入到扩容之后的数据,因为数组的大小变了需要重新计算索引的位置。
查询线性探测散列表中实现查询的思路:根据待查询key的hash函数计算出索引的位置,然后开始判断当前位置上的key是否和待查询key相等,如果相等那么就直接返回value,如果不相等那么就继续查找下一个索引直到遇到某个位置是null才结束,表示查询的key不存在;
删除元素线性探测式的删除稍微麻烦一些,首先需要查找出待删除的元素位置,删除元素之后需要把当前索引之后的连续位置上的元素重新插入;因为是否有空位对于线性探测式散列表的查找至关重要;例如:5->7->9,删除了7之后,如果不重新插入7的位置就是空位,那么get方法将无法查询到9这个元素; 每次删除之后都需要检测一次数组中实际元素的个数,如果与数组的容量相差较大,就可以进行缩容操作;
文中所有源码已放入到了github仓库:
最后(点关注,不迷路)文中或许会存在或多或少的不足、错误之处,有建议或者意见也非常欢迎大家在评论交流。 最后,写作不易,请不要白嫖我哟,希望朋友们可以点赞评论关注三连,因为这些就是我分享的全部动力来源🙏 |
Posted: 11 Apr 2021 03:02 AM PDT 不是吧,不是吧,原来真的有人都2021年了,连TypeScript都没听说过吧?在项目中使用TypeScript虽然短期内会增加一些开发成本,但是对于其需要长期维护的项目,TypeScript能够减少其维护成本,使用TypeScript增加了代码的可读性和可维护性,且拥有较为活跃的社区,当居为大前端的趋势所在,那就开始淦起来吧~ 使用TypeScript封装基础axios库代码如下:
取消多次重复的请求版本在上述代码加入如下代码:
在路由跳转时撤销所有请求在路由文件index.ts中加入
使用封装的axios请求库封装响应格式
封装接口方法举个栗子,进行封装User接口,代码如下~
项目中进行使用代码如下:
|
Posted: 11 Apr 2021 02:46 AM PDT 介绍利用基址和界限寄存器,操作系统很容易将不同进程重定位到不同的物理内存区域。但是,对于这些内存区域,栈和堆之间,有一大块"空闲"空间。栈和堆之间的空间并没有被进程使用,却依然占用了实际的物理内存。因此,简单的通过基址寄存器和界限寄存器实现的虚拟内存很浪费。 泛化的基址/界限为了解决这个问题,分段(segmentation)的概念应运而生。这个想法很简单,在MMU中引入不止一个基址和界限寄存器对,而是给地址空间内的每个逻辑段(segment)一对。一个段只是地址空间里的一个连续定长的区域,在典型的地址空间里有3个逻辑不同的段:代码、栈和堆。分段的机制使得操作系统能够将不同的段放到不同的物理内存区域,从而避免了虚拟地址空间中的未使用部分占用物理内存。 我们来看一个例子,如图所示,64KB的物理内存中放置了3个段。 你会想到,需要MMU中的硬件结构来支持分段:在这种情况下,需要一组3对基址和界限寄存器。下表展示了上面的例子中的寄存器值,每个界限寄存器记录了一个段的大小。 引用的是哪个段硬件在地址转换时使用段寄存器。它如何知道段内的偏移量,以及地址引用了哪个段? 一种常见的方式,有时称为显式(explicit)方式,就是用虚拟地址的开头几位来标识不同的段。在我们之前的例子中,有3个段,因此需要两位来标识。如果我们用14位虚拟地址的前两位来标识,那么虚拟地址如下所示: 前两位告诉硬件我们引用哪个段,剩下的12位是段内偏移。因此,硬件就用前两位来决定使用哪个段寄存器,然后用后12位作为段内偏移。偏移量与基址寄存器相加,硬件就得到了最终的物理地址。请注意,偏移量也简化了对段边界的判断。我们只要检查偏移量是否小于界限,大于界限的为非法地址。 上面使用两位来区分段,但实际只有3个段(代码、堆、栈),因此有一个段的地址空间被浪费。因此有些系统中会将堆和栈当作同一个段,因此只需要一位来做标识。 硬件还有其他方法来决定特定地址在哪个段。在隐式(implicit)方式中,硬件通过地址产生的方式来确定段。例如,如果地址由程序计数器产生(即它是指令获取),那么地址在代码段。如果基于栈或基址指针,它一定在栈段。其他地址则在堆段。 栈的问题栈和其他的内存段有一点关键区别,就是它反向增长,因此地址转换必须有所不同。首先,我们需要一点硬件支持。除了基址和界限外,硬件还需要知道段的增长方向(用一位区分,比如1代表自小而大增长,0反之)。 支持共享随着分段机制的不断改进,人们很快意识到,通过再多一点的硬件支持,就能实现新的效率提升。具体来说,要节省内存,有时候在地址空间之间共享(share)某些内存段是有用的。 为了支持共享,需要一些额外的硬件支持,这就是保护位(protection bit)。基本上就是为每个段增加了几个位,标识程序是否能够读写该段,或执行其中的代码。通过将代码段标记为只读,同样的代码可以被多个进程共享,而不用担心破坏隔离。虽然每个进程都认为自己独占这块内存,但操作系统秘密地共享了内存,进程不能修改这些内存,所以假象得以保持。 有了保护位,前面描述的硬件算法也必须改变。除了检查虚拟地址是否越界,硬件还需要检查特定访问是否允许。如果用户进程试图写入只读段,或从非执行段执行指令,硬件会触发异常,让操作系统来处理出错进程。 操作系统支持分段也带来了一些新的问题。第一个是老问题:操作系统在上下文切换时应该做什么?答案显而易见:各个段寄存器中的内容必须保存和恢复。每个进程都有自己独立的虚拟地址空间,操作系统必须在进程运行前,确保这些寄存器被正确地赋值。 第二个问题更重要,即管理物理内存的空闲空间。新的地址空间被创建时,操作系统需要在物理内存中为它的段找到空间。之前,我们假设所有的地址空间大小相同,物理内存可以被认为是一些槽块,进程可以放进去。现在,每个进程都有一些段,每个段的大小也可能不同。 一般会遇到的问题是,物理内存很快充满了许多空闲空间的小洞,因而很难分配给新的段,或扩大已有的段。这种问题被称为外部碎片(external fragmentation)[R69],如图所示。 该问题的一种解决方案是紧凑(compact)物理内存,重新安排原有的段。例如,操作系统先终止运行的进程,将它们的数据复制到连续的内存区域中去,改变它们的段寄存器中的值,指向新的物理地址,从而得到了足够大的连续空闲空间。但是,内存紧凑成本很高,因为拷贝段是内存密集型的,一般会占用大量的处理器时间。 一种更简单的做法是利用空闲列表管理算法,试图保留大的内存块用于分配。相关的算法可能有成百上千种,包括传统的最优匹配(best-fit,从空闲链表中找最接近需要分配空间的空闲块返回)、最坏匹配(worst-fit)、首次匹配(first-fit)以及像伙伴算法(buddy algorithm)这样更复杂的算法。但遗憾的是,无论算法多么精妙,都无法完全消除外部碎片,因此,好的算法只是试图减小它。 空闲空间管理我们暂且将对虚拟内存的讨论放在一边,先来讨论空闲空间管理(free-space management)的一些问题。在操作系统用分段(segmentation)的方式实现虚拟内存时,以及在用户级的内存分配库(如malloc()和free())中,需要管理的空闲空间由大小不同的单元构成。在这两种情况下,会出现外部碎片的问题,让管理变得较为困难。 假设首先,我们假定基本的接口就像malloc()和free()提供的那样。具体来说,void * malloc(size t size)需要一个参数size,它是应用程序请求的字节数。函数返回一个指针,指向这样大小的一块空间。对应的函数void free(void *ptr)函数接受一个指针,释放对应的内存块。 进一步假设,我们主要关心的是外部碎片的问题,先将内部碎片问题置之脑后。我们还假设,内存一旦被分配给客户,就不可以被重定位到其他位置。最后我们假设,分配程序所管理的是连续的一块字节区域。 底层机制分隔与合并空闲列表包含一组元素,记录了堆中的哪些空间还没有分配。假设有下面的30字节的堆: 这个堆对应的空闲列表如下: 可以看出,任何大于10字节的分配请求都会失败(返回NULL),因为没有足够的连续可用空间。但是,如果申请小于10字节空间,会发生什么?假设我们只申请一个字节的内存,此时分配程序会执行所谓的分割(splitting)动作:它找到一块可以满足请求的空闲空间,将其分割,第一块返回给用户,第二块留在空闲列表中。在我们的例子中,假设这时遇到申请一个字节的请求,分配程序选择使用第二块空闲空间,对malloc()的调用会返回20(1字节分配区域的地址),空闲列表会变成这样: 许多分配程序中也有一种机制,名为合并(coalescing)。如果应用程序调用free(10),归还堆中间的空间,会发生什么?分配程序不只是简单地将这块空闲空间加入空闲列表,还会在释放一块内存时合并可用空间。通过合并,最后空闲列表应该像这样: 追踪已分配空间的大小可以看到,free(void *ptr)接口没有块大小的参数。要完成这个任务,大多数分配程序都会在头结构(header)中保存一点额外的信息,它在内存中的位置通常就在返回的内存块之前。该头块中至少包含所分配空间的大小,它也可能包含一些额外的指针来加速空间释放,包含一个幻数来提供完整性检查,以及其他信息。我们假定,一个简单的头块包含了分配空间的大小和一个幻数:
在内存中看起来像是这样: 用户调用free(ptr)时,库会通过简单的指针运算得到头块的位置。获得头块的指针后,库可以很容易地确定幻数是否符合预期的值,作为正常性检查(assert(hptr->magic == 1234567)),并简单计算要释放的空间大小(即头块的大小加区域长度)。因此,如果用户请求N字节的内存,库不是寻找大小为N的空闲块,而是寻找N加上头块大小的空闲块。 让堆增长大多数传统的分配程序会从很小的堆开始,当空间耗尽时,再向操作系统申请更大的空间。通常,这意味着它们进行了某种系统调用(例如,大多数UNIX系统中的sbrk),让堆增长。操作系统在执行sbrk系统调用时,会找到空闲的物理内存页,将它们映射到请求进程的地址空间中去,并返回新的堆的末尾地址。这时,就有了更大的堆,请求就可以成功满足。 基本策略理想的分配程序可以同时保证快速和碎片最小化。遗憾的是,由于分配及释放的请求序列是任意的,任何策略在某些特定的输入下都会变得非常差。 最优匹配最优匹配(best fit)策略非常简单:首先遍历整个空闲列表,找到和请求大小一样或更大的空闲块,然后返回这组候选者中最小的一块。最优匹配背后的想法很简单:选择最接近用户请求大小的块,从而尽量避免空间浪费。然而,简单的实现在遍历查找正确的空闲块时,要付出较高的性能代价。 最差匹配最差匹配(worst fit)方法与最优匹配相反,它尝试找最大的空闲块,分割并满足用户需求后,将剩余的块加入空闲列表。最差匹配尝试在空闲列表中保留较大的块,而不是像最优匹配那样可能剩下很多难以利用的小块。最差匹配同样需要遍历整个空闲列表,大多数研究表明它的表现非常差,导致过量的碎片,同时还有很高的开销。 首次匹配首次匹配(first fit)策略就是找到第一个足够大的块,将请求的空间返回给用户。首次匹配有速度优势(不需要遍历所有空闲块),但有时会让空闲列表开头的部分有很多小块。因此,分配程序如何管理空闲列表的顺序就变得很重要。一种方式是基于地址排序(address-basedordering),通过保持空闲块按内存地址有序,合并操作会很容易,从而减少了内存碎片。 下次匹配不同于首次匹配每次都从列表的开始查找,下次匹配(next fit)算法多维护一个指针,指向上一次查找结束的位置。其想法是将对空闲空间的查找操作扩散到整个列表中去,避免对列表开头频繁的分割。这种策略的性能与首次匹配很接近,同样避免了遍历查找。 其他方式除了上述基本策略外,人们还提出了许多技术和算法,来改进内存分配。 分离空闲列表它的基本想法很简单:如果某个应用程序经常申请一种(或几种)大小的内存空间,那就用一个独立的列表,只管理这样大小的对象。其他大小的请求都交给更通用的内存分配程序。 这种方法的好处显而易见。通过拿出一部分内存专门满足某种大小的请求,碎片就不再是问题了。而且,由于没有复杂的列表查找过程,这种特定大小的内存分配和释放都很快。 不过这种方式也为系统引入了新的复杂性。例如,应该拿出多少内存来专门为某种大小的请求服务,而将剩余的用来满足一般请求?Solaris系统内核设计的厚块分配程序(slab allocator),很优雅地处理了这个问题。 具体来说,在内核启动时,它为可能频繁请求的内核对象创建一些对象缓存(object cache),如锁和文件系统inode等。这些的对象缓存每个分离了特定大小的空闲列表,因此能够很快地响应内存请求和释放。如果某个缓存中的空闲空间快耗尽时,它就向通用内存分配程序申请一些内存厚块(slab)(总量是页大小和对象大小的公倍数)。相反,如果给定厚块中对象的引用计数变为0,通用的内存分配程序可以从专门的分配程序中回收这些空间,这通常发生在虚拟内存系统需要更多的空间的时候。 厚块分配程序比大多数分离空闲列表做得更多,它将列表中的空闲对象保持在预初始化的状态。数据结构的初始化和销毁的开销很大,通过将空闲对象保持在初始化状态,厚块分配程序避免了频繁的初始化和销毁,从而显著降低了开销。 伙伴系统因为合并对分配程序很关键,所以人们设计了一些方法,让合并变得简单,一个好例子就是二分伙伴分配程序(binary buddy allocator)。 在这种系统中,空闲空间首先从概念上被看成大小为2ⁿ的大空间。当有一个内存分配请求时,空闲空间被递归地一分为二,直到刚好可以满足请求的大小。这时,请求的块被返回给用户。在下面的例子中,一个64KB大小的空闲空间被切分,以便提供7KB的块: 请注意,这种分配策略只允许分配2的整数次幂大小的空闲块,因此会有内部碎片(internal fragment)的麻烦。 伙伴系统的精髓在于内存被释放的时候。如果将这个8KB的块归还给空闲列表,分配程序会检查"伙伴"8KB是否空闲。如果是,就合二为一,变成16KB的块。然后会检查这个16KB块的伙伴是否空闲,如果是,就合并这两块。这个递归合并过程继续上溯,直到合并整个内存区域,或者某一个块的伙伴还没有被释放。 伙伴系统运转良好的原因,在于很容易确定某个块的伙伴。仔细观察的话,就会发现每对互为伙伴的块只有一位不同,正是这一位决定了它们在整个伙伴树中的层次。 其他想法上面提到的众多方法都有一个重要的问题,缺乏可扩展性(scaling)。具体来说,就是查找列表可能很慢。因此,更先进的分配程序采用更复杂的数据结构来优化这个开销,牺牲简单性来换取性能。例子包括平衡二叉树、伸展树和偏序树。 考虑到现代操作系统通常会有多核,同时会运行多线程的程序,因此人们做了许多工作,提升分配程序在多核系统上的表现。感兴趣的话可以深入阅读glibc分配程序的工作原理。 |
Vue 3 组件开发:搭建基于SpreadJS的表格编辑系统(环境搭建) Posted: 11 Apr 2021 08:16 PM PDT Vue是一套用于构建用户界面的渐进式框架,与其它大型 JS 框架不同,Vue 被设计为可以自底向上逐层应用,更易上手,还便于与第三方库或既有项目整合,因此,Vue完全能够为复杂的单页应用提供驱动。 2020年09月18日,Vue.js 3.0 正式发布,作者尤雨溪将其描述为:更快、更小、更易于维护。 Vue 3都加入了哪些新功能?本次发布, Vue框架本身迎来了多项更新,如Vue 此前的反应系统是使用 Object.defineProperty 的 getter 和 setter。 但是,在 Vue 3中,将使用 ES2015 Proxy 作为其观察者机制,这样做的好处是消除了以前存在的警告,使速度加倍,并节省了一半的内存开销。 除了基于 Proxy 的观察者机制,Vue 3的其他新特性还包括: 1. Performance(性能提升) 在Vue 2中,当某个DOM需要更新时,需要遍历整个虚拟DOM树才能判断更新点。而在Vue 3中,无需此项操作,仅需通过静态标记,对比虚拟节点上带有patch flag的节点,即可定位更新位置。 对比Vue 2和Vue 3的性能差异,官方文档中给出了具体数据说明: · SSR速度提高了2~3倍 · Update性能提高1.3~2倍 2. Composition API(组合API) Vue 2中有data、methods、mounted等存储数据和方法的对象,我们对此应该不陌生了。比如说要实现一个轮播图的功能,首先需要在data里定义与此功能相关的数据,在methods里定义该功能的方法,在mounted里定义进入页面自动开启轮播的代码…… 有一个显而易见的问题,就是同一个功能的代码却要分散在页面的不同地方,维护起来会相当麻烦。 为了解决上述问题,Vue 3推出了具备清晰的代码结构,并可消除重复逻辑的 Composition API,以及两个全新的函数setup和ref。 Setup 函数可将属性和方法返回到模板,在组件初始化的时候执行,其效果类似于Vue 2中的beforeCreate 和 created。如果想使用setup里的数据,需要将值return出来,没有从setup函数返回的内容在模板中不可用。 Ref函数的作用是创建一个引用值,主要是对String、Number、Boolean的数据响应做引用。 相对于Vue 2,Vue 3的生命周期函数也发生了变更,如下所示: · beforeCreate -> 请使用 setup() · created -> 请使用 setup() · beforeMount -> onBeforeMount · mounted -> onMounted · beforeUpdate -> onBeforeUpdate · updated -> onUpdated · beforeDestroy -> onBeforeUnmount · destroyed -> onUnmounted · errorCaptured -> onErrorCaptured 需要注意的是,Vue 2使用生命周期函数时是直接在页面中写入生命周期函数,而在Vue 3则直接引用即可: import {reactive, ref, onMounted} from 'vue' 3. Tree shaking support(按需打包模块) 有人将"Tree shaking" 称之为"摇树优化",其实就是把无用的模块进行"剪枝",剪去没有用到的API,因此"Tree shaking"之后,打包的体积将大幅度减少。 官方将Vue 2和Vue 3进行了对比,Vue 2若只写了Hello World,且没有用到任何的模块API,打包后的大小约为32kb,而Vue 3 打包后仅有13.5kb。 4. 全新的脚手架工具:Vite Vite 是一个由原生 ESM 驱动的 Web 开发构建工具。在开发环境下基于浏览器原生 ES imports 开发,在生产环境下基于 Rollup 打包。 和 Webpack相比,具有以下特点: · 快速的冷启动,不需要等待打包 · 即时的热模块更新 · 真正的按需编译,不用等待整个项目编译完成 由于完全跳过了打包这个概念,Vite的出现大大的撼动了Webpack的地位,且真正做到了服务器随起随用。看来,连尤大神都难逃"真香"理论。 Vite究竟有什么魔力?不妨让我们通过实际搭建一款基于Vue 3 组件的表格编辑系统,亲自体验一把。 一、环境搭建使用 Vite 初始化一个 Vue 3 项目1. 执行代码:
我们来看下生成的代码, 因为 vite 会尽可能多地镜像 vue-cli 中的默认配置, 所以,这段代码看上去和 vue-cli 生成的代码没有太大区别。
2. 执行下列命令: 此时如果不通过 npm run dev 来启动项目,而是直接通过浏览器打开 index.html, 会看到下面的报错: 报错的原因:浏览器的 ES module 是通过 http 请求拿到模块的,所以 vite 的一个任务就是启动一个 web server 去代理这些模块,在 vite 里是借用了 koa 来启动的服务。
由于浏览器中的 ESM 是获取不到导入的模块内容的,需要借助Webpack 等工具,如果我们没有引用相对路径的模块,而是引用 node_modules,并直接 import xxx from 'xxx',浏览器便无法得知你项目里有 node_modules,只能通过相对路径或者绝对路径去寻找模块。 这便是vite 的实现核心:拦截浏览器对模块的请求并返回处理后的结果(关于vite 的实现机制,文末会深入讲解)。 3. 生成项目结构: 入口 index.html 和 main.js 代码结构为:
4. 进入项目目录:cd myVue3 5. 安装相关模块:npm install 6. 下载模块: 7. 启动项目:npm run dev 8. 进入地址,当我们看到这个页面时,说明项目已经成功启动了。 Vite 的实现机制1. /@module/ 前缀 对比工程下的 main.js 和开发环境下实际加载的 main.js,可以发现代码发生了变化。 工程下的 main.js:
实际加载的 main.js:
为了解决 import xxx from 'xxx' 报错的问题,vite 对这种资源路径做了统一处理,即添加一个/@module/前缀。 在 src/node/server/serverPluginModuleRewrite.ts 源码的 koa 中间件里可以看到 vite 对 import 做了一层处理,其过程如下: · 在 koa 中间件里获取请求 body · 通过 es-module-lexer 解析资源 ast 拿到 import 的内容 · 判断 import 的资源是否是绝对路径,绝对视为 npm 模块 · 返回处理后的资源路径:"vue" => "/@modules/vue" 2. 支持 /@module/ 在 /src/node/server/serverPluginModuleResolve.ts 里可以看到大概的处理逻辑: · 在 koa 中间件里获取请求 body · 判断路径是否以 /@module/ 开头,如果是取出包名 · 去node_module里找到这个库,基于 package.json 返回对应的内容 3. 文件编译 通过前文,我们知道了 js module 的处理过程,对于vue、css、ts等文件,其又是如何处理的呢? 以 vue 文件为例,在 webpack 里使用 vue-loader 对单文件组件进行编译,在这里 vite 同样拦截了对模块的请求并执行了一个实时编译。 通过工程下的 App.vue 和实际加载的 App.vue,便发现改变。 工程下的 App.vue:
实际加载的 App.vue:
可见,一个 .vue 文件被拆成了三个请求(分别对应 script、style 和template) ,浏览器会先收到包含 script 逻辑的 App.vue 的响应,然后解析到 template 和 style 的路径后,再次发起 HTTP 请求来请求对应的资源,此时 Vite 对其拦截并再次处理后返回相应的内容。
vite对于其他的类型文件的处理几乎都是类似的逻辑,即根据请求的不同文件类型,做出不同的编译处理结果。 扩展阅读· Vue 3 组件开发实战:搭建基于SpreadJS的表格编辑系统(组件集成篇) |
Posted: 10 Apr 2021 09:38 PM PDT ECUG(Effective Cloud User Group,实效云计算用户组)主办的 2021 ECUG Con 于 2021 年 4 月 10 日 - 11 日在上海举办。会上,七牛云 CEO 许式伟以 "数据科学与 Go+" 为主题发表了主题分享,讲述了对数据科学变迁的理解,对新语言 Go+ 的设想和规划,并大胆指出数据科学正迎来爆发期,像字节跳动一样的新型公司只会越来越多。以下为演讲内容整理。 刚才在闲聊说 ECUG 变得越来越高大上,其实我也变得越来越像一个单纯的讲师。今年是 ECUG 社区的第 14 个年头,这场活动也是第 14 届 ECUG Con。其实这一届本来应该在去年办,但因为疫情延后了。 其实,我在 ECUG 一直贯彻的理念有两个: 第一,让自己持续地写代码。因为每一次来 ECUG 我都很紧张,不能什么都没有呀。所以这也是挺好的机会,能让自己持续留在技术一线; 另外,我每年分享的主题都有一定的延续性,呈现了我自己对未来思考的脉络。 去年开始,我在聊数据科学,前面有三年是聊在端上的一些实践。原因是我认为云计算的第一个时代应该是属于机器计算,也就是虚拟机;第二代就是云原生,我认为这是一场被称为"基础架构"的革命。也就是说,第一阶段是资源,第二阶段是基础架构。 第三个阶段,我的判断为应用计算,这会涉及前端和后端的协同。 从去年开始,我的分享转向了数据科学,一个很重要的因素和趋势是,数据时代的到来。尤其是 2017 年之后,数据大量地被数字化以后,在各行各业都会有涉及数据科学的广泛应用。 去年也是蛮巧的,我脑子一热就搞了一个语言出来。我以前搞过蛮多语言,受众也有一些。但是那个都很明确,从来没有想过有一天能够商业化。也许碰巧有一些公司用它来做商业化,但是基本上从出生那一刻开始,就不是冲着商业化去的。 2012 年我花了很多精力在布道 Go,因为当时作为一个初创公司,招人太难。一个比较好的招人逻辑就是让别人觉得你有趣,公司技术氛围很不错。Go+ 是我第一个认认真真希望能够把它商业化的语言,但目前宣传得还不多,1.0 还没发布。我想讲讲我自己对 Go+ 和数据科学的一个思考,为什么认为 Go+ 有商业化的机会。 我今天聊的话题大概有四个方面:
语言的发展首先,我们讲讲语言的发展,程序员对这个话题非常感兴趣。我把语言的发展史分为三个部分来说。 第一,静态语言的发展史。我选的是 TOP20 的语言,这个是根据现在最火的语言排行榜排名选的,前 20 名的语言我排了一下大概是这样的,最早发布的是 C,到现在其实还在排行榜前三的位置。第二是 C++,Objective-C、Java、C#、Go、Swift、Go+。我们可以看到一个比较有趣的现象,差不多每 6-8 年会出现一轮新的、具备影响力的静态语言,这是生产力迭代的表征。 第二,脚本语言的发展。你会发现它们非常不一样。最早是 Visual Basic,然后是 Python、PHP、JavaScript、Ruby,脚本语言是集中大爆发的,差不多全在 Java 出现的前后,来自 90 年代的前 5 个年头。这是非常有趣的一件事情,也是非常值得思考的,背后一定有一些内在的原因。 第三,数据科学相关语言的发展。但数据科学我选的是 TOP50,因为 TOP20 实在太少了。也蛮有意思的,最早的是 SQL,第二个 SAS,MATLAB、Python、R、Julia。Python 最早从来没想过自己会是数据科学语言,但最终变成了人工智能领域最火的语言。 这里又存在一个很明显的特征:它的跨度跟静态语言一样大,所以数据科学发展其实是古老而漫长的,但发展得没有那么快。静态语言差不多每 6-8 年有一个迭代,但数据科学语言不是,中间跨度特别大。但我觉得现在正进入数据科学的加速期。 你可能会想,为什么我要分析语言发展史呢?有几个结论是关键。 首先,我认为脚本语言是特定历史阶段下的产物,长期来看,静态语言更有生命力。 第二,数据科学是计算机的最初需求,最早计算机就是用来做计算的。它历史悠久但进步缓慢,因为数据大爆发的时代一直没有到来。 数据科学的发展聊完语言的发展,接下来我们谈谈数据科学的发展。数据科学也可以分为几个阶段,第一个阶段我叫做"原始时期",也可以叫"数学软件时代",这个时期基本上可归纳为两个特征,第一个是在有限领域里,最典型的是 BI(商业智能);第二个有限数据规模,典型就像 Excel,行列数都是非常有限的,其他的软件也基本上是这样的。 这个时期的数据科学特点是什么?首先它不是一个基础设施,实际上是数学应用软件,但能力非常全,很强大,包括了统计、预测、洞察、规划、决策等等。 第二个时期我叫做"数据科学的基建时期",真正让数据科学成为了基础设施,最典型的代表是大数据的兴起。Map/Reduce 是 Google 2004 年发布的一篇论文,2006 年就出现了 Hadoop,2009 年出现了 Spark。我认为这算是大数据兴起的一个阶段,也是数据科学基础设施化的开始。这个时期跟刚才的数学软件不一样,是以大规模处理能力为先,并不是以功能强大为先,它的功能相对局限。 深度学习的兴起和大数据的兴起间隔时间比较长,深度学习 2015 年开始有 TensorFlow,2017 年开始有 Torch,这是两个知名度最高的深度学习框架,深度学习本质就是通过数据自动推导 y=F(x)中的 F 函数。我们平常通常都是程序员实现这个 F,但深度学习最核心的概念是如何让机器自动产生这个 F,来达成最佳曲线拟合。它其实是基于测量结果的自动计算。 假设今天没有牛顿三大定理,但我有一堆测量数据,理论上应该能够发现牛顿三大定理,这就是深度学习的核心逻辑。它跟大数据并不是相互取代的关系,而是一种能力的加强,更多其实是如何让大数据的能力更进一步,更强悍。 有种看法认为,今天经济发展背后科技的驱动因子其实核心就只有两个,一个是计算,另外一个是数据。 数据核心就是我们今天聊的数据科学,数据科学其实是到了一个新的范式,有一个词叫"第四范式",中国有一个公司也叫第四范式,我们认为数据是更高阶的一种生产能力,它跟计算相比的话站在更高层次的维度。 前面是数据科学的两个阶段,那么第三个阶段是什么?我觉得是数据科学的大爆发时期,也就是今天,用马云的话说是"DT 时代"。原始时期是在有限的领域,有限的数据规模下去做的一种能力。未来首先是全领域的,首先领域不局限于的商业智能( BI )这样的范畴,第二个是大规模的数据,第三个是随处可见,随处可见包括云、智能手机、嵌入式设备等,这些都会植入我们所谓的数据智能。 这就意味着,今天移动互联网的兴起已经让很多公司非常牛,互联网的平民化或互联网应用的诞生,催生了 BAT。但是我们知道,现在新兴的、比较牛的公司,像字节跳动这种,其实不是互联网的成功,而是数据科学的成功。今天仍然不能说,数据科学是平民化的,它的门槛非常高。 但是我们看到,智能应用已经产生了,智能应用不会只局限于抖音这样一个局部领域的生产力放大,各行各业都会被数据智能,也就是刚刚我们提到的第四范式所影响。 数据和数据科学,一定会成为下一代生产力的支撑,今天产生了字节跳动、快手这样的新兴的公司,但他们只是一个开始,绝对不是结局。 在数据科学的原始时期,数据只是副产品。大家想象一下,在 BI 领域,数据只是一个副产品,只是用于后期的运营决策。 但是今天我们看到在大量的应用里,数据就是原材料。这是非常不一样的状态,这也是为什么,我把它叫做数据科学大爆发时期,这是我觉得今天为什么需要 Go+ 的原因,也是其背后的历史背景。 数据科学的未来一定是通用语言和数学软件的融合,从而完成真正意义上的数据科学的基础设施化。但在今天,数据科学的基础设施化还远没有完全完成,这是我自己的判断。 今天的 Python 已经很好了,为何需要 Go+?当然很多人会有疑问:今天的 Python 已经很好了,在深度学习领域已经被非常广泛地使用,为什么 Python 还不够,需要 Go+?其实我是认为,Python 是成不了基础设施的,它是一个脚本语言,我认为仅仅是特定历史阶段的需要。 数据科学本身是一种算力革命,哪怕在芯片领域,数据也能干翻计算,这是 Nvidia 干翻 Intel 的核心原因。上层软件领域就更加如此,一定会有一个新的基础设施承载者需要出现。 算力本质上是一种计算密集型业务,Python 的背后是 C,只靠 Python 还是不行。今天是 C 和 Python 支撑了整个深度学习,但数据科学一定还要进一步下沉,下沉的结果是什么? 这是我们今天需要 Go+ 的原因!前面主要讲我自己为什么认为 Go+有商业化的机会。当然我所说的商业化不一定是赚钱,大家不要误会这一点,语言可能在大多数人心目中是一个不赚钱的东西,但是这不代表它不重要,它非常重要。 Go+ 的设计理念聊完数据科学的发展,接下来我们聊聊 Go+ 的设计理念。Go+为什么是今天这个样子?计算背后要的是程序员,而数据科学背后要的是数据科学家或者叫分析师。这两个角色其实还是不一样的,虽然都是技术工作。我认为培养程序员是相对容易的,今天程序员的数量是非常庞大的,但数据科学家的数量相对较少,这也是为什么前几年深度学习兴起以后,所谓的 AI 工程师薪资被炒翻了,比程序员贵很多。其实就是因为数据科学家不容易找。 这个角色承载着技术和商业的连接,要找到同时具备两种能力的人是很难的。数据科学首先是一个技术工作,要的是技术能力,又要懂商业。今天仍然没有非常体系化的培养数据科学家的能力,没有这样一个体系方法论。 那么 Go+ 的核心理念又是什么呢? 第一个,我们试图用 Go+ 来统一程序员和数据科学家,让他们之间有共同话语,让双方能自然对话,我觉得这是 Go+ 最核心的一个思考点。Go+ 很重要的一个核心逻辑,是用一门语言让两个角色进行对话。 在这个基础上,我们延伸了一些设计逻辑。首先,Go+ 是一个静态语言,语法是完全兼容 Go 的;第二,形式上要比 Go 更像脚本,有更低的学习门槛。Go 虽然在静态语言里,可能学习门槛是低的,但还不够低,没有 Python 那么低;第三,很自然的,我们要做一个数据科学的语言,所以它必然要有更简洁的、数学运算上的语言文法支持;第四是双引擎,同时支持静态编译为可执行文件,也支持编译成字节码来解释执行。 为什么我们会选择语法完全兼容 Go 呢?首先我个人很坚定地认为,静态语言拥有更强的生命力,更能跨越历史的周期。大家也都很容易理解,语言是需要跨越周期的,语言的生命周期通常都非常长。我们不能很局限地说,当前在流行些什么东西,我就如何决定语言的设计,实际上我们要找到那些能够跨越周期的元素。 第二,为什么是 Go?我个人认为,在静态语言里,Go 的语法设计最为精简,学习门槛也是最低的,哪怕你以前没有学过静态语言,也很容易学会 Go。我们公司是最早招聘 Go 程序员的,但大部分招进来的人都不会 Go。我们用 Go 的时候,世界上真没多少人认为 Go 是未来的流行语言。我们自己实践的经验表明,Go 语言两周的学习基本上够了,是门槛非常低的一门静态语言。 但从数据科学语言来讲,Go 的门槛还不够低,Go+ 虽然完全兼容 Go,但我们希望它比 Go 的门槛还要有更低。所以它形式上要比 Go 更像脚本,因为脚本往往更容易理解。我们希望 Go+ 学习门槛和 Python 处于同一个层次。 去年 5、6 月份 Go+ 刚诞生,差不多 10 月份左右,我就开始让 13-14 岁,六年级到初一这个阶段的三个小孩尝试学习 Go+。这个实践证明,这个事情是可行的。他们能理解 Go+ 的设计,能够自如地使用 Go+ 写代码。这也证明了我们在 Go 的基础上做的所有简化的努力是非常划算的。 我这里简单列了一些 Go+ 的语法,当然不是全部,只是一些我认为还是相对比较简洁的表达。有理数 Python 里面没有,我们认为有理数在数据科学里,尤其在无损数值运算里,还是会非常常见。Go+ 内置了有理数的支持。当然 Map、Slice 基本上 Python 都有。 列表理解(List comprehesion)其实也是 Python 有的,但我们对列表理解的支持非常的完整,基本上理解了 Go+ 中 for 循环怎么写也就理解了列表理解。更多的还是数据科学的一些常规操作的简洁表达。以上是一个大概语法示意,如果有朋友没看过 Go+,希望可以大概对 Go+ 有个理解。 Go+ 非常有意思的一点,它是唯一一个选择了双引擎的语言,既支持静态编译,也支持可解析执行。 为什么要做双引擎呢?因为我认为程序员和数据科学家的诉求是不一样的,数据科学家喜欢单步执行,大家可以在心中回想一下你见过的数学软件,包括 SAS、MATLAB,数学软件交互都是单步执行的方式。 这并不是因为数据科学家懒。程序员理解程序逻辑是可以放在脑子里的,我们脑子里知道程序逻辑写得对不对。但数据科学家做计算的时候,不能知道计算结果对不对,因为人的计算能力比计算机弱太多了,所以一定要单步执行看到计算结果,才能知道自己下一步应该怎么办,这是数据科学家和程序员工作模式完全不同的一个点。 因为他是在做计算而不是在做一种程序逻辑,所以他很难不去做单步执行。 但当数据科学家建了一种模型,最终要使用了,这时他仍然希望最终交付的是最大化的执行效率,他一定不希望代码运行很慢,所以这个时候他就又需要静态编译执行,这也是为什么 Go+ 希望设计成双引擎,因为调试阶段和生产使用阶段,工作模式完全不一样。 Go+ 实现上的迭代聊完 Go+ 的设计理念,我们进入最后一个 session,Go+ 实现上的迭代。当前 Go+ 做到了什么份上?Go+ 虽然还没有推出 1.0 版本,但是语法目前支持百分之六七十肯定有了,语法完成度还是不错的。 Go+ 的源代码,通过扫描器转成一个 Go+ 的 Token,再通过一个 parser 变成 Go+ 的抽象的语法数,常见语言都是这么干的。Go+ 的抽象语法树转化后有两个分支,一个生成 Go 的代码从而使其可以静态编译,另外一个分支生成字节码解析执行,分支的多态是通过引入了一个叫执行规范(exec.spec)的东西,其实就是一个抽象的接口。 当前,我个人在迭代的过程中发现了一个问题,对一个初步加入 Go+ 团队的人来说,是需要一段时间熟悉整个业务的。Go+ 执行规范的部分,其实是一种抽象的 SAX 接口,也就是基于事件驱动,我有一个事件发送给接受方,接受方按自己的需要处理这个事件,这在文本处理里面比较常见。 我们之前设计的接口基本上是用事件驱动的模式来把不同组件连接起来。编译器把抽象语法数解析完发出一些事件,这些事件被两个代码生成的模块接收,按照自己的需求去干活。这个模式代码还是有点难理解,尤其是编译器里面又做一些复杂的事情,让代码比较难理解。大家如果了解过 Go 背后的实现逻辑,类型推导在 Go 里面比较复杂,其实我们编译器的复杂性大部分是由类型推导导致的。 我当前在试图重构这个逻辑,想把执行规范部分变得不再是一个抽象的接口,而是一个标准实现的 DOM,这个 DOM 本身包含了类型推导的能力,从而使得编译器相对比较简单。讲实现我今天没法讲的特别细,后面有机会再展开。 下面我想讲一下 Go+ 下一步做的重心是什么。 首先,最核心的逻辑,还是希望今年能够发布 1.0 版本,而 1.0 版本最重要的事情是把用户的使用范式做最大化的确认,1.0 以后我希望和 Go 差不多,后面的语法变更是比较少的。当前最重要的工作,是明确 Go+ 需要哪些最核心的语法,并且在 1.0 版本就尽量去支持,除非有一些特定的考量比如说像 Go 的范型这种特别复杂的语法特性,留到后续的版本去支持。Go+ 也是类似的,我们可能会放弃一些特别复杂的语法特性,但是基本上尽可能把大部分我们需要的语法特性在 1.0 版本里确定下来。 Go+ 1.0 我们会先进行单引擎的迭代,先做好静态编译的引擎,等 1.0 发布以后再迭代脚本的引擎。这也是基于上面我们说的用户的使用范式优先的理念下的一个决策。 最后,我们希望用商业化的方式来运作 Go+,也会招聘 Go+ 的团队成员,欢迎大家加入 Go+ 团队! 我认为 Go+的核心是首先统一了程序员和数据科学家的语言,让双方能够自然对话。另外我非常坚定地相信 Go+ 会是数据科学的下一个变革,我自己非常兴奋能够做这样一件事情,也非常欢迎认可这件事的人加入我们。 这是联系我们的方法,第一个是项目的地址(https://github.com/goplus),第二个投简历的邮箱(jobs@qiniu.com),第三个是我推特的地址(@xushiwei)。 本文作者许式伟是七牛云创始人兼 CEO,Go 语言大中华区首席布道师,Go+ 语言创造者,ECUG 社区发起人。他曾就职于金山、盛大,在搜索和分布式存储相关技术领域有十几年的研发经验。在金山,他以首席架构师的身份主导了 WPS Office 2005 的架构设计和开发。在创立金山实验室后,作为技术总监主导了分布式存储开发,后加入盛大创新院,并成功推出"盛大网盘"和"盛大云"。许式伟在 2020 年被评选为《2020 中国开源先锋 33 人之心尖上的开源人物》。 |
Posted: 05 Apr 2021 12:02 AM PDT 图数据结构,能够更好地表征现实世界。美团业务相对较复杂,存在比较多的图数据存储及多跳查询需求,亟需一种组件来对千亿量级图数据进行管理,海量图数据的高效存储和查询是图数据库研究的核心课题。本文介绍了美团在图数据库选型及平台建设方面的一些工作。 1 前言图数据结构,能够很自然地表征现实世界。比如用户、门店、骑手这些实体可以用图中的点来表示,用户到门店的消费行为、骑手给用户的送餐行为可以用图中的边来表示。使用图的方式对场景建模,便于描述复杂关系。在美团,也有比较多的图数据存储及多跳查询需求,概括起来主要包括以下 4 个方面:
总体来说,美团需要一种组件来管理千亿级别的图数据,解决图数据存储以及多跳查询问题。海量图数据的高效存储和查询是图数据库研究的核心课题,如何在大规模分布式场景中进行工程落地是我们面临的痛点问题。传统的关系型数据库、NoSQL 数据库可以用来存储图数据,但是不能很好处理图上多跳查询这一高频的操作。 Neo4j 公司在社交场景(见图1)里做了传统关系型数据库 MySQL 跟图数据库 Neo4j 的查询性能对比 [1],在一个包含 100 万人、每人约有 50 个朋友的社交网络里找最大深度为 5 的朋友的朋友,实验结果表明多跳查询中图数据库优势明显(见图 2)。然而选取或者自主研发一款高吞吐、低查询延时、能存储海量数据且易用的图数据库非常困难。下面将介绍美团在图数据库选型及平台建设方面的一些工作。 2 图数据库选型在图数据库的选型上我们主要考虑了以下 5 点:(A) 项目开源,暂不考虑需付费的图数据库;(B) 分布式架构设计,具备良好的可扩展性;(C) 毫秒级的多跳查询延迟;(D) 支持千亿量级点边存储;(E) 具备批量从数仓导入数据的能力。 分析 DB-Engines[2] 上排名前 30 的图数据库,剔除不开源的项目,我们将剩余的图数据库分为三类:
DGraph 是由前 Google 员工 Manish Rai Jain 离职创业后,在 2016 年推出的图数据库产品,底层数据模型是 RDF[12],基于 Go 语言编写,存储引擎基于 BadgerDB[13] 改造,使用 RAFT 保证数据读写的强一致性。 NebulaGraph 是由前 Facebook 员工叶小萌离职创业后,在 2019年 推出的图数据库产品,底层数据模型是属性图,基于 C++ 语言编写,存储引擎基于 RocksDB[14] 改造,使用 RAFT 保证数据读写的强一致性。 这两个项目的创始人都在互联网公司图数据库领域深耕多年,对图数据库的落地痛点有深刻认识,整体的架构设计也有较多相似之处。在图数据库最终的选型上,我们基于 LDBC-SNB 数据集[15]对 NebulaGraph、DGraph、HugeGraph 进行了深度性能测评,测试详情见文章:主流开源分布式图数据库 Benchmark,从测试结果看 NebulaGraph 在数据导入、实时写入及多跳查询方面性能均优于竞品。此外,NebulaGraph 社区活跃,问题响应速度快,所以团队最终选择基于 NebulaGraph 来搭建图数据库平台。 3 NebulaGraph架构一个完整的 NebulaGraph 集群包含三类服务,即 Query Service、Storage Service 和 Meta Service。每类服务都有其各自的可执行二进制文件,既可以部署在同一节点上,也可以部署在不同的节点上。下面是NebulaGraph 架构设计(见图 3)的几个核心点16。
NebulaGraph 基于 C++ 实现,架构设计支持存储千亿顶点、万亿边,并提供毫秒级别的查询延时。我们在 3 台 48U192G 物理机搭建的集群上灌入 10 亿美食图谱数据对 NebulaGraph 的功能进行了验证。
4 图数据库平台建设为了统一管理图数据,减少工程同学在图数据库集群上的运维压力,我们基于开源分布式图数据库 NebulaGraph,搭建了一套一站式图数据库自助管理平台(见图 4),该平台包含以下 4 层:
与业界方案相比,团队主导设计的图数据库平台除了支持存储千亿顶点、万亿边,具备毫秒级别查询能力外,还提供了如下四项能力:应用可用性 SLA 达 99.99%;支持每小时百亿量级数据导入;实时写入数据时保证多集群数据最终一致性;易用的图谱可视化能力。下面将介绍具体的设计思路。 4.1 高可用模块设计首先介绍单应用多集群高可用模块的设计(AP 方案)。为什么有 AP 方案的设计呢?因为接入图数据库平台的业务方比较在意的指标是集群可用性。在线服务对集群的可用性要求非常高,最基础的要求是集群可用性能达到 4 个 9,即一年里集群的不可用时间要小于一个小时。对于在线服务来说,服务或者集群的可用性是整个业务的生命线,如果这点保证不了,即使集群提供的能力再多再丰富,那么业务方也不会考虑使用,可用性是业务选型的基础。 另外,公司要求中间件要有跨区域容灾能力,即要具备在多个地域部署多集群的能力。我们分析了平台接入方的业务需求,大约 80% 的场景是 T+1 全量导入数据、线上只读。在这种场景下,对图数据的读写强一致性要求并不高,因此我们设计了单应用多集群这种部署方案。 AP 方案部署方式可以参考图 5,一个业务方在图数据库平台上创建了 1 个应用并部署了 4 个集群,其中北京 2 个、上海 2 个,平时这 4 个集群同时对外提供服务。假如现在北京集群 1 挂了,那么北京集群 2 可以提供服务。如果说真那么不巧,北京集群都挂了,或者北京侧对外的网络不可用,那么上海的集群也可以提供服务。在这种部署方式下,平台会尽可能地通过一些方式来保证整个应用的可用性。然后每个集群内部尽量部署同机房的机器,因为图数据库集群内部 RPC 非常多,如果有跨机房或者跨区域的频繁调用,整个集群对外的性能会比较低。 高可用模块主要包含下面 4 个部分,如上图 6 所示: 第一部分是右侧的图数据库 Agent,它是部署在图数据库集群的一个进程,用来收集机器和图数据库三个核心模块的信息,并上报到图数据库平台。Agent 能够接收图数据库平台的命令并对图数据库进行操作。 第二部分是图数据库平台,它主要是对集群进行管理,并同步图数据库集群的状态到配置中心。 第三部分是图数据库 SDK,主要负责管理连接到图数据库集群的连接。如果业务方发送了某个查询请求,SDK 会进行集群的路由和负载均衡,选择出一条高质量的连接来发送请求。此外,SDK 还会处理图数据库集群中问题机器的自动降级以及恢复,并且支持平滑切换集群的数据版本。 第四部分是配置中心,类似 ZooKeeper,存储集群的当前状态。 4.2 每小时百亿量级数据导入模块设计第二个模块是每小时百亿量级数据导入模块,平台在 2019 年底- 2020 年初全量导入数据的方式是调用 NebulaGraph 对外提供的批量数据导入接口,这种方式的数据写入速率大概是每小时 10 亿级别,导入百亿数据大概要耗费 10 个小时,耗时较长。此外,在以几十万每秒的速度导数据的过程中,会长期占用机器的 CPU、IO 资源,一方面会对机器造成损耗,另一方面数据导入过程中集群对外提供的读性能会变弱。 为了解决上面两个问题,平台进行了如下优化:在 Spark 集群中直接生成图数据库底层文件 SST File,再借助 RocksDB 的 Bulkload 功能直接 ingest 文件到图数据库。 数据导入的核心流程可以参考图 7,当用户执行导数据操作后,图数据库平台会向公司的 Spark 集群提交一个 Spark 任务,在 Spark 任务中会生成图数据库里相关的点、边以及点索引、边索引相关的 SST 文件,并上传到美团的 S3 云存储上。文件生成后,图数据库平台会通知应用中多个集群去下载这些存储文件,之后完成 ingest 跟 compact 操作,最后完成数据版本的切换。 为兼顾各个业务方的不同需求,平台统一了应用导入、集群导入、离线导入、在线导入,以及全量导入、增量导入这些场景,然后细分成下面九个阶段,从流程上保证在导数据过程中应用整体的可用性:SST File 生成 、SST File 下载 、ingest、compact、数据校验、增量回溯、数据版本切换、集群重启、数据预热。 4.3 实时写入多集群数据同步模块设计第三个模块是实时写入多集群数据同步模块,平台约有 15% 的需求场景是在实时读取数据时,还要把新产生的业务数据实时写入集群,并且对数据的读写强一致性要求不高。就是说,业务方写到图数据库里的数据,不需要立马能读到。针对上述场景,业务方在使用单应用多集群这种部署方案时,多集群里的数据需要保证最终一致性。针对这一需求,我们做了以下设计。 第一部分是引入 Kafka 组件,业务方在服务中通过 SDK 对图数据库进行写操作时,SDK 并不直接写图数据库,而是把写操作写到 Kafka 队列里,之后由该应用下的多个集群异步消费这个 Kafka 队列。 第二部分是集群在应用级别可配置消费并发度,来控制数据写入集群的速度。具体流程如下:
第三部分是在实时写入数据过程中,平台可以同步生成一个全量数据版本,并做平滑切换(见图 9),确保数据的不重、不漏、不延迟。 4.4 图可视化模块设计第四个模块是图可视化模块(见图10),主要是用于解决子图探索问题。当用户在图数据库平台通过可视化组件查看图数据时,能尽量通过恰当的交互设计来避免因为节点过多而引发爆屏。主要包括以下几个功能:
5 业务实践5.1 智能助理该项目数据是基于美团商户数据、用户评论构建的餐饮娱乐知识图谱,覆盖美食、酒店、旅游等领域,包含 13 类实体和 22 类关系。目前,点边数量大概在百亿级别,数据 T+1 全量更新,主要用于解决搜索或者智能助理里 KBQA(全称:Knowledge Based Question Answer)类问题。核心处理流程是通过 NLP 算法识别关系和实体后构造出 NebulaGraph SQL 语句,再到图数据库获取数据。 典型的应用场景包括商场找店,比如,某个用户想知道望京新荟城这个商场有没有海底捞,系统可以快速查出结果告诉用户;另一个场景是标签找店,用户想知道望京 SOHO 附近有没有适合情侣约会的餐厅,或者可以多加几个场景标签,系统都可以帮忙查找出来。 5.2 搜索召回该项目数据是基于医美商家信息构建的医美知识图谱,包含 9 类实体和 13 类关系,点边数量在百万级别,同样也是 T+1 全量更新,主要用于大搜底层实时召回,返回与 Query 相关的商户、产品或医生信息,解决医美类搜索词少结果、无结果问题。比如,某个用户搜"啤酒肚"这种症状、或者"润百颜"这类品牌,系统可以召回相关的医美门店。 5.3 图谱推荐理由该项目数据来自用户的画像信息、商户的特征信息、用户半年内收藏/购买行为,数据量级是 10 亿级别,T+1 全量更新。现在美团 App 和点评 App 上默认的商户推荐列表是由深度学习模型生成的,但模型并不会给出生成这个列表的理由,缺少可解释性。 而在图谱里用户跟商户之间天然存在多条连通路径,项目考虑选出一条合适路径来生成推荐理由,在 App 界面上展示给用户推荐某家店的原因。该项目基于用户的协同过滤算法来生成推荐理由,在家乡、消费水平、偏好类目、偏好菜系等多个组合维度中找出多条路径,然后给这些路径打分,选出一条分值较高的路径,之后按照特定 Pattern 产出推荐理由。通过上述方式,就可以获得"在北京喜欢北京菜的山东老乡都说这家店很赞",或者"广州老乡都中意他家的正宗北京炸酱面"这类理由。 5.4 代码依赖分析该项目把代码库中代码依赖关系写入到图数据库。代码库中存在很多服务代码,这些服务会包括对外提供的接口,这些接口的实现依赖于该服务中某些类的成员函数,这些类的成员函数又依赖了本类的成员变量、成员函数或者其它类的成员函数,那么它们之间的依赖关系就形成了一张图,可以把这个图写到图数据库里做代码依赖分析。 典型应用场景是精准测试:当开发同学完成需求并向公司的代码仓库提交了 PR 后,可以把更改实时地写到图数据库中。这样的话,开发同学就能查到他所写的代码影响了哪些外部接口,并且借助图可视化组件查看调用路径。如果开发同学本来是要改接口 A 的行为,改了很多代码,但是他可能并不知道他改的代码也会影响到对外接口 B、C、D,这时候就可以用代码依赖分析来做个 Check,增加测试的完备性。 6 总结与展望目前,图数据库平台基本具备了对图数据的一站式自助管理功能。如果某个业务方要使用这种图数据库能力,那么业务方可以在平台上自助地创建图数据库集群、创建图的 Schema、导入图数据、配置导入数据的执行计划、引入平台提供的 SDK 对数据进行操作等等。平台侧主要负责各业务方图数据库集群的稳定性。目前,美团有三四十个业务已经在平台上落地,基本满足了各个业务方的需求。 未来规划主要有两个方向,第一,根据业务场景优化图数据库内核,提升平台稳定性,开发的通用 Feature 持续反哺 NebulaGraph 社区。第二,挖掘更多的图数据价值。现在平台仅支持图数据存储及多跳查询这种基本能力,后续将基于 NebulaGraph 去探索图学习、图计算的能力,为平台用户提供更多挖掘图数据价值的功能。 7 作者信息登昌、梁帅、高辰、杨鑫、尊远、王超等,均为美团搜索与NLP部工程师。 8 招聘信息如果你对"图存储"、"图学习"、"图计算"感兴趣,欢迎给我们投递简历,投递邮箱:zhaodengchang@meituan.com。 9 参考资料
阅读美团技术团队更多技术文章合集 前端 | 算法 | 后端 | 数据 | 安全 | 运维 | iOS | Android | 测试 | 在公众号菜单栏对话框回复【2020年货】、【2019年货】、【2018年货】、【2017年货】等关键词,可查看美团技术团队历年技术文章合集。 | 本文系美团技术团队出品,著作权归属美团。欢迎出于分享和交流等非商业目的转载或使用本文内容,敬请注明"内容转载自美团技术团队"。本文未经许可,不得进行商业性转载或者使用。任何商用行为,请发送邮件至tech@meituan.com申请授权。 |
为了让 Android 更安全,谷歌推荐开发者使用 Rust 编写系统代码 Posted: 06 Apr 2021 08:23 PM PDT 对于安卓开发者来说,Java 和 Kotlin 是比较流行的选择;而对于从事操作系统以及内部底层的开发者来说,C 和 C++ 是比较热门的选择。但在众多语言中,Rust 作为一款小众的现代系统编程语言,近两年间逐渐受到了各大企业的青睐。 去年,AWS 开源团队宣布聘用 Rust 编译器联合创始人 Felix Klock,未来致力于进一步改进 Rust。苹果也表示对将 C 代码移植到 Rust 感兴趣,希望投身到使用 Rust 编写代码的潮流中,想要在基于 Linux 的服务器端平台上将 C 过渡到 Rust。微软在采访中也表示 C/C++ 无法胜任编写关键任务的软件,业界非常需要高性能、内存安全的编程语言来开发底层系统,而当今市场上最好的选择就是 Rust。 近日,谷歌宣布出于安全性的考虑,推荐开发者使用 Rust 编程语言来开发操作系统。并表示在过去的 18 个月里,它一直在为 Android 开源项目添加 Rust 支持。但在 Android 平台上添加一门新语言是一项巨大的工程。一些工具链和依赖关系需要维护,测试基础设施和工具必须更新,开发人员需要接受培训。 C 和 C++ 的局限性 Android 系统的底层需要 C 和 C++ 等系统编程语言。这些语言为开发者提供了控制和可预测性,这在访问低级系统资源和硬件时非常重要。不幸的是,C 和 C++ 并不能提供内存安全保证,使得它们容易出现错误和安全漏洞。开发者有责任在这些语言上管理内存寿命,但在复杂和多线程的代码库中,这说起来容易做起来难。 C 和 C++ 共同构成了 Android 平台上数以千万计的代码行。这些内存安全漏洞成为最难解决的代码错误来源,占 Android 高严重度安全漏洞的 70% 左右。单纯的修复这些 bug 变得不足以处理问题,更好的方法是在一开始就预防这些 bug。 由于缺乏内存安全保障,迫使开发者在严格约束的无权限沙盒内运行Android进程。但沙盒在资源上的成本很高,会消耗额外的开销,并引入延迟。沙盒也不能完全消除代码的漏洞,而且由于 bug 密度高,沙盒的功效会降低,进一步! 另一个限制,虽然不是 C 和 C++ 独有的,但适用于所有的内存安全问题,那就是错误状态必须在工具化的代码中实际触发,才能被检测到。所以即使你的代码有很好的测试,实际的 bug 也可能一直没有被发现。而当发现bug时,让它们得到修复又是另一项任务,涉及到一个漫长而昂贵的过程,不一定能得到正确的修复。因此,bug 检测变得不可靠,鉴于这些局限性,bug 预防是更好的方法。 Rust 及其优势 2020 年底,Rust 官方在调研了全球 8323 位开发者后,最新发布了 2020 年 Rust 调查报告。通过报告,我们发现 Rust 开发者的用户黏度较高,有 83% 的受访者表示他们一直在使用该语言。 众所周知,当提及 Rust 语言时,安全、性能、并发等特性是其优势。Rust 通过使用编译时检查和运行时检查相结合的方式提供内存安全保证,以强制执行对象的寿命/所有权,并确保内存访问是有效的。在实现这种安全性的同时,还能提供与C和C++相当的性能。Rust 还减少了对沙盒的需求,让开发人员有更多的开销空间来引入更安全、更轻量的新功能。 但是据官方调查报告显示,Rust 仍有许多亟需完善之处。譬如以下几点:
虽然 Rust 确实有它的好处,但一夜之间将整个 Android 操作系统换成 Rust 是不可行的,并且现阶段的 Rust 也远非尽善尽美。而且可能根本不需要这样做,因为大多数 Android 的内存错误都发生在新的或最近修改的代码中,大约有 50% 的代码是不到一年的。谷歌认为,其内存安全语言的工作最好集中在新的开发上,而不是重写成熟的 C 和 C++ 代码。 据 Android 开发人员关系小组的成员称,Google 目前还不打算发布 Rust NDK。应用开发支持的语言将继续是 Kotlin,Java,C 和 C ++。
|
Posted: 08 Apr 2021 04:42 AM PDT 一些「炒冷饭」的背景介绍本文并不会从头开始介绍 Web Worker 的基础知识和基本 API 的使用等(只是部分有涉及),若还未了解过 Web Worker,可参考查阅 W3C 标准 Workers 文档 中的相关介绍。 自从 2014 年 HTML5 正式推荐标准发布以来,HTML5 增加了越来越多强大的特性和功能,而在这其中,工作线程(Web Worker)概念的推出让人眼前一亮,但未曾随之激起多大的浪花,并被在其随后工程侧的 Angular、Vue、React 等框架的「革命」浪潮所淹没。当然,我们总会偶然看过一些文章介绍,或出于学习的目的做过一些应用场景下的练习,甚或在实际项目中的涉及大量数据计算场景中真的使用过。但相信也有很多人和我一样茫然,找不到这种高大上的技术在实际项目场景中能有哪些能起到广泛作用的应用。 究其原因,Web Worker 独立于 UI 主线程运行的特性使其被大量考虑进行性能优化方面的尝试(比如一些图像分析、3D 计算绘制等场景),以保证在进行大量计算的同时,页面对用户能有及时的响应。而这些性能优化的需求在前端侧一方面涉及频率低,另一方面也能通过微任务或服务端侧处理来解决,它并不能像 Web Socket 这种技术为前端页面下的轮询场景的优化能带来质的改变。 直至 2019 年爆火的微前端架构的出现,基于微应用间 JavaScript 沙箱隔离的需求,Web Worker 才得以重新从边缘化的位置跃入到我的中心视野。根据我已经了解到的 Web Worker 的相关知识,我知道了 Web Worker 是工作在一个独立子线程下(虽然这个子线程比起 Java 等编译型语言的子线程实现得还有点弱,如无法加锁等),线程之间自带隔离的特性,那基于这种「物理」性的隔离,能不能实现 JavaScript 运行时的隔离呢? 本文接下来的内容,将介绍我在探索基于 Web Worker 实现 JavaScript 沙箱隔离方案过程中的一些资料收集、理解以及我的踩坑和思考的过程。虽然可能整篇文章内容都在「炒冷饭」,但还是希望我的探索方案的过程能对正在看这篇文章的你有所帮助。 JavaScript 沙箱在探索基于 Web Worker 的解决方案之前,我们先要对当前要解决的问题——JavaScript 沙箱有所了解。 提到沙箱,我会先想到出于兴趣玩过的沙盒游戏,但我们要探索的 JavaScript 沙箱不同于沙盒游戏,沙盒游戏注重对世界基本元素的抽象、组合以及物理力系统的实现等,而 JavaScript 沙箱则更注重在使用共享数据时对操作状态的隔离。 在现实与 JavaScript 相关的场景中,我们知道平时使用的浏览器就是一个沙箱,运行在浏览器中的 JavaScript 代码无法直接访问文件系统、显示器或其他任何硬件。Chrome 浏览器中每个标签页也是一个沙箱,各个标签页内的数据无法直接相互影响,接口都在独立的上下文中运行。而在同一个浏览器标签页下运行 HTML 页面,有哪些更细节的、对沙箱现象有需求的场景呢? 当我们作为前端开发人员较长一段时间后,我们很轻易地就能想到在同一个页面下,使用沙箱需求的诸多应用场景,譬如:
这里我们先回到开头,先将前提假设在我正在面对的微前端架构设计下。在微前端架构(推荐文章 Thinking in Microfrontend 、拥抱云时代的前端开发架构——微前端 等)中,其最关键的一个设计便是各个子应用间的调度实现以及其运行态的维护,而运行时各子应用使用全局事件监听、使全局 CSS 样式生效等常见的需求在多个子应用切换时便会成为一种污染性的副作用,为了解决这些副作用,后来出现的很多微前端架构(如 乾坤)有着各种各样的实现。譬如 CSS 隔离中常见的命名空间前缀、Shadow DOM、 乾坤 sandbox css 的运行时动态增删等,都有着确实行之有效的具体实践,而这里最麻烦棘手的,还是微应用间的 JavaScript 的沙箱隔离。 在微前端架构中,JavaScript 沙箱隔离需要解决如下几个问题:
在 乾坤 架构设计中,关于沙箱有两个入口文件需要关注,一个是 proxySandbox.ts,另一个是 snapshotSandbox.ts,他们分别基于 Proxy 实现代理了 window 上常用的常量和方法以及不支持 Proxy 时降级通过快照实现备份还原。结合其相关开源文章分享,简单总结下其实现思路:起初版本使用了快照沙箱的概念,模拟 ES6 的 Proxy API,通过代理劫持 window ,当子应用修改或使用 window 上的属性或方法时,把对应的操作记录下来,每次子应用挂载/卸载时生成快照,当再次从外部切换到当前子应用时,再从记录的快照中恢复,而后来为了兼容多个子应用共存的情况,又基于 Proxy 实现了代理所有全局性的常量和方法接口,为每个子应用构造了独立的运行环境。 另外一种值得借鉴的思路是阿里云开发平台的 Browser VM,其核心入口逻辑在 Context.js 文件中。它的具体实现思路是这样的:
总的来说,在 Browser VM 的实现中, 可以看出其实现部分还是借鉴了 乾坤 或者说其他微前端架构的思路,比如常见全局对象的代理和拦截。并且借助 Proxy 特性,针对 Cookie、LocalStorage 的读写同样能做一些安全策略的实现等。但其最大的亮点还是借助 iframe 做了一些取巧的实现,当这个为每个子应用创建的 iframe 被移除时,写在其下 window 上的变量和 setTimeout、全局事件监听等也会一并被移除;另外基于 Proxy,DOM 事件在沙箱中做记录,然后在宿主中生命周期中实现移除,能够以较小的开发成本实现整个 JavaScript 沙箱隔离的机制。 除了以上社区中现在比较火的方案,最近我也在 大型 Web 应用插件化架构探索 一文中了解到了 UI 设计领域的 Figma 产品也基于其插件系统产出了一种隔离方案。起初 Figma 同样是将插件代码放入 iframe 中执行并通过 postMessage 与主线程通信,但由于易用性以及 postMessage 序列化带来的性能等问题,Figma 选择还是将插件放入主线程去执行。Figma 采用的方案是基于目前还在草案阶段 Realm API,并将 JavaScript 解释器的一种 C++ 实现 Duktape 编译到了 WebAssembly,然后将其嵌入到 Realm 上下文中,实现了其产品下的三方插件的独立运行。这种方案和探索的基于 Web Worker 的实现可能能够结合得更好,持续关注中。 Web Worker 与 DOM 渲染在了解了 JavaScript 沙箱的「前世今生」之后,我们将目光投回本文的主角——Web Worker 身上。 正如本文开头所说,Web Worker 子线程的形式也是一种天然的沙箱隔离,理想的方式,是借鉴 Browser VM 的前段思路,在编译阶段通过 Webpack 插件为每个子应用包裹一层创建 Worker 对象的代码,让子应用运行在其对应的单个 Worker 实例中,比如:
但在了解过微前端下 JavaScript 沙箱的实现过程后,我们不难发现几个在 Web Worker 下去实现微前端场景的 JavaScript 沙箱必然会遇到的几个难题:
其他诸如 Web Worker 无法访问页面全局变量和函数、无法调用 alert、confirm 等 BOM API 等问题,相对于无法访问 window、document 全局对象已经是小问题了。不过可喜的是,Web Worker 中可以正常使用 setTimeout、setInterval 等定时器函数,也仍能发送 ajax 请求。 所以,当先要解决问题,便是在单个 Web Worker 实例中执行 DOM 操作的问题了。首先我们有一个大前提:Web Worker 中无法渲染 DOM,所以,我们需要基于实际的应用场景,将 DOM 操作进行拆分。 React Worker DOM因为我们微前端架构中的子应用局限在 React 技术栈下,我先将目光放在了基于 React 框架的解决方案上。 在 React 中,我们知道其将渲染阶段分为对 DOM 树的改变进行 Diff 和实际渲染改变页面 DOM 两个阶段这一基本事实,那能不能将 Diff 过程置于 Web Worker 中,再将渲染阶段通过 postMessage 与主线程进行通信后放在主线程进行呢?简单一搜,颇为汗颜,已经有大佬在 5、6 年前就有尝试了。这里我们可以参考下 react-worker-dom 的开源代码。 react-worker-dom 中的实现思路很清晰。其在 common/channel.js 中统一封装了子线程和主线程互相通信的接口和序列化通信数据的接口,然后我们可以看到其在 Worker 下实现 DOM 逻辑处理的总入口文件在 worker 目录下,从该入口文件顺藤摸瓜,可以看到其实现了计算 DOM 后通过 postMessage 通知主线程进行渲染的入口文件 WorkerBridge.js 以及其他基于 React 库实现的 DOM 构造、Diff 操作、生命周期 Mock 接口等相关代码,而接受渲染事件通信的入口文件在 page 目录下,该入口文件接受 node 操作事件后再结合 WorkerDomNodeImpl.js 中的接口代码实现了 DOM 在主线程的实际渲染更新。 简单做下总结。基于 React 技术栈,通过在 Web Worker 下实现 Diff 与渲染阶段的进行分离,可以做到一定程度的 DOM 沙箱,但这不是我们想要的微前端架构下的 JavaScript 沙箱。先不谈拆分 Diff 阶段与渲染阶段的成本与收益比,首先,基于技术栈框架的特殊性所做的这诸多努力,会随着这个框架本身版本的升级存在着维护升级难以掌控的问题;其次,假如各个子应用使用的技术栈框架不同,要为这些不同的框架分别封装适配的接口,扩展性和普适性弱;最后,最为重要的一点,这种方法暂时还是没有解决 window 下资源共享的问题,或者说,只是启动了解决这个问题的第一步。 接下来,我们先继续探讨 Worker 下实现 DOM 操作的另外一种方案。window 下资源共享的问题我们放在其后再作讨论。 AMP WorkerDOM在我开始纠结于如 react-worker-dom 这种思路实际落地开发的诸多「天堑」问题的同时,浏览过其他 DOM 框架因为同样具备插件机制偶然迸进了我的脑海,它是 Google 的 AMP。 AMP 开源项目 中除了如 amphtml 这种通用的 Web 组件框架,还有很多其他工程采用了 Shadow DOM、Web Component 等新技术,在项目下简单刷了一眼后,我欣喜地看到了工程 worker-dom。 粗略翻看下 worker-dom 源码,我们在 src 根目录下可以看到 main-thread 和 worker-thread 两个目录,分别打开看了下后,可以发现其实现拆分 DOM 相关逻辑和 DOM 渲染的思路和上面的 react-worker-dom 基本类似,但 worker-dom 因为和上层框架无关,其下的实现更为贴近 DOM 底层。 先看 worker-thread DOM 逻辑层的相关代码,可以看到其下的 dom 目录 下实现了基于 DOM 标准的所有相关的节点元素、属性接口、document 对象等代码,上一层目录中也实现了 Canvas、CSS、事件、Storage 等全局属性和方法。 接着看 main-thread,其关键功能一方面是提供加载 worker 文件从主线程渲染页面的接口,另一方面可以从 worker.ts 和 nodes.ts 两个文件的代码来理解。 在 worker.ts 中像我最初所设想的那样包裹了一层代码,用于自动生成 Worker 对象,并将代码中的所有 DOM 操作都代理到模拟的 WorkerDOM 对象上:
在 nodes.ts 中,实现了真实元素节点的构造和存储(基于存储数据结构是否以及如何在渲染阶段有优化还需进一步研究源码)。 同时,在 transfer 目录下的源码,定义了逻辑层和 UI 渲染层的消息通信的规范。 总的来看,AMP WorkerDOM 的方案抛弃了上层框架的约束,通过从底层构造了 DOM 所有相关 API 的方式,真正做到了与框架技术栈无关。它一方面完全可以作为上层框架的底层实现,来支持各种上层框架的二次封装迁移(如工程 amp-react-prototype),另一方面结合了当前主流 JavaScript 沙箱方案,通过模拟 window、document 全局方法的并代理到主线程的方式实现了部分的 JavaScript 沙箱隔离(暂时没看到路由隔离的相关代码实现)。 当然,从我个人角度来看,AMP WorkerDOM 也有其当前在落地上一定的局限性。一个是对当前主流上层框架如 Vue、React 等的迁移成本及社区生态的适配成本,另一个是其在单页应用下的尚未看到有相关实现方案,在大型 PC 微前端应用的支持上还无法找到更优方案。 其实,在了解完 AMP WorkerDOM 的实现方案之后,基于 react-worker-dom 思路的后续方案也可以有个大概方向了:渲染通信的后续过程,可考虑结合 Browser VM 的相关实现,在生成 Worker 对象的同时,也生成一个 iframe 对象,然后将 DOM 下的操作都通过 postMessage 发送到主线程后,以与其绑定的 iframe 兑现来执行,同时,通过代理将具体的渲染实现再转发给原 WorkerDomNodeImpl.js 逻辑来实现 DOM 的实际更新。 小结与一些个人前瞻首先聊一聊个人的一些总结。Web Worker 下实现微前端架构下的 JavaScript 沙箱最初是出于一点个人灵光的闪现,在深入调研后,虽然最终还是因为这样那样的问题导致在方案落地上无法找到最优解从而放弃采用社区通用方案,但仍不妨碍我个人对 Web Worker 技术在实现插件类沙箱应用上的持续看好。插件机制在前端领域一直是津津乐道的一种设计,从 Webpack 编译工具到 IDE 开发工具,从 Web 应用级的实体插件到应用架构设计中插件扩展设计,结合 WebAssembly 技术,Web Worker 无疑将在插件设计上占据举足轻重的地位。 其次是一些个人的一些前瞻思考。其实从 Web Worker 实现 DOM 渲染的调研过程中可以看到,基于逻辑与 UI 分离的思路,前端后续的架构设计有很大机会能够产生一定的变革。目前不管是盛行的 Vue 还是 React 框架,其框架设计不论是 MVVM 还是结合 Redux 之后的 Flux,其本质上仍旧还是由 View 层驱动的框架设计(个人浅见),其具备灵活性的同时也产生着性能优化、大规模项目层级升上后的协作开发困难等问题,而基于 Web Worker 的逻辑与 UI 分离,将促使数据获取、处理、消费整个流程的进一步的业务分层,从而固化出一整套的 MVX 设计思路。 当然,以上这些我个人还处于初步调研的阶段,不成熟之处还需多加琢磨。且听之,后续再实践之。 作者:ES2049 / 靳志凯 文章可随意转载,但请保留此原文链接。 |
You are subscribed to email updates from SegmentFault 最新的文章. To stop receiving these emails, you may unsubscribe now. | Email delivery powered by Google |
Google, 1600 Amphitheatre Parkway, Mountain View, CA 94043, United States |
No comments:
Post a Comment