如何使用Angular11和Scully构建Jamstack产品组合

来自菜鸟教程
跳转至:导航、​搜索

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

介绍

作为开发人员,您可以通过多种方式展示您的技能、成就和工作。 这些包括开源贡献、个人项目、演讲活动、博客文章等。 但是,如果您的工作分散在多个平台上并且当人们查找您时很难找到,那么做这一切可能都是徒劳的。 没有一个展示你成就的中心位置可能会对你不利,并可能导致潜在客户、招聘人员和雇主低估你的价值。 作品集可以让您将最好的工作向前推进,让您的成就更容易被发现,帮助您树立自己的品牌,并促进能够带来潜在利润丰厚机会的联系。 将您的作品集与博客配对可以让您分享您的想法、记录您的学习内容并进一步建立您的信誉。

使用它们广泛的强大功能,您可以使用 AngularScully 构建引人入胜且快速的产品组合。 Angular 是一个多功能平台,可让您创建从 Web 到本机和移动应用程序的所有内容。 它提供了各种有用的工具来简化应用程序开发,从而产生更快的应用程序和出色的性能。

Angular 应用程序可以通过将它们设为静态来变得更快。 使用 Scully,您可以将 Angular 应用程序转换为交付速度更快的 Jamstack 应用程序。 Scully 是一个静态站点生成器,它为 Angular 应用程序中的每个路由创建一个静态 HTML 页面。 这些页面交付速度更快,并且对 SEO 有效。 它还提供了博客生成器等工具,您可以使用这些工具制作您的投资组合博客。

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

先决条件

要完成本教程,您需要:

  • 运行 Node.js v12 或更高版本的开发环境。 对于 macOS 或 Ubuntu 18.04,请按照 如何在 macOS 上安装 Node.js 并创建本地开发环境或 如何在 Ubuntu 18.04 上安装 Node.js 中的步骤进行操作。 对于其他操作系统,请查看 Node.js 下载 页面。 本教程是使用 Node.js v16.3.0 和 NPM v7.15.1 创建的。
  • 已安装 Angular CLI,您可以按照 使用 Angular CLI 开始使用 Angular 教程的 Step 1 来完成。 本教程是使用 Angular CLI v11.2.14 创建的。 您可以通过运行以下命令安装此版本:

    npm -g install @angular/cli@11.2.14
    
  • 已安装 Chromium(仅限 Windows 用户)。 Scully 需要 Chromium 。 如果您使用的是 macOS、Ubuntu 或其他操作系统,默认情况下,Scully 会下载一个本地版本,如果尚未安装,它将用于呈现静态站点。 但是,如果您运行的是 Windows,则需要在 WSL 上安装 Chrome,您可以按照 Scully WSL 先决条件的本指南来完成。
  • 了解 HTML、CSS 和 TypeScript,您可以在 如何使用 HTML 教程、如何使用 CSS 设置 HTML 样式和 中找到这些内容]如何在 Typescript 系列中编码。
  • 熟悉 RxJS,可以在 RxJS 产品文档 中找到。
  • 对 Angular 的了解,您可以在 Angular CLI 入门 教程中找到。

第 1 步 — 设置 Angular 应用程序

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

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

ng new portfolio -S

当被问及是否要添加 Angular 路由时,使用 yes 进行响应。 这告诉 CLI 工具生成一个单独的模块来处理应用程序的路由。 它将在 src/app/app-routing.module.ts 上提供。

您还将被提示选择样式表格式。 选择 CSS。 Angular 提供了其他样式选项,例如 SCSS、Sass、Less 和 Stylus。 CSS 更简单,这就是你要在这里使用它的原因。

Output? Would you like to add Angular routing? Yes
? Which stylesheet format would you like to use? (Use arrow keys)
❯ CSS 

安装依赖

此应用程序需要三个依赖项:Scullyng-bootstrapFont Awesome。 Scully 会将 Angular 应用程序转换为静态应用程序。 其他依赖项,Bootstrap 和 Font Awesome,可自定义产品组合的外观。 ng-bootstrap 将提供使用 Bootstrap 样式的 Angular 组件。 这特别有用,因为并非所有的普通 Bootstrap 组件都可以在 Angular 中开箱即用。 Bootstrap 还减少了您必须添加到应用程序中的样式数量,因为它已经为您提供了它。 Font Awesome 将提供图标。

当您将 Scully 添加为依赖项时,它会运行其 init 原理图,该原理图会向您的应用程序添加更改,以准备生成静态站点。 在您的项目目录中,使用此命令将 Scully 添加到您的项目中:

ng add @scullyio/init@2.0.0

接下来,使用此命令添加 ng-bootstrap

ng add @ng-bootstrap/ng-bootstrap@9.1.3

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

npm install --save @fortawesome/fontawesome-free@5.15.4

既然添加了依赖项,您就可以添加配置了。

添加配置

要使应用程序可以使用 Font Awesome,您需要在 angular.json 文件中添加对其缩小 CSS 的引用。 该文件可以在项目的基础上找到。 使用 nano 或您喜欢的文本编辑器,打开此文件:

nano angular.json

在文件中,查找 architect/build 部分。 本节提供 ng build 命令的配置。 您将在 styles 数组中添加缩小的 Font Awesome CSS 参考 node_modules/@fortawesome/fontawesome-free/css/all.min.cssstyles 数组在 projects/portfolio/architect/build/options 下。 将突出显示的行添加到您的文件中:

角.json

{
  ...
  "projects": {
    "portfolio": {
      ...
      "architect": {
        "build": {
          ...
          "options": {
              ...
              "styles": [
                  "node_modules/bootstrap/dist/css/bootstrap.min.css",
                  "node_modules/@fortawesome/fontawesome-free/css/all.min.css",
                  "src/styles.css"
              ],
          }
        },
        ...
      }
    }
  }
}

现在 Font Awesome 将可用于构建。

保存并关闭文件。

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

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

src/index.html

