Coder Social home page Coder Social logo

caboilerplate's Introduction

CABoilerplate

  1. フォルダ構成
  2. View の表示
  3. ユースケースのコードによる表現
  4. ドメインモデルの実装
  5. プレゼンテーション層でのユースケース呼び出し
  6. インフラ層と依存性逆転の原則
  7. 振る舞い駆動開発

1. プロジェクトの作成

1.1 プロジェクトの新規作成

TypeScript + pug + SASS

$ yarn create vite CABoilerplate --template vue-ts
$ yarn add --dev pug sass

import を "@/.." でアクセスできるようにする

TypeScript 側と Vite 側の両方でパスのエイリアスを指定する必要があります。

$ yarn add --dev @types/node
{
    "compilerOptions": {
        ...
        "baseUrl": ".",
        "paths": {
            "@/*": ["src/*"]
            , "@views/*": ["src/service/presentation/views/*"]
        },
        ...
    },
}
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import * as path from 'path'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()]
  , resolve: {
    alias: {
      "@": path.resolve(__dirname, "src/")
      , "@views": path.resolve(__dirname, "src/service/presentation/views")
    }
  }
})

1.2 フォルダ構成

以下のようにし、自動生成される env.d.ts は system/types 以下へ移動させます。

src/
  ├── service
  │   ├── application
  │   ├── domain
  │   │   ├── interfaces
  │   │   └── models
  │   ├── infrastructure
  │   └── presentation
  │       └── views
  ├── system
  │   └── types
  │       └── env.d.ts
  ├── App.vue
  └── main.ts

2. View の表示

2.1 View を作成する

src/service/presentation/views 以下に Home.vue と Signin.vue を用意します。

<script setup lang="ts">
</script>

<template lang="pug">
h1 Home
router-link(to="/signin") -> Signin
</template>
<script setup lang="ts">
</script>

<template lang="pug">
h1 Signin
</template>

2.2 ルーティングを実装する

Vue Router 4.x を利用します。

$ yarn add vue-router@4 rxjs
<script setup lang="ts">
</script>

<template lang="pug">
router-view
</template>
import { createApp } from 'vue'
import { createRouter, createWebHashHistory } from 'vue-router'
import App from './App.vue'
import Home from '@views/Home.vue'
import Signin from '@views/Signin.vue'

const routes = [
    { path: '/', component: Home }
    , { path: '/signin', component: Signin }
]

const router = createRouter({
    history: createWebHashHistory()
    , routes
})

const app = createApp(App)
app.use(router)
app.mount('#app')

3. ユースケースのコードによる表現

ここではユースケースを const enum と Union 型で表現します。

3.1 ユースケースシナリオの記述

service/application/useases フォルダを作成し、boot.ts ファイルを新規作成します。

ユースケースシナリオを以下のように const enum で表現します。 また、シナリオの各シーンを Union 型で定義します。

/**
 * usecase: アプリを起動する
 */
export const enum Boot {
    /* 基本コース */
    userOpenSite = "ユーザはサイトを開く"
    , serviceCheckSession = "サービスはセッションがあるかを確認する"
    , sessionExistsThenPresentHome = "セッションがある場合_サービスはホーム画面を表示する"

    /* 代替コース */
    , sessionNotExistsThenPreesntSignin = "セッションがない場合_サービスはログイン画面を表示する"
}

// 代数的データ型 @see: https://qiita.com/xmeta/items/91dfb24fa87c3a9f5993#typescript-1
export type BootContext = { scene: Boot.userOpenSite }
    | { scene: Boot.serviceCheckSession }
    | { scene: Boot.sessionExistsThenPresentHome }
    | { scene: Boot.sessionNotExistsThenPreesntSignin }
;

3.2 ユースケースの実装

const enum で定義したユースケースのシナリオを実行可能にします。 具体的にはシナリオの一つひとつのシーンを Scene オブジェクトとして定義し、これを再起呼び出しを使って処理していくようにします。

system/interfaces フォルダを作成し、usecase.ts ファイルを新規作成します。

import { Observable, of } from "rxjs";
import { mergeMap, map } from "rxjs/operators";

interface Scene<T> {
    context: T;
    next: () => Observable<this> | null;
}

export abstract class AbstractScene<T> implements Scene<T> {
    abstract context: T;
    abstract next(): Observable<this> | null;

