- 投稿日:2019-08-16T23:48:40+09:00
Vue cli� 環境構築
Laravelでnpm installするのではなく、単独でSPAを作るときに使うときに便利そうです。
// これを入れる npm install -g @vue/cli npm install -g @vue/cli-service-global // 設定を聞かれるがデフォルトでOK // projectを作成する vue create test_app // serveで起動できる cd test_app/ npm run serve // コンパイルされてdistというディレクトリが作成されて、そこにコンパイルされたファイルが格納される npm run build // guiはこちら vue ui
- 投稿日:2019-08-16T22:56:28+09:00
v-listの要素をCRUDするサンプルコード
Vuetifyの v-list の要素を追加・読取・更新・削除するサンプルコードを書いてみました。
公式サンプルだけだと1か所に情報がまとまっていなかったので。Vuetifyのバージョンは1.5系です。
2.0系で使う場合は、v-list-tile を v-list-item に変えれば動くかと思います。See the Pen v-list example by shozzy (@shozzy) on CodePen.
内容
- 追加(Create):タイトルバー右端の+を押すと、リストの要素が1つ増えます。1
- 読取(Read) :各要素のボタン以外を押すと、その内容がalertで表示されます。2
- 更新(Update):各要素の+を押すと、内容の末尾に+が付け足されていきます。3
- 削除(Delete):各要素のごみ箱アイコンを押すと、その要素が削除されます。
あわせて、インデックスつきのv-forや、各要素をv-dividerで区切る方法のサンプルも入っています。
コード
ここにもコードを貼っておきます。
(CodePen用のHTMLとJavaScriptです。Vue.jsの単一ファイルコンポーネントにはなっていません。)HTML<div id="app"> <v-app id="inspire"> <div class="text-xs-center"> <v-layout row> <v-flex xs12 sm6 offset-sm3> <v-card> <v-toolbar color="light-blue" dark> <v-toolbar-title> sample </v-toolbar-title> <v-spacer></v-spacer> <v-btn icon> <v-icon @click="createItem()">playlist_add</v-icon> </v-btn> </v-toolbar> <v-list> <template v-for="(item, index) in items"> <v-list-tile :key="index" @click="showAlert(readItem(index))" > <v-list-tile-content> <v-list-tile-title> {{ item.title }} </v-list-tile-title> </v-list-tile-content> <v-list-tile-action> <v-btn icon> <v-icon @click.stop="updateItem(index)"> add </v-icon> </v-btn> </v-list-tile-action> <v-list-tile-action> <v-btn icon> <v-icon @click.stop="deleteItem(index)"> delete </v-icon> </v-btn> </v-list-tile-action> </v-list-tile> <v-divider v-if="index + 1 < items.length" :key="`divider-${index}`" > </v-divider> </template> </v-list> </v-card> </v-flex> </v-layout> </div> </v-app> </div>JavaScriptnew Vue({ el: '#app', data() { return { items: [ {title: "title1", value: "value1", detail: "detail1"}, {title: "title2", value: "value2", detail: "detail2"}, {title: "title3", value: "value3", detail: "detail3"} ] } }, methods: { createItem(){ title = "title" + (this.items.length+1) value = "value" + (this.items.length+1) detail = "detail" + (this.items.length+1) this.items.push({title: title, value: value, detail: detail}) }, readItem(index){ return JSON.stringify(this.items[index]) }, updateItem(index){ item = this.items[index] item.title = item.title + "+" item.value = item.value + "+" item.detail = item.detail + "+" }, deleteItem(index){ this.items.splice(index, 1) }, showAlert(message){ alert(message) } } })
- 投稿日:2019-08-16T22:13:45+09:00
タイムアウトした時にちゃんとロックを解除してくれる async-lockを作る
Promiseの排他制御をしてくれるライブラリにasync-lockあります。
で、このライブラリには、ロックしている
Promiseの実行が一定時間以上立った場合にErrorを投げてくれるTimeoutというオプションがあるのですが、こいつはTimeOutになってもエラーを投げるだけでロックを解除してはくれません。なので、一定時間経過後に問答無用でロックを解除したい場合は、自分でロックを解除するコードを書かないといけないです。
まあ、処理が長過ぎるときのエラーハンドリングは、内部で実行されている
Promise関数が頑張れと言うことなのかもしれませんが、、それってTimeoutの意味なくない?ってなったので自作しました。(といっても本家のライブラリをラップしただけですが・・・)TypeScriptで書いちゃってますが、型定義部分削ればJavaScriptでも使えると思います。
コード
実行には
async-lockが必要なので事前にインストールしてください$ npm install async-lock $ npm install --save-dev @types/async-lock作成したラッパークラスは以下です
import AsyncLock from 'async-lock' export class PromiseLock { asyncLock: AsyncLock constructor(option?: AsyncLockOptions) { this.asyncLock = new AsyncLock(option) } acquire(func: Function, options: AquireOptions = {}) { let { resolveTimeout, key } = options if (!key) { key = '__DEFAULT_KEY__' } return this.asyncLock.acquire(key, () => { return new Promise(async resolve => { let isResolved = false if (resolveTimeout) { setTimeout(() => { if (!isResolved) { console.error(`${resolveTimeout} ms passed. over resolveTimeout. unlock promiseLock`) resolve() } }, resolveTimeout) } await func() resolve() isResolved = true }) }) } isBusy(key = '__DEFAULT_KEY__') { return this.asyncLock.isBusy(key) } } interface AsyncLockOptions { timeout?: number; maxPending?: number; domainReentrant?: boolean; Promise?: any; } interface AquireOptions { resolveTimeout?: number key?: string | string[] }使い方
使い方は以下のような感じです。だいたい本家と一緒ですが、不要なら
keyとか指定しなくても良くなってます。const promiseLock = new PromiseLock() const asyncHandler = () => { // promiseLock.acquireの第一引数に指定した関数の実行が終わるまでロックする promiseLock.acquire(async () => { console.log('start') await Promise処理() console.log('finish') }) }第2引数に
resolveTimeoutを指定することで、指定した時間経過後に強制的にロックを解除することができます。
本家と違ってErrorもスローしません。(console.error()は吐きますが・・・)promiseLock.acquire(async () => { console.log('start') await Promise処理() console.log('finish') }, { resolveTimeout: 2000 }) // 2秒以上処理が終わらない場合は強制的にロックを解除する一応キーも指定することができます。
promiseLock.acquire(async () => { console.log('start') await Promise処理() console.log('finish') },{ key: 'original_key' })
isBusy()も使えます。これは、acquireの中の関数が実行中であればtrueを返す関数です。特にresolveTimeoutと組み合わせると、「処理が終わるまでは次の呼び出しを実行しないようにし、処理に時間がかかりすぎる場合はロックを解除する」みたいな動きができるので、ゲームのスクロール処理とか、「時間経過で処理を行いたいけど、遅延して実行するぐらいなら処理落ちしたほうが良い」みたいな時に結構使えます。const handler = () => { // acquireの処理中は次の処理を受け付けない if (promiseLock.isBusy()) { console.log('handler is busy. skip exec function') return } promiseLock.acquire(async () => { console.log('start') await Promise処理() console.log('finish') }, { resolveTimeout: 2000 }) // 2秒以上処理が遅延する場合には、強制的にロックを解除し、次の処理を実行する }本家のoptionを使いたい場合は、newする時に指定することもできます。(ただここで指定できる
timeoutは本家のエラーを吐くだけのtimeoutなので気をつけてください)const promiseLock = new PromiseLock({ maxPenging: 100 })以上
- 投稿日:2019-08-16T21:51:22+09:00
FIDOサーバを立ててみた
Googleから最近になって、FIDO認証デバイスの「Titanセキュリティーキー」が発売されたので、購入しました。
[Google Titanセキュリティーキー]
https://store.google.com/jp/product/titan_security_key_kit今のところ、はやっていそうですし、せっかくなのでこれを使って遊んでいきたいと思います。
Titanセキュリティーキーがあれば、Webサーバから見てユーザID/パスワードで認証するのではなく、Titanセキュリティーキーのデバイスを認証してくれます。公開鍵をベースとしているそうです。
また、Titanセキュリティーキーには、USBに加えて、NFCとBLEの接続方法があるので、直接PCに挿さなくとも、デバイス認証してくれます。ということで、今回は、王道のFIDOサーバを立ち上げます。
FIDOの全体像を知る
以下のサイトの記事が非常に参考になりました。
https://techblog.yahoo.co.jp/advent-calendar-2018/webauthn/それから、以下のチュートリアルも非常に参考になりました。
https://github.com/fido-alliance/webauthn-demo頭が混乱してきたら、以下のページで再整理しましょう。
Web Authentication API の裏側と、なぜそうなっているのかを図解したサーバ側に必要なエンドポイントを知る
サーバ側に必要なエンドポイントは以下の4つです。
- POST /attestation/options
- POST /attestation/result
- POST /assertion/options
- POST /assertion/result
(参考)
https://fidoalliance.org/specs/fido-v2.0-rd-20180702/fido-server-v2.0-rd-20180702.html詳細はよくわからないのですが、以下のnpmモジュールを使わせてもらうことで、実装量をかなり省けました。(ありがとうございます!)
fido2-lib
https://github.com/apowers313/fido2-libエンドポイントに対応する関数が定義されていますので、それを呼び出せるように、エンドポイントへの入力パラメータを整理して呼び出します。
対応関係は以下の通りです。
- /attestation/options → f2l.attestationOptions()
- /attestation/result → f2l.attestationResult()
- /assertion/options → f2l.assertionOptions()
- /assertion/result → f2l.assertionResult()
ここで一つややこしいことがありまして、クライアントサーバ間のバイナリデータの受け渡しは、BASE64URLというエンコードするのが通例のようでして、エンドポイントからの入力と関数呼び出しの間、関数戻りからエンドポイントからの出力の間のそれぞれで、エンコード/デコードする必要があります。
さらにややこしいのが、一部fido2-libがエンコード/デコードを処理してなさそうに見えて、一部処理していたりで、ちょっと混乱しました。
後で示しますが、(頑張って理解しようとしたのですが)不具合と思われる個所を修正して、やっと動かすことができました。BASE64URLとは、Base64エンコードに対して、Webで扱いやすいように、「+」を「-」に、「/」を「_」に置き換えたもののようです。
以下のnpmモジュールを使わせていただきました。base64url
https://github.com/brianloveswords/base64urlユーザIDとデバイスの紐づけ
サーバ側から見てデバイスを認証できますが、そのデバイスの持ち主がどのユーザIDの人なのかはわからないので、紐づけが必要となります。
ですので、通常は、ユーザID/パスワードで認証したのち、デバイスで生成した公開鍵をユーザIDに紐づけます。こうすることで、デバイスを認証したこと=ユーザIDのユーザがログインした、ということになります。
一般に、/attestation/options のところで本人認証をしてから、デバイスの登録処理を進めるのですが、今回のデモサーバでは、ユーザID/パスワードの認証は省略しています。Client側の呼び出し関数
Client側は、ブラウザを使いました。
かなり実装が進んでいるようで、やはりChromeは対応していました。以下の2つの関数を呼び出します。
・navigator.credentials.create()
・navigator.credentials.get()まとめると、以下の順番になります。
[デバイスの登録]
・POST /attestation/options
・navigator.credentials.create()
・POST /attestation/result[デバイスでログイン]
・POST /assertion/options
・navigator.credentials.get()
・POST /assertion/resultちょっと補足します。
・POST /attestation/options
デバイスに紐づけたいユーザ名を知らせます。本来であれば、パスワードや認証後のセッションを渡して、ユーザ本人であることを示すのが正しいですが、省略しています。
レスポンスとして、チャレンジが返ってきます。・navigator.credentials.create()
デバイス内部で、公開鍵ペアを生成し、チャレンジに対して署名を付与します。
レスポンスとして、公開鍵値、署名値、公開鍵を識別するIDが返ってきます。・POST /attestation/result
デバイスから取得したレスポンスをサーバに登録します。
サーバでは、署名検証し、公開鍵・公開鍵を識別するID(credId)などを、ユーザ名に紐づけて保持することで登録完了です。・POST /assertion/options
ユーザ名を入力とし、サーバに保持しているユーザ名を検索します。
そして、ユーザ名に紐づけたcredIdを取り出し、またチャレンジを生成して、クライアントに返します。複数のデバイスが紐づけられている可能性もあるので、credIdは複数の場合があります。・navigator.credentials.get()
デバイスに署名生成を要求します。生成に使う公開鍵は、サーバから取得したcredIdで指定します。
レスポンスとして署名に使った公開鍵のIDと署名値が返ってきます。・POST /assertion/result
署名に使った公開鍵のIDでcredIdを特定し、署名を検証します。上記以外にも、細かいチェック処理が入っています。
ソースコード
ソースコード量が多いので、GitHubに上げておきました。
https://github.com/poruruba/fido_serverまずはサーバ部分です。
index.js'use strict'; const HELPER_BASE = process.env.HELPER_BASE || '../../helpers/'; const Response = require(HELPER_BASE + 'response'); const Redirect = require(HELPER_BASE + 'redirect'); const base64url = require('base64url'); const crypto = require('crypto'); const { Fido2Lib } = require("fido2-lib"); const FIDO_RP_NAME = process.env.FIDO_RP_NAME || "Sample FIDO Host"; const FIDO_ORIGIN = process.env.FIDO_ORIGIN || "https://localhost"; var f2l = new Fido2Lib({ rpName: FIDO_RP_NAME }); let database = {}; exports.handler = async (event, context, callback) => { if( event.path == '/assertion/options'){ var body = JSON.parse(event.body); console.log(body); let username = body.username; if(database[username] && !database[username].registered) { return new Response({ 'status': 'failed', 'message': `Username ${username} does not exist` }); } var authnOptions = await f2l.assertionOptions(); authnOptions.challenge = base64url.encode(authnOptions.challenge); let allowCredentials = []; for(let authr of database[username].attestation) { allowCredentials.push({ type: 'public-key', id: authr.credId, transports: ['usb', 'nfc', 'ble'] }) } authnOptions.allowCredentials = allowCredentials; console.log(authnOptions); context.req.session.challenge = authnOptions.challenge; context.req.session.username = username; authnOptions.status = 'ok'; return new Response(authnOptions); }else if( event.path == '/assertion/result'){ var body = JSON.parse(event.body); console.log(body); var attestation = null; for( var i = 0 ; i < database[context.req.session.username].attestation.length ; i++ ){ if( database[context.req.session.username].attestation[i].credId == body.id ){ attestation = database[context.req.session.username].attestation[i]; break; } } if( !attestation ){ return new Response({ 'status': 'failed', 'message': 'key is not found.' }); } var assertionExpectations = { challenge: context.req.session.challenge, origin: FIDO_ORIGIN, factor: "either", publicKey: attestation.publickey, prevCounter: attestation.counter, userHandle: null }; body.rawId = new Uint8Array(base64url.toBuffer(body.rawId)).buffer; var authnResult = await f2l.assertionResult(body, assertionExpectations); console.log(authnResult); if(authnResult.audit.complete) { attestation.counter = authnResult.authnrData.get('counter'); return new Response({ 'status': 'ok', credId: body.id, counter: attestation.counter }); } else { return new Response({ 'status': 'failed', 'message': 'Can not authenticate signature!' }); } }else if( event.path == '/attestation/options'){ var body = JSON.parse(event.body); console.log(body); let username = body.username; if(database[username] && database[username].registered) { return new Response({ 'status': 'failed', 'message': `Username ${username} already exists` }); } var id = randomBase64URLBuffer(); var registrationOptions = await f2l.attestationOptions(); registrationOptions.challenge = base64url.encode(registrationOptions.challenge); registrationOptions.user.id = id; registrationOptions.user.name = username; registrationOptions.user.displayName = username; console.log(registrationOptions); database[username] = { 'name': username, 'registered': false, 'id': id, 'attestation': [] }; context.req.session.challenge = registrationOptions.challenge; context.req.session.username = username; registrationOptions.status = 'ok'; return new Response(registrationOptions); }else if( event.path == '/attestation/result'){ var body = JSON.parse(event.body); console.log(body); var attestationExpectations = { challenge: context.req.session.challenge, origin: FIDO_ORIGIN, factor: "either" }; body.rawId = new Uint8Array(base64url.toBuffer(body.rawId)).buffer; var regResult = await f2l.attestationResult(body, attestationExpectations); console.log(regResult); var credId = base64url.encode(regResult.authnrData.get('credId')); var counter = regResult.authnrData.get('counter'); database[context.req.session.username].attestation.push({ publickey : regResult.authnrData.get('credentialPublicKeyPem'), counter : counter, fmt: regResult.authnrData.get('fmt'), credId : credId }); if(regResult.audit.complete) { database[context.req.session.username].registered = true return new Response({ 'status': 'ok', credId: credId, counter: counter }); } else { return new Response({ 'status': 'failed', 'message': 'Can not authenticate signature!' }); } } }; function randomBase64URLBuffer(len){ len = len || 32; let buff = crypto.randomBytes(len); return base64url(buff); }以下の部分は、環境に合わせて、変更してください。
・process.env.FIDO_RP_NAME
・process.env.FIDO_ORIGIN次は、クライアント側ソースです。
HTMLは以下の通りです。index.html<!DOCTYPE html> <html lang="ja"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-Security-Policy" content="default-src * data: gap: https://ssl.gstatic.com 'unsafe-eval' 'unsafe-inline'; style-src * 'unsafe-inline'; media-src *; img-src * data: content: blob:;"> <meta name="format-detection" content="telephone=no"> <meta name="msapplication-tap-highlight" content="no"> <meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width"> <!-- jQuery (necessary for Bootstrap's JavaScript plugins) --> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script> <!-- Latest compiled and minified CSS --> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> <!-- Optional theme --> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous"> <!-- Latest compiled and minified JavaScript --> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script> <title>FIDO Demo Server</title> <script src="js/methods_utils.js"></script> <script src="js/vue_utils.js"></script> <script src="dist/js/vconsole.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/js-cookie@2/src/js.cookie.min.js"></script> <script src="https://unpkg.com/vue"></script> </head> <body> <div id="top" class="container"> <h1>FIDO Demo Server</h1> <div class="alert alert-info" role="alert">{{message}}</div> <div class="form-inline"> <label>username</label> <input type="text" class="form-control" v-model="username"> </div> <button class="btn btn-default" v-on:click="start_register()">登録開始</button> <div v-if="attestation != null"> <label>rp.name</label> {{attestation.rp.name}}<br> <label>user.displayName</label> {{attestation.user.displayName}}<br> <label>user.id</label> {{attestation_encode.user.id}}<br> <label>challenge</label> {{attestation_encode.challenge}}<br> <label>attestation</label> {{attestation.attestation}}<br> <button class="btn btn-default" v-on:click="do_register()">登録実行</button> </div> <div> <div v-if="registered"> <label>credId</label> {{register_credId}}<br> <label>counter</label> {{register_counter}}<br> </div> <button class="btn btn-default" v-on:click="start_login()">ログイン開始</button> </div> <div v-if="assertion != null"> <div v-for="(cred, index) of assertion_encode.allowCredentials"> <label>cred.id[{{index}}]</label> {{cred.id}}<br> </div> <label>challenge</label> {{assertion_encode.challenge}}<br> <button class="btn btn-default" v-on:click="do_login()">ログイン実行</button> </div> <div v-if="logined"> <label>credId</label> {{login_credId}}<br> <label>counter</label> {{login_counter}}<br> </div> <div class="modal fade" id="progress"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <h4 class="modal-title">{{progress_title}}</h4> </div> <div class="modal-body"> <center><progress max="100" /></center> </div> </div> </div> </div> </div> <script src="dist/js/base64url-arraybuffer.js"></script> <script src="js/start.js"></script> </body>Javascript部分です。
start.js'use strict'; //var vConsole = new VConsole(); const base_url = "【サーバのURL】"; var vue_options = { el: "#top", data: { progress_title: '', message: "", username: 'test', attestation : null, attestation_encode: { user: {} }, register_credId: null, register_counter: -1, registered : false, assertion : null, assertion_encode: { allowCredentials: [] }, login_credId: null, login_counter: -1, logined : false }, computed: { }, methods: { start_register: function(){ this.registered = false; var param = { username: this.username, userVerification: true }; this.progress_open(); do_post( base_url + '/attestation/options', param ) .then(json =>{ this.progress_close(); console.log(json); if( json.status != 'ok'){ alert(json.message); return; } this.attestation_encode.challenge = json.challenge; this.attestation_encode.user.id = json.user.id; json.challenge = base64url.decode(json.challenge); json.user.id = base64url.decode(json.user.id); this.attestation = json; this.message = '登録の準備ができました。'; }) .catch(error =>{ this.progress_close(); alert(error); }); }, do_register: function(){ return navigator.credentials.create({ publicKey: this.attestation }) .then(response =>{ var result = publicKeyCredentialToJSON(response); this.progress_open(); return do_post(base_url + '/attestation/result', result ) }) .then((response) => { this.progress_close(); if(response.status !== 'ok'){ alert(json.message); return; } if( response.status == 'ok'){ this.register_credId = response.credId; this.register_counter = response.counter; this.registered = true; this.message = '登録が完了しました。'; }else{ throw 'registration error'; } }) .catch(error =>{ this.progress_close(); alert(error); }); }, start_login: function(){ this.logined = false; var param = { username: this.username, }; this.progress_open(); do_post( base_url + '/assertion/options', param ) .then(json =>{ this.progress_close(); console.log(json); if( json.status != 'ok'){ alert(json.message); return; } this.assertion_encode.challenge = json.challenge; json.challenge = base64url.decode(json.challenge); for(var i = 0 ; i < json.allowCredentials.length ; i++ ) { this.assertion_encode.allowCredentials[i] = { id: json.allowCredentials[i].id }; json.allowCredentials[i].id = base64url.decode(json.allowCredentials[i].id); } this.assertion = json; this.message = 'ログインの準備ができました。'; }) .catch(error =>{ this.progress_close(); alert(error); }); }, do_login: function(){ console.log(this.assertion); return navigator.credentials.get({ publicKey: this.assertion }) .then(response =>{ var result = publicKeyCredentialToJSON(response); this.progress_open(); return do_post(base_url + '/assertion/result', result ) }) .then((response) => { this.progress_close(); if(response.status !== 'ok') throw new Error(`Server responed with error. The message is: ${response.message}`); console.log('sendWebAuthnResponse received: ', response); if( response.status == 'ok'){ this.login_credId = response.credId; this.login_counter = response.counter; this.logined = true; this.message = 'ログインが成功しました。'; }else{ throw 'login error'; } }) .catch(error =>{ this.progress_close(); alert(error); }); } }, created: function(){ }, mounted: function(){ proc_load(); } }; vue_add_methods(vue_options, methods_utils); var vue = new Vue( vue_options ); function do_post(url, body){ // const headers = new Headers( { "Content-Type" : "application/json; charset=utf-8" } ); const headers = new Headers( { "Content-Type" : "application/json" } ); return fetch(url, { method : 'POST', credentials: 'include', body : JSON.stringify(body), headers: headers }) .then((response) => { if( !response.ok ) throw 'status is not 200.'; return response.json(); }); } function publicKeyCredentialToJSON(pubKeyCred){ if(pubKeyCred instanceof Array) { let arr = []; for(let i of pubKeyCred) arr.push(publicKeyCredentialToJSON(i)); return arr } if(pubKeyCred instanceof ArrayBuffer) { return base64url.encode(pubKeyCred) } if(pubKeyCred instanceof Object) { let obj = {}; for (let key in pubKeyCred) { obj[key] = publicKeyCredentialToJSON(pubKeyCred[key]); } return obj } return pubKeyCred }以下の部分は、環境に合わせて書き換えてください。
・【サーバのURL】fido2-lib の修正
下記に示すように、fido2-libの実装をいじらないとうまく動きませんでした。
lib/validator.js// 172行目あたり // 変更前 if (typeof req.response.userHandle !== "string" && !(req.response.userHandle instanceof ArrayBuffer) && req.response.userHandle != null && req.response.userHandle !== undefined) { throw new TypeError("expected 'response.userHandle' to be base64 String, ArrayBuffer, or undefined"); } // 変更後 if (typeof req.response.userHandle !== "string" && !(req.response.userHandle instanceof ArrayBuffer) && // modified by poruruba req.response.userHandle != null && req.response.userHandle !== undefined) { throw new TypeError("expected 'response.userHandle' to be base64 String, ArrayBuffer, or undefined"); }lib/utils.js// 183行目あたり // 変更前 function bufEqual(a, b) { var len = a.length; if (len !== b.length) { return false; } for (var i = 0; i < len; i++) { if (a.readUInt8(i) !== b.readUInt8(i)) { return false; } } return true; } // 変更後 function bufEqual(a, b) { // modified by poruruba if (!(a instanceof ArrayBuffer && b instanceof ArrayBuffer)) { throw new Error("expected bufEqual to be of type ArrayBuffer"); } var len = a.byteLength; if (len !== b.byteLength) { return false; } for (var i = 0; i < len; i++) { if (a[i] !== b[i]) { return false; } } return true; }lib/parser.js// 249行目あたり // 変更前 let userHandle; if (msg.response.userHandle !== undefined) { userHandle = coerceToArrayBuffer(msg.response.userHandle, "response.userHandle"); if (userHandle.byteLength === 0) { userHandle = undefined; } } let sigAb = coerceToArrayBuffer(msg.response.signature, "response.signature"); let ret = new Map([ ["sig", sigAb], ["userHandle", userHandle], ["rawAuthnrData", msg.response.authenticatorData], ...parseAuthenticatorData(msg.response.authenticatorData) ]); // 変更後 let userHandle; // modified by poruruba // if (msg.response.userHandle !== undefined) { if (msg.response.userHandle !== undefined && msg.response.userHandle !== null) { userHandle = coerceToArrayBuffer(msg.response.userHandle, "response.userHandle"); if (userHandle.byteLength === 0) { userHandle = undefined; } } let sigAb = coerceToArrayBuffer(msg.response.signature, "response.signature"); let ret = new Map([ ["sig", sigAb], ["userHandle", userHandle], // modified by poruruba // ["rawAuthnrData", msg.response.authenticatorData], ["rawAuthnrData", coerceToArrayBuffer(msg.response.authenticatorData, "authnrDataArrayBuffer")], ...parseAuthenticatorData(msg.response.authenticatorData) ]);動作確認
Windows10のChromeブラウザで実施してみました。
以下は、TitanセキュリティキーをUSB端子に挿した場合です。index.htmlを開きます。
usernameに適当なユーザ名を入力して、「登録開始」ボタンを押下します。
以下が表示されます。
Titanセキュリティキーも表面の金属のところを指で触ります。以下が表示されますので、「許可」ボタンを押下して進めます。
これで登録完了です。
さあログインしましょう。
「ログイン開始」ボタンを押下すると、以下のようになります。そして、「ログイン実行」ボタンを押下します。
登録時と同様に、Titanセキュリティキーも表面の金属のところを指で触ります。以下のように表示されて、ログイン完了です。
Androidでもやってみます。
こちらはBLEでやってみました。事前に、BLEデバイスとしてペアリングしておく必要があるようです。
ペアリングは、デバイスのボタンを5秒間長押しするとペアリングが開始されます。それでは、始めましょう。AndroidのChromeからindnex.htmlを開きます。
「登録開始」ボタンを押下します。
「登録実行」ボタンを押下します。
デバイスにあるボタンを押します。
登録完了です。
次はログインです。
「ログイン開始」ボタンを押下すると以下の画面になります。「ログイン実行」ボタンを押下します。
デバイスにあるボタンを押します。
ログインできました!
制限事項
・WindowsでBLEを使って認証しようとしましたが、うまくいきませんでした。原因不明です。
以上
- 投稿日:2019-08-16T21:51:22+09:00
WebAuthnを使ったFIDOサーバを立ててみた
Googleから最近になって、FIDO認証デバイスの「Titanセキュリティーキー」が発売されたので、購入しました。
[Google Titanセキュリティーキー]
https://store.google.com/jp/product/titan_security_key_kit今のところ、はやっていそうですし、せっかくなのでこれを使って遊んでいきたいと思います。
Titanセキュリティーキーがあれば、Webサーバから見てユーザID/パスワードで認証するのではなく、Titanセキュリティーキーのデバイスを認証してくれます。公開鍵をベースとしているそうです。
また、Titanセキュリティーキーには、USBに加えて、NFCとBLEの接続方法があるので、直接PCに挿さなくとも、デバイス認証してくれます。ということで、今回は、王道のFIDOサーバを立ち上げます。
FIDOの全体像を知る
以下のサイトの記事が非常に参考になりました。
https://techblog.yahoo.co.jp/advent-calendar-2018/webauthn/それから、以下のチュートリアルも非常に参考になりました。
https://github.com/fido-alliance/webauthn-demoこれがWebAuthn
https://w3c.github.io/webauthn/頭が混乱してきたら、以下のページで再整理しましょう。
Web Authentication API の裏側と、なぜそうなっているのかを図解したサーバ側に必要なエンドポイントを知る
サーバ側に必要なエンドポイントは以下の4つです。
- POST /attestation/options
- POST /attestation/result
- POST /assertion/options
- POST /assertion/result
(参考) ただし、一部未対応。
https://fidoalliance.org/specs/fido-v2.0-rd-20180702/fido-server-v2.0-rd-20180702.html詳細はよくわからないのですが、以下のnpmモジュールを使わせてもらうことで、実装量をかなり省けました。(ありがとうございます!)
fido2-lib
https://github.com/apowers313/fido2-libエンドポイントに対応する関数が定義されていますので、それを呼び出せるように、エンドポイントへの入力パラメータを整理して呼び出します。
対応関係は以下の通りです。
- /attestation/options → f2l.attestationOptions()
- /attestation/result → f2l.attestationResult()
- /assertion/options → f2l.assertionOptions()
- /assertion/result → f2l.assertionResult()
ここで一つややこしいことがありまして、クライアントサーバ間のバイナリデータの受け渡しは、BASE64URLでエンコードするのが通例のようでして、エンドポイントからの入力と関数呼び出しの間、関数戻りからエンドポイントからの出力の間のそれぞれで、エンコード/デコードする必要があります。
さらにややこしいのが、一部fido2-libがエンコード/デコードを処理してなさそうに見えて、一部処理していたりで、ちょっと混乱しました。
後で示しますが、(頑張って理解しようとしたのですが)不具合と思われる個所を修正して、やっと動かすことができました。BASE64URLとは、Base64エンコードに対して、Webで扱いやすいように、「+」を「-」に、「/」を「_」に置き換えたもののようです。
以下のnpmモジュールを使わせていただきました。base64url
https://github.com/brianloveswords/base64urlユーザIDとデバイスの紐づけ
サーバ側から見てデバイスを認証できますが、そのデバイスの持ち主がどのユーザIDの人なのかはわからないので、紐づけが必要となります。
ですので、通常は、ユーザID/パスワードで認証したのち、デバイスで生成した公開鍵をユーザIDに紐づけます。こうすることで、デバイスを認証したこと=ユーザIDのユーザがログインした、ということになります。
一般に、/attestation/options のところで本人認証をしてから、デバイスの登録処理を進めるのですが、今回のデモサーバでは、ユーザID/パスワードの認証は省略しています。クライアント側の呼び出し関数
Client側は、ブラウザを使いました。
かなり実装が進んでいるようで、やはりChromeは対応していました。以下の2つの関数を呼び出します。
・navigator.credentials.create()
・navigator.credentials.get()まとめると、以下の順番になります。
[デバイスの登録]
・POST /attestation/options
・navigator.credentials.create()
・POST /attestation/result[デバイスでログイン]
・POST /assertion/options
・navigator.credentials.get()
・POST /assertion/resultちょっと補足します。
・POST /attestation/options
デバイスに紐づけたいユーザ名を知らせます。本来であれば、パスワードや認証後のセッションを渡して、ユーザ本人であることを示すのが正しいですが、省略しています。
レスポンスとして、チャレンジが返ってきます。・navigator.credentials.create()
デバイス内部で、公開鍵ペアを生成し、チャレンジに対して署名を付与します。
レスポンスとして、公開鍵値、署名値、公開鍵を識別するIDが返ってきます。・POST /attestation/result
デバイスから取得したレスポンスをサーバに登録します。
サーバでは、署名検証し、公開鍵・公開鍵を識別するID(credId)などを、ユーザ名に紐づけて保持することで登録完了です。・POST /assertion/options
ユーザ名を入力とし、サーバに保持しているユーザ名を検索します。
そして、ユーザ名に紐づけたcredIdを取り出し、またチャレンジを生成して、クライアントに返します。複数のデバイスが紐づけられている可能性もあるので、credIdは複数の場合があります。・navigator.credentials.get()
デバイスに署名生成を要求します。生成に使う公開鍵は、サーバから取得したcredIdで指定します。
レスポンスとして署名に使った公開鍵のIDと署名値が返ってきます。・POST /assertion/result
署名に使った公開鍵のIDでcredIdを特定し、署名を検証します。上記以外にも、細かいチェック処理が入っています。
ソースコード
ソースコード量が多いので、GitHubに上げておきました。
https://github.com/poruruba/fido_serverまずはサーバ部分です。
index.js'use strict'; const HELPER_BASE = process.env.HELPER_BASE || '../../helpers/'; const Response = require(HELPER_BASE + 'response'); const Redirect = require(HELPER_BASE + 'redirect'); const base64url = require('base64url'); const crypto = require('crypto'); const { Fido2Lib } = require("fido2-lib"); const FIDO_RP_NAME = process.env.FIDO_RP_NAME || "Sample FIDO Host"; const FIDO_ORIGIN = process.env.FIDO_ORIGIN || "https://localhost"; var f2l = new Fido2Lib({ rpName: FIDO_RP_NAME }); let database = {}; exports.handler = async (event, context, callback) => { if( event.path == '/assertion/options'){ var body = JSON.parse(event.body); console.log(body); let username = body.username; if(database[username] && !database[username].registered) { return new Response({ 'status': 'failed', 'message': `Username ${username} does not exist` }); } var authnOptions = await f2l.assertionOptions(); authnOptions.challenge = base64url.encode(authnOptions.challenge); let allowCredentials = []; for(let authr of database[username].attestation) { allowCredentials.push({ type: 'public-key', id: authr.credId, transports: ['usb', 'nfc', 'ble'] }) } authnOptions.allowCredentials = allowCredentials; console.log(authnOptions); context.req.session.challenge = authnOptions.challenge; context.req.session.username = username; authnOptions.status = 'ok'; return new Response(authnOptions); }else if( event.path == '/assertion/result'){ var body = JSON.parse(event.body); console.log(body); var attestation = null; for( var i = 0 ; i < database[context.req.session.username].attestation.length ; i++ ){ if( database[context.req.session.username].attestation[i].credId == body.id ){ attestation = database[context.req.session.username].attestation[i]; break; } } if( !attestation ){ return new Response({ 'status': 'failed', 'message': 'key is not found.' }); } var assertionExpectations = { challenge: context.req.session.challenge, origin: FIDO_ORIGIN, factor: "either", publicKey: attestation.publickey, prevCounter: attestation.counter, userHandle: null }; body.rawId = new Uint8Array(base64url.toBuffer(body.rawId)).buffer; var authnResult = await f2l.assertionResult(body, assertionExpectations); console.log(authnResult); if(authnResult.audit.complete) { attestation.counter = authnResult.authnrData.get('counter'); return new Response({ 'status': 'ok', credId: body.id, counter: attestation.counter }); } else { return new Response({ 'status': 'failed', 'message': 'Can not authenticate signature!' }); } }else if( event.path == '/attestation/options'){ var body = JSON.parse(event.body); console.log(body); let username = body.username; if(database[username] && database[username].registered) { return new Response({ 'status': 'failed', 'message': `Username ${username} already exists` }); } var id = randomBase64URLBuffer(); var registrationOptions = await f2l.attestationOptions(); registrationOptions.challenge = base64url.encode(registrationOptions.challenge); registrationOptions.user.id = id; registrationOptions.user.name = username; registrationOptions.user.displayName = username; console.log(registrationOptions); database[username] = { 'name': username, 'registered': false, 'id': id, 'attestation': [] }; context.req.session.challenge = registrationOptions.challenge; context.req.session.username = username; registrationOptions.status = 'ok'; return new Response(registrationOptions); }else if( event.path == '/attestation/result'){ var body = JSON.parse(event.body); console.log(body); var attestationExpectations = { challenge: context.req.session.challenge, origin: FIDO_ORIGIN, factor: "either" }; body.rawId = new Uint8Array(base64url.toBuffer(body.rawId)).buffer; var regResult = await f2l.attestationResult(body, attestationExpectations); console.log(regResult); var credId = base64url.encode(regResult.authnrData.get('credId')); var counter = regResult.authnrData.get('counter'); database[context.req.session.username].attestation.push({ publickey : regResult.authnrData.get('credentialPublicKeyPem'), counter : counter, fmt: regResult.authnrData.get('fmt'), credId : credId }); if(regResult.audit.complete) { database[context.req.session.username].registered = true return new Response({ 'status': 'ok', credId: credId, counter: counter }); } else { return new Response({ 'status': 'failed', 'message': 'Can not authenticate signature!' }); } } }; function randomBase64URLBuffer(len){ len = len || 32; let buff = crypto.randomBytes(len); return base64url(buff); }以下の部分は、環境に合わせて、変更してください。
・process.env.FIDO_RP_NAME
・process.env.FIDO_ORIGIN次は、クライアント側ソースです。
Base64URLエンコードには以下のモジュールを使わせていただきました。Base64URL-ArrayBuffer
https://github.com/herrjemand/Base64URL-ArrayBufferHTMLは以下の通りです。
index.html<!DOCTYPE html> <html lang="ja"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-Security-Policy" content="default-src * data: gap: https://ssl.gstatic.com 'unsafe-eval' 'unsafe-inline'; style-src * 'unsafe-inline'; media-src *; img-src * data: content: blob:;"> <meta name="format-detection" content="telephone=no"> <meta name="msapplication-tap-highlight" content="no"> <meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width"> <!-- jQuery (necessary for Bootstrap's JavaScript plugins) --> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script> <!-- Latest compiled and minified CSS --> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> <!-- Optional theme --> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous"> <!-- Latest compiled and minified JavaScript --> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script> <title>FIDO Demo Server</title> <script src="js/methods_utils.js"></script> <script src="js/vue_utils.js"></script> <script src="dist/js/vconsole.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/js-cookie@2/src/js.cookie.min.js"></script> <script src="https://unpkg.com/vue"></script> </head> <body> <div id="top" class="container"> <h1>FIDO Demo Server</h1> <div class="alert alert-info" role="alert">{{message}}</div> <div class="form-inline"> <label>username</label> <input type="text" class="form-control" v-model="username"> </div> <button class="btn btn-default" v-on:click="start_register()">登録開始</button> <div v-if="attestation != null"> <label>rp.name</label> {{attestation.rp.name}}<br> <label>user.displayName</label> {{attestation.user.displayName}}<br> <label>user.id</label> {{attestation_encode.user.id}}<br> <label>challenge</label> {{attestation_encode.challenge}}<br> <label>attestation</label> {{attestation.attestation}}<br> <button class="btn btn-default" v-on:click="do_register()">登録実行</button> </div> <div> <div v-if="registered"> <label>credId</label> {{register_credId}}<br> <label>counter</label> {{register_counter}}<br> </div> <button class="btn btn-default" v-on:click="start_login()">ログイン開始</button> </div> <div v-if="assertion != null"> <div v-for="(cred, index) of assertion_encode.allowCredentials"> <label>cred.id[{{index}}]</label> {{cred.id}}<br> </div> <label>challenge</label> {{assertion_encode.challenge}}<br> <button class="btn btn-default" v-on:click="do_login()">ログイン実行</button> </div> <div v-if="logined"> <label>credId</label> {{login_credId}}<br> <label>counter</label> {{login_counter}}<br> </div> <div class="modal fade" id="progress"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <h4 class="modal-title">{{progress_title}}</h4> </div> <div class="modal-body"> <center><progress max="100" /></center> </div> </div> </div> </div> </div> <script src="dist/js/base64url-arraybuffer.js"></script> <script src="js/start.js"></script> </body>Javascript部分です。
start.js'use strict'; //var vConsole = new VConsole(); const base_url = "【サーバのURL】"; var vue_options = { el: "#top", data: { progress_title: '', message: "", username: 'test', attestation : null, attestation_encode: { user: {} }, register_credId: null, register_counter: -1, registered : false, assertion : null, assertion_encode: { allowCredentials: [] }, login_credId: null, login_counter: -1, logined : false }, computed: { }, methods: { start_register: function(){ this.registered = false; var param = { username: this.username, userVerification: true }; this.progress_open(); do_post( base_url + '/attestation/options', param ) .then(json =>{ this.progress_close(); console.log(json); if( json.status != 'ok'){ alert(json.message); return; } this.attestation_encode.challenge = json.challenge; this.attestation_encode.user.id = json.user.id; json.challenge = base64url.decode(json.challenge); json.user.id = base64url.decode(json.user.id); this.attestation = json; this.message = '登録の準備ができました。'; }) .catch(error =>{ this.progress_close(); alert(error); }); }, do_register: function(){ return navigator.credentials.create({ publicKey: this.attestation }) .then(response =>{ var result = publicKeyCredentialToJSON(response); this.progress_open(); return do_post(base_url + '/attestation/result', result ) }) .then((response) => { this.progress_close(); if(response.status !== 'ok'){ alert(json.message); return; } if( response.status == 'ok'){ this.register_credId = response.credId; this.register_counter = response.counter; this.registered = true; this.message = '登録が完了しました。'; }else{ throw 'registration error'; } }) .catch(error =>{ this.progress_close(); alert(error); }); }, start_login: function(){ this.logined = false; var param = { username: this.username, }; this.progress_open(); do_post( base_url + '/assertion/options', param ) .then(json =>{ this.progress_close(); console.log(json); if( json.status != 'ok'){ alert(json.message); return; } this.assertion_encode.challenge = json.challenge; json.challenge = base64url.decode(json.challenge); for(var i = 0 ; i < json.allowCredentials.length ; i++ ) { this.assertion_encode.allowCredentials[i] = { id: json.allowCredentials[i].id }; json.allowCredentials[i].id = base64url.decode(json.allowCredentials[i].id); } this.assertion = json; this.message = 'ログインの準備ができました。'; }) .catch(error =>{ this.progress_close(); alert(error); }); }, do_login: function(){ return navigator.credentials.get({ publicKey: this.assertion }) .then(response =>{ var result = publicKeyCredentialToJSON(response); this.progress_open(); return do_post(base_url + '/assertion/result', result ) }) .then((response) => { this.progress_close(); if(response.status !== 'ok') throw new Error(`Server responed with error. The message is: ${response.message}`); console.log('sendWebAuthnResponse received: ', response); if( response.status == 'ok'){ this.login_credId = response.credId; this.login_counter = response.counter; this.logined = true; this.message = 'ログインが成功しました。'; }else{ throw 'login error'; } }) .catch(error =>{ this.progress_close(); alert(error); }); } }, created: function(){ }, mounted: function(){ proc_load(); } }; vue_add_methods(vue_options, methods_utils); var vue = new Vue( vue_options ); function do_post(url, body){ // const headers = new Headers( { "Content-Type" : "application/json; charset=utf-8" } ); const headers = new Headers( { "Content-Type" : "application/json" } ); return fetch(url, { method : 'POST', credentials: 'include', body : JSON.stringify(body), headers: headers }) .then((response) => { if( !response.ok ) throw 'status is not 200.'; return response.json(); }); } function publicKeyCredentialToJSON(pubKeyCred){ if(pubKeyCred instanceof Array) { let arr = []; for(let i of pubKeyCred) arr.push(publicKeyCredentialToJSON(i)); return arr } if(pubKeyCred instanceof ArrayBuffer) { return base64url.encode(pubKeyCred) } if(pubKeyCred instanceof Object) { let obj = {}; for (let key in pubKeyCred) { obj[key] = publicKeyCredentialToJSON(pubKeyCred[key]); } return obj } return pubKeyCred }以下の部分は、環境に合わせて書き換えてください。
・【サーバのURL】fido2-lib の修正
下記に示すように、fido2-libの実装をいじらないとうまく動きませんでした。
lib/validator.js// 172行目あたり // 変更前 if (typeof req.response.userHandle !== "string" && !(req.response.userHandle instanceof ArrayBuffer) && req.response.userHandle !== undefined) { throw new TypeError("expected 'response.userHandle' to be base64 String, ArrayBuffer, or undefined"); } // 変更後 if (typeof req.response.userHandle !== "string" && !(req.response.userHandle instanceof ArrayBuffer) && // modified by poruruba req.response.userHandle != null && req.response.userHandle !== undefined) { throw new TypeError("expected 'response.userHandle' to be base64 String, ArrayBuffer, or undefined"); }lib/utils.js// 183行目あたり // 変更前 function bufEqual(a, b) { var len = a.length; if (len !== b.length) { return false; } for (var i = 0; i < len; i++) { if (a.readUInt8(i) !== b.readUInt8(i)) { return false; } } return true; } // 変更後 function bufEqual(a, b) { // modified by poruruba if (!(a instanceof ArrayBuffer && b instanceof ArrayBuffer)) { throw new Error("expected bufEqual to be of type ArrayBuffer"); } var len = a.byteLength; if (len !== b.byteLength) { return false; } for (var i = 0; i < len; i++) { if (a[i] !== b[i]) { return false; } } return true; }lib/parser.js// 249行目あたり // 変更前 let userHandle; if (msg.response.userHandle !== undefined) { userHandle = coerceToArrayBuffer(msg.response.userHandle, "response.userHandle"); if (userHandle.byteLength === 0) { userHandle = undefined; } } let sigAb = coerceToArrayBuffer(msg.response.signature, "response.signature"); let ret = new Map([ ["sig", sigAb], ["userHandle", userHandle], ["rawAuthnrData", msg.response.authenticatorData], ...parseAuthenticatorData(msg.response.authenticatorData) ]); // 変更後 let userHandle; // modified by poruruba // if (msg.response.userHandle !== undefined) { if (msg.response.userHandle !== undefined && msg.response.userHandle !== null) { userHandle = coerceToArrayBuffer(msg.response.userHandle, "response.userHandle"); if (userHandle.byteLength === 0) { userHandle = undefined; } } let sigAb = coerceToArrayBuffer(msg.response.signature, "response.signature"); let ret = new Map([ ["sig", sigAb], ["userHandle", userHandle], // modified by poruruba // ["rawAuthnrData", msg.response.authenticatorData], ["rawAuthnrData", coerceToArrayBuffer(msg.response.authenticatorData, "authnrDataArrayBuffer")], ...parseAuthenticatorData(msg.response.authenticatorData) ]);動作確認
Windows10のChromeブラウザで実施してみました。
以下は、TitanセキュリティキーをUSB端子に挿した場合です。index.htmlを開きます。
usernameに適当なユーザ名を入力して、「登録開始」ボタンを押下します。
以下が表示されます。
Titanセキュリティキーも表面の金属のところを指で触ります。以下が表示されますので、「許可」ボタンを押下して進めます。
これで登録完了です。
さあログインしましょう。
「ログイン開始」ボタンを押下すると、以下のようになります。そして、「ログイン実行」ボタンを押下します。
登録時と同様に、Titanセキュリティキーも表面の金属のところを指で触ります。以下のように表示されて、ログイン完了です。
Androidでもやってみます。
こちらはBLEでやってみました。NFCでも似たような感じでした。事前に、BLEデバイスとしてペアリングしておく必要があるようです。
ペアリングは、デバイスのボタンを5秒間長押しするとペアリングが開始されます。それでは、始めましょう。AndroidのChromeからindnex.htmlを開きます。
「登録開始」ボタンを押下します。
「登録実行」ボタンを押下します。
デバイスにあるボタンを押します。
登録完了です。
次はログインです。
「ログイン開始」ボタンを押下すると以下の画面になります。「ログイン実行」ボタンを押下します。
デバイスにあるボタンを押します。
ログインできました!
制限事項
・WindowsでBLEを使って認証しようとしましたが、うまくいきませんでした。原因不明です。
・https://github.com/apowers313/fido2-lib をみたら、Androidの指紋認証Attestationが実装されているではないかっ!以上
- 投稿日:2019-08-16T19:57:55+09:00
javascriptでワンライナーの再帰関数
- 投稿日:2019-08-16T18:24:05+09:00
クックパッドのレシピの分量を計算するchrome拡張を作りました
はじめに
クックパッドを見ながら、料理を作るのが好きなのですが、ふと記載されている分量を作る人数に応じて変更できたら、便利かなと思い、人数に応じて分量を計算できるchrome拡張を作成しました
TL;DR
作ったchrome拡張はこちら
人数を変更して、「分量表示」ボタンをクリックすると、入力した人数で作る際に必要な分量を計算して、表示します
chrome拡張作成の流れ
- manifest.jsonを作成します
manifest.json{ "name": "クックパッドのレシピの分量を計算するchrome拡張", "version": "1.0.0", "manifest_version": 2, "description": "Change the amount of cookpad recipe chrome extension", "content_scripts": [{ "matches": [ "https://cookpad.com/recipe/*" ], "js": [ "js/main.js" ] }] }
- 次に処理を行うJavaScriptを書く
- 材料の横に人数の記載があれば、分量を計算できるようにしています
js/main.jsconst color = 'color: #ff8c1a;'; let baseServing = 0; let servingsFor = document.getElementsByClassName('servings_for yield'); if (servingsFor.length != 0) { baseServing = getNum(changeHalf(servingsFor[0].textContent)); const label1 = document.createElement('label'); label1.textContent = '→'; servingsFor[0].appendChild(label1); const input1 = document.createElement('input'); input1.type = 'text'; input1.name = 'servings'; input1.maxLength = 2; input1.size = 2; input1.value = baseServing; input1.style = color; servingsFor[0].appendChild(input1); const label2 = document.createElement('label'); label2.textContent = '人分'; label2.style = color; servingsFor[0].appendChild(label2); const btn1 = document.createElement('input'); btn1.type = 'button'; btn1.value = '分量表示'; btn1.className = 'submit button small'; btn1.onclick = function() { let values = document.getElementsByClassName('ingredient_quantity amount'); for(i = 0; i < values.length; i++) { let changeAmount = values[i].parentNode.getElementsByClassName('ingredient_quantity change_amount'); if (changeAmount.length != 0) { values[i].parentNode.removeChild(changeAmount[0]); } let halfValue = changeHalf(values[i].textContent); let baseQuantity = getNum(halfValue); if (baseQuantity != '') { let serving = document.getElementsByName('servings')[0].value; let per = serving / baseServing; let quantity = baseQuantity * per; const label3 = document.createElement('div'); label3.className = 'ingredient_quantity change_amount'; label3.style = color; label3.textContent = halfValue.replace(baseQuantity, quantity); values[i].parentNode.appendChild(label3); } } }; servingsFor[0].appendChild(btn1); } function getNum(data) { return data.replace(/[^0-9]/g, ''); } function changeHalf(str) { return str.replace(/[0-9]/g, function(s) { return String.fromCharCode(s.charCodeAt(0) - 65248); }); }さいごに
以下のような課題はありますが、まあまあ満足してます
- 人数の記載がない場合、分量の計算はできない
- 人数に1〜2といった記載がある場合、計算できない
- 分量で複数の数値(大さじ1〜2)のような記載があるものは計算されない
今回、chrome拡張を初めて作成しましたが、以下のリンクがとても役に立ちました。ありがとうございます。
一応、ソースコードをgithubにおいてます
- 投稿日:2019-08-16T18:01:09+09:00
Codewars の問題(Kata)文を1日に一個以上翻訳します。変更通知送るので(いいねでなくて)ストックをお願いします
記事の趣旨
Codewarsの問題(Kata)はサインアップしてなくても解くことができます。各問題のタイトルの下にあるURLをブラウザでたどっていただければ問題を解くIDEを開けます。
サインアップしたい場合またはIDEの使い方についてはこちら(https://qiita.com/javacommons/items/7c473cda7825ab99e08c)の記事をご覧ください。2019/08/16現在214個の問題(Kata)を解いていますので、解いた順に訳していこうと思います。記事内に並べる順番は解いた逆順・・・つまり最後に解いたものが先頭に来るように並べようと思います。
ちなみに問題(Kata)の級は、8級が一番簡単で1級が一番難しいものとなっています。尚、私は主に javascript で解いていますので、javascript 以外のプログラミング言語では問題が対応してないケースもあると思います。その際は javascript で解いていただくか、問題自体をスキップしていただくようお願いします。
ここから翻訳した問題(Kata)
- とりあえず数問訳しておきます。最終的には214個(2019/08/16現在の予定)訳します。訳し終わるまでには解いた問題数が増えると思いますので214個以上になると思います。
- 問題文さえ日本語で提示されれば取っ掛かりになるという方がたくさんいると思うので記事にしてみました。
Kata(8級): Array plus array(配列+配列) 翻訳日時: 2019/08/16 19:15
https://www.codewars.com/kata/5a2be17aee1aaefe2a000151
私はコーディング経験が浅いので、2つの配列の合計を求めたいのです...言い換えると、2つの配列の全ての要素を合計したいのです。手助けいただき感謝いたします。
P.S. それぞれの配列は整数のみを含みます。出力も数値となります。
Kata(8級): Basic variable assignment(基礎:変数の代入) 翻訳日時: 2019/08/16 19:08
https://www.codewars.com/kata/50ee6b0bdeab583673000025
このコードは "name" という変数に "codewa.rs" を格納するべきですがうまくいっていません。うまくいかない理由を解明してください。
var a == "code"; var b == "wa.rs"; var name == a + b;Kata(8級): String repeat(文字列の繰り返し) 翻訳日時: 2019/08/16 18:53
https://www.codewars.com/kata/57a0e5c372292dd76d000d7e
repeatString という名前で、与えられた文字列を指定された回数繰り返す関数を書いてください。
repeatStr(6, "I") // "IIIIII" repeatStr(5, "Hello") // "HelloHelloHelloHelloHello"Kata(8級): Even or Odd(偶数か奇数か) 翻訳日時: 2019/08/16 18:48
https://www.codewars.com/kata/53da3dbb4a5168369a0000fe
一つの整数を引数として取り、偶数に対しては "Even" を返し、奇数に対しては "Odd" を返す関数を作ってください。
Kata(7級): Descending Order(降順) 翻訳日時: 2019/08/16 18:34
https://www.codewars.com/kata/5467e4d82edf8bbf40000155
あなたのタスクは任意の負でない整数を引数として取り全ての桁を降順にして返す関数を作ることです。本質的には、桁を再配置して可能な最大の数を作り出してください。
例:
入力: 21445 出力: 54421
入力: 145263 出力: 654321
入力: 1254859723 出力: 9875543221Kata(7級): Disemvowel Trolls(巨人の母音字を削除する) 翻訳日時: 2019/08/16 18:11
https://www.codewars.com/kata/52fba66badcd10859f00097e
巨人があなたのコメントセクションを攻撃しています!
この状況に対処する一般的な方法は、巨人のコメントから全ての母音を取り除いて、脅威を中和することです。
あなたのタスクは一つの文字列を取り、全ての母音が削除された新たな文字列を返す関数を書くことです。例えば, "This website is for losers LOL!" という文字列は "Ths wbst s fr lsrs LL!" となります。
注記: このKataでは y は母音とみなされません。
Kata(7級): Friend or Foe?(味方か敵か?) 翻訳日時: 2019/08/16 17:47
https://www.codewars.com/kata/55b42574ff091733d900002f
文字列のリストをフィルタリングして味方の名前のみが含まれるリストを返すプログラムを作ってください。
もし名前が正確に4文字で構成されるならば、あなたの味方であると確信できます! そうでない場合には味方でないと確信できます...例: インプット = ["Ryan", "Kieran", "Jason", "Yous"], 出力 = ["Ryan", "Yous"]
注記: 入力内の元々の順序を出力内でも保持してください
Kata(6級): Bit Counting(ビットを数える) 翻訳日時: 2019/08/16 17:29
https://www.codewars.com/kata/526571aae218b8ee490006f4
一つの整数を取り、その数の2進数表現内で1と等しいビットの数を返す関数を書いてください。入力は常に負数でないことが保証されています。
例:1234 の2進数表現は 10011010010 ですので、このケースでは関数は 5 を返すべきです。
- 投稿日:2019-08-16T17:46:37+09:00
JavascriptのJも知らない初心者のToDoList作り
はじめに
Pythonの勉強から始まり、あまりにもJavascriptの知識がなかったため、すこしでもJavascriptはどういう言語なのか体験するために、w3schoolsさんのチュートリアルを教科書として流れを書いてみました。
(備忘録のようなポストです。)練習の流れ
https://www.w3schools.com/howto/howto_js_todolist.asp
を見本とし、ついて行きながらカスタムの練習もかねています。HTMLの作成
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <link rel="stylesheet" href = "todolist.css"> </head> <body> <div id="myDIV" class="header"> <h1>To do List</h1> <input type="text" id="myInput" placeholder="今日の予定は?"> <span onclick="newElement()" class="addBtn">追加</span> </div> <ul id="myUL"> </ul> <script src = "todolist.js"></script> </body> </html>見本の内容を
の内容とし、ToDoListのページを作成します。
Title + Input + ボタンの基本枠です。
完成後、F12で検証のところから予定が追加されたとき、
<ul></ul>の間に追加した内容が
<li></li>に囲まれてはいります。CSSの作成
/* リストの設定 */ ul { margin: 0; padding: 0; } ul li { cursor: pointer; position: relative; padding: 12px 8px 12px 40px; background: #eee; padding: 12px; transition: 0.2s; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } ul li:nth-child(odd) { background: #f9f9f9; } ul li:hover { background: #ddd; } ul li.checked { background: #888; color: #fff; text-decoration: line-through; } ul li.checked::before { content: ''; position:absolute; border-color: #fff; border-style: solid; border-width: 0 2px 2px 0; top: 10px; left: 16px; transform: rotate(45deg); height: 15px; width: 7px; } /* リストの削除ボタン */ .close { position:absolute; top: 0; right: 0; padding: 12px 16px 12px 16px; } .close:hover { background-color: #3094ba; color: white; } /* Header-Inputと追加ボタンを含む */ .header { background-color: #5dbcd2; padding: 30px 40px; color:white; text-align: center; } .header:after { content:""; display:table; clear: both; } input { margin: 0; margin-top: -5px; border: none; border-radius: 0; width: 75%; padding: 10px; float: left; font-size: 16px; } .addBtn { padding: 10px; width: 25%; background: #d9d9d9; color: #555; text-align: center; font-size: 16px; cursor: pointer; transition: 0.3s; border-radius: 10px; } .addBtn:hover { background-color: #636b6d; color: #fff }Title . Iuput(追加ボタン) . List(削除ボタン)のCSSを設定しています。見本を基準として、cssの練習で少しの内容の中でズレているところや色を変更してます。
Javascriptの作成
//削除ボタンを生成する。"\u00D7" → Unicode var myNodelist = document.getElementsByTagName("LI"); var i; for (i = 0; i < myNodelist.length; i++) { var span = document.createElement("SPAN"); var txt = document.createTextNode("\u00D7"); span.className = "close"; span.appendChild(txt); myNodelist[i].appendChild(span); } //削除ボタンをクリックするとリストアイテムがなくなる。 var close = document.getElementsByClassName("close"); var i; for (i = 0; i < close.length; i++) { close[i].onclick = function() { var div = this.parentElement; div.style.display = "none"; } } //チェック表示 var list = document.querySelector('ul'); list.addEventListener('click', function(ev) { if (ev.target.tagName === 'LI') { ev.target.classList.toggle('checked'); } }, false); //追加ボタンを押すとリストアイテムが生成される。 function newElement(){ var li = document.createElement("li"); var inputValue = document.getElementById("myInput").value; var t = document.createTextNode(inputValue); li.appendChild(t); if (inputValue === '') { alert("なんか書いてや!"); } else { document.getElementById("myUL").appendChild(li); } document.getElementById("myInput").value = ""; var span = document.createElement("SPAN"); var txt = document.createTextNode("\u00D7"); span.className = "close"; span.appendChild(txt); li.appendChild(span); for (i = 0; i < close.length; i++) { close[i].onclick = function() { var div = this.parentElement; div.style.display = "none"; } } }
- ボタンの生成
- リストからの非表示
- クリックすると ~なる。
練習してみたら....
HTMLやcssはpythonを勉強するときにも少し触ったことがあるのでわかりやすく優しく書いている見本を元に自分の思うことを変更しながら書いてみることができました。
しかし、Javascriptはやはり基本構文の知識がないため、見本を通じてできる機能や表現の仕方を分解しながら勉強する機会になりました。
これからの勉強の中で基本の構文の練習も続けながらいい例文の表現の分析勉強も続けたいと思います。
- 投稿日:2019-08-16T17:33:40+09:00
yarn.lockのconflict解消方法
yarnでJSライブラリのパッケージ管理を行っている場合、
しばしばbranch間でconflictする場合があると思います。dependabotなどでライブラリのバージョンアップをしたり、githubの脆弱性チェックなどで
yarn.lockを直接書き換えたりした場合などに発生したりします。ただし、その場合でも手動でyarn.lockを編集することは非推奨となっています。
(yarn.lock自体に正確なパッケージの依存関係が記載されているため、直接編集すると簡単に壊れる)yarn.lockのconflictを解消する
詳細は下記記事を参考にしてください
https://www.jakewiesler.com/blog/merge-conflicts-in-yarn-lock/作業前にmasterを最新にしている前提です。
masterから派生したコンフリクトしているブランチで次のような作業を行うことで解消できます。$ git rebase master # ①masterブランチのyarn.lockにあわせる $ git checkout master -- yarn.lock # ②その後そのブランチの依存関係を解決(パッケージの再インストール) $ yarn install # ③rebaseを続ける $ git add yarn.lock $ git rebase --continue # 再度conflictした場合は上の①②を行い、③を行う FINyarn.lockを直接編集する(脆弱性などの暫定対応)
ライブラリが参照している内部ライブラリが脆弱性があり、それに対してバージョンを上げたい場合があります。
その場合は手動でyarn.lock内の該当の依存ライブラリ部分を削除して再度yarn installします。参考:
https://github.com/yarnpkg/yarn/issues/4986#issuecomment-395036563例えば、package.jsonに記載されているjsonwebtoken(明示的な依存)が暗黙的に内部依存しているjws^3.0.0を脆弱なjws=3.1.4で解決している場合、代わりにパッチを適用したjws=3.1.5にアップグレードする必要があります。
yarn.lock内の下記のようなjwsのエントリを削除して再度
yarnし直します。
そうすると間接的な依存関係と影響を受けるパッケージは、他のものに触れることなく更新されます(yarn v1.3以降で動作)jws@^3.0.0, jws@^3.1.4: version "3.1.4" resolved "https://registry.npmjs.org/jws/-/jws-3.1.4.tgz#f9e8b9338e8a847277d6444b1464f61880e050a2" dependencies: base64url "^2.0.0" jwa "^1.1.4" safe-buffer "^5.0.1"
- 投稿日:2019-08-16T17:28:43+09:00
golangとjavascriptで日めくりカレンダー的なのを作った。
やりたかったこと
1・1か月ごとのカレンダーを表示
2・スケジュールの入力ができる(todoアプリ的な)
3・タスクがすべて完了したらマスの色を変える。
4・一か月前、一か月後まではボタンで切り替えて表示ここまでを約1週間で行いたかった。
実際は?
現在の年月日、曜日を表示、時刻はデジタル時計のように、リアルタイムで更新するようにしました。
日めくりカレンダーのようにしました。使用言語
golang,javascript,html,cssフォルダ構成
calender |_resources | |_index.html | | | |_styles.css |_hello.go苦戦したところ
goで定義したもの(現在日時など)をどうやってHTMLに埋め込むか
CSSファイルを読み取ってくれなかった。
この二つが特に悩まされた。後はgoにこだわらないでほかの言語なら行けたんじゃね?と思う
(Goを必ず使わなくてはいけなかったのでそこのさじ加減が掴めませんでした)どうやったか
hello.gopackage main import ( "fmt" "html/template" "net/http" "time" ) func main() { fmt.Println("The Server runs with http://localhost:3000/") http.Handle("/resources/", http.StripPrefix("/resources/", http.FileServer(http.Dir("resources/")))) http.HandleFunc("/", Handler) http.ListenAndServe(":3000", nil) } type Time struct { Year int Month string Day1 int Weekday string Hour int Minute int Second int } func Handler(w http.ResponseWriter, r *http.Request) { t := Time{ Year: time.Now().Year(), Month: time.Now().Month().String(), Day1: time.Now().Day(), Hour: time.Now().Hour(), Minute: time.Now().Minute(), Second: time.Now().Second(), Weekday: time.Now().Weekday().String(), } tmpl := template.Must(template.ParseFiles("./resources/index.html")) tmpl.Execute(w, t) }苦戦した箇所である、html、cssを読み込む処理は、
tmpl := template.Must(template.ParseFiles("./resources/index.html"))
tmpl.Execute(w, t)
読み込みたいファイルのパスを指定してあげて、
上で定義した変数を引数にして一緒に実行するようにしました。
これでhtml上にgoを埋め込むことに成功。cssの読み込みは、
http.Handle("/resources/", http.StripPrefix("/resources/", http.FileServer(http.Dir("resources/"))))
この一文でresourcesフォルダ以下の静的ファイルを探してきてくれ、読み込みができるようになりました。index.html<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>テスト</title> <link rel="stylesheet" type="text/css" href="../resources/styles.css"> <script> function digitalClock(){ // 現在日時を取得 var now = new Date(); // 「時」を取得 var hour = now.getHours(); // 「分」を取得 var minute = now.getMinutes(); // 「秒」を取得 var second = now.getSeconds(); // 0埋めで2桁表示にする 例)08:45:09 if(hour < 10) hour = "0" + hour; if(minute < 10) minute = "0" + minute; if(second < 10) second = "0" + second; // 現在時刻を表示する var elem = document.getElementById("clock"); elem.innerHTML = hour + ":" + minute + ":" + second; // 500ミリ秒後に再帰呼び出し setTimeout(digitalClock, 500); } window.onload = function(){ digitalClock(); } </script> </head> <body> <div class="calenderbox"> <pre><h1 class="month_now"><span>{{.Year }}</span></h1></pre> <div id ="box1"> <div id="box2"> <p class="Date">{{.Month }}</p> <p class="Date">{{.Day1 }}</p> <p class="Date">{{.Weekday }}</p> </div> <div id="box3"> <p id="clock"></p> </div> </div> </div> </body>hello.goで処理したので、goの変数を以下のような形で受け取ることができます。
{{.変数名 }}
また、内で時刻を取得し、デジタル時計の見た目に設計したものをhtmlで返しています。
cssファイルは特に変わったことをしていないので割愛させて頂きます。
まとめ
触ったことがないものばかりだったのでここまでできたのも奇跡に等しいです。
ただ、まだ追加したい機能やより良い記述があるはずなのでしばらくこれを継続して取り組みます。
Goはとりあえずチュートリアルを完走します。
あとはjavascriptはじめてまともに使ってマスターしたらかなり便利だと感じました。
こちらもしっかり取り組んでいきます。
- 投稿日:2019-08-16T17:18:32+09:00
メモ:住所から番地以下を除去する
この記事は
- 住所文字列から番地以下を除去して都道府県市区町村のみにしたメモです
コード
- いきなりですが結論です
shrink_address.jsfunction shrinkAddress(address){ // nullは空文字で返却 if(!address) return "" // 番地っぽい文字列を抽出する正規表現 let pattern = /([0-90-9]+|[一二三四五六七八九十百千万]+)*(([0-90-9]+|[一二三四五六七八九十百千万]+)|(丁目|丁|番地|番|号|-|‐|ー|−|の|東|西|南|北){1,2})*(([0-90-9]+|[一二三四五六七八九十百千万]}+)|(丁目|丁|番地|番|号){1,2})/ let result = address.match(pattern) // マッチしたら番地よりも前の文字列を返却 if(!!result){ return address.substring(0, result['index']) } else { return address } }テスト
- 半角、全角、漢数字、ハイフン、丁目などに耐久しています
- 地名に漢数字が混じっていても大丈夫でした
shrinkAddress("東京都中央区銀座1-2-3") "東京都中央区銀座" shrinkAddress("東京都中央区銀座1−2−3") "東京都中央区銀座" shrinkAddress("東京都中央区銀座1の2の3") "東京都中央区銀座" shrinkAddress("東京都中央区銀座一丁目二番三号") "東京都中央区銀座" shrinkAddress("東京都北区十条1-2-3") "東京都北区十条" shrinkAddress("高知県四万十市中村1-2-3ハイツ高知102") "高知県四万十市中村"参照
- 下記記事をものすごく参考にしましたので、正規表現の詳細はこちらの解説をご参照していただければと
- Rubyの埋め込み変数は今回は全て展開しています
- https://qiita.com/YSRKEN/items/5371e67950425ace6a73
- 投稿日:2019-08-16T17:18:11+09:00
Vue.jsでチェス盤を作ってみたら8行だった。
チェス盤はなぜ白黒なのか
あの違い互いの白黒盤のせいでちょっと苦戦した話です。
一応チェスを後々作る前提でボードを作ってみました。
今回のゴール
1.見た目的にチェス盤と認識できるものを作る
2.今後チェスを作る前提の構造になっていないとダメ†考察†
実際チェスを作るとなると必須となるのが、
64マスそれぞれに座標が備わっていること。つまりこういうこと。
[1-1, 1-2, 1-3, 1-4, 1-5, 1-6, 1-7, 1-8, 2-1, 2-2, 2-3, 2-4, 2-5, 2-6, 2-7, 2-8, 3-1, 3-2, 3-3, 3-4, 3-5, 3-6, 3-7, 3-8, 4-1, 4-2, 4-3, 4-4, 4-5, 4-6, 4-7, 4-8, 5-1, 5-2, 5-3, 5-4, 5-5, 5-6, 5-7, 5-8, 6-1, 6-2, 6-3, 6-4, 6-5, 6-6, 6-7, 6-8, 7-1, 7-2, 7-3, 7-4, 7-5, 7-6, 7-7, 7-8, 8-1, 8-2, 8-3, 8-4, 8-5, 8-6, 8-7, 8-8]X軸とY軸の数値を合わせてるような感じ。
この配列を作るだけであれば簡単。
let board = []; for(let x = 1; x < 9; x++) { for(let y = 1; y < 9; y++) { board.push(x + "-" + y) } }8×8のループを行えば一瞬でできてしまうので、
Vue.jsでレンダリングするとしても、v-for="n in 8"を二回やればいいだけ。将棋であればここで完成。
だがしかし、チェス盤となると少しやっかい。白黒白黒白黒白黒
ときて、
今度は
黒白黒白黒白黒白!
まあ冒頭でも言ってますが、
つまり、1行目と2行目だと、1列目の色が反転しているということです。えらいこっちゃ~~~となっていろいろ考えて
しばらくしてやっとこちらの文系エンジニアも気づきました。「奇数行と偶数行で出し分けすればいいんだ」
つまりはこういうこと。
<div class="board"> <div v-for="x in 8"> <div v-for="y in 8"> <div class="square-black" :id='`${x}-${y}`' v-if="(x + y) % 2 == 0"></div> <div class="square-white" :id='`${x}-${y}`' v-else></div> </div> </div> </div>おぉ~~~できた!
ちゃんとそれぞれidも1-1から8-8まで振れている。
そしてなによりコードがかなり短く収まった!!一応解説
8回ループを二回するまでは同じ、
そしてそこからは、一回目のループxと二回目のループyそれぞれの値でv-if使って出し分け。xの一回目とyの一回目であれば1と1になるので、
合計が2、偶数なので黒スタート。xの二回目とyの一回目であれば2と1になるので、
合計が3、奇数なので白スタート。最後に
座標もしっかり入っているので、
チェス作りの道も開けたといった感じですね。筆者はこういう閃きみたいなのが圧倒的に欠如している
文系エンジニアなので時間はかかったものの、
法則を見つけて何かをスッキリ書くのはよだれが出てくるぐらい好きです。いつになるかわからないですが、
次はチェスの動きの部分を投稿したいです。スタイルも含めたコードを載せておきます。
Board.vue<template> <div class="board"> <div v-for="x in 8"> <div v-for="y in 8"> <div class="square-black" :id='`${x}-${y}`' v-if="(x + y) % 2 == 0"></div> <div class="square-white" :id='`${x}-${y}`' v-else></div> </div> </div> </div> </template> <style scoped> .board { width: 640px; height: 640px; border: 4px solid black; } .square-black { width: 80px; height: 80px; background-color: black; float: left; } .square-white { width: 80px; height: 80px; background-color: white; float: left; } </style>
- 投稿日:2019-08-16T16:41:24+09:00
【Electron】続・HTML+JS+C#=クライアントアプリケーション!? Electron.Netで開発・デプロイしてみる
先日投稿した 「【Electron】HTML+JS+C#=クライアントアプリケーション!? Electron.Netを使ってみる」にて、
実際の開発やデプロイまでの流れについて、情報が足りない部分があったのでエントリを追加しました。今回はデバッグの方法とコンパイルしてインストーラを作成するまでの方法を記載しています。
1. VisualStudioでデバッグしてみる
環境の構築はできたけど、開発時にステップ実行したい・・。したくない?
公式ドキュメントによれば、VisualStudioでのデバッグ方法も案内されていました。有能。ソリューションファイルをVisualStudioで開きます。
デバッグタイプが「Electron.NET App」と表示されています。
しかし、このままデバッグしてもブレークポイントで止まりません。悲しい。
デバッガを起動しながら[デバッグ]メニューの[プロセスにアタッチ]をクリック。
[プロセスをフィルター]欄にプロジェクト名を入力します。今回は「electron_test」を入力。
すると、起動中のデバッグプロセスが表示さるので、選択して[アタッチ(_A)]をクリック。
すると、ブレークポイントが有効化されました!嬉しい!
その状態でメニューをクリックすると正常にステップ実行ができます。
FormアプリケーションやWPFの経験がある方ならなじみやすいですね。
2. コンパイルしてみる
じゃあ、このまま構築を行うとして、デプロイはどうするの?
インストーラの作り方なんてわからないよ・・。そんなあなたに朗報です。
Electron.NETでは、インストーラの作成までサポートしています。プロジェクトファイルのパスからコマンドプロンプトを起動します。
以降の作業をGitbashで行った場合、なぜかうまくいきませんでした・・。ワイだけ?
以下のコマンドを投入します。
cmd.exeC:\Users\Delight\source\repos\electron_test\electron_test>electronize build /target win Build Electron Application... Build ASP.NET Core App for win-x64... Executing dotnet publish in this directory: C:\Users\Delight\source\repos\electron_test\electron_test\obj\desktop\win Build ASP.NET Core App for win-x64 under Release-Configuration... Microsoft Windows [Version 10.0.17134.950] (c) 2018 Microsoft Corporation. All rights reserved. C:\Users\Delight\source\repos\electron_test\electron_test>dotnet publish -r win-x64 -c Release --output "C:\Users\Delight\source\repos\electron_test\electron_test\obj\desktop\win\bin" .NET Core 向け Microsoft (R) Build Engine バージョン 15.9.20+g88f5fadfbe Copyright (C) Microsoft Corporation.All rights reserved. C:\Users\Delight\source\repos\electron_test\electron_test\electron_test.csproj の復元が 55.56 ms で完了しました。 electron_test -> C:\Users\Delight\source\repos\electron_test\electron_test\bin\Release\netcoreapp2.2\win-x64\electron_test.dll electron_test -> C:\Users\Delight\source\repos\electron_test\electron_test\bin\Release\netcoreapp2.2\win-x64\electron_test.Views.dll electron_test -> C:\Users\Delight\source\repos\electron_test\electron_test\obj\desktop\win\bin\ C:\Users\Delight\source\repos\electron_test\electron_test> Start npm install... Microsoft Windows [Version 10.0.17134.950] (c) 2018 Microsoft Corporation. All rights reserved. C:\Users\Delight\source\repos\electron_test\electron_test\obj\desktop\win>npm install --production npm notice created a lockfile as package-lock.json. You should commit this file. added 67 packages from 64 contributors and audited 359 packages in 4.246s found 0 vulnerabilities C:\Users\Delight\source\repos\electron_test\electron_test\obj\desktop\win> Start npm install electron-builder... Microsoft Windows [Version 10.0.17134.950] (c) 2018 Microsoft Corporation. All rights reserved. C:\Users\Delight\source\repos\electron_test\electron_test\obj\desktop\win>npm install electron-builder --global C:\Users\Delight\AppData\Roaming\npm\install-app-deps -> C:\Users\Delight\AppData\Roaming\npm\node_modules\electron-builder\out\cli\install-app-deps.js C:\Users\Delight\AppData\Roaming\npm\electron-builder -> C:\Users\Delight\AppData\Roaming\npm\node_modules\electron-builder\out\cli\cli.js + electron-builder@21.2.0 updated 1 package in 4.921s C:\Users\Delight\source\repos\electron_test\electron_test\obj\desktop\win> ElectronHostHook handling started... Build Electron Desktop Application... Executing electron magic in this directory: C:\Users\Delight\source\repos\electron_test\electron_test\bin\desktop Create electron-builder configuration file... Microsoft Windows [Version 10.0.17134.950] (c) 2018 Microsoft Corporation. All rights reserved. C:\Users\Delight\source\repos\electron_test\electron_test\obj\desktop\win>node build-helper.js C:\Users\Delight\source\repos\electron_test\electron_test\obj\desktop\win> Package Electron App for Platform win... Microsoft Windows [Version 10.0.17134.950] (c) 2018 Microsoft Corporation. All rights reserved. C:\Users\Delight\source\repos\electron_test\electron_test\obj\desktop\win>electron-builder . --config=./bin/electron-builder.json --win --x64 -c.electronVersion=5.0.8 窶「 electron-builder version=21.2.0 os=10.0.17134 窶「 loaded configuration file=C:\Users\Delight\source\repos\electron_test\electron_test\obj\desktop\win\bin\electron-builder.json 窶「 packaging platform=win32 arch=x64 electron=5.0.8 appOutDir=C:\Users\Delight\source\repos\electron_test\electron_test\bin\Desktop\win-unpacked 窶「 default Electron icon is used reason=application icon is not set 窶「 building target=nsis file=C:\Users\Delight\source\repos\electron_test\electron_test\bin\Desktop\electron_test Setup 1.0.0.exe archs=x64 oneClick=true perMachine=false 窶「 building block map blockMapFile=C:\Users\Delight\source\repos\electron_test\electron_test\bin\Desktop\electron_test Setup 1.0.0.exe.blockmap C:\Users\Delight\source\repos\electron_test\electron_test\obj\desktop\win> ... doneすると、
.\bin\Desktopにインストーラが生成されます!
- [プロジェクト名 Setup 1.0.0.exe] ... インストーラ形式ファイル
- [win-unpacked] ... インストーラに内蔵されている実行ファイルetcが格納
早速インストーラを実行してみると、インストーラが実行され、スタートメニューに表示されました!実行も問題なし!
お手軽にデプロイまでつなげられるのはうれしい限りですね。以下のコマンドでクロスプラットフォーム向けのコンパイルもできるとのこと。無敵か。
(osx形式の場合はMacでコンパイルする必要があるとのことです。)electronize build /target win electronize build /target osx electronize build /target linuxさらに詳細なコンパイルについては公式ドキュメントをご覧ください。
https://github.com/ElectronNET/Electron.NET#build3. まとめ
Electron.NETはまだまだ浸透していませんが、C#のフロントとして採用の価値はあると思います。
React+ReduxなどのJSフレームワークと組み合わせて構築できるかも試してみたいですね。今回使用したコードは以下のリポジトリにアップしておきましたので、
よければご参照ください。
https://github.com/nqdior/electron_testご質問・ご要望等あればお気軽にコメントくださいませ~。
- 投稿日:2019-08-16T16:41:18+09:00
Elm勉強会@弊社(2019/08/28)
Elmとは
- フロントエンド開発に特化したプログラミング言語
- JavaScriptにコンパイルされて実行される、いわゆるAltJS
- 2012年に登場した、比較的若い言語
- 静的型付言語&純粋関数型言語
- 同じく純粋関数型言語であるHaskellの子孫 (ElmのコンパイラはHaskellで書かれています)
Elmはフレームワーク内蔵言語
React + Redux + TypeScript、またはVue + Vuex + TypeScript相当の機能を内蔵しているので、フレームワーク内蔵言語とも言われています。
ReactやVueの場合、新しい言語を学ばずとも慣れ親しんだJavaScriptで記述できるのがメリットですが、Elmの場合は言語自体を新しく作っているので、例えば「ReduxとReactを導入して繋ぐためのコード」等を書かなくて済みます。そのため、本質の部分だけが書かれたシンプルなコードになりやすいというメリットがあります。
JavaScriptとの違い
変数について 〜JavaScriptの場合〜
JavaScriptでは、
varかletで宣言された変数には後から別の値を再代入することが出来ますが、constで宣言された定数には再代入ができません。JavaScriptvar a = 1; a = 2 let b = 1; b = 2; const c = 1; c = 2; // エラー!
変数について 〜Elmの場合〜
Elmの場合は
varもletもconstもありません。一度宣言したら再代入ができないため、全ての値がイミュータブル(不変)です。Elma = 1 a = 2 -- コンパイルエラー!そもそも代入や再代入という概念が存在せず、変数と呼ぶことも少ないようです。「aという値を定義する」とか「aという名前で値を束縛する」と表現します。
オブジェクト 〜JavaScriptの場合〜
constで宣言した場合でも、オブジェクトのプロパティを上書きすることはできます。JavaScriptconst takashi = { name: "たかし", age: 36, }; takahsi.age = 37; // ageプロパティ上書き。
レコード 〜Elmの場合〜
Elmにはオブジェクトはありませんが、オブジェクトに似たレコードというものがあります。
Elmtakashi = { name = "たかし" , age = 36 } newTakashi = { takashi | age = 37 }レコードは、JSのオブジェクトと異なり完全にイミュータブル(不変)なため、ageだけ上書きするということもできません。
上記のコードも、一部を変えた新しいレコードを作り出しています。
元のレコード(takashi)は36歳のままです。
書く順序による影響 〜JavaScriptの場合〜
↓エラーにならないパターン
JavaScriptconst a = 3; const b = 5; const c = a + b;↓エラーになるパターン
JavaScriptconst c = a + b; const a = 3; const b = 5;
aやbに値を代入するより上の行で、aやbを使った計算などをしようとするとエラーになります。
書く順序による影響 〜Elmの場合〜
Elmc = a + b a = 3 b = 5↑
aやbを定義するより上の行でaやbを計算に使うことができます。
JavaScriptでは状態の変化を直接コードに書くことができる
JavaScriptlet takashi_age = 36; console.log(takashi_age); // 36 takashi_age = 37; console.log(takashi_age); // 37 takashi_age = 38; console.log(takashi_age); // 38 takashi_age = 39; console.log(takashi_age); // 39JavaScriptの場合は変数の値を変更できるので、例えば
console.logをtakashi_age = 〇〇;の上の行に書くか下の行に書くかが重要です。
Elmでは、場面や状態の変化を直接コードで書けない
再代入という概念がなく、コード内の全ての値が不変だからです。
Elmseason = "夏" -- ずーっと夏 age = 36 -- ずーっと36歳そのため「この行でageを呼び出したら
36だけど、もう少し下の行で呼び出したら37だった」ということがありません。再代入ができない=コードの中で状態が変わらない=時間が止まっているようなものなので、コードの中に前とか後とかが無いイメージです。
一瞬の中に全てのコードが書かれているような、スタープラチナ・ザ・ワールドみたいな感じです。
つまり、どの行でその値を呼び出すかは結果に関係なく、その値を宣言するより上の行で呼び出すことも可能です。
再代入できないことによるメリット
- 違うものには違う名前がつくので明解
- この行ではこの変数は何の値を参照しているか?と考えなくていい。どの行でも同じ。
- 値の中身がコロコロ変わらないので、いちいちconsole.log()等で確認する手間が減る
- 値の中身がコロコロ変わらないので、GitHubでソースを読むときも少し楽
関数 〜JavaScriptの場合〜
JavaScriptでは↓こんな感じで関数を宣言したり、または変数なり定数に格納したりしますね。
JavaScriptfunction add (a, b) { return a + b; }または
JavaScriptconst add = function (a, b) { return a + b; }アロー関数式で書くと
JavaScriptconst add = (a, b) => a + b;実行するときは
JavaScriptconst result = add(3, 5);
関数 〜Elmの場合〜
Elmでは↓こんな感じで関数を定義します。
Elmadd a b = a + b実行するときは
Elmresult = add 3 5Elmではカッコもカンマも必要ありません。
戻り値を返すのにreturnを書く必要もありません。関数と変数の境目があまりない感じです。
引数があれば関数です。
関数の返す値も(引数が同じならば)常に一定
それは、全ての値やレコードが不変で、場面や状態といったものを直接コードで書くことができないからです。
Elmscene = "朝" greeting name = if scene == "朝" then name ++ "さん、おはよう!" else name ++ "さん、こんにちわ!"再代入できないため
sceneは常に"朝"です。そのためgreeting関数が返す文字列は、常に"〇〇さん、おはよう!"となります。
コードの中で何回この関数が呼ばれたとしても、外部の変数の状態によって戻り値が変わったりすることはありません。
なぜなら、外部の変数の状態が不変だからです。
同じ引数を受け取った場合、同じ戻り値を返します。
この性質を参照透過性と言います。
Elmは何も変えられない・・・?
Elmには再代入という概念はありません。全ての値は不変です。
そのため、関数の中に「何か外部の値を変えてくれい」という命令的なコードを書くこともできません。
関数は、引数を受け取って、それを元に戻り値を返すことしかできません。
つまり「〇〇は△△である」と言う定義しか書けません。具体的には、JavaScriptでいう、
JavaScriptdoSomething(a, b);的な「戻り値を保存しない」コードは書けません。
doSomething関数の中から外部のものを変えるすべが無いため、戻り値を返して変数なり定数に格納しないと意味がないのです。
関数の中から唯一外の世界に影響を与えられるのは戻り値だけですので。
実行だけして変数に格納しない、というコードを書くことはありません。JavaScriptconst result = someCalculate(a, b);上記のような「関数で何らかの処理をして、変数に格納する」つまり「〇〇は△△である」的なパターンしかありません。
何も変えられなくて、この言語なにができるの?
定義を並べるだけで、命令が書けなくて、コンピュータに何か仕事をさせることができるのでしょうか。
しかし「Webサイトを作る」ということは「ユーザがどんな行動をしたら、Webサイトの持つデータや見た目はどのように変わるべきか」という「定義をしている」とも言えるので、定義さえすればWebアプリを作成することができます。
デモアプリをみてみましょう
デモアプリ
ブラウザエディタEllieでご確認ください。
※好きにコードをいじってもらって大丈夫です!
(私のコードとは別に保存されるので)
定義だけで状態変化を表現できました
しかも、React + Redux(またはhooks)+ TypeScriptよりだいぶコードは少なめです。
オブジェクト指向との違い
オブジェクト指向とは
データと関数(メソッド)をまとめて定義した「クラス」つまり種を定義して、そこから実体となるオブジェクトを生成する。
人間が現実世界を認識するときの考え方に近いため、比較的直感的にプログラミングできるという、非常に強力なスタイル。※諸説ありますが、ここでは上記の認識で進めます。
クラスを使って書いてみる
JavaScriptclass Human { constructor (name, age) { this.name = name; this.age = age; } increment () { this.age++; } decrement () { this.age--; } } const takashi = new Human("たかし", 36); takashi.increment(); takashi.increment(); console.log(takashi.age); // 38関連するデータと関数がクラスの中にまとまって書かれているので「誰のための変数や関数なのか」という所属が一目瞭然なコードになります。
Elmでも、オブジェクト指向っぽく考えることもできる
ElmにはClass構文は存在しません。データ(Model)と関数も特にまとまっていませんでしたね。
データと関数が別れてはいますが、例えば「この関数は、Int型とHuman型の引数を受け取って、Human型の値を返す」ということを、型によって定義できます。ElmaddAge : Int -> Human -> Human -- 型注釈 addAge int human = { human | age = human.age + int }Classのように「誰が持っている関数か」という所属は定義できませんが「誰に何をするための関数か」を型によって定義できます。
そのため、言語仕様はかなり違いますが、割とオブジェクト指向な気分でも書けます。
コンポーネント指向との違いは?
divやbuttonなどのhtml系の関数を使って「任意の情報(引数)を受け取ってhtml要素を返す関数」を書けます。ElmbuttonComponent props = button [ class "common-btn" ] [ text props.text ]これってReactやVueのFunctional Componentですよね。
The Elm Architectureについて
ReduxやVuexの元となった手法
model, view, updateの3つを使った先ほどのパターンをThe Elm Architectureと呼びます。
コード上の全てが不変なため、状態変化を直接コード上で書くことはできませんが、それでも状態変化を表現できるちょっと不思議なやり方です。
ReduxもVuexも、このThe Elm Architectureの影響を受けて作られました。
再代入ができないので、状態変化を表現したい値はModelに組み込んで管理します。
そしてupdate関数で「どんなメッセージが来たら、状態(model)はどんな風に変わるべきか」を定義します。
modelが新しくなるとviewが再描画されます。
そうすることで、命令を書けなくとも、定義だけで状態の変更を表現できました。もともと言語に内蔵されている機能なので「それを使うための準備のコード」がほぼ無く、非常にシンプルです。
そのため、読みやすく書きやすいです。
型システムが優秀なため、修正にも強い
例えば先ほどの年齢カウンターにリセット機能を追加してみましょう。
リセット機能追加の流れ
- リセットボタンをview関数の中に追加
- そのボタンをクリックしたら
Resetというメッセージが発生するように属性を追加Resetなんて知らないよ!とエラーが出る- メッセージの型に
Resetを追加Resetのケースも書かないと!と導いてくれるエラーメッセージが分かりやすいのもElmの特徴です。
そしてこのように、エラーになるようなコードは事前に検査して発見してくれるため、実行時エラーを目にすることは実質ありません。
純粋関数型言語の特徴
副作用を直接書けない
再代入ができないため、
「いくつか関数を実行したら、知らん間にxの値が変わっとった!!!」
みたいな副作用が起こりません。むしろ起こせません。そのため、状態変化を表現したい値は
modelに組み込まなければなりません。また、状態を更新する処理はupdate関数の中に書かなければいけません。この縛りは一見窮屈なようですが、ルールが明確化されていることで、結果的にそれぞれの処理を探しやすくなるというメリットがあります。
参照透過性が担保されている
Elmの関数は、外部の値に依存して振る舞いを変えません。引数が同じであれば、同じ戻り値を返します。
それにより、単体テスト・自動テストがしやすくなります。
逆に、外部に依存しまくっている関数はテストがしづらくてしょうがないです。
例えば、期限付きのタスク管理システム
↑のテストをする場合のことを考えてみます。
- タスクを作成する前に、日時操作をするための専用管理画面に入り、今が何月何日なのか設定する。
- その後Webサイト側で新規タスクを作る(期限日も設定)
- 時間操作の画面で日にちを進める。
- そうするとタスクのステータスがから「期限間近」や「期限を過ぎています」等に変わる。
- ステータスに応じた表示や挙動になっているかどうか、ようやくテストが可能に・・・。
これだとテストしづらいですよね。。。
条件は全て引数として渡す
「周りの状態───つまり日時───によって挙動を変える」関数でなく「日時等の条件はすべて引数として受け取り、引数によってのみ戻り値の結果が決まる」関数・・・つまり参照透過的な関数にすれば、自動テストが書きやすくなります。
テストの自動化
例えばJest等のテストフレームワークで、いくつも引数を変えながら関数の自動テストをするようにテストコードを書いてきます。
gitコミットをするたびにそのテストが走るように設定しておきます。そうしておけば、修正フェーズや追加開発フェーズで「ここのコードを修正したことで逆に別のところがバグってもうた!」的なことをやらかしてしまった場合でも、すぐにテストでエラーが出て気づけるので安心ですよね。
そのためには、引数の渡し方だけでテストを網羅できるようにしておく必要があり、日時や場面といった外部の状態に依存しない参照透過的な関数にしておくことが重要です。
Elmなら参照透過的な関数しか書けないため、TDDとの相性も良いです。
ReactやVueとの比較
ReactやVueはJSで書ける
React + Redux(またはhooks)+ TypeScript。
または、Vue + Vuex + TypeScript。Elmという新言語を学ぶのに比べると、慣れ親しんだJSで書けるところがメリットですよね。
でも、これらのフレームワークについて勉強していると「このオブジェクトは上書きしないようにしましょう。イミュータブル(不変)な物として扱いましょう」とか「reducer関数は引数を元に戻り値を返すだけの純粋な関数にしましょう。副作用を起こさないようにしましょう」
こんな注意事項が出てきます。
むしろReduxやVuexを触っている時の方が、関数型な考え方を意識して、気をつけてコードを書かなければいけないことが割とよくあります。
Elmなら
Elmなら、気をつけなくても参照透過的で副作用のないコードしか書けません。
なので、一度Elmをやることで「再代入」や「副作用を生むコードの書き方」を一度忘れてしまってからReactやVueに戻るのもいいかもしれません。
Elmはフレームワークだけでなく言語自体から作っているので、色々コードを書かなくてもReact + Redux + TypeScript相当の機能をもともと内蔵していて、難しい設定もなく気軽に触ってみることができます。
しかも、初見で機能追加できてしまうくらいシンプルです。
最後に
Elmは難しいどころか、とても入門しやすく、楽な言語です。
ザコーダーの私でも、少し勉強したらピンポンゲームやシングルページアプリケーションを作ることができました。
まだまだ紹介しきれていない魅力がたくさんあるので、ぜひ公式ガイド(日本語版)を読んで、学んでみてください。
(翻訳の質が良くて、とても読みやすいです)
ありがとうございました!
- 投稿日:2019-08-16T16:41:18+09:00
Elm勉強会@弊社
Elmとは
- フロントエンド開発に特化したプログラミング言語
- JavaScriptにコンパイルされて実行される、いわゆるAltJS
- 2012年に登場した、比較的若い言語
- 静的型付言語&純粋関数型言語
- 同じく純粋関数型言語であるHaskellの子孫 (ElmのコンパイラはHaskellで書かれています)
Elmはフレームワーク内蔵言語
React + Redux + TypeScript、またはVue + Vuex + TypeScript相当の機能を内蔵しているので、フレームワーク内蔵言語とも言われています。
ReactやVueの場合、新しい言語を学ばずとも慣れ親しんだJavaScriptで記述できるのがメリットですが、Elmの場合は言語自体を新しく作っているので、例えば「ReduxとReactを導入して繋ぐためのコード」等を書かなくて済むので、本質の部分だけが書かれたシンプルなコードになりやすいというメリットがあります。
JavaScriptとの違い
変数について 〜JavaScriptの場合〜
JavaScriptでは、
varかletで宣言された変数には後から別の値を再代入することが出来ますが、constで宣言された定数には再代入ができません。JavaScriptvar a = 1; a = 2 let b = 1; b = 2; const c = 1; c = 2; // エラー!
変数について 〜Elmの場合〜
Elmの場合は
varもletもconstもありません。一度宣言したら再代入ができないため、全ての値がイミュータブル(不変)です。Elma = 1 a = 2 -- コンパイルエラー!そもそも代入や再代入という概念が存在せず、変数と呼ぶことも少ないようです。「aという値を定義する」とか「aという名前で値を束縛する」と表現します。
オブジェクト 〜JavaScriptの場合〜
constで宣言した場合でも、オブジェクトのプロパティを上書きすることはできます。JavaScriptconst takashi = { name: "たかし", age: 36, }; takahsi.age = 37; // ageプロパティ上書き。
レコード 〜Elmの場合〜
Elmにはオブジェクトはありませんが、オブジェクトに似たレコードというものがあります。
Elmtakashi = { name = "たかし" , age = 36 } newTakashi = { takashi | age = 37 }レコードも完全にイミュータブル(不変)なため、ageだけ上書きすることはできません。
一部を変えた新しいレコードを作り出します。
場面や状態の変化を直接コードで書けない
再代入できないからです!
Elmseason = "夏" -- ずーっと夏 takashi_age = 36 -- ずーっと36歳
関数 〜JavaScriptの場合〜
JavaScriptでは↓こんな感じで関数を宣言したり、または変数なり定数に格納したりしますね。
JavaScriptfunction add (a, b) { return a + b; }または
JavaScriptconst add = function (a, b) { return a + b; }アロー関数式で書くと
JavaScriptconst add = (a, b) => a + b;実行するときは
JavaScriptconst result = add(3, 5);
関数 〜Elmの場合〜
Elmでは↓こんな感じで関数を定義します。
Elmadd a b = a + b実行するときは
Elmresult = add 3 5Elmではカッコもカンマも必要ありません。
戻り値を返すのにreturnを書く必要もありません。関数と変数の境目があまりない感じです。
引数があれば関数です。
書く順序による影響 〜JavaScriptの場合〜
↓エラーにならないパターン
JavaScriptconst a = 3; const b = 5; const c = a + b;↓エラーになるパターン
JavaScriptconst c = a + b; const a = 3; const b = 5;
aやbに値を代入するより上の行で、aやbを使った計算などをしようとするとエラーになります。
書く順序による影響 〜Elmの場合〜
Elmc = a + b a = 3 b = 5↑
aやbを定義するより上の行でaやbを計算に使うことができます。
JavaScriptでは状態の変化を直接コードに書くことができる
JavaScriptlet takashi_age = 36; console.log(takashi_age); // 36 takashi_age = 37; console.log(takashi_age); // 37 takashi_age = 38; console.log(takashi_age); // 38 takashi_age = 39; console.log(takashi_age); // 39JavaScriptの場合は変数の値を変更できるので、
takashi_age = 〇〇;の上にあるか下にあるかが重要です。
Elmでは状態変化を直接コードで表現できない
再代入ができないため「この値を、この行で呼び出したら
36だけど、もう少し下の行で呼び出したら37だった」ということがありません。
再代入ができない=コードの中で状態が変わらない=時間が止まっているようなものなので、コードの中に前とか後とかが無いイメージです。
一瞬の中に全てのコードが書かれているような、スタープラチナ・ザ・ワールドみたいな感じです。
そのため、どの行でその値を呼び出すかは結果に関係なく、その値を宣言するより上の行でも呼び出すことができます。
- 違うものには違う名前がつくので明解
- この行ではこの変数は何の値を参照しているか?と考えなくていい。どの行でも同じ。
- 値の中身がコロコロ変わらないので、いちいちconsole.log()等で確認する手間が減る
- 値の中身がコロコロ変わらないので、GitHubでソースを読むときも少し楽
関数の返す値も(引数が同じならば)常に一定
それは、全ての値やレコードが不変で、場面や状態といったものを直接コードで書くことができないからです。
Elmscene = "朝" greeting name = if scene == "朝" then name ++ "さん、おはよう!" else name ++ "さん、こんにちわ!"再代入できないため
sceneは常に"朝"です。そのためgreeting関数が返す文字列は、常に"〇〇さん、おはよう!"となります。
コードの中で何回この関数が呼ばれたとしても、外部の変数の状態によって戻り値が変わったりすることはありません。
なぜなら、外部の変数の状態が不変だからです。
同じ引数を受け取った場合、同じ戻り値を返します。
この性質を参照透過性と言います。
Elmは何も変えられない・・・?
Elmには再代入という概念はありません。全ての値は不変です。
そのため、関数の中に「何か外部の値を変えてくれい」という命令的なコードを書くこともできません。
関数は、引数を受け取って、それを元に戻り値を返すことしかできません。
つまり「〇〇は△△である」と言う定義しか書けません。具体的には、JavaScriptでいう、
JavaScriptdoSomething(a, b);的な「戻り値を保存しない」コードは書けません。
doSomething関数の中から外部のものを変えるすべが無いため、戻り値を変数なり定数に格納しないと意味がないのです。
唯一外の世界に影響を与えられるのは戻り値だけですので。
実行だけして変数に格納しない、というコードを書くことはありません。JavaScriptconst result = someCalculate(a, b);上記のような「関数で何らかの処理をして、変数に格納する」つまり「〇〇は△△である」的なパターンしかありません。
何も変えられなくて、この言語なにができるの?
定義を並べるだけで、命令が書けなくて、コンピュータに何か仕事をさせることができるのでしょうか。
しかし「Webサイトを作る」ということは「ユーザがどんな行動をしたら、Webサイトの持つデータや見た目はどのように変わるべきか」という「定義をしている」とも言えるので、定義さえすればWebアプリを作成することができます。
デモアプリをみてみましょう
Elm年齢カウンター
ブラウザエディタEllieでご確認ください。
定義だけで状態変化を表現できました
しかも、React + Redux(またはhooks)+ TypeScriptよりだいぶコードは少なめです。
オブジェクト指向との違い
オブジェクト指向とは
データと関数(メソッド)をまとめて定義した「クラス」つまり種を定義して、そこからオブジェクトを生成する。
人間が現実世界を認識するときの考え方に近いため、比較的直感的にプログラミングできるという、非常に強力なスタイル。※諸説ありますが、ここでは上記の認識で進めます。
クラスを使って書いてみる
JavaScriptclass Human { constructor (name, age) { this.name = name; this.age = age; } increment () { this.age++; } decrement () { this.age--; } } const takashi = new Human("たかし", 36); takashi.increment(); takashi.increment(); console.log(takashi.age); // 38コード的にも関連するデータと関数がクラスの中にまとまって書かれているので「誰のための変数や関数なのか」という所属が一目瞭然です。
Elmでも、オブジェクト指向っぽくも捉えられる
ElmにはClass構文は存在しません。データ(Model)と関数も特にまとまっていませんでしたね。
データと関数が別れてはいますが、例えば「この関数は、Int型とHuman型の引数を受け取って、Human型の値を返す」ということを、型によって定義できます。
Classのように「誰が持っている関数か」という所属は定義できませんが「誰に何をするための関数か」を型によって定義できます。
そのため、言語仕様はかなり違いますが、割とオブジェクト指向な気分でも書けます。
コンポーネント指向との違いは?
divとかbuttonなどのhtml系の関数を使って「任意の情報(引数)を受け取ってhtml要素を返す関数」を書けます。
これってReactやVueのFunctional Componentですよね。
The Elm Architectureについて
ReduxやVuexの元となった手法
model, view, updateの3つを使った先ほどのパターンをThe Elm Architectureと呼びます。
コード上の全てが不変なため、状態変化を直接コード上で書くことはできませんが、それでも状態変化を表現できるちょっと不思議なやり方です。
ReduxもVuexも、このThe Elm Architectureの影響を受けて作られました。
再代入ができないので、状態変化を表現したい値はModelに組み込んで管理します。
そしてupdate関数で「どんなメッセージが来たら、状態(model)はどんな風に変わるべきか」を定義します。
modelが新しくなるとviewが再描画されます。
そうすることで、命令を書けなくとも、定義だけで状態の変更を表現できました。もともと言語に内蔵されている機能なので「それを使うための準備のコード」がほぼ無く、非常にシンプルです。
そのため、読みやすく書きやすいです。
型システムが優秀なため、修正にも強い
例えば先ほどの年齢カウンターにリセット機能を追加してみましょう。
リセット機能追加の流れ
- リセットボタンをview関数の中に追加
- そのボタンをクリックしたら
Resetというメッセージが発生するように属性を追加Resetなんて知らないよ!とエラーが出る- メッセージの型に
Resetを追加Resetのケースも書かないと!と導いてくれるエラーメッセージが分かりやすいのもElmの特徴です。
そしてこのように、エラーになるようなコードは事前に検査して発見してくれるため、実行時エラーを目にすることは実質ありません。
純粋関数型言語の特徴
副作用を直接書けない
再代入ができないため、
「いくつか関数を実行したら、知らん間にxの値が変わっとった!!!」
みたいな副作用が起こりません。むしろ起こせません。
何かを変える処理はupdate関数の中で集中管理します。
参照透過性が担保されている
関数は同じ引数なら同じ戻り値を返す(外部の値に依存して振る舞いを変えない)ので、単体テスト・自動テストがしやすいです。
逆に、外部に依存しまくっている関数はテストがしづらくてしょうがないです。例えば期限付きのタスク管理システム
のテストをする場合に・・・
- タスクを作成する前に、日時操作をするための専用管理画面に入り、今が何月何日なのか設定する。
- その後Webサイト側で新規タスクを作る(期限日も設定)
- 時間操作の画面で日にちを進める。
- そうするとタスクのステータスがから「期限間近」や「期限を過ぎています」等に変わる。
- ステータスに応じた表示や挙動になっているかどうか、ようやくテストが可能に・・・。
なんてことになりますね。
「周りの状態───つまり日時───によって挙動を変える」関数でなく「日時等の条件はすべて引数として受け取り、引数によってのみ戻り値の結果が決まる」関数・・・つまり参照透過的な関数にすれば、テストが書きやすくなる。
TDDにも向いています。
テストをゴリゴリに書いておけば、修正フェーズや追加開発フェーズで「ここのコードを修正したことで逆に別のところがバグってもうた!」的なことをやらかしてしまった場合でも、テストでエラーが出て気づけるので安心ですよね。
ReactやVueとの比較
ReactやVueはJSで書ける
React + Redux(またはhooks)+ TypeScript。
または、Vue + Vuex + TypeScript。Elmという新言語を学ぶのに比べると、慣れ親しんだJSで書けるところがメリットですよね。
でも、これらのフレームワークについて勉強していると「このオブジェクトは上書きしないようにしましょう。イミュータブル(不変)な物として扱いましょう」とか「reducer関数は引数を元に戻り値を返すだけの純粋な関数にしましょう。副作用を起こさないようにしましょう」
こんな注意事項が出てきます。
むしろReduxやVuexを触っている時の方が、関数型な考え方を意識して、気をつけてコードを書かなければいけないことが割とよくあります。
Elmなら
Elmなら、気をつけなくても参照透過的で副作用のないコードしか書けません。
なので、一度Elmをやることで「再代入」や「副作用を生むコードの書き方」を一度忘れてしまってからReactやVueに戻るのもいいかもしれません。
Elmはフレームワークだけでなく言語自体から作っているので、色々コードを書かなくてもReact + Redux+ TypeScript相当の機能をもともと内蔵していて、難しい設定もなく気軽に触ってみることができます。
しかも、初見で機能追加できてしまうくらいシンプルです。
最後に
Elmは難しいどころか、とても入門しやすく、楽な言語です。
ザコーダーの私でも、少し勉強したらピンポンゲームやシングルアプリケーションを作ることができました。
まだまだ紹介しきれていない魅力がたくさんあるので、ぜひ公式ガイド(日本語版)を読んで、学んでみてください。
(翻訳の質が良くて、とても読みやすいです)
ありがとうございました!
- 投稿日:2019-08-16T16:41:18+09:00
Elm勉強会@弊社(8/28)
Elmとは
- フロントエンド開発に特化したプログラミング言語
- JavaScriptにコンパイルされて実行される、いわゆるAltJS
- 2012年に登場した、比較的若い言語
- 静的型付言語&純粋関数型言語
- 同じく純粋関数型言語であるHaskellの子孫 (ElmのコンパイラはHaskellで書かれています)
Elmはフレームワーク内蔵言語
React + Redux + TypeScript、またはVue + Vuex + TypeScript相当の機能を内蔵しているので、フレームワーク内蔵言語とも言われています。
ReactやVueの場合、新しい言語を学ばずとも慣れ親しんだJavaScriptで記述できるのがメリットですが、Elmの場合は言語自体を新しく作っているので、例えば「ReduxとReactを導入して繋ぐためのコード」等を書かなくて済みます。そのため、本質の部分だけが書かれたシンプルなコードになりやすいというメリットがあります。
JavaScriptとの違い
変数について 〜JavaScriptの場合〜
JavaScriptでは、
varかletで宣言された変数には後から別の値を再代入することが出来ますが、constで宣言された定数には再代入ができません。JavaScriptvar a = 1; a = 2 let b = 1; b = 2; const c = 1; c = 2; // エラー!
変数について 〜Elmの場合〜
Elmの場合は
varもletもconstもありません。一度宣言したら再代入ができないため、全ての値がイミュータブル(不変)です。Elma = 1 a = 2 -- コンパイルエラー!そもそも代入や再代入という概念が存在せず、変数と呼ぶことも少ないようです。「aという値を定義する」とか「aという名前で値を束縛する」と表現します。
オブジェクト 〜JavaScriptの場合〜
constで宣言した場合でも、オブジェクトのプロパティを上書きすることはできます。JavaScriptconst takashi = { name: "たかし", age: 36, }; takahsi.age = 37; // ageプロパティ上書き。
レコード 〜Elmの場合〜
Elmにはオブジェクトはありませんが、オブジェクトに似たレコードというものがあります。
Elmtakashi = { name = "たかし" , age = 36 } newTakashi = { takashi | age = 37 }レコードは、JSのオブジェクトと異なり完全にイミュータブル(不変)なため、ageだけ上書きするということもできません。
上記のコードも、一部を変えた新しいレコードを作り出しています。
元のレコード(takashi)は36歳のままです。
書く順序による影響 〜JavaScriptの場合〜
↓エラーにならないパターン
JavaScriptconst a = 3; const b = 5; const c = a + b;↓エラーになるパターン
JavaScriptconst c = a + b; const a = 3; const b = 5;
aやbに値を代入するより上の行で、aやbを使った計算などをしようとするとエラーになります。
書く順序による影響 〜Elmの場合〜
Elmc = a + b a = 3 b = 5↑
aやbを定義するより上の行でaやbを計算に使うことができます。
JavaScriptでは状態の変化を直接コードに書くことができる
JavaScriptlet takashi_age = 36; console.log(takashi_age); // 36 takashi_age = 37; console.log(takashi_age); // 37 takashi_age = 38; console.log(takashi_age); // 38 takashi_age = 39; console.log(takashi_age); // 39JavaScriptの場合は変数の値を変更できるので、例えば
console.logをtakashi_age = 〇〇;の上の行に書くか下の行に書くかが重要です。
Elmでは、場面や状態の変化を直接コードで書けない
再代入という概念がなく、コード内の全ての値が不変だからです。
Elmseason = "夏" -- ずーっと夏 age = 36 -- ずーっと36歳そのため「この行でageを呼び出したら
36だけど、もう少し下の行で呼び出したら37だった」ということがありません。再代入ができない=コードの中で状態が変わらない=時間が止まっているようなものなので、コードの中に前とか後とかが無いイメージです。
一瞬の中に全てのコードが書かれているような、スタープラチナ・ザ・ワールドみたいな感じです。
つまり、どの行でその値を呼び出すかは結果に関係なく、その値を宣言するより上の行で呼び出すことも可能です。
再代入できないことによるメリット
- 違うものには違う名前がつくので明解
- この行ではこの変数は何の値を参照しているか?と考えなくていい。どの行でも同じ。
- 値の中身がコロコロ変わらないので、いちいちconsole.log()等で確認する手間が減る
- 値の中身がコロコロ変わらないので、GitHubでソースを読むときも少し楽
関数 〜JavaScriptの場合〜
JavaScriptでは↓こんな感じで関数を宣言したり、または変数なり定数に格納したりしますね。
JavaScriptfunction add (a, b) { return a + b; }または
JavaScriptconst add = function (a, b) { return a + b; }アロー関数式で書くと
JavaScriptconst add = (a, b) => a + b;実行するときは
JavaScriptconst result = add(3, 5);
関数 〜Elmの場合〜
Elmでは↓こんな感じで関数を定義します。
Elmadd a b = a + b実行するときは
Elmresult = add 3 5Elmではカッコもカンマも必要ありません。
戻り値を返すのにreturnを書く必要もありません。関数と変数の境目があまりない感じです。
引数があれば関数です。
関数の返す値も(引数が同じならば)常に一定
それは、全ての値やレコードが不変で、場面や状態といったものを直接コードで書くことができないからです。
Elmscene = "朝" greeting name = if scene == "朝" then name ++ "さん、おはよう!" else name ++ "さん、こんにちわ!"再代入できないため
sceneは常に"朝"です。そのためgreeting関数が返す文字列は、常に"〇〇さん、おはよう!"となります。
コードの中で何回この関数が呼ばれたとしても、外部の変数の状態によって戻り値が変わったりすることはありません。
なぜなら、外部の変数の状態が不変だからです。
同じ引数を受け取った場合、同じ戻り値を返します。
この性質を参照透過性と言います。
Elmは何も変えられない・・・?
Elmには再代入という概念はありません。全ての値は不変です。
そのため、関数の中に「何か外部の値を変えてくれい」という命令的なコードを書くこともできません。
関数は、引数を受け取って、それを元に戻り値を返すことしかできません。
つまり「〇〇は△△である」と言う定義しか書けません。具体的には、JavaScriptでいう、
JavaScriptdoSomething(a, b);的な「戻り値を保存しない」コードは書けません。
doSomething関数の中から外部のものを変えるすべが無いため、戻り値を返して変数なり定数に格納しないと意味がないのです。
関数の中から唯一外の世界に影響を与えられるのは戻り値だけですので。
実行だけして変数に格納しない、というコードを書くことはありません。JavaScriptconst result = someCalculate(a, b);上記のような「関数で何らかの処理をして、変数に格納する」つまり「〇〇は△△である」的なパターンしかありません。
何も変えられなくて、この言語なにができるの?
定義を並べるだけで、命令が書けなくて、コンピュータに何か仕事をさせることができるのでしょうか。
しかし「Webサイトを作る」ということは「ユーザがどんな行動をしたら、Webサイトの持つデータや見た目はどのように変わるべきか」という「定義をしている」とも言えるので、定義さえすればWebアプリを作成することができます。
デモアプリをみてみましょう
デモアプリ
ブラウザエディタEllieでご確認ください。
※好きにコードをいじってもらって大丈夫です!
定義だけで状態変化を表現できました
しかも、React + Redux(またはhooks)+ TypeScriptよりだいぶコードは少なめです。
オブジェクト指向との違い
オブジェクト指向とは
データと関数(メソッド)をまとめて定義した「クラス」つまり種を定義して、そこから実体となるオブジェクトを生成する。
人間が現実世界を認識するときの考え方に近いため、比較的直感的にプログラミングできるという、非常に強力なスタイル。※諸説ありますが、ここでは上記の認識で進めます。
クラスを使って書いてみる
JavaScriptclass Human { constructor (name, age) { this.name = name; this.age = age; } increment () { this.age++; } decrement () { this.age--; } } const takashi = new Human("たかし", 36); takashi.increment(); takashi.increment(); console.log(takashi.age); // 38関連するデータと関数がクラスの中にまとまって書かれているので「誰のための変数や関数なのか」という所属が一目瞭然なコードになります。
Elmでも、オブジェクト指向っぽく考えることもできる
ElmにはClass構文は存在しません。データ(Model)と関数も特にまとまっていませんでしたね。
データと関数が別れてはいますが、例えば「この関数は、Int型とHuman型の引数を受け取って、Human型の値を返す」ということを、型によって定義できます。
Classのように「誰が持っている関数か」という所属は定義できませんが「誰に何をするための関数か」を型によって定義できます。
そのため、言語仕様はかなり違いますが、割とオブジェクト指向な気分でも書けます。
コンポーネント指向との違いは?
divやbuttonなどのhtml系の関数を使って「任意の情報(引数)を受け取ってhtml要素を返す関数」を書けます。
これってReactやVueのFunctional Componentですよね。
The Elm Architectureについて
ReduxやVuexの元となった手法
model, view, updateの3つを使った先ほどのパターンをThe Elm Architectureと呼びます。
コード上の全てが不変なため、状態変化を直接コード上で書くことはできませんが、それでも状態変化を表現できるちょっと不思議なやり方です。
ReduxもVuexも、このThe Elm Architectureの影響を受けて作られました。
再代入ができないので、状態変化を表現したい値はModelに組み込んで管理します。
そしてupdate関数で「どんなメッセージが来たら、状態(model)はどんな風に変わるべきか」を定義します。
modelが新しくなるとviewが再描画されます。
そうすることで、命令を書けなくとも、定義だけで状態の変更を表現できました。もともと言語に内蔵されている機能なので「それを使うための準備のコード」がほぼ無く、非常にシンプルです。
そのため、読みやすく書きやすいです。
型システムが優秀なため、修正にも強い
例えば先ほどの年齢カウンターにリセット機能を追加してみましょう。
リセット機能追加の流れ
- リセットボタンをview関数の中に追加
- そのボタンをクリックしたら
Resetというメッセージが発生するように属性を追加Resetなんて知らないよ!とエラーが出る- メッセージの型に
Resetを追加Resetのケースも書かないと!と導いてくれるエラーメッセージが分かりやすいのもElmの特徴です。
そしてこのように、エラーになるようなコードは事前に検査して発見してくれるため、実行時エラーを目にすることは実質ありません。
純粋関数型言語の特徴
副作用を直接書けない
再代入ができないため、
「いくつか関数を実行したら、知らん間にxの値が変わっとった!!!」
みたいな副作用が起こりません。むしろ起こせません。そのため、状態変化を表現したい値は
modelに組み込まなければなりません。また、状態を更新する処理はupdate関数の中に書かなければいけません。この縛りは一見窮屈なようですが、ルールが明確化されていることで、結果的にそれぞれの処理を探しやすくなるというメリットがあります。
参照透過性が担保されている
Elmの関数は、外部の値に依存して振る舞いを変えません。引数が同じであれば、同じ戻り値を返します。
それにより、単体テスト・自動テストがしやすくなります。
逆に、外部に依存しまくっている関数はテストがしづらくてしょうがないです。
例えば、期限付きのタスク管理システム
↑のテストをする場合のことを考えてみます。
- タスクを作成する前に、日時操作をするための専用管理画面に入り、今が何月何日なのか設定する。
- その後Webサイト側で新規タスクを作る(期限日も設定)
- 時間操作の画面で日にちを進める。
- そうするとタスクのステータスがから「期限間近」や「期限を過ぎています」等に変わる。
- ステータスに応じた表示や挙動になっているかどうか、ようやくテストが可能に・・・。
これだとテストしづらいですよね。
条件は全て引数として渡す
「周りの状態───つまり日時───によって挙動を変える」関数でなく「日時等の条件はすべて引数として受け取り、引数によってのみ戻り値の結果が決まる」関数・・・つまり参照透過的な関数にすれば、自動テストが書きやすくなります。
テストの自動化
例えばJest等のテストフレームワークで、いくつも引数を変えながら関数の自動テストをするようにテストコードを書いてきます。
gitコミットをするたびにそのテストが走るように設定しておきます。そうしておけば、修正フェーズや追加開発フェーズで「ここのコードを修正したことで逆に別のところがバグってもうた!」的なことをやらかしてしまった場合でも、すぐにテストでエラーが出て気づけるので安心ですよね。
そのためには、引数の渡し方だけでテストを網羅できるようにしておく必要があり、日時や場面といった外部の状態に依存しない参照透過的な関数にしておくことが重要です。
Elmなら参照透過的な関数しか書けないため、TDDとの相性も良いです。
ReactやVueとの比較
ReactやVueはJSで書ける
React + Redux(またはhooks)+ TypeScript。
または、Vue + Vuex + TypeScript。Elmという新言語を学ぶのに比べると、慣れ親しんだJSで書けるところがメリットですよね。
でも、これらのフレームワークについて勉強していると「このオブジェクトは上書きしないようにしましょう。イミュータブル(不変)な物として扱いましょう」とか「reducer関数は引数を元に戻り値を返すだけの純粋な関数にしましょう。副作用を起こさないようにしましょう」
こんな注意事項が出てきます。
むしろReduxやVuexを触っている時の方が、関数型な考え方を意識して、気をつけてコードを書かなければいけないことが割とよくあります。
Elmなら
Elmなら、気をつけなくても参照透過的で副作用のないコードしか書けません。
なので、一度Elmをやることで「再代入」や「副作用を生むコードの書き方」を一度忘れてしまってからReactやVueに戻るのもいいかもしれません。
Elmはフレームワークだけでなく言語自体から作っているので、色々コードを書かなくてもReact + Redux+ TypeScript相当の機能をもともと内蔵していて、難しい設定もなく気軽に触ってみることができます。
しかも、初見で機能追加できてしまうくらいシンプルです。
最後に
Elmは難しいどころか、とても入門しやすく、楽な言語です。
ザコーダーの私でも、少し勉強したらピンポンゲームやシングルアプリケーションを作ることができました。
まだまだ紹介しきれていない魅力がたくさんあるので、ぜひ公式ガイド(日本語版)を読んで、学んでみてください。
(翻訳の質が良くて、とても読みやすいです)
ありがとうございました!
- 投稿日:2019-08-16T15:59:39+09:00
日経競馬をスクレイピングしてDBを構築しよう
はじめに
あっというまに夏休みが終わるね(-.-)
なんにもやってない夏休みはつまらないので、Webからデータを作るスクレイピング入門みたいなのやります。
日経競馬からデータを持ってきて、Microsoft Accessへデータをロードするまでの簡単なサンプルを説明します。スクレイピングやったことない人のために。ガンガン応用してください。
盛夏前提
- OS : Windows7以上
- PoweShellのターミナルで実行
- VSCodeでコード編集
- node.js環境構築済み
- Microsoft Accessは2010以上
アプリ構成
№ アプリ 機能 1 jockey_leading_get.js リーディング騎手データのスクレイピング 2 jockey_leading_tocsv.js リーディング騎手データのCSVファイル変換 3 jockey_leading_table.js リーディング騎手データのAccessテーブル作成 4 jockey_leading_load.js リーディング騎手データのcsvのテーブルへロード jockey_leading_get.js
var request = require('request'); var Iconv = require('iconv').Iconv; var fs = require('fs'); const file = ".\\jockey_leading.html"; var options = { url: 'https://db.netkeiba.com/?pid=jockey_leading', encoding: null, method: 'GET' } /** * AppendFile * @param {String} path * @param {String} data */ function AppendFile(path, data) { fs.appendFile(path, data, function (err) { if (err) { throw err; } }) } request(options, function (error, response, body) { if (!error && response.statusCode == 200) { var iconv = new Iconv('EUC-JP', 'UTF-8//TRANSLIT//IGNORE'); body = iconv.convert(body).toString(); AppendFile(file,body); } })説明
- リーディング騎手データを「jockey_leading.html」として保存。
- 日経競馬のページがEUC-JPのため、npm iconvをインストールしてUTF-8にコード変換。
iconv install
npm install iconv --savenode-gypのrebuildで失敗したら、ここを参照。
実行
node jockey_leading_get.js結果
PS C:\Users\~> ls jockey_leading.html ディレクトリ: C:\Users\~ Mode LastWriteTime Length Name ---- ------------- ------ ---- -a--- 2019/08/15 15:22 97440 jockey_leading.htmljockey_leading_tocsv.js
/** * @param {String} html */ function htmltocsv(html){ const XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest; const http = new XMLHttpRequest(); const file = ".\\jockey_leading.csv"; UnlinkFile(file); http.open("GET", html, true); http.responseType = "document"; http.send(null); var csv = []; http.onreadystatechange = function() { if(http.readyState == 4 && http.status==200) { var data = http.responseText; var lines = data.split( '\n' ); var sw = 0; var body = 0; var output = ''; // header const head1 = '順位,騎手名,所属,生年月日,1着,2着,3着,着外,重賞,,特別,,平場,,芝,,ダート,,勝率,連対率,複勝率,収得賞金(万円),代表馬,順位変動' + '\n'; const head2 = ',,,,,,,,出走,勝利,出走,勝利,出走,勝利,出走,勝利,出走,勝利,,,,,' + '\n'; csv.push(head1); csv.push(head2); for ( var i = 0; i < lines.length; i++ ) { // 空行は無視する if ( lines[i] == '' ) { continue; } // <table 抽出 if ( sw == 0 & lines[i].substr(0,6) == '<table' ) { sw = -1; } if ( sw == -1 ) { // body start keyword if (lines[i].substr(0,11) == '<td nowrap>') { body = -1; } if ( body == -1 ) { if ( lines[i] == '</tr><tr>' ) { csv.push(output.substr(0, output.length - 1) + '\n'); var output = ''; } else { output = output + '"' + body_htmltocsv(lines[i]) + '",'; } } } if ( sw == -1 & lines[i].substr(0,8) == '</table>' ) { sw = 0; } } } for ( var i = 0; i < csv.length; i++) { AppendFileSync(file, csv[i]); } } } /** * AppendFileSync * @param {Sting} path * @param {Sting} data */ function AppendFileSync(path, data) { const fs = require("fs"); fs.appendFileSync(path, data, function (err) { if (err) { throw err; } }) } /** * @param {String} path */ function UnlinkFile(path) { const fs = require("fs"); fs.unlink(path, function(err) { if (err) { console.log(err); } }) } /** * * @param {String} data * */ function body_htmltocsv(data) { var pos = data.indexOf("</a>"); // </a>がない if ( pos == -1 ) { var data = data.replace('<td nowrap>', ''); var data = data.replace('</td>', ''); } else { var pos = data.indexOf('<td nowrap>'); // 先頭が<td nowrap> if ( pos == 0 ) { var pos = data.indexOf('>',12); var pos2 = data.indexOf('<',13); var data = data.substr( pos + 1, pos2 - pos -1 ); } else { var pos = data.indexOf('<td class="txt_l" nowrap>'); // 先頭が<td class="txt_l" nowrap> if ( pos == 0 ) { var pos = data.indexOf('[東]'); var pos2 = data.indexOf('[西]'); // [東][西]を含む if ( pos != -1 || pos2 != -1 ) { var pos = data.indexOf('>',29); var pos2 = data.indexOf('<',30); if ( pos != -1 ) { var data = '[東]' + data.substr( pos + 1, pos2 - pos -1 ); } else { var data = '[西]' + data.substr( pos + 1, pos2 - pos -1 ); } } else { var pos = data.indexOf('>',25); var pos2 = data.indexOf('<',26); var data = data.substr( pos + 1, pos2 - pos -1 ); } } } } return data; } htmltocsv('file:' + '.\\jockey_leading.html');説明
- npm xmlhttprequestをインストール。直接htmlをfsで読み込んでもbody_htmltocsv関数は動作可能ですが、あえて将来httpからダイレクトにcsv化をもくろんで。
- ヘッダー2行をスキップしてもいいかも、後続の「jockey_leading_load.js」ではヘッダー2行をスキップしてテーブルにロード。
- http.responseTextを'\n'でsplitして1行ずつ処理します。
- <table ~</table>を処理
- <td nowrap>~</tr>を処理
- jockey_leading.htmlのテーブルを解析して、各要素をbody_htmltocsv関数で取出し配列(csv[])に格納。
- 出力ファイル「jockey_leading.csv」
- 出力する前に「jockey_leading.csv」は削除、ファイル有無は判定しない。無くても続行。
- cvsの書き出しは同期処理にしなければ、並びがバラバラになるので、必ずappendFileSyncで書出します。
xmlhttprequest install
npm install xmlhttprequest -save実行
node jockey_leading_tocsv.js結果
PS C:\Users\~> ls jockey_leading.csv ディレクトリ: C:\Users\~ Mode LastWriteTime Length Name ---- ------------- ------ ---- -a--- 2019/08/15 15:23 9331 jockey_leading.csvjockey_leading_table.js
const check_jockey = "SELECT Name FROM MSysObjects WHERE Name = 'jockey';" const create_table_jockey = "CREATE TABLE jockey (" + "順位 INT NOT NULL," + "騎手名 char(20) NOT NULL," + "所属 char(26)," + "生年月日 DATE," + "1着 INT," + "2着 INT," + "3着 INT," + "着外 INT," + "重賞出走 INT," + "重賞勝利 INT," + "特別出走 INT," + "特別勝利 INT," + "平場出走 INT," + "平場勝利 INT," + "芝出走 INT," + "芝勝利 INT," + "ダート出走 INT," + "ダート勝利 INT," + "勝率 DOUBLE," + "連対率 DOUBLE," + "複勝率 DOUBLE," + "収得賞金 DOUBLE," + "代表馬 char(20)," + "順位変動 char(4)" + ");" const create_index_jockey = "CREATE INDEX idxID ON jockey (順位 ASC);" const ADODB = require('node-adodb'); const connection = ADODB.open('Provider=Microsoft.ACE.OLEDB.12.0;Data Source=jra2019.accdb;'); // Table Exist Check connection .query(check_jockey) .then(data => { // Create Table if (data.length == 0) { connection .execute(create_table_jockey) .then(data => { // Create Index connection .execute(create_index_jockey) .then(data => { }) .catch(error => { console.log(error); }); }) .catch(error => { console.log(error); }); } }) .catch(error => { console.log(error); });説明
- ADODBでDB操作するので、node-adodbをインストールします。
- Microsoft Access のテーブル格納は必ずしもロード順にならないので、インデックスを設定します。
- AccessのMSysObjectsを読取、「jockey」テーブルの存在チェックを行います。
- 実行するまえに、「jra2019.accdb」を作成します。
MSysObjectsへ読みとり権限を付与する
- Microsoft Accessで「jra2019.accdb」を作成。
- 「jra2019.accdb」を開く。
- Ctl+gで「Microsoft Visual Basic for Application」を開く。
- イミディトウィンドウに以下のコマンド
?currentuser() Admin CurrentProject.Connection.Execute "GRANT SELECT ON MSysObjects TO Admin;"
- currentuserでaccdbのユーザを確認して、MSsysObjectsにアクセス権限を付与します。
- これを行わないと、外部からMSsysObjectsを読み取れません。
- Microsoft Accessが旧mdbの場合 "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=xxxxx.mdb;"
node-adodb install
npm install node-adodb --save実行
node jockey_leading_table.js結果
下記の通り、テーブルが作成されればOKです。
jockey_leading_load.js
async function loadcsv(table, data) { const ADODB = require('node-adodb'); const connection = ADODB.open('Provider=Microsoft.ACE.OLEDB.12.0;Data Source=jra2019.accdb;'); try { await connection.execute('INSERT INTO ' + table + ' VALUES(' + data + ');'); } catch (error) { console.error(error); } } function jockeyload() { const fs = require('fs'); const file = ".\\jockey_leading.csv"; var data = new String(); data = fs.readFileSync(file, 'utf8'); var lines = data.split( '\n' ); for ( var i = 0; i < lines.length - 1; i++ ) { if (i < 2) { continue; } loadcsv('jockey', lines[i]); } } jockeyload();説明
- node-adodbでCSVをテーブルにインサートします。
- loadcsvは同期処理(async~await)にします。これをやらないと、csvがバラバラに格納されます。
- jockey_leading.csvを同期(readFileSync)で読取、'\n'でsplitします。
- splitしたデータをロードします。
実行
node jockey_leading_load.js結果
下記のエラーコードは無視してください。
process: { code: -2147467259, message: 'マシン \'xxxxxxx\' のユーザー \'Admin\' がデータベースを開けない状態、またはロックできない状態にしています。' }, exitCode: 0 }jockey
まとめ
スクレイピングをnode.jsとJavaScriptで構築する入門をやってみました。
いわずもがな、初学者向けです。不明点が多々あると思います。
俺だったこうやる的なご意見もありそうです。
とりあえず、目的は達成しています。
AccessをMySQLに置き換えるとか、コードはALLフリーなので色々改造して、さらなるスクレイピングワールドへ。
- 投稿日:2019-08-16T15:20:27+09:00
CircleCIでeslint-disable-lineを検出する
ESLint
JavaScriptのプロジェクトを複数人(もちろん個人でも)で開発する際は、ESLintを使うことでコーディングルールを統一することができます。
またESLintをCI上でも実行することでレビューの前段階としてミスなど発見がしやすくなります。eslint-disable-line
ESLintではインラインコメントでルールを無効にすることができます。
https://eslint.org/docs/user-guide/configuring#disabling-rules-with-inline-commentsローカルで開発する際はちょっと確認のために
eslint-disable-lineを使ってあれこれすることもあるかと思いますが、これを残したままコードをプッシュしてしまうと意図せずコーディングルールにそぐわないコードが紛れてしまう可能性がでてきます。例えば
consoleを禁止するルールがあっても、以下のようなコードはESLintでは検出できません。
(もしできる方法があったら教えてほしい)console.log('hoge') // eslint-disable-lineCircleCIで検出する
eslint-disable-lineが使われていないかチェックするため、CirleCIで以下のコマンドを実行します。- run: name: Search eslint-disable-line command: | git grep --heading --break -n -e 'eslint-disable-line' -- '*.js' '*.vue' || status=$? if [ -z "$status" ]; then exit 1; figit grep --heading --break -n -e 'eslint-disable-line' -- '*.js' '*.vue'これはjs(この例ではvueも、他のパターンも指定可能)に
eslint-disable-lineがないか探すコマンドです、オプションはお好みで。
https://git-scm.com/docs/git-grep後ろに続く
|| status=$?について、上記のgit-grepコマンドは該当する行があった場合はステータスが0となりますが、該当する行が無かった場合はステータスが1となります。つまり該当する行があった場合は
$statusには値がセットされないので、値がセットされていなければexit 1するというのが以下のコマンドです。if [ -z "$status" ]; then exit 1; fiこれでCircleCI上で
eslint-disable-lineを検出することができるようになりました。
- 投稿日:2019-08-16T14:35:15+09:00
[kintone]サブテーブルを行ごとに非活性にする
完成物
以前コミュニティで投稿したやつ
チェックボックスで判定
出来るだけフィールドコードを追加せずにしたかったのでめんどくさい感じになった。
いずれ使いそうなのでメモsample.js(function() { "use strict"; const targetEvents = function(fieldCode) { let arr = []; fieldCode.map(function(cur) { arr.push("app.record.create.change." + cur); arr.push("app.record.edit.change." + cur); }); return arr; }; /***************************************************************************/ // チェックボックスのフィールドコード const events = ["チェック_1", "チェック_2", "チェック_3"]; /***************************************************************************/ kintone.events.on(targetEvents(events), function(event) { const row = event.changes.row.value; const str = String(event.type).match(/(?<=change.)(.*)/)[0]; for (let item in row) { row[item].disabled = false; if (row[str].value.length > 0) { if (item !== str) { row[item].disabled = true; } } } return event; }); kintone.events.on("app.record.edit.show", function(event) { const record = event.record; /***************************************************************************/ // 対象のサブテーブルとチェックボックスのフィールドコードを追加 const targetTable = { Table_1: { value: "チェック_1" }, Table_2: { value: "チェック_2" }, Table_3: { value: "チェック_3" } }; /***************************************************************************/ const tables = Object.keys(targetTable).map(function(cur) { return { [cur]: record[cur].value }; }); for (let item in tables) { Object.values(tables[item]).forEach(function(val) { Object.values(val).forEach(function(val2, index) { let v = Object.values(targetTable)[item].value; let r = Object.keys(targetTable)[item]; if (val2.value[v].value.length > 0) { Object.keys(val2.value).map(function(cur) { record[r].value[index].value[cur].disabled = true; }); record[r].value[index].value[v].disabled = false; } }); }); } return event; }); })();
- 投稿日:2019-08-16T14:21:34+09:00
フリーWebSocketサーバーをAchexからWebsocket.inへ変更
手元にサーバー不要でWebSocket Client間で通信できるフリーサーバー Achex, Websocket.in が便利そう - Qiita
を調査後、Achexを利用していたが、しらばくしてからしばしば接続がcloseされるようになった。デモページ
でも動作が怪しいため、自動再接続を組むよりは乗り換えることにした。
今確認すると好調のようで一時的な不調だったとも思われるが、まる一日は不調だっため書き直してしまった。というわけで
WebSocket.in - The open and free WebSocket server for all.
との乗り換えというか比較になりますが、
WebSocket.inはかなり機能が貧弱。
基本的には立てたインスタンス全体へのstringエコーしか存在しません。
せめてもの機能としてAchexでのHubのようなルームがGETパラメーターとして付加できます。
wss://connect.websocket.in/YOUR_CHANNEL_ID?room_id=YOUR_ROOM_ID
- 単機能でAchexより柔軟性がなく
- Achexよりさらに盗聴されやすく
- 指定ユーザーにWhisperできないため接続全体に受信負荷がかかる
と、Achexにくらべ優位性はあまりありませんでした。
Achexが以後も安定を保つならばこちらのほうがよさそうです。なお、Websocket.in単純なstringの送信しかできませんが、
JSONStringを送信すれば一応、指定ユーザーにだけ受信処理をさせるようなこともできます。
(当然その他のユーザーも受信はするので自分でなければ処理しない、という手間が必要になってきますが)const commentJson = { 'to' : 'receiver', 'msg': [] } socket.send(JSON.stringify(commentJson)); // 受信側 socket.addEventListener('message', function (event) { console.log('message',event) const json = JSON.parse(event.data) console.log(json) if (json.to == 'receiver') { // 自分宛なので処理 } }この処理はAchex時点で書いているはずなので、
Achexからの乗り換えではもともとのto指定の値を受信側で見る処理を加えることと、
Auth処理を外すだけで簡単なWS機能なら乗り換えることができました。
- 投稿日:2019-08-16T13:45:11+09:00
音が鳴るタイマーをつくってみた
思ったより時間かかりました。
https://soundtimer.herokuapp.com/
index.html<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <link rel="stylesheet" href="css/styles.css"> <title>Sound Timer</title> </head> <body> <audio id="music"> <source src="js/alarm.mp3" type="audio/mp3"> </audio> <div id="timer"> 00:00 </div> <form name="minForm" id="minForm"> <input type="text" name="work_minutes" placeholder="minute"> <!-- <input type="text" name="break_minutes" placeholder="Break"> --> </form> <button id="start">start</button> <!-- <button id="stop">stop</button> <button id="reset">reset</button> --> <script src="js/main.js"></script> </body> </html>main.js{ const displayTimer = document.getElementById('timer'); const minForm = document.getElementById('minForm'); const start = document.getElementById('start'); const stop = document.getElementById('stop'); const reset = document.getElementById('reset'); const music = document.getElementById('music'); let countdown; let state = "work"; var audio = document.createElement('audio'); console.log(audio); audio.id = 'sound'; audio.src = 'js/alarm.mp3'; function timer(seconds) { let s = seconds; const now = Date.now(); const then = now + seconds * 1000; displayTimeLeft(seconds); countdown = setInterval(() => { const secondsLeft = Math.round((then - Date.now()) / 1000); // check it we should stop it! if(secondsLeft <= 0) { clearInterval(countdown); audio.play(); } //display it displayTimeLeft(secondsLeft); }, 1000); } function displayTimeLeft(seconds) { const m = Math.floor(seconds / 60); const remainderSeconds = seconds % 60; const display = `${String(m).padStart(2, '0')}:${String(remainderSeconds).padStart(2, '0')}`; displayTimer.textContent = display; } start.addEventListener('click', () => { const mins = minForm.work_minutes.value; timer(mins * 60); minForm.reset(); }); }
- 投稿日:2019-08-16T10:51:02+09:00
[JavaScript] 連想配列から特定の値の配列を探す
let array = [{id: 1}, {id: 2}, {...}, ...] let targetItem = array.filter((item, index) => { if (item.id === this.id) { return item } })
- 投稿日:2019-08-16T09:49:16+09:00
【解決策】axiosでPOSTがGETになってしまう
概要
以下のようにaxiosを使ってPOSTしようとすると、GETメソッドが走って404エラーが出てしまいました。
axios({ method: image.id ? 'PUT' : 'POST', url: `/hoge/${image.id}` })原因
どうやらURLの末尾に
/(スラッシュ)が付いていると、POSTしようとしてもGETメソッドが走ってしまう不具合があるようです。【追記: 2019年8月16日】
不具合ではなくスラッシュがないURLにリダイレクトしようとしているだけでした。コメントでのご指摘ありがとうございました。今回の例だと、POSTメソッドのときはimage.idがnullなので、URLが
/hoge/となっていたんですね。解決策
じゃあ末尾スラッシュをなくせばええやん!ということでコードをサクッと直し解決しました。
let url = '/hoge' if (model.id) url += `/${image.id}` axios({ method: image.id ? 'PUT' : 'POST', url: url })
- 投稿日:2019-08-16T09:08:51+09:00
名前をつけるときは予約語に注意
今回出てたLaravelの開発で出たエラー。結構ハマったのでメモ。
結論:フォルダ名も予約語使わないこと。
PHP Parse error: Syntax error, unexpected T_INTERFACE, expecting T_STRING or '{' on line 1初めは全体通して動かしてたけど問題の切り分けのために、tinkerで直指定してファイル呼び出し。
php artisan tinker >>> use App\Infrastructure\Repositories\Interface\IGreetdictionarysRepository; PHP Parse error: Syntax error, unexpected T_INTERFACE, expecting T_STRING or '{' on line 1ファイルの中身を全部消してもエラー出るので、フォルダ名を変えた。
Interfaceって名前が予約語なのでIRepositoriesに変えた。use App\Infrastructure\Repositories\IRepositories; use App\Infrastructure\Repositories\IRepositories\IGreetdictionarysRepository;これで通った。
他の言語でも同じように注意
Pythonの予約語に注意
Pythonの変数名で避けた方がいい名前は?
https://qiita.com/matsui2019/items/98165df7bee980ae6a18MySqlの予約語に注意
MySqlで予約語を使うと、syntax errorが出る
https://qiita.com/ma_me/items/a98842d1cb65bfcd20cf
- 投稿日:2019-08-16T08:38:08+09:00
"Writing An Interpreter In Go " を JavaScriptで実装してみる その2 Parser
Writing An Interpreter In Go " を JavaScriptで実装してみる その1 Lexer
に引き続き 夏休みの個人的課題を進めています
Writing An Interpreter In Goの103ページまで読み進めました
ほぼ動いてないけど(特定の構文は理解できず無限ループするようです^^;)
かなりバギー
[parser 部分のcode]
https://github.com/freddiefujiwara/saru/blob/master/src/parser.js現時点のコードカバレッジ
-------------------------|----------|----------|----------|----------|-------------------| File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s | -------------------------|----------|----------|----------|----------|-------------------| All files | 96.02 | 82.38 | 95.24 | 95.95 | | block_statement.js | 100 | 50 | 100 | 100 | 12 | boolean_literal.js | 100 | 0 | 100 | 100 | 19 | call_expression.js | 80 | 0 | 60 | 77.78 | 9,15 | expression.js | 100 | 100 | 100 | 100 | | expression_statement.js | 100 | 100 | 100 | 100 | | function_literal.js | 100 | 0 | 100 | 100 | 15 | identifier.js | 100 | 100 | 100 | 100 | | if_expression.js | 78.57 | 50 | 40 | 76.92 | 10,16,22 | infix_expression.js | 100 | 16.67 | 100 | 100 | 28,33 | integer_literal.js | 100 | 100 | 100 | 100 | | let_statement.js | 100 | 100 | 100 | 100 | | lexer.js | 100 | 100 | 100 | 100 | | node.js | 100 | 100 | 100 | 100 | | parser.js | 93.75 | 83.91 | 100 | 93.75 |... 39,342,349,384 | prefix_expression.js | 100 | 0 | 100 | 100 | 26 | program.js | 100 | 100 | 100 | 100 | | return_statement.js | 100 | 100 | 100 | 100 | | statement.js | 100 | 100 | 100 | 100 | | token.js | 100 | 100 | 100 | 100 | | -------------------------|----------|----------|----------|----------|-------------------|インストール方法はこちら
npm i -g saru新しい使い方はこちら
$ saru Hello fumikazu! This is the Monkey programming language! Feel free to type in commands * Please type 'bye' if you want to exit >>>こんな構文が理解できます
>>> 1 + 2 * 3 (1 + (2 * 3)) >>> 1 * 2 - 3 ((1 * 2) - 3) >>> false == 1 (false == 1)またLexerだけを使いたい場合は -l オプションつければLexerモードで起動します
$ saru -l Hello fumikazu! This is the Monkey programming language! Feel free to type in commands * Please type 'bye' if you want to exit >>>さて次はいよいよ Evaluation 18日までに終わるかな
- 投稿日:2019-08-16T06:00:03+09:00
【日記】DjangoでImageFieldを実装したり、object-fitで画像表示したりした。
Django公式チュートリアルで作るpollsアプリケーションの発展型のようなものを作っています。詳しくはまだないしょです。
今日は各投稿の詳細画面をある程度実装しました。デザインはまだ何もいじっていないので見た目カッコ悪いですが……。あとからやります。
ImageField
今日実装したのは画像を扱うフィールド、ImageFieldです。公式チュートリアルでは触れられておらず、ドキュメントでも詳しく触れられていなかったので困りました。ググったところ、Django BrothersのチュートリアルがImageFieldについて扱っていて、非常にわかりやすかったのでこれに沿って実装しました。
僕の場合、default値を設定したかったのですが、ファイルを指定するパスが良くわからずだいぶ四苦八苦しました。結果的にこうなりました。
models.pychoice_image = models.ImageField(upload_to='polls', default=os.path.join(BASE_DIR, '/polls/default_icon.jpg'))詳しくはDjango Brothersチュートリアルを読んでほしいのですが、settings.pyでMEDIA_ROOTを指定したところからのパスで良いようです。
settings.pyMEDIA_ROOT = os.path.join(BASE_DIR, 'media')つまり、ImageFieldのデフォルト画像のパスは以下のようになります。
/media/polls/default_icon.jpg
object-fit
画像を表示できたは良いものの、画像サイズがそのままなので表示が整いません。縦横比を維持しつつ正方形で表示したかったので調べてみたところ、CSSのobject-fitというものを使えばうまくいきそうです。
1行追加でOK!CSSだけで画像をトリミングできる「object-fit」プロパティー|Webクリエイターボックス
実際に書いたコードはこちら
/polls/static/polls/style.css.choice_image { width: 100px; height: 100px; object-fit: cover; }画像にはchoice_imageクラスを指定しました。
細かい話
スーパーリロードというものについて知りました。キャッシュを消してリロードできるので、CSSを変更したのに反映されない!ってときに使うといいです。
Django開発中にクロームでcssが反映されない場合の対処方法 - 知的好奇心
円グラフを表示したかったがうまくいかない
投票された割合を表示する手段として、円グラフを実装したいのですがこれがうまくいかなかったので持ち越しです。Chart.jsというJSライブラリを使おうと思ったのですが、Pythonのfor文とどのように共存させればよいかわからないのです。もしかしたらviews.pyで作成したほうが良いのかもしれないですね。
今後の課題です。がんばろう。
- 投稿日:2019-08-16T02:51:42+09:00
JavaScriptをMacのターミナルで使用する時の注意点
Macのターミナルで初めてJavaScriptを実行した際に、基本的なところでエラーが出たのでその対処法を書いていきます。
console.log()でエラー
まずJavaScript標準で用意されているオブジェクトのnowDate()を用いて、今日の日付と今の時刻を表示しようとしました。
nowDate.jsconst today = nowDate(); console.log(today);こちらのjsファイルをターミナルで実行すると、
Exception: TypeError: undefined is not an object (evaluating 'console.log')このようなエラーが出てきました。
print()に変更
nowDate.jsconst today = nowDate(); print(today);このように変更して実行すると、
Fri Aug 09 2019 11:24:41 GMT+0900 (JST)先ほどのようなエラーが起きず無事に現在の日付・時刻を表示できました。
まとめ
エラーが出ても、焦らず冷静に開発環境やコードをチェックしていきたいですね。
- 投稿日:2019-08-16T01:58:38+09:00
ES10で追加されたflat、flatMapを使ってみた。
概要
遂にES10が導入されましたね!!
今回、タイトル通りES10で追加されたflat、flatMapに触れていこうと思います。
基本的には他の言語にあるflat、flatMapに近いと思いますので、
既に使ったことがある人からすれば遂に来たかって感じだと思います。
flat、flatMapに関してはMDNにも記載されていますし、
基本的には他言語とほぼ変わらないと思っていますのでコーディングがあまり慣れていない方が対象になるのではないかと思っています。注意事項
・深夜テンションで書いていますので、適宜修正していきます。
・記事が読みにくいかもしれません。ごめんなさい。
・今回動作確認した環境がGoogle chromeで確認していますが、まだ対応していないブラウザ、エディタは多いです。
・基本的に頭が悪いので「日本語は変!!」とか「は?」ってなるかもですね。
・実業務で使っていないのでサンプルもあまり意味をなさないかもです。flat
まずはMDNの説明を見てみましょう!!
flat() メソッドは、すべてのサブ配列の要素を指定した深さで再帰的に結合した新しい配列を生成します。
MDN(https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/flat)
です。
これ以上言う事はないですね。
ただ、JavaScriptしか触っていない方やそもそもエンジニアではなく自作サイトを作るのに触ったことがあるだけの方にとってはこれと下部に記載されているサンプルコードでは良くわからないと思いますので、解説していきたいと思います。
先ずは以下データがあると仮定します。
javascriptvar matsuri = [ [164, 44, [85, 59, 85]], ["2月4日", "19歳"] ];このmatsuriの配列に対してflat()をすると
matsuriの最初の配列の中にいる全ての配列を1つの展開してまとめる
事ができます。言葉では難しいと思いますので以下コード書きます。
(説明が下手なのをコードで誤魔化しています。)javascriptvar matsuri = [ [164, 44, [85, 59, 85]], ["2月4日", "19歳"] ]; matsuri.flat(); // [164, 44, [85, 59, 85], "2月4日", "19歳"]flat解説
最初の配列にいた配列は
・[164, 44, [85, 59, 85]]
・["2月4日", "19歳"]の2つでしたので1つの配列にまとめると
・[164, 44, [85, 59, 85], "2月4日", "19歳"]
になります。同じようなことをflatを使用せず実現すると
javascriptvar matsuri = [ [164, 44, [85, 59, 85]], ["2月4日", "19歳"] ]; var tempMatsuri = []; for(var i = 0; i < matsuri[0].length; i++) { tempMatsuri.push(matsuri[0][i]); } matsuri = tempMatsuri;と同じになると思います。
上記記述は1例をなりますので、reduceを使ったりするともっとスッキリと記述することが出来ます。
それをflat()のみで実現することが出来ます。上記例では最初の配列には2つの配列しか無いですが、3つの配列でも、4つの配列でも。54つの配列でも同じようになります。
ネストした配列のflat
しかし、実際に使うシチュエーションではもっと複雑なパターンを扱うことのほうが多いです。
例えば先ほどのmatsuri変数だと [164, 44, [85, 59, 85]] のなかにある [85, 59, 85] の配列も1つの配列にまとめたいパターンが出てきます。
その場合、? < matsuri変数を2回flatすれば出来るから matsuri.flat().flat()かな?
って思うかも知れませんが、1回で出来る方法もあります。
先ずは答えから記述すると
javascriptvar matsuri = [ [164, 44, [85, 59, 85]], ["2月4日", "19歳"] ]; matsuri.flat(2); // [164, 44, 85, 59, 85, "2月4日", "19歳"]です。
違いはflatの丸括弧に数字を書いてあげます
今回flat(2)としたので、
最初の配列にある全ての配列でその中にも配列があれば展開する
となります。これを見て
? < flat()に渡している数字が何で1じゃないの?
って思った方は鋭いですね。素晴らしいです。flat(1)ではないのはflat()とflat(1)は同じ意味だからです。
つまり丸括弧の中に書いた数字分だけ配列の中の配列の中の配列の中の・・・中の配列を展開して1つの配列にまとめることが出来ます。
flatMap
先ほどflatの説明をしましたが、実際には
? < 配列を展開するときに処理みたいんだけど、flatで一回展開してからじゃないとだめなの?
って思う方がいると思います。
その時にflatMapを使うんです。
今回もまずはMDNの説明を見てみましょう!!
flatMap() メソッドは、最初にマッピング関数を使用してそれぞれの要素をマップした後、結果を新しい配列内にフラット化します。
これは深さ 1 の flat が続く map と同じですが、flatMap はしばしば有用であり、2 つのメソッドを 1 つにマージするよりもやや効果的です。MDN(https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/flatMap)
です。
? < は?
今回もサンプルを書いてみましょう。
まずは以下データがあります。javascriptvar matsuri = [ [164, 44, [85, 59, 85]], ["2月4日", "19歳"] ];これデータを以下の形に加工します。
javascriptvar matsuri = [ { "height": 164, "weight": 44, "bust": 85, "waist": 85, "hip": 85, }, { "birthday": "2月4日", "age": "19歳" } };実現するソースコードです。
javascriptvar matsuri = [ [164, 44, [85, 59, 85]], ["2月4日", "19歳"] ]; matsuri.flatMap(function(value, index) { if(index === 0){ return { "height": value[0], "weight": value[1], "bust": value[2][0], "waist": value[2][1], "hip": value[2][2], } } else { return { "birthday": value[0], "age": value[1] } } })*今回は慣れていない方にもわかりやすいようしているつもりなので、
アロー演算子や三項演算子は使っていません。flatMap解説
まずflatMapとは上記説明したflatと配列を順番に処理するmapの機能が組み合わさったものです。
matsuri変数は配列でその中には2つの配列がいます。
mapは配列の要素分ループしますので、matsuri変数では2回ループします。1回目のLoopは
・value => [164, 44, [85, 59, 85]]
・index => 02回目のLoopは
・value => ["2月4日", "19歳"]
・index => 1なります。
flatMapの戻り値はmapと同じで配列になりますので各配列の要素に加工したデータが入ります。
後は少しでも何かしらのコードを書いた事あるならわかると思います。
解説終わり!!
あとがき
いかがだったでしょうか?
深夜テンションの勢いで書いたので結構読みずらいと思います。(深夜2時)
指摘や編集リクエストは受け付けていますので、気になった部分があればお気軽にどうぞ!!
コードの内容は勿論、日本語の添削でも嬉しいです。
まだ導入されたばかりなのでvscodeでも現状使えなかったのです。(2019年8月16日)
まだ実用的ではないので予習的に見てもらっても良いともいます。
MDNでも書いてますがIEでは使えないみたいです。(未確認)
早くIEは・・・勿論trimStartやtrimEnd、Object.fromEntriesと言った機能も追加されていますのでこの調子でjavaScriptが盛り上がってほしいですね!!
まだ、編集中の記事がいくつもあるのでいいねを下さるとモチベ向上となります。
お手数でなければポチッとしていただけると嬉しいです。個人的にはTypeScriptタイプセーフ過激派なので、Optional ChainingやPipeline Operatorを期待していましたが、
今回は見送りっぽいので残念です!!
ぜひes11で!!
- 投稿日:2019-08-16T01:55:39+09:00
JavaScript/Reactの見直し用記事
はじめに
いろんな言語をやっているとどうしても忘れてしまうのでよく忘れるものをメモ
以下の違いって何?って質問された時微妙な答えをしてしまったのでコレもメモ
フロントエンドとは?
- HTMLの構築
- バックエンドへAPIを呼び出す
- データの返却を受け取るバックエンドとは?
- データベースとの接続
- フロントエンドからの要求に対応するデータを返すAPIを用意
- データをフロントエンドへ渡すDOMとは
「Document Object Model」の略。HTMLとJavaScriptをつなぐ役割をもつ。
階層構造を取り、各要素を表すノードで表現されることが多い。Node.jsとは
JavaScript アプリケーションのプラットフォームでサーバーサイドのJavaScript
ReactなどのJSフレームワークを導入する際にはNode.jsが必要となる。(Node.jsが支える側に)
Node.jsを使わずにReactを利用するには、CDN経由で読み込む。node.jsのバージョン管理はnodebrewを使う。これはhomebrewでインストールする。
homebrew自体でもでもnodeは管理できるがおすすめされない。npmとは
「Node Package Manager」の略。Node.jsのパッケージを管理するツールである。
nodebrewからNode.jsをインストールすると付いてくる。yarnとは
npmと同様にJavaScriptのパッケージ管理ツール。
高速で、信頼性が高く、安全性も高い。
homebrewからインストールする。JavaScript
1個目の書き方(関数式)
関数を宣言する際に関数名を記述しなくても良いので、無名関数や匿名関数とも呼ばれる方法
関数式const 定数名 = function(仮引数, 仮引数, ...) { 処理; return 返り値; };関数式let 変数名 = function(仮引数, 仮引数, ...) { 処理; return 返り値; };呼び出し定数名(実引数, 実引数, ...);例const name = function(x) { console.log(x) // 出力: 香風智乃 }; name("香風智乃")2個目の書き方(関数宣言)
関数をそのまま宣言することでプログラム内で利用することができるようにする方法
関数宣言function 関数名(仮引数, 仮引数, ...) { 処理; return 返り値; }呼び出し関数名(実引数, 実引数, ...);例function dispMsg(str){ console.log(str); // 出力: 保登心愛 } dispMsg("保登心愛");3個目の書き方(アロー関数)
ES2015(ES6)から導入され、
functionを使わない代わりに、=>で関数を表現することができるアロー関数const 定数名 = (仮引数, 仮引数, ...) => { 処理; return 返り値; }アロー関数let 変数名 = (仮引数, 仮引数, ...) => { 処理; return 返り値; }呼び出し定数名(実引数, 実引数, ...);例const name = (x) => { console.log(x) } name("香風智乃")参考記事
JavaScriptのthisの理解
アロー関数式
prototype
Object
method
コールバック関数
非同期処理
テンプレートリテラル
React
create-react-app$ npx create-react-app アプリ名 $ cd アプリ名 $ yarn startフォーム
なぜコールバック関数を使用する際はアロー関数を使わないとエラーになるのか
React-Redux
- Quick Start(React Redux 公式サイト)
- React-Reduxの動きがよくわからなくなるのでまとめた
- React + Reduxで作ったIsomorphic(Universal) JSなサービス開発
非同期処理
React Hooks


