... 
<head>
  <meta charset="utf-8">
  <title>Portfolio</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Nunito:wght@200;400;800&display=swap" rel="stylesheet">
</head>
... 

突出显示的行链接到 Google 字体 上的 Nunito 字体。 您将获得三种重量:超轻、常规和超粗。

保存并关闭文件。

添加生物和项目数据

接下来要做的是创建 JSON 文件,其中包含您想要放入投资组合的所有数据。 将模板与数据分离可以更轻松地进行更改并在将来添加更多内容,而不会篡改应用程序。

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

mkdir src/assets/json

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

bio.json 文件的结构类似于:

src/assets/json/bio.json

{
    "firstName": "Jane",
    "lastName": "Doe",
    "intro": [ "paragraph 1", "paragraph 2" ],
    "about": [ "paragraph 1", "paragraph 2" ]
}

intro是首页上显示的简短介绍。 about 是“关于”页面上显示的更扩展的配置文件。

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

src/assets/json/bio.json

{
    "firstName": "Jane",
    "lastName": "Doe",
    "intro": [
        "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.",
        "You can have a look at some of my work here."
    ],
    "about": [
        "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.",
        "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."
    ]
}

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

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

src/assets/json/projects.json

[
    {
        "name": "",
        "stack": {
            "name": "Vue.js",
            "iconClasses": "fab fa-vuejs"
        },
        "description": "",
        "sourceUrl": "",
        "previewUrl": "",
        "featured": false
    }
]

每个项目都有一个名称、描述、一个指向其源代码托管位置的 URL,以及一个预览 URL(如果它部署在某个地方)。 如果项目没有预览 URL,您可以省略它。

stack 对象用于显示构建项目的语言或框架。 所以名称将是语言/框架的名称,iconClasses 是语言/框架图标的 Font Awesome CSS 类。 featured 属性表示项目是否应该显示在主页上。 如果 featured 设置为 false,则它仅显示在“项目”页面上,而不是主页和“项目”页面上。

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

src/assets/json/projects.json

[
    {
        "name": "Soduko",
        "stack": {
            "name": "Angular",
            "iconClasses": "fab fa-angular"
        },
        "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
        "sourceUrl": "https://github.com",
        "previewUrl": "https://github.com",
        "featured": true
    },
    {
        "name": "E-commerce Store",
        "stack": {
            "name": "React",
            "iconClasses": "fab fa-react"
        },
        "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
        "sourceUrl": "https://github.com",
        "previewUrl": "https://github.com",
        "featured": true
    },
    {
        "name": "Algorithm Visualization App",
        "stack": {
            "name": "Angular",
            "iconClasses": "fab fa-angular"
        },
        "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
        "sourceUrl": "https://github.com",
        "previewUrl": "https://github.com",
        "featured": true
    },
    {
        "name": "Time Tracking CLI App",
        "stack": {
            "name": "Node.js",
            "iconClasses": "fab fa-node-js"
        },
        "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
        "sourceUrl": "https://github.com",
        "previewUrl": "https://github.com",
        "featured": true
    }
]

保存并关闭文件。

启动服务器

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

ng serve

此命令构建应用程序并为其提供服务。 如果您进行任何更改,它会重建它。 完成后,应用程序将在 http://localhost:4200/ 提供。

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

每次保存更改时,您都应该看到与此类似的输出:

✔ Browser application bundle generation complete.

Initial Chunk Files           | Names                      |      Size
vendor.js                     | vendor                     |   3.83 MB
styles.css                    | styles                     | 202.25 kB
polyfills.js                  | polyfills                  | 141.85 kB
main.js                       | main                       |  26.08 kB
runtime.js                    | runtime                    |   9.06 kB

                              | Initial Total              |   4.20 MB

Build at:  - Hash:  - Time: 13312ms

** Angular Live Development Server is listening on localhost:4200, open your browser on http://localhost:4200/ **

✔ Compiled successfully.

完成一个步骤后,检查 ✔ Compiled successfully. 消息是否存在。 如果有问题,ng serve命令会输出错误。 发生这种情况时,请执行该步骤以确保您没有遗漏任何内容或犯错误。 完成本教程后,作品集主页应如下所示:

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

第 2 步 - 创建核心模块

在本教程中,您正在构建的应用程序将包含三个模块:coreblogportfolio。 该应用程序的结构如下:

源/应用

src/app
├── blog
├── core
└── portfolio 

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

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

这是核心模块的样子:

源代码/应用程序/核心/

src/app/core
├── header
├── models
└── services

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

ng generate module core

该命令将核心模块添加到src/app/core文件夹中,并将模块文件添加到src/app/core/core.module.ts

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

打开 core.module.ts 并添加突出显示的行(确保在 CommonModule 导入后包含逗号):

src/app/core/core.module.ts

...
import { RouterModule } from '@angular/router';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { HttpClientModule } from '@angular/common/http';

@NgModule({
  declarations: [],
  imports: [
    CommonModule,
    RouterModule,
    NgbModule,
    HttpClientModule
  ]
})
...

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

完成后保存并关闭文件。

生成数据模型

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

源代码/应用程序/核心/模型

src/app/core/models
├── bio.ts
└── project.ts

Bio 模型代表一个配置文件,而 Project 模型是一个展示项目。 您将通过在项目根目录运行以下命令来生成这两个模型:

for 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 文件中,添加下面突出显示的字段。

src/app/core/models/bio.ts

export interface Bio {
    firstName: string;
    lastName: string;
    about: string[];
    intro: string[];
}

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

保存并关闭文件。

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

src/app/core/models/project.ts

export interface Project {
    name: string;
    stack: { iconClasses: string, name: string };
    description: string;
    sourceUrl: string;
    previewUrl: string;
    featured?: boolean;
}

您已为项目模型添加了字段。 每个项目都有一个名称、描述、一个指向其源代码的 URL 以及一个预览 URL(如果它部署在某个地方)。 stack 对象用于显示项目的语言或框架。 (名称将是语言/框架的名称,iconClasses 是语言/框架图标的 Font Awesome CSS 类。) featured 属性指示项目是否应该显示在主页上。 如果 featured 设置为 false,则它仅显示在“项目”页面上,而不是主页和“项目”页面上。

