mdbuf v1.0.0: 最高の Mardkown Preview を目指して

mdbuf, そこそこ使い物になりそうな品質になったので改めて紹介します。

https://markdown-buffer.netlify.com で遊べます。

コンセプト

  • ブラウザで完結
  • 編集とプレビューのみに注力
  • PWA 機能を最大限に活かす

特長: 高速な Markdown プレビュー

色々頑張ってみた結果、高速な入力が可能です。

試した限り、 100000 文字以上だと流石に重くなっていきます。将来的に領域を分割してレンダリングできないか実験中です。

Desktop PWA 対応

PWA アプリとして、オフラインで起動することが可能です。編集中のデータはブラウザ内に保存されます。

編集位置への自動スクロール

Markdown を編集すると、プレビュー側の対応する行へ自動的にフォーカスします。

自分が知る限りこの機能を実現してるのは mdbuf だけです。

アウトライン機能

指定したアウトラインまでジャンプします。

ブラウザ内 Prettier 対応

Cmd+S / Ctrl+Alt+F で入力中の markdown を prettier で整形します。

それによって、例えばこのようなテーブルも自動的に整形されます。

| a   | b   | c   |
| --- | --- | --- |
| 1   | 2   | 3   |

数式

KaTeX による数式レンダリングをサポートしています。

$ y = x ^2 $

コードハイライト

highlight.js でコードを装飾します。

ワードカウント機能

文字数を数えます。自分が執筆業が多いので付けた機能です。

英語だけ等幅に

日本語を書くときは等幅である必要はないんですが、コードを書く時にアルファベットで等幅が崩れるのが嫌だったのでそこだけ等幅になるようにしています。

インデント

Tab / Shift-Tab での簡易なインデントをサポートしています

今後やりたいこと

  • スマホ UI (縦分割) 対応
  • MonacoEditor/CodeMirror 対応
  • textlint 対応
  • 依存なし単一 html へのパッケージング
  • mdx 対応

@9m の作ってるエディ太郎みたいに、Markdown とインラインのコードのハイライトに MonacoEditor を使いたかったのですが、VSCode + Google IME は日本語入力に補完ボックスの制御に致命的な欠陥があり、この Issue 待ちです。

editaro.com

https://github.com/Microsoft/vscode/issues/45629

なぜ作ってるか

これは next-editor https://nedi.appMarkdown 編集機能を切り出して再設計したものです。

応援してくださる方がいましたら patreon もよろしくおねがいします。

www.patreon.com

他、何か要望などがあれば、 twitter @mizchi までお願いします。

今年お世話になったCLIコマンド集

ヒストリ履歴からよく使ってるものをお焚き上げする。

注意点: npm 周り、グローバルコマンドは npm i -g で入れてて、ローカルで扱うものは yarn で使うという癖がある

追記: シェルじゃなくてCLIだろと言われるのが多かったので訂正した

vscode

$ code . -r

現在ディレクトリを VScode で開く。

-r が肝で、新しいウィンドウを生成せず、既存のウィンドウを開き直す。

yarn

$ yarn install --prefer-offline

yarn install 時にローカルキャッシュを優先する。テザリング環境下でリポジトリを作成するのに便利。

フリーランスになってから出先で作業することが多く、ギガ足りない問題が多々発生した。

git

$ git clone <github-url> --depth 1

HEAD だけ clone する。テザリング環境下で便利

typescript

$ yarn tsc -p . --noEmit
$ yarn tsc -p . --noEmit -w # watch mode

TypeScript でビルドせずに型チェックだけを行う。

これが便利というか、これで型チェック + CI でのテストを行っていて、webpack の ts-loader では transpileOnly: true としている。基本は VSCode 上で型エラーを見ている。

prettier

$ npm i -g prettier
$ prettier 'src/**' --write

指定ディレクトリ以下を prettier で整形する

firebase

$ npm i -g firebase-tools
$ firebase init

firebase プロジェクトの雛形を作成するコマンド。対話型で、質問に答えるとそれに沿ったボイラープレートが作成される。

$ firebase deploy --only firestore:rules

firestore.rules が動いているか検証をするのに、これを叩きまくっていた。 firebase はデプロイに時間がかかるやつが多いので、デバッグ時は --only 必須。

gibo

https://github.com/simonwhitaker/gibo

$ gibo dump Node OSX > .gitignore

.gitginore を自動生成

netlify

https://www.netlify.com

$ npm i -g netlify-cli
$ netlify deploy -d dist --prod
$ netlify open:site

dist 以下を netlify にデプロイする。今年は netlify に本当にお世話になった。代わりに github pages を使わなくなった

hub

https://github.com/github/hub

$ hub create mizchi/myprj

GitHubリポジトリを作成する。前からよく使っていたが、今年は本当によく使った。

しかし、未だに hub で pr を作成するコマンドの引数が覚えられない…

parcel

$ yarn add parcel-bundler -D
$ yarn parcel src/index.html --open

本番環境では相変わらず webpack を使っているが、実験プロジェクトは全部 parcel でやるようになった。

フロントエンドのまどろっこしい諸々をすっ飛ばして SPA を作り始めることができる。

$ parcel build src/index.html && netlify deploy -d dist --prod

netlify と組み合わせて、今年はこのコマンドを叩きまくった。今年の MVP コマンド。

ghq

https://github.com/motemen/ghq

$ ghq get -u <github-url>

GitHub リポジトリはすべて ghq で管理するようにした。

面白半分で作って、途中から GitHub に移したものをどう扱うかはまだ悩んでいる。

pwmetics

$ npm i -g pwmetrics
$ pwmetrics http://localhost:1234

指定 URL をヘッドレス Chrome で開いて、そのパフォーマンスメトリクスを取得する。

lighthouse を起動するのが面倒なときに、パフォーマンス取得するのに使っている。lighthouse はパラメータが多いが、こっちは小さくまとまってる。

lighthouse-cli

$ npm install -g lighthouse
$ lighthouse https://google.com

lighthouse を cli から起動する

webpack-bundle-size-analyzer

$ npm i -g webpack-bundle-size-analyzer
$ yarn webpack --json | webpack-bundle-size-analyzer

webpack-bundle-analyzer が HTML でレポートを作成してブラウザを開かないといけないのに対し、これはターミナルで完結してビルド後の JS のサイズを調査することができる。

実行例

$ webpack-bundle-size-analyzer build/client/stats.json
favalid: 330.87 KB (11.5%)
  ramda: 309.32 KB (93.5%)
  <self>: 21.54 KB (6.51%)
react-virtualized: 326.87 KB (11.3%)
core-js: 243.11 KB (8.42%)
lodash: 237.18 KB (8.21%)
redux-form: 199.7 KB (6.92%)
  hoist-non-react-statics: 2.02 KB (1.01%)
  <self>: 197.68 KB (99.0%)
date-fns: 170.93 KB (5.92%)
react-router: 137.13 KB (4.75%)
  hoist-non-react-statics: 1.35 KB (0.988%)
  <self>: 135.77 KB (99.0%)
react-dom: 98.98 KB (3.43%)
styled-components: 76.72 KB (2.66%)
...

mdbuf

https://markdown-buffer.netlify.com

シェルじゃないが、自作の高速な markdown preview 環境。この記事もここで書いている。

最近執筆が多いので、本当に作ってよかった。

ただ今ちょっとバグっているパターンを色々見つけていて、直したい。

書いた動機

フロントエンドのパフォーマンスチューニングの調査と、検証用の小さなプロトタイプを作ってデプロイすることが多い。それぞれは大したことないが、知ってるかどうかで生産性が大きく変わる実感がある。

他の人の手癖を知りたい。

Elm 2日ほどやった感想

12月はなんとなく新しいことをやりたくなる。ということで、elm をやってみた。

大昔に触った気がするけど、文法が Haskell っぽいこと以外、何も覚えてなかった。というか当時触った signal とかがなくなってたので別物になってた。

作ったもの

勉強がてら作った、球拾いゲームみたいな何か

コードはここ https://github.com/mizchi-sandbox/elm-playground

elm-platform, svg の扱い, キー入力、乱数の副作用の分離などが学べた。乱数は面倒くさくなったので外部(JS)からSeed与える方式にして、気持ち純粋になった。

(自分の)環境構築

Parcel を使った

brew install elm # OSに応じて
mkdir elm-playground
cd elm-playground
yarn init -y
yarn add parcel-bundler node-elm-compiler

これだけ

Elm とはなにか

現時点の Elm の理解。

GUI プラットフォームの為に余分なものを削ぎ落とした関数型プログラミング言語。削ぎ落とした結果、正格評価だし、モナドもない。副作用の表現は後述する elm-platform を経由するしかない。

あくまでも GUI のための状態表現で、UI 表現とは何か?という問いから言語仕様を組み上げていってるので、汎用言語として必要なものも捨てていて、汎用プログラミングには使えない。特に後述する elm-platform で顕著。あくまで GUI 専用。

副作用: elm-platform

