硬核!手撕源码第一弹:UpdateNotifier

6 篇文章 0 订阅

目录

  1. 项目简介
  2. 源码分析
  3. 动手实现
  4. 总结

1、项目简介

update-notifier是yeoman团队出品的,之所以选择这个项目,主要是大佬推荐,其次是,项目体积只有15.3kB,功能也不复杂,在github上目前有1.6k star,比较适合当作入门源码。

update-notifier用于检查包的当前版本是否是最新版本,如果不是,则提示更新。

这个项目的使用场景一般是脚手架应用,可以在npm主页上看到有哪些项目用到了update-notifier:

我去考证了一下,实际上npm只在早期版本用了update-notifier,现在的最新版已经没有用到了,究其原因,我猜可能是因为这个项目还有很多问题没有解决,不知道是团队已经打算放弃了还是啥,反正现在还开着20来个issue,上一次更新是8个月前了。

没关系,反正我们就是要看它的源码,只不过不能一味吸收,也要分辨出其中已过时的东西,辩证地看。

话不多说,开撕源码。

2、源码分析

2.1 package.json

package.json里蕴含了包的很多信息。一般npm项目都可以直接从这个文件开始入手。

update-notifier的package.json主要关注下面这几个信息:

  • files:可选配置项,文件数组,指明了当项目作为依赖包被安装时要包含哪些文件或文件夹
  • dependencies:依赖包
  • devDependencies:开发环境的依赖包

files

  • index.js:默认入口文件
  • check.js

dependencies

  • boxen:一个能在控制台打印出方框的工具
  • chalk:给控制台的字体添加颜色
  • configstore:一个加载配置的工具,会在用户配置目录下生成对应的json文件,并保存在$XDG_CONFIG_HOME 或 ~/.config.目录下,如C:\Users\用户名\.config\configstore\
  • has-yarn:检查是否有安装工具yarn
  • import-lazy:懒加载引入依赖包
  • is-ci:检测当前环境是否为 CI 服务器(持续集成服务器)
  • is-installed-globally: 检查依赖包是否是npm全局安装的
  • is-npm:判断是否是作为npm或yarn的脚本命令来运行
  • is-yarn-global:检查包是否通过yarn全局安装的
  • latest-version:获取依赖包的最新版本信息
  • pupa:模板字符串工具
  • semver:语义化版本工具
  • semver-diff:版本比较工具
  • xdg-basedir:linux平台下的,获取 XDG 基本目录路径的工具

devDependencies

  • ava:测试工具
  • clear-module:清除模块缓存
  • fixture-stdout:截获控制台的输入
  • mock-require:模拟引入node.js模块,已弃用
  • strip-ansi:去掉字符串中的ascii转义字符
  • xo:基于eslint的强制代码格式规范工具

2.2 文档

我们先看一下update-notifier文档里的API有哪些:

  • notifier = updateNotifier(options),实例化对象
    • options:配置选项
      • pkg:包信息
        • name:包名
          • version:包的版本
      • updateCheckInterval:更新时间间隔
      • shouldNotifyInNpmScript:允许在脚本运行时通知
      • distTag:定义最新版本指向的是哪个版本,默认是’latest’
  • notifier.fetchInfo():获取版本更新信息的方法,包括最新版本号latest、当前版本号current、当前包的类型type、包名name
  • notifier.notify(options?):输出更新提示的方法,
    • options:可选配置对象
      • defer:当为true时,会等进程退出后再提示
      • message:更新提示的信息,可配置的字段有包名packageName、当前版本currentVersion、最新版本latestVersion、更新命令updateCommand(比如npm或yarn)
      • isGlobal:提示的时候,是否提示使用-g参数npm全局安装(文档中漏了个isYarnGlobal,是yarn的全局安装提示)
      • boxenOptions:提示文本的边框样式
  • –no-update-notifier:node运行时参数,当加上这个参数时不会提示更新
  • NODE_ENV:设置process.env.NODE_ENV为test时不会提示更新
  • 当前环境为CI服务器时不会提示更新。

2.3 例子example

先下载源码,再npm iyarn安装依赖包,源码中有个example.js文件,运行node example,你就会发现什么:

什么都没发生。

运行了个寂寞的我再看了一下example.js里的注释,于是看到了这么一句:

// You have to run this file two times the first time
// This is because it never reports updates on the first run
// If you want to test your own usage, ensure you set an older version

翻译一下就是:

你必须运行两次,因为第一次运行肯定是看不到更新的;

如果你想测试你自己的包,确保你的包版本号不是最新的,这样才能看到控制台打印出更新信息。

早说嘛!

一二三四再来一次:

哎,果然有了。

这是为什么?

这个问题先拿笔记本记下,待会儿分析源码的时候再说。

example.js

可以看到example.js里引入了updateNotifier后,调用了实例化方法;这里检查了一个叫public-ip的包,当前版本是0.9.2,运行后提示我们更新到4.0.4

我们也可以检查一下其他包,比如把name的值改成vue,再运行下,就会看到提示我们更新vue版本:

2.4 index.js

package.json中如果没有配置main属性,则默认index.js是项目的入口文件。所以打开index.js:

将源码折叠一下

这是CommonJS规范导出模块的写法,module.exports导出了UpdateNotifier类和实例化方法。

看看UpdateNotifier类里有啥:

  • constructor:构造函数
  • check:检查是否需要更新
  • fetchInfo:异步方法,获取最新版本的信息
  • notify:在控制台输出提示的方法

这里只大致看了下方法名入参出参,再结合说明文档,半看半猜每个方法的作用,不一定是正确的;

不过错了也没关系,接下来我们会深入看每一个方法的实现细节,如果猜错了最后看完再回来改下就行。

上一节我们通过example知道,使用的是updateNotifier实例化方法,所以我们看看实例化方法具体做了啥:

module.exports = options => {
  const updateNotifier = new UpdateNotifier(options);
  updateNotifier.check();
  return updateNotifier;
};

这三行代码总结起来就是:

new一个UpdateNotifier对象,再调用check方法,最后返回实例化对象。

来,一句句拆解。

2.4.1 构造函数constructor

当new一个对象的时候发生了啥?

  1. 创建一个船新的空JS对象(即{});
  2. 然后给这个空对象添加__proto__属性,并将该属性链接至构造函数的原型对象,这句话意思如下:
const B=new A();
B.__prototype__===A.prototype;// true`
  1. 再将构造函数的this作用域指向这个对象;
  2. 最后,执行构造函数constructor,当构造函数没有返回值时,返回这个new出来的对象;如果构造函数有返回值,则返回构造函数的返回值。

懂了,所以当我们调用实例化方法的时候,就会执行构造函数

update-notifier的构造函数里做了下面这些事:

  • 将入参pkg对象中的name和version,保存到this.options中;
  • 创建configstore实例对象this.config,并保存当前更新时间;

configstore会在用户配置路径~/.config/configstore/下生成一个update-notifier-[包名].json文件。这个文件内容如下:

update-notifier-vue.json

  • 如果你手动把其中的optOut值改为true,程序就不会再提示更新;
  • lastUpdateCheck是最近一次更新的时间戳;
  • update对象是版本更新信息。

当然,除此之外,构造函数还做了:

  • 必要的参数校验,比如name和version的非空校验
  • 可选配置项的初始化默认值,如更新间隔updateCheckInterval的默认初始化为1天
  • 检查环境配置,看当前的NODE_ENV是否为test、运行时参数是否有–no-update-notifier、是否为CI服务器,如果有一项值为是,则不提示更新。
2.4.2 check

首先根据配置项中的参数判断是否需要更新,如果需要更新,再接着往下:

this.update = this.config.get('update');

这里会去取上述json文件中的update对象,如果有update对象,会先把json文件中的update删除,然后开启一个子进程去执行check.js文件中的脚本:

// 开启子进程去执行check.js
spawn(process.execPath, [path.join(__dirname, 'check.js'), JSON.stringify(this.options)], {
      detached: true,
      stdio: 'ignore'
    }).unref(); // unref可以让父进程不用等待子进程退出再退出

check.js只做了一件事:

调用fetchInfo()异步方法,如果当前版本不是最新版本,就会更新本地json文件中的update对象:

const update = await updateNotifier.fetchInfo();

if (update.type && update.type !== 'latest') {
    updateNotifier.config.set('update', update);
}

🍉注意,这里fetchInfo异步的,所以执行完check后,没办法立即更新json文件中的update对象,这就导致了,example.js中调用notify()方法时,第一次的时候取不到update对象的值(理论上,只要你手速够快,不止第一次取不到,在fetchInfo回调之前你都是取不到的)。

