如何使用 Angular 11 和 Scully 构建 Jamstack 投资组合

作者选择/dev/color作为Write for DOnations计划的一部分接受捐赠。

简介

作为一名开发人员,您可以通过多种方式展示自己的技能、成就和工作。其中包括开源贡献、个人项目、演讲活动、博客文章等。但是,如果您的作品分散在多个平台上,当人们查找您时很难找到,那么您所做的这一切可能都是徒劳的。没有一个集中的地方来展示你的成就可能会对你不利,并可能导致潜在客户、招聘人员和雇主低估你的价值。作品集可以让你把最好的作品展示出来,让你的成就更容易被找到,帮助你塑造自己的品牌,并促进联系,从而带来潜在的有利可图的机会。将您的作品集与博客结合起来,您就可以分享您的想法,记录您所学到的东西,并进一步建立您的信誉。

利用它们广泛而强大的功能,您可以使用AngularScully 构建一个引人入胜的快速作品集。Angular 是一个多功能平台,可让您创建从网页到本地和移动应用程序的所有内容。它提供了各种有用的工具,可以简化应用程序开发,从而更快地开发出性能卓越的应用程序。

通过将 Angular 应用程序变为静态应用程序,可以让它们变得更快。使用 Scully,您可以将 Angular 应用程序转换为更快交付的 Jamstack 应用程序。Scully 是一种静态网站生成器,可为 Angular 应用程序中的每个路由创建静态 HTML 页面。这些页面的交付速度更快,对搜索引擎优化也很有效。它还提供博客生成器等工具,你可以用它制作自己的作品集博客。

在本教程中,您将使用 Angular 11 和 Scully 创建一个作品集和博客。您将生成一个 Angular 应用程序,添加页面以显示您的项目和简介,并添加服务以填充这些页面。此外,您还将生成一个博客并为其创建帖子。最后,您将使用 Scully 将应用程序转换为静态网站。

先决条件

要完成本教程,您需要

第 1 步 - 设置 Angular 应用程序

在这一步中,您将使用 Angular CLI 生成投资组合应用程序。CLI 将为应用程序搭建脚手架,并安装运行应用程序所需的所有依赖项。然后,您将添加 Bootstrap、Font Awesome 和 Scully 等依赖项。Scully 将使应用静态化。Bootstrap 将提供组件和样式。Font Awesome 将提供图标。安装完这些依赖项后,您将添加字体和包含投资组合数据的 JSON 文件等资产。

首先,运行以下命令生成应用程序。它将被称为 portfolio。本教程不包括应用程序的测试,因此您可以使用 -S 标志跳过测试文件的生成。如果以后希望添加测试,可以不使用该标记。

1ng new portfolio -S

当被问及是否要添加 Angular 路由时,请回答 "是"。这样 CLI 工具就会生成一个单独的模块来处理应用程序的路由。它将位于 src/app/app-routing.module.ts

系统还会提示您选择样式表格式。选择 "CSS"。Angular 还提供其他样式表选项,如 SCSS、Sass、Less 和 Stylus。CSS 更为简单,因此你将在这里使用它。

1[secondary_label Output]
2? Would you like to add Angular routing? Yes
3? Which stylesheet format would you like to use? (Use arrow keys)
4❯ CSS

安装依赖项

