N通りのCSSの書き方

おはこんばんちは!面白プロデュース事業部 フロントエンジニアの中村です。

とうとう新卒ではなくなり、2年目になった今、入社する前に知りたかったな〜という記事を書きたいと思います。

はじめに

皆さんはCSSを勉強したことがありますでしょうか。

自分の中では、CSSはプロパティをいっぱい知っていて、それを適切なタイミングで適切なものを使えるようにする技術が必要なものだと思っています。

別に暗記してなくても、なんとな〜くこんなのあったな〜ぐらいで覚えていると、調べることができますからね。

しかしながら、よく使われるものや便利なものが優先的に知識としてたまり、意外と身近にあるのに知らないプロパティがあったりします。

例えば、要素をセンタリングするだけでもいろいろな書き方があり、調べていくうちに新たな発見があったりするのではないでしょうか。

今回はそういう感じで、レスポンシブデザインを作っていく中で便利な書き方をいくつか挙げていこうと思います。

レスポンシブデザインの形

近年ではレスポンシブデザインが当たり前のような世界になっていますので、CSS(ここではSCSSを使用)は以下のような書き方をします。

.class-name {
  /* PC用/共通のスタイルを書く */

  @include sp {
    /* SP用のスタイルを書く */
  }
}

SP用のスタイルを書くと、PC用のスタイルを上書きできるので、レスポンシブデザインが満たせるというものになっています。

ここでポイントとして、PCとSPで別々のスタイルを書く場面があるということです。

逆にいうと、共通部分の記述が多ければ多いほどSP用のスタイルの部分に書くものが減り、デザインの変更等に耐えやすいCSSが書けます。

また、不要な部分の記述が少なければ少ないほど、修正時のミスが減り、あれ?デザイン通りじゃないぞ?という時間を減らせます。

margin

最初は基本的でかつ、よく使われるmarginです。

一括指定プロパティ

まずは、右にマージンを当てたいとします。

PCでは10px、SPでは20pxを当てるとします。

.class-name {
  margin: 0px 10px 0px 0px;

  @include sp {
    margin: 0px 20px 0px 0px;
  }
}

これでも「右に指定されたマージンを当てる」という要件は満たせます。

このような、1つのプロパティで複数のプロパティを指定できるもの(ここではmargin)は 一括指定プロパティ と呼ばれます。

しかし、0pxは特に指定する必要がなく冗長ですし、 marginはクラスの当て方が悪いと上書きされる危険性があり、欲しいもの以外を書くメリットがあまりないです。

個別指定プロパティ

そこで、以下のような記述で先程の問題を回避できます。

.class-name {
  margin-right: 10px;

  @include sp {
    margin-right: 20px;
  }
}

これにより、マージンを右側だけに当てていることが一目でわかります。

ここで言う margin-right のようなプロパティを、 margin と対比して 個別指定プロパティ と呼ばれます。

好みが分かれる部分

ここで、左も同じ値を当てたいとします。

.class-name {
  margin-right: 10px;
+ margin-left: 10px;

  @include sp {
    margin-right: 20px;
+   margin-left: 20px;
  }
}

この様に修正をすることもできるんですが、少し冗長な気がします。

そこで、以下のように変えることも出来ます。

.class-name {
  margin: 0px 10px;

  @include sp {
    margin: 0px 20px;
  }
}

この様に書くと、1行で収まります。

しかしながら、指定しなくてもよい0pxを含めることになります。

そのため、上記2つはどちらを用いるかはレビュアーの匙加減になってしまいます。

ただ、その1行ずつを犠牲にして、0pxを含めるような書き方をするのを良しとするか、そのリスクと交換で1行に収めるメリットがあるかをよく考える必要があります。

border-radius

border-radiusも別々に指定することが出来ます。

それでは、まず四角の右上だけを丸くさせることを想定します。

半径をPCでは10px、SPでは20pxにすることにします。

そのまま素直に書いてみると、以下の通りになります。

.class-name {
  border-radius: 0px 10px 0px 0px;

  @include sp {
    border-radius: 0px 20px 0px 0px;
  }
}

これでも全然問題はないんですが、marginの時と同じく、指定する必要がないものがあります。

そういう時に、便利な個別指定プロパティを使いましょう。

.class-name {
  border-top-right-radius: 10px;

  @include sp {
    border-top-right-radius: 20px;
  }
}

こうすることで、わざわざ他のところを0pxと書かなくても、右上だけ半径を指定することが出来ました。

また、右上だけ丸くしているんだなと見ただけでわかるようになり、他の方がコードを見た時にも見やすくなりました。

border

borderのように、数値以外も色々指定できる一括指定プロパティもあります。

それでは、上下に赤色の1本線を引くことを想定します。

ちょっと太いですが、太さはPCでは10px、SPでは20pxにすることにします。

ただ、borderでは上下に指定することは出来ないので、border-topborder-bottomを使用します。

.class-name {
  border-top: 10px solid #ff0000;
  border-bottom: 10px solid #ff0000;

  @include sp {
    border-top: 20px solid #ff0000;
    border-bottom: 20px solid #ff0000;
  }
}

しかし、このままだと色を変更しようとした時、2つとも変更する必要があります。

前述でもあった通り、共通部分はできるだけまとめておいた方が修正が楽なので、以下のようにまとめてみます。

.class-name {
  border-color: #ff0000;
  border-style: solid;
  border-top-width: 10px;
  border-bottom-width: 10px;

  @include sp {
    border-top-width: 20px;
    border-bottom-width: 20px;
  }
}

こうすることで、PCとSPの差分は太さだけであることが明確にわかります。

気をつけないといけないこと

例えば全方位にmarginを当てたいとします。

当てる値は先ほどと同じとして、以下のように書いてみます。

.class-name {
  margin-top: 10px;
  margin-right: 10px;
  margin-bottom: 10px;
  margin-left: 10px;

  @include sp {
    margin-top: 20px;
    margin-right: 20px;
    margin-bottom: 20px;
    margin-left: 20px;
  }
}

こういった場合、わざわざ分ける必要がありません。

以下のようにまとめて書いた方がわかりやすくていいでしょう。

.class-name {
  margin: 10px;

  @include sp {
    margin: 20px;
  }
}

そのため、いかなる時も分けて書く必要があるわけではありません。

あくまで、不要な部分の記述を減らす目的で分けることにより、共通部分が増えたり、PCとSPでのスタイルの差分が見やすくなっていたということを忘れないでください。

また、一括指定プロパティと個別指定プロパティを一緒に書いてしまうと、プロパティの重複でのチェックが引っ掛からなくなるため、注意が必要です。

.class-name {
  margin-right: 30px;
  margin: 10px;

  @include sp {
    margin: 20px;
  }
}

こういった可読性の低下を防ぐため、あえて個別指定プロパティを全て禁止したり、逆に一括指定プロパティを禁止し、全てバラバラに指定するなんていう方針があったりします。

どちらとも利点と欠点がありますが、どちらかに統一した方が読みやすいのは確かです。

そのため、どちらの書き方も出来るようになっておいた方が、共同開発をする際に可読性の低いCSSを書かずに済むようになります。

まとめ

CSSは色んな書き方が出来ます。

特にそれぞれに利点と欠点がなく、答えがないのが事実です。

そのため、どの書き方にも対応出来ておいた方が仕事をする際に役に立つと思います。

今回はレスポンシブデザインにスコープを向けたCSSの書き方でしたが、他にも色んな書き方があるので、よければ調べてみてください!

カヤックでは、CSSを極めたいエンジニアも募集しています!

カスタマイズで広がるAWS Copilotの実践力

SREチームの橋本です。SRE連載の7月号になります。

カヤック社内では弊社藤原のecspressoをAmazon ECSのデプロイツールとして活用していますが、AWS公式のデプロイツールAWS Copilot(現在v1.29)もそのオールインワン的な性質から、開発・運営リソースが限られるプロジェクトでは選択肢に入るようになってきました。

今回はそのAWS Copilot活用のため、背後にあるAWS CloudFormationテンプレートをカスタマイズする手法を紹介します。

AWS CopilotとCloudFormation

AWS CopilotはECSなどのデプロイを簡単にするCLIツールですが、実態としてはManifestと呼ばれるYAMLの設定ファイルからCloudFormationテンプレートを生成し、各種リソースを作成・管理するものです。

AWS Copilotは内部的にCloudFormationを用いている

ECSでサービスを作るとき、典型的な構成としてはロードバランサーを前段に置くことになりますが、単純にこの2つを作成して構築完了とはなりません。 IAMロール、VPC、セキュリティグループなどなど多くの周辺リソースが必要となり、ロードバランサーに対してもターゲットグループやリスナーなどを設定します。 こうした手間をcopilotコマンド一つに集約できるのがAWS Copilotの利点と言えます。

