20210915のJavaScriptに関する記事は14件です。

スーパーキラキラカラフルクッキリディスプレイも最初と最後の文字さえあっていれば読める説

みなさんはタイポグリセミア(Typoglycemia)というものをご存知でしょうか? wikiからの引用ですが 文章中のいくつかの単語で最初と最後の文字以外の順番が入れ替わっても正しく読めてしまう現象である 今まさに新作のiPhoneが発表され、「スーパーキラキラカラフルクッキリディスプレイ」という謎の単語がtwitter上で話題になりました。 それをみて「最初と最後の文字以外の順番が入れ替わっても正しく読めてしまうんじゃね?」と思い、検証プログラムを書いてみました。 とにかくすぐに確認したかったのでjsで書きました (str => str.slice(0, 1) + str.slice(1, -1).split('').sort(() => Math.random() - 0.5).join('') + str.slice(-1))('スーパーキラキラカラフルクッキリディスプレイ') 結果は、、、 スキークスデカキーキッィラプリラフパラレルイ ? 読めませんでした よく考えたら「文章中のいくつかの単語で...」なのでそもそも文章じゃないとダメでした?
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

初学者がReactの開発環境を整える

はじめに JavaScriptでWebアプリ開発する際のフレームワークReactを勉強し始めたので忘れないように記録する。 今回はReactでWeb上でHello World!と表示させる手順ついて記録する。 間違っているところがありましたらご指摘していただけると幸いです。 事前準備 React公式サイト Chromeの拡張機能 Chromeでreact developer toolsと調べるか下記のURLから拡張機能を追加しておく。 React Developer Tools さらにReact Developer Toolsの拡張機能の設定でファイルの URL へのアクセスを許可するをONにする必要がある。 開発 React読み込み 公式サイトからDoc→Getting Started→Web上で試せるオンラインエディタのこのHTMLファイルを開きheadタグ内のScriptタグ2つを自身のhtmlファイルのhead内へコピペする。 公式サイトからDoc→既存のウェブサイトにReacrを追加する→JSXを手軽に試してみるのScriptタグを自身のhtmlファイルのhead内へコピペする。 または、下記の3つのコードをコピペする。 react_load <script src="https://unpkg.com/react@17/umd/react.development.js"></script> <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script> 下記のScriptタグは本番環境では使用しないでください。 react_load <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script> 1番目がReact本体のScriptタグ、2番目がReactの結果をブラウザのDOMに反映させるためのScriptタグ、3番目がJSXなどの やJavaScriptの新しい文法を使うためのBAbelというライブラリのScriptタグである。 自分のScriptコードを書くための準備 scriptタグはReactの読み込みで出てきたBabelを使って変換していくので、type属性を付ける。 ローカルファイルでの開発は自身のscriptを別ファイルにするとエラーになる my_script <script type="text/babel"></script> Hello World!と表示させる 現在のhtmlファイル内のコード react_practice <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>React Practice</title> <link rel="stylesheet" href="css/styles.css"> <!-- react読み込み --> <script src="https://unpkg.com/react@16/umd/react.development.js"></script> <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script> <script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script> </head> <body> <script type="text/babel"> </script> </body> </html> Reactを使ってUIを表示させる。 まず、UIを表示させるための領域を生成する。 JavaScriptで部品を判別するためにidを付け、idはrootとする。 place_create <body> <!-- UI表示のためのdiv --> <div id="root"></div> <script type="text/babel"> </script> </body> </html> このdivにReactで作った部品を描画させる。 即時関数で囲いたいのでアロー関数式を記述する。 arrow <script type="text/babel"> (() => { })(); </script> ReactDOM.renderとし、第一引数に描画したいUIをJSXという記法で書く。 ui_display <script type="text/babel"> (() => { ReactDOM.render( <h1>Hello World!</h1>, ); })(); </script> 第二引数にはUIをどこに描画するかを指定する。 今回はidがrootの場所に描画する。 ui_display <script type="text/babel"> (() => { ReactDOM.render( <h1>Hello World!</h1>, document.getElementByID('root') ); })(); </script> 現在のhtmlファイル内のコード react_practice <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>React Practice</title> <link rel="stylesheet" href="css/styles.css"> <!-- react読み込み --> <script src="https://unpkg.com/react@16/umd/react.development.js"></script> <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script> <script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script> </head> <body> <div id="root"></div> <script type="text/babel"> (() => { ReactDOM.render( <h1>Hello World!</h1>, document.getElementByID('root') ); })(); </script> </body> </html> ここまでのファイルをWeb上で表示させるとHello World!と表示される。 これで開発環境が整えられた。 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

勉強記録 3日目

