くらげになりたい。

くらげのようにふわふわ生きたい日曜プログラマなブログ。趣味の備忘録です。

Nuxt.jsではじめるときのやることリスト(SSRも国際化も自動デプロイも)

Nuxt.jsでプロジェクトをはじめるときにいつもやることが整理してみた。
モジュールが多いので、いろいろ設定が必要だけど、ヌケモレあったりするので。。

Nuxt.jsのバージョンは、2.5.1です。

いつもやることの概要

作成したいプロジェクトは以下な感じ

  • TypeScript/Sassを使う、UIはBuefy/Bulma
  • Firebase Hosting / Cloud Functionsを使ってSSR
  • 英語版/日本語版と国際化したいので、nuxt-i18nも入れる
  • SEOも気にするので、各種設定&各ページで個別にタイトルとかを設定
  • Google Analyticsã‚„sitemapなどももちろん
  • Firebaseへのデプロイは、CircleCIで自動化したい

最終的な例は、GitHubにおいています。

お品書き

やることは以下の14個。ながい。。おおい。。

  1. プロジェクト作成
  2. ディレクトリ構成の変更
  3. TypeScriptの導入
  4. Sassの導入
  5. env用の設定ファイルを追加
  6. サイトマップ生成の設定を追加
  7. 国際化のためにnuxt-i18nを追加
  8. SEO関連の設定
  9. Google Analyticsの設定を追加する
  10. Buefyカスタマイズ用のSCSSを追加
  11. Page Transition用のSCSSを追加
  12. UglifyJsPluginを追加して、console.logを自動削除
  13. Firebase Functionsの設定してSSRできるようにする
  14. CircleCIの設定

1. プロジェクト作成(コミット: e5bf50b)

まずは、プロジェクト作成。npx create-nuxt-appを使います

$ npx create-nuxt-app <プロジェクト名>
npx: 379個のパッケージを12.001秒でインストールしました。
> Generating Nuxt.js project in ...
? Project name <プロジェクト名>
? Project description Nuxt.js Template using TypeScript, Sass and SSR on Firebase Cloud Functions
? Use a custom server framework none
? Choose features to install Axios
? Use a custom UI framework buefy
? Use a custom test framework none
? Choose rendering mode Universal
? Author name Memory Lovers
? Choose a package manager npm

  To get started:

    cd <プロジェクト名>
    npm run dev

  To build & start for production:

    cd <プロジェクト名>
    npm run build
    npm start

作り終えると、このコミット(e5bf50b)な感じに

2. ディレクトリ構成の変更(コミット: 4fbab23)

あとでfunctionsのディレクトリを追加するので、
app用のディレクトリ作成して、移動する

#!/bin/bash
mkdir app
mv assets/ app/
mv components/ app/
mv layouts/ app/
mv middleware/ app/
mv pages/ app/
mv plugins/ app/
mv static/ app/
mv store/ app/

