Vue3はTypeScriptとの親和性も高く、React,svelteにも似た書きやすさ。 vue3アプリの初期化( npm init vue@3 )

Vue3でページがリロードされたかどうかを検知する方法

import { onMounted } from 'vue';

export default {
  setup() {
    onMounted(() => {
      if (performance.navigation.type === 1) {
        console.log("This page is reloaded");
      } else {
        console.log("This page is not reloaded");
      }
    });
  },
};

i

No.2283
02/10 09:23

edit

Vue3のcomposable( hook )を使ったシンプルな確認ダイアログ

● 使い方

src/views/AboutView.vue

import { useConfirmDialog } from "@/composables/useConfirmDialog";
import ConfirmDialog from "@/components/ConfirmDialog.vue";

const { confirm } = useConfirmDialog({
  title: "HOME画面に移動してよろしいですか?",
  text: "OKボタンを押すと画面遷移します",
});

const handleDialog = async () => {
  if (await confirm()) {
    console.log("okがクリックされました");
  } else {
    console.log("cancelがクリックされました");
  }
};
    <button @click="handleDialog">Aboutダイアログ表示</button>
    <ConfirmDialog></ConfirmDialog>

以上です。シンプルに記述できますね。

必要なファイルは2ファイル 「src/composables/useConfirmDialog.ts」 「src/components/ConfirmDialog.vue」 だけです。

src/composables/useConfirmDialog.ts

import { ref } from "vue";

type DialogText = {
  title?: string;
  text?: string;
  ok?: string;
  cancel?: string;
};

const isOpen = ref<boolean>(false);

const dialogText = ref<DialogText>({
  title: "○○します。よろしいですか?",
  text: "確認して以下のボタンをクリックしてください。",
  ok: "OK",
  cancel: "キャンセル",
});

let _resolve: (value: boolean | PromiseLike<boolean>) => void;

export function useConfirmDialog(props: DialogText = {}) {
  if (props.title) dialogText.value.title = props.title;
  if (props.text) dialogText.value.text = props.text;
  if (props.ok) dialogText.value.ok = props.ok;
  if (props.cancel) dialogText.value.cancel = props.cancel;

  const confirm = () => {
    isOpen.value = true;
    return new Promise<boolean>((resolve) => {
      _resolve = resolve;
    });
  };

  const ok = () => {
    isOpen.value = false;
    _resolve(true);
  };

  const cancel = () => {
    isOpen.value = false;
    _resolve(false);
  };

  const close = () => {
    isOpen.value = false;
  };

  return {
    confirm,
    ok,
    cancel,
    close,
    isOpen,
    dialogText,
  };
}

src/components/ConfirmDialog.vue

<template>
  <div
    v-if="isOpen"
    class="confirm-dialog confirm-dialog-show"
    id="confirm-dialog149993"
    style="margin-top: -86px"
  >
    <div class="confirm-dialog-title">
      {{ dialogText.title }}
    </div>
    <div class="confirm-dialog-content">
      <p class="confirm-dialog-message">
        {{ dialogText.text }}
      </p>
    </div>
    <div class="confirm-dialog-action">
      <button @click="ok" class="btn-ok">{{ dialogText.ok }}</button>
      <button @click="cancel" class="btn-cancel">
        {{ dialogText.cancel }}
      </button>
    </div>
  </div>
  <div v-if="isOpen" @click="close" class="confirm-dialog-overlay"></div>
</template>

<script lang="ts" setup>
import { useConfirmDialog } from "@/composables/useConfirmDialog";

const { isOpen, ok, cancel, close, dialogText } = useConfirmDialog();
</script>

<style scoped>
.confirm-dialog {
  color: #1b1919;
  position: fixed;
  z-index: 1061;
  border-radius: 2px;
  width: 400px;
  margin-left: -200px;
  background-color: #fff;
  -webkit-box-shadow: 0 11px 15px -7px rgba(0, 0, 0, 0.2),
    0 24px 38px 3px rgba(0, 0, 0, 0.14), 0 9px 46px 8px rgba(0, 0, 0, 0.12);
  box-shadow: 0 11px 15px -7px rgba(0, 0, 0, 0.2),
    0 24px 38px 3px rgba(0, 0, 0, 0.14), 0 9px 46px 8px rgba(0, 0, 0, 0.12);
  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
  left: 50%;
  top: 50%;
  font-size: 16px;
}

.confirm-dialog.confirm-dialog-show {
  -webkit-animation: bounceIn 0.35s ease;
  -o-animation: bounceIn 0.35s ease;
  animation: bounceIn 0.35s ease;
}

.confirm-dialog .confirm-dialog-title {
  text-align: center;
  padding: 24px 30px 20px 30px;
  font-size: 18px;
  line-height: 1.4;
  font-weight: bold;
  color: #1b1919;
}

.confirm-dialog .confirm-dialog-title + .confirm-dialog-content {
  padding-top: 0;
}

.confirm-dialog .confirm-dialog-content {
  text-align: justify;
  padding: 30px 30px 5px 30px;
}

.confirm-dialog .confirm-dialog-content .confirm-dialog-message {
  font-size: 14px;
  line-height: 1.4;
  text-align: center;
  margin: 0;
  padding: 0;
  color: #635a56;
}

.confirm-dialog .confirm-dialog-content .confirm-dialog-prompt input {
  width: 100%;
  height: 36px;
  display: inline-block;
  padding: 6px 0;
  -webkit-box-shadow: none;
  box-shadow: none;
  border: none;
  outline: none;
  font-size: 16px;
  color: #1b1919;
  border-bottom: 1px solid #d9d6d4;
}

.confirm-dialog .confirm-dialog-action {
  display: flex;
  flex-direction: column;
  margin: 10px 30px;
}

.confirm-dialog .confirm-dialog-action [class*="btn-"] {
  font-size: 14px;
  margin: 7px 0;
  cursor: pointer;
  color: #1b1919;
  height: 36px;
  min-width: 88px;
  text-align: center;
  display: inline-block;
  border: 1px solid #dedede;
  border-radius: 12px;
  -webkit-tap-highlight-color: rgba(0, 0, 0, 0.12);
  -webkit-transition: all 0.45s cubic-bezier(0.23, 1, 0.32, 1);
  -o-transition: all 0.45s cubic-bezier(0.23, 1, 0.32, 1);
  transition: all 0.45s cubic-bezier(0.23, 1, 0.32, 1);
}

.confirm-dialog .confirm-dialog-action .btn-ok {
  background-color: black;
  border-color: black;
  color: #fff;
}

.confirm-dialog .confirm-dialog-action .btn-ok:active {
  background-color: #444;
}

.confirm-dialog .confirm-dialog-action .btn-cancel {
  background-color: #ececec;
  color: #635a56;
}

.confirm-dialog .confirm-dialog-action .btn-cancel:active {
  background-color: #dcdcdc;
}

@media all and (max-width: 540px) {
  .confirm-dialog {
    width: auto;
    margin-left: 0;
    margin-right: 0;
    left: 15px;
    right: 15px;
  }
}

.confirm-dialog-overlay {
  position: fixed;
  background-color: #000;
  z-index: 1060;
  height: 100%;
  width: 100%;
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
  opacity: 0.4;
  -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";
  -webkit-transition: opacity 0.45s cubic-bezier(0.23, 1, 0.32, 1);
  -o-transition: opacity 0.45s cubic-bezier(0.23, 1, 0.32, 1);
  transition: opacity 0.45s cubic-bezier(0.23, 1, 0.32, 1);
}
</style>
添付ファイル1
No.2280
02/03 11:27

edit

添付ファイル

Sentry

● sentry のインストール

npm install --save @sentry/vue @sentry/tracing @sentry/integrations

● sentry のDSNの確認方法

Sentry ログインする
「Settings」 →「Projects」 →「<あなたのプロジェクト>」 →「client keys(DSN)」
から参照する
import { BrowserTracing } from "@sentry/tracing";
import { CaptureConsole } from "@sentry/integrations";
Sentry.init({
  app,
  dsn: "xxxxxxxxxxxxxxxx",
  denyUrls: [<除外したいホスト名の文字列 or regex>],
  ignoreErrors: [<除外したいエラーの文字列 or regex>],
  integrations: [
    new BrowserTracing({
      routingInstrumentation: Sentry.vueRouterInstrumentation(router),
      tracePropagationTargets: [
        "localhost",
        "your.server.com",  // 変更する
        /^\//,
      ],
    }),
    new CaptureConsole({ levels: ["error"] }),
  ],
  tracesSampleRate: 1.0,
});

init のオプション

Sentry.init({
  // Client's DSN.
  dsn: 'xxxxxxx',
  // An array of strings or regexps that'll be used to ignore specific errors based on their type/message
  ignoreErrors: [],
  // An array of strings or regexps that'll be used to ignore specific errors based on their origin url
  denyUrls: [],
  // An array of strings or regexps that'll be used to allow specific errors based on their origin url
  allowUrls: [],
  // Debug mode with valuable initialization/lifecycle informations.
  debug: true,
  // Whether SDK should be enabled or not.
  enabled: true,
  // Custom integrations callback
  integrations(integrations) {
    return [new HappyIntegration(), ...integrations];
  },
  // A release identifier.
  release: '',
  // An environment identifier.
  environment: '',
  // Custom event transport that will be used to send things to Sentry
  transport: HappyTransport,
  // Method called for every captured event
  async beforeSend(event, hint) {
    // Because beforeSend and beforeBreadcrumb are async, user can fetch some data
    // return a promise, or whatever he wants
    // Our CustomError defined in errors.js has `someMethodAttachedToOurCustomError`
    // which can mimick something like a network request to grab more detailed error info or something.
    // hint is original exception that was triggered, so we check for our CustomError name
    if (hint.originalException.name === 'CustomError') {
      const serverData = await hint.originalException.someMethodAttachedToOurCustomError();
      event.extra = {
        ...event.extra,
        serverData,
      };
    }
    console.log(event);
    return event;
  },
  // Method called for every captured breadcrumb
  beforeBreadcrumb(breadcrumb, hint) {
    // We ignore our own logger and rest of the buttons just for presentation purposes
    if (breadcrumb.message.startsWith('Sentry Logger')) return null;
    if (breadcrumb.category !== 'ui.click' || hint.event.target.id !== 'breadcrumb-hint') return null;
    // If we have a `ui.click` type of breadcrumb, eg. clicking on a button we defined in index.html
    // We will extract a `data-label` attribute from it and use it as a part of the message
    if (breadcrumb.category === 'ui.click') {
      const label = hint.event.target.dataset.label;
      if (label) {
        breadcrumb.message = `User clicked on a button with label "${label}"`;
      }
    }
    console.log(breadcrumb);
    return breadcrumb;
  },
});

● sentry integration

・Default Integration

デフォルトで有効なインテグレーションです

https://docs.sentry.io/platforms/javascript/guides/gatsby/configuration/integrations/default/

・Pluggable Integrations

プラグインです

https://docs.sentry.io/platforms/javascript/configuration/integrations/plugin/

● Track Vue Components

https://docs.sentry.io/platforms/javascript/guides/vue/features/component-tracking/

Vueコンポーネントの追跡
SentryのVue SDKは、Vueコンポーネントのパフォーマンスを監視する機能として、コンポーネントトラッキングを提供しています。この機能を有効にすると、コンポーネントのライフサイクルイベントと継続時間を表すスパンがトランザクションに表示されます。これにより、コンポーネントがどのように動作しているかを詳細に把握することができ、遅い初期化や頻繁な更新を特定するなど、アプリのパフォーマンスに影響を与える可能性のあることを行うことができます。

● Sentry で localhost を除外する

1. Log in to your Sentry account and select the project you want to configure.
2. Navigate to the "Settings" section and click on "Filters."
3. Create a new filter by clicking the "Create Filter" button.
4. In the "Filter Name" field, enter a descriptive name for your filter, such as "Exclude Localhost Errors."
5. In the "Filter Expression" field, enter the following expression to exclude errors from localhost:

● エラーを発生させて Sentry へ送信されていることをテストする

・エラーを発生させると 設定が正しく行われている場合 Sentry へ自動的に送信されます

throw new Error('TEST ERROR');

Sentryではこのように表示されます

・CaptureConsole を している場合は consoleへの 出力も送信されます

console.error("TEST CONSOLE ERROR")

・ 明示的にエラーを送信する場合は Error があるのであれば captureException を、そうでなければ captureMessage を使います。

import { Sentry, SentrySeverity } from 'react-native-sentry';
// Error を渡す時
Sentry.captureException(error, {
  tags: {
    // 補足情報
    scene: 'SomeScene', // e.g. どの画面か
    fooState: state.foo // e.g. 関連する State はどのような状態か
  }
}
// メッセージを渡す時
Sentry.captureMessage("MyCustomError", {
  level: "error",
});

Sentryではこのように表示されます

引用: https://medium.com/@tsugitta/sentry-on-react-native-1364c3fb407a

オプションで level など追加できます
https://docs.sentry.io/clients/javascript/usage/

  Sentry.captureMessage("my-error", {
    level: "info",
  });

● どのユーザーがエラーを起こしたかをSentryに記録する

KEY は任意の値です

Sentry.setContext("KEY", {
  id: 'xxxxx',
  userName: 'yyyyy',
});

引用: Sentry で始める快適エラートラッキング on React Native | by tsugitta | Medium

Next.jsでSentryにソースマップを送信する

https://qiita.com/tamonmon/items/d0f0bf26f85d2de18987

添付ファイル1
添付ファイル2
添付ファイル3
No.2279
02/21 11:41

edit

添付ファイル

ios like vue transition

https://codesandbox.io/s/vue-ios-like-transitions-n6v3d?file=/src/App.vue

https://freefrontend.com/css-page-transitions/

App.vue

  <main>
    <router-view v-slot="{ Component }">
      <transition name="next">
        <component :is="Component" />
      </transition>
    </router-view>
  </main>
<style>
* {
  box-sizing: border-box;
}

html,
body {
  height: 100%;
}

#app {
  display: grid;
  grid-template-rows: min-content;
  min-height: 100%;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica,
    Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #2c3e50;
}

nav {
  display: flex;
  align-items: center;
  justify-content: space-around;
  background-color: #125b7f;
  position: sticky;
  top: 0;
  z-index: 1;
}

a {
  color: white;
  text-decoration: none;
  text-transform: uppercase;
  font-weight: bold;
  padding: 1em 0;
  margin: 0 1em;
  border-bottom: 2px solid transparent;
}

a.router-link-exact-active {
  border-color: inherit;
}

main {
  min-height: 100%;
  display: grid;
  grid-template: "main";
  flex: 1;
  background-color: white;
  position: relative;
  z-index: 0;
  overflow-x: hidden;
}

main > * {
  grid-area: main; /* Transition: make sections overlap on same cell */
  background-color: white;
  position: relative;
}

main > :first-child {
  z-index: 1; /* Prevent flickering on first frame when transition classes not added yet */
}

/* Transitions */

.next-leave-to {
  animation: leaveToLeft 500ms both cubic-bezier(0.165, 0.84, 0.44, 1);
  z-index: 0;
}

.next-enter-to {
  animation: enterFromRight 500ms both cubic-bezier(0.165, 0.84, 0.44, 1);
  z-index: 1;
}

.prev-leave-to {
  animation: leaveToRight 500ms both cubic-bezier(0.165, 0.84, 0.44, 1);
  z-index: 1;
}

.prev-enter-to {
  animation: enterFromLeft 500ms both cubic-bezier(0.165, 0.84, 0.44, 1);
  z-index: 0;
}

@keyframes leaveToLeft {
  from {
    transform: translateX(0);
  }
  to {
    transform: translateX(-25%);
    filter: brightness(0.5);
  }
}

@keyframes enterFromLeft {
  from {
    transform: translateX(-25%);
    filter: brightness(0.5);
  }
  to {
    transform: translateX(0);
  }
}

@keyframes leaveToRight {
  from {
    transform: translateX(0);
  }
  to {
    transform: translateX(100%);
  }
}

@keyframes enterFromRight {
  from {
    transform: translateX(100%);
  }
  to {
    transform: translateX(0);
  }
}
</style>
No.2275
01/19 11:15

edit

vue3 で markdown を表示

● vue-markdown-render

<template>
  <div>
    <vue-markdown :source="src">
  </div>
</template>

<script lang="ts">
import VueMarkdown from 'vue-markdown-render'

export default defineComponent({
  name: 'MyComponent',
  components: {
    VueMarkdown
  },
  setup(props, ctx) {
    const src = ref('# header')

    return {
      src
    }
  }
})
</script>

https://www.npmjs.com/package/vue-markdown-render?activeTab=readme

No.2272
01/13 15:56

edit

Vue3 の Composition API での HTMLへのref

<template>
  <h1>typescript google map</h1>
  <div ref="mapRef" class="map"></div>
</template>
<script lang="ts" setup>
const mapRef = ref<HTMLElement>();

// このタイミングでは undefined になります
console.log( mapRef.value );


onMounted(() => {
  // このタイミングで <div class="map"></div> が入ります
  console.log( mapRef.value );
})
</script>
No.2271
01/11 17:17

edit

No.2270
01/11 09:08

edit

vue3での v-model , modelValue

カスタムコンポーネントにおけるv-modelは、modelValueプロパティを渡してupdate:modelValueイベントを発火させるのと等価です。
<Child v-model="text"/>
<!-- ↑ ↓  これらは全く同じです -->
<Child :modelValue="text" @update:modelValue="text = $event"/>

引用: https://tekrog.com/v-model-in-vue3/#Vue3-2

Child.vue はこのように記述します

Child.vue

<template>
  <input :value="modelValue" type="text" @input="onInputText" />
</template>

<script lang="ts" setup>
const props = defineProps<{modelValue: string}>()
const emits = defineEmits<{(e: 'update:modelValue', text: string): void}>()

const onInputText = (e: Event) => {
  const target = e.target as HTMLInputElement
  emits('update:modelValue', target.value)
}
</script>
TypeScript

No.2265
12/16 17:50

edit

vue3( vite )の .env の設定方法

● モード

https://ja.vitejs.dev/guide/env-and-mode.html#modes

デフォルトで、開発サーバ(dev コマンド)は development モードで動作し、
build コマンドは production モードで動作します。

明示的にモードを指定したい場合は以下のようにします(モードを staging にします)

vite build --mode staging

● 読み込まれる .env ファイル

.env                # 全ての場合に読み込まれる
.env.local          # 全ての場合に読み込まれる(git監視対象外)
.env.[mode]         # 指定されたモードでのみ読み込まれる
.env.[mode].local   # 指定されたモードでのみ読み込まれる(git監視対象外)

● 開発用 / 本番用 を分ける例

.env.development  (開発用)
.env.production  (本番用)
No.2263
12/16 10:59

edit

pollingのhook

● hooks

src/composables/usePolling.ts

import {ref} from "vue"

export default function usePolling() {
  let isPollingDisabled = ref<boolean>(false)

  async function startPolling(
    fn: () => void,
    intervalTimeMsec: number = 3000 ,
    waitingTimeMsec: number = 0 ,
  ) {

    if (waitingTimeMsec > 0){
      await new Promise(resolve => setTimeout(resolve, waitingTimeMsec))
    }

    // infinite loop
    for (; ;) {
      fn()
      await new Promise(resolve => setTimeout(resolve, intervalTimeMsec))

      if (isPollingDisabled.value) {
        break;
      }
    }
  }

  function stopPolling() {
    isPollingDisabled.value = true
  }

  return {
    startPolling,
    stopPolling
  }
}

● コンポーネント側

src/views/MyComponent.vue

<template>
    <h2>{{ message }}</h2>
</template>


<script lang="ts" setup>
import {onBeforeMount, onBeforeUnmount, ref} from 'vue'
import usePolling from "@/composables/usePolling"
import dayjs from "dayjs"

const {startPolling, stopPolling} = usePolling()
const message = ref('start')

const getMessageAsync = async () => {
  message.value = 'Ccc:' + dayjs().format('HH:mm:ss')
}

onBeforeMount(() => {
  getMessageAsync()
  startPolling(getMessageAsync, 1000)
})

onBeforeUnmount(() => {
  stopPolling()
})
</script>

引用 : 実践 Vue Composition API - 面白駆動人生

No.2260
12/14 21:22

edit

Vue3 element plus

● installation

npm install element-plus --save

main.ts

import ElementPlus from "element-plus";
import "element-plus/dist/index.css";
const app = createApp(App);
......
......
app.use(ElementPlus);
No.2257
12/09 10:29

edit

vue3 で 現在のパスを取得する。URLパラメーターを取得する。

● vue3 の vue-router で 現在のパスを取得する

<script setup lang="ts">
import {useRoute} from "vue-router";

const route = useRoute();
console.log(route.path);
</script>

● vue3 の vue-router で URLパラメーターを取得する

<script setup lang="ts">
import {useRoute} from "vue-router";

const route = useRoute();
console.log(route.query);
</script>
No.2254
02/20 16:31

edit

vue3 の Composition API

● Composition API

Option APIの書き方

import Vue from "vue"
export default Vue.extend({
})

  ↓

<script lang="ts">
import { defineComponent } from "@vue/composition-api"
 
export default defineComponent({
  setup() {
    const { data: posts } = await useFetch('/api/posts')
  }
})
</script>

  ↓ setup() の糖衣構文が <script setup> になります。次のように記述できます

( <script setup> の中では(現在まだ実験的な機能である Suspense と組み合わせて使用することで)トップレベルの await を使うことができます。その結果、コードは async setup() としてコンパイルされます。
https://bit.ly/3APAi9U

<script setup lang="ts">
const { data: posts } = await useFetch('/api/posts')
</script>

引用 : https://zenn.dev/coedo/articles/86bc31acb4ea47

また script setup はライフサイクルでいうところの、beforeCreate と created のライフサイクルで実行されます

https://v3.ja.vuejs.org/guide/composition-api-lifecycle-hooks.html

引用 : https://bit.ly/3RXRoJ7

Composition API での従来の data は ref, reactive に置き換わります

data → ref, reactive
dataはComposition APIでrefあるいはreactiveで表現される。
refはプリミティブな値を管理し、reactiveはオブジェクトや配列を管理する。
そのため、reactiveの方が今までの使い方に近い。
ただし、refにオブジェクトや配列を渡すと、内部でreactiveが呼ばれるため問題なく使える。

https://nansystem.com/nuxt-composition-api-v2-diff/

ref , reactive の使い分け

特に使い分けるベストプラクティスがあるわけではなさそう(どちらでもご自由に。)

● ref
・プリミティブ、オブジェクト両方で使用できる
・refで定義したリアクティブな変数の値にアクセスするには.valueを使用する必要がある
// ref の定義
const mydata = ref("スタッフ")

// ref の参照
console.log( mydata.value );

// ref の参照(テンプレート内では自動的に内部の値に浅くアンラップ(ref でラップされた値を取り出す)されます)
<template>
  <div>
    <span>{{ mydata }}</span>
  </div>
</template>


// ref の更新
mydata.value = '社長'

● reactive
・プリミティブ値は受け取れない
・リアクティビティの消失に注意(refも同じだと思われるが。。。要検証)

https://kobatech-blog.com/vue-composition-api-ref-reactive/

● Composition API での props

https://vuejs.org/guide/typescript/composition-api.html#typing-component-props

<script setup lang="ts">
type Props = {
  testName: string;
};

// props を受け取る(デフォルト値なし)
const props = defineProps<Props>();

// props を受け取る(デフォルト値あり)
const props = withDefaults(defineProps<Props>(), {
  testName: "hogehoge",
});
</script>
添付ファイル1
No.2253
01/27 10:52

edit

添付ファイル

vue3 で /assets/ フォルダ内の .css や 画像を表示させる

● vue3 で /assets/ .css を読み込む

main.ts に 以下の1行を加えるとCSSを読み込むことができます。

import './assets/my.css'

● vue3 で /assets/ フォルダ内の 画像を表示させる

https://ja.vitejs.dev/guide/assets.html

No.2252
01/05 13:08

edit

Vue3の <script setup> 構文 で await する

● Vue3 の <script setup> 構文直下 で await できません

<script setup> 構文 では await で非同期関数を待つことはできません。 以下のようにして非同期関数を待つようにします

元のコード(エラーで動作しない)

<template>
  <div>
    <h1>MySetupComponent</h1>
    <div>{{ result }}</div>
  </div>
</template>

<script lang="ts" setup>
// 非同期関数
const getMessageAsync = async () => {
  await new Promise((resolve) => setTimeout(resolve, 1000))
  return '1秒たちました!!!'
}

// result に取得した値をセットする
const result = ref('')
const message = await getMessageAsync()  // ### エラーで動作しない
result.value = message
</script>

● A. async なアロー関数を使う

<script lang="ts" setup>
import { ref } from 'vue'

// 非同期関数
const getMessageAsync = async () => {
  await new Promise((resolve) => setTimeout(resolve, 1000))
  return '1秒たちました!!!'
}

// result に取得した値をセットする
const result = ref('')
const getResultAsync = async () => {
  const message = await getMessageAsync()
  result.value = message
}
getResultAsync()
</script>

● B. async な即時関数を使う

<script lang="ts" setup>
import { ref } from 'vue'

// 非同期関数
const getMessageAsync = async () => {
  await new Promise((resolve) => setTimeout(resolve, 1000))
  return '1秒たちました!!!'
}

// result に取得した値をセットする
const result = ref('')

// 即時関数
;(async function () {
  const message = await getMessageAsync()
  result.value = message
})()
</script>

● C. setup なしの <script>に書き換える

<script lang="ts">
import { defineComponent, onMounted, ref } from 'vue'

// 非同期関数
const getMessageAsync = async () => {
  await new Promise((resolve) => setTimeout(resolve, 1000))
  return '1秒たちました!!!'
}

export default defineComponent({
  setup() {
    // result に取得した値をセットする
    const result = ref('')
    onMounted(async () => {
      const message = await getMessageAsync()
      result.value = message
    })

    return {
      result
    }
  }
})
</script>
No.2249
11/29 11:41

edit

Vue3のステート管理 Pinia まとめ

● 1. Piniaのデータ保存場所の作成

/stores/todos.ts を以下の内容で作成します。
これは次のような機能を持ったストアになります

・変数todos                 にTODOリストを保存
・todoOrderedDesc()     でID大きい順に並べ替えてリストを返す
・addTodo()                でリストの最後にメンバーを追加する
・changeName()         でidで指定したメンバの名前を変更する

記述方式はOptions APIライク(Vuexに似た昔ながらの書き方)でも、Composition APIライク(Vue3の書き方)でも
どちらでも記述することができます。 ここでは Composition API ライクで記述してみます。

stores/todos.ts

import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
import _ from 'lodash'

interface Todo {
  id: number
  name: string
}

export const useTodoStore = defineStore('todo', () => {
  const todos = ref<Todo[]>([
    {
      id: 1,
      name: 'TODOその1'
    },
    {
      id: 2,
      name: 'TODOその2'
    }
  ])

  const todoOrderedDesc = computed(() => {
    return _.sortBy(todos.value, 'id').reverse()
  })

  function addTodo(newTodo: Todo) {
    todos.value.push(newTodo)
  }

  function changeName(id: number, name: string) {
    todos.value = todos.value.map((v) => {
      return v.id === id
        ? {
            id: id,
            name: name
          }
        : v
    })
  }

  return { todos, todoOrderedDesc, addTodo, changeName }
})

● 2. Piniaストアのデータの読み取り

import { storeToRefs } from 'pinia'
import { useTodoStore } from '../stores/todos'

const todoStore = useTodoStore()
const { todos } = storeToRefs(todoStore)

storeToRefs で todos を ref にすることでリアクティブに扱うことができます。

<template>
  <ul>
    <li v-for="todo in todos" :key="todo.id">
      <div>{{ todo.id }} : {{ todo.name }}</div>
    </li>
  </ul>
</template>

● 3. Piniaストアの値の更新

/stores/todos.ts に定義した更新メソッドを呼び出して値を更新します

  todoStore.changeName(1, 'TODOその1 ● 変更')

(注意)直接値を更新することもできてしまいます https://github.com/vuejs/pinia/issues/58

  todos.value = []

● 4. Piniaストア値のreadonly

1. 完全な readonly

const { todos } = storeToRefs(todoStore)

  ↓ 次のように修正して readonly にします

const { todos: todosMutable } = storeToRefs(todoStore)
const todos = readonly(todosMutable)

これでこのコンポーネント内では「値の変更やデータの追加、削除などができない事が担保された状態」で扱うことができます。
/stores/todos.ts で定義した更新メソッドもこのコンポーネント内では実行することができなくなります

2. Piniaストア値をコンポーネントで直接編集させないようにする readonly

/stores/todos.ts を以下のように修正します

  return { todos, todoOrderedDesc, addTodo, changeName }

  ↓

  return { todos: readonly(todos), todoOrderedDesc, addTodo, changeName }

これで

// これは以下のエラーによって実行できなくなります
//  [Vue warn] Set operation on key "value" failed: target is readonly.

  todos.value = []

この方法だと更新メソッドは問題なく使用することができます。

● Pinia ストアの値の監視( $subscribe , $onAction , watch)

・$subscribe を使ってストアの値を監視する

https://pinia.vuejs.org/core-concepts/state.html#subscribing-to-the-state

Vuexのsubscribeメソッドと同様に、ストアの$subscribe()メソッドを通じて、状態とその変化を監視することができます。通常の watch() よりも $subscribe() を使う利点は、サブスクリプションがパッチの後に一度だけ起動することです(例:上記の関数版を使用した場合)。

例:userStore の 値を監視してアイコンを変更する

userStore.$subscribe((mutation, state) => {
  changeIcon(state.user.iconURL)
});

・$onAction を使ってストアの値を監視する

https://pinia.vuejs.org/core-concepts/actions.html#subscribing-to-actions

store.onAction()でアクションとその結果を観察することが可能です。これに渡されたコールバックは、アクション自体の前に実行されます。 after handle promisesは、アクションが解決した後に関数を実行することができます。
onErrorでは、アクションがスローまたはリジェクトされた場合に関数を実行することができます。これらは、Vueのドキュメントにあるこのヒントと同様に、実行時にエラーを追跡するのに便利です

要約すると

・$onAction は全てのアクション実行前に呼ばれます。(何かのアクション実行前に毎回呼ばれます)
・特定のアクションにのみ紐付けたい場合は、引数の name からアクション名で判断させます。
・特定のアクションの実行後に関数を実行したい場合は after() を利用します

例:userStore の deleteUser() 実行完了後に 関数deleteAllPosts() を実行させたい場合

const unsubscribe = userStore.$onAction( ({ name: actionName, store, after }) => {
    if (actionName === "deleteUser") {
      after(() => {
        deleteAllPosts();
      });
    }
  }
);

・コンポーネント内の watch を使ってストアの値を監視する

コンポーネント内に 以下のように記述します

watch(
  () => myStore.value,
  (value) => {
    console.log("===== changed ! =====");
    console.log(value);
  }
);

watchの 第一引数は myStore.value ではなくて () => myStore.value と記述します

● Piniaステートの永続化

pinia-plugin-persistのインストール

 npm install pinia-plugin-persist

main.ts

import { createPinia } from "pinia";
import piniaPersist from "pinia-plugin-persist";

const pinia = createPinia();
pinia.use(piniaPersist); // ← 追加
app.use(pinia);

store/xxxx.ts

  persist: {
    enabled: true
  }

https://seb-l.github.io/pinia-plugin-persist/

No.2248
03/02 17:08

edit

Vue3 の ref , reactive 比較

● ref

● 1. ref まとめ

1. アクセス方法
・スクリプト内からアクセスする場合は .value でアクセスする(Reactivity Transform を使用すると .value なしでアクセスできる)
・テンプレート内からアクセスする場合はそのままアクセスできる(階層が深いと .value か?)

2. 値の直接更新 → OK 。 ref はリアクティブを保ったまま更新することができる

3. 値の再代入 → OK 。refはリアクティブを保ったまま再代入することができる

ref のサンプル

<template>
  <ul>
    <li v-for="todo in todos" :key="todo.id">
      <div>{{ todo.id }} : {{ todo.name }}</div>
    </li>
  </ul>
</template>

<script lang="ts" setup>
import { ref } from 'vue'

interface Todo {
  id: number
  name: string
}

const todos = ref<Todo[]>([
  {
    id: 1,
    name: 'TODOその1'
  },
  {
    id: 2,
    name: 'TODOその2'
  }
])

// OK
setTimeout(() => {
  todos.value[0].name = 'TODOその1 ● 変更'
}, 1000)

// OK
setTimeout(() => {
  todos.value = [
    ...todos.value,
    {
      id: 3,
      name: 'TODOその3 ● 追加'
    }
  ]
}, 2000)
</script>

Vue.js は Reactivity Transform でさらに進化する

● 2. reactive まとめ

1. アクセス方法
・スクリプト、テンプレート内からアクセスする場合でもそのままアクセスできる

2. 値の直接更新 → OK 。 reactive はリアクティブを保ったまま更新することができる

3. 値の再代入 → NG × 。const で定義した場合は再代入NG

reactive のサンプル

<template>
  <ul>
    <li v-for="todo in todos" :key="todo.id">
      <div>{{ todo.id }} : {{ todo.name }}</div>
    </li>
  </ul>
</template>

<script lang="ts" setup>
import { reactive } from 'vue'

interface Todo {
  id: number
  name: string
}

const todos = reactive<Todo[]>([
  {
    id: 1,
    name: 'TODOその1'
  },
  {
    id: 2,
    name: 'TODOその2'
  }
])

console.log('### todos.value')
console.log(todos)

// OK
setTimeout(() => {
  todos[0].name = 'TODOその1 ● 変更しました'
}, 1000)

// NG × (constへの再代入のためコンパイルエラー)
setTimeout(() => {
  todos = [
    ...todos,
    {
      id: 3,
      name: 'TODOその3 ● 追加'
    }
  ]
}, 2000)

setTimeout(() => {
  todos.push({
    id: 4,
    name: 'TODOその4 ● push'
  })
}, 3000)
</script>
No.2247
11/29 09:33

edit

vue3の新規アプリ(プロジェクト)を create-vue コマンドで作成する。scssを使用する

● サンプルアプリ vue3-todo-ts-app を作成する

あらかじめ作成したいプロジェクトの親ディレクトリに移動しておきます

vue3プロジェクトの新規作成コマンド

npm init vue@3

(Project nameで指定したディレクトリが自動的に作成されます)

✔ Project name: … vue3-todo-ts-app
✔ Add TypeScript? … No / Yes
✔ Add JSX Support? … No / Yes
✔ Add Vue Router for Single Page Application development? … No / Yes
✔ Add Pinia for state management? … No / Yes
✔ Add Vitest for Unit Testing? … No / Yes
✔ Add an End-to-End Testing Solution? › Cypress
✔ Add ESLint for code quality? … No / Yes
✔ Add Prettier for code formatting? … No / Yes

アプリの起動

cd vue3-todo-ts-app
npm install
npm run lint
npm run dev

ESLint と Prettier を Yes でインストールした場合は、次のコマンドでソース整形が実行できます

npm run lint

● vue3の単体テストvitestを実行する

npm run test:unit

テストを検索するトップディレクトリを / にする場合は以下のコマンドでもokです。

npx vitest --environment jsdom --root ./

デフォルトでは、.env が環境変数ファイルとして読み込まれます。(つまり production )

.env.development を 読み込ませながら、テストを行いたい場合は、次のようにします。

npx vitest --environment jsdom --root ./ --mode development

Nuxt を使わない Vue 3 だけで各 Vue 系 API や自作コンポーネントを自動インポートする | mirumi.tech

● Vue3で scss を使用する

npm i -D sass

main.ts

import "./assets/main.scss"; // 拡張子を .scss に変更する

main.cssmain.scss に 名称変更します

試しに、ネストしたプロパティを記述してみます

#app {
  max-width: 1280px;
  margin: 0 auto;
  padding: 2rem;
  font-weight: normal;
  font-size: 10px;
  * {
    color: red;
    font-weight: 30px;
  }
}

これでアプリを実行して表示される文字が赤くなっていれば成功です。

No.2245
03/31 13:29

edit

vue3 + yup + vee-validate4 のフォームサンプル

npm i vee-validate@next --save
npm i yup

● vee-validate を使ってみる

<script lang="ts" setup>
import { Form, Field, ErrorMessage } from "vee-validate";
import * as yup from "yup";

const schema = yup.object({
  password: yup.string().required().min(4),
  sortNo: yup.number(),
});

function getSubmitFn<Schema extends yup.ObjectSchema<Record<string, any>>>(
  _: Schema,
  callback: (values: yup.InferType<Schema>) => void
) {
  return (values: Record<string, any>) => {
    return callback(values);
  };
}

const handleSubmit = getSubmitFn(schema, (value) => {
  console.log("● value");
  console.log(value);
});
</script>

<template>
  <h1>Samplepageelement</h1>
  <Form @submit="handleSubmit" v-slot="{ values }" :validation-schema="schema">
    <!-- <Form @submit="handleSubmit"> -->
    <div>
      <Field name="password" type="password" />
      <ErrorMessage name="password" style="color: #c45656" />
    </div>

    <div>
      <Field
        name="sortNo"
        type="number"
        @update:modelValue="values.sortNo = Number(values.sortNo)"
      />
      <ErrorMessage name="sortNo" style="color: #c45656" />
    </div>

    <button>送信</button>
  </Form>
</template>

結果

● value
{password: 'asdfasdfads', sortNo: 1234}

number型を期待しているsortNoがそのままだとstringで返ってきてしまうので、@update:modelValue で変換しています。

https://bit.ly/3uRxmpM. https://vee-validate.logaretm.com/v4/guide/components/validation.

● handleSubmit に 型つける

const handleSubmit = (values:any) => {
  console.log( '● value' );
  console.log( value );
};

 ↓ 以下のように書き換えます

function getSubmitFn<Schema extends yup.ObjectSchema<Record<string, any>>>(
  _: Schema,
  callback: (values: yup.InferType<Schema>) => void
) {
  return (values: Record<string, any>) => {
    return callback(values);
  };
}

const handleSubmit = getSubmitFn(schema, (value) => {
  console.log("● value");
  console.log(value);
});

引用 : https://github.com/logaretm/vee-validate/issues/3521

● vee-validate の useFormでリアクティブに値を取得する

<template>
  <div>
    <div>
      <Field name="age" as="input" type="number" />
      <div>{{ errors.age }}</div>
    </div>

    <div>
      <Field name="name" as="input" />
      <div>{{ errors.name }}</div>
    </div>

    <div>
      <Field name="password" as="input" type="password" />
      <div>{{ errors.password }}</div>
    </div>

    <pre>Is form dirty: {{ isDirty }}</pre>

    <!-- print form values -->
    <pre>{{ values }}</pre>
  </div>
</template>

<script lang="ts" setup>
import { defineComponent, computed } from 'vue'
import { Field, useForm } from 'vee-validate'
import * as yup from 'yup'

interface UserFormValues {
  age: number
  name: string
  password: string
}

const schema = yup.object().shape({
  age: yup.number().required(),
  name: yup.string().required(),
  password: yup.string().required().min(8)
})
const { meta, values, errors } = useForm<UserFormValues>({
  validationSchema: schema
})

const isDirty = computed(() => meta.value.dirty)
</script>

● バリデーションの実行タイミングを変更する

// インポートした「configure」を使用してトリガするイベントを変更
configure({
  validateOnBlur: true, // blurイベントで検証をトリガーする必要がある場合、デフォルトはtrue
  validateOnChange: true, // changeイベントで検証をトリガーする必要がある場合、デフォルトはtrue
  validateOnInput: true, // inputイベントで検証をトリガーする必要がある場合、デフォルトはfalse
  validateOnModelUpdate: true, // update:modelValue(v-model)イベントで検証をトリガーする必要がある場合、デフォルトはtrue
});

引用 : https://bit.ly/3iT9bEo

● 手動でエラーを設定する

https://vee-validate.logaretm.com/v4/guide/composition-api/handling-forms#initial-errors

● UIライブラリとともに使用する

<Field name="password" type="text" />
<ErrorMessage name="password" />

  ↓

<Field name="password" v-slot="{ value, field, errorMessage }">
  <el-form-item :error="errorMessage">
    <el-input
      v-bind="field"
      :validate-event="false"
      :model-value="value"
      type="password"
      show-password
    />
  </el-form-item>
</Field>
No.2239
12/22 15:42

edit

Vue-Router まとめ

● Vue-Router まとめ

ルーターの設定を記述するファイル router/index.ts

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: "/profile",
      name: "profile",
      component: ProfileView,
    },
    // リダイレクトさせる場合
    {
      path: "/",
      redirect: "/mypage",
    },
    // 特定のパラメーター (例:isGuest) を渡す 場合
    {
      path: "/login",
      name: "Login",
      component: LoginView,
      meta: { isGuest: true },
    },
  ],
});

const aughGuard = () => {
  if (認証チェック){
    next();
    return;
  }

  next({ path: "/login" });
}

// ルーティング前に認証チェックを実行
router.beforeEach((to, from, next) => {
  authGuard(to, next);
});

● 画面遷移の方法(コンポーネント内のテンプレートからリンクをする)

<template>
    <router-link to="/about">Go to About</router-link>
</template>

● 画面遷移の方法(templateに直接記述)

<button class="btn" @click="$router.push('/login')">戻る</button>

● 画面遷移の方法(メソッドを使用)

nameで指定する場合

import { useRouter } from "vue-router";

const router = useRouter();

await router.push({ name: "Login" });

pathで指定する場合

import { useRouter } from "vue-router";

const router = useRouter();

await router.push("/login");

● Vue-Router で画面遷移した時に、親画面を消さずに、子画面の一部のみを入れ替える

/ の下に /child1 /child2 を子画面として追加する

src/router/index.ts

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: "/",
      name: "home",
      component: HomeView,
      children: [
        {
          path: "/child1",
          components: {
            default: Child1View,
          },
        },
        {
          path: "/child2",
          components: {
            default: Child2View,
          },
        },
      ],
    }
  ],
});

親の src/views/HomeView.vue に 子画面用の router-view を追加します

src/views/HomeView.vue

<template>
  <div class="container">
    <h1>Home</h1>
    <router-view></router-view>
  </div>
</template>

これで、 /child1 , /child2 を表示しているときは HomeView.vueはアンマウントされません。

● this.$router , this.$route にアクセスする

引用: https://qiita.com/azukiazusa/items/9f467fdea7298baf3476

useRouter()・useRoute()に置き換える
ルーターオブジェクトを得るためには、vue-rouerからuseRouterまたはuseRoute関数をインポートします。それぞれの関数は以下のように対応しています。
Options API Composition API
this.$router useRouter
this.$route useRoute

● ページがリロードされたかどうかを検知する

src/router/index.ts

router.beforeEach((to, from, next) => {
  // from.name が取得できる時はパスを保存。できないときは "" を保存
  sessionStorage.setItem("referrer", from.name ? from.path : "");
  authenticate(to, next);
});

各コンポーネント

function isPageReload() {
  return sessionStorage.getItem("referrer") === "";
}

if isPageReload(){
    alert("reloadです");
}

とすれば検知できます。

● htmlヘッダのタイトルを vue-router で設定した値で変更する

https://github.com/vinicius73/vue-page-title#vue-router-integration

● 画面遷移したときのスクロール位置を設定する

https://router.vuejs.org/guide/advanced/scroll-behavior.html

No.2176
02/10 11:06

edit