20210726のNode.jsに関する記事は12件です。

俺流AWS Lambdaデプロイ+IaCのベストプラクティスを考える

概要 Lambdaをコード管理したいのですが、Lambdaのベストプラクティス で良い感じのものがないと思い困り調べてみました。 以下のデプロイ方法がありますが、どれも個人的にはいまいちなため悩んでいました。 AWS CLIでのデプロイする 独自のデプロイツール(Lambdaroll)を使う https://tech.toreta.in/entry/2020/12/05/000000 ServerlessFrameworkを使う AWS CDKでデプロイする https://zenn.dev/faycute/articles/be5599fc093511 課題感 サーバレス関連のツールはどうしても構成管理ツールと紐づいてしまっているのでどうしたものかなぁと思っていました。 例えば、AWS CDKでもServerlessFrameworkでもバックエンドではCloudFormationで動いてしまっています。このようなツールのメリットは1リポジトリでインフラの構成管理とコード管理が完結することがメリットだと思っています。ただ、既存インフラ環境のIaCにTerraformを使っている場合、TerraformとCloudFormationのダブルスタンダードになってしまい個人的には気持ち悪いなと思っています。 それを解消する方法をこの記事では考えていきたいです。 Lambdaリソースの基本設計 設計方針 Lambdaのデプロイ方法はソースコードをzipで固めてS3に置くか、ECRにDocker Imageをプッシュするかを現在は選ぶことができます。 保守性が高く、どの環境でもちゃんとアプリケーションのデバッグができるという点ではDocker Imageの方がいいので、LambdaコードはECRにプッシュすることを選びます。 ちなみにサーバレスアプリケーションを作るときは、アプリケーションごとにリポジトリを作る方が良いとAWSの中の人に聞いたことがありますが、今回の用途としてはヘッダー書替え用やIP制限等のLambdaを扱いたいため1リポジトリでディレクトリを分けてデプロイします。 Lambdaリソースの管理方針 インフラリソースはTerraformで管理 Lambdaへのデプロイはソースコード側で行う CI/CDはGitHub Actionsで行う インフラリソースをTerraform管理にする ここではLambda Functionの管理とIAM、ECRの管理をします。 # IAM resource "aws_iam_role" "lambda_basic" { name = "lambda-basic-role" path = "/service-role/" assume_role_policy = data.aws_iam_policy_document.lambda_assume_role.json } data "aws_iam_policy_document" "lambda_assume_role" { statement { actions = ["sts:AssumeRole"] principals { type = "Service" identifiers = ["lambda.amazonaws.com", "edgelambda.amazonaws.com"] } } } resource "aws_iam_role_policy_attachment" "lambda_basic" { role = aws_iam_role.lambda_basic.name policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" } # ECR resource "aws_ecr_repository" "lambda1" { name = "lambda-function1" image_tag_mutability = "IMMUTABLE" image_scanning_configuration { scan_on_push = true } } # Lambda Function resource "aws_lambda_function" "lambda1" { function_name = "lambda-function1" role = aws_iam_role.lambda_basic.arn package_type = "Image" image_uri = "${aws_ecr_repository.lambda1.repository_url}:latest" timeout = 60 lifecycle { ignore_changes = [image_uri] } } # CloudWatch Logs ## ref https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/monitoring-cloudwatchlogs.html resource "aws_cloudwatch_log_group" "lambda1" { name = "/aws/lambda/${aws_lambda_function.lambda1.function_name}" retention_in_days = 30 } Lambdaコードを作成する 以下のようなディレクトリにコードを配置します。 言語はnode.jsにします。 $ tree . └── lambda-function1 ├── Dockerfile ├── index.js ├── package-lock.json └── package.json 各ファイルの中身は以下の通りです。 { "name": "lambda-container-image-example", "license": "MIT" } npm install して package-lock.json は作っておいてください。 index.js のコード内容は以下の通り。 exports.handler = (event, context, callback) => { const response = event.Records[0].cf.response; const headers = response.headers; const headerNameSrc = 'X-Amz-Meta-Last-Modified'; const headerNameDst = 'Last-Modified'; if (headers[headerNameSrc.toLowerCase()]) { headers[headerNameDst.toLowerCase()] = [ headers[headerNameSrc.toLowerCase()][0], ]; console.log(`Response header "${headerNameDst}" was set to ` + `"${headers[headerNameDst.toLowerCase()][0].value}"`); } callback(null, response); }; Dockerfileはこの通り # AWS ベースイメージを使用 FROM public.ecr.aws/lambda/nodejs:14 # ソースコードを関数のルートディレクトリにコピーします。 # 関数のルートディレクトリは `LAMBDA_TASK_ROOT` 環境変数を上書きすることで変更することができます ( デフォルトは `/var/task` ) 。 COPY index.js package.json package-lock.json /var/task/ RUN npm install # CMD にハンドラを設定します。 # Node.js の場合は `{ファイル名(拡張子なし)}.{関数名}` のように指定します。 # 今回は `index.js` の `handler` 関数をハンドラとして用意しているので以下のようになります。 CMD ["index.handler"] GitHub Actionsのworkflowを作成する Lambdaのソースコードおいてあるリポジトリでこちらのコードを設定してください。 GitHub Actionsで特定のディレクトリで操作された時にデプロイされるように設定してます。 name: "Deploy" on: push: branches: - main paths: - "lambda-function1/**" jobs: lambda: name: "Deploy Lambda" runs-on: ubuntu-latest defaults: run: shell: bash working-directory: ./lambda-function1/ steps: - name: Checkout uses: actions/checkout@v2 with: fetch-depth: 1 - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v1 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: ap-northeast-1 - name: Login to Amazon ECR id: login-ecr uses: aws-actions/amazon-ecr-login@v1 - name: docker build & docker push & lambda update run: | docker build -t XXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/lambda-function1:${{ github.sha }} . docker push XXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/lambda-function1:${{ github.sha }} aws lambda update-function-code --function-name lambda-function1 --image-uri XXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/lambda-function1:${{ github.sha }} Tips GitHub Actionsのマーケットプレイスにあるものは使えない GitHubActionsの公式にLambdaデプロイ用のコードがありますが、これはimage対応していないのでエラーになります。 参考資料: lambda-action 大元はこちらのコードですが、ファイルのみの対応になってしまっています。 image_uri だけ指定してデプロイを試みても400エラーになります。 参考資料: drone-lambda 結論 ServerlessFrameworkのようにそのフレームワークだけ使えばインフラのことを管理しなくても良いのですが、元々のインフラ設計がサーバーレスを中心とした設計でない場合、余計なインフラリソースが勝手に作られて運用が辛い経験がありました。 この方法を使えば、TerraformとLambdaのデプロイを分離できますし、ServerlessFrameworkでできるような余計なインフラリソースが増えてしまう心配がありませんし、不要になれば、Terraformから削除すればAWS環境も綺麗な状態を保てるのでお勧めです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Webpack5 + ESLint7 + Babel7環境構築【ES2021->ES5】

目標 Webpack、ESLint、Babelの環境を作成し、 ES2021で書いたスクリプトをESLintで構文チェックをかける。 構文チェックで問題がなければ、BabelでES5にJSファイルを変換し、1つのJSファイルを出力する。 構文チェックで問題があれば、ファイルを出力せず、エラーを出力する。 ES5に変換する理由 ・下記ブラウザ上でJSが動作する様にしたいので。 ・対象ブラウザ  ・IE11  ・Chrome 最新版  ・Edge 最新版  ・FireFox 最新版  ・Safari 最新版 ✳︎ECMAScriptのブラウザ対応状況についてはこちらをどうぞ GitHub 設定が面倒っていう人は、GitHubに1つリポジトリ作ったのでこちらからお試しください。 もし、うまく動かないなどありましたら、こちらのコメント欄か、GitHubのIssueにお願いします。 動作環境 MacOS Catalina version 10.15.7 Node.js version 16.5.0 npm version 7.19.1 Webpack version 5.45.1 Webpack-cli version 4.7.2 ESLint version 7.31.0 Babel-loader version 8.2.2 @babel/core version 7.14.8 @babel/preset-env version 7.14.8 glob version 7.1.7 前提条件 homebrewがインストール済 複数のJSファイルを1つにまとめる ES2021で書いたJSをES5に変換する 初期設定 nodebrewインストール brew install nodebrew 環境変数設定 今回は、.zshrcに記載しています。 .bashrcでもそんなに変わらないと思います。 //.zshrcを開く vim .zshrc //環境設定を.zshrc内に記載 export PATH=$PATH:<homepath>/.nodebrew/current/bin //.zshrc再読み込み source ~/.zshrc nodeインストール nodebrew install-binary latest 上記コマンドでは、最新のnodeをダウンロードします。 特定バージョンのnodeをダウンロードしたい場合は、下記の様にしてください。 //バージョン一覧確認 nodebrew ls-remote //バージョン指定してダウンロード nodebrew install-binary <version> nodeバージョン指定 && node,npmバージョン確認 //インストールしたバージョン確認 nodebrew ls //使用したバージョンを指定 nodebrew use <version> //nodeとnpmのバージョン確認 node -v // v16.5.0 などuseしたバージョンが表示される。 npm -v // 7.19.1など表示される。 メインディレクトリ作成 mkdir js-build-tool entry、output用ディレクトリ作成 entryはトランスパイル前のjsファイルを格納する場所 outputはトランスパイル後のjsファイルを格納する場所 cd js-build-tool //src = entry, dist = output mkdir src dist npmプロジェクト作成 npm init -y 上記コマンド入力後、package.jsonが作成されていればOKです。 Webpackインストール npm install --save-dev webpack webpack-cli 上記コマンド入力後、package.jsonのdevDependenciesに追加されていればOKです。 "devDependencies": { "webpack": "^5.45.1", "webpack-cli": "^4.7.2" }, globインストール globは指定ディレクトリ直下の全てのJSファイルを変換対象にするために導入します。 npm install glob 上記コマンド入力後、package.jsonのdependenciesに追加されていればOKです。 "dependencies": { "glob": "^7.1.7" } ESLintインストール npm install --save-dev eslint 上記コマンド入力後、package.jsonのdevDependenciesに追加されていればOKです。 "devDependencies": { "eslint": "^7.31.0", "webpack": "^5.45.1", "webpack-cli": "^4.7.2" }, .eslintrc.json作成 ESLintのルールを設定できるファイルを作成 jsonの他にも、.js, .ymlなどで作成可能 ここでは、設定に対する詳細は省きます。 ESLintの設定ファイルの各プロパティの意味を理解する .eslintrc.json { "root": true, "extends": "eslint:recommended", "rules": { //セミコロン強制 "semi": [ "error", "always" ], //セミコロン2つ続き禁止 "no-extra-semi": "error", //return, throw, continue, break後のコード禁止 "no-unreachable": "error", //宣言されていない変数禁止 "no-undef": "error", //console系禁止 "no-console": "error" }, "env": { "browser": true, "es2021": true, //jquery使用していなければ、不要 "jquery": true }, "parserOptions": { "sourceType": "module", "ecmaVersion": 2021 } } Babelインストール npm install --save-dev babel-loader @babel/core @babel/preset-env 上記コマンド入力後、package.jsonのdevDependenciesに追加されていればOKです。 "devDependencies": { "@babel/core": "^7.14.8", "@babel/preset-env": "^7.14.8", "babel-loader": "^8.2.2", "eslint": "^7.31.0", "webpack": "^5.45.1", "webpack-cli": "^4.7.2" }, webpack.config.js作成 こちらも、.eslintrc.json同様、詳細は省きます。 この投稿に詳しく書かれています。 webpack.config.js import path from 'path'; import webpack from 'webpack'; import glob from 'glob'; //src直下のjsファイルを全て取得している。 const entries = glob.sync("./src/**/*.js"); export default function(env) { //npm run の引数にファイル名を渡す const filename = env.filename; const __dirname = path.resolve() return { mode: 'production', entry: entries, output: {        //先ほど作成したdistフォルダに変換後のJSファイルを作成する様にしている path: `${__dirname}/dist`, filename: filename + '.js', }, module: { rules: [ { test: /\.js$/, use: [ { loader: "babel-loader", options: { presets: [ "@babel/preset-env", ] } } ] }, ], }, target: ["web", "es5"], plugins: [ new webpack.ProgressPlugin(), ], } } package.jsonのscripts用意 package.jsonのscriptsは、複数コマンドをまとめることができます。 こちらのサイトが参考になります。 今回の流れは、ESLintで構文チェックをかけて、エラーがあればそこで処理を止める。 エラーがなければ、BabelでES5にJSファイルを変換し、1つのJSファイルにまとめる。 //package.jsonを開く vim package.json //コマンドを追加する "scripts": { "build": "eslint ./src/**/*.js && webpack", }, 上記の様に記載することで、npm run build -- --env filename=hogehogeでESLint構文チェックとBabelの変換処理が走ります。 引数のfilenameには、トランスパイル後のJSファイル名を入力してください。 以上で設定は完了です。 ここからは、実際にこの環境を使って、複数JSファイルを1つにまとめてみましょう。 JSファイル準備 今回は、簡単なJSファイルを2つ用意します。 配列をconsoleに出力するだけです。 dog.js app.js dog.js export const dogs = ['pochi', 'wan', 'dog'] app.js import {dogs} from './dog.js'; console.log(dogs); app.jsを動かすと、下記の様に出力されるはずです。 node app.js //出力結果 [pachi, wan, dog] この2つのファイルを先ほど作成した、srcフォルダの中に入れましょう。 npm run buildしてみる(ESLintエラー発生) npm run build -- --env filename=bundle ここで、下記の様にESLintによるエラーが発生します。 $ npm run build -- --env filename=bundle > js-build-tool@1.0.0 build > eslint ./src/**/*.js && webpack “--env” “filename=bundle” /Users/hogehoge/Documents/js-build-tool/src/app.js 2:1 error Unexpected console statement no-console /Users/hogehoge/Documents/js-build-tool/src/dog.js 1:43 error Missing semicolon semi ×: 2 problems (2 errors, 0 warnings) 1 error and 0 warnings potentially fixable with the `--fix` option. まず、app.jsでは、console.logを使用しているのでエラー そして、dog.jsでは、最後のセミコロンが抜けているのでエラーになっています。 この段階で、distフォルダ内を確認しても、何も出力されていないはずです。 エラー修正 app.jsに関しては、console.logで出力は見たいので、.eslintrc.jsonを修正しましょう。 rules内に記載して、no-consoleをコメントアウトしてみましょう。 .eslintrc.json.rules "rules": { //セミコロン強制 "semi": [ "error", "always" ], //セミコロン2つ続き禁止 "no-extra-semi": "error", //return, throw, continue, break後のコード禁止 "no-unreachable": "error", //宣言されていない変数禁止 "no-undef": "error" //console系禁止 //"no-console": "error" }, dog.jsに関しては、最後のセミコロンをつけましょう。 dog.js export const dogs = ['pochi', 'wan', 'dog']; 再度npm run buildしてみる $ npm run build -- --env filename=bundle > js-build-tool@1.0.0 build > eslint ./src/**/*.js && webpack “--env” “filename=bundle” asset bundle.js 479 bytes [emitted] [minimized] (name: main) runtime modules 416 bytes 2 modules cacheable modules 209 bytes ./src/app.js 69 bytes [built] [code generated] ./src/dog.js 140 bytes [built] [code generated] webpack 5.45.1 compiled successfully in 1328 ms 特にエラーなく、トランスパイルに成功します。 distフォルダの中を見てみると、bundle.jsが作成されているはずです。 変換後のJSファイルを動かす node dist/bundle.js //出力結果を確認する ['pochi', 'wan', 'dog'] 変換前ファイルと同じ結果が得られればOKです。 参考URL Node.js Webpack ESLint 最新版で学ぶwebpack 5入門 Babel 7でES2020環境の構築 glob Github ESLintの設定ファイルの各プロパティの意味を理解する webpack.config.js の書き方をしっかり理解しよう Node.jsユーザーなら押さえておきたい npm-scriptsのタスク実行方法まとめ ECMAScript Compatibility Table
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Angular11】Cannot find namespace 'NodeJS'. にハマった話

