Webpack-Loader
TIP
所谓 loader 只是一个导出为函数的 JavaScript 模块。它接收上一个 loader 产生的结果或者资源文件(resource file)作为入参。也可以用多个 loader 函数组成 loader chain。compiler 需要得到最后一个 loader 产生的处理结果。这个处理结果应该是 String 或者 Buffer(被转换为一个 string)
一 运行流程
二 loader-runner简介
- webpack的loader是通过loader-runner串联起来的
- webpack-loader-api
- loader-runnder官方
- loader-runner是一个执行loader链条的的模块
1 类型
四大类型
- post: 后置
- inline: 内联
- normal: 普通
- pre: 前置
执行优先级:pre > normal > inline > post
叠加顺序为:post(后置) + inline(内联) + normal(普通) + pre(前置)
2 特殊符号
符号 | 变量 | 含义 |
---|---|---|
-! | noPreAutoLoaders | 不要前置和普通 loader |
! | noAutoLoaders | 不要普通 loader |
!! | noPrePostAutoLoaders | 不要前置、后置和普通 loader,只要内联 loader |
3 执行顺序
- pitch 从左往右执行
- loader 从下往上,从右往左执行
4 pitch
- 每个loader都有一个pitch方法,但pitch不是必须的
- pitch两个作用:阻断 loader 链 和 存放自身需要的额外数据
- pitch方法的三个参数:
- remainingRequest:后面的loader+资源路径,loadername!的语法
- precedingRequest:资源路径
- 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-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图解
- 说明:
# 当前索引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的三种方式
- 在resolveLoader里配置alias别名
module.exports = {
resolveLoader:{
alias:{
'babel-loader':path.resolve('./loaders/babel-loader.js')
}
},
module:{
rules:[
{
test: /\.js$/,
use: 'babel-loader'
}
]
}
}
- 在resolveLoader里配置modules别名
module.exports = {
resolveLoader:{
modules: [path.resolve('./loaders'), 'node_modules']
},
module:{
rules:[
{
test: /\.js$/,
use: 'babel-loader'
}
]
}
}
- 在配置 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
loaders/file-loader.js
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
loaders/style-loader.js
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
loaders/to-string-loader.js
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
- css-loader
- css-loader可以把@import and url()翻译成import/require(),然后可以解析处理它们
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 图解
七、PostCSS
- PostCSS 是一个用 JavaScript 工具和插件转换 CSS 代码的工具
- PostCSS官网
- PostCSS Api
- Astexplorer CSS可视化
1 工作机制
- PostCSS 会将CSS代码解析成包含一系列节点的抽象语法树(AST),树上每个节点都是CSS代码中每个属性的符号化表示
- AST被传递给后续的插件进行处理,处理完了之后转化为新的CSS代码
2 类型
- CSS AST 主要有3种父类型
- AtRule @xxx的这种类型,如@screen、 @import
- Comment 注释
- Rule 普通的css规则
- 子类型
- decl 指的是每条具体的css规则
- rule 作用于某个选择器上的css规则集合
3 AST节点
- nodes: CSS规则的节点信息集合
- decl: 每条css规则的节点信息
- prop: 样式名,如width
- 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的子类型
- walkAtRules:遍历所有的AtRules
- walkComments 遍历所有的Comments
- walkDecls 遍历所有的Decls
- walkRules 遍历所有的Rules
- postCss给出了很多操作CSS)的方法
- postcss插件如同babel插件一样,有固定的格式
- 注册个插件名,并获取插件配置参数opts
- 返回值是个函数,这个函数主体是你的处理逻辑,第一个参数是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);
})