整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

最受程序员欢迎的Atom文本编辑器更新了,你用了吗?

itHub刚刚升级了Atom文本编辑器,添加了原生C++缓冲区并重写了DOM交互层。GitHub称Atom下一版本的更新将会改善Git集成和PHP支持。

Atom 1.19版本中,原生C ++文本缓冲区提高了响应速度和内存使用率。GitHub Ian Olsen表示:“现在保存文件现在是异步发生,且不会造成UI阻塞,可以顺利的从一个文件移动到下一个文件。此外,大文件的内存消耗也很小。

DOM交互层被重写,以提高性能并简化代码。重写层利用了新的浏览器功能和虚拟DOM功能。重写也是为了适应包含用于限制浏览器的样式和布局范围的CSS限制边界的API,当元素内容发生变化时,及时通知它们改变大小。

Atom是用HTML,JavaScript和Node.js构建的,可用于Windows,MacOS和Linux,运行在GitHub’s Electron cross-platform framework,用于构建桌面应用程序。即将推出的Atom是1.20版,现在处于beta阶段。为了改进版本1.20中的Git集成,GitHub对diff视图进行了重新设计,以支持待处理的窗格和多个视图。

Atom 1.20还具有PHP语法修复功能。为了提高查找和替换能力,1.20版本中的上下文行可选显示“查找项目”结果。用户可以在软件包设置中设置匹配之前和之后的可用行数,并可在查看结果时内联修改显示。

tom是一个著名的开源编辑器,是由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的扩展机制还是比较简单的,在各种扩展的生命周期中,都可以通过事件来进行通信,从而实现各种功能。这样一种实现,其实也可以在我们日常工作过程中加以借鉴。

篇教程将会教你怎么制作你的第一个 Atom 文本编辑器的插件。我们将会制作一个山寨版的 Sourcerer,这是一个从 StackOverflow 查询并使用代码片段的插件。到教程结束时,你将会制作好一个将编程问题(用英语描述的)转换成获取自 StackOverflow 的代码片段的插件,像这样:

准备

教程须知

Atom 文本编辑器是用 web 技术创造出来的。我们将完全使用 JavaScript 的 EcmaScript 6 规范来制作插件。你需要熟悉以下内容:

  • · 使用命令行
  • · JavaScript 编程
  • · Promises
  • · HTTP

教程的仓库

你可以跟着教程一步一步走,或者看看放在 GitHub 上的仓库,这里有插件的源代码。这个仓库的历史提交记录包含了这里每一个标题。

开始

安装 Atom

根据 Atom 官网 的说明来下载 Atom。我们同时还要安装上 apm(Atom 包管理器的命令行工具)。你可以打开 Atom 并在应用菜单中导航到 Atom > Install Shell Commands 来安装。打开你的命令行终端,运行 apm -v 来检查 apm 是否已经正确安装好,安装成功的话打印出来的工具版本和相关环境信息应该是像这样的:

apm -v
> apm  1.9.2
> npm  2.13.3
> node 0.10.40
> python 2.7.10
> git 2.7.4

生成骨架代码

让我们使用 Atom 提供的一个实用工具创建一个新的 package(软件包)来开始这篇教程。

  1. 启动编辑器,按下 Cmd+Shift+P(MacOS)或者 Ctrl+Shift+P(Windows/Linux)来打开命令面板。
  2. 搜索“Package Generator: Generate Package”并点击列表中正确的条目,你会看到一个输入提示,输入软件包的名称:“sourcefetch”。
  3. 按下回车键来生成这个骨架代码包,它会自动在 Atom 中打开。

如果你在侧边栏没有看到软件包的文件,依次按下 Cmd+K Cmd+B(MacOS)或者 Ctrl+K Ctrl+B(Windows/Linux)

命令面板可以让你通过模糊搜索来找到并运行软件包。这是一个执行命令比较方便的途径,你不用去找导航菜单,也不用刻意去记快捷键。我们将会在整篇教程中使用这个方法。

运行骨架代码包

在开始编程前让我们来试用一下这个骨架代码包。我们首先需要重启 Atom,这样它才可以识别我们新增的软件包。再次打开命令面板,执行 Window: Reload 命令。

重新加载当前窗口以确保 Atom 执行的是我们最新的源代码。每当需要测试我们对软件包的改动的时候,就需要运行这条命令。

通过导航到编辑器菜单的 Packages > sourcefetch > Toggle 或者在命令面板执行 sourcefetch:toggle 来运行软件包的 toggle 命令。你应该会看到屏幕的顶部出现了一个小黑窗。再次运行这条命令就可以隐藏它。

“toggle”命令

打开 lib/sourcefetch.js,这个文件包含有软件包的逻辑和 toggle 命令的定义。

toggle() {
 console.log('Sourcefetch was toggled!');
 return (
   this.modalPanel.isVisible() ?
   this.modalPanel.hide() :
   this.modalPanel.show()
 );
}

toggle 是这个模块导出的一个函数。根据模态面板的可见性,它通过一个三目运算符 来调用 show 和 hide 方法。modalPanel 是 Panel(一个由 Atom API 提供的 UI 元素) 的一个实例。我们需要在 export default 内部声明 modalPanel 才可以让我们通过一个实例变量 this 来访问它。

this.subscriptions.add(atom.commands.add('atom-workspace', {
  'sourcefetch:toggle': () => this.toggle()
}));

上面的语句让 Atom 在用户运行 sourcefetch:toggle 的时候执行 toggle 方法。我们指定了一个 匿名函数 () => this.toggle(),每次执行这条命令的时候都会执行这个函数。这是事件驱动编程(一种常用的 JavaScript 模式)的一个范例。

Atom 命令

命令只是用户触发事件时使用的一些字符串标识符,它定义在软件包的命名空间内。我们已经用过的命令有:

  • · package-generator:generate-package
  • · Window:reload
  • · sourcefetch:toggle

软件包对应到命令,以执行代码来响应事件。

进行你的第一次代码更改

让我们来进行第一次代码更改——我们将通过改变 toggle 函数来实现逆转用户选中文本的功能。

改变 “toggle” 函数

如下更改 toggle 函数。

toggle() {
  let editor
  if (editor = atom.workspace.getActiveTextEditor()) {
    let selection = editor.getSelectedText()
    let reversed = selection.split('').reverse().join('')
    editor.insertText(reversed)
  }
}

测试你的改动

  1. 通过在命令面板运行 Window: Reload 来重新加载 Atom。
  2. 通过导航到 File > New 来创建一个新文件,随便写点什么并通过光标选中它。
  3. 通过命令面板、Atom 菜单或者右击文本然后选中 Toggle sourcefetch 来运行 sourcefetch:toggle 命令。

更新后的命令将会改变选中文本的顺序:

在 sourcefetch 教程仓库 查看这一步的全部代码更改。

原文来自:https://linux.cn:443/article-7865-1.html

本文地址:https://www.linuxprobe.com/first-atom-editor.html编辑员:苏西云,审核员:逄增宝

本文原创地址:https://www.linuxprobe.com/first-atom-editor.html