かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

Ruby Class を case 式で判定しようとしてハマったメモ。

Ruby は深く触ってないので初学者マンです。 Rails の複数のクラスで共通して使われている Decorator 内で case 式でクラスを判定して返す値を変えていた部分で self.class.name の文字列で判定するのではなく self.class で判定すればいいよ。とアドバイスをもらったのですが上手く動作させられなかったのでメモ

case 式だと .class だと上手く比較できない

class A
end
 
def class_type_check a
  p "a.class is #{a.class}"
  # => a.class is A"

  case a.class.name
  when 'A' then p "A"
  when 'B' then p "B"
  end
  # => "A"

  case a.class
  when A then p "A"
  when B then p "B"
  end
  # => 
end

class_type_check_with_case A.new

class_type_check に渡されるクラスオブジェクトaa.classA と表示されます。
クラス名を文字列にした a.class.name で比較する case 式では当然 when "A" が該当となります。
しかし、a.class で比較した case 式では a.classA であるにも関わらず when A の部分が該当となっていませんでした🤔

a.class ではなくa だと上手く動作しました。

class A
end
 
def class_type_check a
  case a
  when A then p "A"
  when B then p "B"
  end
  # => "A"
end

class_type_check_with_case A.new

なーぜー? 🤔🤔🤔
a.class で判定する場合 a.class はAクラスのインスタンスで A そのものではないからオブジェクトIDとかが違って同一オブジェクトではないとかなのでしょうか???

case 式は === で判定している

case は一つの式に対する一致判定による分岐を行います。when 節で指定された値と最初の式を評価した結果とを演算子 === を用いて 比較して、一致する場合には when 節の本体を評価します。
つまり、

case 式0
when 式1, 式2
  stmt1
when 式3, 式4
  stmt2
else
  stmt3
end
は以下の if 式とほぼ等価です。
tmp = 式0
if 式1 === tmp or 式2 === tmp
  stmt1
elsif 式3 === tmp or 式4 === _tmp
  stmt2
else
  stmt3
end

ref. 制御構造 (Ruby 2.6.0)

. (はてなブログ、引用の間に空白以外の何か挟まないと引用を連続して書けないのか…

case式は===演算子を使ったif〜elsif〜end式と等価な処理を行うように実装されている
基本的にこの===演算子は、サブクラスで再定義されていない限り、単にObject#==を呼び出すだけ
ref. Rubyのcase式と===演算子について - しばそんノート

Rubyの case 式は if whenの条件 === case の比較する値 と等価で、 === JavaScriptとかと違ってより厳密に型とかチェックする演算子ではなく、左辺のオブジェクトで定義されている === を呼び出すというものという事っぽい。

上の例を if文に置き換えてみるとこんな感じ

a = A.new
p a.class
# => A
if A === a.class
  p "A"
else
  p false
end
# => false

class クラス=== で判定する結果が返されるということっぽい。
class クラスの === は親クラスの Module クラスで定義されている。

self === obj -> bool
指定された objself かそのサブクラスのインスタンスであるとき真を返します。 また、objself をインクルードしたクラスかそのサブクラスのインスタンスである場合にも 真を返します。上記のいずれでもない場合に false を返します。
言い替えると obj.kind_of?(self)true の場合、 true を返します。
ref. class Module self === obj -> bool

a.class == A なら true だけど、 === の場合は a.class.kind_of?(A)という判定になる。 a.classAは Aクラスそのもの(self)ではないし、a.classAは Aのサブクラスのインスタンスでもないから false ということっぽい。

なので case a としている場合は

a = A.new
if A === a
  p "A"
else
  p false
end
# => "A"

a は Aクラスのインスタンスだから true ということになるっぽい。 (ちゃんと理解している訳ではない

case a.class が当てはまる例

a.classA は Class クラスのオブジェクトなので、case a.class は Class クラスより親のクラスであれば true になる 。

class A
end

a = A.new
# XXX === a.class は a.class.kind_of?(XXX) と等価なので
a.class.kind_of?(Class)
# => true
a.class.kind_of?(Module)
# => true
a.class.kind_of?(Object)
# => true

👇

def class_type_check a
  case n.class
  when A then p "A"
  when B then p "B"
  when String then p "String"
  when Class then p "Class"
  when Module then p "Module"
  end
  # => "Class"
end

class_type_check A.new

完 全 理 解 !

ポエム

JavaScriptの不思議な挙動試してみるのとか好きだったし、こういう言語の挙動みたいな部分面白いからまたちゃんとRuby再入門しようかな〜って気持ちになってきた。(不思議な挙動を抑制されるTypeScriptを枷のように感じてるし、今の所まだ Rails の良さを感じられるに至ってい。disるつもりはなくってnot for meって感じなのだ。こんなんだから僕は稼げるエンジニアにはなれないのだろう…


[参考]

改訂2版 パーフェクトRuby

改訂2版 パーフェクトRuby

sass-lint と scss-lint で lint から除外する場合の書き方が微妙に違ってた

理由があって lint を無視させたい時の書き方が sass-lint と scss-lint で微妙に違う。
sass-lint だと思ってたらプロダクトで使ってるのが scss-lint で ignore できてたなかったので自戒を込めてメモ。

ブロック内だけ lint を無視させる

sass-lint sass-lint:disable-block キーワード

p {
  // sass-lint:disable-block border-zero
  border: none; // No result reported
}

a {
  border: none; // Failing result reported
}

scss-lint scss-lint:disable キーワード

p {
  // scss-lint:disable BorderZero
  border: none; // No lint reported
}

a {
  border: none; // Lint reported
}

一行だけ lint を無視させる

sass-lint sass-lint:disable-line 又は sass-lint:disable-next-line キーワード

p {
  border: none; // sass-lint:disable-line border-zero
}
// 又は
p {
  // sass-lint:disable-next-line border-zero
  border: none;
}

scss-lint scss-lint:disable でOFFにして scss-lint:enable でONにする

p {
  // scss-lint:disable BorderZero
  border: none; // No lint reported
  // scss-lint:enable BorderZero
}

調べてて開発元が違ってたことに気づいた。書き方が異なるのはまぁそりゃそうかという感じ。 lint の設定のキーワードも微妙に違うし…
scss-lint の方は全部 scss-lint:disable で済むっぽいのでこっちの方が覚えることが少なくて楽そう。

SCSS {}; 付け忘れたらエラーになるしで余り好きじゃないんだけど、{} ないと prettier が使えないとかなんとかで SCSS 記法が見直されてるみたいな話を聴いた…
僕は Stylus で戦える方法を探すよ… Stylus 好きだし


👆 これ stylus で prettier っぽいことできそうでない?

Webpack Babel + Pug + Stylus で ホットリロードできるSPA開発環境作った。

react-create-app とかボイラーテンプレートでサクッとできる時代だけど、自分で作ってみて学ぼうと思ったので、秘伝のタレ作ってた 🍲

構成
/build ... ビルドされたファイルが出力される
/src
  |- /stylus
  |     |- App.styl
  |- index.js
  |- index.pug
package.json

環境

@babel/core: 7.4.5
webpack: 4.34.0
webpack-dev-server: 3.7.1
# plugins
html-webpack-plugin: 3.2.0
mini-css-extract-plugin: 0.7.0
clean-webpack-plugin: 3.0.0

つくったのよこせ!

はい。

TL;DR 秘伝のタレレシピ

Babel

install

$ yarn add -D @babel/core @babel/cli @babel/preset-env core-js@3
$ yarn add @babel/polyfill

設定ファイル babel.config.js を作成して設定を書いていく。
複雑なことはせずにシンプルにES6使えるようにする感じで。

babel.config.js

const presets = [
 ["@babel/preset-env", {
  "modules": false,
  "targets": [">0.25% in JP", "not ie <= 10", "not op_mini all"],
  "useBuiltIns": "usage",
  "corejs": 3,
 }],
];

module.exports = { presets };

useBuiltIns を使っている時に core-js のバージョン指定がないと次のようなWARNINGが表示される

WARNING: We noticed you're using the `useBuiltIns` option without declaring a core-js version. Currently, we assume version 2.x when no version is passed. Since this default version will likely change in future versions of Babel, we recommend explicitly setting the core-js version you are using via the `corejs` option.

You should also be sure that the version you pass to the `corejs` option matches the version specified in your `package.json`'s `dependencies` section. If it doesn't, you need to run one of the following commands:

  npm install --save core-js@2    npm install --save core-js@3
  yarn add core-js@2              yarn add core-js@3

core-jsのバージョン指定が無いと2系を指定するけど、将来バージョンが変更になるかもって感じっぽい

Webpack

install

$ yarn add -D webpack webpack-cli webpack-dev-server babel-loader

build コマンドを作成

build する npm script を package.json に追加する

{
  // ... 略
  "scripts": {
    "build": "./node_modules/.bin/webpack --mode production",
    "build:dev": "./node_modules/.bin/webpack --mode development"
  },
  // ... 略
}

設定ファイルの作成

webpack.config.js を作成して設定を追加していく

webpack.config.js

const path = require('path');
// PATH
const ASSET_PATH = process.env.ASSET_PATH || '/';
const src = path.join(__dirname, 'src');
const entrypoint = {
  // app.js という名前で出力する
  app: path.join(src, 'index.js'),
};
const output = path.resolve(__dirname, 'build');

module.exports = (env, argv) => {
  return {
    mode: 'development',
    entry: entrypoint,
    output: {
      path: output,
      publicPath: ASSET_PATH,
      filename: '[name].js',
    },
    module: {
      rules: [
        {
          test: /\.js$/,
          exclude: /node_modules/,
          use: [
            { loader: "babel-loader", }
          ],
        },
      ],
    },
    devtool: 'source-map',
  };
};

eslint とか走ってないけど、とりまモダンなJS出かける環境の設定ができました。

webpack + pug

install

$ yarn add -D pug pug-loader html-webpack-plugin
note. HtmlWebpackPlugin

webpackでbundle したJavaScriptをscriptタグで読み込んだHTMLを生成するプラグインtemplateオプションで pug ファイルなどを生成するHTMLのテンプレートファイルを指定できる。

ref.

pug の設定

webpack.config.js

const path = require('path');
// Plugins
// HTMLを生成するプラグイン
const HtmlWebpackPlugin = require('html-webpack-plugin');
// PATH
// 略
// html template
const htmlTemplate = path.join(src, 'index.pug');

module.exports = (env, argv) => {
  const isProduction = argv.mode === 'production';

  return {
    mode: 'development',
    entry: entrypoint,
    output: {
      path: output,
      publicPath: ASSET_PATH,
      filename: '[name].js',
    },
    module: {
      rules: [
        // 略
        {
          test: /\.pug$/,
          use: {
            loader: 'pug-loader',
            // production mode の時 minify する
            options: !isProduction ? {
              pretty: true
            } : {}
          }
        },
      ],
    },
    devtool: 'source-map',
    plugins: [
      new HtmlWebpackPlugin({
        template: htmlTemplate,
      }),
    ],
  };
};

Template src/index.pug

doctype html
head
  title My HIDEN no TARE!
body
  h1 秘伝のタレ

👇 webpack

$ yarn run build

/build ディレクトリ内にHTMLとJavaScriptファイルが生成される。 HTMLファイルには下記のscriptタグが自動的に挿入されている。

<script type="text/javascript" src="/app.js"></script>

webpack + stylus

install

$ yarn add -D stylus stylus-loader css-loader mini-css-extract-plugin
note. css-loader

importurl の解決などCSSの依存解決を行う loader

ref. GitHub - webpack-contrib/css-loader: CSS Loader

note.

JavaScriptにbundleされるCSSCSSファイルに変換するプラグイン

ref.

stylus の設定

webpack.config.js

const path = require('path');
// Plugins
const HtmlWebpackPlugin = require('html-webpack-plugin');
// CSSファイルを出力するプラグイン
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
// 略

module.exports = (env, argv) => {
  const isProduction = argv.mode === 'production';
  const enabledSourceMap = isProduction || false;
  
  const getStyleLoaders = () => {
    const loaders = [
      {
        loader: MiniCssExtractPlugin.loader,
      },
      {
        loader: 'css-loader',
        options: {
          // production mode の時 source map を出力する
          sourceMap: enabledSourceMap,
          // 事前に適応するloaderの数
          importLoaders: 1
        },
      },
      {
        loader: 'stylus-loader',
        options: {
          // stylus では source map を作らない
          sourceMap: false,
        },
      },
    ];
    return loaders; 
  };

  return {
    // 略
    resolve: {
      // import を js ファイル以外でも使えるようにする
      modules: ['node_modules'],
      // import できる拡張子を指定
      extensions: ['.json', '.js', '.jsx', '.styl'],
    },
    module: {
      rules: [
        // 略
        {
          test: /\.styl$/,
          use: getStyleLoaders(),
          // CSS は Webpack の Tree Shaking での自動的削除は
          // 問題がある可能性があるので sideEffects を true にする
          sideEffects: true,
        },
      ],
    },
    devtool: enabledSourceMap && 'source-map',
    plugins: [
      new HtmlWebpackPlugin({
        template: htmlTemplate,
      }),
      new MiniCssExtractPlugin({
        // stylesheets ディレクトリの中に出力させる
        filename: "stylesheets/[name].css",
      }),
    ],
  };
};

webpack の loader は下から順番に実行されるので CSS の処理は次のような感じ

  1. stylus-loader で stylus を コンパイル
  2. css-loaderCSS の依存を解決し JS ファイルに bundle
  3. MiniCssExtractPlugin.loader で JS に bundle された CSSCSSファイルにして出力
CSSの出力

webpack.config.jsresolve の設定を追加し stylus ファイルを import 出来るようにする。

module.exports = (env, argv) => {
  return {
    // ...
    resolve: {
      // import を js ファイル以外でも使えるようにする
      modules: ['node_modules'],
      // import できる拡張子を指定
      extensions: ['.json', '.js', '.jsx', '.styl'],
    },
   // ...

※ webpack 4 では拡張子の指定に extensions: ['', '.json', '.js', '.jsx', '.styl'] のように空文字('')があるとエラーになる

エントリーポイントになっているsrc/index.jsに読み込ませたい stylus ファイルを読み込ませる。

import './stylus/App.styl';

👇 wepack

$ yarn run build

/build/stylesheets 内に app.cssapp.css.map が生成される。 出力されるHTMLはheadタグ内に自動的に下記のような<link>が作られ css が読み込まれるようになっている

<link href="/stylesheets/app.css" rel="stylesheet">

PostCSS で 自動的にベンダープレフィックスを付けたりよしなにする

stylus の nib を使ってもよいのですが、最近は PostCSS を使ってよしなにするのがナウいらしいので、PostCSSを使ってベンタープレフィックスと nomalize 、メディクリの統合・productionモードの際にminifyをしたいと思います。 (Prettierでコードフォーマットを揃えるのはまたいつか)

install

$ yarn add -D postcss-loader postcss-preset-env postcss-flexbugs-fixes postcss-normalize css-mqpacker cssnano
  • postcss-loader ... PostCSSをwebpackで使うためのloader
  • postcss-preset-env ... モダンCSSをconvertするパッケージ。対応ブラウザに合わせたautoprefixの指定などもできる
  • postcss-flexbugs-fixes ... display: flex のブラウザのバグ対応したCSSを追加してくれる
  • postcss-normalize ... browsersで指定した対応ブラウザに合わせて必要な normalize.css を追加する
  • css-mqpacker ... mediaクエリをまとめるパッケージ。デファクトスタンダードっぽいけど、現在メンテがされなくて deprecated になっているので将来的にforkされたべつのパッケージになるかも
  • cssnano ... CSSをminifyするライブラリ

PostCSS の設定

webpack.config.js

// 略
// CSS
// normalize.css を追加
const postcssNormalize = require('postcss-normalize');
// media query をまとめる
const cssMqpacker = require('css-mqpacker');
// css minify
const cssnano = require('cssnano');

module.exports = (env, argv) => {
  const isProduction = argv.mode === 'production';
  const enabledSourceMap = isProduction || false;
  
  const getStyleLoaders = () => {
    const loaders = [
      {
        loader: MiniCssExtractPlugin.loader,
      },
      {
        loader: 'css-loader',
        options: {
          sourceMap: enabledSourceMap,
          // stylus と PostCSS の2つ
          importLoaders: 2
        },
      },
      {
        loader: 'postcss-loader',
        options: {
          plugins: [
            // production mode の時 minify
            isProduction && cssnano({ preset: 'default' }),
            cssMqpacker({
              sort: true
            }),
            require('postcss-flexbugs-fixes'),
            require('postcss-preset-env')({
              autoprefixer: {
                flexbox: 'no-2009',
              },
              stage: 3,
            }),
            postcssNormalize({
              browsers: [">0.25% in JP", "not ie <= 10", "not op_mini all"],
            }),
          ].filter(Boolean), // false を削除
          sourceMap: enabledSourceMap,
        }
      },
      {
        loader: 'stylus-loader',
        options: {
          sourceMap: false,
        },
      },
    ];
    return loaders; 
  };

  return {
    // 略
    module: {
      rules: [
        // 略
        {
          test: /\.styl$/,
          use: getStyleLoaders(),
          sideEffects: true,
        },
      ],
    },
    // 略
  };
};

build 時に出力先のディレクトリを cleanup する

development mode の時は source map を出力しないようにしているので、build した際に不要なファイルが残っていないようにclean-webpack-plugin プラグインを使って、buildが走る前に出力先buildディレクトリの中をcleanupする。

install

$ yarn add -D clean-webpack-plugin

clean-webpack-plugin の設定

webpack.config.js

const path = require('path');
// plugins
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
// 略

module.exports = (env, argv) => {
  // 略
  return {
    // 略
    plugins: [
      new CleanWebpackPlugin({
        // 実際に削除してしまう前にテストする場合は true にする
        dry: false,
        // 削除するファイル名をconsoleに出力
        verbose: true,
        // static なファイルを置いておく可能性があるので img ディレクトリ内のファイルは削除しない
        cleanOnceBeforeBuildPatterns: ['**/*', '!img', '!img/*']
      }),
      // 略
    ],
  };
};

👇 動作確認

$ yarn run build
$ yarn run build:dev

production mode で build した後に development mode で build して app.css.map など不要なファイルが消えていればOK

ホットリロード可能な確認環境 webpack-dev-server

webpackでwatchできるファイルを監視してホットリロードできるとても便利なサーバー

install

$ yarn add -D webpack-dev-server

webpack-dev-server の設定

open オプションで自動的にブラウザを開く機能があるが、常に新しくページを開かれるのもストレスなのでオプションで新しく開くかどうか選べるようにします。

package.json にサーバー起動の npm script を追加

//略
  "scripts": {
    "start": "./node_modules/.bin/webpack-dev-server --mode development --hot --env.open",
    "restart": "./node_modules/.bin/webpack-dev-server --mode development --hot",
    "build": "./node_modules/.bin/webpack --mode production",
    "build:dev": "./node_modules/.bin/webpack --mode development"
  }

start なら新しくブラウザを開き、restart なら開かない

webpack.config.js

// 略
module.exports = (env, argv) => {
  const isProduction = argv.mode === 'production';
  const enabledSourceMap = isProduction || false;
  const isOpenDevServer = !!env && env.open;
  // 略
  return {
    // 略
    devServer: {
      open: isOpenDevServer,
      port: 3000,
      watchContentBase: true,
    }
  };
};

👇 動作確認

$ yarn run start

localhost:3000 でページが起動し、javascript, pug, stylus を変更でホットリロードされていればOK

 

ポエム

フレームワークのお作法で開発するなら必要のない部分かもですが、秘伝のタレを自分で作ってみることによって理解が深まったように思います。 特に loader は受け取ったものを JS で返し、plugin は受け取ったものを自由な形式で返せる という違いが発見できたことは学びでした。

昨今のフロントエンドまわりパッケージが乱立しててどれを選べば良いのか…ってのが難しい。
一時期より動きは穏やかになったとは言え、まだまだ変化の早めな界隈な感じなので下手に選ぶとすぐ使えなくなったり、更新が止まって廃止とかになったりするので調査に時間がかかる感は否めないです。(Qiitaとか、日付のないblogとかたくさん検索にかかるので余計に検索に時間が…


[参考]

pug

css

PostCSS

デザイニングWebアクセシビリティ - アクセシブルな設計やコンテンツ制作のアプローチ

デザイニングWebアクセシビリティ - アクセシブルな設計やコンテンツ制作のアプローチ

秘伝 焼肉のたれ 575g×2本

秘伝 焼肉のたれ 575g×2本

\秘伝の🌟タレ/