Webpack-Plugin

TIP

所谓 loader 只是一个导出为函数的 JavaScript 模块。它接收上一个 loader 产生的结果或者资源文件(resource file)作为入参。也可以用多个 loader 函数组成 loader chain。compiler 需要得到最后一个 loader 产生的处理结果。这个处理结果应该是 String 或者 Buffer(被转换为一个 string)

一 运行流程

flow

loader-runner是一个执行loader链条的的模块

二 loader-runner简介

1 类型

  • 四大类型

    1. post: 后置
    2. inline: 内联
    3. normal: 普通
    4. pre: 前置
  • 执行优先级:pre > normal > inline > post

  • 叠加顺序为:post(后置) + inline(内联) + normal(普通) + pre(前置)

2 特殊符号

符号变量含义
-!noPreAutoLoaders不要前置和普通 loader
!noAutoLoaders不要普通 loader
!!noPrePostAutoLoaders不要前置、后置和普通 loader,只要内联 loader

3 执行顺序

  • pitch 从左往右执行
  • loader 从下往上,从右往左执行

loader

4 pitch

  • 每个loader都有一个pitch方法,但pitch不是必须的
  • pitch两个作用:阻断 loader 链 和 存放自身需要的额外数据
  • pitch方法的三个参数:
    1. remainingRequest:后面的loader+资源路径,loadername!的语法
    2. precedingRequest:资源路径
    3. metadata:辅助对象,用于存放自身需要的额外数据
  • loader中的this表示上下文,可以访问很多属性和方法
// loader方法
function loader(source) {
	// 获取pitch中定义的data对象
	console.log(this.data.name);
	return source;
}

/**
 * pitch方法,是非必须的
 * 后面的loader等都可以拿到data中的值
 * 每一个loader都有一个自己的data,相互之间是完全 独立的
 */
loader.pitch = function(remainingRequest,previousRequest,data){
    data.name = 'loader-pitch';
}
module.exports = loader;
  • 有pitch的loader执行过程
// use配置loader
use: ['loader1', 'loader2', 'loader3']
// 执行过程
|- loader1 `pitch`
  |- loader2 `pitch`
    |- loader3 `pitch`
      |- requested module is picked up as a dependency
    |- loader3 normal execution
  |- loader2 normal execution
|- loader1 normal execution
  • 阻断loader链条:在 loader2 的 pitch 中返回了一个字符串,执行流程如下:

loader

三 loader-runner使用

# webpack内置了loader runnder,安装了webpack可以直接使用
npm i webpack webpack-cli -D

1 预制八个loader

  • post-loader1.js
function loader(source) {
	console.log('post1');
	return source + '//post1';
}
module.exports = loader;
  • post-loader2.js
function loader(source) {
	console.log('post2');
	return source + '//post2';
}
module.exports = loader;
  • inline-loader1.js
function loader(source) {
	console.log('inline1');
	return source + '//inline1';
}
module.exports = loader;
  • inline-loader2.js
function loader(source) {
	console.log('inline2');
	return source + '//inline2';
}
module.exports = loader;
  • normal-loader1.js
function loader(source) {
	console.log('normal1');
	return source + '//normal1';
}
module.exports = loader;
  • normal-loader2.js
function loader(source) {
	console.log('normal2');
	return source + '//normal2';
}
module.exports = loader;
  • pre-loader1.js
function loader(source) {
	console.log('pre1');
	return source + '//pre1';
}
module.exports = loader;
  • pre-loader2.js
function loader(source) {
	console.log('pre2');
	return source + '//pre2';
}
module.exports = loader;

2 直接使用

let path = require('path');
let fs = require('fs');
let {runLoaders} = require('loader-runner');

// 要读取的资源文件
let resource = path.resolve(__dirname, 'src', 'index.js');

// loader数组
let loaders = [
	path.resolve(__dirname, 'loaders', 'post-loader1.js'),
	path.resolve(__dirname, 'loaders', 'post-loader2.js'),
	path.resolve(__dirname, 'loaders', 'inline-loader1.js'),
	path.resolve(__dirname, 'loaders', 'inline-loader2.js'),
	path.resolve(__dirname, 'loaders', 'normal-loader1.js'),
	path.resolve(__dirname, 'loaders', 'normal-loader2.js'),
	path.resolve(__dirname, 'loaders', 'pre-loader1.js'),
	path.resolve(__dirname, 'loaders', 'pre-loader2.js'),
];

