喻蘭 發表於 2024-7-18 08:34:00

njs最详细的入门手册:Nginx JavaScript Engine

<p>原文链接:https://hi.imzlh.top/2024/07/08.cgi</p>
<h1 id="关于njs">关于njs</h1>
<p>首先,njs似乎在国内外都不受关注,资料什么的只有 官网参考手册,出了个问题只能看到Github Issue<br>
所以,这篇文章将我的探索过程展示给大家,njs对于可用存储空间较小的设备真的很友好,相比较于NodeJS、Deno这种80M起步的运行环境真的很轻量<br>
但是,这里有几点需要提一下,入坑需谨慎:</p>
<ul>
<li>不完善的语法
<ul>
<li>for...of不可用</li>
<li>import和export只能使用默认导出</li>
<li>try...catch 不能不定义捕获的内容,比如这个就不合法<br>
try{<br>
require('fs').statSync('/')<br>
}catch{<br>
ngx.log(ngx.INFO, '找不到模块fs')<br>
}</li>
<li>没有Event支持,如<code>addEventListener</code></li>
<li>...</li>
</ul>
</li>
<li>没有GC<br>
这表明NJS VM是一次性的,除非手动垃圾回收</li>
<li>反人类的API设计<br>
比如,fs.open()后不能<code>seek()</code>,返回的是<code>UInt8Array</code></li>
<li>社区不完善<br>
你可能需要自己摸索,甚至有阅读源码和提Issue的勇气</li>
<li>...</li>
</ul>
<h1 id="入门第一步typescript">入门第一步:TypeScript</h1>
<p>虽然njs不支持TypeScript,但是不影响我们使用TypeScript为代码添加类型检查<br>
NJS官方开发了TypeScript类型定义,开箱即用<br>
将定义放在type文件夹中,然后使用三斜杠ref语法引入</p>
<p><img src="https://img2024.cnblogs.com/blog/2633867/202407/2633867-20240718083048010-842194713.png"></p>
<p>入口上,我们不能使用export function语法(前文提到过),需要定义一个入口函数然后使用默认导出</p>
<pre><code>async function main(h:NginxHTTPRequest){
    // ...
}
export default { main }
</code></pre>
<p><strong>注意</strong><br>
这个时候不能使用<code>njs-cli</code>运行,会显示<code>SyntaxError: Illegal export statement</code><br>
解决办法:<code>njs -c "import M from './main.js'; M.main();"</code></p>
<p><strong>提示</strong><br>
Nginx的Buffer和NodeJS的Buffer很像,我就不多介绍了</p>
<h1 id="文件系统fs">文件系统(fs)</h1>
<p>使用NJS的目标就是代替NginxLUA模块,NJS复用Nginx的事件循环,因此支持异步操作<br>
异步操作用的最多的就是文件IO,即<code>fs</code><br>
使用fs有两种方式(这一点上和NodeJS很像)</p>
<ul>
<li>ES式 <code>import FS from 'fs';</code></li>
<li>CommonJS式 <code>const FS = require('fs');</code></li>
</ul>
<p>FS内有两种,一种是同步IO(不建议,但API简单)和异步IO(共享Nginx的EventLoop)<br>
下面我们以异步IO为例:</p>
<h2 id="access-尝试获取文件">access(): 尝试获取文件</h2>
<p>access最大的作用是确保文件是如你所想的,要知道,<code>Permission Denied</code>很烦人<br>
这个是官方的实例:</p>
<pre><code>import fs from 'fs'
fs.promises.access('/file/path', fs.constants.R_OK | fs.constants.W_OK)
.then(() =&gt; console.log('has access'))
.catch(() =&gt; console.log('no access'))
</code></pre>
<ul>
<li>第一个参数(<em>字符串</em>)是<strong>文件名</strong></li>
<li>第二个参数(<em>数字</em>)是<strong>文件模式</strong>,允许使用位或(<code>|</code>),官方提供了<code>fs.constants</code><br>
<code>fs.constants</code>里有一些预设变量,方便使用
<ul>
<li><strong>R_OK</strong> 可读 (0b100)</li>
<li><strong>W_OK</strong> 可写 (0b10)</li>
<li><strong>X_OK</strong> 可执行 (0b1)</li>
<li><strong>F_OK</strong> 好歹是个文件(夹) (0b0)</li>
</ul>
</li>
</ul>
<p><strong>注意</strong> 这个函数最大的坑就是没有返回值,如果<strong>没有权限就抛出错误</strong>,千万别忘记<code>catch</code></p>
<h2 id="open-打开文件">open(): 打开文件</h2>
<p>这个函数很关键,用于打开文件</p>
<pre><code>open(path: Buffer|string, flags?: OpenMode, mode?: number): Promise&lt;NjsFsFileHandle&gt;;
</code></pre>
<ul>
<li>第一个参数是<strong>文件位置</strong>(<em>string</em>),甚至可以传入<code>Buffer</code></li>
<li>第二个参数是打开模式<br>
| 文件模式 | 描述 |<br>
|-----|-----|<br>
| "a" | 打开文件用于追加。 如果文件不存在,则创建该文件|<br>
| "ax" | 类似于 'a',但如果路径存在,则失败 |<br>
| "a+" | 打开文件用于读取和追加。 如果文件不存在,则创建该文件 |<br>
| "ax+" | 类似于 'a+',但如果路径存在,则失败 |<br>
| "as" | 打开文件用于追加(在同步模式中)。 如果文件不存在,则创建该文件 |<br>
| "as+" | 打开文件用于读取和追加(在同步模式中)。 如果文件不存在,则创建该文件 |<br>
| "r" | 打开文件用于读取。 如果文件不存在,则会发生异常 |<br>
| "r+" | 打开文件用于读取和写入。 如果文件不存在,则会发生异常 |<br>
| "rs+" | 类似于 'r+',但如果路径存在,则失败 |<br>
| "w" | 打开文件用于写入。 如果文件不存在则创建文件,如果文件存在则截断文件 |<br>
| "wx" | 类似于 'w',但如果路径存在,则失败 |<br>
| "w+" | 打开文件用于读取和写入。 如果文件不存在则创建文件,如果文件存在则截断文件 |<br>
| "wx+" | 类似于 'w+',但如果路径存在,则失败 |</li>
</ul>
<p>这个函数重点是返回的结果。什么?看不起?好,那么我们尝试读取文件的一段<br>
我们先看一下结构</p>
<ul>
<li>close()<br>
关闭这个文件fd</li>
<li>fd<br>
文件fd(file description)</li>
<li>read(buffer, buf_offset, read_len, pos)
<ul>
<li><code>buffer</code> 传入一个Buffer用于缓冲。当读取完毕时,这个Buffer里有我们想要的数据</li>
<li><code>buf_offset</code> 这个Buffer开始填充的位置。可以用这个实现一个Buffer读取指定大小的内容</li>
<li><code>read_len</code> 读取长度,但是如果超出了Nginx的Buffer大小,这个数值相对于实际读取的大小会偏大</li>
<li><code>pos</code> 这个是我们今天的重头戏<br>
想要知道如何seek吗?不行,必须使用<code>pos</code><br>
如果设定为数字,将seek到那个地方并开始读取<br>
如果设定为<code>null</code>,不改变文件指针位置,从当前位置开始读取<br>
是不是很反人类?</li>
<li>最后返回<code>NjsFsBytesRead</code>,其中有两个元素
<ul>
<li><code>bytesRead</code>,读取的长度</li>
<li><code>buffer</code>,就是你传入的buffer</li>
</ul>
</li>
</ul>
</li>
<li>stat()<br>
等同于<code>fs.promises.stat()</code>的结果</li>
<li><strong></strong> write(buffer, buf_offset, read_len, pos)
<ul>
<li><code>buffer</code> 老规矩,写入的数据Buffer</li>
<li><code>buf_offset</code> 这个Buffer开始读取的位置</li>
<li><code>read_len</code> 从这个Buffer读取用于写入长度,但是如果超出了Nginx的Buffer大小,这个数值相对于实际读取的大小会偏大</li>
<li><code>pos</code> 和上面<code>read()</code>的pos参数一致</li>
<li>最后返回<code>NjsFsBytesWritten</code>,其中有两个元素
<ul>
<li><code>bytesWritten</code>,写入的长度</li>
<li><code>buffer</code>,就是你传入的buffer</li>
</ul>
</li>
</ul>
</li>
<li><strong></strong> write(string, pos, encoding)
<ul>
<li>write()也可以写入字符串</li>
<li><code>string</code> 等待写入的字符串</li>
<li><code>pos</code> 和上面<code>read()</code>的pos参数一致</li>
<li><code>encoding</code> 编码格式,可选 <code>utf8</code> <code>hex</code> <code>base64</code> <code>base64url</code></li>
</ul>
</li>
</ul>
<p>这是TypeScript定义</p>
<pre><code>interface NjsFsFileHandle {
    close(): Promise&lt;void&gt;;
    fd: number;
    read(buffer: NjsBuffer, offset: number, length: number, position: number | null): Promise&lt;NjsFsBytesRead&gt;;
    stat(): Promise&lt;NjsStats&gt;;
    write(buffer: NjsBuffer, offset: number, length?: number, position?: number | null): Promise&lt;NjsFsBytesWritten&gt;;
    write(buffer: string, position?: number | null, encoding?: FileEncoding): Promise&lt;NjsFsBytesWritten&gt;;
}
</code></pre>
<p>关于使用,可以见 https://github.com/imzlh/vlist-njs/blob/master/main.ts#L130,实现纯粹文件拷贝</p>
<pre><code> const st = await fs.promises.open(from,'r'),
    en = await fs.promises.open(to,'w');
