node angular_使用Angular和Node构建基本的CRUD应用

node angular

本文最初发布在Okta开发人员博客上 感谢您支持使SitePoint成为可能的合作伙伴。

近年来,单页应用程序(SPA)变得越来越流行。 SPA是仅由一个页面组成的网站。 该单独页面充当JavaScript应用程序的容器。 JavaScript负责获取内容并将其呈现在容器中。 内容通常是从Web服务获得的,而RESTful API已成为许多情况下的首选。 组成SPA的应用程序部分通常称为客户端前端 ,而负责REST API的部分称为服务器后端 。 在本教程中,您将基于Node和Express开发一个具有REST后端的简单Angular单页应用程序。

您将使用Angular,因为它遵循MVC模式并将视图与模型完全分开。 创建HTML模板很简单,该模板可以动态填充数据并在数据更改时自动更新。 我之所以喜欢这个框架,是因为它非常强大,拥有庞大的社区和出色的文档。

对于服务器,您将使用带有Express的Node。 Express是一个框架,通过允许定义针对服务器上不同请求运行的代码,可以轻松创建REST API。 可以在全局或根据请求插入其他服务。 在Express的基础上,有许多框架可以自动完成将数据库模型转换为API的任务。 本教程将不会利用这些内容中的任何内容来保持专注。

Angular鼓励使用TypeScript。 TypeScript将类型信息添加到JavaScript中,我认为这是在JavaScript中开发大规模应用程序的未来。 因此,您将使用TypeScript开发客户端和服务器。

以下是您将用于客户端和服务器的库:

  • Angular:用于构建客户端应用程序的框架
  • Okta授权:用于使用Okta在客户端和服务器上管理单点登录授权的插件
  • Angular Material:一个角度插件,提供开箱即用的Material Design
  • 节点:运行JavaScript代码的实际服务器
  • Express:路由库,用于响应服务器请求并构建REST API
  • TypeORM: TypeScript的数据库ORM库

启动您的基本Angular Client应用程序

让我们开始使用Angular实现一个基本客户端。 目的是开发产品目录,使您可以管理产品,其价格和库存水平。 在本节的最后,您将有一个简单的应用程序,包括一个顶部栏和两个视图,即“首页”和“产品”。 产品视图将尚未包含任何内容,并且没有任何内容将受到密码保护。 以下各节将对此进行介绍。

首先,您需要安装Angular。 我将假定您已经在系统上安装了Node,并且可以使用npm命令。 在终端中键入以下命令。

npm install -g @angular/cli@7.0.2

根据您的系统,您可能需要使用sudo运行此命令,因为它将在全局安装该软件包。 angular-cli软件包提供了用于管理Angular应用程序的ng命令。 安装完成后,转到您选择的目录,并使用以下命令创建第一个Angular应用程序。

ng new MyAngularClient

使用Angular 7,这将提示您两个查询。 第一个询问您是否要包括路由。 对此回答 。 第二个查询与您要使用的样式表的类型有关。 将此保留为默认CSS

ng new将创建一个名为MyAngularClient的新目录,并使用应用程序框架填充该目录。 我们花一些时间来查看上一个命令创建的一些文件。 在应用程序的src目录中,您将找到文件index.html ,它是应用程序的主页。 它包含的内容并不多,仅扮演了容器的角色。 您还将看到一个style.css文件。 这包含在整个应用程序中应用的全局样式表。 如果浏览文件夹,可能会注意到目录src/app包含五个文件。

app-routing.module.ts
app.component.css
app.component.html
app.component.ts
app.component.spec.ts
app.module.ts

这些文件定义了将插入index.html的主要应用程序组件。 这是每个文件的简短描述:

  • app.component.css文件包含主要app组件的样式表。 可以在本地为每个组件定义样式
  • app.component.html包含组件HTML模板
  • app.component.ts文件包含控制视图的代码
  • app.module.ts定义您的应用将使用哪些模块
  • 设置app-routing.module.ts来定义您的应用程序的路由
  • app.component.spec.ts包含用于对app组件进行单元测试的框架

