いがにんのぼやき

WEBエンジニアのブログ。IT、WEB、バンド、アニメ。

ParcelとFirebase Hostingで簡単サイト公開

ParcelとFirebase Hostingを使ってみて今はこんなに簡単にサイトが作れるんだと感動したので紹介。

先週こんなサイトを公開した。

reiwa-count-up.web.app

ただの悪ふざけで作ったサイト。
平成のカウントアップなのにreiwa-count-upというサブドメインにしたり。
ちょうど令和100日目にこんなツイートをして公開した。

このサイトはParcelとFirebase Hostingを使用して開発している。

parceljs.org

firebase.google.com

Parcelすっごく簡単

Parcelは簡単に言ってしまえばWebpackの簡易版。
指定のHTMLをエンドポイントにHTML、CSS、JSを良しなにまとめてくれる。
例えば以下のHTMLをparcelでビルドするとmain.jsを main.4217b2e0.jsみたいな感じのファイルとして生成してリンクを張り替えてくれる。

<html>
<body>
<script src="main.js"></script>
</body>
</html

JS内での他ファイルのimportももちろん対応している。

import './style.css';
import hoge from './hoge';

hoge();

試していないがDynamic importsなんかも対応しているとのこと。

npm install --save-dev parcel-bundlerでインストールしnpm scriptでparcel index.html で開発用のサーバーが立ち上がる。
標準はdistというディレクトリに出力される。これは-dオプションで出力ディレクトリを変更可能。
開発ではなく本番用のビルドをするにはparcel build index.html

Parcelの気になったところ

Mochaとの組み合わせ。
テストをするときにParcelでビルドされたものをテストする方法が分からなかった。
なのでテストファイルに書いたimportがそのままでは解決できず、Parcelでは必要ない.babelrcを設定する必要が出た。
こんな感じのnpm scriptを用意して、.babelrcを用意してテストを走らせる必要があった。

"test": "cross-env NODE_ENV=test mocha --require babel-register src/js/*.spec.js",

{
  "presets": ["es2015"]
}

あとは特定の画像などをハッシュ付きにしないで出力ディレクトリに出力する方法が分からなかった。
コピープラグインなどあるようだが、Parcelだけだと出来なそう?
SNS用のOGP画像などは https://reiwa-count-up.web.app/hei.png といったメインでの絶対パス指定になるのでhashがついたパスで出力されるとそこがうまく解決できなかった。
なので同じ画像を出力先ディレクトリに保存しておく運用にしている。
buildした後はhei.pnghei.[hash].pngが存在する形になっている。 これはsitemap.xmlなども同じで出力ディレクトリに保存している。

Parcelについてもっと知りたかったらこの記事を見るといいかも。
よくまとまっている。

qiita.com

細かい設定が出来ないが、すごくシンプルなのでプロトタイプや小規模なものをサクッと作るのに良さそう。

Firebase Hosting色々便利

ドメインもらえる!

Firebase自体もちゃんと使うのは初めてで、Hostingを使ってみたがこれがすごい。
まずドメインが貰える。
正確には[プロジェクト名].web.app[プロジェクト名].firebase.comというドメインをもらうことが出来る。
しかもHTTPSにも対応しているのでそのまま使うことが出来る。

f:id:igatea:20190813015853p:plain

凄くいいなと思ったのはxxx.web.appというドメインが設定できること。
なんかアプリ感あるドメインぽくてよくない?
今までHerokuもそうだけどPaaSの製品名がドメインに入っているのが普通だったけど、ちゃんと普通のサイトっぽいドメインをそのまま使えるのはいい。
最初は独自ドメイン当てようかなと思ってたんだけどこんなちょっとしたサイトでドメイン買うのもなーと思っていたら、このドメインだったのでこれはいいと思ってそのまま使った。

CLIでデプロイ簡単、バージョン管理も

プロジェクトディレクトリでfirebase initコマンドを叩くとfirebaseプロジェクトとして設定を行うことができる。
どの機能を使うか(HostingやFirestoreなど)、プロジェクト名などの設定が簡単にできる。
設定後はfirebase deployで即デプロイが可能。
設定ファイルも以下のような数行で終わる程度。

{
  "hosting": {
    "public": "dist",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    "rewrites": [
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]
  }
}

デプロイするとバージョニングがされ、指定日時にロールバックも可能。 f:id:igatea:20190813020857p:plain

いや~、本当に便利だ。

値段も相当バズったり動画や画像を置きまくらない限り無料プランで完全に賄える。

f:id:igatea:20190813021739p:plain

余談

Google Search Console反映遅すぎ。
3週間くらいかかった。
sitemap.xmlの反映がうまくいってなかったのが原因かもしれないけど。

JavaScriptで指定のスタイルが当たっている要素を探す

JavaScriptで指定の要素に適用されているスタイルや、特定のスタイルが適用されている要素を探したいときってあるよね。
そんなときに簡単にとろうと思ってJSでコードを書いてみると、

document.getElementById('hoge').style

こんなコード書けばとれるんじゃないかと。
で、オブジェクトが返ってくるので中身を見てみるとCSSに書いたスタイルが取れてないんですよ。
この記法だと要素に直接記述されたスタイルしか取れない。

結論、要素に書いたスタイル、それ以外のスタイルも含めて要素に適用されているスタイルをとるならgetComputedStyleを使おうぜって話。

developer.mozilla.org

const elm = document.getElementById('hoge');
const styles = getComputedStyle(elm);

これでCSSも含めてすべてのスタイルが適用された要素のスタイルがCSSStyleDeclarationという型のオブジェクトで返ってくる。
スタイルはそれぞれケバブケースからキャメルケースにした名前でその値が取得できる。

const elm = document.getElementById('hoge');
const style = getComputedStyle(element).backgroundColor; // rgb(0, 0, 0)

f:id:igatea:20190707004417p:plain

注意として色をblack#000としている場合にrgbに変換された値で出てくるので比較するならそれを配慮しなければいけない。

指定の要素から特定のスタイルが当たっている先祖要素を探す

再帰的に検索してあげればいい。
子孫要素から探すなら逆に再帰関数を書けばいける。

/**
 * @param {HTMLElement} element
 * @param {string} styleName
 * @param {string} styleValue
 */
function findHasStyleAncestorElement(element, styleName, styleValue) {
  if (!element) return null;
  const style = getComputedStyle(element)[styleName];
  if (style === styleValue) return element;
  return findHasStyleAncestorElement(
    element.parentElement,
    styleName,
    styleValue
  );
}

const elm = document.getElementById("c");
const blackBackgroundElm = findHasStyleAncestorElement(
  elm,
  "backgroundColor",
  "rgb(0, 0, 0)" // blackではない
);

Dapperでトランザクションを使用、トランザクション分離レベルを変更する方法のメモ

Dapperでトランザクションの分離レベルを変更するときにどういう挙動をするのかよく分かっていなかったので調べたメモ。
ついでにトランザクション周りのSQLも確認。

今回の環境

今回試しているのはSQLServerだが他のRDBでも方言の違いはあれど基本的に同じなはず。

SQL

まずSQLの確認から。

トランザクションの分離レベルの設定。 Serializableであれば下記。

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE

docs.microsoft.com

分離レベル オプションは一度に 1 つだけ設定でき、設定したオプションは明示的に変更されない限り、その接続で継続的に使用されます。 ステートメントの FROM 句内にあるテーブル ヒントで、テーブルに対して別のロック動作やバージョン管理動作が指定されない限り、トランザクションのすべての読み取り操作は、指定した分離レベルのルールに従って実行されます。

公式ドキュメントを見ると接続単位でトランザクション分離レベルが決まるらしい。

実際に今の分離レベルを確認するなら。

DBCC USEROPTIONS

docs.microsoft.com

このSQLを実行するとisolation levelの項目に今の分離レベルが表示される。

