大象是堵墙 發表於 2019-11-7 00:39:00

Angular 学习笔记 (动态组件 & Material Overlay & Dialog 分析)

<p><strong>更新 : 2020-6-22</strong></p>
<p>当 ngtemplate 被丢到千里之外, detech change 失效</p>
<p>refer issue :&nbsp;https://github.com/angular/vscode-ng-language-service/issues/824</p>
<p>举个例子,&nbsp;</p>
<p>比如我们把一个 template 传进去 mat dialog 里面,然后让 dialog 里面负责 container.insert template&nbsp;</p>
<p>template 的 “家” 是打开 dialog 的组件. 假设 template 里面有一个 click 事件会修改外面组件的值.</p>
<p>但是呢,你会发现点击的时候并不会更新.</p>
<p><img src="https://img2020.cnblogs.com/blog/641294/202006/641294-20200622202524280-1115915892.png"></p>
<p>&nbsp;</p>
<p>如果是 index 在 template 内就 ok.&nbsp;</p>
<p>如果你希望它更新的话,那么 click 就要写 cdr.markForCheck 咯。</p>
<p>所以要记得哦,template 被传去千里之外后,它的家就失去对它的监听了.&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p><strong>更新: 2020-06-22</strong></p>
<p>当 ngZone.onStable 遇上&nbsp;container.createEmbeddedView</p>
<p>今天遇到了一个坑.&nbsp;</p>
<p>我们知道 container insert 是不会帮我们做 detect change 的.&nbsp;</p>
<p>但是很奇怪哦,如果在 AfterViewInit 的时候我们 insert template 是 ok 的.&nbsp;</p>
<p>但是如果你是在 ngZone.onStable 里面 insert template 那就不 ok 了</p>
<p>没有花太多时间去研究这个,但是推测应该是 after view 以后其实还是会往下去检测的.</p>
<p>但是 stable 后就肯定是不会了啦.</p>
<p>当然不管是上面那一个都不太逻辑. 因为我们应该要确保 insert 以后一定要 detect change 丫.</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p><strong>更新: 2020-06-01</strong></p>
<p>使用 flexible-connected-position 时, append component after view init 的时候 host element client width 是 1 ?!&nbsp;</p>
<p>今天踩了一个坑</p>
<p>material 的 overlay 在处理 flexible connected position 的时候会有一个&nbsp;</p>
<p>BoundingBox 然后是 panel 然后是我们 append 的组件</p>
<div>我们定义的 width 会被写到 panel 上, 可是呢, penel 一开始有一个 max-weight 100%&nbsp;</div>
<div>也就是要看 parent 脸色, 而 bounding box 这个 parent 是一个 absolution 然后没有定义 width, 只有 min-width 1px&nbsp;</div>
<div>所以一开始的时候 panel 就是 1px 咯</div>
<div>一直到 onstable 以后, overlay 才会去计算, 然后把 boundingbox 的 width set 成 100%, 然后 panel 的 width 才是对的.</div>
<div>而我在组件的 after view init 去获取 width 这个时候就是 1px.&nbsp;</div>
<div>不是很清楚 material overlay 的原理,所以目前的解决方向就是闪....写一个 request animation 就 ok 了。</div>
<div>但是还是要记入好好,可能会有其它隐患出现.</div>
<div>&nbsp;</div>
<p>&nbsp;</p>
<p><strong>更新 :&nbsp; 2020-02-13</strong></p>
<p><strong>关于 position 的细节</strong></p>
<p><strong>1.&nbsp;</strong>withFlexibleDimensions(true)&nbsp;</p>
<p>默认是 true&nbsp;</p>
<p>overlay 在决定 position 时, 它会依据我们给的先后 position&nbsp;</p>
<p>顺序去看,如果其中一个可以完全显示就马上用那个。</p>
<p><img src="https://img2018.cnblogs.com/i-beta/641294/202002/641294-20200213155352355-138363072.png"></p>
<p>&nbsp;</p>
<p>如果全部都不能完整显示,那么就要试试看调位置, 然后比分数</p>
<p><img src="https://img2018.cnblogs.com/i-beta/641294/202002/641294-20200213155442656-1515204872.png"></p>
<p>&nbsp;</p>
<p>&nbsp;然后是这样</p>
<p><img src="https://img2018.cnblogs.com/i-beta/641294/202002/641294-20200213155533340-1046166376.png"></p>
<p>&nbsp;</p>
<p>&nbsp;最后比分数</p>
<p><img src="https://img2018.cnblogs.com/i-beta/641294/202002/641294-20200213160919974-2091660697.png"></p>
<p>&nbsp;</p>
<p>&nbsp;所以这个是配合 minHeight, minWidth 来使用的.&nbsp;</p>
<p>&nbsp;</p>
<p>还有一个设置也要留意</p>
<div>withLockedPosition(true)</div>
<div>overlay 在 2 种情况下会 reposition</div>
<div>一个是 on scroll 一个是 window resize</div>
<div>window resize 的情况下, position 100% 会重新计算, 不管有没有 lock&nbsp;</div>
<div>而 scroll 则是可以被 lock 的.&nbsp;</div>
<div><img src="https://img2018.cnblogs.com/i-beta/641294/202002/641294-20200213161204242-1726693666.png">
<p>window resize 之所以可以绕过 lock 是通过&nbsp;_isInitialRender 实现的</p>
<p><img src="https://img2018.cnblogs.com/i-beta/641294/202002/641294-20200213161242734-1967846161.png"></p>
<p>另外还有一种就是我们手动调用 updatePosition.&nbsp;</p>
<p>如果我们的内容是动态的,或者是 ajax 比较慢加载回来的话, 那么一般上我们都需要使用这个 reposition 来 update 一下.&nbsp;</p>
<p>总结 :&nbsp;</p>
<p>3 种情况下我们会需要 update position&nbsp;</p>
<p>1. onscroll, (lock/nolock)</p>
<p>2. on resize, (no lock)</p>
<p>3. on content change (lock/nolock)</p>
<p>遇到的问题是 1,3 只可以用同一个config, 比如你 lock 那么 2 个都得 lock, 如果 nolock 2个都得 nolock</p>
<p>因为目前 material 并没有给多得接口用, 除非我们调用 private 的&nbsp;_isInitialRender</p>
<p>&nbsp;</p>
</div>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p><strong>更新: 2019-11-24&nbsp;</strong></p>
<p>dialog vs router link&nbsp;</p>
<p>refer :&nbsp;https://stackoverflow.com/questions/51821766/angular-material-dialog-not-closing-after-navigation</p>
<p>今天发现一些场景可能导致 dialog 不会关闭. 比如当子组件打开一个 dialog 后</p>
<p>某一个操作把父组件给销毁了.这个时候 dialog content 会一起销毁掉,&nbsp;</p>
<p>因为 content 是 under 这个逻辑树中 (当然如果你是放到 appRef 里头就另外说)</p>
<p>content 销毁了,但是 overlay 留在 body 丫.&nbsp;</p>
<p><img src="https://img2018.cnblogs.com/blog/641294/201911/641294-20191124171716924-365069836.png"></p>
<p>&nbsp;</p>
<p>material team 有考虑到这种情况所以做了一个 fallback 机制, 但是这个并不能解决上面的问题,因为无论如何 dialog 要求至少要启动 animation start&nbsp;</p>
<p>如果是 router link 切换的话,渲染会在同一个 detech change 下完成,所以 animation start 是不会被触发的。</p>
<p>目前 dialog 没有提供 displose 的方法,所以基本上不无法做到的,除非你去监听 router event 之类的。</p>
<p>那我觉得比较合理的处理方式是。如果组件负责打开 dialog or overlay&nbsp;</p>
<p>那么当这个组件 onDestroy 的时候,必须要确保它负责的 overlay 一定要 displose. 为此 dialog 应该要公开这个接口让我们使用的.&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p><strong>更新 : 2019-11-14&nbsp;</strong></p>
<p>小总结一下&nbsp;angular 动态组件 -&gt; portal -&gt; overlay -&gt; dialog&nbsp;</p>
<div>ComponentFactoryResolver 是我们用 angular 做动态组件的基础. cdk, material 都是基于它的。</div>
<div>在 ng 要做一个动态组件是这样的</div>
<div>&nbsp;</div>
<div>注入 factory resolver 服务</div>
<div>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">constructor(
private componentFactoryResolver: ComponentFactoryResolver,
) { }</span></pre>
</div>
<p>&nbsp;</p>
<p>制作出组件工厂, 把动态组件丢进去就可以了</p>
<div class="cnblogs_code">
<pre>const componentFactory = <span style="color: rgba(0, 0, 255, 1)">this</span>.componentFactoryResolver.resolveComponentFactory(DynamicComponent);</pre>
</div>
<p>&nbsp;</p>
<p>然后就可以创建组件实例了,这个时候需要基于一个注入器</p>
<p>通过 Injector.create 创建出一个新的 injector 并且继承 parent injector, ng 的 injector 有分层的概念 ngModule 的 provider 通常会放到 root injector 里头, lazy load module 则像下面那样创建出第 2 层级的 injector&nbsp;</p>
<div class="cnblogs_code">
<pre>const componentFactory = <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.componentFactoryResolver.resolveComponentFactory(DynamicComponent);
const injector </span>=<span style="color: rgba(0, 0, 0, 1)"> Injector.create({
providers: [{ provide: </span>'extraProvider', useValue: 'dada'<span style="color: rgba(0, 0, 0, 1)"> }],
parent: </span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.injector
});
const componentRef </span>= componentFactory.create(<span style="color: rgba(0, 0, 255, 1)">this</span>.injector);</pre>
</div>
<p>这个时候组件就已经被实例化了,但是还没有发生 detech change, OnInit 也还没跑.</p>
<p>这个时候组件是独立的,我们知道 angular 把所有东西看成 VIew&nbsp;</p>
<p>组件就是组件 view, 模板就是 embedded view</p>
<p>然后所有 view 都必须放到 logical view tree 里头. 这样 change detech 才能遍历执行</p>
<p>所以现在组件创建好以后,我们需要给它一个家.&nbsp;</p>
<p>可以是 ViewContainerRef 或 ApplicationRef</p>
<p>像这样</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.applicationRef.attachView(componentRef.hostView);
</span><span style="color: rgba(0, 0, 255, 1)">this</span>.container.insert(componentRef.hostView, 0);</pre>
</div>
<p>2 者最大的区别是在 detech change 上, 如果你放到 app 里头, 那么组件是在最上层, 一旦 app.tick 触发.&nbsp;此组件就会触发 doCheck&nbsp;</p>
<p>如果你是放到 container 里头,那么要看这个 container 在 logical tree 里面的第几层. app.tick 时就不一定触发 doCheck 了,要看 detech change 有没有流到这一层里头 (OnPush 的情况下)</p>
<p>此外, applicationRef.attachVIew 和 container 还有一个不同是, appRef attach 并不会把 dom append 出去</p>
<p>它只是把组件放进去 logical view tree 而且,并没有 append to dom.&nbsp;</p>
<p>view container 则会做这个事情.&nbsp;</p>
<p>那我们得自己搞, 比如...&nbsp;</p>
<div class="cnblogs_code">
<pre>document.body.appendChild((componentRef.hostView as EmbeddedViewRef&lt;any&gt;).rootNodes as HTMLElement);</pre>
</div>
<p>当然绝大部分情况下,我们应该使用 view container 因为这个是官方教我们正确插入 dom 的方式.</p>
<p>上面这个通常是用在 dialog 那种要 append to body 最外层的情况. 由于那里已经脱离的 angular 的 scope 所以我们得自己弄.&nbsp;</p>
<p><span style="text-decoration: line-through">不管是 container.insert 还是 appRef.attachView 调用后,组件就会被 detech change OnInit 了</span>.&nbsp;</p>
<p>组件 append 出去或需要一个 detech change 的 cycle 才会渲染哦. viewContainer 并不会替我们渲染组件. 它只是单纯的 append 而已.</p>
<p>&nbsp;</p>
<p>注意 :&nbsp;</p>
<p>动态组件的 detech change 是比较难懂的. 我是在发现问题,看了源码之后,找特定关键字才找到了相关的文章</p>
<p>https://netbasal.com/things-worth-knowing-about-dynamic-components-in-angular-166ce136b3eb</p>
<p><img src="https://img2018.cnblogs.com/blog/641294/201911/641294-20191114213938306-1735587878.png"></p>
<p>意思是 componentRef.hostview.detechChange 只会让 component DoCheck 而已.&nbsp;</p>
<p>因为 componentRef.hostView 并不是 componentRef.instance.changeDetectorRef&nbsp;</p>
<p>componentRef.hostView 是一个 RootView 而不是 LView</p>
<p><img src="https://img2018.cnblogs.com/blog/641294/201911/641294-20191114214215506-1004295905.png"></p>
<p>RootView 重写了 detech change</p>
<p><img src="https://img2018.cnblogs.com/blog/641294/201911/641294-20191114214247939-302511057.png"></p>
<p>LView 的 detechChanges 是这样的</p>
<p><img src="https://img2018.cnblogs.com/blog/641294/201911/641294-20191114214353564-93547985.png"></p>
<p>&nbsp;所以你会发现 hostView['_lView'] === instance.changeDetectorRef['_IView']&nbsp;</p>
<p>但是 detechChange 却不一样. 至于为什么这样设计我也不清楚. 总之只有动态 create 出来的 component 才会有这个 RootViewRef&nbsp;</p>
<p>那么问题来了, 外部如何让内部 detech change 呢 ?&nbsp;</p>
<p>第一种方法就是传 rxjs 流进去咯. 里面监听然后 mark for check.</p>
<p>第二种是通过 componentRef.injector 获取到内部的 changeDetectorRef, 然后调用 markForCheck</p>
<p><img src="https://img2018.cnblogs.com/blog/641294/201911/641294-20191114214702291-1425538336.png"></p>
<p>此外别无它法, componentRef.detechChange 由于是上层,它只能让 component DoCheck 而已. 记住了。</p>
<p>所以,记住这几个点.</p>
<p>1.&nbsp;componentFactory.create&nbsp;</p>
<p>这个时候组件只是被实例化, 没有 detech change, 没有 OnInit 没有 DoCheck.此时它也没有在 logical tree 里头</p>
<p>2. ComponentRef.hostView 是 RootViewRef 而不是平常我们看到的 LView&nbsp;</p>
<p>RootViewRef 重写了 changesDetech 方法,所以当我们调用 hostView.changesDetech 的时候,我们的组件并没有渲染, 因为它执行的是 component 的上一层, 这只会让 component 执行 DoCheck 而已.&nbsp;</p>
<p>3. appRef.attachView(componentRef.hostView)&nbsp;</p>
<p>插入到 logical tree 顶端. 每一次 app.tick 就会被执行 hostView.detechChange. &lt;-- 记住它只是让 component DoCheck 而不是 render.&nbsp;</p>
<p>appRef.attach 不会 append dom, 我们需要自己写代码去 append dom.</p>
<p>4.container.insert&nbsp;</p>
<p>插入到 container 这一层级的 logical tree, app.tick 如果有流到这一次就会被 detech change. &lt;-- 还是一样它只能让 component DoCheck 而不是 render.&nbsp;</p>
<p>会 append dom 到 container 的位置.</p>
<p>5. 唯一能让 component detech change render 的方式是传一个 rxjs 流进去, 或者通过 componentRef.injector 获取到内部的 ViewRef (也就是 ChangeDetectorRef)</p>
<p>&nbsp;</p>
<p>好说完 component,现在说说 template&nbsp;</p>
<p>template 是通过 &lt;ng-template&gt; 制作出来的。</p>
<div class="cnblogs_code">
<pre>const context =<span style="color: rgba(0, 0, 0, 1)"> {};
const viewRef </span>= <span style="color: rgba(0, 0, 255, 1)">this</span>.templateRef.createEmbeddedView(context);</pre>
</div>
<p>和 component 相同的,这个时候 viewRef 还没有被 detech change. 也没有在 logical tree 里头.</p>
<p>我们可以通过 appRef.attachView 或者 container.insert 让它插入到 logical tree 里头.</p>
<p>这里主要说说它和 component 不同的地方.</p>
<p>首先它没有 RootView 这个概念, 我们获取到的就是 ViewRef.&nbsp;</p>
<p>另外 template 和 component 一个很大的区别在于它的通讯值.&nbsp;</p>
<p>template 本身就被定义在某个 component 当中, 然后又被丢到另一个可能千里之外的 component.container 里头.</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">&lt;</span><span style="color: rgba(128, 0, 0, 1)">ng-template </span><span style="color: rgba(255, 0, 0, 1)">#template let-age</span><span style="color: rgba(0, 0, 255, 1)">="age"</span> <span style="color: rgba(0, 0, 255, 1)">&gt;</span><span style="color: rgba(0, 0, 0, 1)">
{{ value }} and {{ age }}
</span><span style="color: rgba(0, 0, 255, 1)">&lt;/</span><span style="color: rgba(128, 0, 0, 1)">ng-template</span><span style="color: rgba(0, 0, 255, 1)">&gt;</span></pre>
</div>
<p>value 来自定义 template 的 component, age 来自使用 template 的 component.&nbsp;</p>
<p>那它的 detech change 是这样工作的。</p>
<p>当定义它的 component 发生 detech change 时, value 就被更新了, 使用它的组件并不会因此触发 detech change 之类的, age 也不会从新去拿.&nbsp;</p>
<p>就只是更新了 value 然后渲染出效果而已. 如果你在期中偷偷的修改了 age,ng 是不会发现的, 因为它不会去 get age.&nbsp;</p>
<p>反过来如果是使用 template 的组件做了 detech change, 定义它的组件也不会发生 detech change, 但是呢 value 却会去 getter 一下 (这里和 age 的表现不相同).</p>
<p>&nbsp;</p>
<p>上面说了 component 和 template 的基本用法和 detech change 的更新机制.</p>
<p>现在说说 cdk 的 portal&nbsp;</p>
<p>cdk 提我们封装了上面这些动态创建 component 和 template 的方法. 其实也没有什么好封装的啦,不就那几行...</p>
<p>但是还是得要搞清楚它是怎样用起来的。</p>
<p>我们通常会用到是</p>
<p>ComponentPortal&nbsp;</p>
<p><img src="https://img2018.cnblogs.com/blog/641294/201911/641294-20191115003920560-1891972737.png"></p>
<p>&nbsp;</p>
<p>4个都很合理,我们上一段都有用到这些. 至于为什么可以替换 componentFactory 呢, 这个不是很清楚,不是都一样的吗.. ?&nbsp;</p>
<p>另一个可能会用到得是&nbsp;</p>
<div>PortalInjector</div>
<p><img src="https://img2018.cnblogs.com/blog/641294/201911/641294-20191115004417485-563190639.png"></p>
<p>mat dialog 传递 data 和 dialogRef 就是用这种简单方式做的. 它不像上一段使用 Injector.create 然后提供 provider</p>
<p>它只是用一个 weakmap 来实现而已.&nbsp;</p>
<p>然后是 TemplatePortal&nbsp;</p>
<p><img src="https://img2018.cnblogs.com/blog/641294/201911/641294-20191115004709119-1390023882.png"></p>
<p>&nbsp;</p>
<p>&nbsp;都是动态创建需要的东西.&nbsp;</p>
<p>TemplatePortal 和 ComponentPortal 都继承了 Portal 类</p>
<p><img src="https://img2018.cnblogs.com/blog/641294/201911/641294-20191115004940818-389519035.png"></p>
<p>&nbsp;</p>
<p>&nbsp;没什么特别的,只是有 attach 和 detech 的方法而已。然后主要, 这 2 个方法其实内部是调用了 outlet.attach 和 detech&nbsp;</p>
<p>也就是说逻辑根本没有写在这里,这 2 个方法只是一个委托方法而已,除了让初学者乱没有看出其它意义.</p>
<p>再来一个 DomPortal</p>
<p><img src="https://img2018.cnblogs.com/blog/641294/201911/641294-20191115005216581-1839105192.png"></p>
<p>&nbsp;很简单,就是放了一个 dom 在里面...</p>
<p>最后是 PortalOutlet, outlet 的职责是 container 用来 append dom 的,</p>
<p>这个是最常用到的,可以简单理解它就是 container&nbsp;</p>
<p><img src="https://img2018.cnblogs.com/blog/641294/201911/641294-20191115005454894-1296543699.png"></p>
<p>&nbsp;另一个是专门给 append body 用的,类似上段我们说的 appRef 然后自己 append body&nbsp;</p>
<p><img src="https://img2018.cnblogs.com/blog/641294/201911/641294-20191115005632738-106298273.png"></p>
<p>&nbsp;整个 portal 看下来没有什么奇特的地方,就真的只是封装而已.&nbsp;</p>
<p>我觉得最需要懂得逻辑在这里, portal.attach&nbsp;</p>
<p>cdk outlet 和 dom outlet 区别就在于此</p>
<p>先看看 ckd outlet attach template&nbsp;</p>
<p><img src="https://img2018.cnblogs.com/blog/641294/201911/641294-20191115010003312-1972994900.png"></p>
<p>关键在 viewContainerRef, 这个 container 说的是 outlet 这个指令依赖注入得 container (outlet 这个位置)</p>
<p>再看看 ckd outlet attach component&nbsp;</p>
<p><img src="https://img2018.cnblogs.com/blog/641294/201911/641294-20191115010130704-747380762.png"></p>
<p>关键也在 container, 如果 portal 本身有 container, 就用,不然就用 outlet 的.&nbsp;</p>
<p>这里和刚才 template 不同,template 没有判断 portal 是否有 container&nbsp;</p>
<p>这样的设计我是觉得挺奇怪的,我把 portal 交给了 outlet 结果, portal 被 attach 在原本的 container 里, 这里关 outlet 什么是呢 ?&nbsp;</p>
<p>然后 outlet 被 destroy 时也 destroy 掉 portal ?&nbsp;</p>
<p>我们把疑点留着,等下一起讨论. 继续往下走.</p>
<p>这是 dom outlet 的 attach template&nbsp;</p>
<p><img src="https://img2018.cnblogs.com/blog/641294/201911/641294-20191115010633729-213311089.png"></p>
<p>&nbsp;dom outlet 不是指令,它是 new 出来的,所以它本身不会有 view container ref, 所以 portal 理应要有 view container ref&nbsp;</p>
<p>这里没有任何判断就直接使用了 portal.viewContainerRef ... 挺勇敢的嘛...</p>
<p>此外这里还有一个 cut and paste 的动作. 当 portal 内容 attach 到 container 后, 这里做了一个 dom 操作就是把内容 cut and paste 到 outlet element 里头.</p>
<p>这应该是和 cdk outlet 最大的不同点.&nbsp;</p>
<p>最后是 dom outlet 的 attach component&nbsp;</p>
<p><img src="https://img2018.cnblogs.com/blog/641294/201911/641294-20191115011053176-1783959333.png"></p>
<p>&nbsp;</p>
<p>&nbsp;这里有做 viewcontainer 判断, 和 template 的处理手法不同.&nbsp;</p>
<p>如果 portal 没有 viewcontainer 那么就放到 appRef 里头. 最后依然会 cut and paste.&nbsp;</p>
<p>解析完了。总结一下我觉得不太容易理解的几个点.</p>
<p>1. outlet 决定位置</p>
<p>cdk outlet attach template, dom outlet attach component/template 都可以确保最终的内容渲染在 outlet 位置上.&nbsp;</p>
<p>但是 cdk outlet attach component 却不是这样..</p>
<p>这个确定是一个 bug,&nbsp;https://github.com/angular/components/issues/17650&nbsp;-&gt;&nbsp;https://github.com/angular/components/pull/17731</p>
<p>所以 cdk outlet 的会确保最终内容出现在 outlet 里</p>
<p>2. cdk outlet append component 时,有可能把 component 系在 appRef 上, 但是 append template 时却不会这样.&nbsp;</p>
<p>反而强制要求 portal 一定要有 container (我的想到的一个解是, template 定义在 component 里,所以它肯定是有 container 的)</p>
<p>在我的理解中. template 和 component 应该保持行为一致. 让使用者决定要用哪一种. 可以简单的替换.&nbsp;</p>
<p>可能 cdk 很灵活时因为 mat 需要这么灵活. 但对我来说这些不一致会导致维护起来比较麻烦.&nbsp;</p>
<p>所以我的做法通常是 portal 不需要带 container 逻辑.&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
</div>
<p>&nbsp;</p>
<p><strong>更新: 2019-11-08&nbsp;</strong></p>
<p>记入一下 overlay 的使用</p>
<p>material 有 8 个组件用到了 overlay&nbsp;</p>
<p>autocomplete<br>datepicker<br>select<br>menu<br>bottom sheet<br>dialog<br>snackbar <br>tooltip</p>
<p>在真实项目中,还有很多组件是没有的. 比如&nbsp;</p>
<p>小 form&nbsp;</p>
<p><img src="https://img2018.cnblogs.com/blog/641294/201911/641294-20191108093536268-1250396563.gif"></p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;比如大 message tip</p>
<p><img src="https://img2018.cnblogs.com/blog/641294/201911/641294-20191108093618287-195552731.gif"></p>
<p>&nbsp;</p>
<p>这些都得我们自己去实现. 所以就需要用到 overlay 了.</p>
<p>先说说它的过程</p>
<p>当我们调用 overlay.create 的时候, overlay 会在 body 层创建一个 div&nbsp;</p>
<p>然后依据我们的 width height 在放一个 div 在里面 (其实好像有 3 - 4 层 div)</p>
<p>如果我们要 backdrop 也可以通过 overlay 设置.&nbsp;</p>
<p>有了 backdrop 我们就可以监听点击事件然后关掉 overlay 了.&nbsp;</p>
<p>这里有一个小体验. 很久以前,我是用 body click + stop bubble 来实现这种 modal close 的. 后来发现大家都用 overlay + 透明 backdrop 来做</p>
<p>省去了不少麻烦. stop bubble 在多层次的情况下不太好处理, 但是这个做法也有它的局限. 比如只能 body scroll 因为 backdrop 在最上层, 会把其它 div 挡住, 如果我们依赖其它 div 来做 scroll&nbsp;</p>
<p>那么就 scroll 不了的. 所以多用 body scroll 还是比较正确的姿势.&nbsp;</p>
<p>我还发现一个小秘密,就是 material tooltip 没有使用 backdrop 但是缺可以点击 body 关闭. 它也是通过监听 body click 实现的,因为 tooltip 内只可以是字, 所以不会有点击事件也就不需要顾虑 bubble 的问题. 很巧妙的在设计上躲过了实现的难题.&nbsp;</p>
<p>做小 modal 要搞懂 position strategy</p>
<div class="cnblogs_code">
<pre>const positionStrategy = <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.overlay.position()
.flexibleConnectedTo(origin)
.withTransformOriginOn(</span>'.transformOrigin'<span style="color: rgba(0, 0, 0, 1)">)
.withFlexibleDimensions(</span><span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">)
.withViewportMargin(</span>8<span style="color: rgba(0, 0, 0, 1)">)
.withPush(</span><span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">)
.withLockedPosition(</span><span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">)
.withPositions([
    { originX: </span>'start', originY: 'top', overlayX: 'start', overlayY: 'top'<span style="color: rgba(0, 0, 0, 1)"> },
    { originX: </span>'start', originY: 'top', overlayX: 'start', overlayY: 'bottom'<span style="color: rgba(0, 0, 0, 1)"> },
    { originX: </span>'end', originY: 'top', overlayX: 'end', overlayY: 'top'<span style="color: rgba(0, 0, 0, 1)"> },
    { originX: </span>'end', originY: 'top', overlayX: 'end', overlayY: 'bottom'<span style="color: rgba(0, 0, 0, 1)"> }
]);

const overlayRef </span>= <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.overlay.create({
    positionStrategy,
    scrollStrategy: </span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.overlay.scrollStrategies.reposition(),
    hasBackdrop: </span><span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">,
});</span></pre>
</div>
<p>scroll strategy 用的是 reposition, 这个很好里理解, 就是当 scroll 的时候我们的 modal 需要始终维持对的位置.</p>
<p>来说说 position strategy, 和 big modal 不同, small modal 需要一个位置, 通常是在我们点击按钮的附近.&nbsp;</p>
<p>可以叫它 origin element, 我们要呈现的内容 (content) 必须和 origin element 做一个定位.</p>
<p>flexibleConnectedTo(origin element) 就 content connect to origin 的意思</p>
<p>withPositions 提供一个位置匹配, origin 9 个点, content 9 个点, 所以总共可以摆出 81 一个位置.&nbsp;</p>
<p>&nbsp;<img src="https://img2018.cnblogs.com/blog/641294/201911/641294-20191108100058150-1161659491.png"></p>
<p>&nbsp;</p>
<p>我们提供一个 array 写上各种匹配方式, 要有顺序之分哦,overlay 会先后判断可见范围,找出一个可见度最高的作为展现, 比如 drop down 在屏幕上方,显示位置是下,在屏幕下方显示位置是上,这种体验.</p>
<p>withTransformOriginOn(content element selector string) 主要是给我们做 animation scale 用的,由于 content 出现的位置是不固定的</p>
<p>所以 animation 展示的位置也是不固定的,overlay 会通过我们传入的 selector 找到 element 然后把 transform origin 设置进去.&nbsp;</p>
<p>withFlexibleDimensions 这个我到现在都没有搞懂是啥, default 是 true, 但是我发现它的效果怪怪的,所以就不用了. 跳</p>
<p>withViewportMargin 我们不希望我们的 content 和 viewport 黏在一起, 就可以放这个 margin 给它.&nbsp;</p>
<p><img src="https://img2018.cnblogs.com/blog/641294/201911/641294-20191108095652842-2120394871.png"></p>
<p>红色区域就是那个 margin&nbsp;</p>
<p>withPush 默认是 true, 有了这个, 用户不管 scroll 上下左右, 我们的 content 就会一直保持在可见区, 会跟着 scroll 走.&nbsp;</p>
<p>withScrollableContainers(element) 这个是用于当我们有多层 scroll bar 时用到的,默认情况下, overlay 是通过&nbsp;scrollDispatcher 去监听 body scroll 的.&nbsp;</p>
<p>但是如果我们的 origin 在一个 div scroll 里, 只监听 body scroll 是无法做出正确体验的,所以我们要让 overlay 知道这个事情.&nbsp;</p>
<p>做法是这样的, 我们得把我们能 scroll 的 element 都注册进去&nbsp;scrollDispatcher (可以自己调用 register 或者用 cdkScrollable 指令)</p>
<p>当&nbsp;scrollDispatcher 有了所有的 scrollable div, 当我们调用&nbsp;withScrollableContainers,它会拿我们传入的 element 去 match (element 的 parent 如果有在 scrollable list 中就去监听这个 scrollable 的滚动事件)&nbsp;</p>
<p>这样当 scroll 的时候, 我们的 content 就会正确的被 reposition 了.</p>
<div>
<p>withLockedPosition 当我们 scroll 的时候, overlay 会替我们 reposition 但是有时候这种跳来跳去不一定是好的体验,这个时候我们可以使用 lock, content 显示时会用最佳位置,然后就一直保持这个位置,不管用户 resize or scroll.</p>
</div>
<p>到这个环境, overlay 算是做出来了. 下一个是做 content 的 animation</p>
<p>通常 overlay append content 我们都希望有同一种 animation 体验,所以一般上会封装 animation&nbsp;</p>
<p>它的具体做法是做一个 container 组件, overlay 每次 append 都是这个 container 组件,然后这个组件在 append 我们的动态组件.</p>
<div class="cnblogs_code">
<pre>const containerInjector = <span style="color: rgba(0, 0, 255, 1)">new</span> PortalInjector(<span style="color: rgba(0, 0, 255, 1)">this</span>.vcr.injector, <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> WeakMap());
const containerPortal </span>= <span style="color: rgba(0, 0, 255, 1)">new</span> ComponentPortal(ContainerComponent, <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.vcr, containerInjector);
const container </span>= overlayRef.attach(containerPortal).instance;</pre>
</div>
<p>overlay 内部有一个 dom portal outlet (这个和我们经常用的 cdk portal outlet 指令不是同一个哦),我们调用 overlay.attach(我们的 portal)</p>
<p>overlay 会调用 DomPortalOutlet.attachComponent.</p>
<p><img src="https://img2018.cnblogs.com/blog/641294/201911/641294-20191108101912355-287340604.png"></p>
<p>这里的关键是我们传入的 portal 是否有 viewContainerRef&nbsp; 它会决定之后的 detech change 时机和 injection.</p>
<p>如果有 viewcontainer 那么会把 portal&nbsp; 先创建到 view container 然后通过 outletElement (body 的 div) appendchild (cut and paste) 出去.</p>
<p>如果没有会直接创建 component 然后放入 appRef.views 里面. 然后依然 append to body&nbsp;</p>
<p>大部分情况下我们 portal 应该要有 view container ref.</p>
<p>下一个动作就是 container append 动态组件了.&nbsp;</p>
<div class="cnblogs_code">
<pre>&lt;ng-template cdkPortalOutlet&gt;&lt;/ng-template&gt;</pre>
</div>
<p>我们可以在 container.html 使用 cdkPortalOutlet&nbsp;</p>
<div class="cnblogs_code">
<pre>@ViewChild(CdkPortalOutlet, { static: <span style="color: rgba(0, 0, 255, 1)">true</span> }) portalOutlet: CdkPortalOutlet;</pre>
</div>
<p>通过 viewchild + static 获取到这个指令. (看到 static true 的用途了吧...嘻嘻)</p>
<p>static 的特色是,在 component construtor 运行完后就可以获取到这个属性值了, 不需要等到 after view init.</p>
<div class="cnblogs_code">
<pre>container.animationStateChanged.pipe(filter(e =&gt; e.toState === 'enter' &amp;&amp; e.phaseName === 'done'), take(1)).subscribe(e =&gt;<span style="color: rgba(0, 0, 0, 1)"> {
container.autoFocus();
});
const contentInjector </span>= <span style="color: rgba(0, 0, 255, 1)">new</span> PortalInjector(<span style="color: rgba(0, 0, 255, 1)">this</span>.vcr.injector, <span style="color: rgba(0, 0, 255, 1)">new</span> WeakMap([]));
const contentPortal </span>= <span style="color: rgba(0, 0, 255, 1)">new</span> ComponentPortal(AbcComponent, <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">, contentInjector); // 这里 view container ref 是 null
container.attachComponentPortal(contentPortal);</span></pre>
</div>
<p>注意那个 animationStateChanged. overlay dispose 是很突兀的,所以我们几乎不可能直接调用。</p>
<p>正确的做法是通过控制我们 container 的 animation 来完成关闭, 比如先 fade out container,然后监听 container fade out done 才调用 overlay dispose.</p>
<p>上面这个例子是做了一个 autofocus, 看的出来 container 内部封装了 cdk focus trap 功能.</p>
<p>另一个要留意的是, container.attachComponent&nbsp;</p>
<p>刚才我们说 container 内有一个 cdk portal outlet, 拿我们只需要开一个接口接受动态组件,然后就可以 attach 出去了。</p>
<p>cdk portal outlet vs dom portal outlet&nbsp;</p>
<p><img src="https://img2018.cnblogs.com/blog/641294/201911/641294-20191108103152453-731818761.png"></p>
<p>&nbsp;cdk portal outlet 处理 view container ref 的方式有点不同, cdk poral outlet 本身有自己的 view container ref (刚才 dom outlet 是用 appRef)</p>
<p>如果 portal 自带 view container ref, 那么会直接把 portal 插入到其中, 所以内容不会被 append 到 cdk portal outlet 的位置哦. (这有点怪,注释说了只是逻辑树会插入到 portal 的 view container, 但是渲染应该是在 portal outlet 的位置才对呀. 但是没有..)</p>
<p>提了一个 issue 希望能问个明白</p>
<p>https://github.com/angular/components/issues/17650</p>
<p>如果没有, 就会使用 cdk portal outlet 的 viewcontainer 了. 这通常会是我们想要的结果.</p>
<p>&nbsp;</p>
<div>&nbsp;</div>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>在学习 overlay 和 portal 的时候,一直没有弄明白 viewContainerRef 在其中扮演的角色</p>
<p>这里说一下来龙去脉</p>
<p>当我们创建一个 overlay 时,同时创建了一个 portal outlet</p>
<p><img src="https://img2018.cnblogs.com/blog/641294/201911/641294-20191107003023680-1581301933.png"></p>
<p>&nbsp;</p>
<p>&nbsp;当我们要 append 内容时,内部其实时调用了 DomPortalOutlet 的&nbsp;attachComponentPortal 方法</p>
<p><img src="https://img2018.cnblogs.com/blog/641294/201911/641294-20191107003113480-236444749.png"></p>
<p>&nbsp;</p>
<p>这时候会依据 portal &lt;-- 传入的component portal,不是 portal outlet 哦,不要搞混了.</p>
<p>时候有 viewContainerRef 决定如何创建 component.</p>
<p>如果有就调用 viewContainerRef create component 方法, 这时会 insert component to container 渲染. 然后再通过 dom 操作 cut and paste 去 portal outlet (body)。</p>
<p>如果没有的话就直接通过 component factory create component 然后把 view 放入到全局 appRef 里面. 这时候组件并没有 append to dom 任何地方.&nbsp;</p>
<p>然后&nbsp;cut and paste to portal outlet.</p>
<p>当 app.tick 时,所有的 appRef.views 就会 detech change.&nbsp;</p>
<p>&nbsp;</p>
<p>2 者有什么区别呢 ?&nbsp;</p>
<p><img src="https://img2018.cnblogs.com/blog/641294/201911/641294-20191107002521055-1898481193.png"></p>
<p>&nbsp;</p>
<p>在 portal 的文档里并没有解释太多... 只是说什么逻辑树和 view 树的不同而已.&nbsp;</p>
<p>反而是 dialog 的文档里解释了</p>
<p><img src="https://img2018.cnblogs.com/blog/641294/201911/641294-20191107003643656-1708744250.png"></p>
<p>&nbsp;</p>
<p>从源码上看确实如此.</p>
<p>在使用了 viewContainerRef 之后, detech change 的时机是依据 viewContainerRef 的</p>
<p>而放入 appRef 的情况, detech change 的时机是 app.tick 每一次都触发.</p>
<p><img src="https://img2018.cnblogs.com/blog/641294/201911/641294-20191107004907412-1377708721.png"></p>
<p>&nbsp;</p>
<p>&nbsp;appRef.attachView 将 view 放入了一个 array 中.</p>
<p><img src="https://img2018.cnblogs.com/blog/641294/201911/641294-20191107004959649-732316213.png"></p>
<p>&nbsp;</p>
<p>&nbsp;在 tick 的时候调用 detech change.</p>
<p>&nbsp;</p>
<p>至于 injector 其实蛮困惑的,因为&nbsp;attachComponentPortal injector 是基于 component portal 的 injector,跟 viewContainerRef 没有啥关系丫. 那为什么 dialog 文档说有关系呢</p>
<p><img src="https://img2018.cnblogs.com/blog/641294/201911/641294-20191107004602695-595227833.png"></p>
<p>&nbsp;</p>
<p>看了源码就会发现了,dialog 创建 portal 使用的 injector 是 userInjector || rootInjector, 而所谓的 userInjector 就是 viewContainerRef.injector.</p>
<p>这样就真相大白了咯。</p>
<p>&nbsp;</p><br><br>
来源:https://www.cnblogs.com/keatkeat/p/11809680.html
頁: [1]
查看完整版本: Angular 学习笔记 (动态组件 & Material Overlay & Dialog 分析)