読者です 読者をやめる 読者になる 読者になる

getalog

console.log geta6

こないだ社内の勉強会でwebpackのこととか話したのでまとめた

f:id:geta6:20160127192516p:plain

webpackとは

  • いろんなファイルをtranspileしてES5のJavaScriptに変換してくれるやつ
  • AMDかCommonJSの形式でファイルをロード(CommonJSならrequire)すると、transpileしたファイルをロードしてくれる
    • クライアント側のjsコードでもrequireを使用することができる
    • assetとしてビルドして配布するイメージ
  • コードが共用の場合、設定を変えることで素のrequireを利用するサーバー用コードと、webpackがpolyfillしたrequireを利用するクライアントコードとを別々に生成できる
  • 全てがJavaScriptになる、画像やCSS
    • 画像は「Base64かFilePath」に
    • CSSは「headにstyleを挿入するjsコード」に
  • 特定のファイルをどのようにtranspileするかはpathマッチングでプラグイン形式で設定する
    • webpackではこのプラグインのことを「ローダー」と呼ぶ
    • あるパターンに対して複数のローダーを積むこともできる
    • 例えば、/\.styl$/に対して['style', 'css', 'stylus']のように設定すると、「stylusをcssに変換して、cssを整形して、<style><head>へ挿入するjsコードに変換する」といったことができる

ビルドコードはだいたいこんなイメージ

require('./hoge.jsx');

↓

function require(moduleId) {
  return installedModules[moduleId];
}
const installedModules = [
  function() {
    eval(__TRANSPILED_CODE__);
  }
];
require(0);

csspngをrequireする時の使用例

import React, { Component } from 'react';
import logo from './logo.png';
import style from './MyComponent.styl';

class MyComponent extends Component {
  componentWillMount = () => style.use()
  componentWillUnmount = () => style.unuse()
  render = () => {
    return (
      <div id='MyComponent'>
        <img src={logo} />
        <span>Welcome to My Home Page </span>
      </div>
    );
  }
}

browserifyと何が違うのか

  • browserifyは「何が何でも一つのファイルに全部まとめるマン」
    • webpackは「時と場合に応じて適宜ファイルを分割するおじさん」
  • browserifyはシンプルな仕組みしか用意しない、だいたいタスクランナーが欲しくなるやつ
    • webpackはそのままでもある程度使えるようになってるのでconfig fileを準備する
// プロジェクトルートに置かれたwebpack.config.js
module.exports = {
  entry: {
    main: './src/main.js'
 },
  output: {
    path: './build',
    filename: '[name].js',
  }
};

webpackはgulpとかと同じようにjs-cli/js-interpretを採用しているのでwebpack.config.babel.jsなどとすればES6の記法でconfigが書ける

  • どっちがいいとかは無い、宗教みたいなもん
  • webpackを使えば「pngをロードした時に10kb以下ならbase64、10kb超えたらファイル分割する」とかいうことができる、そんくらい

MultipleEntry

  • 複数の設定・複数のファイルを同時にtranspileすることができる

一つの設定から複数のファイルを書き出すことができる

// webpack.config.js
module.exports = {
  entry: {
    main: './src/main.js'
    renderer: './src/renderer.js',
  },
  output: {
    path: './build',
    filename: '[name].js',
  }
};

複数の設定から複数のファイルを書き出すこともできる

// webpack.config.js
module.exports = [{
  entry: './src/main.js',
  output: {
    path: './build',
    filename: '[name].js',
  },
  target: 'node',
}, {
  entry: './src/renderer.js',
  output: {
    path: './build',
    filename: '[name].js',
  },
}];

DeadCodeElimination

  • webpackに搭載されているプラグインを使用すると、DEAD CODEが簡単に削除できる
    • UglifyJsPlugin、DefinePlugin

DefinePlugin

テンプレートのようなもの、コードの中からパターンを発見して置き換えてくれる

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

コード中にprocess.env.NODE_ENVというパターンを見つけると、'development'などという文字列に置き換えてtranspileする

UglifyJsPlugin

通常通りのコード圧縮

UglifyにはConditional compilationという機能がある

簡単に言うと、常にfalseになるconditional blockはdropするよというモード

組み合わせる

if (process.env.NODE_ENV === 'development') {
  console.log('debug message here');
}if ('development' === 'development') {
  console.log('debug message here');
}

or

if ('production' === 'development') { // ここは常にfalseなのでuglifyするとdropされる
  console.log('debug message here');
}

便利なES6/7の書き方

arrow functionの即時実行

(() => {})()

jsxの文法中に書くのに使う、bindいらない

<div>
  {(() => {
    if (this.isSignedIn()) {
      return <div>Hello, {this.props.account.get('name')}!</div>;
    } else {
      return <a href='/signin'>Please sign in!</a>;
    }
  })()}
</div>

spread operator

DEBUGがtrueなら[1, 2, 3]、DEBUGがfalseなら[1, 2, 3, 4, 5]というarrayを作りたいとすると、下記のように書くことができる

[
  1, 2, 3,
  ...(DEBUG ? [] : [4, 5]),
]

