プログラマという現代の傭兵
エンジニアの転職とかプログラミング教育周りで考えていたこと。
フランス革命と技術のコモディティ化
最近フランス革命やナポレオン戦争やナショナリズム、そしてクラウゼヴィッツの戦争論などを調べたりしていたんだけど、傭兵や専門技術の扱いについて、示唆的なものが多かった。
当時の傭兵は、扱いが難しかった大砲・銃火器を扱う専門集団で、技能職でもあった。それが 18 世紀になり火器の改良が進み、産業革命で効率的な生産が可能になり、そしてナポレオンによる国民軍の創設、そのヨーロッパにおける戦果によって、傭兵はその役割を終えた。
「傭兵はすぐ逃げる」というのが定説だが、彼らは金で動く専門職なので、負ける側に付く理由がないので、当然とも言える…特に戦争という、敗者の支払いが期待できない場では。そして彼らを雇う王侯貴族の経済力が、そのまま軍団の動員力に直結した。常備軍を持たない分、平時のコストも安くついた。
ナポレオン(と彼を分析したクラウゼヴィッツ)は、前線を支える兵士の士気を、愛国心に求めた。銃火器の扱いが平易になったこと、そして(職業軍人と比較して)高度な訓練を必要としない歩兵の運用のメソッドが確立したことで、それが可能になった。
そして、徴兵によって「自分で自分の国を守る」という意識変革を経た結果、貴族階級の存在理由が否定され、王権と封建制度を中心とした古い体制、アンシャンレジームが破壊されて、「国民国家」の現代に至る。
双方引かない国民国家同士の戦争が、いかに悲惨な消耗戦になるかは、第一次世界大戦、第二次世界大戦で明らかとなったわけだが…。
一方、現代では
この話は、本質的には、銃火器の取扱いの平易化という変化が、下部構造の柔軟性を生んだ話だと思っている。ナポレオンのすごいところは、その時代の変化を見逃さず適した組織を作り、軍事的優位を作ったところにある。結果としてナポレオンが敗北した理由も、ナポレオンに対抗するために追い込まれた側がそのメソッドを採用して反撃したところにあるわけで…。
翻ってプログラミング技術について考えると(この話の展開っておっさん臭くて嫌なんだが)…自分は、現代の銃火器は、プログラミング技術なんじゃないかと思っている。しかも、銃火器は戦場でしか役に立たないが、プログラミング技術は日常のあらゆる場面で役に立つ、可能性がある。
プログラミング技術は、個々人がネットワークに接続し、その計算リソースを借りてその能力を拡張するための手段だ。物事を議論の余地がないほどに小さいステップに分解するという、ある種非人間的な訓練を要求される。そして、この訓練メソッドが十分に成熟していない。
コモディティ化しないプログラミング技術
現在のプログラミング技術を取り巻く環境は、銃火器の取扱いが平易になる前のコモディティ化以前の傭兵全盛時代だと思っている。その理由はいくつかあって…
- 教育による能力の再現性がない
- 優秀なプログラマが教育によって育つ例がない
- システム側(社会)による受け入れ体制が整っていない
- プログラミング技術自体が(歴史が浅いので)進化し続けている
その結果、勝手に育った一部の人間が、プログラマに金を出してくれる会社に集まっている、という状態にある。ITの大手にいる人達は、10年前とそう変わらない。
会社の隆盛も早く、終身雇用を前提することが不可能で(日本社会の終身雇用自体が信用できなくなったのもあるが)、誰もが 10 年後にこの会社にいないと思っている。10 年前と変わったのが、コンピューターサイエンスで優秀な成績を修めたアカデミックエリートが、GAFA に行くようになったぐらいか。
個人としての最適と、社会としての最適が異なるのは前提として、自由度が高いコマとして競争力を持つことが、個人としての最適戦略になっている。少なくとも自分はそう思って動いている。
プログラミング教育について
本質的にプログラミング能力とは、物事の細かいステップへの分解能力と、何らかのルール制約下(プログラミング言語の表現力)での対応を発見することにあると思っている。
というのを前提として、国がプログラミング教育を推進したい気持ちもよく分かる。プログラミング教育を否定する人も、論理的思考力を鍛えることが大事、までは同意が取れると思う。従来、それは数学が担っていた分野だが、プログラミングは、数学教育メソッドの伝統的な制約から踏み出さないと訓練できない領域を多分に含むので、プログラミング教育という分野が創出された、という理解をしている。
それに対し、今のプログラマが育つ環境のリアルは、親方が弟子に伝授するという中世のギルド制度に近いのではないか、というのが界隈を見渡した際の予想としてある。親方は大学の教授かもしれないし、最初に入った会社のメンターかもしれないし、Twitter の強い友人かもしれない。どういう親方に技術を学んだかによって、その人の方向性が決まってしまうところが大きい。
ギルド制とはいえ、あんまり堅苦しくないのは、ハッカー文化は MIT の原始共産制っぽいヒッピー文化に強く影響を受けていて、自由を得るための実力主義社会という側面が強い(GNU や OSS)。ただし圧倒的に実力主義なので、実力がないと発言権がない。強者の論理であるとも思う。
プログラミング技術がコモディティ化するときは、ハッカー文化が失われるときであるとも思う。
終わり
こういう記事を書いといて何だが、何が正しいとか、何が正しくないとか言うつもりはない。自分は業界をそういう目で見ている、という話。
現実問題、たぶん、今大規模なサイバー戦争が起きたら(攻殻機動隊をイメージしている)、結果的に頼りになるのはセキュリティ専門家とそれに近いプログラマと手癖が悪いダークウェブ界隈で構成された傭兵部隊だろう。自衛隊も何らかの訓練しているとは思うが、サイバーセキュリティ人材の扱いを見ていると、とても信用できない…
ちょっとプログラミング万能主義的な展開になってしまったが、少なくとも社会のIT技術の受け入れ体制が変わって、かつプログラミング教育が機能したあとの社会を考えると、社会はどう変わっていくか、というのは妄想すると面白いじゃないだろうか、という話。
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 待ちです。
https://github.com/Microsoft/vscode/issues/45629
なぜ作ってるか
これは next-editor https://nedi.app の Markdown 編集機能を切り出して再設計したものです。
応援してくださる方がいましたら patreon もよろしくおねがいします。
他、何か要望などがあれば、 twitter @mizchi までお願いします。
今年お世話になったCLIコマンド集
ヒストリ履歴からよく使ってるものをお焚き上げする。
注意点: npm 周り、グローバルコマンドは npm i -g
で入れてて、ローカルで扱うものは yarn で使うという癖がある
追記: シェルじゃなくてCLIだろと言われるのが多かったので訂正した
vscode
$ code . -r
-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
$ npm i -g netlify-cli $ netlify deploy -d dist --prod $ netlify open:site
dist 以下を netlify にデプロイする。今年は netlify に本当にお世話になった。代わりに github pages を使わなくなった
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/Android の Chrome と、iOS の Safari という対立に収斂されると思われるが、iOS が Chrome を受け入れるか、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.js
を dist/main.js
にコンパイルするようになっています)
node_modules の解決
上の例は素朴な相対パスの解決でしたが、これだけだと、例えば lodash みたいなnode_modulesから参照するライブラリは解決できません。
node の慣習だと、これは const _ = require('lodash')
とすると、node_modules/lodash/package.json
の main
に指定されたモジュールが手に入りますが、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 を使うことが出来る、という状態になっています。勉強したくないからこれで!とやると、おそらく何が起こるのか、その理由がまったくわからなくなって、結果的に辛い感じになると思います。