在 Rails 5 中使用 Angular

508 阅读16分钟

翻译自 Using Angular with Rails 5,by Julio Sampaio Author , Apr 12, 2021

你以前听说过这个故事。你已经有了一个运行在你的分散的、完全工作的后端API上的应用程序,以及一个用任何普通工具集制作的前端。

现在,你想继续使用Angular。或者,你只是在寻找一种方法,将Angular与你的Rails项目结合起来,因为你更喜欢这种方式。我们不怪你。

有了这样的方法,你就可以利用两个世界的优势,并决定是否要使用Rails或Angular的功能来格式化东西。

我们将建立什么

没有必要担心。本教程就是为此而写的。我们将深入研究如何在一个用户域上创建一个完全有效的CRUD应用程序。

在文章的最后,你将了解到围绕Angular的一些基本概念,以及如何建立一个Rails后端项目,直接与前端的Angular集成,如下所示。

CRUD of users 用Rails和Angular制作的用户的CRUD

该应用程序将处理所有四个CRUD操作,这些操作涉及从外部虚假测试网络服务中检索的用户域。该应用将建立在MVC架构之上,每个Angular层都有详细解释,以帮助你更好地理解事情是如何联系在一起的。风格由Bootstrap决定。

设置

正如你可能已经猜到的,我们将需要以下软件。

  • Ruby(我选的是2.7.0preview1版本)。
  • Ruby and Rails(我使用的是5.0.7.2版本)。
  • Node.js(我用的是v13.7.0)。
  • Yarn(至少其版本为1.22.4

确保一切安装正常。然后,我们就可以进入项目了。选择一个你喜欢的文件夹,运行以下命令。

rails new crud-rails-angular

等待设置完成,并在你喜欢的IDE中打开该项目。在这篇文章中,我们将使用VS Code,因为它简单、强大,并能顺利地接受Rails和Angular的语法。

如果你已经使用Rails 5一段时间了,你可能已经注意到,它的new 命令在_Gemfile_中为SQLite配置产生了一个错误。它没有一个最小的版本,这将使它的运行出现错误。让我们来解决这个问题,把它更新为

gem 'sqlite3', '~> 1.3.10'

完美!

Webpacker设置

在Rails中管理类似JavaScript的应用程序的最好方法是通过Webpacker。它利用Webpack在幕后提供的功能,如预处理和捆绑JavaScript应用程序,如Angular,到一个现有的Rails应用程序。

要安装它,只需在你的_Gemfile_中添加一个新行。

gem 'webpacker', '~> 4.3.x'

这将保证你安装的是一个非常新的版本。接下来,运行以下命令。

bundle install
bundle exec rake webpacker:install
bundle exec rake webpacker:install:angular

第一个命令将下载并更新添加的Rails依赖项。

第二个命令相当于npm install ,因为它创建了_node_modules_文件夹,并安装了一堆必要的Angular依赖项,如Babel、Sass、Browserlist和Webpack。现在,我们在同一个项目中同时有一个Node和一个Rails应用。

在最新的命令中,我们有相当于npm install angular ,它将下载所有Angular所需的依赖项,并使其与我们的Rails项目一起工作。

在这些命令的最后,你还可以看到创建的_package.json_文件。我们所有需要的依赖项都放在那里,你可以在将来添加任何你需要的东西。

另外,一些文件夹和文件被创建在_/app_文件夹下,比如新的_/javascript_。在这个文件夹中,你已经创建了一个_/hello_angular_文件夹,以支持你开发的开始。

为了争取一些时间,我请你把你的文件夹和文件结构与下面的结构进行镜像。

Files and folders

一些Angular的调整

Webpacker建议在你生成的Rails项目中进行一系列的调整。所以,让我们花些时间来整理一下。

首先,打开你放在_/packs_文件夹下的_application.js_文件(如上图所示),添加以下代码。

import "core-js/stable";
import "regenerator-runtime/runtime";

这些导入作为一种辅助力量,在Rails项目中稳定了JavaScript环境。

现在,我们需要通知Rails,它必须从哪里挑选输出到其页面。一旦Webpacker完成了打包工作,它就会生成一堆Rails必须知道的可分发静态文件。

转到_app/views/layout_文件夹下的_application.html.erb_文件,将其<head> 标签内容改为如下。

<head>
  <title>CrudRailsAngular</title>
  <base href="/" />
  <!-- 1 -->
  <%= csrf_meta_tags %> <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous" />
  <!-- 2 -->
  <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %> <%= javascript_pack_tag 'application' %>
  <!-- 3 -->
</head>

让我们把它分解一下。

  1. 在这里,我们要添加base 标签,它告诉Rails在应用程序启动时要看哪里。
  2. 我们将利用Bootstrap来推断页面的风格,所以我们可以只关注实现。
  3. 这里是你必须放置Webpacker标签的地方,该标签映射到_/packs_文件夹内容(也就是每次编译后Webpacker自动生成的内容)。

模型和数据库

接着是数据库的设置。为了使事情变得更快,我们将建立一个新的模型,叫做User 。这是你必须运行的命令来实现它。

rails g scaffold User name:string age:integer address:text && rake db:migrate

它将创建我们模型的所有文件夹和文件,我们将需要这些文件来使Rails操作数据库信息(来自SQLite)并将其存储到我们的CRUD操作中。

然后,你会看到在_db/migrate/文件夹下创建了一个新文件_XXX1_create_users.rb。打开它,你会看到新创建的CreateUsers 记录。

在_app/models/_文件夹下,你会在_user.rb_文件中看到当时创建的User 模型。

现在,打开_db/seeds.rb_文件,添加以下代码。

User.create(name: 'Luke Wan', age: 23, address: '123 Test St.')
User.create(name: 'Mary Poppins', age: 41, address: '123 ABC St.')
User.create(name: 'John Neilman', age: 76, address: '000 Test St.')

这段代码将在启动时用一些数据初始化我们的Users 表。保存它,并运行该命令。

这将通过上面列出的命令对表进行播种。接下来,你可以进入SQLite数据库,通过发布命令检查是否成功。

sqlite3 db/development.sqlite3

然后,选择表的数据。

你就可以看到结果了。

用户组件

你需要再安装几个依赖项,以帮助将HTML和CSS转换为我们的Rails页面;添加Angular路由器、表单库和ngx-bootstrap,我们将用它来促进Bootstrap组件的创建和操作。因此,发出以下命令。

yarn add @angular/router @angular/forms html-loader css-loader ngx-bootstrap

然而,在我们跳入组件代码之前,有一些重要的概念我们需要指出,首先是Angular组件的解剖。

什么是组件?

在Angular中,组件的存在是为了连接你的视图和TypeScript中的应用逻辑。

换句话说,一个组件就像一个容器,可以容纳你的视图所需的所有逻辑,以支持其运作。它定义了视图将呈现的值并控制它们的流程。它相当于类似框架中的 "控制器"。

要创建一个组件,你所需要做的就是定义一个新的类,实现 OnInit接口,并在该类中使用 @Component装饰器来注释这个类。

export class UserIndexComponent implements OnInit {
    constructor() { ... }

    ngOnInit() { ... }
}

@Component和OnInit

@Component 装饰器很重要,因为它标志着这个类是可识别的Angular组件,并提供元数据配置,帮助Angular处理它们在运行期间的处理、实例化和使用。

以下面的元数据配置为例。

@Component({
    selector: "users",
    template: templateString,
})

在这里,selector 告诉Angular,所提供的值是它可能用来识别当前指令到模板的CSS选择器;是的,它是下一个元数据属性中提供的同一个模板。

然而,OnInit 接口是可选的,它是在组件完成其生命周期之前初始化东西的一个好方法。它的作用就像一个后建方法。

依赖性注入

Angular是一个DI_(Dependency Injection_)框架,这个特点增加了它的模块化和生产力。

Angular中的依赖关系可以从你的服务和资源库到任何一种你认为适合注入到代码中其他地方的普通对象。

要把一个类变成 "可注入的",你只需要在它身上注上 @Injectable装饰器进行注释。

@Injectable({
    providedIn: "root",
})
export class UserService {
    ...
}

providedIn 表示哪个注入器将提供你正在创建的可注入对象。root 值告诉Angular,这个注入器应该是应用级的。还有更多,你可以在这里查看。

例如,要把类注入到一个组件中,你要求Angular在组件的构造函数中这样做。

constructor(
    private userService: UserService,
) {}

就这么简单!

完成的组件

下面,你可以找到我们的用户组件的最终代码列表。把它放到 _index.component.ts_中,在_javascript/hello\_angular/app/_文件夹下。

import { Component, OnInit, TemplateRef } from "@angular/core";
import { FormGroup, FormBuilder } from "@angular/forms";
import { BsModalRef, BsModalService } from "ngx-bootstrap/modal";

import templateString from "./index.component.html";
import { UserService } from "../user.service";
import { User } from "../user.class";

@Component({
  selector: "users",
  template: templateString,
})
export class UserIndexComponent implements OnInit {
  users: User[];
  modalRef: BsModalRef;
  userForm: FormGroup;
  isNew: Boolean;

  constructor(public fb: FormBuilder, private userService: UserService, private modalService: BsModalService) {}

  public newUser(template: TemplateRef<any>) {
    this.reset();
    this.modalRef = this.modalService.show(template);
  }

  public createUser() {
    this.userService.create(this.userForm.value).subscribe(() => {
      console.log("User created!");
      this.reset();

      this.modalRef.hide();
    });
  }

  public editUser(user, template: TemplateRef<any>) {
    this.isNew = false;
    this.userForm = this.fb.group({
      id: [user.id],
      name: [user.name],
      age: [user.age],
      address: [user.address],
    });

    this.modalRef = this.modalService.show(template);
  }

  public updateUser() {
    const { id } = this.userForm.value;
    this.userService.update(id, this.userForm.value).subscribe(() => {
      console.log("User updated!");
      this.reset();

      this.modalRef.hide();
    });
  }

  public deleteUser(id) {
    if (confirm("Are you sure?")) {
      this.userService.delete(id).subscribe(() => {
        console.log("User deleted!");
        this.reset();
      });
    }
  }

  ngOnInit() {
    this.reset();
  }

  public reset() {
    this.isNew = true;
    this.userService.getUsers().subscribe((users) => {
      this.users = users;
    });

    this.userForm = this.fb.group({
      id: [""],
      name: [""],
      age: [""],
      address: [""],
    });
  }
}

users 数组将保存屏幕上列出的当前表格数据,并从reset 方法中获取,该方法反过来通过UserService (将被创建)调用我们的Rails API。

userForm 只是一个参考,以帮助创建和更新我们的用户,因为两个操作都将使用同一个表单。isNew 也能帮助我们,确定我们目前处于哪个流程。

在这里,我们为每个操作都有一个CRUD等价的方法。它们中的每一个都调用了各自的UserService 方法,在Rails API中提交流程。

我们还需要设置HTML模块,将我们的模板转换为HTML(我们很快会看到更多关于模块的内容)。所以,在同一文件夹内打开_html.d.ts_文件,然后添加。

declare module "*.html" {
  const content: string;
  export default content;
}

Angular服务和模型

让我们继续讨论Angular的UserService 创建。Angular是一个框架,就像Rails一样。所以,这意味着遵守他们的规则是可以的,即使这意味着有重复的(或非常相似的)模型,比如说。

什么是模型?

Angular模型是简单的对象,它持有一起有意义的数据属性(也就是说,它们代表了你领域的一个简洁的部分)。它们就像大多数语言和框架中的任何其他模型。

把你的数据集中在一个地方有很大的帮助,而不是像我们对用户模型那样在整个代码中重复使用它。

export class User {
  constructor(public id: number, public name: string, public age: number, public address: string) {}
}

记住,这是TypeScript,所以你的模型的属性必须总是有一个定义的类型。

在_javascript/hello_angular/app/user/_文件夹下创建一个名为_user.class.ts_的新文件,并将上述代码放入其中。

服务是什么?

服务是一个广泛的概念,但我们可以把它们理解为定义明确、目的明确的对象。它们帮助组件完成更复杂的逻辑,为它们提供经过处理和转换的数据,这些数据通常来自外部服务或数据库。

一个服务不需要任何特定的注释或接口;你只需创建一个类并使其_可注入_,正如我们之前看到的那样。然后,你可以把它注入到你的组件中。

可观察的服务

Angular的另一个有趣的特点是,它允许你在你的类中使用RxJS

例如,Angular的默认HTTP客户端,也就是我们要用来从外部服务获取信息的客户端,会返回RxJSObservables 。这就是为什么,当你在用户组件中调用我们的任何一个UserService 方法时,你可能subscribeObservable 结果。

this.userService.getUsers().subscribe((users) => {
  this.users = users;
});

注意,如果你不熟悉RxJS,我强烈建议你简单地阅读一下它的文档;这并不难!;)

同样,在_javascript/hello_angular/app/user/_文件夹中,创建另一个名为_user.service.ts_的文件。这是它的内容。

import { Injectable } from "@angular/core";
import { HttpClient, HttpHeaders } from "@angular/common/http";
import { map } from "rxjs/operators";
import { Observable } from "rxjs";

import { User } from "./user.class";

@Injectable({
  providedIn: "root",
})
export class UserService {
  constructor(private http: HttpClient) {}

  httpOptions = {
    headers: new HttpHeaders({
      "Content-Type": "application/json",
    }),
  };

  getUsers(): Observable<User[]> {
    return this.http.get("/users.json").pipe(
      map((users: User[]) =>
        users.map((user) => {
          return new User(user.id, user.name, user.age, user.address);
        })
      )
    );
  }

  create(user): Observable<User> {
    return this.http.post<User>("/users.json", JSON.stringify(user), this.httpOptions);
  }

  update(id, user): Observable<User> {
    return this.http.put<User>("/users/" + id + ".json", JSON.stringify(user), this.httpOptions);
  }

  delete(id) {
    return this.http.delete<User>("/users/" + id + ".json", this.httpOptions);
  }
}

你能发现这个文件和我们刚刚创建的组件有什么相似之处吗?这是因为我们需要对应的操作来支持组件中的操作。

注意,HttpClient 也必须被注入到类的构造函数中,所以我们可以和类一起使用它。

每个操作都会对我们的Rails API进行HTTP调用,也就是自动生成的那个。

视图

Angular的视图使用的是模板。模板是一种分层的HTML和JavaScript混合体,它告诉Angular如何渲染每个组件。

然而,在进一步构建我们的视图之前,让我们首先了解Angular是如何将其模板系统分割开来的。

Angular指令

因为Angular的模板基本上是动态的,所以需要一些_指令_来驱动Angular以正确的方式渲染东西。

指令是简单的类,有一个@Directive 装饰器,就像组件一样。是的,@Component 继承自@Directive ,所以它也是一个正式的指令。

然而,还有另外两种类型:_结构_指令和_属性_指令。

结构性指令

这些指令代表了从JavaScript翻译到Angular模板的条件和循环结构。它们有助于使模板尽可能的动态化,就像你在你的vanilla JavaScript代码中编程一样。以下面的例子为例。

<tr *ngFor="let user of users">
  <td>{{ user.name }}</td>
</tr>

该 _*ngFor_指令告诉Angular遍历users ,并将每个用户的名字打印到DOM上。

属性指令

这些直接作用于元素的外观或行为。以下面的例子为例。

<form [formGroup]="userForm" (ngSubmit)="isNew ? createUser() : updateUser()" novalidate></form>

在这里,我们通过有条件地设置表单的submit 函数来修改表单的行为,并利用Angular的 FormGroup对每个表单输入进行数据绑定。

数据绑定

如果不提供数据绑定,用Web框架创建表单可能是一个棘手的、容易出错的任务。

Angular支持双向数据绑定,这意味着你可以直接将你的模板碎片连接到组件上,反之亦然。

上面的表单是一个很好的例子,说明了FormGroup 数据绑定的力量。它自动将每个表单字段与我们组件内创建的userForm 对象绑定。

例如,在editUser 方法中,你可以看到绑定的相反版本,其中userForm'的值是在组件中设置的,并应反映在视图中的表单。

构建索引视图

我们把 _index.component.html_的内容分解成两部分。这是第一个部分。

<div class="container pricing-header px-3 py-3 pt-md-5 pb-md-4 mx-auto text-center">
  <h1 class="display-4">User's Listing</h1>
  <p class="lead">A quick CRUD example of how to integrate Rails with Angular</p>

  <table class="table">
    <tr>
      <th>Id</th>
      <th>Name</th>
      <th>Age</th>
      <th>Address</th>
      <th>Actions</th>
    </tr>

    <tbody>
      <tr *ngFor="let user of users">
        <td>{{ user.id }}</td>
        <td>{{ user.name }}</td>
        <td>{{ user.age }}</td>
        <td>{{ user.address }}</td>
        <td colspan="2">
          <button class="btn btn-secondary" (click)="editUser(user, template)">Edit</button>
          |
          <button class="btn btn-danger" (click)="deleteUser(user.id)">Delete</button>
        </td>
      </tr>
    </tbody>
  </table>

  <button class="btn btn-primary float-right mt-4" (click)="newUser(template)">Insert New</button>
