标签归档:node.js

[node.js]在redis上使用数据搜索

reids本身是一个基于键值对数据存储的内存数据库,也就是只能通过数据的key来获取数据项目,那么它自然也就没有任何数据搜索方面的功能,只能依靠一定规则生成的key来获取数据。

虽然它的本体是这样,但redis也提供了几个模块为其添加了一部分搜索功能的支持,并将这些模块整合为了redis-stack,官网介绍:https://redis.io/docs/stack/,我暂时还没测试它是否可以直接替代原本的redis实例,但就命令形式上来看,应该是兼容的。

本文是对于redis-stack官网上提供的node.js示例记录的笔记。

安装

redis-stack的安装我就不介绍了,直接从官网下载就好,或者使用docker之类的,本文只记录如何使用。

要在node.js中使用redis-stack的相关特性,我们需要redis-om模块,使用npm i redis-om安装进需要它的项目,关于该模块更详细的API介绍可以去其npm包页面上或者github仓库查看:https://github.com/redis/redis-om-node,不过要注意的是官方的示例中有些地方是错的,我会在下面对应的部分说明。

初始化连接和数据结构

本文假设你已经开启了顶层await,如果没有的话把下面所有的代码包进一个async函数里执行即可。

首先从中取出几个目前需要的类 本文来自luojia.me

const { Client ,Entity, Schema } = require('redis-om');

接着创建一个客户端对象

const client = await new Client().open('redis://[[用户名]:密码]@服务地址:端口');

创建一个class,用于指代一类数据,并为它定义数据项目和类型

class Poi extends Entity {} //注意,该类的类名会成为redis中键的一部分,所以尽量不要起太长的名字
//是的没错,这个类只需要这样一行就完成创建了。
//你也可以自己在里面添加getter来根据redis返回的条目生成数据。

//json数据结构定义,由于此数据将以json文本形式存储在redis中,因此也建议不要给属性起太长的名字,在业务许可的范围内越短越好
const schema = new Schema(Poi , {
	time: { type: 'date' }, //定义为日期,在实际json中将以时间戳形式存储
	num: { type: 'number',sortable:true },//定义为数字,并将其定义为可排序的
	msg: { type: 'string' },//定义为普通字符串
	types: { type: 'string[]' },//定义为字符串数组
	value: { type: 'text' },//定义为可执行全文索引的字符串
	isPoi: { type: 'boolean' }, //定义为布尔型数据
	pos: { type: 'point' }, //定义为坐标类型
});

以上就是目前redis-stack支持进行搜索的所有类型了,这里放个链接,关于对数据的附加定义和后续如果有任何更新可以看这个区块:https://github.com/redis/redis-om-node#-define-an-entity-and-a-schema本文来自luojia.me

接下来我们需要创建一个抽象的数据仓库对象,该对象就是主要用于进行搜索和操作数据的对象。

const repo = client.fetchRepository(schema);

//紧接着我们可以执行一次创建索引的指令,如果不存在索引,则进行搜索时会报错
await repo.createIndex();
//不用担心重复执行该指令会有副作用,当有效的索引存在时它不会进行任何操作
//如果你想要手动清除索引,可以执行 await repo.dropIndex()

到此为止对于该数据库的初始化工作就完成了,接下来就是数据的创建和搜索部分。

创建数据

这个包的作者非常热衷于给同一个功能创建很多使用方法,在创建数据方面,我们可以有以下两种流程 本文来自luojia.me

1.首先创建数据条目,然后保存它

const entity= repo.createEntity(); //创建条目
entity.entityId // 可以通过该属性获得该条目随机生成的id,比如 '01FJYWEYRHYFT8YTEGQBABJ43J'
entity.time=new Date();
entity.num=123;
entity.pos={ longitude: -81.6764187, latitude: 41.5080462 };

const id=await repo.save(entity);//保存到redis中

在数据类型中已经定义了属性,但是在这里没有赋值的,会变成null。

2.直接创建并保存条目

const entity=await repo.createAndSave({
	time: new Date(),
	num: 123,
	pos: { longitude: -81.6764187, latitude: 41.5080462 },
});
entity.entityId  //可以通过该属性获得该条目随机生成的id

数据保存完成后在redis中的形式默认是这样的,键名中的冒号:是分隔符,可以自己定义为其它字符,本文不做介绍

  • 键: 数据类名:条目id,在本例中为Poi:01FJYWEYRHYFT8YTEGQBABJ43J
  • 值: JSON字符串

如果条目中所有值都是null,那么保存该条目会使其直接被删除。 本文来自luojia.me

数据操作

有了数据之后我觉得基本的数据操作也需要简单介绍一下,因为和原本的redis包操作方式不太一样。

  • 通过id获取数据: await repo.fetch(条目id); 使用fetch获取数据时,如果id对应的数据不存在,则会返回null
  • 通过id删除数据: await repo.remove(条目id)
  • 为数据设置过期时间: await repo.expire(条目id,秒数)

数据搜索

接下来就是重头戏了,使用搜索功能获取我们需要的所有数据,而不是通过固定的id获取单个数据。官网的说明链接在这里:https://github.com/redis/redis-om-node#-using-redisearch,如前面所说,作者非常喜欢给一个功能定义好几种用法,但我在这里只写最简短的一种,我觉得其它写法都又臭又长,喜欢别的写法的就自己去看官方文档。

搜索始于repo.search(),然后在后面接上条件语句,在官方readme中有一部分的search不是作为方法调用的,而是直接当属性获取其where,亲测是错误的。

下面我尽量在简短的示例代码中包含大部分要点,并在注释里写上作用 本文来自luojia.me

const results=await repo.search()	//该语句还没结束,为了方便注释,我把不同的部分分到了不同行里
	.where('num').eq(123)			//搜索num等于123的数据条目
	.and('msg').not.eq('poi')		//使用'与'条件搜索msg不等于poi的条目
	.or('isPoi').true()				//使用'或'条件搜索isPoi为true的条目
	.or(search=>search.where('value').match('poi').and('num').gt(1))
	//↑使用'或'条件且嵌套子条件,该子条件里需要满足对value字段全文搜索匹配poi并且num大于1
	.return.all()

这里再单独列一下不同类型可以使用的搜索方法

String 字符串
  • 等于 : eq(str)
  • 不等于 : not.eq(str)
Number 数字
  • 等于 : eq(x)
  • 大于 : gt(x)
  • 大于等于 : gte(x)
  • 小于 : lt(x)
  • 小于等于 : lte(x)
  • 在a和b之间 : between(a,b) //包含首尾
  • 在上面几个方法前加上not.就是相反条件
Boolean 布尔型
  • 为true : true()  //讲道理像这样用关键词做函数名真的不可取
  • 为false : false()
  • 也可以用eq(true或false)判断是否为true或false
  • 同样,以上方法前面加上not.就是相反条件
Date 日期
  • 判断是否在某一天中 : on(x)eq(x)   //x可为Date对象、符合ISO 8601的字符串或以秒为单位的unix时间戳,以下相同
  • 在时间x之前 : before(x)lt(x)
  • 在时间x之后 : after(x)gt(x)
  • 在时间x当天或之前 : onOrBefore(x)lte(x)
  • 在时间x当天或之后 : onOrAfter(x)gte(x)
  • 在时间a和b之间 : between(a,b)
  • 同样,以上方法前面加上not.就是相反条件 本文来自luojia.me

注意,官方文档上写的是Date类型由于存储为数字,所以也可以使用数字的搜索方法,因此上面所有带on的方法意思就产生了歧义,我暂时还没测试它到底是只要在当天就可以匹配还是必须要完全相同才可以匹配。

String[] 字符串数组 本文来自luojia.me
  • 数组中包含指定字符串 : contain(str)
  • 数组中包含指定字符串中的一个 : containOneOf(a,b,c, ... )
  • 以上方法前面加上not.就是相反条件
Text 全文索引
  • 匹配内容 : match(str)
  • 精确匹配 : matchExact(str)  //此模式下单词之间的空格不会被认为是关键词分隔符,而是作为关键词的一部分一起搜索
  • 以上方法前面加上not.就是相反条件
在match中可以使用*符号进行词后模糊查询,比如repo.search().where('value').match('poi*'),注意该符号只能放在词后,不能在词前,也不能在matchExact里使用,出现这些用法都将报错。
Point 坐标

坐标搜索需要使用嵌套搜索,嵌套在inRadius方法内,首先写个示例

await repo.search().where('pos').inRadius(
	circle => circle.origin(-81.7758995, 41.4976393).radius(50).meter
)

这里使用origin(经度,纬度)定义搜索中心,使用radius(x)定义搜索半径,后面可以跟以下属性作为单位。

  • mile
  • foot
  • kilometer
  • meter

作者定义了3种写法,包括复数、简写,为了语义明确,我这里只列了单数形式 本文来自luojia.me

如果没有指定搜索中心,默认为(0,0),如果未指定单位,默认为meter(米)。

到这里针对不同类型数据的搜索方法就全部讲完了。

排序

对于我们的搜索结果,可以对其进行预排序再返回,只需要在前面的搜索语句后面加上.sortBy(字段,‘asc'或'desc')即可,asc为从小到大排序,desc为从大到小排序,对于排序方向还有两个简写函数.sortAscending(字段名).sortDescending(字段名)

redis-om有两种存储模式,一个是json,一个是hash,这两种模式下可以排序的数据类型是不同的,详情看这里,我就不多做解释了。

获取结果

搜索写完了就该获取搜索结果了,方法很简单,就是在上面搜索的语句后面加上.return.all(),该方法会返回所有满足条件的结果,完整示例

const results=await repo.search().where('num').eq(123).return.all();

results会成为一个结果数组,存放着所有结果对象。 本文来自luojia.me

有时我们不需要返回所有结果,比如只要获取结果中的第一行可以这么写 : .return.first(),这时候results就不是数组了,而是一个单独的条目对象;

或者我们需要返回结果中的一部分,可以用分页方法 : .return.page(偏移量,数量)

又或者我们同时还想获得结果的总数量 :  .return.count(),这时候results也不是数组,返回的是一个数字;

如果主要结果里的最小值,可以直接使用  .return.min() ,这个是我在源码里翻到的,readme中没有列举,还有maxminIdminKeymaxIdmaxKeypageOfIdspageOfKeysfirstId firstKeyallIdsallKeys

另外经测试,search后面不一定非要跟where,也可以直接写and、or之类的方法,因此那些由程序生成的条件就不需要处理这个例外去凑一个where了,可以直接调用and或or。

[SASS]指定import查找路径

本文为node包sass的使用笔记。

写这篇文章,是因为我碰到了引用node_modules目录中模块的scss文件的需求,但是它总是提示找不到: “Error: Can’t find stylesheet to import.”,于是我搜了搜,发现说是要在渲染参数中添加loadPaths数组来告诉sass去哪里找依赖文件,我在这个模块的typescript类型定义中也找到了这个参数名,但是我加上了之后并没有用,于是研究一番发现这个包使用的实际上并不是loadPaths,而是includePaths这个参数名,所以觉得有必要写篇笔记记录一下。

另外我发现有的解决方案中说可以在import的url前面加个~来表示这个文件在node_modules里,但我试了也没有用,这个特性似乎是webpack给sass引入的importer,所以单独使用sass的render api时此方式是不起作用的,如果使用的是sass-loader的话可以使用这个方法。

[Node.js]ES模块和CommonJS模块的导入

在使用ES导入语法的文件中想要使用require导入一个CommonJS模块的话,可以使用module.createRequire来创建一个require函数。

import { createRequire } from 'module';
const require = createRequire(import.meta.url);

 

在一般的使用CommonJS导入语法的文件中想要使用ES语法导入模块的话,可以使用import函数,要注意的是这是一个异步函数,返回一个Promise,所以需要await它或者使用then来等待返回结果,如下

(async()=>{
	const fs=await import('fs);
})();

 

[Browserify]异步插件导致write after end错误

写了个Browserify插件,之前一直没问题,但今天我改了点打包流程,结果一直报write after end错误,找了半个晚上,发现是因为我的插件函数写的是异步的,里面有个await异步读文件。

之前一直没出问题是因为后面的打包流程时间够长,能让这个插件正常执行完,但今天改了流程之后部分情况下很快就执行完了,于是当插件异步流程执行完后继续就出错了,修正方法是把异步文件读取改成同步的。

[Node.js]进程异常退出,错误码 3221226505

不是我想水一篇博文,在解决完前一篇博文的问题之后,立刻就又碰上另一个奇葩问题,进程直接跑着跑着没了,留下一个错误码3221226505。

在我左思右想把代码改来改去,包换来换去之后依然不能解决问题,然后发现是canvas包的问题,之所以一直没有发现是它,是因为出问题的代码里并没有用到这个包,它是在别的地方被引用到的,可能它的二进制模块破坏了node的环境,导致别的代码执行的时候触发了异常导致程序崩溃。

这个问题其实也不是第一次出现了,而且每次都是因为项目中同时有canvas和sharp两个模块才出现问题,在某些版本下不会出问题,某些版本下又会出现不一样的错误,这次就是和上次不一样所以才没能快速发现。

就这点破事,又浪费了我一个晚上,这时候我真想给搞出这bug的人屁股上来一脚。

[Node.js]解决‘gbk’ codec can’t decode byte 0x80 in position

每次换一个环境执行npm i都会碰到这样的问题,记录一下解决方法,

方法1:手动去报错的文件里指定编码为UTF-8,我自己第一次解决这个问题就是用的这个办法,刚刚想找找看有没有别的解决方法的时候发现别人也有去手动改源码的,放个参考链接

简单地说就是找到报错的那行,给open函数加个encoding='utf-8' 参数就可以解决问题,但这样属于改了人家文件,更新后还是会变回去的,所以现在我不这样做了。

方法2:去控制面板的区域设置里到“管理”标签页下,“更改系统区域设置”,把“Beta版:使用Unicode UTF-8提供全球语言支持(U)”勾上,然后重启一下,python就会默认用utf-8来读文件了。

这个方法会改变整个系统的默认代码页,最主要的影响是会导致使用非Unicode的程序乱码甚至崩溃,还有在gbk设置时以ansi保存的含有中文的bat脚本会乱码。如果影响不大的话倒是可以让系统一直保持在这个区域选项上,影响某些软件的正常使用的话在执行完需要的编译任务后还得改回去。在这种模式下可以使用“locale emulator”这样的软件以Chinese模式启动程序,以解决程序的乱码和崩溃问题。

方法3:我没试过,理论上应该可行,从python本体入手,把它的默认编码设置成utf-8,参考这里的方案2。这个没用

[Node.js]file-namer 文件重命名工具

昨晚写了个重命名工具,使用正则表达式匹配并替换文件名。

现在给npm包起个合适的名字是真难

GitHub: https://github.com/JiaJiaJiang/node-namer

效果

$ namer -f "/.+(?=\.txt$)/" -r "#COUNTERpoi$&"
match: /.+(?=\.txt$)/   find: /.+(?=\.txt$)/   replacement: #COUNTERpoi$&
Match list:
1.txt   >       1poi1.txt
2.txt   >       2poi2.txt
3.txt   >       3poi3.txt
4.txt   >       4poi4.txt
5.txt   >       5poi5.txt
6.txt   >       6poi6.txt
7.txt   >       7poi7.txt
8.txt   >       8poi8.txt
9.txt   >       9poi9.txt
10.txt  >       10poi10.txt
11.txt  >       11poi11.txt
12.txt  >       12poi12.txt
13.txt  >       13poi13.txt
14.txt  >       14poi14.txt
15.txt  >       15poi15.txt
16.txt  >       16poi16.txt
17.txt  >       17poi17.txt
18.txt  >       18poi18.txt
19.txt  >       19poi19.txt
20.txt  >       20poi20.txt
21.txt  >       21poi21.txt
22.txt  >       22poi22.txt
23.txt  >       23poi23.txt
24.txt  >       24poi24.txt
25.txt  >       25poi25.txt
26.txt  >       26poi26.txt
27.txt  >       27poi27.txt
28.txt  >       28poi28.txt
29.txt  >       29poi29.txt
30.txt  >       30poi30.txt
31.txt  >       31poi31.txt
32.txt  >       32poi32.txt
33.txt  >       33poi33.txt
34.txt  >       34poi34.txt
35.txt  >       35poi35.txt
36.txt  >       36poi36.txt


36matches found.
Confirm?    (control+c to exit)

Finished. 36succeeded,0failed

 

[Node.js]拦截process.stdout和process.stderr

由于解决这个问题花了我一些时间,所以记录一下说不定可以帮到其他人。

process.stdout是一个getter,所以我们不能用普通的替换来换掉process.stdout来拦截写入它的数据。同时,process.stdout是一个Writable Stream,所以也不能简单地直接从它里面获取写入的数据。

 

一开始我花了不少时间来研究怎么可以从这个Writable Stream里读出数据,但是这似乎太麻烦了,然后我甚至想到了利用child process来拦截数据。最后发现其实很简单,我们只要重新定义这个Getter就可以了。

var stream=require('stream');


var rawStdout=process.stdout,//先拿到原来的stdout
	newStdout=new stream.PassThrough();//创建一个passthrough流,这是一种特殊的Transform流,会直接把写入的数据吐出来
process.__defineGetter__('stdout',function(){//重新定义process.stdout的Getter
	return newStdout;//返回我们的passthrough流
});

这样我们就成功拦截到标准输出了,要注意,这段代码必须放在有任何输出之前,一旦在之前有了内容输出,它就没用了。

然后我们就可以自己决定怎么处理stdout了比如:

newStdout.pipe(rawStdout);//内容输出到控制台

newStdout.pipe(文件的writable stream);//内容写入文件

newStdout.pipe(其它可写流);//随你怎么处理

 

 

同理,process.stderr也可以这样拦截

[Node.js]防止node自动退出

node执行完所有代码以后就会退出(部分监听服务除外),如果不希望node立刻退出,只要在任意位置加上一行

setInterval(function(){},9999999);//时间设置多少都没有关系,这只影响这个空回调函数的调用频率

node就不会退出了(除非出错

如果你希望即使是出错了,也不让Node退出,那么就需要加入以下事件监听器

process.on('uncaughtException',(err,origin)=>{//捕捉uncaughtException
	console.error('[uncaughtException]',err,origin);
});

process.on('unhandledRejection',(err,promise)=>{//捕捉unhandledRejection
	console.error('[unhandledRejection]',err);
});

加入这两个监听器之后,node遇到任何未捕捉的错误,都会交给第一个监听器处理,碰到任何未catch的异常Promise,都会交给第二个监听器处理,你可以在里面做些事情,比如打个日志之类的。

==========更新log===========

  • 2021/11/16: 加入出错时不退出的方法

node被抛弃,iojs取而代之

从node分离出来农民起义的iojs终于取得了革命胜利。现在node的版本一下从0.12.x飞升到了4.0.0。

Node.js ChangeLog
2015-09-08, Version 4.0.0 (Stable), @rvagg

Notable changes

This list of changes is relative to the last io.js v3.x branch release, v3.3.0. Please see the list of notable changes in the v3.x, v2.x and v1.x releases for a more complete list of changes from 0.12.x. Note, that some changes in the v3.x series as well as major breaking changes in this release constitute changes required for full convergence of the Node.js and io.js projects.

child_process: ChildProcess.prototype.send() and process.send() operate asynchronously across all platforms so an optional callback parameter has been introduced that will be invoked once the message has been sent, i.e. .send(message[, sendHandle][, callback]) (Ben Noordhuis) #2620.
node: Rename "io.js" code to "Node.js" (cjihrig) #2367.
node-gyp: This release bundles an updated version of node-gyp that works with all versions of Node.js and io.js including nightly and release candidate builds. From io.js v3 and Node.js v4 onward, it will only download a headers tarball when building addons rather than the entire source. (Rod Vagg) #2700
npm: Upgrade to version 2.14.2 from 2.13.3, includes a security update, see https://github.com/npm/npm/releases/tag/v2.14.2 for more details, (Kat Marchán) #2696.
timers: Improved timer performance from porting the 0.12 implementation, plus minor fixes (Jeremiah Senkpiel) #2540, (Julien Gilli) nodejs/node-v0.x-archive#8751 nodejs/node-v0.x-archive#8905
util: The util.is*() functions have been deprecated, beginning with deprecation warnings in the documentation for this release, users are encouraged to seek more robust alternatives in the npm registry, (Sakthipriyan Vairamani) #2447.
v8: Upgrade to version 4.5.103.30 from 4.4.63.30 (Ali Ijaz Sheikh) #2632.
Implement new TypedArray prototype methods: copyWithin(), every(), fill(), filter(), find(), findIndex(), forEach(), indexOf(), join(), lastIndexOf(), map(), reduce(), reduceRight(), reverse(), slice(), some(), sort(). See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray for further information.
Implement new TypedArray.from() and TypedArray.of() functions. See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray for further information.
Implement arrow functions, see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions for further information.
Full ChangeLog available at https://github.com/v8/v8-git-mirror/blob/4.5.103/ChangeLog

以上是搬过来的changeLog,同时在changeLog页面往下翻一番,全都是iojs之前的版本号,很明显node原本的项目是直接被iojs项目覆盖掉了,注意这一行
node: Rename "io.js" code to "Node.js" (cjihrig) #2367.

iojs重命名为nodejs,革命军变成了正规军,推翻了原本的帝国统治。

现在点开iojs.org官网里的changelog也是直接跳转到https://github.com/nodejs/node/blob/master/CHANGELOG.md,也就是node的changelog。

 

这对native mod开发者来说是个福音,不用再去考虑兼容iojs和node携带的两个版本差距悬殊的v8版本了。

Error: Cannot find module ‘npmlog’

手动安装node的时候一不注意就会出现这个错误,装好了之后使用命令npm 测试一下出来了这个错误:Error: Cannot find module 'npmlog' 。

 

这一般是放在/bin里的链接不对导致的。

可能情况1

链接指向的不是node_modules/npm/bin/npm-cli.js ,初次手动安装难免会搞不清要链接哪个文件,把链接指向正确的位置即可。

情况2

使用的不是软链接,由于npm-cli.js中使用的是相对路径,要是你一不小心链了个硬链接,那么npm就会从/bin解析相对路径,所以就找不到npmlog模块了。要使用软链接,在ln命令后加上-s 选项:ln -s node_modules/npm/bin/npm-cli.js /bin/npm

 

解决node无法调用全局模块的问题

刚刚我把SPDY装到全局,npm -g install spdy ,

spdy@2.0.4 /usr/local/lib/node_modules/spdy
├── http-deceiver@1.2.4
├── handle-thing@1.2.4
├── select-hose@2.0.0
├── debug@2.2.0 (ms@0.7.1)
└── spdy-transport@1.1.8 (obuf@1.1.1, wbuf@1.7.0, hpack.js@2.1.4, readable-stream@2.0.2)

可以看到这个模块被装到/usr/local/lib/node_modules里了

 

 

 

然后开个环境测试一下,结果

> require('spdy')
Error: Cannot find module 'spdy'
    at Function.Module._resolveFilename (module.js:336:15)
    at Function.Module._load (module.js:286:25)
    at Module.require (module.js:365:17)
    at require (module.js:384:17)
    at repl:1:1
    at REPLServer.defaultEval (repl.js:154:27)
    at bound (domain.js:254:14)
    at REPLServer.runBound [as eval] (domain.js:267:12)
    at REPLServer.<anonymous> (repl.js:308:12)
    at emitOne (events.js:77:13)

吓得我满脑子都是doge!!!这是为什么?

继续阅读解决node无法调用全局模块的问题

[Node.js]用ws模块创建加密的ws服务(wss)

node的ws模块可以很方便地创建一个单纯的标准websocket服务,但是对于创建wss服务并没有提供一个独立的方法,还是使用创建ws服务的方法,在传入参数对象里加了个自定义的https服务。

 

也不知道是我找错了文档还是官方文档就那么简洁,不过readme文件写的文档的确是这个→doc/ws.md,然后我研究了好几天还翻了遍它的源码和examples才终于理解了这个文档。

继续阅读[Node.js]用ws模块创建加密的ws服务(wss)