こんにちは、19のSysAd班の翠(sappi_red)です。
この記事では10/17にリリースされたtraQ v3.3.0でVueのv2からv3に移行した話について話していきます。どんな手順で移行を進めたかやハマりどころ、どうするとよさそうみたいな話ですね。
traQで使われてるライブラリなど
まず、traQではVue CLIなど以下のVue関連のツールを利用して開発していました。
- Vue CLI v4.5.6
- eslint-plugin-vue 6.2.2
- vue-svg-loader 0.16.0
また、Vue関連のライブラリは以下のようなものを利用していました。
- Vue 2.6.12
- @vue/composition-api 1.0.0-beta.14
- v-click-outside 3.1.2
- portal-vue 2.1.7
- vue-slider-component 3.2.5
- vue-textarea-autosize 1.1.1
- Vue Router 3.4.3
- Vuex 3.5.1
- vuex-persist 3.1.0
- direct-vuex 0.12.0
移行手順
2020/08/03から着手して2020/10/08にmasterにマージしたので2ヶ月ですが、途中空けてるのでおそらく実質的には2週間程度だと思います。
事前調査
上記のライブラリやツールの対応状況は
- Vue CLI 4.5.0 (2020/07/24)
- eslint-plugin-vue 7.0.0-alpha.0 (2020/03/14)
- vue-svg-loader 0.17.0-beta.1 (2020/08/24)
- Vue 3.0.0-alpha.0 (2020/01/04)
- Vue Router 4.0.0-alpha.0 (2020/02/26)
- Vuex 4.0.0-alpha.1 (2020/03/15)
- vuex-persist 未対応 (2020/02/02現在) (動くことには動く)
- direct-vuex 0.12.0 (Vuex4でも動作)
となっていて、着手した地点では対応しているのはコアなもののみでライブラリはほぼ全滅の状態でした。(着手した地点ではベータかアルファばっかりだったので当然ですね)
vue-codemodの手順にのっとって書き換える
vue-codemodというreact-codemodっぽい移行ツールが開発されていることを知っていたのでそこのREADMEの手順を参考にしつつ書き換えました。
このツールはその頃もTypeScriptへの対応がされていなかったので、実際に実行はせず手で書き換えました。現在もexperimentalみたいでTypeScriptの対応もしてないみたいです。
書き換えたものが動くかどうかのチェックをするために一旦ライブラリ部分をすべてコメントアウトしました。
表示はうまくいったのですが、ところどころ以下の点で動いてなかったので直しました。
- 自作コンポーネントの
v-model
のvalue
propと$emit('input', v)
- Vue 3では
modalValue
と$emit('update:modelValue')
になった RFC
- Vue 3では
$listeners
- Vue 3ではなくなった RFC
[Vue Router warn]: history.state seems to have been manually replaced without preserving the necessary values
history.state
が利用されるようになったので入っていた情報を消さないようにする必要がある docs
大体手順のここの欄の部分ですね。
ライブラリ部分の対応
- Vue CLI: 4.5.0(2020/07/24)で既に対応してたので特になし
- eslint-plugin-vue: 上の手順でアップデート済み
- vue-svg-loader: 08/24に対応したのでそのときにアップデート、それまでは一旦無効化
- Vue: 上の手順でアップデート済み
- v-click-outside: パッチを作成
- portal-vue: ほぼVue3の
<teleport>
で問題なかったので書き換えの上削除 - vue-slider-component: パッチを作成、その後masterにマージ後に10/09にアップデート
- vue-textarea-autosize: パッチを作成、その後masterにマージ後にライブラリを変更して削除
- Vue Router: 上の手順でアップデート済み
- Vuex: 上の手順でアップデート済み
- vuex-persist: パッチを作成
- direct-vuex: 元々Vuex4でも動作してた
ここの「パッチを作成」というのはpatch-package
というnode_modules
にパッチをあてられるツールを利用しました。
それぞれのライブラリをクローンした上でVue3に書き換えた上でその変更を元にパッチを作成するという手順ですね。vue-slider-componentだけvue-property-decoratorで書かれていてあまり情報を集めていなかった箇所だったので少し手間取りました。
portal-vueから<teleport>
への書き換え
portal-vueでは<portal>
がレンダリングされてから<portal-target>
がレンダリングされるという順番でも問題なかったのですが、<teleport>
はレンダリングするときにto
で指定した要素が存在していることが前提になっています。なので、to
に指定する要素は基本的には<body>
直下にして、どうしてもVueで管理された部分に置く場合は、レンダリングされたかどうかでv-if
で分岐する必要があります。
<body>
直下にする際にはVueのルート要素に指定したCSSなどが適用されないことに注意ですね。CSS変数をルート要素に指定していたので、モーダルにCSSが適用されないってバグが起きました。
バグ取り
ここで大体ほぼ動くようになったのですが、まだ少しバグがいくつか残っていたので、以下のところを直しました。
setup() should not return property which starts with "$" or "_"
$
と_
から始まるプロパティはVueの内部用に利用される可能性があるので含めてはいけないようです- 利用していたころの@vue/composition-apiではエラーが出てませんでした
- 属性に真偽値を渡す
$emit('input', e.target.value)
とネイティブのinput
イベント- Vue3ではコンポーネントのルート要素のイベントが
.native
なしでもlistenされるようになった RFC@click="$emit('click')"
のように子から親にイベントをそのまま流すようなことをしていると二重でイベント発火する(emits
の指定で回避可能)
- Vue3ではコンポーネントのルート要素のイベントが
ここまででmasterにマージしました
Vue 3移行後の問題とか
上の真偽値の変更と.native
なしでもlistenされる変更によるバグがそこそこ発生しました。
emits
Vue3で追加されたemits
というそのコンポーネントで発火するイベントを宣言できるものがあって、それを宣言するとTypeScriptで型がつくようになっています。これは宣伝ですがこれを書くとVeturでイベント名が補完されたりするようにしたので、そういう点でもうれしかったりします。
import { defineComponent } from 'vue'
export default defineComponent({
emits: {
foo: (arg1: number, arg2: string) => true
},
setup(props, context) {
context.emit('foo', 1, 'aa')
context.emit('bar') // error
context.emit('foo', true) // error
}
})
ここで複数のイベントを発火するコンポーネントが存在していて、その内の一つのイベントを発火するようなcomposable(ロジックを意味単位に分割した関数、Reactでいうhookみたいなもの)があるとします。下のような状況です。
import { defineComponent, SetupContext } from 'vue'
const useFoo = (context: SetupContext<{ foo: () => true }>) => {
context.emit('foo')
}
export default defineComponent({
emits: {
foo: () => true,
bar :() => true
},
setup(props, context) {
useFoo(context)
}
})
このときにuseFoo(context)
の行でエラーが発生してしまっていました(vuejs/vue-next#2362)。
useFoo
に渡されているcontext
の型がSetupContext<{ foo: () => true, bar: () => true }>
なのに対して、useFoo
のとる引数はSetupContext<{ foo: () => true }>
です。型だけ比較すると代入できないような気がしますが、意味を考えるとこれは代入できるべきです。foo
とbar
以外のものが発火するのは困りますがどちらか一方しか発火しないのは問題ないですね。
これを解決するためにSetupContext<{ foo: () => true, bar: () => true }>
がそのまま({ emit: ((name: 'foo') => void) | ((name: 'bar') => void) }
)ではなく{ emit: ((name: 'foo') => void) & ((name: 'bar') => void) }
として扱われるようにUnionToIntersection
というちょっと変わった型関数を利用しています。
中でUnionToIntersection
が使われてるので代入できるようになってるはずですが、おそらく問題は外の箇所で下のようにSetupContext
が定義されてた点です。
type SetupContext<E> = {
emit: EmitFn<E>;
};
このように定義されてたとき、TypeScriptはEの型だけで代入可能かどうかの判定を行っているような挙動をしていました(ホバーした際にSetupContext<~>
の形で表示されているあたりとか)。なので、下のように型を実際に評価せざるをえない形にしてみたところ代入できるようにはなりました。(大抵の場合であれば型引数同士の代入可能性だけを評価すれば問題ないみたいな話なのでしょうか…よくわかってません)
type SetupContext<E> = E extends any
? {
emit: EmitFn<E>;
}
: never
移行するなら
時期
移行のドキュメントも以下のように整ってきてコアのライブラリ以外を利用していない場合は今すぐでもそこまで引っかからずに移行できると思います。
ただし、今下記のバグがあるので少し待ったほうが良いかもしれません。
- Vuexでキャッシュが効かない、あるいは効かせようとするとメモリリークする vuejs/vuex#1883
- Reactivity's
effectScope
APIというRFCが出ていてこれを使うと解決する
- Reactivity's
- 上記の
SetupContext
の型の問題
これらは3.1.0では直りそうな予感がしてます。
All of our official libraries and tools now support Vue 3, but most of them are still in beta status and distributed under the next dist tag on NPM. We are planning to stabilize and switch all projects to use the latest dist tag by end of 2020.
https://github.com/vuejs/vue-next#supporting-libraries
年始にコアのライブラリはstableのバージョンが出ていて(vuexはまだRCではありますが)、コンポーネントライブラリなどが対応するのは早くて4~6月ごろだと思います。主要なライブラリのVue3対応状況はvuejs/awesome-vue#3544にまとまっています。
移行で利用できるツールやライブラリ
今移行するなら上記で利用していたツールやライブラリ以外に、以下のツールやライブラリを利用してみるとよさそうです。ただぼく自身はcomposition-apiしか利用していないのでどの程度完成しているかはわからないです。
- vuejs/composition-api: Vue2でcomposition-apiを利用できるようにする
- antfu/vueuse: Vue2(composition-api)/3ともに動作するcomposableライブラリ
- antfu/vue-demi: Vue2(composition-api)/3両対応ライブラリをつくるためのライブラリ
- privatenumber/vue-2-3: Vue2用コンポーネントをVue3で、Vue3用コンポーネントをVue2で利用できるようにする
- vuejs/function-api-converter: options APIで書かれたコードをVue3 composition-apiに変換するツール
- yoyo930021/vc2c: Vue2 Class ComponentsをVue3 composition-apiに変換するツール
まとめ
そこまで大きな書き換えは伴わなかったので比較的簡単に移行することができました。
ただ、ネイティブと同じイベント名でイベントを発火させていると、バグが多発するので予めemits
で発火させるイベントを指定しておくほうがよかったかもしれません。
ただ、traQの場合は素早いフィードバックが得られるのと基本身内向けであることから、報告が来たら直すという形でも十分対応できました。
もっと大きいプロジェクトの場合は、静的解析して検出したほうがよさそうです。
ところでこの記事11月くらいに書いて3ヶ月くらい眠らせちゃってました…
traQもv3.6.4になってていろいろ機能増えてたりします。