while(true){
    // 读取64k 空间
    const buf = new Uint8Array(64 * 1024),
      readed = await st.read(buf, 0, 64 * 1024, null);

    // 读取完成
    if(readed.bytesRead == 0) break;

    // 防漏式写入
    let writed = 0;
    do{
      const write = await en.write(buf, writed, readed.bytesRead - writed, null);
      writed += write.bytesWritten;
    }while(writed != readed.bytesRead);
}
</code></pre>
<h2 id="readdir扫描文件夹">readdir():扫描文件夹</h2>
<p>虽然我们建议返回填满string的数组,但是返回填充了Buffer的数组也不是不行</p>
<pre><code>readdir(path, option)
</code></pre>
<ul>
<li><code>path</code> 路径,同样可以是Buffer</li>
<li><code>option</code> Object对象
<ul>
<li><code>encoding</code> 编码格式,可选 <code>utf8</code>(返回字符串) <code>buffer</code>(返回<code>Buffer</code>)</li>
<li><code>withFileTypes</code> 自带stat文件类型的扫描,指定为true,返回的就是<code>NjsDirent[]</code>了
<ul>
<li><code>isBlockDevice()</code></li>
<li><code>isCharacterDevice()</code></li>
<li><code>isDirectory()</code></li>
<li><code>isFIFO()</code></li>
<li><code>isFile()</code></li>
<li><code>isSocket()</code></li>
<li><code>isSymbolicLink()</code></li>
<li><code>name</code> 文件(夹)名</li>
</ul>
</li>
</ul>
</li>
<li>返回值由option决定,如果什么都没指定,返回字符串数组</li>
</ul>
<h2 id="realpath-相对路径转绝对路径">realpath(): 相对路径转绝对路径</h2>
<pre><code>realpath(path, option)
</code></pre>
<ul>
<li><code>path</code> 路径,同样可以是Buffer</li>
<li><code>option</code> Object对象
<ul>
<li><code>encoding</code> 编码格式,可选 <code>utf8</code>(返回字符串) <code>buffer</code>(返回<code>Buffer</code>)</li>
</ul>
</li>
<li>返回值由option决定,如果什么都没指定,返回字符串</li>
</ul>
<h2 id="rename-移动文件">rename(): 移动文件</h2>
<p><strong>注意</strong>跨文件系统(磁盘)移动不能使用rename(),instead,请<strong>拷贝后再删除</strong><br>
<strong>实用技巧</strong> 什么?你告诉我你不会判断是否跨文件系统(磁盘)?stat()啊</p>
<pre><code>const from = await fs.promises.stat('...'),
    to = await fs.promises.stat('...');
            
