LoginSignup
3
0

More than 3 years have passed since last update.

Vue3に向けてfilterを自動変換したいよね

Last updated at Posted at 2020-08-22

Vue3からはfilterが廃止されます。
何も考えずアップデートすると今までのコードが突如として動かなくなる悲しい場面に遭遇するわけです。

filter依存コードを手動で変換していくのはしんどいです。
特に、突然composition APIに移行するのはなおさらしんどいです。

そこで一旦options APIのままVue3に対応したコードを生成するeslint pluginを作りました。

npmに何か公開するのも、なんならqiitaに投稿するのも人生初です。

対象

  • composition APIへの過渡期として、filterを利用したVue2以前のコードを自動的にVue3に移行したい
  • Options APIを使用している
  • Global filterを1ファイルで登録している
  • const { filterX } = this.$options.filtersとかいう使い方をして いない

何が起きるの?怖いんだけど

2フェイズに分かれています。

フェイズ1

  • global filterをexportに変換
  • 既存のfilterの名前をそのまま使えます
  • filterと同名のでimportされた関数はfilterName as _filterNameみたいに名前を変更しています

before

import Vue from 'vue';
import def, { numeric } from '~/utils/Strings';
import dollar, { yen, euro } from '~/utils/Money';

Vue.filter('stringDefaultFilter', def);

Vue.filter('percent', (value: string) => `${value}%`);

Vue.filter(
  'numeric',
  (value: string) => `${numeric(value)}`,
);
Vue.filter('dollar', dollar);
Vue.filter('yen', yen);
Vue.filter('eur', euro);

Vue.filter('func', function(xxx: string) {
  return `xxx${xxx}xxx`;
});

after

import def, { numeric as _numeric } from '~/utils/Strings';
import dollar, { yen, euro } from '~/utils/Money';

export const stringDefaultFilter = def;

export const percent = (value: string) => `${value}%`;

export const numeric = (value: string) => `${_numeric(value)}`;
export { dollar };
export { yen };
export const eur = euro;

export function func(xxx: string) {
  return `xxx${xxx}xxx`;
}

フェイズ2

  • フェイズ1でexportした関数をimport
  • パイプによるfilterを関数呼び出しに変換
  • (this.)$options.filtersも変換
  • メソッドとしてマッピング
  • local filterにも対応

before

<template>
  <div>
    <p>{{ x.y.z | localFilterA }}</p>
    <p>{{ id[0] | localFilterB }}</p>
    <p>{{ $options.filters.filterA(prop) | localFilterC }}</p>
    <p>{{ prop | localFilterA | filterB }}</p>
    <p>{{ methodFilter(prop) }}</p>
    <p :id="id | idFilter">XXX</p>
    <p :id="$options.filters.idFilter2(id) | localIdFilter">XXX</p>
    <p :id="$options.filters.idFilter3(id)">XXX</p>
  </div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
  filters: {
    localFilterA(value: string): string {
      return value.toUpperCase();
    },
    localFilterB(value: number): string {
      return value.toString();
    },
    localFilterC(obj: { [key: string]: string }): string {
      const texts = Object.entries(obj).reduce((acc: string[], [k, v]) => {
        acc.push(`${k}: ${v}`);
        return acc;
      }, []);
      return texts.join('\n');
    },
    localIdFilter(id: string): string {
      return id.slice(0, 5);
    },
  },
  methods: {
    methodFilter(x: string): string {
      return this.$options.filters.importedFilter(x);
    },
  },
})
</script>

after


<template>
  <div>
    <p>{{ localFilterA(x.y.z) }}</p>
    <p>{{ localFilterB(id[0]) }}</p>
    <p>{{ localFilterC(filterA(prop)) }}</p>
    <p>{{ filterB(localFilterA(prop)) }}</p>
    <p>{{ methodFilter(prop) }}</p>
    <p :id="idFilter(id)">XXX</p>
    <p :id="localIdFilter(idFilter2(id))">XXX</p>
    <p :id="idFilter3(id)">XXX</p>
  </div>
</template>
<script lang="ts">
import Vue from 'vue';
import { filterA, filterB, idFilter, idFilter2, idFilter3, importedFilter } from 'path/to/filters.ts';
export default Vue.extend({

  methods: {
    methodFilter(x: string): string {
      return this.importedFilter(x);
    },
    filterA,
    filterB,
    idFilter,
    idFilter2,
    idFilter3,
    importedFilter,
    localFilterA(value: string): string {
      return value.toUpperCase();
    },
    localFilterB(value: number): string {
      return value.toString();
    },
    localFilterC(obj: { [key: string]: string }): string {
      const texts = Object.entries(obj).reduce((acc: string[], [k, v]) => {
        acc.push(`${k}: ${v}`);
        return acc;
      }, []);
      return texts.join('\n');
    },
    localIdFilter(id: string): string {
      return id.slice(0, 5);
    },
  },
})
</script>

使い方

上記対象に当てはまった方には、使い方です。(そうじゃない方は諦めてPR送るかコード読んでいじるかしてください)

インストール

$ npm install -D eslint-plugin-destroy-vue-filter

準備

eslint設定ファイルを2個作成することをおすすめします。

  • フェイズ1用

ファイル名は好きに(以下、.eslintrc.convert-filterとします)

.eslintrc.convert-filter

{
  "plugins": ["destroy-vue-filter"],
  "rules": {
    "destroy-vue-filter/convert-filter": 1,
    "destroy-vue-filter/resolve-callee": 1
  }
}
  • フェイズ2用

ファイル名は好きに(以下、.eslintrc.destroy-filterとします)

.eslintrc.destroy-filter
{
  "plugins": ["destroy-vue-filter"],
  "rules": {
    "destroy-vue-filter/destroy-filter": ["error", "<global filterを登録しているファイル>"]
  }
}

実行

以下を実行してください

eslint -c .eslintrc.convert-filter --fix <global filterを登録しているファイル>
eslint -c .eslintrc.destroy-filter --fix **/*.vue

なぜeslint?

  • Vue/typescriptをパースしてVue/typescriptに書き戻せるツールが他に見つからなかった(あったら教えて下さい)
  • linterのくせに基本一回しか使わないけどまあいいよね
  • 複数箇所でglobal filterを登録していたらお手上げです。無理
  • const { filterX } = this.$options.filtersみたいな使い方も変数のスコープをめっちゃ見なきゃならないので無理
  • 正直会社でのユースケース(+ 私が思いつく適当に複雑なパターン)に合わせました
  • 僕は適当なんだ。すまない
  • ASTを頑張っていじくってきた経緯からgitのlogはグダグダです。見るな
  • 正直動かなかったら言ってください。僕はフロントエンドエンジニャー歴がとても短いです
3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0