【2019年10月版本】使用Angular+Storybook来创建使用Material和访问Firebase的组件的Stories,可以用Component Story Format编写如下!

在使用 Angular + Firebase 开发的应用程序中,Storybook 包含了 Angular Material 组件。

近来,我连续发现了一些关于Stroybook非常有学习价值的文章。

    • 理想的なStorybookのワークフローとは

 

    VueのUIテストにStorybookを使う

太棒了,真是令人感激的事情。

我手上也有一个使用Angular + Firebase构建的SPA项目,E2E测试方面我已经用Protractor解决了,但是”组件级别的单元测试”却令我有些烦恼。

组件类的单元测试可以使用Karma的ts/js代码级别的测试,但是对于作为组件的UI/UX部分的部件=功能+设计+独立性(与其他组件的分离和非依赖)的测试是不能使用Karma的,所以即使我们努力编写代码也有些困难,而且整个页面的设计和所需功能都是按页面提交的,所以作为组件的规范也是开发者自行决定的部分,即使所有在git上提交代码都可以提交到仓库中,其他人创建的组件也变得不可见了。

在这样的时刻,一束光,切取出黑暗的组成部分,啊,只暴露出组成部分在白日下,以独立无需依赖的状态来处理成员变量的那些事情。

装安了Storybook和Angular插件。

我在下面的博客文章中找到了一个关于使用Angular + Storybook的非常易懂的指南。

    Component Driven Development using StoryBook and Angular

请参考以下步骤,立即开始将 Storybook 引入 Angular 项目吧。
由于您手头已经有一个 Angular 项目,因此我们将从添加 Storybook 开始。

使用以下命令即可完成安装。

$ npx -p @storybook/cli sb init

为什么 @storybook/angular 也被安装了呢?太棒了。

所以,通过以下命令可以启动Storybook的开发服务器?

$ npm run storybook

只需要一个选项来用中文将以下内容进行释义:
如果访问 http://localhost:6006,那就可以了。
如果不喜欢6006,那请在package.json中修改”start-storybook -p 6006″的部分,那也可以!

写 Firebase 参照组件的 Stories 时需要注意的事项

在中文中,上述内容的释义如下:

这是第一个主题,关于组件方面。在组件中,要从 Firestore 中获取数据列表等,需要访问 Firebase 和其服务。但仅仅在组件中加入组件本身的代码,并不能实现对 Firebase 的访问。

import { FirestoreListComponent } from "./firestore-list.component";

export default {
  title: "FirestoreList",
}

// 先頭の5件が表示されるはず・・・
export const Simple = () => ({
  component: FirestoreListComponent ,
  props: {
    selected : ''
  },
  moduleMetadata: {
    imports: [],
    providers: []
  },
});

顺便提一下,这个格式似乎是一种不使用storiesOf API来描述的形式,也就是最新的Storybook格式——Component Story Format(CSF)。 (试用最新格式的Storybook和Component Story Format(CSF))

我立刻去查看storybook页面的控制台,发现。。。

ERROR Error: StaticInjectorError(DynamicModule)[FirestoreListService -> AngularFirestore]: 
  StaticInjectorError(Platform: core)[FirestoreListService -> AngularFirestore]: 
NullInjectorError: No provider for AngularFirestore!

FirestoreListComponent引用的FirestoreListService,以及从那里引用的AngularFirestore的提供者不存在,所以会报错!

因此,我们将在上述供应商中添加AngularFirestore。

...
  moduleMetadata: {
    imports: [],
    providers: [AngularFirestore]
  },
...

當我們查看storybook頁面的控制台時…

ERROR Error: StaticInjectorError(DynamicModule)[AngularFirestore -> InjectionToken angularfire2.app.options]: 
  StaticInjectorError(Platform: core)[AngularFirestore -> InjectionToken angularfire2.app.options]: 
    NullInjectorError: No provider for InjectionToken angularfire2.app.options!

当然了,但是该如何传递访问令牌呢?

试了各种方法后,以下设置不再被责骂。

import { environment } from "src/environments/environment";

...
  moduleMetadata: {
    imports: [AngularFireModule.initializeApp(environment.firebase)],
    providers: [AngularFirestore]
  },