我不会在本教程中讨论测试,但是在实际应用中,您应该使用此功能。 在开始之前,您将需要安装更多软件包。 这些将帮助您快速创建设计良好的响应式布局。 导航到客户端的基本目录MyAngularClient ,然后键入以下命令。

npm i @angular/material@7.0.2 @angular/cdk@7.0.2 @angular/animations@7.0.1 @angular/flex-layout@7.0.0-beta.19

@angular/material@angular/cdk库提供了基于Google材质设计的组件, @angular/animations用于提供平滑的过渡, @angular/flex-layout为您提供了使您的设计具有响应性的工具。

接下来,为app组件创建HTML模板。 打开src/app/app.component.html并将内容替换为以下内容。

<mat-toolbar color="primary" class="expanded-toolbar">
  <button mat-button routerLink="/">{{title}}</button>

  <div fxLayout="row" fxShow="false" fxShow.gt-sm>
    <button mat-button routerLink="/"><mat-icon>home</mat-icon></button>
    <button mat-button routerLink="/products">Products</button>
    <button mat-button *ngIf="!isAuthenticated" (click)="login()"> Login </button>
    <button mat-button *ngIf="isAuthenticated" (click)="logout()"> Logout </button>
  </div>
  <button mat-button [mat-menu-trigger-for]="menu" fxHide="false" fxHide.gt-sm>
    <mat-icon>menu</mat-icon>
  </button>
</mat-toolbar>
<mat-menu x-position="before" #menu="matMenu">
  <button mat-menu-item routerLink="/"><mat-icon>home</mat-icon> Home</button>
  <button mat-menu-item routerLink="/products">Products</button>;
  <button mat-menu-item *ngIf="!isAuthenticated" (click)="login()"> Login </button>
  <button mat-menu-item *ngIf="isAuthenticated" (click)="logout()"> Logout </button>
</mat-menu>
<router-outlet></router-outlet>

mat-toolbar包含材质设计工具栏,而router-outlet是将由铣刨机填充的容器。 app.component.ts文件进行编辑以包含以下内容。

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

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  public title = 'My Angular App';
  public isAuthenticated: boolean;

  constructor() {
    this.isAuthenticated = false;
  }

  login() {
  }

  logout() {
  }
}

这是app组件的控制器。 您可以看到它包含一个名为isAuthenticated的属性以及两个方法loginlogout 。 目前这些都不做。 它们将在下一部分中实现,该部分涵盖使用Okta进行用户身份验证。 现在定义您将要使用的所有模块。 用以下代码替换app.module.ts的内容:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { FlexLayoutModule } from '@angular/flex-layout';
import {
  MatButtonModule,
  MatDividerModule,
  MatIconModule,
  MatMenuModule,
  MatProgressSpinnerModule,
  MatTableModule,
  MatToolbarModule
} from '@angular/material';
import { HttpClientModule } from '@angular/common/http';
import { FormsModule } from '@angular/forms';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    AppRoutingModule,
    BrowserModule,
    BrowserAnimationsModule,
    HttpClientModule,
    FlexLayoutModule,
    MatToolbarModule,
    MatMenuModule,
    MatIconModule,
    MatButtonModule,
    MatTableModule,
    MatDividerModule,
    MatProgressSpinnerModule,
    FormsModule,
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

注意所有的材料设计模块。 @angular/material库要求您为希望在应用程序中使用的每种类型的组件导入模块。 从Angular 7开始,默认应用程序框架包含一个名为app-routing.module.ts的单独文件。 编辑此内容以声明以下路线。

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { ProductsComponent } from './products/products.component';
import { HomeComponent } from './home/home.component';

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

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

这定义了两个与根路径和products路径相对应的路由。 它还将HomeComponentProductsComponent附加到这些路由。 现在创建这些组件。 在Angular客户端的基本目录中,键入以下命令。

