ServerlessでLambdaパーミッションを設定する方法
# 概要 LambdaをEventBridgeから呼び出したくなったので、LambdaはServerlessで作成した関数を対象にLambdaPermissionを設定することにした。 ```yaml functions: Foo: handler: src/functions/Foo.handler name: '${self:provider.stage}-Foo' Resources: FooLambdaPermissionEventBridge: Type: AWS::Lambda::Permission Properties: Action: 'lambda:InvokeFunction' FunctionName: !GetAtt FooLambdaFunction.Arn Principal: 'events.amazonaws.com' SourceArn: !Sub 'arn:aws:events:${opt:region, self:provider.region}:${AWS::AccountId}:rule/*' ``` サーバーレスのデフォルト対応できそうになかったので、LambdaPermissionを自前で実装して対応した。
2021.02.16
Cognitoの既存ユーザーの電話番号を削除する方法
# 概要 CognitoUserPoolの指定の既存ユーザーの電話番号を削除し、加えてphone_number_verifiedもfalseにするユースケースの紹介。 # ソースコード `deleteUserPoolPhoneNumber` を実行すると、指定のusernameユーザーの電話番号属性が削除されます。 ```js const setUserMFAPreference = async ( username, twoStepAuthentication, userPoolId ) => new Promise((resolve) => { const params = { Username: username, UserPoolId: userPoolId, MFAOptions: [] }; if (twoStepAuthentication) { params.MFAOptions.push({ AttributeName: 'phone_number', DeliveryMedium: 'SMS' }); } cognitoIdentityServiceProvider.adminSetUserSettings(params, (err, data) => { if (err) { throw err; } else { console.log( 'Set CognitoUserPool Multi-factor authentication :', username ); resolve(data); } }); }); const deleteUserPoolPhoneNumber = async (username, userPoolId) => new Promise((resolve) => { setUserMFAPreference(username, false, userPoolId) .then(() => { const params = { UserAttributeNames: ['phone_number'], // phone_number_verifiedは自動的にfalseに変更されるので指定しない UserPoolId: userPoolId, Username: username }; cognitoIdentityServiceProvider.adminDeleteUserAttributes( params, (err, data) => { if (err) { throw err; } else { resolve(data); } } ); }) .catch((e) => { throw e; }); }); ``` # まとめ Cognitoの認証実装方法を覚えるといろんなプラットフォームでも応用効くので、初学者はCognitoのSDK仕様を一度調べてみると良いだろうと思う。
2021.01.13
CognitoのテストをMock化してみた
# 概要 テスト実行時のCognitoへリクエストを連打するのはあまりよろしくないので、ローカルでの単体テスト時はMock化しました。 # ソースコード aws-sdk-mockなるライブラリあったけど、動作が不安定だったので結局JestでMock化。 promise()関数で戻り値を取得するタイプと、callback関数で戻り値を取得するタイプと2つあるので注意が必要。 ```js import * as AWS from 'aws-sdk'; beforeAll(async () => { jest .spyOn(AWS.CognitoIdentityServiceProvider.services['2016-04-18'].prototype, 'adminCreateUser') .mockImplementation((request) => { return {promise: () => 'ok'} }) jest .spyOn(AWS.CognitoIdentityServiceProvider.services['2016-04-18'].prototype, 'adminAddUserToGroup') .mockImplementation((request) => { return {promise: () => 'ok'} }) jest .spyOn(AWS.CognitoIdentityServiceProvider.services['2016-04-18'].prototype, 'adminSetUserPassword') .mockImplementation((request, callback) => { callback(null, 'ok'); }) jest .spyOn(AWS.CognitoIdentityServiceProvider.services['2016-04-18'].prototype, 'adminSetUserSettings') .mockImplementation((request, callback) => { callback(null, 'ok'); }) }) ``` callbackの場合のMockの作り方に悩んだ・・。 Amplifyの裏側のソースコードを読んだら答えがあったので、これもシェアしておきます。 https://github.com/aws-amplify/amplify-js/pull/398/files/f0136c16fb10e58d5a779b96bd50397257340592 AWSSDKの仕様で詰まったら、GithubでSDKのソースコードと見ると答えがあるので参考に。
2021.01.07
DynamoDBの指定テーブルを物理削除するBatch処理を作成してみた
# 概要 開発中にDynamoDBを物理削除したい時がしばしば。例えば、CloudFormationを使用したデプロイを行っている際に、DynamoDBのデプロイは疎通したけど他のデプロイタスクで転けた場合に、次回のデプロイでDynamoDBのテーブルが既に存在するというエラーの時には物理削除をする必要があるだろうと思う。 # ソースコード ```js // batch/deleteDynamoDBAllTable.js const prefix = process.argv[2]; // ここに任意のテーブル名を追加 const tables = [ `${prefix}-User`, `${prefix}-Article`, `${prefix}-ArticleCategory` ]; Promise.all( tables.map(async (tableName) => { console.log({ TableName: tableName }); return ddb.deleteTable({ TableName: tableName }, function (err, data) { if (err) { return ( 'Unable to delete table. Error JSON:', JSON.stringify(err, null, 2) ); } return ( 'Deleted table. Table description JSON:', JSON.stringify(data, null, 2) ); }); }) ) .then(console.log) .catch(console.error); ``` ```shell $ node batch/deleteDynamoDBAllTable.js dev ``` 既存でデータが存在していてそれを残したい場合は、かならずバックアップの上実行することを推奨します。
2021.01.07
JestでDynamoDBをMock化してみた
# 概要 Lambdaのテストを書いているとき、DynamoDBに障害が発生したケースをMock化したい時がしばしばあります。AWSSDKのMock化方法は色々とありますが、今回はAWSSDKとJestを使用したシンプルなMock作成方法を紹介します。 # ソースコード ```js const lambdaFunction = require("../../src/functions/ExampleFunctions") test('異常系 : DynamoDB通信障害', () => { jest .spyOn(AWS.DynamoDB.DocumentClient.prototype, 'put') .mockImplementationOnce((request) => { throw new Error('DynamoDB Error'); }); const context = {}; const callback = jest.fn(); const event = {} // 正常系のパラメーターを設定 // DynamoDB通信障害時はThrowされることをテスト await expect( lambdaFunction.handler(immediateEvent, context, callback) ).rejects.toThrow(); }) ``` # 応用 上記を応用すると色々な検証を行うことが可能です。 ```js ... test('異常系 : DynamoDB通信障害', () => { let callDynamoPutOperations = [] jest .spyOn(AWS.DynamoDB.DocumentClient.prototype, 'put') .mockImplementationOnce((request) => { // PUTのリクエストパラメーターを格納しておく callDynamoPutOperations.push(request) throw new Error('DynamoDB Error'); }); const context = {}; const callback = jest.fn(); const event = {} await expect( lambdaFunction.handler(immediateEvent, context, callback) ).rejects.toThrow(); // 初回のPUTで失敗した場合は後続のPUTは実行されないことを検証 expect(_.size(callDynamoPutOperations)).toBe(1) }) ``` # まとめ 本記事を応用するとCognitoやその他のSDKの振る舞いをMock化することが可能です。Mock化覚えてAWSLambdaの開発スキルを上げましょう。(私はFargateにデプロイ想定のソースコードに対しも本記事の方法を利用することが多いです) ## 開発サービスの紹介 https://www.ragate.co.jp/service/aws_solutions/
2021.01.07
Cognitoへサインアップ直後のユーザーを、CONFIRMEDへ強制的に移行させる方法
# 概要 サインアップ直後のユーザープールのユーザーは、パスワード変更要求状態となり、初回サインイン時にパスワード変更を要求されます。ただ、管理者でユーザーをサインアップさせる際に、すぐにサインアップ可能状態にしたいケースがしばしばあります。 # adminSetUserPasswordを使用してユーザーのパスワードを一度更新 createUser関数を実行後に、setAdminPasswordを実行すると該当ユーザーはCONFIRMEDとなり、ユーザーはサインイン可能状態となります。 どうぞコピペでご利用ください〜👍 ```js /** * Cognitoへのユーザーの作成 * @param username * @param password * @param phoneNumber * @param userPoolId * @returns {Promise<void>} */ const createUser = async (username, password, phoneNumber, userPoolId) => { const params = { UserPoolId: userPoolId, Username: username, TemporaryPassword: password }; if (phoneNumber) { _.assign(params, { UserAttributes: [ { Name: 'phone_number', Value: phoneNumber }, { Name: 'phone_number_verified', Value: 'true' } ], MessageAction: 'SUPPRESS' }); } const res = await cognitoIdentityServiceProvider .adminCreateUser(params) .promise(); console.log('Created CognitoUserPool', res); await res; }; /** * ユーザーのパスワードを確定(CONFIRMEDへ強制的に移行) * @param username * @param password * @param userPoolId * @returns {Promise<void>} */ const setAdminPassword = async (username, password, userPoolId) => { const params = { Password: password, UserPoolId: userPoolId, Username: username, Permanent: true }; await cognitoIdentityServiceProvider.adminSetUserPassword(params, function ( err, data ) { if (err) { throw err; } console.log('adminSetUserPassword CognitoUserPool', params); return data; }); }; ``` # 余談 まぁ本来はパスワードを第三者が知ること自体よくないので、ユーザー自身にパスワードを入力させるようにしましょう。Cognitoのデフォルト設定に則りSDK利用することで、セキュア認証を簡単に構築できます。
2021.01.07
Cognitoの2段階認証設定を、AWSSDKで無効化する方法(非公式方法)
# 概要 MFA設定されているユーザーのMFAを無効化したいけど、公式になかなか情報が載ってないのでシェア。 # ソースコード MFAOptions配列を空配列で実行すると無効化できます。逆に指定したい時は適宜配列を指定してください。 ```js /** * MFA設定の有効化 * @param username * @param twoStepAuthentication * @param userPoolId * @returns {Promise<void>} */ const setUserMFAPreference = async ( username, twoStepAuthentication, userPoolId ) => new Promise((resolve) => { const params = { Username: username, UserPoolId: userPoolId, MFAOptions: [] // 空のまま更新すると無効化できる様子(非公式情報) }; if (twoStepAuthentication) { params.MFAOptions.push({ AttributeName: 'phone_number', DeliveryMedium: 'SMS' }); } cognitoIdentityServiceProvider.adminSetUserSettings(params, (err, data) => { if (err) { throw err; } else { console.log( 'Set CognitoUserPool Multi-factor authentication :', username ); resolve(data); } }); }); ``` そのまま関数をコピペして使ってください👍
2021.01.07
ローカルでのAWSSDK実行時にAWSプロファイルを指定する方法
# 概要 ローカルのJavascriptをNodeJSで実行する際に、Javascript上でローカルのAWSプロファイルを指定する方法を紹介。意外と便利です。 # ソースコード ```js // AWSプロファイルの指定 const AWS = require('aws-sdk'); const credentials = new AWS.SharedIniFileCredentials({ profile: 'exampleProfile', filename: '~/.aws/credentials' }); AWS.config.credentials = credentials; // 以降のAWS SDKは、ローカルのexampleProfileを使用して実行されます const {CognitoIdentityServiceProvider} = require('aws-sdk'); const documentClient = new AWS.DynamoDB.DocumentClient({ region: 'ap-northeast-1' }); const cognitoIdentityServiceProvider = new CognitoIdentityServiceProvider({ region: 'ap-northeast-1' }); ``` 使い所としては、ローカルでCognitoやDynamoDBにテストユーザーを作成しておきたい時とかですね。(サインアップ処理が完成していない状況でサインアップを実装したい時がユースケース) 色々と応用方法ありそう。
2021.01.07
Wappalyzerで使用技術を確認しよう
# Wappalyzerとは Googleの拡張機能で提供されている「Wappalyzer」 これは、サイトで使っているCMSや技術・アクセス解析が分かるツールで結構愛用してる。 https://chrome.google.com/webstore/detail/wappalyzer/gppongmhjkpfnbhagpmjfkannfbllamg?hl=ja # 早速使ってみた SIHMBAに使ってみた。 ![](https://article-images.shimba.io/01ETCPVNDWWSCRETJXZZ0KKH0D.png =2110x1358) お、NuxtjsとLodash使ってる。
2020.12.25
depcheckでは、未使用プラグインは確認出来ない?
# 簡単に経緯 Nuxt.jsを使用していて、最初にプラグインをガンガン入れていました。 ようやく開発も落ち着いて来たので、ここらで未使用のプラグインを精査したいなと思いコマンド一つで、未使用プラグインをリストで表示してくれるものを探していました。 そしたら、このプラグインが良いと記事で出てきたので使ってみました。 **depcheck** https://www.npmjs.com/package/depcheck # 早速実行 コマンド一つというのが魅力的だったので、早速実行。 ``` % npx depcheck npx: installed 110 in 10.306s Unused dependencies * @nuxtjs/axios * @riophae/vue-treeselect * core-js * cors * http-proxy-1.0.2 * normalize.css * vue-cascader-select * vue-smooth-scroll * vue2-smooth-scroll Unused devDependencies * @nuxtjs/dotenv * @nuxtjs/eslint-module * @nuxtjs/stylelint-module * @vue/test-utils * babel-core * babel-eslint * babel-jest * postcss * stylelint-config-prettier * stylelint-config-standard * vue-jest ``` んー? # 使用してるパッケージも出てくる どうやら、importとかで使用していないものを未使用と定義しているっぽい? (ちゃんと調べていないので正確では無いです。) nuxt.config.jsに記載していたのですが、未使用プラグインと認定されてしまいました。 ``` modules: [ '@nuxtjs/dotenv', "@nuxtjs/axios" ], ``` # 最終的にどうしたか 結局、プラグイン名を全体検索して、使用していなさそうなのを削除しました。 消したのは以下の3つですね。(効率悪い。。) ``` * vue-cascader-select * vue-smooth-scroll * vue2-smooth-scroll ``` 私の理解不足もあるかと思うので、どなたか未使用プラグインを確認する方法をご教授いただけると嬉しいです。。
2020.12.22
UXの5段階モデルについてメモ
# 5段階モデルとは ジェームズ・ギャレットは、Web構築時に考慮すべきことはユーザーにとってのエクスペリエンスであるとしています。 そして、UXの構築要素を5つに分けました。 1. 戦略(why feature is required) 1. 要件(what feature does) 1. 構造(how feature works) 1. 骨格(how feature forms) 1. 表面(how feature looks) ## 戦略(why feature is required) どのような価値を、誰に届けるのかなどを設計します。 ### 戦略段階での具体的なデザイン手法 サービスやプロダクトを開発する上で、必要になるインプットを行います。 #### インプットを得るためのリサーチ方法 - エグゼクティブインタビュー - 企業戦略の中での事業の位置付けの理解 - 企業戦略と事業戦略の関連を理解することを目的 - ユーザーインタビュー - ユーザーの抱えている課題を理解する #### ビジネス側の観点でのリサーチ・分析 - リーンキャンバス - ビジネスモデルキャンバス - KPIツリー #### その他 - バリュープロポジションマップ - 競合との価値の差別化をどのように行うかを考える場合はを用いる - 競合調査などを行う ### 成果物 - サービス - プロダクトビジョン - コンセプト - ビジネスモデル ## 要件(what feature does) サービス/プロダクトを通してユーザーが体験するストーリー、プロダクトに必要な機能仕様や要件などを設計します。 ### 要件段階での具体的なデザイン手法と成果物 戦略段階で得た成果物を用いて、体験のアイデア創出を行い一連の体験への落とし込みます。 必要な機能の洗い出しなどを行います。 #### アイデア創出 - ストーリーボード - バリュープロポジションキャンバス #### 全体の一連の体験定義を行うには - サービスブループリント - カスタマージャーニーマップなど ## 構造(how feature works) 以下の設計をして、UIの全体構造を作っていきます。 この段階で設計した情報の構造がUIのデータ構造になります。 - ユーザー体験に必要な機能 - インターフェースの設計 - どのような情報とデータの構造で実現するのかという部分など ### 構造段階での具体的なデザイン手法 以下を参考に、プロダクトのドメインを定めます。 - ユーザーストーリー - カスタマージャーニーマップ ### 成果物 - 必要な情報の洗い出し - 情報の関連性の明示 - 定めたドメインにおける概念図の作成 - ナビゲージョンの設計など ## 骨格(how feature forms) ユーザーが触れることになるであろうより具体的なインターフェイスの骨組みを設計します。 情報の構造にレイアウトを当ててUIの骨組みを組み立てていきます。 ### 骨格段階での具体的なデザイン手法と成果物 ナビゲーションの構造図や扱う情報の構造などを用いてワイヤーフレームの作成などを行います。 作ったワイヤーフレームを用いてユーザーテストを行い、そのユーザーテスト結果を分析するプロトタイピングを繰り返してワイヤーフレームの修正を行っていきます。 ## 表面(how feature looks) プロダクトがユーザーにもたらす感性的な要素などを設計します。 UIを通してどのようなプロダクトの印象を与えるのかというUIグラフィックの設計、インターフェースとしてさらにスムーズな情報の伝達や、気持ちよさを感じさせるモーションの設計など構造的な部分だけではなく感性的な領域に深く入り込む部分も考えながらグラフィックやモーションの設計などを行います。 ### 表層段階での具体的なデザイン手法と成果物 プロダクトの表層部分を通してどのような印象を与えたいのかや、ブランドにあったカラー選定を行いUIのグラフィックのデザインをしていきます。 また情報をより伝達し、ユーザーがプロダクトとよりインタラクトしている感覚を与えるモーションもこの段階で設計していきます。 ロゴデザインや、VIデザインもユーザーの中のプロダクトのイメージとして印象に強く残る効果を与えるものでしょう。
2020.12.21
Figmaに関してメモ
# 概要 サンフランシスコのFigma Inc.という会社が提供するデザインツール https://www.figma.com/ ## メリット - オンラインでのリアルタイム共有機能 - 複数人で同時にデザインする事が可能 - 作業の履歴を残せる(Gitのようなバージョン管理機能) - Mac、Windowsどちらも対応 - ブラウザで動作する(アプリでインストールも可能) - レスポンシブデザインが作成可能 - Sketchのファイルをインポート可能 - UI Kidでサンプルが多数 ## デメリット - フォント周りに弱い - 海外製アプリだからなのか、フォント周りにバグなどが多い気がします。 - 日本語テキストがうまく表示されなかったり、文字詰めがいい感じに調整出来なかったり。 - 全員が同じ環境でない事による不具合(有料フォントの有無などで表示崩れがある) - データを恒久的に取っておきたい(.figで書き出せるが、FIgmaで開けない謎現象がある) # 料金 - 年払い:12$ - 月払い:15$
2020.12.21
LocalStackを、EventBridgeのモックに指定してみた
ローカル環境でのEventBridgeの作成・削除などを行う必要が発生したので、実装してみた。 # DockerでLocalStackの起動 ```bash version: '2.1' services: localstack: container_name: '${LOCALSTACK_DOCKER_NAME-localstack_main}' image: localstack/localstack network_mode: bridge ports: - '4566:4566' - '4571:4571' - '${PORT_WEB_UI-8081}:${PORT_WEB_UI-8081}' environment: - SERVICES=${SERVICES- } - DEBUG=${DEBUG- } - DATA_DIR=${DATA_DIR- } - PORT_WEB_UI=${PORT_WEB_UI- } - LAMBDA_EXECUTOR=${LAMBDA_EXECUTOR- } - KINESIS_ERROR_PROBABILITY=${KINESIS_ERROR_PROBABILITY- } - DOCKER_HOST=unix:///var/run/docker.sock - HOST_TMP_FOLDER=${TMPDIR} volumes: - '${TMPDIR:-/tmp/localstack}:/tmp/localstack' - '/var/run/docker.sock:/var/run/docker.sock' ``` # SDKでエンドポイントを指定 ```js if (STAGE === 'local') params.endpoint = 'http://localhost:4566'; const eventBridgeClient = new AWS.EventBridge({ ...params, ...{apiVersion: '2015-10-07'} }); ```
2020.12.18
ローカル環境でAWS SQSを構築する方法
SQSとLambdaのポーリングを実装するときに便利だったのでシェア。 # elasticmq:latest起動+SLSにプラグイン導入 ```bash # elasticmq:latestを起動(Queueのエンドポイント) docker run -it -p 9999:9324 s12v/elasticmq:latest # SQSのプラグインを入れる yarn add serverless-offline-sqs -D ``` # プラグインの設定 autoCreateはtrueにされることを推奨します。Queueが存在しない場合自動的にQueueが作成されます。 ```yaml # serverless.yml plugins: serverless-offline-sqs custom: serverless-offline-sqs: autoCreate: true apiVersion: '2012-11-05' endpoint: http://0.0.0.0:9999 # CMQのエンドポイントを指定 region: ap-northeast-1 accessKeyId: root secretAccessKey: root skipCacheInvalidation: false ``` あとは適当にQueueのエンドポイントへリクエストを投げてみる。 SQS⇨Lambdaキックを指定している場合は、自動的にポーリングされるはずです。 ```bash SQS_MSG=$(cat << EOS { "Example": "サンプル", } EOS ) aws sqs send-message \ --queue-url "http://localhost:9999/queue/example-queue" \ --message-body "${SQS_MSG}" \ --endpoint-url "http://localhost:9999" ``` # 手動でQueue作成される場合 CLIで下記を実行する。 ``` aws sqs create-queue --queue-name example-queue-name --endpoint-url http://localhost:9999 ```
2020.12.18
API GatewayのリクエストバリデーションをJSON schema で自動化してみた
GraphQLの時は、スキーマファイルで値の型情報定義や必須有無を手軽に指定できるけど、APIGatewayなどのRestの場合はどのようにやるのかをシェアします。この方法を覚えるとバリデーションのソースコードの量がグッと減ります。 (ビジネスロジックに依存したバリデーションは流石にソースコードでカバーする必要がありますが) # serverless.yml ```yaml functions: Example: handler: src/functions/Example.handler name: '${self:provider.stage}-Example' events: - http: path: '/example' method: post request: schema: application/json: '${file(rest_request_models/post_example.json)}' ``` # JSONSchema定義 rest_request_models/post_example.json ```json { "definitions": {}, "$schema": "http://json-schema.org/draft-04/schema#", "type": "object", "title": "POST /sample/ schema", "required": ["sample01", "sample02"], "properties": { "sample01": { "type": "string", "title": "Sample01" }, "sample02": { "type": "string", "title": "Sample01" } } } ``` デプロイしたら、実際にリクエストを出してみてください。上記の例では、sample01とsample02をBodyに含めていないリクエストを行なった場合に、400 bad parameterが返ってくるはずです。 また、細かいJSONSchemaの定義方法は、公式サイトを見てキャッチアップされるのを推奨します。JSON Schemaのバージョンに気をつけて定義作成してくださいね。 https://json-schema.org/
2020.12.13
DynamoDBの指定のテーブルを完全に物理削除するバッチ処理を作ってみた
少し力技なところもあるけど、作成したのでシェア。 # ソースコード batch/deleteDynamoDBAllTable.js ```js /** * DynamoDBの全テーブルを一括で削除 */ const AWS = require('aws-sdk'); const ddb = new AWS.DynamoDB({ region: 'ap-northeast-1' }); const credentials = new AWS.SharedIniFileCredentials({ profile: 'mura-kumo', filename: '~/.aws/credentials' }); AWS.config.credentials = credentials; AWS.config.update({ region: 'ap-northeast-1', endpoint: 'https://dynamodb.ap-northeast-1.amazonaws.com' }); const prefix = process.argv[2]; const tables = [ `${prefix}-User`, `${prefix}-Product` ]; Promise.all( tables.map(async (tableName) => { console.log({ TableName: tableName }); return ddb.deleteTable({ TableName: tableName }, function (err, data) { if (err) { return ( 'Unable to delete table. Error JSON:', JSON.stringify(err, null, 2) ); } return ( 'Deleted table. Table description JSON:', JSON.stringify(data, null, 2) ); }); }) ) .then(console.log) .catch(console.error); ``` # 実行方法 プロファイルは適宜変更ください。 ``` AWS_SDK_LOAD_CONFIG=true AWS_PROFILE=example-profile node batch/deleteDynamoDBAllTable.js dev ``` # どんな時に使用する? よく使用するケースとしては、ServerllesFrameworkもしくはCloudFormation直書きの時に、テーブルのPK、SK定義を変更しデプロイしたくなった時です。もちろんステージングもしくは開発環境専用ですけどね・・。
2020.12.13
NuxtJSのS3デプロイ時に環境変数を適用させる方法
NuxtjSをSPAモードで実装し、buildしてDistをS3へデプロイするのはよくあるケースだが、 その際に環境変数がうまく適用されない時がしばしばある。 そんな時はdotenvを使用して対応する。 # パッケージのインストール ```bash yarn add @nuxtjs/dotenv -D yarn add dotenv -D ``` # 環境変数を取得し適用させる ## .envの作成 プロジェクトのルートディレクトリへ.envを作成します。 ```env ENV=dev EXAMPLE=exampleValue ``` ## nuxt.config.jsの修正 nuxt.config.jsを開いて下記のように修正してください。 ```js require("dotenv").config() const {ENV, EXAMPLE} = process.env export default { env: { dev: ENV !== 'production', EXAMPLE, } } ``` # ビルド 設定はこれで済んだので、あとはビルドするだけ。 ビルド後にnuxt start し、console.logなどで値を確認するとちゃんと入っているはずです。 ```bash nuxt build nuxt start ``` ```js console.log(process.env.dev) // true console.log(process.env.EXAMPLE) // exampleValue ``` # TIPS CIなどで自動的に.envを作成する場合は下記のようにします。 ```json // package.json { "scripts" : { "build:dev": "npm run env:dev && nuxt build", "build:prd": "npm run env:prd && nuxt build", "env:dev": "cp env/dev .env", "env:prd": "cp env/prd .env" } } // env/prd ENV=production // env/dev ENV=dev ``` ビルド後はdistディレクトリ以下を、s3 syncでデプロイします。 ``` s3 sync ./dist s3://{bucket_name} ```
2020.12.13
package.jsonのscriptでAWSのプロファイルを切り替える方法
初歩的なことだが、多くのシーンで利用するのでメモしておく。 ```json { "name": "Example", "version": "1.0.0", "private": true, "scripts": { ... "s3:cors:mockdata": "AWS_SDK_LOAD_CONFIG=true AWS_PROFILE=example-profile aws s3api put-bucket-cors --bucket mock-data-bucket --cors-configuration file://cors.json", "s3:sync:mockdata": "AWS_SDK_LOAD_CONFIG=true AWS_PROFILE=example-profile aws s3 sync ./mockData s3://mock-data-bucket/ && yarn cf:invalidation", "s3:sync:dev": "AWS_SDK_LOAD_CONFIG=true AWS_PROFILE=example-profile aws s3 sync ./dist s3://peoduct-bucket", "cf:invalidation:dev": "AWS_SDK_LOAD_CONFIG=true AWS_PROFILE=example-profile aws cloudfront create-invalidation --distribution-id XXXXXX --paths \"/*\"", "cf:invalidation:mockdata": "AWS_SDK_LOAD_CONFIG=true AWS_PROFILE=example-profile aws cloudfront create-invalidation --distribution-id XXXXXX --paths \"/*\"" }, "dependencies": { ... }, "devDependencies": { ... } } ``` モックデータのJSONは、S3に設定することでデプロイ後もローカルと同じ表示にすることができる。 (ローカルAPIサーバーを構築するとホットスタンバイするサーバーが必要となるので面倒だしコスパ悪い)
2020.12.13
OSXをBigSurへ更新したら、brewが疎通しなく立った時に対応方法
今朝更新したら、Nodeなどのコマンドが一式疎通しなくなった。 PATHの情報がうまく引き継がれていない?のかな。 下記のコマンドで疎通するようになったのでシェアします。 ```bash git -C "/usr/local/Homebrew/Library/Taps/homebrew/homebrew-core" fetch --unshallow brew upgrade # やらなくてもいいかも brew update ```
2020.12.13
DynamoDBの結果項目数のScannedCount、Countについて解説
DynamoDBでインデックスまたはクエリーを実行した取得結果(いわゆる検索結果件)について、2つのパラメーターが返却される。 - ScannedCount - Count 初めてDynamoDB触る人は、「検索結果件数が2つあるけど何なの?」「総件数より少ない気がする」と感じるはずなので、解説しておく。 # ScannedCount その名の通り、スキャンした件数である。DynamoDBは一度のクエリー(スキャン)で 1 MB までしかItemを取得できない。 1MB以上のデータがある場合は、NextTokenというカーソルトークン(検索結果の位置のインデックス)と共に、1MBまでのItemを返すようになっている。 Itemを取得する際、どのフィールドを取得指定したのかで、データ量が変わるので一概に1MB=1000件という表現はできない。(不要なデータを取得しオーバーヘッドになっている場合は無論取得できるItem数が減少してくる) ちなみにこのカーソルトークンという仕組みは、お馴染みのElasticSearchでも採用されているので是非慣れておいた方が良い。 # Count 検索結果として返却した件数になる。 例) - Userテーブル(1000件) - 年齢20歳未満のユーザーは200件 - 年齢フィールドにはインデックスは貼られていない - クエリースキャンに20歳未満を指定 ```json { ScannedCount : 1,000, Count : 200 } ``` # Gmailも総件数とっていません、NoSQLで真面目に総件数を取得するのは諦めましょう 総件数をちゃんと取得するのは、DynamoDBでは諦めましょう。 GoogleのGmailなどを見てもちゃんと取得していないですし。(確かに結果の全件数が知りたいなんてユーザーってあまりいないですよね、BIツールじゃありませんし) ![](https://article-images.shimba.io/01ECYQ62FZMR9Z22R4MABD1GTG.png =598x94) どうしても欲しいのなら、ElasticSearchの導入を推奨しますー。
2020.07.11
Amplifyで@connectionに指定したフィールドに対するクエリーを考える
# はじめに まず、@connectionはあくまでもデータをIDでマッピングして取得するだけなので、@connectionに対して細かなフィルターは指定できません。そのため、N対Nの設計に則り設計を行いましょう。 # N対Nの設定方法 例えば、下記のようなシステム管理者を保持するスキーマ構造を考えます。 - ロールは、様々なシステムに対応するためにN個存在 - ロールは、1ユーザーにN個設定が可能 ```js type User { id: ID! name: String! } type Role { id : ID! title: String! } ``` 上記に対し、N対Nの情報を持つマッピングテーブルを作成します。 参考:https://docs.amplify.aws/cli/graphql-transformer/directives#belongs-to ```js type User { id: ID! name: String! roles: [UserRole] @connection(keyName: "byUser", fields: ["id"]) } type UserRole @model(queries: null) @key(name: "byUser", fields: ["userID", "roleID"]) @key(name: "byRole", fields: ["roleID", "userID"]) { id: ID! userID: ID! roleID: ID! user: User! @connection(fields: ["userID"]) role: Role! @connection(fields: ["roleID"]) } type Role { id : ID! title: String! users: [UserRole] @connection(keyName: "byRole", fields: ["id"]) } ``` これで、ロールに属するユーザ一覧、ユーザーに設定されているロール一覧を取得することが可能です。 尚、その範囲の中でフィルターをしたい場合(複雑な探索をしたい)は、ElasticSearchの導入を推奨します。 クエリーフィルターを実行した場合DynamoDBのリードキャパシティユニットを消費してしまいレイテンシーが高くなってしまう可能性が高いので。
2020.07.11
Amplifyにユーザープールがデフォルトで非対応のIDPを追加できるか調査した結果
結論から言うと、管理コンソールから手動でIDP追加は可能だが、Amplify CLIで自動的には追加はできないようだ。対応方法をのせておく。 # amplify/backend/auth配下のAWS CloudFormationテンプレートを編集すれば行けるけど非推奨 いけましたが、amplify update authやamplify env addした場合に設定が消えます。(env addの場合は消えるというよりかは反映されななかった) なので、私は割り切って手動で管理コンソールで構築する方を選択しました。 # 管理コンソールにて手動構築する方法 大まかな流れはこれ。今回はAuth0をOIDCで追加した。(SAML認証でもできるが何となくOIDCにしてみた) 1. Auth0へSignUpしOIDCの設定を実施 2. amplify add auth/amplify update authでOAuth関連の設定を入れる(コールバックURLやドメイン名) 3. Amplifyで生成したCognitoのユーザープールへアクセスし、外部サインインプロバイダーのOIDCへ直接Auth0を追加 4. AmplifyのfederatedSignIn関数へAuth0を指定しフェデレーションサインインを設定 # redirect_mismatchミスマッチエラーが出た場合の対処 ホストUIから起動した場合は正常に行くのに、フロントエンドからCognitoへアクセスした場合にredirect_mismatchエラーで画面が進まない現象について、これはまずAmplifyの不具合だそうでした。 原因は、aws_exports.jsのoauthプロパティの箇所。 ```json "oauth": { ... "redirectSignIn": "http://localhost:3000/thankyou/, http://xxxx:3000/thankyou/, http://bbbb:3000/thankyou/", "redirectSignOut": "http://localhost:3000/, https://example:3000/ ", ... } ``` どうやら、カンマ区切りの文字列をそのままURLパラメーターとしてCognitoへリクエストしてしまうらしい。 Cognitoのアプリクライアントの設定には、確かにカンマ区切りで入っているがCognitoのリクエストの際に送るのはあくまでも単一のURLだ。(とんでもない不具合ですね) 調査結果、Issueが開かれており現在は非対応なようでした。 Allowing multiple redirectSignIn/redirectSignOut urls breaks federated auth https://github.com/aws-amplify/amplify-cli/issues/2792 私は独自ですが下記のように実装を行って回避しました。 環境:Nuxtjs 2系 **src/plugins/amplify.js** ```js import Amplify, {Auth} from 'aws-amplify'; import awsconfig from '../aws-exports'; import _ from 'lodash' // 実行中のURLを送信するようにしました(事前にコールバックURLとして追加する必要があります) _.assign(awsconfig.oauth, { redirectSignIn: window.location.protocol + '//' + window.location.host + "/thankyou/", redirectSignOut: window.location.protocol + '//' + window.location.host + "/" }) Amplify.configure(awsconfig) ``` **nuxt.config.js** ```js plugins: [ ... {src: '@/plugins/amplify.js', mode: 'client'}, ... ], ``` そして最後に、リダイレクトURLを追加しておきます。(私はローカル環境とリモート、全環境を追加しています) ```bash amplify update auth ... ```
2020.07.11
NuxtjsとWordpressRestAPIで、シンタックスハイライトを作ってみました😎
WordpressのRestAPIを、CloudFront with AwsWAFで構築しています。 Wordpressでプログラムコードを含む記事をアップするケースがしばしばあるので、Nuxtjsでコードハイライトを開発してみました。 ブログ:https://www.ragate.co.jp/blog/articles/3730 # Nuxtjsにprismjsをインストールして色々と設定 まずはPrismjsをインストールします。 ```bash $ yarn add prismjs@1.20.0 ``` 次に、Nuxtjsへプラグインを作成します。 **nuxt.config.js** ```json { ... plugins: [ ... '~/plugins/prism.js' ] } ``` **plugins/prism.js** ```js import Prism from 'prismjs' import 'prismjs/themes/prism-solarizedlight.css' import 'prismjs/components/prism-json' import 'prismjs/components/prism-yaml' export default Prism ``` 次に、documentオブジェクトを取得できるタイミングでPrismを実行します。 **pages/blog/article/_article_id.vue** ```js import Prism from '~/plugins/prism' async mounted() { ... Prism.highlightAll() }, ``` # Wordpressでプラグインをインストール ワードプレスへ、[highlighting-code-block](https://wordpress.org/plugins/highlighting-code-block/)をインストールします。 そしてあとは投稿画面から言語指定をするだけです。 ![](https://article-images.shimba.io/01ECY9F6FX86QAYH18546R8BW7.png =1759x937)
2020.07.11
Serverless Frameworkで、API Gateway & Lambdaのマイクロアーキテクチャーを解説
マイクロアーキテクチャーに対して様々な異なる意見を聞くことが多いので、自分なりにちゃんとまとめてみました。 ソースコードを交え解説しようと思います。 # マイクローアーキテクチャーで意識すること - 1つのLambda関数=1つの目的 - Lambda関数同士は完全に疎結合 - 極力共通化しない # API GatewayのLambdaProxy関数はリソース×メソッド単位で構築 例えば、ミドルウェア的な処理実行するために、1つのLambda関数を呼び出しそこでルーティング処理するなどは、ご法度です。 (Lambdaがそのミドルウェアに強く依存してしまう危険性があり、疎結合にならなくなってしまう可能性が高い) ソースコードで言うと、下記のような粒度で構築するのがベストだと言えます。 ## マイクロアーキテクチャーの思想に則ったLambda関数 **serverless.yaml** ```yaml functions: getMe: handler: src/functions/me/get.handler events: - http: path: /me method: get authorizer: authorizer putMe: handler: src/functions/me/put.handler events: - http: path: /me method: put authorizer: authorizer ``` **src/functions/me/get.handler** 目的:ログイン中のユーザー情報を取得 ```js import _ from 'lodash' import {PREFIX, documentClient} from '../../awsUtils/dynamodb' import {ERROR_HEADERS, SUCCESS_HEADERS} from "../../models/headerModel"; export async function handler(event, context, callback) { const userId = event.requestContext.authorizer.principalId; if (!userId || userId === 'NO_AUTH') return callback(null, { statusCode: 400, body: JSON.stringify({ result: 'Invalid user' }), headers: ERROR_HEADERS }); await documentClient .get({ TableName: `${PREFIX}-users`, Key: {id: userId}, }) .promise() .then((res) => { if (res.Item) return callback(null, { statusCode: _.isEmpty(res.Item) ? 204 : 200, body: JSON.stringify({ ...res.Item }), headers: SUCCESS_HEADERS }); callback(null, { statusCode: 400, body: JSON.stringify({ result: 'Bad parameter' }), headers: ERROR_HEADERS }); }) .catch(() => callback(null, { statusCode: 400, body: JSON.stringify({ result: 'Bad parameter' }), headers: ERROR_HEADERS })) } ``` **src/functions/me/put.handler** 目的:ログイン中のユーザー情報を更新 ```js import _ from 'lodash' import {PREFIX, documentClient} from '../../awsUtils/dynamodb' import {ERROR_HEADERS, SUCCESS_HEADERS} from "../../models/headerModel"; export async function handler(event, context, callback) { const userId = event.requestContext.authorizer.principalId; if (!userId || userId === 'NO_AUTH') return callback(null, { statusCode: 400, body: JSON.stringify({ result: 'Invalid user' }), headers: ERROR_HEADERS }); const body = JSON.parse(event.body); await documentClient .get({ TableName: `${PREFIX}-users`, Key: {id: userId}, }) .promise() .then((res) => res.Item) .then((me) => { const newItem = _.merge({}, me, _.pick(body, [ 'fullname', 'company', 'websiteUrl', ]), {id: userId}) return documentClient.put({ TableName: `${PREFIX}-users`, Item: newItem, }) .promise() .then(() => callback(null, { statusCode: _.isEmpty(newItem) ? 204 : 200, body: JSON.stringify({ ...newItem }), headers: SUCCESS_HEADERS })) .catch(() => callback(null, { statusCode: 400, body: JSON.stringify({ result: 'Bad parameter' }), headers: ERROR_HEADERS })) }) } ``` Lambda中のソースコードは非常にシンプルで、初見でサクッと保守しやすいのがわかるかと思います。 PHPやJavaのフレームワークは便利機能を色々と提供してくれますが、継承などのOOP志向な仕組みにより、それなりに広範囲にソースコードを理解する必要があるので、キャッチアップや読解に時間を要します。 マイクロアーキテクチャーのLambda関数は、誰にでもわかりやすい人に依存しない簡潔なコードを提供してくれます。 ### 非推奨のLambda関数の実装の仕方 **serverless.yaml** ```yaml functions: getMe: handler: src/functions/me/index.get ... putMe: handler: src/functions/me/index.put ... ``` **src/functions/me/index.js** ```js export async function get(event, context, callback) { ... } export async function put(event, context, callback) { ... } ``` 1つのファイルに目的が2つ以上存在し、見通しが悪くなってしまっています...。 テストもしにくくなってしまっています。 ## 共通化は極力しないことを推奨 例えば、前述のソースコードだとこの辺りを共通化したくなるはずです。 ```js const userId = event.requestContext.authorizer.principalId; if (!userId || userId === 'NO_AUTH') return callback(null, { statusCode: 400, body: JSON.stringify({ result: 'Invalid user' }), headers: ERROR_HEADERS }); ``` でもあえて共通化しません。共通化のコードが増えれば増えるほど複雑になります。 この辺りはJava出身のエンジニアだとかなり違和感を感じると思いますが、共通化や継承化はプログラミングにそれなりに複雑さを与え、修正しにくくなっていきますし、Lambdaを疎結合からどんどん遠くさせます。 可能な限りLambda関数内で処理が完結するようにしましょう。 ## こんな時は共通化を検討したい - 外部の処理を扱うための手続き(DynamoDBへアクセスするための設定等) - レスポンスに固定値を与えるとき # まとめ もちろん宗教論あると思いますが、私は実務上で最も開発がスムーズに進んだのは本記事のようなアーキテクチャーでした。 プルリクエストが非常に読みやすかったですし、フロントエンドの人や第三者の人が気軽に参加できるサーバーサイドを構築を行うことができました。
2020.07.10
Serverless Frameworkのリソースを外部ファイル化してみた
```yaml ... resources: - ${file(./serverless-dynamodb-tables.yml)} - ${file(./serverless-gateway-responses.yml)} ... ``` **serverless-dynamodb-tables.yml** ```yaml Resources: users: Type: 'AWS::DynamoDB::Table' DeletionPolicy: Retain Properties: TableName: ${self:provider.environment.DYNAMODB_TABLE_PREFIX}-users AttributeDefinitions: - AttributeName: id AttributeType: S - AttributeName: displayUserId AttributeType: S KeySchema: - AttributeName: id KeyType: HASH BillingMode: PAY_PER_REQUEST GlobalSecondaryIndexes: - IndexName: byDisplayUserId KeySchema: - AttributeName: displayUserId KeyType: HASH Projection: ProjectionType: ALL ``` **serverless-gateway-responses.yml** ```yaml Resources: GatewayResponseACCESSDENIED: Type: 'AWS::ApiGateway::GatewayResponse' Properties: ResponseParameters: gatewayresponse.header.Access-Control-Allow-Origin: "'*'" gatewayresponse.header.Access-Control-Allow-Headers: "'*'" gatewayresponse.header.Access-Control-Allow-Methods: "'GET,POST,PUT,DELETE,OPTIONS,HEAD'" ResponseType: ACCESS_DENIED RestApiId: Ref: 'ApiGatewayRestApi' StatusCode: '403' GatewayResponseAPICONFIGURATIONERROR: Type: 'AWS::ApiGateway::GatewayResponse' Properties: ResponseParameters: gatewayresponse.header.Access-Control-Allow-Origin: "'*'" gatewayresponse.header.Access-Control-Allow-Headers: "'*'" gatewayresponse.header.Access-Control-Allow-Methods: "'GET,POST,PUT,DELETE,OPTIONS,HEAD'" ResponseType: API_CONFIGURATION_ERROR RestApiId: Ref: 'ApiGatewayRestApi' StatusCode: '500' GatewayResponseAUTHORIZERCONFIGURATIONERROR: Type: 'AWS::ApiGateway::GatewayResponse' Properties: ResponseParameters: gatewayresponse.header.Access-Control-Allow-Origin: "'*'" gatewayresponse.header.Access-Control-Allow-Headers: "'*'" gatewayresponse.header.Access-Control-Allow-Methods: "'GET,POST,PUT,DELETE,OPTIONS,HEAD'" ResponseType: AUTHORIZER_CONFIGURATION_ERROR RestApiId: Ref: 'ApiGatewayRestApi' StatusCode: '500' GatewayResponseAUTHORIZERFAILURE: Type: 'AWS::ApiGateway::GatewayResponse' Properties: ResponseParameters: gatewayresponse.header.Access-Control-Allow-Origin: "'*'" gatewayresponse.header.Access-Control-Allow-Headers: "'*'" gatewayresponse.header.Access-Control-Allow-Methods: "'GET,POST,PUT,DELETE,OPTIONS,HEAD'" ResponseType: AUTHORIZER_FAILURE RestApiId: Ref: 'ApiGatewayRestApi' StatusCode: '500' GatewayResponseBADREQUESTPARAMETERS: Type: 'AWS::ApiGateway::GatewayResponse' Properties: ResponseParameters: gatewayresponse.header.Access-Control-Allow-Origin: "'*'" gatewayresponse.header.Access-Control-Allow-Headers: "'*'" gatewayresponse.header.Access-Control-Allow-Methods: "'GET,POST,PUT,DELETE,OPTIONS,HEAD'" ResponseType: BAD_REQUEST_PARAMETERS RestApiId: Ref: 'ApiGatewayRestApi' StatusCode: '400' GatewayResponseBADREQUESTBODY: Type: 'AWS::ApiGateway::GatewayResponse' Properties: ResponseParameters: gatewayresponse.header.Access-Control-Allow-Origin: "'*'" gatewayresponse.header.Access-Control-Allow-Headers: "'*'" gatewayresponse.header.Access-Control-Allow-Methods: "'GET,POST,PUT,DELETE,OPTIONS,HEAD'" ResponseType: BAD_REQUEST_BODY RestApiId: Ref: 'ApiGatewayRestApi' StatusCode: '400' GatewayResponseDEFAULT4XX: Type: 'AWS::ApiGateway::GatewayResponse' Properties: ResponseParameters: gatewayresponse.header.Access-Control-Allow-Origin: "'*'" gatewayresponse.header.Access-Control-Allow-Headers: "'*'" gatewayresponse.header.Access-Control-Allow-Methods: "'GET,POST,PUT,DELETE,OPTIONS,HEAD'" ResponseType: DEFAULT_4XX RestApiId: Ref: 'ApiGatewayRestApi' GatewayResponseDEFAULT5XX: Type: 'AWS::ApiGateway::GatewayResponse' Properties: ResponseParameters: gatewayresponse.header.Access-Control-Allow-Origin: "'*'" gatewayresponse.header.Access-Control-Allow-Headers: "'*'" gatewayresponse.header.Access-Control-Allow-Methods: "'GET,POST,PUT,DELETE,OPTIONS,HEAD'" ResponseType: DEFAULT_5XX RestApiId: Ref: 'ApiGatewayRestApi' GatewayResponseEXPIREDTOKEN: Type: 'AWS::ApiGateway::GatewayResponse' Properties: ResponseParameters: gatewayresponse.header.Access-Control-Allow-Origin: "'*'" gatewayresponse.header.Access-Control-Allow-Headers: "'*'" gatewayresponse.header.Access-Control-Allow-Methods: "'GET,POST,PUT,DELETE,OPTIONS,HEAD'" ResponseType: EXPIRED_TOKEN RestApiId: Ref: 'ApiGatewayRestApi' StatusCode: '401' GatewayResponseINTEGRATIONFAILURE: Type: 'AWS::ApiGateway::GatewayResponse' Properties: ResponseParameters: gatewayresponse.header.Access-Control-Allow-Origin: "'*'" gatewayresponse.header.Access-Control-Allow-Headers: "'*'" gatewayresponse.header.Access-Control-Allow-Methods: "'GET,POST,PUT,DELETE,OPTIONS,HEAD'" ResponseType: INTEGRATION_FAILURE RestApiId: Ref: 'ApiGatewayRestApi' StatusCode: '504' GatewayResponseINTEGRATIONTIMEOUT: Type: 'AWS::ApiGateway::GatewayResponse' Properties: ResponseParameters: gatewayresponse.header.Access-Control-Allow-Origin: "'*'" gatewayresponse.header.Access-Control-Allow-Headers: "'*'" gatewayresponse.header.Access-Control-Allow-Methods: "'GET,POST,PUT,DELETE,OPTIONS,HEAD'" ResponseType: INTEGRATION_TIMEOUT RestApiId: Ref: 'ApiGatewayRestApi' StatusCode: '504' GatewayResponseINVALIDAPIKEY: Type: 'AWS::ApiGateway::GatewayResponse' Properties: ResponseParameters: gatewayresponse.header.Access-Control-Allow-Origin: "'*'" gatewayresponse.header.Access-Control-Allow-Headers: "'*'" gatewayresponse.header.Access-Control-Allow-Methods: "'GET,POST,PUT,DELETE,OPTIONS,HEAD'" ResponseType: INVALID_API_KEY RestApiId: Ref: 'ApiGatewayRestApi' StatusCode: '403' GatewayResponseINVALIDSIGNATURE: Type: 'AWS::ApiGateway::GatewayResponse' Properties: ResponseParameters: gatewayresponse.header.Access-Control-Allow-Origin: "'*'" gatewayresponse.header.Access-Control-Allow-Headers: "'*'" gatewayresponse.header.Access-Control-Allow-Methods: "'GET,POST,PUT,DELETE,OPTIONS,HEAD'" ResponseType: INVALID_SIGNATURE RestApiId: Ref: 'ApiGatewayRestApi' StatusCode: '403' GatewayResponseMISSINGAUTHENTICATIONTOKEN: Type: 'AWS::ApiGateway::GatewayResponse' Properties: ResponseParameters: gatewayresponse.header.Access-Control-Allow-Origin: "'*'" gatewayresponse.header.Access-Control-Allow-Headers: "'*'" gatewayresponse.header.Access-Control-Allow-Methods: "'GET,POST,PUT,DELETE,OPTIONS,HEAD'" ResponseType: MISSING_AUTHENTICATION_TOKEN RestApiId: Ref: 'ApiGatewayRestApi' StatusCode: '403' GatewayResponseQUOTAEXCEEDED: Type: 'AWS::ApiGateway::GatewayResponse' Properties: ResponseParameters: gatewayresponse.header.Access-Control-Allow-Origin: "'*'" gatewayresponse.header.Access-Control-Allow-Headers: "'*'" gatewayresponse.header.Access-Control-Allow-Methods: "'GET,POST,PUT,DELETE,OPTIONS,HEAD'" ResponseType: QUOTA_EXCEEDED RestApiId: Ref: 'ApiGatewayRestApi' StatusCode: '429' GatewayResponseREQUESTTOOLARGE: Type: 'AWS::ApiGateway::GatewayResponse' Properties: ResponseParameters: gatewayresponse.header.Access-Control-Allow-Origin: "'*'" gatewayresponse.header.Access-Control-Allow-Headers: "'*'" gatewayresponse.header.Access-Control-Allow-Methods: "'GET,POST,PUT,DELETE,OPTIONS,HEAD'" ResponseType: REQUEST_TOO_LARGE RestApiId: Ref: 'ApiGatewayRestApi' StatusCode: '413' GatewayResponseRESOURCENOTFOUND: Type: 'AWS::ApiGateway::GatewayResponse' Properties: ResponseParameters: gatewayresponse.header.Access-Control-Allow-Origin: "'*'" gatewayresponse.header.Access-Control-Allow-Headers: "'*'" gatewayresponse.header.Access-Control-Allow-Methods: "'GET,POST,PUT,DELETE,OPTIONS,HEAD'" ResponseType: RESOURCE_NOT_FOUND RestApiId: Ref: 'ApiGatewayRestApi' StatusCode: '404' GatewayResponseTHROTTLED: Type: 'AWS::ApiGateway::GatewayResponse' Properties: ResponseParameters: gatewayresponse.header.Access-Control-Allow-Origin: "'*'" gatewayresponse.header.Access-Control-Allow-Headers: "'*'" gatewayresponse.header.Access-Control-Allow-Methods: "'GET,POST,PUT,DELETE,OPTIONS,HEAD'" ResponseType: THROTTLED RestApiId: Ref: 'ApiGatewayRestApi' StatusCode: '429' GatewayResponseUNAUTHORIZED: Type: 'AWS::ApiGateway::GatewayResponse' Properties: ResponseParameters: gatewayresponse.header.Access-Control-Allow-Origin: "'*'" gatewayresponse.header.Access-Control-Allow-Headers: "'*'" gatewayresponse.header.Access-Control-Allow-Methods: "'GET,POST,PUT,DELETE,OPTIONS,HEAD'" ResponseType: UNAUTHORIZED RestApiId: Ref: 'ApiGatewayRestApi' StatusCode: '401' GatewayResponseUNSUPPORTEDMEDIATYPE: Type: 'AWS::ApiGateway::GatewayResponse' Properties: ResponseParameters: gatewayresponse.header.Access-Control-Allow-Origin: "'*'" gatewayresponse.header.Access-Control-Allow-Headers: "'*'" gatewayresponse.header.Access-Control-Allow-Methods: "'GET,POST,PUT,DELETE,OPTIONS,HEAD'" ResponseType: UNSUPPORTED_MEDIA_TYPE RestApiId: Ref: 'ApiGatewayRestApi' StatusCode: '415' GatewayResponseWAFFILTERED: Type: 'AWS::ApiGateway::GatewayResponse' Properties: ResponseParameters: gatewayresponse.header.Access-Control-Allow-Origin: "'*'" gatewayresponse.header.Access-Control-Allow-Headers: "'*'" gatewayresponse.header.Access-Control-Allow-Methods: "'GET,POST,PUT,DELETE,OPTIONS,HEAD'" ResponseType: WAF_FILTERED RestApiId: Ref: 'ApiGatewayRestApi' StatusCode: '403' ```
2020.07.10
Cognitoのユーザープールの削除処理
開発環境のCognitoのユーザープールを一掃したいケースが増えてきたので、スクリプトを作りしました。 ユーザー件数が多いとTooManyRequestsExceptionでとまりますが、一度に数百件ほど削除できた様子なので、根気強くリトライしましょう。 ```js /** * Dev環境のCognitoユーザーを削除します、一度に大量のユーザー削除を行うとTooManyRequestsExceptionエラーで停止しますが、根気強く頑張ってください * @type {string} */ const profile = 'xxxx'; const region = 'ap-northeast-1'; const AWS = require("aws-sdk"); AWS.config.credentials = new AWS.SharedIniFileCredentials({profile}); AWS.config.update({ region }); const userPoolId = 'ap-northeast-1_xxxx'; // dev環境のCognito const cognitoIdentityServiceProvider = new AWS.CognitoIdentityServiceProvider({ apiVersion: '2016-04-18' }); (async () => { const params = { UserPoolId: userPoolId, Limit: 60 }; const res = await cognitoIdentityServiceProvider.listUsers(params).promise() await Promise.all( res.Users.map(user => new Promise((resolve, reject) => { cognitoIdentityServiceProvider.adminDeleteUser({ Username: user.Username, UserPoolId: userPoolId }, (err, data) => { if (err) reject(err) resolve(data) }) })) ); let PaginationToken = res.PaginationToken; while (PaginationToken) { const r = await cognitoIdentityServiceProvider.listUsers({ UserPoolId: userPoolId, Limit: 60, PaginationToken }).promise() await Promise.all( r.Users.map(user => new Promise((resolve, reject) => { cognitoIdentityServiceProvider.adminDeleteUser({ Username: user.Username, UserPoolId: userPoolId }, (err, data) => { if (err) reject(err) resolve(data) }) })) ); if (!r.PaginationToken) break; PaginationToken = r.PaginationToken; } })(); ```
2020.07.10
Serverless FrameworkでAPI Gatewayのエンドポイントを取得する方法
色々と検討したけど、Fn::Joinで実現するのが最短そう。 ```yaml custom: apiEndpoint: Fn::Join: - "" - - "https://" - Ref: "ApiGatewayRestApi" - ".execute-api." - ${self:provider.region}. - Ref: "AWS::URLSuffix" - "/" - ${self:provider.stage} ``` 呼び出すときはこれ。 ```yaml postExample: handler: src/functions/example/post.handler environment: END_POINT: ${self:custom.apiEndpoint} events: - http: path: /example/ method: post ``` ```js import axios from 'axios' export async function handler(event, context, callback) { const result = await axios.post(process.env.END_POINT + "/example") callback(null, { statusCode: 200, body: JSON.stringify({ result }), headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Credentials': 'true' }, }) } ```
2020.07.08
Material-UIのコンポーネントを拡張する
このような関数`Main`のうち`TextField`を再利用したい。このような複数行にわたるコードは、DOMの構造を推測しづらくします。 ```tsx const Main: FunctionComponent = () => { return ( <TextField fullWidth onChange={event => setText(event.target.value)} placeholder={'検索'} value={text} variant={'filled'} InputProps={{ startAdornment: ( <InputAdornment position={'start'}> <Search /> </InputAdornment> ), }} /> ) } ``` `Props`には`TextFieldProps`を用いる。 ```tsx type Props = TextFieldProps const TextFieldSearch: FunctionComponent<Props> = props => { return ( <TextField {...props} fullWidth variant={'filled'} InputProps={{ startAdornment: ( <InputAdornment position={'start'}> <Search /> </InputAdornment> ), }} /> ) } ``` 独自のPropsを定義したい場合はこのようにする。 ```tsx type Props = TextFieldProps & { text: string } ``` `TextField`を`TextFieldSearch`に差し替えることで、関数`Main`は結果的にこのようになる。 ```tsx const Main: FunctionComponent = () => { return ( <TextFieldSearch onChange={event => setText(event.target.value)} placeholder={'検索'} value={text} /> ) } ```
2020.05.06
docker-compose.ymlの環境変数をDockerfileへ渡す
結論、環境変数を直接渡すことができない。 ARGを使用して下記のように渡す。(.envファイルで環境変数を管理しているものとする) # .env ``` DEPLOY_DIR=/var/www/sample ``` # docker-compose.yml ``` version: '3' services: samplecontainer: container_name: samplecontainer build: context: . args: DEPLOY_DIR: $DEPLOY_DIR ports: - "3000:3000" volumes: - ./:$DEPLOY_DIR ``` # Dockerfile ``` FROM centos:centos7 ... ARG DEPLOY_DIR RUN echo $DEPLOY_DIR ... ```
2020.05.04
Dockerfile作成について解説してみた
# Dockerfileの概要 Dockerfileは、Dockerイメージの作成方法・プロビジョニング方法を定義できるもの。 (アプリケーションが動作するOS環境の作りかたを定義できる) まず、Dockerの最大のメリットをおさらいすると、「本番環境へランタイムごとデリバリー可能」ということ。Githubなどで管理するソースコードに加え、動作環境丸ごと本番へ提供できるので「本番へデプロイしたらうまく動作しない!」なんてことはない。~インフラエンジニアとの戦争に終止符を打つことができる。~ # 基本的なDockerファイル作成の流れ ## ローカル開発時 大前提としては、DockerfileファイルはDocker-composeで管理します。アプリ開発時は、複数のDockerfileを用いて開発を行うのが一般的なので、Docker-compose導入を前提としてDockerfileを作成する。(例えばアプリの動作するウェブサーバーのDockerfile、データベースのホスト用のDockerfile等) 1. Dockerfileを作成 1. イメージ(OS)に対してパッケージをインストールしアプリ動作環境を作る 1. 適宜環境変数を設定し、Dev, Prdなどでアプリの動作を切り替え 1. ローカルのディレクトリをマウントさせる(同期させる) 1. マウントしたディレクトリのソースを編集しゴリゴリ開発 1. githubなどへプッシュし開発メンバーへ共有 ## 本番公開 私はECS, Fargate構成が好きなので普段こうしています。(詳しい解説は割愛します、そのうち別の記事で載せます) 1. Githubのmasterブランチへのコミットマージでリリースタグを作成 1. リリースタグ作成によりAws CodePipelineを発火 1. Code BuildでDockerをビルドしECRへプッシュ 1. Code DeployでECSへデプロイ   もちろん。EC2にDockerコンテナーをデーモン起動させれば、もちろんEC2やその他のOSで起動可能。でもLambdaなどの使い捨てOSでは不可能なので注意。 # Dockerfileのサンプルと解説 下記はよく作成するDockerfileのケース。Volumeの指定などはDocker-composeで行うので、Dockerfileでは行わない。 ``` FROM httpd RUN echo "build start!" COPY index.html /usr/local/apache2/htdocs EXPOSE 80 CMD ["apachectl", "-D", "FOREGROUND"] ``` |項目|説明| |--|--| |FROM|元になるイメージを指定します。タグ指定しない場合はlatestとなる。| |COPY|指定のディレクトリへコンテナーへマウント。[ADDよりこっちがCOPYが推奨みたい。](http://docs.docker.jp/engine/articles/dockerfile_best-practice.html)| |ADD|COPYと異なるのは、tar展開などを自動的にやってくれるところ。| |CMD|Dcoker runの時に実行。docker runの引数で上書きできる。| |ENTRYPOINT|CMDの、docker runの引数で上書きできないやつ。(--entrypointであれば上書き可能)| |ENV|OSに環境変数を与える。| |EXPOSE|コンテナが接続用にリッスンするポートを指定。(Apache等なら80とする)| |RUN|いろんなコマンドを実行できう| |WORKDIR|コンテナー内の作業ディレクトリを指定。| # まとめ Dockerって便利だけど小規模開発では大げさな気もするので、使用する際はプロジェクトの規模で検討したいところ。小規模な開発やスタートアップであれば、正直API GatewayとLambda、フロントエンドはSPAであればS3とCloudFrontなどの選択肢もある。
2020.05.04
BlitzにTailwind CSSを組み込む
[Blitz](https://github.com/blitz-js/blitz)は[Next.js](https://nextjs.org/)の上に設計されているので[Tailwind](https://tailwindcss.com/)を組み込むのも手間ではない。 必要なモジュールをインストールする。 ```bash $ yarn add postcss-preset-env tailwindcss ``` `/postcss.config.js` を追加する。PurgeCSSについては[こちら](https://tailwindcss.com/docs/controlling-file-size/#setting-up-purgecss-manually)を参考にする。 ```js module.exports = { plugins: { tailwindcss: {}, 'postcss-preset-env': {}, }, } ``` tailwind.config.jsを追加する。 ```js module.exports = { purge: ['./app/*'], theme: { extend: {}, }, variants: {}, plugins: [], } ``` `/app/styles/index.css`を追加する。 ```css @tailwind base; @tailwind components; @tailwind utilities; ``` `/app/pages/_app.tsx`ではそのファイルを読み込む。 ```tsx import 'app/styles/index.css' import { AppProps } from 'next/app' export const MyApp = ({ Component, pageProps }: AppProps) => { return <Component {...pageProps} /> } export default MyApp ``` `/app/pages/index.tsx`を書き換えて動作を確認する。 ```tsx import { FunctionComponent } from 'react' const Index: FunctionComponent = () => { return ( <div className={'grid justify-center'}> <h1 className={'text-blue-500 text-xl'}>{'Blitz.js + Tailwind CSS'}</h1> </div> ) } export default Index ```
2020.05.11
Dockerでよく使用するコマンド一覧
# コンテナー起動 ```bash docker run -d -p 8080:80 [Repository:tag] docker run -d -p 8080:80 [Repository:tag] docker run -d -p 8080:80 -v [Host_directory]:[process_directory] [Repository:tag] ``` # コンテナーのプロセスへCLIアクセス ```bash docker exec -it [container_id] bash ``` # コンテナーを一覧表示 ```bash docker ps docker ps -a ``` # コンテナー削除 ```bash docker rm [container_id] ``` # コンテナーイメージ一覧 ```bash docker images ``` # コンテナーイメージ削除 ```bash docker rmi [image_id] ``` # コンテナー起動 ```bash docker start [container_id] ``` # コンテナー停止 ```bash docker stop [container_id] ``` # リソースの使用状況を確認 ```bash docker stats ``` # Dockerの標準出力を表示 ```bash docker logs ``` # DockerのプロセスのOSへホストのファイルをコピー ``` docker cp [host_path] [container_id]:[process_directory] ```
2020.04.25
Amplifyで既存のスキーマの@keyを修正したい
まず、こんな感じでGraphQLスキーマを作成してしまった。 ``` type Product @model @searchable @key(fields: ["status", "createdAt"]) @auth( rules: [ {allow: groups, groups: ["admin"]}, {allow: groups, groups: ["customer"], operations: [read]}, ]) { id: ID! name: String! status: Status! createdAt: AWSDateTime! updatedAt: AWSDateTime! } enum Status { PUBLISH DELETED DRAFT } ``` idをハッシュキーにするべきだよね...。 ミスった。 では変更してamplify pushしてみる。 ``` type Product @model @searchable @key(fields: ["id", "status", "createdAt"]) # idを追加 @auth( rules: [ {allow: groups, groups: ["admin"]}, {allow: groups, groups: ["customer"], operations: [read]}, ]) { id: ID! name: String! status: Status! createdAt: AWSDateTime! updatedAt: AWSDateTime! } ``` エラー。 ```shell $ amplify push ... Attempting to edit the key schema of the ProductTable table in the Client stack. An error occured during the push operation: Attempting to edit the key schema of the ProductTable table in the Client stack. ``` 後からキー情報は変更できないと。 まぁ当たり前か。 まだ開発段階なので、いくらでもデータベースは変更できる、ということで下記を実施。 ``` #type Product #@model #@searchable #@key(fields: ["status", "createdAt"]) #@auth( # rules: [ # {allow: groups, groups: ["admin"]}, # {allow: groups, groups: ["customer"], operations: [read]}, # ]) #{ # id: ID! # status: Status! # createdAt: AWSDateTime! # updatedAt: AWSDateTime! #} ``` ```shell $ amplify push ... success! ``` すると、DynamoのProductテーブルが消えています。 その後、再度スキーマを定義しamplify pushします。 ``` type Product @model @searchable @key(fields: ["id" ,"status", "createdAt"]) # idを追加 @auth( rules: [ {allow: groups, groups: ["admin"]}, {allow: groups, groups: ["customer"], operations: [read]}, ]) { id: ID! name: String! status: UserStatus! createdAt: AWSDateTime! updatedAt: AWSDateTime! } ``` ``` $ amplify push ... success! ``` 本番環境で同様の状況が起こった場合はどうしようかな・・。 個人的には、新しくスキーマを作成して、そこへデータ移管するしかないかなと感じます。 ElasticSearchへインデックス貼り直しも必要なので、もっと効率的な方法あるかもしれないですね。 コメントお待ちしております。
2020.03.23
amplifyで、updateミューテーションの処理がエラーなく固まる現象への対処方法
@keyで指定しているフィールドは必ずリクエストに乗せないといけません。 update時にエラーなく一生レスポンス返ってこない。(エラーだせや) あとはキーのフィールドを更新しようとした場合も同じ現象になる様子です。 ``` # scheme.graphql type User @model @searchable @key(fields: ["id" ,"status", "createdAt"]) @auth( rules: [ {allow: groups, groups: ["admin"]}, {allow: groups, groups: ["customer"], operations: [read]}, {allow: groups, groups: ["agency"], operations: [read]}, ]) { id: ID! name: String! email: String status: UserStatus! createdAt: AWSDateTime! updatedAt: AWSDateTime! } enum UserStatus { REGISTERED EXCLUSIVE DISABLED } ``` 上記のスキーマに対して、下記の処理を行うとエラーなく停止する。 ```js # Vuejsのアクションメソッド async editUser({commit}, newUser) { commit('setIsLoading', true) const proposedUser = newUser delete proposedUser.createdAt //!!...これするとエラーなく止まる...!! delete proposedUser.updatedAt delete proposedUser.key const updatedAt = moment().format() const newState = _.assign({}, { ...proposedUser, updatedAt: moment().format(), }) await API.graphql(graphqlOperation(updateUser, {input: newState})) console.log('test') commit('editUser', _.assign({}, { ...newUser, updatedAt })) commit('setIsLoading', false) return; }, ``` # 所感 同じ値で指定しないというね。キー指定しているから仕方ないか。 エラー出して欲しいな。
2020.03.23
Amplifyで一覧表示の際にソート対応について
# 結論、ElasticSearchを使って実現します このページを見ている人は、下記のいずれかの勘違いをしていると思う。 - GraphQLクエリー発行時にorder_by的な指定が可能ではないか? - @keyでレンジキーに追加すればソート指定できるでしょ? 後者の方は、後述のGSIとLSIについてまず理解して欲しい。 ## グローバルセカンダリインデックス(GSI)についておさらい あるテーブルをベースに、異なるパーティションキー・ソートキーのテーブルを作成する仕組みを。 ## ローカルセカンダリインデックス(LSI)についておさらい あるテーブルをベースに、パーティションキーはそのままに、異なるソートキーのテーブルを作成する仕組み。 # 実現する方法は2パターン 下記のいずれかで実現可能かと思います。 - ElacticSearchで解決(私はこれを採用しています) - LSIを作成し異なるキー情報でソートかけたテーブルをもう一つ作成 LGIの思想でテーブル情報が増えると何がなんだかよく分からなくなってくる可能性があるし、ElasticSearchあるんだから使えばいいんじゃん。というのが感想です。(あと誰かが、Dynamo設計のベストプラクティスは「テーブルを個数を極小にすること」と言っていたのを覚えている) ではソースコードをペタする。 **schema.graphql** ```js type Product @model @searchable { id: ID! name: String! createdAt: AWSDateTime! updatedAt: AWSDateTime! } ``` **queries.js** ```js export const searchProduct = /* GraphQL */ ` query SearchClients( $filter: SearchableProductFilterInput $sort: SearchableProductSortInput $limit: Int $nextToken: String ) { searchProduct( filter: $filter sort: $sort limit: $limit nextToken: $nextToken ) { items { id name createdAt updatedAt } nextToken total } } `; ``` **sample.js** ```js import {API, graphqlOperation} from 'aws-amplify' import {searchProduct} from '~/src/graphql/queries' const queryArgument = Object.assign({},{ sort : { // Defaultはパーティションキー順になってしまうので対策 field: 'createdAt', direction: 'desc' }, limit : 30 }) const res = await API.graphql(graphqlOperation(searchProduct, queryArgument)) console.log(res) // res.data.searchProduct.itemsが作成日順に!🎉🎉 ``` 奥深き、Amplify。
2020.03.23
Amplifyでローカル環境に現在と異なる環境をプルしたい
いわゆる`git fetch`的なことをやりたいと。 この手のことをやろうと思ったら、まずはAmplifyコンソールを見るのがベスト。 Amplifyコンソール→環境一覧の画面よりコマンドをコピペする。 ![](https://article-images.shimba.io/01E22NA18VSKRRHMBQM1NFZXFB.png =1920x1090) ```shell $ amplify pull --appId [app_id] --envName prd ``` あとはローカルのCLIで実行するだけ。
2020.03.23
Cognito User Pool の外部IDP経由のサインインへAuth0を設定してみた
# なぜAuth0? CognitoへTwitterログインを実装したかったからです。Cognito User Poolのフェデレーションサインインは、独自IDPを追加する場合、OpenIDConnect, SAMLしか対応できない。TwitterはOAuth2.0だから非対応だ。開発中のアプリ要件にTwitterログインがあるのでどうしても避けることができない。 ![](https://article-images.shimba.io/01E2M97XK9RDZT4HV75YR60CQE.png =1246x414) 一応補足すると、Identity Pool使用すればTwitterのクライアントID・シークレットIDでSTSトークンを受け取ることが可能。 がしかし、今回下記のようにCognitoUserPoolトークンによる認証トークン取得フローが必要だ。 ![](https://article-images.shimba.io/01E2M9D0QPQ88GVFCB14TDA8JM.png =910x640) # では設定してみる ## Auth0の設定画面よりアプリケーションを作成 ### CREATE APPLICATIONをクリックし任意の名称でアプリケーションを作成 ![](https://article-images.shimba.io/01E2M93PZ59RABWRXMKDX504XY.png =1076x676) ### SPA(Single PageWeb Application)を選択 ![](https://article-images.shimba.io/01E2M9H89VHD2EET88ADAX410J.png =796x659) ### 「Addons」より「SAML2 WEB APP2」をクリック ![](https://article-images.shimba.io/01E2M9MS65HS5940KCC3V91DSC.png =1065x641) ### 「Application Callback URL 」へCognitoのコールバックURLを入力 ![](https://article-images.shimba.io/01E2M9V94P4JJH0MCDRMRYNNJ2.png =639x836) ### Settingsのコードを書きのように書き換える logout.callbackのURLはアプリケーションのURLを指定します。 ```json { "audience": "urn:amazon:cognito:sp:ap-northeast-1UserPoolID", "nameIdentifierFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent", "logout": { "callback": "http://localhost:3000", "slo_enabled": true }, "mappings": { "user_id": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", "email": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", "name": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", "given_name": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname", "family_name": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname", "upn": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn", "groups": "http://schemas.xmlsoap.org/claims/Group" } } ``` 疑問なのは、Cognitoへコールバックしたら、そこでアプリケーションのコールバックURLに帰ってきてくれる予想でしたが、自分はうまくいきませんでした...。これだとローカル開発時にいちいちlogout.callbackを変更しないといけないし、開発効率が下がりそうな気が。コールバック単位でAuth0にアプリケーションを作成するのも面倒くさい。 ### 「Usage 」よりCognitoのSAMLプロバイダーにて使用するキー情報をダウンロード ![](https://article-images.shimba.io/01E2M9Z7F9S805YAH0RY7SAHBJ.png =640x840) ## Cognitoユーザープールへ設定 ### SAML IDPの追加 先ほどダウンロードしたキー情報をアップロードし、プロバイダーの作成をクリックし作成を行います。ここでエラー出る場合はキー情報をもう一度ダウンロードして試してみてください。 ![](https://article-images.shimba.io/01E2MA6JWWT3ERJPH7YVJ5394E.png =1258x876) ここで注意なのは、Facebookでもなんでもいいのですが、外部認証プロバイダーを一つ以上登録しておかないと、HostedUI(Cognitoで標準で用意されているログイン画面)を使用できないので、つまりドメイン及びアプリクライアントを作成できない。 外部プロバイダーによるサインインする際は、使用しなくて一つ以上適当なアクセスキー・シークレットキーで登録しておこう。(例えば access_key = XXXXXX, secret_key = XXXXXX) ### アプリクライアントにてAuth0認証有効 Auth0を追加し、変更を保存。 ![](https://article-images.shimba.io/01E2MAJZ7S57G5MQSAKMVVCEFE.png =1669x718) ## JSコード 私はAmplifyユーザーなので、Amplifyのコードを載せておく。 ```js import {Auth, API, graphqlOperation} from 'aws-amplify'; await Auth.federatedSignIn({provider: 'Auth0'}) ``` これであとは実行すればAuth0の画面へリダイレクトされる。 続いて、最後にログアウトの実装を見る。これをやらないとAuth0からログアウトしたことにならず、同一ブラウザでキャッシュクリアしないとAuth0認証画面が表示されない。(自動的に前回のログイン情報で認証処理を行ってしまう) ```js await Auth.signOut() ``` 手順の最初の方でユーザープールへのSAMLプロバイダー追加の際にチェックした「Idpサインアウトフローの有効」にチェックを入れると自動的にAuth0からもSignOut処理をしてくれる。 逆にチェックを入れないとローカルストレージの破棄しかしてくれない。 # 所感 今のどきのアプリであれば、SNSログインなどは当たり前だと感じる。 OAuth2.0しか対応しないIDPへのログイン要件がある場合は、Auth0の導入を検討した方が無難だろう。 実際問題、Auth0を採用せずTwitterログインを実現しようとすると、IdentityProviderに直接認証する感じになるので、ユーザープールのグループを判別した権限管理などが行えない。(厳格にはできるけど独自設定しないといけないのでAmplifyなどで自動化して構築している場合はほぼほぼ詰む)
2020.03.23
Amplifyで@searchableの検索結果の総件数取得したい
# Amplify2.2.5の仕様のままでは不可能 厳格に言うと、AppSyncの中のマッピングテンプレートを変更すれば対応は可能な様子。 ただデプロイする度に消えるから推奨しない、開発効率最悪だよね。(build/配下に展開されデプロイされるので) とりあえずこのパターン紹介しておきます。 ## マッピングテンプレート修正箇所について **Queryリゾルバー、レスポンスマッピングテンプレートの該当箇所** ``` #set( $es_items = [] ) #foreach( $entry in $context.result.hits.hits ) #if( !$foreach.hasNext ) #set( $nextToken = $entry.sort.get(0) ) #end $util.qr($es_items.add($entry.get("_source"))) #end ## [Start] Determine request authentication mode ** #if( $util.isNullOrEmpty($authMode) && !$util.isNull($ctx.identity) && !$util.isNull($ctx.identity.sub) && !$util.isNull($ctx.identity.issuer) && !$util.isNull($ctx.identity.username) && !$util.isNull($ctx.identity.claims) && !$util.isNull($ctx.identity.sourceIp) && !$util.isNull($ctx.identity.defaultAuthStrategy) ) #set( $authMode = "userPools" ) #end ## [End] Determine request authentication mode ** ## [Start] Check authMode and execute owner/group checks ** #if( $authMode == "userPools" ) ## [Start] Static Group Authorization Checks ** #set($isStaticGroupAuthorized = $util.defaultIfNull( $isStaticGroupAuthorized, false)) ## Authorization rule: { allow: groups, groups: ["admin"], groupClaim: "cognito:groups" } ** #set( $userGroups = $util.defaultIfNull($ctx.identity.claims.get("cognito:groups"), []) ) #set( $allowedGroups = ["admin"] ) #foreach( $userGroup in $userGroups ) #if( $allowedGroups.contains($userGroup) ) #set( $isStaticGroupAuthorized = true ) #break #end #end ## Authorization rule: { allow: groups, groups: ["customer"], groupClaim: "cognito:groups" } ** #set( $userGroups = $util.defaultIfNull($ctx.identity.claims.get("cognito:groups"), []) ) #set( $allowedGroups = ["customer"] ) #foreach( $userGroup in $userGroups ) #if( $allowedGroups.contains($userGroup) ) #set( $isStaticGroupAuthorized = true ) #break #end #end ## Authorization rule: { allow: groups, groups: ["agency"], groupClaim: "cognito:groups" } ** #set( $userGroups = $util.defaultIfNull($ctx.identity.claims.get("cognito:groups"), []) ) #set( $allowedGroups = ["agency"] ) #foreach( $userGroup in $userGroups ) #if( $allowedGroups.contains($userGroup) ) #set( $isStaticGroupAuthorized = true ) #break #end #end ## [End] Static Group Authorization Checks ** ## [Start] If not static group authorized, filter items ** #if( !$isStaticGroupAuthorized ) #set( $items = [] ) #foreach( $item in $es_items ) ## No Dynamic Group Authorization Rules ** ## No Owner Authorization Rules ** #if( ($isLocalDynamicGroupAuthorized == true || $isLocalOwnerAuthorized == true) ) $util.qr($items.add($item)) #end #end #set( $es_items = $items ) #end ## [End] If not static group authorized, filter items ** #end ## [End] Check authMode and execute owner/group checks ** #set( $es_response = { "items": $es_items } ) #if( $es_items.size() > 0 ) $util.qr($es_response.put("nextToken", $nextToken)) $util.qr($es_response.put("total", $es_items.size())) #end $util.toJson($es_response) ``` こんな具合にすれば検索結果総数の取得は可能。 ``` #if( $es_items.size() > 0 ) ... $util.qr($es_response.put("searchTotal", $context.result.hits.total)) ← 追加 #end $util.toJson($es_response) ``` $context.result.hits.totalでなぜ取得できるのか。 ElasticSearchにリクエストした際に取得できるレスポンス内容をみて欲しい。 ```shell $ curl -XGET https://search-amplify-elasti-xxxx-xxxxxx.ap-northeast-1.es.amazonaws.com/client/_search ``` ```json { "took": 9, "timed_out": false, "_shards": { "total": 5, "successful": 5, "skipped": 0, "failed": 0 }, "hits": { "total": 11, "max_score": 1.0, "hits": [ { ``` hits以下にtotalがいるよね。これが総件数です。 (ただし総件数は10,000が最大値となるっぽいので、検索結果10,000以上になると10,000で返ってきます) なぜAmplifyが対応しないのかは、このIssueを確認してみて欲しい。 近い将来対応される様子なので期待大。 本件に関連するIssue >> https://github.com/aws-amplify/amplify-cli/issues/1865 ## じゃあどのように対応するべきなのか いずれかのように対応するが良いと思われます。 - amplify add functionしてElasticsearchへリクエストする関数を作成し呼び出す(ESのエンドポイント情報は環境変数で渡すのがベストかな) - nextTokenがnullになるまで繰り返し検索しJSで集計(limitを1000にすればまぁリリース初期では2回から3回の呼び出しで済むと思う) # 所感 今後に期待。GraphQLのQuery結果のtotalと並んでsearchTotal的なのも入ってきそう。
2020.03.23
Elastic Searchの指定インデックスを全て削除
Amplifyで実装を進めていると、開発中にDynamoDBから直接データ削除したくなる。 ただ削除すると、ElasticSearchにインデックスが残るので、DynamoDBから削除時は下記コマンドにてCLIより削除しておく。 ```shell $ curl -XDELETE https://search-amplify-elasti-xxxx-xxxx.ap-northeast-1.es.amazonaws.com/client ``` エンドポイントの見方 ![](https://article-images.shimba.io/01E1Y4XBVTHM7AVPBP39AK2R48.png =1920x1101)
2020.03.23
aws-amplifyのAdminQueriesについて
# 前提のお話 元の情報は[こちら](https://aws.amazon.com/jp/blogs/mobile/amplify-cli-enables-creating-amazon-cognito-user-pool-groups-configuring-fine-grained-permissions-on-groups-and-adding-user-management-capabilities-to-applications/)より # 最初に基本的な使い方 ```js const params = { headers: { "Content-Type": "application/json", Authorization: await Auth.currentSession().then(session => session.getSession().getJwtToken()) } } API.get("AdminQueries", "/listUsers", params) ``` で`https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/listUsers`という感じのAPIを叩きに行ってくれるのでCognitoに登録されているユーザー一覧を取得できる # limitの指定 このAPIの場合はデフォのlimitは25人でパラメータに ```js const params = { queryStringParameters: { limit: 10 }, headers: {〜} } ``` とかってしてやれば10人取れる この情報は`amplify/backend/functions/AdminQueries1234abcd/src/app.js`の`/listUsers`に書いてある # ページネーション ```js const {NextToken, Users} = await API.get(〜) ``` でgraphqlのAPIみたいに情報が取れてパラメータに ```js const params = { queryStringParameters: { token: NextToken limit: 10 }, headers: {〜} } ``` で2ページ目のユーザー情報が取得できる # postの送信の仕方 ```js const params = { body: { username: email }, headers: {〜} } API.post("AdminQueries", "/disableUser", params) ``` という具合でqueryStringParametersとは違いこっちはbodyを使う expressとかに慣れてれば意味は分かるはず… # その他 - 内部的にはaws-sdkを使って[こちら](https://github.com/aws-amplify/amplify-js/issues/1213#issuecomment-484743333)と同じ事をしている - **headersが無いとcorsエラー吐くので注意🤮** ## 次回予告 自動生成されるAPIの使い方について書くよ
2020.03.23
NuxtjsとPM2で、Light Sailに2つNutxjsのランタイムを起動させてみた
# 背景 お客さんの予算の関係で、LightSailへ2つのNuxtjsランタイムを起動させることに。まぁ安く済ませたいからだろうと思う。 # LightSailを起動 SSRが要件だったので、一番安いプランだとCPUとメモリが圧迫されるかも・・・ とりあえず月々20$のプランで設定することに。![](https://article-images.shimba.io/01E365V8K7GNG4TJSHQ7ZVP6N9.png =1920x634) OSはもちろんNodejsがデフォルトで入っているOSを採用。 ![](https://article-images.shimba.io/01E36605Q7YE6J38N4J8WNZNYQ.png =1702x1568) そしてSSH接続する。管理コンソールから直接ブラウザ画面でSSH接続できるから楽チンだよね。接続タブより、「SSHを使用して接続」をクリックするとCLIが起動する。 ![](https://article-images.shimba.io/01E3663QVVBKNHVXFFCAB1WM2R.png =1920x560) ![](https://article-images.shimba.io/01E3663901AEECG1VMJ9G20JBY.png =1920x1073) ログインしたら、CLI上で以降の手順で設定していく。 ## Nutxjsのセットアップ nuxt2の方は、`yarn start`時に、ポートを3333にしておいてください。後ほどApacheから参照するようにします。 ```bash $ cd ~/ $ npm install -h yarn $ git clone [https_url of nuxt1] $ git clone [https_url of nuxt2] $ cd nuxt1 && yarn && yarn build && cd .. $ cd nuxt2 && yarn && yarn build ``` ## PM2のセットアップ ```bash $ cd ~/ $ npm install pm2 -g $ vi pm2.config.js module.exports = { apps : [ { name: "nuxt1", script: "yarn start", cwd : '/home/bitnami/nuxt1' }, { name: "nuxt2", script: "yarn start", cwd : '/home/bitnami/nuxt2' } ] } $ pm2 start pm2.config.js ┌─────┬─────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐ │ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │ ├─────┼─────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤ │ 0 │ nuxt1 │ default │ N/A │ fork │ 20691 │ 0s │ 0 │ online │ 0% │ 26.3mb │ bitnami │ disabled │ └─────┴─────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘ │ 1 │ nuxt2 │ default │ N/A │ fork │ 20691 │ 0s │ 0 │ online │ 0% │ 26.3mb │ bitnami │ disabled │ └─────┴─────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘ $ sudo pm2 startup $ sudo pm2 save ``` ## Apacheの設定 ServerNameでPM2で起動させたそれぞれへProxyするようにします。 ```bash $ cat /opt/bitnami/apache2/conf/httpd.conf ... <VirtualHost *:80> ServerName nuxt1.example.com # ErrorLog /home/example/logs/error_log # CustomLog /home/example/logs/access_log common # <Proxy *> # Require all denied # Require ip xxx.xxx.xxx.xxx # </Proxy> # DocumentRoot /home/example/public_html ProxyPass /.well-known/ ! ProxyPass / http://localhost:3000/ ProxyPassReverse / http://localhost:3000/ </VirtualHost> <VirtualHost *:80> ServerName nuxt2.example.com # ErrorLog /home/example/logs/error_log # CustomLog /home/example/logs/access_log common # <Proxy *> # Require all denied # Require ip xxx.xxx.xxx.xxx # </Proxy> # DocumentRoot /home/example/public_html ProxyPass /.well-known/ ! ProxyPass / http://localhost:3333/ ProxyPassReverse / http://localhost:3333/ </VirtualHost> $ apachectl restart ``` ## HTTPで接続可能だけど... SSL化したいよね。というときは、間にCloudFrontをかませて下さい。CloudFront→LightSailで飛ばせば、SSL接続することが可能です。 ## 所感 実はLightSailの一番低いスペックで起動させたんだけど、CPU利用率とメモリが100%まで達してサーバがダウンしてしまいました。もし物理設計したくない要件であれば、サーバーレスがいいよね。 でも、サーバーレスで例えばLambdaでNuxtをSSRしようとすると、Lambda→API Gatewayで返却できるBodyサイズに制限があったり、そもそもLambdaに対してZIPをアップできないとかあったりする。UIフレームワークやAmplifyなどを採用したNuxtjsアプリだとLambdaでSSRは現実的ではない。そういうときは、本当はFargateとECSのコンビがいいのかなと。
2020.03.12
Nuxt.jsで作ったWebアプリがIE,Edgeで表示されなかった
# 起きたこと とある案件で、Nuxt.jsを使ったWebアプリを開発していたところ、 Google Chrome、Firefox、Safariでは表示できたのに、 Internet Explorer11や MS Edgeではうまく表示されない事態になりました。 Nuxtのデフォルトローディングアイコンが赤くなってぐるぐると回るのです。 ![](https://article-images.shimba.io/01E2QMHFRDDZNNG9J2PJ40K37X.png =398x237) MacからVirtualBoxを使って見ていたのでそのせいかな?と思ったのですがちゃんと実機で見ても見れない・・ IE上では全くよくわからない位置にエラーが・・ なるほど、わからん。さすがIE先生は他のブラウザとやることが違うぜ。 ## 環境 - Nuxt.js v2.11.0 - SPAモードで開発(SSRでも起きていたのであまり関係なさそう) ## まずPolyfillで試みた [参考: Nuxt.jsでIE11対応のPolyfill]( https://www.hypertextcandy.com/nuxt-ie11-polyfill) Polyfillとは要するに「ブラウザやバージョン違いで非対応になっちゃってるやつをなんとか解決する」手法。すごくなんとかしてくれそう。 しかし上記を参考に必要そうなものを足してみるも、なんだかエラーが出てしまいました ``` Module build failed (from ./node_modules/babel-loader/lib/index.js Type Error: ~~~中略~~~ this.setDynamic is not a function ... ``` `this.setDynamic`てなんだ、と調べてみると、同様に困っている海外の方がちらほら。 おっ!と思い真似してみるも解決せず、なんか茨の道を行きそうだと思いPolyfillでやるのを断念 ## transpileで直った こういう時は「エラーをちゃんとよく読め」と先人たちがよく言っているので見てみたところ、 エラーだよ!!!と怒られている位置が`ky`というプラグインの辺りだと判明。 kyってなんだ・・?と調べてみると、[こんな記事](https://qiita.com/il-m-yamagishi/items/a823c1afbdf253e84a34)が出てきました。 要約すると HTTPリクエストを使う時、Nuxt.jsはずっとaxiosの使用を推奨していた →いろいろ不都合が出てきたので、独自の[@nuxt/http](https://github.com/nuxt/http)というモジュールを作り始めた →このモジュールは[sindresorhus/ky](https://github.com/sindresorhus/ky)ライブラリというのをベースにしている ...とのことです。僕自身Nuxt.jsをちゃんとやり始めてまだ半年ほどなので何がなんやら。 新しい友達の親が実は離婚しててお父さんは二人目なんだって言われてへぇ〜って感じですが、この例えはやめましょうね。 そしてkyのREADMEをつらつら読んでいくと、原因らしき部分を発見 ![](https://article-images.shimba.io/01E2YDXTYWC7RCRENR05QEZ7J8.png =828x226) 対応ブラウザが最新(?)のChrome、Firefox、Safariのみ... **IEとedgeがない!!** 気になってさらに「ky nuxt.js」で検索していくと、なんと**Nuxt.jsの公式ドキュメント**へとたどり着きました。 [Nuxtjs.org#transpile](https://ja.nuxtjs.org/api/configuration-build/#transpile) 引用: `特定の依存関係を Babel で変換したい場合、build.transpile を追加することができます。transpile の項目は、マッチする依存ファイル名の文字列または正規表現オブジェクトになります。` ``` module.exports = { build: { transpile: [ ({ isLegacy }) => isLegacy && 'ky' ] } } ``` **めっちゃサンプルコードに使われてるじゃないですか!** このコードを`nuxt.config.js`に追加すると、kyライブラリの依存関係をうまいこと解消してくれるらしい。 「いや、そんなまさか」とおもいつつ追加すると......**直りました!** ## Nuxt.jsでkyトラブルはあるあるネタ? サンプルコードに使われるくらいなので、頻発する問題なのかなと思いました。 その割に文献があまりなかったのは「あんまり起きないレアなトラブル」なのか「備忘録を書くほどでもない余裕なトラブル」なのか...相変わらず初心者には厳しい世界です。 まとめると - **IE、EdgeでNuxt.jsが動かない時は、エラーが出てそうなとこのプラグインをtranspileに入れる** - やっぱりエラーをよく読むのは大事 - やっぱり情報にアンテナ張っておくのも大事 - やっぱりIEは(略
2020.03.09
TwitterとCognitoのフェデレーテッド ID プロバイダ経由でのログイン
# 結論 - Twitterが提供する認証に関する機能はOAuth2.0しか存在しない - OpenIDConnect規格は対応しない(その他SAMLなども非対応) - つまり**認証ではなく認可のみ提供**しているのでCognitoユーザープールへ接続不可(厳密いうとTwitter独自で認証的な機能を提供している様子) # でもどうしても実現したい!! ## 解決策1 Identity Pool使用 Identity PoolへTwitterを追加して、ユーザープールを介さない認証を行えばいけそうです。 しかしこの場合、IAMロールが曲者で、Cogniroのユーザーグループなどを識別し自動的に付与している場合はそのロールをTwitter認証時に手動で当てないといけない。(Amplifyなどでユーザープールを介して認証をしている場合はほぼ詰んでいる) ![](https://article-images.shimba.io/01E2JEN6JREKZ0R371H8H68YQR.png =1915x927) ## 解決策2 Auth0使用 Auth0→Cognitoで認証しトークン発行させれば、ユーザーグループを介して認証を行うことが可能なのでユーザーグループなどの情報を用いることができる。アプリケーションで独自にロールを自動的に生成している場合に有効だ。 しかし、Auth0の欠点でもあるがAuth0へ一度遷移してしまうので、UXを見直さないといけない。SIgnUp、SIgnInボタンをクリックした際はAuth0へ飛ばす的な施策を講じないといけない。 [Auth0 Official](https://auth0.com/jp/)
2020.03.04
Amplifyのsearchableを指定したモデルでGraphQLでSQLのIN句みたいに使う方法
# 最初に これが正規の方法という訳ではなく私が試して上手くいったやり方なだけで、本来のやり方知ってる方いたら教えて欲しいです。 # やりたかった事 SQLの場合ならIN句を使う時は ```sql SELECT id,name FROM users where id IN(1,2,3) ``` 見慣れた光景ですね。 # GraphQLで同等の事をする方法 ```graph query { searchUsers(filter: { or: [{ id: { eq: 1 } }, { id: { eq: 2 } }, { id: { eq: 3 } }] }) { id name } } ``` filterのorで配列を入れてそれぞれをeqで指定する この方法は ```sql SELECT id,name FROM users where id = 1 or id = 2 or id = 3 ``` とやってるのと同じなのでカッコ悪い気がする 他に方法をご存知の方がいればコメントとかで教えていただけると超ありがたいです
2020.03.02
nuxt.jsでのIE表示崩れ対策
AIRIです♪ フロントエンドのコーティングにおいて、各ブラウザでの表示崩れへの対応で、特に IEの表示崩れに悩まされる人はかなり多いのではないかと思います。 旧バージョンのサポートが終了しているとはいえ、現状windows 10に標準搭載されているIE 11に関しては、まだまだ数年はサポートは続くので、しばらくは IEを無視することは出来ない状態ですね。 今回、私が実際行ったIEの表示崩れ対策を詳細していきたいと思います。 # 使用環境 mac OX Mojave 10.14.6 Nuxt.js 2.10.0 以下の環境を使用して、ローカルサーバーにてIE画面を確認しています。 virtual Box 6.1 Internet Explorer 11 ※google chromeやsafariなどの検証ツールを使ってでのIE表示だと普通に表示されてしまう部分があるので、同環境か、実際のwindowsを使用してでの確認をお勧めします。 # エラー内容と対策 ### ### ## 変数が適用されない cssのカスタムプロパティを使用してvarで変数を使おうとすると、適用されません。 <IE検証画面> ![](https://article-images.shimba.io/01E25836K6XDR43QTQGGQRBRC1.png =349x118) ![](https://article-images.shimba.io/01E2583G4EYS2D0NPHM3CJQ8D4.png =283x99) :rootで変数を設定しても、適用されません。 ### 対策 プラグイン、postcss-custom-properties を使用して、静的cssに変換する。 1. postcss-custom-properties をインストールする。 ### 1.全ての変数を、一つのcssファイルにまとめる。 ### #### customProperty.css ```css :root { --sample-color: #fff; } ``` 1. nuxt.config.jsに、 ```js postcss: { plugins: [ require('postcss-custom-properties')({ preserve: false, importFrom: ['client/assets/styles/customProperty.css'] }), ] }, // preserve: false → コンパイル後にカスタムプロパティを残すかどうかの設定 // importFrom: ['~.css'] → 変数をまとめたcssのパス ``` ### こうすると、以下のようにコンパイルされます。 ```css :root { --sample-color: #fff; } .div { back-ground: var(--sample-color); } /*コンパイル後*/ .div { back-ground: #fff; } ``` ### ### ## class名が適用されない HTML 4.01の定義に沿ってclass名が設定されていないと、class名が適用されません。 ```css /* アンダーバー始まり…OK */ .__classname { } /* ハイフン始まり…NG */ .--classname { } /* ハイフン + 数字始まり…NG */ .1st { } /* ハイフン始まり…NG */ .-1st { } ``` <参照記事> https://gist.github.com/think49/d8fbcf7f1c743e21bc79bb7d16178cad 特に、--close、--active など、" -- "始まりのclass名は使うことがあるかと思うので、注意が必要です。 ### 対策 規定に沿って書き直します。 :例) ```css .toggle--close { display: none; } ``` ### ### ## flexと、position併用で要素幅が何故か残る positionも結構バグがありますね…。 色々バグがある中で、ネット検索してもあまり記事が出てこない、"flexと position併用で、要素幅が何故か残る "と言う現象が起きました。 #### やりたいこと ```html <ul class="circle-list"> <li>○</li> <li>○</li> <li>○</li> <li>○</li> <li>○</li> <li>○</li> <li>○</li> </ul> ``` ```css .circle-list { display: flex; position: relative; } /* 擬似要素で、○の後ろのボーダーを作成 */ .circle-list :before { content: ''; height: 1px; width: 100%; background-color: var(--border-color); position: absolute; top: 20px; left: 0; } ``` #### google chrome ### ![](https://article-images.shimba.io/01E25AH4G13A4NRYB7H9Z2KG2D.png =1061x122) #### Internet Explorer ### ![](https://article-images.shimba.io/01E25B9YRGB34WVQF8XBFCPVZB.png =1060x134) 何故か要素が完全に浮かない。(右端に幅が残る) ちなみに、擬似要素を消すと、ちゃんと端っこまで、コンテナが伸びる…謎。 ![](https://article-images.shimba.io/01E25BS54V8K7EWVRMCE2WV9TS.png =1057x130) ### 対策 #### 試したこと - 擬似要素:before、:after両方で検証 - 親要素、子要素、どちらもpaddingやmarginを0で設定 - 全体をdivでラップし、一番外のdivにposition: relativeを設定 ダメでした…。 ### 対策 flexに影響のない場所に、positionを設定する要素を設置する。 ```html <div class="wrap"> <span class="border"></span> <ul class="circle-list"> <li>○</li> <li>○</li> <li>○</li> <li>○</li> <li>○</li> <li>○</li> <li>○</li> </ul> </div> ``` ```css .wrapp { position: relative; } /* 空のspanタグでボーダーを作る */ .border { content: ''; height: 1px; width: 100%; background-color: var(--border-color); position: absolute; top: 20px; left: 0; } .circle-list { display: flex; } ``` すると、ちゃんとこうなる。 ![](https://article-images.shimba.io/01E25CVWHQY8Z9J41C1R4V9288.png =1061x122)
2020.02.28
amplify mockは今の所(2020/02/26時点)@searchableに対応してないらしい
https://aws-amplify.github.io/docs/cli-toolchain/usage#api-mocking-setup > Note that @searchable is not supported at this time. https://github.com/aws-amplify/amplify-cli/issues/2524 > @searchableと@predictionだけ対応していません。 またこんなissueが立ってました https://github.com/aws-amplify/amplify-cli/issues/2524 > Once this gets prioritized by the product manager we will work on this なかなか残念な事になってるっぽいのでelasticsearch使いたい場合は大人しくawsにamplify pushしてテスト環境的なので使うしか無さそうです( ´△`)
2020.02.26
Amplify × ElasticSearchでページネーション途中、リンク直打ちの際のコレクション取得方法
# nextTokenでしかページネーションできません 結論、nextTokenを使って指定のページネーション位置までぶん回します。 例えば下記のようなGraphQLスキーマが存在するとします。 ```graphql type Product @model @searchable @auth( rules: [ {allow: groups, groups: ["admin"]}, {allow: groups, groups: ["customer"], operations: [read]} ]) { id: ID! name: String! price: Int! status: ProductStatus! createdAt: AWSDateTime! updatedAt: AWSDateTime! } enum ProductStatus { OPEN SOLT_OUT } ``` 型定義:https://docs.aws.amazon.com/ja_jp/appsync/latest/devguide/scalars.html `@searchable`が入っていると、`amplify push`時に、ElasticSearchが生成される。 上記のスキーマによって生成されたGraphqlコードを見ていこう。 ```js export const searchProduct = /* GraphQL */ ` query SearchClients( $filter: SearchableProductFilterInput $sort: SearchableProductSortInput $limit: Int $nextToken: String ) { searchProduct( filter: $filter sort: $sort limit: $limit nextToken: $nextToken ) { items { id name price status createdAt updatedAt } nextToken total } } `; ``` 実際に呼び出すとこんな感じ。 ```js import {API, graphqlOperation} from 'aws-amplify' import {searchProduct} from '~/src/graphql/queries' const queryArgument = Object.assign({},{ sort : { // Defaultはパーティションキー順になってしまうので対策 field: 'createdAt', direction: 'desc' }, limit : 30 }) const res = await API.graphql(graphqlOperation(searchProduct, queryArgument)) console.log(res) // data.searchProduct.items 取得したコレクション // data.searchProduct.nextToken ページネーションのトークン ``` そして次回のリクエストで`nextToken`を設定すれば、前回の続きからコレクション情報を取得できる。 ただし、次のコレクション情報が存在しなければ、nullで返ってくるので注意が必要だ。 ここで引っかかるのが、下記のようなケースである。 - 知り合いに検索結果の3ページのURLを共有した - 知り合いは共有されたURLへアクセス そう、ElasticSearchにおけるページネーションは`nextToken`を用いて行われるので、**nextTokenを保有していない状態で途中の番号からコレクションを取得できない**のだ。(もしかしたら最善の方法あるかもですが検索しても情報なかったです) 私は下記のような対応を行いました。 ```js // forcePagination.js import _ from "lodash"; import {API, graphqlOperation} from "aws-amplify"; /** * Amplifyでpage=1以上のリンク直打ちの際に利用するページネーション処理 * * @description 現在のページ位置まで強制的に移動し配列を取得 * @param page 現在のページネーション番号 * @param graphqlQuery グラフQLのクエリー文字列(src/graphql/queries.js) * @param queryArgument クエリーへ適用したい引数 * @param dataIndex データ取得後のdataオブジェクトの直下のインデックス名 * @returns {nextToken,items} カレントのnextTokenと表示する配列 */ export default async function (page, graphqlQuery, queryArgument, dataIndex) { let nextToken = undefined let loopCount = page - 1 let items = [] while (1) { if (loopCount < 0) break; const currentQuery = _.assign(queryArgument, {nextToken}) const res = await API.graphql(graphqlOperation(graphqlQuery, currentQuery)) nextToken = res.data[dataIndex].nextToken items = res.data[dataIndex].items loopCount = loopCount - 1 } return _.assign({}, { nextToken, items }); } ``` お役立ていただければ幸いです。 # 所感 CloudSearch時代にも同じような対応をした経験があるので、上記のような対応を施しました。 もっといい方法あればコメント願います🙇‍♂️
2020.02.26
AmplifyでnextTokenを使用すると Invalid pagination token given.エラーでる件について
# 現象 AmplifyでnextTokenを使用してリクエストしようとすると、ブラウザのコンソールにて下記のようなエラーが発生する。ページネーションができない! ![](https://article-images.shimba.io/01E1V7J3BX8WBF84QAXG731KT4.png =1120x488) ```shell com.amazonaws.deepdish.common.pagination.InvalidPaginationTokenException: Invalid pagination token given. ``` # 原因 Amplifyのバージョンに依存したバグらしい。amplify 4.13.4以上にするとうまくいくみたいだぞ。私は、amplify 4.13.3を使用していました。 https://github.com/aws-amplify/amplify-cli/issues/3411 アップグレードしないと要するにこういうことが一切できないみたいです。 ```js // Amplify4.14.4未満でエラー発生 const res = await API.graphql(graphqlOperation(listUser, {nextToken})) ``` # 手順 ``` $ npm install -g @aws-amplify/cli ``` をして、schema.graphqlファイルに適当に改行を入れて、プッシュ。 ``` $ amplify push ``` # 所感 まぁ、開発が盛んだから先々に期待しよう。 開発リクエスト出すとすぐに修正されるのは魅力的だろう。
2020.02.24
Amplifyで不要な環境を削除
Amplfiy env add コマンドで環境を作成時、間違えてenvという環境を作成してしまった。 ![](https://article-images.shimba.io/01E1NF2CGMDHGK2T5TM1AFKT46.png =613x219) 何やってんねん。睡眠不足が顕著。 ということで、不要な環境を削除していく。 AwsのAmplifyコンソールより削除コマンドを入手する。(最終的にはCLIで実行する感じ) まずは、下記のような画面から対象のプロジェクトをクリックしよう。 (過去の画像方適当に引っ張ってしまった...) ![](https://article-images.shimba.io/01E1NF4EJR629MDZ1Z5BGEKBDW.png =1919x965) そして、表示された各環境のアクションより、削除を選択し実行すると画面上に削除コマンドが表示される。 ![](https://article-images.shimba.io/01E1NF7M3M24VZTD9JQMHX9WAV.png =1916x965) するとこんなんが表示されるので、CLIより実行する。 ![](https://article-images.shimba.io/01E1NF8VXFMRD8W4HQ536PDHED.png =1375x734) ```shell $ amplify env checkout <any-other-env> $ amplify env remove env # 末尾のenvが削除する対象の環境 ``` おっとエラーが出る。 ```shell No environment found with the corresponding name provided ``` 対応する環境が存在いない?ローカルにプルする必要があるのかな。 試してみる。 ```shell $ amplify env add env $ amplify env list | Environments | | ------------ | | tst | | *env | $ amplify env checkout tst | Environments | | ------------ | | *tst | | env | $ amplify env remove env $ amplify env list | Environments | | ------------ | | *tst | ``` Amplifyコンソールを見たら見事消えていました! おめでとう! Amplify最高!
2020.02.22
Amplify pullした時に表示される環境を削除
Amplifyでいくつかプロジェクト運用すると、不要なAmplify環境ができてしまうケースがある。 例えば勉強用に作成したAmplify環境や、間違えて環境追加してしまったケース。 今回私はをAmplify initで、プロジェクト名をアッパーキャメルと普通のキャメルの2つ構築してしまったので、片方を削除することに。 ![](https://article-images.shimba.io/01E1NEMZY4CWHG99589H8AZB24.png =1919x965) 対象のAmplifyプロジェクトをクリックし、アクションボタンより削除を選択し実行。 ![](https://article-images.shimba.io/01E1NESCTR9F84RD5BE7VQ40YC.png =1914x888)
2020.02.22
lodashでcurryingを使って引数書かないで済ませる書き方
### 通常型 ```js let data = { a: 1, b: 2, c: 3, d: 4 }; _(data).values().some((val) => _.eq(val, -1)); // => false data = { a: 1, b: 2, c: 3, d: -1 }; _(data).values().some((val) => _.eq(val, -1)); // => true ``` ### curryを使ったやつ ```js let data = { a: 1, b: 2, c: 3, d: 4 }; _(data).values().some(_.curry(_.eq)(-1)); // => false data = { a: 1, b: 2, c: 3, d: -1 }; _(data).values().some(_.curry(_.eq)(-1)); // => true ``` 引数名とかちょっと思いつかないとか、スコープ外で同名があり使うの躊躇するって時に良さそうな感じ この例だとlodashだったが他の言語でもカリーを使った方が思考のリソースを浪費しないで済むので試しに使ってみるのはどうだろうか ##### ただの独り言 ##### ~~ 物事を後回ししたがるか脳死してる人向け ~~
2020.03.17
AmplifyでPRD→DEVの環境構築をした
既存のプロジェクトでプロダクション環境をAmplifyで構築していた。 ローンチ前だったので、プロダクション環境化で構築を行っていたが、そろそろローンチが近づいてきたのでAmplifyでDev環境を構築することにした。 # 既存環境 ざっくりでこんな感じの環境を持っていた。 ```shell $ amplify status Current Environment: prd | Category | Resource name | Operation | Provider plugin | | -------- | ----------------------------------- | --------- | ----------------- | | Auth | userPoolGroups | No Change | awscloudformation | | Auth | xxxxxx | No Change | awscloudformation | | Function | AdminQueriesxxxxxx | No Change | awscloudformation | | Function | xxxxxxCustomMessage | No Change | awscloudformation | | Function | xxxxxxPostConfirmation | No Change | awscloudformation | | Api | AdminQueries | No Change | awscloudformation | | Api | red | No Change | awscloudformation | ``` # Dev環境を構築してみる ```shell $ amplify env add Note: It is recommended to run this command from the root of your app directory ? Do you want to use an existing environment? No ? Enter a name for the environment dev Using default provider awscloudformation For more information on AWS Profiles, see: https://docs.aws.amazon.com/cli/latest/userguide/cli-multiple-profiles.html ? Do you want to use an AWS profile? Yes ? Please choose the profile you want to use red Adding backend environment dev to AWS Amplify Console app: dplqbaaykltjo ⠏ Initializing project in the cloud... CREATE_IN_PROGRESS amplify-xxxxxx-dev-xxxxxx AWS::CloudFormation::Stack Sat Feb 15 2020 11:21:18 GMT+0900 (Japan Standard Time) User Initiated CREATE_IN_PROGRESS DeploymentBucket AWS::S3::Bucket Sat Feb 15 2020 11:21:22 GMT+0900 (Japan Standard Time) CREATE_IN_PROGRESS AuthRole AWS::IAM::Role Sat Feb 15 2020 11:21:22 GMT+0900 (Japan Standard Time) CREATE_IN_PROGRESS UnauthRole AWS::IAM::Role Sat Feb 15 2020 11:21:22 GMT+0900 (Japan Standard Time) CREATE_IN_PROGRESS AuthRole AWS::IAM::Role Sat Feb 15 2020 11:21:23 GMT+0900 (Japan Standard Time) Resource creation Initiated ⠙ Initializing project in the cloud... CREATE_IN_PROGRESS UnauthRole AWS::IAM::Role Sat Feb 15 2020 11:21:24 GMT+0900 (Japan Standard Time) Resource creation Initiated ⠏ Initializing project in the cloud... CREATE_IN_PROGRESS DeploymentBucket AWS::S3::Bucket Sat Feb 15 2020 11:21:24 GMT+0900 (Japan Standard Time) Resource creation Initiated ⠋ Initializing project in the cloud... CREATE_COMPLETE AuthRole AWS::IAM::Role Sat Feb 15 2020 11:21:38 GMT+0900 (Japan Standard Time) ⠙ Initializing project in the cloud... CREATE_COMPLETE UnauthRole AWS::IAM::Role Sat Feb 15 2020 11:21:39 GMT+0900 (Japan Standard Time) ⠙ Initializing project in the cloud... CREATE_COMPLETE DeploymentBucket AWS::S3::Bucket Sat Feb 15 2020 11:21:45 GMT+0900 (Japan Standard Time) CREATE_COMPLETE amplify-xxxxx-dev-xxxxxx AWS::CloudFormation::Stack Sat Feb 15 2020 11:21:48 GMT+0900 (Japan Standard Time) ✔ Successfully created initial AWS cloud resources for deployments. ✔ Initialized provider successfully. You've opted to allow users to authenticate via Facebook. If you haven't already, you'll need to go to https://developers.facebook.com and create an App ID. ``` ではDev環境追加できたっぽいので、プッシュしてみる。 ```shell $ amplify push ✔ Successfully pulled backend environment dev from the cloud. Current Environment: dev | Category | Resource name | Operation | Provider plugin | | -------- | ----------------------------------- | --------- | ----------------- | | Auth | userPoolGroups | Create | awscloudformation | | Auth | xxxxxx | Create | awscloudformation | | Function | AdminQueriesxxxxxx | Create | awscloudformation | | Function | xxxxxxCustomMessage | Create | awscloudformation | | Function | xxxxxxPostConfirmation | Create | awscloudformation | | Api | AdminQueries | Create | awscloudformation | | Api | red | Create | awscloudformation | ? Are you sure you want to continue? Yes ......... ``` エラー・・。順番対応していく。 ```shell Following resources failed Resource Name: MFALambda (AWS::Lambda::Function) Event Type: create Reason: The runtime parameter of nodejs8.10 is no longer supported for creating or updating AWS Lambda functions. We recommend you use the new runtime (nodejs12.x) while creating or updating functions. (Service: AWSLambdaInternal; Status Code: 400; Error Code: InvalidParameterValueException; Request ID: 23ffa231-3003-4148-8f3c-a865cbb3542c) Resource Name: amplify-xxxxx-xxxxx (AWS::IAM::Policy) Event Type: create Reason: Resource creation cancelled Resource Name: amplify-xxxxx-xxxxx-authredxxxxx-UserPoolPostConfirmationLambdaInvokePermis-xxxxx (AWS::Lambda::Permission) Event Type: create Reason: Resource creation cancelled Resource Name: amplify-xxxxx-xxxxxx-authredxxxxx-UserPoolCustomMessageLambdaInvokePermissio-xxxxx (AWS::Lambda::Permission) Event Type: create Reason: Resource creation cancelled ✖ An error occurred when pushing the resources to the cloud Resource is not in the state stackUpdateComplete An error occured during the push operation: Resource is not in the state stackUpdateComplete ``` ## LambdaのNode8をNode12へアップデート これらのファイルでクラウドフォーメーションの情報を管理している。実際に構築してアプリによってここで修正する対象は変動あると思う。(作り方によって必要なLambda関数の工数も場所も変わってくるので) 私は今回、amplify/以下をnodejs8で検索し、それらをnodejs12.xに置換した。 ```shell amplify/backend/auth/xxxxxx/xxxxxx-cloudformation-template.yml amplify/backend/auth/userPoolGroups/template.json amplify/backend/function/AdminQueriesxxxxxx/AdminQueriesxxxxxx-cloudformation-template.json amplify/backend/function/xxxxxxCustomMessage/xxxxxxCustomMessage-cloudformation-template.json amplify/backend/function/xxxxxxPostConfirmation/xxxxxxPostConfirmation-cloudformation-template.json ``` これでLambdaのバージョンエラー問題は解決。 それ以外のエラーが謎すぎる、S3バケットを新規に作成しようとしているのに既に存在すると怒られる。 (ここのS3バケットはCognitoを構築しようとするときに自動的に作成されるもなので意図したものではない) とりあえず、以前にごにょごにょメンバーがDevを作ろうとしていたみたいだから、ひとまずDevをRemoveして再構築してみる。 ```shell $ amplify env remove dev $ amplify env add .... $ amplify push Resource Name: amplify-xxxxx-xxxxx (AWS::IAM::Policy) Event Type: create Reason: Resource creation cancelled Resource Name: amplify-xxxxx-xxxxx-authredxxxxx-UserPoolPostConfirmationLambdaInvokePermis-xxxxx (AWS::Lambda::Permission) Event Type: create Reason: Resource creation cancelled Resource Name: amplify-xxxxx-xxxxxx-authredxxxxx-UserPoolCustomMessageLambdaInvokePermissio-xxxxx (AWS::Lambda::Permission) Event Type: create Reason: Resource creation cancelled ``` ダメだーエラー変わらず。Devではなく、Tstに変えてみるか。 ```shell $ amplify env remove dev ... $ amplify env add tst ... $ amplify push ``` S3の構築箇所を乗り越えた! ElasticSearchの構築が長いのでしばらく待つ・・。(30分近く) ```shell GraphQL endpoint: https://xxxxxx.appsync-api.ap-northeast-1.amazonaws.com/graphql Hosted UI Endpoint: https://xxxxxx-xxxxxx-tst.auth.ap-northeast-1.amazoncognito.com/ Test Your Hosted UI Endpoint: https://xxxxxx-xxxxxx-tst.auth.ap-northeast-1.amazoncognito.com/login?response_type=code&client_id=xxxxxx&redirect_uri=https://xxxxxx.jp/thankyou/ ``` # 感想 生まれたてのフレームワークだからまだまだ色々と安定しない時があるけど、思想が本当に好きなのでついていきたいと思いました。うちの会社ではフルスタックエンジニアを随時募集or育成しているので浸透させたいと思います。 https://www.ragate.co.jp/
2020.02.15
既存プロジェクトのAmplify2.x.xからAmplify4.x.xへの移行
Amplifyを導入しているプロジェクトで開発中に、Amplifyのメジャーバージョンアップが行われたのでバージョンアップ対応しました。加えて、LambdaのNode8がサポートされなくなるニュースを聞いたのでここは今後を考えバージョンアップしておくのがベターだと考える。 まず、Amplifyのバージョンアップを行う。 ```shell $ npm install -g @aws-amplify/cli ``` プロジェクトのルートで新たにAmplify関連のコマンドを実行してみる。 私の場合はamplify pullを実行した。 そうすると、既存のクラウドフォーメーションのテンプレートのLambdaバージョンで警告が表示される。(旧AmplifyはNode8系でデプロイしていたのでバージョンアップを促される) ```shell $ amplify pull Amplify CLI uses Lambda backed custom resources with CloudFormation to manage part of your backend resources. In response to the Lambda Runtime support deprecation schedule https://docs.aws.amazon.com/lambda/latest/dg/runtime-support-policy.html Nodejs runtime need to be updated from nodejs8.10 to nodejs10.x in the following template files: /Users/username/Code/project/amplify/backend/auth/xxxxxx/xxxxxx-cloudformation-template.yml /Users/username/Code/project/amplify/backend/auth/userPoolGroups/template.json /Users/username/Code/project/amplify/backend/function/AdminQueriesxxxxxx/AdminQueriesxxxxxx-cloudformation-template.json /Users/username/Code/project/amplify/backend/function/xxxxxxCustomMessage/xxxxxxCustomMessage-cloudformation-template.json /Users/username/Code/project/amplify/backend/function/xxxxxxPostConfirmation/xxxxxxPostConfirmation-cloudformation-template.json Please test the changes in a test environment before pushing these changes to production. There might be a need to update your Lambda function source code due to the NodeJS runtime update. Please take a look at https://aws-amplify.github.io/docs/cli/lambda-node-version-update for more information ? Confirm to update the NodeJS runtime version to 10.x : ``` では置換してもらう。 ``` ? Confirm to update the NodeJS runtime version to 10.x : Yes NodeJS runtime version updated successfully to 10.x in all the CloudFormation templates. Make sure the template changes are pushed to the cloud by "amplify push" ``` ごっそり書き換わっている。 ``` $ git status modified: amplify/.config/project-config.json modified: amplify/backend/auth/xxxxxx/xxxxxx-cloudformation-template.yml modified: amplify/backend/auth/userPoolGroups/template.json modified: amplify/backend/function/AdminQueriesxxxxxx/AdminQueriesxxxxxx-cloudformation-template.json modified: amplify/backend/function/xxxxxxCustomMessage/xxxxxxCustomMessage-cloudformation-template.json modified: amplify/backend/function/xxxxxxPostConfirmation/xxxxxxPostConfirmation-cloudformation-template.json ``` デプロイしてみる。 ```shell $ amplify push ✔ Successfully pulled backend environment prd from the cloud. Current Environment: prd | Category | Resource name | Operation | Provider plugin | | --------- | ----------------------------------- | --------- | ----------------- | | Auth | xxxxxx | Update | awscloudformation | | Api | xxxxxx | Update | awscloudformation | | Providers | categories | Delete | | | Auth | userPoolGroups | No Change | awscloudformation | | Function | AdminQueriesxxxxxx | No Change | awscloudformation | | Function | xxxxxxCustomMessage | No Change | awscloudformation | | Function | xxxxxxPostConfirmation | No Change | awscloudformation | | Api | AdminQueries | No Change | awscloudformation | ``` ・・・・・ ・・・・・ ```shell GraphQL endpoint: https://xxxxxx.appsync-api.ap-northeast-1.amazonaws.com/graphql Hosted UI Endpoint: https://xxxxxx-xxxxxx-prd.auth.ap-northeast-1.amazoncognito.com/ Test Your Hosted UI Endpoint: https://xxxxxx-xxxxxx-prd.auth.ap-northeast-1.amazoncognito.com/login?response_type=code&client_id=xxxxxx&redirect_uri=https://xxxxxx.com/thankyou/ ``` 成功! エラーでずにデプロイできた。
2020.02.12
AmplifyのSSR対応状況 ( 2020年2月11日現在 )
# AmplifyでSSRしたいけど Amplifyは、インフラのプロビジョニングからSDKの提供までしてくれる頼もしいテクノロジーパートナー。うちの会社でも重宝して各プロジェクトへ導入しています。 難点なのが、Nuxtjsなどへ組み込んで利用する際にSSR利用できないこと。 つまり、asyncDataメソッドやfetchのサーバーサイドの処理の時に、データを取得できないのだ。 # まずは結論 SSRは非対応なので、諦める。 いきなり弱気かよ!って感じだけど、近い将来にSSR対応してくれそうな匂いがあるし、現状はGraphQLのクライアントとしての利用だけでも力を発揮するので、付き合い方を考えればいいと思う。 開発が圧倒的に効率化されるメリットは捨てがたいので、何かしらの形で利用はしたい。 # SSRの対応状況 まず、このIssueが最も注目したい情報です。 https://github.com/aws-amplify/amplify-js/issues/1613 この中でAmplifyからSSR対応について下記のように行えますと回答ができています。 https://medium.com/open-graphql/ssr-graphql-apps-with-next-js-aws-appsync-eaf7fbeb1bde つまり!!公式的には、SSR時には別途AppSyncのクライアント用意してどうにかしとけ!って感じらしいです...。 個人的には、実装部分として下記のように対応する必要がありますかなと思います。 - クライアントサイドの処理はAmplify - サーバーサイド の処理は自前で作成(aws_exports.js読み込めばいいのかな) 結局工数かかるやんけ!Amplifyの意味!w # 個人的なAmplifyとの付き合い方 僕は下記のユースケースでAmplifyを利用しています、近い将来SSR対応されたら伸び伸びと使いたいです。 - 会員限定コンテンツ配信アプリケーション - 業務/基幹アプリケーション - その他SEO必要としないアプリケーション(オーガニック検索の流入のみ想定)
2020.02.11
2020年のオススメVuejsのUIフレームワーク!
最近、Vuejsを使ったサクッと作る系のアプリ開発相談が多い。 - 運営者向けの管理画面 - 業務アプリ - プロトタイピング ![](https://article-images.shimba.io/01E0S60A54Y2712CS8AK2JVBHM.png =994x705) うちの会社で採用しているor採用検討中のフレームワークを紹介します。 # Ant-Design Github:https://github.com/quasarframework/quasar コンポーネントの種類が多くてコミュニティーとベンダーの開発が盛んな印象。加えて、各種コンポーネントのAPI種類がとても豊富。難点としては、フォームオブジェクトに内包するインプットの数が多くなるとクソ重くなる。そこらへんは仕方なくv-modelとかで取得してごにょごにょする必要がある。(でもどのフレームワークでも発生することあるっぽいので、Ant-Designに限ったリスクではないと思うけどどどうなんだろう...) # Quasar Github:https://github.com/quasarframework/quasar デザインがちょっと最先端すぎるかもなので、日本市場だとあまり採用されないとは思う。最先端というか、ちょっと個人的には好きではない。でも、コンポーネントの種類が多くてAPIの利便性もある。開発が死ぬほど盛んで、毎月何かしらアップデートされている印象がある。逆にいうと安定しない?のかな。その辺りはよくわからんけど、海外のトレンドを積極的にと言いれているっぽいので、興味が湧く。 # Vuetify Github:https://github.com/vuetifyjs/vuetify Vue使いならみんな知っている。王道の中の王道なフレームワーク。軽量でほどよく便利。UIフレームワーク利用したことない、試してみたいならまぁこのフレームワークでしょう。
2020.02.11
javascriptで画像の読み込み待ちっぽいの
まずはコードから ```html <span id="loading" class="icon loading" /> <span id="icon" class="icon hidden" style="background-image: url('https://example.com/iconurl')" /> <style> .hidden { display: none; } .icon { background-size: 100% 100%; } /* この辺から拝借 https://ant.design/components/skeleton/ */ .loading { background: -webkit-gradient(linear, left top, right top, color-stop(25%, #f2f2f2), color-stop(37%, #e6e6e6), color-stop(63%, #f2f2f2)); background: linear-gradient(90deg, #f2f2f2 25%, #e6e6e6 37%, #f2f2f2 63%); background-size: 400% 100%; -webkit-animation: loading 1.4s ease infinite; animation: loading 1.4s ease infinite; } @-webkit-keyframes loading { 0% { background-position: 100% 50%; } 100% { background-position: 0 50%; } } @keyframes loading { 0% { background-position: 100% 50%; } 100% { background-position: 0 50%; } } </style> ``` ```js // yarn add imagesloaded https://imagesloaded.desandro.com/ import imagesLoaded from 'imagesloaded' const icon = document.getElementById('icon') const loading = document.getElementById('loading') imagesLoaded(icon, { background: true }, () => { loading.classList.add('hidden') icon.classList.remove('hidden') }) ``` # 解説 ### まずはパッケージの解説 imagesloadedというパッケージがあり、これは画像読み込みを検知してくれるやつ background-imageであればoptionsに`{ background: true }`としてあげると背景画像の読み込みも待ってからコールバック関数が呼ばれる ### 動作の解説 画像読み込みをしている間はなんかそれっぽいロードしてますよ感のあるやつを表示しておいて、画像を表示するやつは非表示にしておく(cssアニメーション便利) 読み込みが終わったらクラスを足したり消したりして画像を表示する ### 注意点? 画像を表示するタグで読み込みのスタイルを適用しないのは読み込みが完了した直後にCSSアニメーションが少し残ることがあり、その際に画像が横に引っ張られたりして見栄えが少し悪いから よくあるゲームとかのロードとかと発想は同じ # 最後に imagesloadedは以前はまだバックグラウンドの読み込み待ちに対応してなかったが数年前に対応したらしい 時代が進むにつれていろんなものが便利になっていっていてとても良き こうしたらもっと良いと思う!というのあったりしたらコメント残してって下しあ
2020.02.04
APIとSPAによるサイトのリリース時のダウンタイムをゼロにする
## こういう人向け * フロントエンドをSPA(シングルページアプリケーション)で構築している * リリース時にダウンタイムが発生するのをなんとかしたい ## なぜダウンタイムが発生するのか リリース時にダウンタイムが発生する原因は、「APIとSPAの間でリリースに時間差が発生するから」です。 例えばAPIに後方互換性のない破壊的変更があったとします。 そういった場合、通常はAPIとフロントエンドを同時にリリースすれば問題ありませんが、フロントエンドがSPAの場合は厄介です。 なぜならページ遷移してもJavaScriptが再読み込みされないので、そのままでは古い仕様のまま新しいAPIを実行してしまったり、古いAPIを実行してしまったりと、不具合の元となってしまうからです。 それを防ぐためにメンテナンスページを表示したり503エラーを表示させたりするサイトもあります。いわゆるダウンタイムですね。 ## ダウンタイムを発生させないためには JavaScriptを再読み込みさせる最も一般的な方法として「ブラウザのリロード」があります。 ブラウザをJavaScript側からリロードさせることによって強制的にJavaScriptを再読み込みさせることができます。 問題は、どうやってフロントエンドから「APIに破壊的変更があった」という事実を認識するかです。 ## APIの変更をフロントエンドで認識する方法 方法はいろいろありますが、例えば「全APIのレスポンスヘッダにフロントエンドの最新バージョン情報を入れる」という方法があります。 フロントエンドで自身のバージョンを保持しておいて、毎回APIのレスポンスヘッダの最新バージョンと照合し、一致しない場合は最新バージョンがリリースされたと判断してブラウザをリロードするのです。 しかし、この方法には穴があります。 APIのリリースとフロントエンドのリリースに時間差がある場合、その時間内にフロントエンドからAPIを実行されてしまうと不具合が発生してしまう可能性が否定できないからです。 ## APIとフロントエンドのリリース時間差に対応 その解決方法としては、「バージョンごとにAPIサーバーを用意する」という方法があります。 例えば、 `https://api.shimba.io/v1/xxxxx` というURLがあったとして、新しいバージョンのAPIをリリースすると `https://api.shimba.io/v2/xxxxx` というURLを新たに追加します。 こうすることで、APIのv2を新たにリリースしても、フロントエンドはv1を叩き続けるので仕様的な不整合は発生しません。 そして、v2に対応したフロントエンドのリリースが終わった時点で、前述のようにAPIからフロントエンドにレスポンスヘッダ経由で通知して、それを受け取ったフロントエンドがブラウザをリロードして、新しい実装でv2を叩くようになります。 ## AWSとServerless Frameworkなら実現しやすい 自前で上記構成を作ってもいいのですが、なかなか面倒だと思います。 そこでオススメしたいのが、AWSとServerless Frameworkを使う方法です。 Serverless Framework [https://serverless.com/](https://serverless.com/) ![](https://article-images.shimba.io/01E07FXNP2HG3DQ56F9AN7M9C3.png =1920x1353) Serverless Frameworkとは、AWSのLambdaやAPI Gatewayを使ってAPIサーバーをカンタンに構築することができるフレームワークです。 そして、Serverless FrameworkからAPI Gatewayを操作すると、先ほどのような/v1や/v2に新旧バージョンのAPIをデプロイするといった設計を手軽に実現できます。 ## まとめ APIとSPAによるサイトのリリース時のダウンタイムをゼロにするためには、 * Serverless Frameworkを使ってAPIサーバーを構築する * 破壊的変更を伴うリリース時には新たにv2,v3といった新しいURLを用意する * フロントエンドの最新バージョンを全APIのレスポンスヘッダに入れる * フロントエンドはAPIのレスポンスヘッダに現バージョンより新しいバージョンが書かれてあったらブラウザをリロードする といった設計、実装をすることが一番手軽かなと思います。 もちろん、今回ご紹介したソリューションも完璧ではありません。DBスキーマが変わった場合にどうバージョン管理するべきか、パフォーマンスが悪くなってしまわないか、といった課題も残されています。 ですが、「手軽にダウンタイムをゼロにする」という観点で考えるとこの方法がベターなのではないかなと思います。 それにこのあたりの分野は日々進化していて、AmplifyやAWS CDKといった新しい技術によって、もっと手軽に、もっと安全に、ダウンタイムゼロを実現できる日が来るかもしれません。 最新情報のキャッチアップは必須ですね。
2020.02.04
Web Storageに関して
# はじめに 簡単にWeb Storageについて説明をします。 ## Web Storageとは - Cookieよりも大容量のデータをブラウザ側に保存できるAPI - Key-Value型で保存が可能 - ストレージは2種類でsessionとlocalに分けられる ### Cookie - WebサイトによってユーザーのPCに保存される小さなファイル - 「ユーザーに関する情報を覚えておく」を目的にしている ### sessionStorage - ウィンドウ/タブごとのセッションで有効なストレージ - 一回限りのセッションで有効になるため、ウィンドウ/タブを閉じるとデータが失われる - ウィンドウ間でデータが共有されることはない ### localStorage - オリジン単位でデータを永続的に保存するストレージ - ウィンドウ/タブを閉じてもデータは失われない - ウィンドウ/タブ間でデータを共有することが出来る #### 比較 | |Cookie |session |local | |---|---|---|---| |保存容量 |4KB(キロ) |1オリジン当たり5MB(メガ) |1オリジン当たり5MB(メガ) | |データの保存期間 |指定期限まで有効 |ウィンドウ/タブを閉じるまで有効 |永続的に有効 | |サーバーへのデータ送信 |サーバーへアクセスする度に自動送信 |必要なときにスクリプトかフォームで送信 |必要なときにスクリプトかフォームで送信 | |別ウィンドウでのデータ共有 |出来る |出来ない |出来る | #### オリジン - http://www.hoge.com:3000 のような「プロトコル://ドメイン名:ポート番号」のこと - 保存は「ドメイン:ポート番号」の組み合わせになる、これをオリジン単位という # 使ってみた ## setItem ```javascript localStorage.setItem('name', 'shota'); ``` これだけでKeyにname、Valueにshotaが入る。 検証->Application->Local Storageから確認可能。 ※localStorageでデータ保存をしていますが、sessionStorageに書き換えるとSession Storageに入ります。 ```javascript localStorage.setItem('name', 'kubo'); localStorage.setItem('full_name', 'shota_kubo'); ``` Key名を一緒にして、Valueを変えると**shota**は上書きされて**kubo**が入る。 Key名を別にすると、kuboは上書きされずに新しく発行される。 ## getItem ```javascript cosnt getName = localStorage.getItem('name'); console.log(getName); ``` getItemで指定したキーの値を取得出来ます。 console.logで、**検証**->**Console**に出力されていると思います。 ## removeItem ```javascript localStorage.removeItem('name'); const getName = localStorage.getItem('name'); console.log(getName); ``` removeItemを指定すると指定したキーのデータが削除されます。 ここでは、keyが取得出来ずにnullが返されます。 ## clear ```javascript localStorage.setItem('last_name', 'shota'); localStorage.setItem('first_name', 'kubo'); localStorage.setItem('full_name', 'shota_kubo'); localStorage.clear(); ``` clearはすべてのデータを削除します。 よって、localStorageには何も表示されません。 # こぼれ話 Number型でデータを入れても文字列に変更される。 なので、以下のコードを書いても ```javascript localStorage.setItem('num_1', 1); localStorage.setItem('num_2', 2); localStorage.setItem('num_3', localStorage.getItem('num_1') + localStorage.getItem('num_2')); ``` 1 + 2は**12**になる。 # 参考サイト http://www.htmq.com/webstorage/ https://developer.mozilla.org/ja/docs/Web/API/Web_Storage_API#Private_Browsing_Incognito_modes https://www.w3schools.com/html/html5_webstorage.asp # 最後に 以上です、ありがとうございました。 誤字脱字・その他ご指摘がある場合は、コメントいただけたら幸いです。
2020.03.10
Nuxtjs×Amplifyで電話番号認証を実装してみた!
こんにちは! もうすぐ年末ですね。 2019年はAmplifyの年と言えるほど、Amplifyしていました。 電話番号認証を実装したのでシェアします。 # 環境 - Nodejs v10.16.3 - Aws Amplify CLI 3.17.0 #  処理の流れ 1. amplify init 2. amplify add auth 3. amplify push 4. SIgnUp / SMSコードの送信 5. SMSコードの検証 # Amplify init Amplifyの最もな特徴は、「設問に答えるだけでサーバーサイドの処理が完結」ということです。 なのでまずは、設問に回答してサーバーサイドの処理を自動的に作成します。 ``` shell $ amplify init Note: It is recommended to run this command from the root of your app directory ? Enter a name for the project sample ? Enter a name for the environment dev ? Choose your default editor: None ? Choose the type of app that you're building javascript Please tell us about your project ? What javascript framework are you using vue ? Source Directory Path: src ? Distribution Directory Path: dist ? Build Command: npm run-script build ? Start Command: npm run-script serve Using default provider awscloudformation For more information on AWS Profiles, see: https://docs.aws.amazon.com/cli/latest/userguide/cli-multiple-profiles.html ? Do you want to use an AWS profile? Yes ? Please choose the profile you want to use default ⠹ Initializing project in the cloud... CREATE_IN_PROGRESS amplify-sample-dev-120720 AWS::CloudFormation::Stack Sun Dec 22 2019 12:07:21 GMT+0900 (Japan Standard Time) User Initiated CREATE_IN_PROGRESS UnauthRole AWS::IAM::Role Sun Dec 22 2019 12:07:25 GMT+0900 (Japan Standard Time) CREATE_IN_PROGRESS AuthRole AWS::IAM::Role Sun Dec 22 2019 12:07:25 GMT+0900 (Japan Standard Time) CREATE_IN_PROGRESS DeploymentBucket AWS::S3::Bucket Sun Dec 22 2019 12:07:25 GMT+0900 (Japan Standard Time) CREATE_IN_PROGRESS UnauthRole AWS::IAM::Role Sun Dec 22 2019 12:07:25 GMT+0900 (Japan Standard Time) Resource creation Initiated CREATE_IN_PROGRESS AuthRole AWS::IAM::Role Sun Dec 22 2019 12:07:26 GMT+0900 (Japan Standard Time) Resource creation Initiated ⠇ Initializing project in the cloud... CREATE_IN_PROGRESS DeploymentBucket AWS::S3::Bucket Sun Dec 22 2019 12:07:27 GMT+0900 (Japan Standard Time) Resource creation Initiated ⠼ Initializing project in the cloud... CREATE_COMPLETE UnauthRole AWS::IAM::Role Sun Dec 22 2019 12:07:41GMT+0900 (Japan Standard Time) CREATE_COMPLETE AuthRole AWS::IAM::Role Sun Dec 22 2019 12:07:41 GMT+0900 (Japan Standard Time) ⠴ Initializing project in the cloud... CREATE_COMPLETE DeploymentBucket AWS::S3::Bucket Sun Dec 22 2019 12:07:47 GMT+0900 (Japan Standard Time) CREATE_COMPLETE amplify-sample-dev-120720 AWS::CloudFormation::Stack Sun Dec 22 2019 12:07:50 GMT+0900 (Japan Standard Time) ✔ Successfully created initial AWS cloud resources for deployments. ✔ Initialized provider successfully. Initialized your environment successfully. Your project has been successfully initialized and connected to the cloud! Some next steps: "amplify status" will show you what you've added already and if it's locally configured or deployed "amplify <category> add" will allow you to add features like user login or a backend API "amplify push" will build all your local backend resources and provision it in the cloud "amplify publish" will build all your local backend and frontend resources (if you have hosting category added) and provision it in the cloud Pro tip: Try "amplify add api" to create a backend API and then "amplify publish" to deploy everything ``` # amplify add auth ここで、デフォルトの設定はメールアドレスに認証コード送信設定になっているので、SMS で送信するようにします。 該当設問: `Email based user registration/forgot password: Disabled (Uses SMS/TOTP as an alternative)` ``` $ sample amplify add auth Using service: Cognito, provided by: awscloudformation The current configured provider is Amazon Cognito. Do you want to use the default authentication and security configuration? Manual configuration Select the authentication/authorization services that you want to use: User Sign-Up, Sign-In, connected with AWS IAM controls (Enables per-user Storage features for images or other content, Analytics, and more) Please provide a friendly name for your resource that will be used to label this category in the project: samplexxxxxx Please enter a name for your identity pool. samplexxxxxx_identitypool_xxxxxx Allow unauthenticated logins? (Provides scoped down permissions that you can control via AWS IAM) No Do you want to enable 3rd party authentication providers in your identity pool? Yes Select the third party identity providers you want to configure for your identity pool: (Press <space> to select, <a> to toggle all, <i> to invert selection) Please provide a name for your user pool: sampleacxxxxxx_userpool_xxxxxx Warning: you will not be able to edit these selections. How do you want users to be able to sign in? Username Do you want to add User Pool Groups? Yes ? Provide a name for your user pool group: customer ? Do you want to add another User Pool Group No ✔ Sort the user pool groups in order of preference · customer Do you want to add an admin queries API? No Multifactor authentication (MFA) user login options: OFF Email based user registration/forgot password: Disabled (Uses SMS/TOTP as an alternative) Please specify an SMS verification message: Your verification code is {####} Do you want to override the default password policy for this User Pool? No Warning: you will not be able to edit these selections. What attributes are required for signing up? (Press <space> to select, <a> to toggle all, <i> to invert selection)Email Specify the app's refresh token expiration period (in days): 30 Do you want to specify the user attributes this app can read and write? No Do you want to enable any of the following capabilities? (Press <space> to select, <a> to toggle all, <i> to invert selection) Do you want to use an OAuth flow? Yes What domain name prefix you want us to create for you? sampleacxxxxxx Enter your redirect signin URI: https://example.com/ ? Do you want to add another redirect signin URI No Enter your redirect signout URI: https://example.com/ ? Do you want to add another redirect signout URI No Select the OAuth flows enabled for this project. Authorization code grant Select the OAuth scopes enabled for this project. (Press <space> to select, <a> to toggle all, <i> to invert selection)Phone, Email, OpenID, Profile, aws.cognito.signin.user.admin Select the social providers you want to configure for your user pool: (Press <space> to select, <a> to toggle all, <i> to invert selection) ? Do you want to configure Lambda Triggers for Cognito? No Successfully added resource samplexxxxxx locally Some next steps: "amplify push" will build all your local backend resources and provision it in the cloud "amplify publish" will build all your local backend and frontend resources (if you have hosting category added) and provision it in the cloud ``` **SMSの送信はAWSデフォルトでは送信上限が1$となっている** AWSサポートより事前に送信緩和申請を出しておきましょう。開発もできない状態となってしまいます。 # amplify push amplify pushは、amplify statusで表示された差分をクラウド上へデプロイしてくれます。勝手にGit的なファイルの変更を追跡してくれるので、便利ですね。amplifyディレクトリ以下をみているっぽいです。 ``` shell $ sample amplify push Current Environment: dev | Category | Resource name | Operation | Provider plugin | | -------- | ---------------------- | --------- | ----------------- | | Auth | userPoolGroups | Create | awscloudformation | | Auth | samplexxxxxxx | Create | awscloudformation | ? Are you sure you want to continue? Yes ⠏ Updating resources in the cloud. This may take a few minutes... UPDATE_IN_PROGRESS amplify-sample-dev-120720 AWS::CloudFormation::Stack Sun Dec 22 2019 12:15:38 GMT+0900 (Japan Standard Time) User Initiated CREATE_IN_PROGRESS UpdateRolesWithIDPFunctionRole AWS::IAM::Role Sun Dec 22 2019 12:15:43 GMT+0900 (Japan Standard Time) ⠧ Updating resources in the cloud. This may take a few minutes... CREATE_IN_PROGRESS authsamplexxxxxxx AWS::CloudFormation::Stack Sun Dec 22 2019 12:15:43 GMT+0900 (Japan Standard Time) ⠇ Updating resources in the cloud. This may take a few minutes... CREATE_IN_PROGRESS UpdateRolesWithIDPFunctionRole AWS::IAM::Role Sun Dec 22 2019 12:15:44 GMT+0900 (Japan Standard Time) Resource creation Initiated CREATE_IN_PROGRESS authsamplexxxxxxx AWS::CloudFormation::Stack Sun Dec 22 2019 12:15:45 GMT+0900 (Japan Standard Time) Resource creation Initiated ⠹ Updating resources in the cloud. This may take a few minutes... CREATE_IN_PROGRESS amplify-sample-dev-120720-authsamplexxxxxxxAWS::CloudFormation::Stack Sun Dec 22 2019 12:15:45 GMT+0900 (Japan Standard Time) User Initiated ⠼ Updating resources in the cloud. This may take a few minutes... CREATE_IN_PROGRESS SNSRole AWS::IAM::Role Sun Dec 22 2019 12:15:57 GMT+0900 (Japan Standard Time) CREATE_IN_PROGRESS SNSRole AWS::IAM::Role Sun Dec 22 2019 12:15:58 GMT+0900 (Japan Standard Time) Resource creation Initiated ⠋ Updating resources in the cloud. This may take a few minutes... CREATE_COMPLETE UpdateRolesWithIDPFunctionRole AWS::IAM::Role Sun Dec 22 2019 12:16:01 GMT+0900 (Japan Standard Time) ⠦ Updating resources in the cloud. This may take a few minutes... CREATE_COMPLETE SNSRole AWS::IAM::Role Sun Dec 22 2019 12:16:14 GMT+0900 (Japan Standard Time) ⠦ Updating resources in the cloud. This may take a few minutes... CREATE_IN_PROGRESS UserPool AWS::Cognito::UserPool Sun Dec 22 2019 12:16:20 GMT+0900 (Japan Standard Time) CREATE_IN_PROGRESS UserPool AWS::Cognito::UserPool Sun Dec 22 2019 12:16:23 GMT+0900 (Japan Standard Time) Resource creation Initiated CREATE_COMPLETE UserPool AWS::Cognito::UserPool Sun Dec 22 2019 12:16:23 GMT+0900 (Japan Standard Time) ⠧ Updating resources in the cloud. This may take a few minutes... CREATE_IN_PROGRESS UserPoolClient AWS::Cognito::UserPoolClient Sun Dec 22 2019 12:16:26 GMT+0900 (Japan Standard Time) CREATE_IN_PROGRESS UserPoolClientWeb AWS::Cognito::UserPoolClient Sun Dec 22 2019 12:16:27 GMT+0900 (Japan Standard Time) CREATE_IN_PROGRESS UserPoolClient AWS::Cognito::UserPoolClient Sun Dec 22 2019 12:16:28 GMT+0900 (Japan Standard Time) Resource creation Initiated ⠦ Updating resources in the cloud. This may take a few minutes... CREATE_COMPLETE UserPoolClient AWS::Cognito::UserPoolClient Sun Dec 22 2019 12:16:28 GMT+0900 (Japan Standard Time) CREATE_IN_PROGRESS UserPoolClientWeb AWS::Cognito::UserPoolClient Sun Dec 22 2019 12:16:29 GMT+0900 (Japan Standard Time) Resource creation Initiated CREATE_COMPLETE UserPoolClientWeb AWS::Cognito::UserPoolClient Sun Dec 22 2019 12:16:29 GMT+0900 (Japan Standard Time) ⠇ Updating resources in the cloud. This may take a few minutes... CREATE_IN_PROGRESS UserPoolClientRole AWS::IAM::Role Sun Dec 22 2019 12:16:32 GMT+0900 (Japan Standard Time) CREATE_IN_PROGRESS UserPoolClientRole AWS::IAM::Role Sun Dec 22 2019 12:16:33 GMT+0900 (Japan Standard Time) Resource creation Initiated ⠋ Updating resources in the cloud. This may take a few minutes... CREATE_COMPLETE UserPoolClientRole AWS::IAM::Role Sun Dec 22 2019 12:16:48 GMT+0900 (Japan Standard Time) ⠦ Updating resources in the cloud. This may take a few minutes... CREATE_IN_PROGRESS HostedUIProvidersCustomResource AWS::Lambda::Function Sun Dec 22 2019 12:16:52 GMT+0900 (Japan Standard Time) CREATE_IN_PROGRESS UserPoolClientLambda AWS::Lambda::Function Sun Dec 22 2019 12:16:52 GMT+0900 (Japan Standard Time) CREATE_IN_PROGRESS HostedUICustomResource AWS::Lambda::Function Sun Dec 22 2019 12:16:53 GMT+0900 (Japan Standard Time) CREATE_IN_PROGRESS HostedUIProvidersCustomResource AWS::Lambda::Function Sun Dec 22 2019 12:16:53 GMT+0900 (Japan Standard Time) Resource creation Initiated CREATE_IN_PROGRESS UserPoolClientLambda AWS::Lambda::Function Sun Dec 22 2019 12:16:53 GMT+0900 (Japan Standard Time) Resource creation Initiated CREATE_IN_PROGRESS HostedUICustomResource AWS::Lambda::Function Sun Dec 22 2019 12:16:53 GMT+0900 (Japan Standard Time) Resource creation Initiated CREATE_COMPLETE HostedUIProvidersCustomResource AWS::Lambda::Function Sun Dec 22 2019 12:16:53 GMT+0900 (Japan Standard Time) CREATE_COMPLETE UserPoolClientLambda AWS::Lambda::Function Sun Dec 22 2019 12:16:54 GMT+0900 (Japan Standard Time) ⠹ Updating resources in the cloud. This may take a few minutes... CREATE_COMPLETE HostedUICustomResource AWS::Lambda::Function Sun Dec 22 2019 12:16:54 GMT+0900 (Japan Standard Time) ⠦ Updating resources in the cloud. This may take a few minutes... CREATE_IN_PROGRESS UserPoolClientLambdaPolicy AWS::IAM::Policy Sun Dec 22 2019 12:16:58 GMT+0900 (Japan Standard Time) CREATE_IN_PROGRESS HostedUIProvidersCustomResourcePolicy AWS::IAM::Policy Sun Dec 22 2019 12:16:58 GMT+0900 (Japan Standard Time) CREATE_IN_PROGRESS HostedUICustomResourcePolicy AWS::IAM::Policy Sun Dec 22 2019 12:16:58 GMT+0900 (Japan Standard Time) ⠙ Updating resources in the cloud. This may take a few minutes... CREATE_IN_PROGRESS UserPoolClientLambdaPolicy AWS::IAM::Policy Sun Dec 22 2019 12:16:59 GMT+0900 (Japan Standard Time) Resource creation Initiated CREATE_IN_PROGRESS HostedUIProvidersCustomResourcePolicy AWS::IAM::Policy Sun Dec 22 2019 12:16:59 GMT+0900 (Japan Standard Time) Resource creation Initiated ⠦ Updating resources in the cloud. This may take a few minutes... CREATE_IN_PROGRESS HostedUICustomResourcePolicy AWS::IAM::Policy Sun Dec 22 2019 12:17:00 GMT+0900 (Japan Standard Time) Resource creation Initiated ⠸ Updating resources in the cloud. This may take a few minutes... CREATE_COMPLETE HostedUIProvidersCustomResourcePolicy AWS::IAM::Policy Sun Dec 22 2019 12:17:13 GMT+0900 (Japan Standard Time) CREATE_COMPLETE UserPoolClientLambdaPolicy AWS::IAM::Policy Sun Dec 22 2019 12:17:13 GMT+0900 (Japan Standard Time) ⠦ Updating resources in the cloud. This may take a few minutes... CREATE_COMPLETE HostedUICustomResourcePolicy AWS::IAM::Policy Sun Dec 22 2019 12:17:14 GMT+0900 (Japan Standard Time) CREATE_IN_PROGRESS HostedUIProvidersCustomResourceLogPolicy AWS::IAM::Policy Sun Dec 22 2019 12:17:17 GMT+0900 (Japan Standard Time) CREATE_IN_PROGRESS UserPoolClientLogPolicy AWS::IAM::Policy Sun Dec 22 2019 12:17:17 GMT+0900 (Japan Standard Time) CREATE_IN_PROGRESS HostedUICustomResourceLogPolicy AWS::IAM::Policy Sun Dec 22 2019 12:17:18 GMT+0900 (Japan Standard Time) CREATE_IN_PROGRESS HostedUIProvidersCustomResourceLogPolicy AWS::IAM::Policy Sun Dec 22 2019 12:17:18 GMT+0900 (Japan Standard Time) Resource creation Initiated ⠙ Updating resources in the cloud. This may take a few minutes... CREATE_IN_PROGRESS UserPoolClientLogPolicy AWS::IAM::Policy Sun Dec 22 2019 12:17:19 GMT+0900 (Japan Standard Time) Resource creation Initiated ⠼ Updating resources in the cloud. This may take a few minutes... CREATE_IN_PROGRESS HostedUICustomResourceLogPolicy AWS::IAM::Policy Sun Dec 22 2019 12:17:19 GMT+0900 (Japan Standard Time) Resource creation Initiated ⠧ Updating resources in the cloud. This may take a few minutes... CREATE_COMPLETE HostedUIProvidersCustomResourceLogPolicy AWS::IAM::Policy Sun Dec 22 2019 12:17:32 GMT+0900 (Japan Standard Time) CREATE_COMPLETE UserPoolClientLogPolicy AWS::IAM::Policy Sun Dec 22 2019 12:17:33 GMT+0900 (Japan Standard Time) ⠇ Updating resources in the cloud. This may take a few minutes... CREATE_COMPLETE HostedUICustomResourceLogPolicy AWS::IAM::Policy Sun Dec 22 2019 12:17:34 GMT+0900 (Japan Standard Time) ⠏ Updating resources in the cloud. This may take a few minutes... CREATE_IN_PROGRESS HostedUIProvidersCustomResourceInputs Custom::LambdaCallout Sun Dec 22 2019 12:17:37 GMT+0900 (Japan Standard Time) CREATE_IN_PROGRESS UserPoolClientInputs Custom::LambdaCallout Sun Dec 22 2019 12:17:37 GMT+0900 (Japan Standard Time) CREATE_IN_PROGRESS HostedUICustomResourceInputs Custom::LambdaCallout Sun Dec 22 2019 12:17:37 GMT+0900 (Japan Standard Time) ⠧ Updating resources in the cloud. This may take a few minutes... CREATE_IN_PROGRESS HostedUIProvidersCustomResourceInputs Custom::LambdaCallout Sun Dec 22 2019 12:17:41 GMT+0900 (Japan Standard Time) Resource creation Initiated CREATE_IN_PROGRESS UserPoolClientInputs Custom::LambdaCallout Sun Dec 22 2019 12:17:41 GMT+0900 (Japan Standard Time) Resource creation Initiated CREATE_COMPLETE HostedUIProvidersCustomResourceInputs Custom::LambdaCallout Sun Dec 22 2019 12:17:41 GMT+0900 (Japan Standard Time) CREATE_COMPLETE UserPoolClientInputs Custom::LambdaCallout Sun Dec 22 2019 12:17:41 GMT+0900 (Japan Standard Time) CREATE_IN_PROGRESS HostedUICustomResourceInputs Custom::LambdaCallout Sun Dec 22 2019 12:17:42 GMT+0900 (Japan Standard Time) Resource creation Initiated CREATE_COMPLETE HostedUICustomResourceInputs Custom::LambdaCallout Sun Dec 22 2019 12:17:43 GMT+0900 (Japan Standard Time) ⠏ Updating resources in the cloud. This may take a few minutes... CREATE_IN_PROGRESS IdentityPool AWS::Cognito::IdentityPool Sun Dec 22 2019 12:17:45 GMT+0900 (Japan Standard Time) CREATE_IN_PROGRESS OAuthCustomResource AWS::Lambda::Function Sun Dec 22 2019 12:17:46 GMT+0900 (Japan Standard Time) CREATE_IN_PROGRESS OAuthCustomResource AWS::Lambda::Function Sun Dec 22 2019 12:17:46 GMT+0900 (Japan Standard Time) Resource creation Initiated CREATE_COMPLETE OAuthCustomResource AWS::Lambda::Function Sun Dec 22 2019 12:17:47 GMT+0900 (Japan Standard Time) CREATE_IN_PROGRESS IdentityPool AWS::Cognito::IdentityPool Sun Dec 22 2019 12:17:47 GMT+0900 (Japan Standard Time) Resource creation Initiated CREATE_COMPLETE IdentityPool AWS::Cognito::IdentityPool Sun Dec 22 2019 12:17:48 GMT+0900 (Japan Standard Time) ⠸ Updating resources in the cloud. This may take a few minutes... CREATE_IN_PROGRESS OAuthCustomResourcePolicy AWS::IAM::Policy Sun Dec 22 2019 12:17:51 GMT+0900 (Japan Standard Time) CREATE_IN_PROGRESS IdentityPoolRoleMap AWS::Cognito::IdentityPoolRoleAttachment Sun Dec 22 2019 12:17:51 GMT+0900 (Japan Standard Time) CREATE_IN_PROGRESS OAuthCustomResourcePolicy AWS::IAM::Policy Sun Dec 22 2019 12:17:52 GMT+0900 (Japan Standard Time) Resource creation Initiated CREATE_IN_PROGRESS IdentityPoolRoleMap AWS::Cognito::IdentityPoolRoleAttachment Sun Dec 22 2019 12:17:53 GMT+0900 (Japan Standard Time) Resource creation Initiated ⠧ Updating resources in the cloud. This may take a few minutes... CREATE_COMPLETE IdentityPoolRoleMap AWS::Cognito::IdentityPoolRoleAttachment Sun Dec 22 2019 12:17:53 GMT+0900 (Japan Standard Time) ⠋ Updating resources in the cloud. This may take a few minutes... CREATE_COMPLETE OAuthCustomResourcePolicy AWS::IAM::Policy Sun Dec 22 2019 12:18:06 GMT+0900 (Japan Standard Time) ⠸ Updating resources in the cloud. This may take a few minutes... CREATE_IN_PROGRESS OAuthCustomResourceLogPolicy AWS::IAM::Policy Sun Dec 22 2019 12:18:10 GMT+0900 (Japan Standard Time) CREATE_IN_PROGRESS OAuthCustomResourceLogPolicy AWS::IAM::Policy Sun Dec 22 2019 12:18:12 GMT+0900 (Japan Standard Time) Resource creation Initiated ⠸ Updating resources in the cloud. This may take a few minutes... CREATE_COMPLETE OAuthCustomResourceLogPolicy AWS::IAM::Policy Sun Dec 22 2019 12:18:26 GMT+0900 (Japan Standard Time) ⠹ Updating resources in the cloud. This may take a few minutes... CREATE_IN_PROGRESS OAuthCustomResourceInputs Custom::LambdaCallout Sun Dec 22 2019 12:18:30 GMT+0900 (Japan Standard Time) ⠏ Updating resources in the cloud. This may take a few minutes... CREATE_IN_PROGRESS OAuthCustomResourceInputs Custom::LambdaCallout Sun Dec 22 2019 12:18:34 GMT+0900 (Japan Standard Time) Resource creation Initiated CREATE_COMPLETE OAuthCustomResourceInputs Custom::LambdaCallout Sun Dec 22 2019 12:18:34 GMT+0900 (Japan Standard Time) CREATE_COMPLETE amplify-sample-dev-120720-authsamplexxxxxxx AWS::CloudFormation::Stack Sun Dec 22 2019 12:18:38 GMT+0900 (Japan Standard Time) ⠙ Updating resources in the cloud. This may take a few minutes... CREATE_COMPLETE authsamplexxxxxxx AWS::CloudFormation::Stack Sun Dec 22 2019 12:18:50 GMT+0900 (Japan Standard Time) CREATE_IN_PROGRESS UpdateRolesWithIDPFunction AWS::Lambda::Function Sun Dec 22 2019 12:18:53 GMT+0900 (Japan Standard Time) ⠧ Updating resources in the cloud. This may take a few minutes... CREATE_IN_PROGRESS UpdateRolesWithIDPFunction AWS::Lambda::Function Sun Dec 22 2019 12:18:53 GMT+0900 (Japan Standard Time) Resource creation Initiated ⠙ Updating resources in the cloud. This may take a few minutes... CREATE_IN_PROGRESS authuserPoolGroups AWS::CloudFormation::Stack Sun Dec 22 2019 12:18:54 GMT+0900 (Japan Standard Time) CREATE_COMPLETE UpdateRolesWithIDPFunction AWS::Lambda::Function Sun Dec 22 2019 12:18:54 GMT+0900 (Japan Standard Time) CREATE_IN_PROGRESS authuserPoolGroups AWS::CloudFormation::Stack Sun Dec 22 2019 12:18:55 GMT+0900 (Japan Standard Time) Resource creation Initiated CREATE_IN_PROGRESS UpdateRolesWithIDPFunctionOutputs Custom::LambdaCallout Sun Dec 22 2019 12:18:57 GMT+0900 (Japan Standard Time) ⠦ Updating resources in the cloud. This may take a few minutes... CREATE_IN_PROGRESS amplify-sample-dev-xxxxxxx-authuserPoolGroups-xxxxxxxAWS::CloudFormation::Stack Sun Dec 22 2019 12:18:55 GMT+0900 (Japan Standard Time) User Initiated ⠹ Updating resources in the cloud. This may take a few minutes... CREATE_IN_PROGRESS UpdateRolesWithIDPFunctionOutputs Custom::LambdaCallout Sun Dec 22 2019 12:19:01 GMT+0900 (Japan Standard Time) Resource creation Initiated CREATE_COMPLETE UpdateRolesWithIDPFunctionOutputs Custom::LambdaCallout Sun Dec 22 2019 12:19:02 GMT+0900 (Japan Standard Time) ⠇ Updating resources in the cloud. This may take a few minutes... CREATE_IN_PROGRESS customerGroupRole AWS::IAM::Role Sun Dec 22 2019 12:18:59 GMT+0900 (Japan Standard Time) CREATE_IN_PROGRESS LambdaExecutionRole AWS::IAM::Role Sun Dec 22 2019 12:19:00 GMT+0900 (Japan Standard Time) CREATE_IN_PROGRESS LambdaExecutionRole AWS::IAM::Role Sun Dec 22 2019 12:19:01 GMT+0900 (Japan Standard Time) Resource creation Initiated CREATE_IN_PROGRESS customerGroupRole AWS::IAM::Role Sun Dec 22 2019 12:19:01 GMT+0900 (Japan Standard Time) Resource creation Initiated ⠋ Updating resources in the cloud. This may take a few minutes... CREATE_COMPLETE customerGroupRole AWS::IAM::Role Sun Dec 22 2019 12:19:16 GMT+0900 (Japan Standard Time) ⠹ Updating resources in the cloud. This may take a few minutes... CREATE_IN_PROGRESS customerGroup AWS::Cognito::UserPoolGroup Sun Dec 22 2019 12:19:19 GMT+0900 (Japan Standard Time) CREATE_COMPLETE LambdaExecutionRole AWS::IAM::Role Sun Dec 22 2019 12:19:20 GMT+0900 (Japan Standard Time) CREATE_IN_PROGRESS customerGroup AWS::Cognito::UserPoolGroup Sun Dec 22 2019 12:19:21 GMT+0900 (Japan Standard Time) Resource creation Initiated CREATE_COMPLETE customerGroup AWS::Cognito::UserPoolGroup Sun Dec 22 2019 12:19:21 GMT+0900 (Japan Standard Time) CREATE_IN_PROGRESS RoleMapFunction AWS::Lambda::Function Sun Dec 22 2019 12:19:23 GMT+0900 (Japan Standard Time) CREATE_IN_PROGRESS RoleMapFunction AWS::Lambda::Function Sun Dec 22 2019 12:19:23 GMT+0900 (Japan Standard Time) Resource creation Initiated ⠇ Updating resources in the cloud. This may take a few minutes... CREATE_COMPLETE RoleMapFunction AWS::Lambda::Function Sun Dec 22 2019 12:19:24 GMT+0900 (Japan Standard Time) ⠋ Updating resources in the cloud. This may take a few minutes... CREATE_IN_PROGRESS RoleMapFunctionInput Custom::LambdaCallout Sun Dec 22 2019 12:19:26 GMT+0900 (Japan Standard Time) ⠙ Updating resources in the cloud. This may take a few minutes... CREATE_IN_PROGRESS RoleMapFunctionInput Custom::LambdaCallout Sun Dec 22 2019 12:19:29 GMT+0900 (Japan Standard Time) Resource creation Initiated CREATE_COMPLETE RoleMapFunctionInput Custom::LambdaCallout Sun Dec 22 2019 12:19:30 GMT+0900 (Japan Standard Time) CREATE_COMPLETE amplify-sample-dev-xxxxxxx-authuserPoolGroups-xxxxxxx AWS::CloudFormation::Stack Sun Dec 22 2019 12:19:32 GMT+0900 (Japan Standard Time) ⠦ Updating resources in the cloud. This may take a few minutes... CREATE_COMPLETE authuserPoolGroups AWS::CloudFormation::Stack Sun Dec 22 2019 12:19:42 GMT+0900 (Japan Standard Time) ⠏ Updating resources in the cloud. This may take a few minutes... UPDATE_COMPLETE_CLEANUP_IN_PROGRESS amplify-sample-dev-120720 AWS::CloudFormation::Stack Sun Dec 22 2019 12:19:45 GMT+0900 (Japan Standard Time) UPDATE_COMPLETE amplify-sample-dev-120720 AWS::CloudFormation::Stack Sun Dec 22 2019 12:19:46 GMT+0900 (Japan Standard Time) ✔ All resources are updated in the cloud Hosted UI Endpoint: https://sampleac302e93-ac302e93-dev.auth.ap-northeast-1.amazoncognito.com/ Test Your Hosted UI Endpoint: https://sampleac302e93-ac302e93-dev.auth.ap-northeast-1.amazoncognito.com/login?response_type=code&client_id=3ad7h9ulvb6uqrmop29h7qrpvk&redirect_uri=https://example.com/ ``` # SIgnUp / SMSコードの送信 ではいよいよ、実際に電話番号でSignUpさせてみる。下記を実行すると指定の電話番号へ認証コードが送信されてくる。 ``` javascript import {Auth, API, graphqlOperation} from 'aws-amplify' import _ from 'lodash' async verifyPhoneNumber() { const next = async () => { // SignUp後の処理(パスコード入力画面へ遷移を想定) } if (_.isUndefined(this.phoneNumber) || _.isUndefined(_.isEmpty(this.phoneNumber))) return const userName = this.getUserName() const initPassword = this.initialPassword try { await Auth.signIn(this.getUserName(), this.initialPassword) / 以前に一度認証している場合はここ以降に記述 } catch (e) { const errCode = e.code // 電話番号の登録がされていない if (errCode === 'UserNotFoundException') { try { await Auth.signUp({ username: userName, password: initPassword, validationData: [] //optional }).then(() => next()) } catch (e) { const errCode = e.code if (errCode === 'InvalidParameterException') { // 電話番号形式じゃないのにSignUp実行するとThrowされる(入力段階で電話番号形式を保証すること) } } } // SMSコードでの電話番号検証が済んでいない else if (errCode === 'UserNotConfirmedException') { await Auth.resendSignUp(this.getUserName()).then(() => next()).catch(e => { const errCode = e.code // 一定時間でのSMSコードの送信可能数を超えた if (errCode === 'LimitExceededException') { } }) } // 電話番号は登録済みだが初期パスワードが既に変更されている(以前に一度認証を完了している) else if (errCode === 'NotAuthorizedException') { } } } ``` # SMSコードの検証 最後に送信された認証コードを検証する。 ```javascript async confirmVerifyCode() { const next = () => { // 認証コード疎通後の処理 } if (_.isUndefined(this.smsVerifyCode) || _.isUndefined(_.isEmpty(this.smsVerifyCode))) { return } const userName = this.getUserName() const op = { forceAliasCreation: true } await Auth.confirmSignUp(userName, this.smsVerifyCode, op).then(() => next()).catch(e => { const errCode = e.code this.isModalLoading = false if (errCode === 'CodeMismatchException') { // 入力された認証コードは誤っている } }) }, ``` 本来認証処理を実装するにはサーバサイドでパスコードの有効期限の監視やその他いろんな実装が必要になりますが、設問に答えてSDK呼びだけなので、かなり楽ですね。Amplify、感動です。
2020.01.29
Windows 10 上での VirtualBox と Docker の共存
Windows 10 上での VirtualBox と Docker の共存はまだできないようだ。 都度、切り替えが必要。 VirtualBox を起動させたいとき(Hyper-V無効化)、以下のコマンドを実行し Windows の再起動を行う。 ``` > bcdedit /set hypervisorlaunchtype off ``` Docker を起動させたいとき(Hyper-V 有効化)は、以下のコマンドを実行し Windows の再起動を行う。 ``` > bcdedit /set hypervisorlaunchtype auto ``` そのうち改善されるだろうか。
2020.01.24
Lodash.jsを使いこなしてJavaScript上級者を目指す
## 伝えたいこと * Lodash.jsは最高 * Lodash.jsを使うとコードがシンプルになる * Lodash.jsはメソッドチェインを使わないと魅力80%減 * Lodash.jsは最高 ## Lodash.jsとは? https://lodash.com/ * JavaScriptユーティリティライブラリ * つまり便利なライブラリ * 週2000万ダウンロードという超人気ライブラリ(jQueryは200万、Reactは500万) * Underscore.jsより高速かつ高機能 ## 使い方 まずnpmパッケージインストール。 ```bash npm i lodash # または yarn add lodash ``` それを読み込んで_(アンダーバー)で使う。 ```js import _ from 'lodash' const result = _.compact([0, 1, false, 2, '', 3]) console.log(result) // [1, 2, 3] ``` ## undefined/NaN対策 JavaScriptを書いていると苦労するのがundefinedやNaN対策。 ```js let arr; console.log(arr.length) // Error let obj; console.log(obj.hoge) // Error let num; console.log(1 / num) // NaN ``` エラーにならないように下記のような回避コードを書くことが多かった。 ```js let arr; console.log(Array.isArray(arr) ? arr.length : 0) // 0 let obj; if ( typeof obj === 'object' && obj !== null && !Array.isArray(obj) ) { console.log(obj.hoge) } else { console.log(undefined) // undefined } let num; console.log(num > 0 ? 1 / num : 1) // 1 ``` Lodash.jsならこの問題を下記のようにシンプルに解決できる。 ```js let arr; console.log(_.size(arr)) // 0 let obj; console.log(_.get(obj, "hoge")) // undefined let num; console.log(_.divide(1, num)) // 1 ``` もちろん、ちゃんとエラーを返してほしいケースもあるし、Lodash.jsの関数が返す返り値が微妙と感じるなら使わないほうがいいが、ほとんどのケースではLodash.jsを使ったほうがコードの可読性が上がる。 ## メソッドチェイン Lodash.jsはメソッドチェインという使い方ができる。 例えば下記のように、ある配列に対して様々な操作をしたい場合、無駄なコードが多くDRYではない。 ```js let result = [1,2,3,4,5] result = _.concat(result, [3,4,5,6,7]) // 別の配列を結合 result = _.uniq(result) // 重複する要素を削除 result = _.shuffle(result) // ランダムにシャッフル console.log(result) // [6,2,7,1,3,5,4] ``` このコードにメソッドチェインを用いると下記のように書ける。 ```js const result = _.chain([1,2,3,4,5]) .concat([3,4,5,6,7]) .uniq() .shuffle() .value() console.log(result) // [6,2,7,1,3,5,4] ``` メソッドチェインは可読性が上がるだけでなく、下記メリットがある。 * 変数再代入が発生しない * 変数の数を減らせる * 副作用が起こりにくい つまり、関数型プログラミングのようなメリットを享受できるのが特徴。 ## よく使う機能 特によく使う機能をいくつかご紹介。 ### _.compact 配列の中からfalsyな要素(0, false, ""など)をすべて除外してくれる ```js _.compact([0, 1, false, 2, '', 3]) // => [1, 2, 3] ``` ### _.difference 2つの配列のうち第一引数の配列にしかない要素を残す ```js _.difference([4, 2, 1], [2, 3]) // => [1, 4] ``` ### _.differenceBy 2つの配列を関数またはプロパティ名で比較して第一引数の配列にしかない要素を残す ```js _.differenceBy([2.1, 1.2], [2.3, 3.4], Math.floor) // => [1.2] const arr1 = [{ 'x': 2 }, { 'x': 1 }] const arr2 = [{ 'x': 1 }, { 'x': 3 }] _.differenceBy(arr1, arr2, 'x') // => [{ 'x': 2 }] ``` ### _.intersection 2つの配列の中で共通している要素を残す ```js _.intersection([2, 1], [2, 3]) // => [2] ``` ### _.union 2つの配列を重複のない状態で結合する ```js _.union([2], [1, 2]) // => [2, 1] ``` ### _.xor 2つの配列の中で共通している要素を除外し、片方にしか存在しない要素を残す ```js _.xor([2, 1], [2, 3]) // => [1, 3] ``` ### _.uniq 配列の中の重複する要素を1つだけ残して除外する ```js _.uniq([2, 1, 2]) // => [2, 1] ``` ### _.without 配列の中から指定した値を除外する ```js _.without([2, 1, 2, 3], 1, 2) // => [3] ``` ### _.flattenDeep ネストされた配列をすべてフラットにする ```js _.flattenDeep([1, [2, [3, [4]], 5]]) // => [1, 2, 3, 4, 5] ``` ### _.cloneDeep オブジェクトを複製する(ネストされたオブジェクトも含めて全て値渡しする) ```js const obj1 = { 'a': 1, 'b': { 'c': 2 } } const obj2 = obj1 obj2.b.c = 3 console.log(obj1.b.c) // 3 const obj3 = { 'a': 1, 'b': { 'c': 2 } } const obj4 = _.cloneDeep(obj3) obj4.b.c = 3 console.log(obj3.b.c) // 2 ``` ### _.isPlainObject 純粋なオブジェクト型かどうかを判定する ```js _.isPlainObject([1, 2, 3]) // => false _.isPlainObject({ 'x': 0, 'y': 0 }) // => true _.isPlainObject(Object.create(null)) // => true ``` ### _.round 指定した桁数で四捨五入する ```js _.round(4.006) // => 4 _.round(4.006, 2) // => 4.01 _.round(4060, -2) // => 4100 ``` ### _.range 指定した要素数でカウントアップまたはカウントダウンする配列を生成 ```js _.range(4) // => [0, 1, 2, 3] _.range(-4) // => [0, -1, -2, -3] _.range(1, 5) // => [1, 2, 3, 4] _.range(0, 20, 5) // => [0, 5, 10, 15] _.range(0, -4, -1) // => [0, -1, -2, -3] _.range(1, 4, 0) // => [1, 1, 1] _.range(0) // => [] ``` ### _.debounce 関数が呼び出されてから指定した秒数以内に再度関数が呼び出された場合は経過秒数をリセットし、関数を実行しない ```js function handleScroll () { console.log("handleScroll()") } const _handleScroll = _.debounce(handleScroll, 2000) window.addEventListener('scroll', _handleScroll) ```
2020.01.18
画像不要!CSSだけで三角形を表現する方法!
こんにちは。最近タピオカを自宅で茹でて食べているのですが、調子に乗ってたくさん食べたら消化不良で腹を下しました。みなさんも体調不良には気を付けましょう。 今日は僕もよく使う、CSSだけで三角形を表現する方法のご紹介です。 こちらのaタグをご覧ください ```HTML <a href='https://shima.io' id='shimbaLink'>SHIMBA</a> ``` 何の変哲もない、SHIMBAへリンクするだけのaタグです。 idに`shimbaLink`を持っています。 こちらの「SHIMBA」という文字のすぐ後ろに矢印のように三角形を表示させるには、次のようなCSSを設定すると可能になります。 ```CSS #shimbaLink:after{ content: ''; display: inline-block; width: 0; height: 0; border-style: solid; border-width: 5px 0 5px 10px; border-color: transparent transparent transparent #000; } ``` これだけで、「SHIMBA▶︎」のようにリンク文字の後ろに三角形が出現します! SCSSでカッコよく書くと次のような感じです。 ```SCSS #shimbaLink &:after{ content: ''; display: inline-block; width: 0; height: 0; border-style: solid; border-width: 5px 0 5px 10px; border-color: transparent transparent transparent #000; } } ``` ちなみに下のように、:after要素の代わりに:beforeを使うと、「▶︎SHIMBA」文字の前に三角形が出てきます! ```CSS #shimbaLink:before{ content: ''; display: inline-block; width: 0; height: 0; border-style: solid; border-width: 5px 0 5px 10px; border-color: transparent transparent transparent #000; } ``` 「サイズも色も三角の向きも使いたいのと全然違って参考にならな〜〜〜い!」というあなたには、以下のツールが便利です。 http://apps.eky.hk/css-triangle-generator/ 向き、大きさ、色を指定することで、コピペで使える三角系CSSが自動で生成されます! ここで作成したコードを、上の例達の「width: 0;」以下と置き換えると三角形が出てきます。 グラデーションも指定できるのでとっても便利です。 ブックマーク登録して、タピオカミルクティをすすりながら、素敵なフロントエンド生活をしましょう!
2019.12.25