- 投稿日:2019-07-30T23:46:00+09:00
JavaScriptハンズオン資料
はじめに
この記事はJavaScript入門を目的としたハンズオンの資料となります。
本記事ではJavaScriptとHTML・CSSを組み合わせた実用的な部分の入門となるよう書くため、HTML・CSS及びJavaScriptの基本的な部分は解説しません。JavaScriptの文法や機能はJavaScript Primer、HTML・CSSはHTML, CSSを見て頂ければと思います。環境
- 最新のGoogle chrome
- テキストエディタ(メモ帳でないのが望ましい)
- Windows
でのコーディング及び実行を想定しています。
基本の雛型
HTMLファイル上に記述する
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Hello world!</title> </head> <body> <p>Hello world!</p> <script type="text/javascript"> // ここにJavaScriptを書きます </script> </body> </html>
<script>
はbody内の一番最後に記述します。ファイルを分ける
html
ファイルとjs
ファイルを分けて記述することが出来ます。本記事ではファイルを分けてコーディングします。project_directory | +- index.html +- style.css +- index.js +- images | +- hoge.png +- fuga.png +- piyo.png以上のようなディレクトリ構造を想定した場合、
<body> <script type="text/javascript" src="index.js"></script> </body>とHTMLファイル上に記述するのと同様、body内の一番最後に記述します。
イベント
以下、
project | +- index.html +- style.css +- index.jsというディレクトリであることを前提に進めていきます。
ボタンを押してメソッド発火
まずはボタンを押すと、
Hello World
とアラートされるプログラムをつくります。<button onclick="alert('Hello World')">ボタン</button>これでも望む動作をしますが、この後のセクションにスムーズに入るために、別にメソッドを用意して動かしたいと思います。
index.html<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Hello world!</title> </head> <body> <input type="button" value="ボタン" onclick="helloWorld()"> <script type="text/javascript" src="index.js"></script> </body> </html>index.js"use strict" const helloWorld = function(){ window.alert("Hello World"); }このように記述して実行してみましょう。ボタンを押すと
Hello World
とアラートされるはずです。onload
ロードが終わったタイミングや特定のDOMが構成し終わったタイミングでメソッドを発火させたいことがあるかもしれません。そういう時は
onload
を使いましょう。index.html<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Hello world!</title> </head> <body> <p>こんにちわ</p> <script type="text/javascript" src="index.js"></script> </body> </html>index.js"use strict" window.onload = function(){ setTimeout(()=>window.alert("DOMが構成された"), 1000); }
setTimeout
により、DOMが構成された1秒後にアラートが出ます。……ちょっとわかりにくいですね。取得
ページ内の要素を取得
HTML内の要素を取得してみましょう。
DOM内にあるpタグの数を取得してみます。index.html<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Hello world!</title> </head> <body> <p>a</p> <p>b</p> <p>c</p> <input type="button" value="get_p_num" onclick="tagGetFunc()"> <script type="text/javascript" src="index.js"></script> </body> </html>index.js"use strict" const tagGetFunc = function(){ const pNum = document.getElementsByTagName("p"); window.alert(pNum.length); }実行してみましょう。
3
と出たはずです。これは全体の<p>
タグの数です。また、
id
やclass
、name
でも指定して取得できます。
が、ここではid
のみ例を載せます。index.html<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Hello world!</title> </head> <body> <p id="hoge">こんにちわ</p> <input type="button" value="Hello" onclick="helloWorld()"> <script type="text/javascript" src="index.js"></script> </body> </html>index.js"use strict" const helloWorld = function(){ const hoge = document.getElementById("hoge"); window.alert(hoge.textContent); }
- getElementById
- textContent
- getElementsByClassName
- getElementsByClassNameとgetElementByIdの返り値の違い
- getElementsByName
テキストボックスからの取得
次はボタンを押すと、テキストボックスに入力した文字のアラートが出るコードを書きます。
index.html<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Hello world!</title> </head> <body> <input type="text" id="data"><input type="button" value="ボタン" onclick="helloWorld()"> <script type="text/javascript" src="index.js"></script> </body> </html>index.js"use strict" const textData = document.getElementById("data"); const helloWorld = function(){ window.alert(textData.value); }テキストボックスに文字を入力してボタンを押すと、入力した文字がアラートで出たと思います。
制御
ページ内の要素を書き換え
JavaScriptではHTML要素をイジることが出来ます。
ボタンを押したらHTML上の<p id="hoge"></p>
内を書き換えるコードを書いてみましょう。index.html<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Hello world!</title> </head> <body> <p id="hoge">こんにちわ</p> <input type="button" value="English" onclick="helloWorld()"> <script type="text/javascript" src="index.js"></script> </body> </html>index.js"use strict" consy helloWorld = function(){ document.getElementById("hoge").textContent = "Hello!"; }実行してみましょう。
こんにちは
がHello!
になったはずです。ここまでやってきたことを組み合わせてみましょう。
onload
を用いてDOMが構成されてからsetInterval
で1秒ごとにカウントし、pタグ内の数値を更新します。そしてボタンを押すか、5秒経った時点でカウントが止まるプログラムにしてみます。index.html<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Hello world!</title> </head> <body> <p id="timer">0</p> <input type="button" value="stop" onclick="stopCount()"> <script type="text/javascript" src="index.js"></script> </body> </html>index.js"use strict" let count = 0; let timer; const timerNode = document.getElementById("timer"); const stopCount = function(){ clearInterval(timer); window.alert(count + "[s]"); } window.onload = function(){ timer = setInterval(()=>{ count++; timerNode.textContent = count; if(count >= 5){ stopCount(); } }, 1000); }もっと上手なコードがあると思うんですけど、今はこれ以上思いつきません。思いついて、この記事のことを覚えていたら追記します。
5秒目にアラートが出たときページ上での秒数は4
となっていますが、アラートを消すとちゃんと5
になります。DOMの追加
JavaScriptで途中からDOMを追加してみましょう。まずは末尾に要素を生成します。
index.html<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Hello world!</title> </head> <body> <p>これは元からあった要素です</p> <input type="button" value="生成" onclick="generate()"> <script type="text/javascript" src="index.js"></script> </body> </html>index.js"use strict" const p = document.createElement("p"); p.textContent = "新しく追加したノードです"; const generate = function(){ document.body.appendChild(p); }ボタンを押すと初めの一回だけ
<p>新しく追加したノードです</p>
が末尾に追加されます。DOMを生成後、その要素の情報を表示
次は画像要素を生成して、その画像データをアラートさせてみましょう。
画像は読み込みに時間が掛かります。ですから、onload
を用いてDOM生成後に画像のデータを表示させてやる必要があります。index.html<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Hello world!</title> </head> <body> <script type="text/javascript" src="index.js"></script> </body> </html>index.js"use strict" const img = document.createElement("img"); img.onload = function() { window.alert("画像の横幅は" + img.width + "\n" + "画像の縦幅は" + img.height); } img.src = "http://www.lovelive-anime.jp/otonokizaka/img/member/member09_04.png"; document.body.appendChild(img);
document.createElement("img")
はImage()でも可です。mapを用いた要素の分割生成
input
で入力した文字列をsplit(",")
で,
ごとに区切ってリスト化、それを要素一つ一つpタグで出力させてみます。index.html<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Hello world!</title> </head> <body> <input type="text" id="data"><input type="button" value="ボタン" onclick="helloWorld()"> <script type="text/javascript" src="index.js"></script> </body> </html>index.js"use strict" let textData = document.getElementById("data"); const helloWorld = function(){ textData = textData.value.split(","); textData.map((el)=>document.body.appendChild(document.createElement("p")).textContent = el); }ボタンを押すと初めの一回だけpタグで要素が
,
で分割された部分で改行され表示されます。
- split
- map
- Arrow Functionおまけ
コードゴルフを少し載せたいと思います。
FizzBuzz
fizzBuzz=num=>(num%3?"":"Fizz")+(num%5?"":"Buzz")||num; console.log(fizzBuzz(15)); // FizzBuzz連番のリスト
[...Array(5).keys()].forEach(el=>console.log(el)) /* 0 1 2 3 4 */リスト内の合計
console.log([...Array(10).keys()].map(i=>++i).reduce((a,b)=>a+=b,0)); // 55おわりに
よく使いそうだと思ったものをまとめました。ここから色々な技術に繋がればいいなと思います。
発展として
- JavaScript Primer
- JavaScriptでタイピングゲームを作ろう!
- 初心者がモチベーション上げながらプログラミングをしてシューティング(っぽい)ゲームを1本作る!
- Node.js
- Express
- Vue.js
- React.js
- 初心者のためのGoogle Apps Scriptプログラミング入門
- jQuery入門
- phina.js
- とりあえず試してみたいって方のための phina.js 入門
- phina.js version 0.2
- phina.js Tips集
- phina.js チュートリアル集
- phina.js はみ出し制御
- phina.js ジャンプアクション
- phina.js superMethodの使い方
- phina.jsでシューティングゲームをつくりました~
- 投稿日:2019-07-30T23:19:39+09:00
Error: No Firebase App '[DEFAULT]' has been created - call Firebase App.initializeApp()を解決する
- 投稿日:2019-07-30T22:44:04+09:00
obnizで加速度センサー,そしてp5js
概要
ハッカソンに参加した際,うまく行かずに断念してしまったものの復習と,
ついでに以前p5jsと連携したもの( https://qiita.com/tkyko13/items/0720246fbd1bb5eee1ab )の応用をしまーす環境
obniz 2.0.0
加速度センサー KXR94-2050加速度センサー
https://obniz.io/ja/sdk/parts/KXR94-2050/README.md
https://www.petitmonte.com/robot/howto_kxr94_2050.html
https://deviceplus.jp/hobby/entry017/ハッカソン中,値は取れたっぽいのですが,めっちゃ熱くなったのでやめちゃった
それをもう一回配線してみます
地味にobnizのページでは配線乗ってないので(探せばすぐ見つかるが)他のページを参考にしていきます実装
ソースコード
let Obniz = require("obniz"); const obniz = new Obniz(YOUR_ID); obniz.onconnect = async function() { var sensor = obniz.wired("KXR94-2050", { vcc: 0, gnd: 1, x: 2, y: 3, z: 4, enable: 5, self_test: 6 }); sensor.onChange = function(values) { console.log("x:" + values.x); console.log("y:" + values.y); console.log("z:" + values.z); }; };参考サイトのobnizのサイトからのサンプルコードです
そのサイトからでも実行できるので,obnizは便利ですよねぇ
上記のコードはnodeで実行しています結果
簡単に動きましたね
本番はどんな配線していたか忘れましたがハッカソンなどで焦っているときは特に複数のサイトを見てつなげていたのでうまくいかなかったのかもしれませんねp5jsと加速度センサー
前回の記事でやったp5jsとの連携もやってみます
p5jsで描いた3Dモデルを加速度で回転させてみますか...と思ったが,
- p5jsで3Dモデルはwebglモード
- obnizでのdrawは2D前提っぽいなので2Dでボールをコロコロ転がすことにします
ソースコード
前回と同じでobnizの開発コンソールから実行します
今回はp5jsの関数のdraw関数内ではなく,描画を更新するobnizのrepeat関数でp5jsのグラフィックを描くコードも描いています
ブラウザの方の画面のガタつきを軽減したいならばやっぱdrawに書くべきかもしれませんね<html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.8.0/p5.js"></script> <script src="https://obniz.io/js/jquery-3.2.1.min.js"></script> <script src="https://unpkg.com/obniz@2.2.0/obniz.js" crossorigin="anonymous"></script> </head> <body> <div id="obniz-debug"></div> <script> var obniz = new Obniz(YOUR_ID); var x=32; var y=16; var spx = 0; var spy = 0; obniz.onconnect = async function () { const ctx = $(".p5Canvas")[0].getContext('2d'); var sensor = obniz.wired("KXR94-2050", { vcc:0, gnd:1, x:2, y:3, z:4, enable:5, self_test:6 }); obniz.repeat(function(){ var values = sensor.get(); obniz.display.draw(ctx); // 初回values.yにNanが入るので if(values.x && values.y) { background(200); spx -= values.y; spy -= values.z; x += spx; y += spy; if(x < 0 || 64 < x) { spx=-spx*0.9; x = constrain(x, 0, 64); } if(y < 0 || 32 < y) { spy=-spy*0.9; y = constrain(y, 0, 32); } ellipse(x, y, 10, 10); } }); } function setup() { createCanvas(64, 32); } function draw() { } </script> </body> </html>obnizのディスプレイにp5jsのコードで円を描いてみた
— るしわん (@tkyko13) July 24, 2019
加速度センサで動かした〜 pic.twitter.com/UyWgQiUdpYまとめ
機材トラウマをなくすってワードが拮抗しっくりきていていいなって思ってます
次回はobnizのi2cを見ていけたらなと思います
- 投稿日:2019-07-30T22:09:04+09:00
ドットインストールjavascript~ヘルスケアwebサービスを自分で作る医者の日記~
三択クイズ 8から
- 投稿日:2019-07-30T22:04:29+09:00
JSの配列処理に関する便利メソッドについて調べてみた
JavaScriptの便利メソッドについて
JavaScriptについていろいろと調べていたらはいつの間にか便利メソッドが色々と追加されていたので調べてみました。for文で書いた場合との違いを比較してみたところ、これらのメソッドをうまく使うことができれば可読性が良くなりそうです。
Array.prototype.forEach()
与えられた関数を、配列の各要素に対して一度ずつ実行します。
Array.prototype.forEach()const array = [0, 1, 2, 3, 4, 5, 6]; array.forEach(value => { console.log(value *10); }); //上記をfor文で書いた場合 const array = [0, 1, 2, 3, 4, 5, 6]; for(let i=0; i<array.length; i++){ console.log(array[i]*10); }Array.prototype.map()
mapはある配列から新しい配列を作りたいときに使用します。
forEachの場合は配列の中の要素を順次見ていくだけですが、mapメソッドはそれを返り値として返します。Array.prototype.map()const array = [0, 1, 2, 3, 4, 5, 6]; const newArray = array.map(value => { return value * 10 }); console.log(array); //[0, 1, 2, 3, 4, 5, 6] console.log(newArr); //[0, 10, 20, 30, 40, 50, 60] //上記をfor文で書いた場合 let array = [0, 1, 2, 3, 4, 5, 6]; let newArr = []; for(var i=0; i<array.length; i++){ newArr.push(array[i] * 10); } console.log(array); //[0, 1, 2, 3, 4, 5, 6] console.log(newArr); //[0, 10, 20, 30, 40, 50, 60]Array.prototype.filter()
条件に一致した配列の要素をすべて抽出して新しい配列を生成します。
Array.prototype.filter()const array = [0, 1, 2, 3, 4, 5, 6]; const newArray = array.filter(value => { return value % 2 === 1; //奇数だけ抽出する }); console.log(newArray); //[1, 3, 5] //上記をfor文で書いた場合 const array = [0, 1, 2, 3, 4, 5, 6]; let newArray = []; for(let i=0; i<array.length; i++){ //奇数だけ抽出する if(array[i] % 2 === 1){ newArray.push(array[i]); } } console.log(newArray); //[1, 3, 5]Array.prototype.find()
findメソッドは配列の中の条件に一致した最初の要素を一つ返します。
見つからない場合undefinedを返します。Array.prototype.find()const array = [0, 1, 2, 3, 4, 5, 6]; const res = array.find(element => { return element > 3; }); console.log(res); //4 //上記をfor文で書いた場合 const array = [0, 1, 2, 3, 4, 5, 6]; for(let i=0; i<array.length; i++){ if(array[i] > 3){ let res = array[i]; console.log(res); //4 break; } }Array.prototype.reduce()
Array.prototype.reduce()const array = [0, 1, 2, 3, 4, 5, 6]; const res = array.reduce((accumulator, currentValue) => { return accumulator + currentValue; }); console.log(res); //21 //上記をfor文で書いた場合 const array = [0, 1, 2, 3, 4, 5, 6]; let res = 0; for(let i=0; i<array.length; i++){ res += array[i]; } console.log(res); //21Array.prototype.reduceRight()
基本的にはreduceと同じです、違うのは配列を右から処理していく点でになります。
Array.prototype.reduceRight()const array = [0, 1, 2, 3, 4, 5, 6]; const res = array.reduceRight((accumulator, currentValue) => { return accumulator + currentValue; }); console.log(res); //21 //上記をfor文で書いた場合 const array = [0, 1, 2, 3, 4, 5, 6]; let newArray = array.reverse(); //配列を反転 let res = 0; for(let i=0; i<newArray.length; i++){ res += array[i]; } console.log(newArray); //21まとめ
for文でも同様な実装は可能ですが、上記メソッドを使用すると、filterやfindなどメソッド名を見るだけでそれがどういう処理をしているのか容易に判断できるようになり可読性がよくなりそうです。
参考
MDN | Array.prototype.forEach()
MDN | Array.prototype.map()
MDN | Array.prototype.filter()
MDN | Array.prototype.find()
MDN | Array.prototype.reduce()
MDN | Array.prototype.reduceRight()
- 投稿日:2019-07-30T21:54:53+09:00
恐竜でもわかるJavaScript
こちらの記事は、『Modern JavaScript Explained For Dinosaurs』の和訳になります。
本投稿は転載であり、本記事はこちらになります。はじめに
あなたがJavaScriptの進化と共に歩んでこなかったのであれば、今のJavaScriptを学ぶことは難しく感じるでしょう。エコシステムは急速に成長し変化しているため、さまざまなツールが解決しようとしている問題を理解することは困難です。私は1998年からプログラミングを始めて、2014年になって初めてJavaScriptを真剣に学び始めました。当時私はBrowserifyに出会い 、そのキャッチフレーズを見たことを覚えています。
「Browserifyは、あなたの依存関係のすべてをまとめることによって、ブラウザでrequire(‘modules’) を使えるようにします。」
私はこの文章中の単語をほとんど理解しておらず、開発者にとってどのように役立つかのかわかりませんでした。
この記事の目的は、JavaScriptに関連するツールがどのように進化して2017年時点の形になったかという歴史的背景を説明することです。最初から始めて、サンプルのウェブサイトを作成します。恐竜のようにツールは使用せずに、HTMLとJavaScriptだけを使います。その後に私達は一つずつ、問題を解決するためにツールを適用していきます。JavaScriptの進化の歴史をトレースすることで、変化し続けるJavaScriptの状況を学び、順応することができます。それでは、始めましょう!
JavaScriptを「昔ながらの」方法で使う
HTMLとJavaScriptを使用して、手動でファイルをダウンロードしてリンクする「昔ながらの」Webサイトから始めましょう。これは、JavaScriptファイルをリンクする単純なindex.htmlファイルです。
index.html<! DOCTYPE HTML> <html lang = "en"> <head> <meta charset = "UTF-8"> <title>JavaScript Example</title> <script src="index.js"></script> </head> <body> <h1>Hello from HTML!</h1> </body> </html>index.jsconsole.log("Hello from JavaScript!");これがウェブサイトを作るために必要なすべてです!このウェブサイトに、あなたがmoment.jsのような他の人が書いたライブラリ(人間が読める形式で日付をフォーマットするのを助けることができるライブラリ)を追加するケースを考えましょう。
たとえば、次のようにJavaScriptでmomentが提供する関数を使用できます。
moment().startOf('day').fromNow(); // 20時間前
しかし、これはあなたのウェブサイトにmoment.jsを含んでいることを前提としています!
moment.jsのホームページには、次のような内容があります。
画面右側の「インストール」セクションにはたくさんのインストール方法が書かれてていますが、今は無視してください。
HTMLファイルと同じディレクトリにmoment.min.jsファイルをダウンロードし、moment.jsをindex.htmlファイルから参照することでウェブサイトに追加します。index.html<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Example</title> <link rel="stylesheet" href="index.css"> <script src="moment.min.js"></script> <script src="index.js"></script> </head> <body> <h1>Hello from HTML!</h1> </body> </html>moment.min.jsをindex.jsより前にリンクしていることに注意してください。index.jsより前にロードすることで、momentが提供する関数を使用できます。
index.jsconsole.log("Hello from JavaScript!"); console.log(moment().startOf('day').fromNow());このように、他の人が書いたJavaScriptライブラリを使ったウェブサイトを作ることができます!昔ながらの方法のメリットは、理解するのが簡単だということです。デメリットは、ライブラリが更新されるたびに新しいバージョンのライブラリを見つけてダウンロードするという面倒な作業が発生することです。
JavaScriptパッケージマネージャを使用する(npm)
2010年頃から、いくつかの競合するJavaScriptパッケージマネージャが登場し、中央リポジトリからライブラリをダウンロード(アップグレード)するプロセスを自動化できるようになりました。2013年にはBowerが間違いなく最も人気がありましたが、2015年頃にはnpmに追い越されました。( yarnはnpmの代わりとして2016年末ごろから多くの注目を集めていますが、内部ではnpmのパッケージを使用しています。)
npmはもともとnode.js というサーバー上で動作するように設計されたJavaScriptランタイムのために作られたパッケージマネージャでした。そのため、ブラウザで動くJavaScriptのパッケージマネージャとして選択するのは奇妙に感じるでしょう。
注意:パッケージマネージャを使うためには、一般的にコマンドラインを使う必要があります。フロントエンド開発者はこれまでコマンドラインを使う必要がほとんどありませんでした。コマンドラインを一度も使用したことがない場合は、 このチュートリアルを読んで概要を把握してから始めることができます。良くも悪くも、コマンドラインの使い方を知ることは現代のJavaScriptでは重要な要素になります(そしてそれは他の開発分野にも扉を開きます)。
それでは、手動でライブラリをダウンロードするのではなく、npmを使用してmoment.jsパッケージを自動的にインストールする方法を見てみましょう。node.jsがインストールされている場合は、すでにnpmもインストールされています。コマンドプロンプトなどのターミナルを開き、index.htmlファイルがあるフォルダに移動してから次のコマンドを実行します。
$ npm init
いくつかの質問が表示され(デフォルトは大丈夫です。質問ごとに「Enter」を押すとデフォルトが選択されます)、package.jsonという名前の新しいファイルが生成されます。これは、npmがすべてのプロジェクト情報を保存するために使用する設定ファイルです。デフォルトでは、package.jsonの内容は次のようになります。
package.json{ "name": "your-project-name", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC" }次のコマンドを実行して、moment.js パッケージをインストールすることができます。
$ npm install moment --save
このコマンドは2つのことを行います 。
最初に、moment.jsパッケージのすべてのコードをnode_modulesというフォルダーにダウンロードします。
次に、package.jsonファイルを自動的に変更して、プロジェクトの依存関係としてmoment.jsを保持します。package.json{ "name": "modern-javascript-example", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "moment": "^2.22.2" } }これは後で他の人とプロジェクトを共有するときに便利です。node_modulesフォルダ(非常に大きくなることがある)を共有する代わりに、package.jsonファイルを共有するだけでよく、他の開発者はコマンドnpm installで必要なパッケージを自動的にインストールできます。
npmを使うことで、Webサイトからmoment.jsを手動でダウンロードする必要はなくなりました。npmを使用すれば自動的にダウンロードして更新できます。
node_modulesフォルダーの中を見ると、node_modules/moment/minディレクトリにmoment.min.jsファイルがあります。そのため、次のようにしてindex.htmlファイルからnpmダウンロード版のmoment.min.jsを参照できます。
index.html<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>JavaScript Example</title> <script src="node_modules/moment/min/moment.min.js"></script> <script src="index.js"></script> </head> <body> <h1>Hello from HTML!</h1> </body> </html>メリットは、コマンドラインからnpmを使ってパッケージをダウンロードして更新できるようになったことです。
デメリットは、node_modulesフォルダーを調べて各パッケージの場所を見つけ、それを手動でHTMLに記載しなければいけないことです。あまりにも不便なので、次にそのプロセスを自動化する方法を見てみましょう。JavaScriptモジュールバンドラー(webpack)を使う
ほとんどのプログラミング言語は、あるファイルから別のファイルにコードをインポートする方法を提供します。しかし、JavaScriptはこの機能を使用できるように設計されていませんでした。JavaScriptはブラウザ上で実行されるように設計されていて、セキュリティ上の理由からクライアントのコンピュータのファイルシステムにアクセスできないからです。そのため、長い間JavaScriptコードを複数のファイルにまとめるには、各ファイルをグローバルに共有する変数に読み込む必要がありました。
これは実際にmoment.jsの例で行っていることです - moment.min.jsファイル全体がグローバル変数momentに格納され、HTMLにロードされます。moment変数はmoment.min.jsの後にロードされたどのファイルでも利用可能です。(アクセスする必要がないファイルでも参照できてしまいます)
2009年には、ブラウザの外部で動作するJavaScriptのエコシステムの仕様を決めることを目的として、CommonJSというプロジェクトが開始されました。CommonJSの大部分はモジュールの仕様でした。CommonJSにより、JavaScriptはほとんどのプログラミング言語と同じように、グローバル変数を使用せずに直接コードをインポートおよびエクスポートすることができるようになりました。CommonJSモジュールの実装で最もよく知られているのはnode.jsです。
前述したように、node.jsはサーバー上で動作するように設計されたJavaScriptランタイムです。下記のコードは、moment.jsをnode.jsで使う場合のサンプルになります。HTMLスクリプトタグでmoment.min.jsをインポートする代わりに、次のようにJavaScriptファイルから直接読み込むことができます。
index.jsvar moment = require('moment'); console.log("Hello from JavaScript!"); console.log(moment().startOf('day').fromNow());繰り返しになりますが、これがnode.jsでモジュールをロードする仕組みです。node.jsはコンピュータのファイルシステムにアクセスできるサーバーサイドの言語なので、とてもうまくいきます。node.jsはnpmモジュールの位置も知っているので、require( 'node_modules/moment/min')と書く必要はなく、単にrequire(' moment ')と書くことができます。
node.jsを動かす場合にはとても良い仕組みなのですが、ブラウザで上記のコードを使用しようとすると、requireが定義されていないというエラーが表示されます。ブラウザはファイルシステムにアクセスすることができないため、この方法でモジュールをロードするためには複雑な方法をとる必要があるということです。 - ファイルのロードは動的に、同期的(実行速度を落とす)または非同期(タイミングの問題がある)に行う必要があります。
そこで、モジュールバンドラーを導入します。JavaScriptモジュールバンドラーは、ビルドステップ(ファイルシステムへのアクセス権を持つタイミング)で問題を回避するツールです。ファイルシステムへのアクセスする必要がない、ブラウザ互換性のあるファイルを作成します。このケースでは、すべてのrequireステートメント(ブラウザのJavaScriptでは無効な構文)を見つけて、リンクする先の内容に置換するモジュールハンドラが必要になります。最終的な出力は単一のバンドルされたJavaScriptファイルとなります(requireステートメントは含まれていません)。
2011年にリリースされ、フロントエンドでのnode.jsスタイルのrequireステートメントの使用法の先駆けとして、最も人気のあるモジュールバンドラーはBrowserifyでした(これが本質的にnpmをフロントエンドパッケージマネージャーとして使用することを可能にしました)。2015年頃には、webpackがより広く使用されるモジュールバンドラーとなりました(webpackのさまざまな機能をフルに活用したReactフロントエンドフレームワークの普及により促進されました)。
ブラウザで前述したのrequire('moment')の例を動かすためにwebpackを使う方法を見てみましょう。最初にwebpackをプロジェクトにインストールする必要があります。webpack自体はnpmパッケージなので、コマンドラインからインストールできます。
$ npm install webpack webpack-cli --save-dev
webpackとwebpack-cliの2つのパッケージをインストールしていることに注意してください(これでコマンドラインからwebpackを使用できます)。--save-devを引数とすることで開発時の依存関係として保存されます。つまり、本番サーバーではなく開発環境で必要なパッケージとなります。package.jsonファイルに反映されて、自動的に更新されてます。
package.json{ "name": "modern-javascript-example", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "moment": "^2.19.1" }, "devDependencies": { "webpack": "^4.17.1", "webpack-cli": "^3.1.0" } }これでwebpackとwebpack-cliがパッケージとしてnode_modulesフォルダにインストールされました。次のようにコマンドラインからwebpack-cliを使用できます。
$ ./node_modules/.bin/webpack index.js --mode=development
このコマンドは、node_modulesフォルダーにインストールされたwebpackツールを実行し、index.jsファイルから始めて、requireステートメントを見つけ、それらを適切なコードに置換して単一の出力ファイル(デフォルトではdist/main.js)を作成します。
--mode = development引数を使用することで出力されたJavaScriptは開発者にとって読みやすい形式となります。(引数--mode = productionを使用した場合、出力されるJavaScriptは圧縮されます)
webpackにより出力されたdist/main.jsはrequireステートメントを含まないため、ブラウザでindex.jsの代わりに参照することができます。次のようにindex.htmlファイルに反映します。
index.html<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>JavaScript Example</title> <script src="dist/main.js"></script> </head> <body> <h1>Hello from HTML!</h1> </body> </html>ブラウザを更新すると、すべてが以前と同じように機能していることがわかります。
index.jsを変更するたびにwebpackコマンドを実行する必要があることに注意してください。これは面倒で、webpackのより高度な機能を使用するにつれてさらに面倒になるでしょう(変換されたコードから元のコードをデバッグするのを助けるためにソースマップを生成するなど)。
webpackはwebpack.config.jsというプロジェクトのルートディレクトリにある設定ファイルからオプションを読み取ることができます。
webpack.config.jsmodule.exports = { mode: 'development', entry: './index.js', output: { filename: 'main.js', publicPath: 'dist' } };index.jsを変更するたびに、次のコマンドでwebpackを実行できます。
$ ./node_modules/.bin/webpack
webpackはwebpack.config.jsファイルからこれらのオプションを読み込んでいるので、index.jsと--mode = developmentオプションを指定する必要はもうありません。
これは優れていますが、コードを変更するたびにこのコマンドを入力するのは面倒です - 後でこのプロセスを少しスムーズにします。
全体的に見ると大したことではないかもしれませんが、このワークフローにはいくつかの大きな利点があります。グローバル変数を介して外部スクリプトをロードすることはもうありません。
HTMLに新し<script>タグを追加するのではなく、JavaScriptのrequireステートメントを使用して新しいJavaScriptライブラリを追加します。単一のJavaScriptバンドルファイルを使用することになるので、多くの場合パフォーマンスが向上します。
そして、ビルドステップを追加したため、他にも開発ワークフローに追加できる強力な機能がいくつかあります。
新しい言語機能のためのコード変換(babel)
コード変換とは、ある言語のコードを別の言語の同等なコードに変換することです。これはフロントエンド開発では重要なポイントとなります - ブラウザは新しい機能を追加するのが遅いので、新しい言語はブラウザの互換言語にTranspileする実験的な試みとして作成されました。
CSSには、 Sass 、Less 、Stylusがあります。JavaScriptでは、しばらくの間最も人気のあるtranspilerはCoffeeScript (2010年頃にリリース)でしたが、今日ではほとんどの人がbabelかTypeScriptを使っています。CoffeeScriptは、言語を大幅に変更(オプションの括弧、重要な空白など)することによってJavaScriptを改善することに焦点を当てた言語です 。
Babelは新しい言語ではなく、すべてのブラウザでは利用できない機能(ES2015以降の機能)を備えた次世代のJavaScriptを古い互換性のあるJavaScript(ES5)に変換するTranspilerです。TypeScriptは、次世代のJavaScriptと本質的に同一の言語ですが、オプションの静的型付けも追加します。多くの人が標準仕様に近いbabelを選択しています。既存のwebpackのビルド手順でbabelを使用する方法の例を見てみましょう。まず、コマンドラインからプロジェクトにbabel(これはnpmパッケージです)をインストールします。
$ npm install @babel/core @babel/preset-env babel-loader --save-dev
開発時の依存関係として3つの別々のパッケージをインストールしていることに注意してください - @babel/coreはbabelの主要部分です、 @babel/preset-envはどの新しいJavaScript機能をTranspileするか定義するプリセットです、そしてbabel-loaderはwebpackでbabelを使えるようにするためのパッケージです。次のようにwebpack.config.jsファイルを編集することで、babel-loaderを使用するようにwebpackを設定できます。
webpack.config.jsmodule.exports = { mode: 'development', entry: './index.js', output: { filename: 'main.js', publicPath: 'dist' }, module: { rules: [ { test: /\.js$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'] } } } ] } };構文はわかりにくいですが、幸いなことに編集ことがほとんどありません。webpackに.jsファイル(node_modulesフォルダ内のものを除く)を探して、
babel-loader
と@babel/preset-env
プリセットを使用してbabelの変換を適用するように指示しています。あなたはここで webpack設定構文についてもっと読むことができます。これですべての設定が完了したので、JavaScriptでES2015機能を使用すことができます。これは、index.jsファイル内のES2015テンプレート文字列の例です。
index.jsvar moment = require('moment'); console.log("Hello from JavaScript!"); console.log(moment().startOf('day').fromNow()); var name = "Bob", time = "today"; console.log(`Hello ${name}, how are you ${time}?;)モジュールをロードするためにrequireの代わりにES2015 importステートメントを使うこともできます。これは今日多くのコードベースで見られるでしょう:
index.jsimport moment from 'moment'; console.log("Hello from JavaScript!"); console.log(moment().startOf('day').fromNow()); var name = "Bob", time = "today"; console.log(`Hello ${name}, how are you ${time}?;)この例では、import構文はrequire構文とそれほど違いはありませんが、importはより高度な場合にはさらなる柔軟性を持ちます。index.jsを変更したので、コマンドラインで再度webpackを実行する必要があります。
$ ./node_modules/.bin/webpack
ブラウザでindex.htmlを更新してみましょう。これを書いている時点で、最近のほとんどのブラウザはES2015のすべての機能をサポートしているので、babelが動いたことを見分けるのは難しいかもしれません。IE9のような古いブラウザでテストするか、bundle.jsを検索して変換されたコードの行を見つけることができます。
bundle.js// ... console.log('Hello ' + name + ', how are you ' + time + '?;) // ...ここでは、ブラウザの互換性を維持するために、babelがES2015テンプレート文字列を通常のJavaScript文字列連結に変換したことがわかります。この例はそれほど面白くないかもしれませんが、コードを変換する機能は非常に強力です。より良いコードを書くために、async/awaitのようなJavaScriptの刺激的な新機能を今日から使い始めることができます。コード変換は時に退屈で苦痛に思えるかもしれませんが、多くの人々が新機能を試すことで、過去数年間で言語の劇的な改善をもたらしました。
ほとんどの説明をしましたが、ワークフローにはいくつか未完成の部分があります。パフォーマンスが心配な場合は、バンドルファイルを圧縮する必要があります。既にビルドステップを組み込んでいるので、簡単に実行できます。また、JavaScriptを変更するたびにwebpackコマンドを再実行する必要があります。次はこれらの問題を解決するための便利なツールについて紹介します。
タスクランナーを使用する(npmスクリプト)
JavaScriptモジュールが動くためのビルドステップができたので、ビルドプロセスのさまざまな部分を自動化するツールであるタスクランナーを使用すると効果的です。フロントエンド開発の場合、タスクにはコードの圧縮、画像の最適化、テストの実行などがあります。
2013年、Gruntは最も人気のあるフロントエンドのタスクランナーでした。Gulpはそのすぐ後に続きました。どちらも他のコマンドラインツールをラップするプラグインに依存しています。今日最も人気のある選択は、npmパッケージマネージャ自身に組み込まれているスクリプト機能を使用することです。これはプラグインを使用せず、代わりに他のコマンドラインツールで直接動作します。
webpackを使いやすくするためのnpmスクリプトを書いてみましょう。これは簡単で、package.jsonファイルを次のように変更するだけです。
package.json{ "name": "modern-javascript-example", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "webpack --progress --mode=production", "watch": "webpack --progress --watch" }, "author": "", "license": "ISC", "dependencies": { "moment": "^2.22.2" }, "devDependencies": { "@babel/core": "^7.0.0", "@babel/preset-env": "^7.0.0", "babel-loader": "^8.0.2", "webpack": "^4.17.1", "webpack-cli": "^3.1.0" } }ここで、buildとwatchという2つの新しいスクリプトを追加しました。ビルドスクリプトを実行するには、コマンドラインに次のように入力します。
$ npm run build
コマンドを実行すると、(前に作成したwebpack.config.jsの設定を使用して)webpackを進捗率を示す--progressオプションと、本番用のコードを最小化する--mode = productionオプションを付けて実行します。watchスクリプトを実行するには次のコマンドを実行します。
$ npm run watch
webpackの--watchオプションの代わりに、JavaScriptファイルを検知して自動的にリビルドするために使います。開発時に最適なコマンドです。
node.jsは各npmモジュールパスの場所を知っているので、package.jsonのスクリプトはフルパス./node_modules/.bin/webpackを指定しなくてもwebpackを実行できます。とても便利です1webpack-dev-serverをインストールすることで、さらに便利になります。これは、シンプルなWebサーバーとリロードによる自動反映機能を提供する別のツールです。開発時のみに使う設定でインストールするには、次のコマンドを入力します。
$ npm install webpack-dev-server --save-dev
次に、package.jsonにnpmスクリプトを追加します。
package.json{ "name": "modern-javascript-example", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "webpack --progress -p", "watch": "webpack --progress --watch", "server": "webpack-dev-server --open" }, "author": "", "license": "ISC", "dependencies": { "moment": "^2.19.1" }, "devDependencies": { "@babel/core": "^7.0.0", "@babel/preset-env": "^7.0.0", "babel-loader": "^8.0.2", "webpack": "^3.7.1", "webpack-dev-server": "^3.1.6" } }次のコマンドを実行して開発サーバーを起動できます。
$ npm run server
実行すると、localhost:8080のindex.htmlをブラウザで自動的に開きます(デフォルト設定)。JavaScriptを変更するたびに、webpack-dev-serverはそれ自身のバンドルされたJavaScriptを再構築し、ブラウザを自動的に更新します。新しい変更を確認するためにコードとブラウザの間でコンテキストを絶えず切り替える必要がなく、コードに集中することができるので、これは驚くほど便利で時間の節約になります。
ここでは、簡単な機能を紹介しただけで、webpackとwebpack-dev-serverの両方にたくさんのオプションがあります(詳細はここで読むことができます )。もちろん、SassからCSSへの変換、画像の圧縮、テストの実行など、他のタスクを実行するためのnpmスクリプトを作成することもできます。コマンドラインツールを備えたものは、公平に適用することができます。npmスクリプト自体に関する優れた高度なオプションやテクニックもいくつかあります。Kate Hudsonによる講演は、入門には最適です。
結論
一言で言えばこれがモダンなJavaScriptです。HTMLとJSから、サードパーティのパッケージを自動的にダウンロードするためのパッケージマネージャ、単一のスクリプトファイルを作成するためのモジュールバンドル、将来のJavaScript機能を使用するためのTranspiler、そしてビルドプロセスのさまざまな部分を自動化するためのタスクランナーを使いました。特に初心者には参考になるでしょう。Web開発は、プログラミングに慣れていない人にとっては、起動と実行が非常に簡単だったために、非常に優れた入口でした。しかし今日では、さまざまなツールが急速に変わる傾向があるため、初心者がついていくのは非常に困難です。
それでも、それほど悪くはありません。フロントエンドでも実行可能な方法としてnodeエコシステムを採用することで、状況は落ち着いてきました。パッケージマネージャとしてnpm、モジュールのためのnode requireまたはimportステートメント、そしてタスクを実行するためのnpmスクリプトを使うことにより、多くのメリットがあり、一貫した方法を提供します。これは1〜2年前と比較しても非常に単純化されたワークフローです。
ほとんどのフレームワークにはプロセスを開始しやすくするために、初心者も熟練開発者も使えるツールが付属しています。例えばEmberにはember-cliがあります 。これはAngularのangular-cli 、Reactのcreate-react-app 、Vueのvue-cliなどに大きな影響を与えました。これらのツールはすべて、すぐにコードを書き始められるようにプロジェクトをセットアップします。しかし、これらのツールは魔法のようなものではなく、単にすべてを一貫した作業方法で設定するだけです。そのため、この記事で説明したようにwebpack、babelなどを使用するための特別な設定を行う必要があります。
JavaScriptは急速に変化し進化し続けているので、イライラすることもあるでしょう。時には車輪を再発明しているように思えるかもしれませんが、JavaScriptの急速な進化は、ホットリロード、リアルタイムな構文チェック、およびタイムトラベルデバッグなどの革新を推進するのに役立ちました。今は、開発者になるのは刺激的なタイミングです。この記事があなたの旅をお手伝いするロードマップとして役立つことを願っています!
最後に、2003年以来最高の不条理なユーモア(恐竜がウェブを支配したとき)を提供してきたryanqnorthのDinosaur Comicsに特別な感謝を述べます。
翻訳協力
Author:Peter Jang(https://medium.com/@peterxjang)
Thank you for letting us share your knowledge!記事選定: @takitakis
翻訳/技術監査: @aoharu / koike
Markdown化: @aoharu私達と一緒に記事を作りませんか?
私たちは海外の良質な記事の日本語訳を行なっております。
現在は特にMediumで人気のある記事を複数人の優秀なエンジニアの方の協力を経てQiitaにて記事を公開しています。
ご興味ある方は、Slackチームにご招待いたしますので、
MailやTwitter等でメッセージを頂けますと幸いです。
- 投稿日:2019-07-30T21:22:47+09:00
freezeされたオブジェクトとされていないオブジェクトのパフォーマンスの違い
[Run tests]ボタンを押すと自分の環境でテストできます。
https://jsperf.com/performance-frozen-object/74
「operations per second」ってことは1秒間にどれだけ計算できるかってことだから多い方が早いってことですね。
僕の環境ではfreezeされたオブジェクトの方が72%遅かったです。
環境は[Testing in Chrome 75.0.3770 / Windows 10 0.0.0]です。
- 投稿日:2019-07-30T21:17:43+09:00
連続する数字を配列に代入する方法 JavaScript版
1, 2, 3, ...
みたいに連続する数字を使ってアルゴリズムを工夫したら、for
文少なく書けますよね。Ruby には
Array(1..3)
のように、..
演算子を使うことで簡単に書けます。JavaScript では簡単に書けないのでしょうか?
ES2015での記述を使います。
初級編:
for
文を使う素直な発想をすると、
for
文が理解しやすくて忘れません。const seq = [] for (let i = 1; i <= 3; i++) { seq.push(i) } console.log(seq) // [ 1, 2, 3 ]中級編:
function *
文を使う
async
/await
がデファクトになって、ジェネレータ関数はあまり登場頂くことは少ないのですが、for
文が登場するので分かりやすいですね。const seq = [...(function * () { for (let i = 1; i <= 3; i++) { yield i } })()] console.log(seq) // [ 1, 2, 3 ]上級編: 空配列のインデックスを使う
配列を作成する
Array(n)
コンストラクタを呼び出すと、配列の要素数がn
である空の配列を作成します。console.log(Array(3)) // [ <3 empty items> ]この空配列のインデックスを繰り返すと、良い感じで連続する数字が取得できます。
const seq = [...Array(3)].map((i, index) => index + 1) console.log(seq) // [ 1, 2, 3 ]または、
Array.prototype.keys()
を使う方法があります。const seq = [...Array(3).keys()].map(i => i + 1) console.log(seq) // [ 1, 2, 3 ]
- 投稿日:2019-07-30T20:39:13+09:00
物理エンジン cannon.js の使い方(コードの書き方)で悩んでます。どなたか、お助けくださいませ。
どなたか、お助けください。
物理エンジン Cannon.js
物理エンジン Cannon.js でゲームを作ってみたくて、コードを書いているのですが、
addShape()
の動作がわたしの解釈と異なるのです。うまくいかない。うーむ。body.addShape() の第三引数は Quaternion() じゃないの??
ここに body.addShape() のドキュメントがあるのですが。第三引数は Quaternion って書いてますよねー。
https://schteppe.github.io/cannon.js/docs/classes/Body.html問題のソースコードと実行結果は CodePen で。
ここのコードでは、棒状の Box に立方体や球を追加して、アイスキャンディーのような形をいろいろ作りたいのです。
コードでは、3 x 4 = 12 種類の物体を作っているつもりですが、
左の3つは期待通りですが、それ以外は、、、伸びてます。食い込んでいます。で、めっちゃ跳ねています。
なぜ??ちょっと傾けて登録したくて角度を指定しただけなのに、、、See the Pen Cannon.js addShape() test by yamazaki.3104 (@yamazaki3104) on CodePen.
悩んでいるは CANNON.Quaternion() の角度指定
new CANNON.Quaternion()
の角度指定、、、0なら希望通りに動くが、それ以外の角度だと、、、、伸びてます。食い込みます。めっちゃ跳ねています。呼び側は、位置と角度を変えているだけなんです。
add_shape_quaternion( { x:-15, y: 10, z:-10 }, { x:0, y:1, z:0, w: Math.PI/8 * 0 } ) add_shape_quaternion( { x: -5, y: 10, z:-10 }, { x:0, y:1, z:0, w: Math.PI/8 * 1 } ) add_shape_quaternion( { x: 5, y: 10, z:-10 }, { x:0, y:1, z:0, w: Math.PI/8 * 2 } ) add_shape_quaternion( { x: 15, y: 10, z:-10 }, { x:0, y:1, z:0, w: Math.PI/8 * 3 } )ちなみに、Quaternionの座標軸を変えても、同様の現象が発生します。
test_cylinder.addShape( new CANNON.Cylinder( 1.9, 1.2, 1.0, 5 ), // radiusTop radiusBottom height numSegments new CANNON.Vec3( 0, 0, -0.5 ), new CANNON.Quaternion( _q.x, _q.y, _q.z, _q.w ) // ★最終的にはこの角度指定 _q.w )どなたか、迷えるおっさんに愛の手を、、、間違いをご指摘くださいませー。
よろしくお願いいたします。
- 投稿日:2019-07-30T19:23:25+09:00
ES5変換後のコードを見てTypeScriptのデコレータを理解する
TypeScript・JavaScriptのデコレータは、クラス宣言やそのメソッドなどにアタッチできる特別な宣言です。
公式ドキュメント: https://www.typescriptlang.org/docs/handbook/decorators.html使い方としては
@hoge
のようなものを追加するだけという魔法のような機能なのですが、いまいち動き方のイメージがしづらくないでしょうか?今回はメソッドデコレータを例に、ES5へのトランスパイル後のソースを見つつ、「内部的にどう処理されているのか?」にフォーカスして紹介していきます。
デコレータそのものについては説明していないので、「そもそもデコレータとは?」という方には下記の記事をおすすめします。
TypeScriptによるデコレータの基礎と実践ClassからPrototypeへの変換を見てみる
デコレータの挙動を知るためには、前提としてJavaScriptのPrototypeについて理解しておく必要があります。
ということで、まずは「Class構文をES5へトランスパイルしたときに、どんな感じのPrototypeの構文に書き換えられるか」を追っていきましょう。
TypeScriptのソース
index.tsclass Person { name: string = 'Some Name'; sayHello() { console.log('Hello.'); } sayBye() { console.log('Bye.'); } }トランスパイル後のJavaScript
index.js"use strict"; var Person = /** @class */ (function () { function Person() { this.name = 'Some Name'; } Person.prototype.sayHello = function () { console.log('Hello.'); }; Person.prototype.sayBye = function () { console.log('Bye.'); }; return Person; }());グローバルに定義されているPerson関数がクロージャとして内部で別のPerson関数を返しています。
そして、Person関数のオブジェクトは、prototypeを通じてsayHello
とsayBye
を実行できるような作りになっています。ここで理解しておく必要があるのは、
name
はPersonオブジェクトのプロパティであるが、sayHello
とsayBye
はPersonオブジェクトのメソッドではなく、Person.prototypeオブジェクトのメソッドであるということです。
メソッドデコレータの基本的な動きを解析する
それを踏まえた上で、Personクラスのメソッドにデコレータを追加して、デコレータが内部的にどんな動きをしているのかを確認してみましょう。
TypeScriptのソース
下記は、メソッドデコレータを先ほどのPersonクラスに追加したものです。
デコレータはログ収集に使われることが多いみたいなので、便宜的にloggerという名前にしています。index.tsclass Person { name: string = 'Some Name'; @logger // メソッドデコレータを追加 sayHello() { console.log('Hello.'); } sayBye() { console.log('Bye.'); } } // デコレータ function logger(target: any, key: string) { console.log(target); console.log(key); }メソッドデコレータは、第一引数に
target
、第二引数にkey
、第三引数にdescriptor
(今回は省略)を受け取ります。実行結果
このファイルを実行してみると、
Person { greet: [Function], sayBye: [Function] } sayHelloと出力されます。
第一引数のtarget
にはTypeScript上でのPersonクラスが持っているメソッド、
第二引数のkey
にはデコレータの対象としている関数名(sayHello
)が入ってきます。
(第三引数は今回省略していますが、Object.getOwnPropertyDescriptor
によって取得できるオブジェクトが受け取れます。)ここで注目すべきは、
target
にはname
プロパティは含まれないことです。
この理由は、target
として渡されるものが、「トランスパイル後のPerson関数のPrototype」だからです。解説
この挙動に関しては、デコレータを追加したTypeScriptのトランスパイル後コードを見てみれば分かります。
index.js"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var Person = /** @class */ (function () { function Person() { this.name = 'Some Name'; } Person.prototype.sayHello = function () { console.log('Hello.'); }; Person.prototype.sayBye = function () { console.log('Bye.'); }; // ↓↓↓ここに注目↓↓↓ __decorate([ logger ], Person.prototype, "sayHello", null); return Person; }()); function logger(target, key) { console.log(target); // Person { greet: [Function], sayBye: [Function] } console.log(key); // sayHello }
__decorate
関数が定義され、Person関数内で実行されていますね。
そして、Person.prototype
がその第二引数に入っています。
__decorate
関数自体は、ごちゃごちゃといろいろ書いてありますが、今回の理解に必要な部分だけ抜き出すと、var __decorate = function (decorators, target, key, desc) { for (var i = decorators.length - 1; i >= 0; i--) { decorators[i](target, key); } }こんな感じになります。
今回の例で言えば、下記の箇所で行われていることは、
__decorate([ logger ], Person.prototype, "sayHello", null);要するにこれと同じということになります。
logger(Person.prototype, "sayHello");だから、第一引数の
target
にはPersonクラスのメソッドのみを持ったオブジェクト(つまりトランスパイル後のPerson.prototype
)が入り、第二引数のkey
にはメソッド名が入ってくるということですね。ちなみに今回は省略した第三引数の
descriptor
についても、__decorate
関数内で実行されているObject.getOwnPropertyDescriptor(target, key)
の値がそれに当たる部分だということが分かります。なんだ、メソッドデコレータって結局それだけか。という感じですね。
メソッドデコレータの評価順を解析する
デコレータを使うときは評価の順番について理解しておくことが重要ですが、
__decorate
関数の中で何が行われているかだけ把握しておけばそこまで混乱することはありません。
- メソッドデコレータを二つに増やした場合
- Decorator Factoryのパターンを使用した場合
について見ていきましょう。
デコレータを二つに増やした場合
TypeScriptのソース
index.tsclass Person { name: string = 'Some Name'; @logger1 @logger2 // これを追加 sayHello() { console.log('Hello.'); } sayBye() { console.log('Bye.'); } } function logger1(target: any, key: string, desc: PropertyDescriptor) { console.log('This is logger1'); } // これを追加 function logger2(target: any, key: string, desc: PropertyDescriptor) { console.log('This is logger2'); }実行結果
メソッドデコレータが二つの場合、後に書いたデコレータから評価されるようです。
This is logger2 This is logger1解説
例によってES5へのトランスパイル後のソースを見てみましょう。
トランスパイル後は、下記のように、デコレータの関数が記述順に配列に入れられ、それが__decorate
関数の引数となります。index.js// var __decorate = ... var Person = /** @class */ (function () { // function Person() {... __decorate([ // ↓↓↓記述順に配列に入れられる↓↓↓ logger1, logger2 ], Person.prototype, "sayHello", null); return Person; }()); // function logger1(target, key, desc) {... // function logger2(target, key, desc) {...そして、前述したように、メソッドデコレータは本質的には下記のような処理に変換されています。
// 再掲 var __decorate = function (decorators, target, key, desc) { for (var i = decorators.length - 1; i >= 0; i--) { decorators[i](target, key); } }見れば分かる通り、
decorators
配列に入っているものを後ろから実行するような処理になっているので、logger2
、logger1
の順に出力されたということですね。Decorator Factoryのパターンを使用した場合
デコレータを返す関数をDecorator Factoryと呼びます。
デコレータに引数を渡したい場合なんかに使われるのですが、このパターンを使用した場合、ややトリッキーな動きをするので注意が必要です。TypeScriptのソース
index.tsclass Person { name: string = 'Some Name'; @logger1() // ()を追加 @logger2() // ()を追加 sayHello() { console.log('Hello.'); } sayBye() { console.log('Bye.'); } } // Decorator Factoryに変更 function logger1() { console.log('This is logger1 factory'); return function(target: any, key: string, desc: PropertyDescriptor) { console.log('This is logger1'); } } // Decorator Factoryに変更 function logger2() { console.log('This is logger2 factory'); return function(target: any, key: string, desc: PropertyDescriptor) { console.log('This is logger2'); } }実行結果
評価順は、まずDecorator Factoryを上から、そしてデコレータを下から、という感じになります。
This is logger1 factory This is logger2 factory This is logger2 This is logger1解説
デコレータが下から評価されるのは、前述の通りですね。
では、Decorator Factoryが上から評価されるのはなぜでしょうか?これは、トランスパイル後のソースを見れば分かる通り、
__decorate
関数への引数として渡すときに評価されてしまうからです。index.js// var __decorate = ... var Person = /** @class */ (function () { // function Person() {... __decorate([ logger1(), // ここでlogger1のDecorator Factoryが評価される logger2() // ここでlogger2のDecorator Factoryが評価される ], Person.prototype, "sayHello", null); return Person; }()); // function logger1() {... // function logger2() {...つまり、
__decorate
関数への引数として渡す際にまずDecorator Factoryが記述順に評価され、__decorate
関数内でデコレータが記述の逆順に評価される、ということです。トランスパイル後のソースを見れば、評価順に関しても、「なんだ、そういうことだったのか」という感じですね。
今回はメソッドデコレータに絞って解析しましたが、クラスデコレータやプロパティデコレータなども、
__decorate
関数を見れば中でどんな処理がされているかが分かるはずです。予定をドタキャンされていきなり暇になってしまった時などに見てみてください。
- 投稿日:2019-07-30T19:23:10+09:00
TypeScriptの場合はnode系でもrequireではなくimportを使う
TypeScriptの場合はnode系の処理系でもrequireではなくimportを使う
備忘も兼ねて。
javascriptによるModule管理と言うと、webpack利用の場合はimport,node系の場合は余程新しいのを使わない限りはrequireを使うという印象だった。
実際、FirebaseのCloudFunctionのサンプル辺りも、基本はrequireである。が、TypeScriptでCloudFunctionを書こうと思ったら
コンパイルエラーにはならないもののimportを使えと警告が出てしまった。TypeScriptにrequireはない
原理としては単純で、TypeScriptにrequireはないだけだった。
requireでも一応動くのは、TypeScriptがJavaScriptのスーパーセットだからで、全てanyと見做して動かしているためである。
実際、VSCodeでrequireを利用した場合と、importを利用した場合の挙動を比べてみると、
前者では型情報が取得できないためにコードアシストの類がほとんど効かない。ちなみに、コンパイル後はimportで書いてもきちんとrequireに変換されていた。
まとめ
- TypeScriptの場合は基本importでOK
- requireでも動くが型情報が落ちるのでTypeScriptのメリットがなくなる
- 投稿日:2019-07-30T18:00:49+09:00
【JavaScript修行】React,ステータスについて
https://ja.reactjs.org/docs/state-and-lifecycle.html
https://qiita.com/morrr/items/c32a4916d55373b64c70
https://mae.chab.in/archives/2956
こちらを参考にしながら今日も勉強いたします
タイマーの分割
function tick() { const element = ( <div> <h2>タイマー</h2> <p>{new Date().toLocaleTimeString()}.</p> </div> ); ReactDOM.render( element, document.querySelector('body') ); } setInterval(tick, 1000);setIntervalで1秒ごとにtickコンポーネントを更新しています。
これを分割したいのですが、今まで学んだことを使うとfunction Clock(props){ return( <div> <h2>タイマー</h2> <p>{props.date.toLocaleTimeString()}.</p> </div> ); } function tick(){ ReactDOM.render( <Clock date={new Date()} />, document.querySelector('body') ); } setInterval(tick, 1000);でもこれだと、時間処理を毎秒更新する昨日をClockに持たせることができていません。
Clockが自身を更新できるようにしたいです。
そのためにはまず関数コンポーネントからクラスコンポーネントに書き換えます。class Clock extend React.Component { render(){ return( <div> <h2>タイマー</h2> <p>{this.props.date.toLocaleTimeString()}.</p> </div> ); } }クラス変換で追加したrenderメソッドは更新が発生するたびに呼ばれる。
state
state・・・内部で状態を保持できる。
class Clock extends React.Component { constructor(props) { super(props); this.state = {date: new Date()}; } render() { return ( <div> <h2>タイマー</h2> <p> {this.state.date.toLocaleTimeString()}.</p> </div> ); } } ReactDOM.render( <Clock />, document.querySelector('body') );・クラスコンポーネントの場合、引数をpropsにして、super(props)を呼び出す。
・stateの初期状態をコンストラクタで設定する。これでdateの情報をClockがもてるようになりますた。
しかし、時間の更新の機能もコンポーネントに持たせたいですね。ライフサイクルイベント
まずは用語から。
マウント・・・コンポーネントが最初に描画される時。
アンマウント・・・コンポーネントから生成したDOMが削除される時。上記のような状態の変化(ライフサイクル)に合わせて発火するライフサイクルメソッドがReactには準備されている。
メソッド名 発火地点 componentWillMount() マウントされる直前 componentDidMount() マウントされた直後 componentWillReceiveProps() propsが変化した時 shouldComponentUpdate() state,propsが変更され再描画が走る前。trueで再描画される。 componentWillUpdate() 再描画される前 shouldComponentUpdate()がfalseで発火しない componentDidUpdate() 再描画されたタイミング 再描画後のDOMにアクセス可 https://www.to-r.net/media/react-tutorial09/
追加するメソッド①componentDidMount(){ this.timerID = setInterval( ()=> this.tick()こちらはマウント直後に発火します。
tick()
メソッドは後ほど記述します。
this.props
やthis.state
以外なら、this上に任意のデータを格納してOKです。追加するメソッド②componentWillUnmount() { clearInterval(this.timerID); }こちらはマウントされる直前に発火します。
これで前のsetIntervalをクリアできますね。追加するメソッド③tick(){ this.setState({ date:new Date() }); }
setState
・・・class componentのメソッド(なのでthisで呼び出せる)第一引数には更新後のstateの値をオブジェクト or 関数で渡す。
なので、流れとしては、
- componetDidMount()で1秒毎にtick()を呼ぶようにセット
- tick()でstateのdataを新しい時間になるように変更
- stateの変更によってrender()が走る。時間が描写される。
- 今後DOMからClockが削除される場合は、componentWillUnmount()が走ってタイマーが停止する。
ちょっとややこしいのでゆっくり追って理解しました。
class Clock extends React.Component { constructor(props) { super(props); this.state = {date: new Date()}; } componentDidMount() { this.timerID = setInterval( () => this.tick(), 1000 ); } componentWillUnmount() { clearInterval(this.timerID); } tick() { this.setState({ date: new Date() }); } render() { return ( <div> <h2>タイマー</h2> <p>{this.state.date.toLocaleTimeString()}.</p> </div> ); } } ReactDOM.render( <Clock />, document.querySelector('body') );全部まとめるとこうですね。
ちゃんとタイマー機能をClockに分離することができました。stateについてもっと知る
禁忌!
stateを直接変更してはならない。
this.state.animal = '犬'; //ダメ this.setState({animal:'犬'}); //正しいコンストラクター以外ではsetState()を使うこと!
非同期に注意!
state・propsの更新は非同期に行われることがあるのでそれに注意しないといけない
→this.porpsとthis.state同士を依存させるのはダメthis.setState({ sum: this.state.sum + this.props.increment, }); //非同期だからうまく値が取れない場合がこの場合、
setState()
を以下の記述方法にする。this.setState((state,props)=>({ sum: state.sum + props.increment }));疑問...内部でどんな処理が行われて非同期を回避しているんでしょう。
いつか調べたいと思います。今日はちょっとドキュメント読み解きに手こずったので間違いがあるかもしれません,,,
ご指摘等お気軽にお願いします!
- 投稿日:2019-07-30T16:27:47+09:00
Javascriptの関数の引数でよく見る({ id })ってどういう意味?
VueやReactを書いていると、こんな感じの関数をよく見ます。
sample.vuemethods: { userData({ id }) { // 処理 } }Vuexのactionでも({ commit })という記述をよく見ますよね。
正直、初めて見たときは意味不明でした。引数に波括弧ってどういうこと?みたいな。
同じように思っている人も多い(はず)なので、以下でこの独特な記法について解説していきます。
上記の書き方はES6の分割代入の一種っぽい
分割代入とは、以下のようなものです。
const data = { id: 1, name: 'taro' } const { id, name } = data; console.log(id); // 1 console.log(name); // tarodataオブジェクトはidとnameというプロパティを持っています。代入したい変数名とプロパティ名が一致している場合、上記のように書くことで1行にまとめることが可能です。
関数の引数に出てくる波括弧もよく似たもので、上の考え方を少し応用すれば理解できるかと思います。
sample.vue<template> <div> {{ displayUserData(userData) }} // taroと表示される </div> </template> <script> export default { data() { return { userData: { id: 1, name: 'taro' } } }, methods: { displayUserData({ name }) { return name } } } </script>displayUserDataメソッドの引数は({ name })となっています。これは、引数として渡されたオブジェクトのプロパティにnameがあれば、それを利用するという意味です。
このメソッドを使う場合、上記のようにuserDataオフジェクトをそのまま放り込みます。userDataはnameプロパティを持っているので、この場合は'taro'が表示されます。
これは以下の書き方と同義です。
sample.vue<template> <div> {{ displayUserData(userData.name) }} // taroと表示される </div> </template> <script> export default { data() { return { userData: { id: 1, name: 'taro' } } }, methods: { displayUserData(name) { return name } } } </script>displayUserDataの引数から波括弧がなくなり、加えて呼び出し時にuserData.nameと、プロパティを指定しています。
ということは、({ commit })はどういう意味?
Vuexのactionsを定義する際、下記のような書き方をすることが多いかと思います。
sample.jsactions = { somethingAction({ commit }) { commit('somethingMutation') } }実は、上記についてはこのように書くこともできます。
sample.jsactions = { somethingAction(context) { context.commit('somethingMutation') } }Vuexのactionsはdispatchされると、contextオブジェクトを受け取ります。そしてcontextオブジェクトは、以下のようなプロパティを持っています。
{ state, rootState, commit, dispatch, getters, rootGetters }ただ、commitしか使わないケースが大半なので、({ commit })と書くことで簡略化することが多いかと思います。
上記の通り、contextオブジェクトはdispatchプロパティも持っているので、以下のような感じにすればactionから別のactionを呼び出すことも可能です。
actions = { somethingActions({ commit, dispatch }) { commit('somethingMutation') dispatch('otherAction') } }公式ドキュメントにより詳しく書かれているので、もっと詳しく知りたい人はそちらを見てください。。
まとめ
jsフレームワークのconfigファイルとかでもよく出てくる記法なので、理解しておくと便利です。
これらについては、「呼びされるときに呼び出し元からオブジェクトを渡される」ということを知っておくと、より理解しやすいかなと思います。
- 投稿日:2019-07-30T15:42:15+09:00
【JavaScript修行】シンボルって?イテレーターを自作する
本日はJavaScript本格入門の270p〜から学習しています。
参考サイト:https://qiita.com/kura07/items/cf168a7ea20e8c2554c6
https://uhyohyo.net/javascript/16_4.htmlシンボル
ES6から導入された新しいプリミティブ型。
オブジェクトのプロパティのキーにできる。文字列ではない!const obj ={}; const sym = Synbol(); obj[sym] = "mow";キーとして、シンボルのsymが使われています。
キーの特徴はこちらです。
let sym1 = Symbol('sym'); let sym2 = Symbol('sym'); console.log(sym1 === sym2); //falseになる同じsymにも関わらず、比較演算子では異なる結果になっています。
唯一絶対なんですね。
しかも暗黙の型変換が行われないので、文字列/数値になりません。(booleanは除く)console.log(sym1 + ''); //エラーになるsymbolどう使う?
定数指定に便利です。
普通定数っていうとconst MONDAY = 0; const TUESDAY = 1;って感じで書きますが、これにはいくつか問題があります。
・const FRIDAY = 0 などが追加されるとバグに
・数字比較を行うと可読性が悪い
・数値としての利用ができてしまう(識別したいだけなので本来必要ない)そこで
Symbol
の出番です。const MONDAY = Symbol(); const TUESDAY = Symbol();同じ名前でもシンボルはユニーク(唯一絶対無二)なので、これで識別可能です。
プライベートメンバにもなる!
App.jsconst SECRET = Symbol(); //シンボル export default class{ constructior(secret){ this[SECRET] = secret; //symbolで作られたプロパティで初期化を行う。 } checkValue(secret){ return this[SECRET] === secret; //プライベートメンバを扱うメソッド } }private.jsimport App from ./App.js let app = new App('1234'); for(let key in app){ console.log(key); //secretは表示されない } console.log(JSON.stringify(app)); //JSON文字列にしても表示されない console.log(app.checkValue('1234')); //trueただし注意点がいくつかあります。
Object.getOwnPropertySymbols
でアクセスできてしまう- モジュールの中ではSECRETからアクセスできてしまう。
イテレーターって?
イテレーター・・・オブジェクトの内容を列挙するための仕組みを備えたオブジェクト!
組み込みオブジェクト系もfor..of
を使って列挙できますね。let ary = ['いぬ','さる','ねずみ']; for(let item of ary){ console.log(item); }そういった処理が実際内部でどうなっているか探るため、
自分で内部処理を書いてみます。let ary = ['いぬ','さる','ねずみ']; let itr = ary[Symbol.iterator](); let d; while(d = itr.next()){ if(d.done){break;} console.log(d.done); console.log(d.value); }みたこのないメソッドがいっぱいあります!
一個一個みていきます。
Symbol.iterator
・・・Iteratorオブジェクトを返す。
next()
・・イテレーターがもつメソッド。配列の要素を取り出す。
done
・・・イテレータが終端に達したかをブーリアンで返す。
value
・・・次の要素の値を返す。上のコードでは、
d.done
がfalseになるまで(終端に達するまで)
itr.next()
で要素をとり続けています。
for..of
はこのような内部処理でvalueを取り出すシンタックスシュガーとも言えます。イテレーターを自作してみよう!
for..of
文は[Symbol.iterator]メソッド経由でイテレータを取り出していたので、それと同じようにメソッドを実装すればOKです。自作イテレータclass Iterator{ constructor(str){ this.str = str; } [Symbol.iterator](){ let current = 0; let that = this; return{ next(){ return current < that.str.length ? {value: that.str[current++],done:false} : {done:true}; } }; } } let itr = new Iterator(['ごま','さば','食べたい']); for(let item of itr){ console.log(item); }
[Symbol.iterator]()
の内部処理でvalueとdoneが含まれたオブジェクトを返しています。
for..of
で1つ前のコードのような処理が走って、見事オブジェクトの内容を列挙するイテレーターが
作れました!今回ちょっと難しかった...
- 投稿日:2019-07-30T14:44:44+09:00
Elm 0.18だとゼロ除算が実行例外になる
0.18だとこれ実行時例外になるので注意です
> 100 % 0 Error: Cannot perform mod 0. Division by zero error.0.19ならOK
> remainderBy 100 0 0 : Intあら...><
> remainderBy 0 100 NaN : Int > (remainderBy 0 100) + 10 NaN : Int
- 投稿日:2019-07-30T14:37:15+09:00
【Webエンジニア目指している方向け・無料公開】HTML, CSS, JavaScriptが学べる解説動画129本をまとめました!
この2週間で撮ってきたHTML, CSS, JavaScript(基本文法, フロントエンド, バックエンド)の解説動画全てを以下の記事にまとめました^^
https://tsuyopon.xyz/learning-contents/web-dev-movie-list/
動画本数
動画リスト数と解説動画本数は以下のとおりです。
- 動画リスト : 21本
- 解説動画本数 : 129本
- 解説動画の合計時間 : 14時間20分45秒
一般公開している動画なので誰でも無料で観れます!
Webエンジニアを目指している方、参考にどうぞ!
- 投稿日:2019-07-30T14:37:15+09:00
【Webエンジニア目指している方向け】HTML, CSS, JavaScriptが学べる解説動画129本(14時間超え)をまとめました!
この2週間で撮ってきたHTML, CSS, JavaScript(基本文法, フロントエンド, バックエンド)の解説動画全てを以下の記事にまとめました^^
https://tsuyopon.xyz/learning-contents/web-dev-movie-list/
動画本数
動画リスト数と解説動画本数は以下のとおりです。
- 動画リスト : 21本
- 解説動画本数 : 129本
- 解説動画の合計時間 : 14時間20分45秒
一般公開している動画なので誰でも無料で観れます!
Webエンジニアを目指している方、参考にどうぞ!
- 投稿日:2019-07-30T13:44:44+09:00
【Vue.js】v-forの中で外部リンクと内部リンクを混ぜて表示する方法
本記事の内容
v-forを使って要素を繰り返して表示するのはわかったけど、その中身に外部リンクと内部リンクが混ざっている時の実装で少し悩んだので、同じような悩みを抱えている人の助けになる記事です。
前提
・Vue.jsとNuxt.jsを使ったアプリケーションでの実装
・Vuetifyを利用です。
実装
大元のvueファイルの内容(一部抜粋)
(色々省略) <v-btn v-for="link in links" :key="link" color="white" flat round :to="link.url" > {{ link.name }} </v-btn> (色々省略) <script> export default { data: () => ({ links: [ { url: 'hoge', name: '外部リンクへ' }, { url: 'privacy-policy', name: 'プライバシーポリシー'}, { url: 'service-policy', name: '利用規約'}, { url: 'contact', name: 'お問い合わせ'}, { url: 'for_user', name: '出品したい方'}, ] }) } </script>上記の内容では。:to="それぞれのurl"が入ってくる形になっていて、下の4つについてはそれで問題なかったのですが、hogeの部分を https://google.com のような外部リンクに飛ばしたかったので、ここの部分がちゃんと動作するように変更することにしました。
手順としては下記になります。
・もともとあった:to="link.url"を削除
・クリックした時にurlを判定して、特定のものについては外部リンクに遷移するように変更この手順で実装したものが下記になります。
変更後のvueファイルの内容(一部抜粋)
(色々省略) <v-btn v-for="link in links" :key="link" color="white" flat round @click="getCreateUrl(link.url)" > (色々省略) <script> export default { data: () => ({ links: [ { url: 'hoge', name: '外部リンクへ' }, { url: 'privacy-policy', name: 'プライバシーポリシー'}, { url: 'service-policy', name: '利用規約'}, { url: 'contact', name: 'お問い合わせ'}, { url: 'for_user', name: '出品したい方'}, ] }), methods: { getCreateUrl(url) { if(url==='hoge') { location.href="https://google.com/" } else { location.href=`${url}` } } } } </script>これで、無事に動いてくれました。
※他にいいやり方があったらご指摘ください。
- 投稿日:2019-07-30T13:44:15+09:00
コンソールからcontentEditable にする
以下をコンソールから打つべし
document.body.contentEditable = true;
- 投稿日:2019-07-30T12:07:16+09:00
Java学習日記 1日目
初めまして。ミントッパと言います。
現在27歳、大学卒業後5年間製造業やってました。上がらない給料、下がる一方のボーナスに将来の不安を感じ
エンジニアになるべく転職を決意しました。独学でRubyを学び、大したスキルは無いが勉強意欲を評価されて無事内定!
8月よりJavaエンジニアとして働くことになった。エンジニアはアウトプットが大事とのことなので
今日から勉強の記録をしていこうと思う。
- 投稿日:2019-07-30T10:57:34+09:00
【WebGL / GLSL】シェーダー100本ノック 〜 ディストーションエフェクト
シェーダー100本ノック
たいそうなタイトルから始まりましたが。。
自分のGLSL力を向上させるために本企画始動しました。
(一回で終わらないようにしないとな)
「これいいな〜」と思った表現を、
コピペではなくソースコードを見ながら理解していきたい!という趣旨の、
超個人的学習記録です。
みなさま、お手柔らかにお願いします。。ちなみに5月くらいからシェーダーを少しずつ勉強してまして、
下記が学習の記録です。
合わせてご覧いただけると嬉しいです。
- 【WebGL / GLSL】デザイナーにモテるためのWebGL入門(概念編)
- 【WebGL / GLSL】ド文系だけど、行列とかラスタライズとか頑張って理解する
- 【WebGL / GLSL】「The Book of Shaders」のサンプルを、WebGL APIいじるところから頑張って再現する
- 【WebGL / GLSL】GLSLはconsole.log()なんてできないから、「色デバッグ」をするらしい
- 【WebGL / GLSL】webpack,glslify,VS Codeでシェーダーを効率よく開発する!
今回のGOAL
今回作成したものがこちら。
URL: https://glslp-project.jp/distortion文字にホバーすると、背景画像がゆらゆらと切り替わります。
これを実現したいと思ったきっかけが、以下の参考サイトでした。ご覧いただけるとわかるのですが、
キービジュアルの文字の部分をホバーすると背景画像がゆらゆら変わります。
WebGL / GLSLの学習当初は、「シェーダー使ってるんだなあ」ぐらいしか理解できませんでした。実現方法がわからず数日煮詰まっているところ、救世主となるページが現れました。
それが以下のcodropsのページです。WebGL Distortion Hover Effects | Codrops
多少仕様は異なるのですが、概ね最初にあげたサイトとやってることは同じです。
この記事で、ゆらゆら背景画像が変わるこの演出を
「ディストーションエフェクト」と呼ぶことを知りました。
codropsの記事はこのディストーションエフェクトをライブラリとして提供しているのでした。しかし!
このままこのライブラリを使用しても、
仕様が微妙に違うため、最初にあげたサイトの演出を実現できません。
そもそも、
「ライブラリあったんだ!はい、これ使って終わり!」
では何の成長もない!なので、このライブラリのソースコードを読み解き、
「一体このコードは何をしているのか?」を理解することにしました。幸い、GLSLのソースコードはそこまで難しいものではなく、
入門にちょうど良いものだったので、
よい勉強になったというお話です。ディストーションエフェクトってどうやってやるのよ!?
ディストーションエフェクトってどうやってやるのでしょう。
ホバーすることによって黒い画像から指定の画像へ切り替わります。
ここまでは普通ですが、切り替える瞬間に特別な処理を施します。
それは、「displayment画像」と呼ばれる、
特殊な柄の画像を混ぜることです。この一工夫によって、ゆらゆらエフェクトを実現できるわけです。
以上が実装の全体像でした。では次から、JavaScriptとシェーダー記述を、
基礎的な部分から一個ずつ確認していきます!
(three.jsを利用する前提で進めます。)Step.1 シェーダーに画像を渡したい!
まずはシェーダーに画像を渡すところから始めます。
シェーダーに何か情報を送るためには、
「①JavaScript側で情報を定義し送る→②シェーダー側で受け取る」っていう形が基本です。JS側で
new THREE.ShaderMaterial
でテクスチャを定義し、
シェーダー側ではuniform sampler2D u_texture;
のような形で受け取ります。
sampler2D
は、テクスチャを受け取るためのデータの型と思ってください。
以下、説明に必要な該当部分のみ記述します。js側uniforms = { u_texture: {type: "t", value: new THREE.TextureLoader().load(`${texturePath}`)}, //その他の記述。。 }; let material = new THREE.ShaderMaterial({ uniforms: uniforms, //その他の記述。。 });
${texturePath}
は、画像へのパスを入力してください。
続いてシェーダー。fragmentShader.fraguniform sampler2D u_texture;これでテクスチャの受け取りが完了です。
そのままcanvasにテクスチャを表示してみる。
受け渡しが完了したので、そのままcanvasその画像を表示してみます。
そのためのシェーダーは以下の通り。vertexShader.vertvarying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }fragmentShader.frag#ifdef GL_ES precision highp float; #endif varying vec2 vUv; uniform sampler2D u_texture; vec4 _texture = texture2D(u_texture, vUv); gl_FragColor = _texture;コードの説明をする前に、
テクスチャを表示するためにはどうすればいいかを説明します。
そもそも canvasに表示させるためには、
gl_FragColor
で色を出力させる必要があります。ということは、gl_FragColorにテクスチャの色情報を渡せればOKということです。
じゃあテクスチャの色情報を受けとるためにはどうすればいいか?
それがtexture2D
です。
この組み込み関数にテクスチャ、UV座標を入れれば色情報を返してくれます。UV座標ってなによ!?
また知らない単語が出てきました。
UV座標とは、テクスチャ上の座標のことです。テクスチャ座標は以下の二つの特徴があります。
①0〜1の値で正規化されている。
②左下が原点である
(これは前回記事で色デバッグしてわかってたことですね!「【WebGL / GLSL】GLSLはconsole.log()なんてできないから、「色デバッグ」をするらしい」参照!)UV座標は、three.jsが変数としてあらかじめ定義してくれている!!
ここで朗報です。
three.jsを使えば、難しいことを考えずにUV座標を手に入れることができます。
なぜなら、あらかじめ定義されているからです!vertexShader.vertvarying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }上記の
uv
が、three.jsであらかじめ定義されている、UV座標が格納された変数ですね。
これをvUv
に格納して、varying変数でそのままフラグメントシェーダーへ送ります。
これだけ!ちなみに今回は関係ない話ですが、
座標変換のための行列もthree.jsであらかじめ定義されています。
上記で言うとprojectionMatrix
とmodelViewMatrix
がそれに該当します。※座標変換については、過去記事を!
【WebGL / GLSL】ド文系だけど、行列とかラスタライズとか頑張って理解する頂点シェーダーからフラグメントシェーダーへUV座標が送られたので、
これでtexture2D
の関数に渡せば完了!あれ、縦横比崩れてね...???
晴れてcanvasにテクスチャを表示することができました。
ただ一個問題が。画面縮めると、縦横比崩れるやん!
Web開発をしている身としては、
background-size: cover;
みたいな挙動をしてほしいところです。どうしよう。
ここでまた救世主が現れました。
windowサイズいっぱいに広げたPlaneのテクスチャにbackground-size:coverのような挙動をさせる。上記、Qiitaの記事を参考にさせていただきました。
そちらを反映すると、js側uniforms = { u_texture: { type: "t", value: new THREE.TextureLoader().load(`${texturePath}`) }, resolution: { type: "v2", value: new THREE.Vector2() }, imageResolution: { type: 'v2', value: new THREE.Vector2(1024, 512)}, };fragmentShader.fraguniform vec2 resolution; uniform vec2 imageResolution; vec2 ratio = vec2( min((resolution.x / resolution.y) / (imageResolution.x / imageResolution.y), 1.0), min((resolution.y / resolution.x) / (imageResolution.y / imageResolution.x), 1.0) ); vec2 uv = vec2( vUv.x * ratio.x + (1.0 - ratio.x) * 0.5, vUv.y * ratio.y + (1.0 - ratio.y) * 0.5 );
imageResolution
には、テクスチャ画像の縦横サイズを入れてください。これで変数
uv
には縦横比が崩れていないテクスチャ座標が入りました!
ついでに上記コードで正規化されて、原点が左下ではなくテクスチャの真ん中になっています。Step.2 ホバーしてテクスチャを入れ替えたい!
テクスチャを表示することができました。
次なる目標は、ホバーしてテクスチャを入れ替えることです。この画像でいうところの「hover」部分ですね。
これにはもうひとつGLSLの組み込み関数を学ぶ必要があります。
mix
関数でテクスチャを混ぜる!フラグメントシェーダーに、
mix
という関数があります。
これは数学的には線形補間というものらしいのですが、
難しいこと抜きに、字面通り二つのものを混ぜる(mix)と捉えた方がわかりやすいと思っています。
vec4 finalTexture = mix(_texture1, _texture2, 0.5);
というような使い方をします。
最初の引数二つは「テクスチャの色情報」、最後の引数は混ぜる割合です。いま、
_texture1
にはただの黒い画像、
_texuture2
にはhoverした後に表示させる予定の画像の色情報が入っているとします。0.5の割合で混ぜたとき。
つまり、
mix(_texture1, _texture2, 0.5);
ですね。
ちょっと暗くなりました。
50%と50%の割合で混ざっていることが感覚的にわかります。0.1の割合で混ぜた時。
mix(_texture1, _texture2, 0.1);
ほとんど暗くなりました。
黒が強いってことは、
つまり_texture1(ただの黒い画像)
が90%、_texture2
が10%ということですよね!「割合」をいじれば、テクスチャを切り替えられる。
ここでわかったことは、
mix
関数の第3引数をいじれば、
テクスチャを切り替えることが可能ってことです。0に近づけば近づくほど、
_texture1
が強くなりますし、
1に近づけば近づくほど_texture2
が強くなります。ということは,0から1に切り替えれば、テクスチャは切り替わります。
なので、ここで変数を導入しましょう。
名前はなんでもいいのですが、
codropsさんコードリスペクトでdispFactor
と名付けます。
(あとでこの名前にした理由がわかると思います!)そうすると、
mix(_texture1, _texture2, dispFactor);
になりますね。あとはJavaScript側で、
「mouseenterしたらdispFactor
を1に、mouseleaveしたらdispFactor
を0に」すればOK!以下のようにしましょう。
アニメーションさせたいので、TweenMaxを使用します。
(シェーダーに渡すuniform変数って、アニメーションできるんですね。TweenMaxすごい。)js側let mat = new THREE.ShaderMaterial({ uniforms: { dispFactor: { type: "f", value: 0.0 }, }, }); $element.addEventListener('mouseenter', function(e) { TweenMax.to(mat.uniforms.dispFactor, 1.5, { value: 1 }); }); $element.addEventListener('mouseleave', function(e) { TweenMax.to(mat.uniforms.dispFactor, 1.5, { value: 0 }); });fragmentShader.fraguniform float dispFactor; vec4 finalTexture = mix(_texture1, _texture2, dispFactor); gl_FragColor = finalTexture;
$element
はイベントを付与したいDOM要素になります。
関係ある箇所のみ記載しています。
JS側で変数定義して、JS側でイベント処理して数値をアニメーションさせるのがシェーダーのポイントなのですね。これでいい感じにテクスチャが入れ替わりました!
TweenMaxのイージングや秒数をいじればもっと工夫できそうですね。Step.3 uv座標を加工して、ゆらぎを与えたい!
ホバーすることで、テクスチャを入れ替えることに成功しました。
しかし、ただテクスチャを変えるだけでは、シェーダーを使ってる意味がありませんね。
ということで、序盤に話したdisplayment画像の登場です。
下記の画像でいうところの真ん中の画像のやつです。色情報を取得する前に、uv座標を加工する
色々なシェーダーのソースコードを見てきてわかったことがあるのですが、
テクスチャ画像を加工したい場合は、
タイトル通り色情報を取得する前に、uv座標を加工することが定石のようです。より具体的には、
texture2D(u_texture, uv)
する前に、この第二引数uv
を、
四則演算などなどで編集すれば、
さまざまな”ゆらぎ”のエフェクトを与えることができるということです!
ちょっと試しにやってみましょう。uv座標に数値を足してみた。
ちょっと試しにuv座標に0.5を足してみます。
fragmentShader.fragvec2 calcPosition = uv + 0.5; vec4 _texture = texture2D(u_texture, calcPosition); gl_FragColor = _texture;なんじゃこりゃ。
これはこれで前衛的で、何かに使えそうですが。真面目に考えると、左に半分、下に半分平行移動していますね。
引き算するとこれの逆の動きになります。つまり、uv座標に数値を足し引きすると、
平行移動するということになります。なんで0.5で半分移動って、uv座標は「0.0〜1.0」で正規化されているのでした。
あと真ん中を原点にしているので、真ん中を基準に移動していることに注意です。まあこれってつまり高校数学のベクトルの話そのまんまですよね!
足し算・引き算が平行移動っていうのは。
ちなみにさっきuv + 0.5
と記述しましたが、
よりベクトルっぽく書いて
uv + vec2(0.5,0.5);
でも同じです。テクスチャより外の世界を、
fract
関数でリピートさせるテクスチャを平行移動したので、
uv座標が1より大きい値のエリア(下記画像の「?」の部分)が見えるのですが、
ここの表示は、テクスチャの端の色が伸びたように表示されてしまうのですね。ここをなんとかする、、っていうか、マシにする方法があります。
要は、「uv座標は0.0から1.0までなんだから、整数部分は抜きで小数部分だけ返してくれる関数があればいいじゃない!」という発想になります。
それがあるんですね。
GLSL組み込み関数のfract
です。
小数部分を返してくれます!fragmentShader.fragvec2 calcPosition = fract(uv + 0.5); vec4 _texture = texture2D(u_texture, calcPosition); gl_FragColor = _texture;
calcPosition
が小数部分しか返さないようにfract関数を仕込みました。
そうすると。。uv座標が1.0を越えることが無くなったので、
リピートするみたいな感じになりました!!※ただGLSL側でやらなくても、three.js側で
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
と記述すれば、リピートは可能です。uv座標に数値を掛けてみた。
先ほどみた通り、
uv座標が1.0より大きい座標になると、リピートになります。
(※fract
した場合)
なので、掛け算したらその分数値は大きくなるので、
リピート度合いも増えそうです。
では掛け算してみます。fragmentShader.fragvec2 calcPosition = fract(uv * 5.0); vec4 _texture = texture2D(u_texture, calcPosition); gl_FragColor = _texture;うわあああ。。
やっぱり予想通り、かなりリピートしましたね。
全部で25個のテクスチャになってますが、
uv * 5.0
はつまりuv * vec2(5.0,5.0)
、
これは縦方向に5リピート、横方向に5リピート、計25回のリピートになったと考えます。1より小さい数値をかけると...?
掛け算の数値が大きくなると、その分1個あたりのテクスチャは小さくなっていきます。
縮小とでもいうべきでしょうか。
じゃあ逆に1より小さい値をかけるとどうなるでしょうか。fragmentShader.fragvec2 calcPosition = fract(uv * 0.5); vec4 _texture = texture2D(u_texture, calcPosition); gl_FragColor = _texture;縮小の逆、
予想通り拡大されました!特定の行列を作用させると、同じ効果を得られる。
拡大・縮小は、座標に行列を作用させる(=座標変換する)ことでも実現できます。
座標変換と行列については、
以前も紹介した下記記事がわかりやすかったです。記事を参考にして、
拡大縮小のために作用させる行列は以下の通り。mat2 scale(vec2 _scale){ return mat2(_scale.x,0.0, 0.0,_scale.y); }なので、普通に0.5を掛け算してた先ほどの計算は、
この行列を作用させても同じ結果を得ることができます。fragmentShader.fragvec2 calcPosition = fract(uv * scale(vec2(0.5))); vec4 _texture = texture2D(u_texture, calcPosition); gl_FragColor = _texture;なんでわざわざ行列を持ち出したかって、
次の回転の概念を理解するためなんですね。
では次!uv座標を回転させてみた。
回転させるためには、
以下の行列を作用させます。mat2 rotate2d(float _angle){ return mat2(cos(_angle),-sin(_angle), sin(_angle),cos(_angle)); }では、ちょっと試してみましょう。
fragmentShader.frag#define PI 3.14159265359 vec2 calcPosition = fract(uv * rotate2d(PI / 4.0)); vec4 _texture = texture2D(u_texture, calcPosition); gl_FragColor = _texture;
PI / 4.0
だけ回転させると、以下のようになります!textureの色情報を混ぜてみた。
混ぜるというと、先ほどの
mix
関数を思い出しますが、
今回はそれよりも前段階のお話で、
uv座標自体に、他のテクスチャの色情報を混ぜてみよう!ということです。ここで、やっとdisplayment画像の登場です。
このテクスチャの色情報を、uv座標と混ぜてみようという試みをします。
以下、フラグメントシェーダーの記述です。fragmentShader.fragvec4 disp = texture2D(u_texture2, uv); vec2 calcPosition = uv * vec2(disp.r,disp.g); vec4 _texture = texture2D(u_texture1, calcPosition); gl_FragColor = _texture;
disp
は、displayment画像の色情報が入っています。
これは、rgbaのvec4
の四次元配列です。
そして、uv座標にこのrgba情報のうちdisp.r
とdisp.g
を掛け算します。
それを出力すると。。おお!
だいぶディストーションっぽくなりました!
あとはこれをホバーでアニメーションさせればいいことになります。Step.4 ディストーションエフェクト総仕上げ!
ここまできたら、もうあとは知識を組み合わせるだけです!
あらためて目標の動きを確認します。
なんだかんだ、displayment画像でテクスチャが"揺らぐ"のはホバーしている間の数秒です。
なので、ホバーが終わった瞬間には、"揺らぎ"は無くなっていなければなりません。
ということは、「ホバーしたら揺らぎがなくなる」という処理が必要です。ホバーにかかわる変数がひとつありました。
dispFactor
ですね。
これを使用します。fragmentShader.fragvec2 calcPosition = uv + rotate2d(PI) * vec2(disp.r,disp.g) * (1.0 - dispFactor) * 0.1; vec4 _texture = texture2D(u_texture, calcPosition); gl_FragColor = _texture;ポイントは
vec2 calcPosition = uv + rotate2d(PI) * vec2(disp.r,disp.g) * (1.0 - dispFactor) * 0.1;
の部分ですね。
この計算式の意味を読み解きます。
uv + ■
というように、二つの塊としてみてください。
ホバーする、すなわちdispFactor
が1.0になるとき、■は0.0になります。
そうなると、uv
のみになるので、ただテクスチャを出力するだけになります。なので、最初はdisplayment画像が混ざっている状態になりますが、
ホバーすると徐々に普通のテクスチャ戻るアニメーションができあがります。あとは、
mix
でこのテクスチャと黒いテクスチャを混ぜ合わせれば完成です!
以下、完成フラグメントシェーダーコードです。fragmentShader.frag#ifdef GL_ES precision highp float; #endif varying vec2 vUv; #define PI 3.14159265359 uniform sampler2D u_texture1; uniform sampler2D u_texture2; uniform sampler2D u_texture3; uniform vec2 resolution; uniform vec2 imageResolution; uniform float time; uniform float dispFactor; mat2 rotate2d(float _angle){ return mat2(cos(_angle),-sin(_angle), sin(_angle),cos(_angle)); } void main() { vec2 ratio = vec2( min((resolution.x / resolution.y) / (imageResolution.x / imageResolution.y), 1.0), min((resolution.y / resolution.x) / (imageResolution.y / imageResolution.x), 1.0) ); vec2 uv = vec2( vUv.x * ratio.x + (1.0 - ratio.x) * 0.5, vUv.y * ratio.y + (1.0 - ratio.y) * 0.5 ); vec4 disp = texture2D(u_texture3, uv); vec2 calcPosition = uv + rotate2d(PI) * vec2(disp.r,disp.g) * (1.0 - dispFactor) * 0.1; vec4 _texture1 = texture2D(u_texture1, uv); vec4 _texture2 = texture2D(u_texture2, calcPosition); vec4 finalTexture = mix(_texture1, _texture2, dispFactor); gl_FragColor = finalTexture; }まとめ
さすがに「初学者なので。。」とか言い訳はできなくなってきたのですが、
もし間違いの箇所などあればご教示いただけると幸いです!終わり!
- 投稿日:2019-07-30T10:52:56+09:00
フロントJavaScriptからメール送信できるSmtpJS (v3)
サーバーレスでのwebサイトで問い合わせフォームを作りたい
自分はQiita初投稿のひよこエンジニアなのですが、先日webサービスを新たに開発することになり、インフラ系知識ないし運用しんどいなぁ、、と思ったのでFirebaseを活用して開発することにしました。
その際、小規模とはいえサイトを公開しようとするとお問い合わせフォームがやっぱり必要になるので、なんとかそこもフロント完結しようと探したところ、SmtpJSが非常に簡単だったので使ってみました。
意外と日本語の情報が少なく感じた(特にv3が出てから)ので、記事にすることにしました。使い方
SmtpJSにアクセスします。
自分のようにクソほど英語がわからなくて困らないくらい簡潔にガイダンスされているので基本的に説明不要なんですが、まず、以下をHTMLに貼り付けます(もちろんダウンロードしてもOK)。<script src="https://smtpjs.com/v3/smtp.js"></script>あとはメールを送りたい箇所で、Email.sendのメソッドを呼び出すだけです。以下は公式サイトのサンプルそのままです。
Email.send({ Host : "smtp.yourisp.com", Username : "username", Password : "password", To : 'them@website.com', From : "you@isp.com", Subject : "This is the subject", Body : "And this is the body" }).then( message => alert(message) );メールサーバーも簡単に用意できる
いや、もうひとつありました。メールサーバーの準備。
これもSmtpJSがナビゲートしてくれていまして、SmtpJSのページの「No SMTP server?」のボタンから飛べるElastic Emailというサービスで簡単に用意することができます。
アカウントができたら、Account > Settings のページを開いて、必要な情報を取得します。前述のEmail.sendの「Host」のプロパティにSettingsページの「Server」の項目(つまりsmtp.elasticemail.com)を、「Username」「Password」のプロパティにSettingsページの「User name」「Password」の項目を設定します。
あとは当たり前ですが、「To」は送信先のメールアドレス(お問い合わせフォームだったら送り先は自分ですね)、「From」は送信元のメールアドレス、「Subject」がタイトルで「Body」が本文。
「From」はいい加減なアドレスだとダメなようです。v2ではいけていた気がしますが、v3でセキュリティ強化されたためと思われます。セキュリティ対策もOK
フロントJSで使う以上、先の「Password」にパスワードをべたっとコピペすると丸見えになってしまいますが、SmtpJS v3ではそこも対応してくれています。
「Security」のところに「SMTPの資格情報を世界中に公開したくないだって?ふっ、私たちが対策を考えてあげたよ」みたいなことが書いてあって、「Encrypt your SMTP Server」のボタンからトークンが生成されます。ドメインを入力させられるので、トークンが知られても入力したドメイン以外からは使えないということでしょうね。
おわりに
v2ではセキュリティ面がややゆるく、Gmailにやすやすとブロックされたりして少し不安だったSmtpJSですが、v3でかなり初心者にとっても使いやすくなったのではないかと思います。
ちなみに実際に作ったサービスがこちらです(小規模サロン向け予約受付システム)。
https://suzume5489.com/ほぼweb経験値ゼロからスタートして、Qiitaにめちゃくちゃお世話になってようやく形になったので、ひよこながらも開発中に得た知見で他の方の役に立ちそうなトピックを投稿していこうと思います!
- 投稿日:2019-07-30T09:45:36+09:00
nuxt generate + S3 + CloudFront + Lambda Edge で静的サイト構築&ハマりポイントと解決法
Repsona LLCの代表兼エンジニア(ひとり)の、ガッシーです。ひとりで、「理想のタスク管理ツール」Repsona(レプソナ)を作っています。
前回の記事(Nuxt + Sails + TypeScript + Fargateでタスク管理ツールを作ったら快適だった話)でRepsona本体側の全体像をざっくりと書きました。
今回はウェブサイトの構成と、ハマったところと解決法を紹介します。
アーキテクチャ
nuxt generate
で静的ファイル生成- gulpでS3にデプロイ & CloudFront invalidate
- Lambda Edge でオリジンパスをハンドリング
前提
- ドメイン取得済み、Route 53 設定済み、ACM設定済み
- Nuxt環境構築済み、サイト作成済み、
nuxt generate
できる構築手順
公式ドキュメント通りやればよし!なんですが、公式が「シークレットキーを記載したdeploy.shを作って.gitignoreする」というなんだか微妙なかんじなので、ちょっとアレンジした手元の手順を紹介します。
- S3 バケットを作成する
- CloudFront distribution を作成する
- Route 53 を設定する
- Lambda Edge でオリジンパスのハンドリングを設定する
- セキュリティアクセスを設定する
- ビルドスクリプトを作成する
- CloudFront invalidate のスクリプトを修正する
- デプロイして確認する
S3 バケットを作成する
バケットを作ります。全部デフォルト設定のままでよかったはず。CloudFront経由のアクセスのみ許可するので、
静的ウェブホスティング
も無効
でOKです。
CloudFront distribution を作成する
- Create Distribution > Web - Get Started
- Origin Domain Name > さっき作ったバケットを選択 (プルダウンにでてくる)
- Origin Path > (空白)
- Origin ID > (勝手に入る値)
- Restrict Bucket Access > Yes
- Origin Access Identity > 初めてなら Create a New Identity
- Grant Read Permissions on Bucket > Yes, Update Bucket Policy
- Alternate Domain Names > ドメイン名
Repsonaの場合repsona.com www.repsona.com
- SSL Certificate > Custom SSL Certificate (example.com): (プルダウンにでてくる)
- まだ作ってなければ Learn more about using ACM.
- 他項目は各自の都合に合わせてください(デフォルトでもよかったはず)。
Route 53 を設定する
管理下にあるドメインに「レコードセットの作成」から下記の設定を入れて、CloudFront distribution にまわしてやります。
Repsonaの場合repsona.com. タイプ: A エイリアス先: さっき作った CloudFront distribution (プルダウンにでてくる)Repsonaの場合www.repsona.com. タイプ: A エイリアス先: さっき作った CloudFront distribution (プルダウンにでてくる)アクセスできるか確認する
ここまでで、青で囲んだ部分が通っているはずです。
S3バケット直下に
index.html
を置いて、https://ドメイン名/index.html
でアクセスできるか確認しておきましょう。Lambda Edge でオリジンパスのハンドリングを設定する
CloudFront には S3 でいうところのインデックスドキュメントにあたるものがありません。
Default Root Object
を設定すれば、以下のindex.html
ナシはいけますが◯: https://ドメイン名/index.html ◯: https://ドメイン名/ ◯: https://ドメイン名以下の
index.html
ナシはいけません。◯: https://ドメイン名/foo/index.html ×: https://ドメイン名/foo/ ×: https://ドメイン名/fooそこで、Lambda Edge を、
イベントタイプ: origin-request
に仕掛けます。index-handler/index.js// arranged: https://github.com/CloudUnder/lambda-edge-nice-urls const config = { suffix: '/index.html', appendToDirs: 'index.html', } const regexSuffixless = /\/[^/.]+$/ // e.g. "/some/page" but not "/", "/some/" or "/some.jpg" const regexTrailingSlash = /.+\/$/ // e.g. "/some/" or "/some/page/" but not root "/" exports.handler = function handler (event, context, callback) { const {request} = event.Records[0].cf const {uri} = request const {suffix, appendToDirs} = config if (suffix && uri.match(regexSuffixless)) { request.uri = uri + suffix callback(null, request) return } if (appendToDirs && uri.match(regexTrailingSlash)) { request.uri = uri + appendToDirs callback(null, request) return } callback(null, request) }Lambda Function のARN(
arn:aws:lambda:us-east-1:000000000000:function:index-handller:1
みないなの)をコピっておいて、CloudFront > Behaviors の下の方にある、 Lambda Function Associations の Lambda Function ARN にセットします。$LATEST
は使えません。アクセスできるか確認する
ここまでで、青枠全部いけました。
S3バケットにfoo/index.html
を置いて、スラありスラなしなど含めて、アクセス確認をしてみます。セキュリティアクセスを設定する
デプロイ用のユーザーに「バケットへのファイル配置」と「CloudFrontのキャッシュ削除」の権限を与えます。以下のポリシーを作成し、デプロイを実行するユーザーにアタッチしてください。ユーザーがない場合はここで作成し、アクセスキーとシークレットキーを取得してください。
※ 公式ママだとうまくいかず、すこし変更しています。{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:PutObject", "s3:GetObjectAcl", "s3:GetObject", "s3:AbortMultipartUpload", "s3:ListBucket", "s3:DeleteObject", "s3:PutObjectAcl", "s3:ListMultipartUploadParts" ], "Resource": [ "arn:aws:s3:::さっき作ったS3バケット名/*", "arn:aws:s3:::さっき作ったS3バケット名" ] }, { "Effect": "Allow", "Action": [ "cloudfront:ListInvalidations", "cloudfront:GetInvalidation", "cloudfront:CreateInvalidation" ], "Resource": "*" } ] }ビルドスクリプトを作成する
Gulp をインストールする
npmの場合npm install --save-dev gulp gulp-awspublish gulp-cloudfront-invalidate-aws-publish concurrent-transform npm install -g gulpyarnの場合yarn add -D gulp gulp-awspublish gulp-cloudfront-invalidate-aws-publish concurrent-transform yarn global add gulp
gulpfile.js
を作成する※ 公式ママだとうまくいかず、すこし変更しています。
const gulp = require('gulp') const awspublish = require('gulp-awspublish') const cloudfront = require('./modules/gulp-cloudfront-invalidate-aws-publish') const parallelize = require('concurrent-transform') // https://docs.aws.amazon.com/cli/latest/userguide/cli-environment.html const config = { // 必須 params: {Bucket: process.env.AWS_BUCKET_NAME}, accessKeyId: process.env.AWS_ACCESS_KEY_ID, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, // 任意 deleteOldVersions: false, // PRODUCTION で使用しない distribution: process.env.AWS_CLOUDFRONT, // CloudFront distribution ID region: process.env.AWS_DEFAULT_REGION, headers: {'x-amz-acl': 'private' /*'Cache-Control': 'max-age=315360000, no-transform, public',*/}, // 適切なデフォルト値 - これらのファイル及びディレクトリは gitignore されている distDir: 'dist', indexRootPath: true, cacheFileName: '.awspublish.' + environment, concurrentUploads: 10, wait: true, // CloudFront のキャッシュ削除が完了するまでの時間(約30〜60秒) } gulp.task('deploy', function () { // S3 オプションを使用して新しい publisher を作成する // http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#constructor-property const publisher = awspublish.create(config, config) // console.log(publisher) let g = gulp.src('./' + config.distDir + '/**') // publisher は、上記で指定した Content-Length、Content-Type、および他のヘッダーを追加する // 指定しない場合、はデフォルトで x-amz-acl が public-read に設定される g = g.pipe(parallelize(publisher.publish(config.headers), config.concurrentUploads)) // CDN のキャッシュを削除する if (config.distribution) { console.log('Configured with CloudFront distribution') g = g.pipe(cloudfront(config)) } else { console.log('No CloudFront distribution configured - skipping CDN invalidation') } // 削除したファイルを同期する if (config.deleteOldVersions) { g = g.pipe(publisher.sync()) } // 連続したアップロードを高速化するためにキャッシュファイルを作成する g = g.pipe(publisher.cache()) // アップロードの更新をコンソールに出力する g = g.pipe(awspublish.reporter()) return g })
package.json
に追記する
yarn run deploy
でproductionデプロイできるようにscriptsを追加する。{ "scripts": { "deploy": "rm -r ./dist; cross-env NODE_ENV=production nuxt generate; cross-env NODE_ENV=production gulp deploy", },環境変数を設定する
.env等AWS_BUCKET_NAME = バケット名 AWS_CLOUDFRONT = 14文字の大文字のID AWS_ACCESS_KEY_ID = アクセスキー AWS_SECRET_ACCESS_KEY = シークレットキー AWS_DEFAULT_REGION = リージョン(us-east-1みたいなの)CloudFront invalidate のスクリプトを修正する
Lambda Edge でオリジンパスのハンドリングをするおかげで、スラありスラなしでアクセス可能ですが、全て別々のリソースとしてCloudFrontにキャッシュされてしまいます。デプロイ時に同時にinvalidateしたいところですが、ここで使っているライブラリ
gulp-cloudfront-invalidate-aws-publish
のindexRootPaths: true
は、スラなしinvalidateをリクエストしてくれないので、本家に手を加えて利用しています(プルリク)。modules/gulp-cloudfront-invalidate-aws-publish/index.js// https://github.com/lpender/gulp-cloudfront-invalidate-aws-publish/blob/master/index.js var PluginError = require('plugin-error') , log = require('fancy-log') , through = require('through2') , aws = require('aws-sdk') module.exports = function (options) { options.wait = !!options.wait options.indexRootPath = !!options.indexRootPath var cloudfront = new aws.CloudFront() if ('credentials' in options) { cloudfront.config.update({ credentials: options.credentials }) } else { cloudfront.config.update({ accessKeyId: options.accessKeyId || process.env.AWS_ACCESS_KEY_ID, secretAccessKey: options.secretAccessKey || process.env.AWS_SECRET_ACCESS_KEY, sessionToken: options.sessionToken || process.env.AWS_SESSION_TOKEN }) } var files = [] var complain = function (err, msg, callback) { callback(false) throw new PluginError('gulp-cloudfront-invalidate', msg + ': ' + err) } var check = function (id, callback) { cloudfront.getInvalidation({ DistributionId: options.distribution, Id: id }, function (err, res) { if (err) { return complain(err, 'Could not check on invalidation', callback) } if (res.Invalidation.Status === 'Completed') { return callback() } else { setTimeout(function () { check(id, callback) }, 1000) } }) } var processFile = function (file, encoding, callback) { // https://github.com/pgherveou/gulp-awspublish/blob/master/lib/log-reporter.js // var state if (!file.s3) { return callback(null, file) } if (!file.s3.state) { return callback(null, file) } if (options.states && options.states.indexOf(file.s3.state) === -1) { return callback(null, file) } switch (file.s3.state) { case 'update': case 'create': case 'delete': { let path = file.s3.path if (options.originPath) { const originRegex = new RegExp(options.originPath.replace(/^\//, '') + '/?') path = path.replace(originRegex, '') } files.push(path) if (options.indexRootPath && /index\.html$/.test(path)) { files.push(path.replace(/index\.html$/, '')) files.push(path.replace(/\/index\.html$/, '')) // スラなしも invalidate してほしい } break } case 'cache': case 'skip': break default: log('Unknown state: ' + file.s3.state) break } return callback(null, file) } var invalidate = function (callback) { if (files.length == 0) { return callback() } files = files.map(function (file) { return '/' + file }) cloudfront.createInvalidation({ DistributionId: options.distribution, InvalidationBatch: { CallerReference: Date.now().toString(), Paths: { Quantity: files.length, Items: files } } }, function (err, res) { if (err) { return complain(err, 'Could not invalidate cloudfront', callback) } log('Cloudfront invalidation created: ' + res.Invalidation.Id) if (!options.wait) { return callback() } check(res.Invalidation.Id, callback) }) } return through.obj(processFile, invalidate) }デプロイして確認する
それではいってみます!
yarn run deployおめでとう!デプロイがうまくいけば、指定したドメインで、成果物にアクセスできるようになっているはずです。うまく動作したかどうかは、公式によると、下記らしいです。
NOTE: CloudFront invalidation created:XXXX は CloudFront invalidation を行う npm パッケージからの唯一の出力です。それが表示されない場合は、動作していません。
ハマりポイントと解決法
ポリシーがうまく適用されない
公式通りのポリシーをあてているはずなのにうまく行かず、VisualEditorでいろいろといじりながらなんとかうまくいく設定にたどり着きました。
"cloudfront:UnknownOperation"
がダメだったのかな。現状動いていますが、s3:ListBucket
はほんとはリソースわけなきゃいかんかも。デプロイ実行したらでたエラー
[12:32:01] Using gulpfile ~/xxxxxx/repsona-website/gulpfile.js [12:32:01] Starting 'deploy'... Configured with CloudFront distribution [12:32:02] 'deploy' errored after 1.51 s [12:32:02] AccessDenied: Access Denied at Request.extractError (/Users/xxxxxx/xxxxxx/repsona-website/node_modules/aws-sdk/lib/services/s3.js:585:35) at Request.callListeners (/Users/xxxxxx/xxxxxx/repsona-website/node_modules/aws-sdk/lib/sequential_executor.js:106:20) at Request.emit (/Users/xxxxxx/xxxxxx/repsona-website/node_modules/aws-sdk/lib/sequential_executor.js:78:10) at Request.emit (/Users/xxxxxx/xxxxxx/repsona-website/node_modules/aws-sdk/lib/request.js:683:14) at Request.transition (/Users/xxxxxx/xxxxxx/repsona-website/node_modules/aws-sdk/lib/request.js:22:10) at AcceptorStateMachine.runTo (/Users/xxxxxx/xxxxxx/repsona-website/node_modules/aws-sdk/lib/state_machine.js:14:12) at /Users/xxxxxx/xxxxxx/repsona-website/node_modules/aws-sdk/lib/state_machine.js:26:10 at Request.<anonymous> (/Users/xxxxxx/xxxxxx/repsona-website/node_modules/aws-sdk/lib/request.js:38:9) at Request.<anonymous> (/Users/xxxxxx/xxxxxx/repsona-website/node_modules/aws-sdk/lib/request.js:685:12) at Request.callListeners (/Users/xxxxxx/xxxxxx/repsona-website/node_modules/aws-sdk/lib/sequential_executor.js:116:18) error Command failed with exit code 1. info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.Access が Denied であること以外なんもわからん!ポリシーも無事適用できて
put object
もできていただけにかなりハマりました。どうやらS3のアクセス権限設定で、いつからか、ブロックパブリックアクセスがすべてブロックがデフォルトになったようで、そうすると、publicなファイルは配置できません。それで、gulpfile.js
にheaders: {'x-amz-acl': 'private'}
の記述を追加して、privateとしてputするようにしました。https://ホスト名/index.html じゃないと AccessDenied
上述の通り、Lambda Edgeで回避しました。
まとめ
- どこでコケてるかわかりにくいので、確認できるポイント毎に確認すべし
- 構築は結構手間だけど、一度通ってしまえばすごく楽
nuxt generate
で静的ウェブサイトにもコンポーネントの概念を・・すごくイイ- 静的サイト生成なので当たり前だけど、ものすごくはやい
という感じで快適に開発しています。ぜひお試しください。
そして、Repsonaもぜひお試しください。ベータ期間中、制限なく無料で使えます。
チームのための理想のタスク管理ツール | Repsona
- 投稿日:2019-07-30T08:59:23+09:00
【web開発 CSS】ドロップダウンメニュー4作品
はじめに
最近、ずっとほったらかしにしてきたCSSでデザインの勉強。webデザイナーになりたいわけではないので将来どこかweb系に就職して使うかは分からないが、最低限の知識はいるだろうと思って最近サンプルを作っている。入社すぐはフロントサイドとか担当させてもらった時とかに活用できそう。
今回4種類のホバー型ドロップダウンメニューを作成。左からItem1,2,3,4。以下コードの掲載と解説。まずItemの解説をする前にhtmlと共通のcssを以下に記載しておく。なお、こちらには今回の趣旨であるドロップダウンメニューに関するコードだけ記載している。全体のコードはGitHubにあげておく。
html<body> <ul class="sub2"> <li><a href="#">Sub-menu01</a></li> <li><a href="#">Sub-menu02</a></li> <li><a href="#">Sub-menu03</a></li> </ul> <ul class="sub3"> <li><a href="#">Sub-menu01</a></li> <li><a href="#">Sub-menu02</a></li> <li><a href="#">Sub-menu03</a></li> </ul> <div class="sub4_box"></div> <ul class="sub4"> <li><a href="#">Sub-menu01</a></li> <li><a href="#">Sub-menu02</a></li> <li><a href="#">Sub-menu03</a></li> </ul> <header> <section id="title">Title</section> <ul class="menu_bar"> <li class="menu_items"> item1 <div class="underbar"> <ul class="sub1"> <li><a href="#">Sub-menu01</a></li> <li><a href="#">Sub-menu02</a></li> <li><a href="#">Sub-menu03</a></li> </ul> </div> </li> <li class="menu_items"> item2 <div class="underbar"></div> </li> <li class="menu_items"> item3 <div class="underbar"></div> </li> <li class="menu_items"> item4 <div class="underbar"></div> </li> </ul> </header> </body>cssheader{ z-index: 3; /*下からmain,menu,headerの順番*/ position: relative; position: fixed; /*header固定*/ top: 0; left: 0; height: 120px; width: 100%; background-color: #696969; } ul{ z-index: 2; /*下からmain,menu,headerの順番*/ padding: 0; /*左のpadding削除*/ list-style: none; /*デフォルトのちょぼ消す*/ } a{ display: block; font-size: 0.8em; text-transform: uppercase; letter-spacing: .2em; } ul.menu_bar{ height: 40px; padding: 0; /*左のpadding削除*/ padding-top: 10px; margin: 0; display: flex; justify-content: center; /*アイコン、テキストボックスを横方向の中心に*/ }underbarクラス
これは各Itemの下に引いてあるアンダーバーであり、わざわざ各Itemとは別の要素で用意した。この理由は、Itemにborder-bottomを設定した場合にドロップダウンメニューとItemに隙間を開けないようにすると、Itemのborder-bottomとドロップダウンメニューの上部が重なり見栄えが悪くなるから。
Item1
こちらはシンプルなドロップダウンメニュー。メニューの幅はItem1の要素と同じで、表示するときに少し下がりながらfadeinしてくる感じ。
cssul.sub1{ position: absolute; visibility: hidden; /*hover前は非表示*/ opacity: 0; /*透明*/ top: 0; left: 0; width: 100%; background: white; transition: all 0.5s; -webkit-transition: all 0.5s; } ul.sub1 > li > a{ padding: 10px 10px; } .menu_items:nth-child(1):hover > div > ul.sub1{ top: 10px; visibility: visible; /*表示*/ opacity: 1; }hoverの謎
たぶんこれが一番王道のドロップダウンメニューかな?正直これを作っててなんでItemからhoverが外れてドロップダウンメニューにカーソルが写ってもドロップダウンメニューが消えないのかはよくわかっていない。どういう仕組みか分かる方いればご教授願います。
Item2
このドロップダウンメニューもよくwebサイトで見かけるような気がする。
cssul.sub2{ position: absolute; /*横幅をheaderと同じにするためheaderの子要素*/ display: flex; /*横並び*/ justify-content: center; /*アイコン、テキストボックスを横方向の中心に*/ width: 100%; height: 100px; top: 20px; /*画面外*/ left: 0; margin: 0; /*headerとsub2との隙間をなくす*/ background-color: white; } ul.sub2 > li > a{ padding: 30px 40px; }jquery$('header > ul > li:eq(1)').hover(function(){ //hoverした時 $('.sub2').css({ //transformの設定 'transform': 'translate3d(0, 100px, 0)', 'transition': 'all 0.2s', '-webkit-transition': 'all 0.2s' }); },function(){ //hoverが解除された時 //これはitem2からmouseleaveした直後の数回取得する $(':hover').each(function(){ //headerとitem2の間にundefinedがあるため if ($(this).attr('class') != undefined){ if ($(this).attr('class') != 'sub2'){ $('.sub2').css({'transform': 'translate3d(0, 0, 0)'}); } } }); }); $('.sub2').on('mouseleave',function(){ //transformの上書き $('.sub2').css({'transform': 'translate3d(0, 0, 0)'}); });jsとcssでのインデックスの扱い
Item1のcssで書いているように、要素を数えるときcssでは1から始まる。jsは0から始まる。
ドロップダウンメニューの収納
今回Item2,3,4で同じ手法でドロップダウンメニューを収納している。遠回りなやり方かもしれないが他に方法が思いつかなかったのでもっと調べる必要あり。具体的にはjqueryの$(':hover')のところであるが、カーソルがItemから外れた瞬間のカーソルがhoverしている要素がドロップダウンメニュー上であればそのまま表示、それ以外であれば収納、といった感じ。またドロップダウンメニューにはmouseleaveで収納するようになっているため、カーソルがItemかドロップダウンメニューから離れると収納する。
Item3
こちらの他と違う点はドロップダウンメニューの表示・収納時に他の画面も全て動く点。cssに関してはItem2の時と(クラス名がsub3と言う点以外)全く同じなので省略。
jquery$('header > ul > li:eq(2)').hover(function(){ //hoverした時 $('.sub3').css({ //transformの設定 'transform': 'translate3d(0, 100px, 0)', 'transition': 'all 0.2s', '-webkit-transition': 'all 0.2s' }); $('main').css({ //transformの設定 'transform': 'translate3d(0, 100px, 0)', 'transition': 'all 0.2s', '-webkit-transition': 'all 0.2s' }); },function(){ //hoverが解除された時 //これはitem3からmouseleaveした直後の数回取得する $(':hover').each(function(){ //headerとitem3の間にundefinedがあるため if ($(this).attr('class') != undefined){ if ($(this).attr('class') != 'sub3'){ $('.sub3').css({'transform': 'translate3d(0, 0, 0)'}); //mainが動くとz-indexが一番大きくなるため固定してitem1,2が後ろにいかないようにする $('main').css({ 'transform': 'translate3d(0, 0, 0)', 'z-index': '1' }); } } }); }); $('.sub3').on('mouseleave',function(){ //transformの上書き $('.sub3').css({'transform': 'translate3d(0, 0, 0)'}); $('main').css({ 'transform': 'translate3d(0, 0, 0)', 'z-index': '1' }); });z-indexの謎
ここでも正直なぜか理解していないことが発生。それはmainをtranslateで動かした後なぜかmainが画面最上部に表示されてしまうということ。よってz-indexを再設定しないと、Item3のドロップダウンメニューを表示させた後、Item1や2のドロップダウンメニューがmainの後ろ側にいってしまう。ドロップダウンメニューは動かしてもz-index変わらんのになぜ?これも分かる方いれば是非ご教授ください。
Item4
こちらはアニメーションを効かせて見栄えを工夫したもの。
cssul.sub4{ position: absolute; /*横幅をheaderと同じにするためheaderの子要素*/ display: flex; /*横並び*/ justify-content: center; /*アイコン、テキストボックスを横方向の中心に*/ width: 100%; height: 90px; /*sub4_boxよりも小さくすることでmouseleaveを正常に効かせる*/ top: 20px; /*画面外*/ left: 0; margin: 0; /*headerとsub2との隙間をなくす*/ } .sub4_box{ z-index: 2; /*下からmain,menu,headerの順番*/ position: absolute; /*横幅をheaderと同じにするためheaderの子要素*/ width: 100%; height: 100px; top: 20px; /*画面外*/ left: 0; background-color: white; } .sub4_box + ul > li > a{ padding: 30px 40px; }jquery$('header > ul > li:eq(3)').hover(function(){ //hoverした時 $('.sub4_box').css({ //transformの設定 'transform': 'translate3d(0, 100px, 0)', 'transition': 'all 0.2s', '-webkit-transition': 'all 0.2s' }); let time = 0.4; for (let i=0; i<3; i++){ $('body > ul:eq(2) > li:eq('+i+')').css({ //transformの設定 'transform': 'translate3d(0, 100px, 0)', 'transition': 'all '+time+'s', '-webkit-transition': 'all '+time+'s' }); time += 0.3; } },function(){ //hoverが解除された時 //これはitem2からmouseleaveした直後の数回取得する $(':hover').each(function(){ //headerとitem2の間にundefinedがあるため if ($(this).attr('class') != undefined){ if ($(this).attr('class') != 'sub4'){ for (let i=0; i<3; i++){ $('body > ul:eq(2) > li:eq('+i+')').css({ //transformの設定 'transform': 'translate3d(0, 0, 0)' }); } //listが動くのを待つため0.3秒遅らせる setTimeout(function(){ $('.sub4_box').css({'transform': 'translate3d(0, 0, 0)'}); },300); } } }); }); $('.sub4_box').on('mouseleave',function(){ //transformの上書き for (let i=0; i<3; i++){ $('body > ul:eq(2) > li:eq('+i+')').css({'transform': 'translate3d(0, 0, 0)'}); } setTimeout(function(){ $('.sub4_box').css({'transform': 'translate3d(0, 0, 0)'}); },300); });特に難しいことはしていない。強いて言うなら初めてsetTimeout()メソッド使ったくらい。
最後に
今回はドロップダウンメニューを作ってみた。ブロック要素の扱いにだいぶ慣れてきた気がする。あとはいろんなwebサイト参考にしておしゃれなデザインができるようになりたい。
- 投稿日:2019-07-30T08:58:55+09:00
【Programming News】Qiitaまとめ記事 July 29, 2019 Vol.15
筆者が2019/7/29(月)に気になったQiitaの記事をまとめました。昨日のまとめ記事はこちら。
2019/7/21(日)~2019/7/27(土)のWeeklyのまとめのまとめ記事もこちらで公開しております。
Java
Python
- Tips
- Apps
Rails
- Tips
Vue.js
Android
- Tips
Swift
- Beginner
- Tips
Kotlin
- Beginner
- Tips
Flutter
JavaScript
- Beginner
- Tips
Node.js
React
Laravel
Keras
- Beginner
PowerShel
Spark
R言語
MySQL
Azure
AWS
- EC2
- AWS Lambda
- Tips
- AWS CodeStar
Docker
- Tips
- DockerコンテナのロギングにGrafana+Lokiを試してみた
- 最短2文でLibraのテストネットを立ち上げるDockerイメージ作りました
- もっと簡単にDockerでNuxt.jsを始めてみる(続Dockerでローカル環境を汚さずにNuxt.jsを始めてみる)
- Hugo を Docker 上で動作させる
- Nginx版:mkcertを使ってローカル環境でもDockerでも楽々SSL
- Vagrant環境のDockerでJenkinsサーバを構築し、スレーブのWindowsマシンを接続する(前半)
- Vagrant環境のDockerでJenkinsサーバを構築し、スレーブのWindowsマシンを接続する(後半)
- DockerでNuxt.js on TypeScriptを始めてみよう
- DockerでRailsの環境構築
TypeScript
Google Apps Script
機械学習
- Beginner
Raspberry
- Tips
Develop
- Tips
- Meetup
Intellij IDEA
更新情報
Kotlin
- Kotlin入門
Android
Java
IDE
- 投稿日:2019-07-30T07:38:48+09:00
GRPCの非同期処理をPromiseで制御する
GRPCの通信部分をPromiseを使って可読性を意識しながら書いた記事になります。
※GRPC通信に使用しているのはimprobable-eng/grpc-webです概要
以下はGRPC-webを使用してUserの取得、更新、削除を行うサービスのStore部分です。
store/index.jsimport { grpc } from 'grpc-web-client'; import { DeleteUserRequest, GetUserRequest, SetUserRequest, User } from '../ts/_proto/src/user_pb'; import { UserService } from '../ts/_proto/src/user_pb_service'; const defaultUser = { name: 'hoge', followerIdsList: [], info: { description: '', }, }; export const state = () => ({ user: defaultUser, }); export const mutations = { setUser(state, { user }) { state.user = user; }, resetUser(state) { state.user = defaultUser; }, }; export const actions = { getUser({ commit }, { id }) { const client = grpc.client(UserService.GetUser, { host: 'http://localhost:9090', }); const param = new GetUserRequest(); param.setId(id); client.start(); client.send(param); client.onMessage(message => { const pbUser = message.getUser(); const user = pbUser.toObject(); commit('setUser', { user, }); }); }, setUser({ commit, state }, { id }) { const pbInfo = new User.Info(); pbInfo.setDescription(state.user.info.description); const pbUser = new User(); pbUser.setName(state.user.name); pbUser.setFollowerIdsList(state.user.followerIdsList); pbUser.setInfo(pbInfo); const param = new SetUserRequest(); param.setId(id); param.setUser(pbUser); const client = grpc.client(UserService.SetUser, { host: 'http://localhost:9090', }); client.start(); client.send(param); client.onMessage(message => { const pbUser = message.getUser(); const user = pbUser.toObject(); commit('setUser', { user, }); }); }, deleteUser({ commit }, { id }) { const param = new DeleteUserRequest(); param.setId(id); const client = grpc.client(UserService.DeleteUser, { host: 'http://localhost:9090', }); client.start(); client.send(param); client.onMessage(message => { if (message.getResult()) { commit('resetUser'); }; }); }, };全てのActionは通信を行ってその結果をcommitするのみなのですが、上記のようにやっていることは単純でも通信部分のコードは行数が多く冗長になってしまいます。
解決策と懸念点
そこでclient~onMessageのGRPC通信部分を関数に切り出すことにしました。しかし1つある懸念点として、onMessageは非同期で行われるので
client.onMessage(message => { return message });と書くことはできません。またonMessage後にやりたい処理をcallback関数にして通信部分として切り出した関数に渡してあげるのも微妙な感じがします。
そこで切り出した関数部分はPromiseを返すように書きます。index.ts// 中略 export const actions = { async getUser({ commit }, { id }) { const res = await getUserRPC(id); const pbUser = res.getUser(); const user = pbUser.toObject(); commit('setUser', { user, }); }, async setUser({ commit, state }, { id }) { const res = await setUserRPC(id, state.user); const pbUser = res.getUser(); const user = pbUser.toObject(); commit('setUser', { user, }); }, async deleteUser({ commit }, { id }) { const res = await deleteUserRPC(id); if (res.getResult()) { commit('resetUser') } }, }; // ここからが切り出したGRPC通信部分 const getUserRPC = id => { return new Promise(resolve => { const client = grpc.client(UserService.GetUser, { host: 'http://localhost:9090', }); const param = new GetUserRequest(); param.setId(id); client.start(); client.send(param); client.onMessage(message => { resolve(message); }); }); } const setUserRPC = (id, user) => { return new Promise((resolve, reject) => { const pbInfo = new User.Info(); pbInfo.setDescription(user.info.description); const pbUser = new User(); pbUser.setName(user.name); pbUser.setFollowerIdsList(user.followerIdsList); pbUser.setInfo(pbInfo); const param = new SetUserRequest(); param.setId(id); param.setUser(pbUser); const client = grpc.client(UserService.SetUser, { host: 'http://localhost:9090', }); client.start(); client.send(param); client.onMessage(message => { resolve(message); }); }); } const deleteUserRPC = id => { return new Promise((resolve) => { const param = new DeleteUserRequest(); param.setId(id); const client = grpc.client(UserService.DeleteUser, { host: 'http://localhost:9090', }); client.start(); client.send(param); client.onMessage(message => { resolve(message); }); }); }
const res = await...
の部分で切り出した関数の中でresolveされるまで待機されるので、通信処理が意図したところまで終わった時点で初めてconst resに値が入るようになります。上記の書くことでActionsの行数が大幅に削減されますし、また値の戻り値をそのまま使えるという点でonMessageによる非同期処理の複雑さをActionsは意識せず直感的に理解できるコードになります。複数の通信に対応する
今度は1度に複数の通信を行う場合を考えてみたいと思います。
Storeの初期化処理でユーザーの情報と、そのユーザーの設定に関する情報を取得します。store/index.tsimport { grpc } from 'grpc-web-client'; import { GetUserRequest, GetUserConfigRequest } from '../ts/_proto/src/user_pb'; import { UserService } from '../ts/_proto/src/user_pb_service'; const defaultUser = { name: 'hoge', followerIdsList: [], info: { description: '', }, }; const defaultUserConfig = { isPublic: false, isOfficial: false } export const state = () => ({ user: defaultUser, config: defaultUserConfig }); export const mutations = { setUser(state, { user }) { state.user = user; }, setConfig(state, { config }) { state.config = config; }, }; export const actions = { async init({ commit }, { id }) { const getUserResult = await getUserRPC(id); const pbUser = getUserResult.getUser(); const user = pbUser.toObject(); commit('setUser', { user, }); const getUserConfigResult = await getUserConfigRPC(id); const pbUserConfig = getUserConfigResult.getConfig(); const config = pbUserConfig.toObject(); commit('setConfig', { config, }); }, }; const getUserRPC = id => { return new Promise(resolve => { const client = grpc.client(UserService.GetUser, { host: 'http://localhost:9090', }); const param = new GetUserRequest(); param.setId(id); client.start(); client.send(param); client.onMessage(message => { resolve(message); }); }); } const getUserConfigRPC = id => { return new Promise(resolve => { const client = grpc.client(UserService.GetUserConfig, { host: 'http://localhost:9090', }); const param = new GetUserConfigRequest(); param.setId(id); client.start(); client.send(param); client.onMessage(message => { resolve(message); }); }); }前半に紹介したやり方でユーザーの情報とユーザーの設定に関する情報を取得しましたが、この場合問題になるのがこの2つの処理が直列で行われているというところです。複数の処理を1つ1つ待つのはあまりスマートではないので、並列で実行して全ての通信処理が終わったら次の処理を行うというようにしたいです。
これをPromise.allを使って以下のように書くことができます。
store/index.js// 中略 export const actions = { async init({ commit }, { id }) { Promise.all([await getUserRPC(id), await getUserConfigRPC(id)]).then(values => { const getUserResult = values[0]; const pbUser = getUserResult.getUser(); const user = pbUser.toObject(); commit('setUser', { user, }); const getUserConfigResult = values[1]; const pbUserConfig = getUserConfigResult.getConfig(); const config = pbUserConfig.toObject(); commit('setConfig', { config, }); }).catch(e => { // error handling }); }, }; // 中略Promise.allで第一引数の配列に各Promiseを返す関数を入れることでその全てがresolveされてからthenに入るので並列で実行して終わってから値の取得とstateへのcommitを行うことができます。各値の取得からcommitまでの部分も関数に切り出せばより可読性が高いコードになりそうです。
上記のようにPromiseを使うことでActionsのGRPC通信の可読性の向上と複数通信を並列実行を実現できました。また通信部分を切り出した部分をまとめたものを別ファイルにおいておけば通信部分のみをテストすることも可能になるので、そういった部分に関しても恩恵を受けることができそうです。
- 投稿日:2019-07-30T06:57:05+09:00
【GAS】Googleフォームの回答データにindexを割り当てる
はじめに
Googleフォームの回答は新しく行を作って挿入しているため、
左にINDEXの列を作っても、Googleフォームから回答が送られると、下記のようにINDEX列のセルは空白となってしまいます。
↓
GASをいじったことのない方は、チュートリアル記事を用意したので参考にしてみてください。
GASで「Hello,World!」を出力するまでをまとめてみた。実装
タイムスタンプ列の左隣にINDEX列を作成する
この時、A列はタイムスタンプ型に変換されるようになってしまっているため、書式を「自動」に変更する。
スクリプトエディタから新規関数を実装する
機能は、"A列の最終行が空白である時、INDEXを割り振る"というもの。
function myFunction() { var spreadsheet = SpreadsheetApp.openById('<id>'); var sheet = spreadsheet.getSheets()[0]; var range = sheet.getRange(sheet.getLastRow(),1); if(range.isBlank() == true){ range.setValue("=ROW()-1"); } }Googleフォームから回答が送信された時に、上記で実装した関数が起動するように設定する
スクリプトエディタ左上の「編集」→「現在のプロジェクトのトリガー」から新規トリガーを作成する。
結果
適当な回答を送信し、INDEXの値が割り振られるようになったことを確認した。
終わりに
今回の実装を応用すると、Googleフォームの回答データに初期値を持たせることができます。
活用してみてください。
- 投稿日:2019-07-30T01:13:24+09:00
書いて覚えるPromise
Promiseとは?
Promiseは非同期処理の完了or失敗を表すオブジェクト。
- 非同期処理の成功(resolve)、失敗(reject)を表すオブジェクト
then
やcatch
,finally
メソッドを使うことで非同期処理後に関数を実行することができるPromise.all
やPromise.race
を使えば、非同期処理を並列で実行することができるPromiseを使った処理を書いてみよう
resolve(成功)の例
- 非同期処理が成功した場合は、
resolve(結果の値)
を呼ぶ- 非同期処理成功後に行いたい処理は、
then()
メソッドに関数を渡すことで実現できるnew Promise((resolve, reject) => { setTimeout(() => { // 成功する例なので、resolveを返す resolve('非同期処理 成功!!'); }, 3000); }).then((value) => { console.log('=== thenメソッド内 ==='); console.log('resolveされた値: ' + value); }).catch((value) => { console.log('=== catchメソッド内 ==='); console.log('rejectされた値: ' + value); });
- 実行結果
$ node promise.js === thenメソッド内 === resolveされた値: 非同期処理 成功!!reject(失敗)の例
- 非同期処理が失敗した場合は、
reject(Errorオブジェクト)
を呼ぶ- 非同期処理失敗時に実行したい処理は、
catch()
メソッドに関数を渡すことで実現できるnew Promise((resolve, reject) => { setTimeout(() => { // 失敗する例なので、rejectを返す reject(new Error('非同期処理 失敗.....')); }, 3000); }).then((value) => { console.log('=== thenメソッド内 ==='); console.log('resolveされた値: ' + value); }).catch((value) => { console.log('=== catchメソッド内 ==='); console.log('rejectされた値: ' + value); });
- 実行結果
=== catchメソッド内 === rejectされた値: Error: 非同期処理 失敗.....finally 成功しても、失敗しても実行する処理の例
成功しても、失敗しても実行したい処理は、
finally()
に関数を渡すことで実現できる。new Promise((resolve, reject) => { setTimeout(() => { reject(new Error('非同期処理 失敗.....')); }, 3000); }).then((value) => { console.log('=== thenメソッド内 ==='); console.log('resolveされた値: ' + value); }).catch((value) => { console.log('=== catchメソッド内 ==='); console.log('rejectされた値: ' + value); }).finally(() => { console.log('=== finallyメソッド内 ==='); console.log('DONE!!') });
- 実行結果
=== catchメソッド内 === rejectされた値: Error: 非同期処理 失敗..... === finallyメソッド内 === DONE!!非同期処理の並列実行の例
Promise.all
Promise.all()
にPromiseオブジェクトの配列を渡す- 全てresolve(成功)した時に、
then()
が実行される- どれか1つでもreject(失敗)した時は、
catch()
が実行される例えば、2つのAPIを叩いて、2つとも成功したときに何か処理をするみたいなことをしたい場合は、
const postAPI1 = new Promise((resolve, reject) => { console.log('API1を叩く'); setTimeout(() => { resolve('API1成功'); }, 3000); }) const postAPI2 = new Promise((resolve, reject) => { console.log('API2を叩く'); setTimeout(() => { resolve('API2成功'); }, 200); }) Promise.all([postAPI1, postAPI2]).then((values) => { console.log('=== thenメソッド内 ===') console.log(values); });
- 実行結果
API1を叩く API2を叩く === thenメソッド内 === [ 'API1成功', 'API2成功' ]Promise.race
Promise.race()
にPromiseオブジェクトの配列を渡す- 1つでもresolve(成功), reject(失敗)が呼び出されたら、
then
もしくはcatch
が実行されるthen
やcatch
メソッドには最初に完了した処理の値が渡されるconst postAPI1 = new Promise((resolve, reject) => { console.log('API1を叩く'); setTimeout(() => { reject(new Error('API1失敗')); }, 3000); }) const postAPI2 = new Promise((resolve, reject) => { console.log('API2を叩く'); setTimeout(() => { resolve('API2成功'); }, 200); }) // postAPI2の方がsetTimeoutを短めに設定しているので、先に処理が完了する。 // そのためthenの処理が実行される。 Promise.race([postAPI1, postAPI2]) .then((value) => { console.log('=== thenメソッド内 ==='); console.log(value); }).catch((value) => { console.log('=== catchメソッド内 ==='); console.log(value); });
- 実行結果
API1を叩く API2を叩く === thenメソッド内 === API2成功参考