20210613のvue.jsに関する記事は7件です。

推しへの愛を叫べ!推し愛メーター

クラファン作成物への一歩 近々自作アイドルグッズをクラファンにかけようと思っています。 そのための第一歩として今回のものを作成しました。 クラファンは推しの声を自作グッズに閉じ込めるものを作成しようと思っています。 今回は声を数値化するものを作成しました。 推しへの愛ありますか? 皆さんの推しへの愛はどのくらいですか? その愛を声量に変えて可視化できるwebアプリケーションを作成しました。 ソースコード <div id="app"> <p> 推しを聞き取る※開発中<br> <button @click="listen">{{ recogButton }}</button> {{ result }} </p> </div> <div class="level-monitor"> <div class="indicator"></div> </div> <div class="percent"></div> <input type="button" value="推しへの愛を叫ぶ" onclick="main()"> :root { --opacity: 0.6; --radius: 0.2; --red-limit: 70%; --yellow-limit: 30%; } .level-monitor { width: 50px; height: 300px; background-color: pink; position: relative; border-radius: var(--radius); } .indicator { width: 80%; background-color: rgba(0, 0, 0, 0.3);*/ background-attachment: fixed; position: absolute; bottom: 0; left: 10%; border-radius: var(--radius); } .percent { width: 50px; font-family: Verdana, Geneva, Tahoma, sans-serif; font-size: 18px; text-align: center; padding: 5px 0; } const app = new Vue({ el: '#app', data: { recogButton: '聞き取り開始!', recog: null, result: '', speech: null, message: '', changecolor: 'red', }, mounted() { // 音声認識の準備 const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition || window.mozSpeechRecognition || window.msSpeechRecognition || null; this.recog = new SpeechRecognition(); this.recog.lang = 'ja-JP'; this.recog.interimResults = false; this.recog.continuous = false; // 音声認識が開始されたら this.recog.onstart = () => { this.result = ''; this.recogButton = 'ききとりちゅう…'; }; // 音声を認識できたら this.recog.onresult = (event) => { // 認識されしだい、this.resultにその文字をいれる // Vueなので、文字をいれただけで画面表示も更新される this.result = event.results[0][0].transcript; }; // 音声認識が終了したら this.recog.onspeechend = () => { this.recog.stop(); this.recogButton = '停止(クリックして再開)'; }; // 認識できなかったら this.recog.onerror = () => { this.result = '(認識できませんでした)'; this.recog.stop(); this.recogButton = '停止(クリックして再開)'; }; }, methods: { // 認識(聞き取り) listen() { this.recog.start(); }, // 認識(聞き取り) changecolor() { this.recog.start(); }, }, }); let maxvolume = 0; // Copied from https://stackoverflow.com/a/52952907/1204375 async function listen(cb) { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }) const audioContext = new AudioContext(); const analyser = audioContext.createAnalyser(); const microphone = audioContext.createMediaStreamSource(stream); const javascriptNode = audioContext.createScriptProcessor(2048, 1, 1); analyser.smoothingTimeConstant = 0.8; analyser.fftSize = 1024; microphone.connect(analyser); analyser.connect(javascriptNode); javascriptNode.connect(audioContext.destination); javascriptNode.onaudioprocess = function () { const array = new Uint8Array(analyser.frequencyBinCount); analyser.getByteFrequencyData(array); let values = 0; const length = array.length; for (let i = 0; i < length; i++) { values += (array[i]); } const average = Math.round(values / length); cb(average); } } function updateBarIndicator(average) { const $el = document.querySelector('.indicator'); if(maxvolume<average){ maxvolume = average $el.style.height = `${maxvolume}%`; } } function updateTextIndicator(average) { const $el = document.querySelector('.percent'); $el.textContent = `${maxvolume}%`; } function main() { listen((percent) => { updateBarIndicator(percent); updateTextIndicator(percent); }); } 今回は以下のコードを参考に書きました。 レビュー 今回は母と妹にレビューをお願いしました。 前提として自分がなぜこれを作ったのかを説明しました。 フィードバック ・上のボタンは下のグラフには何も影響しないのか? ・機能がすくない ・Webアプリケーションと呼んでいいのか? かなり酷なフィードバックをいただきました。 上のボタンとグラフが連動しなかったのはソースコードを見ておかしいとおもったかたもいるかもしれませんがJavaScriptとVue.jsが混在しているんです。 サンプルを参照して作ったのですが変換がうまくいかず相互の関係がないものになってしまいました。 ほかの二つはもうぐうの音もでませんでした。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TerraformとCognitoとVue.jsで認証機能付きサーバレスWebアプリを構築する

はじめに 記事タイトルの通り、CognitoはWebアプリ等の認証機能をサーバレスでお手軽に作ることができる。 では、実際どれくらいお手軽に作れるかを試してみよう。 なお、Cognito自体はお手軽なものの、Webアプリの基本の部分は結構使うことになる。 過去、記事でまとめた基本要素は、記事中に都度リンクを貼るので、今回は詳しくは説明しない。 前提となる基本知識としては以下だ。 Vue.jsの基本 API GatewayにおけるCORSの対応 S3の静的コンテンツのウェブサイトホスティングの基本 やりたいこと&構成図 今回はシンプルに作るため、未認証時にはログイン画面を表示し、あらかじめCognitoに登録しているユーザIDで認証をしたら、該当ユーザの情報をDynamoDBから取得して画面に表示するといった簡単なアプリにする。 これを、以下のような構成で作っていく。 ①静的コンテンツ 静的コンテンツは、S3のウェブサイトホスティング機能を使う。 今回やりたいことからすると必須ではないが、CloudFrontを使ってS3のPrivateの状態のまま使えるようにしておこう。 このあたりの設定方法は、以下の記事を参考にしていただきたい。 S3の静的WebサイトホスティングをCloudFrontでキャッシュしてみる 今回のコンテンツは、 サインイン後のコンテンツ: index.html 上記のWebアプリ定義: app.js サインイン用のコンテンツ: signin.html 上記のWebアプリ定義: signin.js とする。Vue.js使うのにシングルページじゃないのかよ!というツッコミは無用で……。 簡易に対応するために、CDN版のVue.jsを使っている。 なお、あまり参考にならないかもしれないが、Vue.jsの基本は以下の記事あたりで解説している。 EC2作成直後の状態から最速でVue.js(CDN版)のWebサイトを立ち上げる サインイン後のコンテンツ index.html <html> <head> <style> [v-cloak] { display: none } </style> <meta charset="utf-8"> <title>Cognitoお試し</title> </head> <body> <div id="myapp"> <table border="1" v-if="employee_info" v-cloak> <tr><th>id</th><th>name</th><th>age</th></tr> <tr v-for="item in items"><td>{{ item['id'] }}</td><td>{{ item['name'] }}</td><td>{{ item['age'] }}</td></tr> </table> </div> <!-- Vue.js/axios を読み込む --> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.js"></script> <script src="https://cdn.jsdelivr.net/npm/aws-sdk/dist/aws-sdk.js"></script> <script src="https://cdn.jsdelivr.net/npm/amazon-cognito-identity-js/dist/amazon-cognito-identity.js"></script> <script src="app.js"></script> </body> </html> これ自体は特別なことはしていない。 この後使う機能で、AWSとCognitoのSDKを使うことになるので、上記のようにCDNからそれぞれロードしておく。 app.js const app = new Vue({ el: '#myapp', data: function () { return { employee_info: false, items: null, token: '', cognito_userdata: {} } }, created: function () { AWS.config.region = 'ap-northeast-1' AWS.config.credentials = new AWS.CognitoIdentityCredentials({ IdentityPoolId: '${cognito_identity_pool_id}' }) const poolData = { UserPoolId: '${cognito_user_pool_id}', ClientId: '${cognito_user_pool_client_id}' } const userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData) const cognitoUser = userPool.getCurrentUser() if (cognitoUser == null) { window.location.href = 'signin.html' } else { cognitoUser.getSession((err, session) => { if (err != null) { console.log(err) window.location.href = 'signin.html' } else { this.token = session.idToken.jwtToken cognitoUser.getUserAttributes((err, result) => { if (err) { console.log(err) window.location.href = 'signin.html' } else { for (let i = 0; i < result.length; i++) { this.$set(this.cognito_userdata, result[i].getName(), result[i].getValue()) } axios .get('${apigateway_invoke_url}/employee', { headers: { Authorization: this.token }, params: { id: this.cognito_userdata['custom:id'] } }) .then(response => { this.items = response.data this.employee_info = true }) } }) } }) } } }) app.$mount('#myapp') さて、ここではSDKを使いまくっている。 まずは、以下の部分で接続の設定を行う。 IdentityPoolId は②で取得するのでそこで解説する。なお、都度コンテンツを書き直すのは面倒なので、${cognito_identity_pool_id} の部分は、Terraformのtemplate_fileを使って、作成したリソースを参照した値で置換してS3にアップロードするようにしておくと楽で良い。 AWS.config.region = 'ap-northeast-1' AWS.config.credentials = new AWS.CognitoIdentityCredentials({ IdentityPoolId: '${cognito_identity_pool_id}' }) 次に、認証状態の確認だ。 認証ができていると、AmazonCognitoIdentity.CognitoUserPool() でCognitoのユーザプールを参照し、ユーザプールに接続している現在のユーザの情報をuserPool.getCurrentUser() で取得する。 未認証の場合は、この戻り値がnullになるため、それをハンドリングすることで、認証時と未認証時の動作を振り分けることが可能だ。 今回は、未認証の場合は、signin.htmlを表示するようにしている。 なお、${cognito_user_pool_id}と${cognito_user_pool_client_id}もTerraformのtemplate_fileで参照を行うと楽だ。 const poolData = { UserPoolId: '${cognito_user_pool_id}', ClientId: '${cognito_user_pool_client_id}' } const userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData) const cognitoUser = userPool.getCurrentUser() if (cognitoUser == null) { window.location.href = 'signin.html' } else { さて、認証ができていた場合は、getSession() でセッション情報を取得できる。 セッション情報には、CognitoのIDトークン、アクセストークン、リフレッシュトークンが入っている。APIアクセスではIDトークンが必要になるので、session.idToken.jwtTokenをdataのトークン情報に入れておこう。 cognitoUser.getSession((err, session) => { if (err != null) { console.log(err) window.location.href = 'signin.html' } else { this.token = session.idToken.jwtToken さらに、getUserAttributes()でトークンに入っているユーザ情報から属性を取り出すことができる。 resultはJSON形式ではないので、ここで使いやすくJSON形式にしておいてあげよう。 cognitoUser.getUserAttributes((err, result) => { if (err) { console.log(err) window.location.href = 'signin.html' } else { for (let i = 0; i < result.length; i++) { this.$set(this.cognito_userdata, result[i].getName(), result[i].getValue()) } 最後に、ここで取得したトークンとユーザIDをAPIに送ることで、認証後のコンテンツの画面を表示することが可能だ。 サインイン用のコンテンツ signin.html <html> <head> <style> [v-cloak] { display: none } </style> <meta charset="utf-8"> <title>Cognitoサインインお試し</title> </head> <body> <div id="myapp"> <div> <input type="text" v-model="cognito_id" placeholder="ID"> </div> <div> <input type="text" v-model="cognito_pwd" placeholder="パスワード"> </div> <button v-on:click="signin" v-bind:disabled="is_invalid">サインイン</button> </div> <!-- Vue.js/axios を読み込む --> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script src="https://cdn.jsdelivr.net/npm/aws-sdk/dist/aws-sdk.js"></script> <script src="https://cdn.jsdelivr.net/npm/amazon-cognito-identity-js/dist/amazon-cognito-identity.js"></script> <script src="signin.js"></script> </body> </html> signin.js const app = new Vue({ el: '#myapp', data: { cognito_id: '', cognito_pwd: '', is_invalid: true }, watch: { cognito_id: function (newVal, oldVal) { this.is_invalid = (newVal.length === 0 && this.cognito_pwd.length) }, cognito_pwd: function (newVal, oldVal) { this.is_invalid = (newVal.length === 0 && this.cognito_id.length) } }, methods: { signin: function () { AWS.config.region = 'ap-northeast-1' AWS.config.credentials = new AWS.CognitoIdentityCredentials({ IdentityPoolId: '${cognito_identity_pool_id}' }) const poolData = { UserPoolId: '${cognito_user_pool_id}', ClientId: '${cognito_user_pool_client_id}' } const userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData) const authenticationData = { Username: this.cognito_id, Password: this.cognito_pwd } const authenticationDetails = new AmazonCognitoIdentity.AuthenticationDetails(authenticationData) const userData = { Username: this.cognito_id, Pool: userPool } const cognitoUser = new AmazonCognitoIdentity.CognitoUser(userData) cognitoUser.authenticateUser(authenticationDetails, { onSuccess: function (result) { window.location.href = 'index.html' }, onFailure: function (err) { console.log(err) } }) } } }) app.$mount('#myapp') `` こちらも app.js 同様に、 ```Javascript AWS.config.region = 'ap-northeast-1' AWS.config.credentials = new AWS.CognitoIdentityCredentials({ IdentityPoolId: '${cognito_identity_pool_id}' }) const poolData = { UserPoolId: '${cognito_user_pool_id}', ClientId: '${cognito_user_pool_client_id}' } const userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData) でユーザプールにアクセスする。 その後、以下のようにして認証を行い、成功時は元のコンテンツに飛ばすようにすれば良い。 const authenticationData = { Username: this.cognito_id, Password: this.cognito_pwd } const authenticationDetails = new AmazonCognitoIdentity.AuthenticationDetails(authenticationData) const userData = { Username: this.cognito_id, Pool: userPool } const cognitoUser = new AmazonCognitoIdentity.CognitoUser(userData) cognitoUser.authenticateUser(authenticationDetails, { ②ユーザ認証 さて、ここからが本番のCognitoのTerraformの記述となる。 まずは、以下のようにユーザプールを作成する。 IDについては、sub という払い出しIDが作成されるが、自分で払い出したい場合は以下のように schema で定義しよう。 なお、developer_only_attribute を true にすると、トークン情報に入らなくなるので注意しよう。 また、自分で定義したスキーマ情報については、custom:id といったかたちでトークン情報に入るため、注意しよう。 ```HCL resource "aws_cognito_user_pool" "example" { name = local.cognito_userpool_name schema { name = "id" attribute_data_type = "String" developer_only_attribute = false mutable = true required = false string_attribute_constraints { min_length = 5 max_length = 5 } } } 今回は、以下のようにCLIを使ってあらかじめユーザを払い出しておくようにする。 id は、この後定義する DynamoDB に合わせておく。 locals { cognito_users = [ { "user_name" = "sampleuser00001@google.com", "id" = "00001" }, { "user_name" = "sampleuser00002@google.com", "id" = "00002" }, { "user_name" = "sampleuser00003@google.com", "id" = "00003" }, { "user_name" = "sampleuser00004@google.com", "id" = "00004" }, { "user_name" = "sampleuser00005@google.com", "id" = "00005" }, ] } resource "null_resource" "create_user" { depends_on = [aws_cognito_user_pool.example] for_each = { for cognito_user in local.cognito_users : cognito_user.id => { user_name = cognito_user.user_name id = cognito_user.id } } provisioner "local-exec" { command = <<-EOF aws cognito-idp admin-create-user --user-pool-id ${aws_cognito_user_pool.example.id} --username ${each.value.user_name} --user-attributes Name=custom:id,Value=${each.value.id} && aws cognito-idp admin-set-user-password --user-pool-id ${aws_cognito_user_pool.example.id} --username ${each.value.user_name} --password Pass1234! --permanent EOF on_failure = fail } } 次に、クライアント(アプリ情報)の登録をする。 ここで払い出されたクライアントIDと、IDプールが、先ほどのVue.js内で設定した情報とリンクするのである。 なお、aws_cognito_user_pool_clientのsupported_identity_providers は、他のIdPも指定可能だが、今回はCognitoの機能を利用するため以下の通りとする。OpenID2.0の認証をするためには、以下の通り設定しておけば良い。 resource "aws_cognito_user_pool_client" "example" { user_pool_id = aws_cognito_user_pool.example.id name = local.cognito_client_name supported_identity_providers = ["COGNITO"] allowed_oauth_flows_user_pool_client = true allowed_oauth_flows = ["implicit"] allowed_oauth_scopes = ["openid"] explicit_auth_flows = [ "ALLOW_CUSTOM_AUTH", "ALLOW_REFRESH_TOKEN_AUTH", "ALLOW_USER_SRP_AUTH", ] callback_urls = ["https://${aws_cloudfront_distribution.s3_contents.domain_name}/"] } resource "aws_cognito_identity_pool" "example" { identity_pool_name = local.cognito_idpool_name allow_unauthenticated_identities = true allow_classic_flow = false cognito_identity_providers { client_id = aws_cognito_user_pool_client.example.id provider_name = aws_cognito_user_pool.example.endpoint } } ③API アクセス APIアクセス特に難しいことはしていない。 今回は、GETリクエストを飛ばしてくるので、Lambda側でCORSの対応が必要なのと、Chromeは今回のアクセスパターンでもOPTIONSのプリフライトリクエストを飛ばしてきたので、モック統合を使って処理できるようにしておこう。 モック統合によるOPTIONSメソッドのCORS対応は、以下の記事を参照。 Amazon API GatewayでサクッとCORS対応する また、Lambdaは以下のようにPythonでテキトーに定義した。 DynamoDBのテーブル名とCORSのオリジン名は、環境変数で渡して書き換え不要にしてある。 import os import json import boto3 from boto3.dynamodb.conditions import Key dynamodb = boto3.resource('dynamodb') table = dynamodb.Table(os.environ['DYNAMODB_TABLE_NAME']) def get_item(id): try: response = table.query( KeyConditionExpression=Key('id').eq(id) ) return response['Items'] except Exception as error: print(error) raise Exception('DynamoDB Error') def lambda_handler(event, context): print(event) status_code = 200 items = {} try: event['queryStringParameters']['id'] except: status_code = 400 if status_code == 200: try: items = get_item(event['queryStringParameters']['id']) except: status_code = 500 return { 'isBase64Encoded': False, 'statusCode': status_code, 'headers': { "Access-Control-Allow-Headers" : "*", "Access-Control-Allow-Origin": os.environ['CORS_ORIGIN'], "Access-Control-Allow-Methods": "GET" }, 'body': json.dumps(items) } DynamoDBは以下のように定義している。 resource "aws_dynamodb_table" "employee" { name = local.dynamodb_table_name billing_mode = "PAY_PER_REQUEST" hash_key = "id" attribute { name = "id" type = "S" } } locals { dynamodb_items = [ { "id" = "00001", "name" = "Taro", "age" = "45" }, { "id" = "00002", "name" = "Jiro", "age" = "42" }, { "id" = "00003", "name" = "Saburo", "age" = "40" }, { "id" = "00004", "name" = "Shiro", "age" = "35" }, { "id" = "00005", "name" = "Goro", "age" = "30" }, ] } resource "aws_dynamodb_table_item" "employee" { for_each = { for dynamodb_item in local.dynamodb_items : dynamodb_item.id => { id = dynamodb_item.id name = dynamodb_item.name age = dynamodb_item.age } } table_name = aws_dynamodb_table.employee.name hash_key = aws_dynamodb_table.employee.hash_key range_key = aws_dynamodb_table.employee.range_key item = <<ITEM { "id": {"S": "${each.value.id}"}, "name": {"S": "${each.value.name}"}, "age": {"S": "${each.value.age}"} } ITEM } 最後に、API GatewayのAPIを直接実行してしまうことができないように、APIにオーソライザーを設定しよう。 resource "aws_api_gateway_method" "employee_get" { rest_api_id = aws_api_gateway_rest_api.contents.id resource_id = aws_api_gateway_resource.employee.id http_method = "GET" authorization = "COGNITO_USER_POOLS" authorizer_id = aws_api_gateway_authorizer.cognito.id } resource "aws_api_gateway_authorizer" "cognito" { rest_api_id = aws_api_gateway_rest_api.contents.id name = "CognitoAuthorizer" type = "COGNITO_USER_POOLS" provider_arns = [aws_cognito_user_pool.example.arn] } これで、直接APIを実行しても $ curl -i https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/employee?id=00005 HTTP/2 401 date: Sun, 13 Jun 2021 12:34:59 GMT content-type: application/json content-length: 26 x-amzn-requestid: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx x-amzn-errortype: UnauthorizedException x-amz-apigw-id: xxxxxxxxxxxxxxxx {"message":"Unauthorized"} と、エラーになるようになった。もちろん、トークンを直接貼り付ければアクセスできるし、トークンを改ざんしてアクセスしようとすると、{"Message":"Access Denied"}が返されるようになった(HTTPステータスコードは403)。この時の応答は、APIに入ってくる前なので、何もしないとCORS対応のヘッダが設定されない。必要であれば、aws_api_gateway_gateway_responseで返却するようにしておこう。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

vue-realworld-example-appを読んでみた【ベストプラクティス】

コードリーディングを行ったリポジトリはこちらです。 gothinkster/vue-realworld-example-app codesandbox 更新履歴 2021/6/15 誤字脱字の修正、わかりにくい表現の見直し。全体的な内容の追加。 2021/6/13 初版 ?はじめに ? 記事の背景 vue-realworld-example-app は、実際のアプリで使用する機能(CRUD、認証、ルーティングなど)を盛り込んだVue.jsプロジェクトである。Webアプリケーションの基本的な機能が一通りまとまっている。 本記事は、一般的なVue.jsプロジェクトはどのような構成になっているか、どのような技術が使われているかを知るために、vue-realworld-example-app をコードリーディングし、まとめたものである。 ? ( 参考 前提知識 ) コードリーディングを行うにあたる前提知識として、以下が必要と感じた。 Vue.js v2 (Router、Vuex)の基礎知識 Vue CLI の基礎知識 また、知っているとよいと感じた知識は以下。(調べながらで読めると思います) REST API の基礎知識 JWTの基礎知識 ローカルストレージの基礎知識 ?全体構成の確認 ?フォルダ構成 /src構成と役割は以下の通り。 App.vue、 main.js アプリのエントリーポイント。アプリ全体の設定などはここで行う。 /views 表示する画面のコンポーネントを管理する。ルーティングと対応している。 /router Vue Routerによるルーティングの設定を行っている。 /components アプリ内で使用するコンポーネントを管理する。 /store 状態管理を行うVuexのフォルダ。/views, /components内のから呼ばれる。 /common API、JWT、フィルタ、設定などのアプリ全体で使用する共通機能。 ?データの流れ(参考) データの流れを整理した。わかりにくいかもしれないので参考までに。 ?コンポーネント構成 コンポーネント構成は下記の通り。「緑=/views、黄色=/components」である。/ruterでルーティングされているコンポーネントは/viewsに、それ以外は/componentsで管理している。 ? /store, /commonの構成 /store, /commonの関係性は下記のとおり。 /storeは、4つのモジュールをexport している。それぞれのモジュールは、state, getters, actions, mutationsを持ち、状態管理を行う。状態管理を行う中で、インターフェースのような役割を果たすtypeから関数名を参照したり、共通処理を/commonを参照し、実行している。 /commonは、/storeで使用する共通処理ファイル(.service.js)とその設定ファイル(config.js)、アプリ全体で使用するフィルターファイル(.filter.js)を持つ。 ? ソースコード詳細 各ソースコードの気になったところを抜粋して解説する。 https://github.com/gothinkster/vue-realworld-example-app ? main.js、App.vue ? main.js フィルターの定義 import DateFilter from "./common/date.filter"; import ErrorFilter from "./common/error.filter"; Vue.filter("date", DateFilter); Vue.filter("error", ErrorFilter); アプリ全体で使用する日付変換(DateFilter)や エラー変換(ErrorFilter)のフィルターはここで設定する。フィルターそのものの定義は/commonでしている。 API処理の初期化 import ApiService from "./common/api.service"; ApiService.init(); サーバとのHTTP通信のための初期化を行う。 ApiService.init()では、vue-axiosプラグインを適用し、デフォルトURLの設定をしている。 ページロードごとの認証処理 import { CHECK_AUTH } from "./store/actions.type"; router.beforeEach((to, from, next) => Promise.all([store.dispatch(CHECK_AUTH)]).then(next) ); router.beforeEach()はページロードごとに呼ばれる関数。Promise.all()は、配列を引数にとり、すべてのPromiseを実行する。 ページロードごとにトークンの認証処理をしている。 参考 Vue公式 ナビゲーションガード 参考 MDN Promise.all() ? App.vue ヘッダ、フッタコンポーネントの使用 <template> <div id="app"> <RwvHeader /> <router-view /> <RwvFooter /> </div> </template> ヘッダ、フッタはアプリ全体で表示するため、ここで使用する。 ? / router ? index.js childrenによるネストルーティング routes: [ { path: "/", component: () => import("@/views/Home"), children: [ { path: "", name: "home", component: () => import("@/views/HomeGlobal") }, ... ], ... }, ... ] childrenを使用することで、ネストされたルートとなる。 またnameをつけることで、<router-link :to={ name: 'home' }>と指定できる。 ? /views ? / views / Home.vue 周辺 Home.vue ルーティング概要 <router-link>で HomeGlobal、HomeMyFeed、HomeTag に飛ばすリンクを設定し、 <router-view>で表示する仕組み。 データ(tag)の流れ <script> import { mapGetters } from "vuex"; import { FETCH_TAGS } from "@/store/actions.type"; export default { // ... mounted() { this.$store.dispatch(FETCH_TAGS); }, computed: { ...mapGetters(["isAuthenticated", "tags"]), tag() { return this.$route.params.tag; } } }; </script> mounted()はVuexのactionを使用してFETCH_TAGSを実行し、サーバからtagsを取得する。取得したtagsは、Vuexのstateで管理されるので、mapGetter()で取得し、コンポーネント内で使用している。 mapGetters()は引数で指定したstateの値を取得する関数。import {mapGetters} from "vue"を使用してインポートし、computedで使用する。 HomeGlobal.vue, HomeMyFeed.vue, HomeTag.vue template構成 <template> <div class="home-global"><RwvArticleList type="all" /></div> <!-- <div class="home-my-feed"><RwvArticleList type="feed" /></div> <div class="home-tag"><RwvArticleList :tag="tag"></RwvArticleList></div> --> </template> 上記3つの<template>はどれも似た構成。@/components/ArticleList.vueを属性 ( type )を変えて使用している。 ?/ views / Login.vue , Register.vue, Settings.vue Login.vue フォームとsubmit ボタン <form @submit.prevent="onSubmit(email, password)"> ... </form> v-modelでバインディングして、ボタン押下すると値が送信されるごくごく一般的なフォームの実装となっている。@submit.preventはevent.preventDefault()を呼び出す処理。これにより、フォーム送信後もページのリロードは行われない。.preventはイベント修飾子と呼ばれる。 ログイン成功時、ホーム画面へ import { mapState } from "vuex"; import { LOGIN } from "@/store/actions.type"; export default { ... data() { return { email: null, password: null }; }, methods: { onSubmit(email, password) { this.$store .dispatch(LOGIN, { email, password }) .then(() => this.$router.push({ name: "home" })); } }, ... }; ログインに成功したら、.then(() => { this.$router.push({ name: "home" })});を行い、ホームに飛ばす処理となっている。 state の加工 import { mapState } from "vuex"; export default { ... computed: { // auth.errors を errors に代入する処理。 // ↓ 加工後の名称: state => 加工処理(state.加工対象のステート名) ...mapState({ errors: state => state.auth.errors }) } }; 上記はauth.errors を errors に代入する処理。 Register.vue Login.vueとほぼ同じ構成。違いは、フォームにusernameの欄ができて、Vuexのactionが、LOGINからREGISTERになったくらい。 Setting.vue Login.vueやRegister.vueとほぼ同じ構成。 ?/ views / Profile.vue 周辺 Profile.vue ルーティング概要 <router-link>で ProfileArticles、ProfileFavorited に飛ばすリンクを設定し、<router-view>で表示する仕組み。 認証状態に依存する画面表示 <div v-if="isCurrentUser()"> <router-link class="btn btn-sm btn-outline-secondary action-btn" :to="{ name: 'settings' }" > ... </router-link> </div> <div v-else> <button class="btn btn-sm btn-secondary action-btn" v-if="profile.following" @click.prevent="unfollow()" > ... </button> <button class="btn btn-sm btn-outline-secondary action-btn" v-if="!profile.following" @click.prevent="follow()" > ... </button> </div> <div v-if="isCurrentUser()">を使用することで、ログイン中のユーザかどうかで表示画面が変わる実装となっている。v-if="profile.following"部分も同様で、フォローしているかどうかで表示画面が変わる。 mounted()を使用した初期化処理 import { FETCH_PROFILE, FETCH_PROFILE_FOLLOW, FETCH_PROFILE_UNFOLLOW } from "@/store/actions.type"; export default { ... mounted() { this.$store.dispatch(FETCH_PROFILE, this.$route.params); }, ... watch: { $route(to) { this.$store.dispatch(FETCH_PROFILE, to.params); } } }; 初期化時にmounted()で、ページのユーザ名をパラメータから取得している。 Vue.jsではパラメータが #/@hoge から #/@huga へ遷移するときに同じコンポーネントインスタンスが再利用されるのでmounted()が実行されない。そこで、watch: $route(to){...}を使用して、パラメータの検知をおこなっている。参考 ProfileArticles.vue、ProfileFavorited.vue <template> <div class="profile-page"> <RwvArticleList :author="author" :items-per-page="5"></RwvArticleList> <!-- <RwvArticleList :favorited="favorited" :items-per-page="5"> </RwvArticleList> --> </div> </template> templateのはどれも似た構成。@/components/ArticleListを呼び、属性 ( author, favorited )を変えているだけ。 ?/ views / Article.vue ナビゲーションガードを用いたデータ取得 import store from "@/store"; import { FETCH_ARTICLE, FETCH_COMMENTS } from "@/store/actions.type"; export default { //... beforeRouteEnter(to, from, next) { Promise.all([ store.dispatch(FETCH_ARTICLE, to.params.slug), store.dispatch(FETCH_COMMENTS, to.params.slug) ]).then(() => { next(); }); } //... } beforeRouteEnter()を用いて、FETCH_ARTICLE、FETCH_COMMENTSを呼ぶ。articleとcommentの最新を取得しstateを更新する。 v-htmlを用いた画面表示 <div v-html="parseMarkdown(article.body)"></div> import marked from "marked"; // ... export default { // ... methods: { parseMarkdown(content) { return marked(content); } } } markedはマークダウン解析ツール、htmlを返すため、v-htmlディレクティブを使用している。 ?/ views / ArticleEdit.vue ナビゲーションガードを用いたデータ取得 // [/editor/:slug] => [/editor] の場合、エディタを空にして表示する。 // beforeRouteUpdateは、パラメータが変わったタイミングでも実行される。 async beforeRouteUpdate(to, from, next) { await store.dispatch(ARTICLE_RESET_STATE); return next(); }, // [/editor] の場合、下記のifがfalseになり、実行されない。 // [/editor/:slug] の場合、下記のifがtrueになり、実行される。 async beforeRouteEnter(to, from, next) { await store.dispatch(ARTICLE_RESET_STATE); if (to.params.slug !== undefined) { await store.dispatch( FETCH_ARTICLE, to.params.slug, to.params.previousArticle ); } return next(); }, // [/editor/:slug] から去るときエディタを空にする。 async beforeRouteLeave(to, from, next) { await store.dispatch(ARTICLE_RESET_STATE); next(); }, ルートが変更したら実行されるナビゲーションガードで制御する。ARTICLE_RESET_STATEで記事エディタを空に更新するが、/editor/slugのような場合は、元記事が記載されたまま表示する。 ※ ちなみに上記のナビゲーションガードは、パラメータが変わったタイミングでは実行されないので、 /editor から/editor/:slugに遷移した場合、元記事が取得できない。 参考:https://tsudoi.org/weblog/5738/ 参考:https://router.vuejs.org/ja/guide/advanced/navigation-guards.html タグ登録処理 <template> ... <input type="text" class="form-control" placeholder="Enter tags" v-model="tagInput" @keypress.enter.prevent="addTag(tagInput)" /> <div class="tag-list"> <span class="tag-default tag-pill" v-for="(tag, index) of article.tagList" :key="tag + index" > <i class="ion-close-round" @click="removeTag(tag)"> </i> {{ tag }} </span> </div> ... </template> @keypress.enter.prevent="addTag(tagInput)"のように、Enter押下時にタグ登録処理を行う実装となっている。 ? /components ?/ components / TheHeader.vue ヘッダを表示するコンポーネント。 認証状態に応じた画面表示 <ul v-if="!isAuthenticated" class="nav navbar-nav pull-xs-right"> .... </ul> <ul v-else class="nav navbar-nav pull-xs-right"> .... </ul> 認証状態によって表示する画面をv-ifで切り替えている。 router-linkのパラメータ指定 <router-link class="nav-link" active-class="active" exact :to="{ name: 'profile', params: { username: currentUser.username } }" > {{ currentUser.username }} </router-link> <router-link>では:to="{params:{...}}でパラメータを指定することができる。 ? / components / VTag.vue タグを表示するコンポーネント。 v-text を用いた画面出力 <router-link :to="homeRoute" :class="className" v-text="name"></router-link> v-text="name"は{{ name }}と同じ propsの型指定 export default { props: { name: { type: String, required: true }, className: { type: String, default: "tag-pill tag-default" } } // ... } propsでは、型の指定(type)、必須項目指定(required)、デフォルト値(default)などプロパティの型を指定できる。以降で説明するコンポーネントのpropsにおいても、型の指定を必ず行っていた。 ? / components / ArticleMeta.vue 周辺 ArticleMeta.vue 記事作成者の情報などで構成されるコンポーネント。 フィルタの使用 <span class="date">{{ article.createdAt | date }}</span> main.jsで定義したフィルターが使用されている。 子コンポーネントでの制御 <rwv-article-actions v-if="actions" :article="article" :canModify="isCurrentUser()" ></rwv-article-actions> フォロー、いいねボタンはArticleActionsコンポーネントで制御している。 ArtcleActions.vue フォローやいいねボタンを構成するコンポーネント ユーザ状態に応じた画面表示 <span v-if="canModify"> <!-- Edit Article, DeleteArticleボタンが表示 --> </span> <span v-else> <!-- Follow, Favoriteボタンが表示 --> </span> ユーザが記事の作成者か否かで表示画面を変えている。 また、classを変えるなどの見た目の変更はcomputed、 ボタン押下時の処理はmethodsで行う。 ? / components / CommentEditor.vue 周辺 CommentEditor.vue 記事に対するコメントフォームを構成するコンポーネント。 フォームの実装 <RwvListErrors :errors="errors" /> <form class="card comment-form" @submit.prevent="onSubmit(slug, comment)"> methods: { onSubmit(slug, comment) { this.$store .dispatch(COMMENT_CREATE, { slug, comment }) .then(() => { this.comment = null; this.errors = {}; }) .catch(({ response }) => { this.errors = response.data.errors; }); } } フォームのPostボタンを押下すると、COMMENT_CREATEが実行される。この処理が失敗すると、RwvListErrors部分でエラーメッセージが表示される。 ListError.vue errorsオブジェクトを受け取った時に、エラーメッセージを画面に表示するコンポーネント。 ? / components / Comment.vue コメント表示のコンポーネント。 削除イベント <span v-if="isCurrentUser" class="mod-options"> <i class="ion-trash-a" @click="destroy(slug, comment.id)"></i> </span> computed: { isCurrentUser() { if (this.currentUser.username && this.comment.author.username) { return this.comment.author.username === this.currentUser.username; } return false; }, // ... } @click="destroy(slug, comment.id)"はクリックイベントで、コメントを削除する処理。isCurretntUserは現在のユーザと著者が同じかどうかのフラグで、同じ場合はこの削除ボタンを表示する。 ? / components / ListErrors.vue errorsオブジェクトを受け取った時に、エラーメッセージを画面に表示するコンポーネント。CommentEditor.vueでも使用している。 ? / components / ArticleList.vue 記事プレビュー一覧を表示するコンポーネント。 大まかな処理の流れは、listConfigに記事情報を持たせて、currentpage、 type、 author、 tag、 favoritedをwatchで監視し、変更があったらfetchArticle()で新しい記事の取得。である。 ? / components / VArticlePreview.vue 周辺 VArticlePreview.vue 記事プレビューを表示するコンポーネント。 ArticleMeta.vueとTagList.vueコンポーネントを使用している。 TagList.vue タグ一覧を表示するコンポーネント。 ? / components / VPagination.vue ページ割りの表示をするコンポーネント。 クラスのバインディング <li v-for="page in pages" :data-test="`page-link-${page}`" :key="page" :class="paginationClass(page)" @click.prevent="changePage(page)" > <a class="page-link" href v-text="page" /> </li> :class="paginationClass(page)"で現在のページと一致していたらactive-classをつけ、表示デザインを変える。 $emitを使用した親コンポーネントへのイベント通知 ページ割りのボタンが押下されたら、@click.prevent="changePage(page)"を実行する。 changePage(goToPage) { if (goToPage === this.currentPage) return; this.$emit("update:currentPage", goToPage); }, 親コンポーネント(ArticleList.vue)では、以下のようにコンポーネントを使用している。 <VPagination :pages="pages" :currentPage.sync="currentPage" /> .sync修飾子をつけることで、子コンポーネントからthis.$emit('update:currentPage', goToPage) により親に通知することができる。(親は update:prop名のイベントを監視) 参照1【Vue】知っておきたい .sync修飾子のすゝめ、参考2_公式 ? /store ? / store / index.js import Vue from "vue"; import Vuex from "vuex"; import home from "./home.module"; import auth from "./auth.module"; import article from "./article.module"; import profile from "./profile.module"; Vue.use(Vuex); export default new Vuex.Store({ modules: { home, auth, article, profile } }); storeの中が膨大になるのを防ぐため、複数のファイルで管理する。分割のためにmodulesを使用する。 ? / store / .modules.js 全体構成 .modules.jsは共通して以下の4つをexportしている。参考:Vuex公式 export default { state,   // 状態管理利を行う対象 actions, // 非同期処理を行う。mutationsを呼ぶ。 mutations, // stateの状態を変更する getters // stateを取得する }; auth.modules.js ファイルの分割 import ApiService from "@/common/api.service"; import JwtService from "@/common/jwt.service"; import { LOGIN, LOGOUT, REGISTER, CHECK_AUTH, UPDATE_USER } from "./actions.type"; import { SET_AUTH, PURGE_AUTH, SET_ERROR } from "./mutations.type"; APIおよびJWTの処理は共通処理として切り出している。また、actionとmutationで使用する関数は、.typeとして別ファイルで定義している。 Boolean型へのキャスト const state = { errors: null, user: {}, isAuthenticated: !!JwtService.getToken() }; isAuthenticated: !!JwtService.getToken()の!!は二重否定を行い、Boolean型にキャストする処理。 getters const getters = { currentUser(state) { return state.user; }, isAuthenticated(state) { return state.isAuthenticated; } }; ユーザ状態と認証状態を返す。 actions, mutations const actions = { [LOGIN](context, credentials) { return new Promise(resolve => { ApiService.post("users/login", { user: credentials }) .then(({ data }) => { context.commit(SET_AUTH, data.user); resolve(data); }) .catch(({ response }) => { context.commit(SET_ERROR, response.data.errors); }); }); }, [LOGOUT](context) { context.commit(PURGE_AUTH); }, // ... } const mutations = { [SET_ERROR](state, error) { state.errors = error; }, [SET_AUTH](state, user) { state.isAuthenticated = true; state.user = user; state.errors = {}; JwtService.saveToken(state.user.token); }, [PURGE_AUTH](state) { state.isAuthenticated = false; state.user = {}; state.errors = {}; JwtService.destroyToken(); } }; APIを呼び、成功したらSET_AUTH、失敗したらSET_ERROR、ログアウト時はPURGE_AUTHと、mutationにコミットする。 ? /common ? / common / .service.js api.service.js axios, vue-axiosの使用 import Vue from "vue"; import axios from "axios"; import VueAxios from "vue-axios"; import JwtService from "@/common/jwt.service"; import { API_URL } from "@/common/config"; axios、vue-axiosを使用している。参考 vue-axios トークンの処理、ベースURLの管理は別ファイルに分離している。 処理の分割 const ApiService = { init() {/**/}, setHeader() {/**/}, // http method query(resource, params) {/**/}, get(resource, slug = "") {/**/}, post(resource, params) {/**/}, update(resource, slug, params) {/**/}, put(resource, params) {/**/}, delete(resource) {/**/} }; export default ApiService; export const TagsService = { get() { return ApiService.get("tags"); } }; export const ArticlesService = { // ... }; export const CommentsService = { // ... }; export const FavoriteService = { // ... }; ファイルの中でも処理の分割を行っている。ApiServiceにて「初期化、ヘッダの設定、HTTP通信の基本処理」がまとまっている。それらを用いてTagService、ArticlesServiceなど、具体的な処理を実装していることがわかる。 jwt.service.js ローカルストレージを用いたトークン管理 const ID_TOKEN_KEY = "id_token"; export const getToken = () => { return window.localStorage.getItem(ID_TOKEN_KEY); }; export const saveToken = token => { window.localStorage.setItem(ID_TOKEN_KEY, token); }; export const destroyToken = () => { window.localStorage.removeItem(ID_TOKEN_KEY); }; export default { getToken, saveToken, destroyToken }; ローカルストレージでトークンを管理している。 ※ ローカルストレージでトークンを管理するのはセキュリティ上よくないと聞いたことがあるが、どうなのだろうか?要調査 参考 MDN localStrage ? / common / .filter.js main.jsにてVue.filter(xxx)で使用している。 関数で定義され、フィルターをかますと、戻り値の値に変換される。 参考 : https://jp.vuejs.org/v2/guide/filters.html 所感 Vue.jsに触れてこのリポジトリを見つけたときからいつかは理解したいと思っていたので、一通り理解できた???のでよかったです。 記事を見直すと、トップダウンで書いてきたので、後から出てくる機能が前で使用されていてわかりにくいと感じました。記載の工夫が必要と感じました。ここまで書いたので、これからも定期的に読み直して、不足分は追記していこうと思います。 コードリーディングした感想は、 プログラミングと同じくらい、構成の設計は大切 特に、処理は小さく分割する、機能をまとめることが重要 リーディングを行う際、全体のファイルの関係性を整理すると、頭に入りやすい リポジトリを読むときは、Issuesを見るとヒントが隠れている 実はこれはVue2のプロジェクトで、数年間更新がされていないリポジトリです。今後は、学んだことを自分のアプリに適用すること。それと並行して、Vue3のReal World example appのリーディングに取り組んでいきたいです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

vue-realworld-example-appを読んでみた

(giithub): https://github.com/gothinkster/vue-realworld-example-app (CodeSandbox): https://codesandbox.io/s/github/gothinkster/vue-realworld-example-app/tree/master ?はじめに ? 記事の背景 vue-realworld-example-app は、実際の使用例(CRUD、認証、高度なパターンなど)に近い形のVue.jsプロジェクトである。Webアプリケーションの基本的な機能が一通りまとまっている。 本記事は、一般的なVue.jsプロジェクトはどのような構成になっているか、どのような技術が使われているかを知るために、vue-realworld-example-app をコードリーディングし、まとめたものである。 ? ( 参考 前提知識 ) コードリーディングを行うにあたる前提知識は以下。 Vue.js v2 (Router、Vuex)の基礎知識 Vue CLI の基礎知識 また、知っているといいと感じたものは以下。(知識がなくても調べながらでOK) REST API の基礎知識 JWTの基礎知識 ローカルストレージの基礎知識 ?全体構成の確認 ?フォルダ構成 /src構成は以下の通り。 App.vue、 main.js アプリのエントリーポイント。アプリ全体の設定などはここで行う。 /views 表示する画面。ルーティングと対応している。 /router Vue Routerによるルーティングの設定を行っている。 /components アプリ内で使用するコンポーネント。 /store Vuexによる状態管理。/views, /components内のから呼ばれる。 /common API、JWT、フィルタ、設定などのアプリ全体で使用する共通機能。 ?コンポーネント構成 コンポーネント構成は下記の通り。「緑=/views、黄色=/components」である。/ruterでルーティングされているコンポーネントは/viewsに、それ以外は/componentsで管理している。 ? /store, /commonの構成 /store, /commonの関係性は下記のとおり。 /storeは、4つのモジュールをexport している。それぞれのモジュールは、state, getters, actions, mutationsを持ち、状態管理を行う。状態管理を行う中で、インターフェースのような役割を果たすtypeから関数名を参照したり、共通処理を/commonを参照し、実行している。 /commonは、/storeで使用する共通処理ファイル(.service.js)とその設定ファイル(config.js)、アプリ全体で使用するフィルターファイル(.filter.js)を持つ。 ? ソースコード詳細 各ソースコードの気になったところを抜粋して記載する。 ソースコード: https://github.com/gothinkster/vue-realworld-example-app ? main.js、App.vue ? main.js import DateFilter from "./common/date.filter"; import ErrorFilter from "./common/error.filter"; Vue.filter("date", DateFilter); Vue.filter("error", ErrorFilter); アプリ全体で使用する、日付変換(DateFilter)や エラー変換(ErrorFilter)のフィルターはここで定義する。これらは/commonで定義されている。 import ApiService from "./common/api.service"; ApiService.init(); http request のための初期化を行う。 ApiService.init()では、vue-axiosプラグインを適用し、デフォルトURLを設定している。 import { CHECK_AUTH } from "./store/actions.type"; router.beforeEach((to, from, next) => Promise.all([store.dispatch(CHECK_AUTH)]).then(next) ); router.beforeEach()はページロードごとに呼ばれる関数。Promise.all()は、配列を引数にとり、すべてのPromiseを実行する。 ここでページロードごとにトークンの認証処理をしている。 参考 Vue公式 ナビゲーションガード 参考 MDN Promise.all() ? App.vue <template> <div id="app"> <RwvHeader /> <router-view /> <RwvFooter /> </div> </template> アプリ全体に表示させるため、ヘッダおよびフッタはここに記載する。 ? / router ? index.js routes: [ { path: "/", component: () => import("@/views/Home"), children: [ { path: "", name: "home", component: () => import("@/views/HomeGlobal") }, ... ], ... }, ... ] childrenを使用することで、ネストされたルートにしている。 またnameをつけることで、<router-link :to={ name: 'home' }>と指定できる。 ? /views ? / views / Home.vue 周辺 Home.vue <router-link>で HomeGlobal、HomeMyFeed、HomeTag にリンクを設定し、 <router-view>で表示する仕組み。 <script> import { mapGetters } from "vuex"; import { FETCH_TAGS } from "@/store/actions.type"; export default { // ... mounted() { this.$store.dispatch(FETCH_TAGS); }, computed: { ...mapGetters(["isAuthenticated", "tags"]), tag() { return this.$route.params.tag; } } }; </script> mounted()はVuexのactionを使用してFETCH_TAGSを実行し、APIを用いてtagsを取得する。取得したtagsは、Vuexのstateで管理されるので、mapGetter()で取得する。という流れ。 mapGetters()は引数で指定したstateの値を取得する関数。import {mapGetters} from "vue"を使用してインポートし、computedで使用する。 HomeGlobal.vue, HomeMyFeed.vue, HomeTag.vue <template> <div class="home-global"><RwvArticleList type="all" /></div> <!-- <div class="home-my-feed"><RwvArticleList type="feed" /></div> <div class="home-tag"><RwvArticleList :tag="tag"></RwvArticleList></div> --> </template> 上記3つのtemplateの中身はどれも似た構成。@/components/ArticleListを呼び、属性 ( type )を変えているだけ。 ?/ views / Login.vue , Register.vue, Settings.vue Login.vue <form @submit.prevent="onSubmit(email, password)"> ... </form> v-modelでバインディングして、ボタン押下すると値が送信されるごくごく一般的なフォームの実装となっている。@submit.preventはevent.preventDefault()を呼び出す処理。これにより、フォーム送信後もページのリロードを行わない。.preventはイベント修飾子と呼ばれる。 import { mapState } from "vuex"; import { LOGIN } from "@/store/actions.type"; export default { ... data() { return { email: null, password: null }; }, methods: { onSubmit(email, password) { this.$store .dispatch(LOGIN, { email, password }) .then(() => this.$router.push({ name: "home" })); } }, ... }; ログイン成功したら、.then(() => { this.$router.push({ name: "home" })});を行い、ホームに飛ばす処理になっている。 import { mapState } from "vuex"; export default { ... computed: { // auth.errors を errors に代入する処理。 // ↓ 加工後の名称: state => 加工処理(state.加工対象のステート名) ...mapState({ errors: state => state.auth.errors }) } }; 上記の書き方は、stateを加工した結果をバインドする方法。 Register.vue Login.vueとほぼ同じ構成。違いは、フォームにusernameの欄ができて、Vuexのactionが、LOGINからREGISTERになったくらい。 Setting.vue Login.vueやRegister.vueとほぼ同じ構成。 ?/ views / Profile.vue 周辺 Profile.vue <router-link>で ProfileArticles、ProfileFavorited にリンクをはり、<router-view>で表示する仕組み。 <div v-if="isCurrentUser()"> <router-link class="btn btn-sm btn-outline-secondary action-btn" :to="{ name: 'settings' }" > ... </router-link> </div> <div v-else> <button class="btn btn-sm btn-secondary action-btn" v-if="profile.following" @click.prevent="unfollow()" > ... </button> <button class="btn btn-sm btn-outline-secondary action-btn" v-if="!profile.following" @click.prevent="follow()" > ... </button> </div> <div v-if="isCurrentUser()">でログイン中のユーザかどうかで表示画面が変わる。 v-if="profile.following"部分も同様で、フォローしているかどうかで表示画面が変わる。 import { FETCH_PROFILE, FETCH_PROFILE_FOLLOW, FETCH_PROFILE_UNFOLLOW } from "@/store/actions.type"; export default { ... mounted() { this.$store.dispatch(FETCH_PROFILE, this.$route.params); }, ... watch: { $route(to) { this.$store.dispatch(FETCH_PROFILE, to.params); } } }; 初期化時にmounted()で、ページのユーザ名をパラメータから取得している。 Vue.jsではパラメータが #/@hoge から #/@huga へ遷移するときに同じコンポーネントインスタンスが再利用されるのでwatch: $route(to){...}を使用して、パラメータの検知をおこなっている。参考 ProfileArticles.vue、ProfileFavorited.vue <template> <div class="profile-page"> <RwvArticleList :author="author" :items-per-page="5"></RwvArticleList> <!-- <RwvArticleList :favorited="favorited" :items-per-page="5"> </RwvArticleList> --> </div> </template> templateのはどれも似た構成。@/components/ArticleListを呼び、属性 ( author, favorited )を変えているだけ。 ?/ views / Article.vue import store from "@/store"; import { FETCH_ARTICLE, FETCH_COMMENTS } from "@/store/actions.type"; export default { //... beforeRouteEnter(to, from, next) { Promise.all([ store.dispatch(FETCH_ARTICLE, to.params.slug), store.dispatch(FETCH_COMMENTS, to.params.slug) ]).then(() => { next(); }); } //... } beforeRouteEnter()を用いて、FETCH_ARTICLE、FETCH_COMMENTSを呼び、articleとcommentの最新のものを取得しstateを更新する。 <div v-html="parseMarkdown(article.body)"></div> import marked from "marked"; // ... export default { // ... methods: { parseMarkdown(content) { return marked(content); } } } markedはマークダウン解析ツール、htmlを返すため、v-htmlディレクティブを使用している。 ?/ views / ArticleEdit.vue // [/editor/:slug] => [/editor] の場合、エディタを空にして表示する。 // beforeRouteUpdateは、パラメータが変わったタイミングでも実行される。 async beforeRouteUpdate(to, from, next) { await store.dispatch(ARTICLE_RESET_STATE); return next(); }, // [/editor] の場合、下記のifがfalseになり、実行されない。 // [/editor/:slug] の場合、下記のifがtrueになり、実行される。 async beforeRouteEnter(to, from, next) { await store.dispatch(ARTICLE_RESET_STATE); if (to.params.slug !== undefined) { await store.dispatch( FETCH_ARTICLE, to.params.slug, to.params.previousArticle ); } return next(); }, // [/editor/:slug] から去るときエディタを空にする。 async beforeRouteLeave(to, from, next) { await store.dispatch(ARTICLE_RESET_STATE); next(); }, ルートが変更したら実行されるナビゲーションガードで制御する。ARTICLE_RESET_STATEで記事エディタを空に更新するが、/editor/slugのような場合は、元記事が記載されたまま表示する。 ※ ちなみに上記のナビゲーションガードは、パラメータが変わったタイミングでは実行されないので、 /editor から/editor/:slugに遷移した場合、元記事が記載されない。 参考:https://tsudoi.org/weblog/5738/ 参考:https://router.vuejs.org/ja/guide/advanced/navigation-guards.html <template> ... <input type="text" class="form-control" placeholder="Enter tags" v-model="tagInput" @keypress.enter.prevent="addTag(tagInput)" /> <div class="tag-list"> <span class="tag-default tag-pill" v-for="(tag, index) of article.tagList" :key="tag + index" > <i class="ion-close-round" @click="removeTag(tag)"> </i> {{ tag }} </span> </div> ... </template> タグの登録処理は、@keypress.enter.prevent="addTag(tagInput)"のように、Enter押下時にタグ登録処理を行う実装となっている。 ? /components ヘッダを表示するコンポーネント。 ?/ components / TheHeader.vue <ul v-if="!isAuthenticated" class="nav navbar-nav pull-xs-right"> .... </ul> <ul v-else class="nav navbar-nav pull-xs-right"> .... </ul> 認証状態によって表示する画面をv-ifで切り替えている。 <router-link class="nav-link" active-class="active" exact :to="{ name: 'profile', params: { username: currentUser.username } }" > {{ currentUser.username }} </router-link> <router-link>では:to="{params:{...}}でパラメータを指定することができる。 ? / components / VTag.vue タグを表示するコンポーネント。 <router-link :to="homeRoute" :class="className" v-text="name"></router-link> v-text="name"は{{ name }}と同じ export default { props: { name: { type: String, required: true }, className: { type: String, default: "tag-pill tag-default" } } // ... } propsでは、型の指定(type)、必須項目指定(required)、デフォルト値(default)などプロパティの型を指定できる。以降で説明するコンポーネントのpropsにおいても、型の指定などを必ず行っていた。 ? / components / ArticleMeta.vue 周辺 ArticleMeta.vue 記事作成者の情報などで構成されるコンポーネント。 <span class="date">{{ article.createdAt | date }}</span> main.jsで定義したフィルターが使用されている。 <rwv-article-actions v-if="actions" :article="article" :canModify="isCurrentUser()" ></rwv-article-actions> フォロー、いいねボタンはArticleActionsコンポーネントで制御している。 ArtcleAction.vue フォローやいいねボタンを構成するコンポーネント <span v-if="canModify"> <!-- Edit Article, DeleteArticleボタンが表示 --> </span> <span v-else> <!-- Follow, Favoriteボタンが表示 --> </span> ユーザが記事の作成者か否かで表示画面を変えている。 また、classを変えるなどの見た目の変更はcomputed、 ボタン押下時の処理はmethodsで行う。 ? / components / CommentEditor.vue 周辺 CommentEditor.vue 記事に対するコメントフォームを構成するコンポーネント。 <RwvListErrors :errors="errors" /> <form class="card comment-form" @submit.prevent="onSubmit(slug, comment)"> methods: { onSubmit(slug, comment) { this.$store .dispatch(COMMENT_CREATE, { slug, comment }) .then(() => { this.comment = null; this.errors = {}; }) .catch(({ response }) => { this.errors = response.data.errors; }); } } フォームのPostボタンを押下すると、COMMENT_CREATEが実行される。この処理が失敗すると、RwvListErrors部分でエラーメッセージが表示される。 ListError.vue errorsオブジェクトを受け取った時に、エラーメッセージを画面に表示するコンポーネント。 ? / components / Comment.vue コメント表示のコンポーネント。 <span v-if="isCurrentUser" class="mod-options"> <i class="ion-trash-a" @click="destroy(slug, comment.id)"></i> </span> computed: { isCurrentUser() { if (this.currentUser.username && this.comment.author.username) { return this.comment.author.username === this.currentUser.username; } return false; }, // ... } @click="destroy(slug, comment.id)"はクリックイベントでコメントを削除する。isCurretntUserが現在のユーザと著者が同じ場合はこのボタンを表示する。 ? / components / ListErrors.vue errorsオブジェクトを受け取った時に、エラーメッセージを画面に表示させるコンポーネント。CommentEditor.vueでも使用している。 ? / components / ArticleList.vue 記事プレビューの一覧を表示するコンポーネント。 大まかな処理は、listConfigに記事情報を持たせて、currentpage、 type、 author、 tag、 favoritedをwatchで監視し、変更があったらfetchArticle()で新しい記事を取得する流れである。 ? / components / VArticlePreview.vue 周辺 VArticlePreview.vue 記事プレビューを表示するコンポーネント。 ArticleMeta.vueとTagList.vueコンポーネントを使用している。 TagList.vue タグ一覧を表示するコンポーネント。 ? / components / VPagination.vue ページ割りの表示をするコンポーネント。 <li v-for="page in pages" :data-test="`page-link-${page}`" :key="page" :class="paginationClass(page)" @click.prevent="changePage(page)" > <a class="page-link" href v-text="page" /> </li> :class="paginationClass(page)"で現在のページと一致していたらactive-classをつけ、表示デザインを変える。 ページ割りのボタンが押下されたら、@click.prevent="changePage(page)"を実行する。 changePage(goToPage) { if (goToPage === this.currentPage) return; this.$emit("update:currentPage", goToPage); }, 親コンポーネント(ArticleList.vue)では、以下のようにコンポーネントを使用している。 <VPagination :pages="pages" :currentPage.sync="currentPage" /> .sync修飾子をつけることで、子コンポーネントからthis.$emit('update:currentPage', goToPage) で親に通知することができる。(親は update:prop名のイベントを監視) 参照1【Vue】知っておきたい .sync修飾子のすゝめ、参考2_公式 ? /store ? / store / index.js import Vue from "vue"; import Vuex from "vuex"; import home from "./home.module"; import auth from "./auth.module"; import article from "./article.module"; import profile from "./profile.module"; Vue.use(Vuex); export default new Vuex.Store({ modules: { home, auth, article, profile } }); ストアオブジェクトを複数のファイルで管理するために、modulesを使って分割を行う。 ? / store / .modules.js .modules.jsは共通して以下の4つをexportしている。参考:Vuex公式 export default { state,   // 状態管理利を行う対象 actions, // 非同期処理を行う。mutationsを呼ぶ。 mutations, // stateの状態を変更する getters // stateを取得する }; auth.modules.js import ApiService from "@/common/api.service"; import JwtService from "@/common/jwt.service"; import { LOGIN, LOGOUT, REGISTER, CHECK_AUTH, UPDATE_USER } from "./actions.type"; import { SET_AUTH, PURGE_AUTH, SET_ERROR } from "./mutations.type"; APIおよびJWTの処理は共通処理として切り出している。また、actionとmutationで使用する関数は、.typeとして別ファイルで定義している。 const state = { errors: null, user: {}, isAuthenticated: !!JwtService.getToken() }; isAuthenticated: !!JwtService.getToken()の!!は二重否定を行い、Boolean型にキャストする処理。 const getters = { currentUser(state) { return state.user; }, isAuthenticated(state) { return state.isAuthenticated; } }; ユーザ状態と認証状態を返す。 const actions = { [LOGIN](context, credentials) { return new Promise(resolve => { ApiService.post("users/login", { user: credentials }) .then(({ data }) => { context.commit(SET_AUTH, data.user); resolve(data); }) .catch(({ response }) => { context.commit(SET_ERROR, response.data.errors); }); }); }, [LOGOUT](context) { context.commit(PURGE_AUTH); }, // ... } const mutations = { [SET_ERROR](state, error) { state.errors = error; }, [SET_AUTH](state, user) { state.isAuthenticated = true; state.user = user; state.errors = {}; JwtService.saveToken(state.user.token); }, [PURGE_AUTH](state) { state.isAuthenticated = false; state.user = {}; state.errors = {}; JwtService.destroyToken(); } }; APIを呼び、成功したらSET_AUTH、失敗したらSET_ERROR、ログアウト時はPURGE_AUTHと、mutationにコミットする。 ? /common ? / common / .service.js api.service.js import Vue from "vue"; import axios from "axios"; import VueAxios from "vue-axios"; import JwtService from "@/common/jwt.service"; import { API_URL } from "@/common/config"; axios、vue-axiosを使用している。参考 vue-axios トークンの処理、ベースURLの管理は別ファイルに分離している。 const ApiService = { init() {/**/}, setHeader() {/**/}, // http method query(resource, params) {/**/}, get(resource, slug = "") {/**/}, post(resource, params) {/**/}, update(resource, slug, params) {/**/}, put(resource, params) {/**/}, delete(resource) {/**/} }; export default ApiService; export const TagsService = { get() { return ApiService.get("tags"); } }; export const ArticlesService = { // ... }; export const CommentsService = { // ... }; export const FavoriteService = { // ... }; ファイルの中でも処理の分割を行っている。ApiServiceで初期化、ヘッダの設定、http requestの処理がまとまっていて、それらを用いてTagService、ArticlesServiceなど、具体的な処理を実装していることがわかる。 jwt.service.js const ID_TOKEN_KEY = "id_token"; export const getToken = () => { return window.localStorage.getItem(ID_TOKEN_KEY); }; export const saveToken = token => { window.localStorage.setItem(ID_TOKEN_KEY, token); }; export const destroyToken = () => { window.localStorage.removeItem(ID_TOKEN_KEY); }; export default { getToken, saveToken, destroyToken }; ローカルストレージでトークンを管理している。 ※ ローカルストレージでトークンを管理するのはセキュリティ上よくないと聞いたことがあるがどうなのだろうか?要調査 参考 MDN localStrage ? / common / .filter.js main.jsにてVue.filter(xxx)で使用している。 関数で定義され、フィルターをかますと、戻り値の値に変換される。 参考 : https://jp.vuejs.org/v2/guide/filters.html 所感 Vue.jsに触れてこのリポジトリを見つけたときからいつかは理解したいと思っていたので、一通り理解できた???のでよかったです。 記事を見直すと、トップダウンで書いてきたので、後から出てくる機能が前で使用されていてわかりにくいと感じました。記載の工夫が必要と感じました。ここまで書いたので、これからも定期的に読み直して、不足分は追記していこうと思います。 コードリーディングした感想は、 プログラミングと同じくらい、構成の設計は大切 特に、処理は小さく分割する、機能をまとめることが重要 リーディングを行う際、全体のファイルの関係性を整理すると、頭に入りやすい リポジトリを読むときは、Issuesを見るとヒントが隠れている 実はこれはVue2のプロジェクトで、数年間更新がされていないリポジトリです。今後は、学んだことを自分のアプリに適用すること。それと並行して、Vue3のReal World example appのリーディングに取り組んでいきたいです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【 Vue CLi 環境構築 】コマンド・ライン・インターフェース

前書き VueCLIのバージョン(2.9.6)からVueCLI3系にアップデートするのにものすごく時間がかかってしまい備忘録も兼ねて執筆。 Vue CLIのインストールと初期設定 すでに旧バージョンをインストール済みの場合は、一度アンインストールする $ npm uninstall -g vue/cli 旧バージョンのダウンロード $ npm install -g vue/cli 最新版のダウンロード $ npm install -g @vue/cli Vueのバージョン確認 $ vue --version 新しいプロジェクトの作成 $ vue create プロジェクト名 アップデートするために参考にした記事 Vue2系のプロダクトをVue3へアップデート Vue CLI の変更点・使い方 (vue-cliから@vue/cliにアップデート) Homebrewからnodebrewをインストールして、Node.jsをインストールするまで Vue.js を vue-cli を使ってシンプルにはじめてみる ※Manually select featuresを選択 3つを選択(オプション機能の追加、解除は十字キーで上下に動かし、スペースで選択) → Enterで次へ Vue.jsのバージョンを選択 ※2.xまたは、3.x(preview)のどちらかを選択 ここでの注意点は2.xにしないとvuetifyが使えない! ESlintの種類を選択 →ESLint with error prevention onlyを選択 コード成形のタイミング → Lint on saveを選択 babelやconfigの設定 → In dedicated config filesを選択 設定を保存するかの有無 → No インストール完了 cd ディレクトリー名、 npm run serveを順番に入力する $ cd ディレクトリー名 $ npm run serve ディレクトリ名(ファイル名)を入力 $ cd ディレクトリー名 サーバーの実行 (開発用のサーバーが起動する) $ npm run serve ・ development serveが起動する ・ ローカル環境でサーバーが動く 本番環境構築 $ npm run build
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vuexまとめ

目次 1.はじめに 2.Vuexの項目 3.Vuexの流れ 4.どういう使い方をするか 1. はじめに Vuexの必要性を感じたのでノート的にまとめて頭を整理していきます。 Vuexとは コンポーネント間の共通の処理を記述する場所(現在のイメージ)。 2. Vuexの項目 項目 役割 state データを保持する。Vueのdataに該当 getters 算出プロパティ。Vuex内ののcomputed。 mutations Vuex内の値であるstateを変更する。原則ここ経由以外でのstateの変更はしてはいけない actions 非同期処理を行う。action以外の項目は同期処理 3. Vuexの流れ 同期処理と非同期処理でこの辺は変わる。 同期処理 ① mutationsでstateの値を変更する処理を記述 ↓ ②stateの値を変更 非同期処理 ① actionsで非同期処理の記述とmutationのメソッドを実行 ↓ ② mutationsでstateの値を変更する処理を記述 ↓ ③stateの値を変更 流れのまとめ - 同期処理 mutations→ state -非同期処理 actions → mutations → state 5. どういう使い方をするか 今回は外部APIを使用するので, 非同期処理の流れになる。 ① actionでリクエストを送信 ② mutatioinでstateの配列に受け取ったデータを保存 ③ データを保存してある配列を他のコンポーネントで呼び出し使用
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

便利ページ:JavascriptでHTML5をサクッと試す

便利ページ:Javascriptでちょっとした便利な機能を作ってみた のシリーズです。 HTML5になって久しいですが、スマホの普及もあって、たくさんの機能がブラウザだけで実現できるようになっています。 そこで、以下の機能を、ブラウザ上からサクッと使えるようにしました。(HTML5の範疇ではないものもありますが。。。) ・Audio ・Video ・Image ・GeoLocation ・加速度センサ ・端末オリエンテーション ・バッテリー ・WebUsb ・WebBluetooth ・バイブレーション ・音声発話 ・音声認識 ・WebCam録画 ソースコードは以下のGitHubに上げてあります。 ブラウザから体験したい場合は以下にアクセスしてください。右上からHTML5を選択してください。  https://poruruba.github.io/utilities/ 主に、以下のファイルを追加しました。 js\comp\comp_html5.js Audio 音声ファイルを選択して、再生します。 audio_read: function (files) { if (files.length <= 0){ this.audio_type = ""; return; } var file = files[0]; this.audio_type = file.type; var reader = new FileReader(); reader.onload = () => { var audio = document.querySelector("#html5_audio_player"); audio.src = reader.result; audio.play(); }; reader.readAsDataURL(file); }, Video ビデオファイルを指定して、再生します。 video_read: function (files) { if (files.length <= 0){ this.video_type = ""; return; } var file = files[0]; this.video_type = file.type; var reader = new FileReader(); reader.onload = () => { var video = document.querySelector("#html5_video_player"); video.src = reader.result; video.play(); }; reader.readAsDataURL(file); }, Image 画像ファイルを指定して表示します。(HTML5ではないですが。。。) image_read: function (files) { if (files.length <= 0) { this.image_type = ""; this.image_src = ""; return; } var file = files[0]; this.image_type = file.type; var reader = new FileReader(); reader.onload = () => { this.image_src = reader.result; }; reader.readAsDataURL(file); }, GeoLocation いわゆるGPSを取得して表示します。 location_get: function(){ var options = { enableHightAccuracy: true, maximumAge: 0, timeout: 120000 } navigator.geolocation.getCurrentPosition((position) =>{ this.location = position.coords; }, (error) =>{ console.error(error); alert(error); }, options); }, 加速度センサ 端末に搭載されている加速度センサの測定値を表示します。主にスマホ用です。 motion_start: function () { if (!this.motion_running) { this.motion_running = true; window.addEventListener("devicemotion", this.motion_handler); } else { window.removeEventListener("devicemotion", this.motion_handler); this.motion_running = false; } }, motion_handler: function(event){ if (event.accelerationIncludingGravity.x === null ){ window.removeEventListener("devicemotion", this.motion_handler); this.motion_running = false; this.motion_supported = false; return; } this.accelerationIncludingGravity = event.accelerationIncludingGravity; this.motion_supported = true; }, 端末オリエンテーション 端末の向きを表示します。 orientation_start: function () { if (!this.orientation_running) { this.orientation_running = true; window.addEventListener("deviceorientation", this.orientation_handler); } else { window.removeEventListener("deviceorientation", this.orientation_handler); this.orientation_running = false; } }, orientation_handler: function (event) { if (event.alpha === null) { window.removeEventListener("deviceorientation", this.orientation_handler); this.orientation_running = false; this.orientation_supported = false; return; } this.orientation = event; this.orientation_supported = true; }, バッテリ バッテリの残容量等を表示します。 battery_refresh: function(){ navigator.getBattery() .then(battery => { this.battery = battery; }); } WebUSB WebUSBで、PCに接続されているUSBデバイスを表示します。 usb_request: async function () { try{ var device = await navigator.usb.requestDevice({ filters: [] }); this.usbdevice = device; }catch(error){ alert(error); } }, WebBluetooth WebBluetoothで、PCの周りにあるBLEデバイスを表示します。 ble_request: async function () { try{ var device = await navigator.bluetooth.requestDevice({ acceptAllDevices: [] }); this.bledevice = device; } catch (error) { alert(error); } }, バイブレーション 端末のバイブレーションを振動させます。主にスマホ向けです。 vibration_start: function(){ navigator.vibrate(this.vibration_duration); }, 音声発話 入力した文章を音声でスピーカから発話します。 speech_start: async function(){ var result = await new Promise((resolve, reject) => { var utter = new window.SpeechSynthesisUtterance(); utter.volume = this.speech_volume / 100; utter.rate = this.speech_rate / 100; utter.pitch = this.speech_pitch / 100; utter.text = this.speech_text; utter.lang = "ja-JP"; var ok = false; var ng = false; utter.onend = function () { console.log('Event(Utter) : onend'); if (!ok && !ng) { ok = true; resolve(); } }; utter.onstart = function () { console.log('Event(Utter) : onstart'); }; utter.onerror = function (error) { console.log('Event(Utter) : onerror'); if (!ok && !ng) { ng = true; reject(error); } }; window.speechSynthesis.cancel(); return window.speechSynthesis.speak(utter); }) .catch(error => { console.log(error); }); 音声認識 マイクから録音した音声から文章を認識します。 speech_recognition: async function(){ this.speech_recognized = await new Promise((resolve, reject) => { var recognition = new webkitSpeechRecognition(); recognition.lang = "ja-JP"; recognition.continuous = false; var match = false; var error = false; recognition.onresult = function (e) { console.log('Event(Recog) : onresult'); if (!match && !error) { match = true; var text = ''; for (var i = 0; i < e.results.length; ++i) { text += e.results[i][0].transcript; } resolve(text); } }; recognition.onend = function () { console.log('Event(Recog) : onend'); if (!match && !error) { match = true; reject(error); } recognition.stop(); }; recognition.onnomatch = function () { console.log('Event(Recog) : onnomatch'); if (!match && !error) { error = true; reject('nomatch'); } }; recognition.onerror = function (e) { console.log('Event(Recog) : onerror : ' + JSON.stringify(e)); if (!match && !error) { error = true; reject('onerror'); } }; recognition.start(); }) .catch(error => { console.log(error); }); }, WebCam録画 PCに接続されたWebCamやスマホにあるカメラで動画を録画し、ファイルに出力します。 一般的にwebm形式で保存されるようです。 record_prepare: function(){ this.record_dispose(); this.record_previewing = true; var resolution = this.record_resolution_list[this.record_resolution]; navigator.mediaDevices.getUserMedia({ video: { facingMode: this.record_facing, width: resolution.width, height: resolution.height }, audio: true }) .then((stream) => { g_stream = stream; this.record_preview.srcObject = stream; }) .catch(error =>{ this.record_previewing = false; alert(error); }); }, record_dispose: function(){ if( g_recorder ){ this.record_recording = false; g_recorder.stop(); g_recorder = null; } this.record_preview.pause(); this.record_preview.srcObject = null; g_stream = null; this.record_previewing = false; }, record_start: function(){ if (g_recorder) { g_recorder.stop(); g_recorder = null; } this.record_recording = true; this.record_chunks = []; g_recorder = new MediaRecorder(g_stream); g_recorder.ondataavailable = this.record_onavailable; g_recorder.onstop = this.record_onstop; g_recorder.start(); }, record_stop: function(){ console.log('stop'); g_recorder.stop(); }, record_onavailable: function (e) { console.log('ondataavailable'); this.record_chunks.push(e.data); }, record_onstop: function () { if( !this.record_recording ) return; console.log('onstop'); var blob = new Blob(this.record_chunks, { type: g_recorder.mimeType }); this.chunks = []; var mimeType = g_recorder.mimeType; g_recorder = null; this.record_recording = false; var url = window.URL.createObjectURL(blob); var a = document.createElement("a"); a.href = url; a.target = '_blank'; if (mimeType.startsWith('video/x-matroska') ) a.download = "record.webm"; else a.download = "record.bin"; a.click(); }, 以上
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む