ng generate component Products
ng generate component Home

免费学习PHP!

全面介绍PHP和MySQL,从而实现服务器端编程的飞跃。

原价$ 11.95 您的完全免费

这将为每个组件创建htmlcsstsspec.ts文件。 它还会更新app.module.ts以声明新组件。 在src/app/home目录中打开home.component.html并粘贴以下内容。

<div class="hero">
  <div>
    <h1>Hello World</h1>
    <p class="lead">This is the homepage of your Angular app</p>
  </div>
</div>

home.component.css文件中也包括一些样式。

.hero {
  text-align: center;
  height: 90vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  font-family: sans-serif;
}

现在将ProductsComponent保留为空。 一旦创建了后端REST服务器并能够用一些数据填充它,它将被实现。 为了使一切看起来漂亮,仅剩下两个小任务。 将以下样式复制到src/style.css

@import "~@angular/material/prebuilt-themes/deeppurple-amber.css";

body {
  margin: 0;
  font-family: sans-serif;
}

.expanded-toolbar {
  justify-content: space-between;
}

h1 {
  text-align: center;
}

最后,为了渲染Material Design Icons,在index.html文件的<head>标签内添加一行。

<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">

现在您可以启动Angular服务器,并查看到目前为止所取得的成就。 在客户端应用程序的基本目录中,键入以下命令。

ng serve

然后打开浏览器并导航到http://localhost:4200

向您的节点+ Angular应用添加身份验证

如果您曾经从头开发过Web应用程序,您将知道只需要允许用户注册,验证,登录和注销应用程序就需要进行多少工作。 使用Okta,此过程可以大大简化。 首先,您需要使用Okta的开发者帐户。

developer.okta.com

在浏览器中,导航到developer.okta.com ,然后单击创建免费帐户,然后输入您的详细信息。

开始在Okta建立

完成后,您将进入开发人员仪表板。 单击添加应用程序按钮以创建一个新的应用程序。

添加申请

首先创建一个新的单页应用程序。 选择单页应用程序 ,然后单击下一步

创建新的单页应用

在下一页上,您将需要编辑默认设置。 确保端口号是4200。这是Angular应用程序的默认端口。

我的Angular应用

而已。 现在,您应该看到一个客户端ID ,您需要将其粘贴到TypeScript代码中。

要在客户端中实现身份验证,请安装Angular的Okta库。

npm install @okta/okta-angular@1.0.7 --save-exact

app.module.ts导入OktaAuthModule

import { OktaAuthModule } from '@okta/okta-angular';

app模块的imports列表中,添加:

OktaAuthModule.initAuth({
  issuer: 'https://{yourOktaDomain}/oauth2/default',
  redirectUri: 'http://localhost:4200/implicit/callback',
  clientId: '{YourClientId}'
})

在这里, yourOktaDomain应该替换为导航到Okta仪表板时在浏览器中看到的开发域。 YourClientId必须替换为注册应用程序时获得的客户端ID。 上面的代码使Okta身份验证模块在您的应用程序中可用。 在app.component.ts使用它,然后导入服务。

import { OktaAuthService } from '@okta/okta-angular';

修改构造函数以注入服务并订阅它。

constructor(public oktaAuth: OktaAuthService) {
  this.oktaAuth.$authenticationState.subscribe(
    (isAuthenticated: boolean) => this.isAuthenticated = isAuthenticated
  );
}

现在,身份验证状态的任何更改都将反映在isAuthenticated属性中。 加载组件后,您仍然需要对其进行初始化。 创建一个ngOnInit方法并将implements OnInit添加到您的类定义中

import { Component, OnInit } from '@angular/core';
...
export class AppComponent implements OnInit {
  ...
  async ngOnInit() {
    this.isAuthenticated = await this.oktaAuth.isAuthenticated();
  }
}

最后,实现loginlogout方法以对用户界面做出React,并使用户登录或注销。

