ContractS開発者ブログ

契約マネジメントシステム「ContractS CLM」の開発者ブログです。株式会社HolmesはContractS株式会社に社名変更しました。

コスト分析の「トイル」を撲滅!Amazon Q DeveloperのSREチームでの利用の現在地

  • はじめに
  • コスト分析の「泥臭い現実」
  • Amazon Q Developerによるコスト分析革命
    • 導入のきっかけ
    • 分析プロセスの全体像
    • AI分析の心臓部:プロンプトエンジニアリング
    • 月次分析が「3時間」から「10分」へ
    • 実際の分析結果
  • AI導入前の地道な取り組み
    • 第一歩:まずは「見える化」と「小さな成功体験」
    • 第二歩:地道な最適化と、見えてきた「文化の壁」
  • 今後の展望
  • まとめ
  • 参考にした資料や書籍

はじめに

こんにちは! ContractSでSREをしている福嶌(id:s-fukushima29)です。2023年4月に入社してはや3年目になりました。

自分の経歴としてはプログラマーとしてキャリアをスタートしました。その後、ContractSに入社する前はMSP(マネージドサービスプロバイダ)事業に携わり、日々お客様のAmazon Web Services (AWS)、Google Cloud、Microsoft Azureといったクラウド環境と向き合ってきました。
そこでは、1円のコストがビジネスにどれだけ影響するかを肌で感じていて、「スポンサーが石油王でもない限り、予算は有限だ」という現実を常に意識していました。そのため、「クラウド費用の内訳を分析して把握し、お客様に説明責任を果たすこと」は、お客様の事業成長を支える重要な責務だと考えています。
この考えは、自社サービスにおいても同様に重要です。コストの内訳を説明できなければ、事業環境の変化によるコスト削減の波が来たときに、その価値を正しく示すことが難しくなり削減の対象となってしまうからです。

ContractSに入社して感じたのは、プロダクト開発のスピードが最優先される文化の中で、インフラを見るSREである我々の"コストに対する意識が薄い"ということでした。
もちろん、それは成長するSaaS企業にとって健全な姿の一面でもありますが、MSP出身の自分には、それが「大きな改善のチャンス」であり、チームとして「伸ばすべき能力」だと感じられました。

この記事では、そんな「コスト意識が薄い」状態から、チームでコスト分析と最適化を当たり前にするようになり、Amazon Q Developerという強力なアシスタントを得て分析業務を劇的に効率化した道のりとこれからについてご紹介します。 同じような課題を抱えるSREや開発チームの皆さんにとって、少しでもヒントになれば嬉しいです。

続きを読む

(Java)QueryDslを使用して重複除去(DISTINCT)とソート(ORDER BY)を両立させる

ContractSの倉島です。
最近、DBへのアクセスを型安全にすべくQueryDslを用いているのですが困ったことに直面したので備忘録的に解決手段を記しておきます。

何に困ったか

とあるデータを一覧で取得する際に、「レコードの重複除去を行いつつ取得していない値でソートを行う」必要が出てきました。
しかし、ただ単純にDISTINCTとORDER BYを同時に指定するだけでは順番が狂って取得されてしまいました。

何が原因か

DISTINCTは内部でソートしてから重複除去を行うらしく同時に指定した場合は、DISTINCTが実行された後にORDER BYが実行されてしまうので狂った並び順でソートされてしまうと原因づけました。

解決策

MySQLのクエリだったら、親のテーブルの重複除去を行った結果を副問い合わせとしてそれをソート対象の値があるテーブルと結合すれば、DISTINCTとORDER BYは別々で実行されるので正しく取得できるかと思います。
ただ、QueryDslには副問い合わせの結果をjoinする機能は存在しない(もし存在していたらすみません!)ので、この解決策は使えません。
そこで考えたのが、ORDER BYに指定する値を副問い合わせ結果にしてしまうという策です。

具体的には

