使用Webpack等搭建一个适用于React项目的脚手架(3 - Eslint、Jest)

2,945 阅读4分钟

《使用Webpack等搭建一个适用于React项目的脚手架(2 - React Router、Redux、Sass)》中记录了在项目中使用React-Router、Redux和Sass,以及引入图片。这篇文章主要记录在项目中使用Eslint检查代码风格以及使用Jest等进行单元测试。

使用Eslint

Eslint是用来让代码风格保持一致以及减少错误发生的。

Eslint官网推荐React检查使用eslint-plugin-react

这篇官方文档中得知,需要使用@typescript-eslint/parser进行TypeScript检查。在@typescript-eslint/parser的Github仓库中找到检查TypeScript的方法

本文按照上面提到的这些参考路径进行Eslint配置。(其实在安装好eslint之后,可以用npx eslint --init创建一个配置。执行npx eslint --init后会有一些提问,Eslint会根据回答创建配置文件并安装依赖。)

首先安装依赖:

npm i --save-dev eslint eslint-loader eslint-plugin-react
npm i --save-dev @typescript-eslint/parser @typescript-eslint/eslint-plugin

创建配置文件:touch .eslintrc.js。.eslintrc.js文件内容如下:

module.exports = {
  extends: [
    'eslint:recommended',
    'plugin:react/recommended',
  ],
  overrides: [
    {
      files: ['*.ts', '*.tsx'],
      extends: [
        'plugin:@typescript-eslint/eslint-recommended',
        'plugin:@typescript-eslint/recommended'
      ],
      parser: '@typescript-eslint/parser',
      plugins: [
        '@typescript-eslint/eslint-plugin',
      ],
      rules: {
        '@typescript-eslint/ban-ts-ignore': 'off',
        '@typescript-eslint/no-explicit-any': 'off',
        '@typescript-eslint/no-use-before-define': 'off'
      },
    }
  ],
  settings: {
    react: {
      version: 'detect',
    }
  },
  rules: {
    'react/jsx-no-undef': 'off'
  },
  env: {
    browser: true,
    es2020: true,
    node: true,
    commonjs: true,
  },
  globals: {
    React: true,
    ReduxConnect: true,
    Axios: true,
    UseEffect: true,
  },
  ignorePatterns: ['dist/'],
}

eslint:recommended表示启用Eslint推荐规则plugin:react/recommended启用React推荐规则。

overrides中进行配置,只对.ts文件和.tsx文件进行TypeScript相关的处理,比如只对.ts、.tsx文件使用@typescript-eslint/parser处理器。

设置react: { version: 'detect' }表示自动设置检查的React版本是安装的React的版本。

env中的配置表示支持browser、es2020、node、commonjs中预定义的全局变量。

globals中配置之前的步骤中设置的全局变量,比如React。

ignorePatterns配置忽略的文件,数组的每一个元素相当于.eslintignore中的一行,.eslintignore能覆盖ignorePatterns的配置。Eslint默认会忽略/node_modules/* 和 /bower_components/*文件目录,所以没在ignorePatterns中添加这两个目录。

根据需要关掉了几条规则:

1.关掉在React中不允许未声明变量的规则,因为我们全局设置了React变量,但是Eslint会报错error 'React' is not defined react/jsx-no-undef,所以把这个规则关掉。

'react/jsx-no-undef': 'off'

2.关掉不能在声明前使用的规则,还是因为已经在Webpack中定义了全局变量React,但是Eslint无法进行判断,所以会报错'React' was used before it was defined

'@typescript-eslint/no-use-before-define': 'off'

3.关掉不能使用// @ts-ignore的规则:

'@typescript-eslint/ban-ts-ignore': 'off',

4.关掉不能使用any定义类型的规则(当然尽量不要使用any定义类型):

'@typescript-eslint/no-explicit-any': 'off',

package.json中添加:

  "scripts": {
  	...
    "lint": "eslint . --ext .js,.jsx,.ts,.tsx"
  },

执行npm run lint,会看见检查的结果,根据提示调整代码。

在Webpack(config/webpack.dev.js)中配置如下,表示在对.js、.jsx、.ts、.tsx文件进行处理前先使用eslint-loader对文件进行Eslint检查。

    {
      enforce: 'pre',
      test: /(\.js(x?))|(\.ts(x?))$/,
      exclude: /[\\/]node_modules[\\/]/,
      loader: 'eslint-loader',
    },

执行npm start启动webpack-dev-server,当文件修改并保存之后会重新打包,在打包前会进行Eslint检查,可以根据检查结果进行代码调整。

使用Jest

Jest是一个专注于简化的JavaScript测试框架。本文参考Jest官方文档--使用Reactreact-testing-library--基础例子进行了简单的DOM测试。React Redux测试和React Router测试等可以查看react-testing-library--更多例子

安装依赖:

npm i --save-dev jest @testing-library/react

根据Jest官方文档--使用Babel的内容,安装Jest的时候自动安装了babel-jestbabel-jest会根据babel配置自动转换文件,(《使用Webpack等搭建一个适用于React项目的脚手架(1 - React、TypeScript)》中已经在.babelrc.js中进行了babel配置,所以这里不用再考虑如何处理tsx文件)。

在项目根目录创建一个__tests__文件夹。__tests__文件夹下包含测试文件,创建了3个文件夹分别表示测试组件、测试页面、测试函数。

├── __tests__
│   ├── components
│   ├── functions
│   └── pages
│       └── Novel.test.tsx

如果要对__tests__文件夹中的内容进行类型检查,需要安装jest类型定义包,并且在tsconfig.json文件夹中引入__tests__文件夹。

npm i --save-dev @types/jest

在tsconfig.json的include中添加上__tests__目录:

	...
  "include": [
    ...
    "__tests__/*",
    "__tests__/**/*",
  ]
}

1.写一个例子

调整src/pages/Novel/Novel.tsx文件内容如下:

export default function Novel(): JSX.Element {
  const [ novels, setNovels ] = UseState(['一号小说', '二号小说']);
  function deleteNovel (index): void {
    if (!novels[index]) return;
    const nextNovels = [...novels];
    nextNovels.splice(index, 1);
    setNovels(nextNovels);
  }
  return (
    <div>
      <ul>
        {novels.map((novel, index) => {
          return <li key={index} onClick={deleteNovel.bind(this, index)} style={{ cursor: 'pointer' }}>{novel}</li>;
        })}
      </ul>
    </div>
  );
}

组件渲染小说列表,点击某选项就删除该选项。比如点击渲染出的<li>一号小说</li><li>一号小说</li>就会被删除。

tests/pages/Novel.test.tsx文件内容如下:

import { render, fireEvent, screen } from '@testing-library/react';

import Novel from '@/pages/Novel/Novel.tsx';

test('novel item should not exist after deletion', () => {
  const novelName = '一号小说';
  render(<Novel />);

  expect(screen.queryByText(novelName)).toBeTruthy();

  fireEvent.click(screen.queryByText(novelName));

  expect(screen.queryByText(novelName)).toBeNull();
});

这个测试文件简单地测试以下内容:

​ 组件渲染成DOM后,textContent为'一号小说'的元素(A元素)是存在的,点击A元素之后,DOM中就没有A元素了。

关于代码中用到的test、fireEvent等更详细的信息可以查看Jest官网--使用matcherstesting-library官网firEvent模拟事件

2.Jest配置

在前几篇文章的代码中,我们已经设置了React等全局变量,以及使用@作为src目录的路径别名。Jest中也需要做相应的配置,否则执行npx jest会报错。

在根目录下创建一个jest.config.js文件,用于进行jest配置

touch jest.config.js

jest.config.js文件内容如下:

module.exports = {
  setupFiles: ['<rootDir>/__tests__/jest.setup.js'],
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
  },
  testPathIgnorePatterns: ['<rootDir>/__tests__/jest.setup.js', '/node_modules/']
}

(1)配置全局变量

jest配置中有一个globals属性用于配置全局变量,但是globals对象设置的对象必须是JSON可序列化的,而函数在使用JSON.stringify()转换的时候会被忽略,所以需要使用setupFiles属性。<rootDir>是jest配置文件所在的文件目录。

jest.setup.js文件内容如下:

import React, { useState } from 'react';

global['React'] = React;
global['UseState'] = useState;

因为这里要测试的组件只用到的React和UseState,所以只配置了React和useState,需要用到别的全局变量时,可以按照相同的方法配置。

(2)配置路径别名

在moduleNameMapper属性中设置'^@/(.*)$': '<rootDir>/src/$1'用于配置路径别名,就像之前我们在Webpack和TypeScript中配置的那样,用@代表src目录。一开始我使用的是'^@(.*)$': '<rootDir>/src$1',这样执行npx jest的时候会报错:

  ● Test suite failed to run

    Configuration error:
    
    Could not locate module @babel/code-frame mapped as:
    .../simple-scaffold02/src$1.

经过反复横跳检查,两边的正则表达式都是没错的,但是仔细观察报错信息,发现这样配置把@babel/code-frame引用也映射到src路径下了,然而src路径下是没有babel/code-frame的,自然报错了。所以需要以加一根斜杠的方式匹配路径'^@/(.*)$': '<rootDir>/src/$1'

(3)设置忽略文件

Jest默认会将匹配正则表达式(/__tests__/.*|(\\.|/)(test|spec))\\.[jt]sx?$的文件作为测试文件,前面的jest.setup.js文件是放在__tests__文件夹中的,但它是用来设置全局变量的,并不是测试文件。所以需要在testPathIgnorePatterns中配置忽略它。

(4)Webpack和TypeScript新增配置。

因为新增了一个全局变量UseState代表React的useState模块,所以在Webpack和TypeScript中也需要加上这个全局变量:

config/webpack.common.js

    new webpack.ProvidePlugin({
    	...
      UseState: ['react', 'useState'],
    }),

typings/react.d.ts

import React, { useEffect, useState } from 'react';

declare global {
	...
  const UseState: typeof useState;
}

(5)修改package.json

  "scripts": {
  	...
    "test": "jest"
  },

执行npm run test执行测试用例,得到结果:

> jest

 PASS  __tests__/pages/Novel.test.tsx
  ✓ novel item should not exist after deletion (33ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        3.241s

(6)检查代码风格是否统一

执行npm run lint进行检查,发现因为新创建的jest.setup.js文件中的import React, { useState } from 'react';造成了一个报错:

error  Parsing error: 'import' and 'export' may appear only with 'sourceType: module

只有配置了sourceType: module才能使用import和export。之前的代码中也用了import,但因为是在ts文件和tsx文件中,所以eslint没有报这个错。

在**.eslintrc.js**中添加上以下内容即可:

module.exports = {
	...
  parserOptions: {
    sourceType: 'module',
  },
}

其他

1.根据需要忽略不想使用git上传的文件

touch .gitignore

文件内容如下:

.DS_Store
.vscode

dist
node_modules

比较全的gitignore配置可以参考gitignore例子合集

2.创建README.md

# npm start
  start up project.
# npm run build
  build for production environment.
# npm run buid:dev
  build for development environment.
# npm run lint
  use elint to find problems in code.
# npm run test
  user jest to test code.

下一篇:《使用Webpack等搭建一个适用于React项目的脚手架(4 - 优化)》