/**
 * 1.读取要加载的资源
 * 2.把资源传递给loader链条,一一处理,最后得到结果
 */
runLoaders({
	// 要加载和转化的资源,可以包含查询字符串
	resource,
	// loader的绝对路径数组
	loaders,
	// 额外的loader上下文对象,即loader里面的this指向
	context: {name: 'test'},
	// 读取文件的方法
	readResource: fs.readFile.bind(fs)

}, function(err, result) {
	console.log(err);
	console.log(result);
})

3 执行结果

pre2
pre1
normal2
normal1
inline2
inline1
post2
post1

null

{ result: 
   [ 'console.log(\'index\');\r\n//pre2//pre1//normal2//normal1//inline2//inline1//post2//post1' ],
  resourceBuffer: <Buffer 63 6f 6e 73 6f 6c 65 2e 6c 6f 67 28 27 69 6e 64 65 78 27 29 3b 0d 0a>,
  cacheable: true,
  fileDependencies: 
   [ 'd:\\web_project\\loader\\src\\index.js' ],
  contextDependencies: [],
  missingDependencies: [] 
}

四 loader-runner原理

1 类型与特殊符号

// 引入依赖模块
let path = require('path');
let fs = require('fs');
let {runLoaders} = require('loader-runner');

let filePath = path.resolve(__dirname, 'src', 'index.js');
// 内联loader
let request = `inline-loader1!inline-loader2!${filePath}`;
// 先替换特殊符号,然后切割获取资源名称
let parts = request.replace(/^-?!+/,'').split('!');
// 最后一个元素就是要加载的资源了
let resource = parts.pop();
// 变为绝对路径
let resolveLoader = loader => path.resolve(__dirname, 'loaders', loader);
// inlineLoaders=[inline-loader1绝对路径, inline-loader2绝对路径 ]
let inlineLoaders = parts.map(resolveLoader);

let rules = [
    {
        test:/\.js$/,
        use:['normal-loader1','normal-loader2']
    },
    {
        test:/\.js$/,
        enforce:'post',//post webpack保证一定是后执行的
        use:['post-loader1','post-loader2']
    },
    {
        test:/\.js$/,
        enforce:'pre',//一定先执行eslint
        use:['pre-loader1','pre-loader2']
    },
];

// 解析webpack配置的rules中的loader
let preLoaders = [];
let postLoaders = [];
let normalLoaders = [];
for (let i = 0; i < rules.length; i++) {
	let rule = rules[i];
	if (rule.test.test(resource)) {
		if (rule.enforce == 'pre') {
			preLoaders.push(...rule.use);
		} else if (rule.enforce == 'post') {
			postLoaders.push(...rule.use);
		} else {
			normalLoaders.push(...rule.use);
		}
	}
}
preLoaders = preLoaders.map(resolveLoader);
postLoaders = postLoaders.map(resolveLoader);
normalLoaders = normalLoaders.map(resolveLoader);

// 按照顺序组装loader:post(后置) + inline(内联) + normal(正常) + pre(前置)
let loaders = [];
if (request.startsWith('!!')) {
	// 不要前后置和普通 loader,只要内联 loader
	loaders = [...inlineLoaders];
} else if (request.startsWith('-!')) {
	// 不要前置和普通 loader
	loaders = [...postLoaders,...inlineLoaders];
} else if (request.startsWith('!')) {
	// 不要普通 loader
	loaders = [...postLoaders,...inlineLoaders,...preLoaders];
} else {
	// 全部loader
	loaders = [...postLoaders,...inlineLoaders,...normalLoaders,...preLoaders];
}

/**
 * 1.读取要加载的资源
 * 2.把资源传递给loader链条,一一处理,最后得到结果
 */
runLoaders({
	// 要加载和转化的资源,可以包含查询字符串
	resource,
	// loader的绝对路径数组
	loaders,
	// 额外的loader上下文对象
	context: {name: 'test'},
	// 读取文件的方法
	readResource: fs.readFile.bind(fs)

}, function(err, result) {
	console.log(err);
	console.log(result);
})

2 loader-runner实现

五 loader 案例

  • webpack配置本地loader的三种方式
  1. 在resolveLoader里配置alias别名
