- 投稿日:2020-11-26T19:53:54+09:00
壁打ちnodeサーバーをローカル上に立ててスクレイピングする方法(ExpressJSコードサンプルもあり)
件の通り。初心者向けだと思います。nodeサーバーをローカルに立てて、そのサーバーへスクレイピングを走らせます。
(時短でとにかく立てたい人は記事の一番下にコードサンプルを置いているExpressJSのやり方がオススメです。学習もかねてであれば最初から読んだ方が良いと思います。)nodeで色々とやる上でlocal上でできればサクッと動かしてみたい人は多いと思いますがnodeサーバーサイドでそうした気の利いた?記事はなかったのでまとめました。
献立
①. nodeを実行する環境を準備する
②. nodeサーバーでホスティングするページを準備する
③. nodeサーバーを立てるコードを書いて実行する
④. スクレイピング用のコードを書く①. nodeを実行する環境を準備する
任意のところでnodeServerなど適当な名前でファイルを作成してそのディレクトリへ移動します。
mkdir nodeServer cd nodeServernode自体が入っていない場合は「node 実行環境」などでググりましょう。
今回はシンプルに扱えるscraperjsのライブラリを使用します。
作成したディレクトリ上でインストールします。yarn add scraperjs or npm install --save scraperjsインストールが完了したらファイルをリロードします
②. nodeサーバーでホスティングするページを準備する
pagesファイルを作成してその中にsample1.htmlとsample2.htmlを置きます
mkdir pages cd pages touch sample1.html touch sample2.html cd ..中身は以下のようにします。
sample1.html<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <h1>サンプル1のページです</h1> <h2>このページはaboutとします。</h2> </body> </html>sample2.html<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <h1>サンプル2のページです</h1> <h2>このページはprofileとします。</h2> </body> </html>③. nodeサーバーを立てるコードを書いて実行する
sample1.htmlのみを読み込んでみる
上記のhtmlを読み込むnodeサーバーを立てます。まずは簡易的に説明したいのでsample1.htmlのみを読み込むnodeサーバーを立てます。
まずは親ディレクトリからnodeserver.jsファイルを作成します。
touch nodeserver.jsnodeserver.jsconst http = require('http'); // nodeにデフォルトで入っている http ライブラリ const fs = require('fs'); // nodeにデフォルトで入っているファイル操作の fs ライブラリ const server = http.createServer(function(req, res) { //サーバーを作成 if(req.url === '/about') { // リクエストされたページがaboutの場合にsample1.htmlを呼び出す fs.readFile('./pages/sample1.html','utf-8', function(error, data){ //fs.readFile(オプション)でsample1.htmlをutf-8の文字コードとして呼び出す。 if(error) throw error; res.writeHead(200, {'Content-Type': 'text/html'}); res.write(data); //帰ってきたデータを /about ページへ書き込む res.end(); }) } else { const msg = 'このページは存在しません'; res.writeHead(200, {'Content-Type': 'text/plain; charset=utf-8'}); res.write(msg); res.end(); } }); server.listen(3000); // ポート番号3000でサーバーを起動次に親ディレクトリから上記コードを実行します
node nodeserver.js or node nodeserver次にブラウザのURLに http://localhost:3000/about と打ってみましょう。
上記の通りHTMLが出力されていればOKです。
switch構文と共にページ毎にHTMLを読み込んでみる
次に各ページを読み込めるようリファクタリングします。
pagesのディレクトリに404.htmlを新たに作成しました。sample1.htmlなどをコピペして文章を好きに改変してください(このページは存在しませんやNOT FOUNDなど)
nodeserver.jsconst http = require('http'); const fs = require('fs'); const renderFunc = (res, data) => { //何度も呼び出す処理を関数へ res.writeHead(200, {'Content-Type': 'text/html'}); res.write(data); res.end(); return; } const server = http.createServer(function(req, res) { switch(req.url) { case '/about': fs.readFile('./pages/sample1.html','utf-8',function(error, data){ if(error) throw error; renderFunc(res, data); }) break; case '/profile': fs.readFile('./pages/sample2.html','utf-8',function(error, data){ if(error) throw error; renderFunc(res, data); }) break; default: fs.readFile('./pages/404.html','utf-8',function(error, data){ //例外ページ if(error) throw error; renderFunc(res, data); }) break; } }); server.listen(3000);再度立ち上げ直しましょう。以下を読み込めていればOKです。
このサーバーは立てたままにします。
当然ながら立てたサーバーに対して任意のアクションを起こせます。
今回はスクレイピングしてみましょう。④. スクレイピング用のコードを書く
立てたサーバーのHTMLへ向けてスクレイピングの準備をします。そのためのファイルを作成しましょう。
touch scraping.js親ディレクトリにscraping.jsを作ります。
scraping.jsconst scraperjs = require('scraperjs'); const pages = ['about', 'profile']; // 対象にしたいページの配列 pages.map(page => { // map関数で順番にpagesを取り出す const url = `http://localhost:3000/${page}`; scraperjs.StaticScraper.create(url).scrape(($) => { // scraperjsのStaticScraperメソッドを使いスクレイピング let title = $('h1').text(); // jQueryを使うr let description = $('h2').text(); return {title, description}; }).then(result => { return console.log(result); }).catch((error) => { console.error('Error:', error); }) })コマンドラインに以下のオブジェクトが出力されました。
node scraping.js { title: 'サンプル1のページです', description: 'このページはaboutとします。' } { title: 'サンプル2のページです', description: 'このページはprofileとします。' }壁打ちにする
他に使ってみたいライブラリやモジュールがあれば別にファイルを作って壁打ちできます。
おまけ:ExpressJSでサーバーを立てる場合
同じことをしているわけですがかなりスッキリと書けます。このサーバーでもscraping.jsを走らせることができます。
(yarn add express もしくは npm install --save express でライブラリはインストールしましょう。)expressServer.jsconst express = require('express'), app = express(); app.set("port", 3000); app.get("/about", (req, res) => { res.sendFile(__dirname + "/pages/sample1.html"); }); app.get("/profile", (req, res) => { res.sendFile(__dirname + "/pages/sample2.html"); }); app.get('*', function(req, res){ res.sendFile(__dirname + "/pages/404.html"); }); app.listen(app.get("port"), ()=>{ console.log(`Server リスナ: ${app.get("port")}監視中`); })
- 投稿日:2020-11-26T19:53:54+09:00
壁打ちnodeサーバーをローカル上に立ててスクレイピングする方法
件の通り。初心者向けだと思います。nodeサーバーをローカルに立てて、そのサーバーへスクレイピングを走らせます。
nodeで色々とやる上でlocal上でできればサクッと動かしてみたい人は多いと思いますがnodeサーバーサイドでそうした気の利いた?記事はなかったのでまとめました。献立
①. nodeを実行する環境を準備する
②. nodeサーバーでホスティングするページを準備する
③. nodeサーバーを立てるコードを書いて実行する
④. スクレイピング用のコードを書く①. nodeを実行する環境を準備する
任意のところでnodeServerなど適当な名前でファイルを作成してそのディレクトリへ移動します。
mkdir nodeServer cd nodeServernode自体が入っていない場合は「node 実行環境」などでググりましょう。
今回はシンプルに扱えるscraperjsのライブラリを使用します。
作成したディレクトリ上でインストールします。yarn add scraperjs or npm install --save scraperjsインストールが完了したらファイルをリロードします
②. nodeサーバーでホスティングするページを準備する
pagesファイルを作成してその中にsample1.htmlとsample2.htmlを置きます
mkdir pages cd pages touch sample1.html touch sample2.html cd ..中身は以下のようにします。
sample1.html<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <h1>サンプル1のページです</h1> <h2>このページはaboutとします。</h2> </body> </html>sample2.html<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <h1>サンプル2のページです</h1> <h2>このページはprofileとします。</h2> </body> </html>③. nodeサーバーを立てるコードを書いて実行する
sample1.htmlのみを読み込んでみる
上記のhtmlを読み込むnodeサーバーを立てます。まずは簡易的に説明したいのでsample1.htmlのみを読み込むnodeサーバーを立てます。
まずは親ディレクトリからnodeserver.jsファイルを作成します。
touch nodeserver.jsnodeserver.jsconst http = require('http'); // nodeにデフォルトで入っている http ライブラリ const fs = require('fs'); // nodeにデフォルトで入っているファイル操作の fs ライブラリ const server = http.createServer(function(req, res) { //サーバーを作成 if(req.url === '/about') { // リクエストされたページがaboutの場合にsample1.htmlを呼び出す fs.readFile('./pages/sample1.html','utf-8', function(error, data){ //fs.readFile(オプション)でsample1.htmlをutf-8の文字コードとして呼び出す。 if(error) throw error; res.writeHead(200, {'Content-Type': 'text/html'}); res.write(data); //帰ってきたデータを /about ページへ書き込む res.end(); }) } else { const msg = 'このページは存在しません'; res.writeHead(200, {'Content-Type': 'text/plain; charset=utf-8'}); res.write(msg); res.end(); } }); server.listen(3000); // ポート番号3000でサーバーを起動次に親ディレクトリから上記コードを実行します
node nodeserver.js or node nodeserver次にブラウザのURLに http://localhost:3000/about と打ってみましょう。
上記の通りHTMLが出力されていればOKです。
switch構文と共にページ毎にHTMLを読み込んでみる
次に各ページを読み込めるようリファクタリングします。
pagesのディレクトリに404.htmlを新たに作成しました。sample1.htmlなどをコピペして文章を好きに改変してください(このページは存在しませんやNOT FOUNDなど)
nodeserver.jsconst http = require('http'); const fs = require('fs'); const renderFunc = (res, data) => { //何度も呼び出す処理を関数へ res.writeHead(200, {'Content-Type': 'text/html'}); res.write(data); res.end(); return; } const server = http.createServer(function(req, res) { switch(req.url) { case '/about': fs.readFile('./pages/sample1.html','utf-8',function(error, data){ if(error) throw error; renderFunc(res, data); }) break; case '/profile': fs.readFile('./pages/sample2.html','utf-8',function(error, data){ if(error) throw error; renderFunc(res, data); }) break; default: fs.readFile('./pages/404.html','utf-8',function(error, data){ //例外ページ if(error) throw error; renderFunc(res, data); }) break; } }); server.listen(3000);再度立ち上げ直しましょう。以下を読み込めていればOKです。
このサーバーは立てたままにします。
当然ながら立てたサーバーに対して任意のアクションを起こせます。
今回はスクレイピングしてみましょう。④. スクレイピング用のコードを書く
立てたサーバーのHTMLへ向けてスクレイピングの準備をします。そのためのファイルを作成しましょう。
touch scraping.js親ディレクトリにscraping.jsを作ります。
scraping.jsconst scraperjs = require('scraperjs'); const pages = ['about', 'profile']; // 対象にしたいページの配列 pages.map(page => { // map関数で順番にpagesを取り出す const url = `http://localhost:3000/${page}`; scraperjs.StaticScraper.create(url).scrape(($) => { // scraperjsのStaticScraperメソッドを使いスクレイピング let title = $('h1').text(); // jQueryを使うr let description = $('h2').text(); return {title, description}; }).then(result => { return console.log(result); }).catch((error) => { console.error('Error:', error); }) })コマンドラインに以下のオブジェクトが出力されました。
node scraping.js { title: 'サンプル1のページです', description: 'このページはaboutとします。' } { title: 'サンプル2のページです', description: 'このページはprofileとします。' }壁打ちにする
他に使ってみたいライブラリやモジュールがあれば別にファイルを作って壁打ちできます。
- 投稿日:2020-11-26T18:36:04+09:00
GitHub Actions を使って Next.js × AWS EC2 を自動デプロイした話
※ この記事は K-Ruby #25 のLT資料として書かれた記事です。
こんにちは!
先日、GMOペパボの東証一部上場が決まったことで「東証一部上場の Web 系企業に未経験転職した29歳」になって怪しさに磨きがかかりましたよしこ @k2_yoshikouki です。そろそろエンジニアになれる石売ります。最近 yoshikouki.net という個人サイトを作っている最中で、勉強も兼ねて以下の要件で作っています
- Infrastructure as Code で環境構築
- Chef(ホスト内の実装について定義)
- Terraform(各ホストの関係について定義)
- AWS を使用
- EC2 に nginx (リバースプロキシ)を載せる
- ECS, EKSなどのコンテナサービスは使わない
- フレームワークは Next.js (React) を使用
- バックエンドは node.js (しばらくは Next.js のルーティングを使用したベタ書きかなあと考えている)
- CI/CD は GitHub Actions を利用
苦節一週間の結果、GitHub Actions で自動デプロイ(Continuous Delivery)を実装できたのでその内容を紹介いたします。
Ruby? 知らない子ですね...
ゴール
GitHub Actions を使って AWS EC2 への自動デプロイを導入します
- テストがないのでCIの優先順位が低い
課題
AWS へのデプロイでいい感じにしてくれるサービスはECS, EKS関係が潮流で情報が豊富
(しかし、今回のコンテナ不使用という仕様には合わない)候補のデプロイサービスは AWS CodeDeploy や Capistrano がある
- CodeDeploy はAWS専用サービスかつ情報が少ない
- Capistrano は Ruby環境が必要なので、node.js な今回の仕様でスマートじゃない
コードの移設は rsync なりscp なりでできるが、プロセスの起動
next start
がややこしい
- 色々試行しているとき、ワークフローで
ssh ~~~ "npm run start"
を実行してCI/CDが一生終わらないバグを埋め込んだ
(プロセスが待機状態になるため)nohup npm run start &
などで裏プロセスとして動かせたが、2度目以降のデプロイではプロセスが生きているのでエラーになる
-> 前回のプロセスを kill しないと行けない対応策
- コード移設には rsync コマンドで対応
- Next.js のプロセス管理に PM2 を採用
GitHub Actions ワークフロー
name: Deploy to yoshiko.net on: push: env: ssh_key_path: ~/.ssh/yoshikouki.net.pem app_path: /var/www/ jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Cache multiple paths uses: actions/cache@v2 with: path: | ~/.npm **/node_modules key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} restore-keys: | ${{ runner.os }}-node- - uses: actions/setup-node@v1 with: node-version: '12.x' - run: npm install - run: npm build deploy: if: github.ref == 'refs/heads/main' needs: build runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 - name: Generate SSH key run: | mkdir -p ~/.ssh echo "${{ secrets.SSH_PRIVATE_KEY }}" > ${{ env.ssh_key_path }} chmod 400 ${{ env.ssh_key_path }} eval "$(ssh-agent -s)" ssh-add ${{ env.ssh_key_path }} - name: Deploy with "rsync" command run: | rsync -avL --progress --exclude ".git/" \ -e "ssh -i ${{ env.ssh_key_path }} -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no" \ ./ \ ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:${{ env.app_path }} - name: Build and Start Next.js run: | ssh -i ${{ env.ssh_key_path }} -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no \ ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }} \ 'cd ${{ env.app_path }} \ && sudo npm install \ && sudo npm install pm2 -g\ && sudo npm run build \ && pm2 startOrReload app.json --env production'PM2 のプロセス設定ファイル
{ "name" : "app", "script" : "./node_modules/next/dist/bin/next", "env" : { "NODE_ENV" : "development" }, "env_production" : { "NODE_ENV" : "production" } }PM2 is 何
ADVANCED, PRODUCTION PROCESS MANAGER FOR NODE.JS
PM2 is a daemon process manager that will help you manage and keep your application online 24/7
- "deamon process manager" が分からない方向けの良教材
Linux リテラシ - 第4回 デーモンnode.js 向けのプロセス管理ツール
- 色々できる
- 今回は「Next のプロセスの起動か再読み込みをする」という動きをしてもらっている
Deployment Workflow 機能もあるので、rsync でやっているコード移設や npm 処理周りも PM2 で管理できるがまだやっていない
- PM2 deployment ドキュメント
https://pm2.keymetrics.io/docs/usage/deployment/- このPullRequestで実装中だが、発表に間に合わなかった
まとめ
GitHub Actions の便利アクションやコンテナ技術を使用することで、実際の処理やデプロイ先の環境を意識することなく簡単にCI/CDできることができますが、今回のこの実装で「便利なラッパーが実際どういう処理をしているのか」ということが学べました。
Capistrano のデフォルト挙動では、アップロードしたディレクトリへのシンボリックリンクを公開パスに配置するという頭良すぎる工夫を知ることもできて(なのでロールバックする場合は以前アップロードしたディレクトリにシンボリックリンクを張り直すだけ)、「便利なライブラリをただ利用するだけでなく、その仕組がどうなっているのかまで理解すると応用できる」というエンジニアとしての重要な気付きも得ることができたのでした。
P.S.
Ruby on Rails で最速CDする記事も上げましたので、そちらもよろしくお願いします。本来このLTでやろうと思っていた内容でした(やれよ)
- 投稿日:2020-11-26T18:35:13+09:00
いまさらだけど簡単にFirebase触ってみる(Firebase Cloud Functionsでハローワールド)
概要
これまでAWSでごにょごにょ、はしてきたのですがFirebaseにはほぼ触れてこなかったので簡単に触ってみます。
手順
プロジェクトの作成
・言われるがままにデフォルトっぽい設定で次へ次へ
できました。ウェブアプリの作成
上記スクリーンショットではもう作成された表示になっていますが、ウェブアプリの追加を選択します。
・適当な名前を付けて
・前記事のriot.jsのroot直下のindex.htmlにスクリプトをコピーしてペー。
・何の疑問も持たずにnpm install$ npm install -g firebase-tools・まずはログイン
YesするとブラウザでGoogleのログインフォームが出てくるのでログイン、完了するとサクセスと表示されます。$ firebase login i Firebase optionally collects CLI usage and error reporting information to help improve our products. Data is collected in accordance with Google's privacy policy (https://policies.google.com/privacy) and is not used to identify you. ? Allow Firebase to collect CLI usage and error reporting information? Yes i To change your data collection preference at any time, run `firebase logout` and log in again. ~~~(省略)~~~ ✔ Success! Logged in as [Your Name]・(Riot.jsのルートディレクトリでよかったのか不安になりながら)init
何を作るか聞かれている気がするのでFunctionを選択、あとは適当に次へ次へ
結果的にはここで適当に選択しすぎてプロジェクトが新規追加されたけどご愛嬌。???$ firebase init ######## #### ######## ######## ######## ### ###### ######## ## ## ## ## ## ## ## ## ## ## ## ###### ## ######## ###### ######## ######### ###### ###### ## ## ## ## ## ## ## ## ## ## ## ## #### ## ## ######## ######## ## ## ###### ######## You're about to initialize a Firebase project in this directory: [Your directory] ? Which Firebase CLI features do you want to set up for this folder? Press Space to select fea tures, then Enter to confirm your choices. (Press <space> to select, <a> to toggle all, <i> to invert selection) ◯ Database: Deploy Firebase Realtime Database Rules ◯ Firestore: Deploy rules and create indexes for Firestore ❯◯ Functions: Configure and deploy Cloud Functions ◯ Hosting: Configure and deploy Firebase Hosting sites ◯ Storage: Deploy Cloud Storage security rules ◯ Emulators: Set up local emulators for Firebase features ◯ Remote Config: Get, deploy, and rollback configurations for Remote Config・作成されたfunction/index.jsのコメントアウトを外してデプロイ!
$ firebase deployがっ……駄目っ……!
Error: HTTP Error: 400, Billing account for project 'XXXXXXXXXXXX' is not found. Billing must be enabled for activation of service(s) 'cloudbuild.googleapis.com,containerregistry.googleapis.com' to proceed.突然のエラー
nodeのバージョンが課金対象となっていた(今回だと12)ため、支払いの設定を有効にする必要があるようです。
今回は動けばいいのでバージョンを8に。package.json"engines": { "node": "8" },すると、nodeのバージョン上げろよというメッセージは出つつも、Deploy complete!
✔ functions[helloWorld([location])]: Successful create operation. Function URL (helloWorld): [URL]/helloWorld ⚠ functions: Warning: Node.js 8 functions are deprecated and will stop running on 2021-03-15. Please upgrade to Node.js 10 or greater by adding an entry like this to your package.json: { "engines": { "node": "12" } } The Firebase CLI will stop deploying Node.js 8 functions in new versions beginning 2020-12-15, and deploys from all CLI versions will halt on 2021-02-15. For additional information, see: https://firebase.google.com/support/faq#functions-runtime ✔ Deploy complete!やりました。
おわりに
途中、何も考えずに突っ走ってみましたが普通にハローワールドできました。Firebaseが優しくてよかった。
大体お察しですが、次は Firebase - Riot.js でごにょごにょします。参考
- 投稿日:2020-11-26T18:32:16+09:00
Puppeteer(ぱぺてぃあ)、Headress Recorderで自動化が進んだ
実現させたいこと
とあるサイトのマイページから、データを毎日ダウンロードしたい。しかし、ダウンロードには1,000クリック程度必要。
画面が遷移してもURLが変わらないので、SeleniumやPRAでの実現は難しい。
Chrome操作の自動化ができ、さらにExcelマクロのような録画ツールもあるそうなので、puppeteerを試してみた。
環境構築と習得に1日ほどかかったが、使いやすくて感動!puppeteerとは
Chromeブラウザを操作できるNode.jsのライブラリ
ヘッドレスブラウザを使用できるので高速Headless Recorderを使う
HeadlessRecorderをChromeの拡張機能に追加。
録画ボタンを押して、録画したい操作をすると、Puppeteerコードを出力してくれる。
puppeteerで必要なidやclassを調べる手間が省けて便利。Headless Recorderでできなかったこと
①録画が上手くいかない箇所があった
idやclassが一意でない箇所で、コードに落とせていない操作があった。
お客様番号を使って指定できそうだったので、お客様番号を含むXpathで指定した。お客様番号のクリックconst customerNumber = '123456'; //文字列型で店舗番号を指定 const customerLink = "//a[contains(@onclick,customerNumber)]"; //XPathを検索 await page.waitForXPath(customerLink); const customerLinkClick = await page.$x(customerLink); await customerLinkClick[0].click();②ダウンロード先の指定
ダウンロード先を指定const downloadPath = 'C:\\yyy\\xxx\\test'; // 絶対パスで指定 await page._client.send( 'Page.setDownloadBehavior', { behavior: 'allow', downloadPath: downloadPath } );感想
・学習コストは低め。PythonとGASしか使ったことがない私でも、環境の設定からはじめて、1日あれば使えた。Headless Recorderが偉大。
参考
- 投稿日:2020-11-26T17:41:57+09:00
Nodejsのコンテナを作成してみた。
はじめに
Nodejs
のバックエンドサーバーをコンテナで作成する際、ベースイメージをnodejs(node:10)
にしてDockerfileを作成したが、コンテナにアタッチができず、中の構成などを直接見る方法が分からなかった。
困ったときにはnodejs
の設定などを見れるようにしたかったので、試しにUbuntu
ベースにnodejs
をインストールする方法でDockerfileを作成してみた。
(記事の内容は、試しに動かすところまで記載あり。)実行環境(前提)
【Docker導入環境】
・Ubuntu 20.04 LTS(GCP上)
・docker 19.03.13今回やった事のメモ
今回の作業のためにテストフォルダを作成して、そこに移動。
$ sudo mkdir ./test_container $ cd ./test_containerDockerfileの作成
$ sudo nano ./DockerfileDockerfile# ベースイメージ FROM ubuntu:20.04 # 必要パッケージのインストール RUN apt update RUN apt install -y tzdata RUN apt install -y \ nodejs \ npm # Nodejs関連のパッケージインストール RUN mkdir /usr/src/app WORKDIR /usr/src/app COPY ./package.json ./ RUN npm install # index.jsファイルの設置 COPY ./index.js ./ # ポート開放 EXPOSE 8080 CMD ["node", "index.js"]nodejsモジュールをインストールするための
package.json
を作成
※上記のnpm install
のタイミングで、package.json
の情報を元にインストールが行われる。$ sudo nano ./package.jsonpackage.json{ "name": "test", "description": "test", "version": "0.0.1", "main": "index.js", "private": true, "license": "Apache Version 2.0", "author": "Google Inc.", "engines": { "node": "10" }, "dependencies": { "express": "^4.17.1", "moment-timezone": "^0.5.31", "body-parser": "^1.19.0" } }
index.js
を作成$ sudo nano ./index.jsindex.jsconst express = require('express'); const bodyParser = require('body-parser'); const app = express(); const port = 8080; app.use(bodyParser.urlencoded({ extended: true })); app.get('/get_test1', function(req, res) { res.send('GET1パラメータ取得: ' + req.query.get1) }); app.get('/get_test2', function(req, res) { res.send('GET2パラメータ取得: ' + req.query.get2) }); app.post('/post_test1', function(req, res) { res.send('POST-URLへの送信です。') }); app.post('/post_test2', function(req, res) { res.send('POSTパラメータ取得: ' + req.body.data1) }); app.listen(port)ビルド実行
$ docker image build -t test_container:v1 ./コンテナ作成 & 起動
$ docker container run -it -d -p 80:8080 --name con1 test_container:v1動作確認
GETの動きを確認
以下に接続して、上記で設定したレスポンスが返ってくればOK。
外部IP/get_test1?get1=10
外部IP/get_test2?get2=20
POSTの動きを確認
ローカル環境(自分のPC)に適当なhtml
ファイルを作成して、それをブラウザで開きアクセス。post_test1.html(POST送信のテスト用)<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <title>POSTテスト用</title> </head> <body> <form action="http://[外部IP]/post_tset" method="post"> <input type="hidden" name="data1" value="aiueo"> <input type="submit" value="送信"> </form> </body> </html>post_test2.html(POSTパラメータ取得の確認用)<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <title>POSTテスト用</title> </head> <body> <form action="http://[外部IP]/post_tset2" method="post"> <input type="hidden" name="data1" value="aiueo"> <input type="submit" value="送信"> </form> </body> </html>おまけ
はじめの背景の部分で 『コンテナにアタッチができず、中の構成などを直接見る方法が分からなかった』 と書いたが、コンテナ作成時の[CMD] コマンドを
/bin/bash
で上書きできていなかっただけだった・・・
- 投稿日:2020-11-26T17:14:46+09:00
[Node.js][Deno] オブジェクト合成各種 ベンチマーク比較
先ほどの クラスのベンチマーク の続編。
オブジェクト合成でより良い(というか速い)手法ないかなと悩みつつ。実験環境
同じく
- Node.js 14.15.1
- Deno 1.5.4
実験設定
- 10要素で構成されるオブジェクト2組を1つのオブジェクトにまとめる
- 要素のうち5つは名前が重複しており、合成元を合成先に上書きする
- 合成先はプロトタイプではなく、独立したオブジェクトとして新規に生成される
という前提で
- 合成先オブジェクトを生成するファンクションと、合成元オブジェクトを用意
- 実行時間計測
- 合成先オブジェクト生成ファンクションだけの結果
- 合成先オブジェクト生成ファンクション+各種アルゴリズムの結果
- 結果表示
- 各種アルゴリズムの結果部分を抽出表示
という手順。
オブジェクト合成アルゴリズム ここに集う
今回調査するアルゴリズムを御紹介いたしましょう。
- assign
- Object.assign()
- spread
- スプレッド構文
- for
- for inループ
- forEach
- keys forEachコールバック
- map
- keys mapコールバック
- entries
- entries列挙ループ
- lowtech1
- (対照用) オブジェクト要素として個別に代入
- lowtech2
- (対照用) 連想配列要素として個別に代入
多少のオーバーヘッドは仕方ないにしても、できるだけ対照用で挙げた方法に
近い(というか速い)アルゴリズムを採用したいという趣旨。いざ、実験
今回は記述がコンパクトなので、1ソースでまとめていけます
bench_obj_merge.jsfunction CreateParent(){return {a:0,b:1,c:2,d:3,e:4,f:5,g:6,h:7,i:8,j:9};} var sub={f:10,g:11,h:12,i:13,j:14,k:15,l:16,m:17,n:18,o:19}; // 実行内容 var funx={ dmy:()=>0, // 最初の実行項目は不利な結果が出るのでダミー create:()=>CreateParent(), assign:()=>Object.assign(CreateParent(),sub), spread:()=>{return {...CreateParent(),...sub};}, for:()=>{ var t=CreateParent(); for(var k in sub)t[k]=sub[k]; return t; }, forEach:()=>{ var t=CreateParent(); Object.keys(sub).forEach(k=>t[k]=sub[k]); return t; }, map:()=>{ var t=CreateParent(); Object.keys(sub).map(k=>t[k]=sub[k]); return t; }, entries:()=>{ var t=CreateParent(); for(var [k,v] of Object.entries(sub))t[k]=v; return t; }, lowtech1:()=>{ var t=CreateParent(); t.f=sub.f; t.g=sub.g; t.h=sub.h; t.i=sub.i; t.j=sub.j; t.k=sub.k; t.l=sub.l; t.m=sub.m; t.n=sub.n; t.o=sub.o; return t; }, lowtech2:()=>{ var t=CreateParent(); t['f']=sub['f']; t['g']=sub['g']; t['h']=sub['h']; t['i']=sub['i']; t['j']=sub['j']; t['k']=sub['k']; t['l']=sub['l']; t['m']=sub['m']; t['n']=sub['n']; t['o']=sub['o']; return t; }, }; // 計測結果を書き込むところ var rec={}; // 動作テスト //for(var k in funx)console.log([k,funx[k]()]); // 繰り返し実行時間計測 var loop=Array(1000000); for(var k in funx){ var f=funx[k]; var bgn=new Date; for(var i of loop)f(); var end=new Date; rec[k]=(end-bgn); } // 結果表示 for(var k in rec)console.log(k+' :'+(rec[k]-rec.create));ここで予想外の事態。当初は 前回 と同じ1千万ループだったのですが、なかなか処理終わらなかったので一旦止めて桁減らしちゃいました。
それでループ回数が1桁減っているので、前回の結果と比べるときは御注意ください。
Node.js Deno assign 255.7 265.4 spread 6906.3 6503.3 for 289.2 484.4 forEach 413.6 463.1 map 445.5 505.7 entries 417.3 428.4 lowtech1 10.1 8 lowtech2 10.3 8 まず目を疑ったのが、今(一部で)流行りのスプレッド構文、なんと1桁遅い。
記述が一番シンプルなだけに残念なところ。
で、とりあえず Object.assign() がベストというか幾分マシということで。
もっと素敵な手法ないものかしらん。
- 投稿日:2020-11-26T14:59:12+09:00
Windows開発環境2020 - VScode/Docker/WSL2
WSL2
要件はバージョン 1903 以降、 ビルド 18362 以上。
手順 1 - Linux 用 Windows サブシステムを有効にする
管理者として PowerShell を開き、以下を実行します。
dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart手順 2: 仮想マシンの機能を有効にする
管理者として PowerShell を開き、以下を実行します。
dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart終わったらPC再起動
手順 3 - Linux カーネル更新プログラム パッケージをダウンロードする
以下をインストール
- x64 マシン用 WSL2 Linux カーネル更新プログラム パッケージ手順 4 - WSL 2 を既定のバージョンとして設定する
管理者として PowerShell を開き、以下を実行します。
wsl --set-default-version 2手順 5 - 選択した Linux ディストリビューションをインストールする
Microsoft Store を開き、希望する Linux ディストリビューションを選択します。基本Ubuntuで良いかと。
手順 6 - 新しいディストリビューションを設定する
新しくインストールした Linux ディストリビューションを初めて起動すると、コンソール ウィンドウが開き、ファイルが圧縮解除されて初期設定が走ります。途中でユーザー名とパスワードを決めてくれと言われますので入力。
Docker Desktop
前提としてWindows 10 ProであればHyper-Vの有効化が必須。
HomeであればUEFIから設定かも。詳しくは管理者に聞いてください。インストール
下記からDocker Desktop Windows版 をダウンロード
ダウンロードが出来たらインストーラーを実行します。
必ず「Enable WSL 2 Windows Features」にチェックが入っている事を確認してインストールしてください。開発環境導入
ここからWSL2上で開発環境を構築していきます。
PowerShellでwsl
コマンドを打つ事でWSL2に切り替えできます。最新の状態に更新
まずは最新にしておきましょう
sudo apt update && sudo apt upgrade -ynvmをインストール
インストール
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.37.0/install.sh | bashインストール出来たか確認するためターミナルを閉じて再度WSLに入ってから下記を叩く
command -v nvm
nvm
と返ってきたらインストール完了Node.jsをインストール
安定版(LTS)をインストール
nvm install node --lts確認します
$node -v v14.15.1yarnをインストール
公式ドキュメント通りにインストールします
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list sudo apt update && sudo apt install -y yarn確認します
$ yarn -v 1.22.5同時に入る古いNodejsを削除しておく
sudo apt purge -f libuv1 nodejs
- 投稿日:2020-11-26T14:50:32+09:00
OpenAPIドキュメントから全APIの自動テストを生成する
はじめに
グロースエクスパートナーズ Advent Calendar 2020 3日目。
yitoです。API開発をしている中で、テストするのに毎回クライアントツールにリクエストを用意してAPIを呼び出すのが手間でした。
それを、コマンド一発で、API仕様書に定義した全てのAPIをテストできるようにしたので、その方法を紹介します。概要
OpenAPIドキュメントをAPIクライアントツールで利用可能なJSONに変換し、それをCLIライブラリで実行します。
これを実現する、以下について順番に説明していきます。
- Postmanについて
- OpenAPI 3.0 to Postman Collection v2.1.0 Converter
- Postman Collection SDK
- newman
Postmanについて
APIリクエストのクライアントツールにPostmanがあります。
Postmanは、以下のようなことができます。
- 呼び出したいAPIのリクエスト情報を書き出して保存
- => Postman Collections
- APIのテストスクリプト実装
- ステータスコード、レスポンスボディのプロパティのテストなどが可能
- https://learning.postman.com/docs/writing-scripts/test-scripts/
書き出したリクエスト情報を保存すると、画面左側にあるPostman Collectionsに追加され、フォルダでの管理ができます。
このPostman Collectionsは、Webに公開する(制限をかけることも可能?)か、JSONでのexport/importで、開発チーム内で共有が可能です。OpenAPI 3.0 to Postman Collection v2.1.0 Converter
OpenAPI 3.0 to Postman Collection v2.1.0 Converterは、OpenAPIドキュメントをPostman CollectionのJSONに変換する、NodeJSで実行可能なライブラリです。
使い方は簡単で、OpenAPIのyamlを読み込んで、このライブラリのクラスに渡して実行するだけです。const fs = require('fs'); const readFileData = fs.readFileSync('OpenAPIドキュメントのファイルパス', {encoding: 'UTF8'}); const Converter = require('openapi-to-postmanv2'); Converter.convert({type: 'string', data: readFileData}, {}, (err, conversionResult) => { if (!conversionResult.result) { console.error('API定義をPostman Collectionに変換できませんでした', conversionResult.reason); return; } const convertData = conversionResult.output[0].data; // Postman CollectionsのJSONファイルを生成する fs.writeFile('postman-collections.json', JSON.stringify(convertData, null, 4), (err) => { if(err) console.error(err) }); } );この生成されたJSONファイルを読み込むと、Postmanにて利用ができます。
ただ、このままだと、各APIのリクエストパラメータが
<long>
<string>
のようになっていて、このままだと呼び出したいAPIのパラメータとしては不適切なので、使う際は書き換えが必要です。手で1つ1つ直すのは面倒なので、Postman Collection SDKを使います
Postman Collection SDK
Postman Collection SDKは、Postman Collectionの生成・更新のための、NodeJSで実行可能なSDKです。
OpenAPI 3.0 to Postman Collection v2.1.0 Converterにて生成したPostman CollectionのJSONを、このSDKを使って編集します。Postman CollectionのJSONファイルを読み込み、SDKにてCollectionをnewすると、以下のようなクラスが生成されます。
※クラスについての詳細
- Collection
- Postman Collectionの1つに相当
- メンバーにItemGroupを持つ
- ItemGroup
- Postman Collectionの1フォルダに相当
- メンバーにItemを持つ
- Item
- Postman Collectionの1つのAPIに相当
- メンバーにRequestを持つ
- Request
- APIのURL、リクエストパラメータに関する値
- メンバーにHeaderList, EventList, VariableListなどを持つ
<long>
<string>
のような値を実際に使いたい値に変えるため、Request
の値を書き換えます。
こちらにあるOpenAPIドキュメント(api-spec.yml)、Postman Collection SDKを使った実装(postman-test/index.js)を使いながら説明します。先ほどの実装に以下を追加します。
... const convertData = conversionResult.output[0].data; const collection = new Collection(convertData); collection.items.all().forEach(item => updateItem(item)) // Postman CollectionsのJSONファイルを生成する fs.writeFile('postman-collections.json', JSON.stringify(collection, null, 4), (err) => { if(err) console.error(err) }); ...
collection.items
にItemGroup
またはItem
があります。そこから、Request
を取り出して、<long>
<string>
のような値を、使いたい値に書き換えます。... const { v4: uuidv4 } = require('uuid'); const updateRequestVariable = (request, key, value) => { const findVariable = request.url.variables.find( variable => variable.key === key); if (findVariable) { findVariable.update({ key: key, value: value }) } } const updateRequestQuery = (request, key, value) => { const findQueryParam = request.url.query.find( queryParam => queryParam.key === key); if (findQueryParam) { findQueryParam.update({ key: key, value: value }) } } const updateRequest = (request) => { // Authorization if (request.auth && request.auth.type === 'apikey') { request.auth.update({ "key": "api_key", "value": `api-key-${uuidv4()}` }, 'apikey') } // 書籍関連のAPI if (request.url.getPath().includes('books')) { updateRequestVariable(request, 'bookId', '100000001'); } // ユーザー関連のAPI if (request.url.getPath().includes('users')) { updateRequestVariable(request, 'username', 'user-XXXX'); updateRequestQuery(request, 'username', 'user-XXXX'); updateRequestQuery(request, 'password', 'pass-XXXX'); } return request; } const updateItem = (item) => { if (item.items) { item.items.all().forEach(item => updateItem(item)); } else { item.request = updateRequest(item.request); } return item; } ...レスポンスに対するテストも実行したいので、
updateItem
メソッドに以下のコードを追加します。... const updateItem = (item) => { if (item.items) { item.items.all().forEach(item => updateItem(item)); } else { // 追加 item.events.add({ listen: 'test', script: { exec: "pm.test('response 200 test', () => {\n" + " pm.response.to.have.status(200);\n" + "});" }, type: 'text/javascript' }) ...これでリクエストパラメータの書き換えがされた状態で、Postman CollectionのJSONが生成されます。
毎回JSONをimportしてAPIを呼び出すのは面倒なので、CLIだけで実行できるように、newmanを使います.
newman
newmanは、CLIでPostman CollectionのJSONを読み込みAPIをリクエストする、NodeJSで実行可能なライブラリです。
先ほどのコードで生成したJSONを、newmanで実行するように変えます。
... const collection = new Collection(convertData); collection.items.all().forEach(item => updateItem(item)) fs.writeFile('postman-collections.json', JSON.stringify(collection, null, 4), (err) => { if(err) console.error(err) }); newman.run({ collection: collection, reporters: 'cli', environment: require('./local.postman_environment.json') }, (err) => { if (err) console.error(err); }); ...
newman.run()
- collection: Postman CollectionのJSONを指定
- reports: 実行結果の出力方法の指定。CLI、Junit、JSON等が可能
- envioroment: Postmanには環境ごとの変数の指定があり、生成されたJSONには
baseUrl
の指定が必要なため、向き先を指定したJSONを用意して読み込む実行すると以下のようになります。
% npm run start > node index.js newman Book Management ❏ books ↳ 書籍更新API PUT http://localhost:8080/v1/books [200 OK, 123B, 235ms] ✓ response 200 test ↳ 書籍登録API POST http://localhost:8080/v1/books [200 OK, 123B, 19ms] ✓ response 200 test ↳ 書籍一覧取得API GET http://localhost:8080/v1/books [200 OK, 800B, 11ms] ✓ response 200 test ↳ タグ絞り込み検索API GET http://localhost:8080/v1/books/findByTags?tags=<string>&tags=<string> [200 OK, 800B, 14ms] ✓ response 200 test ❏ books / {book Id} ↳ 書籍詳細取得API GET http://localhost:8080/v1/books/100000001 [200 OK, 290B, 10ms] ✓ response 200 test ↳ 書籍削除API DELETE http://localhost:8080/v1/books/100000001 [200 OK, 123B, 7ms] ✓ response 200 test ❏ users ↳ ユーザー登録API POST http://localhost:8080/v1/users [200 OK, 123B, 8ms] ✓ response 200 test ↳ ユーザー一覧取得API GET http://localhost:8080/v1/users [200 OK, 123B, 6ms] ✓ response 200 test ↳ ログインAPI GET http://localhost:8080/v1/users/login?username=user-XXXX&password=pass-XXXX [200 OK, 123B, 8ms] ✓ response 200 test ↳ ログアウトAPI GET http://localhost:8080/v1/users/logout [200 OK, 123B, 6ms] ✓ response 200 test ❏ users / {username} ↳ ユーザー詳細取得APi GET http://localhost:8080/v1/users/user-XXXX [200 OK, 123B, 6ms] ✓ response 200 test ↳ ユーザー更新API PUT http://localhost:8080/v1/users/user-XXXX [200 OK, 123B, 5ms] ✓ response 200 test ↳ ユーザー削除API DELETE http://localhost:8080/v1/users/user-XXXX [200 OK, 123B, 7ms] ✓ response 200 test → タグ一覧取得API GET http://localhost:8080/v1/tags [200 OK, 123B, 6ms] ✓ response 200 test ┌─────────────────────────┬───────────────────┬──────────────────┐ │ │ executed │ failed │ ├─────────────────────────┼───────────────────┼──────────────────┤ │ iterations │ 1 │ 0 │ ├─────────────────────────┼───────────────────┼──────────────────┤ │ requests │ 14 │ 0 │ ├─────────────────────────┼───────────────────┼──────────────────┤ │ test-scripts │ 14 │ 0 │ ├─────────────────────────┼───────────────────┼──────────────────┤ │ prerequest-scripts │ 0 │ 0 │ ├─────────────────────────┼───────────────────┼──────────────────┤ │ assertions │ 14 │ 0 │ ├─────────────────────────┴───────────────────┴──────────────────┤ │ total run duration: 654ms │ ├────────────────────────────────────────────────────────────────┤ │ total data received: 1.37KB (approx) │ ├────────────────────────────────────────────────────────────────┤ │ average response time: 24ms [min: 5ms, max: 235ms, s.d.: 58ms] │ └────────────────────────────────────────────────────────────────┘以上で、コマンド一発でAPI全てをテストできるようになりました。
参考
- https://learning.postman.com/docs/writing-scripts/test-scripts/
- https://www.postman.com/collection/
- https://github.com/postmanlabs/openapi-to-postman
- https://www.postmanlabs.com/postman-collection/index.html
サンプルコード
- 投稿日:2020-11-26T14:42:35+09:00
mongoose の model と schema に TypeScript で型をつける
Migration mongoose models and schemas from JavaScript to TypeScript
今年は自宅にこもりがちになって腰椎椎間板ヘルニアと坐骨神経痛のコンボを決めた @algas です。
この記事は TypeScript Advent Calendar 2020 の1日目の記事として作成しました。
記事に登場するコードは github リポジトリで公開しています。
https://github.com/algas/typed-mongoose-example概要
mongoose という mongoDB の Node.js では有名なライブラリを使うアプリケーションコードを JavaScript から TypeScript に移行する作業を行いました。
JavaScript で定義済みの mongoose のモデルとスキーマに「正しく」型をつけることができたのでそのノウハウを共有します。
基本的には @types/mongoose を使って型をつけています。
スキーマの型定義の一部が不十分だったので独自の定義をして補いました。対象読者
この記事は次のような読者を対象に想定しています。
- mongoose または mongoDB を使っている
- TypeScript を書いたことがある
mongoDB や mongoose, TypeScript の詳しい説明はしません。
背景
この記事を書くに至った背景を説明します。
なぜ mongoose のコードに型をつけると良いのか
わざわざ書くまでもないとは思いますが、コードを静的型付きにすることでコンパイル時に不具合を見つけやすくなったり開発環境によるコード記述の補完を得られるようになります。適切に型をつけることができればスキーマやモデルに対して定義したフィールドやメソッドとして何が含まれているかやその型の情報を使って効率よく安心して開発をすることができます。
mongoose が TypeScript ネイティブではない
mongoose は JavaScript で古くから開発され続けているライブラリで、そのコードは TypeScript に対応していません。
@types/mongoose
で型情報を別途付与することはできますが、TypeScript のアプリケーションから呼び出すのに便利な実装になっているとは言えません。mongoose のスキーマやモデルに型をつけるのは難しい
mongoose スキーマやモデルに後から TypeScript で型をつけるのには工夫が必要です。mongoose の Collection スキーマには単純にデータベースに値を保持するフィールドだけではなく Virtual Property, Instance Method, Static Method, Plugin などの機能があります。これらに対応するモデルに型をつけ、さらにスキーマ自体にフィールドの型を付与するのが本記事の試みです。
初めから TypeScript で書く場合やこれから新しく mongoose スキーマを定義する場合には別のライブラリを使うなどの手法をオススメします。
実装例
具体的に mongoose で定義したスキーマとモデルに TypeScript で型をつけてみます。
https://github.com/algas/typed-mongoose-example/blob/main/src/model.ts執筆時点では次のバージョンのライブラリにそれぞれ対応しています。
typescript
: 3.xmongoose
: 5.x@types/mongoose
: 5.xmongoose の基本的な使い方は公式ドキュメントに書いてあります。
https://mongoosejs.com/docs/index.html
これを知っている前提で話を進めます。Schema
mongoDB の Collection に入れるデータのスキーマを定義します。
User
という Collection を作ることにします。
Instance method としてfoo
という関数を追加しています。import { Document, model, Model, Schema, Types } from 'mongoose'; interface UserSchemaFields { email: string; firstName: string; lastName: string; age?: number; friends?: Types.ObjectId[]; } const userSchemaFields: SchemaDefinition<UserSchemaFields> = { email: { type: String, required: true, unique: true }, firstName: { type: String, required: true }, lastName: { type: String, required: true }, age: { type: Number }, friends: [Schema.Types.ObjectId], }; const UserSchema: Schema<UserSchemaProperties> = new Schema(userSchemaFields); // Instance methods interface UserSchemaProperties extends UserSchemaFields { foo: () => void; } UserSchema.methods.foo = function() {};ここではスキーマとそのフィールドの定義に明示的に型を与えているのが一般的な方法との違いです。
Instance method の定義はSchema
だけに与えてSchemaDefinition
には与えません。
独自に定義し直したSchemaDefinition
は次のように書けます。
https://github.com/algas/typed-mongoose-example/blob/main/src/mongoose-util.d.tsimport { Schema, SchemaType, SchemaTypeOpts } from 'mongoose'; type SchemaPropType<T> = T extends string ? StringConstructor : T extends number ? NumberConstructor : T extends boolean ? BooleanConstructor : any; type SchemaTypeOptions<T> = Omit<SchemaTypeOpts<T>, 'type'> & { type?: SchemaPropType<T>; }; export type SchemaDefinition<T = any> = { [K in keyof T]: SchemaTypeOptions<T[K]> | Schema | SchemaType; };SchemaTypeOpts から 'type' の定義を取り除いて使いました。
元の実装は次のコードで定義されています。
https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/mongoose/index.d.tsVirtual properties (Document)
mongoose schema には
virtual
という機能を使って仮想的な要素を追加できます。
ここではfullName
を取得する機能を実装します。
UserDocument
にDocument
とUserSchemaProperties
(fields + instance methods) を継承させて virtual propertyfullName
の型を足します。// Virtual properties interface UserDocument extends Document, UserSchemaProperties { fullName: string; } UserSchema.virtual('fullName').get(function () { return [this.firstName, this.lastName].join(' '); });mongoose
Document
の詳細は mongoose の API reference に書かれています。
https://mongoosejs.com/docs/documents.htmlStatics variables and functions
mongoose schema には static の変数や関数を追加できます。
UserModel
に上で定義したUserDocument
を指定したModel<UserDocument>
を継承させて static methodbar
の型を足します。// Static methods interface UserModel extends Model<UserDocument> { bar: () => string; } UserSchema.statics.bar = function(){ return 'bar'; }mongoose
Model
の詳細は mongoose の API reference に書かれています。
https://mongoosejs.com/docs/models.htmlPlugins
他のスキーマで定義された関数などを plugin として呼び出すことができます。
plugin で作った変数や関数の interface は本体とは別に定義して個別に呼び出せるようにすべきです。
たとえばSomePluginSchema
という名前のスキーマで定義されている static 関数を呼び出す場合には次のように書きます。interface SomePluginFunctions { somePluginFunction: () => void; } UserSchema.plugin(SomePluginSchema, {}); interface UserModel extends Model<UserDocument>, SomePluginFunctions { ... }Model
mongoose model を定義します。
引数にはモデルの名前とスキーマを渡します。
型指定にUserDocument
,UserModel
を明示することでUser
モデルの型情報が使えるようになります。export const User = model<UserDocument, UserModel>('User', UserSchema);モデルとスキーマを使ってみる
ここまででスキーマとモデルの定義はおしまいです。
モデルを使うにはインスタンス化する必要があります。const someUserData: UserSchemaFields { ... } const someUser = new User(someUserData);
someUser
はUserDocument
型になります。
User()
の引数に渡すオブジェクトにUserSchemaFields
の型を使えば厳密に定義できます。また
User
データを mongoDB から取得するコードは次のように書きます。User.find(function (err, users) { if (err) return console.error(err); console.log(users); })TypeScript が正しく設定された開発環境を使うと
User
やそのインスタンスにフィールドやメソッドが適切に補完されることを確認できます。まとめ
- 適切に型をつければ TypeScript でも mongoose を使った開発をできる
- mongoose に TypeScript で適切な型をつけるのはちょっと大変
- 新しく TypeScript + mongoDB を使った開発をするのであれば mongoose 以外のライブラリを使うべき
注意点
- 本記事内に登場するコードはMITライセンスで公開しています
https://github.com/algas/typed-mongoose-example- Schema の型定義の変更は
@types/mongoose
にもフィードバックする予定です参考文献
- 投稿日:2020-11-26T14:11:14+09:00
[Node.js][Deno] クラス定義各種 ベンチマーク比較
JavaScriptのクラス定義手法はいっぱいあって、これを解説している記事もいっぱいありますが、パフォーマンスに言及しているところがなかったので、自前で実験してみた。
実験環境
- Node.js 14.15.1
- Deno 1.5.4
なお、webクライアントでの実行コストはあまり気にしてないので、各種ブラウザでの比較はパス。
実験設定
- 継承のあるクラスで
- インスタンス生成コストだけでなく、親メンバへのアクセスコストも重要
- とにかくランタイムで高速なものを追求
- プロトタイプ定義コストは、あまり気にしてない
- 標準的なクラス定義方法との互換性も、あまり気にしてない
- 各メンバの読み書きができれば充分
- ただしもちろん、書き込みによってプロトタイプを破壊しないことは重要
そんなわけで、各手法それぞれ
- TestClassA 定義
- プロパティ a とメソッド getA() を含める
- TestClassB 定義
- TestClassA を継承
- プロパティ b~i を含める
- TestClassC 定義
- TestClassB を継承
- プロパティ j~z を含める (このぐらい余計に定義入れとけばプロトタイプへのアクセスコストに差が出てくること期待)
- TestClassC で最初のインスタンス生成
- ついでに正常にアクセスできるかテスト
- 実行時間計測
- 最初のインスタンスでget (プロパティ a 読み出し)
- 最初のインスタンスでcall (メソッド getA() 呼び出し)
- 新規生成インスタンスでget
- 新規生成インスタンスでcall
- 結果表示
- get call それぞれ
- 新規生成インスタンス版結果 - 最初のインスタンス流用版結果 → インスタンス生成コスト
- 10回実行して平均をとる
な手順で。
実験: 標準的なprototype定義方式
まずは普通に。
bench_class_standard.js// TestClassA 定義 function TestClassA(){ this.a=1; }; TestClassA.prototype.getA=function(){return this.a;}; // TestClassB 定義 function TestClassB(){ TestClassA.call(this); this.b=2; this.c=3; this.d=4; this.e=5; this.f=6; this.g=7; this.h=8; this.i=9; } Object.setPrototypeOf(TestClassB.prototype,TestClassA.prototype); // TestClassC 定義 function TestClassC(){ TestClassB.call(this); this.j=10; this.k=11; this.l=12; this.m=13; this.n=14; this.o=15; this.p=16; this.q=17; this.r=18; this.s=19; this.t=20; this.u=21; this.v=22; this.w=23; this.x=24; this.y=25; this.z=26; } Object.setPrototypeOf(TestClassC.prototype,TestClassB.prototype); // 既存インスタンス準備 var t=new TestClassC(); //console.log(t); //console.log(t.getA()); // 実行内容 var funx={ dmy:()=>0, // 最初の実行項目は不利な結果が出るのでダミー get:()=>t.a, call:()=>t.getA(), new_get:()=>new TestClassC().a, new_call:()=>new TestClassC().getA(), }; // 計測結果を書き込むところ var rec={}; // 繰り返し実行時間計測 var loop=Array(10000000); for(var k in funx){ var f=funx[k]; var a=0; var bgn=new Date; for(var i of loop)a+=f(); var end=new Date; rec[k]=end-bgn; } // インスタンス生成時間を抽出表示 console.log(rec.new_get-rec.get); console.log(rec.new_call-rec.call);
Node.js Deno get 3.7 427.8 call 7.7 434.7 後発品なのになんでこんな遅いんだよDeno
…というわけで詳細に調べたところ、どうやらコンストラクタで親クラスのcall呼んでるところでやたらと時間かかってる模様。
そんなわけでDeno移行は時期尚早かな…って結論出す前に、まず次いってみましょうか。実験: いまどきのclass方式
ECMAScript 2015以降の追加仕様ですね。
めんどいprototype定義ともおさらば、やったー
…って喜ぶ前に、パフォーマンスをみてみましょう。bench_class_modern.js// TestClassA 定義 class TestClassA{ constructor(){ this.a=1; } getA(){return this.a;} } // TestClassB 定義 class TestClassB extends TestClassA{ constructor(){ super(); this.b=2; this.c=3; this.d=4; this.e=5; this.f=6; this.g=7; this.h=8; this.i=9; } } // TestClassC 定義 class TestClassC extends TestClassB{ constructor(){ super(); this.j=10; this.k=11; this.l=12; this.m=13; this.n=14; this.o=15; this.p=16; this.q=17; this.r=18; this.s=19; this.t=20; this.u=21; this.v=22; this.w=23; this.x=24; this.y=25; this.z=26; } } // 既存インスタンス準備 var t=new TestClassC(); //console.log(t); //console.log(t.getA()); // 実行内容 var funx={ dmy:()=>0, // 最初の実行項目は不利な結果が出るのでダミー get:()=>t.a, call:()=>t.getA(), new_get:()=>new TestClassC().a, new_call:()=>new TestClassC().getA(), }; // 計測結果を書き込むところ var rec={}; // 繰り返し実行時間計測 var loop=Array(10000000); for(var k in funx){ var f=funx[k]; var a=0; var bgn=new Date; for(var i of loop)a+=f(); var end=new Date; rec[k]=end-bgn; } // インスタンス生成時間を抽出表示 console.log(rec.new_get-rec.get); console.log(rec.new_call-rec.call);
Node.js Deno get 145.4 91.3 call 139.8 89.3 …遅っ。
だめだこりゃ、次いってみよー実験: コンストラクタをinit()で代用方式
先ほど、親クラスのcallが遅いって書きました。
で、コンストラクタ使わずに自前で初期化メソッド書いちゃえばいんじゃね作戦。bench_class_noctor.js// TestClassA 定義 function TestClassA(){} TestClassA.prototype.init=function(){ this.a=1; }; TestClassA.prototype.getA=function(){return this.a;}; // TestClassB 定義 function TestClassB(){} Object.setPrototypeOf(TestClassB.prototype,TestClassA.prototype); TestClassB.prototype.init=function(){ Object.getPrototypeOf(this).init(); this.b=2; this.c=3; this.d=4; this.e=5; this.f=6; this.g=7; this.h=8; this.i=9; }; // TestClassC 定義 function TestClassC(){} Object.setPrototypeOf(TestClassC.prototype,TestClassB.prototype); TestClassC.prototype.init=function(){ Object.getPrototypeOf(this).init(); this.j=10; this.k=11; this.l=12; this.m=13; this.n=14; this.o=15; this.p=16; this.q=17; this.r=18; this.s=19; this.t=20; this.u=21; this.v=22; this.w=23; this.x=24; this.y=25; this.z=26; }; // 既存インスタンス準備 var t=new TestClassC(); t.init(); //console.log(t); //console.log(t.getA()); // 実行内容 var funx={ dmy:()=>0, // 最初の実行項目は不利な結果が出るのでダミー get:()=>t.a, call:()=>t.getA(), new_get:()=>{var u=new TestClassC(); u.init(); u.a}, new_call:()=>{var u=new TestClassC(); u.init(); u.getA()}, }; // 計測結果を書き込むところ var rec={}; // 繰り返し実行時間計測 var loop=Array(10000000); for(var k in funx){ var f=funx[k]; var a=0; var bgn=new Date; for(var i of loop)a+=f(); var end=new Date; rec[k]=end-bgn; } // インスタンス生成時間を抽出表示 console.log(rec.new_get-rec.get); console.log(rec.new_call-rec.call);
Node.js Deno get 1857.6 1949.5 call 1848.6 1949.3 OMG.
getPrototypeOf() さらに重かった。
しかもこれ、 console.log(t) でTestClassCのぶんしか出ないですよ。
console.log(t.getA()) はちゃんと1って出てるので、継承は正常っぽい。実験: 親クラスのメソッド転記方式
では、 getPrototypeOf() にも頼らず、親クラスのメソッドを自前転記作戦。
bench_class_selflink.js// TestClassA 定義 function TestClassA(){} TestClassA.prototype.init=function(){ this.a=1; }; TestClassA.prototype.getA=function(){return this.a;}; // TestClassB 定義 function TestClassB(){} Object.setPrototypeOf(TestClassB.prototype,TestClassA.prototype); TestClassB.prototype.init_TestClassA=TestClassA.prototype.init; TestClassB.prototype.init=function(){ this.init_TestClassA(); this.b=2; this.c=3; this.d=4; this.e=5; this.f=6; this.g=7; this.h=8; this.i=9; }; // TestClassC 定義 function TestClassC(){} Object.setPrototypeOf(TestClassC.prototype,TestClassB.prototype); TestClassC.prototype.init_TestClassB=TestClassB.prototype.init; TestClassC.prototype.init=function(){ this.init_TestClassB(); this.j=10; this.k=11; this.l=12; this.m=13; this.n=14; this.o=15; this.p=16; this.q=17; this.r=18; this.s=19; this.t=20; this.u=21; this.v=22; this.w=23; this.x=24; this.y=25; this.z=26; }; // 既存インスタンス準備 var t=new TestClassC(); t.init(); //console.log(t); //console.log(t.getA()); // 実行内容 var funx={ dmy:()=>0, // 最初の実行項目は不利な結果が出るのでダミー get:()=>t.a, call:()=>t.getA(), new_get:()=>{var u=new TestClassC(); u.init(); u.a}, new_call:()=>{var u=new TestClassC(); u.init(); u.getA()}, }; // 計測結果を書き込むところ var rec={}; // 繰り返し実行時間計測 var loop=Array(10000000); for(var k in funx){ var f=funx[k]; var a=0; var bgn=new Date; for(var i of loop)a+=f(); var end=new Date; rec[k]=end-bgn; } // インスタンス生成時間を抽出表示 console.log(rec.new_get-rec.get); console.log(rec.new_call-rec.call);
Node.js Deno get 5.6 6.9 call 9.5 1.4 Denoで見違える結果になりました。
で、getよりcallの方が速い謎が増えた。
あと、 console.log(t) は一通り出てきてます。実験: メソッドをアロー演算子で簡略表記方式
prototypeでアロー演算子使ってもうまくいかないんだよねぃ。
というわけで、 getA() の定義を init() 内に移してみた。bench_class_lambda.js// TestClassA 定義 function TestClassA(){} TestClassA.prototype.init=function(){ this.a=1; this.getA=()=>this.a; }; // TestClassB 定義 function TestClassB(){} Object.setPrototypeOf(TestClassB.prototype,TestClassA.prototype); TestClassB.prototype.init_TestClassA=TestClassA.prototype.init; TestClassB.prototype.init=function(){ this.init_TestClassA(); this.b=2; this.c=3; this.d=4; this.e=5; this.f=6; this.g=7; this.h=8; this.i=9; }; // TestClassC 定義 function TestClassC(){} Object.setPrototypeOf(TestClassC.prototype,TestClassB.prototype); TestClassC.prototype.init_TestClassB=TestClassB.prototype.init; TestClassC.prototype.init=function(){ this.init_TestClassB(); this.j=10; this.k=11; this.l=12; this.m=13; this.n=14; this.o=15; this.p=16; this.q=17; this.r=18; this.s=19; this.t=20; this.u=21; this.v=22; this.w=23; this.x=24; this.y=25; this.z=26; }; // 既存インスタンス準備 var t=new TestClassC(); t.init(); //console.log(t); //console.log(t.getA()); // 実行内容 var funx={ dmy:()=>0, // 最初の実行項目は不利な結果が出るのでダミー get:()=>t.a, call:()=>t.getA(), new_get:()=>{var u=new TestClassC(); u.init(); u.a}, new_call:()=>{var u=new TestClassC(); u.init(); u.getA()}, }; // 計測結果を書き込むところ var rec={}; // 繰り返し実行時間計測 var loop=Array(10000000); for(var k in funx){ var f=funx[k]; var a=0; var bgn=new Date; for(var i of loop)a+=f(); var end=new Date; rec[k]=end-bgn; } // インスタンス生成時間を抽出表示 console.log(rec.new_get-rec.get); console.log(rec.new_call-rec.call);
Node.js Deno get 10.8 12.9 call 712.6 556.8 init() の度に再定義しているわけだから重いのは必然だし、
console.log(t) で getA まで出てくる代物なのですが、
定義するだけで呼ばないならさほど大きなコストではないらしい。実験: proto詰め込み方式
console.log(t) を気にしないでいいなら、こんな手法もあり。
プロパティの初期値もprototypeにぶっ込んでしまえば、そのぶんインスタンス生成コストも軽くなるわけで。bench_class_fullproto.js// TestClassA 定義 function TestClassA(){}; TestClassA.prototype.a=1; TestClassA.prototype.getA=function (){return this.a;} // TestClassB 定義 function TestClassB(){} Object.setPrototypeOf(TestClassB.prototype,TestClassA.prototype); TestClassB.prototype.b=2; TestClassB.prototype.c=3; TestClassB.prototype.d=4; TestClassB.prototype.e=5; TestClassB.prototype.f=6; TestClassB.prototype.g=7; TestClassB.prototype.h=8; TestClassB.prototype.i=9; // TestClassC 定義 function TestClassC(){} Object.setPrototypeOf(TestClassC.prototype,TestClassB.prototype); TestClassC.prototype.j=10; TestClassC.prototype.k=11; TestClassC.prototype.l=12; TestClassC.prototype.m=13; TestClassC.prototype.n=14; TestClassC.prototype.o=15; TestClassC.prototype.p=16; TestClassC.prototype.q=17; TestClassC.prototype.r=18; TestClassC.prototype.s=19; TestClassC.prototype.t=20; TestClassC.prototype.u=21; TestClassC.prototype.v=22; TestClassC.prototype.w=23; TestClassC.prototype.x=24; TestClassC.prototype.y=25; TestClassC.prototype.z=26; // 既存インスタンス準備 var t=new TestClassC(); //console.log(t); //console.log(t.getA()); // 実行内容 var funx={ dmy:()=>0, // 最初の実行項目は不利な結果が出るのでダミー get:()=>t.a, call:()=>t.getA(), new_get:()=>new TestClassC().a, new_call:()=>new TestClassC().getA(), }; // 計測結果を書き込むところ var rec={}; // 繰り返し実行時間計測 var loop=Array(10000000); for(var k in funx){ var f=funx[k]; var a=0; var bgn=new Date; for(var i of loop)a+=f(); var end=new Date; rec[k]=end-bgn; } // インスタンス生成時間を抽出表示 console.log(rec.new_get-rec.get); console.log(rec.new_call-rec.call);
Node.js Deno get -6 7.2 call 1.5 3.5 Denoの結果がいまいち。
どうもprototypeが重い御様子。
てゆかNode.js、マイナスってなんだよおい。実験: Object.create() 方式
prototypeが重いなら、 Object.create() ならどうだ。
bench_class_create.js// TestClassA 定義 var TestClassA={a:1}; TestClassA.getA=function(){return this.a;}; // TestClassB 定義 var TestClassB=Object.create(TestClassA); TestClassB.b=2; TestClassB.c=3; TestClassB.d=4; TestClassB.e=5; TestClassB.f=6; TestClassB.g=7; TestClassB.h=8; TestClassB.i=9; // TestClassC 定義 var TestClassC=Object.create(TestClassB); TestClassC.j=10; TestClassC.k=11; TestClassC.l=12; TestClassC.m=13; TestClassC.n=14; TestClassC.o=15; TestClassC.p=16; TestClassC.q=17; TestClassC.r=18; TestClassC.s=19; TestClassC.t=20; TestClassC.u=21; TestClassC.v=22; TestClassC.w=23; TestClassC.x=24; TestClassC.y=25; TestClassC.z=26; // 既存インスタンス準備 var t=Object.create(TestClassC); //console.log(t); //console.log(t.getA()); // 実行内容 var funx={ dmy:()=>0, // 最初の実行項目は不利な結果が出るのでダミー get:()=>t.a, call:()=>t.getA(), new_get:()=>Object.create(TestClassC).a, new_call:()=>Object.create(TestClassC).getA(), }; // 計測結果を書き込むところ var rec={}; // 繰り返し実行時間計測 var loop=Array(10000000); for(var k in funx){ var f=funx[k]; var a=0; var bgn=new Date; for(var i of loop)a+=f(); var end=new Date; rec[k]=end-bgn; } // インスタンス生成時間を抽出表示 console.log(rec.new_get-rec.get); console.log(rec.new_call-rec.call);
Node.js Deno get -2.8 74 call 2.7 74.9 没。
実験: 空オブジェクトに詰め込み方式
おまけ。
既にprototype以外でfunction定義するとすごい重いって結論出ちゃったからあまり意味なくなっちゃったんだけど…bench_class_pureobj.js// TestClassA 定義 function TestClassA(){ var t={a:1}; t.getA=()=>t.a; return t; } // TestClassB 定義 function TestClassB(){ var t=TestClassA(); t.b=2; t.c=3; t.d=4; t.e=5; t.f=6; t.g=7; t.h=8; t.i=9; return t; } // TestClassC 定義 function TestClassC(){ var t=TestClassB(); t.j=10; t.k=11; t.l=12; t.m=13; t.n=14; t.o=15; t.p=16; t.q=17; t.r=18; t.s=19; t.t=20; t.u=21; t.v=22; t.w=23; t.x=24; t.y=25; t.z=26; return t; } // 既存インスタンス準備 var t=TestClassC(); //console.log(t); //console.log(t.getA()); // 実行内容 var funx={ dmy:()=>0, // 最初の実行項目は不利な結果が出るのでダミー get:()=>t.a, call:()=>t.getA(), new_get:()=>TestClassC().a, new_call:()=>TestClassC().getA(), }; // 計測結果を書き込むところ var rec={}; // 繰り返し実行時間計測 var loop=Array(10000000); for(var k in funx){ var f=funx[k]; var a=0; var bgn=new Date; for(var i of loop)a+=f(); var end=new Date; rec[k]=end-bgn; } // インスタンス生成時間を抽出表示 console.log(rec.new_get-rec.get); console.log(rec.new_call-rec.call);
Node.js Deno get 7.4 8.4 call 1173.1 803.3 ということで。
因みに、プロパティ定義で横着して
Object.assign(TestClassA(),{b:2,...})
みたいな書き方すると、さらに悲惨な結果になります。
JavaScriptでのオブジェクト合成って、 何やっても重い ので困ったもんだ。結論
邪道実装でいい案件なら、proto詰め込み方式を推し進めたいところ。
あと、やはりDenoはパフォーマンス改善まで様子見。