维护人:戴荔春 (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才能使用。
所以这里又分成两种情况:
我们可以在开发者电脑上进行自动构建,然后构建完毕后,跟方案一一样,手动部署
直接可以在服务器上配置好自动构建环境,开发者电脑上修改源码,即可看到自动构建效果