f:id:igatea:20190421204934p:plain

SSMSで確認すると切断、再接続をしてもずっと変更後の分離レベルになっているのでここらへんの挙動ははっきりわからない。
恐らくセッションが生きているようで、SSMS自体を再起動すると規定値に戻る。

MySQLで分離レベルを確認する場合はこんな感じらしい。(未検証)

SELECT @@GLOBAL.tx_isolation, @@tx_isolation;

実際に分離レベルを設定してトランザクションを張るにはBEGIN TRAN、コミットはCOMMIT TRAN。

BEGIN TRAN
    /****** 実行する処理  ******/
COMMIT TRAN

実際にはSQLで全部やるならTRYCATCHしてROLLBACKの処理が入るだろう。

Dapper

次にアプリケーション側でどう書くか。
今回はDapperを使って書いてみる。
まずはusingでSqlConnection.BeginTransactionを呼び出す。
これでトランザクションを張ることが出来る。
BeginTransactionは第一引数で分離レベルを設定することが出来る。

docs.microsoft.com

実際に実行するコードはこんな感じに。 このSQLだと分離レベルを変える意味はないがご愛嬌。

using (var tran = conn.BeginTransaction(IsolationLevel.Serializable))
{
    var sql1 = @"
INSERT INTO Samples ( Id, Name )
VALUES ( @id, @name )";
    var sql2 = @"
INSERT INTO Samples ( Id, Name )
VALUES ( @id, @name + 'duplicate' )";

    try
    {
        await conn.ExecuteAsync(sql1, new { id = sample.Id, name = sample.Name }, tran);
        await conn.ExecuteAsync(sql2, new { id = sample.Id, name = sample.Name }, tran);
        tran.Commit();
        return true;
    }
    catch (SqlException e)
    {
        // log出力とか
        return false;
    }
}

usingでBeginTransactionを囲っておけばこのコードが終わった時にコミットされていなければロールバックしてくれる。
ここで分離レベルを指定しなかった場合、Dapperで実行するとそのコネクションプールが保持されている限り、その接続で前に使用されたトランザクションレベルが使用されてしまう。
・・・と思ったけどDBCC USEROPTIONS の結果は固定化されているが using (var tran = conn.BeginTransaction()) したときは一番最初のトランザクションレベルとなっている。

公式ドキュメントにはこのように記載があった。

分離レベルを指定しない場合は、既定の分離レベルが使用されます。 分離レベルを指定する、BeginTransactionメソッドを受け取るオーバー ロードを使用して、isoパラメーター (BeginTransaction)。 トランザクションの分離レベルは、接続が終了または破棄されるまで、トランザクションが完了した後に永続化します。 分離のレベルを設定スナップショットsnapshot 分離レベルが有効になっていないデータベースではない例外をスローします。 既定の分離レベルを使用して、トランザクションが完了します。

日本語訳が変だけど、以下も併せて読んでみるとどうやらREAD COMMITTEDがSQLServerでのデフォルトでになっており、それが使用されるらしい。

stackoverflow.com

ので、分離レベルはこんな感じに切り替わる。

using (var conn = CreateConnection())
{
    var t1 = conn.Query("DBCC USEROPTIONS"); // 前のトランザクションレベルが表示される
 
     conn.Execute("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE"); // BeginTransactionでは意味なし
 
    using (var tran = conn.BeginTransaction())
    {
        var t2 = conn.Query("DBCC USEROPTIONS", new { }, tran); // READ COMMITTEDが表示、既定のトランザクションレベル

        var sql1 = @"
INSERT INTO Samples ( Id, Name )
VALUES ( @id, @name )";
        var sql2 = @"
INSERT INTO Samples ( Id, Name )
VALUES ( @id, @name + 'duplicate' )";

        try
        {
            await conn.ExecuteAsync(sql1, new { id = sample.Id, name = sample.Name }, tran); // READ COMMITTEDで実行される
            await conn.ExecuteAsync(sql2, new { id = sample.Id, name = sample.Name }, tran);
            tran.Commit();
            return true;
        }
        catch (SqlException e)
        {
            // log出力とか
            return false;
        }
    }
    }
}

