はじめに こんにちは! クラシルWebのフロントエンドを担当している all-user です。 今回は、とあるプロジェクトをVue 2からVue 3に書き換えてみたので、その過程と所感についてまとめたいと思います。 この記事は dely #1 Advent Calendar 2020 14日目の記事です。 adventar.org adventar.org 昨日は funzin さんの Carthageで生成したframeworkの管理でRomeを導入してみた でした。 元々使用していたcarthage_cacheをRomeに置き換える過程が分かりやすく解説されています。ぜひこちらも覗いてみてください🙌 さて、今回題材に選んだプロジェクトは小規模なVue 2で書かれたアプリケーションですが、そのスタック構成はかなりクラシルWebに近いものとなっており、今後クラシルWebへの導入を検討する上での良い足がかりにできればと考えております。 目次 はじめに 目次 Vue 3について知る Vue 3の目玉。Composition APIとは Composition APIで何が変わる? Options API(従来のコンポーネント定義)の課題 Composition APIによる関心事の分離 TypeScriptサポートの改善 個人プロジェクトについて 書き換え作業ログ nodeとyarnを最新にアップデート ビルド設定をVue CLIで一気に最新に書き換える スタックを選択 いったんビルドしてみる .babelrcを削除 vue-property-decoratorを外す vuex-smart-moduleを外す その他のライブラリ 関心事を整理してhooksディレクトリに切り出す 実際に書き換えてみての所感 コードの見通しはとても良くなる Vuexの使い方はまだ模索中 VuexのTSサポートはこれから プロダクションに投入できるタイミング まだ試せていないこと さいごに Vue 3について知る なにはともあれ、書き換えるにあたりまずはVue 3のキャッチアップから始めなければなりません。 マイグレーションガイド を読んで解釈した内容を残しておきます。 Vue 3の目玉。Composition APIとは Vue 3で導入された新しいコンポーネント実装のためのAPIです。 Reactの hooks にインスパイアされた機能で、コンセプトもとても似ています。 Composition APIで何が変わる? これまでのViewModelインスタンスを起点にしたロジックの記述( this.xx や vm.xx な記述)ではなく、関数の組み合わせによる記述が可能になります。 たとえばコンポーネントのローカルステートを定義する時、Vue 2(Options API)では data を使いました。 // DisplayCount.vue import { defineComponent } from "vue" ; export default defineComponent ( { // Vue.extendは廃止されたためdefineComponentを使用します data () { return { count: 0 } ; } , methods: { increment () { this .count += 1 ; } , decrement () { this .count -= 1 ; } } } ); テンプレート部分は以下のようになります。 < template > < span v- text = "count" /> <!-- Vue 3ではFragmentがサポートされルート要素が単一の要素である必要がなくなりました --> < button @click= "increment" > + </ button > < button @click= "decrement" > - </ button > </ template > レンダリング結果はこんな感じ。 Vue 3(Composition API)では ref 関数を使います。 // DisplayCount.vue import { defineComponent , ref } from "vue" ; export default defineComponent ( { setup () { const count = ref ( 0 ); const increment = () => ( count.value += 1 ); // thisが消えた const decrement = () => ( count.value -= 1 ); // thisが消えた return { count , increment , decrement } ; }} ); テンプレートの内容は同じです。 setup 関数では count や increment が生えたオブジェクトを返していますが、これらのプロパティをテンプレート内で参照できるようになります。 ref 関数が返すRefオブジェクトには value というプロパティが生えており、このプロパティを通じて現在の値を取得することができます。 このように値をRefオブジェクトでラップすることで、ローカルステートの読み書きを捕捉できるようになり、リアクティブにDOMの更新を行えるようになります。 従来はこのラッパーの役割をViewModelインスタンス( this )が担っていました。 そして、 increment , decrement から this が消えています。 これは、 count というローカルステートおよび+1、-1するメソッドの定義が、特定のコンポーネントに属さなくなったと考えることができます。 そのため、以下のように書き換えることができます。 // useCount.ts import { ref } from "vue" ; export const useCount = () => { const count = ref ( 0 ); const increment = () => ( count.value += 1 ); const decrement = () => ( count.value -= 1 ); return { count , increment , decrement } ; } ; // DisplayCount.vue import { defineComponent } from "vue" ; import { useCount } from "../hooks/useCount" ; export default defineComponent ( { setup () { return { ...useCount () } ; } } ); 「countというローカルステートを持ち、+1、-1することができる」機能を useCount という関数に切り出し、さらに useCount.ts という別ファイルに切り出すことができました。 次に、これが出来るようになることで、どんなうれしいことがあるのかを考えてみます。 Options API(従来のコンポーネント定義)の課題 ViewModelを起点にした記述では、 data , computed , methods などの制約により、関心事の異なるロジック同士が一箇所に束ねられ、逆に関心事を同じくするコードが分散してしまうという問題がありました。 次のコードは先ほどのサンプルに、 display というローカルステートと toggleDisplayText という算術プロパティを加えたものです。 関心事を次の2つとし、 数をカウントしたい 表示を切り替えたい コード上の対応する部分にコメントを入れると次のようになります。 // DisplayCount.vue import { defineComponent } from "vue" ; export default defineComponent ( { data () { return { count: 0 , // a. 数をカウントしたい display: true // b. 表示を切り替えたい } ; } , computed: { toggleDisplayText () : string { // b. 表示を切り替えたい return this .display ? "hide" : "show" ; } } , methods: { increment () { // a. 数をカウントしたい this .count += 1 ; } , decrement () { // a. 数をカウントしたい this .count -= 1 ; } , toggleDisplay () { // b. 表示を切り替えたい this .display = ! this .display ; } } } ); アプリーケーションが大きくなりコンポーネントが複雑化すると、このように分散した関心事を頭の中でマッピングしながら読み解いていくコストが大きくなってきます。 テンプレートも更新します。 < template > < template v-if= "display" > < span v- text = "count" /> < button @click= "increment" > + </ button > < button @click= "decrement" > - </ button > </ template > < button @click= "toggleDisplay" v- text = "toggleDisplayText" /> </ template > レンダリング結果はこんな感じ。 hide をクリックすると以下のようになります。 次にこれをComposition APIに置き換えてみます。 Composition APIによる関心事の分離 Composition APIではロジックの記述がViewModelに依存しなくなり、 data , computed , methods などの制約から開放されます。 computed 関数が登場しましたが、コンセプトは ref の時と同じです。 ViewModelへの参照を無くしたバージョンの算術プロパティと考えればOKです。 数をカウントしたい 表示を切り替えたい コード上の対応する部分にコメントを入れると次のようになります。 // DisplayCount.vue import { defineComponent } from "vue" ; export default defineComponent ( { setup () { const count = ref ( 0 ); // a. 数をカウントしたい const increment = () => ( count.value += 1 ); // a. 数をカウントしたい const decrement = () => ( count.value -= 1 ); // a. 数をカウントしたい const display = ref ( true ); // b. 表示を切り替えたい const toggleDisplay = () => ( display.value = !display.value ); // b. 表示を切り替えたい const toggleDisplayText = computed (() => ( display.value ? "hide" : "show" )); // b. 表示を切り替えたい return { count , // a. 数をカウントしたい increment , // a. 数をカウントしたい decrement , // a. 数をカウントしたい display , // b. 表示を切り替えたい toggleDisplay , // b. 表示を切り替えたい toggleDisplayText // b. 表示を切り替えたい } ; } } ); 関心事ベースでコードをまとめることができていることが分かります。 最初のサンプル同様に、 setup 関数の外にロジックを切り出し、 useCount.ts 、 useToggleDisplay.ts という別ファイルに切り出してみます。 // useCount.ts // a. 数をカウントしたい import { ref } from "vue" ; export const useCount = () => { const count = ref ( 0 ); const increment = () => ( count.value += 1 ); const decrement = () => ( count.value -= 1 ); return { count , increment , decrement } ; } ; // useToggleDisplay.ts // b. 表示を切り替えたい import { computed , ref } from "vue" ; export const useToggleDisplay = () => { const display = ref ( true ); const toggleDisplay = () => ( display.value = !display.value ); const toggleDisplayText = computed (() => ( display.value ? "hide" : "show" )); return { display , toggleDisplay , toggleDisplayText } ; } ; // DisplayCount.vue import { defineComponent } from "vue" ; import { useCount } from "../hooks/useCount" ; // a. 数をカウントしたい import { useToggleDisplay } from "../hooks/useToggleDisplay" ; // b. 表示を切り替えたい export default defineComponent ( { setup () { return { ...useCount (), // a. 数をカウントしたい ...useToggleDisplay () // b. 表示を切り替えたい } ; } } ); それぞれの関心事が DisplayCount コンポーネントから完全に分離されています。 1 これは、これらのロジックが特定のコンポーネントに依存していないことを示しています。 ロジックが特定のコンポーネントに依存していないため、移植性・再利用性を高めることができます。 複数コンポーネント間でロジックを共通化しようとして、extendやmixinsを使って無茶をしたことがあるのは僕だけではないはずです。 Composition APIを使えば、より自然にロジックの共通化を表現できます。 TypeScriptサポートの改善 Composition APIによりTypeScriptの型定義もかなり改善しました。 従来のOptions APIの型定義は、 this に data , computed , methods などの定義を生やすためのコードがとても複雑で、型定義を見に行っては迷子になることもしょっちゅうでした。 ViewModel( this )への参照を無くし、関数の合成によってロジックを表現出来るようになったことで無理なく型を表現できているため、TypeScriptのコードが理解しやすくなりました。 個人プロジェクトについて 今回書き換えるのは rxjs-stream-editor というRxJSの非同期処理を可視化するツールです。 Vue 2, Vuex, TypeScriptを使用しており、コンポーネントの数もルートコンポーネントを入れて8つ、Vuex moduleの数も3つと検証にはもってこいの大きさです。 クラス記法コンポーネント、 vue-property-decorator 、 vuex-smart-module を使用しており、現時点ではVue 3未対応なライブラリなため、今回の検証ではいったん外しつつなるべくVueの素のAPIを使う方針でいきます。 memowomome.hatenablog.com 書き換え作業ログ github.com nodeとyarnを最新にアップデート node 👉 15.3.0にアップデート yarn 👉 1.22.10にアップデート ビルド設定をVue CLIで一気に最新に書き換える rxjs-stream-editorはVue CLIを使用したプロジェクトなので、今回もVue CLIを使用してアップデートします。 vue upgrade というコマンドも用意されていますが、規模も小さいので今回は vue create で上書く方法でやってみました🙌 # プロジェクトのひとつ上のディレクトリに移動 cd .. # 同名のディレクトリを指定して上書き # オプションでGitコミット無し、既存ファイルとマージするように指定 vue create -n --merge rxjs-stream-editor スタックを選択 スタックをマニュアルで選択 TS, Vuex, Stylus, ESLint, Prettierを使用 Vue 3を使用 クラス記法のコンポーネント定義ではなく素のAPIを使用 TypeScriptと一緒にBabelを使用 保存時にLintを実行 ESLint等のコンフィグファイルはpackage.jsonにまとめず、専用のファイルを使用 Vue CLI v4.5.9 ? Please pick a preset: Manually select features ? Check the features needed for your project: Choose Vue version, Babel, TS, Vuex, CSS Pre-processors, Linter ? Choose a version of Vue.js that you want to start the project with 3.x (Preview) ? Use class-style component syntax? No ? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? Yes ? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Stylus ? Pick a linter / formatter config: Prettier ? Pick additional lint features: Lint on save ? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files ? Save this as a preset for future projects? No いったんビルドしてみる エラーがたくさん出ます。 このまま一旦コミットしておきます。 エラーログ全文 ERROR in src/App.vue:21:1 TS1238: Unable to resolve signature of class decorator when called as an expression. Type 'VueClass<any>' is missing the following properties from type 'typeof App': extend, set, delete, directive, and 6 more. 19 | import { ColorDefinition } from './core/ColorDefinition'; 20 | > 21 | @Component({ | ^^^^^^^^^^^^ > 22 | components: { | ^^^^^^^^^^^^^^^ > 23 | AppHeader, | ^^^^^^^^^^^^^^^ > 24 | StreamEditor, | ^^^^^^^^^^^^^^^ > 25 | BottomNav, | ^^^^^^^^^^^^^^^ > 26 | }, | ^^^^^^^^^^^^^^^ > 27 | }) | ^^^ 28 | export default class App extends Vue.extend({ 29 | computed: { 30 | ...domainStreamColorizerModule.mapState(['colorMatcherSourceCode']), ERROR in src/App.vue:21:2 TS2345: Argument of type 'typeof App' is not assignable to parameter of type 'VueClass<any>'. Type 'typeof App' is missing the following properties from type 'typeof import("/Users/okamoto.k/_ghqroot/github.com/all-user/rxjs-stream-editor/node_modules/vue/dist/vue")': useCssModule, useCssVars, createApp, createSSRApp, and 108 more. 19 | import { ColorDefinition } from './core/ColorDefinition'; 20 | > 21 | @Component({ | ^^^^^^^^^^^ > 22 | components: { | ^^^^^^^^^^^^^^^ > 23 | AppHeader, | ^^^^^^^^^^^^^^^ > 24 | StreamEditor, | ^^^^^^^^^^^^^^^ > 25 | BottomNav, | ^^^^^^^^^^^^^^^ > 26 | }, | ^^^^^^^^^^^^^^^ > 27 | }) | ^^^ 28 | export default class App extends Vue.extend({ 29 | computed: { 30 | ...domainStreamColorizerModule.mapState(['colorMatcherSourceCode']), ERROR in src/App.vue:28:22 TS1219: Experimental support for decorators is a feature that is subject to change in a future release. Set the 'experimentalDecorators' option in your 'tsconfig' or 'jsconfig' to remove this warning. 26 | }, 27 | }) > 28 | export default class App extends Vue.extend({ | ^^^ 29 | computed: { 30 | ...domainStreamColorizerModule.mapState(['colorMatcherSourceCode']), 31 | ...domainStreamColorizerModule.mapGetters(['colorDefinitions']), ERROR in src/components/AppHeader/AppHeader.ts:3:1 TS1238: Unable to resolve signature of class decorator when called as an expression. Type '<VC extends VueClass<any>>(target: VC) => VC' is missing the following properties from type 'typeof AppHeader': extend, nextTick, set, delete, and 9 more. 1 | import { Component, Vue } from 'vue-property-decorator'; 2 | > 3 | @Component | ^^^^^^^^^^ 4 | export default class AppHeader extends Vue {} 5 | ERROR in src/components/AppHeader/AppHeader.ts:3:2 TS2769: No overload matches this call. Overload 1 of 2, '(options: ComponentOptionsBase<any, any, any, any, any, any, any, any, string, {}> & ThisType<any> & ThisType<any>): <VC extends VueClass<any>>(target: VC) => VC', gave the following error. Argument of type 'typeof AppHeader' is not assignable to parameter of type 'ComponentOptionsBase<any, any, any, any, any, any, any, any, string, {}> & ThisType<any> & ThisType<any>'. Type 'typeof AppHeader' is not assignable to type 'ComponentOptionsBase<any, any, any, any, any, any, any, any, string, {}>'. Types of property 'call' are incompatible. Type '<T, A extends any[]>(this: new (...args: A) => T, thisArg: T, ...args: A) => void' is not assignable to type '(this: unknown, ...args: unknown[]) => never'. The 'this' types of each signature are incompatible. Type 'unknown' is not assignable to type 'new (...args: unknown[]) => unknown'. Overload 2 of 2, '(target: VueClass<any>): VueClass<any>', gave the following error. Argument of type 'typeof AppHeader' is not assignable to parameter of type 'VueClass<any>'. Type 'typeof AppHeader' is missing the following properties from type 'typeof import("/Users/okamoto.k/_ghqroot/github.com/all-user/rxjs-stream-editor/node_modules/vue/dist/vue")': useCssModule, useCssVars, createApp, createSSRApp, and 108 more. 1 | import { Component, Vue } from 'vue-property-decorator'; 2 | > 3 | @Component | ^^^^^^^^^ 4 | export default class AppHeader extends Vue {} 5 | ERROR in src/components/AppHeader/AppHeader.ts:4:22 TS1219: Experimental support for decorators is a feature that is subject to change in a future release. Set the 'experimentalDecorators' option in your 'tsconfig' or 'jsconfig' to remove this warning. 2 | 3 | @Component > 4 | export default class AppHeader extends Vue {} | ^^^^^^^^^ 5 | ERROR in src/components/BottomNav/BottomNav.ts:9:1 TS1238: Unable to resolve signature of class decorator when called as an expression. Type 'VueClass<any>' is missing the following properties from type 'typeof BottomNav': extend, set, delete, directive, and 6 more. 7 | import StreamColorizer from '../StreamColorizer/StreamColorizer.vue'; 8 | > 9 | @Component({ | ^^^^^^^^^^^^ > 10 | components: { | ^^^^^^^^^^^^^^^ > 11 | MessageOutput, | ^^^^^^^^^^^^^^^ > 12 | StreamColorizer, | ^^^^^^^^^^^^^^^ > 13 | }, | ^^^^^^^^^^^^^^^ > 14 | }) | ^^^ 15 | export default class BottomNav extends Vue.extend({ 16 | computed: { 17 | ...domainStreamEditorModule.mapState(['errorMessage', 'message']), ERROR in src/components/BottomNav/BottomNav.ts:9:2 TS2345: Argument of type 'typeof BottomNav' is not assignable to parameter of type 'VueClass<any>'. Type 'typeof BottomNav' is missing the following properties from type 'typeof import("/Users/okamoto.k/_ghqroot/github.com/all-user/rxjs-stream-editor/node_modules/vue/dist/vue")': useCssModule, useCssVars, createApp, createSSRApp, and 108 more. 7 | import StreamColorizer from '../StreamColorizer/StreamColorizer.vue'; 8 | > 9 | @Component({ | ^^^^^^^^^^^ > 10 | components: { | ^^^^^^^^^^^^^^^ > 11 | MessageOutput, | ^^^^^^^^^^^^^^^ > 12 | StreamColorizer, | ^^^^^^^^^^^^^^^ > 13 | }, | ^^^^^^^^^^^^^^^ > 14 | }) | ^^^ 15 | export default class BottomNav extends Vue.extend({ 16 | computed: { 17 | ...domainStreamEditorModule.mapState(['errorMessage', 'message']), ERROR in src/components/BottomNav/BottomNav.ts:15:22 TS1219: Experimental support for decorators is a feature that is subject to change in a future release. Set the 'experimentalDecorators' option in your 'tsconfig' or 'jsconfig' to remove this warning. 13 | }, 14 | }) > 15 | export default class BottomNav extends Vue.extend({ | ^^^^^^^^^ 16 | computed: { 17 | ...domainStreamEditorModule.mapState(['errorMessage', 'message']), 18 | ...uiBottomNavModule.mapState(['enabled']), ERROR in src/components/MessageOutput/MessageOutput.ts:4:1 TS1238: Unable to resolve signature of class decorator when called as an expression. Type '<VC extends VueClass<any>>(target: VC) => VC' is missing the following properties from type 'typeof MessageOutput': extend, nextTick, set, delete, and 9 more. 2 | import { domainStreamEditorModule } from '../../store/modules/internal'; 3 | > 4 | @Component | ^^^^^^^^^^ 5 | export default class MessageOutput extends Vue.extend({ 6 | computed: { 7 | ...domainStreamEditorModule.mapState(['errorMessage', 'message']), ERROR in src/components/MessageOutput/MessageOutput.ts:4:2 TS2769: No overload matches this call. Overload 1 of 2, '(options: ComponentOptionsBase<any, any, any, any, any, any, any, any, string, {}> & ThisType<any> & ThisType<any>): <VC extends VueClass<any>>(target: VC) => VC', gave the following error. Argument of type 'typeof MessageOutput' is not assignable to parameter of type 'ComponentOptionsBase<any, any, any, any, any, any, any, any, string, {}> & ThisType<any> & ThisType<any>'. Type 'typeof MessageOutput' is not assignable to type 'ComponentOptionsBase<any, any, any, any, any, any, any, any, string, {}>'. Types of property 'call' are incompatible. Type '<T, A extends any[]>(this: new (...args: A) => T, thisArg: T, ...args: A) => void' is not assignable to type '(this: unknown, ...args: unknown[]) => never'. Overload 2 of 2, '(target: VueClass<any>): VueClass<any>', gave the following error. Argument of type 'typeof MessageOutput' is not assignable to parameter of type 'VueClass<any>'. Type 'typeof MessageOutput' is missing the following properties from type 'typeof import("/Users/okamoto.k/_ghqroot/github.com/all-user/rxjs-stream-editor/node_modules/vue/dist/vue")': useCssModule, useCssVars, createApp, createSSRApp, and 108 more. 2 | import { domainStreamEditorModule } from '../../store/modules/internal'; 3 | > 4 | @Component | ^^^^^^^^^ 5 | export default class MessageOutput extends Vue.extend({ 6 | computed: { 7 | ...domainStreamEditorModule.mapState(['errorMessage', 'message']), ERROR in src/components/MessageOutput/MessageOutput.ts:5:22 TS1219: Experimental support for decorators is a feature that is subject to change in a future release. Set the 'experimentalDecorators' option in your 'tsconfig' or 'jsconfig' to remove this warning. 3 | 4 | @Component > 5 | export default class MessageOutput extends Vue.extend({ | ^^^^^^^^^^^^^ 6 | computed: { 7 | ...domainStreamEditorModule.mapState(['errorMessage', 'message']), 8 | }, ERROR in src/components/StreamColorizer/StreamColorizer.ts:2:10 TS2305: Module '"../../../node_modules/vue/dist/vue"' has no exported member 'VueConstructor'. 1 | import { Component, Vue } from 'vue-property-decorator'; > 2 | import { VueConstructor } from 'vue'; | ^^^^^^^^^^^^^^ 3 | import { domainStreamColorizerModule } from '../../store/modules/internal'; 4 | import { Photoshop } from 'vue-color'; 5 | import { ColorDefinition } from '../../core/ColorDefinition'; ERROR in src/components/StreamColorizer/StreamColorizer.ts:7:1 TS1238: Unable to resolve signature of class decorator when called as an expression. Type 'VueClass<any>' is missing the following properties from type 'typeof StreamColorizer': extend, set, delete, directive, and 6 more. 5 | import { ColorDefinition } from '../../core/ColorDefinition'; 6 | > 7 | @Component({ | ^^^^^^^^^^^^ > 8 | components: { | ^^^^^^^^^^^^^^^ > 9 | PhotoshopPicker: Photoshop as VueConstructor, | ^^^^^^^^^^^^^^^ > 10 | }, | ^^^^^^^^^^^^^^^ > 11 | }) | ^^^ 12 | export default class StreamColorizer extends Vue.extend({ 13 | computed: { 14 | ...domainStreamColorizerModule.mapState([ ERROR in src/components/StreamColorizer/StreamColorizer.ts:7:2 TS2345: Argument of type 'typeof StreamColorizer' is not assignable to parameter of type 'VueClass<any>'. Type 'typeof StreamColorizer' is missing the following properties from type 'typeof import("/Users/okamoto.k/_ghqroot/github.com/all-user/rxjs-stream-editor/node_modules/vue/dist/vue")': useCssModule, useCssVars, createApp, createSSRApp, and 108 more. 5 | import { ColorDefinition } from '../../core/ColorDefinition'; 6 | > 7 | @Component({ | ^^^^^^^^^^^ > 8 | components: { | ^^^^^^^^^^^^^^^ > 9 | PhotoshopPicker: Photoshop as VueConstructor, | ^^^^^^^^^^^^^^^ > 10 | }, | ^^^^^^^^^^^^^^^ > 11 | }) | ^^^ 12 | export default class StreamColorizer extends Vue.extend({ 13 | computed: { 14 | ...domainStreamColorizerModule.mapState([ ERROR in src/components/StreamColorizer/StreamColorizer.ts:12:22 TS1219: Experimental support for decorators is a feature that is subject to change in a future release. Set the 'experimentalDecorators' option in your 'tsconfig' or 'jsconfig' to remove this warning. 10 | }, 11 | }) > 12 | export default class StreamColorizer extends Vue.extend({ | ^^^^^^^^^^^^^^^ 13 | computed: { 14 | ...domainStreamColorizerModule.mapState([ 15 | 'colorMatcherSourceCode', ERROR in src/components/StreamEditor/StreamEditor.ts:7:1 TS1238: Unable to resolve signature of class decorator when called as an expression. Type 'VueClass<any>' is missing the following properties from type 'typeof StreamEditor': extend, set, delete, directive, and 6 more. 5 | import debounce from 'lodash-es/debounce'; 6 | > 7 | @Component({ | ^^^^^^^^^^^^ > 8 | components: { | ^^^^^^^^^^^^^^^ > 9 | StreamEditorItem, | ^^^^^^^^^^^^^^^ > 10 | }, | ^^^^^^^^^^^^^^^ > 11 | }) | ^^^ 12 | export default class StreamEditor extends Vue.extend({ 13 | computed: { 14 | ...domainStreamEditorModule.mapGetters(['streamDatasets', 'sourceCode']), ERROR in src/components/StreamEditor/StreamEditor.ts:7:2 TS2345: Argument of type 'typeof StreamEditor' is not assignable to parameter of type 'VueClass<any>'. Type 'typeof StreamEditor' is missing the following properties from type 'typeof import("/Users/okamoto.k/_ghqroot/github.com/all-user/rxjs-stream-editor/node_modules/vue/dist/vue")': useCssModule, useCssVars, createApp, createSSRApp, and 108 more. 5 | import debounce from 'lodash-es/debounce'; 6 | > 7 | @Component({ | ^^^^^^^^^^^ > 8 | components: { | ^^^^^^^^^^^^^^^ > 9 | StreamEditorItem, | ^^^^^^^^^^^^^^^ > 10 | }, | ^^^^^^^^^^^^^^^ > 11 | }) | ^^^ 12 | export default class StreamEditor extends Vue.extend({ 13 | computed: { 14 | ...domainStreamEditorModule.mapGetters(['streamDatasets', 'sourceCode']), ERROR in src/components/StreamEditor/StreamEditor.ts:12:22 TS1219: Experimental support for decorators is a feature that is subject to change in a future release. Set the 'experimentalDecorators' option in your 'tsconfig' or 'jsconfig' to remove this warning. 10 | }, 11 | }) > 12 | export default class StreamEditor extends Vue.extend({ | ^^^^^^^^^^^^ 13 | computed: { 14 | ...domainStreamEditorModule.mapGetters(['streamDatasets', 'sourceCode']), 15 | }, ERROR in src/components/StreamEditor/StreamEditor.ts:25:10 TS1219: Experimental support for decorators is a feature that is subject to change in a future release. Set the 'experimentalDecorators' option in your 'tsconfig' or 'jsconfig' to remove this warning. 23 | }) { 24 | @Watch('sourceCode') > 25 | public watchSourceCode() { | ^^^^^^^^^^^^^^^ 26 | this.evaluateSourceCodeDebounced(); 27 | } 28 | ERROR in src/components/StreamEditorItem/StreamEditorItem.ts:23:1 TS1238: Unable to resolve signature of class decorator when called as an expression. Type 'VueClass<any>' is missing the following properties from type 'typeof StreamEditorItem': extend, set, delete, directive, and 6 more. 21 | }; 22 | > 23 | @Component({ | ^^^^^^^^^^^^ > 24 | components: { | ^^^^^^^^^^^^^^^ > 25 | StreamEditorTextarea, | ^^^^^^^^^^^^^^^ > 26 | }, | ^^^^^^^^^^^^^^^ > 27 | }) | ^^^ 28 | export default class StreamEditorItem extends Vue.extend({ 29 | computed: { 30 | ...domainStreamColorizerModule.mapGetters([ ERROR in src/components/StreamEditorItem/StreamEditorItem.ts:23:2 TS2345: Argument of type 'typeof StreamEditorItem' is not assignable to parameter of type 'VueClass<any>'. Type 'typeof StreamEditorItem' is missing the following properties from type 'typeof import("/Users/okamoto.k/_ghqroot/github.com/all-user/rxjs-stream-editor/node_modules/vue/dist/vue")': useCssModule, useCssVars, createApp, createSSRApp, and 108 more. 21 | }; 22 | > 23 | @Component({ | ^^^^^^^^^^^ > 24 | components: { | ^^^^^^^^^^^^^^^ > 25 | StreamEditorTextarea, | ^^^^^^^^^^^^^^^ > 26 | }, | ^^^^^^^^^^^^^^^ > 27 | }) | ^^^ 28 | export default class StreamEditorItem extends Vue.extend({ 29 | computed: { 30 | ...domainStreamColorizerModule.mapGetters([ ERROR in src/components/StreamEditorItem/StreamEditorItem.ts:28:22 TS1219: Experimental support for decorators is a feature that is subject to change in a future release. Set the 'experimentalDecorators' option in your 'tsconfig' or 'jsconfig' to remove this warning. 26 | }, 27 | }) > 28 | export default class StreamEditorItem extends Vue.extend({ | ^^^^^^^^^^^^^^^^ 29 | computed: { 30 | ...domainStreamColorizerModule.mapGetters([ 31 | 'colorCodeGetter', ERROR in src/components/StreamEditorItem/StreamEditorItem.ts:39:18 TS1219: Experimental support for decorators is a feature that is subject to change in a future release. Set the 'experimentalDecorators' option in your 'tsconfig' or 'jsconfig' to remove this warning. 37 | }, 38 | }) { > 39 | @Prop() public dataset: StreamDataset | undefined; | ^^^^^^^ 40 | @Prop({ required: true }) public index!: boolean; 41 | @Prop({ default: false }) public disabled!: boolean; 42 | ERROR in src/components/StreamEditorItem/StreamEditorItem.ts:40:36 TS1219: Experimental support for decorators is a feature that is subject to change in a future release. Set the 'experimentalDecorators' option in your 'tsconfig' or 'jsconfig' to remove this warning. 38 | }) { 39 | @Prop() public dataset: StreamDataset | undefined; > 40 | @Prop({ required: true }) public index!: boolean; | ^^^^^ 41 | @Prop({ default: false }) public disabled!: boolean; 42 | 43 | get events() { ERROR in src/components/StreamEditorItem/StreamEditorItem.ts:41:36 TS1219: Experimental support for decorators is a feature that is subject to change in a future release. Set the 'experimentalDecorators' option in your 'tsconfig' or 'jsconfig' to remove this warning. 39 | @Prop() public dataset: StreamDataset | undefined; 40 | @Prop({ required: true }) public index!: boolean; > 41 | @Prop({ default: false }) public disabled!: boolean; | ^^^^^^^^ 42 | 43 | get events() { 44 | return this.dataset ? this.dataset.events : []; ERROR in src/components/StreamEditorTextarea/StreamEditorTextarea.ts:5:1 TS1238: Unable to resolve signature of class decorator when called as an expression. Type '<VC extends VueClass<any>>(target: VC) => VC' is missing the following properties from type 'typeof StreamEditorTextarea': extend, nextTick, set, delete, and 9 more. 3 | import { domainStreamEditorModule } from '../../store/modules/internal'; 4 | > 5 | @Component | ^^^^^^^^^^ 6 | export default class StreamEditorTextarea extends Vue.extend({ 7 | methods: { 8 | ...domainStreamEditorModule.mapMutations(['setSourceCode']), ERROR in src/components/StreamEditorTextarea/StreamEditorTextarea.ts:5:2 TS2769: No overload matches this call. Overload 1 of 2, '(options: ComponentOptionsBase<any, any, any, any, any, any, any, any, string, {}> & ThisType<any> & ThisType<any>): <VC extends VueClass<any>>(target: VC) => VC', gave the following error. Argument of type 'typeof StreamEditorTextarea' is not assignable to parameter of type 'ComponentOptionsBase<any, any, any, any, any, any, any, any, string, {}> & ThisType<any> & ThisType<any>'. Type 'typeof StreamEditorTextarea' is not assignable to type 'ComponentOptionsBase<any, any, any, any, any, any, any, any, string, {}>'. Types of property 'call' are incompatible. Type '<T, A extends any[]>(this: new (...args: A) => T, thisArg: T, ...args: A) => void' is not assignable to type '(this: unknown, ...args: unknown[]) => never'. Overload 2 of 2, '(target: VueClass<any>): VueClass<any>', gave the following error. Argument of type 'typeof StreamEditorTextarea' is not assignable to parameter of type 'VueClass<any>'. Type 'typeof StreamEditorTextarea' is missing the following properties from type 'typeof import("/Users/okamoto.k/_ghqroot/github.com/all-user/rxjs-stream-editor/node_modules/vue/dist/vue")': useCssModule, useCssVars, createApp, createSSRApp, and 108 more. 3 | import { domainStreamEditorModule } from '../../store/modules/internal'; 4 | > 5 | @Component | ^^^^^^^^^ 6 | export default class StreamEditorTextarea extends Vue.extend({ 7 | methods: { 8 | ...domainStreamEditorModule.mapMutations(['setSourceCode']), ERROR in src/components/StreamEditorTextarea/StreamEditorTextarea.ts:6:22 TS1219: Experimental support for decorators is a feature that is subject to change in a future release. Set the 'experimentalDecorators' option in your 'tsconfig' or 'jsconfig' to remove this warning. 4 | 5 | @Component > 6 | export default class StreamEditorTextarea extends Vue.extend({ | ^^^^^^^^^^^^^^^^^^^^ 7 | methods: { 8 | ...domainStreamEditorModule.mapMutations(['setSourceCode']), 9 | }, ERROR in src/components/StreamEditorTextarea/StreamEditorTextarea.ts:11:18 TS1219: Experimental support for decorators is a feature that is subject to change in a future release. Set the 'experimentalDecorators' option in your 'tsconfig' or 'jsconfig' to remove this warning. 9 | }, 10 | }) { > 11 | @Prop() public dataset: StreamDataset | undefined; | ^^^^^^^ 12 | @Prop({ default: false }) public disabled!: boolean; 13 | 14 | get sourceCode() { ERROR in src/components/StreamEditorTextarea/StreamEditorTextarea.ts:12:36 TS1219: Experimental support for decorators is a feature that is subject to change in a future release. Set the 'experimentalDecorators' option in your 'tsconfig' or 'jsconfig' to remove this warning. 10 | }) { 11 | @Prop() public dataset: StreamDataset | undefined; > 12 | @Prop({ default: false }) public disabled!: boolean; | ^^^^^^^^ 13 | 14 | get sourceCode() { 15 | return this.dataset ? this.dataset.sourceCode : ''; ERROR in src/main.ts:6:5 TS2339: Property 'use' does not exist on type 'typeof import("/Users/okamoto.k/_ghqroot/github.com/all-user/rxjs-stream-editor/node_modules/vue/dist/vue")'. 4 | import VueTextareaAutosize from 'vue-textarea-autosize'; 5 | > 6 | Vue.use(VueTextareaAutosize); | ^^^ 7 | Vue.config.productionTip = false; 8 | 9 | new Vue({ ERROR in src/main.ts:7:5 TS2339: Property 'config' does not exist on type 'typeof import("/Users/okamoto.k/_ghqroot/github.com/all-user/rxjs-stream-editor/node_modules/vue/dist/vue")'. 5 | 6 | Vue.use(VueTextareaAutosize); > 7 | Vue.config.productionTip = false; | ^^^^^^ 8 | 9 | new Vue({ 10 | store, ERROR in src/main.ts:9:5 TS2351: This expression is not constructable. Type 'typeof import("/Users/okamoto.k/_ghqroot/github.com/all-user/rxjs-stream-editor/node_modules/vue/dist/vue")' has no construct signatures. 7 | Vue.config.productionTip = false; 8 | > 9 | new Vue({ | ^^^ 10 | store, 11 | render: h => h(App), 12 | }).$mount('#app'); ERROR in src/main.ts:11:11 TS7006: Parameter 'h' implicitly has an 'any' type. 9 | new Vue({ 10 | store, > 11 | render: h => h(App), | ^ 12 | }).$mount('#app'); 13 | ERROR in src/store/index.ts:6:5 TS2339: Property 'use' does not exist on type 'typeof import("/Users/okamoto.k/_ghqroot/github.com/all-user/rxjs-stream-editor/node_modules/vue/dist/vue")'. 4 | import { rootModule } from './modules'; 5 | > 6 | Vue.use(Vuex); | ^^^ 7 | 8 | export default createStore(rootModule); 9 | .babelrcを削除 Babel最新ではbabel.config.jsを使用するように変わったようなのですが、Vue CLIでディレクトリをマージした際に.babelrcが残っており、そちらの設定を見に行ってしまいエラーが出ていました。 これを削除します。 vue-property-decoratorを外す デコレータを使ったコンポーネント定義を defineComponent に置き換えていきます。 vuex-smart-moduleを外す Vue 3対応のために素のVuexに書き換えます。 vuex-smart-moduleを通して store にアクセスしていた部分は、 useStore 関数を使用して置き換えます。 この時点で commit , dispatch , getters に付いていたメソッドの型は、 string であればなんでも受け入れるようになってしまいます。 やはりまだvuex-smart-moduleは外せないという所感。 Composition API対応に関するIssueでは対応予定とのコメントもあり今後の動きに期待です。 ktsn/vuex-smart-module | Composition API? #106 テンプレートから参照するものは従来通り mapState や mapGetters を使用し、コンポーネント定義内で参照するものは useStore を使用しています。 この辺りの書き方はどうするのが良いだろう、という感じでまだ模索中です。 その他のライブラリ vue-textarea-autosize テキストの入力に合わせて自動的に高さを調整してくれる Vue 3未対応 とりあえず普通のtextareaに置き換えてしのぐ vue-color カラーピッカー Vue 3未対応 いったんあきらめる 関心事を整理して hooks ディレクトリに切り出す setup にまとめて書かれていた初期化処理を3つのファイルに分割し、各関数内でそれぞれ useStore を使用するように変更します。 カラーパレットの初期化処理。 カラーパレットとイベントをマッピングするためのソースコードの初期化処理。 可視化するRxJS Observableを生成するソースコードの初期化処理。 実際に書き換えてみての所感 コードの見通しはとても良くなる 関心事の分離がうまく表現できるようになり、コードの見通しが良くなりました。 規模の大きいプロジェクトであればより効果を発揮できるのではと感じました。 Vuexの使い方はまだ模索中 今回はテンプレートから参照する値を従来のOptions APIで記述しましたが、Composition APIに最適化されたヘルパーについての議論が行われていていました。 近いうちにベストプラクティスが発明されそうです。 github.com VuexのTSサポートはこれから Vuex 4でTypeScriptのサポートは強化されたものの、state以外の型周りはまだサポートされていません。 現時点ではvuex-smart-moduleなどのTypeScriptサポートのライブラリは必要だと感じました。 こちらのIssueを見ると、本格的なTSサポートの強化はVuex 5を予定しているようです。 github.com また、それに伴うBreaking Changeを検討している模様。 一方で、TS 4.1というゲームチェンジャーの登場によりVuex 4もサポートされる可能性が出てきたようです。 github.com TS 4.1で導入されたTemplate Literal Typesにより文字列の柔軟な型検査が可能になり、これまで難しかったnamespaceをスラッシュで繋いだ文字列に対しての静的な型検査が実装可能になりました。 近い将来TS完全対応が実現するかもしれません。 プロダクションに投入できるタイミング 上記を踏まえると今すぐのプロダクション投入は難しいものの、確実にメリットを感じたので、少しづつ移行に向けて準備を進めていきたいと思います。 まだ試せていないこと ローカルステートを定義しているコンポーネントが無かったため、 ref , reactive を使った複雑な実装はまだ試せていないです vue-routerも使用していないためこちらもまだ未検証です 引き続き検証していきたいと思います さいごに 以上Vue 3への書き換えを通しての所感をまとめてみました。 この記事では紹介できなかった細かいAPIの変更などもありますが、暗黙的な挙動の削除やパフォーマンス改善のための変更など確実にパワーアップしています。 個人的にはFragmentが使えるようになったのが最高です。 これからのVue 3を楽しんでいきましょう! そして明日は sako さんの「UIデザイナーとして働く私が就活生に戻ったら絶対やること5つ」です! めちゃくちゃ知りたい! delyではエンジニアを全方位絶賛募集中です🚀 こちらのリンクからお気軽にエントリーください🙌 join-us.dely.jp また、delyでは普段表に出ない開発チームの裏側をお伝えするイベントをたくさん開催しております! こちらもぜひ覗いてみてください! bethesun.connpass.com ではまた! toggleDisplayText についてはUI都合な部分が大きいため、 useToggleDisplay には含めず DisplayCount コンポーネント側に寄せたいところですが、今回は分かりやすさのためにこのようにしています。 ↩