Madogiwa Blog

主に技術系の学習メモに使っていきます。

ActiveRecord::QueryMethods#whereで発行されるwhereの部分だけのSQLを取得するMEMO

はじめに

下記のような形でActiveRecordが発行するSQLからWHERE句の部分のSQLだけを取得したいケースがあり、色々やり方とか調べたのでMEMO✍

やりたかったこと

今回やりたかったのは、下記のようなscopeを定義していたときにto_sqlするとSQL全体を取得することが出来ますが、

class Book < ApplicationReacord
  scope :viewable, ->(now = Time.current) { where(published_at: now..) }
end

Book.viewable.to_sql
#=> select * from books where published_at > '2020-05-23 15:24:34'

そうではなくWHERE句のpublished_at > '2020-05-23 15:24:34'部分だけ取得したいというものです。

where部分だけのSQLを取得する方法

下記のような感じで取れるみたいです👀(⚠nodocなので動作保証されてないですが・・・!)

Book.viewable.values[:where].ast.to_sql
#=> published_at > '2020-05-23 15:24:34'

values[:where]ActiveRecord::Relation::WhereClauseのオブジェクトを取得することができて、オブジェクトに対してastメソッドを呼び出すことによってArel::Nodes系のオブジェクトを取得できるようです👀

そして取得したオブジェクトにto_sqlを呼び出すことによってそのNodeの部分だけのSQLを取得できるような形で動いてそうな気がしました・・・!

WHEREも含めて取得したいときは下記のようにするととれるようです👀(⚠こちらもnodocなので動作保証されてないです。)

Book.viewable.arel.where_sql
#=> WHERE published_at > '2020-05-23 15:24:34'

ですが、Arel::Nodes::Node#to_sqlはコメントにも無くなる可能性が明記されてるので、素直にSQLをparseした方が良さそうな気もしました😓

      # FIXME: this method should go away.  I don't like people calling
      # to_sql on non-head nodes.  This forces us to walk the AST until we
      # can find a node that has a "relation" member.
      #
      # Maybe we should just use `Table.engine`?  :'(
      def to_sql(engine = Table.engine)
        collector = Arel::Collectors::SQLString.new
        collector = engine.connection.visitor.accept self, collector
        collector.value
      end

rails/node.rb at b1f6d8c8d8ad3e2e5b96e95b455c70f2c895ce14 · rails/rails · GitHub

参考

stackoverflow.com

JavaScriptでhidden等の画面に表示されてない任意の文字列をクリップボードにコピーさせる方法

JavaScriptクリップボードにコピーさせる方法として、select()して、document.execCommand("copy")を呼び出すことで、要素を選択してクリップボードへのコピーコマンドを呼び出す方法を採用することが多いと思うのですが、

async function clipboardCopy(selector) {
  const target = document.querySelector(selector);
  target.select();
  document.execCommand("copy");
}

この方法では明示的に画面上の項目を選択してコピーを行う都合上、hiddenタグの値等、画面に表示されていない値を取得することはできません。

そういうときにはnavigator.clipboard.writeTexを使うと任意の文字列をコピーさせることができます。

developer.mozilla.org

サンプルはこんな感じです。

async function clipboardCopy(text) {
  await navigator.clipboard.writeText(text);
}

clipboardCopy("コピー対象の文字列")

Clipboard API、今まで知らなかったのですが便利ですね✨

参考

qiita.com

developer.mozilla.org

Webpackでentryとentryを読み込むhtmlを動的に設定するMEMO

Webpackを使って複数entryを指定して、それらを読み込むhtmlをhtml-webpack-pluginを使って用意すると下記のような感じの設定になるかと思うのですが、 これだとfileが増えるたびにwebpack.config.jsを変更しないといけないので不便です。。。

