- 投稿日:2020-08-20T23:54:59+09:00
JavaScript(未経験者向け)
JavaScriptとは
ウェブサイトを操作して表示を変化させたいときや、画面を更新せずにサーバと通信したい時に使用します。
ex.いいね機能などJavaとは全く違うよ
Javaというプログラミング言語がありますが、JavaScriptは全くの別言語です。名前が似てるだけで全く別物なので注意です!
基礎文法
●アラートの表示
script.jswindow.alert('こんにちは');テキストはシングルクォート(')かダブルクオート(")で囲みます。
※window.alert()
ブラウザでアラートを表示させるメソッドです。引数としてアラートに表示させる情報を渡します。●コンソールにテキストを表示させる
ブラウザ上で二本指クリック→「検証」を選択→Consoleを選択でコンソールを開く
↓
index.htmlを開いているブラウザをリロード
↓
コンソールにテキストが表示されたら成功!※console.log()
ブラウザのコンソールにテキストを表示させるメソッドです。引数としてコンソールに表示させる情報を渡します。
window.alertと同様に変数を引数に渡すこともできます。Rubyにおけるputsメソッドと同様に使えます。●変数を宣言する
script.jsvar name = 'harada'; console.log(name + 'さん、こんにちは');修正ができたら、もう一度ブラウザをリロードして、「haradaさん、こんにちは」と
表示されていれば成功です。●ES2015(ES6)バージョン以降の変数宣言
ES2015(ES6)バージョン以降は、letとconstを使います。
letの場合script.jslet name = 'mitsui'; console.log(name + 'さん、こんばんは');letは、後で書き換えることができる
constは、後で書き換えることができません条件分岐
if文を使います。
if (条件式1) { // 条件式1がtrueのときの処理 } else if (条件式2) { // 条件式1がfalseで条件式2がtrueのときの処理 } else { // 条件式1も条件式2もfalseのときの処理 }☆条件式は()でくくること
☆条件式の後に続く波括弧{}内の処理が実行されること
☆複数条件を指定する場合は、elseのあとに続けてif文を記述することRubyではelsifですが、JavaScriptではelse ifと書くことを注意です!
num = 36 if num % 15 == 0 puts num.to_s + 'は2と4の倍数です' elsif num % 3 == 0 puts num.to_s + 'は2の倍数です' elsif num % 5 == 0 puts num.to_s + 'は4の倍数です' else puts num.to_s + 'は2の倍数でも、4の倍数でもありません' end配列
JavaScriptにおいても配列の概念があります。
script.jslet list = ['Ruby', 'Ruby on Rails', 'JavaScript', 'HTML', 'CSS']; console.log(list);ブラウザをリロードすると、配列の中身を確認することができます。
1.要素を取得
取り出したい文字列の番号を記述
2.要素数を取得
lengthメソッドが利用できます
3.要素を追加
Pushメソッドが利用できます
4.要素を削除
popメソッドが利用できます
配列の最後の要素を取り除きます。Rubyの場合はpopメソッドの引数に数字を渡すことで「何個分要素を取り除くか」という数を指定できるが、JavaScriptのpopメソッドではそれができません。先頭の要素を取り除くためにshiftメソッドが利用できます
オブジェクト
データのまとまりのこと。オブジェクトは名前と値をセットにしてデータを管理します!
オブジェクトを作成するには波括弧を使用します。オブジェクトの定義
let hoge = { name: 'harada' };script.jslet obj = { name: 'harada', age: 18, address: 'nagoya' }; console.log(obj);変数hogeはnameプロパティを持っています。
プロパティは必ず属性名である「プロパティ名」とデータである「値」をセットで持ちます。
つまり、nameはプロパティ名、'harada'は対応する値です。1.プロパティの値を取得する
let obj = { name: 'mita', age: 30, address: 'tokyo' }; console.log(obj.name);2.プロパティの値を変更する
let obj = { name: 'mita', age: 30, address: 'tokyo' }; obj.name = 'tanaka'; console.log(obj.name);繰り返し処理を実装
for文
for (let i = 0; i < 繰り返す回数; i = i + 1) { // 繰り返す処理の内容 }関数を定義
function文
function 関数名(引数) { // 処理の内容 }関数の戻り値
returnを使う
JavaScriptではreturnを用います!function calc(num1,num2){ return num1*num2; } let num1 = 3; let num2 = 4; console.log(calc(num1,num2));無名関数を定義
JavaScriptにおける関数の定義には2種類で、
一つが関数宣言
二つ目が無名関数を用いたものです。// 関数宣言 function hello(){ console.log('hello'); } // 関数式(無名関数) let hello = function(){ console.log('hello'); }基本編は以上になります。ここまで読んでいただきありがとうございます^^
- 投稿日:2020-08-20T23:03:52+09:00
h:commandButtonでサーバ処理実行後にJavaScriptの処理を実行する方法
- 環境
- CentOS Linux release 7.8.2003 (Core)
- Eclipse IDE for Enterprise Java Developers.Version: 2020-03 (4.15.0)
- openjdk version "11.0.7" 2020-04-14 LTS
- JSF 2.3.9
やりたいこと
- ポップアップで開いた子画面を閉じたい
- 閉じる前にサーバで処理したい
h:commandButtonでサーバ処理実行後にJavaScriptの処理を実行する方法
h:commandButtonのaction属性にサーバ処理を書くh:commandButtonの子要素にf:ajaxを書く(ボタンのサーバ処理はAjaxで実行する)f:ajaxのonevent属性にJavaScriptの関数名を書く- JavaScriptの関数でUI eventをパラメータとして定義する
- UI eventの
statusがcompleteだったら処理を実行する子画面のxhtml<!--省略--> <h:commandButton value="閉じる" action="#{uploadBean.close}"> <f:ajax onevent="windowClose" /> </h:commandButton> <!--省略-->JavaScriptの関数/** * eventが完了していたら画面を閉じる. * @param {Object} event UI event */ function windowClose(event) { if (event.status == 'complete') { window.close(); } }UploadBean.java// ...省略... /** 子画面の[閉じる]ボタン押下処理. */ public void close() { System.out.println("サーバでやりたい処理。"); } // ...省略...子画面で出力されたhtml<input id="j_idt11:j_idt14" type="submit" name="j_idt11:j_idt14" value="閉じる" onclick="mojarra.ab(this,event,'action',0,0,{'onevent':windowClose});return false">調べた情報
f:ajaxの属性
onevent
Ajax処理の起動時に、ブラウザ側で起動するJavaScriptのメソッドを指定する。このメソッドには、サーバーからAjax処理の実行状況を示すデータが渡される。
わかりやすいJava EE ウェブシステム入門 - 秀和システム「実行状況を示すデータ」って何だ?
onevent
The name of the JavaScript function that will handle UI events.
ajax(JSF 2.2 View Declaration Language: Facelets Variant)「UI event」のことのようだが、いろいろあるし・・・
MouseEvent、TouchEvent、FocusEvent、KeyboardEvent、WheelEvent、InputEvent、CompositionEvent といったインターフェイスが、このインターフェイスの直接または間接の子孫です。
UIEvent - Web API | MDNステータスには、
beginとcompleteとsuccessがあるようだが・・・いまいちJavaScriptの処理に渡されるものが何かよくわからない。
誰か教えてほしい。失敗したやり方
サーバ処理は実行されず・・・子画面が閉じた。
子画面のxhtml<!--省略--> <h:commandButton value="閉じる" action="#{uploadBean.close}" onclick="window.close();" /> <!--省略-->⑥JavaScriptを起動しactionも実行するボタン
onclick属性とaction属性を指定する例です。
<h:commandButton value="JS+送信" action="#{bb.next()}" onclick="alert('ボタンが押された')" />
この場合は、JavaScriptのプログラムが先に起動します。action属性のnextメソッドはサーバで実行されるのに対して、JavaScriptはブラウザで実行されるからです。
わかりやすいJava EE ウェブシステム入門 - 秀和システム
- 投稿日:2020-08-20T23:02:32+09:00
Firebase CloudFunctionsで関数名に、ハイフン(-)が使えないときの対処
Firebase CloudFunctionsで - があるエンドポイントを作りたいけど、やり方がわからない
express.jsを使えば特に調べずにできるが、そこまで複雑ではないので、firebaseのみで完結させたい。
頑張って調べてみることにした。
これだと、当然エラーが出ます
exports.hello-world = functions.https.onRequest((request, response) => { response.json({ name: 'first' }) })そんなときは、ブラケット記法だと使ってみてlintでは引っかからなかったけど、firebaseにデプロイするときに弾かれる。
exports['hello-world'] = functions.https.onRequest((request, response) => { response.json({ name: 'first' }) })Running command: npm --prefix "$RESOURCE_DIR" run build > functions@ build /Users/USERNAME/Desktop/PROJECTDIR/functions > tsc ✔ functions: Finished running predeploy script. i functions: ensuring required API cloudfunctions.googleapis.com is enabled... i functions: ensuring required API cloudbuild.googleapis.com is enabled... ✔ functions: required API cloudbuild.googleapis.com is enabled ✔ functions: required API cloudfunctions.googleapis.com is enabled i functions: preparing functions directory for uploading... Error: Function name "hello-world" is invalid. Function names cannot contain dashes.なんで( ;∀;)
結論
exports.hello = { world: functions.https.onRequest((request, response) => { response.json({ name: 'first' }) }), }これが正解なのだそう。
ネストさせるみたいですね。ブログなどの記事を見る前に、githubのissueを先に探しておけばよかったです。
参考文献
- 投稿日:2020-08-20T22:33:45+09:00
GCP: Cloud FunctionsとCloud SQLをVPC経由で接続する方法
この投稿では、Google Cloud Platform(GCP)にて、Cloud FunctionsをCloud SQLに接続する手順をチュートリアル形式で説明します。
構築するインフラ構成は、Cloud FunctionsとCloud SQLをVPCに繋いで、両者が内部ネットワークだけで通信する構成にします:
想定読者
この投稿では様々な技術スタックを使いますが、それぞれの詳しい説明は割愛します。読者としては次のような方を想定しています。
- なんとなく以下が分かる
- Node.jsでサーバサイドプログラミング
- MySQLへのSQLの発行のしかた
- 2週間くらいGCPに触れているGCP初心者
- VPC、Cloud Functions、Cloud SQLの概要はなんとなく把握している
- Cloud FunctionsでHello Worldはしたことがある
- 「サーバレスでSQLを扱いたい!」と考えている人
Cloud FunctionsとCloud SQLをVPC経由で接続する手順
同じVPCにCloud FunctionsとCloud SQLを入れて、接続させるには次の手順を踏んで設定していくことになります:
- VPCネットワークを作る
- VPCに「プライベートサービス接続」のIP範囲を割り当てる
- Cloud SQLのインスタンスを作る
- サーバレスVPCアクセスコネクタを作る
- Cloud Functionsのサービスアカウントに権限を付与する
- Cloud Functionsをデプロイする
手順は、再現性を重視して、スクリーンショット多めで説明します。なので、特にこだわりがなければ、設定値はこの投稿と同じものを使うことをおすすめします。また、既存のプロジェクトではなく、新規に作成したまっさらなプロジェクトでやってみるのがおすすめです。
手順は多いですが、ひとつひとつやっていきましょう。30分ほどあればすべてこなせると思います。
VPCネットワークを作る
まず、VPCネットワークを作ります。
メニューから「VPCネットワーク」を開きます:
VPCネットワークの利用が初めての場合、「Compute Engine API」の画面にリダイレクトされます。その場合は「有効にする」ボタンを押してAPIを有効化してください。
「VPCネットワークを作成」を開きます:
作成画面では、次のように入力します:
- 名前:
my-vpc-1。好きな名前をつけてOK。- Subnet creation mode:
カスタム- New subnet: ゴミ箱アイコンをクリックして消す。今回はサブネットは不要なので、消して構いません。
VPCに「プライベートサービス接続」のIP範囲を割り当てる
VPCネットワークを作ったら、次は「プライベートサービス接続」のIP範囲を割り当てます。このステップは、上で作成したVPCからCloud SQLに接続するために行います。
Cloud SQLのインスタンスは、サービスプロバイダーネットワークという別のVPCにいるため、ユーザが作ったVPCに直接Cloud SQLを配置することができません。「プライベートサービス接続」には、ユーザが作ったVPCとCloud SQLがいるVPCを橋渡しする役割があります。
プライベートサービス接続を有効化するために、上で作ったVPCネットワークの詳細を開きます:
「プライベートサービス接続」のタブを開きます。すると、「Service Networking APIの有効化」について聞かれるので「APIを有効にする」をクリックして有効化します:
続いて「IP範囲の割り当て」を開きます:
入力画面が出るので、次のとおり埋めます:
- 名前:
cloud-sql
好きな名前で構いませんが、用途がわかるような名前がいいでしょう。- IP範囲:
カスタムを選択し、192.168.1.0/24を入力します。
好きなIP範囲で構いませんが、VPCの他のサブネットの範囲と重複するのはNGです。この投稿ではサブネットは作ってませんので気にしなくて構いません。なお、この192.168.1.0/24は「192.168.1.0から192.168.1.255の範囲をCloud SQLに割り当ててあげる」という意味になります。Cloud SQLのインスタンスを作る
ここではCloud SQLのセットアップをしていきます。
メニューから「SQL」を探して開きます:
開いたら「インスタンスを作成」をクリックします:
DBエンジンの選択画面になります。好きなDBを選んでいいですが、ここでは「MySQL」を選ぶことにします:
インスタンスの設定画面が出るので、必要な設定をしていきます。
- ① インスタンスID:
my-database(好きなIDで構いません)- ② rootパスワード: 好きなパスワードを設定します。今回は「生成」を押して自動生成しました。生成した場合は、それをメモして控えておきます。パスワードはCloud Functionsの関数で使うことになります。
- ③ ロケーションのリージョン:
asia-northeast1 (東京)
- ④ 「プライベートIP」にチェックを入れます。
- ⑤ 「関連付けられたネットワーキング」は、作ったVPCネットワークを選択します。
- ⑥⑦ 「マネージド・サービスネットワーク接続」の「IP範囲の選択」は、作っておいたプライベートサービス接続を選択します。
- ⑧ 上を入力し終わったら「接続」を押します。
- ⑨ 「パブリックIP」に入っていたチェックは外します。
今回はお試しなのでマシンスペックなどを最低にして、不必要な費用が発生しないようにします。
- ⑩ 「マシンタイプ」は、「db-f1-micro」を選びます。
- ⑪ 「ストレージの種類」は、「HDD」を選択します。
- ⑫ 「ストレージの自動増量を有効化」のチェックを外しておきます。
また、お試しには不要なバックアップもオフにして、費用が発生しないようにします。
- ⑬ 「バックアップを自動化する」のチェックを外しておきます。
- ⑭ 以上すべてを入力し終えたら、「作成」を押します。
Cloud SQLのインスタンスの作成され、インスタンスの詳細画面にて、そのインスタンスに割り当てられたプライベートIPが確認できます:
今回は、
192.168.1.3が割り当てられました。これはCloud FunctionsからDBに接続するときに使うのでメモしておきます。Cloud SQL側の設定は以上で完了です。
サーバレスVPCアクセスコネクタを作る
ここからはCloud Functionsのための作業になります。
Cloud FunctionsがVPCで通信できるようにするために、サーバレスVPCアクセスコネクタを作っていきます。
メニューの「VPCネットワーク」→「サーバレスVPCアクセス」を開きます:
サーバレスVPCアクセスを使ったことが無い場合、Serverless VPC Access APIを有効にするか聞かれます。「有効にする」をクリックしてこのAPIを有効化してください。
「コネクタを作成」をクリックします:
作成画面が表示されるので、次のとおりに入力していきます:
- ① 名前:
cloudfunctions-connector。 好きな名前で構いませんが、Cloud Functionsをデプロイするときに使うのでメモしておきます。- ② リージョン:
asia-northeast1を選択します。- ③ ネットワーク:
my-vpc-1。本稿の手順で作成したVPCを選びます。- ④ IP範囲:
192.168.2.0。好きな範囲で構いませんが、VPCのサブネットとかぶっていない範囲にする必要があります。この投稿では、my-subnet-1の192.168.0.0/24と、Cloud SQL用の192.168.1.0/24の範囲は使えませんので、それとかぶらない192.168.2.0/28にします。ちなみに、192.168.2.0/28は192.168.2.0から192.168.2.15の範囲になり、これをCloud Functionsのために割り当てるという意味になります。Cloud Functionsのサービスアカウントに権限を付与する
メニューの「IAMと管理」を開きます:
「メンバー」一覧のフィルタに「Cloud Function」などと入力して、「Cloud Functions サービスエージェント」を絞り込みます:
ここでCloud Functionのサービスエージェントが見つからない場合は、Cloud Functions APIを有効にしてから再度試して下さい。
https://console.cloud.google.com/apis/library/cloudfunctions.googleapis.com を開き、ヘッダでプロジェクトを選択し、「APIを有効にする」を押して下さい。絞り込んだら、「Cloud Functionsサービスエージェント」の鉛筆アイコンをクリックします:
権限の設定画面が出るので、「別のロールを追加」をクリックして次の2つのロールを付与します:
- 「Project」→「閲覧者」
- 「Compute Engine」→「Computeネットワークユーザー」
権限の付与は以上です。
Cloud Functionsをデプロイする
続いて、Cloud Functionsに関数を作ってデプロイします。
メニューの「Cloud Functions」を開きます:
関数を作ったことがない場合、Cloud Functionsの一言紹介画面が出るので、そこの「関数を作成」をクリックします。
関数作成画面では、次のように入力します:
- ① Function name:
function-1。好きな名前でかいまいません。- 「トリガー」
- ② リージョン:
asia-northeast1- ③ 認証: 「Allow unauthenticated invocations」を選択します。ここでは、後で
curlで叩いてみたいので、誰でも関数を実行できるようにします。- ④ 「保存」ボタンを押します。
- 「VARIABLES, NETWORKING AND ADVANCED SETTINGS」
「次へ」を押して、コードの入力画面を開きます。
エディタが開くので、まずランタイムとエントリポイントを設定します。
- ① ランタイム: Node.js 12
- ② エントリポイント:
queryDatabases
- ③④ 次に、エントリポイントと同じ関数を
index.jsに実装します。index.jsconst mysql = require("mysql"); /** * @param {import("express").Request} req * @param {import("express").Response} res * @return {Promise<void>} */ exports.queryDatabases = async (req, res) => { const connection = mysql.createConnection({ // hostはCloud SQL作成時に割り当てられたプライベートIP host: "192.168.1.3", port: 3306, user: "root", // passwordはCloud SQL作成時に設定したrootパスワード password: "LMgbiKCAnk1jL0FI", }); try { // DBに接続する await new Promise((resolve, reject) => connection.connect((err) => (err ? reject(err) : resolve())) ); // データベース一覧をクエリする const results = await new Promise((resolve, reject) => connection.query(`SHOW DATABASES`, (err, results) => err ? reject(err) : resolve(results) ) ); // ログにクエリ結果を出す console.log(JSON.stringify({ results })); res.send("OK"); } catch (e) { console.error(e); res.send("ERROR"); } };次に、package.jsonも変更して、
mysqlパッケージへの依存を追記しておきます:package.json{ "name": "cloudfunctions-sql", "version": "1.0.0", "dependencies": { "mysql": "^2.18.1" } }以上、2つのファイルを書き換えたら「デプロイ」を押します。
以上で関数のデプロイ作業は完了です。
関数のデプロイが完了するまでしばらく待ちます。数分かかります。
デプロイした関数を実行してみる
関数のデプロイが完了したら、関数の詳細ページを開き、「トリガー」タブで関数のURLを調べます:
URLをコピーして、
curlで叩いてみます:「OK」というレスポンスが返ってくれば、関数が正常に実行されています。
最後に、関数のログを確認してクエリ結果が取れているか見てみましょう。ログは関数詳細ページの「ログを表示」で見ることができます。
ログを見てみると、データベース名一覧を参照するSQLがうまく動作したことが分かります:
以上で、Cloud FunctionsとCloud SQLをVPC経由で接続する方法の説明は終わりです。お疲れさまでした。
あとかたづけ
この投稿で作成したリソースのうち、Cloud SQLは利用料金が時間単位で発生するので削除しておきましょう。
この投稿のために新規プロジェクトを作った場合は、プロジェクトを削除してしまうのが無駄な出費を抑えるには確実です。
最後までお読みくださりありがとうございました。Twitterでは、Qiitaに書かない技術ネタなどもツイートしているので、よかったらフォローお願いします
→Twitter@suin
- 投稿日:2020-08-20T22:32:00+09:00
コールバック関数とは何か
今回は私のような初心者にとって恐らく最初の壁で、何が起きているのか理解しにくいコールバック関数についてです。
前回2つの記事はES5で書いてたので、今日からES6で書いていきます。
コールバック関数とは何か
関数の引数に渡す関数
※渡される関数は「高階関数」といいます。なので、高階関数の中で実行される関数のことですね。
「関数」の単語ばかりでややこしいですが...実はこれは結構色々なところで見かけます。
例えば、addEventListenerやfind、mapメソッド等ですね。
だから理解は必須で、理解することで何が起こってるかわかりやすくなるということです。早速超絶シンプルな例
let red = function() { console.log('I am red'); } let blue = function(callback) { console.log('I am blue'); callback(); } blue(red); //結果: // I am blue // I am red二つ目のfunctionの引数にcallbackが渡されてますね。
①関数「I am red と表示してね」 をredに代入
②関数「I am blue と表示してね」をblueに代入
この子は関数に引数callbackをもち、そのcallback引数も実行。
③blueの関数が呼び出される時、引数にredを渡す。
この引数callback = red
ちなみにこちらの引数にはわかりやすいように、「callback」の名前を付けてますが、この名前はなんでもいいのです。
ABCとかでも動きます。任意の名前を引数に付ければOK。let red = function() { console.log('I am red'); } let blue = function(abc) { console.log('I am blue'); abc(); } blue(abc); //結果: // I am blue // I am red
次は、よく見る一つの例、mapのコールバック関数について。
mapは、与えられたコールバック関数を配列それぞれに順番に実行し、その結果から新しい配列を作成できるというメソッドですね。
- value : 現在処理中の要素の値
- index : 配列のindex番号
- array : 現在の配列そのもの
詳細はこちらから MDN
mapメソッドを使ってそれぞれの年齢を計算した配列を作ってみます。
const birthYears = [1980, 2000, 1995]; const ages = birthYears.map((value) => { const thisYear = new Date(); return thisYear.getFullYear() - value; }); console.log(birthYears); //結果: [1980, 2000, 1995] console.log(ages); //結果: [40,20,25]mapの引数に、その場で作られた無名関数を渡してます=コールバック関数
①birthYearsに誕生年が格納されてる
②birthYearsに対してmapを実行。
その時その場で作られたコールバック関数も実行。
*今年の年を取得する為Dateオブジェクトも使用してみた
*今年 - 現在の要素の値をリターン
③bithYearsとagesをそれぞれコンソールに表示
以上簡単な二つの例でした。
コールバック関数は非同期処理にも使われていたりするので、
その辺はまた記事にまとめて理解を整理していきます!
- 投稿日:2020-08-20T22:10:38+09:00
生年月日から年齢を計算して表示させる JavaScript PHP
少し前の案件で「会社概要ページの代表者プロフィールに年齢を表記する」ってことがあって、
年齢は毎年変わるから自動で更新させないとなー、と思いつつ面倒くさくてそのまま表記したって事がありました。その事を思い出して、jsとphpで年齢を計算して表示させるってのを作ってみました。
jsかphpのどちらかで実装できるので、好きな方を使ってください。PHPで年齢を計算して表示
"19910402"の部分が生年月日です、年齢を表示させたい人の生年月日に変更してください。
ちなみに1991年4月2日は僕の誕生日です。php年齢は<?php echo floor((date('Ymd') - 19910402) / 10000); ?>歳です。実行結果年齢は29歳です。JavaScriptで年齢を計算して表示
同じく"19910402"の部分が生年月日です、年齢を表示させたい人の生年月日に変更してください。
html年齢は<span id="age"></span>歳です。javascriptfunction calcAge(birthdate) { const today = new Date(); targetdate = today.getFullYear() * 10000 + (today.getMonth() + 1) * 100 + today.getDate(); return (Math.floor((targetdate - birthdate) / 10000)); } document.getElementById("age").innerHTML = calcAge(19910402);実行結果年齢は29歳です。まとめ
現在の日付から生年月日を引いて、1万で割ると年齢が出るらしいです。
数学って不思議。
- 投稿日:2020-08-20T22:00:05+09:00
SPA(Single Page Application)とは?普及した理由を歴史から学ぶ
SPAとは?
「Single Page Application」の略であり、単一のWebページでアプリケーションを構成する設計構造の名称になります。
従来ページの遷移させる際に、ページ全体の更新(次ページの情報取得)が必要でした。
これがページ内の部分的な遷移を可能としたのがSPAであり、これによりページの表示速度や応答速度が向上しました。SPAを用いた具体例
・Google Map
・Slack上記の具体例に上げたGoogle Mapはイメージしやすく、ページ内のマップ部分のみドラッグして動かす事ができます。
これにはAjaxというアプローチ方法が採用されており、一部の情報をサーバーに送信して、それを受け取り反映させる仕組みとなっています。Ajaxとは「Asynchronous JavaScript + XML」の略称で、元々採用されている技術である「DOM」「XML」「JavaScript」等を組み合わせたアプローチ方法です。
その為「新しくて古い技術」と呼ばれており、こういった技術を用いて作成したものがSPAになります。
※「Ajax」はプログラミング言語などの概念ではなく、アプローチ方法という概念になります。SPA(Single Page Application)が普及した理由
Flashを始めとしたRIA(Rich Internet Application)の存在
近年SPAが普及されてきた背景としてはスマートフォンの普及が挙げられます。
従来のWebページはHTML,CSSの構成に対して、動的なアプリケーションを実装する際に「RIA(Rich Internet Application)」と呼ばれる外部プラグインを用いておりました。
代表的なものでいうと「Flash」です。
これにより従来のWebページに動きをつけることができ、表現力が高まったことからRIAは脚光を浴びるようになります。しかし2010年頃からこの「Flash」は衰退していきます。
当時AppleのCEOだったスティーブジョブズが、iPhone OSでAdobeのFlashテクノロジをサポートしない理由を説明するメッセージ「Thoughts on Flash」を発表したことをきっかけにオープン性のあるHTML5を推進するようになりました。簡単に言うとジョブズ氏は、
「Flash」はAdobeが提供しているもので100%プロプリエタリ(独占的)な製品であるが、Webに関連する全ての標準はオープンであるべきだと考え、HTML5やCSS、JavaScriptなどのオープン標準を製品に採用しているということを発表したのです。その後RIAで行っていた処理は、HTML5、CSS3、JavaScriptで実現が可能となり、その概念がSPAとなります。
※2017年7月25日にAdobe Systems社よりFlashのアップデート・配布などのサポートが2020年末に終了することが正式発表されました。
まとめ
時代の流れと共に、技術は移り変わりながらもSPAで用いられている技術はAjaxという元からある技術を組み合わせた構造になっています。
勿論SPAに適したフレームワークや新しい技術等はありますが、技術の組み合わせや使用方法を検討することで、十分にUXの向上を果たせる可能性もあり、Webの世界はまだまだ進化していくことが予想されます。
仮に新しい技術が必要となっても、その根幹を理解するにあたって基礎的な知識は常に求められる為、HTML5,CSS3,JavaScriptはしっかり学んでおく必要がありそうです。参考記事
IT media
https://www.atmarkit.co.jp/ait/articles/0708/23/news134.html
HTML55Experts.jp
https://html5experts.jp/albatrosary/4939/
日経クロステック
https://xtech.nikkei.com/atcl/learning/lecture/19/00034/00003/
- 投稿日:2020-08-20T21:19:43+09:00
【JavaScript関数ドリル】 初級編 join関数の実装
join関数の課題内容
「課題内容」「解説動画」「解答例」を確認したい場合は、以下リンク先のページを参照。
https://js-drills.com/blog/join/
join関数 に取り組む前の状態
- 加えていくメソッドかな?と思いました。
join関数に取り組んだ後の状態
- joinメソッドは、配列の間ごとに第2引数で指定した数字文字を入れていくメソッドです。
join関数の実装コード(答えを見る前)
- joinメソッドの働きを理解して最後まで実装できました。
join関数の実装コード(答えを見た後)
実装完了しました?
function join(array, separator = ',') { const copiedArray = [...array]; let joinedString = copiedArray.shift(); for(let i = 0; i < copiedArray.length; i++) { joinedString += separator + copiedArray[i]; } return joinedString; } console.log( join(['@', '@', '@','@', '@', '@'], '0') );
- 投稿日:2020-08-20T21:02:52+09:00
【JavaScript関数ドリル】 初級編 initial関数の実装
initial関数の課題内容
「課題内容」「解説動画」「解答例」を確認したい場合は、以下リンク先のページを参照。
https://js-drills.com/blog/initial/
initial関数 に取り組む前の状態
- 頭文字が変わるメソッドかなと思いました。
initial関数に取り組んだ後の状態
- initialメソッドは、配列の後ろを出力します。
initial関数の実装コード(答えを見る前)
- initialメソッドの働きを理解していましたが、自力で実装できませんでした。
initial関数の実装コード(答えを見た後)
実装完了しました?
function initial(array) { const copiedArray = [...array]; copiedArray.pop(); return copiedArray; } const numbers = [1, 2, 3]; console.log( initial(numbers) ); console.log('number : ', numbers);
- 投稿日:2020-08-20T20:43:18+09:00
【JavaScript関数ドリル】 初級編 indexOf関数の実装
indexOf関数の課題内容
「課題内容」「解説動画」「解答例」を確認したい場合は、以下リンク先のページを参照。
https://js-drills.com/blog/indexof/
indexOf関数 に取り組む前の状態
- indexofメソッドがどんな働きを持つのかイメージできませんでした。
indexOf関数に取り組んだ後の状態
- 第2引数で指定した番号が第1引数のindex番号にあてはまるときに出力する。
- 第3引数は、検索の開始位置。
- 第2引数が当てはまらないときは-1をはくように設定しました。
indexOf関数の実装コード(答えを見る前)
- 自力で実装はできませんでしたが、indexOfメソッドの働きは理解できていました。
indexOf関数の実装コード(答えを見た後)
実装完了しました?
function indexOf(array, value, fromIndex = 0) { for(let i = fromIndex; i < array.length; i++) { if(array[i] === value) { return i; } } return -1; } console.log( indexOf([1,9,9533,20],20)); console.log( indexOf([1,9,950,555,50],50)); console.log( indexOf([1,9,5,69,3,7,9],9,2)); console.log( indexOf([1,9,5,69,3,7,9],2,2));
- 投稿日:2020-08-20T20:20:09+09:00
【Vue.js】ref属性でマーキングしたコンポーネントを$refsで取得
生成されたコンポーネントを取得できる$refs
取得したいコンポーネントのタグ内に、ref属性を付与することでマーキングし、
実際に$refsでマーキングしたコンポーネントを取得できる。注意点
もちろんすでにレンダリングされたコンポーネントを取得するので、VueライフサイクルにおけるbeforeMount以下のプロパティ内では使用することはできない。
コンポーネントを特定させる
<template> <v-container ref="hoge"> ・・・ </v-container> </template> <script> let hogeComponent = this.$refs.hoge </script>特定したい要素にref属性を付与して、実際の処理する時の対象指定として$refsを使用して特定させる。
配列の要素による、コンポーネントも特定させることができる
<template> <v-container> <v-row> <v-col v-for="item in items" ref="hoge"> </v-col> </v-row> </v-container> </template> <script> let item1 = this.$refs.hoge[0] </script>ref属性の配列の要素の中でも、0番の要素だけを取得できる。
- 投稿日:2020-08-20T19:42:21+09:00
JavascriptでObjectの最後のvalueを取得する方法
const favorite = {music: "HIPHOP", food: "sushi", season: autumn }最後のseasonのValueを取得したい場合
const lastValue = Object.values(favorite)[Object.values(favorite).length - 1]; // "autumn"
- 投稿日:2020-08-20T19:34:04+09:00
コードの中にメモを取る方法(HTML,CSS,JavaScript)
- 投稿日:2020-08-20T18:43:40+09:00
HTML5 勉強メモ(1):JavaScript
MIMEタイプ:text/javascript
<script type="text/javascript"> // ここにjavascriptを記述 </script> <script> // ここにjavascriptを記述、HTML5には既定値として省略可 </script>標準化団体
HTML,CSS,DOM -> W3C
javascript -> ECMA internationl / ECMAScript識別子
- 英字、_、$で始まる
- 2文字以降は数字可
- 予約語以外
柔軟で宣言と違う型を代入可能、動的に型付け
データ型
- primitive(値参照)
- 文字列
- 数字
- 真偽
- null
- undefined
- symbol
- object(アドレス参照)
==
var num1 = new Number(10); var num2 = new Number(20); var str1 = new String("10"); var str2 = new String("20"); console.log(num1 == num2); // flase,object型間の比較がアドレス console.log(num1 == str1); // false,object型間の比較がアドレス console.log(str1 == str2); // false,object型間の比較がアドレス console.log(num1 == 10); // true,object型比較primitime型が、object型->primitime型変換後比較 console.log(str1 == 10); // true,文字列比較数字が、文字列->数字変換後比較NaN、isNaN()
// NaN -> not a number, object type console.log(0 / 0); // NaN console.log(NaN == NaN); // false console.log(isNaN(0/0)); // trueInfinity、isFinite()
// Infinity -> nobject type console.log(Infinity / Infinity); // NaN console.log(Infinity - Infinity); // NaN // その他計算結果はInfinity console.log(Infinity * -1); // -Infinity console.log(isFinite(Infinity)); // falseGC(garbage collection、ガーベージコレクション)
自動で、タイミング決められない
strictモード
- 関数で同じ引数名ができない
- 宣言されてない変数が使用できない
- objectに同じ名前のプロパティーと関数ができない
- delete演算子使用できない
- with使用できない
// 使い方 "use strict"encodeURI()
- encodeURI() // URLに使用できない文字を使用できる文字に変換
- encodeURIComponent() // 意味持つ「/」[&]なども変換するので、機能しなくなる
- decodeURI() // 上記の逆処理
- decodeURIComponent() // 上記の逆処理
組み込みobject
- あらかじめて提供さてれいるobject
- コンストラクタ不要
- Date,JSON,Object,Math,Array,Error,RegExpなど
JSON.parse(),JSON.stringify()
- parse() -> json文字列->object
- stringify() -> object -> json文字列
配列
- 長さ:arrayA.length、not size()
- pop()、push()は自体操作
- contact()は新しいobject返却
wrapper object
primitiveタイプのデータはメソッドやプロパティー持ってないのに、
メソッドやプロパティーを操作することに見えることは、一時的にwrapperしたから。
* Number
* Boolean
* StringDate
- getMonth() -> 0 ~ 11 ✳︎
- getDate() -> 1 ~ 31
- getDay() -> 0 ~ 6 ✳︎
文字列演算
console.log(1 + 2 + "3"); // 33 console.log("100" - "50"); // 50, 変換されて console.log("s" - "50"); // NaN、変換できないので「==」と「===」
console.log(100 == 100); //true console.log(100 === 100); //true console.log(100 == "100"); //true,数字に変換されたので console.log(100 === "100"); //false console.log("100" == "100"); //true console.log("100" === "100"); //truedelete
配列要素(undefinedに、サイズ変化なし)とobjectのプロパティーのみは削除可能
trueかfalseを返却
strictモードで削除失敗でエラーとするin
その時点に、該当プロパティーまたは、メソッドが存在するかを返却
- 投稿日:2020-08-20T18:35:37+09:00
Express 入門
公式ドキュメントもかなりシンプルでわかりやすいですが、自分用メモとして。
Express(日本語ドキュメント)最小構成
最低限、これで動かせるよ!というコードです
さくっとWebサーバ立ててなにか試したい時にインストール
$ mkdir myapp $ cd myapp $ npm init # entry point: app.js => 任意のファイル名 $ npm install expressディレクトリ構成
(root) ├ package.json ├ package-lock.json ├ app.js └ node_modulesコード
app.jsconst express = require('express') const app = express() app.get('/', (req, res) => res.send('Hello World!')) app.listen(3000, () => console.log('Example app listening on port 3000!'))実行
$ node app.js # go to http://localhost:3000/http://localhost:3000/ にアクセスすると「Hello World!」と表示されます
プロジェクトの雛形を作成する
Express プロジェクトの雛形を作成します
テンプレートエンジンの設定や、ディレクトリの作成もしてくれます
本格的にプロジェクトを作成する場合はこちらがおすすめ
詳しくはこちらCLIインストール
雛形作成に必要なパッケージをグローバルにインストールします
$ npm install express-generator -gPJ作成
$ express --view=pug myapp # --view=pug => 任意のテンプレートエンジン(未指定の場合はjadeになります) # myapp => 任意のPJ名 $ cd myapp $ npm install指定できるテンプレートエンジンやその他のオプションは↓で確認できます
$ express -h実行
$ DEBUG=myapp:* npm start # go to http://localhost:3000/ルーティング
メソッド
app.METHOD(PATH, HANDLER)のような形で記述します
Express は、すべてのHTTPリクエストメソッドに対応するメソッドをサポートしています
メソッドの一覧はこちらvar express = require('express') var app = express() app.get('/', function (req, res) { res.send('GET request') }) app.post('/', function (req, res) { res.send('POST request') })また、
all()を用いて、全てのメソッドのミドルウェア的なメソッドを定義することも可能です
next()でそれぞれのメソッドのハンドラに引き渡しますapp.all('/', function (req, res) { res.send('Any request') next() })パス
ルートのパスには正規表現も利用可能です
app.get(/.*fly$/, function (req, res) { res.send('/.*fly$/') })パラメータ
:paramの形で記述します
req.paramsで取得できますapp.get('/users/:userId/books/:bookId', function (req, res) { res.send(req.params) // => { "userId": "hoge", "bookId": "fuga" } })レスポンス
よく使いそうなもの掲載
一覧はこちら// さまざまなタイプのレスポンスを送信します res.send('GET request') res.send('<p>some html</p>') res.send({ some: 'json' }) // etc... // ビュー・テンプレートをレンダリングします res.render('index') // JSON レスポンスを送信します res.json({ user: 'tobi' }) res.status(500).json({ error: 'message' })メソッドをまとめて記述する
GET, POST などのメソッドをルートごとにまとめて記述することができます
すっきりしてよき!app.route('/book') .get(function (req, res) { res.send('Get a random book') }) .post(function (req, res) { res.send('Add a book') }) .put(function (req, res) { res.send('Update the book') })ルートハンドラをモジュール化する
ルートごとにファイルを分けて管理できます
大規模なアプリケーション向きbooks.jsvar express = require('express') var router = express.Router() router.get('/', function (req, res) { res.send('GET Books') }) router.get('/:bookId', function (req, res) { res.send('GET Some Book') }) module.exports = routerapp.jsvar books = require('./books') // ... app.use('/books', books)$ curl http://localhost:3000/books # => GET Books $ curl http://localhost:3000/books/12 # => GET Some Book静的ファイル
画像や CSS/JS など、静的ファイルをロードできるようにします
詳しくはこちらapp.js// public ディレクトリに入っているファイルをロードできるようにする app.use(express.static('public')); // http://localhost:3000/images/kitten.jpg // http://localhost:3000/css/style.css // http://localhost:3000/js/app.js // http://localhost:3000/hello.html
- 投稿日:2020-08-20T18:34:58+09:00
jQueryUIのDatepickerをBootstrapのmodal上に表示する
こちらの記事の改良版です。
BootstrapのモーダルでjQueryUIのDatepickerのz-index問題を解消する小技 - Qiita
環境
- jQuery UI 1.12
コード
$.datepicker.setDefaults({ // options ... beforeShow: function() { if ($(this).parents('.modal').length) { $(this).css('z-index', String(parseInt($(this).parents('.modal').css('z-index') | 0, 10) + 1)) } } });
- 投稿日:2020-08-20T13:38:31+09:00
Vueにおけるフラッシュメッセージ表示のロジック
前提
ライブラリ・フレームワーク バージョン Vue.js 2.6.11 Vuex 3.4.0 導入
Vueを使用して、フラッシュメッセージ(一時的に表示され、一定時間経つと自動的に消えるメッセージ)を作成するために、Vuexのストア機能を利用する。
なぜフラッシュメッセージの表示にVuexを利用するのか?
Vuexのメリットとは?
そもそもVuexを利用するメリットとしては、アプリケーション全体に関わる「状態」を、コンポーネントがバラバラに持たずに、ストアというデータ保管場所で一括で管理することにより、データ移動の複雑性が一気に解消される、という点にある。ちょいとむずいな〜。
どういうことかというと、以下の図をから説明してみる。。。
例えば、Bというコンポーネントで何らかの状態が変化して、DコンポーネントのUIを変更しなければならない場合、Vuexを使用しない場合だと何が起こるのか?
Vueのコンポーネント間では、兄弟関係のデータの受け渡しはできず、必ず「親から子」「子から親」の方向でしか許されないため、図の上のような「工数が多く複雑なデータ移動」を強いられてしまう。。。コンポーネントはお互いが疎結合(コンポーネント同士の結びつきが緩やかで、お互いが独立している状態)であるぼが望ましいので、このようにコンポーネント間のデータ移動に制限がある。しかしこの制限のおかげで、コンポーネント間の依存関係などあまり配慮せずに、コンポーネントの交換や改良を柔軟に行える。
しかし、Vuexをしようするとどうだろう?
Bコンポーネントに状態の変化が起きた場合、まずVuexへ状態の変化を報告し、更新する。そして、Dコンポーネントはその変化を察知し、Vuexの情報を参照し、取得する。これにより、上で示した「Bというコンポーネントで何らかの状態が変化して、DコンポーネントのUIを変更する」という問題を簡潔に解決できるようになる。フラッシュメッセージとVuexの相性
ここまでVuexについて少し考えてきた。フラッシュメッセージというのは、ありとあらゆるコンポーネントで使用したい機能のひとつなはず。ログインに成功したとき、登録に成功したとき、編集に失敗したとき、などなど。各コンポーネントの「状態変化」に応じて、多数のコンポーネントからフラッシュメッセージ機能を呼びたいわけです。ここで普通に処理すると上の図のようなデータ移動に複雑性が生じてきます。
Vuexでフラッシュメッセージの内容をデータ管理することで、どこからでもスムーズにフラッシュメッセージを呼び出すことが可能になります。フラッシュメッセージ実装の流れ
0 Vuexで状態管理するデータ内容
Vuexのステートにて状態管理するデータは「フラッシュメッセージにおけるメッセージ内容」とする。
1 ルートコンポーネントに「Message.vue」を登録する。
メッセージを表示するためだけのコンポーネント(Message.vue)をルートコンポーネントに登録する。これにより、ルートコンポーネントからフラッシュメッセージが表示されるようになる。以後、ルートコンポーネントを「App.vue」とする。
App.vue
Vue.js<div class="container"> <Message /> //追加 <RouterView /> </div>Vue.jsimport Message from './components/Message.vue'Vue.jsexport default { components: { Message, //省略 },2 Messeage.vueの作成
続いて、上で登録したMessage.vueの中身を作成していく。
Message.vue
Vue.js<template> //message関数の返り値がtrueの場合、変数messageを表示させる <div class="message" v-show="message"> {{ message }} </div> </template> <script> import { mapState } from 'vuex' export default { computed: { //メッセージストアのステートにセットされてある「メッセージ内容」を返す ...mapState({ message: state => state.message.content }) } } </script>ここで行っていることは、
1 メッセージストアのステートに値(メッセージ内容)がセットされているかどうかをcomputed内の関数にて確認し、セットされていない場合「空文字」を返し、セットされている場合は、メッセージ内容を返す。
2 メッセージ内容がセットされているときのみメッセージが表示される、つまりフラッシュメッセージが表示される。というロジック。
3 ストアの作成
message.js
Vue.jsconst state = { content: '' } const mutations = { setContent (state, { content, timeout }) { state.content = content if (typeof timeout === 'undefined') { timeout = 3000 } setTimeout(() => (state.content = ''), timeout) } } export default { namespaced: true, state, mutations }外部(コンポーネント)からミューテーションを呼ぶ際、引数にメッセージ内容とメッセージが消えるまでの時間を指定することを可能にした。
これによってミューテーションが呼ばれた後、次の通りの手順を踏む
1 メッセージ内容がストアのステートにセットされる
2 指定の時間が経つと、セットしたメッセージ内容を自動的にクリアするでは次は、ミューテーションを呼ぶ側の実装をする。
4 メッセージを表示させたいタイミングに位置するコンポーネントに処理の記入
Vue.js// メッセージ登録(例) async submit() { const response = await axios.post(`XXXXXX`) if( response.status === CREATED) { //ここでメッセージストアのミューテーション呼ぶ this.$store.commit(`message/setContent`,{ content: 'プロフィールの作成に成功しました!', timeout: 6000 }) this.$router.push(`YYYYYY`) return false } //以下省略 }上のように何らかの処理が成功した後、ミューテーションを呼びフラッシュメッセージを表示させ、直後に画面遷移などする。以上が実装のステップである。
まとめ
フラッシュメッセージが表示されるまでの流れをまとめると以下の通りになる。
1 外部(コンポーネント)から、フラッシュメッセージを表示させたいタイミングで、メッセージ内容とフラッシュメッセージの表示時間を引数にミューテーション呼ぶ。
2 メッセージストアにて、1からもらった引数を受け取り、メッセージ内容をステートにセットする。(ステートにセットした値は、もう一つの引数である表示時間を向かえたとき自動的に消去される)
3 Message.vueのcomputedのmessage関数にて、メッセージストアのstateにメッセージ内容ががセットされているかチェックし、もし値がセットされている場合そのメッセージ内容を返す。メッセージ内容が返ってきた場合のみ、そのメッセージをMessage.vue内にて表示させる。
4 App.vue(ルートコンポーネン)にMessage.vueが直接登録されてあるので、Message.vueにてメッセージが表示されていれば、そのまま画面上にメッセージが表示される。
つまり上で書いた実装の流れと逆の流れで、データはフラッシュメッセージの表示まで走っていく。
以上です。ここまで読んでいただきありがとうございました。何か間違いや補足などありましたらご指摘いただけると幸いです。
参考
Vue + Vue Router + Vuex + Laravelで写真共有アプリを作ろう (10) 写真投稿フォーム
Vuexを使ったローディング表示の実装方法
- 投稿日:2020-08-20T12:37:00+09:00
GASでAPIを叩いて結果をGoogleスプレッドシートに書き込む手順をなるべく詳しくメモしておく
同僚が楽しそうに書いているのを見ていて、前々から触ってみたかったGAS。
そんな折、今期はアウトプット強化が課題となったので、 自分の書いたQiita記事のLGTM数を集計するスクリプト作成を通じてGASに触れてみることにしました。
ただ、割とつまづく箇所も多かったので、自分のための備忘録としてメモを付けてQiitaに残しておきたいと思います。
1. Googleスプレッドシートを作成して、スクリプトエディタに移動する
まず、Googleスプレッドシートを作成します。
今回作成するスプレッドシートのカラムは、
No.、投稿者、LGTM、タイトル、URL、投稿日とします。スクリプトエディタに移動します。
2. メソッドを追加する
スクリプトエディタにあるコードは一旦全て消して、
fetch_qiita_articlesという名前のメソッドを追加します。function fetch_qiita_articles() { }3. GASでAPIを叩く
Qiitaの記事情報を取得したいので、Qiita APIの/v2/users/:user_id/itemsというユーザの記事一覧データを取得できるエンドポイントを叩きます。
以下のように書くことでレスポンスを得られます。(Qiitaのユーザ名は書き換えてください。)
let response = UrlFetchApp.fetch("https://qiita.com/api/v2/users/Qiitaのユーザ名/items");4. GASでJSONレスポンスをパースする
/v2/users/:user_id/itemsは、JSONを返すので以下のようにパースしましょう。
let json = JSON.parse(response);/v2/users/:user_id/itemsは、記事一覧の情報を配列で返すのでforEachで回して、記事名、URL、LGTM数、投稿日時を取得します。
そして、各項目をスプレッドシートのカラム順に並べた配列を作りましょう。
json.forEach(function(json, index){ let article = [index + 1, json["user"]["id"], json["likes_count"], json["title"], json["url"], json["created_at"]]; })5. GASでスプレッドシートに結果を書き込む
後は、先ほど作った配列を、appendRow(rowContents)を使ってスプレッドシートの行の最後に追加するだけです。
let sheet = SpreadsheetApp.openById("スプレッドシートのID").getSheetByName("シート名"); sheet.appendRow(article);※ ちなみに、ここではわかりやすくするため appendRow(rowContents) を使っていますが、これだと1行1行書き込むことになるので効率的ではありません。一旦配列の収めて
getRange(row, column, numRows, numColumns).setValues()で一気に書き込むのがおすすめです。補足
- スプレッドシートのIDは、スプレッドシートのURLのこの部分になります。
- シート名は、スプレッドシートの左下にあるタブ名となります。
では実行してみましょう。
無事スプレッドシートに書き込まれてますね!
6. コード全文
全文も載せておきます。
function fetch_qiita_articles() { let sheet = SpreadsheetApp.openById("スプレッドシートのID").getSheetByName("シート名"); let response = UrlFetchApp.fetch("https://qiita.com/api/v2/users/Qiitaのユーザ名/items"); let json = JSON.parse(response); json.forEach(function(json, index){ let article = [index + 1, json["user"]["id"], json["likes_count"], json["title"], json["url"], json["created_at"]]; sheet.appendRow(article); }) }7. Cronのように時間を指定して自動で読み込む時間を設定する
ちなみに、GASは時間指定の実行も簡単に行えます。
まず、スクリプトエディタのメニューバーにある時計ボタンをクリックします。
トリガー追加ボタンをクリックします。
各項目を設定します。自分は毎朝9時に実行したいので以下のように設定しました。設定が完了したら保存ボタンを押します。
トリガーが追加されたら指定して時間になれば自動でスプレッドシートに結果が書き込まれます。
8. 若干機能を追加した実際に使っているコード
ちなみに現在、実際に使っているのは、このコードになります。
fetch_qiita_articles.gs - gas_samples | Github
https://github.com/kurarararara/gas_samples/blob/master/fetch_qiita_articles.gs開発チームメンバーのQiita記事情報を一度に取得できたり、期間を指定できたり、記事数が多くなっても書き込みが遅くならないようにしていたり と若干機能を加えていますのでよかったら試してみてください。
9. 最後に
いやーGAS楽しいですね。ちょっとのコードで大いに楽できるツールが作れそうで非常に気に入りました。
もう少しなんか作ってみたいと思います。
- 投稿日:2020-08-20T12:37:00+09:00
GASでQiita APIを叩いて結果をGoogleスプレッドシートに自動入力する手順をなるべく詳しくメモしておく
同僚が楽しそうに書いているのを見ていて、前々から触ってみたかったGAS。
そんな折、今期はアウトプット強化が課題となったので、 自分の書いたQiita記事のLGTM数を集計するスクリプト作成を通じてGASに触れてみることにしました。
ただ、割とつまづく箇所も多かったので、自分のための備忘録としてメモを付けてQiitaに残しておきたいと思います。
1. Googleスプレッドシートを作成して、スクリプトエディタに移動する
まず、Googleスプレッドシートを作成します。
今回作成するスプレッドシートのカラムは、
No.、投稿者、LGTM、タイトル、URL、投稿日とします。スクリプトエディタに移動します。
2. メソッドを追加する
スクリプトエディタにあるコードは一旦全て消して、
fetch_qiita_articlesという名前のメソッドを追加します。function fetch_qiita_articles() { }3. GASでAPIを叩く
Qiitaの記事情報を取得したいので、Qiita APIの/v2/users/:user_id/itemsというユーザの記事一覧データを取得できるエンドポイントを叩きます。
以下のように書くことでレスポンスを得られます。(Qiitaのユーザ名は書き換えてください。)
let response = UrlFetchApp.fetch("https://qiita.com/api/v2/users/Qiitaのユーザ名/items");4. GASでJSONレスポンスをパースする
/v2/users/:user_id/itemsは、JSONを返すので以下のようにパースしましょう。
let json = JSON.parse(response);/v2/users/:user_id/itemsは、記事一覧の情報を配列で返すのでforEachで回して、記事名、URL、LGTM数、投稿日時を取得します。
そして、各項目をスプレッドシートのカラム順に並べた配列を作りましょう。
json.forEach(function(json, index){ let article = [index + 1, json["user"]["id"], json["likes_count"], json["title"], json["url"], json["created_at"]]; })5. GASでスプレッドシートに結果を書き込む
後は、先ほど作った配列を、appendRow(rowContents)を使ってスプレッドシートの行の最後に追加するだけです。
let sheet = SpreadsheetApp.openById("スプレッドシートのID").getSheetByName("シート名"); sheet.appendRow(article);※ ちなみに、ここではわかりやすくするため appendRow(rowContents) を使っていますが、これだと1行1行書き込むことになるので効率的ではありません。一旦配列に収めて
getRange(row, column, numRows, numColumns).setValues()で一気に書き込むのがおすすめです。補足
- スプレッドシートのIDは、スプレッドシートのURLのこの部分になります。
- シート名は、スプレッドシートの左下にあるタブ名となります。
では実行してみましょう。
無事スプレッドシートに書き込まれてますね!
6. コード全文
全文も載せておきます。
function fetch_qiita_articles() { let sheet = SpreadsheetApp.openById("スプレッドシートのID").getSheetByName("シート名"); let response = UrlFetchApp.fetch("https://qiita.com/api/v2/users/Qiitaのユーザ名/items"); let json = JSON.parse(response); json.forEach(function(json, index){ let article = [index + 1, json["user"]["id"], json["likes_count"], json["title"], json["url"], json["created_at"]]; sheet.appendRow(article); }) }7. Cronのように時間を指定して自動で読み込む時間を設定する
ちなみに、GASは時間指定の実行も簡単に行えます。
まず、スクリプトエディタのメニューバーにある時計ボタンをクリックします。
トリガー追加ボタンをクリックします。
各項目を設定します。自分は毎朝9時に実行したいので以下のように設定しました。設定が完了したら保存ボタンを押します。
トリガーが追加されたら指定して時間になれば自動でスプレッドシートに結果が書き込まれます。
8. 若干機能を追加した実際に使っているコード
ちなみに現在、実際に使っているのは、このコードになります。
fetch_qiita_articles.gs - gas_samples | Github
https://github.com/kurarararara/gas_samples/blob/master/fetch_qiita_articles.gs開発チームメンバーのQiita記事情報を一度に取得できたり、期間を指定できたり、記事数が多くなっても書き込みが遅くならないようにしていたり と若干機能を加えていますのでよかったら試してみてください。
9. 最後に
いやーGAS楽しいですね。ちょっとのコードで大いに楽できるツールが作れそうで非常に気に入りました。
もう少しなんか作ってみたいと思います。
- 投稿日:2020-08-20T12:37:00+09:00
GASでQiita APIを叩いて結果をGoogleスプレッドシートに自動入力する手順を詳しくメモしておく
同僚が楽しそうに書いているのを見ていて、前々から触ってみたかったGAS。
そんな折、今期はアウトプット強化が課題となったので、 自分の書いたQiitaのLGTM数を集計するスクリプトをGASで作ってみました。
ただ、割とつまづく箇所も多かったので、自分のための備忘録として、なるべく詳しい手順をQiitaに残しておきたいと思います。
1. Googleスプレッドシートを作成して、スクリプトエディタに移動する
まず、Googleスプレッドシートを作成します。
今回作成するスプレッドシートのカラムは、
No.、投稿者、LGTM、タイトル、URL、投稿日とします。スクリプトエディタに移動します。(複数のGoolgeアカウントでログインしているとアクセスエラーが出る場合があるので、その場合は一旦全部ログアウトしてから再度ログインすると移動できます。)
2. メソッドを追加する
スクリプトエディタにあるコードは一旦全て消して、
fetch_qiita_articlesという名前のメソッドを追加します。function fetch_qiita_articles() { }3. GASでAPIを叩く
Qiitaの記事情報を取得したいので、Qiita APIの/v2/users/:user_id/itemsというユーザの記事一覧データを取得できるエンドポイントを叩きます。
以下のように書くことでレスポンスを得られます。(Qiitaのユーザ名は書き換えてください。)
let response = UrlFetchApp.fetch("https://qiita.com/api/v2/users/Qiitaのユーザ名/items");4. GASでJSONレスポンスをパースする
/v2/users/:user_id/itemsは、JSONを返すので以下のようにパースしましょう。
let json = JSON.parse(response);/v2/users/:user_id/itemsは、記事一覧の情報を配列で返すのでforEachで回して、記事名、URL、LGTM数、投稿日時を取得します。
そして、各項目をスプレッドシートのカラム順に並べた配列を作りましょう。
json.forEach(function(json, index){ let article = [index + 1, json["user"]["id"], json["likes_count"], json["title"], json["url"], json["created_at"]]; })5. GASでスプレッドシートに結果を書き込む
後は、先ほど作った配列を、appendRow(rowContents)を使ってスプレッドシートの行の最後に追加するだけです。
let sheet = SpreadsheetApp.openById("スプレッドシートのID").getSheetByName("シート名"); sheet.appendRow(article);※ ちなみに、ここではわかりやすくするため appendRow(rowContents) を使っていますが、これだと1行1行書き込むことになるので効率的ではありません。一旦配列に収めて
getRange(row, column, numRows, numColumns).setValues()で一気に書き込むのがおすすめです。補足
- スプレッドシートのIDは、スプレッドシートのURLのこの部分になります。
- シート名は、スプレッドシートの左下にあるタブ名となります。
では実行してみましょう。
無事スプレッドシートに書き込まれてますね!
6. コード全文
全文も載せておきます。
function fetch_qiita_articles() { let sheet = SpreadsheetApp.openById("スプレッドシートのID").getSheetByName("シート名"); let response = UrlFetchApp.fetch("https://qiita.com/api/v2/users/Qiitaのユーザ名/items"); let json = JSON.parse(response); json.forEach(function(json, index){ let article = [index + 1, json["user"]["id"], json["likes_count"], json["title"], json["url"], json["created_at"]]; sheet.appendRow(article); }) }7. Cronのように時間を指定して自動で読み込む時間を設定する
ちなみに、GASは時間指定の実行も簡単に行えます。
まず、スクリプトエディタのメニューバーにある時計ボタンをクリックします。
トリガー追加ボタンをクリックします。
各項目を設定します。自分は毎朝9時に実行したいので以下のように設定しました。設定が完了したら保存ボタンを押します。
トリガーが追加されたら指定して時間になれば自動でスプレッドシートに結果が書き込まれます。
8. 若干機能を追加した実際に使っているコード
ちなみに現在、実際に使っているのは、このコードになります。
fetch_qiita_articles.gs - gas_samples | Github
https://github.com/kurarararara/gas_samples/blob/master/fetch_qiita_articles.gs開発チームメンバーのQiita記事情報を一度に取得できたり、期間を指定できたり、記事数が多くなっても書き込みが遅くならないようにしていたり と若干機能を加えていますのでよかったら試してみてください。
9. 最後に
いやーGAS楽しいですね。ちょっとのコードで大いに楽できるツールが作れそうで非常に気に入りました。
もう少しなんか作ってみたいと思います。
- 投稿日:2020-08-20T10:45:04+09:00
マークアップエンジニアがNuxt.js案件で必須な知識【5選】
こんにちは、現在フリーのマークアップエンジニアとして活動しているしょーごと申します。
最近はYouTubeなどでも発信しています。普段はWordPress構築や静的サイトコーディングをメインとしているのですが、
最近機会に恵まれまして、Nuxt.jsのマークアップ部分(ロジック以外)を担当しております。結果的に大苦戦してベテランの方に大迷惑をかけてしまったので、
マークアップエンジニアやデザイナーがNuxt.jsのマークアップ部分をアサインされた場合に必要な知識をまとめました。Nuxt.jsはweb制作業界にも少しづつ浸透してきているので、マークアップエンジニアやデザイナーさんもいずれ触る期間があるかもしれない点で、同業の方々に参考にして頂けたらと思います。
俺の屍を超えていけNuxtってなんなの?
Vue.js単体では開発に不足している機能が多いです。なので従来はプラグインやライブラリを導入していましたが、Nuxtは色んな機能が最初から盛り込まれているフレームワークになります。
Vue.js+ライブラリの集まりみたいなものです。Nuxt.jsのマークアップ部分とは?
Nuxt,jsはご存知の通り、
単一ファイルコンポーネントと呼ばれています。HTML,CSS,JSが一つのファイルにまとまっています。
以下コードは超適当なので、scriptにロジックがかいてあるんだとだけ認識頂けたら?default.vue<template> <div> <div class="posts overflow-scroll mb-24"> <post v-for="(post,index) in posts" :key="index" :post="post" /> </div> <el-upload v-if="!imageUrl" action :show-file-list="false" :http-request="uploadFile"> <el-button size="small" type="primary">Click to upload</el-button> </el-upload> <el-input type="textarea" :rows="8" placeholder="Please input" class="mt-8" v-model="text"></el-input> </div> </template> <script> import Post from "~/components/Post.vue"; import { db, firebase } from "~/plugins/firebase"; export default { components: { Post, }, data() { return { posts: [], imageUrl: null, text: null, }; }, methods: { async post() { await db.collection("posts").add({ text: this.text, image: this.imageUrl, createdAt: new Date().getTime(), }); this.text = null; this.imageUrl = null; window.alert("保存されました"); }, }, mounted() { db.collection("posts").onSnapshot((snapshot) => { snapshot.docChanges().forEach((change) => { const doc = change.doc; if (change.type === "added") { this.posts.unshift({ id: doc.id, ...doc.data() }); } }); }); }, }; </script> <style lang="scss" scoped> input { box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16); } .posts { text-align: center; }ここのscript以外が作業範囲になります。
つまり、チーム開発が前提です。厳密に言えば、
1.HTML部分のコーディング
2.Scoped CSS、グローバルCSSの記述と設計(Sassが標準)
3.コンポーネント分け(ここだけscript部分)
4.状態管理でのclass出し分け(v-forやv-bind,propsあたり)
5.CSSフレームワークの利用と共存(Tailwindやbulma、Element-uiやBuefyなど)このあたりが業務範囲になります。
その他はプログラマーさんに担当していただきます。プログラマー→フロントエンド、フルスタックエンジニア
マークアップエンジニア→コーダー、デザイナーでは、これらをふまえてNuxt.jsのマークアップ部分を担当する上で何を知っておくといいのか解説します。
①Nuxtのコンポーネント、レイアウト、ページルーティング
やはりここが一番重要になるかと思います。
以下公式の画像になります。
Nuxtでは一般的にビューの表示は
1.レイアウト(全ページ読み込まれる)
2.ページ
3.コンポーネントの3つに別れます。
レイアウトの役割
レイアウトは各ページでの共通要素を書きます。
大抵はheaderとfooterコンポーネントを呼びこむぐらいだと思います。default.vue<template> <div> <app-header /> <nuxt /> <app-footer /> </div> </template>
<nuxt />という部分に、各ページの内容が入ってきます。ページの役割
そのページ専用の内容を書いていきます、
直接HTMLを書いたり、コンポーネントを呼び込んで構築していきます。
レイアウトでheaderとfooterは呼び込み済みなので、それ以外の部分になります。またNuxtではルーティングがすでに設定してあるため。例えばusersページを作る際には、
これでlocalhost:3000/usersでその配下のindex.vueを表示させることができます。
pagesディレクトリ以下がそのままページのディレクトリ構成を示しています。
コンポーネントの役割
header,footer,sidebar,navigationなどダントツでファイル数が多くなります。
基本的に共通化できそうな塊はだいたいコンポーネント化していきます。
レイアウトやページで読み込んで表示させていきます。
大きめのコンポーネントの場合、コンポーネントの中でコンポーネントを読み込むなどの状況もよくあります。私のブログを例にしてみます。
赤枠:レイアウト
緑枠:ページ
青枠:コンポーネントコンポーネントの読み込みはimportとexport defaultをセットで使います。
userコンポーネントを読み込む場合index.vue<template> <div class="users"> <user /> <user /> </div> </template> <script> import User from "~/components/User.vue"; export default { components: { User, }, }; </script>コンポーネント間でのメソッドや状態の受け渡しなどもあるので、プログラマーと相談しつつコンポーネント分けの粒度を決めるといいかなと思います。
②Scoped CSSとグローバルCSS設計
Scoped CSSについて
Scoped CSSは、
scopedを記入するだけで使えます。その単一ファイル内にしかCSSのスコープが及ばないため、グローバル汚染がなくよく
「CSS is too fragileからの脱却だ!」的に叫ばれますが、しかしやはり銀の弾丸はなく、h2やsectipnなどのセレクタに直接指定すると普通に子コンポーネントを汚染します。
このようにカスタムデータ属性により影響範囲を限定的にしていますまあ普通にBEMとか使っていれば問題ないです。
Scoped CSSにおけるクラス設計
詳しくは以下の記事に譲りますが、Scoped CSSでも暴虐無人プレイができるわけではないです。
ただ、普通にBEM的にクラス名を書いていけば、バッティングの可能性は相当低くなるので、Web制作にNuxt.jsが有用である理由の一つとなっております。
グローバルCSS
全体に適用したいCSSは
assets以下にcssファイルを置いて、それをnuxt.config.jsで読み込むのですが、とりあえず「Scoped CSS」と「グローバルCSS」に分かれているらしいぐらいの認識で大丈夫です。
reset.scssとか_variables.scss,_mixin.scssなど全体で使いそうなものを定義していきます。③状態変化でクラスの出し分け
v-bind,props
意外にここも知っておいたほうがいいです。
例えばよく「トップページのみヘッダーのデザインが違う?」ということはよくあります。
しかしレイアウトでheaderコンポーネントを読み込んでいるため、トップページのみ変更するのは難しく感じます。そこでpropsとv-bindが出てきます。
手順としては
1.トップページ専用のレイアウトを準備し、利用
2.topであることをBooleanで管理し、trueのときにheaderにクラス付与となります。
トップページ専用のレイアウトをtop.vue、trueを渡す先のheaderコンポーネントははheader.vueとし、トップページの表示はindex.vueが行います。
まずはtop.vueで
:is-top="true"とすることでトップであることをheaderコンポーネントに伝えます。top.vue<template> <div> <header :is-top="true" /> <Nuxt /> <footer /> </div> </template> <script> import Header from '~/components/header-nav' import Footer from '~/components/footer-nav' export default { components: { Header, Footer } } </script>その後、index.vueではレイアウトとしてtop.vueを使うと宣言します。
index.vuelayout: 'top',headerコンポーネントでpropsによりtrueを受け取り、v-vindでtrueのときにclassを付与します。
header.vue<template> <header :class="{ 'top-header' : isTop === true }"> </header> </template> <script> props: { isTop: { type: Boolean } } </script>これにより、トップページにのみ
top-headerというクラスが付与できるようになります。他にもv-onやv-forについても知っておいたほうがいいです。
v-onはモーダルの表示、v-forは<li v-for="(item, index) in items">などでindexを取り出しループ内で動的にクラス名を付与したいときに、役立ちます。
ここでは詳しくは割愛します。④npm関連
直接Nuxtとは関係しませんが、npmを普段より利用しているかも重要です。
マークアップ側が自分でパッケージを入れることはほとんどないと思いますが、必須知識です。npm install
チーム開発ではよくパーケージが新しくインストールされていたりします。
その際に、「package.jsonが更新されてる!!ということはnpm iすればいいんだ!!」と知っていればいいのですが、知らないとエラー画面に飛ばされます笑npm run dev
Nuxtのサーバーを起動するnpm scriptsです。
ファイルを保存したら、画面を自動リロードしてコンパイルしてくれます。nvm
Node.jsのバージョン管理ツールです。
開発開始前にnode.jsのバージョンはチームメンバーで合わせておくべきです
npmで入れるモジュールが、Nodeのバージョンに依存するため、チームメンバーのグローバルNodeのバージョンが違うと、地獄と化します。⑤Git
これもNuxtから離れますが、チーム開発ではかなり重要です。
Nuxtを扱うような案件では十中八九必要でしょう。
いやテーム開発でGit運用とか常識やろと声が聞こえそうですが、1人Gitしか経験なく、マークアップ案件でGithub flowでのチーム開発とかほとんどないので自分は大苦戦しました...
diffが見やすいsourcetreeで大丈夫だと思います。最低限抑えておくべきこと
- github flow(機能やページ毎にfeatureブランチを切って運用するぐらい)
- プル→ステージング→コミット→プッシュ→プルリクの流れがわかる
- コンフリクト対応
- スタッシュを知っておく(割と変更内容を退避させるため)
ぐらいかなと思います。コンフリクトの解消など自分はパニクってしまいレビュアー先輩エンジニアの方によしなにしていただきマージしてもらいましたが、これは迷惑かけたのであまりよくないと思いました。
Gitでの開発流れ
1.最新origin/masterからローカルmasterにpull(この際にpackage.jsonが更新されていたら、npm iしてパッケージを入れておく)
2.masterからfeatureブランチを切り作業開始
3.作業が完了したらコミットしプッシュする
4.プルリクを出し、レビュアーにレビューを受ける、問題なければマージされる、問題あれば修正し再度コミットプッシュする
5.マージされたらmasterにチャックアウトし、必ずプルし最新のmasterにする。
6.そして次のfeatureブランチを切り、作業開始少人数の開発&トラブルがなければこんな感じだと思います。
Nuxt.jsでのマークアップ案件は増えるのか
NuxtはfirebaseやヘッドレスCMSと組み合わせてJamstackで利用されているイメージですが、ロジック部分に集中したい人は一定層いると考えています。
その際に、Nuxtの構造を理解しているマークアップエンジニアにスタイリングを外注できたら楽そうなので、
需要としてはポツポツ発生していそうだと思います。Nuxtオンリーでの静的サイト制作については、未知数です。
Web制作で使われるならVueでのアニメーションの知識が必要になるのと、枯れているjQueryの資産が偉大なので、すぐに広まるのは考えにくいです。
しかし保守性の面でコンポーネントやScoped CSSが大変魅力的なので、大規模サイト制作で真価を発揮するかなと思っています。事前に学習で使用していた教材
- 投稿日:2020-08-20T09:44:24+09:00
React v17 何が変わった?
先日、React v17がリリースされましたね!
前回のメジャーリリースからなんと2.5年もかかったようです。
ほとんどが公式の意訳になりますが、何が変わったのか重要そうなところをピックアップして自分メモとしてまとめておきます。
全てを網羅しているわけではないので、より詳しく知りたい方は下記の公式リリースノートを参照ください。
公式リリースノート新しい機能はないよ
React開発者は現在新しい機能追加に向けて取り組んでいますが、今回のv17には実は新機能は追加されていません。
v17は言わば、これからリリース予定の大規模アップデートのための踏み石になっています。段階的アップデート
今までのアップデートには必ず破壊的変更が含まれていました。v15 -> v16の破壊的変更は皆さんの記憶にも新しいかと思います。そのような破壊的変更は、メンテされてないコードは特に、アップデート作業は困難を極めます。
1つのページに複数のバージョンのReactを置く、という対処方法もありますが、元来のReactではその方法ではイベントがうまく起動しないという問題がありました。v17ではそのような問題が解消されています!?
これがreact v17の一番の目玉となっています。
これからのアップデートでも、各react毎にアップデートし、古い部分は残す、という作業が可能になるように設計されていくそうです。先ほど新しい機能の追加はない、と言いましたが、破壊的な変更がないわけではありません。
上記のイベント問題を解消するために、特にイベント部分が今までと違う実装となっています。実際にはFacebookではmアップデートのために変更しなければならなかったコンポーネントは
100,000以上のコンポーネントのなかで20個ほどしかなかったので、ほとんどのアプリでは大きなトラブルはないだろう、とのことです。段階的アップグレードをlazy-loadでどのようにするか、のdemoを公式が用意してくれています。
イベントの委任に関する変更
v17でのイベントの内部変更について見てみましょう。
下記はよくあるReactでのイベント登録です。<button onClick={handleClick}>これをバニラJSに直すと下記になります。
myButton.addEventListener('click', handleClick);上記では、reactでボタンに登録したclickイベントが、そのDOMのイベントとして登録されています。
しかしReactのイベントの中には、登録すると宣言したDOM以外にイベントが登録される場合があります。
どこに登録するかというと、documentに一括で登録されます。
これはイベントの委任(Event Delegation)と呼ばれる一般的なテクニックです。
イベントの委任について詳しくはこちら。
イベントの委任をすることでパフォーマンス改善にもなり、イベントの再生を行うことができるなど、色々な機能が使えます。しかし、この仕組みは段階的アップデートを行う際問題となってしまいます。
もし1ページに複数の異なるバージョンのReactを設置しようとすると、どちらのイベントもdocumentに登録されてしまうため、e. stopPropagation()が上手く動かなくなります。ちなみにイベントの伝播について復習したい方は、下記の記事がおすすめです。
バブリング と キャプチャリング上記問題は実際にissueに上がっているので、理解を進めるためにリリースノートを一旦離れ、issueの内容を見てみます。
Attach event listeners at the root of the tree instead of document #8117To understand how this solves the problem, let's assume that we have OuterComponent which is running React version A and InnerComponent that is running version 2. The inner component attaches the event listener at the top of the inner tree using bubble phase and the outer component at the root of the outer component.
When there's a click event on the innerComponent, the inner version of React will be notified first because it's deeper in the tree, which will dispatch the event through the innerComponent hierarchy and eventually something will call React e.stopPropagation(), which will call the DOM e.stopPropagation(), so that the outer version of React will never be notified.
意訳: この問題を理解するために、OuterComponentというreactのバージョンその1と、InnterComponentというreactのバージョンその2があるとします。InnterComponentはバブルフェーズでinnterのトップにイベントを付与し、OuterComponentも同様にバブルフェーズでouterのトップにイベントを付与します。
innterComponentをクリックした際、ツリーの奥側にあるinnerのバージョンその2reactがouterより先にそれを感知し、イベントをディスパッチします。そしてe.stopPropagation()が呼ばれます。
その為、外側のバージョンその1のreactはイベントを感知することができません。この問題を解消する為v17ではdocumentにイベントを委任せず、reactがrenderされるroot DOMにイベントを委任するようになりました。
const rootNode = document.getElementById('root'); ReactDOM.render(<App />, rootNode); // 上記の場合、 id=root のDOMにイベントが委任される※公式リリースノートから拝借した画像です。
この変更はreactの段階的アップデートのみでなく、他の技術と組み合わせてreactを使う際にも有効です。
外側のコードをjqueryで書き、内側でreactを使う場合でも、e.stopPropagation()に悩まされずに済みます。アップデートの際気をつけること
document.addEventListener(...)を記述している際は要注意です。
上記イベントを登録し、reactのイベント内でe.stopPropagation()を呼び出していても今までのreactバージョンではdocumentのカスタムイベントを走らせることができました。
しかし、v17ではe.stopPropagation()により伝播が止まってしまいます。リクエスト通りになるということですね。document.addEventListener('click', function() { // reactのコンポーネントが e.stopPropagation()を読んでいたらここは実行されない });下記のようにキャプチャリングを
trueにすることで調整しましょう。
これでバブリング時ではなくキャプチャリング時に発火するので、e.stopPropagation()の影響を受けずにすみます。document.addEventListener('click', function() { }, { capture: true });その他破壊的変更
イベントのシステムに関して若干の変更があります。
onScrollがバブリングしなくなりました。onFocusとonBlurはネイティブのfocusinとfocusoutを内部で使用するようになりました。つまり、JS本来の動きに近くなったということですね。onClickCaptureといったキャプチャフレーズイベントは、実際のブラウザのキャプチャフレーズのリスナーとなりました。上記で注意すべきなのは、
onFocusは内部でfocusを使用していたものをfocusinに変更していますが、依然としてバブリングするという点です。Effect クリーンアップのタイミング
useEffectのクリーンアップのタイミングに一貫性を持たせたとのことです。
useEffect(() => { return () => { // クリーンアップ }; });通常、クリーンアップ関数を走らせる場合、画面遷移を遅らせる必要はないので、reactでは画面更新後すぐに非同期処理としてクリーンアップ関数を走らせます。(同期処理が必要な場合は
useLayoutEffectを使うべきです。)
しかし、v16のeffectのクリーンアップ関数では同期的に実行されていました。
これは大きなアプリでの画面遷移では致命的です。
v17のuseEffectのクリーンアップは非同期処理として実行されます。コンポーネントがアンマウントされ、画面が再描画された後に実行されます。加えて、react17では、クリーンアップ関数の実行順が違うことがありましたが、
ツリー内の順番通りに実行されるとのことです。潜在的な問題
useEffect(() => { someRef.current.someSetupMethod(); return () => { someRef.current.someCleanupMethod(); }; });上記の
someRef.currentはimmutableなので、クリーンアップ実行時にはnullになっている場合があります。useEffect(() => { const instance = someRef.current; instance.someSetupMethod(); return () => { instance.someCleanupMethod(); }; });上記のように変数に割り当てておきましょう。
undefined返却で常にエラーとなるようになった
reactではクラスと関数のコンポーネントに関してundefinedとなっていないかのチェックをしています。
function Button() { return // エラー }これは値を返し忘れていることを防止する意図があります。
function Button() { // return書き忘れでundefinedが返却値になってしまっている <button />; }しかし、v16では
forwardRefやmemoといった、同じチェックを必要とするコンポーネントに上記のエラーチェックが含まれていませんでした。
v17ではforwardRefmemoでもundefinedが返されるとエラーが出るように変更されています。(意図的に何も返したくないときは
nullを返してね!)
- 投稿日:2020-08-20T07:52:26+09:00
npmのライブラリの比較と評価方法:npm trends & Github
人気のあるライブラリをnpm trendsで探す
npm trendsは、指定したライブラリのカテゴリと近いものを提示し、選択するとダウンロード数を比較してくれます。
たとえば、dom parserを比較すると以下の用になります。とりあえず、ネットで一つライブラリを探してきて、npm trendsに入れると、候補を提示してくれるので、それでダウンロード数を比較をします。
npm trendsで提示されたライブラリのGithub上でのstarチェック
npm trendsのグラフだけでは、人気を計るのは不十分です。Githubのstarもチェックします。
例えば、上のdom parserの例だと、parse5が一番多いですが、starの数は、cheerioの方が多いのです。google → npm trends → Githubの順でライブラリの調査
タイトルの順番でライブラリを調査すると良さそうなものが見つかるように思います。
ネットの情報はかならず、「比較」と「評価」が必要になりますが、npmについてはこういう手順もよいのではと思います。
- 投稿日:2020-08-20T06:53:05+09:00
【JavaScript関数ドリル】 初級編 head関数の実装
head関数の課題内容
「課題内容」「解説動画」「解答例」を確認したい場合は、以下リンク先のページを参照。
https://js-drills.com/blog/head/
head関数 に取り組む前の状態
- 先頭の値を出力するのかな?とイメージしていました。
head関数に取り組んだ後の状態
- headメソッドは、引数に配列を渡すことで先頭の値が出力されます
head関数の実装コード(答えを見る前)
- 比較的かんたんに使えるメソッドで、実装することができました。
head関数の実装コード(答えを見た後)
実装完了しました?
function head(array) { return array[0]; } console.log( head([1,2,3])); console.log(head([])); console.log(head([20,54,67,3,4,76,5,6,30030]));
- 投稿日:2020-08-20T03:24:35+09:00
GoogleForm回答の集計グラフをコピーする方法
注意: 2020/08/20時点の情報です。GoogleFormの仕様が変わっている場合があります。
GoogleFormが作ってくれるグラフをSVG形式で出力する
GoogleFormは手軽にWebアンケートを作ることができる便利なツールだ。
Webの専門知識がなくてもアンケートの作成から集計まで手を煩わせることなく簡単に行なえるツールとしてあらゆる場面で重宝している。
標準機能として集計結果のグラフを画像としてクリップボード経由で出力できるのだが、ラスター画像であるため、大きめの印刷物にこれを用いようとすると画像を引き伸ばすしかなく画像の劣化を避けられずやや不便である。
F12開発ツールで集計ページのhtmlを覗くと、グラフはもともとSVGで作られているようである。だから、このSVGをそのまま抜き出してしまえばよいのではないかと考えた。
SVGを抜き出してしまえばそのあとは適当なツールを使ってラスター画像に変換したり、自身のWebページに埋め込んだりと自由自在である。手順
- GoogleFormを開き、「回答」タブを表示する
- ブラウザの開発ツールを出してコンソールに後述のJavaScriptを入力する
- ダイアログが表示されるのでファイルをダウンロード
コード
script.jsconst downloadHTML = (html) => { let blob = new Blob([html], {type: "text/html"}); let link = document.createElement("a"); let url = URL.createObjectURL(blob); link.href = url; link.download = "download.html"; link.click(); link.remove(); URL.revokeObjectURL(url); } var html = [].slice.call(document.querySelectorAll(".freebirdAnalyticsViewAnalyticsItem")).map(item => item.outerHTML).join("\n"); downloadHTML(html);
document.querySelectorAll(".freebirdAnalyticsViewAnalyticsItem")がみそです。もしGoogleFormの仕様が変わっていて要素にアクセスできない場合はここをいじってみて下さい。
- 投稿日:2020-08-20T03:09:26+09:00
Vuetifyのv-text-fieldでtype="number"の時に、表示されている値を変更する
こんにちは!
長らくプレーンJavaScriptガリゴリマンでしたが、最近仕事の関係でVue.jsを触り始めた和尚でございますそれでは早速見ていきましょう!
環境
- Vue.js v2.6.11
- Vuex v3.4.0
- Vuetify v2.2.11
注意
※かなりパワープレイです。参考程度に見てください。
本日の議題
ある昼下がりのこと
Aさん「数値しか入れられないフォームに文字が入っちゃうんだけど、どうにかならない?あと、最大値が999のはずなのに、キーボードで入力すると1000以上入っちゃうからついでに直して...」
和尚「まじっすかー。ちょっと調べてみまーす!」
...
今回の要件における Input type="number" の困った仕様
- 最大値と最小値はキーボード入力で超えることが可能
- safariは文字列を入力すると自動削除されない
- chromeは指数表現のe・Eが入力できる
解決しよう!
それではAさんの要件に沿って修正していきましょう!
修正前のサンプルコンポーネントとストアはこちらになります!Count.vue<template> <div> <v-text-field ref="count" label="カウント" type="number" v-model="count" max="999" min="1" /> </div> </template> <script> export default { computed: { count: { get() { return this.$store.getters['count']; }, set(num) { this.$store.commit('changeCount', num); } } } } </script>index.jsimport Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) export default new Vuex.Store({ state: { count: 1 }, getters: { count: (state) => state.count }, mutations: { changeCount(state, num) { if (num) state.count = num; } }, actions: {} })①数値のみが入るようにする
テキストフィールドのvalueに文字列が入力された場合は空文字が返却されます。なのでsafariで文字列が入力されたままでもストアで管理している値に文字列が入ることはありません。(空文字を除く)
ということは、テキストフィールドの中にはvalueとは違う値が表示されているということです。
まずはテキストフィールドに数値のみしか入らないように修正していきましょう!
算術プロパティに記述した、countのsetterの中を少し弄ってテキストフィールドに現在のvalueの値が表示されるようにしましょう!Count.vue<script> export default { computed: { count: { get() { return this.$store.getters['count']; }, set(num) { num = parseInt(num) if (Number.isNaN(num)) num = 1; this.$refs.count.lazyValue = num; this.$store.commit('changeCount', num); } } } } </script>
- valueをparseIntを使用して文字列から数値にする (Input type="number"の値はString型です)
- 数値ではない場合にnumに1を代入
this.$refs.count.lazyValueに値を入れる (v-text-fieldの表示している値を格納しているプロパティ)- ストアの値も変更する
②最大値・最小値の制御
ここまで来たらあとは簡単!
最大値と最小値の制御を入れてあげましょう!Count.vue<script> export default { computed: { count: { get() { return this.$store.getters['count']; }, set(num) { const min = Number(this.$refs.count.$attrs.min); const max = Number(this.$refs.count.$attrs.max); num = parseInt(num) if (Number.isNaN(num) || num < min) { num = min; } else if (num > max) { num = max; } this.$refs.count.lazyValue = num this.$store.commit('changeCount', num); } } } } </script>
this.$refs.countの下に$attrsという属性値が格納されたプロパティがあるので、そこから最大値と最小値を取り出します。- 文字もしくは最小値以下だった場合は、最小値を代入
- 最大値を超えた値が入力された場合は最大値を代入
完成!!
これでchromeでもsafariでも問題なく、要件を満たすことができました!
this.$refs.count.lazyValueを見つけるまでは、ネットで似たようなことをしている記事を探しては試していました。しかし完璧に要件を満たせる方法がなくて諦めかけてたところ、何となくconsole.log(this.$refs.count)を書いてプロパティみてたらそれっぽい値を発見しました!そして、うまく値を書き換えることに成功笑結構稀な対応かとは思いますが、同様のパターンを実装されたい方は是非参考にしていただけると幸いです。
もっと良い案があればコメントください!ではでは!
- 投稿日:2020-08-20T00:56:19+09:00
商品出品機能のコード全文
フリマアプリの商品出品機能
最終課題で実装した商品出品ページの全コードを記録用に書きます!
機能
・ancestry:多階層カテゴリー
・ActiveStorage:画像投稿
・jp_prefecture:都道府県を扱うGem
・active_hash:静的データ作成Active Storageのセットアップ
下記をみて導入方法を確認する
Rails ガイドルーティング
routes.rbRails.application.routes.draw do devise_for :users, controllers: { registrations: 'users/registrations', } devise_scope :user do get 'sending_destinations', to: 'users/registrations#newSendingDestination' post 'sending_destinations', to: 'users/registrations#createSendingDestination' end root "items#index" resources :items, only: [:new, :create, :update] do collection do get 'get_category_child', to: 'items#get_category_child', defaults: { format: 'json' } get 'get_category_grandchild', to: 'items#get_category_grandchild', defaults: { format: 'json' } end end endコントローラー
items_controllerclass ItemsController < ApplicationController def index @items_category = Item.where("buyer_id IS NULL AND trading_status = 0 AND category_id < 200").order(created_at: "DESC") @items_brand = Item.where("buyer_id IS NULL AND trading_status = 0 AND brand_id = 1").order(created_at: "DESC") end def new @item = Item.new @item.item_imgs.new @category_parent = Category.where(ancestry: nil) # 親カテゴリーが選択された後に動くアクション def get_category_child @category_child = Category.find("#{params[:parent_id]}").children render json: @category_child #親カテゴリーに紐付く子カテゴリーを取得 end # 子カテゴリーが選択された後に動くアクション def get_category_grandchild @category_grandchild = Category.find("#{params[:child_id]}").children render json: @category_grandchild #子カテゴリーに紐付く孫カテゴリーの配列を取得 end end def create @item = Item.new(item_params) unless @item.valid? @item.item_imgs.new render :new and return end @item.save redirect_to root_path end private def item_params params.require(:item).permit(:name, :introduction, :price, :prefecture_code, :brand_id, :pref_id, :size_id, :item_condition_id, :postage_payer_id, :preparation_day_id, :postage_type_id, :category_id, :trading_status, item_imgs_attributes: [:url, :id]).merge(seller_id: current_user.id) endHaml
sell.html.haml.sell-container = form_for @item do |f| -# 画像部分 .sell-container__content .sell-title %h3.sell-title__text 出品画像 %span.sell-title__require 必須 .sell-container__content__box %ul.output-box %div#image-input = f.fields_for :item_imgs do |image| = image.label :url, id: 'image-input__label' do = image.file_field :url, accept: "image/*", class: "js-file", data: {index: image.index}, style: 'display: none;' %pre %i.fas.fa-camera.fa-lg .error-messages %p =@item.errors.messages[:item_imgs][0] -# 商品概要部分 .sell-container__content .sell-title %h3.sell-title__text 商品名 %span.sell-title__require 必須 = f.text_field :name, {class: 'sell-container__content__name', maxlength: '40', placeholder: '商品名(必須 40文字まで)'} .error-messages %p =@item.errors.messages[:name][0] .sell-title %h3.sell-title__text 商品の説明 %span.sell-title__require 必須 = f.text_area :introduction,{class: 'sell-container__content__description', rows: '7', maxlength: '1000', placeholder: '商品説明'} .sell-container__content__word-count %p.error-messages =@item.errors.messages[:introduction][0] %span#word-count 0/1000 -# 詳細部分 .sell-container__content %h3.sell-sub-head 商品の詳細 .sell-container__content__details .sell-title %h3.sell-title__text カテゴリー %span.sell-title__require 必須 .sell-collection_select.category .select_collection_select-category = f.collection_select :category_id, @category_parent,:id, :name, {prompt: "--"},{class: 'sell-collection_select__label', id:'parent_category'} .error-messages %p =@item.errors.messages[:category_id][0] .sell-title %h3.sell-title__text サイズ %span.sell-title__require 必須 .sell-collection_select = f.collection_select :size_id, Size.all, :id, :value, {prompt: "--"} ,{class: 'sell-collection_select__label'} .error-messages %p =@item.errors.messages[:size_id][0] .sell-title %h3.sell-title__text ブランド %span.sell-title__require.arbitrary 任意 .sell-collection_select = f.collection_select :brand_id, Brand.all, :id, :name, {prompt: "--"} ,{class: 'sell-collection_select__label'} .error-messages .sell-title %h3.sell-title__text 商品の状態 %span.sell-title__require 必須 .sell-collection_select = f.collection_select :item_condition_id, ItemCondition.all, :id, :name, {prompt: "--"}, {class: 'sell-collection_select__label'} .error-messages %p =@item.errors.messages[:item_condition_id][0] -# 配送部分 .sell-container__content %h3.sell-sub-head %p 配送について .sell-container__content__details .sell-title %h3.sell-title__text 配送方法 %span.sell-title__require 必須 .sell-collection_select = f.collection_select :postage_type_id, PostageType.all, :id, :name, {prompt: "--"}, {class: 'sell-collection_select__label'} .error-messages %p =@item.errors.messages[:postage_type_id][0] .sell-title %h3.sell-title__text 配送料の負担 %span.sell-title__require 必須 .sell-collection_select = f.collection_select :postage_payer_id, PostagePayer.all.map, :id, :name, {prompt: "--"}, {class: 'sell-collection_select__label'} .error-messages %p =@item.errors.messages[:postage_payer_id][0] .sell-title %h3.sell-title__text 発送元の地域 %span.sell-title__require 必須 .sell-collection_select = f.collection_select :prefecture_code, JpPrefecture::Prefecture.all, :code, :name, {include_blank: '--'}, class: 'sell-collection_select__label' .error-messages %p =@item.errors.messages[:prefecture_code][0] .sell-title %h3.sell-title__text 発送までの日数 %span.sell-title__require 必須 .sell-collection_select = f.collection_select :preparation_day_id, PreparationDay.all, :id, :value, {prompt: "--"}, {class: 'sell-collection_select__label'} .error-messages %p =@item.errors.messages[:preparation_day_id][0] -# 価格部分 .sell-container__content %h3.sell-sub-head %p 販売価格(300〜9,999,999) .sell-container__content__price .sell-title %h3.sell-title__text 販売価格 %span.sell-title__require 必須 .sell-container__content__price__form = f.label :price, class: 'sell-container__content__price__form__label' do ¥ = f.number_field :price, {placeholder: '0', value: '', autocomplete:"off", class: 'sell-container__content__price__form__box'} .error-messages#error-price =@item.errors.messages[:price][0] .sell-container__content__commission .sell-container__content__commission__left 販売手数料 (10%) .sell-container__content__commission__right ー .sell-container__content__commission .sell-container__content__commission__left 販売利益 .sell-container__content__commission__right ー .submit-btn = f.submit '出品する', class: 'submit-btn--Btn' .submit-btn = link_to 'もどる', root_path, class: 'submit-btn--Btn return' .attention-box %p 禁止されている行為および出品物を必ずご確認ください。偽ブランド品や盗品物 などの販売は犯罪であり、法律により処罰される可能性があります。また、出品をもちまして加盟店規約に同意したことになります。必須項目が見入力の場合、エラーメッセージが出るように設定しています。
また、住所はjp_prefectureという都道府県を扱うGemを使用しました!*ポイント*
ボックスはf.collectionに統一して記述する。
f.selectを使用するとvalueが文字でDBに送られる為、idが0で登録される為、後々の商品詳細ページでデータを表示させる時にエラーが出ます。JS
items.js$(document).on('turbolinks:load', ()=> { // 画像用のinputを生成する関数 const buildFileField = (index)=> { const html = `<input accept="image/*" class="js-file" data-index="${index}" style="display: none;", type="file" name="item[item_imgs_attributes][${index}][url]" id="item_item_imgs_attributes_${index}_url">`; return html; } // プレビュー用のimgタグを生成する関数 const buildImg = (index, url)=> { const html = `<img data-index="${index}" src="${url}" width="100px" height="100px">`; return html; } // file_fieldのnameに動的なindexをつける為の配列 let fileIndex = [1,2,3,4,5,6,7,8,9,10]; $('#image-input').on('change', '.js-file', function(e) { // labelタグのfor属性を変更 $('#image-input__label').attr('for', 'item_item_imgs_attributes_' + fileIndex[0] + '_url'); // fileIndexの先頭の数字を使ってinputを作る $('#image-input').append(buildFileField(fileIndex[0])); fileIndex.shift(); // 末尾の数に1足した数を追加する fileIndex.push(fileIndex[fileIndex.length - 1] + 1) const targetIndex = $(this).parent().data('index'); // ファイルのブラウザ上でのURLを取得する const file = e.target.files[0]; const blobUrl = window.URL.createObjectURL(file); $('#image-input').before(buildImg(targetIndex, blobUrl)); }); });JSでは大きく分けて2つ。
①画像のプレビュー
②Inputタグの生成get_category_child.json.jbuilderjson.array! @category_child do |child| json.id child.id json.name child.name endget_category_grandchild.json.jbuilderjson.array! @category_grandchild do |grandchild| json.id grandchild.id json.name grandchild.name endモデルは割愛します。
今回は以上です。
間違いなどあればご指摘いただけますと幸いです^^






















































