基于Create React App打造代码规范化的React+Ts项目

Create React App 是一个官方支持的创建 React 单页应用程序的方法。它提供了一个零配置的现代构建设置。

而在实际的项目开发中,我们通常希望可以自行配置一些参数。如:webpack 的alias、typescript 的path、eslint 的rules等。本文记录了基于 create-react-app 打造 react+ts 的过程遇到的各种问题,以及对应的解决方法。

  • npm: 6.9.0

  • node: v12.2.0

  • react-scripts: 4.0.3 (发布新版本的 Create React App,更新 react-scripts 即可 )

创建项目

创建 react + ts 项目: npx create-react-app my-app --typescript

安装 TypeScript 包: npm install --save typescript @types/node @types/react @types/react-dom @types/jest

.js、.jsx 文件重命名为 .ts、.tsx 文件(typeScript文件),运行 npm start 启动项目。

注:不需要手动创建 tsconfig.json 文件。当项目中的 src/ 路径下有TypeScript文件(.ts|tsx)时,运行 npm start 会自动创建tsconfig.json 文件。

自定义webpack

CRA 项目中,webpack 的配置是封装在 react-scripts 包内。扩展 webpack 配置有以下方法:

1. npm run eject (不推荐)

它会将所有配置文件和传递依赖项(Webpack,Babel,ESLint等)复制到项目中,以便你可以完全控制它们。除 eject 之外的所有命令仍然有效,但它们将指向复制的脚本,以便你可以调整它们。

注意:这是单向操作。一旦你 eject ,你就不能回去了!

CRA 通过升级其中的 react-scripts 包来升级 CRA 的特性,而使用了 eject 命令,就再也享受不到 CRA 升级带来的好处了。因为 package 中的 scripts 的命令已经指向了项目中的配置文件,而不是指向 react-scripts 包。

2. react-app-rewired + customize-cra

  • 安装:npm i react-app-rewired customize-cra -S

  • **配置:**在项目根目录添加 config-overrides.js 配置文件

    该工具可以在不 npm run eject, 也不创建额外 react-scripts 的情况下修改 create-react-app 内置的 webpack 配置,然后你将拥有 create-react-app 的一切特性,且可以根据你的需要去配置 webpack plugins, loaders 等。

    查看更多配置

    /* config-overrides.js */
    module.exports = function override(config, env) {
      //do stuff with the webpack config...
      return config;
    }

    默认情况下, config-overrides.js 文件导出单个函数,以便在开发或生产模式下自定义 webpack 配置。此外,该文件中也可以导出一个包含最多三个字段的对象,每个字段都是一个函数。如下:

    /* config-overrides.js */
    module.exports = {
      // 该字段与 config-overrides.js 导出的单个函数的等效项。
      // 它无法在测试模式下配置编译,也不能用于自定义开发模式下的 Webpack Dev Server。
      webpack: function(config, env) {
        return config;
      },
      // 该配置应用于 Jest 模式下。这意味着上述 webpack 配置在测试模式下都是无效的。
      jest: function(config) {
        return config;
      },
      // 在开发模式下运行时,用于生成 dev server 配置。
      devServer: function(configFunction) {
        return function(proxy, allowedHost) {
          const config = configFunction(proxy, allowedHost);
          return config;
        };
      },
      // paths 字段用于为 create-react-app 传递到 webpack 和 jest 的路径提供覆盖。
      paths: function(paths, env) {
        return paths;
      }
    }
  • 使用 customize-cra。

    customize-cra 用于改成 react-app-rewiredconfig-overrides.js文件。通过导入 customize-cra 功能和导出几个函数调用包裹在 override 函数中。这样,就可以很容易地修改组成create-react-app 的配置对象(webpackwebpack-dev-serverbabel等)。

    查看更多customize-cra api

    /* config-overrides.js */
    const {
        override,
        disableEsLint,
        addWebpackAlias,
    } = require("customize-cra");
    const path = require("path");
    
    module.exports = override(
        disableEsLint(),
        addWebpackAlias({
            ["ag-grid-react$"]: path.resolve(__dirname, "src/shared/agGridWrapper.js")
        })

);

* 修改项目启动命令。

```json
/* package.json */
"scripts": {
  "start": "react-app-rewired start",
  "build": "react-app-rewired build",
  "test": "react-app-rewired test",
  "eject": "react-scripts eject"
}
  • 自定义 config-overrides.js 路径。

    比如:您想要使用在 node_modules 中的第三方 config-overrides.js,您可以将以下内容添加到您的 package.json

    "config-overrides-path": "node_modules/some-preconfigured-rewire"

craco 是另一款类似 react-app-rewired 的功能。

  • 安装:npm i @craco/craco -S

  • 配置:在项目根目录下添加 craco.config.js 配置文件。更看更多配置

    /* craco.config.js */
    const { when, whenDev, whenProd, whenTest, ESLINT_MODES, POSTCSS_MODES } = require("@craco/craco");
    
    module.exports = {
      reactScriptsVersion: "react-scripts" /* (default value) */,
      style: {},
      eslint: {},
      babel: {},
      typescript: {},
      webpack: {
        alias: {
          '@': path.resolve(__dirname, "src")
        },
        plugins: {
          add: [],
          remove: [],
        },
        configure: { },
        // configure: (webpackConfig, { env, paths }) => { return webpackConfig; }
      },
      jest: { },
      devServer: { },
      plugins: [ ]
    };
  • 修改项目启动命令

    /* package.json */
    "scripts": {
      "start": "craco start",
      "build": "craco build",
      "test": "craco test",
      "eject": "react-scripts eject"
    }

自定义Typescript

按上述安装方法创建项目后,运行 npm start,在 src/ 有 typescript 文件的情况下,会自动创建 tsconfig.json 文件。 你可以编辑该文件。

在建项过程中发现, 修改 tsconfig.json 文件中的 compilerOptions.paths 属性后,每次运行 npm start,该属性会被自动创建的文件覆盖。

解决方案如下:

  • 安装: npm i @craco/craco -D

  • 新建 tsconfig.paths.json 文件

    // tsconfig.paths.json
    {
      "compilerOptions": {
        "baseUrl": ".",
        "paths": {
          "@/*": ["src/*"]
        }
      }
    }
  • 修改 tsconfig.json 文件。

    // tsconfig.json
    {
      "extends": "./tsconfig.paths.json",
      ...
    }

环境变量

CRA 创建的项目可以使用环境中声明的变量,就像是在 JS 文件中本地声明的变量一样。默认情况下,项目将为你定义 NODE_ENV ,以及以 REACT_APP_ 开头的任何其他环境变量。

环境变量在构建期间嵌入。由于 Create React App 生成静态的 HTML / CSS / JS 包,因此无法在 runtime(运行时) 读取它们。项目中的环境变量,会在构建静态文件时,替换成对应的值。

console.log(process.env)

// FAST_REFRESH: true
// NODE_ENV: "development"
// PUBLIC_URL: ""
// WDS_SOCKET_HOST: undefined
// WDS_SOCKET_PATH: undefined
// WDS_SOCKET_PORT: undefined

NODE_ENV 是一个特殊内置环境变量。当你运行 npm start 时,它总是等于 'development' ,当你运行 npm test 它总是等于 'test' ,当你运行 npm run build 来生成一个生产 bundle(包) 时,它总是等于 'production'你无法手动覆盖NODE_ENV

注意:必须以 REACT_APP_ 开头创建自定义环境变量。除了 NODE_ENV 之外的任何其他变量都将被忽略。更改任何环境变量都需要重新启动正在运行的开发服务器。

自定义环境变量

有两种方式完成自定义环境变量:在 shell 中或在 .env 文件中。

  • 在 Shell 中添加临时环境变量

    定义环境变量可能因操作系统而异。 注意,这种方式对于 shell 会话是暂时的

    # Windows (cmd.exe)
    # 注意:变量赋值需要用引号包裹,以避免尾随空格。)
    set "REACT_APP_SECRET_CODE=abcdef" && npm start
    
    # Windows (Powershell)
    ($env:REACT_APP_SECRET_CODE = "abcdef") -and (npm start)
    
    # Linux, macOS (Bash)
    REACT_APP_SECRET_CODE=abcdef npm start
  • .env 中添加开发环境变量

    要定义永久环境变量,请在项目的根目录中创建名为 .env 的文件:

    # .env
    REACT_APP_WEBSITE_NAME=REACT+TS项目
    REACT_APP_AUTHOR=李兆
    REACT_APP_SECRET_NUMBER=123456

    环境变量间的引用:

    REACT_APP_VERSION=$REACT_APP_SECRET_NUMBER

如何使用环境变量

  • 在 HTML 中引用环境变量

    <!-- index.htm l-->
    <title>%REACT_APP_WEBSITE_NAME%</title>
  • 在 JS 中引用环境变量

    // index.js
    console.log(process.env.REACT_APP_AUTHOR)
  • 在 CSS 中引用环境变量

    # .env.local
    REACT_APP_MAIN_COLOR=red

    方法一:使用行内样式

    <!--行内样式-->
    <div style=\{\{color: process.env.REACT_APP_MAIN_COLOR\}\}></div>

    **方法二:**使用 CSS in JS,即用 js 写 css,可用第三方库实现。如:Style ComponentsEmotion

    安装:npm i styled-components -D

    // index.js
    import styled from 'styled-components'
    const WrapperDiv = styled.div`
      color: ${process.env.REACT_APP_MAIN_COLOR}
    `;
    function App() {
      return <WrapperDiv>css in js</WrapperDiv>;
    }
    export default App;

    方法三:使用 CSS 预处理器来管理全局变量。如:sass。

    安装:npm install node-sass -S

    // craco.config.js
    module.exports = {
      style: {
      	sass: {
      		loaderOptions: {
          	additionalData:  "$my-color: cyan;"
      		}
      	}
      }
    }
    /* index.scss */
    .env_variable {
      color: $my-color;
    }

其他 .env 文件

  • .env:默认。

  • .env.local:本地覆盖。除 test 之外的所有环境都加载此文件

  • .env.development, .env.test, .env.production:设置特定环境。

  • .env.development.local, .env.test.local, .env.production.local:设置特定环境的本地覆盖。

左侧的文件比右侧的文件具有更高的优先级:

  • npm start: .env.development.local, .env.development, .env.local, .env

  • npm run build: .env.production.local, .env.production, .env.local, .env

  • npm test: .env.test.local, .env.test, .env 。注意:没有 .env.local

代码规范化(Eslint\Prettier)

eslint配置

方法一:Create React App 官方给出的解决方案

  • 添加境变量EXTEND_ESLINTtrue

  • 修改 eslant 配置,可直接修改 package.json 配置,或者添加 eslint.js 文件。

// package.json
{
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ],
    "rules": {
      'no-console': process.env.NODE_ENV === 'production' ? 2 : 1,
    }
  },
}
// eslintrc.js
module.exports = {
  extends: [
    "react-app",
    "react-app/jest"
  ],
  rules: {
    'no-console': process.env.NODE_ENV === 'production' ? 2 : 1,
  },
}

方法二:修改 craco.config.js 配置。

module.exports = {
  eslint: {
    enable: true,
    mode: "extends",
    configure: {
      extends: [
        "react-app",
        "react-app/jest"
      ],
      rules: {
        'no-console': process.env.NODE_ENV === 'production' ? 2 : 1,
      },
    },
    pluginOptions: {
    },
  },
}

prettier配置

  • 安装:npm i prettier eslint-plugin-prettier eslint-config-prettier -D

  • **配置:**在 package.json 里添加 prettier 字段,或者在项目的根目录下添加 .prettierrc.prettierrc.js.prettier.config.js.prettierrc.toml 文件。

    // package.json
    {
      ...
      "prettier": {
        "printWidth": 80,
        "tabWidth": 2,
        "useTabs": false,
        "singleQuote": true,
        "semi": false
      }
    }
    // .prettier.config.js
    module.exports = {
      printWidth: 80,
      tabWidth: 2,
      useTabs: false,
      singleQuote: false,
      semi: true,
    }
  • 命令行:

    // package.json
    // 采用.prettier.config.js配置时,命行令中要加 --config .prettier.config.js
    {
      "scripts": {
        "format": "prettier --write \"src/**/*.+(js|jsx|css)\"",
        // "format": "prettier --write --config .prettier.config.js \"src/**/*.+(js|jsx|css)\""
      },
    }

    然后,执行 npm run format即可。

  • **整合:**prettier 和 Git 整合。

    安装:npm i lint-staged husky -D

    配置:

    // package.json
    {
      ...
      "lint-staged": {
        "src/**/*.{js,jsx,ts,tsx,json,css,scss,md}": [
          "prettier --write --config .prettier.config.js \"src/**/*.+(js|jsx|css)\"",
          "git add"
        ]
      },
      "husky": {
        "hooks": {
          "pre-commit": "lint-staged"
        }
      },
    }

添加 Router(路由)

CRA 并未规定特定的Router(路由)解决方案,但 React Router 是最受欢迎的 Router(路由) 解决方案。

import {
  BrowserRouter as Router,
  // HashRouter as Router,
  Switch,
  Route,
  Link
} from "react-router-dom";

import Home from '../home'
import About from '../about'
import Dashboard from '../dashboard'

function App () {
  return (
    <Router>
      <div>
        <ul>
          <li> <Link to="/">Home</Link> </li>
          <li> <Link to="/about">About</Link> </li>
          <li> <Link to="/dashboard">Dashboard</Link> </li>
        </ul>

        <Switch>
          <Route exact path="/" component={Home} />
          <Route path="/about" component={About} />
          <Route path="/dashboard" component={Dashboard} />
        </Switch>
      </div>
    </Router>
  )
}

export default App

React Router中的组件主要分为三类:

  • 路由器,例如<BrowserRouter><HashRouter>

    • <BrowserRouter> 使用常规URL路径。这些通常是外观最好的网址,但它们要求您的服务器配置正确。 具体来说,您的Web服务器需要在所有由React Router客户端管理的URL上提供相同的页面。

      请注意,在部署应用程序之前,你可能需要配置生产服务器以支持客户端路由

    • <HashRouter> 将当前位置存储在URL的哈希部分中,因此URL看起来类似于http://example.com/#/your/page。 由于哈希从不发送到服务器,因此这意味着不需要特殊的服务器配置。

  • 路由匹配器,例如 <Route><Switch>

    渲染 <Switch> 时,它会搜索其子元素 <Route>,以查找其路径与当前URL匹配的元素。当找到一个时,它将渲染该 <Route> 并忽略所有其他路由。这意味着您应该将 <Route> 包含更多特定路径(通常较长)的路径放在不那么特定路径之前。

  • 导航,例如 <Link><NavLink><Redirect>

添加样式

如何引入样式

  • **添加常规 CSS 文件。**即通过 import 的方式导入 .css 文件。

    /**index.css**/
    .app_link {
      color: red
    }
    import './index.css'
    function App() {
      return <div>
    		<p className="app_link">Learn React</p>
    	</div>
    }
    export default App
  • 添加 CSS Modules 样式表

    CRA 项目使用 [name].module.css 文件命名约定支持 CSS Modules 和常规 CSS 。 CSS Modules 允许通过自动创建 [filename]\_[classname]\_\_[hash] 格式的唯一 classname 来确定 CSS 的作用域。

    /**index.module.css**/
    .app_module_link {
      color: orange;
    }
    import styles from './index.module.css';
    function App() {
      return <div>
    		<p className={styles.app_module_link}>Learn React</p>
    	</div>
    }
    export default App

    最终渲染结果:

    <div><p class="index_app_link__1pkYt">Learn React</p></div>
  • 添加 Sass 样式表

    首先,安装:npm install node-sass -S,然后,将文件扩展名改为 .scss.sass (建议用 .scss,书写格式更接近常规css)。

    /**index.scss**/
    .app_scss_link {
      color: $my-color;
    }
    import './index.scss'
    function App() {
      return <div>
    		<p className="app_scss_link">Learn React</p>
    	</div>
    }
    export default App
  • 使用css-in-js。

    // index.js
    import styled from 'styled-components'
    const LinkP = styled.p`
      color: deeppink;
    `;
    function App() {
      return <div>
    		<LinkP>Learn React</LinkP>
    	</div>
    }
    export default App

    最终渲染结果:

    <div><p class="sc-bdnxRM kqYXav">Learn React</p></div>

Postcss预处理器

CRA 内嵌了压缩、自动添加浏览器前缀等插件,同时,你也可以按需要引入一些 Postcss 插件。如:postcss-applypostcss-css-variablespostcss-px2rem-exclude 等。

安装:npm i postcss-px2rem-exclude -D

**配置:**修改 craco.config.js,添加 postcss 插件配置

// craco.config.js
module.exports = {
  style: {
    sass: {
      loaderOptions: {
        additionalData:  "$my-color: cyan;"
      }
    },
    postcss: {
      plugins: [
        require("postcss-import")({
          "path": "src/assets/css"
        }),
        require("postcss-preset-env")({
          features: {
            "custom-properties": {
              preserve: false,
              variables: {}
            },
            "nesting-rules": true
          }
        }),
        require("postcss-apply")({}),
        require("postcss-css-variables")({}),
        require("postcss-px2rem-exclude")({
          remUnit: 16,
          exclude: /node_modules|folder_name/i
        }),
      ]
    },
  },
}

结语

CRA 项目还可以结合 redux 管理页面状态。

除了 create-react-app 外,也可以选用 dvaumi等应用框架。

  • dva

    dva 首先是一个基于 reduxredux-saga 的数据流方案,然后为了简化开发体验,dva 还额外内置了 react-routerfetch,所以也可以理解为一个轻量级的应用框架。

    相比于 CAR 只是多了内置的 reduxredux-saga,帮我们处理了数据流这方面的需求而已。如果只是想要达到这个效果的话,直接在 CAR 中增加 dva 的依赖也是可以做到的。

  • umi

    umi 是蚂蚁金服的底层前端框架,中文可发音为乌米,是一个可插拔的企业级 react 应用框架。umi 以路由为基础的,支持类 next.js 的约定式路由,以及各种进阶的路由功能,并以此进行功能扩展,比如支持路由级的按需加载。然后配以完善的插件体系,覆盖从源码到构建产物的每个生命周期,支持各种功能扩展和业务需求,目前内外部加起来已有 50+ 的插件。

参考链接

Create React App 中文文档

[译]基于create-react-app打造代码规范化的React项目

这些 CSS-in-JS 库,谁更适合你

TypeScript 中文手册

react-router-dom@5.x官方文档翻译

最后更新于