Vueのアップグレードでエラー

どのバージョンで変わったか定かではないのだけど、アップグレード後エラーになって起動しなくなった。

変更点

vue.config.jsのエラーメッセージ

 ERROR  SyntaxError: Cannot use import statement outside a module
E:\Work\wtp\workspace\crawler-client\vue.config.js:1
import { defineConfig } from '@vue/cli-service'

import fromをrequireに変更した。

const { defineConfig } = require('@vue/cli-service')

babel.config.jsのエラーメッセージ

 ERROR  SyntaxError: Unexpected token 'export'
E:\Work\wtp\workspace\crawler-client\babel.config.js:1
export const plugins = {

export const pluginsをmodule.exportsに変更した。

module.exports = {
  presets: [
    '@vue/cli-plugin-babel/preset'
  ]
}

axios 0.27から1.4にアップグレードする

以下のコマンドで1.4.0にアップグレードする。

npm install axios@1.4.0

アップグレード後、自作プログラムのログイン画面で下記のエラーが発生するようになった。

TypeError: Cannot set properties of undefined (setting ‘Authorization’)

config.headers.common.Authorizationからcommon.を削除する必要があるらしく、問題の箇所を以下の通り修正した。

const instance = axios.create(defaultOptions)

instance.interceptors.request.use(function (config) {
  if (sessionStorage.getItem('crawler-client')) {
    const token = JSON.parse(sessionStorage.getItem('crawler-client')).auth.token;
-    config.headers.common.Authorization = token ? 'Bearer ' + token : ''
+    config.headers.Authorization = token ? 'Bearer ' + token : ''
  }
  return config
})

エラーは発生しなくなった。

Nuxt 3が出たので使ってみる

動機

Vue 3に対応したNuxt 3がリリースされたので、早速使ってみる。

ゴール

以前作成した下記のアプリと同等の機能を実装する。すでに、Vue3対応は完了しているため、大きな変更は必要ないはず。
https://github.com/hide6644/crawler-client

今回はインストールまで行う。インストール途中に「

ExperimentalWarning: The Fetch API is an experimental feature. This feature could change at any time

」のワーニングが表示されるが、今は気にしても仕方がないので、無視する。

インストール

実行環境
Windows 11
node 18

npx nuxi init プロジェクト名
cd プロジェクト名
npm install

実行

npm run dev

実行後、http://localhost:3000/にアクセスすると、welcomeページが表示される。

開発環境

開発環境には、Visual Studio Codeを使用する。インストール済みであれば、先ほどのインストールフォルダで、「code ./」を実行すると即座に起動できる。
(プラグインVolar ExtensionTypeScript Vue Plugin (Volar)のインストール推奨)

Vue3でvue-i18nの初期化でエラーになる

以下のようなエラーが出ていた。

Uncaught (in promise) SyntaxError: Not available in legacy mode
    at createCompileError (message-compiler.esm-bundler.js?965a:54:1)
    at createI18nError (vue-i18n.esm-bundler.js?666d:100:1)
    at useI18n (vue-i18n.esm-bundler.js?666d:2228:1)
    at setup (NovelSearch.vue?0b40:12:1)
    at callWithErrorHandling (runtime-core.esm-bundler.js?d2dd:155:1)
    at setupStatefulComponent (runtime-core.esm-bundler.js?d2dd:7165:1)
    at setupComponent (runtime-core.esm-bundler.js?d2dd:7119:1)
    at mountComponent (runtime-core.esm-bundler.js?d2dd:5473:1)
    at processComponent (runtime-core.esm-bundler.js?d2dd:5448:1)
    at patch (runtime-core.esm-bundler.js?d2dd:5038:1)

Vue 3のsetupでuseI18nを構成する場合は、createI18nのlegacyオプションをfalseに設定する必要がある。

const i18n = createI18n({
  legacy: false, // you must set `false`, to use Composition API
  locale: 'ja',
  messages: {
    en: enNames,
    ja: jaNames
  }
});

const app = createApp(App)
app.use(i18n)
app.mount('#app')

参考:https://vue-i18n.intlify.dev/guide/advanced/composition.html

Vuexで検索条件を保持する

やりたいこと

検索条件を保持し、当該画面に戻ってきたときに、前回の検索条件を表示する。

コード例

Vuexで呼び出される処理を定義していく。
※細かいところは省略済み。

//ファイルパス:modules/novel/search.js
const state = {
  searchParameter: {
    title: '',
    writername: '',
    description: ''
  }
}

const getters = {
  getSearchParameter: state => state.searchParameter
}

const actions = {
  // 検索処理
  [NOVEL_SEARCH]: ({ commit }, param) => {
    return new Promise((resolve, reject) => {
      // 検索条件をstateに保存する処理を呼び出す(下記のcommitでmutationsのNOVEL_SEARCHが実行される)
      commit(NOVEL_SEARCH, param)
      novelRepository.get(searchParameterBuilder(param))
      .then(resp => {
        commit(NOVEL_SEARCH_SUCCESS, resp)
        resolve(resp)
      })
      .catch(err => {
        commit(NOVEL_SEARCH_ERROR)
        reject(err)
      })
    })
  }
}

const mutations = {
  [NOVEL_SEARCH]: (state, param) => {
    state.searchParameter = param
  }
}

export default {
  state,
  getters,
  actions,
  mutations
}
import { createStore } from 'vuex'
import novelSearch from './modules/novel/search'

export default createStore({
  modules: {
    novelSearch
  }
})

vueファイルから以下の通り呼び出して、使用する。(記事の都合上、分けて記載しているけど、本来1つのファイル)

<script setup>
import { reactive, computed } from 'vue'
import { useStore } from 'vuex'
import { useI18n } from 'vue-i18n'
import { ElMessage, ElMessageBox } from 'element-plus'

const store = useStore()
const { t } = useI18n()

// Vuexに保持されている検索条件を読み込む
const searchParameter = computed(() => store.getters.getSearchParameter)

const state = reactive({
  title: searchParameter.value.title,
  writername: searchParameter.value.writername,
  description: searchParameter.value.description
})

function search() {
  const { title, writername, description } = state
  // 検索処理を実行する(search.jsのactionsのNOVEL_SEARCHが実行される)
  store.dispatch(NOVEL_SEARCH, { title, writername, description }).catch(error => {
    ElMessage({
      message: error,
      grouping: true,
      type: 'error'
    })
  })
}
</script>
<template>
  <div class="novel-search">
    <el-card class="box-card box-card-wrapper">
      <el-row class="row-wrapper">
        <el-col
          class="col-wrapper"
          :span="6"
        >
          <el-input
            :placeholder="$t('title')"
            v-model="state.title"
            clearable
          />
        </el-col>

Vue Composition APIを試す

Composition APIを使用すれば、同じ論理的な関心事に関連するコードを並べることが出来る?可読性が良くなる?良くわからないけど、前にVue 2で書いたコードを書き換えてみる。

このドキュメントでは実際にどのように変更したかをメインに記載する。

ログイン画面(Login.vue)の例

・Vue 2
一見シンプルに見えるけれども、thisで参照するものが多すぎて、何が使えて、何が使えないのか、後から見るとわかりづらい。

<script>
import {AUTH_REQUEST} from '@/store/actions/auth'

export default {
  data () {
    return {
      username: '',
      password: '',
    }
  },
  methods: {
    login: function () {
      const { username, password } = this
      this.$store.dispatch(AUTH_REQUEST, { username, password }).then(() => {
        this.$router.push(this.$route.query.redirect || '/')
      }).catch(error => {
        this.$message({
          showClose: true,
          message: error,
          type: 'error'
        })
      })
    }
  }
}
</script>

・Vue 3.2
Vue 2で必要だった独特な書き方がなくなって、一般的なJavaScriptの書き方になっている。インポート、宣言部、関数とわかれていて、わかりやすくなっているように感じる。

<script setup>
import { reactive } from 'vue'
import { useStore } from 'vuex'
import { useRouter, useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import { AUTH_REQUEST } from '@/store/actions/auth'

const store = useStore()
const router = useRouter()
const route = useRoute()

const state = reactive({
  username: '',
  password: ''
})

function login() {
  const { username, password } = state
  store.dispatch(AUTH_REQUEST, { username, password }).then(() => {
    router.push(route.query.redirect || '/')
  }).catch(error => {
    ElMessage({
      message: error,
      grouping: true,
      type: 'error'
    })
  })
}
</script>

element-ui 2からelement-plus 2にアップグレードする

今まではelement-ui 2を使用していたが、今後はVue 3に対応しているelement-plus 2を使用する。変更点のメモを残しておく。

element-ui 2の依存関係

"element-ui": "^2.15.6",

element-plus 2の依存関係

"@element-plus/icons-vue": "^1.1.4",
"element-plus": "^2.1.8",

element-ui 2の初期設定

import Vue from 'vue'
import App from './App.vue'
import ElementUI from 'element-ui'
import locale from 'element-ui/lib/locale/lang/ja'
import 'element-ui/lib/theme-chalk/index.css'

Vue.use(ElementUI, {locale})

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

element-plus 2の初期設定

import { createApp } from 'vue'
import App from './App.vue'
import ElementPlus from 'element-plus'
import locale from 'element-plus/lib/locale/lang/ja'
import 'element-plus/dist/index.css'

const app = createApp(App)

app.use(ElementPlus, {locale})

app.mount('#app')

vee-validate 3から4にアップグレードする

APIが完全に変わったため、マイグレーションガイドは存在しないらしい。変更点のメモを残しておく。

vee-validate 3の依存関係

"vee-validate": "^3.4.13",

vee-validate 4の依存関係

"@vee-validate/i18n": "^4.5.10",
"@vee-validate/rules": "^4.5.10",
"vee-validate": "^4.5.10",

vee-validate 3の初期設定

import Vue from "vue"
import { extend, localize } from "vee-validate"
import { required, max, email } from "vee-validate/dist/rules"
import en from "vee-validate/dist/locale/en.json"
import ja from "vee-validate/dist/locale/ja.json"
import enNames from '../locale/enNames.json'
import jaNames from '../locale/jaNames.json'

extend("required", required)
extend("max", max)
extend("email", email)

localize({
  en: {
    messages: en.messages,
    names: enNames
  },
  ja: {
    messages: ja.messages,
    names: jaNames
  }
})

let LOCALE = "ja"

Object.defineProperty(Vue.prototype, "locale", {
  get() {
    return LOCALE
  },
  set(val) {
    LOCALE = val
    localize(val)
  }
})

vee-validate 4の初期設定

import { defineRule, configure } from 'vee-validate';
import { required, max, email } from '@vee-validate/rules';
import { localize, setLocale } from '@vee-validate/i18n';
import en from '@vee-validate/i18n/dist/locale/en.json';
import ja from '@vee-validate/i18n/dist/locale/ja.json';
import enNames from '../locales/enNames.json'
import jaNames from '../locales/jaNames.json'

defineRule('required', required)
defineRule('max', max)
defineRule('email', email)

const customLocalize = localize({
  en: {
    messages: en.messages,
    names: enNames
  },
  ja: {
    messages: ja.messages,
    names: jaNames
  }
})

setLocale('ja')

configure({
  generateMessage: customLocalize
})

vee-validate 3の使用方法

<template>
  <ValidationObserver
    ref="observer"
    v-slot="{ passes }"
  >
    <el-form
      ref="form"
      class="login"
    >
      <h2>Sign up</h2>
      <ValidationProvider
        name="username"
        rules="required|max:16"
        v-slot="{ errors }"
      >
        <el-form-item
          :error="errors[0]"
          class="input-form-wrapper"
        >
          <el-input
            type="text"
            placeholder="Username"
            v-model="username"
          />
        </el-form-item>
      </ValidationProvider>
      <el-button
        type="primary"
        @click="passes(signup)"
      >Signup</el-button>
<script>
import { ValidationProvider, ValidationObserver } from "vee-validate"

export default {
  components: {
    ValidationProvider,
    ValidationObserver
  },
  methods: {
    signup: function () {
      const { username, password, email } = this

vee-validate 4の使用方法

※vee-validate以外にも、element-uiからelement-plusに変更している。element-plusの変更点については、こちらを参照のこと。

<template>
  <Form
    as="el-form"
    :validation-schema="schema"
    @submit="onSubmit"
  >
    <h2>Sign up</h2>
    <Field
      name="username"
      v-slot="{ value, field, errorMessage }"
    >
      <el-form-item
        :error="errorMessage"
        class="input-form-wrapper"
      >
        <el-input
          type="text"
          placeholder="Username"
          v-bind="field"
          :model-value="value"
        />
      </el-form-item>
    </Field>
<script setup>
import { Field, Form } from "vee-validate";

const schema = {
  username: 'required|max:16'
}

function onSubmit(values) {
  const { username, password, email } = values

記述量が減って良いと思う。

vue/cliを3.xから4.xにアップグレードする

最新バージョンを確認する。

D:\>npm outdated
Package                 Current  Wanted  Latest  Location
@vue/cli-plugin-babel    3.12.1  3.12.1   4.1.1  crawler-client
@vue/cli-plugin-eslint   3.12.1  3.12.1   4.1.1  crawler-client
@vue/cli-service         3.12.1  3.12.1   4.1.1  crawler-client

メジャーバージョンは、npm updateでは更新されないので、以下のコマンドを実行する。

D:\>vue upgrade
  Gathering package information...
  Name                    Installed       Wanted          Latest          Command to upgrade
  @vue/cli-service        3.12.1          3.12.1          4.1.1           vue upgrade @vue/cli-service
  @vue/cli-plugin-babel   3.12.1          3.12.1          4.1.1           vue upgrade @vue/cli-plugin-babel
  @vue/cli-plugin-eslint  3.12.1          3.12.1          4.1.1           vue upgrade @vue/cli-plugin-eslint
? Continue to upgrade these plugins? (Y/n) Y

他にも、

Package            Current  Wanted  Latest  Location
eslint              5.16.0  5.16.0   6.7.2  crawler-client
eslint-plugin-vue    5.2.3   5.2.3   6.0.1  crawler-client
sass-loader          7.3.1   7.3.1   8.0.0  crawler-client

などを個別にアップグレードする場合は、以下のコマンドを実行する。

D:\>npm install --save-dev sass-loader@8

以下のようにワーニングが出た場合は指示に従ってインストールする。

npm WARN sass-loader@8.0.0 requires a peer of node-sass@^4.0.0 but none is installed. You must install peer dependencies yourself.
npm WARN sass-loader@8.0.0 requires a peer of fibers@>= 3.1.0 but none is installed. You must install peer dependencies yourself.

D:\>npm install --save-dev sass fibers