    protected instantiate(nextContext: T): this {
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        return new (this.constructor as any)(nextContext);
    }

    just(nextContext: T): Observable<this> {
        return of(this.instantiate(nextContext));
    }
}

export class Usecase {

    static interact<T, U extends Scene<T>>(initialScene: U): Observable<T[]> {

        const _interact = (senario: U[]): Observable<U[]> => {
            const lastScene = senario.slice(-1)[0];
            const observable = lastScene.next();

            // 再帰の終了条件
            if (!observable) {
                // console.log(`[usecase:${lastScene.constructor.name.replace("Scene", "")}:${senario.length-1}:END    ]`, lastScene.context );
                return of(senario);
            } else {
                const tag = (senario.length === 1) ? "START  " : "PROCESS";
                // console.log(`[usecase:${lastScene.constructor.name.replace("Scene", "")}:${senario.length-1}:${tag}]`, lastScene.context );
            }

            // 再帰処理
            return observable
                .pipe(
                    mergeMap((nextScene: U) => {
                        senario.push(nextScene);
                        return _interact(senario);
                    })
                );
        };

        return _interact([initialScene])
            .pipe(
                map((scenes: U[]) => {
                    const performedSenario = scenes.map(scene => scene.context);
                    console.log("performedSenario:", performedSenario);
                    return performedSenario;
                })
            );
    }
}

BootScene を Scene インタフェースに準拠するようにし、next 関数を実装します。 next 関数は、自身が表すシーンの次のシーンを指定します。処理終了の場合には null を返すようにします。

export class BootScene extends AbstractScene<BootContext> {
    context: BootContext;

    constructor(context: BootContext = { scene: Boot.userOpenSite }) {
        super();
        this.context = context;
    }

    next(): Observable<this>|null {
        switch (this.context.scene) {
        case Boot.userOpenSite: {
            // TODO
        }
        case Boot.serviceCheckSession : {
            // TODO
        }
        case Boot.sessionExistsThenPresentHome: {
            // TODO
        }
        case Boot.sessionNotExistsThenPreesntSignin: {
            // TODO
        }
        }
    }
}

例えば以下のようにし、check 関数の中でサインインセッションがあるか否かを調べることとします。

    next(): Observable<this>|null {
        switch (this.context.scene) {
        case Boot.userOpenSite: {
            return this.just({ scene: Boot.serviceCheckSession });
        }
        case Boot.serviceCheckSession : {
            return this.check();
        }
        case Boot.sessionExistsThenPresentHome: {
            return null;
        }
        case Boot.sessionNotExistsThenPreesntSignin: {
            return null;
        }
        }
    }
    private check(): Observable<this> {
        if (/* TODO: ドメインモデルが持つメソッドが結果を返すようにする */ false) {
            return of(this.instantiate({ scene: Boot.sessionExistsThenPresentHome }));
        } else {
            return of(this.instantiate({ scene: Boot.sessionNotExistsThenPreesntSignin }));
        }
    }

5. プレゼンテーション層でのユースケース呼び出し

ユーザの入力イベントなどをトリガーとして、プレゼンテーション層からユースケースを実行する必要があります。

5.1 ユースケースの実行

以下のように、Boot ユースケースを初期化し、interact 関数を実行し、結果をサブスクライブするようにします(これをどこに実装するかについては 5.2 参照)。 結果は実際に実行された Scene の配列(これを scenario と呼ぶことにします)で返ってくるので、その最後の Scene が何だったかによって、次の処理を変更します。

import { Usecase } from "@/system/interfaces/usecase";
import { Boot, BootScene } from "@usecases/boot";
import type { BootContext } from "@usecases/boot";
import { Subscription } from "rxjs";

const boot = () => {
  let subscription: Subscription | null = null;
  subscription = Usecase.interact<BootContext, BootScene>(
    new BootScene()
  ).subscribe({
    next: (performedSenario) => {
      const lastContext = performedSenario.slice(-1)[0];
      switch (lastContext.scene) {
        case Boot.sessionExistsThenPresentHome:
          // TODO
          break;
        case Boot.sessionNotExistsThenPreesntSignin:
          // TODO
          break;
      }
    },
    error: (e) => console.error(e),
    complete: () => {
      console.info("complete");
      subscription?.unsubscribe();
    },
  });
};

