webpack4 - loader 的执行过程和部分常用loader原理实现

webpack4 专栏收录该内容
2 篇文章 0 订阅

webpack4 - loader 的执行过程和部分常用loader原理实现

1.loader 运行的总体流程

    1.Compiler.js中会将用户配置与默认配置合并,其中就包括了loader部分
    2.webpack就会根据配置创建两个关键的对象 ———— NormalModuleFactory 和 ContextModuleFactory , 他们相当于是两个类工厂,通过其可以创建相应的 NormalModule 和 ContextModule
    3.在工厂创建NormalModule实例之前还要通过loader的resolver来解析loader路径
    4.在NormalModule实例创建之后,则会通过其build方法来进行模块的构建,构建模块的第一步就是使用loader来加载并处理模块内容,而loader-runner这个库就是webpack中loader的运行器
    5.最后,将loader处理完毕的模块内容输出,进入后续的编译流程

开始编译 => webpack默认配置 => 创建NormalModuleFactory => 创建NormalModule[使用resolver解析loader路径] => 编译模块[loader-runner]

2.loader 匹配和用法

loader 是导出为一个函数的 node 模块,该函数在 loader 转换资源的时候调用,给定的函数将调用 loaderApi,并通过 this 上下文访问
访问方式可以是直接给 use 数组一个个对象,loader 指定一个绝对路径,而不是直接用 xxx-loader 提供,也可以是 resolveLoader 配置的 module 的查找目录(文件夹),或者使用 npm link 发布到本地 npm 的模块
还有一种方式就是配置 resolveLoader.alias 对象,配置一一 key-value 对应的关系,key 就是可以拿来使用的 loader 别名
 /** loaders/loader1 */
    // 最后到给loader1
    function loader(inputSource) {
    // console.log('index') // loader3 // loader2
    return inputSource + ' // loader1';
    }

    module.exports = loader;

 /** loaders/loader2 */
    // 第二个给loader2
    function loader(inputSource){
        // console.log('index') // loader3
        return inputSource + ' // loader2';
    }

    module.exports = loader;

 /** loaders/loader3 */
// 先给loader3
    function loader(inputSource) {
    // 文件内容 inputSource
    // return inputSource + ' // loader3';
    // 异步写法如下
    const callback = this.async();
    setTimeout(() => {
        callback(null, inputSource + ' // loader3');
    }, 1000);
    }

    module.exports = loader;


 // webpack.config.js
 module.exports = {
    ...
    resolveLoader: {
        // [制定loader查找的时候]
        // 方式2,直接定义查找模块,node_modules找不到再去loaders目录找
        modules: [path.resolve('node_modules'), path.resolve('loaders')]
        // 方式3 配置别名
        // alias: {
        //   'loader1': path.resolve('./loaders/loader1.js'),
        //   'loader2': path.resolve('./loaders/loader2.js'),
        //   'loader3': path.resolve('./loaders/loader3.js'),
        // }
    },
    module: {
        rules: [
        {
            test: /\.js$/,
            // 方式1
            // use: [
            //   {
            //     loader: path.resolve('loaders/loader1.js')
            //   }
            // ],
            use: ['loader1', 'loader2', 'loader3']
        }
        ]
    }
 }

3.用法准则

3.1 简单 [单一独立原则]
  • loader 应该只做单一任务,这不仅使每个 loader 容易维护,也可以在更多场景链式调用
3.2 链式(Chaining)
  • 利用 loader 可以链式调用的优势,写五个简单的 loader 实现五项任务,而不是一个 loader 实现五项任务
3.3 模块化(Modular)
  • 保证输出模块化,loader 生成的模块与普通模块遵循相同的设计原则。
3.4 无状态(Stateless)

确保 loader 在不同模块转换之间不保持状态,每次运行都应该独立与其他编译模块以及相同模块之间的编译结果

3.5 loader 工具库
  • loader-utils 包,它提供了许多有用的工具,但最常用的一种工具是获取传递给 loader 的选项
  • schema-utils 包,它配合 loader-utils,用于保证 loader 选项,进行与 JSON Schema 结果一致的校验.