此应用程序需要三个依赖项:Scullyng-bootstrapFont Awesome。Scully 将把 Angular 应用程序转换为静态应用程序。其他依赖项,即 Bootstrap 和 Font Awesome,则用于定制投资组合的外观和感觉。ng-bootstrap` 将提供使用 Bootstrap 定型的 Angular 组件。这一点尤其有用,因为并不是所有 vanilla Bootstrap 组件都能与 Angular 兼容。Bootstrap 还可以减少您添加到应用程序中的样式,因为它已经为您提供了样式。Font Awesome 将提供图标。

将 Scully 添加为依赖项后,它会运行其 init 方案,为应用程序添加更改,为静态网站生成做好准备。在项目目录中,使用此命令将 Scully 添加到项目中:

1ng add @scullyio/[email protected]

然后,使用此命令添加 ng-bootstrap

1ng add @ng-bootstrap/[email protected]

最后要添加的依赖项是 Font Awesome。

1npm install --save @fortawesome/[email protected]

添加了依赖项后,就可以添加配置了。

添加配置

要在应用程序中使用 Font Awesome,您需要在 angular.json 文件中添加对其已精简 CSS 的引用。该文件位于项目的底部。使用 nano 或你喜欢的文本编辑器打开该文件:

1nano angular.json

在文件中查找 architect/build 部分。该部分为 ng build 命令提供了配置。你将在 styles 数组中添加经过精简的 Font Awesome CSS 引用,即 node_modules/@fortawesome/fontawesome-free/css/all.min.css。样式 "数组位于 "projects/portfolio/architect/build/options "下。将高亮显示的一行添加到文件中:

 1[label angular.json]
 2{
 3  ...
 4  "projects": {
 5    "portfolio": {
 6      ...
 7      "architect": {
 8        "build": {
 9          ...
10          "options": {
11              ...
12              "styles": [
13                  "node_modules/bootstrap/dist/css/bootstrap.min.css",
14                  "node_modules/@fortawesome/fontawesome-free/css/all.min.css",
15                  "src/styles.css"
16              ],
17          }
18        },
19        ...
20      }
21    }
22  }
23}

现在,Font Awesome 可用于构建。

保存并关闭文件。

接下来,您将为项目添加新字体,这有助于个性化作品集的外观和感觉。你可以使用Google Fonts为你的Angular项目添加字体,它提供了大量可用字体。您可以在 "head "标签中使用 "link "标签链接到选定的字体。

在本教程中,您将使用 Nunito 字体。打开 src/index.html,添加下面突出显示的行:

 1[label src/index.html]
 2... 
 3<head>
 4  <meta charset="utf-8">
 5  <title>Portfolio</title>
 6  <base href="/">
 7  <meta name="viewport" content="width=device-width, initial-scale=1">
 8  <link rel="icon" type="image/x-icon" href="favicon.ico">
 9  <link rel="preconnect" href="https://fonts.googleapis.com">
10  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11  <link href="https://fonts.googleapis.com/css2?family=Nunito:wght@200;400;800&display=swap" rel="stylesheet">
12</head>
13...

高亮线条链接到 Google Fonts 上的 Nunito 字体。您将获得三种重量的字体:超轻、普通和超粗。

保存并关闭文件。

添加生物和项目数据

接下来要做的事情就是创建 JSON 文件,这些文件将保存您想放入作品集的所有数据。将模板与数据分开,将来就可以更容易地进行更改和添加更多内容,而无需篡改应用程序。

首先在 src/assets 中创建一个 json 文件夹。

1mkdir src/assets/json

json 文件夹中,您将创建两个 JSON 文件:bio.jsonprojects.json 文件。bio.json包含您想在网站上显示的简介。projects.json是您想展示的项目列表。

生物.json "文件的结构与此相似:

1[label src/assets/json/bio.json]
2{
3    "firstName": "Jane",
4    "lastName": "Doe",
5    "intro": [ "paragraph 1", "paragraph 2" ],
6    "about": [ "paragraph 1", "paragraph 2" ]
7}

简介 "是主页上显示的简短介绍。关于 "是在 "关于 "页面上显示的更详细的简介。

在本教程中,您可以使用示例简介或自定义自己的简介。要使用示例简介,请打开 bio.json 并添加以下内容:

 1[label src/assets/json/bio.json]
 2{
 3    "firstName": "Jane",
 4    "lastName": "Doe",
 5    "intro": [
 6        "I'm a software developer with a passion for web development. I am currently based somewhere in the world. My main focus is building fast, accessible, and beautiful websites that users enjoy.",
 7        "You can have a look at some of my work here."
 8    ],
 9    "about": [
10        "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam aliquam auctor fringilla. Proin scelerisque lacinia nisl vel ultrices. Ut gravida finibus velit sit amet pulvinar. Nunc nisi arcu, pretium quis ultrices nec, volutpat sit amet nulla. Mauris semper elementum placerat. Aenean velit risus, aliquet quis lectus id, laoreet accumsan erat. Curabitur varius facilisis velit, et rutrum ligula mollis et. Sed imperdiet sit amet urna ut eleifend. Suspendisse consectetur velit nunc, at fermentum eros volutpat nec. Vivamus scelerisque nec turpis volutpat sagittis. Aenean eu sem et diam consequat euismod.",
11        "Mauris dolor tellus, sagittis vel pellentesque sit amet, viverra in enim. Maecenas non lectus eget augue convallis iaculis mattis malesuada nisl. Suspendisse malesuada purus et luctus scelerisque. Cras hendrerit, eros malesuada blandit scelerisque, nulla dui gravida arcu, nec maximus nunc felis sit amet mauris. Donec lorem elit, feugiat sit amet condimentum quis, consequat id diam. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Cras rutrum sodales condimentum. Aenean ultrices mi vel augue dapibus mattis. Donec ut ornare nisl. Curabitur feugiat pharetra dictum."
12    ]
13}

完成编辑后,保存并关闭文件。

另一个 JSON 文件 projects.json 的结构与此类似:

 1[label src/assets/json/projects.json]
 2[
 3    {
 4        "name": "",
 5        "stack": {
 6            "name": "Vue.js",
 7            "iconClasses": "fab fa-vuejs"
 8        },
 9        "description": "",
10        "sourceUrl": "",
11        "previewUrl": "",
12        "featured": false
13    }
14]

每个项目都有一个名称、描述、源代码托管 URL 和预览 URL(如果已部署)。如果项目没有预览 URL,可以直接省略。

堆栈 "对象用于显示项目所使用的语言或框架。因此,name 将是语言/框架的名称,而 iconClasses 则是语言/框架图标的 Font Awesome CSS 类。featured "属性表示该项目是否应显示在主页上。如果将 featured 设为 false,则只在 "Projects(项目)"页面上显示,而不是同时在主页和 "Projects(项目)"页面上显示。

在本教程中,您可以使用一些示例项目或添加自己的项目。要使用示例项目,请打开 projects.json 并添加以下内容:

 1[label src/assets/json/projects.json]
 2[
 3    {
 4        "name": "Soduko",
 5        "stack": {
 6            "name": "Angular",
 7            "iconClasses": "fab fa-angular"
 8        },
 9        "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
10        "sourceUrl": "https://github.com",
11        "previewUrl": "https://github.com",
12        "featured": true
13    },
14    {
15        "name": "E-commerce Store",
16        "stack": {
17            "name": "React",
18            "iconClasses": "fab fa-react"
19        },
20        "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
21        "sourceUrl": "https://github.com",
22        "previewUrl": "https://github.com",
23        "featured": true
24    },
25    {
26        "name": "Algorithm Visualization App",
27        "stack": {
28            "name": "Angular",
29            "iconClasses": "fab fa-angular"
30        },
31        "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
32        "sourceUrl": "https://github.com",
33        "previewUrl": "https://github.com",
34        "featured": true
35    },
36    {
37        "name": "Time Tracking CLI App",
38        "stack": {
39            "name": "Node.js",
40            "iconClasses": "fab fa-node-js"
41        },
42        "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
43        "sourceUrl": "https://github.com",
44        "previewUrl": "https://github.com",
45        "featured": true
46    }
47]

保存并关闭文件。

启动服务器

要检查应用程序是否按预期运行,请使用此命令运行 Angular CLI 提供的服务器:

1ng serve

该命令用于构建应用程序并提供服务。如果您做了任何更改,它将重建应用程序。完成后,应用程序将在 http://localhost:4200/ 服务。

打开浏览器并导航至 http://localhost:4200。您应该会看到一个如下所示的占位符页面:

应用程序启动时显示的图像](assets/67804/app-start.png)

每次保存更改时,都会看到类似的输出结果:

 1✔ Browser application bundle generation complete.
 2
 3Initial Chunk Files           | Names                      |      Size
 4vendor.js                     | vendor                     |   3.83 MB
 5styles.css                    | styles                     | 202.25 kB
 6polyfills.js                  | polyfills                  | 141.85 kB
 7main.js                       | main                       |  26.08 kB
 8runtime.js                    | runtime                    |   9.06 kB
 9
10                              | Initial Total              |   4.20 MB
11
12Build at:  - Hash:  - Time: 13312ms
13
14**Angular Live Development Server is listening on localhost:4200, open your browser on http://localhost:4200/**
15
16✔ Compiled successfully.

完成一个步骤后,请检查是否出现 ✔ Compiled successfully. 消息。如果有问题,ng serve 命令将输出错误信息。出现这种情况时,请重新执行该步骤,以确保没有遗漏或出错。完成教程后,作品集主页应该是这样的:

作品集首页截图](assets/67804/home-page.png)

在这一步中,您生成了应用程序,并添加了所有必要的依赖项、资产和配置。您还启动了 Angular CLI 提供的服务器。下一步,您将创建核心模块。

第 2 步 - 创建核心模块

在本教程中,您要构建的应用程序将包含三个模块:核心"、"博客 "和 "作品集"。应用程序的结构如下:

1[label src/app]
2src/app
3├── blog
4├── core
5└── portfolio

博客模块用于博客登陆页面和博客文章页面。核心模块包含应用程序的所有核心内容。其中包括标题组件、数据服务和数据模型。作品集模块包含所有作品集页面:"关于"、"项目 "和主页。

在这一步中,您将生成核心模块。您还将生成并填充页眉组件、服务和数据模型。页眉显示在每个页面的顶部,包含网站名称和菜单。模型构建投资组合数据。服务将获取投资组合数据。

这就是核心模块的样子:

1[label src/app/core/]
2src/app/core
3├── header
4├── models
5└── services

要生成核心模块,请在项目根目录下运行以下命令:

1ng generate module core

此命令在 src/app/core 文件夹中添加核心模块,并在 src/app/core/core.module.ts 中添加模块文件。

核心模块文件定义了核心模块及其导入。在新生成的模块文件中,您需要添加一些导入,以支持头和服务。

打开 "core.module.ts",添加高亮显示的几行(确保在 "CommonModule "导入后加上逗号):

 1[label src/app/core/core.module.ts]
 2...
 3import { RouterModule } from '@angular/router';
 4import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
 5import { HttpClientModule } from '@angular/common/http';
 6
 7@NgModule({
 8  declarations: [],
 9  imports: [
10    CommonModule,
11    RouterModule,
12    NgbModule,
13    HttpClientModule
14  ]
15})
16...

该模块将使用 HttpClientModule 从您之前创建的 JSON 文件中获取数据。它还将使用 NgbModuleRouterModule 中的几个 ng-bootstrap 组件进行路由。您还需要将它们添加到 CoreModule 的导入中。

完成后保存并关闭文件。

生成数据模型

在本节中,您将生成数据模型。数据模型是用于定义 JSON 文件中数据结构的接口。它们将在服务和其他组件中作为返回和参数类型使用。您将需要两个模型:生物",定义生物数据的结构;"项目",定义项目数据的结构。

1[label src/app/core/models]
2src/app/core/models
3├── bio.ts
4└── project.ts

生物 "模型代表一个配置文件,而 "项目 "模型则是一个展示项目。在项目根目录下运行以下命令即可生成这两个模型:

1for model in bio project ; do ng generate interface "core/models/${model}"; done

该命令会循环查看文件路径,并将其传递给 ng generate interface,后者会在 src/app/core/models 文件夹中创建这些文件。

生物模型文件定义了您希望生物包含的信息。在生成的 src/app/core/models/bio.ts 文件中,添加下面突出显示的字段。

1[label src/app/core/models/bio.ts]
2export interface Bio {
3    firstName: string;
4    lastName: string;
5    about: string[];
6    intro: string[];
7}

在此代码块中,您为生物模型添加了名、姓、关于和简介字段。前两个字段是字符串类型,后两个字段是字符串数组,因为它们可能包含多个段落。

保存并关闭文件。

项目文件定义了项目的结构。在这里,您将列出每个项目要使用的字段。在 src/app/core/models/project.ts 文件中,添加高亮显示的行。

1[label src/app/core/models/project.ts]
2export interface Project {
3    name: string;
4    stack: { iconClasses: string, name: string };
5    description: string;
6    sourceUrl: string;
7    previewUrl: string;
8    featured?: boolean;
9}

您已经为项目模型添加了字段。每个项目都有名称、描述、源代码 URL 和预览 URL(如果已部署)。堆栈 "对象用于显示项目的语言或框架。(名称是语言/框架的名称,iconClasses是语言/框架图标的 Font Awesome CSS 类)。featured "属性表示项目是否应显示在主页上。如果 featured 设置为 false,则只在 "Projects(项目)"页面上显示,而不是同时在主页和 "Projects(项目)"页面上显示。

完成后保存并关闭文件。

在本节中,您创建了数据模型。接下来,您将创建获取投资组合数据并使用这些模型的服务。

###发电服务

您将在本节中创建的服务会从您之前创建的 JSON 文件中获取数据。获取数据后,组件就可以调用这些服务并使用数据。模型将被用作这些服务的返回类型。生物服务中使用生物模型,项目服务中使用项目模型。您还将包含一个额外的标题服务,它将帮助决定对标题和其他组件中的项目使用何种路由。核心 "模块有三个服务:生物服务"、"标题服务 "和 "项目服务"。

1[label src/app/core/services]
2src/app/core/services
3├── bio.service.ts
4├── header.service.ts
5└── projects.service.ts

要生成这些服务,请在项目根目录下运行此命令:

1for service in bio projects header; do ng generate service "core/services/${service}"; done

该命令会循环查看文件路径,并将其传递给 ng generate service,后者会在 src/app/core/services 文件夹中创建这些文件。

生物服务会从生物 JSON 文件中获取您的生物数据。为此,您需要添加一个方法来获取这些数据。打开 src/app/core/services/bio.service.ts 文件并添加以下突出显示的行:

 1[label src/app/core/services/bio.service.ts]
 2import { HttpClient } from '@angular/common/http';
 3import { Injectable } from '@angular/core';
 4import { Bio } from '../models/bio';
 5
 6@Injectable({
 7  providedIn: 'root'
 8})
 9export class BioService {
10
11  constructor(private http: HttpClient) { }
12
13  getBio() {
14   return this.http.get<Bio>('assets/json/bio.json');
15  }
16}

生物服务 "的 "getBio "方法会从 "assets/json/bio.json "文件中获取您的生物信息。您将在其构造函数中注入 HttpClient 服务,并在 getBio() 方法中使用该服务向文件发出 GET 请求。

保存并关闭文件。

接下来,您将修改 HeaderService。头服务用于检查当前路由是否是主页。您将添加一个方法来确定当前页面是否是主页。打开 src/app/core/services/header.service.ts 文件并添加高亮显示的行:

 1[label src/app/core/services/header.service.ts]
 2import { Injectable } from '@angular/core';
 3import { NavigationEnd, Router } from '@angular/router';
 4import { filter, map, startWith } from 'rxjs/operators';
 5
 6@Injectable({
 7  providedIn: 'root'
 8})
 9export class HeaderService {
10
11  constructor(private router: Router) { }
12
13  isHome() {
14    return this.router.events.pipe(
15      filter(event => event instanceof NavigationEnd),
16      map(event => {
17        if (event instanceof NavigationEnd) {
18          if (this.checkForHomeUrl(event.url)) {
19            return true;
20          }
21        }
22
23        return false;
24      }),
25      startWith(this.checkForHomeUrl(this.router.url))
26    );
27  }
28
29  private checkForHomeUrl(url: string): boolean {
30    return url.startsWith('/# ') || url == '/';
31  }
32}

在 "HeaderService "中,"isHome "方法会检查当前页面是否是主页。这对于滚动到锚点和在主页上显示特色项目非常有用。

保存并关闭文件。

最后,您将修改 ProjectsService。项目服务从项目 JSON 文件中获取项目数据。您将添加一个方法来获取项目数据。打开 src/app/core/services/projects.service.ts 文件,将内容更改为以下内容:

 1[label src/app/core/services/projects.service.ts]
 2import { HttpClient } from '@angular/common/http';
 3import { Injectable } from '@angular/core';
 4import { Observable } from 'rxjs';
 5import { filter, mergeAll, toArray } from 'rxjs/operators';
 6import { Project } from '../models/project';
 7
 8@Injectable({
 9  providedIn: 'root'
10})
11export class ProjectsService {
12
13  constructor(private http: HttpClient) { }
14
15  getProjects(featured?: boolean): Observable<Project[]> {
16    let projects$ = this.http.get<Project[]>('assets/json/projects.json');
17
18    if (featured) {
19      return projects$.pipe(
20        mergeAll(),
21        filter(project => project.featured || false),
22        toArray()
23      );
24    }
25
26    return projects$;
27  }
28}

项目服务 "有一个 "getProjects "方法,用于获取和筛选项目。它从 assets/json/projects.json 文件中获取项目。您将在其构造函数中注入 HttpClient 服务,并在 getProjects() 方法中使用该服务向文件发出 GET 请求。使用 featured 参数,您可以选择只返回有特色的项目,以便简洁明了。当您只想显示重要项目时,这在主页上非常有用。

保存并关闭文件。

在本节中,您添加了从生物和项目 JSON 文件中获取生物和项目数据的生物和项目数据服务。您还创建了一个页眉服务,用于检查当前页面是否是主页。在下一节中,您将创建一个显示在作品集每一页顶部的页眉。它将使用生物和页眉服务。

生成页眉

页眉组件显示在所有页面的顶部。它包含您的姓名、"关于 "和 "项目 "页面以及博客的链接。生物和页眉服务将在页眉中使用。生物服务将为页眉提供生物数据。页眉服务将用于检查当前页面是否是主页,并据此设置指向 "关于 "和 "项目 "部分或页面的链接。您可以在项目根目录下运行此命令来生成它:

1ng generate component core/header

页眉组件显示在每个页面的顶部。它将使用生物服务获取您的姓和名。它还将使用页眉服务来确定当前页面是否是主页。利用这些信息,它将设置指向 "关于 "和 "项目 "部分或页面的链接。

header.component.ts文件中,您将注入 bio 和 header 服务,并添加一个样式属性来处理组件在不同屏幕尺寸下的响应性。

打开 header.component.ts 并添加高亮显示的行:

 1[label src/app/core/header/header.component.ts]
 2import { Component } from '@angular/core';
 3import { BioService } from '../services/bio.service';
 4import { HeaderService } from '../services/header.service';
 5
 6@Component({
 7  selector: 'app-header',
 8  templateUrl: './header.component.html',
 9  styleUrls: ['./header.component.css']
10})
11export class HeaderComponent {
12  bio$ = this.bioService.getBio();
13  isHome$ = this.headerService.isHome();
14
15  menuItems = [
16    { title: 'About Me', homePath: '/', fragment: 'about', pagePath: '/about' },
17    { title: 'My Projects', homePath: '/', fragment: 'projects', pagePath: '/projects' },
18    { title: 'My Blog', homePath: '/blog', fragment: '', pagePath: '/blog' }
19  ];
20
21  constructor(private bioService: BioService, private headerService: HeaderService) { }
22}

在该组件文件中,您将注入两个服务:"bioService "用于从生物 JSON 文件中获取您的姓名;"headerService "用于确定当前显示的页面是否为主页。通过后者,您可以决定按钮是应该转到单独的页面(如 /projects),还是执行锚滚动(如 /#project)。menuItems 包含要显示的所有菜单项。bio$isHome$` 属性包含来自上述服务的可观察数据。

