anti scroll

ブラウザと小説の新しい関係を模索する

ゼノブレイドをクリア

松方弘樹さんが6時間半の格闘の末に361キロのマグロを釣り上げた今日、自分はゼノブレイドを100時間程度の格闘の末にレベル87でクリアしたのでした。

ところでネタバレにならない程度に紹介しておくと、ゼノブレイドには「重装備」と呼ばれる、ちょっと格好いい防具がありまして。

しかしなぜか身に付けることが出来ないのです。

これを「いずれマッチョが仲間になるんだろう」と勝手に決めつけ、ずっと未来の仲間を想像しながら抱え込んでいたわけですが、そのままエンディングを迎えてしまい、途方に暮れたのでした。

その後「あの装備はなんだったんだろう」と攻略wikiを見たら「重装備の心得」というスキルを身につける必要があったらしく。

しかもそのスキルは、クエストなどを通じて「キズナ」というのを深めて修得するものらしく。

しかし自分の場合、クエストについては「全て引き受け、なにも解決しない」というスタンスでストーリを進めており、キズナもなにもあったもんではなかったのでした。

ともあれゼノブレイド神ゲーの名にふさわしい超傑作でした。

事前にネタバレしないでプレイして本当に良かったです。

Rx(Reactive Extensions)とVirtual DOMで作るリアクティブなUI、およびMVARアーキテクチャについて

元ネタはReactive MVC and the Virtual DOMなのですが、MVIという言葉がしっくりこなかったので、自分なりに消化した結果、こういう枠組みになりました、という話しです。

MVARアーキテクチャ概要

まず頭文字の意味についてですが、

M = Model
V = View
A = Action
R = Renderer

です。ついでに各レイヤーの役割に大雑把な型らしきものを付けると、

Action :: native event -> event Stream
Model :: event Stream -> model Stream
View :: model stream -> virtual-dom Stream
Renderer :: virtual-dom Stream -> html

のような感じになります。

ちなみにActionはViewとModelの双方に参照されます。

ViewとModelが直接つながると、ViewがModelを参照し、ModelがViewを参照する循環参照になってしまうからです。

追記:後からわかりましたが、ActionとModelを1つのレイヤーにまとめても問題なさそう、というかそっちのほうが良いかも?

大雑把な処理の流れ

全体的な処理の流れとしては、

  1. Actionはユーザー操作などで発生したネイティブ・イベント(クリックとか)をイベントストリームに変換する。
  2. Modelはイベントストリームを、モデル状態のストリームへと合成変換する。
  3. Viewはモデル状態のストリームを、VirtualDOMのストリームに変換する。
  4. Rendererは、VirtualDOMストリームを購読して、直近2世代を比較した上で差分だけを画面更新する。

概念コードを書くとこんな感じ(変数の末尾に付く「$」は「ストリーム」の「S」だと思って下さい)。

var initial_state = {title:"no title"};
var action = new Action();

// action -> model$
var model$ = new Model(initial_state).observe(action);

// model$ -> vdom$
var vdom$ = new View(action).observe(model$);

// vdom$ -> html
Renderer.render(vdom$);

Action

ActionはUIから発生したネイティブなイベントを受け取り、それをストリームとして保持するレイヤーです。

var Rx = require("rx");

function Action(){
  this.keyupTitle$ = new Rx.Subject();
};

Action.prototype.onKeyupTitle = function(ev){
  this.keyupTitle$.onNext(ev);
};

細かくは述べませんが、実はRxのSubjectやbacon.jsのBusを使うのは良くないマナーとされています。上のkeyUpTitle$も、本当はEventEmitterとRx.Observable.fromEventを使って作るのが良いのですが、ここでは簡単のためSubjectを使ってます。

Model

Modelは、Actionが保持するイベントストリームを合成し、現在状態を表すストリームを出力するレイヤーです。

var Rx = require("rx");

function Model(initial_state){
  this.initialState = initial_state || {};
}

Model.prototype.observe = function(action){
  var title$ = action.keyupTitle$.map(function(ev){
    return ev.target.value;
  }).startWith(this.initialState.title);

  return new Rx.BehaviorSubject(this.initialState)
     .combineLatest(title$, function(model, title){
       return {title:title};
     });
};

View

Viewは、Modelが合成した状態ストリームを、virtual domのストリームに変換するレイヤーです。

var h = require("virtual-dom/h");

function View(action){
  this.action = action;
}

View.prototype.renderTitle = function(title){
  return h("input", {
    type:"text",
    value:title,
    "ev-keyup":function(ev){
       this.action.onKeyupTitle(ev);
    }.bind(this)
  });
};

View.prototype.renderLabel = function(title){
  return h("label", title);
};

View.prototype.observe = function(model$){
  return model$.map(function(model){
    return h("div.output", [
      this.renderTitle(model.title),
      this.renderLabel(model.title)
    ]);
  }.bind(this));
};

Renderer

Rendererは、Viewが出力したVirtualDOMストリームを購読し、最新のHTMLを出力するレイヤーです。

var h = require("virtual-dom/h");
var patch = require("virtual-dom/patch");
var diff = require("virtual-dom/diff");

var Renderer = {
  render:function(vdom$){
    var $app = document.querySelector("#app");
    var $root = document.createElement("div");

    $app.innerHTML = "";
    $app.appendChild($root);

    $vdom
      .startWith(h()) // 最初は<div></div>だけ
      .bufferWithCount(2,1) // 二世代分をバッファ(1,2) -> (2,3) -> (3,4) etc
      .subscribe(function(vdom2){ // vdom2.length は2
        var patch_obj = diff(vdom2[0], vdom2[1]); // 差分を取る
        $root = patch($root, patch_obj); // 差分だけ更新
      });
  }
};

AppMain

AppMainはアプリケーションのエントリポイントです。

ここでMVARの各レイヤーを初期化し、ストリーム同士を結びつけてあげます。

あとはユーザーが操作してイベントが発生すると、自動的に差分だけが画面更新される、という運びです。

var DOMDelegator = require("dom-delegator");

var App = {
  start:function(){
    var delegator = new DOMDelegator(); // おまじない。virtual-domの'ev-***'を動作させるために必要
    var action = new Action(); // actionはviewとmodelの双方に参照される(view -> action -> modelなので)
    var model$ = new Model({title:"no title"}).observe(action); // (action, initial state) -> model$
    var vdom$ = new View(action).observe(model$); // (action, model$) -> vdom$
    Renderer.render(vdom$);// vdom$ -> html
  }
};

まとめ

FluxではDispatcherがグローバルなインスタンスのような形になりますが、MVARではActionがそれに該当します。

Fluxだと、View -> Action -> Dispatcher -> Model(Store) -> View

MVARだと、View -> Action -> Model -> View

ようするに、Dispatcherのレイヤがなくなってる状態ですね。

以上を踏まえた上で、長所と短所ですが…

長所

  • virtual-domはreact.jsより高速(らしい)
  • viewが状態を持たない(setStateとかgetInitialStateとかがない)
  • Dispatcherのレイヤがなくなり、全体の構造がより簡素化されている
  • 状態とビューがストリーム処理で閉じられているため、リアクティブなUIが作りやすい

短所

  • 元記事に指摘されている通り、ActionがModelとViewの双方に紐付いてしまうため、それぞれのレイヤーの再利用や独立が難しい

リアルタイム・プレビューに対応した新しい投稿フォームをリリースしました

久しぶりの機能アップデートですが、リアルタイム・プレビューに対応した新しい投稿フォームをリリースしました。

投稿フォームの上部にある、以下の案内リンクからお試しいただけます。

f:id:convertical:20150524105126p:plain

まだ試験段階で古いブラウザやPCだとどうなるのかわかりませんが、ご意見ご感想ありましたらメールフォームやらtwitterなどでお聞かせいただけるとありがたいです。

Matt-Esch/virtual-dom覚え書き

こちらの記事で知ったのですが、Reactで言うところのpropsしかないvirtual dom実装とのこと。

github.com

まさに探していたものなので、さっそく試してみたのですが、以下は覚え書きです。

escapeさせたくない中身はattributesinnerHTMLを設定

inlineなタグでも、そのまま設定するとエスケープされてしまうので。

h("div", "<b>escaped!</b>"); // タグ文字はエスケープされる
h("div", {innerHTML:"<b>not escaped!</b>"}); // innerHTMLなら、そのまま出力される

datasetはdata-xxじゃなくて、dataset:{xx:10}

attributesに直に設定しても動きません。dataset属性にオブジェクトとして設定します。

h("a", {"data-id":10}, "click me"); // NG
h("a", {dataset:{id:10}}, "click me"); // OK

差分パッチの対象から外す要素はtype:"Widget"

中身のDOMElementはinitコールバックで出力します。

h("div", {
  type:"Widget",
  init:function(){
    var dom = document.createElement("div");
    dom.innerHTML = "my custom element!";
    return dom;
  }
});

ev-xxxDOMDelegatorインスタンス化していないと動かない。

attributeにev-(event名)で書きますが、ソースのどこかでnew DOMDelegator()していないとイベント通知は動きません。

var _delegator = new DOMDelegator(); // ev-*を使うには、このインスタンス化が必要
h("a", {
  "ev-click":function(ev){
    alert("clicked!");
  }
});

すこし気になったのは、ev-clickreturn falseしても、クリックの戻り値として反映されないところです。

inlineスタイルの書き方

style内部もオブジェクト(camel-case)で書く。

// NG!
h("span", {style:"font-size:0.5em"}, "small!");

// OK
h("span", {style:{fontSize:"0.5em"}}, "small!");

attribute(properties)の書き方

hrefとかはそのままでいいけど、labelタグのforとかはhtmlForとかだったりする。

h("a", {href:"yahoo.co.jp"}, "yahoo!"); // OK
h("label", {for:"foo!"}, "label for foo!"); // NG!
h("label", {htmlFor:"foo!"}, "label for foo!"); // OK

inputのvalue属性は最後に書く

virtual-domのinput要素を作成する時は、value属性は最後に記述する。

そうしないと、表示する際にレースコンディションが発生して、正しくvalueが反映されないことがあることが知られている。

Input type range control has wrong value on initial render · Issue #228 · Matt-Esch/virtual-dom · GitHub

// NG: 時々valueが正しく反映されないことがある
h("input", {
  type:"range",
  value:200,
  min:10,
  max:100
});

// valueは最後に書く
h("input", {
  type:"range",
  min:10,
  max:100,
  value:200 // 最後に!
});

nehan.jsのデモページをReact/Fluxで作ってみた

必然性は全くなかったのですが、Fluxを試してみたかったので作ってみました。

http://tb.antiscroll.com/static/nehan-demo/

使ってみた感想

あんまり感触は良くないかも…

ただ今はなんとなく全体像を掴んだかもっていう段階なので、もっと複雑なUIを作るとなったら、こういうアーキテクチャが生きてくるケースもあるのかも? しれません。

2005年にアジア〜中東〜北アフリカ〜ヨーロッパを旅行したロバ中山さんの旅行記がすごい

2005年、つまり今からちょうど10年ぐらい前、アジア〜中東〜アフリカ〜ヨーロッパを旅行したロバ中山さんの旅行記がすごく面白いです。*1

http://www.sakaguti.org/honmon%20page/top%20page/top%20page.htm

今は大変な状況になってしまったシリアやイエメンにも行ってますし、イラン、ヨルダンイスラエルにも行ってますね。

面白くて二度読みしている最中なのですが、現代の中東情勢を伺い知るには、もってこいの内容じゃないでしょうか。