trasactionの引数でちょっとした罠があり、パラメータがなくてもQueryAsyncの第二引数に匿名オブジェクトを入れて、第三引数にトランザクションを入れなければいけない。
そうしないとIDbTransactionをオブジェクトとして認識してただのクエリに入れるパラメータと判断し、以下のエラーが出る。

InvalidOperationException: ExecuteReader requires the command to have a transaction when the connection assigned to the command is in a pending local transaction. The Transaction property of the command has not been initialized.

分離レベルを変えて、Serializeレベルで行うならこんな感じになる。

using (var conn = CreateConnection())
{
    var t1 = conn.Query("DBCC USEROPTIONS"); // 前のトランザクションレベルが表示される

    using (var tran = conn.BeginTransaction(IsolationLevel.Serializable)) // 変わる箇所
    {
        var t2 = conn.Query("DBCC USEROPTIONS", new { }, tran); // Serializableが表示

        var sql1 = @"
INSERT INTO Samples ( Id, Name )
VALUES ( @id, @name )";
        var sql2 = @"
INSERT INTO Samples ( Id, Name )
VALUES ( @id, @name + 'duplicate' )";

        try
        {
            await conn.ExecuteAsync(sql1, new { id = sample.Id, name = sample.Name }, tran); // Serializableで実行される
            await conn.ExecuteAsync(sql2, new { id = sample.Id, name = sample.Name }, tran);
            tran.Commit();
            return true;
        }
        catch (SqlException e)
        {
            // log出力とか
            return false;
        }
    }
}

実行したソースはこちら。

https://github.com/igayamaguchi/OrmExample/tree/master/InsertDuplicate

調べている上で知ったこと

SQLServerへの接続を切る方法。

クエリ発行をするセッションは切れない。

docs.microsoft.com

切断。

KILL [セッションID]

セッション確認。

 SELECT conn.session_id, host_name, program_name,
     nt_domain, login_name, connect_time, last_request_end_time 
 FROM sys.dm_exec_sessions AS sess
 JOIN sys.dm_exec_connections AS conn
    ON sess.session_id = conn.session_id;

プライマリーキー、一意性違反の検知

SqlExceptionのNumberに2627という数字が入る。
これはSQLServerの重複違反のエラーコード。
使用するデータベースによって数字は異なる。

public enum CreateResult
{
    Success,
    Duplicate
}

try
{
    await conn.ExecuteAsync(sql, new { id = sample.Id, name = sample.Name }, tran);
    tran.Commit();
    return CreateResult.Success;
}
catch (SqlException e) when (e.Number == 2627)
{
    // log出力とか
    tran.Rollback();
    return CreateResult.Duplicate;
}

ORMの使う使わないについて

  • とあるコミュニティでORMを使う使わない、使い方の議論があった
  • 自分の考えを整理するためにも今自分がこうしたほうがいいなって運用方法を書き出してみる
  • 個人の好みによるものが大きいので一意見として
  • ここではサーバーアプリケーションでの文脈

ORMってそもそも何

前提知識としてORMとは

  • インピーダンスミスマッチを解消するもの
  • インピーダンスミスマッチとは
    • 概念モデルと論理モデルの違いを埋める
    • アプリケーションの要件にそった概念モデル(Entity)は、論理モデル(テーブル)と1対1になるとは限らない
    • こういったDB側の都合をアプリケーションで意識しないで済むようになるもの

ORMを使うか使わないか

でもDB側の都合を意識しないで済むって言うけど結局パフォーマンスとか考えたら意識しなくちゃいけないよね?
で、ORM使ってWhereとか書き始めたらそれSQL書いているだけだよね?ORMいらなくない?
っていうのが今回のORM使う使わない問題の根幹としてあると思う