3.6 loader 依赖(Loader Dependencies)
  • 如果一个 loader 使用外部资源(例如从文件系统读取),必须声明它,这些信息用于使缓存 loaders 无效,以及在观察模式(watch mode)下重新编译
3.7 模块依赖(Module Dependencies)
  • 根据模块类型,可能会有不同的模式制定依赖关系,例如在 CSS 中,使用@import 和 url(…)语句来声明依赖,这些依赖关系应该由模块系统解析
3.8 绝对路径(Absolute Paths)
  • 不要在模块代码中插入绝对路径,因为当项目根路径变化时,文件绝对路径也会发生变化,loader-utils 中的 stringifyRequest 方法,可以讲绝对路径转化程相对路径。
3.9 同等依赖(Peer Dependencies)
  • 如果你的 loader 简单包裹另外一个包,你应该把这个包作为一个 peerDependency 引入
  • 这种方式允许应用程序开发者在必要情况下,在 package.json 中指定所需的确定版本。

4.loader 的实现

4.1 babel-loader 实现
// loaders/babel-loader.js
const babel = require('@babel/core');
const loaderUtils = require('loader-utils');
const path = require('path');
/** loader只是一个函数 */
module.exports = function loader(inputSource) {
  // 获取配置参数options的数据
  const options = loaderUtils.getOptions(this);
  // 默认配置
  const baseOptions = {
    ...options, // 合并配置
    // presets: ['@babel/preset-env'],
    sourceMaps: true, // 告诉babel我要生成sourceMao
    filename: path.basename(this.resourcePath)
  };
  // 代码 map文件  ast语法树
  const { code, map, ast } = babel.transform(inputSource, baseOptions);
  // 我们可以把source-map ast 都传递给webpack,这样webpack就不需要自己把源代码转语法树,也不需要自己生成source-map
  return this.callback(null, code, map, ast);
};
// webpack.config.js
module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              // webpack loader的选项配置
              presets: ['@babel/preset-env']
            }
          }
        ]
      }
    ]
  }
  // ...
};
4.2 pitch 实现 (上面实现的 babel-loader,babel1-3 是普通函数,不是 pitch 函数)

pitch function 是先执行的,从左往右执行,执行完毕后执行normal function,从右往左回去,不过执行条件如下

  • 比如 a!b!c!module,正常调用顺序应该是 c、b、a,但是真正调用顺序是 a(pitch)、b(pitch)、c(pitch)、c、b、a,如果其中任何一个 pitching loader 返回了值,就相当于在它以及它右边的 loader 已经执行完毕
  • 比如如果 b 返回了字符串’result b’,接下来只有 a 会被系统执行,且 a 的 loader 收到的参数是’result b’
  • loader 根据返回值可以分成两种,一种是返回 s 代码(一个 module 的代码,含有类似 module export 语句)的 loader,还有不能作为最左边 loader 的其他 loader
  • 有时候我们想把两个第一种 loader chain 起来,比如 style-loader!css-loader,问题是 css-loader 的返回值是一串代码,如果按正常方式写 style-loader 的参数就是一串代码字符串
  • 为了解决这种问题,我们需要在 style-loader 里执行 require(css-loader!resources)
/** loaders/loader1 */
// 最后到给loader1
function loader(inputSource) {
  // console.log('index') // loader3 // loader2
  console.log('loader1');
  return inputSource + ' // loader1';
}
// pitch function
loader.pitch = function(remindingRequest, previousRequest, data) {
  console.log('pitch1');
};

module.exports = loader;

/** loaders/loader2 */
// 第二个给loader2
function loader(inputSource) {
  // console.log('index') // loader3
  console.log('loader2');
  return inputSource + ' // loader2';
}
// pitch function
loader.pitch = function(remindingRequest, previousRequest, data) {
  console.log('pitch2');
};

module.exports = loader;

/** loaders/loader3 */
// 先给loader3
function loader(inputSource) {
  // 文件内容 inputSource
  // return inputSource + ' // loader3';
  // 异步写法如下
  const callback = this.async();
  setTimeout(() => {
    console.log('loader3');
    callback(null, inputSource + ' // loader3');
  }, 1000);
}
// pitch function
loader.pitch = function(remindingRequest, previousRequest, data) {
  console.log('pitch3');
  return 'let name = "loader3";'; // let name = "loader3"; // loader2 // loader1
};

module.exports = loader;

// 执行顺序如下
pitch1;
pitch2;
pitch3;
loader2;
loader1;
// 获得结果是:
let name = 'loader3'; // loader2 // loader1
4.3 loader-runner 的实现 [loader-runner就是loader执行的实现,从pitch function到normal function]
// const { runLoaders } = require('loader-runner');
const path = require('path');
const fs = require('fs');
let isSync = true; // 默认同步
// 创建loader对象
function createLoaderObject(loaderPath) {
  const obj = {
    data: {} // 用来在pitch和normal传递数据
  };
  obj.request = loaderPath; // 这个参数是loader的绝对路径
  const loaderFn = require(loaderPath); // 加载这个模块
  obj.normal = loaderFn; // normal  存放普通对象
  obj.pitch = loaderFn.pitch; // 拿到pitch方法
  return obj;
}
/** 迭代loader的pitch方法  */
function iteratePitchingLoaders(loaderContext, callback) {
  // 走完了
  if (loaderContext.loaderIndex >= loaderContext.loaders.length) {
    loaderContext.loaderIndex--; // 走完往回走
    // 先读文件,然后往回走
    return processResource(loaderContext, callback);
  }
  // 没有走完,继续走
  // 拿到当前loader对象
  const currentLoaderObj = loaderContext.loaders[loaderContext.loaderIndex];
  // 拿到pitch方法
  const pitchFn = currentLoaderObj.pitch;
  // 没有pitch,下一个
  if (!pitchFn) {
    loaderContext.loaderIndex++; // 下一个pitch
    return iteratePitchingLoaders(loaderContext, callback); // 继续调用
  }
  // loader1!loader2!loader3!hello.js
  // remindingRequest!loader3!hello.js remindingRequest除了自己之后的
  // previousRequest === loader1 前面一个,就是前一个执行的pitch相关的
  const result = pitchFn.apply(loaderContext, [
    // 执行pitch function,获得结果,可能没有结果
    loaderContext.remindingRequest,
    loaderContext.previousRequest,
    loaderContext.data
  ]);
  // 有值,停止走下一个pitch,把值给上一个pitch对应的的normal loader方法
  if (result) {
    loaderContext.loaderIndex--; // 返回上一个,然后调用normal function
    iterateNormalLoaders(loaderContext, result, callback); // 迭代正常的loader
  } else {
    // 没值,继续走
    loaderContext.loaderIndex++; // 下一个
    return iteratePitchingLoaders(loaderContext, callback);
  }
}
// 读取文件
function processResource(loaderContext, callback) {
  // 加载模块代码,获取内容 [默认是buffer]
  let result = loaderContext.readResource(loaderContext.resource);
  // 默认的raw,不是true的话,就取反,改成字符串
  if (!loaderContext.loaders[loaderContext.loaderIndex].normal.raw) {
    result = result.toString('utf-8');
  }
  // 把结果传递下去
  iterateNormalLoaders(loaderContext, result, callback);
}
// 执行normal 方法 就是普通的loader方法
function iterateNormalLoaders(loaderContext, result, callback) {
  // 越界了,到头了
  if (loaderContext.loaderIndex < 0) {
    return callback(null, result);
  }
  const currentLoaderObj = loaderContext.loaders[loaderContext.loaderIndex];
  // 拿到normal方法
  const normalFn = currentLoaderObj.normal;
  // 执行,获得返回结果给下一个
  result = normalFn.apply(loaderContext, [result]);
  // 如果是同步就继续走,异步就不执行下面的,让asyncCallback方法执行
  if (isSync) {
    // 执行后,执行上一个
    loaderContext.loaderIndex--;
    iterateNormalLoaders(loaderContext, result, callback);
  } else {
    // 执行完毕后又置成同步
    isSync = true;
  }
}
// 定义属性的
function defineProperty(loaderContext) {
  // 定义属性 request 完整资源的路径数组
  Object.defineProperty(loaderContext, 'request', {
    get() {
      //  loader1!loader2!loader3!hello.js
      return loaderContext.loaders
        .map(loader => loader.request)
        .concat([loaderContext.resource])
        .join('!');
    }
  });
  // 定义属性 remindingRequest 获取当前和前面的除外,到最后
  Object.defineProperty(loaderContext, 'remindingRequest', {
    get() {
      //  loader3!hello.js
      return loaderContext.loaders
        .slice(loaderContext.loaderIndex + 1)
        .map(loader => loader.request)
        .concat([loaderContext.resource])
        .join('!');
    }
  });
  // 定义属性 previousRequest 获取当前除外前面所有的
  Object.defineProperty(loaderContext, 'previousRequest', {
    get() {
      //  loader1
      return loaderContext.loaders
        .slice(0, loaderContext.loaderIndex)
        .map(loader => loader.request)
        .join('!');
    }
  });
  // 定义属性 currentRequest 获取当前的
  Object.defineProperty(loaderContext, 'currentRequest', {
    get() {
      //  loader2!loader3!hello.js
      return loaderContext.loaders
        .slice(loaderContext.loaderIndex)
        .map(loader => loader.request)
        .concat([loaderContext.resource])
        .join('!');
    }
  });
  Object.defineProperty(loaderContext, 'data', {
    get() {
      //  data属性,默认是{}
      return loaderContext.loaders[loaderContext.loaderIndex].data;
    }
  });
}

