webmas!

My tech blog powered by Nuxt.js

最終更新: 2020年03月13日
Nuxt/TypeScriptで始めるDevOpsなAzure開発環境構築

目次

やりたいこと

NuxtをTypeScriptで静的に型付けしつつ簡単にテスト駆動開発っぽくしていきたい
・SPAではなくSSRで動的なサイトを作りたい
主要なPaaSクラウドサービスのうちAWS, GCPは使ったことあるけどAzureはあまり使ったことないので触っておきたい
・ついでなので、Github連携してCI/CDの整備されたDevOpsな開発環境を作りたい
・最終的に作れはしたが、設定項目等JS特有の複雑怪奇なビルド問題のため再現性が低いので、言語化/整理しておきたい

特に上記最後の項目。上述すべてを含めた、
Nuxt/TypeScriptの環境構築からDevOpsなステージング/本番環境へのデプロイまで」が
一気通貫して記述されている記事は見当たらないのでそれだけでも有益かなと思ってる。

(参考)
・DevOpsとは
・2019年11月時点におけるIaaS+PaaSクラウド市場の各社シェア
・SPA、SSR、プリレンダリングの違いをまとめてみた
・Nuxt.jsを使うときに、SPA・SSR・静的化のどれがいいか迷ったら

出来上がったもの

・ステージング環境
・本番環境

ローカルで制作したTypeScript記法のNuxtプロジェクトを、指定のGithubリポジトリにpushするとAzure App Service側で自動ビルド・デプロイの処理が走り、上記ステージング環境に反映される。反映された内容で問題なければ、ワンクリックで本番環境に反映。ステージングスロットは20個まで作成無料。

(追記: 2020.03.19)
→テストは完了しましたので、課金防止のためページ公開停止しました〜

大まかな流れ

NuxtでSSRを実装する場合、ホスト先としてNode.jsのサーバーが必要になる。
ただ、別途サーバを用意するのは面倒なので、Azure App Serviceを用いたServerlessな環境をデプロイ先にする

Azure App Serviceへの登録、設定

この点に関しては下記の記事が詳しい。この項にて下記5点が可能になる。
・Nuxt.js の Universal SSR を Azure App Service で運用する方法

  1. Azure App Service の準備
  2. Blue-Green Deployment の準備
  3. ステージング環境・本番環境の作成、それぞれに固有のURL(デフォルトでSSL対応済)を発行
  4. GitHub との連動、指定リモートリポジトリの内容をステージング環境へ自動ビルド & デプロイ
  5. ステージングの内容をワンクリックで本番に反映、逆もまた然り。ステージングは無料で20個まで作成可能

上記記事ではかなり丁寧にAzure App Serviceについて言語化されているので、本記事では蛇足な補足はしない。ここから先は、上記記事を参考にApp Service側の設定が完了されている前提で、主にローカルでのNuxt/TypeScriptの環境構築に焦点を充てていく。

Nuxt/TypeScriptのインストール・設定

A. 作業用ディレクトリを作成、Nuxtをインストール。使用するツール・モードを設定。

B. ESLint, Prettier, TypeScript, その他諸々のインストール・設定(初学者にはここが一番エグい)

C. http://localhost:3000で動作確認。ビルドエラーが発生しなければGithubのリモートリポジトリにpush

pushされると同時に、上述の通りAzure App Service側で自動的にビルド & デプロイの処理が走り、Staging環境へ反映される

ステージングの表示内容・動作に問題がなければ本番環境へワンクリックで反映!!

大まかな流れはこんな感じ。
というわけで!上記A~Cの実際の流れを下記にまとめてみた。

具体的な手順

最終的なディレクトリ構成(主要部分のみ抜粋)

.
├── components
│   └── Logo.vue       /* ***** vueコンポーネントファイル ***** */
│
├── node-modules
│   └── (略)
│
├── pages
│   └── index.vue      /* ***** 各コンポーネントをimportしてページを構成 ***** */
│
├── .eslintrc.js       /* ***** eslintの設定ファイル ***** */
├── package-lock.json
├── package.json
├── tsconfig.json      /* ***** TypeScriptの設定ファイル ***** */
├── nuxt.config.ts     /* ***** Nuxtの設定ファイル ***** */
└── shims-vue.d.ts     /* ***** TypeScriptがVueファイルを認識するように設定 ***** */

