anti scroll

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

camel caseでもcssプロパティが設定できるようになりました

nehan.js 5.1.1(まだ開発版ですが)から、cssのプロパティをcamel caseでも指定できるようになりました。

どういうことかというと、ver5.1.0以前では、例えばfont-sizeを設定するとき、

Nehan.setStyle({"font-size":"1.6em"}); // chain-case

のようにしか書けなかったのですが、これが

Nehan.setStyle({fontSize:"1.6em"}); // camel-case

のようにも書けるようになりました。

ただしver5.1.0以前では、後者は反映されないので、注意して下さい。

クラス名やID名が短く書けるようになりました

これまで縦書き文庫でマークアップするとき、特定の表示を利用する場合、クラス名にnehan-という接頭辞を付けなければならなかったのですが、これが不要になりました。

例えば行末寄せは、これまでは

<p class="nehan-ta-end">行末に寄せる</p>

だったのですが、今後は

<p class="ta-end">行末に寄せる</p>

マークアップできます。

同様に、これまでnehan.jsのスタイル設定は、idセレクタもclassセレクタnehan-を付けるルールでしたが、これも不要になりました。

Nehan.setStyle(".nehan-foo", {color:"red"});
Nehan.setStyle(".foo", {color:"red"}); // 今後は"nehan-"の部分は不要に

マークアップは少しでも短いほうがいいですからね…

ちなみに今回の仕様変更によって、過去エントリーを修正する必要はありません。

過去の仕様でマークアップされていても、いままで通りに表示されます。

縦書き文庫のマークアップについての詳細は、マークアップヘルプを参照して下さい。

各ユーザーの作品を一覧できるアーカイブページを用意しました

ユーザーの作品を手早く一覧できるページを用意しました。

例えば、図書館アカウントのアーカイブページ

f:id:convertical:20150614213535p:plain

スマホからでも閲覧できるので、自作の一覧を手っ取り早く紹介するページとして便利かもしれません。

アーカイブページのURLは、以下のとおりです。末尾のところを、自分のユーザーIDに変えてください。

http://tb.antiscroll.com/archives/<あなたのユーザーID>

操作法ですが、作品のタイトルをクリックすると、ページ送りのビューアー画面に切り替わります。

f:id:convertical:20150614213555p:plain

シリーズラベルをクリックすると…

f:id:convertical:20150614213612p:plain

こんな風にシリーズの一覧が表示されます。

f:id:convertical:20150614213636p:plain

PC環境では割りと軽快に動くことを確認していますが、実際のモバイルの環境ではどうか。ちょっと心配です。

フィードバックがあれば、いつもどおりメールフォームやらツイッターにて受け付けています。

マークダウン記法をサポートしました

テキストをマークダウン記法で記述できるようになりました。

「その他の項目」の「入力方法」から設定できます。

f:id:convertical:20150602091733p:plain

でも実は「あんまり使われないかもしれないなあ」と思っています。

というのも、マークダウンで改行するには、空行をひとつ開ける必要があるからです。

それだと原稿用紙的なイメージと、直感的に異なるんですよね。

一応よく知らない人のために説明すると、例えば次のようなテキストは、マークダウンでは二行のテキストになりません。「あいうえおかきくけこ」と一行で表示されます。

あいうえお
かきくけこ

これを二行にするには

あいうえお

かきくけこ

と記述する必要があります。

ただし、次のように「ヘッダー」や「リンク」や「太字」などはマークダウンのほうが書きやすいのも事実なので、必要に応じて使い分けるのがいいと思います。

## H2の見出しテキスト

ここを**太字**にする。[リンク](http://google.com)を貼る。

ゼノブレイドをクリア

松方弘樹さんが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 // 最後に!
});