login() {
  this.oktaAuth.loginRedirect();
}

logout() {
  this.oktaAuth.logout('/');
}

在路由模块中,您需要注册将用于登录请求的路由。 打开app-routing.module.ts并导入OktaCallbackComponentOktaAuthGuard

import { OktaCallbackComponent, OktaAuthGuard } from '@okta/okta-angular';

将另一个路由添加到routes数组。

{
  path: 'implicit/callback',
  component: OktaCallbackComponent
}

这将允许用户使用“ 登录”按钮登录 。 为了保护Products免受未经授权的访问路线,添加以下行到products路线。

{
  path: 'products',
  component: ProductsComponent,
  canActivate: [OktaAuthGuard]
}

这里的所有都是它的。 现在,当用户尝试访问“产品”视图时,他们将被重定向到Okta登录页面。 登录后,用户将被重定向回“产品”视图。

实施节点REST API

下一步是实现基于Node and Express的服务器,该服务器将存储产品信息。 这将使用许多较小的库来简化您的生活。 要使用TypeScript开发,您需要typescripttsc 。 对于数据库抽象层,将使用TypeORM 。 这是一个方便的库,可将行为注入TypeScript类并将其转换为数据库模型。 创建一个包含服务器应用程序的新目录,然后在其中运行以下命令。

npm init

回答所有问题,然后运行:

npm install --save-exact express@4.16.4 @types/express@4.16.0 @okta/jwt-verifier@0.0.14 express-bearer-token@2.2.0 tsc@1.20150623.0 typescript@3.1.3 typeorm@0.2.8 sqlite3@4.0.3 cors@2.8.4 @types/cors@2.8.4

我不会详细介绍所有这些库,但是您会看到@okta/jwt-verifier用于验证JSON Web令牌并对其进行身份验证。

为了使TypeScript起作用,请创建文件tsconfig.json并粘贴以下内容。

{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "outDir": "dist",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  },
  "include": [
    "src/**/*.ts"
  ],
  "exclude": [
    "node_modules"
  ]
}

创建服务器的第一步是为产品创建数据库模型。 使用TypeORM,这很简单。 创建一个子目录src并在其中创建文件model.ts 。 粘贴以下内容。

import {Entity, PrimaryGeneratedColumn, Column, createConnection, Connection, Repository} from 'typeorm';

@Entity()
export class Product {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @Column()
  sku: string;

  @Column('text')
  description: string;

  @Column()
  price: number;

  @Column()
  stock: number;
}

TypeORM批注将类定义转换为数据库模型。 我喜欢TypeORM项目,因为它易于使用,并且支持各种SQL和NoSQL数据库连接器。 我建议您在https://github.com/typeorm/typeorm上查看文档。

您还需要访问产品存储库。 同样在model.ts文件中添加以下内容。

let connection:Connection;

export async function getProductRepository(): Promise<Repository<Product>> {
  if (connection===undefined) {
    connection = await createConnection({
      type: 'sqlite',
      database: 'myangularapp',
      synchronize: true,
      entities: [
        Product
      ],
    });
  }
  return connection.getRepository(Product);
}

请注意,这里为了简单起见使用SQLite。 在实际情况下,应使用您选择的数据库连接器替换它。

接下来,创建一个名为product.ts的文件。 该文件将包含产品上CRUD操作的所有路由的逻辑。

import { NextFunction, Request, Response, Router } from 'express';
import { getProductRepository, Product } from './model';

export const router: Router = Router();

router.get('/product', async function (req: Request, res: Response, next: NextFunction) {
  try {
    const repository = await getProductRepository();
    const allProducts = await repository.find();
    res.send(allProducts);
  }
  catch (err) {
    return next(err);
  }
});

router.get('/product/:id', async function (req: Request, res: Response, next: NextFunction) {
  try {
    const repository = await getProductRepository();
    const product = await repository.find({id: req.params.id});
    res.send(product);
  }
  catch (err) {
    return next(err);
  }
});