プロジェクトの作成/Nuxtのインストール・設定

まずは任意のディレクトリにプロジェクトを作成。

npx create-nuxt-app nuxt-ts

※上記nuxt-tsの部分はプロジェクト名になるので任意のネーミングで問題ない。

設定項目はとりあえずこんな感じ。ESLintとPrettierを入れるのをお忘れなく。

✨  Generating Nuxt.js project in /Users/home/github/nuxt-ts
? Project name nuxt-ts
? Project description My Nuxt.js template project
? Author name XXXXXXXXX
? Choose the package manager Npm
? Choose UI framework None
? Choose custom server framework None (Recommended)
? Choose Nuxt.js modules Axios
? Choose linting tools ESLint, Prettier
? Choose test framework None
? Choose rendering mode Universal (SSR)

無事Nuxtがインストールされたら、該当のプロジェクトフォルダに移動し、ローカルが立ち上がるか動作確認。

cd nuxt-ts
npm run dev 

これでhttp://localhost:3000にて表示確認ができるようになる。

Typescriptのインストール・設定

ここまでで、Githubにpushする予定のNuxtインストール済み作業用ディレクトリが作成できた。
ここから、Nuxt v2.9以降でTypescriptをサポートするために必要なパッケージをインストール・設定していく。

ちなみにNuxt Typescriptの公式サイトはこちら。Nuxt Typescript

必要になるパッケージ/モジュール

(この項はこちらの記事から引用)
Nuxt v2.9 + Typescriptの環境構築

・ @nuxt/typescript-build
→ layouts、components、plugins、middlewaresでTypescriptを使用するためのNuxtモジュール
・ @nuxt/typescript-runtime
→ nuxt.configファイル、local modules、serverMiddlewaresのTypeScriptランタイムサポートを提供するNuxtラッパーバイナリ
・ @nuxt/types
→ 型定義ファイル。@nuxt/typescript-buildに梱包されているので個別でのインストールは不要。
要は@nuxt/typescript-buildと@nuxt/typescript-runtimeを入れて設定をちょこちょこっとすれば良い。

必要になるパッケージ/モジュールをインストール・設定

早速 @nuxt/typescript-buildをインストールしていく。

npm install --save-dev @nuxt/typescript-build

nuxt.config.jsのbuildModulesに以下を記述する。
今回はESLintとTypeScriptを入れているので下記のようになる。

export default {
  //中略
  buildModules: [
    // Doc: https://github.com/nuxt-community/eslint-module
    "@nuxtjs/eslint-module",
    "@nuxt/typescript-build"
  ],
  //中略
}

続けてtsconfig.json公式の通りに作成する。
※ "target": "esnext"の部分に関して公式から下記引用のように注意書きがある。

TIP
現時点では esnext が Optional ChainingNullish Coalescing をサポートしていないようです。>これらの機能を使えるようにするためにターゲットに es2018 を指定する必要があることに注意してください。

{
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    "moduleResolution": "node",
    "lib": [
      "esnext",
      "esnext.asynciterable",
      "dom"
    ],
    "esModuleInterop": true,
    "allowJs": true,
    "sourceMap": true,
    "strict": true,
    "noEmit": true,
    "baseUrl": ".",
    "paths": {
      "~/*": [
        "./*"
      ],
      "@/*": [
        "./*"
      ]
    },
    "types": [
      "@types/node",
      "@nuxt/types"
    ]
  },
  "exclude": [
    "node_modules"
  ]
}

次に、@nuxt/typescript-runtimeをインストールする。

npm install @nuxt/typescript-runtime

package.jsonのscriptsを、「nuxt」から「nuxt-ts」に書き換える。
(注: 本記事で作成しているプロジェクト「nuxt-ts」と名称が被って紛らわしいが、プロジェクト名に関係なく「nuxt-ts」に書き換える。)