完成后保存并关闭文件。

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

生成服务

您将在本节中创建的服务从您之前创建的 JSON 文件中获取数据。 一旦他们获取了这些数据,组件就可以调用这些服务并使用这些数据。 这些模型将用作这些服务中的返回类型。 bio 模型用于 bio 服务,project 模型用于项目服务。 您将包含一个额外的标头服务,该服务将帮助决定将哪些路由用于标头和其他组件中的项目。 core模块有BioServiceHeaderServiceProjectsService三种服务。

源代码/应用程序/核心/服务

src/app/core/services
├── bio.service.ts
├── header.service.ts
└── projects.service.ts

要生成这些服务,请从项目的根目录运行以下命令:

for 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 文件并添加以下突出显示的行:

src/app/core/services/bio.service.ts

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Bio } from '../models/bio';

@Injectable({
  providedIn: 'root'
})
export class BioService {

  constructor(private http: HttpClient) { }

  getBio() {
   return this.http.get<Bio>('assets/json/bio.json');
  }
}

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

保存并关闭文件。

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

src/app/core/services/header.service.ts

import { Injectable } from '@angular/core';
import { NavigationEnd, Router } from '@angular/router';
import { filter, map, startWith } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class HeaderService {

  constructor(private router: Router) { }

  isHome() {
    return this.router.events.pipe(
      filter(event => event instanceof NavigationEnd),
      map(event => {
        if (event instanceof NavigationEnd) {
          if (this.checkForHomeUrl(event.url)) {
            return true;
          }
        }

        return false;
      }),
      startWith(this.checkForHomeUrl(this.router.url))
    );
  }

  private checkForHomeUrl(url: string): boolean {
    return url.startsWith('/#') || url == '/';
  }
}

HeaderService中,isHome方法检查你当前所在的页面是否是首页。 这对于滚动到锚点并在主页上显示特色项目很有用。

保存并关闭文件。

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

src/app/core/services/projects.service.ts

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { filter, mergeAll, toArray } from 'rxjs/operators';
import { Project } from '../models/project';

@Injectable({
  providedIn: 'root'
})
export class ProjectsService {

  constructor(private http: HttpClient) { }

  getProjects(featured?: boolean): Observable<Project[]> {
    let projects$ = this.http.get<Project[]>('assets/json/projects.json');

    if (featured) {
      return projects$.pipe(
        mergeAll(),
        filter(project => project.featured || false),
        toArray()
      );
    }

    return projects$;
  }
}

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

保存并关闭文件。

在本节中,您添加了从 bio 和 projects JSON 文件中获取 bio 和 project 数据的 bio 和 project 数据服务。 您还创建了一个标头服务来检查当前页面是否是主页。 在下一部分中,您将创建一个标题,该标题将显示在您的投资组合的每一页的顶部。 它将使用 bio 和 header 服务。

生成标题

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

ng generate component core/header

标题组件显示在每个页面的顶部。 它将使用生物服务来获取您的名字和姓氏。 它还将使用 header 服务来确定当前页面是否是主页。 使用此信息,它将设置指向“关于”和“项目”部分或页面的链接。

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

打开 header.component.ts 并添加突出显示的行:

src/app/core/header/header.component.ts

import { Component } from '@angular/core';
import { BioService } from '../services/bio.service';
import { HeaderService } from '../services/header.service';

@Component({
  selector: 'app-header',
  templateUrl: './header.component.html',
  styleUrls: ['./header.component.css']
})
export class HeaderComponent {
  bio$ = this.bioService.getBio();
  isHome$ = this.headerService.isHome();

  menuItems = [
    { title: 'About Me', homePath: '/', fragment: 'about', pagePath: '/about' },
    { title: 'My Projects', homePath: '/', fragment: 'projects', pagePath: '/projects' },
    { title: 'My Blog', homePath: '/blog', fragment: '', pagePath: '/blog' }
  ];

  constructor(private bioService: BioService, private headerService: HeaderService) { }
}

在这个组件文件中,您将注入两个服务:bioService 用于从 bio JSON 文件中获取您的姓名,headerService 用于确定当前显示的页面是否为主页。 后者允许您决定按钮是否应该像 /projects 那样转到单独的页面,或者像 /#project 那样执行锚点滚动。 menuItems 包含所有要显示的菜单项。 bio$isHome$ 属性包含来自上述服务的 observables。

保存并关闭文件。

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

src/app/core/header/header.component.html

<div class="d-flex min-vh-10 w-100 justify-content-center pb-3 pt-3 pr-4 pl-4">
    <div class="d-flex justify-content-start" *ngIf="bio$ | async as bio" routerLink="/">
        <h2 class="font-weight-bold">{{bio.firstName}}</h2>
        <h2 class="font-weight-light">{{bio.lastName}}</h2>
    </div>
    <div class="d-none d-md-flex flex-grow-1 justify-content-end align-items-start">
        <button type="button" class="ml-2 mr-2 btn btn-outline-dark border-0 font-weight-bold"
            *ngFor="let item of menuItems" [routerLink]="(isHome$ | async) ? item.homePath : item.pagePath"
            [fragment]="(isHome$ | async) ? item.fragment : ''">{{item.title}}</button>
    </div>
    <div class="d-flex d-md-none justify-content-end flex-grow-1">
        <div ngbDropdown class="d-inline-block" display="dynamic" container="body">
            <button class="btn btn-outline-dark border-0" ngbDropdownToggle>
                <i class="fas fa-lg fa-bars"></i>
            </button>
            <div ngbDropdownMenu class="dropdown-menu dropdown-menu-right">
                <button ngbDropdownItem *ngFor="let item of menuItems"
                    [routerLink]="(isHome$ | async) ? item.homePath : item.pagePath"
                    [fragment]="(isHome$ | async) ? item.fragment : ''">{{item.title}}</button>
            </div>
        </div>
    </div>
