前端工作流程自动化——Grunt/Gulp 自动化

时间:2023-03-08 18:19:44

什么是自动化

先来说说为什么要自动化。凡是要考虑到自动化时,你所做的工作必然是存在很多重复乏味的劳作,很有必要通过程序来完成这些任务。这样一来就可以解放生产力,将更多的精力和时间投入到更多有意义的事情上。随着前端开发不再是简单的作坊式作业,而成为一个复杂的工程时,还涉及到性能优化一系列工作等等,这时自动化已然是迫切的需求。

早期的网站开发

在还没有前端工程师这种分工如此明确的岗位时,大家所理解的前端工作无非就是制作网页的人,包括html、css、js等。稍微高级点的可能就是php了,可以读写数据库,可以称之为动态的网页。通过近几年的发展,分工越来越细,在大公司如BAT三家,基本是把前端分开来了,有专门的人写js,有专门的人写css。以前一个网页可以一个人搞定,包括切图,写页面到写逻辑,无非是几个资源链接拼凑起来。当然逻辑性不强,页面不重。

javascript库

有一次需求你做了一个页面,然后第二次需求,你领导又让你做了页面,只是这次与上次的逻辑都差不多,就改改样式皮肤,修改图片等等。这样你就要从原来的地方拷贝一份代码过来。如果有一天发现一个bug,你就需要修改两处地方,这使得你非常的恼火,于是就把公共的逻辑抽取出来,两个页面都引用这段代码,这样就很好的解决了这个问题。以后有第三个第四个页面,你也不会担心了。渐渐的公共的代码越来越大,又是个独立的文件,这个文件就成为了一个库文件了,就像jquery等等。

模块化 (AMD,CMD)(依赖前置,依赖就近)

随着业务的不断扩大,页面越来越多,逻辑越来越重,之前你提取出来的库文件越来越大,功能越来越多。A页面只引用了其中的一部分函数,B页面C页面同样如此,后来你决定将库文件拆分成更小的模块,由各自的功能决定应该在哪个模块。这样一来前端开发就此演化为模块化开发方式。你开发的产品就像搭建积木一样,将各个模块组装在一起。

网页优化(1,减少请求数,2.减少请求资源大小)

好了,现在你的工程很庞大了,文件数量新增了非常的多,JS模块也很多,这时候一个页面也能加载了上十个js文件或者好几个样式文件。用户访问你的网页的时候需要把这些资源从服务器下载下来,所以理论上来说,想要加快你的网站,比必须减少下载的时间。可以从下载的数量和下载的大小出发,在不做大改变的前提下就是减少HTTP请求数以及压缩文件大小。雅虎的网页优化十四条军规中很大一部分是针对这种情况进行优化,如:
1、合并请求
2、使用雪碧图
3、压缩js、css代码(除去空格,混淆等等)
4、延迟加载

在PC时代,这些问题可能不是那么尖锐。移动互联网的兴起,对网页速度提出了更高的要求,因为网速相对比较慢。也有新的优化措施出现,比如缓存js文件等。可是要做到这些,并不是很容易。假如你的页面引入十个JS文件,这样发布出去显然是不好的,所以你要手动合并,将十个JS文件合并成一个,或者几个。然后还要将合并后的文件进行压缩。发出去之后产品经理发现有个小问题需要优化一下,这时候你又要重复刚才的工作。如果这样的事情发生三次,或者更多次,你会变得恼火。当然这只是令你恼火的一点点因素而已,更多的还有合并雪碧图(base64)等等。

经历过几次痛苦之后,你会发现性能优化与工程本身之前存在一些矛盾的地方。就是在优雅的开发的同时,兼顾性能方面的考虑实在难以做到。这时自动化工具太有必要了,将开发与发布隔离开来。按照优化的准则,利用构建工具,在发布的时候轻松一键搞定,这将是最理想化的作业方式。

一些构建工具

nodejs的出现,给前端开发带来了一些改变。在没有任何语言障碍的情况下,前端同学可以转为后台开发,nodejs带来另外的一个福音便是构建工具。之前的压缩合并工具大多是由java编写的,像雅虎的yui compressor,但对没有java基础的前端开发来说,至少要走不少弯路。然后最近一两年比较火的是国外的grunt和gulp,以及国内的FIS。相比而言,国外总是走在前头,在探索更好的开发方式,做出了很多探索。

Grunt/Gulp 都是node.js下的模块,简单来说是自动化任务运行器,两者都有社区及大量的插件支撑,在所有的自动化工具领域里,这两者是最好的前端自动化构建工具。

  前端工作流程自动化——Grunt/Gulp 自动化

首先看看他们能做些什么事情

前端工作流程自动化——Grunt/Gulp 自动化

  

那么问题来了,Grunt和Gulp到底哪家强?在回答这个问题前,先给大家看一组下面的数据:

前端工作流程自动化——Grunt/Gulp 自动化

先介绍下gulp是什么东西

gulp是前端开发过程中对代码进行构建的工具,是自动化项目的构建利器;她不仅能对网站资源进行优化,而且在开发过程中很多重复的任务能够使用正确的工具自动完成;使用她,我们不仅可以很愉快的编写代码,而且大大提高我们的工作效率。

那么grunt呢

Grunt 是一个基于任务的 Javascript 项目命令行构建工具,运行于 Node.js 平台。Grunt 能够从模板快速创建项目,合并、压缩和校验 CSS & JS 文件,运行单元测试以及启动静态服务器。

来看下两个工具基本代码的对比

前端工作流程自动化——Grunt/Gulp 自动化

明显看到gulp的代码更少

那么再来说说效率问题

grunt的工作流程:读文件、修改文件、写文件、读文件、修改文件、写文件.....,这样会产生很多临时文件

如果compass还要合并雪碧图的话,grunt的耗时就更长了,

那么gulp呢

gulp的设计非常的简单,他利用了node stream的便捷,把读取的文件变成一个输入流,经过一个个的管道,最终由输出流写入目标,一气呵成。
而每个插件就像是一根水管,一头进,一头出,可以方便的连接组装或是嵌套。
缓存也就是套在外头的一个处理器而已。

前端工作流程自动化——Grunt/Gulp 自动化

前端工作流程自动化——Grunt/Gulp 自动化前端工作流程自动化——Grunt/Gulp 自动化

相比Grunt,Gulp具备以下优点

  ● 配置更简洁,而且遵循代码优于配置策略,维护Gulp更像是写代码;

  ● 易学,核心API只有5个,通过管道流组合自己想要的任务;

  ● 一个插件只完成一个功能, 这也是Unix的设计原则之一,各个功能通过流进行整合并完成复杂的任务。

  当然也有劣势

  ● 相对Grunt而言,插件相对较少;

  ● 自动化可配置性不够Grunt强。

  ● 基于目前重构/前端的工作内容,需用到自动化功能大多数还是文件的处理,如压缩,合并,打包、检测、构建……,以上提到的两点劣势在目前的工作层面感受不明显,况且Gulp出现的目的是希望能够取代Grunt,成为最流行的自动化任务运行器。

如何使用gulp

首先安装node.js

安装地址nodejs.org

把npm指向淘宝的cnpm

控制台输入

npm install -g cnpm --registry=https://registry.npm.taobao.org

想看具体的点这里 http://npm.taobao.org/

接着先来说传统的项目

首先要清楚我们要准备做一件什么事情

  1.   把HTML代码压缩,并且把里面的路径进行替换
  2.    把CSS代码压缩,合并,并且把里面的路径替换
  3.   图片压缩
  4.   把JS压缩,校验,合并,替换路径。

比如现在的目录结构是这样 前端工作流程自动化——Grunt/Gulp 自动化

里面的详情是这样滴

前端工作流程自动化——Grunt/Gulp 自动化

大家可以看到有个gulpfile.js 文件和package.json文件

先不管gulpfile.js这个东西

先来看package.json  (注意:json文件内是不能写注释的,复制下列内容请删除注释)

package的字面意思 是 安装包的意思

顾名思义 也就是gulp的配置文件

就是说 你要干活了。干活前你应该先干嘛呢

先找人啊 。人都没在干毛线活 对吧

所以这个就是gulp干活前要找的小弟们,也就是配置文件

怎么建立这个文件呢 1.可以手动建立 要是你不嫌麻烦的话

2. 控制台进入该目录 运行cnpm init命令

前端工作流程自动化——Grunt/Gulp 自动化

然后根据提示输入后就变成这样了

   前端工作流程自动化——Grunt/Gulp 自动化

下一步改安装插件了

怎么安装呢

就像这样 前端工作流程自动化——Grunt/Gulp 自动化

安装格式是上面一行,上面只是演示格式

具体的安装是下面一行

那么具体要安装哪些插件呢。这个能下慢慢介绍

比如刚才那个安装完成就变成这样了前端工作流程自动化——Grunt/Gulp 自动化

对吧 插件自动写上去了

然后介绍下插件

这个是传统的打包前端工作流程自动化——Grunt/Gulp 自动化

这个是requireJs项目的打包 前端工作流程自动化——Grunt/Gulp 自动化

看完这个该去看看之前的gulfile.js文件时干嘛的了

这里介绍下一个大概的代码格式,以及几个常用的方法

 gulp.task('任务名称', function () {
return gulp.src('文件')
.pipe(...)
.pipe(...)
// 直到任务的最后一步
.pipe(gulp.dest());
});

基本格式

 非常容易理解!获取要处理的文件,传递给下一个环节处理,然后把返回的结果继续传递给下一个环节……直到所有环节完成。pipe 就是 stream 模块里负责传递流数据的方法而已,至于最开始的 return 则是把整个任务的 stream 对象返回出去,以便任务和任务可以依次传递执行。

老样子 先看简单的传统打包

在之前我们先看下这个gulp-load-plugins这个插件的作用

这个是有很多插件依赖   前端工作流程自动化——Grunt/Gulp 自动化

这是只有一个gulp插件和gulp-load-plugins插件前端工作流程自动化——Grunt/Gulp 自动化

估计大家都知道了  gulp-load-plugins的作用 他会自动去寻找这个js内所需要依赖的插件 并自动加载进来,大大减少了代码量

不过需要注意的是在调用命令的时候需要加上之前赋值的变量名 。


 "use strict";
var gulp = require('gulp'),
gulpLoadPlugin = require('gulp-load-plugins'),
plugins = gulpLoadPlugin(),
//这里是一个配置的参数
config = {
path: './',
src: 'src', //输入
dist: 'build', //输出
dev: 'http://dev.5173cdn.com/newmobile/src',
img01: 'http://img01.5173cdn.com/newmobile/build/1.00'
},
ugJs = function ugJs(type) {
var type = type || null;
return gulp.src(config.src + '/js/*.js') //gulp去读取文件
.pipe(plugins.replace(config.dev, config.img01)) //js替换
.pipe(plugins.jshint()) //js校验
.pipe(plugins.jshint.reporter()) //js校验
.pipe(plugins.uglify()) //js压缩
.pipe(gulp.dest(config.dist + '/1.00/js/')) //输出
.on('end', function () { //和JQ的on方法类似
if ( type ){
ugCss(true);
}
});
},
ugCss = function ugCss(type) {
var type = type || null;
return gulp.src(config.src + '/css/*.css')
.pipe(plugins.replace(config.dev, config.img01))
.pipe(plugins.minifyCss())
.pipe(gulp.dest(config.dist + '/1.00/css/'))
.on('end', function () {
if (type){
ugImage(true);
}
});
},
ugImage = function ugImage(type) {
var type = type || null;
return gulp.src(config.src + '/images/*')
.pipe(plugins.imagemin({
//optimizationLevel :3, //默认压缩等级
progressive: true, //对于jpg的图片压缩
svgoPlugins: [{removeViewBox: false}], //对于svg的图片压缩
use: [] //使用额外的插件
}))
.pipe(gulp.dest(config.dist + '/1.00/images/'))
.on('end', function () {
if (type){
ugHtml();
}
});
},
ugHtml = function () {
return gulp.src(config.src + '/html/*.html')
.pipe(plugins.replace(config.dev, config.img01))
.pipe(gulp.dest(config.dist + '/1.00/html/'));
}; //注册css打包
gulp.task('js', function () {
ugJs();
}); //注册js打包
gulp.task('css', function () {
ugCss();
}); //注册image压缩
gulp.task('img', function () {
ugImage();
}); //html替换
gulp.task('html', function () {
ugHtml();
}); //全套
gulp.task('all', function () { //先看这里就是运行ugJs这个方法
ugJs(true);
}); gulp.task('copy', function () {
return gulp.src(['./src/*', './build/*'])
.pipe(gulp.dest('../../tags/hybrid/'));
});

