20200323のJavaScriptに関する記事は30件です。

初心者がElectron(NW.js)を始めてみて分かったことまとめ

目次

  1. はじめに
  2. 個人的にHTA(WSH?)とNW.jsはオススメしません
  3. electron開発の流れ
    1. プロジェクトフォルダ(名前は何でもOK:ここではmoge)の準備
    2. 中身を編集する
    3. moge/package.jsonを編集する
    4. electronとelectron-builderのインストール
    5. ビルド
  4. electron-builderでビルドしようとするとこのシステムではスクリプトの実行が無効になっているため、~~~というエラーが出るとき
  5. 1つのプロジェクトを再ビルドしたいとき
  6. おわりに

はじめに

突然ですが、絵を描く用の画像資料が10GBを超えてしまいました。
何かいい画像ビューアないかなと探していたのですがイマイチだったので、自分で作ってみようと思いました。
HTML/CSSは少しやったことがあったので、ちょうどいいやということでelectronを始めてみました。?

あとこの記事ではwindows10のみを対象にしています。
macとかlinuxは使ったことがないので分かりません。すみません。
もちろんプログラム初心者です。間違いがあったら教えてください。

さて、googleでelectronについて検索してもヒットするのは古い情報が多いですよね。
(そのせいで合計2日分くらい無駄にしてしまいました)
私みたいな人を増やさないために、今の時点で分かったことを初心者目線でまとめておきます。
(あと頭の中身を整理して今抱えているエラーを解決するため)

個人的にHTA(WSH?)とNW.jsはオススメしません

画像ビューアを作ろうとしたとき、まずはこちらの記事(簡単なウインドウアプリ(GUI)開発のまとめ(Windows偏))を参考にさせていただきました。

まずは一番上にあったHTA(WSH?)を試してみたのですが、

  • 古い技術だからサポート切ってるブラウザが多いらしい?(要検証)
    • 私が試したときはIEでだけ動きました。
    • しかも毎回ポップアップで出てくるやつを許可しなきゃダメでちょっとメンドイ
  • ネット文献はそこそこヒットするけど、どれも文が硬く初心者向けではない

次にNW.jsを使ってみたのですが、

  • electronに比べユーザーが少ない
    • つまり文献も少ない(こういうエラーが出ました、など)
  • electronに比べてお手軽とあるが、やってみるとほとんど同じくらいの労力がかかる
  • なぜかビルドできない(これは個人の能力不足)

で、electronに至りました。electronは、

  • そこそこ文献がある
    • あるけどどれもレベルが高くて初心者が理解するには苦労する必要がある(でもないよりマシ!)
    • あと書籍としての文献もそこそこあるらしい(少し古いけど)
  • バージョンによって有効な書き方が違うことがあって少し大変

みたいな感じです。
個人的に積極的に使いたい子ではないです。
他のはウーンって感じだしC#はハードル高そうだしってなって消極的に採用しました。

他に何かよさげなのあったら教えてください!

electron開発の流れ

まずはNode.jsをインストールしてください。

  1. ダウンロードする(私はWindows Installer (.msi)の64-bitをダウンロードしました)
  2. インストーラの指示に従ってインストールする
  3. 完了!

次におおまかにelectron開発の流れをなぞるとこんな感じです。

  1. プロジェクトフォルダ(名前は何でもOK:ここではmoge)を作る
  2. mogeの中に必要なフォルダとファイルを作る(main.js, index.html, src/package.jsonなど)
  3. moge/package.jsonを編集する
  4. electronとelectron-builderをインストールする
  5. ビルドする

次で詳しく触れていきます。