firebase

$ yarn add firebase

Vuetify

vue-cli を入れる

$ yarn global add @vue/cli
$ vue add vuetify

? Choose a preset: (Use arrow keys)
  Configure (advanced)
  Default (recommended)
❯ Vite Preview (Vuetify 3 + Vite)
  Prototype (rapid development)
  Vuetify 3 Preview (Vuetify 3)

src 以下に plugins フォルダができるので、system 以下に移動する。 vite.config.js ファイルが自動生成されるので、(.ts と重複するため)削除し、.ts を以下のように書き換える。

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vuetify from '@vuetify/vite-plugin'
import * as path from 'path'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue()
    // https://github.com/vuetifyjs/vuetify-loader/tree/next/packages/vite-plugin
    , vuetify({
      autoImport: true,
    })
  ]
  , define: { 'process.env': {} }
  ...
})
import { createApp } from 'vue'
import vuetify from '@/system/plugins/vuetify'
import { loadFonts } from '@/system/plugins/webfontloader'
...
loadFonts()

const app = createApp(App);
app.use(vuetify);
app.use(router);
app.mount('#app');
<script setup lang="ts">
</script>

<template lang="pug">
v-app
  v-main
    router-view
</template>

コードフォーマット

ゴール:TypeScript と vue ファイル内の Pug に対し、保存時に自動でフォーマットがなされるようにする。 実現方法:

  • .ts/.vue の TypeScript は ESLint に、
  • .vue の pug は Vetur 経由で Prettier に、
  • .json は Prettier に

任せる(ESLint で pug 向けの plugin がないため)。

eslint

インストール

$ yarn add --dev eslint
$ yarn create @eslint/config

✔ How would you like to use ESLint? · style
✔ What type of modules does your project use? · esm
✔ Which framework does your project use? · vue
✔ Does your project use TypeScript? · No / Yes
✔ Where does your code run? · browser
✔ How would you like to define a style for your project? · prompt
✔ What format do you want your config file to be in? · JavaScript
✔ What style of indentation do you use? · 4
✔ What quotes do you use for strings? · double
✔ What line endings do you use? · unix
✔ Do you require semicolons? · No / Yes
module.exports = {
    env: {
        browser: true
        , es2021: true
        , node: true
    }
    , extends: [
        "eslint:recommended"
        , "plugin:vue/vue3-recommended"
        , "plugin:@typescript-eslint/recommended"
    ]
    , parser: "vue-eslint-parser"
    , parserOptions: {
        ecmaVersion: "latest"
        , parser: "@typescript-eslint/parser"
        , sourceType: "module"
    }
    , plugins: ["vue", "@typescript-eslint"]
    , rules: {
        indent: ["error", 4]
        , quotes: ["warn", "double"]
        , semi: ["warn", "always"]
        , "comma-style": ["warn", "first"]
        , "comma-spacing": ["warn", { before: false, after: true }]
        , "comma-dangle": ["warn", "never"]
        , "no-var": ["error"]
        , "no-console": ["off"]
        , "no-unused-vars": ["off"]
        , "no-mixed-spaces-and-tabs": ["warn"]
        , "no-warning-comments": ["warn", { terms: ["todo"], location: "anywhere" }]
    }
};

VSCode プラグイン

"dbaeumer.vscode-eslint" をインストール。

{
  "eslint.packageManager": "yarn",
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
  "eslint.format.enable": true,
  "eslint.validate": [
    "typescript",
    "javascript",
    "javascriptreact",
    "vue"
  ]
}

Pritter

$ yarn add --dev prettier @prettier/plugin-pug

VSCode プラグイン

"octref.vetur", "esbenp.prettier-vscode" をインストール。

"vetur.format.defaultFormatter.ts": "none" として、prettier を抑制し、ESLint のみが利くようにする。

{
  ...
  "[json]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode",
  },
  "[vue]": {
    "editor.defaultFormatter": "octref.vetur",
  },
  "vetur.format.enable": true,
  "vetur.format.defaultFormatter.ts": "none",
  "vetur.format.defaultFormatter.pug": "prettier",
  "editor.formatOnSave": true,
}

caboilerplate's People

Contributors

uskithub avatar

Watchers

 avatar James Cloos avatar  avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.