- 投稿日:2019-12-19T23:51:22+09:00
Vueで日本全国ダーツの旅的なものを作ってみた
テックタッチアドベントカレンダー19日目を担当する@kosyです。
18日目は @92thunder による Vuex × TypeScriptにvuex-smart-moduleを選んだ理由 でした。
vuex-smart-moduleがどう良いのかわかる内容で理解が深まりました。型に苦しめられた過去があるからこそ今の成長があるわけですね。
vuex-smart-moduleに即決定したスピード感、さすがテックタッチ。はじめに
日本全国ダーツの旅みたいな感じでランダムに市区町村出してくれるサイトを作りました。
場所だけじゃなく、おすすめ観光スポットも表示されます。
エンジニアに転職した今年4月当初に作ろうとしましたが、何をどうすれば良いのか全くわからず結局何も作れないまま終わりました。9ヶ月経った今なら作れる!ということで作ってみました。
ぜひ遊んでみてください。成果物
https://random-travel.netlify.com/
作成手順
1. 市区町村コードを取得できるAPIの準備
2. Puppeteerで観光地の情報収集
3. Vueでページを作る
4. Netlifyでアップロード1. 市区町村コードを取得できるAPIの準備
内閣府 地方創生推進室が提供するAPIで市区町村コードが取得できるため利用登録 → RESAS
↓こんな感じで都道府県コードを入力すると、その都道府県の市区町村一覧が取得できます。(市区町村一覧API){ "message": null, "result": [{ "prefCode": 1, "cityCode": "01100", "cityName": "札幌市", "bigCityFlag": "2" }, { "prefCode": 1, "cityCode": "01101", "cityName": "札幌市中央区", "bigCityFlag": "1" }, { … }都道府県コードのAPIでは、パラメータが指定できないので全都道府県のコードと名前が一括で返ってきます。なので、都道府県一覧は初回だけ取得したら一覧を保持しておきました。
登録できたらAPIキーが発行されるので、これを使ってリクエストを送ります。2. Puppeteerで観光地の情報収集
Puppeteerも業務で使用しているため勉強も兼ねて使用。
観光地情報はじゃらんから、地図画像はgoogleから取得しています。じゃらんにした理由は、人気観光地TOP3をまとめてくれている&地域毎の観光情報のURLが市区町村コード変えればそのページに行けるものなので情報収集しやすかったからです。
地図はgoogleにあった良い感じの画像のURLを取得して表示させています。
Google maps API使えば周辺地図も見れてよかったのかもしれませんが、市区町村の緯度経度をどこかから取得する必要があり時間と気力の問題から断念。Puppeteerは以前に書いた記事に操作方法をまとめています。基本的にはこの記事で書いた操作しかしていません。
Puppeteerで自動ブラウザ操作するときによく使う操作一覧収集したデータは市区町村ごとに{市区町村コード}.jsonファイルで保存し、vueでランダムに算出した市区町村コードのjsonファイルを呼び出しています。
3. Vueでページを作る
開発時は発行したAPIキーは、Vueのプロジェクト直下にenv.developmentファイルを作成し、そこに
VUE_APP_API_KEY=APIキーのように記載しておきます。
APIキーは悪用されると困るので直接コードに書き込むのではなく、環境変数で管理します。APIを使ってランダムで市区町村を取得するコードは以下のとおりです。
cityCodeList = { data: { message: null, result: [], statusCode: 0 }, status: 0 } // APIのレスポンスを格納する配列 all: any[] = [] prefCode: number = 0 resultCity = {} // 最終的に選ばれた市区町村を格納する変数 error: string = '' apiKey:string = process.env.VUE_APP_API_KEY config = { headers: { 'Content-Type': 'application/json', 'x-api-key': this.apiKey, }, } mounted() { this.calculateDestination(); } async calculateDestination() { this.prefCode = Math.floor((Math.random() * 47) + 1); // 都道府県を乱数で選定 // APIを使う await axios .get(`https://opendata.resas-portal.go.jp/api/v1/cities?prefCode=${this.prefCode}`, this.config) .then((response) => { this.cityCodeList = response; }) .catch((error) => { console.log(error); }); // レスポンスエラーになっていないか確認 if (this.checkNotError()) { // 市区町村を乱数で選定 const randomCity = Math.floor(Math.random() * this.cityCodeList.data.result.length); this.resultCity = this.cityCodeList.data.result[randomCity]; } } checkNotError() { if (this.cityCodeList.data.statusCode) { if (this.cityCodeList.data.statusCode === 429) { this.error = 'しばらく時間をおいてから試してみてください'; } else { this.error = '行き先を決められませんでした'; } return false; } return true; }観光情報を表示させる処理は以下の通りです。puppeteerで取得したJSONファイルを読み込むだけ。
async fetchSpotData(cityCode: string) { this.draftSpotData = await fetch(`./data/${cityCode}.json`) .then(response => response.json()); }4. Netlifyでアップロード
GitHubにコードをあげたら、NetlifyとGitHubを連携させデプロイします。
APIキーはNetlifyのSettings > Build&deploy > Environment で設定すればOK。
以降の更新はgitHubにpushする度に行われます。おわりに
アドベントカレンダーという機会がなければ、作成をずっと後回しにしてそうだったので今回は良い機会でした。
半年前は作れなかったものが作れるようになったのは嬉しい。明日20日目の担当は@mosimosiの「型論ではなくKatalon」です!乞うご期待!
- 投稿日:2019-12-19T22:50:15+09:00
DECK.GLを使ってGoogleMapタイムラインをビジュアライズしてみよう!#4 〜Mapコンポーネントの表示〜
DECK.GLを使ってGoogleMapのタイムラインをビジュアル化してみよう!の第4弾の記事です。
前回はDECK.GLのPathLayerを使って、データの描画を行いました。
今回はこれにマップコンポーネントを組み込んで表示させてみたいと思います。前回のおさらい
前回は、App.jsx に下記のコードを書きました。
App.jsimport React from "react"; import DeckGL from "@deck.gl/react"; import { PathLayer } from "@deck.gl/layers"; // Viewport 初期設定 const viewState = { longitude: -122.5, latitude: 37.75, zoom: 11 }; // 描画する地理データ const data = [ { path: [ [-122.45, 37.7], [-122.5, 37.8], [-122.6, 37.7] ] } ]; export default class App extends React.Component { render() { const layer = [ new PathLayer({ id: "path-layer", data, getPath: d => d.path, getWidth: 30 }) ]; return <DeckGL viewState={viewState} layers={layer}></DeckGL>; } }マップコンポーネント
DECK.GLはマップコンポーネントなしでも独立して動作しますが、地理データをビジュアライズするのに地図情報が表示されないのではおもしろくありませんよね。
DECK.GLと簡単に連携できるマップコンポーネントとして、GoogleMapとMapboxの2つの選択肢がありますが、
今回はMapboxを使って描画したいと思います。※マップコンポーネントの連携についての詳細はこちらをご参照ください。
Mapboxとは?
Mapboxとは、米カリフォルニア州サンフランシスコにある位置データプラットフォームです。要するに、GoogleMapのような地図サービスです。
最近はゼンリンと提携したり、「ソフトバンク・ビジョン・ファンド」から出資を受けたりと、日本での事業も強化しようとしているようです。
Mapboxでは、GoogleMap同様開発者に便利なインターフェースがたくさん提供されています。
例えば、Mapbox GL JSというJavaScriptライブラリが提供されていたり、
mapbox-glという名前でNodeパッケージも公開されています。今回はReactコンポーネントとして利用できるreact-map-glを使ってMapboxを連携させましょう。
※MapboxのDocumentationはこちら
- Mapbox GL JSのGithub
- react-map-glのGithubMapboxを連携させよう
react-map-glライブラリのインストールまず
react-map-glライブラリをインストールします。$ yarn add react-map-gl yarn add v1.17.3 [1/4] ? Resolving packages... [2/4] ? Fetching packages... [3/4] ? Linking dependencies... warning " > @testing-library/user-event@7.1.2" has unmet peer dependency "@testing-library/dom@>=5". warning "react-scripts > @typescript-eslint/eslint-plugin > tsutils@3.17.1" has unmet peer dependency "typescript@>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta". [4/4] ? Building fresh packages... success Saved lockfile. ... (長いので省略) ├─ tinyqueue@2.0.3 ├─ vt-pbf@3.1.1 └─ wgs84@0.0.0 ✨ Done in 24.60s. $MapGLコンポーネントのimport
インストールが終わったら、MapGLコンポーネントをimportしましょう。
また、Mapbox GL JS の css も import しておかないとwarningが出てしまうので、こちらもimportします。App.js(一部)import React from "react"; import DeckGL from "@deck.gl/react"; import { PathLayer } from "@deck.gl/layers"; import MapGL from "react-map-gl"; import "mapbox-gl/dist/mapbox-gl.css";Mapbox のアクセストークン
Mapbox を使う場合は、 Mapbox のアクセストークンを埋め込む必要があります。
アクセストークンは、Mapboxにログインして、「Access tokens」の
+ Create a tokenボタンから作成することができます。こちらからログインしてください
https://account.mapbox.com/
※アカウントを持っていない人は、アカウントの作成が必要ですアクセストークンが取得できたら、下記のように
App.jsxに書きます。
本来アクセストークンのような外部に漏れてはいけない情報は、.envファイルなどの環境変数で管理した方が良いですが、
Mapboxには、指定したURLから発信されたリクエストに対してのみ機能する「制限付きトークン」を作成することができるので、これであればファイルに埋め込んでも問題ありませんね。App.js(一部)// mapbox access token const MAPBOX_ACCESS_TOKEN = "pk.eyJ1IjoiZ......."MapGLコンポーネントの追加
それでは最後にDeckGLにマップコンポーネントを組み込みます。
上記で定義したアクセストークンはmapboxApiAccessTokenに渡します。App.js(一部)return ( <DeckGL viewState={viewState} layers={layer}> <MapGL mapboxApiAccessToken={MAPBOX_ACCESS_TOKEN} /> </DeckGL> );ブラウザで表示
それではここまで書いたコードをブラウザで表示してみましょう!
yarn startコマンドで起動させると・・・
地図の上にパスが表示されました!!
次回予告
次回からはいよいよGoogleMapタイムラインの表示を行なっていきます!
App.jsxの全体像
App.jsimport React from "react"; import DeckGL from "@deck.gl/react"; import { PathLayer } from "@deck.gl/layers"; import MapGL from "react-map-gl"; import "mapbox-gl/dist/mapbox-gl.css"; // mapbox access token const MAPBOX_ACCESS_TOKEN = "pk.eyJ1IjoiZ......."; // Viewport 初期設定 const viewState = { longitude: -122.5, latitude: 37.75, zoom: 11 }; // 描画する地理データ const data = [ { path: [ [-122.45, 37.7], [-122.5, 37.8], [-122.6, 37.7] ] } ]; export default class App extends React.Component { render() { const layer = [ new PathLayer({ id: "path-layer", data, getPath: d => d.path, getWidth: 30 }) ]; return ( <DeckGL viewState={viewState} layers={layer}> <MapGL mapboxApiAccessToken={MAPBOX_ACCESS_TOKEN} /> </DeckGL> ); } }
- 投稿日:2019-12-19T22:47:01+09:00
selectを使わず、datalistで複数選択を頑張ってつける
そもそもの理由
サジェスト機能付きで、単一と複数選択を実装する必要があった。(プラグインは使っていいのかわからない。つかえたらselect2を使うのが良い)
一般的サジェスト
HTML5タグリファレンス引用より、一般的なデータリストの使い方
<input type="search" autocomplete="off" list="keywords"> <datalist id="keywords"> <option value="ウィキペディア"> <option value="ウィルス対策"> <option value="ウィンドウズ"> </datalist>http://www.htmq.com/html5/datalist.shtml
ここのinputタグにmultipleをつけて、複数選択できればいいのだが、単一選択しかできない
方法1(JavaScriptで実装する)
separatorに指定した文字を入力した際に、次の選択候補が出るように実装
ただ、中身のoptionが増えるとすごくパフォーマンスが悪い(リスト再生成しているため)<script> $(function(){ const datalist = $('datalist'); const options = $('datalist option'); const optionsarray = $.map(options ,function(option) { return option.value; }); const input = $('input[list]'); let inputcommas = (input.val().match(/,/g)||[]).length; const separator = ','; function filldatalist(prefix) { if (input.val().includes(separator) && options.length > 0) { datalist.empty(); for (i=0; i < optionsarray.length; i++){ if (prefix.indexOf(optionsarray[i]) < 0 ) { datalist.append('<option value="'+prefix+optionsarray[i]+'">'); } } } } input.on("change paste keyup",function() { inputtrim = input.val().replace(/^\s+|\s+$/g, ""); const currentcommas = (input.val().match(/,/g)||[]).length; if (inputcommas !== currentcommas) { const lsIndex = inputtrim.lastIndexOf(separator); const str = (lsIndex != -1) ? inputtrim.substr(0, lsIndex)+"," : ""; filldatalist(str); inputcommas = currentcommas; } input.val(inputtrim); }); }); </script>送信する値が、selectの時と違うので、細工する必要がある。
方法2 input type emailとform
typeをemailにして、emailにすると入力値がメールアドレスじゃなかった時にバリデーションがかかるので、formにnovalidateをつけてバリデーションをしないようにする必要がある。
<form novalidate> <input type="email" autocomplete="off" name="sample" list="keywords" multiple> <datalist id="keywords"> <option value="ウィキペディア"> <option value="ウィルス対策"> <option value="ウィンドウズ"> </datalist> </form>送信する値が、selectの時と違うので、細工する必要がある。
方法3 select2プラグインを使用する
これが使えるなら、一番楽(サジェスト機能付きセレクトボックス)
multipleを外せば、単一選択のサジェスト機能付きセレクトボックスとなる。
また、送信される値がvalueという点もよい。今回はただ表示しただけだが、カスタマイズ性も高く色や幅などの調整をし、よりかっこよくできる。<select class="sample" name="sample" style="width:1000px" multiple> <option value="1">ウィキペディア</option> <option value="2">ウィルス対策</option> <option value="3">ウィンドウズ</option> </select> <script> $(function(){ $('.sample').select2(); }); </script>まとめ
datalistは、labelとvalueを指定できるが、両方記述すると両方表示される。
labelは指定しなくてもいいが、valueはしていする必要があり、表示されてしまうため、selectで送る際のvalueとは少し違う。
(selectはoptionタグで囲まれているものが表示され、送信される値はvalueといった程度)
以前の記事で、selectboxに送信値を合わせるように、datalistを細工したが、正直めんどくさかった。なぜselectと同じようにHTML側がつくってくれなかったのだろう?
- 投稿日:2019-12-19T22:18:07+09:00
github-pageにReact appをデプロイしてみた
はじめに
Reactを勉強する上でGithub pageでReactアプリを公開できるのか気になったのがきっかけです。
GitHub Pagesについて
GitHub Pages は、GitHub のリポジトリから HTML、CSS、および JavaScript ファイル を直接取得し、任意でビルドプロセスを通じてファイルを実行し、ウェブサイトを公開できる静的なサイトホスティングサービスです。
[GitHub 公式ヘルプ]https://help.github.com/ja/github/working-with-github-pages/about-github-pages参考にしたサイト
- GitHub PagesにReactアプリをデプロイする方法 https://qiita.com/yuitnnn/items/11375ea29ec023d19fdf
- React公式(INSTALLATION>新しいReactアプリを作る)
https://ja.reactjs.org/docs/create-a-new-react-app.html
環境
以下は私が行った環境です.
name version Node v10.14.2 npm 6.4.1 2019/12/19現在ではReact公式サイトに
あなたのマシンに Node >= 8.10 及び npm >= 5.6 の環境が必要です
と記載されていました.
手順
1.Reactアプリの作成
以下のコマンドを実行しました.
今回はreact-pagesという名前のReactアプリを作成しています.$ npx create-react-app react-pages2.gh-pagesのインストール
以下のコマンドで
gh-pagesをインストールします。
以下のコマンドではcd react-pagesで作成したreact-pagesフォルダまで移動しgh-pagesをインストールしています。$ cd react-pages $ npm install gh-pages --save-devnpm
Node.jsで利用するパッケージ管理ツール
gh-pages
GitHubのgh-pagesブランチ(または他の任意の場所)にファイルを公開するためのパッケージ
3.Reactアプリのpackage.jsonを修正
作成したReactアプリのpackage.jsonに以下の設定を追記してください.
ここで"homepage"に書いてあるURLの[GitHub username],[created repository name]はそれぞれ自分が作成した名前をつけてくださいpackage.json//... "homepage": "https://[GitHub username].github.io/[created repository name]/", "scripts": { "predeploy": "npm run build", "deploy": "gh-pages -d build" }, //...4.GitHubでRepositoryを作成
このときのリポジトリの名前は作成したReactアプリと異なっていても大丈夫です
また、GitHubがPROアカウントではない場合はPublicでRepositoryを作成してください。5.GitHubPageの設定
作成したGigHubのリポジトリよりsettingへ移動,その後下へスクロールしGitHub Pages のSourceをmaster branchへ変更する.
6.作成したアプリをデプロイ
以下のコードでデプロイします。また,実行する場所は作成したReactアプリ内で行ってください.以下の前半部分はリポジトリを作成したときに出てくる任意のフォルダーとリポジトリを接続し
README.mdをリポジトリへ追加しているものです.$ git init $ git add README.md $ git commit -m "first commit" $ git remote add origin $ git@github.com:[GitHub username]/[Repository name].git $ git push -u origin master $ npm run deploy結果
GitHubPages設定後表示もう一度GitHubPagesの設定場所へ移動し表示されているURLをクリック.
以下のようなページが開かれていれば成功です.最後に
デプロイの意味とか全然しらない初学者(私)でもちょっと調べたらできるもんだなぁと感激しました。
積極的にいろんなことにトライしてみようと思いました。
次はReactをつかって自分のポートフォリを作りたいと思います。
- 投稿日:2019-12-19T21:37:54+09:00
“console.log()” はもうやめて、ブラウザデバッガーを使い始めよう
こちらの記事は、Parag Zaveri 氏により2018年10月に公開された『 How to stop using console.log() and start using your browser’s debugger 』の和訳です。
本記事は原著者から許可を得た上で記事を公開しています。ソフトウェア開発者になる道を進み始めたとき、私も他の人と同じように、いくつかの困難にぶつかりました。中でも、新人開発者が直面する最も一般的な苦闘の1つは「デバッグ」です。Chrome でコンソールを開き、”console.log()”で値を取り出せば、バグの所在を見つけられることに気づいたとき、最初は聖杯を発見したと思いましたが、後で非常に効率が悪いことに気がつきました。笑い話として、私のお気に入りのユースケースをいくつか紹介します。
console.log( 'Total Price:'、total)//値が保存されたかどうか確認しようとして..
console.log( 'Here')//プログラムの実行が特定の関数に到達したかどうか..ほとんどの開発者は、 実際これがプログラムの(バッグ手法として好ましくないことに気づくでしょう。もっと良い方法があるはずです!
ありがたいことに、ブラウザにデバッグツールが付属しています。ここでは特に、Chrome デベロッパーツールについて説明していきます。
この投稿では、ブレークポイントの使用、コードのステップ実行、ウォッチ式の設定、Chromeデベロッパーツールでの修正の適用について説明します。
このチュートリアルを進めるには、私が作成したコードのサンプルリポジトリを使用する必要があります。こちらをクリックしてください。(ロードに数分かかる場合があります)
Step1:バグの再現
バグを常に再現する、一連のアクションを実行することから始めます。
- ここでは、バグのあるチップ計算機を使用します。サンプルコードを開いていない場合、こちらから開いてください。
- ‘Entree 1’ に 12 を入力
- ‘Entree 2’ に 8 を入力
- ‘Entree 3’ に 10 を入力
- ‘Tax’ に 10 を入力
- ‘Tip’ で 20% を選択
- ‘Calculate Bill’ をクリックします。合計は 39.6 になるはずですが、結果は大きく異なります。代わりに、 14105.09… と表示されます...うわっ!
Step2:ソースパネルの使い方を学ぶ
Chrome でこれをデバッグするには、デベロッパーツール の使用に慣れる必要があります。Chromeデベロッパーツールを開くには、 Command + Option + I(Mac) または Control + Shift + I(Linux) を押します。
訳注:マウス操作では Chrome のオプションを開き「その他のツール」から開くことが可能。上部の ”sources” パネルをクリックすると、デバッグ用の3つのパネルにアクセスできるはずですーファイルナビゲーター、ソースコードエディター、およびデバッグペイン。Step 3に進む前に、いろいろクリックしてみてください。
Step3:ブレークポイントを設定する
最初のブレークポイント設定方法を示す前に、 ”console.log()” を使用した場合を先に見てみましょう。明らかにプログラム上では、小計の計算と並行して何かの処理が進んでいるようです。以下のようにして、プログラムをデバッグすることもできます。
幸いなことに、ブラウザの開発ツールでは、これはもう必要ありません。代わりに、単にブレークポイントを設定し、コードをステップ実行して、ブラウザーが認識している値を見つけることができます。
ブレークポイントを設定する方法について見ていきましょう。ブレークポイントとは、コードの実行を一時停止させるタイミングを知るためにブラウザが探すもので、これによりあなたは停止時点でデバッグを行うことが可能になります。
上記の目的のために、マウスイベントの設定をすることで、プログラム実行の冒頭部分にブレークポイントを設定します。
デバッガーペインで、 ”Event Listener Breakpoints” のビューを展開します。そこから、 ”Mouse” を展開し、 ”Click” にチェックマークを付けます。
“Calculate Bill” ボタンをクリックすると、最初の ”onClick()” 関数の最初の行で、デバッガーは実行を一時停止します。デバッガーがそれ以外のどんな場所にいても、再生ボタンを押すとデバッガーはそれ(一時停止)をスキップします。
Srep4:コードのステップ実行
すべてのデバッグツールで、ユーザーはコードの実行に関する2つのオプションをナビゲーションから選択できます。次の関数の呼び出しで ”step into” するか “step over” することが可能です。
“step into” を選択すると、各関数内でコードの各行が1つずつ実行されます。
次の関数呼び出しを“step into” する“step over” を選択すると、動作確認済みの関数全体をスキップできます。
次の関数呼び出しを“step over” するコードをステップ実行する例を次に示します。画面右側の “scope” タブの配下に最初の3つの ”Entree” の値が表示されています。
Step5:コード行ブレークポイントを設定する
Wow! コードをステップ実行できるのは素晴らしいことですが、ちょっとかさばって面倒ですよね?通常は、特定の場所の値のみを知りたいはずです。はい、その問題の解決策が、コード行ブレークポイントです。
著者注:このコード行ブレークポイントが、まさに”console.log()” の使用をやめ、Chrome デベロッパーツールをお勧めした理由です。
コード行ブレークポイントを設定するには、詳細情報を表示させたい行番号をクリックするだけです。その後、通常どおりにコードを実行すると、設定されたコード行ブレークポイントで実行が停止します。これですべての関数で “step into” , “step over” をいちいち選択しなくても良くなります。
注:問題がある場合は、”mouse” > “click” のチェックボックスが選択解除されていることを確認してください。
ご覧のとおり、 10812 の値を要求したときの小計の値が表示されています。”Entree” の値は ”scope” ペインにも表示され、コード上にオーバーレイで表示されます。
訳注:「 10812 の値を要求」・・・”Entree1”に 10, “Entree2”に 8, “Entree3”に 12の値を要求したときに 10812 として小計を処理している。→以下で修正の話に繋がるうむむ… バグが見つかったようですね...文字列として連結したのは誰かな?
ではいくつかのウォッチ式を設定して、確認してみましょう。
Step6:ウォッチ式を作成する
さて、これで”Entree” の値が適切に加算されていないことがわかったので、各値にウォッチ式を設定しましょう。
ウォッチ式は、コード内の特定の変数または式に関する詳細情報を提供します
「ウォッチ(監視)」する値を定義するには、一番上の “Watch” ペインをクリックし、開いたら + 記号をクリックします。ここで、変数名または他の式を入力できます。
デモでは、 ”Entree1” の値と、その値のタイプにウォッチ式を設定します。
なるほど! 問題が見つかったようです。 ”Entree1” は文字列として保存されているようです!値自体を獲得する方法に起因しているようです。おそらく、”querySelector()” が原因です。他のいくつかの値も同様に影響を受ける可能性があるので、デベロッパーツールでさらにデバッグを進め、コードを修正していきましょう。
Step7:コードの修正
さらに調査してみると、”querySelector()” が犯人で間違いありません!
では、これをどのように修正すればよいのでしょうか?そうですね、例えば 74行目のように”Number(getEntree1())” を使用して、単純に文字列を強制的に数値に変換できます。
実際にコードを編集するには、”sources” の左にある “element” パネルに移動する必要があります。JavaScript コードが表示されない場合は、”script” タグを展開する必要があります。そこからコードを右クリックし、”edit as html” を選択しましょう。
ワークスペースを使用している場合、コードの保存は簡単ですぐに表示できます。そうでない場合は、command + s(mac) または control + s(linux) を使用して Web ページのローカルコピーを保存する必要があります。
そうすればローカルコピーを開くことができるので、変更を表示できます。
出来上がり!
このデバッグ方法論は、Kayce Basques 様の “Get Started with Debugging Javascript in Chrome DevTools” という記事からインスピレーションを得て、developers.google.comでも採用されました。
Demo Code: https://github.com/paragzaveri/chromeDevTools翻訳協力
Original Author: Parag Zaveri
Thank you for letting us share your knowledge!この記事は以下の方々のご協力により公開する事が出来ました。
改めて感謝致します。
選定担当: @aoharu
翻訳担当: @upaldus
監査担当: @nyorochan
公開担当: @aoharu私たちと一緒に記事を作りませんか?
私たちは、海外の良質な記事を複数の優秀なエンジニアの方の協力を経て、日本語に翻訳し記事を公開しています。
活動に共感していただける方、良質な記事を多くの方に広めることに興味のある方は、ぜひご連絡ください。
Mailでタイトルを「参加希望」としたうえでメッセージをいただく、もしくはTwitterでメッセージをいただければ、選考のちお手伝いして頂ける部分についてご紹介させていただく事が可能です。
※ 頂いたメッセージには必ずご返信させて頂きます。ご意見・ご感想をお待ちしております
今回の記事は、いかがだったでしょうか?
・こうしたら良かった、もっとこうして欲しい、こうした方が良いのではないか
・こういったところが良かった
などなど、率直なご意見を募集しております。
いただいたお声は、今後の記事の質向上に役立たせていただきますので、お気軽にコメント欄にてご投稿ください。Twitterでもご意見を受け付けております。
みなさまのメッセージをお待ちしております。
- 投稿日:2019-12-19T21:19:20+09:00
GeoChart (Google Chart Tools)で世界を細かく塗りつぶそう
こんにちわ。Patheeの廣瀬です
世界の行ったところを塗りつぶすサービス
ExplorStateを、
GeoChart(Google Chart Tools)を使って作ってみました今回はこのサービス作成の際に得られたGeoChartのTipsなどを紹介していきます
ExplorState is 何
世界中で旅行した場所を自治体単位で塗りつぶしてドヤるサイトです
都道府県だと経県値さん、国だと色々(MapColoとか)ありますが、
世界の自治体単位のものがなかったので作りましたGeoChart
Google様謹製のチャートライブラリ
Google Chart Toolsの機能の一部で、
データを食わせると簡単に地図を塗りつぶしてくれます
https://developers.google.com/chart/interactive/docs/gallery/geochartこのライブラリはGoogle Analytics向けに作られたものをライブラリ化したようで、
データの範囲やクリックイベントが仕込める場所などが一緒です
(analyticsの地図)
簡単な使い方や日本地図・世界地図を描画するサンプルは日本語文献でもそれなりに出てきますが、
もうちょっと突っ込んだ使い方をご紹介したいと思いますregion
Chartのoptionに指定するregionには、エリアを示すコードを入力します
コードの種類によって四段階で範囲の表示が変わります世界
null or "world" (どちらを使っても表示は一緒)
大陸
亜大陸
国
詳細はContinent Hierarchy and Codesを参照
クリックイベントと組み合わせて、範囲を徐々に絞って表示していくなどの表現ができます
data (地域コード)
大陸、亜大陸、国のデータは上記のContinent Hierarchy and Codesを見れば、塗りつぶす先のデータが分かります
では、自治体の地域コードは?
アメリカのアラスカだと"US-AK"、日本の北海道だと"JP-01"などですが、
先ほどのContinent Hierarchy and Codesには見当たりません
このページのエリアコードがリンクになっているので飛びます
アメリカだとこんなWikipediaのページに飛びます
さらにこのページからもう一つリンクに飛ぶ必要があります。ISO 3166-2:USの部分です
これでようやく自治体の地域コードを手に入れることができました。お疲れ様です
僕のサービスでは存在する全ての国家のデータを、Wikipediaをめぐって手に入れました日本語で情報が欲しい場合は、URLのenをjaにすると良いです
https://en.wikipedia.org/wiki/ISO_3166-2:US
↓
https://ja.wikipedia.org/wiki/ISO_3166-2:US
ただし、日本語情報は古いことがあります
日本ではトップレベルの自治体、つまり都道府県の追加・合併・廃止などは沖縄返還を最後に50年近く行われていませんが、
世界ではままあるようですまた、GeoChartの方も更新についていけていないようで、
こうやって集めてもうまく表示されない自治体があります。
その他にもデータには色々苦労があったのですが、それはまたの機会に
- 投稿日:2019-12-19T20:58:43+09:00
ローグライクゲームを作ってみるその6 ゲームメッセージとプレイヤーのステータス
過去記事一覧
現在のコードについては前回の記事の最後の項を参照してください。
ゲームメッセージ
今回は初めにゲームメッセージを出力する機能を実装します。
ゲームメッセージはゲーム画面の右下の部分に描画することにします。
まず、ゲームメッセージを保持するための
messages変数を用意します。ゲームメッセージはメッセージを表すオブジェクトの配列として保持することにします。var messages = null;次に、保持するメッセージの最大数を
NUM_MESSAGE変数で定義します。保持するメッセージの最大数は8個とします。var NUM_MESSAGE = 8;次に、メッセージを追加するための
add_message関数を実装します。第1引数にはメッセージを表すオブジェクトを渡すものとします。
メッセージを表すオブジェクトは下のようなプロパティを有するものとします。
text・・・メッセージテキストです。type・・・メッセージの種類です。repeat・・・メッセージの繰り返し回数を表します。同じメッセージが連続して出力された場合には繰り返し回数を増加させることによって同じメッセージが連続して出力されたことを表現することにします。この関数では追加されたメッセージが直前に追加されたメッセージと同じものである場合には直前に追加されたメッセージの繰り返し回数を
1増加させ、異なるものである場合にはmessages変数にメッセージを追加します。ただし、保持されているメッセージが8個より多い場合には古いメッセージを削除するようにします。
function add_message (message) { var l = messages[messages.length - 1]; if (message.text === l.text && message.type === l.type) { if (!l.repeat) { l.repeat = 2; } else { l.repeat++; } } else { messages.push(message); while (messages.length > NUM_MESSAGE) { messages.shift(); } } }最後に、
draw関数にメッセージを描画する処理を追加します。メッセージの種類に応じてメッセージの文字色を変更します。
また、メッセージに繰り返し回数が設定されている場合には繰り返し回数も表記するようにします。
con.save(); con.textBaseline = 'top'; con.textAlign = 'left'; con.font = '16px consolas'; con.translate(SX * PX, SCREEN_Y - ((16 + 6) * NUM_MESSAGE + 8 * 2)); for (var i = 0; i < messages.length; i++) { if (messages[i].type === 'normal') { con.fillStyle = 'white'; } else if (messages[i].type === 'special') { con.fillStyle = 'yellow'; } else { throw new Error('not supported.'); } var text = messages[i].text; if (messages[i].repeat) { text += '(' + 'x' + messages[i].repeat + ')'; } con.fillText(text, 8, (16 + 6) * i + 8); } con.restore();これでゲームメッセージを出力する機能が実装できました。
メッセージの追加
実際に幾つかメッセージを出力してみましょう。
まず、メッセージテキストを定義します。
3つのメッセージを出力するようにしてみます。
1つ目のメッセージは新しいゲームを開始した時に出力します。
2つ目のメッセージは下り階段を降りた場合に出力します。
3つ目のメッセージは壁に向かって歩こうとした場合に出力します。
var MSG_INIT = 'あなたは目覚めました。'; var MSG_DOWNSTAIR = '下り階段を降りました。'; var MSG_WALL = '壁に阻まれました。';1つ目のメッセージは
init関数で追加します。
init関数を下のように変更します。種類が
specialでテキストがMSG_INITのメッセージを追加します。function init () { fields = []; fields[0] = create_field(0, [], seed); player = { depth: 0, x: 12, y: 17 }; messages = [{ text: MSG_INIT, type: 'special' }]; }2つ目と3つ目のメッセージは
keydownイベントハンドラで追加します。2つ目の
keydownイベントハンドラを下のように変更します。移動しようとしたマスの種類が壁であった場合に種類が
normalでテキストがMSG_WALLのメッセージを追加します。また、下り階段から1つ下の階に移動した場合に種類が
normalでテキストがMSG_DOWNSTAIRのメッセージを追加します。c.on('keydown', function (e) { (省略) if (e.keyCode >= 37 && e.keyCode <= 40) { (省略) if (x !== player.x || y !== player.y) { var block = fields[player.depth].blocks[x][y]; if (B_CAN_STAND[block.base]) { player.x = x; player.y = y; draw(con, env); } else { if (block.base === B_WALL) { add_message({ text: MSG_WALL, type: 'normal' }); draw(con, env); } return; } } else { return; } } else if (e.keyCode === 32) { var block = fields[player.depth].blocks[player.x][player.y]; if (block.base === B_DOWNSTAIR) { player.depth++; if (!fields[player.depth]) { fields[player.depth] = create_field(player.depth, [{ x: player.x, y: player.y }], seed); } add_message({ text: MSG_DOWNSTAIR, type: 'normal' }); } else { return; } } else { return; } draw(con, env); });メッセージが表示されたゲーム画面は下のようになります。
プレイヤーのステータスの描画
次にプレイヤーのステータスを描画します。
と言っても、現在プレイヤーの状態として存在する値は、
player変数に格納されるdepth、x、yのみです。この中で
xとyは敢えてゲーム画面に表示する必要はありませんが、depthは表示すべきでしょう。また、今後ゲームを開発していくにつれてプレイヤーの状態として様々なものが追加されていくことになるはずです。
その中の幾つかはゲーム画面に表示させるべきものとなるでしょう。
そこで、今後はゲーム画面の右上部分にプレイヤーのステータスを表示していくことにします。
今回はプレイヤーが何階のフロアにいるかを表示する処理を追加してみることにします。
ゲーム画面に描画するテキストを保持する変数を追加します。
var TEXT_DEPTH = '階';
draw関数に下のコードを追加します。con.save(); con.textBaseline = 'top'; con.textAlign = 'left'; con.font = '24px consolas'; con.fillStyle = 'white'; con.translate(SX * PX, 0); con.fillText(player.depth + TEXT_DEPTH, 8, (24 + 6) * 0 + 8); con.restore();これでプレイヤーのいる階がゲーム画面に表示されるようになりました。
ゲーム画面は下のようになります。
今回はここまで
今回はここまでです。
game.jsは下のようになりました。var TITLE = 'シンプルローグライク'; var TEXT_START = 'はじめる'; var TEXT_DEPTH = '階'; var MSG_INIT = 'あなたは目覚めました。'; var MSG_DOWNSTAIR = '下り階段を降りました。'; var MSG_WALL = '壁に阻まれました。'; var SCREEN_X = 1600; var SCREEN_Y = 800; var SX = 25; var SY = 25; var PX = 32; var PY = 32; var B_FLOOR = 0; var B_WALL = 1; var B_DOWNSTAIR = 2; var B_CAN_STAND = []; B_CAN_STAND[B_FLOOR] = true; B_CAN_STAND[B_WALL] = false; B_CAN_STAND[B_DOWNSTAIR] = true; var NUM_MESSAGE = 8; var img = new Image(); img.src = 'Dungeon_B_Freem7.png'; var seed = Date.now().toString(10); var startf = false; var fields = null; var player = null; var messages = null; $(function(){ var canvas = document.getElementById('game'); var con = canvas.getContext('2d'); var keyl = false; var keyu = false; var keyr = false; var keyd = false; var env = { diagonal: false }; var c = $('body'); c.on('keydown', function (e) { if (e.keyCode === 37) { keyl = true; } else if (e.keyCode === 38) { keyu = true; } else if (e.keyCode === 39) { keyr = true; } else if (e.keyCode === 40) { keyd = true; } else { keyl = false; keyu = false; keyr = false; keyd = false; } }); c.on('keyup', function (e) { if (e.keyCode === 37) { keyl = false; } else if (e.keyCode === 38) { keyu = false; } else if (e.keyCode === 39) { keyr = false; } else if (e.keyCode === 40) { keyd = false; } }); c.on('keydown', function (e) { if (!startf) { if (e.keyCode === 90) { startf = true; init(); draw(con, env); } return; } if (e.keyCode === 16) { if (!env.diagonal) { env.diagonal = true; draw(con, env); } return; } if (e.keyCode >= 37 && e.keyCode <= 40) { var nx = fields[player.depth].nx; var ny = fields[player.depth].ny; var x = player.x; var y = player.y; if (e.shiftKey) { if (keyl && keyu) { if (x === 0 || y === 0) { return; } x--; y--; } else if (keyr && keyu) { if (x === nx - 1 || y === 0) { return; } x++; y--; } else if (keyl && keyd) { if (x === 0 || y === ny - 1) { return; } x--; y++; } else if (keyr && keyd) { if (x === nx - 1 || y === ny - 1) { return; } x++; y++; } else { return; } } else { if (e.keyCode === 37) { if (x === 0) { return; } x--; } else if (e.keyCode === 38) { if (y === 0) { return; } y--; } else if (e.keyCode === 39) { if (x === nx - 1) { return; } x++; } else if (e.keyCode === 40) { if (y === ny - 1) { return; } y++; } } if (x !== player.x || y !== player.y) { var block = fields[player.depth].blocks[x][y]; if (B_CAN_STAND[block.base]) { player.x = x; player.y = y; } else { if (block.base === B_WALL) { add_message({ text: MSG_WALL, type: 'normal' }); draw(con, env); } return; } } else { return; } } else if (e.keyCode === 32) { var block = fields[player.depth].blocks[player.x][player.y]; if (block.base === B_DOWNSTAIR) { player.depth++; if (!fields[player.depth]) { fields[player.depth] = create_field(player.depth, [{ x: player.x, y: player.y }], seed); } add_message({ text: MSG_DOWNSTAIR, type: 'normal' }); } else { return; } } else { return; } draw(con, env); }); c.on('keyup', function (e) { if (e.keyCode === 16) { if (env.diagonal) { env.diagonal = false; draw(con, env); } } }); $(window).on('blur', function (e) { if (env.diagonal) { env.diagonal = false; draw(con, env); } }); draw(con, env); }); function init () { fields = []; fields[0] = create_field(0, [], seed); player = { depth: 0, x: 12, y: 17 }; messages = [{ text: MSG_INIT, type: 'special' }]; } function add_message (message) { var l = messages[messages.length - 1]; if (message.text === l.text && message.type === l.type) { if (!l.repeat) { l.repeat = 2; } else { l.repeat++; } } else { messages.push(message); while (messages.length > NUM_MESSAGE) { messages.shift(); } } } function create_field (depth, upstairs, base_seed) { var random = new Random(base_seed + ',' + depth.toString(10)); var nx = 25; var ny = 25; if (depth > 0) { nx = 50; ny = 50; } var blocks = []; for (var i = 0; i < nx; i++) { blocks[i] = []; for (var j = 0; j < ny; j++) { if ((i === 0 || j === 0) || (i === nx - 1 || j === ny - 1)) { blocks[i][j] = { base: B_WALL }; } else { blocks[i][j] = { base: B_FLOOR }; } } } if (depth === 0) { blocks[12][5] = { base: B_DOWNSTAIR }; return { nx: nx, ny: ny, blocks: blocks }; } var rs = [{ x1: 1, x2: nx - 2, y1: 1, y2: ny - 2 }]; var ers = []; var dps = [1, 1, 1, 1, 1, 1, 0.5, 0.5, 0.5, 0.5]; while (rs.length > 0 && dps.length > 0) { var r = rs.shift(); var nrs = split_room(blocks, r, dps.shift(), random); for (var i = 0; i < nrs.length; i++) { rs.push(nrs[i]); } if (nrs.length === 0) { ers.push(r); } } while (rs.length > 0) { ers.push(rs.shift()); } var nds = 1; while (nds > 0) { var x = random.num(nx - 2) + 1; var y = random.num(ny - 2) + 1; var f = true; for (var i = 0; i < upstairs.length; i++) { if (x === upstairs[i].x && y === upstairs[i].y) { f = false; break; } } if (f) { blocks[x][y].base = B_DOWNSTAIR; nds--; } } for (var i = 0; i < upstairs.length; i++) { if (blocks[upstairs[i].x][upstairs[i].y].base = B_WALL) { blocks[upstairs[i].x][upstairs[i].y].base = B_FLOOR; } } return { nx: nx, ny: ny, blocks: blocks }; } function split_room (blocks, r, dp, random) { var ap = random.fraction(); if (ap <= dp) { var dir = random.num(2); if (r.x2 - r.x1 > (r.y2 - r.y1) * 2) { dir = 0; } else if ((r.x2 - r.x1) * 2 < r.y2 - r.y1) { dir = 1; } if (dir === 0) { if (r.x2 - r.x1 <= 6) { return []; } var x = random.num(r.x2 - r.x1 - 6) + 3 + r.x1; if (blocks[x][r.y1 - 1].base !== B_WALL) { return []; } if (blocks[x][r.y2 + 1].base !== B_WALL) { return []; } var y = random.num(r.y2 - r.y1) + r.y1; for (var i = r.y1; i <= r.y2; i++) { if (i !== y) { blocks[x][i].base = B_WALL; } } var r1 = { x1: r.x1, x2: x - 1, y1: r.y1, y2: r.y2 }; var r2 = { x1: x + 1, x2: r.x2, y1: r.y1, y2: r.y2 }; var ord = random.num(2); if (ord === 0) { return [r1, r2]; } else { return [r2, r1]; } } else if (dir === 1) { if (r.y2 - r.y1 <= 6) { return []; } var y = random.num(r.y2 - r.y1 - 6) + 3 + r.y1; if (blocks[r.x1 - 1][y].base !== B_WALL) { return []; } if (blocks[r.x2 + 1][y].base !== B_WALL) { return []; } var x = random.num(r.x2 - r.x1) + r.x1; for (var i = r.x1; i <= r.x2; i++) { if (i !== x) { blocks[i][y].base = B_WALL; } } var r1 = { x1: r.x1, x2: r.x2, y1: r.y1, y2: y - 1 }; var r2 = { x1: r.x1, x2: r.x2, y1: y + 1, y2: r.y2 }; var ord = random.num(2); if (ord === 0) { return [r1, r2]; } else { return [r2, r1]; } } } return []; } function draw (con, env) { con.fillStyle = 'black'; con.fillRect(0, 0, SCREEN_X, SCREEN_Y); if (!startf) { con.textBaseline = 'alphabetic'; con.textAlign = 'center'; con.fillStyle = 'white'; con.font = '48px consolas'; con.fillText(TITLE, SCREEN_X / 2, SCREEN_Y / 4); con.font = '32px consolas'; con.fillText('> ' + TEXT_START, SCREEN_X / 2, SCREEN_Y / 4 * 3); return; } var nx = fields[player.depth].nx; var ny = fields[player.depth].ny; var ox = 0; if (player.x <= Math.floor(SX / 2)) { ox = 0; } else if (player.x >= nx - Math.floor(SX / 2)) { ox = nx - SX; } else { ox = player.x - Math.floor(SX / 2); } var oy = 0; if (player.y <= Math.floor(SY / 2)) { oy = 0; } else if (player.y >= ny - Math.floor(SY / 2)) { oy = ny - SY; } else { oy = player.y - Math.floor(SY / 2); } for (var i = 0; i < SX; i++) { for (var j = 0; j < SY; j++) { var block = fields[player.depth].blocks[ox + i][oy + j]; if (block.base === B_FLOOR) { con.fillStyle = 'white'; con.beginPath(); con.arc((i + 0.5) * PX, (j + 0.5) * PY, 1, 0, Math.PI * 2); con.closePath(); con.fill(); } else if (block.base === B_WALL) { con.strokeStyle = 'white'; con.strokeRect(i * PX, j * PY, PX, PY); con.beginPath(); con.moveTo(i * PX, j * PY); con.lineTo((i + 1) * PX, (j + 1) * PY); con.moveTo((i + 1) * PX, j * PY); con.lineTo(i * PX, (j + 1) * PY); con.closePath(); con.stroke(); } else if (block.base === B_DOWNSTAIR) { con.drawImage(img, 4 * 32, 5 * 32, 32, 32, i * PX, j * PY, PX, PY); } } } var px = player.x - ox; var py = player.y - oy; con.textBaseline = 'middle'; con.textAlign = 'center'; con.fillStyle = 'red'; con.font = '24px consolas'; con.fillText('?\uFE0E', px * PX + (PX / 2), py * PY + (PY / 2)); if (env.diagonal) { con.save(); con.strokeStyle = 'white'; con.translate(px * PX + (PX / 2), py * PY + (PY / 2)); con.rotate(Math.PI / 4); con.beginPath(); con.moveTo((PX / 2) + 4, -4); con.lineTo((PX / 2) + 4 + 8, -4); con.lineTo((PX / 2) + 4 + 8, -4 - 4); con.lineTo((PX / 2) + 4 + 8 + 8, 0); con.lineTo((PX / 2) + 4 + 8, 4 + 4); con.lineTo((PX / 2) + 4 + 8, 4); con.lineTo((PX / 2) + 4, 4); con.closePath(); con.stroke(); con.rotate(Math.PI / 4 * 2); con.beginPath(); con.moveTo((PX / 2) + 4, -4); con.lineTo((PX / 2) + 4 + 8, -4); con.lineTo((PX / 2) + 4 + 8, -4 - 4); con.lineTo((PX / 2) + 4 + 8 + 8, 0); con.lineTo((PX / 2) + 4 + 8, 4 + 4); con.lineTo((PX / 2) + 4 + 8, 4); con.lineTo((PX / 2) + 4, 4); con.closePath(); con.stroke(); con.rotate(Math.PI / 4 * 2); con.beginPath(); con.moveTo((PX / 2) + 4, -4); con.lineTo((PX / 2) + 4 + 8, -4); con.lineTo((PX / 2) + 4 + 8, -4 - 4); con.lineTo((PX / 2) + 4 + 8 + 8, 0); con.lineTo((PX / 2) + 4 + 8, 4 + 4); con.lineTo((PX / 2) + 4 + 8, 4); con.lineTo((PX / 2) + 4, 4); con.closePath(); con.stroke(); con.rotate(Math.PI / 4 * 2); con.beginPath(); con.moveTo((PX / 2) + 4, -4); con.lineTo((PX / 2) + 4 + 8, -4); con.lineTo((PX / 2) + 4 + 8, -4 - 4); con.lineTo((PX / 2) + 4 + 8 + 8, 0); con.lineTo((PX / 2) + 4 + 8, 4 + 4); con.lineTo((PX / 2) + 4 + 8, 4); con.lineTo((PX / 2) + 4, 4); con.closePath(); con.stroke(); con.restore(); } con.save(); con.textBaseline = 'top'; con.textAlign = 'left'; con.font = '24px consolas'; con.fillStyle = 'white'; con.translate(SX * PX, 0); con.fillText(player.depth + TEXT_DEPTH, 8, (24 + 6) * 0 + 8); con.restore(); con.save(); con.textBaseline = 'top'; con.textAlign = 'left'; con.font = '16px consolas'; con.translate(SX * PX, SCREEN_Y - ((16 + 6) * NUM_MESSAGE + 8 * 2)); for (var i = 0; i < messages.length; i++) { if (messages[i].type === 'normal') { con.fillStyle = 'white'; } else if (messages[i].type === 'special') { con.fillStyle = 'yellow'; } else { throw new Error('not supported.'); } var text = messages[i].text; if (messages[i].repeat) { text += '(' + 'x' + messages[i].repeat + ')'; } con.fillText(text, 8, (16 + 6) * i + 8); } con.restore(); } function hash (seed) { var sha256 = new jsSHA('SHA-256', 'TEXT'); sha256.update(seed); return sha256.getHash('HEX'); } class Random { constructor (seed) { this.seed = seed; this.hash = hash(seed); this.pointer = 0; } byte () { if (this.pointer === 64) { this.hash = hash(this.hash); this.pointer = 0; } var value = this.hash.substring(this.pointer, this.pointer + 2); this.pointer += 2; return parseInt(value, 16); } num (max) { if (max <= 0) { throw new Error('max of random.num must be positive.'); } else if (max <= 256) { return this.byte() % max; } else { throw new Error('not supported.'); } } fraction () { return this.byte() / 256; } } function test_random_class_byte () { var a = [68, 9, 150, 66, 71, 184, 42, 152, 84, 31, 148, 195, 79, 121, 253, 235, 87, 142, 108, 87, 64, 95, 18, 186, 184, 92, 200, 43, 179, 155, 117, 136, 209, 241, 173, 107, 190, 11, 178, 50]; var r = new Random('yurina'); for (var i = 0; i < a.length; i++) { if (r.byte() !== a[i]) { throw new Error('test_random_class_byte'); } } } function test_random_class_num () { var a = [0, 1, 0, 2, 1, 4, 0, 0, 3, 1, 5, 3, 1, 9, 13, 11]; var r = new Random('yurina'); for (var i = 0; i < a.length; i++) { if (r.num(i + 1) !== a[i]) { throw new Error('test_random_class_num'); } } }次回は敵キャラクターと満腹度について考えたいと思います。
- 投稿日:2019-12-19T20:52:26+09:00
年末まで毎日webサイトを作り続ける大学生 〜62日目 インベーダーゲーム 当たり判定の完成!〜
はじめに
こんにちは!@70days_jsです。
55日目,61日目にインベーダーゲームを作ろうとしてできませんでした。
それが、ついに、今日!
一応完成しました!とはいえ、ビームを出したら敵が消えて、敵が下まで到達するとゲームオーバーになるというシンプルゲームです。
バグ検証はしていないので、もしかしたら変な動作をするところがあるかもしれません。
あらかじめご了承ください。62日目。(2019/12/19)
よろしくお願いします。サイトURL
https://sin2cos21.github.io/day62.html
やったこと
コードは昨日の記事(年末まで毎日webサイトを作り続ける大学生 〜61日目 インベーダーゲームのリベンジ〜)とほぼ同じなので、変更点だけ書いていきます。
主な変更点は、当たり判定と、上まで行ったビームの削除、ゲームオーバーの機能です。
当たり判定↓
//関数定義 function hit(beam, enemies) { for (var i = 0; enemies.length > i; i++) { let oneEnemy = enemies[i]; if ( (oneEnemy.x < beam.x && oneEnemy.x + enemyWidth > beam.x && oneEnemy.y + enemyHeight > beam.y) || (oneEnemy.x > beam.x && oneEnemy.x < beam.x + oWidth) ) { enemies.splice(i, 1); return true; } } } //ここで使っています↓ if (beams) { for (var i = 0; beams.length > i; i++) { let oneBeam = beams[i]; oneBeam.render(); let hitOrNoHit = hit(oneBeam, enemies);//ここでhit関数を使う if (hitOrNoHit) beams.splice(i, 1); if (oneBeam.y + oHeight < 0) beams.splice(i, 1); console.log(oneBeam.y); } }hit関数はもしビームと敵が被ったらtrueを返すようになっているので、trueが返ったらビームを消すようにしています。
敵の削除はhit関数の中で行なっています。
今回オブジェクトは機体とビームと敵の三つ用意したんですが、当たり判定はどれにも属さないと思いメソッドではなく関数にしました。
ただ、今回作ったものは再利用性がないので改善しないとですね。次に上まで行ったビームの削除ですが、昨日の段階でゲームをし続けるとものすごく重くなっていたのは、この上まで行ったビームを削除してないからだということがわかりました。↓
if (beams) { for (var i = 0; beams.length > i; i++) { let oneBeam = beams[i]; oneBeam.render(); let hitOrNoHit = hit(oneBeam, enemies); if (hitOrNoHit) beams.splice(i, 1); if (oneBeam.y + oHeight < 0) beams.splice(i, 1);//←上までいったビームの削除! console.log(oneBeam.y); } }最後にゲームオーバーの機能です。↓
//ゲームオーバー if (enemy.y >= h - machineHeight) { clearInterval(enemyCreate); enemies.length = 0; beams.length = 0; alert("GAME OVER!!!"); }setInterval()で一秒ごとに敵を作り続けているのでまずそれをclearInterval()消して、↓
let enemyCreate = setInterval(function() {
let enemy = new Enemy(ctx);
enemies.push(enemy);
}, 1000);敵とビームを入れている配列のlengthを0にします。
今日初めて知ったのですが、配列はlengthを0にすると要素を空にしたのと同じことになるようです。
削除(splice)するか迷いましたが、あとでもう一回する機能をつけるときのことを考え、空にするだけにしました。
そして最後にアラートでGAME OVER!!!と出てゲーム終了です。感想
やー、55日目に失敗してから絶対インベーダーゲームは作りきると決めたので、できてよかったです。
作ると宣言してまだ作れてないものはあとオセロが残っていますね。
年末までに作れるようになんとか頑張ります。今年もあとちょっとだ!最後まで読んでいただきありがとうございます。明日も投稿しますのでよろしくお願いします。
参考
配列を空にするにはlength=0で十分。(配列とかおれおれAdvent Calendar2018 – 02日目) | Ginpen.com
大変勉強になりました。ありがとうございます!
- 投稿日:2019-12-19T20:44:08+09:00
これから学ぶ言語についてのまとめ
これから学ぶ言語について
これからプログラミングを学ぶにあたり、HTML,CSS,Javascript,Ruby,Ruby on Railsの5つの言語について調べてみました。
(間違いがございましたらご指摘頂けますと幸いです!)HTML
Hyper Text Markup Language(ハイパーテキスト・マークアップ・ランゲージ)
Webページを作るための最も基本的なマークアップ言語のひとつです。
Webページの文書構造を形作るための言語で、普段観ているWebページのほとんどが、HTMLで作られています。
Hyper Text(ハイパーテキスト)とは、ハイパーリンクを埋め込むことができる高機能なテキストです。
HTMLにはハイパーリンク機能で関連する情報同士を結びつけて情報を整理するという特徴があります。
Markup(マークアップ)とは文書の各部分が、どのような役割を持っているのかを示すということです。
コンピュータに理解できるように文書の構造を定義することが、HTMLの最も重要な役割で、
この際目印をつけるための記号として使用されるのがHTMLタグです。HTMLには「HTML」「XHTML」「HTML5」など、いくつか種類があります。
CSS
“Cascading Style Sheets” (カスケーディング・スタイル・シート)スタイルシート
スタイルシート言語の1つで、文書構造にデザインを施し見栄えを整える役割を担っています。
CSSは、HTMLと組み合わせて使用する言語です。
WEBページ=HTML(文書構造)+CSS(見栄え)で構成されています。ウェブページをどのようなスタイルで表示・出力・再生するかについて指定することができます。
JavaScript
JavaScript(ジャバスクリプト)
- JavaScriptとは、現在、世界中のデベロッパーの間で最も人気のあるプログラミング言語の1つ。私たちはみんなJavaScriptのヘビーユーザー。普段から1日に何十回も使用しています。
特徴
JavaScriptはスプリクト言語のためJavaやC言語と違いコンパイルする必要がありません。
JavaScriptとは、動的にコンテンツを更新したり、マルチメディアを管理したり、その他多くのことができるプログラミング言語
JavaScriptはユーザー側のWebブラウザとWebサイト・ウェブサービスの相互間のやりとりを円滑にします。
JavaScriptのコードはブラウザがWebページにアクセスしたときにダウンロードされ、画面上に表示されるとJavaScriptのコードが実行されて次に実行するべき動作がもしあればブラウザに指示します。
Javascriptで出来ること
フォームのチェック
フォームに入力されたテキストデータをサーバに送る前にチェックできますイベント処理
キーボードやマウスによる操作によってWebページの動的な動きを実現できます。テキストの処理・加工
テキストのスタイルを変化させたり、文字を抜き出したり、テキスト回りの加工も可能です。Cookieの処理
Cookieを作ったり、逆にユーザーのCookieデータを読み込んだりできますウインドウの操作
出現するウィンドウの位置やサイズ、スクロールさせるなど、さまざまな動きを指定できます。ほかにも、JavaScriptでできることはたくさんあります。
Ruby
Ruby(ルビー)
- Rubyは、『まつもとゆきひろ』により開発されたオブジェクト指向スクリプト言語。 日本で開発されたプログラミング言語として初めて国際規格に認証されました。
- 「書くのが楽しい」「気持ちよくかける」とよく言われます。
主な特徴
Rubyは他の言語と比べると記述量が少なくて済みます。
JavaやPHPといった他の言語同様、Rubyはオブジェクト指向なプログラミング言語です。
Rubyはスクリプト言語であり、JavaやC言語と違いコンパイルする必要がありません。
Rubyで出来ること
Webアプリ開発
Ruby on RailsというWebアプリ開発用のフレームワークを利用することで、比較的簡単にオリジナルのアプリケーションを制作することができます。スマホアプリ開発
Webアプリケーションだけでなく、RUBYMOTIONを利用することで、スマホアプリ開発をすることが可能です。スクレイピング
Webサイトから情報を取得し加工するという意味です。Ruby on Rails
Ruby on Rails(ルビーオンレイルズ)RoR 又は Rails
Ruby on RailsはオープンソースのWebアプリケーションフレームワークである。
Railsの基本理念は「同じことを繰り返さない」(DRY:Don't Repeat Yourself)と「設定より規約」(CoC:Convention over Configuration)である。
Ruby on Railsとは、Rubyで構築された、Webアプリケーション開発のための「フレームワーク」です。
Ruby on Railsでは、MVCアーキテクチャ(MVCモデル)という設計法に基づいて開発を行います。
MVCとは
Model(データを扱う)の頭文字Mと、View(ページの見た目)の頭文字V、Controller(ModelとViewとの連携を行う)の頭文字Cを取ってMVCと呼ばれています。気になった調べた言葉
5つの言語を調べているうちに気になって調べた言葉です。
コンパイラ言語
高速で動作する代わりに記述が面倒。 ソースコードを事前にコンパイルして、先に機械語に変換しておく。(C、C++、Java、C#など)
スクリプト言語
低速で動作する代わりに短い記述で処理を実現。 ソースコードを逐次機械語に翻訳しながら実行する。 (Python、Perl、PHP、Ruby、JavaScriptなど)
コンパイラ方式とインタープリタ方式
プログラミング言語を変換するタイミングによって,インタープリタ方式とコンパイル方式の2つの型に分類できます。
インタープリタ方式
ソースコードをその場で順を追って解釈する形式をいいます。(Javascript,Ruby)メリットは作成したソースプログラムをただちに実行できるという点。
コンパイラ方式
ソースプログラムをいったん機械語に翻訳し,その機械語になったプログラムを実行する方式です。メリットは実行速度が速い点。
オブジェクト指向プログラミング
「モノ」を組み立てるように表現して、コンピュータに動作をさせること。
Cookie
Web閲覧時に生成されるCookieデータは、サイトで登録したIDやパスワード、ページを見たときの日時や回数を記録するデータです。
フレームワーク
Webアプリケーションを簡単に開発できるようにする骨組みのこと。
- 投稿日:2019-12-19T20:30:03+09:00
D3.jsでExcelの「ウィンドウ枠を固定」みたいな動きをつくる
はじめに: やりたいこと
タイトルわかりますかね? 見てもらった方が早いので図を貼っておきます。こういうやつです。(こういう機能はなんて言えばいいんだろう?)
ひとつの SVG の中に複数の領域を作って、それぞれが drag できるようになっています。これの作り方について解説します。Git リポジトリの情報可視化 で作ったやつのいち機能ですね。
ちなみに、元ネタは The D3 Graph Gallery の Zooming in d3.js です。そちらでは座標軸 (目盛り) 位置を固定して、プロットされている要素のズームやドラッグをやっています。ここではドラッグだけで、かつ x/y 軸部分の領域は縦または横のみに動く、という形です。やってることは大して違いがないので、これがわかれば同じような話に応用が利くはずです。
上のデモは CodePen で作ってあるのでそちらで触ってみてください。以下、このコードの内容を説明します。
See the Pen d3-multi-part-drag by m.hagiwara (@corestate55) on CodePen.
用語定義
以下のように表示領域に名前を付けています。
使われている SVG の機能
D3.js で作る図は SVG で構成されているので、D3.js での実装はいったん置いておいて、まずは使われている SVG 要素の機能や使い方から押さえましょう。
Group (
g) と座標変換 (transform)Group (
g) はコンテナで、複数の要素をまとめられます。Group にたいしてtransformを設定すると、group の子要素になっている SVG 要素全体に対して座標系を設定する (座標変換を行う) ことができます。今回の場合、各表示領域の左上 (上の図で丸がついているところ) がその表示領域の原点になるように設定しています。単純な平行移動 (translate(x, y))です。ここの「座標系の設定」がとても重要で、SVG 領域で見たときと、各領域 (group) で見たときに扱う座標系が異なってきます。これを示すためにデモの方は各領域内での座標系と SVG 視点(青) で見たときの座標系で特徴的なポイントを表示しています。
たとえば SVG 座標系から見ると
[200, 100]のところが以下のようになります。視点が違うので座標が異なりますが、「SVG 図上の位置」としては同じポイントを指しています。
- xAxis 座標系では
[0, 100]- yAxis 座標系では
[200, 0]- plotArea 座標系では
[0, 0]こうして座標系を設定することにより、表示領域が接する部分の座標(値)をそろえることができました。各領域内の要素をどう動かすかについては実装 (イベントハンドラ) の所ですね。
図形のクリッピング (
clipPath)さて、座標系を設定しましたがまだ「表示領域」が設定されていません、このままだと group の「外」にしたい所にオブジェクトを描画しても表示されます。SVG の表示領域内であればオブジェクトが描画できるのでまああたりまえですね。「特定の領域からはみ出したオブジェクトについては描画しない」を実現するために
clipPath(クリッピング) を使用します。表示領域 =「この範囲内にだけオブジェクトを描きたい範囲」を SVG 要素として設定します。ここでは矩形 (
rect) なので、各エリアそれぞれでclipPathとして使うrectを設定します。表示領域設定用のrectはdefsセクションの中に置いて、要素を再利用可能にします。それを group から参照するという形になります。表示領域用rectを複数の要素に対して適用するためにさらに group を作ります。具体的には以下のような階層を各領域ごとに作ります。+ SVG + Group (座標系の設定) + defs (再利用可能な要素の定義) | + clipPath | + Rect (表示領域の設定) + Group (描画オブジェクトをまとめて clipPath を適用する) + 描画対象のオブジェクト上の階層からわかりますが、表示領域用
rectは座標系を設定した group の子要素になるので、各領域の座標系で位置・サイズを指定することになります。clipPathを適用したグループ内にある SVG 要素は、領域からはみ出した部分はクリッピングされて表示されなくなります。こうして「表示領域」が設定されます。d3.js での実装
Group と clipPath
まずは各表示領域:
g/clipPathのデータを作っていきます。コメント入れてありますが、どこから見た何の値なのかに気をつけてください。基準にする座標系が異なる設定値が混ざっているので、そこを間違うと上手くいきません。1const graphData = [ { area: 'xAxis', // 領域名 (id とかに使う) px: px0, // SVG 座標系から見たときの xAxis 座標系の位置 (transform する移動量) py: 0, clipPath: { // 表示領域の設定: xAxis 座標系から見たときの clipPath の位置とサイズ x: 0, y: 0, width: width1, height: py0 } }, // 省略 ]このデータに基づいてそれぞれの SVG 要素を作っていきます。まず group の作成と座標系の設定から。
const groups = svg.selectAll('g') .data(graphData) .enter() .append('g') .attr('id', d => `${d.area}-group`) .attr('transform', d => `translate(${d.px},${d.py})`) // SVG 座標系から見たときの領域座標系(原点)の位置座標系を設定したらその下に
clipPathとclipPathを適用する group (各領域の中で実際に描画するオブジェクトを格納するコンテナ) を作ります。groups .append('defs') .append('SVG:clipPath') // clipPath の定義 .attr('id', d => `${d.area}-clip`) .append('rect') // clipPath として使う四角形 (rect) を設定 .attr('x', d => d.clipPath.x) // 各領域の下につくるので、その領域の座標系で位置とサイズを指定 .attr('y', d => d.clipPath.y) .attr('width', d => d.clipPath.width) .attr('height', d => d.clipPath.height) groups .append('g') .attr('class', 'clipped-group') .attr('clip-path', d => `url(#${d.area}-clip)`) // URLで clipPath 領域を参照要素の描画
あとは、各表示領域ごとに
clipPathが適用されている group の中に描画対象のオブジェクトを入れていきます。もちろんそれらも、各表示領域の座標系で位置を指定します。ドラッグしてオブジェクトを動かす
各領域ごとに
dragイベントハンドラを設定しますが、個々のオブジェクトに設定するのではなく、表示領域全体でドラッグして動かせるようにするので、イベントをキャプチャするための要素 (rect) を追加します。また、イベントキャプチャ用のrectは、これ自体はドラッグの対象にしないのでclipPathを適用する group の外に置きます。+ SVG + Group (座標系の設定) + defs (再利用可能な要素の定義) | + clipPath | + Rect (表示領域の設定) + Group (描画オブジェクトをまとめて clipPath を適用する) | + 描画対象のオブジェクト + Rect (ポインタイベント) <= 追加, イベントキャプチャのため最後(一番上)におくgroups .append('rect') .attr('class', d => `${d.area} drag-handler`) // ポインタイベントキャプチャ用の rect .attr('x', d => d.clipPath.x) // 位置とサイズは clipPath rect とおなじ .attr('y', d => d.clipPath.y) .attr('width', d => d.clipPath.width) .attr('height', d => d.clipPath.height)ここでポイントなのは CSS の方で
pointer-events: allを設定しておくところです。デモでは各表示領域の範囲がわかりやすいように塗りつぶしアリにしているのですが、実際にはfill: none(塗りつぶしナシ) に設定するでしょう。そうなるとデフォルトではイベントをつかんでくれなくなるので、見えなくてもこのオブジェクトでポインタイベントをつかんでね、と指定しておきます。rect.drag-handler { pointer-events: all; }今回は、各領域に描画するオブジェクトに対して特にポインタイベントを設定しないので、このイベントハンドラ用
rectを最前面に持ってきています。が、もし個々のオブジェクトにもイベントハンドラを設定したい場合、こいつが一番上にあるとすべてのポインタイベントを奪ってしまうので動きません。オブジェクトの順序を変える (重なり方を変える) とかの工夫が必要になります。あとはポインタイベント用
rectに対してイベントハンドラを設定してやるだけです。xAxis/yAxis では x/y いずれか片方の移動量だけを渡すようにしてあります。これはdragAllMarkerは三つの領域すべての要素に対して指定された移動量でオブジェクトを動かすからですね。xAxis/yAxis では縦横どちらか片方向の成分を捨てないと狙った動きになりません。たとえば xAxis 領域内で縦にドラッグすると yAxis/plotArea 内の要素が縦に動く、という状況になります。const dragHandlerOf = { 'xAxis': () => dragAllMarker(d3.event.dx, 0), // 横方向のみひろう 'yAxis': () => dragAllMarker(0, d3.event.dy), // 縦方向のみひろう 'plotArea': () => dragAllMarker(d3.event.dx, d3.event.dy) } Object.keys(dragHandlerOf).forEach(area => { selectGroup(area) .select('rect.drag-handler') .call(d3.drag().on('drag', dragHandlerOf[area])) })Drag するのはオブジェクトか座標系か
今回のデモと同じような動作を実現させるときに、2 種類やり方があると思います。
- 座標系を固定してオブジェクトを動かす (今回のデモで実装しているのはこっち)
- オブジェクトを固定して座標系を動かす
前者が視点を固定してモノを動かす方法、後者はモノを固定して視点を動かすもの、という言い方でもよいかな。後者・座標系を動かす場合は、ドラッグイベントに対して各座標系の
transformの値を変えていくことになります。個々のオブジェクトに対して位置計算をするのではなく、座標系の方をいじった方が簡単になるような気もしますが、合わせてclipPathやポインタイベント用rectの位置も変えていかないといけないんですよね。考え方がちょっとややこしいのでやっていません。2おわりに
複数の表示領域で、中身を個別に動かす・連動して動かす、というのをどうやるかについて説明しました。座標系と表示範囲の設定方法がわかれば、あとは通常のイベントハンドリングでできます。いくつか SVG の機能について知らないとできないので、SVG について知っているかどうかが結構効いてきますね。SVG ドキュメントの構造や座標系設定などに注意しましょう。このあたりの理屈がわかれば、表示領域を増やしたり、zoom を加えたり……みたいな応用はいろいろコントロールできるようになると思います。
Git リポジトリの情報可視化 のコードを元にデモ用のサンプルコードをかいたので変数名とかプロパティ名がそれに引きずられていますが、もうちょっと名前を工夫するとか考えた方が良かった気がしますね…。 ↩
あと、ちょっと試してみたところ、SVG 座標系に対してどの位置に設定するかという操作をするためか、ドラッグした際にカクつくのがあったので。でも、そのときの自分のコードが悪かったかもしれない。あまりちゃんと調べられていないのでこのあたりはもうちょっと調べてみてもいいかな……。 ↩
- 投稿日:2019-12-19T19:51:47+09:00
脱jQuery化を試みて失敗した話
各種ブラウザで使えるJavascriptが統一されてきたこともあり、ここらで自分が管理しているサイトからjQueryを除いてみようと思い試行錯誤をしていたが、結局諦めた。
これから同じような事をする人の参考になればと思い軽く情報共有しようと思う。書き換え可能なもの
DOMの生成完了
//jquery $(document).ready(() => {}) //native document.addEventListener("DOMContentLoaded", () => {});セレクタ(単数/複数)
//jquery $('.className').each((index, elem) => { //native document.querySelector('.className') document.querySelectorAll('.selected input').forEach((el, index) => {属性情報の追加/取得
//jquery $(elem).attr('attribute','value'); $(elem).attr('attribute'); //native elem.setAttribute('attribute','value'); elem.getAttribute('attribute');親要素のクラスを追加/除去
classListで要素に付与されているクラスのリストが取得可能//jquery $(elem).parent().addClass("className"); $(elem).parent().removeClass("className"); //native elem.parentElement.classList.add('className'); elem.parentElement.classList.remove('className')イベントリスナ
//jquery $('hoge').click(() => {}); //native document.querySelector('hoge').addEventListener('click', () => {}); //クラスが一つ document.querySelectorAll('hoge').forEach((elem) => { //クラスが複数 elem.addEventListener('click', () => {}); });domが単数の場合と複数の場合を意識してコードを書かないといけないため結構面倒くさい。
困難なもの
・あるポジションまでAnimationで移動したい時
//jquery const position = $("#id").offset().top; $('html').animate({ scrollTop: position }, 'fast'); //native const position = document.querySelector('#id').offsetTop; window.scrollTo({ top: position, behavior: "smooth" });一応アニメーションでのスクロール自体は実現することができるが、behaviorがsmoothかinstantぐらいの指定しかないため、気持ちの良いアニメーションを実現する事ができない。かといって自前でscroll書くのは結構大変で上手く書かないとコードも汚れる。
・script付きのhtmlを埋め込み実行
const html = '<div>...</div><script>...(some_js_execution)</script>' //jquery $('#id').html(html); // Scriptが実行される //native $('#id').innerHTML = html; // Scriptが実行されないinnerHTMLでは内部にscriptタグがある場合実行できない。ソーシャルボタンを設置する場合は大抵scriptタグがコピーされるソースの中に埋め込まれているので、動的な埋め込みが困難となる。jQueryではcreateElementをしてHTMLのDOMを生成してScriptタグを埋め込むという内部操作でScriptの実行を行っていたが、それを自前で書くのは面倒である。
まとめ
DOMの操作自体は比較的簡単に書けるが、複数DOMの操作は煩雑であった。また細かいAnimation制御ができなかったり、htmlとinnerHTMLなどのように微妙な違いが存在するものもあるので手軽に脱jQueryという訳には行かない事が分かった。
結局まだjqueryが役立つシーンは多いと思われる。Webサイト/Webアプリケーションのjavascriptの使用具合により
native javascriptのみ -> zepto + javascript -> jquery + javascript -> vue.js, react -> react + redux
ぐらいな感じで使い分けていくのが現状のベストプラクティスかなと個人的には思う。
- 投稿日:2019-12-19T18:37:06+09:00
DOM操作
DOM操作基礎編
DOM操作とは
DOM操作とはhtml要素(ノード)を取得して、jsで操作してビューに動きをつけたりすることです。
ノードの取得方法
1. querySelector
でcssで指定する時に使うセレクタで指定(同じセレクタが複数ある場合は一番最初に見つかったもののみ)
querySelectorAllで同じセレクタ名のノード全てを取得
2. getElementById
でid名で取得
3. getElementsByClassName
で指定したクラス名の要素を全て取得〇〇.addEventListener('アクション', 関数)
〇〇に対してアクションが起きたら、関数が実行される。
*例1 btn = document.getElementById('btn'); btn.addEventListener('click', 関数); #btnがクリックされたら関数の処理がされる。 *例2 btn = document.getElementById('btn'); btn.addEventListener('click', () => { console.log('hello world'); }); #{}の中の処理を行うClassを変更する
classList.add('クラス名') またはremoveでクラスをつけたり外したりできる。
*add btn = document.getElementById('btn'); btn = document.getElementById('btn'); btn.addEventListener('click', () => { btn.classList.add('red'); }); ※.remove('red')でredクラスを外す。 #redクラスがidがbtnの要素に加わる
- 投稿日:2019-12-19T18:34:45+09:00
Nuxt + TSのテンプレを作った + 自分なりのおすすめの設定
概要
- Nuxtを1年ぐらい触ってきた
- つくったNuxtのテンプレ紹介するよ
- つくったテンプレ以外におすすめの設定を紹介するよ
前提知識
Nuxtとは、Vue.jsのフレームワーク。
Nuxtは、モダンな web アプリケーションを作成する Vue.js に基づいたプログレッシブフレームワークです。Vue.js 公式ライブラリ(vue、vue-router や vuex)および強力な開発ツール(webpack、Babel や PostCSS)に基づいています。 Nuxt の目標は、優れた開発者エクスペリエンスを念頭に置き、Web 開発を強力かつ高性能にすることです。
SSR(サーバサイドレンダリング)とかの設定が楽だったり、Nuxtのプラグインが充実してたり、他にもいろいろ機能がある。
Nuxtいらない説(vue-cliでよくない)とかあるけど、自分はわりと好き。
QiitaでNuxtがやんわり流行っているのは、「Vueのとっつきやすさ+構築の楽さ+ブログの多さ」が大きな理由だと個人的に感じている。
正直Nuxt自体がVueのテンプレみたいなところもあるし、
npx nuxt-create-appでいろいろ設定できるのでNuxtのテンプレっていう言葉しっくり来てない。
どちらかというと自分なりのNuxtの初期設定っていう話のほうが正しい気がする。作ったNuxtのテンプレの紹介
GitHubに公開してあります↓ スターをつけてもらうと泣いて喜びます。
redshoga/nuxt-ts-gottani-template: Nuxt + Typescript + α project template
主な機能としては以下の通り
- TypeScriptの設定
- 型安全なVuexの構成(
vuex-module-decoratorsを使ってます)- 型安全な環境変数の構成
- SCSSのサポート, Normalize.cssの導入
- Path Autocompleteの設定(
@,~以降でもパスの補完がVSCodeで効く)- ESlintの設定(VSCodeでは保存時に自動フォーマット)
- Stylelintの設定(VSCodeでは保存時に自動フォーマット)
TypeScriptの設定
基本
TypeScript サポート - NuxtJSをやっただけです。
VueはTSにすると以下のどの形で書くかが一番重要な点になると思います。
これをまず先に決めて実装するのがいいと思います。(ここを迷って無駄な時間を浪費した)
Vue.extend({})でJSの時と似たような形 (https://jp.vuejs.org/v2/guide/typescript.html#%E5%9F%BA%E6%9C%AC%E7%9A%84%E3%81%AA%E4%BD%BF%E3%81%84%E6%96%B9)vue-class-componentを使ってクラススタイルVueコンポーネントな形(https://jp.vuejs.org/v2/guide/typescript.html#%E3%82%AF%E3%83%A9%E3%82%B9%E3%82%B9%E3%82%BF%E3%82%A4%E3%83%AB-Vue-%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88)vue-property-decoratorを使ってクラススタイル+デコレータな形(https://github.com/kaorun343/vue-property-decorator)今回紹介しているテンプレは
Vue.extend({})の形で実装しています。Vuex
Vuexの管理は公式のまんま(
vuex-module-decoratorsを使う方法)でやるといいです。ここらへんの実装方法はいろいろあって正直悩みます。Nuxt Typescript公式のものと本で読んだものなど合わせれば5パターンぐらいありそう。チームのスキルとの相性とかも重要だとおもいます。
Sass(.scss)の話
生CSSは辛いので、Sassを導入しています。
css-moduleも考えましたがなるべく素のウェブ構築に近づけたかったため今回は入れていないです。(まだ検討している)実は以下(
style-resources-module)をいれようと考えていました。https://github.com/nuxt-community/style-resources-module
style-resources-moduleを入れるとnuxt.config.jsにグローバルな変数ファイル@import不要で入れることができて便利です。
ただし、@importが不要なので変数がどこから来たものなのかわからなくなったり、コンポーネント自体がNuxtの設定に依存するのであんまりよくないです。使い方は以下のよう
~/assest/sccs/variables.scss$color-main: red;nuxt.config.js... modules: [ '@nuxtjs/style-resources', ], styleResources: { scss: ["~/assest/sccs/variables.scss"], } ...ButtonRectangle.vue<template> <div class="container" @click="..."> </div> </template> <style lang="scss"> .container { /* variables.scssの中身の変数を使える */ background-color: $color-main; } <style>アプリケーションのメインカラーの設定や、z-indexを統合して管理できるのが強い。
けど@importしても同じことができるので、@importしたほうがいいかとというのがここ最近の結論。環境変数の設定
以下の記事をまんまに実装しました。便利便利。
Nuxt.js + TypeScript のアプリケーションで環境変数を安全に管理する - Qiita
最近Nest.js(TSのバックエンドフレームワーク)を触っているのですが、この環境変数の管理もわりとガチガチにやってくれるので以下のような実装もいいのかなって思っています。
内部的にJoiを使って環境変数の検証をしているだけです。Configuration | NestJS - A progressive Node.js web framework
フォーマッタとかの話
Stylelintは早めにいれたほうがいいです。(.vueファイルの中身の
<style>内をフォーマットしてくれるようになります。)
なぜかStylelintを怖がっていたのですが、想像以上に設定は楽なのでおすすめです。またTS,JS,HTML関係はESLint+Veturでフォーマットしています。
TSLintもありますが、TSでもESLintの方が強い気がします。もろもろ。情報も多いですし。
ここら辺の話は自身がないので、お気軽にマサカリ飛ばしてください。設定はVeturを入れて以下の設定を
.vscode/settings.json以下に書くだけです。... "eslint.validate": [ "javascript", { "language": "vue", "autoFix": true } ], "eslint.autoFixOnSave": true, "editor.formatOnSave": false, ...VSCodeを使っているなら、path-autocompleteは入れた方がいいです。(無駄な時間が減らせます)
しかし、Nuxtを使っていると@とか~とかでてきてちゃんと補完してくれません。以下の設定を
.vscode/settings.json以下に書くだけでちゃんと補完してくれるようになります。オススメ。個人的に
@@を使うことはほぼほぼ無いので以下の設定にしています。(YAGNI)"path-autocomplete.pathMappings": { "~": "${folder}", "@": "${folder}" },小話: 将来的に入れたい機能
型安全のHTTPクライアントのaspidaが強そうなので入れたい。
と思っていろいろ調べてたらaspidaの作者様のNuxt+TSのテンプレが出てきた。
小話: JSテンプレ
ちなみに上記のテンプレの元ネタになっているJSのテンプレもあります(TS苦手な人,初学者にはいいかも(?))
redshoga/nuxt-gottani-template: Nuxt project template: various features included
最後に
テンプレを作ったというより、テンプレの作り方みたいな話になっちゃった気がする。
結局のところ、プロジェクトとチームに見合ったものをいれていくといいと思います。(ただしTSへの移行は早めにしたほうがいい)
- 投稿日:2019-12-19T18:08:58+09:00
Vue.jsでmethodsのテストを書く方法
公式とかみてもあんま直接的な情報はなかったので。
複数人で開発していると、本来サーバー側でやってほしい処理などがmethodsとかに入り込みがちですよね。
誰もメンテできなくなる前にしっかりとテストコードを書いておき、いつでもリファクタリングできる状態をキープしておきたいものです。環境
- vue-test-utils
- jest
テスト環境の構築方法などはvue-test-utilsやjestの公式を参考にしてください。
サンプル
こんな感じのかなり尖ったコンポーネントがあるとします。
<template> <button @click="sampleMethod()" /> </template> <script> export default { methods: { sampleMethod() { return 'hello' } } } </script>このコンポーネントの
methodsのテストは下記のように書くことができます。import { createLocalVue, shallowMount } from '@vue/test-utils' import Vuex from 'vuex' import SampleComponent from '~/components/SampleComponent' const localVue = createLocalVue() localVue.use(Vuex) describe('SampleComponent', () => { let wrapper beforeEach(() => { wrapper = shallowMount(UseHighway, { localVue }) }) it('sampleMethodsのテスト', () => { expect(wrapper.vm.sampleMethod).toBe('hello') // 通る }) it('sampleMethodsのテスト(落ちるパターン)', () => { expect(wrapper.vm.sampleMethod).toBe('hey') // NG }) })解説
vue-test-utilsのドキュメントには、
wrapper.vm を使って vm のプロパティとインスタンスメソッドにアクセスできます。これは、Vue コンポーネントのラッパもしくは Vue コンポーネントをバインディングしている HTMLElement のラッパにのみ存在します。
とあります。なので、上記のコードにおいては
wrapper.vmでmethodsを呼び出せるわけですね。
実際には上記のコードのように副作用の全くないコードは珍しく、実際にはsetPropsやsetDataなどで値をセットしてからテストすることになると思います。まとめ
エンジニアとして、やはりフロントエンドにおいてもできるだけ副作用が少なく、テストしやすいコードを書くことを意識していきたいですね。
- 投稿日:2019-12-19T18:02:59+09:00
Advent Calendar だから Illustrator + JavaScript でお菓子を作ってみる
はじめに
この記事は FORK Advent Calendar 2019 19日目の記事です。
Advent Calendar といえばクリスマスに向けて1日ずつ数える習慣で、毎日1つお菓子を食べるなんて楽しみもありますよね!(以下 wikipedia より)
アドベントカレンダー (Advent calendar) は、クリスマスまでの期間に日数を数えるために使用されるカレンダーである。待降節の期間(イエス・キリストの降誕を待ち望む期間)に窓を毎日ひとつずつ開けていくカレンダーである。すべての窓を開け終わるとクリスマスを迎えたことになる。
ということで、大好きな Adobe Illustrator でお菓子を作ります!
ただしここはQiita、プログラムの記事を書かかねばなりません。Illustratorは譲れないので今回はJavaScriptで作ることにします。コードの質も甘めですがそこはご容赦ください。やりたいこと
- Adobe IllustratorにJavaScriptでお菓子の絵を描く
IllustratorでJavaScriptを実行する方法は、
Illustratorのファイル > スクリプト > その他のスクリプトで読み込むか、
IllustratorのアートボードにJavaScriptのファイルをドラッグ&ドロップするかになります。その為、
- Adobe IllustratorのアートボードにJavaScriptのファイルをドラッグ&ドロップするとお菓子が出てくる
といった感じになります。
用意するもの
材料
- Adobe Illustrator
- JavaScript
本記事ではAdobe Illustrator CCを使用しております。
レシピ
英語というだけでアレルギーが出る方も多いかもしれませんが、Illustratorが使えれば割と読めます。
実装
どんなお菓子を作ろうか考えた結果、クッキーを作ることにしました!
安っぽい定番ですね。生地作り
まずは基礎となる生地を作ります。
Illustratorのアートボードに長方形ツールで正方形を描いてみましょう。var doc = app.activeDocument; var dough = doc.pathItems.rectangle(-5, 5, 90, 90);ドキュメントの指定
1行目の
app.activeDocumentについて
appは対象のアプリケーションすなわちIllustrator、
activeDocumentは使用中のドキュメントになります。正方形の描画
2行目の
doc.pathItems.rectangle()について
doc.pathItemsは1行目のドキュメントでパスオブジェクトを取り扱おうとしています。
続いてrectangle()でパスオブジェクトの長方形ツールを使用すると書いています。
rectangle()はrectangle(上のY座標, 左のX座標, 横幅, 高さ)のように数字を指定し、
ここではY座標-5、X座標5の位置に横幅90×高さ90の長方形を描画すると書いています。これで正方形ができました!
※ 画像では黒枠の正方形ですが、実際の色は選択中の色に準じます。色づけ
続いて正方形に色をつけて美味しそうにしていきましょう。
RGBによる色指定
色を指定する場合、RGBで指定する方法とCMYKで指定する方法があります。
今回はRGBで色を指定し、RGBColor()を使います。var color = new RGBColor(); color.red = 255; color.green = 0; color.blue = 0;このように
redgreenblueの数値を指定するとcolorはrgb(255, 0, 0)となります。
なお、CMYKで色を指定する場合はCMYKColorでcyanmagentayellowblackを指定します。色の適応
好きな色を指定できたら、その色をつけてみましょう!
Illsutratorには塗りと線があります。
塗りはパスの内側の色を指しfillColor、線はパスの色を指しstrokeColorで指定します。var doc = app.activeDocument; var dough = doc.pathItems.rectangle(-5, 5, 90, 90); dough.fillColor = getRGB(255, 225, 150); dough.stroked = false; function getRGB(r, g, b) { var color = new RGBColor(); color.red = r; color.green = g; color.blue = b; return color; }生地の塗りに色をつけたいので
dough.fillColorに色を指定します!
ただ色を指定する度に先程のように4行書くのは手間になるので、ここではgetRGB()という関数にしています。getRGB(255, 225, 150)と書くとrgb(255, 225, 150)の色が指定できます。逆に線には色をつけないことにします。
線を使わない場合はdough.stroked = false;と書きます。
塗りの場合はdough.filled = false;です。ちょっとクッキーっぽくなりましたね!
模様づけ
このクッキーのメイン、チェック柄をつけましょう。
小さいチョコレート色の正方形を追加します。パスオブジェクト追加
docに新しくpathItems.rectangle()を書くとパスオブジェクトの長方形を追加できます。var doc = app.activeDocument; var dough = doc.pathItems.rectangle(-5, 5, 90, 90); dough.fillColor = getRGB(255, 225, 150); dough.stroked = false; var choco1 = doc.pathItems.rectangle(-5, 5, 45, 45); choco1.fillColor = getRGB(150, 100, 50); choco1.stroked = false; function getRGB(r, g, b) { var color = new RGBColor(); color.red = r; color.green = g; color.blue = b; return color; }左上に正方形が追加されました。
コピー
同じく右下にも正方形を追加するわけですが、今度はちょっと違う方法を試してみましょう!
左上の正方形をコピーして右下に移動させます。var doc = app.activeDocument; var dough = doc.pathItems.rectangle(-5, 5, 90, 90); dough.fillColor = getRGB(255, 225, 150); dough.stroked = false; var choco1 = doc.pathItems.rectangle(-5, 5, 45, 45); choco1.fillColor = getRGB(150, 100, 50); choco1.stroked = false; var choco2 = choco1.duplicate(); choco2.position = Array(50, -50); function getRGB(r, g, b) { var color = new RGBColor(); color.red = r; color.green = g; color.blue = b; return color; }
choco1.duplicate()と書くと左上の正方形choco1をコピーできます!
var choco2 = choco1.duplicate();でコピーした正方形にchoco2と名前をつけ、
choco2.position = Array(50, -50);でchoco2の位置をX座標50、Y座標-50と指定します。これはほぼほぼクッキーといっても良いのではないでしょうか。
角丸
最後の仕上げにクッキーの角を丸くしましょう!可愛くなります。
ここであのクリッピングマスクの登場です。グループ化
JavaScriptでクリッピングマスクを使用する場合は予めグループ化をする必要があります。
まず、先程までのクッキーをグループ化してみましょう。var doc = app.activeDocument; var cookie = doc.groupItems.add(); var dough = cookie.pathItems.rectangle(-5, 5, 90, 90); dough.fillColor = getRGB(255, 225, 150); dough.stroked = false; var choco1 = cookie.pathItems.rectangle(-5, 5, 45, 45); choco1.fillColor = getRGB(150, 100, 50); choco1.stroked = false; var choco2 = choco1.duplicate(); choco2.position = Array(50, -50); function getRGB(r, g, b) { var color = new RGBColor(); color.red = r; color.green = g; color.blue = b; return color; }
doc.groupItems.add()で新しいグループを作成することが可能です。
ここではcookieというグループを作成し、今までの正方形をcookie.pathItems.rectangle()とすることでcookieというグループの中に正方形を作るように変えています。
Illustrator上で選択するとグループ化されていることを確認できます。角丸長方形の枠を描画
マスクする為の角丸長方形を描画します。
こちらは今まで何度か出てきたpathItemsのroundedRectangle()を使います。var doc = app.activeDocument; var cookie = doc.groupItems.add(); var dough = cookie.pathItems.rectangle(-5, 5, 90, 90); dough.fillColor = getRGB(255, 225, 150); dough.stroked = false; var choco1 = cookie.pathItems.rectangle(-5, 5, 45, 45); choco1.fillColor = getRGB(150, 100, 50); choco1.stroked = false; var choco2 = choco1.duplicate(); choco2.position = Array(50, -50); var outline = cookie.pathItems.roundedRectangle(-5, 5, 90, 90, 10, 10); function getRGB(r, g, b) { var color = new RGBColor(); color.red = r; color.green = g; color.blue = b; return color; }
cookie.pathItems.roundedRectangle(-5, 5, 90, 90, 10, 10)でグループの中に角丸長方形が追加できます。
rectangle()の引数に続いて角丸の水平(横)方向の半径角丸の垂直(縦)方向の半径を指定します。
roundedRectangle(上のY座標, 左のX座標, 横幅, 高さ, 角丸の水平方向の半径, 角丸の垂直方向の半径)です。クリッピングマスク
最後の最後、クリッピングマスクで角丸長方形にします!
クリッピングマスクは簡単、グループのclippedをtrueにします。var doc = app.activeDocument; var cookie = doc.groupItems.add(); var dough = cookie.pathItems.rectangle(-5, 5, 90, 90); dough.fillColor = getRGB(255, 225, 150); dough.stroked = false; var choco1 = cookie.pathItems.rectangle(-5, 5, 45, 45); choco1.fillColor = getRGB(150, 100, 50); choco1.stroked = false; var choco2 = choco1.duplicate(); choco2.position = Array(50, -50); var outline = cookie.pathItems.roundedRectangle(-5, 5, 90, 90, 10, 10); cookie.clipped = true; function getRGB(r, g, b) { var color = new RGBColor(); color.red = r; color.green = g; color.blue = b; return color; }これでチェック柄の記事が角丸になりました!
完成
とても美味しそうなクッキーができましたね!
JavaScriptで絵を描くというと難しそうに聞こえるかもしれませんが、一つ一つの処理を追っていくと意外と簡単に描けると感じてもらえたのではないかと思います。
おまけ
1つだけだと寂しいので追加で2種類用意してみました!
コードはこちら
cookie.js/** * const * */ const DOC = app.activeDocument; const COLOR_DOUGH = getRGB(255, 225, 150); if (app.documents.length > 0) { rand = Math.floor(Math.random() * 4) + 1; if (rand == 1) { bakeCookie1(); } else if (rand == 2) { bakeCookie2(); } else if (rand == 3) { bakeCookie3(); } } /** * bakeCookie1 * */ function bakeCookie1() { var cookie = DOC.groupItems.add(); var dough = cookie.pathItems.rectangle(-5, 5, 90, 90); dough.fillColor = COLOR_DOUGH; dough.stroked = false; var choco1 = cookie.pathItems.rectangle(-5, 5, 45, 45); choco1.fillColor = getRGB(150, 100, 50); choco1.stroked = false; var choco2 = choco1.duplicate(); choco2.position = Array(50, -50); var outline = cookie.pathItems.roundedRectangle(-5, 5, 90, 90, 10, 10); cookie.clipped = true; } /** * bakeCookie2 * */ function bakeCookie2() { var cookie = DOC.groupItems.add(); var point = 24; var dough = cookie.pathItems.star(50, -50, 50, 45, point); dough.fillColor = COLOR_DOUGH; dough.stroked = false; for (i = 0; i < (point * 2); i++) { var anchorLeft = (i == 0) ? dough.pathPoints[(point * 2) - 1].anchor : dough.pathPoints[i - 1].anchor; var anchorRight = (i == (point * 2) - 1) ? dough.pathPoints[0].anchor : dough.pathPoints[i + 1].anchor; dough.pathPoints[i].leftDirection = anchorLeft; dough.pathPoints[i].rightDirection = anchorRight; } for (i = (point * 2 - 1); i >= 0; i--) { if (i % 2 == 0) dough.pathPoints[i].remove(); } var dot = cookie.groupItems.add(); var dotColor = getRGB(200, 150, 100); var dotDiameter = 3; var dotOut = dot.pathItems.ellipse(-25, 25, 50, 50); dotOut.filled = false; dotOut.strokeColor = dotColor; dotOut.strokeDashes = [0, (50 * 3.14 * 2 / point)]; dotOut.strokeCap = StrokeCap.ROUNDENDCAP; dotOut.strokeWidth = dotDiameter; var dotIn = dot.pathItems.ellipse(-37.5, 37.5, 25, 25); dotIn.filled = false; dotIn.strokeColor = dotColor; dotIn.strokeDashes = [0, (25 * 3.14 * 4 / point)]; dotIn.strokeCap = StrokeCap.ROUNDENDCAP; dotIn.strokeWidth = dotDiameter; dot.opacity = 25.0; } /** * bakeCookie3 * */ function bakeCookie3() { var cookie = DOC.groupItems.add(); var dough = cookie.pathItems.ellipse(-0, 0, 100, 100); dough.fillColor = COLOR_DOUGH; dough.stroked = false; var almond = cookie.groupItems.add(); var body = almond.pathItems.ellipse(-35, 35, 30, 40); var ld = body.pathPoints[1].leftDirection; var rd = body.pathPoints[1].rightDirection; body.pathPoints[1].anchor = Array(50, -25); body.pathPoints[1].leftDirection = Array(ld[0] + 5, body.pathPoints[1].anchor[1]); body.pathPoints[1].rightDirection = Array(rd[0] - 5, body.pathPoints[1].anchor[1]); body.fillColor = getRGB(200, 150, 100); body.stroked = false; var lineColor = getRGB(175, 125, 100); var line1 = body.duplicate(); line1.filled = false; line1.strokeColor = lineColor; line1.width = 20; line1.position = Array(body.position[0] + 5, body.position[1]); var line2 = body.duplicate(); line2.filled = false; line2.strokeColor = lineColor; line2.width = 10; line2.position = Array(body.position[0] + 10, body.position[1]); } /** * getRGB * */ function getRGB(r, g, b) { var color = new RGBColor(); color.red = r; color.green = g; color.blue = b; return color; }このコードをIllusratorに持ってくるとランダムで1種類クッキーが出てきます。プログラムっぽい!
他2種類もどんなことをしているか簡単にご紹介します。ギザギザクッキー
ギザギザのクッキーです!
コードでいうとbakeCookie2()です。
最初のクッキーと違う点をピックアップすると
- スターツール
- 破線
このあたりが特徴的かと思います。
スターツール
ギザギザな図形を作るのにスターツールを使います。
var point = 24; var dough = cookie.pathItems.star(50, -50, 50, 45, point);スターツールは
pathItems.star()を使います。
中心のX座標中心のY座標外側の点の半径内側の点の半径点の数を引数に指定します。破線
真ん中の点々を描いていきます。
今回は点線の正円を大小2つ描いています。
Illustratorで点線を描く時は破線にして線端を丸型線端、線分を0にすると簡単にできちゃいます。var dot = cookie.groupItems.add(); var dotColor = getRGB(200, 150, 100); var dotDiameter = 3; var dotOut = dot.pathItems.ellipse(-25, 25, 50, 50); dotOut.filled = false; dotOut.strokeColor = dotColor; dotOut.strokeDashes = [0, (50 * 3.14 * 2 / point)]; dotOut.strokeCap = StrokeCap.ROUNDENDCAP; dotOut.strokeWidth = dotDiameter; var dotIn = dot.pathItems.ellipse(-37.5, 37.5, 25, 25); dotIn.filled = false; dotIn.strokeColor = dotColor; dotIn.strokeDashes = [0, (25 * 3.14 * 4 / point)]; dotIn.strokeCap = StrokeCap.ROUNDENDCAP; dotIn.strokeWidth = dotDiameter;破線は
strokeDashesを使います。 破線の線分と間隔は[線分, 間隔]と指定します。
線端はstrokeCapを使い、丸型線端はStrokeCap.ROUNDENDCAPになります。
線幅はstrokeWidthで指定できます。アーモンドクッキー
アーモンドをトッピングしてみました!
コードでいうとbakeCookie3()で、アーモンドは
- アンカーの移動
- アンカーのハンドル操作
このあたりが特徴的かと思います。
アンカーの移動
アーモンドの形は楕円を描いた後に上のアンカーを移動して尖らせています。
var body = almond.pathItems.ellipse(-35, 35, 30, 40); ~ 略 ~ body.pathPoints[1].anchor = Array(50, -25);パスオブジェクトの
pathPointsでアンカーを取り扱うことができます。
今回は楕円の上のアンカーbody.pathPoints[1]の位置をanchorで指定します。
body.pathPoints[1].anchor = Array(50, -25);でX座標50、Y座標-25とすることで元の位置しています。アンカーのハンドル操作
アーモンドの上を尖らせる為にハンドルも操作して調整します。
var body = almond.pathItems.ellipse(-35, 35, 30, 40); var ld = body.pathPoints[1].leftDirection; var rd = body.pathPoints[1].rightDirection; body.pathPoints[1].anchor = Array(50, -25); body.pathPoints[1].leftDirection = Array(ld[0] + 5, body.pathPoints[1].anchor[1]); body.pathPoints[1].rightDirection = Array(rd[0] - 5, body.pathPoints[1].anchor[1]); body.pathPoints[1].anchor = Array(50, -25);ハンドルは
leftDirectionで左側のハンドル、rightDirectionで右側のハンドルの座標を指定できます。
anchorでアンカーの座標を移動してもハンドルの座標は変わらない為、ハンドルの長さを変えない場合も移動させる必要があります。今回は移動前に
ldrdに元々のハンドルの座標を代入しておき、移動後に改めてその値を使っています。
ld[0] + 5は移動前のハンドルのX座標を少しアンカー寄りに近づけた数値です。
body.pathPoints[1].anchor[1]は移動後のアンカーのY座標です。このようにアンカー、ハンドルも自由に操作できるので好きなカーブを描くことも可能です!
まとめ
普通にIllustratorで描いた方が圧倒的に速いです。が、JavaScriptで描くのも楽しかったです!
Illsutratorで作るものがあるけど今日はコード書きたいなって気分の時なんかにオススメします。参考
Illustrator Scripting
Adobe Illustrator CC 2017 Reference: JavaScript
FORK Advent Calendar 2019
18日目 今年の正月はNuxtとFirebaseで作ったカードゲームで甥っ子に尊敬されたい @yoh_zzzz さん
20日目 @re_sai さんよろしくお願いいたします!
- 投稿日:2019-12-19T17:50:53+09:00
部内プロコンでチャットアプリを作った話
- 投稿日:2019-12-19T17:24:48+09:00
Vue.jsレベルを上げよう!○×ゲームを作ってTypeScript&Vue3のCompositionAPIと仲良くなる
この記事はVue#2アドベントカレンダーの19日目です。昨日は@mgrさんのVue.jsのcomposition-apiにおける"ref"と"reactive"って何が違うの?でした。今日も引き続きComposition APIに絡む話題(今日は入門向け)です!
前振り
こんにちは、SE→UX→コンサルときて、年明けからは晴れて?フロントエンドの人になる予定の「ゆき」です。
Vue界隈の今年はTypeScriptや来たるVue3に向けたCompositionAPIなど、根幹に関わる(=ちょっと難しい)話題が多い一年だったかと思います。みなさん、TypeScriptは使いこなしてますか?CompositionAPIはもうマスターしたでしょうか?
私は全然です
・・・っていうかこれ超難しくね?
対象読者とこの記事の内容
この記事は私と同じように、「今までの"普通"のVueはチョットワカル
」でも「最近の新しいのはよくわからない
」「むしろ怖い
」人のために、簡単な○×ゲームを作ってTypeScript + CompositionAPIを体験してみる内容です。1
○×ゲームくらいならなんとなくできそうな気がするじゃないですか?流れとしては、
- まずは普通のVue + JavaScriptで○×ゲームを実装
- できるだけ原型を維持しながら、TypeScript + CompositionAPIに書き換え
という順で、TypeScript + CompositionAPIを使うと書き方がどう変わるのか + どこらへんが便利になるのかを、比較しながら実装して親しみを持つことがゴールです。ゆるくお付き合いくださいませ。単に「Vueで○×ゲーム作りたい!」って人ももちろんOK
まずは普通に○×ゲームを作る
仕様と方式
動くもの : https://yuneco.github.io/vue-tic-tok-toe/
ソース一式はこちら : https://github.com/yuneco/vue-tic-tok-toe作りをできる限りシンプルにするため、今回作る○×ゲームは以下の単純な仕様とします:
- 3x3のマスに○と×を交互に置いて、一列揃ったら勝ち。揃わずに全マス埋まってしまったら引き分け(普通のルール)
- ローカルかつ人対人のみ(ネットワーク戦や対PC戦はなし)
- ゲームオーバー後のリセット機能はなし(画面リロードしてね)
また、実装にあたってVueRouterやVuexは使わず、単純なコンポーネントの組み合わせのみで実現します。(Vuexを使うと難易度が上がるので。この記事を最後までやってみたらチャレンジしてもいいかも)
プロジェクトを作る
では実際に「普通のVue」でプロジェクトを作るところから始めましょう。このあたりは知ってる方の方が多いと思うので、適時飛ばしながら見てください。プロジェクト名は
ttt-js(ttt = Tic Tok Toe = ○×ゲームの英語名)としました。
vue create ttt-js設定はデフォルトでも大丈夫だと思いますが、一応カスタム設定で、
Check the features... : ◉ Babel ◉ CSS Pre-processor ◉ Linter/Formatterを選択しました。CSSやLintの話は今日はしないので、お好みで構いません。
プロジェクトができたら、
npm run serveでテンプレの画面が出るところまで確認しましょう。確認できたら、サンプルで入っているHelloWorld等は消してしまってもOKですざっくり設計
- Appコンポーネント ... GameBoardを配置し、勝ち負けやターン等のメッセージを表示する
- GameBoardコンポーネント ... ゲームの状態を管理保持し、GameCellを使って画面に表示する
- GameCellコンポーネント ... ○×の一マスを表示する。データやロジックは持たない
GameCellコンポーネント
まずは一番シンプルなGameCellコンポーネントから作ります。
GameCell.vue<template> <div class="game-cell" @click="$emit('click')" :style="{ backgroundImage: owner ? `url('/imgs/stone_${owner}.svg')` : 'none' }" > </div> </template> <script> export default { props: { owner: { type: String } } } </script> <style lang="scss" scoped> // 略 </style>シンプルなのでほとんど説明不要かと思います。
- マスひとつをdivとして表現
- マスの所有者を示す
ownerプロパティに従い、マスに背景画像を表示 ('p1'...○の画像 / 'p2'...×の画像)- クリックされたら
clickイベントを発行○と×のsvg画像はpublic/imgsディレクトリに入れます。
マスのコンポーネント自身には状態やロジックは持たず、あくまでpropの指定に従い背景画像を表示しているだけのものです。
GameBoardコンポーネント
次にゲームの中心になるGameBoardコンポーネントです。
これもまずはコードを全部乗せてから、要所要所を説明していきます。GameBoard.vue<template> <div class="game-board"> <game-cell class="game-cell" v-for="(cell, index) in cells" :key="index" :owner="cell.owner" @click="placeStone(index)" /> </div> </template> <script> import GameCell from './GameCell' /** Player1を示す定数(画像ファイルの名前に使用するためSymbolにはしていません) */ const P1 = 'p1' /** Player2を示す定数(画像ファイルの名前に使用するためSymbolにはしていません) */ const P2 = 'p2' /** まだ石が置かれていないセル */ const BLANK = undefined /** プレイヤー毎の置かれる石(文字) */ const STONES = { [P1]: '○', [P2]: '×' } /** 一直線に並ぶセルの組み合わせ。8個しかないので列挙 */ const LINES = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6] ] export default { components: { GameCell }, data () { return { cells: Array(9).fill(0).map(_ => ({ owner: BLANK })), // 全てのセルをowner = BLANKで初期化 turn: P1 } }, computed: { /** 勝者(P1 or P2)。未決と引き分けの場合undefined */ winner () { let winnerPName LINES.forEach(line => { const ownersInLine = line.map(index => this.cells[index].owner) const isAllSame = ownersInLine[0] === ownersInLine[1] && ownersInLine[0] === ownersInLine[2] if (isAllSame && ownersInLine[0] !== BLANK) { winnerPName = ownersInLine[0] } }) return winnerPName }, /** ゲームの勝敗が決まるか引き分けになっていればtrue */ isGameEnded () { return this.winner || this.cells.filter(cell => cell.owner === BLANK).length === 0 } }, methods: { /** 指定のセルに現在のプレイヤーの石を置いて、ターンを交換。 * すでにゲームが終了している場合や石が置けないセルの場合、何もしない。 */ placeStone (index) { if (this.isGameEnded) { return } if (this.cells[index].owner !== BLANK) { return } this.cells[index].owner = this.turn if (this.isGameEnded) { // どちらかの勝ち or 引き分け this.$emit('gameEnd', STONES[this.winner]) } else { // ターン交代 this.turn = this.turn === P1 ? P2 : P1 this.$emit('nextTurn', STONES[this.turn]) } } }, mounted () { // 最初のターンを通知するためにイベントを発行 this.$emit('nextTurn', STONES[this.turn]) } } </script> <style lang="scss" scoped> // 略 </style>テンプレート部
まずはテンプレート部分からです。
GameBoard.vue(テンプレ部)<template> <div class="game-board"> <game-cell class="game-cell" v-for="(cell, index) in cells" :key="index" :owner="cell.owner" @click="placeStone(index)" /> </div> </template>先ほど作ったGameCellコンポーネントをv-forで並べています。
このあとのscript部分で出てきますがcellsは9マス分のオーナー情報(そのマスに石を置いたプレイヤーはどちらか?)を格納した配列です。見た目については今回は説明しませんが、この9マスをCSS Flexboxで3x3に配置してボードを表現しています。マスをクリックしたら
placeStoneメソッドが呼ばれることもわかるかと思います。このメソッドについてもscript部分で説明します。script冒頭部(定数宣言等)
次にscript部分を見ていきましょう
GameBoard(script部冒頭)/** Player1を示す定数(画像ファイルの名前に使用するためSymbolにはしていません) */ const P1 = 'p1' /** Player2を示す定数(画像ファイルの名前に使用するためSymbolにはしていません) */ const P2 = 'p2' /** まだ石が置かれていないセル */ const BLANK = undefined /** プレイヤー毎の置かれる石(文字) */ const STONES = { [P1]: '○', [P2]: '×' } /** 一直線に並ぶセルの組み合わせ。8個しかないので列挙 */ const LINES = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6] ]GemeCellコンポーネントのimportは問題ないと思うので飛ばして、その次、定数をいくつか定義しています。よくあるテトリスやボードゲームのサンプルコードだと、「白石を1、黒石を2とする」みたいにコメントをつけてコードにはこの数字をベタ書きしているものも多いですが、できるだけ文字列やマジックナンバーはそのまま使わず、名前をつけてあげると良いと思います。
data部
次にコンポーネントのdata部分です
GameBoard(data部)data () { return { cells: Array(9).fill(0).map(_ => ({ owner: BLANK })), // 全てのセルをowner = BLANKで初期化 turn: P1 } }cellsに9マス分の初期値を設定しています。ちょっと綺麗じゃ無い書き方ですが、これは
GameBoard(script部)cells: [ { owner: BLANK }, { owner: BLANK }, // ... 中略 ... { owner: BLANK } ]と書くのと同じです。
turnには先手としてP1を設定しています。computed部
つぎ、computedいきます
GameBoard(computed部)computed: { /** 勝者(P1 or P2)。未決と引き分けの場合undefined */ winner () { let winnerPName LINES.forEach(line => { const ownersInLine = line.map(index => this.cells[index].owner) const isAllSame = ownersInLine[0] === ownersInLine[1] && ownersInLine[0] === ownersInLine[2] if (isAllSame && ownersInLine[0] !== BLANK) { winnerPName = ownersInLine[0] } }) return winnerPName }winnerは勝者を示す算出プロパティです。
勝敗が決まっている場合にはP1またはP2を、まだ決まっていない・引き分けになった場合はundefinedを返します。
○×ゲームでは現在の盤面さえ与えられれば勝ち負けを容易に求められる2ので、勝ち負け判定はメソッドではなくcomputedにしておくのが良いかと思います。ロジックについては解説しませんが、冒頭の定数で宣言した8個の直線パターンを総当たりでチェックしているだけです。
GameBoard(computed部)isGameEnded () { return this.winner || this.cells.filter(cell => cell.owner === BLANK).length === 0 }isGameEndedはゲームが終了しているかどうかを示すcomputedです。
○×ゲームが終了するのは勝敗が決まった時と全てのマスに石が置かれた時なので、それをそのまま書いています。methods部
次にmethodsです。マスの
@clickで指定したplaceStoneの実装ですGameBoard(methods部)placeStone (index) { if (this.isGameEnded) { return } if (this.cells[index].owner !== BLANK) { return } this.cells[index].owner = this.turn if (this.isGameEnded) { // どちらかの勝ち or 引き分け this.$emit('gameEnd', STONES[this.winner]) } else { // ターン交代 this.turn = this.turn === P1 ? P2 : P1 this.$emit('nextTurn', STONES[this.turn]) } }
indexで指定されたマスに石を置けるかどうか判定し、置ける場合には置いた上でゲームが終了するかチェックしています。終了しなかったらターンを交代します。
ゲーム終了とターン交代それぞれでイベントを発行して親コンポーネントにゲームの状態を通知しています。mounted(ライフサイクルフック)部
最後にライフサイクルフックのmountedです。
GameBoard(ライフサイクルフック部)mounted () { // 最初のターンを通知するためにイベントを発行 this.$emit('nextTurn', STONES[this.turn]) }上で石を置いた時にターン交代をイベント通知していましたが、それだけだとゲーム開始時にどちらの手番かわからないので、最初に一発
nextTurnイベントを発行しておきます。これでゲームの主要な要素はできました。
Appコンポーネント
最後にゲームをAppコンポーネントに配置します。
App.vue<template> <div id="app"> <div class="msg">{{ gameMsg }}</div> <game-board @nextTurn="nextTurn" @gameEnd="gameEnd" /> </div> </template> <script> import GameBoard from '@/components/GameBoard' export default { name: 'app', components: { GameBoard }, data () { return { gameMsg: '' } }, methods: { nextTurn (player) { this.gameMsg = `${player}の番です` }, gameEnd (winner) { this.gameMsg = winner ? `${winner}の勝ちです` : '引き分けです' } } } </script> <style lang="scss"> // 略 </style>これも説明は不要かと思います。
配置したGameBoardコンポーネントからのイベントを拾って画面のメッセージを変えているだけですね。TypeScript + Composition APIで○×ゲーム
さて、ここからが本題です。
この○×ゲームをTypeScript + Composition APIを使って書き直していきます。
今回はあくまで仲良くなるための導入なので「できるだけ元の形を維持したまま」書き直していきたいと思います。プロジェクトの作成
新しくプロジェクトを作りましょう。名前は
ttt-tsにしますvue create ttt-tsマニュアル設定で今度はTypeScriptを有効にします
Check the features... ◉Babel ◉TypeScript <<< ✨NEW! ◉CSS Pre-processor ◉Linter/FormatterTypeScriptをONにすると
? Use class-style component syntax? (Y/n)と聞いてきますが、ここはnで答えてください。class-style component syntaxはComposition API以前に有力だった書き方です。これがダメなわけではないのですが、今回はComposition APIを使いたいのでOFFにします。プロジェクトができたら、続けてComposition APIを利用できるようにします。
Composition APIは本来Vue3で提供予定のものですが、ほとんどの機能をVue2用に提供する@vue/composition-apiというパッケージがあるので、これをインストールして使います。npm i -S @vue/composition-apiパッケージを入れたら、/src/main.tsを開いてComposition APIを読み込みます。
src/main.tsimport Vue from 'vue' import App from './App.vue' import VueCompositionApi from '@vue/composition-api' // ⭐︎追加 Vue.config.productionTip = false Vue.use(VueCompositionApi) // ⭐︎追加 new Vue({ render: h => h(App) }).$mount('#app')準備ができたので、次から標準版と同様にGameCellコンポーネント・Appコンポーネント・GameBoardコンポーネントをTypeScript + Composition APIで書いていきます。(説明しやすいよう、複雑なGameBoardコンポーネントは最後に説明します)
GameCellコンポーネント(TS + Composition API)
まずは一番簡単なGameCellコンポーネントからです。
<template>と<style>は変わらないので、以下<script>部分のみ記載します。GameCell.vue<script lang="ts"> import { createComponent } from '@vue/composition-api' export default createComponent({ props: { owner: { type: String } } }) </script>JavaScript版との差分は3箇所だけです。
- scriptタグに
lang="ts"を追加してTypeScriptとしてコンパイルされるようにするimportでcomposition-apiを読み込むexport defaultするのが素のオブジェクトではなくcreateComponent関数で作ったコンポーネントこの3点は「お約束」だと思っても良いと思います。基本Composition APIで書く全てのコンポーネントで共通です。GameCellコンポーネントはもともとpropsの宣言くらいしかしていないので、これでおしまいです。
Appコンポーネント(TS + Composition API)
次にAppコンポーネントを書き換えましょう。
App.vue<script lang="ts"> import GameBoard from '@/components/GameBoard.vue' import { createComponent, ref } from '@vue/composition-api' export default createComponent({ name: 'app', components: { GameBoard }, setup () { const gameMsg = ref('') const nextTurn = (player: string) => { gameMsg.value = `${player}の番です` } const gameEnd = (winner: string) => { gameMsg.value = winner ? `${winner}の勝ちです` : '引き分けです' } return { gameMsg, nextTurn, gameEnd } } }) </script>先ほど重なる部分は省略します。
標準版にあったdataとmethodsがなくなって、setupという新しい関数の中に移動しています。Composition APIでは従来data・computed・methods・watch等、仕組みの種類によって書く場所が別れていたものを全てsetupという単一の関数に統合しています。
dataにあったgameMsgはref()に、methodsにあったnextTurnとgameEndは普通の関数として、setupの中に宣言されています。そして
setupの最後でこれらをまとめてreturnする、というのが基本的な流れです。returnしたものがテンプレート部分からアクセスできるようになります。
補足 「setup関数ってなんかごちゃごちゃしてて見辛くない?」
私は最初そう思いました。正直今でもちょっと思ってはいます。
仕組みごとに書く場所が決まっていた方が整理されたコンポーネントを書けそうな気がしませんか?なぜsetupにまとめたのでしょうか?「テンプレートは、問題ではなく、技術を分けるだけだ」
“Templates separate technologies, not concerns.”Reactを作ったFacebookの有名な言葉です。
元来VueのSFC(Single File Component = .vueのコンポーネント)もこの課題を解決するものでした(関心や責任でコンポーネントを分割して、その中にhtml・js・cssをセットで書く)。しかしそれでもなお、SFCでは実装の仕組み(computedやdata)ごとに書く場所が決められています。実際にはコンポーネントを十分小さく分割することで多くの混乱は回避できる気もしますが、それでもコンポーネントが複雑になってくると、関連するデータやロジックがdataやcomputed、watch等に散在して依存関係が見えにくくなるのはVueのあるある、だと思います。
Composition APIのsetupはこれを一旦一箇所に全て混ぜられるようにした上で、仕組みではなく問題(関心事)で開発者が自由に再整理できるようにしたものです。
(Composition API RFCより引用。新しい書き方の方が意味的に整理した書き方ができる)実際setup関数の中身は(従来のmethodsやcomputedのような)Vueの特別な魔法が無いただのJavaScript(TypeScript)なので、分割したり別ファイルからimportしたり、JavaScript(TypeScript)でできることは基本的に全て許されます。そして、魔法のない普通のJavaScript(TypeScript)ということは、補完や静的解析等のエディタの恩恵も最大限に享受できるということでもあります。
この記事では「できるだけ元の形を維持して」Composition APIで書き直しているので分割等は行っていませんが、本来であれば(大きなコンポーネントは特に)setup関数をベタ書きせずに分割やimportを使って細かい粒度に切り出していくのが良いのだろうと思います。
蛇足ここまで
(私も理解は十分でないので、間違っていたらご指摘ください)
GameBoardコンポーネント(TS + Composition API)
最後に一番大きなGameBoardコンポーネントです。
これも少し長いですが、まずはscript部分を全部のせます。GameBoard.vue<script lang="ts"> import { createComponent, ref, computed, onMounted } from '@vue/composition-api' import GameCell from './GameCell.vue' // 型・インタフェースの宣言 type Player = 'p1' | 'p2' type Owner = Player | undefined interface CellState { owner: Owner } interface GameState { cells: CellState[], turn: Player } /** Player1を示す定数(画像ファイルの名前に使用するためSymbolにはしていません) */ const P1: Player = 'p1' /** Player2を示す定数(画像ファイルの名前に使用するためSymbolにはしていません) */ const P2: Player = 'p2' /** まだ石が置かれていないセル */ const BLANK: Owner = undefined /** プレイヤー毎の置かれる石(文字) */ const STONES = { [P1]: '○', [P2]: '×' } /** 一直線に並ぶセルの組み合わせ。8個しかないので列挙 */ const LINES = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6] ] export default createComponent({ components: { GameCell }, setup (props, ctx) { // 1. 元 data = reactive(), ref()でラップする const cells = ref<CellState[]>(Array(9).fill(0).map(_ => ({ owner: BLANK }))) const turn = ref<Player>(P1) // 2. 元 computedの要素 = computed()でラップする const winner = computed<Player | undefined>(() => { let winnerPName: Player | undefined LINES.forEach(line => { const ownersInLine = line.map(index => cells.value[index].owner) const isAllSame = ownersInLine[0] === ownersInLine[1] && ownersInLine[0] === ownersInLine[2] if (isAllSame && ownersInLine[0] !== BLANK) { winnerPName = ownersInLine[0] } }) return winnerPName }) const isGameEnded = computed<boolean>(() => { return !!(winner.value || cells.value.filter(cell => cell.owner === BLANK).length === 0) }) // 3. 元 methodsの要素 = そのまま(素の関数) const placeStone = (index: number) => { if (isGameEnded.value) { return } if (cells.value[index].owner !== BLANK) { return } cells.value[index].owner = turn.value if (isGameEnded.value) { // どちらかの勝ち or 引き分け ctx.emit('gameEnd', winner.value ? STONES[winner.value] : undefined) } else { // ターン交代 turn.value = turn.value === P1 ? P2 : P1 ctx.emit('nextTurn', STONES[turn.value]) } } // 4. 元 ライフサイクルフック = on + フック名の関数でラップ onMounted(() => { // 最初のターンを通知するためにイベントを発行 ctx.emit('nextTurn', STONES[turn.value]) }) // 最後にテンプレートから参照される物を全て返す return { cells, turn, winner, isGameEnded, placeStone } } }) </script>ポイントになる箇所を見ていきます。
型・インタフェースの宣言
冒頭で主な型とインタフェースを宣言しています。
JavaScriptでは定数程度でしたが、TypeScriptでは型を使ってより強力・明確に変数の内容を縛れることがわかると思います。これで例えば「現在のターン(turn)にnullが入る」と言ったバグは回避できますね。GameBoard(スクリプト冒頭)// 型・インタフェースの宣言 type Player = 'p1' | 'p2' type Owner = Player | undefined interface CellState { owner: Owner } interface GameState { cells: CellState[], turn: Player }元data部
setup関数の中を見ていきまししょう。
まずは元dataだった部分です。先ほどのAppコンポーネントと同様、ref()を使ってラップすることでリアクティブな変数を作ります。ref()には型を明示することもできます(省略・型推測もできますが、たとえばこの例でturnの<Player>を消すとP1型と解釈されP2が代入できなくなります)。GameBoard(元data部)// 1. 元 data = ref(), reactive()でラップする const cells = ref<CellState[]>(Array(9).fill(0).map(_ => ({ owner: BLANK }))) const turn = ref<Player>(P1)この
ref()でラップした変数の値を参照するときは(ラップされてしまっているので).valueをつけてアンラップする必要があることに留意してください。const num = ref(123) // ラップ(リアクティブな変数になる) console.log(num.value) // アンラップ(中身の普通の値を取り出す) num.value = 456 // 中身を書き換えまた、複数の変数をまとめてリアクティブにしたい場合、refの代わりにreactiveを使うこともできます
// 別解: reactiveを使ってオブジェクトを丸ごとラップすることもできる // この場合は'data'の塊ごとreturnするので、アクセスするときもdata.cells[0]のようになる // (reactive内の要素にアクセスする際は".value"は不要) const data = reactive<GameState>({ cells: Array(9)...略 turn: P1 })元Computed部
続いてComputedです。
これもComposition APIでComputed()というそのまんまの名前の関数が提供されているので、これでラップするだけです。GameBoard(元computed部)const winner = computed<Player | undefined>(() => { let winnerPName: Player | undefined LINES.forEach(line => { const ownersInLine = line.map(index => cells.value[index].owner) const isAllSame = ownersInLine[0] === ownersInLine[1] && ownersInLine[0] === ownersInLine[2] if (isAllSame && ownersInLine[0] !== BLANK) { winnerPName = ownersInLine[0] } }) return winnerPName })ref()の場合と同様、型を明示することもできます。
winnerはPlayer型ですが、勝敗が決まっていない・引き分けの場合はundefinedを返すとしているので、それを明示しています。(Owner型と書いても同じです)isGameEndedも同様です。一点、
!!をつけて戻り値がbooleanになるようにしました。GameBoard(元computed部)const isGameEnded = computed<boolean>(() => { return !!(winner.value || cells.value.filter(cell => cell.owner === BLANK).length === 0) })そう、実は最初に書いたJavaScript版のisGameEndedは戻り値がbooleanじゃなかったんです。。
試しにこの!!と<boolean>を外して関数の戻り値型を見てみると... 実はbooleanの他に文字列も返る実装にになってしまっていることがわかります。

空では無い文字列は真偽値として評価するとtrue扱いになるため大抵のケースでは問題は起こりませんが、その分見つかりにくいバグを埋め込むことにも繋がります。TypeScriptを使うとこういう気づきにくい間違いを事前に拾えるのでありがたい限りです元methods部と$emit
methodsの書き換えはAppコンポーネントでも出てきたので簡単に。
GameBoard(元methods部)// 3. 元 methodsの要素 = そのまま(素の関数) const placeStone = (index: number) => { if (isGameEnded.value) { return } if (cells.value[index].owner !== BLANK) { return } cells.value[index].owner = turn.value if (isGameEnded.value) { // どちらかの勝ち or 引き分け ctx.emit('gameEnd', winner.value ? STONES[winner.value] : undefined) } else { // ターン交代 turn.value = turn.value === P1 ? P2 : P1 ctx.emit('nextTurn', STONES[turn.value]) } }イベントの発行は
setup関数の第二引数として受け取れるctxオブジェクトのemitを使います。setup (props, ctx) {元mounted(ライフサイクルフック)部
次はライフサイクルフックです。
ライフサイクルフックは単純にonXXXという名前の関数がComposition APIで提供されているのでそれを使ってコールバックを登録するだけです。GameBoard(元ライフサイクルフック部)// 4. 元 ライフサイクルフック = on + フック名の関数でラップ onMounted(() => { // 最初のターンを通知するためにイベントを発行 ctx.emit('nextTurn', STONES[turn.value]) })テンプレート部からの利用
setup関数内で作った色々なもののうち、テンプレートから利用するものをsetup関数からreturnします。// 最後にテンプレートから参照される物を全て返す return { cells, turn, winner, isGameEnded, placeStone }これでテンプレートからは従来通りcomputedやmethodを扱えるようになります。
GameBoard(テンプレート部)<game-cell class="game-cell" v-for="(cell, index) in cells" :key="index" :owner="cell.owner" @click="placeStone(index)" />テンプレート部分は今まで通りVueの魔法が効いている部分なので、computedにアクセスするときに
.valueをつけてアンラップする必要はありません。(Vueがやってくれます)
その代わりテンプレートの中では型や補完の恩恵はあまり受けられないことに注意する必要があります。まとめ
この記事では3つのコンポーネントで構成されるシンプルな○×ゲームを従来のJavaScriptの記法と、新しいTypeScript + Composition APIの記法の2つで実装しました。
小規模ゆえに新しい書き方のメリットは理解しにくい部分もあるかと思いますが、これらのメリットをまとめている解説はいくつもあるので、まずはこの記事が新しい書き方にステップアップするための補助輪になれば幸いです。
まとめのまとめ
- Vue3で導入されるComposition APIはVue2でももうほとんど使えるよ
- 従来の書き方の大部分はComposition APIでもそこそこ簡単に書き直せるよ
- TypeScript + Composition APIを使うと補完やエラーチェックが効いて嬉しいよ
- Composition APIではコンポーネント内のロジックの書き方が従来よりも自由に。その分正しい分割や切り出しのスキルも必要になりそう
明日は、@amakawa_ さんです!
- 投稿日:2019-12-19T17:13:54+09:00
Canvasで作るスネークゲーム
この記事はtomowarkar ひとりAdvent Calendar 2019の19日目の記事です。
はじめに
キーボード操作で動作するスネークゲームのためスマホだと動きません
スペースキーでゲームスタート、十字キーで操作です。スコア30を超えた方には, 「すごい」と褒めさせていただきます
コード
snake.html<canvas id="game" width="400" height="400"></canvas> <script> canvas = document.querySelector("#game") ctx = canvas.getContext("2d") window.onload = () => { document.addEventListener("keydown", keyEvent) setInterval(game, 1000 / 15) } let play = false dot = 20 let snake = { x: 0, y: 0, vx: 0, vy: 0, trail: [] } let apple = { x: 1, y: 1 } tail = 5 score = highscore = 0 function game() { if (play) { snake.x += snake.vx snake.y += snake.vy if (snake.x < 0) { snake.x = dot - 1; score = 0; tail = 5 } if (snake.x > dot - 1) { snake.x = 0; score = 0; tail = 5 } if (snake.y < 0) { snake.y = dot - 1; score = 0; tail = 5 } if (snake.y > dot - 1) { snake.y = 0; score = 0; tail = 5 } // 背景描画 ctx.fillStyle = "black" ctx.fillRect(0, 0, canvas.width, canvas.height) // 蛇描画 ctx.fillStyle = "lime" for (let i = 0; i < snake.trail.length; i++) { ctx.fillRect(snake.trail[i].x * dot, snake.trail[i].y * dot, dot - 2, dot - 2) if (snake.trail[i].x == snake.x && snake.trail[i].y == snake.y) { tail = 5 score = 0 } } snake.trail.push({ x: snake.x, y: snake.y }) while (snake.trail.length > tail) { snake.trail.shift() } if (apple.x == snake.x && apple.y == snake.y) { tail++ score++ if (score > highscore) { highscore = score } apple.x = Math.floor(Math.random() * dot) apple.y = Math.floor(Math.random() * dot) } // りんご描画 ctx.fillStyle = "red" ctx.fillRect(apple.x * dot, apple.y * dot, dot - 2, dot - 2) // スコア描画 ctx.font = "20px serif"; ctx.fillStyle = "white" ctx.fillText("high score : " + highscore, 10, canvas.height - 10); ctx.fillText("score : " + score, 10, canvas.height - 30); } else { ctx.fillStyle = "white" ctx.fillRect(0, 0, canvas.width, canvas.height) ctx.fillStyle = "black" ctx.font = "30px serif"; ctx.fillText("Welcome to snake game.", 10, 50); ctx.fillText("Press the space key!!", 10, 90); } } function keyEvent(e) { switch (e.keyCode) { case 32: play = !play break case 37: snake.vx = -1 snake.vy = 0 break case 38: snake.vx = 0 snake.vy = -1 break case 39: snake.vx = 1 snake.vy = 0 break case 40: snake.vx = 0 snake.vy = 1 break } } </script>おわりに
以上明日も頑張ります!!
tomowarkar ひとりAdvent Calendar Advent Calendar 2019参考
http://faq.creasus.net/04/0131/CharCode.html
https://www.google.com/search?q=snake&oq=snake&aqs=chrome..69i57j0l5j69i61l2.821j0j7&sourceid=chrome&ie=UTF-8
https://www.youtube.com/watch?v=xGmXxpIj6vs
- 投稿日:2019-12-19T16:34:42+09:00
その時、世界は止まった。JavaScriptの世界では日本時間の西暦275,760年9月13日(土) 9:00:00に終わる...
JavaScriptのDate()ではいつまでサポートしてるんでしょうか?
正の値では1970/1/1 9:00:00から始まります。
>> new Date(0) Date Thu Jan 01 1970 09:00:00 GMT+0900 (日本標準時)では、いつまでサポートしてるのでしょうか?
https://tc39.es/ecma262/#sec-expanded-years をみると
Covering the full time value range of approximately 273,790 years forward or backward from 01 January, 1970
とのこと。1970に273,790足して、275,760で確認します。
>> new Date(275760,0,1,9,0,1) Date Tue Jan 01 275760 09:00:01 GMT+0900 (日本標準時)あれ?275,760年の1/1はサポートされてますね。
実際にいつまで指定したらエラーになるか試してみました。
>> new Date(275760, 8, 13,9,0,0) Date Sat Sep 13 275760 09:00:00 GMT+0900 (日本標準時)はOK。1秒後はどうでしょうか?
>> new Date(275760, 8, 13,9,0,1) Invalid Dateエラーになりました。
JavaScriptの世界では、日本時間の西暦275,760年9月13日(土) 9:00:00以降はサポートされていませんでした。
ちなみに秒数では1970/1/1から8,640,000,000,000,000ミリ秒間となります。日数にすると100,000,000日=1億日分でした。
>> new Date(8640000000000000) Date Sat Sep 13 275760 09:00:00 GMT+0900 (日本標準時)なのでJavaScriptの世界では弥勒菩薩の降臨までは持たないようですね...
- 投稿日:2019-12-19T15:47:34+09:00
課題解決手法としてのAtomic Designの解釈と実装
PLAIDでエンジニアをしている @kei-tamiya です。
本記事は PLAID Advent Calendar 2019 の 20日目になります。社外で個人的に開発しているプロジェクトでAtomic Designを採用しており、ある程度知見が溜まったので、1つの運用例としてご紹介したいと思います。
前提
どんなプロジェクト?
- もともとVue.jsを用いて動いているアプリケーションを1からリファクタするプロジェクト
- 別リポジトリに、Nuxt.js, Typescript, Vue Apolloを使用して開発
- フロントエンドエンジニア3〜4人、デザイナー1人
- 全員リモートで、週1で30〜60minぐらいミーティングできる
- GraphQLのAPIサーバーはおよそある状態
- 主に大きなページごと(Userに関するページなど)をエンジニア1人が担当して開発する
もともと課題に感じていたこと・選定理由
もともとそこまで厳格に規約を決めずシンプルにVue.jsで開発をしていましたが、以下のような課題を感じていました。
- 複数のエンジニアがデザインカンプを見て各々の実装をすると、デザインや実装に統一感がなくなる
- 1つのVueファイルが多くの責務を持って肥大化してしまう
- 車輪の再発明しがち・逆に誰かがコンポーネント作るだろうと様子の見合いになって結局作らない/できても中途半端
- デザイナー・エンジニア間の共通言語を作ることで、将来の追加実装や他のプロダクトを開発する際にも応用できる状態にし、スムーズに開発できるようにしたい
そこで、Atomic Designを採用することでデザインや実装方針をある程度画一化し、品質を担保することを目指しました。
Atomic Designとは?
画面を「原子」や「分子」などの単位になぞらえてパーツやコンポーネントとして分割して、それらを組み合わせることで構築するUIデザイン手法です。
以下の記事が詳しいので特に参考にさせていただきました。
本記事では主に、実際にAtomic Designを用いた知見をまとめることとします。
Atomic Designを用いる上で特に意識したこと
結論からいうと、厳密に定義することを追い求めず、あくまで当初の課題を解決することを目的として、以下の点に注意して進めました。
- 完璧なAtomic Designを定義しない・求めない
- AtomとOrganismは先に整備して、基本的にはMoleculeに追加していく
- 肥大化するので、コンポーネントの種類ごとにディレクトリを切る
- 各コンポーネント自体は外側へのマージン情報を持たない・マージンはマージンのみを管理するコンポーネントを作る
完璧なAtomic Designを定義しない・求めない
いざAtomic Designを始めると、どこまでいっても「これはMoleculeじゃなくてOrganismではないか?」など、感覚の議論になりがちです。
当初はそれぞれの粒度を以下のように定義しましたが、言葉の定義だけでは粒度を統一することはエンジニア間でさえかなり難しく、その上でデザイナー・エンジニア間の意思疎通を行うのは無理があるなという感想を抱きました。- Atomsは「それ以上機能が分解できないもの」。 - Moleculesは「ユーザの動作を促すもの」。 // どこまで作ればユーザは動作を促される..? - Organismsは「コンテンツが完結しているもの」。 // 何をもってコンテンツは完結する..?また、コンポーネントを整備する際はAtomから順に作りたくなりますが、実際の実装ではページをどう分割していくかを考えるためPages → Templates → Organisms →...と逆の順で考えることになり、どうしてもOrganismとMoleculeのどちらにするかを決められないケースがあり、もはや悩む時間や、感覚の議論をして手戻りが発生する時間が不毛だなと感じました。
例えば、以下のような場合に迷いが発生しました。小さい単位(Atom)から考える場合
- Atomを組み合わせて作ったListItemはMoleculeに作るとして、ListはOrganismになる..?
大きい単位(Pages)から考える場合
PagesやTemplateは主にレイアウトのコンポーネントになるため、個別のコンポーネントは基本的にOrganismから考えることになり、以下のような迷いが発生しました。
A. 見出しなしのListだけの場合、Listだけで「コンテンツが完結」しているとみなすと、ListがOrganismになる..?
B. 見出し+Listの場合、見出し+Listで初めて「コンテンツが完結」するので、見出し+ListコンポーネントがOrganismsとなるが、Listは完結していないのでMoleculeになる..?
そしてしばらくすると、MoleculeにListがあり、Organismにも見出し+ListコンポーネントとListコンポーネントが氾濫してしまい、それ以降にジョインするエンジニアがどちらに作るべきかさらに迷う状況になってしまいました。
そこで、
諦めてAtomic Designを自分たちが管理可能な形で解釈して、あくまでチームの当初の目的を満たすための設計手法の一つとして捉えることにしました。AtomとOrganismは先に整備して、基本的にはMoleculeに追加していく
MoleculeかOrganismかで悩むぐらいなら、もういっそほとんどをMoleculeにしてしまう決断をしました。
共通のルールとそれぞれの分類ルールは以下のようなルールとしました。共通ルール
- コンポーネントには外へのマージン情報を持たせない
- borderがあるコンポーネントの場合はborderの内側のpaddingはコンポーネントが持っても良い
- 同列単位以下のものはimport可能、自分より大きい単位のコンポーネントはimport不可(e.g. MoleculeからMoleculeのimportは可能、MoleculeからOrganismのimportは不可)
- LoaderやIconなどのAtomを他のAtomからimportするかは悩みポイントで、Loaderは基本importして使うが、Iconをpropsに持つButtonも許した。この辺はそこまで縛らない方針とした。
- 肥大化するので、コンポーネントの種類ごとにディレクトリを切って分類する
├── molecules │ ├── cards │ │ ├── AppCard.vue │ │ ├── AppCardList.vue │ │ ├── FlatCard.vue │ │ ├── index.tsimport時にpathを深くしたくないので、importはindex.tsからのみ行うようにしています。
Atom
- 「それ以上機能が分解できない」という定義である程度分類できていたのでそのまま
Molecule
- 基本的にはAtom, Organismでなければ全部ここに追加する
- 例えば言葉で定義するとしたら、「その種類として完成する以下のものたち」のようなイメージ
- 種類が「Card」のコンポーネントの場合、CardListItem(ループで複数配置する前提のもの)だけでは完成していないので、CardListまでがMolecule
- CardListはCardListItem間のマージンを規定する責務を持つ
- 見出し(種類:Text)+CardList(種類:Card)の場合、別の種類なので見出し+ListコンポーネントはMoleculeには作らない
- 等間隔のマージンを規定するMoleculeをSeparatorとして用意する(これはAtomにすべきか悩ましい)
- 各種Listコンポーネントが任意のSeparatorを用いてListItem間のマージンを規定することで、マージンに関するロジックをSeparatorの責務にできる
├── molecules │ ├── separators │ │ ├── HorizontalMarginSeparator.vue // 水平方向の等間隔マージンを規定する │ │ ├── HorizontalBorderSeparator.vue // 水平方向の等間隔でのborder区切りを規定する │ │ ├── VerticalBorderSeparator.vue // 垂直方向の等間隔マージンを規定する │ │ ├── VerticalMarginSeparator.vue // 垂直方向の等間隔でのborder区切りを規定する │ │ ├── index.tsOrganism
- 「異なる要素間のマージンを規定するもの」
- 粒度は基本的に、HTMLのセクション関連単位(header, footer, nav, sectionなど)
- 一般的にWebの知識がある人同士で共有できる単位の方が意思の疎通がしやすい
例えば、見出しにはAtomのTextコンポーネント(classをTextとする)を使っている場合、AppSectionは直下のTextに対してのみ下方向マージンを与えるようにしました。
<template> <div class="AppSection"> <slot /> </div> </template> <script lang="ts"> import Vue from 'vue'; export default Vue.extend({}); </script> <style lang="postcss" scoped> .AppSection /deep/ > .Text { margin-bottom: 20px; } </style>これにより、AppSectionの中に見出し(Textコンポーネント)+Listコンポーネントを置くことで、異なる要素間のマージンが規定されます。
上述の見出し + Listコンポーネントの場合、以下のようになります。
Template
- Organismをレイアウトする・Organism間のマージンを規定する
2カラムレイアウトや聖杯レイアウトなど、アプリケーションで用いるレイアウトパターンをTemplateとして整備します。
OrganismがHTMLのセクション関連の単位で整備されているので、Organismをどのように配置するかの構造とスタイルを主に書きます。Pages
- Nuxt.jsでいう通常ルーティングをPageとする
- 基本的にはPage内でのみデータアクセス可能
ディレクトリ構成
各ページのドメイン単位でcomponentも用意する
├── src │ ├── components │ │ ├── common │ │ │ ├── atoms │ │ │ ├── molecules │ │ │ ├── organisms │ │ │ └── templates │ │ └── users │ │ ├── molecules │ │ └── organisms │ │ └── templates │ ├── pages │ │ ├── index.vue │ │ └── users │ │ ├── _id │ │ └── index.vue
src/components/common/以下は、どのページからも使う汎用的なコンポーネントを配置しています。
src/pages/users/以下がAtomic Designでいう「Page」にあたり、Userに関するコンポーネントはsrc/components/users/以下に整備することとしました。
Userのページを作る上でも基本的にAtomic Designに則ってディレクトリを分割し、対応するcommon配下のコンポーネントを自由にimportして組み合わせれば基本的にはそこまでCSSを書き足さなくてもpropsに渡すだけで整うような状態を目指しています。実際に使ってみて、どう感じているか
現時点で感じているメリット・デメリットで挙げてみます。
メリット
- 共通コンポーネントを使えばほぼスタイルを書かなくて良いので、似通ったデザインのページの場合は快適に開発できる
- 各ページに閉じるような共通では使わない特殊なコンポーネントの場合は、わざわざ厳密にAtomic Designを追い求めすぎない判断をとりやすくなった
- 新規に整備する共通コンポーネントは基本的にMoleculeに追加するだけなので、判断に迷わなくなった
- デザインカンプをエンジニアが受け取って独自に解釈して実装するしかなかった前と比べると、明らかに共通言語ができて意図を組んだ実装ができる/デザインを作成できるようになった
デメリット
- ある程度満足に開発を進められるレベルにコンポーネントを整備しきるまでにそこそこ時間がかかる
- 実際整備しきった効果を得るのは、今後長い目で複数人で運用していったときだと思うが、早期・初期からリターンを得られるわけではない
- 新規メンバーには定めたルールの一定の学習コストを払ってもらう必要がある
- 最初から完璧に全てのコンポーネントを用意してから開発できるわけではもちろんないので、都度リファクタや整備していくコストをかける必要はある
当初の課題は解決できたのか
学習コストを払う必要はあるものの、ある程度は解決できる状態になったのかと思います。
複数のエンジニアがデザインカンプを見て各々の実装をすると、デザインや実装に統一感がなくなる
→デザインや実装がずれる元凶だったマージンやサイズなどは共通コンポーネント内に閉じているので、基本的に整備した共通コンポーネントを使うことで統一できるようになりました。
1つのVueファイルが多くの責務を持って肥大化してしまう
→共通コンポーネントの粒度に沿って各ドメインもディレクトリ/ファイルを切るので、巨大なVueファイルはかなり減りました。
ただし、Moleculeの中で多くの責務を持ったコンポーネントが居座ってしまいがちなため、定期的に整備する必要があります。車輪の再発明しがち・逆に誰かがコンポーネント作るだろうとお見合いになる
→最初にコストをかけて少人数で重要な共通コンポーネントをおよそ整備しきったので、コンポーネント開発においてコンフリクトはそこまで発生しない状態になりました。
しかしながら、今回のプロジェクトはそこまで複数の複雑な画面があるわけではなかったため、より大規模なプロジェクトだと継続的に上記問題は発生するかもしれません。デザイナー・エンジニア間の共通言語を作ることで、将来の追加実装や他のプロダクトを開発する際にも応用できる状態にし、スムーズに開発できるようにしたい
→エンジニアが実装時に考慮する情報をデザイナーがある程度理解することができたため、実装とデザインの間で以前よりも無理がないよう最初から意識して設計できるような状態に徐々になっていると感じます。
まだ別のプロダクト開発を進めていないので実際に大きな恩恵は受けていませんが、デザイナー・エンジニア間での認識を擦り合せる時間をかなりとったため、共通言語ができたのは大きいです。その他所感
どのように整備を進めると良いか
初期に整備する手順としては、以下の順で進めると良いなと感じました。
- MiroなどでAtom, Moleculeと思うコンポーネントのスクショをできる限り全て洗い出して、関連するものごとに分類する
- どこまで分割するとAtomとみなせるか、という共通認識を擦り合わせておいた方が良い
- 最初は少人数(エンジニア1〜2人・デザイナー1人など)で意思疎通して、マストなコンポーネントから一気にAtom整備を進めた方が良さそう
- Atomの場合、どういう「機能」を持ったAtomか言葉で定義して分類していく
- その機能はさらに分解できないか検討して分割していく
- デザイナーにマージン・ボーダー・文字サイズ・色・hover時の挙動・transition・最大横幅や改行ルールを洗い出し、説明してもらう
- 逆にデザイナーはPropsの概念を理解しづらいので、エンジニアがどういう粒度でコンポーネントを分割しようとしているか思考を説明すると良い
- デザイン的にはA, Bは全く別物だけど、Propsを受けることで1つのコンポーネントでコスパよく(可読性もよく)まとめられるので1つにできるし、したい、みたいな思考を伝えると良い
- 既存ライブラリのコンポーネントを使うことも考える
- Selectなど自前実装するのは骨が折れるコンポーネントも多いので、ライブラリを用いて車輪の再発明をせずに済ませるのも1つの選択肢
- スタイルを上書きすれば当初のデザインを再現できるのかどうか/その実現可能性に沿ってある程度デザインは調整可能かどうかの相談が必要
- ライブラリ選定のタイミングからデザイナーとエンジニアが協力して調査し、デザインガイドラインを一緒に整備するとより良さそう
Storybookは意思疎通の観点では重要
- デザイナー側の理解を促進できる
- デザイナーからの細かいUIの修正依頼を受ける窓口になる(実プロダクトの実装を進めてからの手戻りを減らせる)
- エンジニアのコンポーネント設計・実装をある程度まとめられる
- CardA, CardBの違いを認識するためコードを頑張って追うより、最初にStorybookで視覚的に認識できた方が早い
相応の覚悟とステークホルダーの理解が必要
- デザイナーとエンジニアがお互いを理解するためには、単純なデザインとプログラミングという境界を超えて歩み寄る覚悟が必要です
- 今すぐに恩恵を得られるものではないので、ある程度周囲の理解は必要になるかと思います
おわりに
Atomic Designをチームにとっての課題解決手法として独自に解釈し、使いやすい形で運用している例をご紹介させていただきました。
どの手法を採用したとしても永遠に完璧なものは存在しないので、常にチームにとっての最善を選び、失敗しながらも改善を続けられるのかが大事なのかなと思います。独自の解釈をしているため、「Atomic Design」と銘打つのはおこがましいかもしれませんが、何か参考になる点があれば幸いです。
- 投稿日:2019-12-19T15:23:13+09:00
ReactでuseStateを上手く扱うにはどのように書くのがベストか
Hooks を使ってコードを書いていると、
useState()の宣言が多くなってしまってごちゃごちゃしてしまうことがある。
このような問題に直面した時にどのように書いていくのが良いのだろうか。単純に書いた場合、Class Component に慣れている方は、stateをまとめて宣言した方が見栄えが良いと思われるかもしれない。
これについてReactのドキュメントのFAQを読むと、次のように書いてある。すべての state を 1 つの useState 呼び出しに含めても動作しますし、フィールドごとに別に useState を持たせることでも動作はします。しかしこれらの両極端の間でうまくバランスを取り、少数の独立した state 変数に関連する state をグループ化することで、コンポーネントは最も読みやすくなります。state のロジックが複雑になった場合は、それをリデューサで管理するか、カスタムフックを書くことをお勧めします。
つまり、基本的に少数の独立した state に分割して宣言することで、Custom Hook を作りやすくなり、リファクタリングしやすくなったり、コードが見やすくなるのである。
また、「どうにも上手くグループ分けができず、汚くなってしまった。」という時に限って
useReducer()を使って、処理をまとめるのが良いと考える。それ以外にも
useReducer()の利用場面として、以下のような場面がある。
- 複数階層にまたがって更新を発生させるようなコンポーネントではパフォーマンスの最適化になる(https://ja.reactjs.org/docs/hooks-faq.html#how-to-avoid-passing-callbacks-down)
- 前回のstateに基づいて、stateを更新したいような場面(https://overreacted.io/ja/a-complete-guide-to-useeffect)
そのため、次のようなステップでコードを書いていくのが良いと思われる。
const x = useState(0);のように独立させて宣言してみるconst [position, setPosition] = useState({ x: 0, y: 0});のようにして役割ごとにまとめてみる- それでも上手くいかない場合は、
useReducer()を使うというようにしたほうが共通化しやすいと考える。メモ程度にまとめてみました!
間違っている所や他に意識しているようなことがあれば教えていただけたら嬉しいです。
- 投稿日:2019-12-19T15:16:08+09:00
Firebase Extensionsの翻訳機能はサービス終了したGoogle翻訳の代わりになるか?
この記事はくふうカンパニー Advent Calendar 2019の19日目の記事となります。
Da Vinci StudioでWebエンジニアをしている@TAByasuです。
くふうグループ内の組織であるDa Vinci Studioではグループ内外の受託開発・デザインを生業にしています。近頃はいくつかの案件で「Webサイト上に機械翻訳の機能を追加できないか」というお問い合わせをいただくことが増えてきました。
そう、Webサイトのヘッダー付近に翻訳対象の言語選択UIが鎮座し選択した言語にWebページをまるっと翻訳してくれる機能です。
有名なところはやはりGoogle翻訳ではないでしょうか。
しかし2019年にWebサイトに追加するタイプのGoogle翻訳はサービス終了してしまいました。
当時は無料で提供される翻訳機能の精度の高さと実装の手軽さからWebサイトに機械翻訳を実装するときに真っ先に検討したものでしたが。もちろん他社が有料提供している機械翻訳サービスも検討の候補にあがります。
しかし導入したいサービスの規模に対してコストの折り合いがつきにくく二の足を踏むことが多いです。そこで低コストで必要なWebページに必要な言語だけを機械翻訳できる術はないか模索しました。
その中でBeta版(2019年12月19日現在)ではありますがFirebase ExtensionsにTranslate TextというGoogle翻訳の拡張機能が提供されているのを見つけたので少し試してみました。Firebaseの設定をざっくり
細かい説明は後回しにしてFirebase Extensionsを設定して試しに動かしてみます。
Firebaseのアカウント登録などは割愛しています。Firebaseプロジェクトの左メニューからExtensionsを選択すると拡張機能一覧が表示されます。
Translate Textを選択して流れに沿って登録していきます。
拡張機能を使うにはBlazeプラン必須なのでクレカ登録する必要があります。
拡張機能の構成を設定。ここで翻訳対象の言語を指定します。(あとで変更もできます)
拡張機能のインストールに数分かかりますが、完了後は詳細画面で翻訳対象言語の設定変更などができます。
Webページの翻訳に使うのでWebアプリのFirebase登録をします。
ここでの手順は割愛。
サンプルコード
サンプルコードはこちら
https://github.com/tabyasu/google-translate-on-firebase/blob/master/sample.htmlこのサンプルコードでは大雑把にbody以下のテキスト、HTMLタグも含めて一気にFirestoreに登録してTranslate Textの翻訳対象にしています。
<script> // FirebaseのWebアプリ設定は割愛 var db = firebase.firestore(); const sleep = time => { return new Promise((resolve, reject) => { setTimeout(resolve, time); }); }; const translate = async e => { e.preventDefault(); // Firestoreに翻訳対象を追加登録 const docRef = await db.collection('translations').add({ input: document.getElementsByTagName("body")[0].innerHTML, created_at: firebase.firestore.FieldValue.serverTimestamp(), }); // 翻訳がすぐに完了しないケースがあるので完了まで待ち let doc = await docRef.get(); // TODO Error handling while (!doc.data().translated) { await sleep(10000); doc = await docRef.get(); }; const translated = doc.data().translated; document.getElementsByTagName("body")[0].innerHTML = translated[e.target.value]; }; // ページ上部にTranslate Textで設定した翻訳先の言語選択ボタンを表示 window.onload = () => { const languages = { en:'英語', 'zh-CN':'中国語(簡体)', ko:'韓国語' } const div = document.createElement('div'); const element = document.getElementsByTagName('body')[0]; element.insertAdjacentHTML('afterbegin', '<div style="background-color:#EDF7FF; height:30px;"></div>'); const translateButtons = element.firstElementChild; Object.keys(languages).forEach(k => { const button = document.createElement('button'); button.setAttribute('type', 'button'); button.setAttribute('name', 'translate'); button.setAttribute('value', k); button.innerText = languages[k]; button.addEventListener('click', translate); translateButtons.appendChild(button); }) } </script> </head> <body> <div>JS導入部分のサンプルです</div> </body> </html>翻訳結果
翻訳対象のテキストデータはFirestoreに登録されます。ドキュメントIDは指定してないのでランダムなIDでドキュメント登録されていますね。
フィールドのinputに翻訳対象のテキストデータが登録され、translatedフィールドにmapで翻訳結果が登録されています。
en:英語、zh-CN:中国語(簡体)、ko:韓国語 に一気に翻訳されたようですね。
考察
料金
FirebaseのBlazeブラン(従量課金)が必須ですが、Sparkプランの無料利用分を含みます。
https://firebase.google.com/pricingTranslate TextはGCPのCloud Translation APIを利用しています。が50万文字までの翻訳は無料です。
https://cloud.google.com/translate/?hl=ja#jump-to-3
一度に翻訳できる量
今回のサンプルや弊社コーポレートサイトのページのテキスト量だと一括で翻訳できましたが、サンプルコードのようにHTMLを文字列として一括で翻訳しようとすると翻訳結果が返ってきませんでした。
下記の注意書きがあったように長文翻訳には向かないようです。Cloud Translation は、短いリクエストの翻訳に最適化されています。リクエストごとに推奨される最大の長さは 2,000 コードポイントです。リクエストが非常に大きいと、利用可能な割り当ての量にかかわらず、Cloud Translation によって拒否され、400 INVALID_ARGUMENT エラーとなります。
https://cloud.google.com/translate/quotas?hl=ja
翻訳の精度?
僕が外国語に疎いので翻訳結果がネイティブに伝わるかどうかの精度判定はできていません。
が、Webページを一括で翻訳しようとするとHTML構文として明日は<br>晴れるかなのような改行を含む文章の翻訳結果がWill it be sunny tomorrow?のように文章にならずtomorrowとWill it be sunnyに分断された翻訳結果になっていました。まとめ
コストについて
- HTMLを文字列として翻訳対象にしてしまうと当然コスト高くつくので、実戦投入する場合はテキストノードのみを翻訳対象にして丁寧に翻訳していく必要がある。
- 更新頻度の少ないページの翻訳であればFirestoreのドキュメントIDにページURLを指定して翻訳結果を登録し、ページに更新日時などの情報をもたせておいて一度翻訳した結果と差分無い場合は再利用できるようにすると翻訳コストを抑えられそう。
利用方法について
- 汎用的に使えるようにFunctionsでAPI化して、ドメイン指定やAPIキーを設定するなどFirebaseのconfig情報をWebサイト上に出さないつくりにしてアクセス制御するとよさそう。
<br>や<span>などが含まれている文章の翻訳結果を期待どおりにするためにJSの実装頑張る必要があるからちょっと大変そう。結論
UI全体を精度高く機械翻訳するには実装工数のコストがかかるが、収益の低いWebサービスに導入するには選択肢になりえる。
・・・というのが僕の感想です。今後、テキストのみの翻訳にブラッシュアップして本番導入できることを目指したい気持ちです?
- 投稿日:2019-12-19T15:16:08+09:00
Firebase Extensionsの翻訳機能はサービス終了したGoogleウェブサイト翻訳ツールの代わりになるか?
この記事はくふうカンパニー Advent Calendar 2019の19日目の記事となります。
Da Vinci StudioでWebエンジニアをしている@TAByasuです。
くふうグループ内の組織であるDa Vinci Studioではグループ内外の受託開発・デザインを生業にしています。近頃はいくつかの案件で「Webサイト上に機械翻訳の機能を追加できないか」というお問い合わせをいただくことが増えてきました。
そう、Webサイトのヘッダー付近に翻訳対象の言語選択UIが鎮座し選択した言語にWebページをまるっと翻訳してくれる機能です。
有名なところはやはりGoogleウェブサイト翻訳ツールではないでしょうか。
しかし2019年にWebサイトに追加するタイプのGoogle翻訳はサービス終了してしまいました。
当時は無料で提供される翻訳機能の精度の高さと実装の手軽さからWebサイトに機械翻訳を実装するときに真っ先に検討したものでしたが。もちろん他社が有料提供している機械翻訳サービスも検討の候補にあがります。
しかし導入したいサービスの規模に対してコストの折り合いがつきにくく二の足を踏むことが多いです。そこで低コストで必要なWebページに必要な言語だけを機械翻訳できる術はないか模索しました。
その中でBeta版(2019年12月19日現在)ではありますがFirebase ExtensionsにTranslate TextというGoogle翻訳の拡張機能が提供されているのを見つけたので少し試してみました。Firebaseの設定をざっくり
細かい説明は後回しにしてFirebase Extensionsを設定して試しに動かしてみます。
Firebaseのアカウント登録などは割愛しています。Firebaseプロジェクトの左メニューからExtensionsを選択すると拡張機能一覧が表示されます。
Translate Textを選択して流れに沿って登録していきます。
拡張機能を使うにはBlazeプラン必須なのでクレカ登録する必要があります。
拡張機能の構成を設定。ここで翻訳対象の言語を指定します。(あとで変更もできます)
拡張機能のインストールに数分かかりますが、完了後は詳細画面で翻訳対象言語の設定変更などができます。
Webページの翻訳に使うのでWebアプリのFirebase登録をします。
ここでの手順は割愛。
サンプルコード
サンプルコードはこちら
https://github.com/tabyasu/google-translate-on-firebase/blob/master/sample.htmlこのサンプルコードでは大雑把にbody以下のテキスト、HTMLタグも含めて一気にFirestoreに登録してTranslate Textの翻訳対象にしています。
<script> // FirebaseのWebアプリ設定は割愛 var db = firebase.firestore(); const sleep = time => { return new Promise((resolve, reject) => { setTimeout(resolve, time); }); }; const translate = async e => { e.preventDefault(); // Firestoreに翻訳対象を追加登録 const docRef = await db.collection('translations').add({ input: document.getElementsByTagName("body")[0].innerHTML, created_at: firebase.firestore.FieldValue.serverTimestamp(), }); // 翻訳がすぐに完了しないケースがあるので完了まで待ち let doc = await docRef.get(); // TODO Error handling while (!doc.data().translated) { await sleep(10000); doc = await docRef.get(); }; const translated = doc.data().translated; document.getElementsByTagName("body")[0].innerHTML = translated[e.target.value]; }; // ページ上部にTranslate Textで設定した翻訳先の言語選択ボタンを表示 window.onload = () => { const languages = { en:'英語', 'zh-CN':'中国語(簡体)', ko:'韓国語' } const div = document.createElement('div'); const element = document.getElementsByTagName('body')[0]; element.insertAdjacentHTML('afterbegin', '<div style="background-color:#EDF7FF; height:30px;"></div>'); const translateButtons = element.firstElementChild; Object.keys(languages).forEach(k => { const button = document.createElement('button'); button.setAttribute('type', 'button'); button.setAttribute('name', 'translate'); button.setAttribute('value', k); button.innerText = languages[k]; button.addEventListener('click', translate); translateButtons.appendChild(button); }) } </script> </head> <body> <div>JS導入部分のサンプルです</div> </body> </html>翻訳結果
翻訳対象のテキストデータはFirestoreに登録されます。ドキュメントIDは指定してないのでランダムなIDでドキュメント登録されていますね。
フィールドのinputに翻訳対象のテキストデータが登録され、translatedフィールドにmapで翻訳結果が登録されています。
en:英語、zh-CN:中国語(簡体)、ko:韓国語 に一気に翻訳されたようですね。
考察
料金
FirebaseのBlazeブラン(従量課金)が必須ですが、Sparkプランの無料利用分を含みます。
https://firebase.google.com/pricingTranslate TextはGCPのCloud Translation APIを利用しています。が50万文字までの翻訳は無料です。
https://cloud.google.com/translate/?hl=ja#jump-to-3
一度に翻訳できる量
今回のサンプルや弊社コーポレートサイトのページのテキスト量だと一括で翻訳できましたが、サンプルコードのようにHTMLを文字列として一括で翻訳しようとすると翻訳結果が返ってきませんでした。
下記の注意書きがあったように長文翻訳には向かないようです。Cloud Translation は、短いリクエストの翻訳に最適化されています。リクエストごとに推奨される最大の長さは 2,000 コードポイントです。リクエストが非常に大きいと、利用可能な割り当ての量にかかわらず、Cloud Translation によって拒否され、400 INVALID_ARGUMENT エラーとなります。
https://cloud.google.com/translate/quotas?hl=ja
翻訳の精度?
僕が外国語に疎いので翻訳結果がネイティブに伝わるかどうかの精度判定はできていません。
が、Webページを一括で翻訳しようとするとHTML構文として明日は<br>晴れるかなのような改行を含む文章の翻訳結果がWill it be sunny tomorrow?のように文章にならずtomorrowとWill it be sunnyに分断された翻訳結果になっていました。まとめ
コストについて
- HTMLを文字列として翻訳対象にしてしまうと当然コスト高くつくので、実戦投入する場合はテキストノードのみを翻訳対象にして丁寧に翻訳していく必要がある。
- 更新頻度の少ないページの翻訳であればFirestoreのドキュメントIDにページURLを指定して翻訳結果を登録し、ページに更新日時などの情報をもたせておいて一度翻訳した結果と差分無い場合は再利用できるようにすると翻訳コストを抑えられそう。
利用方法について
- 汎用的に使えるようにFunctionsでAPI化して、ドメイン指定やAPIキーを設定するなどFirebaseのconfig情報をWebサイト上に出さないつくりにしてアクセス制御するとよさそう。
<br>や<span>などが含まれている文章の翻訳結果を期待どおりにするためにJSの実装頑張る必要があるからちょっと大変そう。結論
UI全体を精度高く機械翻訳するには実装工数のコストがかかるが、収益の低いWebサービスに導入するには選択肢になりえる。
・・・というのが僕の感想です。今後、テキストのみの翻訳にブラッシュアップして本番導入できることを目指したい気持ちです?
- 投稿日:2019-12-19T15:07:15+09:00
Javascriptメモ②
Javascriptのメモです。
アラートのポップアップ表示
<input type='submit' value='wanko' onClick='alert('といぷー');' クリックするとonClick関数が実行される。 alert()でも書ける。演算子
算術演算子 足し算 + 引き算 − 掛け算 * 割り算 / あまり % 代入演算子 a = a+5; a += 5; a = a-5; a -= 5; 同じ意味です。 単項演算子 a = 10 % 4; a++; //3 a--; //2onload
onloadはイベントハンドラーです。 ページを読み込んだ後に実行する処理を決めることが出来ます。 window.onload = () => { alert('toypoo'); }; 単一の要素に対してonloadは単一のイベントハンドラーしか設定できません。 複数指定したい場合はaddEventListenerを使いましょう。filterメソッド
配列オブジェクトから特定の条件の配列を新しく生成します。 配列の要素の値value 数値インデックスindex 配列要素を格納した配列オブジェクトを指定できます。 const date = [1,2,3,4,5]; const result =date.filter(value =>{ return value % 2 ===1; }); const wanko = ['toypoo','pome','siba']; const dog = wanko.filte(value => { return value ==== 'toypoo'; });callback関数
callback関数は他の関数に引数として渡される関数です。 イベントハンドラに多く使われます。 const wanko = () => { // } pome(wanko); const wanko = response => { const data = this.response; console.log('data.name'); } const sayWanko(){ const rewuest = new XMLHttpRequest(); request.open('GET', 'http://',true); reques.responseType = 'josn'; request.addEventListener('load', wanko); request.send(); }foreachメソッド
配列への繰り返し処理が書けます。 const dogs = ['pome','toypoo','siba']; dogs.foreach(dog => { console.log(dog); }); 配列に対しての処理です、dogという引数を受け取り配列の数分繰り返し処理を行ってくれます。 dogを引数にしていますが引数がaでも動きます。 const dogs = ['pome','toypoo','siba']; dogs.foreach((dog,number) => { console.log(number + 1)+'匹目は'+dog); }); 引数が2つになりました。 第一引数に配列のデータ、第二引数にindex番号を受け取っています。 配列の一番目は0ですので+1してあげる必要があります。bind
関数に対してthisや引数を指定、 他の関数との結びつけが出来ます。 function wanko(){ console.log(this); } const dog = say.bind('toypp'); dog(); function wanko(a,b,c){ return a + b + c; } const sum = wanko.bind(null, 10, 10); console.log(sum());onClick
マウスクリックに対しての処理をするイベントハンドラーです。 document.getElementById('button').onClick = () => { document.getElementById('text').innerHTML = 'わんこ'; }; document.getElementById('button').onClick = () => { this.classList.toggle('wanko'); };index
indexメソッドは文字列や配列に指定した文字があるか探して、あればその位置を変えしてくれます。 const arr = ['a','b'.'c']; const index = arr.indexOf('b'); console.log(index); 先頭は0、存在しない場合は-1です。 第二引数に開始位置を指定することも出来ます。クラス
classを使うことでオブジェクト指向プログラミングが簡潔に書けます。 class Wanko { constructor(name) { this.name = name; } say() { console.log(this.name); } } const dog = new Wanko('toypoo'); dog.say(); newでWankoクラスのインスタスを生成するたびにconstructorによる初期化が行われます。replaceメソッド
replaceメソッドでは置換処理を行うことが出来ます。 文字列による置換と正規表現による置換が可能です。 const str 'わんこ<br>わんこ<br>わんこ'; const dog = str.replace(/<br>/g,'\n'); console.log(dog);
- 投稿日:2019-12-19T12:46:29+09:00
vscode-eslint v2
Microsoft が自ら提供している Visual Studio Code の ESLint 拡張 (vscode-eslint) がメジャー バージョンアップして、その設定方法が大幅に変わったのでまとめてみます。
ESLint の自動修正を保存時に自動的に適用するための設定
v1 で提供されていた
eslint.autoFixOnSave設定は廃止され、代わりにeditor.codeActionsOnSaveまたはeditor.formatOnSaveを利用します。言い換えると、vscode-eslint 独自の方法で保存時の処理を実施していたのが、Visual Studio Code の標準的な方法で実施するように変更されたわけですね。2 つありますが、editor.codeActionsOnSaveのほうが推奨されています。
editor.codeActionsOnSaveコードアクション (拡張機能がコードへの指摘をユーザーに示すための機構) を保存時に実行する設定です。Visual Studio Code では ESLint の自動修正はすべてコードアクションとして扱われていて、ポップアップ UI から 1 個ずつ適用できます。これを保存時に全部適用しようというものです。
settings.json{ "editor.codeActionsOnSaveTimeout": 2000, "editor.codeActionsOnSave": { "source.fixAll.eslint": true } }
@typescript-eslint/eslint-pluginなど、解析に時間がかかるプラグインを利用している場合はeditor.codeActionsOnSaveTimeoutを長めに設定してあげる必要があります。デフォルトでは 750ms になっていて、ESLint の処理がこの時間以内に終わらなければ自動修正は行われません。
editor.formatOnSaveこちらは推奨される方法ではありませんが、念のために紹介します。
ESLint をフォーマッターとして利用し、「ドキュメントを整形する」アクションを保存時に実行する設定です。ESLint はフォーマッターではないのでむりやり感がありますね。settings.json{ "editor.formatOnSaveTimeout": 2000, "editor.formatOnSave": true, "[javascript]": { "editor.defaultFormatter": "dbaeumer.vscode-eslint" } }
@typescript-eslint/eslint-pluginなど、解析に時間がかかるプラグインを利用している場合はeditor.formatOnSaveTimeoutを長めに設定してあげる必要があります。デフォルトでは 750ms になっていて、ESLint の処理がこの時間以内に終わらなければ自動修正は行われません。JavaScript 以外のファイルで ESLint を利用する設定
v1 では、vscode-eslint は JavaScript ファイルだけをデフォルトで検証し、その他のファイルで ESLint を使いたいときは
eslint.validate設定を書く必要がありました。v2 では、.eslintrcファイルに適切な設定があれば、TypeScript, Vue, HTML ファイルもデフォルトで検証します。
- TypeScript ... カスタム パーサーとして
@typescript-eslint/parserが設定されている- Vue ... プラグイン設定に
eslint-plugin-vueが存在する- HTML ... プラグイン設定に
eslint-plugin-htmlが存在するこれらの設定が
overridesセクションにあってもしっかり認識してくれるようです。一方、上記以外のファイルで ESLint を使いたいときは引き続き
eslint.validate設定が必要です。モノリポ (mono repository) で ESLint を利用する設定
ESLint を実行するには、CWD (current working directory) の設定が重要です (
.eslintignoreは CWD にあるものしか認識しないので)。ほとんどの場合、Visual Studio Code のワークスペースのディレクトリで良いのですが、モノリポでは各プロジェクトのディレクトリで ESLint を実行したい場合があります。そのようなケースでは
eslint.workingDirectories設定を使います。いくつかのパターンがあります。
settings.json{ "eslint.workingDirectories": { "mode": "auto" } }↑ ワークスペース内の
package.jsonや.eslintignore等のファイルを手がかりに CWD となるディレクトリを自動的に決定します。settings.json{ "eslint.workingDirectories": { "pattern": "./packages/*/" } }↑ Glob パターンで CWD となるディレクトリを指定します。指定したディレクトリ内のファイルをリントする時に使われます。
settings.json{ "eslint.workingDirectories": ["./packages/alice", "./packages/bob"] }↑ CWD となるディレクトリの配列を指定します。指定したディレクトリ内のファイルをリントする時に使われます。
- 投稿日:2019-12-19T12:17:04+09:00
尊いツイートに、何万回でも押せる「尊いボタン」をつけた
この記事は、 Ateam Brides Inc. Advent Calendar 2019 19日目の記事です。
本日は新卒2年目、デザイナの @pyonsomi が担当いたします。尊いということ
こ、これは尊い...?????!!!
↑Twitterで、好みのイラスト・グッとくる漫画・推してるアイドルの写真・猫などのツイートに出会った時の私です。
(尊いとは)私はそんな時、高ぶる思いを発散することができず...
たった1回のいいねやリツイートじゃ足りないんだ!!!
と画面の前で悶えること幾千の時を乗り越えて参りました。
だがしかし!今ここで!この気持ちをぶつけたい!
そこで今回は、chromeの拡張機能を使って、PC版Twitter.comで使える
尊いツイートに気持ちが高ぶった時、何万回でも押せる「尊いボタン」
をjQueryやJsの学習を踏まえて作ってみました。準備物
- パソコン
- テキストエディタ
があれば大丈夫です。
完成
うおおおおおおお〜〜〜〜〜〜〜!!!!!!!!
この気持ちを表現できる日が来るとは。。。。
これいつかツイッターに実装されないかな。。。
一生押せる気がする。。。今回かなりつまずきましたが、先輩方に手助けいただき完成できました?
お世話になったみなさん、本当にありがとうございました?♂️以下実装内容です。
プロセス
- chrome拡張機能を設定
- DOMの読み込み方法を設定
- 尊いボタンを追加
- 尊いボタンを隣にカウンターを追加
コード全て
jquery-3.4.1.min.js//公式からダウンロードmanifest.json{ "name": "ThisIsToutoi", "author": "pyonsomi", "description": "chrome extension for customize CSS and JS.", "version": "1.0.0", "manifest_version": 2, "web_accessible_resources": ["*"], "permissions": ["storage"], "content_scripts": [ { "matches": ["https://twitter.com/*"], "css": ["toutoi.css"],"js": ["jquery-3.4.1.min.js","toutoi.js"] } ] }toutoi.jsjQuery(function(){ var setIntervalId, toutoiCounter = function(){ var parentElem = $("[aria-label='いいね']").parent(); if (parentElem.length > 0) { parentElem.after('<div id="toutoi">?<span id="t_counter"></span></div>'); var countUp = 0, toutoiElm = $('#toutoi'); function countNumber(){ countUp++; return countUp; } toutoiElm.on('click',function(){ toutoiElm.find('span#t_counter').html(countNumber()); }); clearInterval(setIntervalId); delete setIntervalId; } }; setIntervalId = setInterval(toutoiCounter,100); });toutoi.css#toutoi{ padding: 0.85rem; -webkit-user-select: none; user-select: none; } #t_counter{ color: rgb(136, 153, 166); }1 chromeの拡張機能を設定
何番煎じ感ありますがまずはchrome拡張機能の設定から。
その1:デスクトップにフォルダを作成。
今回読み込むファイルたちを格納するフォルダです。
名前は「ThisIsToutoi」とします。その2:manifest.jsonを作成
ThisIsToutoiファイルの中に、manifest.jsonを作成し、下記コードを書きます。
nameには拡張機能の名前、authorには制作者の名前を入れます。manifest.js{ "name": "ThisIsToutoi", "author": "pyonsomi", "description": "chrome extension for customize CSS and JS.", "version": "1.0.0", "manifest_version": 2, "web_accessible_resources": ["*"], "permissions": ["storage"], "content_scripts": [ { "matches": ["https://hoge.com/*"], "css": ["style.css"],"js": ["script.js"] } ] }その3:対象サイトを指定
'content_scripts'でTwitterのURLを指定。
またURLのおしりには * をつけました。
ツイッター全てのページで読み込まれるようにするためです。。manifest.js{ "content_scripts": [ { "matches": ["https://www.twitter.com/*”], "css": [“style.css"],"js": ["script.js"] } ] }その4:関係するファイルを読み込ませる
cssファイルとjsファイルを作成+jQueryをダウンロード。
全てThisIsToutoiフォルダへ追加し、'manifest.js'で指定。manifest.js{ "content_scripts": [ { "matches": ["https://twitter.com/*"], "css": ["toutoi.css"],"js": ["jquery-3.4.1.min.js","toutoi.js"] } ] }その4:Chromeに読み込ませる
Chrome の拡張機能ページ ( chrome://extensions/ )にて
ThisIsToutoiファイルを読み込ませます。
デベロッパーモードをon >「パッケージ化されていない拡張機能を読み込む」からフォルダを選択読み込まれました。これで完了です。
ここから気をつけることは以下です。
ファイルを変更した場合、更新はリアルタイムで反映されないため、拡張機能ページで再読込する必要があります。
何か変更を加えた場合は、拡張機能のrefreshアイコンをクリックし再読み込みのうえ、対象ページの再読み込みも行いましょう。ここまででchromeの設定は完了です。
ここからtoutoi.jsに処理を書きます。2 DOMの読み込み方法を設定
jQueryより先にDOM(HTML)を読み込むための処理をします。
どんな通信環境でも適応できるように
- 要素が0のとき(DOMを読み込めていないとき)、0.1秒ごとに再読み込みする
- 要素を読み込めたら、終了させる。
toutoi.jsjQuery(function(){ //DOM読み込みを設定 var setIntervalId, toutoiCounter = function(){ //いいねボタンの親要素を定義 //親要素がひとつでもみつかった場合 = DOMが読み込めた場合 if (parentElem.length > 0) { //尊いボタンを追加 //カウンタの初期値を設定 //カウントアップする関数 countUp を定義 //クリックイベント //DOMが読み込めたので、チェック終了 clearInterval(setIntervalId); delete setIntervalId; } }; //DOMがすべて読み込み完了したことを0.1秒ごとにチェックする setIntervalId = setInterval(toutoiCounter,100); });3 尊いボタンを追加
今回使ったのは「?」です。
いいねの隣に入れたいので、絶妙なところにいますがまず[aria-label='いいね']をチョイス。そして
[aria-label='いいね']の親の直後に?を入れます。toutoi.jsjQuery(function(){ //DOM読み込みを設定 var setIntervalId, toutoiCounter = function(){ //いいねボタンの親要素を定義 var parentElem = $("[aria-label='いいね']").parent(); //親要素がひとつでもみつかった場合 = DOMが読み込めた場合 if (parentElem.length > 0) { //尊いボタンを追加 parentElem.after('<div id="toutoi">?<span id="t_counter"></span></div>'); //カウンタの初期値を設定 //カウントアップする関数 countUp を定義 //クリックイベント //DOMが読み込めたので、チェック終了 clearInterval(setIntervalId); delete setIntervalId; } }; //DOMがすべて読み込み完了したことを0.1秒ごとにチェックする setIntervalId = setInterval(toutoiCounter,100); });4 尊いボタンの隣にカウンターを追加
最後に、カウンターの処理を書いて完成です。
toutoi.jsjQuery(function(){ //DOM読み込みを設定 var setIntervalId, toutoiCounter = function(){ //いいねボタンの親要素を定義 var parentElem = $("[aria-label='いいね']").parent(); //親要素がひとつでもみつかった場合 = DOMが読み込めた場合 if (parentElem.length > 0) { //尊いボタンを追加 parentElem.after('<div id="toutoi">?<span id="t_counter"></span></div>'); //カウンタの初期値を設定 var countUp = 0, toutoiElm = $('#toutoi'); //カウントアップする関数 countUp の定義 function countNumber(){ //カウンタに 1 を加算 countUp++; //返り値を設定 return countUp; } //クリックイベント toutoiElm.on('click',function(){ toutoiElm.find('span#t_counter').html(countNumber()); }); //DOMが読み込めたので、チェック終了 clearInterval(setIntervalId); delete setIntervalId; } }; //DOMがすべて読み込み完了したことを0.1秒ごとにチェックする setIntervalId = setInterval(toutoiCounter,100); });ここで驚愕の事実
今回使っていたこの絵文字、てっきり顔の前で拝む仕草だと思っていたら
?「Folded Hands(組まれた手)」
2つの手が固く合わさった絵文字で、日本文化における「お願い」や「ありがとう」を意味している。 また、「祈る人」・「お祈りしている手」という意味や「ハイタッチ」の意味でもよく使われている。とのこと。まさかのハイタッチらしいです。
ま じ か
さいごに
最後まで読んでくだった方、ありがとうございました!
今回のTwitter拡張機能、いろいろ遊べそうだと思ったので
「私はこんなの作ったで!!」という方がいたらぜひ教えてください?カウントのデータをちゃんと保持したりなどのクオリティを高めるもありますし、はたまた「クソリプにたくさん?を投げるボタン」にするとか「ボタンを推したらサイリウムを持った自分が出てくる」とかも考えてました(笑)反響があったらブラッシュアップしたい。
多彩な人たちが活躍する私達のチームで働きませんか?
エイチームは、インターネットを使った多様な技術を駆使し、幅広いビジネスの領域に挑戦し続ける名古屋の総合IT企業です。
そのグループ会社である株式会社エイチームブライズでは、一緒に働く仲間を募集しています!上記求人をご覧いただき、少しでも興味を持っていただけた方は、まずは気軽にお話しましょう!
技術的な話だけでなく、私たちが大切にしていることや、一緒にやっていくお仕事についてなど、詳しくお伝えいたします。Qiita Jobsよりメッセージお待ちしております!
- 投稿日:2019-12-19T12:17:04+09:00
尊いツイートに、何万回でも押せる「尊いボタン」をつける
この記事は、 Ateam Brides Inc. Advent Calendar 2019 19日目の記事です。
本日は新卒2年目、デザイナの @pyonsomi が担当いたします。尊いということ
こ、これは尊い...?????!!!
↑Twitterで、好みのイラスト・グッとくる漫画・推してるアイドルの写真・猫などのツイートに出会った時の私です。
(尊いとは)私はそんな時、高ぶる思いを発散することができず...
たった1回のいいねやリツイートじゃ足りないんだ!!!
と画面の前で悶えること幾千の時を乗り越えて参りました。
だがしかし!今ここで!この気持ちをぶつけたい!
そこで今回は、chromeの拡張機能を使って、PC版Twitter.comで使える
尊いツイートに気持ちが高ぶった時、何万回でも押せる「尊いボタン」
をjQueryやJsの学習を踏まえて作ってみました。準備物
- パソコン
- テキストエディタ
があれば大丈夫です。
完成
うおおおおおおお〜〜〜〜〜〜〜!!!!!!!!
この気持ちを表現できる日が来るとは。。。。
これいつかツイッターに実装されないかな。。。
一生押せる気がする。。。今回かなりつまずきましたが、先輩方に手助けいただき完成できました?
お世話になったみなさん、本当にありがとうございました?♂️以下実装内容です。
プロセス
- chrome拡張機能を設定
- DOMの読み込み方法を設定
- 尊いボタンを追加
- 尊いボタンを隣にカウンターを追加
コード全て
jquery-3.4.1.min.js//公式からダウンロードmanifest.json{ "name": "ThisIsToutoi", "author": "pyonsomi", "description": "chrome extension for customize CSS and JS.", "version": "1.0.0", "manifest_version": 2, "web_accessible_resources": ["*"], "permissions": ["storage"], "content_scripts": [ { "matches": ["https://twitter.com/*"], "css": ["toutoi.css"],"js": ["jquery-3.4.1.min.js","toutoi.js"] } ] }toutoi.jsjQuery(function(){ var setIntervalId, toutoiCounter = function(){ var parentElem = $("[aria-label='いいね']").parent(); if (parentElem.length > 0) { parentElem.after('<div id="toutoi">?<span id="t_counter"></span></div>'); var countUp = 0, toutoiElm = $('#toutoi'); function countNumber(){ countUp++; return countUp; } toutoiElm.on('click',function(){ toutoiElm.find('span#t_counter').html(countNumber()); }); clearInterval(setIntervalId); delete setIntervalId; } }; setIntervalId = setInterval(toutoiCounter,100); });toutoi.css#toutoi{ padding: 0.85rem; -webkit-user-select: none; user-select: none; } #t_counter{ color: rgb(136, 153, 166); }1 chromeの拡張機能を設定
何番煎じ感ありますがまずはchrome拡張機能の設定から。
その1:デスクトップにフォルダを作成。
今回読み込むファイルたちを格納するフォルダです。
名前は「ThisIsToutoi」とします。その2:manifest.jsonを作成
ThisIsToutoiファイルの中に、manifest.jsonを作成し、下記コードを書きます。
nameには拡張機能の名前、authorには制作者の名前を入れます。manifest.js{ "name": "ThisIsToutoi", "author": "pyonsomi", "description": "chrome extension for customize CSS and JS.", "version": "1.0.0", "manifest_version": 2, "web_accessible_resources": ["*"], "permissions": ["storage"], "content_scripts": [ { "matches": ["https://hoge.com/*"], "css": ["style.css"],"js": ["script.js"] } ] }その3:対象サイトを指定
'content_scripts'でTwitterのURLを指定。
またURLのおしりには * をつけました。
ツイッター全てのページで読み込まれるようにするためです。。manifest.js{ "content_scripts": [ { "matches": ["https://www.twitter.com/*”], "css": [“style.css"],"js": ["script.js"] } ] }その4:関係するファイルを読み込ませる
cssファイルとjsファイルを作成+jQueryをダウンロード。
全てThisIsToutoiフォルダへ追加し、'manifest.js'で指定。manifest.js{ "content_scripts": [ { "matches": ["https://twitter.com/*"], "css": ["toutoi.css"],"js": ["jquery-3.4.1.min.js","toutoi.js"] } ] }その4:Chromeに読み込ませる
Chrome の拡張機能ページ ( chrome://extensions/ )にて
ThisIsToutoiファイルを読み込ませます。
デベロッパーモードをon >「パッケージ化されていない拡張機能を読み込む」からフォルダを選択読み込まれました。これで完了です。
ここから気をつけることは以下です。
ファイルを変更した場合、更新はリアルタイムで反映されないため、拡張機能ページで再読込する必要があります。
何か変更を加えた場合は、拡張機能のrefreshアイコンをクリックし再読み込みのうえ、対象ページの再読み込みも行いましょう。ここまででchromeの設定は完了です。
ここからtoutoi.jsに処理を書きます。2 DOMの読み込み方法を設定
jQueryより先にDOM(HTML)を読み込むための処理をします。
どんな通信環境でも適応できるように
- 要素が0のとき(DOMを読み込めていないとき)、0.1秒ごとに再読み込みする
- 要素を読み込めたら、終了させる。
toutoi.jsjQuery(function(){ //DOM読み込みを設定 var setIntervalId, toutoiCounter = function(){ //いいねボタンの親要素を定義 //親要素がひとつでもみつかった場合 = DOMが読み込めた場合 if (parentElem.length > 0) { //尊いボタンを追加 //カウンタの初期値を設定 //カウントアップする関数 countUp を定義 //クリックイベント //DOMが読み込めたので、チェック終了 clearInterval(setIntervalId); delete setIntervalId; } }; //DOMがすべて読み込み完了したことを0.1秒ごとにチェックする setIntervalId = setInterval(toutoiCounter,100); });3 尊いボタンを追加
今回使ったのは「?」です。
いいねの隣に入れたいので、絶妙なところにいますがまず[aria-label='いいね']をチョイス。そして
[aria-label='いいね']の親の直後に?を入れます。toutoi.jsjQuery(function(){ //DOM読み込みを設定 var setIntervalId, toutoiCounter = function(){ //いいねボタンの親要素を定義 var parentElem = $("[aria-label='いいね']").parent(); //親要素がひとつでもみつかった場合 = DOMが読み込めた場合 if (parentElem.length > 0) { //尊いボタンを追加 parentElem.after('<div id="toutoi">?<span id="t_counter"></span></div>'); //カウンタの初期値を設定 //カウントアップする関数 countUp を定義 //クリックイベント //DOMが読み込めたので、チェック終了 clearInterval(setIntervalId); delete setIntervalId; } }; //DOMがすべて読み込み完了したことを0.1秒ごとにチェックする setIntervalId = setInterval(toutoiCounter,100); });4 尊いボタンの隣にカウンターを追加
最後に、カウンターの処理を書いて完成です。
toutoi.jsjQuery(function(){ //DOM読み込みを設定 var setIntervalId, toutoiCounter = function(){ //いいねボタンの親要素を定義 var parentElem = $("[aria-label='いいね']").parent(); //親要素がひとつでもみつかった場合 = DOMが読み込めた場合 if (parentElem.length > 0) { //尊いボタンを追加 parentElem.after('<div id="toutoi">?<span id="t_counter"></span></div>'); //カウンタの初期値を設定 var countUp = 0, toutoiElm = $('#toutoi'); //カウントアップする関数 countUp の定義 function countNumber(){ //カウンタに 1 を加算 countUp++; //返り値を設定 return countUp; } //クリックイベント toutoiElm.on('click',function(){ toutoiElm.find('span#t_counter').html(countNumber()); }); //DOMが読み込めたので、チェック終了 clearInterval(setIntervalId); delete setIntervalId; } }; //DOMがすべて読み込み完了したことを0.1秒ごとにチェックする setIntervalId = setInterval(toutoiCounter,100); });ここで驚愕の事実
今回使っていたこの絵文字、てっきり顔の前で拝む仕草だと思っていたら
?「Folded Hands(組まれた手)」
2つの手が固く合わさった絵文字で、日本文化における「お願い」や「ありがとう」を意味している。 また、「祈る人」・「お祈りしている手」という意味や「ハイタッチ」の意味でもよく使われている。とのこと。まさかのハイタッチらしいです。
ま じ か
さいごに
最後まで読んでくだった方、ありがとうございました!
今回のTwitter拡張機能、いろいろ遊べそうだと思ったので
「私はこんなの作ったで!!」という方がいたらぜひ教えてください?カウントのデータをちゃんと保持したりなどのクオリティを高めるもありますし、はたまた「クソリプにたくさん?を投げるボタン」にするとか「ボタンを推したらサイリウムを持った自分が出てくる」とかも考えてました(笑)反響があったらブラッシュアップしたい。
多彩な人たちが活躍する私達のチームで働きませんか?
エイチームは、インターネットを使った多様な技術を駆使し、幅広いビジネスの領域に挑戦し続ける名古屋の総合IT企業です。
そのグループ会社である株式会社エイチームブライズでは、一緒に働く仲間を募集しています!上記求人をご覧いただき、少しでも興味を持っていただけた方は、まずは気軽にお話しましょう!
技術的な話だけでなく、私たちが大切にしていることや、一緒にやっていくお仕事についてなど、詳しくお伝えいたします。Qiita Jobsよりメッセージお待ちしております!
- 投稿日:2019-12-19T12:11:50+09:00
JavaScriptのCallback関数って...何?
はじめに
以前はPHPでシステム開発を行っていたので、その感覚でJavaScriptを触っていると
なかなか慣れることが出来ないそんな中、とあるデザインフレームワークのエラーハンドリングの解説に、
こんな感じ(下記)でAPIを使用してねと書いてありました。rules: [{validator: function(value, callback)}]これを見たとき、callback、、、んっ?となり、少し調べてロジック組んでみたけど動かない、、、。
どうやって使ったらよいの?とハマってしまった経験があるので、今回はコールバック関数について
記載しようと思います!(結果的にはコールバックでない箇所にハマっていたのですけど...)コールバック関数とは?
ウィキペディアによると、
コールバック(英: Callback)とは、プログラミングにおいて、
他のコードの引数として渡されるサブルーチンである。他のコードのひきすうとしてワタサレるサブルーチン......ほほぅ..何?
もう1回ちゃんと調べてみたところ、コールバック関数とは、
「他の関数に渡して実行してもらうための関数」のことをコールバック関数とよびます。例えば、
「ある関数Aに、この関数Bを渡しておくから、Aの処理が終わったら渡しておいた関数Bを実行してね」
というように使います。この関数Bがコールバック関数でした。// コールバック関数B const weather = () => { console.log("晴れ"); } // コールバック関数を渡される関数A const today = (callback) => { console.log('今日の天気は'); callback(); } today(weather); // コンソールに「今日の天気は」「晴れ」と表示されるこう考えると、普段使っている「arr.map(callback)」や「arr.findIndex(callback)」
「arr.filter(callback)」など、callback関数を使っているのが分かりますね。では、なぜ私がハマったのかというと...。
そもそもJavaScriptの関数って...
JavaScriptをよく分かっていないがためにハマったのは実はココでして、、。
「JavaScriptの関数は、関数であり、変数に代入できるオブジェクトでもある」
ということをちゃんと理解していませんでした。例えば下記のような関数の使い方であれば、PHPをやっていたのですんなり受け入れることが
出来ましたが、function hoge() { console.log("hoge"); } hoge(); // コンソールに「hoge」と表示されるJavaScriptでは関数はオブジェクトでもあったので、他の変数に入れることも出来ます。
(この感覚がなかった...)function hoge() { console.log("hoge"); } const hogehoge = hoge; hogehoge(); // コンソールに「hoge」と表示される結果はどちらも同じで「hoge」と表示されますが、ここで重要だったのが、
- 関数として実行する際は呼び出し側に「()」を付ける
- オブジェクトとして渡したりする際は「()」を付けない
ということでした。
私がハマったところ
私がハマったのはもうおわかりかもしれませんが、callback関数として値を渡さなければいけないところに、
下記のように()を付けてしまい、関数として実行していたため動かなかったというオチでした。。// 使い方 // rules: [{validator: function(value, callback)}] // 下記の定義をした際に、 rules: [{validator: checkValidate()}] // ←この()が原因!! // エラーチェック関数 const checkValidate = (value, callback) => { if (value === 'xxxx') { callback('エラーですよ!'); } else { callback(); } }おわりに
JavaScript...慣れてしまえばフロントエンドを自在に操作できるようになるので、
とっても便利な言語ですね。早く覚えていかなくては。この記事が何かのお役に立てれば幸いです。
- 投稿日:2019-12-19T12:08:59+09:00
もうすぐ2020年だけどMeteorでサーバーサイドコードのデバッグがしたい
はじめに
JavaScriptベースのフルスタックフレームワークであるMeteorを使ったアプリケーションの保守開発をしています。
仕事の都合上、Meteorでいい感じにサーバーサイドコードのデバッグがしたくて調べていたのですが、Meteorの日本語資料が少ない&Meteor 1.6以降で仕様が違う&Meteor 1.6以降は日本語じゃなくても資料が少ないため結構苦戦したので、備忘録として残しておきます。Meteorのデバッグ方法
Meteorのサーバーサイドデバッグ機能は、内部的にはNode.jsのデバッグ用APIを起動して利用しており、その仕様も基本的には同じです。
よって、Meteor v1.6以降ではプロセスの起動時に--inspectあるいは--inspect-brkを付ける方法が主流だと思います。
注意するべき点として、Node.jsのデバッグ用APIはV8より前ではDebugger API(node debug)、V8以降ではInspector API(node inspect)と異なることが挙げられます。
内部的にNode.jsを動かしているMeteorでも当然この影響を受けており、V1.6以降以前とそれ以降ではコマンドが異なります。むかし(例)
meteor debugいま(例)
meteor run --inspect既に
meteor debugコマンドは非推奨となっているため、よほどの事情(バージョンが足りないとか)がない限りはinspectコマンドを使ってデバッグを行うべきです。v1.6以降のMeteorのサーバーサイドデバッグですが、Meteor公式ドキュメントや、Node.jsの公式ドキュメント,あるいはNode.jsのサーバーサイドでバッグの記事なども参考にすると良いと思います。 日本語の参考資料は多分ありません。
公式ドキュメントを見てみると、meteorの新しいデバッグコマンドは基本的にはNodeと同じ仕様のようですが、以下の2点で異なるようです。
- フラグの影響範囲がビルドプロセスそのものではなく、ビルドプロセスによって生成されるサーバープロセスに影響する。
- --inspect-brkフラグを使用すると、サーバーコードが読み込まれて実行が開始される前にサーバープロセスが一旦止まり、開発者がいい感じにブレークポイントを指定できる。
v1.6以降のMeteorのデバッガに接続するデバッグクライアントとしては、Chrome DevToolsか、VSCodeやWebStormなどのエディタ・IDEのいずれかが適切だと思います。
個人的にChrome DevToolsを使ってクライアントサイドコードのデバッグと同じようにデバッグができるのが結構好きなので、この記事ではChrome DevToolsでのデバッグ方法を記述していきます。(WebStormやVSCについては元気があったら追記したり別の記事を書いたりします。)
ちなみに、CLIでデバッグすることも可能なのですが、GUIでデバッグできるのにわざわざCUIでデバッグする理由がなんかかっこいいから以外に思い当たらないので、ここでは取り扱いません。でもなんかかっこいいですよね。Chrome DevToolsでデバッグしてみる
ではやってみましょう。
まず、デバッグするMeteorプロジェクトを作りましょう。公式のチュートリアルをなんとなくやってみるとなんとなくできるかと思います。完成したら、ターミナルを開いて該当のディレクトリに移動しましょう。
ターミナルcd /simple-Todos(例)移動したら、inspectフラグをつけてmeteorアプリを起動します。
ここでは、いい感じにGUIでブレークポイントを設定したいので、inspect-brkフラグを使用します。ターミナルmeteor run --inspect-brkすると、何やらうにょうにょ動き始めた後、こんな感じになるかと思います。
こんな感じ[[[[[ ~/simple-todos ]]]]] => Started proxy. => Started MongoDB. W20191219-01:13:49.236(9)? (STDERR) Debugger listening on ws://127.0.0.1:9229/3044cde4-e74e-469a-9081-49af1033a4e3 W20191219-01:13:49.294(9)? (STDERR) For help see https://nodejs.org/en/docs/inspectorこの表示を見ると、meteorのデバッグ機能はnodeのAPIを内部的に使っていることがよく分かりますね。
では、次にクライアントデバッガをアタッチしましょう。
お手持ちのGoogle Chromeを開き、chrome://inspectと入力してみましょう。
こんな感じのやつが出てくるかと思います。
画面上に光り輝いている(?)、inspectを押すとデバッガにアタッチされ、ターミナルにDebugger attached.といういかにもアタッチされたっぽい文言が出現し、クライアントデバッガも現れます。
--inspect-brkフラグをつけてmeteorアプリケーションを起動すると、debug.jsで一旦止まってくれます。コメントに書いてあるように、ここで好きなようにブレークポイントを設定できるので便利です。
ここでは、一旦TODOを新しく作成する部分にブレークポイントを設定してみます。
確認したい箇所にブレークポイントを設定したら、一時停止しているプロセスを再開させましょう。デバッガの右上にある|▶ボタンを押すと、プロセスが再開されます。
すると、ターミナルに以下のようなものが表示されます。ターミナル=> Started your app. => App running at: http://localhost:3000/おとなしくlocalhost:3000にアクセスしてみましょう。するとこんな感じの画面が表示されます。
TODOリストに何もないのもアレなので、何かTODOを書いてみましょう。
ちょうど直属の先輩も欲しがっていましたし、イカを買ってみることにします。
これを送信してみると、デバッガが出現します。
先程TODOの新規作成メソッドにブレークポイントをおいたためです。
右側に変数の値などが出ているかと思います。
確認が終わったら|▶を押して次のブレークポイントまで飛んだり、その近くのボタンを押してステップインしたりステップアウトしたりステップオーバーしたりしましょう。いい感じにデバッグができるはずです。以上が今回の内容です。
2019年はMeteorが買収されたり色々ありました。これまでMeteor JSはAppolo GraphQLへのリソース集中のため3年間 Ben Newman氏1人で開発をしている状況でしたが、買収後は大幅に開発者が増えているようなのでMeteorユーザーが2021年には3億人になっているかもしれないと密かに思っています。
みなさんMeteorをやりましょう。おわり
補足:いろいろな事情があってできない人に向けた代替手段
世界はとても広いため、上記のデバッグ手順が使えないケースというのが往々にしてあります。(
バージョンが足りないとか)
というか私が仕事で関わっているプロジェクトがもろにそのケースなんですが、そういった場合にサーバー側のコードを確認したいんじゃ!という気持ちになってしまった人に向けていくつか方法を記載しておきます。1. console.logで頑張って出す
一番単純ですが、これが使えないという状況は殆どないでしょう。(機能しないという状況は多い気がしますが)
まあでも、もっとイケてる感じでやりたいですよね。デバッグコードとか消し忘れちゃうし。2. Meteor shellを使う
Meteor 1.0.2で実装された機能で、各種関数を実行したり、他所のAPIを叩いてみたりできる対話型実行環境です。
使い方も簡単で、起動しているMeteorプロジェクトのディレクトリ内で以下のコマンドを実行するだけです。ターミナルmeteor shell成功すると以下のような画面が現れます
ターミナルWelcome to the server-side interactive shell! Tab completion is enabled for global variables. Type .reload to restart the server and the shell. Type .exit to disconnect from the server and leave the shell. Type .help for additional help.作ったメソッドのテストやAPI呼び出しくらいならこれで十分だと言えなくもないですが、meteor shell単体では、特定のユーザーをシミュレートする(特定のユーザーとしてログインしていることにする)ことができません。(デバッグコードとして
this.setUserId(ほにゃほにゃ)を埋め込むなどをすれば可能ですが、手間が(1)とあまり変わりません)
Meteorは割とStatefulなフレームワークなので、メソッド呼び出し時に引数としてuserIdを渡すことよりもサーバーサイドコードの先頭でMeteor.userId()を使って認可をすることが多く、そういったケースにおいてmeteor shellは無力な(あるいは非常に面倒くさい)場合が多いと感じています。3.meteor debug を使う
上で「非推奨だからあんまりやるな」と書いておいてアレですが、v1.5以前だとconsole.logを出す以外の方法である程度実用に耐えるレベルのサーバーサイドデバッグをするにはこれを使うしかないと思います。
ただ、この場合chromeのDevToolsを使いたいときはnode-inspectorが必要だと思われます。
また、私が試した限りは先のmeteor v1.6以降のサーバーサイドデバッグと比べて著しくパフォーマンスが低下するように感じています。(公式でも同じような言及があります)
私が普段仕事で関わっているプロジェクトで試した際は1時間以上CPU使用率が100%を超えており、実用に耐えないと判断した経緯があります。が、やりようはあるんじゃないかという気はしています。
- 投稿日:2019-12-19T11:08:32+09:00
ChromeのデベロッパーツールのコンソールでJavaScriptのライブラリをちょこっと使いたいとき
はじめに
CDNがあるライブラリ限定となります。
環境作るのめんどくさいときに役立つかと思います。例
試しにmoment.jsをコンソールで使えるようにします。
const script = document.createElement('script') // scriptタグを作る script.src = 'https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.17.0/moment-with-locales.js' // srcにCDNのURLを入れる document.getElementsByTagName('head')[0].appendChild(script) // headタグにscriptタグを入れる const now = moment() now.toDate() // Thu Dec 19 2019 11:00:54 GMT+0900 (日本標準時)














































