function runLoaders(options, callback) {
  try {
    const loaderContext = options.context || {}; // loader的上下文环境
    loaderContext.resource = options.resource; // 加载的资源
    // 使用的loader,map成对象后赋值给context上
    loaderContext.loaders = options.loaders.map(createLoaderObject);
    loaderContext.readResource = options.readResource; // fs模块放入
    loaderContext.loaderIndex = 0; // 索引
    defineProperty(loaderContext); // 执行给context增加属性
    // 给loaderContext加方法
    loaderContext.async = function() {
      isSync = false; // 转化成异步
      return asyncCallback; // 返回一个异步回调
    };
    // 返回异步的回调函数
    function asyncCallback(error, result) {
      if (error) {
        // 有错误
        return callback(error);
      }
      loaderContext.loaderIndex--;
      iterateNormalLoaders(loaderContext, result, callback);
    }
    // 开始迭代,从左往右,然后从右往左
    // 迭代loader的pitch方法
    iteratePitchingLoaders(loaderContext, callback);
  } catch (error) {
    // 出错了
    callback(error);
  }
}

runLoaders(
  {
    // 要加载的资源
    resource: path.resolve(__dirname, './src/hello.js'),
    // 我们要用这三个loader转化hello.js
    loaders: [
      path.resolve('loaders', 'loader1.js'),
      path.resolve('loaders', 'loader2.js'),
      path.resolve('loaders', 'loader3.js')
    ],
    context: {
      minimize: true
    },
    readResource: fs.readFileSync.bind(fs)
  },
  function(err, result) {
    if (err) {
      console.log(err);
    }
    console.log(result);
  }
);
4.4 loader 的类型 (rules 里面的 enforce 属性)
  • loader 的叠加顺序 = post 后置+inline 内联+normal 普通+pre 前置
  -! noPreAutoLoaders  不要前置loader,普通loader,只剩下内联loader,后置loader
  ! noAutoLoaders 不要普通的loader,其他都要
  !! noPrePostAutoLoaders 不要前置,后置,普通loader,只要内联loader // !!css-loader!./style.css
4.5 less-loader 的实现
const less = require('less');
// const loaderUtils = require('loader-utils');

module.exports = function loader(source) {
  const callback = this.async();
  // less转化程css
  less.render(
    source,
    {
      filename: this.resource
    },
    (err, output) => {
      // 编译然后获取css代码返回
      callback(err, output.css);
    }
  );
};
4.6 style-loader 的实现
const loaderUtils = require('loader-utils');

function loader(source) {
  // // source是css代码
  // const script = `
  //       const style = document.createElement('style'); // 创建style标签
  //       style.innerHTML = ${JSON.stringify(source)};
  //       document.head.appendChild(style); // 然后放入head中
  //   `;
  return script; // 然后把脚本返回 ,这里因为pitch方法的问题,用不到
}

loader.pitch = function(remainingRequest, previousRequest, data) {
  // source是css代码
  // 下面的require直接跳过自己,然后执行 css-loader!!xxx.css
  // 如果不加!! 死循环
  // !! noPrePostAutoLoaders 不要前置,后置,普通loader,只要内联loader
  /*
    "!!../loaders/css-loader.js!../loaders/less-loader.js!./index.less"
    "!!../loaders/css-loader.js!./style.css"
    "!!../loaders/css-loader.js!./base.css"
  */
  const script = `
    const style = document.createElement('style'); // 创建style标签
    style.innerHTML = require(${loaderUtils.stringifyRequest(
      this,
      '!!' + remainingRequest
    )});
    document.head.appendChild(style); // 然后放入head中
`;
  return script; // 然后把脚本返回
};
// pitch如果有两个最左侧的loader要联合使用
module.exports = loader;
4.7 css-loader 的实现
const postcss = require('postcss');
const Tokenizer = require('css-selector-tokenizer');
const loaderUtils = require('loader-utils');
// 插件,用来提取url
function createPlugin(options) {
  return function(css) {
    const { importItems, urlItems } = options;
    // 捕获导入,如果多个就执行多次
    css.walkAtRules(/^import$/, function(rule) {
      // 拿到每个导入
      const values = Tokenizer.parseValues(rule.params);
      // console.log(JSON.stringify(values));
      // {"type":"values","nodes":[{"type":"value","nodes":[{"type":"string","value":"./base.css","stringType":"'"}]}]}
      // 找到url
      const url = values.nodes[0].nodes[0]; // 第一层的第一个的第一个
      importItems.push(url.value);
    });
    // 遍历规则,拿到图片地址
    css.walkDecls(decl => {
      // 把value 就是 值 7.5px solid red
      // 通过Tokenizer.parseValues,把值变成了树结构
      const values = Tokenizer.parseValues(decl.value);
      values.nodes.forEach(value => {
        value.nodes.forEach(item => {
          /*
            { type: 'url', stringType: "'", url: './bg.jpg', after: ' ' }
            { type: 'item', name: 'center', after: ' ' }
            { type: 'item', name: 'no-repeat' }
          */
          if (item.type === 'url') {
            const url = item.url;
            item.url = `_CSS_URL_${urlItems.length}_`;
            urlItems.push(url); // ['./bg.jpg']
          }
        });
      });
      decl.value = Tokenizer.stringifyValues(values); // 转回字符串
    });
    return css;
  };
}