保存并关闭文件。

接下来,您将修改标题组件的模板。这里将显示从生物服务获取的数据。这里还添加了指向 "关于 "和 "项目 "部分或页面的链接。在 src/app/core/header/header.component.html 模板文件中,添加以下代码。

 1[label src/app/core/header/header.component.html]
 2<div class="d-flex min-vh-10 w-100 justify-content-center pb-3 pt-3 pr-4 pl-4">
 3    <div class="d-flex justify-content-start" * ngIf="bio$ | async as bio" routerLink="/">
 4        <h2 class="font-weight-bold">{{bio.firstName}}</h2>
 5        <h2 class="font-weight-light">{{bio.lastName}}</h2>
 6    </div>
 7    <div class="d-none d-md-flex flex-grow-1 justify-content-end align-items-start">
 8        <button type="button" class="ml-2 mr-2 btn btn-outline-dark border-0 font-weight-bold"
 9            * ngFor="let item of menuItems" [routerLink]="(isHome$ | async) ? item.homePath : item.pagePath"
10            [fragment]="(isHome$ | async) ? item.fragment : ''">{{item.title}}</button>
11    </div>
12    <div class="d-flex d-md-none justify-content-end flex-grow-1">
13        <div ngbDropdown class="d-inline-block" display="dynamic" container="body">
14            <button class="btn btn-outline-dark border-0" ngbDropdownToggle>
15                <i class="fas fa-lg fa-bars"></i>
16            </button>
17            <div ngbDropdownMenu class="dropdown-menu dropdown-menu-right">
18                <button ngbDropdownItem * ngFor="let item of menuItems"
19                    [routerLink]="(isHome$ | async) ? item.homePath : item.pagePath"
20                    [fragment]="(isHome$ | async) ? item.fragment : ''">{{item.title}}</button>
21            </div>
22        </div>
23    </div>
24</div>

在模板中,您的姓名("bio.firstName "和 "bio.lastName")将使用 "bio "属性中的数据显示。根据屏幕的大小,会显示一个下拉菜单或来自 menuItems 的按钮列表。模板中的别名管道将处理从可观察对象中取消订阅的情况。本教程将始终遵循这一模式。

保存并关闭文件。

