grunt的学习与应用
前言
在完成一两天的通读
grunt
在线文档之后,原来grunt
也并没有那么地复杂,首先他是一个nodejs程序,无非就是将反复重复的工作(比如有压缩、编译、单元测试、linting等操作)通过脚本来自动化,只需要进行一个命令的执行,即可完成一系列既定执行顺序的操作,可以理解为一系列固定流程的脚本集合,他的庞大主要在于他所提供的插件,在运行grunt
的过程中,可以通过对插件的使用,来满足于实际的业务需要!
Gruntfile文件组成
😕 这里有一个疑问,就是为毛这个Gruntfile.js
要设计为对外暴露一函数对象?这个问题将留到 👇 来进行分析!
😕 关于Gruntfile
文件的组成描述如下:
- 由包裹的 1⃣ 函数包裹,也就是对外暴露一函数方法,该方法接收一
grunt
对象; - 执行grunt的
initConfig
方法,并传递对应的配置; - 加载这个
initConfig
配置中所需的任务/目标所依赖的三方任务(loadNpmTasks); - 注册这个配置文件对外暴露的任务API动作;
grunt程序的运行过程
😕 关键在于liftup
库,它主要用于执行一个CLI脚本程序,这里它写了一个gruntjs程序,然后通过lifeup
库来创建一个系统任务,用来执行这个gruntjs程序,这个liftup
可以简单地理解为是一个读取package.json
以及配置的参数,来实现的可以以一js来作为系统的执行程序的库,最终这个grunt-cli
其底层是调用gruntjs
程序的,也就是说grunt-cli
是脚本grunt
应用程序包装器,通过全局安装的命令来执行这个grunt脚本的!
执行的入口就在grunt.tasks()
函数中
🌠 也就是说,当执行程序(比如grunt
命令),由grunt-cli
命令来执行这个grunt.tasks()
方法,针对之前已经配置好的配置以及依赖的三方任务库,完成一系列既定流程的任务的执行,从 ☝ Gruntfile
文件的组成我们可以晓得grunt
对象所提供的方法有:initConfig
、loadNpmTasks
、registerTask
等,但其实grunt
对象所提供的方法以及属性远不止这些!!
grunt的组成
😕 在开始进入这个
grunt
的组成学习之前,先来了解为什么Gruntfile
必须定义为一函数,而且它里面的grunt
参数又是什么?
要理解这个问题,需要从grunt-cli
的执行程序开始来分析这个!!
🌠 上面这里调用了grunt.tasks()
方法,进入到了任务初始化阶段
🌠 因此,Gruntfile
文件中必须定义为一个方法,该方法中的grunt
参数grunt对象,提供了一系列配置的动作,通过预先配置好的动作,在执行程序的时候好自动加载到对应的配置信息!
关于grunt的组成在运行时的打印如下图所示:
👇 对应结合源码整理一下grunt
的一个架构图:
任务的加载、存储
在
grunt
的模块中,🈶 一个比较核心的模块task
,他负责给grunt
提供最底层的任务支撑,那么关于这个task
,他是怎样的一个组成结构?他是如何来为grunt
提供服务的呢?
通过阅读源码,可以得知grunt
中提供了两个task文件(其中一个task“继承于”另外一个task),一个位于grunt/lib/util/task.js
(这里我们称之为父类),另一个位于grunt/lib/grunt/task.js
(这里我们称之为子类),
而且,关于这个父子关系的继承实现中有一个微妙的用法如下(这里为方便解读,将两者相关的代码写到一起,并做了相应的调整为方便展示):
1 | // 父类task |
从源码的结构我们可以得知,父类task负责任务参数的获取、任务队列(_queue)的管理与执行等相关动作,而子类task则负责加载任务(不管是三方库任务还是自定义的任务),比如通过子类直接从init-->loadTask-->loadNpmTasks-->loadTasks-->loadTask-->registerTask
,来实现将Gruntfile
文件中函数所执行的内容,交由子类task来实现,而且完成配置的过程中,顺便将即将要执行的父类task任务队列(_queue)给丰满起来了!
🌠 这里关键分析一下关于registerTask()
方法的定义与实现(因为父子都有定义,而且这个是核心任务的注册实现过程),以jshint
插件为例(如下图所示):
当调用了grunt.registerTask('test', [jshint]);
时,发生了什么事!!
1 | // 在子类中定义的一对象,用来缓存相关的注册任务 |
👇 关于父类中的registerTask
方法定义(仅展示核心相关代码):
1 | Task.prototype.registerTask = function(name, fn){ |
👇 关于父类中的run
方法定义如下:
1 | Task.prototype.run = function(){ |
👇 是对应的registerTask
的一个流程:
🌠 另外,再补充上关于Task的一个结构图如下:
任务的执行
一切任务的执行,从task.start()开始
该动作有两个流程:
- 执行
task.run(name)
方法将任务添加队列task._queue
队列中;- 执行任务的
task.start()
方法
registerTask与registerMulTasks的区别
首先来看下面的一个一个对比输出结果:
🌠 在定义grunt的任务过程中,可以this.async()
方式来返回一个done
函数,然后在异步执行结束的时候,调用这个done
函数,来告知grunt当前异步已执行完毕,如下图所示:
grunt的插件
对照官方的文档,直接对应初始化了一个插件,对于插件的要求如下:
😕 这里为什么插件的定义一定要在tasks
目录中,而且导出的方法是一个接收grunt
的函数呢?
👇 来分析一下关于插件的加载过程,一切也是从task.loadNpmTasks
方法中入手
1 | task.loadNpmTask = function(name){ |
1 | function loadTasks(tasksdir){ |
1 | function loadTask(filepath){ |
☝ 对上面的代码进行一个简单的描述就是:**loadNpmTasks
的过程,其实就是找寻对应的已经安装的依赖,在其安装目录node_modules
中找到对应的依赖包目录中的tasks目录中的js/coffee
文件,然后来执行这个文件中的方法,并传递对应的grunt对象!**
grunt给我带来了什么?
- 统一的编程范式
- 通过统一的编码习惯结构,可快速尽性web站点或者自研库的自动化测试、打包等流程,实现一键打包的交付操作;
- 编码技巧
- 面向适配器编程,当需要往所有的函数追加一系列自定义的操作时(比如函数执行前后输出函数的执行时间),可以先用一中间变量nfn获取到源fn地址,然后重载源函数,加入对应的执行逻辑代码后,再执行回原本的nfn方法,通过call/apply的方式;
- 面向函数对象”未来实现”编程,可以将自身的服务定义为扩展性很强、支持热拔插的方式(比如插件),将一个个的插件定义为一函数,同时接收库对象(比如grunt),以此来实现程序的”未来实现”编程;
- 实现父子关系继承的手法:采用将父类的实例对象当做子类的原型对象,那么所有的子类都拥有对应的实例方法的同时,还拥有着各自自身的一份属性的拷贝,这有点像java中的面相对象编程;
- 在调试这个grunt的时候,发现需要跟随着源码来进行的断点调试,才能够清楚地知晓关于其中的程序执行过程,因此可以根据grunt的打包结果产物进行执行分析,可以在vscode中进行以下的断点设置来跟进: