メインコンテンツまでスキップ

「javascript」タグの記事が6件件あります

全てのタグを見る

· 約2分
ogumaru

概要

devcontainer 内にて、WebAssembly の npm パッケージをwasm-pack packするとエラーが発生する。

結論

Node.js を devcontainer 側にインストールする。

環境

項目バージョン
cargo1.64.0 (387270bc7 2022-09-16)
wasm-pack0.10.3
dockerversion 20.10.14, build a224086

詳細

mcr.microsoft.com/vscode/devcontainers/rust:0-bullseyeを元にした devcontainer 内に、下記にてwasm-packをインストールしていた。

cargo install wasm-pack

このコンテナ内でwasm-pack buildはできるものの、wasm-pack packは下記のエラーが発生した。

Error: Packaging up your code failed
Caused by: No such file or directory (os error 2)

対応

公式ドキュメントや Web 上を検索しても情報がなく困ったが、npm packを内部で呼び出しているのかと考え、下記コマンドを利用できるようにしたところ、無事wasm-pack packにて npm パッケージを作成することができた。

  • node
  • npm
  • npx

npmとサブコマンドが似ていることから当然といえばそうかもしれないが、気づかずに結構時間を使ってしまった。

なお、aptで入るものはバージョンが古いため、(差分が大きいが)下記コミットのようにアーカイブを展開する形で対応した。

update: devcontainer configuration (ff650765c668002ea1b8e8f056ef8a4907b6a6f8)

Node.js に関する該当箇所は下記

FROM busybox:1.34.1 as nodejs
ENV NODEJS_TARBALL_URL="https://nodejs.org/dist/v18.12.1/node-v18.12.1-linux-x64.tar.xz"
USER root
WORKDIR /root/.local
RUN mkdir -p bin/ node/ \
&& wget -O node.tar.xz "${NODEJS_TARBALL_URL}" --no-check-certificate \
&& xz -d -c node.tar.xz | tar xvf - -C node --strip-components 1 \
&& ln -s "/root/.local/node/bin/node" bin/ \
&& ln -s "/root/.local/node/bin/npm" bin/ \
&& ln -s "/root/.local/node/bin/npx" bin/ \
&& rm node.tar.xz

· 約3分
ogumaru

概要

有効/無効の 2 つの状態を持つ要素を利用して、createContextで作成した状態を保持・切り替えさせたい。

Calcite Design System のCalciteSwitchを利用し、状態をクリックで変更しようとしたが、実際はクリックをしても見た目が有効なまま変わらなかった。

意図した動作としては、選択状態に応じて下の表のようになる。

スイッチの状態表示
有効青、右
無効白、左

環境

項目バージョン
@esri/calcite-components-react0.32.0
react18.2.0

原因・対策

onClickでイベントの処理をしつつchecked={context.isSetAttr}のようにcheckedプロパティで状態管理をすると適切に表示されなくなる。

checked=truecheckedであれば当然青になるが、checked=falseであっても表示が上の表の「有効」のものになる。

ドキュメントにはcheckedtrue, falseを持つとあるが、実際にはfalseだと意図した通りの表示にならない。

下記コードでは状態に関わらず「有効」のものが表示される。

(contextは上位コンポーネントより渡しており、isSetAttrboolean)

<CalciteSwitch
checked={context.isSetAttr}
onClick={(e) => {
const input = e.target as HTMLInputElement;
context.setIsSetAttr(input.checked);
}}
/>

正しくは下記のようにonCalciteSwitchChangeでイベントの処理をしつつ、checkedに状態を持たせない。

<CalciteSwitch
onCalciteSwitchChange={(e) => {
const input = e.target;
context.setIsSetAttr(input.checked);
}}
/>

Fires when the checked value has changed. Note: The event payload is deprecated, use the component's checked property instead.

Switch | Calcite Design System | ArcGIS Developers - (developers.arcgis.com)

これで意図した表示にはなるが、コンポーネント自体に状態を保持しておらず、複数箇所からの操作がある場合に若干不安がある。

· 約4分
ogumaru

概要

