書き換え箇所が多いのでメモしておきます。Vitestも対象。
また、Options APIで書いたMixinは使えなくなるのと、そもそもMixinは唐突に現れる為、可読性が悪かったので、Utilでexportして、importして使うようにします。
同様にInjectも便利なのですが、やはり唐突感があるので、役割が明確な$authや$toast以外のはUtil移動します。
Composable(use始り)も選択肢でしたが、テストを書く時に、Mockにして、テスト範囲を狭め場合に適している。
UtilはMock作らずに動かしちゃいたい場合に適している。
ただ、Nuxtは両者とも自動インポートされますが、VitestやJestではされないので、明示的にimportする必要があります。結果的にimport文があるので、唐突感がなくなって良き。
- script + defineNuxtComponent -> script setup
- components, mixins -> 削除
- props -> defineProps
- computed + get/set -> const computed + get/set
- $refs -> const + defineExpose
- $emit -> defineEmits
- -> 使用するComposable(use始まり)を定義
- data + return -> const + ref
- 【コラム】nullを使うかundefinedを使うか?
- computed -> const + computed
- created -> function + 呼び出し
- Mixin -> Utilでexportしてimport
- Inject(一部) -> Utilでexportしてimport
- Vitest mocks, data, wrapper.vm.$data -> stubGlobal, wrapper.vm[key], wrapper.vm
- TypeScriptのlintエラー対応あれこれ
- 今回のコミット内容
script + defineNuxtComponent -> script setup
<script setup>
と <script> + setup()
の2パターンあります。
setup()内で早期Returnできないので、ネストが深くなる。
結局functionを作る事になるので、前者にしました。
また、returnで値を公開する必要があるので、高機能だけど手間が掛かる。
- <script>
- export default defineNuxtComponent({
+ <script setup lang="ts">
- })
</script>
Composition API: setup() | Vue.js
単一ファイルコンポーネントで Composition API を使用する場合は、 より簡潔で人間工学的な構文のために、<script setup> を強くお勧めします。
components, mixins -> 削除
Composition APIでは不要なので削除。
importはNuxtでは命名規則に従っていれば不要ですが、Vitestでは自動インポートできないので、引き続き残しています。
<script setup lang="ts">
import AppLoading from '~/components/app/Loading.vue'
- import Application from '~/utils/application.js'
- components: {
- AppLoading
- },
- mixins: [Application],
props -> defineProps
template内のみで参照する場合
<script setup lang="ts">
- props: {
+ defineProps({
alert: {
type: String,
default: ''
}
- }
+ })
script内でも参照する場合
上記の3行目を下記に変更。$props.
alert等で参照可能になります。
+ const $props = defineProps({
computed + get/set -> const computed + get/set
親から受け取った値が変更されたら、親に渡して変更して受け取る。この流れは変わらない。
短く書けるようになったのは良いですね。
- computed: {
- syncQuery: {
- get () {
- return this.query
- },
- set (value) {
- this.$emit('update:query', value)
- }
- }
- },
+ const syncQuery = computed({
+ get: () => $props.query,
+ set: (value: object) => $emit('update:query', value)
+ })
$refs -> const + defineExpose
<script setup>
内のfunctionはそのままでは参照できないので、defineExpose
で参照できるようにしてあげる必要があります。
親から呼ばれる事を明示する事になるので、唐突感がなくなり解りやすい。
親(PageやComponent)
refは短い名前にしていましたが、constで定義する事になりスコープが広くなるので、少し長い名前に変えました。
<SpacesSearch
- ref="search"
+ ref="spacesSearch"
<script setup lang="ts">
+ const spacesSearch = ref<any>(null)
- this.$refs.search.setError()
+ spacesSearch.value.setError()
<InvitationsUpdate
- ref="update"
+ ref="invitationsUpdate"
- @show-update="$refs.update.showDialog($event)"
+ @show-update="invitationsUpdate.showDialog($event)"
<script setup lang="ts">
+ const invitationsUpdate = ref<any>(null)
子(Component)
<script setup lang="ts">
+ defineExpose({ setError })
function setError () {
<script setup lang="ts">
+ defineExpose({ showDialog })
async function showDialog (item: any) {
$emit -> defineEmits
template内の$emit
は使用可能ですが、lintエラーになるのでdefineEmitsを追加する事になる。
定義するなら参照した方が分かりやすそう。
@update:model-value="$emit('update:alert','')"
<script setup lang="ts">
+- defineEmits(['update:alert'])
+ const $emit = defineEmits(['update:alert'])
-> 使用するComposable(use始まり)を定義
useNuxtApp()で、Plugin等でProvideしているのをInjectできます。
※Vitestではそれぞれvi.stubGlobalで値を返してあげる必要があります。
const $config = useRuntimeConfig()
const { t: $t, tm: $tm } = useI18n()
const { $auth, $toast } = useNuxtApp()
const $route = useRoute()
data + return -> const + ref
- data () {
- return {
- loading: true,
- processing: false,
- waiting: false,
- dialog: false,
- tabDescription: 'input',
- infomation: null,
- uid: null
- }
- },
+ const loading = ref(true)
+ const processing = ref(false)
+ const waiting = ref(false)
+ const dialog = ref(false)
+ const tabDescription = ref('input')
+ const infomation = ref<any>(null)
+ const uid = ref<string | null>(null)
refの値に、script内で参照・変更するには.value
を付ける必要がある。template内では不要。
リアクティビティー API: コア | Vue.js
【コラム】nullを使うかundefinedを使うか?
極論、どちらでも良いのですが、個人的には、undefinedを書いたり、typeofでundefined判定するのを避けています。
undefined(未定義)
は入れ物がない、null
は入れ物はあるけど値がないなので。
とは言え、undefinedの値は返ってきます。nullは明示するかAPIレスポンスに存在する。
undefinedでもnullでもない
は、下記で判定でき、短く書けるので、好んで使っています。
if (data != null) {
また、上で追加した、これ↓では初期値に{}
や''
は入れず、null
にする事で、取得していない事が明確になるようにしています。ここでは別途、状態(loading)を持っているので、どちらでも動きますが、それぞれに状態を持った方が良いと思います。
+ const infomation = ref<any>(null)
+ const uid = ref<string | null>(null)
例えば、こんなケース。このコードではlocaleが設定されれば問題ないのですが、もしエラー時にalertに''
がセットされてしまったら、エラー処理がされず、true
が返ってしまいます。
- let alert = ''
+ let alert: string | null = null
if (response?.ok) {
if (data?.infomation?.current_page === page.value) {
<成功時の処理>
} else {
alert = $t('system.error')
}
} else {
if (data == null) {
alert = $t(`network.${response?.status == null ? 'failure' : 'error'}`)
} else {
alert = data.alert || $t('system.default')
}
}
- if (alert !== '') {
+ if (alert != null) {
<エラー処理>
}
- return alert === ''
+ return alert == null
逆に、null
(or undefined
)と''
が同じ振る舞いをする場合は、''
の方がシンプルです。
- <v-card-text v-if="alert != null && alert !== ''">
+ <v-card-text v-if="alert !== ''">
<v-icon color="warning">mdi-alert</v-icon>
{{ alert }}
</v-card-text>
<script setup lang="ts">
- const alert = ref<string | null>(null)
+ const alert = ref('')
computed -> const + computed
computedも同様に、script内で参照するには.value
を付ける必要がある。template内では不要。
また、全体的にですが、this.
は使えなくなるので、削除もしています。
1行で書く(パラメータなし)
- computed: {
- invitationURL () {
- return `${location.protocol}//${location.host}/users/sign_up?code=${this.invitation.code}`
- }
- },
+ const invitationURL = computed(() => `${location.protocol}//${location.host}/users/sign_up?code=${invitation.value.code}`)
1行で書く(パラメータあり)
- computed: {
- currentMemberAdmin () {
- return (space) => {
- return space?.current_member?.power === 'admin'
- }
- }
- },
+ const currentMemberAdmin = computed(() => (space: any) => space?.current_member?.power === 'admin')
複数行
- computed: {
- title () {
+ const title = computed(() => {
let label = ''
- if (this.infomation?.label_i18n != null && this.infomation.label_i18n !== '') {
+ if (infomation.value?.label_i18n != null && infomation.value.label_i18n !== '') {
- label = `[${this.infomation.label_i18n}]`
+ label = `[${infomation.value.label_i18n}]`
}
- return label + (this.infomation?.title || '')
+ return label + (infomation.value?.title || '')
- }
- },
+ })
created -> function + 呼び出し
createdの名前は何でもOK。早期Returnを使いたいので、functionを作って呼び出しています。
createdを呼び出す時のawait
は不要です。
- async created () {
+ created()
+ async function created () {
- if (!await this.getInfomationsDetail()) { return }
+ if (!await getInfomationsDetail()) { return }
- this.loading = false
+ loading.value = false
- },
+ }
序でにURLパラメータのID([id].vue)のチェック(文字、0始まり)も追加。
- if (!await getInfomationsDetail()) { return }
+ const id = Number($route.params.id)
+ if (isNaN(id) || String(id) !== String($route.params.id)) { return redirectError(404, {}) }
+ if (!await getInfomationsDetail(id)) { return }
redirectErrorは、Mixin -> Utilでexportしてimport 参照
methods -> function
- methods: {
- async getInfomationsDetail () {
+ async function getInfomationsDetail () {
- }
}
Mixin -> Utilでexportしてimport
appCheckResponseとappCheckErrorResponseはMixinのmethodsで定義していたもの。
呼び出しは簡素に書けていたものの、呼び出し先は複雑になってしまい、可読性が悪くなっていました。Utilでやる事を限定する事で、見通しが良くなったと思います。
redirectErrorのimportは自動インポートの為、不要ですがVitestの為に明示しています。
- import Application from '~/utils/application.js'
+ import { redirectError } from '~/utils/auth'
async function getInfomationsDetail (id: number) {
const url = $config.public.infomations.detailUrl.replace(':id', String(id))
const [response, data] = await useApiRequest($config.public.apiBaseURL + url)
if (response?.ok) {
- if (this.appCheckResponse(data, { redirect: true }, data?.infomation == null)) {
+ if (data?.infomation == null) {
+ redirectError(null, { alert: $t('system.error') })
+ } else {
infomation.value = data.infomation
return true
}
} else {
- this.appCheckErrorResponse(response?.status, data, { redirect: true, require: true }, { notfound: true })
+ if (response?.status === 404) {
+ redirectError(404, { alert: data.alert || $t('system.default'), notice: data.notice })
+ } else if (data == null) {
+ redirectError(response?.status, { alert: $t(`network.${response?.status == null ? 'failure' : 'error'}`) })
+ } else {
+ redirectError(response?.status, { alert: data.alert || $t('system.default'), notice: data.notice })
+ }
}
return false
}
utils/auth.ts
function redirectError (statusCode: any, query: any) {
showError({ statusCode, data: { alert: query.alert, notice: query.notice } })
/* c8 ignore next */ // eslint-disable-next-line no-throw-literal
if (process.env.NODE_ENV !== 'test') { throw 'showError' }
}
export {
redirectError
}
.eslintrc.js
rules: {
+ 'no-lonely-if': 'off'
}
Inject(一部) -> Utilでexportしてimport
- ({{ $dateFormat('ja', infomation.started_at, 'N/A') }})
+ ({{ dateFormat('ja', infomation.started_at, 'N/A') }})
+ import { dateFormat } from '~/utils/helper'
plugins/utils.js -> utils/helper.ts
- export default defineNuxtPlugin((_nuxtApp) => {
- return {
- provide: {
+ export {
sleep,
dateFormat,
- }
- }
- })
+ }
- export const TestPluginUtils = {
- $sleep: sleep,
- $dateFormat: dateFormat
- }
test/setup.ts
- import { TestPluginUtils } from '~/plugins/utils'
config.global.mocks = {
- },
+ }
- ...TestPluginUtils
}
vi.stubGlobal('useRuntimeConfig', vi.fn(() => config.global.mocks.$config))
vi.stubGlobal('useI18n', vi.fn(() => ({ t: config.global.mocks.$t, tm: config.global.mocks.$tm })))
Vitest mocks, data, wrapper.vm.$data -> stubGlobal, wrapper.vm[key], wrapper.vm
useNuxtAppやuseRouteを使うように変更したので、
mountで指定していた「mocks: {
」は効かなくなります。なので、vi.stubGlobal
に変更します。
「data () {
」も参照先が変更になる($dataではなくなる)ので、
スマートではないですが、直接セットするように変更します。
const mountFunction = (loggedIn: boolean, query = {}, values = {}) => {
vi.stubGlobal('useApiRequest', mock.useApiRequest)
vi.stubGlobal('useAuthRedirect', vi.fn(() => mock.useAuthRedirect))
vi.stubGlobal('navigateTo', mock.navigateTo)
+ vi.stubGlobal('useNuxtApp', vi.fn(() => ({
+ $auth: {
+ loggedIn,
+ setData: mock.setData
+ },
+ $toast: mock.toast
+ })))
+ vi.stubGlobal('useRoute', vi.fn(() => ({
+ query: { ...query }
+ })))
- const wrapper = mount(Page, {
+ const wrapper: any = mount(Page, {
global: {
stubs: {
AppProcessing: true,
AppMessage: true,
ActionLink: true
- },
- mocks: {
- $auth: {
- loggedIn,
- setData: mock.setData
- },
- $route: {
- path: '/users/sign_in',
- query: { ...query }
- },
- $toast: mock.toast
}
- },
- data () {
- return values
}
})
expect(wrapper.vm).toBeTruthy()
+ for (const [key, value] of Object.entries(values)) { wrapper.vm[key] = value }
return wrapper
}
また、wrapper.vm.$data
に入って変数は、直下に変更、
Inject(下記では$dateFormat)はimportに変更したので、$を削除します。
※dateFormatの所は固定値でも良いのですが、別途テストを書いている為、正しさはそっちで担保して、ここでは表示されるかのみ確認しています。
- expect(wrapper.vm.$data.infomation).toEqual(data.infomation)
+ expect(wrapper.vm.infomation).toEqual(data.infomation)
- expect(wrapper.text()).toMatch(wrapper.vm.$dateFormat('ja', data.infomation.started_at)) // 開始日
+ expect(wrapper.text()).toMatch(wrapper.vm.dateFormat('ja', data.infomation.started_at)) // 開始日
TypeScriptのlintエラー対応あれこれ
初期値がある場合、ある程度、型推論が効くので、明示する必要はないのですが、推論できない場合があるので、lintで怒られた所だけ対応します。
頑張ると大変だったり、そもそも頑張れないものもあるので、anyで諦めて貰うのが良いと思います。頑張りたいのは、ここではないので。
これは対応できなものらしいので、as any
で逃げます。
型のインスタンス化は非常に深く、無限である可能性があります。ts(2589)
- for (const item of $tm('items.space')) {
+- for (const item of ($tm('items.space') as any)) {
+ for (const [, item] of Object.entries($tm('items.space') as any) as any) {
プロパティ 'user' は型 'SessionData' に存在しません。ts(2339)
- user: computed(() => authData.value?.user),
+ user: computed(() => (authData.value as any)?.user),
プロパティ 'id' は型 '{}' にありませんが、型 'SessionData' では必須です。ts(2741)
- setData: (data: object) => { authData.value = data },
+ setData: (data: object) => { (authData.value as any) = data },
型 'string' の式を使用して型 '{ admin: string; writer: string; default: string; }' にインデックスを付けることはできないため、要素は暗黙的に 'any' 型になります。 型 'string' のパラメーターを持つインデックス シグネチャが型 '{ admin: string; writer: string; default: string; }' に見つかりませんでした。ts(7053)
- <v-icon size="x-small">{{ $config.public.member.powerIcon[power] }}</v-icon>
+ <v-icon size="x-small">{{ ($config.public.member.powerIcon as any)[power] }}</v-icon>
never
は、値を持たない事を意味する型。明示的にanyで対応。
プロパティ 'filter' は型 'never' に存在しません。ts(2339) パラメーター 'item' の型は暗黙的に 'any' になります。ts(7006)
- return items.filter(item => !item.adminOnly || $props.admin)
+ return items.filter((item: any) => !item.adminOnly || $props.admin)
値は文字列だけど、何故、数字だと推論されたのだろうか?
型 'number' を型 'string' に割り当てることはできません。ts(2322)
- :label="label"
+ :label="String(label)"
こちらも同様に文字列に変換で対応。
この呼び出しに一致するオーバーロードはありません。 前回のオーバーロードにより、次のエラーが発生しました。ts(2769)
- const url = $config.public.spaces.detailUrl.replace(':code', $route.params.code)
+ const url = $config.public.spaces.detailUrl.replace(':code', String($route.params.code))
頑張らずにanyで対応。
プロパティ 'showItems' は型 '{ <省略> (......' に存在しません。ts(2339)
- const wrapper = mountFunction(true)
+ const wrapper: any = mountFunction(true)
expect(wrapper.vm.showItems).toEqual(requiredItems)
今回のコミット内容
今回記載した箇所にはありませんが、
以前は、template内でオプショナルチェーン (?.)を書くとテストで落ちましたが、
Vitestになってから(?)落ちなくなったので、簡素に書けて良いです。
とは言え、上手く使わないとバグの温床になるし、無駄に使うと解りにくくなりますが。
origin#507 Options API/JavaScriptをComposition API/TypeScriptに書き換え。Mixinを止める。injectをUtilに書き換え
origin#507 Options API/JavaScriptをComposition API/TypeScriptに書き換え。Mixinを止める。injectをUtilに書き換え