前置情景:有一个组件需要引入外部的css文件,并且不能和项目中其他页面样式冲突。引入的这个css文件是存放在cdn上的,需要通过https//的形式访问。我们的项目使用的是angularjs+grunt,我想实现的效果是用postcss给所有选择器前面加一个当前组件的父类名,这样就可以保证样式不和其他页面冲突。
本篇文章第一部分讲述grunt的使用,第二部分讲述使用grunt和postcss处理css文件,实现上述效果
一、grunt简介
前端的小伙伴想必对命令行不陌生,例如运行npm i
命令的时候,npm
就会执行下载依赖的任务。在项目中,我们可能需要将less
文件编译成css
文件、将js
文件合并、代码压缩等等,这些工作就可以交给相关工具来实现。grunt
是一个任务运行器,我们可以在Gruntfile.js
中定义一个命令行任务,并且配置这个任务具体要干什么,然后通过命令行运行定义好的任务,grunt
就可以帮我们执行相应的事情。相比于其他的构建工具,grunt
更适合对于单个文件或者指定的几个文件进行处理,而不是整个项目的打包压缩。
(一)使用步骤
1、安装grunt-cli
Grunt 0.4.x
必须配合 Node.js >= 0.8.0
版本使用。
grunt-cli
是一个命令行工具,用于在项目中运行和管理grunt
任务。它会将grunt
命令加入到你的系统路径中了,以后就可以在任何目录下执行此命令了。grunt-cli
可以运行当前项目中使用的对应版本的grunt
,因此它可以在同一台机器上管理多个版本的grunt
。
grunt-cli的工作原理:
运行grunt xx
命令行时 ,grunt-cli
就是调用node.js
提供的require()
方法,找本地安装的grunt
,并且加载grunt
,把Gruntfile.js
文件配置的信息传递过去,grunt
就会运行xx
任务。
我用的是mac,安装使用:sudo npm i grunt-cli -g
2、创建一个新项目
我这里创建了一个简单的html+css+js
项目。这样的项目可以在本地运行一个本地服务器,以http://
的形式访问。这里提供一种方法:
🦋 npm i live-server -g
安装live-server工具
🦋 根目录的命令行运行live-server
命令
🦋 就可以以当前根目录中的index.html
为主页面,以协议:host:端口号
的形式打开一个网页
3、运行npm init -y
,初始化package.json
4、复制官网提供的依赖列表到package.json中
"devDependencies": {
"grunt": "~0.4.5",
"grunt-contrib-jshint": "~0.10.0",
"grunt-contrib-nodeunit": "~0.4.1",
"grunt-contrib-uglify": "~0.5.0"
}
5、运行npm i
安装依赖(i
是install
的简写)
6、在根目录创建Gruntfile.js
文件
Gruntfile.js
是grunt
的配置文件,在该文件中定义任务。接下来主要看这个文件的结构
(二)Gruntfile.js
🍣 文件格式
Gruntfile.js文件的运行依赖于node.js,需要使用模块导出的方式导出一个函数,任务的定义都在这个函数内部定义
module.exports = function (grunt) {}
🍣 initConfig初始化配置对象
大多数任务都需要依赖一些配置数据,这些数据需要以对象的形式传递一个grunt.initConfig(配置对象)
方法。配置对象中可以以任务名:任务配置项
键值对的形式定义任务,也可以定义配置对象中用到的变量数据。如果变量的键和任务的键出现同名,变量的定义会被忽略。
grunt.initConfig({
name: 'grunt', // 常量
sayhello: {} // 任务名: 任务配置
})
任务还可以拥有多个目标,例如下边嵌套的方式定义了两个目标,通过grunt sayhello:toyou
运行第一个任务,grunt sayhello:toworld
运行第二个任务,如果运行grunt sayhello
,会依次执行这两个任务。
sayhello: {
toyou: {},
toworld: {}
}
🍣 任务配置对象属性一:options
任务有自己的属性options
,每一个目标也可以有自己的options
,目标可以继承任务的options
并且覆盖同名的属性。options
是使用插件的时候,插件提供的配置项,例如grunt-contrib-clean插件,使用示例:
clean: {
options: {
force: true // 可选项,强制删除文件
},
build: ['dist/*'], // 清除dist目录下的所有文件
temp: ['temp/*'] // 清除temp目录下的所有文件
}
🍣 任务配置对象二:files
grunt中大部分的任务都是用来操作文件的,例如将less文件转换为css文件,那么就需要一个源文件,和一个转换后输出的目标文件。files
属性用来指定操作的源文件和输出的目标文件。files
属性有好几种形式:
① 定义src-dest对应关系的三种格式
🪐 简洁格式
这种形式允许每个目标可以省略files
属性,直接在目标任务的属性位置增加src
属性和dest
属性,分别指向源文件和目标文件。但是不能直接作为单任务的属性,不能写到一级任务属性中。比如下面这样:
grunt.initConfig({
name: 'grunt', // 常量
transcss: {
src: './src/index.css',
dest: './dest/index.css',
}
})
这样会导致src
和dest
被解析成两个目标,而不是配置文件的属性。
正确用法:
grunt.initConfig({
name: 'grunt', // 常量
transcss: {
task1: {
src: 'src/css/style.css',
dest: 'dest/css/style.css'
},
task2: {
src: 'src/css/style2.css',
dest: 'dest/css/style2.css'
}
}
})
这种格式支持额外属性
🪐 文件对象格式
将files
定义为对象,每一个映射定义为一个键值对。属性名就是目标文件,源文件就是它的值(源文件列表则使用数组格式声明)。这种方式不能给每个映射增加额外的属性
transcss: {
files:{
'dest/style.css': ['src/style.css', 'src/style2.css']
}
}
🪐 文件数组格式
将files
定义为数组,每一个映射定义为一个对象。允许映射拥有额外的属性
transcss: {
files:[
{src: 'src/css/style.css', dest: 'dist/css/style.css'},
{src: 'src/css/style2.css', dest: 'dist/css/style2.css'}
]
}
②【额外属性】
简洁格式和文件数组格式支持额外属性。额外属性是grunt提供的帮助我们快速筛选源文件的属性。额外属性的作用,这里用一个插件grunt-contrib-concat,方便演示。这个插件的作用是合并代码。首先说明源文件目录中有css文件和js文件,
基础代码:
// 基础代码
grunt.initConfig({
concat: {
dist: {
src: 'src/**',
dest: 'dist/index.css'
},
},
});
// 加载concat任务
grunt.loadNpmTasks('grunt-contrib-concat');
上述代码会把src目录下所有文件都合并到dest指定的文件中。grunt.loadNpmTasks('grunt-contrib-concat');
用来加载该插件指定的任务,加载完毕后,就可以使用grunt concat
命令运行该任务。此时运行任务,会创建dist/index.css
文件,并且合并源文件中所有代码:
此时想只合并css文件,就可以用filter
属性
🐳 filter
指向一个方法或方法名,这个方法返回布尔值,用来筛选符合条件的源文件。filter
指定的方法接收一个filepath
参数,就是所有符合源文件格式的文件路径,如果以.css
结尾的,就通过检测。
grunt.initConfig({
concat: {
dist: {
src: 'src/**',
dest: 'dist/index.css',
filter: function (filepath) {
return filepath.endsWith('.css');
}
},
},
});
// 加载concat任务
grunt.loadNpmTasks('grunt-contrib-concat');
此时grunt concat
运行的效果:
🐳 nonull
nonull
配置时设置为true即可,用来进行空文件捕捉,主要作用有两个:
1️⃣ 找不到匹配的源文件时,抛错
2️⃣ 不会合并不存在的空文件,但实际的效果也看不出来,AI的解释是不会合并大小为0的文件,但就算合并,也是啥都没有。就算不加nonull
,也会创建dist/index.css
,并且是空文件。
例如下面代码:
grunt.initConfig({
concat: {
dist: {
src: 'sdss/index.css',
dest: 'dist/index.css',
},
distjs: {
src: 'myindex.js',
dest: 'dist/combined.js',
}
},
});
// 加载concat任务
grunt.loadNpmTasks('grunt-contrib-concat');
运行grunt concat
时,虽然sdss/index.css
不存在,但是不会有任何的抛错信息,并且不会影响第二个任务的进行:js文件的合并
如果给css的合并任务配置nonull: true
,就会有错误提示:
--verbore
用来获取更详细的信息,但是在concat任务中,我加不加效果都是一样的。
🐳 expand
处理动态的src-dest
文件映射。下面会讲什么是动态的文件映射。
其他的属性这里就不一一演示了。
③ 定义文件的几种快捷方式
上述讲述了定义src-dest一一对应关系的几种格式。源文件可以直接写成明确的文件目录,也可以用下面这几种方式,提供模版,符合模版的文件就会被当做源文件进行处理
🌺 通配符模式
用法类似于正则表达式
*
匹配任意数量的字符,但不匹配 /
?
匹配单个字符,但不匹配 /
**
匹配任意数量的字符,包括 /,只要它是路径中唯一的一部分
{}
允许使用一个逗号分割的“或”表达式列表
!
在模式的开头用于排除一个匹配模式所匹配的任何文件
// 匹配foo目录下,以th开头的所有js文件:
{src: 'foo/th*.js', dest: ...}
// foo目录下,以a或b开头的所有js文件:
{src: 'foo/{a,b}*.js', dest: ...}
// 效果同上:
{src: ['foo/a*.js', 'foo/b*.js'], dest: ...}
// foo目录中所有的.js文件,按字母顺序排序:
{src: ['foo/*.js'], dest: ...}
// 首先是bar.js,接着是剩下的.js文件,并按字母顺序排序:
{src: ['foo/bar.js', 'foo/*.js'], dest: ...}
// 除bar.js之外的所有的.js文件,按字母顺序排序:
{src: ['foo/*.js', '!foo/bar.js'], dest: ...}
// 按字母顺序排序的所有.js文件,但是bar.js在最后。
{src: ['foo/*.js', '!foo/bar.js', 'foo/bar.js'], dest: ...}
🌺 动态构建文件对象
利用附加属性可以动态构建文件列表。首先expand
属性要设置为true
。下面是几个可以指定条件的附加属性:
1、cwd
,源文件路径前缀。
2、src
, 相对于cwd
路径的匹配模式。
3、dest
,目标文件路径前缀。
4、ext
,目标文件的扩展名。
5、extDot
,指定扩展名开始的位置,可选值last
/first
。例如指定目标文件的扩展名为.min.css
,源文件是index1.my.css
extDot
值设置为first
,表示扩展名从第一个点开始替换,生成的文件名就是index1.min.css
;extDot
值设置为last
,表示扩展名从最后一个点开始替换,生成的文件名就是index1.my.min.css
6、flatten
是否展平文件夹,可选值true
/false
。如果源文件路径中有多个文件夹,将flatten
设置为true
,生成的目标文件夹不会创建子文件夹,会将所有文件放在根目录下;如果flatten
设置为false
,会根据源文件路径结构创建对应的子文件夹。
7、rename
,指定一个方法,用来对目标文件进行重命名或者重新定义路径。接收两个参数:
☕️ dest
:目标文件路径前缀
☕️ src
:经过扩展名转换之后的目标文件
例如下面一段代码,会将目标文件路径中的index
转换为hhh
输出到目标文件夹下
rename: function (dest, src) {
return dest + '/' + src.replace('index', 'hhh')
}
【整体示例】
src目录:
代码:
grunt.initConfig({
concat: {
dist: {
expand: true, // 开启动态扩展
cwd: 'src', // 源文件找src文件夹中的
src: '*/*.css', // 源文件:src文件夹中的所有css文件
dest: 'dist', // 转换之后的文件放到dist文件夹中
ext: '.min.css', // 转换后的文件后缀名
extDot: 'last', // 扩展名是在文件名的最后一个点处
flatten: false, // 根据源文件目录创建目标文件目录结构
rename: function (dest, src) {
console.log(dest, src)
return dest + '/' + src.replace('index', 'hhh');
}
}
},
});
// 加载concat任务
grunt.loadNpmTasks('grunt-contrib-concat');
生成的目标文件夹:
🌺 模版
使用<% %>
分隔符指定,其中可以嵌入表达式,例如下面代码,源文件将会匹配src二层目录下以a或b或c为前缀,后面跟index,并且以.css为结尾的文件
grunt.initConfig({
concat: {
foo:'index',
dist: {
src: 'src/*/{a,b,c}<%= foo%>*.css',
dest: 'dist/index.min.css', // 转换之后的文件
}
},
});
下面几个文件都会被匹配上
🌺 导入外部数据
Grunt有grunt.file.readJSON
和grunt.file.readYAML
两个方法分别用于引入JSON和YAML数据。
官网示例:
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
uglify: {
options: {
banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n'
},
dist: {
src: 'src/<%= pkg.name %>.js',
dest: 'dist/<%= pkg.name %>.min.js'
}
}
});
🍣 grunt.registerTask
grunt.registerTask()
方法有好几种用法
① 一条命令运行多个任务
如果你想只执行一条命令行就运行多个任务,可以通过grunt.registerTask()
方法,指定一个任务列表。例如:
grunt.registerTask('default', ['jshint', 'qunit', 'concat', 'uglify']);
当你运行grunt default
这一行命令的时候,就会执行指定的任务列表中的所有任务
② 多目标任务
如果任务有多个目标,在grunt.registerMultiTask()
的最后一参数方法中可以通过this.target
获取当前运行的任务目标;this.data
属性获取当前任务目标指定的数据
grunt.initConfig({
log: {
foo: [1, 2, 3],
bar: 'hello world',
baz: false
}
});
grunt.registerMultiTask('log', function() {
// this.target:目标,foo,bar,baz
// this.data:数据,[1,2,3],'hello world',false
grunt.log.writeln(this.target + ': ' + this.data);
});
③ 获取命令行传进来的参数
还看上述示例,命令行可以通过grunt log:foo
运行指定任务目标,也可以通过:
分割的方式继续向任务中传参数。例如:
grunt.registerMultiTask('log', function() {
grunt.log.writeln(...arguments); // 展开并打印参数列表
});
当运行grunt log
时,打印undifuned
;当运行grunt log:9:99
时,打印9 99
④ 自定义任务
之前用到的grunt.loadNpmTasks()
是用来加载插件任务的,插件制定好了启动任务的命令行代码,而自定义任务就需要自己指定一个命令行,通过grunt.registerTask()
方法注册,参数二方法中具体要做什么操作,请见下一章《实战环节》
// 定义自定义任务postcss
grunt.registerTask('postcss', function () {
// 获取配置信息
const config = grunt.config('postcss.options');
console.log(config.src, config.dest)
})
(三)实战环节
在使用postcss之前,需要通过npm i postcss
或者 yarn add postcss
安装postcss。
回顾一下需求:
1、从cdn上获取css文件
2、处理文件,给所有选择器前面加上前缀类名选择器
第一步获取文件可以使用node.js
提供的https
模块中的get()
方法;第二步处理文件需要使用postcss
将css文件解析成AST树,然后对样式表进行操作,最终再转换成普通样式字符串,这个原理很复杂,但是使用postcss
提供的方法,实现起来是很简单的;最后需要将样式字符串写到目标文件中。
1、获取文件
首先将文件地址定义到配置对象里面,方便后续复用和维护,并且需要引入https模块
const https = require("https");
grunt.initConfig({
postcss: {
options: {
targetUrl: 'https://img.xkw.com/dksih/xopqbm/preview2.csss',
}
}
});
定义一个独立的根据url
获取文件的方法,方便复用,这个方法也可以放到配置项里面。使用https.get()
方法。由于获取文件这个过程需要请求,所以需要返回一个Promise
,方便处理异步操作,在Promise
对象中,使用https.get()
方法,
https.get()
方法参数一是url
,参数二是请求成功的回调函数,回调函数接收res
参数,这个res
不是返回的数据,而是一个对象,类似于HTML
元素,我们可以监听它的事件。res
对象有两个事件我们需要监听,一个是data
事件,只要有数据返回就会触发这个事件;一个是end
事件,当请求结束的时候就会触发这个事件。
我们需要使用on()
监听res对象的data
事件,data
事件监听的回调函数中获取的数据才是借口返回的数据。需要注意的是,data
可能不是一次性返回的,所以要把多次data
事件监听到的返回值进行拼接。
当监听到end
事件时,说明返回行为已经结束,当前Promise
对象就可以resolve
了。
考虑到错误处理,$http.get()
需要监听error
事件,error
的时候,当前Promise
就可以reject
了
grunt.initConfig({
postcss: {
getSheetFile: function (url) {
return new Promise((resolve, reject) => {
https.get(url, res => {
let data = '';
res.on('data', chunk => {
data += chunk;
})
res.on('end', () => {
resolve(data);
})
}).on('error', e => {
reject(e);
})
})
},
options: {
targetUrl: 'https://img.xkw.com/dksih/xopqbm/preview2.css',
}
}
});
2、定义postcss任务
在任务指定的参数方法中,可以通过grunt.config.get(key)
获取配置对象中key
属性的值。key
不止支持字符串,也支持嵌套的结构,例如postcss.options.targetUrl
。在postcss
任务中,首先要获取配置对象中定义的目标路径和getSheetFile
,并且执行getSheetFile
。
这里还有一点很重要,就是如何定义一个异步任务。如果不进行特殊的配置,命令行是不会等待异步任务执行完毕才结束线程的。异步任务有固定的结构,通过const done = this.async();
定义一个延时对象,当异步任务结束时,执行done()
,通知任务结束。如果不执行done()
,任务永远都不会结束。
后续的步骤先不看,咱们先测试一下至今为止的效果。整体代码:
const https = require("https");
module.exports = function (grunt) {
grunt.initConfig({
postcss: {
getSheetFile: function (url) {
return new Promise((resolve, reject) => {
https.get(url, res => {
let data = '';
res.on('data', chunk => {
data += chunk;
})
res.on('end', () => {
resolve(data);
})
}).on('error', e => {
reject(e);
})
})
},
options: {
targetUrl: 'https://img.xkw.com/dksih/xopqbm/preview2.css',
}
}
});
// 定义自定义任务postcss
grunt.registerTask('postcss', function () {
// 获取目标地址
const url = grunt.config.get('postcss.options.targetUrl');
// 获取方法
const getSheetFile = grunt.config.get('postcss.getSheetFile');
// 定义异步任务
const done = this.async();
getSheetFile(url).then(res => {
console.log(res);
done();
})
})
}
运行grunt postcss
,查看输出,输出了整个文件的所有内容
3、使用postcss处理文件
getSheetFile(url).then()
中的res
是普通的字符串,postcss
提供的parse()
方法可以将样式字符串转换成抽象语法树AST,用root
变量接收,root
的walkRules
方法可以遍历所有的样式规则,类似于数组的forEach()
方法
getSheetFile(url).then(res => {
// postcss解析文件
const root = postcss.parse(res);
// 遍历所有的选择器
root.walkRules(rule => {
console.log(rule.selector);
})
done();
})
此时运行grunt postcss
,查看控制台输出:
在walkRules()
里面就可以给rule.selector
重新赋值
rule.selector = '.similar-quesitons-from-tiku ' + rule.selector
转换完毕之后输出root
,已经给所有选择器增加了一个父级选择器:
4、写文件
接下来需要把转换之后的文件写入到当前文件目录中。写入文件可以使用grunt提供的grunt.file.write()
方法,这个方法接收两个参数,参数一是目标文件,参数二是要写入的内容。目标文件我们也定义到配置文件中
grunt.file.write(grunt.config.get('postcss.options.dest'), root.toString());
运行grunt postcss
看下效果:
5、整体代码
const https = require("https");
module.exports = function (grunt) {
grunt.initConfig({
postcss: {
getSheetFile: function (url) {
return new Promise((resolve, reject) => {
https.get(url, res => {
let data = '';
res.on('data', chunk => {
data += chunk;
})
res.on('end', () => {
resolve(data);
})
}).on('error', e => {
reject(e);
})
})
},
options: {
targetUrl: 'https://img.xkw.com/dksih/xopqbm/preview2.css',
dest: 'dist/index.css'
}
}
});
// 定义自定义任务postcss
grunt.registerTask('postcss', function () {
const url = grunt.config.get('postcss.options.targetUrl');
const getSheetFile = grunt.config.get('postcss.getSheetFile');
const done = this.async();
const postcss = require('postcss');
getSheetFile(url).then(res => {
// postcss解析文件
const root = postcss.parse(res);
// 遍历所有的选择器
root.walkRules(rule => {
rule.selector = '.similar-quesitons-from-tiku ' + rule.selector
})
// 写入文件
grunt.file.write(grunt.config.get('postcss.options.dest'), root.toString());
done();
})
})
}