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

2023-05-31,,

什么是自动化

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

早期的网站开发

在还没有前端工程师这种分工如此明确的岗位时,大家所理解的前端工作无非就是制作网页的人,包括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到底哪家强?在回答这个问题前,先给大家看一组下面的数据:

先介绍下gulp是什么东西

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

那么grunt呢

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

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

明显看到gulp的代码更少

那么再来说说效率问题

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

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

那么gulp呢

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

相比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/

接着先来说传统的项目

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

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

比如现在的目录结构是这样 

里面的详情是这样滴

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

先不管gulpfile.js这个东西

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

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

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

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

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

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

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

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

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

   

下一步改安装插件了

怎么安装呢

就像这样 

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

具体的安装是下面一行

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

比如刚才那个安装完成就变成这样了

对吧 插件自动写上去了

然后介绍下插件

这个是传统的打包

这个是requireJs项目的打包 

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

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

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

基本格式

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

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

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

这个是有很多插件依赖   

这是只有一个gulp插件和gulp-load-plugins插件

估计大家都知道了  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打包

先看下项目结构

代码结构

 "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项目打包

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

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

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

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

就像这样

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

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

数据流是这样子的

前端工作流程自动化——Grunt/Gulp 自动化的相关教程结束。

《前端工作流程自动化——Grunt/Gulp 自动化.doc》

下载本文的Word格式文档,以方便收藏与打印。