啸达同学刚写zone.js系列就说过,NgZone影响着Angular中的变更检测,历时一个多月的笔耕不辍,终于到了他初次下笔时的目的地~
zone.js系列
- zone.js由入门到放弃之一——通过一场游戏认识zone.js
- zone.js由入门到放弃之二——zone.js API大练兵
- zone.js由入门到放弃之三——zone.js 源码分析【setTimeout篇】
- zone.js由入门到放弃之四——Angular对zone.js的应用
初见NgZone
其实在上一篇文章中,大家已经初步窥探过NgZone的芳容了。而且我们也知道了,在NgZone中维护了OuterZone和InnerZone两个Zone。今天的这篇文章,我们主要分析一下InnerZone,并看一下InnerZone是如何跟Angular的变更检测联系到一起的。
InnerZone四方法
NgZone中InnerZone的创建是通过forkInnerZoneWithAngularBehavior
完成的,创建过程的简化版如下,其中又能看到很多熟悉的勾子函数。这里简单复习一下这几个勾子的意义:
onInvokeTask
:zone.js会在初始化的时候将异步方法都Pathc成ZoneTask,从而跟踪异步任务的执行情况的。onInvokeTask
就是其中的一个勾子函数,它会在异步任务执行回调的时候触发。onInvoke
:onInvoke
会在我们手动执行zone.run()的时候执行。onHasTask
:是针对整个任务队列状态改变的监听,当检测任务队列中有任务进入、或是有任务执行完出队列的时候会被执行。onHandleError
:当有异常抛出时被执行
InnerZone对异步任务的控制精华基本上就全部浓缩在这几个勾子函数中了,与此同时,为了更好地配合对异步任务的跟踪,NgZone中还定义了很多状态监控字段。只有理清这些字段的含义才能继续往下深入代码。
不熟悉zone.js原理的可以回看一下zone.js由入门到放弃之一和zone.js由入门到放弃之二(链接见文首)
function forkInnerZoneWithAngularBehavior(zone: NgZonePrivate) {
zone._inner = zone._inner.fork({
name: 'angular',
properties: <any>{'isAngularZone': true},
onInvokeTask: (...): any => {
...
},
onInvoke: (...): any => {
...
},
onHasTask: (...): any => {
...
},
onHandleError: (...): any => {
...
},
});
}
InnerZone五状态
接下来这几个状态属性会贯穿在后面的源码分析的全部过程中,我们也会通过对这几个状态的跟踪了解一下InnerZone事件跟踪的原理。
- hasPendingMacrotasks: boolean 队列中是否有待执行的宏任务
- hasPendingMicrotasks: boolean 队列中是否有待执行的微任务
- _nesting: number 队列中待执行任务的个数
- isStable: boolean 当任务队列中既没有待执行的宏任务,也没有待执行的微任务时,isStable为ture,表示当前是个稳定的状态。反之则代表非稳定状态。
- lastRequestAnimationFrameId: number 这个状态有些特别,它是一个延时器,后面会展开解释。
代码走读
前面在介绍zone.js的时候我们说过,zone.js把异步任务分为MacroTask、MicroTask和Event三种。今天我们就分别把这三种任务都按流程分析一遍。从难易程度上看,MacroTask最简单,Event相对最复杂。接下来,我们就按照这个顺序讲解。
MacroTask
之前在zone.js由入门到放弃之三中,详细介绍过zone.js对setTimeout的Patch过程,如果不了解具体过程的强烈建议先浏览一下那篇文章。
这一次,我们还是通过个setTimeout事件来跟踪NgZone的处理过程,测试代码很简单,如下所示。
export class AppComponent implements OnInit {
title = 'ngzone-process';
ngOnInit(): void {
setTimeout(() => {
console.log('[setTimeout] run in next 5s');
}, 5000);
}
ngDoCheck() {
console.log('rendering...');
}
}
因为zone.js可以感知到任务队列的变化情况,所以当setTimeout
执行时,它可以知道当前有一个宏任务来了,同时会触发onHasTask勾子。
onHasTask
当onHasTask
"检测"到有宏任务到来时,会把hasPendingMacrotasks
设置为true。
onHasTask:
(delegate: ZoneDelegate, current: Zone, target: Zone, hasTaskState: HasTaskState) => {
delegate.hasTask(target, hasTaskState);
if (current === target) {
// ...
} else if (hasTaskState.change == 'macroTask') {
zone.hasPendingMacrotasks = hasTaskState.macroTask;
}
}
},
此时,NgZone中的几个状态值大概是这个样子的,hasPendingMacrotasks变为true,表示当前有一个待执行的MacroTask。
接下来,zone.js会通过调用scheduleFn
,并把封装后的回调函数放在Timer队列中等待时钟到达。
hasPendingMacrotasks | hasPendingMicrotasks | _nesting | isStable | lastRequestAnimationFrameId |
---|---|---|---|---|
true | false | 0 | true | -1 |
onInvokeTask
当时钟到达以后,事件循环会把封装后的回调函数放在任务队列中等待执行。当执行到回调时,回调会触发task.invoke
函数,接下来就会唤醒onInvokeTask勾子函数。
onInvokeTask:
(delegate: ZoneDelegate, current: Zone, target: Zone, task: Task, applyThis: any,
applyArgs: any): any => {
try {
onEnter(zone);
// 执行真正的回调 👇
return delegate.invokeTask(target, task, applyThis, applyArgs);
} finally {
// ...
onLeave(zone);
}
},
delegate.invokeTask(target, task, applyThis, applyArgs);
是用来调用真正的回调函数的。除了这行,我们可以看到在回调之前先后分别还各有一个方法:onEnter
和onLeave
。
onEnter
onEnter
执行过程中,_nesting
会自增,表示了当前新增一个待执行任务。当有任务要执行时,之前的稳定状态会被打破,同时触发一个onUnstable
事件。这个onUnstable
事件被ApplicationRef订阅,ApplicationRef会根据这个事件同步修改它自身的稳定状态(ApplicationRef的代码后面讲解)。
function onEnter(zone: NgZonePrivate) {
zone._nesting++;
if (zone.isStable) {
zone.isStable = false;
zone.onUnstable.emit(null);
}
}
hasPendingMacrotasks | hasPendingMicrotasks | _nesting | isStable | lastRequestAnimationFrameId |
---|---|---|---|---|
true | false | 1 | false | -1 |
onLeave
onLeave
函数执行的时候,说明MarcoTask的回调已经执行完毕,_nesting
会执行一次自减操作。接下来又执行了checkStable
函数。
function onLeave(zone: NgZonePrivate) {
zone._nesting--;
checkStable(zone);
}
checkStable
函数非常关键!每当执行到checkStable的时候,都是变更检测执行的关键。以至于这个函数的每一行都值得拿出来讲一下,我在代码中标记了序号,这样方便后面走读代码。
checkStable
既然是判断是否进行变更检测关键,那么1标识的if子句就是判断的关键。代码的意思大概就是,只有确保当前没有任何待执行的任务,同时当前状态为不稳定状态的时候才需要触发变更检测。- 代码2标识了一个成对的
_nesting
自增、自减操作。这里这么做的原因是代码3这里抛出了事件,对该事件的订阅实际上也是一个异步任务。所以这里通过_nesting
的自增、自减操作说明这里是有一个异步任务的。 - 代码3就是变更检测的关键了,AppliactionRef会订阅
onMicrotaskEmpty
事件,每当onMicrotaskEmpty
触发后,AppliactionRef就会执行一次变更检测。 - 代码4这里大家可能会有疑问,为什么在这里还要对
hasPendingMicrotasks
进行一次判断?这是因为在代码3这里,对onMicrotaskEmpty
的订阅者有可能会在订阅回调中再执行一些异步任务,就像下面这样。此时,并不能保证在checkStable
的过程中,不会有新的任务进入到待执行队列。所以这里,又对hasPendingMicrotasks
的状态做了一次判断。确保在状态变为稳定之前,任务队列中不存在任务微任务。
zone.onMicrotaskEmpty.subscribe(() => {
Promise.resolve(0).then(console.log);
});
- 代码5是对外触发一个状态稳定的事件,这个事件跟
OnEnter
函数中那个onUnstable
相对。但是你可能会好奇,这里为什么要在runOutsideAngular
中执行。我这里解释下,仅代表个人见解。onStable
和onMicrotaskEmpty
存在一样的问题,因为都是可观察对象,所以存在订阅者在回调继续执行异步任务的问题。如果在onStable
的订阅中执行异步任务,那NgZone的状态马上有会变成非稳定的,这将会陷入一个无限的死循环中,NgZone会在稳定和不稳定状态之间来回切换,永不停止。所以这里使用runOutsideAngular
,让zone.js放弃对这里的代码进行跟踪。这样,根据上一讲我们学过的内容,runOutsideAngular
中执行异步不会触发变更检测,当然也不会触发NgZone的状态变化。 - 改变zone的状态为稳定。
function checkStable(zone: NgZonePrivate) {
// 👇 1
if (zone._nesting == 0 && !zone.hasPendingMicrotasks && !zone.isStable) {
try {
// 👇 2
zone._nesting++;
// 👇 3
zone.onMicrotaskEmpty.emit(null);
} finally {
// 👇 2
zone._nesting--;
// 👇 4
if (!zone.hasPendingMicrotasks) {
try {
// 👇 5
zone.runOutsideAngular(() => zone.onStable.emit(null));
} finally {
// 👇 6
zone.isStable = true;
}
}
}
}
}
这里我多补充一点知识,我之前看到这里的代码的时候也觉得有点绕,所以我在这里做了大量的测试。结果发现,如果在onStable的订阅回调中再使用zone.run执行异步任务的时候就会造成一个无限的死循环。这里是我的最小实现仓,够胆的可以试试,你的浏览器会在瞬间崩溃。当然,我也给官方提了issue,原作者也证实了这的确是个问题,感兴趣的可以跟踪一下,持续关注。
截止到这里,我们再看一下NgZone的几个状态指标。此时队列中不存在待执行的任务,NgZone会把自身状态修改为稳定态。
hasPendingMacrotasks | hasPendingMicrotasks | _nesting | isStable | lastRequestAnimationFrameId |
---|---|---|---|---|
true | false | 0 | true | -1 |
onHasTask
整个setTimeout跟踪的最后一步还是这个勾子,这次,勾子函数中会把hasPendingMacrotasks置为false。此时,几个状态已经恢复为最初的问题状态,Angular也在这个过程中执行了一次变更监测。
hasPendingMacrotasks | hasPendingMicrotasks | _nesting | isStable | lastRequestAnimationFrameId |
---|---|---|---|---|
false | false | 0 | true | -1 |
MicroTask
MicroTask和MacroTask的行为大致上一致,只不过由于zone.js在处理MicroTask和MacroTask时有一丢丢的区别,导致这里处理也会有一点不同,这个我会在下文做专门解释。当然,如果你还想关注zone.js在处理MicroTask和MacroTask时到底有什么不一样的,可以关注一下我的下一篇文章(如果有的话),里面会像本系列的第三期一样,详细解释zone.js处理Promise的技术细节。
onHasTask
onHasTask跟之前没什么区别,执行过后,状态如下。与MacroTask不同,这次是hasPendingMicrotasks变为true,表示队列中有一个待执行的微任务。
onHasTask:
(delegate: ZoneDelegate, current: Zone, target: Zone, hasTaskState: HasTaskState) => {
delegate.hasTask(target, hasTaskState);
if (current === target) {
if (hasTaskState.change == 'microTask') {
zone._hasPendingMicrotasks = hasTaskState.microTask;
// ...
}
// ...
}
},
hasPendingMacrotasks | hasPendingMicrotasks | _nesting | isStable | lastRequestAnimationFrameId |
---|---|---|---|---|
false | true | 0 | true | -1 |
onInvokeTask
MicroTask和MacroTask在这个勾子中的处理过程基本上是相同的。但是MicroTask在回调执行的时候和MacroTask还是有一点差异的。前面部分,我们讲MacroTask的时候,delegate.invokeTask(target, task, applyThis, applyArgs);
这句会直接触发setTimeout的回调函数执行。但是在MicroTask中,微任务的回调外部还会包装一层zone.run
,导致MicroTask的回调会通过onInvoke
勾子执行。
onInvokeTask:
(delegate: ZoneDelegate, current: Zone, target: Zone, task: Task, applyThis: any,
applyArgs: any): any => {
try {
onEnter(zone);
// 执行真正的回调 👇
return delegate.invokeTask(target, task, applyThis, applyArgs);
} finally {
// ...
onLeave(zone);
}
},
onInvoke
可以看到onInvoke
和onInvokeTask
函数的内容是差不多的。onEnter
和onLeave
的调用也基本一致,所以这里就不专门分析了。
onInvoke:
(delegate: ZoneDelegate, current: Zone, target: Zone, callback: Function, applyThis: any,
applyArgs?: any[], source?: string): any => {
try {
onEnter(zone);
return delegate.invoke(target, callback, applyThis, applyArgs, source);
} finally {
onLeave(zone);
}
},
当这个函数执行结束后,几个状态值变化如下。
hasPendingMacrotasks | hasPendingMicrotasks | _nesting | isStable | lastRequestAnimationFrameId |
---|---|---|---|---|
false | true | 0 | true | -1 |
onHasTask
最后一个执行的还是onHasTask
函数,这个函数执行完毕后,几个状态又回到初始状态。
hasPendingMacrotasks | hasPendingMicrotasks | _nesting | isStable | lastRequestAnimationFrameId |
---|---|---|---|---|
false | false | 0 | true | -1 |
Event
Event的执行方式跟MacroTask和MicroTask都不太一样。还记得之前我们在讲NgZone的5大状态的时候,有一个lastRequestAnimationFrameId
一直没有用到。那么,在Event的处理过程中,我们会看到它的作用。
onInvokeTask
Event的处理入口是onInvokeTask
而不是onHasTask
,onEnter
和delegate.invokeTask
与之前都差不多,但是在finally子句中,你会发现Event的处理中多了一个delayChangeDetectionForEventsDelegate
函数。其实从函数的函数名大概能猜个七七八八,这个是一个事件延时处理的函数。
onInvokeTask:
(delegate: ZoneDelegate, current: Zone, target: Zone, task: Task, applyThis: any,
applyArgs: any): any => {
try {
onEnter(zone);
return delegate.invokeTask(target, task, applyThis, applyArgs);
} finally {
if ((zone.shouldCoalesceEventChangeDetection && task.type === 'eventTask') ||
zone.shouldCoalesceRunChangeDetection) {
// Event的特殊处理逻辑 👇
delayChangeDetectionForEventsDelegate();
}
onLeave(zone);
}
},
delayChangeDetectionForEventsDelegate
其实我们在上一期讲解中已经介绍了一些通过NgZone进行性能调优的手段,那么这个函数的产生实际上也是用于性能上的优化。我们知道,浏览器很多事件诸如mousemove、scroll这些都会在短时间内产生大量的事件。如果每个这样的事件都会触发一次Angular的变更检测的话,那么对性能上的要求是很大的。所以,NgZone也需要在内部对于这些浏览器的事件做一些特殊处理,让大量的事件积攒一段时间后再统一做一次变更检测。
那么delayChangeDetectionForEventsDelegate
中实际调用的方法是delayChangeDetectionForEvents
,所以我们重点关注一下delayChangeDetectionForEvents
函数的源码。
- 代码1这里,我们第一次见到对lastRequestAnimationFrameId的判断,当第一个Event到来时,这里的lastRequestAnimationFrameId还是初始值-1
hasPendingMacrotasks | hasPendingMicrotasks | _nesting | isStable | lastRequestAnimationFrameId |
---|---|---|---|---|
false | false | 1 | false | -1 |
zone.nativeRequestAnimationFrame
的调用实际上调用的是Window.requestAnimationFrame
。这里,NgZone实际上是希望通过requestAnimationFrame
收集这一帧内的所有事件,在这一帧结束后,再统一执行一次变更检测。requestAnimationFrame
执行的返回值会赋值给lastRequestAnimationFrameId
,这样,在接下来代码每次进入到代码1处的时候,函数会直接返回。updateMicroTaskStatus
被用来更新微任务状态的。那么这里执行之后,状态值中的hasPendingMicrotasks会变为true。这里这么做是为了收集Event的时候可以阻塞微任务触发变更检测,这么做的原因是为了确保Event事件的执行顺序不会被微任务打乱。这里要详细介绍又会有很大篇幅,感兴趣的可以自己看下这个issue;不想关注的可以先跳过这里。
hasPendingMacrotasks | hasPendingMicrotasks | _nesting | isStable | lastRequestAnimationFrameId |
---|---|---|---|---|
false | true | 1 | false | 一个正整数返回值 |
- 当当前帧执行完毕、下一帧要执行的时候会调用一次
checkStable
函数。这个函数在前面讲过,它是触发Angular变更检测的关键。通过执行该方法,Angular会通过ApplicationRef执行变更检测动作。
function delayChangeDetectionForEvents(zone: NgZonePrivate) {
// 👇 1
if (zone.isCheckStableRunning || zone.lastRequestAnimationFrameId !== -1) {
return;
}
// 👇 2
zone.lastRequestAnimationFrameId = zone.nativeRequestAnimationFrame.call(global, () => {
if (!zone.fakeTopEventTask) {
zone.fakeTopEventTask = Zone.root.scheduleEventTask('fakeTopEventTask', () => {
zone.lastRequestAnimationFrameId = -1;
updateMicroTaskStatus(zone);
zone.isCheckStableRunning = true;
// 👇 4
checkStable(zone);
zone.isCheckStableRunning = false;
}, undefined, () => {}, () => {});
}
zone.fakeTopEventTask.invoke();
});
// 👇 3
updateMicroTaskStatus(zone);
}
再见ApplicationRef
上一节中,我们讲过一点ApplicationRef相关的知识,这一次,我们重点看下ApplicationRef跟变更检测相关的代码。
变更检测
前面说到,NgZone在checkStable
中,如果发现当前已经没有待执行的任务的时候,会触发一个onMicrotaskEmpty
事件。在这里,这个事件会被ApplicationRef所捕获。捕获后,会执行ApplicationRef.tick
,而这个tick就是变更检测的入口。
this._onMicrotaskEmptySubscription = this._zone.onMicrotaskEmpty.subscribe({
next: () => {
this._zone.run(() => {
this.tick();
});
},
});
tick
在tick
方法中,我们可以看到ApplicationRef通过调用视图的detectChanges
方法,让组件完成自上而下的变更检测。上一篇文章中,我们介绍过一些手动执行变更检测的方法,其中有提到过ChangeDetectorRef.detectChanges()
这个方法。这个方法可以对当前组件以及当前组件的子组件进行进行变更检测。那么这里看到的view.detectChanges()
跟ChangeDetectorRef.detectChanges()
又有什么关系?
其实从_views
类型的继承链可以发现,_views
的类型InternalViewRef
继承自ViewRef
,ViewRef
又继承自ChangeDetectorRef
。所以调用view.detectChanges()
就相当于调用了ChangeDetectorRef.detectChanges()
,从而完成一次自上而下的变更检测。
tick(): void {
// ...
try {
this._runningTick = true;
for (let view of this._views) {
// 👇 组件的变更检测
view.detectChanges();
}
if (typeof ngDevMode === 'undefined' || ngDevMode) {
for (let view of this._views) {
view.checkNoChanges();
}
}
} catch (e) {
// Attention: Don't rethrow as it could cancel subscriptions to Observables!
this._zone.runOutsideAngular(() => this._exceptionHandler.handleError(e));
} finally {
this._runningTick = false;
}
}
以上就是NgZone和ApplicationRef之间的配合关系。我们整体再回顾一下整个系列课程的内容。zone.js通过Monkey Patch对所有异步方法进行打包;打包后的异步方法被植入了很多勾子函数,而这些勾子函数可以被zone.js的上下文检测到,从而完成对异步任务的监控。
NgZone是对zone.js的一个使用案例,NgZone通过维护InnerZone和OuterZone两个Zone实现了对Angular应用中的异步任务的监控和去监控。NgZone同时在内部也维护了几个对异步任务监控的状态信息,通过这些信息实现了和ApplicationRef之间的“通信”,最终由ApplicationRef完成对Angular应用的监控。
本文小结
到这里,今天的内容就介绍的差不多了。最后,这里还需要像读者说明一点,在NgZone中跟踪Task的运行是一件比较难的事情,本文所有这些Task的举例其实都是理想化的。比如说,在举例setTimeout的时候,你会发现当你想在Angular应用中对异步Task跟踪的时候,会有很多其它Task同时在执行着,这些Task经常会在你调试跟踪的时候对你形成“干扰”。所以,本文的这些举例只是希望让大家看过后,能大致对每种不同任务在NgZone中流程有个认识,而真实的过程会远比我今天讲的内容复杂的多。这同时也从侧面反映出,zone.js默默对Angular作出多大的贡献。
大完结
本系列分享历时将近1个多月,加上前期的一些分析和总结,我个人大概持续关注zone.js有两个多月了。最后的最后,我也分享几点个人感受:
- 有人说zone.js是暴力美学,我个人感觉可能美的地方更多一些吧。作为Angular变更检测的核心,Angular的变更检测在三大框架中是独一份的存在。我觉得比起其它两个通过数据劫持和虚拟Dom的方式进行数据绑定的方式,zone.js显得还是要温柔一些的。毕竟数据劫持是直接“污染”了数据的,而zone.js“改造”的是工具。我没法说谁更好,只是个人更偏向于后者。
- 截止到现在,我个人也没有完全看完zone.js的源码,但是我希望我会在后续的工作中持续关注这个产品。同时我也看到JiaLi(zone.js作者)为了他的作品不断地对zone.js进行改进。所以,请他加油,我希望zone.js可以越来越好!不过话说回来,JiaLi想在Angular社区完成一个PR是不是太难了点啊。我看了他好多的修改,经常要等好久才能审核通过,有点心疼他。🤣
其实最开始的时候,我只是想自己学学zone.js的,并没有规划这个系列分享。但是,我在学习源码的时候,苦于能找到的资料太旧又太少,所以就准备自己写一个有史以来最通俗、最全面、也最适合中国人学的zone.js材料。当然,前两个“最”我可能还不配;但是第三个最,我觉得还是可以搏一搏的✌。
OpenTiny Vue招募贡献者啦!
OpenTiny Vue 正在招募社区贡献者,欢迎加入我们🎉
你可以通过以下方式参与贡献:
- 在 issue 列表中选择自己喜欢的任务
- 阅读贡献者指南,开始参与贡献
你可以根据自己的喜好认领以下类型的任务:
- 编写单元测试
- 修复组件缺陷
- 为组件添加新特性
- 完善组件的文档
如何贡献单元测试:
- 在
packages/vue
目录下搜索it.todo
关键字,找到待补充的单元测试 - 按照以上指南编写组件单元测试
- 执行单个组件的单元测试:
pnpm test:unit3 button
如果你是一位经验丰富的开发者,想接受一些有挑战的任务,可以考虑以下任务:
- ✨ [Feature]: 希望提供 Skeleton 骨架屏组件
- ✨ [Feature]: 希望提供 Divider 分割线组件
- ✨ [Feature]: tree树形控件能增加虚拟滚动功能
- ✨ [Feature]: 增加视频播放组件
- ✨ [Feature]: 增加思维导图组件
- ✨ [Feature]: 添加类似飞书的多维表格组件
- ✨ [Feature]: 添加到 unplugin-vue-components
- ✨ [Feature]: 兼容formily
参与 OpenTiny 开源社区贡献,你将收获:
直接的价值:
- 通过参与一个实际的跨端、跨框架组件库项目,学习最新的
Vite
+Vue3
+TypeScript
+Vitest
技术 - 学习从 0 到 1 搭建一个自己的组件库的整套流程和方法论,包括组件库工程化、组件的设计和开发等
- 为自己的简历和职业生涯添彩,参与过优秀的开源项目,这本身就是受面试官青睐的亮点
- 结识一群优秀的、热爱学习、热爱开源的小伙伴,大家一起打造一个伟大的产品
长远的价值:
- 打造个人品牌,提升个人影响力
- 培养良好的编码习惯
- 获得华为云 OpenTiny 团队的荣誉和定制小礼物
- 受邀参加各类技术大会
- 成为 PMC 和 Committer 之后还能参与 OpenTiny 整个开源生态的决策和长远规划,培养自己的管理和规划能力
- 未来有更多机会和可能
其他说明
OpenTiny 是一套企业级组件库解决方案,适配 PC 端 / 移动端等多端,涵盖 Vue2 / Vue3 / Angular 多技术栈,拥有主题配置系统 / 中后台模板 / CLI 命令行等效率提升工具,可帮助开发者高效开发 Web 应用。
核心亮点:
- 跨端跨框架: 使用 Renderless 无渲染组件设计架构,实现了一套代码同时支持 Vue2 / Vue3,PC / Mobile 端,并支持函数级别的逻辑定制和全模板替换,灵活性好、二次开发能力强
- 组件丰富:PC 端有100+组件,移动端有30+组件,包含高频组件 Table、Tree、Select 等,内置虚拟滚动,保证大数据场景下的流畅体验,除了业界常见组件之外,我们还提供了一些独有的特色组件,如:Split 面板分割器、IpAddress IP地址输入框、Calendar 日历、Crop 图片裁切等
- 配置式组件: 组件支持模板式和配置式两种使用方式,适合低代码平台,目前团队已经将 OpenTiny 集成到内部的低代码平台,针对低码平台做了大量优化
- 周边生态齐全: 提供了基于 Angular + TypeScript 的 TinyNG 组件库,提供包含 10+ 实用功能、20+ 典型页面的 TinyPro 中后台模板,提供覆盖前端开发全流程的 TinyCLI 工程化工具,提供强大的在线主题配置平台 TinyTheme
欢迎加入 OpenTiny 开源社区。添加微信小助手:opentiny-official 一起参与交流前端技术~
OpenTiny 代码仓库:https://github.com/opentiny/
Vue组件库:opentiny.design/tiny-vue
Angular组件库:opentiny.design/tiny-ng
欢迎进入代码仓库 Star🌟TinyVue、TinyNG、TinyCLI~
如果你也想要共建,可以进入代码仓库,找到 good first issue
标签,一起参与开源贡献~
往期文章推荐
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。