原文链接: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
  • [b] 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的数组也不是不行

readdir(path, option)
  • path 路径,同样可以是Buffer
  • option Object对象
    • encoding 编码格式,可选 utf8(返回字符串) buffer(返回Buffer)
    • withFileTypes 自带stat文件类型的扫描,指定为true,返回的就是NjsDirent[]
      • isBlockDevice()
      • isCharacterDevice()
      • isDirectory()
      • isFIFO()
      • isFile()
      • isSocket()
      • isSymbolicLink()
      • name 文件(夹)名
  • 返回值由option决定,如果什么都没指定,返回字符串数组

realpath(): 相对路径转绝对路径

realpath(path, option)
  • 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

rename(from, to)
  • 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)

export { main };

这个是导入(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是不是Arraystring而不能认为只要不是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.headersInh.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<Response>

    和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 类型stringnumber,由js_shared_dict_zone定义
  • worker_id 工作进程的ID,对于定时任务指定很有效

njs最详细的入门手册:Nginx JavaScript Engine的更多相关文章

  1. 史上最详细bitbucket入门手册,手把手操作指南

    老大要我去调研一下有什么好用的免费软件版本管理工具,有利于小团队开发的.我第一个想到的就是git,经常在git下东西,听说它的代码仓库好用,于是就注册了一个github的账号,创建仓库的时候才发现只能 ...

  2. Github快速入门手册

    最近在试用Github,开源的思想也让人觉得把一些经验分享出来是非常好的事情.附件是doc文件,如有需要请注意查收.希望能对你有帮助. GITHUB基于互联网的版本控制快速入门手册 如有不妥,欢迎指正 ...

  3. Node.js 入门手册:那些最流行的 Web 开发框架

    这篇文章与大家分享最流行的 Node.js Web 开发框架.Node 是一个服务器端 JavaScript 解释器,它将改变服务器应该如何工作的概念.它的目标是帮助程序员构建高度可伸缩的应用程序,编 ...

  4. DPDK2.1 linux上开发入门手册

    1引言 本文档主要包含INTEL DPDK安装和配置说明.目的是让用户快速的开发和运行程序.文档描述了如何在不深入细节的情况下在linux应用开发环境上编译和运行一个DPDK应用程序. 1.1文档总览 ...

  5. 【Webpack】320- Webpack4 入门手册(共 18 章)(下)

    介绍 1. 背景 最近和部门老大,一起在研究团队[EFT - 前端新手村]的建设,目的在于:帮助新人快速了解和融入公司团队,帮助零基础新人学习和入门前端开发并且达到公司业务开发水平. 本文也是属于[E ...

  6. 【Webpack】319- Webpack4 入门手册(共 18 章)(上)

    介绍 1. 背景 最近和部门老大,一起在研究团队[EFT - 前端新手村]的建设,目的在于:帮助新人快速了解和融入公司团队,帮助零基础新人学习和入门前端开发并且达到公司业务开发水平. 本文也是属于[E ...

  7. [转帖]Masscan教程和入门手册

    Masscan教程和入门手册 https://www.4hou.com/tools/8251.html 愣娃 安全工具 2017年11月1日发布 收藏 导语:masscan是为了尽可能快地扫描整个互联 ...

  8. TypeScript入门指南(JavaScript的超集)

    TypeScript入门指南(JavaScript的超集)   你是否听过 TypeScript? TypeScript 是 JavaScript 的超集,TypeScript结合了类型检查和静态分析 ...

  9. Step by Step 真正从零开始,TensorFlow详细安装入门图文教程!帮你完成那个最难的从0到1

    摘要: Step by Step 真正从零开始,TensorFlow详细安装入门图文教程!帮你完成那个最难的从0到1 安装遇到问题请文末留言. 悦动智能公众号:aibbtcom AI这个概念好像突然就 ...

  10. IdentityServer4 中文文档 -15- (快速入门)添加 JavaScript 客户端

    IdentityServer4 中文文档 -15- (快速入门)添加 JavaScript 客户端 原文:http://docs.identityserver.io/en/release/quicks ...

随机推荐

  1. .net core 下 DES &MD5加密

    項目中經常會用到加密解密,分享 DES & MD5加密,當然我們建議使用MD5. #region DES encrypt, decrypt public string EncryptDES(s ...

  2. Rainbond 携手 TOPIAM 打造企业级云原生身份管控新体验

    TOPIAM 企业数字身份管控平台, 是一个开源的IDaas/IAM平台.用于管理账号.权限.身份认证.应用访问,帮助整合部署在本地或云端的内部办公系统.业务系统及三方 SaaS 系统的所有身份,实现 ...

  3. 微信小程序设置swiper圆角在ios上失效

    今天在给轮播图添加圆角的时候,发现在安卓机上是有圆角的,但是在苹果手机上圆角却失效了,后来翻阅了文档发现这是个官方的bug 解决方法1 border-radius: 20rpx; /*再设置个tran ...

  4. Java中将jsonArray导出为Excel

        java中使用jxl导出excel时,需指定WritableSheet对象中对应于每个单元格的数据.List类型是一种常用的数据类型,它里面的元素是实体对象,当将它创建为WritableShe ...

  5. vue Ref 动态组件 keeplive

    ref被用来给元素或子组件注册引用信息.引用信息将会注册在父组件的 $refs 对象上.如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素:如果用在子组件上,引用就指向组件实例 # 普通d ...

  6. Chapter1 p1 Output Image

    由于本文章是对TinyRenderer的模仿,所以并不打算引入外部库. 那么我们第一步需要解决的就是图形输出的问题,毕竟,如果连渲染的结果都看不到,那还叫什么Renderer嘛. 由于不引入外部库,所 ...

  7. 任意树遍历,可以使用 goto 跳记号标注的

    先顺序进入到最后一个根的根部,完后扫描同级 同级扫描完用 goto跳代码改层数到倒数地二层 之后操作就是倒着往上搜索的,有难度,但是还是能做到的嘛 用 lisit 好像不需要别的,全用 list 连接 ...

  8. sse 与 编译器自动优化

    direct x 形式的矩阵和向量计算代码在编译的时候是自动汇编为 sse汇编的 何时使用手写sse 指令呢,当你的应用程序需要写一些物理运算时候 可以使用自己编写的sse计算函数来为 3维运算加速 ...

  9. 10位,13位时间戳转为C#.NET格式时间 DateTime

    10位,13位时间戳转为C#.NET格式时间 DateTime - public static DateTime ToDateTime( string timestamp) { var tz = Ti ...

  10. vue2 混入 (mixin) 带来的小惊喜

    最近在review自己写的代码时发现,在很多的地方都用了以下的代码块 1 async initCode() { 2 const resSource = await this.$API.syscode. ...