看完check(),我们再来看看notify()方法做了啥。

2.4.3 notify
const suppressForNpm = !this.shouldNotifyInNpmScript && isNpm().isNpmOrYarn;
    if (!process.stdout.isTTY || suppressForNpm || !this.update || !semver().gt(this.update.latest, this.update.current)) {
    return this;
}

开头的这个if判断,决定了是否要提示更新,以下几种情况,都不会输出提示:

  1. !process.stdout.isTTY:没有输出终端
  2. suppressForNpm:如果是作为npm或yarn脚本运行时但配置了不允许提示
  3. !this.update:没有获取到update对象
  4. !semver().gt(this.update.latest, this.update.current):当前版本号>=最新版本号

看出问题来没?如果本地json文件中没有update对象,那么上一步check方法中的this.update就取不到值,这里就不会提示更新。

并且,这里的更新提示信息,实际是上一次请求的结果,而不是实际上的最新版本。

比如我们手动把json文件中的latest值改为6.6.666,然后执行node example,你就会发现打印出来的是6.6.666:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-M2wMYrja-1633144246700)(https://files.mdnice.com/user/14530/800ff4b1-a0d7-42e0-81c1-9e207b1882e5.png)]

现在你明白了为啥第一次运行的时候什么都没有了吧?

第一次运行的时候,子进程的回调还没结束,json文件中的update对象还没有值,因此check()方法中的this.update取不到值,接着notify()判断this.update时值为undefined,所以直接就return this了。

我给源码加上了注释,欢迎勘误,仅供参考:
https://github.com/youzouzou/update-notifier

3、动手实现

总结下update-notifier的运行流程:

运行流程

核心功能总结:

拉取最新版本信息,保存到本地json文件,以便下一次运行的时候取出信息,与当前版本号进行对比。如果当前版本号小于最新版本,就在控制台输出提示信息。

核心功能用到的包实际主要就三个:last-versionsemverconfigstore,其他都是一些样式或者辅助判断环境的工具包。

模仿这个核心思路,我写了一个精简版本,只实现了核心功能:

last-version异步拉取最新版本信息,在回调函数中用semver比较当前版本是否小于最新版本,若是,则直接在控制台输出更新提示。

我没有用configstore存储上一次的请求结果,也没有开子进程,而是等回调结束后再直接输出更新信息,所以每一次运行都会去拉取实际的最新版本信息。

实现代码如下:

const latestVersion = require('latest-version'); // 用于获取最新版本的 npm 包
const semver = require('semver'); // 语义化版本工具

class MyUpdateNotifier {
  constructor(options) {
    this.check(options)
  }
  async check(options) {
    const { name, version } = options;

    if (!name) {
      console.error("未获取到包名");
      return;
    }
    if (!version) {
      console.error("未获取到当前版本");
      return;
    }

    const latest = await latestVersion(name); // 获取到最新版本号
    console.log(name + "最新版本号", latest)
    if (semver.gt(latest, version)) { // 最新版本号是否大于当前版本
      console.log("请把" + name + "从" + version + "更新到最新的" + latest)
    }
  }
}

module.exports = MyUpdateNotifier;

测试一下:

运行结果

成功!✅

Demo地址:https://github.com/youzouzou/my-update-notifier

运行的时候会发现,有略明显的卡顿,这是因为我没有开子进程,而update-notifier虽然更新信息不一定是实际最新的,但却不会阻塞父进程的运行,用起来会比较丝滑。

4、总结

之前虽然也零散地看了些源码,但是并没有看完,也没有写分析笔记,这算是我第一次完整读完一个项目的源码。

这次的阅读体验还是很愉快的,update-notifier项目文档完善,代码逻辑清晰,难度也不大,看完不仅了解了实现原理,也涨了很多姿势,比如各种实用工具包,node子进程用法等等。


往期文章

前端转后端是一种怎样的体验

当程序员遇到会写代码的产品经理…

手摸手写个webpack plugin

手摸手写个webpack loader

这锅我背了…

ES2021新特性

用魔法打败魔法:前端代码规范化

手摸手教你搭个脚手架

手摸手教你搭建npm私有库

requestAnimationFrame

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值