aknow2

自分の興味のある事を連々と(プログラミング、モバイルアプリ、プログラミング教育)

【Elm】HTMLタグの標準のプロパティではなく独自のプロパティを設定方法

最近、関数型の勉強も兼ねてElmをいじっています。
HTMLのタグを使っていて独自のタグを設定する方法が分からなかったので調べました。
例えば素のHTMLだと、、、

<span target-data="hogehoge">
 fuga
</span>

target-dataがHTML標準には存在せず、独自に定義した箇所になります。
これを静的型付けであるElmで設定するにはどうするか。

Html.Attributesにあるproperty関数を用いる設定する事が出来ます。

import Json.Encode as JsonEncode
span [property "target-data" (JsonEncode.string "hogehoge") ]]

property の二つ目の引数はValue型である必要があるので, Json.Encodeを使ってStringから変換してあげる必要があります。
この様にpropertyを使う事で自由に独自のプロパティを設定する事が出来ます。

参考

https://package.elm-lang.org/packages/elm/html/latest/Html-Attributes#property

Dockerでnode.jsの開発環境でnode_moudlesが見えなくて困ってどうしたか

docker及びdocker-composeでnodejsの開発環境を構築しました。

最初はdockerfileでyarn installする構成で考えてました。
dockerfile及びcomposeはこんな感じ。

FROM node:10.14.2-alpine
WORKDIR /app
COPY package.json ./
COPY yarn.lock ./
RUN yarn install
COPY . /app
version: '3'
services:
  node:
    build: .
    volumes:
      - .:/app/
      - /app/node_modules
    ports:
      - "3000:3000"

dockerファイルでローカル側のpackage.jsonとyarn.lockをコンテナ側にコピーしてyarn installを実行 docker-composeでボリューム先をカレントディレクトリとnode_modulesを指定する。 ここでボリューム先にnode_modulesを指定していないと、マウント時にコンテナ内のnode_modulesフォルダが隠れて見えなくなる。
参照 - Lessons from Building a Node App in Docker Volume Trickの項目

これで問題なく、動くのだがローカル側からnode_modulesフォルダを見ると中身が見えず空っぽになってしまう。
f:id:aknow2:20190127115807p:plain
この状態だとコーディング時にvscode等のエディタ上でlintや参照エラーが大量に表示されて困るし、typescirptを使ってると補完も効かず散々な状態になる。

じゃあ、どうするか?

対応方法1

仕方ないのでlocal側でもnode環境を入れて,、yarn installする。
ただ、ローカル環境が汚染されてDocker入れた意味ないじゃんという感じになる。
敗北感がものすごい

対応方法2

マウント後のボリュームの変更はローカル側にも同期される。 この仕組みを利用してdockerfileでyarn installはせずに, docker-composeのcommandでyarn installするようにした。
起動時に毎回、yarn installが呼ばれてしまうが、2度目以降は差分の確認だけなので1秒ぐらいで終わる。
package.jsonが変更されても、docker-compse buildをしなくてもパッケージが更新されるのでこれで良しとした。
dockerfileとdocker-compose.yamlは以下のように変わります。

FROM node:10.14.2-alpine
WORKDIR /app
version: '3'
services:
  node:
    build: .
    volumes:
      - .:/app/
      - /app/node_modules
    ports:
      - "3000:3000"
   command: yarn install # <- これを追加

これでnode_modulesの中身も見える!やったー!

ただ、、、。

docker-compose側でyarn installしなきゃ駄目で、dockerのimageとしては環境が揃わないというのが、スマートじゃないなー感じてしまう。 悩ましい。。。

TypescriptのInterfaceにはinstanceofは使えない。

Typescriptには、JavaやC#と同じ様にinterfaceがあります。
が、JavaC#と同じ様に扱うと怪我をします。

instanceofでinterfaceは使えない。

