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

最近文章

存档

2026 (2 个月)
2025 (12 个月)
2024 (12 个月)
2023 (12 个月)
2022 (12 个月)
2021 (12 个月)
2020 (12 个月)
2019 (12 个月)
2018 (12 个月)
2017 (12 个月)
2016 (12 个月)
2015 (12 个月)
2014 (12 个月)
2013 (9 个月)
2012 (3 个月)
2011 (1 月)
2010 (1 月)
1991 (1 月)

分类

标签

作者