- 投稿日:2021-04-30T23:08:46+09:00
Node.js+ExpressでWebアプリケーション開発 第1回
はじめに 仕事でNode.js+Expressを使うことになったので、勉強したことをメモしていきます。 環境 node v14.16.1 npm 6.14.12 Visual Studio Code Expressとは Node.js 向けの高速で最小限の Web フレームワークのことです。 詳しくは公式ドキュメントをご参照ください。 http://expressjs.com/ja/ プロジェクトの作成 フォルダを作成してVSCodeで開いてください。 各パッケージをインストール [Ctrl]+@でターミナルを開き、expressとejsをプロジェクトにインストールする。 npm install express ejs package.json を生成する npm init プロジェクトの下に「node_modules」と「package-lock.json」作成されればOK。 各ファイルの作成とソースコード 以下のディレクトリ構成になるようフォルダ&ファイルを作成します。 NodeExpTest │ package-lock.json │ package.json │ server.js ├─node_modules │ (省略) ├─public │ ├─css │ │ index.css │ │ │ ├─img │ └─js │ index.js ├─routes │ index.js └─views index.ejs server.js:Expressサーバの処理を定義。(一番初めに起動) public\css\index.css:index.ejsのCSSを定義。 public\js\index.js:index.ejsのクライアント側の処理を定義。 routes\index.js:index.ejsのサーバサイドの処理を定義。(Java相当) views\index.ejs:js:index.ejsの画面デザインを定義。(JSP相当) package.json { "name": "nodeexptest", "version": "1.0.0", "description": "", "main": "server.js", "dependencies": { "ejs": "^3.1.6", "express": "^4.17.1" }, "devDependencies": {}, "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "node server.js" }, "author": "", "license": "MIT" } server.js const express = require('express'); // express の実態 Application を生成 const app = express(); const port = 3000; // テンプレートエンジンを EJS に設定 app.set('views', './views'); app.set('view engine', 'ejs'); // 静的ファイルは無条件に公開 app.use('/public', express.static('public')); // ルーティング設定 app.use('/', require('./routes/index.js')); // HTTPサーバを起動する app.listen(port, function () { console.log(`listening at http://localhost:${port}`); }); // アプリケーション開始ログ console.log(`Server running at http://localhost:${port}`); routes\index.js const express = require('express'); const router = express.Router(); // デフォルトルーティング router.get('/', function (request, response) { response.render('index', { title: 'Sample NodeExpTest', message: 'Hello World!' }); }); module.exports = router; views\index.ejs <!DOCTYPE html> <html lang="ja"> <head> <title><%= title %></title> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="/public/css/index.css" /> <script type="text/javascript" src="/public/js/index.js"></script> </head> <body> <div class="container"> <p><%= message %></p> </div> </body> </html> 実行とデバッグ 作成されたlaunch.jsonを以下の通り設定します。 launch.json { "version": "0.2.0", "configurations": [ { "type": "node", "request": "launch", "name": "Launch Express", "program": "${workspaceFolder}\\server.js" } ] } F5をクリックし、実行します。 ブラウザを起動して、http://localhost:3000/にアクセスして以下のような画面が表示されればOKです。 また、server.jsやroutes\index.jsにブレークポイントを立てて、再度デバッグ実行してみましょう。 デバッグできれば成功です。 ひとまず、今回はここまで。 次回はGETとPOSTにチャレンジしたいと思います。 (続きはこちら) https://qiita.com/pocota5260/items/6f972b4455f609dbfeec ソースコードはこちら。 https://github.com/pocota5260/NodeExpTest
- 投稿日:2021-04-30T18:52:38+09:00
npm ws のクライアントインスタンス生成,接続時イベントの登録,接続手続きの順番
node.jsで簡単にwebsocketの機能をつかえるようになるライブラリwsを つかっていて,サーバーの方はまあわかるとして,クライアントClass: Websocketの挙動がというか,設計がわかりにくかったので実験したらふぇぇって感じだった. (とはいえまあサーバーの方で大量に管理するクライアントの記述を思うとこれでちょうどいいのかもしれない.) 気になった点 クライアントクラスの実行例として以下のようなモノがGitにある client const WebSocket = require('ws'); const ws = new WebSocket('ws://www.host.com/path'); ws.on('open', function open() { const array = new Float32Array(5); for (var i = 0; i < array.length; ++i) { array[i] = i / 2; } ws.send(array); }); これだけである.簡単!と思えればいいのだけど, 素朴に考えると,接続完了時に発行される'open'に対するEventを登録(ws.on())してから接続を開始する気がする. しかしながらこの例には続きがなく,これで完結しており,実際別プロセスでws.Serverのサンプルプログラムを動かしてからこれを実行すると,ちゃんと動く. .on()は継承(?)しているEventなんとかクラスのものであって,Websocketクラス固有のものではない. したがって接続を開始するとしたらコンストラクタでしかありえない. 実際ソースを見てみると,そんな感じだった. 詳しくは見ておらず,間違っている可能性も大いにあるが,流れとしては クライアント: コンストラクタでいろいろ調整 クライアント: おそらくhttpでupgradeが承認されたとき用のイベントを登録 クライアント: httpでupgradeをhttpサーバーに要請 ここでコンストラクタの処理が終わる. そしてhttpサーバーがwssocketサーバーにアップグレードした旨が伝わり次第,クライアントにopenイベントが発行される. という感じであった. すなわち,ws.on()はコンストラクタの処理が終わって,サーバーからupgradeが通知されるまでの間に行われないと,'open'イベントをキャッチできない. 実験 実際以下のようなプログラムで比較するとわかる.ちなみにサーバー側ではsend(data)で送った内容(data)がそっくりそのまま返ってくる. const websocket = require("ws"); const sock = new websocket("ws://127.0.0.1:5001", { perMessageDeflate: false }); const openev = () => { sock.on("open", () => { console.log("event as connected "); }); } sock.on("message", data => { console.log("event as getting message from the server"); console.log("echo: " + data); }); sock.on("close", () => { console.log("event as closed"); }); ↑ここまで共有 Case 1: console.log("ws state:" + sock.readyState); openev(); //<-コンストラクタが終わってすぐくらいにopen event時のコールバック設定 //1s後にデータを送ってから接続を終える. setTimeout(() => { console.log("ws state:" + sock.readyState); sock.send('a'); sock.close(); }, 1000); 結果: ws state:0 event as connected ws state:1 event as getting message from the server echo: a event as closed case 2: console.log("ws state:" + sock.readyState); //openev(); //1s後にデータを送ってから接続を終える. setTimeout(() => { console.log("ws state:" + sock.readyState); openev(); //<-1s後にopen event時のコールバック設定 sock.send('a'); sock.close(); }, 1000); 結果: ws state:0 ws state:1 event as getting message from the server echo: echo back: a event as closed case 1とcase 2を比較するとws state:0とws state:1との間にあったevent as connectedが表示されなくなっている. といわけで,なんだか意図しないデータの取りこぼしとか起こりそうだなと思ってなんか気もち悪い気がしたというわけでした. ちなみに,コンストラクタで接続を開始せずに,あとから接続を開始できる関数がないか探しましたが見当たりませんでした.というか検索していると再接続もできないので,別のを作ったり使ってる人が見受けられました.もし接続用の関数があれば再接続もできるはずなので,やはりないのでしょう. ここまで書いて誰かすでにQiitaで記事書いてそう.だけどいい練習になったのでよし.
- 投稿日:2021-04-30T18:05:35+09:00
TOTPの任意の時間のコードをNode.jsで出力する
2段階認証でxx AuthenticatorのようなアプリでTOTP(Time-Based One-Time Password)を表示しているが、 残り時間が少なく入力が間に合わない場合、次のパスワードが出るまで待つ必要がある。 次のパスワードも出してくれたらいいのに…と思い、Node.jsで出すようにしてみたときのメモ QRコード生成 Authenticatorアプリで同じパスワードが出ているか確認したいが、本当に使っているアカウントで試すのも躊躇われるので、アプリに登録するためのQRコードを作成する。 QRコードの内容は下記。パスワード生成に必要なSecretはTESTAPPSECRETAAAとする。 otpauth://totp/testapp:testuser@example.com?secret=TESTAPPSECRETAAA&issuer=testapp `otpauth:...のURIについてはGoogle AuthenticatorのKey-Uri-Formatを参照。 このURIからqrcode-terminalやqrcode等を利用してQRコードを生成する。 ワンタイムパスワード生成 npmパッケージのotplibを利用する。 otpSample.js const { authenticator } = require('otplib'); const SECRET = 'TESTAPPSECRETAAA'; console.log(authenticator.generate(SECRET)); console.log(`残り時間: ${authenticator.timeRemaining()}`); authenticator.options = {epoch: Date.now() + (30 * 1000)}; console.log(`次のコード: ${authenticator.generate(SECRET)}`); otplibはTOTP、HOTPのワンタイムパスワードを扱うライブラリーであるが、otplib.authenticator.generate(${Secret Key})で某Authenticator互換の設定がプリセットされた状態でパスワードが生成できる。 デフォルトでは現在時刻をベースにパスワードを生成するが、オプション(authenticator.options)のepochで任意の時刻を指定すればその時刻がベースになる。 パスワードの有効期間は30秒なので、現在時刻+30秒後の時刻を入れれば次のパスワードを取得できる。 残り時間がauthenticator.timeRemaining()で取得できるので一応表示する。 出力例 $ node otpSample.js 675136 残り時間: 5 次のコード: 793242 ※ 文中の社名、商品名等は各社の商標または登録商標である場合があります。 ※ Google and Google Authenticator are trademarks of Google LLC and this document is not endorsed by or affiliated with Google in any way. ※ QRコードは株式会社デンソーウェーブの登録商標です。
- 投稿日:2021-04-30T18:04:39+09:00
TOTPのQRコードをNode.jsのコンソールで出力する
2段階認証のワンタイムパスコードを生成するQRコードを自作したかったので、Node.jsスクリプトを作成した。 QRコード仕様 Google AuthenticatorのKey-Uri-Formatによると、QRコードの内容は下記とのこと otpauth://${TYPE}/${LABEL}?${PARAMETERS} TYPE: hotp(HMAC-Based One-Time Password Algorithm)かtotp(Time-Based One-Time Password Algorithm)。某Authenticatorはtotp LABEL: ${発行者}:${アカウント名}または${アカウント名}で記入。発行者が記入されているほうを推奨するとのこと PARAMETERS: secret=${SECRET}&issuer=${発行者}のように記入 secret: 必須。Base32文字列を記入。16文字が多い。アカウント毎に異なるものを指定する issuer: 強く推奨。LABELに記入した発行者と同じものを記入 algorithm: optional。アルゴリズム。デフォルトはSHA1 digits: optional。パスコードの長さ。6か8を指定可で、デフォルトは6 counter: TYPEがhotpであれば必須。HOTP用カウンター period: optional。TOTPでパスコードが有効な秒数を指定。デフォルトは30 PARAMETERSのoptionalで指定できるパラメーターはアプリによっては無視される様子。 QRコード出力 npmパッケージのqrcode-terminalを利用してスクリプトを作成した。QRコードがCLIで出力される。 otpQRGenerator.js const qrcode = require('qrcode-terminal'); const TYPE = 'totp'; const ISSUER = 'testapp'; const ACCOUNT_NAME = 'testuser@example.com'; const SECRET = 'TESTAPPSECRETAAA'; // SECRETを生成する場合、otplib(https://www.npmjs.com/package/otplib)で生成できる // const { authenticator } = require('otplib'); // const SECRET = authenticator.generateSecret(); const url = `otpauth://${TYPE}/${ISSUER}:${ACCOUNT_NAME}?secret=${SECRET}&issuer=${ISSUER}`; qrcode.generate(url, {small: true}, (qrcodeImage) => { console.log(qrcodeImage); process.exit(0); }); 実行例 $ node otpQRGenerator.js ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ █ ▄▄▄▄▄ █▄▀ ▀ ▄▄▀ ▄▄ █ ▄███▄█ ▄▄▄▄▄ █ █ █ █ █ █▀█▀▀▄▀▄█▄▀▀█▄ ▀▄█ █ █ █ █ █▄▄▄█ █▄█▀ ▄▀▄▀ ▄ █▄█ ██ █ █▄▄▄█ █ █▄▄▄▄▄▄▄█▄█ █ █▄▀ ▀ █ █▄▀ █ ▀▄█▄▄▄▄▄▄▄█ █ ▄ ▄▀▀▄█▄▀ ▄ ▀ █▄▄▄▀▀█▀██▄█▀ █▀▄▄▄▀▄█ (中略) ███████▄█▀█▄▄▀▀█▀██ ▀▀█▄▀▄▄▄▀ ▄▄▄ █ █ █ ▄▄▄▄▄ ██▄ ▄ ██▄▀▀█▀ ▄█▀▀▄ ▀ █▄█ █ █ █ █ █ █▀█▄▄ ▀█▀▄ ▄▀▀▀ ██▄█▄ █▀▀▀█ █ █▄▄▄█ █ ▀▄▄▄▄█▀ ▄ ▀▀▄█ ▄█▀▄ ▄ ███ █▄▄▄▄▄▄▄█▄▄██▄█▄▄▄▄███▄▄▄▄▄▄███▄▄██▄▄▄█ これをAuthenticatorのアプリで読み込むと testapp(testuser@example.com)で登録される。 QRコードはqrcode.generateのオプションに{small: true}を追加するとアスキーアートで表示されるが、フォントによってはうまく読み込めない。その場合はオプションを外すか、qrcode等で画像ファイルで出力したほうがよい。 ※ 文中の社名、商品名等は各社の商標または登録商標である場合があります。 ※ Google and Google Authenticator are trademarks of Google LLC and this document is not endorsed by or affiliated with Google in any way. ※ QRコードは株式会社デンソーウェーブの登録商標です。
- 投稿日:2021-04-30T15:31:36+09:00
時間経過につれて文字がかっこよく遷移するUIを作った件
時間経過につれて文字がかっこよく遷移するUIを作った件 フリーランスでWeb開発を行なっているozoraです。 時間経過につれて文字が遷移していくUIを公開しました。 自分のポートフォリオサイトのトップページでサンプルを公開しています。 自分のサイトでは"Hello there!" -> "こんにちは" -> ... と世界の国の挨拶を順に表示していくように設定しています。 Reactコンポーネントとして実装しているのでReactプロジェクトならどんなサイトでも利用できます。 git clone https://github.com/ozora-ogino/react-text-morpherで簡単に利用できます。 以下で公開していますのでよければ使ってみてください。 (GithubにStarもらえるとozoraは喜びます。)
- 投稿日:2021-04-30T15:21:33+09:00
openapi3-tsについて調べたことをまとめたい!
序 いろいろと前提を書き連ねますが、読むのが面倒な人は本まで飛んでください! 記事内容について この記事は「筆者(orifuji)がopenapi3-tsを試してみるシリーズ」の最初の投稿となります 今回は主にライブラリの仕様調査、およびOAS自体の説明を行います これで脱稿とするよりも、知見の増加・認識の更新に合わせてしばらくは編集を続けようと思っています 次回は実際に使ってみて、OpenAPI Specificationに則ったjsonないしyamlなファイルをアウトプットするところまでを扱います 最終的には、わたし自身のユースケースに沿ったラッパーライブラリを実装します 筆者の属性 TS初心者 会社のプロダクトではTSがWeb Frontendに採用されています ただ、自分はServerメインのため知見はさほどないです OpenAPI Specification知識はある 前職で新規プロジェクト開発に際して導入作業を一部行いました 筆者のTS学習も兼ねているため、言語仕様に知悉している方からすると冗長な記述が目立つかもしれません そもそもOpenAPI Specificationとはなにか OpenAPI Specification(OAS)は、実装(プログラミング言語等々)に依存しないinterface定義のためのWeb API仕様記述フォーマットです OASはyamlまたはjsonで表現できるように構造化された仕様になっています 仕様の表現それ自体とは別に得られる恩恵として ドキュメント生成ツール コード生成ツール テストツール (OASを読み込んでレスポンスを生成する)スタブサーバーツール などのような、OAS準拠のサードパーティツール群の恩恵に与れることが挙げられます なお、上記説明は公式の記述をもとに、自分の言葉に置き換えたものです なぜOASファイルをプログラムから生成するのか ざっくり下記のようなメリットがあるかと思います API定義ファイルがプログラマブルな対象になると、API定義ファイル内の共通化や(一本ファイルへ統合するための)ビルドなどが容易になる OAS自体はファイル分割に対応した仕様ですが、クライアントツールが追従していない場合もあるため、分割→再統合のためのシステムを自前で組むことになりがち プログラムで書く場合、プログラム内で分割や共通化を扱っておいて、それをファイル生成時に一本化するだけなので、上記のようなシステムの構成をあまり気にしなくて良くなる (開発言語選定次第では)強力な静的解析・補完の恩恵に与れる 対してデメリットは stoplight studioなどのOASをターゲットとしたエディタのパワフルな機能に頼れなくなる ある程度、自力でOASに詳しくなる必要が出てきます TSの選定理由 だいたいこのような理由を総合して、とりあえず試しにTypeScriptでやってみようかなと決めました ライブラリ(openapi3-ts)を見つけた 静的型付け言語である VSCode上で簡単に扱える Web Frontendを中心に、TS経験のある開発者の人口が多い 特に自分の場合は、プロダクトチーム内のほとんどの開発者が扱えるため、TSを選びました チームで実際に採用するかは分かりませんが、巻き込める状況が大事かなと思ってのことです わたし自身がTSに慣れ親しみたい ちなみに、OAS - TSの組み合わせのベネフィットに関しては夕暮おこはさんの記事が最高にわかりやすいです! Acknowledgment @suin さんの下記リポジトリを実用事例として参考にしました 仕様把握・コードリーディングに際して、めちゃくちゃ助かりました 感謝しております 本 以下では、openapi3-tsの仕様の概観を述べます 概要 openapi3-tsは、最新のOASの仕様に準拠しているライブラリで、下記が特徴です OASの各種コンポーネントを、TypeScriptの型として表現している それらを用いつつ、実体としてのOASを組み上げるためのAPIを提供している 提供されているクラス・interface ライブラリのディレクトリ構造に、簡潔に表現されています 下記の二つのサブディレクトリが存在して、それぞれの責務は次に付記する通りです src/model OpenAPI SpecificationをTypeScriptの型定義で表現するためのinterface群 src/dsl 上記モデルをインスタンス化するための操作を、DSL(domain-specific language)的に実装したBuilderクラス src/index.tsで、ライブラリが公開するfunction, class, interfaceなどがexportされています 基本的にこれを使う側で拾ってあげれば良いかと思います How to use ライブラリ本体に現状Docがないので、簡単にHow to useを整理したいと思います わたし個人のメモ程度の代物であることを念頭にお読みいただければさいわいです How to install 為念 npm管理できるので、npm installやyarn addでプロジェクトに導入しましょう OASの構造と対応したパーツをひとつひとつ作ってゆく まずは個別のendpoint = pathを作ったり、共通部分(汎用的なヘッダやレスポンスボディ構造など)を括り出したりしてゆきます 以下はイメージを掴んでもらうための疑似コードです ランタイムでの動作を確認していません 次回以後、実際に動作するコードを書きますのでお待ちください。また、こちらも適宜修正してゆきます const tag_todos: TagObject = { description: "TODOに関するエンドポイント", "x-displayName": "TODO", }, const schema_todos_200: SchemaObject = { // 長くなるためいったん省略(あとでそれらしいコードにしておきます) } // このPathItemObjectが個別のエンドポイントの仕様定義になります const path_todos: PathItemObject = { get: { summary: "やること一覧を取得する", operationId: "get-todos", tags: [tag_todos], responses: { "200": schema_todos_200, }, }, }; OpenApiBuilderで、OASに準拠した構造のオブジェクトをインスタンス化する Builderにそれぞれのパーツを渡し、OAS全体を作り上げてゆきます // 予めoas_builderを初期化している想定で書いています // getSpec()はOpenApiBuilderのメンバーを元に、OAS準拠のオブジェクトを返却します const oas: OpenAPIObject = oas_builder.addPath(path_todos).getSpec(); インスタンスを別のプログラムに渡してあげる 例えば、jsonファイルの出力を行うモジュールに渡してあげます // それをjson形式に変換して const output_json: string = JSON.stringify(oas); // ここでは新規ファイル作成用に作っておいたラッパー関数に渡す想定で書いています writeNewFile("./docs/oas_my_todo_app.json", output_json); 結 openapi3-tsの概観は掴めたかなと思います 次回からは、実用に入ります 簡単なAPI定義を想定しつつ、↓のようなことをしてみようかなと思います 愚直にTSで仕様書を組む 使いやすくするために、ライブラリをラップし、拡張的な操作を加えてゆく 前回:- 次回:
- 投稿日:2021-04-30T14:23:59+09:00
Azure Communication Services ことはじめ (4) : Microsoft Teams 会議にビデオ通話参加ができるまで
Azure Communication Services (以下 ACS) は、リアルタイム コミュニケーション基盤となるサービスで、テキスト | 音声 | ビデオ によるコミュニケーションのハブとなり、接続やコントロールを行うアプリやサービスを SDK などを用いて容易に開発できます。 今回は、2021 年 3 月 から ACS に追加された Microsoft Teams 会議参加ができる機能を使って、Teams 会議にビデオ通話(音声+カメラ動画)で参加できるまでの手順を追って確認し、Node.js の Web アプリを作成します。 ひとまず Teams 会議に音声で参加できるまでの手順はこちらに掲載しています: Azure Communication Services ことはじめ (3) : Microsoft Teams 会議に音声参加ができるまで 開発環境 開発環境 Windows 10 (20H2 - Build 19042) Visual Studio Code (ver 1.55.2) Node.js (12.18.4) 利用ライブラリー、フレームワークなど Azure Communication Services azure/communication-common@1.0.0-beta.5 azure/communication-calling@1.0.0-beta.10 azure/communication-identity@1.0.0 Webpack webpack@4.42.0 webpack-cli@3.3.11 webpack-dev-server@3.10.3 0. 事前準備 Azure Communication Services サービス Azure Communication Services ことはじめ (1) : チャットができるまで の 0.事前準備 と同様に、Azure Portal で ACS のサービスを作成し、接続文字列とエンドポイントを取得しておきます。 Microsoft Teams tenant interoperability (相互運用) 有効化 Azure Communication Services ことはじめ (3) : Microsoft Teams 会議に音声参加ができるまで の 0.事前準備 と同様に申請を行い、有効化しておきます。 1. ビデオ通話ができるまで (ライブラリーの使い方) Azure Communication Services ことはじめ (3) : Microsoft Teams 会議に音声参加ができるまで との差分は、動画ストリームのコントロールです。 1-0. ライブラリの追加 以下のライブラリを冒頭に追加します。動画ストリームを扱うモジュール(VideoStreamRenderer, VideoStream)が追加になります。 ライブラリ azure/communication-common ユーザーの作成、アクセストークン取得 azure/communication-identity (同上) azure/communication-calling 音声およびビデオ通話のコントロール import { AzureCommunicationTokenCredential } from '@azure/communication-common'; import { CommunicationIdentityClient } from "@azure/communication-identity"; import { CallClient, VideoStreamRenderer, LocalVideoStream } from "@azure/communication-calling"; 1-1. ユーザーを作成し、アクセストークンを取得する Azure Communication Services ことはじめ (3) 1-1 と全く同様です。 1-2. 通話クライアントを作成 Azure Communication Services ことはじめ (3) 1-2 と全く同様です。 1-3. Teams 会議に参加する CallAgent から、参加したい Teams 会議にアクセスします。Teams 会議 URL がそのまま引数になります。 PCカメラの画像を送る場合は、CallClient から DevideManager を呼び出し、デバイス(PC)のカメラ情報(VideoDevices)を取得します。カメラを指定して (※環境に合わせて videoDevices[x] の x 値を変更してください)、LocalVideoStream として取得し、Teams 会議情報に追加します。 client.js let call let deviceManager; let localVideoStream; deviceManager = callClient.getDeviceManager() const videoDevices = deviceManager.getCameras(); const videoDeviceInfo = videoDevices[0]; localVideoStream = new LocalVideoStream(videoDeviceInfo); call = callAgent.join( { meetingLink: "TEAMS_MEETING_URL" }, { videoOptions: { localVideoStreams: [localVideoStream] } }); Teams 会議 URL は以下のような URL をエンコードしたものになります。 https://teams.microsoft.com/l/meetup-join/19:meeting_xxx...xxx@thread.v2/0?context={"tid":"xxx...xxx","oid":"xxx...xxx"} 1-4. Teams 会議の動画をコントロールする 1-4-1. PC カメラからの動画を使用 LocalVideoStream から VideoStreamRenderer で View を作成し、HTMLElement として append すれば OKです。 let rendererLocal; rendererLocal = new VideoStreamRenderer(localVideoStream); const view = await rendererLocal.createView(); document.getElementById("myVideo").appendChild(view.target); 1-4-2. Teams 会議 (の参加者) の動画を使用 Teaams 会議の動画である RemoteVideoStream を取得したら、VideoStreamRenderer で View を生成して HTMLElement として append するという流れは同様です。 let rendererRemote; rendererRemote = new VideoStreamRenderer(remoteVideoStream); const view = await rendererRemote.createView(); document.getElementById("remoteVideo").appendChild(view.target); RemoteVideoStream は 会議情報をストアする Call から取得します。Teams会議参加者(Allay)情報から動画ストリームを取り出します。参加者の動画が切断されたら(remoteVideoStream.on('isAvailableChanged'...)、RemoteVideoStream を破棄します。 //CallAgent callAgent; let call; call = callAgent.join({ meetingLink: meetingUrlInput.value }); subscribeToRemoteParticipantInCall(call) function subscribeToRemoteParticipantInCall(call) { call.on('remoteParticipantsUpdated', e => { e.added.forEach( p => { subscribeToParticipantVideoStreams(p); }) }); call.remoteParticipants.forEach(p => { subscribeToPaticipantVideoStreams(p); }) } function subscribeToParticipantVideoStreams(remoteParticipant){ remoteParticipant.on('videoStreamsUpdated', e=> { e.added.forEach(v => { handleVideoStream(v); }) }); remoteParticipant.videoStreams.forEach(v => { handleVideoStream(v); }); } function handleVideoStream(remoteVideoStream) { remoteVideoStream.on('isAvailableChanged', async () => { if (remoteVideoStream.isAvailable) { remoteVideoView(remoteVideoStream); } else { rendererRemote.dispose(); } }); if (remoteVideoStream.isAvailable) { remoteVideoView(remoteVideoStream); } } 2. Web アプリの開発 (手順) 以上の手順を踏まえて、Visual Studio Code で Node.js Web アプリを作成します。 2-1. 新規 Node.js アプリの作成 Node.js アプリを作成するフォルダーを作成し、Visual Studio Code で開き、npm init コマンドで package.json を作成します。 npm init -y 2-2. Azure Communication Services のライブラリのインストール npm install コマンドで Azure Communication Services の必要なライブラリ (パッケージ) をインストールします。 npm install @azure/communication-common@1.0.0-beta.5 --save npm install @azure/communication-identity@1.0.0 --save npm install @azure/communication-calling@1.0.0-beta.10 --save 2-3. Webpack のインストール 今回は Webpack を利用して、JavaScript のモジュールバンドル & ローカルでの実行確認 を行います。 npm install コマンドで、webpack、webpack-cli、webpack-dev-server をインストールします。 npm install webpack@4.42.0 webpack-cli@3.3.11 webpack-dev-server@3.10.3 --save-dev 今回は Azure Communication Services ドキュメント 推奨バージョンを指定してインストールしています。 2-4. コーディング 画面 (index.html) ユーザーからの操作および音声入出力、各種情報を表示するため、index.html という名前でファイルを作成し、以下のような UI を作成します。 コードは以下になります。 ACSTeamsVideoWeb202104/index.html カメラ動画 & 音声 を使った通話機能 (client.js) client.js という名前でファイルを作成し、Teams 会話 (カメラ動画、音声) をコントロールする機能を記述します。 後ほど client.js を index.js にビルドして index.html で読み込みます。 利用ライブラリー 今回は、これらのライブラリーを利用します。 client.js import { AzureCommunicationTokenCredential } from '@azure/communication-common'; import { CommunicationIdentityClient } from "@azure/communication-identity"; import { CallClient, Features } from "@azure/communication-calling"; 接続文字列 client.js に connectionString を記載しておきます。 YOUR_CONNECTION_STRING は 事前準備で取得した 接続文字列に置き換えてください。 セキュリティの観点から別の設定ファイルなどに記載して読み出すのが一般的ですが、今回は動作を確認するのみのアプリなので本体に記載しています。 client.js let connectionString = "YOUR_CONNECTION_STRING"; UI 画面の入出力 Web 画面から Teams 会議リンク(URL) の入力を取得できるようにします。 また、Teams 会議の参加、終了のボタンのクリック、および カメラ動画のオン、オフボタンのクリックを取得できるようにします。 出力は、UserId、UserToken、動作に対するメッセージ(messageOutput)を表示するようにしておきます。 動画(自分の PC カメラ動画および Teams 会議動画) は後ほど設定します client.js // 入力 const meetingUrlInput = document.getElementById("meeting-url-input"); // Teams 会議 URL const joinMeetingButton = document.getElementById("join-meeting-button"); // Teams 会議参加ボタン const hangUpButton = document.getElementById("hang-up-button"); // Teams 会議退出ボタン const startVideoButton = document.getElementById("start-video-button"); // カメラ動画開始ボタン const stopVideoButton = document.getElementById("stop-video-button"); // カメラ動画停止ボタン // 出力 const callerIdOutput = document.getElementById("caller-id-output"); // ACS UserId const callerTokenOutput = document.getElementById("caller-token-output"); // ACS UserToken const messageOutput = document.getElementById("message-output"); // 状態メッセージ CommunicationUser の新規作成、Token の取得、CallAgent の生成 接続文字列から User を新規作成して Token を取得します。Token を利用して、Teams 会議のコントロールを行う CallAgent を生成します。CAllAgent が無事生成出来たら、joinMeetingButton (Teams会議参加ボタン) をクリック可能(disabled = false)にします。 client.js // ACS User の作成とトークンの取得 const identityClient = new CommunicationIdentityClient(connectionString); const callClient = new CallClient(); let callAgent; let call; identityClient.createUser().then(identityResponse => { callerIdOutput.value = identityResponse.communicationUserId; // ACS User Id 画面出力 identityClient.getToken(identityResponse, ["voip"]).then(tokenResponse => { const userToken = tokenResponse.token; callerTokenOutput.value = userToken; // ACS User Token 画面出力 messageOutput.innerText += "Got user token."; // 状態メッセージ画面出力 const tokenCredential = new AzureCommunicationTokenCredential(userToken); callClient.createCallAgent(tokenCredential, {displayName: 'ACS user'}).then(async agent => { callAgent = agent; deviceManager = await callClient.getDeviceManager() joinMeetingButton.disabled = false; // Teams 会議参加ボタンをクリック可能に messageOutput.innerText += "\nReady to join MSTeam's Meeting."; // 状態メッセージ画面出力 }); }); }); 動画の設定 自分の PC カメラ動画 および Teams 会議動画 の操作を設定します。 client.js let deviceManager // PCカメラを操作 let localVideoStream; // PCカメラの動画ストリーム let rendererLocal; // PCカメラの動画レンダラー let rendererRemote; // Teams 会議の動画レンダラー // 自分の PC カメラ動画を配置 async function localVideoView(){ rendererLocal = new VideoStreamRenderer(localVideoStream); const view = await rendererLocal.createView({ scalingMode: 'Crop'}); document.getElementById("myVideo").appendChild(view.target); } // Teams 会議の参加者の動画を配置 async function remoteVideoView(remoteVideoStream) { rendererRemote = new VideoStreamRenderer(remoteVideoStream); const view = await rendererRemote.createView({ scalingMode: 'Crop'}); document.getElementById("remoteVideo").appendChild(view.target); } // Teams 会議の動画有無、参加者の増減による変更をハンドリング // Teams 会議への参加者が確認出来たら動画ストリームを取得 function subscribeToRemoteParticipantInCall(callInstance) { callInstance.on('remoteParticipantsUpdated', e => { e.added.forEach( p => { subscribeToParticipantVideoStreams(p); }) }); callInstance.remoteParticipants.forEach(p => { subscribeToPaticipantVideoStreams(p); }) } function subscribeToParticipantVideoStreams(remoteParticipant){ remoteParticipant.on('videoStreamsUpdated', e=> { e.added.forEach(v => { handleVideoStream(v); }) }); remoteParticipant.videoStreams.forEach(v => { handleVideoStream(v); }); } // 取得したストリームを配置 function handleVideoStream(remoteVideoStream) { remoteVideoStream.on('isAvailableChanged', async () => { if (remoteVideoStream.isAvailable) { remoteVideoView(remoteVideoStream); } else { rendererRemote.dispose(); } }); if (remoteVideoStream.isAvailable) { remoteVideoView(remoteVideoStream); } } Teams 会議への参加 joinMeetingButton (Teams 会議参加ボタン) がクリックされたら、callAgent から Teams 会議 (meeting-url-input TextBox から取得) への参加をリクエストします。また、PC カメラを取得し、カメラ動画 と Teams 会議動画 を出力します。 call のステータスを確認して、会議参加リクエストの承認、会議参加の状態を表示します。 client.js joinMeetingButton.addEventListener("click", async () => { // PC カメラの取得 const videoDevices = await deviceManager.getCameras(); const videoDeviceInfo = videoDevices[0]; // PC カメラ動画の出力 localVideoStream = new LocalVideoStream(videoDeviceInfo); localVideoView(); // カメラ動画の開始、停止ボタンのステータス変更 startVideoButton.disabled = true; // カメラ動画開始ボタンをクリック不可に stopVideoButton.disabled = false; // カメラ動画停止ボタンをクリック可能に // Teams 会議参加リクエスト call = callAgent.join( { meetingLink: meetingUrlInput.value }, { videoOptions: { localVideoStreams: [localVideoStream] } } ); // Teams 会議動画の出力 subscribeToRemoteParticipantInCall(call); // 会議参加ステータスのチェック call.on('stateChanged', () => { messageOutput.innerText += "\nMeeting:" + call.state; }) // 各ボタンのステータス変更、状態メッセージ表示 hangUpButton.disabled = false; // Teams 会議退出ボタンをクリック可能に joinMeetingButton.disabled = true; // Teams 会議参加ボタンをクリック不可に }); Teams 会議からの退出 hangupButton (Teams 会議退出ボタン) がクリックされたら、callAgent の Teams 会議を切断します。 client.js hangUpButton.addEventListener("click", () => { // Teams 会議から退出 call.hangUp(); // 動画レンダラーの終了 rendererLocal.dispose(); // 各ボタンのステータス変更、状態メッセージ表示 hangUpButton.disabled = true; // Teams 会議退出ボタンをクリック不可に joinMeetingButton.disabled = false; // Teams 会議参加ボタンをクリック可能に stopVideoButton.disabled = true; // カメラ動画停止ボタンをクリック不可に messageOutput.innerText += "\nNow hanged up."; // 状態メッセージ画面出力 }); PCカメラ動画の開始、停止 startVideoButton(PCカメラ動画開始)、stopVideoButton(PCカメラ動画停止) がクリックされたら、カメラ動画を開始または停止します。 client.js startVideoButton.addEventListener("click", async () => { await call.startVideo(localVideoStream); localVideoView(); startVideoButton.disabled = true; // カメラ動画開始ボタンをクリック不可に stopVideoButton.disabled = false; // カメラ動画停止ボタンをクリック可能に }); stopVideoButton.addEventListener("click", async () => { await call.stopVideo(localVideoStream); rendererLocal.dispose(); startVideoButton.disabled = false; // カメラ動画開始ボタンをクリック可能に stopVideoButton.disabled = true; // カメラ動画停止ボタンをクリック不可に }); 最終的なコードはこちらになります。 ACSTeamsVideoWeb202104/client.js 3. Teams 会議へのビデオ通話参加 を試してみる 今回は Webpack を利用しているので、client.js を index.js にビルドして起動します。 npx webpack-dev-server --entry ./client.js --output index.js --debug --devtool inline-source-map 起動したら、ブラウザーから http://localhost:8080 にアクセスします。 User Id と Token が取得できると [Join Teams Meeting] のボタンがアクティブになります。 Teams 会議へのビデオ通話参加のチェック Teams 会議を作成し、参加 URL を取得します。 Teams 会議参加 URL を入力して Join Team's Meeting をクリックします。 Teams 会議(主催者側)に参加リクエストが表示されたら許可します。 会議に接続され、お互いに双方のカメラ画面が表示されます。 [Hang Up] をクリックすると会議から退出します。
- 投稿日:2021-04-30T13:05:30+09:00
DockerコンテナをつかったNodeJS開発・実行環境
DockerをつかったNodeJS開発・実行環境 NodeJSの開発をしているとき、実行環境を個別に作って残しておきたかったり、Nodeバージョンをコロコロ変えたかったりしたいので、Dockerコンテナを使ってNodeJSの開発・実行環境を作れるようにしました。 実行のたびにクリーンなコンテナを立てて、実行終わったらコンテナごと消すやり方もありますが、今回はコンテナ立てたまま動かすやり方です。 使い方としては、 開発始める前にコンテナ立てる 開発中はコンテナ立てっぱなしで、execすることでNodeJSを実行する コードの編集はホスト側(Windows/MacOS)でやる コンテナ作り直したかったらdocker-compose downで落として、docker-compose upする という形を想定しています。 Docker Composeでコンテナ立てて動かす 軽量なAlpine上でNodeJSのv16系を動かしたイメージにします。 以下のdocker-compose.ymlを作成。 docker-compose.yml version: '3' services: app: image: node:16-alpine container_name: node-container tty: true volumes: - ./src:/src working_dir: "/src" これをプロジェクトのルートフォルダにおいて、./src/以下にソースコードやpackage.jsonを置くようにします。 コンテナをたてて実行するときは、以下コマンド。 これで、コンテナが立ちっぱなしになります。 bash docker-compose up -d 必要なライブラリをインストール(npm install)する 上のdocker-compose.ymlで、 app: というかたちで、名前がappのサービスを作ったので、そのサービスを指定することで、コマンドを実行できます。 npm install をしたいので、docker-compose execを使い、 bash docker-compose exec app npm install で、さっき立てたコンテナ上でnpm installすることができます。 execで立ってるコンテナ上でプログラムを実行 npm installと同じく、docker-compose execを利用します。 bash docker-compose exec app node main.js もしくは、package.jsonをしっかり書いているようであれば、 bash docker-compose exec app npm start で、プログラムを実行できます。 コード書いて開発していく docker-compose.ymlのvolumesの欄で、./srcディレクトリをコンテナ内にマウントしてあるので、ホスト側(Windows/MacOS側)で編集した内容が、常にコンテナ内にも反映されるようになっています。 なので、Windows/MacOS上で./src/main.jsなどのソースコードをもりもり編集し、 docker-compose exec app npm start することで、もりもり開発していけます。 コンテナ立てなおす 試行錯誤するうちにコンテナの環境が汚れてくることもあるので、その時はコンテナ立てなおします。 それも簡単で、docker-compose.ymlがあるディレクトリで、 bash docker-compose down docker-compose up -d を実行すると、今まで使ってたコンテナを捨てて、新しいコンテナを立てなおすことができます。 備考 都度都度コンテナを立てて実行する NodeJSのpackage.jsonをこんな感じにします。。 ポイントはscript.clean-startのところ。 node_modulesディレクトリを消して、モジュールインストールして、Nodeを実行する流れです。 package.json { "scripts": { "start": "node ./main.js", "clean-start": "rm -r ./node_modules && npm install && npm start" }, "dependencies": { } } これで、以下のコマンドたたくと、コンテナ立てて、上記の処理を行い、終わったらコンテナを都度都度消してくれます。 bash docker-compose run --rm app npm run-script clean-start 参考にしたリンク https://qiita.com/nikadon/items/995c5705ff1171f7484d https://docs.docker.jp/compose/compose-file.html
- 投稿日:2021-04-30T10:03:37+09:00
【Jest】名前付きインポートしたクラスとそのメソッドをそれぞれモック化する
例として@slack/webhookパッケージを使ってslackにメッセージを送るコードをテストする。 // IncomingWebhookクラスを名前付きインポートして import { IncomingWebhook } from '@slack/webhook'; const webHookUrl = 'https://hooks.slack.com/services/~' // 1. IncomingWebhookをインスタンス化して const webhook = new IncomingWebhook(webHookUrl); // 2. sendメソッドで通知を送る webhook.send({ text: 'メッセージ内容', }); 1,2をテストするために、名前付きインポートしたクラスとsendメソッドをそれぞれモック化する。 import { IncomingWebhook } from '@slack/webhook'; const mockSend = jest.fn(); jest.mock('@slack/webhook', () => ({ IncomingWebhook: jest.fn(() => ({ send: mockSend })), })); 下のようなマッチャでテストできる。 // IncomingWebhookが1回インスタンス化された expect(IncomingWebhook).toHaveBeenCalledTimes(1); // sendメソッドが1回実行された expect(mockSend).toHaveBeenCalledTimes(1); メソッドだけでいいなら sendメソッドだけモック化するなら下のようにも書ける import { IncomingWebhook } from '@slack/webhook'; const mockSend = jest .spyOn(IncomingWebhook.prototype, 'send') .mockImplementation();
- 投稿日:2021-04-30T02:27:20+09:00
Node.jsでWindowsのショートカット(.lnkファイル)を作成する
いきさつ ユーザー権限で作成・削除が可能なリンクが欲しかった。 ポータブルアプリでスタートアップの登録解除が可能になる。 方法 PowerShellのショートカット作成コマンドをchild_process.execから実行する。 ショートカットなので、ファイルでもディレクトリでも作成可能。 削除する場合は、fs.unlinkでショートカットのパスを指定すればよい。 code.ts import { exec } from "child_process" // 作成したいショートカットのパス // 末尾の.lnkは必要 let dist = "/path/to/shortcut.lnk" // リンク先としたいパス let source = "/path/to/target" let command = ` $WshShell = New-Object -ComObject WScript.Shell; $ShortCut = $WshShell.CreateShortcut("${dist}"); $ShortCut.TargetPath = "${source}"; $ShortCut.Save(); ` exec(command, {'shell':'powershell.exe'}) 参考サイト ショートカット(lnk)の作成 - yanor.net/wiki Using PowerShell to Create a Shortcut - ComputerPerformance