webpack 3 から webpack 4 へアップデートする際にハマりそうなこと

1. Overview

なにかと破壊的な変更が多い webpack ですが, v3 から v4 にアップデートする際にも破壊的な変更があるので, 自分の備忘録として残しておきます.

2. webpack-cli

webpack 4 からは, コマンドライン機能が webpack-cli に分離されました. したがって, npm install する際に以下のようにする必要があります.

$ npm install --save-dev webpack webpack-cli

この対応は, あっという間に完了するかと思います.

3. mini-css-extract-plugin

以下の, webpack.config.js は v3 であれば正常に動作するものです.

const webpack            = require('webpack');
const ExtracktTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
  entry: ['./src/main.js', './src/main.css'],
  output: {
    filename: 'app.js',
    path: `${__dirname}`
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: 'babel-loader'
      },
      {
        test: /\.css$/,
        use: [
          ExtracktTextPlugin.extract({
            use: [
              'css-loader',
              'postcss-loader'
            ]
          })
        ]
      },
      {
        test: /\.png$/,
        use: 'url-loader'
      }
    ]
  },
  plugins: [
    new webpack.LoaderOptionsPlugin({
      options: {
        config : {
          path: './postcss.config.js'
        }
      }
    }),
    new ExtracktTextPlugin({
      filename: './app.css'
    })
  ],
  devtool: 'source-map'
};

ところが, v4 からは, extract-text-webpack-plugin を使うとエラーが発生します. 代わりに, mini-css-extract-plugin を使う必要があります. それを使って書き直した webpack.config.js です.

const webpack            = require('webpack');
const ExtracktTextPlugin = require('mini-css-extract-plugin');

module.exports = {
  entry: ['./src/main.js', './src/main.css'],
  output: {
    filename: 'app.js',
    path: `${__dirname}`
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: 'babel-loader'
      },
      {
        test: /\.css$/,
        use: [
          ExtracktTextPlugin.loader,
          'css-loader',
          'postcss-loader'
        ]
      },
      {
        test: /\.png$/,
        use: 'url-loader'
      }
    ]
  },
  plugins: [
    new webpack.LoaderOptionsPlugin({
      options: {
        config : {
          path: './postcss.config.js'
        }
      }
    }),
    new ExtracktTextPlugin({
      filename: './app.css'
    })
  ],
  devtool: 'source-map'
};

CSS の module.rules.use が変わっていることに着目してください. extract-text-webpack-plugin では extract というクラスメソッドがありましたが, mini-css-extract-plugin では, それがありません. 代わりに, 上記のように loader を指定する必要があります.

コンピュータサイエンスの世界に入って 10 年が経ちました

Prologue

今日, 2018 年 5 月 6 日は, 私がコンピュータサイエンスの世界に入って 10 年が経った日です. つまり, 2008 年 5 月 6 日にコンピュータサイエンスの世界に入りました. きっかけは … 大学時代, とりあえず建築学科に進学しましたが, やる気なしおちゃん … 3 年を棒に振りました. しかし, 大学 4 回生のときに受講したプログラミングの講義をきっかけにこれなら熱中できるかも … と強引な予測のもと, とりあえず基本情報技術者の資格を取得しよう … それが始まりでした. つまり, 10 年前の今日, 基本情報技術者の 1 回目の講義日だったわけです. 普通の人は, いまどきだと, iOS アプリを作ってみたいとか Web アプリを作ってみたいとかプログラミングがコンピュータサイエンスの入り口という方が多いと思いますが, 私の場合は, きっかけがちょっと変わってますよね.

2008

約 3 ヶ月の学習の末, 基本情報技術者を取得することができました. 就職活動もやめていたし, 大学生活が不完全燃焼ということもあって, 専攻を変えて大学院に進学することを決めました. 院試のために, C 言語の基礎中の基礎みたいなのを独学で学びました. 朝は, 大学の図書館で院試の勉強, 昼から夕方まで卒論, 夕方から22 : 00 まで再度, 大学の図書館で院試の勉強という生活が半年ほど続きました.

2009

何はともあれ, 北陸先端科学技術大学院大学に進学したわけですが, 初めて実際にプログラムを書いたり, UNIX OS (Solaris) に触れたり, LaTex に苦戦したりの前半半年を過ごしました. 進学当初は, 音楽が好きだったこともあり, 楽器メーカーで電子楽器の製作 (組み込みプログラミング) をしたいと考えていましたので, 音情報処理研究室を選択しました. しかし, 結構な誤算で音の物理特性や音信号処理の数学には強くなったものの, あまりプログラミングをしたり, コンピュータサイエンスを学んだりするような専攻ではなかったので徐々にモチベーションは下がっていきました.

2010

研究のモチベーションがあがらない, 就職活動でなんども北陸 <-> 東京の往復, 強制収容所での生活などのストレスが重なり, うつになりました.

2011 – 2014

My Library を参照してください.

2015 – 2016

D 社に新卒入社. しかし, まともにプログラミングさせてもらえず, テスター・社内ニートになり, ここでいたら自分の目的, すなわち, 東京のレベルの高い Web 企業でスキルを向上させることが達成できないと考え, 2016 年 10 月 31 日で退社しました. そして, 2016 年 11 月 1 日に C 社に転職しました.

2017 – 2018

たまたま受検した, 応用情報技術者試験をきっかけにコンピュータサイエンスを学ぶ楽しさやスキル向上のために東京にきたことを思い出し, できるだけ仕事を早く終わって, 東京都立図書館にいって学ぶ毎日 … とても充実しています !

Epilogue

この 10 年間を振り返ってみると, いくつかのターニングポイントがあったように思います.

  • 基本情報技術者試験の学習を始めたこと
  • 大学院に進学したこと
  • うつになったこと
  • Y 社から内定をいただき, 入社辞退したこと
  • JavaScript, Web Audio API に出会ったこと
  • 学生 1 年 + ニート 1.5 年 + アルバイト 1.5 年 の 4年間
  • 東京にきたこと
  • 応用情報技術者試験をきっかけに, 忘れかけていたことを思い出したこと

次は, どんなターニングポイントが待っているのかわかりませんが, いつどこでどんなターニングポイントが待っていようと, 自分のやりたいことをやるだけです. 通勤中に自分のプロダクトを実装したり, 技術書を読んだり, 仕事で学んで, 早く帰って図書館にいって学ぶ … もしかすると, そういったルーティンが自分にとってよきターニングポイントを呼び寄せるのかもしれません.

Media Source Extensions API

1. Overview

Media Source Extensions API (以下, MSE) は, W3C によって標準化されている HTTP ダウンロードを利用してストリーミング再生するために作られた JavaScript API です. この記事では, MSE を利用して, 簡単な MPEG-DASH コンテンツを再生するための実装を紹介します.

2. Create resources

再生するコンテンツのリソースを生成します. 生成には, ffmpeg と MP4Box が必要になるので, brew などでインストールしておきます.

$ brew install ffmpeg
$ brew install MP4Box

次に, 対象となる MP4 動画を ffmpeg でエンコードします.

ffmpeg -i ./input.mp4 \
  -vcodec libx264 \
  -vb 500k \
  -r 30 \
  -x264opts no-scenecut \
  -g 15 \
  -acodec aac \
  -ac 2 \
  -ab 128k \
  -frag_duration 5000000 \
  -movflags frag_keyframe+empty_moov \
  ./encoded.mp4

そして, MP4Boxを利用して, 動画を分割し, セグメントファイルとプレイリストを生成します.

MP4Box -frag 4000 \            
  -dash 4000 \
  -rap \
  -segment-name sample \
  -out ./output.mp4 \
  ./encoded.mp4

以上で, プレイリストである output.mpd と output.m4s と連番になったセグメントファイルが生成されます. MPD (Media Presentation Description) ファイルの実体は, セグメントファイルをどの順番で再生するかが記述された XML ファイルです.

3. Media Source Extensions API

リソースの準備ができたので, MSE を利用した JavaScript の実装を紹介します.

'use strict';

class MSE {
    constructor(video, file) {
        this.getDescription(video, file);
        this.segmentIndex = 0;
        this.mediaAppended = false;
    }

    // MPD ファイルを取得し, MIME タイプやコーデックの情報を取得する
    getDescription(video, file) {
        const xhr = new XMLHttpRequest();

        xhr.open('GET', file, true);
        xhr.responseType = 'document';

        xhr.onload = () => {
            this.mpd = xhr.response;

            const representation = this.mpd.querySelector('Representation');
            const mimeType       = representation.getAttribute('mimeType');
            const codecs         = representation.getAttribute('codecs');

            this.type = `${mimeType}; codecs="${codecs}"`;

            this.initVideo(video);
        };

        xhr.send(null);
    }

    // セグメントを追加できるようにする
    initVideo(video) {
        this.mediaSource = new MediaSource();
        this.mediaSource.addEventListener('sourceopen', this.initSourceBuffer.bind(this), false);

        video.src = window.URL.createObjectURL(this.mediaSource);
    }

    // 初期化情報が入ったセグメント (初期化セグメント) とメディア本体のセグメントを追加できるようにする
    initSourceBuffer() {
        this.sourceBuffer = this.mediaSource.addSourceBuffer(this.type);
        this.sourceBuffer.addEventListener('updateend', this.appendMediaSegment.bind(this), false);
        this.appendInitSegment();
    }

    // セグメントファイルのロードが完了したタイミングで, SourceBuffer に追加する
    appendSegment(event) {
        this.sourceBuffer.appendBuffer(event.currentTarget.response);

        if (this.mediaAppended) {
            this.sourceBuffer.removeEventListener('updateend', this.appendMediaSegment.bind(this), false);
        }
    }

    // 初期化セグメントが SourceBuffer に追加されて更新されたら, メディア本体のセグメントファイル (メディアセグメント) を取得して, SourceBuffer に追加する. この処理を, メディアセグメントの数だけ繰り返す.
    appendMediaSegment() {
        const xhr = new XMLHttpRequest();

        const segmentURL = this.mpd.querySelectorAll('SegmentURL')[this.segmentIndex++];

        if (!segmentURL) {
            this.mediaAppended = true;
            return;
        }

        xhr.open('GET', segmentURL.getAttribute('media'), true);
        xhr.responseType = 'arraybuffer';
        xhr.onload = this.appendSegment.bind(this);
        xhr.send(null);
    }

    // MDP から セグメントファイルの URL を取得する
    appendInitSegment() {
        const xhr = new XMLHttpRequest();

        xhr.open('GET', this.mpd.querySelector('Initialization').getAttribute('sourceURL'), true);
        xhr.responseType = 'arraybuffer';
        xhr.onload = this.appendSegment.bind(this);
        xhr.send(null);
    }
}

new MSE(document.querySelector('video'), 'output.mpd');

これで, MPEG-DASH コンテンツを再生するための最小限の実装ができました.

Media Source Extensions API リポジトリ (Safari のみ)

参考 : フロントエンドエンジニアのための動画ストリーミング技術基礎

React Context API

1. Overview

React v16.3 から Context API というのが導入されました. この API を利用することによって Redux と同じようなデータフローを簡単に実装可能になるようです.

2. Implement

重要になるコンポーネントが, React#createContext メソッドによって生成される, Provider と Consumer です. その名前のとおり, Provider の value props に渡した値が, Consumer に渡ってきます.

2 – 1. Provider

Redux での, Provider, createStore に相当する.

2 – 2. Consumer

react-redux の connect に相当する.

'use strict';

import React from 'react';

const { Provider, Consumer } = React.createContext();

const Counter = () => (
    <Consumer>
        {({ state, actions }) => (
            <React.Fragment>
                <span>{state.count}</span>
                <button type="button" onClick={actions.increment}>+1</button>
                <button type="button" onClick={actions.decrement}>-1</button>
            </React.Fragment>
        )}
    </Consumer>
);

export default class App extends React.Component {
    state = {
        count : 0
    };

    constructor(props) {
        super(props);

        this.onClickIncrement = this.onClickIncrement.bind(this);
        this.onClickDecrement = this.onClickDecrement.bind(this);
    }

    onClickIncrement() {
        this.setState(prevState => {
            return { count : (prevState.count + 1) };
        });
    }

    onClickDecrement() {
        this.setState(prevState => {
            return { count : (prevState.count - 1) };
        });
    }

    render() {
        return (
            <Provider
                value={{
                    state   : this.state,
                    actions : {
                        increment : this.onClickIncrement,
                        decrement : this.onClickDecrement
                    }
                }}
            >
                <Counter />
            </Provider>
        );
    }
}

3. Conclusion

小規模なアプリであれば, わざわざ Redux (や MobX) を導入しなくても, Context API でいけるのでは ? と思いました.

Context API のサンプルリポジトリ