CSSのソリューションバトル:JS対CSSのモジュール対SASS


現代のフロントエンドの開発、特に反応では、Vueではより少ない範囲に、これらは私たちのコンポーネントのためのCSSを書くためのさまざまな方法です.
この記事では、私はサイドバイサイドの比較を行いません.私はプロジェクトの「CSSアーキテクチャ」を決定する人の立場にある状況で、私がガイドしている特定の解決策の興味深い特徴と問題を強調します.

SASS ( CSS , CSSプリプロセッサ)


SASS(さらにCSS)は簡単に学ぶことは非常に維持するのは難しいです.それはどういう意味ですか.
純粋なCSSの主な問題は、彼はコンポーネントごとにスタイルの分離の問題を解決しないでください.そして、あなたのすべてのスタイルは他の構成要素に漏れます、そして、これは大きなプロジェクトで多くの問題を引き起こします.
ええ、この問題は世界と同じくらい古くなっていて、私たちにはこの問題を解決する方法が違います.

  • BEM 方法論
  • BEM paired with Atomic Design
  • 他の解決策は、原子CSS、SMASS、等.
  • しかし、この解決策のすべては、それだけのメトリドネスです、これは開発者が考える必要性を取り除きません、そして、これは我々がまだ野蛮な人間の不注意のような問題を持っていることを意味します.
    そして第2の問題は、すべてのスタイルが抽象的なグローバルCSSであるため、私たちのスタイルが実際に存在することを確認するためのタイプスクリプトのサポートを持っていないからです.そして、結果として生じる問題は、私たちが良いIDEインテリジェンスを持っていないadditionalData SASS VARSとmixinsを含むいくつかのファイルのインポートWebpack/Vite 設定)
    はい、次のような解決策があります.

  • SCSS IntelliSense SASS/SCSS変数の自動補完を行うには

  • SCSS Everywhere JSX/TSXファイルのクラスの自動補完を行います.
  • しかし、これらはIDE用のプラグインであり、既存のCSSクラスを使用しているかどうかをチェックするためにCI/CDパイプラインに組み込むことはできません.

    CSSモジュール


    そして、この点で、グローバルCSSを持つすべての問題を解決するために、ステージCSSモジュールに入ります.
    Basically CSS Modules = CSS in JS objects .
    CSSモジュールと同じです.主な違いはメソッドを呼び出すことはあまりにも異なっている.
    CSSモジュールは、CSSクラスへのリンクを含むいくつかのJSモジュール表現を提供します.そして、私たちのクラス名は<div className={style.css_class} /> , そして、フードの下の我々のクラスセレクタは、何かに変わります[name]__[local]__[hash:base64:5] 詳細here ), CSSクラスの分離問題を解決します.
    しかし、typescriptサポートについてはどうですか?
    ここでいくつかの解決策があります.

  • TypeScript plugin CSS Modules , インポートされたCSSモジュールファイルを含むクラスセレクタについての言語サービス情報を提供するプラグインです.しかし、vscodeのために、我々はセットアップスペースタイプを必要とします.
    詳しくはhere or here .
  • それはTS/TSXファイルのために存在しないクラス名を使用している問題を解決します、しかし、我々がVUEと.vue ファイル?
    ここでは問題があります.なぜなら、Volarは、より詳細な情報を得るために、タイプスクリプトプラグインCSSモジュールのサポートを提供していないからですhere .
    そして舞台に入る.

  • Vite plugin sass dts これはCSSモジュールのtypescript宣言を生成します.
  • またはCSS Modules TypeScript loader
  • そして、我々はVueプロジェクトのためのタイプチェックを持っています🥳
    そして、SCSS/sass vars、mixinsのIDE自動補完についてはどうですか?
    すべてはここで同じですSCSS IntelliSense
    しかし、CSSは機能豊富な言語です.どのようにさらに柔軟性を追加し、スタイルを書くの開発経験を向上させることができますか?

    CSSで


    また、私たちがJSを書くので、私たちは私たちのCSS断片のためにヘルパー機能を書くことができます.
    基本的な例については、メディアのクエリ、およびjsのテーマの変数です.
    export const screenSizes = {
      mobile: 767,
      tablet: 1023,
      computer: 1440,
      desktop: 1920,
    } as const
    
    export const makeMedia = (from: null | number, to?: null | number) => `@media screen${
      from
        ? ` and (min-width: ${from}px)`
        : ''}${
      to
        ? ` and (max-width: ${to - 1}px)`
        : ''}`
    
    export const media = {
      mobile: makeMedia(null, screenSizes.mobile),
      tablet: makeMedia(null, screenSizes.tablet),
      computer: makeMedia(null, screenSizes.computer),
      desktop: makeMedia(null, screenSizes.desktop),
      largescreen: makeMedia(screenSizes.desktop),
      tabletOnly: makeMedia(screenSizes.mobile, screenSizes.tablet),
      computerOnly: makeMedia(screenSizes.tablet, screenSizes.computer),
      desktopOnly: makeMedia(screenSizes.computer, screenSizes.desktop),
      aboveMobile: makeMedia(screenSizes.mobile),
      aboveTablet: makeMedia(screenSizes.tablet),
      aboveComputer: makeMedia(screenSizes.computer),
      aboveDesktop: makeMedia(screenSizes.desktop),
    }
    
    export const color = {
      primary: '#FF6B38',
      primaryOpacity27: 'rgba(255, 107, 56, .27)',
      primaryOpacity35: 'rgba(255, 107, 56, .35)',
      primaryLighten: '#F5F5F5',
      primaryLighten2: '#FDA38A',
      blackOpacity80: 'rgba(0, 0, 0, .8)',
      blackOpacity60: 'rgba(0, 0, 0, .6)',
      blackLight: '#161616',
      blackLightOpacity42: 'rgba(22, 22, 22, .42)',
    
      backgroundGray: '#161616',
      backgroundGrayLight: '#969696',
    } as const
    
    使用例:
    // Component style.ts file
    import styled from 'styled-components'
    import { media, color } from 'ui/theme'
    
    export const StyledWrapper = styled.div`
        position: relative;
        z-index: 1;
    
        background-color: ${color.white};
        border-radius: 36px;
        box-shadow: 0 10px 20px ${color.shadowPrimary2};
    `
    
    export const StyledTopGutter = styled.div`
        padding: 46px 46px 24px;
    
        display: flex;
        flex-flow: column wrap;
    
        ${media.mobile} {
            padding: 24px;
        }
    `
    
    しかし、私たちのCSSコードが実際にはJavaScriptであるので、それはすべてではありません.私たちは、ユーザー・エージェントを決定するためにユーザーエージェントのために見ることができて、いくつかの特定のブラウザーのために若干のスタイルを混ぜます.
    import { css } from 'styled-components'
    
    // Works only on the client-side
    // For SSR we need have some Context to Provide User-Agent from request context to React application context
    const USER_AGENT = window.navigator.userAgent;
    
    // More details about browser detect regex
    // here - https://github.com/ua-parser/uap-core/blob/master/regexes.yaml
    export const checkIsIE10OrOlder = /MSIE /g.test(USER_AGENT);
    export const checkIsIE11 = /Trident\//g.test(USER_AGENT);
    export const checkIsEdge = /Edge\//g.test(USER_AGENT);
    export const checkIsFireFox = /Firefox\//gi.test(USER_AGENT);
    export const checkIsChrome = /Chrome\//gi.test(USER_AGENT);
    export const checkIsSafari = /Safari\//gi.test(USER_AGENT);
    export const checkIsYandex = /YaBrowser\//gi.test(USER_AGENT);
    
    export const styleIE11Browser = (...args) => checkIsIE11 ? css(...args) : null;
    export const styleEdgeBrowser = (...args) => checkIsEdge ? css(...args) : null;
    export const styleMicrosoftBrowsers = (...args) => checkIsIE11 || checkIsEdge || checkIsIE10OrOlder ? css(...args) : null;
    export const styleIsNotMicrosoftBrowsers = (...args) => !checkIsIE11 && !checkIsIE10OrOlder ? css(...args) : null;
    export const styleFireFoxBrowser = (...args) => checkIsFireFox ? css(...args) : null;
    export const styleSafariBrowser = (...args) => checkIsSafari ? css(...args) : null;
    export const styleYandexBrowser = (...args) => checkIsYandex ? css(...args) : null;
    
    export const browser = {
        ie: styleMicrosoftBrowsers,
        ie11: styleIE11Browser,
        edge: styleEdgeBrowser,
        notIE: styleIsNotMicrosoftBrowsers,
        firefox: styleFireFoxBrowser,
        moz: styleFireFoxBrowser,
        safari: styleSafariBrowser,
        yandex: styleYandexBrowser,
    };
    
    または、ユーザーのブラウザを決定するためにCSSセレクタを使用することができます.
    // Works with both client-side and server-side rendering
    export const isIECssDetect = (...args) => css`@media all and (-ms-high-contrast:none) {${css(...args)}}`;
    export const isFireFoxCssDetect = (...args) => css`@-moz-document url-prefix() {${css(...args)}}`;
    
    export const browser = {
        css: {
            ie: isIECssDetect,
            firefox: isFireFoxCssDetect,
            moz: isFireFoxCssDetect,
        },
    };
    
    使用例:
    import styled from 'styled-components'
    import { browser } from 'ui/theme'
    
    export const StyledBackground = styled.img`
        position: absolute;
        object-fit: contain;
        object-position: right;
        top: 0;
        left: 0;
        z-index: -2;
        width: 100%;
        height: 100%;
    
        ${browser.ie`
            width: auto;
            right: 0;
            left: auto;
        `}
    `;
    
    そして、いくつかの基本的なコンポーネントを作成するために非常に役立つ、いくつかの基本的なコンポーネントを作成するためのいくつかの基本的なコンポーネントを作成するためにdisplay: flex; justify-content: center .
    そして、このように非常に役立つので、以下のような小さなヘルパーコンポーネントを作成できます.
    import styled, { css } from 'styled-components'
    
    interface LayoutProps {
        flow: 'column' | 'row' | 'column-reverse'
        wrap?: 'wrap' | 'nowrap'
        padding?: string
        margin?: string
        justify?: 'center' | 'flex-start' | 'flex-end' | 'space-between' | 'space-around' | 'stretch'
        align?: 'center' | 'flex-start' | 'flex-end' | 'space-between' | 'space-around' | 'stretch'
        width?: string
        height?: string
        shrink?: string
        'data-name'?: string
        grow?: string
    }
    
    export const Layout = styled.div<LayoutProps>`
      display: flex;
      flex-direction: ${p => p.flow};
      flex-wrap: ${p => p.wrap};
      padding: ${p => `${p.padding}`};
      margin: ${p => `${p.margin}`};
      ${p => p.width && css`
        width: ${p.width}
      `};
      ${p => p.height && css`
        height: ${p.height};
      `};
      ${p => p.justify && css`
        justify-content: ${p.justify}
      `};
      ${p => p.align && css`
        align-items: ${p.align}
      `};
    
      ${p => p.shrink && css`
        & > * + * {
          flex-shrink: ${p.shrink};
        }
      `};
      ${p => p.grow && css`
        flex-grow: ${p.grow};
      `};
    `
    
    Layout.defaultProps = {
        wrap: 'nowrap',
        padding: '0',
        margin: '0',
        justify: undefined,
        align: undefined,
        width: '',
        height: '',
        shrink: undefined,
        'data-name': 'layout',
        grow: '',
    }
    
    使用します.
    import { Layout } from 'ui/atoms'
    import { SeparateTitle } from 'ui/molecules'
    import { StyledWrapper } from './style'
    
    const OrderResponseForm: FC<Props> = () => {
        // Some code
    
        return (
            <Layout flow="column" wrap="wrap" margin="40px 0 0">
                <SeparateTitle line={false}>
                    {i18n.t('ORDER_DETAILS_FORM_TITLE')}
                </SeparateTitle>
                <StyledWrapper
                    flow="row"
                    padding="24px 30px 20px 24px"
                >
                    {`* Some more JSX *`}
                </StyledWrapper>
            </Layout>
        )
    }
    
    インスタイル.レイアウトコンポーネントの拡張機能
    小道具タイプチェックを保存する
    export const StyledWrapper = styled(Layout)`
        border-radius: 36px;
        box-shadow: 0 4px 20px ${color.shadowBlack2};
    
        ${media.tablet} {
            padding: 24px;
            margin-bottom: 8px;
        }
    `
    
    または、テキスト用の再利用可能なコンポーネントを作成することもできます.
    import styled, { css } from 'styled-components'
    import {
        color as colors,
        selectWeight,
        WeightType,
    } from 'ui/theme'
    
    interface TextProps {
        align?: string
        size?: string
        color?: keyof typeof colors
        weight?: WeightType
        lineHeight?: string
        whiteSpace?: 'pre-wrap' | 'initial' | 'pre' | 'nowrap' | 'pre-line' | 'normal'
        letterSpacing?: string
        transform?: string
        'data-name'?: string
        decoration?: string
    }
    
    export const Text = styled.span<TextProps>`
        line-height: ${p => p.lineHeight};
        font-size: ${({ size }) => size};
        color: ${({ color = 'text' }) => colors[color] ? colors[color] : color};
        letter-spacing: ${({ letterSpacing }) => letterSpacing};
        text-align: ${({ align }) => align};
        text-decoration: ${({ decoration }) => decoration};
        font-weight: ${({ weight = 'normal' }) => selectWeight(weight).weight};
        white-space: ${p => p.whiteSpace};
    
        ${({ transform }) => transform && css`
            text-transform: ${transform};
        `}
    `
    
    Text.defaultProps = {
        align: 'initial',
        size: '14px',
        color: 'text',
        weight: 'normal',
        lineHeight: 'normal',
        whiteSpace: 'initial',
        letterSpacing: 'initial',
        decoration: 'initial',
        'data-name': 'text',
    }
    
    JSのCSSは新しいレベルの開発者エクスペリエンス(DX)になります.なぜなら、スタイルの分離の問題を解決して、我々のJSXではなくATTRRを定義するようなクールな機能を持ってくるのですが、スタイル宣言変数では以下のようになります.
    const StyledPrecheckInner = styled(Layout).attrs<Props>(() => ({
        flow: 'column',
        width: '100%',
    }))`
        max-width: 378px;
        margin: 0 auto;
    
        > ${Text} {
            margin: 8px 0;
        }
    `
    
    またはより具体的な場合:
    export const StyledIndicator = styled.button.attrs<Props>(({
        isHasError,
        isLoading,
        isOpen,
        ...props
    }) => ({
        ...props,
        type: 'button',
        children: isLoading
            ? (
                <Loader
                    width="16px"
                    height="16px"
                    margin="0"
                    inline
                />
            )
            : (
                <IconArrow
                    data-dir={props.isOpen ? 'up' : 'down'}
                    stroke={isHasError ? 'textDangerExtra' : 'primary'}
                    width="16"
                    height="16"
                />
            ),
    }))`
        // CSS code
    `;
    
    そして、それはサポートダイナミック小道具(上記のより多くの例)です:
    const StyledNotch = styled.div<Props>`
        height: ${p => p.isShowPlaceholder
            ? p.height
            : 'initial'}
    `
    
    でも.JSは私たちにより多くのパワーを与え、いくつかの狂ったランタイムCSS変換を行うことができます.
    // A simplified example, but here you may have much more logic inside, you are limited only by JavaScript
    const StyledSeparator = styled.div<Props>`
        // Some CSS
    
        // A function call that returns an object, or it could be a switch case
        ${({ rule }) => ({
            day: css`
                margin: 24px 0 16px;
            `,
            year: css`
                position: relative;
    
                width: calc(100% - 48px);
                margin: 32px 24px 16px;
            `,
        })[rule]}
    `
    
    そしてすべてのサポートタイプスクリプトです.
    そして、JSのSSRケースCSSでは、「重要なCSS」を生成する能力を私たちに与えます.Wickは特にこのページに必要なだけのCSSを生成します.
    // Some server setup code
    
    server.get("/*", async (req, res) => {
      const sheet = new ServerStyleSheet();
    
      try {
        const app = renderToString(
          <StyleSheetManager sheet={sheet.instance}>
            <App />
          </StyleSheetManager>
        );
    
        const styledComponentTags = sheet.getStyleTags();
    
        const html = renderToStaticMarkup(
          <HtmlTemplate app={app} styledComponentTags={styledComponentTags} />
        );
    
        res.status(status).send(html);
      } catch (error) {
        logger.error(error);
        res.status(500).send(<ErrorPage />);
      } finally {
        sheet.seal();
      }
    });
    
    そして、それは私たちのバンドルとの関係を作るのは難しいことではない、我々はWebpack、Vite、またはログープなど何を使用しても.
    あなただけのバベル、esbuild、swcなどのいくつかのJavaScriptプロセッサが必要です.
    それは本当に素晴らしい音!
    しかし、最初に、SSSや他のCSSベースのソリューションが分離されたCSS(スタイルタグのスタイルを考慮しないでください)に含まれている間、コンポーネントがスクリーン上にある場合のみ、JSスタイルでCSSが生成されます.
    第二にJavaScript操作の力によるCSSの生成は自由ではありません😢
    すべての場合、それは音のように、スタイルのコンポーネントは、例えば、非常に高速な場合は、いくつかの要素の静的に孤立したスタイルのスタイルのコンポーネントを使用する場合は、非常に高速なコンポーネントのスタイルが、再起動可能なコンポーネントであまりにも多くのダイナミックな小道具を使用すると、それは非常に高速であり、非常に顕著にアプリケーションを遅くする😔
    そして、彼らはJS解決(またはJSのゼロランタイムCSS)でステージコンパイル時CSSに行きます

    CSSでコンパイル時のCSSを設定する


    私はいくつかのプレーヤーをシングルアウトします.

  • Linaria (最も人気のある、サポート反応とsvelte)

  • Vanilla extract (非常に興味深いですが、Linariaよりも多くのバンドルをサポートしています).

  • Compiled (アトラシアンからのJSソリューションのコンパイル時CSS )
  • 私は、「コンパイル時」という名前から何かを理解しています.
    たとえば、Linariaは、動的なプロップを持つコンポーネントのような、スタイル付きコンポーネント機能に似ています.
    import { styled } from '@linaria/react';
    
    const StyledTitle = styled.h1<TitleProps>`
        line-height: ${p => p.lineHeight};
        font-size: ${({ size }) => size};
        color: ${({ color = 'text' }) => colors[color] ? colors[color] : color};
        letter-spacing: ${({ letterSpacing }) => letterSpacing};
        text-align: ${({ align }) => align};
    `;
    
    フード・リニアリアの下では、動的なスタイルのためには、CSS変数を使用するラッパーコンポーネントを作成します.
    詳細here またはlinaria docsガイドを提供するhow it implement manually
    しかし、コンパイルステップは私たちにいくつかの制限をもたらすcss'' FNので、ダイナミックな、それはCSSのスコープのクラスのようなものです.
    そして、あなたのスタイルのutils出力はクラスの構成のようになります.
    import { css, cx } from '@linaria/core';
    
    export const selectButtonTheme = (theme: ButtonTheme, ...otherClasses: Array<string | undefined>) => {
      const cssBase = css`
        width: 170px;
        padding: 10px 0;
        display: flex;
      `
    
      const classes = [cssBase, ...otherClasses]
    
      switch (theme) {
        case 'outline':
          classes.push(css`
            border: 2px solid ${colors.primary};
          `)
          break
        case 'solid-gradient':
          classes.push(css`
            background: linear-gradient(0deg, ${colors.yellow} -80%, ${colors.orange1} 104.11%);
          `)
          break
      }
    
      return cx(...classes)
    }
    
    また、JavaScriptを書くので、ユーティリティ関数を使用することができますが、コンパイルステップは私たちにいくつかの制限をもたらす.例えば、私は絶対的な輸入を使いたいです、しかし、Linariaは時々私の「UI/テーマ」ファイルを輸入することができません、そして、我々が使う必要があるこの問題を解決するためにbabel-plugin-import-resolver .
    {
      "presets": ["@linaria"],
      "plugins": [
        ["module-resolver", {
          "root": ["./"],
          "alias": {
            "ui/theme": "./src/ui/theme",
            "ui/keyframes": "./src/ui/keyframes"
          }
        }]
      ]
    }
    
    しかし、それはすべてではない、あなたがLinariaを使用し始める前に、あなたはあなたのバンドル😄
    どういうわけか、インタビューにおいて「リナリアのためのWebpackの設定の難しさについて考えてみました.その時点で、私は、SSRと共にリニアニアをセットアップするための解決策を見つけることが、単純な仕事ではない」と言いましたRazzle 設定:
    const path = require('path')
    const LoadableWebpackPlugin = require('@loadable/webpack-plugin')
    const MiniCssExtractPlugin = require('mini-css-extract-plugin')
    
    module.exports = {
      modifyWebpackConfig: ({ env: { target, dev }, webpackConfig: config }) => {
        if (target === 'web') {
          // Loadable
          config.plugins.push(
            new LoadableWebpackPlugin({
              outputAsset: false,
              writeToDisk: {
                filename: path.resolve(__dirname, 'build'),
              },
            })
          )
    
          // Linaria
          config.module.rules.push({
            loader: '@linaria/webpack-loader',
            options: {
              sourceMap: process.env.NODE_ENV !== 'production',
              url: false,
            },
          })
    
          if (!dev) {
            config.plugins.push(
              new MiniCssExtractPlugin({
                filename: 'styles.css',
              })
            )
          }
    
          config.module.rules = config.module.rules.map(rule => {
            if (rule.test && !Array.isArray(rule.test) && rule.test.test('some.css')) {
              rule.use = rule.use.map(use => {
                if (use.ident === 'razzle-css-loader') {
                  return {
                    ...use,
                    options: {
                      ...use.options,
                      url: false,
                    },
                  }
                }
                return use
              })
            }
        }
    
        return config
      },
      plugins: [
        {
          name: 'typescript',
          options: {
            useBabel: true,
          },
        },
      ],
      experimental: {
        newBabel: true,
        newExternals: true,
        reactRefresh: false,
      },
    }
    
    nextjs設定詳細情報here .
    そして、あなたはバベルに縛られているものを覚えておく必要があり、また、バンドルとしてのVITEを使用すると、バベル(VATEのデフォルトで使用するだけでESBuild、バンドル時間をスピードアップする必要があります).そして、Nextjs 12もバベルを拒否しました.問題ではないが,ビルド時間が遅くなり,開発経験が悪化する.
    そして、セットアップの後SSR(RAGULE)とのプロジェクトの成長として私はHMRでいくつかの問題を抱えていたときに私のwebpackは、ページの完全なリフレッシュを行うだけではなく、代わりにバックグラウンドでホット更新スタイル.そして、この行動は永続的ではなかった.
    また、リナリアはスタイルコンポーネントに比べてIDEのサポートがあまり良くありません.
    しかし、私は人々がJSで原子コンパイル時CSSのような解決策を作成しようとすることを賞賛します🤯

    結論


    どのようなアプローチは、アプリケーションのスタイルを書くための選択?
    私は、我々が書いているどんな種類のアプリケーションに依存すると思います.
    各々のアプローチはそれ自身の支柱と短所を持っています.
    これについての私の話
  • CSSでは、-あなたがカスタムデザインを持つアプリケーションを持っていないときに選択してください.たとえば、会社の従業員が内部的に使用する「バックオフィス」アプリケーションがたくさんあります.この場合、JSのCSSは、コードの可読性を最大限に高めるエレガントで柔軟なAPIを提供します.
  • CSSモジュール-ときにいくつかの機能の重要なアプリケーションを選択します.たとえば、あなたは何百万ものユーザーによって使用される銀行クライアントの個人口座を開発しています.またはちょうど電子商取引😝