维护人:戴荔春 (6016)
实际项目中,当服务器开启了缓存后,如何更好的控制静态文件资源(img,js,css)更新,成为了需要首要面对的问题,本文提供了一个基于gulp自动构建的解决方案
本文主要介绍基于gulp自动构建解决前端文件缓存更新问题
没有gulp基础?没关系,你可以先看 缓存问题与解决方案,里面有讲解md5签名解决文件缓存的必要性,了解完这项技术的必要性后,再去了解Node和gulp吧!
前人栽树,后台乘凉,本文参考了以下来源
本文是基于gulp自动构建的,所以需要注意
如没有,请移步 Node.js开发大纲
如没有,请移步 gulp自动构建指南
如没有,请移步 svn使用指南
没有一项技术是会无缘无故出现的,md5签名文件解决缓存自然也有它的缘由,以下进行一一分析
事实上,几乎每个前端项目都会遇到静态资源缓存问题,下面解释下这个问题的缘由
归根结底,还是因为浏览器为了节省带宽,提高性能,会对以访问的静态资源进行缓存处理,当再次请求时,会默认使用缓存
理论上来说,html文件也一样能被强制缓存,但实际应用中,html页面一般是使用 "Last-Modified"机制,只要文件修改了就会刷新,而js和css一般会采用更为变态的“Cache-control”策略(比如有的就是强制浏览器永不更新这类资源的,只要文件uri没变,则不会再次请求)
这里举一个最简单的例子,如图,浏览器使用了强制缓存(如果本地有缓存,只用缓存)。项目中A.html引用了B.js和C.css。这时候当A.html,B.js,C.css同时做修改,如果没有采用缓存策略,那么只有A.html会刷新,另外两者仍然是用的缓存
以上介绍了缓存的来由和由此造成的问题,现在开始介绍解决方案
为了解决缓存问题,那么项目中就必须又有一套缓存机制。缓存机制分很多种,有比较低级的由一个统一脚本控制文件版本号的,也有自动构建给文件添加不同版本号的,也有自动构建,通过数据摘要算法对文件md5签名,生成不同文件的。
本文采取的方案就是: 自动构建,文件md5签名解决静态资源缓存和更新问题
强烈推荐大家看下这篇文章,坚持看完后肯定就阔然开朗了 变态的静态资源缓存与更新
参考: 自动构建md5签名文件的实现方案
以下介绍这种方案的实现,基于gulp自动构建和一些gulp插件
请注意,前提是Node.js和gulp都已正常安装
这里提供一个示例demo,大家可以下载示例demo,体验自动构建md5签名的效果
点击 双击安装本示例gulp依赖插件.bat,即可安装本示例依赖的gulp插件
也可通过CMD命令自行安装(前提是Node和gulp已经安装)
cnpm install gulp-clean-css gulp-htmlmin gulp-rev gulp-rev-collector gulp-clean gulp-uglify run-sequence merge-stream gulp-imagemin gulp-cache gulp-plumber -g
点击手动编译源码.bat,之后等待构建,构建完后出现 build, rev 文件夹。其中build文件夹中是编译好的项目,rev文件夹中是对一些md5签名文件的记录
点击开启自动构建监听.bat,之后会开启自动监听,每次检测到 src/test.dcloud.testGulpDemo/gulpWatch.json文件改变时,即会自动编译项目
可以发现,里面的js,css,图片都自动加上了md5签名,html中的引用名字也自动替换了
请注意,这里js/libs/mui.min.js 是没有签名的,因为示例demo中只会默认签名js/core 和js/bizlogic 下的脚本。同样,也只会处理css/目录下的css文件。具体请看gulpfile.js 源码
到了这一步,相信大家都能看出来已经解决缓存问题了,因为我们每次修改js,css文件时,重新部署出来的名字就是不同的,所以浏览器本地就不存在缓存问题了。而部署的同时html页面中也会自动将以前的引用替换为新的js和css的。所以可以确保每次都是最新的资源文件
原因可能是与权限设置有关,可以暂时修改gulpfile.js文件,先注释 图片压缩功能,之后就可以构建了
注意,dealMd5Img里面的图片压缩也要注释
本示例主要是基于一些gulp插件编写 gulpfile.js,通过Node执行gulpfile.js,从而执行里面的插件,进行自动构建和部署。另外本例中,gulpfile.js里面写一些自动构建的逻辑,config.js里写一些路径配置。更好的分离。
var config = require('./config');
var gulp = require('gulp');
var minifyCss = require('gulp-clean-css'); //- 压缩CSS为一行;
var htmlmin = require('gulp-htmlmin');
var rev = require('gulp-rev'); //- 对文件名加MD5后缀
var revCollector = require('gulp-rev-collector'); //- 路径替换
var clean = require('gulp-clean');
var uglify = require('gulp-uglify')
//串行执行任务
var runSequence = require('run-sequence');
//同个task下合并多个步骤
var mergeStream = require('merge-stream');
//图片压缩相关
var imagemin = require('gulp-imagemin');
//cach
var cache = require('gulp-cache');
//错误抛出的补丁->防止异常情况下停止程序
var plumber = require('gulp-plumber');
var handleErrors = function(error) {
//继续监听
console.log("~~~错误:" + error);
};
//情况以前目录
gulp.task('clean', function() {
return gulp.src([
config.clean.src
])
//异常处理的补丁
.pipe(plumber({
errorHandler: handleErrors
}))
.pipe(clean({
force: true
}));
});
//处理需要md5签名的图片
gulp.task('dealMd5Img', function() {
//排除html/**/img下面的
//排除/libs/**/img下的
return gulp.src([config.img.src, '!' + config.html.img.src, '!' + config.libs.img.src])
.pipe(plumber({
errorHandler: handleErrors
}))
// .pipe(cache(imagemin({
// optimizationLevel: 3,
// progressive: true,
// interlaced: true
// })))
.pipe(rev())
.pipe(gulp.dest(config.img.dest))
.pipe(rev.manifest(config.img.revJson, {
//不合并
merge: false
}))
//因为已经进入了json了,所以默认./就行
.pipe(gulp.dest('./'));
});
//处理其他的图片-除去上面压缩后的
gulp.task('dealOtherImg', function() {
//html/**/img下面的
///libs/**/img下的
var htmlImg = gulp.src([config.html.img.src])
.pipe(plumber({
errorHandler: handleErrors
}))
// .pipe(cache(imagemin({
// optimizationLevel: 3,
// progressive: true,
// interlaced: true
// })))
.pipe(gulp.dest(config.html.img.dest));
var libsImg = gulp.src([config.libs.img.src])
.pipe(plumber({
errorHandler: handleErrors
}))
.pipe(cache(imagemin({
optimizationLevel: 3,
progressive: true,
interlaced: true
})))
.pipe(gulp.dest(config.libs.img.dest));
return mergeStream(htmlImg, libsImg);
});
//压缩并处理 json-这个是json文件夹下面的配置,只处理这个文件夹
gulp.task('dealJSON', function() {
return gulp.src([config.json.src])
.pipe(plumber({
errorHandler: handleErrors
}))
.pipe(cache(imagemin({
optimizationLevel: 3,
progressive: true,
interlaced: true
})))
.pipe(rev())
.pipe(gulp.dest(config.json.dest))
.pipe(rev.manifest(config.json.revJson, {
//不合并
merge: false
}))
//因为已经进入了json了,所以默认./就行
.pipe(gulp.dest('./'));
});
//处理css
gulp.task('dealCss', function() {
//- 需要处理的css文件,先替换里面的图片,然后再压缩md5签名
//排除libs下的css
return gulp.src([config.rev.revJson, config.css.src])
.pipe(plumber({
errorHandler: handleErrors
}))
.pipe(revCollector())
.pipe(minifyCss()) //- 压缩处理成一行
.pipe(rev()) //- 文件名加MD5后缀
.pipe(gulp.dest(config.css.dest)) //- 输出文件本地,*号会自动输出
.pipe(rev.manifest(config.css.revJson, {
merge: false
}))
.pipe(gulp.dest('./')); //- 将 rev-manifest.json 保存到 rev 目录内
});
//先处理框架第三方不会依赖其它脚本的文件,排除seajs的配置,排除cacheConfig配置
//先处理的css,所以core里就算有引用css也没问题
gulp.task('dealCoreJs', function() {
//- 需要处理的css文件,先替换里面的图片,然后再压缩md5签名
return gulp.src([config.rev.revJson, config.js.coreJs.src, '!' + config.js.seaConfigJs.src, '!' + config.js.cacheConfigJs.src])
.pipe(plumber({
errorHandler: handleErrors
}))
.pipe(revCollector())
.pipe(uglify({
//mangle: true,//类型:Boolean 默认:true 是否修改变量名
mangle: {
except: ['require', 'exports', 'module', '$']
} //排除混淆关键字
}))
.pipe(rev())
.pipe(gulp.dest(config.js.coreJs.dest))
.pipe(rev.manifest(config.js.coreJs.revJson, {
merge: false
}))
.pipe(gulp.dest('./'));
});
//再处理业务js,会依赖于以上的两种js
//排除业务处理的config
gulp.task('dealBizlogicJs', function() {
//- 需要处理的css文件,先替换里面的图片,然后再压缩md5签名
return gulp.src([config.rev.revJson, config.js.bizlogicJs.src, '!' + config.js.bizConfigJs.src])
.pipe(plumber({
errorHandler: handleErrors
}))
.pipe(revCollector())
.pipe(uglify({
//mangle: true,//类型:Boolean 默认:true 是否修改变量名
mangle: {
except: ['require', 'exports', 'module', '$']
} //排除混淆关键字
}))
.pipe(rev())
.pipe(gulp.dest(config.js.bizlogicJs.dest))
.pipe(rev.manifest(config.js.bizlogicJs.revJson, {
merge: false
}))
.pipe(gulp.dest('./'));
});
//再处理seajs的配置,或者是项目中的配置
gulp.task('dealSeaConfig', function() {
//- 需要处理的css文件,先替换里面的图片,然后再压缩md5签名
return gulp.src([config.rev.revJson, config.js.seaConfigJs.src])
.pipe(plumber({
errorHandler: handleErrors
}))
.pipe(revCollector())
.pipe(uglify({
//mangle: true,//类型:Boolean 默认:true 是否修改变量名
mangle: {
except: ['require', 'exports', 'module', '$']
} //排除混淆关键字
}))
.pipe(rev())
.pipe(gulp.dest(config.js.seaConfigJs.dest))
.pipe(rev.manifest(config.js.seaConfigJs.revJson, {
//允许合并以前的bizConfig
//这里注意,如果是通过路径找到的json,则merge有用,否则merge并没有用
merge: true
}))
.pipe(gulp.dest('./'));
});
//处理bizConfigJs的配置,或者是项目中的配置
//排除业务设置的路径
gulp.task('dealBizConfig', function() {
//- 需要处理的css文件,先替换里面的图片,然后再压缩md5签名
return gulp.src([config.rev.revJson, config.js.bizConfigJs.src])
.pipe(plumber({
errorHandler: handleErrors
}))
.pipe(revCollector())
.pipe(uglify({
//mangle: true,//类型:Boolean 默认:true 是否修改变量名
mangle: {
except: ['require', 'exports', 'module', '$']
} //排除混淆关键字
}))
.pipe(rev())
.pipe(gulp.dest(config.js.bizConfigJs.dest))
.pipe(rev.manifest(config.js.bizConfigJs.revJson, {
merge: true
}))
.pipe(gulp.dest('./'));
});
//cacheController->控制引入seaConfig
gulp.task('dealCacheConfigJs', function() {
//- 需要处理的css文件,先替换里面的图片,然后再压缩md5签名
return gulp.src([config.rev.revJson, config.js.cacheConfigJs.src])
.pipe(plumber({
errorHandler: handleErrors
}))
.pipe(revCollector())
.pipe(uglify({
//mangle: true,//类型:Boolean 默认:true 是否修改变量名
mangle: {
except: ['require', 'exports', 'module', '$']
} //排除混淆关键字
}))
.pipe(rev())
.pipe(gulp.dest(config.js.cacheConfigJs.dest))
.pipe(rev.manifest(config.js.cacheConfigJs.revJson, {
merge: true
}))
.pipe(gulp.dest('./'));
});
//处理html
gulp.task('dealHtml', function() {
//- 需要处理的css文件,先替换里面的图片,然后再压缩md5签名
return gulp.src([config.rev.revJson, config.html.src])
.pipe(plumber({
errorHandler: handleErrors
}))
.pipe(revCollector())
.pipe(htmlmin({
collapseWhitespace: true,
collapseBooleanAttributes: true,
removeComments: true,
removeEmptyAttributes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
minifyJS: true,
minifyCSS: true
})) //压缩
.pipe(gulp.dest(config.html.dest));
});
//处理其它,除去img,css,html,js,以外的,单独包括.project文件
gulp.task('dealOthers', function() {
//出去svn
return gulp.src([config.other.src, config.other.project.src, '!' + config.js.coreJs.src, '!' + config.js.bizlogicJs.src, '!' + config.css.src, '!' + config.html.src, '!' + config.img.src, '!' + config.other.svn.src, '!' + config.other.settings.src])
.pipe(plumber({
errorHandler: handleErrors
}))
.pipe(gulp.dest(config.other.dest));
});
// 看守
gulp.task('watch', function() {
console.log("路径:" + config.src + '/gulpWatch.json');
// 看守所有位在 dist/ 目录下的档案,一旦有更动,便进行重整
// gulp.watch([config.src+'/gulpWatch.json']).on('change', function(file) {
// console.log("改动");
// });
gulp.watch(config.src + '/gulpWatch.json', ['default']);
});
gulp.task('default', function(callback) {
runSequence('clean', 'dealMd5Img', 'dealOtherImg', 'dealJSON', 'dealCss', 'dealCoreJs', 'dealBizlogicJs', 'dealSeaConfig', 'dealBizConfig', 'dealCacheConfigJs', 'dealHtml', 'dealOthers',
callback);
});
源码中主要有两种功能,一种是自动构建项目(包括删除以前项目,压缩js,css,图片,对一些特定文件夹下的文件签名,文件类的引用替换,自动输出部署等),一种是自动监听配置文件的变化(gulpWatch.json),当配置文件变动时,执行第一种命令,重新部署
注意,源码中的自动构建是基于公司开发规范的跨平台项目制定的,所以适用于符合跨平台开发规范开发的项目,如果需要适用于自己的其它类型项目,请修改gulpfile.js里的构建逻辑
/**
* 作者: dailc
* 时间: 2016-06-22
* 描述: gulp的一些配置,包括路径,项目层级
*/
(function() {
//项目path,默认为''代表不使用项目path
//示例/testproject
//var projectPath = '/testDemo'
//src下的项目名称,可以换为自己项目
var projectPath = '/test.dcloud.testGulpDemo';
var src = './src' + projectPath;
//输出目录,可以用../指向上一级,具体部署时可以自定义路径
var dest = './build' + projectPath;
var rev = './rev' + projectPath;
//定义的一些文件编译和输出路径,可以不用管太多
module.exports = {
//如果src前有**,目录会自动补齐
src:src,
dest:dest,
rev:rev,
css: {
//所有需要编译的css
src: src + '/**/css/**/*.css',
//输出目录
dest: dest + "",
//json目录
revJson: rev + "/rev-manifest-css.json"
},
js: {
src: src + '/js/**/*.js',
coreJs: {
src: src + '/**/js/core/**/*.js',
dest: dest + "",
revJson: rev + "/rev-manifest-coreJs.json"
},
bizlogicJs: {
src: src + '/**/js/bizlogic/**/*.js',
dest: dest + "",
revJson: rev + "/rev-manifest-bizlogicJs.json"
},
//seaConfig-以下三个config会合并
seaConfigJs: {
src: src + '/**/sea.config.js',
dest: dest + "",
revJson: rev + "/rev-manifest-config.json"
},
//业务处理的config ->包括自定义sea别名等
bizConfigJs: {
src: src + '/**/bizlogic/config/*.js',
dest: dest + "",
revJson: rev + "/rev-manifest-config.json"
},
//cacheController->控制引入seaConfig
cacheConfigJs: {
src: src + '/**/cacheControl.config.js',
dest: dest + "",
revJson: rev + "/rev-manifest-config.json"
}
},
//默认图片只处理img目录下的,其它目录下的由于路径问题不好替换
//所以img文件夹下请别放其它文件
//由于有些项目直接在html里又有img,所以构建时得单独排除那一部分
img: {
jpg: {
src: src + '/img/**/*.jpg'
},
png: {
src: src + '/img/**/*.png'
},
gif: {
src: src + '/img/**/*.gif'
},
src: src + '/**/img/**/*',
dest: dest + "",
revJson: rev + "/rev-manifest-img.json"
},
//只处理json文件夹下面的json
json: {
src: src + '/**/json/**/*.json',
dest: dest + "",
revJson: rev + "/rev-manifest-json.json"
},
clean: {
src: dest
},
html: {
img: {
src: src + '/html/**/img/**/*',
dest: dest + "/html"
},
src: src + "/**/*.html",
dest: dest + ""
},
//引用,构建图片时需要过滤掉libs下引用的图片
libs: {
img: {
src: src + '/libs/**/img/**/*',
dest: dest + "/libs"
}
},
other: {
project: {
src: src + "/**/.project",
},
svn: {
src: src + "/**/.svn/*",
},
settings: {
src: src + "/**/.settings/*",
},
src: src + "/**/*",
dest: dest + ""
},
rev: { //use rev to reset html resource url
revJson: rev + "/**/*.json"
}
};
})();
config.js里主要是定义路径,比如以上文件中定义了需要构建的文件路径,需要压缩的js,css,图片路径,需要md5签名的文件路径
注意,实际部署中,当需要把部署目录部署到项目以外的文件夹时(gulpfile.js以外的),gulpfile.js中的clean任务必须设置为force: true
gulpfile.js中定义了构建逻辑,本示例中的构建流程为:
以上讲解了gulp自动构建并md5签名文件,那么结合SVN自动更新,可以做到自动的效果
开发者本机提交svn源码,服务器端部署项目自动更新,并且没有缓存问题
服务器端安装SVN服务端,用来存储数据。同时也安装一个客户端,里面用来自动更新最新svn代码。然后安装gulp,采用示例中的gulp自动构建md5解决缓存方案,监听svn客户端源码,每当监听到源码配置文件有变动时,自动部署到Web容器目录,然后用户在外网可以通过网址访问web容器目录里的部署项目
SVN自动更新参考: SVN自动更新方案
gulp自动构建参考: gulp监听文件变动并自动构建部署文件
文件md5签名解决缓存方案参考本文
以上的实现原理和步骤以及示例demo都有提供,参照各自步骤即可实现
介绍以前采用的缓存控制方案,以及本示例中的方案和以前方案对比
以前采用的缓存控制方案大概分为以下几部分
里面封装有动态写入其它js,css的方法,通过这个脚本写入的css,js会自动加上时间戳
document.write("<script type='text/javascript' src='../../js/core/RayWeb/cacheControl.config.js?" + Math.random() + "'></scr" + "ipt>");
加上版本号的目的是确保每次引入最新js
SrcBoot.output([
'js/lib/ztree/jquery.ztree.core.min.js',
'./index.js'
]);
任何技术方案,都需要互相对比才能知道各自优劣,下面将通过对比来分析本方案的优势以及采用本方案的必要性。这里将以前的方案简称方案一,将本示例中的方案简称方案二
方案一:每次更新缓存需要修改版本号,所以只要改变一次版本号,其它所有为修改的js,css缓存均不能使用,浪费了大量流量
方案二:每次更新时,由于采用的数据摘要算法,所以文件修改后名字会变,缓存无效,而未修改的文件名字不会变,仍然会使用缓存,所以不会浪费流量
方案一:只能控制css,js这些可以通过js动态引入的文件的缓存,一些直接通过标签引入的资源文件缓存无法控制(比如无法控制图片缓存)
方案二:通过自动构建,可以控制任何的资源缓存(可以自定义gulpfile.js),适用于任何项目。而且关键的是,就算你以前使用的方案一控制缓存,源码可以不做任何变动,只需要修改gulpfile.js,定制你的构建逻辑,即可自动构建
比如本文中的示例就是由方案一转为方案二的,毫无障碍
方案一:通过源码编写后,部署时,一些需要压缩的js,css需要通过第三方工具压缩,或者手动压缩,部署项目也得手动部署
方案二:通过自动构建,可以拓展脚本,让代码控制,自定压缩,自定签名,自动部署,只需要掌握一次技术,就可以不算复用
方案一:只适用于js,css缓存控制
方案二:通过自动构建,除了缓存控制,还可以自定义拓展压缩,混淆代码,签名文件,合并多个文件,条件编译部署,自动监听部署等功能
方案一:所有能写代码的电脑均能做到
方案二:自动构建依赖于Node,所以需要电脑上能安装Node才能使用。
所以这里又分成两种情况:
我们可以在开发者电脑上进行自动构建,然后构建完毕后,跟方案一一样,手动部署
直接可以在服务器上配置好自动构建环境,开发者电脑上修改源码,即可看到自动构建效果