This page was saved using WebZIP 7.0.3.1030 offline browser on 12/02/19 14:55:01.
Address: http://www.zhufengpeixun.cn/ahead/html/26.webpack-6-loader.html
Title: 珠峰架构师成长计划  •  Size: 127249  •  Last Modified: Sun, 01 Dec 2019 11:37:00 GMT

1.loader运行的总体流程 #

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

loader

2. loader配置 #

loader是导出为一个函数的node模块。该函数在loader转换资源的时候调用。给定的函数将调用loader API,并通过this上下文访问。

2.1 匹配(test)单个 loader #

匹配(test)单个 loader,你可以简单通过在 rule 对象设置 path.resolve 指向这个本地文件

{
  test: /\.js$/
  use: [
    {
      loader: path.resolve('path/to/loader.js'),
      options: {/* ... */}
    }
  ]
}

2.2 匹配(test)多个 loaders #

,你可以使用 resolveLoader.modules 配置,webpack 将会从这些目录中搜索这些 loaders。

resolveLoader: {
   modules: [path.resolve('node_modules'), path.resolve(__dirname, 'src', 'loaders')]
},

2.1 4 alias #

resolveLoader: {
        alias: {
            "babel-loader": resolve('./loaders/babel-loader.js'),
            "css-loader": resolve('./loaders/css-loader.js'),
            "style-loader": resolve('./loaders/style-loader.js'),
            "file-loader": resolve('./loaders/file-loader.js'),
            "url-loader": resolve('./loaders/url-loader.js')
        }
    },

3. loader用法 #

3.1 单个loader用法 #

3.2 多个loader #

当链式调用多个 loader 的时候,请记住它们会以相反的顺序执行。取决于数组写法格式,从右向左或者从下向上执行。

4 用法准则 #

4.1 简单 #

4.2 链式(Chaining) #

4.3 模块化(Modular) #

保证输出模块化。loader 生成的模块与普通模块遵循相同的设计原则。

4.4 无状态(Stateless) #

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

4.5 loader 工具库(Loader Utilities) #

4.6 loader 依赖(Loader Dependencies) #

如果一个 loader 使用外部资源(例如,从文件系统读取),必须声明它。这些信息用于使缓存 loaders 无效,以及在观察模式(watch mode)下重编译。

4.7 模块依赖(Module Dependencies) #

根据模块类型,可能会有不同的模式指定依赖关系。例如在 CSS 中,使用 @import 和 url(...) 语句来声明依赖。这些依赖关系应该由模块系统解析。

4.8 绝对路径(Absolute Paths) #

不要在模块代码中插入绝对路径,因为当项目根路径变化时,文件绝对路径也会变化。loader-utils 中的 stringifyRequest 方法,可以将绝对路径转化为相对路径。

4.9 同等依赖(Peer Dependencies) #

5. API #

5.1 缓存结果 #

webpack充分地利用缓存来提高编译效率

 this.cacheable();

5..2 异步 #

当一个 Loader 无依赖,可异步的时候我想都应该让它不再阻塞地去异步

// 让 Loader 缓存
module.exports = function(source) {
    var callback = this.async();
    // 做异步的事
    doSomeAsyncOperation(content, function(err, result) {
        if(err) return callback(err);
        callback(null, result);
    });
};

5.3 raw loader #

默认的情况源文件是以 UTF-8 字符串的形式传入给 Loader,设置module.exports.raw = true可使用 buffer 的形式进行处理

module.exports.raw = true;

5.4 获得 Loader 的 options #

const loaderUtils = require('loader-utils');
module.exports = function(source) {
  // 获取到用户给当前 Loader 传入的 options
  const options = loaderUtils.getOptions(this);
  return source;
};

5.5 返回其它结果 #

Loader有些场景下还需要返回除了内容之外的东西。

module.exports = function(source) {
  // 通过 this.callback 告诉 Webpack 返回的结果
  this.callback(null, source, sourceMaps);
  // 当你使用 this.callback 返回内容时,该 Loader 必须返回 undefined,
  // 以让 Webpack 知道该 Loader 返回的结果在 this.callback 中,而不是 return 中 
  return;
};

完整格式

