20200920のvue.jsに関する記事は10件です。

【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も参照ができるわけです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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.json
scripts: {
  "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.jsoutputDirを弄っている場合は各自の環境に合わせて読み替えてください。

build.jpg

tags.jsonとattributes.jsonを生成するためのスクリプトを作成する

ライブラリ化で出力されたjsからコンポーネント群を読み込み、tags.jsonattributes.jsonを生成するためのスクリプトを作成します。
自分はsrc/build.vetur.jsとしました。

build.vetur.js
const 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.json
scripts: {
  "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.jsonattributes.jsonが出力されているかと思います。
VSCodeを再起動すれば、tagの補完とpropsの補完が有効になっているかと思います。

問題点

1つ問題として、build.vetur.jsでVueC3を使ったコンポーネントを読み込むとwindow is not definedと出てしまいます。
出力されたjsを見るとSVG関連で使っている様で、node環境ではwindowオブジェクトが無いのでエラーが出るのも致し方無い。
もしこのようなエラーが出てしまう場合は、ライブラリ化するコンポーネントから除外してください。
現状、僕の知識では解決方法がわからないので、手動追記(スクリプト内に埋め込む)する形を取っています。(以下例)

build.vetur.js
tags["graph"] = {
  attributes: ["type"],
  desctiption: "",
};

attributes["graph/type"] = {
  type: "string",
  description: "",
};

tags,attributes共にconstではなくletに変更するのをお忘れなく

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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.json
scripts: {
  "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.jsoutputDirを弄っている場合は各自の環境に合わせて読み替えてください。

build.jpg

tags.jsonとattributes.jsonを生成するためのスクリプトを作成する

ライブラリ化で出力されたjsからコンポーネント群を読み込み、tags.jsonattributes.jsonを生成するためのスクリプトを作成します。
自分はsrc/build.vetur.jsとしました。

build.vetur.js
const 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.json
scripts: {
  "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.jsonattributes.jsonが出力されているかと思います。
VSCodeを再起動すれば、tagの補完とpropsの補完が有効になっているかと思います。

問題点

1つ問題として、build.vetur.jsでVueC3を使ったコンポーネントを読み込むとwindow is not definedと出てしまいます。
出力されたjsを見るとSVG関連で使っている様で、node環境ではwindowオブジェクトが無いのでエラーが出るのも致し方無い。
もしこのようなエラーが出てしまう場合は、ライブラリ化するコンポーネントから除外してください。
現状、僕の知識では解決方法がわからないので、手動追記(スクリプト内に埋め込む)する形を取っています。(以下例)

build.vetur.js
tags["graph"] = {
  attributes: ["type"],
  desctiption: "",
};

attributes["graph/type"] = {
  type: "string",
  description: "",
};

tags,attributes共にconstではなくletに変更するのをお忘れなく

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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の公式ドキュメントです。

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/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経由でtwitterを書き換えています。

ここらへんから、けっこう 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 で実装する

目的地

すごくシンプルなpricequantityの乗算によって、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オブジェクトを使用されているため、少し挙動は変わるようなので、次はそっちも見ていきたいです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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のそれぞれでparserparserOptionが指定されているため、以下のように書き換えます。
(最終的な内容は本記事の上段の内容を参照)

 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: {
   },
 };
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

静的サイトジェネレーターをGUIで操作できるアプリをElectronで作成しました

静的サイトジェネレーターをGUIで操作できるアプリ「らい帳」をElectronで作成しました。

ターミナルでのコマンド操作に不慣れな人やノンプログラマーでも簡単にブログが作成できるように本アプリを作成しました。

記事作成画面.png

紹介ページ: 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に不慣れな人向けに、ツールバーを用意しました。

記事作成画面.png

ツールバーのボタンをクリックしたらエディタに文字を挿入しますが、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の技術でデスクトップアプリが作れるのはとても便利ですね。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vue.js】クリック処理 @click

【ゴール】

画面収録 2020-09-20 9.12.12.mov.gif

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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Webのフロントでコンポーネントを作る意味(コンポーネントライブラリへの感想)

0. 記事作成の動機:コンポーネントを作る煩わしさ

 ReacやVueなどのJsでコンポーネントをつくるのがめんどくさいと感じるときが多々あった。
 なので、コンポーネントを作る意味を再考してみる。

 以前、似たような記事を作ったけど、それをもう少し整理してみた。

:arrow_forward: 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を持ったせて、対応づけをするのは良いと思う。

:arrow_forward: Angular公式:Introduction to components and templates

component
export 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を変更しないデータバインディングライブリである。

 ただし、双方向バインディングではなく一方向だけである。そもそも、双方向である必要はいつもあるとは限らない。

:arrow_forward: Simulacra.js

image.png

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>

上記のタグに対応するモデルとなるオブジェクトリテラルを作る。

model
var state = {
  name: 'Pumpkin Spice Latte',
  details: {
    size: [ 'Tall', 'Grande', 'Venti' ],
    vendor: 'Coffee Co.'
  }
}

 
バインディング。

bind
var 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のフィルター関数を作ってしまえばいいと思います。
 コーディングが楽です。

:arrow_forward: 11ty

わざわざGatsbyみたいにJsでコンポーネント作らなくても、pug、nanjucks、markdown、htmlなどでHTMLを作れます。
もちろん、フィルター関数とかも作れるので、繰り返し作業とか動的な処理も対応できます。

image.png

7. まとめ:コンポーネントという「発想・視点」は大切

(1) コンポーネントライブラリを使う煩わしさ

 この記事を書いた動機は、受託をしていると毎回デザインがかわり、HTMLやCSSも変わる事が多く、その都度、コンポーネントを別途つくる手間が凄くめんどくさかった。

 また、わざわざコーディングしてあるHTMLをバラすという手間も無駄に感じた。

 その他、VuexやReduxなどの状態管理が煩わしく、そもそも双方向バインディングなどをせず、ワントランザクションでSPAをつくれば、状態も複雑化しないので、使う必要をあまり感じなかった。

 というように、コンポーネントという「考え方・設計」はいいのだけど、その実現方法について無駄や手間を感じたので、こうした記事を書いてみた。

 

(2) コンポーネントライブラリを使う場所(自社サービス・大規模開発)

 利用シーンによっては煩わしいのだけど、自社サービスの大規模開発の場合は、ReactやVueなどの有名なコンポーネントライブラリを使うのがよいと思う。
 
 その方がドキュメントの整備も公式に任せればいいし、ネット上に情報が沢山ある。

 またコーディングを規制することができるので、書き方をプログラマー間で統一させやすい。
 
 自社サービスの場合、デザインやHTMLの構造などもコンポーネント側に合わせた開発できる。

 裏返せば、受託で毎回デザインが変わる場合、必ずしもコンポーネントライブラリを使うべきかは考えようだと思う。

 以下に分かりやすいコーディング設計ができるかに依存する。でも、それができないと考えるなら、有名なライブラリを使うのが無難だと思う。

(3) コンポーネントは「発想・視点」が大切

 コードを独立化、局所化するという発想がコンポーネントはよいと思う。
 これは疑義なく支持したい。
 けれど、その実現方法については、ケースバイケースかなという気もする。
 
 自社サービスか受託開発、大規模か小規模か、などプロジェクトの前提条件によっても異なると思う。

 とにかくコンポーネントを作る手間はどんなものでも発生するのだけど、できるだけその手間が最小化するものがよいと思う。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Webのフロントでコンポーネントを作る意味(JsとHTMLの依存性、コンポーネントライブラリへの感想)

0. 記事作成の動機:コンポーネントを作る煩わしさ

 ReacやVueなどのJsでコンポーネントをつくるのがめんどくさいと感じるときが多々あった。
 なので、コンポーネントを作る意味を再考してみる。

 以前、似たような記事を作ったけど、それをもう少し整理してみた。

:arrow_forward: 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を持ったせて、対応づけをするのは良いと思う。

:arrow_forward: Angular公式:Introduction to components and templates

component
export 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を変更しないデータバインディングライブリである。

 ただし、双方向バインディングではなく一方向だけである。そもそも、双方向はいつも必要だろうか?

:arrow_forward: Simulacra.js

image.png

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>

上記のタグに対応するモデルとなるオブジェクトリテラルを作る。

model
var state = {
  name: 'Pumpkin Spice Latte',
  details: {
    size: [ 'Tall', 'Grande', 'Venti' ],
    vendor: 'Coffee Co.'
  }
}

 
バインディング。

bind
var 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のフィルター関数を作ってしまえばいいと思います。
 コーディングが楽です。

:arrow_forward: 11ty

わざわざGatsbyみたいにJsでコンポーネント作らなくても、pug、nanjucks、markdown、htmlなどでHTMLを作れます。
もちろん、フィルター関数とかも作れるので、繰り返し作業とか動的な処理も対応できます。

image.png

7. まとめ:コンポーネントという「発想・視点」は大切

(1) コンポーネントライブラリを使う煩わしさ

 この記事を書いた動機は、受託をしていると毎回デザインがかわり、HTMLやCSSも変わる事が多く、その都度、コンポーネントを別途つくる手間が凄くめんどくさかった。

 また、わざわざコーディングしてあるHTMLをバラすという手間も無駄に感じた。

 その他、VuexやReduxなどの状態管理が煩わしく、そもそも双方向バインディングなどをせず、ワントランザクションでSPAをつくれば、状態も複雑化しないので、使う必要をあまり感じなかった。

 というように、コンポーネントという「考え方・設計」はいいのだけど、その実現方法について無駄や手間を感じたので、こうした記事を書いてみた。

 

(2) コンポーネントライブラリを使う場所(自社サービス・大規模開発)

 利用シーンによっては煩わしいのだけど、自社サービスの大規模開発の場合は、ReactやVueなどの有名なコンポーネントライブラリを使うのがよいと思う。
 
 その方がドキュメントの整備も公式に任せればいいし、ネット上に情報が沢山ある。

 またコーディングを規制することができるので、書き方をプログラマー間で統一させやすい。
 
 自社サービスの場合、デザインやHTMLの構造などもコンポーネント側に合わせた開発できる。

 裏返せば、受託で毎回デザインが変わる場合、必ずしもコンポーネントライブラリを使うべきかは考えようだと思う。

 以下に分かりやすいコーディング設計ができるかに依存する。でも、それができないと考えるなら、有名なライブラリを使うのが無難だと思う。

(3) コンポーネントは「発想・視点」が大切

 コードを独立化、局所化するという発想がコンポーネントはよいと思う。
 これは疑義なく支持したい。
 けれど、その実現方法については、ケースバイケースかなという気もする。
 
 自社サービスか受託開発、大規模か小規模か、などプロジェクトの前提条件によっても異なると思う。

 とにかくコンポーネントを作る手間はどんなものでも発生するのだけど、できるだけその手間が最小化するものがよいと思う。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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まで確認する必要性があるみたいです。
他の方とコードを共有している場合はコメントアウトなどを利用して変数を書いてあげると親切かもしれませんね。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む