gulp是什么?
一个基于node的前端自动化任务构建工具,使用经典回调+链式调用的方式实现任务的自动化 (src.pipe(...).pipe)
,gulp其实和webpack很相似,但是gulp侧重点不同,gulp更侧重前端流程自动化、任务执行(通过任务使开发提效),就像一条流水线。而webpack则是更侧重用于打包前端资源,一切皆可打包成模块。
官方文档:https://www.gulpjs.com.cn/
gulp的应用场景?为什么用gulp?
1.构建前端自动化流程比较好的方案,一些重复的人工操作可以让gulp去做,并且代码量不大,提升开发效率(自动化完成前端工作流任务:单元测试、优化、打包)。
2.与其他构建工具相比开发简单,易上手。基于nodejs 文件系统流,运行速度快。
3.更适合原生前端项目的打包。有助于理解前端工程化。
4.发布通用型组件或者npm库的时候可以用gulp来进行打包。
gulp的安装
在你的项目目录中执行
npm install -g gulp
在根目录下创建gulp的配置文件gulpfile.js
,install一下文件中的依赖,之后就可以直接在这个文件中定义我们的gulp任务了
var gulp=require("gulp");//引入gulp模块
gulp.task('default',function(){
console.log('hello world');
});
然后直接终端中进入当前目录运行 gulp
命令就开始执行任务了。
gulp后面也可以加上要执行的任务名,例如gulp task1
,如果没有指定任务名,则会执行任务名为default
的默认任务。
gulp常用的几个api : task, series, parallel, src, pipe, dest
- task: 创建一个任务
- series:顺序执行多个任务
- prallel:并行执行多个任务
- src:读取数据源转换成stream
- pipe:管道-可以在中间对数据流进行处理
- dest:输出数据流到目标路径
gulp实际应用之原生前端项目打包:
以一个jQuery原生项目为例子,目录结构:
路径表:
因为项目结构有点特殊,资源比较分散,html的js、css保存在对应模块文件夹中。而公共的js、css却在html的外面的文件中,所以就设置了2个入口(以js资源为例):一个根目录入口、一个html入口
scriptsjs: {//根目录入口
src: [
"./temp/js/**/*.js",//temp/js/之下的所有文件夹下的js
"!./temp/**/*.min.js", //不匹配压缩过的js,防止二次压缩
"!./temp/js/common.js",
"!./temp/mpcc/**/*.js",//这个文件不需要压缩处理所以不匹配
],
dest: destDir + "/js",//出口
},
scriptshtml: {//html入口
src: [
"./temp/html/**/*.js",
"!./temp/html/BasicSet/RoleManage/js/*.js",//不匹配
"!./temp/html/BasicSet/UserAuthorization/js/*.js",
],
dest: destDir + "/html",
},
你没看错,是不是有点像webpack中的entry
和output
。* 通配符
是代表所有文件夹、!
是代表不匹配。这里可以自己选哪些文件不需要进行匹配
gulpfile.js 完整代码:
var gulp = require("gulp"),
sourcemaps = require("gulp-sourcemaps");
var babel = require("gulp-babel");
var uglify = require("gulp-uglify");
var del = require("del");
var minifycss = require("gulp-minify-css");
var through = require("through2");
var path = require("path");
var fs = require("fs");
var crypto = require("crypto");
var ramdomHash = function (len) {
//获取随机hash值
var random = Math.random().toString();
return crypto
.createHash("md5")
.update(new Date().valueOf().toString() + random) //加入时间戳和随机数保证hash的唯一
.digest("hex")
.substr(0, len);
};
var destDir = "workOrder"; //生产包文件目录
//包路径表
var paths = {
stylescss: {
src: ["./temp/css/**/*.css", "!./temp/**/*.min.css"],
dest: destDir + "/css",
},
styleshtml: {
src: ["./temp/html/**/*.css", "!./temp/**/*.min.css"],
dest: destDir + "/html",
},
scripts: {
src: "./temp/**/*.js",
dest: destDir + "/",
},
scriptsjs: {
src: [
"./temp/js/**/*.js",
"!./temp/**/*.min.js",
"!./temp/js/common.js",
"!./temp/mpcc/**/*.js",
],
dest: destDir + "/js",
},
scriptshtml: {
src: [
"./temp/html/**/*.js",
"!./temp/html/BasicSet/RoleManage/js/*.js",
"!./temp/html/BasicSet/UserAuthorization/js/*.js",
],
dest: destDir + "/html",
},
html: {
src: "./temp/**/*.html",
dest: destDir + "/",
}
};
//删除生产包
function clean() {
return del([destDir]);
}
//清除temp文件夹
function revClean() {
return del(["temp"]);
}
//复制到temp,避免污染src
function revCopy() {
return gulp
.src("./workorder_dev/**/*", { base: "./workorder_dev" })
.pipe(gulp.dest("./temp/"));
}
//html中资源路径加版本号,更改所有的文件里的资源路径,以便接下来的增加版本号工作.
function revHtmlPathReplace() {
var ASSET_REG = {
SCRIPT:
/("|')(.[^('|")]*((\.js)|(\.css)|(\.json)|(\.png)|(\.jpg)|(\.ttf)|(\.eot)|(\.gif)|(\.woff2)|(\.woff)))(\1)/gi,
};
return gulp
.src("./temp/html/**/*.html")
.pipe(
(function () { //利用through读取html文件夹下的所有html文件
return through.obj(function (file, enc, cb) {
if (file.isNull()) {
this.push(file);
return cb();
}
if (file.isStream()) {
this.emit(
"error",
new gutil.PluginError(PLUGIN_NAME, "Streaming not supported")
);
return cb();
}
var content = file.contents.toString();
var filePath = path.dirname(file.path);
for (var type in ASSET_REG) { //获取html文件内容直接使用replace+正则进行替换
content = content.replace(
ASSET_REG[type],
function (str, tag, src) {
var _f = str[0];
src = src.replace(/(^['"]|['"]$)/g, "");
if (/\.min\./gi.test(src)) {
//压缩文件不加版本号
return src;
}
var assetPath = path.join(filePath, src);
if (fs.existsSync(assetPath)) {
var buf = fs.readFileSync(assetPath);
var md5 = ramdomHash(7); //获取版本号hash,只需要7位hash不需要太长
var verStr = "" + md5;
src = src + "?v=" + verStr;
}
src = _f + src + _f;
return src;
}
);
}
file.contents = new Buffer(content);
this.push(file);
cb();
});
})()
)
.pipe(gulp.dest("./temp/html/"));
}
//css中资源加版本号
function assetRev(options) {
var ASSET_REG = {
SCRIPT: /(<script[^>]+src=)['"]([^'"]+)["']/gi,
STYLESHEET: /(<link[^>]+href=)['"]([^'"]+)["']/gi,
IMAGE: /(<img[^>]+src=)['"]([^'"]+)["']/gi,
BACKGROUND: /(url\()(?!data:|about:)([^)]*)/gi,
};
return through.obj(function (file, enc, cb) {
options = options || {};
if (file.isNull()) {
this.push(file);
return cb();
}
if (file.isStream()) {
this.emit(
"error",
new gutil.PluginError(PLUGIN_NAME, "Streaming not supported")
);
return cb();
}
var content = file.contents.toString();
var filePath = path.dirname(file.path);
for (var type in ASSET_REG) {
if (type === "BACKGROUND" && !/\.(css|scss|less)$/.test(file.path)) {
} else {
content = content.replace(ASSET_REG[type], function (str, tag, src) {
src = src.replace(/(^['"]|['"]$)/g, "");
if (!/\.[^\.]+$/.test(src)) {
return str;
}
if (options.verStr) {
src += options.verStr;
return tag + '"' + src + '"';
}
// remote resource
if (/^https?:\/\//.test(src)) {
return str;
}
var assetPath = path.join(filePath, src);
if (src.indexOf("/") == 0) {
if (
options.resolvePath &&
typeof options.resolvePath === "function"
) {
assetPath = options.resolvePath(src);
} else {
assetPath = (options.rootPath || "") + src;
}
}
if (fs.existsSync(assetPath)) {
var buf = fs.readFileSync(assetPath);
var md5 = ramdomHash(7);
var verStr = (options.verConnecter || "") + md5;
src = src + "?v=" + verStr; //增加版本号
} else {
return str;
}
return tag + '"' + src + '"';
});
}
}
file.contents = new Buffer(content);
this.push(file);
cb();
});
}
//为css中引入的图片/字体等添加hash编码
function revAssetCsscss() {
return gulp
.src(paths.stylescss.src) //该任务针对的文件
.pipe(assetRev()) //该任务调用的模块
.pipe(gulp.dest("./temp/css")); //编译后的路径
}
function revAssetCsshtml() {
return gulp
.src(paths.styleshtml.src) //该任务针对的文件
.pipe(assetRev()) //该任务调用的模块
.pipe(gulp.dest("./temp/html")); //编译后的路径
}
//压缩css,并添加sourcemap
function stylesMinifyCss() {
return (
gulp
.src(paths.stylescss.src)
.pipe(sourcemaps.init())
// .pipe(less())
// .pipe(cleanCSS())
// // pass in options to the stream
// .pipe(rename({
// basename: 'main',
// suffix: '.min'
// }))
.pipe(minifycss())
.pipe(sourcemaps.write("./maps"))
.pipe(gulp.dest(paths.stylescss.dest))
);
}
//把html文件夹下的css进行压缩
function stylesMinifyHtml() {
return (
gulp
.src(paths.styleshtml.src)
.pipe(sourcemaps.init())
.pipe(minifycss())
.pipe(sourcemaps.write("./maps"))
.pipe(gulp.dest(paths.styleshtml.dest))
);
}
//压缩js,并添加sourcemap
function scriptsjs() {
return gulp
.src(paths.scriptsjs.src, { sourcemaps: true })
.pipe(sourcemaps.init()) //源码映射便于调试
.pipe(sourcemaps.identityMap())
.pipe(babel()) //es6转换
.pipe(uglify()) //压缩
.pipe(sourcemaps.write("./maps"))
.pipe(gulp.dest(paths.scriptsjs.dest));
}
//压缩html文件夹下的js
function scriptshtml() {
return gulp
.src(paths.scriptshtml.src, { sourcemaps: true })
.pipe(sourcemaps.init())
.pipe(sourcemaps.identityMap())
.pipe(babel())
.pipe(uglify())
.pipe(sourcemaps.write("./maps"))
.pipe(gulp.dest(paths.scriptshtml.dest));
}
//把临时文件拷贝到生产目录
function copy() {
return gulp
.src("./temp/**/*", { base: "./temp" })
.pipe(gulp.dest(destDir + "/"));
}
//创建一个json文件保存标识用于识别当前是否是线上环境
function updateEnv(done) {
fs.writeFile(
"./temp/env.json",
JSON.stringify({ env: "prod" }),
function (err) {
if (err) {
console.error(err);
}
done();
console.log("--------------------updateEnv");
}
);
}
var build = gulp.series(//串行任务
clean,//清除上一次的生产包
revClean,//删除temp文件夹
revCopy,//拷贝开发目录到temp
revHtmlPathReplace,//html加版本号
revAssetCsscss,//给css资源加版本号
revAssetCsshtml,
updateEnv,//生成运行环境json
copy, //copy之后再压缩
gulp.parallel( //对两个入口的资源压缩、优化的并行任务
stylesMinifyCss,
stylesMinifyHtml,
scriptsjs,
scriptshtml
),
revClean //删除temp
);
exports.clean = clean;
exports.stylesMinifyCss = stylesMinifyCss;
exports.stylesMinifyHtml = stylesMinifyHtml;
exports.updateEnv = updateEnv;
exports.scriptsjs = scriptsjs;
exports.scriptshtml = scriptshtml;
exports.default = build;
可以看出每个任务就是一个函数,在最后对定义的任务(函数)按顺序进行执行一遍。
打包流程:
其实看最后的gulp.series
就能看出来,串行任务里面含有一个并行任务,直接运行gulp命令就能直接看到打包的过程。
所有任务执行的先后顺序:
删除上次生产包 > 删除temp文件夹 > 把开发目录拷贝到temp文件夹 > html内容加版本号 > 资源内容加版本号 > 创建json > 把temp文件拷到生产目录 > 开始并行压缩 > 最后删除temp文件夹
gulp实现防缓存
为什么要防缓存?
如果不防缓存,在原生项目上线后,浏览器会把前端的css,js资源缓存在本地,下次打开的时候如果资源不变就会直接使用本地的缓存来加载页面,这样会造成用户必须手动清除浏览器缓存才能使用新的功能,影响体验,所以就需要在页面引入资源的时候给文件加上版本号?v=xxxx,这样浏览器就能识别到资源有变化就会从服务器上重新获取资源
我们回看一下gulpfile文件中加版本号的任务revHtmlPathReplace
:
//html中资源路径加版本号,更改所有的文件里的资源路径,以便接下来的增加版本号工作.
function revHtmlPathReplace() {
var ASSET_REG = {
SCRIPT:
/("|')(.[^('|")]*((\.js)|(\.css)|(\.json)|(\.png)|(\.jpg)|(\.ttf)|(\.eot)|(\.gif)|(\.woff2)|(\.woff)))(\1)/gi,
};
return gulp
.src("./temp/html/**/*.html")
.pipe(
(function () { //利用through读取html文件夹下的所有html文件
return through.obj(function (file, enc, cb) {
if (file.isNull()) {
this.push(file);
return cb();
}
if (file.isStream()) {
this.emit(
"error",
new gutil.PluginError(PLUGIN_NAME, "Streaming not supported")
);
return cb();
}
var content = file.contents.toString();
var filePath = path.dirname(file.path);
for (var type in ASSET_REG) { //获取html文件内容直接使用replace+正则进行替换
content = content.replace(
ASSET_REG[type],
function (str, tag, src) {
var _f = str[0];
src = src.replace(/(^['"]|['"]$)/g, "");
if (/\.min\./gi.test(src)) {
//压缩文件不加版本号
return src;
}
var assetPath = path.join(filePath, src);
if (fs.existsSync(assetPath)) {
var buf = fs.readFileSync(assetPath);
var md5 = ramdomHash(7); //获取版本号hash,只需要7位hash不需要太长
var verStr = "" + md5;
src = src + "?v=" + verStr;
}
src = _f + src + _f;
return src;
}
);
}
file.contents = new Buffer(content);
this.push(file);
cb();
});
})()
)
.pipe(gulp.dest("./temp/html/"));
}
通过遍历所有html文件时使用through
获取到文件的文本内容,然后利用正则对文本内容中需要加版本的路径加上hash版本然后替换上去,最后再输出一个新的文件文件。同时assetRev
任务也类似
这个任务的代码其实是借鉴了gulp-asset-rev/index.js
中的源码,把原来源码中的css文件内容的资源路径加版本拿出来改成了给html文件内容加版本。
加版本后效果:
顺带说一下gulp另一种比较麻烦的加版本防缓存方案就是使用rev
模块,需要修改多处源码。这种方案有不好的地方,因为是生成rev-manifest.json
对应关系来加版本 ,如果因为某些原因在这个json中没有对应文件对照,会导致某些特殊路径如(../../xxx.js)
的文件加版本号没加上 就会漏掉某些文件没加版本。还有就是node_modules源码改动后重新安装就会被覆盖。
最后:
对打包流程进行一些优化
1.因为每次打包后是都要手动进行压缩、命名一下再发给后端部署。解放双手,写一个打包后对生产包自动进行压缩的任务distZip:
这个distZip
任务要放到最后执行,默认压缩当前目录下的dist文件夹
//对生产包自动压缩成zip
function distZip(done) {
var archiver = require("archiver");
var now = new Date();
var filename = [
__dirname + "/dist",
now.getMonth(),
now.getDate(),
now.getHours(),
now.getMinutes(),
now.getSeconds(),
".zip",
].join(""); //当前时间拼接
var output = fs.createWriteStream(filename);
//设置压缩格式为zip
var archive = archiver("zip", {
zlib: { level: 9 }, // Sets the compression level.
}); archive.on("error", function (err) {
throw err;
});
archive.pipe(output);
archive.directory("./dist/");
archive.finalize();
done();
}
2.在原生项目打包完成时没有像webpack有process.env
来区分是否是生产环境,可以在打包阶段写一个生成当前运行环境配置文件的任务updateEnv
,自动生成一个json文件,项目进入时提前加载这个json文件,在代码中就可以用这个json里的env
标识来判断当前是否在生产环境。
function updateEnv(done) {
fs.writeFile(
"./temp/env.json",
JSON.stringify({ env: "prod" }),
function (err) {
if (err) {
console.error(err);
}
done();
console.log("--------------------updateEnv");
}
);
}
3.以上打包的js、css资源文件没有做文件合并的任务,如果提前进行js,css文件合并就可以解决了文件分散的问题,就不需要多设置入口了
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。