关山难越,谁悲失路之人;萍水相逢,尽是他乡之客。
百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程教程 > 技术文章 > 正文

Atom源码阅读系列一

guanshanw 2023-08-18 14:54 26 浏览 0 评论

Atom是一个著名的开源编辑器,是由Chris Wanstrath在2008年作为其个人的编外项目发展而来。据说在今年(2022)年底,这款编辑器也将进入关停状态。而且目前大部分程序员都把VS Code作为其最主要的开发工作,但是Atom本身的设计和代码实现都是非常优秀的,通过阅读它的源码,我们还是可以学到很多相关的编程技巧。

主流程分析

atom的代码结构非常清晰,整个项目可以分为两个部分,一个是atom本身的代码,另一个是atom的插件。atom本身的代码又可以分为两部分,一个是atom的核心业务逻辑,另一个是atom的UI代码。核心业务逻辑主要

是用来设置环境变量,调度窗口、调度系统资源等等。UI代码则主要负责处理atom的界面,比如菜单栏,工具栏,状态栏等等。

作为使用electron框架编写的应用程序,整体都是使用js来写的(早期是使用coffee来编写的),可以从其目录中看到,整个项目的目录结构如下:

|-src // 核心业务逻辑
|-|-main-process
|-|-|-atom-application.js
|-|-|-atom-environment.js
|-|-|-atom-window.js
|-static // UI代码
|-packages // 其它扩展包
...

众所周知,用Electron框架写成的应用,都可以分为主线程和渲染进程。对应到atom中,主线程的代码都是在src/main-process目录下,而渲染线程的代码则是直接src目录下。静态UI资源则在static目录下。

我们先从主线程的入口代码开始看起,代码位于src/main-process/main.js路径下:

// 命令行工具入口,
const args = yargs(process.argv)
  // Don't handle --help or --version here; they will be handled later.
  .help(false)
  .version(false)
  .alias('d', 'dev')
  .alias('t', 'test')
  .alias('r', 'resource-path').argv;
// 下面省略大量代码,主要用于处理命令行参数,用来专门处理使用命令行打开atom的情况
// 真正的入口
const start = require(path.join(resourcePath, 'src', 'main-process', 'start'));
start(resourcePath, devResourcePath, startTime);

可以从上面代码看出,其实真正的处理入口还是在start函数中(src/main-process/start.js):

module.exports = function start(resourcePath, devResourcePath, startTime) {
  // 处理错误情况
  process.on('uncaughtException', function(error = {}) {
  });
  process.on('unhandledRejection', function(error = {}) {
  });
  // 初始化各种参数
  app.commandLine.appendSwitch('enable-experimental-web-platform-features');
  const args = parseCommandLine(process.argv.slice(1));
  const previousConsoleLog = console.log;
  console.log = nslog;
  args.resourcePath = normalizeDriveLetterName(resourcePath);
  args.devResourcePath = normalizeDriveLetterName(devResourcePath);
  atomPaths.setAtomHome(app.getPath('home'));
  atomPaths.setUserData(app);
  const config = getConfig();
  const colorProfile = config.get('core.colorProfile');
  if (colorProfile && colorProfile !== 'default') {
    app.commandLine.appendSwitch('force-color-profile', colorProfile);
  }
  if (handleStartupEventWithSquirrel()) {
    return;
  } else if (args.test && args.mainProcess) {
    // 处理测试情况
    app.setPath(
      'userData',
      temp.mkdirSync('atom-user-data-dir-for-main-process-tests')
    );
    console.log = previousConsoleLog;
    app.on('ready', function() {
      const testRunner = require(path.join(
        args.resourcePath,
        'spec/main-process/mocha-test-runner'
      ));
      testRunner(args.pathsToOpen);
    });
    return;
  }
  const releaseChannel = getReleaseChannel(app.getVersion());
  let appUserModelId = 'com.squirrel.atom.' + process.arch;
  if (releaseChannel !== 'stable') {
    appUserModelId += `-${releaseChannel}`;
  }
  // 这个方法可以防止win10在任务栏中显示重复的atom图标
  app.setAppUserModelId(appUserModelId);
  app.on('open-file', addPathToOpen);
  app.on('open-url', addUrlToOpen);
  // 当应用关闭的时候,需要上报一些数据
  app.on('will-finish-launching', () =>
    startCrashReporter({
      uploadToServer: config.get('core.telemetryConsent') === 'limited',
      releaseChannel
    })
  );
  if (args.userDataDir != null) {
    app.setPath('userData', args.userDataDir);
  } else if (args.test || args.benchmark || args.benchmarkTest) {
    app.setPath('userData', temp.mkdirSync('atom-test-data'));
  }
  app.on('ready', function() {
    app.removeListener('open-file', addPathToOpen);
    app.removeListener('open-url', addUrlToOpen);
    // 构造一个atomApplication对象
    const AtomApplication = require(path.join(
      args.resourcePath,
      'src',
      'main-process',
      'atom-application'
    ));
    // 并将之前的参数传入
    AtomApplication.open(args);
  });
};