...

这是在 Angular 的 App.module.ts 等文件中进行的 AngularFireModule 的初始化处理,如果将其传递给 imports 如上所述,它就会变得安静。

最后,结果如下所示。

import { FirestoreListComponent } from "./firestore-list.component";
import { AngularFireModule } from "@angular/fire";
import { AngularFirestore } from "@angular/fire/firestore";
import { environment } from "src/environments/environment";

export default {
  title: "FirestoreList",
}

// 無事に先頭の5件が表示される!
export const Simple = () => ({
  component: FirestoreListComponent ,
  props: {
    selected : ''
  },
  moduleMetadata: {
    imports: [AngularFireModule.initializeApp(environment.firebase)],
    providers: [AngularFirestore]
  },
});

现在,您可以在storybook上完美地显示Firestore的测试数据!

补充一点,在添加另一个故事时也需要上述规定。如下所示。

import { FirestoreListComponent } from "./firestore-list.component";
import { AngularFireModule } from "@angular/fire";
import { AngularFirestore } from "@angular/fire/firestore";
import { environment } from "src/environments/environment";

export default {
  title: "FirestoreList",
}

// 無事に先頭の5件が表示される!
export const Simple = () => ({
  component: FirestoreListComponent ,
  props: {
    selected : ''
  },
  moduleMetadata: {
    imports: [AngularFireModule.initializeApp(environment.firebase)],
    providers: [AngularFirestore]
  },
});

// `One` が選択されている。
export const WithOneSelected = () => ({
  component: FirestoreListComponent ,
  props: {
    selected : 'One'
  },
  moduleMetadata: {
    imports: [AngularFireModule.initializeApp(environment.firebase)],
    providers: [AngularFirestore]
  },
});

嗯,可能有点烦人…

本题2. 使用Angular Material组件的组件。

接下来是第二个主题,关于材料的标签也不是一马平川的。

假设有一个组件包含了 AngularMaterial。例如,如果包含了标签,那么可能会因为不知道mat-icon标签而被怒斥。

Unhandled Promise rejection: Template parse errors:
'mat-icon' is not a known element:
1. If 'mat-icon' is an Angular component, then verify that it is part of this module.
2. If 'mat-icon' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message. 

只需传递 MatIconModule,就可以解决这个错误。

import { MyIconListComponent } from "./my-icon-list.component";
import { MatIconModule } from "@angular/material";

export default {
  title: "My Icon List",
}

export const StarUserMail = () => ({
  component: MyIconListComponent ,
  props: {
    values : ['star', 'user', 'mail']
  },
  moduleMetadata: {
    imports: [MatIconModule],
    providers: []
  },
});

这样就不会出现上述的错误了。然而…

材料的 CSS 没有生效

如果保持现状,Material的主题(CSS)将无效,导致外观严重受损。如果依赖于Material主题的CSS(只更改高亮部分,或从Material的边距和填充进行进一步调整等),外观可能会完全破坏。

除了上面提到的使用StoryBook和Angular的Component Driven Development页面的内容之外,还需要参考以下示例,重点关注与Material相关的处理。

    https://github.com/amcdnl/material-storybook

我已经按照以下内容创建了.storybook/preview-head.html文件。


<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<style>
@import '@angular/material/prebuilt-themes/deeppurple-amber.css'

html, body {
  margin: 0;
  height: 100%;
}
storybook-dynamic-app-root {
  display: flex;
  flex-direction: column;
  justify-content: center;
  height: 100%;
  padding: 25px;
}
storybook-dynamic-app-root > ng-component {
  margin: 0 auto;
}

似乎可以通过创建preview-head.html,在组件预览区的开头插入这个HTML。在上述博客文章中,@import ‘@angular/material/prebuilt-themes/deeppurple-amber.css’这一部分被提及。

编辑`src/styles.scss`,并导入一个主题`’@angular/material/prebuilt-themes/deeppurple-amber.css’`。

但是修改styles.scss有点麻烦…所以我在这里进行了记录。
如果以上样式起作用,Material组件应该会变得整齐漂亮。

就是这样了!

那么,我也决定核实一下现有的组件是否正确地“作为组件”。

bannerAds