标题必须在所有页面上都可见。要做到这一点,您需要采取几个步骤。首先,CoreModule 需要导出 HeaderComponent 使其可以访问。要导出该组件,请在 src/app/core/core.module.ts 中添加高亮显示的几行。别忘了在 imports 数组后添加逗号。

 1[label src/app/core/core.module.ts]
 2...
 3@NgModule({
 4  ...
 5
 6  imports: [
 7    ...
 8  ],
 9  exports: [ HeaderComponent ]
10})
11...

要使页眉可见,还需要将其添加到 AppModule 中的 AppComponent 模板中。AppModule 还必须导入 CoreModule 才能访问页眉。您将在后面的步骤中完成这些附加任务。

在这一步中,您创建了组织投资组合数据的模型。您还创建了使用模型获取投资组合数据的服务。此外,您还创建了一个页眉服务,帮助决定页眉项使用的路径。最后,您生成了一个标题组件,将显示在所有作品集页面上。下一步,您将生成投资组合模块,其中包含投资组合的所有主要页面。作品集模块中的页面将使用在本节中创建的生物和项目服务及模型。

第 3 步 - 生成投资组合模块

在上一步中,您创建了核心模块,该模块用于保存标题,并包含用于获取投资组合数据的所有服务和模型。在这一步中,您将生成投资组合模块,该模块包含投资组合的所有基本页面。其中包括主页、"关于 "和 "项目 "页面。在这一步中,您将使用在核心模块中创建的服务和模型来创建这些页面。您还将为每个页面添加路径。

主页将使用页眉和简介服务显示您的简介摘要。关于 "页面是更深入的简介,将使用生物服务和模型。项目 "页面将使用项目服务和模型展示您的项目。本步骤结束后,您的作品集模块将结构如下:

1[label src/app/portfolio]
2src/app/portfolio
3├── about
4├── home
5└── projects

首先,您将生成两个模块:投资组合模块和投资组合路由模块。投资组合模块包含投资组合的所有主要页面。投资组合路由模块负责路由到这些页面。

要生成这两个模块,请在项目根目录下运行此命令:

1ng generate module portfolio --module app --routing  --route portfolio

此命令将创建 app/portfolio 文件夹,并在 app/portfolio/portfolio.module.ts 中添加一个模块文件。你将在 app/src/app-routing.module.ts 中看到添加的路由。routing "标志指定生成投资组合路由模块。该路由模块将位于 app/portfolio/portfolio-routing.module.ts

route "标记会在应用模块中创建一个由"--module "标记指定的懒加载路由。你将在 app/src/app-routing.module.ts 中看到添加的路由。它还添加了一个用于路由目的的占位符组件,将在下一节讨论。

该组合模块应在 / 路径下可用。这就需要在 --route 标志中使用空字符串,如 --route=""。但是,ng generate module 不允许为 --route 标志提供空字符串。因此,您必须使用一个占位符 portfolio。然后,您将在 src/app/app-routing.module.ts中使用空字符串替换该占位符,该模块将处理整个应用程序的路由。

打开 src/app/app-routing.module.ts,替换高亮显示的行:

1[label src/app/app-routing.module.ts]
2...
3const routes: Routes = [
4  {
5    path: '',
6    loadChildren: () => import('./portfolio/portfolio.module').then(m => m.PortfolioModule)
7  }
8];
9...

这将确保投资组合模块中的所有页面都能从 / 路径开始使用。

保存并关闭文件。

创建主页

创建投资组合模块的命令还会创建一个 "投资组合组件"。这是一个占位符组件,在为模块设置路由时使用。不过,这个组件更合适的名称应该是 "HomeComponent"。主页是作品集的登陆页面。它将包含整个作品集的摘要。这样,用户无需浏览多个页面,就能更轻松地概览您的作品,从而降低用户失去兴趣的风险。

要更改该组件的名称,首先要为其创建一个新文件夹。在项目根目录下运行以下命令:

1mkdir -p src/app/portfolio/home

然后,将所有的 "PortfolioComponent "文件移动到这个新文件夹中。

1mv src/app/portfolio/portfolio.component.* src/app/portfolio/home/

此命令会将所有名称以 portfolio.component.* 开头的文件移动到 src/app/portfolio/home/ 文件夹中。

然后将 portfolio.component.* 文件重命名为 home.component.*

1find src/app/portfolio/home -name 'portfolio* ' -exec bash -c ' mv $0 ${0/\portfolio./home.}' {} \;

运行上述命令后,由于组件名称和路径的改变,会出现一些错误。要解决这个问题,你必须修改几个文件:投资组合路由模块、投资组合模块和主页组件文件。在这些文件中,您将把 PortfolioComponent 的所有实例改为 HomeComponent。还将把路径从 ./portfolio.component 更新为 ./home/home.component

首先打开 src/app/portfolio/portfolio-routing.module,该模块用于处理投资组合模块的路由。进行高亮显示的更改:

1[label src/app/portfolio/portfolio-routing.module]
2...
3import { HomeComponent } from './home/home.component';
4
5const routes: Routes = [{ path: '', component: HomeComponent }];
6...

保存并关闭文件。

接下来,打开src/app/portfolio/portfolio.module.ts,即投资组合模块文件。进行高亮显示的修改:

 1[label src/app/portfolio/portfolio.module.ts]
 2...
 3import { HomeComponent } from './home/home.component';
 4
 5@NgModule({
 6  declarations: [
 7    HomeComponent
 8  ],
 9  ...
10})
11...

保存并关闭文件。

最后,打开src/app/portfolio/home/home.component.ts,即主页组件文件。进行高亮显示的修改:

 1[label src/app/portfolio/home/home.component.ts]
 2...
 3@Component({
 4  selector: 'app-home',
 5  templateUrl: './home.component.html',
 6  styleUrls: ['./home.component.css']
 7})
 8export class HomeComponent implements OnInit {
 9...
10}

保存并关闭文件。

在这些文件中,你将 PortfolioComponent 的所有实例都改为了 HomeComponent,并更新了路径以指向 HomeComponent。做完这一切后,投资组合模块应该是这样的。

1[label src/app/portfolio]
2src/app/portfolio
3├── home
4│   ├── home.component.css
5│   ├── home.component.html
6│   └── home.component.ts
7├── portfolio-routing.module.ts
8└── portfolio.module.ts

现在您已经更新了主目录组件文件的名称和路径。

接下来,你将在主页组件模板中填充内容并设计样式。主页组件是作品集的主页面,显示简介摘要。(这就是上文从作品集组件更名为主页组件的组件)。在该组件中,您需要获取要显示的简介数据,并添加样式,使页面在不同尺寸的屏幕上都能响应。

打开 src/app/portfolio/home/home.component.ts,更新代码以符合以下内容:

 1[label src/app/portfolio/home/home.component.ts]
 2import { Component } from '@angular/core';
 3import { BioService } from '../../core/services/bio.service';
 4
 5@Component({
 6  selector: 'app-home',
 7  templateUrl: './home.component.html',
 8  styleUrls: ['./home.component.css']
 9})
10export class HomeComponent {
11  bio$ = this.bioService.getBio();
12
13  respOptions = [
14    { viewClasses: 'd-none d-md-flex', headingClass: 'display-3', useSmallerHeadings: false },
15    { viewClasses: 'd-flex d-md-none', headingClass: '', useSmallerHeadings: true }
16  ];
17
18  constructor(private bioService: BioService) { }
19}

主页将显示您的姓名和简短的简历,这些信息是从您在此处注入的 BioService 中获取的。调用其 getBio 方法后,观察结果将存储在 bio$ 属性中。respOptions 属性存储有助于确保视图响应的配置。

保存并关闭文件。

接下来,您将修改主页组件的模板。它负责在不同尺寸的屏幕上显示来自生物服务的信息。您将添加您的姓名、简介以及稍后将介绍的关于和项目组件。

