やはりプレビューが使えないと色々と不便なので、プレビューボタンを復活させました。
投稿ボタンの下に「プレビュー」ってボタンがありますので、使ってみてください。
プレビューを押すと、
こんな感じで、簡易ビューアーが表示されます。
プレビューボタンが表示されない方は、リロードするか、それでも駄目ならタブを閉じてから開き直してみてください。
やはりプレビューが使えないと色々と不便なので、プレビューボタンを復活させました。
投稿ボタンの下に「プレビュー」ってボタンがありますので、使ってみてください。
プレビューを押すと、
こんな感じで、簡易ビューアーが表示されます。
プレビューボタンが表示されない方は、リロードするか、それでも駄目ならタブを閉じてから開き直してみてください。
縦書き文庫をリニューアルしました。
これまではスマホの投稿画面ではルビや太字ぐらいしか選べなかったのですが、すべてのボタンがPCの投稿画面と同様に使えるようになりました。
PCだとこんなふうで、
スマホとかだとこういう感じです。
これまでは10個までの制限があったのですが、いくらでも挟めるようになりました。
以下は、しおりを入れるボタン
以下はしおりを開くボタン
しおりを開くと、しおりの一覧が出るので選択します。
ちなみに、しおりの数に制限はありませんが、一つの作品に大して10個以上の栞を挟んだ場合、直近の10件までが表示されます。
スマホ等のタッチで操作する端末に親和性が高い(とされている)マテリアルデザインを採用しました。
色々な操作がアニメーションするので、操作がより直感的になったのではないでしょうか。
シリーズ作品の並び替えも、より直感的にドラッグ・アンド・ドロップで可能になりました。
ダッシュボードページを新設し、作品を読んだ、読まれたの履歴がパッと確認できるようになりました。
「読まれた」のタブには、自作がページ送りされてポイントが入った履歴が出ます。
「読んだ」のタブには、自分が他者の作品を読んで得たカルマの履歴が出ます。
利用者があまりいないのと、サーバーへの負担を考慮して廃止となりました。
一部の古いコードは動くかもしれませんが、動作は保証されません。
スマホの横幅に足りないというのが主な理由なのですが「ファン」についてはPC限定で復活するかもしれません。
廃止したのは、表紙の有無が作品のクリック率にあんまり寄与しない(ように見える)ほか、著作権の確認なども大変だったからです。
賛否両論あるかもしれませんが、表紙画像をなくすことで、サイトの読み込みも速くなるので、自分としては良かったかなあと思っています。
去年の年末にPCがぶっ壊れてしまったのですが、奇跡的にデータが残ったまま修理が完了したので、そのまま最後まで開発することができました。
自分にとっては今回が人生初Angular(とNgrx)だったのですが、すごく開発しやすかったです。
Angular+Ngrxでは、状態をObservableで閉じることができるので、論理的に矛盾した状態が作られにくい、というところが素晴らしいと思いました。
先月から試験的にAngular+Ngrxを使って縦書き文庫を作り直してみる、ということをしていたんですけど、パソコンが壊れバックアップも取ってなかったので、全て消え去りました(この記事はChromebookで書いてます)。
真面目にやると一年ぐらいかかる作業なので、一ヶ月程度の作業で済んだのは不幸中の幸い?なんでしょうかね。。。
Angular+Ngrxの感想としては、コツを掴むまでかなり時間がかかるということと、fluxなんでaction/reducer/state/effectの開発ループを回すのが面倒くさいなあ、ということです。
正直なところ「このまま続けるべきか?」と思ってたところなんで、もしかしたら中止できてよかったのかも。。。
TypescriptとRxに関しては多少ですが経験があったのでなんとかなりそうだったのですが、それ以外に覚えないといけないことが沢山あって大変でした。
よくAngular+Ngrxは大規模なチーム開発に最適などと言われますが、学習コストを嫌ってVueという選択をする人が多いのも、なんとなくわかるような気がします。
Ngrxを採用すると、とにかくRxでEffectsを書きまくることになるのですが、ここで割とわかりにくい地雷を踏みまくってしまいました。
特に危険なのはストリームを止めちゃう位置にcatchErrorを書いちゃったり、逆にストリームを止めるべきところでdispatch:falseを指定しないで無限ループを仕込んじゃったりとかです。
まずEffects中のcatchErrorですが、actions$直下のpipeではなく、map中のpipeで宣言しないといけません。
@Effects() getFoo$ = this.actions$.pipe( ofType("foo"), switchMap(action => return fetchFoo(action.payload.id).pipe( map(result => new GetFooDone(result.body)), // エラーは内側のpipeで補足する catchError(error => of(new GetFooFail({error}))); ); ), // ここに書いてはいけない // catchError(error => new GetFooFail({error})) );
上の例は、foo
に対応するactionを直下のactions$からフィルタリングし、そのデータを元にfetchFoo
でサーバーから何かしらのデータを取得しようとするコードです。
この処理の失敗を、action$直下のcatchErrorで補足すると、fetchFoo
でエラーが起きたときだけじゃなく、大本のactions$で起きたエラーも補足してしまいます。
上の例だと、GetFooFail
というアクションを出したいのは、fetchFoo
が失敗したときだけですよね。
気をつけましょう。
Effectsが新しいActionを返さないのにdispatch:false
をデコレーターに設定しないと、永遠にその処理が繰り返されます。
@Effect({dispatch: false}) logFoo$ = this.actions$.pipe( ofType("foo"), tap(_ => console.log("foo")) );
上の処理で{dispatch: false}
を忘れると、無限ループです。
自分はCPUがうるさくてパソコンが熱くなったので気づきました。自動ビルドで開発してたのですが、開いてる先のタブがいつのまにかCPU使用率100%になっていたりとか。
これがパソコンの熱暴走を起こし、寿命を縮めたんじゃないのか、と今では疑っています。
ngrx/storeの状態をangular materialのコンポーネントのディレクティブにダイレクトに反映させてしまうと、アニメーションが効かなかったり(例えばチェックボックスとか)、一部のUI要素が正しく計算できなくて表示できなくなったり(例えばタブコンポーネントの下線)みたいなことがあります。
なのでマテリアルコンポーネントを使用するときは、コンテナーというUIクッションを用意して、マテリアル要素はその下に置くようにします。
で、マテリアルなコンポーネントを含む下層のコンポーネントには「状態のコピー」を渡し、ローカルでその状態を操作させるようにします。そうしないと、アニメーションが正しく機能しないからです。その上で、子供の最新状態は@Output
を使ってコンテナーに吸い上げます。
つまり一元管理された状態を使用して制御できるのはコンテナーUIまでで、そこから下は状態のコピーが作成されることになります。全てが理想通り、とはいかないですね。
ちなみに壊れたMacbookの修理はざっくり7万ぐらいとか言われたんですけど、まあ買い直すよりかは、と思って依頼しちゃいました。
でも年末で忙しいから14営業日かかるとのこと。。。今からだと来年の10日前後?
そういえば最近、中国の開発者が「Immutableなんてほとんどのケースで必要ない。本当の未来はshadow-domを使い、素直にmutableな構造で、かつVueっぽいものだ」みたいなコンセプトでなんかフレームワーク作ってて、すごいスターをもらってた記憶があるのですが、名前を忘れました。
ネットでExpressionChangedAfterItHasBeenCheckedError
を検索すると、よく「子が親の状態を変更するとビューに一貫性がなくなることが警告される」という説明を目にしますが、少し混乱を招きかねない説明なんじゃないかなあと思いました。
正確には「Angularが変更検知をする際に行う探索順において、後からチェックされるビューから、先にチェックされるビューの状態を変える処理を書くと警告される」と説明すべきなのではないでしょうか。
なぜなら、HTMLには親子関係だけではなく、隣接(兄弟)関係というものがあるからです。親が子より先に探索されるのは当たり前ですが、隣接関係については自明ではありません。
例えば次のようなツリーがあったとします。
A, B | C
AとBは隣接(sibling)関係で、Cの親はAです。このときCの親であるAがCより先に探索されることは自明ですが、Bはどうでしょうか。Cより先でしょうか、後でしょうか。
もしAngularの変更検知が「幅優先探索」の順で行われるなら、Bが先です。しかし実際のAngularの変更検知順は「深さ優先探索」なので、検知される順に番号をふると、次のようになります。
A(1), B(3) | C(2)
つまりBは、Cよりも「後に」変更検知されるわけです。
だから探索順に置いて最後になるBにおいて、Cが依存するような状態を変更すると(先に確定されたCの状態を後からBが変えることになるので)ExpressionChangedAfterItHasBeenCheckedError
というやつが出ます(ただし警告してくれるのは開発ビルドのときだけ)。
よくウェブで見る「親と子」という説明の仕方だと、幅優先か深さ優先かで受け取る人の解釈が曖昧になるので「探索順」を使った説明をしたほうが親切なのではないでしょうか。
書いておかないと忘れちゃいそうなので、残しておきます(たまに追記するかも)。
これが便利なのは、こういう感じのコードにおいてです。
class Foo { say(){ console.log("foo"); } } let isFoo = (val: any): boolean => { return val instanceof Foo; }
このisFoo
を使って、ある値の型がFoo
である場合に(Foo
のメソッドである)say
を呼ぶ、といった処理を書きたくて
let func = (val: any) => { if(isFoo(val)){ val.say(); // val は any なのでエラー } }
などとやると、コンパイル・エラーが出ます。上のコードでは(当たり前ですが)val
の型がany
とみなされるからです。
しかしこれでは、せっかくFoo
であることを確認するisFoo
関数の意味がなくなってしまいます。
しかしこの問題は、isFoo
を、次のように書き換えることで解決します。
let isFooEx(val: any): val is Foo { return val instanceof Foo; }
戻り値の型の部分に記述されたval is Foo
の働きによって、引数のval
の型は呼び出し元にFoo
だよ、と認識され、次のコードが通るようになります。
let func(val: any){ if(isFooEx(val)){ val.say(); // val は Foo なのでOK! } }
このval is Foo
の部分のことを、Type Guard と呼ぶのだそうです。
詳しくは、Type Guards and Differentiating Types を参照して下さい。
TypeScript 2.3からですが、次のような感じで型パラメータにデフォルト値を設定できるようです。
class Component<Props = any, State = any> { props: Props; state: State; } type StateActionPair<T, V extends Action = Action> = { state: T | undefined; action?: V; };
二番目の例にあるV extends Action = Action
の部分は、Action
の部分型であるV
のデフォルト型はAction
である、という意味です。
これまでこういったことを知らなくて、色々と汚いコードを書いてました…反省。
null
が入り得る型なんだけど、そのチェックがいらないことが明らかな場合、コンパイラにnull
チェックを無視させることができます。
let foo = (entity?: Entity): string { return entity!.name; }
entity!
という部分がそれです。
上のプログラムにおいてentity
という引数は、Entity
型もしくはnull
もしくはundefined
になる恐れのある引数だ、と言ってるので、普通にentity.name
と書くと、コンパイルエラーになります(ただし--strictNullChecks
が有効時)。
しかしこういうnull
などになり得る変数のあとに!
をつけると「ここは特別にnull
にならないからチェックしなくて良いよ!」という意味になります。
これも使えると時々は便利です。
参考:Non null assertion operator
例えばこんなオブジェクトがあるとして
const obj: any = { foo: { bar: 'bar' } };
次のようにアクセスするとエラーになります。
const value = obj.bar.bar // Cannot read property 'bar' of undefined
こういう木構造の途中がundefined
かもしれないものにアクセスするときに、いちいち
const value = obj.bar? obj.bar.bar : undefined;
みたいに書くのは面倒ですよね。しかし?
演算子を使うと、これを
const value = obj.bar?.bar;
というように、短く書くことができます。
仮にobj.bar
がundefined
でも、obj.bar?
で評価が止まり、全体がundefined
になるわけですね。
2018年9月にようやく正式リリースされた opam2。
公式が提供するスクリプトでインストールできれば問題ないのですが、それだとうまくいかない環境もあり、色々とインストールが面倒だったのでメモしておきます。
ちなみに以下の内容は次のスクリプトで成功する人には何の価値もない内容ですのでご注意ください。
sh <(curl -sL https://raw.githubusercontent.com/ocaml/opam/master/shell/install.sh)
で、どうやってインストールしたかというと、自分の場合はソースからインストールしました…
ocamlbrew
なる便利そうなのもあるのですが、ここ一年ぐらい更新されておらず、opam2が正式リリースされたのが先月(2018年9月)というのも考慮し、今回は見送りました。
なおopam2は4.02.3
以降じゃないとビルドできませんので、先に
opam switch
とやって表示される中で、4.02.3以降のコンパイラに変更する必要があります(もちろんこれは古いままのopamで実行しても問題なし)。
自分は素直に現時点での最新バージョンを入れました。
opam switch 4.07.0
コンパイラのバージョンに問題がないなら、opam2.0.0のソースディレクトリに入ってビルドします。
./configure make lib-ext make sudo make install
make
するまえにmake lib-ext
が入ることに注意してください。
ちなみに最後のsudo make install
なのですが、ホームディレクトリの.opam/<ocamlのバージョン>/bin/
以下にあるツール(jbuilder
やocamlfind
)を使うので、普通に実行しようとすると、おそらくrootがそれらを見つけられずにコケます。
よってsudoer
を編集します(他にもっといい方法が絶対にありそうなのですが、思いつかなかった…)。
visudo
を起動して
Defaults env_keep += "PATH"
を追加して、
#Defaults secure_path = /sbin:/bin:/usr/sbin:/usr/bin
をコメントアウト。ついでに.bashprofile
などに
PATH:=$PATH:~/.opam/4.07.0/bin export $PATH
みたいなのも追記しておきましょう(終わったらsource .bash_profile
も忘れずに)。
これでローカルのjbuilder
やらocamlfind
やらのパス解決もできて、sudo make install
が無事に通ります。
さて、インストールが終わると、新しいopamを利用するためのセットアップ処理が走るのですが、途中でbubblewrap
という、サンドボックス環境を使うための仕組みを設定するかどうかを聞かれます。
セキュリティーを考えたら使ったほうが良さそうなのですが、後で調べてみたら自分の環境ではインストールできないツールだったので、インストール後に.opam/config
から以下のbubblewrap
に関する設定を削除しました。
# 以下の3つの設定を消す wrap-build-commands: ["%{hooks}%/sandbox.sh" "build"] {os = "linux" | os = "macos"} wrap-install-commands: ["%{hooks}%/sandbox.sh" "install"] {os = "linux" | os = "macos"} wrap-remove-commands: ["%{hooks}%/sandbox.sh" "remove"] {os = "linux" | os = "macos"}
ちなみに理由はよくわかりませんが、先の質問に「No」と答えても、上の3つの設定は.opam/config
に追加されてしまうので注意してください。
セットアップ用の質問に答え終わると、すごく長い「何かしらの処理」が始まります。
途中経過とかが全く出てこないので「え、フリーズ?」と心配してしまうかもしれませんが、気長に待ってたらちゃんと終わるので、安心して下さい。
さて、初期化の処理が終わると、これまでopam1.2系列を使っていたことで止まっていたopam upgrade
が、また進むようになります。
拙作のjinja2互換テンプレートエンジンであるjingooも、opam1.2系列では1.2.18
までしかインストールできませんが、opam2にすると1.2.19
以降もインストールできるようになります。
公式の説明を見る限り、もうopam1.2系列はサポートしないらしいですし、既に新しいパッケージを公開する際は、opam2.0系列を強制されてしまいます。
これで先月はopam-repository
への新パッケージの申請が軒並みコケていて、大混乱でしたね。自分もその一人でした。
バージョン2系列はバージョン1系列のopamファイルと互換性がなく、微妙にフィールド名が変わっていたりするので注意が必要です。
自分の場合は、とりあえずopam-repositoryにopam1のままのpull requestを投げてみて、弾かれたらエラーログを見ながら直す、みたいにして、体で覚えました。
nehan version6 を公開しました。
version6はnehan.js
ではなく、nehan
という名前で開発することになりました。
それに伴い、リポジトリのURLなども変わっているので、ご注意ください。
npm install nehan
Typescriptなら、
import * as Nehan from "nehan";
let Nehan = require("nehan");
過去バージョンのnehanもそうでしたが、古いnehan.jsとの互換性はまったくありませんので、ご注意ください!
あと古いブラウザはざっくりと切り捨ててしまったので、動かないかもしれません。
ちなみに縦書き文庫については、古いブラウザも当面サポートしなければならないので、旧バージョンのnehan.jsのまま動かしていますが、最新のNehan Reader
は、このversion6を使って動かしています。もし良かったら、試してみてください。
投稿画面に「対象年齢」という設定欄を新設しました。
ジャンルとしてはホラーだけど、内容はR18みたいなこともあるからです。
ちなみにジャンルが「官能・BL小説」の場合は、自動で「成人向け」という設定で投稿されます。