</div>

它的大部分是由普通的HTML组成。我们将不详细讨论Bootstrap类。

这里重要的部分是表格行上的ngFor 指令。它有助于迭代users 数组(记得吗?),通过{{ … }} 操作符将其每个属性打印到HTML输出中。

每当你想添加一个DOM事件,比如_onClick_,只要用圆括号包住事件名称,并添加点击时将调用的组件函数。

构建模态视图

第二部分与模态内容有关,所以把它加在前面的部分下面。

<ng-template #template>
  <div class="modal-header">
    <h4 class="modal-title pull-left">{{ isNew ? "New User" : "Update User" }}</h4>
    <button type="button" class="close pull-right" aria-label="Close" (click)="modalRef.hide()">
      <span aria-hidden="true">&times;</span>
    </button>
  </div>
  <div class="modal-body">
    <form [formGroup]="userForm" (ngSubmit)="isNew ? createUser() : updateUser()" novalidate>
      <input type="hidden" formControlName="id" class="form-control" />
      <div class="form-group">
        <label>Name</label>
        <input type="text" formControlName="name" class="form-control" />
      </div>
      <div class="form-group">
        <label>Age</label>
        <input type="text" formControlName="age" class="form-control" />
      </div>
      <div class="form-group">
        <label>Address</label>
        <textarea class="form-control" formControlName="address" rows="3"></textarea>
      </div>

      <button type="submit" class="btn btn-primary">Submit</button>
    </form>
  </div>
</ng-template>

注意,我们正在使用<ng-template> 标签,它允许你在HTML和Angular之间锚定元素。模板ID就在# 符号之后。

在表单中,也注意到我们正在利用isNew 组件变量来验证这个表单的当前使用是否与用户的创建或更新有关。

最后,我们需要将整个 _hello\_angular_应用程序注入到Rails的_index.html.erb_页面。因此,打开_views/users/_文件夹下的这个文件,将其内容改为如下。

<hello-angular>We're almost done...</hello-angular> <%= javascript_pack_tag 'hello_angular' %>

Angular模块

现在,我们需要告诉Angular去哪里找东西。这发生在其模块的配置中。

让我们从添加内容到_app-bootstrap.module.ts_开始。

import { NgModule } from "@angular/core";
import { CommonModule } from "@angular/common";

import { ModalModule } from "ngx-bootstrap/modal";

