容器化单页面应用中RESTful API的访问

栏目: 后端 · 前端 · 发布时间: 6年前

内容简介:(总访问量:22;当日访问量:1)

最近在工作中,需要让运行在容器中的单页面应用程序能够访问外部的RESTful API。这个需求看起来并不困难,不过实现起来还是有些曲折的。在此,我就将这部分内容总结一下。

容器化单页面应用中RESTful API的访问 在入正题之前,有个一问题,就是为什么要将单页面应用放在容器中运行?这个问题其实跟“为什么要将应用程序容器化”是一个问题。简单来讲,容器化的应用程序可以运行在任何具有容器执行环境的宿主平台上,比如可以在 Linux 系统中运行容器,也可以在MacOS或者Windows下使用Docker Desktop for Mac或者Docker for Windows来运行容器化的应用程序。无论在什么平台中运行,容器化的应用程序都可以使用统一化的配置方式(比如环境变量、虚拟磁盘路径的挂载等),并向外界提供一致的访问端点。将应用程序容器化最重要的一点是,通过它可以非常方便地将应用程序部署在云环境中,使应用程序具有很好的横向扩展能力,而且跨云的迁移也变得非常便捷。由此可见,通过容器的使用,我们可以采用不同的技术来实现应用程序的不同部分,然后可以得到统一的部署和运维体验,这一点对于微服务架构的实践有着非常深远的意义。在工作中,我所接触的系统包含了多个团队的贡献,有的团队使用nodejs,有的团队使用Scala,有的团队使用Go,这些独立分散的项目都以一个个独立的服务进行开发和交付,最终通过容器化的途径实现了整个应用程序的一体化部署。当然,与各种软件架构风格类似,微服务架构也是有利有弊,并不是所有的项目和团队都应该采用这种架构,还是应该根据项目和团队的实际情况来决定软件系统的架构方式,这部分内容就不在此过多讨论了。

回到本文的主题,我会通过一个案例来总结在不同场景下,容器化单页面应用访问RESTful API的方式。

名称列表应用

我们的案例是一个提供名称列表的RESTful API,外加一个显示名称列表的前端单页面应用。不必理会什么是“名称列表”,它只不过是一个字符串列表。在这里我们也不必关心这个字符串列表包含哪些内容,只要让单页面应用能够访问到这个RESTful API即可。继续阅读本文,你将了解到这个案例是多么的简单。

RESTful API

首先创建一个能够返回名称列表的RESTful API,实现方式有很多种,我选择我熟悉的ASP.NET Core Web API项目来创建RESTful API。在命令行执行以下命令以创建一个ASP.NET Core Web API的项目:

dotnet new webapi --name NameList.Service

然后,使用Visual Studio Code编辑器打开该项目,删除ValuesController,然后新增NamesController,当然也可以基于ValuesController修改,代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;

namespace NameList.Service.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class NamesController : ControllerBase
    {
        // GET api/values
        [HttpGet]
        public ActionResult<IEnumerable<string>> Get()
            => new string[] { "Brian", "Frank", "Sunny", "Chris" };
    }
}

目前我们不需要启用HTTPS重定向,将其从Startup.cs中删除,同时调整launchSettings.json文件,直接侦听 http://*:5000 ,然后使用dotnet run命令,启动RESTful API,使用cURL工具进行测试:

$ curl -s http://localhost:5000/api/names
["Brian","Frank","Sunny","Chris"]

API调用成功。为了后续的实验能够顺利进行,我们在服务端启用CORS:

public class Startup
{
    private const string CorsPolicy = "DefaultCorsPolicy";
    
    //...

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
        services.AddCors(options =>
        {
            options.AddPolicy(CorsPolicy, builder =>
            {
                builder.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin();
            });
        });
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        //...
        app.UseCors(CorsPolicy);
        //...
    }
}

接下来,开发我们的前端单页面应用,以调用该API并将名称列表显示在前端页面。

单页面应用

同样,前端页面也可以采用很多种框架和技术进行开发,比如使用React、Vue或者Angular,或者直接使用jQuery,都可以完成我们的目标。我还是选择我最熟悉的Angular 7,依照下面的步骤开发这个单页面应用。

首先,使用Angular CLI,创建我们的应用程序:

ng new name-list