打开 src/app/portfolio/home/home.component.html 并添加以下代码:

 1[label src/app/portfolio/home/home.component.html]
 2<div class="d-flex flex-column justify-content-center align-items-center w-100" * ngIf="bio$ | async as bio">
 3    <div class="d-flex flex-column min-vh-95 justify-content-center align-items-center w-100">
 4        <div * ngFor="let options of respOptions" [ngClass]="options.viewClasses"
 5            class="flex-column justify-content-center align-items-start w-75">
 6            <h1 [ngClass]="options.headingClass" class="text-left">Hello, 👋. My name is <span
 7                    class="font-weight-bold">{{bio.firstName+'
 8                    '+bio.lastName}}.</span></h1>
 9            <div * ngFor="let par of bio.intro">
10                <h2 class="text-left" * ngIf="!options.useSmallerHeadings">{{par}}</h2>
11                <h5 class="text-left" * ngIf="options.useSmallerHeadings">{{par}}</h5>
12            </div>
13            <button class="mt-3 mb-5 btn btn-outline-dark" routerLink="/" fragment="projects">
14                See My Work
15                <i class="ml-1 fas fa-angle-right"></i>
16            </button>
17        </div>
18    </div>
19
20    <div class="d-none d-md-block mt-5"></div>
21    <app-about id="about" class="mb-3"></app-about>
22
23    <div class="d-none d-md-block mt-5"></div>
24    <app-projects id="projects" class="mb-5"></app-projects>
25</div>

在此模板中,您将显示姓名 bio.firstName++bio.lastName 以及来自 bio 的简介 bio.intro。您还将显示关于组件 app-about 和项目组件 app-projects,这些组件将在下一步中生成。

<$>[注] 注: 本模板中还添加了一些其他组件,但这些组件目前还不存在。它们是 "关于 "和 "项目 "组件。这些是您接下来要添加的内容。如果您正在运行服务器,这将产生一个错误。您可以将这些行注释掉,直到生成它们之后。

1[label src/app/portfolio/home/home.component.html]
2...
3<app-about id="about" class="mb-3"></app-about>
4
5...
6
7<app-projects id="projects" class="mb-5"></app-projects>
8...

<$>

接下来,您可以为主页组件添加样式。打开 src/app/portfolio/home/home.component.css 并添加以下几行:

1[label src/app/portfolio/home/home.component.css]
2.min-vh-95 {
3    height: 95vh;
4}

在这里,您要为主页组件添加样式,以便在主页的主要内容和浏览器窗口的边缘之间留出一些空间。

完成后,主页将如下所示(您可以在最后一步预览网站):

主页](assets/67804/95bkjuo.png)

在这一步中,您创建了显示作品集摘要的主页。在下一部分,您将生成 "关于 "和 "项目 "组件。这些组件将显示在主页上,也将作为独立页面使用。

生成 "关于 "和 "项目 "页面

您可以运行一条命令,一次性生成其余的 "项目 "和 "关于 "页面,而无需单独生成每个页面。在项目根目录下运行以下命令即可:

1for page in about projects; do ng generate component "portfolio/${page}"; done

该命令将循环查看每个页面名称并生成它们。

填充关于页面

关于 "页面将显示更深入的个人简介。该页面上的信息是从生物服务中获取的,也使用生物模型。该组件将显示在主页上。它也是一个独立的页面,有自己的路径。

要在 "关于 "页面中填充您的简历,您需要修改 "关于 "组件文件以使用简历服务。您还将设置选项,使页面在不同的显示屏上都能响应。打开 src/app/portfolio/about/about.component.ts,添加高亮显示的行:

 1[label src/app/portfolio/about/about.component.ts]
 2import { Component } from '@angular/core';
 3import { BioService } from '../../core/services/bio.service';
 4
 5@Component({
 6  selector: 'app-about',
 7  templateUrl: './about.component.html',
 8  styleUrls: ['./about.component.css']
 9})
10export class AboutComponent {
11  bio$ = this.bioService.getBio();
12
13  respOptions = [
14    { viewClasses: 'd-none d-md-flex', headingClass: 'display-3', useSmallerHeadings: false },
15    { viewClasses: 'd-flex d-md-none', headingClass: '', useSmallerHeadings: true }
16  ];
17
18  constructor(private bioService: BioService) { }
19}

关于 "信息将来自 "BioService",一旦调用其 "getBio "方法,可观测信息将存储在 "bio$"属性中。respOptions 通过为不同的显示尺寸提供可选的 CSS 类来帮助提高响应速度。

保存并关闭文件。

接下来,您将修改 "关于 "页面的模板,以便显示从生物服务中获取的信息。打开 src/app/portfolio/about/about.component.html 并添加以下几行:

 1[label src/app/portfolio/about/about.component.html]
 2<div class="d-flex justify-content-center vw-90 mx-auto" * ngIf="bio$ | async as bio">
 3    <div * ngFor="let options of respOptions" [ngClass]="options.viewClasses"
 4        class="flex-column align-items-center text-center w-75">
 5        <h1 [ngClass]="options.headingClass" class="mb-5"><span class="font-weight-bold">About</span> Me</h1>
 6        <div * ngFor="let par of bio.about">
 7            <h4 * ngIf="!options.useSmallerHeadings" class="mb-4">{{par}}</h4>
 8            <h5 * ngIf="options.useSmallerHeadings" class="mb-4">{{par}}</h5>
 9        </div>
10    </div>
11</div>

在此模板中,您将显示来自 bio$ 观察对象的数据。您将循环浏览 "关于 "部分的信息,并将其作为段落添加到 "关于 "页面。

保存并关闭文件。

完成后,"关于 "页面将如下所示(您可以在最后一步预览网站):

关于页面](assets/67804/LwDzDz3.png)

填充项目页面

项目 "页面将显示从项目服务获取的所有项目。该组件将用于主页,同时也是一个独立页面。它将与 "关于 "组件一起显示在主页上。在主页上使用该组件时,只能看到特色项目。有一个 "查看更多项目 "按钮,它只会出现在主页上。点击后,该按钮将重定向到项目完整列表页面。

要填充 "项目 "页面,需要修改其组件文件,以便从项目服务中获取项目。您还将使用页眉服务来决定是显示所有项目还是突出显示项目。您还将添加选项,使页面在不同尺寸的屏幕上都能响应。打开 src/app/portfolio/projects/projects.component.ts 并添加突出显示的行:

 1[label src/app/portfolio/projects/projects.component.ts]
 2import { Component } from '@angular/core';
 3import { mergeMap } from 'rxjs/operators';
 4import { HeaderService } from '../../core/services/header.service';
 5import { ProjectsService } from '../../core/services/projects.service';
 6
 7@Component({
 8  selector: 'app-projects',
 9  templateUrl: './projects.component.html',
10  styleUrls: ['./projects.component.css']
11})
12export class ProjectsComponent {
13  isHome$ = this.headerService.isHome();
14  projects$ = this.isHome$.pipe(
15    mergeMap(atHome => this.projectsService.getProjects(atHome))
16  );
17
18  respOptions = [
19    { viewClasses: 'd-none d-md-flex', displayInColumn: false, useSmallerHeadings: false, titleClasses: 'display-3' },
20    { viewClasses: 'd-flex d-md-none', displayInColumn: true, useSmallerHeadings: true, titleClasses: '' }
21  ];
22
23  constructor(private projectsService: ProjectsService, private headerService: HeaderService) { }
24}

项目来自 "ProjectsService"。您将使用 HeaderService 来确定当前页面是否是主页。您将使用 isHome$ 的值来确定是获取完整的项目列表,还是只获取特色项目。

保存并关闭文件。

接下来,你将修改项目组件的模板。使用从项目服务中获取的项目,在这里循环添加它们。您将在一个卡片中显示每个项目的基本信息,并添加链接指向其代码的托管位置和预览位置。

打开 src/app/portfolio/projects/projects.component.html 并添加以下几行:

 1[label src/app/portfolio/projects/projects.component.html]
 2<div * ngFor="let options of respOptions" [ngClass]="options.viewClasses"
 3    class="flex-column align-items-center text-center vw-90 mx-auto">
 4    <h1 [ngClass]="options.titleClasses" class="mb-5"><span class="font-weight-bold">My</span> Projects</h1>
 5    <div class="d-flex vw-90"
 6        [ngClass]="{'justify-content-center flex-wrap': !options.displayInColumn, 'flex-column align-items-center': options.displayInColumn}"
 7        * ngIf="projects$ | async as projects">
 8        <div * ngFor="let project of projects" class="card project-card m-3"
 9            [ngClass]="{'m-3': !options.displayInColumn, 'mb-3': options.displayInColumn}">
10            <div class="card-body d-flex flex-column">
11                <h5 class="card-title font-weight-bold text-left project-title" [title]="project.name">
12                    {{project.name}}
13                </h5>
14                <h6 class="card-subtitle mb-2 font-weight-lighter text-left">
15                    <i [ngClass]="project.stack.iconClasses"></i>
16                    {{project.stack.name}}
17                </h6>
18                <p class="card-text text-left">
19                    {{project.description}}
20                </p>
21                <div class="d-flex flex-row justify-content-start">
22                    <a [href]="project.previewUrl" * ngIf="project.previewUrl" class="btn btn-dark mr-2">
23                        <i class="fa-lg mr-1 far fa-eye"></i>
24                        Preview
25                    </a>
26                    <a [href]="project.sourceUrl" * ngIf="project.sourceUrl" class="btn btn-dark">
27                        <i class="fa-lg mr-1 fab fa-github-alt"></i>
28                        Source
29                    </a>
30                </div>
31            </div>
32        </div>
33    </div>
34    <button * ngIf="isHome$ | async" routerLink="/projects" class="mt-3 btn btn-dark">
35        See More Projects
36        <i class="ml-1 fas fa-angle-right"></i>
37    </button>
38</div>