router.post('/product', async function (req: Request, res: Response, next: NextFunction) {
  try {
    const repository = await getProductRepository();
    const product = new Product();
    product.name = req.body.name;
    product.sku = req.body.sku;
    product.description = req.body.description;
    product.price = Number.parseFloat(req.body.price);
    product.stock = Number.parseInt(req.body.stock);

    const result = await repository.save(product);
    res.send(result);
  }
  catch (err) {
    return next(err);
  }
});

router.post('/product/:id', async function (req: Request, res: Response, next: NextFunction) {
  try {
    const repository = await getProductRepository();
    const product = await repository.findOne({id: req.params.id});
    product.name = req.body.name;
    product.sku = req.body.sku;
    product.description = req.body.description;
    product.price = Number.parseFloat(req.body.price);
    product.stock = Number.parseInt(req.body.stock);

    const result = await repository.save(product);
    res.send(result);
  }
  catch (err) {
    return next(err);
  }
});

router.delete('/product/:id', async function (req: Request, res: Response, next: NextFunction) {
  try {
    const repository = await getProductRepository();
    await repository.delete({id: req.params.id});
    res.send('OK');
  }
  catch (err) {
    return next(err);
  }
});

该文件有些冗长,但不包含任何令人惊讶的内容。 Product对象已创建并保存到数据库中或从数据库中删除。

让我们再次将注意力转向身份验证。 您将要确保只有经过身份验证的用户才能访问该服务。 创建一个名为auth.ts的文件,然后粘贴以下内容。

import { Request, Response, NextFunction} from 'express';

const OktaJwtVerifier = require('@okta/jwt-verifier');

const oktaJwtVerifier = new OktaJwtVerifier({
  clientId: '{YourClientId}',
  issuer: 'https://{yourOktaDomain}/oauth2/default'
});

export async function oktaAuth(req:Request, res:Response, next:NextFunction) {
  try {
    const token = (req as any).token;
    if (!token) {
      return res.status(401).send('Not Authorised');
    }
    const jwt = await oktaJwtVerifier.verifyAccessToken(token);
    req.user = {
      uid: jwt.claims.uid,
      email: jwt.claims.sub
    };
    next();
  }
  catch (err) {
    return res.status(401).send(err.message);
  }
}

就像在客户端应用程序中yourOktaDomain应将yourOktaDomain替换为开发域,并将YourClientId替换为应用程序客户端ID。 oktaJwtVerifier实例获取JWT令牌并进行身份验证。 如果成功,则用户ID和电子邮件将存储在req.user 。 否则,服务器将以401状态代码响应。 完成服务器的最后一步是实际启动服务器并注册到目前为止定义的中间件的主要入口点。 创建具有以下内容的文件server.ts

import * as express from 'express';
import * as cors from 'cors';
import * as bodyParser from 'body-parser';
const bearerToken = require('express-bearer-token');
import {router as productRouter} from './product'
import {oktaAuth} from './auth'

const app = express()
  .use(cors())
  .use(bodyParser.json())
  .use(bearerToken())
  .use(oktaAuth)
  .use(productRouter);

app.listen(4201, (err) => {
  if (err) {
    return console.log(err);
  }

  return console.log('My Node App listening on port 4201');
});

要编译TypeScript,请运行以下命令

npx tsc

然后,如果要启动服务器,只需运行:

node dist/server.js

完成您的Angular客户

现在服务器已经完成,让我们结束客户端。 第一步是创建一个包含产品数据的类。 该类类似于服务器应用程序中的Product类,但没有TypeORM批注。 它将包含在一个名为product.ts的文件中。

export class Product {
  id?: string;
  name: string;
  sku: string;
  description: string;
  price: number;
  stock: number;
}

将此文件保存在与products组件相同的目录中。 最好将对REST API的访问封装在单独的服务中。 通过运行以下命令来创建Products服务。

ng generate service products/Products

这将在src/app/products目录中创建一个名为product.service.ts的文件。 填写以下内容。

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { OktaAuthService } from '@okta/okta-angular';
import { Product } from './product';

const baseUrl = 'http://localhost:4201';

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

  constructor(public oktaAuth: OktaAuthService, private http: HttpClient) {
  }

  private async request(method: string, url: string, data?: any) {
    const token = await this.oktaAuth.getAccessToken();

    console.log('request ' + JSON.stringify(data));
    const result = this.http.request(method, url, {
      body: data,
      responseType: 'json',
      observe: 'body',
      headers: {
        Authorization: `Bearer ${token}`
      }
    });
    return new Promise<any>((resolve, reject) => {
      result.subscribe(resolve as any, reject as any);
    });
  }

  getProducts() {
    return this.request('get', `${baseUrl}/product`);
  }

  getProduct(id: string) {
    return this.request('get', `${baseUrl}/product/${id}`);
  }

  createProduct(product: Product) {
    console.log('createProduct ' + JSON.stringify(product));
    return this.request('post', `${baseUrl}/product`, product);
  }

  updateProduct(product: Product) {
    console.log('updateProduct ' + JSON.stringify(product));
    return this.request('post', `${baseUrl}/product/${product.id}`, product);
  }

  deleteProduct(id: string) {
    return this.request('delete', `${baseUrl}/product/${id}`);
  }
}

ProductsService为REST API的每个路由包含一个公共方法。 HTTP请求封装在单独的方法中。 请注意,请求如何始终包含从OktaAuthService获得的Bearer令牌。 这是服务器用来验证用户身份的令牌。

现在可以实现ProductsComponent 。 以下代码可以解决问题。

import { Component, OnInit } from '@angular/core';
import { MatTableDataSource } from '@angular/material';
import { ProductsService } from './products.service';
import { Product } from './product';

@Component({
  selector: 'app-products',
  templateUrl: './products.component.html',
  styleUrls: ['./products.component.css']
})
export class ProductsComponent implements OnInit {
  displayedColumns: string[] = ['name', 'sku', 'description', 'price', 'stock', 'edit', 'delete'];
  dataSource = new MatTableDataSource<any>();

  selectedProduct: Product = new Product();
  loading = false;

  constructor(public productService: ProductsService) {
  }

  ngOnInit() {
    this.refresh();
  }

  async refresh() {
    this.loading = true;
    const data = await this.productService.getProducts();
    this.dataSource.data = data;
    this.loading = false;
  }

  async updateProduct() {
    if (this.selectedProduct.id !== undefined) {
      await this.productService.updateProduct(this.selectedProduct);
    } else {
      await this.productService.createProduct(this.selectedProduct);
    }
    this.selectedProduct = new Product();
    await this.refresh();
  }

  editProduct(product: Product) {
    this.selectedProduct = product;
  }

  clearProduct() {
    this.selectedProduct = new Product();
  }

  async deleteProduct(product: Product) {
    this.loading = true;
    if (confirm(`Are you sure you want to delete the product ${product.name}. This cannot be undone.`)) {
      this.productService.deleteProduct(product.id);
    }
    await this.refresh();
  }
}

展示产品的布局,在products.component.html ,由两部分组成。 第一部分使用mat-table组件显示产品列表。 第二部分显示用户可以编辑新产品或现有产品的表单。