在回答几个问题之后(使用默认选项即可),前端单页面应用也就创建好了,首先在app.modules.ts中启用HttpClientModule:

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

import { AppComponent } from './app.component';
import { HttpClientModule } from '@angular/common/http';

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

然后,在environment.ts和environment.prod.ts中加入RESTful API的BaseURI:

export const environment = {
  production: false,
  serviceUri: 'http://localhost:5000'
};

接着,新建一个AppService服务(app.service.ts),在该服务中提供一个getNames的方法,用以调用RESTful API以获取名称列表,并将获得的列表返回给调用方:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
import { environment } from 'src/environments/environment';

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

  constructor(private http: HttpClient) { }

  getNames(): Observable<string[]> {
    return this.http.get<string[]>(`${environment.serviceUri}/api/names`)
    .pipe(
      tap(_ => console.log('fetched names')),
      catchError(this.handleError<string[]>([]))
    );
  }

  private handleError<T>(result?: T) {
    return (error: any): Observable<T> => {
      console.error(error);
      return of(result as T);
    };
  }
}

然后,修改app.component.ts,以便在页面初始化的时候,调用AppService获取名称列表,并将获得的列表保存在变量中:

import { Component, OnInit } from '@angular/core';
import { AppService } from './app.service';

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

  names: string[];

  constructor(private appService: AppService) { }

  ngOnInit(): void {
    this.getNames();
  }

  getNames(): void {
    this.appService.getNames()
      .subscribe(names => this.names = names);
  }
}

最后,修改app.component.html,通过HTML将获得的名称列表显示在页面上:

<h2>Names</h2>
<ul>
  <li *ngFor="let name of names">
    <span>{{name}}</span>
  </li>
</ul>

现在,将RESTful API运行起来,然后使用ng serve命令将前端页面也运行起来,应该能够看到下面的效果:

容器化单页面应用中RESTful API的访问

接下来,我们将RESTful API和前端页面编译成容器镜像(Docker Images)。

单页面应用容器化

现在,我们将上面开发的单页面应用编译成 docker 镜像,然后让它在容器中运行。在Angular项目的根目录下,新建一个Dockerfile,内容如下:

FROM nginx AS base
WORKDIR /app
EXPOSE 80

FROM node:10.16.0-alpine AS build
RUN npm install -g @angular/cli@8.0.3
WORKDIR /src
COPY . .
RUN npm install
RUN ng build --prod --output-path /app

FROM base AS final
COPY --from=build /app /usr/share/nginx/html
CMD ["nginx", "-g", "daemon off;"]

大概介绍一下,在上面的Dockerfile中,将nginx定义为base image,因为最终我会将Angular单页面应用运行在nginx上;然后,基于node镜像,安装Angular CLI,并将本地前端代码复制到容器中的/src目录下进行编译,最终将编译输出的html、js、css以及相关资源复制到nginx容器的/usr/share/nginx/html目录下,最后启动nginx来服务单页面应用站点。

现在,我们启动RESTful API,依旧让其侦听5000端口,然后通过以下docker命令,启动这个前端单页面应用容器:

docker run -it -p 8088:80 namelist-client

容器启动后,打开浏览器,访问8088端口,我们可以得到同样的结果,可以注意到,前端页面会发送请求到 http://localhost:5000 以获得名称列表:

容器化单页面应用中RESTful API的访问

整个实验看似已经非常成功,但是,我们忽略了一个重要问题,目前RESTful API的地址在前端代码中是写死(hard code)的,即使是在environment.prod.ts文件中指定,也是编译时就已经确定的事情,那如果RESTful API部署在不同的机器上,或者侦听端口不是5000呢?这样的话,前端单页面应用是无法访问RESTful API服务的。下面我们就来解决这个问题。

RESTful API访问问题的解决

有一种比较简单粗暴的办法,就是在编译的时候,通过持续集成环境的设置,将RESTful API的地址写入environment.prod.ts文件中,但这样编译出来的容器只能在特定环境下运行,否则前端页面还是无法访问RESTful API。要让容器能够通用,还是应该在容器启动的时候,以环境变量的方式将RESTful API的地址注入到容器中。在此,我们讨论两种场景:RESTful API独立部署的场景,以及RESTful API也以容器的方式运行的场景。

RESTful API独立部署的场景

首先做个实验,将前端Angular项目中environment.prod.ts里的serviceUri改为一个相对路径,比如:

export const environment = {
  production: true,
  serviceUri: '/name-service'
};

重新将前端应用编译成docker镜像并执行,不出意料,页面无法正确加载,因为调用的RESTful API地址不正确,调用返回404:

容器化单页面应用中RESTful API的访问

接下来,可以使用nginx的反向代理功能,将/name-service的部分proxy_pass到真实的RESTful API地址,而真实的RESTful API地址可以在nginx的配置中通过读取环境变量来动态设置。在前端代码的根目录下,新建nginx.conf文件:

load_module "modules/ngx_http_perl_module.so";
env API_URI;

events {
    worker_connections 1024;
}

http {

    perl_set $api_uri 'sub { return $ENV{"API_URI"}; }';

    server {
      listen        80;
      server_name   localhost;

      include  /etc/nginx/mime.types;

      location / {
        root /usr/share/nginx/html;
        index  index.html  index.htm;
      }

      location ~ ^/name-service/(.*)$ {
        rewrite ^ $request_uri;
        rewrite ^/name-service/(.*)$ $1 break;
        return 400;
        proxy_pass http://$api_uri/$1;
      }
    }
}

该配置文件通过使用nginx的 perl 模块,读取系统环境变量并在nginx中使用这个环境变量,然后设置location,指定当客户端请求/name-service时,将请求proxy_pass到由API_URI环境变量设置的RESTful API地址。由于需要使用perl模块,所以,Dockerfile也要做相应修改:

FROM nginx:perl AS base
WORKDIR /app
EXPOSE 80

FROM node:10.16.0-alpine AS build
RUN npm install -g @angular/cli@8.0.3
WORKDIR /src
COPY . .
RUN npm install
RUN ng build --prod --output-path /app

FROM base AS final
COPY --from=build /app /usr/share/nginx/html
COPY --from=build /src/nginx.conf /etc/nginx/nginx.conf
CMD ["nginx", "-g", "daemon off;"]

Base Image由nginx改为nginx:perl,然后需要将nginx.conf文件复制到nginx容器中的/etc/nginx目录。之后,重新编译前端docker镜像。

现在,启动容器时就可以使用-e参数指定RESTful API的地址了:

docker run -it -p 8088:80 -e API_URI=192.168.0.107:5000 namelist-client

再次刷新前端页面,可以看到,页面正确显示,API调用成功:

容器化单页面应用中RESTful API的访问

RESTful API容器化的场景

如果我们将RESTful API也容器化,并与前端应用一起在容器中运行,那么就可以使用容器连接的方式,让前端页面访问后端的API。此时,只需要对前端nginx.conf进行一些修改:

events {
    worker_connections 1024;
}

http {

    server {
      listen        80;
      server_name   localhost;

      include  /etc/nginx/mime.types;

      location / {
        root /usr/share/nginx/html;
        index  index.html  index.htm;
      }

      location ~ ^/name-service/(.*)$ {
        proxy_pass http://namelist-service/$1$is_args$args;
      }
    }

    upstream namelist-service {
        server namelist-service:5000;
    }
}

分别使用以下两条命令启动RESTful API和前端应用容器:

docker run -it --name namelist-service namelist-service
docker run -it -p 8088:80 --link namelist-service namelist-client

注意到在启动前端应用容器时,需要使用—link参数链接到namelist-service容器,而且服务端也不需要暴露出TCP端口,起到了一定的保护作用:

容器化单页面应用中RESTful API的访问

总结

本文以容器为背景,结合nginx的使用,介绍了容器化单页面应用中访问RESTful API的两种方法。由于单页面应用无法读取系统的环境变量,因此,解决RESTful API访问地址的问题就变得稍微有点复杂。本文相关的案例源代码: https://github.com/daxnet/name-list

容器化单页面应用中RESTful API的访问

(总访问量:22;当日访问量:1)


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

High Performance Python

High Performance Python

Andrew Lewis / O'Reilly Media, Inc. / 2010-09-15 / USD 34.99

Chapter 1. Introduction Section 1.1. The High Performance Buzz-word Chapter 2. The Theory of Computation Section 2.1. Introduction Section 2.2. Problems Section 2.3. Models of Computati......一起来看看 《High Performance Python》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码