ディレクトリを変えたので、nuxt.config.jsも変更

  const config: NuxtConfiguration = {
   mode: 'universal',
+  srcDir: "app",

3. TypeScriptの導入(コミット: c1106e0)

Nuxt.jsv2.5でTypeScriptサポートが強化されたので、だいぶ楽に。。
@nuxt/typescriptを入れるだけでOKらしい。(TypeScript Support - Nuxt.js)

$ npm install --save -D @nuxt/typescript
# デコレータやvuexも使いたいので追加
$ npm install --save nuxt-property-decorator vuex-class

# tsconifig.jsonを用意すると、Nuxtが設定を自動生成してくれる
# なので、とりあえず、空ファイルを作成
$ echo "{}" > tsconfig.json

# 一回、ビルドして、tsconifig.jsonを生成
$ npm run build

3.1. tsconfig.jsonをディレクトリ構成に合わせる(コミット: 8473099)

appディレクトリ配下に変更しているので、自動生成したtsconfig.jsonも合わせる

  "paths": {
    "~/*": [
-     "./*"
+     "./app/*"
    ],
    "@/*": [
-     "./*"
+     "./app/*"
      ]
    },

3.2. nuxt.config.jsをnuxt.config.tsに変換(コミット: 8473099)

まずは、nuxt.config.jsをTypeScript化
長いので、差分はコミットを参照

3.3. 型定義の配置ディレクトリを追加(コミット: 90f3a1e)

mkdir app/types
touch app/types/index.d.ts

# Stateの型定義
touch app/types/state.d.ts

# Vueの型定義の拡張
touch app/types/vue.d.ts

3.4. 今ある.vueファイルをTypeScriptに変換(コミット: 8dcffc3)

昔の記事や公式の記事を参考に、書き換える。
nuxt-community/nuxt-property-decoratorも参考になる

4. Sassの導入(コミット: 9cde2e1)

Sassの導入は、npm installするだけ

$ npm install --save-dev node-sass sass-loader

5. env用の設定ファイルを追加(コミット: 1cbc14a)

個人的な趣味で、開発用と本番用のenvを分けるときは、
nuxt.config.tsに書かず、別ファイルにしている。

# 開発用のenv
$ touch env.development.js
# 本番用のenv
$ touch env.production.js

中身はこんな感じ。基本情報とかはenvに入れている

module.exports = {
  baseUrl: "http://localhost:3000",
  contactFormUrl: "TODO_YOUR_CONTACT_FORM_URL",
  policyUrl: "TODO_YOUR_PRIVACY_POLICY_URL",
  tosUrl: "TODO_YOUR_TERMS_OF_SERVICE_URL",
  twitterUrl: "TODO_YOUR_TWITTER_URL",
  firebaseConfig: "TODO_YOUR_FIREBASE_CONFIG",
  GA_ID: "TODO_YOUR_GOOGLE_ANALYTICS_ID"
};

env.jsを読み込むようにnuxt.config.tsを変更

  import NuxtConfiguration from '@nuxt/config'
  import pkg from './package.json';
+ const environment = process.env.NODE_ENV || "development";
+ const envSet = require(`./env.${environment}.js`);

  const config: NuxtConfiguration = {
    mode: 'universal',
    srcDir: "app",

+  /*
+   ** ENVIRONMENT PROPERTIES
+   ** See https://ja.nuxtjs.org/api/configuration-env
+   */
+  env: envSet,
+

6. サイトマップ生成の設定を追加(コミット: 0f3bb98)

Google Search Consoleにアップロードするようにsitemap.xmlをnuxt build時に生成するように設定。
@nuxtjs/sitemapを利用すればOK!

まずは、インストール

$ npm install --save @nuxtjs/sitemap

nuxt.config.tsに設定を追加

  const config: NuxtConfiguration = {
   modules: [
     ...
+    // Doc: https://github.com/nuxt-community/sitemap-module
+    "@nuxtjs/sitemap",
   ],
+
+  /*
+  ** Sitemap
+  */
+  sitemap: {
+    path: "/sitemap.xml",
+    hostname: envSet.baseUrl,
+    generate: true
+  },

7. 国際化のためにnuxt-i18nを追加

国際化重要。日本語だけだと範囲が狭いので、英語には対応したい。。
なので、nuxt-i18nを使ってi18n対応できるようにする。

詳しい使い方は、別記事でも。

7.1. インストールして、設定を追加(コミット: 43108e2)

まずは、インストール

$ npm install --save nuxt-i18n

# メッセージのJSONを配置するディレクトリを追加
$ mkdir app/assets/msg
# メッセージファイルを追加
$ echo "{}" > app/assets/msg/common.json

nuxt.config.tsに設定を追加

  const config: NuxtConfiguration = {
   modules: [
     ...
+    // Doc: https://nuxt-community.github.io/nuxt-i18n/
+    ['nuxt-i18n', {
+      parsePages: false,
+      locales: [
+        { code: 'en', iso: 'en_US' },
+        { code: 'ja', iso: 'ja_JP' },
+      ],
+      defaultLocale: 'en',
+      vueI18n: {
+        fallbackLocale: 'en',
+        messages: require("./app/assets/msg/common")
+      },
+      vueI18nLoader: true // vue-i18n-loaderを有効にする
+    }],
     // Doc: https://github.com/nuxt-community/sitemap-module
     "@nuxtjs/sitemap",
   ],

ポイント

  1. nuxt-i18nの設定が@nuxtjs/sitemapに反映されるように、先に書く。
  2. TypeScript+decoratorを使っている場合は、parsePages: falseが必要。。

7.2. メッセージファイルを追加して、各.vueを国際化(コミット: afe0c96)

細かい変更は、コミット差分を参照。
メッセージファイルは、トランスノートというWebサービスを使って生成

7.3. メニューに言語変更を追加(コミット: c40d7bb)

英語と日本語を切り替えれるように、言語変換を追加。
詳細は、コミット差分を参照

8. SEO関連の設定

SEO的に各ページのtitleやdescriptionをちゃんとしたい&i18n使ってるのでLocaleにあわせて日英切り替えたい!
いろいろやってみたけど、Vue.jsのmixinを使うのが良さそうなので、
「Faviconなどの共通的なhead()」と「titleやOGPなど各ページのhead()を差し込む枠組み」を追加してく

各要素などは、過去の記事を参照 - SEO/OGP関連のmetaタグをまじめに対応しようとしてみた。 - くらげになりたい。

8.1. 共通的なhead()(コミット: 96e82d8)

共通的なheadは、別ファイル(commonHead.js)にまとめて、nuxt.config.tsでグローバルとして設定する。

詳細は、コミット差分を参照。

favicon一式は、favicon generatorで生成したものを、app/static配下に配置した想定の設定。

8.2. mixinでpageごとに設定(コミット: 12a80ed)

mixinsはこんな感じ。mixinsのhead()で各ページで設定するmetaタグを生成する。
各ページ個別の情報をheadInfo()に書くと、HeadMixinのhead()がそっちを参照しながら、いい感じに。

import Vue from "vue";
import Component from "vue-class-component";
import { MetaInfo } from "vue-meta";
import { HeadInfo } from "~/types";

 @Component
export default class HeadMixin extends Vue {
  public headInfo(): HeadInfo {
    return {};
  }

  public head(): MetaInfo {
    const i18nSeo = this.$nuxtI18nSeo();
    const t = this.$t.bind(this);
    const info = this.headInfo();

    const siteName: string = t("site_name") as string;
    const title: string = t(info.title || "site_name") as string;
    const description: string = t(info.description || "site_description") as string;

    const baseUrl: string = process.env.baseUrl || "";
    const thisUrl: string = `${baseUrl}${this.$route.path}`;

    const ogImageUrl: string = `${baseUrl}${info.ogImagePath || "/ogp.png"}`;
    return {
      title: title,
      htmlAttrs: {
        prefix: "og: http://ogp.me/ns# fb: http://ogp.me/ns/ fb#",
        ...i18nSeo.htmlAttrs
      },
      meta: [
        { hid: "description", name: "description", content: description },
        { name: "application-name", content: siteName },

         // OGP / Social Meta Tag
        { property: "og:type", name: "og:type", content: "website" },
        { property: "og:title", name: "og:title", content: title },
        {
          property: "og:description",
          name: "og:description",
          content: description
        },
        { property: "og:url", name: "og:url", content: thisUrl },
        { property: "og:image", name: "og:image", content: ogImageUrl },
        { property: "og:site_name", name: "og:site_name", content: siteName },
        ...i18nSeo.meta
      ],
      link: [{ rel: "canonical", href: thisUrl }, ...i18nSeo.link]
    };
  }
}

そのままだと、$nuxtI18nSeo()や$tで型定義が足りてない!と、怒られるので、
tsconfig.jsonやapp/types/vue.d.tsにnuxt-i18nなどの定義を追加する。

HeadInfoは、各ページで設定するパラメタを規定するために、独自に型定義を用意。

nuxt-i18nもseo関連のmetaタグを生成してくれるが、セルフマージが必要。。
なので末尾に...i18nSeo.htmlAttrsなどを追加

8.3. 各ページにmixinを導入(コミット: 12a80ed)

各ページはこんな感じ。mixins(HeadMixin)でmixinsを継承して、
headInfo()でページのtitleやdescriptionのkeyを設定。

- import { Component, Vue } from "nuxt-property-decorator";
+ import { Component, Vue, mixins } from "nuxt-property-decorator";
+ import HeadMixin from "~/mixins/HeadMixin";
+ import { HeadInfo } from "~/types";
  import Card from "~/components/Card.vue"    

  @Component({ components: { Card } })
- export default class IndexPage extends Vue {
+ export default class IndexPage extends mixins(HeadMixin) {
    name:string = 'HomePage';
+   public headInfo(): HeadInfo {
+     return {
+       title: "home_title",
+       description: "home_details"
+     };
    }

9. Google Analyticsの設定(コミット: 51cc5db)

PVとかもちゃんと計測したいので、GoogleAnalyticsを使えるようにする。
@nuxtjs/google-analyticsを入れるだけでOK

$ npm install --save @nuxtjs/google-analytics

設定とかはコミット差分を参照。 過去の記事でも。

10. Buefyカスタマイズ用のSCSSを追加(コミット: 5b8372b)

Bulmaの$primaryの色とかはテーマとして変更したいので、 カスタム用のCSSを追加

ベースだけなので、ほぼ公式のまま。
- Customization | Buefy

11. Page Transitionを追加(コミット: b19e28e)

ページ遷移時のアニメーションもつけたいので、グローバルにCSSを追加

設定とかはコミット差分を参照。もちろん公式のガイドも。

12. UglifyJsPluginでconsole.logを自動削除(コミット: d485999)

個人的にbuild時に自動でconsole.logを削除してほしい。
なので、UglifyJsPluginを使って設定

13. Firebase Cloud Functionsの設定してSSRできるようにする

SPAだと、metaタグがいい感じならないので、SSRしたい。。
Firebase Hostingにstaticやassetsをおいて、それ以外はCloud FunctionsでSSRする

13.1. firebase initで初期設定(コミット: fcbf5f7)

とりあえず、初期設定。HostingとFunctionを選択する

$ firebase init

13.2. Nuxtを呼び出す関数を用意して、rewriteを追加(コミット: 9f8d986)

ssrというFunctionを追加。
Hostingのrewriteで基本すべてのリクエストをssrに渡すようにする。

    "rewrites": [
      {
        "source": "**",
        "function": "ssr"
      }
    ],

headersも合わせて設定しているが、キャッシュコントロールのため。不要ならなくてもOK

ssr関数自体は過去記事を参照。
- Nuxt2.5でFirebaseでSSRするCloud Functionsの書き方 - くらげになりたい。

13.3. functionsのpackage.jsonへ依存関係を追加(コミット: 31028f5)

ssr関数内で実行されるNuxtオブジェクトが実行できるように、 app/package.jsonのdependenciesと同じパッケージを、functions/package.jsonにも追加

13.4. buildの一連の流れをpackage.jsonに追加(コミット: 606cda6)

個人的にCloud FunctionsでSSRするときの肝っぽいところ。
一連の流れは、以下のような感じ。

  1. nuxt buildで app配下をビルドし、.nuxt/を作成
  2. functions/index.jsからNuxtを呼び出せるよう、.nuxt/をfunctions配下に配置
  3. Hostingでdeployするディレクトリとして、public/をクリーンして、新規作成
  4. public/にビルドしたうちのassets/部分をコピー。app/staticもコピー
  5. Hostingのdeployでpublic/ディレクトリをアップロード
  6. Functionsのdeployでfunctions/ディレクトリをアップロード

よくハマったのは、2./4.のコピーする部分。
- Functionsは全部必要なので、そのまま配置、 - Hostingはjs/css/画像など、一部のみなので、選別して配置

特にHostingには優先順位があり、実際のファイルのほうがリライトよりも優先度が高いので、
/index.htmlがあると、ssr関数は全く呼び出されなくなる。。

その点を踏まえて、一連の流れをpackange.jsonのscriptに用意しておけば、
npm run build && npm run deployでデプロイできる

14. CircleCIの設定(コミット: 4cec55f)

めんどくさがりなので、masterにpushしたらデプロイしてほしい。。
なので、CircleCIで自動リリースできるように設定

詳細はコミット差分を参照。過去の記事も。
- CircleCIでFirebaseへ自動デプロイ(Hosting+Funcsions) - くらげになりたい。

以上!!

これでチェックアウトするだけで、初期構築の作業がほとんど完了するのでだいぶ楽になる!!

参考になる書籍

Nuxt.jsビギナーズガイド―Vue.js ベースのフレームワークによるシングルページアプリケーション開発

Nuxt.jsビギナーズガイド―Vue.js ベースのフレームワークによるシングルページアプリケーション開発

Vue.js入門 基礎から実践アプリケーション開発まで

Vue.js入門 基礎から実践アプリケーション開発まで

基礎から学ぶ Vue.js

基礎から学ぶ Vue.js

改訂新版 Vue.jsとFirebaseで作るミニWebサービス (技術の泉シリーズ(NextPublishing))

改訂新版 Vue.jsとFirebaseで作るミニWebサービス (技術の泉シリーズ(NextPublishing))

参考にしたサイト様