【源码阅读 | 04】update-notifier 检查包更新

1. 场景

  用于将当前 package.json 中的包与包管理工具库(如 npm)进行比对,若存在可以更新的包,则进行提示。

2. 使用

1)安装

$ npm install update-notifier

2)使用

const updateNotifier = require('update-notifier');
const pkg = require('./package.json');

const notifier = updateNotifier({pkg});
notifier.notify();

console.log(notifier.update);
/*
{
	latest: '1.0.1',
	current: '1.0.0',
	type: 'patch', // Possible values: latest, major, minor, patch, prerelease, build
	name: 'pageres'
}
*/

3. 源码

1)阅读 readme.md

源码地址:https://github.com/yeoman/update-notifier

readme 中可了解到如下内容

  • 可以运行根目录下的 example.js 进行调试

  • 为了用户体验,update-notifier 并不会每次运行都检查,而是会在 一定时间间隔后进行检查

  • update-notifier 检查会开一个子进程来进行,尽管调用 process.exit 退出进程,也不会影响到检查

2)克隆源码仓库

# 克隆
git clone https://github.com/yeoman/update-notifier.git

# 安装依赖
cd update-notifier
npm install

3)运行调试

1)通过 package.json 可以看到,调试命令如下

npm run test

2)查看调试结果

  首次运行时不会打印信息,只有在第二次开始才会提示更新信息。

  这是因为 首次启动时找不到旧版本进行对比,因此会先将首次的信息持久化地存储起来,第二次执行时将上次存储的信息,与本次运行的结果进行对比。img

4)源码解读

example.js

'use strict';
const updateNotifier = require('.');

// 将一个包信息传入
// 首次运行会将该包 0.9.2 版本存储起来
// 第二次运行会拿到 0.9.2 与现有库中最新的版本对比
updateNotifier({
	pkg: {
		name: 'public-ip',
		version: '0.9.2'
	},
  // 检查的时间间隔
	updateCheckInterval: 0
}).notify();

check.js

/* eslint-disable unicorn/no-process-exit */
'use strict';
let updateNotifier = require('.');

const options = JSON.parse(process.argv[2]);

updateNotifier = new updateNotifier.UpdateNotifier(options);

(async () => {
  // 若运行超时,则退出进程
	setTimeout(process.exit, 1000 * 30);

  // 获取更新检查得到的包版本信息
	const update = await updateNotifier.fetchInfo();

  // 将当前时间作为最后一次检查更新时间
	updateNotifier.config.set('lastUpdateCheck', Date.now());

  // 如果当前包不是最新版本,则抛出提示
	if (update.type && update.type !== 'latest') {
		updateNotifier.config.set('update', update);
	}

	process.exit();
})().catch(error => {
	console.error(error);
	process.exit(1);
});

index.js

注:index.js 源码较多,以下会分成多个代码块进行解读

总代码流程个人理解为:

  1. 首次运行,提取出当前 package.json 中存储的所有包的版本信息,生成文件并进行持久化存储,记录下当前的时间
  2. 下次运行时,将当前时间与上一次生成文件的时间 比对,若超过检查间隔,则进行 udpate check。检查比对 npm 库,若发现包版本有可以更新的,则在控制台进行提示

跟随流程来阅读源码

1)引入工具包

'use strict';
const {spawn} = require('child_process');
const path = require('path');
const {format} = require('util');
// 懒加载模块:只有在调用到对应包的时候,再进行引入
const importLazy = require('import-lazy')(require);

const configstore = importLazy('configstore');
const chalk = importLazy('chalk');
const semver = importLazy('semver');
const semverDiff = importLazy('semver-diff');
const latestVersion = importLazy('latest-version');
const isNpm = importLazy('is-npm');
const isInstalledGlobally = importLazy('is-installed-globally');
const isYarnGlobal = importLazy('is-yarn-global');
const hasYarn = importLazy('has-yarn');
const boxen = importLazy('boxen');
const xdgBasedir = importLazy('xdg-basedir');
const isCi = importLazy('is-ci');
const pupa = importLazy('pupa');

const ONE_DAY = 1000 * 60 * 60 * 24;

2)声明 UpdateNotifier 类,后续无特殊说明,作用域均在类中

class UpdateNotifier {
	... 
}

3)声明类的构造函数

class UpdateNotifier {
	constructor(options = {}) {
		this.options = options;
		options.pkg = options.pkg || {};
		options.distTag = options.distTag || 'latest';

    // 读取包名和版本号信息
		options.pkg = {
			name: options.pkg.name || options.packageName,
			version: options.pkg.version || options.packageVersion
		};

		if (!options.pkg.name || !options.pkg.version) {
			throw new Error('pkg.name and pkg.version required');
		}

		this.packageName = options.pkg.name;
		this.packageVersion = options.pkg.version;
    // 更新检查间隔,默认值:1天
		this.updateCheckInterval = typeof options.updateCheckInterval === 'number' ? options.updateCheckInterval : ONE_DAY;
		this.disabled = 'NO_UPDATE_NOTIFIER' in process.env ||
			process.env.NODE_ENV === 'test' ||
      // process.argv:Node.js 进程时传入的命令行参数
			process.argv.includes('--no-update-notifier') ||
      // CI持续集成环境
			isCi();
		this.shouldNotifyInNpmScript = options.shouldNotifyInNpmScript;

		if (!this.disabled) {
      // 生成文件存储版本信息
			try {
				const ConfigStore = configstore();
				this.config = new ConfigStore(`update-notifier-${this.packageName}`, {
					optOut: false,
          // 生成文件时,将当前时间记录起来,方便下次比对是否超出检查的时间间隔
					lastUpdateCheck: Date.now()
				});
			} catch {
				const message =
					chalk().yellow(format(' %s update check failed ', options.pkg.name)) +
					format('\n Try running with %s or get access ', chalk().cyan('sudo')) +
					'\n to the local update config store via \n' +
					chalk().cyan(format(' sudo chown -R $USER:$(id -gn $USER) %s ', xdgBasedir().config));

				process.on('exit', () => {
					console.error(boxen()(message, {align: 'center'}));
				});
			}
		}
	}
  ...
}

4)声明 check 方法

class UpdateNotifier {
  ...
	check() {
		if (
			!this.config ||
			this.config.get('optOut') ||
			this.disabled
		) {
			return;
		}

    // 读取此次检查获取的版本信息
		this.update = this.config.get('update');

		if (this.update) {
      // 使用最新的版本,而非缓存版本
			this.update.current = this.packageVersion;

      // 清除缓存中的信息
			this.config.delete('update');
		}

		// Only check for updates on a set interval
    // 仅在检查时间间隔内进行检查,如:设置1周内不重复检查
		if (Date.now() - this.config.get('lastUpdateCheck') < this.updateCheckInterval) {
			return;
		}

    // 使用子进程来执行任务
		spawn(process.execPath, [path.join(__dirname, 'check.js'), JSON.stringify(this.options)], {
			detached: true,
			stdio: 'ignore'
		}).unref();
	}
}

5)获取版本信息

class UpdateNotifier {
  ...
	// 获取版本信息
	async fetchInfo() {
		const {distTag} = this.options;
		const latest = await latestVersion()(this.packageName, {version: distTag});

		return {
			latest,
			current: this.packageVersion,
      // 判断包是否为最新版本,联系 check.js 中 update.type === 'latest'
			type: semverDiff()(this.packageVersion, latest) || distTag,
			name: this.packageName
		};
	}
}

6)发出通知提醒

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

		options = {
			isGlobal: isInstalledGlobally(),
			isYarnGlobal: isYarnGlobal()(),
			...options
		};

    // 消息提醒模板
		let installCommand;
		if (options.isYarnGlobal) {
			installCommand = `yarn global add ${this.packageName}`;
		} else if (options.isGlobal) {
			installCommand = `npm i -g ${this.packageName}`;
		} else if (hasYarn()()) {
			installCommand = `yarn add ${this.packageName}`;
		} else {
			installCommand = `npm i ${this.packageName}`;
		}

    // 消息提醒模板
		const defaultTemplate = 'Update available ' +
			chalk().dim('{currentVersion}') +
			chalk().reset(' → ') +
			chalk().green('{latestVersion}') +
			' \nRun ' + chalk().cyan('{updateCommand}') + ' to update';

		const template = options.message || defaultTemplate;

    // 消息提醒边框样式
		options.boxenOptions = options.boxenOptions || {
			padding: 1,
			margin: 1,
			align: 'center',
			borderColor: 'yellow',
			borderStyle: 'round'
		};

    // 拼接提醒消息
		const message = boxen()(
			pupa()(template, {
				packageName: this.packageName,
				currentVersion: this.update.current,
				latestVersion: this.update.latest,
				updateCommand: installCommand
			}),
			options.boxenOptions
		);

		if (options.defer === false) {
			console.error(message);
		} else {
			process.on('exit', () => {
				console.error(message);
			});

			process.on('SIGINT', () => {
				console.error('');
				process.exit();
			});
		}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值