module.exports = {
  entry: {
    index: './src/entries/index.js',
    home: './src/entries/home.js',
  },
  plugins: [
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: './src/pages/index.html',
      chunks: [index]
    }),
    new HtmlWebpackPlugin({
      filename: 'home.html',
      template: './src/pages/home.html',
      chunks: [home]
    })
  ]
}

今回はそれに対応するためにentryとentryを読み込むhtmlを動的に設定する方法を調べて実装してみたので、そのへんもMEMOしておきます。

entries配下のファイルをentryに指定する

方針としては、下記のような形を考えてみました。

  • 指定ディレクトリに配置されたjs/tsファイルを取得
  • 拡張子を除外したファイル名をkey、ファイルパスをvalとしたobjectを作成
  • それをentryに設定

そういうわけで指定ディレクトリに配置されたjs/tsファイルを取得して、 拡張子を除外したファイル名をkey、ファイルパスをvalとしたobjectを作成するmoduleを作ってみました。

const glob = require('glob');
const path = require('path');

/**
 * 指定したentryのルートディレクトリ配下のjsまたはtsファイルのファイル名とパスのobjectを取得
 * 例) /entries/foo.ts => { foo: "/entries/foo.ts" }
 * ※ルートディレクトリ配下にディレクトリを切り、そこにjs,tsファイルをおいた場合は無視される。
 * @param {string} entryRoot entryのルートディレクトリ
 */
module.exports = function getEntries(entryRoot) {
  let ret = {}
  const filePaths = glob.sync(`${entryRoot}/*.{js,ts}`)
  filePaths.forEach(filePath => { ret[path.basename(filePath, path.extname(filePath))] = filePath })
  return ret
}

このmoduleを使って下記のようにしてあげると特定のディレクトリ配下のファイルを自動的にentryとして指定できます💪

const getEntries = require('./config/webpack/utils/getEntries.js')
const entries = getEntries('./src/entries/')

module.exports = {
  entry: entries,
  plugins: [
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: './src/pages/index.html',
      chunks: [index]
    }),
    new HtmlWebpackPlugin({
      filename: 'home.html',
      template: './src/pages/home.html',
      chunks: [home]
    })
  ]
}

entryとしたファイルと同名のhtmlファイルをtemplateとして指定する

方針としては、下記のような形を考えてみました。

  • entryとテンプレートのhtmlを配置しているディレクトリのパスを引数で取る
  • ディレクトリ配下のentryと同名のファイルとentry名を紐付けるようにHtmlWebpackPluginのインスタンスを生成して配列にして返却
  • 生成した配列をpluginsに展開して設定

そういうわけでentryとテンプレートのhtmlを配置しているディレクトリのパスを引数で取って、ディレクトリ配下のentryと同名のファイルとentry名を紐付けるようにHtmlWebpackPluginのインスタンスを生成して配列にして返却するmoduleを作ってみました。

const HtmlWebpackPlugin = require('html-webpack-plugin')

/**
 * 指定したentriesと同名のhtmlテンプレートを読み込みHtmlWebpackPluginを生成する
 * ※ルートディレクトリ配下にディレクトリを切り、そこにhtmlファイルをおいた場合は無視される。
 * @param {object} entries { foo: "/entries/foo.ts" }のようなentryを表すオブジェクト
 * @param {string} templateRootPath entryのjsを読み込むtemplateファイル(html)のRootパス
 */
module.exports = function buildHtmlWebpackPlugins(entries, templateRootPath) {
  return Object.keys(entries, templateRootPath).map(
    entryName => new HtmlWebpackPlugin({
      filename: `${entryName}.html`,
      template: `${templateRootPath}${entryName}.html`,
      chunks: [entryName]
    })
  )
}

このmoduleを使って下記のようにしてあげると指定ディレクトリのhtmlファイルをentryと紐付けることが出来ます🤝

const getEntries = require('./config/webpack/utils/getEntries.js')
const entries = getEntries('./src/entries/')

