かもメモ

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

JavaScript parseInt, paeseFloat が NaN になるとき 0 を返したい

JavaScriptparseInt, parseFloat10px とか単位がある文字列でもいい感じに数値変換して返してくれますが、空文字列 "" の時 NaN が返されます。その後の計算でエラーになることも多いので NaN の代わりに 0 を返したいときのメモ

空文字や undefined が渡された時 parseInt, parseFloat は NaN を返す

parseInt("10px"); // => 10
parseInt(""); // => NaN
parseInt(undefined); // => NaN

parseFloat('2.5rem'); // => 2.5
parseFloat(""); // => NaN
parseFloat(undefined); // => NaN

|| を使えばOK

|| は左辺が falsy な時、右辺を返すのを利用してデフォルト値を返すようにすることができる

parseInt("10px") || 0; // => 10
parseInt("") || 0; // => 0
parseInt(undefined) || 0; // => 0

parseFloat('2.5rem') || 0; // => 2.5
parseFloat("") || 0; // => 0
parseFloat(undefined) || 0; // => 0

parseInt, parseFloatNaN になるときは 0 を返すことが出来るようになりました!
₍ ᐢ. ̫ .ᐢ ₎ ヤッタネ!

parseIntInfinity を返すこともあるけど、あまりなさそうだし style から取得した値をかを変換して NaN が返される可能性がある場合は parseInt(val) || 0 で事足りそう。


[参考]

CSS Modules やっていき

CSS Modules とゎ?

ざっくりいうと CSS は全部が global 定義なので、コンポーネントごとにクラス名をハッシュ化したモジュールにしてしまってスタイルの影響範囲をコンポーネントの中に閉じ込めようというもの

e.g.

/* style.css */
.logo { color: red; }
import stytles from './style.css'
'<div class="' + styles.logo + '">'

👇コンパイル

/* css */
._23_aKvs-b8bW2Vg3fwHozO { color: red; }
// 👇 import styles from './style.css' でバンドルされる中身
exports.locals = {
  logo: "_23_aKvs-b8bW2Vg3fwHozO",
};

'<div class="' + styles.logo + '">';

SCSS を CSS Modules にする最低限の webpack の設定

css-loader の module オプションを true にすれば OK

webpack.config.js

module.exports = {
  // ...
  rules: [{
    test: /\.scss$/,
    use: [
      'style-loader', // JS 内にある CSS を <head> にインラインで出力する loader
      'css-loader?modules', // CSS を JS で扱えるようにする loader
      'sass-loader',  // sass | scss の変換
    ]
  }],
}

CSS.scss ファイルとして出力する場合は mini-css-extract-plugin を使う

cf. webpack css-loader と style-loader の違いについて学んだ - かもメモ

webpack.config.js

const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
  // ...
  rules: [{
    test: /\.scss$/,
    use: [
      MiniCssExtractPlugin.loader,
      'css-loader?modules',
      'sass-loader',
    ]
  }],
}

CSS Modules の Mode

CSS Modules 化で全ての CSS の指定が hash 化されてしまうと、normalize.css とか共通の utility クラスが作れなくなってしまうので、CSS Modules には hash 化される local なスタイルと共通で使える global なスタイルという概念があります。 css-loader は内部的に local なスタイルを :local(.className) {...}, global なスタイルを :global(.className) {...} という形に変換した上で local なスタイルを hash 化して最終的な CSS に変換しているようです。

SCSS では明示的に :local, :global キーワードで囲って定義してあるスタイルが local なスタイルなのか、global なスタイルなのかを表すことができます。 CSS Modules への変換方法には local, global という2つの mode があり、local スタイルと global スタイルへの変換方法が異なります。

  • local モード (default) では CSS ファイルにベタ書きしたスタイルを local なスタイルに変換する
  • global モードでは CSS ファイルにベタ書きされたスタイルを global なスタイルとして扱う

local モード

css-loader の modules オプションの mode を 'local' 又は true 又は css-loader?modules と指定する

webpack.config.js