修正前(これだと狂った並び順で取得される)

              queryFactory
                .select(Projections.constructor(
                        SearchedResultEntity.class,
                        qMainEntity.mainId,
                        qMainEntity.xxx,
                        qSubEntity.subId,
                        qSubEntity.yyy
                ))
                .distinct()
                .from(qMainEntity)
                .innerJoin(qSubEntity)
                .on(qSubEntity.mainId.eq(qMainEntity.mainId))
                .leftJoin(qSubSubEntity)
                .on(qSubSubEntity.subId.eq(qSubEntity.subId))
                .orderBy(qSubSubEntity.property.asc());

修正後(これで正しい順番で取得される)

              SubQueryExpression<Long> subQuery =
                JPAExpressions.select(qSubSubEntity.value).from(qSubSubEntity)
                        .where(qSubSubEntity.propertyId.eq('sortPropertyId'),
                                qSubSubEntity.subId.eq(qContractHistoryEntity.subId));

              OrderSpecifier<Long> sort = Expressions.asNumber(subQuery).asc()

              queryFactory
                .select(Projections.constructor(
                        SearchedResultEntity.class,
                        qMainEntity.mainId,
                        qMainEntity.xxx,
                        qSubEntity.subId,
                        qSubEntity.yyy
                ))
                .distinct()
                .from(qMainEntity)
                .innerJoin(qSubEntity)
                .on(qSubEntity.mainId.eq(qMainEntity.mainId))
                .leftJoin(qSubSubEntity)
                .on(qSubSubEntity.subId.eq(qSubEntity.subId))
                .orderBy(sort);

取得する際はfetch()の実行もお忘れなく。
また、副問い合わせを使用する関係上、扱うデータが大量だとパフォーマンスに対する懸念が生まれるのでご注意ください。

最後に

クエリ文を直接記述する方法なら、最初の「親テーブルを重複除去した結果」を結合してしまえば解決していたことを考えると、今回の問題に限って言えばQueryDslは少し不便かもしれないですね。
しかしながらそれを補って余りある型安全やメソッドチェーンによる可読性の向上があると考えているので今後もQueryDslを有効活用できればと思います。

複数フィールドのバリデーションエラーを集約して表示するVeeValidate v4の活用法

こんにちは。ContractSでフロントエンドエンジニアをしている北原です。

弊社では、UIのバリデーションとしてVeeValidate v4を採用しています。 この記事では、複数のインプットを1つのバリデーションフィールドとして扱う方法について解決策を提示したいと思います。

目次

前提知識

今回の記事の前提知識です。VeeValidate v4についてご存知の方はスルーしてください。

VeeValidateとは?

Vue.jsにおけるフォームバリデーションを簡素化する人気のライブラリです。
日本語ドキュメントも充実しており、大変扱いやすいものになっています。 vee-validate.logaretm.com

VeeValidate v4 の Composition関数

1. useField

const { value, errorMessage } = useField(() => 
  'email',
  yup.string().email().required(),
)

useFieldは、コンポーネントをフィールドとして扱うことができる関数です。

v-modelにbindして使う実際の入力値valueや、バリデーションルールにそぐわない場合に生成されるerrorMeesageなどを有しています。 vee-validate.logaretm.com

2. useForm

const { handleSubmit } = useForm()

useFormは、記述したコンポーネントをフォームとして扱うことができる関数です。

前述したuseFieldを有するコンポーネントを子コンポーネント以下に持つ場合に用いることで、その値を取得して送信できるhandleSubmitなどを有しています。 vee-validate.logaretm.com

なぜ必要なのか?

弊社のプロダクトでは、契約書を管理するに当たって契約項目と呼ばれる関連情報を、各契約書が有しています。
それらをユーザの方へ入力していただく場合に必須項目としたいパターンが存在します。

このとき、以下のような別々のフィールドを、エラーメッセージを1箇所に集約してバリデーションをしたいパターンが出てきました。

エラーメッセージを1箇所に集約したい例

このフィールドは、契約書に対する自動更新機能を有効にしたとき、契約書の契約期間終了後に新しく引き延ばす契約期間の長さを入力するフィールドです。
左側は値(半角数字)のインプット、右側は単位(日、ヶ月、年から選択)のプルダウンとなっており、2つで1つです。

しかしVeeValidateの仕様上、このようなパターンでは左右それぞれのフィールドが、個々のバリデーションを持つ別のフィールド値として扱われてしまうため、上の画像のようにエラーメッセージの表示箇所を集約できません。