環境 Angular CLI: 11.0.4 Node: 10.13.0 TypeScript: 2.9.2 やろうとしてたこと setInterval を使ったタイマを作成し、タイマーを止めるために返り値のNodeJS.Timeout 型のプロパティで保持しようとしていました。ただ、この型宣言で、「NodeJSっていう名前空間が見つからんよ!」って怒られちゃいました。 sample.component.ts private timer: NodeJS.Timeout; // Cannot find namespace 'NodeJS'. public setTimer() { this.timer = setInterval(()=>{}); } ハマったこと 修正方法を日本語・英語の両方で調べると、結構ヒットします。ただ、そのほとんどがtsconfig.json を修正する記事ばかりで、知識のない私が意味も理解せずに、その通りに修正しても直りませんでした。 解決策 src/tsconfig.app.json がある場合は、そちらのcompilerOptions のtypes にnode を追加で解決できました。 src/tsconfig.app.json { "extends": "./tsconfig.json", "compileOnSave": false, "compilerOptions": { "baseUrl": "./", "types": [ "node" // 追加 ], }, } @types/node をインストールしていなければ、下記コマンドでインストールします。 npm install --save-dev @types/node 何が違ったのか よくよく自分のプロジェクト構成をみると、継承したtsconfig.app.json がありました。いくらtsconfig.json を修正しても、そちらで上書きされてしまっていたようです。 なお、調べたサイトをちゃんと見返すとtsconfig.app.jsonって書いてありました。。。先入観で記事を見てはダメですね。。。 参考 styled-componentsの「Cannot find namespace 'NodeJS'.」 - Qiita Cannot find namespace 'NodeJS' - Stack Overflow
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

npx gulp watchでError: Node Sass does not yet support your current environment: OS X 64-bit with Unsupported runtime (93)

npx gulp watchを実行すると $ npx gulp watch Error: Node Sass does not yet support your current environment: OS X 64-bit with Unsupported runtime (93) For more information on which environments are supported please see: https://github.com/sass/node-sass/releases/tag/v4.14.1 以下略 エラー文の通り、https://github.com/sass/node-sass/releases/tag/v4.14.1にアクセスして確認すると、下の表の通り。 Supported Environments OS Architecture Node Windows x86 & x64 0.10, 0.12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14 OSX x64 0.10, 0.12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14 Linux x86 & x64 0.10, 0.12, 1, 2, 3, 4, 5, 6, 7, 8*, 9, 10^, 11^, 12^, 13^, 14*^ Alpine Linux x64 6, 8, 10, 11, 12, 13, 14 FreeBSD i386 amd64 10, 12, 13 サポートはNodeのバージョン14までになっている。 バージョン確認 $ node --version v16.4.2 手元の環境はバージョン16になっていました。 Nodeアンインストール brew uninstall node Error: Refusing to uninstall /usr/local/Cellar/node/16.4.0 because it is required by yarn, which is currently installed. You can override this and force removal with: brew uninstall --ignore-dependencies node $ brew uninstall --ignore-dependencies node Uninstalling /usr/local/Cellar/node/16.4.0... (2,459 files, 48.1MB) エラーが出ましたが指示に従いつつアンインストール成功 Nodeのインストール nvm install --lts --latest-npm Installing latest LTS version. Downloading and installing node v14.17.3... Downloading https://nodejs.org/dist/v14.17.3/node-v14.17.3-darwin-x64.tar.xz... ######################################################################### 100.0% Computing checksum with sha256sum Checksums matched! Now using node v14.17.3 (npm v6.14.13) 以下略 サポートされている最新のNodeをインストールできました。 rebuildしてgulp watch $npm rebuild node-sass rebuilt dependencies successfully $ npx gulp watch [15:25:21] Using gulpfile ~/hoge/gulpfile.js [15:25:21] Starting 'watch'...
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【AWS Lambda×LINE Messaging API】AWS SAMで翻訳アプリを作ろう

