Webpack-Loader

TIP

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

一 运行流程

loader-runner

二 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';
}
loader.pitch = function(){
	console.log('post1-pitch');
}
module.exports = loader;
  • post-loader2.js
function loader(source) {
	console.log('post2');
	return source + '//post2';
}
loader.pitch = function(){
	console.log('post2-pitch');
}
module.exports = loader;
  • inline-loader1.js
function loader(source) {
	console.log('inline1');
	return source + '//inline1';
}
loader.pitch = function(){
	console.log('inline1-pitch');
}
module.exports = loader;
  • inline-loader2.js
function loader(source) {
	console.log('inline2');
	return source + '//inline2';
}
loader.pitch = function(){
	console.log('inline2-pitch');
}
module.exports = loader;
  • normal-loader1.js
function loader(source) {
	console.log('normal1');
	return source + '//normal1';
}
loader.pitch = function(){
	console.log('normal1-pitch');
}
module.exports = loader;
  • normal-loader2.js
function loader(source) {
	console.log('normal2');
	return source + '//normal2';
}
loader.pitch = function(){
	console.log('normal2-pitch');
}
module.exports = loader;
  • pre-loader1.js
function loader(source) {
	console.log('pre1');
	return source + '//pre1';
}
loader.pitch = function(){
	console.log('pre1-pitch');
}
module.exports = loader;
  • pre-loader2.js
function loader(source) {
	console.log('pre2');
	return source + '//pre2';
}
loader.pitch = function(){
	console.log('pre2-pitch');
}
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 执行结果

post1-pitch
post2-pitch
inline1-pitch
inline2-pitch
normal1-pitch
normal2-pitch
pre1-pitch
pre2-pitch

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实现

let fs = require('fs');
let readFile = fs.readFile.bind(this);

/**
 * 创建loaders的对象
 */
function createLoaderObject(request){
	let loaderObj = {
		request,
		normal: null, // loader函数本身
		pitch: null, // pitch函数本身
		raw: false, // 是否需要转成字符串,默认是转的
		data: {}, // 每个loader都会有一个自定义的data对象,用来存放一些自定义信息
		pitchExecuted: false, // pitch函数是否已经执行过了
		normalExecuted: false // normal函数是否已经执行过了
	}
	// 加载并赋值
	let normal = require(loaderObj.request);
	loaderObj.normal = normal;
	loaderObj.raw = normal.raw;
	loaderObj.pitch = normal.pitch;
	return loaderObj;
}

/**
 * 执行loader的pitch方法
 * @param {*} processOptions {resourceBuffer}
 * @param {*} loaderContext loader里的this,就是所谓的上下文对象loaderContext
 * @param {*} finalCallback loader全部执行完会执行此回调
 */
function iteratePitchingLoaders(processOptions, loaderContext, finalCallback){
	//如果已经越界,读取最右边的一个loader的右边了
	if (loaderContext.loaderIndex >= loaderContext.loaders.length){
		return processResource(processOptions,loaderContext,finalCallback);
	}

	// 获取当前的loader
	let currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];
	// 已经执行过了,继续遍历
	if (currentLoaderObject.pitchExecuted) {
		loaderContext.loaderIndex++;
		return iteratePitchingLoaders(processOptions, loaderContext, finalCallback);
	}
	// 没有执行过
	let pitchFunction = currentLoaderObject.pitch;
	// 表示pitch函数已经执行过了
	currentLoaderObject.pitchExecuted = true;
	// 如果此loader没有提供pitch方法,跳过继续迭代
	if (!pitchFunction) {
		return iteratePitchingLoaders(processOptions, loaderContext, finalCallback);
	}
	// 如果有pitch方法,则执行
	let pitchArgs = [loaderContext.remainingRequest, loaderContext.previousRequest, loaderContext.data];
	runSyncOrAsync(pitchFunction, loaderContext, pitchArgs, (err, ...values) => {
		//如果有返回值
		if (values.length > 0 && !!values[0]) {
			// 索引减1,回到上一个loader,执行上一个loader的normal方法
			loaderContext.loaderIndex--;
			iterateNormalLoaders(processOptions, loaderContext, values, finalCallback);
		}else{
			iteratePitchingLoaders(processOptions, loaderContext, finalCallback);
		}
	});
}

/**
 * 执行loader的normal方法
 * @param {*} processOptions {resourceBuffer}
 * @param {*} loaderContext loader里的this,就是所谓的上下文对象loaderContext
 * @param {*} args 参数
 * @param {*} finalCallback loader全部执行完会执行此回调
 */
function iterateNormalLoaders(processOptions, loaderContext, args, finalCallback){
	// 如果索引已经小于0了,就表示所有的normal执行完成了
	if (loaderContext.loaderIndex < 0) {
		return finalCallback(null, args);
	}
	let currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];
	if (currentLoaderObject.normalExecuted) {
		loaderContext.loaderIndex--;
		return iterateNormalLoaders(processOptions, loaderContext, args, finalCallback);
	}
	let normalFunction = currentLoaderObject.normal;
	currentLoaderObject.normalExecuted = true; // 表示pitch函数已经执行过了
	// 转参数
	convertArgs(args, currentLoaderObject.raw);
	runSyncOrAsync(normalFunction, loaderContext, args, (err,...values) => {
		if (err) finalCallback(err);
		iterateNormalLoaders(processOptions, loaderContext, values, finalCallback);
	});
}