</div>

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

保存并关闭文件。

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

src/app/core/core.module.ts

...
@NgModule({
  ...
  
  imports: [
    ...
  ],
  exports: [ HeaderComponent ]
})
...

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

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

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

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

主页将使用标题和生物服务显示您的个人资料摘要。 “关于”页面是一个更深入的配置文件,将使用生物服务和模型。 “项目”页面展示您的项目,使用项目服务和模型来展示您的项目。 在此步骤结束时,您的投资组合模块的结构如下:

src/应用程序/组合

src/app/portfolio
├── about
├── home
└── projects

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

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

ng 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 并替换突出显示的行:

src/app/app-routing.module.ts

...
const routes: Routes = [
  {
    path: '',
    loadChildren: () => import('./portfolio/portfolio.module').then(m => m.PortfolioModule)
  }
];
...

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

保存并关闭文件。

创建主页

创建投资组合模块的命令也会创建一个 PortfolioComponent。 这是为模块设置路由时使用的占位符组件。 但是,此组件更合适的名称是 HomeComponent。 主页是您的投资组合的登陆页面。 它将包含您整个投资组合的摘要。 这使用户可以更轻松地了解您的工作,而无需导航到多个页面,从而降低失去兴趣的风险。

要更改此组件的名称,您将首先创建一个新文件夹来存放它。 从项目根目录运行以下命令:

mkdir -p  src/app/portfolio/home

接下来,您将把所有 PortfolioComponent 文件移动到这个新文件夹中。

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

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

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

find 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,它处理投资组合模块的路由。 进行突出显示的更改:

src/app/portfolio/portfolio-routing.module

...
import { HomeComponent } from './home/home.component';

const routes: Routes = [{ path: '', component: HomeComponent }];
...

保存并关闭文件。

接下来,打开组合模块文件 src/app/portfolio/portfolio.module.ts。 进行突出显示的更改:

src/app/portfolio/portfolio.module.ts

...
import { HomeComponent } from './home/home.component';


@NgModule({
  declarations: [
    HomeComponent
  ],
  ...
})
...

保存并关闭文件。

最后,打开src/app/portfolio/home/home.component.ts,主组件文件。 进行突出显示的更改:

src/app/portfolio/home/home.component.ts

...
@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.css']
})
export class HomeComponent implements OnInit {
...
}

保存并关闭文件。

在这些文件中,您将 PortfolioComponent 的所有实例更改为 HomeComponent,并将路径更新为指向 HomeComponent。 完成所有这些之后,投资组合模块应该如下所示。

src/应用程序/组合

src/app/portfolio
├── home
│   ├── home.component.css
│   ├── home.component.html
│   └── home.component.ts
├── portfolio-routing.module.ts
└── portfolio.module.ts

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

接下来,您将使用内容和样式填充 home 组件模板。 主页组件是投资组合的主页,并显示配置文件摘要。 (这是上面从投资组合组件重命名为主页组件的组件。)在此组件中,您需要获取要显示的生物数据并添加样式以使页面在不同的屏幕尺寸上响应。

打开 src/app/portfolio/home/home.component.ts 并更新代码以匹配以下内容:

src/app/portfolio/home/home.component.ts

import { Component } from '@angular/core';
import { BioService } from '../../core/services/bio.service';

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.css']
})
export class HomeComponent {
  bio$ = this.bioService.getBio();

  respOptions = [
    { viewClasses: 'd-none d-md-flex', headingClass: 'display-3', useSmallerHeadings: false },
    { viewClasses: 'd-flex d-md-none', headingClass: '', useSmallerHeadings: true }
  ];

  constructor(private bioService: BioService) { }
}

主页将显示您的姓名和简短的简历,这是从您在此处注入的 BioService 中检索到的。 一旦你调用了它的 getBio 方法,结果 observable 将存储在 bio$ 属性中。 respOptions 属性存储有助于确保视图响应的配置。

保存并关闭文件。

接下来,您将修改 home 组件的模板。 它负责在响应式不同的屏幕尺寸上显示来自生物服务的信息。 您将添加您的姓名、简短介绍以及稍后将介绍的 about 和 projects 组件。

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

src/app/portfolio/home/home.component.html

<div class="d-flex flex-column justify-content-center align-items-center w-100" *ngIf="bio$ | async as bio">
    <div class="d-flex flex-column min-vh-95 justify-content-center align-items-center w-100">
        <div *ngFor="let options of respOptions" [ngClass]="options.viewClasses"
            class="flex-column justify-content-center align-items-start w-75">
            <h1 [ngClass]="options.headingClass" class="text-left">Hello, 👋. My name is <span
                    class="font-weight-bold">{{bio.firstName+'
                    '+bio.lastName}}.</span></h1>
            <div *ngFor="let par of bio.intro">
                <h2 class="text-left" *ngIf="!options.useSmallerHeadings">{{par}}</h2>
                <h5 class="text-left" *ngIf="options.useSmallerHeadings">{{par}}</h5>
            </div>
            <button class="mt-3 mb-5 btn btn-outline-dark" routerLink="/" fragment="projects">
                See My Work
                <i class="ml-1 fas fa-angle-right"></i>
            </button>
        </div>
    </div>

    <div class="d-none d-md-block mt-5"></div>
    <app-about id="about" class="mb-3"></app-about>

    <div class="d-none d-md-block mt-5"></div>
    <app-projects id="projects" class="mb-5"></app-projects>
</div>

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

注意: 此模板中添加了一些尚不存在的其他组件。 这些是 About 和 Projects 组件。 这些是您接下来要添加的内容。 如果您正在运行服务器,这将产生错误。 您可以在生成它们之前将这些行注释掉。

src/app/portfolio/home/home.component.html

...
<app-about id="about" class="mb-3"></app-about>

...

<app-projects id="projects" class="mb-5"></app-projects>
...

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

src/app/portfolio/home/home.component.css

.min-vh-95 {
    height: 95vh;
}

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

完成后,主页将如下所示(您将能够在最后一步中预览该站点):

在此步骤中,您创建了显示投资组合摘要的主页。 在下一节中,您将生成“About”和“Project”组件。 这些将显示在主页上,也将用作独立页面。

生成关于和项目页面

您可以运行一个命令来同时创建剩余的“项目”和“关于”页面,而不是单独生成每个页面。 您可以通过从项目根目录运行以下命令来执行此操作:

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

此命令将遍历每个页面名称并生成它们。

填充关于页面

“关于”页面将显示您更深入的个人资料。 此页面上的信息是从 bio 服务中检索的,并且也使用 bio 模型。 该组件将显示在主页上。 它也将是一个独立的页面,有自己的路线。

要使用您的简历填充“About”页面,您将修改“About”组件文件以使用 bio 服务。 您还将设置选项以使页面在不同的显示器上响应。 打开 src/app/portfolio/about/about.component.ts 并添加突出显示的行:

src/app/portfolio/about/about.component.ts

import { Component } from '@angular/core';
import { BioService } from '../../core/services/bio.service';

@Component({
  selector: 'app-about',
  templateUrl: './about.component.html',
  styleUrls: ['./about.component.css']
})
export class AboutComponent {
  bio$ = this.bioService.getBio();

  respOptions = [
    { viewClasses: 'd-none d-md-flex', headingClass: 'display-3', useSmallerHeadings: false },
    { viewClasses: 'd-flex d-md-none', headingClass: '', useSmallerHeadings: true }
  ];

  constructor(private bioService: BioService) { }
}

“关于”信息将来自 BioService,一旦调用了它的 getBio 方法,observable 将存储在 bio$ 属性中。 respOptions 通过为不同的显示尺寸提供可选的 CSS 类来帮助提高响应能力。

保存并关闭文件。

接下来,您将修改“关于”页面的模板,以便您可以显示从生物服务检索到的信息。 打开 src/app/portfolio/about/about.component.html 并添加以下行:

src/app/portfolio/about/about.component.html

<div class="d-flex justify-content-center vw-90 mx-auto" *ngIf="bio$ | async as bio">
    <div *ngFor="let options of respOptions" [ngClass]="options.viewClasses"
        class="flex-column align-items-center text-center w-75">
        <h1 [ngClass]="options.headingClass" class="mb-5"><span class="font-weight-bold">About</span> Me</h1>
        <div *ngFor="let par of bio.about">
            <h4 *ngIf="!options.useSmallerHeadings" class="mb-4">{{par}}</h4>
            <h5 *ngIf="options.useSmallerHeadings" class="mb-4">{{par}}</h5>
        </div>
    </div>
</div>

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

保存并关闭文件。

完成后,“关于”页面将如下所示(您将能够在最后一步中预览该站点):

填充项目页面

“项目”页面将显示您从项目服务中检索到的所有项目。 该组件将在主页上使用,也将是一个独立页面。 它将与“关于”组件一起显示在主页上。 在主页上使用此组件时,应仅显示特色项目。 有一个 See More Projects 按钮,只会出现在主页上。 单击时,它会重定向到完整列表项目页面。

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

src/app/portfolio/projects/projects.component.ts

import { Component } from '@angular/core';
import { mergeMap } from 'rxjs/operators';
import { HeaderService } from '../../core/services/header.service';
import { ProjectsService } from '../../core/services/projects.service';

@Component({
  selector: 'app-projects',
  templateUrl: './projects.component.html',
  styleUrls: ['./projects.component.css']
})
export class ProjectsComponent {
  isHome$ = this.headerService.isHome();
  projects$ = this.isHome$.pipe(
    mergeMap(atHome => this.projectsService.getProjects(atHome))
  );

  respOptions = [
    { viewClasses: 'd-none d-md-flex', displayInColumn: false, useSmallerHeadings: false, titleClasses: 'display-3' },
    { viewClasses: 'd-flex d-md-none', displayInColumn: true, useSmallerHeadings: true, titleClasses: '' }
  ];

  constructor(private projectsService: ProjectsService, private headerService: HeaderService) { }
}

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

保存并关闭文件。

接下来,您将修改项目组件的模板。 使用您从项目服务中获得的项目,您将在此处循环并添加它们。 您将在卡片中显示有关每个项目的基本信息,并添加指向其代码托管位置以及可以预览它们的位置的链接。

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

src/app/portfolio/projects/projects.component.html

<div *ngFor="let options of respOptions" [ngClass]="options.viewClasses"
    class="flex-column align-items-center text-center vw-90 mx-auto">
    <h1 [ngClass]="options.titleClasses" class="mb-5"><span class="font-weight-bold">My</span> Projects</h1>
    <div class="d-flex vw-90"
        [ngClass]="{'justify-content-center flex-wrap': !options.displayInColumn, 'flex-column  align-items-center': options.displayInColumn}"
        *ngIf="projects$ | async as projects">
        <div *ngFor="let project of projects" class="card project-card m-3"
            [ngClass]="{'m-3': !options.displayInColumn, 'mb-3': options.displayInColumn}">
            <div class="card-body d-flex flex-column">
                <h5 class="card-title font-weight-bold text-left project-title" [title]="project.name">
                    {{project.name}}
                </h5>
                <h6 class="card-subtitle mb-2 font-weight-lighter text-left">
                    <i [ngClass]="project.stack.iconClasses"></i>
                    {{project.stack.name}}
                </h6>
                <p class="card-text text-left">
                    {{project.description}}
                </p>
                <div class="d-flex flex-row justify-content-start">
                    <a [href]="project.previewUrl" *ngIf="project.previewUrl" class="btn btn-dark mr-2">
                        <i class="fa-lg mr-1 far fa-eye"></i>
                        Preview
                    </a>
                    <a [href]="project.sourceUrl" *ngIf="project.sourceUrl" class="btn btn-dark">
                        <i class="fa-lg mr-1 fab fa-github-alt"></i>
                        Source
                    </a>
                </div>
            </div>
        </div>
    </div>
    <button *ngIf="isHome$ | async" routerLink="/projects" class="mt-3 btn btn-dark">
        See More Projects
        <i class="ml-1 fas fa-angle-right"></i>
    </button>
</div>

在这里,您将 projects$ 中的每个 project 添加到卡中。 在卡片中,您将显示项目名称 (project.name)、其中使用的技术堆栈 (project.stack) 以及其作用的简要说明 (project.description) . 您还将添加指向项目代码托管位置的链接。 此外,如果项目已部署,您将添加一个链接,指向可以预览项目的位置。 最后,还有一个 See More Projects 按钮,它只显示在主页上。 在主页上,只显示特色项目。 单击此按钮时,用户将被路由到完整的项目列表。

保存并关闭文件。

接下来,您将通过修改项目模板来设置项目卡的样式。 打开 src/app/portfolio/projects/projects.component.css 并添加以下行:

src/app/portfolio/projects/projects.component.css

.vw-20 {
    width: 20vw;
}

.project-card {
    width: 290px;
    height: 250px;
}

.project-title {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 20ch;
}

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

完成后,完整列表的“项目”页面将如下所示(您将能够在最后一步中预览该站点):

添加其余的投资组合路线

为了使每个页面都可以访问,您需要为每个页面创建一个路由。 您将在 PortfolioRoutingModule 中添加这些,它处理 PortfolioModule 的路由。 “关于”页面应该在 /about 和“项目”页面在 /projects 可用。

要为组合模块页面创建路由,您将修改负责路由的组合路由模块文件。 打开 src/app/portfolio/portfolio-routing.module.ts 并添加突出显示的行:

src/app/portfolio/portfolio-routing.module.ts

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { ProjectsComponent } from './projects/projects.component';
import { AboutComponent } from './about/about.component';

const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'about', component: AboutComponent },
  { path: 'projects', component: ProjectsComponent }
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class PortfolioRoutingModule { }

在这里,您通过指定组件的路径并将它们添加到 routes 数组,将路由添加到“关于”和“项目”页面。

在此步骤中,您通过创建其三个页面中的每一个并为它们添加路由来完成投资组合模块。 在下一步中,您将生成博客组件。

第 4 步 - 生成博客模块

在此步骤中,您将生成博客模块,其中包含您的博客登录页面和帖子页面。 您将使用 Scully 示意图来设置正常运行的博客所需的所有内容,而不是从头开始构建博客。 Scully 原理图生成模块,添加路由模块来处理博客的路由,并创建显示博客文章的博客组件。 博客组件将显示您在降价文件中编写的帖子。 当您创建新的博客文章时,您将在稍后的步骤中看到这些降价文件所在的位置。 在呈现博客时,Scully 会获取您创建的博客文章的降价版本并将它们转换为静态 HTML 页面,这样可以更快地交付给读者。

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

ng generate @scullyio/init:blog

上面的命令在 src/app/blog 处创建博客模块,在项目的基础上创建一个 blog 文件夹,博客降价文件将驻留,在 [X189X 中为模块添加延迟加载路由],并在模块的基础上创建一个博客组件。

接下来,在博客组件所在的模块中创建一个文件夹。

mkdir src/app/blog/blog

要将博客组件移动到此文件夹中,请运行:

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

这将产生这个博客模块结构:

源/应用程序/博客

src/app/blog
├── blog
│   ├── blog.component.css
│   ├── blog.component.html
│   ├── blog.component.spec.ts
│   └── blog.component.ts
├── blog-routing.module.ts
└── blog.module.ts

由于此模块已经过重组,一些路径将被破坏并且需要更新。 blog-routing.module.tsblog.module.ts 这两个文件必须使用 BlogComponent 的新路径进行更新。

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

src/app/blog/blog-routing.module.ts

...
import { BlogComponent } from './blog/blog.component';
...

保存并关闭文件。

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

src/app/blog/blog.module.ts

...
import { BlogComponent } from './blog/blog.component';
...

保存并关闭文件。

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

src/app/blog/blog/blog.component.html

<div class="vw-70">
    <scully-content></scully-content>
</div> 

添加到模板的样式将使博客组件在页面内更好地间隔。 <scully-content></scully-content> 将渲染降价博客内容。

保存并关闭文件。

接下来,您将通过使标题居中来修改样式,从而为博客组件创建更好的外观。 打开 src/app/blog/blog/blog.component.css 并将内容替换为以下几行:

src/app/blog/blog/blog.component.css

h1, h2, h3, h4, h5, h6 {
  text-align: center;
  padding: 1rem;
}

保存并关闭文件。

完成后,博客将如下所示(您将能够在最后一步中预览该站点):

生成博客登陆页面

现在您已经创建了博客模块并为博客文章添加了样式,您将生成博客登录页面并向登录页面添加样式。

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

ng generate component blog/blog-landing

这将导致以下结构:

源/应用程序/博客

src/app/blog
├── blog
│   ├── blog.component.css
│   ├── blog.component.html
│   ├── blog.component.spec.ts
│   └── blog.component.ts
├── blog-landing
│   ├── blog-landing.component.css
│   ├── blog-landing.component.html
│   └── blog-landing.component.ts
├── blog-routing.module.ts
└── blog.module.ts

接下来,您将修改博客登录页面的组件文件以列出所有博客文章。 在这里,您将获得路径中包含 /blog/ 的所有页面,并将它们显示在列表中。 您还将添加选项以使页面响应不同的屏幕尺寸。

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

src/app/blog/blog-landing/blog-landing.component.ts

import { Component } from '@angular/core';
import { ScullyRoute, ScullyRoutesService } from '@scullyio/ng-lib';
import { map } from 'rxjs/operators';

@Component({
  selector: 'app-blog-landing',
  templateUrl: './blog-landing.component.html',
  styleUrls: ['./blog-landing.component.css']
})
export class BlogLandingComponent {
  links$ = this.scully.available$.pipe(
    map(routes => routes.filter((route: ScullyRoute) => route.route.startsWith('/blog/')))
  );

  respOptions = [
    { viewClasses: 'd-none d-md-flex', displayInColumn: false, titleClasses: 'display-3' },
    { viewClasses: 'd-flex d-md-none', displayInColumn: true, titleClasses: '' }
  ];

  constructor(private scully: ScullyRoutesService) { }
}

要获取所有博客路由的列表,您将使用 ScullyRoutesServiceavailable$ observable 将返回由 Scully 渲染并标记为 published 的所有路线。 您可以在其 markdown 文件 frontmatter 中标记博客文章是否已发布。 (这将在下一步中介绍。)这个 observable 将返回所有路由,包括来自投资组合的路由。 因此,您将仅过滤包含前缀 /blog/ 的路由。 博客路由将由 links$ 属性保存。 respOptions 属性将有助于响应。

保存并关闭文件。

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

src/app/blog/blog-landing/blog-landing.component.html

<div *ngFor="let options of respOptions" [ngClass]="options.viewClasses"
    class="flex-column align-items-center text-center vw-90 mx-auto">
    <h1 [ngClass]="options.titleClasses" class="mb-5"><span class="font-weight-bold">Jane's</span> Blog</h1>
    <div [ngClass]="{'justify-content-center flex-wrap': !options.displayInColumn,  'flex-column align-items-center': options.displayInColumn}"
        class="d-flex vw-90">
        <div *ngFor="let page of links$ | async" class="card post-card m-3">
            <div class="card-img-top bg-dark">
                <i class="far fa-newspaper fa-4x m-5 text-white"></i>
            </div>
            <div class="card-body d-flex flex-column">
                <h5 class="card-title post-title" [title]="page.title">{{page.title}}</h5>
                <p class="card-text post-description flex-grow-1">{{page.description}}</p>
                <a [routerLink]="page.route" class="btn btn-outline-dark align-self-center">
                    <i class="fa-lg mr-1 far fa-eye"></i>
                    Read
                </a>
            </div>
        </div>
    </div>
</div>

在此模板中,您将遍历 Scully 路由器服务返回的所有博客文章。 对于每篇博文,您都将添加一张卡片。 在每张卡片中,都会显示博客文章的标题和描述。 还添加了一个链接,可以单击该链接转到博客文章。

保存并关闭文件。

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

src/app/blog/blog-landing/blog-landing.component.css

.post-card {
    width: 290px;
    height: 360px;
}

.post-title {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 20ch;
}

保存并关闭文件。

完成后(并且在您添加了博客文章之后),博客登录页面将如下所示(您将能够在最后一步中预览该站点):

添加博客登陆路线

要使博客登录页面可在 /blog 路径中访问,您必须在 BlogRoutingModule 中为其添加路由。 如果不添加此内容,则该应用程序将无法使用它。 打开 src/app/blog/blog-routing.module.ts 并添加突出显示的行:

src/app/blog/blog-routing.module.ts

...
import { BlogLandingComponent } from './blog-landing/blog-landing.component';

const routes: Routes = [
  { path: '', component: BlogLandingComponent },
  { path: ':slug', component: BlogComponent },
  { path: '**', component: BlogComponent }
];
...

在这里,您将 BlogLandingComponent 的路由添加到了 routes 数组。 这将使它可以在 /blog 路线上访问。

保存并关闭文件。

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

第 5 步 — 添加新的博客文章

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

在你可以发帖之前,你需要想出一个名字。 对于本教程,您将创建一个标题为“Blog Post 1”的帖子。 您将使用项目根目录中的 --name 标志将此名称提供给以下命令。

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

输出将类似于以下内容:

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

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

博客/blog-post-1.md

---
title: Blog Post 1
description: blog description
published: false
---

# Blog Post 1

将内容添加到博客文章并对其感到满意后,您可以将 published 更改为 true,它会在您呈现网站时出现在博客登录页面上。 要查看尚未发布的帖子,您可以使用 slug 属性。

例如,假设您添加了这个 slug:

博客/blog-post-1.md

---
title: Blog Post 1
description: blog description
published: true
slug: alternate-url-for-blog-post-1
---

# Blog Post 1

当您运行服务器时,您将能够在 https://localhost:1668/blog/alternate-url-for-blog-post-1 上查看此帖子。 但是,除非标记为 published: true,否则此未发布的帖子不会显示在博客登录页面上。 在生成 Scully 路线时,您将在后面的步骤中看到,Scully 将为您所有未发布的帖子添加一个 slug,因此您不必这样做。

要将内容添加到您的帖子,请在标题之后开始。 所有帖子内容都需要在markdown中。 这是您可以在生成的降价帖子中使用的内容示例:

/blog/blog-post-1.md

---
title: Blog Post 1
description: Your first blog post
published: true
---

# Blog Post 1

Lorem 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.

完成后保存并关闭文件。

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

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

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

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

第 6 步 — 启用锚点滚动和清理应用程序组件模板

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

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

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

src/app/app-routing.module.ts

...
@NgModule({
  imports: [RouterModule.forRoot(routes, { anchorScrolling: 'enabled' })],
  exports: [RouterModule]
})
...

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

保存并关闭文件。

当您生成 Angular 应用程序时,主应用程序组件 (src/app/app.component.html) 的模板包含占位符内容。 此占位符内容显示在您投资组合的所有页面上。 它看起来像这样:

由于您的投资组合中不需要此占位符内容,因此您将删除它。

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

src/app/app.component.html

<div class="d-flex flex-column h-100 w-100">
    <app-header></app-header>
    <div class="d-flex flex-column flex-grow-1 align-items-center justify-content-center">
        <router-outlet></router-outlet>
    </div>
</div>

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

接下来,您需要确保 AppModule 可以访问 app-header。 由于 app-header 存在于不同的模块中,因此 App Module 当前无权访问它。 您需要将 CoreModule 添加为 src/app/app.module.ts 的导入,因为 CoreModule 提供对标头组件的访问。 打开 app.module.ts 并添加导入,如下所示。

src/app/app.module.ts

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { ScullyLibModule } from '@scullyio/ng-lib';
import { CoreModule } from './core/core.module';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    ScullyLibModule,
    CoreModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

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

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

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

src/styles.css

html, body {
    width: 100%;
    height: 100%;
}

body {
    font-family: 'Nunito', Arial, Verdana, Geneva, Tahoma, sans-serif;
    background: white;
    background-image: radial-gradient(lightgray 5.5%, transparent 0);
    background-size: 30px 30px;
}

.vw-90 {
    width: 90vw;
}

.vw-80 {
    width: 80vw;
}

.vw-70 {
    width: 80vw;
}

.min-vh-10 {
    min-height: 10vh;
}

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

在此步骤中,您启用了锚点滚动、添加了全局样式并清理了应用程序组件模板。 在下一步中,您将构建站点、渲染 Scully 路线并提供静态作品集。

第 7 步 — 预览静态站点

现在您已完成所有必要的代码更改,您可以使用 Scully 预览您的作品集。 这将涉及构建您的站点,生成 Scully 路由,然后提供站点的静态版本。 在这一步中,Scully 将您的 Angular 应用程序预渲染到一个静态站点中,并提供一个服务器来为 Angular 应用程序和静态产品组合提供服务。

在 Scully 可以预渲染您的作品集之前,您需要构建它。

ng build

此命令会将您的投资组合编译为 dist/portfolio

输出将类似于以下内容:

Compiling @angular/core : es2015 as esm2015
Compiling @angular/common : es2015 as esm2015
Compiling @angular/platform-browser : es2015 as esm2015
Compiling @angular/router : es2015 as esm2015
Compiling @angular/platform-browser-dynamic : es2015 as esm2015
Compiling @angular/common/http : es2015 as esm2015
Compiling @angular/forms : es2015 as esm2015
Compiling @scullyio/ng-lib : es2015 as esm2015
Compiling @ng-bootstrap/ng-bootstrap : es2015 as esm2015
✔ Browser application bundle generation complete.
✔ Copying assets complete.
✔ Index html generation complete.

Initial Chunk Files           | Names                      |      Size
vendor.js                     | vendor                     |   3.49 MB
styles.css                    | styles                     | 202.25 kB
polyfills.js                  | polyfills                  | 141.85 kB
main.js                       | main                       |  24.91 kB
runtime.js                    | runtime                    |   9.06 kB

                              | Initial Total              |   3.86 MB

Lazy Chunk Files              | Names                      |      Size
portfolio-portfolio-module.js | portfolio-portfolio-module |  34.19 kB
blog-blog-module.js           | blog-blog-module           |  15.28 kB

Build at:  - Hash:  - Time: 29012ms

构建完成后,运行:

npx scully

Scully 将通过采用每条路线并为每条路线创建单独的 index.html 来预渲染整个投资组合。 预渲染的投资组合将位于 dist/static。 该文件夹应与此类似。 (为清楚起见,一些文件已被删除。)

分布/静态

dist/static
├── about
│   └── index.html
├── assets
├── blog
│   ├── angular-unit-testing
│   │   └── index.html
│   ├── create-a-blog-using-vue.js
│   │   └── index.html
│   ├── how-to-create-a-twitter-bot
│   │   └── index.html
│   └── index.html
├── index.html
└── projects
    └── index.html

请注意每条路线如何拥有自己单独的 index.html 文件。

要预览静态站点,请运行:

npm run scully:serve

此命令将在 http://localhost:1668/ 上启动静态 Scully 服务器并为您的静态投资组合提供服务。 (一旦你完成了网站的预览,你可以在服务器运行的终端上使用 Ctrl + C 终止服务器。)

注意: Scully 可能在定位 Puppeteer 时遇到问题。 当它尝试在受限环境中运行应用程序时会发生这种情况,例如在 CI 服务或云中的虚拟机上。 如果您尝试在 DigitalOcean Droplet 上运行应用程序,您可能会收到此错误。 错误看起来像这样:

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

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

scully.portfolio.config.ts

import { ScullyConfig } from '@scullyio/scully';
export const config: ScullyConfig = {
  projectRoot: "./src",
  projectName: "portfolio",
  outDir: './dist/static',
  routes: {
    '/blog/:slug': {
      type: 'contentFolder',
      slug: {
        folder: "./blog"
      }
    },
  },
  puppeteerLaunchOptions: {args: ['--no-sandbox', '--disable-setuid--sandbox']}
};

puppeteerLaunchOptions 选项可以更改 Puppeteer 的默认选项,并用适用于您的环境的选项覆盖它们。 --no-sandbox--disable-setuid--sandbox 禁用为 Puppeteer 提供的 多层沙盒。 您可以阅读有关此 Chrome 故障排除资源 的更多信息。 根据您的设置,您可能还需要安装其他依赖项来运行 Chromium,您可以在 Puppeteer 故障排除指南 中了解更多信息。


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

在这一步中,您构建了 Angular 应用程序,将其预渲染到静态站点中,并使用 Scully 提供服务。

结论

在本教程中,您生成并配置了一个 Angular 投资组合应用程序。 您还创建了一个核心模块来处理您的投资组合数据并保存应用程序的核心组件。 此外,您制作了一个投资组合模块,其中包含展示您的简历、项目和个人资料的基本页面。 您构建了一个由您的博客登录页面和帖子页面组成的博客模块。 最后,您使用 Scully 将您的 Angular 作品集转换为静态网站。

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

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

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

注意: 本教程是使用 Angular 主要版本 11 创建的。 考虑升级到与最新版本的 Scully 兼容的更新版本。 您可以使用 Angular 的更新工具 来弄清楚如何做到这一点。 您还可以考虑更新本教程中使用的其他依赖项,例如 Font Awesome 和 ng-bootstrap。