ArcGIS API for JavaScript (@arcgis/core)で MapView を利用する際、インスタンス化時に container プロパティにセレクタの文字列かHTMLElementを指定することで、そこに地図を表示することができる。

React を利用する都合上、静的な HTML ではなく、表示対象となる要素(React コンポーネント)が描画されたあと、そこに表示されるようにしたい。

実装は ES Modules および TypeScript を利用して行った。

結論

useEffect内で__esri.Accessor.set()からcontainerを指定する。

import { useEffect } from "react";
import MapView from "@arcgis/core/views/MapView";

// ここでは container は指定しない。
// レイヤの操作をする都合上、
// export する必要があるためsetup内でインスタンス化できない。
export const mapView = new MapView({
/** 略 **/
});

const setup = () => {
// 動的に container を指定
mapView.set("container", "viewDiv");
};

export const App = () => {
useEffect(setup, []);
return (
<>
<div id="viewDiv"></div>
</>
);
};

環境

項目内容
React18.2.0
ArcGIS API for JavaScript4.24.7
TypeScript4.8.4
webpack5.74.0

動的な配置

MapViewがインスタンス化されたときにcontainerに指定した際、この時点で該当の HTML 要素がない場合は地図が表示されない。

React を利用している場合、コンポーネントがマウントされたタイミングで描画されるように、useEffect内でMapViewをインスタンス化すれば問題なく表示ができる。

containerプロパティ

サンプルコードなどでは下記のようにコンストラクタでcontainerに HTML 要素のid文字列を指定することが多い。

const mapView = new MapView({
// (略)
container: "viewDiv",
});

このとき、containerプロパティの型は string | HTMLDivElementとなっているため、TypeScript の型エラーは出ない。

(ヒントには__esri.DOMContainerProperties.container?: string | HTMLDivElementとでるが、該当の型はドキュメントには見つからなかった)

MapView | API Reference | ArcGIS API for JavaScript 4.24 | ArcGIS Developers - (developers.arcgis.com)

一方、MapViewがインスタンス化された場合、containerプロパティはDOMContainer.containerを指してしまい、指定できるのはHTMLDivElementのみとなる。

DOMContainer | API Reference | ArcGIS API for JavaScript 4.24 | ArcGIS Developers - (developers.arcgis.com)

そのため下記コードでは型エラーが発生してしまう。

const mapView = new MapView(...);
mapView.container = "viewDiv"

Type 'string' is not assignable to type 'HTMLDivElement'.ts(2322)

MapViewではなく、これが継承している__esri.Accessorsetメソッドを持っており、これを利用することで動的にcontainerを指定することができた。

Accessor | API Reference | ArcGIS API for JavaScript 4.24 | ArcGIS Developers - (developers.arcgis.com)

· 約4分
ogumaru

概要

ウェブブラウザの File API を利用して、ファイルのドラッグ & ドロップを利用した機能を実装している際、ファイルの検証を行っているにもかかわらず、エラーが発生した。

必ず失敗するわけではなく、たまに成功するため、動作の確認を行った。

原因

ファイルドロップ時にその中身が変わるのは、Wayland 環境における現状の挙動らしかった。

参照:

挙動の確認

環境

環境バージョン
OSUbuntu 22.04.1 LTS
ChromiunChromium 106.0.5249.119 snap
FirefoxMozilla Firefox 105.0.3
FilesGNOME nautilus 42.2

挙動の詳細と確認

デバッガを利用して確認を行った。

下記のようなコードにおいて、csv.getAsFile()nullになるために例外が発生していた。

体感として失敗する割合のほうが多いように感じる。

再現コード
// CSV形式かどうかの確認
const isCSVItem = (item) => {
const type = item.type;
const isCSV = [
type === "text/plain",
type === "text/csv",
type === "application/vnd.ms-excel",
type === "application/octet-stream",
].some((result) => result === true);
return isCSV;
};

document.body.addEventListener("dragover", (event) => {
event.stopPropagation();
event.preventDefault();
if (!event.dataTransfer) return;
event.dataTransfer.dropEffect = "copy";
});