module.exports = {
  entry: entries,
  plugins: [
    ...buildHtmlWebpackPlugins(entries, './src/templates)
  ]
}

おわりに

今回はWebpackで動的に色々やる方法を調べて、自分なりに方法を考えてみました。これが正しいかはわからないですが参考になれば🙏

参考

moneyforward.com

nodejs.org

github.com

developer.mozilla.org

Vue.jsでVue.extendを使ってTypeScriptで書くときのMEMO

Vue.jsをTypeScriptで書くときには、vue-class-componentvue-propaty-decoratorを使うような情報ふが多いですが、Vue.extendを使うとそれらを使わないピュアなVue.jsでもTypeScriptを使って書くことが出来ます。

jp.vuejs.org

そのへんの書き方とかを、あまり理解出来てなかったのでMEMOしておきます✍

基本的な書き方

下記にvue-class-componentのOverviewにあったカウンターをちょっと改変してVue.extendを使って書き直したものを記載しました

class-component.vuejs.org

vue-class-componentだと素のVue.jsのコンポーネントとかなり書き方が違ってしまうのですが、

<template>
  <div>
    <button @click="decrement(1)">-</button>
    {{ count }}
    <button @click="increment(1)">+</button>
  </div>
</template>
<script lang="ts">
import Vue from 'vue'

interface Data {
  count: number
}

export default Vue.extend({
  props: {
    initCount: Number
  },
  data() : Data {
    return {
      count: this.initCount | 0
    }
  },
  methods: {
    decrement(num: number) {
      this.count -= num;
    },
    increment(num: number) {
      this.count += num
    }
  }
})
</script>

ちょっとDataの型指定が癖がある感じがしますが素のVue.jsのコンポーネントライクで書けるので読みやすいですね👍

Propsを独自で定義したObjectで受け取るケースは下記のような形になるようです👀
※下記は受け取ったコメントのオブジェクトをそのまま表示するようなコンポーネントです

<template>
  <div class="comment" style="border: solid; margin: 5px;">
    <p class="attribute">{{comment.id}}</p>
    <p class="attribute">{{comment.body}}</p>
  </div>
</template>
<script lang="ts">
import Vue, { PropType } from 'vue'
import { Comment } from '../types/types'

export default Vue.extend({
  props: {
    comment: Object as PropType<Comment>
  }
})
</script>

PropTypeはVue.jsのAPIです。 https://github.com/vuejs/vue/issues/6850

Vue.jsのpropsは内部的にはObjectそのものではなくObjectのコンストラクタが渡されており、 TypeScriptの型情報はObjectのコンストラクタの実行時には存在しないのでエラーになってしまうようです。(あまりわかってない)

frontendsociety.com

そのため今までは関数として定義していたようですが、それがわかりにくいということでPropTypeが追加された経緯のようです。

<template>
  <div class="comment" style="border: solid; margin: 5px;">
    <p class="attribute">{{comment.id}}</p>
    <p class="attribute">{{comment.body}}</p>
  </div>
</template>
<script lang="ts">
import Vue, { PropType } from 'vue'
import { Comment } from '../types/types'

export default Vue.extend({
  props: {
    comment: Object as (() => Comment)
  }
})
</script>

参考

qiita.com

count0.org

Webpackで構築したTypeScript + Vue環境で`Uncaught TypeError: Cannot set property 'render' of undefined` が発生する

事象

WebpackでTypeScript + Vue.jsの環境構築時にlang=tsを指定したComponentを.tsファイルでimportしたときに下記のエラーが発生し、コンポーネントを読み込むことが出来なかった😢

componentNormalizer.js:26 Uncaught TypeError: Cannot set property 'render' of undefined

そのときのエントリーファイル

import Vue from "vue"
import TsCounter from "../components/tsCounter.vue"

new Vue({
  el: "#vue-root",
  components: { TsCounter },
})

Vueコンポーネント

<template>
  <div>
    <button v-on:click="decrement">-</button>
    {{ count }}
    <button v-on:click="increment">+</button>
  </div>
</template>
<script lang="ts">
import Vue from 'vue'
import Component from 'vue-class-component'

// Define the component in class-style
@Component
export default class TsCounter extends Vue {
  // Class properties will be component data
  count:number = 0

  // Methods will be component methods
  increment() {
    this.count++
  }

  decrement() {
    this.count--
  }
}
</script>

解決方法

原因としてはts-loaderでtest: /\.ts/,を指定しているので.tsファイルしかtypescriptとして扱われない。そのため.vuescpript内のコードがtypescriptとして認識されず、必要なpropatyが定義されなかったという感じのようです😓

そのためts-loaderのoptionsにappendTsSuffixTo: [/\.vue/]を追加して、.vueファイルもts-loaderの対象にしたところエラーが解消しました🥳

module.exports = {
  // 省略
  module: {
    rules: [
      {
        test: /\.ts/,
        exclude: /(node_modules|bower_components)/,
        loader: "ts-loader",
        options: {
          // /vueをtypescriptとして扱う
          appendTsSuffixTo: [/\.vue/]  // NOTE: この設定を追加
        }
      },
     // 省略

公式のREADMEのちゃんと書いてありましたね😅

github.com

参考

qiita.com

Webpackを色々触ってみたので基本的な使い方とかをMEMO🕸📦

RailsにはWebpackerとか設定をいい感じにやってくれるものがありますが、 最近脱WebpackerしてWebpackをそのまま使ったり、Simpackerを導入したりするような話を耳にすることが多くなってきました。

inside.pixiv.blog

github.com

そんな中で、Webpackが何もわかってなかったので今回は下記の記事(とてもわかりやすかった🙏✨)を参考にWebpackに入門して、いろいろイジってみたのでMEMOしておきます。

ics.media

Webpackとは?

webpack is a module bundler. Its main purpose is to bundle JavaScript files for usage in a browser, yet it is also capable of transforming, bundling, or packaging just about any resource or asset. https://github.com/webpack/webpack

Webpackとはモジュールバンドラーです。jsだけでなくcssやimage等もバンドルすることが出来ます。

複数に分割されたファイルを1つにまとめることでリクエスト数を削減でき、パフォーマンスの向上が見込めます。

またpluginを用いてbabel等をあわせて実行することができるまた、フロントエンドまわりのタスクを一本化できるところも魅力のようです👀

Webpackの導入方法

導入には、まず下記のコマンドを実行します。

npm i -D webpack webpack-cli

次にバンドルするファイルを作成します。Webpackはデフォルだと下記のような設定でバンドルするので、それぞれ作成します。

項目 内容
entry src/index.js
output dist/main.js

※entry: バンドルする対象のファイル、output: バンドル後の出力先

あとはpackage.jsonのscriptsにビルド用のコマンドを追加してあげて、

  "scripts": {
    "build": "webpack",

npm run buildで実行出来ます📦✨

$ npm run build
Hash: 9ca4e2bf9b0dda113944
Version: webpack 4.42.1
Time: 190ms
Built at: 2020-04-19 12:23:29
  Asset       Size  Chunks             Chunk Names
main.js  952 bytes       0  [emitted]  main
Entrypoint main = main.js
[0] ./src/index.js 131 bytes {0} [built]

Webpackをカスタマイズする

Webpackをカスタマイズするには、webpack.config.jsを作成してそこに値を記載していきます。

例えばbuildモードをdevelopmentに変更する場合は下記のようにします。

module.exports = {
  // ビルド時のモード
  // development: ビルド時間が短くソースマップに対応しているが容量の圧縮がかからない。
  // production: 本番用build、コメント削除等の圧縮がかかり容量が少ない(デフォルト)
  mode: "development"
}

他にも色々な項目が用意されているので詳しくはこちら👇

webpack.js.org

サンプル集

ちょっと個人的に色々設定をいじってみたので、サンプル集として載せておきます!

webpack-dev-serverを使う

毎回ファイルを編集したときにnpm run buildを実行するのは面倒くさいですし、全量ビルドが走るので効率がよくないです。そういう場合にはwebpack-dev-serverが便利です。(webpack --watchでも良いですが)

github.com

npm i -D webpack-dev-serverを実行してwebpack-dev-serverをinstall

下記のようにwebpack-dev-server用の設定をwebpack.config.jsに追加

module.exports = {
  // ビルド時のモード
  // development: ビルド時間が短くソースマップに対応しているが容量の圧縮がかからない。
  // production: 本番用build、コメント削除等の圧縮がかかり容量が少ない(デフォルト)
  mode: "development",
  // webpack-dev-server用の設定
  devServer: {
    // webpackのoutput pathを指定
    contentBase: `${__dirname}/dist`,
    // 実行時にブラウザを開く
    open: true
  }

scriptsにも起動用のコマンドを追加して、

  "scripts": {
    "build": "webpack",
    "build-dev-server": "webpack-dev-server",

npm run build-dev-serverを実行すると、Webpackビルド用のサーバーが立ち上げって自動的に差分ビルドが走るようになります🤖

ℹ 「wds」: Project is running at http://localhost:8080/
ℹ 「wds」: webpack output is served from /
ℹ 「wds」: Content not from webpack is served from /Users/morita.jun/Documents/repo/javascript/webpack_study/dist
ℹ 「wdm」: wait until bundle finished: /
ℹ 「wdm」: Hash: 560347cd3ca9f479cd19
Version: webpack 4.42.1
Time: 1816ms
Built at: 2020-04-19 12:33:53

ダイジェストを付与してバンドルする

webpackでバンドルしたときに毎回同じファイル名で出力してしまうとブラウザキャッシュにより、 うまく変更が反映されないといった問題が発生します。

なので出力時のファイル名にhashを付与するように設定を変更してみます。

やり方は簡単でoutputfilenameの中で[hash]をつけてあげるだけです。

module.exports = {
  // ビルド時のモード
  // development: ビルド時間が短くソースマップに対応しているが容量の圧縮がかからない。
  // production: 本番用build、コメント削除等の圧縮がかかり容量が少ない(デフォルト)
  mode: "development",
  // 出力設定
  // デフォルトはdist/main.js
  output: {
    // 出力先のディレクトリ
    path: `${__dirname}/dist`,
    // 出力先のファイル
    filename: 'main-[hash].js'
  },
  // webpack-dev-server用の設定
  devServer: {
    // webpackのoutput pathを指定
    contentBase: `${__dirname}/dist`,
    // 実行時にブラウザを開く
    open: true
  }
}

この設定で実行するとmain-b562cb2621248a80cacb.jsのようにdigestを付与することが出来ます。

しかし、digestを付与してあげるとhtmlでスクリプトを読み込むときに毎回srcを変更しないといけないのが不便です。

そんなときは、html-webpack-pluginが便利です。

github.com

npm install -D html-webpack-pluginで導入して下記のように設定すると自動的にsrcを指定してくれます。

const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  // ビルド時のモード
  // development: ビルド時間が短くソースマップに対応しているが容量の圧縮がかからない。
  // production: 本番用build、コメント削除等の圧縮がかかり容量が少ない(デフォルト)
  mode: "development",
  // 出力設定
  // デフォルトはdist/main.js
  output: {
    // 出力先のディレクトリ
    path: `${__dirname}/dist`,
    // 出力先のファイル
    filename: 'main-[hash].js'
  },
  // webpack-dev-server用の設定
  devServer: {
    // webpackのoutput pathを指定
    contentBase: `${__dirname}/dist`,
    // 実行時にブラウザを開く
    open: true
  },
  plugins: [
    // distのHTMLを自動生成するplugin、digest等をよしなに対応してくれる
    new HtmlWebpackPlugin(),
  ]
}

下記のように良い感じにsrcを設定してくれます👍

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Webpack App</title>
  <meta name="viewport" content="width=device-width, initial-scale=1"></head>
  <body>
  <script src="main-c6653b46c86daeb1df0d.js"></script></body>
</html>

entryを複数用意してentry毎にバンドルする

デフォルトの設定だとentryもoutputも一つのファイルなので、 実際のプロジェクトだとentry/outputが肥大化して、初回のリクエストに時間が掛かる等が問題になることもあるかと思います。

webpackのentryとoutputの設定を変更することで、entryとなるファイルと出力先を複数ファイルにして、 ファイルの肥大化を抑えることが出来ます。

下記の設定では、entryをsrc/index.jssrc/home.jsとして、そのentryファイル毎に出力するようにしています。※[name]を使うことでentryのファイル名を取得できる。

またHtmlWebpackPluginのオブジェクトをentry単位に用意してそれぞれを読み込むようにしています📦

const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  // ビルド時のモード
  // development: ビルド時間が短くソースマップに対応しているが容量の圧縮がかからない。
  // production: 本番用build、コメント削除等の圧縮がかかり容量が少ない(デフォルト)
  mode: "development",
  // jsのエントリーポイント
  // デフォルトはsrc/index.js
  entry: {
    main: './src/index.js',
    home: './src/home.js'
  },
  // 出力設定
  // デフォルトはdist/main.js
  output: {
    // 出力先のディレクトリ
    path: `${__dirname}/dist`,
    // 出力先のファイル
    filename: '[name]-[hash].js'
  },
  // webpack-dev-server用の設定
  devServer: {
    // webpackのoutput pathを指定
    contentBase: `${__dirname}/dist`,
    // 実行時にブラウザを開く
    open: true
  },
  plugins: [
    // 個別のbuild後のjsを読み込むHTMLを生成
    new HtmlWebpackPlugin({
      filename: 'main.html',
      chunks: ['main']
    }),
    new HtmlWebpackPlugin({
      filename: 'home.html',
      chunks: ['home']
    })
  ]
}

cssをバンドルする

RailsだとcssのビルドはSproketsで行うことが多いですが、Webpackでもcssのバンドルを行うことが出来ます。

Webpackでcssのビルドを行うためにはstyle-loadercss-loaderが必要です。

github.com

github.com

下記でinstallします。

npm -D style-loader css-loader

設定は下記のような形です。moduleruleに適用したいloader(今回だとstyle-loadercss-loader)の設定を追加します。

const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  // ビルド時のモード
  // development: ビルド時間が短くソースマップに対応しているが容量の圧縮がかからない。
  // production: 本番用build、コメント削除等の圧縮がかかり容量が少ない(デフォルト)
  mode: "development",
  // jsのエントリーポイント
  // デフォルトはsrc/index.js
  entry: {
    main: './src/index.js',
    home: './src/home.js'
  },
  // 出力設定
  // デフォルトはdist/main.js
  output: {
    // 出力先のディレクトリ
    path: `${__dirname}/dist`,
    // 出力先のファイル
    filename: '[name]-[hash].js'
  },
  // webpack-dev-server用の設定
  devServer: {
    // webpackのoutput pathを指定
    contentBase: `${__dirname}/dist`,
    // 実行時にブラウザを開く
    open: true
  },
  module: {
    rules: [
      // stylesheetのbuild用の設定
      {
        // 対象のファイル
        test: /\.css/,
        // 使用するloader
        use: [
          "style-loader",
          "css-loader"
        ]
      }
    ]
  },
  plugins: [
    // 個別のbuild後のjsを読み込むHTMLを生成
    new HtmlWebpackPlugin({
      filename: 'main.html',
      chunks: ['main']
    }),
    new HtmlWebpackPlugin({
      filename: 'home.html',
      chunks: ['home']
    })
  ]
}

これでcssのbundleが行われるようになりました🙌 ※実際は下記のようにjs内でimportして使うので、jsファイルとしてバンドルされ動的にhtmlに反映されます。

// src/index.js
import "./styles/index.css"

バンドル時にbabelを実行する

リアルワールドではIE11といったレガシィブラウザを扱うことも多いので、babelを通す必要があるケースが多いです。

Webpackにはbabel用のプラグインも用意されていて比較的簡単にbabelを実行してビルドすることが出来ます。

github.com

まずは必要なものをinstallします。

npm install -D babel-loader @babel/core @babel/preset-env
  • babel-loader : Webpack用のloader
  • @babel/core : babelの本体
  • @babel/preset-env : Babelのプラグインを自動判定してよしなにしてくれるやつ

その後は下記のようにbabel用の設定を追加します。

const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  // ビルド時のモード
  // development: ビルド時間が短くソースマップに対応しているが容量の圧縮がかからない。
  // production: 本番用build、コメント削除等の圧縮がかかり容量が少ない(デフォルト)
  mode: "development",
  // jsのエントリーポイント
  // デフォルトはsrc/index.js
  entry: {
    main: './src/index.js',
    home: './src/home.js'
  },
  // 出力設定
  // デフォルトはdist/main.js
  output: {
    // 出力先のディレクトリ
    path: `${__dirname}/dist`,
    // 出力先のファイル
    filename: '[name]-[hash].js'
  },
  // webpack-dev-server用の設定
  devServer: {
    // webpackのoutput pathを指定
    contentBase: `${__dirname}/dist`,
    // 実行時にブラウザを開く
    open: true
  },
  module: {
    rules: [
      // stylesheetのbuild用の設定
      {
        // 対象のファイル
        test: /\.css/,
        // 使用するloader
        use: [
          "style-loader",
          "css-loader"
        ]
      },
      // babelのbuild用の設定
      {
        // 対象のファイル
        test: /\.m?js$/,
        // 対象外のファイル
        exclude: /(node_modules|bower_components)/,
        // 使用するloader
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env']
          }
        }
      }
    ]
  },
  plugins: [
    // 個別のbuild後のjsを読み込むHTMLを生成
    new HtmlWebpackPlugin({
      filename: 'main.html',
      chunks: ['main']
    }),
    new HtmlWebpackPlugin({
      filename: 'home.html',
      chunks: ['home']
    })
  ]
}

これでbabelも合わせて実行されるようになりました🗼

おわりに

今回はwebpackを色々触ってみました。今まで触ったことがなかったので何もわからなかったのですが、単純なことをやるだけであれば既存のライブラリが充実しているので意外と設定出来そうな感じがしました(便利✨)

参考

qiita.com

ics.media

E2EテストでJavaScript上の時間を固定するgem `travel_to_javascript`を作りました💎✨

JavaScriptの処理で時間に依存してたりする実装を行っているとE2Eのときに時間を固定したいときが稀によくあるのですが、 travel_toを使っても結局はサーバーサイド上の時刻しか固定出来ません😢

というわけでJavaScript上の時刻を固定するtravel_to_javascriptというgemを作ってみました💎

github.com

中身は前に記事にしたHelperです😅

madogiwa0124.hatenablog.com

使い方

まずは下記のような形でGemfileに追記してbundle installしてください📦

gem 'travel_to_javascript'

あとは任意のfeature spec内でrequire 'travel_to_javascript'を実行してinclude TravelToJavascriptしてください。

そうするとtravel_to_javascriptが使用出来るようになり、Capybara::Sessionのオブジェクトと固定したい時間を引数で渡すと、 block内のJavaScript上の現在時刻を引数で渡した時間で固定出来ます🕛

Rspecのサンプルは下記のような感じです。

require 'spec_helper'
require 'travel_to_javascript'

RSpec.describe 'SampleFeatureSpec', type: :feature do
  include TravelToJavascript

  it 'sample spec' do
   # NOTE: Use a JavaScript enabled driver.
    page = Capybara::Session.new(:headless_chrome, TestApp)
    travel_to_javascript(page, DateTime.parse('2000-01-01 1:11:11.111+9:00')) do
      page.execute_script('console.error(Date.now(), new Date())')
      pp page.driver.browser.manage.logs.get(:browser).map(&:message)
      # locks time by args in block.
      # => ["console-api 2:32 946656671111 Sat Jan 01 2000 01:11:11 GMT+0900"]
    end
    page.execute_script('console.error(Date.now(), new Date())')
    pp page.driver.browser.manage.logs.get(:browser).map(&:message)
    # restore time outside block.
    # => ["console-api 2:32 1586652460142 Sun Apr 12 2020 09:47:40 GMT+0900"]
  end
end

Minitestでも使えます。

require 'test_helper'
require 'travel_to_javascript'

class SampleFeatureTest < Minitest::Test
  include TravelToJavascript

  def test_sample
     # NOTE: Use a JavaScript enabled driver.
    page = Capybara::Session.new(:headless_chrome, TestApp)
    travel_to_javascript(page, DateTime.parse('2000-01-01 1:11:11.111+9:00')) do
      page.execute_script('console.error(Date.now(), new Date())')
      pp page.driver.browser.manage.logs.get(:browser).map(&:message)
      # locks time by args in block.
      # => ["console-api 2:32 946656671111 Sat Jan 01 2000 01:11:11 GMT+0900"]
    end
    page.execute_script('console.error(Date.now(), new Date())')
    pp page.driver.browser.manage.logs.get(:browser).map(&:message)
    # restore time outside block.
    # => ["console-api 2:32 1586652460142 Sun Apr 12 2020 09:47:40 GMT+0900"]
  end
end

仕組み

このGemの仕組みは渡された時間をiso8601形式に変換してJavaScript上のDateDate.nowを渡された時間を返すようにオーバーライドします。 ※このときにJavaScript上に引数を渡していた場合は固定せずに、そのままの時間を返すようにしています。

その後yieldでblockに渡された処理を実行して、時間を元に戻しています🕛

  def travel_to_javascript(page, datetime)
    page.execute_script time_stop_javascript(datetime)
    yield
    page.execute_script time_undo_javsctipt
  end

  def time_stop_javascript(rb_datetime)
    <<~JS
      originDate = Date;
      Date = #{time_stop_js_function_for_date(rb_datetime)};
      Date.now = #{time_stop_js_function_for_date_now(rb_datetime)};
    JS
  end

  def time_stop_js_function_for_date(rb_datetime)
    <<~JS
      function (datetime) {
        if (datetime) {
          return new originDate(datetime);
        } else {
          return new originDate("#{rb_datetime.iso8601(6)}");
        }
      }
    JS
  end

  def time_stop_js_function_for_date_now(rb_datetime)
    <<~JS
      function (datetime) {
        if (datetime) {
          return new originDate(datetime).getTime();
        } else {
          return new originDate("#{rb_datetime.iso8601(6)}").getTime();
        }
      }
    JS

travel_to_javascript/travel_to_javascript.rb at master · Madogiwa0124/travel_to_javascript · GitHub

出来ないこと

下記のような場合にはこのgemだと対応出来ません😢

  • travel_to_javascriptの中でvisit等をしてページ跨ぎで時間を止めるようなことはページがリロードされたタイミングでJavaScriptもリロードされてしまうので出来ません。

  • ページ読み込み時にJavaScript側で判定するような場合にはexecute_scriptの実行前に判定が行われる可能性があるので効かなそうです。

おわりに

今回は以前に作ったHelperをgemとして使いやすくしてみました💎対応出来るケースはちょっと限られているかもですが、JavaScript上で時間を固定したいときには使えるかもしれないです🙇‍♂️