在webpack中自定义loader

96 阅读3分钟

loader

类型

  • loader本身不具备类型,但(在webpack中)使用的时候可以通过enforce指定配置类型,默认normal
  • 类型有四种
    • post 后置
    • inline 内联
    • normal 正常
    • pre 前置
  • 我们可以根据这些来配置执行时机,比如eslint做前置,因为编译完再检查没什么意义
  • 一般来说3个就够了,post、normal、pre
  • 执行顺序为pre、normal、inline、post
  • 这个类型实际上还是我们根据传入的参数,拼接成一个数组,然后执行,只是webpack帮我们排了

特殊符号记忆

  • -! noPreAuto 不要pre
  • ! noAuth 不要normal
  • !! noPrePostAuto 不要前置后置normal
  • 应用给inline的,这样就可以来筛选本次的配置了,放在inline前面
// inline表现为写在一行,以!分割,最右侧为源代码
let request = `inline-loader1!inline-loader2!${entryFile}`; 
// 不要normal
let request = `!inline-loader1!inline-loader2!${entryFile}`;
// 不要pre
let request = `-!inline-loader1!inline-loader2!${entryFile}`;
// 不要前置后置normal
let request = `!!inline-loader1!inline-loader2!${entryFile}`;

使用

  • 以runLoaders来模拟,按照分类然后拼接
  • 执行node runnder.js
// runnder.js
const { runLoaders } = require("loader-runner");
const path = require("path");
const fs = require("fs");
const entryFile = path.resolve(__dirname, "src/index.js");

let request = `!!inline-loader1!inline-loader2!${entryFile}`;
const rules = [
  {
    test: /\.js$/,
    use: ["normal-loader1", "normal-loader2"],
  },
  {
    test: /\.js$/,
    enforce: "pre",
    use: ["pre-loader1", "pre-loader2"],
  },
  {
    test: /\.js$/,
    enforce: "post",
    use: ["post-loader1", "post-loader2"],
  },
];
// 先将inline的前缀去掉,避免特殊情况
const parts = request.replace(/^-?!+/, "").split("!");
let resource = parts.pop(); // 获取最后一项,也就是文件地址

let inlineLoaders = parts;

let preLoaders = [];
let postLoaders = [];
let normalLoaders = [];

for (let i = 0; i < rules.length; i++) {
  let rule = rules[i];
  if (resource.match(rule.test)) {
    if (rule.enforce === "pre") {
      preLoaders.push(...rule.use);
    } else if (rule.enforce === "post") {
      postLoaders.push(...rule.use);
    } else {
      normalLoaders.push(...rule.use);
    }
  }
}

// 和
let loaders = [];

// 特殊处理,判断inline的前缀
if (request.startsWith("!!")) {
  // 不要前置后置和normal
  loaders = inlineLoaders;
} else if (request.startsWith("-!")) {
  // 不要pre
  loaders = [...postLoaders, ...inlineLoaders, ...normalLoaders];
} else if (request.startsWith("!")) {
  // 不要normal
  loaders = [...postLoaders, ...inlineLoaders, ...preLoaders];
} else {
  loaders = [...postLoaders, ...inlineLoaders, ...normalLoaders, ...preLoaders];
}

// 把loader批量处理下,加上前缀路径,因为是我们自己写的loader
// loader-chain是文件夹名
/*
以inline-loader1举例
function loader(source) {
  console.log("inline-loader1");
  return source + " inline-loader1";// 每次拼接上前缀
}

module.exports = loader;

*/
loaders = loaders.map((loader) =>
  path.resolve(__dirname, "loader-chain", loader)
);

runLoaders(
  {
    resource, // 要处理的文件
    loaders, // 资源文件经过哪些loader处理
    readResource: fs.readFile, // 读取文件的方法
  },
  (err, result) => {
    if (err) {
      console.log(`错误信息:${err}`);
    } else {
      console.log("结果", result.result[0].toString()); // 转换后的结果
      console.log(
        "源文件",
        result.resourceBuffer && result.resourceBuffer.toString()
      ); // 转换前的源文件
    }
  }
);