なんとか解決する手段を考えた結果がこの記事になります。

TL;DR

  1. エラーメッセージを自身の管理対象として加える関数をprovideするラッパーコンポーネントを作成
  2. useFieldを持つコンポーネントが、1. のコンポーネントからprovideされた関数をinjectしてエラーメッセージを管理対象に加える

以上の流れで実現します。

やってみる

参考

VeeValidate v4開発者、Abdelrahman AwadさんのCodeSandboxにあった以下の内容を参考にしました。

https://codesandbox.io/p/sandbox/handling-nested-forms-in-vee-validate-v4-p45up?file=%2Fsrc%2Fcomponents%2FChildForm.vue%3A30%2C3-53%2C3

実装例

1. ラッパーコンポーネントの作成

まず以下のように、動的にuseFieldの値を複数管理するための関数を提供するラッパーコンポーネントを実装します。

script

// Types
interface Injection {
  errorMessage: Ref<string>
  // 他に管理したい対象があれば追加する
}
export type ProvideAddField = (injection: Injection) => void

// Variables
const errorMessages = ref<Ref<string>[]>([])

// Computed
const allErrorMessages = computed<string[]>(() => (errorMessages.value.map(it => it.value).filter(it => !!it)))

/**
 * エラーメッセージを登録する関数をprovideする
 */
onBeforeMount(() => {
  provide(ProvideKey.INCLUDE_FIELD_ERROR_MESSAGE,
    (injection: Injection) => {
      errorMessages.value.push(injection.errorMessage)
    },
  )
})

onBeforeMountのライフサイクルで、このコンポーネントへエラーメッセージを登録する関数をprovideしておき、それによって追加されるエラーメッセージ群のerrorMessagesも持っておきます。

また、実際にエラーが発生しているものだけに絞り込んだallErrorMessagesも合わせて持っておきます。

template

<template>
  <div>
    <slot />
    <!-- 複数のメッセージを表示したい場合はv-forすれば良い -->
    <AtomsErrorMessage v-show="allErrorMessages.length !== 0" class="mt-1" :message="allErrorMessages[0]" />
  </div>
</template>

template部には、provideした関数により追加されたエラーメッセージの表示箇所と、当該コンポーネントへエラーメッセージを追加したいフィールドコンポーネントを入れ込むためのslotを記述します。

2. フィールドコンポーネントから関数をinject

script

const { value, errorMessage } = useField(() => 
  'email',
  yup.string().email().required(),
)

// (中略)

/**
 * エラーメッセージを、ラッパーコンポーネントの管理対象として追加します ※
 */
const includeField = (errorMessage: Ref<string>) => {
  const injection: ProvideAddField | undefined = inject(ProvideKey.INCLUDE_FIELD_ERROR_MESSAGE)
  if (injection) injection({ errorMessage })
}

onMounted(() => {
  includeField('email', errorMessage)
})

フィールドコンポーネントへ、onBeforeMountedprovideされている関数を、次のonMountedライフサイクルで呼出す処理を追加しておきます。

※ ここでは便宜上素で定義していますが、composablesにしておくのが良いです

3. 実際に使ってみる

ラッパーコンポーネントのslotへ、実際にバリデーションしたいフィールドコンポーネントを入れこみます。

これにより、エラーが発生された場合に1箇所へ集約して表示できるようになります。

テンプレート記述例

まとめ

実装自体に手間はかかりますが、一度仕組み化してしまえば汎用的に利用できるため利便性は高いと思います。
もう少し簡素に実現できる方法がありましたら、ぜひご教授ください。

今回の記事が、少しでも同じような課題を抱えている方の助けになれば幸いです。

最後に

お読みいただきありがとうございました。
ContractSでは一緒にプロダクトを進化させていくエンジニアを募集中です。

recruit.jobcan.jp

recruit.jobcan.jp

recruit.jobcan.jp

APIファーストで契約管理システムの連携力を高める

こんにちは。ContractSでバックエンドエンジニアをしている毛見です。
弊社がAPIファースト開発を取り入れた背景や利点、実際のアプローチを整理してブログにしました。

