- 投稿日:2020-12-08T23:16:35+09:00
GASとClaspとTypeScriptについて・・・書けませんでした
GASで動かしているプログラムって、
- Githubで管理できない
- 本番環境(Webエディタ上)でしか実行できない
- ECMAScriptだから型とか不安がある
という欠点がありますよね。
そこで、Google先生が提供している clasp というライブラリを利用すると、ローカルでGAS開発ができるので、ソースコードのGithub管理が不要らしいんです!
さらにTypeScriptにも対応したらしいんです!
らしいというのは、まだ手元で動かせてないんです (ToT)そのうち手元で動かしてこの文章アップデートします!!
参考文献
https://qiita.com/HeRo/items/4e65dcc82783b2766c03
https://qiita.com/HeRo/items/f2ce057c6b1456e896ad
- 投稿日:2020-12-08T22:39:41+09:00
【JavaScript】ESModulesの使い方 import・export
はじめに
VueCLIを勉強中しているのですが、モジュールの使い方について、しっかりとした理解が必要だと気づいたので、今回はESModeulsの使い方についてまとめておきます。
大前提としてモジュール管理とは何かという話ですが、
「ソースコードを分割して、メンテナンスをし易くするための仕組み」のことです。1つのファイルに全ての処理を記述すると、当然ですがコードは肥大化してきますし、メンテナンスも大変です。
その為、機能ごとに別ファイル(モジュール)に分割して必要に応じて読み込むことで、より効率なコードになるわけです。
ESModulesとは、ECMAScriptの規格に基づいたモジュール管理の仕組みで、主にブラウザ上で動作します。importやexportというキーワードを使って、モジュールの読み込みや出力を行います。
他にもNode.jsで動作する、Common.jsというモジュール管理の仕組みもあります。
こちらはrequireやexportsというキーワードを使い、モジュールの読み込みを行います。(しっかりと試せていませんが)モジュール管理する方法は、実行環境などによって異なる種類があるということですね。
ESModulesを試す
では、実際にESModulesのモジュール管理のやり方を試していきましょう。
まず、moduleAとmoduleBという名前のjsファイルと、ブラウザで結果を確認するのでhtmlを用意します。moduleB.jslet num = 10; function methodA(){ console.log("This number : " + num); } //num変数と、methodAメソッドをエクスポート。 export {num,methodA};moduleA.js//インポート import {num ,methodA} from "./moduleB.js"; console.log(num); // 10 methodA(); // This number : 10index.html<body> //typeをmoduleと指定する。 <script type="module" src="moduleA.js"></script> </body>まず最初に、モジュール管理を行う場合、スクリプト読み込み時に
type="module"
を指定する必要があります。この指定が無いとESmodulesは使えません。moduleBでは、オブジェクトリテラルにexportしたい変数や関数を指定します。
moduleAでは、importの後に、moduleBでの指定と同じ様に
{num ,methodA}
とします。ここで、export時と異なる命名にすると、エラーになるので注意しましょう。そして、fromの後に、どのファイルからのimportをするかを指定します。ちなみにファイル指定では、../などパス指定をする必要があります。
from 'moduleB.js'
のように、ファイル名だけではエラーとなります。また、拡張子の.jsも省略できないのでご注意を。こうすることで、moduleAでmoduleBの変数や関数を扱うことができるようになりました!
またexportとimportのモジュール名は同名にする必要があると言いましたが、asを使うことで別名の変数や、関数に変更することも可能です。
moduleA.js//asで名前変更 import {num as n ,methodA as A} from "./moduleB.js"; console.log(n); // 10 A(); // This number : 10モジュール管理の基本的な流れは、こんな感じですが、
実はexportの方法は2種類あります。上のやり方は名前付きexportとなります。
次は、もう1つのexport defaultのやり方を見ていきましょう。
export default
moduleB.jsclass Person{ constructor(val){ this.val = val; } print(){ console.log(this.val * 2); } } //デフォルトエクスポート export default Person;moduleA.jsimport Person from "./moduleB.js"; const person = new Person(10); console.log(person.val); //10 person.print(); // 20今回はmoduleBでクラスを作り、それをmoduleAでimportします。
名前付きexportとの違いは、出力できるモジュールの数です。
名前付きexportでは、先ほどの例の様に、変数や関数など複数のモジュールをエクスポートできますが、
export defaultの場合、エクスポートできるモジュールの数は1つとなります。
なので例えば、下記の様に複数のクラスをexport defaultすることはできません。
moduleB.jsclass Person{ constructor(val){ this.val = val; } print(){ console.log(this.val * 2); } } class Child{ constructor(val){ this.val = val; } print(){ console.log(this.val * 2); } } //複数エクスポートはできない。エラー export default {Person,Child}; //SyntaxError:また、defaultを使った場合、importのモジュール名はasを使うことなく変更することができます。
moduleA.js//例えばモジュール名をPに変更 import P from "./moduleB.js"; //Pでインスタンス化 const person = new P(10); console.log(person.val); person.print();ちなみに、defaultは1つのスクリプトファイルに対して、1つという決まりがありますが、
名前付きexportは複数記述が可能です。
なので、例えば以下の様に複数に分けてexportをすることもできます。
moduleB.jsclass Person{ constructor(val){ this.val = val; } print(){ console.log(this.val * 2); } } let num = 300; function methodA(){ console.log("This number : " + num); } function methodB(){ console.log("Hello World"); } export default Person; //exportを複数回使用 export {num,methodA}; export {methodB};importでも、分割して読み込むことができます。
まとめて読み込みたい場合は、下の様にまとめて読み込むことも可能です。moduleA.js//分けることもできる。必要に応じて読み込み。 import P from "./moduleB.js"; import {num} from "./moduleB.js"; import {methodA} from "./moduleB.js"; import {methodB} from "./moduleB.js"; //まとめてimportもできる。 import P,{num, methodA,methodB} from "./moduleB.js";以上、ESmodulesでのモジュール管理についてでした。ありがとうございました。
参考
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/export
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/import
- 投稿日:2020-12-08T22:31:51+09:00
cordova-inappbrowserでwindow.open()が効かなくなった
起こったこと
- cordovaを使ったハイブリッドアプリを実装中のできごと。他SNSでのシェアを、
window.open(url, "_system")
でやっていたが、突然動かなくなる。- 「あれ?ビルドミスった?」とあわてる。
- いろいろ調べて、答えらしきものにたどり着く
なんだったのか
The window.open clobber was removed in the InAppBrowser v4 release
- inappbrowserのv4以降では、window.openが使えなくなってるよ、とのこと
- そして
window.cordova.InAppBrowser.open()
をつかってね、とのことでした- いろいろ調べてみたら日本語の記事もばっちりありました〜〜
- 投稿日:2020-12-08T22:11:34+09:00
【JavaScript】try...catch文、throw文について【例外処理】
Nuxt.jsの教材で初めて見て「なにこれ?」となったので
throw文
開発者が明示的にエラーを投げる(throw)ことが出来る。
main.jsthrow 'エラーです!!'
ただ、直接文字列を投げるのはNGらしい。
(スタックトレースを取得できない為)
Error
オブジェクトを使うのが正解main.jsthrow new Error('エラーです!!');
何も投げないのもNG
main.jsthrow;
エラーが発生した時点で処理が止まります
main.jsconsole.log('実行されます!!'); throw new Error('エラーです!!'); console.log('実行されません!!');try...catch文
エラー発生時に何らかの処理を続行できる
main.jstry { <エラーが発生する可能性のある処理> } catch(<エラー>) { <エラー時の処理> } finally { <後処理(エラーが有っても無くてもする処理)> }main.jstry { console.log('実行されます!!'); throw new Error('エラーです!!'); console.log('実行されません!!'); } catch (e) { console.log(e); } finally { console.log('後処理!!'); }
catch
でエラーが処理されるとそのエラーが無かったことになり、実行が継続される。
catch
、finally
はそれぞれ省略できる、
だが、両方省略はできない。
finally
を省略するmain.jstry { console.log('実行されます!!'); throw new Error('エラーです!!'); console.log('実行されません!!'); } catch(e) { console.log(e); }
catch
を省略するmain.jstry { console.log('実行されます!!'); throw new Error('エラーです!!'); console.log('実行されません!!'); } finally { console.log('後処理!!'); }しかし、この場合エラーは処理されていないので、外側のエラーが発生する。
エラー内容が入っている
e
も省略できるmain.jstry { console.log('実行されます!!'); throw new Error('エラーです!!'); console.log('実行されません!!'); } catch { console.log('こっちがホントのエラーです!!'); } finally { console.log('後処理!!'); }
catch
で再度エラーを投げるmain.jstry { console.log('実行されます!!'); throw new Error('エラーです!!'); console.log('実行されません!!'); } catch(e) { throw new Error(e); } finally { console.log('後処理!!'); } console.log('実行されません!!');
catch
で再度エラーを投げてもfinally
の処理は実行される。ネストもできる
main.jstry { console.log('実行されます!!'); try { console.log('実行されます!!'); throw new Error('エラーです!!'); console.log('実行されません!!'); } catch (e) { console.log('内側' + e); } } catch (e) { console.log('外側' + e); }この場合エラーは内側でのみ処理される。
関数からもエラーを補足できる
main.jsconst error = () => { throw new Error('関数からエラーです!!'); console.log('実行されません!!'); } try { console.log('実行されます!!'); error() console.log('実行されません!!'); } catch (e) { console.log(e); } finally { console.log('後処理!!'); }
- 投稿日:2020-12-08T22:10:44+09:00
DroidScriptを(windows10)NoxPlayerでインストールして、自作javascriptを実行してみた。
大丈夫ですか?
(参考)NoxPlayerに危険性はある?利用時に考えられる3つの可能性
https://mayonez.jp/topic/1088522環境:
windows10
NoxPlayer バージョン7.0.0.7 2020/11/27
DroidScript バージョン1.80(Android 要件4.1 以上) 更新日2020年1月20日自作javascript:
DroidScriptで、3行3列のボタンのレイアウトをループや配列を使って、短くなりますか?
https://ja.stackoverflow.com/questions/72486/droidscript%e3%81%a7-3%e8%a1%8c3%e5%88%97%e3%81%ae%e3%83%9c%e3%82%bf%e3%83%b3%e3%81%ae%e3%83%ac%e3%82%a4%e3%82%a2%e3%82%a6%e3%83%88%e3%82%92%e3%83%ab%e3%83%bc%e3%83%97%e3%82%84%e9%85%8d%e5%88%97%e3%82%92%e4%bd%bf%e3%81%a3%e3%81%a6-%e7%9f%ad%e3%81%8f%e3%81%aa%e3%82%8a%e3%81%be%e3%81%99%e3%81%8b?noredirect=1&lq=1
- 投稿日:2020-12-08T21:29:01+09:00
k6を使って負荷テストをやってみる
はじめに
負荷テスト=JMeterって疑いもなく思って今いたが、k6というパフォーマンス計測ツールを知ったので、試してみました。
公式サイトはこちら
https://k6.io/JavaScriptで負荷テストのコードが書けるのですごく便利です。
環境
- Windows 10
- WSL2
- Docker Desktop
- Ubunt 20.04
インストール
インストールしてもいいのですが、Dockerコンテナが提供されているので、それを利用することもできます。
公式サイトに従って、インストールします。
$ sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 379CE192D401AB61 $ echo "deb https://dl.bintray.com/loadimpact/deb stable main" | sudo tee -a /etc/apt/sources.list $ sudo apt-get update $ sudo apt-get install k6Docker使うなら、こうですね。
$ docker pull loadimpact/k6
負荷シナリオを書く
負荷シナリオはJavaScriptで書きます。以下のような形になるのですが、
リクエストの部分を、シナリオ風にしてもいいし、同じリクエストを繰り返して実行することもできます。
使えるAPIはマニュアルを見るのが一番ですね。こちら
あとは、サンプル集も参考になります。こちらimport http from "k6/http"; import { check } from "k6"; // 最初に1回だけ実行される export function setup() { console.log("setup"); } // ここにテストシナリオを記載する export default function() { let url = 'http://xxxxxx.com/api/xxxxx/xxxx'; const payload = JSON.stringify(data); const params = { headers: { 'Content-Type': 'application/json;charset=UTF-8', } }; const response = http.post(url, payload, params); }; // 最後に一回実行される export function teardown(data) { console.log("teardown"); }負荷シナリオを実行する
では実行してみます。
k6 run example.jsと実行すると、こんな感じで実行結果が得られます。
data_received..............: 14 MB 79 kB/s data_sent..................: 8.1 MB 45 kB/s http_req_blocked...........: avg=1.96ms min=200ns med=300ns max=3.19s p(90)=700ns p(95)=1.1µs http_req_connecting........: avg=1.64ms min=0s med=0s max=3.16s p(90)=0s p(95)=0s http_req_duration..........: avg=129.67ms min=41.77ms med=115.8ms max=1.39s p(90)=163.85ms p(95)=187.93ms http_req_receiving.........: avg=345.74µs min=21µs med=99.05µs max=213.61ms p(90)=326.04µs p(95)=565.87µs http_req_sending...........: avg=258.93µs min=25.2µs med=112.6µs max=91.84ms p(90)=318.97µs p(95)=444.83µs http_req_tls_handshaking...: avg=226.59µs min=0s med=0s max=107.53ms p(90)=0s p(95)=0s http_req_waiting...........: avg=129.07ms min=41.63ms med=115.38ms max=1.39s p(90)=163.16ms p(95)=187.02ms http_reqs..................: 7224 39.837763/s iteration_duration.........: avg=999.17ms min=1.3µs med=995.64ms max=4.16s p(90)=1.04s p(95)=1.06s iterations.................: 7224 39.837763/s vus........................: 7 min=7 max=40 vus_max....................: 40 min=40 max=40http_req_duration、http_reqsあたりが、性能指標値として使いやすいかと思います。
ユーザ数や実行時間などを指定する
よく使うオプションを紹介します。
-u ユーザ数を指定する 5、10
-d 実行時間を指定する 180s、10m、などを指定する。デフォルト値は30秒
--rps 指定したスループットになるように時刻を調整k6 run -u 180 -d 180s --rps 180実行結果をinfluxdbに登録する
k6には標準で、便利な機能が備わっていて、influxdb実行結果を登録することができます。
k6 run --out influxdb=http://localhost:8086/k6db -u 180 -d 180s --rps 180k6dbというローカルに立てたinfluxdbに登録できます。その結果をGrafanaでグラフ化。
わずか1時間程度で、計測→集計が完了。超らくちん。こんなグラフがすぐできます。さいごに
JMeterに代わるものとして、k6を使ってみました。
今回、新しいAPIの機能の性能計測に利用しましたが、使い始めてから計測、評価完了まで1週間もかからず。
JavaScriptなので学習コストも低く、influxdbとの連携で、ログの集計とかもらくちんでした。もう少し大きいシナリオを書いたりすると、少し難しい面も出てくる気がしますが、
現時点では、すごくいいツールという感想です。またほかの機能を使ってみたら記事にしようとおもいます。
- 投稿日:2020-12-08T20:58:20+09:00
jsで外部サイトのファイルをダウンロードする
ファイルをダウンロードさせたい
aタグの
download
属性を指定するとページ移動ではなくファイル保存させることができますが
同一originでしか動作しません。代替手段
fetchでファイルを取得後に「FileSaver」のsaveAsで保存させることで実現することができます
https://github.com/eligrey/FileSaver.js/const videoDownload = async (url: string) => { const data = await fetch(url); const blob = await data.blob(); saveAs(blob); };<a onClick="videoDownload('http://[ダウンロードさせたいファイルのURL]')"></a>
- 投稿日:2020-12-08T20:36:05+09:00
JavaScript classの定義
今日はJavaScriptにおけるclass定義についてまとめてみます。
class
クラスは値やメソッドといった様々な機能を自身で持つことのできるオブジェクトのようなものであり、クラスを定義することでコードの再利用性が上がったり、保守運用が容易になるといったメリットがあります。
このような指向でプログラムを組むことはオブジェクト指向ともいわれます。定義したクラスはnew演算子を使用することでインスタンス化を行います。
インスタンスが生成されるタイミングでクラス内で定義した機能が実行されます。
このクラスとインスタンスの関係性はしばしば、クラスを実態のない設計図、インスタンスを設計図から作られる実態を持った物とも表現されます。例えばアニマルクラスを定義するとします。
クラス内に名前、鳴き声、足の本数...といったように性質を与えます。
この設計図をもとにインスタンス化を行う時、新たに作成したい動物の名前、鳴き声、足の本数、などを与えてやることで様々な種類の動物を生み出せます。コードで見てみます。
animal.jsclass Animal{ //コンストラクタ関数 constructor(name, bark, foot){ this.name = name; this.bark = bark; this.foot = foot; } }クラスを定義する際はclassの後に任意の名前をつけます。
名前は慣習的に最初の文字を大文字にします。
クラス名に続けて{ }内に値や、機能を定義するのですが、まずconstructor関数という関数を実行することでこのクラスを初期化します。
上記の場合は引数にそれぞれ、名前、鳴き声、足の本数といった値を渡しているのですが、constructor関数はクラスをインスタンス化した際に必ず実行される関数であるため、インスタンスによって異なる値をこのクラス内で使用できるようになります。animal.jsclass Animal{ //コンストラクタ関数 constructor(name, bark, foot){ this.name = name; this.bark = bark; this.foot = foot; } } //インスタン化 const dog = new Animal('犬', 'ワン', 4); const cat = new Animal('猫', 'ニャー', 4);上記のように動物の種類によって渡してやる値を変えれば、似たような機能を持った様々なインスタンスを作成することができます。
そしてクラスではメソッドが定義でき、このメソッド内ではコンストラクター関数内の変数を使用できます。
animal.jsclass Animal{ constructor(name, bark, foot){ this.name = name; this.bark = bark; this.foot = foot; } introduce(){ console.log(`我輩は${this.name}である。${this.bark}と鳴き、足は${this.foot}本である。`); } } //インスタンス化して変数に代入 const dog = new Animal('犬', 'ワン', 4); const cat = new Animal('猫', 'ニャー', 4); //クラスメソッドの実行 dog.introduce(); cat.introduce();コンソール出力結果
我輩は猫である。ニャーと鳴き、足は4本である。 我輩は犬である。ワンと鳴き、足は4本であるクラス内のメソッドの実行はオブジェクト同様、ドット記法で変数名に続けてメソッド名を記述し( )をつけることで実行。
上記のように、インスタンスによって戻ってくる値が変わる機能をクラス内に定義することで、重複する記述を減らすことができたり、コードの修正もしやすくなるというメリットが生まれる。
以上がクラスの基本的な内容です。
駆け足での説明、至らない点なども多いですが指摘箇所などございましたら指導いただけると幸いです。
- 投稿日:2020-12-08T19:55:11+09:00
DeepLで翻訳前と翻訳後のテキストをまとめてコピーするスニペット
はじめに
対象読者
- DeepL を頻繁に使っている人
- DeepL の翻訳前テキストと翻訳後テキストを、別の箇所に同時に貼り付けるのを楽にしたい人
前置き
- 2020/12 時点での DeepL の仕様に依存します
- 特に DOM に指定されている id 名
動作環境
- Chrome 86 / にて確認
- devtool の console にてスニペットを実行してください
- 一度実行したコマンドは、次回実行時は十字キーの
↑
を入力することで履歴から呼び出せるので、そうして再実行させていくのが楽で便利です完成形
スニペット
let beforetext = document.getElementById('source-dummydiv').textContent let aftertext = document.getElementById('target-dummydiv').textContent let textarea = document.createElement('textarea') textarea.textContent = aftertext + '\n' + beforetext let box = document.getElementById('dl_translator') box.appendChild(textarea) textarea.select() document.execCommand('copy') box.removeChild(textarea)クリップボードに格納されたテキスト
I shouldn't have time to write articles like this or make snippets, but I'm escaping reality.
However, I should have been able to get out of the lumberjack dilemma.
このような記事を書いたりスニペットを作ったりしている時間はないはずなのに、現実逃避をしている。
ただし、木こりのジレンマからは脱せられているはずだ。中身の解説
テキストを取得
- 翻訳前テキスト
let beforetext = document.getElementById('source-dummydiv').textContent
- 翻訳後のテキスト
let aftertext = document.getElementById('target-dummydiv').textContentテキストエリアに格納
- 二つのテキストをまとめて格納するための要素を作成
let textarea = document.createElement('textarea')
- 二つのテキストを改行区切りで格納
- ここでは個人的な用途から翻訳後テキストを上にしています
- 上下逆にしたい人は、逆にしてください
textarea.textContent = aftertext + '\n' + beforetexttextarea を一時的に 近くの ID 内に作成
- 作成させたい ID 要素の定義
let box = document.getElementById('dl_translator')
- box 要素内に textarea 要素を作成
box.appendChild(textarea)textarea にカーソルを入れてコピー
- テキスト選択
textarea.select()
- コピー
document.execCommand('copy')後処理として textarea 要素の削除
box.removeChild(textarea)おわりに
感想
- Slack でやり取りする際、多国籍のチームがあるため、二つの言語を両方コピペしている日々でした。これによってちょっと……いや、だいぶ楽になった……? と思う。
参考記事
- Webllica - JavaScript でテキストをクリップボードへコピーする方法
- これは本当、うまいこと考えたものだなーと感心しました。感謝。
- 投稿日:2020-12-08T19:39:52+09:00
PugとScssをまとめなさいと天命を受けた私はwebpackプラグインを生み出した
こんにちは。NIJIBOXのエンジニアのつんあーです。
最近は社内で"あーさん"と呼んでいただいているので、
そろそろQiitaの表示名も直した方がいいのかなと思っています。去年のアドカレは全面的にネタに振っていたのですが、
今年は割と正直に成果物で勝負をかけてみる方針にしました。力こそパワーだもんねっ!★
本日のお品書き
前々から「あったらいいなぁ」と思っていた
Scss in Pug
なwebpackプラグインを作ってみたので、ご紹介いたします。
- pug-stylekit-webpack-pluginのご紹介
- 概要
- 使い方
- モチベーション
- あんなこといいな、できたらいいな
- おまけ:webpackプラグインの作り方と、pug-stylekit-webpack-pluginのコード解説
自分的には絶対ウケると思って「雨が夜明け過ぎに雪へと変わるChrome拡張」を作ろうとしていたのですが、
構想を知人に壁打ちしたところ2秒で「何に使うのソレ」とFBをいただいたため潔く却下した次第です。「無駄や失敗にまみれた不本意な毎日こそ、人生を形作っているものだと思う」
by 山田ルイ53世私の人生を形作っているものは、しょうもなさそうなものから再び価値を見出すことへの興味でできているのかもしれません。
ルネサンスですね。違いますね。
pug-stylekit-webpack-pluginのご紹介
概要
最新バージョン:0.8.11
→まだまだ機能を充実させる余地があるのと、十分なテストもしていないのでバージョンは低めです。GitHub:https://github.com/ats05/pug-stylekit-webpack-plugin
npm:https://www.npmjs.com/package/pug-stylekit-webpack-pluginpug-stylekit-webpack-pluginは、Pugファイルの中に直接Scss(Sass)を書き込み、ビルドできるプラグインです。
Pugをhtmlファイルにコンパイルしつつ、
Pug中に挿入されたstylekitブロックをまとめてcssファイルとして、
それぞれビルドディレクトリ内に書き出すことができます。下記のサンプルの
//- stylekit
というブロックコメントが、
pug-stylekit-webpack-pluginで認識できるstylekitブロックです。
▼インプット:index.pug
index.pugdoctype html html(lang='ja') head meta(charset="utf-8") link(rel="stylesheet" href="./style.css") body.body h1.body__title Hello, this is the pug-stylekit-webpack-plugin! //- stylekit .body { &__title { color: #FF0000; } }▼アウトプット:index.html
index.html<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <link rel="stylesheet" href="./style.css"> </head> <body class="body"> <h1 class="body__title">Hello, this is the pug-stylekit-webpack-plugin!</h1> </body> </html>(↑見やすいように整形しています)
▼アウトプット:style.css
style.css.body__title { color: #FF0000; }使い方
すでにnpmで公開済みなので、下記コマンドでインストールができます。
npm i --save-dev pug-stylekit-webpack-plugin
webpackの設定
ミニマム設定のサンプルは以下の通りです。
オプションを指定してplugins
に入れてあげるだけでOKです。webpack.config.jsconst path = require('path'); const SOURCE_DIR = path.resolve(__dirname, 'src'); const OUTPUT_DIR = path.resolve(__dirname, 'dist'); const PugStyleKitWebpackPlugin = require('pug-stylekit-webpack-plugin'); module.exports = { entry: SOURCE_DIR + '/entry.js', output: { filename: 'build.js', path: OUTPUT_DIR }, module: {}, plugins: [ new PugStyleKitWebpackPlugin({ target: { from: SOURCE_DIR + '/index.pug', to: { html: OUTPUT_DIR + '/index.html', css: OUTPUT_DIR + '/style.css', }, } }), ] };
オプションの書き方
▼基本的な書き方はこんな感じ。
target.from
:書き出し対象のPugファイルtarget.to.html
:htmlファイルの書き出し先target.to.css
:cssファイルの書き出し先ex)
webpack.config.js... new PugStyleKitWebpackPlugin({ target: { from: SOURCE_DIR + '/index.pug', to: { html: OUTPUT_DIR + '/index.html', css: OUTPUT_DIR + '/style.css', }, } }), ...▼
target
オプションに配列を渡すことで、複数ファイルのビルドが可能です。ex)
webpack.config.js... new PugStyleKitWebpackPlugin({ target: [ { from: SOURCE_DIR + '/index.pug', to: { html: OUTPUT_DIR + '/index.html', css: OUTPUT_DIR + '/style.css', }, }, { from: SOURCE_DIR + '/about/index.pug', to: { html: OUTPUT_DIR + '/about/index.html', css: OUTPUT_DIR + '/about/style.css', }, } ], }), ...▼
target.to.css
オプションを省略すると、cssファイルは生成せず、単なるPugのコンパイラとして機能します。ex)
webpack.config.js... new PugStylekitWebpackPlugin({ target: { from: SOURCE_DIR + '/index.pug', to: { html: OUTPUT_DIR + '/index.html', }, } }), ...
コードの書き方
stylekit
というラベルをつけたブロックコメントの中に、Scssのコードを記述します。
このコードはPugではブロックコメントとして認識されるため、htmlには出力されません。stylekitブロックの内部では、基本的にScss(Sass)の構文がすべて使えます。
Pugファイルでinclude、extends等を行った場合、
include先のPugファイルに含まれるstylekitブロックも、include元のcssにまとめて書き出されます。index.pug.block include ./_parts.pug p.block__element ほげほげ //- stylekit .block { &__element { color: #FF0000; } }_parts.pug.parts .parts__child //- stylekit .parts { &__child { color: #00FF00; } }▼出力結果
style.css.parts__child { color: #00FF00; } .block__element { color: #FF0000; }モチベーション
最近は業務でフロントエンド領域を触ることが増えてきており、
コーディング環境の初期構築を行ったりすることも多々あります。ぶっちゃけ、PugとScssでディレクトリ構造を合わせるの、結構しんどくないでしょうか?
モジュール化の粒度が違ったり、微妙にネーミングが変わったりしてくると一気にメンテナンス性が落ちてくる気がします。
知見者「ああ、Pugは
_navigation.pug
だけどScssは_sidebar.scss
だからね」ex)
. └── src ├── pug │ ├── index.pug │ └── modules │ └── _navigation.pug └── scss ├── index.scss └── module └── _sidebar.scss
まあPugの分割とScssの分割が必ずしも一致するわけではないので、
世界のコーダーに「ディレクトリ構造統一せんかい!」と言えるわけではないのですが、
さほど大規模でないサイト制作の中で、ムダにファイル数を増やしすぎるもの何だかなぁと思うわけです。特にBEMをとても厳密に考えると、「blockごとにScssファイルを分けなさい」というルールがあったりしますしね。
今まで何度もPugファイルの中に直接
style
タグを書き込むという悪魔の閃きと心の中で戦ってきたのですが、
「いっそのことちゃんと動くようにして作っちゃえばいいんじゃね!?」ってことで、着手した次第です。Reactの
styled-components
などと同じようなことができればいいなぁ、というような感じです。
あんなこといいな、できたらいいな
@use、@importの相対パス化
Pugファイルをモジュールとして分割していったときに、
分割したモジュールから@import
や@use
を使う可能性ってもちろんありますよね。現状だと、
target.from
に指定したPugファイルからの相対パスのみ解決できるようになっています。
が、各Pugモジュールから相対パスで取得できた方が見通しがいいと思っています。dart-sassの
includePaths
オプションとかでいけるかな?cssファイルの統合
現状、
target
に20個のPugファイルを渡すと、20個のcssファイルが出力されます。画面ごとにcssが別になるとブラウザキャッシュが効きにくくなるので、
規模によってはサイト全体の閲覧パフォーマンスが落ちてしまう場合があります。そのため、cssをまとめる機能があってもいいかなと思っています。
できたらいいな.js... new PugStyleKitWebpackPlugin({ target: [ { group: { outcss: OUTPUT_DIR + '/style.css', pugs: [ { from: SOURCE_DIR + '/index.pug', to: OUTPUT_DIR + '/index.html' }, { from: SOURCE_DIR + '/about.pug', to: OUTPUT_DIR + '/about.html' }, { from: SOURCE_DIR + '/contact.pug', to: OUTPUT_DIR + '/contact.html' } }, } } ], }), ...ただ、これを解消しようとすると、cssの完全に重複する部分をマージしつつ、
意図的に上書きしている部分は残すような作りを考える必要が出てきます。テンプレートファイル的な書き方になるのかなぁ。
Pug、Scssのコンパイルオプション
PugやScssは、コンパイル時に様々なオプションを渡せますね。
インデントを潰したりのオプションを渡せるようにしたいのですが、どこまで自由度を持たせようかなぁ、というところが悩みどころ。やっぱり、minifyくらいは必要だよね。
ソースマップの有効化
現状、Scssのソースマップをオフにしています。
これをオンにしつつ(これ自体はdart-sassにオプション渡すだけ)、
cssからどのPugファイルに書いてあるのかを辿れたら、デバッグしやすいなと思っています。stylus対応
あったほうがいいよね。
各種テスト
現状、ScssとおそらくSassも動きます。まだ試していないです。
簡単にPugのextendsやmixinも試していて、一応動くのですが、こちらももっと厳密にテストしてみた方がいいと思っています。
せっかく作ったので、こっそりといろいろなところで使ってみようと思っています。
その中で課題を見つけたり、アップデートをしたりとメンテナンスしていこうと思っています。だんだん便利にできればと思うので、どうかお見守りいただけたら幸いです。
そして、PRやissueも、お待ちしています。
私自身、OSSコミュニティに属したりといったことはほぼなく、
OSSコミュニティのルールみたいなものもあまり意識したことがありません。ゆるりとやっていこうと思っています。
「OSSなんてむりむり!」な方の練習台になったらいいな、くらいの想いです。
(おまけ)webpackプラグインの作り方と、pug-stylekit-webpack-pluginのコード解説
今回初めてwebpackのプラグインを作ってみたのですが、
いろいろと勉強になりました。
実は探してみるとplugin周りのドキュメントってあまり多くないのですね。
なので、実際のところは公式リファレンスや、公式のプラグインの実装を参考にしながら作ったりしました。
(私は正直、始めるまではloaderとpluginの違いもよくわかっていなかったです)もしwebpackプラグインを作ってみたい、と考えている方がいれば、参考にしてみてください。
loaderとpluginを私の言葉で説明すると、こんな感じ。
- loader:.js
以外のファイルを、jsにバンドルするための変換器(webpackの本来の目的ですね)
- plugin:webpackのプロセス全体にアクセスできるプログラム。もちろんwebpackと全然関係ないこともできる。嫌いなアイツのコンパイル時間を256倍にすることだってできちゃうpug-stylekit-webpack-pluginでは、
- webpackがコンパイルを行うタイミングをフックし、処理開始
- Pugファイルを読み込んでhtmlにコンパイル
- Pugファイルの中から特定の記述を切り出してつなげ、cssとしてコンパイル
- htmlとcssをwebpackの出力対象ファイルとして差し込む
という感じで、処理を行っています。
それでは、プラグインの本体となる下記のソースコードを見ていきましょう。
https://github.com/ats05/pug-stylekit-webpack-plugin/blob/main/index.jswebpackのリファレンスにも、Writing a Pluginとしてプラグインの作り方が説明されています。
まずは、骨組みを見ていきます。
プラグインの全体像
index.jsconst schemaUnits = require('schema-utils'); const {Compilation, sources: {RawSource}} = require('webpack'); ...省略... const schema = { ...省略... }; const PLUGIN_NAME = 'PugStylekitWebpackPlugin'; class PugStylekitWebpackPlugin { constructor(options = {}){ schemaUnits.validate(schema, options, { name: PLUGIN_NAME }); this.options = options; } apply(compiler) { compiler.hooks.compilation.tap(PLUGIN_NAME, compilation => { ...省略... }); } } module.exports = PugStylekitWebpackPlugin;こうしてみると結構シンプルですね。
省略したのは、私が自分で処理を組み立てた部分です。リファレンスにはこんな風に書いてあります。
A plugin for webpack consists of:
- A named JavaScript function or a JavaScript class.
- Defines apply method in its prototype.
- Specifies an event hook to tap into.気合(Google翻訳)で訳すと、
- 名前付きのJavaScript関数、もしくはクラスです。
- applyプロトタイプメソッドを定義します。
- 使用するイベントフックを指定します。
てな感じでしょうか。
要するに、最低この3要素を満たすように作れば良いのです。
これを踏まえて、上のコードから一部を抜粋します。
index.jsapply(compiler) { compiler.hooks.compilation.tap(PLUGIN_NAME, compilation => { ...省略... }); }
apply
メソッドが定義されていて、引数がcompiler
となっています。
compiler
は、webpack
コマンドで実行されるjsのコンパイル処理のことを指しています。
(上手い説明ができず、スミマセン。「ほーん」くらいのテンションで聞き流してください)詳しいことはこの辺に書いてあります。多分。
英語はフィーリングで読んでいます。アイキャンアンダースタンド。
compilerオブジェクトを使うと、
webpack
コマンドで実行されるコンパイル処理全体にアクセスできます。
コンパイル前に処理を挟んだり、コンパイル後に処理を挟んだりができます。complierオブジェクトから参照できるフックはこちらにまとまっています(めっちゃたくさんあります)
https://webpack.js.org/api/compiler-hooks/pug-stylekit-webpack-pluginでは、その中のcompilationフックを利用しています。
tap
というメソッドについては↓に解説があるのですが、正直よくわかりませんでした。むつかしい。
https://webpack.js.org/api/plugins/#tapable
要するに、コンパイル処理になんらかのプログラムを差し込むためのInterfaceのようです。
同期的(tap)だったり、非同期(tapAsync)だったり、promiseで解決させるもの(tabPromise)だったりいろいろあるので、用途に合わせてってことですかね。
compiler.hooks.compilation.tap
のコールバック関数の引数として、compilation
オブジェクトがあります。
これはコンパイル処理のコンテキストのようなもので、コンパイル対象のファイル名やコンパイル結果の吐き出し場所などが格納されています。省略していた箇所をもう少し、紐解いてみましょう。
index.jsapply(compiler) { compiler.hooks.compilation.tap(PLUGIN_NAME, compilation => { compilation.hooks.processAssets.tap( { name: PLUGIN_NAME, stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONS, }, (assets) => { ...省略... compilation.emitAsset(distPath, new RawSource(html)); ...省略... }); }); }
compilation
オブジェクトにもたくさんのフックがあります。
https://webpack.js.org/api/compilation-hooks/今回は、その中でprocessAssetsフックを利用します。
このフックは、コンパイルが完了しファイルを出力する段階に処理を差し込むことができます。
stage
属性にCompilation.PROCESS_ASSETS_STAGE_ADDITIONS
を設定して、
「アセット(=出力するファイル)を追加するフックだよ〜」てなことを教えています。
実際のところ、効用はよくわかりません。世の中には不思議がいっぱいだ。
フックの中にこんな箇所があります。
compilation.emitAsset(distPath, new RawSource(html));
distPath
とhtml
の中身はそれぞれこんな感じ。console.log(distPath) => "index.html" console.log(html) => "<!DOCTYPE html><html lang="ja"><head>....."これを実行すると、webpack.config.jsの
output.path
で指定したディレクトリに、
index.html
という名称で"<!DOCTYPE html><html lang="ja"><head>....."
という内容のファイルが出力されます。$ npx webpack [webpack-cli] Compilation finished asset index.html 214 bytes [compared for emit] <- これ asset build.js 0 bytes [compared for emit] [minimized] (name: main) ./test/src/entry.js 1 bytes [built] [code generated] webpack 5.10.0 compiled successfully in 597 msここまででようやく、webpackを通してファイルを出力できるようになりました。
ちなみに、今回は使いませんでしたが、compilerオブジェクトではjsのコンパイル処理のかなり具体的なところまでアクセスできるようです。
https://webpack.js.org/api/parser/
プラグインのオプション
ソースコードの冒頭に、こんな箇所があります。
index.jsconst schemaUnits = require('schema-utils'); ...省略... const schema = { target: { anyOf: [ { type: 'array' }, { type: 'object', propaties: { from: { type: 'string' }, to: { anyOf: [ { type: 'object', propaties: { html: { type: 'string' }, css: { type: 'string' }, }, }, { type: 'string' } ] }, }, } ] } }; ...省略... class PugStylekitWebpackPlugin { constructor(options = {}){ schemaUnits.validate(schema, options, { name: PLUGIN_NAME }); this.options = options; } ...省略...ここは何かというと、webpack.config.jsでこのプラグインを呼び出す際に設定するオプションのバリデーションを行う箇所です。
↓ここで渡すオプションですね。webpack.config.js... new PugStyleKitWebpackPlugin({ target: { from: SOURCE_DIR + '/index.pug', to: { html: OUTPUT_DIR + '/index.html', css: OUTPUT_DIR + '/style.css', }, } }), ...
schema
という変数にオプションの構造を定義しておき、コンストラクターの中でバリデーションを実行しています。
オプションの形式がschemaと合っていないと、ここでエラーが出ます。バリデーションを通ったオプションオブジェクトは、
this.options
として格納し後で使えるようにしておきます。詳しいことはこの辺に書いてあります
https://github.com/webpack/schema-utilsここまでくれば、あとはフックの中に好きな処理を書いていくだけです。
ここから先は、pug-stylekit-webpack-pluginに限った具体的な内容ですので、さらっと説明します。
処理が気になったら、実際にソースコードを覗いてみてくださいね。Pugのコンパイル
fileStreamモジュールを使ってPugファイルを読み込み、Pugパッケージを使ってコンパイル、アセットに追加します。
index.jsconst pug = require('pug'); const fs = require('fs'); ... const pugReadBuffer = fs.readFileSync(inputFile, 'utf8'); ... const html = pug.render(pugReadBuffer, options); ... compilation.emitAsset(distPath, new RawSource(html));Scssのコンパイル
Scssのコンパイルはちょっとややこしいですね。
Pugファイルからstylekitブロックを見つけ出す必要があります。pug-lexerとpug-parserという便利なパッケージがあります。
さきほどはPugパッケージを使って一気にコンパイルしましたが、
pug-parserを使ってPugの意味を解析し、stylekitブロックだけを取り出してみます。index.jsconst pugLexer = require('pug-lexer'); const pugParser = require('pug-parser'); ... _parseFile(filename) { const buffer = fs.readFileSync(filename, 'utf8'); const tokens = pugLexer(buffer, {filename}); const ast = pugParser(tokens, {filename, buffer}); return ast; } ...
ast
というのは抽象構文木(abstract syntax tree)の略です。
難しいことはおいといて、ast
の中身をみてみましょう。console.log(ast) => { "type": "Block", "nodes": [ { "type": "Doctype", "val": "html", "line": 1, "column": 1, "filename": "src/index.pug" }, { "type": "Tag", "name": "html", "selfClosing": false, "block": { "type": "Block", "nodes": [ { "type": "Tag", "name": "head", "selfClosing": false, "block": { "type": "Block", "nodes": [ { "type": "Tag", "name": "meta", "selfClosing": false, "block": { "type": "Block", "nodes": [], "line": 4, "filename": "src/index.pug" }, ... { "type": "BlockComment", "val": " stylekit", "block": { "type": "Block", "nodes": [ { "type": "Text", "val": ".body {", "line": 9, "column": 13, "filename": "src/index.pug" }, { "type": "Text", "val": "\n", "line": 10, "column": 1, "filename": "src/index.pug" }, { "type": "Text", "val": " &__title { ", "line": 10, "column": 13, "filename": "src/index.pug" }, ...長くて読みづらいですが、Pugのブロックごとにオブジェクトとして分解されて、格納されているのがわかるかと思います。
stylekitブロックは、BlockComment
タイプと、1行ずつがText
タイプのval属性として解釈されました。あとは、この
Text
タイプを1行ずつつなげたテキストを、Scssファイルとしてコンパイルするだけです。
テキストをsassBufferとして、Sass(dart-sass)パッケージに渡してコンパイルします。index.jsconst sass = require('sass') ... _createScss(blocks) { let sassBuffer = ''; ... sassBufferに書き込む... return sass.renderSync({ data: sassBuffer, outputStyle: "expanded", }); } ...
renderSync
から返ってくるresultオブジェクトは、result.css.toString()
とすることでcssのテキストに変換できます。
これを先ほど同様に、emitAsset
してあげればOKです。index.jsconst resultSass = this._createScss(styleKitBlocks); const distPath = path.relative(outputPath, outputFile.css); compilation.emitAsset(distPath, new RawSource(resultSass.css.toString()));
最後に
最後はちょっと駆け足気味になってしまいましたが、
詳細は実際のコードをみていただくのが早いかと思います。webpackはなかなか奥が深いですね。
フックの一覧をみるだけでも、もっといろいろできる気がしてきます。webpackではpluginだけでなく、loaderも自作することができます。
どちらも、イメージよりずっと簡単に実装ができます。もし機会があったら、是非チャレンジしてみてください。
去年に引き続き長文になってしまいましたが、お読みいただきありがとうございました。
弊社ではこの記事に引き続き、NIJIBOX Advent Calendar 2020として記事を公開しております。
ガッツリコードを書く記事から、チームビルディングまで多種多様な内容となっておりますので、
是非読んでみていただけますと幸いでございます。それでは、メリークリスマス。
おしまい。
※オチは特にありません。
- 投稿日:2020-12-08T19:39:52+09:00
PugとScssをまとめなさいと天命を受けたのでwebpackプラグインを生み出した
こんにちは。NIJIBOXのエンジニアのつんあーです。
最近は社内で"あーさん"と呼んでいただいているので、
そろそろQiitaの表示名も直した方がいいのかなと思っています。去年のアドカレは全面的にネタに振っていたのですが、
今年は割と正直に成果物で勝負をかけてみる方針にしました。力こそパワー。
本日のお品書き
前々から「あったらいいなぁ」と思っていた
Scss in Pug
なwebpackプラグインを作ってみたので、ご紹介いたします。
- pug-stylekit-webpack-pluginのご紹介
- 概要
- 使い方
- モチベーション
- あんなこといいな、できたらいいな
- おまけ:webpackプラグインの作り方と、pug-stylekit-webpack-pluginのコード解説
自分的には絶対ウケると思って「雨が夜明け過ぎに雪へと変わるChrome拡張」を作ろうとしていたのですが、
構想を知人に壁打ちしたところ2秒で「何に使うのソレ」とFBをいただいたため潔く却下した次第です。「無駄や失敗にまみれた不本意な毎日こそ、人生を形作っているものだと思う」
by 山田ルイ53世私の人生を形作っているものは、しょうもなさそうなものから再び価値を見出すことへの興味でできているのかもしれません。
ルネサンスですね。違いますね。
pug-stylekit-webpack-pluginのご紹介
概要
最新バージョン:0.8.11
→まだまだ機能を充実させる余地があるのと、十分なテストもしていないのでバージョンは低めです。GitHub:https://github.com/ats05/pug-stylekit-webpack-plugin
npm:https://www.npmjs.com/package/pug-stylekit-webpack-pluginpug-stylekit-webpack-pluginは、Pugファイルの中に直接Scss(Sass)を書き込み、ビルドできるプラグインです。
Pugをhtmlファイルにコンパイルしつつ、
Pug中に挿入されたstylekitブロックをまとめてcssファイルとして、
それぞれビルドディレクトリ内に書き出すことができます。下記のサンプルの
//- stylekit
というブロックコメントが、
pug-stylekit-webpack-pluginで認識できるstylekitブロックです。
▼インプット:index.pug
index.pugdoctype html html(lang='ja') head meta(charset="utf-8") link(rel="stylesheet" href="./style.css") body.body h1.body__title Hello, this is the pug-stylekit-webpack-plugin! //- stylekit .body { &__title { color: #FF0000; } }▼アウトプット:index.html
index.html<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <link rel="stylesheet" href="./style.css"> </head> <body class="body"> <h1 class="body__title">Hello, this is the pug-stylekit-webpack-plugin!</h1> </body> </html>(↑見やすいように整形しています)
▼アウトプット:style.css
style.css.body__title { color: #FF0000; }使い方
すでにnpmで公開済みなので、下記コマンドでインストールができます。
npm i --save-dev pug-stylekit-webpack-plugin
webpackの設定
ミニマム設定のサンプルは以下の通りです。
オプションを指定してplugins
に入れてあげるだけでOKです。webpack.config.jsconst path = require('path'); const SOURCE_DIR = path.resolve(__dirname, 'src'); const OUTPUT_DIR = path.resolve(__dirname, 'dist'); const PugStyleKitWebpackPlugin = require('pug-stylekit-webpack-plugin'); module.exports = { entry: SOURCE_DIR + '/entry.js', output: { filename: 'build.js', path: OUTPUT_DIR }, module: {}, plugins: [ new PugStyleKitWebpackPlugin({ target: { from: SOURCE_DIR + '/index.pug', to: { html: OUTPUT_DIR + '/index.html', css: OUTPUT_DIR + '/style.css', }, } }), ] };
オプションの書き方
▼基本的な書き方はこんな感じ。
target.from
:書き出し対象のPugファイルtarget.to.html
:htmlファイルの書き出し先target.to.css
:cssファイルの書き出し先ex)
webpack.config.js... new PugStyleKitWebpackPlugin({ target: { from: SOURCE_DIR + '/index.pug', to: { html: OUTPUT_DIR + '/index.html', css: OUTPUT_DIR + '/style.css', }, } }), ...▼
target
オプションに配列を渡すことで、複数ファイルのビルドが可能です。ex)
webpack.config.js... new PugStyleKitWebpackPlugin({ target: [ { from: SOURCE_DIR + '/index.pug', to: { html: OUTPUT_DIR + '/index.html', css: OUTPUT_DIR + '/style.css', }, }, { from: SOURCE_DIR + '/about/index.pug', to: { html: OUTPUT_DIR + '/about/index.html', css: OUTPUT_DIR + '/about/style.css', }, } ], }), ...▼
target.to.css
オプションを省略すると、cssファイルは生成せず、単なるPugのコンパイラとして機能します。ex)
webpack.config.js... new PugStylekitWebpackPlugin({ target: { from: SOURCE_DIR + '/index.pug', to: { html: OUTPUT_DIR + '/index.html', }, } }), ...
コードの書き方
stylekit
というラベルをつけたブロックコメントの中に、Scssのコードを記述します。
このコードはPugではブロックコメントとして認識されるため、htmlには出力されません。stylekitブロックの内部では、基本的にScss(Sass)の構文がすべて使えます。
Pugファイルでinclude、extends等を行った場合、
include先のPugファイルに含まれるstylekitブロックも、include元のcssにまとめて書き出されます。index.pug.block include ./_parts.pug p.block__element ほげほげ //- stylekit .block { &__element { color: #FF0000; } }_parts.pug.parts .parts__child //- stylekit .parts { &__child { color: #00FF00; } }▼出力結果
style.css.parts__child { color: #00FF00; } .block__element { color: #FF0000; }モチベーション
最近は業務でフロントエンド領域を触ることが増えてきており、
コーディング環境の初期構築を行ったりすることも多々あります。ぶっちゃけ、PugとScssでディレクトリ構造を合わせるの、結構しんどくないでしょうか?
モジュール化の粒度が違ったり、微妙にネーミングが変わったりしてくると一気にメンテナンス性が落ちてくる気がします。
知見者「ああ、Pugは
_navigation.pug
だけどScssは_sidebar.scss
だからね」ex)
. └── src ├── pug │ ├── index.pug │ └── modules │ └── _navigation.pug └── scss ├── index.scss └── module └── _sidebar.scss
まあPugの分割とScssの分割が必ずしも一致するわけではないので、
世界のコーダーに「ディレクトリ構造統一せんかい!」と言えるわけではないのですが、
さほど大規模でないサイト制作の中で、ムダにファイル数を増やしすぎるもの何だかなぁと思うわけです。特にBEMをとても厳密に考えると、「blockごとにScssファイルを分けなさい」というルールがあったりしますしね。
今まで何度もPugファイルの中に直接
style
タグを書き込むという悪魔の閃きと心の中で戦ってきたのですが、
「いっそのことちゃんと動くようにして作っちゃえばいいんじゃね!?」ってことで、着手した次第です。Reactの
styled-components
などと同じようなことができればいいなぁ、というような感じです。
あんなこといいな、できたらいいな
@use、@importの相対パス化
Pugファイルをモジュールとして分割していったときに、
分割したモジュールから@import
や@use
を使う可能性ってもちろんありますよね。現状だと、
target.from
に指定したPugファイルからの相対パスのみ解決できるようになっています。
が、各Pugモジュールから相対パスで取得できた方が見通しがいいと思っています。dart-sassの
includePaths
オプションとかでいけるかな?cssファイルの統合
現状、
target
に20個のPugファイルを渡すと、20個のcssファイルが出力されます。画面ごとにcssが別になるとブラウザキャッシュが効きにくくなるので、
規模によってはサイト全体の閲覧パフォーマンスが落ちてしまう場合があります。そのため、cssをまとめる機能があってもいいかなと思っています。
できたらいいな.js... new PugStyleKitWebpackPlugin({ target: [ { group: { outcss: OUTPUT_DIR + '/style.css', pugs: [ { from: SOURCE_DIR + '/index.pug', to: OUTPUT_DIR + '/index.html' }, { from: SOURCE_DIR + '/about.pug', to: OUTPUT_DIR + '/about.html' }, { from: SOURCE_DIR + '/contact.pug', to: OUTPUT_DIR + '/contact.html' } }, } } ], }), ...ただ、これを解消しようとすると、cssの完全に重複する部分をマージしつつ、
意図的に上書きしている部分は残すような作りを考える必要が出てきます。テンプレートファイル的な書き方になるのかなぁ。
Pug、Scssのコンパイルオプション
PugやScssは、コンパイル時に様々なオプションを渡せますね。
インデントを潰したりのオプションを渡せるようにしたいのですが、どこまで自由度を持たせようかなぁ、というところが悩みどころ。やっぱり、minifyくらいは必要だよね。
ソースマップの有効化
現状、Scssのソースマップをオフにしています。
これをオンにしつつ(これ自体はdart-sassにオプション渡すだけ)、
cssからどのPugファイルに書いてあるのかを辿れたら、デバッグしやすいなと思っています。stylus対応
あったほうがいいよね。
各種テスト
現状、ScssとおそらくSassも動きます。まだ試していないです。
簡単にPugのextendsやmixinも試していて、一応動くのですが、こちらももっと厳密にテストしてみた方がいいと思っています。
せっかく作ったので、こっそりといろいろなところで使ってみようと思っています。
その中で課題を見つけたり、アップデートをしたりとメンテナンスしていこうと思っています。だんだん便利にできればと思うので、どうかお見守りいただけたら幸いです。
そして、PRやissueも、お待ちしています。
私自身、OSSコミュニティに属したりといったことはほぼなく、
OSSコミュニティのルールみたいなものもあまり意識したことがありません。ゆるりとやっていこうと思っています。
「OSSなんてむりむり!」な方の練習台になったらいいな、くらいの想いです。
(おまけ)webpackプラグインの作り方と、pug-stylekit-webpack-pluginのコード解説
今回初めてwebpackのプラグインを作ってみたのですが、
いろいろと勉強になりました。
実は探してみるとplugin周りのドキュメントってあまり多くないのですね。
なので、実際のところは公式リファレンスや、公式のプラグインの実装を参考にしながら作ったりしました。
(私は正直、始めるまではloaderとpluginの違いもよくわかっていなかったです)もしwebpackプラグインを作ってみたい、と考えている方がいれば、参考にしてみてください。
loaderとpluginを私の言葉で説明すると、こんな感じ。
- loader:.js
以外のファイルを、jsにバンドルするための変換器(webpackの本来の目的ですね)
- plugin:webpackのプロセス全体にアクセスできるプログラム。もちろんwebpackと全然関係ないこともできる。嫌いなアイツのコンパイル時間を256倍にすることだってできちゃうpug-stylekit-webpack-pluginでは、
- webpackがコンパイルを行うタイミングをフックし、処理開始
- Pugファイルを読み込んでhtmlにコンパイル
- Pugファイルの中から特定の記述を切り出してつなげ、cssとしてコンパイル
- htmlとcssをwebpackの出力対象ファイルとして差し込む
という感じで、処理を行っています。
それでは、プラグインの本体となる下記のソースコードを見ていきましょう。
https://github.com/ats05/pug-stylekit-webpack-plugin/blob/main/index.jswebpackのリファレンスにも、Writing a Pluginとしてプラグインの作り方が説明されています。
まずは、骨組みを見ていきます。
プラグインの全体像
index.jsconst schemaUnits = require('schema-utils'); const {Compilation, sources: {RawSource}} = require('webpack'); ...省略... const schema = { ...省略... }; const PLUGIN_NAME = 'PugStylekitWebpackPlugin'; class PugStylekitWebpackPlugin { constructor(options = {}){ schemaUnits.validate(schema, options, { name: PLUGIN_NAME }); this.options = options; } apply(compiler) { compiler.hooks.compilation.tap(PLUGIN_NAME, compilation => { ...省略... }); } } module.exports = PugStylekitWebpackPlugin;こうしてみると結構シンプルですね。
省略したのは、私が自分で処理を組み立てた部分です。リファレンスにはこんな風に書いてあります。
A plugin for webpack consists of:
- A named JavaScript function or a JavaScript class.
- Defines apply method in its prototype.
- Specifies an event hook to tap into.気合(Google翻訳)で訳すと、
- 名前付きのJavaScript関数、もしくはクラスです。
- applyプロトタイプメソッドを定義します。
- 使用するイベントフックを指定します。
てな感じでしょうか。
要するに、最低この3要素を満たすように作れば良いのです。
これを踏まえて、上のコードから一部を抜粋します。
index.jsapply(compiler) { compiler.hooks.compilation.tap(PLUGIN_NAME, compilation => { ...省略... }); }
apply
メソッドが定義されていて、引数がcompiler
となっています。
compiler
は、webpack
コマンドで実行されるjsのコンパイル処理のことを指しています。
(上手い説明ができず、スミマセン。「ほーん」くらいのテンションで聞き流してください)詳しいことはこの辺に書いてあります。多分。
英語はフィーリングで読んでいます。アイキャンアンダースタンド。
compilerオブジェクトを使うと、
webpack
コマンドで実行されるコンパイル処理全体にアクセスできます。
コンパイル前に処理を挟んだり、コンパイル後に処理を挟んだりができます。complierオブジェクトから参照できるフックはこちらにまとまっています(めっちゃたくさんあります)
https://webpack.js.org/api/compiler-hooks/pug-stylekit-webpack-pluginでは、その中のcompilationフックを利用しています。
tap
というメソッドについては↓に解説があるのですが、正直よくわかりませんでした。むつかしい。
https://webpack.js.org/api/plugins/#tapable
要するに、コンパイル処理になんらかのプログラムを差し込むためのInterfaceのようです。
同期的(tap)だったり、非同期(tapAsync)だったり、promiseで解決させるもの(tabPromise)だったりいろいろあるので、用途に合わせてってことですかね。
compiler.hooks.compilation.tap
のコールバック関数の引数として、compilation
オブジェクトがあります。
これはコンパイル処理のコンテキストのようなもので、コンパイル対象のファイル名やコンパイル結果の吐き出し場所などが格納されています。省略していた箇所をもう少し、紐解いてみましょう。
index.jsapply(compiler) { compiler.hooks.compilation.tap(PLUGIN_NAME, compilation => { compilation.hooks.processAssets.tap( { name: PLUGIN_NAME, stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONS, }, (assets) => { ...省略... compilation.emitAsset(distPath, new RawSource(html)); ...省略... }); }); }
compilation
オブジェクトにもたくさんのフックがあります。
https://webpack.js.org/api/compilation-hooks/今回は、その中でprocessAssetsフックを利用します。
このフックは、コンパイルが完了しファイルを出力する段階に処理を差し込むことができます。
stage
属性にCompilation.PROCESS_ASSETS_STAGE_ADDITIONS
を設定して、
「アセット(=出力するファイル)を追加するフックだよ〜」てなことを教えています。
実際のところ、効用はよくわかりません。世の中には不思議がいっぱいだ。
フックの中にこんな箇所があります。
compilation.emitAsset(distPath, new RawSource(html));
distPath
とhtml
の中身はそれぞれこんな感じ。console.log(distPath) => "index.html" console.log(html) => "<!DOCTYPE html><html lang="ja"><head>....."これを実行すると、webpack.config.jsの
output.path
で指定したディレクトリに、
index.html
という名称で"<!DOCTYPE html><html lang="ja"><head>....."
という内容のファイルが出力されます。$ npx webpack [webpack-cli] Compilation finished asset index.html 214 bytes [compared for emit] <- これ asset build.js 0 bytes [compared for emit] [minimized] (name: main) ./test/src/entry.js 1 bytes [built] [code generated] webpack 5.10.0 compiled successfully in 597 msここまででようやく、webpackを通してファイルを出力できるようになりました。
ちなみに、今回は使いませんでしたが、compilerオブジェクトではjsのコンパイル処理のかなり具体的なところまでアクセスできるようです。
https://webpack.js.org/api/parser/
プラグインのオプション
ソースコードの冒頭に、こんな箇所があります。
index.jsconst schemaUnits = require('schema-utils'); ...省略... const schema = { target: { anyOf: [ { type: 'array' }, { type: 'object', propaties: { from: { type: 'string' }, to: { anyOf: [ { type: 'object', propaties: { html: { type: 'string' }, css: { type: 'string' }, }, }, { type: 'string' } ] }, }, } ] } }; ...省略... class PugStylekitWebpackPlugin { constructor(options = {}){ schemaUnits.validate(schema, options, { name: PLUGIN_NAME }); this.options = options; } ...省略...ここは何かというと、webpack.config.jsでこのプラグインを呼び出す際に設定するオプションのバリデーションを行う箇所です。
↓ここで渡すオプションですね。webpack.config.js... new PugStyleKitWebpackPlugin({ target: { from: SOURCE_DIR + '/index.pug', to: { html: OUTPUT_DIR + '/index.html', css: OUTPUT_DIR + '/style.css', }, } }), ...
schema
という変数にオプションの構造を定義しておき、コンストラクターの中でバリデーションを実行しています。
オプションの形式がschemaと合っていないと、ここでエラーが出ます。バリデーションを通ったオプションオブジェクトは、
this.options
として格納し後で使えるようにしておきます。詳しいことはこの辺に書いてあります
https://github.com/webpack/schema-utilsここまでくれば、あとはフックの中に好きな処理を書いていくだけです。
ここから先は、pug-stylekit-webpack-pluginに限った具体的な内容ですので、さらっと説明します。
処理が気になったら、実際にソースコードを覗いてみてくださいね。Pugのコンパイル
fileStreamモジュールを使ってPugファイルを読み込み、Pugパッケージを使ってコンパイル、アセットに追加します。
index.jsconst pug = require('pug'); const fs = require('fs'); ... const pugReadBuffer = fs.readFileSync(inputFile, 'utf8'); ... const html = pug.render(pugReadBuffer, options); ... compilation.emitAsset(distPath, new RawSource(html));Scssのコンパイル
Scssのコンパイルはちょっとややこしいですね。
Pugファイルからstylekitブロックを見つけ出す必要があります。pug-lexerとpug-parserという便利なパッケージがあります。
さきほどはPugパッケージを使って一気にコンパイルしましたが、
pug-parserを使ってPugの意味を解析し、stylekitブロックだけを取り出してみます。index.jsconst pugLexer = require('pug-lexer'); const pugParser = require('pug-parser'); ... _parseFile(filename) { const buffer = fs.readFileSync(filename, 'utf8'); const tokens = pugLexer(buffer, {filename}); const ast = pugParser(tokens, {filename, buffer}); return ast; } ...
ast
というのは抽象構文木(abstract syntax tree)の略です。
難しいことはおいといて、ast
の中身をみてみましょう。console.log(ast) => { "type": "Block", "nodes": [ { "type": "Doctype", "val": "html", "line": 1, "column": 1, "filename": "src/index.pug" }, { "type": "Tag", "name": "html", "selfClosing": false, "block": { "type": "Block", "nodes": [ { "type": "Tag", "name": "head", "selfClosing": false, "block": { "type": "Block", "nodes": [ { "type": "Tag", "name": "meta", "selfClosing": false, "block": { "type": "Block", "nodes": [], "line": 4, "filename": "src/index.pug" }, ... { "type": "BlockComment", "val": " stylekit", "block": { "type": "Block", "nodes": [ { "type": "Text", "val": ".body {", "line": 9, "column": 13, "filename": "src/index.pug" }, { "type": "Text", "val": "\n", "line": 10, "column": 1, "filename": "src/index.pug" }, { "type": "Text", "val": " &__title { ", "line": 10, "column": 13, "filename": "src/index.pug" }, ...長くて読みづらいですが、Pugのブロックごとにオブジェクトとして分解されて、格納されているのがわかるかと思います。
stylekitブロックは、BlockComment
タイプと、1行ずつがText
タイプのval属性として解釈されました。あとは、この
Text
タイプを1行ずつつなげたテキストを、Scssファイルとしてコンパイルするだけです。
テキストをsassBufferとして、Sass(dart-sass)パッケージに渡してコンパイルします。index.jsconst sass = require('sass') ... _createScss(blocks) { let sassBuffer = ''; ... sassBufferに書き込む... return sass.renderSync({ data: sassBuffer, outputStyle: "expanded", }); } ...
renderSync
から返ってくるresultオブジェクトは、result.css.toString()
とすることでcssのテキストに変換できます。
これを先ほど同様に、emitAsset
してあげればOKです。index.jsconst resultSass = this._createScss(styleKitBlocks); const distPath = path.relative(outputPath, outputFile.css); compilation.emitAsset(distPath, new RawSource(resultSass.css.toString()));
最後に
最後はちょっと駆け足気味になってしまいましたが、
詳細は実際のコードをみていただくのが早いかと思います。webpackはなかなか奥が深いですね。
フックの一覧をみるだけでも、もっといろいろできる気がしてきます。webpackではpluginだけでなく、loaderも自作することができます。
どちらも、イメージよりずっと簡単に実装ができます。もし機会があったら、是非チャレンジしてみてください。
去年に引き続き長文になってしまいましたが、お読みいただきありがとうございました。
弊社ではこの記事に引き続き、NIJIBOX Advent Calendar 2020として記事を公開しております。
ガッツリコードを書く記事から、チームビルディングまで多種多様な内容となっておりますので、
是非読んでみていただけますと幸いでございます。それでは、メリークリスマス。
おしまい。
※オチは特にありません。
- 投稿日:2020-12-08T17:28:29+09:00
SunCalcを使ってJavaScriptで日の出・日の入りや月の出・月の入りを計算する
はじめに
日の出・日の入りや月の出・月の入りなどの天文計算は複雑で、自分で実装するとなると面倒である。そこでSunCalcというライブラリを用いて、JavaScriptで計算してみることにした。また、計算結果を国立天文台ウェブサイトのものと比較した。
SunCalcはnpmを用いてインストールし、node.jsで実行した。用いたバージョンは以下の通りである。SunCalc自体は他のパッケージへの依存性はなくDateオブジェクトで入出力を行うが、ここでは日時計算ライブラリのMoment.jsを用いている。
package.json"dependencies": { "moment": "^2.29.1", "suncalc": "^1.8.0" }コード例
以下に日の出・日の入りの時刻と方位、南中時刻と高度、月の出・月の入りの時刻と方位、月齢を求めるコード例を示す。
- SunCalc自体はDateオブジェクトで入出力を行うが、日時計算のためMoment.jsを用いてラップしている。
- SunCalcでは方位角は南から西回りで表現されるが、北から東回りで表現するために180°を加えている。
- 月齢(phase)は日数では無く、0から1までの浮動小数点数で返される。もし日数に直す場合は、月の満ち欠け周期(29.3-29.8日で可変)を用いて計算する必要がある
- 月齢(phase)の計算は、正午月齢と比較するために12JSTの値を出力している。
sun.jsimport moment from "moment"; import SunCalc from "suncalc"; const RADTODEG = 180 / Math.PI; function calcSun(datem, longitude, latitude) { const { sunrise, sunset, solarNoon } = SunCalc.getTimes( datem.toDate(), latitude, longitude ); let sunriseAzimuth, sunsetAzimuth, noonElevation; if (sunrise) { const { azimuth } = SunCalc.getPosition(sunrise, latitude, longitude); sunriseAzimuth = azimuth * RADTODEG + 180; } else { sunriseAzimuth = null; } if (sunset) { const { azimuth } = SunCalc.getPosition(sunset, latitude, longitude); sunsetAzimuth = azimuth * RADTODEG + 180; } else { sunsetAzimuth = null; } if (solarNoon) { const { altitude } = SunCalc.getPosition( solarNoon, latitude, longitude ); noonElevation = altitude * RADTODEG; } else { noonElevation = null; } return { sunrise: sunrise ? moment(sunrise) : null, sunriseAzimuth, noon: solarNoon ? moment(solarNoon) : null, noonElevation, sunset: sunset ? moment(sunset) : null, sunsetAzimuth }; } function calcMoon(datem, longitude, latitude) { const date = datem.toDate(); const times = SunCalc.getMoonTimes(date, latitude, longitude); const date12 = datem .clone() .add(12, "hours") .toDate(); const { phase } = SunCalc.getMoonIllumination(date12); let moonriseAzimuth, moonsetAzimuth; if (times.rise) { const { azimuth } = SunCalc.getMoonPosition( times.rise, latitude, longitude ); moonriseAzimuth = azimuth * RADTODEG + 180; } else { moonriseAzimuth = null; } if (times.set) { const { azimuth } = SunCalc.getMoonPosition( times.set, latitude, longitude ); moonsetAzimuth = azimuth * RADTODEG + 180; } else { moonsetAzimuth = null; } return { moonrise: times.rise ? moment(times.rise) : null, moonset: times.set ? moment(times.set) : null, moonriseAzimuth, moonsetAzimuth, phase }; }比較結果
2020年12月の東京(35.6581N, 139.7414E)について、国立天文台ウェブサイトのこよみの計算と SunCalcの計算結果を比較した。
日の出・日の入り・南中時刻については1分程度の差があった。方位や高度については0.1°程度の差があった。
月の出・月の入りについては最大8分程度の差があった。方位については1°程度の差があった。月齢については直接比較できないが、30倍すると概ね一致することは確認できる。
太陽
国立天文台
年月日 出 方位[°] 南中 高度[°] 入り 方位[°] 2020/12/01 6:32 116.5 11:30:04 32.5 16:28 243.4 2020/12/02 6:33 116.7 11:30:27 32.4 16:28 243.2 2020/12/03 6:34 116.9 11:30:50 32.2 16:28 243.0 2020/12/04 6:35 117.1 11:31:14 32.1 16:28 242.8 2020/12/05 6:36 117.3 11:31:39 32.0 16:28 242.7 2020/12/06 6:36 117.4 11:32:04 31.8 16:28 242.5 2020/12/07 6:37 117.6 11:32:30 31.7 16:28 242.4 2020/12/08 6:38 117.7 11:32:56 31.6 16:28 242.2 2020/12/09 6:39 117.8 11:33:23 31.5 16:28 242.1 2020/12/10 6:40 117.9 11:33:50 31.4 16:28 242.0 2020/12/11 6:40 118.1 11:34:17 31.3 16:28 241.9 2020/12/12 6:41 118.2 11:34:45 31.3 16:28 241.8 2020/12/13 6:42 118.2 11:35:14 31.2 16:29 241.7 2020/12/14 6:43 118.3 11:35:42 31.1 16:29 241.6 2020/12/15 6:43 118.4 11:36:11 31.1 16:29 241.6 2020/12/16 6:44 118.5 11:36:40 31.0 16:29 241.5 2020/12/17 6:44 118.5 11:37:10 31.0 16:30 241.5 2020/12/18 6:45 118.5 11:37:39 31.0 16:30 241.4 2020/12/19 6:46 118.6 11:38:09 31.0 16:31 241.4 2020/12/20 6:46 118.6 11:38:39 30.9 16:31 241.4 2020/12/21 6:47 118.6 11:39:09 30.9 16:32 241.4 2020/12/22 6:47 118.6 11:39:38 30.9 16:32 241.4 2020/12/23 6:48 118.6 11:40:08 30.9 16:33 241.4 2020/12/24 6:48 118.6 11:40:38 31.0 16:33 241.4 2020/12/25 6:49 118.6 11:41:08 31.0 16:34 241.5 2020/12/26 6:49 118.5 11:41:38 31.0 16:34 241.5 2020/12/27 6:49 118.5 11:42:07 31.1 16:35 241.6 2020/12/28 6:50 118.4 11:42:36 31.1 16:36 241.6 2020/12/29 6:50 118.3 11:43:05 31.2 16:36 241.7 2020/12/30 6:50 118.2 11:43:34 31.2 16:37 241.8 2020/12/31 6:50 118.2 11:44:03 31.3 16:38 241.9 SunCalc
年月日 出 方位[°] 南中 高度[°] 入り 方位[°] 2020/12/01 6:33 116.7 11:31:32 32.5 16:29 243.6 2020/12/02 6:34 116.9 11:31:55 32.4 16:29 243.4 2020/12/03 6:35 117.1 11:32:18 32.2 16:29 243.2 2020/12/04 6:36 117.2 11:32:42 32.1 16:29 243.0 2020/12/05 6:37 117.4 11:33:06 32.0 16:29 242.9 2020/12/06 6:38 117.6 11:33:30 31.9 16:29 242.7 2020/12/07 6:38 117.7 11:33:55 31.7 16:29 242.6 2020/12/08 6:39 117.8 11:34:21 31.6 16:29 242.4 2020/12/09 6:40 118.0 11:34:47 31.5 16:29 242.3 2020/12/10 6:41 118.1 11:35:13 31.4 16:29 242.2 2020/12/11 6:41 118.2 11:35:40 31.3 16:29 242.1 2020/12/12 6:42 118.3 11:36:07 31.3 16:29 241.9 2020/12/13 6:43 118.4 11:36:35 31.2 16:29 241.9 2020/12/14 6:44 118.5 11:37:02 31.1 16:30 241.8 2020/12/15 6:44 118.5 11:37:30 31.1 16:30 241.7 2020/12/16 6:45 118.6 11:37:58 31.0 16:30 241.6 2020/12/17 6:45 118.7 11:38:27 31.0 16:30 241.6 2020/12/18 6:46 118.7 11:38:55 31.0 16:31 241.5 2020/12/19 6:47 118.7 11:39:24 30.9 16:31 241.5 2020/12/20 6:47 118.8 11:39:53 30.9 16:32 241.5 2020/12/21 6:48 118.8 11:40:21 30.9 16:32 241.5 2020/12/22 6:48 118.8 11:40:50 30.9 16:33 241.5 2020/12/23 6:49 118.8 11:41:19 30.9 16:33 241.5 2020/12/24 6:49 118.7 11:41:48 30.9 16:34 241.5 2020/12/25 6:49 118.7 11:42:17 30.9 16:34 241.5 2020/12/26 6:50 118.7 11:42:46 31.0 16:35 241.6 2020/12/27 6:50 118.6 11:43:14 31.0 16:35 241.6 2020/12/28 6:50 118.6 11:43:43 31.1 16:36 241.7 2020/12/29 6:51 118.5 11:44:11 31.1 16:37 241.7 2020/12/30 6:51 118.4 11:44:40 31.2 16:37 241.8 2020/12/31 6:51 118.3 11:45:08 31.2 16:38 241.9 月
国立天文台
年月日 出 方位[°] 入り 方位[°] 正午月齢[日] 2020/12/01 17:06 61.4 7:01 297.4 15.9 2020/12/02 17:52 59.5 7:59 299.9 16.9 2020/12/03 18:44 59.2 8:54 300.9 17.9 2020/12/04 19:42 60.7 9:45 300.1 18.9 2020/12/05 20:44 63.9 10:31 297.6 19.9 2020/12/06 21:48 68.7 11:12 293.5 20.9 2020/12/07 22:54 74.8 11:48 288.0 21.9 2020/12/08 --:-- ----- 12:22 281.6 22.9 2020/12/09 0:01 81.7 12:53 274.4 23.9 2020/12/10 1:08 89.3 13:25 266.9 24.9 2020/12/11 2:17 97.1 13:57 259.3 25.9 2020/12/12 3:29 104.7 14:33 252.2 26.9 2020/12/13 4:42 111.5 15:14 246.0 27.9 2020/12/14 5:56 116.9 16:01 241.4 28.9 2020/12/15 7:07 120.4 16:56 238.9 0.4 2020/12/16 8:13 121.5 17:57 238.7 1.4 2020/12/17 9:09 120.3 19:02 240.7 2.4 2020/12/18 9:57 117.2 20:08 244.6 3.4 2020/12/19 10:36 112.6 21:12 249.7 4.4 2020/12/20 11:10 107.2 22:13 255.5 5.4 2020/12/21 11:39 101.2 23:11 261.8 6.4 2020/12/22 12:06 94.9 --:-- ----- 7.4 2020/12/23 12:31 88.7 0:08 268.2 8.4 2020/12/24 12:57 82.5 1:03 274.5 9.4 2020/12/25 13:23 76.6 1:59 280.7 10.4 2020/12/26 13:52 71.1 2:55 286.5 11.4 2020/12/27 14:25 66.3 3:53 291.7 12.4 2020/12/28 15:03 62.5 4:51 296.0 13.4 2020/12/29 15:47 60.0 5:50 299.1 14.4 2020/12/30 16:37 59.1 6:47 300.7 15.4 2020/12/31 17:34 60.0 7:41 300.6 16.4 SunCalc
年月日 出 方位[°] 入り 方位[°] 正午phase 2020/12/01 16:58 60.6 7:02 298.1 0.52 2020/12/02 17:45 58.6 8:01 300.9 0.55 2020/12/03 18:38 58.2 8:58 302.0 0.58 2020/12/04 19:38 59.8 9:50 301.1 0.62 2020/12/05 20:42 63.4 10:37 298.3 0.65 2020/12/06 21:50 68.5 11:18 293.8 0.69 2020/12/07 22:57 74.9 11:56 288.1 0.72 2020/12/08 --:-- ----- 12:29 281.1 0.76 2020/12/09 0:06 82.3 13:02 273.8 0.79 2020/12/10 1:16 90.0 13:33 266.1 0.83 2020/12/11 2:26 97.9 14:06 258.7 0.87 2020/12/12 3:36 105.2 14:43 251.9 0.91 2020/12/13 4:48 111.5 15:24 246.2 0.95 2020/12/14 5:58 116.3 16:11 242.1 0.98 2020/12/15 7:07 119.4 17:06 240.0 0.02 2020/12/16 8:09 120.3 18:05 239.9 0.06 2020/12/17 9:04 119.1 19:08 241.9 0.09 2020/12/18 9:52 116.2 20:11 245.5 0.13 2020/12/19 10:32 111.9 21:14 250.3 0.16 2020/12/20 11:05 106.6 22:14 255.9 0.19 2020/12/21 11:36 100.9 23:13 262.0 0.22 2020/12/22 12:02 94.6 --:-- ----- 0.25 2020/12/23 12:29 88.5 0:09 268.3 0.28 2020/12/24 12:53 82.3 1:05 274.8 0.31 2020/12/25 13:19 76.3 2:01 281.0 0.34 2020/12/26 13:48 70.8 2:57 286.9 0.37 2020/12/27 14:19 65.8 3:54 292.3 0.40 2020/12/28 14:55 61.7 4:53 296.8 0.43 2020/12/29 15:38 59.1 5:51 300.1 0.47 2020/12/30 16:28 58.0 6:49 301.9 0.49 2020/12/31 17:25 58.8 7:42 301.8 0.53
- 投稿日:2020-12-08T17:16:35+09:00
[JavaScript] すごく初歩のJavaScript (コメントアウト,省略形,テンプレートリテラル)
- 投稿日:2020-12-08T17:12:28+09:00
既存のMPAなシステムをTypeScriptに移行した時のメモ
はじめに
この記事はラクスアドベントカレンダーの9日目の記事です。
毎年その年に取り組んだことを書いてますが、弊社でアドベントカレンダーを始めてから皆勤賞で今回5回目の参加になります。
- 2019年 チームでスクラムをはじめた1年のふりかえり
- 2018年 PostgreSQLでの中間一致検索の性能改善
- 2017年 社内にRedashを導入してみた & 最近追加された機能のご紹介
- 2016年 EclipseからIntelliJへの移行で準備したこと/困ったこと/良かったこと
毎年書いていると自分のやったことの棚卸に良いですね!
今年はTypeScript移行について書いていきたいと思います。想定読者
- jQueryなどで構築したMPA(Multi-Page Application)システムのTypeScript移行を検討している方
自分でやってみて、ReactやVue.jsなどSPAの情報は豊富にあるけど、MPAの情報が少ないと感じたのもあり記事にしてみます。
今回の対応の前提事項
- 対象のjsは約100ファイル
- IE11のサポートは必須
- 私を含めて3人で作業(基本的にみんなTS未経験)
- あまり工数は掛けない
- jsファイルの単位は変えない
- any型を使ってもOK
- グローバル変数はゼロには出来ないし目指さない
- TS化以外の修正は同時にやらない
作業をしていると既存ソースの粗が気になってしまいますが、そこの修正はぐっと堪えました。
やったこと
- Study
- @types/xxxのインストール
- ts-migrateを使って機械的にts化
- webpackを設定
- ts-migrateが吐き出した
//@ts-expect-error
の箇所をひたすら直していく2〜4は準備を除くと実施だけでしたら1日で終わるレベルの作業です。
一応、4.までやると一応TS化したことになります。
5.についてはゆっくりやっても大丈夫ですのでうちのチームでは毎週時間を決めて少しずつ対応していく方針を取りました。続けて具体的にやったことを書いていきます。
1. Study
チーム全員にTypeScriptの概要レベルを押さえてもらため勉強会を開催しました。
下記のスライドを教材にさせてもらいましたが、内容が浅すぎず深すぎず絶妙なバランスの神スライドでした。多謝!
https://speakerdeck.com/rtechkouhou/typescript-bootcamp-2020また、チーム外の人を含めた有志でプログラミングTypeScript読書会も開催しています。
こちらは言語仕様が深く書かれていて難易度がちょっと高いですが、すごく勉強になります。
TS移行を担当するメンバーはこちらにも全員参加して日々TS力を高めています。2. 型情報(@types/xxx)のインストール
TSの概要を押さえたところで具体的な作業に入っていきます。
jQueryなど、Definition Typedで公開されている型情報をインストールしておきます。
これでエディタ上での外部ライブラリの使用箇所のエラーが出なくなります。$ npm install -D @types/jquery @types/jqueryui2. ts-migrateを使って機械的にts化
.js
から.ts
への拡張子のリネームなどは手動でやろうと思ってましたが、ts-migrateというAirbnbが公開しているツールを上司が教えてくれたのでそれを使用しました。$ npm install --save-dev ts-migrate $ npx ts-migrate-full src/jsツールを流すと以下のことを一気にやってくれます。
- tsconfig.jsonを生成
- 拡張子jsをtsにリネーム
- tsファイルの中身を一括変換
上記の単位でgitへのコミットまでしてくれるので後から変更が追いやすいです。
d2e91aeb77e04ec5cb449b5687b4d435f74f4975 (HEAD -> master) [ts-migrate][src] Run TS Migrate 5dbe3669ec05232824dcb42c321d18983e451a54 [ts-migrate][src] Rename files from JS/JSX to TS/TSX bae0dfb6790808310865ceb7f00ab0c0f161013a [ts-migrate][src] Init tsconfig.json fileまた、個別に実行するオプションもあるのでツールが何をやってるか理解しながら進めることもできます。
jQueryなプロジェクトだと自動変換してくれることは少ないですが、関数の引数の型をanyでアノーテートしたり、varをletに置換するなど退屈な作業をやらないで済むのは良かったです。
ちなみに、git上でリネームされるので.js時代の古いgit logも引き継いで見れるので安心です。4. webpackを設定
このタイミングでwebpackを導入しました。
ts-loaderでトランスパイルするのですが、MPAなのでエントリーポイントを沢山宣言する必要があります。
そうすると今までとJSの出力単位が変わらないので、HTML側の修正が必要なく影響を極小に抑えることができます。
libraryTargetはIE11でも動くようにumdを設定しています。webpack.config.jsmodule.exports = { // 既存のjsの数だけエントリーポイントを定義 // 実際はここに約100個記述します entry: { 'a.js': '/src/a.ts', 'b.js': '/src/b.ts' }, module: { rules: [ { // ts-loaderでトランスパイルする test: /\.ts$/, exclude: /node_modules/, use: { loader: 'ts-loader', options: { transpileOnly: true, } } }, ], }, resolve: { extensions: ['.ts', '.js'], }, output: { // IE11でも動かすためumdを指定する libraryTarget: 'umd', // entryに指定した名前でファイルを出力する設定 filename: '[name]', path: `${__dirname}/webapp/htdocs/js` } }100ファイルぐらいでしたらビルド時間は大したことありませんでした。
5. ts-migrateが吐き出した
//@ts-expect-error
の箇所をひたすら直していくひとまずanyを使っても良いので、
@ts-expect-error
エラーコメントをひたすら直していきます。
前述した通り、時間を決めて少しずつ直していきます。つまずいたこと
グローバル変数の値が更新されない
exportした変数を呼び先で変更してるのに変更が反映されませんでした。
調べてみるとumdだとこんな感じでexportした変数がself(window)に設定されるようです。// 変数をエクスポートすると... export let hoge = 1; // 裏ではこんな感じで値がself(windowと同じ)に設定されている self.hoge = exports.hoge = 1;その後、値を更新するのですが下記のように値が反映しません。
// hogeの値を更新 hoge = 2; // 裏ではこんな感じに値を更新している exports.hoge = 2;これは、self.hoge = eports.hogeのようにアドレスを共有しているのですが
プリミティブで値を上書きしてリンクが切れてしまいself.hoge(window.hoge)の値が変わらないことが原因でした。
(hogeがオブジェクトだったら中身を書き換える分には問題ないのです)
このケースは、window.hogeのようにグローバルであることを明示してアクセスするようにしました。thisの型が合わなくて怒られる
例えば、以下のようにjQueryのonイベントのコールバック内でthisに対して何か操作をする時にエラーになりました。
$('#hoge').on('click', function() { // TS2339: Property 'checked' does not exist on type 'HTMLElement'. // TSはthisをHTMLElementだと推論している if (this.checked) {このケースは、HTMLInputElementのように、より具体的な型を関数の第一引数に自分で指定する必要がありました。
$('#hoge').on('click', function(this: HTMLInputElement) { if (this.checked) {関数の第一引数のthisはTSに型を伝えるための特別な扱いになっていて、トランスパイル後のjsには出力されません。
他の言語にはない概念なので少し戸惑いました。どうしてもグローバル依存がはずせないものがある
外部ライブラリと共有する変数など、どうしてもグローバル依存が外せないものがありました。
その場合は諦めてwindow.globalValue = 1
のように明示するようにしました。
TS23339: Property 'globalValue' does not exist on type 'Window & type of globalThis'
のエラーになるため、globals.d.tsというファイルを作ってWindowに宣言を追加しました。globals.d.tsinterface Window { globalValue: number; }Definition Typedに存在しないライブラリの扱い
それほど数が多くなかったので必要なものだけ自分で定義しました。
今後、使用箇所を増やす予定はないので中身は頑張らずanyとしました。interface JQuery { // jquery.datePicker.js datePicker(options: any); // jquery.ajaxfileupload.js ajaxfileupload(options: any); }webpackで出力したファイルがIE11で動かない
tsconfig.jsonのtargetにES5を指定して安心していると、webpackが出力するファイルの中でアロー関数が使われているためIE11での実行時にエラーになりました。
アロー関数はES6(ES2015)で追加されたもので、IE11ではサポートしていないためです。
対策として、package.jsonに"browserslist": ["IE >= 11"]
を追加してIE11でも解釈できる形式で出力するようにしました。package.json"browserslist": [ "IE >= 11" ],変更前のトランスパイル結果と比較する
途中で気づいたんですが、ts-migrateした直後のコードをtscでトランスパイルした結果を取っておいて、
TS対応後のトランスパイル結果とdiffがなければOKみたいにすると、動作確認はかなり省略できるんじゃないかと思います。おわりに
以上で型の支援がある開発環境を手に入れることができました。
副次的な効果として既存ソースの課題も見えて来たので
機能開発で手を入れる時などにコツコツとリファクタリングしていこうと思います
最後にts-migrateはもう使わないで消しておきましょう。
- 投稿日:2020-12-08T16:42:33+09:00
N予備校の拡張機能作成のすゝめ
この記事は、N高等学校 (1) Advent Calendar 2020 9日目の記事です。
はじめに
はじめまして"N予備校を自分好みにカスタマイズするだけして勉強やらない人"です。
N高等学校の5期生でピチピチの一年生です。
※ この記事はレポートの日程を考えないでadvent calenderに申請した人が書いています。
内容が雑かもしれませんがよろしくおねがいします今回書くこと
今回はN予備校の大雑把にですがいちからN予備校の作る方法や
知っておくと便利な事を書いておこうと思います。
具体的にこんなのが作れます(露骨な宣伝)
※Chromeの拡張機能の基本的な作り方は省きます。作り方其の一
https://www.nnn.ed.nico/courses/*/chapters/*
のページにcontent_scriptを適用したい場合
これは簡単ですね。manifest.json{ /* 省略 */ "content_scripts": [{ "matches": ["https://www.nnn.ed.nico/courses/*/chapters/*"], "js": [ "content_script.js" ] }] }manifest.json の content_scripts の matches にURLを入力するだけです。
工夫が必要になってくるのは次からです。作り方其の二
レポートやテストのページに適用したい場合
レポートやテストのページはiframeになっているのでmanifest.jsonに一項目追加しなければなりません。
URLはhttps://www.nnn.ed.nico/contents/courses/*/chapters/*/*/* でmanifest.json{ /* 省略 */ "content_scripts": [{ "matches": ["https://www.nnn.ed.nico/contents/courses/*/chapters/*/*/*"], "js": [ "content_script.js" ] // 追加 "all_frames": true }] }
all_iframes: true
を追加することでiframeで読み込んだときも適用されるようになります。作り方其の三
動画ページに適用した場合 ※ズルは士ちゃダメヨ!単位剥奪がまってる!!
動画ページも上同様iframeなのですが
何故か動画ページだけ複数URLあるので虱潰しで記述しないといけないです。
私が知ってるURLは以下ですmanifest.json{ /* 省略 */ "content_scripts": [{ "matches": [ "https://*.tokyo-shoseki.co.jp/*", "https://www.nnn.ed.nico/contents/links/*", "https://www.nnn.ed.nico/contents/courses/*/chapters/*/movies/*", "https://cdn.fccc.info/*" ], "js": [ "content_script.js" ] // 追加 "all_frames": true }] }誰かURL一覧まとめてください☆(他力本願寺)
その他tip
私は勉強しないで拡張機能を作ってるときに気づいた便利そうなものおいておきます。
いろんなデータを取得する
https://www.nnn.ed.nico/courses/*/chapters/*
には理由はわかりませんが其のページの教材のデータがjsonで記述されています。
それを使って色々拡張機能作れると思います。const data = JSON.parse( document .querySelector('div[data-react-class="App.Chapter"][data-react-props]').dataset.reactProps )データの中身は貼っていいものなのかわからないので貼りませんが
上のコードで取得できます。
reactがわからないので何であるかはほんとにわかりません。
教えて強い人!!終わりに
以上レポートがまだあるのに考えなしにadvent calenderに
申し込みして初めて書いたQiita記事です。見にくい点、誤字脱字があったらぜひご指摘お願いします
それとそれと
ここで
宣伝(ゴホンゴホン)皆様のレポート活動を潤滑に進めれるように
作成した拡張機能を紹介させていただこうと思います。文字数カウンタ
github
これは単純に文字数をカウントしてくれるものになります。ボリュームセッター
こちらは動画の音量を保存して毎回適用してくれるようにする拡張機能です。読みにくいを記事最後まで読んでくださりありがとうございました
- 投稿日:2020-12-08T16:32:57+09:00
ループを使わずに配列上の連続する2つの値を要素とするリストを作る
はじめに
SENSYN Robotics(センシンロボティクス)の中山です。
Webアプリやそのインフラ周りと、Web側とドローンの接続を行うデバイスドライバ的な部分を担当しています。ここ1〜2年探していた、ループを使わずにリスト上の連続する2つの値を要素とするリストを作る方法をやっと思いついたので、それを記録するためのエントリです。
なにをしたいか
直近でやりたかったのは、ドローンの飛行記録から実際に飛行した距離を算出することでした。ドローンは飛行している場所(緯度・経度)を定期的に通知しています。CSVにすると、以下のようなイメージのデータです。
時刻,緯度,経度 2020-12-08T02:26:33Z.231,35.166264279608,138.67945381096 2020-12-08T02:26:33.834Z,35.166264279610,138.67945381097 2020-12-08T02:26:34.102Z,35.166264279611,138.67945381098 ...二点の緯度・経度が分かれば、その二点間の距離は計算可能です。そして、2点間の距離が計算できれば、あとは各2点間の距離を足し合わせて飛行した距離が計算できます。
ループを使って実装してみる
2点間の距離の計算には
npm install geolib
してgeolibを使います。ループを使うとこんな感じでしょうか。
const geolib = require('geolib'); const route = [ {latitude: 35.166264279608, longitude: 138.67945381096}, {latitude: 35.167274290660, longitude: 138.67945381157}, {latitude: 35.168284321721, longitude: 138.67945381218}, ]; let from = null; let dist = 0.0; for (const to of route) { if (from != null) { dist += geolib.getDistance(from, to); } from = to; } console.log(`distance: ${dist}m`);連続する2つの値を扱うために、前回のループの値をfromに入れて、今回の値をtoとして扱っています。
これはこれで悪くないのですが、fromやdistを書き換えているのが気持ち悪いです。if文があるのも美しさに欠けます。
reduce()を使って実装してみる
変数の書き換えを排除するために
reduce()
を使ってみると、こんな風になりました。const geolib = require('geolib'); const route = [ {latitude: 35.166264279608, longitude: 138.67945381096}, {latitude: 35.167274290660, longitude: 138.67945381157}, {latitude: 35.168284321721, longitude: 138.67945381218}, ]; const dist = route .reduce((acc, v) => [...acc, {from: acc[acc.length - 1].to, to: v}], [{from: route[0], to: route[0]}]) .filter(v => v.from != v.to) .map(v => geolib.getDistance(v.from, v.to)) .reduce((sum, d) => sum + d, 0); console.log(`distance: ${dist}m`);最初のreduce()
.reduce((acc, v) => [...acc, {from: acc[acc.length - 1].to, to: v}], [{from: route[0], to: route[0]}])ここで連続する2つの値を要素とするリストを作っています。
route
の各要素に対して、適用結果の配列の最後の要素とのペアを作っていきます。
acc
の初期値[{from: 要素0, to: 要素0}]
- 1回目の適用
[{from: 要素0, to: 要素0}, {from: 要素0, to: 要素0}]
- 2回目の適用
[{from: 要素0, to: 要素0}, {from: 要素0, to: 要素0}], {from: 要素0, to: 要素1}]
- 3回目の適用
[{from: 要素0, to: 要素0}, {from: 要素0, to: 要素0}], {from: 要素0, to: 要素1}, {from: 要素1, to: 要素2}]
スプレッド演算子
...
を使わずに、(acc, v) => {acc.push({from: acc[acc.length - 1].to, to: v}); return acc}
とかでもいいです。filter()
ここで目的の「連続する2つの値を要素とするリスト」を含むリストができたのですが、最初の2つの要素は
from
とto
がどちらも要素0を指しています。距離を計算するのであればこのままでもいいのですが、汎用性を考えるとちょっと問題があります。そこでfrom
とto
が同じものを排除して、「連続する2つの値を要素とするリスト」だけを取り出すのがここのfilter()
です.filter(v => v.from != v.to)map()
2点から2点間の距離を計算しています。
.map(v => geolib.getDistance(v.from, v.to))2個目のreduce()
距離を合計して、ルート全体の飛行距離を計算しています。
.reduce((sum, d) => sum + d, 0);まとめ
ループを使わずに配列上の連続する2つの値を要素とするリストを作るには、以下のようなコードを書けば良い。
list .reduce((acc, v) => [...acc, {from: acc[acc.length - 1].to, to: v}], [{from: list[0], to: list[0]}]) .filter(v => v.from != v.to)JavaScript以外でも、
reduce()
とfilter()
が使えれば同じ手法が使えると思います。
- 投稿日:2020-12-08T16:18:55+09:00
スクロールするとコンテンツをフワっとアニメーションで表示する(javascript)
はじめに
タイトルについて記事にしました。
この記事で得る内容は以下の通りです。・ JavaScriptの基礎知識が増える
・ スクロールするとコンテンツをフワっとアニメーションで表示する方法JavaScript
① まずCSSセレクタを対象に要素を配列で取得します
script.jsconst targetElement = document.querySelectorAll(".animationTarget")② 変数
targetElement
の対象の数だけ、for文で処理を繰り返しますscript.jsconst targetElement = document.querySelectorAll(".animationTarget") for (let i = 0; i < targetElement.length; i++) {}③
targetElement
の上からの距離と、要素の高さを取得して、現在どのくらいスクロールしているのかを比較する処理を記述しますscript.jsconst targetElement = document.querySelectorAll(".animationTarget") for (let i = 0; i < targetElement.length; i++) { const getElementDistance = targetElement[i].getBoundingClientRect().top }・ getBoundingClientRect().topメソッド
対象の要素
.animationTarget
から、今見えているブラウザの上側の距離を取得するメソッド
現在ブラウザで見えている灰色の所と水色のdiv.animationTarget.show
の引いた距離を取得します④ この処理をスクロールする度に取得する為、スクロールイベントを対象にした関数を定義して、先ほど記述したfor文を関数の中に移動します
script.jsconst targetElement = document.querySelectorAll(".animationTarget") document.addEventListener("scroll", function () { for (let i = 0; i < targetElement.length; i++) { const getElementDistance = targetElement[i].getBoundingClientRect().top } });⑤
window.innerHeight
で現在のブラウザの高さを取得し、if
文を使い、変数getElementDistance
よりも画面の高さが高い時(対象の要素が見え始めた時)にクラスを付与しますscript.jsconst targetElement = document.querySelectorAll(".animationTarget") document.addEventListener("scroll", function () { for (let i = 0; i < targetElement.length; i++) { const getElementDistance = targetElement[i].getBoundingClientRect().top if (window.innertHeight > getElementDistance) { targetElement[i].classList.add("show"); } } });対象の要素が少し見えた時にクラスを付与しても、何が起きたのか分からないと思いますので、変数
getElementDistance
に処理を付け加えます⑥ 変数
targetElement
から半分ほど高さを取得して調整します・ clientHeightプロパティ
対象の表示域の高さを取得するプロパティ
script.jsconst targetElement = document.querySelectorAll(".animationTarget") document.addEventListener("scroll", function () { for (let i = 0; i < targetElement.length; i++) { const getElementDistance = targetElement[i].getBoundingClientRect().top + targetElement[i].clientHeight * 0.6 if (window.innertHeight > getElementDistance) { targetElement[i].classList.add("show"); } } });これで要素の半分〜6割程見えたところでアニメーションすると思いますが、高さをいくつかけるかはアニメーションの内容等で微調整が必要です
haml
続いてhamlとscssを書いていきます
アニメーションさせたい要素の親として.animationTarget
を配置しますindex.haml.animationTarget .container-contact#CONTACT %p.top Contact .header-line.middlescss
基本は予め透明にして、
show
クラスがついたら透明を解除してtransition
でふわっとアニメーションをさせて表示させます上下左右からアニメーションして表示する場合は
trasform: translate()
で予め要素を移動させておいて
transform: none;
で移動させた要素を元に戻すという流れですindex.scss// 基本形 .container-contact { opacity: 0; transform: translateY(50px); transition: 1s; } .animationTarget.show .container-contact, .animationTarget.show .language__block, .animationTarget.show .picture, .animationTarget.show .work, .animationTarget.show .work.translateXscroll { opacity: 1; transform: none; } // 以下参考 // コンテンツの左から右に行くにつれ遅く見せる .language__block { opacity: 0; transform: translateY(30px); } .language__block.delay1 { transition: 1s; } .language__block.delay2 { transition: 1.2s; } .language__block.delay3 { transition: 1.4s; } .language__block.delay4 { transition: 1.6s; } .language__block.delay5 { transition: 1.8s; } // 画像だけ遅れて表示させる .picture { opacity: 0; transition: 1s 0.5s; // 0.5秒遅れて1秒かけてアニメーションする } // 左右対象に表示する .work { opacity: 0; transform: translateX(60px); transition: 1.5s; } .work.translateXscroll { opacity: 0; transform: translateX(-60px); transition: 1.5s; }
- 投稿日:2020-12-08T15:41:20+09:00
[JavaScript] undefined と null の発生と挙動 2020
JavaScript の
undefined
とnull
はどんなときに現れてどんな風に振る舞うのか、思いつきで挙げていきます。
undefined
の発生
- グローバルオブジェクトの
undefined
プロパティ
globalThis.undefined
- 未初期化の変数の値
let x; x
- オブジェクトの未定義プロパティアクセス
({}).x
- 関数に実引数が渡されない場合の仮引数の値
(x => x)()
- 関数が明示的に値を返さない場合の戻り値
(() => {})()
- 任意の式に void 演算子を適用した値
void 1
- Optional Chaining で左辺値が
null
またはundefined
の場合
null?.x
undefined?.x
Array.prototype.find()
やMap.prototype.get()
で要素が見つからない場合
[].find(() => false)
(new Map).get('x')
null
の発生
null
キーワード
null
- プロトタイプのないオブジェクトのプロトタイプ
Object.getPrototypeOf(Object.prototype)
Object.getPrototypeOf(Object.create(null))
String.prototype.match()
やRegExp.prototype.exec()
でマッチしない場合
''.match(/a/)
/a/.exec('')
- Web Platform API で何かが見つからない場合や設定可能な何かが未設定の場合
localStorage.getItem('')
sessionStorage.getItem('')
document.documentElement.parentElement
document.getElementById('')
document.querySelector('x')
document.createElement('a').getAttribute('x')
document.createElement('a').onclick
- etc...
null
とundefined
の挙動
typeof
typeof undefined === "undefined"
typeof null === "object"
- 真偽値化
Boolean(undefined) === false
Boolean(null) === false
- 文字列化
String(undefined) === "undefined"
String(null) === "null"
- 数値化
isNaN(+undefined)
+null === 0
isNaN(parseInt(undefined, 10))
isNaN(parseInt(null, 10))
isNaN(parseFloat(undefined))
isNaN(parseFloat(null))
- ビット列化
(undefined | 0) === 0
(null | 0) === 0
- JSON(オブジェクトプロパティ):
undefined
は無視される
JSON.stringify({ x: undefined }) === '{}'
JSON.stringify({ x: null }) === '{"x":null}'
- JSON(配列要素):
undefined
はnull
に変換される
JSON.stringify([undefined]) === '[null]'
JSON.stringify([null]) === '[null]'
- 関数のデフォルト引数が利用されるのは
undefined
のみ
((x = 3) => x)(undefined) === 3
((x = 3) => x)(null) === null
CSSStyleDeclaration
のプロパティへの代入
document.body.style.display = undefined
→ 何も起こらないdocument.body.style.display = null
→ 当該プロパティがクリアされる(空文字列も同様)- プロトタイプを持たないオブジェクトの作成方法は
Object.create(null)
(undefined
ではダメ)- 等価性
undefined !== null
undefined == null
document.all == null
- 投稿日:2020-12-08T15:33:15+09:00
猪木の名言で元気をくれるbotを作ったら、想定外の応答で笑いが止まらなくなったから、ぜひ試して欲しい。
元気が出ない朝は、猪木さんの出番です。
例えば、月曜日の朝だとか、どうしても『元気が出ない朝』とかってあるじゃないですか。
そんなとき、皆さんはどうされていますか??そんな朝は、やっぱり、猪木さんですよね??
ということで、今回の完成形は以下の通りです。
特に、最後の『ダー!』が、想定の斜め上を行くので、ぜひ皆さん一度ご賞味ください。
クローバーの、最後の「ダー!」がジワるので、最後まで見てください。#protoout#元気があればなんでもできる pic.twitter.com/ryRnjqsweJ
— 北城雅照@足立慶友整形外科 (@kutuyanomusuko) December 4, 2020ちなみにこのボットは、猪木さんの名言をランダムに返してくれるように設定しました。
ランダムに返信するようにも設定完了。
— 北城雅照@足立慶友整形外科 (@kutuyanomusuko) December 4, 2020
「元気が出るbot:猪木名言編」#protoout #元気ですか pic.twitter.com/ZNQQ4Bk05u最後の名言(迷言)『ダー!』は共通です。
では行きましょう。開発環境の下準備
1) VScodeのインストール
VScodeのインストールついては、googleなどで他の記事を検索してください。
2) node.jsとnpmのインストール
Macでの環境作りは、こちらの別の記事にまとめてあります。
参考にしてください。
Macにnode.js,npmのインストール方法今回の開発の手順
開発の手順に関しては、下記の記事を参考に行いました。
是非参考に進んでください。
Clova CEKでのスキル開発の始め方〜Node.jsで開発スタート編〜ハマったところ:スキルがうまく発動しない
スキルを発動する際に、「ねぇクローバー、<スキル名>を起動して。」の声かけで、登録したスキルが発動するのですが、この認識能力が低いように感じます。うまくスキルが発動されない場合は、下記の点を確認してください。
1) スキル名を変更する
スキル名が特定の用語を示す名詞の場合、うまくスキルとして認識されません。
その場合は、「〜bot」と名付けるとうまくいくことがあります。
今回の場合、開発当初はスキル名を「元気があればなんでもできる」にしていました。
そこで、「ねぇクローバー、<元気があればなんでもできる>を起動して。」と話しかけたところ…。
「そうですね!」
とクローバーに明るく返答されました。
クローバー、確かに返答としては正解だけど、機能としては失敗だよ…。2) スキル名と呼び出し方の設定を確認する
CLOVA Develper Center→スキル設定→基本情報とすすみ、
基本情報の中にある、呼び出し名(メインと)
次に、CLOVA Develper Center→スキル設定→ユーザー設定とすすみ、
代表サンプル発話の1つ目の部分のスキル名が必ず一致しているかを確認してください。
この部分が異なっていると、スキルが発動されないので、注意が必要です。
inoki.jsのコード
inoki.jsconst clova = require('@line/clova-cek-sdk-nodejs'); const express = require('express'); const clovaSkillHandler = clova.Client .configureSkill() //起動時に喋る .onLaunchRequest(responseHelper => { responseHelper.setSimpleSpeech({ lang: 'ja', type: 'PlainText', value: '猪木さん、元気ですか?と聞いてください。', }); }) //ユーザーからの発話が来たら反応する箇所 .onIntentRequest(async responseHelper => { const intent = responseHelper.getIntentName(); const sessionId = responseHelper.getSessionId(); console.log('Intent:' + intent); if(intent === 'InokiIntent'){ const slots = responseHelper.getSlots(); console.log(slots); //デフォルトのスピーチ内容を記載 - 該当スロットがない場合をデフォルト設定 let speech = { lang: 'ja', type: 'PlainText', value: `猪木さん、元気ですか?と聞いてください。` } if(slots.inokiwords === '元気ですか'){ switch(~~(5 * Math.random())) { case 0: speech.value = `元気があれば、何でもできる。いくぞー!1、2、3、だー!`; break; case 1: speech.value = `夢を持て!でかければでかいほどいい。とにかく、夢を持て。いくぞー!1、2、3、だー!`; break; case 2: speech.value = `人は歩みを止めたときに、そして挑戦をあきらめたときに、年老いていくのだと思います。いくぞー!1、2、3、だー!`; break; case 3: speech.value = `踏み出せば、その一足が道となる。いくぞー!1、2、3、だー!`; break; case 4: speech.value = `迷わず行けよ。行けば分かるさ。いくぞー!1、2、3、だー!`; break; } } responseHelper.setSimpleSpeech(speech); responseHelper.setSimpleSpeech(speech, true); } }) //終了時 .onSessionEndedRequest(responseHelper => { const sessionId = responseHelper.getSessionId(); }) .handle(); const app = new express(); const port = process.env.PORT || 3000; //リクエストの検証を行う場合。環境変数APPLICATION_ID(値はClova Developer Center上で入力したExtension ID)が必須 const clovaMiddleware = clova.Middleware({applicationId: 'Extention ID'}); app.post('/clova', clovaMiddleware, clovaSkillHandler); app.listen(port, () => console.log(`Server running on ${port}`));応用として
僕は猪木さんの名言で元気になれますが、皆さんの中には、猪木さんの言葉では元気になれない方も変わった方もいらっしゃると思います。
そこで、ご自身の好きな曲をMP3データに変換し、ランダムに再生することも可能です。返答を記載している部分である、
inoki.jsspeech.value = `返答したい言葉`を下記のように書き換えれば、曲の再生が可能です。
inoki.jsclova.SpeechBuilder.createSpeechUrl('MP3のURL')是非ためして見てください。
その他の記事
近すぎると小池都知事が『密です。』と連呼するデバイスを作ったら腹筋が崩壊したので、皆さんにも試して欲しい。
誰が使うかわからないけど、膝のレントゲン写真を送ったら、その膝がどの程度痛んでいるのか教えてくれるラインbotを作ってみた。
- 投稿日:2020-12-08T15:21:25+09:00
今日から始める負担にならないWAI-ARIA
WAI-ARIAとはなにか
「Web Accessibility Initiative - Accessible Rich Internet Applications」の略で、「ウェイ・アリア」と読みます。
WAI-ARIAをつかえば、HTMLだけでは不足しているセマンティクス(意味)を属性で補完することができ、支援技術(スクリーンリーダーなど)を通じて、障害をもつ人に対し適切な情報を伝えられるようになります。WAI-ARIAの注意点
あくまでも「必要な場合のみ使用する」技術です。
HTML5タグのセマンティクスで解決できるのであれば、HTMLで対応するのが基本です。WAI-ARIAのつかいかた
決められた「属性」をHTMLタグに追記して使用します。
以下、すぐにでも導入が可能で効果が期待できそうな項目を挙げてみました。
(その他、詳細については参考サイトをご覧ください。)ランドマーク
role="**"
要素の「役割」を定義します。
たとえば「フレームワークの仕様でdivタグしか使えない」といったような、適切なHTMLが使用できないときにも有効です。
まずは大きなレイアウト要素から使い始めてみるのがわかりやすいかもしれません。
設定するとスクリーンリーダーで「ナビゲーション ランドマーク サンプル」と読み上げられます(※<nav>サンプル</nav>
でも同様の音声になります)。index.html<!-- 例 --> <div role="navigation">サンプル</div>キーボードのアクセシビリティ
tabindex="**"
tabindex属性を使えば、aタグやbuttonタグなどのようなキーボード操作(タブキーによるフォーカス移動など)を可能にします。
属性値に0を指定すると、文書のソース内の順序でフォーカスを持たせることができます。index.html<div class="btn" tabindex="0">ボタン</div>文字列の定義
aria-label="**"
要素に「ラベル付けする文字列」を定義できます。
インラインSVGにはalt属性がないのでaltの代わりとして利用できます。index.html<button aria-label="閉じる" class="js_btn">X</button> <svg role="img" aria-label="画像の説明">...</svg>UIコントロール
aria-expanded="**"
aria-controls="**"
aria-hidden="**"
aria-selected="**"
aria-checked="**"
などクリックやマウスオーバーで開閉するようなUI操作時に使用します。
JavaScriptによりユーザー操作にあわせてaria属性値を適宜変更します。
「CSSと密結合」させることにより、挙動や状態が理解しやすいコードになります。index.html<!-- 閉じた状態 --> <button aria-expanded="false" aria-controls="drawer">ボタン</button> <div id="drawer" aria-hidden="true" class="drawer">ドロワー</div>index.html<!-- 開いた状態 --> <button aria-expanded="true" aria-controls="drawer">ボタン</button> <div id="drawer" aria-hidden="false" class="drawer">ドロワー</div>style.css.drawer[aria-hidden="true"] { display: none; }実装効率化の一例
jQueryの
toggleClass
のようなメソッドを用意しておくと、コードが簡略化しWAI-ARIAによる実装コストが下がります。See the Pen jQuery Plugin to toggle WAI-ARIA . by p_toro (@p_toro) on CodePen.
WAI-ARIAのまとめ
- 支援技術(スクリーンリーダーなど)を通じて、障害をもつ人に対し適切な情報を伝えられる。
- 必要な場合のみ使用する。基本的にはHTML5のタグで対応。HTMLで対応するのが基本。
- つかいどころを理解し効率よく利用することで、それほど作業工数を上げることなく導入が可能。
それでは、WAI-ARIAを活用した良いアクセシビリティライフを!
参考サイト
- 投稿日:2020-12-08T15:00:12+09:00
フィールド値変更イベントで気を付けること
今年もあっという間にアドベントカレンダーの季節がやってまいりましたね。
Qiitaに記事を投稿したことがなかったのですが、自身のアウトプットの練習と備忘録を兼ねて
何か記事を書けたらなということで、アドベントカレンダーに参加をしてみました。はじめに
- Qiita初投稿なのでツッコミどころが多いと思う
- 上記の通り、備忘録でもあるということ
- kintoneカスタマイズ初心者に向けた記事かも?
- たぶん既出のネタ
と、記事を書いている本人も思っていますので、ある程度手加減していただけると幸いです。
フィールド値変更イベントとは
基本的なkintoneカスタマイズについてお話をしておくと、
サイボウズ様が用意した kintone JavaScript API を利用して、特定のイベントが動作した際に
カスタマイズ処理を動作させるのが、基本的なカスタマイズの方法だと思います。その中でも今回は、
レコード追加画面のフィールド値変更時イベント
https://developer.cybozu.io/hc/ja/articles/201941984#step3レコード編集画面のフィールド値変更時イベント
https://developer.cybozu.io/hc/ja/articles/202166270#step3についての記事になります。
ざっくりと説明すると、特定のフィールドの値が変わった際に、カスタマイズを動かしたい時に使うイベントです。
フィールド値変更イベントの使い方
例えば、ドロップダウンフィールドと文字列1行フィールドをアプリに配置し、前述のフィールド値変更時イベントのカスタマイズを記述します。
const watch = 'ドロップダウン'; const changeEvent = [ `app.record.create.change.${watch}`, `app.record.edit.change.${watch}` ]; kintone.events.on(changeEvent, event => { event.record['文字列'].value = 'ヨシ!' return event; });そして、レコード追加(編集)画面でドロップダウンフィールドを変更すると・・
文字列フィールドに、カスタマイズで文字が挿入されました。
基本的な使い方としてはこんな感じではないでしょうか。それでは本題に入ります。
本題
基本的な使い方については先ほど説明した通りですが、ある程度カスタマイズに慣れてくると、もっと応用することもあるかと思います。
例えば、ルックアップの「ほかのフィールドのコピー」でフィールドの値が変更された時に、フィールド値変更イベントを動作させる。なんてことも可能ですよね。
ただし、この場合に注意しなければいけないことがある。というのが本題です。実は、ルックアップの「ほかのフィールドのコピー」でフィールド値変更イベントが動作した場合、ハンドラーの引数に渡されるオブジェクトの中身は、ルックアップのフィールドコピーが完了していないものが格納されています。
例えば、
- ルックアップフィールドを用意し「ほかのフィールドのコピー」を設定
- 「ほかのフィールドのコピー」で設定したフィールドに対して、フィールド値変更イベントのカスタマイズを用意
const watch = '担当者名'; const changeEvent = [ `app.record.create.change.${watch}`, `app.record.edit.change.${watch}` ]; kintone.events.on(changeEvent, event => { console.log({ event }) return event; });上記のカスタマイズが実行されると、コンソールに event が出力されます。
※ChromeであればF12キーを押すことで、コンソールが表示できます。では、ルックアップの取得を行い、カスタマイズを動作させてみましょう。
アプリの画面上では、すべてのグレーアウトしているフィールドに値が入っていますが、
イベント発火時にコンソールに出力されたオブジェクトを見ると、ルックアップの「ほかのフィールドのコピー」が
中途半端にしか動いていない(途中である) ことがわかると思います。画像の例で言うと、ルックアップの取得により担当者名フィールドに値が入ったタイミングで、カスタマイズが動いているようで、その時点では [郵便番号] ~ [住所] までのフィールドには、値のコピーが実施されていない ということです。
これを知らずに、以下のようなカスタマイズを作ったとします。
- イベント発火は [担当者名]フィールド のフィールド値変更イベント
- [日報]フィールド に [担当者名] と [住所] の値を入れる
const contact = '担当者名'; const address = '住所'; const changeEvent = [ `app.record.create.change.${contact}`, `app.record.edit.change.${contact}` ]; kintone.events.on(changeEvent, event => { console.log({ event }) event.record['日報'].value = ` 担当者名:${event.record[contact].value} 住所:${event.record[address].value}`; return event; });これを実行すると、前述の説明の通り、[住所]に値がコピーされる前に動作するため、正しく取得することができません。
じゃあどうするの?
どうやら、このルックアップの値コピーについては、画面上の上から順番に値のコピーを行っている(ような気がする)。
なので、カスタマイズソースを修正せずに対応する場合は [担当者名] を下に移動すること。なので、[担当者名] を下に移動させると・・
引数に渡されたオブジェクトを見ると、ルックアップの「ほかのフィールドのコピー」がすべて動いた後に、
イベントが発火していることがわかります。(担当者名が一番最後にコピーされるはずなので。)
また、先ほどは "undefined" となってしまった [日報] に記載の住所、こちらも正しい値が格納できています。これでほぼ解決かなと思いますが、
[担当者] が当初の配置場所よりも、下のほうに配置することになった
のが少し気になります。
この場合は、フィールドの配置は以前のままで、イベント発火のきっかけとなるフィールドを
[担当者名] → [住所]
に変更してあげることで解決するのですが、これはこれで
住所が入力されてないレコードをルックアップしたとき動かないじゃん。ということになりますので、どちらかの調整が必要
ということですね。ここまでをまとめると
- フィールド値変更イベントの引数に渡されるオブジェクトは、フィールド構成(配置)とイベント発火方法によっては、値が空になっているケースがある (ルックアップなどで値がコピーされてイベントが発火した場合)
- 回避するには、イベント発火のきっかけとなるフィールドを下に持ってくる
- もしくは、イベント発火のきっかけとなるフィールドを変更する
と、いうことでしょうか。
フィールド値変更イベントで、もう少し難しいこと(非同期処理)をやる場合は
恐らく、みんなこういうコーディングをしているんじゃないでしょうか。(思い込み)
const contact = '担当者名'; const changeEvent = [ `app.record.create.change.${contact}`, `app.record.edit.change.${contact}` ]; kintone.events.on(changeEvent, event => { setReport(); // 非同期の関数を呼び出す console.log({ event }); }); const setReport = async () => { /* なにかしらの非同期処理 */ const event = kintone.app.record.get(); event.record['日報'].value = "絶対ヨシ!"; kintone.app.record.set(event); }フィールド値変更イベントでは、Promise が使えないので、ハンドラーでは
return event;を書かずに、関数を呼び出し、最終的に自分で kintone.app.record.set() を使ってレコードの値を変更する。
kintone.app.record.set(event);確かにこれで動きます。
動きますが、上記例で言うと、setReport関数がとても速く処理された場合、以下のようなエラーが出ます。
エラーメッセージの通り、
イベントハンドラの処理中は、"kintone.app.record.set()" は使えないよ。
※kintone.app.record.get()も同じとのこと。
要はこういう時にエラーが起きるんですね。
イベントハンドラが終了する前に、setReport関数の kintone.app.record.set(event) が実行されている。
setReport関数は 非同期関数(async)なので、イベントハンドラで呼び出した際に、setReport関数の終了を待たずに
処理が進んでいますが、イベントハンドラの終了の前に、setReport関数内の kintone.app.record.set(event) が
呼ばれると、上記のようなエラーが発生するようです。ん?これって
フィールド値変更イベントは Promise に対応していない。
↓
じゃあ、イベントハンドラで非同期関数呼び出して、イベントハンドラでは何も return しなきゃいいんじゃない?
↓
概ねうまくいくけど、非同期関数が早く終わり過ぎると、エラーが出る。・・・
フィールド値変更イベントが Promise に対応するアップデートを待つ
※サイボウズ様、よろしくお願いします。まとめ
イベントハンドラで同期処理しかないケース(先ほどの再掲)
- フィールド値変更イベントの引数に渡されるオブジェクトは、フィールド構成(配置)とイベント発火方法によっては、値が空になっているケースがある (ルックアップなどで値がコピーされてイベントが発火した場合)
- 回避するには、イベント発火のきっかけとなるフィールドを下に持ってくる
- もしくは、イベント発火のきっかけとなるフィールドを変更する
イベントハンドラで非同期処理があるケース
- イベントハンドラで非同期関数を呼び出す
- イベントハンドラの終了よりも、呼び出した非同期関数が先に終わらないように意識する
というところでしょうか。
間違っていたらごめんなさい。あとがき
記事を書くというのは難しいですね。
普段何気なく Qiita で自分の困りごとを解決してくれる記事を読んだりしますが
記事を投稿するのは結構大変ですね。感謝しかないです。それと、実は私が所属する会社は CybozuDays 2020 東京に出展し、私自身もブースに立っておりました。
ブースに立ち寄ってくださったみなさま、ありがとうございました。
- 投稿日:2020-12-08T14:50:38+09:00
Full Static Generationを試す
Nuxt.jsの Full Static Generationとは Nuxt 2.13で導入された
APIレスポンスも合わせてすべて静的化するための機能です。今回はnpm packageのjson-serverをつかってモックapiを作成して、
APIの値を取得して静的書き出しをするところまでやってみます。
(Nuxt @ v2.14.9
で試しています)API側の準備
jsonを用意
db.json{ "news": [ { "id": "title_001", "body": "Hello World" }, { "id": "title_002", "body": "おはようございます" }, { "id": "title_003", "body": "こんにちは" }, { "id": "title_004", "body": "おやすみ" } ] }コマンドの追加
package.json に
npm run api_server
というコマンドを追加します。package.json"scripts": { "dev": "nuxt", "build": "nuxt build", "start": "nuxt start", "generate": "nuxt generate", "api_server" : "node_modules/.bin/json-server --watch db.json --port 3333" }Nuxtの準備
nuxt.config.jsの設定
はじめにnuxt.config.jsを
generateモードに設定するためにtargetをstaticにします。nuxt.config.jstarget :"static",今回のFile構成
-| pages/ ---| index.vue ---| news/ -----| _slug.vueNuxt v2.13から generate時に内部的にクローリング処理が行われ、
リンク先を自動的に検出してページ生成を行われるようになりました。たとえばnews/index.vueを作成して
news/index.vue<template> <div class="container"> <ul> <li v-for="item in news"> <nuxt-link :to="`/news/${item.id}`"> {{item.body}} </nuxt-link> </li> </ul> </div> </template> <script> export default { async asyncData ({ params}) { return axios.get('http://localhost:3333/news').then((res) => { return {news : res.data} }).catch((error) => { return { error: error } }) } } </script>このようなファイルを用意して
npm run generate
をたたくと
APIから取得したデータと<nuxt-link>
から動的に静的ファイルが生成されます。
(Nuxt v2.13以降)今回はリンクされていないページを想定し
_slug.vueにAPIにあるidの値を元に動的にファイルを生成させることにします。nuxt.config.jsにAPIを取得するコードを追加
nuxt.config.jsgenerate: { routes () { return axios.get('http://localhost:3333/news') .then((res) => { return res.data.map((news) => { return { route: '/news/' + news.id, payload: news } }) }) } }generate routesで使われるのがAPIのレスポンスデータのnews.idを使ってのものだけだと
_sulg.vue側で都度取得することになってしまい生成時間の増加につながってしまうらしく、
そういう場合はpayloadを設定して(今回でいうとAPIのデータ "id","body"をpaylodで渡せる)、動的ルーティング生成の高速化をおこなうようです。_sulg.vue側のコード
動的に表示する_sulg.vueは下記の様に設定します。
_sulg.vue<template> <div class="container"> <div> <p>title : {{id}}</p> <p>content : {{body}}</p> </div> </div> </template> <script> import axios from "axios"; export default { data () { return { id:"", body:"" } }, async asyncData ({ params, error, payload }) { if (payload) { return { id: payload.id, body: payload.body, } } else { return axios.get('http://localhost:3333/news').then((res) => { return res.data.find((post) => post.id === params.slug); }).catch((error) => { return { error: error } }) } } } </script>
npm run dev
で開発できるように
payloadの条件分岐をわけています。generate
この状態で
1.json-server を立ち上げnpm run api_server2.generateする
npm run generateAPIの情報を取り込みファイルを書き出せました。
-| dist/ ---| index.html ---| news/ -----| title_001/ -------| index.html -----| title_002/ -------| index.html ...略無事APIレスポンスを取り込んで書き出すことができました。
今回はAPIの部分をjson-serverを使ったモックで済ませましたが本来 strapi+nuxt.jsでアドベントカレンダーを作ろうのようにHeadless CMSからのAPIを取得から生成を試して見たかったのですが、それはまたどこかで。
FORK Advent Calendar 2020
15日目 前の日の記事のタイトル @yoh_zzzz
17日目 次の日の記事のタイトル @sy12345
- 投稿日:2020-12-08T12:42:03+09:00
うちでChrome拡張機能を作ろう
この記事はNTTコミュニケーションズアドベントカレンダーの8日目の記事です。
昨日は、@suzusuzu さんの 「Rustで実装するNetflow Collector」でした。
はじめに
どうもこんにちは
昨年はこんな記事を書いたtetrapod117です。今年はおうち時間が長かったので、いろいろ仕事以外でも趣味の開発をやったりしていました。
その中でChrome拡張を作ったりもしたので、今回は2020年っぽいChrome拡張を作って、Chrome拡張の開発について学ぶ記事を書いていこうと思います。
(お)うちでChrome拡張機能を作(るのに役立てばいいだ)ろう
という感じで書いていきますちなみに、Chrome拡張はお仕事ではなくプライベートな趣味で作ってます。
まず、Chrome拡張って?
Chromeの機能を拡張できるプログラムのことです。
拡張機能はChromeウェブストアから追加することができます。
https://chrome.google.com/webstore/category/extensions?hl=ja&オススメ拡張
ついでに僕がオススメの拡張機能を紹介します。
OneTab
まずは、OneTabです。
https://chrome.google.com/webstore/detail/onetab/chphlpgkkbolifaimnlloiipkdnihall?hl=ja仕事で調べものとかしてるとタブを大量に開いてしまって整理するのが大変。
そんな時にOneTabは大活躍します。
ワンクリックでタブをリスト化して、すっきりさせてかつメモリの消費も抑えることができます。Peek-a-tab
次はPeek-a-tabです。こいつはこりゃまたTab管理の拡張です。
僕はTabをたくさん開きますし、ウィンドウもたくさん開きます。なので、あのページどこやったかなとなる時が多々あるので、そんな時はPeek-a-tab
でページを検索して見つけてます。
https://chrome.google.com/webstore/detail/peek-a-tab-tabs-manager-f/nnpdamdaknpnohmlbnmgphiodghbohopChrome拡張機能を作ろう
という感じで
これらのChrome拡張機能はHTML,CSS,JavaScriptを使って自分で作ることもできます。
では、早速作っていきましょうそれでは、張り切ってどうぞ〜
密回避拡張
今年は3密という言葉が流行りましたね〜
こうQiitaの記事を見ているとなんか文字が密な感じがしてきますねということで、Qiitaの文字間隔を開ける拡張を作っていきましょう。
manifest.jsonの用意
まずChromeの拡張機能を作るにはmanifest.jsonを用意する必要があります。
ざっくりいうと設定ファイルです。
https://developer.chrome.com/extensions/manifest
作業用のディレクトリを作成し、manifest.jsonファイルを作成します。以下のような感じで書きます
{ "manifest_version": 2, "name": "Mitsu Qiita", "version": "1.0.0", "description": "Qiitaの文章を密回避する拡張です", "content_scripts": [ { "matches": ["https://qiita.com/*"], "css": ["style.css"] } ] }ここで重要なのはcontent_scriptsの記載内容です
content_scriptsはブラウザ上に表示されているページに対して、スクリプトを挿入してDOM操作を行えるようにします。
https://developer.chrome.com/extensions/content_scripts
今回はhttps://qiita.com/*
にマッチするWebページにstyle.cssを読み込ませます。style.cssの用意
ということでstyle.cssを用意します
こんな感じです* {letter-spacing: 1em}
letter-spacing
で文字間隔を変えてます動作確認
それでは密回避拡張の動作確認をしてみましょう。
現在、ディレクトリにはmanifest.jsonとstyle.cssがあると思います。
chromeブラウザで以下のリンクを開いてみてください。
chrome://extensions/
デベロッパーモードをONにすると3つのボタンが出てくるので、そこから
パッケージ化されていない拡張機能を読み込む
を選択しますファイル選択で先ほどの作業ファイルを選択します。
拡張一覧にMitsuQiitaがあれば成功です。このままQiitaを開いてみましょう
めっちゃ読みづらくなってますね!
ただ、ちゃんと密は回避できていますこんな感じでcontent_scriptを使うとJSやCSSでWebページを気軽に切り替えることができます。
次はJavaScriptとかを使ってもっと動きのある拡張を作ってみましょう
鬼滅の刃を別の文字に変えてみよう
ということで次はBroser Actionを使った拡張機能を作ってみましょう。
今年は鬼滅の刃の映画が国内の興行収入歴代1位に迫ってきていて、凄まじいくらいに大ヒットしてますね。
ということで、鬼滅の刃を別の文字に変換する拡張機能を作ってみましょう。Browser Actionとは?
追加するとURLのバーの横にアイコンが追加され、それでいろいろ機能が使えるタイプの拡張機能です。
ここらへんのやつです。
Chrome拡張と言えば真っ先に思いつくタイプのものかなと思います。
似たようなものでPage Actionというのもあります
違いはPageActionは特定のページに使い、Browser Actionはページに依存せず拡張機能を使うことができます。manifest.jsonの作成
まずは、先ほどと同様拡張機能の設定などを記載するmanifest.jsonを用意します
{ "manifest_version": 2, "name": "kimetsu2bo-bobo", "description": "鬼滅の刃の文字をボボボーボ・ボーボボに置換する", "version": "1.0", "permissions": ["http://*/*", "https://*/*"], "browser_action": { "default_icon": { "16": "/icon16.png", } "default_popup": "popup.html" } }ディスクリプションにも書いてますが、今回は鬼滅の刃の文字をボボボーボ・ボーボボに変換します。
今回重要なのは
permissions
とbrowser_action
の項目です。permissionsについて
permissions
は権限の設定です。
chrome.* API
を使う時に適切な権限を渡さないといけないです。
設定については以下のURL参照
https://developer.chrome.com/extensions/declare_permissions今回はChrome.tabのAPIを使うのですが、こちらのページのManifestに書いてある通り、一部のAPIを使う時のみにpermissionsの設定が必要になりますが、今回使うものには必要ないので設定していません。
https://developer.chrome.com/extensions/tabspermissionsについてですが、もし作った拡張機能をリリースしたい場合に、不必要な権限を要求していたら審査の際に弾かれてしまうので気をつけてください。
browser_actionについて
こちらは右上の拡張機能で使うアイコンとアイコンをクリックした時に出てくるポップアップを設定しています。
アイコンは16x16のファイルを適当に用意して配置します。
アイコンについては自分の環境で試す時は別に入れる必要はないですが、リリースをする時は必要になるのでリリースを考えてる時は作成する必要があると覚えておきましょう。ポップアップはHTMLファイルを用意する必要があるので早速作っていきましょう。
popup.htmlの作成
ということで、default_popupに設定しているpopup.htmlを作りましょう
<!DOCTYPE html> <head> <meta charset="UTF-8" /> <link rel="stylesheet" type="text/css" href="popup.css"> </head> <body> <div> <h3>鬼滅の刃をボボボーボ・ボーボボに変換します</h3> <button type="button">変換</button> </div> <script src="popup.js"></script> </body> </html>popup.cssはこんな感じです
body { min-width: 300px; }今回は簡単にボタンだけ設置してみました。
CSSではポップアップの横幅を設定しています。読み込んでみるとこんな感じになります
JSの記述
次にページにアクションを起こすJSを書いていきます
popup.jsになりますlet buttonElement = document.getElementById('button'); buttonElement.addEventListener('click', (event) => { chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { chrome.tabs.executeScript( tabs[0].id, {code: `document.body.innerHTML=document.body.innerHTML.replace(/鬼滅の刃/g, 'ボボボーボ・ボーボボ');`} ); }); });buttonのクリックをトリガーにtab.queryでページ情報を取得して、executeScriptで文字列を変更するJSを実行しています。
https://developer.chrome.com/extensions/tabs#method-query文字列を変換しているのはこちらのコードです
document.body.innerHTML=document.body.innerHTML.replace(/鬼滅の刃/g, 'ボボボーボ・ボーボボ');
ちなみに、この1行だけをChromeのデベロッパーツールのconsoleに入れて実行しても同じような動作をします。動作確認
では、
読み込んでみて動作確認してみましょう。
chrome://extensions/これが拡張を使うと
こうすることができます
ちなみに劇場版「ボボボーボ・ボーボボ」無限列車編はかなりパワーワードですね
開発については以上です
こんな感じでBrowser Actionを使うとポップアップを使えて、ユーザーのアクションを起点にした拡張の開発を行えます。
その他Chrome拡張Tips
次は作ってる時のTipsをおもむろに書いていきます。
リリース方法について
自分の作った拡張機能をリリースする際はデベロッパーダッシュボードを使います
デベロッパー登録には5ドル必要です。
https://chrome.google.com/webstore/developer/dashboardmanifest.jsonを含んだディレクトリをzipで固め、新しいアイテムを追加からzipをアップロードして、説明やスクショなどを追加して、審査してもらう感じになります。
この時の注意ですが、パーミッションが
["http://*/*", "https://*/*"]
のようにほぼ全てのWebサイトに適用できる場合は審査にすごく時間がかかります。
僕も先月審査に出したのですが、1回目の返信に2週間近くかかり、余計なパーミッションを追加していたため拒否されたので、削除して出し直してまだ返信はありません。
急いではないのであまり気にしてないですが、急いでリリースしたい!みたいなことがあれば特定のサイトでしか使えないようにするなど、パーミッションを見直す必要があります。Background Pagesについて
今回は使わないでしたが、拡張機能にはBackground Pagesというものもあります。
こちらは拡張機能がインストールされてからページが常駐し続けます。Background PagesではChromeのJavaScriptAPIが全て使えるようになります。
これまで紹介した、Browser Actionなどでは一部のAPIが使えないようなので、使いたいものがある場合はBackground Pagesを使うことも視野に入れておくといいでしょう。Background Scriptsでしか使えないものの一例ですが、右クリックのメニューを拡張する
chrome.contextMenusなどがあります。参考
https://developer.chrome.com/extensions/background_pages (Background Pagesについて)https://developer.chrome.com/extensions/contextMenus (contexMenusについて)
最後に
という感じで、いくつかChrome拡張機能の作り方を書いていきました。
Chrome拡張であれば、今回のようなちょっとしたネタ拡張ならさっと作ることができますし、他のアプリケーションストアなどに比べてリリースの敷居もそこまで高くないので、軽くアウトプットも出せると思います(といいつつ、僕の拡張まだリリースされてないですが...)今年は暗いニュースが多かったので、こういうネタ拡張を作ってちょっと笑ってみるのもいいかもしれないですね!
明日は @y-i さんの記事になります。
- 投稿日:2020-12-08T12:21:15+09:00
ts migrateを使ってReactのアプリをJavaScriptからTypeScriptに書き換えよう!!
皆さんこんにちは。
最近、毎日の様にTypeScriptという単語が耳に入ってきて、「TypeScriptって伸びてるみたいだし、そんなにいいのか」と思い、いろいろ調べていたのですが、どうやらなかなかに良さそうな言語だったので、基礎的な部分を少し勉強してみました。
そこで、「試しにReactのアプリもjsxからtsxに置き換えてみるか」と思い、調べていると
ts migrate
という、Airbnbが開発したパッケージがあり、これを利用するのが良さそうだとわかったので、試しに使ってみたところめっちゃ便利だったので、今回はts migrate
についての使い方の記事を書こうと思いました。ts migrateってなに?
ts migrateの公式リポジトリの説明によれば、
ts-migrate はコードの TypeScript への移行を支援するツールです。JavaScript、または部分的な TypeScript プロジェクトを取り込み、コンパイルした TypeScript プロジェクトを出力します。ts-migrate は TypeScript の移行プロセスを高速化することを目的としています。結果として得られるコードはビルドを通過しますが、型の安全性を向上させるためのフォローアップが必要です。たくさんの // @ts-expect-error や、時間をかけて修正する必要があるものがあるでしょう。一般的には、ゼロから始めるよりもずっといいでしょう。
とのことです。
まとめると、
- TypeScriptへの移行ツール
- TypeScriptへの移行を(手作業と比べて)高速化することが目的
- しかし、変換されたコードは型の安全性が低いので修正が必要
こんな感じです。
少し補足すると、
コマンド一発で選択したディレクトリ配下のjsファイルを全てtsファイルに変換し、中身の関数などにも型をつけてくれるもの
です。
ただ、型は全てany
なので後から変えていく必要があるよ、ということです。実際に使ってみる
さて、実際に使ってみましょう。
以下の
使い方
のところで出てきますが、先にgitをクリーンな状態にしておきましょう。ローカルでの変更がremoteにcommitされているかmasterが最新の状態になっているか確認しましょう。確認ができたら、作業用のブランチを切ります。
$ git checkout -b ts-migrateインストール
インストールは簡単です。
npmの場合は
$ npm i ts-migrateyarnの場合は
$ yarn add ts-migrateこれで、インストール完了です!
使い方
以下のコマンドで、ファイルをTS化できます。
$ npx ts-migrate-full <folder>このの部分は変換したいフォルダを選択します。今回は、Reactのsrcフォルダ配下のファイルをtsx化したいので、
$ npx ts-migrate-full srcとして実行します。
実行すると、以下の様なとても親切なメッセージが表示されます。This script will migrate a frontend folder to a compiling (or almost compiling) TS project. It is recommended that you take the following steps before continuing... 1. Make sure you have a clean git slate. Run `git status` to make sure you have no local changes that may get lost. Check in or stash your changes, then re-run this script. 2. Check out a new branch for the migration. For example, `git checkout -b ts-migrate` if you're migrating several folders or `git checkout -b ts-migrate-src` if you're just migrating src. 3. Make sure you're on the latest, clean master. `git fetch origin master && git reset --hard origin/master` 4. Make sure you have the latest npm modules installed. `npm install` or `yarn install` If you need help or have feedback, please file an issue on Github! Continue? (y/N)
訳すと、
- git をクリーンな状態にしておきましょう。git status` を実行して、ローカルでの変更が失われていないことを確認します。チェックインするか変更を保存してから、このスクリプトを再実行します。
- 移行のための新しいブランチをチェックアウトします。例えば、複数のフォルダを移行する場合は
git checkout -b ts-migrate
とします。srcを移行するだけならgit checkout -b ts-migrate-src
。- 最新のクリーンなマスターを使用していることを確認してください。
git fetch origin master && git reset --hard origin/master
です。- 最新の npm モジュールがインストールされていることを確認してください。
npm install
またはyarn install
を実行する。全部OKならyを入力。
変換が開始されます。All done! The recommended next steps are... 1. Sanity check your changes locally by inspecting the commits and loading the affected pages. 2. Push your changes with `git push`. 3. Open a PR!
いろいろログが出た後に、上記の様に
All done!
とでていれば成功です。ちゃんと変換ができています!!(感動)
jsxファイルは以下の様にtsxファイルになりました。
関数コンポーネントも
Post.tsxconst Post = ({content, createdAt, title, uid}: any) => { //以下省略この様に型がつけられています?
typescriptをインストールしていない場合はインストール
npm install typescript
tsconfigの設定
tsconfigは以下の様に設定しました。
tsconfig.json{ "compilerOptions": { "jsx": "react", "target": "es5", "module": "esnext", "strict": true, "noImplicitAny": true, "moduleResolution": "node", "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "lib": [ "dom", "dom.iterable", "esnext" ], "allowJs": true, "allowSyntheticDefaultImports": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true }, "include": [ "src" ] }いろいろなerrorを消していこう
簡単に変換はできましたがいろいろerrorが出るので、それらを直していきます。
よくあるのは、
@ts-expect-error
です。この
@ts-expect-error
がコメントで表示されているので、各ファイルのコメントを消していきましょう。コメントを消すと特になにも起こらない箇所もありますが、errorが起きる箇所もあります。僕の場合は、コメントを消してerrorが起きたところは大抵
型がないパターン
だったのでとりあえずany
をつけました。元々のファイルの数が多いと、結構errorを潰していくのが面倒です...w
全てのerrorを消したら完了です!!
お疲れ様でした‼️参考
ts-migrate(公式)
ts-migrate: A Tool for Migrating to TypeScript at Scale
JSからTSへの移行ツール、ts-migrateを試してみた
Introduce TypeScript to react(js) project with ts-migrate?
- 投稿日:2020-12-08T12:11:50+09:00
BurpでAxiosのHTTPS通信をのぞき見る
Burpを使ってAxiosの通信内容を確認する方法です。
要点は、burp+ブラウザの場合とは異なりトンネルの確立を明示的にやらなければならない、ということです。
証明書の準備
derファイルを出力
BurpのProxyタブ→Optionタブから、
Import / export CA certificate
をクリックし、ウィザードでCertificate in DER format
を選択してder形式で(以下ではout.der
)出力します。derをpemに変換
以下のコマンドを実行して、derファイルをpem形式(以下では
cert.pem
)に変換します。openssl x509 -inform der -in out.der -out cert.pem以下では生成したpemをスクリプトと同一のディレクトリに置くものとします。
スクリプト
ライブラリの準備
axios
,fs
の他にtunnel
を使います。コード
axios-burp.tsimport axios from "axios"; import * as fs from "fs"; import * as tunnel from "tunnel"; async function main() { try { const agent = tunnel.httpsOverHttp({ // Burp proxy: { host: "127.0.0.1", port: 8080 }, ca: [fs.readFileSync("./cert.pem")] }); await axios.get("https://postman-echo.com/get?foo1=bar1&foo2=bar2", { httpsAgent: agent }); } catch (e) { } } main();実行結果
% ts-node axios-burp.ts { args: { foo1: 'bar1', foo2: 'bar2' }, headers: { 'x-forwarded-proto': 'https', 'x-forwarded-port': '443', host: 'postman-echo.com', 'x-amzn-trace-id': 'Root=1-5fc6e5e4-36f4714f2d2c8a9a5d91149d', accept: 'application/json, text/plain, */*', 'user-agent': 'axios/0.21.0' }, url: 'https://postman-echo.com/get?foo1=bar1&foo2=bar2' }この他にも
axios.get
のオプションでproxy
を指定して(このときscheme
は"https"
を指定しないとエラー)、burpのinvisible proxyを有効にすることで、tunnel
を使わずに通信内容をのぞき見ることが可能です。ただし、この方法だとクライアントとburpの間の通信(CONNECT
リクエストなど)を観察できないので1、上の方法がおすすめです。参考リンク
プロキシを介したHTTPS通信のしくみについては以下の記事が参考になりました。
HTTPSとCONNECTメソッド - ITの窓辺から
burpから見る方法はないのだろうか? ↩
- 投稿日:2020-12-08T10:53:03+09:00
本当にあった UI の指摘事項
メインページ
これは、私が実際に業務で体験した、UIへの指摘事項の話です。 私は数年間、社内ポータルの開発に携わってきました。 これまで学んできた UI のお作法に則って開発をしてきましたが、 まさか、そこに様々な指摘事項を貰うことになるとは 思いもよりませんでした。概要
UI は我々がPCなどで日常的に目にしているもので、
人の顔と同じ様に、アプリやページによって様々な特徴があります。
そして、顔に目、口、鼻、髪などが揃っている様に、
UI にもお決まりのテンプレートがあります。ただ、万人に通用する UI のテンプレートはまず存在しないでしょう。
例えば、人によって好きな髪型は違うと思います。
UI も同じ様に、使う人、見る人によって好みが分かれます。今回紹介する記事は、
私が UI 作成に携わってきた中で得た、
様々な方々からの指摘・意見をまとめたものです。UI を考える方、作成している方、
これから UI 開発に携わりたい方に、是非見て頂ければと思います。各記事へのリンク
営業担当からの指摘編
Windows 利用者からの指摘編
運用担当者からの指摘編
あとがき
実際の利用者から得た指摘事項は、UI 開発におけるとても大切な財産です。
これをヘアーカタログの様に、UI(事例)カタログとしてまとめておけば、
様々な種類の顧客に提案しやすくなるのではないでしょうか。
- 投稿日:2020-12-08T10:26:59+09:00
React + Unstated Next: リデューサを使ったうえでコンテナに変換する
Unstated Nextは、複数コンポーネントにより組み立てられたツリーの中で、状態を共有して管理するライブラリです。基本的な使い方は「React + Unstated Next: 複数コンポーネントのツリーの中で状態を共有して管理する」で、簡単なカウンターのサンプルをつくりながらご説明しました(図001)。
図001■Unstated Nextを使ったカウンター
本項では、カウンターのサンプルにリデューサ(reducer)を加えます。そのうえで、コンテナに変換して、状態の操作と保持のロジックを分離してみます。上記にリンクしたカウンターのサンプルに手を加えるかたちで進めましょう。
リデューサを使う
リデューサを使うと、コンテナから値の保持が切り分けられます。用いるフックは
useReducer
です。引数にはつぎのように、リデューサ関数と初期状態(オブジェクト)を渡します。戻り値は配列で、要素は現行の状態(state
)とアクション配信関数(dispatch
)のふたつです。const [state, dispatch] = useReducer(リデューサ関数, 初期状態);
useReducer
は、コンテナモジュールsrc/useCounter.js
からつぎのように呼び出します。dispatch
は、アクションと呼ばれるオブジェクトをリデューサに送る関数です。アクションには何が起こったかを示すtype
プロパティが含まれ、必要に応じてその他のデータも加えられます(今回用いたアクションはtype
プロパティしかもちません)。プロバイダから子コンポーネントに渡したい状態は、useReducer
が返した配列要素のstate
から参照してフックの戻り値に加えてください。src/useCounter.js// import { useCallback, useState } from "react"; import { useCallback, useReducer } from 'react'; const useCounter = (initialState = 0) => { // const [count, setCount] = useState(initialState); const [state, dispatch] = useReducer(reducer, { count: initialState }); // const decrement = useCallback(() => setCount((prevCount) => prevCount - 1), []); const decrement = useCallback(() => dispatch({type: 'decrement'}), []); // const increment = useCallback(() => setCount((prevCount) => prevCount + 1), []); const increment = useCallback(() => dispatch({type: 'increment'}), []); // return { count, decrement, increment }; return { count: state.count, decrement, increment }; };新たなリデューサモジュール
src/reducer.js
の記述は以下のコード001のとおりです。イベントと違って、アクションごとのハンドラはもちません。そのため、アクションのtype
プロパティに応じて処理を分けるswitch
文で組み立てます。アクションの配信と同じく、リデューサも状態(state
)を直にはいじりません。改めた状態のプロパティをオブジェクトに収めて返すだけです。これで、
useReducer
フックにより状態の保持が切り分けられました。書き直したカスタムフックのモジュールsrc/useCounter.js
も、併せてコード001に掲げます。コード001■useReducerフックで状態の保持を切り分ける
src/reducer.jsconst reducer = (state, action) => { switch (action.type) { case 'decrement': return {count: state.count - 1}; case 'increment': return {count: state.count + 1}; default: return state; } }; export default reducer;src/useCounter.jsimport { useCallback, useReducer } from 'react'; import { createContainer } from 'unstated-next'; import reducer from './reducer'; const useCounter = (initialState = 0) => { const [state, dispatch] = useReducer(reducer, { count: initialState }); const decrement = useCallback(() => dispatch({type: 'decrement'}), []); const increment = useCallback(() => dispatch({type: 'increment'}), []); return { count: state.count, decrement, increment }; }; export const CounterContainer = createContainer(useCounter);リデューサをコンテナにする
さらに、リデューサ(
react:src/reducer.js
)もコンテナにしてみましょう。コンテナにすることで、ロジックがよりはっきり切り分けられます。コンテナにするには、まずカスタムフックに書き替えなければなりません。フックの関数(
useCounterReducer
)を新たに加え、useReducer
はその中から呼び出します。戻り値は、子コンポーネントに共有する参照が収められたオブジェクトです。カスタムフックをcreateContainer
に渡して、コンテナをつくってください。src/reducer.jsimport { useReducer } from 'react'; import { createContainer } from 'unstated-next'; const initialState = { count: 0 }; const useCounterReducer = () => { const [state, dispatch] = useReducer(reducer, initialState); return { dispatch, count: state.count }; } // export default reducer; export default createContainer(useCounterReducer);状態を操作するコンテナ(
src/useCounter.js
)は、もはやuseReducer
は用いず、リデューサコンテナ(reducer
)に対して呼び出したuseContainer
から参照を得ます。注目していただきたいのは、useReducer
と異なり状態(state
)が丸ごと直に触れないことです。何を参照してよいかは、リデューサコンテナが決められます。src/useCounter.js// import { useCallback, useReducer } from 'react'; import { useCallback } from 'react'; const useCounter = (initialState = 0) => { // const [state, dispatch] = useReducer(reducer, { count: initialState }); const { count, dispatch } = reducer.useContainer(reducer); // return { count: state.count, decrement, increment }; return { count, decrement, increment }; };リデューサコンテナ(
reducer
)もプロバイダでコンポーネントツリーを包みます。状態を操作するコンテナ(CounterContainer
)はリデューサを参照しますので、リデューサの子にしなければなりません。src/App.jsimport reducer from './reducer'; function App() { return ( <reducer.Provider> <CounterContainer.Provider> </CounterContainer.Provider> </reducer.Provider> ); }状態操作のコンテナ(
src/useCounter.js
)に加えて、リデューサもコンテナ(src/reducer.js
)にしました。書き替えた3つのモジュールの記述は、つぎのコード002にまとめたとおりです。他のモジュールの記述や動きについては、以下のサンプル002をご覧ください。コード002■リデューサをコンテナに変換した
src/reducer.jsimport { useReducer } from 'react'; import { createContainer } from 'unstated-next'; const reducer = (state, action) => { switch (action.type) { case 'decrement': return {count: state.count - 1}; case 'increment': return {count: state.count + 1}; default: return state; } }; const initialState = { count: 0 }; const useCounterReducer = () => { const [state, dispatch] = useReducer(reducer, initialState); return { dispatch, count: state.count }; } export default createContainer(useCounterReducer);src/useCounter.jsimport { useCallback } from 'react'; import { createContainer } from 'unstated-next'; import reducer from './reducer'; const useCounter = (initialState = 0) => { const { count, dispatch } = reducer.useContainer(reducer); const decrement = useCallback(() => dispatch({type: 'decrement'}), []); const increment = useCallback(() => dispatch({type: 'increment'}), []); return { count, decrement, increment }; }; export const CounterContainer = createContainer(useCounter);src/App.jsimport React from 'react'; import reducer from './reducer'; import { CounterContainer } from './useCounter'; import CounterDisplay from './CounterDisplay'; import './App.css'; function App() { return ( <reducer.Provider> <CounterContainer.Provider> <div className="App"> <CounterDisplay /> </div> </CounterContainer.Provider> </reducer.Provider> ); } export default App;サンプル001■リデューサをコンテナに変換したカウンター
参考:「Create React App フックによる状態管理 04: カスタムフックにUnstated Nextを組み合わせる」
- 投稿日:2020-12-08T10:14:32+09:00
【初心者向けTips】単純作業はもう嫌だ!GASツール作りの4つの技
この記事はモチベーションクラウドシリーズアドベントカレンダー2020の9日目の記事です。
最近GASでツールを作成したので、その経験を基に、GASツールを作成するための4つの技を紹介します。
この4つの技を覚えれば、GASツールで煩わしい作業から解放されます!4つの技
GASツールを作成するために覚える4つの技は「関数」「イベントトリガー」「スプレッドシート連携」「外部API連携」です。
この4つを覚えればGASツールを作成できます。関数:プログラムを定義する技
イベントトリガー:関数を起動する技
スプレッドシート連携:スプレッドシートを読み込んだり書き込んだりする技
外部API連携:Slackなどの外部APIと連携する技関数:プログラムを定義する技
プログラムを定義するために関数を作成します。GASは定義した関数を実行することができます。
スプレッドシートのメニューバーからツール>スクリプトエディタの順にクリックすると、別タブにスクリプトエディタが表示されます。
スクリプトエディタからgsファイルを編集して、以下のように関数を定義します。
function hello() { // 実行するプログラム }例ではhello関数を定義しました。helloは定義したい関数の名前にあわせて入力してください。
イベントトリガー:関数を起動する技
作成した関数も起動しなければ動きません。作成したプログラムを起動するための3つのイベントトリガーを紹介します。
ボタン
関数を起動するためのボタンを作成して、ボタンがクリックされたら関数が起動するようにできます。
ボタンの作成方法
スプレッドシートのメニューバーから挿入>図形描画をクリックすると、図形描画モーダルが表示されます。
図形描画モーダルでボタンを作成することができます。
図形描画モーダルのメニューバーから図形>図形>ボタンのような図形を選択して図形描画モーダルにボタンを追加します。
図形描画モーダルにボタンを追加したら、ボタンに文言を入力しましょう。
保存して終了ボタンをクリックすると、シートにボタンを作成できます。
作成したボタンを右クリックすると、ボタンの右上に︙が表示されます。
︙>スクリプトを割り当てをクリックすると、スクリプトを割り当てモーダルが表示されます。
hello関数を割り当てる場合は、helloと入力してOKボタンをクリックすれば、hello関数を割り当てることができます。
タイマー
タイマーを設定して、関数を特定の時間に起動するようにできます。
タイマーの設定方法
スプレッドシートのメニューバーからツール>スクリプトエディタの順にクリックすると、別タブにスクリプトエディタが表示されます。
スクリプトエディタのメニューバーから現在のプロジェクトトリガーをクリックすると、別タブにトリガー画面が表示されます。
トリガーを追加ボタンをクリックすると、GAS Botsのトリガーを追加モーダルが表示されます。
設定項目 設定内容 実行する関数を選択 タイマーで起動したい関数を選択します。 実行するデプロイを選択 Headを選択します。 イベントのソースを選択 タイマーを設定したいので時間主導型を選択します。 イベントのソースを選択で時間主導型を選択すると、時間ベースのトリガーのタイプを選択が設定できるようになります。
- 特定の日時・・・特定の日時に1回関数を起動します。
- 分ベースのタイマー・・・指定の分おき毎に関数を起動します。1分おきから30分おきの範囲で選択できます。
- 時間ベースのタイマー・・・指定の時間おき毎に関数を起動します。1時間おきから12時間おきの範囲で選択できます。
- 日付ベースのタイマー・・・特定の時刻に関数を起動します。午前0時〜1時から午後11時〜午前0時の範囲で選択できます。
- 週ベースのタイマー・・・特定の曜日の特定の時刻に関数を起動します。週は毎週月曜日から毎週日曜日の範囲で選択できます。時刻は午前0時〜1時から午後11時〜午前0時の範囲で選択できます。
- 月ベースのタイマー・・・特定の日の特定の時刻に関数を起動します。日は1日から31日の範囲で選択できます。時刻は午前0時〜1時から午後11時〜午前0時の範囲で選択できます。
Webhook
WebhookとはSlackなどの外部サービスのイベントを受信するための仕組みです。
WebhookでイベントをHTTPリクエストとして受信して、関数を起動するようにできます。Webhookの実装方法
WebhookはdoGet関数/doPost関数で実装します。
doGet関数は連携する外部サービスがGETメソッドでリクエストを送信する場合に利用します。
doPost関数は連携する外部サービスがPOSTメソッドでリクエストを送信する場合に利用します。
連携する外部サービスがどのようなリクエストを送信するかによってdoGet関数/doPost関数のどちらを利用するかは変わります。doGetの実装例
function doGet(event) { const target = event.parameter.target; hello(target) }例ではリクエストのtargetパラメータを取得して、hello関数を実行します。
event.parameterプロパティからリクエストのパラメータを取得できます。doPostの実装例
function doPost(event) { const parameter = JSON.parse(event.postData.contents); const target = parameter.target; hello(target) }例ではリクエストのtargetプロパティを取得して、hello関数を実行します。
event.postData.contentsからリクエストのbodyを取得できるので、JSON.parseでパースしてオブジェクトに変換して利用します。複数の外部サービスからリクエストを受け付ける場合
doGet関数/doPost関数は1つしか実装できないため、doGet関数/doPost関数内でif文などを利用して外部サービス毎に処理を切り替える必要があります。Webhook URLの生成方法
Webhookの関数を作成した後、Webhookを起動するためのURLを生成する必要があります。
スプレッドシートのメニューバーからツール>スクリプトエディタの順にクリックすると、別タブにスクリプトエディタが表示されます。
スクリプトエディタのメニューバーから公開>ウェブ アプリケーションとして導入…をクリックすると、「Deploy as web app」モーダルが表示されます。
設定項目 設定内容 Project version Newを選択します。 Execute the app as 自分のメールアドレスを選択します。 Who has access to the app: Anyone, even anonymousを選択します。
Deploy
ボタンをクリックします。
Current web app URL
が表示されます。このURLが外部サービスからリクエストを送信するとWebhookが実行できるWebhook用のURLです。
「非公開」にはできませんので、第三者に漏洩しないように取り扱いには十分にご注意ください。
Webhook用のURLを生成した後に修正した場合
修正を反映するためにWebhook用のURLを再生成する必要があります。スプレッドシート連携:スプレッドシートを読み込んだり書き込んだりする技
スプレッドシートと連携して、関数の実行結果をスプレッドシートに書き込んだり、関数を実行する用の設定をスプレッドシートから読み込んだりすることができます。
対象のセルを取得
スプレッドシートに書き込みをする場合でも読み込みをする場合でも、操作対象のセル範囲を取得する必要があります。
操作対象のセル範囲を取得するには、以下のメソッドを利用します。
SpreadsheetApp.getActiveSpreadsheet()
・・・対象のスプレッドシートを取得します。Spreadsheet#getSheetByName(name)
・・・指定のシート名に該当するシートを取得します。Sheet#getRange(row, column)
・・・指定の引数に該当するセル範囲を取得します。Sheet#getRange(row, column, numRows)
・・・指定の引数に該当するセル範囲を取得します。row, column
で範囲の開始位置を指定して、numRows
で範囲の終了位置を開始位置から相対的に指定します。Sheet#getRange(row, column, numRows, numColumns)
・・・指定の引数に該当するセル範囲を取得します。row, column
で範囲の開始位置を指定して、numRows, numColumns
で範囲の終了位置を開始位置から相対的に指定します。Sheet#getRange(a1Notation)
・・・指定の引数に該当するセル範囲を取得します。文字列でgetRange("A1")
、getRange("A1:B2")
、getRange("A2:A")
などと指定します。
Sheet#getRange(*)
の指定方法が分かりづらい場合、こちらが分かりやすいかもしれません。書き込み
対象のセル範囲に値を書き込むには、Range#setValues(values)
メソッドを利用します。valuesは値の2次元配列です。以下の例シートのように値を設定する場合は、
A B C 1 A1 B1 C1 2 A2 B2 C2 以下のように実装します。
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet(); const sheet = spreadsheet.getSheetByName("例"); const range = sheet.getRange("A1:C2"); range.setValues([ ["A1", "B1", "C1"], ["A2", "B2", "C2"] ]);読み込み
対象のセル範囲から値を読み込むには、Range#getValues()
メソッドを利用します。戻り値は値の2次元配列です。以下の例シートから値を設定する場合は、
A B C 1 A1 B1 C1 2 A2 B2 C2 以下のように実装します。
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet(); const sheet = spreadsheet.getSheetByName("例"); const range = sheet.getRange("A1:C2"); const values = range.getValues(); console.log(values); // output: [["A1", "B1", "C1"], ["A2", "B2", "C2"]]外部API連携:Slackなどの外部APIと連携する技
関数から外部APIにHTTPリクエストを送信することができます。
外部API連携することでGASからSlackにメッセージを送信するなどができます。URL Fetch App
GASから外部APIにHTTPリクエストを送信するには、UrlFetchApp.fetch(url[, params])
メソッドを利用します。戻り値はHTTPResponseです。引数は主に以下の値を指定します。
引数 型 説明 url String HTTPリクエストを送信する宛先のURL params.contentType String リクエストのContent-Typeを指定します。JSONを送信する場合は "application/json"
を指定します。params.headers Object リクエストヘッダをkey/valueのオブジェクトで指定します。 params.method String HTTPメソッドを指定します。デフォルトは "get"
です。params.payload String リクエストボディを指定します。GETメソッドの場合は指定できません。オブジェクトを指定すると application/x-www-form-urlencoded
かmultipart/form-data
かとして扱われます。HTTPResponseは主に以下のメソッドを利用します。
メソッド 戻り値の型 説明 getContentText() String レスポンスボディを取得します。 getHeaders() Object レスポンスヘッダを取得します。 getResponseCode() Integer 200
などのHTTPステータスコードを取得します。Slackのsampleチャンネルに"hello"というメッセージを送信する場合は、以下のように実装します。
const data = { channel: "sample", text: "hello" }; response = UrlFetchApp.fetch( "https://slack.com/api/chat.postMessage", { contentType: "application/json" headers: { "Authorization": "Bearer xoxp-xxxxxxxxx-xxxx" }, method: "post", payload: JSON.stringify(data) }); console.log(response.getResponseCode()); // output: 200 console.log(response.getContentText()); // output: '{ "OK": true, ... }'まとめ
GASでツールを作成するための4つの技を紹介しました。
ほとんどのツールは4つの技の組み合わせで作成することができます。
日々の業務やプライベートで自動化したい作業があれば、ぜひ自動化して楽になってもらえれば嬉しいです!私は4つの技を駆使して、単純作業だった勤怠連絡をツール化して、楽をしてます!
皆様もGASでツールを作成してエンジョイライフを過ごしましょう!
【補足】なぜ、GAS?
GUI/CUIを作るコストが少ない
GUIはスプレッドシートの機能で簡単に作成することができます。実行環境を準備する必要がない
GASはGoogleのスプレッドシート上で動作するため、実行環境を準備する必要がありません。簡単に共有できる
GASはGoogleのスプレッドシート上で動作するため、他のユーザーにも簡単に共有できます。【おまけ】GASで何を作った?
私は4つの技を駆使して、毎日Slackに勤怠連絡していた作業をツール化しました。
勤怠連絡
ワンクリックで勤怠連絡を行いたかったので、ボタンをクリックしたらSlackに勤怠連絡できるようにしました。
イベントトリガーのボタンを作成して、ボタンクリックに割り当てた関数から外部API連携を実行して、Slackに勤怠連絡メッセージを送信できるようにしました。
ボタンは「勤務開始ボタン/勤務終了ボタン/休憩開始ボタン/休憩終了ボタン」を実装し、ボタン毎に勤怠連絡メッセージが変わります。
ツールを作る前はSlackでメッセージと勤怠日時を手動入力してました。ユーザ設定
ツールは他の開発メンバーにも共有したかったので、ユーザ設定シートを作って、勤怠連絡メッセージのユーザ名とアイコンを開発メンバー毎に設定できるようにしました。
スプレッドシート連携でユーザ設定シートと連携して、ユーザ設定シートからユーザ名とアイコンを読み込んで、勤怠連絡メッセージのパラメータに設定するようにしました。
タイムカード
勤怠連絡した日時を簡単に視認できるようにしたかったので、タイムカードシートを作成して、タイムカードに勤怠日時を記録するようにしました。
勤怠連絡の操作時に、スプレッドシート連携でタイムカードシートと連携して、タイムカードシートに勤怠日時を書き込みました。
タイムカードシートに日々の稼働状況が記録されているため、「日付/出勤時間/退勤時間/非稼働時間」を一覧で見ることができます。
ツールを作る前はSlackに連絡した内容をスクロールで遡って確認してました。月次の稼働報告のために1ヶ月分も遡っていたので大変でした。
- 投稿日:2020-12-08T09:01:19+09:00
超初心者向けのNode-RED基本のき
☆・:。〇。:・☆・:。〇。:・☆・:。〇。:・☆・:。〇。:・☆
アドベントカレンダー8日目です!
☆・:。〇。:・☆・:。〇。:・☆・:。〇。:・☆・:。〇。:・☆私自身、まだ、Node-REDでの実装経験は浅いのもあり、今回は超初心者向けの記事を書きたいと思います!
(年末にかけて学習を進めてNode-RED関連の投稿数を増やしていく予定です。)
新参者ですが、温かい目で見守っていただけると幸いです。①Node-REDとは?
Node-REDはハードウェアデバイス、APIおよびオンラインサービスを新しく興味深い方法で接続するためのツールです。
ブラウザベースのエディタによってパレットに並ぶ多種多様なノードを結びつけて用意にフローを作成でき、さらにシングルクリックで実行環境にデプロイすることができます。エンジニアであれば何となくイメージは湧くと思いますが・・・
つまりIoT(モノとプログラミングをつなげる技術)や、
Webページなどのオンラインサービスだったりを、
簡単なマウス操作で、簡単に動かすことができるのがNode-REDみたいです!超初心者向けの記事ということで、
早速ですが、上記の引用で出てきたワードを辞書的に記載してみようと思います。
Node-REDの話とは少しそれてしまうかもしれませんが、ご了承ください。。。APIとは?
APIとはソフトウェアやアプリケーションなどの一部を外部に向けて公開することにより、第三者が開発したソフトウェアと機能を共有できるようにしてくれるものです。
USBは外部デバイスとパソコンを繋ぐインターフェースですが、APIはソフトウェア同士を繋げます。
つまり、異なるソフトウェアやサービス間で認証機能を共有したり、チャット機能を共有したり、片方から数値データを取り込み、別のプログラムでそのデータを解析したりできるようになります。
アプリとアプリを繋げることによって、機能性を拡張させ、さらに便利に使えるようにし、欲を言えば両方のアプリにとってウィン・ウィンの状態を生み出すのがAPIの狙いです。
引用:今さら聞けないIT用語:やたらと耳にするけど「API」って何?実行環境とは?
そのソフトなりプログラムなりを動かすために必要な物が揃った『場』のこと
引用:https://wa3.i-3-i.info/word13151.html
デプロイとは?
使える状態にすること
引用:https://wa3.i-3-i.info/word16767.html
Node-REDで開発したFlow(プロジェクト)はJSONを利用してインポート、エクスポートすることができます。
JSONとは?
JSONとは「JavaScriptのオブジェクト記法を用いたデータ交換フォーマット」です。
Python、PHP、JavaScript、C++、Javaなど様々な言語でサポートされており、JSONを間に挟むことで各プログラミング言語間のデータの受け渡しがとても簡単にできます。引用:https://products.sint.co.jp/topsic/blog/json#toc-0
例.json{ "name": "Tanaka", "age": 26 }JSONは、Swiftなどでのアプリ開発でもよく見かけますね。
②Node-REDの仕組み
こちらの表現がわかりやすかったので引用させていただきます。
メッセージはJSONデータで構成され、msgという一番上のオブジェクトの中で、
各ノードで処理された内容がバケツリレーのようにやり取りされていきます引用:大人のSTEM教育!話題のローコードプログラミングを Node-RED で体験!」に関するつぶやきのまとめ
Node-REDでは、ノードをつないでプログラミングをしますね。
ノードというのは機能をもつ(Scratchでいう)ブロックみたいなものです。msgとは
メッセージ(message)の略です。プロパティセットを持つことができる、JavaScriptオブジェクトです。
通常はpayloadプロパティを持ちます。プロパティの型には、以下のようなJavaScriptの型が存在します。
Boolean - true, false Number - 例 0, 123.4 String - "hello" Array - [1,2,3,4] Object - { "a": 1, "b": 2} NullNode-REDで実装するには、プロパティや型など、
テキストコードの基礎知識もそれなりに必要みたいですね!③Node-REDの操作感
操作エリアは3つに分かれていて、
プログラミング未経験者でも、基本用語だけ理解しておけば、何となく操作感が分かりそうなシンプルさです。
ノードもシンプルな英単語で構成されています。
セクションで別れているので、扱いやすそうですね。スクラッチにも少し画面構成が似ていますね。
まとめ
基礎中の基礎を記載させていただきました。
まとめると
- Node-REDでは、JavascriptやJSONを使う!
- プロパティや型などのプログラミングの基本的な概念を理解すれば、簡単に実装できそう!
- 操作感もわかりやすい!
ということがわかりました。
何か不足点などございましたら、コメントにてご指摘いただけますと幸いです。
参考
- 投稿日:2020-12-08T07:44:11+09:00
カーソルAPIを使ってレコードを一括取得したい(ただし可読性の高いものに限る)
こんにちは、tarimoです。
kintone Advent Calendar 2020本日12/8の担当です!
久しぶりにQiitaに投稿するので、Markdownのやり方を一部忘れてしまい想定以上に時間がかかっております・・・?そもそもカーソルAPIってなんだ?
さて、皆さんはカーソルAPIお使いでしょうか?
実はリリースされたのは2019年7月。大量なデータを一括で取得するためのAPIですが、私の周りでは当時もあまり注目度は高くなかったように思えます。ですが、予期せずして、これまでの通常のREST APIでのレコード一括取得について制限がかかるアナウンスがありました。
kintone API レコード一括取得APIのoffsetの上限値制限についてレコード一括取得APIでoffsetに大きな値を指定した場合、サーバーに非常に高い負荷がかかることが確認されています。
これを回避するため、レコード一括取得APIのoffsetの上限値を10,000までに制限するとともに、大量データの一括取得を行っても負荷の低い(現行のレコード一括取得APIと比較して)API「cursor.json」を新しく提供することにしました。要は「データを10,000件超える一括取得」は負荷が高いので控えてくださいね。というお話です。至ってごもっともな話です。
「警告が出るだけでしょ?ウチは大丈夫(意訳:API書き換えめんどくさい)」
そして再度2020年4月に記事に追記されます。
kintone API レコード一括取得APIのoffsetの上限値制限について
2020年7月の定期メンテナンス以降、レコード一括取得APIでoffset上限値10,000を超える処理を行った場合、そのリクエストは処理されなくなります。
ただし、2020年7月の定期メンテナンス以前からご利用いただいているお客様については、kintone管理者および該当のアプリ管理者の画面上に「上限値を超過した旨」が警告表示され、リクエストは処理される仕様となっています。
※警告が表示されたお客様は上記「3. 本件仕様変更にかかる対応方法」をご確認の上、対応のご検討をお願いします。このあたりから
「どうしようか」「書き換えめんどくさいな」「誰かやってくんない?」
など私の周りがざわつき始めました。。? 何なら、7月以降に「なんかエラー出たよ?」など連絡をいただく始末。。。OH...?そもそもカーソルAPIの仕組みってどんなの?
わかりやすいように説明用の図解してみました。↓
カーソルAPIに載せ替えも結構だけどちょっと待って!!
より安全に大量なデータを取得できるのがカーソルAPIですが、一寸お待ちを。
カーソルAPIで取得しようとしているそのデータ、「ホントに必要でしょうか??」載せ替えの前に今一度考えてみたほうがいいかもしれません。また、レコードIDを利用して取得する方法もあります。
仕組みが理解できた上で、カーソルAPIの載せ替えを試みます。ただし・・・。
そこで「可読性の高いソースの提供をお願いしたい!」 というオーダーを頂きました。
申し遅れていましたが、ワタクシ社内のkintone管理以外にも、結構な数お客様のkintoneのカスタマイズや管理者の方へのサポートをしております。(むしろ近年はそちらがメイン業務)
「まあ自分はわかるからちゃちゃっと・・・」ではなく、弊社お客様向けにはなぜ動くかの動作原理の説明込みでわかりやすいコーディングサンプル提示をする必要があります。公式のコーディング例はアカンのか?(そんなことはない)
レコード一括取得の JavaScript コーディング例:カーソル API を利用する方法
/* * get all records function by cursor id sample program * Copyright (c) 2019 Cybozu * * Licensed under the MIT License */ // カーソルを作成する var postCursor = function(_params) { var MAX_READ_LIMIT = 500; var params = _params || {}; var app = params.app || kintone.app.getId(); var filterCond = params.filterCond; var sortConds = params.sortConds; var fields = params.fields; var conditions = []; if (filterCond) { conditions.push(filterCond); } var sortCondsAndLimit = (sortConds && sortConds.length > 0 ? ' order by ' + sortConds.join(', ') : ''); var query = conditions.join(' and ') + sortCondsAndLimit; var body = { app: app, query: query, size: MAX_READ_LIMIT }; if (fields && fields.length > 0) { body.fields = fields; } return kintone.api(kintone.api.url('/k/v1/records/cursor', true), 'POST', body).then(function(r) { return r.id; }); }; // 作成したカーソルからレコードを取得する var getRecordsByCursorId = function(_params) { var params = _params || {}; var id = params.id; var data = params.data; if (!data) { data = { records: [] }; } var body = { id: id }; return kintone.api(kintone.api.url('/k/v1/records/cursor', true), 'GET', body).then(function(r) { data.records = data.records.concat(r.records); if (r.next) { return getRecordsByCursorId({ id: id, data: data }); } return data; }); }; /* * @param {Object} params * - app {String}: アプリID(省略時は表示中アプリ) * - filterCond {String}: 絞り込み条件 * - sortConds {Array}: ソート条件の配列 * - fields {Array}: 取得対象フィールドの配列 * @return {Object} response * - records {Array}: 取得レコードの配列 */ var getRecords = function(_params) { return postCursor(_params).then(function(id) { return getRecordsByCursorId({ id: id }); }); };リンク先からベタっと引用してみましたが、わかりにくいことはないソースだと思います。
ただ、お客様先のコーディングスタイルとはちょっと違いました。
- 関数の再帰呼び出しはあまり使われていない
なるほど、取ってつけたように一部分が違うコーディングスタイルになると気持ち悪いですもんね、そこは合わせましょう。
- カーソルを開いて、データ取得のあたりがネストが深くなりそう
ここはawait/async でネストが深くならないようフラットに実装したほうが良さそうです。ここも作り変えましょう。
早速サンプルアプリ作成開始!
こんな感じで30,000件ほどのデータが格納されているサンプルアプリを作成。
app.record.index.showの一覧表示あたりで30,000件のデータをカーソルAPIから取得するサンプルを作ってみました。
app.record.index.showでカーソルAPIからデータ取得してコンソールに表示します。
(function() { "use strict"; kintone.events.on('app.record.index.show', async function(event) { try{//カーソルAPIを使用してデータを全件取得する。 let readCount = 0;//カーソルAPIから何度呼び出したか let cursorId = await createCursor();//新規カーソルを作成&得られたカーソルID let totalData = {records: []};//カーソルAPIで取得される全件レコード let isReading = true;//カーソルAPIからのデータ取得中であるかを示す真偽値 console.log("■■■■■■ 処理開始 ■■■■■■"); while (isReading == true){//作成したカーソルより、500件づつデータを取得&結合 let retValue = await getRecordsByCursorId(cursorId); totalData.records = totalData.records.concat(retValue.records);//取得したデータを結合 readCount=readCount+1;//カーソルAPIからデータを取得したのでカウントアップ console.log("データ取得【"+readCount+"】回目"); if(retValue.next==false){ isReading = false; } if(readCount>=10000){/*念の為の防波堤。。デバック中に暴発(無限ループ)しないように!*/ console.log("■■ 処理中断 ■■"); isReading = false; break; } } console.log("■■■■■■ 処理完了 ■■■■■■"); console.log("取得したデータは??"); console.dir(totalData.records); } catch{ /** * データ取得中にエラーが発生し取り込みが中断された場合、明示的にカーソルを削除する必要がある。 * ※カーソル経由で全てのレコードを取得すると、当該カーソルは自動的に削除される。 */ await deleteCursor(cursorId); } finally{ /** * ここにカーソルAPIからのデータ取得後処理が入る。 */ console.log("処理おわり!!"); return event; } }); /*************************************** * 概要:カーソルを削除する * cursorId {String}: カーソル * return {Allay}: * - records : カーソルから取得した一部レコード * - next : 次のカーソルで取得するデータが存在するか ***************************************/ async function getRecordsByCursorId(cursorId) { let data = {records: []}; let body = {id: cursorId}; return kintone.api(kintone.api.url('/k/v1/records/cursor', true), 'GET', body).then(async function(resp) { return {records:resp.records,next:resp.next}//取得したデータおよび、次のカーソル取得データ(next)が存在するか?を返却 }); }; /*************************************** * 概要:カーソルを作成する * return cursorId{String}: 新たに取得されたカーソル。取得失敗の場合は値はセットされない。 ***************************************/ async function createCursor() { const getPerSize = 500;/* 500件づつ取得する */ let body = { 'app': kintone.app.getId(), 'fields': ['レコード番号', '作成者', '作成日時'], 'query': 'order by レコード番号 asc', 'size': getPerSize }; return kintone.api(kintone.api.url('/k/v1/records/cursor', true), 'POST', body).then(async function(resp) { // success console.log("■■ カーソル作成 ■■"); console.log("cursorIdは"+resp.id); return resp.id; }, function(error) { // error console.log(error); return; }); } /*************************************** * 概要:カーソルを削除する * cursorId {String}: カーソル * return {boolean}: true 成功 / false失敗 ***************************************/ async function deleteCursor(cursorId){ let body = {'id': cursorId}; return kintone.api(kintone.api.url('/k/v1/records/cursor', true), 'DELETE', body).then(async function(resp) { // success console.log(resp); return true; }, function(error) { // error console.log(error.message); return false; }); } })();まだエラー処理など足りない部分もあるかもですが、動作させてみましょう!
動作結果
まとめ
「そのJavascriptカスタマイズ、ホントに必要?」を挟んでから実装しよう!
コピペする前にサンプルアプリで検証して自分の血肉にしてから実装しよう!
制限事項もよく読もう!→カーソルAPIの制限事項
今日のところはいじょうです。