SMARTCAMP Engineer Blog

スマートキャンプ株式会社(SMARTCAMP Co., Ltd.)のエンジニアブログです。業務で取り入れた新しい技術や試行錯誤を知見として共有していきます。

Vue 2.xとVue 3を共存させようと思ったけどダメだった話

こんにちは!!!

スマートキャンプでエンジニアをしている吉永です!

去年の8月に入社し、BOXILのフロントエンド開発に主に関わっています。

弊社の主力サービスであるBOXILは、リリースしてから既に何年も経っているということもあり全てが最新という訳ではなく、インフラからフロントまで様々な技術的負債を抱えています。

フロントエンドでは古いライブラリを使ってしまっているケースや、UIライブラリに依存してしまっているというケースが挙げられます。

他には、CoffeeScriptの中でnew Vue...としてVueを動かしている部分もあり、可読性や保守性に大きな弊害をもたらしてしまっている状況です。

昨年Vue 3がリリースされましたが、各種ライブラリの対応はまだ追いついていない部分も多く、「Vueのバージョンを2系から3にあげたいけど、主要なライブラリがいまだ対応していないからVue 3にあげることができない」といったケースが多くあるかと思います。

立ち上がったばかりのサービスとは違い、どうしても古いコードを全てをVue 3に対応した形式に作り直すというのは莫大なコストと手間がかかってしまうのではないかと考えました。

そこで今回、Vue 2.xの部分を全て書き直すのではなく、Vue 2.xと3を共存させる形でプロジェクトを管理できないかと考え、その第一歩としてVue 3をVue 2.xと一緒のページでレンダリングしてみようと思い、試してみました。

注: 本記事は失敗したという内容とそこで得た学びが書かれています。成功した話ではないのでもし解決方法を知っている方がいればSNSやはてブコメントにて教えていただけると幸いです。

すでに存在した事例

最初に、既に同じような事例が存在しないかどうかを調べてみました。

その結果

  1. プロジェクトをVue 3に対応できる状態にした後、バージョンを上げる
  2. 先にバージョンを上げてしまい、大量に出たエラーを修正する

Vue 2.xからVue 3にあげた系の記事では、主にこの2つに分類される例しか見つかりませんでした(自分調べ)

やったこと

前述した通り、走り出してからかなり経っているプロジェクトに上記の実例と同じ方法を取るのはかなり危険な上に膨大な工数を必要とします。

よって1, 2の方法とはまた違う、共存できそうな方法を2つ挙げてみました。

  1. Vue 2.xのCDNや、vue.min.jsと同じような感じでVue 3を持ってくる方法
  2. npm install vue@next的なのを使って上手くVue 2.xとVue 3をどちらも入れる方法

今回はその検証用に、以下のVue 2.xのプロジェクトを用意しました。

3,4どちらの方法でも、共通でmain.jsに記述して最終的にVue2とVue3のコンポーネントが同じページに描画できれば成功とします。

そして、Vue 3のレンダリング用タグとして、IDをappV3としたタグをindex.htmlに設置しました。

結果発表

Vue2のCDNや、vue.min.jsと同じような感じでVue3を持ってくる方法(失敗)

CDNで提供されているVue 3をローカルに落としてきて、new Vue3...的な方法で行けるのではないかと思い試してみました。

import Vue from 'vue'
import App from './App.vue'

import { Vue3 } from './vue3'
import AppVue3 from './AppVue3.vue'

Vue.config.productionTip = false

new Vue({
  render: h => h(App),
}).$mount('#app')

Vue3.createApp(AppVue3).mount('#appV3')

こんな感じで、Vue 3のjsファイルをimportしてきて、createApp(Vue 2.xだとnew Vue)します。

この状態で実行してみると以下のようなエラーが出ました。

まず、Vueファイルが正常に呼び出されているのか、処理ができていないのはレンダリングだけなのかなどを調べるべく、main.jsのコードを以下のように変更しました。

import Vue from 'vue'
import App from './App.vue'

import { Vue3 } from './vue3'

Vue.config.productionTip = false

new Vue({
  render: h => h(App),
}).$mount('#app')

const testAppVue3 = {
  render() {
    return "HelloVue3"
  },
  created() {
    console.log("HelloVue3")
  }
}

Vue3.createApp(testAppVue3).mount('#appV3')

importしたVueファイルをマウントする形ではなく、オブジェクトを作りその中でcreatedと、正当な使い方ではないですが確認のためrenderに文字列を渡し、走らせてみました。

すると...

HelloVue3が正常に出力されていることがわかります。

画面にも正常に表示されているため、Vue 3としては正常に呼び出せていそうということがわかりました。

しかし、同じようにrenderでcomponentをreturnしたり、templateで頑張って描画しようとすると上記の画像と同じエラーが出てしまうため、templateを変換するときにVue 2.xの処理とバッティングしてしまっているのではないかと考えました。