// 结果 "基础信息"; inline-loade2 inline-loader1
// 源文件 "基础信息";

pitch

  • 以行内举例 a!b!c!source.js
    • 目前理解到的执行顺序为读文件->c->b->a
    • 但其实前面还有一步,为pitch阶段,正确的执行顺序为a(pitch)->b(pitch)->c(pitch)->读文件->c->b->a
  • 一旦pitch有返回值,那么会跳过,后续阶段,将当前返回值,给到后边的loader,说起来有点绕,举例说明:
    • 在b的pitch有返回值,那么执行顺序为 a(pitch)->b(pitch)->a 跳过了4步c(pitch)、读文件、c、b
  • pitch会有两个形参
    • remainingRequest 还没走的loader路径和文件路径,以!间隔
    • request 已经走过的loader路径,以!间隔
将request的!!去掉,然后执行
// pre-loader2
// pre-loader1
// normal-loader2
// normal-loader1
// inline-loader2
// inline-loader1
// post-loader2
// post-loader1
// 结果 "基础信息";pre-loade2 pre-loade1 normal-loader2 normal-loader1 inline-loade2 inline-loader1 post-loade2 post-loade1
// 源文件 "基础信息";

现在给post-loader1.js里面加一个pitch

function loader(source) {
  console.log("pre-loader1");
  return source + " pre-loade1";
}
loader.pitch = () => {
  console.log("post1-pitch");
  return "跳不跳";
};
module.exports = loader;

// post1-pitch
// 结果 跳不跳
// 源文件 null

// 对比
// 例1
    // pitch阶段
    post1->post2->inl1->inl2->nor1->nor2->pre1->pre2
    // 正常阶段
    读文件->pre2->pre1->nor2->nor1->inl2->inl1->post2->post1
// 例2
    post1-pitch

在第一个pitch处返回内容了,那么后续全部跳过了,webpack接收的内容为 "跳不跳",因为没有读文件,所以源文件为null

在webpack中使用自定义loader

  • 3种方式
    • 1.使用绝对路径的形式
    • 2.自定义别名
    • 3.指定查找目录 推荐
const path = require("path");

// 方式1
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          // loader: "babel-loader",
          loader: path.resolve(__dirname, "loaders/babel-loader.js"),
          options: {
            presets: ["@babel/preset-env"],
          },
        },
      }
    ],
  },

//方式2
  resolveLoader: {
    alias: {
      "babel-loader": path.resolve(__dirname, "loaders/babel-loader.js"),
    },
  },
// 用的时候直接用babel-loader就行啦

// 方式3,指定查找目录为loaders和node_modules,写法也是babel-loader
  resolveLoader: {
    modules: [path.resolve("loaders"), "node_modules"],
  },

实现babel-loader

const babel = require("@babel/core");
function loader(source) {
  console.log("我自己的loader");
  // 获取到webpack中自己的配置项,options
  // 这个this是loader的上下文对象
  const options = this.getOptions();
  // 同步
  // const { code } = babel.transformSync(source, options);
  // return code;

  // 异步
  const callback = this.async();// 告诉babel-runner当前执行为异步执行
  babel.transformAsync(source, options).then(({ code }) => {
    // 当前执行完了,可以执行下一个了
    callback(null, code);
  });
}

module.exports = loader;
  • 在使用过程中,我们需要使用sourcemap建立堆栈,因为源代码与打包后的代码差异过大,我们需要在开发时候通过打包后的产物定位源码
  • 而且每到一个babel,就需要重新建立语法树,其实是没必要的
