Nginx 中运行 JavaScript

Nginx 中运行 JavaScript

njs 从 2016 年的 0.1 版别到现在的 0.78 版别,还不断迭代更新中。

今年的 0.7.x 新版别中新增支撑了 Async/Await、HTTPS Fetch API、WebCrypto API 等不少新特效。

导言

Nginx 作为市场占有率最高的Web服务器,主打高功用、可扩展。自带了很多核心功用模块,而且也有很多的第三方模块。

Web 服务中灰度计划的完成,很多会采用 Nginx + Lua + Redis 计划。Lua 是一个轻量级的脚本言语,体积小、发动速度快、功用高。通过 lua-nginx-module 模块将 Lua 言语嵌入到 Nginx 中,能够运用 Lua 脚本扩展 Nginx 功用,并能够拜访 MySQL、Redis 等数据库。

Nginx 中运转 JavaScript

Lua 虽然是个强壮的脚本言语,但过于小众。Nginx 团队选择非常盛行的 JavaScript 研发 NGINX JavaScript 模块 (njs),让更多工程师能够运用 JavaScript 来扩展 Nginx 功用,然后更好的开展 Nginx 社区生态。

Nginx 中运转 JavaScript

NGINX JavaScript 简介

NGINX JavaScript 简称 njs,是 JavaScript 言语的子集,完成了部分 ECMAScript 5.1(strict mode)规范和 ECMAScript 6 规范,能够运用 njs 来扩展 Nginx 功用。

njs 与 Node.js、JavaScript 的区别

一、运转时不同

Node.js 运用 V8 引擎,njs 是专门为 Nginx 定制规划的运转时。Node.js 运用 V8 引擎在内存中有一个耐久化的 JavaScript 虚拟机 (VM) 并履行废物收集以进行内存管理;而 njs 是专门为 Nginx 规划,非常轻量,会为每个恳求初始化一个新的 JavaScript VM 和必要的内存,并在恳求完成时开释内存。

二、言语规范差异

JavaScript 的规范是由 ECMAScript 规范定义,随着规范版别的更新迭代,会支撑更多的言语功用;njs 自研的服务端运转时,更多的优先支撑服务于 Nginx,只完成了 ECMAScript 5.1 和部分 ECMAScript 6,完成更多规范规范的同时,更多会考虑是否是 Nginx 所需求的。

njs 装置&装备

装置 nginx-module-njs 动态模块,需求 Nginx 版别为 1.9.11 之后支撑动态模块的载入。

yum install nginx-module-njs

装置后,在装备文件 nginx.conf 中需求运用 load_module 指令加载 njs 动态模块。

load_module modules/ngx_http_js_module.so;

njs 运用

Hello World

nginx.conf:

http {
    js_import http.js;
    # or js_import http from http.js;
    server {
        listen 8000;
        location / {
            js_content http.hello;
        }
    }
}

http.js:

function hello(r) {
    r.return(200, "Hello world!");
}
export default { hello };

js_import : 导入一个 njs 模块,没有指定模块称号则默以为文件称号。

js_content : 运用 njs 模块里导出的办法处理这个恳求。

Fetch API

可恳求外部的 URL 获取内容。 nginx.conf:

js_import https.js;
resolver 1.1.1.1;
server {
    listen 80;
    location / {
        js_content main.fetch;
    }
}

https.js:

async function fetch(r) {
    let reply = await ngx.fetch('https://www./');
    let text = await reply.text();
    let footer = "------www.-------";
    r.return(200, `${footer}n${text.substring(0, 200)} ...${text.length - 200} left...n${footer}`);
}
export default { fetch };

HTTP Proxying

运用 njs 模块处理 HTTP 恳求,并运用 subrequest 发起子恳求。

nginx.conf:

js_import http.js;
location /start {
    js_content http.content;
}
location /foo {
    proxy_pass <http://backend1>;
}
location /bar {
    proxy_pass <http://backend2>;
}

http.js:

function content(r) {
    r.subrequest('/api/5/foo', {
          method: 'POST',
          body: JSON.stringify({ foo: 'foo', bar: "bar" })
    }, function(res) {
            if (res.status != 200) {
                r.return(res.status, res.responseBody);
                return;
            }
            var json = JSON.parse(res.responseBody);
            r.return(200, json.content);
    });
}
export default { content };

r.subrequest : 能够去恳求内部的其他 API ,headers 和该恳求相同,而且能够在 location 块里运用 proxy_set_header 来设置或覆盖原来的 header。

自定义日志输出格局

运用 njs 定制 Nginx 日志的输出格局。

nginx.js:

js_import  logging.js;
js_set     $access_log_headers logging.kvAccess;
log_format kvpairs $access_log_headers;
server {
    listen 80;
    root /usr/share/nginx/html;
    access_log /var/log/nginx/access.log kvpairs;
}

logging.js:

function kvAccess(r) {
    var log = `${r.variables.time_iso8601} client=${r.remoteAddress} method=${r.method} uri=${r.uri} status=${r.status}`;
    r.rawHeadersIn.forEach(h => log += ` in.${h[0]}=${h[1]}`);
    r.rawHeadersOut.forEach(h => log += ` out.${h[0]}=${h[1]}`);
    return log;
}
export default { kvAccess }

js_set : 将 njs 模块里的 kvAccess 办法履行后,履行成果放到 $access_log_headers 变量中。但假如只被引用在 log_format 中,则只会在日志记录阶段被履行。

r : HTTP request 目标。特点列表:nginx.org/en/docs/njs…

拜访数据库

一、拜访 Redis

运用 redis2-nginx-module 动态模块,结合 subrequest 来拜访 Redis 数据。

nginx.conf:

js_import http.js;
# GET /redis_get?key=some_key
location = /redis_get {
     # 解码 uri 中的参数 key,赋值到变量 $key
     set_unescape_uri $key $arg_key;
     redis2_query get $key;
     redis2_pass 127.0.0.1:6379;
}
# GET /redis_set?key=one&val=first%20value
location = /redis_set {
     set_unescape_uri $key $arg_key;
     set_unescape_uri $val $arg_val;
     redis2_query set $key $val;
     redis2_pass 127.0.0.1:6379;
}
# GET /get_redis_data?key=some_key
location /get_redis_data {
    js_content http.get_redis_data;
}

http.js:

function serialize(obj) {
    var str = [];
    for (var p in obj) {
        if (obj.hasOwnProperty(p)) {
            str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
        }
    }
    return str.join("&");
};
function get_redis_data(r) {
    r.subrequest('/redis_get', {
          args: serialize(r.args),
          method: 'GET'
    }, function(res) {
            if (res.status != 200) {
                r.return(res.status, res.responseBody);
                return;
            }
            r.return(200, res.responseBody);
    });
    return log;
}
export default { get_redis_data }

set_unescape_uri :解码 uri 中参数的 %XX 编码。

redis2_query : 履行的 Redis 指令。

redis2_pass : Redis 后端服务。

redis2_pass 返回值为相似 redis-cli 履行后的返回值,需求有一个 parser 来解析是否履行成。

二、拜访 MySQL

运用 drizzle-nginx-module 动态模块,结合 subrequest 来拜访 MySQL 数据。

nginx.conf:

upstream backend {
    drizzle_server 127.0.0.1:3306 dbname=test
        password=some_pass user=monty protocol=mysql;
}
server {
    js_import http.js;
    location /mysql {
         set_unescape_uri $name $arg_name;
         # 为避免 SQL 注入进犯,运用 set_quote_sql_str 来设置 sql 语句中的变量
         set_quote_sql_str $quoted_name $name;
         drizzle_query "select * from cats where name = $quoted_name";
         drizzle_pass backend;
         drizzle_connect_timeout    500ms; # default 60s
         drizzle_send_query_timeout 2s;    # default 60s
         drizzle_recv_cols_timeout  1s;    # default 60s
         drizzle_recv_rows_timeout  1s;    # default 60s
    }
    # GET /get_mysql_data?name=cat_name
    location /get_mysql_data {
        js_content http.get_mysql_data;
    }
}

http.js:

function serialize(obj) {
    var str = [];
    for (var p in obj) {
        if (obj.hasOwnProperty(p)) {
            str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
        }
    }
    return str.join("&");
};
function get_mysql_data(r) {
    r.subrequest('/mysql', {
          args: serialize(r.args),
          method: 'GET'
    }, function(res) {
            if (res.status != 200) {
                r.return(res.status, res.responseBody);
                return;
            }
            r.return(200, res.responseBody);
    });
    return log;
}
export default { get_mysql_data }

set_quote_sql_str : 为避免 SQL 注入进犯,来设置 sql 语句中的变量。

drizzle_query : 履行的 SQL 语句。

drizzle_pass : Drizzle 或 MySQL 服务的 upstream。

更多示例

官方示例:github.com/nginx/njs-e…

结语

在 njs 之前,Nginx + Lua 生态虽然已日趋成熟,但 Nginx 毕竟是一个 Web 服务器,JavaScript 作为 Web 开发的最盛行的言语,能够运用 JavaScript 生态来扩展 Nginx 的功用,可能会愈加的有一些想象力做更多的工作。

参考文献

  • 2022年10月 Web 服务器排行榜 news.netcraft.com/archives/20…
  • njs scripting language nginx.org/en/docs/njs…
  • NJS Learning Materials github.com/soulteary/n…
  • Harnessing the Power and Convenience of JavaScript for Each Request with the NGINX JavaScript Module www.nginx.com/blog/harnes…
  • Introducing Nginx NJS www.mywaiting.com/weblogs/int…