在这里,您要将 projects$ 中的每个 project 添加到一个卡片中。在卡片中,您将显示项目名称(project.name)、项目中使用的技术栈(project.stack)以及项目功能的简要说明(project.description)。您还将添加项目代码托管位置的链接。此外,如果项目已部署,您还将添加一个链接,指向可预览项目的位置。最后,还有一个 "查看更多项目 "按钮,只显示在主页上。在主页上,只显示特色项目。点击该按钮后,用户将进入完整的项目列表。

保存并关闭文件。

接下来,你将通过修改项目模板来设计项目卡片。打开 src/app/portfolio/projects/projects.component.css 并添加以下几行:

 1[label src/app/portfolio/projects/projects.component.css]
 2.vw-20 {
 3    width: 20vw;
 4}
 5
 6.project-card {
 7    width: 290px;
 8    height: 250px;
 9}
10
11.project-title {
12    white-space: nowrap;
13    overflow: hidden;
14    text-overflow: ellipsis;
15    max-width: 20ch;
16}

在这里,您可以设置项目卡和项目标题的大小,项目标题往往会稍长一些。

完成后,完整列表的 "项目 "页面将如下所示(您可以在最后一步预览网站):

完整列表项目页面

添加其余投资组合路线

为了使每个页面都能被访问,您需要为每个页面创建一个路由。您将在 "PortfolioRoutingModule "中添加这些路由,该模块负责处理 "PortfolioModule "的路由。关于 "页面应位于 /about,"项目 "页面应位于 /projects

要为投资组合模块页面创建路由,需要修改负责路由的投资组合路由模块文件。打开 src/app/portfolio/portfolio-routing.module.ts,添加高亮显示的行:

 1[label src/app/portfolio/portfolio-routing.module.ts]
 2import { NgModule } from '@angular/core';
 3import { RouterModule, Routes } from '@angular/router';
 4import { HomeComponent } from './home/home.component';
 5import { ProjectsComponent } from './projects/projects.component';
 6import { AboutComponent } from './about/about.component';
 7
 8const routes: Routes = [
 9  { path: '', component: HomeComponent },
10  { path: 'about', component: AboutComponent },
11  { path: 'projects', component: ProjectsComponent }
12];
13
14@NgModule({
15  imports: [RouterModule.forChild(routes)],
16  exports: [RouterModule]
17})
18export class PortfolioRoutingModule { }

在这里,您为 "关于 "和 "项目 "页面添加了路由,方法是指定组件的路径并将其添加到routes数组中。

在这一步中,您创建了作品集模块的三个页面,并为它们添加了路由。下一步,您将生成博客组件。

第 4 步 - 生成博客模块

在这一步中,您将生成博客模块,其中包含博客登陆页面和文章页面。您将使用 Scully 原理图来设置一个正常运行的博客所需的一切,而不是从头开始构建博客。Scully 方案生成模块,添加路由模块以处理博客路由,并创建一个显示博文的博客组件。博客组件将显示以 markdown 文件编写的文章。在后面的步骤中,当你创建新博文时,你将看到这些标记文件的位置。在渲染博客时,Scully 会将您创建的标记符版本的博文转换为静态 HTML 页面,以便更快地发送给读者。

您可以从项目根目录运行以下命令,为应用程序启用博客支持并生成模块:

1ng generate @scullyio/init:blog

上述命令在 src/app/blog 中创建了博客模块,在项目基础中创建了一个 blog 文件夹,用于存放博客标记文件,在 AppRoutingModule 中为模块添加了一个懒加载路由,并在模块基础中创建了一个博客组件。

接下来,在模块中创建一个文件夹,用于存放博客组件。

1mkdir src/app/blog/blog

要将博客组件移入该文件夹,请运行

1mv src/app/blog/blog.component.* src/app/blog/blog/

这样就形成了这种博客模块结构:

1[label src/app/blog]
2src/app/blog
3├── blog
4│   ├── blog.component.css
5│   ├── blog.component.html
6│   ├── blog.component.spec.ts
7│   └── blog.component.ts
8├── blog-routing.module.ts
9└── blog.module.ts

由于该模块已被重组,一些路径将被破坏并需要更新。两个文件,blog-routing.module.tsblog.module.ts 将需要更新,加入到 BlogComponent 的新路径。

打开 blog-routing.module.ts 并更新导入,如图所示:

1[label src/app/blog/blog-routing.module.ts]
2...
3import { BlogComponent } from './blog/blog.component';
4...

保存并关闭文件。

接下来,打开 blog.module.ts 并更新导入,如图所示:

1[label src/app/blog/blog.module.ts]
2...
3import { BlogComponent } from './blog/blog.component';
4...

保存并关闭文件。

接下来,您将修改博客组件的模板。博客组件的作用是显示博文。该组件只需极少的编辑,因为 Scully 博客示意图已经填充了该组件。您将为容纳博文内容的容器添加样式。打开 src/app/blog/blog/blog.component.html,用以下几行替换模板内容:

1[label src/app/blog/blog/blog.component.html]
2<div class="vw-70">
3    <scully-content></scully-content>
4</div>

添加到模板中的样式将使博客组件在页面中的间距更合理。<scully-content></scully-content>将呈现标记符博客内容。

保存并关闭文件。

接下来,你将修改样式,将标题居中,这样就能为博客组件创造更好的外观和感觉。打开 src/app/blog/blog/blog.component.css,将内容替换为以下几行:

1[label src/app/blog/blog/blog.component.css]
2h1, h2, h3, h4, h5, h6 {
3  text-align: center;
4  padding: 1rem;
5}

保存并关闭文件。

完成后,博客将是这样的(您可以在最后一步预览网站):

博客文章页面](assets/67804/7d4nSOT.png)

生成博客登陆页面

现在,您已创建了博客模块并为博客文章添加了样式,您将生成博客着陆页并为着陆页添加样式。

博客登陆页面将列出所有博客文章。您可以在项目根目录下运行以下命令生成该页面:

1ng generate component blog/blog-landing

这样就形成了这种结构:

 1[label src/app/blog]
 2src/app/blog
 3├── blog
 4│   ├── blog.component.css
 5│   ├── blog.component.html
 6│   ├── blog.component.spec.ts
 7│   └── blog.component.ts
 8├── blog-landing
 9│   ├── blog-landing.component.css
10│   ├── blog-landing.component.html
11│   └── blog-landing.component.ts
12├── blog-routing.module.ts
13└── blog.module.ts

接下来,您将修改博客登陆页面的组件文件,以列出所有博客文章。在此,您将获取路径中包含 /blog/的所有页面,并将其显示在列表中。您还将添加选项,使页面在不同尺寸的屏幕上都能响应。

打开 src/app/blog/blog-landing/blog-landing.component.ts 并进行以下更改:

 1[label src/app/blog/blog-landing/blog-landing.component.ts]
 2import { Component } from '@angular/core';
 3import { ScullyRoute, ScullyRoutesService } from '@scullyio/ng-lib';
 4import { map } from 'rxjs/operators';
 5
 6@Component({
 7  selector: 'app-blog-landing',
 8  templateUrl: './blog-landing.component.html',
 9  styleUrls: ['./blog-landing.component.css']
10})
11export class BlogLandingComponent {
12  links$ = this.scully.available$.pipe(
13    map(routes => routes.filter((route: ScullyRoute) => route.route.startsWith('/blog/')))
14  );
15
16  respOptions = [
17    { viewClasses: 'd-none d-md-flex', displayInColumn: false, titleClasses: 'display-3' },
18    { viewClasses: 'd-flex d-md-none', displayInColumn: true, titleClasses: '' }
19  ];
20
21  constructor(private scully: ScullyRoutesService) { }
22}