サーバーアプリケーションからDBの値を引く

サーバーアプリケーションからプログラムを書いて、DBから値を読み込み、アプリケーションで使用する、その方法は大体こんな感じだと思う

  • SQLを直接書いてORMを用いずマッピング
  • ORMを使用するパターン
    • ORMの機能(クエリビルダーなど)を使用する
    • ORMとSQLを組み合わせる

個人的意見

個人の好みによる ORMでクエリビルダーを有効活用するのが自分の最適解

SQLを直接書いてORMを用いずマッピング

ここからは各方法を実際のコード(C#)で良いところ、悪いところを挙げていく
ORMを使用しないマッピングの場合、

  • SQL書く
  • 何かで実行、結果は配列みたいなものに入る
  • 自分で毎回マッピング
 var sql = $@"
 SELECT
  USER.NAME
  ARTICLE.TITLE
 FROM
  USER
 INNER JOIN
  ARTICLE ON ARTICLE.ID = USER.ID
 WHERE
  ID = @id
 ";
 
 return Connection.Query(sql, new { id });
 
 // 上の呼び出し元
 foreach(result in results)
 {
    var name = user["Name"]
    var title = user["Title"]
    
    // 何かの処理
 }

良いところ

  • クエリを直接かけるのでハイパフォーマンス
  • 特別構文解釈の負荷もかかりにくい

悪いところ

  • 取り回しが悪い

そのままSQLからしっかりデータベースのことを考えてチューニングできるのでいい
でもビジネス要件に耐えられるものではないかなあと思ってしまう
上記は単純なSQLだが複雑度が増し、その複雑度の増したSQLが画面やAPIによって若干の条件が変わるごとに同じSQLを書くことになったりして辛くなるのでは
サーバーアプリケーションにおいてはこれを用いることによって行えるパフォーマンス向上も一般的なサーバーアプリケーションでは早すぎる最適化なのではと思う

ORMとSQL書くパターン

前の直SQLだけの場合と比べるとマッピングが追加されただけ

 var sql = $@"
 SELECT
  USER.NAME
  ARTICLE.TITLE
 FROM
  USER
 INNER JOIN
  ARTICLE ON ARTICLE.ID = USER.ID
 WHERE
  ID = @id
 ";
 
 // Userクラスにマッピングされる
 return Connection.Query<User>(sql, new { id });

良いところ

  • クエリを直接かけるのでハイパフォーマンス

悪いところ

  • 取り回しが悪い

これも前述の取り回しの問題が解決できない

ORMでクエリビルダーで書くパターン

C#だとメソッドで書くパターンとクエリ式というもので書くパターンがある

  // メソッド形式
 var result1 = _context.Users
                .Join(
                _context.Articles,
                user => user.ID,
                article => article.UserId,
                (user, article) => new { user, article })
                .Where(m => m.user.ID == id)
                .Select(m => new { m.user.Name, m.article.Title }).ToArray();
 
 // クエリ式
 var result2 = (from user in _context.Users
                join article in _context.Articles
                on user.ID equals article.UserId
                where user.ID == 1
                select new { user.Name, article.Title }).ToArray();
  

良いところ

  • (静的解析であれば)補完が効く
  • 通化、拡張がしやすい

悪いところ

  • 独自構文、メソッドを覚えなくてはいけない
  • 発行されるSQLを把握しとく必要がある

これのいいところは取得するカラムの絞り込みを後から行いやすかったり、Whereの共通化、追加を行いやすい
JoinやWhereの部分を拡張メソッドとして切り出してあげることも可能

  public static IQueryable<User> HasArticle(this IQueryable<User> query)
 {
     return query.Where(user => user.Articles.Any());
 }
 
 var result = _context.Users
                .HasArtice()
                .ToArray();

みたいな
ここらへんはORMによりけり
C#のクエリ式はSQLそのままに近く、かつプログラミング言語の機能を生かせるのでかなりいいと思っている
リッチなORMであれば上記のようなクエリビルダーを使うこともないかもしれない
ここらへんは後でもう少し深堀したいところ

Vue.js スタイルガイドのすヽめ

概要

この記事は Vue.js #3 Advent Calendar 2018 の 10 日目の記事です。

この記事ではVue.jsのスタイルガイドを今一度皆さんにすすめたいと思います。

jp.vuejs.org

Vue.jsのスタイルガイドって?

Vue.jsでコードを書くときに命名や記述順序など細かい設計で議論を呼ばないように公式で公開されているスタイルガイドです。
HTML、JavaScriptのスタイルの言及ではなくVue.jsのスタイルの言及のみを行っています。
例えば、これから紹介するコンポーネントのファイル名だったりプライベートなプロパティ名の命名規則なんかが明記されています。

現実的なことが書いてある

このスタイルガイドは細かいことは議論をスキップできるという意味でいいのものです。
加えて特にいいポイントが、公式が現実的な解を示しているというところです。
この現実的な解というのは例えば昨今のエディタを意識していたりといったことです。

例えば 密結合コンポーネントの名前 の詳細な説明をみて欲しいです。

このスタイルは親コンポーネントと密結合した子コンポーネントは、親コンポーネントの名前をプレフィックスとして含めるというものです。
その中で以下のような記述があります。(一部省略)

この問題を、子コンポーネントを親コンポーネントの名前を元に命名したディレクトリの中に入れることで解決したいと思うかもしれません。
例えば:

components/
|- TodoList/
   |- Item/
      |- index.vue
      |- Button.vue
   |- index.vue

これは推奨されません。以下のような結果を生むからです:
同じような名前のファイルがたくさんできてしまい、コードエディタ上で素早くファイルを切り替えるのが難しくなります。
ネストしたサブディレクトリがたくさんできてしまい、エディタのサイドバーでコンポーネントを参照するのに時間がかかるようになります。

これの正しい例としては下記のようになります。

components/
|- TodoList.vue
|- TodoListItem.vue
|- TodoListItemButton.vue

このスタイルガイドではこのように、現代のエディタに合わせて開発をしやすいように現実的な解答をしていたりします。

また、コンポーネント名における単語の順番 にあるようにコンポーネント名はそのコンポーネントとして重要な単語を最初にするということも書いてあります。

例えば検索関連のコンポーネントでは、以下のような自然言語のようなコンポーネント名ではなく

components/
|- ClearSearchButton.vue
|- ExcludeFromSearchInput.vue
|- LaunchOnStartupCheckbox.vue
|- RunSearchButton.vue
|- SearchInput.vue
|- TermsCheckbox.vue

以下のように何に関するコンポーネントなのかを最初に明記するということを推奨しています。

components/
|- SearchButtonClear.vue
|- SearchButtonRun.vue
|- SearchInputExcludeGlob.vue
|- SearchInputQuery.vue
|- SettingsCheckboxLaunchOnStartup.vue
|- SettingsCheckboxTerms.vue

一般的にエディタではファイルはアルファベット順に並ぶので、コンポーネント間のあらゆる重要な関連性はひと目ではっきりと分かります。

こういった現実的な解を定義しています。

他に

といったことが定義されています。
一部はeslintのプラグインを入れることで自動化が可能なので導入をおすすめします。

github.com

これで快適なVue.jsライフを!

Vue CLIのvue serveが便利

Vue CLIを触っていたらこんなものがあることを知らなかった。
単体でVueのコンポーネントを試したりできるらしい。

cli.vuejs.org

手順としては簡単。

  • npm install -g @vue/cli
  • npm install -g @vue/cli-service-global
  • vue serve [実行したい.vueファイル]

Vue CLIの基本的な設定(webpackやbabel,eslintなど)が適用されてビルドされている。
地味に -o オプションでいちいちコピペしなくてもブラウザが自動で開くか選択できるとか気が聞いている。

だからVueの構文試したいなとかなったらすぐに作ってすぐに試してが出来て便利。
こんな風にコンポーネントを作って。

App.vue

<template>
  <div>
    <h1>Hello!</h1>
    <ul>
      <list-item v-for="item in list" :value="item"/>
    </ul>
  </div>
</template>
<script>
import ListItem from './ListItem.vue';

export default {
  components: { ListItem },
  data() {
    return {
      list: [1,2,3]
    };
  },
};
</script>

ListItem.vue

<template>
  <li>{{ value }}</li>
</template>
<script>
export default {
  props: {
    value: {
      required: true,
      type: Number,
    },
  },
};
</script>

そのまま vue serve -o とするとApp.vueが実行されたブラウザが開かれる。

$ vue serve -o
 INFO  Starting development server...
 98% after emitting                                                           t
 DONE  Compiled successfully in 1146ms                                  11:38:35


  App running at:
  - Local:   http://localhost:8080/
  - Network: http://192.168.2.108:8080/

  Note that the development build is not optimized.
  To create a production build, run npm run build.

JSも実行できるのでVue RouterとかVuexも使えそうかな?
用途としてはコンポーネント単体を試すくらいがちょうどいいかも。

.Net Core MVCでTag Helperを自作する

docs.microsoft.com

.Net Core MVCのRazorではHTML Helperとは別にTag Helperというものが使えるようになっている。
そのTag Helperを自作したときのメモ。

作り方

public class PriceTagHelper : TagHelper
{
    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        output.TagName = "span";
    }
}

