【grunt+postcss应用】给一个css文件中所有选择器加上父级选择器

前置情景:有一个组件需要引入外部的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安装依赖(iinstall的简写)
6、在根目录创建Gruntfile.js文件
Gruntfile.jsgrunt的配置文件,在该文件中定义任务。接下来主要看这个文件的结构

(二)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',
    }
})

这样会导致srcdest被解析成两个目标,而不是配置文件的属性。
正确用法:

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.readJSONgrunt.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变量接收,rootwalkRules方法可以遍历所有的样式规则,类似于数组的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();
        })
    })
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值