Atomコードリーディング Part 2
エントリーポイント以降
前回はAtomのビルドからアプリケーションのエントリーポイントまでみた。今回はエントリーポイントからhtmlのロードまでみた。
src/browser/atom-application.coffee
EventEmitter
を継承したAtomApplication
クラスがある。Atomアプリケーションのシングルトンクラス。エントリーポイントであり、アプリの状態を保持する。
open()
src/browser/main.coffee
から呼ばれるAtomApplication
クラスのスタティックメソッド。
# Public: The entry point into the Atom application. @open: (options) -> options.socketPath ?= DefaultSocketPath createAtomApplication = -> new AtomApplication(options) # FIXME: Sometimes when socketPath doesn't exist, net.connect would strangely # take a few seconds to trigger 'error' event, it could be a bug of node # or atom-shell, before it's fixed we check the existence of socketPath to # speedup startup. if (process.platform isnt 'win32' and not fs.existsSync options.socketPath) or options.test createAtomApplication() return # ローカルドメインソケットに接続 client = net.connect {path: options.socketPath}, -> client.write JSON.stringify(options), -> client.end() app.terminate() # なぜ終了? client.on 'error', createAtomApplication
windowsでなくて、ローカルドメインソケット(macならatom-#{process.env.USER}.sock
)がなかったら、AtomApplication
自身のインスタンスを生成して終わり。
それ以外の場合は、AtomApplication
のインスタンス化はしないで、代わりにnet
モジュールでローカルドメインソケットにつないで、コールバックでデータ送信してる。データ送信終わったらapp.terminate()
が呼ばれるようになってるんだけど、なんだろこれ?
constructor
処理を順にみる。
自身をグローバルに追加
global.atomApplication = this
AtomApplication
はいろんなとこで参照するらしい。
3つのクラスのインスタンス化
以下をインスタンス化してた。
AutoUpdateManager
(src/browser/auto-update-manager.coffee
)ApplicationMenu
(src/browser/auto-update-manager.coffee
)AtomProtocolHandler
(src/browser/atom-protocol-handler.coffee
)
AutoUpdateManager
はAtomの自動更新を管理するクラス。ApplicationMenu
はグローバルメニューを管理するクラス。AtomProtocolHandler
はatomのURLスキーム(atom://
)を扱うクラス。必要になったら中身を追えばよさそう。
atomコマンドの複数回実行対応
@listenForArgumentsFromNewProcess()
メソッドを追って見る。
# Creates server to listen for additional atom application launches. # # You can run the atom command multiple times, but after the first launch # the other launches will just pass their information to this server and then # close immediately. listenForArgumentsFromNewProcess: -> @deleteSocketFile() server = net.createServer (connection) => connection.on 'data', (data) => @openWithOptions(JSON.parse(data)) server.listen @socketPath server.on 'error', (error) -> console.error 'Application server failed', error
コメントによるとatomコマンドは複数回実行できるようになってるらしい。2回目以降の起動は、初回の起動で生成したサーバーに情報送るだけして、クローズ(app.teminate()
?)されるらしい。open
メソッド中でローカルドメインソケットにつなごうとしてたのはこのためみたい。2回目以降はすでに起動しているサーバーにウィンドウを開いてもらっている。
chromiumのコマンドラインにスイッチを追加
app.commandLine.appendSwitch 'js-flags', '--harmony'
chromiumのコマンドラインにスイッチを追加は、app
モジュールのAPIでやってる。多分、rendererプロセスの方でES6使えるようにしてるんだと思う。
各種イベントハンドラの設定
@handleEvents()
大きく分けて3種類あった。
AtomApplication
インスタンスで監視するアプリ固有のイベント。イベント名はapplication:*
のような感じapp
モジュールのライフサイクル系のイベントipc
モジュールのイベント
ipc
モジュールはmainプロセスとrendererプロセス間の通信を行うためのモジュール。ここで、前回とばした2つのプロセスの違いを簡単にまとめておく(ほぼドキュメントの訳)。
mainプロセスとrendererプロセスの違い
mainプロセスはBrowserWindow
インスタンスを生成することにより、ウェブページを生成する。BrowserWindow
インスタンスは自身のrendererプロセスでウェブページを実行する。BrowserWindow
インスタンスが破棄されたら、対応するrendererプロセスも終了する。
あとmainプロセスはネイティブのAPIを呼べるみたい。rendererプロセスでは無理。でもipc
を使えばrendererプロセスを起点にmainプロセスを通してネイティブのAPIを呼べる。
新しいウィンドウを開く
@openWithOptions(options)
パスで開くか、URLスキームで開くかなどを判定してる。パスで開くとこはopenPaths
メソッド見ればよさそう。プロセスIDのを管理していて、同じパスで開いたらウィンドウが使いまわされる実装になってる。ちなみに-n or --new-window
オプションを使って起動すると同じパスでも新しいウィンドウで開ける。
とりあえず単純に新しいウィンドウを開く場合を追ってみる。
bootstrapScript ?= require.resolve('../window-bootstrap') resourcePath ?= @resourcePath openedWindow = new AtomWindow({locationsToOpen, bootstrapScript, resourcePath, devMode, safeMode, windowDimensions})
まずsrc/window-bootstrap.coffee
のパスをrequire.resolve()
で取得してる。(ここではじめてsrc/browser
以下以外のファイルがでてきた。)その後、取得したパスや各種設定情報を引数としてAtomWindow
のコンストラクタに渡してAtomWindow
クラス(src/browser/atom-window.coffee
)をインスタンス化をしてる。src/window-bootstrap.coffee
は名前からして、rendererプロセスの最初の方で実行するスクリプトなんだと思う。
AtomWindow
はコンストラクタでBrowserWindow
をインスタンス化してる。これが上で書いたmainプロセスにおけるBrowserWindow
インスタンスの生成にあたる。
src/browser/atom-window.coffee
EventEmitter
を継承したAtomWindow
クラスがある。BrowserWindow
クラスのラッパークラスみたいな立ち位置。
constructor
適当に処理を抜粋してみる。
BrowserWindow
インスタンスの生成
@browserWindow = new BrowserWindow options
インスタンスはbrowserWindow
プロパティで保持
AtomApplicationにウィンドウを追加
global.atomApplication.addWindow(this)
AtomApplication.addWindow(window)
でアプリケーションに自分自身をウィンドウとして追加してる。
AtomApplication.addWindow(window)
の中身。
# Public: Adds the {AtomWindow} to the global window list. addWindow: (window) -> @windows.push window @applicationMenu?.addWindow(window.browserWindow) window.once 'window:loaded', => @autoUpdateManager.emitUpdateAvailableEvent(window) unless window.isSpec focusHandler = => @lastFocusedWindow = window window.browserWindow.on 'focus', focusHandler window.browserWindow.once 'closed', => @lastFocusedWindow = null if window is @lastFocusedWindow window.browserWindow.removeListener 'focus', focusHandler
AtomWindow
インスタンスのwindow:loded
イベントのハンドラを設定してる。ハンドラの中でAutoUpdateManager
がでてきた。
AutoUpdateManager
に寄り道。
emitUpdateAvailableEvent: (windows...) -> return unless @releaseVersion? for atomWindow in windows atomWindow.sendMessage('update-available', {@releaseVersion}) return
Atomの更新が利用できる場合に、AtomWindow.sendMessage
でウインドウにupdate-available
メッセージを送ってる。
再度、AtomWindow
に戻る。
sendMessage: (message, detail) -> @browserWindow.webContents.send 'message', message, detail
BrowserWindow.webContents
を介して、rendererプロセスにメッセージを送ってる。renderer側のどこかでこのメッセージを処理してると思う。
各種イベントハンドラの設定
@handleEvents()
閉じたときとか反応しなくなったときなどのイベントを設定してた。
ロード設定をセットする
@setLoadSettings(loadSettings)
loadSettings
というローカル変数にはいろいろつめてた。
メソッドの中を追ってみる。
setLoadSettings: (loadSettingsObj) -> # Ignore the windowState when passing loadSettings via URL, since it could # be quite large. loadSettings = _.clone(loadSettingsObj) delete loadSettings['windowState'] @browserWindow.loadUrl url.format protocol: 'file' pathname: "#{@resourcePath}/static/index.html" slashes: true hash: encodeURIComponent(JSON.stringify(loadSettings))
さっそくBrowserWindow.loadUrl(url)
でhtmlをロードしてる。fileプロトコルでstatic/index.html
(macなら/Applications/Atom.app/Contents/Resources/app/static/index.html
)を指定してる。loadSettings
はJSONにしてURLエンコードしてURLのハッシュに入れるみたい。
コメントには、アプリの状態(windowState
)は巨大だから削ってると書いてある。確認のためAtomのDeveloper Tools(⌥⌘I)でURLハッシュをデコードして確認してみる。
decodeURIComponent(location.hash.substr(1)) "{"locationsToOpen":[{"pathToOpen":"/Users/sangotaro/work/atom"}],"bootstrapScript":"/Applications/Atom.app/Contents/Resources/app/src/window-bootstrap.js","resourcePath":"/Applications/Atom.app/Contents/Resources/app","devMode":false,"safeMode":false,"appVersion":"0.188.0","initialPaths":["/Users/sangotaro/work/atom"]}"
htmlのロードまできたので、mainプロセス側の初期化処理はだいたい終わりだと思う。初期化処理の最後にロード時間をコンソールに出力してた。
# src/browser/main.coffeeのstart関数の最後 console.log("App load time: #{Date.now() - global.shellStartTime}ms") unless args.test
mainプロセスでのコンソール出力はatom -f
で起動すると確認できる。mainプロセスを確認したいときに便利そう。
$ atom -f 2015-04-08 01:02:46.684 Atom[55711:507] App load time: 226ms