- 投稿日:2020-03-23T23:42:33+09:00
初心者がElectron(NW.js)を始めてみて分かったことまとめ
目次
- はじめに
- 個人的にHTA(WSH?)とNW.jsはオススメしません
- electron開発の流れ
- electron-builderでビルドしようとすると
このシステムではスクリプトの実行が無効になっているため、~~~
というエラーが出るとき- 1つのプロジェクトを再ビルドしたいとき
- おわりに
はじめに
突然ですが、絵を描く用の画像資料が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をインストールしてください。
- ダウンロードする(私はWindows Installer (.msi)の64-bitをダウンロードしました)
- インストーラの指示に従ってインストールする
- 完了!
次におおまかにelectron開発の流れをなぞるとこんな感じです。
- プロジェクトフォルダ(名前は何でもOK:ここでは
moge
)を作るmoge
の中に必要なフォルダとファイルを作る(main.js
,index.html
,src/package.json
など)moge/package.json
を編集する- electronとelectron-builderをインストールする
- ビルドする
次で詳しく触れていきます。
プロジェクトフォルダの準備(名前は何でも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.json
、moge
直下の方をmoge/package.json
と呼ぶことにします。
(これ何で名前同じの2つ作らせる仕様なんですかね?ややこしい!)色んなの資料読んでるとどっちの
package.json
を指してるのか分かりにくくて初心者的には大変です。
体感ですが、moge/package.json
の方を指してることが多いと思います。ていうか多分そう。中身を編集する
まずは
main.js
からです。
main.js
は、どのプロジェクトでもほとんど同じようなものになるらしいです。
必要に応じて書き足したりするらしいですが、私にはよく分かりません。公式ドキュメントの読み方が分からない...!?♀️
main.jsconst { // アプリ作成用のモジュールを読み込む 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 -vPS D:\electron\moge> node -v v12.16.0確認できたら、
npm i -D electron
とnpm i -D electron-builder
を実行してください。
どちらも少し時間がかかります。
これは、electronとelectron-builderをmoge
フォルダにインストールするという工程です。
-D
を-g
にすることで、グローバルなインストールができるらしいですが、electronはローカルにインストールするのがいいらしいです?
下みたくなったらOKです。npm i -D electronPS D:\electron\moge> npm i -D electron + electron@8.0.2 added 85 packages from 93 contributors and audited 102 packages in 10.474snpm i -D electron-builderPS 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_modules
とpackage-lock.json
は何もしなくてOKです。プロジェクトフォルダmoge ├ node_modules ├ src │ ├ main.js │ ├ index.html │ └ package.json ├ package.json └ package-lock.json確認できたら、
npm start
を実行してください。npm startPS D:\electron\moge> npm start > moge@1.0.0 start D:\electron\moge > electron .テンション上がりますね!うおおお?
このままだとNode.jsやelectronがインストールされている環境でPowerShellから呼び出さないと起動できませんが、ビルドするとダブルクリックひとつ(?)で起動できるようになります!
次でビルドしていきます。ビルドする
ここによると、PowerShellはセキュリティの都合でよく分からないコマンドを実行できないような設定になってるらしいです。
electron-builderはその「よく分からないコマンド」に属しているらしく、初期設定のままだと実行できません。
なので、さっきの記事に従って設定を変更しましょう。
ちなみに私はSet-ExecutionPolicy RemoteSigned
を実行しました。そしたら、
electron-builder build --win
を実行しましょう。
この処理は1~2分くらいかかります。
下みたくなったら成功です。electron-builder build --winPS 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
と同じウィンドウが出てきましたね!
成功です?
以上がelectron開発の流れになります。
(長かった…)1つのプロジェクトを再ビルドしたいとき
単純にもう一回
electron-builder build --win
を実行するとエラーが出ることがあります。
そのときは、
dist
フォルダを削除するC:/Users/(Username)/AppData/Roaming/moge
を削除するで再ビルドできます。
もしフォルダを削除しようとして使用中なので削除できませんというエラーが出たときは、
moge 1.0.0.exe
を閉じる- タスクマネージャーでelectronアイコンのやつを全て閉じる
をするとフォルダを削除できるようになります。
(electron-rebuilderとかいうやつがあるとかないとか聞きますが、使い方が分かりません?♀️)おわりに
私が分かっていることは以上です。
こうして頭の中を整理してみましたが、今抱えている
「ビルドしたあとに外部ファイルを読み込ませるにはどうしたらいいか」
を解決する糸口は全くつかめませんでした?どなたか助けていただけると幸いです…
「公式ページのここに書いてありそう」とかでも構わないので教えてください…で、初心者の方にも分かりやすい構成を目指して書いてみたのですが、どうだったでしょうか(冗長すぎたかも)。
こんな記事でしたが、最後まで読んでいただいてありがとうございました!?
- 投稿日:2020-03-23T23:36:50+09:00
【学習記録】『スラスラ読める JavaScriptふりがなプログラミング』読書感想 4章編 (前半) 〜N予備校の復習を添えて〜
【学習記録】『スラスラ読める JavaScriptふりがなプログラミング』読書感想 3章編( https://qiita.com/Molly95554907/items/dc1654b7019442f893fa ) の続き
第四章 関数を作ろう
・アロー関数
オリジナルの関数を作成する。
let 関数名 = () => {
//関数として実行する処理
};以上で作成完了。
関数名();
を実行すると、//関数として実行する処理 が実行される
・引数を受け取る関数
・引数とは:関数の中で処理される変数のこと、と理解してる。
let 関数名 = (引数となる変数) =>{
//関数として実行する処理
console.log(引数となる変数 +'です');
};以上で作成完了
引数となる変数に文字や数値を代入できる
関数名('山田');
↓
山田ですが出力される。
・function文で関数を作る
function 関数名(引数となる変数){
//関数として実行する処理
}※function文で関数を作る時は、関数の終わりに ; が要らない!!!
今後はアロー関数が主流になっていく流れ!
【N予備校では一章「あなたのいいところ診断」から登場】
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-23T22:53:47+09:00
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を記述をする。本日は、ありがとうございました。
- 投稿日:2020-03-23T22:28:13+09:00
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的には問題ないので、ガンガン代入しちゃってください
- 投稿日:2020-03-23T21:58:24+09:00
【学習記録】『スラスラ読める 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
(復習)
ここでは、HTTPから与えられるパスがどれかによって、実行する関数を振り分けていた。・配列
配列とは、変数を箱として、中に値を入れられる仕組み…と今のところ理解してる。
let dog = ['柴犬', 'プードル', 'サモエド犬'];
dogという変数の中には'柴犬','プードル','サモエド犬'という値が入る。
要素とは、変数の箱の中にしまわれている値のこと。
インデックスとは、要素(箱の中にしまわれた値)に振り当てられた番号。dog[0]='柴犬'
【N予備校 第一章いいところ診断の配列】
変数answerの中に、配列として診断結果を入れる
添字(インデックス)から値を取り出す。
・配列を使ってできる操作
・配列.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予備校では三章・「集計処理を行うプログラム」で登場】
冒頭で
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の最初の要素を消していく。
- 投稿日:2020-03-23T21:41:02+09:00
【自分用】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が終わったら書きます。
- 投稿日:2020-03-23T20:49:06+09:00
ブックマークレット(Bookmaeklet)の書き方
ブックマークレットとは
ブックマークとは別もの?
ブックマークはサイトのURLが記載されていて
ブックマークをクリックするとそのサイトを開くものです。
一方ブックマークレットはJavascriptによるプログラムが書かれていて
クリックすると現在開いているページでプログラムを実行します。何が出来るの?
プログラムを実行することによって作業の自動化等が出来ます。
どうやって作るの?
最初にブックマークを作成し内容をJavascriptに書き換えます。
この時に内容の先頭はjavascript:としコードは一行で書かなければなりません。ブックマークレットの書き方
ブックマークレットはjavascriptを一行で書かなければなりません。
以下にある要素を繰り返しクリックする通常のコードを示します。通常のjavascriptconst 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);}以上がブックマークレットの書き方の簡単な説明です。
読んでくれて有難うございます
- 投稿日:2020-03-23T20:10:13+09:00
インフィニティ、ページネーション、もっと見る... は、けっきょくどれ?
インフィニティ、ページネーション、もっと見る... は、けっきょくどれ?
- 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 の ページネーション ナビゲーション
前後ボタン
- 市場においてこのUIについての呼び名が存在するかどうか不明...
- 前ページ、次ページのみの遷移ナビゲーション
- 1回分の情報表示の終端に前ページ、次ページへの遷移ボタンのみ配置される
- ページネーションのページ数リンク部分を削除したようなナビゲーションデザイン
- Airbnb、Amazon が採用
Airbnb の 前後ボタン ナビゲーション
Amazon の 前後ボタン ナビゲーション
"もっと見る" ボタン
- もっと見るボタン、Show more button、Load more button などと呼ばれる
- 1回分の情報表示の終端に「もっと見る」等とラベリングされたボタンを配置する
- ユーザーのボタンタップ操作により追加情報を表示する
- Google検索結果が採用
Google検索結果 の "もっと見る" ボタン ナビゲーション
ユーザー観点における比較
インフィニティスクロール
- ユーザーにとって、操作コストが低い
- 機能に対する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択以外の定番も今後間違いなく生まれてくる
- つまり、新定番を発明する余地がまだまだ存在する分野である
- 投稿日:2020-03-23T20:05:14+09:00
二重ビット否定(~~)を使ったコード 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
~-1
が0
になる性質を生かして、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
にその配列内での要素ごとの個数をカウントする事できます。
- 投稿日:2020-03-23T19:57:23+09:00
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 イベント発生元の要素の左上を原点とする座標 まだよくわかっていないが、
event.target
とevent.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()によるイベント処理の使い方!
- 投稿日:2020-03-23T19:36:32+09:00
【学習記録】『スラスラ読める 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('入力してね');」
ユーザーが入力した文字が、変数に代入される
・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になってしまう。
- 投稿日:2020-03-23T19:16:37+09:00
魔法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で配列を操るメソッドの面白さと奥深さ、少しでも伝われば幸いです。お付き合いありがとうございました。
- 投稿日:2020-03-23T18:36:13+09:00
魔法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
- 投稿日:2020-03-23T17:18:39+09:00
【Nuxt.js】Vue Router復習編:params, queryを使おう
前置き
前回の続きです!
基礎編の復習と思ってもらえれば⭕️https://note.com/aliz/n/ndf76ebe9853b
値を見てみよう!
consoleの値と
実際画面上でどうなるか
確認していきましょう??_idでどんな文字列がきても
良い状態にしておきます。【ディレクトリ 】
filepages/ --| _id/ -----| index.vueindex.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
【解説】
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.htmlindex.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
queryをuser=privateにしているので
{{ $router.query.user }}と変更したことで
privateが表示されていますね?pagination
ページネーションも
考え方はこれと同じです!
paramsは同じまま、
queryだけを変えていきます。
これにより同じページ内でソートを書け
1ページ目だけを表示、ということができます?
liの1を押せば1ページ目にいきます?【基礎構文】
変数を使う時は${変数}にします。
テンプレートリテラルについては
ここが分かりやすいです!
https://qiita.com/kura07/items/c9fa858870ad56dfec12userIdを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
【ディレクトリ 】
filecomponents/ --| RouterPush.vue pages/ --| user/ -----| _id.vue --| index.vueRouterPush.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
- 投稿日:2020-03-23T14:53:28+09:00
任意のボタンからの訪問はお問い合わせフォームのselectboxでその選択肢が選択済のまま表示されるようにする
やりたいことを日本語で表現するのはムズイデズネ...
やりたいこと
任意のページのボタンからのお問い合わせフォームへの訪問は、あらかじめその項目が選択済となってお問い合わせフォームが表示されるようにしてほしいとの要望があったので初心者ながらも調べてみました。
(これが最適かどうかもわかりませんのでより良い方法があれば教えてください)流れ
- 元のaタグにURLパラメーターを設置しておく
- 訪問先であるお問い合わせフォームにて、そのURLパラメーターの取得
- もしそのパラメーターのidとselectbox の選択肢(option)のidが一致すればその選択肢が選択済になるようにする
という流れにしようと思いました
1. 元のaタグにURLパラメーターを設置
<a href="hogehoge/contact/?id=lecture">2. 訪問先であるお問い合わせフォームにて、そのURLパラメーターの取得
// URLのパラメータを取得 var urlParam = location.search.substring(1);
location.search
は現在のページのクエリ情報(URLの末尾にある「?〜」)の文字を取得
▼参考サイト
JavaScriptのlocation.searchでクエリ情報を取得してみる
substring()
の使い方は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]
とかで要素を取得してもできまする。
- 投稿日:2020-03-23T14:00:55+09:00
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の使い方を丁寧に説明した記事になります。
- 投稿日:2020-03-23T13:49:16+09:00
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リクエストの場合に呼ばれる endHTMLを返す場合は、該当するビューを呼びその中データを生成していましたが、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.jbuilderjson.content @message.content => { content: "@messageのcontent" }jbuilderは左がキー・右がバリューのようなハッシュの形になっています。
上記の例だと、json.contentがkeyで、@message.contentがvalueとなります。
- 投稿日:2020-03-23T13:36:05+09:00
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 workshopSDKのインストール
workshopディレクトリにて
[iOS]cordova platforms add ios[Android]
https://developer.android.com/studio からインストール後cordova platforms add androidworkshopディレクトリにて、基本的なプラグインを追加
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に保存される。
- 投稿日:2020-03-23T11:45:26+09:00
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
- 投稿日:2020-03-23T11:44:27+09:00
Kinx 実現技術 - Switch-Case
Switch-Case
はじめに
「見た目は JavaScript、頭脳(中身)は Ruby、(安定感は AC/DC)」 でお届けしているスクリプト言語 Kinx。作ったものの紹介だけではなく実現のために使った技術を紹介していくのも貢献。その道の人には当たり前でも、そうでない人にも興味をもって貰えるかもしれない。
前回のテーマは構文解析。今回のテーマは Switch-Case。
- 参考
- 最初の動機 ... スクリプト言語 KINX(ご紹介)
- リポジトリ ... https://github.com/Kray-G/kinx
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 はジャンプテーブル化しないと使い物にならないので頑張った。おわりに
ここまで読んでいただいてありがとうございます。最後はいつもの以下の定型フォーマットです。
- 最初の動機は スクリプト言語 KINX(ご紹介) を参照してください(もし宜しければ「
いいねLGTM」ボタンをポチっと)。- リポジトリは ここ(https://github.com/Kray-G/kinx) です。こちらももし宜しければ★をポチっと。
- 投稿日:2020-03-23T11:15:15+09:00
フロントエンドで始める「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/axiosapis ディレクトリを作成し、なにか一つ 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.tsimport { 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/axios
をaspida
が用意している 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
- 投稿日:2020-03-23T11:09:25+09:00
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
from
はObservableを生成するファクトリーメソッド(Creation Operator)。1,2,3のvalueが順に発生して流れている。pipe
メソッドにより、Operatorを追加している。- ここでは、
map
により、渡ってきた値をイテレイティブに処理して返している。返り値は再びObservable。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
- Subjectを作成
- subscribeを2つ登録。それぞれ異なる処理が書いてある
- 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の値が呼び出される例。
- Creation Operator である
interval()
でObservableを作る
なお、interval() オペレータも、内部でAsyncSchedularを使っている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
- 投稿日:2020-03-23T10:36:22+09:00
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
をアクセスする処理を削除すると、余計なアニメーションがなくなり、真白画面もなくなる。対応
Reflowを発生させる上記処理をsetTimeoutで遅延することで、余計なアニメーションがなくなる。
参考記事
- 投稿日:2020-03-23T10:11:37+09:00
async/await スリープ関数
// スリープ const sleep = delay => new Promise(resolve => setTimeout(resolve, delay)); (async() => { // 5秒待機 await sleep(5000); })();
- 投稿日:2020-03-23T08:25:15+09:00
[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加算演算子は文字の連結と数値の加算を担うことがわかりました。
次に処理の流れをみていきましょう。
- Let lref be the result of evaluating AdditiveExpression.
- Let lval be ? GetValue(lref).
- Let rref be the result of evaluating MultiplicativeExpression.
- Let rval be ? GetValue(rref).
- Let lprim be ? ToPrimitive(lval).
- Let rprim be ? ToPrimitive(rval).
- 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.- Let lnum be ? ToNumeric(lprim).
- Let rnum be ? ToNumeric(rprim).
- If Type(lnum) is different from Type(rnum), throw a 1. TypeError exception.
- Let T be Type(lnum).
- 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
になるのかが確信を持って言えるようになりました!これからはこんな挙動を質問されてもキリッと答えられますね!
"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 なのなんでだろ 」
みたいな質問が流れてきたのがきっかけでした。ちゃんと根拠を持って教えてあげようと思い、初めて仕様書リーディングに挑戦してみました。
こちらが、仕様書を読む勇気をもらった素晴らしい記事です。(唐突な宣伝。)
JavaScriptの「継承」はどう定義されるのか仕様書を読んで理解する - Qiitaいつもだったら、「キモい動きするなー、雑学として覚えておこう。」くらいで終わっていたと思います。やっぱり仕様は大事ですね。
参考
- 投稿日:2020-03-23T08:14:48+09:00
雑談に応答する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を作るのか
- 暇なときにたわいもない雑談ができる
- 愚痴や悩みを聞いてくれる
- 手軽にセリフのカスタマイズができる
- 飽きてきたら口調を変えられる
作り方
利用する技術
Google Apps Script (GAS)
GASでユーザからLINE Botへの発話を受け取り、Botの応答を送信します。Chaplus - 雑談対話API
Chaplus APIというユーザの発話に対しての応答を取得できるAPIを利用します。
様々な雑談が可能なことに加え、ボットのセリフのカスタマイズや口調の変更等ができます。
Chaplus APIは下記の記事で詳しく紹介をしています。
https://qiita.com/maKunugi/items/b1afb6441571119729a7LINE Messaging API
LINEが提供するサービスとLINEユーザーの双方向コミュニケーションを可能にするAPIです。
Messaging APIMessaging 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に設定すれば作業は完了です!LINEで話しかけてみましょう!
まとめ
GAS + Chaplus API を利用して雑談LINE Botを作成する方法について紹介しました。LINE Botの開発に興味のある方の参考になれば幸いです!
- 投稿日:2020-03-23T01:06:30+09:00
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.pyimport 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=usdStep1:サーバー側でチェックアウト・セッションを実装する
セッションの作成はサーバー側で行う。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.pyclass 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.pyfrom . 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.pyfrom 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.pyfrom . import views from django.urls import path,include urlpatterns = [ ..., path('config/',views.stripe_config), ..., ]
python manage.py runserver
でとりあえずサイトを起動してhttp://localhost:8000/
にアクセスすると以下の感じの画面が出るはずだ。ただ、これだと
PAYMENT
を押しても、何も起きないはず。なぜなら、ボタン押されたあとの実装を行っていないから。具体的には自作javascriptのcreateCheckoutSession
関数はとあるURLにPOSTしているが、Django側で設定していないので、設定を行う必要がある。それにはurls.py
にcreate-checkout-session
を追加すればいい。stripe_test/checkout/urls.pyurlpatterns = [ ..., path('create-checkout-session/',views.onetime_payment_checkout , name='create-checkout'), ..., ]以上まで実装を行えば、Stripe側にURLが飛んで決済情報を入力する画面が表示されるはずです(参考:Stripe Checkout)。後は、決済情報を入力し終わったリダイレクト先のページ(
success.html
とcanceled.html
)を実装していけば、一通り決済はできるようになっている。テスト
テスト用の決済カードは複数用意されているので、適宜選ぶ。特にこだわりがないのならば
4242424242424242
を使用。もし、うまく実装できていればダッシュボード上に支払いが反映されているので、確認してみてください。
参考:Github
上記の実装はGithubに公開しているので、参考にしてください。
- Github上のサンプル参考サイト
- 投稿日:2020-03-23T01:06:12+09:00
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! Japan と Google へリクエスト送信を並列処理で行います。
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文へ書き直してみてください。
- 投稿日:2020-03-23T00:57:43+09:00
コピペで始めるNext.js + Firebase Hosting 【静的サイト】
概要
Next.jsで静的サイトをホスティングする手順
※この記事はさくっとデプロイまでの手順を記述しているだけなので、解説とかはありません。
準備
firebase-toolsをインストールしておく
・Firebase CLI リファレンスNext.jsプロジェクトの作成
$ npm init <プロジェクト名> $ cd <プロジェクト名> $ npm install --save next react react-dom
package.json
にscripts
を追加する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/nodeindex.jsとabout.jsを作成
src/pages/index.jsconst Home = () => { return <div>Home</div>; }; export default Home;src/pages/about.jsconst 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.json
のscripts
に以下を追加するpackage.json"scripts": { "deploy": "firebase deploy" }デプロイ
ビルドしてデプロイ
$ npm run export $ npm run deploy
- 投稿日:2020-03-23T00:26:34+09:00
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.jsconst 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.jsyen = -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.jsconst 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.jsconst 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.jsconst 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