最初に私がビックリした事です。JavaC#では'instanceof 'で型をチェックしたり、条件分岐で使えたりします。が、Typescriptでは使えません。 例えば、IActionを定義してActionというクラスで実装して見ます。それをinstaceof IActionでチェックしてみると、、、

interface IAction {
  dosomething(): void;
}

class Action implements IAction {
  dosomething() {
    console.log('hoge');
  }
}

if (Action instanceof IAction) {
  // something to do
}

VScodeを使っていれば、if文の'IAction'の箇所でインテリセンスによってエラーが出ます。

[ts] 'IAction' only refers to a type, but is being used as a value here.

訳すと、IActionは型としてしか参照出来ません、ここでは値として参照されています。。。
これはどういう事でしょうか?JavaC#脳の人にとっては混乱するエラー文です。
instanceof はタイプ、型で検査するのだから合っているのでは?値として参照とはどういう事?
コレが何故ダメなのか、すぐに分かる方法があります。 試しにIActionの例のif文の部分を削除して、Javascriptコンパイルしてみましょう。
すると↓の様になります。

var Action = /** @class */ (function () {
    function Action() {
    }
    Action.prototype.dosomething = function () {
        console.log('hoge');
    };
    return Action;
}());

なんと、Javascript変換すると'IAction'は存在しなくなっています!
これで何故interfaceinstanceofが使えないのか分かった様な気がします。
interfaceinstanceofで比較しようにも、Javascript上に存在しないので比較出来ません。 そして、型とはJavascriptに変換後と評価出来ないもの、値とはJavascriptに変換後も評価可能なものと言えます。

というわけで、どうあがいてもTypescriptのinterfaceではinstaceofを使った型チェックをする事が出来ません。

どうしてもinstanceofを使いたいという人はinterfaceではなくclassを使うしかないでしょう。classはjavascript変換後にも残りますし、Typescript上でも型としても認識されます。抽象classを上手く使えば良いかも。

abstract class BaseAction {
  abstract dosomething(): void;
}

class Action extends BaseAction {
  dosomething() {
    console.log('hoge');
  }
}

if (Action instanceof BaseAction) { // true
  // something to do
}

Typescript ブラケット記法(Object[key])でno index signatureエラーをtype safeに解決したい。

TypescriptでObjectに対して[文字列]でアクセスするブラケット記法を用いると発生するエラー。
例えば、この様に書くとエラーが出てきてコンパイルが通りません。

interface ISomeObject {
  firstKey:      string;
  secondKey:     string;
}

const obj = {
  firstKey: "a",
  secondKey: "b",
} as ISomeObject;

const key: string = 'secondKey';
const secondValue: string = obj[key];  // Element implicitly has an 'any' type because type '() => void' has no index signature.

ブラケット記法を使いたいため、keyに'secondKey'という文字列を指定しています。obj[key]でプロパティにブラケット記法でアクセスするとコンパイラは何型が返ってくるか推定出来ずにエラーとなります。
この現象に対して色々と回避方法があるので、まとめてみました。

その1・コンパイラオプションで蹴る。

↓をtsconfigに書き加えればOK。

--suppressImplicitAnyIndexErrors

でも、コレは使いたくない。  
コレを指定するとブラケット記法でプロパティにアクセスした場合にany型を許容することになります。そうなると何型の値が返ってくるか分かりません。つまり、type safeで無くなってしまう。type safeが利点であるTypescriptでanyを許容するのはいただけない。もし、keyをtypoしてしまったら実行するまでその間違いに気づけません。

その2・ interfaceを準備して[key: string]: <型>を追加する

次の解決策、ブラケット記法でアクセスした場合に返ってくる型をコンパイラに教えてあげる。
例えば、interfaceのプロパティに[key: string]: stringを追加すると、ブラケット記法でアクセスした場合、stringが返ってくるという意味になります。 こんな感じです。

interface ISomeObject {
    firstKey:      string;
    secondKey:     string;
    [key: string]: string; // <-この行を追加!
}

const obj = {
  firstKey: "a",
  secondKey: "b",
} as ISomeObject;