module.exports = {
  // ...
  rules: [{
    test: /\.scss$/,
    use: [
      MiniCssExtractPlugin.loader,
      {
         loader: 'css-loader',
         options: {
           modules: {
             mode: 'local', // mode: true でもOK
           }
         }
      },
      'sass-loader',

style.scss

// ベタ書きのスタイル
.ichigo { color: red; }
// local なスタイル
:local {
  .aoi { color: blue; }
}
// global なスタイル
:global {
  .ran { color: purple; }
}

👇 コンパイル

._2EvoXuCC19CRPsPIfLzc34 { color: red; }
.yqLGVvuQreTgr2chBe--O { color: blue; }
.ran { color: purple; }

global モード

css-loader の modules オプションの mode を 'global' に指定 webpack.config.js

module.exports = {
  // ...
  rules: [{
    test: /\.scss$/,
    use: [
      MiniCssExtractPlugin.loader,
      {
         loader: 'css-loader',
         options: {
           modules: {
             mode: 'global',
           }
         }
      },
      'sass-loader',

style.scss

// ベタ書きのスタイル
.ichigo { color: red; }
// local なスタイル
:local {
  .aoi { color: blue; }
}
// global なスタイル
:global {
  .ran { color: purple; }
}

👇 コンパイル

.ichigo { color: red; }
.yqLGVvuQreTgr2chBe--O { color: blue; }
.ran { color: purple; }

JavaScriptCSS Modules を扱う

default import したオブジェクトの中に local なスタイル指定が 元のクラス名: hash化されたクラス名 という形で渡される

/* style.scss */
.ichigo { color: red; }
:local {
  .aoi { color: blue; }
}
:global {
  .ran { color: purple; }
}

👇 local モードで JS で読み込んだ場合

import styles from './style.scss';
console.log(styles);
// => { ichigo: "_2EvoXuCC19CRPsPIfLzc34", aoi: "yqLGVvuQreTgr2chBe--O" }

// global なクラス名は import されるオブジェクトには含まれない
console.log(styles.ran); // undefined

const Soleil: FC = () => (
  <>
    <p className={styles.ichogo}>星宮いちご</p>
    <p className={styles.aoi}>霧矢あおい</p>
    <p className="ran">text 紫吹蘭</p>
  </>
);

css-loader は CSS ファイル内の local なスタイルだけを exports.local = { className: "hashed_className" } という形に変換して JavaScript に渡す。

local なスタイルと global なスタイルの扱いの違い

import されるオブジェクト内に global なスタイルの指定は含まれない。
つまり、global なスタイルは CSS Modules にしても JS ファイルのサイズには影響を与えない。 逆に言えば local なスタイルの指定は JS ファイルとして読み込まれるので、使用していないスタイルを含む CSS ファイルを import した場合 不要なJSなコードが含まれてしまうので、モジュール単位で分解されていないようなCSS を読み込むと JS のサイズが肥大してしまうので CSS ファイルの分割に注意が必要になる

SCSS から JS に変数を渡したい

SCSS / Sass で作れる変数を JS に渡して使いたいようなケース $ で始まる変数はそのままでは JS ファイルに渡すことは出来ない。 :export キーワード内に $ 無しで定義すると JS 側では変数として扱うことができる

// style.scss
$widthTablet: 720px;

:export {
  widthTablet: $widthTablet
  $widthMobile: 320px;
}
import styles from './styles.scss';
console.log(styles);
// => { widthTablet: "720px" }

$ で始まる SCSS の変数は :export キーワード内に定義しても JS には渡されません。 また、 :export キーワードブロック内にスタイルを定義しても JS では扱えず、最終的にコンパイルされた CSS:export .className {...} の様な使えないスタイルが出力されてしまうので、:export キーワード内には JS に渡したい変数だけを定義するのが良さそうです。

map な変数はそのままでは渡せない

SCSS の map な変数は、そのままおオブジェクトとして JS に渡せるのかと思ったのですがエラーになるようで、 map を展開した変数として渡してあげる必要があるようです。

// style.scss
$soleil: (
  ichogo: red,
  aoi: blue,
  ran: purple,
);

:export {
  seleil: $soleil; // => エラーになる
}

// map を展開して個別の変数にして export する必要がある
@each $name, $color in $soleil {
  :export {
    #{$name}: $color;
  }
}

👇

import styles from './styles.scss';
console.log(styles);
// => { ichogo: "red", api: "blue", ran: "purple" }

:local, :global 内に変数を定義した場合

:local { maxWidth: 720px } の様に :export でないキーワードのブロック内に JS に渡したい変数を定義しても、JS では扱うことが出来ず 最終的に出力される CSS にもこの変数定義が出力されてしまいました。

SCSS と JS 共通して扱いたい変数は必ず :export キーワード内に記述する必要がありそうです。

JS 内で使用するだけ、CSS としては不要な場合は style-loader, MiniCssExtractPlugin は使わなくてもOK

レアケースだけど、CSS と JS で変数だけを共有して使いたいような場合。CSS を出力する必要がなければ CSS として書き出すための style-loader, MiniCssExtractPlugin は使わなくても OK

webpack.config.js

module.exports = {
  // ...
  rules: [{
    test: /\.scss$/,
    use: [
      'css-loader?modules',
      'sass-loader',
    ]
  }],
}

CSS Modules を named import したい

// style.scss
.ichigo { color: red; }
:export {
  widthTablet: 720px;
}
import { ichigo, widthTablet } from './style.scss';
console.log(ichigo); // => "_2EvoXuCC19CRPsPIfLzc34"
console.log(widthTablet); // => "720px"

css-loader のオプションには namedExport オプションがありこれを使うと named import ができるようになる。 css-loader の後に実行される style-loader や MiniCssExtractPlugin にもこのオプションを指定してあげる必要がある。
この設定を忘れると default import は空オブジェクト {} になり named import は undefined になってしまう。

cf. namedExport css-loader

style-loader の場合

  • style-loader v2.0.0
  • css-loader v5.0.0

cf. namedExport style-loader

webpack.config.js

module.exports = {
  // ...
  rules: [{
    test: /\.scss$/,
    use: [
      {
        loader: 'style-loader',
        options: {
          esModule: true,
          modules: {
            namedExport: true,
          },
        }
      },
      {
        loader: 'css-loader',
        options: {
          esModule: true,
          modules: {
            namedExport: true,
          },
        }
      },
      'sass-loader', 
    ]
  }],
}

MiniCssExtractPlugin プラグインを使うパターン

  • mini-css-extract-plugin v1.1.0
  • css-loader v5.0.0

CSS ファイルを作成する mini-css-extract-plugin プラグインにも namedExport のオプションがありこれを true にしてあげれば OK

cf. namedExport MiniCssExtractPlugin

webpack.config.js

const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
  // ...
  rules: [{
    test: /\.scss$/,
    use: [
      {
        loader: MiniCssExtractPlugin.loader,
        options: {
          esModule: true,
          modules: {
            namedExport: true,
          },
        }
      },
      {
        loader: 'css-loader',
        options: {
          esModule: true,
          modules: {
            namedExport: true,
          },
        }
      },
      'sass-loader', 
    ]
  }],
}

これで CSS Modules で named import ができるようになりました!

namedExport が設定されていると default import は undefined になる

namedExport を設定した場合内部的には JS に渡される CSS のクラス名・変数が下記のような形でになっているので default import はできなくなる

// style.scss
.ichigo { color: red; }
:export {
  widthTablet: 720px;
}

JS には下記の形に変換されたものが渡される

// CONCATENATED MODULE: ./style.scss
// extracted by mini-css-extract-plugin
const widthTablet = "720px";
const ichigo = "_2EvoXuCC19CRPsPIfLzc34";
// CONCATENATED MODULE: ./src/index.js

👇 なので default import は undefined になる

import styles from './style.scss';
console.log(styles); // => undefined

CSS Modules は named import でも tree shaking はされないっぽい

mode = production でビルドされたJSファイルの中を見てみた感じ、JS に import していないクラス名も全て変数として定義されていたので CSS Modules は named import にしても tree shaking は効かないみたいです。

/* ./style.scss */
.ichigo { color: red; }
:local {
  .aoi { color: blue; }
}
:global {
  .ran { color: purple; }
}
:export {
  widthTablet: 720px;
}
/* ./base.scss */
.app { font-size: 1rem; }
import './base.scss';
import { ichigo, widthTablet } from './style.scss';

👇 ビルドされた JS ファイルの中

// CONCATENATED MODULE: ./base.css
// extracted by mini-css-extract-plugin
const app = "w54bpbuIyvtuFEpomSGPt";
// CONCATENATED MODULE: ./style.scss
// extracted by mini-css-extract-plugin
const widthTablet = "720px";
const ichigo = "_2b0mtVMhYRrlk4p9CgzKCc";
const aoi = "_3fxOvTsExbr0n7UyKw53cD";

js では import していない api も変数として定義されているので、 named import する場合も CSS ファイル内にある local 変数は全て JS に読み込まれるみたいなので named import しない場合もコンポーネントで使うスタイルだけ書かれた小さな CSS ファイルを import するように心がける必要は変わらずありそうです。

namedExport した際の ES5 対応について

namedExport された CSS Modules は const className = "hashed_class_name" の形で JS ファイルに書き出されます。
css-loader から渡されたコードは JavaScript / Typescript の loader を通らないようなので、これらで ES5 に変換する設定をしていても最終的に出力されるコードに const が残ってしまいます。

webpack.config.js

module.exports = {
  // ...
  rules: [
    {
      // css-loader で変換された JS はこの変換設定を通らない
      test: /\.js$/,
      use: [
        exclude: /node_modules/,
        loader: 'babel-loader',
        options: {
          "presets": [
            "@babel/preset-env"
          ],
        },
      ],
    },
    {
      test: /\.scss$/,
      use: [
        {
          loader: MiniCssExtractPlugin.loader,
          options: {
            esModule: true,
            modules: {
              namedExport: true,
            },
          }
        },
        {
          loader: 'css-loader',
          options: {
            esModule: true,
            modules: {
              namedExport: true,
            },
          }
        },
        'sass-loader', 
      },
    ]
  ],
}

流石にもう const は大丈夫だと思いますが最終的に出力されるスクリプトを Linter などでチェックしている場合は css-loader の後に babel などで ES5 の形に変換してあげる必要があります。

webpack.config.js

module.exports = {
  // ...
  rules: [
    // …
    {
      test: /\.scss$/,
      use: [
        {
          loader: MiniCssExtractPlugin.loader,
          options: {
            esModule: true,
            modules: {
              namedExport: true,
            },
          }
        },
        // JS に変換された CSS を babel で ES5 の形式に変換
        {
          loader: 'babel-loader',
          options: {
            "presets": [
              "@babel/preset-env"
            ],
          },
        },
        {
          loader: 'css-loader',
          options: {
            esModule: true,
            modules: {
              namedExport: true,
            },
          }
        },
        'sass-loader', 
      },
    ]
  ],
}

css-loader で JS 形式に変換されたコードを babel で ES5 にトランスコンパイルをすると named import したスタイルは var className = "hashed_class_name" の形式に変換されるので、最終的に出力されるコード に const が残ることはなくなりました。

まとめ

これで CSS Modules が使える設定を作ることができました。
CSS の設計は全部がグローバルなので難しい。 CSS Modules ならモジュールごとにスタイルを閉じ込めることが出きて便利!
ただし、閉じ込めるスタイルの分だけ JS ファイルが肥大化するので、本当に必要なスタイルだけを読み込む様に気をつける。
という印象でした。

つまり、CSS Modules にしても CSS はちゃんと設計する必要がある。ということだと思います。
人類は CSS 設計からは逃れられない…


[参考]

webpack css-loader と style-loader の違いについて学んだ

CSS を webpack で扱う時にセットで使いがちな css-loader と style-loader の違いをちゃんと理解してなかったので CSS Modules について考えるに当たって調べ直したのでメモ

webpack の loader の処理順

e.g. SCSS を webpack で扱う際の設定

// webpack.config.js
module.exports = {
  // ...
  rules: [
    test: /\.(scss|sass)$/,
    use: [
      'style-loader', 
      'css-loader',
      'sass-loader',
    ]
  ],
}

webpack の loader は下から順番に実行するので下記のような順で処理が実行される

sass-loader > scss-loader > style-loader

style-loader

Inject CSS into the DOM.
cf. GitHub - webpack-contrib/style-loader: Style Loader

webpack のページのサンプルを見るとわかりやすかったです。
雑にまとめると style-loaderCSS を HTML の head タグ内にインラインスタイル (<style></style> ) を出力する loader

css-loader

The css-loader interprets @import and url() like import/require() and will resolve them. cf. GitHub - webpack-contrib/css-loader: CSS Loader

css-loader は機能が多いのですが、これも雑にまとめると
css-loaderCSS を JS で扱える形に変換する loader

SCSS / SASS の変換 rules を見返すと…

// webpack.config.js
module.exports = {
  // ...
  rules: [
    test: /\.(scss|sass)$/,
    use: [
      'style-loader', 
      'css-loader',
      'sass-loader',
    ]
  ],
}

この設定は次のように変換をしていると読み取ることができました。

  1. sass-loader で scss / sass を css に変換
  2. scss-loader で JS で読み込める形式に変換
  3. style-loader で JS として読み込んだ CSS を HTML の にインラインスタイルとして挿入

style-loader をもう少し詳しく

style-loader は HTML の <head> 内にインラインスタイルを出力するけど、build しても CSS ファイルは作成されず bundle.js ファイルが作成されるだけです。

つまり、style-loader は css-loader で JS で扱える形にした CSS.js ファイルの中に持っておいて HTML が読み込まれ script が実行された時に <style> タグを生成して HTML の <head> に挿入するという処理を行うということのようです。

CSS ファイルとして出力したい場合は MiniCssExtractPlugin を使う

MiniCssExtractPlugin | webpack

This plugin extracts CSS into separate files. It creates a CSS file per JS file which contains CSS. It supports On-Demand-Loading of CSS and SourceMaps.
cf. GitHub - webpack-contrib/mini-css-extract-plugin: Lightweight CSS extraction plugin

このプラグインを使えば CSS ファイルが生成され <link> で読み込まれるようになります。

// webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
  // ...
  rules: [
    test: /\.(scss|sass)$/,
    use: [
      MiniCssExtractPlugin.loader,
      'css-loader',
      'sass-loader', 
    ]
  ],
  plugins: [
    // plugins 内で出力されるファイル形式などのオプションを指定できる
    new MiniCssExtractPlugin({
      filename: '[name].[hash].css',
    }),
  ],
}

この設定で build すると、CSS のファイルが生成されるようになりました!

webpack-dev-server を使う dev モードでも onMemory で CSS ファイルが作成され問題なく動作するので、 MiniCssExtractPlugin を使うのであれば style-loader は不要。

build した時だけ MiniCssExtractPlugin で CSS ファイルを生成したい場合

webpack の mode を利用して使用する loader を分岐させればOK!
コマンドから --mode development の様に送る場合は argv.mode で値を取得できる。

// webpack.modules.js
module.export = function (env, argv) {
  const mode = process.env.NODE_ENV || argv.mode || 'development';
  const isProduction = mode === 'production';

  return {
    //...
     module: {
      rules: [
        {
          test: /\.(scss|sass)$/,
          use: [
            !isProduction && 'style-loader',
            isProduction && MiniCssExtractPlugin.loader,
            'css-loader',
            'sass-loader', 
          ].filter(Boolean), // false が残るとエラーになるので filter(Boolean) で除去する
        }
      //...
  };
}

₍ ᐢ. ̫ .ᐢ ₎❤︎

まとめと感想

あいまいだった style-loader と css-loader の違いがクリアになった気がします。

  • インラインスタイルを JS から出力したい場合は style-loader
  • .css ファイルを生成して読み込ませたい場合は MiniCssExtractPlugin

という使い分けをすれば良さそうです。

create-react-app を eject して webpack.config.js を見てみると development の時は style-loader を使い production でビルドする際は MiniCssExtractPlugin を使うような設定になっていました。
Hot Reload で頻繁にファイルが変わる development では都度 css ファイルを生成しない style-loader の方が高速で、実際のプロダクトの状態では css ファイルを読み込ませる方がパフォーマンスが良いのからそうしているのかな〜と感じました。

個人的にも JS から <style> タグを生成するのは script でその他の処理がブロックされている状態で CSS が出力され script の実行を待ってレイアウトのレンダリングがされるというイメージです。
プロジェクトが大きくなると JS も CSS も大きくなるでしょうから、CSSbundle.js に含まれているとそれだけファイルサイズが大きくなり script の実行で処理がブロックされる時間が長くなりそうな気がします。(測定したわけではないのであくまでイメージですが…)


[参考]

速習 webpack 第2版 速習シリーズ

速習 webpack 第2版 速習シリーズ

毛穴撫子 お米のマスク 10枚入

毛穴撫子 お米のマスク 10枚入

  • メディア: ヘルスケア&ケア用品