要获取所有博客路由的列表,您需要使用 ScullyRoutesServiceavailable$ Observable 将返回 Scully 呈现并标记为 published 的所有路由。您可以在博文的标记文件 frontmatter 中标记博文是否已发布。(这个观察项将返回所有路径,包括来自组合的路径。因此,你将只过滤包含/blog/前缀的路由。博客路由将由 links$ 属性保存。respOptions 属性将有助于提高响应速度。

保存并关闭文件。

接下来,您将修改博客登陆页面的模板,以卡片形式列出所有可用的博客文章并链接到它们。它还包含博客的标题。打开 src/app/blog/blog-landing/blog-landing.component.html 并添加以下几行:

 1[label src/app/blog/blog-landing/blog-landing.component.html]
 2<div * ngFor="let options of respOptions" [ngClass]="options.viewClasses"
 3    class="flex-column align-items-center text-center vw-90 mx-auto">
 4    <h1 [ngClass]="options.titleClasses" class="mb-5"><span class="font-weight-bold">Jane's</span> Blog</h1>
 5    <div [ngClass]="{'justify-content-center flex-wrap': !options.displayInColumn,  'flex-column align-items-center': options.displayInColumn}"
 6        class="d-flex vw-90">
 7        <div * ngFor="let page of links$ | async" class="card post-card m-3">
 8            <div class="card-img-top bg-dark">
 9                <i class="far fa-newspaper fa-4x m-5 text-white"></i>
10            </div>
11            <div class="card-body d-flex flex-column">
12                <h5 class="card-title post-title" [title]="page.title">{{page.title}}</h5>
13                <p class="card-text post-description flex-grow-1">{{page.description}}</p>
14                <a [routerLink]="page.route" class="btn btn-outline-dark align-self-center">
15                    <i class="fa-lg mr-1 far fa-eye"></i>
16                    Read
17                </a>
18            </div>
19        </div>
20    </div>
21</div>

在此模板中,您将循环浏览 Scully 路由器服务返回的所有博文。您将为每篇博文添加一个卡片。在每个卡片中,都会显示博文的标题和描述。此外,还添加了一个链接,点击后可进入博文。

保存并关闭文件。

最后,您将为博客登陆模板添加样式。它将为添加到页面中的项目卡设计样式。打开 src/app/blog/blog-landing/blog-landing.component.css 并添加以下几行:

 1[label src/app/blog/blog-landing/blog-landing.component.css]
 2.post-card {
 3    width: 290px;
 4    height: 360px;
 5}
 6
 7.post-title {
 8    white-space: nowrap;
 9    overflow: hidden;
10    text-overflow: ellipsis;
11    max-width: 20ch;
12}

保存并关闭文件。

完成后(添加博客文章后),博客登陆页面将如下所示(您可以在最后一步预览网站):

博客登陆](assets/67804/pC7jnfO.png)

添加博客登陆路径

要在 /blog 路径下访问博客登陆页面,必须在 BlogRoutingModule 中为其添加路由。如果不添加路由,应用程序将无法访问该页面。打开 src/app/blog/blog-routing.module.ts,添加高亮显示的行:

 1[label src/app/blog/blog-routing.module.ts]
 2...
 3import { BlogLandingComponent } from './blog-landing/blog-landing.component';
 4
 5const routes: Routes = [
 6  { path: '', component: BlogLandingComponent },
 7  { path: ':slug', component: BlogComponent },
 8  { path: '**', component: BlogComponent }
 9];
10...

在这里,您将 BlogLandingComponent 的路由添加到了 routes 数组中。这样就可以通过 /blog 路由访问该组件。

保存并关闭文件。

在此步骤中,您创建了一个包含两个页面的博客模块:博客文章页面和博客登陆页面。您为这些页面添加了样式,并添加了博客登陆路径,这样登陆页面就可以通过 /blog 路径访问。下一步,您将添加新的博客文章。

第 5 步 - 添加新的博客帖子

在这一步中,您将使用 Scully 生成新的博客文章,这些文章将显示在博客登陆页面上。您可以使用 Scully 生成标记符文件,作为您的博文。在上一步中生成的博客组件将读取博客文章的 Markdown 版本,然后将其显示出来。Markdown 可以让你轻松快捷地编写格式丰富的博客内容。Scully 会创建这些文件,并为你添加文件夹来存放它们。它还会为每篇文章添加标题和描述等元数据。其中一些元数据用于确定帖子的显示方式。稍后,你将使用 Scully 为这些标记符博文生成静态 HTML 页面版本。

在撰写博文之前,您需要为博文取一个名字。在本教程中,你将创建一个名为 "博文 1 "的博文。你将使用项目根目录中的 --name 标志为下面的命令提供这个名称。

1ng generate @scullyio/init:post --name="Blog Post 1"

输出结果将与此类似:

1[secondary_label Output]
2? What's the target folder for this post? blog
3    ✅️ Blog ./blog/blog-post-1.md file created
4CREATE blog/blog-post-1.md (103 bytes)

这将在项目根目录下创建一个 /blog/blog-post-1.md 文件。该文件的内容类似于下面的内容:

1[label blog/blog-post-1.md]
2---
3title: Blog Post 1
4description: blog description
5published: false
6---
7
8# Blog Post 1

为博文添加内容并对其感到满意后,可将 published 更改为 true,这样在渲染网站时,博文就会出现在博客登陆页面上。要查看尚未发布的博文,可以使用 slug 属性。

例如,假设您添加了这样一个标签:

1[label blog/blog-post-1.md]
2---
3title: Blog Post 1
4description: blog description
5published: true
6slug: alternate-url-for-blog-post-1
7---
8
9# Blog Post 1

运行服务器时,您可以在 https://localhost:1668/blog/alternate-url-for-blog-post-1 查看这篇文章。不过,除非标记为 "published: true",否则这篇未发布的文章不会显示在博客登陆页面上。在生成 Scully 路由时,您将在后面的步骤中看到,Scully 会为所有未发布的帖子添加一个标签,因此您不必这样做。

要为帖子添加内容,请从标题后开始。所有帖子内容都需要使用标记符。下面是一个内容示例,你可以在生成的标记符帖子中使用:

 1[label /blog/blog-post-1.md]
 2---
 3title: Blog Post 1
 4description: Your first blog post
 5published: true
 6---
 7
 8# Blog Post 1
 9
10Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus vitae tempor erat, eget accumsan lorem. Ut id sem id massa mattis dictum ullamcorper vitae massa. In luctus neque lectus, quis dictum tortor elementum sit amet. Mauris non lacinia nisl. Nulla tristique arcu quam, quis posuere diam elementum nec. Curabitur in mi ut purus bibendum interdum ut sit amet orci. Duis aliquam tristique auctor. Suspendisse magna magna, pellentesque vitae aliquet ac, sollicitudin faucibus est. Integer semper finibus leo, eget placerat enim auctor quis. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed aliquam nibh in mi convallis mattis nec ac mi. Nam sed sagittis purus.

完成后保存并关闭文件。

您可以通过运行来生成其他帖子:

1ng generate @scullyio/init:post --name="Blog Post 2"
2ng generate @scullyio/init:post --name="Blog Post 3"

这些命令将在 /blog/ 文件夹中创建另外两个标记文件,文件名由您指定。您可以像在第一篇文章中所做的那样,用上面的示例内容填充生成的文件。

在这一步中,您创建了第一篇 Scully 博文。下一步将介绍完成应用程序所需的更改。

第 6 步 - 启用锚滚动并清理应用程序组件模板

在预览应用程序之前要做的最后一件事包括启用锚滚动、添加全局样式和清理 app.component.html

在主页上,当访问者点击页眉中的项目时,他们应该被引导到同一页面上的特定部分。为此,您需要在 Angular 应用程序上启用锚滚动。进行所有这些更改后,就可以滚动到主页的各个部分。

首先,您将修改应用程序路由模块的模块文件。该模块负责整个应用程序的路由。在这里,您将启用锚滚动。打开 src/app/app-routing.module.ts 并添加突出显示的部分:

1[label src/app/app-routing.module.ts]
2...
3@NgModule({
4  imports: [RouterModule.forRoot(routes, { anchorScrolling: 'enabled' })],
5  exports: [RouterModule]
6})
7...

添加 { anchorScrolling: 'enabled' } 可以在路由器模块上启用 anchorScrolling,这样就可以跳转到主页上的不同部分。

保存并关闭文件。

生成 Angular 应用程序时,主应用程序组件的模板(src/app/app.component.html)包含占位符内容。这些占位符内容将显示在作品集的所有页面上。它看起来像这样

应用程序组件上的占位符内容](assets/67804/app-start.png)

由于您不需要在作品集中使用这些占位符内容,因此可以将其删除。

要删除主页面上生成的占位符代码,请打开 src/app/app.component.html,并用以下几行替换其内容:

1[label src/app/app.component.html]
2<div class="d-flex flex-column h-100 w-100">
3    <app-header></app-header>
4    <div class="d-flex flex-column flex-grow-1 align-items-center justify-content-center">
5        <router-outlet></router-outlet>
6    </div>
7</div>

在此文件中,您添加了标题组件 app-header 并在 router-outlet 周围放置了一个容器 div,以便在其下方显示路由页面。

