Atomコードリーディング Part 3
rendererプロセス側の初期化
前回は、mainプロセスの初期化からBrowserWindow
でhtmlがロードされるとこまでみた。今回はそのhtmlから始まるrendererプロセスの初期化周りをおってみる。
static/index.html
mainプロセスでAtomWindow
(BrowserWindow
)からロードされるhtml。唯一のhtmlだったのでAtomは完全にSPAだと思う。
<!DOCTYPE html> <html style="background: #fff"> <head> <title></title> <meta http-equiv="Content-Security-Policy" content="default-src *; script-src 'self'; style-src 'self' 'unsafe-inline';"> <script src="index.js"></script> </head> <body tabindex="-1"> </body> </html>
中身はこれだけ、static/index.js
が起点になる。cssはどこでロードするんだろう?
static/index.js
window.onload
のハンドラを設定してる。
ATOM_HOME
の設定
setupAtomHome();
関数の中を見るとprocess
オブジェクトにアクセスしてる。rendererプロセスでもアクセスできることがわかった。もちろんmainプロセスとは別プロセスなので違うもの。
ロード設定のデコード
var rawLoadSettings = decodeURIComponent(location.hash.substr(1)); // デコード var loadSettings; try { loadSettings = JSON.parse(rawLoadSettings); } catch (error) { console.error("Failed to parse load settings: " + rawLoadSettings); throw error; }
前回、mainプロセスでロード設定をURLのlocation.hash
につめてたけど、ここではそれのデコードをしてる。
各種キャッシュディレクトリの設定
setupCoffeeCache(cacheDir); ModuleCache = require('../src/module-cache'); ModuleCache.register(loadSettings); ModuleCache.add(loadSettings.resourcePath); // Start the crash reporter before anything else. require('crash-reporter').start({ productName: 'Atom', companyName: 'GitHub', // By explicitly passing the app version here, we could save the call // of "require('remote').require('app').getVersion()". extra: {_version: loadSettings.appVersion} }); setupVmCompatibility(); setupCsonCache(cacheDir); setupSourceMapCache(cacheDir); setupBabel(cacheDir); setupTypeScript(cacheDir);
mainプロセスでもCoffeeScriptのキャッシュをしていたが、rendererプロセスの方では他にもキャッシュしてる。ざっとあげると、
- CoffeScript
- node_modules
- cson
- source map
- babel
- TypeScript
大規模アプリだから、このくらいしないと辛いのかな。注目すべきは、renderer側ならbabelとTypeScriptにも対応しているという点。
src/window-bootstrap
の実行
require(loadSettings.bootstrapScript);
上でも書いたけどloadSettings
にはmainプロセスからもらった設定が入ってる。
ipc
でwindow-command
チャネルに'window:loaded'メッセージを送信
require('ipc').sendChannel('window-command', 'window:loaded');
まわりくどい感じだけど、ここでプロセス間通信をしてる。ipc
モジュールのイベントハンドラはAtomApplication
クラス(src/browser/atom-application.coffee
)で設定していたので、そちらをみる。
ipc.on 'window-command', (event, command, args...) -> win = BrowserWindow.fromWebContents(event.sender) win.emit(command, args...)
window-comman
チャネルにメッセージがきたら、メッセージを送ってきたウインドウを取得して、イベントを発火してる。今回はwindow:loaded
が発火する。このwindow:loaded
をウインドウ自身が監視してるんだけど、それは前回見た。
処理はざっとこんなものなので、src/window-bootstarp.coffee
をみる。
src/window-bootstarp.coffee
# Like sands through the hourglass, so are the days of our lives. require './window' # windowにユーティリティを生やす Atom = require './atom' window.atom = Atom.loadOrCreate('editor') # `Atom`のインスタンス化 atom.initialize() # エディタの初期化 atom.startEditorWindow() # エディタ開始 # Workaround for focus getting cleared upon window creation windowFocused = -> window.removeEventListener('focus', windowFocused) setTimeout (-> document.querySelector('atom-workspace').focus()), 0 window.addEventListener('focus', windowFocused)
エディタを起動しているようにみえる。Atom
クラスがアプリ本体かな。
src/atom.coffee
Atom
クラスがある。theoristというライブラリのModel
クラスを継承している。GitHubページには"A reactive model toolkit for CoffeeScript"と書いてあるだけで、ドキュメントはない。
src/window-bootstarp.coffee
から呼ばれてるメソッドを中心にみていく。
loadOrCreate(mode)
エディタの状態をロードしてAtom
クラスのインスタンスを生成して返す。エディタの状態ファイルは、デフォルトだと~/.atom/storage/editor-*
となりそう。*
の部分は起動パスで決まるSHA1ハッシュだった。
このメソッドに渡す引数mode
はspec
モードとeditor
モードがあった。普通にアプリ起動する場合はeditor
モードを渡す。
constructor
# Call .loadOrCreate instead constructor: (@state) -> @emitter = new Emitter {@mode} = @state DeserializerManager = require './deserializer-manager' @deserializers = new DeserializerManager() @deserializeTimings = {}
Emitter
はevent-kitというライブラリのもので、多分EventEmitter
みたいなものだと思う。
DeserializerManager
(src/deserializer-manager.coffee
)はなんだろう?必要になったら追う。
initialize()
window.onerrorのハンドラ設定
いろいろエラー処理がある。
nodeのグローバルパスにパスを追加
# Add 'exports' to module search path. exportsPath = path.join(resourcePath, 'exports') require('module').globalPaths.push(exportsPath) # Still set NODE_PATH since tasks may need it. process.env.NODE_PATH = exportsPath
exports
ディレクトリをグローバルに追加してる。macだと/Applications/Atom.app/Contents/Resources/app/exports/
になる。
exports
にあるのは、atom.coffee
だけみたいだけど、なんのためのディレクトリなんだろう?exports/atom.coffee
はmodule.exports
でいろんなクラスがエクスポートされてる。reactなんかもあった。
各種クラスのインスタンス化
@config = new Config({configDirPath, resourcePath}) @keymaps = new KeymapManager({configDirPath, resourcePath}) @keymap = @keymaps # Deprecated @keymaps.subscribeToFileReadFailure() @tooltips = new TooltipManager @notifications = new NotificationManager @commands = new CommandRegistry @views = new ViewRegistry @packages = new PackageManager({devMode, configDirPath, resourcePath, safeMode}) @styles = new StyleManager document.head.appendChild(new StylesElement) @themes = new ThemeManager({packageManager: @packages, configDirPath, resourcePath, safeMode}) @contextMenu = new ContextMenuManager({resourcePath, devMode}) @menu = new MenuManager({resourcePath}) @clipboard = new Clipboard() @grammars = @deserializers.deserialize(@state.grammars ? @state.syntax) ? new GrammarRegistry() Object.defineProperty this, 'syntax', get: -> deprecate "The atom.syntax global is deprecated. Use atom.grammars instead." @grammars @subscribe @packages.onDidActivateInitialPackages => @watchThemes() Project = require './project' TextBuffer = require 'text-buffer' @deserializers.add(TextBuffer) TokenizedBuffer = require './tokenized-buffer' DisplayBuffer = require './display-buffer' TextEditor = require './text-editor' @windowEventHandler = new WindowEventHandler
クラスが多すぎるので、個々の実装は必要になったらみる。各インスタンスはそれぞれプロパティで保持しているので、Developer Toolsのコンソールでatom
を参照すればアクセスできる。
startEditorWindow()
ようやくエディタが起動しそう。
# Call this method when establishing a real application window. startEditorWindow: -> {resourcePath, safeMode} = @getLoadSettings() # エディタからatom、apmコマンドを使えるようにしてる? CommandInstaller = require './command-installer' CommandInstaller.installAtomCommand resourcePath, false, (error) -> console.warn error.message if error? CommandInstaller.installApmCommand resourcePath, false, (error) -> console.warn error.message if error? dimensions = @restoreWindowDimensions() # ウインドウの位置、幅、高さを取得 @loadConfig() # configのロード @keymaps.loadBundledKeymaps() # バンドルされてるキーマップのロード @themes.loadBaseStylesheets() # ベースのスタイルシートをロード @packages.loadPackages() # パッケージのロード @deserializeEditorWindow() # エディタウインドウをデシリアライズ @watchProjectPath() @packages.activate() # パッケージの有効化 @keymaps.loadUserKeymap() # ユーザー定義のキーマップのロード @requireUserInitScript() unless safeMode # ユーザー定義の起動スクリプトを実行 @menu.update() # メニューの更新 @subscribe @config.onDidChange 'core.autoHideMenuBar', ({newValue}) => @setAutoHideMenuBar(newValue) @setAutoHideMenuBar(true) if @config.get('core.autoHideMenuBar') # メニューバーの可視を設定 @openInitialEmptyEditorIfNecessary() # 必要なら空のエディタを開く maximize = dimensions?.maximized and process.platform isnt 'darwin' @displayWindow({maximize}) # ウインドウの表示
気になったところを追う。
configのロード
loadConfig: -> @config.setSchema null, {type: 'object', properties: _.clone(require('./config-schema'))} @config.load()
src/config-schema.coffee
に設定のスキーマがある。
エディタウインドウをデシリアライズ
deserializeEditorWindow: -> @deserializePackageStates() @deserializeProject() @deserializeWorkspaceView()
順にみる。まずはパッケージのデシリアライズから
deserializePackageStates: -> @packages.packageStates = @state.packageStates ? {} delete @state.packageStates
stateにあったらそこからパッケージの状態を復元してる。次はproject
deserializeProject: -> Project = require './project' startTime = Date.now() @project ?= @deserializers.deserialize(@state.project) ? new Project() @deserializeTimings.project = Date.now() - startTime
Project
クラスのインスタンスを生成してる。インスタンスは@state
からデシリアライズできなかったら、new
で生成してる。
deserializeWorkspaceView: -> Workspace = require './workspace' WorkspaceView = require './workspace-view' startTime = Date.now() @workspace = Workspace.deserialize(@state.workspace) ? new Workspace workspaceElement = @views.getView(@workspace) @__workspaceView = workspaceElement.__spacePenView @deserializeTimings.workspace = Date.now() - startTime @keymaps.defaultTarget = workspaceElement document.querySelector(@workspaceViewParentSelector).appendChild(workspaceElement) # workspaceのDOM要素追加
Project
クラスと同様にWorkspace
クラスのインスタンスを生成してる。ようやくconstructor
で出てきたDeserializerManager
クラスが何をやっているかわかってきた。
あと、ようやくDOMのAPIが出てきた。@workspaceViewParentSelector
はbodyだったので、body直下にworkspaceのDOM要素を追加している。workspaceの要素はViewRegistry
クラスのインスタンスである@views
を介して取得している。ViewRegistry
クラスは重要そうだったので、後で追ってみる。
workspaceのDOM要素をDeveloper Toolsで確認してみたら、<atom-workspace></atom-work-space>
となっていた。遂にWeb ComponentsのCustom Elementsが出てきた。ルートの要素がCustom ElementsということはがっつりWeb Componentsを導入しているみたいですね。
ユーザー定義の起動スクリプトを実行
getUserInitScriptPath: -> initScriptPath = fs.resolve(@getConfigDirPath(), 'init', ['js', 'coffee']) initScriptPath ? path.join(@getConfigDirPath(), 'init.coffee') requireUserInitScript: -> if userInitScriptPath = @getUserInitScriptPath() try require(userInitScriptPath) if fs.isFileSync(userInitScriptPath) catch error atom.notifications.addError "Failed to load `#{userInitScriptPath}`", detail: error.message dismissable: true
~/.atom/init.coffee
がユーザースクリプトみたい。デフォルトではコメントしか書いてない。
ウインドウの表示
# Schedule the window to be shown and focused on the next tick. # # This is done in a next tick to prevent a white flicker from occurring # if called synchronously. displayWindow: ({maximize}={}) -> setImmediate => @show() @focus() @setFullScreen(true) if @workspace.fullScreen @maximize() if maximize
とりあえず@show()
をみる。
# Public: Show the current window. show: -> ipc.send('call-window-method', 'show')
mainプロセスにメッセージを送ってる。main側のコード(src/browser/atom-application.coffee
)をみる。
ipc.on 'call-window-method', (event, method, args...) -> win = BrowserWindow.fromWebContents(event.sender) win[method](args...)
BrowserWindow
のインスタンスを取得して、BrowserWindow.show()
を実行してる。ここで、実際に画面にワークスペースが表示されるはず。
この辺でrendererプロセス側の初期化処理は終わりな気がする。
上ででてきた重要そうなクラスをみる。
src/deserializer-manager.cofee
DeserializerManager
クラスがある。ファイルにコメントがあったのでみてみる。
# Extended: Manages the deserializers used for serialized state # # An instance of this class is always available as the `atom.deserializers` # global. # # ## Examples # # ```coffee # class MyPackageView extends View # atom.deserializers.add(this) # # @deserialize: (state) -> # new MyPackageView(state) # # constructor: (@state) -> # # serialize: -> # @state # `
シリアライズされたstateに対して使うデシリアライザーを管理するとのこと。globalのatom.deserializers
として使えるようにする想定。例にあるようにあるデシリアライザークラスを管理したい場合は、そのクラスフィールドでatom.deserializers.add(this)
とする必要があるみたい。
add
メソッドをみる。
# Public: Register the given class(es) as deserializers. # # * `deserializers` One or more deserializers to register. A deserializer can # be any object with a `.name` property and a `.deserialize()` method. A # common approach is to register a *constructor* as the deserializer for its # instances by adding a `.deserialize()` class method. add: (deserializers...) -> @deserializers[deserializer.name] = deserializer for deserializer in deserializers new Disposable => delete @deserializers[deserializer.name] for deserializer in deserializers return
.name
プロパティと.deserialize()
メソッドをもったオブジェクトがデシリアライザーになりうる。管理から外したい場合は、このメソッドの返り値であるDisposable
においてdispose()
を呼べばいいらしい。
実際に使われてるところがみたいので、Workspace
クラス(src/workspace.coffee
)を追ってみる。ざっとみたところ.name
プロパティがなかったけど、これは継承元であるtheoristのModel
クラスがもっていた。.deserialize()
メソッドの方はserializableというライブラリをミックスインすることにより解決していた。以下はWorkspace
クラスの冒頭。
module.exports = class Workspace extends Model atom.deserializers.add(this) # デシリアライザーをatomの管理下に加える Serializable.includeInto(this) # Serializableをミックスイン
src/atom.coffee
で実際にWorkspace
がデシリアライズされるとこ。
# deserializeWorkspaceViewメソッドの中 @workspace = Workspace.deserialize(@state.workspace) ? new Workspace
シリアライズされるとこ。
# unloadEditorWindowメソッドの中 @state.workspace = @workspace.serialize()
Workspace
のようにアプリケーションでstateを管理するクラスはほぼデシリアライザーでありシリアライザブルになっている感じがする。エディタくらいになると管理すべきstateが多いから、さくっとシリアライズして保存しておくのが楽なんだと思う。
あと、theoristもserializableもatomのライブラリなんだけど、細かくライブラリに切り出してるなという印象を受ける。atomにロックインしているわけではなく、単体でも使えそうだからすごい。
src/view-registry.coffee
ViewRegistry
クラスがある。コメントからみてみる。ここで気づいたんだけど、このコメントからAPI Referenceが生成されてた。
# Essential: `ViewRegistry` handles the association between model and view # types in Atom. We call this association a View Provider. As in, for a given # model, this class can provide a view via {::getView}, as long as the # model/view association was registered via {::addViewProvider} # # If you're adding your own kind of pane item, a good strategy for all but the # simplest items is to separate the model and the view. The model handles # application logic and is the primary point of API interaction. The view # just handles presentation. # # View providers inform the workspace how your model objects should be # presented in the DOM. A view provider must always return a DOM node, which # makes [HTML 5 custom elements](http://www.html5rocks.com/en/tutorials/webcomponents/customelements/) # an ideal tool for implementing views in Atom. # # You can access the `ViewRegistry` object via `atom.views`.
ViewRegistry
はmodelとviewの間の接続を担当する。その接続のことをview providerと呼ぶ。modelに対しては::getView
を通してviewを提供する。model/viewの接続は::addViewProvider
で登録する。
定番だけど、modelとviewの分離はいい戦略であると書いてある。modelはアプリケーションロジックを担当する。そして、API interactionのプライマリーポイントでもある。viewはプレゼンテーションを担当する。
view providerは、workspaceにmodelがどのようにDOMをプレゼンテーションすべきかを知らせる。view providerは常にDOMノードを返すべきである。DOMノードとして、HTML 5 custom elementsを使ってる。
ざっくりとだけどAtomのGUI部分の設計について説明しているようにみえる。ソースコードと照らしあわせてみたけど、実装は単純だった。ちゃんとviewインスタンスの管理もやってた。
まとめ
次は、projectとworkspaceの実装を追っていく。