前端网关踩坑实践

简介: 在后端微服务中,常见的通常会通过暴露一个统一的网关入口给外界,从而使得整个系统服务有一个统一的入口和出口,收敛服务;然而,在前端这种统一提供网关出入口的服务比较少见,常常是各个应用独立提供出去服务,目前业界也有采用微前端应用来进行应用的调度和通信,其中nginx做转发便是其中的一种方案,这里为了收敛前端应用的出入口,项目需要在内网去做相关的部署,公网端口有限,因而为了更好接入更多的应用,这里借鉴了后端的网关的思路,实现了一个前端网关代理转发方案,本文旨在对本次前端网关实践过程中的一些思考和踩坑进行归纳和总结,也希望能给有相关场景应用的同学提供一些解决方面的思路

后端 | 前端网关踩坑实践.png

项目背景

在后端微服务中,常见的通常会通过暴露一个统一的网关入口给外界,从而使得整个系统服务有一个统一的入口和出口,收敛服务;然而,在前端这种统一提供网关出入口的服务比较少见,常常是各个应用独立提供出去服务,目前业界也有采用微前端应用来进行应用的调度和通信,其中nginx做转发便是其中的一种方案,这里为了收敛前端应用的出入口,项目需要在内网去做相关的部署,公网端口有限,因而为了更好接入更多的应用,这里借鉴了后端的网关的思路,实现了一个前端网关代理转发方案,本文旨在对本次前端网关实践过程中的一些思考和踩坑进行归纳和总结,也希望能给有相关场景应用的同学提供一些解决方面的思路

架构设计

名称 作用 备注
网关层 用来承载前端流量,作为统一入口 可以使用前端路由或后端路由来承载,主要作用是流量切分,也可以将单一应用布置于此处,作为路由与调度的混合
应用层 用来部署各个前端应用,不限于框架,各个应用之间通信可以通过http或者向网关派发,前提是网关层有接收调度的功能存在 不限于前端框架及版本,每个应用已经单独部署完成,相互之间通信需要通过http之间的通信,也可以借助k8s等容器化部署之间的通信
接口层 用来从后端获取数据,由于后端部署的不同形式,可能有不同的微服务网关,也有可能有单独的第三方接口,也可能是node.js等BFF接口形式 对于统一共用的接口形式可将其上承至网关层进行代理转发

方案选择

目前项目应用系统业务逻辑较为复杂,不太便于统一落载在以类SingleSPA形式的微前端形式,因而选择了以nginx为主要技术形态的微前端网关切分的形式进行构建,另外后续需要接入多个第三方的应用,做成iframe形式又会涉及网络打通之间的问题。由于业务形态,公网端口有限,需要设计出一套能够1:n的虚拟端口的形态出来,因而这里最终选择了以nginx作为主网关转发来做流量及应用切分的方案。

层级 方案 备注
网关层 使用一个nginx作为公网流量入口,利用路径对不同子应用进行切分 父nginx应用作为前端应用入口,需要作一个负载均衡处理,这里利用k8s的负载均衡来做,配置3个副本,如果某一个pod挂掉,可以利用k8的机制进行拉起
应用层 多个不同的nginx应用,这里由于做了路径的切分,因而需要对资源定向做一个处理,具体详见下一部分踩坑案例 这里利用docker挂载目录进行处理
接口层 多个不同的nginx应用对接口做了反向代理后,接口由于是浏览器正向发送,因而这里无法进行转发,这里需要对前端代码做一个处理,具体详见踩坑案例 后续会配置ci、cd构建脚手架以及一些配置一些常见前端脚手架如:vue-cli、cra、umi的接入插件包

踩坑案例

静态资源404错误

[案例描述] 我们发现在代理完路径后正常的html资源是可以定位到的,但是对于js、css资源等会出现找不到的404错误

[案例分析] 由于目前应用多为单页应用,而单页应用的主要都是由js去操作dom的,对于mv*框架而言通常又会在前端路由及对一些数据进行拦截操作,因而在对应模板引擎处理过程中需要对资源查找进行相对路径查找

