はじめに
この記事では、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.json
のscripts
に追加され、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
に記述するようにしています。
'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'),
}),
],
}
'use strict';
window.Vue = require('vue')
window.lib = {
initialize: require('./lib/initialize'),
}
window.components = {
MainComponent: require('./components/main'),
}
<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/
にフォルダを作って格納しています。
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
のみで記述されています。
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の方が記述の自由度が高く、一人で開発する分には楽しくて良いなーと思いました。
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
で置き換えるような処理を記述しています。
<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>
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のようなものを活用する機会がなかったので、今回はいい勉強になりました。
おわりに
今までは手順を紹介する記事ばかり書いていたので、今回のように理由を説明する記事を書いてみて、自分の考えを言語化することの難しさを感じました。また、こういう記事を書いて訓練しようと思いました。