elm は IO モナドがないので、副作用を剥がして純粋関数に渡すには、 elm-platform を通る必要がある。redux で例えると elm-platform は redux middleware に相当する。

というわけで elm-platform で乱数を扱うっぽい処理を typescript で書くとこうなるよなー、というやつを書いてみた。

function update(model: Model, msg: Msg): [Model, Cmd | null] {
  switch (msg.msg) {
    case "roll": {
      return [model, random(newFace)];
    }
    case "newFace": {
      return [
        { face: msg.payload, history: [msg.payload, ...model.history] },
        null
      ];
    }
    default: {
      return [model, null];
    }
  }
}

全部はこちら https://gist.github.com/mizchi/bdaa2250f1707d6fb09f4115644da0aa

元ネタはここ https://package.elm-lang.org/packages/elm-lang/core/latest/Random

この関数は [model, cmd] を返す。 cmd によって副作用が発生して、次のアクションが発生する。 redux のreducer に比べると、middleware 的な処理まで一歩踏み込んでいる。

型の安心感

TypeScript の、「努力目標で型ついてたらいいよね」的な世界観と比べると、本物の静的な型の安心感が手に入り、その安心感は圧倒的。今の所ランタイムエラーは一度も発生してない。

乱数の表現の Random(Generator) や Promise 相当の Task がある。

JavaScript 世界のオブジェクトを触るときは Json.Decoder で丁寧にアクセスする必要があったり、port という外部にアクセスする専用の RPC を使い必要があり、そこはだるい。

関数型界隈特有の「サンプル書かなくても型はこうなってるから後はわかるよね?」的な世界観もややあって、 example にないものに手を出そうとすると、慣れが必要。今もまだ慣れきってはいない。

エコシステム

今回は parcel を使ったが、parcel が公式で対応してるので、めちゃくちゃ簡単だった。 import { Elm } from './Main.elm' するだけ。

公式のフォーマッタの elm-format, というかスタイルガイドはかなり癖がある。関数同士は二行空ける。特に強い意見もないので、一応従ったが、なんとなくコードが間延びした印象を受ける。

vscode で elm パッケージを入れてコードを書いている。型チェックは信頼できるが、コード補完はそこそこ程度。型チェックは通っても循環参照でコンパイルエラーが起きたことはあった。

ドキュメントが Elm 0.19 に対応していないのがよく見られる。さっきの Random のドキュメントでも Random.map2 (,) (Random.int 0 10) (Random.int 0 10) が、 Random.map2 Tuple.Pair (Random.int 0 10) (Random.int 0 10) だったり、 公式パッケージや公式ドキュメントだとしても、微妙に追いついていない。最新のドキュメントを追い続けてないと、変更についていくのは大変だろう。 0.19 がどれぐらい安定しているのか不明だが…

Reason との比較

Reason と Elm は表面的な書き心地が似ている。

Reason は一旦 ocamlコンパイルされ、表現力・制約は ocaml と同程度。ocaml が簡単に副作用や例外を発生させられてしまうのに対し、elm はその部分を外に投げることで純粋性を担保している。そのかわりに Reason で JS オブジェクトにアクセスするのが非常に簡単。泥臭いことをするなら Reason のほうがいいが、そもそも泥臭いことをするならJSでいい気がする。

学習コストに関して、 Elm は Elm で閉じた世界観だが、 Reason は ocaml と JS に精通している必要がある。reason は使い手が ocaml に強いならやっと選択肢になりうるという感じ。

使えるか

ある種のUIでは間違いなく強力だが、常に Elm が最適解ではない、という印象を受けた。Elm コミュニティもその辺割り切ってそうな印象がある。

実際に泥臭い処理をやる場合、portを駆使したり、小さいコンポーネントとして分割統治したりする必要がありそう。

おまけ: parcel と webcomponents を使った最小 Elm 入門

Elm を使うのに大仰なセットアップが必要、と思ってる人もいるかもしれないので、parcel を使った最小の例を示す。

Foo をマウントする <elm-foo> 要素を定義する。

-- elements/foo.elm
import Html exposing (div, text)
main = div [] [text "foo"]
// elements/foo.js
import { Elm } from "./Foo.elm";

customElements.define(
  "elm-foo",
  class extends HTMLElement {
    connectedCallback() {
      Elm.Main.init({ node: this });
    }
  }
);

これをマウントする

import "./elements/foo";
document.body.innerHTML = '<elm-foo></elm-foo>'

webpack でも同等の表現できると思うけど、parcel だとデフォルトでそのまま動くので楽、という感じ。

react-redux と useContext を組み合わせて使う実験

react-redux v6 で中身が新しい方の Context API になったと聞いたので、コード読んでみたらContext自体が外部に export されていた。じゃあ hooks の useContext と組み合わせて使えるじゃん、と思って実験してみた。

useContext で connect 相当の処理を置き換えてみる。

https://github.com/reduxjs/react-redux/blob/master/src/index.js

import React, { useContext } from "react";
import ReactDOM from "react-dom";
import { createStore, Store } from "redux";
import { ReactReduxContext, Provider } from "react-redux";

type State = {
  count: number;
};

// いつもの reducer
const initialState: State = { count: 0 };

function reducer(state: State = initialState, action: any) {
  switch (action.type) {
    case "reset": {
      return initialState;
    }
    case "increment": {
      return { count: state.count + 1 };
    }
    case "decrement": {
      return { count: state.count - 1 };
    }
    default: {
      return state;
    }
  }
}

function Counter() {
  // useContext で store を取り出す
  const {
    storeState,
    store: { dispatch }
  } = useContext(ReactReduxContext);
  const counterValue = storeState.count;
  return (
    <div>
      Count: {counterValue}
      <button onClick={() => dispatch({ type: "reset" })}>Reset</button>
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
      <button onClick={() => dispatch({ type: "decrement" })}>-</button>
    </div>
  );
}

function App(props: { store: Store<State> }) {
  return (
    <Provider store={props.store}>
      <Counter />
    </Provider>
  );
}

const store: Store<State> = createStore(reducer);
ReactDOM.render(<App store={store} />, document.querySelector(".root"));

これで動いた。が、いくつか問題がある。

react-redux の connect は map された値の変更を監視していて、HOCが自分に関係ない値の更新処理を抑制してくれていた。だが、この素朴に useContext している版は、 store のすべての値に反応してしまう。

useContext した時点ですべての更新を listen してしまうので、結局これ自身が子への抑制処理を行う必要がある。

色々頑張った結果、こうなった

import React, { useContext, useCallback } from "react";
import ReactDOM from "react-dom";
import { createStore, Store, Dispatch, AnyAction } from "redux";
import { ReactReduxContext, Provider } from "react-redux";

// helper
function useDispatch(): Dispatch<AnyAction> {
  const {
    store: { dispatch }
  } = useContext(ReactReduxContext);
  return dispatch;
}

function useConnect<T, U>(fn: (t: T) => U): U {
  const { storeState } = useContext(ReactReduxContext);
  return fn(storeState);
}

type State = {
  count: number;
  resetCount: number;
};

const initialState: State = { count: 0, resetCount: 0 };

function reducer(state: State = initialState, action: AnyAction) {
  switch (action.type) {
    case "reset": {
      return { ...initialState, resetCount: state.resetCount + 1 };
    }
    case "increment": {
      return { ...state, count: state.count + 1 };
    }
    case "decrement": {
      return { ...state, count: state.count - 1 };
    }
    default: {
      return state;
    }
  }
}

const CounterInner = React.memo(function CounterInner(props: {
  counterValue: number;
  onClickReset: () => void;
  onClickIncrement: () => void;
  onClickDecrement: () => void;
}) {
  console.log("update counter inner");
  return (
    <div>
      Count: {props.counterValue}
      <button onClick={props.onClickReset}>Reset</button>
      <button onClick={props.onClickIncrement}>+</button>
      <button onClick={props.onClickDecrement}>-</button>
    </div>
  );
});

function Counter() {
  console.log("update counter", Date.now());

  const dispatch = useDispatch();
  const counterValue = useConnect<State, number>(state => state.count);
  const onClickReset = useCallback(() => dispatch({ type: "reset" }), []);
  const onClickIncrement = useCallback(
    () => dispatch({ type: "increment" }),
    []
  );
  const onClickDecrement = useCallback(
    () => dispatch({ type: "decrement" }),
    []
  );
  return (
    <CounterInner
      counterValue={counterValue}
      onClickReset={onClickReset}
      onClickIncrement={onClickIncrement}
      onClickDecrement={onClickDecrement}
    />
  );
}

function App(props: { store: Store<State> }) {
  return (
    <Provider store={props.store}>
      <Counter />
    </Provider>
  );
}

const store: Store<State> = createStore(reducer as any);
ReactDOM.render(<App store={store} />, document.querySelector(".root"));

Counter は自分自身で element の詳細を生成せず、CounterInner に props を渡す。その際の関数は、useCallback を使ってメモ化する。

CounterInner 側では React.memo を使って shallow equal の比較によって更新処理を抑制している。CounterInner は count に興味があるが、 resetCount には興味がない。reset を連打した際は CounterInner の更新が行われないようにすることができた。

感想

やってみたけど、メモ化を駆使して自力でuseContextでとった値の更新をコントロールするのは結構だるい。useContext に mapState 相当の処理がほしい。

たぶん react-redux の大きなグローバル変数を持つみたいな考え方じゃなくて、connect される Context を都度生成する、みたいな発想の転換が必要な気がする。もうちょっと研究が必要。

Edge 終了に寄せて

初報を聞いたとき、描画系だけ blink に入れ替えて処理系は V8 使わず ChakraCore などに入れ替えるのかな、と思っていたが、どうも V8、というか chromium 一式を使うらしい。

正直に言って、Edge が死ぬことに、そこまで強く思うところはない。Edge は内部的に自身のベンダープレフィックスwebkit と名乗るぐらい (標準ではなく) webkit との互換性の意識が高いので、お前自分のことを webkit だと思ってるもんな、みたいな気持ちがあった。

webkit みたいなブラウザが消えて、webkit 後継のブラウザをベースに作り直される。開発者として追うべきは、個別の実装ではなく依然として標準仕様であって、それだけの話。

リリースサイクル

僕が思うに、 MS の抱えていた真の問題は、Windows に紐付けられたリリースサイクルとサポートにあって、Windows Update によって環境を破壊されるんじゃないか、というユーザーの不信を生んでしまったことにある。とくにタブブラウザ化した IE7 の不出来は時代を大きく変えた潮目の一つだと思っていて、自動更新をオフにすることが情報強者だ、といった風潮を作ってしまった。

現状、Web の進化のスピードは、IE のサポートが順次切れるスピードで律速となっている。EdgeHTML から Chromium に移ることで、すべての現行ブラウザが事実上ローリングアップデートを採用することになるだろう。その点は歓迎できる。

Chromium の成功した理由の一つは、バックグラウンドの自動アップデートと、それを気づかせない後方互換性の担保と、それが可能な品質にある。なんか最近は急に UI がまるっこくなったりしたが、その程度だ。

仕様策定について

実装の多様性が失われることで仕様に与える影響については他の人が言ってる通り、Google の発言力が強くなりすぎることを懸念する。あまりに UI というものを規定しすぎる WebComponents の初期仕様や、ブラウザに Dart を採用する Dartium は採用されなかったが、今後誰が止められるだろうか。とりあえず今は Mozilla なのだろうが…

これは、Chrome が標準に準拠するか否かではなく、Chromeが標準を私物化しはしないか、という懸念。

今後は Desktop/AndroidChrome と、iOSSafari という対立に収斂されると思われるが、iOSChrome を受け入れるか、Safari が Web の仕様から乖離したときが真に Web というものが試されるときだと思う。

本音を言うと、一開発者としては、優秀なエンジンの寡占こそが、短期的には一番「楽」な状況ではあるが、これが IE の二の舞にならないかというと、その自信は全くない。Chromeで動けばよいみたいなトレンドが生まれてしまうのは、誰がどんな高尚なことを言おうが、状況が生む必然だと思う。水は低きに流れる。

Webpack の考え方について

www.slideshare.net

この記事バズってたけど、わからない人がよりわからなくなる、という点で問題だと思っていて、webpack の目的の本質的な部分から整理する必要があると思います。

(あと友人が webpack に挑戦していたので入門資料も兼ねてる)

Webpack の本質的な部分は次の3つです。それ以外は全部おまけ機能だと思ってよいです。

  • ES Modules のエミュレート
  • node_modules のリンカ
  • 拡張子ごとの変形

Webpack が本当にやりたいこと

こういうコードがあるとします。

// src/a.js
export default () => console.log('hello');
// src/index.js
import a from './a.js'
a();

このコードは、今現在 IE 以外のブラウザで既に実行可能ですが、IE と、あと Google のクローラ は Chrome41相当となっていて、実行できません。また現行のブラウザのサポートがあると言っても、とりあえず実行できるという程度で、パフォーマンスはよくありません。(ネットワーク越しに一個一個ダウンロードして依存を判明させるのを想像してみてください)

とりあえず、これを node 環境を使って実行してみましょう。

$ npm install esm --save-dev
$ node -r esm  src/index.js
hello

(esm は node で import/export を有効化するおまじないと思ってください)

ブラウザで実行する際は、これらを一つのファイルに結合します。つまりこの import/export を使わない形式に書き換えた上で結合する必要があります。

$ npm install --save-dev webpack webpack-cli
$ npm run webpack --mode development