从上面代码可以看出,前置处理也是各种参数的初始化,以及为了便于测试,做的一些定制处理。在应用初始化结束后,就会动态加载应用模块,构造 AtomApplication 实例。可以注意到,这里使用按需加载的目的是希望能够在需要的时候才会去加载对应的模块,这样可以减少内存的占用。

接着,我们来看一下atom-application.js的代码,这块代码量比较大,是整个atom的核心代码,我们先来看一下整体的结构:

// 是一个单列模式, 继承自`EventEmitter`模块,主要因为内部会大量应用事件处理机制来分发逻辑。
class AtomApplication extends EventEmitter {
    static open(options) {
        // 初始化一些参数
        // 创建一个atomApplication对象
        // 并将之前的参数传入
        return new AtomApplication(options);
    }
    exit(status) {
        app.exit(status);
      }
      constructor(options){}
    async initialize(options) {}
}

程序启动的入口只有AtomApplication.open这一个方法,这个方法会创建一个AtomApplication对象,然后调用它的initialize方法,层层递进,再调用创建窗口、加载配置等方法,最终完成程序的启动。

其中,比较值得注意的是使用了一个叫做event-kit的模块。它是一个事件处理器模块,提供了一个事件处理器的抽象,可以让我们更容易地处理事件。最重要的作用是它实现了CompositeDisposable类,可以在需要的时候,释放资源。虽然javascript是一个有垃圾回收机制的语言,但是如果没有手动释放一些资源的话,会造成大量的内存占用。

在使用过程中,也十分简单

class AtomApplication extends EventEmitter {
  // 省略其它代码
  constructor(options) {
      // 省略其它代码
      this.disposable = new CompositeDisposable();
  }
  async destroy() {
    const windowsClosePromises = this.getAllWindows().map(window => {
      window.close();
      return window.closedPromise;
    });
    await Promise.all(windowsClosePromises);
    // 在销毁的时候统一释放
    this.disposable.dispose();
  }
  // 注册事件处理函数
  handleEvents() {
      // 省略其它代码,
    // 在注册事件回调的时候,直接将对象添加到disposable的依赖中去
    this.disposable.add(
      ipcHelpers.on(app, 'before-quit', async event => {...})
    );
  }
}

扩展机制

atom作为一个编辑器,它的扩展机制是非常重要的。和其他的IDE类似,扩展机制也是使用的微内核模式(或者插件模式)来实现。微内核架构是一种十分常见的软件架构,它将应用系统分为两个部分:一个微内核和一组外部的插件。微内核负责管理插件,提供插件之间的通信机制,以及提供一些基础的服务。插件则负责提供具体的功能。这样的架构可以让我们更容易地扩展软件的功能,而不需要修改软件的核心代码。

在atom中,插件主要是通过package类来实现的。package是atom扩展的基本单元,它可以包含一些功能,比如语法高亮、代码提示、代码格式化等等,也提供了让第三方开发者扩展的能力。那么这些扩展是如何加载的呢?我们先来看一下package-manager.js的代码:

// 可以加载、激活、停用、卸载包
// 加载包读取并解析包的元数据和资源,例如快捷键、菜单、样式表等
// 激活包注册加载的资源并调用包的主模块的`activate()`方法
// 停用包取消注册包的资源并调用包的主模块的`deactivate()`方法
// 卸载包从包管理器中完全移除
// 可以通过`core.disabledPackages`配置项和调用`enablePackage()/disablePackage()`方法来启用/禁用包
class PackageManager {
  preloadPackage(packageName, pack) {
    ...
  }
  loadPackages() {
    ...
  }
  enablePackage(packageName) {
    ...
  }
  // 触发事件,用来注册回调
  onDidActivatePackage(callback) {
  }
}

