tag:crieit.net,2005:https://crieit.net/tags/Webpack/feed 「Webpack」の記事 - Crieit Crieitでタグ「Webpack」に投稿された最近の記事 2022-08-24T23:53:31+09:00 https://crieit.net/tags/Webpack/feed tag:crieit.net,2005:PublicArticle/18287 2022-08-24T23:45:46+09:00 2022-08-24T23:53:31+09:00 https://crieit.net/posts/clean-dist-folder-by-webpack 【JS/Webpack】ビルドしたフォルダの不要ファイルを削除する <p>こんにちは、しきゆらです。今回は、Webpackでビルドしたものが出力されるフォルダにできる不要ファイルを削除する方法を知ったのでメモしておきます。</p> <p>WebpackでJSのコードをビルドしていると、結構な頻度で不要なファイルができたりします。例えば、デバッグ用に生成したソースマップファイルがそのまま残っていたり、過去に設定を変えてあるところから出力しなくなったファイルが残っていたり等、ビルド先のdistフォルダの中はごちゃごちゃしがちです。これを何とかしたいなぁ、と思って調べたら簡単に解決できました。</p> <p>Webpackでこういうことできないかなぁと調べると大抵プラグインが出てきて追加しないといけないことが多く、今回もそのパターンでよくわからないものを追加しないといけないのかなぁ・・・と思っていたのですが、どうも標準機能として用意されているようでした。ということで結論。</p> <h2 id="結論: outputの中に[clean: true]を追加すればよい"><a href="#%E7%B5%90%E8%AB%96%3A+output%E3%81%AE%E4%B8%AD%E3%81%AB%5Bclean%3A+true%5D%E3%82%92%E8%BF%BD%E5%8A%A0%E3%81%99%E3%82%8C%E3%81%B0%E3%82%88%E3%81%84">結論: outputの中に[clean: true]を追加すればよい</a></h2> <p>ドキュメントを読むことは大事ですね。しっかり記載がありました。</p> <p><a target="_blank" rel="nofollow noopener" href="https://webpack.js.org/guides/output-management/#cleaning-up-the-dist-folder">https://webpack.js.org/guides/output-management/#cleaning-up-the-dist-folder</a></p> <p>こちらによると、ビルドしたものを置くdistフォルダの中身をビルドのたびにきれいにしたい場合はoutput.cleanオプションを使え、とあります。ただし、この方法が使えるのはWebpack5以降のみなので注意してください。</p> <pre><code class="javascript">// webpack.config.jsの一部分 ~~~~~~ output: { filename: "output.js", path: path.resolve(__dirname, "dist"), clean: true // この1行を追加すればよい } </code></pre> <p>はい、たったこれだけ。1行追加するだけで、ビルドのたびにdistフォルダの中身をきれいにしてくれるので、過去にビルドした残骸が残ることはなくなりました。</p> <p>また、こちらのページには細かい設定方法もありました。</p> <p><a target="_blank" rel="nofollow noopener" href="https://v4.webpack.js.org/guides/output-management/#cleaning-up-the-dist-folder">https://v4.webpack.js.org/guides/output-management/#cleaning-up-the-dist-folder</a></p> <p>そして、前述の通りこの方法が使えるのはWebpack5以降のみとのことでした。Webpack4などを使っている方は、プラグインでの対応となるようです。</p> <h2 id="Webpack4を使っている方: clean-webpack-pluginを使う"><a href="#Webpack4%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%81%84%E3%82%8B%E6%96%B9%3A+clean-webpack-plugin%E3%82%92%E4%BD%BF%E3%81%86">Webpack4を使っている方: clean-webpack-pluginを使う</a></h2> <p><a target="_blank" rel="nofollow noopener" href="https://v4.webpack.js.org/guides/output-management/#cleaning-up-the-dist-folder">https://v4.webpack.js.org/guides/output-management/#cleaning-up-the-dist-folder</a></p> <p>Webpack4版のドキュメントを見ると、同様の項目ですが対応方法が異なっていました。「clean-webpack-plugin」というプラグインを使えば削除できるので、導入・設定しましょうとのことでした。以下にドキュメントにあったサンプルを載せておきます。</p> <pre><code class="bash"># プラグインのインストール npm install --save-dev clean-webpack-plugin </code></pre> <p>webpack.config.jsはこんな感じ。</p> <pre><code class="javascript">// webpack.config.js const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); // プラグインを読み込む const { CleanWebpackPlugin } = require('clean-webpack-plugin'); module.exports = { entry: { app: './src/index.js', print: './src/print.js', }, plugins: [ new CleanWebpackPlugin(), // この行を追加する new HtmlWebpackPlugin({ title: 'Output Management', }), ], output: { filename: '[name].bundle.js', path: path.resolve(__dirname, 'dist'), }, }; </code></pre> <p>これにより、output.pathで指定したフォルダにある不要ファイルを削除したうえでビルドしてくれるとのことでした。さらに、Webpack5では細かい設定ができましたが、clean-webpack-pluginでも似たような設定はできそうです。</p> <p><a target="_blank" rel="nofollow noopener" href="https://www.npmjs.com/package/clean-webpack-plugin">https://www.npmjs.com/package/clean-webpack-plugin</a></p> <h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2> <p>今回は、Webpackで過去にビルドした残骸をきれいに削除する方法をまとめました。不必要なプラグインを導入せずに、たった1行の設定追加だけでやりたかったことが実現できてとてもハッピーです。</p> <p>そして、Webpack5ではデフォルトでこの機能が入っていることを知りませんでした。過去に調べたときはプラグイン対応だったよなぁと思いつつ調べると、やはりプラグインを導入しろ、という記事が多く出てきました。とりあえずググるのは大事ですが、ドキュメントをさっくりと眺めておく、というのも必要ですね。</p> <p></p> しきゆら tag:crieit.net,2005:PublicArticle/16709 2021-03-01T23:48:35+09:00 2021-03-01T23:48:35+09:00 https://crieit.net/posts/refer-bundled-library-from-external-js-20210301 Webpack でバンドルしたライブラリを他の JavaScript から参照したい <p>大体の JavaScript は Webpack でバンドルするのですが、一部対象外の JavaScript を用意したときに Webpack でバンドルしたライブラリを外部から参照できなかったのでメモ。</p> <h2 id="構成"><a href="#%E6%A7%8B%E6%88%90">構成</a></h2> <h3 id="ディレクトリ構造"><a href="#%E3%83%87%E3%82%A3%E3%83%AC%E3%82%AF%E3%83%88%E3%83%AA%E6%A7%8B%E9%80%A0">ディレクトリ構造</a></h3> <p>今回は以下のような環境を想定。</p> <pre><code>PROJECT_ROOT/ ├ dist/ │ ├ js/ │ │ └ external.js // Webpack でバンドルしない JavaScript │ │ │ └ index.html │ ├ src/ │ └ js/ │ └ app.js // Webpack でバンドルする JavaScript (ここでは例として jQuery を import) │ ├ package.json └ webpack.config.js // src/js/app.js を dist/js/app.js に出力 </code></pre> <h3 id="src/js/app.js"><a href="#src%2Fjs%2Fapp.js">src/js/app.js</a></h3> <pre><code class="javascirpt">import $ from 'jquery'; $(() => { $('#verse1').text('Watching unseen untouched'); }); </code></pre> <p>jQuery を import して、内部で使用している簡単な jQuery を作成。</p> <h3 id="dist/js/external.js"><a href="#dist%2Fjs%2Fexternal.js">dist/js/external.js</a></h3> <pre><code class="javascirpt">$(() => { $('#verse2').text('Empty exposed'); }); </code></pre> <p>バンドルしないスクリプトも似たような構成。ただし、 <code><script></code>タグ で jQuery を読み込むことを前提とした作りになっています。</p> <h3 id="dist/index.html"><a href="#dist%2Findex.html">dist/index.html</a></h3> <pre><code class="html"><!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Empty Exposed</title> <meta name="description" content="expose-loader のテスト"> </head> <body> <p id="verse1"></p> <p id="verse2"></p> <script src="./js/app.js" defer></script> <script src="./js/external.js" defer></script> </body> </html> </code></pre> <p>単純に <code>dist/js/app.js</code> と <code>dist/js/external.js</code> を読み込んで実行する前提の HTML です。</p> <h3 id="webpack.config.js"><a href="#webpack.config.js">webpack.config.js</a></h3> <pre><code class="javascript">const webpackTerser = require('terser-webpack-plugin'); const path = require('path'); const glob = require('glob'); const entry = () => { const entries = glob .sync( '**/*.js', { cwd: './src/js' } ) .map(function (key) { return [key, path.resolve('./src/js', key)]; }); return Object.fromEntries(entries) }; const configs = { mode: 'development', entry: entry(), output: { filename: '[name]', path: path.join(path.join(__dirname, 'dist'), 'js') }, optimization: { minimizer: [ new webpackTerser({ extractComments: 'some', terserOptions: { compress: { drop_console: false, }, }, }), ], }, devtool: 'inline-source-map' }; module.exports = configs; </code></pre> <p><code>webpack.config.js</code> はこのような感じで。</p> <h2 id="検証"><a href="#%E6%A4%9C%E8%A8%BC">検証</a></h2> <p>この状態で Webpack でバンドルすると、 <code>dist/js/external.js</code> からは <code>dist/js/app.js</code> にバンドルされた jQuery が参照できません。</p> <p>そのため、以下のようなエラーが表示されてしまいます。</p> <p><a href="https://crieit.now.sh/upload_images/3fd233f2c4e32f3c4200b3dedae5b337603c5dc1e35bc.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/3fd233f2c4e32f3c4200b3dedae5b337603c5dc1e35bc.jpg?mw=700" alt="外部JSからはバンドルされたライブラリが参照できないため、Uncaught ReferenceError: $ is not defined エラーが発生" /></a></p> <blockquote> <p>Uncaught ReferenceError: $ is not defined</p> </blockquote> <h2 id="対処"><a href="#%E5%AF%BE%E5%87%A6">対処</a></h2> <p>そこで、 <a target="_blank" rel="nofollow noopener" href="https://www.npmjs.com/package/expose-loader">expose-loader</a> を加えた ( <code>yarn add -D expose-loader</code> ) 上で <code>webpack.config.js</code> を以下のように書き換えます。</p> <pre><code class="javascript">const webpackTerser = require('terser-webpack-plugin'); const path = require('path'); const glob = require('glob'); const entry = () => { const entries = glob .sync( '**/*.js', { cwd: './src/js' } ) .map(function (key) { return [key, path.resolve('./src/js', key)]; }); return Object.fromEntries(entries) }; const configs = { mode: 'development', entry: entry(), output: { filename: '[name]', path: path.join(path.join(__dirname, 'dist'), 'js') }, module: { // ここから rules: [ { test: require.resolve("jquery"), loader: "expose-loader", options: { exposes: ["$", "jQuery"], } } ] }, // ここを追記 optimization: { minimizer: [ new webpackTerser({ extractComments: 'some', terserOptions: { compress: { drop_console: false, }, }, }), ], }, devtool: 'inline-source-map' }; module.exports = configs; </code></pre> <p>これでもう一度ビルド。</p> <p><a href="https://crieit.now.sh/upload_images/0a6513df7e09df57e54f4f49538d42e2603c5db61be66.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/0a6513df7e09df57e54f4f49538d42e2603c5db61be66.jpg?mw=700" alt="外部JSからもバンドルされたライブラリを参照できるようになり、エラーがなくなって画面へのテキスト出力もされるようになった" /></a></p> <p>今度はエラーがなく、出力処理が正常にできたことが確認できました。OKです。</p> <h2 id="参考"><a href="#%E5%8F%82%E8%80%83">参考</a></h2> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/jkr_2255/items/5bff71d71286979e8a4e">Webpackと外部のJavaScriptを連携する - Qiita</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://www.npmjs.com/package/expose-loader">expose-loader - npm</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/gale07/items/3df21b245d8bd7edd793">Phaser 3 + Webpack + Typescript でゲームを作る - Qiita</a></li> </ul> arm-band tag:crieit.net,2005:PublicArticle/16654 2021-01-27T04:15:28+09:00 2021-04-28T08:56:12+09:00 https://crieit.net/posts/webpack-watch webpack --watch で差分ビルドの完了時に任意のコマンドを実行する <p>「任意のコマンドを実行」としたけど、要するに完了にフックして通知がしたい。</p> <p>(※ 2019-01-29 に書いた記事のクロス投稿です)</p> <hr /> <p>単純な watch の実行:</p> <pre><code>$(npm bin)/webpack --watch --config webpack.config.js </code></pre> <p>これだけだと(差分)ビルド完了の検出がやりにくそうなので、 <code>--info-verbosity verbose</code> を追加する。</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://webpack.js.org/configuration/watch/#info-verbosity">Watch and WatchOptions > info-verbosity</a></li> </ul> <blockquote> <p>Setting info-verbosity to verbose will also message to console at the beginning and the end of incremental build. info-verbosity is set to info by default.</p> </blockquote> <pre><code>$(npm bin)/webpack --watch --config webpack.config.js \ --info-verbosity verbose </code></pre> <p>開始時と完了時にこういうメッセージが出力される:</p> <pre><code>Compilation starting… Compilation finished </code></pre> <p>あとは適当なラッパーを書いて、完了メッセージを検出してコマンドを実行すればOK。 Ubuntu だと <code>notify-send</code> コマンドで通知できる。 Webpack の設定でできるか調べるよりこっちの方が速かった……。まあでもこの方法なら webpack 以外でも使いまわせるし、いいよね、ということにしておく。</p> <pre><code class="ruby">#!/usr/bin/env ruby require "pty" npm_bin = `npm bin`.chomp watch_cmd = [ "#{npm_bin}/webpack", "--watch", "--config webpack.config.js", "--color=false", "--info-verbosity verbose" ].join(" ") PTY.spawn(watch_cmd) do |i, o| loop do line = i.gets print line if line.chomp == "Compilation finished" timestamp = Time.now.strftime("%F %T") system "notify-send 'Compilation finished #{timestamp}'" end end end </code></pre> <p>試しにやってはみたものの、今手元にあるものは規模が小さくてビルドがすぐ終わるので便利になったかよく分からない(やる前に気付こう)。</p> <hr /> <pre><code>$(npm bin)/webpack --version 4.29.0 </code></pre> <h1 id="関連"><a href="#%E9%96%A2%E9%80%A3">関連</a></h1> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/entry/20180218/1518907742">(Ruby) PTY.spawn("bash -i")でコマンド実行してプロンプトをexpectしつつ途中の出力も随時表示する</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://memo88.hatenablog.com/archive/category/Ruby">その他 Ruby 関連で書いたもの</a></li> </ul> sonota486 tag:crieit.net,2005:PublicArticle/16653 2021-01-27T00:00:53+09:00 2021-01-27T00:00:53+09:00 https://crieit.net/posts/webpack-dynamically-change-entrypoint-20210121 Webpack (webpack-stream) で エントリーポイントを動的に変更する <p>Scss に続いて JS でも動的に読み込むファイルを切り替えたいと考えました。</p> <h2 id="今回の方法"><a href="#%E4%BB%8A%E5%9B%9E%E3%81%AE%E6%96%B9%E6%B3%95">今回の方法</a></h2> <h3 id="ディレクトリ構造"><a href="#%E3%83%87%E3%82%A3%E3%83%AC%E3%82%AF%E3%83%88%E3%83%AA%E6%A7%8B%E9%80%A0">ディレクトリ構造</a></h3> <p>サンプルとして、以下のようなディレクトリ構造にしたとします。</p> <pre><code> / ├ dist/ │ └ js/ │ ├ app.min.js // app.js からビルド │ └ parts.min.js // parts/ ディレクトリ下のjsをバンドル └ src/ └ js/ ├ app.js │ └ parts/ ├ parts1.js // 単独 ├ parts2.js // parts3.js に依存 └ parts3.js </code></pre> <ul> <li><code>src/js/app.js</code>: (存在すれば)常に出力、通常の処理 <ul> <li><code>dist/js/app.min.js</code> にビルド</li> </ul></li> <li><code>src/js/parts/</code>ディレクトリ下: フラグによって動的に読み込むファイルを切り替える <ul> <li><code>dist/js/parts.min.js</code> にバンドル</li> </ul></li> </ul> <p>状況的にはこのようにしたいと考えています。</p> <h3 id="webpack.config.js"><a href="#webpack.config.js">webpack.config.js</a></h3> <p>以上を踏まえて <code>webpack.config.js</code> 。</p> <pre><code class="javascript">const webpackTerser = require('terser-webpack-plugin'); const { CleanWebpackPlugin } = require('clean-webpack-plugin'); const path = require('path'); const glob = require('glob'); const devFlag = true; // dynamic import test const partsPath = 'src/js/parts'; const flag = { parts1: true, parts2: true }; const entry = () => { const entries = glob .sync( '**/*.js', { cwd: 'src/js', //ignore ignore: [ 'parts/**/*.js', '**/_*.js' ] } ) .map(function (key) { return [key, path.resolve('src/js', key)]; }); // `{ 'app.js': 'PATH/TO/PROJECT/src/js/app.js' }` のようなオブジェクトを entriesObj とする let entriesObj = Object.fromEntries(entries); // 以下、 parts に関する処理 partsCount = 0; // flag の中で true になっている個数をカウント Object.keys(flag).forEach(function (index) { if (flag[index]) { partsCount++; } }); if (partsCount > 0) { // flag の中で true になっている個数が少なくとも1つある場合は、 `parts.js` キーを entriesObj に追加 entriesObj[`parts.js`] = []; // entriesObj の `parts.js` キー に配列で複数のファイル (parts1.js, parts2.js) を指定 Object.keys(flag).forEach(function (index) { if (flag[index]) { entriesObj[`parts.js`].push(path.resolve(partsPath, `${index}.js`)); } }); } return entriesObj; }; const configs = { mode: development, entry: entry(), output: { filename: '[name]', }, devtool: 'inline-source-map', plugins: [ new CleanWebpackPlugin({ cleanOnceBeforeBuildPatterns: [ 'dist/js/**/*.js' ], }), ], optimization: { minimizer: [ new webpackTerser({ extractComments: 'some', terserOptions: { compress: { drop_console: false, }, }, }), ], }, }; module.exports = configs; </code></pre> <ul> <li>エントリーポイントには <code>glob.sync</code> から始まる処理で指定 <ul> <li><code>src/js</code>下 の <code>.js</code> ファイルを <code>{ 'app.js': 'PATH/TO/PROJECT/src/js/app.js' }</code> のようにキーはファイル名、値がファイルへのフルパス、となるようなオブジェクトを作っています <ul> <li><code>ignore</code> で <code>src/js/parts/</code>下 は最初は除外しています</li> <li>本題からは逸れますが <code>_*.js</code> で <code>_</code> 始まりのファイルも <code>ignore</code> 指定しています</li> </ul></li> </ul></li> <li>オブジェクト <code>flag</code> のキーと <code>src/js/parts/</code>下 のファイル名を一致させている、という条件下でフラグの <code>true</code> or <code>false</code> によりエントリーポイントを変化させています <ul> <li>今回の出力は <code>dist/js/parts.min.js</code> でひとまとめにする前提にしています。 <code>app.min.js</code> の中には混ぜません</li> <li>そのため、エントリーポイントを <code>{ 'app.js': [ 'PATH/TO/PROJECT/src/js/app.js', 'PATH/TO/PROJECT/src/js/parts/parts1.js', ... ] }</code> とするのではなく、 <code>{ 'app.js': 'PATH/TO/PROJECT/src/js/app.js', 'parts.js': [ 'PATH/TO/PROJECT/src/js/parts1.js', 'PATH/TO/PROJECT/src/js/parts/parts2.js', ... ] }</code> となるように処理を加えました</li> </ul></li> </ul> <h3 id="Gulpタスク (JavaScriptに関する部分)"><a href="#Gulp%E3%82%BF%E3%82%B9%E3%82%AF+%28JavaScript%E3%81%AB%E9%96%A2%E3%81%99%E3%82%8B%E9%83%A8%E5%88%86%29">Gulpタスク (JavaScriptに関する部分)</a></h3> <pre><code class="javascript">const { dest } = require('gulp'); const plumber = require('gulp-plumber'); const notify = require('gulp-notify'); const rename = require('gulp-rename'); const webpackStream = require('webpack-stream'); const webpackConfig = require('webpack.config'); const jsBuild = () => { return webpackStream(webpackConfig) .pipe(plumber({ errorHandler: notify.onError({ message: 'Error: <%= error.message %>', title: 'jsLibBuild' }) })) .pipe(rename((path) => { path.basename += '.min' path.extname = '.js' })) .pipe(dest('dist/js')); }; module.exports = jsBuild; </code></pre> <p>これでひとまず意図した出力が得られました。</p> <h2 id="参考"><a href="#%E5%8F%82%E8%80%83">参考</a></h2> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sansaisoba/items/921438a19cbf5a31ec53">webpackの基本だけどハマりやすいentryの設定と[name] - Qiita</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://speakerdeck.com/tomof/webpackgahe-gu-bi-yao-de-he-gu-fen-kariduraifalseka">webpackが何故必要で、 何故分かりづらいのか - Speaker Deck</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/kaoru-furusawa/items/fb3f6a3b5023013f3122">webpack + glob でentryファイルを複数指定する方法 - Qiita</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://webpack.js.org/concepts/entry-points/#multi-page-application">webpackのentryファイルを複数指定、globパッケージの使い方 - Qiita</a></li> </ul> arm-band tag:crieit.net,2005:PublicArticle/16640 2021-01-21T00:22:56+09:00 2021-01-21T00:22:56+09:00 https://crieit.net/posts/webpack-clean-webpack-plugin-note-20210120 Webpack の clean-webpack-plugin のメモ <p>よんどころなき事情により Webpack で <code>clean-webpack-plugin</code> を使おうとしたのですが、過去の記事を見ていたら嵌まったのでメモしておきます。</p> <h2 id="経緯"><a href="#%E7%B5%8C%E7%B7%AF">経緯</a></h2> <p>処理中のフラグによって <code>.js</code> ファイルを複数出力(数はフラグにより増減)しようとしたのですが、減らすときは当然 output 先 (ここでは <code>dist/js</code> )は前の出力結果が残ってしまっているので、余計なゴミとして残ってしまうことに気付きました。</p> <p>そこで Webpack のビルド直前に output 先をクリーニングしようと考え、検索すると <code>clean-webpack-plugin</code> がヒットしたので、これを試そうとしました。</p> <p>なお、パッケージの指定は以下の通りです。</p> <pre><code> "devDependencies": { "webpack": "^5.15.0", "webpack-stream": "^6.1.1", "terser-webpack-plugin": "^5.1.1", "clean-webpack-plugin": "^3.0.0" }, "resolutions": { "webpack": "^5.15.0" }, </code></pre> <p>Gulp から <code>webpack-stream</code> 経由で Webpack を使用、という状況です。</p> <h2 id="現象"><a href="#%E7%8F%BE%E8%B1%A1">現象</a></h2> <p><code>clean-webpack-plugin</code> の使い方の記事を見ると、 <code>webpack.config.js</code> に以下のように書くケースが散見されました。</p> <pre><code class="javascript">const webpackTerser = require('terser-webpack-plugin'); const { CleanWebpackPlugin } = require('clean-webpack-plugin'); // require const path = require('path'); const glob = require('glob'); const entry = () => { const entries = glob .sync( '**/*.js', { cwd: 'src/js', ignore: [ '**/_*.js' ] } ) .map(function (key) { return [key, path.resolve('src/js', key)]; }); return Object.fromEntries(entries); }; const configs = { mode: 'development', entry: entry(), output: { filename: '[name]', }, devtool: 'inline-source-map', plugins: [ // plugin new CleanWebpackPlugin([ 'dist/js' // cleanup path ]), ], optimization: { minimizer: [ new webpackTerser({ extractComments: 'some', terserOptions: { compress: { drop_console: false, }, }, }), ], }, }; module.exports = configs; </code></pre> <p>今回引っかかったのは <code>CleanWebpackPlugin</code> インスタンスへの引数で「クリーニングしたいディレクトリのパスの配列」を指定する部分。</p> <pre><code>Error: clean-webpack-plugin only accepts an options object. See: https://github.com/johnagan/clean-webpack-plugin#options-and-defaults-optiona </code></pre> <p><code>clean-webpack-plugin</code> の引数はオブジェクトの形式ですって?</p> <p>散見される記事は配列でした。</p> <p>上記エラー文の通り、 Github を参照すると……確かに<strong>オブジェクトの形式</strong>で指定されています。</p> <p>これ以上は突っ込んでいませんが、どうやらどこかのバージョン (今回インストールした <code>clean-webpack-plugin</code> は <code>3.0.0</code> だったので、2系と3系で乖離があるのかもしれません) で仕様が変わったのかもしれません。</p> <p>これを受けて以下のように修正。</p> <pre><code class="javascript">const webpackTerser = require('terser-webpack-plugin'); const { CleanWebpackPlugin } = require('clean-webpack-plugin'); // require const path = require('path'); const glob = require('glob'); const entry = () => { const entries = glob .sync( '**/*.js', { cwd: 'src/js', ignore: [ '**/_*.js' ] } ) .map(function (key) { return [key, path.resolve('src/js', key)]; }); return Object.fromEntries(entries); }; const configs = { mode: 'development', entry: entry(), output: { filename: '[name]', }, devtool: 'inline-source-map', plugins: [ new CleanWebpackPlugin({ cleanOnceBeforeBuildPatterns: [ 'dist/js/**/*.js' // cleanOnceBeforeBuildPatterns キーの中に配列指定。また、ディレクトリパスの指定ではなく、 glob での指定 ], }), ], optimization: { minimizer: [ new webpackTerser({ extractComments: 'some', terserOptions: { compress: { drop_console: false, }, }, }), ], }, }; module.exports = configs; </code></pre> <p>変更点は以下の2つ。</p> <ol> <li>指定そのものを <code>cleanOnceBeforeBuildPatterns</code> キーに対する値として配列を記述するようにしたこと</li> <li>ディレクトリパスではなく glob 指定ということ (拡張子指定が不要な場合は <code>dist/js/**/*</code> というような形)</li> </ol> <p>これで意図した挙動になりました。</p> <h2 id="余談"><a href="#%E4%BD%99%E8%AB%87">余談</a></h2> <p>最初とりあえず試すだけ試そうとして</p> <pre><code class="javascript"> plugins: [ new CleanWebpackPlugin(), ], </code></pre> <p>途中試験のため <code>cwd</code> も抜いていたせいで、引数なしで <code>CleanWebpackPlugin</code> インスタンスを起こしたところ、プロジェクトルートからファイルを削除しようとして焦りました (デフォルトが <code>dry: false</code>(テストなし), <code>verbose: false</code>(削除するファイルをコンソールに出力しない), <code>cleanOnceBeforeBuildPatterns: ['**/*']</code>(ルートからの全てのファイル) という指定だったため)。</p> <p>うっかり走らせないこと。</p> <h2 id="参考"><a href="#%E5%8F%82%E8%80%83">参考</a></h2> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/johnagan/clean-webpack-plugin#options-and-defaults-optional">johnagan/clean-webpack-plugin: A webpack plugin to remove your build folder(s) before building</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://chaika.hatenablog.com/entry/2019/06/18/083000">Webpack Babel + Pug + Stylus で ホットリロードできるSPA開発環境作った。 - かもメモ</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://stackoverflow.com/questions/64617228/cleanwebpackplugin-does-not-clean-in-webpack-5">CleanWebpackPlugin does not clean in Webpack 5 - Stack Overflow</a></li> </ul> <h3 id="(余談) エントリーポイントの ignore 指定"><a href="#%28%E4%BD%99%E8%AB%87%29+%E3%82%A8%E3%83%B3%E3%83%88%E3%83%AA%E3%83%BC%E3%83%9D%E3%82%A4%E3%83%B3%E3%83%88%E3%81%AE+ignore+%E6%8C%87%E5%AE%9A">(余談) エントリーポイントの ignore 指定</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/masato_makino/items/7130bbe408ca929e7f0d">webpackのentryファイルを複数指定、globパッケージの使い方 - Qiita</a></li> </ul> arm-band tag:crieit.net,2005:PublicArticle/16236 2020-11-24T23:34:09+09:00 2020-11-24T23:34:09+09:00 https://crieit.net/posts/gulp4-webpack5-terser-webpack-plugin-error-resolutions-20201124 続・ Gulp 4 + Webpack 5 を試す (resolutions 使用) <p>以前、<a href="https://crieit.net/posts/gulp4-webpack5-terser-webpack-plugin-error-20201020">Gulp 4 + Webpack 5 を試す ( 未完 / terser-webpack-plugin で TypeError: Cannot read property ‘javascript’ of undefined エラーになる)</a>の記事で Gulp 4 + Webpack 5 の実験をしましたが、その続きです。</p> <h2 id="経緯"><a href="#%E7%B5%8C%E7%B7%AF">経緯</a></h2> <p><a href="https://crieit.net/posts/backstopjs-test-20201122">BackstopJS を試す (Error: Failed to launch the browser process! エラー発生→ puppeteer のバージョンを指定して解決)</a>で <code>package.json</code> に <code>resolutions</code> を記述して Yarn で内部依存パッケージのバージョンを強制的に変更する手法を取りましたが、同様の手法が使えるのではないか、と思った次第です。</p> <h2 id="検証"><a href="#%E6%A4%9C%E8%A8%BC">検証</a></h2> <h3 id="package.json"><a href="#package.json">package.json</a></h3> <p>```json:package.json<br /> // 略<br /> "devDependencies": {<br /> // 略<br /> "webpack": "^5.6.0",<br /> "webpack-stream": "^6.1.1",<br /> "terser-webpack-plugin": "^5.0.3",<br /> // 略<br /> },<br /> "resolutions": {<br /> "webpack": "^5.6.0"<br /> },<br /> // 略</p> <pre><code><br />上述のように `resolutions` で Webpack のバージョンを指定。 ### gulp/tasks/js.js `gulp/tasks/js.js` は前回のまま。 ### webpack.config.js `webpack.config.js` は source map のための設定を追加した以外はそのままです。 ```javascript:webpack.config.js const _ = require('./gulp/plugin'); const dir = require('./gulp/dir'); const mode = () => { return process.env.DEV_MODE === 'dev' ? 'development' : 'production'; }; const modeFlag = () => { return process.env.DEV_MODE === 'dev' ? false : true; }; const entry = () => { const entries = _.glob .sync( '**/*.js', { ignore: [ '_plugins/**' ], cwd: dir.src.js } ) .map(function (key) { return [key, _.path.resolve(dir.src.js, key)]; }); return Object.fromEntries(entries) }; const configs = { mode: mode(), entry: entry(), output: { filename: '[name]' }, optimization: { minimizer: [ new _.webpackTerser({ extractComments: 'some', terserOptions: { compress: { drop_console: modeFlag(), }, }, }), ], } }; if (process.env.DEV_MODE === 'dev') { // 追加 configs.devtool = 'inline-source-map'; } module.exports = configs; </code></pre> <p>これで <code>yarn restart</code> などすると</p> <pre><code class="bash">$ gulp # 略 [hh:ii:ss] asset app.js 226 KiB [emitted] [minimized] (name: app.js) 1 related asset webpack 5.6.0 compiled successfully [hh:ii:ss] Finished 'jsBuild' after 20 s </code></pre> <p>動きました!</p> <hr /> <p>後々は <code>resolutions</code> なしでも動くようにしたいですが、ひとまず動く形になったのでメモしておきます。</p> <h2 id="参考"><a href="#%E5%8F%82%E8%80%83">参考</a></h2> <h3 id="Yarn の reasolutions"><a href="#Yarn+%E3%81%AE+reasolutions">Yarn の reasolutions</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://nju33.com/yarn/resolutions%20%E3%81%A7%E4%BE%9D%E5%AD%98%E3%81%AE%E4%BE%9D%E5%AD%98%E3%81%AE%E3%83%90%E3%83%BC%E3%82%B8%E3%83%A7%E3%83%B3%E3%82%92%E6%8C%87%E5%AE%9A%E3%81%99%E3%82%8B">yarn: resolutions で依存の依存のバージョンを指定する - nju33</a></li> </ul> arm-band tag:crieit.net,2005:PublicArticle/16235 2020-11-24T23:28:18+09:00 2022-02-25T22:47:24+09:00 https://crieit.net/posts/gulp4-webpack5-terser-webpack-plugin-error-20201020 Gulp 4 + Webpack 5 を試す ( 未完 / terser-webpack-plugin で TypeError: Cannot read property 'javascript' of undefined エラーになる) <p>以前、<a target="_blank" rel="nofollow noopener" href="https://labor.ewigleere.net/2020/10/06/browserify-babelify-gulp-transpile-ie/">browserify + babelify + Gulp で IE11対応を試す</a>の記事で browserify + babelify を試しましたが、今回は Webpack に挑んでみます。</p> <p>ただし、プレーンな Gulp 環境ではなく、 <a target="_blank" rel="nofollow noopener" href="https://github.com/arm-band/kiribi_ususama/tree/8a06ef719725236c26337a71bb2b1f9b64ef0900">Ususama</a> で。</p> <h2 id="コード"><a href="#%E3%82%B3%E3%83%BC%E3%83%89">コード</a></h2> <h3 id="package.json"><a href="#package.json">package.json</a></h3> <pre><code class="json">// 略 "devDependencies": { // 略 "glob": "7.1.6", "webpack": "^5.1.3", "webpack-stream": "^6.1.0", "terser-webpack-plugin": "^5.0.0", // 略 } // 略 </code></pre> <p><code>gulp-uglify-es</code> と <code>gulp-concat</code> を除き、代わりに <code>webpack</code> 本体と Gulp で Webpack を使用するために必要な <code>webpack-stream</code> 、そして minifier の terser の Webpack 用プラグインである <code>terser-webpack-plugin</code> を追加。</p> <h3 id="gulp/tasks/js.js"><a href="#gulp%2Ftasks%2Fjs.js">gulp/tasks/js.js</a></h3> <pre><code class="javascript">const gulp = require('gulp'); const plumber = require('gulp-plumber'); const notify = require('gulp-notify'); const webpack = require('webpack'); const webpackStream = require('webpack-stream'); const rename = require('gulp-rename'); const dir = { dist: { js: './dist/js' } }; const webpackConfig = require('../../weppack.config'); const jsBuild = () => { return _.webpackStream(webpackConfig) .pipe(_.plumber({ errorHandler: _.notify.onError({ message: 'Error: <%= error.message %>', title: 'jsLibBuild' }) })) .pipe(_.rename((path) => { path.basename += '.min' path.extname = '.js' })) .pipe(_.gulp.dest(dir.dist.js)); }; module.exports = jsBuild; </code></pre> <h3 id="webpack.config.js"><a href="#webpack.config.js">webpack.config.js</a></h3> <pre><code class="javascript">const webpack = require('webpack'); const webpackTerser = require('terser-webpack-plugin'); const path = require('path'); const glob = require('glob'); const dotenv = require('dotenv').config(); const dir = { src: { js: './src/js' } }; const mode = () => { return process.env.DEV_MODE === 'dev' ? 'development' : 'production'; }; const modeFlag = () => { return process.env.DEV_MODE === 'dev' ? false : true; }; const entry = () => { const entries = _.glob .sync( '**/*.js', { ignore: [ '_plugins/**' ], cwd: dir.src.js } ) .map(function (key) { return [key, _.path.resolve(dir.src.js, key)]; }); return Object.fromEntries(entries) }; module.exports = { mode: mode(), entry: entry(), output: { filename: '[name]' }, optimization: { minimizer: [ new _.webpackTerser({ extractComments: 'some', terserOptions: { compress: { drop_console: modeFlag(), }, }, }), ], } }; </code></pre> <p>いくつかの記事を参考にしながらタスクを組みます。</p> <p>自分でカスタマイズした部分は以下。</p> <ul> <li><code>.env</code> で <code>dev</code>, <code>demo</code>, <code>prod</code> のモードを切り替えているので、その部分を <code>process.env.DEV_MODE</code> で振り分け <ul> <li>Webpack の設定の <code>mode</code> と <code>terser-webpack-plugin</code> の <code>drop_console</code> のフラグが関係しています</li> </ul></li> <li>複数の <code>.js</code> ファイルをエントリポイントにしたかったのでその部分は<a target="_blank" rel="nofollow noopener" href="https://qiita.com/masato_makino/items/7130bbe408ca929e7f0d">webpackのentryファイルを複数指定、globパッケージの使い方 - Qiita</a>を参考に</li> <li>最終的なファイル名は <code>XXX.min.js</code> の形にしたかったので <code>gulp-rename</code> を通しました</li> </ul> <h2 id="jQuery の扱い"><a href="#jQuery+%E3%81%AE%E6%89%B1%E3%81%84">jQuery の扱い</a></h2> <p>タスク自体は上記のやり方で走ることが確認できました( <code>DEV_MODE=dev</code> )。</p> <p>次は現時点ではまだ jQuery を使用しているので、 jQuery をどう読み込ませるかが課題ですが、以下のようにして動作することを確認しました。</p> <h3 id="app.js"><a href="#app.js">app.js</a></h3> <pre><code class="javascript">import $ from 'jquery'; import 'jquery.easing/jquery.easing'; $(() => { /* 処理 */ }); </code></pre> <h3 id="sitesearch.js"><a href="#sitesearch.js">sitesearch.js</a></h3> <pre><code class="javascript">import $ from 'jquery'; import List from 'list.js'; //サイト内検索 export default () => { const options = { valueNames: ['searchTitle', 'searchText'], }; const searchList = new List('listSearch', options); //hits searchList.on('searchComplete', function (a) { $('#hits').text(a.matchingItems.length); }); }; </code></pre> <p>サイト内検索で <a target="_blank" rel="nofollow noopener" href="https://listjs.com">List.js</a> を使用しているのですが、これについては<a target="_blank" rel="nofollow noopener" href="https://github.com/javve/list.js/issues/559">How am I suppose to import list.js with es6 and webpack ? · Issue #559 · javve/list.js</a>の Issues の方法で解決しました。</p> <p>ここまでは比較的順調でした。</p> <p>しかし、間も無く壁に突き当たることになります……。</p> <h2 id="TypeError: Cannot read property 'javascript' of undefined エラー"><a href="#TypeError%3A+Cannot+read+property+%27javascript%27+of+undefined+%E3%82%A8%E3%83%A9%E3%83%BC">TypeError: Cannot read property 'javascript' of undefined エラー</a></h2> <p><code>DEV_MODE=dev</code> で動作することは確認できたので、 <code>DEV_MODE=prod</code> に切り替えました。</p> <p>すると、以下のエラーが出てしまいました。</p> <pre><code>TypeError: Cannot read property 'javascript' of undefined at PATH\TO\PROJECT\node_modules\terser-webpack-plugin\dist\index.js:366:38 </code></pre> <p><code>DEV_MODE=dev</code> に戻すと先ほどと同じように問題なく動作。上記でこのフラグが関係するのは <code>mode</code> と <code>terser-webpack-plugin</code> の <code>drop_console</code> の2箇所なので、そのどちらかだろうとアタリを付けます。</p> <p>試しに <code>drop_console</code> を常に <code>false</code> としましたが、 <code>DEV_MODE=prod</code> でエラーは再現しました。</p> <p>となると、 <code>mode</code> の方ということになります。</p> <p>ここでエラー文で検索すると、以下の Issues を発見。 <code>terser-webpack-plugin</code> 本家のリポジトリです。</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/webpack-contrib/terser-webpack-plugin/issues/335">TypeError: Cannot read property 'javascript' of undefined ・ Issue #335 ・ webpack-contrib/terser-webpack-plugin</a></li> </ul> <p>発生個所も含めてエラー文が同じです。</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/webpack-contrib/terser-webpack-plugin/issues/335#issuecomment-709997726">TypeError: Cannot read property 'javascript' of undefined · Issue #335 · webpack-contrib/terser-webpack-plugin</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/webpack-contrib/terser-webpack-plugin/issues/335#issuecomment-710004332">TypeError: Cannot read property 'javascript' of undefined ・ Issue #335 ・ webpack-contrib/terser-webpack-plugin</a></li> </ul> <p>別の方のコメントを見ると、原因は以下の模様。</p> <ul> <li>terser-webpack-plugin 5 は Webpack 4 とは互換性がない</li> <li><code>webpack-stream</code> の内部で使用している Webpack がバージョン 4 系</li> </ul> <blockquote> <p>yep, we are working on it, release with fix will be today/tomorrow</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/webpack-contrib/terser-webpack-plugin/issues/335#issuecomment-710032380">TypeError: Cannot read property 'javascript' of undefined ・ Issue #335 ・ webpack-contrib/terser-webpack-plugin</a> (2020/10/16日 22:04 JST)</p> </blockquote> <p>わりとタイムリーなものを踏んでしまったようなので、 fixed されるのを待つ感じですかね……。</p> <h2 id="参考"><a href="#%E5%8F%82%E8%80%83">参考</a></h2> <h2 id="Gulp + Webpack"><a href="#Gulp+%2B+Webpack">Gulp + Webpack</a></h2> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/tonkotsuboy_com/items/2d4f3862e6d05dc0bea1">Gulpで始めるwebpack 4入門 - Qiita</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/am10/items/2516fa04def815195ffe">gulp + webpack + babelをつかってみた - Qiita</a></li> </ul> <h3 id="webpack-stream"><a href="#webpack-stream">webpack-stream</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://www.npmjs.com/package/webpack-stream">webpack-stream - npm</a></li> </ul> <h2 id="Webpack, 複数エントリポイントとoutput"><a href="#Webpack%2C+%E8%A4%87%E6%95%B0%E3%82%A8%E3%83%B3%E3%83%88%E3%83%AA%E3%83%9D%E3%82%A4%E3%83%B3%E3%83%88%E3%81%A8output">Webpack, 複数エントリポイントとoutput</a></h2> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/masato_makino/items/7130bbe408ca929e7f0d">webpackのentryファイルを複数指定、globパッケージの使い方 - Qiita</a></li> </ul> <h2 id="List.js import"><a href="#List.js+import">List.js import</a></h2> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/javve/list.js/issues/559">How am I suppose to import list.js with es6 and webpack ? · Issue #559 · javve/list.js</a></li> </ul> <h2 id="terser-webpack-plugin"><a href="#terser-webpack-plugin">terser-webpack-plugin</a></h2> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/webpack-contrib/terser-webpack-plugin/issues/335">TypeError: Cannot read property 'javascript' of undefined ・ Issue #335 ・ webpack-contrib/terser-webpack-plugin</a></li> </ul> arm-band tag:crieit.net,2005:PublicArticle/15389 2019-09-11T12:01:00+09:00 2019-09-11T12:01:00+09:00 https://crieit.net/posts/Nuxt-js-Markdown-FAQ-md Nuxt.jsでMarkdownファイルからFAQページなどを作れるようしてみた(.mdファイル内でコンポーネントも使える) <p>ほそぼそと作っているWebサービスの<a target="_blank" rel="nofollow noopener" href="https://tsundoku.site">積読ハウマッチ</a>、<br /> <a target="_blank" rel="nofollow noopener" href="https://gigazine.net/news/20190907-tsundoku-how-much-review/">Gigazineさん</a>に紹介されました🎉🎉🎉</p> <p>おかげさまでユーザやアクセスが増えたのですが、<br /> 問い合わせやコメントも急増したので、FAQを用意したほうがいいなと...</p> <p>Nuxt.jsのページのうち、FAQのような静的なページだけ、<br /> Markdownで作成できないかを調査したときの備忘録。</p> <p>環境はnuxt(2.8.1) / typescript(3.6.2)</p> <h4 id="ちょっと長めですが..."><a href="#%E3%81%A1%E3%82%87%E3%81%A3%E3%81%A8%E9%95%B7%E3%82%81%E3%81%A7%E3%81%99%E3%81%8C...">ちょっと長めですが...</a></h4> <ol> <li>Markdownだけのシンプルな方法と</li> <li>Markdown内でVueコンポーネントを使う凝った方法</li> </ol> <p>の2パターンをまとめています。</p> <p>これを使うと一部のページをマークダウンで書けるので、<br /> FAQとか以外に<strong>ブログとか記事とかも簡単にmarkdownで書けるようになるかもと</strong>。</p> <p><strong>長めですが、見出しとソースコードを見てくとだいたい分かるかも</strong>。</p> <hr /> <h3 id="できあがったもの: 1.シンプルな方法"><a href="#%E3%81%A7%E3%81%8D%E3%81%82%E3%81%8C%E3%81%A3%E3%81%9F%E3%82%82%E3%81%AE%3A+1.%E3%82%B7%E3%83%B3%E3%83%97%E3%83%AB%E3%81%AA%E6%96%B9%E6%B3%95">できあがったもの: 1.シンプルな方法</a></h3> <p>こんな感じのMarkdownファイルを読み込んで</p> <pre><code class="markdown"># よくある質問 / FAQ ## 積読本しか登録してはいけないのですか? そんなことないです!**読み終わった本だけでも OK です ♪** まだ積読ハウマッチで**誰も登録していない本**もあるので、 **おすすめの本**などあればどんどん登録してみてください! また、同じ本を読んでいる人や積んでる人もわかるので、 **本の好みが近い人が見つかるかも知れません** 😊 </code></pre> <p><a target="_blank" rel="nofollow noopener" href="https://tsundoku.site/faq">こんな感じの画面</a>を作れちゃいます!</p> <p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/24182a0b-eb74-44cd-7200-6752dd57e152.png" width="600"/></p> <h3 id="できあがったもの: 2.凝った方法"><a href="#%E3%81%A7%E3%81%8D%E3%81%82%E3%81%8C%E3%81%A3%E3%81%9F%E3%82%82%E3%81%AE%3A+2.%E5%87%9D%E3%81%A3%E3%81%9F%E6%96%B9%E6%B3%95">できあがったもの: 2.凝った方法</a></h3> <p>ツイートを埋め込めるよう、Markdownファイルの中で<br /> <a target="_blank" rel="nofollow noopener" href="https://github.com/tonickkozlov/vue-tweet-embed#readme">vue-tweet-embed</a>を利用しています。</p> <pre><code class="markdown"># 更新情報 ## 2019/09/09  洋書検索に対応 ✨ <Tweet id="1170893520787271680" :options="{ conversation: 'none' }"></Tweet> </code></pre> <p>1.の方法だと<code><Tweet></code>タグがそのままのため、なにも表示されませんが、<br /> 2.の方法だと、<a target="_blank" rel="nofollow noopener" href="https://tsundoku.site/updates">こんな感じの画面</a>を作れちゃいます!</p> <p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/75231d61-c3a5-9309-c56c-580ea718b0a7.png" width="600"/></p> <hr /> <h1 id="ここから、それぞれの方法の説明"><a href="#%E3%81%93%E3%81%93%E3%81%8B%E3%82%89%E3%80%81%E3%81%9D%E3%82%8C%E3%81%9E%E3%82%8C%E3%81%AE%E6%96%B9%E6%B3%95%E3%81%AE%E8%AA%AC%E6%98%8E">ここから、それぞれの方法の説明</a></h1> <h2 id="1. Markdownだけのシンプルな方法"><a href="#1.+Markdown%E3%81%A0%E3%81%91%E3%81%AE%E3%82%B7%E3%83%B3%E3%83%97%E3%83%AB%E3%81%AA%E6%96%B9%E6%B3%95">1. Markdownだけのシンプルな方法</a></h2> <p>シンプルな方法はこんな感じ</p> <ol> <li>Markdownファイルを読み込んで、</li> <li>markedでHTML化して、</li> <li>v-htmlでHTMLを挿入</li> </ol> <p>Nuxt.jsでは、Markdownファイル(.md)のようなファイルを<br /> そのまま取り込む方法が無いので、<br /> Webpackプラグインの<a target="_blank" rel="nofollow noopener" href="https://github.com/webpack-contrib/raw-loader">raw-loader</a>を使います。</p> <h4 id="raw-loaderなどのインストール"><a href="#raw-loader%E3%81%AA%E3%81%A9%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB">raw-loaderなどのインストール</a></h4> <p>必要なパッケージをインストール</p> <pre><code class="console">$ npm install --save raw-loader marked highlightjs </code></pre> <h4 id="nuxt.config.jsの設定"><a href="#nuxt.config.js%E3%81%AE%E8%A8%AD%E5%AE%9A">nuxt.config.jsの設定</a></h4> <p>.mdを読み込む際に、raw-loaderを使うように設定。</p> <pre><code class="typescript">import NuxtConfiguration from "@nuxt/config"; const config: NuxtConfiguration = { build: { extend(config, ctx) { if (!!config.module) { // .mdファイルだったら、raw-loaderを使うように設定 config.module.rules.push({ test: /\.md$/, use: ["raw-loader"] }); } } }, } </code></pre> <h4 id=".vueはこんな感じ"><a href="#.vue%E3%81%AF%E3%81%93%E3%82%93%E3%81%AA%E6%84%9F%E3%81%98">.vueはこんな感じ</a></h4> <p>これだけで、.mdファイルを読み込んでページが作れちゃいます!<br /> あとは読み込むファイルを変えるだけで、簡単に静的なページを量産(<em>´ω`</em>)</p> <pre><code class="html"><template> <!-- v-htmlでmarked()の結果を渡す --> <div class="marked" v-html="text"></div> </template> <script lang="ts"> import { Component, Vue } from "nuxt-property-decorator"; import marked from "marked"; import hljs from "highlightjs"; // requireで.mdファイルを読み込む。 const mdText = require("~/assets/faq.md"); @Component export default class FaqPage extends Vue { created() { // 作成時にmarkedの初期設定 // ハイライトにhighlightjsを使うようにする marked.setOptions({ langPrefix: "", breaks: true, highlight: function(code, lang) { return hljs.highlightAuto(code, [lang]).value; } }); } // **************************************************** // * computed // **************************************************** private get text() { // 読み込んだ.mdファイルをmarkedでHTML化する return marked(mdText.default); } } </script> </code></pre> <p>そのままだといい感じの見た目にならないので、<br /> <code>class="marked"</code>のようにしておいて、<br /> markdown用のCSSを設定していけばOK。</p> <p>どんなCSSがいいかは、<a target="_blank" rel="nofollow noopener" href="https://qiita.com/kira_puka/items/8b5bc56a78fbb98d3df9#%E3%83%9E%E3%83%BC%E3%82%AF%E3%83%80%E3%82%A6%E3%83%B3%E7%94%A8%E3%81%AEcss%E3%82%92%E8%BF%BD%E5%8A%A0%E3%81%99%E3%82%8B">前の記事</a>を参照ください。</p> <h4 id="注意: ハマった点..."><a href="#%E6%B3%A8%E6%84%8F%3A+%E3%83%8F%E3%83%9E%E3%81%A3%E3%81%9F%E7%82%B9...">注意: ハマった点...</a></h4> <p>2つ注意というか、ハマった点...</p> <h5 id="A) .mdファイルを読み込むときはrequireを使う"><a href="#A%29+.md%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%92%E8%AA%AD%E3%81%BF%E8%BE%BC%E3%82%80%E3%81%A8%E3%81%8D%E3%81%AFrequire%E3%82%92%E4%BD%BF%E3%81%86">A) .mdファイルを読み込むときはrequireを使う</a></h5> <p>ぼくの環境だけかも知れませんが、<br /> <code>import mdText from "~/assets/faq.md"</code>とすると<br /> VSCode上でエラーがきませんでした。。<br /> TypeScriptの設定やVeturの問題?</p> <p>そのため、requireを使い、<br /> <code>const mdText = require("~/assets/faq.md");</code><br /> としてます。</p> <p>それによりモジュールとして読み込まれてしまうため、<br /> <code>mdText.default</code>でテキストを取得しています。</p> <p>(本当はimport文がいいんですが、あきらめてrequireに...)</p> <h5 id="B) v-htmlで挿入したHTMLにScoped CSSが効かない"><a href="#B%29+v-html%E3%81%A7%E6%8C%BF%E5%85%A5%E3%81%97%E3%81%9FHTML%E3%81%ABScoped+CSS%E3%81%8C%E5%8A%B9%E3%81%8B%E3%81%AA%E3%81%84">B) v-htmlで挿入したHTMLにScoped CSSが効かない</a></h5> <p>VueでScoped CSSを使う場合、<a target="_blank" rel="nofollow noopener" href="[https://vue-loader.vuejs.org/guide/scoped-css.html](https://vue-loader.vuejs.org/guide/scoped-css.html)">data-v-*属性が付与されます</a>が、<br /> v-htmlでHTMLを挿入しているため、その属性がついていません。。</p> <p>これにより、Scoped CSSが効かないため、<br /> markdown用のCSSを利用する際はグローバルで設定する必要があります。</p> <p>(これでだいぶハマりました...)</p> <hr /> <h2 id="2. コンポーネントを使う凝った方法"><a href="#2.+%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E3%82%92%E4%BD%BF%E3%81%86%E5%87%9D%E3%81%A3%E3%81%9F%E6%96%B9%E6%B3%95">2. コンポーネントを使う凝った方法</a></h2> <p>1.の方法で、ツイートを埋め込んでみようと思ったらうまく行かず...<br /> vueでツイートを埋め込むには、<a target="_blank" rel="nofollow noopener" href="https://github.com/tonickkozlov/vue-tweet-embed">vue-tweet-embed</a>が必要なようで、<br /> 以下のようにしてみたら、</p> <pre><code class="markdown"># 更新情報 ## 2019/09/09  洋書検索に対応 ✨ <Tweet id="1170893520787271680" :options="{ conversation: 'none' }"></Tweet> </code></pre> <p>こんな感じに...</p> <p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/ae9412bf-ae08-f681-53f7-807a3111721a.png" width="600"/></p> <p>ツイッターで、</p> <blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Vueのv-htmlのテキストにコンポーネントをいれることってできないかなぁ?markdownのなかにコンポーネント書けると色々便利そう。。</p>— 積読ハウマッチ📚きらぷか (@kira_puka) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/kira_puka/status/1171077003694362624?ref_src=twsrc%5Etfw">September 9, 2019</a></blockquote> <p>と悩んでたところ、<a target="_blank" rel="nofollow noopener" href="https://twitter.com/mizuki_r">@mizuki_r</a>さんから、こんなアドバイスが(<em>´ω`</em>)</p> <blockquote> <p>v-htmlだとvueの解釈は出来ないので、独自のdirective定義してvueをコンパイルできるようにしたことはあります</p> <p>vueのtemplateオプションはHTML文字列を渡してvueのcomponentとしてcompileします。つまり、directiveで受け取ったHTML文字列をvueのtemplateオプションに渡してvueのコンポーネントを作ると…?</p> </blockquote> <p>なるほど...( ゚д゚)!</p> <h4 id="大まかな流れ"><a href="#%E5%A4%A7%E3%81%BE%E3%81%8B%E3%81%AA%E6%B5%81%E3%82%8C">大まかな流れ</a></h4> <ol> <li><a target="_blank" rel="nofollow noopener" href="https://jp.vuejs.org/v2/guide/custom-directive.html">カスタムディレクティブ</a>を用意</li> <li>カスタムディレクティブ内で<a target="_blank" rel="nofollow noopener" href="https://jp.vuejs.org/v2/guide/components.html">コンポーネント</a>を作成し、</li> <li>コンポーネントのtemplateに<code>marked()</code>のHTMLを渡して、</li> <li>Vueのコンパイル(=<code>$mounte()</code>の呼び出し)</li> <li>Vueのコンパイルした結果からHTML要素を取得(<code>$el</code>)して、</li> <li>カスタムディレクティブを設定したHTML要素の子に挿入(<code>el.appendChild(instance.$el)</code>)</li> </ol> <p>長いし、複雑...</p> <h4 id=".vueはこんな感じ"><a href="#.vue%E3%81%AF%E3%81%93%E3%82%93%E3%81%AA%E6%84%9F%E3%81%98">.vueはこんな感じ</a></h4> <p>.vue自体はほぼ同じ。<br /> <code>v-html</code>がカスタムディレクティブの<code>v-md</code>に変わってるだけ。</p> <pre><code class="html"><template> <!-- **v-md**でmarked()の結果を渡す --> <div class="marked" v-md="text"></div> </template> <script lang="ts"> // ... 略 </script> </code></pre> <h4 id="とりあえず、vue-tweet-embedのインストール"><a href="#%E3%81%A8%E3%82%8A%E3%81%82%E3%81%88%E3%81%9A%E3%80%81vue-tweet-embed%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB">とりあえず、vue-tweet-embedのインストール</a></h4> <p>とりあえず、vue-tweet-embedを使うのでインストール</p> <pre><code class="console">$ npm install --save vue-tweet-embed </code></pre> <h4 id="カスタムディレクティブ内で動的コンパイル"><a href="#%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%A0%E3%83%87%E3%82%A3%E3%83%AC%E3%82%AF%E3%83%86%E3%82%A3%E3%83%96%E5%86%85%E3%81%A7%E5%8B%95%E7%9A%84%E3%82%B3%E3%83%B3%E3%83%91%E3%82%A4%E3%83%AB">カスタムディレクティブ内で動的コンパイル</a></h4> <p>カスタムディレクティブの用意していく。<code>~/plugins/v-md.ts</code>に作成</p> <pre><code class="typescript">import Vue from "vue"; import { Tweet } from "vue-tweet-embed"; Vue.directive("md", { inserted: function(el, binding) { // v-md="value"のvalue部分を取得 const val = binding.value; // コンポーネントを作成。ルートが1つになるように<div>で囲む // Tweetをコンポーネントで使うので、componentsも設定 const cmp = Vue.extend({ components: { Tweet }, template: `<div>${val}</div>` }); // Vueコンポーネントをコンパイルして、インスタンスを取得 const instance = new cmp().$mount(); // インスタンス化からHTML要素を取得して、 const element = instance.$el // カスタムディレクティブを設定したHTML要素の子に挿入 el.appendChild(element); } }); </code></pre> <h4 id="作ったプラグインをnuxt.config.tsに追加"><a href="#%E4%BD%9C%E3%81%A3%E3%81%9F%E3%83%97%E3%83%A9%E3%82%B0%E3%82%A4%E3%83%B3%E3%82%92nuxt.config.ts%E3%81%AB%E8%BF%BD%E5%8A%A0">作ったプラグインをnuxt.config.tsに追加</a></h4> <p>作ったプラグインを有効にするため、nuxt.config.tsに設定を追加</p> <pre><code class="typescript">import NuxtConfiguration from "@nuxt/config"; const config: NuxtConfiguration = { plugins: [ { src: "~/plugins/v-md", ssr: false }, ], } </code></pre> <h4 id="これでいけるかと思いきや..."><a href="#%E3%81%93%E3%82%8C%E3%81%A7%E3%81%84%E3%81%91%E3%82%8B%E3%81%8B%E3%81%A8%E6%80%9D%E3%81%84%E3%81%8D%E3%82%84...">これでいけるかと思いきや...</a></h4> <p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/d3ac53a2-ca4c-3a0f-243d-0dc1d1f4febd.png" width="600"/></p> <p>まっしろ(<em>´ω`</em>)<br /> タイトルすら出ない...</p> <p>Consoleを見るとエラーが...<br /> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/cc06fe27-7d63-beae-fed8-3ffe674915f8.png" width="600"/></p> <blockquote> <p>error [Vue warn]: You are using the runtime-only build of Vue where the template compiler is not available. Either pre-compile the templates into render functions, or use the compiler-included build.</p> </blockquote> <p>メッセージを見てみると、<br /> 「ランタイム限定なのでコンパイルできないよ!」<br /> とのこと...なるほど...</p> <p>このあたりを見てみると、「ランタイム + コンパイラ」を選択できるので、<br /> nuxt.config.tsの設定を変更していく。</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://jp.vuejs.org/v2/guide/components.html#DOM-%E3%83%86%E3%83%B3%E3%83%97%E3%83%AC%E3%83%BC%E3%83%88%E3%83%91%E3%83%BC%E3%82%B9%E6%99%82%E3%81%AE%E8%AD%A6%E5%91%8A">DOM テンプレートパース時の警告 — Vue.js</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://jp.vuejs.org/v2/guide/installation.html#%E3%83%A9%E3%83%B3%E3%82%BF%E3%82%A4%E3%83%A0-%E3%82%B3%E3%83%B3%E3%83%91%E3%82%A4%E3%83%A9%E3%81%A8%E3%83%A9%E3%83%B3%E3%82%BF%E3%82%A4%E3%83%A0%E9%99%90%E5%AE%9A%E3%81%AE%E9%81%95%E3%81%84">ランタイム + コンパイラとランタイム限定の違い — Vue.js</a></li> </ul> <h4 id="実行時にコンパイルできるように設定を変更"><a href="#%E5%AE%9F%E8%A1%8C%E6%99%82%E3%81%AB%E3%82%B3%E3%83%B3%E3%83%91%E3%82%A4%E3%83%AB%E3%81%A7%E3%81%8D%E3%82%8B%E3%82%88%E3%81%86%E3%81%AB%E8%A8%AD%E5%AE%9A%E3%82%92%E5%A4%89%E6%9B%B4">実行時にコンパイルできるように設定を変更</a></h4> <p>公式ドキュメントの「<a target="_blank" rel="nofollow noopener" href="https://jp.vuejs.org/v2/guide/installation.html#%E3%81%95%E3%81%BE%E3%81%96%E3%81%BE%E3%81%AA%E3%83%93%E3%83%AB%E3%83%89%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6">さまざまなビルドについて</a>」を見ると、<br /> <code>vue/dist/vue.common</code>を使えば良さそう。</p> <p>なので、<code>build</code>に以下の3行を追加。</p> <pre><code class="typescript">import NuxtConfiguration from "@nuxt/config"; const config: NuxtConfiguration = { plugins: [ { src: "~/plugins/v-md", ssr: false }, ], build: { extend(config, ctx) { if (!!config.module) { config.module.rules.push({ test: /\.md$/, use: ["raw-loader"] }); } // 「ランタイム限定」から「ランタイム + コンパイラ」に変更 if (!!config.resolve && !!config.resolve.alias) { config.resolve.alias["vue$"] = "vue/dist/vue.common"; } } }, } </code></pre> <p>すると...</p> <p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/75231d61-c3a5-9309-c56c-580ea718b0a7.png" width="600"/></p> <p>出た(<em>´ω`</em>)!!</p> <h4 id="注意: 未解決..."><a href="#%E6%B3%A8%E6%84%8F%3A+%E6%9C%AA%E8%A7%A3%E6%B1%BA...">注意: 未解決...</a></h4> <p>1点対応していない部分が...<br /> Markdownファイル内で<code><nuxt-link></code>を使うと、エラーが...</p> <p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/6d8f508f-8c9c-b807-d105-f1ad4918fd8a.png" width="600"/></p> <blockquote> <p>app.js:1971 Uncaught TypeError: Cannot read property 'resolve' of undefined</p> </blockquote> <p>Nuxt側からcontextやvue-routerの情報を渡していないためだと思いますが、<br /> 詳しく見れていませんが、nuxt-linkを使うとエラーになるため、ご注意ください...</p> <h2 id="おわりに"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB">おわりに</a></h2> <p>marked+highlightjs+raw-loaderを使って、<br /> Markdownファイルでページコンテンツを作れるように!</p> <p>カスタムディレクティブを使うと、<br /> Markdownファイル内でVueコンポーネントを使えるように!</p> <p>今回のアップデートでFAQや更新情報のページに利用してますが、<br /> サイトの説明や使い方ページなどにも使えたり、<br /> Nuxt.jsアプリにmarkdownで書いた記事なども簡単にできそう(<em>´ω`</em>)</p> <h2 id="こんなのつくってます!!"><a href="#%E3%81%93%E3%82%93%E3%81%AA%E3%81%AE%E3%81%A4%E3%81%8F%E3%81%A3%E3%81%A6%E3%81%BE%E3%81%99%EF%BC%81%EF%BC%81">こんなのつくってます!!</a></h2> <p>上記を使ってFQAや更新情報ページを追加した、積読用の読書管理アプリ「積読ハウマッチ」!<br /> <a target="_blank" rel="nofollow noopener" href="https://tsundoku.site">積読ハウマッチ</a>はNuxt.js(SPA)+Firebaseで開発してます!</p> <p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/572d4947-f40b-e4dc-1c9c-bc584cd2a66c.png" width="25%"/></p> <p>もしよかったら、遊んでみてくださいヽ(=´▽`=)ノ</p> <p>要望・感想・アドバイスなどあれば、<br /> 公式アカウント(<a target="_blank" rel="nofollow noopener" href="https://twitter.com/MemoryLoverz">@MemoryLoverz</a>)や開発者(<a target="_blank" rel="nofollow noopener" href="https://twitter.com/kira_puka">@kira_puka</a>)まで♪</p> きらぷか@積読ハウマッチ/SSSAPIなど tag:crieit.net,2005:PublicArticle/14622 2018-12-03T23:42:44+09:00 2018-12-13T10:34:17+09:00 https://crieit.net/posts/SP-12 SP☆12参考表(地力表)支援サイト を支える技術 <h1 id="SP☆12参考表(地力表)支援サイト を支える技術"><a href="#SP%E2%98%8612%E5%8F%82%E8%80%83%E8%A1%A8%28%E5%9C%B0%E5%8A%9B%E8%A1%A8%29%E6%94%AF%E6%8F%B4%E3%82%B5%E3%82%A4%E3%83%88+%E3%82%92%E6%94%AF%E3%81%88%E3%82%8B%E6%8A%80%E8%A1%93">SP☆12参考表(地力表)支援サイト を支える技術</a></h1> <p>この記事はcrieitでの <a href="https://crieit.net/advent-calendars/2018/technology">個人開発サービスに用いられている技術 Advent Calendar 2018</a> の9日目の記事です。<br /> 昨日は<a href="https://crieit.net/users/ckoshien">ckoshien</a>さんの<a href="https://crieit.net/posts/0431d68ab5279f65797e41cfc18a0eb5">野球リーグスコア管理システムに用いている技術について</a>という記事でした。<br /> 明日は<a href="https://crieit.net/users/tRiaeZ1">iotas𓆡創作支援アプリ運営中𓅬</a>さんの記事です!</p> <h2 id="SP☆12参考表(地力表)支援サイト とは"><a href="#SP%E2%98%8612%E5%8F%82%E8%80%83%E8%A1%A8%28%E5%9C%B0%E5%8A%9B%E8%A1%A8%29%E6%94%AF%E6%8F%B4%E3%82%B5%E3%82%A4%E3%83%88+%E3%81%A8%E3%81%AF">SP☆12参考表(地力表)支援サイト とは</a></h2> <p>ゲームセンター等でプレイできるbeatmania IIDXというゲームの成長支援サイトです。<br /> 2014年11月に開発を開始し、運用を始めて4年ほど経過しました。<br /> 現時点で登録者数は9200人ほどで、GAのマンスリーアクティブユーザは11k人ほどです。</p> <p>beatmania IIDXは音ゲーと呼ばれるジャンルのゲームで、楽曲に難易度が設定されています。<br /> 楽曲の難易度はおおまかに言うと☆1から☆12までに分類されます。</p> <p>この分類はシングルプレイ(SP)、ダブルプレイ(DP)といったモードごとに存在しているのですが、本サイトではその中でもSPの☆12についてだけ取り扱ってます。</p> <p>なお、このサイトはGitHub上でpublic repositoryとして<a target="_blank" rel="nofollow noopener" href="https://github.com/8398a7/abilitysheet">公開しています</a>。<br /> 参考になったらスターをいただけるとモチベーションになります。</p> <h3 id="なぜSP☆12だけなのか"><a href="#%E3%81%AA%E3%81%9CSP%E2%98%8612%E3%81%A0%E3%81%91%E3%81%AA%E3%81%AE%E3%81%8B">なぜSP☆12だけなのか</a></h3> <p>ここではゲーム性の説明を行います。<br /> 技術的な話を知りたい方はスキップしてもらって構いません。</p> <p>SP☆12は現時点で(未解禁のものも含めると)300曲を超えています。<br /> そして他の難易度帯と比較すると☆12は易しい曲と難しい曲の難易度差が最も激しくなっています。<br /> ☆12に挑戦し始めの人は噂程度に「この曲は☆12の中でも難しいと言われている気がする…」という選曲判断が必要になってきます。<br /> 公式にも指標に近いものがあるのですが、割とあてにならないという背景もあります。<br /> ※ 人によって得意不得意分野があるため参考表も万人に当てになるわけではないです</p> <p>また、☆12の攻略には長い時間が必要になります。<br /> 自身の例ですと、1つのゴールであろう皆伝という最上位段位の取得に☆12触り始めから1年以上の時間が必要でした。<br /> 皆伝という段位は☆12の中でも上位の難しさに位置している楽曲で構成されるコースのため、簡単な☆12からスタートし、段階を踏んで難しい☆12もクリアできるようになる必要があります。</p> <p>私の元々の開発モチベーションとしては、このステップをモチベを維持しつつ「続けていける」ようにすることでした。<br /> そのため自分がプレイしているSPであり、皆伝を取得するのに避けられない☆12を対象としたサイトを作り始めました。</p> <p>スタート地点としてはPHPで簡易的な☆12参考表サイトを作成していました。<br /> このサイトは知人等にインターナルに利用してもらっていたのですが、需要がありそうということで公開を前提に作り直し始めたのが2014年11月です。</p> <h2 id="採用している技術たち"><a href="#%E6%8E%A1%E7%94%A8%E3%81%97%E3%81%A6%E3%81%84%E3%82%8B%E6%8A%80%E8%A1%93%E3%81%9F%E3%81%A1">採用している技術たち</a></h2> <p>ざっくりと書くと、</p> <ul> <li>サーバ <ul> <li>[email protected]</li> <li>Sidekiq</li> <li>[email protected]</li> </ul></li> <li>フロント <ul> <li>[email protected]</li> <li>React/Redux</li> <li>ImmutableJS</li> </ul></li> <li>インフラ <ul> <li>さくらVPS(2コア、4GB、SSD 50GB)</li> <li>(ローカル環境のみ)k8s対応</li> <li>nginx</li> <li>god</li> <li>mackerel</li> <li>BigQuery(アクセスログ解析)</li> </ul></li> </ul> <p>といった形になっています。<br /> AWS/GCP等も検討したことはあるのですが、VPSと比較すると月額コストが跳ね上がるため見送っています。</p> <h3 id="技術的な解説"><a href="#%E6%8A%80%E8%A1%93%E7%9A%84%E3%81%AA%E8%A7%A3%E8%AA%AC">技術的な解説</a></h3> <p>はじめてRailsを使って作成したアプリだったことによるイマイチコードがいたるところに点在しています。<br /> 合間を見て少しずつ改善はしているものの既にユーザが利用していると変更しにくい部分があり、手がつけにくいところもあります。<br /> 特にRailsのroutes.rbの記法の修正は既にブックマークをしているユーザへの影響を考えると変更しにくいです。</p> <p>一度ルーティングの変更をリダイレクトサポートをしながら変更したことがあったのですが、1年半ほどそのページを開くとリダイレクトしつつnoticeでブックマークの変更を促していたにも関わらず、いざサポートを切るとdead linkを参照してしまう方は想像より多かったのを記憶しています。</p> <p>技術的な要素でいうと、「いたって普通なRailsアプリ」をメインポリシーとして作成しています。<br /> 特殊なことをいろいろと行うと、その分メンテナンスコストも上がるため極力Rails Wayに乗っておこうという精神です。<br /> メインで開発し続けているサービスなら多少の獣道も選択としてはありですが、社会人が片手間に行う分には大衆に乗っかるほうが楽でした。</p> <p>最近まではフロントエンドとの連携に自作のgem使ったりしながら特殊なことをしていたのですが、<br /> そのあたりはwebpackerに乗り換え、スタンダードRailsへの変身しました。<br /> (この変身コストはとても大きかったです。。)</p> <h4 id="webpacker 4.0.0.pre.3"><a href="#webpacker+4.0.0.pre.3">webpacker 4.0.0.pre.3</a></h4> <p>webpackerのstable releaseとしては3系ですが、このプロジェクトでは4系を利用しています。<br /> 理由としては最新のts-loaderを使いたかったためです。</p> <p>ts-loaderの最新を利用するためにはwebpack4系である必要があるのですが、stable releaseのwebpackerではwebpack3系までしか対応していなかったためです。</p> <p>webpackerを採用した理由としては「現在はRailsの中でそれが主流」という理由以上のことはありません。<br /> 疎なwebpackインテグレーションを自作しても良かったのですが、メンテナンスを続けるモチベが持てなさそうなので敬遠しました。<br /> 実際使ってみると細かいところで不満はあるものの、トータル的には体感は悪くないという印象を受けました。</p> <p>webpackerでは下記のように少しだけ設定を変えています。</p> <pre><code class="javascript">process.env.NODE_ENV = process.env.NODE_ENV || 'production' const environment = require('./environment') module.exports = environment.toWebpackConfig() module.exports.devtool = 'source-map' </code></pre> <p>といった設定にしてproductionでは devtoolの <code>nosources-nource-map</code> を <code>source-map</code> に置き換えていることです。<br /> source-map自体を切る修正を入れる人が多そうな印象ですが、自分の環境ではあえて設定しています。<br /> 理由はsentryでのエラートラッキングが快適になるためです。</p> <p><a href="https://crieit.now.sh/upload_images/caa3aa2a085238231a7bef714bcb36675c05373413280.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/caa3aa2a085238231a7bef714bcb36675c05373413280.png?mw=700" alt="sentry-source-map.png" /></a><br /> このような形でエラー部分の元ソースコードを表示することができます。<br /> また <code>raven-for-redux</code> というnpmを利用することでそのときのactionやstateの状態までわかります。</p> <p>そのエラーになるまでのactionの履歴<br /> <a href="https://crieit.now.sh/upload_images/381f2f4536d46c0c6f9e9aea4a1af9e05c0537bbb3cf5.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/381f2f4536d46c0c6f9e9aea4a1af9e05c0537bbb3cf5.png?mw=700" alt="action-history.png" /></a></p> <p>そのエラーが出たときのクライアント側のstate<br /> <a href="https://crieit.now.sh/upload_images/4debee22b3854b335cf6daf70a0d43f95c0537eb5e318.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/4debee22b3854b335cf6daf70a0d43f95c0537eb5e318.png?mw=700" alt="reducer.png" /></a></p> <p>OSSなので特にこのサービスに関しては隠蔽する必要はないのですが、mapファイルはsentryサーバ以外からアクセスできないようにしています。<br /> 自前でsentryを立てているので特定IPからのアクセスは弾くようになっています。</p> <pre><code class="nginx">location ^~ /packs/ { gzip_static on; set $response_file 1; if ($request_uri ~ 'map$') { set $response_file 0; } if ($remote_addr = 'xxx.xx.xxx.xxx') { set $response_file 1; } if ($response_file = '0') { return 403; } } </code></pre> <p>typescriptに置き換えてからまだ一度もエラーが出ていなかったため、この画面は意図的にエラーを仕込んで発生させました…w</p> <p>今のところ、webpacker4系に対する不具合は踏んでおらず問題なく運用できています。</p> <h4 id="react-rails"><a href="#react-rails">react-rails</a></h4> <p>Reactとのつなぎ込みにはreact-railsというgemを利用しています。<br /> react_on_railsというgemのほうが多機能なのですが、相容れないところがありreact-railsを採用することにしました。</p> <p>react_on_railsのほうが優れている点としてはrailsContextというオブジェクトが付与されており、<br /> この中にtimezone/localeなどを始めとした開発者がほしいであろうメタ情報が入っています。<br /> またRailsにxhrリクエストをする際にcsrf-tokenをheaderにセットするAPIも用意されています。</p> <p>ただcomponentをrenderするためのhookに <code>turbolinks:render</code> を利用しており、このhookではページがチャタるケースがあったため、採用しませんでした。</p> <p>react-railsはreact_on_railsと比べると非常にシンプルで、コンポーネントをrenderする以上のサポートはあまりないようです。<br /> ただそれを行ってくれるだけで自分の要件的には十分だったため、採用しています。<br /> react-railsではcomponentをrenderするhookに <code>turbolinks:load</code> を採用しています。</p> <p>チャタるとは何かをもう少しわかりやすく言うと、一瞬古いページが表示された後に新しいページが表示される現象のことを指しています。<br /> <a href="https://crieit.now.sh/upload_images/1e989a869fadcebc92d18312745796605c05397505626.gif" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/1e989a869fadcebc92d18312745796605c05397505626.gif?mw=700" alt="chattering.gif" /></a></p> <p>コンテンツが一瞬表示される->白くなる->再度表示されるというループになっていることがわかると思います。<br /> 一瞬表示された後と再度表示された後でAPIリクエストが2回走ってしまうという問題やUXの観点から許容できない、という判断になりました。</p> <p>上記はturbolinks的には「仕様」の挙動です。<br /> <a target="_blank" rel="nofollow noopener" href="https://github.com/turbolinks/turbolinks#full-list-of-events">GitHub - turbolinks/turbolinks: Turbolinks makes navigating your web application faster</a> から抜粋すると以下のように書かれています。</p> <blockquote> <p><code>turbolinks:render</code>fires after Turbolinks renders the page. This event fires twice during an application visit to a cached location: once after rendering the cached version, and again after rendering the fresh version.</p> </blockquote> <p>react_on_railsのgem自体にはissueを投げてみたのですが、少し私の想像を超えた範疇の問題があるらしく、今回はreact-railsを採用することにしました。</p> <h4 id="ts-routes"><a href="#ts-routes">ts-routes</a></h4> <p>Railsとフロントの繋ぎ込みには <a target="_blank" rel="nofollow noopener" href="https://github.com/bitjourney/ts_routes-rails">GitHub - bitjourney/ts_routes-rails: Exports Rails URL helpers to TypeScript, inspired by js-routes</a> というgemを利用させていただきました。<br /> 似たようなgemにjs-routesと呼ばれるものがあり、そちらを採用していたのですがts化にあたって載せ替えを行っています。<br /> パラメタ不足などの不正なルーティングに関して検知しやすくなるため、tsとrailsを組み合わせるならお薦めです。</p> <h4 id="Sidekiq"><a href="#Sidekiq">Sidekiq</a></h4> <p>兄弟サイトのIST(Iidx Score Table)からのデータ取り込みを始めとした外部サービスの連携や定期処理(sidekiq-cron)などに利用しています。</p> <p>先述した通り☆12の譜面はかなり数が多く、1年を通して「解禁」され数が増え続けていきます。<br /> これらを全て人手によって捕捉し続けるのはコストが高い、と感じ1日に1回IST側のデータを利用して新規の☆12が解禁されたら自動追加するようにしています。<br /> ただ☆12の中での難易度自体は議論によって決められるもののため、難易度が決定した後の反映処理は人手で行っています。</p> <p>当サイトはVPS1台で運用しているので、バックアップ等の日次処理も必要になってきます。<br /> これらの処理もSidekiq側に任せています。<br /> <a target="_blank" rel="nofollow noopener" href="https://github.com/8398a7/abilitysheet/blob/master/lib/service_dumper.rb">abilitysheet/service_dumper.rb at master · 8398a7/abilitysheet · GitHub</a> というlibを作り、</p> <ul> <li>DBのdump(pg_dump)</li> <li>carrierwaveで保存された画像等の収集</li> </ul> <p>を行った後にtar.gzに固めてS3に送る処理です。<br /> これらの処理をjobにして朝の5時過ぎに行うようにしています。<br /> また、手元でproduction環境の再現を行いたいときにもS3からデータを取ってきています。</p> <p>運用しているいくつかのサービスは全てこのような形でバックアップを取っており、いつVPSが死んでしまってもすぐにサービスを復旧できるようにしています。</p> <h4 id="CI"><a href="#CI">CI</a></h4> <p>CircleCIを使ってテストを行っています。<br /> テストフレームワークはrspecでjs部分のテストはjest等では書いていません。<br /> 代わりにsystem specとしてchromedriverを利用したE2Eで担保しています。<br /> 昔に書いたコードなので、コントローラ系のspecも多いですがこのあたりも粛清したいところです…w</p> <p>system specではjsを使うところ使わないところで以下のように条件分岐を書いています。<br /> systemであっても <code>js: true</code> でないところは <code>rack_test</code> で行うようにしているところと、<code>NO_HEADLESS</code> という環境変数を定義して実行するとchromeが実際に立ち上がるようにしています。</p> <pre><code class="ruby"> config.before(:each, type: :system) do |example| if example.metadata[:js] if example.metadata[:iphone6] display_size = [375, 667] args = %w[--headless --disable-gpu --user-agent=iPhone] else display_size = [1920, 1080] args = %w[--headless --disable-gpu] end args.shift if ENV['NO_HEADLESS'] caps = Selenium::WebDriver::Remote::Capabilities.chrome(chromeOptions: { args: args }) driven_by :selenium, screen_size: display_size, options: { desired_capabilities: caps } else driven_by :rack_test end end </code></pre> <p>実行する際には <code>NO_HEADLESS=true bundle exec rspec spec/systems</code> といったコマンドで実行します。<br /> spec内に <code>binding.pry</code> を仕込むことでElementも触れるのでspecがうまくいかないときの不具合調査などに利用しています。</p> <p><a href="https://crieit.now.sh/upload_images/99997c2f5ff8b294ccd760ab48ea0c1b5c0a992fc4512.gif" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/99997c2f5ff8b294ccd760ab48ea0c1b5c0a992fc4512.gif?mw=700" alt="e2e.gif" /></a></p> <h4 id="運用サポート"><a href="#%E9%81%8B%E7%94%A8%E3%82%B5%E3%83%9D%E3%83%BC%E3%83%88">運用サポート</a></h4> <p>死活監視にはmackerelとgodを利用しています。<br /> <a target="_blank" rel="nofollow noopener" href="https://mackerel.io">Mackerel(マカレル): 新世代のサーバ管理・監視サービス</a> はSaaS型のサービスでプロセスが死んでいるか生きているかの監視もできます。</p> <p>死活監視の肝としては <a target="_blank" rel="nofollow noopener" href="http://godrb.com/">God - A Process Monitoring Framework in Ruby</a> を採用しています。<br /> sidekiq/puma/redis/postgresql/nginxなどのサービス継続に必要なプロセスは全てgodに監視させています。<br /> 再起動時などにもgodに任せて起動させているため、再起動するとサービスが落ちたままになっているということもないです。</p> <p>特にsidekiqは知らない間にメモリの使いすぎでプロセスが落ちていることもあったため、godに監視させて死んでいたら起こすようにするのは精神的安定感が増し増しでした。<br /> デプロイには <code>capistrano</code> を採用していますが、こいつはsidekiqのgod監視とやや相性が悪いです。<br /> pumaと違ってgraceful restart時にプロセスがいない瞬間ができてしまうため、<br /> capistranoがsidekiqを起こすのとgodがsidekiqを起こすのが衝突してしまい、二重でsidekiqが起動してしまうケースが有るためです。<br /> capistranoと併用する場合は素直にcapではsidekiq:stopだけして起こすのはgodに任せたほうが良いという感覚を持っています。<br /> <a target="_blank" rel="nofollow noopener" href="https://github.com/8398a7/abilitysheet/blob/master/config/deploy.rb#L53">abilitysheet/deploy.rb at master · 8398a7/abilitysheet · GitHub</a></p> <h4 id="k8s"><a href="#k8s">k8s</a></h4> <p>k8sもローカル環境での動作自体はサポートしています。<br /> VPSを2台所持しているため、2台でなんとか運用できないか検討していたのですが、結局2台では割と厳しいということと、<br /> capistranoに代わるデプロイツールを自作し、メンテし続けるコストを払える自信がなかったため見送りました。</p> <p>一通りproductionとしてローカル環境で動作させるところまでは確認しているのでrailsでのk8sに参考にしたい方はどうぞ。<br /> <a target="_blank" rel="nofollow noopener" href="https://github.com/8398a7/abilitysheet/tree/master/kubernetes">abilitysheet/kubernetes at master · 8398a7/abilitysheet · GitHub</a><br /> ただ運用もしていないコードなので穴はたくさんあると思います…</p> <h4 id="BigQuery"><a href="#BigQuery">BigQuery</a></h4> <p>nginxのアクセスログをfluentd経由でBigQueryに投げるようにしています。<br /> SSDで容量が厳しく、割と細かめにログを削除しているため後でまとめて見るためにBigQueryを利用しています。</p> <p>元々はS3に置いてAthenaで集計していたのですが、BigQueryのほうがいろいろと捗りそうなので移管しました。<br /> <a target="_blank" rel="nofollow noopener" href="https://github.com/kaizenplatform/fluent-plugin-bigquery">GitHub - kaizenplatform/fluent-plugin-bigquery</a> というプラグインを利用させていただき、bqにログを送っています。</p> <p>主にARのconnection poolがタイムアウトになっていたり、急激な負荷によるアラートの調査などに使っていたりします。<br /> railsユーザであればnginxのltsvログに <code>'request_id:$sent_http_x_request_id\t'</code> を追加しておくことで、調査が捗ることがあるためおすすめします。<br /> nginxのログから問題のrequest_idがわかればproduction.logをrequest_idでgrepすることで該当行のログに辿り着きやすくなります。</p> <h3 id="おまけ IE対応"><a href="#%E3%81%8A%E3%81%BE%E3%81%91+IE%E5%AF%BE%E5%BF%9C">おまけ IE対応</a></h3> <p>最近webpackerベースのreactにリプレースを行っていたのですが、その際に困った問題と解決した手法についても記載しておきます。</p> <p><code>tsconfig.json</code> ではtargetをes5としていましたが、babel-polyfillを仕込み忘れていたことが1点目です。<br /> 記法自体はes5まで落としてくれるのですが、polyfillがないと <code>Promise</code> などがIEにはないため正常に動作しません。<br /> polyfillの入れ方に関しては、headerに <code>https://cdn.polyfill.io/v2/polyfill.min.js</code> を追加するのがおすすめです。<br /> ブラウザのUAを見て必要なpolyfillを行ってくれます。<br /> 試しにIEで見ると結構な量をpolyfillしてくれますが、最新版のchromeではほぼ空であることが確認できると思います。<br /> 自分でbundleすると配布するjsファイルが巨大化するため、cdn経由で入れることにしました。</p> <p>2点目はes5にトランスパイルされていないライブラリを利用してしまっていることでした。<br /> query-stringというライブラリで<a target="_blank" rel="nofollow noopener" href="https://github.com/sindresorhus/query-string/blob/master/index.js#L31">query-string/index.js at master · sindresorhus/query-string · GitHub</a>の行でアローファンクションを使っているのですが、これがIEだと解釈できないという問題です。<br /> webpackerのbabel-loaderはexcludeで <code>node_modules</code> が指定されているため、使っているライブラリまではes5にトランスパイルされません。<br /> よくよくドキュメントを見るとレガシーブラウザは5系を使え(最新は6系)ということが記載されていたので、5系を利用することで解決しました。</p> <p>当初は上記2点の問題がわからぬまま、IEでは見れないといった声が上がっていたため、最初の対応としてはelectronでサイトをラップしたexeを配布していました。<br /> それには<a target="_blank" rel="nofollow noopener" href="https://github.com/jiahaog/nativefier#readme">GitHub - jiahaog/nativefier: Make any web page a desktop application</a>というライブラリを利用したのですが、幅広くの場面で役に立つので困ったら利用してみてください。</p> <h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2> <p>今回はSP☆12参考表(地力表)支援サイト を支える技術をご紹介しました。<br /> 他にもいくつかサービスがありご紹介したい技術もあったのですが、割と長くなってしまったので今回はこのような形で締めたいと思います。</p> <p>サービスを運用している方の参考になれば幸いです。</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/8398a7/abilitysheet">GitHub - 8398a7/abilitysheet: This app is ability sheet for beatmania iidx music of level 12.</a></p> <p>P.S. 作者は2018年5月に念願の冥ハードを成し遂げ、ゆるふわ離脱モードに突入しました。</p> 8398a7 tag:crieit.net,2005:PublicArticle/14611 2018-11-26T21:32:02+09:00 2019-04-24T08:21:51+09:00 https://crieit.net/posts/Webpack 初心者はWebpackの事は一度忘れた方が良い <p>最近Webpackの初心者に対する話がちょこちょこ話題になっている。でも最近はWebpackへの理解がそこまで重要では無くなってきつつある感じがするので、適当にそのあたりのことを書いてみる。</p> <p>ちなみに話題になったのは下記のスライドが発端。</p> <p><a target="_blank" rel="nofollow noopener" href="https://www.slideshare.net/ssuser46977e/webpack-why-cant-you-understand-the-webpack">なぜ初心者は webpackが解らないのか?- Why can’t you understand the webpack? - </a></p> <p>あとこのあたりとか<br /> <a target="_blank" rel="nofollow noopener" href="https://mizchi.hatenablog.com/entry/2018/11/26/164523">Webpack の考え方について - mizchi's blog</a></p> <p>ちゃんと知りたい人は当記事じゃなくて上記ふたつを見た方がいい。当記事は単なる雑記のため。仕事できっちりやらないといけない人には向かない内容。(そもそもWebpack初心者と、Web開発初心者という意味の違いがある)</p> <h2 id="そもそもWebpackとは何なのか"><a href="#%E3%81%9D%E3%82%82%E3%81%9D%E3%82%82Webpack%E3%81%A8%E3%81%AF%E4%BD%95%E3%81%AA%E3%81%AE%E3%81%8B">そもそもWebpackとは何なのか</a></h2> <p>Webpackは大雑把に言えばプログラムを一つのファイルに纏めてくれるもの。</p> <p>例えば、Windowsの実行ファイルなども色々なプログラムをビルドして一つのexeファイルになる。それと同じで、最終的に一つのjsファイルとかcssファイルとかにまとめられる。</p> <p>実行ファイルではないので下記のような感じでHTML上で読み込まれる。</p> <pre><code class="html"><script src="js/matomerareta.js"></script> </code></pre> <p>もしWebpackでまとめられていない場合、はるか昔などは上記のような読み込みを何行もかけて書いてたくさんのjsファイルを読み込んでいた。</p> <h2 id="初心者は特にWebpackのことを知らなくても何か作れる"><a href="#%E5%88%9D%E5%BF%83%E8%80%85%E3%81%AF%E7%89%B9%E3%81%ABWebpack%E3%81%AE%E3%81%93%E3%81%A8%E3%82%92%E7%9F%A5%E3%82%89%E3%81%AA%E3%81%8F%E3%81%A6%E3%82%82%E4%BD%95%E3%81%8B%E4%BD%9C%E3%82%8C%E3%82%8B">初心者は特にWebpackのことを知らなくても何か作れる</a></h2> <p>Webアプリケーション用のフロントエンドのアプリケーションを開発する場合、最近はJavaScriptのフレームワークを使うことが主流になってきていると思う。</p> <p>もちろんその時Webpackを使う必要があるのだが、だいたいフレームワークにラップされているため、最初はほとんど意識しなくても良くなっている。例えばNext.jsやNuxt.js、Angular等を利用する場合、だいたいプロジェクトを新規作成するためのCLI(vue-cliやAngular CLI、create-nuxt-app等)が用意されており、それを実行するだけで一通りプロジェクトの雛形を作ってくれ、すぐに動かせる状態にまでしてくれる。そのため、そもそもWebpackの設定を一から作ってプロジェクトを構築する、ということは必要ではなくなってきている。</p> <p>PHPのフレームワークのLaravelですらLaravel MixというWebpackのラッパーが入っていて、全く意識すること無くVueを使っていくことができるようになっている。</p> <p>そのような感じで今はWebpackを知らなくてもアプリケーションが作成できる。</p> <p>もちろん細かい設定の変更や追加をする場合はWebpackの設定を弄らなければならない場面なども出てくるので、その段階になって少しずつわからないところや必要なところを調べていけばいいと思う。</p> <p>Twitter上でも時々「Webpackを学び始めたけど分からない」みたいな発言を見かけたりするが、Webpackから入るのではなく、Webpackが梱包されているフレームワークから学び始める方が色々と近道になるような気がする。</p> <h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2> <p>そんな感じでWebpackは触る機会が少なくなっていくのでは、という気がしている。個人的にもそれでよくて、余計なことは考えずアプリケーションの開発に没頭できる方がよいのでは、と思っている。</p> <p>また例が極端だけどWindows用のアプリケーションを作るときだってだいたいVisual Studioが何となくあれこれ勝手にやってくれて、ちょこちょこ必要な設定をいじる以外はビルドの仕組みをあまり理解しなければならない、ということもない。Webpackもそういうものになっていくとよいかな…と思う。(とはいえ専用の開発用ソフトウェアがあるわけではくそこまでは行かないと思うので、今の通りちょこちょこいじっていかなければ行けない状態は続くと思うけど)</p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/14599 2018-11-12T00:32:00+09:00 2018-11-12T00:32:00+09:00 https://crieit.net/posts/Gulp-4-SCSS-Stylelint-Webpack-ESLint-Edge-js-BrowserSync Gulp 4 で SCSS + Stylelint + Webpack + ESLint + Edge.js + BrowserSync なフロントエンド環境構築 <p>(<a target="_blank" rel="nofollow noopener" href="https://www.hypertextcandy.com/gulp-4-frontend-development-starter/">ブログ</a>からのクロス投稿です。)</p> <p><img src="https://cdn.hypertextcandy.com/posts/gulp-4-frontend-development-starter/EyeCatch.jpg" alt="Eye catch" /></p> <p>この記事では、Gulp 4でフロントエンドの開発環境を構築する方法を紹介します。Gulp 4をタスクランナーとして、CSSのトランスパイルに SCSS + Stylelint、JavaScript のトランスパイルに Webpack + ESLint、、テンプレートエンジンとして Edge.js、さらにローカルサーバーとして BrowserSync を組み合わせます。</p> <p>私はどちらかというとサーバサイドな人でフロントエンド専門ではないのですが、モックアップ作ったりで HTML コーディングしたくなって、いまさらながら Gulp で自分なりの環境構築をしたので結果を共有します。</p> <p>フロントエンドと言っても全面的に React や Vue でゴリゴリアプリケーションを書くというより HTML コーディングの範疇内でのユースケースを想定しています(フレームワークでゴリゴリしたいなら <a target="_blank" rel="nofollow noopener" href="https://github.com/facebook/create-react-app">create-react-app</a> とか <a target="_blank" rel="nofollow noopener" href="https://cli.vuejs.org/">vue-cli</a> とか使うといいです)。</p> <h2 id="概要"><a href="#%E6%A6%82%E8%A6%81">概要</a></h2> <p>サンプルコードも含めた設定の全体については<a target="_blank" rel="nofollow noopener" href="https://github.com/MasahiroHarada/gulp-4-starter">こちらのリポジトリ</a>を参照してください。</p> <h3 id="依存パッケージ"><a href="#%E4%BE%9D%E5%AD%98%E3%83%91%E3%83%83%E3%82%B1%E3%83%BC%E3%82%B8">依存パッケージ</a></h3> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/MasahiroHarada/gulp-4-starter/blob/master/package.json">https://github.com/MasahiroHarada/gulp-4-starter/blob/master/package.json</a></p> <p>ちなみに開発環境は Node v10.7.0、npm v6.4.1 です。</p> <h3 id="ビルドコマンド"><a href="#%E3%83%93%E3%83%AB%E3%83%89%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89">ビルドコマンド</a></h3> <p>最終的なビルドコマンドは以下の通りです。</p> <pre><code class="console">// 開発ビルド:監視モードで実行されます。 $ npm run dev // 本番ビルド:JavaScript が圧縮されます。 $ npm run prod </code></pre> <h2 id="ビルドタスク"><a href="#%E3%83%93%E3%83%AB%E3%83%89%E3%82%BF%E3%82%B9%E3%82%AF">ビルドタスク</a></h2> <h3 id="gulpfile.babel.js"><a href="#gulpfile.babel.js">gulpfile.babel.js</a></h3> <p>ES Modules(import/export)を使用したかったのでファイル名に <code>.babel</code> をつけて Babel のトランスパイルがかかるようにしました。</p> <p>gulpfile.babel.js</p> <pre><code class="js">import { series, parallel, watch } from 'gulp'; import { reload, serve } from './tasks/server'; import { styles } from './tasks/styles'; import { scripts } from './tasks/scripts'; import { templates } from './tasks/templates'; import { images } from './tasks/images'; import { clean } from './tasks/clean'; import { sass as sassConfig, scripts as jsConfig, images as imagesConfig, templates as templatesConfig } from './tasks/config'; /** * ファイルの変更を監視 */ function watchFiles() { // Sass watch(sassConfig.src, series(styles, reload)); // Templates watch( [templatesConfig.edges, templatesConfig.data, templatesConfig.helper], series(templates, reload) ); // JavaScript watch(jsConfig.src, series(scripts, reload)); // Images watch(imagesConfig.src, series(images, reload)); } /** * 開発用ビルド */ export const dev = series( clean, parallel(styles, templates, scripts, images), serve, watchFiles ); /** * 本番用ビルド */ export const build = series( clean, parallel(styles, templates, scripts, images) ); </code></pre> <p>前述の通り gulpfile を ES2015 以降の構文で書くために Babel の設定ファイルも追加します。</p> <p>.babelrc</p> <pre><code class="json">{ "presets": [ [ "@babel/preset-env", { "targets": { "node": "current" } } ] ] } </code></pre> <p>タスクを色々書くと長くなってしまうので、個別のタスクはそれぞれ別ファイルに分け、<code>tasks</code> ディレクトリに格納しています。</p> <p>それぞれ見ていきましょう。</p> <h3 id="設定ファイル"><a href="#%E8%A8%AD%E5%AE%9A%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB">設定ファイル</a></h3> <p>まずは設定ファイルです。パスの設定が入っています。</p> <p>tasks/config.js</p> <pre><code class="js">const ASSET_ROOT = 'src'; const DEST_ROOT = 'public'; export const sass = { src: `${ASSET_ROOT}/sass/**/*.scss`, dest: `${DEST_ROOT}/styles` }; export const scripts = { srcRoot: `${ASSET_ROOT}/js`, src: `${ASSET_ROOT}/js/**/*.js`, dest: `${DEST_ROOT}/js`, babelrc: { presets: [['@babel/env', { targets: '> 0.25%, not dead' }]] } }; export const templates = { root: `${ASSET_ROOT}/templates`, edges: `${ASSET_ROOT}/templates/**/*.edge`, pages: `${ASSET_ROOT}/templates/pages/**/*.edge`, data: `${ASSET_ROOT}/templates/data.json`, helper: `${ASSET_ROOT}/templates/helper.js`, dest: DEST_ROOT }; export const images = { src: `${ASSET_ROOT}/images/**/*.*`, dest: `${DEST_ROOT}/images` }; export const isProd = process.env.NODE_ENV === 'production'; </code></pre> <h3 id="BrowserSync"><a href="#BrowserSync">BrowserSync</a></h3> <p>ファイルの変更を検知して自動でブラウザをリロードさせるため、<a target="_blank" rel="nofollow noopener" href="https://browsersync.io/">BrowserSync</a> のタスクを作成します。設定方法は<a target="_blank" rel="nofollow noopener" href="https://github.com/gulpjs/gulp/blob/4.0/docs/recipes/minimal-browsersync-setup-with-gulp4.md">こちら(Minimal BrowserSync setup with Gulp 4)</a>を参考にしました。</p> <p>tasks/server.js</p> <pre><code class="js">import browserSync from 'browser-sync'; const server = browserSync.create(); /** * 開発用サーバ再起動 */ export function reload(cb) { server.reload(); cb(); } /** * 開発用サーバ起動 */ export function serve(cb) { server.init({ server: { baseDir: './public' } }); cb(); } </code></pre> <h3 id="Clean"><a href="#Clean">Clean</a></h3> <p>ファイルの出力前に出力先の既存ファイルを削除しておくためのタスクです。</p> <p>tasks/clean.js</p> <pre><code class="js">import del from 'del'; /** * 出力先のディレクトリを空にする */ export function clean() { return del(['public']); } </code></pre> <h3 id="スタイル"><a href="#%E3%82%B9%E3%82%BF%E3%82%A4%E3%83%AB">スタイル</a></h3> <p>スタイルに関しては2つのタスクを定義しています。</p> <p>まず1つめはSCSS を CSS に変換するためのタスクです。ソースマップ生成(<a target="_blank" rel="nofollow noopener" href="https://github.com/gulp-sourcemaps/gulp-sourcemaps">gulp-sourcemaps</a>)とファイル圧縮(<a target="_blank" rel="nofollow noopener" href="https://github.com/scniro/gulp-clean-css">gulp-clean-css</a>)も実行しています。</p> <p>2つめは CSS(今回は SCSS)の構文チェックツール <a target="_blank" rel="nofollow noopener" href="https://stylelint.io/">Stylelint</a> を実行するためのタスクです。<a target="_blank" rel="nofollow noopener" href="https://github.com/olegskl/gulp-stylelint">gulp-stylelint</a> を使用します。またファイルの監視中に毎回すべてのファイルを lint せず変更があったファイルのみチェックするために <a target="_blank" rel="nofollow noopener" href="https://github.com/alexgorbatchev/gulp-changed-in-place">gulp-changed-in-place</a> というプラグインも使用します。</p> <p>tasks/styles.js</p> <pre><code class="js">import gulp from 'gulp'; import gulpSass from 'gulp-sass'; import sourcemaps from 'gulp-sourcemaps'; import cleancss from 'gulp-clean-css'; import plumber from 'gulp-plumber'; import gulpStylelint from 'gulp-stylelint'; import changed from 'gulp-changed-in-place'; import { sass as config, isProd } from './config'; /** * SCSS -> CSS */ export function sass() { return gulp .src(config.src) .pipe(plumber()) .pipe(sourcemaps.init()) .pipe(gulpSass().on('error', gulpSass.logError)) .pipe(cleancss()) .pipe(sourcemaps.write('.')) .pipe(gulp.dest(config.dest)); } /** * Stylelint */ export function stylelint() { return gulp .src(config.src) // firstPass は初回実行時に全ファイルを対象とするかどうかのオプション .pipe(changed({ firstPass: true })) .pipe(gulpStylelint({ failAfterError: isProd, reporters: [{ formatter: 'verbose', console: true }], syntax: 'scss' })); } export const styles = gulp.series(stylelint, sass); </code></pre> <p>Stylelint の設定ファイルです。</p> <p>```json:.stylelintrc<br /> {<br /> "extends": "stylelint-config-standard",<br /> "plugins": ["stylelint-order"],<br /> "rules": {<br /> "order/properties-alphabetical-order": true<br /> }<br /> }</p> <pre><code><br />### 画像圧縮 [gulp-imagemin](https://github.com/sindresorhus/gulp-imagemin) を使って画像圧縮を行うタスクです。 オプションはお好みでどうぞ。 tasks/images.js ```js import gulp from 'gulp'; import imagemin from 'gulp-imagemin'; import { images as config } from './config'; export function images() { return gulp .src(config.src) .pipe(imagemin()) .pipe(gulp.dest(config.dest)); } </code></pre> <h3 id="Edge.js → HTML"><a href="#Edge.js+%E2%86%92+HTML">Edge.js → HTML</a></h3> <p>テンプレートエンジン <a target="_blank" rel="nofollow noopener" href="https://edge.adonisjs.com/">Edge.js</a> のファイルを HTML に変換するためのタスクです。</p> <p>Edge.js について詳しくは<a target="_blank" rel="nofollow noopener" href="https://www.hypertextcandy.com/build-frontend-with-edgejs">こちらの記事</a>もご覧ください。</p> <p>tasks/templates.js</p> <pre><code class="js">import gulp from 'gulp'; import edge from 'edge.js'; import tap from 'gulp-tap'; import rename from 'gulp-rename'; import fs from 'fs'; import path from 'path'; import { templates as config } from './config'; /** * Edge.js -> HTML */ export function templates() { // テンプレートを読み込む edge.registerViews(path.join(__dirname, `../${config.root}`)); // データファイルを読み込む const data = fs.existsSync(config.data) ? JSON.parse(fs.readFileSync(config.data, 'utf8')) : {}; // ヘルパー関数を読み込む fs.existsSync(config.helpers) && require(`../${config.helper}`); return gulp .src(config.pages) .pipe( tap(file => { const contents = edge.renderString(String(file.contents), data); file.contents = new Buffer(contents); }) ) .pipe(rename({ extname: '.html' })) .pipe(gulp.dest(config.dest)); } </code></pre> <h3 id="JavaScript"><a href="#JavaScript">JavaScript</a></h3> <p>タスク <code>esTranspile</code> は <a target="_blank" rel="nofollow noopener" href="https://www.npmjs.com/package/webpack-stream">Webpack</a> により JavaScript のトランスパイル〜バンドルを行います。</p> <p>また <code>esLint</code> タスクではコードのチェックのために <a target="_blank" rel="nofollow noopener" href="https://eslint.org/">ESLint</a> を実行します。<a target="_blank" rel="nofollow noopener" href="https://github.com/adametry/gulp-eslint">gulp-eslint</a> プラグインを使用します。</p> <p>tasks/scripts.js</p> <pre><code class="js">import gulp from 'gulp'; import gulpIf from 'gulp-if'; import plumber from 'gulp-plumber'; import gulpEslint from 'gulp-eslint'; import webpack from 'webpack'; import gulpWebpack from 'webpack-stream'; import changed from 'gulp-changed-in-place'; import { scripts as config, isProd } from './config'; export function esTranspile() { return gulp .src(config.src) .pipe(plumber()) // gulp.watch と一緒に使う場合は第二引数が必須らしい。 // https://www.npmjs.com/package/webpack-stream#usage-with-gulp-watch .pipe(gulpWebpack(require('../webpack.config.js'), webpack)) .pipe(gulp.dest(config.dest)); } export function esLint() { return gulp .src(config.src) .pipe(changed({ firstPass: true })) .pipe(gulpEslint()) .pipe(gulpEslint.format()) .pipe(gulpIf(isProd, gulpEslint.failAfterError())); } export const scripts = gulp.series(esLint, esTranspile); </code></pre> <p>Webpack の設定ファイルです。</p> <p>webpack.config.js</p> <pre><code class="js">import { scripts as config } from './tasks/config'; module.exports = { mode: process.env.NODE_ENV ? 'production' : 'development', entry: { app: `./${config.srcRoot}/app.js` }, module: { rules: [ { test: /\.js$/, use: 'babel-loader' } ] }, output: { filename: '[name].js', }, devtool: 'source-map' } </code></pre> <p>複数のファイルを出力したい場合は、以下のように <code>entry</code> に追記すれば OK です。</p> <pre><code class="js">entry: { app: `./${config.srcRoot}/app.js`, foo: `./${config.srcRoot}/foo.js` }, </code></pre> <p>こちらはESLint の設定ファイルです。</p> <p>.eslintrc</p> <pre><code class="json">{ "extends": ["eslint:recommended"], "parserOptions": { "ecmaVersion": 6, "sourceType": "module" }, "env": { "browser": true, "node": true } } </code></pre> <h3 id="フォーマッタ"><a href="#%E3%83%95%E3%82%A9%E3%83%BC%E3%83%9E%E3%83%83%E3%82%BF">フォーマッタ</a></h3> <p>フォーマッタはオマケです。お好みの設定&エディタと連携していただければ。</p> <h4 id="EditorConfig"><a href="#EditorConfig">EditorConfig</a></h4> <p>.editorconfig</p> <pre><code class="ini">root = true [*] end_of_line = lf insert_final_newline = true [*.{html,edge,scss,css,js,svg}] indent_style = space indent_size = 2 </code></pre> <h4 id="Prettier"><a href="#Prettier">Prettier</a></h4> <p>.prettierrc</p> <pre><code class="json">{ "singleQuote": true } </code></pre> <p>以上です。参考になれば幸いです。</p> Masahiro Harada