module.exports = {
   resolveLoader:{
        alias:{
            'babel-loader':path.resolve('./loaders/babel-loader.js')
        }
    },
    module:{
        rules:[
            {
				test: /\.js$/,
				use: 'babel-loader'
            }
        ]
    }
}
  1. 在resolveLoader里配置modules别名
module.exports = {
   resolveLoader:{
		modules: [path.resolve('./loaders'), 'node_modules']
    },
    module:{
        rules:[
            {
				test: /\.js$/,
				use: 'babel-loader'
            }
        ]
    }
}
  1. 在配置 rules 的时候直接指定 loader 的绝对路径
module.exports = {
    module:{
        rules:[
            {
				test: /\.js$/,
                use: [path.resolve('./loaders/babel-loader.js')],
                include: path.resolve('src')
            }
        ]
    }
}

1 babel-loader

npm i @babel/preset-env @babel/core -D
  • loaders/babel-loader.js
const core = require('@babel/core');

/**
 * ES6 转化为 ES5
 * @param {*} source 上一个loader给我这个loader的内容或者最原始模块内容
 * @param {*} inputSourceMap 上一个loader传递过来的sourceMap
 */
function loader(source, inputSourceMap) {
	const options = {
		presets: ['@babel/preset-env'],
		inputSourceMap,
		sourceMap: true, // 告诉babel要生成sourceMap
		filename: path.basename(this.resourcePath) // /src/index.js
	}
	// code:转换后的代码 map:sourcemap ast:抽象语法树
	let {code, map, ast} = core.transform(source, options);
	/**
	 * 当你需要返回多值的时候需要使用 this.callback来传递多个值
	 * 只需要返回一个值,可以直接 return 
	 * map 可以让我们进行代码调试 debug的时候可以看到源代码
	 * ast 如果你返回了ast给webpack。webpack则直接分析 就可以,不需要自己转AST了,节约 时间
	 */
	return this.callback(null, code, map, ast);
}
module.exports = loader;
  • webpack.config.js
const path = require('path');
module.exports = {
    module:{
        rules:[
            {
                test: /\.js$/,
                use: [path.resolve('./loaders/babel-loader.js')],
                include: path.resolve('src')
			}
        ]
    }
}

2 file-loader

const path = require('path');
const {getOptions,interpolateName} = require('loader-utils');

/**
 * 负责把资源文件拷贝到对应的目录,不做文件转换,只会生成一个唯一的文件名
 * 1.把此文件内容拷贝到目标目录里
 * 2. 生成一个唯一的hash文件名
 * @param {*} content 内容
 */
function loader(content) {
	// 获取在loader中配置的参数对象
	let options  = getOptions(this) || {};
	// 根据 options.name 以及文件内容生成一个唯一的文件名
	let filename = interpolateName(this, options.name || '[contenthash].[ext]', {content});
	// 输出文件,webpack 会根据参数创建对应的文件,放在 public path 目录下
	this.emitFile(filename, content);
	// 根据不同的配合导出不同的模块
	if (typeof options.esModule === 'undefined' || options.esModule) {
		// es modules 》 使用需要加default
		return `export default "${filename}"`;
	} else {
		// commonjs 》直接使用
		return `module.exports = ${JSON.stringify(filename)}`;
	}
}
// 二进制格式
loader.raw = true;
module.exports = loader;
  • webpack.config.js
const path = require('path');
module.exports = {
    module:{
        rules:[
            {
                test:/\.(jpg|png|gif)$/,
                use:[{
                    loader:path.resolve('./loaders/file-loader.js'),
                    options:{
						name:'[hash:8].[ext]',
						esModule: false
                    }
                }],
                include:path.resolve('src')
            }
        ]
    }
}

3 url-loader

npm i mime -D
  • loaders/url-loader.js
const mime = require('mime');
const {getOptions} = require('loader-utils');

/**
 * 对file-loader的加强,根据配置的参数,来看判断是否需要将图片资源变为base64内嵌到文件中,减少请求
 * @param {*} content 内容
 */
