こんにちは。GameWith のエンジニアの tiwu です。 今回は Vue.js, Typescript で開発しているダッシュボードの Vue 3 へのアップグレードについて書いていきたいと思います。 ダッシュボードについては以下の記事を参照してください。 タイトルに書いてあるとおり Vue3 へのアップグレードを何回かチャレンジしていました。 一気に Vue3 にアップグレードしようとした結果、依存関係の影響で全てを一気にアップグレードする必要があり対応の難易度が跳ね上がって毎回挫折してました・・・。 特に 今回ステップを踏んでアップグレードしたおかげで、無事 Vue3 にアップグレードすることができました! Vue 3 へのアップグレードは 3 つのステップで行いました。 それぞれ紹介します。 まず初めに Vue を2系の最新バージョンにアップグレードしました。 アップグレード前 アップグレード後 アップグレード前 アップグレード後 アップグレード前 アップグレード後 アップグレード前 アップグレード後 パスなどの修正のため対象ファイル数はかなり多かったですが、一括置換で対応を行ったため作業時間はそれほどかかりませんでした。 次に vue-cli から vite に移行しました。 アップグレード前 アップグレード後 アップグレード前 アップグレード後 アップグレード前 アップグレード後 アップグレード前 アップグレード後 アップグレード前 アップグレード後 アップグレード前 アップグレード後 アップグレード前
アップグレード後
アップグレード前 アップグレード後 アップグレード前
アップグレード後
開発しているダッシュボードはローカルでは PHP でサーバーを立ち上げつつ vite(vue-cli-serve) でもサーバーを立ち上げています。
開発・本番環境では PHP でダッシュボードサーバーを立ち上げ、js や css は S3 にアップロードしています。 この変則編成のため、vite への移行の際は config ファイルの設定の移行に手間取りました。 最後に Vue 2.7.16 から 3.2.25 にアップグレードしました。 アップグレード前 アップグレード後 また、 を追加しました。 アップグレード前 アップグレード後 アップグレード前 アップグレード後 アップグレード前 アップグレード後 アップグレード前 アップグレード後 アップグレード前 アップグレード後 2020年に Vue を導入した際に、Vue3 に備えて 一番苦労した点としては jsx + vuedraggable の組み合わせ箇所の修正でした。 jsx のスロットは vite + vue3 + jsx の例が世の中的に少なく、実装にかなり時間がかかりました。 さらにこの jsx 内で vuedraggable を利用しており、vuedraggable のアップグレードの修正にも時間がかかりました。 Vue3 にアップグレード後 eslint のアップグレードや jest から vitest 移行なども実施しており、これらの対応のおかげで Circle CI の時間がかなり短縮されました。 こちらについてはまた別のブログで紹介しようと思います!
はじめに
挫折
vue-cli
のプラグイン周りの依存関係の解消が大変でした。Vue 3 への移行の手順
Vue 2.6.12 から 2.7.16 にアップグレード
依存関係
@vue/composition-api
を削除し、vue
と vue-template-compiler
を 2.7.16 にアップグレードしました。主な修正箇所
@vue/composition-api
の削除import VueCompositionApi from '@vue/composition-api'
Vue.use(VueCompositionApi)
from '@vue/composition-api'
を from 'vue'
に変更import { defineComponent, getCurrentInstance } from '@vue/composition-api';
import { defineComponent, getCurrentInstance } from 'vue';
root.$route
を root.proxy.$route
に変更const gameId = Number(root.$route.params.gameId);
const gameId = Number(root.proxy.$route.params.gameId);
root
の型定義を SetupContext['root']
から Vue
に変更const useHoge = (root: SetupContext['root']) => {
}
const root = getCurrentInstance();
useHoge(root);
const useHoge = (root: Vue) => {
}
const root = getCurrentInstance();
useHoge(root.proxy);
補足など
vue-cli から vite に移行
依存関係
@vue/cli
関連を削除し、vite
, vite-plugin-env-compatible
, @vitejs/plugin-vue2
, @vitejs/plugin-vue2-jsx
, @babel/preset-env
を追加しました。主な修正箇所
コマンドの変更
{
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"test:unit": "vue-cli-service test:unit",
"lint": "vue-cli-service lint --no-fix",
}
}
{
"scripts": {
"serve": "vite",
"build": "vite build",
"test:unit": "jest -- --mode test",
"lint": "eslint src --no-fix",
}
}
環境変数の修正
VUE_APP_HOGE=XXXXX
VITE_HOGE=XXXXX
babel.config.js
の修正module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
};
module.exports = {
presets: [
'@babel/preset-env'
]
};
jest.config.js
の修正module.exports = {
preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel',
};
module.exports = {
preset: 'ts-jest',
moduleNameMapper: {
'@/(.*)': '<rootDir>/src/$1',
},
testEnvironment: 'jsdom',
transform: {
'.*\\.(vue)$': '@vue/vue2-jest'
}
};
tsconfig.json
の修正{
"compilerOptions": {
"types": [
"webpack-env",
"jest",
"jquery",
"bootstrap-vue/esm",
],
},
}
{
"compilerOptions": {
"types": [
"vite/client",
"jest",
"jquery",
"bootstrap-vue/esm",
],
},
}
index.html
の修正public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>hoge</title>
</head>
<body>
<div id="app"></div>
</body>
</html>
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>hoge</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
App.vue
の修正<template>
<div id="app">
<router-view/>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
});
</script>
<template>
<router-view/>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
});
</script>
vue.config.js
の削除、vite.config.ts
の追加vue.config.js
const WebpackAssetsManifest = require('webpack-assets-manifest');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
let publicPath = '/dist/';
if (process.env.IS_BUILD) {
publicPath = 'https://XXXXX.s3.amazonaws.com/dist/';
}
module.exports = {
parallel: process.env.CI ? false : require('os').cpus().length > 1,
publicPath: publicPath,
outputDir: './dist',
filenameHashing: true,
productionSourceMap: false,
css: {
extract: false,
sourceMap: true
},
configureWebpack: config => {
config.plugins = config.plugins.concat(
new WebpackAssetsManifest({
output: 'asset-manifest.json'
})
);
config.plugins = config.plugins.concat(
new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns: ['dist/*']
})
);
config.devtool = 'eval-source-map';
config.resolve.fallback = {
crypto: false,
};
},
chainWebpack: config => {
config.plugins.delete('html');
config.plugins.delete('preload');
config.plugins.delete('prefetch');
},
devServer: {
host: 'localhost',
port: 9000,
hot: true,
},
pluginOptions: {
proxy: {
context: (pathname) => {
return !pathname.startsWith('/dist') && pathname.startsWith('/');
},
options: {
target: `https://XXXX.jp`,
secure: false,
changeOrigin: true,
autoRewrite: true,
cookieDomainRewrite: 'localhost',
xfwd: true,
router: {
'/hoge': `http://YYY.jp/hoge`,
},
},
},
}
};
vite.config.ts
import { defineConfig, loadEnv } from 'vite';
import vue from '@vitejs/plugin-vue2';
import vueJsx from '@vitejs/plugin-vue2-jsx';
import envCompatible from 'vite-plugin-env-compatible';
import path from 'path';
export default defineConfig(({ command, mode }) => {
const env = loadEnv(mode, process.cwd());
let base = '/';
if (command === 'build') {
base = 'https://XXXXX.s3.amazonaws.com/dist/';
}
return {
base,
outputDir: './dist',
plugins: [
vue(),
vueJsx(),
envCompatible({
prefix: 'VITE',
mountedPath: 'process.env'
})
],
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
}
},
build: {
rollupOptions: {
output: {
entryFileNames: 'assets/[name].js',
chunkFileNames: 'assets/[name].js',
assetFileNames: 'assets/[name].[ext]'
}
}
},
server: {
port: 9000,
proxy: {
'/hoge': {
target: `http://XXXX.jp/hoge`,
secure: false,
changeOrigin: true,
autoRewrite: true,
cookieDomainRewrite: 'localhost',
xfwd: true,
},
}
}
};
});
補足など
Vue 2.7.16 から から 3.2.25 にアップグレード
依存関係
vue
, vue-router
, vuedraggable
, @vue/test-utils
, bootstrap-vue
, vue-i18n
をアップグレードしました。
vuejs-datepicker
の代わりに @vuepic/vue-datepicker
@vue/vue2-jest
の代わりに @vue/vue3-jest
@vitejs/plugin-vue2
の代わりに @vitejs/plugin-vue
@vitejs/plugin-vue2-jsx
の代わりに @vitejs/plugin-vue-jsx
vue-template-compiler
の代わりに @vue/compiler-sfc
主な修正箇所
jest.config.js
の修正module.exports = {
transform: {
'.*\\.(vue)$': '@vue/vue2-jest'
}
};
module.exports = {
transform: {
'.*\\.(vue)$': '@vue/vue3-jest'
}
};
vite.config.ts
の修正import vue from '@vitejs/plugin-vue2';
import vueJsx from '@vitejs/plugin-vue2-jsx';
export default defineConfig(({ command, mode }) => {
return {
plugins: [
vue(),
vueJsx(),
],
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
}
},
};
});
import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx';
export default defineConfig(({ command, mode }) => {
return {
plugins: [
vue({
template: {
compilerOptions: {
compatConfig: {
MODE: 2
}
}
}
}),
vueJsx(),
],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
vue: '@vue/compat'
}
},
};
});
vue-router
を利用したテストの修正import { mount } from '@vue/test-utils';
import List from '@/components/pages/list.vue';
describe('list.vue', () => {
afterEach(() => {
jest.resetAllMocks();
});
it('一覧が表示されている', async() => {
const $route = {
query: {
startDate: '2020-03-25',
endDate: '2020-04-01'
},
};
const wrapper = mount(List, {
mocks: {
$route,
},
});
expect(wrapper.html()).toMatchSnapshot();
});
});
import { mount } from '@vue/test-utils';
import { createRouter, createWebHistory } from 'vue-router';
import List from '@/components/pages/list.vue';
const routes = [
{ path: '/',
name: 'list',
component: List
}
];
const router = createRouter({
history: createWebHistory(),
routes,
});
describe('list.vue', () => {
afterEach(() => {
jest.resetAllMocks();
});
it('一覧が表示されている', async() => {
router.push({ path: '/', query: { startDate: '2020-03-25', endDate: '2020-04-01' } });
await router.isReady();
const wrapper = mount(List, {
global: {
plugins: [router]
},
});
expect(wrapper.html()).toMatchSnapshot();
});
});
jsx
の修正:イベントハンドラexport default defineComponent({
render() {
return (
<div>
<form vOn:submit_stop_prevent={this.onSubmit}>
<input type="text" />
</form>
<button vOn:click={this.onClick}>button</button>
</div>
)
}
});
export default defineComponent({
render() {
return (
<div>
<form onSubmit={this.onSubmit}>
<input type="text" />
</form>
<button onClick={this.onClick}>button</button>
</div>
)
}
});
jsx
の修正:スロットexport default defineComponent({
render() {
return (
<custom-modal>
<div>
<p>hoge</p>
<p>fuga</p>
</div>
<template slot="modal-footer">
<p>piyo</p>
</template>
</custom-modal>
)
}
});
export default defineComponent({
render() {
return (
<custom-modal>
{{
default: () => {
return (
<div>
<p>hoge</p>
<p>fuga</p>
</div>
)
}
'modal-footer': () => {
return (
<p>piyo</p>
)
}
}}
</custom-modal>
)
}
});
補足など
@vue/composition-api
を導入していたため Vue 自体の修正箇所は少なかったです。おわりに