実質これだけ。
これでView側のcshtmlに@addTagHelper *,[アセンブリ名]と記述するだけで使用できる。
ちゃんと公式に書いてあるんだけど勝手にここを名前空間を指定するものと思ってハマった。

こんな感じ。

@addTagHelper *,SampleCoreWeb
<price>10000</price>

出力結果

<span>10000</span>

カスタマイズ

上記はただタグを差し替えているだけなのでこれだけじゃ使う意味はない。
なのでカスタマイズしていく。

例えばお金を扱うことにしよう。
よくある要件でお金の値を3桁ごとのカンマ区切りにして末尾に円を付けるとする。
実際どうするかって言うとこうなる。

public class PriceTagHelper : TagHelper
{
    public decimal Value { get; set; }

    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        output.TagName = "span";
        output.TagMode = TagMode.StartTagAndEndTag;
        
        var yen = Value.ToString("C").Substring(1);
        output.Content.SetContent($"{yen}円");
    }
}
<price value="10000" />

出力結果

<span>10,000円</span>

output.TagMode = TagMode.StartTagAndEndTagというのは<hoge />のような開始終了タグが一つになっているときに、HTMLでは開始終了タグを別にそれぞれ出力するために指定している。
TagHelperを継承したクラスにプロパティを宣言するとそのプロパティ名のケバブケースを属性として扱うことができる。
なのでここではvalue属性を使ってdecimalの値を入れることが出来る。

開始終了を単一のタグにするか分けるのか

今回のような値を入れて終わりという要件であれば開始終了が単一のタグの形式でいいのかなと思う。
開始終了のタグが別である場合というのは、中に文章など落とし込みたい時などで使えるのかな。
一応さらっと今回のケース(カンマ区切りはしてないけど)も載せておく。

public class PriceTagHelper : TagHelper
{
    public decimal Value { get; set; }

    public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
    {
        output.TagName = "span";
        var content = await output.GetChildContentAsync();
        output.Content.SetContent($"{content.GetContent()}円");
    }
}
<price>10000</price>

カンマ区切り以外は同じ結果が出力される。

<span>10,000円</span>

単一のタグに属性を付与する利点

  • 値を複数付与できる
  • モデルを入れたりも可能
  • 補完が効く

大体属性をうまく使ったほうが利点が大きい。
そもそも組み合わせることも可能なのでうまく使い分けたり組み合わせていきたいところ。

属性の補完が効くのはとてもいい。

f:id:igatea:20180531002111p:plain