まだまだ機能は乏しいし、既読化も実装していません(テストでフィードがなくなっちゃうと痛いので後回し)が、ぼく自身、常用しているショートカットまでを実装できたので、ご紹介。
使っている npm パッケージ
- React:コンポーネントとかレンダリングとか
- react-modal:元記事表示用のモーダルを作る時に
- mousetrap:キーボードショートカットで
- request:API と通信
- lodash:なんやかんやでいつも使う
できること
- フィード一覧の取得、表示
- 選択されたフィードを読み込んで、記事一覧を表示
- 元記事の表示
- 最低限のキーボードショートカット
(まだ)できないこと
- 認証
- その他いっぱい
構成
main.js
app/
component/
feeds.js
items.js
html/
index.html
style/
style.css
app.js
src/
feeds.jsx
items.jsx
main.js がアプリのエントリーポイントになります。package.json からアクセストークンを取得してウィンドウを作っているだけですね。app/html/index.html がウィンドウに読み込まれます。
app/html/index.html は VirtualDOM のコンテナを作って、app/app.js を読み込んでいるだけです。コンテナは、サイドバーのフィード一覧と、フィードが読み込まれた際のアイテム一覧、元記事表示ようのモーダルになります。あとは CSS を読み込んだり。
app/app.js で実際にページを作っていきます。API を叩いているだけですね。mousetrap で r
キーをフィード一覧の更新に割り当てていますが、サイドバーをまるっと読み込みなおしています。たぶん React 的にもっとかっちょいい方法があると思いますが、今後書きなおすかもしれません。
app/component/feeds.js は、src/feeds.jsx からコンパイルされて作られます。サイドバーのフィード一覧ですね。mousetrap で a
と s
が割り当てられています。前と次のフィードへ戻ったり進んだりするショートカットです。
render の中で <ul> と <li> を作っているのですが、当初、<li> は別コンポーネントだったんですね。マウスオーバー・アウトした時やショートカットで移動する時、コンポーネント間のやりとりはどうするんだろう?って感じだったのですが、ドキュメントを見てみると、この書き方が一般的なようで書き直しました。
render: function(){
return (
<ul>{this.props.feeds.map(function(item, index){
return (
<Feed item={item}/>
);
}, this)}
</ul>
);
}
これが、
render: function(){
return (
<ul>{this.props.feeds.map(function(item, index){
return (
<li key={item.subscribe_id}
onMouseOver={this.doMouseOver.bind(this, index)}
onMouseOut={this.doMouseOut.bind(this, index)}
onClick={this.doClick.bind(this, index)}>
<img src={item.icon}/> {item.title} ({item.unread_count})
</li>
);
}, this)}
</ul>
);
}
こうなりました。
書いてみると、なるほどね、と納得です。クリックやマウスオーバー・アウトのイベントが一つ一つのコンポーネントに貼られてしまうし、this.props.feeds
があるので他のフィードを意識できるしで、コンポーネントを分けすぎるのもダメですねって感じでした。
あとは、フィードをクリックすれば記事のアイテムが右ペインに読み込まれます。ほぼ LDR と同じですね。
app/component/items.js は、src/items.jsx からコンパイルされて作られます。記事のアイテムがずらーっと読み込まれます。mousetrap で j
k
v
n
が割り当てられています。記事の移動と元記事の開閉になります。なんでぼくが LDR のアプリ化をしたかというと、ほぼ、ココのためになります。ブラウザだと、元記事開くとタブが開かれるんですよね。タブを移動したくないのです。このアプリでは v
で react-modal を使って、モーダルで元記事を開きます。LDR にはありませんが、n
で元記事のモーダルを閉じます。
app/component/feeds.js と同じように子コンポーネントを作っていたのですが、ここも書き直しました。記事の移動のためですね。フィードの移動はクリックイベントを偽装していたのですが、記事の移動は scrollIntoView
を使っています。
元記事の表示には Electron の <webview> を使っています。ここ、React って楽だなあと思ったところです。src
は this.state.url
になっていますので、this.setState({ url: '元記事の URL' });
してあげるだけで表示する元記事を切り替えられます。とっても楽!モーダルの開閉も this.state
使っていますね。ホント楽!
render: function(){
return (
<ul>{this.props.items.map(function(item, index){
return (
<li id={item.id} key={item.id}>
<p style={style.title} onClick={this.doOpen.bind(this, index)}>{item.title}</p>
<div dangerouslySetInnerHTML={{__html: item.body}}/>
</li>
);
}, this)}
<Modal isOpen={this.state.modalIsOpen}>
<div style={style.close}><button onClick={this.doClose}>閉じる</button></div>
<webview src={this.state.url} style={style.browser}></webview>
</Modal>
</ul>
);
}
うごかしかた
まずはソースコードをチェックアウトしてきましょう。
$ git clone git@github.com:k0sukey/Electron-LDR.git
$ cd Electron-LDR
で、npm パッケージをインストールします。
$ npm install
認証ができていないので、http://api.ma.la/reader.html からアクセストークンをもらってきます。authorize して、インスペクタ等でローカルストレージにあるアクセストークンを package.json にコピペしてください。有効期限が切れたらもう一度 authorize すれば OK です。
{
"access_token": "****************************************",
"token_type": "Bearer",
"expires_in": 7200,
"created_at": 1442133518
}
↑の access_token を、
{
"name": "Electron-LDR",
// 略
"devDependencies": {
"electron-packager": "^5.0.2",
"electron-prebuilt": "^0.31.0",
"gulp": "^3.9.0",
"gulp-babel": "^5.2.1",
"gulp-load-plugins": "^0.10.0"
},
"token": "ここにコピペしてください"
}
コマンドラインで起動します。
$ npm start
まとめ
あらためて、Electron と React って親和性高いなって思いました。あと、自前で認証できないのは致命的ですね...。何とかしないと。