契約ライフサイクル管理 (CLM: Contract Lifecycle Management) は、単なる電子契約、契約書の保管という枠を超え、営業、法務、財務、購買などの多様な業務との連携によって業務の統制や効率化を図ります。昨今の企業では、業務ごとに専用のSaaSや基幹システムを導入しており、これらのシステムとシームレスに連携することがCLMに求められています。
私たちは、この連携を柔軟に実現するために APIファーストのアプローチを採用しています。この記事では、その利点や実践内容、得られる効果についてご紹介します。

APIファーストとは?

APIファーストとは、システムやサービスの設計・開発において 「まずAPIを定義することから始めるアプローチ」 です。このアプローチでは、APIを単なる技術的なインターフェースではなく、システム全体の基本構造として捉えます。
参考: Postman What is API-first?

APIファーストの利点

利用者視点のインターフェース設計

APIの定義を先に行うことで、フロントエンド、他システム、外部の開発者の視点を意識した設計が可能になります。それにより他システムやパートナーとの連携が迅速で容易になります。

ノーコード/ローコードでシステム構築ができる世界を実現する

APIの定義において、RESTやOpenAPI(Swagger)などの業界標準を採用することで、ノーコード/ローコードでの連携が可能になり、外部システムやSaaSとの連携がスムーズになります。

柔軟なユースケース対応

ビジネス要件に応じてAPIを再利用・組み合わせることで、多様なユースケースに柔軟に対応できます。新しい製品やサービスを迅速に立ち上げる際にも、APIファーストの設計は大きな強みとなります。

APIファーストをどのように取り入れているか?

設計フェーズと実装フェーズ

APIの仕様書を作る方法として、実装後にコントローラーから生成する方法があります。それに対し、APIファーストの開発では、まず設計フェーズでAPI仕様をOpenAPIで記載します。記載したAPI仕様について開発者、プロダクトオーナーで合意し、テストケースを作成します。この時点で、外部サービスとの連携、既存のWebサービスで利用できるAPIとなるように検討を済ませます。その後に実装フェーズに移ります。

既存のWebアプリケーション用のバックエンドからストラングラーフィグパターンで移行する

ContractS ではAPIファーストを取り入れる以前のWebアプリケーション用のバックエンドサービスが存在します。そのバックエンドサービスには仕様が蓄積されていますが、外部サービスとの連携を考慮していないため、APIとして公開できません。そのため、公開も可能なAPIサービスとして作り直す必要があります。そこで、機能ごとにストラングラーフィグパターンで新たなAPIに移行をしました。既存のバックエンドサービスは移行先のAPIを呼ぶようにし、BFF(backend for frontend)として振る舞うことで、フロントエンドへの影響を最小限にとどめながら移行が可能になります。

テストと品質保証の自動化

実現の途中ではありますが、CI/CDによってAPIのE2Eテストを自動化し、相互運用性の担保を行います。これによりWebアプリケーションだけでなく、連携先のAPIに対しても品質を保証することができます。

ビジネス的な効果

顧客への導入提案において、Webアプリケーションだけでは実現が難しい要求に対し、API連携を利用した実現方法を提案できるため、「APIを使ってできます」と言えるようになります。 また、他システムとの連携について詰めていく中で、想定外の要件がでることもあるかと思います。そういった時に、特定のユースケースのためのAPIではなく、汎用的なAPIとして設計してあれば、それを組み合わせるだけで追加開発の必要なく要件に対応することができます。

最後に

契約ライフサイクルは、さまざまなシステムとの連携なしには成り立ちません。そのため、私たちは APIを核に柔軟な連携基盤を構築しています。これにより、顧客の多様な業務要件に応え、契約管理の効率化を推進しています。
実現途中のAPIのE2Eテストについてもブログ化を検討していますので、楽しみにしていただければと思います。

ContractSでは一緒にプロダクトを進化させていくエンジニアを募集中です。

recruit.jobcan.jp

recruit.jobcan.jp

recruit.jobcan.jp

Pub-Subモデルを活用して通知基盤を改善した話

というタイトルで、12/13(金)に開催されたDisruptors Tech Meetに登壇しました。
こんにちは。ContractSの友野です。

Disruptors Tech Meetとは

株式会社ディスラプターズのグループ会社に所属するエンジニアが

  • 各社事例を見て・聞いて刺激をもらう
  • 普段触れない技術の話を聞いてスキルアップに繋げる
  • 発表や意見交換の経験を積む

といったことを目的に、日頃の成果を発表・共有する勉強会です。
ContractSはディスラプターズキャリアインデックスから商号変更)グループの一員として、継続的なアウトプットの場として活用しています。

同会には、キャリアインデックス社マージナル社のエンジニアが参加しており、毎度ワイワイと楽しんでいます。

Disruptors Tech Meetは、グループ会社間のクローズドな勉強会ではあるものの、クオーター毎に継続開催され、そろそろ3年目に突入します。
いずれパブリックな勉強会にしていく狙いもあります。お楽しみに。
(今回は登壇側でしたが、筆者は普段は運営側の人間です)

Springにおけるアプリケーションイベント

登壇資料では具体的な技術スタックに触れておりませんが、Java/Spring Boot環境で基盤は構築しています。
Springでは、アプリケーションイベントの発行はApplicationEventPublisherで行います。

ApplicationEventPublisher (Spring Framework API) - Javadoc

余談ですが、数年前まで(Spring 4.2以前)はイベント発行のためにApplicationEventJavadoc)というクラスを継承する必要がありましたが、今現在はPOJOのデータモデルが使えるので、今回の通知基盤だったり、ドメインイベントへの適用だったり、活用はかなりしやすくなったと感じています。

発行されたイベントは、TransactionalEventListenerアノテーションを付与したメソッドでリッスンします。評価されるタイミングはデフォルトはトランザクションコミット後(AFTER_COMMIT)です。

TransactionalEventListener (Spring Framework API) - Javadoc

サンプル

以下、簡単なコードイメージです(イメージしやすさ優先で、適切な責務分割ではないです…)。

// イベント発行側
public class ApplicationEventNotifier {
    // 通知用文面ファクトリ
    private final NotifiableContentsFactory notifiableContentsFactory;
    private final ApplicationEventPublisher applicationEventPublisher;

    // タスクが作成されたことをユーザーに通知する
    public void taskCreated(Set<UserId> destinations) {
        destinations.stream()
                .map(userRepository::findById)
                .map(notifiableContentsFactory::taskCreated)
                .forEach(this::notify);
    }

    private void notify(Notifiable event) {
        applicationEventPublisher.publishEvent(event);
    }
}


// タスク作成イベント
public record TaskCreated(MailAddress destination, String subject, String body) implements Notifiable {
}


// イベント購読側
public class NotifiableEventListener {
    private final MailClient mailClient;

    // タスク作成時に呼ばれるリスナー
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void onTaskCreated(TaskCreated event) {
        mailClient.send(event.destination(), event.subject(), event.body());
    }
}

このサンプルだけ見ると効果は感じにくいかもしれませんが、実際には通知イベントインタフェースの統合やリスナーの共通化によって、フレームワークのような形で使えるようにしています。
今回はアプリケーションの構造を整理したのみで、メッセージブローカーの導入などまでは踏み込んでいません。いずれこのあたりも拡張後、記事にまとめていこうと思います。

最後に

簡単ではありますが、登壇資料の公開と、Springにおけるアプリケーションイベントの実装サンプルをまとめました。今回の文脈では通知のみですが、また別の形でドメインイベントへ適用も進めています。こちらも機会を見つけてまとめておこうと思います。

最後に、ContractSでは一緒にプロダクトを進化させていくエンジニアを募集中です。少しでも興味がある方、「CLM?なにそれ美味しいの?」という方もカジュアル面談から可能ですので、以下のページからお気軽にお声掛けください。

recruit.jobcan.jp

recruit.jobcan.jp

recruit.jobcan.jp

Vue 3, Nuxt 3 マイグレーションを経験してみて

はじめに

こんにちは。 ContractSでフロントエンドエンジニアをしている村澤です。

先日、一時的に Vue 2.x から Vue 3.x のマイグレーションへ携わる機会があったため、その間の学びを共有します。

ContractS CLMでは、Vue.js を用いて開発をしています。