const key: string = 'secondKey';
const secondValue: string = obj[key]; 

だが、コレにもちょっと問題がある。
例で示したinterfaceに存在するプロパティはstring型のみ、普通は様々な型が入りますよね。
複数の型が存在する場合は複数の型を指定すれば一応、コンパイルは通ります。
例えば、string,number,booleanがinterfaceのプロパティに存在する場合、、、

interface ISomeObject {
    firstKey:      string;
    secondKey:     number;
    thirdKey: boolean
    [key: string]: string|boolean|number;  //<-or条件で型を指定 
}

const key: string = 'secondKey';
const secondValue = obj[key]; // secondValue => string|boolean|number型

コンパイルは通りますが, secondValueはnumber型ではなく、string|boolean|number型となってしまいます。
結局、ブラケット記法でアクセスすると何型が返ってくるか分かりません。その1の解決策と同じ様な状況になってしまいます。
この方法を取れば、影響範囲はinterfaceを実装しているオブジェクトのみで、型もある程度は推定出来るので、その1の方法よりかは幾分マシですが完璧とは言えません。

その3・keyofを使う。

前提を一つ壊すことになりますが、そもそも、keyをstring型にしているのが間違いだよ!という話。
const key: stringとしていた箇所をconst key: keyof ISomeObjectに変えてみましょう。
こうする事でkeyはISomeObjectのプロパティであるfirstKey,secondKey,thirdKeyのいずれかの文字列が入るstring literal typeに変身します。

interface ISomeObject {
  firstKey:      string;
  secondKey:     number;
  thirdKey:      boolean;
}

const obj = {
  firstKey: "a",
  secondKey: 2,
  thirdKey: false
} as ISomeObject;

const key: keyof ISomeObject  = 'secondKey';
const secondValue = obj[key]; // secondValueがnumber型に!

こうすれば、secondValueがちゃんとnumber型として認識してくれます!
また、keyをtypoしてもコンパイラが間違っている事を教えてくれます。
こんな感じに、、、。

const key: keyof ISomeObject  = 'scdKey';  //  Type '"sndKey"' is not assignable to type '"secondKey" | "firstKey" | "thirdKey"'.

また、ジェネリクスを使って、type safeにブラケット記法が使える関数を作るとこんな感じになります。

const accessByBracket = <S, T extends keyof S>(obj: S, key: T) => {
  return obj[key];
};

const obj = {
  firstKey: "a",
  secondKey: 2,
  thirdKey: false
} as ISomeObject;

const value = accessByBracket(obj, 'secondKey'); // value => 2

まとめ

コンパイルオプションをいじったり、interfaceにプロパティを追加する前にkeyofを上手く使って型安全な設計に出来ないか検討しましょう。

【React Native(RN)】RNのプロジェクトフォルダ内に、別のNode.jsプロジェクトを作るとRNのビルドエラーになる

どんな時に発生するか

React Nativeのプロジェクトフォルダ内に、別のNodejsプロジェクトを作った場合に発生します。
私の場合、Node.js for MobileというスマホアプリからNode.jsが実行できるモジュールを使った際に発生しました。

現象

以下のエラーログが吐かれRNのビルドに失敗する。

jest-haste-map: @providesModule naming collision: Duplicate module name
なぜ起きるのか

ReactNativeで使用しているMetroに原因がある。Metroはnpmのグローバル環境に通常はインストールされ,ソースコードの変更をウォッチしている。なので、ビルドする時にRNのプロジェクトルートにあるpackage.jsonとnode_modulesだけでなく、子フォルダにあるpackage.jsonやnode_modulesも見に行き名前の衝突が発生してビルドエラーになる。

対策

metroに特定のフォルダを無視する様に設定する。
手順 RNのプロジェクト直下にrn-cli.config.jsを作成する。
rn-cli.config.jsに以下の様に記述する。

