1.Node.js回调黑洞全解:Async、Promise 和 Generator
2.Vue—关于响应式(二、异步更新队列原理分析)
3.使用Sequelize快速构建PostgreSQL数据的CRUD操作详解
4.Token 刷新并发处理解决方案
5.Axios源码深度剖析 - AJAX新王者
6.React组件设计-仿网易有道翻译主页
Node.js回调黑洞全解:Async、Promise 和 Generator
本文将深入探讨Node.js中的回调问题,特别是Async、Promise和Generator在流控制中的58同城系统源码应用。首先,回调问题,常称为“回调黑洞”或“回调金字塔”,源于异步代码中复杂且嵌套的调用结构,这可能导致代码难以理解和维护。目标是减少嵌套层数,实现模块化和可测试的代码。
例如,假设我们要找出目录中最大的文件,可以分步解决。首先,用嵌套代码实现,虽然直观但可能难以管理。这种代码可以拆分成三个模块:一个读取目录文件的模块,一个处理文件信息的模块,以及一个组合模块。这种模块化方法有利于重用和测试,但手动处理并行任务仍存在挑战。
接下来,我们尝试引入Async模块,它简化了回调管理并处理错误,实现了并行任务的自动化。Promise则提供了错误处理和函数式编程特性,通过Q模块,代码变得更简洁,错误处理更为智能。此外,Generator作为一种新的技术,自Node 0..2版本起可用,它利用yield关键字控制异步操作,使得看似同步的代码编写成为可能,比如通过TJ Holowaychuk的co库。
总结,本文研究了多种流控制方法,如Async、Promise和Generator,以应对回调问题。对于像Generator这样的新技术,如koa框架的潜力,作者表示了兴趣。如果你对本文中的代码示例或更多Generator实例感兴趣,可以参考Github repo,原文首发于StrongLoop的文章。
Vue—关于响应式(二、异步更新队列原理分析)
本节学习要点:Event Loop、Promise
关于Event Loop的介绍,可以参考阮一峰老师的文章。
关于Promise,请访问:developer.mozilla.org/z...
上一节介绍了Vue通过Object.defineProperty拦截数据变化的响应式原理,数据变化后会触发notify方法来通知变更。这一节将继续分析,收到通知后Vue会开启一个异步更新队列。
以下是两个问题:
一、异步更新队列
首先看一段代码演示。获取应用源码工具
将上一节的代码拿过来,假设我们现在不仅依赖x,还有y、z,分别将x、y、z输出到页面上。我们现在依赖了x、y、z三个变量,那么我们应该把onXChange函数名改为watch,表示它可以监听变化,而不仅仅是监听一个x的变化。
可以看到这三个值都被打印在页面上。
现在我们对x、y、z的value进行修改。
查看页面,结果没有问题,每个数据的变化都被监听到并且进行了响应。
既然结果是对的,那我们的问题是什么?
这个问题是:每次数据变化都进行了响应,每次都渲染了模板,如果数据变化了一百次、一千次呢?难道要重复渲染一百遍、一千遍吗?
我们都知道频繁操作DOM会影响网页性能,涉及重排和重绘的知识感兴趣请阅读阮一峰老师的文章:ruanyifeng.com/blog/...
因此,既要保证所有的依赖都准确更新,又要保证不能频繁渲染成为了首要问题。现在我们修改x.value、y.value、z.value都是同步通知依赖进行更新的,有没有一种机制可以等到我修改这些值之后再执行更新任务呢?
这个答案是——异步。
异步任务会等到同步任务清空后执行,借助这个特点和我们前面的分析,我们需要:
按照步骤,我们创建如下代码:
接着我们需要修改一下notify的代码,监听到数据变化后不立即调用依赖进行更新,而是将依赖添加到队列中。
回到页面,我们发现页面上还是重复渲染了三次模板。
那么我们写的这段代码有什么用呢?异步又体现在哪里呢?接着往下看。
二、nextTick原理分析
上面的代码中,虽然我们开启了一个队列,并且成功将任务推入队列中进行执行,但本质上还是同步推入和执行的。我们要让它变成异步队列。
于是到了Promise发挥作用的时候了。关于宏任务和微任务的介绍请参考:zhuanlan.zhihu.com/p/...
我们创建nextTick函数,nextTick接收一个回调函数,返回一个状态为fulfilled的Promise,并将回调函数传给then方法。
然后只需要在添加任务时调用nextTick,将执行任务的flushJobs函数传给nextTick即可。
回到页面。
虽然修改了x、y、z三个变量的游资潜伏指标源码value,最后页面上只渲染了一次。
再来总结一下这段代码的执行过程:
这也正是Vue采用的解决方案——异步更新队列,官方文档描述得很清楚。
文档地址:cn.vuejs.org/v2/guide/r...
三、结合Vue源码来看nextTick
在Vue中,我们可以通过两种方式来调用nextTick:
(至于什么时候使用nextTick,如果你不偷懒看了官方文档的话,都能找到答案哈哈)
以下源码节选自vue2.6.版本,这两个API分别在initGlobalAPI函数和renderMixin函数中挂载,它们都引用了nextTick函数。
nextTick源码如下:
在内部,它访问了外部的callbacks,这个callbacks就是前面提到的队列,nextTick一调用就给队列push一个回调函数,然后判断pending(pending的作用就是控制同一时间内只执行一次timerFunc),调用timerFunc(),最后返回了一个Promise(使用过nextTick的应该都知道吧)。
我们来看一下callbacks、pending、timerFunc是如何定义的。
可以看到timerFunc函数只是调用了p.then方法并将flushCallbacks函数推入了微任务队列,而p是一个fulfilled状态的Promise,与我们自己的nextTick功能一致。
这个flushCallbacks函数又干了什么呢?
flushCallbacks中重新将pending置为初始值,复制callbacks队列中的任务后将队列清空,然后依次执行复制的任务,与我们自己的flushJobs函数功能一致。
看完上面的源码,可以总结出Vue是这么做的,又到了小学语文之——提炼中心思想的时候了。
对比一下我们自己写的代码,你学会了吗?
以上演示代码已上传github:github.com/Mr-Jemp/VueS...
后面要学习的内容在这里:
Vue—关于响应式(三、Diff Patch原理分析)
Vue—关于响应式(四、深入学习Vue响应式源码)
本文由博客一文多发平台OpenWrite发布!
使用Sequelize快速构建PostgreSQL数据的CRUD操作详解
之前写过一个专栏《布道API》来介绍API的REST风格及推荐实践,今天开始来构建一个管理系统的API服务,首先需要处理的就是数据存储,本文将结合实际开发总结在NodeJS下使用Sequelize快速构建PostgreSQL数据的CRUD操作。项目源代码:github.com/QuintionTang/pretender-service
SequelizeSequelize是一个基于promise的Node.jsORM工具,它具有强大的事务支持、关联关系、预读和延迟加载、读取复制等功能,支持的数据库包括:PostgreSQL、MySQL、MariaDB、SQLite和MSSQL。
Sequelize类是引用sequlize模块后获取一个顶级对象,通过它来创建sequlize实例,也可以通过该对象来获取模内其它对象的引用,如:Utils工具类、Transaction事务类等。创建实例后,可以通过实例来创建或定义Model(模型)、执行查询、同步数据库结构等操作。
官方网站:docs.sequelizejs.com/
添加和配置在安装模块之前,首先安装开发工具Sequelize-CLI
sudonpminstall-gsequelize-cli接下来在项目目录下安装数据存储相关的模块。
npminstallsequelize--savenpminstallpgpg-hstore--save现在在项目根目录下创建文件.sequelizerc,韶关到湛江源码代码如下:
constpath=require('path');module.exports={ "config":path.resolve('./config','db.json'),"models-path":path.resolve('./models'),'seeders-path':path.resolve('./seeders'),'migrations-path':path.resolve('./migrations')};该文件将告诉Sequelize初始化,以生成config、models到特定目录。接下来,输入命令初始化Sequelize。
sequelizeinit该命令将创建config/db.json,models/index.js,migrations和seeders目录和文件。命令执行完毕之后打开并编辑config/db.json来配置数据库连接信息。
{ "development":{ "username":"dbusername","password":"dbpassword","database":"crayon-admin","host":".0.0.1","dialect":"postgres","options":{ "operatorsAliases":false},"logging":false},"test":{ "username":"dbusername","password":"dbpassword","database":"crayon-admin","host":".0.0.1","dialect":"postgres"},"production":{ "username":"dbusername","password":"dbpassword","database":"crayon-admin","host":".0.0.1","dialect":"postgres"}}目录说明:
migrations:所有迁移文件,通过sequelizedb:migrate创建相应数据表
seeders:种子文件,即初始化需要插入到数据库中的数据,运行sequelizedb:seed:all
创建Models和Migrations使用CLI工具Sequelize-CLI创建administrators表
sequelizemodel:create--nameadministrators--attributesid:integer,add_time:integer,last_login:integer,username:string,email:string,login_ip:string执行后会生成两个文件
/src/migrations/-create-administrators.js:创建数据表脚本,用于数据库初始化。
"usestrict";module.exports={ up:async(queryInterface,Sequelize)=>{ awaitqueryInterface.createTable("administrators",{ id:{ allowNull:false,autoIncrement:true,primaryKey:true,type:Sequelize.INTEGER,},add_time:{ type:Sequelize.INTEGER,},last_login:{ type:Sequelize.INTEGER,},username:{ type:Sequelize.STRING,},password:{ type:Sequelize.STRING,},email:{ type:Sequelize.STRING,},login_ip:{ type:Sequelize.STRING,},});},down:async(queryInterface,Sequelize)=>{ awaitqueryInterface.dropTable("administrators");},};/src/models/administrators.js:生成的model文件
"usestrict";const{ Model}=require("sequelize");module.exports=(sequelize,DataTypes)=>{ classadministratorsextendsModel{ }administrators.init({ id:{ type:DataTypes.INTEGER,autoIncrement:true,primaryKey:true,},add_time:DataTypes.INTEGER,last_login:DataTypes.INTEGER,username:DataTypes.STRING,password:DataTypes.STRING,email:DataTypes.STRING,login_ip:DataTypes.STRING,},{ sequelize,indexes:[{ unique:true,fields:["id"],},],freezeTableName:true,timestamps:false,//是否自动添加时间戳createAt,updateAtmodelName:"administrators",});returnadministrators;};现在执行命令:
sequelizedb:migrate执行成功后将在连接的数据库中创建数据表:administrators。
创建seedseed用于初始化插入数据,如管理员,在系统运行前需要创建一个默认账号,这些默认账号信息就写在seed文件中。
创建seed命令如下:
sequelizeseed:create--nameadministrator执行成功后将会在seeders文件夹中创建文件,修改代码如下:
npminstallsequelize--savenpminstallpgpg-hstore--save0现在将seed中的数据插入到数据库中,执行一下命令:
npminstallsequelize--savenpminstallpgpg-hstore--save1创建Services创建文件夹services,文件夹中代码封装与model交互的方法,包括所有CRUD(创建,读取,更新和删除)操作,创建administrators.js,实现的逻辑为获取账号信息、更新账号信息,代码如下:
npminstallsequelize--savenpminstallpgpg-hstore--save2创建Controllers上面创建的services文件用于控制器,在控制器文件夹中创建一个名为administrators.js的文件,代码如下:
npminstallsequelize--savenpminstallpgpg-hstore--save3创建Routers在文件夹routers中创建文件administrators.js文件,代码如下:
npminstallsequelize--savenpminstallpgpg-hstore--save4创建入口现在来为服务创建接口,项目根目录下创建文件app.js,代码如下:
npminstallsequelize--savenpminstallpgpg-hstore--save5现在执行命令nodeapp.js启动服务,将看到终端效果如下:
至此,完成一个基本的API登录服务,还有待完善,后续在迭代中完善。文章涉及的代码在GitHub上。
作者:天行无忌Token 刷新并发处理解决方案
处理并发请求导致重复刷新 Token 的问题,是设计刷新 Token 的关键挑战。文章将通过前后端方案来解决此问题,详细内容如下:
后端采用 Redis 缓存策略。在用户首次登录时,将生成的 Token 数据(token 和 createTime)存入 Redis。当 Token 过期时,系统重新生成新的 Token 数据,并更新 Redis 缓存。同时,创建一条过期时间很短(如秒)的 Token 过渡数据。若后续请求发现 Token 已刷新,检查 Redis 中是否存在 Token 过渡数据。存在则放行,确保同一时间的请求均能通过。
前端通过请求拦截解决并发问题。当请求几乎同时发起且 Token 失效时,第一个请求进入 Token 刷新流程,其他请求需等待。前端利用 Angular 的阿奇源码小说请求拦截器和 BehaviorSubject 实现 Token 刷新状态监听。当 Token 刷新成功,放行后续请求进行重试。
前端还利用 Promise 进行请求队列管理。将请求存入队列后,返回一个持续处于 Pending 状态的 Promise,确保所有请求等待刷新完成再执行。刷新 Token 请求完成时,调用 Promise 的 resolve 方法,依次重试队列中的请求。
相关源码和示例在 Github 上有提供,用于实现前后端的刷新 Token 处理方案。
Axios源码深度剖析 - AJAX新王者
Axios 是一个基于 Promise 的 HTTP 请求库,支持浏览器和 Node.js 环境。其源码在 GitHub 上开源,欢迎 fork 使用并提出指正。以下为 Axios 的核心目录结构说明,主要关注在 /lib/ 目录下的文件。
在使用 Axios 时,你可能会遇到多种调用方式,本文将带你深入了解这些方式及其原理。
首先,我们来了解一下 Axios 的基本用法。你可以使用以下几种方式发起请求:
1. `axios(option)`:提供一个配置对象进行调用。
2. `axios(url[, option])`:传入 URL 和配置对象。
3. 对于 GET、DELETE 等方法:`axios[method](url[, option])`。
4. 对于 POST、PUT 等方法:`axios[method](url[, data[, option]])`。
5. 使用默认实例:`axios.request(option)`。
通过以上方式,你可以轻松发起 HTTP 请求。
深入源码分析,你将发现 Axios 的强大之处。通过 `axios.js` 文件的入口,核心在于 `createInstance` 方法,该方法能生成一个指向 `Axios.prototype.request` 的 Function,从而实现多种调用方式。
在 Axios 的核心 `Axios` 类中,`request` 方法是所有功能的中枢,无论是 GET、POST 还是其他方法,最终都通过 `request` 方法实现。
配置项是 Axios 与用户交互的关键,它涵盖了几乎所有功能的配置。配置项从低到高优先级顺序为:默认配置对象、`defaults` 属性、`request` 方法参数。
在使用 Axios 时,配置项是如何生效的?答案在于合并多个配置源,最终得到一个综合配置对象。
此外,Axios 提供了拦截器系统,让你可以控制请求前后的数据处理。每个 Axios 实例都有 `interceptors` 属性,用于管理拦截器,让你实现精细的控制。
核心的 `dispatchRequest` 方法则负责处理请求流程,包括请求适配器、发送请求、数据转换等步骤。最后,通过 Promise,你可以优雅地处理异步请求。
数据转换器让你能轻松地在请求和响应数据之间进行转换,如将对象转换为 JSON 格式。默认情况下,Axios 自动处理 JSON 数据转换。
在使用 Axios 时,你还能灵活地控制超时、取消请求、设置 header、携带 cookie 等功能。通过源码分析,你可以深入理解 Axios 的内部机制。
总结,Axios 以其强大、灵活的功能和简洁的 API 设计,成为现代应用中不可或缺的 HTTP 请求工具。通过本文的深入探讨,你将对 Axios 的运作机制有更深刻的理解,从而更好地利用其功能。
React组件设计-仿网易有道翻译主页
前言
React组件化开发非常有利于搭建项目,也提高了组件的复用性。由于频繁使用网易有道翻译这个软件,让我萌生出想要征服ta的冲动。开发过程中遇到了些许问题,页面还有很多功能还未完善,现在只有一个首页,后续功能持续完善中。
前期准备在组件页面成型之初需要几个开源组件库:
axios:它是一个基于promise的网络请求库,用于获取后端数据(fastmock网站可以让你在没有后端程序的情况下能真实地在线模拟ajax请求),是前端常用的数据请求工具;
antd-mobile:由蚂蚁金融团队推出的一个开源的react组件库,这个组件库拥有很多使用的组件;
swiper:能实现触屏焦点图、触屏Tab切换、触屏轮播图切换等常用效果。
styled-compenonts:真正的cssinjs,增强CSS以对React组件系统进行样式设置的结果,具有简单的动态样式、轻松维护等优点。
正文组件展示?
组件设计思路顶部:用flex布局,方便快捷(一切皆可flex)
搜索栏:使用antd-mobile组件库的SearchBar,点击转跳到搜索页面
图标轮播和轮播图:主要使用swiper进行设计,实现自动轮播效果
底部栏:用fixed固定住
组件封装先对项目进行脚手架的建构(使用vite脚手架,使用起来快速方便)
npminit@viteja/appsrc下的目录内容
api:存放与数据相关的链接,组件所有的数据将会在这一个文件夹下的request.js中使用ajax进行数据请求
assets:存放静态资源,font、image等
components:放置重复使用的组件
config:存放页面标题配置
modules:配置页面自适应横竖屏
pages:各个页面
routes:页面的路由
搜索栏直接使用antd-mobile的SearchBar
importReactfrom'react'import{ SearchBar}from'antd-mobile'import{ Link}from'react-router-dom'import{ Wrapper}from'./style'exportdefaultfunctionSearch(){ return(
{ /*点击搜索框跳转搜索页面*/})}```
数据请求前端页面数据的展示不能写死在代码里面,需要数据请求,fastmock则走入了我的视野,在线接口Mock工具fastmock?在线模拟ajax请求(fastmock在没有后端程序的情况下可以实现ajax请求,有需要的小伙伴可以去尝试)
api文件夹下的request.js进行axios数据请求
importaxiosfrom'axios'exportconstgetBanners=()=>axios.get('/post/抽丝剥茧:Electron与Node.js的奇葩Bug
起因是最近在用Electron开发一个桌面端项目,有个需求是需要遍历某个文件夹下的所有JavaScript文件,对它进行AST词法语法分析,解析出元数据并写入到某个文件里。需求整体不复杂,只是细节有些麻烦,当我以为开发的差不多时,注意到一个匪夷所思的问题,查的我快怀疑人生。
缘起
什么问题呢?
原来这个需求一开始仅是遍历当前文件夹下的文件,我的获取所有JS文件的代码是这样的:
后来需求改为要包含文件夹的子文件夹,那就需要进行递归遍历。按照我以前的做法,当然是手撸一个递归,代码并不复杂,缺点是递归可能会导致堆栈溢出:
但做为一个紧跟时代浪潮的开发者,我知道Node.js的fs.readdir API中加了一个参数recursive,表示可以进行递归,人家代码的鲁棒性肯定比我的好多了:
只改了一行代码,美滋滋~
兼容性怎么样呢?我们到Node.js的API文档里看下:
是从v..0添加的,而我本地使用的Node.js版本正是这个(好巧),我们Electron中的Node.js版本是多少呢?先看到electron的版本是.0.4:
在Electron的 发布页上能找到这个版本对应的是..1,比我本地的还要多一个小版本号:
这里需要说明一下,Electron之所以优于WebView方案,是因为它内置了Chrome浏览器和Node.js,锁定了前端与后端的版本号,这样只要Chrome和Node.js本身的跨平台没有问题,理论上Electron在各个平台上都能获得统一的UI与功能体验。 而以Tauri为代表的WebView方案,则是不内置浏览器,应用程序使用宿主机的浏览器内核,开发包的体积大大减小,比如我做过的同样功能的一个项目,Electron版本有M+,而Tauri的只有4M左右。虽然体积可以这么小,又有Rust这个性能大杀器,但在实际工作中的技术选型上,想想各种浏览器与不同版本的兼容性,换我也不敢头铁地用啊! 所以,尽管Electron有这样那样的缺点,仍是我们开发客户端的不二之选。 之所以提这个,是因为读者朋友需要明白实际项目运行的是Electron内部的Node.js,而我们本机的Node.js只负责启动Electron这个工程。
以上只是为了说明,我这里使用fs.readdir这个API新特性是没有问题的。
排查
为方便排查,我将代码再度简化,提取一个单独的文件中,被Electron的Node.js端引用:
能看到控制台打印的 Node.js 版本与我们刚才查到的是一样的,文件数量为2:
同样的代码使用本机的Node.js执行:
难道是这个小版本的锅?按理说不应该。但为了排除干扰,我将本机的Node.js也升级为..1:
这下就有些懵逼了!
追踪
目前来看,锅应该是Electron的。那么第一思路是什么?是不是人家已经解决了啊?我要不要先升个级?
没毛病。
升级Electron
将Electron的版本号换成最新版本v.1.0:
再看效果:
我去,正常了!
不过,这个包的升级不能太草率,尤其正值发版前夕,所以还是先改代码吧,除了我上面手撸的代码外,还有个包readdirp也可以做同样的事情:
这两种方式,在原版本的Electron下都没有问题。
GitHub上搜索
下来接着排查。
Electron是不是有人发现了这个Bug,才进行的修复呢?
去 GitHub issue里瞅一瞅:
没有,已经关闭的问题都是年提的问题,而我们使用的Electron的版本是年月日发布的。 那么就去 代码提交记录里查下(GitHub这个功能还是很好用的):
符合条件的就一条,打开看下:
修复的这个瞅着跟我们的递归没有什么关系嘛。
等等,这个文件是什么鬼?
心血来潮的收获
我们找到这个文件,目录在lib下:
从命名上看,这个文件是对Node.js的fs模块的一个包装。如果你对Electron有了解的话,仔细一思索,就能理解为什么会有这么个文件了。我们开发时,项目里会有许多的资源,Electron的Node.js端读取内置的文件,与正常Node.js无异,但事实上,当我们的项目打包为APP后,文件的路径与开发状态下完全不一样了。所以Electron针对打包后的文件处理,重写了fs的各个方法。
这段代码中重写readdir的部分如下:
上面的判断isAsar就是判断是否打包后的归档文件,来判断是否要经Electron特殊处理。如果要处理的话,会调用Electron内部的C++代码方法进行处理。
我发现,这里Electron并没有对打包后的归档文件处理递归参数recursive,岂不是又一个Bug?应该提个issue提醒下。
不过,我们目前的问题,并不是它造成的,因为现在是开发状态下,上面代码可以简化为:
对Promise了如指掌的我怎么看这代码也不会有问题,只是心血来潮执行了一下:
我去,差点儿脑溢血!
好的一点是,曙光似乎就在眼前了!事实证明,心血来潮是有用的!
Node.js的源码
这时不能慌,本地切换Node.js版本为v,同样的代码再执行下:
这说明Electron是被冤枉的,锅还是Node.js的!
Node.js你这个浓眉大眼的,居然也有Bug!呃,还偷偷修复了!
上面的情况,其实是说原生的fs.readdir有问题:
也就是说,fs.promises.readdir并不是用util.promisify(fs.readdir)实现的!
换成同步的代码readdirSync,效果也是一样:
我们来到Node.js的GitHub地址,进行 搜索:
打开这两个文件,能发现,二者确实是不同的实现,不是简单地使用util.promisify进行转换。
fs.js
我们先看 lib/fs.js。
当recursive为true时,调用了一个readdirSyncRecursive函数,从这个命名上似乎可以看出有性能上的隐患。正常来说,这个函数是异步的,不应该调用同步的方法,如果文件数量过多,会把主进程占用,影响性能。
如果没有recursive,则是原来的代码,我们看到binding readdir这个函数,凡是binding的代码,应该就是调用了C++的底层库,进行了一次『过桥』交互。
我们接着看readdirSyncRecursive,它的命名果然没有毛病,binding readdir没有第4个参数,是完全同步的,这个风险是显而易见的:
fs/promises.js
在lib/internal/fs/promises.js中,我们看到binding readdir的第4个参数是kUsePromises,表明是个异步的处理。
当传递了recursive参数时,将调用readdirRecursive,而readdirRecursive的代码与readdirSyncRecursive的大同小异,有兴趣的可以读一读:
fs.js的提交记录
我们搜索readdir的提交记录,能发现这两篇都与深度遍历有关:
其中 下面的这个,正是我们这次问题的罪魁祸首。
刚才看到的fs.js中的readdirSyncRecursive里这段长长的注释,正是这次提交里添加的:
从代码对比上,我们就能看出为什么我们的代码遍历的程序为2了,因为readdirResult是个二维数组,它的长度就是2啊。取它的第一个元素的长度才是正解。坑爹!
也就是说,如果不使用withFileTypes这个参数,得到的结果是没有问题的:
发版记录
我们在Node.js的发版记录中,找到这条提交记录,也就是说,v..0才修复这个问题。
而Electron只有Node.js更新到v后,这个功能才修复。
而从Electron与Node.js的版本对应上来看,得更新到v了。
只是需要注意的是,像前文提过的,如果是遍历的是当前项目的内置文件,Electron并没有支持这个新的参数,在打包后会出现Bug。
fs的同步阻塞
其实有人提过一个 issue:
确实是个风险点。所以,建议Node.js开发者老老实实使用fs/promises代替fs,而Electron用户由于坑爹的fs包裹,还是不要用这个特性了。
总结
本次问题的起因是我的一个Electron项目,使用了一个Node.js fs API的一个新参数recursive递归遍历文件夹,偶然间发现返回的文件数量不对,就开始排查问题。
首先,我选择了升级Electron的包版本,发现从v.0.4升级到最新版本v.1.0后,问题解决了。但由于发版在即,不能冒然升级这么大件的东西,所以先使用readdirp这个第三方包处理。
接着排查问题出现的原因。我到Electron的GitHub上搜索issue,只找到一条近期的提交,但看提交信息,不像是我们的目标。我注意到这条提交的修改文件(asar-fs-wrapper.ts),是Electron针对Node.js的fs API的包装,意识到这是Electron为了解决打包前后内置文件路径的问题,心血来潮之下,将其中核心代码简化后,测试发现问题出在fs.promises readdir的重写上,继而锁定了Node.js版本v..1的fs.readdir有Bug。
下一步,继续看Node.js的源码,确定了fs.promises与fs是两套不同的实现,不是简单地使用util.promisify进行转换。并在fs的代码找到关于recursive递归的核心代码readdirSyncRecursive。又在提交记录里,找到这段代码的修复记录。仔细阅读代码对比后,找到了返回数量为2的真正原因。
我们在Node.js的发版记录中,找到了这条记录的信息,确定了v..0才修复这个问题。而内嵌Node.js的Electron,只有v版本起才修复。
不过需要注意的是,如果是遍历的是当前项目的内置文件,由于Electron并没有支持这个新的参数,在打包后会出现Bug。而且由于fs.readdir使用recursive时是同步的,Electron重写的fs.promises readdir最终调用的是它,可能会有隐性的性能风险。
本次定位问题走了些弯路,一开始将目标锁定到Electron上,主要是它重写fs的锅,如果我在代码中用的fs.readdirSync,那么可能会更早在Node.js上查起。谁能想到我调用的fs.promises readdir不是真正的fs.promises readdir呢?
最后,针对此次问题,我们有以下启示:
PS:我给Electron提了个 issue,一是让他们给fs.readdir添加recursive参数的实现,二是让他们注意下重写时fs.promises readdir的性能风险。