プロジェクトフォルダの準備(名前は何でもOK:ここではmoge

適当なとこにmogeファイルを作ります。
mogeの中にsrcという名前のフォルダとpackage.jsonを作ります。
この段階では中身は空でOKです。
srcの中に、main.js, index.html, package.jsonを作ります。
これも中身は空でOKです。

プロジェクトフォルダ
moge
 ├ src
 │ ├ main.js
 │ ├ index.html
 │ └ package.json
 └ package.json

package.jsonが2つありますが、src直下の方をsrc/package.jsonmoge直下の方をmoge/package.jsonと呼ぶことにします。
(これ何で名前同じの2つ作らせる仕様なんですかね?ややこしい!)

色んなの資料読んでるとどっちのpackage.jsonを指してるのか分かりにくくて初心者的には大変です。
体感ですが、moge/package.jsonの方を指してることが多いと思います。ていうか多分そう。

中身を編集する

まずはmain.jsからです。
main.jsは、どのプロジェクトでもほとんど同じようなものになるらしいです。
必要に応じて書き足したりするらしいですが、私にはよく分かりません。

公式ドキュメントの読み方が分からない...!?‍♀️

main.js
const { // アプリ作成用のモジュールを読み込む
  app,
  BrowserWindow,
  Menu
} = require('electron');

let mainWindow;  // メインウィンドウ

function createWindow() {
  mainWindow = new BrowserWindow({  // メインウィンドウを作成する
    width: 960, // 横幅
    height: 540,  // 縦幅
    alwaysOnTop: true,  // 常に最前面
    webPreferences: {
      nodeIntegration: true // 必要なものらしい
    }
  });

  process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = '1'; // 警告(Electron Security Warning)が出なくなる
  mainWindow.loadFile('./src/index.html'); // メインウィンドウに表示するやつを指定する
  Menu.setApplicationMenu(null); // メニューバー削除
  mainWindow.webContents.openDevTools(); // デベロッパーツールの起動

  mainWindow.on('closed', function() {
    mainWindow = null; // ×が押されたらメインウィンドウを閉じる
  });
}
app.on('ready', createWindow);  // 初期化が完了したらcreateWindow()を呼び出す
app.on('window-all-closed', function() {  // 全てのウィンドウが閉じたら
  if (process.platform !== 'darwin')
    app.quit();  // macでない限りアプリを終了させる
});
app.on('activate', function() { // アプリがアクティブになったら
  if (mainWindow === null)
    createWindow(); // メインウィンドウが消えてたらもう一回メインウィンドウを作る
});

// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.

次はindex.htmlです。
今回はCSSで装飾してJSのログとHTMLのpを使ってみました。

html
<!DOCTYPE html>

<html>

<head>
  <title>moge</title>
  <style>
    body {
      background-color: #e5ddb0;
      padding: 20px;
      width: 100%;
      max-width: 800px;
      margin: 0 auto;
    }
  </style>
  <script>
    // メイン
    function main() {
      console.log("JSのテスト");
    }
  </script>
</head>

<body onLoad="main()">
  <p>HTMLのテスト</p>
</body>

</html>

最後にsrc/package.jsonです。
エッこれだけ!?って感じですよね。
src/package.jsonも、どのプロジェクトでもほとんど同じようなものになるらしいです。

src/package.json
{
  "main": "main.js"
}

moge/package.jsonを編集する

基本的にはこんな感じらしいです。

moge/package.json
{
  "name": "moge",
  "version": "1.0.0",
  "description": "",
  "main": "./src/main.js",
  "scripts": {
    "start": "electron ."
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "build": {
    "productName": "moge",
    "appId": "moge",
    "win": {
      "target": "portable"
    }
  }
}

"target": "portable"は、ビルドするときはポータブルアプリになるようにしてねって意味らしいです。
ポータブルアプリは、インストールしなくても使える.exeのことらしいです。

これでファイルの準備はおしまいです!

electronとelectron-builderをインストールする

エクスプローラでmogeフォルダの余白をshift + 右クリックして、PowerShell ウィンドウをここで開く(S)を選択してください。
(どうでもいいですけどPowerShell ウィンドウ「で」ここ「を」開く(S)じゃないですかね?)

そしたら紺色のウィンドウが出てくるはずです。
これはPowerShellというもので、コマンドを打ち込むとwindowsに命令を下せます。
electronの母体であるNode.jsは、モジュール?をインストールするのにこのPowerShellを使う必要があります。
よく見る.exeのインストーラーは使わないらしいです。

そしたら、まずはnode -vを実行してください。
下みたくなったらNode.jsのインストールに成功していることが確認できます。

node -v
PS D:\electron\moge> node -v
v12.16.0

確認できたら、npm i -D electronnpm i -D electron-builderを実行してください。
どちらも少し時間がかかります。
これは、electronとelectron-builderをmogeフォルダにインストールするという工程です。
-D-gにすることで、グローバルなインストールができるらしいですが、electronはローカルにインストールするのがいいらしいです?
下みたくなったらOKです。

npm i -D electron
PS D:\electron\moge> npm i -D electron

+ electron@8.0.2
added 85 packages from 93 contributors and audited 102 packages in 10.474s
npm i -D electron-builder
PS D:\electron\moge> npm i -D electron-builder

+ electron-builder@22.3.2
added 138 packages from 107 contributors and audited 770 packages in 15.356s

このとき、プロジェクトフォルダは次みたくなってるはずです。
追加されたnode_modulespackage-lock.jsonは何もしなくてOKです。

プロジェクトフォルダ
moge
 ├ node_modules
 ├ src
 │ ├ main.js
 │ ├ index.html
 │ └ package.json
 ├ package.json
 └ package-lock.json

確認できたら、npm startを実行してください。

npm start
PS D:\electron\moge> npm start

> moge@1.0.0 start D:\electron\moge
> electron .

少し待つとこんな感じのウィンドウが出てくるはずです!
無題.png

テンション上がりますね!うおおお?
このままだとNode.jsやelectronがインストールされている環境でPowerShellから呼び出さないと起動できませんが、ビルドするとダブルクリックひとつ(?)で起動できるようになります!
次でビルドしていきます。

ビルドする

ここによると、PowerShellはセキュリティの都合でよく分からないコマンドを実行できないような設定になってるらしいです。
electron-builderはその「よく分からないコマンド」に属しているらしく、初期設定のままだと実行できません。
なので、さっきの記事に従って設定を変更しましょう。
ちなみに私はSet-ExecutionPolicy RemoteSignedを実行しました。

そしたら、electron-builder build --winを実行しましょう。
この処理は1~2分くらいかかります。
下みたくなったら成功です。

electron-builder build --win
PS D:\electron\moge> electron-builder build --win
  electron-builder  version=22.3.2 os=10.0.17763
  loaded configuration  file=package.json ("build" field)
  description is missed in the package.json  appPackageFile=D:\electron\moge\package.json
  writing effective config  file=dist\builder-effective-config.yaml
  packaging       platform=win32 arch=x64 electron=8.0.2 appOutDir=dist\win-unpacked
  default Electron icon is used  reason=application icon is not set
  building        target=portable file=dist\moge 1.0.0.exe archs=x64

このシステムではスクリプトの実行が無効になっているため、~~~みたいなエラーが出た方は、上の記事に従って再度PowerShellの設定を変更してみてください。

ビルドに成功したら、mogeフォルダにdistというフォルダが追加されているはずです。

プロジェクトフォルダ
moge
 ├ dist
 ├ node_modules
 ├ src
 │ ├ main.js
 │ ├ index.html
 │ └ package.json
 ├ package.json
 └ package-lock.json

そしたらdistの中のmoge 1.0.0.exeを起動してみましょう!
さっきのnpm startと同じウィンドウが出てきましたね!
ddba20001abfb7f026aa13144984f2de.jpg

成功です?

以上がelectron開発の流れになります。
(長かった…)

1つのプロジェクトを再ビルドしたいとき

単純にもう一回electron-builder build --winを実行するとエラーが出ることがあります。
そのときは、

  • distフォルダを削除する
  • C:/Users/(Username)/AppData/Roaming/mogeを削除する

で再ビルドできます。
もしフォルダを削除しようとして使用中なので削除できませんというエラーが出たときは、

  • moge 1.0.0.exeを閉じる
  • タスクマネージャーでelectronアイコンのやつを全て閉じる

をするとフォルダを削除できるようになります。
(electron-rebuilderとかいうやつがあるとかないとか聞きますが、使い方が分かりません?‍♀️)

おわりに

私が分かっていることは以上です。

こうして頭の中を整理してみましたが、今抱えている
「ビルドしたあとに外部ファイルを読み込ませるにはどうしたらいいか」
を解決する糸口は全くつかめませんでした?

どなたか助けていただけると幸いです…
「公式ページのここに書いてありそう」とかでも構わないので教えてください…

で、初心者の方にも分かりやすい構成を目指して書いてみたのですが、どうだったでしょうか(冗長すぎたかも)。
こんな記事でしたが、最後まで読んでいただいてありがとうございました!?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【学習記録】『スラスラ読める JavaScriptふりがなプログラミング』読書感想  4章編 (前半) 〜N予備校の復習を添えて〜

【学習記録】『スラスラ読める JavaScriptふりがなプログラミング』読書感想  3章編( https://qiita.com/Molly95554907/items/dc1654b7019442f893fa ) の続き

第四章 関数を作ろう

・アロー関数

オリジナルの関数を作成する。

let 関数名 = () => {
//関数として実行する処理
};

以上で作成完了。

関数名();

を実行すると、//関数として実行する処理 が実行される

・引数を受け取る関数

・引数とは:関数の中で処理される変数のこと、と理解してる。

let 関数名 = (引数となる変数) =>{
//関数として実行する処理
console.log(引数となる変数 +'です');
};

以上で作成完了

引数となる変数に文字や数値を代入できる
関数名('山田');

山田です     

が出力される。

・function文で関数を作る

function 関数名(引数となる変数){
//関数として実行する処理
}

※function文で関数を作る時は、関数の終わりに ; が要らない!!!

今後はアロー関数が主流になっていく流れ!

【N予備校では一章「あなたのいいところ診断」から登場】

スクリーンショット 2020-03-23 22.42.16.png

function文でremoveAllChidren関数を作成。診断するたびに要素が増えていくのを消している。
アロー関数で、userNameInput.onkeydown関数を作成。キーを押した時に起こるイベントを関数で作成している。
アロー関数で、assessmentButton.onclick関数を作成。クリックした時に起こるイベントを関数で作成している。

・アロー関数を使って関数を作成すると、「オブジェクト.プロパティ」で関数を作成することができる!!!

・テンプレート文字列

バッククオート(`)で囲んだ範囲に書いた文字列のこと。
この範囲内での文字、改行、スペースは、そのままプログラムの実行結果に反映される。

テンプレート文字列の中で変数を書く時は 

${変数名}

と書く
・関数には複数の引数を渡すことができる

let recommendBook = ( title, pages ) => {
let book = 私のおすすめの本は${title}、
ページ数は${pages}です。
;
console.log(book);
};

recommendBook('デカメロン', 1325);

私のおすすめの本はデカメロン、
ページ数は1325です。

※関数内で作った変数は「ローカル変数」
関数外では呼び出せない。

ここでは let book を、let recommendBook関数の外で呼び出そうとしてもundefinedになる、ということ。

・テンプレート関数を使わず書く
  + を使って出力する物を書いていく。改行は「\n」で行う。   let recommendBook = ( title, pages ) => { let book = '私のおすすめの本は' + title + '、\n' + 'ページ数は' + pages + 'です。'; console.log(book); };
【N予備校では、三章「集計処理を行うプログラム」などで登場】

スクリーンショット 2020-03-23 23.26.29.png

テンプレート関数を使わずに、出力結果を書いている

スクリーンショット 2020-03-23 23.28.10.png

同じ出力結果を、テンプレート関数を使って書いている

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

while文

while文とは

ループ処理を行う場合に使用する構文の1つです。
特徴として、
ループ処理の回数の記述がないので、
無限ループにならないように、
必ずどこかで条件がfalseになるようにします。

記述方法

下記をご覧下さい↓↓


while (条件式) {
繰り返す処理
}

繰り返す条件(条件式)の結果がtrueの間{ }ブロック内の処理を繰り返します。

+1を繰り返す記述例を見ていきましょう↓↓

1.var cnt = 1;
2.while ( cnt<50 ) {
3.  document.write(cnt);
4.  cnt = cnt + 1;
}

//1 2 3  ...47 48 49

処理順として、

1.変数cntに1が代入されます。
2.cntが50未満か比較します(条件式)。
3.cntの値がdocument.writeに、表示されます。
4.cntの値に1が加算されます
5.条件式の結果が50(false)になるまで2~4が繰り返されます。
6.条件式の結果が50(false)になると処理を終了します。

まとめ

while文には、
処理の繰り返し回数の指定がないので、
必ず、falseを記述をする。

本日は、ありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ReactにおけるDIについて

はじめに

以前、ReactでDIはどうやるのか社内で話題になったので、色々調べてまとめておきます

Inversify

inversifyではDI用のコンテナーを作成できます

公式

README通りですが、紹介します

packageをインストールします

npm install inversify reflect-metadata --save

デコレータや追加の型のために以下の項目をtsconfig.jsonに適宜追加します

tsconfig.json

{
    "compilerOptions": {
        "target": "es5",
        "lib": ["es6"],
        "types": ["reflect-metadata"],
        "module": "commonjs",
        "moduleResolution": "node",
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true
    }
}

インターフェイスを定義します

interfaces.ts

export interface Warrior {
    fight(): string;
    sneak(): string;
}

export interface Weapon {
    hit(): string;
}

export interface ThrowableWeapon {
    throw(): string;
}

これを元に本番の実装やモックの実装をします

次に識別子を作ります

TSのInterfaceは実行環境に影響を及ぼせないので、この識別子とinterfaceを後に結びつけてDIに使用します

types.ts

const TYPES = {
    Warrior: Symbol.for("Warrior"),
    Weapon: Symbol.for("Weapon"),
    ThrowableWeapon: Symbol.for("ThrowableWeapon")
};

export { TYPES };

次に実装をします、インジェクトするものは@injectableデコレータを

インジェクトするときには@injectデコレータを使います

@injectデコレータに先ほど作成した識別子を渡します

そうすることでインジェクトされます

entities.ts

import { injectable, inject } from "inversify";
import "reflect-metadata";
import { Weapon, ThrowableWeapon, Warrior } from "./interfaces";
import { TYPES } from "./types";

@injectable()
class Katana implements Weapon {
    public hit() {
        return "cut!";
    }
}

@injectable()
class Shuriken implements ThrowableWeapon {
    public throw() {
        return "hit!";
    }
}

@injectable()
class Ninja implements Warrior {

    private _katana: Weapon;
    private _shuriken: ThrowableWeapon;

    public constructor(
        @inject(TYPES.Weapon) katana: Weapon,
        @inject(TYPES.ThrowableWeapon) shuriken: ThrowableWeapon
    ) {
        this._katana = katana;
        this._shuriken = shuriken;
    }

    public fight() { return this._katana.hit(); }
    public sneak() { return this._shuriken.throw(); }

}

export { Ninja, Katana, Shuriken };

最後にコンテナーを作ります

inversify.config.ts

import { Container } from "inversify";
import { TYPES } from "./types";
import { Warrior, Weapon, ThrowableWeapon } from "./interfaces";
import { Ninja, Katana, Shuriken } from "./entities";

const myContainer = new Container();
myContainer.bind<Warrior>(TYPES.Warrior).to(Ninja);  // ←先ほど作った識別子を使って実装とインターフェイスが結びつけられる
myContainer.bind<Weapon>(TYPES.Weapon).to(Katana);
myContainer.bind<ThrowableWeapon>(TYPES.ThrowableWeapon).to(Shuriken);

export { myContainer };

以下のように利用します

index.ts

import { myContainer } from "./inversify.config";
import { TYPES } from "./types";
import { Warrior } from "./interfaces";

const ninja = myContainer.get<Warrior>(TYPES.Warrior);

expect(ninja.fight()).toBe("cut!");

これを適当にContextAPIか普通にimportでcomponentで使えるようにすればDIの完成です

テストするときはモックをbindをしなおせばモックがインジェクトされます

ContextAPi

ContextAPIはシンプルです

import React, { createContext, useContext, useMemo } from 'react';
import { AuthApi } from '../../../lib/api/AuthApi';
import { SampleApi } from '../../../lib/api/SampleApi';
import _ from 'lodash';
import { DeepPartial } from 'redux';


// インジェクトしたいもの集まりの構造を定義
export type Dependencies = {
  api: {
    auth: AuthApi;
    sample: SampleApi;
  };
};


// 構造に合わせてデフォルトの値を作る
export const defaultDependencies: Dependencies = {
  api: {
    auth: new AuthApi(),
    sample: new SampleApi(),
    productionNumber: new ProductionNumberApi()
  }
};


// Context作成
const DIContext = createContext(defaultDependencies);


// Providerを作成、propsとdefaultをマージして注入
export const DIProvider: React.FC<{
  dependencies?: DeepPartial<Dependencies>;
}> = ({ children, dependencies = {} }) => {
  const newValue = useMemo(
    () => _.merge({}, defaultDependencies, dependencies),
    [dependencies]
  );
  return <DIContext.Provider value={newValue}>{children}</DIContext.Provider>;
};

type MapDepToProps = (dep: Dependencies) => any;


// 使いやすいようにcustom hookを作成
export const useDICotext = <T extends MapDepToProps>(selector: T) => {
  const context = useContext(DIContext);
  return useMemo<ReturnType<T>>(() => {
    return selector(context);
  }, [selector, context]);
};


これだけ作って

使いたい側で以下のようにします

const authApi = useDIContext(dep => dep.api.auth)

完璧ですね

Misc

reduxのmiddleware系はContextAPIの外側でかつ、関数のみで構成されているためinversifyが使えず、上記のDIが使えません

なのでmiddlewareは引数で渡す形になります

redux-thunk

redux-thunk(以下thunk)でDIするときはextra argumentsという機能を使います

thunkではmiddlewareにwithExtraArgumentという関数が生えています

これに渡した引数が、thunkの関数のdispatch,getState,につぐ第3の引数に渡されます↓

thunkMiddleware.withExtraArgument(dependencies)



const actionCreator = ()=>(dispatch, getState, dependencies)=>{
   dependencies.api().then(()=>{
     dispatch(action)
   }
}

redux-saga

redux-saga(以下saga)では特殊な機能はなく、愚直に引数に渡し続けます

task.run(saga, dependencies)



function* saga(dependencies){
  yield fork(saga2, dependencies)
}



function* saga2(dependencies){
  yield call(dependencies.api)
  yield take(saga3, dependencies)
}

Conclude

DIも問題なくできますね

Jestのモック機能は優秀なので必要ないかと思いますが、あったらあったで便利かもしれないですね

2020/12/4追記
storybookのstoryを書くときにめちゃくちゃ便利でした

Jest

おまけです

本来Reactのテストでモックするのはjestです

DIする必要はありません

なので基本これを使いましょう

jestでmockする方法はいくつかあります

  • moduleのモック
  • objectのメソッドのモック

基本mockするためのapiはここを参照して下さい

moduleのモック

これは2つ方法があります

  • __mocks__ディレクトリを使う
  • jest.mockを使う

__mocks__ディレクトリを使う

└── models
    ├── __mocks__
    │   └── user.js
    └── user.js

このような構成でモックのコードを置きます

そしてテストコードの中で以下のコードによりmockを使うことを明記すれば、user.js__mocks__/user.jsとしてimportされます

jest.mock("path/to/user")

モックファイルの中からrequireActualを使うと本物のモジュールが、genMockFromModuleで自動モックされたモジュールを持って来れるので、虚実組み合わせて使うこともできます

あらかじめテスト用のモジュールを用意したい時に、この__mocks___ディレクトリが利用されます

jest.mockを使う

jest.mock("path/to/user",()=>{
  return {
  foo: jest.fn()
}
})

これでモックされます

テストコード側でモックコードを指定したい時に利用します

objectのメソッドのモック

jest.fnをそのままobjectのメソッドに代入します

もしモックの実装をしたい場合、実装の関数を引数として渡すか、mockImplementation関数をチェーンして、その引数に実装の関数を渡すことができます

[https://jestjs.io/docs/ja/jest-object#jestspyonobject-methodname]

spyOnの説明文で言明されてるように、 オブジェクトのメソッドの置き換えは問題ない操作だと思われます

TS的にはNGかもしれませんが、JS的やJest的には問題ないので、ガンガン代入しちゃってください

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【学習記録】『スラスラ読める JavaScriptふりがなプログラミング』読書感想  3章編 〜N予備校の復習を添えて〜

【学習記録】『スラスラ読める JavaScriptふりがなプログラミング』読書感想  1、2章編(https://qiita.com/Molly95554907/items/90194e1cd5631dc927aa) の続き

第三章   ~繰り返し文~

・while文

・while文は回数が 決まっていない 繰り返しに向いている
・for文は回数が 決まっている 繰り返しに向いている

while( 継続条件式or関数 ){
繰り返す処理
};

継続条件式or関数がtrueである限り繰り返す。

・代入演算子で式を短縮

この短縮方法、感覚的に掴みづらいので早く慣れたい。

a=a+10 → a+=10
a=a-10 → a-=10
a=a*10 → a*=10
a=a/10 →a/=10

・for文

for( let i = 100; i >= 0; i-- ){
console.log( i + '日後に死ぬワニ' );
}

これはだいぶ慣れてきた、はず。

・繰り返しからの脱出と、繰り返しの先頭に戻り継続

・break; は繰り返し文を中断するためのもの。
・continue; は繰り返しの処理を1回スキップするためのもの。

 繰り返し処理の、while文やfor文の中で使える。

while(継続条件){
if(脱出条件){
break; ←繰り返し文を中断
}
if(スキップ条件){
continue;  ←条件に当てはまるものをスキップ
}
}

・疑問点

条件分岐のswitch文に登場するbreak;と本質的には同じもの?
どちらも処理を止めて文から脱出するためのものだし。

switch(式){
case 値1:
//処理1
  break;

case 値2:
//処理2
break;

default:
//処理3
  break;
}

↑(式)の結果が値1なら処理1、値2なら処理2、その他なら処理3

【N予備校では第三章・掲示板作成で登場】
スクリーンショット 2020-03-23 20.20.31.png

(復習)
ここでは、HTTPから与えられるパスがどれかによって、実行する関数を振り分けていた。

・配列

配列とは、変数を箱として、中に値を入れられる仕組み…と今のところ理解してる。

let dog = ['柴犬', 'プードル', 'サモエド犬'];

dogという変数の中には'柴犬','プードル','サモエド犬'という値が入る。
要素とは、変数の箱の中にしまわれている値のこと。
インデックスとは、要素(箱の中にしまわれた値)に振り当てられた番号。

dog[0]='柴犬'

【N予備校 第一章いいところ診断の配列】

スクリーンショット 2020-03-24 0.51.12.png

変数answerの中に、配列として診断結果を入れる

スクリーンショット 2020-03-24 0.53.24.png

添字(インデックス)から値を取り出す。

・配列を使ってできる操作

・配列.length → 配列の要素の数を返す
・配列.push() → 配列の末尾に要素を追加する

・配列.pop() → 配列の最後の要素を削除して返す
・配列.shift() → 配列の最初の要素を削除して返す

・配列.remove(値) → 配列から、指定した要素を取り除く
・配列.sort() → 配列の要素を並べ替える

ここでは、配列がオブジェクト
     操作がメソッド
     ()がプロパティ

オブジェクトはメソッド(機能)プロパティ(変数)の集合体。
ということは、ここでの「オブジェクト」である「配列」を厳密に定義すると、操作されたあとの結果として残る「配列」ってこと??
う〜〜ん、よくわからない、今後の課題。

・for〜of文

★ここら辺から定着度が怪しいのでしっかり理解!

for~of文は、

配列から1要素ずつ順番に取り出して繰り返しを行う文!!

let dailyLunch = ['うどん','ラーメン','カレー','蕎麦','パスタ'];
for(let lunch of dailyLunch){
console.log('今日のお昼は' + lunch);
}

を実行すると「今日のお昼はうどん」から「今日のお昼はパスタ」までがコンソールに表示される。

ofの前に作った新しい変数に、変数(配列)の中の値を1つづつ順番に入れていく。

ピンと来なかったけど、英語に立ち返ったら理解しやすい。
【前置詞ofのコアイメージ】
of とは、全体の部分をさす。
a piece of cake
なら、cakeが全体、a pieceが部分

top of the world
なら、the worldが全体、topが部分

だから、ofの前に作られた変数の中に、ofのあとに置かれた変数(配列)の要素が入っていく。

【N予備校では三章・「集計処理を行うプログラム」で登場】

スクリーンショット 2020-03-23 21.26.46.png

冒頭で
const prefectureDataMap = new Map ();
と、連想配列(Map)を取得する変数宣言をしている。

for(let [key, value] of prefectureDataMap){

の箇所では、連想配列Mapの中の要素を1つづつ[key, value]に代入して{}の処理を行っている。

※for-of文をfor文で表すと

let dailyLunch = ['うどん','ラーメン','カレー','蕎麦','パスタ'];
for( let i =0; i < dailyLunch.length; i++ ){
console.log( '今日のお昼は' + dailyLunch[i] );
}

・総当たりの表を作る

アルゴリズムって難しい!いつかぶち当たる壁だとおもう。いろんなアルゴリズムのパターンを覚えていって、知識で押せば何とかなるのかな??無からこの解き方を思いつくのは厳しい…

総当たりの対戦表を作る。

赤、白、青、黄、黒の5チームを対戦させる。その際の組み合わせをプログラミングで出力させる。

やりたいことを整理!
①5チームvs5チームの総当たり戦の組み合わせを書く
②同一チームどうしを省く
③同じ組み合わせどうし(赤vs白、白vs赤)を省く

①のプログラム

 let team = ['赤','白','青','黄','黒'];
for(let t1 of team){
for(let t2 of team){
console.log(t1 + 'vs' t2);
}
}

②のプログラム ← t1 == t2を排除する

let team = ['赤','白','青','黄','黒'];
for(let t1 of team){
for(let t2 of team){
if(t1 != t2){
console.log(t1 + 'vs' t2);
}
}
}

③のプログラム 

t1が'赤'の時、t2は'白','青','黄','黒'
t1が'白'の時、t2は'青','黄','黒'
t1が'青'の時、t2は'黄','黒'

つまり、対戦相手となる配列の要素を一つづつ消していけばいい!

let team = ['赤','白','青','黄','黒'];
let opponents = ['赤','白','青','黄','黒'];
for(let t1 of team){
opponents.shift();
for(let t2 of opponents){
if(t1 != t2){
console.log(t1 + 'vs' t2);
}
}

配列の操作、
・配列.shift() → 配列の最初の要素を削除して返す
を利用して、配列opponentsの最初の要素を消していく。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【自分用】Rust and WebAssembly を試す 【2020/03/23更新】

初投稿、勉強用メモ

なぜこれを書くのか

Rust、WebAssemblyに興味があった
Webブラウザ上で高速に動作するアプリを作る方法が知りたい
学んだ内容を見返すためのメモ

進め方

Rust ? and WebAssembly ?を適宜端折ってまとめていきます。
誤っている箇所などあればコメントを頂けると幸いです。
当方Rust, WebAssembly, javascriptすべて初心者です。

何を学ぶか? (Rust and WebAssemblyより抜粋)

  • RustからWebAssemblyへコンパイルする際に使うツールのセットアップ
  • Rust, WebAssembly, javascript, HTML, CSS等、多言語で構成されるプログラムを作る際におけるワークフロー
  • Rust, WebAssembly, javascriptの強みを生かすためのAPIを設計する方法
  • RustでコンパイルされたWebAssemblyをDebugする方法
  • RustとWebAssemblyで書かれたプログラムを高速化する方法
  • RustとWebAssemblyで作成された.wasmファイルのサイズを小さくする方法

次節よりtutorialのまとめです。

1.はじめに

誰のための本?

  • Web上で高速かつ信頼性の高いコードを書くためにRustをWebAssemblyへコンパイルしたい人
  • Rustをまあまあ知っていてかつjs、html、cssに馴染みがある人。(私はどちらにも当てはまりませんがとりあえず写経しながら進めます)

この本の読み方

  • RustとWebAssemblyを一緒に使う動機とその背景に関するページを読んでね
  • tutorial章は最初から最後まで取り組むように書かれているよ
  • 書いて、コンパイルして、走らせるまでを自分でちゃんとやってね

2.RustでWebAssemblyを使う理由

使いやすい低レベル制御

  • javascript Webアプリケーションでは信頼できるパフォーマンスの維持に苦労している。少しのコード修正でJITのパフォーマンスが著しく低下する場合がある。そこでRustを用いて信頼性の高い低レベル制御を実現しよう

小さな".wasm"サイズ

  • .wasmはネットからダウンロードするのでコードサイズは大切。Rustならruntimeがないのでサイズが小さくなります

すべてをRustに書き換える必要はない

  • 今あるコードをすべて捨ててRustに書き換える必要はなく、パフォーマンスにクリティカルなところだけ書き換えればOK。ある程度高速化して満足すればそこでやめてもOK。

ほかとも遊ぶ (うまく翻訳できず)

  • jsの他のツールと共存できます

3.WebAssemblyとは何か?

  • 多くの仕様を持つハードウェアモデルと実行形式のフォーマットである
  • 可搬性が高くコンパクト

プログラミング言語としてのWebAssemblyは2つの形式で書かれる。1つ目は.watテキスト形式、2つ目は.wasmバイナリ形式である。.watは読める形式、wasmはバイナリ形式。wat2wasmデモでwat形式とwasm形式を比較可能。

リニアメモリ

  • 本質的にフラットなリニアメモリモデルを持つ。メモリモデルはページサイズの倍数(64KB)で拡張できる。

WebAssemblyはWeb専用か?

  • wasmはホスト環境を指定しないので将来的にポータブルな実行形式となる可能性はある。
  • 現状はJSとの関わりが深い。

4.チュートリアル (Conway's Game of Life)

RustとWebAssemblyでコンウェイのゲームオブライフを実装するチュートリアル

チュートリアルの対象者

  • Rustとjavascriptの基本操作を知っている人
  • Rust, javascript, HTMLを一緒に使う方法を知りたい人

何を学べるか?

  • RustからWebAssemblyへコンパイルする際に使うツールのセットアップ
  • Rust, WebAssembly, javascript, HTML, CSS等、多言語で構成されるプログラムを作る際におけるワークフロー
  • Rust, WebAssembly, javascriptの強みを生かすためのAPIを設計する方法
  • RustでコンパイルされたWebAssemblyをDebugする方法
  • RustとWebAssemblyで書かれたプログラムを高速化する方法
  • RustとWebAssemblyで作成された.wasmファイルのサイズを小さくする方法

4.1 セットアップ

4.2 Hello, World!

4.3 ライフゲームのルール

4.4 ライフゲームの実装

4.5 ライフゲームのテスト

4.6 ライフゲームのデバッグ

4.7 インタラクティブ機能の追加

4.8 パフォーマンスの改善

4.9 .wasmのサイズを小さくする

4.10 npmへ公開

まとめ

tutorialが終わったら書きます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ブックマークレット(Bookmaeklet)の書き方

ブックマークレットとは

ブックマークとは別もの?

ブックマークはサイトのURLが記載されていて
ブックマークをクリックするとそのサイトを開くものです。
一方ブックマークレットはJavascriptによるプログラムが書かれていて
クリックすると現在開いているページでプログラムを実行します。

何が出来るの?

プログラムを実行することによって作業の自動化等が出来ます。

どうやって作るの?

最初にブックマークを作成し内容をJavascriptに書き換えます。
この時に内容の先頭はjavascript:としコードは一行で書かなければなりません。

ブックマークレットの書き方

ブックマークレットはjavascriptを一行で書かなければなりません。
以下にある要素を繰り返しクリックする通常のコードを示します。

通常のjavascript
const interval=1000;
const maxCount=100;
let count=0;

const ID=setInterval(()=>{
    if(count < maxCount){
        count++;
        const elem = document.getElementById("elem-id");
        elem.click();
    } else {
        //終了時処理
        clearInterval(ID);
    }
},interval);

次にこれを一行にして先頭にjavascript:を追加したブックマークレットのコードを示します。

ブックマークレット
javascript:const interval=1000;const maxCount=120;let count=0;const ID=setInterval(()=>{if(count<maxCount){count++;const elem=document.getElementById("ID");elem.click();}else{clearInterval(elem-id);}},interval);

となります。
また空白は%20としても構いません。
コードの前後に{}(ブレース)で囲って名前空間を汚さないようにした方が良いかもしれません

ブックマークレット
javascript:{const interval=1000;const maxCount=120;let count=0;const ID=setInterval(()=>{if(count<maxCount){count++;const elem=document.getElementById("ID");elem.click();}else{clearInterval(elem-id);}},interval);}

以上がブックマークレットの書き方の簡単な説明です。
読んでくれて有難うございます

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

インフィニティ、ページネーション、もっと見る... は、けっきょくどれ?

インフィニティ、ページネーション、もっと見る... は、けっきょくどれ?

  • Web UIにおいて頻出するリスト表現
    • リストUI、フィードリスト などと呼ばれる
    • 複数の情報ブロックが縦に、あるいは数カラムのグリッド構造などで羅列された、リスト状のUI
    • Twitter、Facebook、Instagram ともに、メインUIにリスト構造が採用されている
    • Google 検索結果もリスト構造
  • リスト表現の多くは、対象情報の件数が多量になる場合がある
    • 1回の表示件数を数十件以内に抑えて、UX や Webパフォーマンス を担保することが一般的
      • 情報の続きを追加表示する手段を提供する必要がある
  • 情報の続きを追加表示する手段の例
    • インフィニティスクロール
    • ページネーション
    • 前後ボタン
    • "もっと見る"ボタン
  • 本考察においては表示デバイスをスマートフォンと仮定する
  • 遷移やリクエストなどの各挙動ごとにURLは書き換えられ、SEO性は担保できていると仮定する

情報の続きを追加表示する手段

インフィニティスクロール

  • インフィニティスクロール、無限スクロール、ロングスクロール、Infinite scroll、Infinite Scrolling などと呼ばれる
  • ユーザーのスクロール操作により1回分の情報表示の終端が表示される際に追加情報を表示する
  • SNS(SP UI) の多くが採用

ページネーション

  • ページネーション、ページング、ページャー、Paging、Pagination などと呼ばれる
  • 書籍における"ページ"に例えて2ページ目、3ページ目のように追加情報を遷移表示する
  • PC UIにおいて散見される
  • スマートフォン UIでは限られた画面サイズの中で十分なUXを提供しづらい場合がある
  • ebay が採用

ebay の ページネーション ナビゲーション

https://www.ebay.com/

前後ボタン

  • 市場においてこのUIについての呼び名が存在するかどうか不明...
  • 前ページ、次ページのみの遷移ナビゲーション
  • 1回分の情報表示の終端に前ページ、次ページへの遷移ボタンのみ配置される
  • ページネーションのページ数リンク部分を削除したようなナビゲーションデザイン
  • Airbnb、Amazon が採用

Airbnb の 前後ボタン ナビゲーション

https://www.airbnb.jp/

Amazon の 前後ボタン ナビゲーション

https://www.amazon.co.jp/

"もっと見る" ボタン

  • もっと見るボタン、Show more button、Load more button などと呼ばれる
  • 1回分の情報表示の終端に「もっと見る」等とラベリングされたボタンを配置する
  • ユーザーのボタンタップ操作により追加情報を表示する
  • Google検索結果が採用

Google検索結果 の "もっと見る" ボタン ナビゲーション

https://www.google.com/

ユーザー観点における比較

インフィニティスクロール

  • ユーザーにとって、操作コストが低い
  • 機能に対するUIパーツが一切不要
  • ユーザーに、画面の状態に応じて"自動的に処理してくれている"感覚を提供する
  • ユーザーにとって、操作の"主導権を奪われている"感覚を与える可能性がある

ページネーション

  • ユーザーに対して操作コストを要求する
  • "ページ"というメタファにより、情報の全量や序列を把握しやすい
  • ユーザーに、堅苦しいイメージを与える可能性がある

前後ボタン

  • ユーザーに対して操作コストを要求する
  • 情報の全量や序列を感覚的に伝える装置が排除されている
  • シンプルなUIが提供できる
  • ユーザーに要求する操作は簡潔
  • 1アクションの操作が簡潔であるがゆえに、操作コストに納得感を得られない可能性がある

"もっと見る" ボタン

  • ユーザーに対して操作コストを要求する
  • シンプルなUIが提供できる
  • ユーザーに要求する操作は簡潔
  • 1アクションの操作が簡潔であるがゆえに、操作コストに納得感を得られない可能性がある

メリデメ(pros and cons)による比較

インフィニティスクロール

メリット pros

  • ユーザーアクションコストが低い
  • コンテンツ利用の集中力が持続しやすい
  • 利用しやすいと感じられるケースが多い

デメリット cons

  • 再訪性へのケアが必要
  • 遷移フロー設計のコストが高い
    • 遷移フローが破綻しないための仕様定義
    • 大量に表示したリストから遷移後のもどるボタンの挙動へのケア
  • コンテンツフッターを基本的には配置できない
  • 大量にスクロールした場合のブラウザパフォーマンスへのケアが必要
  • スクリーンリーダーユーザーにとって意図しない挙動へのケアが必要

ページネーション

メリット pros

  • 画面フローが破綻しない
  • ユーザーにとって自身の操作の状態を把握しやすい

デメリット cons

  • ユーザーに対して操作を要求する
  • スマートフォンUIにおいてUXが減損する場合がある

前後ボタン

メリット pros

  • 画面フローが破綻しない

デメリット cons

  • ユーザーに対して操作を要求する
  • ユーザーにとって自身の操作の状態を把握しづらい

"もっと見る" ボタン

メリット pros

  • アクションが直感的でハードルが低い
  • コンテンツ利用の集中力が途切れにくい

デメリット cons

  • ユーザーに対して操作を要求する
  • 再訪性へのケアが必要
  • 遷移フロー設計のコストが高い
    • 遷移フローが破綻しないための仕様定義
    • 大量に表示したリストから遷移後のもどるボタンの挙動へのケア
  • 大量にリストを表示した場合のブラウザパフォーマンスへのケアが必要

著者の個人的な所感まとめ

  • あくまで著者の予測であり、検証には開発実践やユーザーテストによるエビデンスが必要

インフィニティスクロール

  • メリデメの数に比例して要件仕様も増大するため、実装コストは他の選択肢と比較するとはるかに大きくなる
  • 再訪再現性のケアに関するコストが高い
  • ユーザー満足度は高いため、実装コストが十二分にかけられる場合は選択したい
  • アクセシビリティ課題にどう取り組むか、も、選択のポイント

ページネーション

  • 安心確実な実装
  • スマホUIにおいては、面積をとりすぎたり、省スペース化するため本来の効果が得にくい
  • 実装コストはそこまで低くないので、得られる結果を考慮するとコストパフォーマンスが高いとは言えない

前後ボタン

  • スマホUIにおいて割り切った実装であり、ユーザーにとって直感的で抵抗が少ないことが期待できる
  • アウトプットに対する実装のコストパフォーマンスが優れていると判断できる

"もっと見る" ボタン

  • シンプルではあるがインフィニティスクロールの半分程度の要件仕様は発生する
  • インフィニティスクロールと同様、再訪再現性のケアに関するコストが高い
  • 実装コストに一定以上の余裕があれば選択肢になる
  • もっと見るボタンと前後ボタンのABテストが必要と感じる
    • どこかに実績があれば教えてください m(_ _)m

最後に

  • 以上は4者択一とは限らない
    • 4者のうち2者複合のような実績も存在する
    • ユーザー選択式のようなアイディア提案もなされている
  • また、以上の4択以外の定番も今後間違いなく生まれてくる
  • つまり、新定番を発明する余地がまだまだ存在する分野である
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

二重ビット否定(~~)を使ったコード Javascript

はじめに

配列の要素ごとの要素数のカウントを調べている時にチルダ演算子の「なるほど」と思った使い方をご紹介します。

チルダ演算子(~)とは

NOT 演算は、各ビットで実行します。NOT a は、a を反転した値 (1 の補数として知られています) を出力します。
引用

~9の場合
10進数9を2進数(1001)に変換。正確には32ビットになったものとして扱います。
つまり、00000000000000000000000000001001となります。
これを反転した値11111111111111111111111111110110=-10(10進数)が~9です。
文字列の場合NaNになり-1になるみたいです。

let num = 9;
console.log(~num);
// -10

let str = "a";
console.log(~str);
// -1

~-10になる性質を生かして、indexOfで検索したい際に値が存在しない場合を以下のように書けます。

let arr = ['a', 'b', 'c'];
let str = 'd';
if (!~arr.indexOf(str)) {
    console.log('なし')
}

二重でチルダ(~~)を使用した時

32ビットの整数になるため、小数点は切り捨てられます。

console.log(~~1.0)
// 1
console.log(~~2)
// 2
console.log(~~2.3)
// 2
console.log(~~1.045)
// 1

// 文字の場合
console.log(~~"a")
// 0
console.log(~~"bb")
// 0
console.log(~~"ccc")
// 0
console.log(~~NaN)
// 0

チルダ(~)を要素ごとのカウント

    let arr = [1, 2, 3, 4, 1, 1, 2];
    let map = {};
    arr.filter((num) => (map[num] = ~~map[num] + 1));
    console.log(map);
    // { '1': 3, '2': 2, '3': 1, '4': 1 }

上記の性質を使い、各配列の要素をkeyとし、valueにその配列内での要素ごとの個数をカウントする事できます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptでのイベント実装方法

3種類のイベントハンドラの実装方法

コードで書かれているtargetはイベントハンドラをつけたいオブジェクトや要素です。

HTML属性を使用

<body onload="myfunc()">
</body>

<script>
  const myfunc () => {
    //実行したい処理
  }
</script>

イベントプロパティを使用

無名関数を使用
target.onload = () => {
  //実行したい内容
}
target.onload = myfunc;

const myfunc = () => {
  //実行したい内容
}

イベントリスナーを使用

即時関数を使用
target.addEventListener('DOMContentLoaded', () => {
  //実行したい内容
});
target.addEventListener('DOMContentLoaded', myfunc);

const myfunc = () => {
  //実行したい内容
}

イベントリスナーを使うメリットは下記2点。
・同じ要素に複数のイベント設定が可能
・早く実行される
 ⇒画像の読み込みなどで時間がかかり、早い段階でJavaScriptの処理を行いたい場合は、DOMContentLoadedを使った方がよい。

addEventListenerの詳細

対象要素.addEventListener(種類, 関数, [,useCapture])

第3引数のuseCaptureはページの読み込みをする順番を設定しています。

trueの場合は上から実行される。(Capture Phase)
fasleの場合は下から実行される。(Bubbling Phase)
記載しない場合はfalseの設定になっていて、基本的にはfalseで問題ないです。
(参考:addEventListener()の第3引数の意味とかをちゃんと理解する為のメモ)

ただし、第三引数はboolean以外も設定可能ということでデフォルトは下記にしておくのがよさそう。

対象要素.addEventListener(種類, 関数, {
once: true,
passive: true,
capture: false})

(参考:addEventListener の第3引数が拡張されてるという話)

イベントの種類

イベントの種類 内容
load Webページの読み込みが完了した時に発動(画像などのリソースすべて含む)
DOMContentLoaded Webページが読み込みが完了した時に発動(画像などのリソースは含まない)
click マウスボタンをクリックした時に発動
mousedown マウスボタンを押している時に発動
mouseup マウスボタンを離したときに発動
keydown キーボードのキーを押したときに発動
keyup キーボードのキーを離したときに発動
keypress キーボードのキーを押している時に発動
change フォーム部品の状態が変更された時に発動
submmit フォームのsubmitボタンを押したときに発動
scroll 画面がスクロールした時に発動

多すぎるので他は下記を参照。
JavaScriptのイベントハンドラ一覧

イベント発生時に内容を受け取る方法

document.getElementById("id").onclick = functionName;

const myhandler = (e) => {
  console.log(e.target);
}

イベント発生時に受け取るオブジェクトについて

コード 意味
e.type イベントの種類を取得
e.target イベントが送られたオブジェクト
e.srcElement イベントが送られたオブジェクト
e.clientX/Y ブラウザ領域の左上を原点とする座標
e.screenX/Y 画面左上を原点とする座標
e.pageX/Y ページの左上を原点とする座標
e.offsetX/Y イベント発生元の要素の左上を原点とする座標

2020-03-23_22h50_54.jpg

まだよくわかっていないが、event.targetevent.srcelementはブラウザによって動く動かないがあるらしい。
次のサイトにどちらでも動くように実装するコードが載っていたのでこちらを採用しよう。

function getTarget() { return this.srcElement || this.target; }
Event.prototype.getTarget = getTarget;

targetで使えるプロパティ・メソッド

基本的には下記URLに乗っているメソッドは使えるのかな?
Element

便利で使えそうなメソッドをメモ

メソッド・プロパティ 説明
innerHTML 要素内容のマークアップをStringで返す
tagName 要素のタグ名をStringで返す
getBoundingClientRect() 要素のサイズと、ビューポートにおける位置を返します。

あまりわからなかったので随時追加

【参考資料】
ゲームで学ぶJavaScript入門 HTML5&CSSも身につく!
【JavaScript入門】addEventListener()によるイベント処理の使い方!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【学習記録】『スラスラ読める JavaScriptふりがなプログラミング』読書感想  1、2章編

ありがたいことに、2020年3月10日から2020年3月31日まで、インプレスブックスが人気書籍の無料公開キャンペーンを行ってくれている(https://book.impress.co.jp/items/tameshiyomi?page=topbanner)

それにあやかって「スラスラ読める JavaScriptふりがなプログラミング」を読んだので、特に勉強になったところを記録しておく。特に、4章「関数を作ろう」と5章「Webページに組み込もう」が為になった。スラスラ読めるシリーズには今後もお世話になりたいところ。

第一章

・新しく学んだJavaScriptのメソッド

・promptメソッドで、ユーザーに入力させた文字を変数にいれることができる。

「let 変数名 = prompt('入力してね');」

スクリーンショット 2020-03-23 19.05.35.png

ユーザーが入力した文字が、変数に代入される

・parseInt()関数で、引数に入れられた変数を整数に変換して返す。
(これは初見じゃないけど役立つのでメモ)

・「オブジェクト」って何なの?

オブジェクトとは、メソッド(機能)とプロパティ(変数)の集合体である。
 console.log(moji) なら

 consoleがオブジェクト
 logがメソッド
 mojiがプロパティ

つまり、「プロパティ(変数)をメソッド(機能)でいじったものがオブジェクト」
ってこと??「これを(プロパティ)、こうして(メソッド)、こうじゃ(オブジェクト)」

みたいな??
オブジェクトという概念の理解、今後とも意識していきたい。

・エラーメッセージ

・NaN というエラーは Not a Number
数値ではない、ということ

・Typoと闘うツーつ

Diffchekerというツールを本書ではおすすめしてくれていた。
(でも登録がいるっぽいし、私の見つけた『文章比較ツール』でも良いような…。使い心地を比較したい)

第二章

・isNaN() 関数

isNaN() は is Not a Number?という意味。
「数字じゃないよね?」なので、
trueなら「Not a Number」(文字)
falseなら「Number」(数字)

・比較演算子

<= 左は右以下
>= 右は左以下
!= 右と左は等しくない

等号は比較演算子の後!!

 == と === の違いは 「左右は等しい」「左右は厳密に等しい」の違い。
!= と !== の違いは 「左右は等しくない」「左右は厳密に等しくない」の違い。

厳密な比較をしないと、 aaa == 'aaa'がtrueになってしまう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

魔法JS☆あれい 最終話「flatの、最高のflatMap」

登場人物

丹生(にゅう)あれい
魔法少女「魔法(マジカル)JS(女子小学生)☆あれい」として活動中。

イテレー太
正体不明の魔法生物。

第1部「ミューテーター・メソッド編」
* 第1話「popでpushした、ような……」
* 第2話「shiftはとってもunshiftって」
* 第3話「もうsortもreverseも怖くない」
* 第4話「fillも、spliceも、copyWithinもあるんだよ」

第2部「アクセサ・メソッド編」
* 第5話「joinなんて、concatなわけない」
* 第6話「indexOfなの絶対lastIndexOf」
* 第7話「includesのsliceと向き合えますか?」

第3部「イテレーション・メソッド編」
* 第8話「filterって、ほんとfindとfindIndex」
* 第9話「someなの、everyが許さない」
* 第10話「もうmapにもforEachにも頼らない」
* 第11話「最後に残ったreduceとreduceRight」

flat()

イテレー太「早いもので、今回でついに最終話だよ……」
あれい「短い付き合いだったな駄犬」
イ「だけど、戦いはまだ続いてるよ! 召喚したデータは第8話を参照してね!」
あ「おう」
イ「そして、最後の敵の弱点は、『記事で使われているすべてのタグ(tags)の一覧』だよ! さあ、タグの一覧をreturnして敵を倒してよ!」
あ「最後まで面倒くせえなあ……」

return items.map(item => item.tags).flat();

あ「これでいいのか」
イ「さすがあれい、最後まで頑なに盛り上がりポイントを作らなかったね! そして解説をお願い!」
あ「面倒くせえなあ……map() メソッドですべての記事にアクセスして、tagsの値を取得している。ただし、そのままだと……」

[
  [
    { name: 'JavaScript', versions: [] },
  ],
  [
    { name: 'JavaScript', versions: [] },
  ],
  ...
]

あ「このように階層化(ネスト)された配列が出来上がるので、flat() メソッドで配列をフラットにしているわけだ。その結果……」

[
  { name: 'JavaScript', versions: [] },
  { name: 'JavaScript', versions: [] },
  ...
]

あ「こういう配列が完成する」
イ「!!! 最終話にして、あれいが積極的に解説してるよ! 僕は感涙を禁じ得ないよ!」
あ「やかましい」

解説

flat() メソッドは、すべてのサブ配列の要素を指定した深さで再帰的に結合した新しい配列を生成します。

MDNより)

flat()メソッドは、階層化(ネスト)された配列をフラット化するメソッドです。
なお、「フラット化する階層の深さ」を引数に指定することができます。既定値は1です。

flatMap()

あ「ちなみに、さっきのはflatMap()を使うとさらに簡単に記述できるぞ」

return items.flatMap(item => item.tags);

イ「あ、あれいの様子が今までと違うよ! 僕の知らないうちに世界が再構築されたんだろうか!?」
あ「うるせえしばくぞ」

解説

flatMap() メソッドは、最初にマッピング関数を使用してそれぞれの要素をマップした後、結果を新しい配列内にフラット化します。これは深さ 1 の flat が続く map と同じですが、flatMap はしばしば有用であり、2 つのメソッドを 1 つにマージするよりもやや効果的です。

MDNより)

まさに名前の通りflat()map()なメソッドです。配列内のすべての要素にmap()メソッドと同様にコールバック関数を実行し、その結果をフラット化した配列にして返します。

エピローグ

……というわけで、魔法(マジカル)JS(女子小学生)☆あれいの物語はこれで完結です(すべてのメソッドを網羅できたわけではないですが)。JavaScriptで配列を操るメソッドの面白さと奥深さ、少しでも伝われば幸いです。お付き合いありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

魔法JS☆あれい 第11話「最後に残ったreduceとreduceRight」

登場人物

丹生(にゅう)あれい
魔法少女「魔法(マジカル)JS(女子小学生)☆あれい」として活動中。

イテレー太
正体不明の魔法生物。

第1部「ミューテーター・メソッド編」
* 第1話「popでpushした、ような……」
* 第2話「shiftはとってもunshiftって」
* 第3話「もうsortもreverseも怖くない」
* 第4話「fillも、spliceも、copyWithinもあるんだよ」

第2部「アクセサ・メソッド編」
* 第5話「joinなんて、concatなわけない」
* 第6話「indexOfなの絶対lastIndexOf」
* 第7話「includesのsliceと向き合えますか?」

第3部「イテレーション・メソッド編」
* 第8話「filterって、ほんとfindとfindIndex」
* 第9話「someなの、everyが許さない」
* 第10話「もうmapにもforEachにも頼らない」

reduce()

イテレー太「さあ、そろそろ戦いのクライマックスだよ!」
あれい「まだあるのかこの駄犬」
イ「召喚したデータは第8話を参照してね! そして、今回の敵の弱点は、『すべての記事のLGTM(likes_count)数の合計値』だよ!」
あ「はあ……」
イ「さあ、LGTMの合計値をreturnして敵を倒してよ!」
あ「はあ……面倒くせえなあ……」

var sum = 0;
items.forEach(item => {
  sum += item.likes_count;
});
return sum;

イ「……あれい、コードのレベルの急落幅が凄まじいよ。目に余るものがあるよ」
あ「もう疲れてきたんだよ」
イ「ダメだよ! こんなコードじゃ敵を倒せないよ! 特にforEach()は前回『使いみちがよくわからない』って言っちゃってるよ!」
あ「結果だけ合ってりゃいいだろ」
イ「そんなんじゃ魔法少女じゃないよ! いつものように1行でビシッと決めてよ!」
あ「面倒くせえなあ……」

return items.reduce((sum, item) => sum += item.likes_count, 0);

あ「これでいいのか」
イ「それでこそあれいだよ! そして解説をお願い!」
あ「はあ、面倒くせえなあ……だから、reduce() メソッドですべての記事にアクセスして、likes_countの値を順にsumに加算してるんだよ。で、予めアキュムレータであるsumの初期値を0に設定している」
イ「いやー、さすがはあれい! やればできるのに最初あからさまに手を抜いたね!」
あ「うるせえ」

解説

reduce() は配列の各要素に対して(引数で与えられた)reducer 関数を実行して、単一の値にします。

MDNより)

reduce()メソッドは、配列のメソッドの中でも最もややこしいメソッドの一つです。ただし、コツさえつかめば非常に便利です。
reduce()メソッドは、「コールバック関数、アキュムレータの初期値」の2つの引数を使用できます。さらにコールバック関数では、「アキュムレータ(コールバックの戻り値を蓄積する変数)、現在の要素、現在の要素のインデックス、元の配列」という4つの引数を使用できます。
……と書くと非常にややこしいんですが、単にmap()メソッドのように「すべての要素に関数を実行」し、その結果が順にアキュムレータに蓄積されていく……と考えると、少しはわかりやすいかもしれません。
また、アキュムレータの初期値を指定しなかった場合、配列の最初の要素が使用されます。

例えば、LGTM数の最大値を算出したい場合は、次のような記述になります。

return items.reduce((max, item) => max > item.likes_count ? max : item.likes_count, 0);

reduceRight()

解説

reduceRight()は、隣り合う 2 つの配列要素に対して(右から左へ)同時に関数を適用し、単一の値にします。

MDNより)

さて、あれいも疲れているようなので、reduceRight() メソッドについては解説のみとなります。
reduce()メソッドとの違いは、「コールバック関数が最後の要素から逆順に実行される」「アキュムレータの初期値を指定しなかった場合、配列の最後の要素が使用される」の2点です。

次回予告

ネストされた配列が扱いにくいだなんて言われたら、
私、そんなのはフラット化できるって、何度でもそう言い返せます。
きっといつまでも言い張れます。

最終話 flatの、最高のflatMap

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Nuxt.js】Vue Router復習編:params, queryを使おう

前置き

前回の続きです!
基礎編の復習と思ってもらえれば⭕️

https://note.com/aliz/n/ndf76ebe9853b

値を見てみよう!

consoleの値と
実際画面上でどうなるか
確認していきましょう??

_idでどんな文字列がきても
良い状態にしておきます。

【ディレクトリ 】

file
pages/
--| _id/
-----| index.vue
index.vue
<template>
 <div class="page">
   <p>params: {{ $route.params.id }}</p>
   <p>query: {{ $route.query.id }}</p>
 </div>
</template>

<script>

export default {
 fetch ({ params, query }) {
   console.log(params, query)
 }
}
</script>

【URL】

localhost:59037/hoge

【表示】
picture_pc_63d933e050fe2d158023130629f1a993.png

【解説】
consoleを見ていきましょう?
・paramsが{id: "hoge"}
 urlのpath部分がhogeのため
 templateの参照も
 route.params.idで一致し
 うまく表示されています?
 これが$route.params.userにしてみると
 一致せず何も表示されません。。。
・queryが{}
 urlに?がないためqueryは空

✅$router.pushを追加してみます!
https://router.vuejs.org/ja/guide/essentials/dynamic-matching.html

index.vue
<template>
 <div class="page">
   <p>params: {{ $route.params.id }}</p>
   <p>query: {{ $route.query.user }}</p>
   <button
     type="button"
     @click="$router.push({ path: 'hogehoge', query: { user: 'private' } })"
   >
     移動!
   </button>
 </div>
</template>

<script>

export default {
 fetch ({ params, query }) {
   console.log(params, query)
 }
}
</script>

【URL】
ボタンを押す前

localhost:59037/hoge

ボタンを押した後

localhost:59037/hogehoge?user=private

【表示】
スクリーンショット 2020-03-13 17.04.06.png

queryをuser=privateにしているので
{{ $router.query.user }}と変更したことで
privateが表示されていますね?

pagination

ページネーションも
考え方はこれと同じです!
paramsは同じまま、
queryだけを変えていきます。
これにより同じページ内でソートを書け
1ページ目だけを表示、ということができます?
liの1を押せば1ページ目にいきます?

【基礎構文】
変数を使う時は${変数}にします。
テンプレートリテラルについては
ここが分かりやすいです!
https://qiita.com/kura07/items/c9fa858870ad56dfec12

userIdをpropsとして渡し
親でuserId=123とすれば
/user/123へ飛びます。
https://router.vuejs.org/ja/guide/essentials/navigation.html

基礎構文
router.push({ path: `/user/${userId}` }) // -> /user/123

【飛びたいURL】

localhost:3000/home?members=1

【Pagination.vue】
queryをpropsとして渡します。
queryは?から始まるので?から始まり
その後ろに変数のqueryを入れます。
変数を使う時は${変数}にします。

Pagination.vue
<template>
 <div class="page">
   <ul class="list">
     <li
       @click="$router.push(`?${query}=1`)"
     >
       <span class="text">
         1
       </span>
     </li>
 </div>
</template>

<script>
export default {
 props: {
   query: {
     type: String,
     required: true,
   },
 },
}
</script>
home.vue
<template>
 <div class="page">
    <Pagination
      query="members"
    />
 </div>
</template>

queryを親で指定して
members一覧部分の
1ページ目でソートすることができます?

?router.pushの文頭に/を追加し
 $router.push(/?${query}=1)
 にしてしまうと

【飛びたいURL】
pages/home.vue内で
メンバーの一覧部分をソート

localhost:3000/home?members=1

【実際のURL】
pages/index.vue内で
メンバーの一覧部分をソート

localhost:3000/?members=1

となってしまいます!?

理解度チェック

✅あまり実用的ではないですが
理解度チェックのためのクイズです?

飛びたいURLから
足りない部分を書き足しましょう✍️

【飛びたいURL】

localhost:3000/user/hoge

【ディレクトリ 】

file
components/
--| RouterPush.vue

pages/
--| user/
-----| _id.vue
--| index.vue
RouterPush.vue
<template>
   <button
     type="button"
     @click="$router.push(`${query}`)"
   >
     home
   </button>
</template>
index.vue
<template>
 <div class="page">
   <RouterPush
   />
 </div>
</template>

<script>
import RouterPush from '~/components/RouterPush.vue'

export default {
 components: {
   RouterPush
 },
}
</script>

分かりましたか??
書き足す部分はこちらです!

index.vue
<template>
 <div class="page">
   <RouterPush
     query="user/hoge"
   />
 </div>
</template>

<script>
import RouterPush from '~/components/RouterPush.vue'

export default {
 components: {
   RouterPush
 },
}
</script>

【解説】

RouterPush.vueで
${変数}を使っているため
親で変数queryを行きたいURLに指定します??
行きたいURLはuser/_id.vueで
_idはhogeとしているのでquery="user/hoge"
文字列をそのまま渡しているので
:queryにする必要はありません。
:query="変数"の場合は使用します?
これで復習もバッチリですね!!

記事が公開したときにわかる様に、
note・Twitterフォローをお願いします?

https://twitter.com/aLizlab

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

任意のボタンからの訪問はお問い合わせフォームのselectboxでその選択肢が選択済のまま表示されるようにする

やりたいことを日本語で表現するのはムズイデズネ...

やりたいこと

任意のページのボタンからのお問い合わせフォームへの訪問は、あらかじめその項目が選択済となってお問い合わせフォームが表示されるようにしてほしいとの要望があったので初心者ながらも調べてみました。
(これが最適かどうかもわかりませんのでより良い方法があれば教えてください)

流れ

  1. 元のaタグにURLパラメーターを設置しておく
  2. 訪問先であるお問い合わせフォームにて、そのURLパラメーターの取得
  3. もしそのパラメーターのidとselectbox の選択肢(option)のidが一致すればその選択肢が選択済になるようにする

という流れにしようと思いました

1. 元のaタグにURLパラメーターを設置

<a href="hogehoge/contact/?id=lecture">

2. 訪問先であるお問い合わせフォームにて、そのURLパラメーターの取得

// URLのパラメータを取得
var urlParam = location.search.substring(1);
var str = '文字列';
str.substring(開始位置, 終了位置);

で、引数が一つなので、一文字目から最後までを抽出という意味です

▼参考サイト
substringで文字の切り出し

3.もしそのパラメーターのidとselectbox の選択肢(option)のidが一致すればその選択肢が選択済になるようにする

もしそのパラメーターのidとselectbox の選択肢(option)のidが一致すれば

var urlParam = location.search.substring(1);

// URLにパラメータが存在する場合
if(urlParam) {
    //処理
}

選択肢が選択済になるようにする

document.getElementById('lecture').selected = true;

ということで1、2、3を合わせると、

var urlParam = location.search.substring(1);

// URLにパラメータが存在する場合
if(urlParam) {
  document.getElementById('lecture').selected = true;
}

出来上がりですね

selectboxにidをつけて、
その子要素の何番目option[2]とかで要素を取得してもできまする。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ajaxとjbuilderの参考リファレンスなど

Ajax

【jQuery日本語リファレンス 】Ajax
Ajaxのリファレンスになります。オプションも含めて説明しています。
【js STUDIO】$.ajax()
オプションや動作を含めて説明されています。
Ruby on RailsのAjax処理
RailsでのAjaxの流れを図解つきで説明されています。

jbuilder

【GitHub】jbuilder
jbuilderのドキュメントです。
Rails4でJSONを作るならto_jsonよりjbuilder
jbuilderの使い方を丁寧に説明した記事になります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

APIとは

API

Railsは必要なHTMLを組み立てて返すのが仕事ですが、JavaScriptから便利に扱えるようにJSON形式でデータを返すようにすることもできます。
HTMLだけではなく、必要なデータだけをJSONなどの形式で返すサーバの仕組みのことをAPIと呼びます。

APIの作り方

元々あるコントローラに対して非同期通信の場合には非同期通信用のデータを返す実装を付け加えます。
コントローラーの1つのアクションの中でHTMLとJSONなどのフォーマット毎に条件分岐できる仕組みがあります。
フォーマットごとに処理を分けるには、respond_toを使用します。

respond_to

respond_toメソッドを使うと、リクエストに含まれているレスポンスのフォーマットを指定する記述を元に条件分岐ができます。
【例】

respond_to do |format|
  format.html { render ... } # この中はHTMLリクエストの場合に呼ばれる
  format.json { render ... } # この中はJSONリクエストの場合に呼ばれる
end

HTMLを返す場合は、該当するビューを呼びその中データを生成していましたが、JSONを返す場合はRubyのハッシュの状態のままrenderメソッドに渡すだけでJSONに変換してくれるので、コントローラーから直接データを返すことができます。
【例】

respond_to do |format|
  format.json { 
    render json: { id: @user.id, name: @user.name }
  }
end

上記のように記述することでcontrollerだけでレスポンスを完結させることもできますが、jbuilderを使用する、つまりファイルを分割することで、よりわかりやすい形でJSON形式のデータを作ることができます。
【例】

app/views/messages/show.json.jbuilder
json.content @message.content

=> { content: "@messageのcontent" }

jbuilderは左がキー・右がバリューのようなハッシュの形になっています。
上記の例だと、json.contentがkeyで、@message.contentがvalueとなります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Cordovaのチュートリアルをやってみる

公式チュートリアル:http://ccoenraets.github.io/cordova-tutorial/index.html

Cordovaプロシェクトの作成

Node.jsの最新バージョンをインストール

Cordova CLIのインストール

[windows]
npm install -g cordova

すでにインストール済みの場合は最新バージョンにアップデート
npm update -g cordova

[mac]
sudo npm install -g cordova

すでにインストール済みの場合は最新バージョンにアップデート
sudo npm update -g cordova

プロジェクトを保存するディレクトリに移動し、workshopという名前のディレクトリにWorkshopというCordovaプロジェクトを作成。

cordova create workshop com.yourname.workshop Workshop

プロジェクトディレクトリに移動。

cd workshop

SDKのインストール
workshopディレクトリにて
[iOS]

cordova platforms add ios

[Android]
https://developer.android.com/studio からインストール後

cordova platforms add android

workshopディレクトリにて、基本的なプラグインを追加

cordova plugin add org.apache.cordova.device
cordova plugin add org.apache.cordova.console

上記は旧式らしくエラー出る。

cordova plugin add cordova-plugin-splashscreen
cordova plugin add https://github.com/apache/cordova-plugin-console.git#1.0.0

上記で対応可?
詳細は未調査なので後ほど探ります。

エラー対応の参考:https://qiita.com/konyavic/items/ca6bec224ddd6abeb0ac

ディレクトリ構成

wwwが、コーディングファイル保存場所

platformsは、iOS/Androidなど異なるプラットフォームのためのアプリケーションをビルドする場所。
いじらない。

プラグインはpluginsにインストールされる。

アプリケーションパラメータ(名前、作成者など)はconfig.xmlに保存される。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Node.js: printf風の文字列フォーマットをする方法

Node.jsでprintf風の文字列フォーマットをするには、util.format関数を使うと良い。

const {format} = require('util')
const message = format('値は %s のうちのどれかにしてください', [1, 2, 3])
console.log(message)
//=> 値は [ 1, 2, 3 ] のうちのどれかにしてください

公式ドキュメント: Util | Node.js v13.11.0 Documentation

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Kinx 実現技術 - Switch-Case

Switch-Case

はじめに

「見た目は JavaScript、頭脳(中身)は Ruby、(安定感は AC/DC)」 でお届けしているスクリプト言語 Kinx。作ったものの紹介だけではなく実現のために使った技術を紹介していくのも貢献。その道の人には当たり前でも、そうでない人にも興味をもって貰えるかもしれない。

前回のテーマは構文解析。今回のテーマは Switch-Case。

Switch-Case

なぜ Switch-Case を取り上げるかというと、やはり ジャンプテーブル の魅力があるから。基本多分岐なので、複数の条件に対して一発でジャンプできるのが期待値だろう。ただ、そのために結構複雑な処理をしている。

整数値・それ以外

まず、数値(整数)かそれ以外かで動作が異なる。整数値以外は基本 if-else にするしかないのだが、整数値は可能ならジャンプテーブルにしようと試みる。なので、まずは整数値かどうか確認し、どちらかに分岐させる。

整数値の場合

整数値の場合、ジャンプ・テーブル化を試みる。ただし、下限と上限の差がありすぎる場合、無駄なジャンプが増えすぎてしまうという問題がある。そこで、閾値を設定して複数のブロックに分割する。例えば以下のケースをパースした場合、

switch (a) {
case  10: break;
case  11: break;
case   1: break;
case   2: break;
case   3: break;
case   4: break;
case   5: break;
case   6: break;
case  51: break;
case  52: break;
case  53: break;
case  54: break;
case 100: break;
case 'aaa': break;
default: break;
}

1~100 までのジャンプテーブルを作ってしまうと 13 個だけが有効で、残りの 87 個は default にジャンプするだけの無駄が多いテーブルを作ってしまう。アドレス(ポインタ)サイズが 64bit の場合、100 個のジャンプを用意するだけで 800 バイト必要になる。800 バイトが多いかどうかは別にして、無駄なスペースが沢山あることに違いはない。

そこで、まず数値自体をソートして順に確認していき、閾値(デフォルトは 16)以上のインターバルがあった場合、別々のグループ(ブロック)に分けて、それぞれでジャンプテーブル化させるようにする。どのブロックに分岐するかは二分探索で選択する形でコード出力する(場合によっては線形探索)。

ちなみにブロック内の選択肢が少ない場合はあえてジャンプテーブル化はしない。線形探索や二分探索の方が効率が良い場合がある。なぜなら、ジャンプテーブルの場合、上限・下限値を越えないか比較し、評価値から下限値を減算してその上でジャンプさせることになるので都合3回は比較が入る。例えば比較値が 1 種類の場合、単に一発比較するだけの方が効率が良い。したがって、これにもブロック内の要素数を考慮してどの方式を使うかを決定する。

ちなみに、今後触れようと思うが VM コードの実行で Switch-Case を採用する場合のペナルティ(ダイレクト・スレッディングの有効性)は一般的には CPU パイプラインの投機実行ミスを指摘されるが、この比較回数もあるんじゃないかと思う。何せ 1 命令実行するために最低 3 回は比較と分岐が入る。明らかに上限・下限を超えないと判断できない限り、範囲外の条件判断は必要だしね。この辺は投機実行でカバーできているのかもしれないが。

上記の場合、1~11 のブロック、51~54 のブロック、100 だけのブロックに分かれる。評価値(a)の値によって二分探索(または線形探索)でどのグループを探索するかを決め、それぞれのグループの中で実際のジャンプ先を決定する。

それ以外

それ以外の場合、単に線形探索でジャンプ先を決定する。つまり if-else の連続で値をチェックする。

出力例

具体的な出力コードは以下のような感じ。break しかしてないので結局最後に jmp しているだけで、本気で最適化したらこのコード自体出力されないよなものだが現時点ではサーチのエッセンスは出力されている。

  .L460
     d04:   enter                   7, vars(1), args(1)
  .L462
     d06:   pushvl0                 $0(0)
     d07:   dup
     d08:   typeof                  is integer
     d09:   jz                      .L463(d1d)
     d0a:   dup
     d0b:   lti                     1
     d0c:   jnz                     .L464(d22)
     d0d:   dup
     d0e:   gti                     11
     d0f:   jnz                     .L464(d22)
     d10:   subi                    1
     d11:   jmptbl
     d12:   jmp                     .L482(d47)
     d13:   jmp                     .L482(d47)
     d14:   jmp                     .L482(d47)
     d15:   jmp                     .L482(d47)
     d16:   jmp                     .L482(d47)
     d17:   jmp                     .L482(d47)
     d18:   jmp                     .L463(d1d)
     d19:   jmp                     .L463(d1d)
     d1a:   jmp                     .L463(d1d)
     d1b:   jmp                     .L482(d47)
     d1c:   jmp                     .L482(d47)
  .L463
     d1d:   dup
     d1e:   pushs                   "aaa"
     d1f:   eqeq
     d20:   jnz                     .L482(d47)
     d21:   jmp                     .L482(d47)
  .L464
     d22:   dup
     d23:   lti                     51
     d24:   jnz                     .L465(d32)
     d25:   dup
     d26:   gti                     54
     d27:   jnz                     .L465(d32)
     d28:   dup
     d29:   eqeqi                   51
     d2a:   jnz                     .L482(d47)
     d2b:   dup
     d2c:   eqeqi                   52
     d2d:   jnz                     .L482(d47)
     d2e:   dup
     d2f:   eqeqi                   53
     d30:   jnz                     .L482(d47)
     d31:   jmp                     .L482(d47)
  .L465
     d32:   dup
     d33:   neqi                    100
     d34:   jnz                     .L463(d1d)
     d35:   jmp                     .L482(d47)
  .L466
    (省略)
  .L482
     d47:   ret                     null
     d48:   halt

出力コードを見るとまだまだ改善点はあるものの、初版としては十分かなー、と。

Switch-Case の魅力はやはり ジャンプテーブル と言っても過言ではない。それが無ければ Switch-Case を使う意味は間違いなく半減するよね。それができることを期待して Switch-Case を選ぶということも多いですし。例えば、Yacc で出力された構文解析器なんかは Switch-Case の塊なので、全ての比較が if-else で行われていたら正直パフォーマンス的にやってられないレベルで遅くなってしまう。つまり、Switch-Case はジャンプテーブル化しないと使い物にならないので頑張った。

おわりに

ここまで読んでいただいてありがとうございます。最後はいつもの以下の定型フォーマットです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

フロントエンドで始める「API の型定義」のススメ(Nuxt×TS×aspida)

概要

本記事では、Nuxt.js のプロジェクトに TypeScript を導入するにあたって、バックエンドの API を型定義することで API 呼び出しにも型付けの恩恵を受けられる方法を示します。

本記事の通りに API を型定義すると、API のレスポンスだけでなく、Path も TypeScript で定義できます。これにより、レスポンスを型安全に扱えますし、API の Path が変更された時にコンパイルエラーで変更箇所を検知できるといったメリットを享受できます。

API の型定義はライブラリの導入によって実現できます。Nuxt.js を例に説明しますが、他のフロントエンドにも導入しやすいはずなので、ぜひご活用ください。

本記事の流れ

はじめに既存実装の問題点を提示し、そのあとライブラリの導入によって API を型定義した結果を示します。問題点が解消できることを確認し、具体的な導入手順を補足します。随所に弊社で僕が実際に感じている課題、実際に運用して解決できそうな課題についても補足することで、実運用へのヒントを示します。

ソリューションはライブラリの導入なので、導入方法等はライブラリの README が適切であることから、僕の記事では問題点の提起と実運用に関して論述するところに厚みを持たせます。How To が気になる方は途中で示すライブラリの GitHub へ飛んでもらえればと思います。

既存実装の問題点

一例として、下記のように@nuxtjs/axiosを用いて API を叩いている事例を示します(※)。

const userResponse = await this.$axios.$get("/api/v1/contents/" + params.id);

このコードが抱えている課題として以下があります。

  • API の Path をベタ打ちしているので、typo しても気づかない
  • API の仕様が変わったとき、Grep して当該 API を使っている場所を探さないといけない
  • 返り値の型がわからない。せっかく TypeScript を使っているのに any になってしまう(特に TypeScript で書いている場合)

Path のベタ打ちや、返り値が any になるのは axios のみならず fetch や ky を使っていたとしても当然とも思われますが、これらの課題を API の型定義によって解消できる、というのが本記事の主眼です。


※)独自の axios ラッパーを定義してエラーハンドリング等を共通化する事例もありますが、本記事では axios をそのまま使っている例を出しました

弊社の状況

ここで、せっかくなので、自社で導入した背景を補足として説明します。

Nuxt.js に TypeScript を導入したのは Nuxt.js で構成したサイトをリリースしておよそ半年経過してからでした。

導入の際、主な懸念(TS を導入してもさほど効果が上がらないリスク)は

  • Vuex の型付け
  • Template 内の型付け
  • REST API の型付け

でした。

Vuex についてはほとんど使用しないようにしていて、ユーザーのステート管理くらいでしたので、vuex-module-decoratorsを導入して少し実装を調整すると終わりました。

Template 内の型付けも、(導入当時はまだ機能不足の感がありましたが)VSCode の Vetur プラグインで補完や null 判定等もかなり DX を保って行えるため、そこの懸念も小さかったです。

ただ問題は REST API との通信で、GET した値が any になるなら as で型定義することになるでしょうし、POST/PUT 時に投げる値については都度 Body の内容を型定義しても、他のページで同じ API に POST するときに Body に型定義することを強制できないです。

(ちょっと乱暴な例ですが)下記のように僕が実装したとして、

await this.$axios.$put(`/api/users/${id}`, {
  title: "hogehoge",
  body: "hogehoge",
} as UserAccount);

他のメンバーが同じ API を型付けせずに実装しても、コンパイルエラーしないため、運用でカバー、レビューでカバーみたいに風化することが予想できますね。

await this.$axios.$put(
  `/api/users/${id}`,
  {
    tilte: "hogehoge",
    boby: "hogehoge",
  } // 型付けしなくても別に通るし、なんなら上のtitle, bodyがtypoしているけどコンパイルエラーしないですね
);

TypeScript API Type Definitionといった単語でググっても全然ヒットしないので(※)、一般的なライブラリのような形に落とし込まれたソリューションは無いのかなと思っていた、というところまでが弊社の状況でした。


※)ある程度のエキスパートであれば、もしかすると各プロジェクトに応じて何かしらの解決策、例えば API の Path と渡す Body の型定義を辞書のようにした巨大なオブジェクトを使って Repository を構成するようなことをやってのけているかもしれませんが、そこまで頑張るのなら TypeScript やらなくていいかな・・・と思っていました。

API を型定義するとどうなるか

それではここまでの前提を踏まえて、API を型定義することで問題点を解消していけることを説明していきます。

ライブラリの概要

API を型定義するには aspida(アスピーダ)というライブラリを使います。国産のライブラリ、というか、普通に都内のフロントエンドの勉強会に登壇されているような方が 2019 年末に立ち上げたプロジェクトであるため、気軽に作者に連絡や改善要望できる点は嬉しいポイントです。

ここまでの話で、気になって仕方なくなった&ある程度 README 読んだらわかりそうだって方は以下のリポジトリを見てください。

https://github.com/aspidajs/aspida

API を型定義したあとの世界

それでは aspida を導入することで実装内容がどのように変わり、どのような利点を享受できるのかを説明していきます。

実装内容が変わること

まずは実装で変わることを説明します。

以下の既存実装のソースコードがあるとします。先程例示したように、API の Path をベタ打ちで、リクエストの Body は無理やり型定義しています。

await this.$axios.$put(`/api/users/${this.$route.params.id}`, {
  title: "hogehoge",
  body: "hogehoge",
} as UserAccount);

aspida を導入し、API の型定義をした後は下記のように書けます。

await this.$api.users._id(this.$route.params.id).$put({
  data: {
    title: "hogehoge",
    body: "hogehoge",
  },
});

見た目の観点で変わっていることとしては

  • API の Path を文字列ベタ書きではなく、オブジェクトのプロパティをネストして表現している
  • $axiosではなく$apiという別のクライアントを使っている

といったところです。

続いて、このように実装内容が変わることで、どのような利点があるのかを実装時、運用面の両方から整理します。

実装時に得をすること

もし API の Path を typo すると、コンパイルエラーになる

aspida で API を定義すると、API の Path がオブジェクトのプロパティとして表現されます。

したがって、例えば/api/usersの users を user を打ち間違えるとコンパイルエラーになります。

PUT する Body の内容を typo してもコンパイルエラーになる

同様に、渡すリクエストボディも予め型定義された状態で扱えます。

例えば title を tilte に打ち間違えるとコンパイルエラーになります。

運用時に得をすること

API を誰が使ってもリクエストボディ、レスポンスボディの内容を型定義できる

解説は省略しましたが、レスポンスボディも型定義した状態で扱えます。
そのため、aspida の記法で API を扱っている全エンジニアが、型の恩恵を受けた状態で開発ができます。

API の仕様変更時に追従していない実装をコンパイルエラーで気づくことができる

API を仕様変更したとき、例えば

  • POST を PUT に変えた
  • user を users に変えた
  • リクエストボディの内容が変わった
  • レスポンスの内容が変わった

といった場合、aspida 上で型定義した内容を書き換えると、対応した実装がすべてコンパイルエラーになります。

このあと、実際の導入手順を示した上で、コンパイルエラーで検知できる例を示しますので、合わせてご覧ください。

導入方法

それでは実際の導入手順を示していきます。詳しくはリポジトリの README を見てください。

大まかには下記の手順で実施します。

  • aspida の npm install
  • apis ディレクトリを作成し、なにか一つ API を型定義してaspida --build
  • $api オブジェクトを$axiosを元に Wrap し、Vue インスタンスと Nuxt.js コンテキストに Inject する

npm install

aspida は 2020 年 3 月現在、axios、ky、fetch を Wrap して型定義機能のついた HTTP クライアントに変換することができます。

ここでは axios を Wrap する前提で説明します。また、すでに@nuxtjs/axiosを利用していることも前提です。していない場合は別途、素の axios をインストールしてください。

npm install @aspida/axios

apis ディレクトリを作成し、なにか一つ API を型定義してaspida --build

ここが aspida の最大の特色です。

apis ディレクトリをルートディレクトリに作成したら、そこから API の Path の構造と同じ構造でディレクトリを作成します。
ちょうど、Nuxt.js でページのルーティングをディレクトリ構造で表現するのと同じです。

/api/v1/users/${userId}という API を作成したい場合は、apis/v1/users/_userId@number.tsにその API の型定義を記載します(※)。実際の型定義の方法は README をご覧ください。

しかし、ここで型定義を書いたとしても、最終形ではthis.$api.hogehoge.hogehoge....という形で利用できることから、$apiオブジェクトを生成する必要があります。

それをやってくれるのがaspida --buildコマンドです。
/apisディレクトリの中のディレクトリ構造から、自動で API の型定義ファイルを生成してくれます。

package.json
{
  "scripts": {
    "api:build": "aspida --build"
  }
}

※)$axiosのほうで、BASE_URL を設定していれば、$axiosを Wrap するクライアントを作るのが aspida なので/apiディレクトリを作成する必要はありません。

$api オブジェクトを$axiosを元に Wrap し、Vue インスタンスと Nuxt.js コンテキストに Inject する

aspida --buildコマンドが行うことの実態は、/apis/$api.tsに自分が定義した API をすべて吐き出すことです。実際に出力された内容を読むと理解しやすいと思います。

続いて、これらを Nuxt.js Component で利用する必要があるので、inject します。

/plugins/apiClient.ts
import { Plugin } from '@nuxt/types'
import `axios` Client from '@aspida/axios'
import api, { ApiInstance } from '@/apis/$api'

declare module 'vue/types/vue' {
  interface Vue {
    $api: ApiInstance
  }
}

declare module '@nuxt/types/app' {
  interface NuxtAppOptions {
    $api: ApiInstance
  }
}

declare module 'vuex/types/index' {
  interface Store<S> {
    $api: ApiInstance
  }
}

const plugin: Plugin = ({ $axios }, inject) => {
  inject('api', api(axiosClient($axios)))
}

export default plugin

最後のapi(axiosClient($axios))がポイントで、@nuxtjs/axiosaspida が用意している Wrap メソッドのaxiosClient()を実行することで Wrap オブジェクトを生成し、その結果を inject しています。

弊社の場合だと、$axios 自体に BASEURL の設定などをすでに終わらせていたので、設定内容がそのまま aspida にも適用できたのが便利ポイントです。

こうすることで、API の Path がディレクトリに基づいてオブジェクトのプロパティのネストで表現できるようになり、例えば返り値のAxiosResponseも型定義ファイルで定義した型を扱えるように Wrap されます。

コンポーネントで使ってみる

コンポーネントで使うときはthis.$apiまたはcontext.$apiで利用できます。エディタで実際に書いてみると、以降の API の内容がプロパティになっているので補完されていくことが確認できるでしょう。

以降は、バックエンドエンジニアから API の仕様を聞いたら aspida の書き方に落とし込んで型定義ファイルを自動生成し、実装していくのを繰り返します。

注意点

一度勘違いされたことがあるのですが、型定義自体をミスってしまうことは防げません(そもそも TypeScript がそうですからね)。
なので、例えばサーバーサイドから返ってくる API のtitleを間違ってnumberと定義してしまうと、実行時エラーが起きる原因になります。

利点の検証

さて、実際に aspida を導入する内容を説明したところで、上記で示した API の型定義の利点を検証してみましょう。

ここでは、API の仕様変更時に追従していない実装をコンパイルエラーで気づくことができるを検証してみます。

API の仕様変更でコンパイルエラーになる実際の事例

実際に弊社のソースコードで、POST /user/registerというユーザー登録の API があるのですが、これをPOST /usersという RESTful な API に書き換えてみます。

やるべきことは以下の 2 つです。

  • /apis/user/register/index.tsの内容を/apis/users/index.tsにコピー
  • aspida --buildを実行

その瞬間、npm run devを実行しているコンソールが動き出し、下記のようなエラーが出ます。

 ERROR  ERROR in /Users/.../signup.vue(87,53):
87:53 Property 'register' does not exist on type '{}'.
    85 |   methods: {
    86 |     async submit (params: RegisterBody) {
  > 87 |       const registerResponse = await this.$api.user.register
       |                                                     ^
    88 |         .$post({
    89 |           data: {
    90 |             email: params.email,

このように、API の仕様を変えたとき、既存実装が Type Error を吐いてくれるので、移行すべき実装に気がつくことができます。

API を容赦なく仕様変更したり削除できるかどうかは長期的に見て大きなメリットがあるはずです。特に弊社はほとんど Web 開発を僕 1 人でやっているようなベンチャーですが、PMF するまでの改善プロセスでプロダクトを作っては捨てていくことが多く、aspida のように容赦なく捨てられるような工夫を仕込んでおくことにメリットを感じています。

導入にあたっての注意点など

ライブラリの成熟度

aspida自体は始まったばかりのライブラリなので、破壊的な仕様変更が発生する可能性があります(2020 年 3 月現在、まだメジャーバージョンが 0)。弊社のように小回りがきくベンチャーなら導入しやすいですが、タイトな現場ではまだ様子見したほうがいいかもしれません。

逆に、作者の方は Twitter 等でも活動していたり discord でコミュニティを作っているので、改善要望等の連絡はつきやすいということが利点としてあげられます。僕は破壊的変更があったときにアップデートタイプする際、めっちゃ DM で質問攻めにさせていただきましたw
https://twitter.com/m_mitsuhide

まとめ

API の型定義自体は応用範囲が非常に広く、backend が strapi や microCMS、Firebase の REST API で作られています、という場合にも応用できると考えられます。
本記事を読んだ方が、REST API をより便利に TypeScript から利用する契機を掴んでいただけたら幸いです。

告知

僕の会社ではオンライン家庭教師に特化したマッチングサービス NoSchool(https://noschool.asia/ )を運営しています。

親御さんは生徒にどんな教育を受けさせていいかわからない、家庭教師側は個人で活動していても集客がままならない、といった双方の課題をどうやって解決していくかを考えつつ、Zoom 等の普及に伴ってオンライン家庭教師市場そのものを大きくしていこうというベンチャーです。
Nuxt×TypeScript + Laravel + AWS + Firebase な環境で開発しています。事業領域、開発内容に興味ある方はぜひお気軽に遊びに来てください!
https://twitter.com/Meijin_garden

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RxJSv6入門:概念のまとめと日本語簡易リファレンス

Angularで登場してきたのでざっくり学習。厳密な理論ではなくて、使い所勘所まとめです。
v6ベースです。

TL;DR

ゆるふわにまとめると、

  • RxJSは、流れてくるデータのイテレイティブな処理の記述をシンプルにすることが出来るもの
  • 使い所は、EventEmitterやPromiseに似てる
  • 勘所的には、非同期的な配列をイメージすると良さそう

基本

  • Observer
    • データの処理の仕方を記述したもの、データの処理方法
    • next, error, completeの3つが定義できる。それぞれPromiseのresolve, reject, (finaly callback)に相当する。
    • ようは値を受け取って、処理をして、返すもの
      • nextだけ定義するパターンが多い
      • Observableのsubscribe()の引数に匿名関数わたして定義しちゃうのが多いっぽい
  • Observable
    • データの流れを抽象化したもの
    • ファクトリーメソッドで、of(1,2,3), from([1,2,3])といったように作れる
      • (この場合、1,2,3のプリミティブが順に流れていく)
    • pipeメソッドでOperatorという便利関数をチェーンできる、
    • subscribe()メソッドにobserverを登録すると、流れたデータを次々にobserverで処理する
    • だいたい外部ライブラリ呼び出した結果に、Observableが返ってくる
  • Operator
    • Observableに繋ぐ便利関数
    • データの流れ方を整理、変更できちゃう、すごい
  • Subscription
    • subscribe() の返り値
    • .unsubscribe() メソッドをもつ。データが流れてくることをやめる。それだけ。
  • Subject
    • Observableの一種で、値を複数のObserverにマルチキャストできるやつ
      • subscribeで複数のobserverを登録でき、一気にマルチキャストする
    • しかも、Observerにもなれる(値を受け取れる)
      • 他のObservableのsubscribeに渡してもOK
  • Schedular
    • スケジュール、つまりタイミングを制御してくれるやつ

概念がややこしいが、大事なのは Observableで、

  • Observableにoperatorをpipeしていける
  • Observableは、最後に、.subscribe() で値の処理=値の受け取り側を指定する
  • その指定されるやつがObserverである

をまず最初に理解してしまうのが早い。

具体的にコードで見てみる。

Ovservable

const observable = from([1,2,3])                                 // 1
                   .pipe(                                        // 2
                       map((n) => n*2)                           // 3
                   ).subscribe((n)=> console.log(`value: ${n}`));// 4
  1. fromはObservableを生成するファクトリーメソッド(Creation Operator)。1,2,3のvalueが順に発生して流れている。
  2. pipeメソッドにより、Operatorを追加している。
  3. ここでは、mapにより、渡ってきた値をイテレイティブに処理して返している。返り値は再びObservable。
  4. subscribe()により、Observerを登録。関数を指定すれば、そのままObserverのnextとして取り扱われる。

以下、リファレンス的に細部の説明をしていく。

pipe()

Observableに生えている基本的なメソッド。

observableInstance.pipe(operator())

みたいな感じでpipeを使ってOperatorをつなげていく。
Pipable Operatorは純粋な関数であり、新しいObservableインスタンスが生成される。
Observableに対して、複数のPipable Operatorをつなげることができる。

observableInstance.pipe(
  op1(),
  op2(),
  op3(),
  op3(),
)

もちろん、OperatorがObservableを返すので

observableInstance.pipe(op1()).pipe(op2())

とも書ける(もちろん、ちょっと意味は違う)

Operators

Observableを作ったり、加工したりできるやつら。

Creation Operator

Observableを作る基本的なファクトリーメソッド。

  • of(1,2,3)
    • 引数がそのまま流れてくる
  • from([1,2,3])
    • 配列を与えると、それがそのまま流れてくる
  • ajax
    • Using ajax() to fetch the response object that is being returned from API.
    • HTTP RequestのResponseをObservableにしてくれる
  • interval(1000)
    • intervalごとにシーケンシャルな数字を流してくれる
  • range(1,10)
    • 指定した範囲の数字を流してくれる

Join Creation Operators

Observableまとめ隊。複数のObservableから1つのObservableへ。

merge(observable1, observable2,...concurrent)

  • 複数のObservableをまとめて1つのObservableにしてくれるすごいやつ
  • [1,2,3] + [a,b,c] ==> [1,2,a,b,3,c]
  • なお、concurrentで並列度を決めることが出来る
    • 並列度2にしておいて、3このObservableを与えると、最後の1つは、先に与えた2つが終了したら流れてくる
    • [1,2,3] + [a,b,c] + [d,e,f] ==> [1,2,a,b,3,c,d,e,f]
    • [d,e,f]は後回しになった

concat(observable1, observable2)

  • "Concatenates multiple Observables together by sequentially emitting their values, one Observable after the other."
  • 複数のObservableをまとめて1つのObservableにしてくれるが、完了するまで待つやつ
  • 順番は登録した順
  • [1,2,3] + [a,b,c] ==> [1,2,3,a,b,c]

zip(observable1, observable2)

  • 複数のObservableをまとめて、各入力Observableの値をひとまとめにしたObservableにしてくれる
  • [1,2,3] + [a,b,c] ==> [[1,a],[2,b],[3,c]]みたいな感じ

conbineLatest(observable1, observable2)

  • 複数のObservableをまとめて、それぞれの最新の値を組合せた値を流すObservableにしてくれる
  • [1,2,3] + [a,b,c] ==> [[1,a], [2,a], [2,b], [3,b], [3,c]] みたいな。(うーん分かりづらいので公式見てね

race(observable1, observable2)

  • 複数のObservableのうち、一番最初に値が流れてきたObservableのみ返す。文字通りレース。

forkJoin([observable1, observable2])

  • 渡すのはObservableをValueにもつObjectでも可
  • 複数のObservableが流れ終わった後に、最後の値を組合せた配列またはオブジェクトを流すObservableを返す

Transformation Operators

合体変形し隊。

map

  • 配列のmapと同じ、流れてくる値を順番に処理する
  • たぶん一番良く出てくる

pluck

  • オブジェクトから値を取り出す
  • { message: 'ok' ) から pluck('message')ok だけ取り出せる

scan((acc, one) => acc + one, seed)

  • 配列のreduceっぽい処理ができる(ただし、reduceは別にある)
  • accumulatorに指定した関数で、流れてくる値を次々と処理した結果のObservableを作れる

mergeMap

  • "Projects each source value to an Observable which is merged in the output Observable."
  • 引数側の関数内に指定したObservable(=内部Observable)に対し、元のObservable(=ソースObservable)を投影してくれる
  • 2つのObservableを合成し、時系列順に流れるObservableを作り出してくれる
    • 当たり前だが内部Observableがきちんと終了しないと、無限に増え続ける

concatMap

  • "Projects each source value to an Observable which is merged in the output Observable, in a serialized fashion waiting for each one to complete before merging the next."
  • mergeMapに似て、こちらもソースObservableを捨てることなく、2つのObservableを合成してくれる
  • ただし、時系列は、内部Observableが完了したら、次のソースObservableの値を処理する…といった仕組みになる
    • ソースObservableの値は保持して、待っている形になる
    • ということは、内部Observableが無限に終了しなかったり、内部Observableが終了する前にソースObservableから値が届き続けると、無限にバッファを食ってしぬことに…

switchMap

  • "Projects each source value to an Observable which is merged in the output Observable, emitting values only from the most recently projected Observable."
  • mergeMapと違い、ソースObservableからの値が到着したら内部Observableを発火する
  • ソースから新たな値が来た場合には、内部Observableは途中でも終了する。このとき、内部Observableの値は捨てられる(ソースObservable優先)

exhaustMap

  • "Projects each source value to an Observable which is merged in the output Observable only if the previous projected Observable has completed."
  • switchMapと同様、ソースObservableからの値が到着したら内部Observableを発火する
  • ただし、switchMapと逆で、内部Observableが終了するまではソースObservableの値を捨てていく(内部Observable優先)
    • ソースObservableの値が捨てられる可能性があることに注意
    • やっぱり内部Observableが終了しないと、無限に終わらない

buffer

  • bufferに指定したObservableが発火するまでは、オリジナルのObservableを溜め込んでおき、発火したら配列として値を流すObservableを作ってくれる
  • 他にも一定数カウントごとに発火、秒数毎に発火、Observableをトリガーにしてトグルするものなど亜種が結構ある

window

  • "Branch out the source Observable values as a nested Observable whenever windowBoundaries emits."
  • window側に指定したObservableから値が流れるたびに、branchを作成する

Filter Operator

Observableから流れてくる値に対してフィルタリングしていくやつ。

  • take
    • 一番多く見るフィルター
    • 指定した数だけ値が届くと、Observableを完了する
  • filter
    • 文字通り、配列のfilterと同じ動きをするフィルター
  • takeLast
    • 最後からn番目までの値をまとめて取り出したObservableを作ってくれる
  • first, last
    • これも文字通り、最初 or 最後の値だけを返す
    • といっても返すのはObservableなのに注意
  • distinct
    • ダブってたら吐き出さないやつ
  • skip
    • 指定した数だけ値をスキップする
  • sample
    • 内部Observableに値が届いたタイミングで、ソースObservableの直近の値を取り出して流してくれる

Join Operator

Observable-of-Observable、つまり入れ子になったObservableをよしなにしてくれる。flattenに似ている。

  • mergeAll
    • Higher observableをflat化する
  • concatAll
    • Higher observableをflat化する
    • その他のconcat系と同様、内部Observableが完了次第、次のObservableが流れる
  • exhaust
    • Higher observableをflat化する
    • ただし、内部Observableが完了後、すでに別の内部Observableがスタートしていた場合、それは無視し、次の新しい内部Observableが発火するまで待機する
  • combileAll
    • "Flattens an Observable-of-Observables by applying combineLatest when the Observable-of-Observables completes."
    • Higher observable完了時に、combineLatestを用いてflat化する
    • どういうときに使うのかな

Subjectと仲間たち

Observableでもあり、ObserverでもあるSubject。IObservable<T>IObserver<T>を実装したもの。

Subjectのsubscribeにovserverを割り当てれば、マルチキャストしてくれる。
逆に、他のObservableのsubscribeに登録すれば、値を受け取ることが出来る。

import { Subject, from } from 'rxjs';

const subject = new Subject<number>();        // 1

subject.subscribe({                           // 2-1
  next: (v) => console.log(`observerA: ${v}`)
});
subject.subscribe({                           // 2-2
  next: (v) => console.log(`observerB: ${v}`)
});

const observable = from([1, 2, 3]);

observable.subscribe(subject);                // 3
  1. Subjectを作成
  2. subscribeを2つ登録。それぞれ異なる処理が書いてある
  3. Observableでsubscribeする。こうすると、1,2,3が順番に流れる。

さらに、Subjectは、asObservable()メソッドでObservable化することもできる(Observableインスタンスが返ってくる)
このとき、Subjectのnext(value)で値を流すと、Observableインスタンスで受け取ることができる。
(Subjectを使って任意のタイミングで値を流せる、とも言えそう)

次のように使うっぽい

  • あるロジックで変化があったら、値を流す(nextを使う)
  • 他方、別のサービスでasObservable()で作成したObservableを利用し、値を受け取り、observerで処理する

いくつかSubjectには亜種がある。

  • BehaviorSubject
    • 現在の値(直近の値)を保持する
    • subscribeに新たなobserverが登録されると、この値を流してくれる
  • ReplaySubject
    • 指定された個数の値を保持する
    • subscribeに新たなobserverが登録されると、この値を次々に流してくれる
  • AsnycSubject
    • 最後の値を保持する
    • subjectが完了(complete)したら、subscribeに登録されているobserverに最後の値だけ流す

Schedular

Subscriptionの開始(データの流れ始め)と通知を制御するもの。文字通りスケジュールを制御する。
概念だけ理解しておけばいいやつ。

observeOn

operatorの一種。Docsには、 "Re-emits all notifications from source Observable with specified scheduler." と書いてある。
指定されたスケジューラを使用して、ソースObservableからのすべての通知を再発行できる。
引数は、(scheduler: SchedulerLike, delay: number = 0) となっていて、第一引数 SchedularLikeがスケジューラ。

使い所の例:

import { interval } from 'rxjs';
import { observeOn } from 'rxjs/operators';

const intervals = interval(10);                // 1

intervals.pipe(
  observeOn(animationFrameScheduler),          // 2 
)
.subscribe(val => {
  someDiv.style.height = val + 'px';
});

ブラウザーの再描画の直前に、subscribeの値が呼び出される例。

  1. Creation Operator である interval() でObservableを作る
    なお、interval() オペレータも、内部でAsyncSchedularを使っている
  2. animationFramesSchedulerスケジューラをobserveOnで指定し、ブラウザ再描画の直前にスケジュールする

subscribeOn

同様のものにObservableのメソッド subscribeOn() がある。これは、subscribe()を指定されたスケジュールでsubscribeするもの。

import { of, merge, asyncScheduler } from 'rxjs';
import { subscribeOn } from 'rxjs/operators';

const a = of(1, 2, 3, 4).pipe(subscribeOn(asyncScheduler));
const b = of(5, 6, 7, 8, 9);
merge(a, b).subscribe(console.log);

もし、pipeでsubscribeOnオペレータを繋いでいない場合、ofオペレータは同期的に、つまり順番に発動するので、 1 2 3 4 5 6 7 8 9 と表示される。
subscribeOnオペレータをpipeしていると、of オペレータで作られたa Observableは非同期に、バックグラウンドで動作するため、 5 6 7 8 9 1 2 3 4 と表示される。

このとき、a Observableを非同期化しますよ〜と指定した asyncScheduler がスケジューラ。

Schedularの種類

  • AsyncScheduler
    • "Schedules work with setInterval. Use this for time-based operations."
    • は?どゆ意味?と思ったけど、ようはsetInterval()のように動いて、非同期処理になる
    • 時間にまつわるOperatorの多くは、当然非同期で動作しているので、内部的にはこれを使ってる
      • 逆に同期的=コードに書いてある順番に上から下へ、といった使い方は、RxJSの使い所としては少ないので、ほとんど非同期的なのでは
  • asapScheduler
    • "Schedules on the micro task queue, which is the same queue used for promises. Basically after the current job, but before the next job. Use this for asynchronous conversions."
    • マイクロタスクキューにスケジュールを加える
    • マイクロタスクはここを参照、あまりよく知らなかった
    • 非同期化するにあたり、process.nextTick() や Web workerなどを利用する
  • queueScheduler
    • "Schedules on a queue in the current event frame (trampoline scheduler). Use this for iteration operations."
    • 現在のイベントフレーム(trampoline scheduler)のキューにスケジュールを加える
    • 繰り返し動作向け
  • animationFrameScheduler
    • ブラウザの再描画直前にスケジュールされる

使い所
Creation Operatorなどで作るObservableを明示的に非同期にしたい場合には、 asyncScheduler を指定することになるのだろう。
また、animationFrameScheduler は、アニメーションをスムーズにするのに使うのが一般的なようだった。

Ref

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

componentDidMountでReflow

chromeブラウザでReflowがCSSアニメーションと相性悪いようで、余計なアニメーションが走ってしまうことがある。

画面内容

画面遷移が発生する時に、次のように遷移先の画面が右側から左に移動するようCSSアニメーションが実装されている。

    &SlideFromRight-enter,
    &SlideFromRight-appear {
        left: 100%;
        transition: all 300ms cubic-bezier(.165, .84, .44, 1);
        pointer-events: none;
        opacity: 0;

        &-active {
            left: 0;
            opacity: 1;
        }
    }

    &SlideFromRight-exit {
        left: 0;
        transition: all 300ms cubic-bezier(.165, .84, .44, 1);
        pointer-events: none;
        opacity: 1;

        &-active {
            left: -100%;
            opacity: 0;
        }
    }

問題点

低スペックのandroid端末で、偶に真白な画面が描画されてしまって、アニメーションが止まってしまったように見える。

調査

React.ComponentのcomponentDidMountでwindow.innerHeightなどをアクセスしている為、Reflowが発生してしまう。その時、余計なアニメーションが走っていることがわかる。

window.innerHeightをアクセスする処理を削除すると、余計なアニメーションがなくなり、真白画面もなくなる。

image.png

対応

Reflowを発生させる上記処理をsetTimeoutで遅延することで、余計なアニメーションがなくなる。

参考記事

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

async/await スリープ関数

// スリープ
const sleep = delay => new Promise(resolve => setTimeout(resolve, delay));

(async() => {
  // 5秒待機
  await sleep(5000);
})();
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[JavaScript] "10" + 1 は "101" だが "10" - 1 は 9 を理解する

そういうものなんです!仕様です!(完)

だけではあれなので、仕様をちゃんとみてみる

加算演算子(+)

12.8.3 The Addition Operator ( + )をみていきます。

NOTE The addition operator either performs string concatenation or numeric addition.

12.8.3.1 Runtime Semantics: Evaluation
AdditiveExpression:AdditiveExpression+MultiplicativeExpression

加算演算子は文字の連結と数値の加算を担うことがわかりました。
次に処理の流れをみていきましょう。

  1. Let lref be the result of evaluating AdditiveExpression.
  2. Let lval be ? GetValue(lref).
  3. Let rref be the result of evaluating MultiplicativeExpression.
  4. Let rval be ? GetValue(rref).
  5. Let lprim be ? ToPrimitive(lval).
  6. Let rprim be ? ToPrimitive(rval).
  7. If Type(lprim) is String or Type(rprim) is String, then
    a. Let lstr be ? ToString(lprim).
    b. Let rstr be ? ToString(rprim).
    c. Return the string-concatenation of lstr and rstr.
  8. Let lnum be ? ToNumeric(lprim).
  9. Let rnum be ? ToNumeric(rprim).
  10. If Type(lnum) is different from Type(rnum), throw a 1. TypeError exception.
  11. Let T be Type(lnum).
  12. Return T::add(lnum, rnum).

流れを確認すると7.で型チェックをしていますね。
どうやら評価する値がStringだった場合はToStringする仕様となっています。
そうでない場合はToNumericですね。

確認してわかったこと

加算演算子(+)は文字の連結と数値の加算の機能を担っている。
評価する値を型チェックして文字列か数値に変換している。

減算演算子(ー)

12.8.4 The Subtraction Operator ( - )をみていきます。

12.8.4.1 Runtime Semantics: Evaluation
AdditiveExpression:AdditiveExpression-MultiplicativeExpression

1. Let lref be the result of evaluating AdditiveExpression.
1. Let lval be ? GetValue(lref).
1. Let rref be the result of evaluating MultiplicativeExpression.
1. Let rval be ? GetValue(rref).
1. Let lnum be ? ToNumeric(lval).
1. Let rnum be ? ToNumeric(rval).
1. If Type(lnum) is different from Type(rnum), throw a TypeError exception.
1. Let T be Type(lnum).
1. Return T::subtract(lnum, rnum).

お気づきになったと思いますが、型チェックがないですね。
減算演算子には文字列連結の機能はないので型変換する必要がないということでしょう。
評価する値を数値に変換してそのまま処理していることがわかりました。

確認してわかったこと

減算演算子(ー)は数値の減算を担う機能。
評価する値を(型チェックせずに)数値に変換している。

まとめ

加算演算子(+)は、型チェックの結果、文字の連結として処理されるから"10" + 1 は "101"となる。
減算演算子(ー)は、数値の減算のみを行うから"10" - 1 は 9となる。

これで何故 '10' + 1 は '101' だが '10' - 1 は 9になるのかが確信を持って言えるようになりました! :tada:

これからはこんな挙動を質問されてもキリッと答えられますね!

"10" + 1       // => '101'
"10" - 1       // => 9
"10" + 1 - 1   // => 100
"10" + (1 - 1) // => "100"
"1" - 1 + "1"  // => "01"
1 - "1" + 1    // => 1

余談

「'10' + 1 は '101' だけど '10' - 1 は 9 なのなんでだろ :thinking:
みたいな質問が流れてきたのがきっかけでした。

ちゃんと根拠を持って教えてあげようと思い、初めて仕様書リーディングに挑戦してみました。
こちらが、仕様書を読む勇気をもらった素晴らしい記事です。(唐突な宣伝。)
JavaScriptの「継承」はどう定義されるのか仕様書を読んで理解する - Qiita

いつもだったら、「キモい動きするなー、雑学として覚えておこう。」くらいで終わっていたと思います。やっぱり仕様は大事ですね。

参考

https://tc39.es/ecma262/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

雑談に応答するLINE Botを爆速で作成する方法(Google Apps Script + Chaplus雑談対話API)

対象読者

・手軽にLINE Botを作って見たい方
・LINE Botに会話(雑談)をさせてみたい方

概要

Google Apps ScriptとChaplus 雑談対話APIを利用してLINE Botを爆速で作成する方法について紹介します。(作業所用時間は10分程度です)

関連の記事として、Google Apps ScriptとChaplus 雑談対話APIを利用したSlack Botの作り方に関しましては以下の記事で紹介しているので、ぜひ併せてお読みください。

たった10分で雑談ができる脱力系Slack Botを作成する方法(セリフや口調のカスタマイズも簡単)

どんな雑談LINE Botを作るのか

S__25419855.jpg

  • 暇なときにたわいもない雑談ができる
  • 愚痴や悩みを聞いてくれる
  • 手軽にセリフのカスタマイズができる
  • 飽きてきたら口調を変えられる

作り方

利用する技術

  • Google Apps Script (GAS)
    GASでユーザからLINE Botへの発話を受け取り、Botの応答を送信します。

  • Chaplus - 雑談対話API
    Chaplus APIというユーザの発話に対しての応答を取得できるAPIを利用します。
    様々な雑談が可能なことに加え、ボットのセリフのカスタマイズや口調の変更等ができます。
    Chaplus APIは下記の記事で詳しく紹介をしています。
    https://qiita.com/maKunugi/items/b1afb6441571119729a7

  • LINE Messaging API
    LINEが提供するサービスとLINEユーザーの双方向コミュニケーションを可能にするAPIです。
    Messaging API

Messaging API 利用の準備

Messaging APIを利用するための準備をしていきます。
Messaging APIの利用にはLINE Developersに登録が済んでいる必要があります。

https://developers.line.me/ja/
上記のページからLINE Developersに登録をしてください。(個人のLINEアカウントを利用して登録が可能です)画面に沿いながら直感的に登録ができるので、詳細については省略します。

LINE Developersに登録が完了したら、下記を開き「プロバイダー」を作成していきます。
https://developers.line.biz/console/

プロバイダーが作成できたら次に「チャネル」を作成します。

チャネルの種類を聞かれるので「Messaging API」を選択します。

入力が必要な項目を埋めましょう。

利用規約に同意しチャネル作成が完了するとLINE Botが作られ、友達登録をしてLINEでトークをすることが可能になります。

チャネルは様々な設定が可能ですが、今回特に関わってくる項目を先に紹介しておきます。

  • Webhook設定

Messaging APIタブにある「Webhook設定」には後ほど作成するGASで作ったアプリケーションのURLが入ります。

  • チャンネルアクセストークン

後ほどGASで利用します。

  • 応答設定

応答モードや挨拶メッセージ等を設定できます。
今回はBotの自動応答を行うので、「応答モード」は「Bot」にしておきます。
Botは動的に生成した文章を返したいので、「詳細設定」の「応答メッセージ」については「オフ」を選択します。

以上でLine Developers側での設定は終了です。
(最後にWebhook URLを設定しに戻ってきます。)

Google Apps Scriptの用意

Google Apps ScriptからMessaging APIを利用してLINE BOTの応答を実装していきます。

Google Apps Scriptの準備

LINE Botへのコメントを管理するスプレッドシートを用意します。
用意ができたら、「スクリプトエディタ」を開きましょう

LINE Botへのメッセージを受け取る

Google Apps ScriptでLINE Botへのメッセージを受け取りましょう。

var CHANNEL_ACCESS_TOKEN = '<チャネルのアクセストークン>';

function doPost(e) {
  var event = JSON.parse(e.postData.contents).events[0];
  var replyToken= event.replyToken;

  if (typeof replyToken === 'undefined') {
    return;
  }

  var userId = event.source.userId;
  var username = getUserName(userId);

  if(event.type == 'message') {
    var userMessage = event.message.text;
    // TODO: メッセージの返信処理
    return ContentService.createTextOutput(
      JSON.stringify({'content': 'ok'})
    ).setMimeType(ContentService.MimeType.JSON);
  }
}

function getUserName(userId){ 
  var url = 'https://api.line.me/v2/bot/profile/' + userId;
  var userProfile = UrlFetchApp.fetch(url,{
    'headers': {
      'Authorization' :  'Bearer ' + CHANNEL_ACCESS_TOKEN,
    },
  })
  return JSON.parse(userProfile).displayName;
}

LINE BotにユーザからメッセージがあるとMessaging APIがPostでevent情報を送ってくれます。event情報からユーザ情報やメッセージの内容を取得します。

メッセージに対するBotの応答を取得する

LINE Botが行うユーザへのリプライ用のメッセージを取得します。ユーザのメッセージに対する応答文を取得するには、先述したChaplus APIという雑談対話APIを利用します。

公式サイトにて利用規約の確認とメールアドレスの登録を行い、メールに送られてくるAPI KEYを入手します。

※ 送られてくるメールが迷惑メールに振り分けられることがあるので注意

API KEYが取得できたらChaplus APIを利用して応答文を取得しましょう。
応答文を取得し、一番精度の高い返答を返すメソッドは以下のような実装になります。

function getChaplusMessage(mes, username) {
  var dialogue_options = {
    'utterance': mes,
    'username' : username,
    'agentState' : {
      'agentName' : 'sample_bot',
      'age' : '0歳',
      'tone' : 'kansai'
    }
  }
  var options = {
    'method': 'POST',
    'contentType': 'text/json',
    'payload': JSON.stringify(dialogue_options)
  };

  var chaplusUrl = "https://www.chaplus.jp/v1/chat?apikey=<APIキー>";
  var response = UrlFetchApp.fetch(chaplusUrl, options);
  var content = JSON.parse(response.getContentText());

  var answer = content.bestResponse.utterance;
  return answer;
}

API KEYは任意の値に変更してください。
上記では下記の項目を指定しています。

  • utterance: ユーザの発話
  • username: ユーザの名前
  • agentState
    • agentName: LINE Botの名前
    • age: LINE Botの年齢(設定)
    • tone:LINE Botの口調

LINE Botの口調は、normal(default)、kansai(関西弁風)、koshu(甲州弁風)、dechu(赤ちゃん言葉風)を指定できます。
APIの仕様については詳しく触れませんが、他にもセリフをカスタマイズできたり、NGワードを指定するなど様々な利用方法ができます。詳しくは下記の記事をご参照ください。

https://qiita.com/maKunugi/items/b1afb6441571119729a7

LINE Botのリプライを送信する

応答が取得できたらLINE Botでリプライを送信します。
下記のようにメッセージを送信します。

function sendMessage(replyToken, replyMessage) {
  var url = 'https://api.line.me/v2/bot/message/reply';
  UrlFetchApp.fetch(url, {
     'headers': {
       'Content-Type': 'application/json; charset=UTF-8',
       'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN,
     },
     'method': 'post',
     'payload': JSON.stringify({
       'replyToken': replyToken,
       'messages': [{
         'type': 'text',
         'text': replyMessage,
       }],
     }),
   });
}

スプレッドシートにログを残す

必要であればスプレッドシートにログを残しましょう。

function appendRow(text) {
  var spreadsheetId = "<スプレッドシートのID>";
  var sheetName = "<スプレッドシートの名前>";
  var spreadsheet = SpreadsheetApp.openById(spreadsheetId);
  var sheet = spreadsheet.getSheetByName(sheetName);
  sheet.appendRow([new Date(),text]);
  return text;  
}

スプレッドシートのIDはスプレッドシートファイルのURLに記載があります。
例: https://docs.google.com/spreadsheets/d/<スプレッドシートのID>/edit#gid=0

Google Apps Script 全文

上記で紹介したスクリプトの全文になります。

var CHANNEL_ACCESS_TOKEN = '<アクセストークン';

function doPost(e) {
  var event = JSON.parse(e.postData.contents).events[0];
  var replyToken= event.replyToken;

  if (typeof replyToken === 'undefined') {
    return;
  }

  var userId = event.source.userId;
  var username = getUserName(userId);

  if(event.type == 'message') {
    var userMessage = event.message.text;
    var replyMessage = getChaplusMessage(userMessage, username);
    appendRow(userMessage);
    sendMessage(replyToken, replyMessage);
    return ContentService.createTextOutput(
      JSON.stringify({'content': 'ok'})
    ).setMimeType(ContentService.MimeType.JSON);
  }
}

function sendMessage(replyToken, replyMessage) {
  var url = 'https://api.line.me/v2/bot/message/reply';
  UrlFetchApp.fetch(url, {
     'headers': {
       'Content-Type': 'application/json; charset=UTF-8',
       'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN,
     },
     'method': 'post',
     'payload': JSON.stringify({
       'replyToken': replyToken,
       'messages': [{
         'type': 'text',
         'text': replyMessage,
       }],
     }),
   });
}

function getUserName(userId){ 
  var url = 'https://api.line.me/v2/bot/profile/' + userId;
  var userProfile = UrlFetchApp.fetch(url,{
    'headers': {
      'Authorization' :  'Bearer ' + CHANNEL_ACCESS_TOKEN,
    },
  })
  return JSON.parse(userProfile).displayName;
}

function getChaplusMessage(mes, username) {
  var dialogue_options = {
    'utterance': mes,
    'username' : username,
    'agentState' : {
      'agentName' : 'sample_bot',
      'age' : '0歳',
      'tone' : 'kansai'
    }
  }
  var options = {
    'method': 'POST',
    'contentType': 'text/json',
    'payload': JSON.stringify(dialogue_options)
  };

  var chaplusUrl = "https://www.chaplus.jp/v1/chat?apikey=<APIキー>";
  var response = UrlFetchApp.fetch(chaplusUrl, options);
  var content = JSON.parse(response.getContentText());

  var answer = content.bestResponse.utterance;
  return answer;
}

function appendRow(text) {
  var spreadsheetId = "<スプレッドシートのID>";
  var sheetName = "log";
  var spreadsheet = SpreadsheetApp.openById(spreadsheetId);
  var sheet = spreadsheet.getSheetByName(sheetName);
  sheet.appendRow([new Date(),text]);
  return text;  
}

チャネルのアクセストークンやChaplus APIのAPIキー、スプレッドシートのID等は適宜リプレイスしてください。

デプロイ

ここまで記述したら、スクリプトをデプロイします。

「公開」-> 「ウェブアプリケーションとして導入」を選択します。

公開をするとアプリケーションのURL(Current web app URL)が取得できます。
このURLをLINE DevelopersでMessaging APIのWebhook URLに設定すれば作業は完了です!

S__25419855.jpg

LINEで話しかけてみましょう!

まとめ

GAS + Chaplus API を利用して雑談LINE Botを作成する方法について紹介しました。LINE Botの開発に興味のある方の参考になれば幸いです!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Stripe決済のDjangoでの実装

初めに

Stripeを使えば比較的簡単に決済機能を実装できます。この記事ではStripeが用意した決済フォームにページをリダイレクトさせることで、自サイトではクレジットカード情報を保持することなく支払い機能を自サイトに組み込むことができます。

基本はStripeのドキュメントGithub上のサンプルを基にして実装しています。また、Djangoで実装します。正直、試行錯誤しながら適当に作ったので実装に関して突っ込みどころ満載です。

最終的にはStripeのサンドボックスの劣化版が実装されます。また、以下の情報は2020年3月時点のものです。

環境設定

  • Python==3.8.1
  • Django==3.0.4
  • stripe==2.43.0
  • django-environ==0.4.5

Step0: 前準備

適当にディレクトリを作成する。

mkdir stripe-test
cd stripe-test

pipenvを使って仮想環境を構築します。

pipenv --python 3.8
pipenv install django stripe django-environ

pipenv shellで仮想環境を起動する。pipenvについてさらに詳しく知りたい場合はPipenvを使ったPython開発まとめを参照。

# shell起動
pipenv shell

Djangoプロジェクトを作成する。

django-admin startproject stripe_test .
django-admin startapp checkout

Stripeにアカウント登録

Stripeにアカウント登録する。アカウント作成やダッシュボードの見方についてはRailsのdeviseとStripe(とBootstrap)の組み合わせで、ただただユーザー登録させたユーザーに定期更新購読させるだけのappを作りました②に詳しい。

公開ビジネス情報を入力する。2020年3月現在だと以下のURL。

次に開発用の公開キーとシークレットキーを確認する。2020年3月現在だと以下のURL。

Django Setting

Djangoのsettingを以下のようにする。.envファイルを取り込むのにdjango-environを使用している。

stripe_test/stripe_test/settings.py
import os
import environ

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PROJECT_NAME = os.path.basename(BASE_DIR)

# django-environから環境変数を読み込む
env = environ.Env()
env.read_env(os.path.join(BASE_DIR, '.env'))

### 省略 ###

INSTALLED_APPS = [
    ...,
    'stripe',#追加
    'checkout.apps.CheckoutConfig', # 自作 チェックアウト
]

### 省略 ###

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')], #デフォルトから変更
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

### 省略 ###
# Static files (CSS, JavaScript, Images)

STATIC_URL = '/static/'
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]

STRIPE_SECRET_KEY = env('STRIPE_SECRET_KEY')
STRIPE_PUBLISHABLE_KEY = env('STRIPE_PUBLISHABLE_KEY')

.envには例えばこういう設定を行う。使用通貨がアメリカドル(usd)の場合はセント単位で計算するので1234と入力した場合は$12.34となる。

.env
# Stripe API keys
STRIPE_PUBLISHABLE_KEY=pk_12345
STRIPE_SECRET_KEY=sk_12345

# Checkout settings 
DOMAIN=http://localhost:8000/
BASE_PRICE=1099
CURRENCY=usd

Step1:サーバー側でチェックアウト・セッションを実装する

セッションの作成はサーバー側で行う。Python + Django でJSONの送受信を参考にして、以下のようにDjangoを使って、Checkout Sessionを作成する。

stripe_test/checkout/views.py
# -*- coding: utf-8 -*-
import logging
import os
import json

from django.shortcuts import render
from django.conf import settings
from django.views import generic

import stripe
from django.http.response import JsonResponse
from django.views.decorators.csrf import csrf_exempt

logger = logging.getLogger(__name__)

@csrf_exempt
def onetime_payment_checkout(request):
    if request.method == 'POST':
        data = json.loads(request.body)
        domain_url = os.getenv('DOMAIN')
        stripe.api_key = settings.STRIPE_SECRET_KEY
        try:
            # Create new Checkout Session for the order
            # ?session_id={CHECKOUT_SESSION_ID} means the redirect will have the session ID set as a query param
            checkout_session = stripe.checkout.Session.create(
                success_url=domain_url +
                "checkout/success?session_id={CHECKOUT_SESSION_ID}",
                cancel_url=domain_url + "checkout/canceled/",
                payment_method_types=["card"],
                line_items=[
                    {
                        "name": "Pasha photo",
                        "images": ["https://picsum.photos/300/300?random=4"],
                        "quantity": data['quantity'],
                        "currency": os.getenv('CURRENCY'),
                        "amount": os.getenv('BASE_PRICE'),
                    }
                ]
            )
            logger.debug( str(checkout_session))
            return JsonResponse({'sessionId': checkout_session['id']})
        except Exception as e:
            logger.warning( str(e) )
            return JsonResponse({'error':str(e)})

Step2:チェックアウトにリダイレクト

基本的にJavascriptを使って実装していく。実現していることはstripe.jsを使ってStripe側にデータを渡して、Stripeで決済してもらった後に自サイトに戻る。

stripe_test/templates/base.html
{% load i18n static %}
<!DOCTYPE html>{% get_current_language as LANGUAGE_CODE %}
<html lang="{{ LANGUAGE_CODE|default:"en-us" }}">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}">
        <title>{% block title %}{% endblock %}</title>
        <script src="{% static 'js/jquery-3.3.1.slim.min.js' %}"></script>
        <script src="{% static 'js/bootstrap.min.js' %}"></script>
        {% block additional_script %}
        {% endblock %}
    </head>
    <body>
        <div class="container">
            {% block content %}
            {% endblock %}
        </div>
    </body>
</html>
stripe_test/templates/checkout/checkout_test.html
{% extends "base.html" %}

{% load i18n static %}
{% block title %}Checkout_test{% endblock title %}

{% block additional_script %}
<script src="https://js.stripe.com/v3/"></script>
<script src="{% static 'checkout/js/script.js' %}"></script>
{% endblock additional_script%}


{% block content %}
<div>
    <h1 data-i18n="headline"></h1>
    <h4 data-i18n="subline"></h4>
    <div class="pasha-image">
      <img
        src="https://picsum.photos/280/320?random=4"
        width="140"
        height="160"
      />
    </div>
  </div>
  <div class="quantity-setter">
    <input type="number" id="quantity-input" min="1" max="2" value="1" />
  </div>
  <p class="sr-legal-text" data-i18n="sr-legal-text"></p>

  {% csrf_token %}
  <button
    id="submit"
  >PAYMENT</button>
</section>
<div id="error-message"></div>
</div>
{% endblock content %}

自作Scriptは以下のサイトを参考にした
- 参考Github
- Fetch を使う

stripe_test/static/checkout/js/script.js
/* Handle any errors returns from Checkout  */
var handleResult = function(result) {
    if (result.error) {
      var displayError = document.getElementById("error-message");
      displayError.textContent = result.error.message;
    }
  };

  // Create a Checkout Session with the selected quantity
  var createCheckoutSession = function() {
    var inputEl = document.getElementById("quantity-input");
    var quantity = parseInt(inputEl.value);
    return fetch("/checkout/create-checkout-session/", {
      method: "POST",
      headers: {
        "Content-Type": "application/json"
      },
      body: JSON.stringify({
        quantity: quantity,
      })
    }).then(function(result) {
      return result.json();
    });
  };

  /* Get your Stripe publishable key to initialize Stripe.js */
  fetch("/checkout/config")
    .then(function(result) {
      return result.json();
    })
    .then(function(json) {
      window.config = json;
      var stripe = Stripe(config.publicKey);
      // Setup event handler to create a Checkout Session on submit
      document.querySelector("#submit").addEventListener("click", function(evt) {
        createCheckoutSession().then(function(data) {
          stripe
            .redirectToCheckout({
              sessionId: data.sessionId
            })
            .then(handleResult);
        });
      });
    });

HTMLとJavascriptが実装できたら、checkout_test.htmlが見られるようにviews.pyを以下を追加する。

stripe_test/checkout/views.py
class IndexView(generic.TemplateView):
    template_name = "checkout/checkout_test.html"
    def get(self, request, *args, **kwargs):
        return render(request, self.template_name )

とりあえずはcheckoutアプリのマッピング情報を記述して、最初のページだけ見られるようにする。これはcheckout/urls.pyを以下のように編集することで実現できる。

stripe_test/checkout/urls.py
from . import views
from django.urls import path,include

app_name = 'checkout'

urlpatterns = [
    path('',views.IndexView.as_view(), name="index"),
]

Djangoアプリではない、ホームページ側のurls.py(この記事ではstripe_test/stripe_test/urls.py)を以下のように修正する。

stripe_test/stripe_test/urls.py
from django.contrib import admin
from django.urls import path,include
from django.views.generic import RedirectView

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', RedirectView.as_view(url='/checkout')),
    path('checkout/', include('checkout.urls')),
]

上記自作のjavascript(stripe_test/static/checkout/js/script.js)ではfetch("/checkout/config")を行っているが、これはjsonでstripeAPIの公開キーを受け取っている。サーバー側の実装は以下のとおり。

stripe_test/checkout/views.py
@csrf_exempt
def stripe_config(request):
    if request.method == 'GET':
        stripe_config = {
            'publicKey': settings.STRIPE_PUBLISHABLE_KEY,
            'basePrice': os.getenv('BASE_PRICE'),
            'currency': os.getenv('CURRENCY'),
        }
        return JsonResponse(stripe_config, safe=False)
stripe_test/checkout/urls.py
from . import views
from django.urls import path,include

urlpatterns = [
    ...,
    path('config/',views.stripe_config),
    ...,
]

python manage.py runserverでとりあえずサイトを起動してhttp://localhost:8000/にアクセスすると以下の感じの画面が出るはずだ。

サンプルページ.png

ただ、これだとPAYMENTを押しても、何も起きないはず。なぜなら、ボタン押されたあとの実装を行っていないから。具体的には自作javascriptのcreateCheckoutSession関数はとあるURLにPOSTしているが、Django側で設定していないので、設定を行う必要がある。それにはurls.pycreate-checkout-sessionを追加すればいい。

stripe_test/checkout/urls.py
urlpatterns = [
    ...,
    path('create-checkout-session/',views.onetime_payment_checkout , name='create-checkout'),
    ...,
]

以上まで実装を行えば、Stripe側にURLが飛んで決済情報を入力する画面が表示されるはずです(参考:Stripe Checkout)。後は、決済情報を入力し終わったリダイレクト先のページ(success.htmlcanceled.html)を実装していけば、一通り決済はできるようになっている。

テスト

テスト用の決済カードは複数用意されているので、適宜選ぶ。特にこだわりがないのならば 4242424242424242を使用。

もし、うまく実装できていればダッシュボード上に支払いが反映されているので、確認してみてください。

参考:Github

上記の実装はGithubに公開しているので、参考にしてください。
- Github上のサンプル

参考サイト

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

for文を使わないでやってみた

はじめに

for文は色々な言語で用意されており、様々なところで使われています。
実際、同じ処理を複数回するときに便利です。
また、for文が使われていれば、容易に繰り返し同じことをしていると分かります。
簡単であり、可読性が高いという点で、for文は優れていると思います。

しかし、それは処理という面で捉えるからです。
オブジェクトという面で見たときは、拡張for文の登場によって扱えるとは言え、優れているとは言えないでしょう。
JavaScriptにはちゃんと Array というビルドインオブジェクトが用意されているので、それを使って繰り返す処理を書くことも選択肢に入れましょう。

という訳で、今回はfor文からArrayを使った書き方に移行する方法を紹介していきたいと思います。

指定回数繰り返す

10回ログに出力します。

// for文
for(let i=0; i<10; i++) {
    console.log('repeat:', i);
}


// Array
[...Array(10)].forEach((_, i) => {
    console.log('repeat:', i);
});

[...Array(10)] のようにすることで、固定長の配列を作成することができます。

ここで使っているのは、 Array.prototype.forEach() です。
与えられた配列の中身を順番に実行して、新しく配列を生成する必要の無い場合は、これを使うことができます。

指定範囲の配列を作成する

1から10までの2つおきの配列を作成します。

// for文
const vFor = [];
for(let i=1; i<=10; i+=2) {
    vFor.push(i);
}
console.log(vFor); // [ 1, 3, 5, 7, 9 ]


// Array
const range = (start: number, stop: number, step: number) => {
    return Array.from({ length: (stop - start) / step + 1}, (_, i) => start + (i * step));
}
const vArray = range(1, 10, 2);
console.log(vArray); // [ 1, 3, 5, 7, 9 ]

ここで使っているのは、 Array.from() です。
新しく配列を生成したいときに使うことができます。

配列の値それぞれを加工する

5人の国語・数学・英語・理科・社会の5教科の合計得点を計算します。

const scores = [
    { '国語': 85, '数学': 78, '英語': 92, '理科': 62, '社会': 69 },
    { '国語': 23, '数学': 44, '英語': 78, '理科': 44, '社会': 50 },
    { '国語': 86, '数学': 10, '英語': 56, '理科': 82, '社会': 90 },
    { '国語': 31, '数学': 42, '英語': 96, '理科': 49, '社会': 68 },
    { '国語': 70, '数学': 55, '英語': 48, '理科': 89, '社会': 11 },
];

// for文
const vFor = [];
for(const score of scores) {
    vFor.push(score.国語 + score.数学 + score.英語 + score.理科 + score.社会);
}
console.log(vFor); // [ 386, 239, 324, 286, 273 ]


// Array
const vArray = scores.map((score) => {
    return score.国語 + score.数学 + score.英語 + score.理科 + score.社会;
});
console.log(vArray); // [ 386, 239, 324, 286, 273 ]

ここで使っているのは、 Array.prototype.map() です。
Array.prototype.forEach と異なり、与えられた配列の中身を順番に実行して、新しく配列を作成したい場合に、これを使うことができます。

配列の値をまたがって加工して1つの値を作成する

教科ごとの合計得点を計算します。

const scores = [
    { '国語': 85, '数学': 78, '英語': 92, '理科': 62, '社会': 69 },
    { '国語': 23, '数学': 44, '英語': 78, '理科': 44, '社会': 50 },
    { '国語': 86, '数学': 10, '英語': 56, '理科': 82, '社会': 90 },
    { '国語': 31, '数学': 42, '英語': 96, '理科': 49, '社会': 68 },
    { '国語': 70, '数学': 55, '英語': 48, '理科': 89, '社会': 11 },
];

// for文
const vFor = { '国語': 0, '数学': 0, '英語': 0, '理科': 0, '社会': 0 };
for(const score of scores) {
    vFor.国語 += score.国語;
    vFor.数学 += score.数学;
    vFor.英語 += score.英語;
    vFor.理科 += score.理科;
    vFor.社会 += score.社会;
}
console.log(vFor); // { '国語': 295, '数学': 229, '英語': 370, '理科': 326, '社会': 288 }


// Array
const vArray = scores.reduce((previous, score) => {
    return {
        '国語': previous.国語 + score.国語,
        '数学': previous.数学 + score.数学,
        '英語': previous.英語 + score.英語,
        '理科': previous.理科 + score.理科,
        '社会': previous.社会 + score.社会,
    };
}, { '国語': 0, '数学': 0, '英語': 0, '理科': 0, '社会': 0 });
console.log(vArray); // { '国語': 295, '数学': 229, '英語': 370, '理科': 326, '社会': 288 }

ここで使っているのは、 [Array.prototype.reduce()] です。(https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce) です。
与えられた配列の中身を順番に実行して、1つの値を作成したい場合に、これを使うことができます。

データを基に並列処理を行う

Yahoo! JapanGoogle へリクエスト送信を並列処理で行います。

import fetch from 'node-fetch';

const urls = [
    'https://www.yahoo.co.jp/',
    'https://www.google.com/',
];

// for文
(async () => {
    const promises = [];
    for(const url of urls) {
        promises.push((async () => {
            const response = await fetch(url);
            return await response.text();
        })());
    }
    console.log(await Promise.all(promises));
})();


// Array
(async () => {
    console.log(
        await Promise.all(
            urls.map(async (url) => {
                const response = await fetch(url);
                return await response.text();
            })
        )
    );
})();

リクエストの送信には、 node-fetchを使用しています。
Array側の方が階層は深くなってしまっていますが、書いている内容はシンプルです。
内容としては、 Array.prototype.map() を使って、 Promise が返る配列を作成し、それを Promise.all に渡しています。

(番外編)for...in

教科の一覧を表示します。

const score = { '国語': 86, '数学': 10, '英語': 56, '理科': 82, '社会': 90 };

// for文
for(const key in score) {
    console.log(key);
}


// Array
Object.keys(score).forEach((key) => {
    console.log(key);
});

for...in の書き換えには、 Object.keys() を併用します。
keyの一覧を配列として取得してからは、 Array.prototype.forEach() でも Array.prototype.map() でも Array.prototype.reduce() でも使用することができます。

おわりに

for文とArrayを使った書き方は、どちらがお好みでしょうか?
試しに作っているコードを、for文からArrayへ、Arrayからfor文へ書き直してみてください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

コピペで始めるNext.js + Firebase Hosting 【静的サイト】

概要

Next.jsで静的サイトをホスティングする手順

※この記事はさくっとデプロイまでの手順を記述しているだけなので、解説とかはありません。

Next.js公式

準備

firebase-toolsをインストールしておく
Firebase CLI リファレンス

Next.jsプロジェクトの作成

$ npm init <プロジェクト名>
$ cd <プロジェクト名>
$ npm install --save next react react-dom

package.jsonscriptsを追加する

package.json
"scripts": {
  "dev": "next",
  "build": "next build",
  "start": "next start",
  "export": "next build && next export"
}

typescriptの導入

typescriptを使用しない場合は飛ばしてください

npm install --save-dev typescript @types/react @types/node

index.jsとabout.jsを作成

src/pages/index.js
const Home = () => {
  return <div>Home</div>;
};

export default Home;
src/pages/about.js
const About = () => {
  return <div>About</div>;
};

export default About;

firebaseの設定

$ firebase init
...
/* アプリをデプロイするだけなのでHostingのみでOK */
 ◯ Database: Deploy Firebase Realtime Database Rules
 ◯ Firestore: Deploy rules and create indexes for Firestore
 ◯ Functions: Configure and deploy Cloud Functions
❯◉ Hosting: Configure and deploy Firebase Hosting sites
 ◯ Storage: Deploy Cloud Storage security rules
/* 既存のFirebaseプロジェクトを指定するか、新しくプロジェクトを作る */
? Select a default Firebase project for this directory: 
  [don't setup a default project] 
❯ next-static-sample (next-static-sample) 
  [create a new project]
/* この辺の設定はお好きにどうぞ */
? What language would you like to use to write Cloud Functions? 
? Do you want to use TSLint to catch probable bugs and enforce style? (Y/n)
? Do you want to install dependencies with npm now?
? What do you want to use as your public directory?
? Configure as a single-page app (rewrite all urls to /index.html)? 

設定が終わると「.firebaserc」や「firebase.json」ファイルが生成されます。

以下のコマンドも入れておきましょう。
.firebasercで使用するプロジェクトを指定します。

$ firebase use default

※ 初期設定ではコンソールで設定したプロジェクトのみ存在していると思います。

firebase.jsonを以下のように変更

firebase.json
{
  "hosting": {
    "public": "out",
    "rewrites": [
      {
        "source": "/about",
        "destination": "/about.html"
      }
    ]
  }
}

src/pagesの中身を追加した際は、"rewrites"内の"about"のように追加していく

package.jsonscriptsに以下を追加する

package.json
"scripts": {
  "deploy": "firebase deploy"
}

デプロイ

ビルドしてデプロイ

$ npm run export
$ npm run deploy
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptメモ

JavaScriptメモ

データ型

  • 文字列(String) 'hello', "うおおおお"
  • 数値(Number) 1, 1.2, -9, -1.7, NaN
  • Undefined
  • null
  • 真偽値(Boolean) true, false
  • オブジェクト(Object) {a: 1, b: 2}

論理演算子

  • (等しい) ===
  • (等しくない) !==
  • (かつ) &&
  • (または) ||
  • (でない) !

条件演算子

main.js
条件式 ? 条件式がtrueのときの処理 : 条件式がfalseのときの処理

switch文

main.js
const number = 1;

switch (number) {
  case 9:
    console.log(9);
    break;
  case 8:
  case 7:
    console.log(8 + 'or' + 7);
    break;
  default:
    console.log('numberは9でも8でも7でもありません');
    break;
}

中身が一度は実行されるwhile文

main.js
yen = -10
do {
  console.log(`${yen} 円残ってます`);  // バッククオートと&{}で変数を埋め込む
  yen -= 10;
} while (yen > 0);

関数式

const 定数名 = function(仮引数, ...) {
処理;
return 返り値;
};

呼び出し→ 定数名(実引数, ...);

アロー関数

main.js
// const sum = function(a, b, c) {
//   return a + b + c;
// }; と同じ意味↓
const sum = (a, b, c) => a + b + c;

配列

main.js
const numbers = [0, 1, 2, 3];  // 配列
numbers.length; // 配列の長さ
numbers.push(4); // 配列の末尾に4を追加。削除はpop()
numbers.unshift(-1);  // 配列の先頭に-1を追加。削除はshift()
console.log(numbers);  // [-1, 0, 1, 2, 3, 4]
numbers.splice(1, 2, 5, 6);  // splice(変化が開始する位置, 削除数, 追加する要素, ...)
console.log(numbers);  // [-1, 5, 6, 2, 3, 4]

スプレット構文

...ってやつ!

main.js
    const numbers = [10, 20];
    const otherNumbers = [90, 80, ...numbers];

    console.log(otherNumbers); // [90, 80, 10, 20]

    function sum(a, b) {
        console.log(a + b);
    }
    sum(...numbers); // 30

分割代入

main.js
   const scores = [60, 40, 30, 80];

    const [a, b, c, d] = scores;
    console.log(a); // 60
    console.log(b); // 40
    console.log(c); // 30
    console.log(d); // 80

    const [e, f, ...others] = scores;
    console.log(e); // 60
    console.log(f); // 40
    console.log(others); // [30, 80]

    let x = 1;
    let y = 2;
    [x, y] = [y, x];
    console.log(x); // 2
    console.log(y); // 1
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む