- 投稿日:2021-04-27T22:56:03+09:00
Rails APIモード + devise_token_auth + Vue.js 3 で認証機能付きのSPAを作る(ファイル投稿編)
はじめに 本記事はAPIをRailsのAPIモードで開発し、フロント側をVue.js 3で開発して、認証基盤にdevise_token_authを用いてトークンベースの認証機能付きのSPAを作るチュートリアルのファイル投稿編の記事になります。 前回: Rails APIモード + devise_token_auth + Vue.js 3 で認証機能付きのSPAを作る(Navigation Guard編) ActiveStorageの導入 今回はファイルの保存をRails5.2系以降から使えるようになったActiveStorageを用いて実現してみようと思います。 ActiveStorageとは? Railsガイドの説明を引用します。 Active StorageとはAmazon S3、Google Cloud Storage、Microsoft Azure Storageなどの クラウドストレージサービスへのファイルのアップロードや、ファイルをActive Recordオブジェクトにアタッチする機能を提供します。 出典: https://railsguides.jp/active_storage_overview.html S3やGCSに容易に保存を可能にするGemも提供されています。 今回は本番環境用のストレージに保存する手順については説明を割愛させて頂きます。 導入コマンドの実行 ActiveStorageを導入するには、以下のコマンドを実行する必要があります。 $ bundle exec rails active_storage:install $ bundle exec rails db:migrate active_storage:install を実行すると、ファイルを保存するためのテーブルを定義するmigrationファイルが作成されます。 テーブル構成は以下のように定義されます。(ReleaseNotesテーブルはActiveStorageと関連付けを行ったテーブルです。今回の実装には必要ありません) ファイル保存のattrを定義 ActiveStorageを用いてファイルを保存するには、has_one_attachedメソッドまたはhas_many_attachedメソッドを用いる必要があります。 has_one_attachedメソッドはファイルを1つだけ保存を可能にするメソッドで、 has_many_attachedメソッドはファイルを複数保存できるようにするメソッドです。 今回はhas_one_attachedメソッドの方を用いて実装を行います。 Postモデルに以下の記述を追加してください。 class Post < ApplicationRecord belongs_to :user has_one_attached :icatch # 追加 end これで、icatch属性がPostインスタンスに定義され、読み取りと書き込みが可能になります。 投稿機能APIの修正 ファイルを保存するためにcontrollerを修正する必要がありますので、posts_controller.rbに手を入れます。 controller内でファイルを保存できるように修正 以下のようにcontrollerを修正をしてください。 # app/controllers/posts_controller.rb # N+1回避のために以下の修正を加える def index @posts = Post.eager_load(:user).with_attached_icatch end # 以下を修正 def post_params params.permit(:title, :body, :icatch).merge(user: current_user) end permitメソッドに has_one_attachedで定義した :icatch を指定することで、createアクション内でPostインスタンスを保存するときにファイルも一緒に保存できるようになります。 viewで画像のurlを返却できるように修正 ファイルを保存しただけではフロントにデータを返すことができません。 今回は画像データが保存されると仮定して、画像のURLを返すように実装します。 class Post < ApplicationRecord # 以下のヘルパーを追加 include Rails.application.routes.url_helpers belongs_to :user has_one_attached :icatch # 以下メソッドの追加 def icatch_url return nil unless icatch.attached? url_for(icatch) end end このままだと、icatch_urlメソッドの実行時に以下の例外が発生しますので、 config/environments/development.rbでdefault_url_optionsを設定します。 # 例外 Missing host to link to! Please provide the :host parameter # config/environments/development.rb # Set url_helpers for url_for methods Rails.application.routes.default_url_options[:host] = 'localhost' Rails.application.routes.default_url_options[:port] = 3000 この状態でicatch_urlメソッドの返り値をコンソールで確認してみます。 まずは適当な画像を添付したレコードを生成してください。 $ mkdir db/seed_data $ mv 適当な画像 db/seed_data $ rails c $ post = Post.first $ post.icatch.attach(io: File.open(Rails.root + 'db/seed_data/適当な画像'), filename: '適当な画像') # 後ほど使うのでsaveします $ post.save! $ Post.first.icatch_url => "http://localhost:3000/rails/active_storage/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBDZz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--59a7d22bb43b6ee92bcb55321a0b1a2b1c622cdd/適当な画像" 上記のような返り値が返ればOKです。 フロントにicatch_urlを返却するために、app/views/_post.json.jbuilderに以下を追加してください。 json.extract! post, :id, :title, :body, :created_at, :icatch_url # 追加 保存できる拡張子の制限を設定 今のままだとどんな拡張子のファイルでも保存ができてしまうので、保存できる拡張子を制限できるGemを導入します。 # In Your Gemfile gem 'activestorage-validator' $ bundle install ActiveStorageにはvalidationの機能がデフォルトで備わっていないため、上記のGemを用いています。 app/models/post.rbに以下を追加してください。 validates :icatch, blob: { content_type: ['image/png', 'image/jpg', 'image/jpeg',]} content_typeにMIME-TYPEを指定することでバリデーションが可能になります。 投稿画面コンポーネントの修正 ここからVue.js側の修正を行なっていきます。 formDataオブジェクトを用いた画像投稿 画像の投稿を行うためにJavaScriptのFormDataオブジェクトを用います。 src/views/NewPost.vueを以下のように編集してください。 <template> <div class="flex items-center h-screen w-full bg-teal-lighter"> <div class="w-full bg-white rounded shadow-lg p-8 m-4"> <h1 class="block w-full text-center text-grey-darkest mb-6">New Post</h1> <div class="flex flex-col mb-4"> <label class="mb-2 font-bold text-lg text-grey-darkest" for="title">Title</label> <input v-model='title' class="border py-2 px-3 text-grey-darkest" type="text" name="first_name" id="first_name"> </div> <div class="flex flex-col mb-4"> <label class="mb-2 font-bold text-lg text-grey-darkest" for="body">Body</label> <textarea v-model='body' class="border py-2 px-3 text-grey-darkest" name="body" id="body"></textarea> </div> <div class="flex flex-col mb-4"> <label class="mb-2 font-bold text-lg text-grey-darkest" for="icatch">iCatch</label> <input @change="setIcatch($event)" class="border py-2 px-3 text-grey-darkest" type="file"> </div> <button @click='handleCreatePost()' class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 uppercase text-lg mx-auto rounded" type="submit">Create Post</button> </div> </div> </template> <script lang="ts"> import { createPost } from '@/api/post' import router from '@/router' import { defineComponent, reactive, toRefs } from 'vue' export default defineComponent({ name: 'NewPost', setup () { const postData = reactive({ title: '', body: '' }) const formData = new FormData() const setIcatch = (e: Event) => { e.preventDefault() if (e.target instanceof HTMLInputElement && e.target.files) { formData.append('icatch', e.target.files[0]) } } return { ...toRefs(postData), setIcatch, handleCreatePost: async () => { formData.append('title', postData.title) formData.append('body', postData.body) await createPost(formData) .then(() => { router.push('/posts') }) } } } }) </script> ファイル選択イベントがあった場合にsetIcatch関数を呼び出すようにして、formDataオブジェクトにアップロードされた画像を入れるようにしています。 <input @change="setIcatch($event)" class="border py-2 px-3 text-grey-darkest" type="file"> このままではcreatePost関数を叩くときに例外が起きてしまいますので、修正します。 投稿APIに渡す引数の修正 src/api/post.tsを以下のように修正します。 import Client from '@/api/client' import { getAuthDataFromStorage, getAuthDataFromStorageWithFormData } from '@/utils/auth-data' export const getPosts = async () => { return await Client.get('/posts', { headers: getAuthDataFromStorage() }) .then((response) => { return response.data }) .catch((err) => { console.log(err) }) } export const getPost = async (id: string) => { return await Client.get(`/posts/${id}`, { headers: getAuthDataFromStorage }) .then((response) => { return response.data }) } export const createPost = async (formData: any) => { return await Client.post( '/posts', formData, { headers: getAuthDataFromStorageWithFormData() } ) .then((response) => { return response.data }) } formData型の値を扱うため、postForRequest型は削除していきます。 // src/types/post.ts export type Post = { title: string; body: string; userName: string; createdAt: string; icatchUrl: string; // 追加 } // 以下を削除 export type PostForRequest = Pick<Post, 'title' | 'body'> // src/api/post.ts import Client from '@/api/client' // PostForRequestを削除 import { Post } from '@/types/post' // PostForRequest型からFormData型に修正 export const createPost = async (formData: FormData) => { return await Client.post( '/posts', formData, { headers: getAuthDataFromStorage() } ) .then((res: AxiosResponse<Post>) => { return res.data }) } multipart/form-data形式でデータ送信を行うために、src/utils/auth-data.tsに以下の関数を追加します。 export const getAuthDataFromStorageWithFormData = (): AuthHeaders => { return { 'access-token': localStorage.getItem('access-token'), client: localStorage.getItem('client'), expiry: localStorage.getItem('expiry'), uid: localStorage.getItem('uid'), 'Content-Type': 'multipart/form-data' } } getAuthDataFromStorageとの違いは、Content-Typeに指定する項目がapplication/jsonではなく、multipart/form-dataとしている点です。 画像を投稿する場合には getAuthDataFromStorageWithFormData 関数の方を用いるようにします。 投稿一覧コンポーネントの修正 最後に画像を表示するように修正を行います。 // src/components/AppPost.vue <template> <div class="rounded overflow-hidden shadow-lg pt-8 mr-8"> <img :src="post.icatch_url"> <div class="font-bold text-xl mb-2">{{post.title}}</div> <p class="text-grey-darker text-base"> {{ post.body }} </p> </div> </template> <script lang="ts"> import { Post } from '@/types/post' import { defineComponent, PropType } from 'vue' export default defineComponent({ name: 'AppPost', props: { post: { type: Object as PropType<Post>, required: true } } }) </script> <style scoped> </style> この状態で一通り動作確認を行なってみます。 ログインした後、 http://localhost:8080/posts に アクセスして、先ほどsave!した画像が表示されていれば問題なく動作しているかと思います。 投稿機能も試してみましょう。 画像のリサイズやレイアウト等が未完成ですが、最低限の画像アップロード & 表示機能ができたかと思います。 まとめ 次回はRspecを実装する予定です。
- 投稿日:2021-04-27T20:50:08+09:00
Vue2 コンポーネントまとめ ①
はじめに Vue.jsを勉強していてコンポーネントって大事そうなんだけど、いまいちよくわかっていないので 手を動かしながら学んだことの備忘録 Vue3もリリースされていますが、今回はVue2についてです。 Vue2 コンポーネントまとめ② Vue.jsコンポーネントとは // button-counter と呼ばれる新しいコンポーネントを定義します Vue.component('button-counter', { data: function () { return { count: 0 } }, template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>' }) コンポーネントは名前付きの再利用可能な Vue インスタンスです。この例の場合、です。このコンポーネントを new Vue で作成されたルート Vue インスタンス内でカスタム要素として使用することができます。 (Vue.js公式より引用) ふむふむ。再利用できるインスタンスかあ。Railsでいうと、ヘッダーとかフッター、サイドバー、フォームなどを_form.html.erbなどでrenderメソッドで呼び出したりしますがそんな感じってことですね。 これにより開発、メンテナンスの効率が格段に上がるとのことです。 コンポーネントの設計 コンポーネントの作り方 javascript Vue.component('コンポーネントの名前',{ template : 'HTMLに表示させたい内容' }) ちなみにVue3はこんな書き方になるようです。 Vue.createApp({ // 省略 }) .component('コンポーネントの名前', { template: 'HTMLに表示させたい内容' }) .mount('#app'); 今回は特に説明はしません。Vue3でやるとエラーになることだけご理解ください。 またコンポーネントの名前はケバブケース(hello-world)またはパスカルケース(HelloWorld)とする必要があります。 コンポーネントの例 Vue.component('hello-world', { template : '<h1>Hello World</h1>' }) これでhello-worldというコンポーネントができました。 これをVueインスタンス下にあるhtmlから直接コンポーネントを利用することができます。 コンポーネント作成時に登録した名前のタグを組み込みます。 今回はとid='app'として進めていきます。 See the Pen Vue.js vueコンポーネント練習1 by morioka (@rm5912) on CodePen. html <div id="app"> <hello-world></hello-world> </div> javascript Vue.component('hello-world',{ template : '<h1>Hello World</h1>' }) new Vue({ el: '#app' }) コンポーネントの定義はnew Vueよりも前に記述する必要があります。 new Vueのあとに定義した場合はエラーになり、 ページに何も表示されなくなります。 コンポーネントの再利用 コンポーネントの特性と言っていい、再利用。 hello-worldのタグを複数記述するだけで同じ内容を表示させることができます。 See the Pen Vue.js vueコンポーネント練習2 by morioka (@rm5912) on CodePen. グローバルコンポーネントとローカルコンポーネント コンポーネントは2種類あります。 グローバルコンポーネントとローカルコンポーネントです。 今まで説明してきたのはグローバルコンポーネントでVue.componentを使う場合はグローバルへの登録になり、別のコンポーネントで利用することができます。 ある特定のコンポーネントだけで利用したい場合はローカルで登録し、利用することができます。 ローカルへの登録はVueインスタンスにcomponentsプロパティを追加することで行います。 javascript var app = new Vue({ el: '#app', data: { }, components: { 'hello-world': { template: '<h1>Hello World</h1>' } } }) グローバルとローカルの違い See the Pen Vue.js vueコンポーネント練習3 by morioka (@rm5912) on CodePen. javascript Vue.component('hello-world',{ template : '<h1>Hello World</h1>' }) var app1 = new Vue({ el: '#app1' }) var app2 = new Vue({ el: '#app2', components: { 'hello-vue': { template: '<h1>Hello Vue.js</h1>' } } }) html <div id="app1"> <h2>App1</h2> <hello-world></hello-world> <hello_vue></hello_vue> </div> <div id="app2"> <h2>App2</h2> <hello-world></hello-world> <hello-vue></hello-vue> </div> 1つ目のapp1のVueインスタンスではapp2にローカル登録したhello-vueが利用できないため Hello Vue.jsが表示されていませんが、ローカル登録されたapp2には表示されています。 templateに複数のタグ Vue3では制限がなくなっているようですが、Vue2の場合複数のタグをtemplateに入れる場合には 注意が必要です。 どうやらコンポーネントテンプレートは1つのルート要素を持たなければならないという規則があるようです。 javascript Vue.component('hello-world',{ template: '<p>{{ message }}</p><p>Hello World</p>', data: function(){ return { message : 'Hello Vue' } } }) この場合2つ目のHello Worldが表示されていません。 See the Pen Vue.js vueコンポーネント練習4 by morioka (@rm5912) on CodePen. pタグが2つ挿入されているため、1つのルート要素になっていなく2つのルート要素になっているからです。 そのため、この2つのpタグを含む上位のタグをつける必要があります。(divなど) 上位タグをつけることで1つのルート要素になり、2つのpタグはdivの子要素となりHello Worldも表示されるようになります。 javascript Vue.component('hello-world',{ template: '<div><p>{{ message }}</p><p>Hello World</p></div>', data: function(){ return { message : 'Hello Vue' } } }) See the Pen Vue.js vueコンポーネント練習5 by morioka (@rm5912) on CodePen. コンポーネントでdataを使う コンポーネントでもdataプロパティは使えるのですが dataは関数にする必要があります。 javascript Vue.component('hello-world',{ template: '<h1>{{ message }}</h1>', data: function(){ return { message : 'Hello Vue' } } }) 複数のdataを取り扱う場合 javascript data: function(){ return { message : 'Hello Vue', message2 : 'Hello World' } } dataを関数にすることでdataを持つ複数のコンポーネントを使った場合に コンポーネントごとにdataの独立の値を保持することができます。 コンポーネントでmethodsを使う コンポーネントはdataプロパティだけでなくmethodsプロパティも使うことができます。 ボタンを押すとアラートが表示されます。 javascript Vue.component('hello-world',{ template: '<button v-on:click="alert">アラート</button>' , methods: { alert: function(event) { alert("アラートが表示されました") } } }) var app1 = new Vue({ el: '#app1' }) See the Pen Vue.js vueコンポーネント練習6 by morioka (@rm5912) on CodePen. コンポーネントを複数作成した場合の独立性 コンポーネントは再利用できることを学びました。 コンポーネントを複数作成した場合はそれぞれが独立したものとして作成されます。 See the Pen Vue.js vueコンポーネント練習7 by morioka (@rm5912) on CodePen. Upボタンでカウントが増えてDownボタンでカウントが減りますが、3つのコンポーネントは独立した状態で動作しているのがわかります。 template内のhtmlを改行する templateに記述するhtmlは当然長くなることが多くなります。その場合シングルクオーテーションではエラーになり表示されません。 javascript Vue.component('hello-world',{ template: '<p>ロレム・イプサムの嘆き、トマト大好き学部のエリット、しかし時と活力、そのような労働と悲しみ、ブラインド行うにはいくつかの重要な事柄に座ります。 長年にわたり、私は学区と長寿であれば、そのような刺激の取り組み、彼女のうち、運動の利点を分注を邪魔されたする人が来ます。 クピダタットのつるの痛みになりたい宿題に、批判されてきたら痛み、マグナ逃亡しても結果の喜びを生成しません。先例クピダタットブラックは先例していない、つまり、彼らはあなたの悩みに責任がある人の、一般的な義務を捨て、魂を癒しています。</p>' }) var app = new Vue({ el: '#app' }) See the Pen Vue.js vueコンポーネント練習8 by morioka (@rm5912) on CodePen. 何も表示されていません。 templateで設定するhtmlが長い場合はバッククォートを使います。(``) バッククォートで囲むと改行を行っても表示されています。 See the Pen Vue.js vueコンポーネント練習9 by morioka (@rm5912) on CodePen. javascript Vue.component('hello-world',{ template: `<p>ロレム・イプサムの嘆き、トマト大好き学部のエリット、しかし時と活力、そのような労働と悲しみ、ブラインド行うにはいくつかの重要な事柄に座ります。 長年にわたり、私は学区と長寿であれば、そのような刺激の取り組み、彼女のうち、運動の利点を分注を邪魔されたする人が来ます。 クピダタットのつるの痛みになりたい宿題に、批判されてきたら痛み、マグナ逃亡しても結果の喜びを生成しません。先例クピダタットブラックは先例していない、つまり、彼らはあなたの悩みに責任がある人の、一般的な義務を捨て、魂を癒しています。</p>` }) var app = new Vue({ el: '#app' }) 終わりに コンポーネントについてまとめてみました。 まだまだありますがとりあえずはここら辺で①を終わりにします。 気づいたことありましたら教えていただけるとありがたいです。 ②に続きます。 参考サイト Vue.js公式サイト REFFECT vue.jsコンポーネントの基礎 Qiita Vue】単一ファイルコンポーネントの命名規則まとめ【ファイル名から記法まで】
- 投稿日:2021-04-27T20:50:06+09:00
Nuxt.js(Vue.js)にGoogle DFP(DoubleClick for Publishers)を導入する
SSRモードのNuxt.jsにGoogle DFPを導入しました。 SPAの事例もいくつかあるようですが、事例は多くはないですね。 DFPの業者さんから頂いたタグを貼ろうとしたら、思ったより結構苦戦したのでうまく行った結果を残しておきます。 裏側の技術を深く知れていないので、長々と語らすに結果だけ残します。 (この記事にたどり着いた多くのエンジニアの方々にとっては、結果だけわかればそれでいいと思いますので。) 業者さんに貼って欲しいと言われたタグ 以下の二つ。 よくある「headの中」と「bodyの中」というやつです。 <!-- <head>のすぐ下に設置 --> <script async src="https://securepubads.g.doubleclick.net/tag/js/gpt.js"></script> <script> window.googletag = window.googletag || {cmd: []}; googletag.cmd.push(function() { googletag.defineSlot('***', [[***, ***], [***, ***]], 'div-gpt-ad-***').addService(googletag.pubads()); googletag.pubads().enableSingleRequest(); googletag.pubads().collapseEmptyDivs(); googletag.enableServices(); }); </script> <!-- 該当のボディ内に設置 --> <div id='div-gpt-ad-***' style='margin:auto;text-align:center;'> <script> googletag.cmd.push(function() { googletag.display('div-gpt-ad-***'); }); </script> </div> 実際に貼ったタグ Vue.js向けに結構コード変えないといけないんですとね、、(意外としんどい作業) 初期ロードだとうまく行ったり、でも vue-routerの遷移後はうまくいかなかったり、などしんどい開発になりますよね。 結論 nuxt.config.tsに記載するのはこれだけ。後の部分はコンポーネントに持たせる。 nuxt.config.ts script: [ { src: 'https://securepubads.g.doubleclick.net/tag/js/gpt.js', async: true, }, ], 広告用のコンポーネント。 /components/atoms/ADfp.vue ADfp.vue <template> <div :id="adId" :style="style" /> </template> <script lang="ts"> import Vue, { PropType } from 'vue' const style = 'margin: auto; text-align: center;' interface Data { style: string adId: string } declare const window: any export default Vue.extend({ name: 'ADfp', data: (): Data => ({ adId: 'div-gpt-ad-***', style, }), beforeDestroy() { window.googletag.cmd.push(() => { window.googletag.destroySlots() }) }, mounted() { window.googletag = window.googletag || { cmd: [] } window.googletag.cmd.push(() => { window.googletag .defineSlot( '***', [ [***, ***], [***, ***], ], this.adId, ) .addService(window.googletag.pubads()) window.googletag.pubads().enableSingleRequest() window.googletag.pubads().collapseEmptyDivs() window.googletag.enableServices() window.googletag.display(this.adId) }) }, }) </script> あとはこのコンポーネントを広告設置箇所に配置します。 ファイルの中に定数がちりばめられており、保守性には欠けるので、必要に応じて定数ファイルを作成したり props を足したり computed で切り分けて書いておくと良いですね。 最後に Qiita向けに、本質的でない部分を隠したり定数を直書きする時、ちょっと寂しくなる。 良き広告ライフを。
- 投稿日:2021-04-27T19:15:19+09:00
vue-chartjsでグラフが表示されない問題
vue-chartjsを初めて使用した際につまずいた点をメモしておきます。 問題の背景 公式ガイドを参考に、Chart.jsとvue-chartjsのインストールからチャートコンポーネントの作成までを終え、 ファイルを実行したところグラフが上手く表示されなかった。 Chart.jsのバージョンを戻して解決 Chart.jsは最近大きなアップデートがあったが、vue-chartjsはそのアップデートにまだ対応出来ていない様子。(2021年4月27日時点) 公式ガイドにあるインストール方法ではChart.jsの最新版がインストールされてしまう為、 vue-chartjsを使用するには以下の様にバージョンを直接指定するなどして、Chart.jsを3.0.0より古いバージョンに戻す必要がある。 npm install chart.js@2.9.4
- 投稿日:2021-04-27T16:39:15+09:00
v-paginationにページURLのクエリパラメータを取得してページ更新後もページ数を指定して表示させる
<v-pagination v-model="page" circle :length="pageLength" :total-visible="7" @input="executePageFeed" ></v-pagination> data() { return { /** * ページャー */ page: 1, } } async mounted() { /** ページ数をセット */ await this.setPage() }, methods: { setPage() { this.page = Number(this.$route.query.page) || 1 } } 参考: v-pagination Nuxt.jsでURLののクエリパラメータを取得する方法 Nuxt.jsのライフサイクルを知っておく
- 投稿日:2021-04-27T09:52:31+09:00
Nuxtで作ったページをIEで見た時に別ページへ飛ばす(IE対応)
思い TLDR: IE11はレガシーってことをユーザーに気づかせてあげたほうが良いんじゃない? EdgeもChromium版になり世界平和が近づく中、未だ一般的なサポートはまだまだ継続中のIE11は「モダンではないWebブラウザ」として国内で未だ7%程度の利用率をほこり対応が必要なケースは多いです。 ただJavaScriptの対応状況がいけてなかったりでモダンWeb開発をする上では弊害になるため、思い切って「このブラウザーは対応していないよ!」って表示してあげるケースもあります。個人的にはこういうのどんどん進めてITリテラシーが低いユーザーにも「IEはもう古いんか……」って思ってもらうことも大切な気がします。7%のためにわざわざ対応することのコスト対効果を考えて、プロダクトごとに適切な判断をしていきましょう。 対応方法 1. staticにIE11でも見れる優しいページを作る static/browser-check.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>IE11は対応していません</title> </head> <body> IE11は対応していません。モダンなWebブラウザを使ってください。 <li> <ul>Chrome</ul> <ul>Firefox</ul> <ul>Safari</ul> <ul>Edge</ul> </li> </body> </html> CSSなんかも必要に応じてセットで置いておきましょう 2. pluginsにIEチェック用のプラグインを作成する plugins/browser-check.js const userAgent = window.navigator.userAgent // IEの場合、browser.htmlを開く if (userAgent.indexOf('Trident') !== -1) { window.location.href = 'browser.html' } UserAgentによるこの判別方法はエンジンを元に判定しているので、IE11以外のIEに反応するかはわかりません。 3. nuxt.config.jsからpluginsを読み込む nuxt.config.js // 前略 /* ** Plugins to load before mounting the App ** https://nuxtjs.org/guide/plugins */ plugins: ['~/plugins/ie-taiou.js'], // 後略 プラグインはリストの順番が読み込む順番になるので、必要に応じて順番を変えてあげてください。 以上で対応は完了です。 シンプルですがブラウザごとの個別の対応なんかももしあれば同じような方法でできると思います。 ただ当然画面に影響を与えるような処理は、コンポーネント側でやった方がいいのであまり出番はなさそう。