- 投稿日:2021-06-21T23:32:07+09:00
ルパンのサブタイトルをVueを使って再現してみた
はじめに Vue.jsの勉強がてら何か動きのあるページを作ってみようと思い、今回のものを作りました。 初めてVueを触った+基礎しか理解できていないということでコードが雑になっています。 あしからず。 概要 ルパンのサブタイトル(ググれば出てくる)を再現できるWebアプリを作ってみた。 理由は最初にある通り。細かく言えばVue独特の双方向のデータバインドを試したかった。 環境 開発環境 MacOS Sierra(10.12) vue(2.6.12) Vim 実行環境(サーバサイド) AWS EC2(Amazon Linux) Route53 S3 nginx AWSの構成図はこんなかんじ 実行環境(クライアントサイド) Chrome(91.0.4472.106) OSはWindows10/MacOS Sierra で実行できたことを確認済み 作成物 githubはこちら↓ https://github.com/KenFujita/Rupan_no_turn ソースコードは上記を参照されたし。 工夫点 双方向のデータバインドが活かされるように、リアルタイムで再現できるようにした。 あとは最後の「タッタラターンダダッダダッダダッダダッダッダッダン!」という音を鳴らしているときは文字入力を受け付けないようにしている <input type="text" v-bind:disabled="isDis" @keydown.enter="isEnter" @compositionstart="composing=true" @compositionend="composing=false" v-on:input="write" v-model="word" > こんな感じに入力フォームを作成し if(!this.composing || (this.composing && this.word.length > 25)){ this.isDis = true; this.enterPush.currentTime = 0; this.enterPush.play(); const res = await this.donotWrite(this.enterPush); console.log(res); this.isDis = false; this.word = ''; } donotWrite:function(aud) { return new Promise((resolve) => { aud.addEventListener('ended',()=>{ resolve('done'); }); }); } これらをmethodsで関数として宣言してあげると、音楽が鳴っている間は入力させないといった機能が実装できる。 難しかったところ あまり難しくないコードなので詰まるほどのものはなかった。 強いて言うなら、全角文字を扱う場合の文字変換の確定をするEnterとサブタイの音楽を鳴らすトリガーになるEnterの判定をしないといけなかった。 (そうしないと文字確定時のEnterキー押下イベントで音楽がなってしまうため) あとVueのライフサイクル(今回だとcreatedとmountedの違い)がよくわからなかった。 その他音楽ファイルを読み込むタイミング、文字入力を受け付けない処理の実装、divタグの使い方、CSS、AWSなど 今後 CI/CDで継続的うんたらをやってみたい。 今の所AWS構成図のPrivate EC2が無職なのでそこで実践してみたいと思う 参考 https://wordpress.ideacompo.com/?p=13989 https://aqua-engineer.com/post-884/ https://gotohayato.com/content/496/ おまけ 既に何名もの方々が再現アプリを作っていた模様。 https://qiita.com/Sirloin/items/3db49c97763042e95eb5 何番煎じになっているのかはさておき、先駆者様の作成したものも触れてみよう
- 投稿日:2021-06-21T23:25:48+09:00
なぜrequire又はimportができない?!(a.k.awebpack投入の理由)
0. 当時の環境 C:\git\keepOnEye_money>node -v v12.18.4 C:\git\keepOnEye_money>npm -v 6.14.6 エラーモメント ライブラリーをインストールは無事完了。 C:\git\keepOnEye_money>npm install pinyin > nodejieba@2.5.2 install C:\git\node_modules\nodejieba > node-pre-gyp install --fallback-to-build [nodejieba] Success: "C:\git\node_modules\nodejieba\build\Release\nodejieba.node" is installed via remote npm WARN saveError ENOENT: no such file or directory, open 'C:\git\package.json' npm WARN enoent ENOENT: no such file or directory, open 'C:\git\package.json' npm WARN git No description npm WARN git No repository field. npm WARN git No README data npm WARN git No license field. + pinyin@2.10.2 added 60 packages from 97 contributors and audited 306 packages in 10.529s 19 packages are looking for funding run `npm fund` for details found 0 vulnerabilities 2 packages are looking for funding run `npm fund` for details found 0 vulnerabilities ドキュメントのサンプールコードを叩いてみよう。 app.js var unirest = require('unirest'); でも画面では効かないよう、、 Uncaught ReferenceError: require is not defined 原因はCommonJSとes6の違い(と思いきや違ったことを後でわかる) とても親切な説明がありまして貼っておくと、 https://zenn.dev/naoki_mochizuki/articles/46928ccb420ee733f78f 要するに commonJS: requireが es06: importで使うことになる なので特にどっちを選んでも機能は同じのこと。 試しにimportに変えてみた。 app.js import unirest from 'unirest'; Uncaught SyntaxError: Cannot use import statement outside a module モジュール関係の問題らしい、、 webpackの登場。 簡単に言いますと、散らかっているモジュルを一つにまとめてくれるもの。開発環境とブラウザーは環境が違うため、ブラウザーで表現したい機能の実現のために開発環境から必要道具(?)をまとめて送る必要がある。 袋みたいに一つにまとめるものの認識。 webpackをセットアップしてみよう。 npm init npm i -D webpack webpack-cli 上記のコマンドを作成してwebpack.config.jsを生成、中身を埋める。 const path = require('path'); module.exports = { mode: 'production', // or "development" or "none", output: { filename: 'bundle.js', path: path.resolve(__dirname, './dist'), }, entry:{ main : "./app.js" }, }; HTMLのjsパスをOUTPUTのパスに修正する。 index.html <!-- <script src="app.js"></script> --> <script src="./dist/bundle.js"></script> package.jsonへwebpack実行用コマンドを追加する。 package.json "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "webpack" }, 最後に確認用コードを追加してみる app.js var pinyin = require("pinyin"); console.log(pinyin("中心")); それでは実行してみようー! npm run build できました!! なぜlibraryのrequire(import)が効かなかった?はnode.js はサーバーの言語なのでfrontのブラウザーと話すことがが違う とのことでした。今のエラーはnode.jsのみの環境だと問題なしと思われて、画面側に持ち出した時の問題だったこと!
- 投稿日:2021-06-21T23:15:07+09:00
JavaScriptの === について
最近、仕事でよくJavaScriptを使用するようになったのですが、その中で「===」という演算子が出てきたのが気になったので調べてみました。 相変わらず自分用メモです。 「===」は、「===厳密等価演算子」と言って、「型を変換することなく厳密に等しいかどうか」をチェックする演算子です。 Javaなどでよく使われる「==等価演算子」は、たとえば数値型の「1」と文字列型の「1」なら、自動的に文字列型の「1」を数値型に変換し、変換後の値が等価であればTrueを返してくれます。 しかし、「===厳密等価演算子」は型を変換せずに「等価であるか」を判定するため、上記のような例の場合はFalseを返します。 「===厳密等価演算子」でTrueの結果を得たい場合は、型、値の両方が一致していなければならないのです。 また、「===」の否定は「!==」となります。記述するときに迷ったので書き残しておく。 当然ながら、先程とは逆で数値型の「1」と文字列型の「1」の場合はTrueを返し、数値型の「1」同士ならFalseを返します。
- 投稿日:2021-06-21T20:57:49+09:00
JavaScript オブジェクト 未定義判定(途中の添字も含む)
追記 オプショナルチェインニング演算子 コメントにて、もっとシンプルな方法を教えていただきました。ありがとうございます。 const test = { apple: { 2019: { A: 15000, B: 16000, C: 12000, D: 21000 }, 2021: { A: 15000, C: 16000 }, }, }; console.log(test?.["apple"]?.[2020]?.["A"]); // undefined console.log(test?.["apple"]?.[2021]?.["C"]); // 16000 この例では、?. を用いています。オプショナルチェインニング演算子、という名前らしいです。なかなか格好いい名前です。 ドキュメントによると、 オプショナルチェイニング演算子 ?. は、接続されたオブジェクトチェーンの深くに位置するプロパティの値を、チェーン内の各参照が正しいかどうかを明示的に確認せずに読み込むことを可能にします。 ?. 演算子の機能は . チェーン演算子と似ていますが、参照が nullish (null または undefined) の場合にエラーとなるのではなく、式が短絡され undefined が返されるところが異なります。 関数呼び出しで使用すると、与えられた関数が存在しない場合、 undefined を返します。 これは、参照が失われた可能性のある連結されたプロパティにアクセスする時、結果的に短く単純な式になります。また、必要なプロパティの存在が保証されていない場合にオブジェクトのコンテンツを探索するのにも役立ちます。 参考:Optional chaining (?.) - JavaScript | MDN ということで、未定義のオブジェクトにアクセスしようとしたとき、エラーではなく undefined を返してくれる演算子です。 これを使えば、別で関数を用意する必要もなく、一行でシンプルに書けるため、これを使うのが一番良さそうですね。 ただ、この演算子は IE には対応していないようなので、もし対応が必要であれば、下に書いた方法を参考にしていただくと良いと思います。 教えていただき、ありがとうございました。 はじめに 私はよく、JavaScriptで多次元のオブジェクトを使います。今回はオブジェクトの未定義の判定についてです。途中で未定義になる場合の対処について書いてみます。 単純に、オブジェクトが未定義、または null であるかは以下のように判定できます。 const test = {"id":1,"name":test}; if(test["age"] == null){ console.log("未定義です。"); } // 未定義です。 しかし、以下のような場合はエラーとなります。 const test = { apple: { 2019: { A: 15000, B: 16000, C: 12000, D: 21000 }, 2021: { A: 15000, C: 16000 }, }, }; if(test["apple"][2020]["A"] == null){ console.log("未定義です。"); } // Uncaught TypeError: test.apple[2020] is undefined この場合、test["apple"][2020] が未定義であるため、test["apple"][2020]["A"] のnull判定をする前にエラーが出てしまうというわけです。 こんなオブジェクト作るなよ、と思われるかもしれませんが、とりあえず、このようなオブジェクトで途中でもちゃんと判定できるようにする方法を2つ考えてみました。 1つ目 論理演算子 const test = { apple: { 2019: { A: 15000, B: 16000, C: 12000, D: 21000 }, 2021: { A: 15000, C: 16000 }, }, }; if ( ((test && test["apple"] && test["apple"][2020] && test["apple"][2020]["A"]) || null) == null ) { console.log("未定義です。"); } // 未定義です。 この例では、論理演算子を使っています。 A && B とすると、A が true であれば B の値を、A が false であれば A の値を採用します。 A || B とすると、A が false であれば B の値を、A が true であれば A の値を採用します。 undefined は false 扱いですので、 test test["apple"] test["apple"][2020] test["apple"][2020]["A"] のうちどれかが未定義であれば false となり、false || null で右の値が採用されて、null となります。 逆に、全てが定義されていれば、true || null で左の値が採用されるというわけです。 しかしこの方法では、一々記述が長くなる、undefined 以外に false 判定される値(0とか)が入っているとうまく動かないといった問題点があります。ということで、関数で判定できるように書いてみました。 2つ目 関数 const test = { apple: { 2019: { A: 15000, B: 16000, C: 12000, D: 21000 }, 2021: { A: 15000, C: 16000 }, }, }; function returnLastKeysValueIfDefined(object, keys) { if (object == null) { return false; } for (let i = 0; i < keys.length; i++) { if (!(object instanceof Object) || object[keys[i]] == null) { return false; } object = object[keys[i]]; } return object; } if(!(returnLastKeysValueIfDefined(test,["apple",2020,"A"]))){ console.log("未定義です。"); } // 未定義です。 console.log(returnLastKeysValueIfDefined(test,["apple",2021,"A"])); // 15000 この関数では、引数1つ目に判定したいオブジェクトを入れます。引数2つ目には、判定したいキーを、順番に配列にして入れます。 見て分かる通り、この関数では、単純に最初から見ていき、null であれば false を返しています。すべて値があれば、最後の値を返しています。 普通は null の判定だけでよいのですが、 const str = "こんにちは。"; console.log(str[1]); // ん このように、string 型に数字の添字を使うと1文字ずつアクセスできます。今回の場合はこれでは都合が悪いため、 if (!(object instanceof Object) || object[keys[i]] == null) { return false; } このようにオブジェクトであるかの判定を入れて、回避しています。 最後に、正直JavaScript(というかプログラミング自体)があまり良くわかっていない状態で書いていますので、致命的なミスや、こっちのほうが良いなどあると思います。その時は教えていただけるとありがたいです。 読んでいただき、ありがとうございました。
- 投稿日:2021-06-21T20:02:58+09:00
JavaScript 関数をキャッシュする(Reactとかで使えるかも)
React でJSX内に、アロー関数式を書いたりしていると、毎度呼び出すときにレンダリングが走っちゃうんじゃないのかな、 ということを回避するための仕組みの試作です。 const _cashFuncMap = new Map(); const cashFunc = (func) => { const strFunc = func.toString(); let result = _cashFuncMap.get(strFunc); if (result === undefined) { result = func; _cashFuncMap.set(strFunc, func) } return result; }; const f1 = () => 'test'; console.log(f1()); // "test" const f2 = cashFunc(f1); console.log(f2()); // "test" console.log(f1 === f2); // true const cf = cashFunc; const f3 = cf( (a) => a + a ) console.log(f3('ABC')); // "ABCABC" const f4 = cf( (a) => a + a ) console.log(f4('DEF')); // "DEFDEF" console.log(f3 === f4); // true これで、文字列として変換した形が同じ関数なら、アロー関数式を動的に書いたとしてもcf(関数)の形式で書いておけば、キャッシュされた値によって同値が返るので再レンダリングされない、みたいな。 でも、Reactの再レンダリングの仕組みよりも、functionのtoStringしてMapで値を持ってくるほうが遅いんじゃないだろうか、みたいな。検証してないからしらんけど。 動的に関数式を生成しないけど、即時でかけるような専用の構文がほしい。あるいは、React側が再レンダリングしないように対応してくれるようにならないかと思ったり。 以上です。
- 投稿日:2021-06-21T18:53:06+09:00
InDesign スクリプト 変数名と一致するテキストを変数に
変数名と一致するテキストを変数にするスクリプトは、これで良いのかな・・・? /* 更新 2021/6/22 */ // アプリ指定 #target "indesign"; // スクリプト名 var scriptName = "変数名と一致するテキストを変数に"; //スクリプト動作指定(一つのアンドゥ履歴にする、及び、アンドゥ名の指定) app.doScript(function () { // ダイアログ var dialogueFlg = confirm("カスタムテキスト変数の名前と一致するテキストを変数に置換します。" + "\r\r" + "選択がない場合はアクティブドキュメント全体で置換、選択がある場合は選択内で置換します。 ", "", scriptName); // Noの場合 if (dialogueFlg == false) { // スクリプトを終了 exit(); } // 置換数 var replaceNumber = 0; // 検索の初期化 app.findTextPreferences = NothingEnum.nothing; // テキスト変数の数だけ繰り返す for(var i = 0; i < app.activeDocument.textVariables.count(); i++){ // カスタムテキスト変数の場合 if(app.activeDocument.textVariables.item(i).variableType == VariableTypes.CUSTOM_TEXT_TYPE){ // 検索テキストに変数名を入れる app.findTextPreferences.findWhat = app.activeDocument.textVariables.item(i).name; // 選択がない場合 if(app.activeDocument.selection.length == 0){ // テキスト変数挿入 replaceNumber += insertTextVariableInstanceByName(app.activeDocument,app.activeDocument.textVariables.item(i)); // 以外の場合 }else{ // 選択の数だけ繰り返す for (var ii = 0; ii < app.activeDocument.selection.length; ii++){ // オブジェクトのメソッドにfindTextがある場合 if (typeof(app.activeDocument.selection[ii].findText) == "function"){ // テキスト変数挿入 replaceNumber += insertTextVariableInstanceByName(app.activeDocument.selection[ii],app.activeDocument.textVariables.item(i)); } } } } } // 検索の初期化 app.findTextPreferences = NothingEnum.nothing; // 結果の文 var resultText; // 選択がない場合 if(app.activeDocument.selection.length == 0){ resultText = "アクティブドキュメント全体で置換をおこないました。"; // 以外の場合 }else{ resultText = "選択内で置換をおこないました。"; } // 結果を表示 alert(resultText + "\r\r" + "置換数 " + replaceNumber, scriptName); //スクリプト動作指定(一つのアンドゥ履歴にする及びアンドゥ名の指定)の続き }, ScriptLanguage.JAVASCRIPT, [scriptName], UndoModes.ENTIRE_SCRIPT, scriptName); /* テキスト変数挿入関数、引数(検索対象、テキスト変数)の宣言 */ function insertTextVariableInstanceByName(searchTarget,targetTextVariable){ // 検索結果の配列 var searchResultArray = searchTarget.findText(); // 検索結果の範囲 var searchResultRange; // 置換数 var replaceNumber = 0; // 逆順に検索結果の数だけ繰り返す for(i = searchResultArray.length - 1; i >= 0; i--){ // 検索結果の範囲を入れる searchResultRange = searchResultArray[i].characters.itemByRange(0,-1); // テキスト変数を挿入 searchResultArray[i].insertionPoints.lastItem().textVariableInstances.add().associatedTextVariable = targetTextVariable; // 検索結果を削除 searchResultRange.remove(); replaceNumber++; } // 置換数を戻す return replaceNumber; }
- 投稿日:2021-06-21T16:58:54+09:00
SAP Cloud SDK for JavaScriptを使ってみる (2)サービスの作成
はじめに この記事は、SAP Cloud SDK for JavaScriptを使ってみるシリーズの2回目です。この記事では、SAP Cloud SDK を使ってサービスを開発する手順について説明します。 ベースとするのは以下のチュートリアルです。 Create an App Using SAP Cloud SDK for JavaScript Build an Address Manager with the SAP Cloud SDK's OData Virtual Data Model チュートリアルでは、S/4HANAのOData APIであるBusiness Partnerを使い、SAP Cloud SDKのOData Clientの使い方を確認します。OData Clientの使い方自体はチュートリアルで詳しく説明されていますので、そちらを参考にしてください。 この記事では、NestJSのCLIを使って必要なファイルを作る方法について説明します。 事前準備 テストで使えるS/4HANAの環境がない場合、接続先としては以下のオプションがあります。 ①API Business Hub ②モックのBusiness Partner Serviceをローカルに作成 今回は、テストを行う都合上、結果が一定になる②の方法を選択します。 モックのBusiness Partner Serviceの作り方については、以下に書いてあります。 https://sap.github.io/cloud-s4-sdk-book/pages/mock-odata.html 簡単に説明すると、 GitHubのブランチmock-serverをチェックアウト git clone https://github.com/SAP/cloud-s4-sdk-book cd cloud-s4-sdk-book git checkout mock-server モックサーバーを起動 npm i npm start http://localhost:3000/sap/opu/odata/sap/API_BUSINESS_PARTNER/A_BusinessPartnerにアクセスすると以下のようにデータが表示されます。 ステップ プロジェクトを生成 Module、Controller、Serviceを生成 Business Partner取得のロジックを実装 Cloud Foundryにデプロイ 1. プロジェクトを生成 前提:npm install -g @sap-cloud-sdk/cliでSAP Cloud SDK CLIがインストールされていること 1.1. プロジェクトを生成 sap-cloud-sdk init my-sdk-project 一つ目の質問(nest.jsのプロジェクトをターゲットのディレクトリで初期化するか)にはyを選択します。 二つ目の質問(usage analyticsを提供するか)にはyまたはnを選択します。 The target directory (my-sdk-project) does not contain a `package.json.` Should a new `nest.js` project be initialized in the target directory? (y|n): y Building application scaffold... done Enter project name (for use in manifest.yml) [my-sdk-project]: Do you want to provide anonymous usage analytics to help us improve the SDK? (y|n): y 最終的に以下の画面が表示されます。 +--------------------------------------------------------------+ ✅ Init finished successfully. ? Next steps: - Run the application locally (`npm run start:dev`) - Deploy your application (`npm run deploy`) ? Consider setting up Jenkins to continuously build your app. Use `sap-cloud-sdk add-cx-server` to create the setup script. +--------------------------------------------------------------+ 1.2. src/main.jsを変更 デフォルトでは、生成されたアプリケーションはポート3000で動くようになっていますが、モックサーバーをポート3000で実行している都合で、ポートを8080に変更します。 import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); await app.listen(process.env.PORT || 3000); //->8080に変更 } bootstrap(); 1.3. 実行してみる ターミナルにnpm run start:devと入力し、ブラウザでhttp://localhost:8080/を開きます。Hello Worldの画面が表示されればOKです。 2. Module、Controller、Serviceを生成 続いて、Module、Controller、Serviceを生成します。Module、Controller、Serviceの関係については前回の記事をご参照ください。 2.1. Moduleを生成 以下のコマンドでModuleを生成します。 nest generate module business-partner srcフォルダの配下にbusiness-partnerというフォルダとモジュールができました。 app.module.tsのimportsにBusinessPartnerModuleが追加されます。 src/app.module.ts import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { BusinessPartnerModule } from './business-partner/business-partner.module'; @Module({ imports: [BusinessPartnerModule], //自動で追加される controllers: [AppController], providers: [AppService], }) export class AppModule {} 2.2. Controllerを生成 以下のコマンドでControllerを生成します。 nest generate controller business-partner business-partnerフォルダ配下にcontrollerとつくファイルが2つ追加されました。このうち、spec.tsはユニットテストで使うためのファイルです。 business-partner.module.tsにControllerが自動で追加されました。 src/business-partner/business-partner.module.ts import { Module } from '@nestjs/common'; import { BusinessPartnerController } from './business-partner.controller'; @Module({ controllers: [BusinessPartnerController] }) export class BusinessPartnerModule {} 2.3. Serviceを生成 以下のコマンドでServiceを生成します。 nest generate service business-partner serviceとつくファイルが2つ追加されました。 business-partner.module.tsにServiceが自動で追加されました。 src/business-partner/business-partner.module.ts import { Module } from '@nestjs/common'; import { BusinessPartnerController } from './business-partner.controller'; import { BusinessPartnerService } from './business-partner.service'; @Module({ controllers: [BusinessPartnerController], providers: [BusinessPartnerService] }) export class BusinessPartnerModule {} 3. Business Partner取得のロジックを実装 SAP Cloud SDKのOData Clientを使い、Business Partnerを取得します。 3.1. @sap/cloud-sdk-vdm-business-partner-serviceをインストール 以下のコマンドでBusiness PartnerのODataサービスにアクセスするためのライブラリをインストールします。 npm i @sap/cloud-sdk-vdm-business-partner-service 3.2. Serviceの実装 Serviceに以下のロジックを実装します。 src/business-partner/business-partner.service.ts import { Injectable } from '@nestjs/common'; import { BusinessPartner } from '@sap/cloud-sdk-vdm-business-partner-service' @Injectable() export class BusinessPartnerService { getAllBusinessPartners(): Promise<BusinessPartner[]> { return BusinessPartner.requestBuilder() .getAll() .execute({ destinationName: 'MockServer' }) } } ここでは以下のことを行っています。 @sap/cloud-sdk-vdm-business-partner-serviceからBusinessPartnerをインポート(2行目) getAllBusinessPartnersメソッドでBusiness Partnerを取得 GetAll Request Builderで全てのエンティティを取得するリクエストを作成 executeでHTTPリクエストを実行 executeについて executeメソッドにはDestinationオブジェクト、またはDestinationNameAndJwtオブジェクトを渡します。上の例では、BTPのDestinationを使用するためDestinationNameAndJwtを使用しています。URLを直接指定する場合はDestinationオブジェクトを使い以下のようにURLを設定します。 .execute({ url: 'http://localhost:3000' }) ローカル実行のための設定 ローカル実行時にDestinationが取得できるように、以下の設定を行います。 ①.envファイルを作成 プロジェクトのルートに.envファイルを作成し、Destinationの設定をします。 .env destinations=[{"name": "MockServer", "url": "http://localhost:3000"}] ②@nestjs/configをインストール npm i @nestjs/config ③ConfigModuleをapp.mocule.tsに追加 .envファイルが環境変数として使用されるように、@nestjs/configのConfigModuleをapp.mocule.tsに追加します。 import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; //追加 import { AppController } from './app.controller'; import { AppService } from './app.service'; import { BusinessPartnerModule } from './business-partner/business-partner.module'; @Module({ imports: [ConfigModule.forRoot(), BusinessPartnerModule], //追加 3.3. Controllerの実装 Controllerに以下のロジックを実装します。 src/business-partner/business-partner.controller.ts import { Controller, Get } from '@nestjs/common'; import { BusinessPartnerService } from './business-partner.service'; import { BusinessPartner } from '@sap/cloud-sdk-vdm-business-partner-service' @Controller('business-partner') export class BusinessPartnerController { constructor(private readonly businessPartnerService: BusinessPartnerService) {} @Get() getAllBusinessPartners(): Promise<BusinessPartner[]> { return this.businessPartnerService.getAllBusinessPartners(); } } ここでは以下のことを行っています。 @nestjs/commonからControllerおよびGetをインポート @sap/cloud-sdk-vdm-business-partner-serviceからBusinessPartnerをインポート BusinessPartnerControllerクラスのコンストラクタでBusinessPartnerServiceを使用することを宣言(※) getAllBusinessPartnersメソッドでBusinessPartnerServiceのgetAllBusinessPartnersメソッドを呼ぶ ※これにより、実行時にNestJSが自動的にBusinessPartnerServiceをインスタンス化して挿入してくれる。このことをDependency Injectionという。この仕組みのおかげで、Unit Testで依存先のモジュールをテスト用のモジュールに簡単に置き換えることができる。 Controller、Getについて これらはいずれもstring型のパスを引数に取ることができます。 Controllerで指定したパスがリクエストのベースパスに一致する場合、そのControllerで処理が行われます。同様に、Getで指定したパスがベースパスの次のパスに一致する場合、そのGetの下のメソッドで処理が行われます。 以下の例では、/business-partners/getAllというリクエストがgetAllBusinessPartnersメソッドで処理されます。 @Controller('business-partner') export class BusinessPartnerController { constructor(private readonly businessPartnerService: BusinessPartnerService) {} @Get('getAll') getAllBusinessPartners(): Promise<BusinessPartner[]> { return this.businessPartnerService.getAllBusinessPartners(); } } 3.4. 実行してみる ブラウザにhttp://localhost:8080/business-partnerと入力します。 以下の画面が表示されればOKです。 3.4. 追加のクエリオプションの指定 OData Clientでは、select, filter, top skipなどのクエリオプションを指定することも可能です。 以下のチュートリアル、およびドキュメントを参照して、クエリオプションを試してみてください。 Build OData Queries with the SAP Cloud SDK's Virtual Data Model (Step8~) Use the OData v2 Client 4. Cloud Foundryにデプロイ 4.1. Destinationを使用するための設定 事前準備で作成したモックのBusiness Partner ServiceをCloud Foundryにデプロイし、Destinationとして登録します。デプロイ、およびDestinationの登録手順については以下のチュートリアルのStep2~4をご参照ください。 Deploy Application to Cloud Foundry with SAP Cloud SDK for JavaScript 4.2. Cloud Foundryにデプロイ Cloud Foundryにデプロイするためには、package.jsonに定義されたdeployスクリプトを実行します。 npm run deploy deployスクリプトは以下のように3つのコマンドから成っています。 "deploy": "npm run build && sap-cloud-sdk package && cf push", 各コマンドは次のことを行います。 npm run build distフォルダにビルドした結果のファイルを格納する。 sap-cloud-sdk package deploymentフォルダにdistフォルダ、およびpackage.json、package-lock.jsonをコピーする。 cf push manifest.ymlの内容に従い、アプリケーションをCloud Foundryにデプロイする。 manifest.ymlでは、deployment配下のものをデプロイする設定になっている。 manifest.yml applications: - name: my-sdk-project path: deployment/ buildpacks: - nodejs_buildpack memory: 256M command: npm run start:prod random-route: true services: - my-destination - my-xsuaa
- 投稿日:2021-06-21T15:59:40+09:00
Engress(作成中)
事前準備 コーディング手順 対応ブラウザ、パスの指定方法、納品形式、フォルダの構造、アニメーションの実装などの仕様を確認。 デザインを見て、頭の中でコードを組み立てる。この際、「クラス名の命名」「共通要素の洗い出し」「実際のコーディングで直面しそうな問題の把握(実装の仕方がわからないなど)」を明確にする。 画像の書き出し。画像ファイルの命名も「ttl_」「bg_」「ico_」「btn_」「img_」「nav_」などの識別子をつけ、画像がどのような意味を持つのか考えながら命名。 「ページ名/ブロック名」_「識別子」_「連番」の順で命名する。 また、背景透過、画質などを考慮し「png-8」「png-24」「jpg」「gif」などを適切な拡張子を選択する。 加えて、高解像度ディスプレイ対応のため2倍の大きさで書き出し。 さらに、ファイルサイズを考慮し、適切に圧縮をかける。 このテンプレートを使用し、まずは大枠、共通部分のコーディング、それから細部のコーディングを進める。 コーディング完了後はW3Cのバリデーションツールで文法のエラーをチェック。 各種チェック項目 修正の対応/完了 a.altが正しく入ってるか b.誤字がないか c.タイポグラフィーがあっているか d.色があっているか e.マークアップの構造が適切か f.どの画面幅でも崩れていないか g.アニメーションの挙動は適切か h.デザインとのずれがないかパーフェクトピクセルで確認 i.IE、Edge、Safari、FireFox、Chrome各ブラウザで表示確認 j.リンクは適切に入ってるか k.横スクロースしないか l.画像が重すぎないか コーディング規則 HTML、SCSS共に、セクションごとの開始と終わりにコメントで区切りをわかりやすくする。 SCSSのプロパティ順はposition, padding, margin, top,bottomなど「位置、余白」に関わるものを一番上に書き、あとはアルファベット順に整理する。 命名規則は基本はMindBEMding、共通要素が多く、下層ページの多いサイトにはモジュールコーディングを採用(接頭辞つけるやつ)。 パスは指定がなければ基本相対パス(ドキュメントパス)を使用。 文章構造はHTML、装飾はCSSに分離(ただしマテリアルデザインなどの場合は例外的にHTMLで装飾を施すこともやむを得ない)。 どの画面幅でも表示が崩れないようなリキッドデザインを極力使用。(320px~1900pxの画面幅で確認) ブラウザは「safari」「firefox」「IE(x)」「chrome」「iPhone」「アンドロイド」で確認*IEに関してはWindowsの実機で確認 ブレイクポイントは基本的にはBootstrapのものに合わせて「スマホ 768px」で「タブレット 992px」(デザインによっては例外あり) 位置や色ではなく、役割や意味を表す識別名をつけることを命名ルールにする。また、その単語からパーツを推測できるようにする。加えて、長くなりすぎないように意味が分かる範囲で単語を短くする。 padding,margin,backgroundのように複数の値を一度に指定できるプロパティに対して、基本的に2つ以上に個別指定が必要な場合は、ショートハンドプロパティを使って一括して指定する。 命名の際に数字を用いる際は「01」「02」のように2桁の連番で記述する。 基本的にはBlockで左右上下の余白を指定し、残りの要素には下に対して余白をつけていく Local By Flywheel CSS設計法 ディレクトリ構造 CSS reset.css style.css style.css.map images js swiper swiper.min.css swiper.min.js jquery-3.5.1.min.js script.js parts breadcrumb.php cta.php fnav.php gnav.php lowerLayerMv.php tel.php scss base _base.scss _reset.scss mixin _animation.scss _btn.scss _etc.scss _font.scss _keyflame.scss _mixin.scss _over.scss _utility.scss module _content.scss _etc.scss _fnav.scss _gnav.scss _main.scss _share.scss _snav.scss page common _footer.scss _header.scss _sidebar.scss top _article.scss _cta.scss _flow.scss _fv.scss _problem.scss _qa.scss _strength.scss _success.scss _tel.scss _archive-archive _archive-news.scss _page.scss _page-contact.scss _page-price.scss _single.scss _single-archive.scss _single-news.scss plugins _slick.scss slick.css.map setting _block.scss _color.scss _config.scss _function.scss _size.scss _typography style.scss templates 404.php archive.php archive-archive.php archive-news.php category.php footer.php front-page.php functions.php header.php index.php page.php page-contact.php page-price.php sidebar.php single.php single-archive.php single-news.php screenshot.png style.css slick.css.map style.css.map style.scss // ========================================================================== // setting // ========================================================================== @import "./setting/config"; @import "./setting/function"; @import "./setting/block"; @import "./setting/color"; @import "./setting/size"; @import "./setting/typography"; // ========================================================================== // mixin // ========================================================================== @import "./mixin/animation"; @import "./mixin/btn"; @import "./mixin/etc"; @import "./mixin/font"; @import "./mixin/_keyframe"; @import "./mixin/_mixin"; @import "./mixin/over"; @import "./mixin/utility"; // ========================================================================== // base // ========================================================================== @import "./base/reset"; @import "./base/base"; // ========================================================================== // module // ========================================================================== @import "./module/_content"; @import "./module/_etc"; @import "./module/_fnav"; @import "./module/_gnav"; @import "./module/_main"; @import "./module/_share"; @import "./module/_snav"; // ========================================================================== // page // ========================================================================== @import "./page/common/header"; @import "./page/common/sidebar"; @import "./page/common/footer"; @import "./page/top/fv"; @import "./page/top/problem"; @import "./page/top/strength"; @import "./page/top/success"; @import "./page/top/flow"; @import "./page/top/qa"; @import "./page/top/article"; @import "./page/top/cta"; @import "./page/top/tel"; @import "./page/page"; @import "./page/page-price"; @import "./page/page-contact"; @import "./page/archive-archive"; @import "./page/archive-news"; @import "./page/single"; @import "./page/single-archive"; @import "./page/single-news"; // ========================================================================== // plugins // ========================================================================== @import "./plugins/slick"; 吉本式BEM設計法 mixin PHP 動的部分マーキング Plugin Advanced Custom Fields Breadcrumb NavXT Custom Post Type UI MW WP Form WP Social Bookmarking Light All-in-One WP Migration Show Current Template WordPress インポートツール 難しかった部分 related-post reccomend-post custom-field custom-post-type pagination SNS-button Breadcrumb contact-form scroll-hint よく使うテンプレートタグ <?php echo get_template_part(''); ?> <?php echo get_template_part('parts/lowerLayerMv'); ?> <?php echo get_template_part('parts/breadcrumb'); ?> <?php echo get_template_part('parts/cta'); ?> <?php echo get_template_part('parts/tel'); ?> <?php get_template_directory_uri(); ?> <?php echo esc_url(home_url('/')); ?> 不明点 あ
- 投稿日:2021-06-21T14:18:56+09:00
Vroid→blender→Mixamo→PlayCanvasでオリジナル3Dキャラを操作できるようにする!
オリジナルの3DキャラクターをPlayCanvas上で動かしたい!と思っても中々ハードルが高いかと思います。そこで、VroidとMixamo、Blenderを使い、無料且つ専門知識や技術を使わずオリジナルキャラを走り回らせてみようと思います。 使用するコンテンツは以下のモノです。 Vroid:キャラクターを作成します。 Blender:キャラクターの調整をします。 Mixamo:キャラクターにアニメーションを付けます。 PlayCanvas:キャラクターのアニメーション制御、ビルドを行います。 Vroidでキャラクターメイキング キャラクターを作成していくには、通常、BlenderやMAYA、3dsMAXなどの3D作成ソフトからポリゴンモデリング、テクスチャ作成、リギング等の作成を行う必要があり、かなりハードルが高いです。 そこで、キャラクターをカスタマイズし、VRMでエクスポート出来る"Vroid"を使用します。 Vroidをインストール 下記URLからDLし、それぞれの環境に合ったものをインストールしてください。 https://vroid.com/studio 起動するとキャラクターの選択画面がでます。今回はサンプルのキャラクターを任意に選んでください。 キャラクターを選択し、右上部の"撮影・エクスポート"ボタンをクリック キャラクターをエクスポート キャラクターのマテリアル数を最小にし、テクスチャーサイズも2048×2048へ変更します。 VRMデータとしてエクスポートします。 左側の"エクスポート"を選択後、右側の"エクスポート"をクリック VRM設定のモーダルがでますので、タイトルと作者を記入したください。OKをクリックし、任意の場所に保存してください。 Blenderをインストール 下記URLからBlender LTS版をDLし、インストールしてください。最新のBlenderでは対応していないアドオンが複数ありますので、今回はBlender 2.83 LTSで作業していきます。 https://www.blender.org/download/lts/ BlenderへVRMを読み込むアドオンを入れる VRMデータを直接インポートすることはできないので、"cats-blender-plugin"というアドオンを入れます。 https://github.com/GiveMeAllYourCats/cats-blender-plugin ダウンロードした"cats-blender-plugin-master.zip"をBlenderのプリファレンスで設定します。 Catを使用しVRMデータを調整する まずはVRMモデルをインポートします。 3Dモデルの修正 モデ修正ボタンをクリックし、最適化しておきます。 この際、アイトラッキングやビムス(口の動き)などの指定もできますが、今回は特におこないません。 続いて、シェイプキーを削除します。シェイプキーが存在していると、モデルとボーンのリンクを切ることができないので、ここで切れる状態にしておきます。 モディファイヤーリストからArmachereを"適用"しスタックから削除ます。 足のボーンを選択し、回転で角度を調整。もしくはボーンリストから数値で調整します。 だいたいZ:0.03 -0.03で丁度良い開きになります。この処理をしておかないと、Mixamoで見たときに足のポリゴンウェイトが正確に反映されずに、くっついた状態で処理されてしまいます。 VRMデータをfbxでエクスポート オブジェクト"body"を選択します。 ファイル→エクスポート→FBXをクリック パスモードをコピーに変更し、その隣のマークをアクティブにします。 Limit toの"選択したオブジェクト"にチェック。ファイル名は任意に決めてください。今回は"test"という名前にしています。 "FBXをエクスポート"ボタンをクリック。任意の場所に保存してください。 MixamoでアニメーションをDLしよう Mixamoにログイン、もしくはサインアップしてください。 https://www.mixamo.com/#/ キャラクター.fbxをインポート&設定 "UPLOAD CHARACTER"をクリックします。 fbxデータをdrag&dropもしくは"Select character file"からフォルダ指定してください。 キャラクターのセットアップ画面になります。モデルに問題が無ければ"NETX"をクリックしてください。 顎、手足の間接をマークして設定していきます。 右側のガイド画像を参考に大まかに位置をあわせるだけでOKです。セットできたら"NEXT"をクリックします。 オートリギングが完了するとマネキンがインポートしたキャラクターへ変わります。 検索窓に"Locomotion"を入力し、"Male Locomotion Pack"を選択し、右側のDLボタンをクリックします。ローカルフォルダの任意の場所に保存します。オリジナルのFBXがある場所がベストです。 今回使用するアニメーションは"Male Locomotion Pack"内にある以下の4つです。 test.fbx(Tポーズのオリジナルデータ) idle.fbx jump.fbx walking.fbx standard run.fbx Blenderでアニメーションを調整 歩く、走るのアニメーションはその場で歩く、走るアニメーションにする必要があります。MixamoでDLしたアニメーションはZ軸方向に移動のアニメーションが入っているので、これを削除します。 インポート時にアーマチェアの"ボーン方向の自動整列"にチェックをいれてください。 1.Animationタブをクリック 2.ポーズモードに変更 3.腰のボーンを選択 4.Z軸のキーフレームを全選択し、削除します。 ポーズモードをオブジェクトモードに変更し、ボーンオブジェクトを選択し、FBXでエクスポートします。エクスポート時に"NLAストリップ"と"全アクション"のチェックを外します。 PlayCanvasでアニメーションを制御しよう PlayCanvas上でDLしたアニメーションデータを展開し、マウスをクリックした場所にキャラクターが走りながら移動するようにしていきます。 PlayCanvasのアカウントをお持ちでない方は下記URLからアカウントを作成してください。 https://playcanvas.jp/ プロジェクトの作成 ログインするとマイページへ移動します。"NEW"ボタンをクリック プロジェクトの種類、プロジェクト名(任意の名前)を入力し、"CREATE"ボタンをクリック 作成された新規プロジェクトのENTERをクリックし、エディターを起動します。 中央のボックスは非表示にするか、削除してしまって問題ありません。 非表示にする場合、ビューポート上の非表示にしたいオブジェクトを選択し、右側インスペクター内の"ENTITY"の"Enabled"のチェックを外します。 .fbxのインポート 下部ASSETS欄に3Dモデル用の新規フォルダを作成し、その中にインポートします。 root(一番上の階層)を選択し、ASSETS横の"+"ボタンから"Folder"を選択します。 新しくフォルダが作成されました。フォルダ名を任意につけてください。フォルダを選択した状態で右側のインスペクターからリネームできます。 まずはオリジナルデータ"test.fbx"をアップロードします。ローカルフォルダから"ASSETS"欄の"3Dmodel"フォルダ内にdrag&dropするか、"+"ボタンから"Upload"でファイルを選択します。 続いて、"test.glb"をビューポートへdrag&dropします。 マテリアルの調整 ビューポート上に表示された"test.glb"は顔のテクスチャが崩れています。これはBlender上とPlayCanvas上でのアルファ処理の相違で発生する問題です。 PlayCanvasのインスペクターからマテリアル"OPACITY"の内容を変更して修正します。 Blend TypeをAlpha 空きスロットルに"texture_0.png"(顔部分のテクスチャ)をdrag&drop Colore Channelを"A" Alpha To Coverageにチェックを入れる これでモデルの表示は整いました。 CollisionとRigidBody キャラクターが歩く為のフィールドを作成します。 HIERARCHYの"Root"を選択した状態で、HIERARCHY横の"+"ボタンから"Primitive"→"Plane"を選択してください。 右側のインスペクターのスケール値を15/0/15くらいのサイズにして調整します。 続いて、キャラクターに、当たり判定"Collision"と剛性"rigitbody"を持たせます。 ADD COMPONENTから"Collision"と"RigidBody"を選択してください。 CollisionのTypeを"Box"へ変更し、Half Extentsを7.5/0/7.5くらいにして、Planeと同じ大きさにします。 RigidBodyの設定はデフォルトの状態でOKです。 キャラクターとカメラの位置調整 ビューポート右上にある"Perspective"をクリックすると、Cameraからの視点モードに切り替えられます。 斜め上から見下ろすような角度に任意で調整して、"Perspective"モードに戻ってください。 プロジェクトの複製"Fork"とスクリプトのコピー クリックした箇所にキャラクターが移動するスクリプトをコピーしてきます。 こちらのURLからプロジェクトを複製"Fork"します。 https://playcanvas.com/project/806425/overview/animcomponentlocomotion "Fork"ボタンをクリックしてください。 任意の名前を入力し、右下の"Fork"ボタンをクリック 複製されたプロジェクトの"EDITOR"をクリックし、スクリプトをコピーします。 "Animation.js"と"Locomotion.js"を選択し、コピーします。 続いて、オリジナルのプロジェクトへペーストします。 スクリプトをキャラクターに追加 キャラクターを選択し、ADD COMPONENTから"Script"をクリックし、インスペクター内に表示された"ADD SCRIPT"をクリック。プロジェクト内で使用できるスクリプト一覧が表示されますので、"Animation.js"と"Locomotion.js"を選択します。 実行してみる ビューポート右上の三角マーク"Launch"ボタンをクリックすると別ウィンドウで現在設定されているプロジェクトが実行されます。 マウスをクリックした箇所へキャラクターが歩く。 歩いている間に"X"をクリックしていると走る。 スペースボタンでジャンプです。 これでプロジェクトは完成です。続いて、公開用のURLを生成します。 publish&buildで公開用のURLを生成する 上部の"Manage Scenes"をクリックしてくださ。 最初にシーンを選択した画面が出てきます。右側の"PUBLISH"をクリックし、"PUBLISH TO PLAYCANVAS"をクリックします。 名称の変更やディスクリプション、バージョン情報等を記載する箇所があります。 一番下までスクロールすると"PUBLISH NOW"ボタンがありますので、クリックしてください。 赤枠内にあるURLが共有用のULRとなります! これで完成です。おつかれさまでした。 まとめ 3Dキャラクターを作成し、ゲーム上で動かすというのは一昔前ではかなりの作業量が必要でした。 今回使用したVroid、Mixamo、Blender、PlayCanvasは全て無料!で且つ、かなりクオリティの高いモノを短時間で作成することができます。
- 投稿日:2021-06-21T13:58:43+09:00
【javascript】addEventListener()の第1引数 よく使うイベントタイプ
よく使うイベントタイプをまとめました。 ビューイベント イベントタイプ 説明 resize ドキュメントビューのサイズが変更された時。 scroll 画面がスクロールされた時。 リソースイベント イベントタイプ 説明 error リソースを読み込めなかったとき。 abort リソース読み込みが中止されたとき。 load リソースの読み込みが完了したとき。 beforeunload アンロードされる前。 unload リソースがアンロードされたとき。 キーイベント イベントタイプ 説明 keydown キーを押されたとき keypress Shift,Fn,CapsLockを除くキーが押された状態にあるとき。 keyup キーを離したとき フォームイベント イベントタイプ 説明 reset リセットボタンが押された時。 submit サブミットボタンが押された時。 フォーカスイベント イベントタイプ 説明 focus 要素がフォーカスされているとき。 blur 要素がフォーカスを失ったとき。 マウスイベント イベントタイプ 説明 click 要素上でクリックしたとき発生。 dblclick 要素をダブルクリックしたとき発生。 mouseenter 要素にカースルが移動したとき発生。 mouseup マウスのボタンが要素上で離されたとき。 mousemove マウスが要素上を移動しているとき。 contextmenu マウスの右ボタンがクリックされたとき。 select テキストが選択されているとき。 ドラッグイベント イベントタイプ 説明 drag 要素もしくは選択文字列がドラッグされている間。(350 ミリ秒ごとに断続的に発火します) 参考
- 投稿日:2021-06-21T12:39:10+09:00
Javascriptでエレメントをフリードラッギング
概要 Javascriptでドラッグ&ドロップするライブラリは山ほどありますが、画面にフローティングウィンドウのようなエレメントを配置して自由にドラッグできるようにしたかっただけなので、簡単に自分で実装できないかと思って試したら意外に面倒だったという話です。 コード export function draggable(element) { element.style.position = 'absolute' // ドラッグ中のtransformの値を保持 const transformPos = {x: 0, y: 0} // ドラッグスタート時のマウスとエレメントのオフセット const dragStartOffset = {x: 0, y: 0} // ハンドルにイベント登録 const handles = element.querySelectorAll('.draggable-handle') for (var i = 0; i < handles.length; i++) { handles[i].addEventListener('mousedown', startDragging) handles[i].addEventListener('touchstart', startDragging) } function getClientPosition(event){ if(event.touches){ return {x: event.touches[0].clientX, y: event.touches[0].clientY} } else { return {x: event.clientX, y: event.clientY} } } function startDragging(e) { e.preventDefault() const clientPos = getClientPosition(e) dragStartOffset.x = clientPos.x - element.offsetLeft dragStartOffset.y = clientPos.y - element.offsetTop document.addEventListener('mouseup', endDragging) document.addEventListener('touchend', endDragging) document.addEventListener('mousemove', elementDrag) // [Intervention] Unable to preventDefault inside passive event listener due to target being treated as passive. // See https://www.chromestatus.com/features/5093566007214080 document.addEventListener('touchmove', elementDrag, { passive: false }) } function elementDrag(e) { e.preventDefault() const clientPos = getClientPosition(e) transformPos.x = clientPos.x - element.offsetLeft - dragStartOffset.x transformPos.y = clientPos.y - element.offsetTop - dragStartOffset.y element.style.transform = `translate(${transformPos.x}px,${transformPos.y}px)` } function endDragging() { document.removeEventListener('mouseup', endDragging) document.removeEventListener('touchend', endDragging) document.removeEventListener('mousemove', elementDrag) document.removeEventListener('touchmove', elementDrag) // ドラッグ終了時に場所とtransformをリセット element.style.top = (element.offsetTop + transformPos.y) + "px"; element.style.left = (element.offsetLeft + transformPos.x) + "px"; transformPos.x = 0 transformPos.y = 0 element.style.transform = `translate(${transformPos.x}px,${transformPos.y}px)` // fire event const event = new CustomEvent('draggable.drop') element.dispatchEvent(event) } } import { draggable } from '../functions/draggable' const elem = document.querySelector('#dialog') draggable(elem) <div id="dialog"> <div class="dragging-handle" style="top: 100px; left: 200px;">ダイアログ</div> <div>...</div> </div> 解説 こちらを参考にしましたがいくつか変更してます。 スマホでも動くようにtouchに対応。 ドラッグ中はCSSのtransformで場所を変更するようにした。 ストップ時にイベント発火。 戦略として面白いと思ったのはドラッグ中のみイベントを登録し、止まるとイベントを削除してしまうところですかね。コードはできるだけ最低限に留めておきましたが、absoluteをここでやるのは微妙かもしれないですね。繰り返し登録しないようにするとか工夫するのもありかも。 Chrome以外テストしてません。new CustomEventがたしかIEダメでしたよね。まあ何か気づいたら修正しておきます。 reactで使う おまけ。Reactのアプリ内で使ったので参考までに置いておきます。 import classNames from 'classnames' import React, { Component } from 'react' import { draggable } from '../functions/draggable' export class DraggableHandle extends Component { render () { return ( <div className={classNames('draggable-handle', this.props.className)}> {this.props.children} </div> ) } } export default class Draggable extends Component { constructor(props){ super(props) this.state = { left: this.props.left || 0, top: this.props.top || 0, } this.elemRef = React.createRef() } componentDidMount(){ draggable(this.elemRef.current) this.elemRef.current.addEventListener('draggable.drop', e => this.props.onDrop(e)) } componentDidUpdate(prevProps) { if (this.props.left !== prevProps.left || this.props.top !== prevProps.top) { this.setState({top: this.props.top, left: this.props.left}) } } render () { return ( <div ref={this.elemRef} style={{top: this.state.top, left: this.state.left}} className={this.props.className} > {this.props.children} </div> ) } } Draggable.defaultProps = { onDrop: event => {} } import Draggable, { DraggableHandle } from '../../../shares/components/Draggable' class App extends Component { render(){ return( <Draggable top={0} left={0} onDrop={data => console.log(data)}> <DraggableHandle>ダイアログ</DraggableHandle> <div>...</div> </Draggable> ) } }
- 投稿日:2021-06-21T11:48:27+09:00
javascriptでイベントで値を送る方法
メモ 取得用イベントを作る document.addEventListener("originalname", function(e) { console.log(e.detail); // Prints "Example of an event" }); こっちは送信用イベント var event = new CustomEvent("originalname", { "detail": "hello world" }); document.dispatchEvent(event);
- 投稿日:2021-06-21T10:54:30+09:00
【JavaScript】オブジェクトについて
はじめに JavaScriptの基礎を勉強したらとても良い学びになったのでメモ JavaScriptのオブジェクトについてです こちらの本を参考にさせていただきました 概要 オブジェクトとは: 名前をキーにアクセスできる配列 連想配列との違い 連想配列:複数要素の集合体(個々の要素が主体) オブジェクト:1つのモノを表現するために、複数の属性情報をもつ(モノ全体が主体) オブジェクト=プロパティ+メソッド プロパティ:オブジェクトの状態や特性を表すための情報 メソッド:オブジェクトを操作するための道具 new演算子(インスタンス) オブジェクトは自身でデータを保持できる 競合を避けるためオブジェクトのコピーを作成して操作する →インスタンス化 インスタンス生成方法 var 変数名 = new オブジェクト名([引数,…]) プロパティ/メソッド呼び出し方法 変数名.プロパティ名 変数名.メソッド名 例外)静的プロパティ/メソッド 組み込みオブジェクト …JavaScriptが動作するすべての環境で利用可能なオブジェクト ex) Object:すべてのオブジェクトのひな型となる機能を提供 Array:配列を操作するための手段を提供 Map:キー/値からなる連想配列を操作するための手段を提供 String:文字列を操作するための手段を提供 Function:関数を操作するための手段を提供 →リテラルをそのまま対応する組み込みオブジェクトとして利用できるため、インスタンス化をほとんど意識する必要がない Objectオブジェクト ...全てのオブジェクトの基本オブジェクト ex) toStringメソッド:そえぞれのオブジェクトの内容を基本型の値に変換する assignメソッド:既存のオブジェクトを結合できる createメソッド:オブジェクトを生成する Globalオブジェクト ...グローバル変数やグローバル関数を管理するため、JavaScriptが自動的に生成する便宜的なオブジェクト ex) Numberオブジェクト:数値にかかわる機能 isFinite, isNaN, parseFloat, parseInt encodeURI/encodeURIComponent関数:クエリ情報をエスケープ処理する URLの末尾「~?」以降に「キー名=値 &…」の形式 →クエリ情報(=サーバー上で動くアプリへの問い合わせに必要なデータ) URIエンコード(=クエリ情報は渡せる情報に制限があるため、無害な文字列に変換する) eval関数:与えられた文字列をJavaScriptのコードとして評価・実行する 第三者が任意にスクリプトを実行できてしまうセキュリティリスク・処理速度が遅いという理由からJSON.parseで代用 おわりに 今までなんとなく使っていたものの仕組みとかを知ると、伏線回収みたいになってとても面白いですね オブジェクトという概念 関数をfunction()で定義していたのも実は、組み込みオブジェクトを利用していた という2点が特に勉強になりました 他のことも学んだらQiitaでアウトプットしていきたいです
- 投稿日:2021-06-21T09:30:18+09:00
Reactアプリケーションを高速化するための方法
Reactアプリケーションを高速化するための方法 結論から 無駄な再レンダリングを発生させない 処理しないこと 高速化とは処理しないことです 矛盾しているように思えますが、処理が減ればアプリケーションは高速化します Reactの処理を減らすためには 再レンダリングをしないことです もちろん完全に再レンダリング無しというのは不可能なので、いかにそれを減らしていくかが重要です 再レンダリングの妥当性 妥当な再レンダリング コンポーネントのstateに変化があって自身の挙動を変更しなければならないとしたら、その再レンダリングは妥当です 妥当ではない再レンダリング コンポーネント自身がそのstateを必要としておらず、下位に配るためstateを保持しているとしたら、その際レンダリングは妥当ではありません そういう構造を作ってしまえば、配下にいる全コンポーネントがstateと無関係に再レンダリングされます。 何故、こんな構造がまかり通ってしまうのか useStateが三つの機能を一つに集約してしまったのが原因です。詳しくは[React]useStateによるstate管理の問題点とその解決方法を模索した結果、3つの機能を分離するに至った話を参照してください。 解決方法 上位コンポーネントで下位に配るためのstateを保持しない 無駄なstateの保持、これが諸悪の根源です とにかく自身で使用しないstateを保持するのをやめる必要があります 上位コンポーネントでstateを保持しないためには state管理ライブラリを使用する ReduxやContextAPIを使用すれば、親コンポーネントを経由せずともstateを配ることが出来ます ただし、以下の欠点があります stateのスコープがグローバルになってしまい、特定のコンポーネントグループ内で閉じさせるのが困難 仕組みを作成するコストが大きく、気軽に利用しにくい state管理をコンポーネントグループでローカル化する 特定コンポーネント間で閉じられたstate管理が出来て、簡単に利用出来れば問題は解決です ということで作りました stateの作成、更新、取得を分離させたライブラリ @react-libraries/use-local-state コンポーネント間でイベント通知を行うライブラリ @react-libraries/use-local-event 両方とも、stateやeventを扱うための識別ハンドルを上位コンポーネントで作成し、それを下位コンポーネントで使用する仕組みです ハンドルを作成した上位コンポーネントはstateの更新やevent通知の影響を受けないので、再レンダリングはされません state管理のグループローカル化の具体例 Sample1 特に対策を打たない、通常の書き方 Inputコンポーネントにボタンが配置されており、押すごとにstateが+1されます Test1とTest2はstateを受け取って、値を1万回表示します Test3はstateとは無関係にTest3を1万回表示します import React, { useState } from 'react'; const count = 10000; const Test1 = ({ value }: { value: number }) => ( <div> <div>Test1</div> {new Array(count).fill(0).map((_, index) => ( <div key={index}>{value}</div> ))} </div> ); const Test2 = ({ value }: { value: number }) => ( <div> <div>Test2</div> {new Array(count).fill(0).map((_, index) => ( <div key={index}>{value}</div> ))} </div> ); const Test3 = () => ( <div> <div>Test3</div> {new Array(count).fill(0).map((_, index) => ( <div key={index}>Test3</div> ))} </div> ); const Input = ({ setValue }: { setValue: React.Dispatch<React.SetStateAction<number>> }) => ( <button onClick={() => setValue((v) => v + 1)}>+1</button> ); const App = () => { const [value, setValue] = useState(0); return ( <> <div>通常</div> <Input setValue={setValue} /> <hr /> <div style={{ display: 'flex' }}> <Test1 value={value} /> <Test2 value={value} /> <Test3 /> </div> </> ); }; export default App; Sample2 memo化 Sample1をmemo化したものです import React, { memo, useState } from 'react'; const count = 10000; const Test1 = memo(({ value }: { value: number }) => ( <div> <div>Test1</div> {new Array(count).fill(0).map((_, index) => ( <div key={index}>{value}</div> ))} </div> )); const Test2 = memo(({ value }: { value: number }) => ( <div> <div>Test2</div> {new Array(count).fill(0).map((_, index) => ( <div key={index}>{value}</div> ))} </div> )); const Test3 = memo(() => ( <div> <div>Test3</div> {new Array(count).fill(0).map((_, index) => ( <div key={index}>Test3</div> ))} </div> )); const Input = memo(({ setValue }: { setValue: React.Dispatch<React.SetStateAction<number>> }) => ( <button onClick={() => setValue((v) => v + 1)}>+1</button> )); const App = () => { const [value, setValue] = useState(0); return ( <> <div>memo化</div> <Input setValue={setValue} /> <hr /> <div style={{ display: 'flex' }}> <Test1 value={value} /> <Test2 value={value} /> <Test3 /> </div> </> ); }; export default App; Sample3 グループローカル化 useLocalStateによって、上位コンポーネントの再レンダリングを行わない書き方になっています import { LocalState, mutateLocalState, useLocalState, useLocalStateCreate, } from '@react-libraries/use-local-state'; import React from 'react'; const count = 10000; const Test1 = ({ state }: { state: LocalState<number> }) => { const [value] = useLocalState(state); return ( <div> <div>Test1</div> {new Array(count).fill(0).map((_, index) => ( <div key={index}>{value}</div> ))} </div> ); }; const Test2 = ({ state }: { state: LocalState<number> }) => { const [value] = useLocalState(state); return ( <div> <div>Test2</div> {new Array(count).fill(0).map((_, index) => ( <div key={index}>{value}</div> ))} </div> ); }; const Test3 = () => ( <div> <div>Test3</div> {new Array(count).fill(0).map((_, index) => ( <div key={index}>Test3</div> ))} </div> ); const Input = ({ state }: { state: LocalState<number> }) => ( <button onClick={() => mutateLocalState(state, (v) => v + 1)}>+1</button> ); const App = () => { const state = useLocalStateCreate(0); return ( <> <div>localState</div> <Input state={state} /> <hr /> <div style={{ display: 'flex' }}> <Test1 state={state} /> <Test2 state={state} /> <Test3 /> </div> </> ); }; export default App; 結果の検証 ボタンを押してstateを変化させたときのprofile結果です Render durationがレンダリング時間です Sample1の結果 レンダリング時間 228.2ms 特に何も対応していないので、全てのコンポーネントが再レンダリングされています 当然、一番遅いです Sample2の結果 レンダリング時間 162.2ms memo化によってTest3の更新はスキップされていますが、Appは再レンダリングされています 何もしないよりは速度が向上していますが、まだ無駄が残っています Sample3の結果 レンダリング時間 95.2ms 本当に更新が必要なコンポーネントのみ再レンダリングされています 処理を行う内容は必要最低限です まとめ Reactアプリケーションを高速化させるためには、更新の必要が無いコンポーネントを再レンダリングしないことです。そのためにはとにかく上位コンポーネントに余計なstateを持たせないというのが基本です 今回は効率的なstate管理のために、stateをグループで閉じるためのライブラリを使用しました。その他にstateを使わずにevventのみ配るライブラリもあるので、次回はそちらも紹介したいと思います
- 投稿日:2021-06-21T08:45:28+09:00
GASで簡単に作るDiscord Bot
GAS(Google Apps Script)を使うと、簡単にDiscordのボットが作成できたので作り方を紹介します。 Discordで開発者モードを有効にする Discord左下の歯車を右クリック > 詳細設定 を選択します。 開発者モードを有効にします DiscordのWebHookURLを作成する 左サイドバーのボットを導入したいサーバのアイコンを右クリック > サーバー設定 > 連携サービス を選択します。 連携サービスメニュー内にあるウェブフックを作成を押し、ウェブフック設定画面に移動します。 ウェブフック設定画面ではボットの名前やチャットの投稿先となるチャンネルを設定できます。 名前やチャンネルを変更した際は変更の保存を忘れずに行ってください。 ボットのテスト用に#bot-sandboxのようなプライベートチャンネルを作ってもいいかもしれません。 ボットの設定が完了したら、ウェブフックURLをコピーを押してください。 このコピーしたウェブフックURLはGAS作成時に使います。 以上でDiscordの設定は終わりです。 Discordにチャットを投稿するGASを作る チャットを投稿する方法はシンプルで、投稿するチャット内容と設定をまとめたオブジェクトを作成して、Jsonに整形します。 そして、先ほど作成したウェブフックURLに対してPostリクエストを投げます。 function myFunction() { // discord側で作成したボットのウェブフックURL const discordWebHookURL = "https://discord.com/api/webhooks/000000/XXXXX"; // 投稿するチャット内容と設定 const message = { "content": "Hello!", // チャット本文 "tts": false // ロボットによる読み上げ機能を無効化 } const param = { "method": "POST", "headers": { 'Content-type': "application/json" }, "payload": JSON.stringify(message) } UrlFetchApp.fetch(discordWebHookURL, param); } GASエディタ上部にある「実行」ボタンを押すことで、チャットがDiscordに投稿されます。 ちなみにウェブフックURLのような他人に知られたくないものは、プロパティ機能を利用して隠蔽するといいと思います。 プロパティ機能設定方法(GASエディタが新しくなってGUIで設定できなくなっています...) メンション 文中に<@[ユーザID]>を含めるとメンションを飛ばせます ユーザIDはサーバ内に表示されている名前ではなく18桁の数字です。(IDの確認方法) const message = { "content": "<@1234567890> Hello!!", "tts": false, } 埋め込みコンテンツ 埋め込みコンテンツでは通常のテキストよりもリッチなチャットを投稿することができます 埋め込みコンテンツの投稿にはembedsフィールドを利用します。 colorフィールドで投稿文の色を指定できますが、カラーコードは10進数である必要があるため、const colorCode = parseInt("ff6fac", 16);で16進数のカラーコードを10進数に変換しています。 const colorCode = parseInt("ff6fac", 16); const message = { "content": "おすすめ動画です!", "tts": false, "embeds": [ { // 本文 "title": "【ウマ娘 プリティーダービー】OP映像", // リンク(上のタイトルと組み合わせるとリンクテキストになります) "url": "https://youtu.be/cmuTy73jzSs", // 投稿文右上に指定URLの画像をサムネイル表示 "thumbnail": { "url": "https://cdn.icon-icons.com/icons2/1584/PNG/512/3721679-youtube_108064.png" }, // 画像を埋め込み "image": { "url": "http://img.youtube.com/vi/cmuTy73jzSs/mqdefault.jpg" }, // 投稿文の色を指定(10進数で指定が必要) "color": colorCode, // 簡単なタイトルと文章を表のように並べることができます(1行:3つまで, サムネイルが入ると2つまで表示) "fields": [ { "name": ":arrow_forward: 再生数", "value": "362万", "inline": true }, { "name": ":thumbsup: 高評価", "value": "6.5万", "inline": true } ], // フッター(アイコンも指定可能) "footer": { "text": "https://twitter.com/uma_musu?s=20", "icon_url": "https://help.twitter.com/content/dam/help-twitter/brand/logo.png" } } ] } 結果 この他にも指定できるパラメータがあるので詳しくは下記を参照してください https://discord.com/developers/docs/resources/webhook#execute-webhook-jsonform-params ちなみにpythonでも実装が可能です(こちらはHeroku等のサーバーを用意する必要があります) https://qiita.com/1ntegrale9/items/9d570ef8175cf178468f 参考 https://qiita.com/bakuwarorin/items/164f29c446f8723d787b https://discord.com/developers/docs/resources/webhook
- 投稿日:2021-06-21T07:38:26+09:00
【Node.js, TypeScript】天気予報アプリをLINE MessageAPIで作ってみた!
先日、Node.jsでアプリを天気予報アプリを作成しました。 今回はNode.jsとTypeScriptで作ってみました。 完成形としては以下の通りです。 以前作成したLaravelに関してはこちらからどうぞ。 追記(2021/06/22) AWSでデプロイしました。 AWS勉強中の方はこちらの記事もどうぞ! どのようなアプリか 皆さんは、今日の気温を聞いて、「快適に過ごすために今日のファッションをこうしよう」ってパッと思いつきますか? 私は、最高気温、最低気温を聞いてもそれがどのくらい暑いのか、寒いのかがピンと来ず、洋服のチョイスを外したことがしばしばあります。 こんな思いを2度としないために今回このアプリを作りました。 line-bot-sdk-nodejsの型定義で多少躓きましたが、TypeScript初心者でもそこまで時間かからずにできるかと思います。 なので、TypeScriptを勉強中の方はぜひ取り組んでみてください。 アプリの流れ アプリの流れは大まかに以下の4つのステップで成り立っています。 ・①クライアントが現在地を送る ・②OpenWeatherから天気予報を取得 ・③データの整形 ・④クライアントに送る GitHub 完成形のコードは以下となります。 では実際に作成していきましょう! LINE Developersにアカウントを作成する LINE Developersにアクセスして、「ログイン」ボタンをクリックしてください。 その後諸々入力してもらったら以下のように作成できるかと思います。 注意事項としては、今回Messaging APIとなるので、チャネルの種類を間違えた方は修正してください。 チャネルシークレットとチャネルアクセストークンが必要になるのでこの2つを発行します。 ではこの2つを.envに入力します。 .env LINE_CHANNEL_SECRET=abcdefg123456 LINE_CHANNEL_ACCESS_TOKEN=HogeHogeHoge123456789HogeHogeHoge package.jsonの作成 以下のコマンドを入力してください。 これで、package.jsonの作成が完了します。 ターミナル $ npm init -y 必要なパッケージのインストール dependencies dependenciesはすべてのステージで使用するパッケージです。 今回使用するパッケージは以下の4つです。 ・@line/bot-sdk ・express ・dotenv ・axios 以下のコマンドを入力してください。 これで全てのパッケージがインストールされます。 ターミナル $ npm install @line/bot-sdk express dotenv axios --save devDependencies devDependenciesはコーディングステージのみで使用するパッケージです。 今回使用するパッケージは以下の7つです。 ・typescript ・@types/node ・@types/express ・ts-node ・ts-node-dev ・rimraf ・npm-run-all 以下のコマンドを入力してください。 これで全てのパッケージがインストールされます。 ターミナル $ npm install -D typescript @types/node @types/express ts-node ts-node-dev rimraf npm-run-all package.jsonにコマンドの設定を行う npm run devが開発環境の立ち上げに使います。 npm run startが本番環境用です。 package.json { "scripts": { "dev": "ts-node-dev --respawn api/src/index.ts", "clean": "rimraf dist", "tsc": "tsc", "build": "npm-run-all clean tsc", "start": "npm run build && node ." }, } tsconfig.jsonの作成 以下のコマンドを実行しTypeScriptの初期設定を行います。 ターミナル $ npx tsc --init それでは、作成されたtsconfig.jsonの上書きをしていきます。 tsconfig.json { "compilerOptions": { "target": "ES6", "module": "commonjs", "sourceMap": true, "outDir": "./api/dist", "strict": true, "moduleResolution": "node", "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, "include": ["api/src/**/**/*"] } 簡単にまとめると、 api/srcディレクトリ以下を対象として、それらをapi/distディレクトリにES6の書き方でビルドされるという設定です。 tsconfig.jsonに関して詳しく知りたい方は以下のサイトをどうぞ。 また、この辺りで必要ないディレクトリはGithubにpushしたくないので、.gitignoreも作成しておきましょう。 .gitignore node_modules package-lock.json .env dist https://localhost:3000にアクセスするとhello worldが表示 APIサーバーが正しく動くか検証のため一応作っておきましょう。 api/src/index.ts // Load the package import { Client, ClientConfig } from '@line/bot-sdk'; import express from 'express'; require('dotenv').config(); // Read the ports from the process.env file const PORT = process.env.PORT || 3000; // Load the access token and channel secret from the .env file const config: ClientConfig = { channelAccessToken: process.env.CHANNEL_ACCESS_TOKEN || '', channelSecret: process.env.CHANNEL_SECRET || '', }; // Instantiate const app: express.Express = express(); const client = new Client(config); // Do routing // Testing Routing app.get('/', (req: express.Request, res: express.Response): void => { res.send('Hello World'); }); // Start the server app.listen(PORT, (): void => { console.log('http://localhost:3000'); }); 上記の内容としては、 ①必要なパッケージを読み込む ②PORT番号を選択する(デプロイ先でPORT番号が指定されるパターンに備えて一応.envを読み込む形式にしています。) ③config の作成(これはおまじないのようなものです) ④インスタンス化を行う。(clientもおまじない) ⑤ルーティングの作成 ⑥WEBサーバーの実行 おまじないだけで片付けるのもアレなので公式サイトを貼っておきます。 またLINEBot関連の型定義に関してもこちらは基本なので、参考コードがあります。 それを丸パクリしましょう。 localhost.runで開発用のhttpsを取得 前回のNode.jsの記事でも使ったlocalhost.runを使います。 ということでここからはターミナル2つ使って開発していきます。 こんな感じです。 ターミナルに2つのコードを貼り付けて実行してください。 ターミナル $ npm run dev $ ssh -R 80:localhost:3000 localhost.run Webhook URLの登録 localhost.runで作成したhttpsのURLをコピーしてください。 私の場合は以下のURLです。 これをLINE DevelopersのWebhookに設定します。 これで初期設定は完了です。 ここからの流れはこのような感じです。 ①「今日の洋服は?」というメッセージを受け取る ②「今日の洋服は?」を受け取ったら、位置情報メッセージを送る ③「今日の洋服は?」以外を受け取ったら、「そのメッセージには対応していません」と送る ④「位置情報メッセージ」を受け取る ⑤「位置情報メッセージ」を受け取ったら、緯度と経度を使って天気予報を取得する ⑥「位置情報メッセージ」を受け取ったら、天気予報メッセージを送る では作っていきましょう! またこれら全てのコードをapi/src/index.tsに書くとコードが肥大化し可読性が落ちます。 なのでCommonディレクトリに関数に切り分けて作成していきます。 またここからはLINEBotのオリジナルの型が頻出します。 1つずつ説明するのはあまりに時間がかかるので、知らない型が出てきたらその度に以下のサイトで検索するようにしてください。 ①「今日の洋服は?」というメッセージを受け取る api/src/index.ts // Load the package import { Client, middleware, ClientConfig, MiddlewareConfig, WebhookEvent } from '@line/bot-sdk'; import express from 'express'; import dotenv from 'dotenv'; dotenv.config(); // Load the module import { SendMessage } from './Common/Send/ButtonOrErrorMessage'; // Read the ports from the process.env file const PORT: string | 3000 = process.env.PORT || 3000; // Load the access token and channel secret from the .env file const clientConfig: ClientConfig = { channelAccessToken: process.env.CHANNEL_ACCESS_TOKEN || '', channelSecret: process.env.CHANNEL_SECRET || '', }; const middlewareConfig: MiddlewareConfig = { channelAccessToken: process.env.CHANNEL_ACCESS_TOKEN || '', channelSecret: process.env.CHANNEL_SECRET || '', }; // Instantiate const app: express.Express = express(); const client: Client = new Client(clientConfig); // Do routing // Testing Routing app.get('/', (req: express.Request, res: express.Response): void => { res.send('Hello World'); }); // API Routing app.post( '/api/line/message', middleware(middlewareConfig), async (req: express.Request, res: express.Response): Promise<void> => { const events: WebhookEvent[] = req.body.events; events.map( async (event: WebhookEvent): Promise<void> => { try { await SendMessage(client, event); } catch (err) { console.error(err); } } ); } ); // Start the server app.listen(PORT, (): void => { console.log('http://localhost:3000'); }); ②「今日の洋服は?」を受け取ったら、位置情報メッセージを送る api/src/Common/Template/ButtonMessageTemplate.ts // Load the package import { TemplateMessage } from '@line/bot-sdk'; export const ButtonMessageTemplate = (): TemplateMessage => { return { type: 'template', altText: 'This is a buttons template', template: { type: 'buttons', text: '今日はどんな洋服にしようかな', actions: [ { type: 'uri', label: '現在地を送る', uri: 'https://line.me/R/nv/location/', }, ], }, }; }; api/src/Common/Send/ButtonOrErrorMessage.ts // Load the package import { Client, WebhookEvent } from '@line/bot-sdk'; // Load the module import { ButtonMessageTemplate } from '../Template/ButtonMessageTemplate'; export const SendMessage = async (client: Client, event: WebhookEvent): Promise<void> => { try { if (event.type !== 'message' || event.message.type !== 'text') { return; } const { replyToken } = event; const { text } = event.message; if (text === '今日の洋服は?') { await client.replyMessage(replyToken, ButtonMessageTemplate()); } else { // エラーメッセージを送る } } catch (err) { console.log(err); } }; ボタンメッセージのJSON作成に関しては公式サイトを参考にしましょう。 ③「今日の洋服は?」以外を受け取ったら、「そのメッセージには対応していません」と送る api/src/Common/Template/ErrorMessageTemplate.ts // Load the package import { TextMessage } from '@line/bot-sdk'; export const ErrorMessageTemplate = (): TextMessage => { return { type: 'text', text: 'ごめんなさい、このメッセージは対応していません。', }; }; api/src/Common/Send/ButtonOrErrorMessage.ts // Load the package import { Client, WebhookEvent } from '@line/bot-sdk'; // Load the module import { ButtonMessageTemplate } from '../Template/ButtonMessageTemplate'; import { ErrorMessageTemplate } from '../Template/ErrorMessageTemplate'; export const SendMessage = async (client: Client, event: WebhookEvent): Promise<void> => { try { if (event.type !== 'message' || event.message.type !== 'text') { return; } const { replyToken } = event; const { text } = event.message; if (text === '今日の洋服は?') { await client.replyMessage(replyToken, ButtonMessageTemplate()); } else { await client.replyMessage(replyToken, ErrorMessageTemplate()); } } catch (err) { console.log(err); } }; テキストメッセージのJSON作成に関しては公式サイトを参考にしましょう。 ④「位置情報メッセージ」を受け取る api/src/index.ts // Load the package import { Client, middleware, ClientConfig, MiddlewareConfig, WebhookEvent } from '@line/bot-sdk'; import express from 'express'; import dotenv from 'dotenv'; dotenv.config(); // Load the module import { SendMessage } from './Common/Send/ButtonOrErrorMessage'; import { FlexMessage } from './Common/Send/FlexMessage'; // Read the ports from the process.env file const PORT: string | 3000 = process.env.PORT || 3000; // Load the access token and channel secret from the .env file const clientConfig: ClientConfig = { channelAccessToken: process.env.CHANNEL_ACCESS_TOKEN || '', channelSecret: process.env.CHANNEL_SECRET || '', }; const middlewareConfig: MiddlewareConfig = { channelAccessToken: process.env.CHANNEL_ACCESS_TOKEN || '', channelSecret: process.env.CHANNEL_SECRET || '', }; // Instantiate const app: express.Express = express(); const client: Client = new Client(clientConfig); // Do routing // Testing Routing app.get('/', (req: express.Request, res: express.Response): void => { res.send('Hello World'); }); // API Routing app.post( '/api/line/message', middleware(middlewareConfig), async (req: express.Request, res: express.Response): Promise<void> => { const events: WebhookEvent[] = req.body.events; events.map( async (event: WebhookEvent): Promise<void> => { try { await SendMessage(client, event); await FlexMessage(client, event); } catch (err) { console.error(err); } } ); } ); // Start the server app.listen(PORT, (): void => { console.log('http://localhost:3000'); }); ⑤「位置情報メッセージ」を受け取ったら、緯度と経度を使って天気予報を取得する Flex Messageの作成方法に関してファイル名も出しながら説明します。 【ファイル名】GetWeatherForecast.ts 天気予報を取得します。 まずはOpenWeatherで天気予報を取得するために必要な情報が3つあります。 ①API ②経度 ③緯度 それではこの3つを取得していきましょう。 ①API 以下にアクセスしてください。 アカウントを作成し、APIキーを発行してください。 発行できたらこのAPIを.envに保存します。 .env # OpenWeather(https://home.openweathermap.org/api_keys) WEATHER_API = "a11b22c33d44e55f66g77" あとは関数内で.envを取得するだけです。 ②経度、③緯度 これら2つは、eventから取得できます。 ということで作っていきましょう。 api/src/Common/Template/WeatherForecast/GetWeatherForecast.ts // Load the package import { WebhookEvent } from '@line/bot-sdk'; import axios, { AxiosResponse } from 'axios'; export const getWeatherForecastData = async (event: WebhookEvent): Promise<any> => { try { if (event.type !== 'message' || event.message.type !== 'location') { return; } // Get latitude and longitude const latitude: number = event.message.latitude; const longitude: number = event.message.longitude; // OpenWeatherAPI const openWeatherAPI: string | undefined = process.env.WEATHER_API || ''; // OpenWeatherURL const openWeatherURL: string = `https://api.openweathermap.org/data/2.5/onecall?lat=${latitude}&lon=${longitude}&units=metric&lang=ja&appid=${openWeatherAPI}`; const weatherData: AxiosResponse<any> = await axios.get(openWeatherURL); return weatherData; } catch (err) { console.log(err); } }; 【ファイル名】FormatWeatherForecast.ts 取得した天気予報のデータの整形を行う。 こちらでは、const weatherとconst weatherArrayの2つで型定義ファイルを作成する必要があります。 ということで作成しましょう。 api/src/Common/Template/WeatherForecast/types/weather.type.ts export type WeatherType = { dt: number; sunrise: number; sunset: number; moonrise: number; moonset: number; moon_phase: number; temp: { day: number; min: number; max: number; night: number; eve: number; morn: number; }; feels_like: { day: number; night: number; eve: number; morn: number; }; pressure: number; humidity: number; dew_point: number; wind_speed: number; wind_deg: number; wind_gust: number; weather: [ { id: number; main: string; description: string; icon: string; } ]; clouds: number; pop: number; rain: number; uvi: number; }; api/src/Common/Template/WeatherForecast/types/weatherArray.type.ts export type WeatherArrayType = { today: string; imageURL: string; weatherForecast: string; mornTemperature: number; dayTemperature: number; eveTemperature: number; nightTemperature: number; fashionAdvice: string; }; 作成した型定義を使ってファイルを完成させます。 api/src/Common/Template/WeatherForecast/FormatWeatherForecast.ts // Load the package import { WebhookEvent } from '@line/bot-sdk'; import { AxiosResponse } from 'axios'; // Load the module import { getWeatherForecastData } from './GetWeatherForecast'; // types import { WeatherType } from './types/weather.type'; import { WeatherArrayType } from './types/weatherArray.type'; export const formatWeatherForecastData = async (event: WebhookEvent): Promise<WeatherArrayType> => { // Get the getWeatherForecastData const weathers: AxiosResponse<any> = await getWeatherForecastData(event); // Util const weather: WeatherType = weathers.data.daily[0]; // Five required data // 1) Today's date const UNIXToday: number = weather.dt; const convertUNIXToday: Date = new Date(UNIXToday * 1000); const today: string = convertUNIXToday.toLocaleDateString('ja-JP'); // 2) Weather forecast const weatherForecast: string = weather.weather[0].description; // 3) Temperature (morning, daytime, evening, night) const mornTemperature: number = weather.feels_like.morn; const dayTemperature: number = weather.feels_like.day; const eveTemperature: number = weather.feels_like.eve; const nightTemperature: number = weather.feels_like.night; // Bifurcate your clothing by maximum temperature const maximumTemperature: number = Math.max( mornTemperature, dayTemperature, eveTemperature, nightTemperature ); // 4) Fashion Advice let fashionAdvice: string = ''; // 5) Fashion Image let imageURL: string = ''; if (maximumTemperature >= 26) { fashionAdvice = '暑い!半袖が活躍する時期です。少し歩くだけで汗ばむ気温なので半袖1枚で大丈夫です。ハットや日焼け止めなどの対策もしましょう'; imageURL = 'https://uploads-ssl.webflow.com/603c87adb15be3cb0b3ed9b5/60aa3c44153071e6df530eb7_71.png'; } else if (maximumTemperature >= 21) { fashionAdvice = '半袖と長袖の分かれ目の気温です。日差しのある日は半袖を、曇りや雨で日差しがない日は長袖がおすすめです。この気温では、半袖の上にライトアウターなどを着ていつでも脱げるようにしておくといいですね!'; imageURL = 'https://uploads-ssl.webflow.com/603c87adb15be3cb0b3ed9b5/6056e58a5923ad81f73ac747_10.png'; } else if (maximumTemperature >= 16) { fashionAdvice = 'レイヤードスタイルが楽しめる気温です。ちょっと肌寒いかな?というくらいの過ごしやすい時期なので目一杯ファッションを楽しみましょう!日中と朝晩で気温差が激しいので羽織ものを持つことを前提としたコーディネートがおすすめです。'; imageURL = 'https://uploads-ssl.webflow.com/603c87adb15be3cb0b3ed9b5/6087da411a3ce013f3ddcd42_66.png'; } else if (maximumTemperature >= 12) { fashionAdvice = 'じわじわと寒さを感じる気温です。ライトアウターやニットやパーカーなどが活躍します。この時期は急に暑さをぶり返すことも多いのでこのLINEで毎日天気を確認してくださいね!'; imageURL = 'https://uploads-ssl.webflow.com/603c87adb15be3cb0b3ed9b5/6056e498e7d26507413fd853_4.png'; } else if (maximumTemperature >= 7) { fashionAdvice = 'そろそろ冬本番です。冬服の上にアウターを羽織ってちょうどいいくらいです。ただし室内は暖房が効いていることが多いので脱ぎ着しやすいコーディネートがおすすめです!'; imageURL = 'https://uploads-ssl.webflow.com/603c87adb15be3cb0b3ed9b5/6056e4de7156326ff560b1a1_6.png'; } else { fashionAdvice = '凍えるほどの寒さです。しっかり厚着して、マフラーや手袋、ニット帽などの冬小物もうまく使って防寒対策をしましょう!'; imageURL = 'https://uploads-ssl.webflow.com/603c87adb15be3cb0b3ed9b5/6056ebd3ea0ff76dfc900633_48.png'; } // Make an array of the above required items. const weatherArray: WeatherArrayType = { today, imageURL, weatherForecast, mornTemperature, dayTemperature, eveTemperature, nightTemperature, fashionAdvice, }; return weatherArray; }; 【ファイル名】FlexMessageTemplate 整形したデータを取得して Flex Messageのテンプレートを作成する。 api/src/Common/Template/WeatherForecast/FlexMessageTemplate.ts // Load the package import { WebhookEvent, FlexMessage } from '@line/bot-sdk'; // Load the module import { formatWeatherForecastData } from './FormatWeatherForecast'; export const FlexMessageTemplate = async (event: WebhookEvent): Promise<FlexMessage> => { const data = await formatWeatherForecastData(event); return { type: 'flex', altText: '天気予報です', contents: { type: 'bubble', header: { type: 'box', layout: 'vertical', contents: [ { type: 'text', text: data.today, color: '#FFFFFF', align: 'center', weight: 'bold', }, ], }, hero: { type: 'image', url: data.imageURL, size: 'full', }, body: { type: 'box', layout: 'vertical', contents: [ { type: 'text', text: `天気は、「${data.weatherForecast}」です`, weight: 'bold', align: 'center', }, { type: 'text', text: '■体感気温', margin: 'lg', }, { type: 'text', text: `朝:${data.mornTemperature}℃`, margin: 'sm', size: 'sm', color: '#C8BD16', }, { type: 'text', text: `日中:${data.dayTemperature}℃`, margin: 'sm', size: 'sm', color: '#789BC0', }, { type: 'text', text: `夕方:${data.eveTemperature}℃`, margin: 'sm', size: 'sm', color: '#091C43', }, { type: 'text', text: `夜:${data.nightTemperature}℃`, margin: 'sm', size: 'sm', color: '#004032', }, { type: 'separator', margin: 'xl', }, { type: 'text', text: '■洋服アドバイス', margin: 'xl', }, { type: 'text', text: data.fashionAdvice, margin: 'sm', wrap: true, size: 'xs', }, ], }, styles: { header: { backgroundColor: '#00B900', }, hero: { separator: false, }, }, }, }; }; ⑥「位置情報メッセージ」を受け取ったら、天気予報メッセージを送る api/src/Common/Send/FlexMessage.ts // Load the package import { Client, WebhookEvent } from '@line/bot-sdk'; // Load the module import { FlexMessageTemplate } from '../Template/WeatherForecast/FlexMessageTemplate'; export const FlexMessage = async (client: Client, event: WebhookEvent): Promise<void> => { try { if (event.type !== 'message' || event.message.type !== 'location') { return; } const { replyToken } = event; const message = await FlexMessageTemplate(event); await client.replyMessage(replyToken, message); } catch (err) { console.log(err); } }; これで完成です! めちゃくちゃ簡単ですね。 最後にデプロイをしましょう 今回もデプロイはGlitchを使います。 アカウントは、Githubで作るのがおすすめです。 作成しましたら、プロジェクトを作成します。 「import from GitHub」をクリックします。 ここには、GithubのURLを貼り付けます。 ちょっと待つとこのように読み込まれます。 便利なのは全てのファイルが確認できるところです。 HerokuなどはどちらかというとCUIであり、GUIのGlitchは直感的に操作できてすごく良かったです。 最後に.envに値を入力します。 ここまで行えばデプロイは成功です! ちなみに URL変えたいときはここをいじってください ShareボタンをクリックすればURLがLive siteに書いているよ Webhookの設定を変更 これで完成です! 最後に FlexMessageなどコードにミスが起きやすいので、TypeScriptの型定義さえあればミスに気づきやすくなるなぁと実感しました。 次は、このアプリをAWSへデプロイするハンズオン記事を書いていきます。
- 投稿日:2021-06-21T06:56:34+09:00
【入門】はじめての Cypress
はじめに Cypress の勉強のため、公式ドキュメントの概要~入門を簡単にまとめました。 皆様の参考になれば幸いです。 この記事で分かること Cypress の概要 Selenium との比較 入門 (サンプルアプリの作成) Next.js + TypeScript + Cypress の簡単なサンプルアプリの作成 環境 { "cypress": "^7.5.0", "typescript": "^4.3.4" } 1. Cypress とは Github: cypress-io/cypress: Fast, easy and reliable testing for anything that runs in a browser. 公式 Doc: Why Cypress? | Cypress Documentation Cypress はフロントエンドのテストツールであり、以下のすべてのを作成・テストできる。 エンドツーエンド(E2E)テスト 統合(インテグレーション)テスト ユニットテスト 1.1. Cypress のエコシステム (Cypress ecosystem) Cypress はローカルにインストールされるテストランナーと、テスト記録用のダッシュボードで構成される。 開発中: ローカルでアプリケーションを開発中、常にテストを回すことができる (=TDD に最適!) 開発後: テストを構築し、Cypress を CI ツールと統合した後、ダッシュボードでテストの実行結果を記録・確認できる 1.2. 特徴(Features) タイムトラベル: Cypress はテスト実行時にスナップショットを取得できる コマンドログにカーソルを合わせると、各ステップの詳細を確認できる デバッグ可能: テストの失敗理由を推測せず、開発ツールから直接デバッグできる そのため、読み込み可能なエラーとスタックトレースにより、デバッグが非常に高速になる 自動待機: テストに await や sleep はいらない Cypress は、コマンドやアサーション時に要素が見つからない場合、自動で再試行する Stubs, Spies, Clocks: sinonの Spy や Stub をラップしている Clocks(Date、setTimeout、clearTimeout、setInterval, clearInterval)のラッパーも提供している ネットワークトラフィック制御 サーバーを使用せずに、ネットワークリクエストを制御、Stub, テストできる 一貫した結果 Selenium や WebDriver を使用していないため、高速で一貫性のある信頼性の高いテストを提供する スクリーンショットとビデオ 失敗時に、以下を表示できる 自動的に撮影されたスクリーンショット CLI から実行した場合のテストスイート全体のビデオ クロスブラウザテスト CI パイプライン上で、Firefox および Chrome 系のブラウザを用いて最適なテストを実行できる 1.3. サンプル 公式動画: Cypress をインストールしてから実際にテストケースを蹴るところまで、簡潔にまとまっている cypress-io/cypress-realworld-app: Cypress のテスト手法やシナリオを実践するためのアプリケーション 1.4. テストタイプ E2E テスト 一般的な E2E テストのように、ブラウザでアプリケーションにアクセスし、実際のユーザーと同じように UI を介してアクションを実行する。 it("adds todos", () => { cy.visit("https://todo.app.com"); cy.get(".new-input").type("write code{enter}").type("write tests{enter}"); // confirm the application is showing two items cy.get("li.todo").should("have.length", 2); }); コンポーネントテスト 一部の Web フレームワーク (Rect or Vue) からコンポーネントをマウントすることで、コンポーネントテストも実行できる。 jest や mocha のようにコンポーネントを jsdom でレンダリングするのではなく、実際のブラウザ上にコンポーネントをマウントし、そのコンポーネントに対してテストを行う。 ※ただし、現状(2021/06/19)段階でアルファ版なので注意 import { mount } from "@cypress/react"; // or @cypress/vue import TodoList from "./components/TodoList"; it("contains the correct number of todos", () => { const todos = [ { text: "Buy milk", id: 1 }, { text: "Learn Component Testing", id: 2 }, ]; mount(<TodoList todos={todos} />); // the component starts running like a mini web app cy.get("[data-testid=todos]").should("have.length", todos.length); }); API テスト Cypress は任意の HTTP 呼び出しを実行できるため、API テストも可能。 it("adds a todo", () => { cy.request({ url: "/todos", method: "POST", body: { title: "Write REST API", }, }) .its("body") .should("deep.contain", { title: "Write REST API", completed: false, }); }); その他 多数のプラグインを活用することで、a11y、画像回帰、電子メールテスト等々が可能。 2. Selenium との比較 2.1. アーキテクチャ Selenium は、ブラウザの外部で実行され、ネットワーク経由でコマンドを実行する Cypress は、ブラウザ内で、アプリケーションと同じランタイムで実行される Cypress の特徴 アプリケーションのイベントにリアルタイムでアクセス可能 ネットワークトラフィックをその場で読み取り、変更できるため、ネットワーク層でも動作する それにより、ブラウザに出入りするすべてを変更可能 ブラウザの自動化を妨げるコードを変更できる Cypress は自動化プロセス全体を制御する それによりブラウザの内外で発生するすべてを理解できる それにより、Cypress はどのテストツールよりも一貫した結果を提供できる Cypress はローカルマシンにインストールされる 自動化タスクのために、OS の機能を追加利用できる スクリーンショットの撮影、ビデオの録画、ファイル操作、ネットワーク操作、等々 2.2. ホストオブジェクトへのネイティブアクセス Cypress はアプリケーション内で動作するため、全てのホストオブジェクト(window, document, DOM element, Application Instance, function, timer, service worker, etc)へのネイティブアクセスを提供する。 つまり、テストコードにおいて、アプリケーションコードと同じオブジェクトにアクセス可能。 2.3. 新しい種類のテスト 上記の特徴により、従来では時間と費用のかかった以下のようなケースを人為的に作成可能。 ブラウザ or アプリケーションの機能をスタブし、テストケースで必要に応じて動作するように強制する Redux のようにデータストアを公開し、テストコードから直接アプリケーションの状態を変更する サーバーから空のレスポンスを返すことで、「空のビュー」などのエッジケースをテストする レスポンスのステータスコードを 500 に変更することで、アプリケーションの異常系のテストが可能 DOM 要素を直接変更(Ex. 非表示 DOM を表示するように) プログラムでサードパーティのプラグインを利用可能 以下のような複雑な UI ウィジェットの代わりに、テストコードから直接メソッドを呼び出し、制御が可能 Ex. 複数選択、オートコンプリート、ドロップダウン、ツリービュー、カレンダー テスト時、アプリケーションコードが実行される前に、Google Analytics が読み込まれないようにする アプリケーションが新しいページに移行したり、アンロードされるたびに同期通知を受け取る テストで必要な時間を待たずにタイマー or ポーリングが自動的に起動するように、時間を前後に動かす アプリケーションに応答する独自のイベントリスナーを追加する テスト中に異なる動作をするようにアプリケーションコードを更新できる テストコードから WebSocket メッセージを制御する サードパーティのスクリプトを条件付きでロードする アプリケーションで直接関数を呼び出す 2.4. ショートカット Cypress は、特定の状況を生成するために「ユーザーのように振る舞う」ことを矯正しない。 Cypress では、プログラムからアプリケーションを操作及び制御できる。 例 他のツール ログインページにアクセス ユーザー名とパスワードを入力 送信 リダイレクト後、テストを実施 (これをテストケース毎に実行) Cypress cy.request()で直接同期的にログインリクエストを送信し、テストを行う cy.request()を利用した場合、リクエストがブラウザから送信されたかのように Cookie を自動的に取得・設定する。また、CORS も完全にパイバスされる。 2.5. 取りこぼしのないテスト (Flake resistant) Cypress はアプリケーションで同期的に発生するすべてを理解している ページロード、アンロードのタイミングで通知される イベント発生時、要素の変更を見逃さない 要素がアニメーション中か自動で判定し、停止するまで待機する さらに、要素がちゃんと表示され、有効(disabled=false)になり、親要素にカバーされなくなるまで待機する ページ遷移が始まると、次ページが完全に読み込まれるまで待機する 特定のネットワークリクエストが終了するのを待つように指示もできる 2.6. デバッグ可能 Cypress は使いやすさを大切にしている。 テストに失敗すると、失敗した正確な理由を示す数百のエラーメッセージが出力される 以下の要素を視覚的に表現する豊富な UI がある コマンドの実行、アサーション、ネットワークリクエスト、スパイ、スタブ、ページロード、URL の変更 アプリケーションのスナップショットにより、コマンドが実行された状態にタイムトラベルできる テスト実行中に開発者ツールが使える すべてのコンソールメッセージ、ネットワークリクエストを確認できる 要素の検査ができる テストコードやアプリケーションコードでデバッガーステートメントを使用できる これらにより、開発とテストのすべてを蔵時に行うことができる。 2.7. トレードオフ これらの機能を可能にするに行ったトレードオフがある。 テストコードはあくまでクライアントサイドで評価される。サーバーサイド(Node.js)では評価されない 複数タブ、複数ブラウザをサポートしていない Same Origin には対応しているが、Cross Origin には対応していない 3. Getting Started 簡単な Next.js (+ Typescript) アプリを起動し、テストを作成・実行してみる。 ※ Next.js の部分は任意のフレームワークに置き換えて良いです。 ※ 不要であれば読み飛ばしてください。 3.1. 環境 OS 別のセットアップ方法はこちらに詳細が記載されているので、適宜確認する。 $ node -v v16.3.0 $ npm -v 7.15.1 3.2. ソース 3.3. 準備 3.3.1. インストール # Next.jsインストール。他でも可。既存プロジェクトでも可。 yarn create next-app --typescript # or npx create-next-app --ts cd my-app # 依存関係を分離するため、直下にe2eフォルダを作成し、そこにCypressをインストールする mkdir e2e cd e2e echo e2e/node_modules >> .gitignore yarn init -y # or npm init -y yarn add -D cypress typescript # or npm install -D cypress typescript npx tsc --init --types cypress --lib dom,es6 3.3.2. Cypress を開く npx cypress open, yarn run cypress openで実行可能。 package.jsonを使う場合は、 scripts に以下を追加する。 { "scripts": { "cypress:open": "cypress open", "cypress:run": "cypress run" } } yarn cypress:open これにより、以下が行われる。 テストランナー用のブラウザが起動される /e2e/cypressフォルダが作られ、配下にサンプルテストケースが配置される /e2e/cypress.jsonが作成される テストランナー起動後、Testsの適当なファイル名をクリックすると、テストが実行されます。 CLI ツールの詳細はこちら ダッシュボードの使い方はこちら 3.4. 新しいテストケースの追加 新しいテストケースを追加する。 その前に、Next.js を使用している場合は、一旦サーバーを別のターミナルで起動しておく。 yarn dev echo " context('Next.js test', () => { it('should access localhost', () => { cy.visit('http://localhost:3000'); cy.get('h1') .should('have.text', 'Welcome to Next.js!') }); }); " > e2e/cypress/integration/sample.spec.ts Next.js を使用していない場合は、適当なサイトにアクセスするテストケースを作成する (Cypress がサンプルページを提供している)。 echo " context('google search', () => { it('should perform basic google search', () => { cy.visit('https://google.com'); cy.get('[name="q"]') .type('subscribe') .type('{enter}'); }); }); " > e2e/cypress/integration/sample.spec.ts yarn cypress:openで起動したブラウザにおいて、Testsにsample.spec.tsが追加される。 そして、sample.spec.tsをクリックすると、作成したテストケースが実行される。 3.5. 成功/失敗するテストケースの追加 先程のsample.spec.tsもしくは別のファイルに、以下を追記し、実行する。 すると、成功した場合はチェックが付き、失敗した場合にエラーが表示されることが分かる。 基本的なページアクセス、クエリ、イベント発火方法等々については、こちらを見るか、サンプルテストを見ると分かりやすい。 エラーの見方は、こちらに詳細がある。 describe("My First Test", () => { it("Match!", () => { expect(true).to.equal(true); }); it("Does not match!", () => { expect(true).to.equal(false); }); }); 3.6. アプリのテスト 同じアプリケーション(ホスト)に頻繁にアクセスする場合、baseUrlを指定すると良い。 それにより、cy.visit() と cy.request() に自動的にプレフィックスが付与されるため、ホスト名とポートを省略できる。 cypress.json { "baseUrl": "http://localhost:3000" } sample.spec.ts describe('The Home Page', () => { it('successfully loads', () => { cy.visit('/') }) }) 3.7. テスト戦略 3.7.1. 初期データの用意 テスト前にサーバーサイド(データベース等)でセットアップや破棄を行いたい場合、beforeEachやafterEachを使う。 また、以下のメソッドも有用。 cy.exec() システムコマンドを実行する。 describe("The Home Page", () => { beforeEach(() => { // reset and seed the database prior to every test cy.exec("npm run db:reset && npm run db:seed"); }); it("successfully loads", () => { cy.visit("/"); }); }); cy.task() pluginsFile を介して Node でコードを実行する。 例 1: ログの出力 spec.js cy.task("hello", { greeting: "Hello", name: "World" }); plugins/index.js module.exports = (on, config) => { on("task", { // deconstruct the individual properties hello({ greeting, name }) { console.log("%s, %s", greeting, name); return null; }, }); }; 例 2: TODO アプリで、TODO が追加された後、それが本当に DB に保存されているかテストする spec.js import { enterTodo, resetDatabase } from './utils' describe('cy.task', () => { beforeEach(resetDatabase) beforeEach(() => { cy.visit('/') }) it('finds record in the database', () => { // random text to avoid confusion const id = Cypress._.random(1, 1e6) const title = `todo ${id}` enterTodo(title) // confirm the new item has been saved // https://on.cypress.io/task cy.task('hasSavedRecord', title).should('equal', true) }) }) cypress/plugins/index.js const fs = require('fs') const path = require('path') const repoRoot = path.join(__dirname, '..', '..') const findRecord = title => { const dbFilename = path.join(repoRoot, 'data.json') const contents = JSON.parse(fs.readFileSync(dbFilename, 'utf8')) const todos = contents.todos return todos.find(record => record.title === title) } module.exports = (on, config) => { // "cy.task" can be used from specs to "jump" into Node environment // and doing anything you might want. For example, checking "data.json" file! on('task', { hasSavedRecord (title) { console.log('looking for title "%s" in the database', title) return Boolean(findRecord(title)) } }) } 参考: Incredibly Powerful cy.task | Better world by better software 3.7.2. サーバーのスタブ cy.request()で HTTP リクエストを行う際、事前にcy.intercept()を実行しておくことで、HTTP 通信をスタブできる。 cy.intercept( { method: "GET", // Route all GET requests url: "/users/*", // that have a URL that matches '/users/*' }, [] // and force the response to be: [] ); 参考: Network Requests | Cypress Documentation 3.7.3. ログイン ログイン機能のテストを行う場合は、以下のように UI を通してテストするのがおすすめ。 describe("The Login Page", () => { beforeEach(() => { // reset and seed the database prior to every test cy.exec("npm run db:reset && npm run db:seed"); // seed a user in the DB that we can control from our tests // assuming it generates a random password for us cy.request("POST", "/test/seed/user", { username: "jane.lane" }) .its("body") .as("currentUser"); }); it("sets auth cookie when logging in via form submission", function () { // destructuring assignment of the this.currentUser object const { username, password } = this.currentUser; cy.visit("/login"); cy.get("input[name=username]").type(username); // {enter} causes the form to submit cy.get("input[name=password]").type(`${password}{enter}`); // we should be redirected to /dashboard cy.url().should("include", "/dashboard"); // our auth cookie should be present cy.getCookie("your-session-cookie").should("exist"); // UI should reflect this user being logged in cy.get("h1").should("contain", "jane.lane"); }); }); しかし、各テストの前に毎回上記のログイン処理を行うのは冗長。 そこで、/loginに直にログイン情報をポストすることで、ログイン処理を簡略化できる。 it("logs in programmatically without using the UI", function () { // destructuring assignment of the this.currentUser object const { username, password } = this.currentUser; // programmatically log us in without needing the UI cy.request("POST", "/login", { username, password, }); // now that we're logged in, we can visit // any kind of restricted route! cy.visit("/dashboard"); // our auth cookie should be present cy.getCookie("your-session-cookie").should("exist"); // UI should reflect this user being logged in cy.get("h1").should("contain", "jane.lane"); }); その他 こちらの Best Practicesも参考になる。 参考 Why Cypress? | Cypress Documentation Incredibly Powerful cy.task | Better world by better software Cypress - TypeScript Deep Dive Basic Features: TypeScript | Next.js
- 投稿日:2021-06-21T04:11:03+09:00
Chrome拡張作ったら異世界転生した件
詠唱 配列(文字列)からランダムに要素を取り出すコードを書くじゃろ // ランダムに要素を取り出す関数 const randomPick = (items) => { const index = Math.floor(items.length * Math.random()); return items[index]; }; randomPick("123456"); // 1〜6のどれか 文字列の範囲を展開するコードを書くじゃろ // 文字列の範囲を展開する関数 const expandCharRange = (str) => str.replace(/.-./g, (range = 'a-z') => { // ハイフンを挟んだ文字列を全て置換 const first = range.codePointAt(0); // 1文字目の文字コード const last = range.codePointAt(2); // 2文字目の文字コード const length = last - first + 1; // 文字数 const codePoints = [...Array(length)].map((_, i) => first + i); // 全ての文字コード return String.fromCodePoint(...codePoints); // 文字列に変換 }); expandCharRange("a-z0-9"); // "abcdefghijklmnopqrstuvwxyz0123456789" ルーン文字をランダムで生成して... // ルーン文字を生成する関数 const runes = expandCharRange('ᚠ-ᛪᛮ-ᛰᛱ-ᛳᛴ-ᛸ'); const randomRune = () => randomPick(runes); [...Array(3)].map(randomRune) // ["ᛴ", "ᚶ", "ᚳ"] レイアウトが崩れたりしないようにテキストノードだけ置換するコードを書くんじゃ // 指定要素以下の全ノードを取得する関数 const allNodes = (root = document.body) => [root, ...[...root.childNodes].flatMap(allNodes)]; // 無害そうなテキストノードだけ取得 const allVisibleTextNodes = allNodes() .filter((node) => node.nodeType === Node.TEXT_NODE) // テキストノードだけ抽出 .filter((node) => !/script|style/i.test(node.parentElement.tagName)) // 親がヤバいヤツは除外 .filter((node) => node.nodeValue.trim()); // 空っぽなヤツは除外 // 空白以外は全てルーン文字に置換 allVisibleTextNodes.forEach((node) => { node.nodeValue = node.nodeValue.replace(/\S/g, randomRune); }); Chrome拡張を作成 (必要なファイルは2つだけ) 2つのファイルを作って適当なフォルダにぶち込む manifest.json { "manifest_version": 3, "version": "0.1.0", "name": "異世界拡張", "description": "Chrome拡張作ったら異世界転生した件", "background": { "service_worker": "isekai.js" }, "action": { "default_title": "転生する" }, "permissions": ["activeTab", "scripting"] } isekai.js 'use strict'; // 上記のまとめ関数 (若干変えた) function tensei() { // ランダムに要素を取り出す関数 const randomPick = (items) => { const index = Math.floor(items.length * Math.random()); return items[index]; }; // 文字列の範囲を展開する関数 const expandCharRange = (str) => str.replace(/.-./g, (range = 'a-z') => { // ハイフンを挟んだ文字列を全て置換 const first = range.codePointAt(0); // 1文字目の文字コード const last = range.codePointAt(2); // 2文字目の文字コード const length = last - first + 1; // 文字数 const codePoints = [...Array(length)].map((_, i) => first + i); // 全ての文字コード return String.fromCodePoint(...codePoints); // 文字列に変換 }); // ルーン文字を生成する関数 const runes = expandCharRange('ᚠ-ᛪᛮ-ᛰᛱ-ᛳᛴ-ᛸ'); const randomRune = () => randomPick(runes); // 空白・記号・絵文字・数字・ギリシャ文字以外をルーン文字に置換する関数 const conv = (str) => str.replace(/[^\p{White_Space}\p{Symbol}\p{Punctuation}\p{Emoji}\p{Number}\p{sc=Greek}]/gu, randomRune); // 指定要素以下の全ノードを取得する関数 const allNodes = (root = document.body) => [root, ...[...root.childNodes].flatMap(allNodes)]; // 無害そうなテキストノードだけ取得 const allVisibleTextNodes = allNodes() .filter((node) => node.nodeType === Node.TEXT_NODE) // テキストノードだけ抽出 .filter((node) => !/script|style/i.test(node.parentElement.tagName)) // 親がヤバいヤツは除外 .filter((node) => node.nodeValue.trim()); // 空っぽなヤツは除外 // ルーン・オリジナルを切り替え allVisibleTextNodes.forEach((node) => { if (node.original) { [node.original, node.nodeValue] = ['', node.original]; } else { [node.original, node.nodeValue] = [node.nodeValue, conv(node.nodeValue)]; } }); } // 拡張のアイコンを押すたびに実行 chrome.action.onClicked.addListener((tab) => { chrome.scripting.executeScript({ target: { tabId: tab.id }, function: tensei }); }); Chromeの拡張機能のページを開いてデベロッパーモードを有効化 「パッケージ化されていない拡張機能を読み込む」からフォルダを選択して完了じゃ! 結果 右上のパズルのピースみたいなアイコンから「異世界拡張」をクリックすればいつでも異世界 楽しい!✌('ω'✌ )三✌('ω')✌三( ✌'ω')✌ 参考 ルーン文字 - Wikipedia Unicode ルーン文字 - CyberLibrarian
- 投稿日:2021-06-21T01:19:58+09:00
【Javascript】配列によく使われる関数ー学習ノート
初めに javascriptの配列によく使われる関数について学習した内容のoutput用記事です。 ※内容に間違いなどがある場合はご指摘をよろしくお願いします。 ※こちらの記事はあくまでも個人で学習した内容のoutputとしての記事になります。 slice()メソッド 配列の中身を何番目から何番目までを切り出して、新しい配列を生成するメソッド。元の配列の値は変更されない。 使ってみる indexは0から始まるので配列の左から4番目の値からスタートします。何番目までかを指定する第2引数は省略することができ、その場合は最後までがindexになります。 let names = ['Tanaka', 'Suzuki', 'Honda', 'Matsuyama', 'Kobayashi']; console.log(names.slice(3)); //(2) ["Matsuyama", "Kobayashi"] 第2引数を指定した場合、その引数の含まないindexが範囲になります。以下のように第2引数として4を指定した場合、新しく生成される配列の最後の値は配列の左から3番目(0,1,2,3)の'Matsuyama'になります。 console.log(names.slice(2,4)); //(2) ["Honda", "Matsuyama"] 代入する引数の値がマイナス(-)だった場合には配列の一番右側から-1,-2,-3と数えられ、-1番目から指定した引数の-n番目までの値を持つ配列が生成されます。 console.log(names.slice(-1)); //["Kobayashi"] console.log(names.slice(-2)); //(2) ["Matsuyama", "Kobayashi"] これを応用すれば左から2番目からスタートし、右から数えて2番目までを含まない値を新しい配列として生成することができます。 console.log(names.slice(1, -2)); //(2) ["Suzuki", "Honda"] また、sliceメソッドで引数を指定しないと、対象の配列のコピーが作れます。 console.log(names.slice()); //(5) ["Tanaka", "Suzuki", "Honda", "Matsuyama", "Kobayashi"] これはスプレッド構文を使った結果と同じです。配列のコピーを作るとき、slice()メソッドを使うかスプレッド構文を使うかは人の好みでありどちらを使ってもいいです。 console.log([...names]); //(5) ["Tanaka", "Suzuki", "Honda", "Matsuyama", "Kobayashi"] splice()メソッド 生成する配列はslice()メソッドと全く同じですが、元の配列が変更されるところが違います。このようにsplice()メソッドを使うと切り出した配列の部分を除いたものが元の配列になります。 console.log(names.splice(3)); //(2) ["Matsuyama", "Kobayashi"] console.log(names); //(3) ["Tanaka", "Suzuki", "Honda"] reverse()メソッド 配列の要素の順番を逆にするメソッドです。引数は取りません。 console.log(names.reverse()); //(5) ["Kobayashi", "Matsuyama", "Honda", "Suzuki", "Tanaka"] reverse()メソッドは元の配列も変更してしまうため、注意が必要です。 console.log(names); //(5) ["Kobayashi", "Matsuyama", "Honda", "Suzuki", "Tanaka"] concat()メソッド 2つの配列の要素同士をくっ付けて一つの新しい配列を生成します。元の配列は変更されません。 const alpha = ['c', 'd', 'e', 'f', 'g']; const nums = [1, 2, 3, 4, 5]; const letters = alpha.concat(nums); console.log(letters); //(10) ["c", "d", "e", "f", "g", 1, 2, 3, 4, 5] これもスプレッドメソッドを使えば同じ配列を作ることができます。 const letters2 = [...alpha, ...nums]; console.log(letters2); //(10) ["c", "d", "e", "f", "g", 1, 2, 3, 4, 5] これも好みによるもので、どちらでも構いません。 join()メソッド 引数に文字列を指定し、配列の要素同士をその指定した文字列で繋げます。元の配列は変更されません。 const nums = [1, 2, 3, 4, 5]; console.log(nums.join(' * ')); //1 * 2 * 3 * 4 * 5 参考サイト https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/slice https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/splice https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/reverse https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/concat
- 投稿日:2021-06-21T00:53:04+09:00
Alpine.js 5分で説明するよ
はこです、こんにちわ。 今日は Alpine.js の話をします。 Alpine.js 何? Alpine(アルパイン: 英語風発音) は 『アルプスの・転じて登山用の』を意味します。 アルペン: Alpine のドイツ語読み。スノーレジャーやアウトドアグッズとかのお店。 アルパインブーツ・アルパインウェア: 雪山や高山に適した超軽量かつ高機能な服や靴。 モンベルのアルパインクッカー など…。 Alpine Linux: 超軽量Linux. Small Simple Secure が合言葉。 Alpine.js: 超軽量なフロントエンドフレームワーク。 当初は7kb(gzipped), 今は機能が増えてそれでも11kb Vueの58kbよりもかなり軽い 覚えることは20個のみ Vue, React, Angular 等の SPAツールの 超シンプル版だと思って間違いないです。 Alpineを名乗るだけあって、 軽量だけど高機能 って感じです。 合言葉は Simple. シンプル. Lightweight. 軽量. Powerful as hell. 鬼ヤバにパワフル. ざっくり概要 HTMLのシンタックスを使った、モダンWebにおけるjQuery(のような立ち位置のもの) 既存サイトからの部分的な改良も可( <script src="alpine.js"> 入れるだけ. vueと同じ) 覚えることは22個だけ。 22個の覚えること 公式に書いてあるよ x- から始まる 14個 の属性(アトリビュート) "Directives" $- から始まる 6個 のプロパティ "Magics" Alpine.data と Alpine.store の 2個のメソッド 以上!! 省略記法 めっちゃ使う やつだけは、省略記法(シュガーシンタックス)があります。 2個だけ! x-on: は @ @click="someMethod(...)" で使います x-bind: は : <div :class="closed ? 'hidden' : ''"> のように css classをつけたり、 <input type="text" :placeholder="'入力してください'"> のように html の属性に設定したりします。 これで覚えることはおしまい! すくない!! サンプル もうこれで完璧です! 覚えることの時間はおしまい! サンプルコードを読んでみましょう! シンプルなカウンタ <div x-data="{ count: 0 }"> <button x-on:click="count++">カウンタが増えるボタン</button> <span x-text="count"></span> <!-- 数字はここに出る --> </div> See the Pen wvJNdqK by Kohashi (@kohashi) on CodePen. はい!超シンプルです。 ボタンをクリックしたら、開く <!-- x-dataで書いたオブジェクトは子要素からアクセス可能、ここではopen状態の値を保持するやつ --> <div x-data="{ open: false }"> <!-- @click は onclick の代わりだよ。クリックしたら open=true を代入する(開くよ) --> <button @click="open = true" x-text="open ? '開いてるよ' : 'クリックで開くよ'"></button> <!-- x-show は 指定した変数や式が true なら、表示するよ --> <span x-show="open" @click.away="open = false" style="border: solid black 1px; padding: 10px;" > 開いたよ。この枠の外をクリックすると、閉じるよ。 </span> <!-- @click.away の .away は「ここ以外を xxしたら(clickしたら)」 の修飾子だよ。 「画面外をクリックしたら」でよく使うよね --> </div> See the Pen eYvoVpp by Kohashi (@kohashi) on CodePen. スタイルとか当ててない機能サンプルですが、これでダイアログボックスやタブが作れますね! HTMLテンプレートを使用しないカウンタ 「よりjsだけで書きたい!」ということも出来ます。 最初のカウンタのサンプルと同じですが、このように書くことも出来ますー。 // DOM参照して取ってくるだけ let button = document.querySelector('button'); let span = document.querySelector('span'); // Alpine.reactiveで、リアクティブなデータとして定義 let data = Alpine.reactive({ count: 1 }); // Alpine.effectで、リアクティブなデータが変更されると呼ばれる関数を設定します。 Alpine.effect(() => { span.textContent = data.count; }) // ここはバニラなJSですねー button.addEventListener('click', () => { data.count = data.count + 1 }) 詳しくは ドキュメント見てね。 その他のサンプル ここに色々のってるよ。 (注意: 2系のサンプルがまだ多いよ. 最新は3系だよ. でもあんま違いはないよ) 通信どうすんの? Fetch API 使うといいよ! fetch('url...') は IE以外では使えますし、ポリフィルもある ので実質全ブラウザで使えますね(暴言)。 表示する内容を fetch するサンプル <span x-text="getLabel()"></span> <script> async function getLabel() { let response = await fetch('/api/label'); // 任意のAPIのURL return await response.text(); // 戻り値を返す } </script> マウスオーバーしたときに、次のページ取得するサンプル <!-- open で開くよ。最初はfalseなので閉じてる --> <div x-data="{ open: false }"> <!-- マウスオーバーしたときに1度だけ(@mouseenter.once) fetchして、結果の中身のhtmlをnextPageDivに代入するよ --> <button @mouseenter.once=" fetch('/next-page.html') .then(response => response.text()) .then(html => { $refs.nextPageDiv.innerHTML = html }) " @click="open = true" >次のページへ!</button> <!-- クリックしたとき(open===trueのとき) 表示されるよ。 --> <div x-ref="nextPageDiv" x-show="open"> <!-- fetch完了前は「ページ読込中...」、完了後は next-page.htmlの中身が表示されるよ --> ページ読込中... </div> </div> 自分のディレクティブ作りたい! Alpine.directive でできるよ。 <script> // こういうのを定義する Alpine.directive('to-neko', el => { el.textContent = el.textContent + 'にゃ! ? ?' }) </script> <!-- こう使う --> <div x-to-neko>はろー</div> <!-- はろーにゃ! ? ? と表示される --> ドキュメント見てね ユースケース これは私見になるのですが、「既に構築された(大規模に作り直すことが困難な)Webサイトを部分的にインタラクティブにしたい」というケースには非常によくマッチすると思います。 以前はこのようなケースでは Vue.js が選ばれることが多かったように思いますが、Vueよりもシンプルながらよい作りのAlpineがより適してるでしょう。 逆に、「ゼロから作りたい!」というユースケースでも充分に役に立ちます。 シンプルなゆえに覚えることが少なく、サクッと作れるのは魅力です。 技術検証や最初のリリースとしては充分適しているでしょう。 反面、TypeScriptはサポート外であることや、高機能なcliは付属しないこと、htmlテンプレートのエディタのサポートの弱さや自動テストが困難な点など、大規模に多人数が関わって開発するのにはやや不足感は否めません。 しかし、関わる人が慣れてる人だけである場合や、小規模に高速に開発していくケースでは力になりそうです! まとめ Alpine.js は アルパインの名の通り、無骨で最小限ですが高機能なフロントエンドフレームワークでした。 また、同様に軽量CSSフレームワークの Tailwind CSS との相性が良さそうです。 どちらもシンプルで覚えることが少ないため、半日もあれば充分書けるようになるのではないでしょうか。 参考資料 公式 公式の日本語訳 https://github.com/alpinejs/alpine/blob/2.x/README.ja.md ちょっと更新が古いけど、そもそも覚えることが少ないから大丈夫だよ!