<h1 class="h1">Product Inventory</h1>
<div fxLayout="row" fxLayout.xs="column" fxLayoutAlign="space-between stretch" class="products">
  <table mat-table fxFlex="100%" fxFlex.gt-sm="66%" [dataSource]="dataSource" class="mat-elevation-z1">
    <ng-container matColumnDef="name">
      <th mat-header-cell *matHeaderCellDef> Name</th>
      <td mat-cell *matCellDef="let product"> {{product.name}}</td>
    </ng-container>
    <ng-container matColumnDef="sku">
      <th mat-header-cell *matHeaderCellDef> SKU</th>
      <td mat-cell *matCellDef="let product"> {{product.sku}}</td>
    </ng-container>
    <ng-container matColumnDef="description">
      <th mat-header-cell *matHeaderCellDef> Description</th>
      <td mat-cell *matCellDef="let product"> {{product.description}}</td>
    </ng-container>
    <ng-container matColumnDef="price">
      <th mat-header-cell *matHeaderCellDef> Price</th>
      <td mat-cell *matCellDef="let product"> {{product.price}}</td>
    </ng-container>
    <ng-container matColumnDef="stock">
      <th mat-header-cell *matHeaderCellDef> Stock Level</th>
      <td mat-cell *matCellDef="let product"> {{product.stock}}</td>
    </ng-container>
    <ng-container matColumnDef="edit">
      <th mat-header-cell *matHeaderCellDef></th>
      <td mat-cell *matCellDef="let product">
        <button mat-icon-button (click)="editProduct(product)">
          <mat-icon>edit</mat-icon>
        </button>
      </td>
    </ng-container>
    <ng-container matColumnDef="delete">
      <th mat-header-cell *matHeaderCellDef></th>
      <td mat-cell *matCellDef="let product">
        <button mat-icon-button (click)="deleteProduct(product)">
          <mat-icon>delete</mat-icon>
        </button>
      </td>
    </ng-container>
    <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
    <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
  </table>
  <mat-divider fxShow="false" fxShow.gt-sm [vertical]="true"></mat-divider>
  <div>
    <h2>Selected Product</h2>
    <label>Name
      <input type="text" [(ngModel)]="selectedProduct.name">
    </label>
    <label>SKU
      <input type="text" [(ngModel)]="selectedProduct.sku">
    </label>
    <label>Description
      <input type="text" [(ngModel)]="selectedProduct.description">
    </label>
    <label>Price
      <input type="text" [(ngModel)]="selectedProduct.price">
    </label>
    <label>Stock Level
      <input type="text" [(ngModel)]="selectedProduct.stock">
    </label>
    <button mat-flat-button color="primary" (click)="updateProduct()">{{(selectedProduct.id!==undefined)?'Update':'Create'}}</button>
    <button mat-flat-button color="primary" (click)="clearProduct()">Clear</button>
  </div>
  <div class="loading" *ngIf="loading">
    <mat-spinner></mat-spinner>
  </div>
</div>

最后,在products.component.css向布局添加一些样式。

.products {
  padding: 2rem;
}

label, input {
  display: block;
}

label {
  margin-bottom: 1rem;
}

.loading {
  position: absolute;
  display: flex;
  justify-content: center;
  align-content: center;
  width: 100%;
  height: 100%;
  background-color: rgba(255, 255, 255, 0.8);
}

完成所有步骤后,您可以启动客户端和服务器并测试您的应用程序。 只是重复一遍,在包含服务器的目录中运行:

node dist/server.js

并在客户端目录中运行:

ng serve

您的应用程序应类似于以下内容

产品库存

了解有关Angular,Node和Express的更多信息

在本教程中,我指导您使用Angular和Node开发单页Web应用程序。 仅使用几行代码,您就可以对客户端和服务器实施用户身份验证。 Angular使用TypeScript(它是JavaScript语言的超集)并添加类型信息。 TypeScript使代码更稳定,这就是为什么我决定也使用这种语言实现Node / Express服务器的原因。 如果您还不熟悉TypeScript,请查看Todd Motto的出色介绍 。 他在Angular上也有一些不错的文章。

可以在GitHub上找到本教程的完整代码。

如果您准备了解有关Angular或Node / Express的更多信息,我们还有一些其他资源可供您检查:

与往常一样,我们很乐意让您关注我们,以获取更酷的内容和我们团队的更新。 您可以在Twitter @ oktadevFacebookLinkedIn上找到我们。

翻译自: https://www.sitepoint.com/build-a-basic-crud-app-with-angular-and-node/

node angular

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值