接下来,您需要确保 AppModule 可以访问 app-header。由于 app-header 存在于另一个模块中,因此 AppModule 目前无法访问它。您需要将 CoreModule 作为导入添加到 src/app/app.module.ts,因为 CoreModule 可以访问头组件。打开 app.module.ts 并添加如下所示的导入。

 1[label src/app/app.module.ts]
 2import { NgModule } from '@angular/core';
 3import { BrowserModule } from '@angular/platform-browser';
 4
 5import { AppRoutingModule } from './app-routing.module';
 6import { AppComponent } from './app.component';
 7import { ScullyLibModule } from '@scullyio/ng-lib';
 8import { CoreModule } from './core/core.module';
 9
10@NgModule({
11  declarations: [
12    AppComponent
13  ],
14  imports: [
15    BrowserModule,
16    AppRoutingModule,
17    ScullyLibModule,
18    CoreModule
19  ],
20  providers: [],
21  bootstrap: [AppComponent]
22})
23export class AppModule { }

进行此更改可确保 AppModule 能访问 app-header

最后,您将通过修改 src/styles.css 对应用程序的全局样式进行一些调整。应用程序中的多个组件都会使用该文件中的样式。它有助于提升应用程序的整体外观和感觉,并可防止重复,因为样式会在各个组件中重复使用。

在继续运行网站之前,打开src/styles.css并添加以下几行:

 1[label src/styles.css]
 2html, body {
 3    width: 100%;
 4    height: 100%;
 5}
 6
 7body {
 8    font-family: 'Nunito', Arial, Verdana, Geneva, Tahoma, sans-serif;
 9    background: white;
10    background-image: radial-gradient(lightgray 5.5%, transparent 0);
11    background-size: 30px 30px;
12}
13
14.vw-90 {
15    width: 90vw;
16}
17
18.vw-80 {
19    width: 80vw;
20}
21
22.vw-70 {
23    width: 80vw;
24}
25
26.min-vh-10 {
27    min-height: 10vh;
28}

在该文件中,您将确保 htmlbody 采用全页高度和宽度。此外,您还将 Nunito 设为默认字体,并包含各种样式类,用于设置宽度和高度。

在这一步中,您启用了锚滚动、添加了全局样式并清理了应用程序组件模板。下一步,您将构建网站、呈现 Scully 路由并提供静态组合。

第 7 步 - 预览静态网站

现在您已经完成了所有必要的代码修改,可以使用 Scully 预览您的作品集了。这将包括构建网站、生成 Scully 路由,然后提供网站的静态版本。在这一步中,Scully 会将您的 Angular 应用程序预渲染为静态网站,并提供服务器为 Angular 应用程序和静态作品集提供服务。

在 Scully 对您的投资组合进行预处理之前,您需要构建投资组合。

1ng build

此命令将把您的作品集编译到 "dist/portfolio"。

输出结果将与此类似:

 1Compiling @angular/core : es2015 as esm2015
 2Compiling @angular/common : es2015 as esm2015
 3Compiling @angular/platform-browser : es2015 as esm2015
 4Compiling @angular/router : es2015 as esm2015
 5Compiling @angular/platform-browser-dynamic : es2015 as esm2015
 6Compiling @angular/common/http : es2015 as esm2015
 7Compiling @angular/forms : es2015 as esm2015
 8Compiling @scullyio/ng-lib : es2015 as esm2015
 9Compiling @ng-bootstrap/ng-bootstrap : es2015 as esm2015
10✔ Browser application bundle generation complete.
11✔ Copying assets complete.
12✔ Index html generation complete.
13
14Initial Chunk Files           | Names                      |      Size
15vendor.js                     | vendor                     |   3.49 MB
16styles.css                    | styles                     | 202.25 kB
17polyfills.js                  | polyfills                  | 141.85 kB
18main.js                       | main                       |  24.91 kB
19runtime.js                    | runtime                    |   9.06 kB
20
21                              | Initial Total              |   3.86 MB
22
23Lazy Chunk Files              | Names                      |      Size
24portfolio-portfolio-module.js | portfolio-portfolio-module |  34.19 kB
25blog-blog-module.js           | blog-blog-module           |  15.28 kB
26
27Build at:  - Hash:  - Time: 29012ms

构建完成后,运行

1npx scully

Scully 将预先渲染整个作品集,方法是获取每条路由,并为每条路由创建单独的 index.html。预渲染的作品集将位于 dist/static。该文件夹应与下面相似。(为清晰起见,删除了部分文件)。

 1[label dist/static]
 2dist/static
 3├── about
 4│   └── index.html
 5├── assets
 6├── blog
 7│   ├── angular-unit-testing
 8│   │   └── index.html
 9│   ├── create-a-blog-using-vue.js
10│   │   └── index.html
11│   ├── how-to-create-a-twitter-bot
12│   │   └── index.html
13│   └── index.html
14├── index.html
15└── projects
16    └── index.html

请注意每个路由都有自己独立的 index.html 文件。

要预览静态网站,请运行

1npm run scully:serve

此命令将在 http://localhost:1668/ 上启动一个静态 Scully 服务器,并为您的静态作品集提供服务。(完成网站预览后,可以在服务器运行的终端上使用 Ctrl + C 命令杀死服务器)。

<$>[注] 注: Scully 在定位 Puppeteer 时可能会遇到问题。当它试图在受限环境(如 CI 服务或云中的虚拟机)中运行应用程序时,就会出现这种情况。如果尝试在 DigitalOcean Droplet 上运行应用程序,可能会出现此错误。该错误如下所示

 1=================================================================================================
 2Puppeteer cannot find or launch the browser. (by default chrome)
 3 Try adding 'puppeteerLaunchOptions: {executablePath: CHROMIUM_PATH}'
 4 to your scully.* .config.ts file.
 5Also, this might happen because the default timeout (60 seconds) is to short on this system
 6this can be fixed by adding the --serverTimeout=x cmd line option.
 7   (where x = the new timeout in milliseconds)
 8When this happens in CI/CD you can find some additional information here:
 9https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md
10=================================================================================================

要解决这个问题,请将突出显示的部分添加到 scully.portfolio.config.ts

 1[label scully.portfolio.config.ts]
 2import { ScullyConfig } from '@scullyio/scully';
 3export const config: ScullyConfig = {
 4  projectRoot: "./src",
 5  projectName: "portfolio",
 6  outDir: './dist/static',
 7  routes: {
 8    '/blog/:slug': {
 9      type: 'contentFolder',
10      slug: {
11        folder: "./blog"
12      }
13    },
14  },
15  puppeteerLaunchOptions: {args: ['--no-sandbox', '--disable-setuid--sandbox']}
16};

通过 puppeteerLaunchOptions 选项,可以更改 Puppeteer 的默认选项,并用适合你的环境的选项覆盖它们。no-sandbox "和 "disable-setuid--sandbox "禁用为 Puppeteer 提供的多层沙盒。你可以在Chrome 浏览器故障排除资源阅读更多相关信息。根据你的设置,你可能还需要安装额外的依赖项来运行 Chromium,你可以在 Puppeteer 故障排除指南 中了解更多信息。
<$>

http://localhost:4200 "的主页应该是这个样子:

主页](assets/67804/home-page.png)

在此步骤中,您构建了 Angular 应用程序,将其预渲染为静态网站,并使用 Scully 提供服务。

结论

在本教程中,您生成并配置了一个 Angular 投资组合应用程序。您还创建了一个核心模块,用于处理您的作品集数据并保存应用程序的核心组件。此外,您还创建了一个作品集模块,该模块由展示您的简介、项目和个人资料的重要页面组成。您还创建了一个博客模块,由您的博客登陆页面和文章页面组成。最后,使用 Scully 将 Angular 作品集转换为静态网站。

您的作品集还有很多内容可做。您可以添加页面,展示您的技能和撰写的文章。您还可以添加联系页面,以便人们与您取得联系。如果你有演讲活动、视频教程频道、播客或会议演讲,你可以创建页面来展示它们。

此外,Scully 还提供其他有用的功能,例如为博文中的代码集成语法高亮功能。您可以在 Scully 产品文档 中了解有关使用 prismjs 进行语法高亮的更多信息。

最后,您可以添加测试并部署投资组合。您可以在作者的 GitHub上查看此应用程序的实时版本。本项目的源代码(以及更高级版本)可在 GitHub 上获取。要了解如何在 DigitalOcean 上部署类似的静态网站,请查看 App Platform 上的 这些教程

<$>[注] 注: 本教程使用 Angular 主版本 11 制作。请考虑升级到与 Scully 最新版本兼容的最新版本。您可以使用Angular 的更新工具 来了解如何升级。您还可以考虑更新本教程中使用的其他依赖项,例如 Font Awesome 和 ng-bootstrap。
<$>

Published At
Categories with 技术
comments powered by Disqus