使用Auth0对Firebase和Angular进行身份验证:第2部分

本文最初发布在Auth0.com博客上 ,并经许可在此处重新发布。

在这个由两部分组成的教程系列中,我们将学习如何构建一个使用Auth0身份验证保护Node后端和Angular前端安全的应用程序。 我们的服务器和应用程序还将使用自定义令牌对Firebase Cloud Firestore数据库进行身份验证,以便用户在使用Auth0登录后可以安全方式留下实时评论。 可以在angular-firebase GitHub存储库中找到Angular应用程序代码,在firebase-auth0-nodeserver存储库中找到Node API。

本教程的第一部分“ 使用Auth0验证Firebase和Angular:第1部分”介绍了:

  • Auth0和Firebase的介绍和设置
  • 实施一个安全的Node API,以生成自定义Firebase令牌并为我们的应用程序提供数据
  • 具有模块和延迟加载的Angular应用程序体系结构
  • 具有服务和路由保护的Auth0的角度身份验证
  • 共享的Angular组件和API服务。

使用Auth0对Firebase和Angular进行身份验证:第2部分

本教程的第2部分将介绍:

  1. 显示狗:异步和NgIfElse
  2. 带有路由参数的狗详细信息
  3. 注释模型类
  4. Firebase Cloud Firestore和规则
  5. 评论组件
  6. 评论表格组件
  7. 实时评论
  8. 结论

我们完成的应用程序将如下所示:

带有Auth0自定义令牌的Angular Firebase应用

让我们从使用Auth0:第1部分对Firebase和Angular进行身份验证的结尾处停下来的地方继续 。

显示狗:异步和NgIfElse

让我们实现应用程序的主页-狗列表。 设置Angular应用程序的体系结构时,我们为此组件创建了脚手架。

重要说明:确保您的Node.js API正在运行。 如果您需要有关API的更新知识,请参阅如何使用Auth0认证Firebase和Angular:第1部分-Node API 。

狗组件类

立即打开dogs.component.ts类文件并实现以下代码:

// src/app/dogs/dogs/dogs.component.ts
import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { ApiService } from '../../core/api.service';
import { Dog } from './../../core/dog';
import { Observable } from 'rxjs/Observable';
import { tap, catchError } from 'rxjs/operators';@Component({selector: 'app-dogs',templateUrl: './dogs.component.html'
})
export class DogsComponent implements OnInit {pageTitle = 'Popular Dogs';dogsList$: Observable;loading = true;error: boolean;constructor(private title: Title,private api: ApiService) {this.dogsList$ = api.getDogs$().pipe(tap(val => this._onNext(val)),catchError((err, caught) => this._onError(err, caught)));}ngOnInit() {this.title.setTitle(this.pageTitle);}private _onNext(val: Dog[]) {this.loading = false;}private _onError(err, caught): Observable {this.loading = false;this.error = true;return Observable.throw('An error occurred fetching dogs data.');}}