[解决方案] 我们项目构建主要是通过docker+k8s进行部署的,因而这里我们想到将资源路径统一放在一个路径目录下,而这个目录路径需要和父nginx应用转发路径的名称相一致,也就是说子应用需要在父应用中需要注册一个路由信息,后续就可以通过服务注册方式进行定位变更等

父应用nginx配置

{
    "rj": {
        "name": "xxx应用",
        "path: "/rj/"
    }
}
server {
    location /rj/ {
        proxy_pass http://ip:port/rj/;
    }
}

子应用

FROM xxx/nginx:1.20.1
COPY ./dist /usr/share/nginx/html/rj/

接口代理404错误

[案例描述] 在处理完静态资源之后,我们父应用中请求接口,发现接口居然也出现了404的查询错误

[案例分析] 由于目前都是前后端分离的项目,因而后端接口通常也是通过子应用的nginx进行方向代理实现的,这样通过父应用的nginx转发过来后由于父应用的nginx中没有代理接口地址,因而会出现没有资源的情况

[解决方案] 有两种解决方案,一种是通过父应用去代理后端的接口地址来进行,这样的话会出现一个问题就是子应用代理的名称如果相同,并且接口并不只是来自一个微服务,或者会有不同的静态代理以及BFF形式,那样对父应用的构建就会出现复杂度不可控的情形;另一种则是通过改变子应用中的前端请求路径为约定好的一种路径,比如加上约定好的服务注册中的路径进行隔离。这里我们兼而有之,对于我们自研项目的接入,会在复应用中进行统一的网关及静态资源转发代理等配置,与子应用约定好路径名,比如后端网关统一以/api/进行转发,对于非自研项目的接入,我们目前需要接入应用进行接口的魔改,后续我们会提供一个插件库进行常见脚手架的api魔改方案,比如vue-cli/cra/umi等,对于第三方团队自研的脚手架构建应用需要自行手动更改,但一般来说自定义脚手架团队通常会有一个统一配置前端请求的路径,对于老应用如以jq等构建的项目,则需要各自手动更改

这里我以vue-cli3构建的方案进行一个示范:

// config
export const config = {
    data_url: '/rj/api'
};
// 具体接口
// 通常这里会做一些axios的路由拦截处理等
import request from '@/xxx';
// 这里对baseUrl做了统一入口,只需更改这里的baseurl入口即可
import { config } from '@/config';

// 具体接口
export const xxx = (params) => 
    request({
        url: config.data_url + '/xxx'
    })

源码浅析

nginx作为一个轻量的高性能web服务器,其架构及设计是极具借鉴意义的,对node.js或其他web框架的设计具有一定的指导思路

nginx是用C语言书写的,因而其将整个架构通过模块进行组合,其中包含了常见的诸如:HTTP模块、事件模块、配置模块以及核心模块等,通过核心模块来调度和加载其它模块,从而实现了模块之间的相互作用

这里我们主要是需要通过location中的proxy_pass对应用进行转发,因而,我们来看一下proxy模块中对proxy_pass的处理

ngx_http_proxy_module

static char *ngx_http_proxy_pass(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);

static ngx_command_t ngx_http_proxy_commands[] = {
    {
        ngx_string("proxy_pass"),
        NGX_HTTP_LOC_CONF | NGX_HTTP_LIF_CONF | NGX_HTTP_LMT_CONF | NGX_CONF_TAKE1,
        ngx_http_proxy_pass,
        NGX_HTTP_LOC_CONF_OFFSET,
        0,
        NULL
    }
};


