- 投稿日:2020-09-20T23:54:36+09:00
【Vue.js】Promiseのcallback処理にComponentで定義したmethodの使用
概要
Vue.jsを使用する際、外部の通信などでPromiseを使用することはあると思います。Promiseを使用する際に、callbackで事前にComponentで定義したメソッドを使おうとしたのですが、うまく呼び出せずにはまったのでメモとして残しておきます。
はまったこと
以下のようにfirebaseにクエリを投げて、取得した内容を事前に定義したメソッドで下記のようなイメージで処理します。この際に、定義したmethodの
processDoc
がundefinedになってしまいます。methods: { getFirebaseData() { const db = firebase.firestore(); // firestoreの最新情報の取得 const docRef = db.collection("test_collection").doc("test_doc_id"); docRef .get() .then(function(doc) { // 受け取ったdocの処理 this.processDoc(doc); }) .catch(function(error) { console.log(error); }); }, processDoc(doc) { // ここでデータを処理 } }対応
how to call a method in the Promise in Vue componentによると、コールバックの処理時に、無名関数でなくアロー関数を使う対応が挙げられています。なお、無名関数を使う場合は、事前にPromiseの外側でthisの変数を定義してから関数を呼び出す形になるそうです。アロー関数を使って下記のように書き直すと、methodの呼び出しができました。
methods: { getFirebaseData() { const db = firebase.firestore(); // firestoreの最新情報の取得 const docRef = db.collection("test_collection").doc("test_doc_id"); docRef .get() .then((doc) => { // 受け取ったdocの処理 this.processDoc(doc); }) .catch((error) => { console.log(error); }); }, processDoc(doc) { // ここでデータを処理 } }なぜか
Vue.jsに書いてある「アロー関数は、this が期待する Vue インスタンスではなく・・・」とは?に解説されていますが、無名関数とアロー関数で同じ機能と思いきやthisの取り扱いが変わってきます。従来の無名関数では、「それが何を指し示すかは文脈によって変化する」、「そのメソッドにアクセスしたオブジェクト(インスタンス)が、this」となるのに対し「アロー関数は呼び出された場所をthisとする」といった違いがあります。
Promiseのコールバックにおいては、無名関数だと指し示されるオブジェクトが自分自身なので、その外のmethodが参照できないです。アロー関数だと呼び出し元からthisのスコープが引き継がれるので、methodも参照ができるわけです。
- 投稿日:2020-09-20T23:26:38+09:00
【Vue.js】独自コンポーネントのpropsをauto-completeしたい(願望)
大量にあるコンポーネントpropsを手で書くのは怠いので、どうにか自動で生成できないか?と調べに調べ、若干面倒ではありますが生成することが出来たので記事にしようと思います。
つまり、vetur用のhelper-json(tags.jsonとattributes.json)を自動生成する備忘録です。自動生成したいコンポーネント群をまとめてライブラリ化する
ビルド用スクリプトを記述する
コンポーネント群をまとめてライブラリ化するためのスクリプトを作成します。
自分はsrc/build.components.js
としました。build.component.js// require.contextで出力したいコンポーネント群を取得する // サブディレクトリの検索をON const files = require.context('出力したいコンポーネント群の相対パス', true, /.*\/vue$/); let components = {}; // コンポーネント群をグローバル登録する時みたいな感じでcomponentsオブジェクトに格納していく Object.values(files.keys()).forEach((key) => { const component = files(key); const name = component.default.name || key .split("/") .pop() .replace(/\.\w+$/, ""); components[name] = files(key).default; }); // 格納したコンポーネント群のオブジェクトをexport export default components;ビルドするために、package.jsonにnpmスクリプトを追記する
package.json
にビルド用scriptを追記(scripts部分抜粋)package.jsonscripts: { "serve": "vue-cli-service serve", "build": "vue-cli-service build", "lint": "vue-cli-service lint", "build:components": "vue-cli-service build --target lib --name components ./src/build.components.js", // これを追記 }
npm run build:components
を実行するとdist
に以下画像のようにファイルが出力されます。
vue.config.js
でoutputDir
を弄っている場合は各自の環境に合わせて読み替えてください。tags.jsonとattributes.jsonを生成するためのスクリプトを作成する
ライブラリ化で出力されたjsからコンポーネント群を読み込み、
tags.json
とattributes.json
を生成するためのスクリプトを作成します。
自分はsrc/build.vetur.js
としました。build.vetur.jsconst fs = require("fs"); const Vue = require("vue"); // .defaultを付与しないとexportされたオブジェクトを読み込めない const files = require("../dist/components.common").default; // 読み込んだコンポーネント群をVueオブジェクトに登録する Object.values(files).forEach((key) => { const component = key; const name = component.name || key .split("/") .pop() .replace(/\.\w+$/, ""); Vue.component(name, component); }); const hyphenateRE = /\B([A-Z])/g; function hyphenate(str) { return str.replace(hyphenateRE, "-$1").toLowerCase(); } function parseFunctionParams(func) { const groups = /function\s_.*\((.*)\)\s\{.*/i.exec(func); if (groups && groups.length > 1) return `(${groups[1]}) => {}`; else return "null"; } function getPropType(type) { if (Array.isArray(type)) { return type.map((t) => getPropType(t)); } if (!type) return "any"; return type.name.toLowerCase(); } function getPropDefault(def, type) { if ( def === "" || (def == null && type !== "boolean" && type !== "function") ) { return "undefined"; } else if (typeof def === "function" && type !== "function") { def = def.call({}); } if (type === "boolean") { return def ? "true" : "false"; } if (type === "string") { return def ? `'${def}'` : def; } if (type === "function") { return parseFunctionParams(def); } return def; } function genProp(name, prop) { const type = getPropType(prop.type); return { name, type, default: getPropDefault(prop.default, type), }; } function parseProps(component, array = []) { const options = component.options; const props = options.props || {}; Object.keys(props).forEach((key) => { const generated = genProp(key, props[key], component.name); array.push(generated); }); return array.sort((a, b) => a.name > b.name); } function writeJsonFile(obj, file) { const stream = fs.createWriteStream(file); stream.once("open", () => { stream.write(JSON.stringify(obj, null, 2)); stream.end(); }); } const components = {}; // Vue.componentで登録したコンポーネント群が格納されている const installedComponents = Vue.options._base.options.components; // 不要なコンポーネントを除外する(何故か登録されている) const excludes = ["KeepAlive", "Transition", "TransitionGroup"]; for (const name in installedComponents) { if (excludes.includes(name)) continue; const component = installedComponents[name]; const kebabName = hyphenate(name); components[kebabName] = { props: parseProps(component) }; } const tags = Object.keys(components).reduce((t, k) => { t[k] = { attributes: components[k].props .map((p) => p.name.replace(/([A-Z])/g, (g) => `-${g[0].toLowerCase()}`)) .sort(), description: "", }; return t; }, {}); const attributes = Object.keys(components).reduce((attrs, k) => { const tmp = components[k].props.reduce((a, prop) => { let type = prop.type; if (!type) type = ""; else if (Array.isArray(type)) type = type.map((t) => t.toLowerCase()).join("|"); else type = type.toLowerCase(); const name = prop.name.replace(/([A-Z])/g, (g) => `-${g[0].toLowerCase()}`); a[`${k}/${name}`] = { type, description: "", }; return a; }, {}); return Object.assign(attrs, tmp); }, {}); // 自分はsrc/utils/vetur下にtags.jsonとattributes.jsonを吐き出したいので // src/utils/veturがない場合は作成するようにしています if (!fs.existsSync("src/utils/vetur")) { fs.mkdirSync("src/utils/vetur", 0o755); } // tags.jsonとattributes.jsonを出力 writeJsonFile(tags, "src/utils/vetur/tags.json"); writeJsonFile(attributes, "src/utils/vetur/attributes.json"); console.log("tags.jsonとattributes.jsonが生成されました");コンポーネントを読み込んだ後のjsonファイルを生成する部分はvuetify.jsの
api-generator
にあったビルドスクリプトをほぼパクってきて、自分用に要らない部分を削っています。package.jsonにnpmスクリプトを追記する
スクリプトの作成ができたので
package.json
にスクリプトを追記しますpackage.jsonscripts: { "serve": "vue-cli-service serve", "build": "vue-cli-service build", "lint": "vue-cli-service lint", "build:api": "vue-cli-service build --target lib --name components ./src/build.components.js", "gen:api": "node ./src/build.vetur.js" // 追記 }json生成、package.jsonにvetur設定を追記する
veturにカスタムtags.jsonとattributes.jsonを読み込ませるための設定を記述します
"vetur": { "tags": "src/utils/vetur/tags.json", // 自動生成させるtags.jsonとattributes.jsonのパスを記述 "attributes": "src/utils/vetur/attributes.json" },
npm run gen:api
を実行するとエラーが出なければsrc/utils/vetur
ディレクトリが作成されtags.json
とattributes.json
が出力されているかと思います。
VSCodeを再起動すれば、tagの補完とpropsの補完が有効になっているかと思います。問題点
1つ問題として、
build.vetur.js
でVueC3を使ったコンポーネントを読み込むとwindow is not defined
と出てしまいます。
出力されたjsを見るとSVG関連で使っている様で、node環境ではwindowオブジェクト
が無いのでエラーが出るのも致し方無い。
もしこのようなエラーが出てしまう場合は、ライブラリ化するコンポーネントから除外してください。
現状、僕の知識では解決方法がわからないので、手動追記(スクリプト内に埋め込む)する形を取っています。(以下例)build.vetur.jstags["graph"] = { attributes: ["type"], desctiption: "", }; attributes["graph/type"] = { type: "string", description: "", };
tags,attributes
共にconst
ではなくlet
に変更するのをお忘れなく
- 投稿日:2020-09-20T23:26:38+09:00
【Vue.js】独自コンポーネントのタグとpropsをauto-completeしたい(願望)
大量にあるコンポーネントpropsを手で書くのは怠いので、どうにか自動で生成できないか?と調べに調べ、若干面倒ではありますが生成することが出来たので記事にしようと思います。
つまり、vetur用のhelper-json(tags.jsonとattributes.json)を自動生成する備忘録です。自動生成したいコンポーネント群をまとめてライブラリ化する
ビルド用スクリプトを記述する
コンポーネント群をまとめてライブラリ化するためのスクリプトを作成します。
自分はsrc/build.components.js
としました。build.component.js// require.contextで出力したいコンポーネント群を取得する // サブディレクトリの検索をON const files = require.context('出力したいコンポーネント群の相対パス', true, /.*\/vue$/); let components = {}; // コンポーネント群をグローバル登録する時みたいな感じでcomponentsオブジェクトに格納していく Object.values(files.keys()).forEach((key) => { const component = files(key).default; const name = component.name || component .split("/") .pop() .replace(/\.\w+$/, ""); components[name] = component; }); // 格納したコンポーネント群のオブジェクトをexport export default components;ビルドするために、package.jsonにnpmスクリプトを追記する
package.json
にビルド用scriptを追記(scripts部分抜粋)package.jsonscripts: { "serve": "vue-cli-service serve", "build": "vue-cli-service build", "lint": "vue-cli-service lint", "build:components": "vue-cli-service build --target lib --name components ./src/build.components.js", // これを追記 }
npm run build:components
を実行するとdist
に以下画像のようにファイルが出力されます。
vue.config.js
でoutputDir
を弄っている場合は各自の環境に合わせて読み替えてください。tags.jsonとattributes.jsonを生成するためのスクリプトを作成する
ライブラリ化で出力されたjsからコンポーネント群を読み込み、
tags.json
とattributes.json
を生成するためのスクリプトを作成します。
自分はsrc/build.vetur.js
としました。build.vetur.jsconst fs = require("fs"); const Vue = require("vue"); // .defaultを付与しないとexportされたオブジェクトを読み込めない const files = require("../dist/components.common").default; // 読み込んだコンポーネント群をVueオブジェクトに登録する Object.values(files).forEach((key) => { const component = key; const name = component.name || component .split("/") .pop() .replace(/\.\w+$/, ""); Vue.component(name, component); }); const hyphenateRE = /\B([A-Z])/g; function hyphenate(str) { return str.replace(hyphenateRE, "-$1").toLowerCase(); } function parseFunctionParams(func) { const groups = /function\s_.*\((.*)\)\s\{.*/i.exec(func); if (groups && groups.length > 1) return `(${groups[1]}) => {}`; else return "null"; } function getPropType(type) { if (Array.isArray(type)) { return type.map((t) => getPropType(t)); } if (!type) return "any"; return type.name.toLowerCase(); } function getPropDefault(def, type) { if ( def === "" || (def == null && type !== "boolean" && type !== "function") ) { return "undefined"; } else if (typeof def === "function" && type !== "function") { def = def.call({}); } if (type === "boolean") { return def ? "true" : "false"; } if (type === "string") { return def ? `'${def}'` : def; } if (type === "function") { return parseFunctionParams(def); } return def; } function genProp(name, prop) { const type = getPropType(prop.type); return { name, type, default: getPropDefault(prop.default, type), }; } function parseProps(component, array = []) { const options = component.options; const props = options.props || {}; Object.keys(props).forEach((key) => { const generated = genProp(key, props[key], component.name); array.push(generated); }); return array.sort((a, b) => a.name > b.name); } function writeJsonFile(obj, file) { const stream = fs.createWriteStream(file); stream.once("open", () => { stream.write(JSON.stringify(obj, null, 2)); stream.end(); }); } const components = {}; // Vue.componentで登録したコンポーネント群が格納されている const installedComponents = Vue.options._base.options.components; // 不要なコンポーネントを除外する(何故か登録されている) const excludes = ["KeepAlive", "Transition", "TransitionGroup"]; for (const name in installedComponents) { if (excludes.includes(name)) continue; const component = installedComponents[name]; const kebabName = hyphenate(name); components[kebabName] = { props: parseProps(component) }; } const tags = Object.keys(components).reduce((t, k) => { t[k] = { attributes: components[k].props .map((p) => p.name.replace(/([A-Z])/g, (g) => `-${g[0].toLowerCase()}`)) .sort(), description: "", }; return t; }, {}); const attributes = Object.keys(components).reduce((attrs, k) => { const tmp = components[k].props.reduce((a, prop) => { let type = prop.type; if (!type) type = ""; else if (Array.isArray(type)) type = type.map((t) => t.toLowerCase()).join("|"); else type = type.toLowerCase(); const name = prop.name.replace(/([A-Z])/g, (g) => `-${g[0].toLowerCase()}`); a[`${k}/${name}`] = { type, description: "", }; return a; }, {}); return Object.assign(attrs, tmp); }, {}); // 自分はsrc/utils/vetur下にtags.jsonとattributes.jsonを吐き出したいので // src/utils/veturがない場合は作成するようにしています if (!fs.existsSync("src/utils/vetur")) { fs.mkdirSync("src/utils/vetur", 0o755); } // tags.jsonとattributes.jsonを出力 writeJsonFile(tags, "src/utils/vetur/tags.json"); writeJsonFile(attributes, "src/utils/vetur/attributes.json"); console.log("tags.jsonとattributes.jsonが生成されました");コンポーネントを読み込んだ後のjsonファイルを生成する部分はvuetify.jsの
api-generator
にあったビルドスクリプトをほぼパクってきて、自分用に要らない部分を削っています。package.jsonにnpmスクリプトを追記する
スクリプトの作成ができたので
package.json
にスクリプトを追記しますpackage.jsonscripts: { "serve": "vue-cli-service serve", "build": "vue-cli-service build", "lint": "vue-cli-service lint", "build:api": "vue-cli-service build --target lib --name components ./src/build.components.js", "gen:api": "node ./src/build.vetur.js" // 追記 }json生成、package.jsonにvetur設定を追記する
veturにカスタムtags.jsonとattributes.jsonを読み込ませるための設定を記述します
"vetur": { "tags": "src/utils/vetur/tags.json", // 自動生成させるtags.jsonとattributes.jsonのパスを記述 "attributes": "src/utils/vetur/attributes.json" },
npm run gen:api
を実行するとエラーが出なければsrc/utils/vetur
ディレクトリが作成されtags.json
とattributes.json
が出力されているかと思います。
VSCodeを再起動すれば、tagの補完とpropsの補完が有効になっているかと思います。問題点
1つ問題として、
build.vetur.js
でVueC3を使ったコンポーネントを読み込むとwindow is not defined
と出てしまいます。
出力されたjsを見るとSVG関連で使っている様で、node環境ではwindowオブジェクト
が無いのでエラーが出るのも致し方無い。
もしこのようなエラーが出てしまう場合は、ライブラリ化するコンポーネントから除外してください。
現状、僕の知識では解決方法がわからないので、手動追記(スクリプト内に埋め込む)する形を取っています。(以下例)build.vetur.jstags["graph"] = { attributes: ["type"], desctiption: "", }; attributes["graph/type"] = { type: "string", description: "", };
tags,attributes
共にconst
ではなくlet
に変更するのをお忘れなく
- 投稿日: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-20T11:32:40+09:00
Vue+TypeScriptをESLint+Prettierでなるべくコンパクトな設定でLint+整形する
概要
Vue+TypeScriptを使用したソースコードに対しESLintとPrettierを適用するにあたり、ESLint関連のパッケージそれぞれがどのような役割を持っているか調べてみると思ったよりもコンパクトな内容(plugins,parser,parserOptionsなどは指定しなくても大丈夫)でLint+整形が出来ることを確認できたため、最終的に確認できた設定と調べた内容を共有します。
動作確認はVSCodeで行っています。
事前に準備が必要なもの
- Vue3+TypeScriptで動作するソースコード (Vue+TypeScriptの組み合わせを試したのがVue3だったため。Vue2でもこの記事の内容はほとんど変わらないと思います)
- 私は以下の記事を参考に用意しました。
VSCode
- ESLintプラグイン(
dbaeumer.vscode-eslint
v2.1.8)以下の設定が行われていること (
eslint.validate
は拡張子tsに適用するために必要でした){ "editor.codeActionsOnSave": { "source.fixAll.eslint": true }, "eslint.validate": [ "typescript" ], }最終的に必要だったパッケージと.eslintrc.js
最終的な
.eslintrc.js
とインストールしたパッケージの情報は以下の通りです。
(ルール設定は別途必要になると思いますが、この記事では省略しています).eslintrc.js
module.exports = { extends: [ "eslint:recommended", "plugin:vue/vue3-recommended", // "plugin:vue/vue3-strongly-recommended"もあります "@vue/typescript", "plugin:prettier/recommended", "prettier/vue", // 必須ではありません(ルールを書き換えるだけのため) "prettier/@typescript-eslint", // 必須ではありません(ルールを書き換えるだけのため) ], env: { node: true, }, rules: {}, };yarnでインストールしたパッケージ
yarn add -D eslint prettier eslint-config-prettier eslint-plugin-prettier yarn add -D @typescript-eslint/parser @typescript-eslint/eslint-plugin yarn add -D eslint-plugin-vue@next @vue/eslint-config-typescript
参考: 今回の作業後のpackage.json(クリックで展開)
{ "private": true, "scripts": { "dev": "webpack-dev-server --host 0.0.0.0", "build": "webpack --env.prod" }, "dependencies": {}, "devDependencies": { "@typescript-eslint/eslint-plugin": "^4.1.1", "@typescript-eslint/parser": "^4.1.1", "@vue/compiler-sfc": "^3.0.0", "@vue/eslint-config-typescript": "^5.1.0", "css-loader": "^4.2.2", "eslint": "^7.9.0", "eslint-config-prettier": "^6.11.0", "eslint-plugin-prettier": "^3.1.4", "eslint-plugin-vue": "^7.0.0-beta.3", "file-loader": "^6.0.0", "mini-css-extract-plugin": "^0.11.0", "postcss-loader": "^4.0.2", "prettier": "^2.1.2", "tailwindcss": "^1.8.10", "ts-loader": "^8.0.3", "typescript": "^4.0.2", "url-loader": "^4.0.0", "vue": "^3.0.0", "vue-loader": "^16.0.0-beta.7", "vue-router": "^4.0.0-beta.10", "webpack": "^4.42.1", "webpack-cli": "^3.3.11", "webpack-dev-server": "^3.10.3" } }
詳細
以下は、上記の結果に至るまでの過程をまとめたものです。
ESLint+Prettierのインストール
以下でESLintとPrettierをインストールします。
yarn add -D eslint prettier eslint-config-prettier eslint-plugin-prettier
この時点での
.eslintrc.js
は以下のような内容にします。
この設定でESLint+Prettierが動作することの説明は、「ESLint と Prettier の共存設定とその根拠について - Ojisan」が参考になりました。module.exports = { extends: ["eslint:recommended", "plugin:prettier/recommended"], env: { node: true, }, rules: {}, };TypeScriptに対するESLintを有効にする
TypeScriptのLintと整形を有効にするために以下をインストールします。
この点は「Getting Started - Linting your TypeScript Codebase」を参考にしています。yarn add -D @typescript-eslint/parser @typescript-eslint/eslint-plugin
この時点での
.eslintrc.js
は以下のような内容にします。
(後述しますが、ここで追加している内容は@vue/eslint-config-typescript
で同様の設定が行われるため、Vue3+TypeScriptの組み合わせでは最終的に削除できます)module.exports = { extends: [ "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", "plugin:prettier/recommended", ], + plugins: ["@typescript-eslint"], + parser: "@typescript-eslint/parser", + parserOptions: { + sourceType: "module", + }, env: { node: true, }, rules: {}, };ここまでで、
.ts
ファイルに対するLintと整形は出来るようになると思います。Vue3のSFCに対してLintが行えるようにする
上記の段階では
.vue
のSFCに対してはLINT等が行われないため、以下を追加でインストールします。yarn add -D eslint-plugin-vue@next @vue/eslint-config-typescript
ここで追加した2つのパッケージですが、node_modulesフォルダの下のパッケージのソースコードを確認する限り、以下のようなことを行っています。
パッケージ 内容 eslint-plugin-vue 公式サイトにも書かれていますが、このパッケージにより.vueのSFC内にある<template>,<script>タグがESLintによって解析可能になるようです。ソースコードは node_modules/eslint-plugin-vue/lib
配下にあってここからも確認できますが、parserをvue-eslint-parserに変更する、vue向けのルールの追加、plugin:vue/vue3-recommended
のようなconfigをエクスポートする記述が含まれています。@vue/eslint-config-typescript 実装を確認すると分かり易いですが、@typescript-eslint/parser, @typescript-eslint/eslint-pluginを使いつつ、拡張子.vueも対象にするための設定が含まれています。ソースコードはnode_modules/@vue/eslint-config-typescriptフォルダ配下です。このパッケージを導入すると、最終的な.eslintrc.jsに@typescript-eslintに関する宣言は記述不要になりそうです。また、内部でparserOptions.parserに@typescript-eslint/parserを指定しています。 この2つを追加した後の
.eslintrc.js
の内容は、
eslint-plugin-vue
と@vue/eslint-config-typescript
のそれぞれでparser
やparserOption
が指定されているため、以下のように書き換えます。
(最終的な内容は本記事の上段の内容を参照)module.exports = { extends: [ "eslint:recommended", + "plugin:vue/vue3-recommended", + "@vue/typescript", //仕組みは分かっていませんが、これで"@vue/eslint-config-typescript"が読まれるようです。 - "plugin:@typescript-eslint/eslint-recommended", //@vue/eslint-config-typescriptが同じものを追加するので削除 "plugin:prettier/recommended", ], - plugins: ["@typescript-eslint"], // @vue/eslint-config-typescriptが同じものを追加するので削除 - parser: "@typescript-eslint/parser", // eslint-plugin-vueがvue-eslint-parserに変更するので削除 - parserOptions: { // @vue/eslint-config-typescriptがparserOptions.parserなど幾つかののオプションを変更するので削除 - sourceType: "module", // eslint-plugin-vueなどが同じものを定義するので削除 - }, env: { node: true, }, rules: {}, };この設定を行うと、
.vue
配下の<template>
や<script lang="ts">
にもLintと整形が行われるはずです。Prettierに関するルールの調整
PrettierにはVueやTypescriptに関するデフォルトの設定が定義されているため、その設定を追加します。
こちらは実装を確認すると、どちらもルール定義を上書きする内容になっていました。
各設定を細かく確認は出来ていないですが、自分の手元ではprettier/vue
は入れておかないと、<template>
内で短すぎる範囲で改行されるなどの影響があったので追加しています。module.exports = { extends: [ "eslint:recommended", "plugin:vue/vue3-recommended", "@vue/typescript", "plugin:prettier/recommended", + "prettier/vue", + "prettier/@typescript-eslint", ], env: { node: true, }, rules: { }, };
- 投稿日:2020-09-20T10:55:40+09:00
静的サイトジェネレーターをGUIで操作できるアプリをElectronで作成しました
静的サイトジェネレーターをGUIで操作できるアプリ「らい帳」をElectronで作成しました。
ターミナルでのコマンド操作に不慣れな人やノンプログラマーでも簡単にブログが作成できるように本アプリを作成しました。
紹介ページ: https://raychonote.com/download.html
アプリで使用している技術や工夫したポイントなどを紹介いたします。
使用ライブラリ
主な使用ライブラリは以下になります。
Electron
ベースとなるプロジェクトはelectron-vueを使用しました。
electron-vueはElectronなどのライブラリのバージョンが古いため、ライブラリは随時自分でバージョンを上げながら開発を進めていきました。
Vue
本アプリではVueを使用しています。
また、Vueに関連するライブラリとして、以下も使用しています。
- Vuetify
- Vuex
- Vue Router
- Vue i18n
- Vee Validate
VuetifyはUI部分の実装にとても役立ちました。ほぼすべての要素はVuetifyのコンポーネントを使用しており、divやspanなどのプレーンなhtmlタグを実装することはほとんどありませんでした。
monaco-editor
エディタにはmonaco-editorを使用しています。
WebエディタといえばAceやCodemirrorがよく使われていると思いますが、monaco-editorはVSCodeでも使用されているブラウザで動作するエディタですので、VSCodeと同じ操作性を簡単にブラウザ上で実現できます。
marked
本アプリでは、記事はmarkdownで記載します。
markdownのライブラリはmarkedを使用し、markedをベースに独自記法も追加しています。(独自記法は使用しない人からすれば煩わしいと思うので、無効にする機能もあります)
highlight.js
markdownのシンタックスハイライトのライブラリはhighlight.jsを使用しています。
highlight.jsを各記事に組み込んでハイライトさせるのではなく、本アプリでビルド時に、ハイライトされた状態の静的なhtmlを出力します。
そのため、各記事のページでJavaScriptを実行する必要がないため、読み込み時間の短縮になります。
nunjucks
テンプレートエンジンはnunjucksを使用しています。
nunjucksはmozilla製のJavaScriptテンプレートエンジンで、ベーステンプレートの継承機能など、便利な機能がたくさんあります。
工夫したポイント
記事作成画面
記事作成画面では、Qiitaの投稿画面のようにプレビュー画面を並べて記事を作成できるようにしました。また、markdownに不慣れな人向けに、ツールバーを用意しました。
ツールバーのボタンをクリックしたらエディタに文字を挿入しますが、monaco-editorでは以下のように簡単に実装できます。
// this.editorはEditorオブジェクト // 選択範囲を取得 const sel = this.editor.getSelection(); // 選択したテキストを取得 const selectedText = this.editor.getModel().getValueInRange(sel); // 指定した範囲にテキストを設定 this.editor.executeEdits('', [ { range: new monaco.Range( sel.startLineNumber, sel.startColumn, sel.endLineNumber, sel.endColumn ), text: `**${selectedText}**`, // 選択中のテキストに強調スタイルを適用 }, ]);画像を貼り付け
画像は以下の操作で追加できるようにしました。
- ファイル選択ダイアログ
- ドラッグアンドドロップ
- クリップボードにコピーした画像を貼り付け
クリップボードにコピーした画像を貼り付ける機能はslackなどを使っていてとても便利だと感じたので実装しました。
// pasteイベント async pasteImage(e) { if ( !e.clipboardData || !e.clipboardData.types || e.clipboardData.types.length !== 1 || e.clipboardData.types[0] !== 'Files' ) { // 貼り付けたデータがファイルでない場合は処理終了 return; } const { items } = e.clipboardData; const blob = items[0].getAsFile(); // ファイルデータを読み込み const data = await new Promise((resolve) => { const reader = new FileReader(); reader.onload = (event) => { resolve(event.currentTarget.result); }; reader.readAsDataURL(blob); }); // 以降、画像を追加する処理まとめ
Electronを使うのは初めてでしたが、WEBアプリと同じように実装を進められました。
WEBの技術でデスクトップアプリが作れるのはとても便利ですね。
- 投稿日:2020-09-20T09:20:07+09:00
【Vue.js】クリック処理 @click
【ゴール】
Vue.jsを用いてクリック処理を実施
【環境】
mac catarina 10.156
Vue.js v2.6.12
【実装】
index.html<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <div id="app"> <p>{{number}}</p> <button @click="increaseNumber">増やす</button> </div> . . . <script> new Vue({ el: '#app', //対象を取得 data: { number: 0 //numberをセット }, methods: { increaseNumber: function(){ return this.number += 1; //numberに対して、1ずつ加えていく } } }) </script>【まとめ】
■ htmlでは、{{}}でvuejsのデータを表示
■ vuejsでは@click等の便利なディレクティブが多数ある【オススメ記事】
■ 【Vue.js】 IF文・For文 条件分岐、繰り返し処理
https://qiita.com/tanaka-yu3/items/0ccf9a11525331b764de■ 【node.js】ルーティング設定ホーム画面表示させる。
https://qiita.com/tanaka-yu3/items/1c8859e16070e67d73c0■ 【node.js】 node.jsインストール 芋っていたけど、簡単だった件...
https://qiita.com/tanaka-yu3/items/739db5ffed24a8d9ae4b
- 投稿日: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-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まで確認する必要性があるみたいです。
他の方とコードを共有している場合はコメントアウトなどを利用して変数を書いてあげると親切かもしれませんね。