导入后,我们将设置一些本地属性:

  • pageTitle :设置页面的

    </code> </li><li> <code>dogsList$</code> :我们的API HTTP请求返回的可观察对象,以获取狗列表数据 </li><li> <code>loading</code> :在发出API请求时显示加载图标 </li><li> <code>error</code> :如果在从API提取数据时出错,则显示错误。 </li></ul> <p> 我们将使用声明性异步管道来响应API <code>GET</code>请求返回的<code>dogsList$</code> observable。 使用异步管道,我们不需要在<code>DogsComponent</code>类中进行订阅或取消订阅:订阅过程将自动进行管理! 我们只需要设置可观察的。 </p> <p> 通过将<code>Title</code>和<code>ApiService</code>传递给构造函数,使它们对我们的类可用,然后将我们的<code>dogsList$</code>设置<code>dogsList$</code>可观察的。 我们将使用RxJS运算符<code>tap</code> (以前称为<code>do</code>运算符)和<code>catchError</code>来调用处理程序函数。 <code>tap</code>运算符执行副作用,但不影响发射的数据,因此非常适合设置其他属性。 <code>_onNext()</code>函数会将<code>loading</code>设置为<code>false</code> (因为已成功发射数据)。 <code>_onError()</code>函数将适当地设置<code>loading</code>和<code>error</code>并抛出错误。 如前所述,我们不需要<em>订阅</em>或<em>取消订阅</em>可观察到的<code>dogsList$</code>因为异步管道(我们将在模板中添加)将为我们处理。 </p> <p> 在初始化组件时,我们将使用<code>ngOnInit()</code>监视OnInit生命周期挂钩以设置文档<code><title></code> 。 </p> <p> 我们的Dogs组件类就是这样! </p> <h3 id="dogscomponenttemplate"> 狗组件模板 </h3> <p> 让我们转到<code>dogs.component.html</code>的模板: </p> <pre class="markup language-markup"><code><!-- src/app/dogs/dogs/dogs.component.html --> <h1 class="text-center">{<!-- -->{ pageTitle }}</h1><ng-template #noDogs><app-loading *ngIf="loading"></app-loading><app-error *ngIf="error"></app-error> </ng-template><p class="lead">These were the top <a href="https://www.imspm.com/addons/cms/go/index.html?url=http%3A%2F%2Fwww.akc.org%2Fcontent%2Fnews%2Farticles%2Fthe-labrador-retriever-wins-top-breed-for-the-26th-year-in-a-row%2F" >10 most popular dog breeds in the United States in 2016</a>, ranked by the American Kennel Club (AKC).</p><img class="card-img-top" [src]="dog.image" [alt]="dog.breed"><h5 class="card-title">#{<!-- -->{ dog.rank }}: {<!-- -->{ dog.breed }}</h5><p class="text-right mb-0"><a class="btn btn-primary" [routerLink]="['/dog', dog.rank]">Learn more</a></p> <app-comments></app-comments> </code></pre> <p> 此模板中有几件事,我们将仔细研究: </p> <pre class="markup language-markup"><code>... <ng-template #noDogs><app-loading *ngIf="loading"></app-loading><app-error *ngIf="error"></app-error> </ng-template>...... </code></pre> <p> 这段代码声明性地做了一些非常有用的事情。 让我们来探索。 </p> <p> 首先,我们有一个带有模板引用变量 ( <code>#noDogs</code> )的<code><ng-template></code>元素 。 <code><ng-template></code>元素永远不会直接呈现。 它旨在与结构性指令(例如NgIf)一起使用。 在这种情况下,我们使用<code><ng-template #noDogs></code>创建了一个嵌入式视图,其中包含加载和错误组件。 这些组件中的每个组件都将根据条件进行渲染。 除非指示,否则<code>noDogs</code>嵌入式视图本身不会渲染。 </p> <p> 那么我们如何(何时)告诉此视图进行渲染? </p> <p> 下一个<code>实际上是一个NgIfElse,使用星号前缀作为语法糖 。我们还将async管道与<code>dogsList$</code> observable并设置一个变量,以便我们可以在我们的流中引用流的发射值template( <code>as dogsList</code> )。如果<code>dogsList$</code> observable出现问题,我们可以通过<code>else noDogs</code>语句告诉模板渲染<code><ng-template #noDogs></code>视图,这是在成功从中获取数据之前进行的。 API,或者观察者抛出错误。 </p> <p> 如果<code>dogsList$ | async</code> <code>dogsList$ | async</code>已成功发出一个值,div将呈现,并且我们可以使用NgForOf( <code>*ngFor</code> )结构性指令来显示每个<code>dogsList</code>值(如我们的组件类中所指定的,它是<code>Dog</code>的数组)狗的信息。 </p> <p> 正如您在其余HTML中所看到的那样,将为每只狗显示图片,等级,品种以及指向其个人详细信息页面的链接,我们将在下面创建它们。 </p> <p> 通过浏览到应用程序的主页http:// localhost:4200来查看浏览器中的Dogs组件。 Angular应用程序应该向API发出请求,以获取狗列表并显示它们! </p> <p> <em>注意:我们还包括了<code><app-comments></code>组件。</em> <em>由于我们已经生成了此组件,但尚未实现其功能,因此它应该在用户界面中显示为“注释有效!”的文本。</em> </p> <p> 要测试错误处理,可以停止API服务器(服务器的命令提示符或终端中的<code>Ctrl+c</code> )。 然后尝试重新加载页面。 由于无法访问API,因此应该显示错误组件,并且我们应该在浏览器控制台中看到相应的错误: </p> <p><img referrerpolicy="no-referrer" src="http://imgconvert.csdnimg.cn/aHR0cHM6Ly9kYWIxbm1zbHZ2bnRwLmNsb3VkZnJvbnQubmV0L3dwLWNvbnRlbnQvdXBsb2Fkcy8yMDE4LzAzLzE1MjIyODIyODZkb2dzLWVycm9yLTEwMjR4MzgwLmpwZw?x-oss-process=image/format,png" alt="带有Node.js API的Angular应用显示数据错误" /></p> <h2 id="dogdetailswithrouteparameters"> 带有路由参数的狗详细信息 </h2> <p> 接下来,我们将实现我们的Dog组件。 该路由组件用作每条狗的详细信息页面。 在本教程的第一部分中,我们已经设置了Dog模块架构以及路由和延迟加载 。 我们现在要做的就是执行! </p> <p> <em>提醒:您可能从第1部分中回想起,狗详细信息页面受<code>AuthGuard</code>路由保护器保护 。</em> <em>这意味着访问者必须经过身份验证才能访问页面。</em> <em>此外, API调用需要访问令牌才能返回数据。</em> </p> <h3 id="dogcomponentclass"> 狗组件类 </h3> <p> 打开<code>dog.component.ts</code>类文件并添加: </p> <pre class="typescript language-typescript"><code>// src/app/dog/dog/dog.component.ts import { Component, OnInit, OnDestroy } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Title } from '@angular/platform-browser'; import { ApiService } from '../../core/api.service'; import { DogDetail } from './../../core/dog-detail'; import { Subscription } from 'rxjs/Subscription'; import { Observable } from 'rxjs/Observable'; import { tap, catchError } from 'rxjs/operators';@Component({selector: 'app-dog',templateUrl: './dog.component.html',styles: [`.dog-photo {background-repeat: no-repeat;background-position: 50% 50%;background-size: cover;min-height: 250px;width: 100%;}`] }) export class DogComponent implements OnInit, OnDestroy {paramSub: Subscription;dog$: Observable<DogDetail>;loading = true;error: boolean;constructor(private route: ActivatedRoute,private api: ApiService,private title: Title) { }ngOnInit() {this.paramSub = this.route.params.subscribe(params => {this.dog$ = this.api.getDogByRank$(params.rank).pipe(tap(val => this._onNext(val)),catchError((err, caught) => this._onError(err, caught)));});}private _onNext(val: DogDetail) {this.loading = false;}private _onError(err, caught): Observable<any> {this.loading = false;this.error = true;return Observable.throw('An error occurred fetching detail data for this dog.');}getPageTitle(dog: DogDetail): string {const pageTitle = `#${dog.rank}: ${dog.breed}`;this.title.setTitle(pageTitle);return pageTitle;}getImgStyle(url: string) {return `url(${url})`;}ngOnDestroy() {this.paramSub.unsubscribe();}} </code></pre> <p> 该组件与我们的Dogs清单组件非常相似,只是有一些主要区别。 </p> <p> 我们将导入必要的依赖项,并在类中私下使用<code>ApiService</code>和<code>Title</code>服务。 </p> <p> 狗细节组件依赖于路由参数来确定我们需要为获取数据<em>,</em>狗。 route参数与十种最受欢迎​​的狗列表中的所需狗的排名相匹配,如下所示: </p> <pre class="bash language-bash"><code># URL for dog #2: http://localhost:4200/dog/2 </code></pre> <p> 为了在组件类中访问此参数,我们需要导入ActivatedRoute接口 ,将其传递给构造函数,然后<em>订阅</em>激活的路由的observable <code>params</code> 。 </p> <p> 然后,我们可以将<code>rank</code>参数传递给我们的<code>getDogByRank$()</code> API服务方法。 当组件被销毁时,我们还应<em>取消订阅</em>可观察到的路由参数。 我们的<code>dog$</code>可观察到的可以使用类似于我们的Dogs列表组件的<code>tap</code>和<code>catchError</code>处理程序。 </p> <p> 我们还需要两种方法来帮助我们的模板。 </p> <p> <code>getPageTitle()</code>方法使用API​​数据来生成包含狗的等级和品种的页面标题。 </p> <p> <code>getImgStyle()</code>方法使用API​​数据返回背景图像CSS值。 </p> <h3 id="dogcomponenttemplate"> 狗组件模板 </h3> <p> 现在,让我们在<code>dog.component.html</code>模板中使用以下方法: </p> <pre class="markup language-markup"><code><!-- src/app/dog/dog/dog.component.html --> <ng-template #noDog><app-loading *ngIf="loading"></app-loading><app-error *ngIf="error"></app-error> </ng-template><h1 class="text-center">{<!-- -->{ getPageTitle(dog) }}</h1><ul class="list-unstyled col-12 col-sm-6"><li><strong>Group:</strong> {<!-- -->{ dog.group }}</li><li><strong>Personality:</strong> {<!-- -->{ dog.personality }}</li><li><strong>Energy Level:</strong> {<!-- -->{ dog.energy }}</li></ul><p class="lead mt-3" [innerHTML]="dog.description"></p><p class="clearfix"><a routerLink="/" class="btn btn-link float-left">← Back</a><aclass="btn btn-primary float-right"[href]="dog.link"target="_blank">{<!-- -->{ dog.breed }} AKC Info</a></p> </code></pre> <p> 总体而言,此模板的外观和功能与“狗列表”组件模板相似,不同之处在于,我们不对数组进行迭代。 相反,我们只显示一只狗的信息,页面标题是动态生成的,而不是静态生成的。 我们将在Bootstrap CSS类的帮助下,使用可观察对象发出的<code>dog</code>数据(来自<code>dog$ | async as dog</code> )显示详细信息。 </p> <p> 完成后,组件在浏览器中应如下所示: </p> <p><img referrerpolicy="no-referrer" src="http://imgconvert.csdnimg.cn/aHR0cHM6Ly9kYWIxbm1zbHZ2bnRwLmNsb3VkZnJvbnQubmV0L3dwLWNvbnRlbnQvdXBsb2Fkcy8yMDE4LzAzLzE1MjIyODI1MjZkb2ctZGV0YWlsLTEwMjR4NzY0LmpwZw?x-oss-process=image/format,png" alt="具有异步管道和身份验证的Angular应用-狗详细信息" /></p> <p> 要进入任何狗的详细信息页面, <code>AuthGuard</code>都会提示未经<code>AuthGuard</code>验证的用户首先登录。 他们通过身份验证后,将被重定向到其请求的详细信息页面。 试试看! </p> <h2 id="commentmodelclass"> 注释模型类 </h2> <p> 现在我们的狗列表和详细信息页面已完成,是时候添加实时评论了! </p> <p> 我们要做的第一件事是建立注释的形状,以及初始化新注释实例的方法。 让我们在Angular应用中实现<code>comment.ts</code>类: </p> <pre class="typescript language-typescript"><code>// src/app/comments/comment.ts export class Comment {constructor(public user: string,public uid: string,public picture: string,public text: string,public timestamp: number) {}// Workaround because Firestore won't accept class instances// as data when adding documents; must unwrap instance to save.// See: https://github.com/firebase/firebase-js-sdk/issues/311public get getObj(): object {const result = {};Object.keys(this).map(key => result[key] = this[key]);return result;}} </code></pre> <p> 与我们的<code>Dog</code>和<code>DogDetail</code>模型不同,我们的<code>Comment</code>模型是一个<em>类</em> ,而不是一个<em>接口</em> 。 我们最终将在注释表单组件中初始化<code>Comment</code>实例,为此,必须有一个类。 另外,Firestore在将文档添加到集合时仅接受常规JS对象,因此我们需要在类中添加一个将实例解包到对象的方法。 另一方面,接口仅提供对象的<em>描述</em> 。 这足以满足<code>Dog</code>和<code>DogDetail</code> ,但不足以进行<code>Comment</code> 。 </p> <p> 渲染后,我们希望注释看起来像这样: </p> <p><img referrerpolicy="no-referrer" src="http://imgconvert.csdnimg.cn/aHR0cHM6Ly9kYWIxbm1zbHZ2bnRwLmNsb3VkZnJvbnQubmV0L3dwLWNvbnRlbnQvdXBsb2Fkcy8yMDE4LzAzLzE1MjIyODI2ODFkb2dzLWNvbW1lbnRzLW5vdGxvZ2dlZGluLTEwMjR4NTUyLmpwZw?x-oss-process=image/format,png" alt="带有注释的Angular Firebase应用" /></p> <p> 如您所见,每个评论都有一个用户名,图片,评论文本以及日期和时间。 注释还需要一个唯一标识符,在数据中以<code>uid</code> 。 此唯一的ID确保用户有权删除自己的评论,但不能删除其他人留下的评论。 </p> <p> 现在,我们已经对注释的外观有了一定的了解,让我们开始设置Firebase Firestore规则。 </p> <h2 id="firebasecloudfirestoreandrules"> Firebase Cloud Firestore和规则 </h2> <p> 我们将使用Firebase的Cloud Firestore数据库存储应用程序的评论。 Cloud Firestore是NoSQL,灵活,可扩展,由云托管的数据库,可提供实时功能。 在撰写本文时,Firestore处于beta版,但它是所有新的移动和Web应用程序的推荐数据库。 您可以在此处阅读有关在实时数据库(RTDB)和Cloud Firestore之间进行选择的更多信息。 </p> <p> <em>提醒:如果您需要快速入门Firebase产品,请重新阅读如何使用Auth0 –第1部分:Firebase和Auth0来认证Firebase和Angular 。</em> </p> <p> Firestore将数据组织为<em>集合中的</em> <em>文档</em> 。 如果您有像MongoDB这样的面向文档的NoSQL数据库的经验,则应该熟悉此数据模型。 现在,选择Cloud Firestore作为我们的数据库。 </p> <ol><li> 登录到在本教程的第1部分中创建的Firebase项目 。 </li><li> 单击侧边栏菜单中的<em>数据库</em> 。 </li><li> 在数据库页面标题旁边的下拉列表中,选择<em>Cloud Firestore</em> 。 </li></ol> <h3 id="addcollectionandfirstdocument"> 添加收藏夹和第一个文档 </h3> <p> 默认情况下,将显示“ <em>数据”</em>选项卡,并且数据库中目前没有任何内容。 让我们添加集合和文档,以便我们可以在Angular中查询数据库并返回某些内容。 </p> <p> 点击<em>+添加收藏集</em> 。 为您的收藏<code>comments</code>命名,然后单击“ <em>下一步”</em>按钮。 系统将提示您添加第一个文档。 </p> <p><img referrerpolicy="no-referrer" src="http://imgconvert.csdnimg.cn/aHR0cHM6Ly9kYWIxbm1zbHZ2bnRwLmNsb3VkZnJvbnQubmV0L3dwLWNvbnRlbnQvdXBsb2Fkcy8yMDE4LzAzLzE1MjIyODI5NjRmaXJlYmFzZS1hZGQtZG9jdW1lbnQtZml4ZWQucG5n?x-oss-process=image/format,png" alt="Firebase控制台-添加文档" /></p> <p> 在<em>文档ID</em>字段中,点击<em>自动ID</em> 。 这将自动为您填充一个ID。 接下来,添加我们先前在<code>comment.ts</code>模型中建立的字段,其中包含适当的类型和一些占位符数据。 我们只需要此种子文档,直到我们知道清单在Angular应用程序中正确呈现为止,然后我们可以使用Firebase控制台将其删除,并使用前端的表单正确输入评论。 </p> <p> 但是,由于我们还没有构建表单,因此种子数据会有所帮助。 输入正确的字段和类型后,可以随意填充值。 这是一个建议: </p> <pre class="text language-text"><code>user <string>: Test User uid <string>: abc-123 picture <string>: https://cdn.auth0.com/avatars/tu.png text <string>: This is a test comment from Firebase console. timestamp <number>: 1514584235257 </code></pre> <p> <em>注意:</em>一旦设置了Firebase安全规则, <em>带有<code>uid</code>值的注释将</em> <em>不会</em>对任何经过身份验证的真实用户进行验证。 如果我们以后想删除种子文件,则需要使用Firebase控制台将其删除。 正如您将在以下规则中看到的那样,我们无权使用Angular应用程序中的SDK方法将其删除。 </p> <p> 输入假用户的评论后,单击“ <em>保存”</em>按钮。 新的集合和文档应填充在数据库中。 这提供了我们可以在Angular应用程序中查询的数据。 </p> <h3 id="firebaserules"> Firebase规则 </h3> <p> 接下来,让我们设置Firestore数据库的安全性。 现在切换到“ <em>规则”</em>选项卡。 </p> <p> Firebase安全规则提供了后端<em>安全性</em>和<em>验证</em> 。 在应用程序的Node API中,我们验证了用户是否已使用Auth0和JWT身份验证中间件来访问端点 。 我们已经在API和Angular应用中设置了Firebase身份验证,并且将使用规则功能来授权数据库后端的权限。 </p> <blockquote> <p> 规则是一种表达式,可以评估该表达式以确定是否允许请求执行所需的操作。 — Cloud Firestore安全规则参考 </p> </blockquote> <p> 在Firebase数据库规则编辑器中添加以下代码。 我们将在下面详细介绍。 </p> <pre class="javascript language-javascript"><code>// Firebase Database Rules for Cloud Firestore service cloud.firestore {match /databases/{database}/documents {match /comments/{document=**} {allow read: if true;allow create: if request.auth != null&& request.auth.uid == request.resource.data.uid&& request.resource.data.text is string&& request.resource.data.text.size() <= 200;allow delete: if request.auth != null&& request.auth.uid == resource.data.uid;}} } </code></pre> <p> Firestore具有规则请求方法 : <code>read</code>和<code>write</code> 。 读取包括<code>get</code>和<code>list</code>操作。 写入包括<code>create</code> , <code>update</code>和<code>delete</code>操作。 我们将实现<code>read</code> , <code>create</code>和<code>delete</code>规则。 </p> <p> <em>注意:我们不会在我们的应用程序中添加评论编辑功能,因此不包括<code>update</code> 。</em> <em>但是,如果您想自己添加此功能,请随时添加<code>update</code>规则!</em> </p> <p> 当用户请求<code>match</code>文档路径<code>match</code>时,将执行规则。 路径可以全称,也可以使用通配符。 我们的规则适用于我们创建的<code>comments</code>集合中的所有文档。 </p> <p> 我们希望<em>所有人</em> (匿名用户和经过身份验证的用户)都能<em>阅读</em>评论。 因此, <code>allow read</code>的条件就是<code>if true</code> 。 </p> <p> 我们只希望经过<em>身份验证的</em>用户能够<em>创建</em>新评论。 我们将验证用户是否已登录,并确保要保存的数据具有与用户的身份验证<code>uid</code> (Firebase规则中的<code>request.auth.uid</code> )匹配的<code>uid</code>属性。 另外,我们可以在此处进行一些字段验证。 我们将检查请求的数据是否具有<code>text</code>属性,该属性是字符串,并且为200个字符或更少(我们还将很快在Angular应用中添加此验证)。 </p> <p> 最后,我们只希望用户能够<em>删除自己的</em>评论。 如果通过身份验证的用户的UID使用<code>resource.data.uid</code>匹配现有注释的<code>uid</code>属性,我们可以<code>allow delete</code> 。 </p> <p> <em>注意:您可以在Firebase文档中了解有关request和resource关键字的更多信息。</em> </p> <h2 id="commentscomponent"> 评论组件 </h2> <p> 现在我们的数据库已经准备好了,是时候返回我们的Angular应用程序并实现实时注释了! </p> <p> 我们要做的第一件事是显示评论。 我们希望评论能够实时异步更新,因此让我们探索如何使用Cloud Firestore数据库和angularfire2 SDK进行操作 。 </p> <h3 id="commentscomponentclass"> 注释组件类 </h3> <p> 我们已经为Comments模块创建了架构,所以让我们开始构建<code>comments.component.ts</code> : </p> <pre class="typescript language-typescript"><code>// src/app/comments/comments/comments.component.ts import { Component } from '@angular/core'; import { AngularFirestore, AngularFirestoreCollection, AngularFirestoreDocument } from 'angularfire2/firestore'; import { Observable } from 'rxjs/Observable'; import { map, catchError } from 'rxjs/operators'; import { Comment } from './../comment'; import { AuthService } from '../../auth/auth.service';@Component({selector: 'app-comments',templateUrl: './comments.component.html',styleUrls: ['./comments.component.css'] }) export class CommentsComponent {private _commentsCollection: AngularFirestoreCollection<Comment>;comments$: Observable<Comment[]>;loading = true;error: boolean;constructor(private afs: AngularFirestore,public auth: AuthService) {// Get latest 15 comments from Firestore, ordered by timestampthis._commentsCollection = afs.collection<Comment>('comments',ref => ref.orderBy('timestamp').limit(15));// Set up observable of commentsthis.comments$ = this._commentsCollection.snapshotChanges().pipe(map(res => this._onNext(res)),catchError((err, caught) => this._onError(err, caught)));}private _onNext(res) {this.loading = false;this.error = false;// Add Firestore ID to comments// The ID is necessary to delete specific commentsreturn res.map(action => {const data = action.payload.doc.data() as Comment;const id = action.payload.doc.id;return { id, ...data };});}private _onError(err, caught): Observable<any> {this.loading = false;this.error = true;return Observable.throw('An error occurred while retrieving comments.');}onPostComment(comment: Comment) {// Unwrap the Comment instance to an object for Firestore// See https://github.com/firebase/firebase-js-sdk/issues/311const commentObj = <Comment>comment.getObj;this._commentsCollection.add(commentObj);}canDeleteComment(uid: string): boolean {if (!this.auth.loggedInFirebase || !this.auth.userProfile) {return false;}return uid === this.auth.userProfile.sub;}deleteComment(id: string) {// Delete comment with confirmation prompt firstif (window.confirm('Are you sure you want to delete your comment?')) {const thisDoc: AngularFirestoreDocument<Comment> = this.afs.doc<Comment>(`comments/${id}`);thisDoc.delete();}}} </code></pre> <p> 首先,我们将导入必要的angularfire2依赖项以使用Firestore,集合和文档。 我们还需要<code>catchError</code> <code>Observable</code> , <code>map</code>和catchError,我们的<code>Comment</code>模型和<code>AuthService</code> 。 </p> <p> 接下来,我们将宣布成员。 私有<code>_commentsCollection</code>是一个Firestore集合,其中包含<code>Comment</code>形状的项目。 <code>comments$</code> observable是一个流,该流的值采用<code>Comment</code>数组的形式。 然后,我们有了通常的<code>loading</code>和<code>error</code>属性。 </p> <p> 将<code>AngularFirestore</code>和<code>AuthService</code>传递给构造函数后,我们需要从Cloud Firestore获取集合数据。 我们将使用angularfire2方法<code>collection()</code>进行此操作,将<code>Comment</code>指定为类型,传递我们集合的名称( <code>comments</code> ), 按<code>timestamp</code>对结果进行排序,并限制为最后15条注释。 </p> <p> 接下来,我们将使用<code>_commentsCollection</code>创建可观察的<code>_commentsCollection</code> <code>comments$</code> 。 我们将使用<code>map()</code>和<code>catchError()</code> RxJS运算符来处理发出的数据和错误。 </p> <p> 在我们的<code>_onNext()</code>私有处理程序中,我们将<code>loading</code>和<code>error</code>设置为<code>false</code> 。 我们还将Firestore文档ID添加到<code>comments$</code>流发出的数组中的每个项目中。 我们需要这些ID,以便用户删除个别评论。 为了将ID添加到发出的值,我们将使用<code>snapshotChanges()</code>方法访问meta 。 然后,我们可以使用Spread运算符将文档<code>id</code> <code>map()</code>到返回的数据中。 </p> <p> <em>注意:您可能会注意到,在我们的狗或狗可观察对象的成功方法中,我们没有将<code>error</code>设置为<code>false</code> ,但是我们在这里这样做。</em> <em>每当</em> <em>任何</em>用户实时添加评论时, <em>评论流就会发出一个值</em> 。 因此,作为响应,我们可能需要异步重置错误状态。 </p> <p> 私有<code>_onError()</code>处理函数应该从我们的其他组件中看起来非常熟悉。 它设置<code>loading</code>和<code>error</code>属性并引发错误。 </p> <p> 当用户使用评论表单组件(稍后将构建)提交评论时,将运行<code>onPostComment()</code>方法。 <code>onPostComment()</code>有效负载将包含一个<code>Comment</code>实例,该实例包含用户的注释数据,然后需要将其解包装为普通对象才能保存在Firestore中。 我们将使用Angular Firestore <code>add()</code>方法保存解包后的注释对象。 </p> <p> <code>canDeleteComment()</code>方法检查当前用户是否为任何给定注释的所有者。 如果他们创建了评论,则也可以将其删除。 此方法验证登录用户的<code>userProfile.sub</code>属性是否与注释的<code>uid</code>相匹配。 </p> <p> 当用户单击图标删除评论时, <code>deleteComment()</code>方法将运行。 此方法将打开一个确认对话框,以确认操作,如果确认,则使用<code>id</code>参数从Firestore集合中删除正确的注释文档。 (这就是为什么我们在映射observations <code>comments$</code> observable发出的值时需要在数据中添加文档<code>id</code>的原因。) </p> <p> <em>注意:请记住,我们的Firestore规则还阻止用户删除他们未创建的评论。</em> <em>我们应始终确保访问权限强制在</em>前端和适当的安全后端<em>两种</em> 。 </p> <h3 id="commentscomponenttemplate"> 注释组件模板 </h3> <p> 现在让我们在UI中使用我们的类功能。 打开<code>comments.component.html</code>文件并添加: </p> <pre class="markup language-markup"><code><!-- src/app/comments/comments/comments.component.html --> <section class="comments py-3"><h3>Comments</h3><ng-template #noComments><p class="lead" *ngIf="loading"><app-loading [inline]="true"></app-loading>Loading comments...</p><app-error *ngIf="error"></app-error></ng-template><ul class="list-unstyled"><li *ngFor="let comment of commentsList" class="pt-2"><img [src]="comment.picture" class="avatar rounded"><strong>{<!-- -->{ comment.user }}</strong><small class="text-info">{<!-- -->{ comment.timestamp | date:'short' }}</small><strong><a*ngIf="canDeleteComment(comment.uid)"class="text-danger"title="Delete"(click)="deleteComment(comment.id)">×</a></strong><p class="comment-text rounded p-2 my-2" [innerHTML]="comment.text"></p></li></ul><app-comment-form (postComment)="onPostComment($event)"></app-comment-form><ng-template #logInToComment><p class="lead" *ngIf="!auth.loggedIn">Please <a class="text-primary" (click)="auth.login()">log in</a> to leave a comment.</p></ng-template> </section> </code></pre> <p> 我们将主要使用Bootstrap类来设置注释的样式,然后再添加一些自定义CSS。 我们的注释模板(如我们的狗和狗组件模板)具有一个<code><ng-template></code> ,并将异步管道与NgIfElse一起使用以显示适当的UI。 </p> <p> 评论列表应显示评论的<code>picture</code> (作者的用户头像),用户<code>name</code>以及使用DatePipe格式化的<code>timestamp</code> 。 我们将注释的<code>uid</code>传递给<code>canDeleteComment()</code>方法,以确定是否应显示删除链接。 然后,使用绑定到<code>innerHTML</code>属性来显示注释<code>text</code> 。 </p> <p> 最后,我们将创建元素以显示评论表单或一条消息,指示用户如果要发表评论,请登录。 </p> <p> <em>注意:当用户提交评论时,我们的<code><app-comment-form></code>将使用事件绑定来发出名为<code>postComment</code>的事件。</em> <em><code>CommentsComponent</code>类侦听该事件,并使用我们创建的<code>onPostComment()</code>方法处理该事件,并使用<code>$event</code>有效负载将提交的注释保存到Firestore数据库。</em> <em>在下一节中创建表单时,我们将连接<code>(postComment)</code>事件。</em> </p> <h3 id="commentscomponentcss"> 注释组件CSS </h3> <p> 最后,打开<code>comments.component.css</code>文件,让我们在注释列表中添加一些样式: </p> <pre class="css language-css"><code>/* src/app/comments/comments/comments.component.css */ .avatar {display: inline-block;height: 30px; } .comment-text {background: #eee;position: relative; } .comment-text::before {border-bottom: 10px solid #eee;border-left: 6px solid transparent;border-right: 6px solid transparent;content: '';display: block;height: 1px;position: absolute;top: -10px; left: 9px;width: 1px; } </code></pre> <h2 id="commentformcomponent"> 评论表格组件 </h2> <p> 现在我们有了实时更新的评论列表,我们需要能够在前端添加新评论。 </p> <h3 id="commentformcomponentclass"> 注释表单组件类 </h3> <p> 打开<code>comment-form.component.ts</code>文件,让我们开始吧: </p> <pre class="typescript language-typescript"><code>// src/app/comments/comment-form/comment-form.component.ts import { Component, OnInit, Output, EventEmitter } from '@angular/core'; import { Comment } from './../../comment'; import { AuthService } from '../../../auth/auth.service';@Component({selector: 'app-comment-form',templateUrl: './comment-form.component.html' }) export class CommentFormComponent implements OnInit {@Output() postComment = new EventEmitter<Comment>();commentForm: Comment;constructor(private auth: AuthService) { }ngOnInit() {this._newComment();}private _newComment() {this.commentForm = new Comment(this.auth.userProfile.name,this.auth.userProfile.sub,this.auth.userProfile.picture,'',null);}onSubmit() {this.commentForm.timestamp = new Date().getTime();this.postComment.emit(this.commentForm);this._newComment();}} </code></pre> <p> 如前所述,我们需要从此组件向父<code>CommentsComponent</code>发出一个事件,该事件将新注释发送到Firestore。 <code>CommentFormComponent</code>负责使用从经过身份验证的用户及其表单输入中收集的适当信息构造<code>Comment</code>实例,并将该数据发送给父级。 为了发出<code>postComment</code>事件,我们将导入<code>Output</code>和<code>EventEmitter</code> 。 我们还需要我们的<code>Comment</code>类和<code>AuthService</code>来获取用户数据。 </p> <p> 我们的评论表单组件的构件包括输出装饰 ( <code>postComment</code> ),其为EventEmitter类型的<code>Comment</code> ,并<code>commentForm</code> ,这将是一个实例<code>Comment</code>来存储表单数据。 </p> <p> 在<code>ngOnInit()</code>方法中,我们将使用私有<code>_newComment()</code>方法创建一个新的<code>Comment</code>实例。 此方法将本地<code>commentForm</code>属性设置为带有已验证用户<code>name</code> , <code>sub</code>和<code>picture</code>的<code>Comment</code>的新实例。 注释<code>text</code>为空字符串, <code>timestamp</code>设置为<code>null</code> (提交表单时将添加<code>timestamp</code> )。 </p> <p> 当在模板中提交评论表单时,将执行<code>onSubmit()</code>方法。 此方法添加<code>timestamp</code>并发出带有<code>commentForm</code>数据作为其有效载荷的<code>postComment</code>事件。 它还调用<code>_newComment()</code>方法来重置评论表单。 </p> <h3 id="commentformcomponenttemplate"> 注释表单组件模板 </h3> <p> 打开<code>comment-form.component.html</code>文件并添加以下代码: </p> <pre class="markup language-markup"><code><!-- src/app/comments/comment-form/comment-form.component.html --> <form (ngSubmit)="onSubmit()" #tplForm="ngForm"><inputtype="text"class="form-control col-sm-10 mb-2 mb-sm-0"name="text"[(ngModel)]="commentForm.text"maxlength="200"required><buttonclass="btn btn-primary col ml-sm-2"[disabled]="!tplForm.valid">Send</button> </form> </code></pre> <p> 评论表单模板非常简单。 表单的唯一字段是文本输入,因为所有其他注释数据(如名称,图片,UID等)都动态添加到了类中。 我们将使用简单的模板驱动表单来实现我们的评论表单。 </p> <p> <code><form></code>元素侦听<code>(ngOnSubmit)</code>事件,我们将使用<code>onSubmit()</code>方法处理该事件。 我们还将添加一个名为<code>#tplForm</code>的模板引用变量,并将其设置为<code>ngForm</code> 。 这样,我们可以在模板本身中访问表单的属性。 </p> <p> <code><input></code>元素应具有绑定到<code>commentForm.text</code>的<code>[(ngModel)]</code> 。 这是我们在用户在表单字段中键入内容时要更新的属性。 回想一下,我们将Firestore规则设置为接受200个字符或更少的注释文本,因此我们将此<code>maxlength</code>和<code>required</code>属性一起添加到前端,以便用户无法提交空注释。 </p> <p> 最后,如果表单无效,则应<code>[disabled]</code> <code><button></code>提交表单的<code><button></code> 。 我们可以使用添加到<code><form></code>元素的<code>tplForm</code>参考变量来参考<code>valid</code>属性。 </p> <h2 id="realtimecomments"> 实时评论 </h2> <p> 在浏览器中验证注释是否按预期显示。 到目前为止,唯一的注释应该是我们直接在Firebase中添加的种子注释。 提取并呈现后,我们的评论列表应如下所示: </p> <p><img referrerpolicy="no-referrer" src="http://imgconvert.csdnimg.cn/aHR0cHM6Ly9kYWIxbm1zbHZ2bnRwLmNsb3VkZnJvbnQubmV0L3dwLWNvbnRlbnQvdXBsb2Fkcy8yMDE4LzAzLzE1MjIyODM1ODZ0ZXN0LWNvbW1lbnQucG5n?x-oss-process=image/format,png" alt="带有Angular的Firebase Firestore注释" /></p> <p> 如果用户已通过身份验证,则应显示评论表单。 登录并尝试添加评论。 </p> <h3 id="deleteseedcomment"> 删除种子评论 </h3> <p> 用户可以删除自己的评论。 如果用户是评论的所有者,则评论的日期和时间旁边应显示一个红色的<code>x</code> 。 单击此删除图标提示确认,然后实时删除评论。 </p> <p> 请记住,我们在Firebase中添加的种子文档无法在Angular应用中删除,因为其<code>uid</code>属性与任何实际用户的数据都不匹配。 让我们现在手动将其删除。 </p> <p> 打开Firebase控制台并查看Firestore <code>comments</code>集合。 查找包含种子注释的文档。 使用右上方的菜单下拉菜单,选择<em>删除文档</em>以将其删除: </p> <p><img referrerpolicy="no-referrer" src="http://imgconvert.csdnimg.cn/aHR0cHM6Ly9kYWIxbm1zbHZ2bnRwLmNsb3VkZnJvbnQubmV0L3dwLWNvbnRlbnQvdXBsb2Fkcy8yMDE4LzAzLzE1MjIyODM2OTJmaXJlYmFzZS1kZWxldGUtZG9jLnBuZw?x-oss-process=image/format,png" alt="Firebase删除评论" /></p> <p> 现在,添加到我们数据库中的所有注释都应该可以由其作者在后端删除。 </p> <h3 id="addcommentsinangularapp"> 在Angular App中添加评论 </h3> <p> 添加注释后,它们应该会显示出来,这很棒,但是并不能真正显示Firestore数据库的真正<em>实时</em>性。 我们也可以使用传统的服务器和数据库在UI中添加注释而无需刷新,只需更新视图即可。 </p> <p><img referrerpolicy="no-referrer" src="http://imgconvert.csdnimg.cn/aHR0cHM6Ly9kYWIxbm1zbHZ2bnRwLmNsb3VkZnJvbnQubmV0L3dwLWNvbnRlbnQvdXBsb2Fkcy8yMDE4LzAzLzE1MjIyODM3NjNkb2dzLWNvbW1lbnRzLWxvZ2dlZGluLTEwMjR4NTUyLmpwZw?x-oss-process=image/format,png" alt="Firebase Firestore的角度形式" /></p> <p> 为了真正看到我们的实时数据库在工作,请在第二个浏览器中打开该应用程序,然后使用其他登录名进行身份验证。 在两个浏览器都可见的情况下,在一个浏览器中添加评论。 它将同时出现在第二个浏览器中。 </p> <p><img referrerpolicy="no-referrer" src="http://imgconvert.csdnimg.cn/aHR0cHM6Ly9kYWIxbm1zbHZ2bnRwLmNsb3VkZnJvbnQubmV0L3dwLWNvbnRlbnQvdXBsb2Fkcy8yMDE4LzAzLzE1MjIyODQxMDdyZWFsdGltZS1jb21tZW50LTEwMjR4MzYxLmdpZg" alt="在Angular中使用Firestore实时评论" /></p> <p> 这就是Firebase的实时数据库可以做到的! </p> <h2 id="conclusion"> 结论 </h2> <p> 恭喜你! 您现在有了一个Angular应用,该应用通过Auth0对Firebase进行身份验证,并基于可扩展的体系结构构建。 </p> <p> 我们的教程的第一部分, 如何使用Auth0验证Firebase和Angular:第1部分 ,内容包括: </p> <ul><li> Auth0和Firebase的介绍和设置 </li><li> 实施一个安全的Node API,以生成自定义Firebase令牌并为我们的应用程序提供数据 </li><li> 具有模块和延迟加载的Angular应用程序体系结构 </li><li> 具有服务和路由保护的Auth0的角度身份验证 </li><li> 共享的Angular组件和API服务。 </li></ul> <p> 本教程的第二部分介绍: </p> <ul><li> 使用异步管道和NgIfElse显示数据 </li><li> 使用路线参数 </li><li> 用类建模数据 </li><li> Firebase Cloud Firestore数据库和安全规则 </li><li> 使用angularfire2在Angular中实现Firestore数据库 </li><li> 具有组件交互作用的简单模板驱动形式。 </li></ul> <h3 id="angulartestingresources"> 角度测试资源 </h3> <p> 如果您有兴趣了解有关在Angular中进行测试的更多信息,而本教程未涵盖此内容,请查看以下一些资源: </p> <ul><li> 角度-测试 </li><li> 深度角度测试:服务 </li><li> 深度角度测试:HTTP服务 </li><li> 深度角度测试:组件 </li><li> 如何使用Auth0集成正确测试Angular 4应用程序 </li></ul> <h3 id="additionalresources"> 其他资源 </h3> <p> 您可以在以下位置找到有关Firebase,Auth0和Angular的更多资源: </p> <ul><li> Firebase文档 </li><li> Cloud Firestore文档 </li><li> angularfire2文档 </li><li> Auth0文档 </li><li> Auth0定价和功能 </li><li> Angular文档 </li><li> 角度CLI </li><li> 角备忘单 </li></ul> <h3 id="whatsnext"> 下一步是什么? </h3> <p> 希望您学到了很多有关使用Angular构建可扩展应用程序以及使用自定义令牌对Firebase进行身份验证的知识。 如果您正在寻找可以扩展我们所构建内容的想法,请参考以下建议: </p> <ul><li> 实施不适当的语言过滤器以进行评论 </li><li> 实施授权角色以创建具有删除其他人评论的权限的管理员用户 </li><li> 添加功能以支持评论编辑 </li><li> 使用其他Firestore集合向单个狗详细信息页面添加评论 </li><li> 添加测试 </li><li> 以及更多! </li></ul> <p>From: https://www.sitepoint.com/authenticating-firebase-angular-auth0-2/</p> </p> <p><br /><pre><code style="font-size:16px;">本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击<a href="https://shimo.im/forms/N2A1gvJRpPh7K9qD/fill" target="_blank" rel="nofollow">【内容举报】</a>进行投诉反馈!</code></pre></p> <!-- E 正文 --> <link href="https://qiniu.techgrow.cn/readmore/dist/readmore.css" type="text/css" rel="stylesheet"> <script src="https://qiniu.techgrow.cn/readmore/dist/readmore.js" type="text/javascript"></script> <script> var regex = /(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i var isMobile = navigator.userAgent.match(regex); if (!isMobile) { try { var plugin = new ReadmorePlugin(); plugin.init({ id: "readmore-container", blogId: "55721-7689706765131-406", name: "财经早读", keyword: "666", qrcode: "https://www.imspm.com/assets/img/caijingzaodu.jpg", type: "website", height: "auto", expires: "7", interval: "60", random: "1" }) } catch (e) { console.warn("readmore plugin occurred error: " + e.name + " | " + e.message); } } </script> </div> <!-- S 付费阅读 --> <!-- E 付费阅读 --> <!-- S 点赞 --> <div class="article-donate"> <a href="javascript:" class="btn btn-primary btn-like btn-lg social-share-icon icon-heart addbookbark" data-type="archives" data-aid="395856" data-action="/addons/cms/ajax/collection.html">收藏</a> </div> <!-- E 点赞 --> <div class="entry-meta"> <ul> <!-- S 归档 --> <li>标签:<a href="/dev.html" class="tag" rel="tag" target="_blank">技术</a></li> <!-- S 归档 --> </ul> <ul class="article-prevnext"> <!-- S 上一篇下一篇 --> <li> <span>上一篇 ></span> <a href="/dev/395855.html" target="_blank">Typescript Error Module ' Source/node_modules/firebase/index' has no exported member 'functions'</a> </li> <li> <span>下一篇 ></span> <a href="/dev/395857.html" target="_blank">Firebase 命令行工具</a> </li> <!-- E 上一篇下一篇 --> </ul> </div> <div class="related-article"> <div class="row" style="margin: 0 -15px;"> <!-- S 相关文章 --> <div class="col-xs-12"> <h3 style="font-size: 1.1em;">相关文章</h5> </div> <div class="col-xs-12"> <p style="margin-top: 17px;margin-bottom: 8.5px;"><a href="/dev/747938.html" target="_blank">Duilib中list控件支持ctrl和shif多行选中的实现</a></p> </div> <div class="col-xs-12"> <p style="margin-top: 17px;margin-bottom: 8.5px;"><a href="/dev/747937.html" target="_blank">[ICML2015]Batch Normalization:Accelerating Deep Network Training by Reducing Internal Covariate Shif</a></p> </div> <div class="col-xs-12"> <p style="margin-top: 17px;margin-bottom: 8.5px;"><a href="/dev/747936.html" target="_blank">win10系统 微软输入法 于eclipse ctrl+shif+f冲突间接处理办法</a></p> </div> <div class="col-xs-12"> <p style="margin-top: 17px;margin-bottom: 8.5px;"><a href="/dev/747935.html" target="_blank">Codeforces Round #259 (Div. 2) B. Little Pony and Sort by Shif</a></p> </div> <div class="col-xs-12"> <p style="margin-top: 17px;margin-bottom: 8.5px;"><a href="/dev/747934.html" target="_blank">读LDD3,内存映射与DMA--PAGE_SHIF…</a></p> </div> <div class="col-xs-12"> <p style="margin-top: 17px;margin-bottom: 8.5px;"><a href="/dev/747933.html" target="_blank">VMware虚拟机安装XP【要先分区,再设置BOOT 启动CD,shif+上移】</a></p> </div> <div class="col-xs-12"> <p style="margin-top: 17px;margin-bottom: 8.5px;"><a href="/dev/747932.html" target="_blank">更换iBus五笔的左与右Shif</a></p> </div> <div class="col-xs-12"> <p style="margin-top: 17px;margin-bottom: 8.5px;"><a href="/dev/747931.html" target="_blank">sublime ctrl+shif+f 没用解决办法</a></p> </div> <div class="col-xs-12"> <p style="margin-top: 17px;margin-bottom: 8.5px;"><a href="/dev/747930.html" target="_blank">idea 对 ctrl + z 的撤销 是 ctrl + shif + z</a></p> </div> <div class="col-xs-12"> <p style="margin-top: 17px;margin-bottom: 8.5px;"><a href="/dev/747929.html" target="_blank">计算机最早的设计师应用于,计算机应用基础选择题doc.doc</a></p> </div> <div class="col-xs-12"> <p style="margin-top: 17px;margin-bottom: 8.5px;"><a href="/dev/747928.html" target="_blank">win10自带截图神器:Win+Shift+S</a></p> </div> <div class="col-xs-12"> <p style="margin-top: 17px;margin-bottom: 8.5px;"><a href="/dev/747927.html" target="_blank">Python基础之文件目录操作</a></p> </div> <div class="col-xs-12"> <p style="margin-top: 17px;margin-bottom: 8.5px;"><a href="/dev/747926.html" target="_blank">python简述目录_Python基础之文件目录操作(示例代码)</a></p> </div> <div class="col-xs-12"> <p style="margin-top: 17px;margin-bottom: 8.5px;"><a href="/dev/747925.html" target="_blank">tp5 如何做数据采集</a></p> </div> <div class="col-xs-12"> <p style="margin-top: 17px;margin-bottom: 8.5px;"><a href="/dev/747924.html" target="_blank">任务2-7(服务器字体+阿里巴巴矢量库)</a></p> </div> <div class="col-xs-12"> <p style="margin-top: 17px;margin-bottom: 8.5px;"><a href="/dev/747923.html" target="_blank">html标签(1):h1~h6,p,br,pre,hr</a></p> </div> <div class="col-xs-12"> <p style="margin-top: 17px;margin-bottom: 8.5px;"><a href="/dev/747922.html" target="_blank">TI 电量计介绍与芯片选型指南</a></p> </div> <div class="col-xs-12"> <p style="margin-top: 17px;margin-bottom: 8.5px;"><a href="/dev/747921.html" target="_blank">几款TI电源芯片简介</a></p> </div> <div class="col-xs-12"> <p style="margin-top: 17px;margin-bottom: 8.5px;"><a href="/dev/747920.html" target="_blank">TI DSP芯片C2000系列读取FLASH数据</a></p> </div> <div class="col-xs-12"> <p style="margin-top: 17px;margin-bottom: 8.5px;"><a href="/dev/747919.html" target="_blank">德州仪器(Ti)平台嵌入式开发基础</a></p> </div> <div class="col-xs-12"> <p style="margin-top: 17px;margin-bottom: 8.5px;"><a href="/dev/747918.html" target="_blank">TI三相电机智能栅极驱动芯片特点分类</a></p> </div> <div class="col-xs-12"> <p style="margin-top: 17px;margin-bottom: 8.5px;"><a href="/dev/747917.html" target="_blank">省选模拟(12.08) T3 圈圈圈圈圈圈圈圈</a></p> </div> <div class="col-xs-12"> <p style="margin-top: 17px;margin-bottom: 8.5px;"><a href="/dev/747916.html" target="_blank">Hadoop生态圈技术栈(上)</a></p> </div> <div class="col-xs-12"> <p style="margin-top: 17px;margin-bottom: 8.5px;"><a href="/dev/747915.html" target="_blank">大数据开发基础入门与项目实战(三)Hadoop核心及生态圈技术栈之6.Impala交互式查询</a></p> </div> <div class="col-xs-12"> <p style="margin-top: 17px;margin-bottom: 8.5px;"><a href="/dev/747914.html" target="_blank">小猿圈之Linux下Mysql 操作命令</a></p> </div> <div class="col-xs-12"> <p style="margin-top: 17px;margin-bottom: 8.5px;"><a href="/dev/747913.html" target="_blank">大数据Hadoop生态圈常用面试题</a></p> </div> <div class="col-xs-12"> <p style="margin-top: 17px;margin-bottom: 8.5px;"><a href="/dev/747912.html" target="_blank">大数据开发基础入门与项目实战(三)Hadoop核心及生态圈技术栈之4.Hive DDL、DQL和数据操作</a></p> </div> <div class="col-xs-12"> <p style="margin-top: 17px;margin-bottom: 8.5px;"><a href="/dev/747911.html" target="_blank">备战Noip2018模拟赛11(B组)T3 Monogatari 物语</a></p> </div> <div class="col-xs-12"> <p style="margin-top: 17px;margin-bottom: 8.5px;"><a href="/dev/747910.html" target="_blank">【智能优化算法-圆圈搜索算法】基于圆圈搜索算法Circle Search Algorithm求解单目标优化问题附matlab代码</a></p> </div> <div class="col-xs-12"> <p style="margin-top: 17px;margin-bottom: 8.5px;"><a href="/dev/747909.html" target="_blank">NYOJ 78 圈水池</a></p> </div> <div class="col-xs-12"> <p style="margin-top: 17px;margin-bottom: 8.5px;"><a href="/dev/747908.html" target="_blank">递归问题 跑道 汽车 绕圈问题 Python实现</a></p> </div> <div class="col-xs-12"> <p style="margin-top: 17px;margin-bottom: 8.5px;"><a href="/dev/747907.html" target="_blank">Hadoop生态圈(三):MapReduce</a></p> </div> <!-- E 相关文章 --> </div> </div> <div class="clearfix"></div> </div> </div> </main> <aside class="col-xs-12 col-md-4"> <!--@formatter:off--> <!--@formatter:on--> <!-- S 内容推荐 --> <div class="panel panel-default hot-article"> <div class="panel-heading"> <h3 class="panel-title">内容推荐</h3> </div> <div class="panel-body"> <div class="media media-number"> <div class="media-left"> <span class="num tag">1</span> </div> <div class="media-body"> <a class="link-dark" href="/jiaohutiyan/753475.html" title="大厂出品!保姆级教程帮你掌握「用户体验要素」" target="_blank">大厂出品!保姆级教程帮你掌握「用户体验要素」</a> </div> </div> <div class="media media-number"> <div class="media-left"> <span class="num tag">2</span> </div> <div class="media-body"> <a class="link-dark" href="/jiaohutiyan/753348.html" title="大厂实战案例!设计师如何助力京东快递业务增长?" target="_blank">大厂实战案例!设计师如何助力京东快递业务增长?</a> </div> </div> <div class="media media-number"> <div class="media-left"> <span class="num tag">3</span> </div> <div class="media-body"> <a class="link-dark" href="/jiaohutiyan/753116.html" title="总监干货!5个常见的UI设计规范创建误区" target="_blank">总监干货!5个常见的UI设计规范创建误区</a> </div> </div> <div class="media media-number"> <div class="media-left"> <span class="num tag">4</span> </div> <div class="media-body"> <a class="link-dark" href="/kaifagongju/752540.html" title="数据库管理利器——Navicat Premium v17.0.4学习版(Windows+MacOS+Linux)" target="_blank">数据库管理利器——Navicat Premium v17.0.4学习版(Windows+MacOS+Linux)</a> </div> </div> <div class="media media-number"> <div class="media-left"> <span class="num tag">5</span> </div> <div class="media-body"> <a class="link-dark" href="/jiaohutiyan/750353.html" title="进阶必学!快速掌握10种国际主流设计模型" target="_blank">进阶必学!快速掌握10种国际主流设计模型</a> </div> </div> <div class="media media-number"> <div class="media-left"> <span class="num tag">6</span> </div> <div class="media-body"> <a class="link-dark" href="/jiaohutiyan/750352.html" title="春节期间,10个大厂的产品细节走心设计" target="_blank">春节期间,10个大厂的产品细节走心设计</a> </div> </div> <div class="media media-number"> <div class="media-left"> <span class="num tag">7</span> </div> <div class="media-body"> <a class="link-dark" href="/jiaohutiyan/747940.html" title="如何帮助用户度过新人期?来看雪球APP的实战总结!" target="_blank">如何帮助用户度过新人期?来看雪球APP的实战总结!</a> </div> </div> <div class="media media-number"> <div class="media-left"> <span class="num tag">8</span> </div> <div class="media-body"> <a class="link-dark" href="/ruanjianzixun/42357.html" title="Sketch 95.3最新版下载 (Sketch矢量绘图应用软件)" target="_blank">Sketch 95.3最新版下载 (Sketch矢量绘图应用软件)</a> </div> </div> <div class="media media-number"> <div class="media-left"> <span class="num tag">9</span> </div> <div class="media-body"> <a class="link-dark" href="/ruanjianzixun/42356.html" title="Axure RP 9 最新正式版安装软件与汉化语言包下载(2023年3月30日更新)" target="_blank">Axure RP 9 最新正式版安装软件与汉化语言包下载(2023年3月30日更新)</a> </div> </div> <div class="media media-number"> <div class="media-left"> <span class="num tag">10</span> </div> <div class="media-body"> <a class="link-dark" href="/chanpinsheji/42343.html" title="嘘!SaaS产品的差异化设计细节,一般人我不告诉他" target="_blank">嘘!SaaS产品的差异化设计细节,一般人我不告诉他</a> </div> </div> </div> </div> <!-- E 内容推荐 --> <div class="panel panel-blockimg"> <script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6421005227861480" crossorigin="anonymous"></script> <!-- 右侧正方形 --> <ins class="adsbygoogle" style="display:block" data-ad-client="ca-pub-6421005227861480" data-ad-slot="1989994359" data-ad-format="auto" data-full-width-responsive="true"></ins> <script> (adsbygoogle = window.adsbygoogle || []).push({}); </script> </div> <div class="panel panel-default lasest-update"> <!-- S 最近更新 --> <div class="panel-heading"> <h3 class="panel-title">最新更新</h3> </div> <div class="panel-body"> <ul class="list-unstyled"> <li> <span><a href="/chanpinjingli.html" target="_blank">[产品经理]</a></span> <a class="link-dark" href="/chanpinjingli/758173.html" title="3分钟绘制流程图!这个AI+绘图工具的神仙组合,学完老板直呼内行" target="_blank">3分钟绘制流程图!这个AI+绘图工具的神仙组合,学完老板直呼内行</a> </li> <li> <span><a href="/chanpinjingli.html" target="_blank">[产品经理]</a></span> <a class="link-dark" href="/chanpinjingli/758172.html" title="商业潜规则:打败你的不是AI,而是人性" target="_blank">商业潜规则:打败你的不是AI,而是人性</a> </li> <li> <span><a href="/chanpinsheji.html" target="_blank">[产品设计]</a></span> <a class="link-dark" href="/chanpinsheji/758171.html" title="DeepSeek+智能派单系统的实践分享" target="_blank">DeepSeek+智能派单系统的实践分享</a> </li> <li> <span><a href="/chanpinjingli.html" target="_blank">[产品经理]</a></span> <a class="link-dark" href="/chanpinjingli/758170.html" title="一文读懂本年实际损益借(贷)方发生额" target="_blank">一文读懂本年实际损益借(贷)方发生额</a> </li> <li> <span><a href="/chuangyexueyuan.html" target="_blank">[创业学院]</a></span> <a class="link-dark" href="/chuangyexueyuan/758169.html" title="大客户 vs 中小企业:需求竟天差地别?以企业培训数字化为例" target="_blank">大客户 vs 中小企业:需求竟天差地别?以企业培训数字化为例</a> </li> <li> <span><a href="/chanpinjingli.html" target="_blank">[产品经理]</a></span> <a class="link-dark" href="/chanpinjingli/758168.html" title="不要将员工的“猴子”背到自己身上:职场管理中的权责划分" target="_blank">不要将员工的“猴子”背到自己身上:职场管理中的权责划分</a> </li> <li> <span><a href="/chanpinjingli.html" target="_blank">[产品经理]</a></span> <a class="link-dark" href="/chanpinjingli/758167.html" title="人工智能的三层架构:从应用层到基础服务层,解密智能革命" target="_blank">人工智能的三层架构:从应用层到基础服务层,解密智能革命</a> </li> <li> <span><a href="/chanpinsheji.html" target="_blank">[产品设计]</a></span> <a class="link-dark" href="/chanpinsheji/758166.html" title="一文讲清楚iOS的SKAN4.0" target="_blank">一文讲清楚iOS的SKAN4.0</a> </li> </ul> </div> <!-- E 最近更新 --> </div> <!-- S 热门标签 --> <div class="panel panel-default hot-tags"> <div class="panel-heading"> <h3 class="panel-title">热门标签</h3> </div> <div class="panel-body"> <div class="tags"> <a href="/channel/数量.html" class="tag" target="_blank"> <span>数量</span></a> <a href="/channel/AI技术趋势.html" class="tag" target="_blank"> <span>AI技术趋势</span></a> <a href="/channel/用户角色.html" class="tag" target="_blank"> <span>用户角色</span></a> <a href="/channel/心智游移.html" class="tag" target="_blank"> <span>心智游移</span></a> <a href="/channel/自然生态系统.html" class="tag" target="_blank"> <span>自然生态系统</span></a> <a href="/channel/会员权益.html" class="tag" target="_blank"> <span>会员权益</span></a> <a href="/channel/AirDrop.html" class="tag" target="_blank"> <span>AirDrop</span></a> <a href="/channel/hashmap.html" class="tag" target="_blank"> <span>hashmap</span></a> <a href="/channel/小龙虾.html" class="tag" target="_blank"> <span>小龙虾</span></a> <a href="/channel/焦虑.html" class="tag" target="_blank"> <span>焦虑</span></a> <a href="/channel/危机处理.html" class="tag" target="_blank"> <span>危机处理</span></a> <a href="/channel/发展.html" class="tag" target="_blank"> <span>发展</span></a> <a href="/channel/微信群折叠.html" class="tag" target="_blank"> <span>微信群折叠</span></a> <a href="/channel/toast.html" class="tag" target="_blank"> <span>toast</span></a> <a href="/channel/测评新算法.html" class="tag" target="_blank"> <span>测评新算法</span></a> <a href="/channel/改版.html" class="tag" target="_blank"> <span>改版</span></a> <a href="/channel/wireshark.html" class="tag" target="_blank"> <span>wireshark</span></a> <a href="/channel/投放方式.html" class="tag" target="_blank"> <span>投放方式</span></a> <a href="/channel/音频播放动效.html" class="tag" target="_blank"> <span>音频播放动效</span></a> <a href="/channel/timer.html" class="tag" target="_blank"> <span>timer</span></a> <a href="/channel/女性商业.html" class="tag" target="_blank"> <span>女性商业</span></a> <a href="/channel/古典自媒体.html" class="tag" target="_blank"> <span>古典自媒体</span></a> <a href="/channel/海外博主.html" class="tag" target="_blank"> <span>海外博主</span></a> <a href="/channel/repeater.html" class="tag" target="_blank"> <span>repeater</span></a> <a href="/channel/转账.html" class="tag" target="_blank"> <span>转账</span></a> <a href="/channel/万能钥匙.html" class="tag" target="_blank"> <span>万能钥匙</span></a> <a href="/channel/秋招.html" class="tag" target="_blank"> <span>秋招</span></a> <a href="/channel/快服务.html" class="tag" target="_blank"> <span>快服务</span></a> <a href="/channel/个人演讲.html" class="tag" target="_blank"> <span>个人演讲</span></a> <a href="/channel/客户共识.html" class="tag" target="_blank"> <span>客户共识</span></a> </div> </div> </div> <!-- E 热门标签 --> </aside> </div> </div> </main> <footer> <div id="footer"> <div class="container"> <div class="row footer-inner"> <div class="col-xs-12"> <div class="footer-logo pull-left mr-4"> <a href="/"><i class="fa fa-bookmark"></i></a> </div> <div class="pull-left"> Copyright © 2025 All rights reserved. 超级产品经理 <a href="https://beian.miit.gov.cn" target="_blank" rel="noopener">浙ICP备14026978号-4</a> <ul class="list-unstyled list-inline mt-2"> <li><a href="/p/aboutus.html" target="_blank">关于网站</a></li> <li><a href="/contactus.html" rel="nofollow" target="_blank">联系我们</a></li> </ul> </div> </div> </div> </div> </div> </footer> <div id="floatbtn"> <!-- S 浮动按钮 --> <a class="hover" href="/index/cms.archives/post.html" target="_blank"> <i class="iconfont icon-pencil"></i> <em>立即<br>投稿</em> </a> <div class="floatbtn-item floatbtn-share"> <i class="iconfont icon-share"></i> <div class="floatbtn-wrapper" style="height:50px;top:0"> <div class="social-share" data-initialized="true" data-mode="prepend"> <a href="#" class="social-share-icon icon-weibo" target="_blank"></a> <a href="#" class="social-share-icon icon-qq" target="_blank"></a> <a href="#" class="social-share-icon icon-qzone" target="_blank"></a> <a href="#" class="social-share-icon icon-wechat"></a> </div> </div> </div> <a href="javascript:;"> <i class="iconfont icon-qrcode"></i> <div class="floatbtn-wrapper"> <div class="qrcode"><img src="https://www.imspm.com/assets/img/gongzhonghao.jpg"></div> <p>微信公众账号</p> <p>微信扫一扫加关注</p> </div> </a> <a id="back-to-top" class="hover" href="javascript:;"> <i class="iconfont icon-backtotop"></i> <em>返回<br>顶部</em> </a> <!-- E 浮动按钮 --> </div> <script type="text/javascript" src="/assets/libs/jquery/dist/jquery.min.js?v=1.0.10"></script> <script type="text/javascript" src="/assets/libs/bootstrap/dist/js/bootstrap.min.js?v=1.0.10"></script> <script type="text/javascript" src="/assets/libs/fastadmin-layer/dist/layer.js?v=1.0.10"></script> <script type="text/javascript" src="/assets/libs/art-template/dist/template-native.js?v=1.0.10"></script> <script type="text/javascript" src="/assets/addons/cms/js/jquery.autocomplete.js?v=1.0.10"></script> <script type="text/javascript" src="/assets/addons/cms/js/swiper.min.js?v=1.0.10"></script> <script type="text/javascript" src="/assets/addons/cms/js/share.min.js?v=1.0.10"></script> <script type="text/javascript" src="/assets/addons/cms/js/cms.js?v=1.0.10"></script> <script type="text/javascript" src="/assets/addons/cms/js/common.js?v=1.0.10"></script> </body> </html>