document.body.addEventListener("drop", (event) => {
event.stopPropagation();
event.preventDefault();
if (!event.dataTransfer) return;

// item の確認
// 同じ操作でも異なる結果になる
for (const item of event.dataTransfer.items) {
console.log(item);
}

Array.from(event.dataTransfer.items)
.filter((item) => isCSVItem(item))
// ここで返り値が null になる
.map((csv) => csv.getAsFile())
.map((file) => {
if (!file) throw new Error("Failed to load csv file.");
const reader = new FileReader();
// file が たまに null になるため例外が発生する
reader.readAsDataURL(file);
reader.addEventListener("load", (event) => {
// ファイルに対する処理
});
});
});

成功する場合と失敗する場合を比較すると、上記コードのevent.dataTransfer.itemsの要素(DataTransferItem)の中身に違いがあり、下記 2 通りになっていることがわかった。

処理kindtype
成功filetext/csv
失敗stringtext/plain

Firefox ではこの問題は発生せず、kind: fileなものが取得できるため問題なく動作した。

対策

  1. ドロップされたデータのkindfileかどうか判定する
  2. input要素からファイルを選択して取得する
    input[type="file"]ではこの問題は発生しない
  3. ログイン時に [Ubuntu on Xorg] を利用する

今回は 1, 2 の対策を行って実装を行った。

· 約3分
ogumaru

概要

Prisma を利用して、 1:N のリレーションを持つ単純なレコードの値を配列として取得する。

結論

Proxyを利用する。

実行環境

実行環境バージョン
nodev16.15.0
prisma4.2.1
@prisma/client4.2.1

やりたいこと

LeavesとBranchesのER図

schema.prisma
generator client {
provider = "prisma-client-js"
}

datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}

model Branches {
id Int @id @default(autoincrement())
leaves Leaves[]
}

model Leaves {
id Int @id @default(autoincrement())
branch Branches @relation(fields: [branchId], references: [id])
branchId Int
value String
}

上図の単純な Branches:Leaves = 1:N となるテーブルに対して、Prisma で取得したオブジェクトに対し、

console.log(branches.leaves)
> ["hoge", "huga", ...]

のようにプロパティアクセスで子となるレコードの値のみを取得したい。

実際の挙動

Prisma ではレコードはオブジェクトとなる。

const select = async () => {
const branch = await prisma.branches.findFirstOrThrow({
include: { leaves: true },
});
return branch;
};

とすると下記のような形でレコードが返される。

{
"id": 1,
"leaves": [
{
"id": 1,
"branchId": 1,
"value": "hoge"
},
{
"id": 2,
"branchId": 1,
"value": "huga"
}
]
}

Proxy を利用したルーティング

leavesに対するアクセスを下記のように処理すると値の配列として取得できる。

interface ILeaf {
id: number;
value: string;
}

interface IBranch {
id: number;
leaves: Array<ILeaf>;
}

const select = async () => {
const proxy: ProxyHandler<IBranch> = {
get: (obj, prop) => {
if (prop === "leaves") {
return obj.leaves.map((leaf) => leaf.value);
} else {
if (Object.hasOwn(obj, prop)) {
return obj[prop as keyof IBranch];
} else {
return undefined;
}
}
},
};
const branch = await prisma.branches.findFirstOrThrow({
include: { leaves: true },
});
return new Proxy(branch, proxy);
};

leavesは単純な値の配列となる。

{
"id": 1,
"leaves": ["hoge", "huga"]
}

留意点

プロパティをプライベートにするものではないため、値の更新をする場合には注意が必要。

const records = {
private: [
{ id: 0, value: "secret" },
{ id: 1, value: "keys" },
],
};

const proxy = {
get: (obj, prop) => {
if (prop === "private") {
return obj[prop].map((record) => record.value);
} else {
return obj[prop];
}
},
};
const proxied = new Proxy(records, proxy);

console.log(proxied.private);
// > [ { id: 0, value: 'secret' }, { id: 1, value: 'keys' } ]

proxied.private = ["hoge", "huga"];

console.log(proxied.private);
// > [ undefined, undefined ]