this.callback(
    // 当无法转换原内容时,给 Webpack 返回一个 Error
    err: Error | null,
    // 原内容转换后的内容
    content: string | Buffer,
    // 用于把转换后的内容得出原内容的 Source Map,方便调试
    sourceMap?: SourceMap,
    // 如果本次转换为原内容生成了 AST 语法树,可以把这个 AST 返回,
    // 以方便之后需要 AST 的 Loader 复用该 AST,以避免重复生成 AST,提升性能
    abstractSyntaxTree?: AST
);

5.6 同步与异步 #

Loader 有同步和异步之分,上面介绍的 Loader 都是同步的 Loader,因为它们的转换流程都是同步的,转换完成后再返回结果。 但在有些场景下转换的步骤只能是异步完成的,例如你需要通过网络请求才能得出结果,如果采用同步的方式网络请求就会阻塞整个构建,导致构建非常缓慢。

module.exports = function(source) {
    // 告诉 Webpack 本次转换是异步的,Loader 会在 callback 中回调结果
    var callback = this.async();
    someAsyncOperation(source, function(err, result, sourceMaps, ast) {
        // 通过 callback 返回异步执行后的结果
        callback(err, result, sourceMaps, ast);
    });
};

5.7 异步处理 #

this.callback(
  err:Error|null,
  content:string|Buffer,
  sourceMap?:SourceMap,
  meta?:any
);

1.4.7 处理二进制数据 #

在默认的情况下,Webpack 传给 Loader 的原内容都是 UTF-8 格式编码的字符串。 但有些场景下 Loader 不是处理文本文件,而是处理二进制文件,例如 file-loader,就需要 Webpack 给 Loader 传入二进制格式的数据。 为此,你需要这样编写 Loader:

module.exports = function(source) {
    // 在 exports.raw === true 时,Webpack 传给 Loader 的 source 是 Buffer 类型的
    source instanceof Buffer === true;
    // Loader 返回的类型也可以是 Buffer 类型的
    // 在 exports.raw !== true 时,Loader 也可以返回 Buffer 类型的结果
    return source;
};
// 通过 exports.raw 属性告诉 Webpack 该 Loader 是否需要二进制数据 
module.exports.raw = true;

5.8 缓存 #

在有些情况下,有些转换操作需要大量计算非常耗时,如果每次构建都重新执行重复的转换操作,构建将会变得非常缓慢。 为此,Webpack 会默认缓存所有 Loader 的处理结果,也就是说在需要被处理的文件或者其依赖的文件没有发生变化时, 是不会重新调用对应的 Loader 去执行转换操作的。

module.exports = function(source) {
  // 关闭该 Loader 的缓存功能
  this.cacheable(false);
  return source;
};

5.9 其它 Loader API #

方法名 含义
this.context 当前处理文件的所在目录,假如当前 Loader 处理的文件是 /src/main.js,则 this.context 就等于 /src
this.resource 当前处理文件的完整请求路径,包括 querystring,例如 /src/main.js?name=1。
this.resourcePath 当前处理文件的路径,例如 /src/main.js
this.resourceQuery 当前处理文件的 querystring
this.target 等于 Webpack 配置中的 Target
this.loadModule 但 Loader 在处理一个文件时,如果依赖其它文件的处理结果才能得出当前文件的结果时,就可以通过 this.loadModule(request: string, callback: function(err, source, sourceMap, module)) 去获得 request 对应文件的处理结果
this.resolve 像 require 语句一样获得指定文件的完整路径,使用方法为 resolve(context: string, request: string, callback: function(err, result: string))
this.addDependency 给当前处理文件添加其依赖的文件,以便再其依赖的文件发生变化时,会重新调用 Loader 处理该文件。使用方法为 addDependency(file: string)
this.addContextDependency 和 addDependency 类似,但 addContextDependency 是把整个目录加入到当前正在处理文件的依赖中。使用方法为 addContextDependency(directory: string)
this.clearDependencies 清除当前正在处理文件的所有依赖,使用方法为 clearDependencies()
this.emitFile 输出一个文件,使用方法为 emitFile(name: string, content: Buffer/string, sourceMap: {...})
loader-utils.stringifyRequest Turns a request into a string that can be used inside require() or import while avoiding absolute paths. Use it instead of JSON.stringify(...) if you're generating code inside a loader 把一个请求字符串转成一个字符串,以便能在require或者import中使用以避免绝对路径。如果你在一个loder中生成代码的话请使用这个而不要用JSON.stringify()
loader-utils.interpolateName Interpolates a filename template using multiple placeholders and/or a regular expression. The template and regular expression are set as query params called name and regExp on the current loader's context. 使用多个占位符或一个正则表达式转换一个文件名的模块。这个模板和正则表达式被设置为查询参数,在当前loader的上下文中被称为name或者regExp

6.loader实战 #

6.1 babel-loader #

属性
this.request /loaders/babel-loader.js!/src/index.js'
this.userRequest /src/index.js
this.rawRequest ./src/index.js
this.resourcePath /src/index.js
$ cnpm i @babel/preset-env @babel/core -D
const babel = require("@babel/core");
function loader(source,inputSourceMap) {
    //C:\webpack-analysis2\loaders\babel-loader.js!C:\webpack-analysis2\src\index.js
    console.log(this.request);
    const options = {
        presets: ['@babel/preset-env'],
        inputSourceMap:inputSourceMap,
        sourceMaps: true,//ourceMaps: true 是告诉 babel 要生成 sourcemap
        filename:this.request.split('!')[1].split('/').pop()
    }
    //在webpack.config.js中 增加devtool: 'eval-source-map'
    let {code,map,ast}=babel.transform(source,options);
    return this.callback(null,code,map,ast);
}

module.exports = loader;
resolveLoader: {
    alias: {//可以配置别名
      "babel-loader": resolve('./build/babel-loader.js')
    },//也可以配置loaders加载目录
    modules: [path.resolve('./loaders'), 'node_modules']
},
{
    test: /\.js$/,
    use:['babel-loader']
}

loaders\banner-loader.js

const loaderUtils = require('loader-utils');
const validateOptions = require('schema-utils');
const fs = require('fs');
function loader(source) {
    //把loader改为异步,任务完成后需要手工执行callback
    let cb = this.async();
    //启用loader缓存
    this.cacheable && this.cacheable();
    //用来验证options的合法性
    let schema = { 
        type: 'object',
        properties: {
            filename: {
                type: 'string'
            },
            text: {
                type: 'string'
            }
        }
    }
    //通过工具方法获取options
    let options = loaderUtils.getOptions(this);
    //用来验证options的合法性
    validateOptions(schema, options);
    let {filename } = options;
    fs.readFile(filename, 'utf8', (err, text) => {
        cb(err, text + source);
    });
}

module.exports = loader;

6.2.2 banner.js #

loaders\banner.js

/**copyright: zhufengjiagu*/

6.2.3 webpack.config.js #

webpack.config.js

{
test: /\.js$/,
use:[{
        loader:'banner-loader',
        options:{filename:path.resolve(__dirname,'loaders/banner.js')}
    },'babel-loader']
}

6.3 pitch #

pitch与loader本身方法的执行顺序图

|- a-loader `pitch`
  |- b-loader `pitch`
    |- c-loader `pitch`
      |- requested module is picked up as a dependency
    |- c-loader normal execution
  |- b-loader normal execution
|- a-loader normal execution

6.3.1 loaders\loader1.js #

loaders\loader1.js

function loader(source) {
    console.log('loader1',this.data);
    return source+"//loader1";
}
loader.pitch = function (remainingRequest,previousRequest,data) {
    data.name = 'pitch1';
    console.log('pitch1');
}
module.exports = loader;

6.3.2 loaders\loader2.js #

loaders\loader2.js

function loader(source) {
    console.log('loader2');
    return source+"//loader2";
}
loader.pitch = function (remainingRequest,previousRequest,data) {
    console.log('remainingRequest=',remainingRequest);
    console.log('previousRequest=',previousRequest);
    console.log('pitch2');
    //return 'console.log("pitch2")';
}
module.exports = loader;

6.3.3 loaders\loader3.js #

loaders\loader3.js

function loader(source) {
    console.log('loader3');
    return source+"//loader3";
}
loader.pitch = function () {
    console.log('pitch3');
}
module.exports = loader;

6.3.4 webpack.config.js #

 {
    test: /\.js$/,
    use: ['loader1', 'loader2', 'loader3']
 }

6.3.5 run-loader #

let readFile = require("fs");
let path = require("path");
function createLoaderObject(loader) {
  let obj = { data: {} };
  obj.request = loader;
  obj.normal = require(loader);
  obj.pitch = obj.normal.pitch;
  return obj;
}
function runLoaders(options, callback) {
  let loaderContext = {};
  let resource = options.resource;
  let loaders = options.loaders;
  loaders = loaders.map(createLoaderObject);
  loaderContext.loaderIndex = 0;
  loaderContext.readResource = readFile;
  loaderContext.resource = resource;
  loaderContext.loaders = loaders;
  let isSync = true;
  var innerCallback = (loaderContext.callback = function(err, args) {
    loaderContext.loaderIndex--;
    iterateNormalLoaders(loaderContext, args, callback);
  });
  loaderContext.async = function async() {
    isSync = false;
    return innerCallback;
  };
  Object.defineProperty(loaderContext, "request", {
    get: function() {
      return loaderContext.loaders
        .map(function(o) {
          return o.request;
        })
        .concat(loaderContext.resource)
        .join("!");
    }
  });
  Object.defineProperty(loaderContext, "remainingRequest", {
    get: function() {
      return loaderContext.loaders
        .slice(loaderContext.loaderIndex + 1)
        .map(function(o) {
          return o.request;
        })
        .concat(loaderContext.resource || "")
        .join("!");
    }
  });
  Object.defineProperty(loaderContext, "currentRequest", {
    enumerable: true,
    get: function() {
      return loaderContext.loaders
        .slice(loaderContext.loaderIndex)
        .map(function(o) {
          return o.request;
        })
        .concat(loaderContext.resource || "")
        .join("!");
    }
  });
  Object.defineProperty(loaderContext, "previousRequest", {
    get: function() {
      return loaderContext.loaders
        .slice(0, loaderContext.loaderIndex)
        .map(function(o) {
          return o.request;
        })
        .join("!");
    }
  });
  Object.defineProperty(loaderContext, "data", {
    get: function() {
      return loaderContext.loaders[loaderContext.loaderIndex].data;
    }
  });
  iteratePitchingLoaders(loaderContext, callback);
  function iteratePitchingLoaders(loaderContext, callback) {
    if (loaderContext.loaderIndex >= loaderContext.loaders.length) {
      loaderContext.loaderIndex--;
      return processResource(loaderContext, callback);
    }

    let currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];
    let fn = currentLoaderObject.pitch;
    if (!fn) return iteratePitchingLoaders(options, loaderContext, callback);

    let args = fn.apply(loaderContext, [
      loaderContext.remainingRequest,
      loaderContext.previousRequest,
      currentLoaderObject.data
    ]);
    if (args) {
      loaderContext.loaderIndex--;
      return iterateNormalLoaders(loaderContext, args, callback);
    } else {
      loaderContext.loaderIndex++;
      iteratePitchingLoaders(loaderContext, callback);
    }
    function processResource(loaderContext, callback) {
      let buffer = loaderContext.readResource.readFileSync(
        loaderContext.resource,
        "utf8"
      );
      iterateNormalLoaders(loaderContext, buffer, callback);
    }
  }
  function iterateNormalLoaders(loaderContext, args, callback) {
    if (loaderContext.loaderIndex < 0) return callback(null, args);

    var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];
    var fn = currentLoaderObject.normal;
    if (!fn) {
      loaderContext.loaderIndex--;
      return iterateNormalLoaders(loaderContext, args, callback);
    }
    args = fn.apply(loaderContext, [args]);
    if (isSync) {
      loaderContext.loaderIndex--;
      iterateNormalLoaders(loaderContext, args, callback);
    }
  }
}

let entry = "./src/world.js";

let options = {
  resource: path.join(__dirname, entry),
  loaders: [
    path.join(__dirname, "loaders/loader1.js"),
    path.join(__dirname, "loaders/loader2.js"),
    path.join(__dirname, "loaders/loader3.js")
  ]
};

runLoaders(options, (err, result) => {
  console.log(result);
});

6.4 css #

6.4.1 loader类型 #

let result = [useLoadersPost,useLoaders,useLoadersPre];
loaders = results[0].concat(loaders, results[1], results[2]);
useLoadersPost+inlineLoader+useLoaders(normal loader)+useLoadersPre

Configuration

符号 变量 含义
-! noPreAutoLoaders 不要前置和普通loader Prefixing with -! will disable all configured preLoaders and loaders but not postLoaders
! noAutoLoaders 不要普通loader Prefixing with ! will disable all configured normal loaders
!! noPrePostAutoLoaders 不要前后置和普通loader,只要内联loader Prefixing with !! will disable all configured loaders (preLoaders, loaders, postLoaders)

6.4.2 less-loader.js #

let less = require('less');
function loader(source) {
    let callback = this.async();
    less.render(source, { filename: this.resource }, (err, output) => {
        callback(err, output.css);
    });
}
module.exports = loader;

6.4.2 style-loader #

let loaderUtils=require("loader-utils");
 function loader(source) {
    let script=(`
      let style = document.createElement("style");
      style.innerHTML = ${JSON.stringify(source)};
      document.head.appendChild(style);
    `);
    return script;
} 
module.exports = loader;

6.4.3 两个左侧模块连用 #

6.4.3.1 less-loader.js #
let less = require('less');
function loader(source) {
    let callback = this.async();
    less.render(source, { filename: this.resource }, (err, output) => {
        callback(err, `module.exports = ${JSON.stringify(output.css)}`);
    });
}
module.exports = loader;
6.4.3.2 style-loader.js #
let loaderUtils=require("loader-utils");
 function loader(source) {
    let script=(`
      let style = document.createElement("style");
      style.innerHTML = ${JSON.stringify(source)};
      document.head.appendChild(style);
    `);
    return script;
} 
//remainingRequest 剩下的路径
//previousRequest 之前的路径
// !! noPrePostAutoLoaders 不要前后置和普通loader
//https://github.com/webpack/webpack/blob/v4.39.3/lib/NormalModuleFactory.js#L339
loader.pitch = function (remainingRequest,previousRequest,data) {
    //C:\webpack-analysis2\loaders\less-loader.js!C:\webpack-analysis2\src\index.less
    console.log('remainingRequest',remainingRequest);
    console.log('previousRequest',previousRequest);
    console.log('data',data);
    let style = `
    var style = document.createElement("style");
    style.innerHTML = require(${loaderUtils.stringifyRequest(this, "!!" + remainingRequest)});
    document.head.appendChild(style);
 `;
    return style;
}
module.exports = loader;

6.4.4 css-loader.js #

6.4.4.1 walkAtRules #
var postcss = require("postcss");
var loaderUtils = require("loader-utils");
var Tokenizer = require("css-selector-tokenizer");

function plugin(options) {
  return function(css) {
    css.walkAtRules(/^import$/i, function(rule) {
      console.log('rule.params',rule.params)
      var values = Tokenizer.parseValues(rule.params);
      console.log('values',values);
      console.log('values.nodes',values.nodes);
      var url = values.nodes[0].nodes[0];
      console.log('values.nodes[0].nodes[0]',values.nodes[0].nodes[0]);
    });  
    css.walkDecls(function(decl) {
      var values = Tokenizer.parseValues(decl.value);
        console.log('values',values)
      values.nodes.forEach(function(value) {
         console.log('value',value)
        value.nodes.forEach(item => {
           console.log('item',item)
        });
      });
    });
  };
}

 var pipeline = postcss([plugin({})]);

pipeline.process('@import "index.css"').then(function(result) {
    //console.log(result);
});
/**
rule.params "index.css"
values { type: 'values', nodes: [ { type: 'value', nodes: [Array] } ] }
values.nodes [ { type: 'value', nodes: [ [Object] ] } ]
*/
6.4.4.2 walkDecls #
var postcss = require("postcss");
var loaderUtils = require("loader-utils");
var Tokenizer = require("css-selector-tokenizer");

function plugin(options) {
  return function(css) {
    css.walkDecls(function(decl) {
      var values = Tokenizer.parseValues(decl.value);
        console.log('values',values)
        values.nodes.forEach(function(value) {
         console.log('value',value)
        value.nodes.forEach(item => {
           console.log('item',item)
        });
      });
    });
  };
}

 var pipeline = postcss([plugin({})]);

pipeline.process('border:1px solid red;').then(function(result) {
    //console.log(result);
});
/**
values { type: 'values', nodes: [ { type: 'value', nodes: [Array] } ] }
value {
  type: 'value',
  nodes: [
    { type: 'item', name: '1px', after: ' ' },
    { type: 'item', name: 'solid', after: ' ' },
    { type: 'item', name: 'red' }
  ]
}
item { type: 'item', name: '1px', after: ' ' }
item { type: 'item', name: 'solid', after: ' ' }
item { type: 'item', name: 'red' }
*/
6.4.4.3 css-loader #

index.js

require('./style.css');

style.css

@import './global.css';
h1 {
  color: #f00;
}

.avatar {
  width: 100px;
  height: 100px;
  background-image: url('./avatar.jpg');
  background-size: cover;
}

global.css

body {
    background-color: green;
}
var postcss = require("postcss");
var loaderUtils = require("loader-utils");
var Tokenizer = require("css-selector-tokenizer");

function plugin(options) {
  return function(css) {
    let importItems = options.importItems;
    let urlItems = options.urlItems;
    css.walkAtRules(/^import$/i, function(rule) {
      var values = Tokenizer.parseValues(rule.params);
      var url = values.nodes[0].nodes[0];
      importItems.push({ url: url.value });
    });
    css.walkDecls(function(decl) {
      var values = Tokenizer.parseValues(decl.value);
      values.nodes.forEach(function(value) {
        value.nodes.forEach(item => {
          if (item.type === "url") {
            var url = item.url;
            item.url = "___CSS_LOADER_URL___" + urlItems.length + "___";
            urlItems.push({ url });
          }
        });
      });
      decl.value = Tokenizer.stringifyValues(values);
    });
  };
}

module.exports = function(inputSource) {
  var callback = this.async();
  let options = { importItems: [], urlItems: [] };
  var pipeline = postcss([plugin(options)]);

  pipeline.process(inputSource).then(function(result) {
    let importJs = options.importItems
      .map(function(imp) {
        return (
          "require(" +loaderUtils.stringifyRequest(this, imp.url) +")"
        );
      })
      .join("\n");

    var cssAsString = JSON.stringify(result.css);
    cssAsString = cssAsString.replace(/@import\s+['"]([^'"]+?)['"]/g,'');
    var URLREG_G = /___CSS_LOADER_URL___(\d+)___/g;
    var URLREG = /___CSS_LOADER_URL___(\d+)___/;
    cssAsString = cssAsString.replace(URLREG_G, function(item) {
      var match = URLREG.exec(item);
      if (!match) return item;
      const url = options.urlItems[+match[1]].url;
      return '" + require("' + url + '") + "';
    });
    callback(
      null,
      `
      ${importJs}
      module.exports= ${cssAsString};
      `
    );
  });
};

6.4.4 bundle.js #

{
 "./loaders/css-loader.js!./src/global.css":
 (function(module, exports) {
  module.exports= "body {\r\n    background-color: green;\r\n  }";
 }),

 "./loaders/css-loader.js!./src/style.css":
 (function(module, exports, __webpack_require__) {
     __webpack_require__(/*! ./global.css */ "./src/global.css")
      module.exports= ";\r\nh1 {\r\n  color: #f00;\r\n}\r\n\r\n.avatar {\r\n  width: 100px;\r\n  height: 100px;\r\n  background-image: url('" + __webpack_require__(/*! ./avatar.jpg */ "./src/avatar.jpg") + "');\r\n  background-size: cover;\r\n}";
 }),

 "./src/avatar.jpg":
 (function(module, exports, __webpack_require__) {
module.exports = __webpack_require__.p + "be95ec5844e2eb652fd8775b0915a9c2.jpg";
 }),
 "./src/global.css":
 (function(module, exports, __webpack_require__) {
    var style = document.createElement("style");
    style.innerHTML = __webpack_require__("./loaders/css-loader.js!./src/global.css");
    document.head.appendChild(style);
 }),

 "./src/index.js":
 (function(module, exports, __webpack_require__) {
__webpack_require__("./src/style.css");
 }),
 "./src/style.css":
 (function(module, exports, __webpack_require__) {
    var style = document.createElement("style");
    style.innerHTML = __webpack_require__("./loaders/css-loader.js!./src/style.css");
    document.head.appendChild(style);
 })
});

6.4.5 exact-loader.js #

function loader(source) {
    //发射或者说输出一个文件,这个文件的内容 就是css文件的内容
    this.emitFile('main.css', source);
    let script = `
     let link  = document.createElement('link');
     link.setAttribute('rel','stylesheet');
     link.setAttribute('href','main.css');
     document.head.appendChild(link);
  `;
    return script;
}
module.exports = loader;

6.5 file #

file-loader 并不会对文件内容进行任何转换,只是复制一份文件内容,并根据配置为他生成一个唯一的文件名。

6.5.1 file-loader #

const { getOptions, interpolateName } = require('loader-utils');
function loader(content) {
  let options=getOptions(this)||{};
  let url = interpolateName(this, options.filename || "[hash].[ext]", {content});
  this.emitFile(url, content);
  return `module.exports = ${JSON.stringify(url)}`;
}
loader.raw = true;
module.exports = loader;

6.5.2 url-loader #

let { getOptions } = require('loader-utils');
var mime = require('mime');
function loader(source) {
    let options=getOptions(this)||{};
    let { limit, fallback='file-loader' } = options;
    if (limit) {
      limit = parseInt(limit, 10);
    }
    const mimetype=mime.getType(this.resourcePath);
    if (!limit || source.length < limit) {
        let base64 = `data:${mimetype};base64,${source.toString('base64')}`;
        return `module.exports = ${JSON.stringify(base64)}`;
    } else {
        let fileLoader = require(fallback || 'file-loader');
        return fileLoader.call(this, source);
    }
}
loader.raw = true;
module.exports = loader;

6.6 sprite-loader #

6.6.1 webpack.config.js #

const path = require('path');
module.exports = {
    module:{
        rules:[
            {//css-loader是用来处理解析@import "base.css" url(./bg.jpg)
                test:/\.css$/,
                use:['style-loader','sprite-loader']
            },
             {
                test:/\.(jpg|png|gif)$/,
                use:[
                    {
                        loader:'url-loader',
                        options:{
                            limit:0
                        }
                    }
                ]
            }
        ]
    }
}

6.6.2 sprite-loader.js #

loaders\sprite-loader.js

const postcss = require('postcss');
const path = require('path');
const loaderUtils = require('loader-utils');
const SpriteSmith = require('spritesmith');
const Tokenizer = require('css-selector-tokenizer');
function loader(inputSource){
   let callback = this.async(); 
    let that = this;//this.context代表被加载资源的上下文目录
   function createPlugin(postcssOptions){
       return function(css){//代表CSS文件本身
          css.walkDecls(function(decl){
              let values = Tokenizer.parseValues(decl.value);
              values.nodes.forEach(value=>{
                   value.nodes.forEach(item=>{
                       if(item.type == 'url' && item.url.endsWith('?sprite')){
                           //拼一个路径,找到是这个图片绝对路径
                           let url = path.resolve(that.context,item.url);
                           item.url = postcssOptions.spriteFilename;
                           //按理说我要在当前规则下面添一条background-position
                           postcssOptions.rules.push({
                               url,//就是原始图片的绝对路径,未来要用来合并雪碧图用
                               rule:decl.parent //当前的规则
                           });
                       }
                   });
              });
              decl.value = Tokenizer.stringifyValues(values);//直接把url地址改成了雪碧图的名字
          });
          postcssOptions.rules.map(item=>item.rule).forEach((rule,index)=>{
                  rule.append(
                      postcss.decl({
                         prop:'background-position',
                         value:`_BACKGROUND_POSITION_${index}_`   
                      })
                  );
          });
       }
   }
   const postcssOptions = {spriteFilename:'sprite.jpg',rules:[]}
   let pipeline = postcss([createPlugin(postcssOptions)]);
   pipeline.process(inputSource,{from:undefined}).then(cssResult=>{
       let cssStr = cssResult.css;
       let sprites = postcssOptions.rules.map(item=>item.url.slice(0,item.url.lastIndexOf('?')));
       SpriteSmith.run({src:sprites},(err,spriteResult)=>{
        let coordinates = spriteResult.coordinates;
        Object.keys(coordinates).forEach((key,index)=>{
            cssStr= cssStr.replace(`_BACKGROUND_POSITION_${index}_`,`-${coordinates[key].x}px -${coordinates[key].y}px`);
        });
        that.emitFile(postcssOptions.spriteFilename,spriteResult.image);
        callback(null,`module.exports = ${JSON.stringify(cssStr)}`);
       });
   });
}
loader.raw = true;
module.exports = loader;

6.6.3 sprit.css #

src\sprit.css

*{
    padding: 0;
    margin: 0;
}
.one{
    background: url(./images/1.jpg?sprite);
    width:500px;
    height:422px;
}
.two{
    background: url(./images/2.jpg?sprite);
    width:500px;
    height:332px;
}
.three{
    background: url(./images/3.jpg?sprite);
    width:500px;
    height:352px;
}

6.7 html-layout-loader #

6.7.1 webpack.config.js #

{
  test: /\.html$/,
    use: {
          loader: 'html-layout-loader',
          options: {
              layout: path.join(__dirname, 'src', 'layout.html'),
              placeholder: '{{__content__}}'
          }
  }
}

plugins: [
        new HtmlWebpackPlugin({
            template: './src/login.html',
            filename: 'login.html'
        }),
        new HtmlWebpackPlugin({
            template: './src/home.html',
            filename: 'home.html'
        })
]

6.7.2 html-layout-loader #

const path = require('path');
const fs = require('fs');
const loaderUtils = require('loader-utils');
const defaultOptions = {
    placeholder: '{{__content__}}',
    decorator: 'layout'
}
module.exports = function (source) {
    let callback = this.async();
    this.cacheable && this.cacheable();
    const options = Object.assign(loaderUtils.getOptions(this), defaultOptions);
    const { placeholder, decorator, layout } = options;
    fs.readFile(layout, 'utf8', (err, html) => {
        html = html.replace(placeholder, source);
        callback(null, `module.exports = ${JSON.stringify(html)}`);
    })
}
const path = require('path');
const fs = require('fs');
const loaderUtils = require('loader-utils');
const defaultOptions = {
    placeholder:'{{__content__}}',
    decorator:'layout'
}
module.exports = function(source){
    let callback = this.async();
    this.cacheable&& this.cacheable();
    const options = {...loaderUtils.getOptions(this),...defaultOptions};
    const {placeholder,layout,decorator} = options;
    const layoutReg = new RegExp(`@${decorator}\\((.+?)\\)`);
    let result = source.match(layoutReg);
    if(result){
        source = source.replace(result[0],'');
        render(path.resolve(this.context,result[1]), placeholder, source, callback)
    }else{
        render(layout, placeholder, source, callback);
    }

}
function render(layout, placeholder, source, callback) {
    fs.readFile(layout, 'utf8', (err, html) => {
        html = html.replace(placeholder, source);
        callback(null, `module.exports = ${JSON.stringify(html)}`);
    })
}

7.loader测试 #

7.1 安装依赖 #

cnpm install --save-dev jest babel-jest babel-preset-env
cnpm install --save-dev webpack memory-fs

7.2 src/loader.js #

let {getOptions} = require('loader-utils');
function loader(source){
   const options = getOptions(this);
   source=source.replace(/\[name\]/g,options.name);
   return `module.exports = ${JSON.stringify(source)}`;
}
module.exports=loader;

7.3 test/example.txt #

hello [name]

7.4 test/compile.js #

const path=require('path');
const webpack=require('webpack');
let MemoryFs=require('memory-fs');
module.exports = function(fixture,options={}) {
    const compiler=webpack({
        mode:'development',
        context: __dirname,
        entry: `./${fixture}`,
        output: {
            path: path.resolve(__dirname),
            filename:'bundle.js'
        },
        module: {
            rules: [
                {
                    test: /\.txt$/,
                    use: {
                        loader: path.resolve(__dirname,'../src/loader.js'),
                        options:{name:'Alice'}
                    }
                }
            ]
        }
    });
    compiler.outputFileSystem=new MemoryFs();
    return new Promise(function (resolve,reject) {
        compiler.run((err,stats) => {
            if (err) reject(err);
            else resolve(stats);
        });
    });
}

7.5 test/loader.test.js #

let compile=require('./compile');
test('replace name',async () => {
    const stats=await compile('example.txt');
    const data=stats.toJson();
    const source=data.modules[0].source;
    expect(source).toBe(`module.exports = "hello Alice"`);
});

7.6 package.json #

"scripts": {
  "test":"jest"
}

参考 #