はじめに 皆さん、Lambdaをご存知でしょうか? Lambdaはサーバーレスアーキテクチャを実現する上で根幹となるサービスです。 サーバーレスアーキテクチャとは AWSにおけるサーバーレスとは、「インスタンスベースの仮想サーバー(EC2など)を使わずにアプリケーションを開発するアーキテクチャ」を指します。 一般にシステムの運用には、プログラムを動かすためのサーバーが必要です。 そしてそのサーバーは、常に稼働していなければなりません。 しかし開発者がやりたいことは、「サーバーの管理」なのでしょうか? エンドユーザーに価値を届けることこそが使命なわけです。 ということで、こういうめんどくさい作業から解放してくれるのがサーバーレスアーキテクチャなわけです。 サーバーレスアーキテクチャでよく使われるサービスは以下の通りです。 特に、丸で囲っている3つがよく使われます。 ということで、この3つ全てを使った翻訳アプリを作りたいと思います。 また、構成やデプロイはAWS SAMを使用します。 AWS SAMを使うことでコマンドのみで環境構築やデプロイを行えます。 アーキテクチャ 以下の2つの条件を満たしたら成功です。 ①LINEで「こんにちは」と入力したら、「Hello」と返ってくる ②タイムスタンプと「こんにちは」、「Hello」がDBに保存される GitHub 完成形のコードは以下となります。 ハンズオン 前提 初めてAWSを使う方に対しての注意です。 ルートユーザーで行うのはよろしくないので、全ての権限を与えたAdministratorユーザーを作っておいてください。 公式サイトはこちらです。 文章は辛いよって方は、初学者のハンズオン動画があるのでこちらからどうぞ。 sam initを実行する ゼロから書いていってもいいのですが、初めての方はまずはsam initを使いましょう。 以下のように選択していってください。 ターミナル $ sam init Which template source would you like to use? 1 - AWS Quick Start Templates 2 - Custom Template Location Choice: 1 What package type would you like to use? 1 - Zip (artifact is a zip uploaded to S3) 2 - Image (artifact is an image uploaded to an ECR image repository) Package type: 1 Which runtime would you like to use? 1 - nodejs14.x 2 - python3.8 3 - ruby2.7 4 - go1.x 5 - java11 6 - dotnetcore3.1 7 - nodejs12.x 8 - nodejs10.x 9 - python3.7 10 - python3.6 11 - python2.7 12 - ruby2.5 13 - java8.al2 14 - java8 15 - dotnetcore2.1 Runtime: 7 Project name [sam-app]: Translate AWS quick start application templates: 1 - Hello World Example 2 - Step Functions Sample App (Stock Trader) 3 - Quick Start: From Scratch 4 - Quick Start: Scheduled Events 5 - Quick Start: S3 6 - Quick Start: SNS 7 - Quick Start: SQS 8 - Quick Start: App Backend using TypeScript 9 - Quick Start: Web Backend Template selection: 1 ここまでできれば作成されます。 このような構成になっていればOKです。 .Translate ├── events/ │ ├── event.json ├── hello-world/ │ ├── tests │ │ └── integration │ │ │ └── test-api-gateway.js │ │ └── unit │ │ │ └── test-handler.js │ ├── .npmignore │ ├── app.js │ ├── package.json ├── .gitignore ├── README.md ├── template.yaml 必要ないファイルなどがあるのでそれを削除していきましょう。 .Translate ├── hello-world/ │ ├── app.js ├── .gitignore ├── README.md ├── template.yaml また、ディレクトリ名やファイル名を変えましょう。 .Translate ├── api/ │ ├── index.js ├── .gitignore ├── README.md ├── template.yaml 次は、template.yamlを修正して、SAMの実行をしてみたいところですが、一旦後回しにします。 先にTypeScriptなどのパッケージを入れ、ディレクトリ構造を明確にした後の方が理解しやすいので。。 ということでパッケージを入れていきましょう。 package.jsonの作成 以下のコマンドを入力してください。 これで、package.jsonの作成が完了します。 ターミナル $ npm init -y 必要なパッケージのインストール dependencies dependenciesはすべてのステージで使用するパッケージです。 今回使用するパッケージは以下の4つです。 ・@line/bot-sdk ・aws-sdk 以下のコマンドを入力してください。 これで全てのパッケージがインストールされます。 ターミナル $ npm install @line/bot-sdk aws-sdk --save ちなみに、Lambdaでは元よりaws-sdkが使えるようなのでなくても問題ないです。 インストールしなければその分容量が軽くなるので、レスポンスは早くなります。 devDependencies devDependenciesはコーディングステージのみで使用するパッケージです。 今回使用するパッケージは以下の5つです。 ・typescript ・@types/node ・ts-node ・rimraf ・npm-run-all 以下のコマンドを入力してください。 これで全てのパッケージがインストールされます。 ターミナル $ npm install -D typescript @types/node ts-node rimraf npm-run-all package.jsonにコマンドの設定を行う npm run buildでコンパイルを行います。 package.json { "scripts": { "clean": "rimraf dist", "tsc": "tsc", "build": "npm-run-all clean tsc" }, } tsconfig.jsonの作成 以下のコマンドを実行しTypeScriptの初期設定を行います。 ターミナル $ npx tsc --init それでは、作成されたtsconfig.jsonの上書きをしていきます。 tsconfig.json { "compilerOptions": { "target": "ES2018", "module": "commonjs", "sourceMap": true, "outDir": "./api/dist", "strict": true, "moduleResolution": "node", "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, "include": ["api/src/*"] } 簡単にまとめると、 api/srcディレクトリ以下を対象として、それらをapi/distディレクトリにES2018の書き方でビルドされるという設定です。 tsconfig.jsonに関して詳しく知りたい方は以下のサイトをどうぞ。 また、この辺りで必要ないディレクトリはGithubにpushしたくないので、.gitignoreも作成しておきましょう。 .gitignore node_modules package-lock.json .aws-sam samconfig.toml dist 最終的にはこのようなディレクトリ構成にしましょう。 .WeatherFashion ├── api/ │ ├── dist(コンパイル後) │ │ └── node_modules(コピーする) │ │ └── package.json(コピーする) │ ├── src(コンパイル前) │ │ └── index.ts ├── node_modules(コピー元) ├── .gitignore ├── package.json(コピー元) ├── package-lock.json ├── README.md ├── template.yaml ├── tsconfig.json やるべきことは以下の2つです。 ①distディレクトリを作成する ②distディレクトリに、node_modules, package.jsonをコピーする 次に、template.yamlを書いていきましょう。 SAM Templateを記載する ファイル内にコメントを残しています。 これで大まかには理解できるかと思います。 詳しくは公式サイトを見てください。 template.yaml # AWS CloudFormationテンプレートのバージョン AWSTemplateFormatVersion: '2010-09-09' # CloudFormationではなくSAMを使うと明記する Transform: AWS::Serverless-2016-10-31 # CloudFormationのスタックの説明文(重要ではないので適当でOK) Description: > Translate Globals: # Lambda関数のタイムアウト値(3秒に設定) Function: Timeout: 3 Resources: # API Gateway TranslateAPI: # Typeを指定する(今回はAPI Gateway) Type: AWS::Serverless::Api Properties: # ステージ名(APIのURLの最後にこのステージ名が付与されます) StageName: v1 # DynamoDB TranslateDynamoDB: # Typeを指定する(今回はDynamoDB) Type: AWS::Serverless::SimpleTable Properties: # テーブルの名前 TableName: translations # プライマリキーの設定(名前とプライマリキーのタイプ) PrimaryKey: Name: TimeStamp Type: String # プロビジョニングされたキャパシティの設定(今回の要件では最小の1でOK) ProvisionedThroughput: ReadCapacityUnits: 1 WriteCapacityUnits: 1 # Lambda TranslateFunction: # Typeを指定する(今回はLambda) Type: AWS::Serverless::Function Properties: # 関数が格納されているディレクトリ(今回はコンパイル後なので、distディレクトリを使用する) CodeUri: api/dist # ファイル名と関数名(今回はファイル名がindex.js、関数名がexports.handlerなので、index.handlerとなります) Handler: index.handler # どの言語とどのバージョンを使用するか Runtime: nodejs12.x # ポリシーを付与する(今回はLambdaの権限とSSMの読み取り権限とDynamoDBのフルアクセス権限とAmazon translateのフルアクセス権限を付与) Policies: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AmazonSSMReadOnlyAccess - arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess - arn:aws:iam::aws:policy/TranslateFullAccess # この関数をトリガーするイベントを指定します Events: # API Gateway TranslateAPI: Type: Api Properties: # どのAPIを使用するか(!Refは値の参照に使用します) RestApiId: !Ref TranslateAPI # URL Path: / # POSTメソッド Method: post Outputs: TranslateAPI: Description: 'API Gateway' # URLを作成(!Subは${}で値を指定することができます) Value: !Sub 'https://${TranslateAPI}.execute-api.${AWS::Region}.amazonaws.com/v1' TranslateFunction: Description: 'Lambda' # ロールの値を返す Value: !GetAtt TranslateFunction.Arn TranslateFunctionIamRole: Description: 'IAM Role' # ロールの値を返す Value: !GetAtt TranslateFunctionRole.Arn LINE Developersにアカウントを作成する LINE Developersにアクセスして、「ログイン」ボタンをクリックしてください。 その後諸々入力してもらったら以下のように作成できるかと思います。 注意事項としては、今回Messaging APIとなるので、チャネルの種類を間違えた方は修正してください。 チャネルシークレットとチャネルアクセストークンが必要になるのでこの2つを発行します。 これで必要な環境変数は取得できました。 それでは、これをSSMを使ってLambda内で使えるようにしていきましょう。 SSMパラメータストアで環境変数を設定 なぜSSMパラメータストアを使うのか? SAMのLambda設定にも、環境変数の項目はあります。 しかし、2点問題点があります。 ①Lambdaの環境変数の変更をしたいとき、Lambdaのバージョンも新規発行をしなければならない ②Lambdaのバージョンとエイリアスを紐付けて管理をするとき、もし環境変数にリリース先環境別の値をセットしていると、リリース時に手動で環境変数の変更をしなければならないケースが発生する 簡単にまとめると、「リアルタイムで反映できないし、人為的なミスのリスクもあるよ」ということです。 SSMパラメータストアで値を管理すると以下の3点のメリットがあります。 ①Lambdaの環境変数の管理が不要 ②Lambdaも含めた値関連情報を一元管理できる ③Lambda外部からリアルタイムに環境変数を変更制御できる ということで、SSMパラメータストアを使用しましょう。 みんな大好きクラスメソッドの記事にやり方が書いてあります。 こちらの記事が完璧なのでこちらを見てやってみてください。 私は以下のように命名して作成しました。 SSMパラメータが取得できているかconsole.logで検証 api/src/index.ts // import import aws from 'aws-sdk'; // SSM const ssm = new aws.SSM(); exports.handler = async (event: any, context: any) => { const LINE_TRANSLATE_CHANNEL_ACCESS_TOKEN = { Name: 'LINE_TRANSLATE_CHANNEL_ACCESS_TOKEN', WithDecryption: false, }; const CHANNEL_ACCESS_TOKEN: any = await ssm .getParameter(LINE_TRANSLATE_CHANNEL_ACCESS_TOKEN) .promise(); const channelAccessToken: string = CHANNEL_ACCESS_TOKEN.Parameter.Value; console.log('channelAccessToken: ' + channelAccessToken); }; これをコンパイルしてデプロイしていきましょう。 ターミナル // コンパイル $ npm run build // ビルド $ sam build // デプロイ $ sam deploy --guided Configuring SAM deploy ====================== Looking for samconfig.toml : Not found Setting default arguments for 'sam deploy' ========================================= // CloudFormation スタック名の指定 Stack Name [sam-app]: Translate // リージョンの指定 AWS Region [us-east-1]: ap-northeast-1 // デプロイ前にCloudformationの変更セットを確認するか #Shows you resources changes to be deployed and require a 'Y' to initiate deploy Confirm changes before deploy [y/N]: y // SAM CLI に IAM ロールの作成を許可するか(CAPABILITY_IAM) #SAM needs permission to be able to create roles to connect to the resources in your template Allow SAM CLI IAM role creation [Y/n]: y // API イベントタイプの関数に認証が含まれていない場合、警告される HelloWorldFunction may not have authorization defined, Is this okay? [y/N]: y // この設定を samconfig.toml として保存するか Save arguments to samconfig.toml [Y/n]: y これでデプロイが完了します。 では、API GatewayのURLを確認しましょう。 Webhook URLの登録 先ほどAPI Gatewayで作成したhttpsのURLをコピーしてください。 これをLINE DevelopersのWebhookに設定します。 それではSSMパラメータが正しく取得できているか確認しましょう。 CloudWatchで確認しましょう! 取得できていますね! ここからの流れはこのような感じです。 ①翻訳機能を作成 ②翻訳された言葉をDBに保存 今回は、翻訳する部分、DBにデータを登録する部分と様々な機能があるため動作ごとにファイルを切り分けてあげましょう。 以下のように作っていきます。 . ├── api/ │ ├── src/ │ │ ├── Common/ │ │ └── getTranslate.ts │ │ └── putDynamoDB.ts │ └── index.ts またここからはLINEBotのオリジナルの型が頻出します。 1つずつ説明するのはあまりに時間がかかるので、知らない型が出てきたらその度に以下のサイトで検索するようにしてください。 ①翻訳機能を作成 api/src/index.ts // パッケージのインストール import { ClientConfig, Client, WebhookEvent, TextMessage } from '@line/bot-sdk'; import aws from 'aws-sdk'; // モジュールのインストール import { getTranslate } from './Common/getTranslate'; // SSM const ssm = new aws.SSM(); const LINE_TRANSLATE_CHANNEL_ACCESS_TOKEN = { Name: 'LINE_TRANSLATE_CHANNEL_ACCESS_TOKEN', WithDecryption: false, }; const LINE_TRANSLATE_CHANNEL_SECRET = { Name: 'LINE_TRANSLATE_CHANNEL_SECRET', WithDecryption: false, }; exports.handler = async (event: any, context: any) => { try { // SSM (.env) const CHANNEL_ACCESS_TOKEN: any = await ssm .getParameter(LINE_TRANSLATE_CHANNEL_ACCESS_TOKEN) .promise(); const CHANNEL_SECRET: any = await ssm.getParameter(LINE_TRANSLATE_CHANNEL_SECRET).promise(); const channelAccessToken: string = CHANNEL_ACCESS_TOKEN.Parameter.Value; const channelSecret: string = CHANNEL_SECRET.Parameter.Value; // client const clientConfig: ClientConfig = { channelAccessToken: channelAccessToken, channelSecret: channelSecret, }; const client: Client = new Client(clientConfig); // JSONとして解析して値やオブジェクトを構築する const body: any = JSON.parse(event.body); // LINE Eventを取得 const response: WebhookEvent = body.events[0]; // 送られるメッセージがテキスト以外の場合 if (response.type !== 'message' || response.message.type !== 'text') { return; } // 翻訳を行うために必要な情報 const input_text: string = response.message.text; const sourceLang: string = 'ja'; const targetLang: string = 'en'; const res: any = await getTranslate(input_text, sourceLang, targetLang); const output_text: string = res.TranslatedText; // メッセージ送信のために必要な情報 const replyToken = response.replyToken; const post: TextMessage = { type: 'text', text: output_text, }; // メッセージの送信 await client.replyMessage(replyToken, post); } catch (err) { console.log(err); } }; では次に、getTranslate.tsを作っていきましょう。 コードだけ書いても訳がわからないと思うので、リファレンスを見ましょう。 入力されたテキストをソース言語からターゲット言語に変換する、translateTextを使います。 必須項目は以下の3つで、SourceLanguageCodeに元の言語コード、TargetLanguageCodeに変換先の言語コード、Textに変換するテキストを入れればいいことがわかります。 SourceLanguageCode: 'STRING_VALUE', /* required */ TargetLanguageCode: 'STRING_VALUE', /* required */ Text: 'STRING_VALUE', /* required */ そのあとはこのデータを実行するだけです。 translate.translateText(params, function(err, data) { if (err) console.log(err, err.stack); // an error occurred else console.log(data); // successful response }); APIが理解できたところで進めていきましょう。 api/src/Common/getTranslate.ts // パッケージのインストール import aws from 'aws-sdk'; // 必要なAWSサービス const translate = new aws.Translate(); export const getTranslate = (input: string, inLang: string, outLang: string) => { return new Promise((resolve, reject) => { // 必要なデータ const params = { Text: input, SourceLanguageCode: inLang, TargetLanguageCode: outLang, }; // 翻訳を行う translate.translateText(params, (err, data) => { if (err) { console.log(err); reject(); } else { resolve(data); } }); }); }; ②翻訳された言葉をDBに保存 api/src/index.ts // パッケージのインストール import { ClientConfig, Client, WebhookEvent, TextMessage } from '@line/bot-sdk'; import aws from 'aws-sdk'; // モジュールのインストール import { getTranslate } from './Common/getTranslate'; import { putDynamoDB } from './Common/putDynamoDB'; // SSM const ssm = new aws.SSM(); const LINE_TRANSLATE_CHANNEL_ACCESS_TOKEN = { Name: 'LINE_TRANSLATE_CHANNEL_ACCESS_TOKEN', WithDecryption: false, }; const LINE_TRANSLATE_CHANNEL_SECRET = { Name: 'LINE_TRANSLATE_CHANNEL_SECRET', WithDecryption: false, }; exports.handler = async (event: any, context: any) => { try { // SSM (.env) const CHANNEL_ACCESS_TOKEN: any = await ssm .getParameter(LINE_TRANSLATE_CHANNEL_ACCESS_TOKEN) .promise(); const CHANNEL_SECRET: any = await ssm.getParameter(LINE_TRANSLATE_CHANNEL_SECRET).promise(); const channelAccessToken: string = CHANNEL_ACCESS_TOKEN.Parameter.Value; const channelSecret: string = CHANNEL_SECRET.Parameter.Value; // client const clientConfig: ClientConfig = { channelAccessToken: channelAccessToken, channelSecret: channelSecret, }; const client: Client = new Client(clientConfig); // JSONとして解析して値やオブジェクトを構築する const body: any = JSON.parse(event.body); // LINE Eventを取得 const response: WebhookEvent = body.events[0]; // 送られるメッセージがテキスト以外の場合 if (response.type !== 'message' || response.message.type !== 'text') { return; } // 翻訳を行うために必要な情報 const input_text: string = response.message.text; const sourceLang: string = 'ja'; const targetLang: string = 'en'; const res: any = await getTranslate(input_text, sourceLang, targetLang); const output_text: string = res.TranslatedText; // メッセージ送信のために必要な情報 const replyToken = response.replyToken; const post: TextMessage = { type: 'text', text: output_text, }; // メッセージの送信 await client.replyMessage(replyToken, post); // DB-タイムスタンプ const date = new Date(); const Y = date.getFullYear(); const M = ('00' + (date.getMonth() + 1)).slice(-2); const D = ('00' + date.getDate()).slice(-2); const h = ('00' + (date.getHours() + 9)).slice(-2); const m = ('00' + date.getMinutes()).slice(-2); const s = ('00' + date.getSeconds()).slice(-2); const dayTime = Y + M + D + h + m + s; // DynamoDB保存 await putDynamoDB(dayTime, input_text, output_text); } catch (err) { console.log(err); } }; 次に、putDynamoDB.tsを作ります。 コードだけ書いても訳がわからないと思うので、リファレンスを見ましょう。 アイテム(レコード)を作成したいので、putItemを使います。 必須項目は以下の3つで、Itemにデータ、ReturnConsumedCapacityに集計、TableNameにテーブルの名前を入れればいいことがわかります。 var params = { Item: { "AlbumTitle": { S: "Somewhat Famous" }, "Artist": { S: "No One You Know" }, "SongTitle": { S: "Call Me Today" } }, ReturnConsumedCapacity: "TOTAL", TableName: "Music" }; そのあとはこのデータを実行するだけです。 dynamodb.putItem(params, function(err, data) { if (err) console.log(err, err.stack); // an error occurred else console.log(data); // successful response }); APIが理解できたところで進めていきましょう。 api/src/Common/putDynamoDB.ts // パッケージのインストール import aws from 'aws-sdk'; // 必要なAWSサービス const dynamodb = new aws.DynamoDB(); export const putDynamoDB = (dayTime: string, input: string, output: string) => { return new Promise((resolve, reject) => { const params = { Item: { TimeStamp: { S: dayTime, }, InputText: { S: input, }, OutputText: { S: output, }, }, ReturnConsumedCapacity: 'TOTAL', TableName: 'translations', }; dynamodb.putItem(params, (err, data) => { if (err) { console.log(err); reject(err); } else { resolve(data); } }); }); }; これで完成です! では、デプロイしていきましょう。 デプロイ まずは、npm run buildでコンパイルしましょう。 ターミナル $ npm run build コンパイルされた後は、ビルドしてデプロイしていきましょう。 ターミナル // ビルド $ sam build // デプロイ $ sam deploy --guided 最後に動作検証をしましょう。 DynamoDBも確認しましょう。 しっかり保存されていますね! 最後に 以前すべて手作業で行いましたが、SAMを使うと効率的にデプロイが行えます。 SAMテンプレートの書き方を学ぶコストは発生しますが、1度作ればそれをそのまま使えるので汎用性も高いのでおすすめです。 サーバーレスアーキテクチャを勉強する方がいましたらぜひSAMも勉強してみてください!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

webpack5

最近、webpack ver.4 -> ver.5 にアプデした途端に、ビルドエラーが起きるようになったが、 原因がわかりにくかったのでメモ書き 背景 webpack4でWeb開発していましたが、 yarn audit で webpack4の脆弱性警告発生 脆弱性対応のためにwebpack4->webpack5 にアプデすることになった 問題 webpack のメジャーバージョンアップ ということで、以下の手順書を読みながら移行 https://webpack.js.org/migrate/5/ webpack.common.js で、loaderをuseに手動で変更する必要があった点などわかりにくいので注意 (options 引数が無い場合、loader は使えないらしい) まあこの辺はビルドエラーメッセージに出てくるので恐らく大丈夫だと思います 一通りの手順を踏んだが、以下のエラーが発生 「Module not found: Error: Can't resolve "○○"」 ○○には、ファイルパスが入ります 解決方法 ググってみても、似た問題はたくさんヒットするが、解消できる記事は見当たらず webpackの実行時のコンソールログを出しまくって解析 どうやら、「○○」というファイルに記載されている型定義が読み込めていない模様 そのファイルをimport している箇所を検索して見比べてみると、 ファイルパスの末尾にスラッシュが入っているかどうかにバラつきがあった import { XXX } from '****' or import { XXX } from '****/' ←この書き方だとNGの場合がある 末尾のスラッシュを除去すると、webpackのビルド成功 原因 原因としては、webpack4では問題なかった書き方が、webpack5になったタイミングで問題になった ファイルパスの末尾にスラッシュを付けると、 webpack4 まではいい感じに型定義を探してきてくれたが、 webpack5 になると、必ず ****/index.d.ts を探しにいってしまうようになった パッケージのフォルダ構成次第で、****/index.d.ts に型定義は入っていないものがあると、そこでimport エラー スラッシュを除去したファイルパスにすれば解消 この原因をわかりにくくしている要因として、 該当するパッケージを最初にimport した時点で問題ないと、 その後のimport でファイルパスの書き方に問題があった場合(スラッシュをつけてしまっている場合)、 webpackがパッケージの内容をキャッシュしているのでimportエラーが起きない つまり、再現性が高くなく、100%起きるわけではないことが挙げられる まとめ webpack5にあげた途端に急にimportエラーでビルドエラーが起きるようになった件に対して、 import しているファイルパスの末尾スラッシュを除去すると解消するかも 同じお悩みを抱えて検索して来てくださった方のお役に立てればと思います 参考になったという方はGoodボタン押して頂けると嬉しいです
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

webpack4->webpack5で「Module not found: Error: Can't resolve "○○"」というエラーが解消しない場合の対処方法

最近、webpack ver.4 -> ver.5 にアプデした途端に、ビルドエラーが起きるようになったが、 原因がわかりにくかったのでメモ書き 背景 webpack4でWeb開発していましたが、 yarn audit で webpack4の脆弱性警告発生 脆弱性対応のためにwebpack4->webpack5 にアプデすることになった 問題 webpack のメジャーバージョンアップ ということで、以下の手順書を読みながら移行 https://webpack.js.org/migrate/5/ webpack.config.js で、loaderをuseに手動で変更する必要があった点などわかりにくいので注意 (options 引数が無い場合、loader は使えないらしい) まあこの辺はビルドエラーメッセージに出てくるので恐らく大丈夫だと思います 一通りの手順を踏んだが、以下のエラーが発生(○○には、ファイルパスが入ります) 「Module not found: Error: Can't resolve "○○"」 What's? 解決方法 ググってみても、似た問題はたくさんヒットするが、解消できる記事は見当たらず webpackの実行時のコンソールログを出しまくって解析 どうやら、「○○」というファイルに記載されている型定義が読み込めていない模様 そのファイルをimport している箇所を検索して見比べてみると、 ファイルパスの末尾にスラッシュが入っているかどうかにバラつきがあった import { XXX } from '****' or import { XXX } from '****/' ←この書き方だとNGの場合がある 末尾のスラッシュを除去すると、webpackのビルド成功 原因 原因としては、webpack4では問題なかった書き方が、webpack5になったタイミングで問題になった ファイルパスの末尾にスラッシュを付けると、 webpack4 まではいい感じに型定義を探してきてくれたが、 webpack5 になると、必ず ****/index.d.ts を探しにいってしまうようになった パッケージのフォルダ構成次第で、****/index.d.ts に型定義は入っていないものがあると、そこでimport エラー スラッシュを除去したファイルパスにすれば解消 (もしくは、型定義ファイルindex.d.ts への正しいファイルパスを入れてもOK) この原因をわかりにくくしている要因として、 該当するパッケージを最初にimport した時点で問題ないと、 その後のimport でファイルパスの書き方に問題があった場合(スラッシュをつけてしまっている場合)、 webpackがパッケージの内容をキャッシュしているのでimportエラーが起きない つまり、再現性が高くなく、100%起きるわけではないことが挙げられる まとめ webpack5にあげた途端に急にimportエラーでビルドエラーが起きるようになった件に対して、 import しているファイルパスの末尾スラッシュを除去すると解消するかも 同じお悩みを抱えて検索して来てくださった方のお役に立てればと思います 参考になったという方はGoodボタン押して頂けると嬉しいです
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

webpack5で「Module not found: Error: Can't resolve "○○"」というエラーが解消しない場合の対処方法

最近、webpack ver.4 -> ver.5 にアプデした途端に、ビルドエラーが起きるようになったが、 原因がわかりにくかったのでメモ書き 背景 webpack4でWeb開発していましたが、 yarn audit で webpack4の脆弱性警告発生 脆弱性対応のためにwebpack4->webpack5 にアプデすることになった 問題 webpack のメジャーバージョンアップ ということで、以下の手順書を読みながら移行 https://webpack.js.org/migrate/5/ webpack.config.js で、loaderをuseに手動で変更する必要があった点などわかりにくいので注意 (options 引数が無い場合、loader は使えないらしい) まあこの辺はビルドエラーメッセージに出てくるので恐らく大丈夫だと思います 一通りの手順を踏んだが、以下のエラーが発生(○○○には、ファイルパスが入ります) Module not found: Error: Can't resolve "○○○" What's? 解決方法 ググってみても、似た問題はたくさんヒットするが、解消できる記事は見当たらず webpackの実行時のコンソールログを出しまくって解析 どうやら、「○○○」というファイルに記載されている型定義が読み込めていない模様 そのファイルをimport している箇所を検索して見比べてみると、 ファイルパスの末尾にスラッシュが入っているかどうかにバラつきがあった import { ○○○ } from '○○○'; or import { ○○○ } from '○○○/'; // ←この書き方だとNGの場合がある 末尾のスラッシュを除去すると、webpackのビルド成功 Why? 原因 原因は、webpack4では問題なかった import の書き方が、webpack5になったタイミングで問題になったこと import の書き方の問題とは? import するファイルのファイルパスの末尾にスラッシュを付けると、 webpack4 まではいい感じに型定義を探してきてくれたが、 webpack5 になると、必ず 「○○○/index.d.ts」 を探しにいってしまうようになった パッケージのフォルダ構成次第で、○○○/index.d.ts に型定義が入っていないものがあると、そこでimport エラー スラッシュを除去したファイルパスにすれば解消した (もしくは、型定義ファイルindex.d.ts への正しいファイルパスを入れてもOK) 今回我々の例だと、kintoneのUIライブラリ(kintone-ui-component)でimport エラー ○○○/index.d.ts ではなく、その下に掘られたフォルダ内に型定義ファイルが存在していた なぜこの問題がわかりにくいのか まず、エラーメッセージが、OSSで「○○○の定義がありません」と出るので、 OSSに問題があるのでは?とyarn install し直したりキャッシュを削除したりNodeを入れ直したりといった 誤った対処方法を取ろうとしてしまいがち OSSには問題がないので、上記対処方法をとっても解消しません さらに、この原因をわかりにくくしている要因として、 最初にパッケージをimport した時点で問題ない場合、 その後のimport でファイルパスの書き方に問題があってもimportエラーが起きない(!!) ※webpackが一度型定義を読み込めたらそれを流用するためだと考えられる ファイルパスの末尾のスラッシュ有無にバラツキがあると、ビルドが成功したりエラーになったり 実装状況や環境によって変化する まとめ import文のファイルパスの末尾スラッシュは付けないのが正しいんだろう webpack5にあげた途端に急にimportエラーでビルドエラーが起きるようになった件に対して、 import しているファイルパスの末尾スラッシュを除去すると解消するかも 同じお悩みを抱えて検索して来てくださった方のお役に立てればと思います 参考になったという方はGoodボタン押して頂けると嬉しいです
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

jestでfirebase-adminのauth().verifyIdToken()をモックする

環境 TypeScript, Node(Hapi) コード import admin from "firebase-admin"; const decoded = adimn.auth().verifyIdToken(token); このverifyIdToken(token)で返すデータをJestでモックしたかった。 テスト test.ts let spy: jest.SpyInstance; afterEach(() => { spy.mockRestore(); }); test("テスト", () => { const admin = require("firebase-admin"); const mockVerifyIdToken = jest.fn().mockResolvedValue({ uid: "uid" }); const mockAuth = jest.fn().mockReturnValue({ verifyIdToken: mockVerifyIdToken }); spy = jest.spyOn(admin, "app").mockReturnValue({ auth: mockAuth }); }) 複数のテストで違う値を返したかったのでspyOnを使いました。 これで const decoded = adimn.auth().verifyIdToken(token); の部分は{ uid: "uid" }を返しました。 ちゃんとは読んでないけど参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Node.js, TypeScript】LINE MessagingAPIで作った天気予報アプリをAWS SAMを使ってデプロイしてみる!

先日、Node.jsとTypeScriptで天気予報アプリを作成しました。 完成形としては以下の通りです。 この記事内では、Glitchでデプロイをしています。 まぁ無料なわけで色々問題があります。 ・プロジェクトは、利用されていないときは5分でスリープ状態になる ・4000件/1hのリクエスト制限がある(Error: 429 too many requests) ということで、AWSのLambdaを使ってデプロイしました。 動作を確認するために所々デバッグして進めていきたいですが、 その度S3にアップロードしてそれをLambdaにもアップロードする必要があります。 こんなのめんどくさいですよね・・ ということで、ローカルでデバッグやテストを可能にしてくれる、かつコマンドのみでデプロイしてくれるSAMを使ってみましょう。 作成後記 LINE MessagingAPIではHTTPSサーバーが必要になります。しかし、SAMではHTTPサーバーしか作れないのでデバッグは不可能でした。それでもコマンドのみでデプロイできるのは便利でした。 アーキテクチャ アーキテクチャの説明の前にまずは、サーバーレスアーキテクチャに関して説明します。 サーバーレスアーキテクチャとは AWSにおけるサーバーレスとは、「インスタンスベースの仮想サーバー(EC2など)を使わずにアプリケーションを開発するアーキテクチャ」を指します。 一般にシステムの運用には、プログラムを動かすためのサーバーが必要です。 そしてそのサーバーは、常に稼働していなければなりません。 しかし開発者がやりたいことは、「サーバーの管理」なのでしょうか? エンドユーザーに価値を届けることこそが使命なわけです。 ということで、こういうめんどくさい作業から解放してくれるのがサーバーレスアーキテクチャなわけです。 サーバーレスアーキテクチャでよく使われるサービスは以下の通りです。 特に、丸で囲っている3つがよく使われます。 それではアーキテクチャに関してみていきましょう。 今回は、Lambda, API Gateway, S3の3つをSAMでデプロイを行い、環境変数をSSM(AWS Systems Manager)で管理していきます。 追記 AWSのEC2を使ってデプロイした記事もあります。 サーバーレスよりもEC2に興味があるぞという方はこちらの記事もどうぞ。 どのようなアプリか 皆さんは、今日の気温を聞いて、「快適に過ごすために今日のファッションをこうしよう」ってパッと思いつきますか? 私は、最高気温、最低気温を聞いてもそれがどのくらい暑いのか、寒いのかがピンと来ず、洋服のチョイスを外したことがしばしばあります。 こんな思いを2度としないために今回このアプリを作りました。 GitHub 完成形のコードは以下となります。 では実際に作成していきましょう! sam initを実行する ゼロから書いていってもいいのですが、初めての方はまずはsam initを使いましょう。 以下のように選択していってください。 ターミナル $ sam init Which template source would you like to use? 1 - AWS Quick Start Templates 2 - Custom Template Location Choice: 1 What package type would you like to use? 1 - Zip (artifact is a zip uploaded to S3) 2 - Image (artifact is an image uploaded to an ECR image repository) Package type: 1 Which runtime would you like to use? 1 - nodejs14.x 2 - python3.8 3 - ruby2.7 4 - go1.x 5 - java11 6 - dotnetcore3.1 7 - nodejs12.x 8 - nodejs10.x 9 - python3.7 10 - python3.6 11 - python2.7 12 - ruby2.5 13 - java8.al2 14 - java8 15 - dotnetcore2.1 Runtime: 7 Project name [sam-app]: WeatherFashion AWS quick start application templates: 1 - Hello World Example 2 - Step Functions Sample App (Stock Trader) 3 - Quick Start: From Scratch 4 - Quick Start: Scheduled Events 5 - Quick Start: S3 6 - Quick Start: SNS 7 - Quick Start: SQS 8 - Quick Start: App Backend using TypeScript 9 - Quick Start: Web Backend Template selection: 1 ここまでできれば作成されます。 このような構成になっていればOKです。 .WeatherFashion ├── events/ │ ├── event.json ├── hello-world/ │ ├── tests │ │ └── integration │ │ │ └── test-api-gateway.js │ │ └── unit │ │ │ └── test-handler.js │ ├── .npmignore │ ├── app.js │ ├── package.json ├── .gitignore ├── README.md ├── template.yaml 必要ないファイルなどがあるのでそれを削除していきましょう。 .WeatherFashion ├── hello-world/ │ ├── app.js ├── .gitignore ├── README.md ├── template.yaml また、ディレクトリ名やファイル名を変えましょう。 .WeatherFashion ├── api/ │ ├── index.js ├── .gitignore ├── README.md ├── template.yaml 次は、template.yamlを修正して、SAMの実行をしてみたいところですが、一旦後回しにします。 先にTypeScriptなどのパッケージを入れ、ディレクトリ構造を明確にした後の方が理解しやすいので。。 ということでパッケージを入れていきましょう。 package.jsonの作成 以下のコマンドを入力してください。 これで、package.jsonの作成が完了します。 ターミナル $ npm init -y 必要なパッケージのインストール dependencies dependenciesはすべてのステージで使用するパッケージです。 今回使用するパッケージは以下の4つです。 ・@line/bot-sdk ・aws-sdk ・axios 以下のコマンドを入力してください。 これで全てのパッケージがインストールされます。 ターミナル $ npm install @line/bot-sdk aws-sdk axios --save ちなみに、Lambdaでは元よりaws-sdkが使えるようなのでなくても問題ないです。 インストールしなければその分容量が軽くなるので、レスポンスは早くなります。 devDependencies devDependenciesはコーディングステージのみで使用するパッケージです。 今回使用するパッケージは以下の5つです。 ・typescript ・@types/node ・ts-node ・rimraf ・npm-run-all 以下のコマンドを入力してください。 これで全てのパッケージがインストールされます。 ターミナル $ npm install -D typescript @types/node ts-node rimraf npm-run-all package.jsonにコマンドの設定を行う npm run buildでコンパイルを行います。 package.json { "scripts": { "clean": "rimraf dist", "tsc": "tsc", "build": "npm-run-all clean tsc" }, } tsconfig.jsonの作成 以下のコマンドを実行しTypeScriptの初期設定を行います。 ターミナル $ npx tsc --init それでは、作成されたtsconfig.jsonの上書きをしていきます。 tsconfig.json { "compilerOptions": { "target": "ES2018", "module": "commonjs", "sourceMap": true, "outDir": "./api/dist", "strict": true, "moduleResolution": "node", "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, "include": ["api/src/*"] } 簡単にまとめると、 api/srcディレクトリ以下を対象として、それらをapi/distディレクトリにES2018の書き方でビルドされるという設定です。 tsconfig.jsonに関して詳しく知りたい方は以下のサイトをどうぞ。 また、この辺りで必要ないディレクトリはGithubにpushしたくないので、.gitignoreも作成しておきましょう。 .gitignore node_modules package-lock.json .aws-sam samconfig.toml dist 最終的にはこのようなディレクトリ構成にしましょう。 .WeatherFashion ├── api/ │ ├── dist(コンパイル後) │ │ └── node_modules(コピーする) │ │ └── package.json(コピーする) │ ├── src(コンパイル前) │ │ └── index.ts ├── node_modules(コピー元) ├── .gitignore ├── package.json(コピー元) ├── package-lock.json ├── README.md ├── template.yaml ├── tsconfig.json やるべきことは以下の2つです。 ①distディレクトリを作成する ②distディレクトリに、node_modules, package.jsonをコピーする 次に、template.yamlを書いていきましょう。 SAM Templateを記載する ファイル内にコメントを残しています。 これで大まかには理解できるかと思います。 詳しくは公式サイトを見てください。 template.yaml # AWS CloudFormationテンプレートのバージョン AWSTemplateFormatVersion: '2010-09-09' # CloudFormationではなくSAMを使うと明記する Transform: AWS::Serverless-2016-10-31 # CloudFormationのスタックの説明文(重要ではないので適当でOK) Description: > WeatherFashion Globals: # Lambda関数のタイムアウト値(3秒に設定) Function: Timeout: 3 Resources: # API Gateway WeatherFashionAPI: # Typeを指定する(今回はAPI Gateway) Type: AWS::Serverless::Api Properties: # ステージ名(APIのURLの最後にこのステージ名が付与されます) StageName: v1 # CORSの設定 Cors: AllowMethods: "'POST, GET, OPTIONS'" AllowHeaders: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'" AllowOrigin: "'*'" MaxAge: "'600'" # Lambda WeatherFashionFunction: # Typeを指定する(今回はLambda) Type: AWS::Serverless::Function Properties: # 関数が格納されているディレクトリ(今回はコンパイル後なので、distディレクトリを使用する) CodeUri: api/dist # ファイル名と関数名(今回はファイル名がindex.js、関数名がexports.handlerなので、index.handlerとなります) Handler: index.handler # どの言語とどのバージョンを使用するか Runtime: nodejs12.x # ポリシーを付与する(今回はLambdaの権限とSSMの読み取り権限を付与) Policies: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AmazonSSMReadOnlyAccess # この関数をトリガーするイベントを指定します Events: # API Gateway WeatherFashionAPI: Type: Api Properties: # どのAPIを使用するか(!Refは値の参照に使用します) RestApiId: !Ref WeatherFashionAPI # URL Path: / # POSTメソッド Method: post Outputs: WeatherFashionAPI: Description: 'API Gateway' # URLを作成(!Subは${}で値を指定することができます) Value: !Sub 'https://${WeatherFashionAPI}.execute-api.${AWS::Region}.amazonaws.com/v1' WeatherFashionFunction: Description: 'Lambda' # ロールの値を返す Value: !GetAtt WeatherFashionFunction.Arn WeatherFashionFunctionIamRole: Description: 'IAM Role' # ロールの値を返す Value: !GetAtt WeatherFashionFunctionRole.Arn LINE Developersにアカウントを作成する LINE Developersにアクセスして、「ログイン」ボタンをクリックしてください。 その後諸々入力してもらったら以下のように作成できるかと思います。 注意事項としては、今回Messaging APIとなるので、チャネルの種類を間違えた方は修正してください。 チャネルシークレットとチャネルアクセストークンが必要になるのでこの2つを発行します。 OpenWeatherのAPIを取得する 以下にアクセスしてください。 アカウントを作成し、APIキーを発行してください。 これで必要な環境変数は取得できました。 それでは、これをSSMを使ってLambda内で使えるようにしていきましょう。 SSMパラメータストアで環境変数を設定 なぜSSMパラメータストアを使うのか? SAMのLambda設定にも、環境変数の項目はあります。 しかし、2点問題点があります。 ①Lambdaの環境変数の変更をしたいとき、Lambdaのバージョンも新規発行をしなければならない ②Lambdaのバージョンとエイリアスを紐付けて管理をするとき、もし環境変数にリリース先環境別の値をセットしていると、リリース時に手動で環境変数の変更をしなければならないケースが発生する 簡単にまとめると、「リアルタイムで反映できないし、人為的なミスのリスクもあるよ」ということです。 SSMパラメータストアで値を管理すると以下の3点のメリットがあります。 ①Lambdaの環境変数の管理が不要 ②Lambdaも含めた値関連情報を一元管理できる ③Lambda外部からリアルタイムに環境変数を変更制御できる ということで、SSMパラメータストアを使用しましょう。 みんな大好きクラスメソッドの記事にやり方が書いてあります。 こちらの記事が完璧なのでこちらを見てやってみてください。 私は以下のように命名して作成しました。 SSMパラメータが取得できているかconsole.logで検証 api/src/index.ts // import import aws from 'aws-sdk'; // SSM const ssm = new aws.SSM(); exports.handler = async (event: any, context: any) => { const LINE_WEATHER_FASHION_CHANNEL_ACCESS_TOKEN = { Name: 'LINE_WEATHER_FASHION_CHANNEL_ACCESS_TOKEN', WithDecryption: false, }; const CHANNEL_ACCESS_TOKEN: any = await ssm .getParameter(LINE_WEATHER_FASHION_CHANNEL_ACCESS_TOKEN) .promise(); const channelAccessToken: string = CHANNEL_ACCESS_TOKEN.Parameter.Value; console.log('channelAccessToken: ' + channelAccessToken); }; これをコンパイルしてデプロイしていきましょう。 ターミナル // コンパイル $ npm run build // ビルド $ sam build // デプロイ $ sam deploy --guided Configuring SAM deploy ====================== Looking for samconfig.toml : Not found Setting default arguments for 'sam deploy' ========================================= // CloudFormation スタック名の指定 Stack Name [sam-app]: WeatherFashion // リージョンの指定 AWS Region [us-east-1]: ap-northeast-1 // デプロイ前にCloudformationの変更セットを確認するか #Shows you resources changes to be deployed and require a 'Y' to initiate deploy Confirm changes before deploy [y/N]: y // SAM CLI に IAM ロールの作成を許可するか(CAPABILITY_IAM) #SAM needs permission to be able to create roles to connect to the resources in your template Allow SAM CLI IAM role creation [Y/n]: y // API イベントタイプの関数に認証が含まれていない場合、警告される HelloWorldFunction may not have authorization defined, Is this okay? [y/N]: y // この設定を samconfig.toml として保存するか Save arguments to samconfig.toml [Y/n]: y これでデプロイが完了します。 では、API GatewayのURLを確認しましょう。 Webhook URLの登録 先ほどAPI Gatewayで作成したhttpsのURLをコピーしてください。 これをLINE DevelopersのWebhookに設定します。 それではSSMパラメータが正しく取得できているか確認しましょう。 CloudWatchで確認しましょう! 取得できていますね! ここからの流れはこのような感じです。 ①「今日の洋服は?」というメッセージを受け取る ②「今日の洋服は?」を受け取ったら、位置情報メッセージを送る ③「今日の洋服は?」以外を受け取ったら、「そのメッセージには対応していません」と送る ④「位置情報メッセージ」を受け取る ⑤「位置情報メッセージ」を受け取ったら、緯度と経度を使って天気予報を取得する ⑥「位置情報メッセージ」を受け取ったら、天気予報メッセージを送る では作っていきましょう! またこれら全てのコードをapi/src/index.tsに書くとコードが肥大化し可読性が落ちます。 なのでCommonディレクトリに関数に切り分けて作成していきます。 またここからはLINEBotのオリジナルの型が頻出します。 1つずつ説明するのはあまりに時間がかかるので、知らない型が出てきたらその度に以下のサイトで検索するようにしてください。 ①「今日の洋服は?」というメッセージを受け取る api/src/index.ts // パッケージのインストール import { ClientConfig, Client, WebhookEvent } from '@line/bot-sdk'; import aws from 'aws-sdk'; // SSMパラメータストア const ssm = new aws.SSM(); const LINE_WEATHER_FASHION_CHANNEL_ACCESS_TOKEN = { Name: 'LINE_WEATHER_FASHION_CHANNEL_ACCESS_TOKEN', WithDecryption: false, }; const LINE_WEATHER_FASHION_CHANNEL_SECRET = { Name: 'LINE_WEATHER_FASHION_CHANNEL_SECRET', WithDecryption: false, }; const LINE_WEATHER_FASHION_WEATHER_API = { Name: 'LINE_WEATHER_FASHION_WEATHER_API', WithDecryption: false, }; exports.handler = async (event: any, context: any) => { // SSMパラメータストアで値を取得する const CHANNEL_ACCESS_TOKEN: any = await ssm .getParameter(LINE_WEATHER_FASHION_CHANNEL_ACCESS_TOKEN) .promise(); const CHANNEL_SECRET: any = await ssm.getParameter(LINE_WEATHER_FASHION_CHANNEL_SECRET).promise(); const WEATHER_API: any = await ssm.getParameter(LINE_WEATHER_FASHION_WEATHER_API).promise(); const channelAccessToken: string = CHANNEL_ACCESS_TOKEN.Parameter.Value; const channelSecret: string = CHANNEL_SECRET.Parameter.Value; const weatherApi: string = WEATHER_API.Parameter.Value; // SSMパラメータストアを使ってclientを作成 const clientConfig: ClientConfig = { channelAccessToken: channelAccessToken, channelSecret: channelSecret, }; const client: Client = new Client(clientConfig); // post const body: any = JSON.parse(event.body); const response: WebhookEvent = body.events[0]; // action try { await actionButtonOrErrorMessage(response, client); } catch (err) { console.log(err); } }; // ボタンメッセージもしくはエラーメッセージを送る関数 const actionButtonOrErrorMessage = async (event: WebhookEvent, client: Client) => { try { if (event.type !== 'message' || event.message.type !== 'text') { return; } const { replyToken } = event; const { text } = event.message; if (text === '今日の洋服は?') { // ボタンメッセージを送る } else { // エラーメッセージを送る } } catch (err) { console.log(err); } }; ②「今日の洋服は?」を受け取ったら、位置情報メッセージを送る api/src/Common/ButtonMessage/ButtonMessageTemplate.ts // パッケージを読み込む import { TemplateMessage } from '@line/bot-sdk'; export const buttonMessageTemplate = (): Promise<TemplateMessage> => { return new Promise((resolve, reject) => { const params: TemplateMessage = { type: 'template', altText: 'This is a buttons template', template: { type: 'buttons', text: '今日はどんな洋服にしようかな', actions: [ { type: 'uri', label: '現在地を送る', uri: 'https://line.me/R/nv/location/', }, ], }, }; resolve(params); }); }; api/src/index.ts // パッケージのインストール import { ClientConfig, Client, WebhookEvent } from '@line/bot-sdk'; import aws from 'aws-sdk'; // モジュールを読み込む import { buttonMessageTemplate } from './Common/ButtonMessage/ButtonMessageTemplate'; // SSMパラメータストア const ssm = new aws.SSM(); const LINE_WEATHER_FASHION_CHANNEL_ACCESS_TOKEN = { Name: 'LINE_WEATHER_FASHION_CHANNEL_ACCESS_TOKEN', WithDecryption: false, }; const LINE_WEATHER_FASHION_CHANNEL_SECRET = { Name: 'LINE_WEATHER_FASHION_CHANNEL_SECRET', WithDecryption: false, }; const LINE_WEATHER_FASHION_WEATHER_API = { Name: 'LINE_WEATHER_FASHION_WEATHER_API', WithDecryption: false, }; exports.handler = async (event: any, context: any) => { // SSMパラメータストアで値を取得する const CHANNEL_ACCESS_TOKEN: any = await ssm .getParameter(LINE_WEATHER_FASHION_CHANNEL_ACCESS_TOKEN) .promise(); const CHANNEL_SECRET: any = await ssm.getParameter(LINE_WEATHER_FASHION_CHANNEL_SECRET).promise(); const WEATHER_API: any = await ssm.getParameter(LINE_WEATHER_FASHION_WEATHER_API).promise(); const channelAccessToken: string = CHANNEL_ACCESS_TOKEN.Parameter.Value; const channelSecret: string = CHANNEL_SECRET.Parameter.Value; const weatherApi: string = WEATHER_API.Parameter.Value; // SSMパラメータストアを使ってclientを作成 const clientConfig: ClientConfig = { channelAccessToken: channelAccessToken, channelSecret: channelSecret, }; const client: Client = new Client(clientConfig); // post const body: any = JSON.parse(event.body); const response: WebhookEvent = body.events[0]; // action try { await actionButtonOrErrorMessage(response, client); } catch (err) { console.log(err); } }; // ボタンメッセージもしくはエラーメッセージを送る関数 const actionButtonOrErrorMessage = async (event: WebhookEvent, client: Client) => { try { if (event.type !== 'message' || event.message.type !== 'text') { return; } const { replyToken } = event; const { text } = event.message; if (text === '今日の洋服は?') { const buttonMessage = await buttonMessageTemplate(); await client.replyMessage(replyToken, buttonMessage); } else { // エラーメッセージを送る } } catch (err) { console.log(err); } }; ボタンメッセージのJSON作成に関しては公式サイトを参考にしましょう。 ③「今日の洋服は?」以外を受け取ったら、「そのメッセージには対応していません」と送る api/src/Common/ButtonMessage/ErrorMessageTemplate.ts // パッケージを読み込む import { TextMessage } from '@line/bot-sdk'; export const errorMessageTemplate = (): Promise<TextMessage> => { return new Promise((resolve, reject) => { const params: TextMessage = { type: 'text', text: 'ごめんなさい、このメッセージは対応していません。', }; resolve(params); }); }; api/src/index.ts // パッケージのインストール import { ClientConfig, Client, WebhookEvent } from '@line/bot-sdk'; import aws from 'aws-sdk'; // モジュールを読み込む import { buttonMessageTemplate } from './Common/ButtonMessage/ButtonMessageTemplate'; import { errorMessageTemplate } from './Common/ButtonMessage/ErrorMessageTemplate'; // SSMパラメータストア const ssm = new aws.SSM(); const LINE_WEATHER_FASHION_CHANNEL_ACCESS_TOKEN = { Name: 'LINE_WEATHER_FASHION_CHANNEL_ACCESS_TOKEN', WithDecryption: false, }; const LINE_WEATHER_FASHION_CHANNEL_SECRET = { Name: 'LINE_WEATHER_FASHION_CHANNEL_SECRET', WithDecryption: false, }; const LINE_WEATHER_FASHION_WEATHER_API = { Name: 'LINE_WEATHER_FASHION_WEATHER_API', WithDecryption: false, }; exports.handler = async (event: any, context: any) => { // SSMパラメータストアで値を取得する const CHANNEL_ACCESS_TOKEN: any = await ssm .getParameter(LINE_WEATHER_FASHION_CHANNEL_ACCESS_TOKEN) .promise(); const CHANNEL_SECRET: any = await ssm.getParameter(LINE_WEATHER_FASHION_CHANNEL_SECRET).promise(); const WEATHER_API: any = await ssm.getParameter(LINE_WEATHER_FASHION_WEATHER_API).promise(); const channelAccessToken: string = CHANNEL_ACCESS_TOKEN.Parameter.Value; const channelSecret: string = CHANNEL_SECRET.Parameter.Value; const weatherApi: string = WEATHER_API.Parameter.Value; // SSMパラメータストアを使ってclientを作成 const clientConfig: ClientConfig = { channelAccessToken: channelAccessToken, channelSecret: channelSecret, }; const client: Client = new Client(clientConfig); // post const body: any = JSON.parse(event.body); const response: WebhookEvent = body.events[0]; // action try { await actionButtonOrErrorMessage(response, client); } catch (err) { console.log(err); } }; // ボタンメッセージもしくはエラーメッセージを送る関数 const actionButtonOrErrorMessage = async (event: WebhookEvent, client: Client) => { try { if (event.type !== 'message' || event.message.type !== 'text') { return; } const { replyToken } = event; const { text } = event.message; if (text === '今日の洋服は?') { const buttonMessage = await buttonMessageTemplate(); await client.replyMessage(replyToken, buttonMessage); } else { const errorMessage = await errorMessageTemplate(); await client.replyMessage(replyToken, errorMessage); } } catch (err) { console.log(err); } }; テキストメッセージのJSON作成に関しては公式サイトを参考にしましょう。 ④「位置情報メッセージ」を受け取る api/src/index.ts // パッケージのインストール import { ClientConfig, Client, WebhookEvent } from '@line/bot-sdk'; import aws from 'aws-sdk'; // モジュールを読み込む import { buttonMessageTemplate } from './Common/ButtonMessage/ButtonMessageTemplate'; import { errorMessageTemplate } from './Common/ButtonMessage/ErrorMessageTemplate'; // SSMパラメータストア const ssm = new aws.SSM(); const LINE_WEATHER_FASHION_CHANNEL_ACCESS_TOKEN = { Name: 'LINE_WEATHER_FASHION_CHANNEL_ACCESS_TOKEN', WithDecryption: false, }; const LINE_WEATHER_FASHION_CHANNEL_SECRET = { Name: 'LINE_WEATHER_FASHION_CHANNEL_SECRET', WithDecryption: false, }; const LINE_WEATHER_FASHION_WEATHER_API = { Name: 'LINE_WEATHER_FASHION_WEATHER_API', WithDecryption: false, }; exports.handler = async (event: any, context: any) => { // SSMパラメータストアで値を取得する const CHANNEL_ACCESS_TOKEN: any = await ssm .getParameter(LINE_WEATHER_FASHION_CHANNEL_ACCESS_TOKEN) .promise(); const CHANNEL_SECRET: any = await ssm.getParameter(LINE_WEATHER_FASHION_CHANNEL_SECRET).promise(); const WEATHER_API: any = await ssm.getParameter(LINE_WEATHER_FASHION_WEATHER_API).promise(); const channelAccessToken: string = CHANNEL_ACCESS_TOKEN.Parameter.Value; const channelSecret: string = CHANNEL_SECRET.Parameter.Value; const weatherApi: string = WEATHER_API.Parameter.Value; // SSMパラメータストアを使ってclientを作成 const clientConfig: ClientConfig = { channelAccessToken: channelAccessToken, channelSecret: channelSecret, }; const client: Client = new Client(clientConfig); // post const body: any = JSON.parse(event.body); const response: WebhookEvent = body.events[0]; // action try { await actionButtonOrErrorMessage(response, client); await actionFlexMessage(response, client, weatherApi); } catch (err) { console.log(err); } }; // ボタンメッセージもしくはエラーメッセージを送る関数 const actionButtonOrErrorMessage = async (event: WebhookEvent, client: Client) => { try { if (event.type !== 'message' || event.message.type !== 'text') { return; } const { replyToken } = event; const { text } = event.message; if (text === '今日の洋服は?') { const buttonMessage = await buttonMessageTemplate(); await client.replyMessage(replyToken, buttonMessage); } else { const errorMessage = await errorMessageTemplate(); await client.replyMessage(replyToken, errorMessage); } } catch (err) { console.log(err); } }; // 天気予報とファッションレコメンドメッセージを送る関数 const actionFlexMessage = async (event: WebhookEvent, client: Client, weatherApi: string) => { try { if (event.type !== 'message' || event.message.type !== 'location') { return; } // ファッションレコメンドメッセージを送る } catch (err) { console.log(err); } }; ⑤「位置情報メッセージ」を受け取ったら、緯度と経度を使って天気予報を取得する Flex Messageの作成方法に関してファイル名も出しながら説明します。 【ファイル名】GetWeatherForecast.ts 天気予報を取得します。 まずはOpenWeatherで天気予報を取得するために必要な情報が3つあります。 ①API ②経度 ③緯度 それではこの3つを取得していきましょう。 ①API APIはSSMパラメータストアで取得しています。 ②経度、③緯度 これら2つは、eventから取得できます。 ということで作っていきましょう。 api/src/Common/WeatherForecastMessage/GetWeatherForecast.ts // Load the package import { WebhookEvent } from '@line/bot-sdk'; import axios, { AxiosResponse } from 'axios'; export const getWeatherForecastData = async ( event: WebhookEvent, weatherApi: string ): Promise<any> => { return new Promise(async (resolve, reject) => { 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; // OpenWeatherURL const openWeatherURL: string = `https://api.openweathermap.org/data/2.5/onecall?lat=${latitude}&lon=${longitude}&units=metric&lang=ja&appid=${weatherApi}`; const weatherData: AxiosResponse<any> = await axios.get(openWeatherURL); resolve(weatherData); } catch (err) { reject(err); } }); }; 【ファイル名】FormatWeatherForecast.ts 取得した天気予報のデータの整形を行う。 こちらでは、const weatherとconst weatherArrayの2つで型定義ファイルを作成する必要があります。 ということで作成しましょう。 api/src/Common/WeatherForecastMessage/types/FormatWeatherForecast.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; }; export type WeatherArrayType = { today: string; imageURL: string; weatherForecast: string; mornTemperature: number; dayTemperature: number; eveTemperature: number; nightTemperature: number; fashionAdvice: string; }; 作成した型定義を使ってファイルを完成させます。 api/src/Common/WeatherForecastMessage/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, WeatherArrayType } from './types/FormatWeatherForecast.type'; export const formatWeatherForecastData = async ( event: WebhookEvent, weatherApi: string ): Promise<WeatherArrayType> => { return new Promise(async (resolve, reject) => { // Get the getWeatherForecastData const weathers: AxiosResponse<any> = await getWeatherForecastData(event, weatherApi); // 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, }; resolve(weatherArray); }); }; 【ファイル名】FlexMessageTemplate 整形したデータを取得して Flex Messageのテンプレートを作成する。 api/src/Common/WeatherForecastMessage/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, weatherApi: string ): Promise<FlexMessage> => { return new Promise(async (resolve, reject) => { const data = await formatWeatherForecastData(event, weatherApi); resolve({ 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/index.ts // パッケージのインストール import { ClientConfig, Client, WebhookEvent } from '@line/bot-sdk'; import aws from 'aws-sdk'; // モジュールを読み込む import { buttonMessageTemplate } from './Common/ButtonMessage/ButtonMessageTemplate'; import { errorMessageTemplate } from './Common/ButtonMessage/ErrorMessageTemplate'; import { flexMessageTemplate } from './Common/WeatherForecastMessage/FlexMessageTemplate'; // SSMパラメータストア const ssm = new aws.SSM(); const LINE_WEATHER_FASHION_CHANNEL_ACCESS_TOKEN = { Name: 'LINE_WEATHER_FASHION_CHANNEL_ACCESS_TOKEN', WithDecryption: false, }; const LINE_WEATHER_FASHION_CHANNEL_SECRET = { Name: 'LINE_WEATHER_FASHION_CHANNEL_SECRET', WithDecryption: false, }; const LINE_WEATHER_FASHION_WEATHER_API = { Name: 'LINE_WEATHER_FASHION_WEATHER_API', WithDecryption: false, }; exports.handler = async (event: any, context: any) => { // SSMパラメータストアで値を取得する const CHANNEL_ACCESS_TOKEN: any = await ssm .getParameter(LINE_WEATHER_FASHION_CHANNEL_ACCESS_TOKEN) .promise(); const CHANNEL_SECRET: any = await ssm.getParameter(LINE_WEATHER_FASHION_CHANNEL_SECRET).promise(); const WEATHER_API: any = await ssm.getParameter(LINE_WEATHER_FASHION_WEATHER_API).promise(); const channelAccessToken: string = CHANNEL_ACCESS_TOKEN.Parameter.Value; const channelSecret: string = CHANNEL_SECRET.Parameter.Value; const weatherApi: string = WEATHER_API.Parameter.Value; // SSMパラメータストアを使ってclientを作成 const clientConfig: ClientConfig = { channelAccessToken: channelAccessToken, channelSecret: channelSecret, }; const client: Client = new Client(clientConfig); // post const body: any = JSON.parse(event.body); const response: WebhookEvent = body.events[0]; // action try { await actionButtonOrErrorMessage(response, client); await actionFlexMessage(response, client, weatherApi); } catch (err) { console.log(err); } }; // ボタンメッセージもしくはエラーメッセージを送る関数 const actionButtonOrErrorMessage = async (event: WebhookEvent, client: Client) => { try { if (event.type !== 'message' || event.message.type !== 'text') { return; } const { replyToken } = event; const { text } = event.message; if (text === '今日の洋服は?') { const buttonMessage = await buttonMessageTemplate(); await client.replyMessage(replyToken, buttonMessage); } else { const errorMessage = await errorMessageTemplate(); await client.replyMessage(replyToken, errorMessage); } } catch (err) { console.log(err); } }; // 天気予報とファッションレコメンドメッセージを送る関数 const actionFlexMessage = async (event: WebhookEvent, client: Client, weatherApi: string) => { try { if (event.type !== 'message' || event.message.type !== 'location') { return; } const { replyToken } = event; const message = await flexMessageTemplate(event, weatherApi); await client.replyMessage(replyToken, message); } catch (err) { console.log(err); } }; これで完成です! では、デプロイしていきましょう。 デプロイ まずは、npm run buildでコンパイルしましょう。 ターミナル $ npm run build コンパイルされた後は、ビルドしてデプロイしていきましょう。 ターミナル // ビルド $ sam build // デプロイ $ sam deploy --guided 最後に 以前すべて手作業で行いましたが、SAMを使うと効率的にデプロイが行えます。 SAMテンプレートの書き方を学ぶコストは発生しますが、1度作ればそれをそのまま使えるので汎用性も高いのでおすすめです。 サーバーレスアーキテクチャを勉強する方がいましたらぜひSAMも勉強してみてください!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Serverless Framework でじゃんけんのLINEbotを作る(リファクタリング編)

概要 一つ前に投稿した記事のリファクタリング編です。 じゃんけんの勝ち負けを判定するロジックが美しくなかったので、数学的に処理しました。 アジェンダ 問題の実装 数学的処理 リファクタリング 1. 問題の実装 janken.jsの抜粋 /** * じゃんけんの勝ち負けを判定します。 * @param {string} userHand - ユーザの出した手 * @param {string} cpHand - コンピュータの出した手 * @return {string} - message 判定結果 */ //TODO ロジックが美しくない。数学的に処理する方法を後日の記事にする。 function hantei(userHand, cpHand){ let message = '' if(userHand == 'グー'){ if(cpHand == 'グー'){ message = 'あいこです。(あなた:'+userHand + '、CP:'+ cpHand +')' }else if(cpHand == 'チョキ'){ message = 'あなたの勝ちです。(あなた:'+userHand + '、CP:'+ cpHand +')' }else{ message = 'あなたの負けです。(あなた:'+userHand + '、CP:'+ cpHand +')' } }else if(userHand == 'チョキ'){ if(cpHand == 'チョキ'){ message = 'あいこです。(あなた:'+userHand + '、CP:'+ cpHand +')' }else if(cpHand == 'パー'){ message = 'あなたの勝ちです。(あなた:'+userHand + '、CP:'+ cpHand +')' }else{ message = 'あなたの負けです。(あなた:'+userHand + '、CP:'+ cpHand +')' } }else{ if(cpHand == 'パー'){ message = 'あいこです。(あなた:'+userHand + '、CP:'+ cpHand +')' }else if(cpHand == 'グー'){ message = 'あなたの勝ちです。(あなた:'+userHand + '、CP:'+ cpHand +')' }else{ message = 'あなたの負けです。(あなた:'+userHand + '、CP:'+ cpHand +')' } } return message } うーむ、いかにも美しくないですよね。if文の中にif文があって、とても気持ち悪い。 なので、数学的に処理してリファクタリングしましょう!!! 2. 数学的処理 では数学的に処理しましょう。グーやパーなどのテキストデータだと処理しにくいので、数値に変換します。数値に変換した後は法則を見つけるだけです!!詳細は下記の画像の通りです。 はい、画像の通り、判定方法は((自分-相手)+3)%3の結果を見れば判定できることがわかりましたね。では実装していきましょう。 3.リファクタリング janken.js 'use strict'; // パッケージのインストール const line = require('@line/bot-sdk'); // LINEアクセストークンの設定 const config = { channelAccessToken: 'アクセストークン', channelSecret: 'シークレットトークン', }; // インスタンス化 const client = new line.Client(config); exports.webhook = async (event, context) => { /* リクエストボディ*/ const body = JSON.parse(event.body) /* Lineのevent情報*/ const response = body.events[0] /* ユーザの出した手を取得*/ const userHand = response.message.text /* コンピュータの出した手を取得 */ const cpHand = getCpHand() let message = hantei(userHand, cpHand) /* メッセージ送信のために必要な情報 */ const replyToken = response.replyToken console.log('replyToken:' + replyToken) const post = { type: 'text', text: message, } try { await client.replyMessage(replyToken, post) } catch (err) { console.log(err) } }; /** * コンピュータの手を決定します。 * @return {string} cpHand - コンピュータの手 */ function getCpHand(){ /**ランダム数値を取得(範囲:0~2) */ let num = Math.floor(Math.random() * 3) let cpHand = '' if(num ==0){ cpHand = 'グー' }else if(num ==1){ cpHand = 'チョキ' }else{ cpHand = 'パー' } return cpHand } /** * じゃんけんの勝ち負けを判定します。 * @param {string} userHand - ユーザの出した手 * @param {string} cpHand - コンピュータの出した手 * @return {string} - message 判定結果 */ function hantei(userHand, cpHand){ let message = '' /**ユーザの手を数値化する。(グー:0、チョキ:1、パー:2) */ const userHandNum = transformHand(userHand) /**コンピュータの手を数値化する。(グー:0、チョキ:1、パー:2) */ const cpHandNum = transformHand(cpHand) /**判定値を算出((自分の手 - 相手の手)+3)%3 */ const hanteiNum = ((userHandNum - cpHandNum)+3)%3 if(hanteiNum == 0){ message = 'あいこです。(あなた:'+userHand + '、CP:'+ cpHand +')' }else if(hanteiNum == 1){ message = 'あなたの負けです。(あなた:'+userHand + '、CP:'+ cpHand +')' }else{ message = 'あなたの勝ちです。(あなた:'+userHand + '、CP:'+ cpHand +')' } return message } /** * じゃんけんの手を数値化します。 * @param {string} userHand - ユーザの出した手 * @return {num} - userHandNum ユーザの手の数値 */ function transformHand(userHand){ let userHandNum if(userHand == 'グー'){ userHandNum = 0 } else if(userHand == 'チョキ'){ userHandNum = 1 }else{ userHandNum = 2 } return userHandNum }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Serverless Framework でじゃんけんのLinbot を作る(リファクタリング編)

概要 一つ前に投稿した記事のリファクタリング編です。 じゃんけんの勝ち負けを判定するロジックが美しくなかったので、数学的に処理しました。 アジェンダ 問題の実装 数学的処理 リファクタリング 1. 問題の実装 janken.jsの抜粋 /** * じゃんけんの勝ち負けを判定します。 * @param {string} userHand - ユーザの出した手 * @param {string} cpHand - コンピュータの出した手 * @return {string} - message 判定結果 */ //TODO ロジックが美しくない。数学的に処理する方法を後日の記事にする。 function hantei(userHand, cpHand){ let message = '' if(userHand == 'グー'){ if(cpHand == 'グー'){ message = 'あいこです。(あなた:'+userHand + '、CP:'+ cpHand +')' }else if(cpHand == 'チョキ'){ message = 'あなたの勝ちです。(あなた:'+userHand + '、CP:'+ cpHand +')' }else{ message = 'あなたの負けです。(あなた:'+userHand + '、CP:'+ cpHand +')' } }else if(userHand == 'チョキ'){ if(cpHand == 'チョキ'){ message = 'あいこです。(あなた:'+userHand + '、CP:'+ cpHand +')' }else if(cpHand == 'パー'){ message = 'あなたの勝ちです。(あなた:'+userHand + '、CP:'+ cpHand +')' }else{ message = 'あなたの負けです。(あなた:'+userHand + '、CP:'+ cpHand +')' } }else{ if(cpHand == 'パー'){ message = 'あいこです。(あなた:'+userHand + '、CP:'+ cpHand +')' }else if(cpHand == 'グー'){ message = 'あなたの勝ちです。(あなた:'+userHand + '、CP:'+ cpHand +')' }else{ message = 'あなたの負けです。(あなた:'+userHand + '、CP:'+ cpHand +')' } } return message } うーむ、いかにも美しくないですよね。if文の中にif文があって、とても気持ち悪い。 なので、数学的に処理してリファクタリングしましょう!!! 2. 数学的処理 では数学的に処理しましょう。グーやパーなどのテキストデータだと処理しにくいので、数値に変換します。数値に変換した後は法則を見つけるだけです!!詳細は下記の画像の通りです。 はい、画像の通り、判定方法は((自分-相手)+3)%3の結果を見れば判定できることがわかりましたね。では実装していきましょう。 3.リファクタリング janken.js 'use strict'; // パッケージのインストール const line = require('@line/bot-sdk'); // LINEアクセストークンの設定 const config = { channelAccessToken: 'アクセストークン', channelSecret: 'シークレットトークン', }; // インスタンス化 const client = new line.Client(config); exports.webhook = async (event, context) => { /* リクエストボディ*/ const body = JSON.parse(event.body) /* Lineのevent情報*/ const response = body.events[0] /* ユーザの出した手を取得*/ const userHand = response.message.text /* コンピュータの出した手を取得 */ const cpHand = getCpHand() let message = hantei(userHand, cpHand) /* メッセージ送信のために必要な情報 */ const replyToken = response.replyToken console.log('replyToken:' + replyToken) const post = { type: 'text', text: message, } try { await client.replyMessage(replyToken, post) } catch (err) { console.log(err) } }; /** * コンピュータの手を決定します。 * @return {string} cpHand - コンピュータの手 */ function getCpHand(){ /**ランダム数値を取得(範囲:0~2) */ let num = Math.floor(Math.random() * 3) let cpHand = '' if(num ==0){ cpHand = 'グー' }else if(num ==1){ cpHand = 'チョキ' }else{ cpHand = 'パー' } return cpHand } /** * じゃんけんの勝ち負けを判定します。 * @param {string} userHand - ユーザの出した手 * @param {string} cpHand - コンピュータの出した手 * @return {string} - message 判定結果 */ function hantei(userHand, cpHand){ let message = '' /**ユーザの手を数値化する。(グー:0、チョキ:1、パー:2) */ const userHandNum = transformHand(userHand) /**コンピュータの手を数値化する。(グー:0、チョキ:1、パー:2) */ const cpHandNum = transformHand(cpHand) /**判定値を算出((自分の手 - 相手の手)+3)%3 */ const hanteiNum = ((userHandNum - cpHandNum)+3)%3 if(hanteiNum == 0){ message = 'あいこです。(あなた:'+userHand + '、CP:'+ cpHand +')' }else if(hanteiNum == 1){ message = 'あなたの負けです。(あなた:'+userHand + '、CP:'+ cpHand +')' }else{ message = 'あなたの勝ちです。(あなた:'+userHand + '、CP:'+ cpHand +')' } return message } /** * じゃんけんの手を数値化します。 * @param {string} userHand - ユーザの出した手 * @return {num} - userHandNum ユーザの手の数値 */ function transformHand(userHand){ let userHandNum if(userHand == 'グー'){ userHandNum = 0 } else if(userHand == 'チョキ'){ userHandNum = 1 }else{ userHandNum = 2 } return userHandNum }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む