/**
 * 同步和异步的执行方法
 * 异步使用的两种方式:
 * let callback = this.async();
 * callback(null, source);
 * 
 * this.callback(null, source);
 */
function runSyncOrAsync(fn, context, args, callback) {
	// 每个loader函数的执行同步异步都是独立的,因为是闭包的局部变量来控制
	let isSync = true; // 是否同步,默认是的
	let isDone = false; // 是否fn已经执行完成,默认是false

	// 定义异步方法
	// 当前loader执行完成后会调用这个方法
	const innerCallback = context.callback = function(err, ...values) {
		isDone= true;
		isSync = false;
		callback(err, ...values);
	}
	// 绑定异步方法
	context.async = function(){
		// 把同步标志设置为false,意思就是改为异步
		isSync = false;
		return innerCallback;
	}

	// pitch的返回值可有可无
	let result = fn.apply(context, args);
	// 如果isSync标志是true,意味着是同步
	if (isSync) {
		isDone = true; // 直接完成
		return callback(null, result); // 调用回调
	}
}

function processResource(processOptions, loaderContext, finalCallback){
	loaderContext.loaderIndex = loaderContext.loaderIndex - 1;//索引等最后一个loader的索引
	// 读取文件
	let resource = loaderContext.resource; // c:/src/index.js
	loaderContext.readResource(resource, (err,resourceBuffer) => {
		if (err) finalCallback(err);
		processOptions.resourceBuffer = resourceBuffer; // 放的是资源的原始内容
		iterateNormalLoaders(processOptions, loaderContext, [resourceBuffer], finalCallback);
	});
}

function convertArgs(args,raw){
	if (raw && !Buffer.isBuffer(args[0])) {
		// 想要Buffer,但不是Buffer,转成Buffer
		args[0] = Buffer.from(args[0]);
	} else if(!raw && Buffer.isBuffer(args[0])) {
		// 想要Buffer,但不是Buffer,转成Buffer
		args[0] = args[0].toString('utf8');
	}
}

function runLoaders(options, callback) {
	let resource = options.resource || ''; // 要加载的资源 c:/src/index.js?name=zhufeng#top
	let loaders = options.loaders || []; // loader绝对路径的数组
	let loaderContext = options.context || {}; // loader函数执行时候的上下文对象this
	let readResource = options.readResource || readFile; // 读取硬盘上文件的默认方法
	let loaderObjects = loaders.map(createLoaderObject) // loaders对象映射
	// 给上下文对象赋值
	loaderContext.resource = resource;
	loaderContext.readResource = readResource;
	loaderContext.loaders = loaderObjects; // 存放着所有的loaders对象
	loaderContext.loaderIndex = 0; // 指针,通过修改它来控制当前在执行哪个loader
	loaderContext.callback = null; // 回调函数
	loaderContext.async = null; // 可以把loader的执行从同步改为异步的一个函数
	// 要加载的资源  loader1.js!loader2.js!loader3.js!c:/src/index.js
	Object.defineProperty(loaderContext, 'request', {
		get(){
			return loaderContext.loaders.map(l => l.request).concat(loaderContext.resource).join('!')
		}
	});
	// 剩下的请求
	Object.defineProperty(loaderContext, 'remainingRequest', {
		get(){
			return loaderContext.loaders.slice(loaderContext.loaderIndex + 1).concat(loaderContext.resource).join('!')
		}
	});
	// 当前请求
	Object.defineProperty(loaderContext, 'currentRequest', {
		get(){
			return loaderContext.loaders.slice(loaderContext.loaderIndex).concat(loaderContext.resource).join('!')
		}
	});
	// 前面的请求
	Object.defineProperty(loaderContext, 'previousRequest', {
		get(){
			return loaderContext.loaders.slice(0,loaderContext.loaderIndex).join('!')
		}
	});
	// 当前loader的data
	Object.defineProperty(loaderContext, 'data', {
		get(){
			let loaderObj = loaderContext.loaders[loaderContext.loaderIndex];
			return loaderObj.data;
		}
	});
	let processOptions = {
		resourceBuffer: null
	}
	// 开始执行loader
	iteratePitchingLoaders(processOptions, loaderContext, (err,result)=>{
		callback(err, {
			result,
			resourceBuffer: processOptions.resourceBuffer
		});
	});
}

exports.runLoaders = runLoaders;

3 loader-runnder中request图解

loader-runnder

  • 说明:
# 当前索引index = 2,即在loader3
request = loader1!loader2!loader3!loader4!loader5!index.js
remainingRequest = loader4!loader5!index.js
currentRequest = loader3!loader4!loader5!index.js
previousRequest = loader1!loader2

五 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);
})