npm install vue@next的なのを使って上手くVue2とVue3をどちらも入れる方法

Vue3のチュートリアルにもあるように、npm install vue@nextを使うと現在の最新安定版のVue 3をインストールすることができます。

ですが、このままインストールしてしまうと、既存のVue 2.xの記述も3に上書きされてしまい動かないので、package.jsonをこのように修正します。

無理やりvue3と名前をつけてやることでインストールしたい考えでしたが、

No valid versions available for vue3

というエラーが出てインストールに失敗してしまいます。

調べてみると

"dependencies": {
    "core-js": "^3.8.1",
    "vue": "^2.6.11",
    "vue3": "npm:vue@^3.0.0"
},

という形でpackage.jsonに記述すると「名前を分けてバージョンごとにインストール出来るらしい」という情報を見つけ試した結果、正常にインストールすることができました。

それではmain.jsを編集していきます。 まずは先ほどの実装でできていた、objectを渡す方法から試してみましょう。

import Vue from 'vue'
import App from './App.vue'

import { createApp } from 'vue3';

Vue.config.productionTip = false

new Vue({
  render: h => h(App),
}).$mount('#app')

const testAppVue3 = {
  render() {
    return 'HelloVue3'
  },
  created() {
    console.log('HelloVue3')
  }
}

createApp(testAppVue3).mount('#appV3')

結果は

やはり、VueファイルをimportしないのであればVue 3は動く。ということがわかります。

ですが、やりたいことはVue 2.xと共存させて動かしたいということなので、先ほどと同じようにVueファイルをimportしてcreateAppすると...

ダメみたい。

まとめ

結果として、3,4どちらの方法でもcreatedやdataなど画面のレンダリングに関係ない部分の処理であれば動かすことができました。

が、レンダリングが関わってくるとどうやっても上記のエラーが出てしまうため、時間の関係もあり一旦ここで調査終了となりました。無念...

Vue 2.xだけど一部だけcompositionAPIを使ってみたいみたいな要望だったり、ここだけVue 3で処理したいみたいなのであれば実装できる...かもしれないという結果に落ち着くのかなあと感じました。

更にcompositionAPIを使いたいだけであれば、既にそれ用のライブラリが存在するため、わざわざ今回の方法で実装する必要はないかな...といった感じです。

よって、共存できないと仮定すると「全てVue 3にあげた上で工数をかけてエラーを少しづつ消していく」、というのが今のところの最適解なのではないかなと思います。

やり残したこととしては、なぜselfHook.bind is not a functionのエラーが出てしまうのか、他に共存した上で少しずつVue 3に上げていく方法を模索するなどがあると思っており、次回のテックブログなどで続編が出ることを期待していただければという形で、本記事を締め括らせていただきたいと思います。

追記(2021/2/8)

本記事を見て、Vue2.xとVue3の共存はできないのかという検証をしていただいた方がいました。

- その方の記事はこちらから

その記事中でなんと、上記で掲げていたエラーを解決してくださっており、私の環境でも再現できるか試してみたいと思います!

何が違ったのか

基本的なコードに大きな違いはないですが、成功している方ではwebpack.config.jsにてVueのエイリアス設定をしている部分がありました。

なので、vue.config.jsを作成しその中に以下のコードを追記します。

module.exports = {
  configureWebpack: {
    resolve: {
      alias: {
        'vue$': 'vue/dist/vue.common.dev.js',
        'vue3$': 'vue3/dist/vue.esm-browser.js',
      }
    }
  }
}

ついでにmain.jsのVue3実行部分も少し変更して...

const testAppVue3 = {
  template: `<p>Hello {{ message }}</p>`,
  data() {
    return {
      message: 'Vue3!'
    }
  },
  created() {
    console.log('HelloVue3')
  }
}

Vue3.createApp(testAppVue3).mount('#appV3')

そして、そのまま実行してみると

動いてる!!!!!

ではこれを、.vueファイルをimportする形で試してみると

import Vue from 'vue'
import App from './App.vue'

import * as Vue3 from 'vue3';
import Vue3Component from "./AppVue3.vue"

Vue.config.productionTip = false

new Vue({
  render: h => h(App),
}).$mount('#app')

Vue3.createApp(Vue3Component).mount('#appV3')

なるほど、これは失敗してしまう。

import時にVue3ファイルとして読み込んで欲しいところをVue2.xとして読み込んじゃっているのかなという感じもしますが、とにかく一歩前進しました。次回記事までにこの辺を解決できたらなと思います!

最後になりますが、記事を読んでくださっただけではなく、そこから追加検証して頂き、さらにそれを記事として載せていただいたことに深く感謝しております。本当にありがとうございました!