- 投稿日:2019-07-04T23:48:17+09:00
jQueryがそのページに読み込まれているか確認する方法
こんにちは、プログラミングスクールのレビューサイト「スクールレポート」を運営しているアカネヤ(@ToshioAkaneya)です。
jQueryがそのページに読み込まれているか確認する方法
実は、jQueryを読み込むと、グローバルに
jQuryという関数が宣言されています。これは$と同じ関数です。
すなわち、以下の式がtrueであればjQueryが読み込まれていると分かります。
typeof jQuery === "function"この記事が参考になれば幸いです。
はてなブックマーク・Pocketはこちらから
- 投稿日:2019-07-04T23:45:02+09:00
Vue.jsでQiita簡易クライアントを作成
こんにちは
普段はバックエンドがメインなのですが、フロントエンドの勉強がてらVue.jsをやってみます開発環境
- Vue CLI 3
- Vuex
- ESLint + Prettier
- BootstrapVue
- Qiita API v2
画面遷移しないのでVue Routerは使用しません
成果物
Qiita APIを利用してタグ検索できます
ソースコード
src/main.jsimport Vue from "vue"; import App from "./App.vue"; import store from "./store"; import BootstrapVue from "bootstrap-vue"; import "bootstrap/dist/css/bootstrap.css"; import "bootstrap-vue/dist/bootstrap-vue.css"; Vue.use(BootstrapVue); Vue.config.productionTip = false; new Vue({ store, render: h => h(App) }).$mount("#app");src/App.vue<template> <div id="app"> <qiita-form /> <qiita-content /> </div> </template> <script> import Form from "./components/Form.vue"; import Content from "./components/Content.vue"; export default { name: "app", components: { "qiita-form": Form, "qiita-content": Content } }; </script>ストア
src/store.jsimport Vue from "vue"; import Vuex from "vuex"; Vue.use(Vuex); const fetchItmes = async value => { const response = await fetch( `https://qiita.com/api/v2/tags/${value}/items?page=1&per_page=20`, { mode: "cors" } ); return response.json(); }; export default new Vuex.Store({ state: { selected: null, items: [] }, getters: { selected: state => state.selected, items: state => state.items }, mutations: { selected(state, selected) { state.selected = selected; }, items(state, items) { state.items = items; } }, actions: { onChange({ commit }, value) { commit("selected", value); }, async search({ commit, state }) { commit("items", []); const items = await fetchItmes(state.selected); commit("items", items); } } });コンポーネント
src/components/Form.vue<template> <div class="sticky-top"> <b-card> <b-form-group label="Qiitaくらいあんと: タグを付けた日時の降順で20件取得します" > <b-form-select :options="options" v-model="selected"> <template slot="first"> <option :value="null" disabled>-- 選択してください --</option> </template> </b-form-select> </b-form-group> <b-button @click="search" :disabled="isDisabled" variant="info" block >検索</b-button > </b-card> </div> </template> <script> export default { name: "Form", computed: { selected: { get() { return this.$store.getters.selected; }, set(value) { this.$store.dispatch("onChange", value); } }, isDisabled() { return this.$store.getters.selected === null; }, options: () => [ { value: "javascript", text: "JavaScript" }, { value: "typescript", text: "TypeScript" }, { value: "elm", text: "Elm" } ] }, methods: { search() { this.$store.dispatch("search"); } } }; </script>src/components/Content.vue<template> <div> <b-list-group v-for="item in items" :key="item.id"> <qiita-item :item="item" /> </b-list-group> </div> </template> <script> import Item from "./Item.vue"; export default { name: "Content", components: { "qiita-item": Item }, computed: { items() { return this.$store.getters.items.filter(item => !item.private); } } }; </script>src/components/Item.vue<template> <b-list-group-item> <div> <b-link :href="item.url" target="_blank">{{ item.title }}</b-link> <small>{{ updateAt }}</small> </div> <div> <span v-for="tag in item.tags" :key="tag.name"> <b-badge variant="info" pill>{{ tag.name }}</b-badge> </span> </div> <div> <small :id="userName">{{ `by ${userName}` }}</small> </div> </b-list-group-item> </template> <script> export default { name: "Item", props: { item: { url: String, title: String, updated_at: String, tags: Array, user: { name: String, id: String } } }, computed: { updateAt() { const date = new Date(this.item.updated_at); return ` (最終更新日: ${date.toLocaleDateString()})`; }, userName() { return this.item.user.name || `@${this.item.user.id}`; } } }; </script>
- 投稿日:2019-07-04T22:50:31+09:00
WordPressでjsやcssを読み込むときにキャッシュを更新させる対策
概要
WordPress開発をしているとき、jsやcssを編集したのに表示が変わっていないぞという経験は、ありませんか?
wp_enqueue_script()やwp_enqueue_style()関数で、jsとcssファイル編集後にキャッシュを更新させる対策を紹介します。
ブラウザキャッシュとは
chromeやfirefoxなどの多くのブラウザには、Webページの表示高速化を目的にキャッシュという機能があります。
このキャッシュは、Webページにアクセスした時、ブラウザで見える内容のデータ(js、css等)をキャッシュして表示を速くします。便利な機能ですが、jsやcssを編集してあと、Webページを更新してみると、表示が変わっていないことがあります。
これは、ブラウザにキャッシュされた古いjsとcssを読み込んでいるためです。キャッシュを更新して最新のページを表示させる対策として、以下があります。
①スーパーリロードをする。
スーパーリロードとは、ブラウザのキャッシュを無視してWebページの最新のデータを読み込む機能です。
これによって、キャッシュが上書きされ、最新のWebページが表示されます。ただ、サイトを閲覧している人誰もがスーパーリロードを知っているとは限らないし、jsやcssが変わっていることを知りません。
誰もが最新の状態でWebページを見ることができるように開発者側が配慮する必要があります。
そのため、この方法は好ましくありません。chromeスーパーリロードのショートカット
// Windows版 Shift + F5 // Mac版 Cmd + Shift + R②クエリパラメータにタイムスタンプを指定する。
クエリパラメータにタイムスタンプを指定することで、プラウザが別のファイルとして認識され、最新のjsとcssが読み込まれます。
タイムスタンプの指定例
// js <script type="text/javascript" src="example.js?date=20190101120000"></script> // css <link rel="stylesheet" type="text/css" href="style.css?date=20190101120000">パラメータ名は、特に決まりはありません。書かなくても動作します。
今回は、分かりやすいようにパラメータ名をdateにしています。
あとは、jsとcssファイル更新日時のタイムスタンプを動的に変えてやればOKです。WordPressでブラウザキャッシュ対策
WordPressでも簡単にキャッシュ対策できます。
jsとcssを読み込むwp_enqueue_script()やwp_enqueue_style()関数には、第4引数にバージョンを指定するパラメータが用意されています。バージョンを指定しない場合、デフォルトで設定されるWordPressのバージョンが指定されるようです。
そのため、jsとcssを編集しても、WordPressをアップデートされない限りは、最新のjsとcssが読み込まれないことになります。
バージョンにタイムスタンプを指定することで、上記で紹介したクエリパラメータにタイムスタンプを指定する対策と同様のことが可能です。タイムスタンプの指定例
// js wp_enqueue_script( 'example', plugins_url( '/js/example.js', __FILE__ ), array(), date_i18n( 'YmdHis', filemtime( plugins_url( '/js/example.js', __FILE__ ) ) ) ); // css wp_enqueue_style( 'style', plugins_url( '/css/style.css', __FILE__ ), array(), date_i18n( 'YmdHis', filemtime( plugins_url( '/css/style.css', __FILE__ ) ) ) );filemtime関数でファイルの更新日時のタイムスタンプを取得しています。
ちなみに、以下方法でも可能のようです。
こちらの方法では、date以外にデフォルトで設定されるWordPressのバージョンのクエリパラメータも追加で付与されるようです。
dateのクエリパラメータだけを表示させたい場合は、上記の方法が良いです。// js wp_enqueue_script( 'example?date=' . date_i18n( 'YmdHis', filemtime( plugins_url( '/js/example.js', __FILE__ ) ) ), plugins_url( '/js/example.js', __FILE__ ) ); // css wp_enqueue_style( 'style?date=' . date_i18n( 'YmdHis', filemtime( plugins_url( '/css/style.css', __FILE__ ) ) ), plugins_url( '/css/style.css', __FILE__ ) );参考URL(WordPress Codex)
wp_enqueue_script関数
https://wpdocs.osdn.jp/%E9%96%A2%E6%95%B0%E3%83%AA%E3%83%95%E3%82%A1%E3%83%AC%E3%83%B3%E3%82%B9/wp_enqueue_script
wp_enqueue_style関数
https://wpdocs.osdn.jp/%E9%96%A2%E6%95%B0%E3%83%AA%E3%83%95%E3%82%A1%E3%83%AC%E3%83%B3%E3%82%B9/wp_enqueue_style
- 投稿日:2019-07-04T22:23:57+09:00
redux-form (5) - Initialize From State
redux-form (1) - Simple Form Example
redux-form (2) - Synchronous Validation Example
redux-form (3) - Field-Level Validation Example
redux-form (4) - Submit Validation Example
redux-form (5) - Initialize From State
ReactでForm componentを作るときに、とても便利なredux-formの説明です。
redux-formの概説についてはまず以下の記事を参考にしてください。
redux-form (1) - Simple Form Example
Initialize From State
Initialize From State - Getting Started With redux-form
redux-formでは、decorated form component(wrapped component)のform stateを初期化するために、initialValuesを用います。initialValuesは、例えばサーバから取得した値を、React Redux の connect()を使って、mapStateToPropsでRedux state をinitialValues propに変換して、decorated form componentに渡すようにします。
initialValuesでformを初期化できるのは、デフォルトで1回のみです。
decorated form componentはreset() propを受け取りますが、reset()を実行することでformをinitialValuesに戻すことができます。
以下が、reducerとaction creatorのソースです。
src/account.js// Quack! This is a duck. https://github.com/erikras/ducks-modular-redux const LOAD = 'redux-form-examples/account/LOAD' const reducer = (state = {}, action) => { switch (action.type) { case LOAD: return { data: action.data } default: return state } } /** * Simulates data loaded into this reducer from somewhere */ export const load = data => ({ type: LOAD, data }) export default reducer以下がredux-formのサンプルです。ポイントの説明を下にまとめてあります。
src/InitializeFromStateForm.jsimport React from 'react' import { connect } from 'react-redux' import { Field, reduxForm } from 'redux-form' import { load as loadAccount } from './account' const data = { // used to populate "account" reducer when "Load" is clicked firstName: 'Jane', lastName: 'Doe', age: '42', anniversaryDate: '2018-08-22', sex: 'female', employed: true, favoriteColor: 'Blue', bio: 'Born to write amazing Redux code.' } const colors = ['Red', 'Orange', 'Yellow', 'Green', 'Blue', 'Indigo', 'Violet'] let InitializeFromStateForm = props => { const { handleSubmit, load, pristine, reset, submitting } = props return ( <form onSubmit={handleSubmit}> <div> <button type="button" onClick={() => load(data)}> Load Account </button> </div> <div> <label>First Name</label> <div> <Field name="firstName" component="input" type="text" placeholder="First Name" /> </div> </div> <div> <label>Last Name</label> <div> <Field name="lastName" component="input" type="text" placeholder="Last Name" /> </div> </div> <div> <label>Age</label> <div> <Field name="age" component="input" type="number" placeholder="Age" /> </div> </div> <div> <label>Anniversary Date</label> <div> <Field name="anniversaryDate" component="input" type="date" /> </div> </div> <div> <label>Sex</label> <div> <label> <Field name="sex" component="input" type="radio" value="male" />{' '} Male </label> <label> <Field name="sex" component="input" type="radio" value="female" />{' '} Female </label> </div> </div> <div> <label>Favorite Color</label> <div> <Field name="favoriteColor" component="select"> <option value="">Select a color...</option> {colors.map(colorOption => ( <option value={colorOption} key={colorOption}> {colorOption} </option> ))} </Field> </div> </div> <div> <label htmlFor="employed">Employed</label> <div> <Field name="employed" id="employed" component="input" type="checkbox" /> </div> </div> <div> <label>Bio</label> <div> <Field name="bio" component="textarea" /> </div> </div> <div> <button type="submit" disabled={pristine || submitting}> Submit </button> <button type="button" disabled={pristine || submitting} onClick={reset}> Undo Changes </button> </div> </form> ) } // Decorate with reduxForm(). It will read the initialValues prop provided by connect() InitializeFromStateForm = reduxForm({ form: 'initializeFromState' // a unique identifier for this form })(InitializeFromStateForm) // You have to connect() to any reducers that you wish to connect to yourself InitializeFromStateForm = connect( state => ({ initialValues: state.account.data // pull initial values from account reducer }), { load: loadAccount } // bind account loading action creator )(InitializeFromStateForm) export default InitializeFromStateFormReact-Reduxのconnect()関数について少し説明します。
mapStateToProps で initialValues propが設定されています。initialValues は reduxForm が処理します。initialValues は { field1: 'value1', field2: 'value2' } の形をしており、componentWillMount() において form を初期化します。
initialValuesはここの例のようにpropで受け渡す以外に、reduxForm() のconfig parameterで受け渡す方法もあります。mapDispatchToProps は object を指定することができます。この場合、objectの各fieldがaction creatorとなり、 React-Redux は dispatchをaction creatorにbindします。
connect() -React ReduxInitializeFromStateForm = connect( state => ({ // mapStateToProps initialValues: state.account.data }), { load: loadAccount } // mapDispatchToProps )(InitializeFromStateForm)以下はindex.jsですが、オリジナルのものを最小化してあります。
src/index.jsimport React from 'react' import ReactDOM from 'react-dom' import { Provider } from 'react-redux' import { createStore, combineReducers } from 'redux' import { reducer as reduxFormReducer } from 'redux-form' import account from './account' const dest = document.getElementById('content') const reducer = combineReducers({ account, form: reduxFormReducer // mounted under "form" }) const store = createStore(reducer) const showResults = values => new Promise(resolve => { setTimeout(() => { // simulate server latency window.alert(`You submitted:\n\n${JSON.stringify(values, null, 2)}`) resolve() }, 500) }) let render = () => { const InitializeFromStateForm = require('./InitializeFromStateForm').default ReactDOM.hydrate( <Provider store={store}> <h2>Form</h2> <InitializeFromStateForm onSubmit={showResults} /> </Provider>, dest ) } render()実行画面
初期画面です
「Load Account」ボタンでformを初期化します。最初の一回目だけ有効です。初期状態を変更してから、再度初期状態に戻すためには、UndoChangesボタンでreset()を発行する必要があります。
submitボタンを押します。
今回は以上です。
- 投稿日:2019-07-04T22:06:00+09:00
jQueryで表示非表示活性非活性する方法
表示になっているものを非表示にする
$(要素).hide(); $(要素).toggle(false);非表示になっているものを表示する
$(要素).show(); $(要素).toggle(true);実行されるたびに表示と非表示を切り替える
$(要素).toggle();活性化
$(要素).prop('disabled', false); // disabled属性を削除する $(要素).removeAttr('disabled');非活性化
$(要素).prop('disabled', true);注意1 : disabled属性のある要素はサーバへ送られない
注意2 : attr()は使わない
attrで活性非活性の切り替えは古いjQueryならできるらしいけど、ソースにあるとわかりにくいから使わない方が良いと思うattr(属性, 値)で、属性に値を設定する
$(要素).attr('disabled', true);は、disabled属性の値をtrueにするということ$(要素).attr('disabled', false);は、disabled属性の値をfalseにするということ- disabled属性は「存在すれば非活性」「存在しなければ活性」なので属性の値はどうでもいい
/** これは全部非活性になる. */ <input type="text" disabled> <input type="text" disabled="disabled"> <input type="text" disabled="true"> <input type="text" disabled="false">出展
- 【jQuery】要素の表示・非表示について (show, hide, toggle) - TASK NOTES
- jQueryのprop()でdisabled属性を切り替える - Qiita
- jQuery - attr()とprop()の違いと使い分け - Miuran Business Systems
他のことを知りたい
JavaScriptの実装に役立つリンク集 - Qiita
JavaScriptでファイルの入出力をしたい時に役立つリンク集 - Qiita
JavaScriptでいろいろ取得したい時に見てみるリンク集 - Qiita
- 投稿日:2019-07-04T21:13:50+09:00
JavaScriptでいろいろ取得したい時に見てみるリンク集
要素のidを取得したい
/** jQuery. */ let id = $(要素名).attr('id'); // onClickイベントなどで let id = $(this).attr('id');ラジオボタンの選択値を取得したい
/** 普通. */ let radios = document.getElementsByName("name属性"); let selected; for (let i = 0; i < radios.length; i++){ if(radios[i].checked){ selected = radios[i].value; break; } } /** jQuery. */ let selected = $('input[name="name属性"]:checked').val();
input type="hidden"の値を取得したい/** jQuery. */ // name属性指定 let a = $('input:hidden[name="name属性"]').val(); // id属性指定 let d = $('#id属性').val();家族要素を取得したい
- jQueryで親要素や子要素を取得する方法 | tagamidaiki.com
- jQueryで親要素を取得する:parent(), parents(), closest() | UX MILK
- jQueryで子要素を取得する方法:children(), find() | UX MILK
jQueryを使って要素を取得したい
- jQueryでdocument.getElementByIdしたいです - ゆとり日記
- document.getElementById('id') と $('#id') が同じだと思っていて、ドハマリしました
![]()
- Bugle Diary: [jQuery]inputのname属性にアクセス
- jQueryでclassやidをワイルドカード指定 - Qiita
jQueryを使わないで要素を取得したい
他のことを知りたい
JavaScriptの実装に役立つリンク集 - Qiita
JavaScriptでファイルの入出力をしたい時に役立つリンク集 - Qiita
jQueryで表示非表示活性非活性する方法 - Qiita
- 投稿日:2019-07-04T21:02:52+09:00
Ajaxのフワッと理解
注意事項
この記事は、「ぶっちゃけ下書きが溜まってきて処分したかったけど消すのはもったいない」という筆者の気まぐれで作成された記事である。
某5才児教育番組みたいなノリの記事なので真剣にAjaxを学びたい人は回れ右でお願いします。
この記事を読んで得られるものは、ふわっとしたAjaxの理解だけです。Ajaxってなに?
javascriptでWebページを再読込(ページ更新処理)せずにページの内容を変更する技術のこと。
名前の由来は「Asynchronous JAvascript + Xml」の頭文字を取ったもの。そもそもAjaxを使うと何ができるの?
画面をポチポチしても、画面が固まらなくなるよ!
何で画面をポチポチすると固まるの?
画面をポチポチすると、新しい画面のデータを取りに行くから、戻ってくるまで画面が固まるんだよ!
じゃあ何でAjaxだと固まらないの?
見えないところで新しい画面のデータを取りに行って、今の画面にこっそり渡してるから固まらないんだよ!
ちょっとだけまじめに同期と非同期のお話
- 同期とは(画面が固まる方)
画面の更新処理(HTTPリクエスト)をサーバーに送信すると、サーバーから応答(HTTPレスポンス)が返ってくるまで処理を待機する(=画面操作ができなくなる)こと。- 非同期とは(画面が固まらない方)
画面の更新処理(HTTPリクエスト)をサーバーに送信しても、サーバーからの応答(HTTPレスポンス)の有無にかかわらず処理を継続する(=待機中も画面操作ができる)こと。Ajaxでできること(めっちゃざっくり)
Google検索のサジェスト、FXのチャートとかなんかリアルタイムで動いてるやつ。
- 投稿日:2019-07-04T20:19:11+09:00
AWSマネジメントコンソールの色をアカウントによって変更するChrome拡張機能を作った(実装編)
はじめに
先日、AWSマネジメントコンソールの色をアカウントによって変更するChrome拡張機能を作成してリリースしました。
機能や使い方はこちらの記事をご覧下さい。
AWSマネジメントコンソールの色をアカウントによって変更するChrome拡張機能を作った(紹介編)実装詳細
オプション画面はjqueryを使用して値を取得しています。
input要素を適当に配置しただけのHTMLですので特記する事はありません。
AWSのサイトで起動するスクリプトはJavaScriptです。ソースはこちらに
https://github.com/sh-nakayama/AWS_COLOR_CHANGER苦労した点
オプション画面で入力した値を保存して、AWSのマネコンのサイトで起動するJavaScriptに読み込ませるのに苦労しました。
最初はオプション画面で入力した値をlocalstrageに保存していましたが、
localStorageはページごとに保存するデータが分かれている事が判明、読み書きは同じ場所でないと値が共有できません。つまりAWSのマネコンのサイトで起動するJavaScriptではlocalStorageに保存した値が読めません。
回避方法はあるみたいですが、
https://mae.chab.in/archives/2861苦労した点の対応
今回はchrome.storageを使用することで対応しました。
下記の記事を参考に作成しました。
https://easyramble.com/chrome-storage-set-and-get.html
https://qiita.com/shimutaya/items/e8835d6ce794ef6c73cf終わりに
意外と簡単にリリースまでこぎつけました。
実は最も苦労した点はmanifestファイルかもしれません。スペルミスや、インデントミスでかなり時間を取られました参考にさせていただいた記事
- 投稿日:2019-07-04T19:43:59+09:00
Google App ScriptでDriveAppを使わずに、自動でファイルダウンロードする機能を作った
概要
GASでJsonのダウンロードができれば良いなと思っていたが、調べていると大体、一旦Google Driveに入れろということが書いてありました。
しかし、いろいろ面倒なのでなにか他に手段が無いかと調べていたところ、GASでHTMLを表示できるということなので、DataURL使えばいいじゃんと思ったので、やってみました。
GASの設定など、TypeScriptでの環境構築などは他でいろいろ紹介されていると思うので、そちらを参照してください。
今回紹介するのは単純にダウンロード部分です。GAS上でのプロジェクトの構成
以下のようになっているとします。
. ├── json.gas ├── main.gas └── download.html中身
HTMLを表示するための関数とJsonを作成する用の関数です。
webpackでトランスパイルされて、json.gas になる想定です。そのため、globalで定義します。json.tsdeclare let global: any; global.body = "" // HTML上で参照される変数 function() { // この関数がJson文字列を作成すると仮定する。この中でspread sheetを読み込むでもよしdocumentを読み込むでもよし return JSON.stringify({ firstName: "john", lastName: "satoh" }, null, 2); } function jsonDownloadDialog() { global.body = createJson(); const name = "jsonの中身"; // ダイアログの名前 var output = HtmlService.createTemplateFromFile('download'); //同じディレクトリにあるHTMLを参照している var html = output.evaluate().setHeight(420).setWidth(600); var ui = SpreadsheetApp.getUi(); ui.showModalDialog(html, name); } global.jsonDownloadDialog = jsonDownloadDialog;こちらは「JSON」→「ダウンロード」というメニューを用意するためのスクリプトです。
こちらはwebpackに含まれない想定です。(globalに定義しても呼ばれなかったので別にしました。main.jsfunction onOpen() { var spreadsheet = SpreadsheetApp.getActiveSpreadsheet(); var entries = [{ name : "ダウンロード", functionName : "jsonDownloadDialog" // globalに定義されているjson.gasの関数を参照している }]; spreadsheet.addMenu("JSON", entries); }こちら作成したJsonを表示し、さらに自動的にファイルをダウンロードするスクリプトが入っているHTMLです。
#jsonの中身をFileとしてObjectURLにしている感じです。download.html<!DOCTYPE html> <html lang="jp"> <head> <base target="_top"> <script> window.addEventListener('load', function() { console.log(document.querySelector("#json").textContent) var link = document.createElement('a'); link.download = 'data.json'; // ファイル名 var blob = new Blob([document.querySelector("#json").textContent], {type: 'text/plain'}); link.href = window.URL.createObjectURL(blob); link.text = "もう一回ダウンロード"; document.querySelector("#download_area").appendChild(link); link.click(); // aタグを叩く }); </script> </head> <body> <pre id="json"><? output.append(body); ?></pre> <div id="download_area"> </div> </body> </html>出来上がり
まとめ
GAS面白い。
claspでhtmlがあげられるのを知りませんでしたし、メニューを追加できるのも知りませんでした。。。
これなら本当に小さいアプリケーションは作れてしまいますね。。。
- 投稿日:2019-07-04T17:19:45+09:00
v-forを使う時、配列に初期値があると初めから画面に表示されてしまう問題
vueでtodoリストを作る際に、v-forを使い配列の中身を順に表示させようとしたのですが、ボタン要素を作る為にhtmlに
<button>を記述したところ、初期の状態からボタン要素が見えてしまっている不備があった為、少し躓いたので解決策を記載します。html
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> <link rel="stylesheet" href="style.css"> </head> <body> <div id="app"> <h1>ToDoList</h1> <table> <thead> <tr> <th>コメント</th> <th>状態</th> </tr> </thead> <tbody> <tr v-for="todo in todos" :key="todo.value"> <td>{{ todo.item }}</td> <td><button @click="">{{ todo.state }}</button></td> </tr> </tbody> </table> <input type="text" v-model="newItem"> <button @click="addItem">追加</button> </div> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script src="script.js"></script> </body> </html>ここは問題なしです。
javascript(vue) 不備があったコード。
let vm = new Vue({ el: '#app', data: { newItem: '', todos: [{ item: this.newItem, state: '' }] }, methods: { //タスクを追加する関数 addItem: function () { this.todos.push({item: this.newItem, state: '作業中'}) } } });このコードだと、htmlに初めからボタン要素が見えてしまいます。
じゃあhtmlの<button>を消して、javascriptの関数内でボタン要素を作り、それを配列todosにpushしよう!
と思ったのですが、vueには生のjavascriptでお世話になったdocument.createElementに相当するものは無いようなので、少し躓いてしまいました。難しく考えていましたが解決策は非常にシンプルでした。
解決したコード
let vm = new Vue({ el: '#app', data: { newItem: '', todos: [] }, methods: { //タスクを追加する関数 addItem: function () { this.todos.push({item: this.newItem, state: '作業中'}) } } });【解決策】
配列todosを空にするだけです。【原因】
htmlに初めからボタン要素が表示されていた原因は、配列todosのボタン要素になるプロパティstateに初期値を与えてしまったせいです。
ですので配列todosを空にし、関数内でpushする時にプロパティを作ってあげれば解決です。
(プロパティitemは今回のボタン要素の表示には関係ありませんが、同じ関数内で一緒にpushしてあげた方がスマートで保守性と可読性も上がると思います。)
以上です。
よく考えれば当たり前なのに理解不足もあり少し時間を取られたので記載しておきました。
補足や訂正などありましたら、ぜひご教授いただければ嬉しいです。
最後まで見ていただきありがとうございます。
- 投稿日:2019-07-04T17:19:45+09:00
v-forを使う時、配列に初期値があると初めから画面に表示されてしまう
vueでtodoリストを作る際に、v-forを使い配列の中身を順に表示させようとしたのですが、ボタン要素を作る為にhtmlに
<button>を記述したところ、初期の状態からボタン要素が見えてしまっている不備があった為、少し躓いたので解決策を記載します。html
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> <link rel="stylesheet" href="style.css"> </head> <body> <div id="app"> <h1>ToDoList</h1> <table> <thead> <tr> <th>コメント</th> <th>状態</th> </tr> </thead> <tbody> <tr v-for="todo in todos" :key="todo.value"> <td>{{ todo.item }}</td> <td><button @click="">{{ todo.state }}</button></td> </tr> </tbody> </table> <input type="text" v-model="newItem"> <button @click="addItem">追加</button> </div> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script src="script.js"></script> </body> </html>ここは問題なしです。
javascript(vue) 不備があったコード。
let vm = new Vue({ el: '#app', data: { newItem: '', todos: [{ item: this.newItem, state: '' }] }, methods: { //タスクを追加する関数 addItem: function () { this.todos.push({item: this.newItem, state: '作業中'}) } } });このコードだと、htmlに初めからボタン要素が見えてしまいます。
じゃあhtmlの<button>を消して、javascriptの関数内でボタン要素を作り、それを配列todosにpushしよう!
と思ったのですが、vueには生のjavascriptでお世話になったdocument.createElementに相当するものは無いようなので、少し躓いてしまいました。難しく考えていましたが解決策は非常にシンプルでした。
解決したコード
let vm = new Vue({ el: '#app', data: { newItem: '', todos: [] }, methods: { //タスクを追加する関数 addItem: function () { this.todos.push({item: this.newItem, state: '作業中'}) } } });【解決策】
配列todosを空にするだけです。【原因】
htmlに初めからボタン要素が表示されていた原因は、配列todosのボタン要素になるプロパティstateに初期値を与えてしまったせいです。
ですので配列todosを空にし、関数内でpushする時にプロパティを作ってあげれば解決です。
(プロパティitemは今回のボタン要素の表示には関係ありませんが、同じ関数内で一緒にpushしてあげた方がスマートで保守性と可読性も上がると思います。)
以上です。
よく考えれば当たり前なのに理解不足もあり少し時間を取られたので記載しておきました。
補足や訂正などありましたら、ぜひご教授いただければ嬉しいです。
最後まで見ていただきありがとうございます。
- 投稿日:2019-07-04T17:06:35+09:00
axiosでネットワークエラー・タイムアウトエラーをハンドルする
axiosでネットワークエラー・タイムアウトエラーをハンドルする場合、以下のように判定可能です。
axios.interceptors.response.use( function(error) { //タイムアウトの場合 const isTimeout = error.code === "ECONNABORTED"; if (isTimeout) { //do something return; } //ネットワークエラーの場合 if (!error.response) { //do something return; } } );
- 投稿日:2019-07-04T16:43:01+09:00
【GAS Webアプリ】ホワイトリストを使ってWebページのアクセス制御する
はじめに
Google Apps Scriptを使えばウェブアプリケーションを簡単に作成できる。
Gsuiteのユーザーの場合、[アプリケーションにアクセスできるユーザー]をGsuiteユーザー内で公開できるが、「全員」に公開した場合細かな アクセス制御 がGUIからできないので、それを簡単に実装するコードやり方
前準備
シート1にアクセス権限を管理するデータを入力する。
statusがokのユーザのみがアクセスできる。
ソースコード
スプレッドシートの[ツール]→[スクリプトエディタ]を開き以下のようにファイルを作成する。
index.htmlは表示したいHTMLファイル
error.htmlはアクセス権限の内ユーザーに対するHTMLファイル※HTMLファイルの中身は自由に変えてもらって大丈夫です。
index.html<!DOCTYPE html> <html> <head> <base target="_top"> </head> <body> <h1>こんにちは世界!</h1> </body> </html>error.html<!DOCTYPE html> <html> <head> <base target="_top"> </head> <body> <h1>アクセス権限のないユーザーです。</h1> </body> </html>コード.gs//現在ログインしているユーザーのメールアドレスを取得 LOGIN_USER = Session.getActiveUser().getEmail(); function doGet(e) { //ログインしているユーザーのメールアドレスを使ってアクセス権を確認 if(getUserData(LOGIN_USER).status == 'ok'){ htmlOutput = HtmlService.createTemplateFromFile("index").evaluate(); }else{ htmlOutput = HtmlService.createTemplateFromFile("error").evaluate(); } htmlOutput .setTitle('メールアドレスでアクセス権限してみる') .addMetaTag('viewport', 'width=device-width, initial-scale=1') return htmlOutput } //シート名を指定してシートを取得 function getSheetData(sheetName) { var sheet = SpreadsheetApp.getActive().getSheetByName(sheetName); var rows = sheet.getDataRange().getValues(); var keys = rows.splice(0, 1)[0]; return rows.map(function(row) { var obj = {}; row.map(function(item, index) { obj[String(keys[index])] = String(item); }); return obj; }); } //メールアドレスをしていしてユーザーデータを返す function getUserData(email) { var user_data = getSheetData('シート1'); var user = user_data.filter(function(value) { return email == value.email; }); if (user[0] != undefined) { return user[0]; } Logger.log('そのメールアドレスに対応するユーザーは存在しません。'); return {status: 'ng'}; }[公開]→[ウェブアプリケーションとして導入]を押して、承認画面がいろいろ出てくるので、承認してから
アプリケーションにアクセスできるユーザーを全員にすると使えます!
もちろんGsuite内のユーザーの中で更にアクセス制御・制限したい場合でも使えます。お疲れ様です
うまく行けば以下のように表示されるはずです!
うまくいかない場合は アプリケーションのURL末尾が
/execになっているか確認してください。/devの場合はテストモードなのでスクリプトエディタにアクセスできるユーザーではないとアクセスできません。
- 投稿日:2019-07-04T15:28:03+09:00
YouTubeで再生範囲指定したプレイリスト的なものを作った
Why use IFrame API
初めは、Data APIのplaylistItemsを使おうと思ってたんです。そしたら、すでに廃止済み(予定?)だったらしいです。(そうなら、Referenceから消てくれ~)
それで、いろいろ探した結果IFrame APIにたどり着きました。つくったもの
AZKi Live Looper
推しのラジオのライブパートをループするページです。
動画下のボタンで何回目のラジオか選べます。
それだけ!プレイヤーの設定
メインとなるjsは公式から4.までほぼコピペです。プレイヤーのサイズとかは適宜設定してください。
それと、動画のIDと開始時間、終了時間が必要なので次のようなjsonファイルを用意しました。
LiveData.json{ "contents":[ { "video":"VIDEO_ID", "start":"2000", "end":"3060" }, 以下同様に作っていく ] }読み込みはjQuery(参考ページ)で
player.jsvar liveData = []; $.getJSON('./LiveData.json', function (data) { liveData = data['contents']; });LiveData.jsvar player; var count = 0; function onYouTubeIframeAPIReady() { player = new YT.Player('player', { videoId: liveData[count].video, //変更箇所 playerVars: { start: liveData[count].start, end: liveData[count].end }, events: { 'onReady': onPlayerReady, 'onStateChange': onPlayerStateChange } }); }変更点は、
playerVarsを加えたことで、この中で各パラメータが設定できるのでstart(開始時間)とend(終了時間)を設定しました。両方とも動画の頭からの秒数を文字列で指定します。
これでとりあえず最初の動画が表示出来ます。次の動画の読み込み
プレイヤーで動画の再生が終了したときに次の動画を読み込めるように、次のような
onPlayerStateChangeを定義します。player.jsvar done = false; function onPlayerStateChange(event) { if (event.data == 0 && !done) { count += 1; done = true; if (!(count < liveData.length)) { count = 0; } player.loadVideoById({ videoId: liveData[count].video, startSeconds: Number(liveData[count].start), endSeconds: Number(liveData[count].end) }); } else if (event.data == 1 && done) { done = false; } }
onPlayerStateChangeはプレイヤーの再生ステータスが変化したときに実行され、プレイヤーが停止したときにcountをインクリメントしてloadVideoByIdで次の動画を読み込み再生します。変数
doneは動画再生中はfalse、次の動画の読み込み中にtrueとなります。これがないとcountが2回インクリメントされてしまったりします。これでお気に入り部分の垂れ流しができました。
動画選択ボタン
オプションです。
ボタンを作ってindex.html<input type="button" value="#1" onclick="setLive(0)">こんな感じで、
loadVideoByIdで動画をロードさせるだけplayer.jsvar setLive = function (Num) { count = Num; player.loadVideoById({ videoId: liveData[count].video, startSeconds: Number(liveData[count].start), endSeconds: Number(liveData[count].end) });ソースコード
- 投稿日:2019-07-04T15:12:44+09:00
今更だけどfind()とchildren()の違いについて
もう使われなくなってきたjQueryですが、コードを読むときにやたらjQueryが使われている&Reactを用いた開発でもjQueryと共存ができるのでさきほど勉強をはじめました。
そこでfind()とchildren()が似てたので、使い分けについて説明していきます。find()とchildren()の違いとはズバリ
小要素を取得できる階層の違いです。
find()は小要素すべてを取得でき、children()は1階層下の要素のみ取得できます。HTMLが以下のときに
<div id="セレクタ"> <a>あいうえお</a> <div> <a>かきくけこ</a> </div> </div>find()の場合
$('セレクタ').find('a');で、親要素のセレクタ内にある小要素
すべての<a>を取得します。
なので、あいうえおとかきくけこ両方取得できます。children()の場合
$('セレクタ').children('a');で、親要素のセレクタ内にある
1階層下の小要素<a>のみ取得します。
なので、あいうえおのみ取得できます。
- 投稿日:2019-07-04T13:54:50+09:00
Laravel MixのIE11対応
LaravelMixのextensionにlaravel-mix-polyfilltというものがあり、簡単に使えたので書いておきます。
https://laravel-mix.com/extensions/polyfill
laravel-mix-polyfillのインストール
npm install laravel-mix-polyfill --save-devlaravel-mix-polyfillを使うための設定
let mix = require('laravel-mix'); require('laravel-mix-polyfill'); mix.js('resources/js/app.js', 'public/js') .sass('resources/sass/app.scss', 'public/css') .polyfill({ enabled: true, useBuiltIns: "usage", targets: {"firefox": "50", "ie": 11} });
- 投稿日:2019-07-04T13:41:18+09:00
[JS]連想配列に任意の値と等しい要素があれば、それらのインデックスを全て取ってくるサンプル~flatMap編~
いきなり結論コード
See the Pen 配列の一致インデックス取得flatMap版 by riotam (@riotam4) on CodePen.
解説
キモになる部分return this.users.flatMap((v,k) => v.gender === '男' ? k : [])実際に処理を行なっているのは、↑この部分です。
this.usersにデータが入っていて、それをflatMapしています。
基本的には関連記事のmap版と似ているのですが、1つだけ簡単に書ける部分があります。map版との違い
map版だとこんな感じlet idx = [] this.users.map((v,k) => v.gender == '男' ? idx.push(k) : []) return idxズバリ、map版ではidx変数を宣言してそこにpushしているのですが、これが自体がいらなくなります。
逆に結論から説明すると、
flatMap版return this.users.flatMap((v,k) => v.gender === '男' ? k : [])これを
mapに書き換えたら…return this.users.map((v,k) => v.gender === '男' ? k : []) // => [ [], 1, 2 ]こうなっちゃいます。
[]を''にしたら[ '', 1, 2 ]と、
[]をnullにしたら[ null, 1, 2 ]と、返されちゃいます。
mapはたとえnullでも、何かしらの値を入れた配列を作りますが、
対してflatMapはfalsyな値なら無しにしてくれます。便利。最後に
最後までありがとうございました。
関連記事
同じ挙動を、mapメソッドで作ったのがこちら
- 投稿日:2019-07-04T13:17:32+09:00
[JS]連想配列に任意の要素があれば、それらのインデックスを全て取ってくるサンプル~map編~
いきなり結論コード
See the Pen 配列の一致インデックス取得map版 by riotam (@riotam4) on CodePen.
解説
キモになる部分let idx = [] this.users.map((v,k) => v.gender == '男' ? idx.push(k) : []) return idxまず、idxという変数を用意。
this.usersにデータが入っているので、それらをmapします。mapメソッドの引数は、関数型で書く必要があり、アロー関数と三項演算子でワンライナーにしてしまってます。
要は、this.usersをv(value)とk(key)にして順番に呼び出す。
→そのvのgenderキーが'男'と等しければ、idx変数にpushしていく。そうじゃなければ、という感じ。ちなみに
[]部分はnullや''、0など、falsyなものなら、挙動に問題はありません。最後に
さいごまでありがとうございました。
関連記事
- 投稿日:2019-07-04T13:17:32+09:00
[JS]連想配列に任意の値と等しい要素があれば、それらのインデックスを全て取ってくるサンプル~map編~
いきなり結論コード
See the Pen 配列の一致インデックス取得map版 by riotam (@riotam4) on CodePen.
解説
キモになる部分let idx = [] this.users.map((v,k) => v.gender == '男' ? idx.push(k) : []) return idxまず、idxという変数を用意。
this.usersにデータが入っているので、それらをmapします。mapメソッドの引数は、関数型で書く必要があり、アロー関数と三項演算子でワンライナーにしてしまってます。
要は、this.usersをv(value)とk(key)にして順番に呼び出す。
→そのvのgenderキーが'男'と等しければ、idx変数にpushしていく。そうじゃなければ、という感じ。ちなみに
[]部分はnullや''、0など、falsyなものなら、挙動に問題はありません。最後に
さいごまでありがとうございました。
関連記事
同じ挙動のものをflatMapで作ってみました。こちら
- 投稿日:2019-07-04T12:56:34+09:00
[JS]連想配列の任意要素に一致するインデックスを取得する際、findIndex関数だと1つしか取得できない件
今回したいこと
users:[ {name:'花子', age:'10', sex:'女'}, {name:'太郎', age:'11', sex:'男'}, {name:'次郎', age:'12', sex:'男'}, ],このデータから、sex='男'のuserのインデックスを取得したい。
つまり、1と2が変えればOK。findIndexを使ったサンプル
See the Pen findIndexサンプル by riotam (@riotam4) on CodePen.
ご覧の通り、男は次郎くんもいるのですが、帰ってくるインデックスは1…つまり、太郎君しか認識できません。
これは、よくない。fileterを使ったサンプル
See the Pen filterサンプル by riotam (@riotam4) on CodePen.
今度はfilterを使ってみました。
次郎君も認識できますが、インデックスを返してくれないので、微妙。結論
- findIndexは要素1つしか取れない
- 2つ目以降の一致したインデックスは無視されるので注意
- 全て取りたいなら、forEach等で回して、一致するインデックスを1つずつ配列にpushしていくパターンが早そう # 追記 Twitterで募集してみたが、やはり一発で全インデックスを取得できるメソッドはなさそうな。 みなさま、貴重なご意見ありがとうございました。
結局、map,flatMap,reduce等を使って作るのが良いとのこと。
また、サンプルコードつくります!
ありがとうございました。
- 投稿日:2019-07-04T12:56:34+09:00
[JS]連想配列に任意の値と等しい要素があるとき、findIndex関数だとインデックスが1つしか取得できない件
今回したいこと
users:[ {name:'花子', age:'10', sex:'女'}, {name:'太郎', age:'11', sex:'男'}, {name:'次郎', age:'12', sex:'男'}, ],このデータから、sex='男'のuserのインデックスを取得したい。
つまり、1と2が変えればOK。findIndexを使ったけど1つしか取れない
See the Pen findIndexサンプル by riotam (@riotam4) on CodePen.
ご覧の通り、男は次郎くんもいるのですが、帰ってくるインデックスは1…つまり、太郎君しか認識できません。
これは、よくない。fileterを使ったけど、インデックスが取れない
See the Pen filterサンプル by riotam (@riotam4) on CodePen.
今度はfilterを使ってみました。
次郎君も認識できますが、インデックスを返してくれないので、微妙。結論
- findIndexは要素1つしか取れない
- 2つ目以降の一致したインデックスは無視されるので注意
- 全て取りたいなら、forEach等で回して、一致するインデックスを1つずつ配列にpushしていくパターンが早そう
追記
Twitterで募集してみたが、やはり一発で全インデックスを取得できるメソッドはなさそうな。
みなさま、貴重なご意見ありがとうございました。結局、map,flatMap,reduce等を使って作る感じとのこと。
ということで、サンプルコードつくって関連記事に貼ってますので、参考にどうぞ。
ありがとうございました。関連記事
- 投稿日:2019-07-04T11:59:15+09:00
【React】最小限のreact-router実装で仕組みを理解する
概要
react-routerの使い方を理解するために最小限の実装をする
成果物
手順
- Reactの新規プロジェクト作成
- react-routerをインストール
- srcディレクトリの中身を削除
- index.jsとApp.jsを作成
1. Reactの新規プロジェクト作成
$ npx create-react-app hogehoge2. react-routerをインストール
$ cd hogehoge $ npm install react-router3. srcディレクトリの中身を削除
$ rm -rf ./src/*4. index.jsとApp.jsを作成
index.jsimport React from 'react'; import ReactDOM from 'react-dom'; import AppRouter from './App.js'; ReactDOM.render( <AppRouter />, document.getElementById('root') );App.jsimport React from 'react'; import { BrowserRouter as Router, Route, Link } from 'react-router-dom'; function Index(){ return <h2>Home page is here!</h2> } function About(){ return <h2>This is About page.</h2> } function Users(){ return <h2>This is User page.</h2> } function AppRouter(){ return( <Router> <div> <nav> <ul> <li> <Link to='/'>Home</Link> </li> <li> <Link to='/about/'>About</Link> </li> <li> <Link to='/users/'>Users</Link> </li> </ul> </nav> <Route path='/' exact component={Index} /> <Route path='/about/' component={About} /> <Route path='/users/' component={Users} /> </div> </Router> ); } export default AppRouter;Happy Hacking
!
参考
https://reacttraining.com/react-router/web/guides/quick-start
- 投稿日:2019-07-04T10:57:55+09:00
Vue.js・Nuxt.js ハマりポイントまとめ
Vue.js や Nuxt.js で開発していてハマったポイントを雑多にまとめています。(随時追加予定)
props を子コンポーネントにそのまま流したい
具体的な例をあげると、UI コンポーネントをラップしたコンポーネントを作りたい場合などです。
こういうときは、
$attrsを使うとよいです。
https://isoppp.com/note/2018-12-16/what-is-vue-attrs/computed は型アノテーションが必須
明らかに推論できるような場合でも、型アノテーションが必須です。
記述していない場合はエディタがエラーを表示します。computed: { fullName(): string { // 明らかに string を返すが、アノテーションは必須 return `${firstName} ${lastName}`; } }正しく Vue コンポーネントを記述しているのに何かエラーになる場合、これが原因のことが多いです。
リロードした時だけ動かない
具体的な例をあげると、「リロードした時だけ動かない。他のページから遷移してきた場合は動く。」のようなケースです。
これは、SSR と CSR に起因しているケースが多いです。
サーバーサイド(クライアントサイド)でしか実行してはいけない関数を実行してしまっていないか確認しましょう。例えば、サーバーサイドで window オブジェクトにアクセスしているとかです。テーブル周りでエラーになる
theadやtbodyを書けば直ることがあります。
Vue では、自動的にこれらを補完するため、SSR と CSR で DOM が一致せず、エラーになります。
- 投稿日:2019-07-04T09:45:59+09:00
JavaScriptで正規表現を使って対象文字列が意図した文字列か判定する方法
例えば6桁の半角英数字のみTrueにしたい場合、
const hoge = 'abc123'; const hogeRegExp = new RegExp('^[a-zA-Z0-9]{6}$'); if(hogeRegExp.test(hoge)){ console.log('true'); }else{ console.log('false'); } //trueが表示される
- 投稿日:2019-07-04T07:52:27+09:00
WSL2のコロコロ変わるIPをMyDNSで何とかする
1.WSL2のIPは起動するごとに変わる気まぐれさん
Windows10Build18922に搭載されているWSL2はお試しバージョンということだけあって、ネットワークに難があります。その最たるものは、起動するごとにプライベートIPが変化する部分です。これではsshやhttp接続の時に、面倒くさいことこの上ない状態となります。
IPを固定する方法を試行錯誤してみたのですが、ホストOS側の仮想ブリッジが再作成されてネットワークそのものが変えられてしまうため、小手先でどうにかなる問題ではありませんでした。
ということで対応策を考えた結果、ドメインと関連付ければ良いという結論に至りました。
2.そうだ、MyDNSを利用しよう
まずは、MyDNSでアカウントを作成し、ドメインを一つ設定します。ローカルで使うだけなので、無料の中から好きなものを選びましょう。
3.プログラムのインストール
お察しの通り、まっとうな方法ではプライベートIPを登録することは出来ないので、WSL上でnodeを入れてゴニョゴニョします。
sudo apt install nodejs npmwsl2mydnsをnpmからインストールします。これがゴニョゴニョするプログラムです。
sudo npm -g wsl2mydnsこれで必要なもののインストールは完了です。
4.Windows側でbatを作成
wsl_start2.bat@echo off start wsl wsl2mydns MyDNS-ID MyDNS-PASS pauseWSL2を使うときに、このbatをWindowから実行すればOKです。IPはMyDNSに通知され、ドメインとプライベートIPが結びつきます。
5.仕組み
MyDNSに通常の方法でIPを通知すると、自分の使っているネットワークのグローバルIPが設定されてしまいます。ということで、プログラムでWSL2上のeth0のIPを通知するようになっています。
ソースコードはGitHubに入れてあります。TypeScriptを使いました。
6.困ったこと
Windowsからビルドしたものをnpmに登録すると、コマンドとして実行させるための以下の行、
#!/usr/bin/env nodeここの改行コードに文句を言われ、コマンドが正常に実行できない状態になりました。おかげで何度もnpmに再アップする羽目になりました。
7.作成に使ったTypeScript
思い立ったが吉日で、さくっと作りました。MyDNSの操作ライブラリは以前から作ってあったので、DirectIP登録の部分だけ追加するだけで済みました。やはりTypeScriptを使うと、素のJavaScriptでやるよりも、プログラミングが楽です。
こちらにTypeScriptの入門ネタを作り始めました。時間のあるときに内容を書き足していきたいと思っています。ちなみにそのシステム自体も、フロントエンドとバックエンド両方ともTypeScriptで出来ています。
- 投稿日:2019-07-04T02:16:13+09:00
React HooksのみでWebアプリを作りました! ( 参考にどうぞ ! )
この記事について
私が作ったWebアプリをオープンソース化したので、そのWebアプリがどのような形で作られているかを解説した記事です。
オープンソース化した理由としては、これから
React Hooksを導入する人やWebアプリを作ってみたい人などの参考になればと思いオープンソース化しました。作ったもの
画像投稿ができるWebアプリです。
デモページ
ソースコード
製作期間
- 三か月ほど
実装した機能
- 認証機能
- 画像投稿機能
- いいね機能
- フォロー機能
- 通知機能
使用したものなど
- React
- React-Router
- dayjs
- TypeScript
- Firebase Authentication
- Firebase FireStore
- Firebase Storage
- Firebase Hosting
なぜ作ったのか?
前回の記事でJavaScriptで使えるフレームワークを作成したのですが、記事を投稿した後、中々上手い実装などが浮かばず、行き詰ってしまいました。
この状況から何とか抜け出そうと考えた結果、Webアプリを作ってみようと思い、出来たのが今回のWebアプリです。解説
ここからは、Webアプリがどのように出来ているかを解説したいと思います。
フォルダー構成について
│ ├─node_modules ├─public └─src ├─assets | └─icons ├─components │ ├─atoms │ ├─modules │ ├─molecules │ ├─organisms │ ├─pages │ └─templates ├─logics │ ├─actions │ ├─reducer │ └─util | └─uses └─testsフォルダーのみ表示してます
解説は、
srcフォルダー内のみにします。src/assets
画像などを保存するフォルダーです。
src/components
Reactコンポーネントを記述したファイルを入れるフォルダーです。
src/componentsのフォルダー構成は、Atomic Designに基づいて以下のようにコンポーネントのファイルを分割してます。components/atoms
一番小さいコンポーネントの単位です。
React Hooksを使って無いコンポーネントのみを入れるようにします。
atomsのコンポーネント例
atomsのコンポーネント例import React from "react"; import styles from "./LoadingBar.module.scss"; export const LoadingBar: React.FC<{ isLoading: boolean }> = ({ isLoading }) => { return isLoading ? <div className={styles.loading_bar} /> : null; };
components/molecules
atomsの次に小さいコンポーネントの単位です。
こちらも、atomsと同じくReact Hooksを使ってないコンポーネントのみを入れますが、
atomsとの違いは、要素数がatomsより多いコンポーネントになります。( 要素数がだいたい3~4個以上 )
moleculesのコンポーネント例
moleculesのコンポーネント例import React, { FormEvent } from "react"; import styles from "./SearchInput.module.scss"; interface SearchInputProps extends React.Props<{}> { maxWidth?: number; placeholder?: string; onSubmit: (inputValue: string) => void; } export const SearchInput: React.FC<SearchInputProps> = ({ onSubmit, maxWidth, placeholder = "キーワードを検索" }) => { const submit = (e: FormEvent<HTMLFormElement>) => { e.preventDefault(); onSubmit((e.currentTarget.search as HTMLInputElement).value); }; return ( <form className={styles.search__input} style={{ maxWidth }} onSubmit={submit} > <div className={styles.search_icon} /> <input type="text" name="search" autoComplete="off" placeholder={placeholder} /> </form> ); };
components/organisms
コンポーネントの大きさ関係なく
React Hooksを使っていれば問答無用でこちらのフォルダーに入れられます。
organismsのコンポーネント例
organismsのコンポーネント例import React from "react"; import { useUsers } from "../../../logics/util/uses/users"; import { useUtil } from "../../../logics/util/uses/util"; import { User } from "../../../types"; import { Button } from "../../atoms/Button"; interface FollowButtonProps extends React.Props<{}> { uid: User["id"]; className?: string; } export const FollowButton: React.FC<FollowButtonProps> = React.memo( ({ uid, className }) => { const { alerts } = useUtil(); const { isFollowee, current_user, followUser, unfollowUser } = useUsers(); const isFollowed = isFollowee(uid); const currentUserId = current_user ? current_user.id : ""; const onClick = () => { if (uid && currentUserId && currentUserId !== uid) { if (isFollowed) { unfollowUser(uid); } else { followUser(uid); } } else if (currentUserId) { alerts.follow_user.warn(); } else { alerts.required_login.warn(); } }; return ( <Button disabled={!uid} className={className} color={isFollowed ? "red" : "blue"} size="small" onClick={onClick} > {isFollowed ? "フォローを解除" : "フォローする"} </Button> ); } );
components/pages
一つのページを構成するコンポーネントを入れるフォルダーです。
pagesのコンポーネント例
pagesのコンポーネント例import React from "react"; import { RouteComponentProps } from "react-router"; import { Board } from "../../atoms/Board"; import { BoardItem, NotifyBoardItem } from "../../atoms/BoardItem"; import { DashboardTemplate } from "../../templates/DashboardTemplate"; import styles from "./Dashboard.module.scss"; import { useDashboard } from "./use"; export const Dashboard: React.FC<RouteComponentProps> = () => { const { notifies, finished, getNotifies } = useDashboard(); return ( <DashboardTemplate> <main className={styles.dashboard}> <Board headerLabel="通知一覧" minWidth={500}> {notifies.map(notify => ( <NotifyBoardItem key={`notify-item-${notify.id}`} notify={notify} /> ))} {finished ? null : ( <BoardItem className={styles.link} label="さらに10件の通知を表示" onClick={getNotifies} /> )} </Board> </main> </DashboardTemplate> ); };
components/templates
pagesでの共通部分を抜き出して、再利用できるようにしたコンポーネントを入れるフォルダーです。
templatesのコンポーネント例
templatesのコンポーネント例import React, { useEffect } from "react"; import useReactRouter from "use-react-router"; import { GlobalHeader } from "../../organisms/GlobalHeader"; import styles from "./PageTemplate.module.scss"; interface PageTemplateProps extends React.Props<{}> { title?: string; background?: string; className?: string; } export const PageTemplate: React.FC<PageTemplateProps> = ({ children, className, background }) => { const { location } = useReactRouter(); useEffect(() => { window.scrollTo(0, 0); }, [location.pathname]); return ( <div className={styles.page} style={{ background }}> <GlobalHeader /> <div className={`${styles.body} ${className || ""}`}>{children}</div> <div className={styles.footer}> <p>©︎ 2019 uttk</p> </div> </div> ); };
components/modules
上記のコンポーネントの分割に当てはまらないコンポーネントを入れるフォルダーです。
modulesのコンポーネント例
modulesのコンポーネント例import React, { useEffect, useState } from "react"; import { IconButton } from "../../atoms/Button"; import styles from "./Alerts.module.scss"; /** * 長いので省略 */ interface AlertProps extends React.Props<{}> {} export const Alert: React.FC<AlertProps> = React.memo(() => { const [alerts, setAlert] = useState<AlertElement[]>([]); const len = alerts.length; AlertsMg.elements = alerts; AlertsMg.dispatch = (elements: AlertElement[]) => { setAlert(elements); }; return ( <div> {alerts.map((alert, i) => ( <AlertBar key={alert.id} top={(len - i) * 64} alert={alert} displayTime={3000} /> ))} </div> ); }); interface AlertBarProps extends React.Props<{}> { top: number; alert: AlertElement; displayTime: number; } export const AlertBar: React.FC<AlertBarProps> = React.memo( ({ top, alert, displayTime }) => { const [style, setStyle] = useState(alert.style); useEffect(() => { let timeoutId = setTimeout(() => { setStyle(`${style} ${styles.alert_show}`); timeoutId = setTimeout(() => { setStyle(alert.style); timeoutId = setTimeout(alert.onClose, 300); }, displayTime); }, 0); return () => clearTimeout(timeoutId); }, []); return ( <li className={style} style={{ top }}> <p>{alert.message}</p> <IconButton size={16} icon="close_white" color="transparent" className={styles.close_btn} onClick={alert.onClose} /> </li> ); } );
src/logics
ロジック部分の処理を書いたファイルを格納するフォルダーです。
logics/actions
通信処理などをする関数を定義したファイルを入れるフォルダーです。
logics/reducer
後述するuseClutchで使う
reducerを定義したファイルを入れるフォルダーです。logics/util
actionsやreducerには当てはまらないものをここに入れます。logics/util/uses
このフォルダーには、コンポーネントで使う
React HooksのカスタムHooksを定義したファイルを入れます。コンポーネントについて
コンポーネントは、以下のフォルダー構成になります。
コンポーネントのフォルダー構成/ComponentName ├─ index.tsx <- JSXを書くファイル ├─ ComponentName.moduel.scss <- index.tsxのスタイルを書くファイル (フォルダーと同じ名前にする) └─ use.ts <- index.tsxで使うコールバックや変数を定義するファイル
index.tsxに描画部分を書き、ロジックなどはuse.tsに書くことによって、
描画部分はより描画に専念して書けますし、ロジックも同じように書けます。
修正する時も、色々とやりやすかったりします。
今回のWebアプリではpagesのみこのようにしていますが、後々は全部のコンポーネントをこの構成したいです。
ComponentName.module.scssは、index.tsxでindex.tsximport styles from "./ComponentName.module.scss"のようにするためです。
State管理について
このWebアプリでは、
Reactは使っていますが、Reduxは使って無いので工夫する必要があります。useClutch
このWebアプリでは、useClutchという
カスタムHooksを作成しました。
以下が、データフローになります。useClutchがやっていることは単純で、Promiseを返す
reducerを実行して、awaitして結果を受け取ると、
内部でsetStateをして描画を更新します。
こうすることで、reducer内で非同期処理が出来て処理を一か所にまとめることができます。clutchの使用例type Action = { type: "increment" } | { type: "decrement" }; interface StoreType { counter: number; } const state : StoreType = { counter: 0 }; const sleep = (t:number) => new Promise(r => setTimeout(r,t,t)); const reducer = async (state:number, action:Action) : Promise<StoreType> => { switch(action.type){ case "increment": await sleep(5000); return state + 1; case "decrement": await sleep(5000); return state - 1; default: return state; } } const App : React.FC = () => { const clutch = useClutch(reducer, store); const increment = () => clutch.dispatch("increment", { type: "increment" }).catch(console.error); const decrement = () => clutch.dispatch("decrement", { type: "decrement" }).catch(console.error); // 複数のactionを繋げることもできます。 // 描画更新は、すべてのactionが終了したときに発生します。 const add = () => clutch.pipe( "test", state => ({ type: "increment" }), state => state.counter > 10 ? null : ({ type: "increment" }) // 前のactionの結果を受け取って10以下ならさらにincrementする ).catch(console.error) return ( <div> <p>カウント : {clutch.counter}<p> <button onClick={increment}>カウントアップ</button> <button onClick={decrement}>カウントアップ</button> <button onClick={add}>1足して10以下ならさらに1足す</button> </div> ); }デザインについて
デザインは勉強の意味も込めて自分でやりましたが、ダサいので参考にはならないと思います。
しかしあれですね、デザイナーさんは凄いですね!
私もデザインについてもっと勉強しないといけないと感じました。( 特にAdobe。。。 )React HooksとTypeScriptは相性がいい!
React HooksとTypeScriptを使って開発しましたが、これが予想以上に良かったです!
React Hooksで、コンポーネントの記述量を減らして、その減らした分をTypeScriptに使うことで、
従来の記述量と同じくらいか少ないのに、型安全で、より設計しやすいコーディングをすることができました。
これから開発する際には、TypeScriptを使ったほうがよさそうですね。また、
React Hooksは柔軟に組み合わせることができるので、複雑な処理も関数に閉じ込めることができます。
上記で紹介したuseClutchもカスタムHooksで実装してます。
useClutchのソースコード
useClutch.tsimport { useRef, useState } from "react"; type Reducer<S, A> = (preState: S, action: A) => Promise<S>; type GetStoreType<R extends Reducer<any, any>> = R extends Reducer<infer S, any> ? S : any; type GetActionType<R extends Reducer<any, any>> = R extends Reducer< any, infer A > ? A : any; type ActionCreator<S, A> = (preState: S) => A | null; type RequestStatus = "start" | "success" | "cancel" | "error"; type ListenRequestCallback = (request: string, status: RequestStatus) => void; type CancelCallback = () => void; export interface Clutch<StoreType, ActionType> { state: StoreType; request: <T>( req: string, promiseCreator: () => Promise<T> ) => Promise<T | null>; cancelRequest: (request: string) => boolean; listenRequest: (cb: ListenRequestCallback) => () => void; isLoading: (request?: string) => boolean; pipe: ( request: string, ...funcs: Array<ActionCreator<StoreType, ActionType>> ) => Promise<void>; dispatch: (request: string, action: ActionType) => Promise<void>; } export const useClutch = <R extends Reducer<any, any>>( asyncReducer: R, initializeState: GetStoreType<R> ): Clutch<GetStoreType<R>, GetActionType<R>> => { type StoreType = GetStoreType<R>; type ActionType = GetActionType<R>; // 描画を更新するためのState const [pureState, setState] = useState<StoreType>(initializeState); const { progressCancel, progressPromise, listenCallbacks, notifyRequest, resolveAsync, updateState, clutch } = useRef({ // 実行中のPromise処理をキャンセルするコールバックを保持するMap progressCancel: new Map<string, CancelCallback>(), // 実行中のPromiseインスタンスを保持するMap progressPromise: new Map<string, Promise<any>>(), // 非同期処理状況をlistenする関数を保持するSet listenCallbacks: new Set<ListenRequestCallback>(), // 非同期処理の状態をlistenCallbackに伝える関数 notifyRequest: (request: string, status: RequestStatus) => { listenCallbacks.forEach(cb => cb(request, status)); }, // 引数に渡されたPromiseを実行しawaitして結果を返す resolveAsync: <T>( request: string, cb: () => Promise<T> ): Promise<T | null> => { const promise = progressPromise.get(request); if (promise) { return promise as Promise<T | null>; } notifyRequest(request, "start"); const cancelCallback = new Promise<"cancel">(re => { progressCancel.set(request, () => re("cancel")); }); const progress = Promise.race([cb(), cancelCallback]) .then(result => { notifyRequest(request, result === "cancel" ? "cancel" : "success"); progressCancel.delete(request); progressPromise.delete(request); return result === "cancel" ? null : result; }) .catch(e => { notifyRequest(request, "error"); progressCancel.delete(request); progressPromise.delete(request); return Promise.reject(e); }); progressPromise.set(request, progress); return progress; }, // stateを更新する関数 updateState: async ( request: string, oldState: StoreType, promiseCreator: (oldState: StoreType) => Promise<StoreType> ) => { const newState = await resolveAsync(request, () => promiseCreator(oldState) ); const updated: Partial<StoreType> = {}; if (newState) { for (const key in oldState) { if (oldState[key] !== newState[key]) { updated[key] = newState[key]; } } if (Object.keys(updated).length > 0) { clutch.state = { ...clutch.state, ...updated }; setState(clutch.state); } } }, // Propsに渡されるclutchオブジェクト clutch: { state: pureState, // 処理が実行中かのフラグを返す関数 isLoading: (request?: string): boolean => { if (request) { return progressPromise.has(request); } return !!progressPromise.size; }, // reducerを通さないPromiseを監視するようにする request: <T>( req: string, promiseCreator: () => Promise<T> ): Promise<T | null> => { return resolveAsync<T>(req, promiseCreator); }, // 実行中のPromiseを中断する cancelRequest: (request: string): boolean => { const cancel = progressCancel.get(request); if (cancel) { cancel(); return true; } else { return false; } }, // listenCallbackを設定する関数 listenRequest: (cb: ListenRequestCallback): (() => void) => { listenCallbacks.add(cb); return () => listenCallbacks.delete(cb); }, // 引数に渡されたActionCreatorの順番で、reducerを実行する pipe: async ( request: string, ...funcs: Array<ActionCreator<StoreType, ActionType>> ): Promise<void> => { const promiseCreator = async (oldState: StoreType) => { for (const fn of funcs) { const action = fn(oldState); if (action) { oldState = await asyncReducer(oldState, action); } } return oldState; }; await updateState(request, { ...clutch.state }, promiseCreator); }, // action&payloadをreducerに渡して実行する dispatch: async (request: string, action: ActionType): Promise<void> => { const promiseCreator = (oldState: StoreType) => { return asyncReducer(oldState, action); }; await updateState(request, { ...clutch.state }, promiseCreator); } } }).current; return clutch; };
使った技術の感想
React & React Hooks
Reactは以前から使っていたので色々とやり易かったですが、React Hooksによってより使いやすくなりました。
例えば、上で説明したpagesのコンポーネントのindex.tsxの中身は、以下のようになりました。pages/Home/index.tsximport React from "react"; import { RouteComponentProps } from "react-router"; import { Button } from "../../atoms/Button"; import { Icon } from "../../atoms/Icon"; import { PageTemplate } from "../../templates/PageTemplate"; import styles from "./Home.module.scss"; import { useHome } from "./use"; export const Home: React.FC<RouteComponentProps> = () => { const { isLogin, isCheked, login, goToTrend } = useHome(); return ( <PageTemplate className={styles.page}> <div className={styles.home}> <div className={styles.title}> <div className={styles.wrapper}> <h1 className={styles.title_label}>NIJINOWA</h1> <div className={styles.wrapper}> <p> <big>NIJINOWA</big> は二次創作を投稿できるサービスです </p> </div> <div className={styles.container}> <div className={styles.wrapper}> <Button color="blue" onClick={goToTrend}> トレンドを見る </Button> </div> <div className={styles.wrapper}> <Button disabled={!isCheked} onClick={login}> {isLogin ? "ダッシュボードに移動" : "Googleアカウントでログイン"} </Button> </div> <div className={styles.wrapper}> <a href="https://twitter.com/uttk8128" target="_blank" className={styles.twitter_btn} > <Icon icon="twitter_white" size={32} /> 製作者のTwitter </a> </div> </div> </div> </div> </div> </PageTemplate> ); };見てわかるように、ロジックの処理が書かれていないことが分かります。
これは、useHome()というカスタムHooksにロジック部分を書いているためです。
以下がuseHome()の実際のソースコードになりますpages/Home/use.tsimport { useEffect, useState } from "react"; import useReactRouter from "use-react-router"; import { useAuth } from "../../../logics/util/uses/auth"; export const useHome = () => { const { history } = useReactRouter(); const { isLogin, isCheked, loginCheck, loginWithGoogle } = useAuth(); const [isPush, setPush] = useState(false); const login = () => { if (isLogin) { history.push("/dashboard"); } else { setPush(true); loginWithGoogle(); } }; useEffect(() => { if (!isCheked) { loginCheck(); } else if (isLogin && isPush) { history.push("/dashboard"); } }, [history, isCheked, isPush, isLogin]); return { isLogin, isCheked, login, goToTrend: () => history.push("/trends") }; };こうやって、
ViewとLogicを分けて書けるようになったのも、React Hooksによるところが大きいかと思います。
また、ファイルを分けているので関心の分離といった観点からもいいのではないでしょうか。TypeScript
TypeScriptを使うことによって、コンポーネントやカスタムHooksに型情報を追加し、より扱い易いものを作ることができます。
また、上記でも言ったように、本当にReact HooksとTypeScriptの相性は良く、
TypeScriptの記述量の多さに嫌気が差す方もいるかとは思いますが、React Hooksがコンポーネントの記述量を減らしてくれているので、
実質今までの記述量とあまり変わらないです。むしろ少なくなっている方です。
さらに、同じ記述量でもTypeScriptの方が型安全ですし、VSCodeなどのTypeScriptをサポートしている
エディタを使えば入力補完も効いて、素晴らしい開発者体験が得られます。
React Hooksを導入する・しているなら、TypeScriptも導入することをお勧めします。Firebase
Firebaseは、小さいサービスなどを作るのに本当に便利です。
今回のWebアプリでは、Auth,FireStore,Storage,Hostingを使いました。
今回の開発では、特にFireStoreのデータ構造とreducerによる処理の分割が、相性がとてもよかったです。例えば、
userドキュメントにfollowerサブコレクションがあったとき、followerのreducerを作れば、
それだけで、followerの取得や変更などがアプリに追加できます。
これは、後からデータを追加する際にも同じような感じでアプリを拡張できます。つまり何が言いたいのかというと、
FireStoreのコレクションごとにreducerを作ることにより、
コレクションを新しく追加したとき、新しくreducerを作って、Webアプリに機能を追加し、
編集の際も、そのreducerのみを変更すればいいのです。また、ログイン機能を簡単に入れれたり、サイトを簡単にデプロイ出来たりと
Firebaseは凄さに恐縮してしまいます。作ってみた感想
実は今回Webアプリ製作はフレームワークの行き詰まりの解消のためでもあったので、
普通に作るのではなく、二つの制限をかけて作りました。制限
1. React.jsで作るが、Classコンポーネントは使わない
2. なるべく、他のnpmモジュールに頼らず自分で作る上記の制限の中で作ることにより、より設計やデータの流れについて考えるようになり色々とフレームワークのアイディアが見えてきたので、Webアプリを作ってよかったと思いました。
あと、やっぱりモノづくりは楽しい!
後書き
今回のWebアプリを作るきっかけになった、前回の記事で紹介したフレームワークですが、
前回の記事にいいね!などしてくれた方たちには大変申し訳ないですが、
このまま開発してもよくならないように感じたので、開発するのは終わりにしたいと思います。
しかし、また別のフレームワークを企画しています!最後まで、読んでいただきありがとうございます。
何か気になることなどがあれば、お気軽にコメントください!
あと、最近Twitter始めたのでそちらでも大丈夫です!
それではまた?
- 投稿日:2019-07-04T01:37:48+09:00
Reactコンポーネントのライフサイクル
React で使われているコンポーネントは、プロパティやステートの変化を受けて状態を変化します。最終的にぺージから破棄されるまでの間をコンポーネントのライフサイクルと言われます。
Reactでは、ライフサイクルの辺ごと、様々なメソッドが呼びだれています。
これにより、コンポーネントの表示や破棄の各タイミングで、独自の振る舞いを実装することができます。以下がライフサイクル時に呼ばれるメソッド群です。
componentWillMount : コンポーネントの描画の直前に呼びばれる。
componentDidMount :コンポーネントの描画の直後に呼ばれる。
componentWillUnmount :コンポーネントの解放のタイミングで呼ばれる。
componentWillReceiveProps:プロパティを受け取る直前に呼ばれる。
shouldComponentUpdate コンポーネントの更新の可否を決定する
componentWillUpdate コンポーネントの更新の直前に呼ばれる。
componentDidUpdate コンポーネントの更新の直後に呼ばれる。このうちは、componentWillMount :は、コンストラクタで代用可能であり、非推奨とされているらしいです。
コンポーネントのライフサイクルは大きく2つに分類され例外の処理をハンドリングすることもできます。実行例は追々。
コンポーネントのMount(生成および破棄)
コンポーネントのUpdate(更新)コンポーネントのErrorHandling (例外発生)
- 投稿日:2019-07-04T00:07:39+09:00
JS 【JavaScript初心者向け】Generative Art With JavaScript 受講
はじめに
表題通り受講予定
前日の知識詰め込み用でもある。
1 https://gist.github.com/x-hack-git/70c9c99ee33b8c5d1495b409d01849b9
2 https://x-hack.connpass.com/event/136668/事前準備
参考noteより抜粋
https://note.mu/kiwatchi1991/n/n9be787993ab0
https://note.mu/pocho_chonsuke/n/n6ab154f59100
API
線を描くと宣言します。: ctx.beginPath();
書き始める場所を指定します。: ctx.moveTo(20, 20);
引きたい線の終点を指定します。: ctx.lineTo(120, 20);
引きたい線の終点を指定します。: ctx.lineTo(120, 120);
引きたい線の終点を指定します。: ctx.lineTo(20, 120);
描いた線を閉じます。: ctx.closePath();
できた図形を線で描画します。: ctx.stroke();ctx.fillStyle = "#FF0000"; ctx.fillRect(20, 20, 150, 100);getContextメソッドに渡せる引数は2dのみのようなので、getContext("2d")
図形描写色々
https://ics.media/tutorial-createjs/shape_draw/
関数コーナー
・getRandomNumber (// 0-255のランダムな数値を取得する)
・getRandomColor (// ランダムな色を返す)
・Math.random()は、0~1の数値をランダムに返す関数・Math.round()で四捨五入
https://qiita.com/SPdotTS/items/8e427d9cbbbdc7560e8b
Math.Round 関数は単純な四捨五入ではない一般的な四捨五入にしたい場合は、MidpointRounding.AwayFromZero を指定します。
続く