勉強三日目 勉強時間 35分 ドットインストール Javascript オブジェクト編 9回目 map map()とは配列と関数を使って新しい配列を作るときに使う。 const 新配列名 = 元配列名.map((変数名を自由に決める) => { return 関数}); { const prices = [180, 190, 200]; //省略前 const updatedPrices = prices.map((price) => { return price + 20;}); //省略後 const updatedPrices = prices.map(price => price + 20); console.log(updatedPrices); } ポイント! いろいろな省略。 ・アロー関数の場合、引数が一つなら変数の周りの()省略可 ・式の場合は{}とreturn省略可 注意。省略しすぎて自分がわからなくなるとよくないため、省略はほどほどに。 ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー 10回目 filter ある配列の一部を選んで新しい配列を作るときに使う。 配列の要素を変数として、filterの関数がtrueのときその要素は残り、falseのときその要素は排除される。 { 'use strict'; { const numbers = [1, 4, 7, 8, 10]; // 省略前 const evenNumbers = numbers.filter(number => { if (number % 2 === 0) { return true; } else { return false; } }); //省略後 const evenNumbers = numbers.filter(number => number % 2 === 0); console.log(evenNumbers); } } 省略は結局『number % 2 === 0』がtrueかfalseかという話なのでifのところを全部なくせる。 (number => number % 2 === 0)これですむ。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

日本の旅客鉄道の変遷をMapbox GL JSで表示してみました

はじめに 国土数値情報の鉄道時系列データを用いて、日本の旅客鉄道の変遷(1950~2020年)をMapbox GL JS(v2)のタイムスライダー機能で表示してみました。 鉄道時系列データは以下の国土数値情報の提供サイトよりgeojsonファイル(令和2年)をダウンロードして使用しています。 https://nlftp.mlit.go.jp/ksj/gml/datalist/KsjTmplt-N05-v1_3.html 背景地図にはMapboxを使用しているため、別途、Mapboxにてアクセストークンの取得及びstyleのURLが必要になります。 ★★★↓高速道路バージョンはこちらの記事を参照願います。★★★ 高速道路整備の変遷をMapbox GL JSで表示してみました アウトプットイメージ MapboxGLJSで変遷シリーズ?の鉄道バージョンを作成。首都圏の旅客鉄道の変遷。データの出典は国土数値情報の鉄道時系列データ。70年くらいのスパンで見ると、1950年の東京の鉄道網は現在と大きく異なることがわかる?鉄道の場合は見せ方の工夫が必要かな(事業者別など)?#Mapbox #旅客鉄道の変遷 pic.twitter.com/YEgmv2C7hw— shi_works? (@syanseto) August 22, 2021 ※「鉄道時系列データ」(国土交通省) (https://nlftp.mlit.go.jp/ksj/gml/datalist/KsjTmplt-N05-v1_3.html) を用いて作成 前提条件 鉄道時系列データは令和2年の「N05-20_RailroadSection2.geojson」を用いています※。 ※ただし、フィルタで使用する、geojsonファイルの属性(設置期間(設置開始)(N05_005b)と設置期間(設置終了)(N05_005e))が文字列のため、あらかじめQGIS等で整数に変換したものを用いています。 下記のコードでMapboxのアクセストークン、styleのURL及びN06-20_HighwaySection.geojsonのパスを入力してください。 なお、フィルタにはN05-20_RailroadSection2.geojsonの属性の「設置期間(設置開始)(N05_005b)」と「設置期間(設置終了)(N05_005e)」を用いています。 また、ラベル(鉄道路線の名称)にはN05-20_RailroadSection2.geojsonの属性の「路線名(N05_002)」を用いています。 html、CSS、JavaScript Railway_transition.html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>日本の鉄道路線の変遷</title> <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no"> <link href="https://api.mapbox.com/mapbox-gl-js/v2.4.1/mapbox-gl.css" rel="stylesheet"> <script src="https://api.mapbox.com/mapbox-gl-js/v2.4.1/mapbox-gl.js"></script> <style> body { margin: 0; padding: 0; } #map { position: absolute; top: 0; bottom: 0; width: 100%; } </style> </head> <body> <style> .map-overlay { font: 18px/20px 'Helvetica Neue', Arial, Helvetica, sans-serif; position: absolute; width: 25%; top: 0; left: 0; padding: 10px; } .map-overlay .map-overlay-inner { background-color: #fff; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); border-radius: 3px; padding: 10px; margin-bottom: 10px; } .map-overlay h2 { line-height: 24px; display: block; margin: 0 0 10px; } .map-overlay .legend .bar { height: 10px; width: 100%; background: linear-gradient(to right, #00008b, #00ffff); } .map-overlay input { background-color: transparent; display: inline-block; width: 100%; position: relative; margin: 0; cursor: ew-resize; } </style> <div id="map"></div> <div class="map-overlay top"> <div class="map-overlay-inner"> <h1>日本の旅客鉄道の変遷</h1> 使用データ:<br> <a href="https://nlftp.mlit.go.jp/ksj/gml/datalist/KsjTmplt-N05-v1_3.html" target="_blank" rel="noopener"> 国土数値情報, 鉄道時系列データ(国土交通省)</a><br><br> <label id="year_label"></label> <input id="slider" type="range" min="1950" max="2020" step="1" value="1950"> </div> <div class="map-overlay-inner"> <div id="legend" class="legend"> <div class="bar"></div> <div align="center">昔(1950年)← → 最近(2020年)</div> </div> </div> </div> <script src="https://d3js.org/d3.v3.min.js" charset="utf-8"></script> <script> mapboxgl.accessToken = 'Mapboxのアクセストークンを入力してください'; const map = new mapboxgl.Map({ container: 'map', style: 'styleのURLを入力してください', // 例)mapbox://styles/mapbox/satellite-v9 center: [139.7527995, 35.685175], zoom: 8 }); // ズーム・回転 map.addControl(new mapboxgl.NavigationControl()); // フルスクリーンモードのオンオフ map.addControl(new mapboxgl.FullscreenControl()); // スケール表示 map.addControl(new mapboxgl.ScaleControl({ maxWidth: 200, unit: 'metric' })); function filterBy(year) { const filters = ['all', ['<=', 'N05_005b_int', year], ['>=', 'N05_005e_int', year]]; map.setFilter('railway-lines', filters); map.setFilter('railway-labels', filters); // スライダー表示用ラベル document.getElementById('year_label').textContent = year.toString() + '年'; } map.on('load', () => { // jsonCallback(highway); d3.json( 'N05-20_RailroadSection2.geojsonのパスを入力してください', // 例)data/N05-20_RailroadSection2.geojson jsonCallback ); }); function jsonCallback(data) { map.addSource('railway', { 'type': 'geojson', 'data': data }); // ライン map.addLayer({ 'id': 'railway-lines', 'type': 'line', 'source': 'railway', 'layout': { 'line-join': 'round', 'line-cap': 'round' }, 'paint': { 'line-color': [ 'interpolate', ['linear'], ['get', 'N05_005b_int'], 1950, '#00008b', 2020, '#00ffff' ], 'line-width': 5, 'line-opacity': 0.7 } }); // ラベル(鉄道路線の名称) map.addLayer({ 'id': 'railway-labels', 'type': 'symbol', 'source': 'railway', 'layout': { 'text-field': ['concat', ['to-string', ['get', 'N05_002']]], 'text-font': ['Open Sans Bold', 'Arial Unicode MS Bold'], 'text-size': 12 }, 'paint': { 'text-color': 'rgba(0,0,0,0.5)' } }); // フィルター実行 filterBy(1950); // 初期表示 document.getElementById('slider').addEventListener('input', (e) => { const year = parseInt(e.target.value, 10); // スライダーで選択した年次を整数化 filterBy(year); }); } </script> </body> </html> 参考文献
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Redux ToolitのRTK Queryについて

RTK Queryおもしろそうなので調べてみたり触ってみたりした。 どちらかというと雰囲気を伝えることを目的とした記事で、勘違いしている可能性もあるかもしれないので(つっこんでほしい) 詳細については公式を読んでください。 対象 Reactを使ってるひと Reactでのデータ取得のキャッシュの扱いとか管理がしんどいなってひと Reduxは分からなくてもOK RTK Query is 何? Redux Toolkitのチームが作ったデータ取得とキャッシングのためのツール。 データを取得するのが楽になる。キャッシュのロジックを手書きする必要がなくなる。 最近のReactコミュニティでは「データの取得とキャッシュ」は「状態管理」を別のものとして管理したいという需要があった。 → RTK Queryは「データの取得とキャッシュ」に特化したツール。(Reduxは「状態管理」に特化したツール) 実際のデータ取得とキャッシングのロジックは、Redux ToolkitのcreateSliceとcreateAsyncThunkのAPIの上に構築されている。 → いい感じに隠蔽されているため、ThunksとかReducerとか書く必要がない。 → ReduxとかRedux Toolkitとか分からなくても使える 簡単にいうと Hooksを使って簡単に非同期データ取得。 キャッシュの実装もいろんなパターンを簡単にできる。 環境構築 create react appでredux-typescriptテンプレートを使えば最初から入っている。 npx create-react-app my-app --template redux-typescript もちろん既存プロジェクトに追加でもOK。 npm i -S @reduxjs/toolkit 使い方 Query Endpointsの定義 createApiを使い、endpointsに、builder.queryメソッドを使用してフィールドを定義する。 たとえば、 ユーザを全取得するgetUsersとユーザを1件取得するgetUserを定義したい場合、以下のように書く。 import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' export interface User { id: string name: string } type Users = User[] export const userApi = createApi({ baseQuery: fetchBaseQuery({ baseUrl: 'http://example.com/api/v2/' }), endpoints: (builder) => ({ getUsers: builder.query<Users, void>({ query: () => `users`, }), getUser: builder.query<User, string>({ query: (userID: string) => `users/${userID}`, }), }), }) // use + endpointsで設定した名前 + QueryでHooksが作られる export const { useGetUsersQuery,useGetUserQuery } = userApi use + endpointsで設定した名前 + QueryでHooksが作られるため、これをExportしておく。 Storeに設定 reducerとmiddlewareに追加する。これでキャッシュをredux内でいい感じに管理してくれるようになる。 import { configureStore } from '@reduxjs/toolkit' import { userApi } from './service/user' export const store = configureStore({ reducer: { [userApi.reducerPath]: userApi.reducer, }, middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(userApi.middleware), }); index.tsxとかでstoreをReduxのProviderに設定 import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; import { store } from './app/store'; import { Provider } from 'react-redux'; ReactDOM.render( <React.StrictMode> <Provider store={store}> <App /> </Provider> </React.StrictMode>, document.getElementById('root') ); Hooksを利用してデータを取得する Hooksを使ってデータ取得を行うことができる。 import React from 'react' import { useGetUserQuery } from './app/service/user' const App: React.FC = () => { const { data, error, isFetching } = useGetUserQuery('A001') return ( <div> {error ? ( <div>エラー</div> ) : isFetching ? ( <div>ロード中</div> ) : data ? <div>{data.name}</div> : <div>データなし</div>} </div> ); } export default App; Hooksの返却値はこんな感じ * data : クエリで取得したデータが入る * error : エラーが発生した場合、エラーの値 * isUninitialized : まだクエリが発行されていない場合、true (後述) * isLoading : クエリが初めて発行されていて、返却されていない場合true * isFetching : クエリが発行されていて、返却されていない場合true * isSuccess : クエリが成功したデータがある場合true * isError : クエリにエラーがある場合true * refetch : 再度クエリを実行するための関数 isLoadingとisFetchingの違いは、isLoadingは初回のクエリ発行のタイミングのみ走るものであるということ。 → isLoadingはページ読み込み時にスケルトンを出すときとかに使うイメージ キャッシュの動作について Hooksを利用したデータ取得ではキャッシュが利用される。 基本のキャッシュ RTK Queryでは基本のキャッシュを APIエンドポイント シリアル化されたクエリパラメータ アクティブなサブスクリプションの参照数 で管理する。 同じエンドポイントで同じクエリパラメータのクエリが別で実行されている場合にはクエリの発行を行わず、キャッシュデータを利用する。 const Component1: React.FC = () => { const { data } = useGetUserQuery('A001') return <div>...</div> } const Component2: React.FC = () => { const { data } = useGetUserQuery('A002') return <div>...</div> } const Component3: React.FC = () => { const { data } = useGetUserQuery('A002') return <div>...</div> } たとえばこんな感じのコンポーネントがあってすべてマウントされた場合、Component2とComponent3は同じエンドポイントで同じクエリパラメータのため、どちらかではキャッシュが利用される。 キャッシュデータはアクティブなサブスクリプションがある限り残り、アクティブなサブスクリプションがすべてなくなって60秒経つと削除される。 上記の例だとComponent2とComponent3がアンマウントされてから60秒経つとキャッシュが削除され、またマウントされるタイミングでクエリの発行が行われる。 なんか分かりづらいので簡単に言うと、同じクエリを別のところで投げている場合、自動的にキャッシュが利用されるということ。 refetch refetchを行った場合、キャッシュを無効にして必ずデータの再取得を行う。 import React from 'react' import { useGetUserQuery } from './app/service/user' const RefetchComponent: React.FC = () => { const { data, refetch } = useGetUserQuery('A001') return ( <div> <div>{data && data.name}</div> <button onClick={() => refetch()} > データを再取得する </button> </div> ); } export default RefetchComponent; refetch後、取得データはキャッシュされ、同じエンドポイントで同じクエリパラメータを利用しているHooksのdataもその値になる。 キャッシュの設定変更 オプションを設定するとキャッシュのタイミングなどを調整することができる。 keepUnusedDataFor アクティブなサブスクリプションがすべてなくなって60秒経つと の60秒の部分を変えられる。numberで秒数指定する。 import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' export interface User { id: string name: string } type Users = User[] export const userApi = createApi({ baseQuery: fetchBaseQuery({ baseUrl: 'http://example.com/api/v2/' }), // APIに対するglobalな設定値。ここに書くとendpoints全てに反映される。 keepUnusedDataFor: 30, endpoints: (builder) => ({ getUsers: builder.query<Users, void>({ query: () => `users`, }), getUser: builder.query<User, string>({ query: (userID: string) => `users/${userID}`, // ここに書くとクエリ毎に設定できる。 keepUnusedDataFor: 5, }), }), }) export const { useGetUsersQuery,useGetUserQuery } = userApi refetchOnMountOrArgChange デフォルトの動作よりも頻繁にデータ再取得したいときに設定する。 booleanかnumberを設定する falseを設定した場合 : デフォルトと同じ。 trueを設定した場合 : クエリに新しいサブスクライバが追加されたときにクエリが常に再実行されるようになる。 numberを設定した場合 : クエリに新しいサブスクライバが追加されたとき、最後にクエリが実行されてから指定した数字の秒数が経過していた場合にクエリが常に再実行される。 import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' export interface User { id: string name: string } type Users = User[] export const userApi = createApi({ baseQuery: fetchBaseQuery({ baseUrl: 'http://example.com/api/v2/' }), // APIに対するglobalな設定値。ここに書くとendpoints全てに反映される。 refetchOnMountOrArgChange: true, endpoints: (builder) => ({ getUsers: builder.query<Users, void>({ query: () => `users`, }), getUser: builder.query<User, string>({ query: (userID: string) => `users/${userID}` }), }), }) export const { useGetUsersQuery,useGetUserQuery } = userApi const Component2: React.FC = () => { // 個別のクエリに設定することもできる const { data } = useGetUserQuery('A002',{ refetchOnMountOrArgChange: true }) return <div>...</div> ちょっとわかりづらいので、trueを設定した場合の挙動について説明する。 const Component2: React.FC = () => { const { data } = useGetUserQuery('A002') return <div>...</div> } const Component3: React.FC = () => { const { data } = useGetUserQuery('A002',{ refetchOnMountOrArgChange: true }) return <div>...</div> } があって、Component2をマウントした後、Component3をマウントすると refetchの時と同様、Component3をマウント時(サブスクライバが追加)にデータ取得が再度行われ、その結果でComponent2のdataも書き換わる。(Component2でまたデータ取得が行われるわけではない。) refetchOnFocus これ結構すごいなって思ったやつ これがtrueになっていると、アプリケーションがフォーカスを取り戻した時にデータ取得する。 別のタブとかから戻ってきたタイミングでデータの再取得が行われる。 import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' export interface User { id: string name: string } type Users = User[] export const userApi = createApi({ baseQuery: fetchBaseQuery({ baseUrl: 'http://example.com/api/v2/' }), // APIに対するglobalな設定値。ここに書くとendpoints全てに反映される。 refetchOnFocus: true, endpoints: (builder) => ({ getUsers: builder.query<Users, void>({ query: () => `users`, }), getUser: builder.query<User, string>({ query: (userID: string) => `users/${userID}` }), }), }) export const { useGetUsersQuery,useGetUserQuery } = userApi これ(と次のrefetchOnReconnect)は別でstoreの設定変更も必要 import { configureStore } from '@reduxjs/toolkit' import { setupListeners } from '@reduxjs/toolkit/dist/query' import { userApi } from './service/user' export const store = configureStore({ reducer: { [userApi.reducerPath]: userApi.reducer, }, middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(userApi.middleware), }); // ここを追加 setupListeners(store.dispatch) また、後述するskipがtrueの場合はデータの再取得が動かないことも注意。(refetchOnReconnectも) refetchOnReconnect ネットワークコネクションが復活したときにデータ取得する。 import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' export interface User { id: string name: string } type Users = User[] export const userApi = createApi({ baseQuery: fetchBaseQuery({ baseUrl: 'http://example.com/api/v2/' }), // APIに対するglobalな設定値。ここに書くとendpoints全てに反映される。 refetchOnReconnect: true, endpoints: (builder) => ({ getUsers: builder.query<Users, void>({ query: () => `users`, }), getUser: builder.query<User, string>({ query: (userID: string) => `users/${userID}` }), }), }) export const { useGetUsersQuery,useGetUserQuery } = userApi storeの設定変更も必要(refetchOnFocusを参照) Mutationによるキャッシュ操作 データの登録更新削除が行われたときにその操作に影響のあるデータを再取得する必要がある。(再取得しないと登録したのにデータないじゃんってなる) RTK Queryでは、cache tagというシステムを使って再データ取得を管理する。 タグを使って、あるMutationが他のエンドポイントからのクエリによって提供されたデータを無効にする意図があるかどうかを判断する。 例えば、 ユーザIDA001を取得するクエリ ユーザIDA002を取得するクエリ ユーザをすべて取得するクエリ があったとして、 ユーザIDA002の情報を更新すると上記の2,3の結果は変わるが、1の結果には影響しない。 つまり、2,3はキャッシュを無効化しデータを取得しなおすが、1のキャッシュは無効化しないようにしたい。 具体的にはクエリにprovidesTagsを設定する。 ユーザIDA001を取得するクエリ → providesTags : [{ type: 'Users', id: 'A001' }], ユーザIDA002を取得するクエリ → providesTags : [{ type: 'Users', id: 'A002' }], ユーザをすべて取得するクエリ → providesTags : [{ type: 'Users', id: 'A001' },{ type: 'Users', id: 'A002' }...] そしてMutationにinvalidatesTagsを設定する。 ユーザIDA002を更新するMutation → invalidateTags : [{ type: 'Users', id: 'A002' }], invalidateTagsを持つMutationが実行されたときに、そのタグの内容と一致するprovidesTagsを持つクエリのキャッシュを無効にし、データの再取得を行わせる。 実装としてはこんな感じ import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' export interface User { id: string name: string } type Users = User[] export const userApi = createApi({ baseQuery: fetchBaseQuery({ baseUrl: 'http://example.com/api/v2/' }), // 利用するタイプをここに書く。リソース毎にタイプを定義するんじゃないだろうか。 tagTypes: ['Users'], endpoints: (builder) => ({ getUsers: builder.query<Users, void>({ query: () => `users`, providesTags: (result) => result ? [ ...result.map(({ id }) => ({ type: 'Users', id } as const)), // addUserがあったときのために特別なタグを用意する。 { type: 'Users', id: 'LIST' }, ] : // エラーがあった場合でもユーザ追加をしたタイミングで再データ取得をする。 [{ type: 'Users', id: 'LIST' }], }), getUser: builder.query<User, string>({ query: (userID: string) => `users/${userID}`, providesTags: (result, error, id) => [{ type: 'Users', id }], }), addUser: builder.mutation<User, Partial<User>>({ query(body) { return { url: `users`, method: 'POST', body, } }, // getUsersのキャッシュを無効化する invalidatesTags: [{ type: 'Users', id: 'LIST' }], }), updateUser: builder.mutation<User, Partial<User>>({ query(data) { const { id, ...body } = data return { url: `users/${id}`, method: 'PUT', body, } }, // 更新したユーザIDを含むクエリのキャッシュのみを無効化する。 invalidatesTags: (result, error, { id }) => [{ type: 'Users', id }], }), deletePost: builder.mutation<{ success: boolean; id: string }, string>({ query(id) { return { url: `users/${id}`, method: 'DELETE', } }, // 削除したユーザIDを含むクエリのキャッシュのみを無効化する。 invalidatesTags: (result, error, id) => [{ type: 'Users', id }], }), }), }) // Mutationはuse + Mutation名 + Mutationという名前のHookができる。 export const { useGetUsersQuery,useGetUserQuery,useAddUserMutation,useUpdateUserMutation,useDeletePostMutation } = userApi 詳しいサンプルは公式のCodeSandboxがわかりやすかった(突然の丸投げ) https://codesandbox.io/s/github/reduxjs/redux-toolkit/tree/master/examples/query/react/mutations Skipを使った条件付きのデータ取得 基本的にはコンポーネントがマウントされたタイミングでクエリは自動的に実行されるが、自動で実行したくない場合もある。 クエリを自動的に実行しないためには、skipパラメータを利用する。 import React from 'react' import { useGetUserQuery } from './app/service/user' const SkipComponent: React.FC = () => { const [skip, setSkip] = React.useState(true) const { data, isUninitialized } = useGetUserQuery('A002', { skip, }) return ( <div> <div>{isUninitialized && 'データ未取得'}</div> <div>{data && data.name}</div> <button onClick={() => setSkip(false)} > データを取得する </button> </div> ); } export default SkipComponent; skipがtrueの場合はクエリが実行されない。 また、この時isUninitializedがtrueになる。 おわりに キャッシュのコントロールをうまくやったりするのにはとてもよさそう。 Reduxのthunkにつらみを覚えている人は手を出すのありかも。 個人的にはRedux Toolkitも最高って感じなので合わせて使っていきたい気持ち。 公式ドキュメントまだ半分くらいしか読んでないのでアレですがちらっと見た感じWebSocketとかとも使えたり自動生成とかもあるみたい。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

非同期いいね

osotmy.rb class Ostomy < ApplicationRecord has_many :favorites, dependent: :destroy def favorited_by?(staff) favorites.where(staff_id: staff.id).exists? end fovorite.rb class Favorite < ApplicationRecord belongs_to :staff belongs_to :ostomy end staff.rb class Staff < ApplicationRecord has_many :favorites end fovorite.contoroller class Staff::FavoritesController < ApplicationController #医療staffが患者さん記録にいいねつける before_action :authenticate_staff! def create @ostomy = Ostomy.find(params[:ostomy_id]) favorite = @ostomy.favorites.new(staff_id: current_staff.id) favorite.save end def destroy @ostomy = Ostomy.find(params[:ostomy_id]) favorite = @ostomy.favorites.find_by(staff_id: current_staff.id) favorite.destroy end private def ostomy_params params.require(:ostomy).permit(:color,:edema,:skin,:h_size,:w_size,:comment,:image) end end /_fovorite.html.erb <% if ostomy.favorited_by?(current_staff) %> <%= link_to staff_ostomy_favorites_path(ostomy), method: :delete,remote: true do%> <%= ostomy.favorites.count%>❤︎よいいね <%end%> <% else %> <%= link_to staff_ostomy_favorites_path(ostomy), method: :post,remote: true do %> <%= ostomy.favorites.count%>♡︎いいね <%end%> <% end %> staff/ostomy/show.html.erb <div class="favorite"><%= render 'staff/favorites/favorite',ostomy: @ostomy %></div> create.js.erbとdestroy.js.erb $(".favorite").html("<%= j(render'staff/favorites/favorite', ostomy: @ostomy) %>");
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React: useRefで参照したinputから同じformのsubmitボタンを辿る

ReactでuseRefで参照したinputから同じformのsubmitボタンを辿って別コンポーネントからsubmitボタンをクリックするメモ ——formのref(例えばformRefという名前)を渡して、formRef.submit()すると、ページ更新されてしまうのでsubmitボタンをクリックすることにした。なにかいい方法がある気はするが、とりあえず回避策—— ComponentBからComponentAのformをSubmitする。但し渡すのはinputのrefとする。つまり、ComponentBからinputに値をセットしてからSubmitしたい。 "form" と Element ID で辿れる。 ComponentA export default function ComponentA(props) { const inputTextRef = useRef() const onSubmit = (e) => { e.preventDefault() ...何かの処理... } return ( <form onSubmit={ onSubmit }> <input type="text" ref={ inputTextRef } /> <button type="submit" id="submitButton">Submit</button> </form> <ComponentB inputTextRef={ inputTextRef } /> ) } ComponentB export default function ComponentB(props) { const submitWithValue = (e) => { e.preventDefault() props.inputTextRef.current.value = "Special Value" props.inputTextRef.current.form.submitButton.click() } return ( <button onClick={ submitWithValue } >Submit ComponentA with a special value</button> ) }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Leaflet.js】住所から緯度経度を取得しその地点にピンを立てる

Leaflet.js はシンプルで拡張性の高いオープンソースの地図描画ライブラリです。 今回は Leaflet 地図に住所検索機能を追加してみたいと思います。 作りたいもの 入力された住所から緯度経度を求める 目的地点に地図の中心を移動 その地点にピンを立てて情報を表示 こんな感じのイメージ ↓ Leaflet 地図を準備 ファイル構成 . ├── index.html └── main.js index.html leaflet.css と leaflet.js の読み込み順序に注意 地図を描画する div 要素に対して width 属性を指定しないと表示されないので注意 index.html <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>住所検索</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/leaflet@1.7.1/dist/leaflet.min.css"> <script src="https://cdn.jsdelivr.net/npm/leaflet@1.7.1/dist/leaflet.min.js"></script> <style> #mymap { height: 500px; width: 500px; } </style> </head> <body> <input type="text" id="address" /> <button id="search">検索</button> <div id="mymap"></div> <script type="module" src="./main.js"></script> </body> </html> main.js main.js const map = L.map("mymap", { center: [35.021028, 135.755583], zoom: 15 }); const baseLayer = L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", { attribution: "&copy; <a href='https://www.openstreetmap.org/copyright'>OpenStreetMap</a> contributors'" }); baseLayer.addTo(map); 住所の入力を受け付け緯度経度を求める geolonia/normalize-japanese-addresses の紹介 住所から緯度経度を求めるのにはオープンソースの住所正規化ライブラリgeolonia/normalize-japanese-addressesを用います。 このライブラリは引数に与えた住所をパース・正規化した後、Geolonia 住所データに基づいてその地域の代表点の緯度経度を返します。 バージョン 2.3.0 から ES Modules 形式に対応されたようで、import 句を使って読み込むことができるようになりました。使用例を下に示します。 使用例 import { normalize } from 'https://cdn.skypack.dev/@geolonia/normalize-japanese-addresses'; normalize("滋賀県大津市御陵町3-1").then(result => { console.log(result); }); 出力 { pref: "滋賀県", city: "大津市", town: "御陵町", addr: "3-1", lat: 35.018665, lng: 135.855868, level: 3 } Leaflet 地図にピンを立てる方法 bindPopupの引数には HTML タグを入れたりすることもできるのですが、詳細は割愛します。公式ドキュメントをご覧ください。 const latitude = 35.018665; // 緯度 const longitude = 135.855868; // 経度 const marker = L.marker([latitude, longitude]); // markerオブジェクトを作成 marker.bindPopup("大津市役所"); // markerをクリックしたとき"大津市役所"と表示されるようにする marker.addTo(map); // markerをLeaflet地図に追加 あと、ピンを立てても表示範囲になければ変化がわかりませんので、ピンを立てると同時に地図の表示範囲もその地点に移動するようにしましょう。 map.flyTo([latitude, longitude]); コードを組み立てる 最初に準備したコードを次のように書き換えます。 index.html 変更なし main.js main.js import { normalize } from 'https://cdn.skypack.dev/@geolonia/normalize-japanese-addresses' const map = L.map("mymap", { center: [35.021028, 135.755583], zoom: 15 }); const baseLayer = L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", { attribution: "&copy; <a href='https://www.openstreetmap.org/copyright'>OpenStreetMap</a> contributors'" }); baseLayer.addTo(map); // 以下を追加 document.getElementById("search").addEventListener("click", () => { const address = document.getElementById("address").value; normalize(address).then(result => { map.flyTo([result.lat, result.lng]); // 住所の地点に移動 const marker = L.marker([result.lat, result.lng]); // ピンを作成 marker.bindPopup(address); // ピンをクリックすると住所が表示されるようにする marker.addTo(map); // 地図にピンを立てる }); }); 完成! これでとりあえず動くものができました! 宣伝 本記事で作成したプログラムにエラー処理の追加やインターフェースの改良を施したものを Leaflet プラグインとして公開しました! Cocon/Leaflet.CommunityGeoCoder 実際に触って動かせるデモページも作ってみました。ぜひご活用いただき、バグや機能提案などあればご気軽にお願いします。 Demo | Cocon/Leaflet.CommunityGeoCoder
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

BotUIを使ってチャットボットを開発してみた

はじめに はじめまして! t_powerです。 本記事では、社内FAQをBotUIでチャットボット化した話を扱います。 プログラミング知識があまりない・・・ チャットボットを作ってみたいけど難しそう・・・ と思っている人にはぴったりのツールだと思うので、是非ご覧ください! 今回作成する上で下記のサイトを参考にさせていただきました。 この記事を見て実際に作ろうと思っている方はこちらも参考にしてください。 1. JavaScriptだけで本格的なチャットボットを開発できるライブラリ「BotUI」を使ってみた! 2. チャットボットを作ろう:2. JekyllサイトでBotUI 今回作成したもの 「はじめる」と書かれているバナーからチャットボットを使うことができます。 作ろうと思った経緯 before 従来は、問い合わせをメールなどで受け付けて、担当者が直接回答をしていました。 類似の問い合わせも多く、担当者の負担になっていました。 after それなら・・・ AIが流行っているし、問い合わせ対応をチャットボット化すればいいじゃないか! という流れになり、チャットボットを開発することになりました。 BotUIとは チャットボットのUIを構築するためのJavaScriptフレームワーク。直観的なJavaScript APIを使用して、メッセージを追加し、ユーザーが実行できるアクションを表示できる。 (出典: BotUI公式ドキュメント) 要するに、これを使えば、簡単にチャットボットが作れるよ~ってわけです! チャットボットは大きく分けるとシナリオ型とAI型(人工知能型)の2種類存在していて、BotUIは、シナリオ型を簡単に作ることが出来るフレームワークになっています。 ※シナリオ型とAI型の違いについてはこちらをご覧ください シナリオ型・AI型のチャットボットを比較~導入メリットとシナリオ作成のコツ チャットボット用のフレームワークやライブラリはたくさんあるようですが、今回はBotUIが適していそうだったので、こちらを採用しました。 事前準備 作業環境 OS: Windows10 テキストエディタ: Visual Studio Code ブラウザ: Microsoft Edge フォルダ構成 css/botui-theme-default.css 独自のテーマを作成することができるファイル(デザインの変更はこちらのファイルがメイン) css/botui.min.css 基本レイアウトを設定しているファイル(あまり書き換える必要ない) ※CSSファイルはCDN経由で読み込んでもよいのですが、今回はデザインを変更したかったため、GitHubからダウンロードしました ※とにかく動くチャットボットをはやく作りたい!デザインなんてどうでもいい!という方はダウンロードする必要はございません images/sample.png BOT側のアイコン ※もしアイコンを表示させたい場合は、こちらの任意の画像と、botui.min.cssをダウンロードして、書き換える必要があります(無くてもチャットボットは動きます) bot/index.html チャットボットを表示させるファイル bot/answer.js 質問に対する回答をまとめたファイル bot/bot.js BotUIを使ってシナリオを構成するファイル 基本的な使い方 index.html index.html <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>チャットボット</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" href="../css/botui-theme-default.css" /> <link rel="stylesheet" href="../css/botui.min.css" /> <!-- link rel="stylesheet" href="https://unpkg.com/botui/build/botui.min.css" / --> <!-- link rel="stylesheet" href="https://unpkg.com/botui/build/botui-theme-default.css" / --> </head> <body> <div id="botui-app" style="white-space:pre-wrap; word-wrap:break-word;"> <bot-ui></bot-ui> </div> <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.5/vue.min.js"></script> <script src="https://unpkg.com/botui/build/botui.min.js"></script> <script src="./answer.js"></script> <script src="./bot.js"></script> </body> </html> index.htmlは上記のコードをそのままコピペでOKです。 (デザインにこだわらない人は<!--link rel="stylesheet" href="https://unpkg.com/botui/build/botui.min.css" />と<!-- link rel="stylesheet" href="https://unpkg.com/botui/build/botui-theme-default.css" / --> のコメントを外して、お使いください) また、Bootstrapを導入して、スマホ、PC対応のレスポンシブな画面にしてもよいかもしれません。 style="white-space:pre-wrap; word-wrap:break-word;"について CDN経由で読み込んでいるVue.jsのバージョンがなぜ最新バージョンではないのか 上記2点は苦戦した点にて記載しています。 bot.js bot.js var botui = new BotUI('botui-app'); var Photo = '../images/sample.png' //初期メッセージ botui.message.add({ photo: Photo, content: 'チャットボットです', delay: 1000 }).then(showQuestions); //質問の選択肢を表示する関数 function showQuestions(){ botui.message.add({ photo: Photo, content: '質問をお選びください', delay: 1000 }).then(function(){ return botui.action.button({ autoHide: false, delay: 1000, action: [ {icon: 'sticky-note-o', text: '採用', value: 'recruitment'}, {icon: 'user', text: '新入社員', value: 'newEmployee'}, {icon: 'ellipsis-h', text: 'その他', value: 'other'}] }); }).then(function(res){ botui.action.hide(); switch(res.value){ case 'recruitment': showRecruitment(); break; case 'newEmployee': showNewEmployee(); break; case 'other': showOther(); break; default: end(); } }); } //採用カテゴリの質問の選択肢を表示する関数 function showRecruitment(){ botui.message.add({ photo: Photo, delay: 1000, content: '当てはまるものをお選びください' }).then(function(){ //ボタンを表示 return botui.action.button({ autoHide: false, delay: 1000, action: [ {icon: 'circle', text: '一次面接と最終面接の違い', value: 'interviewDifference'}, {icon: 'circle', text: '筆記試験の合格基準を教えていただけないでしょうか', value: 'successCriteria'}, {icon: 'long-arrow-left', text: '1つ戻る', value: 'return'}] }); }).then(function(res){ botui.action.hide(); switch(res.value){ case 'interviewDifference': showInterviewDifference(); break; case 'successCriteria': showSuccessCriteria(); break; case 'return': showQuestions(); break; default: end(); } }); } function showInterviewDifference(){ botui.message.add({ photo: Photo, delay: 1000, content: ansInterviewDifference }).then(askEnd); } /* 省略 */ //プログラムを終了するか聞く関数 function askEnd(){ botui.message.add({ photo: Photo, delay: 2000, content: '他に質問はありますか' }).then(function(){ return botui.action.button({ delay: 1500, action: [ {icon: 'circle-o', text: 'はい', value: true}, {icon: 'close', text: 'いいえ', value: false}] }); }).then(function(res){ res.value ? showQuestions() : end(); }); } //プログラムを終了する関数 function end(){ botui.message.add({ photo: Photo, delay: 1500, content: ansEnd }) } 今回作成したシナリオはこのような構成です。 (上記のコードには記載していませんが、「新入社員」、「その他」についても、「採用」と同様に作りました) bot.jsもphotoのパス指定だけ気を付ければ、コピペで問題ないです。 今回使用したメソッドやオブジェクトについて説明します。 method 名前 説明 botui.message.add 新規メッセージを追加する botui.action.button ボタンを表示する botui.action.hide アクションを非表示にする action object 名前 説明 autoHide 自動的に非表示になるのを防ぐ botui.action.hide()と一緒に使い、これが呼び出されたときにボタンを消す action 今回はbutton objectのicon、text、valueでそれぞれ、ボタンのアイコン、表示させる文、値を設定できる また、iconはFontAwesomeを利用している botui.action.button()などと一緒に使う message object 名前 説明 photo 画像のパスを指定して、アイコンを指定 content 表示されるメッセージを指定 delay 表示されるまでの秒数を設定できる ※ミリ秒単位なので、1000は1秒にあたる 他のメソッドやオブジェクトに関してはこちらをご覧ください。 bot.js }).then(function(res){ botui.action.hide(); switch(res.value){ case 'recruitment': showRecruitment(); break; case 'newEmployee': showNewEmployee(); break; case 'other': showOther(); break; default: end(); } }); 選択した項目からチャットを進めていくために、switch文で条件分岐させ、選択したボタンのvalueをresで取得し、それに合った関数を呼び出しています。 bot.js //プログラムを終了するか聞く関数 function askEnd(){ botui.message.add({ photo: Photo, delay: 2000, content: '他に質問はありますか' }).then(function(){ return botui.action.button({ delay: 1500, action: [ {icon: 'circle-o', text: 'はい', value: true}, {icon: 'close', text: 'いいえ', value: false}] }); }).then(function(res){ res.value ? showQuestions() : end(); }); } プログラムを終了するか聞く関数のaskEnd()では、「はい」か「いいえ」の2択しかないため、3項演算子を使用して分岐させています。 ?の左側res.valueがtrueならshowQuestions()、falseならend()を呼び出します。 answer.js 回答メッセージの変数をまとめたファイルです。 後で、回答メッセージをDBから取得するように改修する予定なので、一旦別ファイルに分けて宣言をしています。 answer.js var ansInterviewDifference = `一次面接は採用担当が実施します。 最終面接は社長が実施します。` /* 省略 */ var ansEnd = `ご利用いただきありがとうございました。 [TOPページへ戻る](http://xxx.xxx.xx.xxx)` このように、質問に対する回答をまとめています。 また、表示させるメッセージについては、`(バッククォート)で囲むことで、実際にファイルに記載したようにメッセージボックスにも表示されるようになります。 [TOPページへ戻る](http://xxx.xxx.xx.xxx)については、苦戦した点にて記載します。 index.html <script src="./answer.js"></script> <script src="./bot.js"></script> 今回のように別のjsファイルに質問に対する回答をまとめる場合、index.htmlで先に回答ファイルanswer.jsを読み込む必要があるので注意! 苦戦した点 指定した場所で改行できない index.html <div class="container" id="botui-app" style="white-space:pre-wrap; word-wrap:break-word;"> answer.js var ansEnd = `ご利用いただきありがとうございました。 [TOPページへ戻る](http://xxx.xxx.xx.xxx)` メッセージ内で\nや\r\nなどの改行コードを試してもうまくいかず・・・ 色々調べてみると・・・ Vue.jsで文字列を改行する場合 style="white-space:pre-wrap; word-wrap:break-word;" bot-uiタグが囲まれているdivタグの中にこれを記載し、先ほどanswer.jsで紹介したメッセージの内容を`(バッククォート)で囲む必要があるみたいです。 参考記事 Vue.jsでdataの文字列を改行したい HTMLクイックリファレンス white-space HTMLクイックリファレンス word-wrap リンク化できない index.html <!-- これだとリンク化できない --> <script src="https://cdn.jsdelivr.net/vue/latest/vue.min.js"></script> <!-- 2.0.5にすることでリンク化できる --> <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.5/vue.min.js"></script> 公式ガイドに記載されている通り、Vue.jsの最新バージョンをCDN経由で読み込んでいました。しかし、その場合URLの後に別のメッセージが表示されると、リンク化されなくなってしまうみたいです。 この問題を解決するためには、Vue.jsのバージョンを2.0.5に落とすことで、どのような状況でもリンク化されるようになります。 参考記事 URL loses its markup in javascript framework (BotUI) URLを新規タブで展開できない answer.js var ansEnd = `ご利用いただきありがとうございました。 [TOPページへ戻る](http://xxx.xxx.xx.xxx)` [表示文字](URL)で指定すると、同じタブで画面が切り替わってしまって、チャットボットが閉じてしまいます。 answer.js var newline = `[BotUI 公式マニュアル](https://docs.botui.org/index.html)^` 新規タブで展開するには、カッコの後ろに^(キャレット)を付けることで解決できました。 デザイン 今回デザインを変更するために、GitHubからダウンロードして読み込んだ、 botui.theme-default.cssとbotui.min.cssで重要だと思った点を紹介します。 botui.theme-default.css .botui-cotainer botui-theme-default.css .botui-container{ font-size:14px; background-color:#fff; font-family:"Open Sans",sans-serif } font-size メッセージの文字やボタンの文字の大きさを指定 background-color チャット背景色を指定 font-family フォントを指定(フォントファミリーと総称フォントの2つがある) .botui-messages-container botui-theme-default.css .botui-messages-container{ padding:10px 20px } padding メッセージのパディングを指定 .botui-actions-container botui-theme-default.css .botui-actions-container{ padding:0px 20px } padding アクション(今回はボタン)のパディングを指定 .botui-message botui-theme-default.css .botui-message{ min-height:30px } min-height メッセージ間の最小幅を指定 .botui-message-content botui-theme-default.css .botui-message-content{ padding:7px 13px; border-radius:15px; color:#000000; background-color:#8bf8f8 } padding メッセージ領域内のパディングを指定 color BOT側(左側)に表示されるメッセージの色を指定 background-color BOT側(左側)に表示されるメッセージ領域内の背景色を指定 ※border-radiusは外枠の丸み具合を変更するプロパティだが、ここで値を変更しても変わりませんでした(メッセージの要素内を変更するプロパティだから効かない?) .botui-message-content.human botui-theme-default.css .botui-message-content.human{ color:#000000; background-color:#f8ddaa } color 人間側(右側)に表示されるメッセージの色を指定 background-color 人間側(右側)に表示されるメッセージ領域内の背景色を指定 .botui-message-content.text botui-theme-default.css .botui-message-content.text{ line-height:150% } line-height メッセージ領域内の行間の高さを指定 .botui-message-content.embed botui-theme-default.css .botui-message-content.embed{ padding:5px; border-radius:5px } padding 動画のパディングを指定 border-radius 外枠の丸みを指定 .botui-message-content-link botui-theme-default.css .botui-message-content-link{ color:#1A0DAB } color リンクの色を指定 .botui-actions-buttons-button botui-theme-default.css .botui-actions-buttons-button{ border:1; color:#fff; line-height:1; cursor:pointer; font-size:14px; font-weight:normal; padding:10px 20px; border-radius:4px; font-family:"Open Sans",sans-serif; background:#777979; box-shadow:2px 3px 4px 0 rgba(0,0,0,.25) } cursor ボタンにカーソルを合わせたときの種類を指定 font-weight ボタンの文字の太さを指定 botui.min.css .botui-message-content botui.min.css .botui-message-content{ width:auto; max-width:80%; display:inline-block } max-width メッセージボックスの幅を指定 .profil botui.min.css .profil{ position:relative; top: -10px; border-radius:50% } top 基準の位置からどれくらい上に動かすかを指定 ※元々topプロパティは無かったが、アイコンを大きくしたらズレてしまったので、追記して丁度いい位置にくるように指定しました .profil>img botui.min.css .profil>img{ width:50px; height:50px; border:2px solid #e8e8e8 } width アイコン画像の幅を指定 height アイコン画像の高さを指定 .profil>img.agent botui.min.css .profil>img.agent{ /*初期設定だと変な画像になっているため、削除推奨 content:url(http://decodemoji.com/img/logos/blue_moji_hat.svg); */ border-radius:50% } content ここで画像指定していると、photoで表示したい画像を選んでもBOT側のアイコンは強制的にここで設定している画像になるため、削除推奨 おまけ YouTube動画の埋め込み方 今回作成したチャットボットでは使用していませんが、試したので記載します。 bot.js botui.message.add({ type: 'embed', content: 'https://www.youtube.com/embed/1oSeywNsjak', delay: 1000 }).then(showQuestions); YouTubeの動画を埋め込む際に注意する必要があります。 embedは外部要求を許可しているが、watchは許可していないため、URLを書き換える必要があります。 × https://www.youtube.com/watch?v=1oSeywNsjak ○ https://www.youtube.com/embed/1oSeywNsjak 参考記事 youtube html 埋め込み おわりに JavaScript初心者でも、BotUIを使うことで、簡単にシナリオ型のチャットボットを作ることが出来ました。 直観的に操作することが出来るので、プログラミングがよくわからなくても、それっぽいものを作れてしまうのが、BotUIのいいところだと思いました。 しかし、数が増えるとボタンも増える(分岐が増えてしまう)ため、見栄えがとても悪くなってしまいます。そのため、今後はAI型にシフトさせるなどの対策が必要だと思いました。 最後まで読んでいただき、ありがとうございました
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

初心者のための静的サイト・ジェネレーター・ガイド③

初心者のための静的サイト・ジェネレーター・ガイド②の続きです。 ②を読んでいない方はそちらから読むことをおすすめします 著者:Thom Krupa@thomkrupa 2020年11月18日 原文:https://bejamas.io/blog/static-site-generators/ 6. Nuxt 最終更新日:2020年10月24日 eコマースサイトの実験として始まったこのプロジェクトは、生産性の高いVueアプリケーションのための高レベルのフレームワークとなりました。このプロトタイプは、Next.jsの最初の公式リリースから数週間後に一般公開されました。Nuxtは、NextがReactに対して行っているのと同じように、Vue用のアプリを構築する際の多くの問題を解決するために作られました。今日では、エンドユーザーに最高の体験を提供できるように、十分に研究されたベストプラクティスに基づいて、最も重要な要素とインテリジェントなデフォルト値があらかじめ設定されています。 公開された当初、Nuxtは驚くほど軽量で、アプリケーションの開発を支援するコマンドラインインターフェースであるVue CLIの上にテンプレートとしてインストールすることしかできませんでした。とはいえ、すでにサーバーサイドレンダリングも可能で、静的なWebサイトを生成することもできました。 6-1 Nuxt 1.0 2018年1月8日に数名の追加貢献者の協力を得てリリースされ、Now(Vercel)でホストされています。彼らのドキュメントはNuxt.jsそのもので作られており、このリリースの時点ですでに6つの言語に翻訳されています。レイアウトの切り替えやミドルウェアの機能向上など、様々な新機能を搭載しています。 6-2 2020年にはさらなるチャンスがある 5月、NuxtJS社は、200万ドルのシードラウンドを終了することを決定したと発表しました。なお、Nuxt.jsフレームワークの開発者は、Nuxt.jsは常にオープンソースであり、コミュニティ主導のプロジェクトであることを表明しています。最新の情報は、ブログでご確認ください。 6-3 ファイル構成 これは、create-nuxt-appを使用する際に、セットアップの質問にどのように答えるかによって決まる、おおよそのデフォルト構造です。すべてのディレクトリには、中に入れるべきものを説明するREADME.mdファイルがあり、ドキュメントへのリンクも含まれています。 上記の構造の中にcontentというディレクトリがありますが、これは少し曖昧に見えるかもしれませんが、gitベースのヘッドレスCMSであるモジュールNuxt Contentをインストールすることを選択したために追加されたものです。 hello.mdからコンテンツを取得して使用する方法は後ほど紹介します。 6-4 データの取得 ここでは、Contentfulからデータを取得する基本的な例をご紹介します。まず、Nuxtモジュールをインストールする必要があります。また、セキュリティのためにローカル環境変数を管理するためにdotenvモジュールが必要です。Nuxt は .env をデフォルトで .gitignore に追加しました。 次に、プロジェクトのルートにある.envファイルに秘密鍵を設定します。 そして、Nuxtにそのことを知らせ、Contentfulの設定をnuxt.config.jsに追加します。 最後に、ページ内のデータを取得します。 asyncDataは、その戻り値をコンポーネントのローカルな状態、つまりデータとしてマージします。そして、次のように.NET Frameworkに追加することで、表示させることができます。 ここでは、例として1つのフィールドを取得していますが、Contentfulのデータ構造を扱う際の難しさがお分かりいただけると思います。GraphQLを使用することが強く推奨されており、Nuxtではこのためにvue-apolloを使用するモジュールを提供しています。 6-5 Nuxtのコンテンツ Nuxtは、Markdown、CSV、YAML、JSON、XMLを扱うことができる独自のヘッドレスCMSを提供しています。ご覧の通り、デフォルトのhello.mdからMarkdownを取得するのは驚くほど簡単です。 コンテンツの表示には、付属のコンポーネントを使用します。 6-6 エコシステム 機能を拡張するには、多くのNuxtモジュールを使用することができます。ライブラリには、PWAの統合から、Prismic、Storyblok、Sanityなどの様々なヘッドレスCMSまで、140種類以上のモジュールが含まれています。 6-7 どうやって始めるの? 最も簡単な方法は、create-nuxt-appを使うことです。 このプロジェクトで使用したい様々なオプションや技術について、一連の質問を受けます。完了すると、すべての依存関係がインストールされ、プロジェクトにナビゲートしてすぐに起動することができます。 6-8 Nuxt.jsのデプロイメント よくある質問のページに移動し、左側のメニューをスクロールすると、展開に関する項目があります。Vercel、Netlify、AWSなどの代表的なプラットフォームへのデプロイは、わずか数ステップで完了します。 6-9 結論 Nuxt.jsは、フルサービスのフレームワークです。SSG、SPA、サーバーサイド・レンダリング・アプリとして使用することもできます。JavaScriptベースのフレームワークの中でも最も高度な機能がパッケージされています。これには、新しいルートにナビゲートする前にミドルウェアを実行する機能や、外部サーバーを必要とせずに追加のAPIルートを登録できるサーバーサイドミドルウェアも含まれます。選択肢は無限にあり、その柔軟性は実に素晴らしいものです。 7. Scully 最終更新日:2020年10月28日 ScullyはJamstackのためのAngularの答えです。HeroDevs社の専門家によって作成されたこの製品は、GoogleのJSフレームワークをベースにしたおそらく唯一の静的サイトジェネレーターであり、すべてのAngularプロジェクトにJamstackの可能性をもたらしています。 Scullyはかなり新鮮なソリューションで、Jorge Canoが作成した最初のコミットは2019年12月12日でした。数ヶ月の作業を経て、安定した1.0.0がリリースされたため、実際のプロジェクトですぐに使用できました。 7-1 Angularの統合 Scullyは既存のAngularプロジェクトと統合されており、セットアップコマンドを1つ実行するだけで、ウェブサイトを簡単に静的にすることができます。 Scullyの興味深い特徴は、機械学習の実装です。Guess.jsを使ってサイトのすべてのルートを検索し、各ページをプレーンなHTMLとCSSにプリレンダリングします。 7-2 ファイル構成 プロジェクトでScullyを実行すると、いくつかの要素が作成されますが、それらはすべて、プロジェクトの設定を変更したり、プラグインを追加したり、ツールの動作をカスタマイズしたりするためのシンプルな設定ファイルです。 7-3 エコシステム Scullyはまだ歴史が浅いにもかかわらず、カスタムプラグインを作成する方法、APIとの統合、サードパーティのスクリプトを提供しています。プラグインの小さなライブラリがあり、それぞれのプラグインのインストールと設定についての詳細な説明があります。 7-4 どうやって始めるの? すでにAngularアプリケーションが作成されている場合は、コマンドを1つだけ実行します。 また、Angular以外のワークスペースでは、Scullyパッケージを以下のようにインストールします。 次に、プロジェクトを作成します。 プロジェクトをビルドするには、次のように実行します。 プロジェクトがビルドされたら、npm run scullyでScullyを実行できます。以上です。 7-5 結論 今のところ、ScullyはAngular専用に作られた唯一の静的サイトジェネレーターです。既存のプロジェクトを静的なウェブサイトにしたい場合や、Angular環境でJamstackのアプローチを試してみたい場合には最適です。 8. Gridsome 最終更新日:2020年10月24日 Jamstackのワークフローに合わせて作られており、Gatsby(同様のアーキテクチャを持つ)に強く影響を受けていますが、Vue.jsのスタイルで作られています。スピードと使いやすさのために最適化されています。Gridsomeはまだ新しい製品ですが、あらゆるデータソースとの統合が可能な堅実な製品です。 2018年10月10日、Gridsomeチームは、最初のベータリリースを正式に発表しました。その目的は、Gatsbyに代わるVue.jsを作ることでした。Gatsbyはすでに3年以上にわたって活動してきたGatsbyチームの知識や進歩を土台にすることができたため、多くの人気を獲得しました。 8-1 ファイル構成 gridsome.config.jsファイルを開くと、いくつかのコメントとこれらの設定だけが表示されます。 もう一つの重要なファイルはgridsome.server.jsです。こちらは、Gridsome Serverに接続して、様々なAPIにアクセスしたり、ローカルファイルや外部APIからデータを読み込んだり、プログラムでページを作成したりすることができます。 8-2 データの取得 1つのページに対してContentfulからどのようにデータを取得するかを見てみましょう。まず、Contentfulプラグインをインストールする必要があります。 次に、プロジェクトのルートにある.envファイルに秘密鍵を設定します。 つまり、自分でGitリポジトリをセットアップする必要があるだけでなく、.gitignoreファイルに.env*を必ず追加しなければなりません。これは、秘密鍵を共有しないようにするために、絶対に必要なことです。なぜなのかというと、環境ごとに異なる変数を用意できるからです。ファイル名は、「.env.development」「.env.production」のように決められています。 Contentfulプラグインを使用するには、gridsome.config.jsの設定を変更してください。このように表示されます。 Gatsby同様、GridsomeもGraphQLデータレイヤーを提供してくれます。GraphQLに慣れていない方のために、便利なプレイグラウンドが用意されていますので、http://localhost:8080/___explore。gridsome developmentを実行すると、Contentfulのデータを取得するためのクエリが自動的に作成されます。ここでは、Index.vueでのクエリの構成を説明します。 製品を掲載するために、私たちは単純にループ状のエッジを使っています。 8-3 エコシステム Gridsomeのプラグインコレクションは、Gatsbyのそれには及びませんが、例えば、お好みの人気CMSへの接続、Google Analyticsの設定、サイトマップの生成などに役立つものが多数あります。 8-4 どうやって始めるの? Gatsbyと同様、最初のステップはGridsome CLIをグローバルにインストールすることです。 公式のスターター3人と、コミュニティによって追加された20人のスターターをコレクションしています。デフォルトのスターターを使って、new-projectをお好みのプロジェクト名に置き換えることをお勧めします。 プロジェクトのディレクトリに移動し、実行してください。 これで、http://localhost:8080 でサイトを開くことができるはずです。 8-5 Gridsomeの導入 Gridsomeチームは、選択したGitベースのリポジトリからサイトを構築するデプロイサービスの接続を推奨しています。上位のサービスとしては、AWS Amplify、Vercel、Github Pagesなどが挙げられます。しかし、その中でもトップに位置するのが「Netlify」であり、これ以上ないほどシンプルなものです。 8-6 結論 Gridsomeは非常に軽量で、シンプルな静的サイトを素早く立ち上げるのに最適な選択肢です。また、「数千ページを数秒で生成する」ことができるように、複雑なレポートにも対応できます。Gridsomeは、大規模なサイトでも検討する価値があると思います。しかし、このSSGはまだ初期の段階であることを忘れてはいけません。個人的には、VueベースのサイトではNuxt.jsの方が安全だと思います。今後、Gridsomeチームが新しいバージョンの開発に取り組んでいくことになりますので、ご期待ください。 初心者のための静的サイト・ジェネレーター・ガイド④に続きます 読んで下さり、ありがとうございました Jamstackに関心がある方はこちらまでお問合せください! 株式会社ヒューマンサイエンス https://www.science.co.jp/
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

プログラミング初心者が⭕️❌ゲームを作ってみた。

はじめに 初めまして、こんにちは! 私は現在32歳で未経験からエンジニアへの転職を目指して学習しています。 トレーニングとしてJavaScriptを使い、⭕️❌ゲームを作成し勝敗判定ができるコードを書いてみました。 その過程を記事にまとめていきます。 要件定義 今回はJavaScriptを中心に書き、HTMLとCSSでは簡単な枠組みを用意します。 ・ブラウザを開くと、5X5のマスが表示される。 ・いずれかのマスをクリックすると、初めに⭕️、次に❌と交互に反映される。 ・縦横斜めの何かで一列揃うと勝敗が判定され、アラートが表示される この内容を実現できる内容を目指します。 実装 1 レイアウト 初めにマスを用意します。 今回は5x5のマスを用意します。 <div id="mas"> <table id="board"> <!-- 行1 --> <tr> <td></td> <td></td> <td></td> <td></td> <td></td> </tr> <!-- 行2 --> <tr> <td></td> <td></td> <td></td> <td></td> <td></td> </tr> <!-- 行3 --> <tr> <td></td> <td></td> <td></td> <td></td> <td></td> </tr> <!-- 行4 --> <tr> <td></td> <td></td> <td></td> <td></td> <td></td> </tr> <!-- 行5 --> <tr> <td></td> <td></td> <td></td> <td></td> <td></td> </tr> </table> </div> そして装飾も簡単に行います。 <style type="text/css"> #mas { padding: 50px; } table { border: 1px solid #999; max-width: 100%; background: blue; text-align: center; } td { border: 1px solid #fff; width: 80px; height: 80px; font-size: 35px; color: #fff; } </style> このようにブラウザで表示されるようになります。 2 マスをクリックした時のイベント実装 次にマスをクリックした時に⭕️や❌という文字を表示させるようにします。 //行列の変数 const lineNumber = 5; let board = [] //配列を変数を使い列の数だけ作成 const newArray = new Array(lineNumber).fill(0); for (let j = 0; j < newArray.length; j++) { board[j] = newArray.map((num) => { return 0; }); } この後に実装していく勝敗判定についてですが、配列を用います。 boardという空の配列を用意します。 ここに配列を複数格納し、多次元配列とします。 それぞれの要素に数字の0を入れるためにfor文とmapメソッドを使い実装しました。 結果として初期状態としては、このようになっています。 board = [ [0,0,0,0,0], [0,0,0,0,0], [0,0,0,0,0], [0,0,0,0,0], [0,0,0,0,0], ]; この0が1になると⭕️、2になると❌と表示され、 それぞれの行で⭕️が5個格納されれば勝利という判定方法を実行することを目指します。 //1回のみのクリックを許容する変数 const oneTime = { once: true }; //td要素の取得 let td = document.getElementsByTagName("td"); //クリックカウント let count = 0; //tdの要素1つ1つにクリックイベントを起こせるようにする for (let index = 0; index < td.length; index++) { td[index].addEventListener('click', (event) => { let action = event.target; //カウントを増やしながら1回事に交互に繰り返すことを書く let text = null; if (count % 2 === 0) { text = "○" //マルだったら配列に1を入れる board[Math.floor(index / lineNumber)][index % lineNumber] = 1; } else { text = "✖︎"; //バツだったら配列に2を入れる board[Math.floor(index / lineNumber)][index % lineNumber] = 2; } count++; action.textContent = text; }, oneTime) } for分を用いて列ごとに配列を取得します。 その中でaddEventListenerでそれぞれのtdタグ要素にクリックイベントを起こせるようにします。 マスのそれぞれの場所はboard[ ][ ]と記すことで指定することができます。 例えばboard[0][0]は左上のマスになります。 board[Math.floor(index / lineNumber)][index % lineNumber] ここでクリックした場所がどのマスかを特定して、⭕️の時は1が入る、❌の時には2が入るようにしています。 そしてクリックしたらカウントが増え、textContentに⭕️もしくは❌が反映されるようになります。 3 横判定の実装 ここまでで準備は整いました。 ここからは判定するコードを書いていきます。 //勝敗結果のアラート関数 function alert_Maru() { setTimeout(() => { alert('○の勝ちです'); }, 300); }; function alert_Batsu() { setTimeout(() => { alert('✖︎の勝ちです'); }, 300); } }, false); まずは勝敗判定の関数を定義しておきます。 //横列のマルとバツの勝ち判定関数 function besideWin() { for (let i = 0; i < lineNumber; i++) { //縦を固定しその配列内で1と2の要素の配列をそれぞれarr1とarr2へ格納しその長さで判定 const arr1 = board[i].filter((one) => { return one === 1; }); const arr2 = board[i].filter(two => two === 2); if (arr1.length === lineNumber) { alert_Maru(); }; if (arr2.length === lineNumber) { alert_Batsu(); }; } } 横列の判定は一番簡単です。 それぞれの配列の要素の数をカウントすればいいです。 配列内の要素が1もしくは2になればarr1、arr2に格納されるようにし、 その長さが列の長さと一致すれば勝敗判定ができるという仕組みです。 4 縦列判定の実装 続いて縦列の判定です。これは少し考える必要があります。 基本的には横列の判定と同じ考え方をしますが、縦で見ると配列を跨ぐので、 全く同じにはできません。 //縦列を判定する関数 function verticalWWin() { for (let i = 0; i < lineNumber; i++) { //現在の配列から縦でみる配列を生成する。 const oneCount = board.map((one) => { return one[i] }); const twoCount = board.map((two) => { return two[i]; }); //縦で見れる配列で各要素の1と2を探しそれぞれ変数へ格納。 const arr1 = oneCount.filter((one) => { return one === 1; }); const arr2 = twoCount.filter((two) => { return two === 2; }); if (arr1.length === lineNumber) { alert_Maru(); }; if (arr2.length === lineNumber) { alert_Batsu(); } }; 横列判定と比較すると1つ手順を追加しています。 oneCountとtwoCountという変数を用意します。 mapメソッドを使うことで縦を判断できる配列を作成し、その配列内の1と2の数をカウントして 勝敗判定を実装しています。  5 斜め列の実装 最後の判定に斜めの列の判定を実装します。 //斜めでの勝利判定 function xWin() { //斜め判定のループ 添字とその逆で斜めを判定 const diagonal = board.map((one, index) => { return one[index] || one[(lineNumber - 1) - index]; }) const arr1 = diagonal.filter((one) => { return one === 1; }); const arr2 = diagonal.filter((two) => { return two === 2; }) if (arr1.length === lineNumber) { alert_Maru(); }; if (arr2.length === lineNumber) { alert_Batsu(); } }; 斜めは縦列判定の応用です。 diagonalという配列で斜めのマスをカウントしてもらわないといけません。 左上から右斜め下への場合は、 board[0][0],board[1][1],board[2][2],board[3][3],board[4][4] 右上から左斜め下への場合は、 board[0][4],board[1][3],board[2][2],board[3][1],board[4][0] このマスで配列を作ることができれば、その後は今までと同じ方法で判定できます。 今回はmapメソッドのcallback関数の第二引数を使い、配列のindexを取得できます。 このindexを利用して、斜めに格納されている値を取り出した配列を作りました。 const diagonal = board.map((one, index) => { return one[index] || one[(lineNumber - 1) - index]; }) 6 仕上げ これまで実装した判定の関数をマスをクリックした時のイベントに追加して、 動作を確認します。 ここまで書いた全体のコードを記載します。 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>マルバツゲーム 5 x 5</title> <style type="text/css"> #mas { padding: 50px; } table { border: 1px solid #999; max-width: 100%; background: blue; text-align: center; } td { border: 1px solid #fff; width: 80px; height: 80px; font-size: 35px; color: #fff; } </style> </head> <body> <div id="mas"> <table id="board"> <!-- 行1 --> <tr> <td></td> <td></td> <td></td> <td></td> <td></td> </tr> <!-- 行2 --> <tr> <td></td> <td></td> <td></td> <td></td> <td></td> </tr> <!-- 行3 --> <tr> <td></td> <td></td> <td></td> <td></td> <td></td> </tr> <!-- 行4 --> <tr> <td></td> <td></td> <td></td> <td></td> <td></td> </tr> <!-- 行5 --> <tr> <td></td> <td></td> <td></td> <td></td> <td></td> </tr> </table> </div> <script> "use strict"; //行列の変数 const lineNumber = 5; let board = [] //配列を変数を使い列の数だけ作成 const newArray = new Array(lineNumber).fill(0); for (let j = 0; j < newArray.length; j++) { board[j] = newArray.map((num) => { return 0; }); } //勝敗結果のアラート関数 function alert_Maru() { setTimeout(() => { alert('○の勝ちです'); }, 300); }; function alert_Batsu() { setTimeout(() => { alert('✖︎の勝ちです'); }, 300); } //1回のみのクリックを許容する変数 const oneTime = { once: true }; //td要素の取得 let td = document.getElementsByTagName("td"); //クリックカウント let count = 0; //tdの要素1つ1つにクリックイベントを起こせるようにする for (let index = 0; index < td.length; index++) { td[index].addEventListener('click', (event) => { let action = event.target; //カウントを増やしながら1回事に交互に繰り返す変数 //カウントの名前をクリックカウントへ変更する let text = null; if (count % 2 === 0) { text = "○" //マルだったら配列に1を入れる board[Math.floor(index / lineNumber)][index % lineNumber] = 1; } else { text = "✖︎"; //バツだったら配列に2を入れる board[Math.floor(index / lineNumber)][index % lineNumber] = 2; } count++; action.textContent = text; verticalWWin(); besideWin(); xWin(); //引き分け判定 if (count === lineNumber * lineNumber) { setTimeout(() => { alert('引き分けです'); }, 300); } }, oneTime) } //横列のマルとバツの勝ち判定関数 function besideWin() { for (let i = 0; i < lineNumber; i++) { //縦を固定しその配列内で1と2の要素の配列をそれぞれarr1とarr2へ格納しその長さで判定 const arr1 = board[i].filter((one) => { return one === 1; }); const arr2 = board[i].filter(two => two === 2); if (arr1.length === lineNumber) { alert_Maru(); }; if (arr2.length === lineNumber) { alert_Batsu(); }; } } //縦列を判定する関数 function verticalWWin() { for (let i = 0; i < lineNumber; i++) { //現在の配列から縦でみる配列を生成する。 const oneCount = board.map((one) => { return one[i] }); const twoCount = board.map((two) => { return two[i]; }); //縦で見れる配列で各要素の1と2を探しそれぞれ変数へ格納。 const arr1 = oneCount.filter((one) => { return one === 1; }); const arr2 = twoCount.filter((two) => { return two === 2; }); if (arr1.length === lineNumber) { alert_Maru(); }; if (arr2.length === lineNumber) { alert_Batsu(); } }; } //斜めでの勝利判定 function xWin() { //斜め判定のループ添字とその逆で斜めを判定 const diagonal = board.map((one, index) => { return one[index] || one[(lineNumber - 1) - index]; }) const arr1 = diagonal.filter((one) => { return one === 1; }); const arr2 = diagonal.filter((two) => { return two === 2; }) if (arr1.length === lineNumber) { alert_Maru(); }; if (arr2.length === lineNumber) { alert_Batsu(); } }; </script> </body> </html> これでブラウザで表示させると無事にゲームが作動します。 最後に このゲームの作成はかなり苦戦しました。 特に2つの部分で実力不足だと感じました。 ・実現したいものをどのデータをどのように操作することで実現できるのかを言語化できないこと ・データアルゴリズムの理解が浅いこと ここが本当に足りてませんでした。 まだまだ学習し続けていきます。 何か気になる点があれば遠慮なくコメントください。 最後まで見ていただきありがとうございました!!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

deno.json(Deno設定ファイル)の書き方

2021年9月14日リリースのv1.14で、deno.json(Denoの設定ファイル)が導入されした。 この記事では、設定ファイルの書き方について解説します。 なぜdeno.jsonが導入されたか 元々、DenoにはCargo.toml(Rust)やpackage.json(Node.js)のような設定ファイルが存在しませんでした。 しかし、 権限指定用のコマンドラインフラグ(--allow-xxxなど)が増えた 既にコマンドラインフラグで指定できるファイルが存在している(import-mapとtsconfig) deno fmtやdeno lintに対する細かい設定を可能にして欲しいという要望が出ていた という理由から、設定ファイルが導入されることになりました。 deno.jsonの特徴 Node.jsにおけるpackage.jsonの反省を踏まえた設計になっています。 ブラウザと同様、deno.jsonが無くても動作する(あくまでオプションという扱い) ファイル中にコメントを書く場合は、拡張子をjsoncにする必要がある 未知のプロパティを許可しない(他のプログラムの設定ファイルとして使うことはできない) モジュールの読み込みには関与しない Denoにはパッケージの概念が無く、一つ一つのファイルがモジュールであり、web上のあらゆる場所から参照されうる package.jsonの「パスのルートまで遡って設定ファイルを発見する」という動作は、URLからのimport時にサーバーに無駄な負荷をかける 実行時のエントリポイントでのみ使用される つまり、外部モジュールの読み込みには関与しません。 外部モジュールのトップにdeno.jsonが置いてあったとしても、外部モジュールをimportするプログラムからは単に無視されます。ここがpackage.jsonとの大きな違いです。 deno.jsonの構文 deno.json(例) { "compilerOptions": { "allowJs": true, "lib": ["deno.window"], "strict": true }, "lint": { "files": { "include": ["src/"], "exclude": ["src/testdata/"] }, "rules": { "tags": ["recommended"], "include": ["ban-untagged-todo"], "exclude": ["no-unused-vars"] } }, "fmt": { "files": { "include": ["src/"], "exclude": ["src/testdata/"] }, "options": { "useTabs": false, "lineWidth": 80, "indentWidth": 2, "singleQuote": false, "proseWrap": "always" } } } compilerOptions: tsconfig.jsonと同じ内容を指定 lint: deno lintの設定 files: include: lintするファイル exclude: lintしないファイル rules: deno lintで使用するルールを指定(ルールの一覧はhttps://lint.deno.land/ に掲載) tags: ["recommended"]を指定するとデフォルトのルールセットになる include: tagsに加えて適用するルール exclude: tagsから除外するルール fmt: deno fmtの設定 files: include: フォーマットするファイル exclude: フォーマットしないファイル options: deno fmtの設定オプション useTabs: "インデントにタブ(true)とスペース(false)のどちらを使用するか(デフォルト: false) lineWidth: 1行当たりの文字数(デフォルト: 80) indentWidth: インデントの文字数(デフォルト: 2) singleQuote: 引用符にシングルクォーテーション(true)を使用するかダブルクォーテーション(false)を使用するか。(デフォルト: false) proseWrap: 文章をMarkdownファイルで改行する方法(always又はnever又はpreserve、デフォルト: always) ファイル中にコメントを書く場合は拡張子をjsoncにします。 上記以外のプロパティがあった場合、エラーが発生します。 v1.14時点ではリンターとフォーマッタとtsconfigのみサポートされていますが、将来的にはコマンドラインフラグの指定やimport-mapの指定にも使えるようになる見込みです。 deno.jsonの使い方 コマンドラインフラグの--configで指定します。 > deno run --config ./deno.json ./mod.ts なおファイル名ですが、将来的に自動検出が導入される予定なので、deno.jsonかdeno.jsoncとしておくのが良いようです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】Data Confirm Modal - backdrop

backdrop とは 直訳すると劇場幕のことで、Webではモーダルを開いたときの背景の黒いところのことを言います。 backdropをクリックしてもモーダルが非表示にならない方法 dataConfirmModal.confirm({ title: 'Are you sure?', text: 'Really do this?', commit: 'Yes do it', cancel: 'Not really', zIindex: 10099, onConfirm: function() { alert('confirmed') }, onCancel: function() { alert('cancelled') }, onHide: function() { alert('hidden') }, // これを追加することで、backdropをクリックしてもモーダルが非表示にならない backdrop: 'static' }); 参考 GitHub ifad / data-confirm-modal https://github.com/ifad/data-confirm-modal
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GraphQL + Sequelize でテーブル定義やクエリの実装などをちょっとだけ簡略化する方法

はじめに 業務でGraphQLを使うことになったんですが、GraphQLのモジュール宣言って面倒くさいですよね。 こういうの。 server/modules.js const typeDefs = gql` type User { id: Int! name: String! email: String! recipes: [Recipe!]! } type Recipe { id: Int! title: String! ingredients: String! direction: String! user: User! } type Query { user(id: Int!): User allRecipes: [Recipe!]! recipe(id: Int!): Recipe } type Mutation { createUser(name: String!, email: String!, password: String!): User! createRecipe(userId: Int!, title: String!, ingredients: String!, direction: String!): Recipe! } ` const resolvers = { Query: { async user(root, { id }, { models }) { return models.User.findByPk(id) }, async allRecipes(root, args, { models }) { return models.Recipe.findAll() }, async recipe(root, { id }, { models }) { return models.Recipe.findByPk(id) } }, Mutation: { async createUser(root, { name, email, password }, { models }) { return models.User.create({ name, email, password: await bcrypt.hash(password, 10), }) }, async createRecipe(root, { userId, title, ingredients, direction }, { models }) { return models.Recipe.create({ userId, title, ingredients, direction }) } }, User: { async recipes(user) { return user.getRecipes() }, }, Recipe: { async user(recipe) { return recipe.getUser() }, }, } DBのテーブルをひとつ追加するのに修正箇所が多すぎると感じたので、簡略化するためのモジュールクラスを作ってみました。 もしかしたら、もっといい方法や似たようなライブラリがあるかもしれませんが、とりあえず、一例として共有します。 環境構築から説明しますが、不要な方は読み飛ばしてください。 リポジトリ この記事に記載のコードは上記リポジトリに格納されています。 環境等はこちらをご参照ください。 環境構築 開発環境はWindowsで、WSL上にリポジトリを置き、さらにNode環境を構築したDockerコンテナ上で作業しています。 ただ、今回の内容にはほぼ関係ないので「Linux上で作業している」くらいにとらえてもらえれば良いかと。 こちらの環境を前提に説明していきます。 ちなみにNode.jsはインストール済みの体でよろしく。 こちらのサイトを参考にセットアップしていきます。 使用するDBは参考サイトと同じくSQLiteを使用します。 Sequelize関数の修正 一部Sequelizeのバージョンアップの影響で関数を変更する必要があります。 resolversのQueryの中で実行しているfindByIdをfindByPkに変更しましょう。 async user(root, { id }, { models }) { return models.User.findById(id) } ↓ async user(root, { id }, { models }) { return models.User.findByPk(id) } DBファイルの生成 DBのファイルはプッシュしていませんので、サンプルコードをクローンした場合は、以下のコマンドを実行してください。 DBファイルを生成。 touch ./database/database.sqlite DBをマイグレートします。 npx sequelize db:migrate Playgroundの設定 dockerコンテナ上で実行する場合、Playgroundが動作しない場合があります。 そんな時はsrc/index.jsを以下のように修正しましょう。 src/index.js const { ApolloServer } = require('apollo-server') const { typeDefs, resolvers } = require('../server/modules') const models = require('../models') // playgroundをロード const { ApolloServerPluginLandingPageGraphQLPlayground } = require('apollo-server-core') const server = new ApolloServer({ typeDefs, resolvers, context: { models }, plugins: [ApolloServerPluginLandingPageGraphQLPlayground({})], // playgroundを設定する }) server.listen({ port: 3000, }).then(({ url }) => console.log('Server is running on localhost:3000')) 動作確認 サーバを起動して、Playgroundが起動したら成功です。 GraphQLモジュール モジュール部分はserver/modules.jsという1ファイルにまとめています。 リポジトリではEnvironment-construction-completedタグを付けてますが、ここにも全文を載せてみます。 server/modules.js const { gql } = require('apollo-server') const bcrypt = require('bcryptjs') const typeDefs = gql` type User { id: Int! name: String! email: String! recipes: [Recipe!]! } type Recipe { id: Int! title: String! ingredients: String! direction: String! user: User! } type Query { user(id: Int!): User allRecipes: [Recipe!]! recipe(id: Int!): Recipe } type Mutation { createUser(name: String!, email: String!, password: String!): User! createRecipe(userId: Int!, title: String!, ingredients: String!, direction: String!): Recipe! } ` const resolvers = { Query: { async user(root, { id }, { models }) { return models.User.findByPk(id) }, async allRecipes(root, args, { models }) { return models.Recipe.findAll() }, async recipe(root, { id }, { models }) { return models.Recipe.findByPk(id) } }, Mutation: { async createUser(root, { name, email, password }, { models }) { return models.User.create({ name, email, password: await bcrypt.hash(password, 10), }) }, async createRecipe(root, { userId, title, ingredients, direction }, { models }) { return models.Recipe.create({ userId, title, ingredients, direction }) } }, User: { async recipes(user) { return user.getRecipes() }, }, Recipe: { async user(recipe) { return recipe.getUser() }, }, } module.exports = { typeDefs, resolvers } このファイル内には、DBのUserテーブルとRecipeテーブルの情報が書いてあります。 DBに新たなテーブルを追加した場合、このファイルに追加することになりますが、複数個所に手を加えなければいけないので少々面倒です。 また、すべてのテーブルの情報が1ファイルに混在しているのも混乱の素になります。 そこで、テーブルごとにクラス化することを考えてみようと思います。 今回は色々試し試し実装していったので、基本クラスとかは作ってないです。 同じことを試そうとする方がいらっしゃれば、基本クラスに切り出したりしてみてください。 Userテーブル まずはUserテーブル関連の処理をモジュールクラス化します。 コード全文載せちゃいます。 server/modules/UserModule.js const bcrypt = require('bcryptjs') class UserModule { get apiType() { const result = []; result.push(` type User { id: Int! name: String! email: String! recipes: [Recipe!]! } `); return result.join('\n'); } get queryType() { const result = []; result.push(`user(id: Int!): User`); return result.join('\n'); } get queryList() { const result = []; result.push(this.user); return Object.fromEntries(result.map((func) => [func.name, func])); } get mutationType() { const result = []; result.push(`createUser(name: String!, email: String!, password: String!): User!`); return result.join('\n'); } get mutationList() { const result = []; result.push(this.createUser); return Object.fromEntries(result.map((func) => [func.name, func])); } get otherResolver() { return { User: { async recipes(user) { return user.getRecipes() }, }, }; } async user(root, { id }, { models }) { return models.User.findByPk(id); } async createUser(root, { name, email, password }, { models }) { return models.User.create({ name, email, password: await bcrypt.hash(password, 10), }); } } module.exports = UserModule; ひとつひとつ説明します。 API宣言 API(型)宣言部です。 get apiType() { const result = []; result.push(` type User { id: Int! name: String! email: String! recipes: [Recipe!]! } `); return result.join('\n'); } 基本的には文字列を返します。 InputTypesなどを追加したい時はresult.push()を追加することで対応可能です。 result.push(` input ReviewInput { stars: Int! commentary: String } `); Query Query部はふたつの関数に分かれています。 Queryの実体はクラスのメンバ関数として実装します。 get queryType() { const result = []; result.push(`user(id: Int!): User`); return result.join('\n'); } get queryList() { const result = []; result.push(this.user); return Object.fromEntries(result.map((func) => [func.name, func])); } : 中略 : async user(root, { id }, { models }) { return models.User.findByPk(id); } 追加したい場合は、メンバ関数としてQueryの実体を実装した後、いずれもresult.push()を追記するだけでよいです。 Mutation Mutation部も基本的にQueryと同様です。 get mutationType() { const result = []; result.push(`createUser(name: String!, email: String!, password: String!): User!`); return result.join('\n'); } get mutationList() { const result = []; result.push(this.createUser); return Object.fromEntries(result.map((func) => [func.name, func])); } : 中略 : async createUser(root, { name, email, password }, { models }) { return models.User.create({ name, email, password: await bcrypt.hash(password, 10), }); } Resolver 追加のResolverを作りたいときは、関数を持ったオブジェクトを返すようにします。 get otherResolver() { return { User: { async recipes(user) { return user.getRecipes() }, }, }; } Recipeテーブル Userテーブルと同様にRecipeテーブルも新しいファイルを作成してモジュールクラスを実装します。 server/modules/RecipeModule.js class RecipeModule { get apiType() { const result = []; result.push(` type Recipe { id: Int! title: String! ingredients: String! direction: String! user: User! } `); return result.join('\n'); } get queryType() { const result = []; result.push('allRecipes: [Recipe!]!'); result.push('recipe(id: Int!): Recipe'); return result.join('\n'); } get queryList() { const result = []; result.push(this.allRecipes); result.push(this.recipe); return Object.fromEntries(result.map((func) => [func.name, func])); } get mutationType() { const result = []; result.push('createRecipe(userId: Int!, title: String!, ingredients: String!, direction: String!): Recipe!'); return result.join('\n'); } get mutationList() { const result = []; result.push(this.createRecipe); return Object.fromEntries(result.map((func) => [func.name, func])); } get otherResolver() { return { Recipe: { async user(recipe) { return recipe.getUser() }, }, }; } async allRecipes(root, args, { models }) { return models.Recipe.findAll() } async recipe(root, { id }, { models }) { return models.Recipe.findByPk(id) } async createRecipe(root, { userId, title, ingredients, direction }, { models }) { return models.Recipe.create({ userId, title, ingredients, direction }) } } module.exports = RecipeModule; モジュールファイル 作成したモジュールクラスをGraphQLに渡します。 多態性を持たせているので、配列にインスタンスを作成してぶん回します。 server/modules.js const { gql } = require('apollo-server') const UserModule = require('./modules/UserModule') const RecipeModule = require('./modules/RecipeModule') const moduleList = [ new UserModule(), new RecipeModule(), ] const typeDefs = gql` ${moduleList.map((m) => m.apiType).join('\n')} type Query { ${moduleList.map((m) => m.queryType).join('\n')} } type Mutation { ${moduleList.map((m) => m.mutationType).join('\n')} } ` const resolvers = { Query: { ...Object.fromEntries( moduleList.map((m) => Object.entries(m.queryList)).flat() ), }, Mutation: { ...Object.fromEntries( moduleList.map((m) => Object.entries(m.mutationList)).flat() ), }, ...Object.fromEntries( moduleList.map((m) => Object.entries(m.otherResolver)).flat() ), } module.exports = { typeDefs, resolvers } おわりに 以上が私が行った簡略化方法になります。 ライブラリを使用したり、デザインパターンを用いたとかではなく、小手先の方法に始終していますが、DBのテーブルを追加したり、修正したりするのがわかりやすくなりました。 もうちょっと頑張ればTypeScript化したり、さらに簡略化してライブラリ化とかも可能だと思いますが需要はあるんでしょうかね。 もっと簡単な方法があるぜとか、便利ライブラリがあるぜみたいな情報があれば教えてください。 参考URL https://github.com/apollographql/apollo-server https://www.digitalocean.com/community/tutorials/how-to-set-up-a-graphql-server-in-node-js-with-apollo-server-and-sequelize
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む