かもメモ

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

webpack v5 npx webpack-dev-server で dev server が動かないにハマる

久しぶりに webpack 環境を作っていて web-pack-dev-server を動かそうとしてハマったのでメモ

package.json

"devDependencies": {
  "webpack": "^5.6.0",
  "webpack-cli": "^4.2.0",
  "webpack-dev-server": "^3.11.0"
},

Error: Cannot find module 'webpack-cli/bin/config-yargs'

npm のドキュメントにある通り webpack-dev-server を起動しようとしたらエラーになった

$ npx webpack-dev-server
Cannot find module 'webpack-cli/bin/config-yargs'

どうやら webpack-cli/bin/config-yargscli の v3 系で削除されているらしい

webpack v5, webpack-cli v4 系では webpack serve コマンドを使う

webpack のドキュメントに次のように書かれていました。

"scripts": {
    "start": "webpack serve --open",
   },
cf. [https://webpack.js.org/guides/development/#using-webpack-dev-server:title]

コマンドが変わっていたみたいです。

$ npx webpack serve
ℹ : Project is running at http://localhost:8080/

起動しました 👌

--open オプションでエラー

$ npx webpack serve --open
error: option '--open <value>' argument missing

webpack のドキュメントにある通り自動でブラウザを開く --open オプションを付けると option '--open <value>' argument missing 引数がないというエラーになってしまいました。

どのブラウザで開くのかを --open の引数に渡してあげる必要がある

$ npx webpack serve --open 'google chrome'
ℹ : Project is running at http://localhost:8080/

chrome でタブが開いて dev server が起動しました!
引数があれば何でも良いみたいで npx webpack serve --open foo みたいにしてもブラウザが開かないだけで問題なくどうさしました。(良いのか…

webpack.config.js にオプション指定すれば --open の引数は不要

webpack.config.jsdevServer.open オプションを使えば何で開くかの引数は不要でデフォルトブラウザで起動できるようでした。

webpack.config.js

module.exports = {
  // ...
  devServer: {
    contentBase: outputPath,
    open: true,
  },
}

package.json

"scripts": {
  "start:dev": "webpack serve",
  // …
},

👇 実行

$ npm run start:dev
ℹ : Project is running at http://localhost:8080/

デフォルトブラウザでタブが開いて dev server が起動されました!
₍ ᐢ. ̫ .ᐢ ₎ 👌

設定ファイルを作って npm script から実行させるのが良さそうです。
おわり


[参考]

絵柄がめっちゃ好みだった〜

Express な API でエラーハンドリングするよ

TypeScript な Express でAPI作ってエラーを返す処理を都度書いているとめんどいので、エラーハンドリングして返すようにしたメモ

エラーハンドリングのミドルウェアはルーティングより後に書く

上から順番にルーティングにマッチしていくか見ていって、該当するルーティングが無ければエラーハンドリングするミドルウェアに処理が流れてくるイメージ

import express, { HttpException, Request, Response, NextFunction } from 'express';
import apiRouter from './api/controller'
const app = express();
const router = express.Router();

// Routing
app.get('/api/user', await (req: Request, res: Response, next: NextFunction): Promise<data> => {
  try {
    const data = await something(….);
    return data;
  } catch (err) {
    return next(err);
  }
});

// error handling
export default function errorHandler(
  err: HttpException,
  req: Request,
  res: Response,
): void {
  res.status(500).send('error message');
}

ルーティング内で next(new Error()) されるとエラーハンドリングに処理が流れる

エラーのステータスを変えたりエラー時にデータを返したい

Nest.js みたいにエラーのステータスごとの関数を使ってエラーが返せるようになると楽そう。
一緒にエラー時にもデータを返したい!

HttpException クラスを拡張してエラーを返す関数を作成すれば OK

// errorException.ts 
import { Request, Response } from 'express';

class HttpException extends Error {
  statusCode?: number;
  message: string;
  constructor(statusCode: number, message: string) {
    super(message);
    this.statusCode = statusCode || 500;
    this.message = message;
  }
}

export const badRequestException =
  (message = '400 Bad Request'): HttpException => {
    return new HttpException(400, message);
};

export const forbiddenException =
  (message = '403 Forbidden'): HttpException => {
    return new HttpException(403, message);
};

// Error Handler Middleware
export default function errorHandler(
  err: HttpException,
  req: Request,
  res: Response,
): void {
  res.status(err.statusCode || 500).send(err.message);
}
// app.ts
import express, { HttpException, Request, Response, NextFunction } from 'express';
import apiRouter from './api/controller'
import errorHandler, { badRequestException } fromn './errorException';
const app = express();
const router = express.Router();

// Routing
app.get('/api/user', await (req: Request, res: Response, next: NextFunction): Promise<data> => {
  try {
    const data = await something(….);
    return data;
  } catch (err) {
    return next( badRequestException('ジョニー別府!') );
  }
});

app.use(errorHandler);

ブラウザで直接アクセスするとエラーメッセージが表示される。
axios でこの API を叩いている場合、エラーメッセージは error.response.data に格納されるので API で使用する場合は次のようにエラーメッセージを取得する

try {
  // ...
} catch (err) {
  const errorMessage = err?.response?.data || err.message;
  // => "ジョニー別府!"
}

これでデフォルトのエラーオブジェクトの代わりにステータスコードを変更してカスタムメッセージを付けたエラーハンドリングができるようになしました!

エラーに独自のオブジェクトを追加して返す

API として使う場合、メッセージ以外にも情報を返したいケースがあります。
その場合はフロントで使いたい情報のオブジェクトを返すようにすればOK

// errorException.ts 
import { Request, Response } from 'express';

type errorData = {
  [key: string]: any;
};

class HttpException extends Error {
  statusCode?: number;
  message: string;
  data: errorData;
  constructor(statusCode: number, message: string, data?: errorData) {
    super(message);
    this.statusCode = statusCode || 500;
    this.message = message;
    this.data = data ? { ...data } : {};
  }
}

export const badRequestException =
  (message = '400 Bad Request', data?: errorData,): HttpException => {
    return new HttpException(400, message, data);
};

export const forbiddenException =
  (message = '403 Forbidden', data?: errorData,): HttpException => {
    return new HttpException(403, message, data);
};

// Error Handler Middleware

export default function errorHandler(
  err: HttpException,
  req: Request,
  res: Response,
): void {
  // 直接 メッセージだけを入れられないので error {} 内にメッセージ・データを格納する
  res.status(err.statusCode || 500).send({
    message: err.message,
    error: {
      ...err.data,
    },
  });
}
// Routing
app.get('/api/user', await (req: Request, res: Response, next: NextFunction): Promise<data> => {
  try {
    const data = await something(….);
    return data;
  } catch (err) {
    return next( badRequestException('ジョニー別府!', {
      type: "Not idol, Great teacher!"
    }) );
  }
});

フロント

try {
  // ...
} catch (err) {
  const { message, error } = err?.response?.data || err;
  // => "ジョニー別府!", { type:  "Not idol, Great teacher!" }
}

err.response.data が無い時は通常のエラーオブジェクトが使われ、 errorundefined になる。

これで Express を API にしていい感じにエラーハンドリングができるようになりました!
₍ ᐢ. ̫ .ᐢ ₎ ヤッタネ
Nest.js 使えるようになれよ…って話でもあるケド


[参考]

自転車えくすぷれす

自転車えくすぷれす

  • 発売日: 2014/04/01
  • メディア: MP3 ダウンロード
さよならポニーテールを聞こう!

JavaScript margin 含めた width / height を取得したい

要素マージンを含めた width / height を取得するのがちょいと大変だったのでメモ。

html { font-size: 16px; }

.div {
  margin: 0.3rem 0.2rem; // 4.8px 3.2px 相当
  border: .3rem solid #000; // 4.8px 相当
  width: 6.2rem; // 99.2px 相当
  height: 6.2rem; // 99.2px 相当
}

margin を含んだ値で取得できる API は無いっぽい

  • clientWith / clientHeight … border, margin を含まない Int 値
  • offsetWidth / offsetHeight … border は含み margin は含まない Int 値
  • getBoundingClientRect … border は含み margin は含まない Float 値

clientWith / clientHeight は border, margin を含まない

border, margin を含まず、四捨五入で丸められた Int 値が返される

const elm = document.querySelector('.div');
elm.clientWidth; // => 99 Math.round(99.2)
elm.clientHeight; // => 99 Math.round(99.2)

offsetWidth / offsetHeight は border を含むが margin は含まない

border を含んだ値で、四捨五入で丸められた Int 値が返される

const elm = document.querySelector('.div');
elm.offsetWidth; // => 112 Math.round(99.2 + 4.8 * 2)
elm.offsetHeight; // => 112 Math.round(99.2 + 4.8 * 2)

getBoundingClientRect は border を含むが margin は含まない

border を含んだ値で Float値で取得できる

const elm = document.querySelector('.div');
const { width, height } = elm.getBoundingClientRect();
width; // => 108.78125 ( 99.2 + 4.8 * 2 相当)
height; // =>  108.78125 ( 99.2 + 4.8 * 2 相当)

margin は style から取得する

window.getComputedStyle(element) で要素の持つ CSS を取得してその中から margin の値を使う。

const elm = document.querySelector('.div');
const styles = window.getComputedStyle(elm);
const {marginTop, marginRight, marginBottom, marginLeft} = styles;
// => "4.8px", "3.2px", "4.8px", "3.2px" 

// 単位を含む文字列で取得されるので parseFloat で数値に変換する
Object.fromEntries(
  Object.entries({
    marginTop,
    marginRight,
    marginBottom,
    marginLeft,
  })
  .map(([key, value]) => [key, parseFloat(value) || 0])
);
// => { marginTop: 4.8, marginRight: 3.2, marginBottom: 4.8, marginLeft: 3.2 }

getBoundingClientRect で border を含む width / height を取得して、getComputedStyle で取得できた margin の値を合算する

clientWith / clientHeight, offsetWidth / offsetHeight は丸められた整数値になるので、emrem 指定の場合 margin を足すと丸められた少数の誤差の累積で表示されているサイズとの誤差が大きくなりそうなので getBoundingClientRect を使うのが良さそう。

const getBoundingSize = (element) => {
  const { width, height } = element.getBoundingClientRect();
  return { width, height };
};

const getElementMargin = (element) => {
  const styles = window.getComputedStyle(element);
  return ['top', 'right', 'bottom', 'left'].reduce((obj, key) => {
    return {
      ...obj,
      [key]: parseFloat(styles.getPropertyValue(`margin-${key}`)) || 0,
    };
  });
};

const getOuterSize = (element) => {
  const { top, right, bottom, left } = getElementMargin(element);
  const { width, height } = getBoundingSize(element);
  return {
    width: width + right + left,
    height: height + top + bottom,
  };
};

const elm = document.querySelector('.div');
const { width, height } = getOuterSize(elm);
width; // => 115.18125 ( 99.2 + 4.8 * 2 + 3.2 * 2 相当)
height; // => 118.38125 ( 99.2 + 4.8 * 2 + 4.8 * 2 相当)

example

See the Pen How to get a Element size with margin by KIKIKI (@kikiki_kiki) on CodePen.

パッケージ化してみました。

感想

margin を含んだ値で取得できる API が無いっぽいのは意外だった。
位置を取得できる offsetTop や clientTop も margin を含まないっぽいので、margin を含んだ値を使いたいときは style から取得するしかなさそうな印象でした。
jQueryouterWidth, outerHeight 便利だったんですね… (しみじみ…)


[参考]

初めてのJavaScript 第3版 ―ES2015以降の最新ウェブ開発

初めてのJavaScript 第3版 ―ES2015以降の最新ウェブ開発

  • 作者:Ethan Brown
  • 発売日: 2017/01/20
  • メディア: 単行本(ソフトカバー)

プロテイン MyProtein のミルクティが一番好きかも