// 相同dev使用rename
if(from.dev == to.dev){
    await fs.promises.rename(...);
}else{
    // copy()
    await fs.promises.unlink('...');
}
</code></pre>
<p>实例参考:https://github.com/imzlh/vlist-njs/blob/master/main.ts#L622</p>
<pre><code>rename(from, to)
</code></pre>
<ul>
<li><code>from</code> 路径,除了string同样可以是Buffer</li>
<li><code>to</code> 路径,同理,除了string同样可以是Buffer</li>
<li>没有返回值,注意<code>catch</code>错误情况</li>
</ul>
<h2 id="unlink-删除文件">unlink() 删除文件</h2>
<pre><code>unlink(path: PathLike): Promise&lt;void&gt;;
</code></pre>
<ul>
<li><code>path</code> 路径,同样可以是Buffer</li>
<li>没有返回值</li>
</ul>
<h2 id="rmdir-删除文件夹">rmdir() 删除文件夹</h2>
<pre><code>rmdir(path: PathLike, options?: { recursive?: boolean; }): Promise&lt;void&gt;;
</code></pre>
<ul>
<li><code>path</code> 路径,同样可以是Buffer</li>
<li><code>options</code>
<ul>
<li><code>recursive</code> <strong>递归删除</strong>,相当于大名鼎鼎的<code>rm -r</code><br>
<del>建议体验这个命令,你就知道什么是递归删除了: <code>rm -rf /</code></del></li>
</ul>
</li>
<li>没有返回值</li>
</ul>
<h2 id="stat-获取文件夹状态">stat() 获取文件(夹)状态</h2>
<pre><code>stat(path: PathLike, options?: { throwIfNoEntry?: boolean; }): Promise&lt;NjsStats&gt;;
</code></pre>
<ul>
<li><code>path</code> 路径,同样可以是Buffer</li>
<li><code>options</code> Object对象
<ul>
<li><code>throwIfNoEntry</code><br>
如果设置为<code>true</code>,文件不存在时直接报错,否侧返回 <strong>undefined</strong></li>
</ul>
</li>
<li>返回NjsStat
<ul>
<li>isBlockDevice()</li>
<li>isCharacterDevice()</li>
<li>isDirectory()</li>
<li>isFIFO()</li>
<li>isFile()</li>
<li>isSocket()</li>
<li>isSymbolicLink()</li>
<li><code>dev</code>: number 处于的文件系统ID</li>
<li><code>ino</code>: number inode数量</li>
<li><code>mode</code>: number 文件模式,8进制</li>
<li><code>nlink</code>: number 这个文件实际地址硬链接数量,即引用数</li>
<li><code>uid</code>: number 所有者User ID</li>
<li><code>gid</code>: number 所有者Group ID</li>
<li><code>rdev</code>: number 这个文件代表文件系统时表示此文件代表的文件系统ID</li>
<li><code>size</code>: number 文件大小</li>
<li><code>blksize</code>: number</li>
<li><code>blocks</code>: number</li>
<li><code>atimeMs</code>: number 最后访问时间戳</li>
<li><code>mtimeMs</code>: number 最后修改文件修饰(模式)时间戳</li>
<li><code>ctimeMs</code>: number 最后修改时间戳</li>
<li><code>birthtimeMs</code>: number 创建时间</li>
<li><code>atime</code>: <strong>Date</strong>;</li>
<li><code>mtime</code>: <strong>Date</strong>;</li>
<li><code>ctime</code>: <strong>Date</strong>;</li>
<li><code>birthtime</code>: <strong>Date</strong>;</li>
</ul>
</li>
</ul>
<h2 id="symlink-创建-软-链接">symlink() 创建 <em>软</em> 链接</h2>
<pre><code>symlink(target: PathLike, path: PathLike): Promise&lt;void&gt;;
</code></pre>
<ul>
<li><code>target</code> 目标(要创建软连接的)文件路径,同样可以是Buffer</li>
<li><code>path</code> 新建的软连接的路径,同样可以是Buffer</li>
<li>没有返回值</li>
</ul>
<h2 id="writefile和readfile-偷懒读写文件的好方法">writeFile和readFile 偷懒读/写文件的好方法</h2>
<pre><code>readFile(path: Buffer|string): Promise&lt;Buffer&gt;;
readFile(path: Buffer|string, options?: {
    flag?: "a" | "ax" | "a+" | "ax+" | "as" | "as+" | "r" | "r+" | "rs+" | "w" | "wx" | "w+" | "wx+"
}): Promise&lt;Buffer&gt;;
readFile(path: Buffer|string, options: {
    flag?: "a" | "ax" | "a+" | "ax+" | "as" | "as+" | "r" | "r+" | "rs+" | "w" | "wx" | "w+" | "wx+",
    encoding?: "utf8" | "hex" | "base64" | "base64url"
} | "utf8" | "hex" | "base64" | "base64url"): Promise&lt;string&gt;;
writeFile(path: Buffer|string, data: string | Buffer | DataView | TypedArray | ArrayBuffer, options?: {
    mode?: number;
    flag?: "a" | "ax" | "a+" | "ax+" | "as" | "as+" | "r" | "r+" | "rs+" | "w" | "wx" | "w+" | "wx+"
}): Promise&lt;void&gt;;
</code></pre>
<p>不多作介绍,看定义就行</p>
<h1 id="请求request">请求(request)</h1>
<p>请求,就是传入主函数的一个参数,函数由<code>export</code>导出和<code>js_import</code>导入以供nginx调用<br>
这个是函数定义(main.js)</p>
<pre><code>async main(h:NginxHTTPRequest):any;
</code></pre>
<p>这个是导出(main.js)</p>
<pre><code>export { main };
</code></pre>
<p>这个是导入(nginx http)</p>
<pre><code>js_import SCRIPT from 'main.js';
</code></pre>
<p>这个是使用(nginx location)</p>
<pre><code>location /@api/{
    js_content SCRIPT.main;
}
</code></pre>
<p>这样,每当请求<code>/@api/</code>时,<code>main()</code>就会被调用,所有Promise完成时VM会被回收<br>
这里讲4个很常用的技巧</p>
<h2 id="args-get参数">args GET参数</h2>
<p><code>h.args</code> 是一个数组,官方是这么说的</p>
<blockquote>
<p>Since 0.7.6, duplicate keys are returned as an array, keys are<br>
case-sensitive, both keys and values are percent-decoded.<br>
For example, the query string<br>
<code>a=1&amp;b=%32&amp;A=3&amp;b=4&amp;B=two%20words</code><br>
is converted to r.args as:<br>
<code>{a: "1", b: ["2", "4"], A: "3", B: "two words"}</code></p>
</blockquote>
<p>args会自动解码分割,允许重复且重复的会变成一个<code>Array</code>。<br>
这里就很重要了,每一个请求你都需要检查你需要的arg是不是<code>Array</code>或<code>string</code>而不能认为只要不是<code>undefined</code>就是<code>string</code>,下面的代码就是最好的反例</p>
<pre><code>if(typeof h.args.action != 'string')
    return h.return(400,'invaild request: Action should be defined');