Vue.jsとNuxt.jsは、Web開発において広く利用されている人気の高いフレームワークです。
技術の進化とともに、それらのバージョンも進化を続けています。Webフレームワークのマイグレーションは、開発者にとって重要なタスクです。本記事では、その必要性と経験について述べます。

Vue 3, Nuxt 3へのマイグレーションがなぜ必要なのか

Vue 2は、2023年にEOL(End of Life)を迎えました。

v2.vuejs.org

これは、Vue 2の新しい機能やセキュリティパッチが提供されなくなったことを意味します。また、Nuxt 2.xの依存関係にあたるライブラリが更新されることも無くなるため、脆弱性が発見された際のパッチ適用が難しくなります。
したがって、品質の維持とセキュリティの確保のために、Vue 3およびNuxt 3へのマイグレーションが必要です。

破壊的変更の学習

Vue 3への移行には、破壊的変更を伴います。これは、Vue 2から廃止された機能、仕様の大きな変更などが含まれます。 その中でも影響が大きいと感じた変更はv-modelの扱いです。
Vue 3 からは以下のような仕様変更がなされています。

バージョン デフォルトプロパティ名 デフォルトイベント名
Vue 2 value input
Vue 3 modelValue update: modelValue

Vue 2では、props.sync修飾子で、子コンポーネントからのemit イベント名をupdate:(prop名)とする仕様がありました。 対してv-modelのデフォルトイベント名は、上記の表のように.syncと形式が異なったため、Vue 3ではv-modelにおいても同様の仕様に統一された形になります。 このような変更について学習し、変更する必要があります。

v3-migration.vuejs.org

プラグイン・ライブラリの選定

Vue 2で使用していたプラグインやライブラリの多くは、Vue 3に対応していないもの、もしくはVue 3に対応するために仕様変更されたものがあります。これにより、移行コストが高くなります。さらに、Vue 3に対応する代替ライブラリが存在しない場合もあります。
このような場合、開発者は自らライブラリを更新するか、あるいは代替手段を模索する必要があります。また、既存のライブラリとの互換性の問題も発生する可能性があります。

例えば、Nuxt 3.8でbootstrap-vue-next@0.14.10を導入すると、ビルドエラーが発生するという問題があります。このような問題に対処するためには、開発者は適切な代替手段を見つける必要があります。

Vue 2コンポーネントの移行コスト

今まで Vue 2で記述されていたコンポーネントを、すべて Vue 3 の記法へ変更する必要があります。 プロダクトの大きさにもよりますが、CCLMの場合300近くのコンポーネントがあり、これをすべて書き換えるとなると膨大な時間がかかることは想像に容易いと思います。
しかし、この効率をあげる方法に画期的なものはなく、現在有力なのはVue 2のプロジェクトをVue 2.7までバージョンを上げ、バックポートされているVue 3の機能を取り入れることです。

blog.vuejs.org

マイグレーション全体を通して

上記に書ききれなかった学びを簡単に記述します。

  • 公式推奨のプラグイン・ライブラリでもアプリケーションに合わないことがある。

    • ex) ストーリー記述としてStoryBookを推奨しているが、Viteでのエイリアス解決失敗とビルドエラーにより、Historeを採用
  • 思い切って既存のプラグイン・ライブラリを別のプラグイン・ライブラリに変更する
    • ex) BootstrapVueからPrimeVueに変更
  • 原因不明のビルドエラーが発生し、解消できない場合はプラグイン・ライブラリのissuesを確認する

さいごに

他にも多くの課題がありますが、今回は主要な課題を取り上げました。 マイグレーションは現在も進行中ですが、上記に上げたように多くの学びが有り、貴重な経験でした。

【Vue.js】Painless なコンポーネント開発のプラクティス

はじめに

こんにちは!ContractS株式会社の北原です。
フロントエンドのリードとして、プロジェクトの技術選定や品質保守等を担当しています。

早速ですが...

みなさまは Vue 3 をお使いになられていますか?
これには、以前の Vue 2 と比較して多種多様な改善がふんだんに盛り込まれています。
2020/09/18 にリリースされてから暫く経っているので、実際の業務や個人で触られた方も多いと思います。

例えば、

  • Composition API の正式採用
  • script setup 構文の対応
  • メモリ使用量削減による負荷軽減