ただ設定が簡単な分ある程度は決め打ちな部分もあり、例えばCloudFrontは環境につき1つだけといった制約もあります。(具体的には環境のcdnで設定します。) こうした部分をカスタマイズするときに有用なのがAddonやオーバーライドといった機能です。

例えばcopilot inittechblogアプリケーションを作成し、myserviceというLoad Balanced Web Serviceを作成、インストラクションに従ってtest環境も作成すると、以下の4つのCloudFormationスタックが作成されます。

  • techblog-infrastructure-roles
  • StackSet-techblog-infrastructure-*** (末尾にハッシュ的な文字列が付くようです)
  • techblog-test
  • techblog-test-myservice

1つ目はCloudFormation自身が使うためのIAMロール、2つ目は環境間で共通のリソース(KMSキー、成果物を置くS3など)で特に触ることはないので、カスタマイズというと3つ目の環境、4つ目のサービスに関するスタックへ変更を加えることになります。

Addonでリソースを追加する

Addon機能では、単にCloudFormationテンプレートを書くことで追加のリソースを定義することができます。

環境のディレクトリ下のaddons/形式に従うテンプレートを置くと、環境のデプロイ時に一緒にAddonのデプロイが行われます。

例えばCloudFrontディストリビューションを追加で配置する場合、以下のようなテンプレートを書くことになります。

Parameters:
  App:
    Type: String
  Env:
    Type: String

Resources:
  MyCloudFrontDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        DefaultCacheBehavior:
          AllowedMethods:
            - HEAD
            - GET
          CachedMethods:
            - HEAD
            - GET
          CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6 # CachingOptimized
          Compress: true
          OriginRequestPolicyId: 216adef6-5c7f-47e4-b989-5492eafa07d3 # AllViewer
          TargetOriginId: s3
          ViewerProtocolPolicy: redirect-to-https
        Enabled: true
        Origins:
          - Id: s3
            DomainName: 'example-bucket.s3.ap-northeast-1.amazonaws.com'
            OriginPath: ''
            OriginAccessControlId: !GetAtt MyDistributionOriginAccessControl.Id
            S3OriginConfig:
              OriginAccessIdentity: ''
            ConnectionAttempts: 3
            ConnectionTimeout: 10
  MyDistributionOriginAccessControl:
    Type: AWS::CloudFront::OriginAccessControl
    Properties: 
      OriginAccessControlConfig: 
          Name: example-bucket
          OriginAccessControlOriginType: s3
          SigningBehavior: always
          SigningProtocol: sigv4

これでcopilot env deployすると、内部的には環境のCloudFormationスタックの対してネストされたスタックが作成されます。

Addonはネストされたスタックになる

またv1.27からは差分のプレビュー機能が追加され、deploy --diffによりCloudFormationテンプレートの差分が分かるようになりました。

$ copilot env deploy --diff
Only found one environment, defaulting to: test
~ Resources:
    + AddonsStack:
    +     Metadata:
    +         'aws:copilot:description': 'A CloudFormation nested stack for your additional AWS resources'
    +     Type: AWS::CloudFormation::Stack
    +     Properties:
    +         Parameters:
    +             App: !Ref AppName
    +             Env: !Ref EnvironmentName
    +         TemplateURL: https://stackset-techblog-infras-pipelinebuiltartifactbuc-***.s3.ap-northeast-1.amazonaws.com/manual/addons/environments/***.yml

  Continue with the deployment? (y/N)

これは環境testのスタックにネストされたスタックが追加されるという内容で、TemplateURLのファイルを見るとネストされたスタック(Addonに当たる部分)の内容も確認できます。 この場合上のYAMLと同内容になります。

単に追加のリソースが作られるのが基本ですが、ワークロードのAddonの場合、Outputs:に以下のようなリソースを記述するとECSタスクロールやECSサービスなどに付与することができます。(ドキュメント参照)

  • IAMポリシー
  • セキュリティグループ
  • シークレット
  • 環境変数

元々ワークロードに対してサポートされていたAddonは、v1.25で環境に対してもサポートされ、例えば複数のサービスで利用するS3といったリソースがより自然に扱えるようになりました。(またv1.29ではパイプラインにもサポートされました。) なおcopilot storageというコマンドがあり、これによりRDSやS3をワークロードのAddonであったり、環境のAddonとしても定義することができます。

オーバーライドで生成されるテンプレートを書き換える

例えばCloudFrontディストリビューションへのCloudFront Functions(あるいはLambda@Edge)の紐づけは今のところサポートされていません。 CloudFrontディストリビューションはManifestから生成されるリソースのため、Addonでは触れませんが、v1.27で追加されたoverrideコマンドによりこうしたケースにも対応が可能となりました。

オーバーライドはCDK(AWS Cloud Development Kit)とYAMLパッチの両方に対応しています。ここでは特に複雑なことをしないので、よりシンプルなYAMLパッチによるオーバーライドを試してみます。

まず再びAddonsで、CloudFront関数を用意します。(IPを返すだけのレスポンスに書き換えてしまうので実用性はありませんが、動作確認は簡単にできます)

Parameters:
  App:
    Type: String
  Env:
    Type: String

Resources:
  RewriteFunction:
    Type: AWS::CloudFront::Function
    Properties:
      AutoPublish: true
      FunctionCode: !Sub |
        function handler(event) {
          var request = event.request;
          var clientIp = event.viewer.ip;
          return {
            statusCode: 200,
            statusDescription: '200 OK',
            body: clientIp
          };
        }
      FunctionConfig:
        Comment: !Sub 'Respond with the client IP address'
        Runtime: cloudfront-js-1.0
      Name: !Sub "${AWS::StackName}-RewriteFunction"

Outputs:
  RewriteFunctionARN:
    Description: 'The ARN of the CloudFront function that rewrites the request'
    Value: !Ref RewriteFunction
    Export:
      Name: !Sub ${App}-${Env}-RewriteFunctionARN

最後のExportによってOutputsの値が他のスタックからも利用可能になります。なお名前空間はリージョン単位なので、${App}-${Env}-という風にプレフィックスを付けることが推奨されています。

この関数をCloudFrontディストリビューションに紐づけます。対象となるディストリビューションは、例えばcopilot env packageで環境のCloudFormationテンプレートを出力すると、以下のように見つけることができます。

Resources:
    ……
    CloudFrontDistribution:
        Metadata:
            'aws:copilot:description': 'A CloudFront distribution for global content delivery'
        Condition: CreateALB
        Type: AWS::CloudFront::Distribution
        Properties:
            DistributionConfig:
                DefaultCacheBehavior:
                    AllowedMethods: ["GET", "HEAD", "OPTIONS", "PUT", "PATCH", "POST", "DELETE"]
                    CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad # See https://go.aws/3bJid3k
                    TargetOriginId: !Sub 'copilot-${AppName}-${EnvironmentName}-origin'
                    OriginRequestPolicyId: 216adef6-5c7f-47e4-b989-5492eafa07d3 # See https://go.aws/3BIE8CP
                    ViewerProtocolPolicy: allow-all
                ……

CloudFront関数を紐づけるには、DefaultCacheBehavior下にFunctionAssociationsの項目を追加します。具体的には以下のようなYAMLパッチでオーバーライドします。

# copilot/environments/overrides/cfn.patches.yml
- op: add
  path: /Resources/CloudFrontDistribution/Properties/DistributionConfig/DefaultCacheBehavior/FunctionAssociations
  value:
    - EventType: viewer-request
      FunctionARN:
        Fn::ImportValue:
          !Sub ${AppName}-${EnvironmentName}-RewriteFunctionARN

あとはデプロイするだけです。

$ copilot env deploy --diff
Only found one environment, defaulting to: test
~ Description: CloudFormation environment template for infrastructure shared among Copilot workloads. -> CloudFormation environment template for infrastructure shared among Copilot workloads using AWS Copilot with YAML patches.
~ Resources/CloudFrontDistribution/Properties/DistributionConfig/DefaultCacheBehavior:
    + FunctionAssociations:
    +     - EventType: viewer-request
    +       FunctionARN:
    +         Fn::ImportValue: !Sub ${AppName}-${EnvironmentName}-RewriteFunctionARN
Continue with the deployment?

便利ですね!

終わりに

AWS CopilotはVPC周りなどの細かい手間が減るので個人的に好きなツールでしたが、最近のアップデートによって大きくカスタマイズ性が向上し実用性が高まりつつあると感じています。 たとえ今AWS Copilotを使っていない、という方にとってもこの記事でその高まりの片鱗を感じて頂ければ嬉しく思います。

カヤックではツールのアップデート情報が大好きなエンジニアを募集しています!

hubspot.kayac.com