これは次のようなコードを出力します。(全部読まなくていいです。雰囲気で)

/******/ (function(modules) { // webpackBootstrap
/******/    // The module cache
/******/    var installedModules = {};
/******/
/******/    // The require function
/******/    function __webpack_require__(moduleId) {
/******/
/******/        // Check if module is in cache
/******/        if(installedModules[moduleId]) {
/******/            return installedModules[moduleId].exports;
/******/        }
/******/        // Create a new module (and put it into the cache)
/******/        var module = installedModules[moduleId] = {
/******/            i: moduleId,
/******/            l: false,
/******/            exports: {}
/******/        };
/******/
/******/        // Execute the module function
/******/        modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/        // Flag the module as loaded
/******/        module.l = true;
/******/
/******/        // Return the exports of the module
/******/        return module.exports;
/******/    }
/******/
/******/
/******/    // expose the modules object (__webpack_modules__)
/******/    __webpack_require__.m = modules;
/******/
/******/    // expose the module cache
/******/    __webpack_require__.c = installedModules;
/******/
/******/    // define getter function for harmony exports
/******/    __webpack_require__.d = function(exports, name, getter) {
/******/        if(!__webpack_require__.o(exports, name)) {
/******/            Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/        }
/******/    };
/******/
/******/    // define __esModule on exports
/******/    __webpack_require__.r = function(exports) {
/******/        if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/            Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/        }
/******/        Object.defineProperty(exports, '__esModule', { value: true });
/******/    };
/******/
/******/    // create a fake namespace object
/******/    // mode & 1: value is a module id, require it
/******/    // mode & 2: merge all properties of value into the ns
/******/    // mode & 4: return value when already ns object
/******/    // mode & 8|1: behave like require
/******/    __webpack_require__.t = function(value, mode) {
/******/        if(mode & 1) value = __webpack_require__(value);
/******/        if(mode & 8) return value;
/******/        if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/        var ns = Object.create(null);
/******/        __webpack_require__.r(ns);
/******/        Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/        if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/        return ns;
/******/    };
/******/
/******/    // getDefaultExport function for compatibility with non-harmony modules
/******/    __webpack_require__.n = function(module) {
/******/        var getter = module && module.__esModule ?
/******/            function getDefault() { return module['default']; } :
/******/            function getModuleExports() { return module; };
/******/        __webpack_require__.d(getter, 'a', getter);
/******/        return getter;
/******/    };
/******/
/******/    // Object.prototype.hasOwnProperty.call
/******/    __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/    // __webpack_public_path__
/******/    __webpack_require__.p = "";
/******/
/******/
/******/    // Load entry module and return exports
/******/    return __webpack_require__(__webpack_require__.s = "./src/index.js");
/******/ })
/************************************************************************/
/******/ ({

/***/ "./src/a.js":
/*!******************!*\
  !*** ./src/a.js ***!
  \******************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony default export */ __webpack_exports__[\"default\"] = (() => console.log('hello'));\n\n\n//# sourceURL=webpack:///./src/a.js?");

/***/ }),

/***/ "./src/index.js":
/*!**********************!*\
  !*** ./src/index.js ***!
  \**********************/
/*! no exports provided */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _a_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./a.js */ \"./src/a.js\");\n\nObject(_a_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"])();\n\n\n//# sourceURL=webpack:///./src/index.js?");

/***/ })

/******/ });

前半のコメント付きの部分が webpack 自体のランタイムで、あとは __webpack_require__ によって、単なるパスとそれに対応する辞書が組み立てられ、素朴な関数として実行されているのがわかります。

webpack は本当に、ただこの結合をやりたいだけ、というライブラリです。

(ここで一つ暗黙的な知識を使っていて、無設定の webpack は src/index.jsdist/main.jsコンパイルするようになっています)

node_modules の解決

上の例は素朴な相対パスの解決でしたが、これだけだと、例えば lodash みたいなnode_modulesから参照するライブラリは解決できません。

node の慣習だと、これは const _ = require('lodash') とすると、node_modules/lodash/package.jsonmain に指定されたモジュールが手に入りますが、webpack はこの名前解決ルールをエミュレーションしています。

index.js を次のようにします

// npm install lodash --save してから
import { flatten } from 'lodash';
console.log(flatten([[1], [2]]));
$ node -r esm src/index.js
[1, 2]

そうすると、ビルド済みファイルの一部がこんな感じ

/***/ "./node_modules/lodash/lodash.js":
/*!***************************************!*\
  !*** ./node_modules/lodash/lodash.js ***!
  \***************************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
// 以下実装

require と ESM の import/export の関係もまたややこしいのですが、とりあえず今は import/export だけ使うようにしてればいいはずです。

transform

webpack 環境下では、実際には plane な js を書くことはなく、babel か typescript を使うことが多いと思います。babel-loader を使うと .babelrc を参照するし、ts-loader を使うと tsconfig.json を参考にします。

これを実現するのが module.rules で、babel を使うだけだったら単にこれだけです。

//webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: "babel-loader",
        exclude: /node_modules/
      }
    ]
  }
};

test の正規表現でヒットした rule が、 use で指定された babel-loader を通して(その過程で .babelrc の設定を参照して) .js を変形します。

基本的には「拡張子ごとに特定のloader を通して変形する」という考え方でいいと思います。バンドラーがトランスフォームまで行うのは、これらを役割を兼ねた方がパフォーマンスがよくなり、結果としての設定も少なくなる、という webpack 以前に使われていた browserify からの伝統です。

細々とした最適化

webpack を使っていて、これら以外は基本的に学習者にとっての「ノイズ」に映ることと思います。具体的には、他にこのようなことをやるわけです。

  • 複数のリソースから共有できるビルドステージを作る
  • インメモリ上に展開
  • 最終出力をどう最適化するか(uglifyjs)、どう出力パスを決定するか(webpack-manfiest-plugin)
  • SPA 用のエントリポイントを生成 (html-webpack-plugin)
  • JavaScript 以外、 css を js から読み込んだり(style-loader/css-loader), 画像を base64 化したり…
  • 未使用モジュールの削除

このうち、特に js 以外を扱う操作が曲者だと思っていて、「ES Modules をエミュレートしたい」というwebpack本来のスコープに含まれていません。その結果、実現方法がどれも歪です。

Webpack が嫌がられるのは、これらを覚えることでプログラミングの深淵に迫れるわけでもなければ、他に転用できる知識があるわけではない点です。単なる設定の組み合わせにすぎません。だるいですね。

要約

  • JS を ESM に沿った形で変形/結合するのは、本来の目的に沿うので OK
  • それ以外は Webpack 以外での実現も考えてはどうか

難しい(ややこしい)部分は大抵後者なので、触れずに済むならそれでいいはずです。

style-loader + css-loader 程度だったらライブラリのJSと協調するCSS引くのに有用だけど、これに scss や postprefixer とかはいれたくないな、というのが個人的な気持ち。

将来的に Webpack を捨てられるか

Webpack を捨てるのに、大きなブロッカーが3つあります

  • IE
  • 使ってる npm モジュールが ES Modules の形式で配布されるか
  • JSX/TypeScript のような拡張構文

IEはまあ死んでもらうのをまつとして、IEが死んだあとに素の ESModules を使えるかどうかは、使ってるライブラリ次第です。lodash や react などは一応対応していますが、現状作者側にとっても対応の敷居が高いので、対応は後回しになりがち。(rollupを使う)

むしろ、将来的に難しいのは TypeScript の問題で、最近のトレンドだと、もはや JS = TypeScript になりそうなぐらいの勢いですが、ただし当然のごとく非標準な文法なので、依存してしまうと捨てるのが難しいのではないか、という気がしています。同様の理由で非標準の JSX も厳しい気がします。

一応今tc39にプロポーザルが出ているのですが、ジェネリクスみたいなセマンティクスの議論の余地がある文法も含んだ提案が、すんなり通るとは思えません。

https://github.com/samuelgoto/proposal-optional-types

プリコンパイル一回するなら二回するのも中間生成物があるのは変わらないし、手間はそう変わらない、ということで、 Webpack + TypeScript or Babel がしばらく生き残ってしまうのではないでしょうか。

おまけ: Parcel

webpack の alternative として parcel があります。

https://github.com/parcel-bundler/parcel

webpack ユーザー的な解釈だと、便利なデフォルト設定集なんですが、実際には webpack の慣習やお気持ちに深く精通して初めて parcel の挙動が理解できる、という感じが強いです。 その結果 webpack を完全に理解した仙人なら設定をサボるのに parcel を使うことが出来る、という状態になっています。勉強したくないからこれで!とやると、おそらく何が起こるのか、その理由がまったくわからなくなって、結果的に辛い感じになると思います。

WebComponents: ReactNative.View のような CSS の既定値を持つだけの x-view を作ってみる

ReactNative 触った事ある人なら、ReactNative.View の iOS の UIView や Android View みたいな一貫性のある基底クラスが羨ましい人も多いと思う。 今の Webでは実質 div がそれを担っているわけだが、div になんでも押し付けるのはよくないという意見もわかるところがあり、それ専用の基底となる x-view 要素を作るとどうなるだろうか。

ほしいもの

  • デフォルトが display: flex;
  • デフォルトが box-sizing: border-box;

flex は便利だが Web のプリミティブ要素として既定値でflex を持つものはないので、毎度手数が多くなって嫌だなぁぐらいの気持ちがあった。

実装

とりあえず ReactNativeWeb がどう実装してるか真似しつつ実装した

const defaultViewStyle = document.createElement("style");
defaultViewStyle.textContent = `
  :host {
    border-width: 0;
    border-style: solid;
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    margin: 0;
    padding: 0;
    position: relative;
    z-index: 0;
    min-height: 0;
    min-width: 0;
  }
`;

customElements.define(
  "x-view",
  class extends HTMLElement {
    constructor() {
      super();
      const shadow = this.attachShadow({ mode: "open" });
      shadow.appendChild(document.importNode(defaultViewStyle, true));
      shadow.appendChild(document.createElement("slot"));
    }
  }
);

children を展開する slot を持ち、自分自身(:host)にのみ適用するstyleを持つ。div を継承せず直接 HTMLElement の子要素になってる

style は毎度インラインのHTMLから生成するより、style を clone したほうが速いかも?と思ってこうしてるが、特に根拠はない。どうするのがいいんですかね。

これでこういうHTMLが動くようになる

<x-view style='height: 200px;'>
  <x-view style='flex: 1'>a</x-view>
  <x-view style='flex: 2'>b</x-view>
</x-view>

デフォルト display: flex なので flex 値の付与だけでいい。(実際こんなstyleを直接書く書き方はしないが、簡単のため)

TypeScript

この要素を使って、React で

ReactDOM.render(<x-view>a</x-view>, element); 

しようとすると、未定義要素を生成しようとしてるので怒られる。

React の型定義から、これを通す型宣言を追加する。

declare namespace JSX {
  interface IntrinsicElements {
    "x-view": React.DetailedHTMLProps<
      React.HTMLAttributes<HTMLElement>,
      HTMLElement
    >;
  }
}

たぶんこんんあ感じ。

x-pane

x-view は ReactNative 互換を意識したのであんまり攻めた感じではなかったのだが、実際自分がCSS書く際は width: 100%; height: 100%; justify-content: center; align-items: center; を規定値として持つように作ることが多い。一般的なCSS仕草かは知らない。

名前思いつかないが、とりあえず x-pane とした。

const defaultPaneStyle = document.createElement("style");
defaultPaneStyle.textContent = `
  :host {
    align-items: center;
    border-width: 0;
    border-style: solid;
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    height: 100%;
    justify-content: center;
    margin: 0;
    min-height: 0;
    min-width: 0;
    padding: 0;
    position: relative;
    width: 100%;
    z-index: 0;
  }
`;

customElements.define(
  "x-pane",
  class extends HTMLElement {
    constructor() {
      super();
      const shadow = this.attachShadow({ mode: "open" });
      shadow.appendChild(document.importNode(defaultPaneStyle, true));
      shadow.appendChild(document.createElement("slot"));
    }
  }
);

declare namespace JSX {
  interface IntrinsicElements {
    "x-pane": React.DetailedHTMLProps<
      React.HTMLAttributes<HTMLElement>,
      HTMLElement
    >;
  }
}
<x-view style="height: 300px;">
  <x-view style="flex: 2; background: gray;">
    <x-pane style="color: green;">Pane0</x-pane>
  </x-view>
  <x-view style="flex: 1;">
    <x-view style="flex-direction: row; height: 100%;">
      <x-view style="flex: 3; background: #a88;">
        <x-pane>Pane1</x-pane>
      </x-view>
      <x-view style="flex: 1; background: #8a8;">
        <x-pane>Pane2</x-pane>
      </x-view>
    </x-view>
  </x-view>
</x-view>

https://camo.githubusercontent.com/259981a9bbd9f14b8d8429af76514ad480c2b7e0/68747470733a2f2f6779617a6f2e636f6d2f65653934353133346366373934313561343337326535623230366666356331632e706e67

みたいな感じになる。

感想

IE webcomponents ポリフィルフレンドリーな書き方するとまた別の書き方になると思う。さして手が増えるわけではないが。 正直、この程度だったら .view {} でよい。webcomponents があふれかえる時代になったら、基底要素としてこういう書き方しても怒られないかもしれない、ぐらいの気持ち。