function loader(content) {
	// 获取在loader中配置的参数对象
	let options  = getOptions(this) || {};
	// 解析限制参数
	let {limit=8*1024, fallback="file-loader"} = options;
	// 获取文件的类型
	const mimeType = mime.getType(this.resourcePath);//image/jpeg
	// 判断文件大小,输出不同内容
	if (content.length < limit) {
		// 转成base64内嵌
		let base64Str = `data:${mimeType};base64,${content.toString('base64')}`;
		return `module.exports = ${JSON.stringify(base64Str)}`;
	} else {
		// 走file-loader的逻辑
		let fileLoader = require(fallback);
		return fileLoader.call(this, content);
	}
}
// 二进制格式
loader.raw = true;
module.exports = loader;
  • webpack.config.js
const path = require('path');
module.exports = {
    module:{
        rules:[
           {
                test:/\.(jpg|png|gif)$/,
                use:[{
                    loader:path.resolve('./loaders/url-loader.js'),
                    options:{
                        name:'[hash:8].[ext]',
                        limit:60*1024,
                        fallback:path.resolve('./loaders/file-loader.js')
                    }
                }],
                include:path.resolve('src')
            }
        ]
    }
}

4 less-loader

npm i less -D
  • loaders/less-loader.js
const less = require('less');
/**
 * 把LESS编译成CSS字符串
 * @param {*} content 内容
 */
function loader(content) {
	// 默认情况下loader执行是同步的
	// 通过调用this.async方法可以返回一个函数,会把loader的执行变成异步的,不会直接往下执行了
	let callback = this.async();
	// less 转化
	less.render(content, {filename: this.resource}, (err, output) => {
		// 会让loader继续往下执行
		callback(err, output.css);
	});
}
module.exports = loader;
  • webpack.config.js
const path = require('path');
module.exports = {
    module:{
        rules:[
          {
                test:/\.less$/,
                use:[
					path.resolve('./loaders/less-loader.js')
				],
                include:path.resolve('src')
            }
        ]
    }
}

5 style-loader

let loaderUtils = require('loader-utils');

/**
 * 把CSS变成一个JS脚本
 * 脚本就是动态创建一个style标签,并且把这个style标签插入到HTML里header
 * 什么时候会用到pitch loader
 * 当你想把两个最左侧的loader级联使用的时候
 * @param {*} inputSource 内容
 */
function loader(inputSource) {}

loader.pitch = function(remainingRequest, previousRequest, data) {
	// remainingRequest > 剩下的loader!要加载的路径
	// !!只要行内样式
	// !!./loaders/css-loader.js!./src/index.css
	let style = `
		let style = document.createElement('style');
		style.innerHTML = require(${loaderUtils.stringifyRequest(this, "!!" + remainingRequest)}).toString();
		document.head.appendChild(style);
	`;
	return style;
}
module.exports = loader;
  • webpack.config.js
const path = require('path');
module.exports = {
    module:{
        rules:[
          {
                test:/\.less$/,
                use:[
					path.resolve('./loaders/style-loader.js')
				],
                include:path.resolve('src')
            }
        ]
    }
}

6 to-string-loader

let loaderUtils = require('loader-utils');

/**
 * 根据不同的配置输出字符串
 */
function loader(inputSource) {}

loader.pitch = function(remainingRequest, previousRequest, data) {
	return `
		let result = require(${loaderUtils.stringifyRequest(this, "!!" + remainingRequest)});
		if(result && result.__esModule){
			result = result.default;
		}
		if(typeof result === 'string'){
			module.exports = result;
		}else{
			module.exports = result.toString();
		}
	`;
}
module.exports = loader;
  • webpack.config.js
const path = require('path');
module.exports = {
    module:{
        rules:[
          {
                test:/\.css$/,
                use:[
					path.resolve('./loaders/to-string-loader.js')
				],
                include:path.resolve('src')
            }
        ]
    }
}

六. css-loader

1 安装

npm i webpack webpack-cli webpack-dev-server html-webpack-plugin css-loader css-selector-tokenizer file-loader less less-loader postcss style-loader to-string-loader -D

2 使用

  • webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
	mode:'development',
	devtool:false,
	module:{
		rules:[
			{
				test:/\.css$/,
				use:[
					"to-string-loader",
					{
						loader:'css-loader',
						options:{
							url:false,
							import:false,
							esModule: false
						}
					}
				],
				include:path.resolve('src')
			}
		]
	},
	plugins:[
		new HtmlWebpackPlugin({template:'./src/index.html'}),
	]
}
  • src\index.js
const css = require("./global.css");
console.log(css);
  • src\global.css
body {
    background-color: green;
}

