源码学习目录

1. 前言

1.1 环境

  1. 操作系统: macOS 11.5.2
  2. 浏览器: Chrome 94.0.4606.81
  3. node 14.18.1
  4. update-notifier 5.1.0

    1.2 阅读该文章可以get以下知识点

  5. update-notifier

  6. 检测npm包是否更新,比如组件库更新或者其他npm包更新,在控制台提醒

    2. update-notifier源码解析

    地址: update-notifier

    2.1 index.js 引入的npm包

    ```javascript // spawn 创建一个异步子进程 const {spawn} = require(‘child_process’); const path = require(‘path’); // 转换函数 const {format} = require(‘util’); // Import a module lazily 引入模块懒加载 const importLazy = require(‘import-lazy’)(require);

// Easily load and persist config without having to think about where and how // 配置的持久化存储 const configstore = importLazy(‘configstore’); // 控制台输出,带有颜色,vue-next使用了这个库 const chalk = importLazy(‘chalk’); // 版本控制,vue-next使用了这个库 const semver = importLazy(‘semver’); // 版本对比 const semverDiff = importLazy(‘semver-diff’); // 最新版本 const latestVersion = importLazy(‘latest-version’); // 是不是npm包 const isNpm = importLazy(‘is-npm’); // 是不是全局安装 const isInstalledGlobally = importLazy(‘is-installed-globally’); // 是不是yarn全局安装 const isYarnGlobal = importLazy(‘is-yarn-global’); // 有没有yarn const hasYarn = importLazy(‘has-yarn’); // Create boxes in the terminal 创建box在终端上 const boxen = importLazy(‘boxen’); // Get XDG Base Directory paths mac断点是获取路径/user/username下面的文件 const xdgBasedir = importLazy(‘xdg-basedir’); // Returns true if the current environment is a Continuous Integration server 判断当前环境是ci服务吗 const isCi = importLazy(‘is-ci’); // Simple micro templating 模板语法 const pupa = importLazy(‘pupa’);

  1. xdg-basedir的文档
  2. ```javascript
  3. // sindresorhus是指用户的文件名
  4. const xdgBasedir = require('xdg-basedir');
  5. xdgBasedir.data;
  6. //=> '/home/sindresorhus/.local/share'
  7. xdgBasedir.config;
  8. //=> '/home/sindresorhus/.config'
  9. xdgBasedir.dataDirs
  10. //=> ['/home/sindresorhus/.local/share', '/usr/local/share/', '/usr/share/']

2.2 UpdateNotifier 整体结构

  1. class UpdateNotifier {
  2. constructor(options = {}) {
  3. }
  4. check() {}
  5. async fetchInfo() {}
  6. notify(options) {}
  7. }
  8. module.exports = options => {
  9. // new一个实例,执行constructor的内容
  10. const updateNotifier = new UpdateNotifier(options);
  11. // 执行check函数
  12. updateNotifier.check();
  13. return updateNotifier;
  14. };
  15. module.exports.UpdateNotifier = UpdateNotifier;

2.3 UpdateNotifier constructor

  1. constructor(options = {}) {
  2. this.options = options;
  3. options.pkg = options.pkg || {};
  4. options.distTag = options.distTag || 'latest';
  5. // Reduce pkg to the essential keys. with fallback to deprecated options
  6. // TODO: Remove deprecated options at some point far into the future
  7. // 设置名称和版本,这里应该是做了兼容性判断,先找pkg.name,不存在在找option.packageName
  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. // 设置定时器的时长,如果option.updateCheckInterval存在就用这个,不存在设置为一天
  19. this.updateCheckInterval = typeof options.updateCheckInterval === 'number' ? options.updateCheckInterval : ONE_DAY;
  20. // 禁用状态,只要一个为true,就禁用,env.NO_UPDATE_NOTIFIER存在||NODE_ENV=test||参数包含--no-update-notifier|| 是不是ci
  21. this.disabled = 'NO_UPDATE_NOTIFIER' in process.env ||
  22. process.env.NODE_ENV === 'test' ||
  23. process.argv.includes('--no-update-notifier') ||
  24. isCi();
  25. // 在npm脚本中需要通知
  26. this.shouldNotifyInNpmScript = options.shouldNotifyInNpmScript;
  27. if (!this.disabled) {
  28. try {
  29. // 创建配置store,存入到~/.config/configStore中
  30. const ConfigStore = configstore();
  31. this.config = new ConfigStore(`update-notifier-${this.packageName}`, {
  32. optOut: false,
  33. // Init with the current time so the first check is only
  34. // after the set interval, so not to bother users right away
  35. lastUpdateCheck: Date.now()
  36. });
  37. } catch {
  38. // Expecting error code EACCES or EPERM
  39. const message =
  40. chalk().yellow(format(' %s update check failed ', options.pkg.name)) +
  41. format('\n Try running with %s or get access ', chalk().cyan('sudo')) +
  42. '\n to the local update config store via \n' +
  43. chalk().cyan(format(' sudo chown -R $USER:$(id -gn $USER) %s ', xdgBasedir().config));
  44. // 监听程序退出,输出一个box里面的信息
  45. process.on('exit', () => {
  46. console.error(boxen()(message, {align: 'center'}));
  47. });
  48. }
  49. }
  50. }

2.4 UpdateNotifier check 检测包的更新信息

  1. check() {
  2. // config不存在||config.optOut为true||disabled,直接返回,不校验
  3. if (
  4. !this.config ||
  5. this.config.get('optOut') ||
  6. this.disabled
  7. ) {
  8. return;
  9. }
  10. // 初始化的时候是空的
  11. this.update = this.config.get('update');
  12. // update表示需要更新的信息
  13. if (this.update) {
  14. // Use the real latest version instead of the cached one,如果当前版本和包版本一致,直接删除
  15. this.update.current = this.packageVersion;
  16. // Clear cached information
  17. this.config.delete('update');
  18. }
  19. // Only check for updates on a set interval
  20. // 判断时间有效期,在更新范围内,不需要往下走
  21. if (Date.now() - this.config.get('lastUpdateCheck') < this.updateCheckInterval) {
  22. return;
  23. }
  24. // Spawn a detached process, passing the options as an environment property
  25. // 子进程执行check.js文件,并将option转成字符串传入
  26. spawn(process.execPath, [path.join(__dirname, 'check.js'), JSON.stringify(this.options)], {
  27. detached: true,
  28. stdio: 'ignore'
  29. // unref 子进程让父进程退出
  30. }).unref();
  31. }
  32. // check.js
  33. /* eslint-disable unicorn/no-process-exit */
  34. 'use strict';
  35. let updateNotifier = require('.');
  36. // 字符串转对象
  37. const options = JSON.parse(process.argv[2]);
  38. // UpdateNotifiers 实例化
  39. updateNotifier = new updateNotifier.UpdateNotifier(options);
  40. // 异步函数
  41. (async () => {
  42. // Exit process when offline, 超时直接关闭进程
  43. setTimeout(process.exit, 1000 * 30);
  44. // 执行fetchInfo
  45. const update = await updateNotifier.fetchInfo();
  46. // Only update the last update check time on success
  47. // 更新最近check时间
  48. updateNotifier.config.set('lastUpdateCheck', Date.now());
  49. // 如果有type并且不是最新,更新update数据
  50. if (update.type && update.type !== 'latest') {
  51. updateNotifier.config.set('update', update);
  52. }
  53. // Call process exit explicitly to terminate the child process,
  54. // otherwise the child process will run forever, according to the Node.js docs
  55. // 结束
  56. process.exit();
  57. })().catch(error => {
  58. console.error(error);
  59. process.exit(1);
  60. });

很有意思的写法

  1. // 自执行异步函数,然后通过catch捕获异常
  2. (async () => {})().catch(error => {
  3. console.error(error);
  4. process.exit(1);
  5. });

2.5 UpdateNotifier fetchInfo 获取包的最新版本信息

  1. async fetchInfo() {
  2. // 获取配置
  3. const {distTag} = this.options;
  4. // 获取最新的版本
  5. const latest = await latestVersion()(this.packageName, {version: distTag});
  6. // 返回信息,最后会在check函数中存入到configStore本地缓存
  7. return {
  8. latest,
  9. current: this.packageVersion,
  10. // 版本类型
  11. type: semverDiff()(this.packageVersion, latest) || distTag,
  12. name: this.packageName
  13. };
  14. }

