这是我参与更文挑战的第21天,活动详情查看: 更文挑战
前言
在上一篇文章 gulp 的使用(七):使用 babel 转译 js 里,介绍了如何使用 babel 转译 js。
我们都知道有时候程序是分多个环境,如:开发环境和发布环境。在这一篇文章里,将介绍如何在 gulp 项目里去分离环境。
环境变量
通常前端 node 项目的环境的区分都是依赖于 process.env.NODE_ENV 这个变量,那么我们需要在程序启动或打包时去改变这个变量的值,让程序根据这个变量去做一些改变。
在我写的 webpack5 系列文章 webpack5 的使用(二):多个环境配置 里的 “环境变量 NODE_ENV” 小节里也有介绍到如何使用 NODE_ENV,在这篇文章仅简单做介绍 NODE_ENV。
在 windows 命令行里,我们可以需要使用 set
命令来设置环境变量;而在 linux 命令行里需要使用 export
命令来设置环境变量。这里建议使用 cross-env 插件来统一设置环境变量的命令,不需要再考虑是什么系统了。
安装 cross-env
npm i -D cross-env
复制代码
修改项目里的 package.json 的 scripts
。
{
...
"scripts": {
...
"build": "cross-env NODE_ENV=production gulp build",
"dev": "cross-env NODE_ENV=development gulp"
},
...
}
复制代码
我们在 gulpfile.js 写上测试代码。
console.log('环境变量:' + process.env.NODE_ENV)
复制代码
运行 npm run build
,可以看到终端输出了下面内容。
build
命令的 NODE_ENV=production
起作用了。
分离环境
分离 gulpfile.js 配置
事实上,gulpfile.js 通常并不需要环境分离,不像 webpack 的配置,毕竟它是以任务为单位,并且任务可以随意组合,像下面用两个 exports
即可分离开发环境和生产环境。
// 运行 gulp 命令,就是开发环境
exports.default = series(clean, html, libJs, js, css, scss, libCss, img, devServer, watcher)
// 运行 build 命令,就是生产环境
exports.build = series(clean, html, libJs, js, css, scss, libCss, img)
复制代码
但是,当 gulpfile.js 配置过多的时候怎么办呢?我们应该将每个小任务或者一个系列任务(如:专门处理 js 的任务)拆分放到一个个文件里,在 gulpfile.js 引入这些文件来调用里面的任务。
下面以 js 系列任务为例。
我们先在根目录创建一个 gulpfile-js.js 文件,用于存放 js 相关任务。
const { src, dest } = require('gulp')
const webpack = require('webpack-stream')
const webpackConfig = require("./webpack.config.js")
const named = require('vinyl-named')
const path = require('path')
const plumber = require('gulp-plumber')
const changed = require('gulp-changed')
const uglify = require('gulp-uglify')
function js() {
return src(['src/js/**/*.js'])
.pipe(changed('dist/js/**/'))
// .pipe(babel({
// presets: ['@babel/env'],
// plugins: ['@babel/transform-runtime']
// }))
.pipe(named(function (file) {
return file.relative.slice(0, -path.extname(file.path).length)
}))
.pipe(webpack(webpackConfig))
.pipe(plumber())
.pipe(uglify())
.pipe(dest('dist/js'))
}
function libJs() {
return src(['src/lib/**/*.js'])
.pipe(changed('dist/lib/**/'))
.pipe(plumber())
.pipe(dest('dist/lib'))
}
module.exports = {
js, libJs
}
复制代码
然后,修改 gulpfile.js,将 js 相关任务注释掉,并且引入刚才的 gulpfile-js.js 文件
const { js, libJs } = require('./gulpfile-js')
复制代码
运行 npm run build
,可以发现 js 任务执行成功。
当然,这种做法有自身的缺陷,它以任务为单位拆分为不同文件,而不是以环境为单位创建不同的任务,这可能会导致在同一个任务里,难以根据不同环境区别对待,这个时候可以用 process.env.NODE_ENV 去做一些判断,或者可以创建一个新的任务。
如果你不喜欢这种“组装任务”区分环境的做法,而是要有一个很明确的环境分离边界,你也可以在根目录里创建两个很明确区分环境的文件:gulpfile.dev.js 和 gulpfile.prod.js,然后像上面那样在 gulpfile.js 做环境判断分别导出导入相关文件,在这里,就不做演示了。
分离 webpack-stream 的 webpack 配置
我在文章 webpack5 的使用(二):多个环境配置 里已经很详细介绍过 webpack 的配置分离了,有需要的小伙伴可以查阅这篇文章。
根据 NODE_ENV 变量改变程序
有时候,我们可能会有一些特殊的要求,比如说不单单只有“开发环境”和“生产环境”,可能还会有一个“演示环境”,而“演示环境”它可能需要更换网页的一些文字、样式或者逻辑,相当于在程序的原基础上套上一层皮肤。
在这个时候,该怎么办?
现在我们可以利用上环境变量 NODE_ENV 了。
我们在根目录创建一个 config.js,这个 config.js 是用于接收命令的变量,并且根据变量去判断生产更多的变量,比如:在生产环境里,主页的头部要变为红色,脚部要变为黄色,那么我们就创建这两个变量出来,后面使用插件来根据这些变量去变颜色。
伪代码
if (process.env.NODE_ENV == 'production') {
header = red
footer = yellow
}
复制代码
当然,事实上你也可以不这么做,可以直接在业务代码里去根据变量做判断来改变颜色。
我们在 config.js 写如下代码。
const CONFIG = {}
CONFIG.ENV = process.env.NODE_ENV
module.exports = CONFIG
复制代码
注意:为了方便,这里暴露的属性名是 ENV
,不是 NODE_ENV
。
在 gulpfile.js 引入 config.js。
const config = require('./config.js')
复制代码
改变 html
gulp-preprocess 是一个预处理 html 的插件,它可以在程序运行或打包前注入 html 的代码。
安装
npm i -D gulp-preprocess
复制代码
修改 gulpfile.js 配置,引入 gulp-preprocess。
const preprocess = require("gulp-preprocess")
...
function html() {
return src(['src/**/*.html', '!src/include/**.html'])
.pipe(changed('dist'))
.pipe(plumber())
.pipe(fileinclude({
prefix: '@@', //引用符号
basepath: './src/include', //引用文件路径
}))
.pipe(preprocess({
context: { ...config }
}))
.pipe(htmlmin({
removeComments: true, //清除HTML注释
collapseWhitespace: true, //压缩HTML
collapseBooleanAttributes: true, //省略布尔属性的值 <input checked="true"/> ==> <input />
removeEmptyAttributes: true, //删除所有空格作属性值 <input id="" /> ==> <input />
removeScriptTypeAttributes: true, //删除<script>的type="text/javascript"
removeStyleLinkTypeAttributes: true, //删除<style>和<link>的type="text/css"
minifyJS: true, //压缩页面JS
minifyCSS: true //压缩页面CSS
}))
.pipe(dest('dist'))
}
复制代码
上面的 preprocess 的 context 里面传入 json 对象,这个 json 对象的各个变量会被引入到 html 里,这里用了 es6 的对象结构,将 config 对象的所有属性赋予到 context 里。
这个时候,我们可以用到 html 的变量,在 index.html 里,我们写入以下内容
<!-- @if ENV = 'production' -->
<div><!-- @echo ENV --></div>
<!-- @endif -->
复制代码
这个代码意思是使用 @if 判断 ENV
变量是不是 production,如果是,则在 html 里写入 <div><!-- @echo ENV --></div>
内容,而这里面的 <!-- @echo ENV -->
会输出 ENV
变量值 “production”。
注意:
- 这里的
<!-- -->
注释是要加上去的,不能去除,预处理器会自动匹配这样的代码。 @if
的判断,如果是判断相等,用=
(只有一个等号),如果是不相等,用!=
。
运行 npm run build
,我们查看 dist/index.html,可以看到有 <div>production</div>
的相关内容。
运行 npm run dev
,我们可以发现 dist/index.html 是没有上述内容的。
改变 js
不幸的是,gulp 似乎没有插件可以改变 js,gulp-preprocess 插件的文档是有提到可以在 js 上用,但经过我的试验,发现不行,有想法的小伙伴可以查阅下相关文档。
当然你也可以选择直接在 js 使用 process.env.NODE_ENV
,但是这样之前写的 config.js 文件就没有任何意义了。
如果你的 gulp 项目有用到 webpack-stream,那么你可以装一个 webpack 的 preprocess-loader 来解决,该 loader 就是模仿 gulp-preprocess 而写的,只不过它似乎也只能用于 js。
安装
npm i -D preprocess-loader
复制代码
在 webpack.config.js 里配置
const config = require('./config')
...
module: {
module: {
rules: [
...
{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
use: [
{
loader: 'preprocess-loader',
options: {
// 填写变量
...config,
ppOptions: {
type: 'js'
}
}
}
]
}
]
},
...
}
复制代码
同理,这里也是引入 config.js,并且利用了 es6 的解构。
在 src/index.js 里,写上测试代码。
const env = '/* @echo ENV */'
if (env == 'production') {
console.log(env)
}
复制代码
运行 npm run build
,打开 dist/index.html 文件,再打开浏览器的控制台,可以发现,“production” 被打印出来了。
改变 scss
我们都知道 scss 是有变量的,现在我们需要根据环境改变这些 scss 变量值,gulp-sass-variables 插件可以做到这个效果。
安装
npm i -D gulp-sass-variables
复制代码
我们假设要根据不同环境去修改 cdn 地址,在开发环境里使用 http://2.com
前缀地址,在生产环境里使用 http://1.com
的前缀地址。
我们修改下 config.js
const CONFIG = {}
CONFIG.ENV = process.env.NODE_ENV
CONFIG.CDN = ''
if (CONFIG.ENV == 'production') {
CONFIG.CDN = 'http://1.com'
} else {
CONFIG.CDN = 'http://2.com'
}
module.exports = CONFIG
复制代码
配置 gulpfile.js
const sassVariables = require('gulp-sass-variables')
const config = require('./config.js')
...
function scss() {
return src(['src/scss/**/*.scss'])
.pipe(changed('dist/scss/**/'))
.pipe(plumber())
.pipe(sassVariables({
$CDN: config.CDN,
}))
.pipe(sass({ fiber }))
.pipe(cleanCss())
.pipe(dest('dist/scss'))
}
复制代码
注意,这里不能用 es6 的解构,因为 scss 的变量有规定变量名要有前缀 $
。
我们修改下 src/scss/index.scss 文件。
html {
body {
// background: blanchedalmond;
background: url(#{$CDN}/simao.jpg);
}
}
复制代码
上面使用 #{$CDN}
引入 CDN
变量。
运行 npm run build
,可以发现 dist/scss/index.scss 文件写入了相应的 CDN 地址。
完整代码
目录
gulpfile.js
const { series, parallel, src, dest, watch } = require('gulp')
const Path = require('path')
const htmlmin = require('gulp-htmlmin')
const fileinclude = require('gulp-file-include')
const changed = require('gulp-changed')
const webserver = require('gulp-webserver')
const del = require('del')
const uglify = require('gulp-uglify')
const plumber = require('gulp-plumber')
const cleanCss = require('gulp-clean-css')
const sass = require('gulp-sass')
const fiber = require('fibers') // 变量名命名没 s
const imagemin = require('gulp-imagemin')
const cache = require('gulp-cache')
const webpack = require('webpack-stream')
const webpackConfig = require("./webpack.config.js")
const named = require('vinyl-named')
const path = require('path')
// const babel = require('gulp-babel')
const preprocess = require("gulp-preprocess")
const sassVariables = require('gulp-sass-variables')
const { js, libJs } = require('./gulpfile-js')
const config = require('./config.js')
sass.compiler = require('sass')
//sass.compiler = require('node-sass')
console.log('环境变量:' + process.env.NODE_ENV)
function html() {
return src(['src/**/*.html', '!src/include/**.html'])
.pipe(changed('dist'))
.pipe(plumber())
.pipe(fileinclude({
prefix: '@@', //引用符号
basepath: './src/include', //引用文件路径
}))
.pipe(preprocess({
context: { ...config }
}))
.pipe(htmlmin({
removeComments: true, //清除HTML注释
collapseWhitespace: true, //压缩HTML
collapseBooleanAttributes: true, //省略布尔属性的值 <input checked="true"/> ==> <input />
removeEmptyAttributes: true, //删除所有空格作属性值 <input id="" /> ==> <input />
removeScriptTypeAttributes: true, //删除<script>的type="text/javascript"
removeStyleLinkTypeAttributes: true, //删除<style>和<link>的type="text/css"
minifyJS: true, //压缩页面JS
minifyCSS: true //压缩页面CSS
}))
.pipe(dest('dist'))
}
// function js() {
// return src(['src/js/**/*.js'])
// .pipe(changed('dist/js/**/'))
// // .pipe(babel({
// // presets: ['@babel/env'],
// // plugins: ['@babel/transform-runtime']
// // }))
// .pipe(named(function (file) {
// return file.relative.slice(0, -path.extname(file.path).length)
// }))
// .pipe(webpack(webpackConfig))
// .pipe(plumber())
// .pipe(uglify())
// .pipe(dest('dist/js'))
// }
// function libJs() {
// return src(['src/lib/**/*.js'])
// .pipe(changed('dist/lib/**/'))
// .pipe(plumber())
// .pipe(dest('dist/lib'))
// }
function css() {
return src(['src/css/**/*.css'])
.pipe(changed('dist/css/**/'))
.pipe(plumber())
.pipe(cleanCss())
.pipe(dest('dist/css'))
}
function scss() {
return src(['src/scss/**/*.scss'])
.pipe(changed('dist/scss/**/'))
.pipe(plumber())
.pipe(sassVariables({
$CDN: config.CDN,
}))
.pipe(sass({ fiber }))
.pipe(cleanCss())
.pipe(dest('dist/scss'))
}
function libCss() {
return src(['src/libCss/**/*.css'])
.pipe(changed('dist/libCss/**/'))
.pipe(plumber())
.pipe(dest('dist/libCss'))
}
function img() {
return src(['src/assets/img/**/*.{png,jpg,gif,jpeg,ico}']) //后缀都用小写,不然不识别
.pipe(
cache(
imagemin({
optimizationLevel: 5, //类型:Number 默认:3 取值范围:0-7(优化等级)
progressive: true, //类型:Boolean 默认:false 无损压缩jpg图片
interlaced: true, //类型:Boolean 默认:false 隔行扫描gif进行渲染
multipass: true //类型:Boolean 默认:false 多次优化svg直到完全优化
})
)
)
.pipe(dest('dist/assets/img'))
}
function devServer() {
return src('dist').pipe(webserver({
port: 3000,
livereload: true, // 是否实时加载
// directoryListing: true, // 是否开启浏览目录
// open: true, // 是否自动打开
// proxies: [ // 代理,可以用来解决跨域问题
// {source: '/api', target: 'http://xxxx.com', options: {headers: {"Content-Type": 'application/x-www-form-urlencoded'}}}
// ]
}))
}
function watcher() {
watch('src/**/*.html', series(html)).on('unlink', function (path) {
del('dist/**/*.html' + Path.basename(path))
})
watch('src/js/**/*.js', series(js)).on('unlink', function (path) {
del('dist/js/**/*.js' + Path.basename(path))
})
watch('src/lib/**/*.js', series(libJs)).on('unlink', function (path) {
del('dist/lib/**/*.js' + Path.basename(path))
})
watch('src/css/**/*.css', series(css)).on('unlink', function (path) {
del('dist/css/**/*.css' + Path.basename(path))
})
watch('src/scss/**/*.scss', series(scss)).on('unlink', function (path) {
del('dist/scss/**/*.css' + Path.basename(path))
})
watch('src/libCss/**/*.css', series(libCss)).on('unlink', function (path) {
del('dist/libCss/**/*.css' + Path.basename(path))
})
watch('src/assets/img/**/*.{png,jpg,gif,jpeg,ico}', series(img)).on('unlink', function (path) {
del('dist/assets/img/**/*.{png,jpg,gif,jpeg,ico}' + Path.basename(path))
})
}
function clean() {
return del('dist')
}
exports.default = series(clean, html, libJs, js, css, scss, libCss, img, devServer, watcher)
exports.build = series(clean, html, libJs, js, css, scss, libCss, img)
复制代码
gulpfile-js.js
const { src, dest } = require('gulp')
const webpack = require('webpack-stream')
const webpackConfig = require("./webpack.config.js")
const named = require('vinyl-named')
const path = require('path')
const plumber = require('gulp-plumber')
const changed = require('gulp-changed')
const uglify = require('gulp-uglify')
function js() {
return src(['src/js/**/*.js'])
.pipe(changed('dist/js/**/'))
// .pipe(babel({
// presets: ['@babel/env'],
// plugins: ['@babel/transform-runtime']
// }))
.pipe(named(function (file) {
return file.relative.slice(0, -path.extname(file.path).length)
}))
.pipe(webpack(webpackConfig))
.pipe(plumber())
.pipe(uglify())
.pipe(dest('dist/js'))
}
function libJs() {
return src(['src/lib/**/*.js'])
.pipe(changed('dist/lib/**/'))
.pipe(plumber())
.pipe(dest('dist/lib'))
}
module.exports = {
js, libJs
}
复制代码
webpack.config.js
console.log('利用了 webpack !!!')
const path = require('path')
const config = require('./config')
module.exports = {
mode: 'development',
//devtool: 'eval-cheap-module-source-map',
module: {
rules: [
{
test: /\.m?js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
plugins: ['@babel/plugin-proposal-object-rest-spread',
'@babel/plugin-transform-runtime'],
cacheDirectory: true,
}
}
},
{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
use: [
{
loader: 'preprocess-loader',
options: {
// 填写变量
...config,
ppOptions: {
type: 'js'
}
}
}
]
}
]
},
resolve: {
alias: {
}
},
}
复制代码
config.js
const CONFIG = {}
CONFIG.ENV = process.env.NODE_ENV
CONFIG.CDN = ''
if (CONFIG.ENV == 'production') {
CONFIG.CDN = 'http://1.com'
} else {
CONFIG.CDN = 'http://2.com'
}
module.exports = CONFIG
复制代码
src/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>首页</title>
<link rel="stylesheet" href="./css/index.css">
<link rel="stylesheet" href="./scss/index.css">
</head>
<body>
@@include('./header.html', {"name": "hello")
<div>首页内容</div>
@@include('./footer.html')
<!-- @if ENV = 'production' -->
<div><!-- @echo ENV --></div>
<!-- @endif -->
<script src="./js/index.js"></script>
</body>
</html>
复制代码
src/js/index.js
function print() {
console.log('测试')
}
print()
import utils from './utils'
utils.test()
const env = '/* @echo ENV */'
if (env == 'production') {
console.log(env)
}
复制代码
src/scss/index.scss
html {
body {
// background: blanchedalmond;
background: url(#{$CDN}/simao.jpg);
}
}
复制代码