· 約5分
ogumaru

概要

TypeScript にて、タプル風([T, T])の配列のコレクションがあり、これの中身をユニークにしたい場面があった。

Setを経由すれば重複排除できると思ったが、見た目上同じオブジェクトの重複は排除されなかった。

結論

JavaScript における配列の同値比較がfalseになるため。

厳密にユニークなコレクションを作成する場合は、deepEqual 相当の確認が必要そう。

実行環境

実行環境バージョン
nodev16.15.0
python3Python 3.10.4

JavaScript の挙動と Python との比較

タプル風の配列リテラルをaddすると、重複した値がそれぞれが追加されてしまう。

const unique = new Set();
unique.add(["hoge", "huga"]);
// Set(1) { [ 'hoge', 'huga' ] }
unique.add(["hoge", "huga"]);
// Set(2) { [ 'hoge', 'huga' ], [ 'hoge', 'huga' ] }

Python ではできた気がしたので確認する。

(なお["hoge", "huga"]Hashableでないためsetには追加できない)

unique = set()
unique.add(("hoge", "huga"))
# {('hoge', 'huga')}
unique.add(("hoge", "huga"))
# {('hoge', 'huga')}

リテラルのタプルに対して重複排除ができている。

JavaScript でも、オブジェクトが同一になるため、下記では意図した通りに重複が排除される。

const unique = new Set();
const tuple = ["hoge", "huga"];
unique.add(tuple);
// Set(1) { [ 'hoge', 'huga' ] }
unique.add(tuple);
// Set(1) { [ 'hoge', 'huga' ] }

JavaScript では Python におけるタプル相当のデータ型がないため、上記コードでは実際にはミュータブルな配列となる。

Object.freeze()でイミュータブルにしてみたが、結果は変わらなかった。

const unique = new Set();
unique.add(Object.freeze(["hoge", "huga"]));
// Set(1) { [ 'hoge', 'huga' ] }
unique.add(Object.freeze(["hoge", "huga"]));
// Set(2) { [ 'hoge', 'huga' ], [ 'hoge', 'huga' ] }

仕様の確認

MDN のSetのページを見ると、-0+0について触れられている。

See "Key equality for -0 and 0" in the browser compatibility table for details.

Set - JavaScript | MDN (developer.mozilla.org)

等価比較のページを見ると、

SameValueZero: used by %TypedArray% and ArrayBuffer constructors, as well as Map and Set operations, and also String.prototype.includes and Array.prototype.includes since ES2016

Equality comparisons and sameness - JavaScript | MDN (developer.mozilla.org)

SameValueZeroの TC39 へリンクされていたのでこちらも確認。

SameValueZero differs from SameValue only in that it treats +0𝔽 and -0𝔽 as equivalent.

SameValueZero | ECMAScript® 2023 Language Specification (tc39.es)

SameValue+0-0の比較結果のみ異なるとあるため、SameValueを確認すると、

  1. If Type(x) is different from Type(y), return false.

  2. If Type(x) is Number, then

    a. Return Number::sameValue(x, y).

  3. If Type(x) is BigInt, then

    a. Return BigInt::sameValue(x, y).

  4. Return SameValueNonNumeric(x, y).

とのことなのでSameValueNonNumericを確認。

  1. Assert: Type(x) is the same as Type(y).

  2. If Type(x) is Undefined, return true.

  3. If Type(x) is Null, return true.

  4. If Type(x) is String, then

    a. If x and y are exactly the same sequence of code units (same length and same code units at corresponding indices), return true; otherwise, return false.

  5. If Type(x) is Boolean, then

    a. If x and y are both true or both false, return true; otherwise, return false.

  6. If Type(x) is Symbol, then

    a. If x and y are both the same Symbol value, return true; otherwise, return false.

  7. If x and y are the same Object value, return true. Otherwise, return false.

タプルの場合7にて評価されると思われ、same Object valueではないということになるようだ。

same Object value とは

TC39にはこれ以上のリンクがなかったが、deepEqual相当の比較を行うことで重複の確認はできそう。

修正 PR いただけるとありがたいです。