static char *
ngx_http_proxy_pass(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
    ngx_http_proxy_loc_conf_t *plcf = conf;

    size_t                      add;
    u_short                     port;
    ngx_str_t                  *value, *url;
    ngx_url_t                   u;
    ngx_uint_t                  n;
    ngx_http_core_loc_conf_t   *clcf;
    ngx_http_script_compile_t   sc;

    if (plcf->upstream.upstream || plcf->proxy_lengths) {
        return "is duplicate";
    }

    clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);

    clcf->handler = ngx_http_proxy_handler;

    if (clcf->name.len && clcf->name.data[clcf->name.len - 1] == '/') {
        clcf->auto_redirect = 1;
    }

    value = cf->args->elts;

    url = &value[1];

    n = ngx_http_script_variables_count(url);

    if (n) {

        ngx_memzero(&sc, sizeof(ngx_http_script_compile_t));

        sc.cf = cf;
        sc.source = url;
        sc.lengths = &plcf->proxy_lengths;
        sc.values = &plcf->proxy_values;
        sc.variables = n;
        sc.complete_lengths = 1;
        sc.complete_values = 1;

        if (ngx_http_script_compile(&sc) != NGX_OK) {
            return NGX_CONF_ERROR;
        }

#if (NGX_HTTP_SSL)
        plcf->ssl = 1;
#endif

        return NGX_CONF_OK;
    }

    if (ngx_strncasecmp(url->data, (u_char *) "http://", 7) == 0) {
        add = 7;
        port = 80;

    } else if (ngx_strncasecmp(url->data, (u_char *) "https://", 8) == 0) {

#if (NGX_HTTP_SSL)
        plcf->ssl = 1;

        add = 8;
        port = 443;
#else
        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                           "https protocol requires SSL support");
        return NGX_CONF_ERROR;
#endif

    } else {
        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "invalid URL prefix");
        return NGX_CONF_ERROR;
    }

    ngx_memzero(&u, sizeof(ngx_url_t));

    u.url.len = url->len - add;
    u.url.data = url->data + add;
    u.default_port = port;
    u.uri_part = 1;
    u.no_resolve = 1;

    plcf->upstream.upstream = ngx_http_upstream_add(cf, &u, 0);
    if (plcf->upstream.upstream == NULL) {
        return NGX_CONF_ERROR;
    }

    plcf->vars.schema.len = add;
    plcf->vars.schema.data = url->data;
    plcf->vars.key_start = plcf->vars.schema;

    ngx_http_proxy_set_vars(&u, &plcf->vars);

    plcf->location = clcf->name;

    if (clcf->named
#if (NGX_PCRE)
        || clcf->regex
#endif
        || clcf->noname)
    {
        if (plcf->vars.uri.len) {
            ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                               "\"proxy_pass\" cannot have URI part in "
                               "location given by regular expression, "
                               "or inside named location, "
                               "or inside \"if\" statement, "
                               "or inside \"limit_except\" block");
            return NGX_CONF_ERROR;
        }

        plcf->location.len = 0;
    }

    plcf->url = *url;

    return NGX_CONF_OK;
}

ngx_http

static ngx_int_t
ngx_http_add_addresses(ngx_conf_t *cf, ngx_http_core_srv_conf_t *cscf,
    ngx_http_conf_port_t *port, ngx_http_listen_opt_t *lsopt)
{
    ngx_uint_t             i, default_server, proxy_protocol;
    ngx_http_conf_addr_t  *addr;
#if (NGX_HTTP_SSL)
    ngx_uint_t             ssl;
#endif
#if (NGX_HTTP_V2)
    ngx_uint_t             http2;
#endif

    /*
     * we cannot compare whole sockaddr struct's as kernel
     * may fill some fields in inherited sockaddr struct's
     */

    addr = port->addrs.elts;

    for (i = 0; i < port->addrs.nelts; i++) {

        if (ngx_cmp_sockaddr(lsopt->sockaddr, lsopt->socklen,
                             addr[i].opt.sockaddr,
                             addr[i].opt.socklen, 0)
            != NGX_OK)
        {
            continue;
        }

        /* the address is already in the address list */

        if (ngx_http_add_server(cf, cscf, &addr[i]) != NGX_OK) {
            return NGX_ERROR;
        }

        /* preserve default_server bit during listen options overwriting */
        default_server = addr[i].opt.default_server;

        proxy_protocol = lsopt->proxy_protocol || addr[i].opt.proxy_protocol;

#if (NGX_HTTP_SSL)
        ssl = lsopt->ssl || addr[i].opt.ssl;
#endif
#if (NGX_HTTP_V2)
        http2 = lsopt->http2 || addr[i].opt.http2;
#endif

        if (lsopt->set) {

            if (addr[i].opt.set) {
                ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                                   "duplicate listen options for %V",
                                   &addr[i].opt.addr_text);
                return NGX_ERROR;
            }

            addr[i].opt = *lsopt;
        }

        /* check the duplicate "default" server for this address:port */

        if (lsopt->default_server) {

            if (default_server) {
                ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                                   "a duplicate default server for %V",
                                   &addr[i].opt.addr_text);
                return NGX_ERROR;
            }

            default_server = 1;
            addr[i].default_server = cscf;
        }

        addr[i].opt.default_server = default_server;
        addr[i].opt.proxy_protocol = proxy_protocol;
