- 投稿日:2020-05-21T21:13:08+09:00
Vue(Vue-CLI)とTypeScriptのhighchartsグラフの数値に桁区切りを入れる
はじめに
highchartsではデフォルトの区切りにスペースを使用しています。highchartsのグラフにカーソルを合わせたときに表示される数値に桁区切りを入れるところでハマったので、本記事を書きました。
通常の表示
Graph.vue<template> <div> <highcharts :options="graph"></highcharts> </div> </template> <script lang="ts"> import { Component, Vue } from 'vue-property-decorator'; import { Chart } from 'highcharts-vue'; export type DataType = { graph: any; } @Component ({ components: { highcharts: Chart }, }) export default class Graph extends Vue { data (): DataType { return { graph: { title: { text: 'Xperiaの値段(au)' }, xAxis: { categories: ['Xperia 1', 'Xperia 5'], crosshair: true }, yAxis: { title: false, labels: { format: '{value} 円' }, opposite: false, }, credits: { enabled: false }, tooltip: { pointFormat: '{series.name}:{point.y:,.0f} 円' }, series: [{ name: '一括価格', type: 'column', data: [92880, 81400], marker: { enabled: true }, }], } } } } </script> <style> div { width: 80%; height: auto; margin: 20px auto auto auto; } </style>このように、桁区切りがスペースで表示されます。Xperia 1の値段を92 880円から、92,880円に表示したい場合は下記のようにします。
桁区切りで表示
import HighchartsとHighcharts.setOptionsを追加します。
Graph.vueimport { Chart } from 'highcharts-vue'; + import Highcharts from 'highcharts'; + Highcharts.setOptions({ + lang: { + thousandsSep: ',' + } + }); export type DataType = {Graph.vue<template> <div> <highcharts :options="graph"></highcharts> </div> </template> <script lang="ts"> import { Component, Vue } from 'vue-property-decorator'; import { Chart } from 'highcharts-vue'; import Highcharts from 'highcharts'; Highcharts.setOptions({ lang: { thousandsSep: ',' } }); export type DataType = { graph: any; } @Component ({ components: { highcharts: Chart }, }) export default class Graph extends Vue { data (): DataType { return { graph: { title: { text: 'Xperiaの値段(au)' }, xAxis: { categories: ['Xperia 1', 'Xperia 5'], crosshair: true }, yAxis: { title: false, labels: { format: '{value} 円' }, opposite: false, }, credits: { enabled: false }, tooltip: { pointFormat: '{series.name}:{point.y:,.0f} 円' }, series: [{ name: '一括価格', type: 'column', data: [92880, 81400], marker: { enabled: true }, }], } } } } </script> <style> div { width: 80%; height: auto; margin: 20px auto auto auto; } </style>3桁区切りで表示することができました。
おわりに
合わせてこちらもご覧ください。
Vue(Vue-CLI)とTypeScriptでhighchartsのグラフ表示
- 投稿日:2020-05-21T20:16:37+09:00
Nuxt.js & Contentful & Netlifyを使いポートフォリオサイトを作成しました
はじめに
簡単に自己紹介をしますと私は現在Vue.js中心に学習中のエンジニア未経験、フロントエンドエンジニア志望の者です。
ただ淡々と学習するのではなくアウトプットを通じて学習したことを深く理解すること、記事を残すことでこれを見た人へ何かしらのヒントになる情報を与えられることができればと思い作成しました。
目標物
TechpitのNuxt.js & Contentfulでハイスペックなポートフォリオサイトを超簡単に公開しよう!【JAMstack】という教材からポートフォリオサイトを作成しました。
Nuxt.jsとは?
公式サイト
Vueから設計されたフレームワークです。
SSR(サーバサイドレンダリング)アプリケーションを簡単に作ることができます。SSR(サーバサイドレンダリング)
本来クライアントサイドで実行して、レンダリングされるjavascriptの処理を、サーバサイドで実行して、レンダリングする仕組み。Contentfulとは?
公式サイト
Wordpress等と同じCMSの一種です。CMS
=> Contents Management System(コンテンツ・マネジメント・システム)webサイトを簡単に構築・管理・更新できるシステムです。
JAM stackとは?
公式サイト
サーバーやデータベースに依存せずにサイトやアプリを作成します。
記事データはAPIで用意し、Nuxt.jsで各ページをマークアップすることで動的なコンテンツで静的なウェブサイトを構築する技術です。主なメリット
- 表示が高速である
- バックエンドをいじらないためセキュリティを気にしないで済む
環境
エディタ
Visual Studio Codeライブラリ等
node 12.16.3
npm 6.14.4
nuxt 2.12.2
+ nuxtjs/markdownit 1.2.9
+ nuxt-fontawesome 0.4.0
+ fortawesome/free-solid-svg-icons 5.13.0
+ fortawesome/free-brands-svg-icons 5.13.0
+ contentful .14.4完成した物
こちらのリンクから実際のサイトをご覧できます。
画像
設計
ポートフォリオサイトページ構成
ページ 表示 トップページ 作品一覧 作品個別ページ 各作品に関する詳細情報 カテゴリページ 特定のカテゴリに属する作品の一覧 タグページ 特定のタグを持つ作品の一覧 検索結果ページ 特定のキーワードを含む作品の一覧 作品データ(Contentful)
フィールド名 説明 Tittle 記事のタイトル Slug 記事のスラッグ Subtitle 記事のサブタイトル Date 作成日時 Category カテゴリ Tags タグ(複数登録可) Content 記事本文 Image 記事のサムネイル画像 Slug(スラッグ)について
SlugとはContentfulで記事を作成した時に設定する、サイト内の特定のページを識別するためのURLの一部です。
「vue.js-nuxt.js-portfolio」のように、キーワードをつなげて作ります。作成中に起きたトラブル
問題1.
contentfulの記事データが読み込めない
解決方法
APIkeyを使う
「Space ID」と「Content Delivery API - access token」// .contentful.json ファイル内 { "CTF_SPACE_ID": "Space ID", "CTF_CDA_ACCESS_TOKEN": "Content Delivery API - access token" }問題2.
ブラウザに【未定義のプロパティ「field」を読み取れません。】と出てサイトが表示しなくなる。
解決方法
原因:Contentfulで新規記事作成の時Imageを追加していなかった
解決:サムネイル画像を設定問題3.
npmインストールしたらディレクトリ構成が変わってサイトが表示しなくなってしまった
解決方法
作業内容を戻す$ git reset --hard HEAD問題4.
NetlifyでGitHubと連携したデプロイができない。
Error: Cannot find module ‘Contentful’
解決方法?
Git連携を諦めて普通にアップロードする
$ npm run generateでdistファルダ作成
Netfilyでdistフォルダをドラッグ&ドロップまとめ
最後に連携で詰まってしまい、5時間くらいかかってできなかったので一旦普通にデプロイしました。
いずれ再挑戦します。フロントエンドエンジニア志望としては
Contentfulというサービスの使い方は学んでおいて損はなさそうです。
簡単にコンテンツが追加できるのは楽しいですね。Nuxt.jsは初めてでしたがVue.jsが基本なので苦手な感じはしませんでした。
色々とエラーで苦しみながら成長できた気がします。
いい経験になりました!これから自分なりに機能を追加したりスタイルを変えたりしてみようと思います。
ここまで読んでいただきありがとうございました。
Link
- 投稿日:2020-05-21T17:14:30+09:00
electron-vue でNeDBを使用する
のじみんです。
今回はelectron-vueでNeDBを使用する方法について
インストールからDB設定するところまでを説明します※ 公式ドキュメントと同じ内容なのでそちらをまず参考にしてください。↓
electron-vue - ローカルファイルの読み書き実行環境とバージョン一覧
- macOS Mojave v10.14.6
- npm v6.9.0
- node v10.16.3
- electron v1.4.13
Installing
まずは
NeDB
をインストールしましょうnpmの場合$ npm install --save nedbもしくは
yarnの場合$ yarn add nedb
Settings
rendererディレクトリの配下に
datastore.js
というファイルを作成する$ cd <YOUR_PROJECRT> $ touch ./src/renderer/datastore.js
datastore.js
の中身は以下のようにする./src/renderer/datastore.jsimport path from 'path'; import { remote } from 'electron'; import Datastore from 'nedb'; const dbPath = path.join(remote.app.getPath('userData'), '/data.db'); // DB初期化 export default new Datastore({ autoload: true, filename: dbPath, });上記ではelectronのアプリケーションディレクトリの
data.db
ファイルを読み込む処理をしています。
data.db
がない場合は上の処理でファイルが作成されるのでエラーがでることはありません。
main.js
に以下の処理を追記する./src/renderer/main.js... import db from './datastore'; Vue.prototype.$db = db; ...以上でどのコンポーネントからもDBを呼び出すことができるようになりました。やったね!
usage
このままだと
data.db
の中身は空なのでデータを引っ張ってくることができません。
なのであらかじめ適当なデータを突っ込んでから
electron-vue
でdbからデータを取得していきましょうデータを挿入する
まず適当に
data.db
にデータを突っ込んでみましょう。
main.js
に以下の文を追記します。./src/renderer/main.js... import db from './datastore'; Vue.prototype.$db = db; // 今回追加する処理 db.find({}, (err, doc) => { const data = [ { name: '山田太郎', age: 20 }, { name: 'のじ先生' age: 10 }, ]; // 初回のみ:dbのデータが空だった場合、テーブルの構築をする if (doc.length === 0) { db.insert(docs); } }); ...一度アプリをビルドする
$ yarn run devここまで終われば、無事
data.db
にデータを入れることができました。
今度はelectorn-vue
でそのデータを取得してみましょう。electron-vueでdbからデータを取得する
適当なコンポーネントのscript部分に以下を記述する
※ 以下ではApp.vueで記述していると想定して書いています。./src/renderer/App.vue<script> this.$db.find({}, (err, doc) => { console.log(doc); }); </script>はい。以上でおしまいです。
あとはビルドしてみてコンソール出力に格納されたデータが表示されている確認してみてください。$ yarn run dev アプリケーションのコンソールから出力されているか確認ほんでは
参考
- 投稿日:2020-05-21T15:52:46+09:00
TwitterAPIを使用したユーザー検索機能
1.TwitterAPIを使用したTwitterアカウントの検索機能をLaravelと Vue.jsで実装したい
開発中のプロダクトでTwitterアカウントの検索機能を実装する必要があったので、
その方法を備忘録的に残しておきます。
完成形はこんな感じで、キーワードを入力するとそれに紐づくTwitterアカウントが検索結果に表示されるというものです。
2.TwitterAPI申請方法について
TwitterAPIを使用するには、TwitterDeveloperへの申請が必要です。
申請方法はこの辺りの記事を参考にすると良いと思います!
画像付きで詳しく説明してくれています。2020年度版 Twitter API利用申請の例文からAPIキーの取得まで詳しく解説
Laravel5.7でTwitterOAuthを使ってタイムライン取得(申請から説明)3.Laravelのプロジェクトの変更
TwitterDeveloperへの申請が完了し、API_KeyとTokenが取得できたら、
Laravelのプロジェクトに下記、変更を加えています。① Composerを使ってabraham/twitteroauthをインストール
② .envファイルにAPI_KeyとTokenなどの情報をいれる
③ configディレクトリにファイルを作成し、.envファイルに書いた定数を呼び出す処理を記述最低限ここまで設定をすれば、Controllerに諸々記述をすることで
TwitterAPIから情報を取得することができます。Laravel5.7でTwitterOAuthを使ってタイムライン取得(申請から説明)
こちらの記事では①〜③の方法、さらにServiceProviderとFacadesを使ってControllerでの記述を簡単にする方法までを丁寧に説明してくれていますので、こちらの記事を参考にしてください!
(参考にさせていただきました!ありがとうございます^^)
何度もTwitterAPIから情報を取得する場合、設定しておくととても便利です。今回は①〜③までの手順を書かせていただきます!
①Composerを使ってabraham/twitteroauthをインストール
ターミナルでLaravelプロジェクトがインストールされているディレクトリに移動します。
そこで下記コマンドを打ってください。composer require abraham/twitteroauthターミナルに下記メッセージが表示されたら、成功です!
Package manifest generated successfully.②.envファイルにAPI_KeyとTokenなどの情報をいれる
続いて、.envファイルに取得したAPI_KeyとTokenなどを追記します。
.envTWITTER_CLIENT_ID = TwitterAPIkey TWITTER_CLIENT_SECRET = TwitterAPI SecretKey TWITTER_ACCESS_TOKEN = AccessToken TWITTER_ACCESS_TOKEN_SECRET = AccessTokenSecret TWITTER_CLIENT_CALLBACK = コールバックしたいURL③configディレクトリにファイルを作成し、.envファイルに書いた定数を呼び出す処理を記述
.envファイルに記述したAPI keyなどの情報をconfigファイルを介して呼びだすように設定します。
.envファイルに定義した変数をそのまま呼び出すのはダメみたいです。
その辺は【Laravel】環境変数の使い方の記事を参考に。まず、configディレクトリに
twitter.php
というファイルを作成します。
そして、twitter.php
に下記のコードをコピペしてください。config/twitter.php<?php return [ 'twitter-api' => env('TWITTER_CLIENT_ID',''), 'twitter-api-secret' => env('TWITTER_CLIENT_SECRET',''), 'twitter-token' => env('TWITTER_ACCESS_TOKEN',''), 'twitter-token-secret' => env('TWITTER_ACCESS_TOKEN_SECRET',''), 'call_back_url' => env('TWITTER_CLIENT_CALLBACK',''), ];ここまでで、一旦①〜③の設定は完了です!
続いてTwitterAPIから情報を取得していきましょう。4.アカウント情報を取得する
それでは、ControllerにTwitterAPIから情報を取得するための記述を記入していきます。
TwitterDeveloperのドキュメントを確認すると
アカウント検索の際にはGET users/search
を使用することがわかります。
情報を取得する際の必須パラメーターq
に検索クエリを入れます。
その他のパラメータは下記通りです!
名前 必須 説明 q 必須 検索キーワード page オプション 取得する結果のページを指定します。 count オプション ページごとに取得するユーザー結果の数。(最大値は20) include_entities オプション entitiesの取得を省略 【①〜③のみを設定した場合】
TwitterController.php<?php namespace App\Http\Controllers; use App\Http\Controllers\Controller; use Illuminate\Http\Request; use Abraham\TwitterOAuth\TwitterOAuth; class TwitterController extends Controller { // Twitterのアカウント検索 public function index(Request $request) { // Vueファイルで入力された検索されたキーワードの定義 $q = $request->keyword; // API keyなどを定義 $consumer_key = config('twitter.twitter-api'); $consumer_secret = config('twitter.twitter-api-secret'); $access_token = config('twitter.twitter-token'); $access_token_secret = config('twitter.twitter-token-secret'); $connection = new TwitterOAuth($consumer_key, $consumer_secret, $access_token, $access_token_secret); $twitterRequest = $connection->get('users/search', array( "q" => $q, "count" => 20)); return response()->json(['result'=>$twitterRequest], 200); }※今回は、Vueファイルにデータを返すのでJSON形式で
return response()->json(['result'=>$twitterRequest], 200);
という記述になってます。【ServiceProviderとFacadesまで設定した場合】
TwitterController.php<?php namespace App\Http\Controllers; use App\Http\Controllers\Controller; use Illuminate\Http\Request; use Abraham\TwitterOAuth\TwitterOAuth; class TwitterController extends Controller { // Twitterのアカウント検索 public function index(Request $request) { // 検索されたキーワードの定義 $q = $request->keyword; // TwitterAPIからユーザー情報を取得 $twitterRequest = \Twitter::get('users/search', array("q" => $q, "count" => 20)); return response()->json(['result'=>$twitterRequest], 200); }サービスプロバイダとファサードまで設定しておくと、リクエストを投げるときに簡潔に書くことができます!
Laravel5.7でTwitterOAuthを使ってタイムライン取得(申請から説明)
是非こちらの記事を参考に設定してみてください^^5.TwitterAPIから取得した情報について
ここまででデータを取得できたのですが、少し問題があります。
GET users/search
は、アカウント名やアカウント画像、認証アカウントなのか・・・
など様々な情報を取得できます。
ただ、アカウント画像に関してはデフォルトでは48x48ピクセルのものになります。
少し小さめですね・・・オリジナルサイズの画像を取得したい場合、画像のURLを変更することでそれが可能になります〜
TwitterAPIドキュメントにも記載がありますが、画像のURLから_normal
を除いてあげればオッケーです。
では、Controllerの記述に追記していましょう〜TwitterController.php// 一部省略してます〜 (TwitterAPIからユーザー情報を取得の記述から〜) // TwitterAPIからユーザー情報を取得 $twitterRequest = \Twitter::get('users/search', array("q" => $q, "count" => 20) // TwitterAPIからのレスポンス プロフィール画像のURLから _normalの文字列を省く) foreach($twitterRequest as $res){ $image = $res->profile_image_url_https; $fullImg = str_replace('_normal', '', $image); $res->full_img = $fullImg; $twitterRes[] = $res; }その他レスポンスボディについては下記記事が参考になります〜
Tweet objects
Twitter 開発者 ドキュメント日本語訳これで表示に必要な情報は取得できたかと思います〜!
長くなったので、Vue.jsを使用したインクリメントリサーチの方法などはまた改めて書こうと思います。
読んでくれた方ありがとうございました^^開発環境
PHP 7.2
Laravel 6.0
Vue 2.5.17参考リンク
Laravel5.7でTwitterOAuthを使ってタイムライン取得(申請から説明)
2020年度版 Twitter API利用申請の例文からAPIキーの取得まで詳しく解説
【Laravel】環境変数の使い方
Tweet objects
Twitter 開発者 ドキュメント日本語訳
TwitterDeveloperのドキュメント
TwitterAPIドキュメント
- 投稿日:2020-05-21T15:19:42+09:00
Nuxt.jsをVue.jsに解体するTips(Layout編)
背景
- Nuxt.jsで動いているSPAアプリをVue.jsに解体してほしい的な話があり、部分部分をTipsとして投稿します。
- APIはLaravel(6.x)です。なおLaravelとNuxt.jsは同一リポジトリです(なぜ)
- Nuxt.jsは2.9。Vueは2.6.10
- 今回はLayout編です。
Layoutを分けたい
nuxt/pages/~~.vue<script> export default { layout: 'noauth',
- 認証に見れるとLayoutと非認証時に見れるLayoutを分けた時があります。そういった時Nuxt.jsは上記のように簡単にできますが、Vue.jsだとコツが必要です。
Layoutをそれぞれ作成する
認証用のLayout
resources/js/layouts/default.vue<template> <div>default <router-view /> </div> </template>非認証時に見れるレイアウト
resources/js/layouts/noauth.vue<template> <div>noauth <router-view /> </div> </template>resources/js/router.jsの編集
Routerでmeta layoutを定義する
resources/js/router.jsimport Vue from 'vue' import Router from 'vue-router' import Index from '~/pages/index' Vue.use(Router) const router = new Router({ mode: 'history', routes: [ { path: '/', name: 'Index', meta: { layout: 'default' }, component: Index, }, { path: '/noauth', name: 'NoAuth', meta: { layout: 'noauth' }, component: Index, }, ], });resources/js/App.vueの編集
メタフィールドをみてLayoutを変更
resources/js/App.vue<template> <div id="app"> <component v-bind:is="layout" /> </div> </template> <script> export default { data() { return { defaultLayout: "default" }; }, computed: { layout() { return (this.$route.meta.layout || this.defaultLayout) + "-layout"; } } }; </script>Layout用のコンポーネントを定義
resources/js/app.jsimport Vue from "vue"; import router from '~/router' import App from '~/App.vue' import DefaultLayout from '~/layouts/default' import NoAuthLayout from '~/layouts/noauth' Vue.component('default-layout', DefaultLayout) Vue.component('noauth-layout', NoAuthLayout) new Vue({ router: router, render: h => h(App), }).$mount('#app')参考
関連リンク
- 投稿日:2020-05-21T15:03:22+09:00
ASP.Net Core+RazorPage+Vue(テキストボックス作成、タグヘルパーとバリデーション属性とvueコンポーネントの関係)
今回の課題
今回は、Vueコンポーネントとして一番単純な文字列のinputを拡張したコンポーネントを作成。バリデーションを行う上での各種クラスの関連性を見ていきます。
(1)TagHelperの処理
(2)バリデーション属性の処理
(3)vueコンポーネントのバリデーションに関する処理以前、「ASP.Net Core でjQuery無しでHTML5のバリデーションを利用する」でいくつか書いていますが、その内容も含みます。その時は属性は「MergeAttributeは上書きできない」と書いたのですが、いろいろとやってみるとバリデーションクラスでtypeを書き換えちゃってます。内部処理によってはできるものとできないものがあるのかもしれません。書き方が悪かったのかもしれません。まだまだ試行錯誤がありそうです。
前提
ASP.NetCore RazorPage+Vue+blumaの環境を利用します。
以前に書いた「Vue.jsを利用してみる(1)」と「Vue.jsを利用してみる(1)」を参照しての環境を構築します。タグヘルパー
数値や日時などの入力でも共通して使うことを考えて、入力タグの基本クラスを定義します。テキスト入力は、この基本クラスを継承してますが、何も変えていません。
まず基本クラスです。
VueInputTagHelper.csusing Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.TagHelpers; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal; using Microsoft.AspNetCore.Razor.TagHelpers; using System; using System.Collections.Generic; namespace RazorPageVue.VueTagHelpers { public class VueInputTagHelper : TagHelper { /// <summary> /// バインド対象のcshtmlのタグの属性名 /// </summary> protected const string ForAttributeName = "asp-for"; ///// <summary> ///// type属性に設定する値(空ならデータ型に合わせて設定される) ///// </summary> //protected string _overrideType = null; // それぞれのタグを作成するので不要 /// <summary> /// バインド対象取得用のプロパティ。HtmlAttributeNameの引数名のタグ属性名が対象となる /// </summary> [HtmlAttributeName(ForAttributeName)] public ModelExpression For { get; set; } /// <summary> /// タグ属性「name」を受け取るプロパティ。(バインド対象が未設定の場合に出力するタグのname属性の値になる) /// </summary> public string Name { get; set; } /// <summary> /// タグ属性「value」を受け取るプロパティ。(バインド対象が未設定の場合に出力するタグのvalue属性の値になる) /// </summary> public string Value { get; set; } /// <summary> /// ビューのコンテキスト /// </summary> [HtmlAttributeNotBound] [ViewContext] public ViewContext ViewContext { get; set; } /// <summary> /// バリデーターの処理を実施させる為に設定するテキストボックス用のジェネレータ作成用 /// (コンストラクタのデータインジェクションで設定) /// </summary> protected IHtmlGenerator Generator { get; } /// <summary> /// コンストラクタ /// </summary> /// <param name="generator">The <see cref="IHtmlGenerator"/>.</param> public VueInputTagHelper(IHtmlGenerator generator) { Generator = generator; } /// <summary> /// タグヘルパーの実行実装 /// </summary> /// <param name="context">タグヘルパーコンテキスト</param> /// <param name="output">タグヘルパー出力</param> public override void Process(TagHelperContext context, TagHelperOutput output) { // 引数のコンテキストと出力がnullならエラー if (context == null) throw new ArgumentNullException(nameof(context)); if (output == null) throw new ArgumentNullException(nameof(output)); // テキストボックスのタグビルダーを作成し、バリデーションなどの属性で設定されているものをタグに取り込む // この時、すでに設定されているタグの属性は更新できないので注意 output.MergeAttributes(GenerateTextBox(Value)); } /// <summary> /// テキストボックスのタグビルダーを作成 /// </summary> /// <param name="value">タグのvalueに設定されている値(無ければnull)</param> /// <returns>標準のinputタグに近いタグビルダー</returns> private TagBuilder GenerateTextBox(object value) { var modelExplorer = For.ModelExplorer; IDictionary<string, object> htmlAttributes = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase); //// typeはforまたはプレフィックスが設定されていて_overrideTypeが設定され手入れば設定する。 //// これを利用して継承クラスからtypeを設定できるようにしている //if (!string.IsNullOrEmpty(_overrideType)) htmlAttributes.Add("type", _overrideType); // それぞれのタグを作成するので不要 // asp-forが設定されているかどうかでnameとvalueの異なるタグビルダーを作成 if (string.IsNullOrEmpty(For.Name) && string.IsNullOrEmpty(ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix)) { // asp-forが未設定の場合、nameやvalueに設定されている値でタグを作成 return Generator.GenerateTextBox( ViewContext, modelExplorer, Name, value, null, htmlAttributes); } else { // asp-forが設定されている場合、nameやvalueはasp-forの内容に強制される return Generator.GenerateTextBox( ViewContext, modelExplorer, For.Name, modelExplorer.Model, null, htmlAttributes); } } /// <summary> /// 入力するデータの型のヒントリストを取得する /// 基本的に「InputTagHelper」の実装をそのまま利用している /// </summary> /// <returns>バインドしているデータ型のヒントリスト</returns> protected IEnumerable<string> GetInputValueTypeHints() { var modelExplorer = For.ModelExplorer; // テンプレートヒントが有ればそれをリストにして返す if (!string.IsNullOrEmpty(modelExplorer.Metadata.TemplateHint)) { yield return modelExplorer.Metadata.TemplateHint; } // データタイプ名が有ればそれをそれをリストにして返す if (!string.IsNullOrEmpty(modelExplorer.Metadata.DataTypeName)) { yield return modelExplorer.Metadata.DataTypeName; } // In most cases, we don't want to search for Nullable<T>. We want to search for T, which should handle // both T and Nullable<T>. However we special-case bool? to avoid turning an <input/> into a <select/>. var fieldType = modelExplorer.ModelType; if (typeof(bool?) != fieldType) { fieldType = modelExplorer.Metadata.UnderlyingOrModelType; } foreach (var typeName in TemplateRenderer.GetTypeNames(modelExplorer.Metadata, fieldType)) { yield return typeName; } } } }モデルとのバインド
タグの入力とモデルをバインドさせているのは
public ModelExpression For { get; set; }の部分で、このプロパティーにバインドしているモデルの変数情報が入ってきます。この時、このプロパティの属性[HtmlAttributeName(ForAttributeName)]でcshtlmのタグの属性名を設定できるようです。つまり「asp-for」でなくともよいということになります。ここでは定数「ForAttributeName」に"asp-for"を設定して基本のままにしています。
html作成時の処理
cshtmlからhtmlを作るときは、メソッド「Process」が呼び出され、引数の「output」にhtml作成の情報を構築していくようです。
inputタグのタグビルダー
「GenerateTextBox(Value)」メソッドでinputタグのタグビルダーを作成して返します。このタグビルダーがデータのバインド(nameとvalueを設定)とtype属性の設定をしているようです。
ここでは今後の数値や時刻等の入力の為に、メンバ「_overrideType」を設定しておけば任意の「type」になるように小細工しています。(個別にタグ作っていていらないので削除しました)
また、「asp-for」が指定されていない場合は「name」や「value」に設定されている値を利用するようにしています。
cshtmlのタグ内の「name」や「value」はそれぞれ「Name」「Value」に設定されます。変数名が違うのは言語の仕様の問題でhtmlで利用されるケバブスケースの文字列はプロパティーにできないのでキャメルケースに変換されているからです。少しわかりにくいですね。これも「asp-for」みたいに属性指定にした方がわかりやすいと個人的には思うのですが...。バリデーションの対応
タグビルダーの結果とバリデーションで設定したいタグの属性を以下の部分でマージしているようです。バリデーションについては後で記述します。
output.MergeAttributes(GenerateTextBox(Value));引数の型のヒント
「GetInputValueTypeHints()」メソッドは引数の型を判定するためのメソッドです。文字列入力では利用していませんが、時刻や数値の場合に利用しますので継承メソッドとして定義しています。まあ、型判定を入れて文字列以外はエラーにするようにしてもいいかもしれません。
文字列用のタグヘルパー
VueTextInputTagHelperusing Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Razor.TagHelpers; namespace RazorPageVue.VueTagHelpers { [HtmlTargetElement("vue-text-input", Attributes = ForAttributeName, TagStructure = TagStructure.NormalOrSelfClosing)] public class VueTextInputTagHelper : VueInputTagHelper { /// <summary> /// コンストラクタ /// </summary> /// <param name="generator">The <see cref="IHtmlGenerator"/>.</param> public VueTextInputTagHelper(IHtmlGenerator generator) : base(generator) { } } }クラス属性の「HtmlTargetElement」でこのタグヘルパーの対象となるcshtml内のタグ名を「vue-text-input」に設定しています。Attributes には基本クラスで設定ている「asp-for」が入るようにしています。このようにテキストの入力クラスのタグヘルパーは処理は一切変更がありません。数値や日時などではいろいろと変わってくる予定です。
バリデーション
文字列長のバリデーションを以下に示します。
StringLengthAttribute.csusing Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using System; using System.ComponentModel.DataAnnotations; namespace RazorPageVue.VueValidations { [AttributeUsage(AttributeTargets.Property)] public class StringLengthAttribute : ValidationAttribute, IClientModelValidator { /// <summary> /// 最小長 0 ならチェック対象外 /// </summary> public int MaxLength { get; set; } = 0; /// <summary> /// 最大を超えた場合のエラーメッセージ /// </summary> public string OverMaxErrorMessage { get; set; } /// <summary> /// 最大長 0 ならチェック対処具合 /// </summary> public int MinLength { get; set; } = 0; /// <summary> /// 最大を超えた場合のエラーメッセージ /// </summary> public string UnderMinErrorMessage { get; set; } /// <summary> /// バリデーション(サーバーサイド) /// </summary> /// <param name="value">値</param> /// <param name="validationContext">バリデーションコンテキスト</param> /// <returns></returns> protected override ValidationResult IsValid( object value, ValidationContext validationContext) { // 入力が空の場合は常に正常(空のチェックは入力必須でおこなう) if (value == null) return ValidationResult.Success; // 最小桁数チェック if ((MinLength > 0) && (value.ToString().Trim().Length < MinLength)) { return new ValidationResult(GetUnderMinErrorMessage(validationContext.DisplayName)); } // 最大桁数チェック if ((MaxLength > 0) && value.ToString().Trim().Length > MaxLength) { return new ValidationResult(GetOverMaxErrorMessage(validationContext.DisplayName)); } return ValidationResult.Success; } /// <summary> /// クライアントでのバリデーション用の操作 /// </summary> /// <param name="context">クライアントのバリデーションコンテキスト</param> public void AddValidation(ClientModelValidationContext context) { if (context == null) throw new ArgumentNullException(nameof(context)); if (MinLength > 0) { // 最小値が設定されている場合以下のタグ属性を設定する // minlength 最小桁数 // min-length-err-msg バリデーションで設定されたエラーメッセージ context.Attributes["minlength"] = MinLength.ToString(); if (!string.IsNullOrWhiteSpace(UnderMinErrorMessage)) context.Attributes["minlength-err-msg"] = UnderMinErrorMessage; } if (MaxLength > 0) { // 最大値が設定されている場合以下のタグ属性を設定する // maxlength 最大桁数 // max-length-err-msg バリデーションで設定されたエラーメッセージ context.Attributes["maxlength"] = MaxLength.ToString(); if (!string.IsNullOrWhiteSpace(OverMaxErrorMessage)) context.Attributes["maxlength-err-msg"] = OverMaxErrorMessage; } } /// <summary> /// 最大多数のサーバーバリデーション時のエラーメッセージ取得 /// </summary> /// <param name="displayName">表示名称(DisplayNameアトリビュートで変更できる)</param> /// <returns>必須エラーメッセージ</returns> string GetOverMaxErrorMessage(string displayName) { if (string.IsNullOrEmpty(OverMaxErrorMessage)) { return displayName + "の値が最大値「" + MaxLength.ToString() + "」を超えています。"; } else { return OverMaxErrorMessage; } } /// <summary> /// 最小桁数のサーバーバリデーション時のエラーメッセージ取得 /// </summary> /// <param name="displayName">表示名称(DisplayNameアトリビュートで変更できる)</param> /// <returns>必須エラーメッセージ</returns> string GetUnderMinErrorMessage(string displayName) { if (string.IsNullOrEmpty(UnderMinErrorMessage)) { return displayName + "の値が最小値「" + MinLength.ToString() + "」より小さいです。"; } else { return UnderMinErrorMessage; } } } }クラスの前に「[AttributeUsage(AttributeTargets.Property)]」と書かれています。これでこのクラスのクラス名から「Attribute」を除いた名前のプロパティ用のアトリビュートができます。バリデーションクラスを継承しているのでバリデーションアトリビュートになります。
4つのプロパティに最小長、最小長エラーメッセージ、最大長、最大長エラーメッセージを設定しています。
「IsValid」メソッドは、サーバーまで来たときに行われるバリデーションで、クライアントでバリデーションエラーになるとここまで来ません(画面を使わずに直接POST処理を実施すれば別ですが)。
「AddValidation」メソッドがクライアントバリデーションのポイントになります。これは「IClientModelValidator」インターフェースの実装で、この処理がタグヘルパーの「output.MergeAttributes」の中で呼ばれているようです。これによって、ページモデルの参照用のプロパティにこのバリデーションを追加すると実施のhtmlの作成時にタグの属性を追加することができます。ここではプロパティーに設定されている最小長、最小長エラーメッセージ、最大長、最大長エラーメッセージをタグの属性に追加しています。URLやEmailのバリデーションをセットした場合はここでtype属性を変更させます。Vueコンポーネント
vueTextInput.jsVue.component('vue-text-input', { props: { id: String, // id name: String, // name required: String, // 必須属性 requiredErrMsg: String, // 必須エラーメッセージ maxlength: Number, // 文字列の最大長 maxlengthErrMsg: String, // 最大長エラーメッセージ minlength: Number, // 文字列の最小長 minlengthErrMsg: String, // 最小長エラーメッセージ compareId: String, // 同一比較するコンポーネントのID compareErrMsg: String, // 同一比較エラーメッセージ typemismatchErrMsg: String, // 型異常(url,email)のエラーメッセージ value: String // 入力された値 }, data: function (){ return { compareCompornent: null, // 自信が比較設定している比較対象のコンポーネント comparedComponents: [], // 自信が比較設定されている対象のコンポーネント inputText: this.value }; }, computed: { // inputタグのtypeに設定する値 dataType: function () { if ((type === "text") || (type === "url") || (type === "email") || (type === "password")) { return type; } return "text"; } }, methods: { //------------------------------------------------------------ // 入力が変更された場合に各種バリデーションをチェックする //------------------------------------------------------------ onChange: function () { // デフォルトのバリデーションエラーが有れば処理終了 if (window.IsDefaultValidationError(this.$el.validity)) { // ここでカスタムエラーを削除 this.$el.setCustomValidity(""); return; } // 入力必須バリデーション処理 if (!window.RequiredValidation(this.$el, this.required, this.requiredErrMsg)) return; // 入力文字数バリデーション処理 if (!window.StringLengthValidation(this.$el, this.maxlength, this.maxlengthErrMsg, this.minlength, this.minlengthErrMsg)) return; // 比較バリデーション処理 if (this.compareCompornent) { if (!window.CompareValidation(this.$el, this.compareCompornent.inputText, this.compareErrMsg )) return; } for (var i = 0; i < this.comparedComponents.length; i++) { this.comparedComponents[i].onChange(); } // エラーが無いのでカスタムエラーを削除 this.$el.setCustomValidity(""); }, //------------------------------------------------------------ // バリデーションエラーでメッセージが設定されている場合エラーメッセージを変更する //------------------------------------------------------------ onInvalid: function (e) { // 入力必須エラーメッセージの変更(変更した場合は処理終了) if (window.requiredMsgChange(this.$el, this.requiredErrMsg)) return; // 入力文字数エラーメッセージの変更 if (stringLengthMsgChange(this.$el, this.maxlengthErrMsg, this.minlengthErrMsg)) return; // 型異常(URL,Email)エラーメッセージの変更 if (typeMismatchMsgChange(this.$el, this.typemismatchErrMsg)) return; } }, mounted: function () { // 比較対象が設定されている場合、比較対象コンポーネントと相互に関連付けを行う if (this.compareId) { for (var j = 0; j < this.$root.$children.length; j++) { var targetItem = this.$root.$children[j]; if (this.compareId === targetItem.$el.id) { this.compareCompornent = targetItem; targetItem.comparedComponents.push(this); } } } // バリデーションを実施させる this.onChange(); }, template: '<input :id=id :name=name type=dataType() v-model="inputText" \ :required=required \ :maxlength=maxlength \ :minlength=minlength \ @change=onChange @invalid=onInvalid>' });Vueコンポーネントの記述です。バリデーション処理は入力の変更された「onChange」イベント処理で行います。カスタムエラー以外のバリデーションエラーがある場合は、カスタムエラーを消して処理を終了しておきます。(カスタムエラーを消すタイミングがここしかない)
以下はエラーの判定処理です。(別途Vue用の共通のjsのファイルとして作っています。)function IsDefaultValidationError(validity) { // デフォルトのバリデートアエラーが有ればtrueを返す if (validity.valueMissing || validity.badInput || validity.patternMismatch || validity.rangeOverflow || validity.rangeUnderflow || validity.stepMismatch || validity.tooLong || validity.tooShort || validity.typeMismatch || validity.badInput) { return true; } else { return false; } }「onChange」でいろいろとエラーチェックしていますが、文字列長のチェックは
// 入力文字数バリデーション処理 if (!window.StringLengthValidation(this.$el, this.maxlength, this.maxlengthErrMsg, this.minlength, this.minlengthErrMsg)) return;の部分で、この実装も「IsDefaultValidationError」と同じファイルに以下のように記述しています。
function StringLengthValidation(element, maxLength, maxlengthErrMsg, minLength, minlengthErrMsg) { // 空ならtrue;(Requiredでチェックする) if (!element.value) return true; // 最大長チェック(ほとんどのブラウザで入力できない様になるので不要と思われる) if (maxLength) { if (element.value.length > maxLength) { if (maxlengthErrMsg) { element.setCustomValidity(maxlengthErrMsg); } else { element.setCustomValidity(maxLength + "文字以下で入力してください。"); } return false; } } // 最小長チェック(一部のブラウザでチェックしていない模様) if (minLength) { if (element.value.length < minLength) { if (minlengthErrMsg) { element.setCustomValidity(minlengthErrMsg); } else { element.setCustomValidity(minLength + "文字以上で入力してください。"); } return false; } } return true; }エレメントの内容とバリデーションで設定されたタグの属性を利用指定最大、最小をチェックしています。
上記のバリデーション処理でエラーとなった場合はエラーメッセージも入っていれば対応していますが、ブラウザの標準機能でエラーとなった場合は「onInvalid」イベントでメッセージの変更を行っており、文字列長については
if (stringLengthMsgChange(this.$el, this._props.maxlengthErrMsg, this._props.minlengthErrMsg)) return;の部分で行っており、この実装は以下のようになっています。
// 文字数チェックのエラーメッセージ変更 function stringLengthMsgChange(element, maxlengthErrMsg, minLengthErrMsg) { if (element.validity.tooLong) { if (maxlengthErrMsg) { element.setCustomValidity(maxlengthErrMsg); return true; } } else if (element.validity.tooShort) { if (minlengthErrMsg) { element.setCustomValidity(minLengthErrMsg); return true; } } return false; }「element.validity.tooLong」が「true」なら最大長のエラー、「element.validity.tooShort」が「true」なら最小長エラーなので、その場合はエラーメッセージをカスタムエラーを設定しています。カスタムエラーが設定されるとブラウザではそのメッセージ優先されます。
ちょっと変わったところでは、他の入力と比較するバリデーションでは、比較先のエレメントに参照しているエレメントリストをコンポーネント作成時に設定しておき、どちらが変更されてもバリデーションを実行するようにしています。
一応、これで単純なVueコンポーネントのタグヘルパーとバリデーションは作れると思います。
最後に
次回は整数・実数の入力コンポーネントを作るのですが、これはChromeやFireFoxでは加算、減算のボタンがついていますが、IEやEdgeではついていないので、Vueコンポーネントを実行時に動的に置き換える方法で、IEやEdgeでもChromeに似せたコンポーネントに置き換得るようにしてみました。
****
時折LGTMしてくださる方や、何か琴線に触れたのかフォローしてくれた方が居られるようですので、こんな技術にも多少興味がある型が居られるようですので、ぼちぼち書いていきます。
****
- 投稿日:2020-05-21T12:24:15+09:00
Nuxt.jsをVue.jsに解体するTips(JavaScriptコンパイル編)
背景
- Nuxt.jsで動いているSPAアプリをVue.jsに解体してほしい的な話があり、部分部分をTipsとして投稿します。
- APIはLaravel(6.x)です。なおLaravelとNuxt.jsは同一リポジトリです(なぜ)
- Nuxt.jsは2.9。Vueは2.6.10
- 今回はJavaScriptコンパイル編です。
LaravelのディレクトリにNuxtのプロジェクトを移行
- resources/jsにNuxtのプロジェクトをそのまま移行します
下記のようなディレクトリ構造になります。
resources/js ├── assets ├── components ├── layouts ├── pages ├── plugin └── storewebpack.mix.jsにaliasを設定する
- Nuxt.jsのモジュールのパスの指定は相対パスではなくaliasを設定してパスを設定しています。
- webpack.mix.jsにデフォルトではそんな機能はないので設定します。
webpack.mix.jsconst mix = require('laravel-mix'); const path = require('path'); mix.webpackConfig({ resolve: { alias: { vue$: 'vue/dist/vue.esm.js', '~': path.resolve(__dirname, 'resources/js/'), }, }, })resources/js/app.jsを作成する
- VueやVue Routerなどの読み込み設定ファイルを作成します。
resources/js/app.jsimport Vue from "vue"; import router from '~/router' import App from '~/App.vue' new Vue({ router: router, render: h => h(App), }).$mount('#app')resources/js/App.vueを作成する
- レンダリングするVueファイルを作成し、resources/js/layouts/default.vueを読み込みます。
resources/js/layouts/default.vue
resources/js/App.vue<template> <div id="app"> <layout /> </div> </template> <script> import Layout from '~/layouts/default' export default { components: { Layout } };resources/js/layouts/default.vueを編集する
- をに変更します。
resources/js/layouts/default.vue<template> <div> <router-view /> </div> </template>resources/js/router.jsを作成する
- ルーティングは自動生成されないので手動で設定します。
resources/js/router.jsimport Vue from 'vue' import Router from 'vue-router' import Index from '~/pages/index' Vue.use(Router) const router = new Router({ mode: 'history', routes: [ { path: '/', name: 'Index', component: Index, }, ], });package.jsonの編集
- Laravel-Mixが使えるようにnpm scriptsを編集します。
- Laravel-Mix公式ドキュメントよりスクリプトを参照しています。
package.json"scripts": { "dev": "npm run development", "development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", "watch": "npm run development -- --watch", "hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js", "prod": "npm run production", "production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", },webpack.mix.jsの編集
- resources/js/app.jsのコンパイルを設定します。
webpack.mix.jsconst mix = require('laravel-mix'); const path = require('path'); mix.webpackConfig({ resolve: { alias: { vue$: 'vue/dist/vue.esm.js', '~': path.resolve(__dirname, 'resources/js/'), }, }, }) mix.js('resources/js/app.js', 'public/js')必要なnpmモジュールをインストール
$ npm install cross-env vue vue-router laravel-mix --save-devコンパイル
$ npm run devJavaScriptのコンパイルは通るはずです。(画面確認時にはコンソール上でエラーが出まくると思いますが)
プロジェクトによっては不足しているモジュールがあるかもしれません。随時追加してください。画面で確認したい場合
- Sassのコンパイルは未設定ですので、画面崩れまくりますが、画面確認したい時あるかもしれません。
Laravelのルーティングを編集
- 全部のルーティングをindex.bladeに集約します。
routes/web.phpRoute::get('{any}', function() { return view('index'); })->where('any', '.*');index.blade.phpの編集
- index.blade.phpでapp.jsを読み込みます。
resources/views/index.blade.php<!DOCTYPE html> <html lang="{{ str_replace('_', '-', app()->getLocale()) }}"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>~~~~</title> </head> <body> <div id="app"> </div> <script src="{{ mix('/js/app.js') }}"></script> </body> </html>これで画面の確認は可能です。
- 投稿日:2020-05-21T08:51:33+09:00
新型コロナの自宅待機中に、ビデオチャット上で遊べるアプリを作った話
ビデオチャット上で遊べるアプリを何人かに手伝ってもらいながら3週間で作りました。自分でSFUサーバ立てたりと、思っていたより力作になったので、得られた知見やノウハウを共有したいと思います。
自己紹介
普段はpythonを用いてデータ分析やサービス設計を研究している、東京大学の大学院生です。最近新型コロナの影響でリモート授業になり、家にいる時間を活用してWebアプリを作っています。
作ったサービス
「オンライン遊び場サービス wh.im」
要はビデオチャット上で色々ゲーム等を遊べるアプリです。早押しクイズとかワードウルフとか遊べます。オンライン飲み会とかで活躍すると思います。ちなみにサービス名は最近流行りのurlそのままパターンを採用してみました。
開発の背景
新型コロナで外出抑制が続き、人とのコミュニケーションが不足してます。特に自分みたいな一人暮らしの大学生とかは2,3日しゃべらないとかもザラにあり、なんとか友達と前みたいに遊べないかと考えていました。最近はzoom飲みとかも流行っていますが、それもだんだん飽きてきたのは自分だけではないはず。
オンラインでどうコミュニケーションするか
まずは、どうしたらオンラインのコミュニケーションが良くなるかを考えました。オンラインではオフラインコミュニケーションに比べ色々な制約があります。画面の部分しか映らない、声が聞き取りづらい、遅延がある、複数人で喋ったらもう聞き取れない、通信量使う、、、。それを踏まえ、オンラインで上手いことコミュニケーションするには、オンラインの良さを生かさないといけません。
オンラインの強み
オンラインでは、オフラインと違い目の前に超高性能計算機・パソコンがあります。これはオフラインにはない特徴です。オンラインではコミュニケーションの最中、常にパソコンを触っているのです。となると、パソコンでゲームしながらコミュニケーションしたらいいのではないのだろうか。こうして私はビデオチャット&ゲームが同時にできる、「オンライン遊び場サービス wh.im」を開発に着手しました。
サービスとして工夫したこと
サービスを設計にあたって、次の3つのことを意識しました。
- ビデオチャットとゲームの分離
- 会員登録は一切なし、遊び場を作るボタンを押すだけ
- 導線は極力シンプルにビデオチャットとゲームの分離
wh.im上に色々なアプリを作りたかったので、ビデオチャット部分とゲーム部分が分離された作りにしようと思いました。vueのコンポーネントで分ける等色々考えましたが、最終的にiframeでゲーム部分を上から載せるのが、シンプルで良いのではないかと考えました。以下イメージ図です。
会員登録は一切なし、遊び場を作るボタンを押すだけ
会員登録は一切ありません。urlを発行するだけです。ビデオチャットが始めやすく、誘いやすいように設計しました。招待を受けた側もurlをクリックするだけで wh.im を始めることができます。
導線は極力シンプルに
ボタン等の導線を極力減らし、サービス上でできることを制限しました。ここはzoom等のビジネスチャットとは違う部分で、ミュート、画面off、画面共有といった機能は省き、友達と会話できる、ゲームできるというコアな部分を重視しました。
開発に使った技術
主に以下の技術を使いました。
- mediasoup(webRTC)
- firebase(BaaS)
- Nuxt.js(JavaScriptフレームワーク)webRTC
一番苦労しました。日本語の文献も少ないです。
今回一番ネックであったのはログイン不要という部分です。ログイン不要というのは、ユーザーにとって便利な反面、サービス開始の敷居が低くて意図しないサービスの使われ方や、ユーザー数制限といった利用量を抑えるのは難しいのではないか、という懸念がありました。結果として利用が増えても金銭的な負担が発生しにくい方法を考える必要がありました。
webRTCを実装する上で最も簡単なのはNTTのやっているSkyWayだと思います。日本語の記事も比較的多いです。ただ一方、無料枠には500GBの通信量制限があり、有料プランは月額基本費用として10万円かかるということだったので、諦めました。
他にも有名どころですと、agora.io, opentok, twilio, AWS Chimeなどがあります。これらは大体料金は従量制で相場は1人1分0.5円といったところです。4人で一時間使うと120円ほどになります。個人で利用する分にはそれほど高くはありませんが、サービスリリースとなるとどれぐらいの利用料金になるか未知数なので今回は採用しませんでした。結局無償で使用できるOSSを使い実装することにし、その中でもmediasoupを選びました。
webRTCでの技術選択に関しては時雨堂さんの記事がすごい参考になりました。フローチャート形式で、自分のやるべきことがわかります。感謝。
webRTC OSS 個人的比較
webRTCのOSSで有名どころをStarHistoryで比較してみました。
人気が最もあるjanus-gatewayか、後発で順調にスターが伸びてるmediasoupかで悩みましたが、以下の理由でmediasoupに決めました。(webRTCを触るのは初めてだったのでここらへんはフィーリングです。)
- mediasoupはNode.jsのライブラリになっており、とっつきやすい。janus-gatewayはC言語。
- janusはmeetechoという会社が作っており、meetechoがjanusの導入サポートっぽいことを行っているので、逆にjanusをサポートなしで導入するのは難しいのではないかという邪な推測。
- ドキュメントがかっこいい
- 作者がイケメンFirebase
バックエンドとして使ってます。神サービス。感謝。
FirebaseからはAuthentication, Firestore, Strage, Hostingを使っています。(メインはFirestoreです。)
今回は複数のユーザーが通信することが前提にあるので、firestoreの変更があればクライアント側に通知するリアルタイムアップデート機能が大助かりしました。Nuxt.js
実は今回始めてNuxt.jsを使ってみました(今まではVue.js)。サーバーサイドレンダリングを使わなかったらVueCLIでもいいかなというのが正直な感想です。ちなみにデザインやcssは苦手なのでVuetifyを使いました。Vuetifyはすごくいいライブラリなのですが、一度使い出したらなかなか抜けれないという沼のようなライブラリでもあります(個人の感想です)。
ゲーム部分の作り込み
ゲーム部分はVue.jsで作りました。友達の協力も得ながら、じゃんけん、早押しゲーム、ワードウルフなどを作ってみました。
ビデオチャット部分とゲーム部分は別々に動いているので、そのwindow間でプレイヤー情報等を送信するためにWebの仕様であるpostMessageを用いました(このAPIは今回初めてしりました)。オンライン通信ゲームなので、ユーザー間でデータを同期しなければなりません。データ同期用にfirestoreを使うことを考えましたが、ゲームを作るたびにfirebaseでproject立ち上げて、、とやるのは些か面倒でした。ビデオチャット部分ではすでにfirebaseを使っていたので、上記と同様postMessageを活用することで、データ同期をビデオチャット経由で簡単に行えるようにしました。ここらへんの詳細はこちらにまとめました。
作ってみたゲーム
- じゃんけん
- ワードウルフ
- 早押しクイズ
- NGワードゲーム
- YesNoゲーム
- ジェスチャーゲーム
さらに、wh.im では、ゲーム部分を外に切り出したことで、ビデオチャットの部分を実装しなくても、簡単にオリジナルなゲームを作ることができます。フロントの技術をある程度持っていれば、自分のやりたいゲームを開発可能です。例えば...
- 麻雀
- サッカー
- おおぎり
- その他オリジナルゲームなどなど...!
簡単に作れるように、アプリ開発のためのドキュメントをまとめたので、是非参考にしてください。まとめ
アイデアの着想したときの見積もりより壮大になり合計3週間ほどかかりました。しかしビデオチャットとゲームを分離することで、設計がシンプルになり、ゲームの開発に集中することができました。 wh.im を使うと、簡単にチャットビデオ上で動くゲームを作れるので、是非使ってみてください。
- 投稿日:2020-05-21T08:51:33+09:00
新型コロナの自宅待機中に、ビデオチャットしながらゲームで遊べるサービスを作った話
ビデオチャット上で遊べるアプリを何人かに手伝ってもらいながら3週間で作りました。自分でSFUサーバ立てたりと、思っていたより力作になったので、得られた知見やノウハウを共有したいと思います。
自己紹介
普段はPythonを用いてデータ分析やサービス設計を研究している、東京大学の大学院生です。最近新型コロナの影響でリモート授業になり、家にいる時間を活用してWebアプリを作っています。
作ったサービス
「オンライン遊び場サービス wh.im」
要はビデオチャット上で色々なゲームで遊べるアプリです。早押しクイズとかワードウルフとか遊べます。オンライン飲み会とかで活躍すると思います。ちなみにサービス名は最近流行りのurlそのままパターンを採用してみました。lineとかslackに投稿するとそのままurlとして認識してくれます。ウィム!
開発の背景
新型コロナで外出抑制が続き、人とのコミュニケーションが不足してます。特に自分みたいな一人暮らしの大学生とかは2,3日人としゃべらないとかもザラにあり、なんとか友達と前みたいに遊べないかと考えていました。最近はzoom飲みとかも流行っていますが、それもだんだん飽きてきたのは自分だけではないはず。
オンラインでどうコミュニケーションするか
まずは、どうしたらオンラインのコミュニケーションが良くなるかを考えました。オンラインではオフラインコミュニケーションに比べ色々な制約があります。画面の部分しか映らない、声が聞き取りづらい、遅延がある、複数人で喋ったらもう聞き取れない、通信量使う、、、。しかし、オンラインにはオンラインの良さがあります。オンラインでしか遊べないなら、そのオンラインの良さを生かさないといけません。
オンラインの強み
オンラインでは、オフラインと違い目の前に超高性能計算機・パソコンがあります。これはオフラインにはない特徴です。オンラインではコミュニケーションの最中、常にパソコンを触っているのです。となると、パソコンでゲームしながらコミュニケーションしたらいいのではないのだろうか。こうして私はビデオチャット&ゲームが同時にできる、「オンライン遊び場サービス wh.im」の開発に着手しました。
サービスとして工夫したこと
サービスを設計にあたって、次の3つのことを意識しました。
- ビデオチャットとゲームの分離
- 会員登録は一切なし、遊び場を作るボタンを押すだけ
- 導線は極力シンプルにビデオチャットとゲームの分離
wh.im上に色々なアプリを作りたかったので、ビデオチャット部分とゲーム部分が分離された作りにしようと思いました。vueのコンポーネントで分ける等色々考えましたが、最終的にiframeでゲーム部分を上から載せるのが、シンプルで良いのではないかと考えました。以下イメージ図です。
会員登録は一切なし、遊び場を作るボタンを押すだけ
会員登録は一切ありません。urlを発行するだけです。ビデオチャットが始めやすく、誘いやすいように設計しました。招待を受けた側もurlをクリックするだけで wh.im を始めることができます。
導線は極力シンプルに
ボタン等の導線を極力減らし、サービス上でできることを制限しました。ここはzoom等のビジネスチャットとは違う部分で、ミュート、画面off、画面共有といった機能は省き、友達と会話できる、ゲームできるというコアな部分を重視しました。
開発に使った技術
主に以下の技術を使いました。
- mediasoup(webRTC)
- firebase(BaaS)
- Nuxt.js(JavaScriptフレームワーク)webRTC
一番苦労しました。日本語の文献も少ないです。
今回一番ネックであったのはログイン不要とい形の実現です。ログイン不要というのは、ユーザーにとって便利な反面サービス開始の敷居が低く、意図しないサービスの使われ方や、ユーザー数制限といった利用量を抑えるのが難しくなってしまうのではないか、という懸念がありました。結果として利用が増えても金銭的な負担が発生しにくい方法を考える必要がありました。
webRTCをマネージドのサーバを使って実装しようとするとある程度のお金が必要になります。webRTCを実装する上で最も簡単なのはNTTのやっているSkyWayだと思います。SkyWayは日本語の記事も比較的多い一方、無料枠には500GBの通信量制限があり、有料プランは月額基本費用として10万円かかるということだったので、諦めました。
他にも有名どころですと、agora.io, opentok, twilio, AWS Chimeなどがあります。これらは大体料金は従量制で相場は1人1分0.5円といったところです。4人で一時間使うと120円ほどになります。個人で利用する分にはそれほど高くはありませんが、サービスリリースとなるとどれぐらいの利用料金になるか未知数なので今回は採用しませんでした。
結局無償で使用できるOSSを使い実装することにし、その中でもmediasoupを選びました。
webRTCでの技術選択に関しては時雨堂さんの記事がすごい参考になりました。フローチャート形式で、自分のやるべきことがわかります。感謝。webRTC OSS 個人的比較
webRTCのOSSで有名どころをStarHistoryで比較してみました。
人気が最もあるjanus-gatewayか、後発で順調にスターが伸びてるmediasoupかで悩みましたが、以下の理由でmediasoupに決めました。(webRTCを触るのは初めてだったのでここらへんはフィーリングです。)
- mediasoupはNode.jsのライブラリになっており、とっつきやすい。janus-gatewayはC言語。
- janusはmeetechoという会社が作っており、meetechoがjanusの導入サポートっぽいことを行っているので、逆にjanusをサポートなしで導入するのは難しいのではないかという邪な推測。
- ドキュメントがかっこいい
- 作者がイケメンFirebase
バックエンドとして使ってます。神サービス。感謝。
FirebaseからはAuthentication, Firestore, Strage, Hostingを使っています。(メインはFirestoreです。)
今回は複数のユーザーが通信することが前提にあるので、firestoreの変更があればクライアント側に通知するリアルタイムアップデート機能が大助かりしました。Nuxt.js
実は今回始めてNuxt.jsを使ってみました(今まではVue.js)。サーバーサイドレンダリングを使わなかったらVueCLIでもいいかなというのが正直な感想です。ちなみにデザインやcssは苦手なのでVuetifyを使いました。Vuetifyはすごくいいライブラリなのですが、一度使い出したらなかなか抜けれないという沼のようなライブラリでもあります(個人の感想です)。
ゲーム部分の作り込み
ゲーム部分はVue.jsで作りました。友達の協力も得ながら、じゃんけん、早押しゲーム、ワードウルフなどを作ってみました。
ビデオチャット部分とゲーム部分は別々に動いているので、そのwindow間でプレイヤー情報等を送信するためにWebの仕様であるpostMessageを用いました(このAPIは今回初めて知りました)。
オンライン通信ゲームなので、ユーザー間でデータを同期しなければなりません。データ同期用にfirestoreを使うことを考えましたが、ゲームを作るたびにfirebaseでproject立ち上げて、、とやるのは些か面倒でした。ビデオチャット部分ではすでにfirebaseを使っていたので、上記と同様postMessageを活用することで、データ同期をビデオチャット経由で簡単に行えるようにしました。ここらへんの詳細はこちらにまとめました。作ってみたゲーム
いい感じのサムネを友達がいらすとやで作ってくれました。いらすとやすごい。感謝。
さらに、wh.im では、ゲーム部分を外に切り出したことで、ビデオチャットの部分を実装しなくても、簡単にオリジナルなゲームを作ることができます。フロントの技術をある程度持っていれば、自分のやりたいゲームを開発可能です。例えば...
- 麻雀
- サッカー
- おおぎり
- その他オリジナルゲーム
などなど...!
簡単に作れるように、アプリ開発のためのドキュメントをまとめたので、是非参考にしてください。開発からwh.im上での公開までに必要な情報をまとめています。まとめ
アイデアの着想したときの見積もりより壮大になり合計3週間ほどかかりました。しかしビデオチャットとゲームを分離することで、設計がシンプルになり、ゲームの開発に集中することができました。 wh.im を使うと、簡単にチャットビデオ上で動くゲームを作れるので、是非使ってみてください。
- 投稿日:2020-05-21T01:19:57+09:00
webカメラを使って、1000px ×1000px のプロフィール画像を量産してくれるサイトをつくった
完成品
サイト名:1000×1000 証明写真
https://id-photo.ml (ダメだったらこっち:https://id-photo.herokuapp.com/)作り方!!
・まずファイル構造はこんな感じ
今回herokuを使ってデプロイするために、gitやら何やらが入っています。(もしかしたら、必要ないファイルもあるかも。すみません...)
・コード記述内容
index.html<!DOCTYPE html> <html> <head> <title>1000×1000証明写真</title> </head> <body> <b>1000×1000証明写真</b> <div id="app"> <div> <video ref="video" id="video" width="500" height="500" autoplay></video> <div> <button color="info" id="snap" v-on:click="capture()"><img src="img/icon.png"></button> </div> <canvas ref="canvas" id="canvas" width="1000" height="1000"></canvas> <ul> <li class="capture" v-for="c in captures" v-bind:key="c.d"> <img v-bind:src="c" height="50" /> </li> </ul> </div> </div> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script> new Vue({ el: '#app', data: { video: {}, canvas: {}, captures: [] }, mounted () { this.video = this.$refs.video if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { navigator.mediaDevices.getUserMedia({ video: true }).then(stream => { this.video.srcObject = stream this.video.play() }) } }, methods: { capture () { this.canvas = this.$refs.canvas this.canvas.getContext('2d').drawImage(this.video, 0, 0, 1000, 1000) this.captures.push(this.canvas.toDataURL('image/png')) console.log(this.captures) } } }); </script> </body> </html> <style> #canvas { display: none; } .capture { /* display: inline; */ padding: 5px; } </style>index.jsconst express = require('express'); const app = express(); // public というフォルダに入れられた静的ファイルはそのまま表示 app.use(express.static(__dirname + '/public')); // bodyParser var bodyParser = require('body-parser'); app.use(bodyParser.json()); // POSTリクエストを受け付ける app.post('/post', function (req, res) { for (key in req.body) { console.log(key, '=', req.body[key]); } res.end(); res.send('hello world(POST)'); }); // GET リクエストを受け付ける app.get('/get', function (req, res) { res.send('hello world(GET)'); }); //app.listen(8080); app.listen(process.env.PORT || 8080); console.log("server start! (heroku)");.gitignore# Dependency directories node_modules/ # Optional npm cache directory .npmProcfileweb: node index.jsそのほかのファイルは下記コマンドからインストールできます。
npm init -y npm i body-parser express完成
触って遊んでみましょう。
step1「サイトへ飛ぶ」
https://id-photo.ml (ダメだったらこっち:https://id-photo.herokuapp.com/)
step2「カメラアイコンをクリックして撮影」
カメラのアイコンをクリックすると、1000px × 1000px の画像が下に量産されていきます。
step3「画像をダウンロード」
量産された画像の上で、右クリックをするとメニューがでるので、そこで「名前を付けて画像を保存...」を選択してください。
ダウンロードされた画像が、少し縦に伸びている感じになっているのはスリム効果です。
おわり
一応、今回使用した画像を貼っておきます。ご自由に使用してください。
また今回参考にさせていただいた記事です
・https://qiita.com/kino15/items/8f8feffca54015555f4b最後まで読んでいただきありがとうございました。