// css-loader是用来处理,解析@import "base.css"; url('./assets/logo.jpg')
module.exports = function loader(source) {
  const callback = this.async();
  // 开始处理
  const options = {
    importItems: [],
    urlItems: []
  };
  // 插件转化,然后把url路径都转化成require('./bg.jpg'); // ...
  const pipeline = postcss([createPlugin(options)]);
  // 1rem 75px
  pipeline
    //   .process("background: url('./bg.jpg') center no-repeat;")
    .process(source)
    .then(result => {
      // 拿到导入路径,拼接
      const importCss = options.importItems
        .map(imp => {
          // stringifyRequest 可以把绝对路径转化成相对路径
          return `require(${loaderUtils.stringifyRequest(this, imp)})`; // 拼接
        })
        .join('\n'); // 拿到一个个import
      let cssString = JSON.stringify(result.css); // 包裹后就是"xxx" 双引号
      cssString = cssString.replace(/@import\s+?["'][^'"]+?["'];/g, '');
      cssString = cssString.replace(/_CSS_URL_(\d+?)_/g, function(
        matched,
        group1
      ) {
        // 索引拿到,然后拿到这个,替换掉原来的_CSS_URL_0_哪些
        const imgURL = options.urlItems[+group1];
        // console.log('图片路径', imgURL);
        // "background: url('"+require('./bg.jpg')+"') center no-repeat;"
        return `"+require('${imgURL}').default+"`;
      }); // url('_CSS_URL_1_')
      // console.log(JSON.stringify(options));
      // console.log(result.css);
      callback(
        null,
        `
        ${importCss}
        module.exports = ${cssString}
      `
      );
    });
};
4.8 file-loader 的实现
const { getOptions, interpolateName } = require('loader-utils');
// interpolateName方法 重新生成文件名

// 读取源图片文件内容,并且重命名写入到新的输出目录下
function loader(inputSource) {
  const options = getOptions(this) || {}; // 获取参数
  const filename = options.filename || '[hash].[ext]'; // 获取文件名
  // 获取的图片文件名  [name].[hash].[ext]
  const outputFilename = interpolateName(this, filename, {
    content: inputSource
  });
  // 图片文件名 输出的内容inputSource,输出到outputFilename中
  this.emitFile(outputFilename, inputSource);

  return `module.exports = ${JSON.stringify(outputFilename)}`;
}
// 原生的意思,默认情况下loader得到的内容是字符串,如果你想得到二进制文件,需要把raw = true
loader.raw = true;

module.exports = loader;
4.9 url-loader 的实现
const { getOptions } = require('loader-utils');
const fileLoader = require('file-loader');
const mime = require('mime');
/** 如果文件大小小于limit,就不再生成新的文件,而是返回base64 */
function loader(inputSource) {
  // 获取参数
  const options = getOptions(this) || {};
  const filename = options.filename || '[hash].[ext]'; // 获取文件名
  const limit = options.limit || 1024 * 100; // 100kb以上提取出去
  // 小于,内嵌
  if (inputSource.length < limit) {
    // 获取此图片的mime类型
    const contentType = mime.getType(this.resourcePath);
    let base64 = `data:${contentType};base64,${inputSource.toString('base64')}`;
    return `module.exports = ${JSON.stringify(base64)}`; // 直接返回base64的代码
  }
  // 指定this
  return fileLoader.call(this, inputSource);
}
// 原生的意思,默认情况下loader得到的内容是字符串,如果你想得到二进制文件,需要把raw = true
loader.raw = true;

module.exports = loader;
4.10 sprite-loader 的实现
  • 找出哪些图片要加入雪碧图,所以要加个标识
  • 合并出来一个雪碧图,还要计算出它们的大小,位置
  • 改变 css 文件,把旧的路径改成新的路径,另外要添加 CSS 规则 background-position
const postcss = require('postcss');
const path = require('path');
const loaderUtils = require('loader-utils');
const spritesmith = require('spritesmith'); // 多张小图转化成大图
const Tokenizer = require('css-selector-tokenizer');
// const fs = require('fs');

function createPlugin(options, that) {
  return function(css) {
    // 捕获导入,如果多个就执行多次
    css.walkAtRules(/^import$/, function(rule) {
      // 拿到每个导入
      const values = Tokenizer.parseValues(rule.params);
      // console.log(JSON.stringify(values));
      // {"type":"values","nodes":[{"type":"value","nodes":[{"type":"string","value":"./base.css","stringType":"'"}]}]}
      // 找到url
      const url = values.nodes[0].nodes[0]; // 第一层的第一个的第一个
      options.importItems.push(url.value);
    });
    // 遍历每个css属性
    css.walkDecls(decl => {
      // 把value转换成树
      const values = Tokenizer.parseValues(decl.value);

      values.nodes.forEach(value => {
        value.nodes.forEach(item => {
          // 拿到每项值
          // 判断是不是url,并且以 ?sprite 结尾
          if (item.type === 'url' && item.url.endsWith('?sprite')) {
            // 这样需要变成雪碧图
            // 拼接成图片的绝对路径  that.context表示被加载的资源的目录
            const url = path.resolve(that.context, item.url);
            // 图片url都换成雪碧图的路径
            item.url = options.spriteFilename;
            // 按理,我要在当前规则下添加一条background-position,这条规则
            // 但是现在添加不了,因为我还不知道要怎么添加,所以先保存起来
            options.rules.push({
              url, // 原本图片的绝对路径,未来拿来合并雪碧图用
              rule: decl.parent // 当前的规则 parent就是一个css选择器 .one .two .three
            });
          } else if (item.type === 'url') {
            const url = item.url;
            item.url = `_CSS_URL_${options.urlItems.length}_`;
            options.urlItems.push(url); // ['./bg.jpg']
          }
        });
      });
      // 直接把url地址改成雪碧图的名字
      decl.value = Tokenizer.stringifyValues(values);
    });

    // 映射,添加规则,占位
    options.rules
      .map(item => item.rule)
      .forEach((rule, index) => {
        rule.append(
          // 加一项属性,就是规则
          postcss.decl({
            prop: 'background-position',
            value: `_BACKGROUND_POSITION_${index}_`
          })
        );
      });
  };
}
/* 合成雪碧图的loader */
function loader(inputSource) {
  const options = {
    spriteFilename: 'sprite.jpg', // 雪碧图的名字
    rules: [], // 存放规则
    urlItems: [], // 不拿来切成雪碧图的图片url
    importItems: [] // 存放导入的资源
  };
  const callback = this.async(); // 获取回调函数
  // 拿到导入路径,拼接
  const importCss = options.importItems
    .map(imp => {
      // stringifyRequest 可以把绝对路径转化成相对路径
      return `require(${loaderUtils.stringifyRequest(this, imp)})`; // 拼接
    })
    .join('\n'); // 拿到一个个import
  // 解析成树, 创建,通过插件
  const pipeline = postcss([createPlugin(options, this)]);
  // 解析输入的内容
  pipeline.process(inputSource, { from: undefined }).then(resCss => {
    // console.log(resCss.css);
    const sprites = options.rules.map(item =>
      item.url.slice(0, item.url.lastIndexOf('?'))
    );
    // 让人家自己生成
    let cssStr = JSON.stringify(resCss.css);
    // 对@import进行删除,替换成require了
    cssStr = cssStr.replace(/@import\s+?["'][^'"]+?["'];/g, '');
    // 对不是雪碧图的也进行替换
    cssStr = cssStr.replace(/_CSS_URL_(\d+?)_/g, function(matched, group) {
      // 索引拿到,然后拿到这个,替换掉原来的_CSS_URL_0_哪些
      const imgURL = options.urlItems[+group];
      // console.log('图片路径', imgURL);
      // "background: url('"+require('./bg.jpg')+"') center no-repeat;"
      return `"+require('${imgURL}').default+"`;
    });
    // 合并雪碧图
    spritesmith.run({ src: sprites }, (err, result) => {
      if (err) {
        callback(null, `module.exports = ${cssStr};`);
        return;
      }
      // 替换占位
      const coordinates = result.coordinates;
      Object.keys(coordinates).forEach((key, index) => {
        const position = coordinates[key];
        cssStr = cssStr.replace(
          `_BACKGROUND_POSITION_${index}_`,
          ` -${position.x}px -${position.y}px ` // 替换成对应的变量xy
        );
      });
      // 写入图片
      //   fs.writeFileSync('./test-sprite.jpg', result.image, 'utf-8');
      this.emitFile(options.spriteFilename, result.image);
      // 加入规则 background-position: xxx;

      // 返回结果
      callback(
        null,
        `
        ${importCss}
        module.exports = ${cssStr};`
      );
    });
  });
}
// 原生的意思,默认情况下loader得到的内容是字符串,如果你想得到二进制文件,需要把raw = true
loader.raw = true;

module.exports = loader;
  • 0
    点赞
  • 0
    评论
  • 2
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值