- 投稿日:2020-09-20T21:46:08+09:00
Vue2 のリアクティブシステムをプレーンな Javascript で実装する
目的
Object.defineProperty
の getter/setter を使って簡易的な Vue のリアクティブシステムを実装していくことがこの記事の目的です。実装は、Vue Mastery の「Build a Reactivity System」を参考にしています。https://www.vuemastery.com/courses/advanced-components/build-a-reactivity-system
公式ドキュメントによると...
Vue2 のリアクティブシステムは、
Object.defineProperty
の getter/setter を使って実装されています。これについては Vue 公式ドキュメントの「リアクティブの探求」のセクションで触れられています。プレーンな JavaScript オブジェクトを data オプションとして Vue インスタンスに渡すとき、Vue はその全てのプロパティを渡り歩いて、それらを Object.defineProperty を使用して getter/setter に変換します。
https://jp.vuejs.org/v2/guide/reactivity.html
Object.defineProperty
のド基礎
Object.defineProperty
の公式ドキュメントです。ディスクリプターとは
Javascript のオブジェクトは、値(value)以外に詳細設定を持っています。詳細設定は、ディスクリプターと呼ばれており、例えば、書き換え可能なオブジェクトなのかどうか、などがあります。
// ふつうのオブジェクト。値以外に詳細設定を持つ。 const obj = { name: 0 };ディスクリプターは、
getOwnPropertyDescriptor
で確認することができ、ふつうにオブジェクトを定義した場合のデフォルト値はtrue
になります。例えば、書き換え可能なオブジェクトかどうかを表すディスクリプターはwritable
であり、以下の例は、true
なので書き換え可能である、ということになります。const obj = { name: 0 }; const descriptor = Object.getOwnPropertyDescriptor(obj, 'name') console.log(descriptor) // {value: 0, writable: true, enumerable: true, configurable: true}
defineProperty
で定義した場合は、ふつうにオブジェクトを定義した場合の挙動と異なり、デフォルト値はfalse
になります。const obj = {}; Object.defineProperty(obj, 'name', { value: 43 }) console.log(obj) Object.getOwnPropertyDescriptor(obj, 'name') // {value: 43, writable: false, enumerable: false, configurable: false}getter/setter もディスクリプター
getter/setter は、オプショナルなディスクリプターで、デフォルト値は
undefined
なので、使いたい場合は追加で定義する必要があります。const obj = { _name: 'qiita' }; Object.defineProperty(obj, 'name', { get: function() { return this._name; }, set: function (val) { this._name = val } }) obj.name // "qiita" obj.name = 'twitter' // "twitter" obj.name // "twitter"
obj.name
では、getter
経由でqiita
という戻り値があり、obj.name = 'twitter'
では、setter
経由でここらへんから、けっこう Vue のリアクティブなコードの記法と一緒なので、これが Vue のリアクティブシステムのコアであると言われてるとわりとすんなり理解できる気がしています。
変更が追跡できないパターン
Object.defineProperty
の getter/setter によってリアクティブシステムは実装されているため、 getter/setter のリアクティブの限界が Vue のリアクティブの限界でもあります(一部は Vue が補っている部分もあるため、完全なイコールではないです)。例えば、配列への要素の追加の変更は追跡できないため、リアクティブになりません。
var vm = new Vue({ data: { items: ['a', 'b', 'c'] } }) vm.items[1] = 'x' // is NOT reactive vm.items.length = 2 // is NOT reactiveリアクティブシステムを Javascript で実装する
目的地
すごくシンプルな
price
とquantity
の乗算によって、totalPrice
を算出する Vue のコードをObject.defineProperty
の getter/setter を使って実装していきます。以下は、すごくシンプルな Vue のコードです。これと同じことを Javasciript で実装します。
<div id="app"> <div>Price: ${{ price }}</div> <div>Total: ${{ totalPrice }}</div> </div> <script src="https://cdn.jsdelivr.net/npm/vue"></script> <script> var vm = new Vue({ el: '#app', data: { price: 5.00, quantity: 2 }, computed: { totalPrice() { return this.price * this.quantity } } }) </script>実装する ~その1~
まずは、オブジェクトの定義から書いていきます。
let data = { price: 5, quantity: 2 }次は、
price
あるいはquantity
の値が変更されたときの再計算の関数を定義します.
target
が呼ばれ、data.total
の再計算が行われます。let target = null target = function () { data.total = data.price * data.quantity }次に
data
に対して、getter/setter を設定します。let internalValue = data.price Object.defineProperty(data, 'price', { get() { return internalValue }, set(newVal) { internalValue = newVal target() } })現時点の動作を確認すると、
data.price = 10
の実行により、setter が呼ばれ、その中でtarget()
が実行されることで、data.total
が更新されています。data.total // undefined data.price = 10 // 10 data.total // 20ここまで実装すれば、リアクティブの実装の仕組みは理解できたはずです。
実装する ~その2~
このコーディングには以下の2点の問題があります。
- quantity のリアクティブの実装はまだしていない
- コードに柔軟性が足りない
quantity
の getter/setter を定義したいですが、もう一つdefineProperty
を書くのはナンセンスです。また、今はprice
の値が変更されればtarget()
を実行することを決め打ちで書いているので、set()
に直接、target()
を書いていますが、他の関数にしたいなどの要望があると追加しづらいです。そのため、次は、依存関係を管理するクラスを定義します。
ようは、data.price
の値が書き換えられたら、target()
を実行する必要がある、という依存関係を管理します。
Dep
クラスを定義し、値が変更されたときに実行する関数(依存関係)をdepend()
によってsubscribers
リストに格納しています。これはあくまでも依存関係を取得しているのみであり、実際に値が変更されたときは、notify()
によって、実行する関数を呼びます。class Dep { constructor () { this.subscribers = [] } depend() { if (target && !this.subscribers.includes(target)) { this.subscribers.push(target) } } notify() { this.subscribers.forEach(sub => sub()) } }
data
のキーが複数でも問題ないようにObject.keys
での実装に書き換えています。また、get()
でdep.depend()
を実行することで、依存関係を取得し、値が書き換えられるset()
でdep.notify()
を実行することで、実行する関数を読んでいます。Object.keys(data).forEach(key => { let internalValue = data[key] const dep = new Dep() Object.defineProperty(data, key, { get() { dep.depend() return internalValue }, set(newVal) { internalValue = newVal dep.notify() } }) })最後に
watcher()
を定義し、ここでやっとtarget()
は実行されます。watcher()
でラップしているのは、target()
以外の関数でもいいようにです。function watcher(myFunc) { target = myFunc target() target = null } watcher(target)最終成果物
実行結果。
data.price = 50 // 50 data.total // 100 data.quantity = 100 // 100 data.total // 5000コード。
let data = { price: 5, quantity: 2 } let target = null target = function () { data.total = data.price * data.quantity } class Dep { constructor () { this.subscribers = [] } depend() { if (target && !this.subscribers.includes(target)) { // Only if there is a target & it's not already subscribed this.subscribers.push(target) } } notify() { this.subscribers.forEach(sub => sub()) } } Object.keys(data).forEach(key => { let internalValue = data[key] const dep = new Dep() Object.defineProperty(data, key, { get() { dep.depend() // console.log('i was assessed', internalValue) return internalValue }, set(newVal) { // console.log('i was changed', internalValue, newVal) internalValue = newVal dep.notify() } }) }) function watcher(myFunc) { target = myFunc target() target = null } watcher(target)まとめ
defineProperty
の基礎から、簡単な Vue のリアクティブの実装をしました。
かなりシンプルですが、Vue のリアクティブシステムの実装ができたかと思います。Vue3 からは、リアクティブの実装は
Proxy
オブジェクトを使用されているため、少し挙動は変わるようなので、次はそっちも見ていきたいです。
- 投稿日:2020-09-20T20:02:06+09:00
Webフロント:Javascriptで扱う事前データを遅延なく読み込む
フロント側:レンダリング以前に必要なデータの読み込み方法
- HTMLヘッダ内にデータ読み込みのJavascriptを指定する。
- Javascriptはサーバサイドで動的に作成する。
- javascriptの読み込みはpreload属性をつける。MDN web docs:rel="preload" によるコンテンツの先読み
head<head> <link rel="preload" href="htts://www.example.com/app/preset" as="script"> </head>
- headの内容を読み込まないとbody移行の処理は実行されないが、事前データはすべて揃ってから後段の処理をするため、この読み込みの仕様に即するほうが合目的である。
- Ajaxなどで同期で読み込むとレンダリングの遅延が発生する。
- 非同期であっても、読み込み完了のチェックを検査するため、やはりレンダリング遅延が発生する。
- 上記のようにヘッダ内で、動的に作成するJavascripをpreloadで読み込めば、読み込みが完了してから後段の処理が実行されるため、部分的なレンダリング遅延などは起らない。
- そもそも事前に送信するデータ量を押さえれば画面が真っ白になることはほぼない。(おそらく認証データや初回表示データぐらいなら問題ない。経験的にも)
サーバ側:動的にJavascriptファイルを作成
- 認証データなど事前に必要となるデータをJavascriptファイルとして動的に送信するAPIを作成する。
- 以下、PHPでの例。
- windowオブジェクトに値を設定するコードを返却
- windowオブジェクトは事前データとして必ずアクセスでき、グローバル参照可能。
preset.php//------------------------------------------------------------------------- // http://www.example.com/app/prestでアクセスした場合に以下を返却 //------------------------------------------------------------------------- //スクリプトを返すヘッダを送信 $data = ['auth' => 1]; $json = json_encode($data); header('Content-Type: application/x-javascript; charset=utf-8'); echo "(function(){window[\"preset\"} = ${json};)();";フロント側:データの取得
windowオブジェクトに設定されたデータを読み込む。
init.jsdocument.addEventListener("DOMContentLoaded", () => { //presetキーのチェック(なければ終了) if(false === 'preset' in window)return; //authキーのチェック(なければ終了) if(false === 'auth' in window.preset)return; //認証チェック(フラグが1でなければ終了) if(1 !== Number(window.preset.auth)) return; //認証成功 alert('valid user!'); });備考
- 実際に、上記の方法で、初回表示でコンテンツが真っ白になったりすることはほぼなくなった。
- サーバサイドレンダリングをしなくてもこの方法でレンダリングの遅延はある程度防げる。
- 投稿日:2020-09-20T20:02:06+09:00
Webフロント:Javascriptで扱う事前データを遅延なくAPIから読み込む
フロント側:レンダリング以前に必要なデータの読み込み方法
- HTMLのhead内にデータ読み込みのJavascriptを指定する。
- Javascriptはサーバサイドで動的に作成する。
- Javascriptの読み込みはpreload属性をつける。MDN web docs:rel="preload" によるコンテンツの先読み
head<head> <link rel="preload" href="htts://www.example.com/app/preset" as="script"> </head>
- headの内容を読み込まないとbody移行の処理は実行されないが、事前データはすべて揃ってから後段の処理したい為、この読み込みの仕様に即するほうが合目的である。
- Ajaxなどで同期で読み込むとレンダリングの遅延が発生する。
- 非同期であっても、読み込み完了のチェックを検査するため、やはりレンダリング遅延が発生する。
- 上記のようにヘッダ内で、動的に作成するJavascripをpreloadで読み込めば、読み込みが完了してから後段の処理が実行されるため、部分的なレンダリング遅延などは起らない。
- そもそも事前に送信するデータ量を抑えれば画面が真っ白になることはほぼない。(おそらく認証データや初回表示データぐらいなら問題ない。経験的にも。)
サーバ側:動的にJavascriptファイルを作成
- 認証データなど事前に必要となるデータをJavascriptファイルとして動的に送信するAPIを作成する。
- 以下、PHPでの例。
- windowオブジェクトに値を設定するコードを返却
- windowオブジェクトは事前データとして必ずアクセスでき、グローバル参照可能。
- 以下は、簡易的にしているが、windowsの直下はアプリケーションを識別するプロジェクトキーをつけるといいかもしれない。
preset.php//------------------------------------------------------------------------- // http://www.example.com/app/prestでアクセスした場合に以下を返却 //------------------------------------------------------------------------- //スクリプトを返すヘッダを送信 header('Content-Type: application/x-javascript; charset=utf-8'); //送信するデータのJSONを作成 $data = ['auth' => 1]; $json = json_encode($data); //事前設定データをJavascriptのwindowオブジェクトに設定するコードを送信 echo "(function(){window[\"preset\"} = ${json};)();";フロント側:データの取得
- windowオブジェクトに設定されたデータを読み込む。
init.jsdocument.addEventListener("DOMContentLoaded", () => { //presetキーのチェック(なければ終了) if(false === 'preset' in window)return; //authキーのチェック(なければ終了) if(false === 'auth' in window.preset)return; //認証チェック(フラグが1でなければ終了) if(1 !== Number(window.preset.auth)) return; //認証成功 alert('valid user!'); });備考
- 実際に、上記の方法で、初回表示でコンテンツが真っ白になったりすることはほぼなくなった。
- サーバサイドレンダリングをしなくてもこの方法でレンダリングの遅延はある程度防げる。
- 事前に読み込むデータは1つのソース、つまり、HTMLのheadには1つのタグのみ指定し、その中で必要なデータすべてを受け取るようにする方が、アクセス回数を減らせるので良い。
- 事前データを1つのアクセスで取得する場合、RESTful設計のリソースの考え方を広げて考える必要がある。DBテーブルなど一つのリソースを統合した、「commonSet」、「sharedSet」など統合リソースを作るほうがよいと思う。
shared_set.php$data = [ 'auth' => [ 'name' => 'test user'], 'products' => $products ]; $json = json_encode($data);
- 投稿日:2020-09-20T20:02:06+09:00
Webフロント:Javascriptで扱う事前データをレンダリングの遅延なくAPIから読み込む
0. 目的
- ユーザーの操作が関係しない、事前にサーバサイドで設定できるデータ(例えば認証結果)をフロントのJSに送信する。
- 上記のデータをレンダリングの遅延なく読み込む。つまり、画面が白くなったり、コンポーネントに一時的にデータがない状態を防ぐ。
1. フロント側:レンダリング以前に必要なデータの読み込み方法
- HTMLのhead内にデータ読み込みのJavascriptを指定する。
- Javascriptはサーバサイドで動的に作成する。
- Javascriptの読み込みはpreload属性をつける。MDN web docs:rel="preload" によるコンテンツの先読み
head<head> <link rel="preload" href="htts://www.example.com/app/preset" as="script"> </head>
- headの内容を読み込まないとbody移行の処理は実行されないが、事前データはすべて揃ってから後段の処理を実行したい為、この読み込みの仕様に即するほうが合目的である。
- Ajaxなどで同期で読み込むとレンダリングの遅延が発生する。
- 非同期であっても、読み込み完了のチェックを検査するため、やはりレンダリング遅延が発生する。
- 上記のようにヘッダ内で、動的に作成するJavascripをpreloadで読み込めば、読み込みが完了してから後段の処理が実行されるため、部分的なレンダリング遅延などは起らない。
- そもそも事前に送信するデータ量を抑えれば画面が真っ白になることはほぼない。(おそらく認証データや初回表示データぐらいなら問題ない。経験的にも。)
2. サーバ側:動的にJavascriptファイルを作成
- 認証データなど事前に必要となるデータをJavascriptファイルとして動的に送信するAPIを作成する。
- 以下、PHPでの例。
- windowオブジェクトに値を設定するコードを返却。
- windowオブジェクトは事前データとして必ずアクセスでき、グローバル参照可能。
- 以下は、簡易的にしているが、windowsの直下はアプリケーションを識別するプロジェクトキーをつけるといいかもしれない。
preset.php//------------------------------------------------------------------------- // http://www.example.com/app/prestでアクセスした場合に以下を返却 //------------------------------------------------------------------------- //スクリプトを返すヘッダを送信 header('Content-Type: application/x-javascript; charset=utf-8'); //送信するデータのJSONを作成 $data = ['auth' => 1]; $json = json_encode($data); //事前設定データをJavascriptのwindowオブジェクトに設定するコードを送信 echo "(function(){window[\"preset\"] = ${json};)();";3. フロント側:データの取得
- windowオブジェクトに設定されたデータを読み込む。
init.jsdocument.addEventListener("DOMContentLoaded", () => { //presetキーのチェック(なければ終了) if(false === 'preset' in window)return; //authキーのチェック(なければ終了) if(false === 'auth' in window.preset)return; //認証チェック(フラグが1でなければ終了) if(1 !== Number(window.preset.auth)) return; //認証成功 alert('valid user!'); });4. 備考
- 実際に、上記の方法で、初回表示でコンテンツが真っ白になったりすることはほぼなくなった。
- サーバサイドレンダリングをしなくてもこの方法でレンダリングの遅延はある程度防げる。
- データがない場合は、空文字(何も文字がない、Empty)を返し、無駄なJsの処理を発生させない。
- 事前に読み込むデータは1つのソース、つまり、HTMLのheadには1つのタグのみ指定し、その中で必要なデータすべてを受け取るようにする方が、アクセス回数を減らせるので良い。
- 事前データを1つのアクセスで取得する場合、RESTful設計のリソースの考え方を広げて考える必要がある。複数のDBテーブルのデータを統合した、「commonSet」、「sharedSet」など統合リソースを作るほうがよいと思う。
- RESTful設計でなく、GraphQLなら柔軟に設計できると思う。
shared_set.php$data = [ 'auth' => [ 'name' => 'test user'], 'products' => $products ]; $json = json_encode($data);
- 投稿日:2020-09-20T19:53:41+09:00
GoogleMapsAPI, GeocodingAPIを使って投稿内容から緯度経度情報を取得し地図を表示させる (and 地図から投稿を検索できるようにする)
実務未経験者です。
ポートフォリオに組み込んだGoogleMapsAPI関連について、作っておきながら面談で全く説明できなかったので、
頭を整理するために流れをまとめたものを書いてみます。アプリ: ラーメン屋の写真や情報を友達と共有できるSNS
Ruby: 2.6.5
Rails: 5.2.0
前提: GoogleCloudPlatformでAPIキーを取得すること投稿内容から緯度経度情報を保存
postモデルでbefore_save時 (投稿保存する時)にgeocodeメソッドを呼ぶ。
geocodeメソッドでは、バリデーションの後にGeocoding APIを使って
店名と最寄駅情報から緯度経度を取得、緯度経度カラムに保存させている。shop_nameカラム: 店名情報
nearestカラム: 最寄駅情報 (とてもわかりにくい)
latitudeカラム: 緯度情報
longitudeカラム: 経度情報app/models/post.rbclass Post < ApplicationRecord ~ ~ before_save :geocode ~ ~ private def geocode uri = URI.escape( "https://maps.googleapis.com/maps/api/geocode/json?address=" + self.shop_name + " " + self.nearest + "&key=" + Rails.application.credentials.GCP[:API_KEY] # APIキーを入れてください ) res = HTTP.get(uri).to_s response = JSON.parse(res) if response["status"] == "OK" self.latitude = response["results"][0]["geometry"]["location"]["lat"] self.longitude = response["results"][0]["geometry"]["location"]["lng"] else self.latitude = 1 self.longitude = 1 end end endgeocordingAPIを使って緯度経度情報を取得するには、下記のURIを用います。
https://maps.googleapis.com/maps/api/geocode/json?address=緯度経度情報を調べたい住所など&key=APIキー投稿詳細画面
app/views/posts/show.html.erb・ ・ <div class="post_map"> <div id="map"></div> <!-- cssでwidth, heightを指定しないと地図が表示されません --> </div> ・ ・ <script> var latLng; var marker; var infoWindow; function initMap() { latLng = {lat: <%= @post.latitude %>, lng: <%= @post.longitude %>}; map = new google.maps.Map(document.getElementById('map'), { center: latLng, zoom: 15, mapTypeControl: false, streetViewControl: false }); marker = new google.maps.Marker({ position: latLng, map: map }); infoWindow = new google.maps.InfoWindow({ content: "<a href='http://www.google.com/search?q=<%= @post.shop_name %> <%= @post.nearest %>' target='_blank' style='color: #00f;'><%= @post.shop_name %> を検索</a><br><br><a href='http://www.google.com/search?q=<%= @post.shop_name %> ラーメン&tbm=isch' target='_blank'>画像検索 by google</a>" }); marker.addListener('click', function() { infoWindow.open(map, marker); }); } initMap(); </script>app/views/layouts/application.html.erb(レイアウトファイル)<!DOCTYPE html> <html> <head> <title>・・・・・・</title> <!-- APIキーを読み込んでいます --> <script src="https://maps.googleapis.com/maps/api/js?key=<%= Rails.application.credentials.GCP[:API_KEY] %>&callback=initMap" defer></script> ・ ・ ・ </head> <body> ・ ・ <%= yield %> ・ </body> </html>地図から投稿を検索できるようにする
form_with と radio_button で、どのユーザーの投稿を地図に表示させるか選択する
↓
mapsコントローラのmapアクションで、投稿内容を含む変数@postsを定義
(投稿内容は.to_jsonでjson形式に変換、respond_to doで返す)
↓
アクション名と同じmap.js.erbがRailsによって自動的に開かれ、@postsを受け取り、
地図とマーカー (投稿内容)をセットapp/views/maps/index.html.erb<%= form_with url: map_request_path, method: :get do |f| %> <%= f.radio_button :posts, "all_user", checked: true %>全てのユーザーの投稿 <%= f.radio_button :posts, "following", disabled: current_user.nil? %>自分とフォロー中のユーザーの投稿 <%= f.radio_button :posts, "current_user", disabled: current_user.nil? %>自分の投稿 <%= f.submit '投稿されたお店を表示', class: "btn btn-primary" %> <button type="button" class="btn btn-success current_position" onclick="getLocation()"> 地図を現在地周辺に切り替える </button> <% end %> <div id="map_index"></div> <!-- cssでwidth, heightを指定しないと表示されません --> <script> var map function initMap(){ map = new google.maps.Map(document.getElementById('map_index'), { center: {lat: 37.67229496806523, lng: 137.88838989062504}, // 地図の中心を指定 zoom: 6, // 地図のズームを指定 mapTypeControl: false, streetViewControl: false }); } function getLocation(){ // 現在地周辺に地図を移動させる navigator.geolocation.getCurrentPosition( function(position) { var latitude = position.coords.latitude; var longitude = position.coords.longitude; var latlng = new google.maps.LatLng(latitude, longitude); map.setCenter(latlng); map.setZoom(12); } ); } initMap(); </script>config/routes.rbRails.application.routes.draw do ~ ~ resources :maps, only: [:index] get '/map_request', to: 'maps#map', as: 'map_request' endapp/controllers/maps_controller.rbclass MapsController < ApplicationController def index end def map if params[:posts] == "all_user" @posts = Post.all.to_json.html_safe elsif params[:posts] == "current_user" @posts = current_user.posts.to_json.html_safe elsif params[:posts] == "following" @posts = current_user.feed.to_json.html_safe end respond_to do |format| format.js { @posts } end end endapp/views/maps/map.js.erbvar posts = <%= @posts %>; var marker = []; var infoWindow = []; function initMap(){ for (let i = 0; i < posts.length; i++) { marker[i] = new google.maps.Marker({ position: {lat: parseFloat(posts[i].latitude), lng: parseFloat(posts[i].longitude)}, map: map, animation: google.maps.Animation.DROP }); infoWindow[i] = new google.maps.InfoWindow({ content: posts[i].shop_name + "<br>" + "<a href='/posts/" + posts[i].id + "' target='_blank' style='color: #00f;'>このお店に関する投稿を表示</a>" }); markerEvent(i); } }; initMap(); function markerEvent(i) { marker[i].addListener('click', function() { // マーカーをクリックしたとき infoWindow[i].open(map, marker[i]); // 吹き出しの表示 }); }こんな感じ
参考にさせていただきました
- 投稿日:2020-09-20T18:45:09+09:00
JavaScriptの基本をまとめました
JavaScriptの基本
JavaScript(EC6)の基本的な構文をまとめました。
JavaScriptでの書き方を見返すためのチートシートとして作成しました。出力
//コンソールへの出力 console.log("hogehoge");テンプレートリテラル
let name = "佐藤"; //変数や定数を文字列に埋め込んで出力する場合には以下が必要 //①バッククォート(`)で囲む //②${}で囲む console.log(`名前は${name}です`); //名前は佐藤ですが出力される変数・定数の定義
var(変数)
再定義:可能
再代入:可能var name = "佐藤"; console.log(name); //佐藤が出力される var name = "田中"; console.log(name); //田中が出力される(再定義可能) name = "鈴木"; console.log(name); //鈴木が出力される(再代入可能)let(変数)
再定義:不可能
再代入:可能let name = "佐藤"; console.log(name); //佐藤が出力される let name = "田中"; console.log(name); //エラーになる(再定義不可能) name = "鈴木"; console.log(name); //鈴木が出力される(再代入可能)const(定数)
再定義:不可能
再代入:不可能
これで定義された定数は必ず同じ値が入っていることを保証できるconst name = "佐藤"; console.log(name); //佐藤が出力される const name = "田中"; console.log(name); //エラーになる(再定義不可能) const name = "鈴木"; console.log(name); //エラーになる(再代入不可能)if文(条件分岐)
与えた条件によって処理を分岐させたい場合
if(条件式1){ 条件式1がtrueなら実行する処理 } else if(条件式2) { 条件式2がtrueなら実行する処理 } else { 条件式が全てfalseなら実行する処理 }switch文(条件分岐)
switch(num){ case 2: console.log("2です"); break; case 3: console.log("3です"); break; default: console.log("当てはまる数字はない") break; }
break
はswitch文を抜け出すために記述するもの。case2だった場合にbreakを書かないとcase3の処理もdefaultの処理も実行してしまう。
default
は当てはまるケースがない場合に実行する処理を記述する繰り返し処理
while
基本の形
いずれ条件式がfalseになる処理を記述しないと無限ループしてしまうwhile (条件式) { 条件式がtrueなら実行する処理 }記述の例
numberが100以下ならばその数字を出力をするlet number = 1; //変数numberに1を定義 while (number <= 100) { //①numberに入っている数字が100以下ならtrueを返す。falseなら処理終了 console.log(number); //②numberに入っている数字を出力する number += 1; //③numberに入っている数字に1を足す } //再び①の処理に戻るfor
while文よりも記述が少なくかける繰り返し処理
forの( )内に記述する処理は;
で区切るfor(変数の定義; 条件式; 変数の更新){ 処理 }記述の例
numberが100以下ならばその数字を出力する//下記は以下を行なっている //変数numberに1を入れて定義 //numberが100以下ならばtrueを返す。falseならば処理狩猟 //numberに1を足す for(let number = 1; number <= 100; number += 1){ console.log(number); //numberが出力される }配列
配列は
[ ]
で囲むことで定義できる。
配列の中の値にはインデックス番号(順番)
がついている。この番号は0
から始まるので注意。
例として下の配列の中の1
のインデックス番号は0
である。const number = [1, 2, 3, 4];インデックス番号を指定すると値を指定できる
console.log(number[0]); //1が出力されるオブジェクト
オブジェクトは
プロパティ
と値
をセットで持つことができる。
オブジェクトは{}
で囲む
プロパティと値の間には:
をいれるconst food = {name: "おにぎり", price: 150};オブジェクトのプロパティを指定して値を取り出すことができる
console.log(food.name); //おにぎりが出力されるundefind
オブジェクトにないプロパティを指定すると
undefind
となるconsole.log(food.size); //存在しないsizeを指定すると //undefindが出力される関数
関数とはいくつかの処理をまとめたものである。
関数の定義
基本の定義
const 定数名 = function() { //処理 }上記の形で処理を定数名をつけてまとめることができる。
アロー関数
functionを書くよりも
() =>
を使ってシンプルに書くことができる。const 定数名 = () => { //処理 }引数が必要な場合には以下のように記述する
const 定数名 = (引数) => { //処理 }戻り値
関数から返ってくる値を
戻り値
という
戻り値を返すにはreturn
を使用する//引数に受け取った値を2倍にして戻り値として返す関数であるdoubleNumberを定義 const doubleNumber = (num) => { return num * 2; //returnが無いと戻り値を返せない } //変数resultに関数doubleNumberの戻り値を代入。引数は1 let result = doubleNumber(1); //引数で渡した1を2倍にした数である2が出力される console.log(result);コールバック関数
基本の定義
関数の中でさらに関数を呼び出すことができる。//関数oneを定義 const one = () => { console.log("関数1"); //関数1が出力される } //コールバック関数であるcallを定義 const call = (callback) => { console.log("コールバック関数を呼び出す"); callback(); } //コールバック関数の呼び出し call(one); //コンソールは以下が出力される //コールバック関数を呼び出す //関数1クラス
クラスはオブジェクトの設計図である。
クラス名は大文字から始まることに注意
クラスの定義//クラスFoodを定義 class Food { }インスタンス
設計図であるクラスから作られた子どもがインスタンスである。
インスタンスの生成const food = new Food();コンストラクタ
コンストラクタはインスタンスの生成時に実行する処理や設定を追加するためもの。
このコンストラクタに書いた処理はインスタンスを生成した直後に実行される。
オブジェクトのようにプロパティと値を持つことができる。class Food { //クラスFoodの定義 constructor(name, pprice){ //コンストラクタの処理。引数nameとpriceを受け取る this.name = name; //このクラスのプロパティnameに引数のnameを代入 this.name = price; //このクラスのプロパティpriceに引数のpriceを代入 } } //foodという定数名でnameがおにぎり、priceが150のインスタンスを生成する const food = new Food("おにぎり", 150);メソッド
クラスに関数としてメソッドを持たせることができる。
class Food { constructor(){ } smell(){ console.log("美味しい匂いがする"); } } const food = new Food(); //インスタンスを生成 food.smell(); //インスタンスが持つメソッドを実行 //美味しい匂いがするが出力されるメソッドと関数は何が違うのか
メソッドはクラスの持つ動作を指す。
クラスに関数を持たせたものがメソッドである。
オブジェクト指向において現実世界にそのオブジェクトを置き換えた際に持つ概念や動作をメソッドとして定義している。
例として現実世界の動物である犬
を表現したいclass Dog
があったとして、犬は吠える
という動作を持っているのでメソッドとしてhoeru
を持つという考え方ができる。継承
クラスを別のクラスに継承することができる。
extends
を記述してその後に継承元のクラス名を記述する。
継承すると継承元が持っていたプロパティやメソッドを継承先でも呼び出すことができる。
親のクラスが子クラスが持つプロパティやメソッドなどを呼び出すことはできない。class Menu extends Food { }オーバーライド
継承すると親クラスのメソッドを子クラスでも呼び出せる。
その継承したメソッド名と同じメソッドを子クラスでも定義した場合には子クラスのメソッドが優先される。
上から上書きしていることからオーバーライド
という。最後に
簡単ではありますがいつでも見返せるようにまとめさせていただきました。
- 投稿日:2020-09-20T18:36:31+09:00
【JS】見出しタグ(<h1>~<h6>)を一括取得する関数(親要素やクラス等の指定にも対応)
見出しタグを
.querySelectorAll()
で取得する場合,いちいち.querySelectorAll( 'h1, h2, h3, h4, h5, h6' )
などと書かなければならず面倒くさい.SCSS なら,.post--content { h1, h2, h3, h4, h5, h6 { &.heading--background-black { // なんらかのスタイル指定. } } }みたいにネストさせて書けるセレクタも,いちいち,
document.querySelectorAll( '.post__content h1.heading--background-black' + '.post__content h2.heading--background-black' + '.post__content h3.heading--background-black' + '.post__content h4.heading--background-black' + '.post__content h5.heading--background-black' + '.post__content h6.heading--background-black', );といった具合に書かなければならず面倒くさい.こういう面倒くさいのをいちいち手作業でやってると,エラーの原因にもなりがちなので,関数化しました.
なお,本記事における “hx” は、“h1”,“h2”, “h3”, “h4”, “h5”, “h6” の総称です.
コード
TypeScript
headings.ts/** * @author JuthaDDA * @param elderSelector - ex. '.post', 'section >', 'hr +', '.meta ~'. * White space should be added after this selector. * @param classSelector - ex. '.heading--red', ':hover', ':not(:first-child)'. * White space before this selector should be trimmed * to avoid being interpreted as a nested selector. * @param topLevel - should be corrected to an integer in the range 1-6 * (ex. 0 to 1, 42 to 6, 1.9 to 1). * @param bottomLevel - should be corrected to an integer * in the range from `topLevel` to 6. */ interface HxSelectorOptions { elderSelector?:string, classSelector?:string, topLevel?:number, bottomLevel?:number } /** * @author JuthaDDA */ interface HxOptions extends HxSelectorOptions { root?:Element|Document, } /** * @author JuthaDDA */ const getHxSelector = ( options:HxSelectorOptions ):string => { const { min, max, floor } = Math; const elderSelector = options.elderSelector ? `${ options.elderSelector } ` : ''; const classSelector = options.classSelector ? options.classSelector.trim() : ''; const topLevel = floor( min( max( options.topLevel || 1, 1 ), 6 ) ); const bottomLevel = floor( min( max( options.bottomLevel || 6, topLevel ), 6 ) ); return Array.from( Array( bottomLevel - topLevel + 1 ), ( v, k ) => { const hxTagName = `h${ k + topLevel }`; return elderSelector + hxTagName + classSelector; } ).join( ',' ); }; /** * @author JuthaDDA */ const getHxs = ( options:HxOptions ):NodeListOf<HTMLHeadingElement> => { const root = options.root || document; return root.querySelectorAll( getHxSelector( options ) ); };JavaScript
headings.js/** * @author JuthaDDA * @param {Object} [options] * @param {string} [options.elderSelector] * - ex. '.post', 'section >', 'hr +', '.meta ~'. * White space will be added after this selector. * @param {string} [options.classSelector] * - ex. '.heading--red', ':hover', ':not(:first-child)'. * White space before this selector will be trimmed * to avoid being interpreted as a nested selector. * @param {number} [options.topLevel] * - will be corrected to an integer in the range 1-6 * (ex. 0 to 1, 42 to 6, 1.9 to 1). * @param {number} [options.bottomLevel] * - will be corrected to an integer in the range from `topLevel` to 6. * @return {string} */ const getHxSelector = ( options = {} ) => { const { min, max, floor } = Math; const elderSelector = options.elderSelector ? `${ options.elderSelector } ` : ''; const classSelector = options.classSelector ? options.classSelector.trim() : ''; const topLevel = floor( min( max( options.topLevel || 1, 1 ), 6 ) ); const bottomLevel = floor( min( max( options.bottomLevel || 6, topLevel ), 6 ) ); return Array.from( Array( bottomLevel - topLevel + 1 ), ( v, k ) => { const hxTagName = `h${ k + topLevel }`; return elderSelector + hxTagName + classSelector; } ).join( ',' ); }; /** * @author JuthaDDA * @see getHxSelector for further about `options`. * @param {Object} [options] * @param {Element|Document} [options.root] * @param {string} [options.elderSelector] * @param {string} [options.classSelector] * @param {number} [options.topLevel] * @param {number} [options.bottomLevel] * @return {NodeList} */ const getHxs = ( options = {} ) => { const root = options.root || document; return root.querySelectorAll( getHxSelector( options ) ); };説明
getHxSelector()
第 1 引数
options
のプロパティは,すべてオプショナルです.
options.elderSelector
は,各hx
の前に附くセレクターです.子孫結合子のほか,子結合子,一般兄弟結合子,隣接兄弟接合子も使えます.後ろに半角スペースが挿入されるので,子孫結合子の半角スペースは省略可能です.
options.classSelector
は,各hx
の後に附くセレクターです.クラス・セレクター,擬似クラス・セレクターが使えます.半角スペースが頭に含まれると子孫結合子として解釈されるので,.trim()
されます.
options.topLevel
とoptions.bottomLevel
で取得する見出しレベルの範囲を指定できます.たとえば,{ topLevel: 2, bottomLevel: 4 }
と指定した場合は,h2, h3, h4 が対象となり,h1, h5, h6 は対象外となります.
Array.from( Array( n ), ( v, k ) => k )
MDN は,JavaScript でライブラリを使用せず 0 から n-1 の配列を作る方法の 1 つです1.第 2 引数のコールバック関数を書き換えることで,lodash.range( n ).map( ( n ) => someFunc( n ) )
Lodash 的なことをライブラリなしのメソッド 1 つで書けるので,覚えておいて損はないかと思います.戻り値は
string
です.getHxs()
options.root
は,document
が既定で,Element
を指定することで適用範囲を限定できます.その他のoptions
のプロパティは,そのままgetHxSelector()
に渡され,getHxSelector()
から戻ってきたセレクターに一致する見出し要素のNodeList
がgetHxs()
の戻り値になります2.あとがき
前回の記事で予告した “ タグのレベルを一括して変更する関数” で今回の関数を使っていることに気付いたので,べつの記事にしたほうがいいかなと思い,こちらを先に投稿しました.
以上です.
TypeScript での戻り値の型指定が
NodeListOf<Element>
じゃなくてNodeListOf<HTMLHeadingElement>
でエラーが出ないのが,ちょっと不思議です. ↩
- 投稿日:2020-09-20T18:32:31+09:00
value.hasOwnProperty('tag') エラーが発生する
目的
- 原因の特定に時間がかかったためシェアする
- このエラーメッセージで検索しても日本語では情報が出なかったため、今後同じ問題が発生した人の調査に役立てたい
事象
- エラーメッセージは以下
console.error: There was a problem sending log messages to your development environment Error: value.hasOwnProperty is not a function. (In 'value.hasOwnProperty('tag')','value.hasOwnProperty' is udefined)環境
- react-native
- expo 3.21.13
原因と対処
- この問題は、Expoの内部のバグ
console.log()
の引数内にjsonを直接入れた時に発生する場合がある- なので、サーバリクエストなどの結果をconsole.logに出そうとした時にエラーが発生する
- 例えば、以下の時に発生した
//ReactNativeの画面で入力した結果をstateに入れている。 // それをUploadを行う非同期処理メソッドに直接入れている uploadTokoAsync(state) // 投稿を実施する uploadTokoAsync = (state) => { // 入ってきた引数の中身をログにだす → ここでエラーが発生する console.log(state) //以下はstateの中身ををアップロードする処理 }
- チケット6496上の議論での対処としては、出したい値を以下のようにJSON.stringfyで展開してタグ付けして解決をしているらしい
console.log('Test', JSON.stringify(state));
- 投稿日:2020-09-20T17:44:36+09:00
よく使うjQueryの書き方
サーバーサイドエンジニアでも簡単なJSやjqueryくらいは使えないといけないので、自分が実装したことのある管理画面の仕様とかでよく使うであろうものをメモしておく。
チェックボックス・ラジオボタンのクリックで表示・非表示
form_withの中身.form-group %label= model_t('reward.only_subscription_purchase') %div %label.checkbox-inline.pl0.radio-label = f.radio_button :only_subscription_purchase, true = t('stripe.subscription_plan.select_active.enabled') .form-group.text %label = model_t('reward.price') = f.text_field :price, class: 'form-control'$(function(){ let nowChecked = $('input[name="reward[only_subscription_purchase]"]:checked').val(); $('input[name="reward[only_subscription_purchase]"]').click(function(){ if($(this).val() == nowChecked) { $(this).prop('checked', false); nowChecked = false; $('.text').show(); } else { nowChecked = $(this).val(); $('.text').hide(); } }); });
let nowChecked
でイベントが発生する前のDOMを読み込んだ直後の状態を取得して代入している- クリックイベントをセット
this
は$('input[name="reward[only_subscription_purchase]"]')
で作ったjqueryオブジェクト$(this).val() == nowChecked
でクリックした時の値と元々取得していた値を比較。一致していると言うことは元々チェックが入っていた状態のラジオボタンをクリックしたと言うことなので、選択状態を解除したい時だと判断できる。if分岐がtrueであれば、$(this).prop('checked', false);
でチェックと外し、nowChecked = false;
で現在のチェックボックスの状態をfalseにする。変数に現在の状態を入れておかないと、ブラウザのリロードをしない状態でとチェックを入れたり外したりと言うことが出来ない。最後に、$('.text').show();
で表示したい部分を表示させる。- elseなら、
nowChecked = $(this).val();
で、イベントでチェックした状態をnowChecked変数の中身に代入し、$('.text').hide();
でフォーム表示。※補足:
$('input[name="reward[only_subscription_purchase]"] :checked').val();
の部分は以下のようにもかける$('input[name="reward[only_subscription_purchase]"]').prop('checked');$('input[name="reward[only_subscription_purchase]"]').is('checked');セレクトボックスの選択した値によって表示・非表示
= f.number_field :duration_in_months, min: 1, max: 100, class: 'form-control', placeholder: t('placeholder.stripe.subscription_plan.duration_in_months'), style: 'display: none;'$(function(){ $("#subscription_plan_duration").change(function() { const extractionVal = $(this).val(); if(extractionVal == "repeating") { $('#subscription_plan_duration_in_months').css('display', 'block'); }else { $('#subscription_plan_duration_in_months').css('display', 'none'); } }); });
- changeイベントのセット
const extractionVal = $(this).val();
changeイベントで発生したjqueryオブジェクトのvalue値を取得して代入- それが条件にある値かどうか判断
- jqueryオブジェクトのcssメソッドでcss追加で隠したり、表示したりする
チェックボックスの全選択・全解除
<table> <thead><tr> <th><label>全選択<br><input type="checkbox" id="all"></label> <th>Hoge </thead> <tbody id="boxes"> <tr><td ><input type="checkbox" name="chk[]" value="A"><td>Hoge1 <tr><td><input type="checkbox" name="chk[]" value="B"><td>Hoge2 <tr><td><input type="checkbox" name="chk[]" value="C"><td>Hoge3 </tbody> </table>$(function() { // 「全選択」する $('#all').on('click', function() { $("input[name='chk[]']").prop('checked', this.checked); }); // 「全選択」以外のチェックボックスがクリックされたら、 $("input[name='chk[]']").on('click', function() { if ($('#boxes :checked').length == $('#boxes :input').length) { // 全てのチェックボックスにチェックが入っていたら、「全選択」のチェックボックスをtrueに $('#all').prop('checked', true); } else { // 1つでもチェックが入っていない状態になったら、「全選択」のチェックボックスをfalseに $('#all').prop('checked', false); } }); });
- 「全選択」をクリックした時の動きの解説
- 該当のセレクタに対してclickイベントでの発火をセット
$("input[name='chk[]']").prop('checked', this.checked);
は、$("input[name='chk[]']").prop('checked', $(this).is('checked'));
や、$("input[name='chk[]']").prop('checked', $(this).prop('checked'));
と同義。(isは戻り値がBoolean、propは戻り値がプロパティーの値である違いがある。今回のように値がブール値しかない場合は2つは同じ文脈で代用できる。)ここで言うthis
は、$('#all')
で作ったjqueryオブジェクトを指すので、ここではオブジェクトの状態を取得している。取得した状態をpropメソッドの第二引数に活用している。- 'checked'はjqueryオブジェクトのプロパティーを指している。
- 「全選択」以外のチェックボックスがクリックされた時の動きの解説
- 「全選択」以外のチェックボックスがクリックされた時のイベントをセット
- 該当のセレクタの状態を確認。具体的には、チェックされているボックスの数とセットされているinput要素の数を比較する。チェック数とセットされているinput要素の数がイコールであれば、全ての選択肢チェックが入っている状態だと判断できる。
- 条件が成立していれば、該当のセレクタ(全選択部分)をtrue or false にする。
クリックしたら全部選択
textareaのオプションにjqueryを直接記述する方法
- textareaをクリックすると全部の文字が選択状態になる記述
.template %textarea.form-control.mt24#project-default-textarea{ onclick: "this.focus();this.select()", readonly: "readonly", rows: '20' } :preserve hogehogehoge # コピペする内容
- textareaのオプションで、onclickイベントをそのまま書いてしまう方法。thisはtextareaオブジェクトを表している。
- readonly: "readonly"は読み取り専用のオプション
scriptにjsの記述を切り出す方法
パターン1
%textarea.form-control#project-default-textarea{ readonly: "readonly", rows: '20' } :preserve hogehoge # コピペする内容$(function() { $('#project-default-textarea').click(function() { $(this).focus(); $(this).select(); }); });
- クリックイベントをセット
- focus()メソッドで要素にフォーカスを当て、select()メソッドで全部選択状態にする
パターン2(あまりおすすめしない方法)
.template %textarea.form-control#project-default-textarea{ onclick: "hoge()", readonly: "readonly", rows: '20' } :preserve hogehoge # コピペする内容function hoge() { $('#project-default-textarea').focus(); $('#project-default-textarea').select(); }
$(function() {})
や$(document).ready(function(){})
で囲んで、DOMの読み込み完了後にjsを実行できるようにするが一般的ではあるが、textareaのオプションのonclickを使った場合は、そちらでDOMの読み込み完了を検知しているようなので、jsのメソッドには書かなくても良い。読み込みの記述を書くと、DOMの読み込みの時にしかイベントが発火しなくなるようだ。ボタンを押すとテキストエリア内の内容を全選択してクリップボードへコピー
%a.btn.btn-primary.btn-template__copy-button.mt14.mb28{ href: "javascript:void(0)", onclick: "$('#project-default-textarea').focus(); $('#project-default-textarea').select();document.execCommand('copy') ? alert('クリップボードにコピーしました。') : alert('こちらの環境ではコピーできませんでした。お手数ですが直接選択してコピーをしてください。');" } = t('word.template_copy')
href: "javascript:void(0)"
について、aタグをクリックしたときにonclickに記述したJavaScriptのコードを動作させるためには、hrefを無効にしなければならないこと、hrefを空白にするだけだとリンクとして認識されないことから、JavaScriptの「void(0)」と言う、常にUndefinedを返してくれる演算子を使っているfocusして、selectで選択するところまで既出の理屈と同じで、
document.execCommand('copy')
を使って、js側から選択した文字列をクリップボードにコピーしている。もちろん、js部分の記述を切り出して書くこともできる。(前述の方法と同じ)
Railsでajax通信を行う
Railsでajax通信を行う方法は大きく二つあります。(vue.jsなどを使わない場合)
1. remote: trueをlink_toやform_withに設定
2. JSファイルにイベント(clickイベントなど)を設置しajax通信を発火させるお気に入りボタンを非同期で切り替える
コントローラー側の実装が必要になるが、一旦、通常のcreateアクションや、destroyアクションと変わらない。違うところは、redirect_toを削除し、レスポンスをcreate.js.erbなどのjs用のテンプレートファイルを作成し、ページ全体を更新するのではなく、該当部分だけ非同期で切り替えるイメージ。
class BookmarksController < ApplicationController def create @board = Board.find(params[:board_id]) current_user.bookmark(@board) end def destroy @board = Board.find(params[:id]) current_user.unbookmark(@board) end end # bookmark,unbookmarkなどのメソッドはモデルに定義している。 # redirect_toがないあとは、リクエストを送る部分に、remote: trueを指定し、JS形式でリクエストを送信する。
_bookmark.html.erb<%= link_to icon('far', 'star'), board_bookmarks_path(board.id), method: :post, remote: true, id: :"js-bookmark-button-for-board-#{board.id}" %>_unbookmark.html.erb# remote: ture を追記 <%= link_to icon('fas', 'star'), board_bookmarks_path(board.id), method: :delete, remote: true, id: :"js-bookmark-button-for-board-#{board.id}" %>あとは、viewの該当箇所にid属性を付与しておき、以下の記述で、id属性で指定した要素のjqueryオブジェクトを取ってきて、それに対してreplaceWithメソッドを使い、置き換える。テンプレートはcontroller側の実装により、createアクションの時とはcreate.js.erbを返し、destroyアクションの時は、destroy.js.erbを返すようになっている。
create.js.erb$('#bookmark_btn_<%= @board.id %>').replaceWith('<%= j(render 'boards/unbookmark', board: @board)%>');destroy.js.erb$('#bookmark_btn_<%= @board.id %>').replaceWith('<%= j(render 'boards/bookmark', board: @board)%>');コメント投稿・削除・編集を非同期で行う
remote: trueを使って投稿・削除
- 投稿のform_with
<%= form_with(model: [@board, @comment], remote: true) do |f| %> <%= f.label :body, t('.comment')%> <%= f.text_area :body, class: "form-control mb-3", placeholder: "コメント" , rows: "4" %> <%= f.submit t('.post'), class: "btn btn-primary" %> <% end %>
- 削除のlink_to
<%= link_to comment_path(comment.id), method: :delete, remote: true do %>※コントローラー側はredirect_toを書かない。
- js形式の通信で発火するテンプレート
create.js<% if @comment.errors.any? %> # バリデーションエラーがあるかどうか $('#error_messages').remove(); # 元々エラーメッセージが表示されていれば取り除く $('.js-comment-body').after("<%= j(render 'shared/errors_messages', { model: @comment }) %>"); <% else %> $('#error_messages').remove(); # 元々エラーメッセージが表示されていれば取り除く $('#js-table-comment').prepend('<%= j(render @comment)%>'); # コメントを入れる $(comment_body).val(''); # コメント入力欄を空にする <% end %>destroy.js$('#<%= "comment-#{@comment.id}"%>').remove();ajax関数を使ってコメントの編集を投稿
$(function () { $(document).on('click', '.js-edit-comment-button', function () { // 表示 const id = $(this).data('id'); $('#js-comment-' + id).hide(); $('#js-textarea-comment-box-' + id).show(); }); $(document).on('click', '.js-button-edit-comment-cancel', function () { // 非表示 const id = $(this).data('comment-id'); $('#js-comment-' + id).show(); $('#js-textarea-comment-box-' + id).hide(); }); $(document).on('click', '.js-button-comment-update', function () { const id = $(this).data('comment-id'); const textField = $('#js-textarea-comment-' + id); const body = textField.val(); $.ajax({ type: 'PUT', url: '/comments/' + id, data: { comment: { body: body } } }).done(function () { // 成功処理 $('#js-textarea-comment-box-' + id).hide(); $('#js-comment-' + id).text(body); $('#js-comment-' + id).show(); }).fail(function () { // 失敗処理 $('#js-textarea-comment-' + id).before('<p>コメントを入力してください</p>'); }); }); })
- $(document)のjqueryオブジェクトの部分をid属性で絞ったオブジェクトにしていないのは、非同期で追加したコメントを画面をリロードする前に編集する場合でも非同期状態で編集できるようにするため。
- doneとfailについてajax関数でのリクエストが成功ならdoneを、失敗ならfailを実行する。この成功失敗はHTTPステータスコードで判断する。200番台ならdoneへ、400番台ならfailへ処理が流れる。
- コントローラー側では,成功と失敗をifで分岐させた先で、
head :ok
、head :bad_request
などを記述してステータスコードを設定し、レスポンスの結果を操作する。def update @comment = current_user.comments.find(params[:id]) if @comment.update(comment_update_params) head :ok else head :bad_request end end もしくは def update @comment = current_user.comments.find(params[:id]) if @comment.update(comment_update_params) render json: @comment else head :bad_request # ①ステータスコードを400番で返す end end もしくは def update @comment.update!(comment_update_params) render json: @comment end成功時にjson形式でオブジェクトを返す記述をしておくと、ajax関数内で、
.done(function (data) { hogehoge.text(data.body); })とかで、引数にとることが出来るので、ajax関数内で使えたり、viewに渡せたりする。
- 投稿日:2020-09-20T17:21:28+09:00
npm経由で取得したNode-REDをカスタマイズして起動する(1)
何のための記事?
Node-REDをHerokuで運用する際に、Heroku環境では一部機能が動作しない場合があった。
その際、Node-REDの実装を調査する必要があったので関連するメモを整理して投稿します。
※本記事はHerokuと直接関係はありません1. Node-REDを自作プログラムから動かす
- Node.jsなどは予めインストールしておく。
1.1 導入
$ mkdir node-red-sample $ cd node-red-sample $ npm -y init $ npm install node-red $ node ./node_modules/node-red/red.js --userDir ./data --settings ./node_modules/node-red/settings.jsブラウザで http://127.0.0.1:1880/ を開く。
1.2. 解説
上記の手順では、下記のオプションを指定しています。
--userDir : 追加するノードなどが保存される場所を指定する
--settings : 使用する設定ファイルを指定する
※デフォルトで red.js と同じフォルダに存在するsettings.jsを参照するので--settings以降は省略可能
2. 設定をカスタマイズする
2.1 カスタマイズ方法(1) : settings.js を指定して起動
settings.js を自分用に書き換えてred.jsの起動パラメータに渡す事で、好みの設定で起動するように変更できます。
package.json と同じフォルダに settings.js をコピーする。
$ cp ./node_modules/node-red/settings.js ./settings.js を書き換えたら下記のようにコマンドを入力して起動する。
$ node ./node_modules/node-red/red.js --userDir ./data --settings ./settings.js2.2 カスタマイズ方法(2) : settings.js を userDir に入れておく
$ mkdir data $ cp ./node_modules/node-red/settings.js ./data/settings.js を書き換えたら下記のようにコマンドを入力して起動する。
$ node ./node_modules/node-red/red.js --userDir ./data3. エントリーポイント(?)を変更する
エントリーポイントを
./node_modules/node-red/red.js
ではなく、独自のファイルにする方法。$ cp node_modules/node-red/red.js ./main.jsコピーして作成した main.js を一部修正する。
- var RED = require("./lib/red.js"); + var RED = require("node-red");書き換えたら起動する。
$ node main.js --userDir ./dataまとめ
これでローカルでのデバッグできる環境は整いました。
- 投稿日:2020-09-20T17:21:28+09:00
npm から取得した Node-RED をカスタマイズして起動する(1)
何のための記事?
Node-REDをHerokuで運用する際に、Heroku環境では一部機能が動作しない場合があった。
その際、Node-REDの実装を調査する必要があったので関連するメモを整理して投稿します。
※本記事はHerokuと直接関係はありません1. Node-REDを自作プログラムから動かす
- Node.jsなどは予めインストールしておく。
1.1 導入
$ mkdir node-red-sample $ cd node-red-sample $ npm -y init $ npm install node-red $ node ./node_modules/node-red/red.js --userDir ./data --settings ./node_modules/node-red/settings.jsブラウザで http://127.0.0.1:1880/ を開く。
1.2. 解説
上記の手順では、下記のオプションを指定しています。
--userDir : 追加するノードなどが保存される場所を指定する
--settings : 使用する設定ファイルを指定する
※デフォルトで red.js と同じフォルダに存在するsettings.jsを参照するので--settings以降は省略可能
2. 設定をカスタマイズする
2.1 カスタマイズ方法(1) : settings.js を指定して起動
settings.js を自分用に書き換えてred.jsの起動パラメータに渡す事で、好みの設定で起動するように変更できます。
package.json と同じフォルダに settings.js をコピーする。
$ cp ./node_modules/node-red/settings.js ./settings.js を書き換えたら下記のようにコマンドを入力して起動する。
$ node ./node_modules/node-red/red.js --userDir ./data --settings ./settings.js2.2 カスタマイズ方法(2) : settings.js を userDir に入れておく
$ mkdir data $ cp ./node_modules/node-red/settings.js ./data/settings.js を書き換えたら下記のようにコマンドを入力して起動する。
$ node ./node_modules/node-red/red.js --userDir ./data3. エントリーポイント(?)を変更する
エントリーポイントを
./node_modules/node-red/red.js
ではなく、独自のファイルにする方法。$ cp node_modules/node-red/red.js ./main.jsコピーして作成した main.js を一部修正する。
- var RED = require("./lib/red.js"); + var RED = require("node-red");書き換えたら起動する。
$ node main.js --userDir ./dataまとめ
これでローカルでのデバッグできる環境は整いました。
- 投稿日:2020-09-20T15:18:15+09:00
【JavaScript】自分なりのワンライナー(配列操作)
元記事はこちらになります。
ワンライナーで行こう!(JavaScriptで配列操作いろいろ)
https://qiita.com/snst-lab/items/cf1fe64cfad70838ee93こちらで気になったものを自分なりに。
1年以上前の記事に今更かもしれませんが…重複の削除は
filter
使うよりArray.form(new Set(arr))
が良いという記事はあちこちで見るので割愛。startからendまでの連続した数値配列を生成する
2行目だけ見て書いたら、1行目がほぼ同じだったので削除
配列の中身を結合した文字列を作る
const string = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].join(''); console.log( string ); //'0123456789'元記事では正規表現でreplaceしてたのがきになりました。
隣り合うN個の要素をまとめた配列の配列を作る(N=3)
const array = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; const wrapInArray = (arr, n) => Array(Math.floor(arr.length/n)).fill().map( (_, i) => arr.slice(i*n, i*n+n) ); console.log ( wrapInArray(array, 3) ); //[[0, 1, 2], [3, 4, 5], [6, 7, 8]] console.log ( wrapInArray(array, 2) ); //[[0, 1], [2, 3], [4, 5], [6, 7], [8, 9]]N個未満になる配列も残したい時は、
Math.floor()
ではなくMath.ceil()
に変更const wrapInArray = (arr, n) => Array(Math.ceil(arr.length/n)).fill().map( (_, i) => arr.slice(i*n, i*n+n) );まとめ
Array(n).fill().map((_, i)) => {...});
を使いたいだけの記事でした。
- 投稿日:2020-09-20T13:00:55+09:00
【Rails】PAYJPのトークン生成後のエラー解決
現在プログラミング学習中で、これはメモしたいと思ったものを、自分用に分かりやすく残しておきます。
(前回投稿した内容の続きというか別ルートみたいな感じになります。)
【Rails】PAYJPでトークン生成時のエラー解決PAYJPを導入してトークンを生成したが、決済が成功しない...
テスト用のカード情報を入力して、購入ボタンを押したら、
のように表示されました。前回の内容に記述しておりますので割愛しますが、
トークン生成のJavaScriptの記述中にconsole.log()
を書いてstatus
を確認したところ、200
と出ており、トークン生成に成功していることは確認が取れております。
(トークンがいない?? そんなはずはない!!!)現状把握
エラーの原因を探るために、
binding.pry
を使用して、params
の中身を確認したところ、pry(#<OrdersController>)> params => <ActionController::Parameters {"authenticity_token"=>"uRxJ+Ho4c2vi4Pc8MrK/s7UhNbujnVBDt7qjJ11pFpeHWDMltGl3eu/ls94DaALSPNfIpoMtUd4aeOcs4Z8Y4w==", "item_order"=>{"postal_code"=>"555-0000", "prefecture_id"=>"9", "city"=>"市区町村", "block_number"=>"番地", "building_name"=>"建物名", "phone_number"=>"09012345678"}, "token"=>"tok_3b5890d13fb07a96a6cf2fa832e0", "controller"=>"orders", "action"=>"create", "id"=>"3"} permitted: false>後半部分を見ると分かりますが、
"token"=>"tok_3b5890d13fb07a96a6cf2fa832e0"
token
はしっかりparams
の中に入っていました!!!トークンはしっかり生成されているのです!
では次に、コントローラーの現状を見てみましょう。
****_controller.rb(一部抜粋)def order_params params.require(:item_order).permit(:token, :postal_code, :prefecture_id, :city, :block_number, :building_name, :phone_number).merge(item_id: @item[:id], user_id: current_user.id) end def set_item @item = Item.find(params[:id]) end def pay_item Payjp.api_key = ENV["PAYJP_SECRET_KEY"] Payjp::Charge.create( amount: @item.selling_price, card: order_params[:token], currency:'jpy' ) end現状の記述としては、
params
に入っている:item_order
の中の:token
をorder_params[:token]
として受け取っている。
ということになります。では、もう一度
binding.pry
で今度はorder_params
と入力して確認してみましょう。pry(#<OrdersController>)> order_params => <ActionController::Parameters {"postal_code"=>"555-0000", "prefecture_id"=>"9", "city"=>"市区町村", "block_number"=>"番地", "building_name"=>"建物名", "phone_number"=>"09012345678", "item_id"=>3, "user_id"=>3} permitted: true>"building_name"=>"建物名", "phone_number"=>"09012345678"} permitted: false>
token
がいない!!!!!!!params.require(:item_order).permit(:token, ~~~~)このように、
require
とpermit
で書いているのになんで???解決
bindng.pry
で確認したところ、token
が生成されているのは確認できたので、生成されたトークンの移動の仕方に問題がありました。
現在token
が生成されている場所は、item_order
の外であり、params
の直下なので、
そもそものtoken
の場所が違ったということです。
binding.pry
で、試しにparams[:token]
と入力してみますと、pry(#<OrdersController>)> params[:token] => "tok_14078502197e031107d18bb7e428"と出ます。
なので、このように記述する必要があったということです。
****_controller.rbdef order_params params.require(:item_order).permit(:postal_code, :prefecture_id, :city, :block_number, :building_name, :phone_number).merge(item_id: @item[:id], user_id: current_user.id, token: params[:token]) end少し長いですが、
merge
メソッドの中にtoken
を記述しています。
item_order
の中にではなく、order_params
に引っ付けるための記述です。こうすることで、
order_params
に引っ付けることができたので、記述を変更した後にbinding.pry
で確認してみますと、pry(#<OrdersController>)> order_params => <ActionController::Parameters {"postal_code"=>"333-0000", "prefecture_id"=>"13", "city"=>"市区町村", "block_number"=>"番地", "building_name"=>"建物名", "phone_number"=>"09012345678", "item_id"=>3, "user_id"=>3, "token"=>"tok_14078502197e031107d18bb7e428"} permitted: true>これでバッチリです☆
order_params[:token]
で受け取れるようになり、決済も無事成功しました!!!まとめ
token
の居場所をしっかり理解する!
merge
メソッドを使用して、引っ付ける!
- 投稿日:2020-09-20T13:00:55+09:00
【Rails】PAYJPのトークン生成後のエラー解決(別ルート編)
現在プログラミング学習中で、これはメモしたいと思ったものを、自分用に分かりやすく残しておきます。
(前回投稿した内容の続きというか別ルートみたいな感じになります。)
【Rails】PAYJPでトークン生成時のエラー解決PAYJPを導入してトークンを生成したが、決済が成功しない...
テスト用のカード情報を入力して、購入ボタンを押したら、
のように表示されました。前回の内容に記述しておりますので割愛しますが、
トークン生成のJavaScriptの記述中にconsole.log()
を書いてstatus
を確認したところ、200
と出ており、トークン生成に成功していることは確認が取れております。
(トークンがいない?? そんなはずはない!!!)現状把握
エラーの原因を探るために、
binding.pry
を使用して、params
の中身を確認したところ、pry(#<OrdersController>)> params => <ActionController::Parameters {"authenticity_token"=>"uRxJ+Ho4c2vi4Pc8MrK/s7UhNbujnVBDt7qjJ11pFpeHWDMltGl3eu/ls94DaALSPNfIpoMtUd4aeOcs4Z8Y4w==", "item_order"=>{"postal_code"=>"555-0000", "prefecture_id"=>"9", "city"=>"市区町村", "block_number"=>"番地", "building_name"=>"建物名", "phone_number"=>"09012345678"}, "token"=>"tok_3b5890d13fb07a96a6cf2fa832e0", "controller"=>"orders", "action"=>"create", "id"=>"3"} permitted: false>後半部分を見ると分かりますが、
"token"=>"tok_3b5890d13fb07a96a6cf2fa832e0"
token
はしっかりparams
の中に入っていました!!!トークンはしっかり生成されているのです!
では次に、コントローラーの現状を見てみましょう。
****_controller.rb(一部抜粋)def order_params params.require(:item_order).permit(:token, :postal_code, :prefecture_id, :city, :block_number, :building_name, :phone_number).merge(item_id: @item[:id], user_id: current_user.id) end def set_item @item = Item.find(params[:id]) end def pay_item Payjp.api_key = ENV["PAYJP_SECRET_KEY"] Payjp::Charge.create( amount: @item.selling_price, card: order_params[:token], currency:'jpy' ) end現状の記述としては、
params
に入っている:item_order
の中の:token
をorder_params[:token]
として受け取っている。
ということになります。では、もう一度
binding.pry
で今度はorder_params
と入力して確認してみましょう。pry(#<OrdersController>)> order_params => <ActionController::Parameters {"postal_code"=>"555-0000", "prefecture_id"=>"9", "city"=>"市区町村", "block_number"=>"番地", "building_name"=>"建物名", "phone_number"=>"09012345678", "item_id"=>3, "user_id"=>3} permitted: true>"building_name"=>"建物名", "phone_number"=>"09012345678"} permitted: false>
token
がいない!!!!!!!params.require(:item_order).permit(:token, ~~~~)このように、
require
とpermit
で書いているのになんで???解決
bindng.pry
で確認したところ、token
が生成されているのは確認できたので、生成されたトークンの移動の仕方に問題がありました。
現在token
が生成されている場所は、item_order
の外であり、params
の直下なので、
そもそものtoken
の場所が違ったということです。
binding.pry
で、試しにparams[:token]
と入力してみますと、pry(#<OrdersController>)> params[:token] => "tok_14078502197e031107d18bb7e428"と出ます。
なので、このように記述する必要があったということです。
****_controller.rbdef order_params params.require(:item_order).permit(:postal_code, :prefecture_id, :city, :block_number, :building_name, :phone_number).merge(item_id: @item[:id], user_id: current_user.id, token: params[:token]) end少し長いですが、
merge
メソッドの中にtoken
を記述しています。
item_order
の中にではなく、order_params
に引っ付けるための記述です。こうすることで、
order_params
に引っ付けることができたので、記述を変更した後にbinding.pry
で確認してみますと、pry(#<OrdersController>)> order_params => <ActionController::Parameters {"postal_code"=>"333-0000", "prefecture_id"=>"13", "city"=>"市区町村", "block_number"=>"番地", "building_name"=>"建物名", "phone_number"=>"09012345678", "item_id"=>3, "user_id"=>3, "token"=>"tok_14078502197e031107d18bb7e428"} permitted: true>これでバッチリです☆
order_params[:token]
で受け取れるようになり、決済も無事成功しました!!!まとめ
token
の居場所をしっかり理解する!
merge
メソッドを使用して、引っ付ける!
- 投稿日:2020-09-20T11:24:17+09:00
【Rails】PAYJPでトークン生成時のエラー解決
プログラミング学習中で、これはメモしたいと思ったものを、自分ように分かりやすく残しておきます。
PAYJPを導入して、決済システムを実装したい!
テスト用のカード情報を入力して、決済が完了している状態です。
決済が成功しない...
まずは、
payjp
のgemを導入し、学習した通りにJavaScriptのファイルを作ったり、コントローラーやビューへ記述しました。元の画面に戻ってしまう...
(んん?? トークンが空っぽだと???)これはコントローラーで、決済が成功しなかったら画面が変わらないように設定しており、エラーも表示させるようにしているからですが、最初は原因が分かりませんでした...
現在のコントローラーのコードはこちらです。(読みづらいかと思いますが、長くなるのでcreateアクションの部分のみ抜粋しております。)
****_controller.rbdef create @order = ItemOrder.new(order_params) if @order.valid? pay_item @order.save return redirect_to root_path else render 'new' end endカードの情報を受け取って、トークンを生成するためのJavaScriptの記述はこちらです。
****.jsconst pay = () => { Payjp.setPublicKey(process.env.PAYJP_PUBLIC_KEY); const form = document.getElementById("charge-form"); form.addEventListener("submit", (e) => { e.preventDefault(); const formResult = document.getElementById("charge-form"); const formData = new FormData(formResult); const card = { card_number: formData.get("card-number"), card_cvc: formData.get("card-cvc"), card_exp_month: formData.get("card-exp-month"), card_exp_year: `20${formData.get("card-exp-year")}`, }; Payjp.createToken(card, (status, response) => { if (status == 200) { const token = response.id; const renderDom = document.getElementById("charge-form"); const tokenObj = `<input value=${token} type="hidden" name='token'>`; renderDom.insertAdjacentHTML("beforeend", tokenObj); } document.getElementById("card-number").removeAttribute("name"); document.getElementById("card-cvc").removeAttribute("name"); document.getElementById("card-exp-month").removeAttribute("name"); document.getElementById("card-exp-year").removeAttribute("name"); document.getElementById("charge-form").submit(); document.getElementById("charge-form").reset(); }); }); }; window.addEventListener("load", pay);現状把握
どこが間違っているのかを見るために、
pry-rails
のgemを導入し、createアクションにbinding.pry
を記述して、params
の中身を確認しました。pry(#<OrdersController>)> params => <ActionController::Parameters {"authenticity_token"=>"pvPlrZPKlxtcYotX8kK4N/OjbTuWNkiq5bOCJxqNI+OYt59wXZuTClFnz7XDmAVWelWQJraGSTdIccYspnstlw==", "item_order"=>{"postal_code"=>"555-0000", "prefecture_id"=>"3", "city"=>"市区町村", "block_number"=>"番地", "building_name"=>"建物名", "phone_number"=>"09012345678"}, "controller"=>"orders", "action"=>"create", "id"=>"1"} permitted: false>たしかに、
token
がいない....
(authenticity_token
は全く別のものだそうです。)ということは、うまくtokenが生成されていない可能性があります。
次に、JavaScriptへの記述を調べるために,
console.log()
を使用して調べました。const pay = () => { Payjp.setPublicKey(process.env.PAYJP_PUBLIC_KEY); console.log(process.env.PAYJP_PUBLIC_KEY) // 環境変数が定義できているか確認 const form = document.getElementById("charge-form"); form.addEventListener("submit", (e) => { e.preventDefault(); const formResult = document.getElementById("charge-form"); const formData = new FormData(formResult); const card = { card_number: formData.get("card-number"), card_cvc: formData.get("card-cvc"), card_exp_month: formData.get("card-exp-month"), card_exp_year: `20${formData.get("card-exp-year")}`, }; console.log(card) // カード情報が受け取れているかの確認 Payjp.createToken(card, (status, response) => { console.log(status) // ステータスの数字を確認 if (status == 200) { const token = response.id; const renderDom = document.getElementById("charge-form"); const tokenObj = `<input value=${token} type="hidden" name='token'>`; renderDom.insertAdjacentHTML("beforeend", tokenObj); } document.getElementById("card-number").removeAttribute("name"); document.getElementById("card-cvc").removeAttribute("name"); document.getElementById("card-exp-month").removeAttribute("name"); document.getElementById("card-exp-year").removeAttribute("name"); document.getElementById("charge-form").submit(); document.getElementById("charge-form").reset(); }); }); }; window.addEventListener("load", pay);そして、binding_pryで停止しているので、そこでコンソールを確認すると、
カード情報はしっかり受け取れているみたいです!
しかし、ステータスが400
なのでトークンが生成できない...カード情報が受け取れているのに、なんで???
解決
そこで、知識のある方に相談させて頂き、ようやく解決しました!
取得するcard情報を格納する記述に誤りがあったみたいです。この部分の記述の、
****.jsconst card = { card_number: formData.get("card-number"), card_cvc: formData.get("card-cvc"), card_exp_month: formData.get("card-exp-month"), card_exp_year: `20${formData.get("card-exp-year")}`, };
card_number:
、card_cvc:
、card_exp_month:
、card_exp_year:
の記述をすると、正しくPayjpと通信しないみたいです。この形は決まっていると教わりまして、
number:
、cvc:
、exp_month:
、exp_year:
というように記述を直しました。const card = { number: formData.get("card-number"), cvc: formData.get("card-cvc"), exp_month: formData.get("card-exp-month"), exp_year: `20${formData.get("card-exp-year")}`, };と記述することで、
まとめ
PAYJP
を導入する時は、取得したcard情報を格納する記述を、決まっている形式の記述にする必要がある。
binding_pry
とconsole.log()
を使うことで、どこで不具合が起きているのかを探すことができる。
- 投稿日:2020-09-20T07:06:58+09:00
Webのフロントでコンポーネントを作る意味(コンポーネントライブラリへの感想)
0. 記事作成の動機:コンポーネントを作る煩わしさ
ReacやVueなどのJsでコンポーネントをつくるのがめんどくさいと感じるときが多々あった。
なので、コンポーネントを作る意味を再考してみる。以前、似たような記事を作ったけど、それをもう少し整理してみた。
SPA(Single Page Application) を採用するメリット再考(2020年)
1. コンポーネントを作ることの目的
knockoutjsを皮切りに10年以上SPAでのアプリを作ってきた。
最近感じるのは、VueやReactでのコンポーネント分割はすごく手間がかかるということ。
そこで改めて、コンポーネントを作ることを考えてみる。コンポーネントを作る目的は大きく分けて2つあると思う。
- 再利用
- コード分割(独立性)
以下、上記について補足。
(1) 再利用
コンポーネントを作ることの醍醐味であり、便利さを実感できるのがこの再利用性。
一度作ったコンポーネントをプロパティを替えていろいろな箇所で使える。
最近では、bit.devみたいなコンポーネントのリポジトリがあってそこで広く使えるようになると凄く便利。とはいえ、そこまで汎用性をもったコンポーネントというのはそうたくさんはできない。
どうしても汎用的に再利用しようとするとすごくシンプルなものになってくる。
そうすると、「わざわざつくらなくても、jQueryベースでもよくない?」となりかねない。プロジェクトでも、1度しか使わないようなコンポーネントがあったりしないだろうか?
では、1度しか使わないコンポーネントは要らないのか?
いや、そうではない。それが次の項目。(2) コード分割
コンポーネントのもう一つの側面は、ある目的や関心を小さく区切ってそれだけに焦点をあててコーディングすること。
そうすることで、コードの可読性があがったり、修正時の影響が極限化される。
これを踏まえた上で、いろいろなところで使われるようになると再利用性が実現される。とはいえ、1度しか使わないようなコンポーネントですら、上記のようなコーディング上のメリットがある。
2. JavascriptとHTML/CSSとの関係性:ライブラリの2つのグループ
コンポーネントライブラリを大きく分けると2つに分類できると思う。
- Javascript内にHTMLやCSSを内包する。(Javascriptベース型)
- Javascript、HTML、CSSを独立させるが一つのファイルで管理する。(分離パッケージ型)
以下、補足。
(1) Javascriptベース型
ReactやMithrillをベースにしたコンポーネントライブラリは、JSXによりJavascript(以下、Js)内にHTMLやCSSを内包する。そして、Jsベースでコンポーネントを作る。
(2) 分離パッケージ型
Angular、Vue、Svelte、Riotなどは、JsとHTML/CSSを分けて操作する。その代わり、HTMLに独自の属性(v-if)などを付与する。
3. ライブラリグループの評価
前節のグループのそれぞれの利点と問題点を指摘する。
(1) Javascriptベース型
a. 利点
Reactの場合、JsのクラスとHTML構造がマッピングされる。要は、HTMLのデータモデルをJsのクラスで表現する。
HTMLを扱う場合に、Js視点でコーディングでき、思考が1元化して分かりやすい。
また、HTMLとクラスをマッピングするという考え方により、コーディングルールが明確化し、コードの可読性が向上する。b. 問題点
ReactのようにJSをベースにすると、HTMLやCSSを別でコーダーさんがつくった場合、分割して、さらにJs内に取り込むという作業が発生する。
単なる分割は、分離パッケージ型でも発生するのだが、さらにJs内に取り込むという2段階の分割過程があり、意図通りに表示されないことがある。(2) 分離パッケージ型
a. 利点
Js、HTML、CSSをそれぞれ分離しながらも一つのコンポーネントファイル(たとえば、.vue)にまとめるので、関心が絞られて分かりやすい。また、js、HTML、CSSのコンテキスト(文脈)が別れているので読みやすく、コーダーさんが作ったものを分割するときも比較的手間が少ない。
タグ内に、独自属性を埋め込むことで、HTMLベースで構造ををプログラム化できるので、見た目を意識したい人には分かりやすい。ここは、Jsベースが良い人と好みが分かれるところかもしれない。
b. 問題点
HTMLやCSSをコーダーさんが作った場合分割する手間が発生する。
また、HTMLに独自属性を埋めていく手間が発生する。4. JavascriptとHTMLの関係:コンポーネントライブラリとjQuery
(1) 有名ライブラリはJsとHTMLが密接に依存する
上記にあげたライブラリを使うと、JsとHTMLは完全に独立しているわけでなく、HTMLを分割し、Jsに内包したり、HTMLに独自属性を付与するという依存性、手間が発生する。
そもそも、そうしないとコンポーネントはつくれないだろうか?
そんなことはない。(2) HTMLを壊さずに操作するjQueryの視点
jQueryはコンポーネントを作るためのライブラリではない。あくまでもHTML(DOM)を手軽に操作するシンタックスシュガー、ユーティリティライブラリである。
ただ、このライブラリの観点は、HTMLをReactのようなJsベース型のように内側から操作するのではなく、外側から操作する。
また、Vueのような分離パッケージ型のように独自属性も挿入しない。
つまり、jQueryは、HTMLと完全に独立した存在で、HTMLを変更せず、完全に外側から操作する。
コーダーさんがつくったコードを分割することも、変更することもなく操作することができる。(3) jQueryの欠点:Js内の値(モデル)とHTML(DOM)の関係性が曖昧、不定
jQueryは前述したとおり、コンポーネントをつくることを目的にはしていない。
なので、書き手によっていろいろな操作コードが書かれることになり、可読性が必ずしも保たれない。ReactのようにJsとHTMLの関係性が明確であれば、コードリーディングは用意であるが、jQueryの場合そのルールがないので、コードリーディンが必ずしも容易とはならない。
特に、データモデルというJs内でのデータとDOMとの関係性をどうつくるかは明示されないので、書き手任せになってしまう。
ここは、ReactのようなJsベース型ライブラリの方が優位である。
5. JsとHTMLを独立化させる利点
jQueryにはJsとの関係性が曖昧、不定になるという欠点はあったが、JsがHTMLと独立化するというのは利点もある。
(1) 構造と振る舞いを独立し、異なるデザインに対応
自社サービスなどでは、デザインやHTMLの構造がそう大きく変わることはないが、受託開発の場合、デザインもHTML、CSSも案件ごとに変わることがある。
それを自社開発したコンポーネントに合わせて変換させていくこともできるが、やはり分割や調整の手間が発生する。JsとHTMLが依存関係になければこのようなことはあまり発生しない。適宜、HTMLにidやclassなどセレクターを打ち直すか、Js側で指定するセレクターを変えるだけでよい。
(2) JsとHTML/CSSの技術的な変化速度の違い
Jsの技術変化の速度は速い。一方、HTMLやCSSの仕様が変化される速度はとても遅い。つまり、JsとHTMLが依存化している場合、Jsの変更にあわせてHTMLやCSSも変更させる可能性がある。
JsとHTMLを独立しておけば、Jsが変わろうともHTMLとCSSの構造やスタイルは変更することは少ない。6. JsとHTMLを独立させつつ対応付け
(1) ReactとjQueryの視点を合わせると良いのでは?
ここまでにみてきたとおり、有名なコンポーネントライブラリはHTMLとの依存性が高い。けれど、それは表裏一体で、HTMLとJsとの関係性が分かりやすくもある。
ただ、ソースコードとして、それらを混ぜ合わせておく必要があるだろうか?
コードのルールとしてそれがあればよいのではないだろうか?
そうすることで、JsとHTMLの依存による問題も起きないが、コードリーディングもしやすくなる。
つまり、ReactのようなDOMをJsのクラスで表現しつつ、jQueryのようにHTMLに変更を加えず、操作する方法であれば、可読性と独立性が保たれる。ただし、VueのようにHTMLに属性をうって、HTMLをみながら構造を操作するのが好きな人は直感性が失われるかもしれない。
ただ、コーダーさんが作ったものを分割する手間は減るので、かなり効率性はあがると思う。
ちなみに、外側からDOMを対応させるクラスを作るのであればAngularのComponentクラスがよいと思う。クラスのプロパティにDOMを指示するselectorを持ったせて、対応づけをするのは良いと思う。
Angular公式:Introduction to components and templates
componentexport class HeroListComponent implements OnInit { heroes: Hero[]; selectedHero: Hero; constructor(private service: HeroService) { } ngOnInit() { this.heroes = this.service.getHeroes(); } selectHero(hero: Hero) { this.selectedHero = hero; } }(2) Simulacra.js:JsとHTMLを独立化させたデータバインディングライブラリ
このような考えができるライブラリとしては、以下のライブラリがある。要は、SPAでいうところのデータバインディングの問題で、HTMLを変更しないデータバインディングライブリである。
ただし、双方向バインディングではなく一方向だけである。そもそも、双方向である必要はいつもあるとは限らない。
JsでHTMLのセレクターをキーとするオブジェクトリテラルをつくり、ライブラリに渡すと、データバインディングしてくれる。
template<template id="product"> <h1 class="name"></h1> <div class="details"> <div><span class="size"></span></div> <h4 class="vendor"></h4> </div> </template>上記のタグに対応するモデルとなるオブジェクトリテラルを作る。
modelvar state = { name: 'Pumpkin Spice Latte', details: { size: [ 'Tall', 'Grande', 'Venti' ], vendor: 'Coffee Co.' } }
バインディング。bindvar bindObject = require('simulacra') // or `window.simulacra` var template = document.getElementById('product') var node = bindObject(state, [ template, { name: '.name', details: [ '.details', { size: '.size', vendor: '.vendor' } ] } ]) document.body.appendChild(node)個人的に、全体的な視点や発想はいいと思うが、モデルとセレクタのバインディングコードが無駄な気がしている。
この部分もなくすことができる。(自作したことがある)(3) eleventy.js(静的サイトジェネレーター):HTMLの再利用
JsとHTMLを分けると、HTMLを再利用するときどうするのかというと、シンプルに静的サイトジェネレーターのinclude機能を使えばいいと思います。
オススメの静的サイトジェネレーターはJsで動く「11ty:イレブンティ」。
デプロイ前に動的に作りたいなら、11tyでJsのフィルター関数を作ってしまえばいいと思います。
コーディングが楽です。わざわざGatsbyみたいにJsでコンポーネント作らなくても、pug、nanjucks、markdown、htmlなどでHTMLを作れます。
もちろん、フィルター関数とかも作れるので、繰り返し作業とか動的な処理も対応できます。7. まとめ:コンポーネントという「発想・視点」は大切
(1) コンポーネントライブラリを使う煩わしさ
この記事を書いた動機は、受託をしていると毎回デザインがかわり、HTMLやCSSも変わる事が多く、その都度、コンポーネントを別途つくる手間が凄くめんどくさかった。
また、わざわざコーディングしてあるHTMLをバラすという手間も無駄に感じた。
その他、VuexやReduxなどの状態管理が煩わしく、そもそも双方向バインディングなどをせず、ワントランザクションでSPAをつくれば、状態も複雑化しないので、使う必要をあまり感じなかった。
というように、コンポーネントという「考え方・設計」はいいのだけど、その実現方法について無駄や手間を感じたので、こうした記事を書いてみた。
(2) コンポーネントライブラリを使う場所(自社サービス・大規模開発)
利用シーンによっては煩わしいのだけど、自社サービスの大規模開発の場合は、ReactやVueなどの有名なコンポーネントライブラリを使うのがよいと思う。
その方がドキュメントの整備も公式に任せればいいし、ネット上に情報が沢山ある。またコーディングを規制することができるので、書き方をプログラマー間で統一させやすい。
自社サービスの場合、デザインやHTMLの構造などもコンポーネント側に合わせた開発できる。裏返せば、受託で毎回デザインが変わる場合、必ずしもコンポーネントライブラリを使うべきかは考えようだと思う。
以下に分かりやすいコーディング設計ができるかに依存する。でも、それができないと考えるなら、有名なライブラリを使うのが無難だと思う。
(3) コンポーネントは「発想・視点」が大切
コードを独立化、局所化するという発想がコンポーネントはよいと思う。
これは疑義なく支持したい。
けれど、その実現方法については、ケースバイケースかなという気もする。
自社サービスか受託開発、大規模か小規模か、などプロジェクトの前提条件によっても異なると思う。とにかくコンポーネントを作る手間はどんなものでも発生するのだけど、できるだけその手間が最小化するものがよいと思う。
- 投稿日:2020-09-20T07:06:58+09:00
Webのフロントでコンポーネントを作る意味(JsとHTMLの依存性、コンポーネントライブラリへの感想)
0. 記事作成の動機:コンポーネントを作る煩わしさ
ReacやVueなどのJsでコンポーネントをつくるのがめんどくさいと感じるときが多々あった。
なので、コンポーネントを作る意味を再考してみる。以前、似たような記事を作ったけど、それをもう少し整理してみた。
SPA(Single Page Application) を採用するメリット再考(2020年)
1. コンポーネントを作ることの目的
knockoutjsを皮切りに10年以上SPAでのアプリを作ってきた。
最近感じるのは、VueやReactでのコンポーネント分割はすごく手間がかかるということ。
そこで改めて、コンポーネントを作ることを考えてみる。コンポーネントを作る目的は大きく分けて2つあると思う。
- 再利用
- コード分割(独立性)
以下、上記について補足。
(1) 再利用
コンポーネントを作ることの醍醐味であり、便利さを実感できるのがこの再利用性。
一度作ったコンポーネントをプロパティを替えていろいろな箇所で使える。
最近では、bit.devみたいなコンポーネントのリポジトリがあってそこで広く使えるようになると凄く便利。とはいえ、そこまで汎用性をもったコンポーネントというのはそうたくさんはできない。
どうしても汎用的に再利用しようとするとすごくシンプルなものになってくる。
そうすると、「わざわざつくらなくても、jQueryベースでもよくない?」となりかねない。プロジェクトでも、1度しか使わないようなコンポーネントがあったりしないだろうか?
では、1度しか使わないコンポーネントは要らないのか?
いや、そうではない。それが次の項目。(2) コード分割
コンポーネントのもう一つの側面は、ある目的や関心を小さく区切ってそれだけに焦点をあててコーディングすること。
そうすることで、コードの可読性があがったり、修正時の影響が極限化される。
これを踏まえた上で、いろいろなところで使われるようになると再利用性が実現される。とはいえ、1度しか使わないようなコンポーネントですら、上記のようなコーディング上のメリットがある。
2. JavascriptとHTML/CSSとの関係性:ライブラリの2つのグループ
コンポーネントライブラリを大きく分けると2つに分類できると思う。
- Javascript内にHTMLやCSSを内包する。(Javascriptベース型)
- Javascript、HTML、CSSを独立させるが一つのファイルで管理する。(分離パッケージ型)
以下、補足。
(1) Javascriptベース型
ReactやMithrillをベースにしたコンポーネントライブラリは、JSXによりJavascript(以下、Js)内にHTMLやCSSを内包する。そして、Jsベースでコンポーネントを作る。
(2) 分離パッケージ型
Angular、Vue、Svelte、Riotなどは、JsとHTML/CSSを分けて操作する。その代わり、HTMLに独自の属性(v-if)などを付与する。
3. ライブラリグループの評価
前節のグループのそれぞれの利点と問題点を指摘する。
(1) Javascriptベース型
a. 利点
Reactの場合、JsのクラスとHTML構造がマッピングされる。要は、HTMLのデータモデルをJsのクラスで表現する。
HTMLを扱う場合に、Js視点でコーディングでき、思考が1元化して分かりやすい。
また、HTMLとクラスをマッピングするという考え方により、コーディングルールが明確化し、コードの可読性が向上する。b. 問題点
ReactのようにJsをベースにすると、HTMLやCSSを別でコーダーさんがつくった場合、分割して、さらにJs内に取り込むという作業が発生する。
単なる分割は、分離パッケージ型でも発生するのだが、さらにJs内に取り込むという2段階の分割過程があり、意図通りに表示されないことがある。(2) 分離パッケージ型
a. 利点
Js、HTML、CSSをそれぞれ分離しながらも一つのコンポーネントファイル(たとえば、.vue)にまとめるので、関心が絞られて分かりやすい。また、Js、HTML、CSSのコンテキスト(文脈)が別れているので読みやすく、コーダーさんが作ったものを分割するときも比較的手間が少ない。
タグ内に、独自属性を埋め込むことで、HTMLベースで構造ををプログラム化できるので、見た目を意識したい人には分かりやすい。ここは、Jsベースが良い人と好みが分かれるところかもしれない。
b. 問題点
HTMLやCSSをコーダーさんが作った場合分割する手間が発生する。
また、HTMLに独自属性を埋めていく手間が発生する。4. JavascriptとHTMLの関係:コンポーネントライブラリとjQuery
(1) 有名ライブラリはJsとHTMLが密接に依存する
上記にあげたライブラリを使うと、JsとHTMLは完全に独立しているわけでなく、HTMLを分割し、Jsに内包したり、HTMLに独自属性を付与するという依存性、手間が発生する。
そもそも、そうしないとコンポーネントはつくれないだろうか?
そんなことはない。(2) HTMLを壊さずに操作するjQueryの視点
jQueryはコンポーネントを作るためのライブラリではない。あくまでもHTML(DOM)を手軽に操作するシンタックスシュガー、ユーティリティライブラリである。
ただ、このライブラリの観点は、HTMLをReactのようなJsベース型のように内側から操作するのではなく、外側から操作する。
また、Vueのような分離パッケージ型のように独自属性も挿入しない。
つまり、jQueryは、HTMLと完全に独立した存在で、HTMLを変更せず、完全に外側から操作する。
コーダーさんがつくったコードを分割することも、変更することもなく操作することができる。(3) jQueryの欠点:Js内の値(モデル)とHTML(DOM)の関係性が曖昧、不定
jQueryは前述したとおり、コンポーネントをつくることを目的にはしていない。
なので、書き手によっていろいろな操作コードが書かれることになり、可読性が必ずしも保たれない。ReactのようにJsとHTMLの関係性が明確であれば、コードリーディングは容易であるが、jQueryの場合そのルールがないので、コードリーディンが必ずしも容易とはならない。
特に、データモデルというJs内でのデータとDOMとの関係性をどうつくるかは明示されないので、書き手任せになってしまう。
ここは、ReactのようなJsベース型ライブラリの方が優位である。
5. JsとHTMLを独立化させる利点
jQueryにはJsとの関係性が曖昧、不定になるという欠点はあったが、JsがHTMLと独立化するというのは利点もある。
(1) 構造と振る舞いを独立し、異なるデザインに対応
自社サービスなどでは、デザインやHTMLの構造がそう大きく変わることはないが、受託開発の場合、デザインもHTML、CSSも案件ごとに変わることがある。
それを自社開発したコンポーネントに合わせて変換させていくこともできるが、やはり分割や調整の手間が発生する。JsとHTMLが依存関係になければこのようなことはあまり発生しない。適宜、HTMLにidやclassなどセレクターを打ち直すか、Js側で指定するセレクターを変えるだけでよい。
(2) JsとHTML/CSSの技術的な変化速度の違い
Jsの技術変化の速度は速い。一方、HTMLやCSSの仕様が変化される速度はとても遅い。つまり、JsとHTMLが依存化している場合、Jsの変更にあわせてHTMLやCSSも変更させる可能性がある。
JsとHTMLを独立しておけば、Jsが変わろうともHTMLとCSSの構造やスタイルは変更することは少ない。6. JsとHTMLを独立させつつ対応付け
(1) ReactとjQueryの視点を合わせると良いのでは?
ここまでにみてきたとおり、有名なコンポーネントライブラリはHTMLとの依存性が高い。けれど、それは表裏一体で、HTMLとJsとの関係性が分かりやすくもある。
ただ、ソースコードとして、それらを混ぜ合わせておく必要があるだろうか?
コードのルールとしてそれがあればよいのではないだろうか?
そうすることで、JsとHTMLの依存による問題も起きないが、コードリーディングもしやすくなる。
つまり、ReactのようなDOMをJsのクラスで表現しつつ、jQueryのようにHTMLに変更を加えず、操作する方法であれば、可読性と独立性が保たれる。ただし、VueのようにHTMLに属性をうって、HTMLをみながら構造を操作するのが好きな人は直感性が失われるかもしれない。
ただ、コーダーさんが作ったものを分割する手間は減るので、かなり効率性はあがると思う。
ちなみに、外側からDOMを対応させるクラスを作るのであればAngularのComponentクラスがよいと思う。クラスのプロパティにDOMを指示するselectorを持ったせて、対応づけをするのは良いと思う。
Angular公式:Introduction to components and templates
componentexport class HeroListComponent implements OnInit { heroes: Hero[]; selectedHero: Hero; constructor(private service: HeroService) { } ngOnInit() { this.heroes = this.service.getHeroes(); } selectHero(hero: Hero) { this.selectedHero = hero; } }(2) Simulacra.js:JsとHTMLを独立化させたデータバインディングライブラリ
このような考えができるライブラリとしては、以下のライブラリがある。要は、SPAでいうところのデータバインディングの問題で、HTMLを変更しないデータバインディングライブリである。
ただし、双方向バインディングではなく一方向だけである。そもそも、双方向はいつも必要だろうか?
JsでHTMLのセレクターをキーとするオブジェクトリテラルをつくり、ライブラリに渡すと、データバインディングしてくれる。
template<template id="product"> <h1 class="name"></h1> <div class="details"> <div><span class="size"></span></div> <h4 class="vendor"></h4> </div> </template>上記のタグに対応するモデルとなるオブジェクトリテラルを作る。
modelvar state = { name: 'Pumpkin Spice Latte', details: { size: [ 'Tall', 'Grande', 'Venti' ], vendor: 'Coffee Co.' } }
バインディング。bindvar bindObject = require('simulacra') // or `window.simulacra` var template = document.getElementById('product') var node = bindObject(state, [ template, { name: '.name', details: [ '.details', { size: '.size', vendor: '.vendor' } ] } ]) document.body.appendChild(node)個人的に、全体的な視点や発想はいいと思うが、モデルとセレクタのバインディングコードが無駄な気がしている。
この部分もなくすことができる。(自作したことがある)(3) eleventy.js(静的サイトジェネレーター):HTMLの再利用
JsとHTMLを分けると、HTMLを再利用するときどうするのかというと、シンプルに静的サイトジェネレーターのinclude機能を使えばいいと思います。
オススメの静的サイトジェネレーターはJsで動く「11ty:イレブンティ」。
デプロイ前に動的に作りたいなら、11tyでJsのフィルター関数を作ってしまえばいいと思います。
コーディングが楽です。わざわざGatsbyみたいにJsでコンポーネント作らなくても、pug、nanjucks、markdown、htmlなどでHTMLを作れます。
もちろん、フィルター関数とかも作れるので、繰り返し作業とか動的な処理も対応できます。7. まとめ:コンポーネントという「発想・視点」は大切
(1) コンポーネントライブラリを使う煩わしさ
この記事を書いた動機は、受託をしていると毎回デザインがかわり、HTMLやCSSも変わる事が多く、その都度、コンポーネントを別途つくる手間が凄くめんどくさかった。
また、わざわざコーディングしてあるHTMLをバラすという手間も無駄に感じた。
その他、VuexやReduxなどの状態管理が煩わしく、そもそも双方向バインディングなどをせず、ワントランザクションでSPAをつくれば、状態も複雑化しないので、使う必要をあまり感じなかった。
というように、コンポーネントという「考え方・設計」はいいのだけど、その実現方法について無駄や手間を感じたので、こうした記事を書いてみた。
(2) コンポーネントライブラリを使う場所(自社サービス・大規模開発)
利用シーンによっては煩わしいのだけど、自社サービスの大規模開発の場合は、ReactやVueなどの有名なコンポーネントライブラリを使うのがよいと思う。
その方がドキュメントの整備も公式に任せればいいし、ネット上に情報が沢山ある。またコーディングを規制することができるので、書き方をプログラマー間で統一させやすい。
自社サービスの場合、デザインやHTMLの構造などもコンポーネント側に合わせた開発できる。裏返せば、受託で毎回デザインが変わる場合、必ずしもコンポーネントライブラリを使うべきかは考えようだと思う。
以下に分かりやすいコーディング設計ができるかに依存する。でも、それができないと考えるなら、有名なライブラリを使うのが無難だと思う。
(3) コンポーネントは「発想・視点」が大切
コードを独立化、局所化するという発想がコンポーネントはよいと思う。
これは疑義なく支持したい。
けれど、その実現方法については、ケースバイケースかなという気もする。
自社サービスか受託開発、大規模か小規模か、などプロジェクトの前提条件によっても異なると思う。とにかくコンポーネントを作る手間はどんなものでも発生するのだけど、できるだけその手間が最小化するものがよいと思う。
- 投稿日:2020-09-20T06:24:34+09:00
vercel・typescript・next.js・redux toolkit・express・bootstrap5・react-string-replace。無料でSSRなWebアプリを動かす。
Node.jsを無料で使える神サービスVercelに作ったWebアプリを乗せる。
https://vercel.com成果物
リポジトリ
https://github.com/yuzuru2/nextboard2
構成
フロントエンド: Vercel(無料) Next.js
バックエンド: Vercel(無料) TypeScript Express
データベース: MongoDB Atlas(DBaaS) 無料
ワイの成果物
https://qiita.com/yuzuru2/items/b5a34ad07d38ab1e7378
①コード共有サイト(SSR) Next.js
https://code.itsumen.com②画像共有サイト(SPA) React
https://gazou.itsumen.com③チャット(SSR) Nuxt.js
https://nuxtchat.itsumen.com④チャット(SPA) React
https://chat4.itsumen.com⑤掲示板(SSR) Next.js
https://nextboard.itsumen.com⑥掲示板(SPA) Vue
https://board.itsumen.com⑦レジの店員を呼ぶスマホアプリ(Android)
https://play.google.com/store/apps/details?id=com.itsumen.regi&hl=ja⑧ブログ(静的サイトジェネレータ) Hugo
https://yuzuru.itsumen.comDM送信先
Twitter: https://twitter.com/yuzuru_program
LINE: https://line.me/ti/p/-GXpQkyXAm
- 投稿日:2020-09-20T06:05:58+09:00
Angular Observer patternについて
概要
AngularにおけるRxJSを利用したObserver patternはRxJSの基礎であると共に、躓きやすいポイントの一つです。
大体書いた直後とかは何やっているか理解できるけど、しばらくたって同じコードを見ると「Subjectって何やってるんだっけ?」「subscribeがどこにも見当たらないんだが?」「Observer? Observable?」みたいな状態なってしまい、それを何回も繰り返してしまいます(サンプル数1)。
このドキュメントを通して、AnbularでのObserver patternにおける登場人物が何をやっていて、お互いにどういう風に関連しあっているのかを整理をして行きたいと思います。Observer pattern?
Observerパターンとはデザインパターンの一種です。wikipediaで書かれているのは「プログラム内のオブジェクトのイベントを他のオブジェクトへ通知する処理」とのこと。自分はこの説明を聞いたとき具体的に何ができるのか今一想像できませんでした。
それよりも私はpublish/subscriber patternの一部と言われたほうがイメージがしやすかったです。publish/subscriber
publish/subscribeとは「メッセージの送信者(publisher)が特定の受信者(subscriber)を想定せずにメッセージを送るようプログラムされたものである。」とwikipediaにあります。プログラムに限らずこのモデルは様々な所で利用されており、メーリングリストなどは一番有名なpublish/subscribeer patternの利用用途ですね。
Observer patternはその中でpublisherが管理しているオブジェクトの変更の通知をsubscriberが購読(subscribe)するという特殊な用途の一つなのではないかなと考えています。全体図
このObservable patternでデータがどのように流れるかの概略図は以下になります。
Publisherが監視しているObjectに変更があった場合にひとつのPublisherから複数のSubscriberに「データがAからBに変わりましたよ」というデータが一方的に流れるようなデータの流れをします。
これはデータを取得したいSubscriberはポーリングなど積極的にデータの変更を検知する努力をする必要がなく、ただSubscriberは口を開けて待っているだけでPublisherが「AからBに変更したよ」という情報を送ってくれることになります。やばいですね。では一体どうやって実現しているのでしょうか。
上の図よりももうちょっと内部実装よりな図が以下になりますこの図を元にデータの受け取り側(Subscriber)と送り側(Publisher)が何をしているのか見ていきましょう。
Subscriber(Consumer)は何をしているの?
登場人物その1。Subscriberは最終的にPublisherから値を受け取りたいのです。
そのためには以下の2ステップを踏みます。
1. observerを定義
2. そのobserverをpublisherに登録(購読)observerとは
observerとは
next()
,error
,complete()
を実装した関数を持つオブジェクトです。
next
は変更検知したというデータを受け取ってPublihserが実行するハンドラーで、例えばこのnextハンドラーを通じてSubscriberAが持っているObjectに変更を反映させるということもできます。
error
はなにかエラーが発生したときに実行するハンドラー(実装は任意)
complete
は実行が完了したときに実行されるハンドラー(実装は任意)この定義したハンドラーは値の変更が通知されたときにSubscriberがその値をどうしたいのかが定義されています。
observerをPublisherに登録する(購読する)
定義したobserverはPublihserに登録します。
Angular(RxJS)でよく見るsubscibe(observer)
がまさにその購読処理になります。Publisherは何をしているの?
登場人物その2。PublisherはSubscriberが登録したハンドラーを監視しているオブジェクトの変更をトリガーに実行します。RxJSではObservableというインスタンスのsubscirbe関数にSubscriberが定義したobserverを渡すことで実現します。
Observableとは
Observableは以下のことをしてくれます。
- データを流す
- そのデータを流したときに実行する関数(observer)を登録する。データを流す
このデータの流れは色々な所から取得することができ、それはhttpリクエストだったりとか、イベントだったりとか、またRxJSならSubjectを利用すれば
next
関数でデータを流すこともできますPublisherは以下のことをします。
- 監視対象のオブジェクトの変更を検知
- 登録されているSubscriberのハンドラーを実行する。
- この登録される関数はいくつでも登録可能なので1対Nの通信(一方的にデータを垂れ流すだけですが)が可能
- 1はPublisherでNはSubscriber。Angularにおける実装
TBD
なんか文章量も多くなってきたし、別で書くかも
- 投稿日:2020-09-20T05:23:25+09:00
vuejsのwatchで複数の値を監視したい!!!
初めに
Vuejsのwatchはとても便利で値の変化を監視することができます。
実現したかったこと
- watchで2つの値を監視したい。
とのことで、僕は最初に
test-case1.vue<template> <div> <input type="number" v-model="numberObj1"> <input type="number" v-model="numberObj2"> <p>{{ tashizan }}</p> </div> </template> <script> data() { return { numberObj1: '', numberObj2: '', tashizan: '' } }, watch: { numberObj1 & numberObj2 () { this.tashizan = this.numberObj1 + this.numberObj2; } } </script>便利なVuejsだしこれで行けるやろ!ってどこかで思ってました。
そんなに世界は甘くはなかった。これでは行けなかった。結論
sample.vue<script> (省略) computed: { MathObject() { return [this.numberObj1,this.numberObj2]; } }, watch: { MathObject (val) { this.tashizan = this.numberObj1 + this.numberObj2; } } </script>この方法で複数の値を監視することができました。
サンプルコードは短いからみやすいですけど、実業務になってくるとコードが長くなってくるのでこのMathObjectに何があるか分からないため、computedまで確認する必要性があるみたいです。
他の方とコードを共有している場合はコメントアウトなどを利用して変数を書いてあげると親切かもしれませんね。
- 投稿日:2020-09-20T00:34:36+09:00
【javascript】TABLEセル内のテキストデータを二次元配列で全取得(colspan/rowspan対応)
何かあればご指摘いただけると助かります。
概要
head1 head2 haed3 head4 head5 1-1 1-2 1-3 1-4 1-5 2-1 2-2 2-3 2-4 2-5 3-1 3-2 3-3 3-4 3-5 こんな感じのテーブルを
[ ['head1','head2', 'haed3', 'head4', 'head5'], ['1-1', '1-2', '1-3', '1-4', '1-5'], ['2-1', '2-2', '2-3', '2-4', '2-5'], ['3-1', '3-2', '3-3', '3-4', '3-5'] ]二次元配列として取得します。
結合されたセルに対しては、結合元と同じテキストで埋めます。コード
const getTableTexts = table => { const tableTexts = []; const tableRows = table.rows; //行数 const numOfRows = tableRows.length; //列数(1行目参照) const numOfCols = [...tableRows[0].cells].reduce( (n, cell) => n + (cell.colSpan-0), 0); //rowspan管理用 const rowspans = Array(numOfCols).fill(0); const getLineTexts = lineIdx => { const lineTexts = []; //colspan分の穴埋め + 行を配列で取得[rowspan, text-data] const dataOfLine = [...tableRows[lineIdx].cells].flatMap( cell => Array(cell.colSpan-0).fill([cell.rowSpan-1, cell.textContent]) ); let dataIdx = 0; //dataOfLine用のindex for (let i=0; i<numOfCols; i++) { //列ループ if (rowspans[i]) { //数値が1以上:rowspan範囲内なので一行上のデータで穴埋め rowspans[i]--; lineTexts.push(tableTexts[lineIdx-1][i]); } else { //0の時:rowspan範囲外なのでdataOfLineのデータを挿入 const [num, text] = dataOfLine[dataIdx]; if (num) rowspans[i] += num; lineTexts.push(text); dataIdx++; } } tableTexts.push(lineTexts); }; //行ループ for (let i=0; i<numOfRows; i++) getLineTexts(i); return tableTexts; }; const tableTexts = getTableTexts(document.getElementsByTagName('table')[0]);