2.6 UpdateNotifier notify 通知包更新

  1. notify(options) {
  2. // shouldNotifyInNpmScript是配置项,默认undefined,true的时候再去判断是npm或者yarn,如果是这两个就不通知
  3. const suppressForNpm = !this.shouldNotifyInNpmScript && isNpm().isNpmOrYarn;
  4. // 一些不通知的条件,只要一个成立就return,isTTY(系统默认就是true,设置为false不通知),suppressForNpm,update(没有更新信息), semver(版本对比,最新没大于当前版本)
  5. if (!process.stdout.isTTY || suppressForNpm || !this.update || !semver().gt(this.update.latest, this.update.current)) {
  6. return this;
  7. }
  8. // 是否全局安装
  9. options = {
  10. isGlobal: isInstalledGlobally(),
  11. isYarnGlobal: isYarnGlobal()(),
  12. ...options
  13. };
  14. // 安装脚本例子的说明
  15. let installCommand;
  16. if (options.isYarnGlobal) {
  17. installCommand = `yarn global add ${this.packageName}`;
  18. } else if (options.isGlobal) {
  19. installCommand = `npm i -g ${this.packageName}`;
  20. } else if (hasYarn()()) {
  21. installCommand = `yarn add ${this.packageName}`;
  22. } else {
  23. installCommand = `npm i ${this.packageName}`;
  24. }
  25. // 默认末班
  26. const defaultTemplate = 'Update available ' +
  27. chalk().dim('{currentVersion}') +
  28. chalk().reset(' → ') +
  29. chalk().green('{latestVersion}') +
  30. ' \nRun ' + chalk().cyan('{updateCommand}') + ' to update';
  31. const template = options.message || defaultTemplate;
  32. // boxen的配置项,用来在命令行现在盒子的样式
  33. options.boxenOptions = options.boxenOptions || {
  34. padding: 1,
  35. margin: 1,
  36. align: 'center',
  37. borderColor: 'yellow',
  38. borderStyle: 'round'
  39. };
  40. // 模板生成的数据
  41. const message = boxen()(
  42. pupa()(template, {
  43. packageName: this.packageName,
  44. currentVersion: this.update.current,
  45. latestVersion: this.update.latest,
  46. updateCommand: installCommand
  47. }),
  48. options.boxenOptions
  49. );
  50. // 是否延迟执行
  51. if (options.defer === false) {
  52. console.error(message);
  53. } else {
  54. process.on('exit', () => {
  55. console.error(message);
  56. });
  57. process.on('SIGINT', () => {
  58. console.error('');
  59. process.exit();
  60. });
  61. }
  62. return this;
  63. }

2.7 整体调用顺序

  1. new UpdateNotifier() 初始化
  2. 实例对象check,检测更新信息,变更本地configStore缓存
  3. 实例对象调用notify,打印到控制台,通知更新

    2.8 如何使用

    ```javascript // Notify using the built-in convenience method notifier.notify();

// notifier.update contains some useful info about the update console.log(notifier.update); /* { latest: ‘1.0.1’, current: ‘1.0.0’, type: ‘patch’, // Possible values: latest, major, minor, patch, prerelease, build name: ‘pageres’ }

const notifier = updateNotifier({ pkg, updateCheckInterval: 1000 60 60 24 7 // 1 week });

if (notifier.update) { console.log(Update available: ${notifier.update.latest}); } ```

4. 总结

通过阅读update-notifier源码学到了不少东西

  1. async异步自执行函数,不需要通过try/catch的写法,可以直接在外层,直接catch,因为这是一个promise对象
  2. 了解configstore的npm,可以将配置本地缓存到~/.config/configSore下面,方便全局管理
  3. 了解了node process的一些用法.on监听exit/SIGINT 都是进程退出,exit退出进程
  4. child_process.spawn 开启子进程,unref关闭父进程
  5. boxen可以写命令行盒子样式

    参考文档

  6. update-notifier[

](https://zhuanlan.zhihu.com/p/372018093)