PS: 引用顺序为 index.js > global.css

  • dist\main.js - 打包文件
(() => {
	var __webpack_modules__ = ({
		"./node_modules/css-loader/dist/cjs.js??ruleSet[1].rules[0].use[1]!./src/global.css":
		((module, exports, require) => {
			var api = require("./node_modules/css-loader/dist/runtime/api.js");
			var EXPORT = api(function (i) {
				return i[1]
			});
			EXPORT.push([
				module.id, 
				"body {\r\n\tbackground-color: green;\r\n}", 
				""
			]);
			module.exports = EXPORT;
		}),

		"./node_modules/css-loader/dist/runtime/api.js": ((module) => {
			module.exports = function (cssWithMappingToString) {
				var list = [];
				list.toString = function toString() {
					return this.map(function (item) {
						var content = cssWithMappingToString(item);
						if (item[2]) {
							return "@media ".concat(item[2], " {").concat(content, "}");
						}
						return content;
					}).join('');
				}; 
				list.i = function (modules, mediaQuery, dedupe) {
					if (typeof modules === 'string') {
						modules = [[null, modules, '']];
					}

					var alreadyImportedModules = {};
					if (dedupe) {
						for (var i = 0; i < this.length; i++) {
							var id = this[i][0];
							if (id != null) {
								alreadyImportedModules[id] = true;
							}
						}
					}

					for (var _i = 0; _i < modules.length; _i++) {
						var item = [].concat(modules[_i]);
						if (dedupe && alreadyImportedModules[item[0]]) {
							continue;
						}

						if (mediaQuery) {
							if (!item[2]) {
								item[2] = mediaQuery;
							} else {
								item[2] = "".concat(mediaQuery, " and ").concat(item[2]);
							}
						}

						list.push(item);
					}
				};
				return list;
			};
		}),

 		"./src/global.css": ((module, exports, require) => {
			var result = require("./node_modules/css-loader/dist/cjs.js??ruleSet[1].rules[0].use[1]!./src/global.css");
			if (typeof result === "string") {
				module.exports = result;
			} else {
				module.exports = result.toString();
			}
		})
	});

 	var cache = {};
 	function require(moduleId) {
 		if (cache[moduleId]) {
 			return cache[moduleId].exports;	
		}
 		var module = cache[moduleId] = {
 			id: moduleId,
 			exports: {}
		};
 		__webpack_modules__[moduleId](module, module.exports, require);
 		return module.exports;
	}
	
	// 入口
	(() => {
		const css = require("./src/global.css")
		console.log(css);
	})();
})();
  • 分析打包文件1
// 直接导出字符串给浏览器使用
(() => {
	var __webpack_modules__ = ({
 		"./src/global.css": ((module, exports, require) => {
			 // 直接导出字符串
			module.exports = "body {\r\n\tbackground-color: green;\r\n}";
		})
	});

 	var cache = {};
 	function require(moduleId) {
 		if (cache[moduleId]) {
 			return cache[moduleId].exports;	
		}
 		var module = cache[moduleId] = {
 			id: moduleId,
 			exports: {}
		};
 		__webpack_modules__[moduleId](module, module.exports, require);
 		return module.exports;
	}
	
	// 入口
	(() => {
		const css = require("./src/global.css")
		console.log(css);
	})();
})();
  • 分析打包文件2
// 使用数组存储所有的引用模块,方便后续合并输出
(() => {
	var __webpack_modules__ = ({
 		"./src/global.css": ((module, exports, require) => {
			 // 数组是为了保存多次引用@import,全部模块存储到一个数组中
			var list = [];
			list.push([
				module.id, "body {\r\n\tbackground-color: green;\r\n}"
			]);
			// 一个映射函数,把每一个CSS描述对象转成CSS代码,取每个数组的索引为1的代码
			let cssWithMappingToString  = item => item[1];
			let css =  list.map(cssWithMappingToString).join("");
			module.exports = css;
		})
	});

 	var cache = {};
 	function require(moduleId) {
 		if (cache[moduleId]) {
 			return cache[moduleId].exports;	
		}
 		var module = cache[moduleId] = {
 			id: moduleId,
 			exports: {}
		};
 		__webpack_modules__[moduleId](module, module.exports, require);
 		return module.exports;
	}
	
	// 入口
	(() => {
		const css = require("./src/global.css")
		console.log(css);
	})();
})();
  • 分析打包文件3