spread演算子(...)によって続くカッコの中身が展開され、concatのような挙動になる

これはObjectにも適用することができる

{
  ...(DEBUG ? {a: 'a'} : {})
}

object assignment

こんな機能がある

const { a, b } = { a: 1, b: 2 };

実はキーワード引数的に利用出来る

const test = ({ key, val }) => {
  console.log(key); // hoge
}

test({ key: 'hoge' });

変数からobjectを作る時も楽

const key = 'hoge';
console.log({ key }); // { key: 'hoge' }

object return function

arrow functionからいきなりobjectを返す時に楽

(context) => ({
  hoge: context.getHoge(),
  fuga: context.getFuga(),
});

async/await

右辺にPromiseをとると同期的に待ってくれるようになる文法

functionの前にasyncと宣言すると、直下のスコープでawaitが使えるようになる

もう非同期も怖くない

const keys = (key) => new Promise((resolve, reject) => {
  redisClient.keys(key, (err, val) => err ? reject(err) : resolve(val));
});

async () => {
  const keyList = await keys('*');
  console.log(keyList);
};

waitも書ける

const wait = (time) => new Promise(resolve => setTimeout(resolve, time));

async () => {
  console.log('hoge');
  await wait(5000);
  console.log('fuga'); // 5秒後に表示される
};

asyncされた関数はPromiseを返すようになるので、async functionはawaitできる

async () => {
  await (async () => {
    await wait(1000);
  })();
};

右辺にとるのはthennableなら何でもよい、並列実行もできる

async () => {
  const [hoge, fuga, piyo] = await Promise.all([
    axios.get('/hoge.json'),
    axios.get('/fuga.json'),
    axios.get('/piyo.json'),
  ]);
  console.log(hoge, fuga, piyo);
};

babelのstage-0とruntimeが必要、ES5でも動くようにtranspileしてくれる

class context binding

あるES6 classについて、すべてのメソッドのcontextを自身に拘束したい場合がある(主にReact.Component)

通常ならばおそらくこう書かれる

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.bindedMethod = this.bindedMethod.bind(this);
  }

  bindedMethod() {
    console.log(this); // always MyComponent
  }
}

ところで、クラスには下記のような感じでメンバー変数を定義できる

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.bindedMethod = this.bindedMethod.bind(this);
  }

  hoge = 32 // コレ

  bindedMethod() {
    console.log(this.hoge); // always 32
  }
}
  • 「arrow functionがthisをバイパスすること・メンバー変数を定義できること」
  • この二つを利用して下記のように書くと、簡単にcontextを拘束することができる
class MyComponent extends React.Component {
  constructor(props) {
    super(props);
  }

  hoge = 32

  bindedMethod = () => {
    console.log(this.hoge); // always 32
  }
}

よく使うjsライブラリ

strip-loader

  • webpackのloader
  • 'console.log'ってStringを渡せばtranspileした時にそのコードが消滅する
const strips = [
  'console.log',
  'console.info',
  'console.warn',
  'console.error',
  'console.assert',
].map(strip => `strip[]=${strip}`).join(',');

{
  loaders: [
    {
      test: /\.jsx?$/,
      exclude: /node_modules/,
      loaders: [...(DEBUG ? [] : [`strip?${strips}`]), 'babel'],
    },
  ]
};

classNames

  • HTMLのクラス名を生成する
this.setState({
  classNames: { hoge: true, fuga: true },
}, () => {
  className(this.state.classNames); // => 'hoge fuga'
});

domready

  • jQuery$.ready(function() {})相当
  • いざjQueryを破滅させて一番最初に困るのがdocument.readyの取り方だと思う
  • $.readyと互換性がある、既にreadyなら実行してくれる
domready(() => {
  console.log('ready!!');
  setTimeout(() => {
    domready(() => {
      console.log('ready!!'); // ちゃんと呼ばれる
    });
  }, 1000);
});

axios

  • Promiseを返すHTTP requester
    • つまりasync/await最適
  • nodeでもclientでも使える
    • nodeでもclientでも使える(重要)

mousetrap

  • 「Shift押しながらK」とか「Command押しながらG」とかのキーバインドを、Stringで指定できる
  • キーを順番に押した時の挙動なども簡単にbind可能
// single keys
Mousetrap.bind('4', () => console.log('4'));
Mousetrap.bind('esc', () => console.log('keyup escape'));


// combinations
Mousetrap.bind('command+shift+k', () => console.log('command shift k'));


// multiple
Mousetrap.bind('up up down down left right left right b a enter', () => {
  console.log('konami code');
});

上のコードで御察しの通り、インスタンス化することもできる

Velocity

  • きれいなアニメーション
  • jQueryありでもなしでも動く
  • tumblrのダッシュボードでも使用されてるらしい

jest-cliでcollectCoverageが動かない時の直し方メモ

備忘録

  • babel*@5 (勘弁して
  • jest-cli@0.8.2

TL;DR

preprocessor.jsbabel.transformretainLinesオプションを渡して、キャッシュをクリアする

くわしいめも

jestの設定はこんな感じ

{
  "name": "test",
  "rootDir": "./src",
  "cacheDirectory": "<rootDir>/../tmp",
  "collectCoverage": true,
  "scriptPreprocessor": "<rootDir>/../preprocessor.js",
  "preprocessorIgnorePatterns": [
    "node_modules"
  ],
  "unmockedModulePathPatterns": [...]
}

最初に書いたpreprocessor.jsがこれ

var babel = require('babel-core');

module.exports = {
  process: function(src, filename) {
    if (!babel.canCompile(filename)) {
      /* ごにょごにょする */
    } else {
      return babel.transform(src, { filename: filename }).code;
    }
  }
};

出るエラーがこちら

Failed with unexpected error.
/path/to/project/node_modules/jest-cli/src/jest.js:230
        throw error;
        ^

TypeError: Cannot read property 'text' of undefined
    at /path/to/project/node_modules/jest-cli/node_modules/istanbul/lib/report/html.js:236:45
    at Array.forEach (native)
    at annotateFunctions (/path/to/project/node_modules/jest-cli/node_modules/istanbul/lib/report/html.js:219:26)
    at HtmlReport.Report.mix.writeDetailPage (/path/to/project/node_modules/jest-cli/node_modules/istanbul/lib/report/html.js:422:9)
    at /path/to/project/node_modules/jest-cli/node_modules/istanbul/lib/report/html.js:484:26
    at SyncFileWriter.extend.writeFile (/path/to/project/node_modules/jest-cli/node_modules/istanbul/lib/util/file-writer.js:57:9)
    at FileWriter.extend.writeFile (/path/to/project/node_modules/jest-cli/node_modules/istanbul/lib/util/file-writer.js:147:23)
    at /path/to/project/node_modules/jest-cli/node_modules/istanbul/lib/report/html.js:483:24
    at Array.forEach (native)
    at HtmlReport.Report.mix.writeFiles (/path/to/project/node_modules/jest-cli/node_modules/istanbul/lib/report/html.js:477:23)
npm ERR! Test failed.  See above for more details.

どうやらjestじゃなくてistanbulのエラーっぽい

このエラー、なんか見たことあると思ったらbabelでretainLinesをつけずにtranspileしたコードをistanbulにかけるとlinenoが取れなくなって出るやつでした

なので、preprocessorでretainLinesを渡すようにした、ついでにcoverageちゃんととれるようにignore lineつけるようにする

var babel = require('babel-core');

module.exports = {
  process: function(src, filename) {
    if (!babel.canCompile(filename)) {
      /* ごにょごにょする */
    } else {
      return babel.transform(src, { filename: filename, retainLines: true, auxiliaryCommentBefore: 'istanbul ignore next' }).code;
    }
  }
};

キャッシュディレクトリにpreprocess-cacheってのができてると思うので、削除する

で、動かすとなおる

electron@0.35のコードをwebpackしたら"electron"がないって言われた

electronをcommonjsモジュールとして追加してあげる必要がある。

最初、何を思ったのかexternalとかIgnorePluginとかで無視しようとして時間を浪費したのでメモ。

NG

export default {
  plugins: [
    new webpack.IgnorePlugin(/^(?:electron)$/),
  ]
}

NG

export default {
  externals: ['electron'],
}

OK

export default {
  plugins: [
    new webpack.ExternalsPlugin('commonjs', ['electron']),
  ]
}

apolloで買った楽曲をまとめてダウンロードするためのchrome拡張を作った

ネット同人音楽即売会APOLLOが開催の運びとなりました。

早速楽曲を買ったんですが、購入した曲をダウンロードする時に一個一個ばらけてて非常にめんどくさい。

なので、これをまとめてダウンロードできるようにするボタンを購入後ページに表示するchrome拡張を作りました。

Apollo Booster - Chrome Web Store

使い方は簡単、インストールしてから下記いずれかの注文後商品ダウンロードページを開くだけです。

  • 購入後のチェックアウト画面
  • 購入商品一覧

はかどる。

babel6のバグを引き当てた

gulpfileの中でasync/await使ってgazeでwatchしてたんだけど、watcherの中でconstを使うとSyntaxErrorが出る問題に遭遇した。

const copy = (source, dest) => new Promise((resolve, reject) => {
  ncp(source, dest, err => err ? reject(err) : resolve());
});

const watch = pattern => new Promise((resolve, reject) => {
  gaze(pattern, (err, watcher) => err ? reject(err) : resolve(watcher));
});

gulp.task('copy', async () => {
  const watcher = await watch(`${src}/**/*`);
  watcher.on('changed', async file => {
    util.log('[changed]', file);
    const rel = file.substr(path.join(__dirname, `..${src}`).length);
//  ^^^^^ SyntaxError: iter.js: "rel" is read-only (This is an error on an internal node. Probably an internal error. Location has been estimated.)
    await copy(`${src}/${rel}`, `${dst}/${rel}`);
  });
});

どうやらbabelのバグらしいという情報を得た、issue見る限り同じ症状っぽい。

タグがpending triageになってたから直るまで時間かかりそうで、ちょっと作りたいものがあるので直るまで5系使って待つことにした。