</code></pre>
<p>当请求<code>/@api/?action=a&amp;action=b</code>时,这个函数会错误报错,事实上<code>Action</code>已经定义</p>
<h2 id="headersio">headersIO</h2>
<p><code>h.headersIn</code>和<code>h.headersOut</code>是Nginx分割好的Header,你可以直接使用<br>
但是这两个常量有很大的限制,必须是Nginx内部专门定义的Header才会出现<br>
其中,<code>headersIn</code>的定义是这样的</p>
<pre><code>readonly 'Accept'?: string;
readonly 'Accept-Charset'?: string;
readonly 'Accept-Encoding'?: string;
readonly 'Accept-Language'?: string;
readonly 'Authorization'?: string;
readonly 'Cache-Control'?: string;
readonly 'Connection'?: string;
readonly 'Content-Length'?: string;
readonly 'Content-Type'?: string;
readonly 'Cookie'?: string;
readonly 'Date'?: string;
readonly 'Expect'?: string;
readonly 'Forwarded'?: string;
readonly 'From'?: string;
readonly 'Host'?: string;
readonly 'If-Match'?: string;
readonly 'If-Modified-Since'?: string;
readonly 'If-None-Match'?: string;
readonly 'If-Range'?: string;
readonly 'If-Unmodified-Since'?: string;
readonly 'Max-Forwards'?: string;
readonly 'Origin'?: string;
readonly 'Pragma'?: string;
readonly 'Proxy-Authorization'?: string;
readonly 'Range'?: string;
readonly 'Referer'?: string;
readonly 'TE'?: string;
readonly 'User-Agent'?: string;
readonly 'Upgrade'?: string;
readonly 'Via'?: string;
readonly 'Warning'?: string;
readonly 'X-Forwarded-For'?: string;
</code></pre>
<p>这个是<code>headersOut</code></p>
<pre><code>'Age'?: string;
'Allow'?: string;
'Alt-Svc'?: string;
'Cache-Control'?: string;
'Connection'?: string;
'Content-Disposition'?: string;
'Content-Encoding'?: string;
'Content-Language'?: string;
'Content-Length'?: string;
'Content-Location'?: string;
'Content-Range'?: string;
'Content-Type'?: string;
'Date'?: string;
'ETag'?: string;
'Expires'?: string;
'Last-Modified'?: string;
'Link'?: string;
'Location'?: string;
'Pragma'?: string;
'Proxy-Authenticate'?: string;
'Retry-After'?: string;
'Server'?: string;
'Trailer'?: string;
'Transfer-Encoding'?: string;
'Upgrade'?: string;
'Vary'?: string;
'Via'?: string;
'Warning'?: string;
'WWW-Authenticate'?: string;
'Set-Cookie'?: string[];
</code></pre>
<p>其中最需要注意的是<code>h.headersOut['Set-Cookie']</code>是一个数组<br>
当然,大部分情况下这些Header足够你玩了,但是有的时候还是需要自定义的,这个时候<code>raw</code>开头的变量上场了</p>
<pre><code>readonly rawHeadersIn: Array&lt;&gt;;
readonly rawHeadersOut: Array&lt;&gt;;
</code></pre>
<p>这些都是按照数组 <code></code> 排的,你可以用下面的代码快速找到你想要的</p>
<pre><code>const headers = {} as Record&lt;string, Array&lt;string&gt;&gt;;
h.rawHeadersIn.forEach(item =&gt; item in headers ? headers].push(item) : headers] = ])
h['X-user-defined']; // 你想要的
</code></pre>
<p>如果是自定义输出的话,第一个想到的是不是应该也是<code>h.rawHeadersOut</code>?<br>
然而,我发现 官方的示例 中用的不是<code>rawHeadersOut</code>而是<code>headersOut</code><br>
的确,我在<code>rawHeadersOut</code>这些东西的定义下面都发现了</p>
<pre><code>: string | string[] | undefined;
</code></pre>
<p>这个让rawHeaders系列更加意味不明了,我也不清楚官方的做法<br>
总之用 <code>headersOut</code> 准没错</p>
<h2 id="用这些函数响应客户端">用这些函数响应客户端</h2>
<p>这个函数发送的是整个请求,调用后这个请求就结束了</p>
<pre><code>return(status: number, body?: NjsStringOrBuffer): void;
</code></pre>
<p>这三个函数是用来搭配响应的,但是我不清楚 官方的用意。<br>
嘛,大部分时间还是别这么玩吧</p>
<pre><code>sendHeader(): void;
send(part: NjsStringOrBuffer): void;
finish(): void;
</code></pre>
<h2 id="nginx的特色">NGINX的特色</h2>
<pre><code>internalRedirect(uri: NjsStringOrBuffer): void;
parent?: NginxHTTPRequest;
subrequest(uri: NjsStringOrBuffer, options: NginxSubrequestOptions &amp; { detached: true }): void;
subrequest(uri: NjsStringOrBuffer, options?: NginxSubrequestOptions | string): Promise&lt;NginxHTTPRequest&gt;;
subrequest(uri: NjsStringOrBuffer, options: NginxSubrequestOptions &amp; { detached?: false } | string,
         callback:(reply:NginxHTTPRequest) =&gt; void): void;
