angular
angular
PHODAL

查看标签 angular

坑多了,想吐槽一下。

我们在做一个后台管理系统,它使用的是 Tab 页 + 无限滚动的方式实现。一旦我们从这个列表页进入详情页,再返回列表页时。不仅会回到列表的最上面,而且还会重置 Tab 的状态。为此,我们的业务需求是:记错这个页面的状态。

项目中采用了 Angular 作为前端框架,并采用的其自带的 Jasmine 作为测试框架。而在过去的一段时间里,一直在遭遇下面的问题:

Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL

由于,我们有差不多 600 个测试案例,而测试一直会因为这个原因挂掉。因此,我们不得不尝试去解决这个问题。

> 如果用一个 UI 库不能解决问题,那就用两个 UI 库;如果用一个 UI 框架不能解决问题,那就用两个框架。 **跨框架的 UI 库**,即前端 UI 库可以不经任何修改,直接能运行在 React、Angular、Vue 等框架上。 在开源电子书《[微前端的那些事儿](https://microfrontend.cn/)》 中,我们讨论到了 Web Components 技术,一种新的 Web 前端容器化技术。在电子书里,我们主要介绍的是:如何使用 Web Components 来构建微服务。而在这篇文章里,我们讨论的是 Web 组件的下半场:**跨框架的 UI 库**。 ## 背景 最近的一段时间里,我花费大量地时间在练习微前端技术。在我的新 Markdown 编辑器 [Phodit](https://github.com/phodal/phodit) 中,我有意无意地去拆分出一个个的小组件,每个小的组件使用不同的技术构建,React、Angular、Stencil.js、原生 JavaScript 等等。如: - Stencil.js + Web Components 来放置 Terminal 的关闭窗口 - React.js 制作了左侧的树形文件树 - Angular 6 完成了重命名文件的交互 - sweetalert 用来做 Dialog 提醒 - …… 编辑器仍然在开发中,这并不是最后的所有技术。引入这么多框架的 “hello, world”,然后构建一个个简单的组件,大概、可能、也许是为了 炫技 练习。虽是这么说,事实是 SimpleMDE 已经封装了 CodeMirror 的一系列 API,为了能快速用上自己的编辑器,我决定地接基于SimpleMDE 来修改。而 SimpleMDE 并不能直接用在 Angular 等前端框架上,这也意味着,因为这个 Editor 的存在,我不得不将页面**撕裂**成几部分:左侧菜单、Terminal 窗口栏、辅助栏、状态栏等等的几部分。 换句话来说,就是这是一个**组件化架构**最好的应用场景。 过去我们谈论前端的组件化架构时,通常指的是**框架限制的组件化架构**。而当我们拥有基础的 UI 组件库时,我们的架构则是**基于 UI 组件库的组件化架构**,两者间的不同在于共性的第一次提取。而当我们在业务组件的基础上,进行对一些通用业务组件的封装时,我们的架构则基于**基于 UI 组件库和业务组件的组件化架构**。 可不论是哪种方式,最后我们都限定于**框架限制**——我们将系统绑定在框架上。而对于团队的技术决策者来说,绑定上框架的实现是一种冒险的作法。未来,这些都是风险,那么有没有可能将底层的 UI 组件库、 复合组件和业务组件库通用呢? ## 铺垫:React 中引入 Angular 组件 为了在我的编辑器中使用 Angular,我用 Angular 编写了一个重命名功能。而为了使用它,我得再次使用一次 ``customEvent``,而在这个微前端架构的系统中,其事件通讯机制已经相当的复杂。在这部分的代码进一步恶化之前,我得尝试有没有别的方式。于是,我想到了之前在其它组件中使用的 Web Components 技术,而 Angular 6 正好可以支持。 ### HTML 中引入 Web Components 我所需要做的事情也相当的简单,只需要将我的组件注册为一个 customElements,稍微改一下 ``app.module.ts`` 文件。在这种情况之下,我们就可以构建出独立于框架的组件。 如下是原始的 module 文件: ```javascript @NgModule({ declarations: [AppComponent], imports: [BrowserModule], bootstrap: [AppComponent] }) export class AppModule { } ``` 如下则是新的 module 文件: ```javascript @NgModule({ declarations: [InteractBar], imports: [BrowserModule], entryComponents: [InteractBar] }) export class AppModule { constructor(private injector: Injector) { const interactBar = createCustomElement(InteractBar, {injector}); customElements.define('interact-bar', interactBar); } } ``` 然后,只需要就可以在 HTML 中传递参数: ````,或者监听对应的 ``@Output`` 事件: ```javascript const bar = document.querySelector('interact-bar'); bar.addEventListener('action', (event: any) => { ... }) ``` 事实证明,使用 Angular 构建的 Web Components 组件是可以用的。于是,我便想,不如在 React 中引入 Angular 组件吧。 ### React 中引入 Angular 组件 于是,便使用 ``create-react-app`` 创建了一个 DEMO,然后引入组件: ```
logo

Welcome to React

To get started, edit src/App.js and save to reload.

``` 嗯,it works。至少 ``filename`` 参数可以成功地传递到 Angular 代码中,而 action 在当前似乎还不行。但是毫无疑问,它在未来是可用的。 Demo 见:[https://phodal.github.io/wc-angular-demo/](https://phodal.github.io/wc-angular-demo/) Repo 见:[https://github.com/phodal/wc-angular-demo](https://github.com/phodal/wc-angular-demo) 这个时候,我遇到了一个问题,我使用 Angular 构建的这个组件,大概是有 257kb。这个大小的组件,但是有点恐怖。 ### Web Components 框架构建组件 在那些微前端相关的文章中,我们指出类似于 [Stencil](https://github.com/ionic-team/stencil) 的形式,将组件直接构建成 Web Components 形式的组件,随后在对应的诸如,如 React 或者 Angular 中直接引用。 如下是一个使用 Stencil 写的 Web Components 的例子: ```javascript @Component({ tag: 'phodit-header', styleUrl: 'phodit-header.css' }) export class PhoditHeader { @State() showCloseHeader = false; componentDidLoad() {...} handleClick() {...} render() { if (this.showCloseHeader) {...} return (
); } } ``` 使用它构建出来的组件,大概可以在 30kb 左右的大小。 不论是不是一个经量级的方案,但是它至少证明了组件复用的可行性。 ## 跨平台 UI 库 在有了上面的技术基础之后,我们可以发现:我们可以构建跨 UI 框架的组件库。那么,它就可以解决我们在构建内部 UI 库时,面对不同技术框架,需要编写不同业务逻辑的问题。这个时候我们的 UI 架构,就会发生一系列的变化。原先我们需要为 React、Angular 和 Vue 等几个不同框架写几个不同的 UI 组件库,但是现在,我们只需要写一套 UI 组件库即可。 自此,我们的 UI 库架构变得更加简单、轻量。 那么问题来了,为什么还没有这样的 UI 库?原因主要有两个: **技术不够成熟**。主要原因是,现有的前端框架对于 Web Components 的支持并不是那么好,诸如我尝试使用 React 来使用时,遇到一些问题。与此同时,前端框架都能支持构建出这样的组件,那么也需要浏览器对于 Web Components 的支持。我们需要诸如 ``custom-elements-es5-adapter.js`` 等的支持,而像 Polymer 这样的 Web Components 框架也需要 IE 11+ 的支持。 **Web Components 技术重写组件**。是的,我们需要将之前使用 TypeScript 或者 JSX 或者 .vue 编写的组件,使用更轻量级框架来构建。UI 框架中的很多要素,是我们在编写组件的时候不需要的——我们只在需要的时候,引入我们所需要的组件即可。 而现在,正是构建这种跨平台 UI 库的最好时机。

为了在我的编辑器中使用 Angular,我用 Angular 编写了一个重命名功能。而为了使用它,我得再次使用一次 customEvent,而在这个微前端架构的系统中,其事件通讯机制已经相当的复杂。在这部分的代码进一步恶化之前,我得尝试有没有别的方式。于是,我想到了之前在其它组件中使用的 Web Components 技术,而 Angular 6 正好可以支持。

在前端应用中,动画是一个常见的场景。在使用了一系列的动画库之后,终于需要自己来实现一个动画了。这次的动画则是基于 Angular 框架。我的场景是一个类似于添加购物车的动画。在这个场景里,需要两个动画,一个是购物车数量的增加动画,一个则是折叠页面元素的动画。 在实现的过程上,我采用了两种不同的 Angular 动画的方式: - 使用 TypeScript 控制动画 - 使用 @Component 中的 animations Angular 动画基础 --- 如 Angular 官网中的示例那样,要在 Angular 应用中添加动画是比较简单的一件事——前提是我们懂得添加的法则。如下是官网的示例: ``` @Component({ selector: 'app-hero-list-basic', template: `
  • {{hero.name}}
`, styleUrls: ['./hero-list.component.css'], animations: [ trigger('heroState', [ state('inactive', style({ backgroundColor: '#eee', transform: 'scale(1)' })), state('active', style({ backgroundColor: '#cfd8dc', transform: 'scale(1.1)' })), transition('inactive => active', animate('100ms ease-in')), transition('active => inactive', animate('100ms ease-out')) ]) ] }) ``` 要使用动画,需要在模板中使用 ``[@heroState]``语法,这里的 ``heroState`` 对应着 ``@Component`` 中的 ``heroState`` 相关的动画。 - 在这个 ``trigger`` 中,我们定义了 ``inactive`` 和 ``active`` 两个不同的 ``state``。即当模板中的 ``hero.state`` 发生变化的时候,我们就会找到对应的 ``state`` 的样式等等的内容。 - 在这个 ``trigger`` 中,我们还定义了两个 ``transition``,即当我们的 ``state`` 从 ``inactive => active`` 或者 ``active => inactive`` 时,我们就会执行后面的动画。 原理上,大概就是这么多了。然后,我就开始了我的动画之旅。 购物车数量增加动画 --- 对于我的场景来说,要添加这个动画并不难。无非就是上一个值淡出,新的值淡入: ``` trigger('count', [ transition('void => current', [ animate( '400ms 150ms', keyframes([ style({ opacity: 0.6, transform: 'translateY(0)', offset: 0 }), style({ opacity: 0.3, transform: 'translateY(-15px)', offset: 0.5 }), style({ opacity: 0, transform: 'translateY(-30px)', offset: 1 }) ]) ) ]), transition('void => last', [ animate( 250, keyframes([ style({ opacity: 0, transform: 'translateY(100%)', offset: 0 }), style({ opacity: 0.3, transform: 'translateY(15px)', offset: 0.5 }), style({ opacity: 0.8, transform: 'translateY(0)', offset: 1.0 }) ]) ) ]) ]) ``` 代码就是这么简单,这里用到了关键帧 ``keyframes``,来进行一些简单的动画转换。 页面缩放动画 --- 随后,我需要做的就是对页面的元素进行缩放等效果,这个时候就需要用到 AnimationBuilder 来实现了: ``` const myAnimation = this.animationBuilder.build([ animate( 1000, keyframes([ style({ opacity: 0.8, transform: 'scale(0.8)', offset: 0.3 }), style({ opacity: 0.3, transform: 'scale(0.3)', offset: 0.5 }), style({ opacity: 0.2, transform: 'scale(0.2) translate(12000px, 8000px)', offset: 1 }) ]) ) ]); const player = myAnimation.create(forkFormComponent); player.play(); player.onDone(() => { const nativeElement = this.cartContainer.nativeElement; nativeElement.removeChild(nativeElement.childNodes[0]); this.renderer.setStyle(nativeElement, 'display', 'none'); }); ``` 在那之前,我先复制了页面元素: ``` const formElement = this.formElement.nativeElement; const forkFormComponent = this.cartContainer.nativeElement; forkFormComponent.appendChild(formElement.cloneNode(true)); this.renderer.setStyle(forkFormComponent, 'display', 'block'); this.renderer.setStyle(forkFormComponent, 'position', 'absolute'); this.renderer.setStyle(forkFormComponent, 'top', '-300px'); this.renderer.setStyle(forkFormComponent, 'left', '0'); ``` 这样一来,就能复制页面的 DOM,然后实现缩放效果了。
之前做过一个 Angular 下的路由重用,即记录 SPA 应用的上一个页面的内容。最开始的时候,并没有考虑 Lazyload,因此实现起来就比较简单: ``` export class SimpleReuseStrategy implements RouteReuseStrategy { public static handlers: { [key: string]: DetachedRouteHandle } = {}; public shouldDetach(route: ActivatedRouteSnapshot): boolean { return true; } public store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void { SimpleReuseStrategy.handlers[route.routeConfig.path] = handle; } public shouldAttach(route: ActivatedRouteSnapshot): boolean { return !!route.routeConfig && !!SimpleReuseStrategy.handlers[route.routeConfig.path]; } public retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle { if (!route.routeConfig) { return null; } return SimpleReuseStrategy.handlers[route.routeConfig.path]; } public shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean { return future.routeConfig === curr.routeConfig; } } ``` 后来项目中添加了 Lazyload,因此便添加了一个 reusePath 的参数来控制是否缓存,最后实现如下: ``` export class SimpleReuseStrategy implements RouteReuseStrategy { public static handlers: { [key: string]: DetachedRouteHandle } = {}; public shouldDetach(route: ActivatedRouteSnapshot): boolean { if (!!route.data && !!route.data.reusePath) { return true; } return false; } /** 当路由离开时会触发。按reusePath作为key存储路由快照&组件当前实例对象 */ public store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void { SimpleReuseStrategy.handlers[route.data.reusePath] = handle; } /** 若 path 在缓存中有的都认为允许还原路由 */ public shouldAttach(route: ActivatedRouteSnapshot): boolean { if (!!route.data.reusePath && !!SimpleReuseStrategy.handlers[route.data.reusePath]) { return true; } return false; } /** 从缓存中获取快照,若无则返回nul */ public retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle { if (!!route.data && !!route.data.reusePath) { return SimpleReuseStrategy.handlers[route.data.reusePath]; } return null; } /** 进入路由触发,判断是否同一路由 */ public shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean { if (future.data && curr.data && future.data.reusePath && curr.data.reusePath) { return future.data.reusePath === curr.data.reusePath; } return false; } } ``` 为了保存路由,需要单独额外的 在 Routing 里 添加一个 key : ``` const routes: Routes = [ { data: { reusePath: 'hello/hello' }, path: 'hello/hello', loadChildren: './hello/hello.module#CheckinModule' } ] ```

Angular 基于 Component 的思想,可以让其在一个页面上同时运行多个 Angular 应用。而在这其中最麻烦的就是第三方模块冲突问题,那么 iframe 呢?

上周,知乎上有几篇关于 Angular 和 Vue 对比的文章。本来想着的是,这些文章倒是可以指导下新手,作一些技术选型。可遗憾的是,开始的文章失去了一些偏颇,后面的文章则开始了一些攻击性行为。慢慢的,整个知乎上便是充满了一些戾气,开始了无尽的网络暴力。

于是,我想分享一下之前使用这些 MV* 框架的经验。

受 Growth 3.0 开发的影响,最近更新文章的频率会有所降低。今天,让我们来谈谈一个好的 Git、SVN 提交信息是怎样规范出来的。

Feeds

RSS / Atom

最近文章

关于作者

Phodal Huang

Engineer, Consultant, Writer, Designer

ThoughtWorks 技术专家

工程师 / 咨询师 / 作家 / 设计学徒

开源深度爱好者

出版有《前端架构:从入门到微前端》、《自己动手设计物联网》、《全栈应用开发:精益实践》

联系我: h@phodal.com

微信公众号: 最新技术分享

存档

分类

标签

作者