#if (NGX_HTTP_SSL)
        addr[i].opt.ssl = ssl;
#endif
#if (NGX_HTTP_V2)
        addr[i].opt.http2 = http2;
#endif

        return NGX_OK;
    }

    /* add the address to the addresses list that bound to this port */

    return ngx_http_add_address(cf, cscf, port, lsopt);
}

static ngx_int_t
ngx_http_add_addrs(ngx_conf_t *cf, ngx_http_port_t *hport,
    ngx_http_conf_addr_t *addr)
{
    ngx_uint_t                 i;
    ngx_http_in_addr_t        *addrs;
    struct sockaddr_in        *sin;
    ngx_http_virtual_names_t  *vn;

    hport->addrs = ngx_pcalloc(cf->pool,
                               hport->naddrs * sizeof(ngx_http_in_addr_t));
    if (hport->addrs == NULL) {
        return NGX_ERROR;
    }

    addrs = hport->addrs;

    for (i = 0; i < hport->naddrs; i++) {

        sin = (struct sockaddr_in *) addr[i].opt.sockaddr;
        addrs[i].addr = sin->sin_addr.s_addr;
        addrs[i].conf.default_server = addr[i].default_server;
#if (NGX_HTTP_SSL)
        addrs[i].conf.ssl = addr[i].opt.ssl;
#endif
#if (NGX_HTTP_V2)
        addrs[i].conf.http2 = addr[i].opt.http2;
#endif
        addrs[i].conf.proxy_protocol = addr[i].opt.proxy_protocol;

        if (addr[i].hash.buckets == NULL
            && (addr[i].wc_head == NULL
                || addr[i].wc_head->hash.buckets == NULL)
            && (addr[i].wc_tail == NULL
                || addr[i].wc_tail->hash.buckets == NULL)
#if (NGX_PCRE)
            && addr[i].nregex == 0
#endif
            )
        {
            continue;
        }

        vn = ngx_palloc(cf->pool, sizeof(ngx_http_virtual_names_t));
        if (vn == NULL) {
            return NGX_ERROR;
        }

        addrs[i].conf.virtual_names = vn;

        vn->names.hash = addr[i].hash;
        vn->names.wc_head = addr[i].wc_head;
        vn->names.wc_tail = addr[i].wc_tail;
#if (NGX_PCRE)
        vn->nregex = addr[i].nregex;
        vn->regex = addr[i].regex;
#endif
    }

    return NGX_OK;
}

总结

对于前端网关而言,不只可以将网关单独独立出来进行分层,也可以采用类SingleSPA的方案利用前端路由进行网关的处理和应用调起,从而实现实现还是单页应用的控制,只是单独拆分出了各个子应用,这样做的好处是各个子应用之间可以通过父应用或者总线进行相互间的通信,以及公共资源的共享和各自私有资源的隔离,对于本项目而言,目前业态更适合使用单独网关层的方式来实现,而使用nginx则可以实现更小的配置来接入各个应用,实现前端入口的收敛,这里后续会为构建ci、cd过程提供脚手架,方便应用开发者接入构建部署,从而实现工程化的效果,对于能够成倍数复制的操作,我们都应该想到利用工程化的手段来进行解决,而不是一味的投入人工,毕竟机器更擅长处理单一不变的批量且稳定产出的工作,共勉!!!