subrequest(uri: NjsStringOrBuffer, callback:(reply:NginxHTTPRequest) =&gt; void): void;
</code></pre>
<p>是不是很心动?的确,你可以使用<code>subrequest</code>分割任务,<code>internalRedirect</code>快速服务文件,<code>parent</code>在子请求内直接操纵响应<br>
举个例子,你验证完Token想要发送给客户端一个文件</p>
<p>nginx.conf:</p>
<pre><code>location /@files/{
    internal;
    alias /file/;
}
</code></pre>
<p>file.js</p>
<pre><code>// ....
h.internalRedirect('/@files/' + file_path);
// 这个时候客户端就接收到了`/files/{file_path}`这个文件
</code></pre>
<h2 id="buffer系列">Buffer系列</h2>
<p>请注意这一句话</p>
<blockquote>
<p>*** if it has not been written to a temporary file.</p>
</blockquote>
<p>详情请参看我的这篇踩坑文章 https://hi.imzlh.top/2024/07/09.cgi<br>
总之,这是Nginx的Buffer,而客户端的上传如果大于<code>client_body_buffer_size</code>会被写入文件并暴露在变量中 <code>h.variables.request_body_file</code></p>
<pre><code>readonly requestBuffer?: Buffer;
readonly requestText?: string;
</code></pre>
<p>需要注意的是,下面的两项是<strong>subrequest</strong>返回的内容而不会写入客户端Buffer<br>
想要给客户端则需要这样: <code>r.return(res.status, res.responseText)</code><br>
这个是Nginx官方的例子</p>
<pre><code>readonly responseBuffer?: Buffer;
readonly responseText?: string;
</code></pre>
<h2 id="输出到日志的函数">输出到日志的函数</h2>
<pre><code>error(message: NjsStringOrBuffer): void;
log(message: NjsStringOrBuffer): void;
warn(message: NjsStringOrBuffer): void;
</code></pre>
<p>这些很好理解,就是<code>log</code> <code>warn</code> <code>error</code>三个等级的日志</p>
<h2 id="这些函数不要碰">这些函数不要碰</h2>
<p>这些函数是<code>js_body_filter</code>才能使用的,对于新手像我一样找不到为什么出错的很致命</p>
<pre><code>sendBuffer(data: NjsStringOrBuffer, options?: NginxHTTPSendBufferOptions): void;
done(): void;
</code></pre>
<h2 id="其他你感兴趣的">其他你感兴趣的</h2>
<ul>
<li><code>httpVersion: string</code> HTTP版本号</li>
<li><code>method: string</code> HTTP方法,是大写的</li>
<li><code>remoteAddress: string</code> 客户端地址</li>
<li><code>uri: string</code> 请求的URL,在subrequest则是subrequest的URL</li>
<li><code>variables: NginxVariables</code> Nginx变量,是UTF8字符串</li>
<li><code>rawVariables: NginxRawVariables</code> Nginx变量,不同的是值是<code>Buffer</code></li>
</ul>
<h1 id="全局命名空间">全局命名空间</h1>
<h2 id="njs">njs</h2>
<p>NJS有一个全局命名空间<code>njs.*</code>,这里面的东西全局可用不分场合</p>
<ul>
<li><code>version: string</code> njs版本</li>
<li><code>version_number: number</code> njs版本,字符串版本</li>
<li><code>on(event: "exit", callback: () =&gt; void): void</code> VM退出时的回调</li>
<li><code>dump(value: any, indent?: number): string</code> pre打印,输出到日志</li>
</ul>
<h2 id="ngx">ngx</h2>
<p>还有一个命名空间叫做<code>ngx.*</code>,这里面的东西与nginx相关<br>
东西太多,我就介绍最重要的</p>
<ul>
<li><code>fetch(init: NjsStringOrBuffer | Request, options?: NgxFetchOptions): Promise&lt;Response&gt;</code><br>
和Web很像的<code>fetch</code>API,只是第二个参数大缩水了
<ul>
<li><code>body?: string</code></li>
<li><code>headers?: NgxHeaders</code></li>
<li><code>method?: string</code></li>
<li><code>verify?: boolean</code> 是否验证SSL证书,默认验证,不符合会报错</li>
</ul>
</li>
<li><code>log(level: number, message: NjsStringOrBuffer): void</code><br>
写入到Nginx日志,level可以是这些
<ul>
<li><code>ngx.INFO</code></li>
<li><code>ngx.WARN</code></li>
<li><code>ngx.ERR</code></li>
</ul>
</li>
<li><code>readonly shared: NgxGlobalShared</code><br>
共享池,这个很有用,重点介绍下<br>
当多个VM需要共享一个数据时,我们第一个想到的解决方法时数据库(DataBase)<br>
但是njs现在不支持数据库,作为过渡,这个<code>shared</code>就是解决方法<br>
通过共享池,共享同样的数据,再使用共享锁就可以实现了<br>
其中共享池名称 大小 类型由<code>js_shared_dict_zone</code>定义<br>
这些是可利用的所有函数
<ul>
<li>ngx.shared.[共享池名称].add()</li>
<li>ngx.shared.[共享池名称].capacity 共享池大小</li>
<li>ngx.shared.[共享池名称].clear()</li>
<li>ngx.shared.[共享池名称].delete()</li>
<li>ngx.shared.[共享池名称].freeSpace()</li>
<li>ngx.shared.[共享池名称].get()</li>
<li>ngx.shared.[共享池名称].has()</li>
<li>ngx.shared.[共享池名称].incr() 增大一个键对应的值的大小</li>
<li>ngx.shared.[共享池名称].items()</li>
<li>ngx.shared.[共享池名称].keys()</li>
<li>ngx.shared.[共享池名称].name</li>
<li>ngx.shared.[共享池名称].pop()</li>
<li>ngx.shared.[共享池名称].replace()</li>
<li>ngx.shared.[共享池名称].set()</li>
<li>ngx.shared.[共享池名称].size() 这个共享池元素的数量</li>
<li>ngx.shared.[共享池名称].type 类型<code>string</code>或<code>number</code>,由<code>js_shared_dict_zone</code>定义</li>
</ul>
</li>
<li><code>worker_id</code> 工作进程的ID,对于定时任务指定很有效</li>
</ul><br><br>
来源:https://www.cnblogs.com/imzlh/p/18308662
頁: [1]
查看完整版本: njs最详细的入门手册:Nginx JavaScript Engine