从零搭建React企业级项目

2,278 阅读14分钟

写在前面

这篇文章致力于从npm init 一步一步搭建react企业级项目, 主要包含一下几点:

  • webpack配置和优化
  • react-router 基本配置和使用
  • react-redux 基本配置和使用
  • 项目规范和风格配置

webpack相关配置参考这篇文章 webpack配置

首先奉上项目地址react-base-project

0.基础文件

  1. mkdir react-pro && cd react-pro
  2. npm init -y
  3. touch .gitignore
.DS_Store
.vscode
node_modules/
dist/
npm-debug.log
yarn.lock
package-lock.json

1.安装依赖包

1. webpack5相关依赖

npm i webpack webpack-cli webpack-dev-server --save-dev

2.babel7-编译javascript文件

npm i @babel/core babel-loader --save-dev

presets 使用babel需要安装的一组插件(也就是支持哪些语法转换成es5)

依赖包描述
@babel/core调用babel api进行转码的核心库,babel-loader的核心依赖
babel-loaderwebpack编译js文件的loader

3. html文件输出

npm i html-webpack-plugin --save-dev

根目录新建index.html和favicon.ico

src目录新增入口文件(index.js)

document.querySelector('#root').innerHTML='<h1>Hello,Javascript</h1>';

2.webpack相关基础配置

  1. 根目录下新建config目录
  • 统一管理node环境所有目录(paths.js)
const path = require('path');
const fs = require('fs');
const appDirectory = fs.realpathSync(process.cwd()); //项目根目录
const resolveApp = (relativePath) => path.resolve(appDirectory, relativePath);

module.exports = {
  rootDir: appDirectory,
  appIndex: resolveApp('src/index'), //入口文件
  appSrc: resolveApp('src'), //项目代码主目录
  appDist: resolveApp('dist'), //打包目录
  appHtml: resolveApp('index.html'), //模板文件
  appPages: resolveApp('src/pages'),
  appStatic: resolveApp('src/static'),
  appUtil: resolveApp('src/util'),
  appInterfaces: resolveApp('src/interfaces'),
  apis: resolveApp('src/apis'),
  appNodeModules: resolveApp('node_modules'),
};
  • 基础配置文件(webpack.config.js)
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');

const paths = require('./paths');
module.exports = function (webpackEnv) {
  const host = process.env.HOST || '0.0.0.0';
  const isEnvDevelopment = webpackEnv === 'development';
  const isEnvProduction = webpackEnv === 'production';
  let webpackConfig = {
    mode: isEnvProduction ? 'production' : 'development',
    target: 'web',
    entry:{
      app: paths.appIndex
    },
    output: {
      path: paths.appDist,
      publicPath: '/',
      filename: `static/js/[name]${isEnvProduction ? '.[contenthash:8]' : ''}.js`,
      clean: true,
    },
    module: {
      rules: [
        {
          test: /\.jsx?$/,
          loader: 'babel-loader',
          include: paths.appSrc,
        },
      ],
    },
    plugins: [
      new HtmlWebpackPlugin(
        {
        inject: true,
        filename: 'index.html',
        template: paths.appHtml,
        favicon: 'favicon.ico',
      }),
    ],
    devServer: {
      host,
      allowedHosts: 'all',
      compress: true,
      port: 9006,
      historyApiFallback: true,
      open: false,
      hot: true,
    },
  };
  return webpackConfig;
};
  • 用于启动开发环境的配置文件(start.js)
const configFactory = require('./webpack.config');
const config = configFactory('development');

module.exports = config;

至此,最基础的版本已完成,目录如下:

.
├─ .gitignore
├─ config
│    ├─ paths.js
│    ├─ start.js
│    └─ webpack.config.js
├─ favicon.ico
├─ index.html
├─ package-lock.json
├─ package.json
└─ src
     └─ index.js

运行webpack server --config config/start.js可在浏览器看到

image.png

3.加入React

1. react相关依赖

npm i react react-dom --save
依赖包描述
reactreact的核心库
react-dom从react中剥离的涉及DOM操作的部分

react:react的核心思想是虚拟DOM;主要包括:React.createElement生成虚拟DOM,Component相关的React.createClass,React.Component,React.Children

react-dom:v0.14+从react核心库中拆离;负责浏览器和DOM操作。还有一个兄弟库react-native,用来编写原生应用。

react-dom主要包括方法有:

方法描述
render将虚拟DOM渲染到真是DOM中
hydrate服务端渲染,避免白屏
unmountComponentAtNode从 DOM 中移除已装载的 React 组件
findDOMNode访问原生浏览器DOM
createPortal渲染react子元素到制定的DOM中

2. react根组件

  • 项目入口文件(index.js)
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));
  • 项目根组件(App.js)
import { hot } from 'react-hot-loader/root';
import React, { Component } from 'react';

class App extends Component {
  render() {
    return <h1>hello-react</h1>;
  }
}
export default hot(App);

3. babel预设解析react

什么是preset,可以理解为是一系列plugin的集合

npm i @babel/preset-env @babel/preset-react @babel/plugin-proposal-class-properties --save-dev

presets 使用babel需要安装的一组插件(也就是支持哪些语法转换成es5)

presets描述
@babel/preset-envES语法分析包,根据运行环境对代码做相应的编译
@babel/preset-react编译react语法
@babel/plugin-proposal-class-properties支持class语法插件

像上面的react语法如果没有@babel/preset-react,则会报错↓ image.png

babel7发布后的变化:

  1. 弃用年份preset和babel-preset-stage-x(stage-0/1/2/3/4)

@babel/preset-env替换之前所有的babel-prese-es20xx和babel-preset-stage-x,提案的5个阶段,0表示只是一个想法,4表示已完成

  1. 重命名:Scoped Packages(@babel/x) 解决:命名困难;是否被他人占用;区分官方包名

  2. 重命名:-proposal-

任何提案都将被以 -proposal- 命名来标记他们还没有在 JavaScript 官方之内。

所以 @babel/plugin-transform-class-properties 变成 @babel/plugin-proposal-class-properties,当它进入 Stage 4 后,会把它命名回去。

Babel 几乎可以编译所有最新的 JavaScript 语法(例如箭头函数、const、对象解构),但对于 APIs 来说却并非如此。例如: Promise、Set、Map 等新增对象,Object.assign、Object.entries等静态方法。这时候就需要polyfill来解决了。

APIs-polyfill方案

为了达成使用这些新API的目的,社区又有2个实现流派:@babel/polyfill和babel-runtime+babel-plugin-transform-runtime

  1. @babel/polyfill用来处理APIs的兼容问题;通过修改全局变量的方式实现。
    如:[1,2,3].includes(1) 如果不进行兼容处理,打包出来就是直接是调用Array原型includes方法,在低版本浏览器可能会有兼容问题。

安装和配置:

npm install --save core-js@3 @babel/polyfill
"presets": [
    [
      "@babel/preset-env",
      {
        "modules": false,
+       "useBuiltIns": "usage", //设置entry,需要手动在代码顶部导入@babel/polyfill,否则没有polyfill;usage 会分析项目使用了哪些自动导入相应的polyfill
+       "corejs": 3
      }
    ]
  ],

下面两张图可以看出,core-js通过修改Array的原型对象对数组扩展; 这样会带来一个问题,如果第三方库也修改了原型方法,可能会导致冲突

image.png image.png 注意:如果没有安装core-js的话,打包会报错 image.png

问题2:babel转义js代码需要,会在每个bundle中生成工具函数,如果bundle较多时,会额外增加代码体积(这个问题不是使用core-js导致的问题,是babel转义时产生的)

辅助函数代码如下:

为了解决以上两个问题,有了方案2

  1. @babel/plugin-transform-runtime

@babel/plugin-transform-runtime插件作用是,如发现源码中有Promise、Map等新类型时自动按需加入合适的polyfill以解决兼容问题(不需要每个模块手动导入import Promise from 'babel-runtime/core-js/xxx')。同时解决了使用@babel/polyfill导致的污染全局变量和辅助代码复用的问题。需要用到两个依赖包@babel/runtime-corejs3:加载必要的新API;@babel/runtime:提供帮助程序。

//注意:用方案2就不需要方案1的两个包
npm install --save-dev @babel/plugin-transform-runtime
npm install --save @babel/runtime @babel/runtime-corejs3
"plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "corejs": 3
      }
    ]
 ]

image.png image.png

  1. 根目录下新建.babelrc
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "modules": false
      }
    ],
    "@babel/preset-react"
  ],
  "plugins": [
    "@babel/plugin-proposal-class-properties",
    [
      "@babel/plugin-transform-runtime",
      {
        "corejs": 3
      }
    ]
  ]
}

4.Resolve配置

webpack启动后会从入口模块分析,找出所有依赖的模块,resolve配置就是告诉webpack,如何寻找对应的文件,webpack内置JavaScript模块化语法解析功能,默认会采用模块化标准里约定好的规则去寻找,但你可以根据自己的需要修改默认的规则

    1. alias(别名) resolve.alias配置项通过别名来把导入路径映射成一个新的路径

下面的伪代码示例:

const paths = require('./paths');
alias: {
    '@pages': paths.appPages,
    '@util': paths.appUtil,
},

这里的路径最好用绝对路径,在项目中就不会存在替换后的路径问题了

import bridge from '@util/bridge'; 
//'@util/bridge'等价于'{项目在计算机中的目录地址}/react-pro/src/util'
  • extensions(扩展名) 在导入路径中如果没有文件后缀,webpack会自动加上后缀尝试查找文件是否存在,resolve.extensions用于配置在尝试过程中用到的后缀列表,默认是:
extensions: ['.js', '.json']

这里由于新加了react,所以需要修改默认配置为

extensions: ['.js', '.jsx', '.json']

4.资源文件配置

1.样式(less,scss,css)文件配置

项目实际开发中还需要样式文件

细节可以参考另一篇webpack配置- 样式类,下面是loader配置

1. 安装依赖包

npm i node-sass sass-loader postcss-loader autoprefixer css-loader style-loader mini-css-extract-plugin --save-dev

2.新增loader和plugins

const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const getStyleLoaders = ({modules = true}) =>
[
  isEnvProduction ? MiniCssExtractPlugin.loader : 'style-loader',
  {
    loader: 'css-loader',
    options: {
      modules: modules ?  {
        localIdentName: '[name]-[local]-[hash:base64:5]',
      } : false,
    },
  },
  'postcss-loader',
  'sass-loader',
].filter(Boolean);


module: {
    rules:[
        {
          test: /\.(sc|c)ss$/,
          use: getStyleLoaders({ modules:true }),
          include: paths.appSrc,
        }
    ]
},
plugins:[
    isEnvProduction && new MiniCssExtractPlugin({
      filename: 'static/css/[name].[contenthash:10].css',
    })
].filter(Boolean)

3.postcss配置

//postcss.config.js
module.exports = {
  plugins: autoprefixer: {}
};

4.添加兼容浏览器列表(package.json)

"browserslist": ["iOS >= 8","> 1%","Android > 4","last 5 versions"]

5.项目入口引入全局样式文件

import './assets/styles/app.less';

重新启动项目,可以看到样式已经插入head中

2.图片和字体类

参考:webpack5内置静态资源构建能力

loader新增:

{
  test: /\.(gif|png|jpe?g|svg)(\?.*)?$/,
  type: 'asset',
  generator: {
    filename: 'static/img/[name][ext]?[hash]',
  },
  parser: {
    dataUrlCondition: {
      maxSize: 10 * 1024,
    },
  },
},
{
  test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
  type: 'asset/resource',
  include: [paths.appSrc],
  generator: {
    filename: 'fonts/[name].[hash:7][ext]',
  },
},

3.模块热替换

参考:模块热替换方案-HotModuleReplacementPlugin(HMR)

5.路由

npm install react-router-dom --save

首先当然是依赖包,为什么是react-router-dom而不是react-router
react-router4.0.0+版本;官方提供了一套基于react-router封装的(包含react-router所有内容)用于运行在浏览器端的react-router-dom和用于开发reactNative应用的react-router-native

React Router中有三类组件

  • router组件(BrowserRouter,HashRouter)
  • route matching组件(Route,Routes)
  • navigation组件(Link)