// 使用css-loader重写加载一次global.css
(() => {
	var __webpack_modules__ = ({
 		"css-loader.js!./src/global.css": ((module, exports, require) => {
			var api = require("api.js");
			// 一个映射函数,把每一个CSS描述对象转成CSS代码,取每个数组的索引为1的代码
			let cssWithMappingToString  = item => item[1];
			let EXPORT = api(cssWithMappingToString);
			EXPORT.push([
				module.id, "body {\r\n\tbackground-color: green;\r\n}"
			]);
			module.exports = EXPORT ;
		}),
		"api.js": ((module) => {
			module.exports = function(cssWithMappingToString) {
				// 数组是为了保存多次引用@import,全部模块存储到一个数组中
				var list = [];
				// 重写tostring方法
				list.toString = function() {
					return this.map(cssWithMappingToString).join("");
				}
				return list;
			}
		}),
		"./src/global.css": ((module,exports,require)=>{
			var result = require("css-loader.js!./src/global.css");
			module.exports = result.toString();
		  })
	});

 	var cache = {};
 	function require(moduleId) {
 		if (cache[moduleId]) {
 			return cache[moduleId].exports;	
		}
 		var module = cache[moduleId] = {
 			id: moduleId,
 			exports: {}
		};
 		__webpack_modules__[moduleId](module, module.exports, require);
 		return module.exports;
	}
	
	// 入口
	(() => {
		const css = require("./src/global.css")
		console.log(css);
	})();
})();

3 源码

  • 思想:遍历css的ast,找到import和url,然后分别利用各自规则进行处理

  • import:找到@import,先删除,然后放入一个数组中,最后利用剩余的loaders,进行require对应的css模块

  • url:找到url,替换为require(url)就行了

  • loaders/css-loader.js

let loaderUtils = require('loader-utils');
let postcss = require('postcss'); // css转为AST
let Tokenizer = require('css-selector-tokenizer'); // 节点转化为CSS脚本

function loader(inputSource) {
	// 获取配置参数
	let loaderOptions = loaderUtils.getOptions(this) || {};
	const cssPlugin = (options) => {
		return (root) => {
			// 处理@import
			if (loaderOptions.import) {
				// 1.删除所有的@import 2.把导入的CSS文件路径添加到options.imports里
				root.walkAtRules(/^import$/i, rule => {
					// 在CSS脚本里把这@import删除
					rule.remove();
					// 截取css放入import中。类似./global.css
					options.imports.push(rule.params.slice(1, -1));
				});
			}
			// 处理url
			if (loaderOptions.url) {
				// 2.遍历语法树,找到里面所有的url
				// 因为这个正则只能匹配属性
				root.walkDecls(/^background-image/, decl => {
					let values = Tokenizer.parseValues(decl.value);
					values.nodes.forEach(node => {
						node.nodes.forEach(item => {
							if (item.type === 'url') {
								// stringifyRequest可以把任意路径标准化为相对路径
								let url = loaderUtils.stringifyRequest(this, item.url);
								// 标识字符串类型为单引号
								item.stringType = "'";
								item.url = "`+require("+url+")+`";
								// require会给webpack看和分析,webpack一看你引入了一张图片
								// webpack会使用file-loader去加载图片
							}
						});
					});
					let value = Tokenizer.stringifyValues(values);
					decl.value = value;
				});
			}
		}
	}

	// 返回异步回调函数
	let callback = this.async();
	//将会用它来收集所有的@import
	let options = { imports: [] };
	let pipeline = postcss([cssPlugin(options)]);
	// 处理源代码,返回promise
	pipeline.process(inputSource).then(result => {
		let {importLoaders = 0} = loaderOptions; // 获取配置的几个前置loader
		let {loaders, loaderIndex} = this; // 所有的loader数组和当前loader的索引
		let loaderRequest = loaders.slice(
			loaderIndex,
			loaderIndex + importLoaders + 1 // 截取不包含最后一位,所以需要+1
		).map(x => x.request).join('!'); // request是loader绝对路径

		// !css-loader.js的绝对路径!less-loader.js的绝对路径!./global.css
		// stringifyRequest: 可以将绝对路径转化为相对路径的字符串(带引号)
		let importCss = options.imports
			.map(url => `list.push(...require(` + 
				loaderUtils.stringifyRequest(this, `-!${loaderRequest}!${url}`) 
			+ `))`)
			.join('\r\n');

		let script = `
			var list = [];
			list.toString = function(){return this.join('')};
			${importCss}
			list.push(\`${result.css}\`);
			module.exports = list;
		`;
		// 异步返回结果
		callback(null, script);
	});
}

