依旧是迟到很久的阅读源码系列 非常感谢若川大佬的阅读源码活动~

1.阅读前准备

曾经npm install之后会发现会自动弹出npm依赖包升级的页面screenshot.png

现在就来研究下到底是怎么做到的

2.源码地址

https://github.com/yeoman/update-notifier

3.开始阅读

UpdateNotifier构造函数中,分为三部分函数:check()检查函数;fetchInfo获取信息函数;notify通知函数

整个代码中引用了很多第三方库

先从UpdateNotifier构造函数入手

3.1 构造函数

  1. constructor(options = {}) {
  2. // 主要是对传入的options对象中的参数进行校验
  3. this.options = options;
  4. options.pkg = options.pkg || {};
  5. options.distTag = options.distTag || 'latest';
  6. // Reduce pkg to the essential keys. with fallback to deprecated options
  7. // TODO: Remove deprecated options at some point far into the future
  8. options.pkg = {
  9. name: options.pkg.name || options.packageName,
  10. version: options.pkg.version || options.packageVersion
  11. };
  12. // 如果缺少必要 则抛出异常
  13. if (!options.pkg.name || !options.pkg.version) {
  14. throw new Error('pkg.name and pkg.version required');
  15. }
  16. this.packageName = options.pkg.name;
  17. this.packageVersion = options.pkg.version;
  18. // 检查传入的时间戳,如果不是时间则采用默认时间戳
  19. this.updateCheckInterval = typeof options.updateCheckInterval === 'number' ? options.updateCheckInterval : ONE_DAY;
  20. this.disabled = 'NO_UPDATE_NOTIFIER' in process.env ||
  21. process.env.NODE_ENV === 'test' ||
  22. process.argv.includes('--no-update-notifier') ||
  23. isCi();
  24. this.shouldNotifyInNpmScript = options.shouldNotifyInNpmScript;
  25. if (!this.disabled) {
  26. try {
  27. const ConfigStore = configstore();
  28. this.config = new ConfigStore(`update-notifier-${this.packageName}`, {
  29. optOut: false,
  30. // Init with the current time so the first check is only
  31. // after the set interval, so not to bother users right away
  32. lastUpdateCheck: Date.now()
  33. });
  34. } catch {
  35. // Expecting error code EACCES or EPERM
  36. const message =
  37. chalk().yellow(format(' %s update check failed ', options.pkg.name)) +
  38. format('\n Try running with %s or get access ', chalk().cyan('sudo')) +
  39. '\n to the local update config store via \n' +
  40. chalk().cyan(format(' sudo chown -R $USER:$(id -gn $USER) %s ', xdgBasedir().config));
  41. process.on('exit', () => {
  42. console.error(boxen()(message, {align: 'center'}));
  43. });
  44. }
  45. }
  46. }

3.2 check检查函数

  1. check() {
  2. // 如果出现以下几种情况时,则直接退出
  3. if (
  4. !this.config ||
  5. this.config.get('optOut') ||
  6. this.disabled
  7. ) {
  8. return;
  9. }
  10. // 获取包更新信息(第一次获取的时候为undefined)
  11. this.update = this.config.get('update');
  12. if (this.update) {
  13. // 如果存在,则赋值最新版本
  14. this.update.current = this.packageVersion;
  15. // 并清理缓存
  16. this.config.delete('update');
  17. }
  18. // 如果最后一次获取更新的时间小于用户设置的检查时间 则直接退出
  19. if (Date.now() - this.config.get('lastUpdateCheck') < this.updateCheckInterval) {
  20. return;
  21. }
  22. // 调用子进程执行check文件
  23. // 这里的unref 方法用于断绝与父进程的关系,父进程退出不会造成子进程的退出
  24. spawn(process.execPath, [path.join(__dirname, 'check.js'), JSON.stringify(this.options)], {
  25. detached: true,
  26. stdio: 'ignore'
  27. }).unref();
  28. }

在进行check函数的执行后,会进入check.js文件的执行,进一步看check.js中又发生了什么

  1. let updateNotifier = require('.');
  2. const options = JSON.parse(process.argv[2]);
  3. updateNotifier = new updateNotifier.UpdateNotifier(options);
  4. (async () => {
  5. setTimeout(process.exit, 1000 * 30);
  6. // 在这里继续调用updateNotifier中的获取数据的方法
  7. const update = await updateNotifier.fetchInfo();
  8. // 并更新最后更新检查时间的时间字段
  9. updateNotifier.config.set('lastUpdateCheck', Date.now());
  10. // 如果此时时间不是最新,则会更新为最新
  11. if (update.type && update.type !== 'latest') {
  12. updateNotifier.config.set('update', update);
  13. }
  14. // Call process exit explicitly to terminate the child process,
  15. // otherwise the child process will run forever, according to the Node.js docs
  16. process.exit();
  17. })().catch(error => {
  18. console.error(error);
  19. process.exit(1);
  20. });

check.js这里主要为开启子进程,获取最新版本信息的步骤,执行完成后将退出子进程

3.3 fetchInfo()获取信息函数

  1. async fetchInfo() {
  2. const {distTag} = this.options;
  3. // 这里主要通过懒加载的方式执行获取包信息的步骤
  4. const latest = await latestVersion()(this.packageName, {version: distTag});
  5. return {
  6. latest,
  7. current: this.packageVersion,
  8. // 这里会做信息的diff
  9. type: semverDiff()(this.packageVersion, latest) || distTag,
  10. name: this.packageName
  11. };
  12. }

3.4 notify()通知函数

  1. notify(options) {
  2. const suppressForNpm = !this.shouldNotifyInNpmScript && isNpm().isNpmOrYarn;
  3. if (!process.stdout.isTTY || suppressForNpm || !this.update || !semver().gt(this.update.latest, this.update.current)) {
  4. return this;
  5. }
  6. options = {
  7. // 是否为全局安装
  8. isGlobal: isInstalledGlobally(),
  9. // 是否为yarn全局安装
  10. isYarnGlobal: isYarnGlobal()(),
  11. ...options
  12. };
  13. let installCommand;
  14. // 根据yarn和npm的判断 展示不同的命令指示符给用户
  15. if (options.isYarnGlobal) {
  16. installCommand = `yarn global add ${this.packageName}`;
  17. } else if (options.isGlobal) {
  18. installCommand = `npm i -g ${this.packageName}`;
  19. } else if (hasYarn()()) {
  20. installCommand = `yarn add ${this.packageName}`;
  21. } else {
  22. installCommand = `npm i ${this.packageName}`;
  23. }
  24. const defaultTemplate = 'Update available ' +
  25. chalk().dim('{currentVersion}') +
  26. chalk().reset(' → ') +
  27. chalk().green('{latestVersion}') +
  28. ' \nRun ' + chalk().cyan('{updateCommand}') + ' to update';
  29. const template = options.message || defaultTemplate;
  30. options.boxenOptions = options.boxenOptions || {
  31. padding: 1,
  32. margin: 1,
  33. align: 'center',
  34. borderColor: 'yellow',
  35. borderStyle: 'round'
  36. };
  37. const message = boxen()(
  38. pupa()(template, {
  39. packageName: this.packageName,
  40. currentVersion: this.update.current,
  41. latestVersion: this.update.latest,
  42. updateCommand: installCommand
  43. }),
  44. options.boxenOptions
  45. );
  46. if (options.defer === false) {
  47. console.error(message);
  48. } else {
  49. process.on('exit', () => {
  50. console.error(message);
  51. });
  52. process.on('SIGINT', () => {
  53. console.error('');
  54. process.exit();
  55. });
  56. }
  57. return this;
  58. }


4.总结

整个过程比较简单,主体就为updateNotifier构造函数,其中执行的步骤为:

  • new了一个updateNotifier对象
  • 执行check()函数
    • 判断是否需要执行check.js
  • 执行fetchInfo()函数获取信息
  • set(‘lastUpdateCheck’)设置最后获取更新的时间
  • set(‘update’)设置一个需要更新的列表
  • notify()函数,展示给用户目前可更新的依赖包,并根据包类型展示命令符