Router设置

基于React Router的web应用,根组件应该是一个router组件;react-router-dom提供了两种路由模式;
<BrowserRouter>:使用HTML5 提供的 history API (pushState, replaceState 和 popstate 事件),动态展示组件
<HashRouter>:通过监听window.location.hash的改变,动态展示组件

最直观的感受就是BrowserRouter不会再浏览器URL上追加#,为了地址的优雅当然首选这种模式,但如果是静态服务器,那就只能使用备选方案HashRouter了。

Route路由匹配

  • React Router提供两个匹配路由的组件<Route><Routes>

路由匹配是通过将组件的path属性与当前locationpathname进行匹配,当一个组件匹配了,则展示

我们可以在组件树的任何位置放置<Route>组件。但是更常见的情况是将几个<Route>写在一起。<Routes>组件可以用来将多个<Route>“包裹”在一起。

新建路由组件(src/routes/index.js)。

<BrowserRouter>
    <Routes>
      <Route path="/" element={<Home/>} />
      <Route path="/shopping" element={<Shopping/>} />
      <Route path="/detail/:id" element={<Detail/>} />
      {/* 如果上面的Route的路径都没有匹配上,则 <NoMatch>被渲染,我们可以在此组件中返回404 */}
      <Route path="*" element={<NoMatch/>} />
    </Routes>
</BrowserRouter>

useSearchParams获取路由参数

Link 导航组件

//to: string
<Link to="/about?tab=name" />

//to: object
<Link
  to={{
    pathname: "/courses",
    search: "?sort=name",
    hash: "#the-hash",
    state: { fromDashboard: true } //传入下一个页面额外的state参数
  }}
/>

useNavigate以编程方式导航

在不同的React版本中,使用方法稍有差异,下面总结了各版本的使用方法

  1. ReactRouter 6 (using hooks and React >16.8) 你可以使用 useNavigate hook 在函数组件中使用编程时导航,ReactRouter5使用useHistory
import { useNavigate } from "react-router-dom";

function HomeButton() {
  let navigate = useNavigate();
  navigate('/some/path', { replace: true })
};
  1. React-Router 4.0.0+ 在4.0+, 在组件中使用props中的 history 对象
class Example extends React.Component {
   // use `this.props.history.push('/some/path')` here
};
  1. React-Router 3.0.0+ 在3.0+, 在组件中使用props中的 router.
class Example extends React.Component {
   // use `this.props.router.push('/some/path')` here
};

5. 状态管理

npm install redux react-redux --save

redux是一个“可预测的状态容器”,参考了flux的设计思想,

Redux三大原则

  1. 单一数据源
    一个应用只有唯一的数据源,好处是整个应用的状态都保存在一个对象中,这样可以随时去除整个应用的状态进行持久化;当然如果一个复杂项目也可以用Redux提供的工具函数combineReducers对数据进行拆分管理。

  2. 状态是只读的
    React并不会显示定义store,而使用Reducer返回当前应用的状态(state),这里并不是修改之前的状态,而是返回一个全新的状态。
    React提供的createStore方法会根据Reducer生成store,最后可以用store.disputch方法修改状态。

  3. 状态修改均由纯函数完成
    这使得Reducer里对状态的修改变得简单、纯粹

Redux核心API

Redux的核心是一个store,这个store由Redux提供的createStore(reducers[,initalState])方法生成。
reducers必传参数用来响应由用户操作产生的action,reducer本质是一个函数,其函数签名为reducer(previousState,action)=>newState;reducer的职责就是根据previousState和action计算出新的state;在实际应用中reducer在处理previousState时,需要有一个非空判断。很显然,reducer第一次执行的时候没有任何previousState,而reducer的职责时返回新的state,因此需要在这种特殊情况返回一个定义好的initalState。

与React绑定

Redux 官方提供的 React 绑定-react-redux。这是一种前端框架或类库的架构趋势,即尽可能做到平台无关。 react-redux提供了一个组件和一个API,一个是React组件,接受一个store作为props,它是整个Redux应用的顶层组件;一个是connect(),它提供了在整个React应用的任意组件中获取store中数据的功能。

项目使用redux

  1. 入口文件加入Provider组件
import { Provider } from 'react-redux';
import store from './redux/index';
ReactDOM.render(<Provider store={store}><App /></Provider>, rootEl);
  1. 创建store文件
import reducers from './reducers/index'
export default createStore(reducers);
  1. 创建reducers文件
export default (state=[],action)=>{
 switch (action.type){
   case 'RECEIVE_PRODUCTS':
     return action.products;
   default:
     return state;
 }
}
  1. 容器组件中dispatch触发reducers改变state
import { connect } from 'react-redux'

const ProductsContainer = ({products,getAllProducts}) => (
    <button onClick={getAllProducts}>获取数据</button>
)
const mapStateToProps = (state) => ({
  products:state.products
})
const mapDispatchToProps = (dispatch, ownProps)=> ({
  getAllProducts:() => {
    dispatch({ type: 'RECEIVE_PRODUCTS', [1,2,3]})
  }
})
export default connect(mapStateToProps, mapDispatchToProps)(ProductsContainer)

6.环境全局变量

项目中测试环境和生产环境常常有些全局变量是不同的;最典型的api接口域名部分、跳转地址域名部分; 我们可以在webpack的plugin中设置DefinePlugin:

//向浏览器环境注入全局变量,非window下
new webpack.DefinePlugin({
    'process.env': env //env 获取本地的静态文件
})

但在webpack node环境中还不能区分测试和生产环境,因为webpack build打包向node注入的NODE_ENV都是produiction,所以process.env.NODE_ENV是相同的。

这里结合cross-env向node环境手动注入一个标记参数NODE_ENV_MARK;package代码如下:

"scripts": {
    "dev": "cross-env NODE_ENV_MARK=dev webpack-dev-server --config config/start.js",
    "build:test": "cross-env NODE_ENV_MARK=test node config/build.js",
    "build:prod": "cross-env NODE_ENV_MARK=production node config/build.js"
  }

webpack.config.js中根据NODE_ENV_MARK变量获取对应的文件:

const env = require(`../env/${process.env.NODE_ENV_MARK}.env`);

env目录下添加dev.env.js/test.env.js/production.env.js;文件内容根据实际情况进行编辑

module.exports = {
  NODE_ENV: '"production"',
  prefix: '"//api.abc.com"'
};

这样在浏览器环境中就可以使用process.env.prefix变量了。

到此项目配置基本告一段落,一下是对项目进行的一些优化。

7.项目优化

webpack相关优化可以参考优化

8.项目规范和风格配置

eslint:代码风格和语法规则检查工具

ESLint 是一种检验JavaScript代码格式的工具,目标是使代码更加一致并避免错误, ESLint是一个代码限制的工具 注意:这里的格式不仅包含风格,还包括一些最佳实践,(比如for in循环对象时,eslilt就建议用Object.keys(obj),然后循环数组,这样避免for in循环出原型对象)

  1. 安装 npm install eslint --save-dev
  2. 初始化配置文件 npx eslint --init(根据项目情况选择)
module.exports = {
    //脚本运行的环境
    "env": {
        "browser": true,
        "es2021": true
    },
    "extends": [
        "plugin:react/recommended",
        "airbnb"
    ],
    //如果未使用ts请用 @babel/eslint-parser 解析
    "parser": "@typescript-eslint/parser",
    "parserOptions": {
        "ecmaFeatures": {
            "jsx": true
        },
        "ecmaVersion": 13,
        "sourceType": "module"
    },
    // 第三方插件为ESLint定义了额外的rules、env、配置
    "plugins": [
        "react",
        "@typescript-eslint"
    ],
    //启用的规则
    "rules": {
    }
};

初始化后会安装一下依赖

  • eslint@8.6.0 JavaScript检查器,eslint核心库
  • @typescript-eslint/parser@5.9.1 Typescript语法的解析器
  • eslint-config-airbnb@19.0.4 Airbnb的ESLint配置,作为一种扩展的共享配置
  • eslint-plugin-import@2.25.4 ES6+ import/export语法校验,防止文件路径和名称拼写错误的问题
  • eslint-plugin-react@7.28.0 分析react特性
  • eslint-plugin-jsx-a11y@6.5.1 检查JSX语法规范
  • eslint-plugin-react-hooks@4.3.0
  • @typescript-eslint/eslint-plugin@5.9.1

这时候运行eslint src/index.tsx 会报错

image.png 项目中使用了typescript,需要安装eslint-config-airbnb-typescript,增强Airbnb的ESLint,其实就是针对ts的一些配置和关闭一些rules

eslintrc.js更新

extends: [
  'airbnb',
+ 'airbnb-typescript'
]
+ parserOptions: {
+   project: './tsconfig.json'
+ }
+ ignorePatterns: ['.eslintrc.js'],
  1. 忽略文件 根目录下新建.eslintignore,配置的目录或文件将不进行eslint格式校验
**/dist/**
**/node_modules/**
**/config/**

如果是新项目加入eslint,extends建议使用airbnb,这样会约束你编写出更加优雅的代码,这样渐渐的也就会成为你的编码风格

如果是老项目加入eslint,extends建议使用"eslint:recommended""plugin:react/recommended"

这里项目中使用airbnb;这时候运行npx eslint src,会发现有很多类似这种的报错 大部分报错都是编码风格的报错

可以使用npx eslint src --fix;可以自动修复编码风格问题,(比如使用单双引号singleQuote、行位是否加分号semi);在我理解自动修复不会新增行或者移动代码。
运行之后,发现还剩下类似这种的报错,剩下的就需要手动修复了 如果是vscode编辑器,可以安装eslint插件并配置,开启保存自动格式化,方便查看报错提示和修改

"eslint.validate": [
    "javascript",
    "javascriptreact"
],
"editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
 }

老版本的项目如果配置了resolve.alias,还会出现一下问题: eslint报错找不到路径;安装插件并新增以下配置即可解决

npm install eslint-plugin-import eslint-import-resolver-alias --save-dev
settings: {
    'import/resolver': {
      alias: {
        map: [
          ['@redux', paths.appRedux]
          ['@pages', paths.appPages]
          ['@util', paths.util]
        ],
      },
    },
}

但这时你会发现ctrl/command+鼠标左键无法识别路径,开发体验不是很好。

在根目录新建jsconfig.json

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@redux/*": ["src/redux/*"],
      "@pages/*":["src/pages/*"],
      "@util/*":["src/util/*"]
    }
  }
}

stylelint规范sass文件

  1. 安装
npm install --save-dev stylelint stylelint-config-standard-scss
  1. 配置
    根目录新增.stylelintrc.js
module.exports = {
  "extends": "stylelint-config-standard-scss", //扩展三方配置
  "ignoreFiles": ["node_modules/**/*.scss"],
  "rules": {
    "selector-pseudo-class-no-unknown": [
      true,
      {
        "ignorePseudoClasses": ["global"]
      }
    ]
  }
};

rules分三类

  • Possible errors 语法类错误 大部分不可 --fix修复
  • Limit language features 限制写法类 比如小数点后默认只能有4位、不允许0后面的单位、透明值不允许百分比,少部分可以--fix修复
  • Stylistic issues 编码风格类 比如16进制颜色指定大小写、url值是否需要引号、大括号开始/结束是否需要换行,此类基本都可以--fix修复

规则列表中如果带有(Autofixable)的,--fix都是可以自动修复的,其他的需要手动修复。

VSCode 自动格式化配置

安装ESlintStylelint 插件

"editor.codeActionsOnSave": { 
    "source.fixAll.eslint": true,
    "source.fixAll.stylelint": true
}, 
"eslint.validate": [ "vue","html","javascript","typescript","javascriptreact","typescriptreact" ], 
"eslint.alwaysShowStatus": true, 
"stylelint.validate": [ "css", "less", "postcss", "scss", "vue", "sass" ],

Prettier

一款用于格式化代码风格的工具,它的设计哲学是:Opinionated,就是强制性用官方的风格,只提供少量的配置选项(20个左右),目的在于停止关于代码风格的争论。

上面已经用了eslint,为什么还需要引入Prettier呢?在我理解eslint职责在于检测代码是否符合rules规则, 不符合的会给出提示,--fix还可以解决一些风格问题;prettier用于格式化代码风格避免这些报错;当然prettier无法格式化代码质量和语法类问题,这些还需要eslint来解决(手动解决)。

  1. 安装
npm i -save-dev --save-exact prettier
npm i -save-dev eslint-config-prettier eslint-plugin-prettier
presets描述
eslint-config-prettiereslintprettier兼容,关闭所有不必要或可能与prettier冲突的规则
eslint-plugin-prettier运行eslint会静默prettier检查,反馈给eslint显示错误
prettier-eslintprettier输出传递给eslint --fix
stylelint-config-prettierstylelintprettier兼容
  1. eslint修改配置
extends: [
    'airbnb',
    + 'plugin:prettier/recommended' //需放在最后,不然会被其他规则冲掉,此写法已包括在plugins中加prettier
]
  1. 配置package.json命令
"scripts": {
    "format": "prettier --write \"src/**/*.{js,jsx,scss}\""
}

运行 npm run format 发现文件已经被格式化,但语法错误和一些代码质量问题还是需要手动修改

如果出现保存格式化两次,是eslint和prettier冲突了,需在extends中加入覆盖规则

husky 添加git hooks

上面配置了检测代码的eslint和stylelint,如何让每次提交的代码都符合规范,还需要借助自动化工具

  1. 安装
npm i -D husky
npx husky install

执行husky installgit hooks的目录指定为.husky/

npm install后自动启用hooks

npm set-script prepare "husky install"

prepareNPM 操作生命周期中的一环,在执行 install 的时候会按生命周期顺序执行相应钩子: NPM7: preinstall -> install -> postinstall -> prepublish -> preprepare -> prepare -> postprepare

  1. .husky/中添加hook
  • pre-commit 提交前验证eslint规则
"scripts": {
  "prepare": "husky install",
  "lint": "eslint src --ext .jsx && stylelint \"./src/**/*.scss\""  
}
npx husky add .husky/pre-commit "npm run lint"

执行commit提交会发现报错,并阻止了代码提交,这样可以避免把错误代码提交到线上导致线上报错。 代码报错修复完,即可提交。

这个方式虽然可以解决提交前校验的问题,但项目庞大后,修改一个文件后,会校验所有的文件,比较费时,lint-staged可以解决这个问题

npm i -D lint-staged
"lint-staged": {
    "src/**/*.scss": [
      "stylelint --fix"
    ],
    "src/**/*.{ts,tsx,js,jsx}": "eslint --fix"
},

在.kusky文件夹内修改pre-commit

#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

npx --no-install lint-staged

这样commit文件后就只校验修改的文件了

  • commit-msg 规范提交信息
npm install --save-dev @commitlint/config-conventional @commitlint/cli

在根目录新建 commitlint.config.js

module.exports = { extends: ["@commitlint/config-conventional"] };

.husky创建commit-msg hook

npx husky add .husky/commit-msg 'npx --no -- commitlint --edit "$1"'

Commit message格式,注意冒号后面有空格

<type>(<scope>): <subject>

type:用于说明 commit 的类别,只允许使用下面7个标识,也可以自己在配置文件中更改或者扩展。

type说明
feat新功能(feature)
fix修补bug
docs文档变更(documentation)
style代码格式(不影响功能,例如空格、分号等格式修正)
refactor代码重构
perf性能优化
build变更项目构建或外部依赖(例:scopes: webpack、gulp、npm)
test测试
ci更改持续集成软件的配置
chore构建过程或辅助工具的变动
revert代码回滚

scope:(可选)说明commit影响的范围;在业务项目中可以根据菜单或功能木块划分 subject: commit 的简短描述,不能超过72个字符,且结尾不加英文句号。

如果type为feat和fix,则该 commit 将肯定出现在 Change log 之中

交互式commit

npm i -D commitizen cz-conventional-changelog

package.json

"config":{ 
    "commitizen":{ "path":"node_modules/cz-conventional-changelog" } 
    "scripts": { "commit": "git-cz",}
}

提交代码可以执行

npm run commit

项目地址

github.com/futurewan/r…

文章有什么问题欢迎评论区讨论~