react + typescript 项目的定制化过程

12,308 阅读6分钟

前言

  • 如果要使用 react 的话,对新手来说,首选脚手架大概就是使用由 facebook 官方出的脚手架 create-react-app 了(传送门 👉create-react-app中文文档)。
  • create-react-app 将 webpack 的配置,lint 的配置,babel 的配置等封装成 react-scripts,这种做法保证了底层依赖版本升级和迁移的时候,可以平滑迁移,不会影响到业务项目。
  • create-react-app 支持开发者对项目进行个性化配置(通过配合 react-app-rewired 使用或yarn eject 暴露 相关配置后进行修改)。
  • 下文介绍了本人在进行业务代码开发前通常对项目进行的一些特殊配置,有利于后期的工程开发。

初始化项目

  • 在使用脚手架之前,需要使用 npm 命令全局安装脚手架:
npm install -g create-react-app
  • 安装完成后,即可通过脚手架搭建项目:
create-react-app my-app
  • TypeScript 是 JavaScript 的类型超集,可编译为纯 JavaScript 。通过运行下面的命令可以使用 TypeScript启动新的 create-react-app 项目:
create-react-app my-app --typescript
  • 特别说明:下文介绍的项目配置均是针对 react + typescript 项目所进行的配置。

安装最新的 create-react-app

  • 在使用旧的 create-react-app 包创建项目时,控制台会有以下提示:
A template was not provided. This is likely because you're using an outdated version of create-react-app.
Please note that global installs of create-react-app are no longer supported.

解决方案:

  • 使用 npm uninstall -g create-react-app 卸载老版本
  • 使用 which create-react-app 命令查看下是否卸载成功

如果你像我一样提示 (/usr/local/bin/create-react-app),请运行 rm -rf /usr/local/bin/create-react-app以删除此。

  • 安装新版本:npm install -g create-react-app

react-scripts

  • 本节主要是介绍react-scripts一些相关内容,如果只对配置感兴趣的同学可以跳过本节。

  • 前面介绍到,create-react-app 在 webpack 上封装了一层 react-scripts,一方面是可以使得不习惯 eslint,babel 和 webpack 的新手只需关注于组件的编写,另一方面是可以不断的更新和改进默认选项,而不会影响到业务代码。

  • 可见,react-scripts 的作用就是通过将一些底层配置封装起来,从而向上屏蔽了众多细节,使得业务开发者只需关注业务代码的开发。

  • 去到项目 node_modules 目录下,可以看到 create-react-app + typescript 里的react-scripts的目录结构如下:

    react-scripts目录结构
    • 其中,scripts 文件夹里面包含了项目的开发脚本和构建脚本,对应的 webpack 配置则放在在 config 文件夹里面。
  • 如果要修改这些配置有三种办法:

    (1)通过 react-app-rewired 覆盖默认的 webpack 配置。

    (2)fork 对应的 react-scripts包, 自己维护这个依赖包。

    (3)直接 eject 出整个配置在业务项目里维护。该操作的缺点是不可逆,一旦配置文件暴露后就不可再隐藏。

  • 由于本人技术尚浅,本人采用第三种方案。

  • 首先,进入项目目录:

cd my-app
  • 暴露react-scripts包:
yarn eject

yarn run v1.17.3
$ react-scripts eject
NOTE: Create React App 2+ supports TypeScript, Sass, CSS Modules and more without ejecting: https://reactjs.org/blog/2018/10/01/create-react-app-v2.html

? Are you sure you want to eject? This action is permanent. (y/N)
# 输入 y 即可
  • 一般是使用脚手架搭建好项目后就使用以上命令暴露react-scripts包。而如果先安装了其他依赖或改动项目其他内容之后,再使用 yarn eject 命令时就会报错:
This git repository has untracked files or uncommitted changes:

Remove untracked files, stash or commit any changes, and try again.
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! XXX@0.1.0 eject: `react-scripts eject`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the XXX@0.1.0 eject script.
npm ERR! This is probably not a problem with npm. There is likely additional log
ging output above.

npm ERR! A complete log of this run can be found in:
npm ERR! C:\Users\Administrator\AppData\Roaming\npm-cache\_logs\2019-8-1T0
3_18_15_677Z-debug.log
  • 不要慌,解决方案是依次执行以下命令:
git add .
git commit -am "init"
yarn eject
  • 成功 eject 出配置后,可以发现项目目录的变化如下:

    项目目录的变化
  • 如果需要定制化项目,一般就是在config目录下对默认的 webpack 进行修改。

完善定制化项目

  • 下面将分别介绍如何在项目中引入 less、添加 tslintstylelint、引入 react-router、封装 fetch 请求、引入 react-loadable 和按需加载 antd

引入less

  • 安装 lessless-loader
yarn add less less-loader –dev
  • 修改 webpack 配置,即在 config/webpack.config.js 文件中新增 less 配置变量:
const lessRegex = /\.less$/;  // 新增less配置
const lessModuleRegex = /\.module\.less$/; // 新增less配置
  • 同时,在 config/webpack.config.js 文件中的 module 里面增加 rule 规则:
    module: {
      strictExportPresence: true,
      rules: [
        /* 省略代码 */
        {
          oneOf: [
            /* 省略代码 */
            /* 下面是原有代码块 */
            {
              test: cssModuleRegex,
              use: getStyleLoaders({
                importLoaders: 1,
                sourceMap: isEnvProduction && shouldUseSourceMap,
                modules: true,
                getLocalIdent: getCSSModuleLocalIdent,
              }),
            },
            /* 上面是原有代码块 */
            /* 下面是添加代码块 */
            {
              test: lessRegex,
              exclude: lessModuleRegex,
              use: getStyleLoaders({
                importLoaders: 1,// 值是1
                sourceMap: isEnvProduction && shouldUseSourceMap
              },
                "less-loader"
              ),
              sideEffects: true
            },
            {
              test: lessModuleRegex,
              use: getStyleLoaders({
                importLoaders: 1,
                sourceMap: isEnvProduction && shouldUseSourceMap,
                modules: true, // 增加这个可以通过模块方式来访问less
                getLocalIdent: getCSSModuleLocalIdent
              },
                "less-loader"
              )
            },
            /* 上面是添加代码块 */
            /* 下面是原有代码块 */
            {
              test: sassRegex,
              exclude: sassModuleRegex,
              use: getStyleLoaders(
                {
                  importLoaders: 2,
                  sourceMap: isEnvProduction && shouldUseSourceMap,
                },
                'sass-loader'
              ),
              sideEffects: true,
            },
            /* 上面是原有代码块 */
          ],
        },
      ],
    },
  • 通过上面的配置可以实现模块化的 less(以xx.module.less命名的文件) 并且和全局 less(以xx.less命名的文件)区分开。
  • 传送门 👉CSS Modules 详解及 React 中实践
  • 模块化引入:
import * as styles from ./index.module.less
  • 重点!如果要在项目中进行模块化引入 less,还需要在 src/react-app-env.d.ts 文件中进行配置,否则ts会发生报 错 Cannot find module './index.module.less',配置内容如下:
declare module '*.less' {
  const styles: any;
  export = styles;
}
  • 以上则完成了 less 在 react + typescript 项目中的引入。

编辑器配置

  • 在日常开发中,经常会切换不同编辑器,总要设置一遍的配置。团队开发,每个人使用不同的编辑器和具有不同的配置风格。通过在项目根目录中添加.editorconfig文件并配置一定规则,就可以设置不同编辑器保持一致代码规范。
  • 下面给出我的配置:
# http://editorconfig.org
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
trim_trailing_whitespace = false

[Makefile]
indent_style = tab

项目中添加tslint 和 stylelint

  • tslint:类似于 eslint,实际上就是约束我们的逻辑代码风格,可通过在项目根目录添加.tslint.json文件进行相关的配置。一般可以直接引用诸如 "extends": ["tslint-react"], 如果有特殊规则也可以自己加。内容示例:
{
  "extends": ["tslint-react"],
  "rules": {
    /* 自己添加的特殊规则 */
  }
}
  • stylelint:可以约束我们的样式的样式代码风格,可通过在项目根目录添加.stylelintrc文件进行相关的配置。一般可以直接引用诸如 "extends": ["stylelint-config-standard"], 如果有特殊规则也可以自己加。内容示例:
{
  "extends": "stylelint-config-standard",
  "rules": {
    /* 自己添加的特殊规则 */
  }
}
  • 以上则完成了项目中 tslintstylelint 的添加。

引入react-router

  • 有的人会想说,不就是yarn add react-router吗,还需要教?其实不然,看👇面:
  • React Router 现在已经被划分成了三个包:react-routerreact-router-domreact-router-native
  • 在开发中不应该直接安装 react-router,这个包为 React Router 应用提供了核心的路由组件和函数,另外两个包提供了特定环境的组件(浏览器和 react-native 对应的平台),不过他们也是将 react-router 导出的模块再次导出。
  • 我们应该选择这两个中适应开发环境的包,由于本人需要构建一个网站(在浏览器中运行),所以我安装的是 react-router-dom。同时,由于项目中我还使用了typescript,所以还要安装@types/react-router-dom。安装命令:
yarn add react-router-dom
yarn add @types/react-router-dom --dev
  • 以上则完成了项目中 react-router 的引入。

封装 fetch 请求

  • 如果只是简单的请求,没必要引入 aixos,通过将fetch请求的相关代码封装在request.js/request.ts文件中,在使用的时候引入相关请求方法即可,好处有几点:

    • 请求的地方代码更少。

    • 公共的错误统一在一个地方添加即可。

    • 请求定制的错误还是请求自己也可以处理。

    • 扩展性好,添加功能只需要改一个地方。

  • 下面给出我在项目中封装的 request.ts 文件具体内容:

// path:src/utils/request.ts
const request = (url: string, config: any) => {
  return fetch(url, config)
    .then((res: any) => {
      if (!res.ok) {
        // 服务器异常返回
        throw Error('接口请求异常');
      }
      return res.json();
    })
    .catch((error: any) => {
      return Promise.reject(error);
    });
};

// GET请求
export const get = (url: string) => {
  return request(url, { method: 'GET' });
};

// POST请求
export const post = (url: string, data: any) => {
  return request(url, {
    body: JSON.stringify(data),
    headers: {
      'content-type': 'application/json',
    },
    method: 'POST',
  });
};
  • 根据功能建立不同的请求模块,如列表模块:
// path:src/services/api/list.ts

import * as Fetch from '../../utils/request';

export async function getListData () {
  return Fetch.get('URL1');
}

export async function getListItemDetail (id: number) {
  return Fetch.get(
    `URL2/${id}`,
  ); 
}
  • 暴露 api:
// path:src/services/api.ts

export * from './api/list';
  • 组件中使用:
// path:src/components/xxx.tsx

import React from 'react';
import * as api from '../../services/api';

class HomePage extends React.Component<any> {
  /* 省略代码 */ 

  async loadListData () {
    try {
      const res = await api.getListData();
      this.setState({
        listData: res.data.list,
      });
    } catch (error) {
      // do something
    }
  }
  
  /* 省略代码 */ 
}

export default HomePage;

  • 以上则成功完成 fetch 请求的封装。

引入 react-loadable

  • 在使用React.js单页应用程序时,应用程序有增长的趋势。应用程序(或路径)的一部分可能会导入大量首次加载时不必要的组件。这会增加我们应用的初始加载时间。
  • 当我们使用yarn build 打包项目时, create-react-app 将生成一个大文件,它包含我们的应用程序所需的所有JavaScript。但是,如果用户只是加载登录页面进行登录;我们用它加载应用程序的其余部分是没有意义的。
  • 为了解决这个问题, create-react-app 有一个非常简单的内置方法来分割我们的代码,这个功能被称为代码分割(Code Splitting)。
  • 项目设置支持通过 动态import() 进行代码拆分。我们可使用一个叫react-loadable的第三方库,考虑了组件加载失败、加载中等多种情况。
  • 首先,安装react-loadable,同时,由于项目中我还使用了typescript,所以还要安装@types/react-loadable。安装命令:
yarn add react-loadable
yarn add @types/react-loadable --dev
  • 为了让入口文件看起来更加简洁,我将把路由配置分离出来放在routes.tsx文件中,在入口路由文件 App.tsx 中只需要将 routeData 引入使用即可:
// path:src/App.tsx

import { createHashHistory } from 'history';
import React from 'react';
import { Router } from 'react-router';
import routeData from './common/route';

const history = createHashHistory();

const App: React.FC = () => {
  return (
    <Router history={history}>
      <Switch>
        {routeData.map(({ path, component, exact }: IRouterItem) => (
          <Route key={path} path={path} component={component} exact={exact} />
        ))}
        <Route component={NotFound} />
      </Switch>
    </Router>
  );
};

export default App;

  • routes.tsx文件的内容大概如下:
// path:src/common/route.tsx

import * as React from 'react';
import Loadable from 'react-loadable';
import Loading from '../components/Loading';

const routeConfig: any = [
  {
    path: '/',
    component: asyncLoad(() => import('../views/HomePage')),
  },
  {
    path: '/detail/:id',
    component: asyncLoad(() => import('../views/DetailPage')),
  },
  /**
   * Exception 页面
   */
  {
    path: '/exception/404',
    component: asyncLoad(() => import('../views/Exception')),
  },
];

function generateRouteConfig (route: IRouteConfig[]) {
  return route.map(item => {
    return {
      key: item.path,
      exact: typeof item.exact === 'undefined' ? true : item.exact,
      ...item,
      component: item.component,
    };
  });
}

function asyncLoad (loader: () => Promise<any>) {
  return Loadable({
    loader,
    loading: props => {
      if (props.pastDelay) {
        return <Loading />;
      } else {
        return null;
      }
    },
    delay: 500,
  });
}

export default generateRouteConfig(routeConfig);
  • 通过封装动态加载路由的asyncLoad函数,可以实现只有在切换到对应路由的时候才渲染相关组件。

按需加载 antd

  • antd:是蚂蚁金服推出的一个很优秀的 react UI 库,其中包含了很多我们经常使用的组件。
  • 当我们没有进行任何配置直接在这个项目中使用antd库时,会在控制台看到如下提示:
    控制台提示
  • antd库大小大概有80M,全量引入该库必然会影响我们应用的网络性能,按需引入显得尤为重要。
  • 官方文档(传送门 👉antd 文档)提供了两种方式来实现antd的按需加载:
  • 本文采用 babel-plugin-import 来进行按需加载。下面介绍具体步骤。
  • 安装 antd
yarn add antd
  • 安装 babel-plugin-import
yarn add babel-plugin-import --dev
  • 在 config/webpack.config.js 文件中的 module 里面增加 rule 规则:
    module: {
      strictExportPresence: true,
      rules: [
        /* 省略代码 */
        {
          oneOf: [
            /* 省略代码 */
            /* 下面是原有代码块 */
            {
              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?-svgo,+ref![path]',
                        },
                      },
                    },
                  ],
            /* 上面是原有代码块 */
            
                  /* 下面是添加代码块 */
                  [
                    'import',{  // 导入一个插件
                      libraryName: 'antd',   // 暴露的库名
                      style: 'css' // 直接将antd样式文件动态编译成行内样式插入,就不需要每次都导入
                    }
                  ],
                ],
                /* 上面是添加代码块 */
                
                /* 下面是原有代码块 */
                cacheDirectory: true,
                cacheCompression: isEnvProduction,
                compact: isEnvProduction,
                /* 上面是原有代码块 */
              },
            },
          ],
        },
      ],
    },
  • 因此就可以实现antd组件的按需加载且无需每次都导入相关样式文件:
import { Button } from 'antd';
  • 注意:由于项目中实现了模块化的 less,如果要在模块内修改 antd 组件的样式,需要使用:global,如:
:global {
  .ant-divider {
    margin: 0 0;
  }
}
  • 以上则是全文的介绍内容,相关配置均经过本人呕心沥血的亲身实践,如有问题欢迎留言。