dackdive's blog

新米webエンジニアによる技術ブログ。JavaScript(React), Salesforce, Python など

WEB+DB PRESS Vol.97のReactサンプルアプリをReduxで書き直した

特集の React 記事を読んでいます。
React/Redux でのクライアントサイドルーティングや SSR(サーバーサイドレンダリング)について学びたく、とりあえずサンプルコードが flux を使って書かれていたのを Redux を使う形に書き直した。

(PR は #1

いじらなかった components/ 以下のコンポーネントと containers/ でファイル名の命名ルールが揃っていなかったり、container はもう少し presentational な役割と切り離した方がいいんじゃないかという気がするが
本来学びたかったことのための土台はとりあえずできた。
正直 flux の書き方になっている action や store を順次 Redux の action や reducer に置き換えていくだけで移行自体に面白味はなかった。

メモ

React Router の基本的な使い方として、

<Route path="notes/:id/edit" component={NoteEdit} />

とすると、コンポーネント側では URL 中のパラメータを this.props.params.id のようにして取得できる。


main.js

<Route path="/" component={Dashboard}>
  <Route path="notes/:id/edit" component={NoteEdit} />
</Route>

Dashboard.js

<div className="page-Dashboard-main" role="form">
  {this.props.children ? React.cloneElement(this.props.children, { note }) : null}
</div>

としておくと、<NoteEdit />Dashboard から note を作って props として渡すことができる。
この目的で子コンポーネントにする方法は今まで知らなかった。

webpackでReact/Reduxを本番環境用にビルド(production build)する

はじめに

React に限らず、JavaScript を使ってると本番環境用のビルド(以後、production build)として圧縮や難読化を行う必要があります。
webpack にはそのためのプラグインとして UglifyJsPlugin があるのは知っていましたが、
単純にこのプラグインを適用しただけだとブラウザで以下のような warnings が出ていることがわかりました。

f:id:dackdive:20170307045306p:plain

  • Warning: It looks like you're using a minified copy of the development build of React. When deploying React apps to production, make sure to use the production build which skips development warnings and is faster. See https://fb.me/react-minification for more details.
  • You are currently using minified code outside of NODE_ENV === 'production'. This means that you are running a slower development build of Redux. You can use loose-envify (https://github.com/zertosh/loose-envify) for browserify or DefinePlugin for webpack (http://stackoverflow.com/questions/30030031) to ensure you have the correct code for your production build.

そのため、このエラーが表示されなくなるような、正しい production build 用の webpack の構成を一度調べてみたいと思います。

加えて、UglifyJsPlugin を使っていても redux-logger のログが出力されており、これは本番環境では出力されないことが望ましいので
そのような切り替えを行う方法についても一緒に調べてみます。

※ webpack v1を対象としていますが、分かる範囲で v2 との違いについても言及しています。


サンプル(GitHub

先に、今回の調査を踏まえたサンプルリポジトリはこちらです。

(この記事を書いたときの最新コミットはbdff7ec


やること1:webpack の各種プラグインを導入&設定する

使用する webpack のプラグインは以下です。


DefinePlugin で process.env.NODE_ENV を置換する

DefinePlugin はビルド実行時、コード中に埋め込まれたグローバル変数プラグインで定義した値に置換してくれるプラグインです。
たとえば

// webpack.config.js
plugins: [
  new webpack.DefinePlugin({
    PRODUCTION: JSON.stringify(true),
  }),
],

という定義があった場合、コード中の

if (PRODUCTION) {
  console.log('production');
}

if (true) {
  console.log('production');
}

に置き換えられます。

React などのライブラリには

if (process.env.NODE_ENV === 'development') {
  // only in development
}

のような処理が埋め込まれており、DefinePlugin でこの部分を置換します。

// webpack.config.js
plugins: [
  new webpack.DefinePlugin({
    'process.env': {
      NODE_ENV: JSON.stringify(process.env.NODE_ENV),
    },
  }),
],

これの何が良いかと言うと、次に紹介する UglifyJsPlugin と組み合わせることで到達しないコードが削除され、ファイルサイズの圧縮につながります。
※到達しないコード = if (false) { ... } のような部分のこと


UglifyJsPlugin を production build 時のみ適用する

冒頭でも紹介した UglifyJsPlugin も webpack.config.js に追加します。
これは開発時には OFF になってほしいので、たとえば NODE_ENV=production を指定した時のみ有効になるよう、以下のようにします。

// webpack.config.js
const PRODUCTION = process.env.NODE_ENV === 'production';

plugins: [
  new webpack.DefinePlugin({
    'process.env': {
      NODE_ENV: JSON.stringify(process.env.NODE_ENV),
    },
  }),
  ...(
    PRODUCTION ? [
      new webpack.optimize.UglifyJsPlugin({
        compress: {
          warnings: false,
        },
      }),
    ] : []
  ),
]    

UglifyJsPlugin のオプションですが、{ compress: { warning: false } } を指定しないとビルド時に

WARNING in bundle.js from UglifyJs
Condition always false [../~/react/lib/React.js:32,0]
Dropping unreachable code [../~/react/lib/React.js:32,0]
...

のような warning がずらっと出力されるので、指定しておきます。

また、参考にした create-react-app の webpack.config.prod.js ではそれ以外のオプションも色々と指定しているようですが、README に記載がないのとほとんどがデフォルト値のようなのでとりあえずこれでいいかと。


DedupePlugin, OccurenceOrderPlugin も production build 時のみ適用する

最後に、DedupePlugin と OccurrenceOrderPlugin という 2 つのプラグインも導入します。
DedupePlugin は重複するモジュールはひとつにまとめてくれるプラグイン、OccurenceOrderPlugin は利用頻度の高いモジュールのにをなるべく短い ID を降ることでファイルサイズの圧縮を図るプラグインという理解です。
先程の UglifyJsPlugin に続けてプラグインを定義します。

// webpack.config.js
const PRODUCTION = process.env.NODE_ENV === 'production';

plugins: [
  new webpack.DefinePlugin({
    'process.env': {
      NODE_ENV: JSON.stringify(process.env.NODE_ENV),
    },
  }),
  ...(
    PRODUCTION ? [
      new webpack.optimize.UglifyJsPlugin({
        compress: {
          warnings: false,
        },
      }),
      new webpack.optimize.OccurrenceOrderPlugin(),
      new webpack.optimize.DedupePlugin(),
    ] : []
  ),
]    

DedupePlugin については 公式ドキュメント

Note: Don’t use it in watch mode. Only for production builds.

という記載があるので production build 時のみ適用すべきですが、OccurrenceOrderPlugin については特にそういう記述はないので常に有効でもいいのかもしれません。


補足:webpack v2 との差分

webpack v1 -> v2 の migration guide を読むと、今回紹介したプラグインで関係している変更は以下のあたりでしょうか。

  • UglifyJsPlugin
    • { compress: { warnings: false } } はデフォルトになったので指定不要に(参考
    • sourceMap がデフォルトで出力されなくなったので、出力したい場合は明示的に { sourceMap: true } を追加(参考
  • OccurrenceOrderPlugin
    • 指定しなくても有効になったので、記載不要(参考
  • DedupePlugin
    • こちらも webpack2 では不要(参考


やること2:NODE_ENV = production のときは redux-logger が読み込まれないようにする

webpack の設定が終わったので、続いて Redux まわりの設定を。
redux-logger が production build 時には読み込まれないようにします。

これは README にも記載がありますが、webpack のときと同様に process.env.NODE_ENV の値を使って
process.env.NODE_ENV === 'development' のときだけ redux-logger がインポートされるようにします。

reducer の Hot Module Replacement の設定も考慮すると、store の設定は以下のようになります。

// store/configureStore.js

/* eslint-disable global-require */
import { createStore, applyMiddleware } from 'redux';
import rootReducer from '../reducers';

// eslint-disable-next-line no-underscore-dangle
const __PRODUCTION__ = process.env.NODE_ENV === 'production';

export default function configureStore(initialState) {
  const middlewares = [];
  if (!__PRODUCTION__) {
    const createLogger = require('redux-logger');
    middlewares.push(createLogger());
  }
  const store = createStore(
    rootReducer,
    initialState,
    applyMiddleware(...middlewares),
  );

  if (!__PRODUCTION__ && module.hot) {
    module.hot.accept('../reducers', () => {
      const newRootReducer = require('../reducers').default;
      store.replaceReducer(newRootReducer);
    });
  }

  return store;
}

これで production build 時には redux-logger がインポートされなくなりました。


おまけと TODO

冒頭で述べたブラウザの warning について

それぞれ ReactDOM および Redux 内で定義されていました。

NODE_ENV = production じゃない状態で minify されているかどうか」の判定方法、面白いですね。
(ダミーの function を定義し、function.name が関数名そのものと一致するかどうか判定し、一致しない = minify されている)

webpack の -p オプション

production build は webpack のデフォルトのオプションとしてもいくらかサポートされてるようです。
参考:https://webpack.github.io/docs/cli.html#production-shortcut-p

$ webpack -p

$ webpack --optimize-minimize --optimize-occurrence-order

というオプションをつけるのと同じで、それぞれのオプションについては https://github.com/webpack/docs/wiki/optimization を参照。

TODO

production build 時には console.log も除去したい。こちらを使えばできる?


リファレンス

webpack.config.js について

create-react-app の webpack.config.prod.js が参考になるほか、日本語記事では

Redux の configureStore.js について

この記事が参考になりました。
Practical Redux, Part 3: Project Planning and Setup · Mark's Dev Blog

ただ、記事では Redux Devtools などの設定も行っていますが、今回はそこまで手が出せなかった...

Inside Frontend #1に参加したメモ

こちらのイベントに行ってきました。

AMA(休憩時間中の質問コーナー)という取り組みが非常に良かったです。 以下、話を聞きながら手元でメモしてた内容。


Web over ServiceWorker by @jxck__

  • Capability against
    • Native/Low API
    • Installable
    • Performance
    • Permission
    • Offline
    • etc
  • The Extensible Web Manifesto : https://extensiblewebmanifesto.org/
  • Service Worker
    • Service Worker is API for Offline Support
    • Service Worker is Platform for New Capability
  • Proxy としての Service Worker
    • データのfetchなどを一旦 ServiceWorker がフック。キャッシュにあればそちらを利用し、なければ実際にfetchしてキャッシュに突っ込んでおく
  • Pusher としての Service Worker
  • TaskManager としての Service Worker
    • オフライン時に行われたアクションを覚えておき、回線が復活したときにサーバー側に同期する
    • onsync !== on-line/offline
  • ブラウザやタブが開いてなくても使える
  • 中級者向け Service Worker Tutorial | blog.jxck.io : https://blog.jxck.io/entries/2016-04-24/service-worker-tutorial.html
Foreign Fetch
  • 3rd Party が自分たちのサービス(JS)とそれに付随するService Workerを提供できるようにする仕組み
  • サブドメインを分けることで SW の責務を分けることができるようになった
  • Navigation Preload
    • fetch 時に Service-Worker-Navigation-Preload: true でリクエストすると、SW の起動を待たずに fetch できる


AMA: Progressive Web Apps

PWA化することによって改善される問題・PWA化のタイミング · Issue #4 · insidefrontend/issue1-ama
  • PWA 化のタイミングについては難しいが、SW は「使える環境であればより便利になるが、なくても普通に動く」ように作るべき
ChromeのブラウザPush通知 / Progressive Web Apps · Issue #12 · insidefrontend/issue1-ama
  • いきなり通知送る実装にしていることにも問題あり。Slack とかはうまくやっている
  • SW はドメイン単位で ON/OFF されるので、通知うざいからって無効化されると今後通知以外の実装を行ったときも使ってもらえない
  • 「再エンゲージとかにこだわる必要ない」はあくまであなたのユースケースであることに注意。それから、今の Web はユースケースよりも Capability を重視している
    • 例:エンゲージを高めるために SW を実現する(ユースケース)、SW というものを実現すると何かできることが増えるはず(Capability)


Web フロントエンドにおけるコンポーネント化のアプローチ by @1000ch

  • CSS のスタイルガイド
    • プロジェクトUIのドキュメントとして
    • デザイナーとの協業手段として
      • 理想:プロジェクトUIの整理や再利用のため
      • 現実:メンテされない
    • -> 何を解決すればいいのか本質的に見えてなかった
  • コンポーネントとはいったいなんなのか
  • コンポーネントの価値
    • デザインにおいてはUIの一貫性・統一性
    • ソフトウェアにおいては再利用性
  • これまでの問題点
  • これまでのワークフロー:
    • (Design) デザインデータを参考にする→デザインデータを更新する→
    • (Dev)HTML+CSSで再現する→スタイルガイドを更新する
    • → 誰がメンテナンスする問題
  • 協働のためのデザイン思考の再構築 : could : http://www.yasuhisa.com/could/article/ddd-ooux-job/
  • CSS のスコープ問題
    • HTML&CSS にはスコープがない
    • スコープが欲しい
      • CSS Modules, CSS in JSといった技術的に解決する方法と、Shadow DOM という仕様で解決する方法


アメブロ2016: 実録、アメブロフロントリニューアル275日


karmaを使ったSPA向けのE2Eテスト技法 by @kyo_ago

  • はじめに:「E2E テスト」より「Integration テスト」のほうが言葉としては適切
  • 伝えたいこと:SPA のテストにWebDriverは向かない
  • WebDriver
    • Selenium から発展したツール
    • ブラウザ
    • 2004年から始まる歴史あるツール
  • なぜ向かないか
    • あくまで画面を遷移していく「Webサイト」のためのツールだから
    • Driver によっては Dev Tools と競合(Dev Tools を同時に使えない状況でテストはつらい)
    • ターミナルだと console.log(obj) しても [Object] としか表示されなくてつらい
  • SPA の Integration テストには Karma
    • 元は AngularJS 用に開発されたが、現在は汎用的なツール
    • Protractor とは違う(Protractor はWebDriver 系)
  • Karma?
    • Unit テストがブラウザ上で動く
    • ver 1.0.0 で追加された customContextFile
    • HTMl に Unit テストを
  • Karma v1.0.0から登場したcustomContextFile。Unit テストをHTMLに埋め込むことができるので、実際のサイト上でテストを走らせるかのように書ける。ただしlocalhost
  • localhost 問題をどうするか
  • WebDriverは受け入れ用のテストツール、karmaは開発者向けのテストツール
  • おまけ:E2Eテストの指すもの
    • E2Eは対象範囲の話で、目的の話はしていない
    • 検証のため?開発のため?


AMA: React.js

自分が質問した内容。Redux でつらいこと2点

どこまでが Redux state で、どこまでが React の State か悩みます
  1. ほとんど Redux state に寄せているが、チェックボックスラジオボタンの状態ぐらいだと React state で良いかも。ただその判断基準は明確でないので、そのせいで判断に困ることが多くなるようであれば思い切って Redux に全部寄せるのはあり。
ビジネスロジックをどこに閉じ込めてますか?
  1. Redux とは関係なく扱うリソースごとに Model みたいなのを定義している。Model を Redux に乗せている、というイメージ


JSON Schema in Web Frontend by @pirosikick

終わった後に質問してみた:
react-jsonschema-form で逆にできないこと(どうしてもカスタマイズできないこと)はありますか?
A. バリデーションエラー時のメッセージがカスタマイズできない(多言語化できない?)ので、かなり無理矢理実装することになる。


参加できなかったセッションの資料

データビジュアライゼーションの作り方

shimizu’s Blocks - bl.ocks.org:D3 のサンプル集として参考になりそう

react-lightning-design-systemをVisualforceで使う

件名の通りですが少しハマりどころがあったのでメモ。
普通に react-lightning-design-system を使おうとすると SVG アイコン使用時に Unsafe attempt to load URL エラーが表示されます。

Unsafe attempt to load URL https://zakiyama-dev-ed.my.salesforce.com/assets/icons/utility-sprite/svg/symbols.svg#event from frame with URL https://zakiyama-dev-ed--c.ap2.visual.force.com/apex/LDSTest. Domains, protocols and ports must match.

いわゆる CORS: Cross Origin Resource Sharing、つまり salesforce.comvisual.force.com という異なるオリジンでリソースのやり取りをしようとしているのが原因です。
Issue にもあります。

これを回避するためにはコメントにもあるように、util.setAssetRoot(path) というメソッドを使います。
また setAssetRoot に渡す静的リソースへのパスは、Visualforce 側で

{!URLFOR($Resource.slds)}

を使い取得します。

Visualforce ページと、React 側のエントリーポイントとなる index.js は以下のようになります。

<apex:page showHeader="false" standardStyleSheets="false" docType="html-5.0">
<html>
  <head>
    <meta charset="utf-8" />
    <apex:stylesheet value="{!URLFOR($Resource.slds,'assets/styles/salesforce-lightning-design-system.min.css')}" />
  </head>
  <body>
    <div id="root"></div>

    <apex:includeScript value="{!URLFOR($Resource.app, 'bundle.js')}" />
    <script type="text/javascript">
      App.init(document.getElementById('root'), '{!URLFOR($Resource.slds)}/assets');
    </script>
  </body>
</html>
</apex:page>
// src/scripts/index.js
import React from 'react';
import { render } from 'react-dom';
import { util } from 'react-lightning-design-system';

import App from './components/App';
import '../stylesheets/index.scss';

export const init = function(el, assetRoot) {
  util.setAssetRoot(assetRoot);
  console.log('Set asset root as ', util.getAssetRoot());
  render(<App />, el);
};

この例では、webpack.config.js の設定でビルドした JavaScriptApp というライブラリ名でエクスポートしてます(参考

また、初期化用関数の引数として渡してますが

window.assetRoot = '{!URLFOR($Resource.slds)}/assets';

のように window 関数に直接変数を生やして index.js 側で参照するという方法もあります。


余談:<script> タグと <apex:includeScript> タグの読み込み順序に関して

上の例では、<apex:includeScript> による JS 読み込みが続く <script> タグ内のスクリプトより先に実行される必要がありますが
https://developer.salesforce.com/docs/atlas.ja-jp.204.0.pages.meta/pages/pages_compref_includeScript.htm
を見ると、loadOnReady を明示的に true にしない限り <apex:includeScript> の方がすぐに読み込まれるようです。
(すぐに、というのが曖昧ですが、一旦こちらの読み込みが先行すると考えておいて良さそうです)


Spring'17 で追加された <apex:slds> を使う場合

Spring'17 から Visualforce ページでも、静的リソースにアップロードすることなくプラットフォームから提供される SLDS が使えるようになります。
参考:Visualforce ページでの Lightning Design System の使用

これを使う場合、先ほどのコードは

 <html>
   <head>
     <meta charset="utf-8" />
-    <apex:stylesheet value="{!URLFOR($Resource.slds,'assets/styles/salesforce-lightning-design-system.min.css')}" />
+    <apex:slds />
   </head>
   <body>
     <div id="root"></div>

     <apex:includeScript value="{!URLFOR($Resource.app, 'bundle.js')}" />
     <script type="text/javascript">
-      App.init(root, '{!URLFOR($Resource.slds)}/assets');
+      App.init(root, '{!URLFOR($Asset.SLDS)}/assets');
     </script>
   </body>
 </html>

というように、静的リソースから SLDS を読み込んでいた部分を <apex:slds /> に置き換え、さらにパス部分は $Asset.SLDS という変数を使って取得するようにします。

(2017/02/20追記)

f:id:dackdive:20170220103817p:plain

バージョンは最新バージョンの 2.2.1 でなく 2.1.3 のよう。
あと読み込んでいるリソースが salesforce-lightning-design-system-vf.min.css なのも気になる。
(追記ここまで)


さらに、applyBodyTag または applyHtmlTag が false だった場合

リリースノートおよび開発者ガイドの該当ページによると、
Using the Lightning Design System | Visualforce Developer Guide | Salesforce Developers

if you set applyBodyTag or applyHtmlTag to false, you must include the scoping class slds-scope

とあるので、

-  <body>
+  <body class="slds-scope">

などとする必要があります。
(そういう意味で、上の例では applyBodyTag および applyHtmlTag を false にしていないのに <html> や <body> を使っているのは適切ではないですね)


おまけ:<apex:slds> の既知のバグ

検証中、<apex:slds> を使うと最初の Unsafe attempt to load URL エラーが発生することがわかりました。
どうやら $Asset.SLDS の URL が salesforce.com になっており、調べてみると StackExchange にありました。バグのようです。

近日中に修正されるようなので1週間後ぐらいに確認してみることとします。

(2017/02/20追記)
今日見たら直ってました。
StackExchange のコメントに更新はなく、Known Issues にもないので、動きを確認した限りですが。
(追記ここまで)


リポジトリ

webpack2でTree Shaking以外に何ができるようになったのかメモ

メモ。
webpack 2 正式リリース(バージョンは 2.2)だとか Tree Shaking という機能がいいらしい とかは目にしていたけど
結局 v1 -> v2 とメジャーバージョンが上がって Tree Shaking 以外に何が変わったの?というのがよくわからなかったので調べてみた。
なお、Tree Shaking については最後に記載している。

参考にしたサイト

「webpack 2 whats new」とかでググって、見つけられたのはここ。

What's new in webpack 2 · GitHub

(WIP て書いていて最新の情報かどうかわからないけど)ここに書いてある内容で気になったものをピックアップする。

なお、Getting Started with webpack 2 という記事は残念ながら v1 → v2 における新機能などについては言及されていなかった。


v2 からできるようになったこと

ES6 Modules をサポート

Babel を使ってトランスパイルしなくても import, export をそのまま解釈できるようになった。
ただし、Babel なしだと import/export 以外の ES2015 をトランスパイルしてくれるわけではない ので注意。
(こちらで確認した限り、たとえば constconst のまま、という意味)

また、Babel なしだと webpack.config.babel.js というファイル名で ES2015 形式で書けるようにはならないので注意。


System.Import による動的インポート


(2017/02/08追記)

Twitter

というのを教えていただいた。

確認したところ
Release v2.1.0-beta.28 · webpack/webpack

でたしかに

add import() as Code Splitting construct. It should be used instead of System.import when possible. System.import will be deprecated in webpack 2 release (removed in webpack 3) as it's behavior is incorrect according to the spec.

という記載があったので、System.import() のかわりに単に import() を使った方がいい。
試していないが、以下 System.import() 部分を import() に置き換えても動くはず。

(追記ここまで)


コード中に System.import('モジュールへのパス') と書くことで動的にモジュールをインポートできる。
reuqire と役目は同じ。

function onClick() {
    System.import("./module").then(module => {
        module.default;
    }).catch(err => {
        console.log("Chunk loading failed");
    });
}

特徴としては System.Import の結果が Promise として返される ところ。
これによりモジュール読み込みに失敗したときにこちら側でハンドリングすることができる。

System.Import で読み込もうとしているモジュールはチャンクファイルとして本体のファイルとは別のファイルが生成されるよう。

たとえば上のコードを index.js として、ディレクトリ構成が

├── src
│   ├── index.js
│   └── module.js
└── webpack.config.js

webpack.config.js

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'build')
  },
  ...

となっていた場合、ビルド後は

├── build
│   ├── 0.bundle.js
│   └── bundle.js

となった。

また、

System.import("./routes/" + path + "/route")

のように変数で指定することも可能。
この場合は(require のときも同じだと思うが)webpack は可能性のあるファイルを自動的に判別してくれる。
このときも可能性のあるモジュールファイルごとにチャンクファイルが生成される。


webpack.config.js で関数を export することが可能に

v1 のときは、webpack.config.js は

module.exports = {
  entry: './src/index.js',
  ...
}

というようにオブジェクトを export していたが、関数を export することもできるようになった。

module.exports = function(options) {
  return {
    entry: './src/index.js',
    ...
  };
}

これで何が嬉しいのかと言うと、コマンドライン実行時に --env を指定することでこの関数に任意の設定情報を渡せるようになった

--env foo

のように文字列を渡すか、

--env.foo 1 --env.bar 2

のようにするとオブジェクトを渡すことができる。

例:
// webpack.config.js
module.exports = function(options) {
  console.log(options);
  return {
    entry: './src/index.js',
    ...
# "scripts": { "webpack": "webpack" } は package.json に記載済み
$ npm run webpack -- --env foo
> webpack2-playground@1.0.0 webpack /Users/yamazaki/workspace/webpack2-playground
> webpack "--env" "foo"

foo

$ npm run webpack -- --env.foo 1 --env.bar 2

> webpack2-playground@1.0.0 webpack /Users/yamazaki/workspace/webpack2-playground
> webpack "--env.foo" "1" "--env.bar" "2"

{ foo: 1, bar: 2 }


webpack.config.js の書き方が一部変更

これは機能ではないが一応。
Migrating from v1 to v2 に従ってマイグレーションが必要。


おまけ:Tree Shaking について

最後におそらく webpack2 の目玉機能である Tree Shaking について。
すごいざっくりとした理解だけど、「export してるけどどこからも import されていないモジュールをバンドル時に自動的に削除して、ファイルサイズを小さくしてくれる」機能のよう。

Tree Shaking を利用するには Babel で import/export をトランスパイルしないように設定してあげる必要があり、具体的には .babelrc

{
  "presets": ["es2015"]
}

としていたところを

{
  "presets": [["es2015", { "modules": false }]]
}

のように変更してあげる必要がある。

ただ、

という問題にぶち当たった。これは、es2015 の preset を webpack.config.js に寄せるしかないのか...

日本語の解説記事だと冒頭でもリンク貼ったけどこちらが参考になる。

また、英語だと

Webpack 2 Tree Shaking Configuration – Modus Create: Front End Development – Medium
はわかりやすいが若干情報が古く、ただ サンプルコード の方は情報がアップデートされてるので参考にできる。

あとは
Tree-shaking ES6 Modules in webpack 2
だろうか。


おわりに

参考にしたサイト自体の情報が古かったり間違ってたりする可能性はあるが、とりあえず↑に書いたことは自分でも動作確認できた(webpack のバージョンは 2.2.1)。
日頃からリポジトリをウォッチしてたり changelog 見てたら正しい情報をキャッチアップし続けられるんだろうけど、そういうことできる人はほんとすごいなと思う。
(私はこういうことが知りたいと思ったときにどこを見るようにすればいいのかもよくわかってません...)

gulpタスクに.envで定義した環境変数を渡すならgulp-envでなくnode-env-fileを使う

はじめに

タイトルの通り。
gulp タスクに .env のようなファイルで定義した環境変数を渡したくて、
gulp-envnode-env-file を比較した。
結論としては node-env-file の方が使い勝手が良かった。


それぞれの使い方

node-env-file
// gulpfile.js
var gulp = require('gulp');
var env = require('node-env-file');

env('.env');

gulp.task('default', function() {
  console.log(process.env.FOO);
});

こんな感じ。.env には

FOO=foo

としておくと gulp タスク側で process.env.FOO で参照できる。

gulp-env
// gulpfile.js
var gulp = require('gulp');
var env = require('gulp-env');

env({
  file: '.env.json',
  vars: {
    // any variables you want to overwrite 
  }
});

gulp.task('default', function() {
  console.log(process.env.BAR);
});

こう。
vars はオプションで、指定しなくても良い。指定すると .env.json の変数の内容をさらに上書きできる。(使いみちがわからず)

.env.json はこのように JSON 形式で記述する。(json 以外のフォーマットも対応しているよう)

{
  "BAR": "bar"
}


node-env-file が gulp-env より優れていると感じた点

実行時に変数を上書きできる

node-env-file は gulp タスク実行時に

$ FOO=hoge gulp

とすることで、.env に定義した変数を上書きすることができる。

### node-env-file の場合
$ gulp
foo

$ FOO=hoge gulp
hoge

### gulp-env の場合
$ gulp
bar

# コマンド実行時に上書きできない
$ BAR=hoge gulp
bar
オプションで、.env ファイルが存在しなくてもエラーにならないようにすることができる

node-env-file には raise というオプションがあって、これを false にするとファイルが存在しなくてもエラーにしないことができる。

env('.env', { raise: false });


別解として

node-foreman をインストールするという手もある。

$ nf run gulp

[Vim]NeomakeでFlowを実行したときにexit code 64が表示されたときのメモ

Vim の Lint チェックに Neomake を使っていて、実行時に

Neomake: flow: completed with exit code 64.

と表示されてしまったときのメモ。
Flow のバージョンは 0.37.4。


原因と解決策

https://github.com/neomake/neomake/blob/master/doc/neomake.txt#L319-L327
によると g:neomake_verbose という変数があるので、.vimrc

g:neomake_verbose=3

を追加して Neomake を再度実行する。

:messages

でメッセージを表示したところ、以下のようになっていた。

Neomake [0.850]: [#2] stderr: flow: ['flow: unknown option ''--old-output-format''.', 'Usage: flow [COMMAND] ', '', 'Valid values for COMMAND:', '  ast             Print the AST', '  autocomple
te    Queries autocompletion information', '  check           Does a full Flow check and prints the results', '  check-contents  Run typechecker on contents from stdin', '  coverage        Show
s coverage information for a given file', '  find-module     Resolves a module reference to a file', '  find-refs       Gets the reference locations of a variable or property', '  force-recheck
   Forces the server to recheck a given list of files', '  gen-flow-files  EXPERIMENTAL: Generate minimal .js.flow files for publishing to npm.', '  get-def         Gets the definition location
 of a variable or property', '  get-importers   Gets a list of all importers for one or more given modules', '  get-imports     Get names of all modules imported by one or more given modules',
'  init            Initializes a directory to be used as a flow root directory', '  ls              Lists files visible to Flow', '  port            Shows ported type annotations for given file
s', '  server          Runs a Flow server in the foreground', '  start           Starts a Flow server', '  status          (default) Shows current Flow errors by asking the Flow server', '  sto
p            Stops a Flow server', '  suggest         Shows type annotation suggestions for given files', '  type-at-pos     Shows the type at a given file and position', '  version         Pri
nt version information', '', 'Default values if unspecified:', '  COMMAND^Istatus', '', 'Status command options:', '  --color                           Display terminal output in color. never,
always, auto (default: auto)', '  --from                            Specify client (for use by editor plugins)', '  --help                            This list of options', '  --ignore-version
                 Ignore the version constraint in .flowconfig', '  --json                            Output results in JSON format', '  --no-auto-start                   If the server is not ru
nning, do not start it; just exit', '  --one-line                        Escapes newlines so that each error prints on one line', '  --pretty                          Pretty-print JSON output (
implies --json)', '  --quiet                           Suppress output about server startup', '  --retries                         Set the number of retries. (default: 3)', '  --retry-if-init
                 retry if the server is initializing (default: true)', '  --sharedmemory-dep-table-pow      The exponent for the size of the shared memory dependency table. The default is 17, i
mplying a size of 2^17 bytes', '  --sharedmemory-dirs               Directory in which to store shared memory heap (default: /dev/shm/)', '  --sharedmemory-hash-table-pow     The exponent for t
he size of the shared memory hash table. The default is 19, implying a size of 2^19 bytes', '  --sharedmemory-log-level          The logging level for shared memory statistics. 0=none, 1=some',
 '  --sharedmemory-minimum-available  Flow will only use a filesystem for shared memory if it has at least these many bytes available (default: 536870912 - which is 512MB)', '  --show-all-error
s                 Print all errors (the default is to truncate after 50 errors)', '  --strip-root                      Print paths without the root', '  --temp-dir                        Direct
ory in which to store temp files (default: /tmp/flow/)', '  --timeout                         Maximum time to wait, in seconds', '  --version                         (Deprecated, use `flow vers
ion` instead) Print version number and exit', '']
Neomake [0.854]: Channel has been closed: channel 2 closed

どうやら --old-output-format という古いオプションをつけて実行しており、そんなオプションないよと怒られているようだ。

参考(関係ある?):Remove --old-output-format · Issue #2844 · facebook/flow

で、今度は Neomake 側を調べてみたところこの PR でどうも修正したように見える。

Fix flow output by rafaelrinaldi · Pull Request #880 · neomake/neomake

そういえばしばらくプラグインのアップデートとかしてなかったなと思い、私は dein.vim を使っているので

:call dein#update()

でアップデートしてみたところ lint が通るようになった。

※ただ、それ以外にも色々 Neomake ではうまくいかないことがあったので現在は ALE を使うことにした。それはまた別途書くことにする。