You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
import{inject}from'inject-instance'importBfrom'./B'exportdefaultclassA{
@inject('B')privateb: Bpublicname='aaa'say(){console.log('A inject B instance',this.b.name)}}
B.ts
import{inject}from'inject-instance'importAfrom'./A'exportdefaultclassB{
@inject('A')privatea: Apublicname='bbb'say(){console.log('B inject A instance',this.a.name)}}
入口文件如下,期望输入注释中的结果:
importinjectInstancefrom'inject-instance'constinstances1=injectInstance(A,B)instances1.get('A').say()instances1.get('B').say()instances1.get('A').name='c'instances1.get('B').say()// A inject B instance bbb// B inject A instance aaa// B inject A instance cconstinstances2=injectInstance(A,B)instances2.get('A').say()instances2.get('B').say()// A inject B instance bbb// B inject A instance aaa
1 背景
本文开发框架基于 React,涉及 React 部分会对背景做简单铺垫。
前端开源江湖非常有意思,竞争是公平的,而且不需要成本,任何一个初入茅庐的学徒都可以找江湖高手过招,且迟早会自成门派,而今前端门派已经灿若繁星,知名的门派也不计其数,其『供需链』大致如下:
w3c规范 ==> 浏览器实现 ==> 开发引擎 ==> 数据框架 ==> UI框架 ==> 开发者 ==> 用户
『可视化在线编辑器』指的是引擎这一环,虽然开发引擎在前端并不常见,但看看游戏界就能知道,脱离游戏引擎编码是多么痛苦的一件事。前端和游戏共同点是都要考虑 UI 和 数据逻辑,其实微软在做界面开发时就有很多引擎出现,现在前端一点一点向全栈迈进,架构越来越重,分工越来越细,因为 node 让许多后端开发者接触前端,将后端沉淀的精髓带到了前端,而今前端又将触手延伸到客户端、PC端甚至硬件领域,逐渐吸收了开发引擎的思想,促进前端进入工业时代。
在线编辑器是我在百度负责的主要项目之一,因为需要在 RN 的支持下兼容三端,因此就要设计得更加通用,为了循序渐进的讲解,我准备以 设计理念 功能实现 拓展架构设计 的顺序叙述。
2 设计理念
在头脑风暴之前,我们有几个目标需要提前明确,就像做游戏引擎一样,如果整体架构没有设计好,之后的开发将非常痛苦,以下是我重构两次后总结出的整体要领。
2.1 模块化
typescript
的用户需要的类型库组件。2.2 编辑器核心功能精简
所有编辑功能由插件提供,编辑器只需要实现"任何位置和功能都能由插件替代"的功能即可(拓展架构设计详细说明),这样编辑器可以理解为一块神奇磁铁,其特殊的引力将插件规律的吸附在四周。
2.3 展示器不关注平台细节
即不要对组件进行 dom 结构的包装,就可以适应任何平台(由组件内部实现决定)。
2.4 事件设计
事件可以让程序活起来,就像 Playmaker 可以不用写一行代码,在 Unity3d 做一款小游戏一样。事件分为 触发条件 与 动作效果:
onScroll
、按钮组件的onClick
都可以作为触发条件。2.5 数据流设计
React
本身只是View
层,仅提供了组件内部状态State
以及不建议使用的Context
维护简单数据状态。编辑器复杂度较高,必须借助外部数据流管理,我们使用Mobx
以及Inversify
实现双向绑定和依赖注入,数据流向如下图所示:从页面组件开始看,将
Action
与Store
分别注入到页面中,由于希望数据变动后页面立刻刷新,我们用mobx
将Store
注入到组件的props
中,而Action
则通过Inversify
直接注入为实例的成员变量。Action
之间也可以相互注入,同样Store
也可以相互注入。只允许Action
修改Store
,进而触发页面props
变化,页面刷新。3 功能实现
3.1 编辑器
需要实现两种状态:编辑态 和 预览态
3.1.1 编辑状态
编辑状态需要捕获
click
hover
等鼠标事件,由于组件不一定将回调透传,我们通过ReactDOM.findDOMNode
拿到组件的dom
节点直接监听。再实现 实时编辑 与 拖拽功能 ,编辑器的核心业务逻辑就完成了。
3.1.2 预览状态
因为预览与实际部署效果一致,所以调用 展示器 传入当前页面编辑信息即可。
3.1.3 拖拽功能
由于支持了内部排序,与外部拖拽,社区的 SortableJs 非常合适担此重任。
我们将
Sortablejs
与React
结合即可完成拖拽功能,在结合前先介绍一下React
在dom
方面的特征:我们转换策略,仅仅将
Sortable
库作为中间动画使用,并依托其拖拽生命周期,在拖拽结束后获取用户拖拽意图,将 dom 的改动完全还原,再将意图交由React
来实现。伪代码
如下:3.1.4 实时编辑
将页面所有编辑元素打平,每个元素渲染时绑定其对应
id
的数据,修改属性时直接修改对应数据,mobx
会直接更新目标组件实例,如图所示:Map
中有一个根节点,从根节点开始渲染,每个节点从数据库中取到自身数据,如果有子元素,则会递归渲染,子元素再从数据库获取子元素自身的数据,依次循环,当循环完毕后,我们会得到一颗与Map
数据一一对应绑定的dom
树,Map
中任何一个元素发生改变,Mobx
会通过之前getter
记录的关联关系,主动找到绑定的实例执行forceUpdate
刷新。3.1.5 设置为组合
编辑器中,除了设定好在菜单中的组件,还可以让任意组合形成模板,将模板作为新组件放在组件菜单中。
关键点在于如何从打平的数据中获取组件间关联关系,并独立抽出来。
生成模板配置只需获取全量编辑信息,并进行瘦身即可,
伪代码
如下:将模板插入到页面中,首先将瘦身的信息补全,再给内部每个组件设置一个全新的
key
, 但关联关系保持不变,最后将最外层组件挂载到拖拽到的父级上。伪代码
如下:关联关系不变,比如组合是
a
有一个子元素b
,key
分别是keyA
keyB
,因为组件 map 需要保证 key 的唯一性,生成一对新的 keykeyC
keyD
,但keyB
父级关联的keyA
同时也会改为keyC
。3.2 展示器
如图所示,展示器负责部署在各端,目前支持网页、安卓和苹果。核心思想是利用
react-native
将组件直接渲染到端上,为了同时适配网页,使用 react-native-web 配合webpack
,将react-native
代码在网页端编译时alias
到react-native-web
,用其提供的兼容样式展现。展示器还负责将仅预览状态有效的 事件机制、变量配置、动作等激活,利用自身生命周期,以及子组件的回调函数挂上动作钩子。
3.3 动态拓展
如果说编辑与展示给了应用健壮的躯体,那动态拓展就让应用活了起来。
动态数据对编辑器来说,是一个拓展功能,分别可以拓展组件的 功能、数据来源 以及 融入应用自身的数据流。
3.3.1 功能注入
就是将平台特有的功能注入到编辑器生成的页面中,其实这是一种反向注入的过程,编辑器申明自己想要什么,具体功能是如何实现,效果如何,都完全交由各平台自己去实现。
更加自由的方式是申明回调函数,编辑器可以发出带有任意参数的回调,供部署到的平台任意拓展,平台部署的
伪代码
如下:3.3.2 传参注入
在网页显示一篇文章,一定是通过 url 获取 id,在端上也是通过页面传参拿到的,我们在部署端将可能拿到的参数全部注入到展示器中。
3.3.3 数据流接入
如果页面部署在普通网页上,比如做运营页,那就没有数据流概念一说。如果部署在端上,或者部署在一个网页平台上,那部署端自身一定有自己的数据流系统,可能是
redux
mobx
等等mvc
mvp
的设计,我们需要考虑将数据流接入这些自有体系中。Action
的方式展现与操作数据。也就是让每个组件都依赖数据接口,组件即便拆出来单独使用,但一旦部署到端上,将会自动接入端上数据流。3.4 事件
事件只发生在展示器阶段,事件分为 触发条件 与 动作效果,我们在展示器对每个组件包一层高阶组件,让其支持触发和响应事件。
3.4.1 触发条件
伪代码
如下:3.4.2 动作效果
props
做merge
即可。事件的整体流程如下图所示:
4 拓展架构设计
为了让编辑器拓展性更强,我们可以将编辑器所有功能以插件方式组装,插件可以插入到编辑器任何位置,也可以插件嵌套插件;插件可以使用编辑器数据流,也可以提供数据流供其它插件使用。
也就是拓展分为数据流拓展与UI拓展。
在 数据流设计 这一章提到了非常灵活的数据注入,首先
mobx-react
利用context
实现了任意Action
Store
注入在任意React
组件中,我们只需要实现在Action
与Store
中相互注入即可。4.1 数据流拓展
我们希望任意
Action
Store
之间都能随意注入,不会引发循环依赖,可以通过引入中间人的方式解决。我们有A.ts
B.ts
两个文件,分别在各自的类中引入对方实例,并期望所有对引用的操作都发生在同一实例下(如果组件被实例化多次,我们一定不希望多个实例共享数据),希望的结果伪代码
如下:A.ts
B.ts
入口文件如下,期望输入注释中的结果:
可以看出,如果实现了
inject-instance
,就可以在componentWillMount
的生命周期调用injectInstance
,并传入所有Action
Store
,不同实例之间数据流独立。4.1.1 inject-decorator 实现原理
inject-decorator
是装饰器,给字段打一个tag
,告诉之后要执行的injectInstance
方法:"这个字段要注入 XXX Class,到时候帮我替换一下!"。伪代码
如下:将变量值替换成注入类名称,只是标记到时候替换成什么类的实例,而
injectArray
字段才是打tag
,执行injectInstance
时会根据这个字段来替换对应成员变量。4.1.2 injectInstance 实现原理
将传入的所有类根据类名放入
Map
(仅加快查找用,用空间换时间),因为返回对应实例,所以先全部实例化,再遍历所有实例,根据inject-decorator
打的tag
变量injectArray
将对应字段替换为实例。最后,编辑器将得到的全部实例传入
mobx-react
的provider
中,实现了 UI 组件注入数据与数据流中注入的数据是统一份实例的效果。更多注入细节,查看 inject-instance。
4.2 UI拓展
就是允许插件插入到页面任何节点,与数据注入不同,数据注入是将所有插件数据流与编辑器自身数据流混在一起,其结构是打平的,像一个
Map
。而UI注入,结构像Tree
是层叠的,编辑器自身预留许多插槽,允许任何插件插入。为了更好的拓展性,也允许插件留下插槽,让其它插件插入,而这样的好处不仅在于位置灵活,还可以优雅实现『自定义编辑功能』的能力,这个之后再说。
在编辑器或者插件中留一个插槽的
伪代码
如下:如果插件类中静态属性
Positon = 'navbarLeft'
,他就会插入在左侧导航条中。别忘了,依赖与
inject-instance
的数据流注入功能,插件也可以随时调用这个方法,因此轻松实现插件预留插槽的功能。4.2.1 利用 UI 注入实现自定义编辑类型
编辑器一般会提供基础编辑类型,比如纯文本的
text
,下拉选择框select
等等,如果用户希望自定义一种array
编辑类型,实现对数组字段编辑功能,可以用 UI 注入的方式实现。为了实现这种方式,编辑组件中,判断编辑类型的
伪代码
如下:注意,预留插槽的属性可以存在变量,而且以传入的编辑类型为结尾,就可以拓展编辑类型了,其它类型的拓展也不在话下。
那么希望支持
array
类型时,编辑器会试图加载editorAttributeArray
UI组件,那我们定义一个Position = 'editorAttributeArray'
的组件就可以显示在这个位置,之后读取编辑器核心数据流的currentEditComponent
对当前编辑组件进行操作即可。4.3 拓展架构总结
用一张图总结插件拓展的全貌:
插件与编辑器的数据流是双向互通的,插件的UI可以插入编辑器UI,插件也可以插入插件的UI(不能循环引用)。
5 结语
看到这里,其实编辑器实现原理倒并不重要了,重要的是对数据流、拓展性的设计思路,这些思想迁移到普通类型项目依然适用。当然,如果还有兴趣可以读读编辑器实现源码。
The text was updated successfully, but these errors were encountered: