Node.js
Markdown
SinglePageApplication
webpack
More than 1 year has passed since last update.

はじめに

この記事では、Markdownを帳票出力するシングルページアプリケーションの開発手順について解説します。特に、実装のポイントを中心にまとめます。

ソースコード

ソースコードは下記のURLよりご覧になれます。

構成

この記事の構成を下記に示します。

  • ディレクトリ構成
  • Webpackのエントリーポイント
  • Vue.jsとコンポーネント
  • ローカルストレージ
  • Gulpを使用しないビルド
  • Google Analytics
  • Google AdSense

ディレクトリ構成

ディレクトリ構成を下記に示します。

  • md2html/
    • bin/
    • config/
    • defaults/
    • public/
    • css/
    • data/
    • fonts/
    • js/
      • vendor/
    • partials/
    • src/
    • components/
    • lib/
    • utils/

--

bin/

bin/に格納されたスクリプトはpackage.jsonscriptsに追加され、npm run xxxでコマンドを実行できるようにしています。

--

config/

Google AnalyticsのトラッキングIDなどの設定がJSONで記述されます。src/の中か外か迷ったのですが、bin/build.jsで使用するので外に出しました。

--

src/

src/にはソースコードが格納されます。ビルドするとdist/というディレクトリが生成されるので、対応づけるためにsrc/というディレクトリを使用しています。

--

src/components/

src/components/にはVue.jsのコンポーネント的なものを記述したソースコードを格納します。

--

src/lib/

src/lib/には、このアプリケーション固有と思われる処理を記述したソースコードを格納します。

--

src/utils/

src/utils/には、汎用的に使えそうな処理を記述したソースコードを格納します。

Webpackのエントリーポイント

モジュールバンドラにはWebpackを使用してします。エントリーポイントはsrc/entry.jsとしています。DefinePluginを使用してENVという名前の定数を定義しています。これは開発時はdevelopmentという文字列になり、ビルド時にはproductionという文字列になります。開発時と運用時で処理を切り替えたい場合などに使用します。

src/entry.jsには、手続き的なことも記述しても大丈夫ですが、自分の場合は宣言のみを記述し、手続き的なことはpublic/index.htmlに記述するようにしています。

webpack.config.js
'use strict';

var path = require('path')
var webpack = require('webpack')

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

  output: {
    path: path.join(__dirname, './dist/'),
    filename: 'js/bundle.js',
  },

  plugins: [
    new webpack.DefinePlugin({
      ENV: JSON.stringify('development'),
    }),
  ],
}
src/entry.js
'use strict';

window.Vue = require('vue')
window.lib = {
  initialize: require('./lib/initialize'),
}

window.components = {
  MainComponent: require('./components/main'),
}
public/index.htmlより抜粋
<script>
  (function () {
    var initialize = window.lib.initialize
    var MainComponent = window.components.MainComponent

    var mainComponent = new MainComponent()

    initialize()
      .then(function (options) {
        mainComponent.initialize(options)

        var vm = new Vue(mainComponent.getVueOptions())
        vm.$mount('main')
      })
      .catch(function (err) {
        console.error(err.stack)
      })

    window.mainComponent = mainComponent
  })();
</script>

Vue.jsとコンポーネント

HTMLとJavaScriptをつなぐのにVue.jsを使用しています。Vueのコンストラクタに渡すオブジェクトにそのまま記述しても良いのですが、専用のプロトタイプを作成して、getVueOptionsというメソッドで、Vueのコンストラクタに渡すオブジェクトを生成しています。このようにしておくと、気持ち的にVueに依存していない感じになってちょっと安心できます、実際にはバリバリ依存しているのですが...なんとなく役割的にWebComponentっぽい感じがするので、それにちなんでコンポーネントと呼ぶようにしています。コンポーネントはsrc/components/にフォルダを作って格納しています。

src/components/main/index.jsより抜粋
MainComponent.prototype.getVueOptions = function (options) {
  options = options || {}

  var self = this
  var data = options.data || { self: self }
  var template = options.template || MainComponent.defaults.template

  return {
    data: data,

    methods: {
      onClickLoadConfig: function (event) {
        self.onClickLoadConfig(event)
      },

      onClickLoadTemplate: function (event) {
        self.onClickLoadTemplate(event)
      },

      onClickDownloadConfig: function (event) {
        self.onClickDownloadConfig(event)
      },

      onClickDownloadTemplate: function (event) {
        self.onClickDownloadTemplate(event)
      },

      onClickConvert: function (event) {
        self.onClickConvert(event)
      },
    },

    template: template,

    ready: function () {
      self.onReady()
    },
  }
}

ローカルストレージ

同じPCで作業するときには設定ファイルのURLを記録しておいて欲しいと思ったので、データの格納先としてローカルストレージを選びました。今まであまり使うことがなかったのですが、ちょっとしたデータを格納するときには便利なのに加えて、使い方がめちゃくちゃシンプルで素敵です。なお、ローカルストレージの読み込みは初期化を担当するsrc/lib/initialize/index.jsのみで記述されています。

src/lib/initialize/index.jsより抜粋
function initializeConfig() {
  var configUrls = []

  if (typeof window.localStorage.getItem('config-url') === 'string') {
    configUrls.push(window.localStorage.getItem('config-url'))
  }

  configUrls.push(urlConfig.default)
  configUrls.push(urlConfig.fallback)

  var fetchConfigPromise

  if (configUrls.length === 3) {
    return fetchConfig(configUrls[0])
      .then(function (result) {
        if (result.isValid) {
          return result
        }

        window.localStorage.removeItem('config-url')

        return fetchConfig(configUrls[1])
          .then(function () {
            if (result.isValid) {
              return result
            } {
              return fetchConfig(configUrls[2])
            }
          })
      })
  } else if (configUrls.length === 2) {
    return fetchConfig(configUrls[0])
      .then(function (result) {
        if (result.isValid) {
          return result
        } else {
          return fetchConfig(configUrls[1])
        }
      })
  } else {
    throw new Error('invalid configUrls.length')
  }
}

Gulpを使用しないビルド

今までビルド手順を記述するのにGulpを使用してしましたが最近はノーマルなNode.jsのコードで記述するようにしています。非同期な処理が入ってきてもcoが便利すぎるので普通のNode.jsのコードでも楽々と書けるようになりました。

Gulpも便利ですが、Node.jsの方が記述の自由度が高く、一人で開発する分には楽しくて良いなーと思いました。

bin/build.jsより抜粋
function main() {
  co(function *() {
    yield [
      taskClean()
    ]

    yield [
      taskCopy(),
      taskWebpack()
    ]

    yield [
      taskIndex(),
      taskRemove()
    ]
  })
    .catch(function (err) {
      console.error(err.stack)
    })
}

Google Analytics

Google Analyticsなど解析用のコードは公開時にだけ有効になれば良いので、ビルドでindex.htmlに細工するようにしました。<-- insert:xxx -->のようになっている部分をpublic/partials/xxx.htmlで置き換えるような処理を記述しています。

public/index.htmlより抜粋
  <body>
    <main>
      <div id="spinner">
        <i class="fa fa-refresh fa-spin fa-5x"></i>
      </div>
    </main>
    <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
    <script>window.jQuery || document.write('<script src="js/vendor/jquery-1.11.2.min.js"><\/script>')</script>
    <script src="js/vendor/ejs.min.js"></script>
    <script src="js/bundle.js"></script>
    <script>
      (function () {
        var initialize = window.lib.initialize
        var MainComponent = window.components.MainComponent

        var mainComponent = new MainComponent()

        initialize()
          .then(function (options) {
            mainComponent.initialize(options)

            var vm = new Vue(mainComponent.getVueOptions())
            vm.$mount('main')
          })
          .catch(function (err) {
            console.error(err.stack)
          })

        window.mainComponent = mainComponent
      })();
    </script>
    <script>
      (function () {
        window.mainComponent.on('ready', function () {
          $('#adsense').append(/* insert:partials/adsense.html */)
        })
      })();
    </script>
    <!-- insert:partials/analytics.html -->
  </body>
bin/build.jsより抜粋
function *taskIndex() {
  var adsense = yield readFile(path.join(__dirname, '../public/partials/adsense.html')),
  adsense = adsense.replace('{{ client }}', adsenseConfig.client)
  adsense = adsense.replace('{{ slot }}', adsenseConfig.slot)
  adsense = adsense.replace('{{ format }}', adsenseConfig.format)
  adsense = JSON.stringify(adsense)
  adsense = adsense.replace(/<\/script>/g, '<\\/script>')

  var analytics = yield readFile(path.join(__dirname, '../public/partials/analytics.html')),
  analytics = analytics.replace('{{ trackingId }}', analytics.trackingId)

  var index = yield readFile(path.join(__dirname, '../public/index.html'))
  index = index.replace('/* insert:partials/adsense.html */', adsense)
  index = index.replace('<!-- insert:partials/analytics.html -->', analytics)

  yield writeFile(path.join(__dirname, '../dist/index.html'), index)
}

Google AdSense

Google AdSenseも同様にindex.htmlとbuild.jsの合わせ技で解決しています。ただ、こちらの場合はJavaScriptのコードに注入するので/* insert:xxx */のようにしています。また</script><\/script>にエスケープする点に関しても注意しました。

ちなみにGoogle AdSenceの審査の結果、十分なコンテンツがないため失格となりました(泣)あまりAdSenceのようなものを活用する機会がなかったので、今回はいい勉強になりました。

おわりに

今までは手順を紹介する記事ばかり書いていたので、今回のように理由を説明する記事を書いてみて、自分の考えを言語化することの難しさを感じました。また、こういう記事を書いて訓練しようと思いました。