const blacklist = require('metro').createBlacklist;
module.exports = {
    resolver: {
      blacklistRE: blacklist([
        /無視したいフォルダ名\/.*/,
      ])
    },
  };

無視したいフォルダ名を問題の起きているフォルダ名に変更すれば出来上がり、これでビルドは通るはず。

Logcatを使って実機Androidのデバッグログをウォッチする

CordovaやReactNativeなどXプラットフォームな開発をしていて シミュレータでは大丈夫だったのに実機にインストールして起動すると何も言わずにアプリが落ちた!
そんな時はlogcatを使ってログを見てみると解決する糸口が見つかったりします。
logcatを実行するのは簡単

adb logcat

が、logcatをそのまま起動すると大量のログが吐かれ、自分のアプリのログがどこにあるか分かりません。 アプリのパッケージ名を指定して以下のコマンドを実行すれば、特定のアプリのみのログが出力される様になります。

adb shell 'logcat --pid=$(pidof -s <アプリのパッケージ名>)'

ReactNativeで簡単なアプリを作ってExpoとReactNativeCLIの比較をしてみた

作ったもの

食材毎に大さじとか小さじをグラムに変換するアプリ
既に似たような物が出ているけど、家族からの要望で作ることにした。
折角なので、前から気になっていたReactNativeで作ってみる事にした。
ちなみにReactjsは使ったことはありますが、ReactNativeは初めて触りました。

大さじ?g

大さじ?g

  • tomoaki kanayama
  • Food & Drink
  • Free

play.google.com

課題を作る

ただ作るだけでは一瞬で作れそうで面白くないので、知見を深めるために課題を作った。

  • Expo とReactNative CLI両方を試す。
  • 広告を乗せる(Admob)
  • Typescriptで書く

作った感想

ネイティブモジュールを使わないのであれば、Expoが断然、楽です。今回作ったアプリもネイティブモジュールを使わないアプリなので、Expoでまず作ってみましたが環境構築から実装完了まで2日ぐらいで出来ました。React.jsと殆どのかわらない感じなので、迷うことなく作る事が出来ました。また、リリースビルドもiOS側でXcodeを使わずにビルド出来てしまうので、マジに楽で感動します。 一方、次にReactNative cliを利用した方では結局、3日近くかかりました。アプリのソースコードはExpoアプリからコピペで移したのにです。なので、もしコーディングから始めていたら実質一週間ぐらいはかかった事になると思います。

react native cliでは何にそんなにハマったのか、まず環境構築でハマる。react-native initで吐き出されたテンプレートプロジェクトをそのまま実行しても動かない。。。issueを漁りversion固有の問題だと分かり何とか動来ましたが、最初にコードを追ってバグを探していたので時間がかかりました。無駄に時間を浪費するだけなのでReactNativeで上手くいかないと思ったら、まずはGithubのissueをチェックした方がいいです。その他には、admobのプラグインもすんなり動作せず、これもgithub issueを漁り解決しました。Expoで作っている時はあまり感じなかったのですが、ReactNativeってやっぱりまだまだ不安定な要素があるのだなと痛感しました。おかげでAndroidiOSのビルド周りに詳しくなりましたが、、、。

対して、ExpoではExpo kitで提供しているAPIであれば、ほぼ問題なく動作する安心です。Expoで作っている時にはCliで作っていた時の様な解読するのが困難なエラーが出て困った事は起こらなかったです。

また、デバッグに関してもExpoの方が素早く軽快にデバッグできました。まず、Expoではnativeのビルドが無くJSをExpoアプリへバンドルするだけなのでビルドが圧倒的に早いです。CLIで作る場合はネイティブも含めビルドされるので、結構時間がかかる。また、ホットリロードも上手く行かなくなる事があり一回アプリを落として再ビルドというパターンもCLIで作っている時は良くありました。

結論

という事で在り来たりな結論ですが、expoのAPIにはカメラやセンサー類、プッシュ通知も提供しているので、APIを眺めて要件に足りているのであればExpoで作らない理由はないと思います