这个包管理器类PackageManager,可以管理扩展包的整个生命周期,主要负责包的加载、卸载、更新等操作。而所有的包都绑定在主内核的atom.packages这个全局变量上,我们可以通过这个变量来访问应用上加载的所有扩展。

那么packageManager是如何负责管理包的安装和卸载呢?:

class PackageManager {
    constructor(packages) {
        this.packages = packages;
    }
    getPackages() {
        return this.packages;
    }
    getPackage(name) {
        return this.packages.find(pkg => pkg.name === name);
    }
    // 禁用包,从内存中将包去除,然后通知应用程序或者扩展来执行禁用操作
    async deactivatePackage(name, suppressSerialization) {
        const pack = this.getLoadedPackage(name);
        if (pack == null) {
          return;
        }
        if (!suppressSerialization && this.isPackageActive(pack.name)) {
          this.serializePackage(pack);
        }
        const deactivationResult = pack.deactivate();
        if (deactivationResult && typeof deactivationResult.then === 'function') {
          await deactivationResult;
        }
        delete this.activePackages[pack.name];
        delete this.activatingPackages[pack.name];
        this.emitter.emit('did-deactivate-package', pack);
      }
}

比如在扩展ui-watcher中,就可以在监听到did-deactivate-package事件后,执行一些清理操作:

watchForPackageChanges() {
    this.subscriptions.add(
      atom.packages.onDidDeactivatePackage(pack => {
        // This only handles packages - onDidChangeActiveThemes handles themes
        const watcher = this.watchedPackages.get(pack.name);
        if (watcher) watcher.destroy();
        this.watchedPackages.delete(pack.name);
      })
    );
}

Package类,则包含了包的基础信息,包括键位设置、配置、样式等,并且有完整的生命周期。

class Package {
    constructor(params) {
      this.config = params.config;
      this.packageManager = params.packageManager;
      this.styleManager = params.styleManager;
      this.commandRegistry = params.commandRegistry;
      this.keymapManager = params.keymapManager;
      this.notificationManager = params.notificationManager;
      this.grammarRegistry = params.grammarRegistry;
      this.themeManager = params.themeManager;
      this.menuManager = params.menuManager;
      this.contextMenuManager = params.contextMenuManager;
      this.deserializerManager = params.deserializerManager;
      this.viewRegistry = params.viewRegistry;
      this.emitter = new Emitter();
      // 此处省略大量的细节
    }
    preload() {
        // do something
    }
    load() {
        // do something
    }
    unload() {
        // do something
    }
    activate() {
        // do something
    }
    deactivate() {
        // do something
    }
    finishLoading() {
        // do something
    }
}

Package(扩展)实例本身要和主应用进行通信,atom是直接通过全局对象的方式进行调用的,这样做的好处是不用考虑通信的问题,但是也有一些弊端,比如不方便重构等。

在应用入口处,会将PackageManager实例挂载在应用实例上。后续我们可以通过atom.packages来访问包管理器实例,从而获取包的信息。

// atom-application.js
this.packages = new PackageManager({
    ... // 一堆的配置
});
this.packages.initialize(...);

而在渲染进程中,可以通过在window上挂载atom对象来访问包管理器实例,从而获取所有扩展包的信息,进行预加载操作。

// initialize-application-window.js
/ 初始化 AtomEnvironment
global.atom = new AtomEnvironment({
  clipboard,
  applicationDelegate: new ApplicationDelegate(),
  enablePersistence: true
});
TextEditor.setScheduler(global.atom.views);
// 初始化应用窗口
global.atom.preloadPackages();
// ... 省略大量代码
module.exports = function({ blobStore }) {
 // 省略大量代码
 // 在startEditorWindows内部,当窗口初始化完成后,会正式调用`loadPackages`方法来加载所有的扩展包
 return global.atom.startEditorWindow().then(function() {
    // Workaround for focus getting cleared upon window creation
    const windowFocused = function() {
      window.removeEventListener('focus', windowFocused);
      setTimeout(() => document.querySelector('atom-workspace').focus(), 0);
    };
    window.addEventListener('focus', windowFocused);
    ipcRenderer.on('environment', (event, env) => updateProcessEnv(env));
  });
}

总体而言,atom的扩展机制还是比较简单的,在各种扩展的生命周期中,都可以通过事件来进行通信,从而实现各种功能。这样一种实现,其实也可以在我们日常工作过程中加以借鉴。

相关推荐

七条简单命令让您玩转Git
七条简单命令让您玩转Git

凭借着出色的协作能力、快速部署效果与代码构建辅助作用,Git已经得到越来越多企业用户的青睐。除了用于开发商业及消费级应用之外,众多科学及政府机构也开始尝试使用这...

2023-10-07 12:14 guanshanw

基本完整的关于Git分支branch的操作
基本完整的关于Git分支branch的操作

Git使用背景项目中要用到dev或者其他分支开发完代码,需要将该分支合并到master的需求操作步骤下面以dev名称为lex为分支名为例来操作一遍客户端操作:...

2023-10-07 12:14 guanshanw

Git 进阶(合并与变基)
Git 进阶(合并与变基)

在Git中整合来自不同分支的修改主要有两种方法:合并(merge)以及变基(rebase)合并(merge)merge流程图merge的原理是找到这两个分...

2023-10-07 12:13 guanshanw

Git学习笔记 003 Git进阶功能 part5 合并(第一部分)

合并(merge)是很常用的操作。尤其是一个庞大的很多人参与开发的企业级应用。一般会设定一个主分支,和多个副分支。在副分支开发完成后,合并到主分支中。始终保持主分支是一个完整的,稳定的最新状态的分支。...

非标题党,三张图帮你理解git merge和git rebase的区别
非标题党,三张图帮你理解git merge和git rebase的区别

初始场景:基于正常的开发分支修改几个小bug,然后在合并到开发分支上。gitmergegitcheckoutfeaturegitmergeho...

2023-10-07 12:13 guanshanw

git 初次使用(01)
git 初次使用(01)

先从github上克隆代码下来:使用vscode克隆代码如下图,填写上github仓库地址:vscode有时候克隆代码速度比较慢,可以用命令行方式克隆gitc...

2023-10-07 12:12 guanshanw

Git 远程操作

4.Git远程操作命令说明gitremote远程版本库操作gitfetch从远程获取版本库gitpull下载远程代码并合并gitpush上传远程代码并合并4.1远程版本库操作gitre...

Git常用命令-总结
Git常用命令-总结

创建git用户$gitconfig--globaluser.name"YourName"$gitconfig--globaluser.em...

2023-10-07 12:12 guanshanw

git中删除从别人clone下来项目的git信息,并修改为自己的分支

如果你从别人的Git存储库中克隆了一个项目,并想要删除与该存储库相关的Git信息,并将其修改为你自己的分支,则可以执行以下步骤:使用gitclone命令克隆存储库:gitclone<u...

git系列-回滚和放弃本地修改

回滚历史提交就是reset的功能。这种情况是已经提交远程仓库,需要回滚到之前的提交。gitreset--hardcommitId//注:强制提交后,当前版本后面的提交版本将会删掉!gi...

GIT使用小技巧大全
GIT使用小技巧大全

在大型软件工程的开发过程中,版本控制是无法绕过去的;目前来说,最火的版本控制软件就是GIT了。早两年SVN比较火,不过被大神linus喷了几次后,就日落西山了,...

2023-10-07 12:11 guanshanw

git相关命令-上
git相关命令-上

这些命令都是看了文档后,个人觉得比较有用的一些,展示给大家。回到远程仓库的状态抛弃本地所有的修改,回到远程仓库的状态。gitfetch--all&...

2023-10-07 12:10 guanshanw

Git命令行接口:掌握Git的必备技能
Git命令行接口:掌握Git的必备技能

Git是一款强大的分布式版本控制工具,它支持命令行界面操作。熟练掌握Git命令行接口,是开发者使用Git的必备技能之一。在这篇文章中,我们将介绍Git命令行接口...

2023-10-07 12:10 guanshanw

Git命令详解
Git命令详解

相信各位小伙伴们应该都对git有一些了解,毕竟作为代码管理的神器,就算不是IT行业的小伙伴肯定也或多或少的听说过一些。今天就来和小伙伴们分享一下自己总结的常用命...

2023-10-07 12:10 guanshanw

工作7年收集到的git命令
工作7年收集到的git命令

概念git中的术语解释:仓库也叫版本库(repository)stage:暂存区,add后会存到暂存区,commit后提交到版本库git安装linux...

2023-10-07 12:10 guanshanw

取消回复欢迎 发表评论: