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 条评论
登录 后参与评论

相关文章

来自专栏技术墨客

React由0到1

    本文记录了本人以及目前团队从无到有使用React的过程,我们将从webpack开始说起,一步一步展现React最基本的开发生态。在这里并不会介绍任何js...

843
来自专栏游戏杂谈

rtx登录内网系统

公司内部使用rtx进行沟通和交流,经常遇到订餐的问题,用php写了一个订餐系统,实现rtx上点击链接打开系统就自动登录了,无需再次输入用户名和密码。

861
来自专栏技术墨客

React 搭建开发环境

本文记录了本人以及目前团队从无到有使用React的过程,我们将从webpack开始说起,一步一步展现React最基本的开发生态。在这里并不会介绍任何jsx或es...

1091
来自专栏Python中文社区

几个提高工作效率的Python内置小工具

專 欄 ❈本文作者:赖明星 博客地址: https://www.zhihu.com/people/mingxinglai❈ 在这篇文章里,我们将会介绍4个Pyt...

2168
来自专栏IMWeb前端团队

让chrome插件在手机上跑起来

本文作者:IMWeb moonye 原文出处:IMWeb社区 未经同意,禁止转载 创建一个chrome的插件,并让这个插件能够作为一个app,运行在终...

1765
来自专栏古时的风筝

如何用django开发一个简易个人Blog

功能概要:(目前已实现功能) 公共展示部分: 1.网站首页展示已发布的博客记录,包括名称、摘要信息、发布日期、阅读量及评论数。 2.首页文章列表可按照分类筛选。...

2267
来自专栏优启梦

全平台通用评论神器一键自动填写昵称、邮箱和网址

我们在访问网站时,看到一篇文章,想发表评论时,是否经常要在评论框里手动填写自己的昵称、E-mail 和网址等留言评论信息?重复的打字会让我们感到很乏味。 为了解...

43915
来自专栏Youngxj

emlog新方法:导航栏加入Font Awesome图标,可自定义

1593
来自专栏前端大白专栏

关于roadhogrc 新版本问题

1175
来自专栏北京马哥教育

最实用也最容易被遗忘的 Linux 命令行使用技巧

作为一个日常在Linux环境下工作的工程师,每天都要大量使用Linux命令行。有时候我们会在网上翻查命令行的使用技巧,但是一旦未能及时进行练习,很快就会把这些小...

931

扫码关注云+社区