その他色々ありますが、以上のような機能により、従来のメジャーバージョンからご利用になっていた開発者からすると嬉しいものが満載です。

しかし、ただひとつ。 頭を悩ませる要素があるのです。
それこそが本稿を作ったきっかけです。

:deep 機能について

Vue 3 には:deepという擬似クラスとして利用できる機能があります。
これは、コンポーネントから、子コンポーネントの特定要素に対してスタイルを当てたい時に避けては通れない道です。

:deep の具体的な使い方については、以下の公式ドキュメントをご覧ください
ja.vuejs.org

:deep 機能のつらいところ

Atomic Design程やらないまでも、多くのデザインシステムではatomsのように基底コンポーネントを作成し、各コンポーネントで使う形だと思います。

自社で全ての基底コンポーネントをスクラッチで作成し利用すれば、SFC で運用しても自由にスタイルをカスタマイズできるので問題ないですが、多くのプロジェクトでは VuetifyQuasar のようなUIライブラリをご利用になると思います。

また、そのままの見た目で利用することは少なく、UX統一等の理由でスタイルを適用すると思います。
当然ライブラリなので、日々バージョンアップが行なわれていきます。その際にコンポーネントのDOM構造が変化することもあるでしょう。

:deepは前述の通り擬似クラスであり、CSSセレクタである以上、UIライブラリのコンポーネントのDOMを知らないといけません。
方法としては様々あると思いますが、検証ツール等を用いてスタイルを当てたい要素を探し、その要素を一意に特定できるセレクタを作ってスタイルを当てる必要があります。

これを頻繁にメンテナンスするのは高コストで、コンポーネントに新規のバリエーションを持たせたい時にもスピーディな開発を阻害します。

そんなあなたに PrimeVue

PrimeVue は、Vue 3 で利用可能なUIライブラリのひとつであり、90をも超える多彩なコンポーネントと堅牢なFWを提供しています。
今回紹介する機能を抜きにしても、単品として高いアクセシビリティを誇るので、是非以下で触ってみてください。

primevue.org

PrimeVue の Pass Through 機能

Pass Through (パススルー) 機能は、ドキュメントで以下のように説明されています。

The Pass Through props is an API to access the internal DOM Structure of the components.

要するに、このAPIを利用することでPrimeVueのコンポーネント内部のDOM構造へアクセスが可能になるというわけです。
すでに:deepで感じていた pain を解決できる匂いがしますね。

Pass Through の利用方法

PrimeVueで提供される全てのコンポーネントには、ptという名称のpropsが用意されています。
そこに、PrimeVueのDOM毎の命名をkeyとして持つObjectをbindすることで、DOMのカスタマイズが可能です。

これだけだと分かりづらいと思うので、今回は Vue Button Component を具体例として挙げます。

ご覧いただくと、PASS THROUGHタブ内で、画像のようにDOM毎に連番が割振られていると思います。

PrimeVue Button の Pass Through 連番

ここで、4番のラベル部分のスタイルをカスタマイズしたいとします。
そのまま下にスクロールしていただくと、対象の連番に対して命名があると思います。

PrimeVue Button の Pass Through 命名

labelという命名がなされていますね。
これをkeyとした Object を、SFC上で以下のようにbindします。

PrimeVue Button の Pass Through Bind

Tailwind CSS のクラスであるfont-boldを適用してみました。

なんとこれだけで、PrimeVue Button のDOM構造を知らずともスタイルが適用できてしまいます。
画像のようにcomputedを bind することで動的なスタイル変更も可能です。
今回はTailwind CSSを利用しましたが、グローバルなUtility Classとも非常に相性がいいです。

最後に

いかがでしたでしょうか?

:deepで大変な目に遭われた、もしくは今まさに遭われている方もいらっしゃるかと思います。私も同じで、Nuxt 2からNuxt 3へマイグレーションを行なう際にとても痛い目を見ています...笑

もし良さそう!と感じていただけましたら、導入は以下で簡単に行なえますので、是非お試しください!

出典

グループ会社間で行なった勉強会の内容となります。
本稿よりも、もう少し実際の利用例や他機能も交えた解説を行なっていますので、よろしければご覧ください!