module.exports = loader;
  • webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
	mode:'development',
	devtool:false,
	resolveLoader: {
		modules: [path.resolve(__dirname, 'loaders'), 'node_modules']
	},
	module:{
		rules:[
			{
				test:/\.css$/,
				use:[
					"to-string-loader",
					{
						loader:'css-loader',
						options:{
							url: true, // 是否解析url()
							import: true, // 是否解析@import语法
							esModule: false, // 不包装成ES MODULE,默认是common.js导出
							importLoaders: 0 // 在处理导入的CSS的时候,要经过几个前置loader的处理
						}
					}
				],
				include:path.resolve('src')
			},
			{
				test:/\.(jpg|png|gif)/,
				use:[
					{
						loader:"file-loader",
						options:{
							esModule: false
						}
					}
				]
			}
		]
	},
	plugins:[
		new HtmlWebpackPlugin({template:'./src/index.html'}),
	]
}
  • src目录
// src/index.js
const css = require("./index.css");
console.log(css);
// src/index.css
@import "./global.css";
body {
	color: red;
}
#root {
	background-image: url(./images/kf.jpg);
	background-size: contain;
	width: 100px;
	height: 100px;
}
// src/global.css
@color:orange;
body{
    background-color: @color;
}

PS: 引用顺序为 index.js > index.less > global.css

4 图解

css-loader

七、PostCSS

1 工作机制

  • PostCSS 会将CSS代码解析成包含一系列节点的抽象语法树(AST),树上每个节点都是CSS代码中每个属性的符号化表示
  • AST被传递给后续的插件进行处理,处理完了之后转化为新的CSS代码

PostCSS

2 类型

  • CSS AST 主要有3种父类型
    1. AtRule @xxx的这种类型,如@screen、 @import
    2. Comment 注释
    3. Rule 普通的css规则
  • 子类型
    1. decl 指的是每条具体的css规则
    2. rule 作用于某个选择器上的css规则集合

3 AST节点

  • nodes: CSS规则的节点信息集合
    1. decl: 每条css规则的节点信息
    2. prop: 样式名,如width
    3. value: 样式值,如10px
  • type: 类型
  • source: 包括start和end的位置信息,start和end里都有line和column表示行和列
  • selector: type为rule时的选择器
  • name: type为atRule时@紧接rule名,譬如@import 'xxx.css'中的import
  • params: type为atRule时@紧接rule名后的值,譬如@import 'xxx.css'中的xxx.css
  • text: type为comment时的注释内容

4 操作

  • walk: 遍历所有节点信息,无论是atRule、rule、comment的父类型,还是rule、 decl的子类型
    1. walkAtRules:遍历所有的AtRules
    2. walkComments 遍历所有的Comments
    3. walkDecls 遍历所有的Decls
    4. walkRules 遍历所有的Rules
  • postCss给出了很多操作CSS)的方法
    1. postcss插件如同babel插件一样,有固定的格式
    2. 注册个插件名,并获取插件配置参数opts
    3. 返回值是个函数,这个函数主体是你的处理逻辑,第一个参数是AST的根节点

5 示例

// 将px转化为rem的小插件
var postcss = require("postcss");
const cssPlugin = (options) => {
	// CSS AST语法树的根节点 
	return (root,result) => {
		//遍历所有的@Rule @import
		root.walkAtRules();
		// 遍历decls - 具体的css代码规则
		root.walkDecls((decl) => {
			// console.log(decl);
			//  转化
			if(decl.value.endsWith('px')){
				decl.value = parseFloat(decl.value)/75 + 'rem';
			}
		});
	};
};
let options = {};
// 管道
let pipeline = postcss([cssPlugin(options)]);
// 源代码
let inputSource = `
#root{
	width:750px;
}`;
/**
 * post内部
 * 1 pipeline其实先把CSS源代码转成CSS抽象语法树
 * 2 遍历语法树,让插件进行处理
 */
pipeline.process(inputSource).then((result) => {
	console.log(result.css);
})