参考

相关文章
|
1月前
|
缓存 前端开发 JavaScript
利用代码分割优化前端性能:策略与实践
在现代Web开发中,代码分割是提升页面加载性能的有效手段。本文介绍代码分割的概念、重要性及其实现策略,包括动态导入、路由分割等方法,并探讨在React、Vue、Angular等前端框架中的具体应用。
|
15天前
|
编解码 前端开发 开发者
探索无界:前端开发中的响应式设计深度实践与思考###
本文将带你领略响应式设计的精髓,一种超越传统页面布局限制的设计策略,它要求开发者以灵活多变的思维,打造能够无缝适应各种设备与屏幕尺寸的Web体验。通过深入浅出的讲解、实际案例分析以及技术实现细节的探讨,本文目的是激发读者对于响应式设计深层次的理解与兴趣,鼓励在实际应用中不断创新与优化。 ###
60 10
|
29天前
|
编解码 前端开发 开发者
前端开发中的响应式设计实践
前端开发中的响应式设计实践
|
1月前
|
编解码 前端开发 UED
探索无界:前端开发中的响应式设计深度解析与实践####
【10月更文挑战第29天】 本文深入探讨了响应式设计的核心理念,即通过灵活的布局、媒体查询及弹性图片等技术手段,使网站能够在不同设备上提供一致且优质的用户体验。不同于传统摘要概述,本文将以一次具体项目实践为引,逐步剖析响应式设计的关键技术点,分享实战经验与避坑指南,旨在为前端开发者提供一套实用的响应式设计方法论。 ####
64 4
|
1月前
|
监控 安全 应用服务中间件
微服务架构下的API网关设计策略与实践####
本文深入探讨了在微服务架构下,API网关作为系统统一入口点的设计策略、实现细节及其在实际应用中的最佳实践。不同于传统的摘要概述,本部分将直接以一段精简的代码示例作为引子,展示一个基于NGINX的简单API网关配置片段,随后引出文章的核心内容,旨在通过具体实例激发读者兴趣,快速理解API网关在微服务架构中的关键作用及实现方式。 ```nginx server { listen 80; server_name api.example.com; location / { proxy_pass http://backend_service:5000;
|
26天前
|
编解码 前端开发 UED
探索无界:前端开发中的响应式设计哲学与实践####
本文不拘泥于传统摘要的框架,而是以一种对话的方式,引领读者踏入响应式设计的奇妙世界。想象一下,互联网如同一片浩瀚的海洋,而网页则是航行其中的船只。在这片不断变化的海域中,如何让我们的“船只”既稳固又灵活地适应各种屏幕尺寸和设备?这正是响应式设计的魅力所在。通过深入浅出的探讨,我们将一同揭开它背后的哲学思想与实战技巧,让你的网页在任何设备上都能展现出最佳姿态。 ####
21 0
|
29天前
|
前端开发 JavaScript API
现代前端框架中的响应式编程实践
现代前端框架中的响应式编程实践
36 0
|
2月前
|
人工智能 资源调度 数据可视化
【AI应用落地实战】智能文档处理本地部署——可视化文档解析前端TextIn ParseX实践
2024长沙·中国1024程序员节以“智能应用新生态”为主题,吸引了众多技术大咖。合合信息展示了“智能文档处理百宝箱”的三大工具:可视化文档解析前端TextIn ParseX、向量化acge-embedding模型和文档解析测评工具markdown_tester,助力智能文档处理与知识管理。
|
1月前
|
缓存 监控 前端开发
前端开发中的性能优化:策略与实践
前端开发中的性能优化:策略与实践
|
2月前
|
前端开发 JavaScript 开发者
构建工具对比:Webpack与Rollup的前端工程化实践
【10月更文挑战第11天】本文对比了前端构建工具Webpack和Rollup,探讨了它们在模块打包、资源配置、构建速度等方面的异同。通过具体示例,展示了两者的基本配置和使用方法,帮助开发者根据项目需求选择合适的工具。
28 3