原文链接:https://hi.imzlh.top/2024/07/08.cgi
关于njs
首先,njs似乎在国表里都不受关注,资料什么的只有 官网参考手册,出了个题目只能看到Github Issue
以是,这篇文章将我的探索过程展示给大家,njs对于可用存储空间较小的设备真的很友好,相比较于NodeJS、Deno这种80M起步的运行环境真的很轻量
但是,这里有几点需要提一下,入坑需谨慎:
- 不完善的语法
- for...of不可用
- import和export只能使用默认导出
- try...catch 不能不定义捕获的内容,比如这个就不合法
try{
require('fs').statSync('/')
}catch{
ngx.log(ngx.INFO, '找不到模块fs')
}
- 没有Event支持,如addEventListener
- ...
- 没有GC
这表明NJS VM是一次性的,除非手动垃圾回收
- 反人类的API设计
比如,fs.open()后不能seek(),返回的是UInt8Array
- 社区不完善
你可能需要自己摸索,甚至有阅读源码和提Issue的勇气
- ...
入家世一步:TypeScript
虽然njs不支持TypeScript,但是不影响我们使用TypeScript为代码添加范例检查
NJS官方开发了TypeScript范例定义,开箱即用
将定义放在type文件夹中,然后使用三斜杠ref语法引入
入口上,我们不能使用export function语法(前文提到过),需要定义一个入口函数然后使用默认导出- async function main(h:NginxHTTPRequest){
- // ...
- }
- export default { main }
复制代码 注意
这个时候不能使用njs-cli运行,会显示SyntaxError: Illegal export statement
办理办法:njs -c "import M from './main.js'; M.main();"
提示
Nginx的Buffer和NodeJS的Buffer很像,我就不多介绍了
文件系统(fs)
使用NJS的目的就是代替NginxLUA模块,NJS复用Nginx的变乱循环,因此支持异步操纵
异步操纵用的最多的就是文件IO,即fs
使用fs有两种方式(这一点上和NodeJS很像)
- ES式 import FS from 'fs';
- CommonJS式 const FS = require('fs');
FS内有两种,一种是同步IO(不建议,但API简朴)和异步IO(共享Nginx的EventLoop)
下面我们以异步IO为例:
access(): 尝试获取文件
access最大的作用是确保文件是如你所想的,要知道,Permission Denied很烦人
这个是官方的实例:- import fs from 'fs'
- fs.promises.access('/file/path', fs.constants.R_OK | fs.constants.W_OK)
- .then(() => console.log('has access'))
- .catch(() => console.log('no access'))
复制代码
- 第一个参数(字符串)是文件名
- 第二个参数(数字)是文件模式,答应使用位或(|),官方提供了fs.constants
fs.constants里有一些预设变量,方便使用
- R_OK 可读 (0b100)
- W_OK 可写 (0b10)
- X_OK 可执行 (0b1)
- F_OK 好歹是个文件(夹) (0b0)
注意 这个函数最大的坑就是没有返回值,如果没有权限就抛出错误,千万别忘记catch
open(): 打开文件
这个函数很关键,用于打开文件- open(path: Buffer|string, flags?: OpenMode, mode?: number): Promise<NjsFsFileHandle>;
复制代码
- 第一个参数是文件位置(string),甚至可以传入Buffer
- 第二个参数是打开模式
| 文件模式 | 形貌 |
|-----|-----|
| "a" | 打开文件用于追加。 如果文件不存在,则创建该文件|
| "ax" | 雷同于 'a',但如果路径存在,则失败 |
| "a+" | 打开文件用于读取和追加。 如果文件不存在,则创建该文件 |
| "ax+" | 雷同于 'a+',但如果路径存在,则失败 |
| "as" | 打开文件用于追加(在同步模式中)。 如果文件不存在,则创建该文件 |
| "as+" | 打开文件用于读取和追加(在同步模式中)。 如果文件不存在,则创建该文件 |
| "r" | 打开文件用于读取。 如果文件不存在,则会发生异常 |
| "r+" | 打开文件用于读取和写入。 如果文件不存在,则会发生异常 |
| "rs+" | 雷同于 'r+',但如果路径存在,则失败 |
| "w" | 打开文件用于写入。 如果文件不存在则创建文件,如果文件存在则截断文件 |
| "wx" | 雷同于 'w',但如果路径存在,则失败 |
| "w+" | 打开文件用于读取和写入。 如果文件不存在则创建文件,如果文件存在则截断文件 |
| "wx+" | 雷同于 'w+',但如果路径存在,则失败 |
这个函数重点是返回的效果。什么?看不起?好,那么我们尝试读取文件的一段
我们先看一下结构
- close()
关闭这个文件fd
- fd
文件fd(file description)
- read(buffer, buf_offset, read_len, pos)
- buffer 传入一个Buffer用于缓冲。当读取完毕时,这个Buffer里有我们想要的数据
- buf_offset 这个Buffer开始添补的位置。可以用这个实现一个Buffer读取指定大小的内容
- read_len 读取长度,但是如果超出了Nginx的Buffer大小,这个数值相对于现实读取的大小会偏大
- pos 这个是我们今天的重头戏
想要知道如何seek吗?不行,必须使用pos
如果设定为数字,将seek到谁人地方并开始读取
如果设定为null,不改变文件指针位置,从当前位置开始读取
是不是很反人类?
- 最后返回NjsFsBytesRead,其中有两个元素
- bytesRead,读取的长度
- buffer,就是你传入的buffer
- stat()
等同于fs.promises.stat()的效果
- [A] write(buffer, buf_offset, read_len, pos)
- buffer 老规矩,写入的数据Buffer
- buf_offset 这个Buffer开始读取的位置
- read_len 从这个Buffer读取用于写入长度,但是如果超出了Nginx的Buffer大小,这个数值相对于现实读取的大小会偏大
- pos 和上面read()的pos参数同等
- 最后返回NjsFsBytesWritten,其中有两个元素
- bytesWritten,写入的长度
- buffer,就是你传入的buffer
- write(string, pos, encoding)
- write()也可以写入字符串
- string 等待写入的字符串
- pos 和上面read()的pos参数同等
- encoding 编码格式,可选 utf8 hex base64 base64url
这是TypeScript定义- interface NjsFsFileHandle {
- close(): Promise<void>;
- fd: number;
- read(buffer: NjsBuffer, offset: number, length: number, position: number | null): Promise<NjsFsBytesRead>;
- stat(): Promise<NjsStats>;
- write(buffer: NjsBuffer, offset: number, length?: number, position?: number | null): Promise<NjsFsBytesWritten>;
- write(buffer: string, position?: number | null, encoding?: FileEncoding): Promise<NjsFsBytesWritten>;
- }
复制代码 关于使用,可以见 https://github.com/imzlh/vlist-njs/blob/master/main.ts#L130,实现纯粹文件拷贝- 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);
- }
复制代码 readdir():扫描文件夹
虽然我们建议返回填满string的数组,但是返回添补了Buffer的数组也不是不行
- path 路径,同样可以是Buffer
- option Object对象
- encoding 编码格式,可选 utf8(返回字符串) buffer(返回Buffer)
- withFileTypes 自带stat文件范例的扫描,指定为true,返回的就是NjsDirent[]了
- isBlockDevice()
- isCharacterDevice()
- isDirectory()
- isFIFO()
- isFile()
- isSocket()
- isSymbolicLink()
- name 文件(夹)名
- 返回值由option决定,如果什么都没指定,返回字符串数组
realpath(): 相对路径转绝对路径
- path 路径,同样可以是Buffer
- option Object对象
- encoding 编码格式,可选 utf8(返回字符串) buffer(返回Buffer)
- 返回值由option决定,如果什么都没指定,返回字符串
rename(): 移动文件
注意跨文件系统(磁盘)移动不能使用rename(),instead,请拷贝后再删除
实用技巧 什么?你告诉我你不会判定是否跨文件系统(磁盘)?stat()啊- 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('...');
- }
复制代码 实例参考:https://github.com/imzlh/vlist-njs/blob/master/main.ts#L622
- from 路径,除了string同样可以是Buffer
- to 路径,同理,除了string同样可以是Buffer
- 没有返回值,注意catch错误情况
unlink() 删除文件
- unlink(path: PathLike): Promise<void>;
复制代码
- path 路径,同样可以是Buffer
- 没有返回值
rmdir() 删除文件夹
- rmdir(path: PathLike, options?: { recursive?: boolean; }): Promise<void>;
复制代码
- path 路径,同样可以是Buffer
- options
- recursive 递归删除,相称于大名鼎鼎的rm -r
建议体验这个命令,你就知道什么是递归删除了: rm -rf /
- 没有返回值
stat() 获取文件(夹)状态
- stat(path: PathLike, options?: { throwIfNoEntry?: boolean; }): Promise<NjsStats>;
复制代码
- path 路径,同样可以是Buffer
- options Object对象
- throwIfNoEntry
如果设置为true,文件不存在时直接报错,否侧返回 undefined
- 返回NjsStat
- isBlockDevice()
- isCharacterDevice()
- isDirectory()
- isFIFO()
- isFile()
- isSocket()
- isSymbolicLink()
- dev: number 处于的文件系统ID
- ino: number inode数目
- mode: number 文件模式,8进制
- nlink: number 这个文件现实地址硬链接数目,即引用数
- uid: number 全部者User ID
- gid: number 全部者Group ID
- rdev: number 这个文件代表文件系统时表示此文件代表的文件系统ID
- size: number 文件大小
- blksize: number
- blocks: number
- atimeMs: number 最后访问时间戳
- mtimeMs: number 最后修改文件修饰(模式)时间戳
- ctimeMs: number 最后修改时间戳
- birthtimeMs: number 创建时间
- atime: Date;
- mtime: Date;
- ctime: Date;
- birthtime: Date;
symlink() 创建 软 链接
- symlink(target: PathLike, path: PathLike): Promise<void>;
复制代码
- target 目的(要创建软连接的)文件路径,同样可以是Buffer
- path 新建的软连接的路径,同样可以是Buffer
- 没有返回值
writeFile和readFile 偷懒读/写文件的好方法
- readFile(path: Buffer|string): Promise<Buffer>;
- readFile(path: Buffer|string, options?: {
- flag?: "a" | "ax" | "a+" | "ax+" | "as" | "as+" | "r" | "r+" | "rs+" | "w" | "wx" | "w+" | "wx+"
- }): Promise<Buffer>;
- 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<string>;
- 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<void>;
复制代码 不多作介绍,看定义就行
哀求(request)
哀求,就是传入主函数的一个参数,函数由export导出和js_import导入以供nginx调用
这个是函数定义(main.js)- async main(h:NginxHTTPRequest):any;
复制代码 这个是导出(main.js)这个是导入(nginx http)- js_import SCRIPT from 'main.js';
复制代码 这个是使用(nginx location)- location /@api/{
- js_content SCRIPT.main;
- }
复制代码 如许,每当哀求/@api/时,main()就会被调用,全部Promise完成时VM会被回收
这里讲4个很常用的技巧
args GET参数
h.args 是一个数组,官方是这么说的
Since 0.7.6, duplicate keys are returned as an array, keys are
case-sensitive, both keys and values are percent-decoded.
For example, the query string
a=1&b=%32&A=3&b=4&B=two%20words
is converted to r.args as:
{a: "1", b: ["2", "4"], A: "3", B: "two words"}
args会自动解码分割,答应重复且重复的会变成一个Array。
这里就很重要了,每一个哀求你都需要检查你需要的arg是不是Array或string而不能以为只要不是undefined就是string,下面的代码就是最好的反例- if(typeof h.args.action != 'string')
- return h.return(400,'invaild request: Action should be defined');
复制代码 当哀求/@api/?action=a&action=b时,这个函数会错误报错,事实上Action已经定义
headersIO
h.headersIn和h.headersOut是Nginx分割好的Header,你可以直接使用
但是这两个常量有很大的限制,必须是Nginx内部专门定义的Header才会出现
其中,headersIn的定义是如许的- 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;
复制代码 这个是headersOut- '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[];
复制代码 其中最需要注意的是h.headersOut['Set-Cookie']是一个数组
当然,大部分情况下这些Header足够你玩了,但是有的时候还是需要自定义的,这个时候raw开头的变量上场了- readonly rawHeadersIn: Array<[string, string|undefined]>;
- readonly rawHeadersOut: Array<[string, string|undefined]>;
复制代码 这些都是按照数组 [key, value] 排的,你可以用下面的代码快速找到你想要的- const headers = {} as Record<string, Array<string>>;
- h.rawHeadersIn.forEach(item => item[0] in headers ? headers[item[0]].push(item[1]) : headers[item[0]] = [item[1]])
- h['X-user-defined'][0]; // 你想要的
复制代码 如果是自定义输出的话,第一个想到的是不是应该也是h.rawHeadersOut?
然而,我发现 官方的示例 中用的不是rawHeadersOut而是headersOut
简直,我在rawHeadersOut这些东西的定义下面都发现了- [prop: string]: string | string[] | undefined;
复制代码 这个让rawHeaders系列更加意味不明了,我也不清楚官方的做法
总之用 headersOut 准没错
用这些函数响应客户端
这个函数发送的是整个哀求,调用后这个哀求就结束了- return(status: number, body?: NjsStringOrBuffer): void;
复制代码 这三个函数是用来搭配响应的,但是我不清楚 官方的用意。
嘛,大部分时间还是别这么玩吧- sendHeader(): void;
- send(part: NjsStringOrBuffer): void;
- finish(): void;
复制代码 NGINX的特色
- internalRedirect(uri: NjsStringOrBuffer): void;
- parent?: NginxHTTPRequest;
- subrequest(uri: NjsStringOrBuffer, options: NginxSubrequestOptions & { detached: true }): void;
- subrequest(uri: NjsStringOrBuffer, options?: NginxSubrequestOptions | string): Promise<NginxHTTPRequest>;
- subrequest(uri: NjsStringOrBuffer, options: NginxSubrequestOptions & { detached?: false } | string,
- callback:(reply:NginxHTTPRequest) => void): void;
- subrequest(uri: NjsStringOrBuffer, callback:(reply:NginxHTTPRequest) => void): void;
复制代码 是不是很心动?简直,你可以使用subrequest分割任务,internalRedirect快速服务文件,parent在子哀求内直接操纵响应
举个例子,你验证完Token想要发送给客户端一个文件
nginx.conf:- location /@files/{
- internal;
- alias /file/;
- }
复制代码 file.js- // ....
- h.internalRedirect('/@files/' + file_path);
- // 这个时候客户端就接收到了`/files/{file_path}`这个文件
复制代码 Buffer系列
请注意这一句话
*** if it has not been written to a temporary file.
详情请参看我的这篇踩坑文章 https://hi.imzlh.top/2024/07/09.cgi
总之,这是Nginx的Buffer,而客户端的上传如果大于client_body_buffer_size会被写入文件并暴露在变量中 h.variables.request_body_file- readonly requestBuffer?: Buffer;
- readonly requestText?: string;
复制代码 需要注意的是,下面的两项是subrequest返回的内容而不会写入客户端Buffer
想要给客户端则需要如许: r.return(res.status, res.responseText)
这个是Nginx官方的例子- readonly responseBuffer?: Buffer;
- readonly responseText?: string;
复制代码 输出到日志的函数
- error(message: NjsStringOrBuffer): void;
- log(message: NjsStringOrBuffer): void;
- warn(message: NjsStringOrBuffer): void;
复制代码 这些很好理解,就是log warn error三个等级的日志
这些函数不要碰
这些函数是js_body_filter才气使用的,对于新手像我一样找不到为什么出错的很致命- sendBuffer(data: NjsStringOrBuffer, options?: NginxHTTPSendBufferOptions): void;
- done(): void;
复制代码 其他你感兴趣的
- httpVersion: string HTTP版本号
- method: string HTTP方法,是大写的
- remoteAddress: string 客户端地址
- uri: string 哀求的URL,在subrequest则是subrequest的URL
- variables: NginxVariables Nginx变量,是UTF8字符串
- rawVariables: NginxRawVariables Nginx变量,差别的是值是Buffer
全局命名空间
njs
NJS有一个全局命名空间njs.*,这里面的东西全局可用不分场所
- version: string njs版本
- version_number: number njs版本,字符串版本
- on(event: "exit", callback: () => void): void VM退出时的回调
- dump(value: any, indent?: number): string pre打印,输出到日志
ngx
还有一个命名空间叫做ngx.*,这里面的东西与nginx相关
东西太多,我就介绍最重要的
- fetch(init: NjsStringOrBuffer | Request, options?: NgxFetchOptions): Promise
和Web很像的fetchAPI,只是第二个参数大缩水了
- body?: string
- headers?: NgxHeaders
- method?: string
- verify?: boolean 是否验证SSL证书,默认验证,不符合会报错
- log(level: number, message: NjsStringOrBuffer): void
写入到Nginx日志,level可以是这些
- ngx.INFO
- ngx.WARN
- ngx.ERR
- readonly shared: NgxGlobalShared
共享池,这个很有用,重点介绍下
当多个VM需要共享一个数据时,我们第一个想到的办理方法时数据库(DataBase)
但是njs如今不支持数据库,作为过渡,这个shared就是办理方法
通过共享池,共享同样的数据,再使用共享锁就可以实现了
其中共享池名称 大小 范例由js_shared_dict_zone定义
这些是可利用的全部函数
- ngx.shared.[共享池名称].add()
- ngx.shared.[共享池名称].capacity 共享池大小
- ngx.shared.[共享池名称].clear()
- ngx.shared.[共享池名称].delete()
- ngx.shared.[共享池名称].freeSpace()
- ngx.shared.[共享池名称].get()
- ngx.shared.[共享池名称].has()
- ngx.shared.[共享池名称].incr() 增大一个键对应的值的大小
- ngx.shared.[共享池名称].items()
- ngx.shared.[共享池名称].keys()
- ngx.shared.[共享池名称].name
- ngx.shared.[共享池名称].pop()
- ngx.shared.[共享池名称].replace()
- ngx.shared.[共享池名称].set()
- ngx.shared.[共享池名称].size() 这个共享池元素的数目
- ngx.shared.[共享池名称].type 范例string或number,由js_shared_dict_zone定义
- worker_id 工作进程的ID,对于定时任务指定很有效
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |