背景
在项目工程化中,通常根据不同的环境对项目进行构建,一般环境主要有开发、测试、预生产、生产。开发环境下关注规范、测试,而其他环境主要关注性能、安全,比如开发环境下进行 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
语法。
开发环境构建结果
生产环境构建结果