ただし、そのままだと読みづらい気がしたので、自分は NehanReader で次のような設定をして読みました。

  1. 「涅」ボタンを右クリックして「オプション」
  2. 「table of selectors to main article」の欄に、以下の一行を追加。
www.sakaguti.org, td[width="491"], table>tbody>tr>td

こうして設定した後、NehanReader で読むと、こんな感じで読めます。

f:id:convertical:20150424202024p:plain

非常に長い日記ですが、最初から読む必要はなく、興味がある国だけ読んでも面白いと思います。

それにしてもイエメンのヤヒヤさんやアミンさんは無事なのだろうか…と思ってしまいますね。

[2014/02/14 追加]

現在サイトは閉鎖されてしまったようですが、以下の本などで同じような内容が読めるようです。

*1:2017/02/14 現在は閉鎖されています。

Nehan Readerアップデート。サイトごとに変換対象を設定できるようになりました

Nehan Reader ver0.9.63から、サイトごとに変換対象を設定できるようになりました。

chrome.google.com

設定の仕方

「涅」のボタンを右クリック→「オプション」と進みます。

f:id:convertical:20150419133103p:plain

「table of selectors to main article」の欄に

[siteのURL], [selector]

を改行で区切って記述し「Save」ボタンで保存します。

サンプル設定

www3.nhk.or.jp, #news
toyokeizai.net/articles, #article-body
www.asahi.com/articles, #Main
togetter.com, .contents_main
ncode.syosetu.com, #novel_contents
lifehacker.jp, article
gigazine.net, #maincol
blog.livedoor.jp, .article-outer
news.yahoo.co.jp, #main
headlines.yahoo.co.jp, #main
hatenablog.com, #main
hatenablog.jp, #main
hateblo.jp, #main
hatenadiary.com, #main
hatenadiary.jp, #main
anond.hatelabo.jp, .day
www.huffingtonpost.jp, article.entry
wired.jp, .article_maincontents

例えば上の内容をコピペするだけで、ヤフーニュースを始めとするニュース記事や、小説家になろうの小説、はてなブログの記事などが読みやすくなります。

また変換するのがメインの文章だけになるので、高速にもなります。

css framework上の全てのセレクタに特定のprefixを付ける

chrome拡張とかでも、普段使っているcssフレームワークが使いたくなることがあります。

しかし大抵のcssフレームワークは、グローバルな名前空間でスタイルを宣言しています。そのまま導入すると、拡張機能CSSが読み込まれてしまった結果、訪問したサイトの本来のスタイルを崩してしまうことでしょう。

もちろん拡張機能を特定のサイトでだけ動くようなポリシーにすれば防げます。しかし、NehanReaderのように、全てのサイトで使えるようにしている拡張機能も、たくさんあると思います。

そういう場合、フレームワークで宣言されている全てのセレクタに、強引に何かのプレフィックスを足してしまえば良いわけですが、手動でやるのは流石にキツイ、というか無理です。

だから「なんか良いツールないかなあ」と探して見つけたのがrework-mutate-selectorsです。

github.com

インストール

今回はgulpを使いたかったので、gulp-reworkも一緒に導入しました。

npm install rework
npm install rework-mutate-selectors
npm install gulp-rework

gulpやgulp-renameがない人は、先にインストールしておいて下さい。

使ってみる

var gulp = require("gulp");
var rework = require("gulp-rework");
var selectors = require("rework-mutate-selectors");
var rename = require("gulp-rename");

gulp.task("default", function(){
  return gulp.src("framework.css")
    .pipe(rework(selectors.prefix(".my-module"))) // prefixに".my-module"を追加
    .pipe(rename("my-module.framework.css"))
    .pipe(gulp.dest("."));
});

こうすると例えば

/* framework.css */
div a{ color:red }

が、

/* my-module.framework.css */
.my-module div a{ color:red }

と出力されます。

結論

一瞬、自分で作ろうかとも考えましたが、諦めずに探して良かったとです。