乐趣区

gulp构建react项目十根据不同环境进行构建配置eslintsourcemap

背景
在项目工程化中,通常根据不同的环境对项目进行构建,一般环境主要有开发、测试、预生产、生产。开发环境下关注规范、测试,而其他环境主要关注性能、安全,比如开发环境下进行 eslint 检查,而其他环境不做 eslint 检查。

项目相关依赖

cnpm i -D cross-env gulp-if gulp-eslint babel-eslint gulp-sourcemaps exorcist

exorcist用于在外部生成 map 文件
目录结构

gulp
├── src
│   ├── components
│   │   ├── Test
│   │   |   └── index.jsx
│   │   ├── Child
│   │   |   ├── index.css
│   │   |   └── index.jsx
│   │   ├── Count
│   │   |   ├── index.css
│   │   |   └── index.jsx
│       └── App.jsx 
├── node_modules
├── .eslintignore
├── .eslintrc.js
├── index.js
├── gulpfile.js
├── index.html
└── package.json

package.json 执行脚本

"scripts": {
    "dev": "cross-env NODE_ENV=dev gulp dev",
    "build": "cross-env NODE_ENV=prod gulp build"
  },

脚本

// index.js
import './src/App.jsx'

// src/App.jsx
import React from 'react'
import {render} from 'react-dom'
import Test from './components/Test/index.jsx'

render(<Test />, document.getElementById("app"))

// src\components\Test\index.jsx
import React from 'react'
import Child from '../Child/index.jsx'
import Count from '../Count/index.jsx'

export default class Test extends React.Component {
  state = {msg: 'hello, world'}

  render() {const { msg} = this.state
    return (
      <React.Fragment>
        <Child msg={msg}/>
        <Count />
      </React.Fragment>
    )
  }
}

// src\components\Count\index.jsx
import React from 'react'

export default class Count extends React.Component {
  state = {
    count: 0,
    msg: '显示一个计数器'
  }

  subFn = () => {this.setState((prevState, props) => ({count: prevState.count - 1}))
  }

  plusFn = () => {this.setState((prevState, props) => ({count: prevState.count + 1}))
  }

  render() {const { count, msg} = this.state
    return (
      <div>
        <p className="count-text">{msg}</p>
        <div>
          <button onClick={this.subFn}>-</button>
          <span>{count}</span>
          <button onClick={this.plusFn}>+</button>
        </div>
      </div>
    )
  }
}

// src\components\Count\index.css
.count-text {
  font-size: 14px;
  font-family: Arial, Helvetica, sans-serif;
  font-weight: 400;
  color: red;
}

// src\components\Test\index.jsx
import React from 'react'

const Child = (props) => <div className="color">{props.msg}</div>

export default Child

// src\components\Test\index.css
.color {color: red;}

构建脚本

const gulp = require('gulp')
const path = require('path')
const babelify = require('babelify')
const browserify = require('browserify')
    // 转成 stream 流,gulp 系
const source = require('vinyl-source-stream')
    // 转成二进制流,gulp 系
const buffer = require('vinyl-buffer')
const {series, parallel} = require('gulp')
const rm = require('rimraf')
const concat = require('gulp-concat')
const sass = require('gulp-sass')
const cheerio = require('gulp-cheerio')
const uglify = require('gulp-uglify')
const cssmin = require('gulp-cssmin')
const minifyHtml = require('gulp-minify-html')
const rename = require('gulp-rename')
const resversion = require('gulp-res-version')
const connect = require('gulp-connect')
const gulpif = require('gulp-if')
const eslint = require('gulp-eslint')
const sourcemaps = require('gulp-sourcemaps')
const exorcist = require('exorcist')

const PORT = process.env.PORT || 8080
const NODE_ENV = process.env.NODE_ENV
const isDev = NODE_ENV === 'dev'
const isProd = NODE_ENV === 'prod'

const _path = {
  main_js: './index.js',
  js: ['./index.js', './src/*.jsx', './src/**/**/*.jsx'],
  lint_js: ['./src/*.jsx', './src/**/**/*.jsx'],
  scss: ['./src/components/**/*.scss'],
  html: './index.html'
}

const clean = (done) => {
  rm('./dist', error => {if (error) throw error
    done()})
}

const _css = () => {return gulp.src(_path.scss)
    .pipe(sourcemaps.init())
    .pipe(sass())
    .pipe(concat('app.css'))
    .pipe(gulpif(isProd, cssmin()))
    .pipe(gulpif(isProd, rename({suffix: '.min'})))
    .pipe(gulpif(isDev, sourcemaps.write('/')))
    .pipe(gulp.dest('./dist/css'))
    .pipe(gulpif(isDev, connect.reload()))
}

const _lint = () => {return gulp.src(_path.lint_js)
    .pipe(eslint({useEslintrc: true}))
    .pipe(eslint.format())
    .pipe(eslint.failAfterError())
}

const _script = () => {
  return browserify({
      entries: _path.main_js,
      debug: true, // 生成 inline-sourcemap
      transform: [babelify.configure({presets: ['@babel/preset-env', '@babel/preset-react'],
        plugins: [
          '@babel/plugin-transform-runtime', 
          ["@babel/plugin-proposal-decorators", { "legacy": true}], 
          ["@babel/plugin-proposal-class-properties", { "loose": true}]
        ]
      })]
    })
    .bundle()
    .pipe(gulpif(isDev, exorcist(path.resolve(__dirname, `dist/js/app${isDev ? '':'.min'}.js.map`))))
    .pipe(source('app.js'))
    .pipe(buffer())
    .pipe(gulpif(isProd, uglify()))
    .pipe(gulpif(isProd, rename({suffix: '.min'})))
    .pipe(gulp.dest('./dist/js'))
    .pipe(gulpif(isDev, connect.reload()))
}

const _html = () => {return gulp.src(_path.html)
  .pipe(cheerio(($ => {$('script').remove()
    $('link').remove()
    $('body').append(`<script src="./js/app${isDev ?'' : '.min'}.js"></script>`)
    $('head').append(`<link rel="stylesheet" href="./css/app${isDev ?'' : '.min'}.css">`)
  })))
  .pipe(gulpif(isProd, minifyHtml({
    empty: true,
    spare: true
  })))
  .pipe(gulp.dest('./dist'))
  .pipe(gulpif(isDev, connect.reload()))
}

const _rev = () => {return gulp.src('./dist/*.html')
    .pipe(resversion({
      rootdir: './dist/',
      ignore: [/#data$/i]
    }))
    .pipe(gulp.dest('./dist'))
}

const _server = () => {
  connect.server({
    root: 'dist',
    port: PORT,
    livereload: true
  })
}

const _watch = () => {gulp.watch(_path.html, _html)
  gulp.watch(_path.js, _script)
  gulp.watch(_path.scss, _css)
}

module.exports = {build: series(clean, parallel(_script, _css, _html), _rev),
  dev: series(clean, _lint, parallel(_script, _css, _html), _rev, parallel(_server, _watch))
}

.eslintrc.js 配置

module.exports = {
    "env": {
        "browser": true,
        "es2020": true,
        "node": true
    },
    "extends": [
        "eslint:recommended",
        "plugin:react/recommended"
    ],
    // 开发环境与 ESLint 当前的解析功能不兼容
    "parser": "babel-eslint",
    "parserOptions": {
        "ecmaFeatures": {
            "experimentalObjectRestSpread": true, // 启用实验室特性
            "jsx": true
        },
        "ecmaVersion": 11,
        "sourceType": "module"
    },
    "plugins": ["react"],
    "rules": {
        // "no-undef": "off",
        "react/prop-types": "off",
        "no-unused-vars": "off"
    }
};

注意:如果 eslint 解析时报了下面错误,请指定 eslint 解析器为 babel-eslint,因为eslint 默认解析器 Espree 不支持 ES7 语法。

开发环境构建结果

生产环境构建结果

退出移动版