// babel1
const babel = require("@babel/core");
function loader(source) {
  const options = this.getOptions();
  let babelOptions = {
    ...options,
    ast: true, // 生成ast
    sourceMaps: true, // 生成sourcemap
  };
  // 异步
  const callback = this.async(); // 告诉babel-runner当前执行为异步执行
  babel.transformAsync(source, babelOptions).then(({ code, ast, map }) => {
    // code为转换后的代码,ast为本次生成的ast语法树,map为courcemap
    callback(null, code, ast, map);
  });
}

module.exports = loader;

// babel-loader2
const babel = require("@babel/core");
function loader(source, ast, inputSourceMap) {
  const options = this.getOptions();
  let babelOptions = {
    ...options,
    sourceMaps: true,
    ast: true,
    inputSourceMap, // 沿用上一个sourcemap,最终的代码与源代码差别还是很大的,所以一层一层的延续引用
  };
  // 异步
  const callback = this.async();
  // 调用transformFromAst解析上一步的语法树,不需要重复解析了,map映射也放进来
  babel.transformFromAstAsync(ast, babelOptions).then(({ code }) => {
    callback(null, code);
  });
}

module.exports = loader;

// config.js示例
... 
rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader2",
        },
      },
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
          options: {
            presets: ["@babel/preset-env"],
          },
        },
      },
    ],

实现less-loader

  • 会获取源文件,然后调用less编译
const 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;

实现style-loader

  • 逻辑:
    • 会先调用less-loader将代码编译成模块,模块导出的是默认编译好的css
    • 然后由入口文件使用require引入
    • 这样的话,会有一个问题,那就是style-loader接收的是一个module.exports = css....,这样是不合理的,所以在pitch阶段执行
    • 在pitch阶段执行,且返回,那么会跳过less-loader和读取less,但我们需要less-loader来解析文件
    • 那么我们使用pitch的参数1(未走路径),然后返回字符串,字符串会直接给到webpack
    • 字符串中使用行间方法的!!,防止死循环,
      • webpack识别到我们需要查找less,且标识了行间loader,那么这次就会读文件,然后走less-loader,然后返回
      • 在这个过程中,我们把babel的概念去掉,将less理解成模块就行了,最终产出还是个模块,只不过是我们在style-loader中将它插入了
const path = require("path");
function loader() {}
// /xxxxxxxx/loader/loaders/less-loader.js!/xxxxxxxx/loader/src/index.less
loader.pitch = function (remainingRequest) {
  // 先补一个!!,让它只走行内loader,也就是我们指定的后续没走的loader,否则会再次找webpack的rule,那么就死循环了
  // 获取剩下跳过的loader绝对路径
  // 以!分割路径
  // 替换绝对路径为相对路径,this.context为项目根目录,我们指定的entry
  // 然后再还原回去,以!间隔,这时候我们就将绝对路径变为相对路径了
  const request =
    
    remainingRequest
      .split("!")
      .map(
        (requestAbsPath) =>
          "./" + path.posix.relative(this.context, requestAbsPath)
      )
      .join("!");
  let script = `
    let styleCss = require(${JSON.stringify(request)});
    let style = document.createElement('style');
    style.innerHTML  = styleCss;
    document.head.appendChild(style);
    `;
  return script;
};
module.exports = loader;

使用

      {
        test: /\.less$/,
        exclude: /node_modules/,
        use: ["style-loader", "less-loader"],
        // use: ["style-loader", "css-loader", "less-loader"],
      },

结果

...
  var __webpack_modules__ = {
    "./loaders/less-loader.js!./src/index.less": (module) => {
      module.exports =
        "body {\n  background-color: greenyellow;\n}\nbody div {\n  color: red;\n}\n";
    },
    "./src/index.less": (
      __unused_webpack_module,
      __unused_webpack_exports,
      require
    ) => {
      let styleCss = require("./loaders/less-loader.js!./src/index.less");
      let style = document.createElement("style");
      style.innerHTML = styleCss;
      document.head.appendChild(style);
    },
  };
...