Grunt :初次使用及前端构建经验

这是我们部门前端同学cobish的学习笔记,笔者编辑了一下并分享给大家。

在使用 Grunt 之前,项目静态文件几乎没进行压缩合并便直接放到线上,部分文件手动复制粘贴到某压缩网站进行压缩。没压缩合并的文件显然耗资源,手动压缩的文件后期不易维护,每修改一次便要重复复制粘贴,很不方便。Grunt 的加入帮忙解决了以上问题,让开发人员更加专注于开发。这里有一篇「Grunt教程——安装Grunt」很好地教会我们如何搭建 Grunt 环境。

「官网」的入门文档写得很详细,建议阅读并动手一遍。 网上有人会纠结该用 Grunt 还是 glup。个人认为,其实无论是 Grunt 还是 glup 都是构建工具,基本的功能都差不多,与其浪费时间纠结该使用哪个,还不如先开始选择一个使用,等过段时间熟悉后再考虑是否接触另一个,最后再比较出哪个更适合自己岂不更好。

合并压缩静态资源

我开始使用 Grunt 的时候只是用来对 css,js 文件进行合并压缩,使用到的插件分别如下:

"devDependencies": {
  "grunt": "^0.4.5",
  "grunt-contrib-clean": "^0.6.0",
  "grunt-contrib-concat": "^0.5.1",
  "grunt-contrib-cssmin": "^0.14.0",
  "grunt-contrib-uglify": "^0.10.0",
  "grunt-contrib-watch": "^0.6.1"
}

我先通过 watch 监控静态文件,一旦文件有改动并保存,便用 concat 把 css 或 js 目录下的文件进行了合并,再用 cssmin 或 uglify 把刚刚合并的文件压缩,最后用 clean 把合并但未压缩的文件删除掉。部分代码(以 js 为例)如下:

// 文件合并
concat: {
  js: {
    files: {
      'dest/js/index.js': ['src/js/index/*.js']
    }
  }
},

// 压缩js代码
uglify: {
  build: {
    expand: true,
    cwd: 'dest/js',
    src: ['**/*.js', '!*.min.js'],
    dest: 'dest/js',
    ext: '.min.js'
  }
},

// 删除多余的文件
clean: {
  js: ['dest/js/*js', '!dest/js/*.min.js']
},

// 文件监控
watch: {
  js: {
    files: 'src/js/**/*.js',
    tasks: ['concat:js', 'uglify', 'clean:js']
  }
}

Source Map

后来发现 cssmin 和 uglify 其实已包含了合并的功能,于是乎把 concat 和 clean 给移除掉,因为它们功能重复了。代码如下:

// 合并压缩js代码
uglify: {
  build: {
    files: {
      'dest/js/index.min.js': ['src/js/index/*.js']
    }
  }
}

是的,用了 cssmin 和 uglify 后在浏览器的调试工具下便无法定位到源代码处,这是有办法解决的。办法就是使用 source map,chrome 和 firefox 的调试工具都支持,具体详情请移步「JavaScript Source Map 详解」

grunt-newer

使用了 cssmin 和 uglify 之初项目还不算大的时候,你也许已经发现了一个现象。那就是我们每次一修改保存文件的时候,watch 插件便会立马调用 cssmin 和 uglify。而它们在配置里是对所有的 css 和 js 文件进行操作,虽然只对其中一个文件修改,但是目录下的所有文件都会大动干戈地进行合并压缩。配置高的电脑还行,配置低的电脑就悲剧了,至少我试过每次一保存文件都要等待个两三秒钟后合并压缩完成才能去刷新浏览器。一旦静态文件多起来,那这等待的时候只会增多不会减少。后来我找到了「grunt-newer」这个插件来缓解燃眉之急。newer 只会对改动的文件进行操作,这样至少不会每次保存都对全部文件进行操作。它的使用方法很简单:

// 监控
watch: {
  js: {
    files: 'src/js/**/*.js',
    tasks: ['newer:uglify']
  }
}

引入资源

以上便是我目前用于项目的阶段,而此时我做进行开发的项目中主要用了类似于 thinkPHP 的框架,于是添加 css 或 js 外部文件是在 php 代码里添加,如下:

<?php
  $this->addMoreCss('dest/index.min.css');
  $this->addMoreJs('dest/index.min.js');
?>

这样虽然开发使用到的文件跟上线的文件一致,但也有一些弊端,比如每次改动保存静态文件便会去执行合并压缩代码,我们每天都在时时刻刻地用 ctrl+s,这是没有必要的。我们应该只在准备发版上线的时候才去合并压缩。但这时如果在开发时使用原始文件则会是这样:

<?php
  $this->addMoreCss('src/index/test1.css');
  $this->addMoreCss('src/index/test2.css');
  $this->addMoreCss('src/index/test3.css');
  $this->addMoreJs('src/index/test1.js');
  $this->addMoreJs('src/index/test2.js');
?>

上面一段代码在上线时是需要注释掉的,那在修复时又要重新打开这份代码,注释掉上面上线使用的代码。如果涉及到多个页面的修改,那得手动打开很多份类似这样的代码,而在修复完成后又得重新重复地进行注释和打开上线代码。万一有哪一段代码没看见忘了就不好了。

接下来

所以接下来我打算在 Grunt 中使用「grunt-contrib-sass」「grunt-contrib-requirejs」,这样在 php 函数都只需要引入一个入口文件,然后 sass 通过 import,requirejs 通过 require 便可去加载它们需要的文件。具体结果得等我实践后才知道,但我相信如果 ok 的话我便可以移除 cssmin 和 uglify 两个插件,因为 Sass 和 requirejs 也有合并压缩的功能。

<?php
  // 开发
  $this->addMoreCss('src/main.css');
  $this->addMoreJs('src/main.js');

  // 上线
  $this->addMoreCss('dest/main.css');
  $this->addMoreJs('dest/main.js');
?>

添加版本号

为了上线之后用户能使用到最新的静态资源,大部分人会使用添加时间戳来清掉缓存,类似于下面这样的代码。读过张云龙的「大公司里怎样开发和部署前端代码」,意识这种方法有几个弊端。一则是每次修改一下时间戳全部的静态资源都会重新被下载一次,没有修改过的文件又重新下载一遍明显是一种浪费。二则是这种方法是一种覆盖式发布,无论先部署页面还是先部署静态资源,期间都可能有用户访问到页面,都有可能造成了页面显示错乱问题,所以需要一种非覆盖式的发布方法来避免这种情况。

<!-- css -->
<link rel="stylesheet" type="text/css" href="index.css?t=20160121" />

<!-- js -->
<script type="text/javascript" src="index.js?t=20160121"></script>

总结上诉理论,此刻我们需要一种非覆盖式发布的方法,而此时这种方法就是将静态资源的内容hash后修改其文件名,做到文件名不同从而起到类似于时间戳的作用。如以下静态资源hash后的文件名发生的变化:

css/index.css  ->  css/index.aa59f6ab.css
img/demo.png   ->  img/demo.aa59f6ac.png

接下要怎么实现以上方法呢?要用的工具是 Grunt,使用到的插件如下:

"devDependencies": {
  "grunt": "^0.4.5",
  "grunt-contrib-clean": "^1.0.0",
  "grunt-contrib-copy": "^1.0.0",
  "grunt-filerev": "^2.3.1",
  "grunt-usemin": "^3.1.1"
}

这里暂时不涉及到 js 文件,处理 js 文件跟处理 css 文件类似。使用了「grunt-filerev」便可以很轻松地生成 hash 戳后的静态文件。

// 静态文件hash
filerev: {
  img: {
    src: 'src/img/**/*.png',
    dest: 'dest/img/'
  },
  css: {
    src: 'src/css/**/*.css',
    dest: 'dest/css/'
  }
}

静态文件生成后便可以使用「grunt-usemin」对使用到这些静态文件的文件里进行文件名替换,改成hash后的静态文件名。

// 替换
usemin: {
  options: {
    assetsDirs: [
      'dest',
      'dest/img',
      'dest/css'
    ]
  },
  css: 'dest/css/**/*.css',
  html: 'dest/html/**/*.html'
}

以下的步骤都会避免修改到源文件。具体步骤则是先将图片 hash 后放置于 dest 目录(发布目录)。然后将 css 代码都复制到一个tmp目录(临时目录),替换里面变更的图片名字,再将 css 文件 hash 后放置于 dest 目录。接着将 html 代码复制到 dest 目录,替换里面引用到的图片和 css 文件名。最后将 tmp 目录删除。具体代码实现如下:

