我的项目背景

在后端微服务中,常见的通常会通过裸露一个对立的网关入口给外界,从而使得整个零碎服务有一个对立的入口和进口,收敛服务;然而,在前端这种对立提供网关出入口的服务比拟少见,经常是各个利用独立提供进来服务,目前业界也有采纳微前端利用来进行利用的调度和通信,其中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.1COPY ./dist /usr/share/nginx/html/rj/

接口代理404谬误

[案例形容] 在解决完动态资源之后,咱们父利用中申请接口,发现接口竟然也呈现了404的查问谬误

[案例剖析] 因为目前都是前后端拆散的我的项目,因此后端接口通常也是通过子利用的nginx进行方向代理实现的,这样通过父利用的nginx转发过去后因为父利用的nginx中没有代理接口地址,因此会呈现没有资源的状况

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

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

// configexport 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_tngx_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_tngx_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过程提供脚手架,不便利用开发者接入构建部署,从而实现工程化的成果,对于可能成倍数复制的操作,咱们都应该想到利用工程化的伎俩来进行解决,而不是一味的投入人工,毕竟机器更善于解决繁多不变的批量且稳固产出的工作,共勉!!!

参考

  • 搞懂nginx的rewrite模块
  • 前端网关的思考
  • Nginx系列之代理之后无奈加载动态资源解决办法
  • 前端 重定向和转发