おふくろさまより愛をこめて

mmmpa ふとしです。誠実なプログラミングを心がけたい。

Recent entries

簡単な Webpack plugin を作成して Webpack と仲良くなる (ビルド時情報を console.log に表示する)

JavaScript Advent Calendar 2018 - Qiita 5 日目のエントリーです。未投稿だったので差し込みました。

このエントリーは Webpack 4.26.1 を基準に書かれました。このエントリー用に書いたコードは mmmpa/create-webpack-plugin-practice にあります。

開発者以外の方々に改修後の JavaScript の動作確認を行っていただくことは多々あります。そこでよく問題になるのが、ブラウザキャッシュにより、改修以前の状態を確認されてしまうという事態です。

見栄えに関わる単純な改修だとキャッシュが効いていると察しがつく場合もありますが、ロジック関係の修正だと非常に気づきにくくなり「なおってません」という連絡をいただくことになります。

そこで意図したビルドを確認していただいているかを明確にするために、 JavaScript にビルド日時や Git のコミットハッシュ値を埋めこみ、それを双方からの連絡時に併記するという方法があります。

自動化のために Webpack plugin を作成する

手作業で埋めこむのは現実的ではありませんから、ビルドを行う段で自動的に JavaScript に埋めこまれるようにします。ソースをみていただくようなことはできないので、起動時にコンソールに表示されるようにします。

今回はこの単純な plugin を作成する手順を書いていきます。

Webpack config に plugin を設定する

まだ plugin を用意していませんが、設定は可能です。一般的に plugin はクラスとして実装され、インスタンス化された形で Webpack 設定ファイルの plugins に挿入されます。plugin の名前を BuildingInformationPlugin とし、そのように設定を変更します。

const path = require('path')
const BuildingInformationPlugin = require('./building-information-plugin')

module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: 'index.js',
  },
  module: {},
  plugins: [new BuildingInformationPlugin()],
}

下記のような index.js を読みこみ、index.js を出力します。今回 loader は主題ではないので一つも設定していません。

console.log('Start my main process.')

Start my main process. が出力される前にビルド情報が表示されるように plugin を書いていきます。

plugin のコードを書く

plugin には Webpack から引数として compilation オブジェクトを受けとる apply メソッドが必要です。

class BuildingInformationPlugin {
  apply(compiler) {
  }
}

module.exports = BuildingInformationPlugin

apply は起動時に一度だけ Webpack から呼びだされます。その時に compilation オブジェクトに対して、この plugin がどのタイミングでどのような作用をするか設定します。

表示用のコードを埋めこむタイミングを決める

Webpack plugin はビルドにおける各段階にフックして様々な介入を行えます。

ビルド前のファイルは loader で処理されることを前提としているため、下手に手を加えると処理の内容を変えてしまうかもしれないため、タイミングとしては不適切です (lint 系の回避など無駄な手間がある)。そこで今回は assets として JavaScript ファイルが書き出された段階で、そのファイルの先頭に console.log を挿入したいと思います。

Compiler hooks

各段階のフック一覧 を見ると afterEmit というフックがありますので、これを使います。

After emitting assets to output dir

フックをかける

compiler.hooks[hookName].tap(name, callback) とすると任意の段階にフックすることができます。callback の引数は各メソッドの Parameters に記してあるもので、afterEmit の場合は compilation が渡されます。compilation は compile におけるさまざまな情報が入ったオブジェクトです。

  apply(compiler) {
+   compiler.hooks.afterEmit.tap('BuildInformationPlugin', (compilation) => {
+   })
  }

加工するファイルのありかを知る

前述したとおり compilation は compile におけるさまざまな情報が入っています。なので、Webpack の output 設定を参照すれば最終的に出力されるファイルを知ることができます。

  apply(compiler) {
    compiler.hooks.afterEmit.tap('BuildInformationPlugin', (compilation) => {
+     const { options: { output: { path: dir, filename } } } = compilation
+     const at = path.resolve(dir, filename)
    })
  }

情報を表示する console.log を埋めこむ

ここまでくればあとはファイルを読みこみ、console.log を追加して上書きするだけです。

  apply(compiler) {
+   const hash = childProcess.execSync('git rev-parse HEAD').toString().replace(/[\n\r]/, '')
+
    compiler.hooks.afterEmit.tap('BuildInformationPlugin', (compilation) => {
      const { options: { output: { path: dir, filename } } } = compilation
      const at = path.resolve(dir, filename)
+
+     const log = [
+       '##################################################',
+       `commit:   ${hash}`,
+       `built at: ${new Date().toLocaleString()}`,
+       '##################################################',
+     ].join('\n')
+      
+     fs.writeFileSync(at, [
+       `console.log(${JSON.stringify(log)});`,
+       fs.readFileSync(at),
+     ].join('\n\n'))
    })
  }

ビルドする

console.log("##################################################\ncommit:   93cd2cfa63730e7641988bf169aada2bac8b0382\nbuilt at: 2018-12-3 11:26:33\n##################################################");

!function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=0)}([function(e,t){console.log("Start my main process.")}]);

これを HTML に読みこむと、無事、ビルド情報が起動前に表示されました。

##################################################
commit:   93cd2cfa63730e7641988bf169aada2bac8b0382
built at: 2018-12-3 11:26:33
##################################################
Start my main process.

まとめ

このように Webpack plugin は非常に簡単に書くことができます。

パッケージとして配布するような汎用的な plugin ではこのように簡単には行きませんが、開発に合わせた使い捨ての plugin はカジュアルに書いていけるというのがわかっていただけると思います。今回の場合では、本体側のコードを変更することなく開発に有用な処理を埋めこむことができました。

難しい、複雑であると思われがちな Webpack ですが (実際設定で難航することはありますが)、こうやって付き合っていくうちにもっと仲良くなれたらいいなと思っています。

簡単な Webpack loader をいくつか書いて Webpack と仲良くなる

JavaScript2 Advent Calendar 2018 - Qiita 8 日目のエントリーです。

このエントリーは Webpack 4.26.1 を基準に書かれました。このエントリー用に書いたコードは mmmpa/create-webpack-loader-practice にあります。

Webpack の設定の複雑さには毎回目が回ります。ある目的があってその目的に合う設定を見つけられればコピペで済みますが、それに loader を加えたくなった途端に複雑さが襲ってくるような印象があります。

基本的に Webpack loader はあるファイルを加工して別の状態にするのが主な作用なので、ひとつひとつの働きは把握しやすいはずです。そこで簡単な loader をつくりつつ loader 何ができるのかを見ていきたいと思います。

基本的な入力/出力

基本的に Webpack は JavaScript を出力することを目的としています。loader が処理するファイルは特に設定がないかぎり utf-8 文字列で入力され、最終的には JavaScript に import できる形で出力されることが望まれています。

以下は対象のテキストファイルの文字列を反転して出力する loader です。

module.exports = function (content, map, meta) {
  let result = ''
  for (let i = content.length; i; result += content[--i]) {}
  return `module.exports = ${JSON.stringify(result)}`
}

module.exports = ... という JavaScript で処理できる形の文字列として出力されています。Webpack の処理により、以下のように JavaScript 内ではモジュールとして使用することができます。

# index.txt
abcde
import indexText from './index.txt'
console.log(indexText) //=> edcba

Webpack が loader に提供するメソッドは this 経由で使用できる

loader から Webpack が提供する機能を利用することができます。例えば content 以外の情報を返す loader の場合は this.callback (Loader API::callback) というメソッドを使用します。

this は Webpack の context でなくてはならないため、loader の定義でアロー関数を使うことはできないので注意が必要です。

module.exports = c => this.callback(null, c, {}, {}) //-> TypeError: this.callback is not a function

loader に与えられた options 見る

options として最大文字数 max と省略記号 ellipsis を受けとり、長い文字列を省略する loader を書いてみます。

loader には 2 通りの options の渡し方があります。

一つは config ファイルでオブジェクトとして渡す形式です。

use: {
  loader: './truncate-loader.js',
  options: { max: 10, ellipsis: '...' },
},

もう一つはクエリ文字列として渡す方式です。

use: './truncate-loader.js?max=10&ellipsis=...',

this.query で参照した場合、それぞれの生の値が loader に渡されます。前者は { max: 10 } として得られ、後者は ?max=10 として得られます。option の与えられ方によって値が変わるので、これを直接は使えません。

loaderUtils.getOptions を使う

これらを JavaScript から利用しやすくするように正規化する loaderUtils.getOptions (loaderUtils.getOptions) というメソッドが用意されています。

loaderUtils.getOptions を利用することで、常に同じアクセス方法で options を参照できるようになります。ただし、クエリ文字列として渡された値は型情報を持たないので 常に文字列 となることに注意しなければなりません (前記の例だと { max: '10' } として得られます)。

(厳密等価演算子 === などがないかぎり特に問題にならないところが JavaScript ですね)

const loaderUtils = require('loader-utils')

module.exports = function (content, map, meta) {
  const {
    max: raw = 20,
    ellipsis = ' (snip)',
  } = loaderUtils.getOptions(this)

  // クエリ文字列から渡される値は常に文字列になるので注意する
  // この例だとなくても動く
  const max = +raw

  const s = content.length <= max
    ? content
    : (content).slice(0, max - ellipsis.length) + ellipsis

  return `module.exports = ${JSON.stringify(s)}`
}

非同期な処理を行う

例えば Nodejs ではファイル IO は非同期に行うことが推奨されています。普通の関数宣言で loader を定義すると、読みこみ完了を待つことなく処理が進んでしまい意図した処理をすることができません。

以下のような json を読みこんで禁止語句を置き換える loader を書いてみます。

{
  "ass": "3 letters",
  "asshole": "7 letters"
}

this.async() を使う

this.async() (Loader API::async) で処理終了通知用の関数を得られます。非同期処理済ませ処理が終わった段階でその関数を呼ぶことで loader の処理の完了とすることができます。

得られる関数の引数は this.callback (Loader API::callback) と同一です。

const util = require('util')
const fs = require('fs')
const readFile = util.promisify(fs.readFile)
const loaderUtils = require('loader-utils')

function censor(content, list) {
  return Object.keys(list).reduce(
    (a, k) => a.replace(new RegExp(`\\b${k}\\b`, 'gm'), list[k]),
    content,
  )
}

module.exports = function (content, map, meta) {
  const callback = this.async()
  const { deniedList } = loaderUtils.getOptions(this)

  readFile(deniedList).then(s => {
    const result = censor(content, JSON.parse(s))

    callback(null, `module.exports = ${JSON.stringify(result)}`)
  })
}

async/await を使う

Webpack は loader の結果として Promise を受けつけます。よって await を使用できます。

//
// snip
//
module.exports = async function (content, map, meta) {
  const { deniedList } = loaderUtils.getOptions(this)

  const list = await readFile(deniedList)
  const result = censor(content, JSON.parse(list))

  return `module.exports = ${JSON.stringify(result)}`
}

ファイルを書き出す

loader は独自にファイルを出力することができます。アプリケーション実行時には行いたくないがリソースファイルには含めたくない、といったサブデータを先行して書き出しておきたい場合などに便利です。

this.emitFile を使う

this.emitFile (Loader API::emitFile) を使うと Webpack の設定に従った output 用のディレクトリにファイルが書き出されます。Writing a Loader::Guidelines にあるように、絶対パスの使用は避けましょう。

以下の loader は markdown の見出しを元に目次を作成します。markdown には目次用の <a /> を埋めこみ次の loader に渡します。一方で目次自体は別ファイルとして書き出しています。

const path = require('path')
const marked = require("marked");

module.exports = function (content, map, meta) {
  const reg = /^(#+)( *)(.+)/mg

  let replaced = content
  let match
  let indexing = ''
  while (match = reg.exec(content)) {
    const [r, n, , s] = match
    const { index } = match
    indexing += `${toSpace(n)}- [${s}](#anchor-${index})\n`
    replaced = replaced.replace(r, `${n} <a id="anchor-${index}"></a>${s}`)
  }

  const base = path.basename(this.resourcePath, '.md')
  this.emitFile(`${base}.with-index.html`, marked(indexing))

  return replaced
}

function toSpace(h) {
  let s = ''
  for (let n = h.length - 1; n--;) {
    s += '  '
  }
  return s
}

pitch を理解する

use: [
  './a-loader.js',
  './b-loader.js',
  './c-loader.js',
],

通常 loader は末尾から作用します。上記の例だと c -> b -> a の順に処理を進めます。しかし実際の処理を始める前に、まず先頭から pitch というメソッドを起動するサイクルがあります。

┌ pitch ┐                    ┌ default ┐
a -> b -> c -> load resource -> c -> b -> a

pitch に定義したメソッドでは loader で使うためのデータをセットできます。また undefined 以外の値を返すことで自分より後ろの loader を無視した結果を返せます。

例えば bpitch で値を返した場合は以下のようなフローになります。

┌ pitch
├─┐    ┌ default
a -> b -> a

本来発生するリソースファイルの読みこみも無視されることに注意して実装する必要があります。

censor-loader を無視する block-loader を書く

pitch の仕組みを使って、任意のファイルの場合は censor-loader の処理を無視する block-loader を書きます。

webpack config

指定のファイルでは検閲置換を行わない、というストーリーを想定します。

use: [
  './reverse-loader.js',
  {
    loader: './block-loader.js',
    options: { blockingList: ['pitch-block.txt'] },
  },
  {
    loader: './censor-loader.js',
    options: { deniedList: './deniedWords.json' },
  },
],

block-loader.js

指定ファイル以外では loader としての作用を持たなくてはならないので、受けつけた content をそのまま返すデフォルトメソッドを定義します。

指定ファイルが来た場合はリソースの読みこみが行われないことを踏まえ、独自にリソースファイルの読みこみを行いました。これで pitch-block.txt には検閲が行われなくなりました。

const path = require('path')
const util = require('util')
const fs = require('fs')
const readFile = util.promisify(fs.readFile)
const loaderUtils = require('loader-utils')

module.exports = function (content, map, meta) {
  return content
}

module.exports.pitch = function (remainingRequest, precedingRequest, data) {
  const { blockingList } = loaderUtils.getOptions(this)

  return blockingList.includes(path.basename(this.resourcePath))
    ? readFile(this.resourcePath)
    : undefined
}

ところで pitch で async/await を使うとあらゆる場合で Promise返した という扱いになり常にブロックされます。この場合だと、ブロックしていないつもりでも reverse-loader には censor-loader の処理結果ではなく undefined がわたってしまうので注意しましょう。

まとめ

loader に必要な機能を使いつつ、いくつかの loader を書きました。

基本的な処理を追うことによって、例えば、なぜ markdown-loader に必ず html-loaderraw-loader が必要なのかがわかりました。また、基本的な機能を把握することにより、アプリケーションに必要な適切な加工やサブファイルの書き出しが簡単に行えることがわかりました。

loader の設定変更だけではどうにもならないようなアプリケーション特有の特殊な処理を、独自の loader で手早く済ませることも可能になりました。検索とコピペを繰り返すのみで思うようにならず複雑さばかりを感じていたときよりも、より身近な存在になったと思います。

みなさまも一度簡単な loader を書いて Webpack と仲良くなってみてはいかがでしょうか。

loader をつくって学ぶ Webpack

このエントリーは Webpack 4.26.1 を基準に書かれました。

Webpack とは

Webpack は loader と呼ばれるモジュールを使って、任意のファイルやデータを JavaScript 内で使えるようにすることを主たる目的としたプログラムです。最新の文法を使った JavaScript や CSS などを一般に普及しているウェブブラウザで機能させるために、最近のフロントエンド開発でほぼ確実に用いられています。

Webpack の設定の難

Webpack で機能する loader が数多く存在します。そしてそれらを自由に組み合わせてファイルを加工できます。loader の多様さと組み合わせの自由さは大きな利点ですが、一方で、設定項目は多く、設定ファイルは入り組みがちです。設定項目を一から調べて書くということはあまりなく、コピペで組み立てていくというのが一般的な Webpack 設定ファイル作成方法ではないでしょうか。

そのような設定をしていると、いざエラーが起こった場合、このエラーの原因は何かを速やかに理解することが難しくなります。エラーメッセージで検索し、似たような問題に対する解決方を見つけられなければお手上げとなってしまいます。あるいは的はずれな検索に多くの時間を割いてしまうでしょう。

本エントリーの目的

そこで今回は自分の手で loader を造ることによって、loader が作用する仕組みを追ってみました。また loader では完結できない処理があったため、plugin も造ることになりました。最終的には以下のようなことを学べたと思います。

  • loader での入力出力
  • 結果ファイルを Nodejs で扱うために書き出しの方式を変更する方法
  • 最終的には plugin でどうとでもなるということ

markdown 内リンクを挿入する loader を造る

今回は題材として md-indexing-loader を造っていきたいと思います。

この loader は markdown を受けとり、その見出しへのリンクをリスト上に構成して markdown の任意の位置に挿入する、という作用を持ちます。

Webpack の最小限の設定とサンプルファイル

単純に index.md をエントリーポイントとし index.md を出力する設定をします。loader はパッケージ化せず、./md-indexing-loader/index.js に配置します。

const path = require('path')

module.exports = {
  mode: 'production',
  entry: './src/index.md',
  output: {
    path: path.resolve(__dirname, '../dist'),
    filename: 'index.md',
  },
  module: {
    rules: [
      {
        test: /\.md$/,
        use: ['./md-indexing-loader/index.js'],
      },
    ],
  },
}
# Section 1

text

## Section 1-2

text

### Section 1-2-1

text

## Section 1-3

text

# Section 2

text

# Section 3

text

## Section 3-1

まず最小限の loader を造り、どのような処理が必要か知る

処理

何が入力されてどのように出力するのかを見るために、まずは最小限の loader を書きます。loader は入力として utf-8 文字列を受けとり、なんらかの値を返したり返さなかったりします。

module.exports = function (content, map, meta) {
  const json = JSON.stringify(content)
  return `module.exports = ${json}`
}

出力

一般的な loader では最終的に JSON.parse された上で JavaScript の式や文として出力されます。Webpack が最終的に出力ものは以下のようになります。

(Webpack のヘルパーメソッド部は省略しています)

/***/ (function(module, exports) {

module.exports = "# Section 1\n\ntext\n\n## Section 1-2\n\ntext\n\n### Section 1-2-1\n\ntext\n\n## Section 1-3\n\ntext\n\n# Section 2\n\ntext\n\n# Section 3\n\ntext\n\n## Section 3-1\n"

/***/ })

今回は index.md に markdown をそのまま出力することを目的としているので、本文の解析などの処理の他にも、結果の返し方も変更する必要があります。

なお、この最小限の loader の結果をそのまま JavaScript で扱いたい場合は、module.exports によりデフォルト値として読みこまれるので import を用いて以下のように使えます (これは raw-loader が行っていることとほぼ同じです)。

import md from './index.md' // module.exports = "# Section 1\n\n..."
console.log(md)             // => "# Section 1\n\n..."

必要な処理はなにか

ごく基本的な動きを知ることで、この loader に何が必要かを把握できました。

  • 入力された文字列を解析してインデックスを作成する
  • 作成したインデックスと本体を合わせた markdown を作成する
  • 出力した文字列を JavaScript ではなく markdown で出力する

loader に必要な処理を実装する

入力された markdown を解析してインデックスを挿入する

入力された markdown から見出しに当たる部分を抽出し、インデックスとして構成しながらアンカリングのために <a name="" /> を挿入していきます。

文字列 to 文字列の加工なので、特に難しい部分はありません (本題ではないので厳密な抽出はしていません)。

module.exports = function (content, map, meta) {
  const reg = /^(#+)( *)(.+)/mg

  let replaced = content
  let match
  let indexing = ''
  while (match = reg.exec(content)) {
    const [r, n, , s] = match
    const { index } = match
    indexing += `${toSpace(n)}- [${s}](#anchor-${index})\n`
    replaced = replaced.replace(r, `${n} <a name="anchor-${index}"></a>${s}`)
  }

  const combined = `${indexing}\n\n${replaced}`

  const json = JSON.stringify(combined)
  return `module.exports = ${json}`
}

function toSpace(h) {
  let s = ''
  for (let n = h.length - 1; n--;) {
    s += '  '
  }
  return s
}

出力した文字列を JavaScript ではなく markdown ファイルで出力する

loader から直接ファイルを出力する

ファイルを出力する方法として、Webpack から提供される emitFile という関数があります。これを用いると config ファイルに設定された output に従った場所にファイルが出力されます。ただし、この時は最終的に出力される index.md は使用できません。そこで、ひとまず index.with-index.md として結果を出力します。

ハードコーディングされたファイル名で出力するのはよくないので、実際に入力されたデータの持ち主のファイル名を変形します。ファイル名といったような loader 内で活用できる各種情報は Loader API に一覧されています。loader が何を参照しているのかがわかるので一読しておくと良いでしょう。

 module.exports = function (content, map, meta) {
 // snip

   const combined = `${indexing}\n\n${replaced}`
+  const fileName = path.basename(this.resourcePath, '.md')
+  this.emitFile(`${fileName}.with-index.md`, combined)

 // snip
 }

無事、以下のようなファイルが出力されました。

- [Section 1](#anchor-0)
  - [Section 1-2](#anchor-19)
    - [Section 1-2-1](#anchor-41)
  - [Section 1-3](#anchor-66)
- [Section 2](#anchor-88)
- [Section 3](#anchor-107)
  - [Section 3-1](#anchor-126)


# <a name="anchor-0"></a>Section 1
<!-- snip -->

出力される index.md 自体を markdown にする

さて、上の方法でファイルは出力されたものの、本来の結果ファイルである index.md は依然として JavaScript ファイルとして出力されています。markdown ファイルを入力し markdown ファイルを出力するための loader ですから、出力結果として markdown を得られる仕組みを提供する必要があります。

そこで本来のファイル名で出力するための plugin を書きます。pluign は loader にはできないあらゆることを解決するために作成されます。

They also serve the purpose of doing anything else that a loader cannot do.

JavaScript ファイルではなく markdown ファイルとして出力するための plugin を書く

loader と同じように、まず最小限の plugin を書き、必要な処理を処理を実装します。

webpack に設定を追加

 const path = require('path')
+const MdLoaderPlugin = require('../md-indexing-loader/plugin')

 module.exports = {
   mode: 'production',
   entry: './src/index.md',
   output: {
     path: path.resolve(__dirname, '../dist'),
     filename: 'index.md',
   },
   module: {
     rules: [
       {
         test: /\.md$/,
         use: [
           './md-indexing-loader/index.js',
         ],
       },
     ],
   },
+  plugins: [new MdLoaderPlugin()]
 }

最小限の plugin

Plugin は起動時にインスタンス化され、Webpack の complier オブジェクトを引数として apply を呼ばれます。complier を使って Webpack のライフサイクルに従ったイベントフックに関数を設定することで、任意のタイミングで任意の処理を行えます。

今回は出力結果のファイルに対する処理となるので、compilation のさらに細分化されたイベントの needAdditionalPass にフックしました。この段階ではすべての処理が終えられているため、compilation に十分な情報がセットされています。

class MdLoaderPlugin {
  apply(compiler) {
    compiler.hooks.compilation.tap('MdLoaderPlugin', compilation => {
      compilation.hooks.needAdditionalPass.tap('MdLoaderPluginA', () => {
        console.log(compilation)
      })
    })
  }
}

module.exports = MdLoaderPlugin

index.md を Nodejs で読めるようにする

Webpack でのコンパイル中では import で読み込んで値を扱えますが、コンパイル後はブラウザで起動されることが前提となっているのでクロージャーですべてが隔離されています。そのためデフォルトの設定では index.md の値に直接アクセスできません。

そこで Nodejs のライブラリのように外部から読みこまれる形でコンパイルするように設定を変更しなければなりません。

Webpack の設定をライブラリモードに変更

Nodejs の読みこみを前提としているため、commonjs モードで出力します。

 module.exports = {
 // snip
   output: {
+    library: 'index',
+    libraryTarget: 'commonjs',
     path: path.resolve(__dirname, '../dist'),
     filename: 'index.md',
   },
 // snip
 }

これにより index.md の出力結果に exports["index"] = が加えられ、require で読みこんだ際には index を識別子としてアクセスできるようになります。

plugin が index.md を markdown ファイルに書き換える処理を書く

まず初期情報として出力ファイル名とライブラリ名を引数として与えるように、Webpack の設定を変更します。汎用的な plugin ではさまざまな動的処理によりファイルを判別しています。

 module.exports = {
 // snip
-  plugins: [new MdLoaderPlugin()],
+  plugins: [new MdLoaderPlugin('index.md', 'index')],
 }

あとは compilation.assets から結果ファイルパスを調べ、それを値として読みこみます。読みこんだ値は素の markdown ですから、それをそのままファイルに上書きすれば markdown ファイルが完成します。

const fs = require('fs')

class MdLoaderPlugin {
  constructor(target, name) {
    this.target = target
    this.name = name
  }

  apply(compiler) {
    compiler.hooks.compilation.tap('MdLoaderPlugin', compilation => {
      compilation.hooks.needAdditionalPass.tap('MdLoaderPluginA', () => {
        const { existsAt } = compilation.assets[this.target]
        const md = require(existsAt)[this.name]

        fs.writeFileSync(existsAt, md)
      })
    })
  }
}

module.exports = MdLoaderPlugin

loader 内のファイル出力は不要になったので削除します。

 module.exports = function (content, map, meta) {
 // snip

   const combined = `${indexing}\n\n${replaced}`
-  const fileName = path.basename(this.resourcePath, '.md')
-  this.emitFile(`${fileName}.with-index.md`, combined)

 // snip
 }

出力結果

- [Section 1](#anchor-0)
  - [Section 1-2](#anchor-19)
    - [Section 1-2-1](#anchor-41)
  - [Section 1-3](#anchor-66)
- [Section 2](#anchor-88)
- [Section 3](#anchor-107)
  - [Section 3-1](#anchor-126)


# <a name="anchor-0"></a>Section 1

text

## <a name="anchor-19"></a>Section 1-2

text

### <a name="anchor-41"></a>Section 1-2-1

text

## <a name="anchor-66"></a>Section 1-3

text

# <a name="anchor-88"></a>Section 2

text

# <a name="anchor-107"></a>Section 3

text

## <a name="anchor-126"></a>Section 3-1

まとめ

今回はエラー処理などを完全に無視して書きましたが、本来は他の loader との連携に当たっては入力の型チェックや出力内容の統一など、様々な処理が増えることになります。様々な loader を組み合わせていくなかで発生する不可解なエラー、そしてそれの追いづらさの理由が少しわかった気がします。

自分で複雑な loader を書くことはあまりないかもしれません。しかし、複雑な設定が必要な loader で頭を悩ませるよりもプロジェクトドメインにそったシンプルな使い捨ての loader や plugin を書くのが最も適した解決になる局面もありそうだと思いました。

Webpack の設定はつらいことで有名です。できればあまり近寄りたくはなかったのですが、こうして簡単な loader と plugin を作成することにより、いざとなったら介入できる (かもしれない) という心の余裕ができました。設定をコピペするときも、その設定内容の意図が見えるようになるような気がします。

行ったことは簡単で単純でしたが、得たものは多かったと感じました。Webpack に苦手意識があるかたは、是非一度 loader を造ってみることをおすすめします。

並行コンピューティング技法という本をやり終えました。

夏季休暇中 (8/11 - 8/19) の課題としてはじめた 並行コンピューティング技法 ―実践マルチコア/マルチスレッドプログラミング という本を、先日ようやくやり終えました。

久しぶりに技術書的な本を一冊やりおえたので、本の紹介や思い出日記を書きます。

今回の学習について

なぜやろうと思ったか。なぜわたしに必要だと思ったか。

仕事では Ruby、JavaScript を主に使っていますが、個人的には Go 言語を使いこなせるようになりたいと思っています。

Go 言語には並列処理を簡単にハンドリングできる goroutine という仕組みがあります。並列処理のための排他制御や終了を取りあつかうための仕組みも準備されており、使用開始は容易です。

しかし、何を並列化できるか、並列化をどのように行うかという素養が全くありませんでした。外部 API に対するリクエストなど並列化のメリットがわかりやすいものはともかく、一般的な処理になるとお手上げです。

そこで言語の機能やライブラリの使用法ではなく、並列化自体を解説しているこの本をやろうと思い至りました。

学習方法について

この本をやっている最中に Learn Better――頭の使い方が変わり、学びが深まる6つのステップ という本を読みました。

  • 学習する対象の価値を見出す
  • 具体的な目標を設定する
  • 学習対象は少し難しいものを選ぶ
  • 学習対象へ能動的に行動する (読む・聞く・写すなど受動行動だけではなく、自分や他人に対して説明・要約など行う)
  • 時間をおいた分散学習 (一度やって終わりではなく、時間をおいて再度同じことを学ぶ)

など、よりよく学習するための方法が紹介されています。

今回たまたまいくつかの点を満たして学習を開始しました。能動的にという点は当初より 読書メモ を残すということで無意識にやっていましたが、メモをより明確な意図を持って行うようになりました。

時間をおくという点については、あるアルゴリズムを並列化した直後ではなく、次のアルゴリズムの途中かそれが終わってからメモをおこすことで実践してみました。本ではなく実装前にノートに書いたメモ書きや記憶やコードを頼りに書くことにより、再び考える機会が訪れて良かったように思います。

この日記の後段でも作成した図とともに学習した内容について触れています。これもまた分散学習の一助になるように思います。

説明の有効性

説明の有効性については、最近読んだ 知ってるつもり――無知の科学 でも言及されていました。学習という観点ではなく、人がどれだけ自分の無知に無自覚であるかを検査する方法 (物事を説明しようとすることにより自分がどれだけその物事について知らないかを自覚する) としての紹介でした。

自分に対して説明することにより本当に理解できたかどうか検査できるので、有用性は確かなものであるようです。(実際コードを書きおえて動くようになってから説明を書いてみて、理解がずれていることに気づいたアルゴリズムがありました)

本について

前半

並列化において念頭におかなければならないことを解説しています。

  • 逐次処理で行われていたものを並列化するので、
    • 並列化処理を実装すること自体がオーバーヘッドであること。
    • 並列化実行コストが必ずオーバーヘッドであること。
  • 適切な処理の粒度にしないと無用なオーバーヘッドを生むこと。
  • 並列化スレッド同士の処理順序は絶対に予測不可能なこと。
  • 処理の中にはある処理が出した結果が他の処理に影響を与える場合があり、それをなんらかの方法で並列化した処理同士で共有する必要があること。
  • 処理の中には同じデータ領域を読み書きする場合があり、排他制御が必要なこと。
  • 扱うデータの範囲によって並列化する場合は、そのデータの形によって最終的なデータ統合の処理の量が変わること。

など、実際に並列化を数多く行ってきた著者が、実際的な問題として語ってくれます。

後半

よく知られたアルゴリズムを並列化することにより、並列化処理で実際に直面する問題への対応を解説しています。

処理の順序が決まっていて並列化が無理そうなソートアルゴリズムを並列化します。また、逆に各処理の独立性が高く並列化するのが楽そうなアルゴリズムにおいては、再帰をループに書きかえたり、終了条件を明確化に設定しなければならないなど、処理だけではない実装のオーバーヘッドについても体験できます。

以下のようなよく知られたアルゴリズムを並列化しました。

  • ソートの章: バブルソート、シェルソート、クイックソート、奇偶転置ソート、基数交換ソート、直接基数ソート
  • サーチの章: 線形探索、二分探索 (を並列化する n 分探索)
  • グラフの章: 深さ優先探索、Floyd アルゴリズム (全頂点対最短経路) 、Prim アルゴリズム (最小全域木)

基本的なアルゴリズムは以前別の本でも学習しましたが、並列化するにあたって長時間接することにより、より具体的に理解できたような気がします。

学習中の思い出

学習中に図を多く描いたので、せっかくなので思い出話とともに羅列します。

図はほとんど whimsical で作成しました。機能が少なく、選択できる色も少ないので、逆に安定した図が描けるので便利です。

ただ、グラフは描きづらかったので、そこは Cacoo を使用しました。

データの形状について

分割したデータの形状によっても処理量が変わるという目からウロコな解説でした。ゴーストセルはスレッド間で競合するデータをどうやって持つかの一例です。

imageimage

データ分解について

これは学習中に書いた別の日記で言及した処理の図です。要は for i := threadNumber; i < l; i += threadTotalCount で回すとスレッド数分に分割された処理量になるという話でした。

imageimage

プリフィックススキャン

プリフィックススキャンは配列の各要素を足すなどの処理を加えていくので、一見並列化が難しそうな処理です。しかし、各スレッドの結果をプリフィックススキャンした結果を各スレッドに適用するとちゃんと結果が得られるという話でした。

imageimage

PRAM

無限の CPU を持つ抽象機械による並列和はこうなるけど実際にはそんなものないから使えないよ。

という話が最初の方であるのですが、各スレッドが出した結果をリダクションするときにはスレッド数分 (大体の場合において CPU 数分) のデータしかないから使えるよねと突如復活する話でした。

image

並列ウェイブフロント

バブルソートは順序依存が激しい処理ですが、並列化はできるという話でした。

imageimage

奇偶転置ソート

他のスレッドの割当範囲も更新するんだけど、アルゴリズムの特性上問題ないんだよという話でした。当初、データ分割量を必ず偶数にしないといけないと気づかなくて、データ競合して大変でした。

imageimage

シェルソート

遠くからの値をとっておおまかにソートしてから挿入ソートするとすごく速くなるというシェルソートの話です。実際の h はいい感じの速度がでるステップがあります。 (…, 121, 40, 13, 4, 1)

imageimage

クイックソート

おなじみクイックソートです。pivot 指定されたものと length == 1 になった部分は整列済みとして扱いカウント、カウントが配列の長さと同じになるのが終了条件という話でした。

image

基数交換ソート

ビットで行うクイックソート (pivot ナシ) ですね。終了条件は length == 1 になったもの、もしくはビットが末尾に至ったものという点で、並列クイックソートとは少しちがうのがミソという話です。

直接基数ソートは図を描くのが大変なので諦めて table にしました。こちらは末尾から任意のビット数でソートしていきます。

プリフィックススキャンによる array パッキングがミソです。

image

base 1 pass 2 pass 3 pass
485 340 526 041
041 041 739 188
340 485 340 340
526 526 041 387
188 387 485 485
739 188 387 488
489 988 188 489
387 488 988 526
988 739 488 739
488 489 489 988

線形探索

線形探索です。サーチは全データをさわりますが、探索は見つかった時点で処理が終わりなので、並列化においては他のスレッドにその終わりをどう伝えるかがミソになってくるという話です。

imageimage

n 分探索

範囲ではなくあくまで分割位置で探索する n 分探索で、毎ループごとに待ち合わせる必要があります。

imageimage

グラフ

おなじみグラフです。エッジ情報を行列で扱うという方法を知りませんでした。便利ですね。

imageimageimage

深さ優先探索スタック

深さ優先探索を再帰ではなくループにした場合、次に訪れるノードの管理はスタックで行うという図です。キューではないのがミソでした。

image

全域木、最小全域木

実質最終章のシメの問題でした。

imageimage

まとめ

一冊全てやり終えると気持ちが良い。

先日の並行和で順序依存があった問題を解決

酢と塩 — ただの足し算だし順序依存ないだろと思っていたらそうでもなかった話 では float64 の並行和によって丸め誤差が発生して逐次処理と結果が異なってしまうという話をしました。

「当初逐次処理と結果が異なる」ことが悪だと思っていました。しかし冷静に考えると円周率の近似値を求めるという要求は満たしています。

同じ条件での返り値を保証する

コンピューターの性質を考えると丸め誤差は発生して当然です。ただし並行処理の順序により毎回返り値が変わってしまうのは大問題です。

並行処理内の結果は常に同一なのですから、後は部分和を足し合わせる順序を保証すれば、答えは常に同一になります・

<br/>type SumResult struct {
    Offset int
    Sum    float64
}
func compute(rectCount, workers int) float64 {
    sum := 0.0
    width := 1.0 / float64(rectCount)
    ch := make(chan SumResult)
    for i := 0; i &lt; workers; i++ {
        go func(ch chan SumResult, offset int) {
            sum := 0.0
            for i := offset; i &lt; rectCount; i += workers {
                mid := (float64(i) + 0.5) * width
                height := 4.0 / (1.0 + mid*mid)
                sum += height
            }
            ch &lt;- SumResult{
                Offset: offset,
                Sum:    sum,
            }
        }(ch, i)
    }
    // 部分和を足し合わせる順序を保証する
    results := make([]float64, workers)
    for i := 0; i &lt; workers; i++ {
        result := &lt;-ch
        results[result.Offset] = result.Sum
    }
    for _, n := range results {
        sum += n
    }
    return sum * width
}

これで rectCountworkers が同一なら毎回同じ答えが返るようになりました。処理速度も毎回結果が変わってしまう最速のものとほぼ同じです。

無駄な処理を省くテクニック

本を読み進めると載っていました。

前回のコードでは各 worker に割りあてる restCount の範囲を (それなりの処理で) 以下のように分割していました。

restCount = 10 worker = 4 の場合です。

しかし今回のように worker 数で for をステップしていくとそのような分割は必要がないようです。便利ですね。