// 步骤一:对图片进行处理
grunt.registerTask('img', [
  'filerev:img'
]);

// 步骤二:对css进行处理
grunt.registerTask('css', [
  'copy:css',
  'usemin:css',
  'filerev:css'
]);

// 步骤三:对html进行处理
grunt.registerTask('html', [
  'copy:html',
  'usemin:html',
  'clean:tmp'
]);

未解决的问题:如上代码,我把它分成了三份分别按步骤运行,但是放在一个任务里却会遇到问题,比如css里的图片名称没有被替换等。如哪位朋友有解决办法,不妨传授我一下,感激!

基于 Grunt 的前端构建

继续对 Grunt 进行探索研究,例子参考「grunt-project」。这一次不再使用 php 进行 include 静态文件,而是在 html 里面进行 include。然后主要将 Grunt 用于两个大的方向,一个是用于开发期间,一个用于上线前期打包。使用到的插件可能有些更换。具体目录如下,src 目录用于开发与维护,dist 目录是打包后的项目,用于上线:

├─ dist/
    ├─ css/
    ├─ images/
    ├─ js/
    └─ view/
└─ src/
    ├─ css/
    ├─ images/
    ├─ js/
    ├─ sass/
    └─ view/

在开发期间,使用到的 Grunt 插件如下,watch 插件用了监听文件,一旦文件被修改,可以让它触发浏览器自动刷新:

"devDependencies": {
  "grunt": "^0.4.5",
  "grunt-contrib-jshint": "^0.12.0",
  "grunt-contrib-sass": "^0.9.2",
  "grunt-contrib-watch": "^0.6.1"
}

图片不需要压缩,css 使用 sass 编译,js 使用了 requirejs,并使用 jshint 进行检错。其中 sass 编译好后会在同一目录下生成对应的 css 目录与文件。jshint 的具体配置参考「例子」。

sass: {
  dev: {
    options: {
      style: 'expanded'
    },
    files: [{
      expand: true,
      cwd: 'src/sass/',
      src: ['**/*.scss'],
      dest: 'src/css/',
      ext: '.css'
    }]
  }
},

jshint: {
  options: {
    curly: true,
    newcap: true,
    eqeqeq: true
    // ...
  },
  files: {
    src: ['Gruntfile.js', 'src/**/*.js']
  }
}

在开发结束后,接下来就是让项目上线了,于是就有了打包项目的过程。看过张云龙博客里讲的「大公司里怎样开发和部署前端代码?」,于是便有了非覆盖式发布和静态文件hash,用到了「grunt-filerev」和「grunt-usemin」这两个插件。网上有很多教程都是图片、css、js 文件同一时间进行 hash,但我觉得这样不妥,毕竟 css(js)代码里引用到了图片,得先图片进行 hash 后替换了 css(js)里引用的路径,然后再对 css(js)进行hash才能保证哪些文件是修改过的。

打包分四个步骤。按顺序分别是图片的打包、css 文件的打包、js 文件的打包、html 文件的打包。使用到的插件如下:

"devDependencies": {
  "grunt": "^0.4.5",
  "grunt-contrib-clean": "^0.7.0",
  "grunt-contrib-copy": "^0.8.2",
  "grunt-contrib-cssmin": "^0.14.0",
  "grunt-contrib-htmlmin": "^0.6.0",
  "grunt-contrib-imagemin": "^1.0.0",
  "grunt-contrib-jshint": "^0.12.0",
  "grunt-contrib-requirejs": "^0.4.4",
  "grunt-contrib-sass": "^0.9.2",
  "grunt-contrib-watch": "^0.6.1",
  "grunt-css-sprite": "^0.2.2",
  "grunt-filerev": "^2.3.1",
  "grunt-include-replace": "^3.2.0",
  "grunt-newer": "^1.1.1",
  "grunt-replace": "^0.11.0",
  "grunt-usemin": "^3.1.1",
  "load-grunt-tasks": "^3.3.0",
  "time-grunt": "^1.2.1"
}

首先得将 dist 目录给删除掉,因为是非覆盖式部署,所以删掉一些过期用不到的静态文件。第一个步骤是图片打包,将需要合并的图片合并了(并修改对应的 css 文件)放置于临时目录(tmp),不需要合并的图片则复制粘贴到临时目录(tmp)。然后对临时目录里的图片进行压缩,最后 hash 后放置于 dist 生产环境目录。

// 步骤一:对图片进行打包
grunt.registerTask('img', [
  'clean:dist',
  'sprite',
  'copy:images',
  'imagemin',
  'filerev:img'
]);

第二个步骤是 css 文件的打包,先用 sass 将 css 压缩到临时目录(tmp)中,接着用 usemin 替换掉里面的已经 hash 的图片资源,最后将 css 文件进行 hash 后放置于 dist 生产环境目录。

// 步骤二:对css进行打包
grunt.registerTask('css', [
  'sass:dist',
  'usemin:css',
  'filerev:css'
]);

第三个步骤是 js 文件的打包,用的是 requirejs 插件将 js 文件合并压缩到临时目录(tmp),然后替换掉文件里的图片资源路径,最后 hash 到生产环境目录(dist),并把不需要 hash 的第三方库复制到 dist 生产环境目录。

// 步骤三:对js进行打包
grunt.registerTask('js', [
  'requirejs',
  'usemin:js',
  'filerev:js',
  'copy:js'
]);

第四个步骤则是 html 文件的打包,先用 grunt-replace 把里面的 php include 替换成特定的模式放置于临时目录(tmp),然后再用 grunt-include-replace 把 html 依赖的 html 片段复制粘贴到一个 html 中,紧接着替换到 html 中的已 hash 的静态文件(包括css,js,image),最后将 html 压缩至 dist 目录下。

// 步骤四:对html进行打包
grunt.registerTask('html', [
  'replace:before',
  'includereplace',
  'usemin:html',
  'replace:after',
  'htmlmin',
  'clean:tmp'
]);

如果你想问我为什么上面的四个步骤不直接写成一个 task 呢,这是我一直解不开的问题。我试过写成一个 task,后果则是文件里的图片资源路径没能够替换成功,可能是在一个 task 内 usemin 插件无法执行多次,于是我就分类写成四个了。 最后总结一下,以上的方式的好处就在于开发时期不需要去合并压缩文件,方便调试。而生产环境则是尽可能去合并压缩,减少用户的请求时间。

原创声明,本文系作者授权云+社区-专栏发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Laoqi's Linux运维专列

集群架构の のzabbix 监控

36610
来自专栏刘望舒

React Native探索之环境搭建与Hello World(Windows/Mac)

1224
来自专栏iOS开发攻城狮的集散地

iOS 友盟崩溃日志定位代码

如果我的介绍没帮到你,可以看看这篇文章: http://www.jianshu.com/p/77d8b5e0d8c3

601
来自专栏云计算教程系列

如何在Ubuntu上构建Android ROM

Android是当今世界上最流行的操作系统。数以百计的不同设备制造商选择将其安装在他们的设备上,因为它是免费的开源的,并且围绕它构建了大量的应用程序和服务生态系...

1250
来自专栏Java技术栈

jmap, jhat, jvisualvm:java堆内存对象分析利器

jmap -help查看命令用法。 jmap -heap <pid> 查看堆使用情况。 jmap -dump导出堆对象文件进行内存分析。 jhat -J-Xm...

3336
来自专栏互联网杂技

入门Webpack(上)

写在前面的话 阅读本文之前,先看下面这个webpack的配置文件,如果每一项你都懂,那本文能带给你的收获也许就比较有限,你可以快速浏览或直接跳过;如果你和十天前...

3129
来自专栏慎独

如何在Github上给别人的项目贡献代码

1134
来自专栏Aloys的开发之路

Groovy实现原理分析——准备工作

首先说明一下为什么要写这样一系列分析Groovy实现原理的博文。我之前在华为大数据部门曾维护过一份规则引擎的项目,该项目说白了就是一种DSL(Domain Sp...

2936
来自专栏潇涧技术专栏

Using Git with multiple Public Keys

很多时候,如果我们在多个网站有了Git账号,例如Github、GitCafe、CodingNet等,当我们与不同网站的代码库进行连接的时候可能会因为我们没有配置...

552
来自专栏JMCui

Docker 系列五(Docker Compose 项目).

    Docker Compose 是官方编排项目之一,负责快速的部署分布式应用。它允许用户通过一个单独的 docker-compose.yml 模板文件(Y...

692

扫码关注云+社区