传统项目打包

然后看一下稍微复杂的点requirejs打包

先看下项目结构前端工作流程自动化——Grunt/Gulp 自动化

代码结构

 "use strict";
var //引入gulp,及其打包依赖插件
gulp = require('gulp'),
through2 = require('through2'), //nodeJs文件流控制
gulpLoadPlugin = require('gulp-load-plugins'),
plugins = gulpLoadPlugin(),
//描述一个配置参数
config = {
dir: './', //相对目(根目录),暂且看不出有什么用
src: 'src/v1',
dist: 'build/v1', //打包后的文件存放的跟目录名,可配置,但尽量不要配置
tmp: '.tmp', //不明觉厉的配置目录
version: '1.0.0', //当前版本号
baseUrl: 'http://img.m.5173cdn.com/app', //当前项目的绝对URL
dev: 'http://dev.m.5173cdn.com/app/src/v1',
img: 'http://img.m.5173cdn.com/app/build/v1',
timestamp: function (char) { // 格式化当前时间戳
var c = char || '',
t = new Date(),
y = t.getFullYear() + c,
m = t.getMonth() + 1,
m = m >= 10 ? m + c : '0'+ m + c,
d = t.getDate(),
d = d >= 10 ? d + c : '0'+ d + c,
h = t.getHours(),
h = h >= 10 ? h + c : '0'+ h + c,
f = t.getMinutes(),
f = f >= 10 ? f + c : '0'+ f + c,
s = t.getSeconds(),
s = s >= 10 ? s + c : '0'+ s + c;
return y + m + d + h + f + s;
}
},
//这里开始定义相关方法
cleanHas = function cleanHas(name, who) {
gulp.src(config.dist + '/' + who + '/' + name, {read: false})
.pipe(plugins.clean());
//.on('end', function () {
//
//});
}, //定义图片压缩方法
imageMin = function imageMin(name) {
//图片压缩合适进行都可以,暂时放到css压缩替换之后
var name = name || '';
return gulp.src(config.src + '/images/' + name + '/*')
.pipe(plugins.imagemin({
progressive: true,
svgoPlugins: [{
removeViewBox: false
}],
use: []
}))
.pipe(gulp.dest(config.dist + '/images/' + name));
}, //定义HTML文档替换及压缩方法
htmlReplace = function htmlReplace(name){
var htmlSrc = config.src+ '/html/' + name + '.html',
jsSrc = config.dist + '/js/**/*.js',
cssSrc = config.dist + '/css/**/*.css',
arg = [],
argBase = {},
argBusiness = {}; return gulp.src([jsSrc, cssSrc])
.pipe(through2.obj(function (file, enc, cb) { //file 是数据流 enc是utf-8 cb是一个函数
this.push(file.relative); //file.relative是需要压缩的所有JS和CSS文件
cb();
}))
.on('data', function (data) {
arg.push(data); //这是css和js文件名字
})
.on('end', function () {
for(var i = 0, n = arg.length; i < n; i++){
if (arg[i].indexOf('.css') > -1){ //这里吧名字赋值到对象里
if ( arg[i].indexOf('base') ){
argBase.css = arg[i];
}else if (arg[i].indexOf(name)){
argBusiness.css = arg[i];
}
}else if (arg[i].indexOf('.js') > -1){
if ( arg[i].indexOf('base') ){
argBase.js = arg[i];
}else if (arg[i].indexOf(name)){
argBusiness.js = arg[i];
}
}
}
//console.log(argBase, argBusiness);
//if(name === 'base'){
//
//}
gulp.src(htmlSrc) //对html内部进行替换
.pipe(plugins.htmlReplace({ //进行替换
'js': [
config.baseUrl + '/' + config.dist + '/js/' + argBusiness.js,
config.baseUrl + '/' + config.dist + '/js/'+ argBase.js
],
'css': [
config.baseUrl + '/' + config.dist + '/css/'+ argBusiness.css,
config.baseUrl +'/' + config.dist + '/css/'+ argBase.css
]
}))
.pipe(plugins.htmlmin({
collapseWhitespace: true //去空格
}))
.pipe(gulp.dest(config.dist + '/html/')); });
}, //定义css文件压缩方法
cssUg = function ugCss(name, type) {
//cleanHas(name, '/css/');
var //都会用到的参数
howToDo = type || null,
suffixNumber= name === 'base' ? config.version : config.timestamp();
return gulp.src(config.src + '/css/'+ name + '/*.css')
//.pipe(gulp.src('./css/'+ name +'/*.css'))
.pipe(plugins.replace(config.dev, config.img))
.pipe(plugins.minifyCss())
.pipe(plugins.rename(name +'.min_' + suffixNumber + '.css'))
.pipe(gulp.dest(config.dist +'/css/' + name))
.on('end', function () {
//引入图片压缩
imageMin(name);
//判断是否进行html替换压缩
if ( type === 'html' ){
if ( name !== 'base' ){
htmlReplace(name);
}
} });
}, //定义js文件压缩方法
jsUg = function jsUg(name, type) {
//cleanHas(name, '/js/');
var //都会用到的参数
howToDo = type || null,
suffixNumber = name === 'base' ? config.version : config.timestamp(), //如果是基类那么加版本号 ,要么加时间戳
//先对js打包做准备
rjs2Name = name === 'base' ? './bower_components/almond/almond' : name, //almond.js是一个简化版的require.js 只提供require define export还是model方法?
rjs2Out = name + '.js',
rjs2Include = name === 'base' ? ['rem', 'zepto', 'can', 'underscore', 'fastclick'] : null,
rjs2InsertRequire = name === 'base' ? null : [name],
rjs2Exclude = name === 'base' ? null : ['rem', 'zepto', 'can', 'underscore', 'fastclick'],
rjs2Wrap = name === 'base' ? false : true
//然后对css做准备
//暂时好像没什么要准备的
; //先对js进行压缩打包
return plugins.rjs2({
baseUrl: './',
mainConfigFile: 'src/v1/js/config.js', //配置到requirejs的配置文件,类似于r.js打包的配置文件,解决打包的依赖顺序
name: rjs2Name, //requireJs的入口文件.js require(['index']);
out: rjs2Out, //输出名字
include: rjs2Include, //需要依赖的模块
exclude: rjs2Exclude, //不需要依赖的模块
// insertRequire 在 RequireJS 2.0 中被引入,在 built 文件( built是r.js打包的配置文件 )的末尾插入 require([]) 以触发模块加载并运行
// insertRequire: ["index"] 即 require(["index"])
// 和name进行一个配合
insertRequire: rjs2InsertRequire,
removeCombined: true, //删除之前压缩合并的文件,默认值 false。
findNestedDependencies: true, // 处理级联依赖,默认为 false,此时能够在运行时动态 require 级联的模块。为 true 时,级联模块会被一同打包
optimizeCss: 'none', //CSS 代码优化方式,可选的值有: http://www.cnblogs.com/lhb25/p/requirejs-ptimizer-using.html
optimize: 'none', //JavaScript 代码优化方式
skipDirOptimize: true,
wrap: rjs2Wrap // 另一种模块包裹方式
})
.pipe(plugins.replace(config.dev, config.dist))
.pipe(plugins.uglify())
.pipe(plugins.rename(name + '.min_' + suffixNumber + '.js'))
.pipe(gulp.dest(config.dist + '/js/' + name))
.on('end', function () {
//判断是否进行css及html压缩
if ( howToDo === 'css' ){
cssUg(name, 'html');
}else if ( howToDo === 'html' ){
htmlReplace(name);
}
});
}, //定义清空之前文件方法
clean = function clean(name, type) {
var name = name || '';
return gulp.src(config.dist + '/'+ type +'/' + name, {read: false})
.pipe(plugins.clean());
}, //定义打什么包的方法
//仅仅是css压缩(一般情况用不到)
onlyCss = function onlyCss(name){
cssUg(name);
},
//仅仅是js压缩(一般情况也用不到)
onlyJs = function onlyJs(name) {
jsUg(name);
},
//仅仅是html替换压缩一般情况依然用不到
onlyHtml = function onlyHtml(name) {
htmlReplace(name);
},
//既然上述三个方法一般情况都用不到还定义干嘛,可是万一二般了呢 //只进行css压缩并更新到HTML中,(一般应用在只对css进行了修改的情况)
cssHtml = function cssHtml(name) {
cssUg(name, 'html');
},
//只进行js压缩并更新到HTML中,(一般应用在只对js进行了修改的情况)
jsHtml = function jsHtml(name){
jsUg(name, 'html');
},
//一体化,(一般应用在第一次对当前项目经行打包,或者同时修改js,css文件时打包)也就是对全部文件进行打包
allToDo = function allToDo(name) {
jsUg(name, 'css');
}
;
//再次之前 我们先定义一个test方法,
gulp.task('test', function () {
//这里写测试方法
//测试图片压缩
//imageMin();
console.log(1);
console.log(plugins.replace);
return gulp.src(config.src + '/css/' + 'index/*.css')
.pipe(plugins.replace(config.dev, config.img))
.pipe(gulp.dest('./test/')); }); //这里我们定义一些 可能会用到的工具类方法,可能会不断增加新方法 //定义清空文件方法,注意这里清空的都是各自的根目录
//清空所有打包目录下文件
gulp.task('clean', function () {
return gulp.src(config.dist + '/', {read: false})
.pipe(plugins.clean());
}); //清空打包后js部分
gulp.task('cleanJs', function () {
return gulp.src(config.dist + '/js/', {read: false})
.pipe(plugins.clean());
}); //其他的依样画葫芦 //开始定义方法了,
//关于参入的参数,只是当前模块页面的name
//如果是基类 只能是’base‘
//其他的参数名是自定义的模块页面名 //首先对base,基类js,css进行打包
//首先需要明确的是,js基类变化的几率应该不大,
//可能有机会修改的应该是css基类,有可能会因为产品原型变化或迭代,增加新的公用样式
//so,对于基类,可以先创建一个js,css打包的命令,在做一个仅仅可能会用到的css基类打包
//话虽如此,但是关于基类请千万不要频繁更改,若是更改也尽量不要该版本号,否则后果十分悲惨,
//这个问题也是这个配置最为鸡肋的弱点,还在寻求解决的方法 //在这里,传入的这个参数必须是’base‘,这个是我规定死的,不可修改,否则会挂
//然后还需要明白一点,关于所有的基类打包,仅仅是打包,不涉及html替换和压缩 //增加基类js,css一起合并打包方法
gulp.task('base', function () {
allToDo('base');
}); //增加基类仅仅对js打包方法
gulp.task('baseJs', function () {
onlyJs('base');
}); //增加基类仅仅对css打包方法
gulp.task('baseCss', function () {
onlyCss('base');
}); //举个例子,对于当前我的这个例子,只有一个模块,即首页模块’index.page‘
//基类以及打过了,无需在管 //一般情况用不到的3个放不写了。
//针对这个例子我们设置如下可能会用到几率相对高的方法 //对index.page整个模块进行打包
//清空当前模块已有构建后的文件
//清空ALL
gulp.task('indexC', function () {
cleanHas('index', 'css');
cleanHas('index', 'css');
});
//清空css
gulp.task('indexCC', function () {
cleanHas('index', 'css');
});
//清空js gulp.task('indexCJ', function () {
cleanHas('index', 'js');
});
//清空图片 gulp.task('indexCI', function () {
clean('index', 'images');
});
gulp.task('index', function () {
allToDo('index');
}); //当仅仅修改了js模块
gulp.task('indexJH', function () {
jsHtml('index');
}); //当仅仅修改了index.page的css部分
gulp.task('indexCH', function () {
cssHtml('index');
});

requireJs项目打包

首先要清楚我们要准备做一件什么事情

  1.   把HTML代码压缩,并且把里面的路径进行替换
  2.    把CSS代码压缩,合并,并且把里面的路径替换
  3.   图片压缩
  4.   把JS压缩,校验,合并,替换路径。

这里上面3项其实和传统项目差不多

值得一提的是html里的路径替换需要写注释

就像这样

前端工作流程自动化——Grunt/Gulp 自动化

麻烦的只是对于如何处理各个JS模块的依赖问题

所以这里用了gulp-rjs2插件进行处理。具体看代码

数据流是这样子的