为 CRA 工程增加 styled-jsx 支持

当前比较流行的 CSS-in-JS 项目有 styled-jsx 和 styled-components 两个,说实话 styled-jsx 可以通过插件支持 PostCSS 差不多已经完美,使用 styled-components 的话开发思路需要一些转变,相对 styled-jsx 来说有些繁琐(支持 css prop 但有点内联 style 的意思),倒是符合 React 的组件哲学。

create-react-app 工程中支持 styled-jsx 有两种方式,一是使用 babel macro,一是使用 babel plugin,这两种方式都有一些问题,下面分别进行说明。

使用 babel macro

create-react-app 原生支持 babel macro,而 styled-jsx 自身也支持 babel macro,因此不需要对工程做任何配置即可以直接 styled-jsx,示例如下:

import classNames from 'classnames';
import {resolve} from 'styled-jsx/macro';

const example = resolve`
  .App {
    text-align: center;
  }

  .App-logo {
    animation: App-logo-spin infinite 20s linear;
    height: 40vmin;
    pointer-events: none;
  }

  .App-header {
    background-color: #282c34;
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    font-size: calc(10px + 2vmin);
    color: white;
  }

  .App-link {
    color: #61dafb;
  }

  @keyframes App-logo-spin {
    from {
      transform: rotate(0deg);
    }
    to {
      transform: rotate(360deg);
    }
  }
`;

class App extends Component {
  render() {
    return (
      <div className={classNames(example.className, "App")}>
        <header className={classNames(example.className, "App-header")}>
          <img src={logo} className={classNames(example.className, "App-logo")} alt="logo" />
          <p>
            Edit <code>src/App.js</code> and save to reload.
          </p>
          <a
            className={classNames(example.className, "App-link")}
            href="https://reactjs.org"
            target="_blank"
            rel="noopener noreferrer"
          >
            Learn React
          </a>
        </header>

        {example.styles}
      </div>
    );
  }
}

使用 bable macro 存在如下几个问题:

  1. 显然这种使用 resolve 定义样式的方式与我们所熟知的 <style jsx>...</style> 方式不一致;
  2. 使用类选择器时需要显式使用 classNames 进行连接;
  3. 不能使用 styled-jsx 插件[1],这样使用 styled-jsx 的意义就大大降低了;
  4. 对 typescript 的支持不好,当前 ‘styled-jsx/macro’ 和 ‘styled-jsx/style’ 两个模块在 @types/styled-jsx 包中均未定义,因此如有需要,需要自己定义 .d.ts 文件;

可以为 CRA 工程简单的定义所需的 .d.ts 文件以解决编译错误:

~$ vi src/example.d.ts
declare module 'styled-jsx/macro'
declare module 'styled-jsx/style'

当然,styled-components 也可以以 babel macro 的方式使用,并使用 polished 来实现类似 PostCSS 提供的功能。

使用 babel plugin

这是熟悉的 styled-jsx 使用方式:

class App extends Component {
  render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <p>
            Edit <code>src/App.js</code> and save to reload.
          </p>
          <a
            className="App-link"
            href="https://reactjs.org"
            target="_blank"
            rel="noopener noreferrer"
          >
            Learn React
          </a>
        </header>

        { /*language=CSS*/ }
        <style jsx>{`
          .App {
            text-align: center;
          }

          .App-logo {
            animation: App-logo-spin infinite 20s linear;
            height: 40vmin;
            pointer-events: none;
          }

          .App-header {
            background-color: #282c34;
            min-height: 100vh;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            font-size: calc(10px + 2vmin);
            color: white;
          }

          .App-link {
            color: #61dafb;
          }

          @keyframes App-logo-spin {
            from {
              transform: rotate(0deg);
            }
            to {
              transform: rotate(360deg);
            }
          }
      `}</style>
      </div>
    );
  }
}

但此时需要 eject 以支持 styled-jsx 配置(注意:eject 要求 git 工程工作目录是 clean 的):

~$ yarn eject
~$ vi config/webpack.config.js
{
  test: /\.(js|mjs|jsx|ts|tsx)$/,
  include: paths.appSrc,
  loader: require.resolve('babel-loader'),
  options: {
    customize: require.resolve(
      'babel-preset-react-app/webpack-overrides'
    ),

    plugins: [
      [
        require.resolve('babel-plugin-named-asset-import'),
        {
          loaderMap: {
            svg: {
              ReactComponent:
                '@svgr/webpack?-prettier,-svgo![path]',
            },
          },
        },
      ],
      ['styled-jsx/babel', { "plugins": ["styled-jsx-plugin-postcss"] }],
    ],
    ...
},

此处添加了 PostCSS 插件,因此可以使用各种 PostCSS 的特性:

~$ yarn add styled-jsx-plugin-postcss
~$ yarn add postcss-easy-media-query
~$ vi postcss.config.js
const postcssEasyMediaQuery = require(`postcss-easy-media-query`);

module.exports = () => ({
  plugins: [
    postcssEasyMediaQuery({
      breakpoints: {
        tablet: 600,
        desktop: 1024
      }
    }),
  ],
});

可惜的是,在 react-app-rewired 逐渐淡出的情况下,yarn eject 显得有些无奈。

混合使用 babel macro 与 babel plugin

在某些特殊需求下,可能会用到:

import classNames from 'classnames';
import {resolve} from 'styled-jsx/macro';
import _JSXStyle from 'styled-jsx/style';

const example = resolve`
  .App {
    text-align: center;
  }

  .App-logo {
    animation: App-logo-spin infinite 20s linear;
    height: 40vmin;
    pointer-events: none;
  }
`;

class App extends Component {
  render() {
    return (
      <div className={classNames(example.className, "App")}>
        <header className="App-header">
          <img src={logo} className={classNames(example.className, "App-logo")} alt="logo" />
          <p>
            Edit <code>src/App.js</code> and save to reload.
          </p>
          <a
            className="App-link"
            href="https://reactjs.org"
            target="_blank"
            rel="noopener noreferrer"
          >
            Learn React
          </a>
        </header>

        {example.styles}
        { /*language=CSS*/ }
        <style jsx>{`
          .App-header {
            background-color: #282c34;
            min-height: 100vh;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            font-size: calc(10px + 2vmin);
            color: white;
          }

          .App-link {
            color: #61dafb;
          }

          @keyframes App-logo-spin {
            from {
              transform: rotate(0deg);
            }
            to {
              transform: rotate(360deg);
            }
          }
      `}</style>
      </div>
    );
  }
}

注意不要删除 import _JSXStyle 这条没有用到的 import 指令,否则会出现类似如下的报错:

Module parse failed: Identifier '_JSXStyle' has already been declared (8:7)
You may need an appropriate loader to handle this file type.
| var _jsxFileName = "/home/runsisi/workingcopy/test/app/src/App.js";
| import _JSXStyle from "styled-jsx/style";
> import _JSXStyle from "styled-jsx/style";
| import React, { Component } from 'react';

参考资料

[1] Can you use Styled JSX plugins with Babel macros?

https://spectrum.chat/styled-jsx/general/can-you-use-styled-jsx-plugins-with-babel-macros~2d2331c7-07d6-4bef-a7df-dab24fc0055b

[2] Static Type Checking

https://reactjs.org/docs/static-type-checking.html#typescript

[3] A starter template for TypeScript and React

https://github.com/Microsoft/TypeScript-React-Starter#typescript-react-starter

[4] Creating TypeScript typings for existing React components

https://templecoding.com/blog/2016/03/31/creating-typescript-typings-for-existing-react-components/

[5] Clarify how to add a new declaration file

https://github.com/Microsoft/TypeScript/issues/21344

[6] Adding Custom Type Definitions to a Third-Party Library

https://www.detroitlabs.com/blog/2018/02/28/adding-custom-type-definitions-to-a-third-party-library/


最后修改于 2019-02-23