@NgModule({
  imports: [CommonModule, ModalModule.forRoot()],
  exports: [ModalModule],
})
export class AppBootstrapModule {}

这只限于我们从ngx-bootstrap继承的Bootstrap组件。我们现在唯一要利用的组件是Bootstrap modal。

然后,打开_app-routing.module.ts_文件,将其内容改为如下。

import { RouterModule, Routes } from "@angular/router";
import { NgModule } from "@angular/core";

import { UserIndexComponent } from "./user/index/index.component";

const appRoutes: Routes = [
  { path: "users", component: UserIndexComponent },
  { path: "", redirectTo: "/users", pathMatch: "full" },
];

@NgModule({
  imports: [RouterModule.forRoot(appRoutes, { scrollPositionRestoration: "enabled" })],
  exports: [RouterModule],
})
export class AppRoutingModule {}

这将确保Angular在调用_/users_路径时匹配到正确的User的组件。

最后,在主AppModule 类中注册所有这些组件。打开_app.module.ts_文件,确保它看起来像这样。

import { BrowserModule } from "@angular/platform-browser";
import { NgModule } from "@angular/core";
import { HttpClientModule } from "@angular/common/http";
import { FormsModule, ReactiveFormsModule } from "@angular/forms";

import { AppComponent } from "./app.component";
import { AppRoutingModule } from "./app-routing.module";
import { AppBootstrapModule } from "./app-boostrap.module";
import { UserIndexComponent } from "./user/index/index.component";

@NgModule({
  declarations: [AppComponent, UserIndexComponent],
  imports: [HttpClientModule, AppRoutingModule, BrowserModule, FormsModule, ReactiveFormsModule, AppBootstrapModule],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

在这里,一切都被映射了。从我们的表单、HTTP客户端和用户组件到Bootstrap模块的配置,以及路由。

完成配置

在我们进入测试之前,我们需要完成一些事情,首先是_app.component.ts_文件。

import { Component } from "@angular/core";

@Component({
  selector: "hello-angular",
  template: "<router-outlet></router-outlet>",
})
export class AppComponent {
  name = "Angular!";
}

主应用程序组件需要知道如何路由路径,所以RouterOutlet将完成这项工作。

然后,我们需要确保Webpacker能够理解我们到目前为止正在使用的HTML扩展。为此,打开_webpacker.yml_文件,在_/config_文件夹下,搜索_extensions_部分并添加以下项目。

Webpacker只识别Angular默认附带的内置TypeScript加载器。我们需要处理HTML,这就是我们之前安装_html-loader_依赖项的原因。要设置它,请打开_config/webpack_文件夹下的_environment.js_文件,并添加以下加载器配置。

environment.loaders.append("html", {
  test: /\.html$/,
  use: [
    {
      loader: "html-loader",
      options: {
        minimize: true,
      },
    },
  ],
});

最后,为了防止我们的Angular服务在其HTTP调用中收到错误,我们需要禁用Rails执行的CSRF令牌检查。为此,打开_app/controllers_文件夹下的_application_controller.rb_文件,并将其内容改为如下。

class ApplicationController < ActionController::Base
  protect_from_forgery with: :null_session
end

测试

这就是了!这看起来有点棘手,因为设置需要大量的定制,但结果是值得的。

为了测试,保存所有东西,并通过发出rails s 命令来启动服务器。

然后,进入你的网络浏览器,输入地址http://localhost:3000/users。继续,玩一玩CRUD网络应用程序。

结论

让这个CRUD启动和运行是一条漫长的道路。在第一次试验之后,你会发现,对于你未来的项目,事情会变得更容易。我希望这个项目有助于为那些想通过加入这两个技术的人快速启动项目建立一个起点。

虽然我们没有一个开源的脚手架项目来帮助它,但我们依靠彼此的努力来拥有这样的材料。现在,轮到你了;分叉这个项目(或者从头开始创建),开始进行你的定制。

这个例子的GitHub资源库可以在这里找到。玩得开心点!