"scripts": {
  "dev": "nuxt-ts",
  "build": "nuxt-ts build",
  "generate": "nuxt-ts generate",
  "start": "nuxt-ts start"
}

「nuxt.config.js」の名称を「nuxt.config.ts」に変更する
TypeScriptファイルにすると、extend(config, ctx) {}の部分が暗黙的anyでエラー表示されるので、明示的にany型を指定する。

  build: {
    /*
     ** You can extend webpack config here
     */
-    extend(config, ctx) {}
+    extend(config: any, ctx: any) {}
  }

さらに、nuxt.config.tsに以下を追加。

typescript: {
    typeCheck: true,
    ignoreNotFoundWarnings: true
  }

まだまだ続きます。
既に@nuxtjs/eslint-configがインストールされている場合、これをアンインストールして@nuxtjs/eslint-config-typescriptをインストールする。
(@nuxtjs/eslint-config-typescriptに内包されているため。)

npm uninstall @nuxtjs/eslint-config

npm i -D @nuxtjs/eslint-config-typescript

package.jsonのscriptsの、lint部分を下記のように修正。

  "scripts": {
    "dev": "nuxt-ts",
    "build": "nuxt-ts build",
    "start": "nuxt-ts start",
    "generate": "nuxt-ts generate",
-    "lint": "eslint --ext .js,.vue --ignore-path .gitignore ."
+    "lint": "eslint --ext .ts,.js,.vue --ignore-path .gitignore ."
  }

次に.eslintrc.jsを修正する。
@nuxtjs/eslint-config-typescriptの構成に@typescript-eslint/parserが入っているので、
parser部分のbabel-eslint, extendsの@ nuxtjsを削除し下記のように修正。

  parserOptions: {
-   parser: 'babel-eslint'
  },
  extends: [
-   '@nuxtjs',
+   '@nuxtjs/eslint-config-typescript',
    'prettier',
    'prettier/vue',
    'plugin:prettier/recommended',
    'plugin:nuxt/recommended'
  ],

ここまでで、コマンドを叩けばEslintの構文チェックが作動するようになっているが、保存時に自動的に処理が走るように修正する。nuxt.config.tsのbuildに以下を追加。

  build: {
    /*
     ** You can extend webpack config here
     */
      extend(config: any, ctx: any) {
      // Run ESLint on save
      if (ctx.isDev && ctx.isClient) {
        config.module.rules.push({
          enforce: 'pre',
          test: /\.(js|vue)$/,
          loader: 'eslint-loader',
          exclude: /(node_modules)/
        })
     }
  }

この次はおまけ。
TypeScriptでクラスベースでNuxtを記述する場合はvue-property-decoratorではなくnuxt-property-decoratorを使用する(らしい。これから学習するのでここはまだ把握してない)。
とりあえずインストールしておく。

npm install --save nuxt-property-decorator

nuxt.config.tsに追記。

build: {
  babel: {
    plugins: [
      ["@babel/plugin-proposal-decorators", { legacy: true }],
      ["@babel/plugin-proposal-class-properties", { loose: true }]
    ]
  }
}

さて、このままだとTypeScriptは.vue拡張子ファイルのモジュール/コンポーネントを認識できないので、run devを走らせてもビルドエラーになるはず。
これを認識するように、TypeScriptの型拡張ファイル「shims-vue.d.ts」を作成する。

declare module '*.vue' {
  import Vue from 'vue'
  export default Vue
}

上記shims-vue.d.tsの場所等をtsconfig.jsonに明示的に追記する。

{
  "files": ["shims-vue.d.ts"],
  "include": [
    "components/**/*.ts",
    "components/**/*.vue",
    "layouts/**/*.ts",
    "layouts/**/*.vue",
    "pages/**/*.ts",
    "pages/**/*.vue"
  ],
}

ここまで読んで、初学者が初見で、何がどういう理屈で行われているのか一発で理解できているなら天才だと思う。JavaScriptは暗黒言語
最後に、Logo.vueとindex.vueのscript部分をtypescriptに書き換える。

<script lang="ts">
import { Vue, Component } from 'nuxt-property-decorator'

@Component
export default class Logo extends Vue {}
</script>
<script lang="ts">
import { Vue, Component } from 'nuxt-property-decorator'
import Logo from '~/components/Logo.vue'

@Component({
  components: {
    Logo
  }
})
export default class IndexPage extends Vue {}
</script>

動作確認

ここまで出来たら、ようやく動作確認。
http://localhost:3000がエラーなく表示されるか確認。

npm run dev

問題なく表示されたら、App Serviceに設定したgithubのリモートリポジトリにプッシュ。
自分の場合は、下記リンクのように正常に自動ビルド・デプロイが実行された。(嘘。ビルドエラーと三日三晩の闘いの末、勝利。)
(その後テスト環境を本番環境にマージ済みなので同一の表示になっている。)

・ステージング環境
・本番環境

ここまでのインストール・設定作業のうち、各モジュール間のバージョンの互換性や使用するツールの置き場所等の設定項目によって、一箇所でも誤り/不具合があるとビルドエラーが発生し、ページ自体が一切表示されなくなる場合がある。
JSのビルド問題は根深く、これほどの危険な言語を静的型付けや各構文チェックツール、DevOpsな環境を用意せずに場当たり的な開発を行うことは、それ自体相当なリスクを背負っていると認識した方がいいかもしれない。(This is 個人の主観)

##苦戦したところ

ローカルでは問題なくビルドが成立。コンソール確認してもエラーも警告も一つも発生しないのに、GithubのリモートリポジトリにpushしてAzure App ServiceのStaging環境用スロットで自動ビルド・デプロイが実行されると、下記のようなビルドエラーが発生していた。
Nuxt Fatal Error: Cannot find module '@nuxt/typescript-build'

@nuxt/typescript-buildモジュールが見つからないとのことで、再インストールを試したりバージョン互換性によるエラーを調べたり、App Service側の再起動が必要かな、とか.gitignoreで該当のモジュールを弾いちゃってるのかな、とか、本業繁忙期の中マジな三日三晩の闘い。検索かけてもなかなか原因が特定できない中、他のPaasサービスでNuxtやってる場合でも同じ現象起きそうだな、とふと思い、そっち寄りで調べたらついに出てきた。

・NuxtアプリをGAEにデプロイする(Node.js 10)

原因はpackage.jsonの"devDependencies"と"dependencies"の差異によるもの。
初学者あるある、基礎ができてないと意外な落とし穴に陥ることがある。めげない。

・【いまさらですが】package.jsonのdependenciesとdevDependencies

package.jsonを下記のように修正。

{
  "dependencies": {
    "@nuxt/typescript-runtime": "^0.4.0",
    "nuxt": "^2.0.0",
    "nuxt-property-decorator": "^2.5.1",
+   "@nuxt/typescript-build": "^0.6.0",
+   "@nuxtjs/eslint-module": "^1.1.0"
  },
  "devDependencies": {
-   "@nuxt/typescript-build": "^0.6.0",
-   "@nuxtjs/eslint-module": "^1.1.0",
    "@nuxtjs/eslint-config-typescript": "^1.0.2",
    "babel-eslint": "^10.0.1",
    "eslint": "^6.1.0",
    "eslint-config-prettier": "^6.10.0",
    "eslint-plugin-nuxt": ">=0.4.2",
    "eslint-plugin-prettier": "^3.1.2",
    "prettier": "^1.19.1"
  }
}

最終的な各設定ファイルの内容

module.exports = {
  root: true,
  env: {
    browser: true,
    node: true
  },
  parserOptions: {},
  extends: [
    '@nuxtjs/eslint-config-typescript',
    'prettier',
    'prettier/vue',
    'plugin:prettier/recommended',
    'plugin:nuxt/recommended'
  ],
  plugins: ['vue', 'prettier'],
  rules: {
    'vue/html-closing-bracket-newline': 'off',
    'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
    'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
    'space-before-function-paren': 0,
    'prettier/prettier': ['error', { semi: false }]
  }
}
{
  "name": "nuxt-ts",
  "version": "1.0.0",
  "description": "My kryptonian Nuxt.js project",
  "author": "UserName",
  "private": true,
  "scripts": {
    "dev": "nuxt-ts",
    "build": "nuxt-ts build",
    "start": "nuxt-ts start",
    "generate": "nuxt-ts generate",
    "lint": "eslint --ext .ts,.js,.vue --ignore-path .gitignore ."
  },
  "dependencies": {
    "@nuxt/typescript-runtime": "^0.4.0",
    "nuxt": "^2.0.0",
    "nuxt-property-decorator": "^2.5.1",
    "@nuxt/typescript-build": "^0.6.0",
    "@nuxtjs/eslint-module": "^1.1.0"
  },
  "devDependencies": {
    "@nuxtjs/eslint-config-typescript": "^1.0.2",
    "babel-eslint": "^10.0.1",
    "eslint": "^6.1.0",
    "eslint-config-prettier": "^6.10.0",
    "eslint-plugin-nuxt": ">=0.4.2",
    "eslint-plugin-prettier": "^3.1.2",
    "prettier": "^1.19.1"
  }
}
{
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    "moduleResolution": "node",
    "lib": ["esnext", "esnext.asynciterable", "dom"],
    "esModuleInterop": true,
    "experimentalDecorators": true,
    "allowJs": true,
    "sourceMap": true,
    "strict": true,
    "noEmit": true,
    "baseUrl": ".",
    "paths": {
      "~/*": ["./*"],
      "@/*": ["./*"]
    },
    "types": ["@types/node", "@nuxt/types"]
  },
  "files": ["shims-vue.d.ts"],
  "include": [
    "components/**/*.ts",
    "components/**/*.vue",
    "layouts/**/*.ts",
    "layouts/**/*.vue",
    "pages/**/*.ts",
    "pages/**/*.vue"
  ],
  "exclude": ["node_modules"]
}
export default {
  mode: 'universal',
  /*
   ** Headers of the page
   */
  head: {
    title: process.env.npm_package_name || '',
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      {
        hid: 'description',
        name: 'description',
        content: process.env.npm_package_description || ''
      }
    ],
    link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }]
  },
  /*
   ** Customize the progress-bar color
   */
  loading: { color: '#fff' },
  /*
   ** Global CSS
   */
  css: [],
  /*
   ** Plugins to load before mounting the App
   */
  plugins: [],
  /*
   ** Nuxt.js dev-modules
   */
  buildModules: [
    // Doc: https://github.com/nuxt-community/eslint-module
    '@nuxtjs/eslint-module',
    '@nuxt/typescript-build'
  ],
  /*
   ** Nuxt.js modules
   */
  modules: [],
  /*
   ** Build configuration
   */
  build: {
    babel: {
      plugins: [
        ['@babel/plugin-proposal-decorators', { legacy: true }],
        ['@babel/plugin-proposal-class-properties', { loose: true }]
      ]
    },
    /*
     ** You can extend webpack config here
     */
    extend(config: any, ctx: any) {
      if (ctx.isDev && ctx.isClient) {
        config.module.rules.push({
          enforce: 'pre',
          test: /\.(js|vue)$/,
          loader: 'eslint-loader',
          exclude: /(node_modules)/
        })
      }
    }
  },
  typescript: {
    typeCheck: true,
    ignoreNotFoundWarnings: true
  }
}

参考にしたサイト

・Nuxt.js の Universal SSR を Azure App Service で運用する方法
・Nuxt v2.9 + Typescriptの環境構築
・Nuxt.js + TypeScriptでの環境構築と作業の進め方まとめ
・Nuxt v2.9.2でTypeScript, eslint, Prettier環境構築 + VSCodeの設定
・Nuxt.js + TypeScript(Class-based)にはnuxt-property-decorator
・はじめてのvue-property-decorator (nuxtにも対応)
・Nuxt.js で ESLint & Prettier を導入する
・ESLint 最初の一歩
・Sass を Nuxt.js で使ってみよう
・Nuxt.jsで「style-resources-module」を使って、グローバルで変数やmixinなどを使えるようにする
・Nuxt + TypeScript のスターターテンプレート作りました!

他。

HOME
TOP