webpack 源码分析系列 ——loader

想要更好的格式阅读体验,请查看原文:webpack 源码分析系列 ——loader

为什么需要 loader


webpack是一个用于现代 JavaScript 应用程序的静态模块打包工具。内部通过构建依赖图管理模块之间的依赖关系,生成一个或多个 bundle 静态资源。
image.png

但是 webpack 只能处理 JavaScript 、Json 模块。应用程序除了JavaScript 、Json 模块以外还有图片、音频、字体等媒体资源、less、sass 等样式文件等非 js 代码的模块。所以需要一种能力,将非 js 资源模块解析成能够被 webpack 管理的模块。这也就是 loader 的作用。


举个例子,比如对于 less 样式文件来说,在 webpack 配置文件中如果处理 index.less 文件会经过 less-loader、css-loader、style-loader 处理,如下代码所示:

module.exports = {
   
  module: {
   
    rules: [
      {
   
        test: /\.less$/,
        use: ['style-loader', 'css-loader', 'less-loader']
      }
    ]
  }
};

webpack 解析到 index.less 模块的时候,首先会使用类似于 fs.readFile 去读取文件并且获取到文件内的源代码文本 source;拿到的 source 是需要去经过 js parser 转成 ast 的,但是在这之前会去 webpack 配置的 loader 中看看是否有处理该文件的 loader,发现有 [‘style-loader’, ‘css-loader’, ‘less-loader’] 三个 loader 按照顺序去处理的,所以 webpack 会将 source 源码以及 loader 处理器 交给 loader-runner 这个 loader 处理库,处理库会对源文件按照一定的规则经过层层 loader 进行加工处理,最终得到 webpack 可以识别的模块;然后转成 ast 进行进一步的处理,比如分析 ast ,收集模块的依赖,直到将依赖链路分析完毕为止。


到此为止应该知道 index.less 源文件会经过 三个 loader 按照一定的规则处理后得到 js 模块。那三个 loader 都是干了什么事情使得可以从样式文件转成 js 文件呢?


首先会将 source 作为入参经过 less-loader 处理,less-loader 能够将 less 代码经过 less 解析生成器 转化成 css 代码。当然转化后的 css 代码也是不能直接使用的,因为在 css 中会存在 import 依赖其他的 css 文件。


将 less-loader 解析后的 css 代码传入到 css-loader 中,在 css-loader 中会使用 css parser 解析也就是 postcss 解析 css,比如会将 import 解析成 js 中 require 的形式来引用其他的样式资源,同时还会将 css 代码转化成字符串, 通过 module.exports 抛出,此时已经将 css 文件转成了 js 模块,webpack 能够处理了。但是还不能使用,因为并没有作为 style 标签中被引用。所以需要经过 style-loader 处理。


将 css-loader 解析后的 js 代码 传入到 style-loader 中,经过 loader-utils 中路径转化函数对 require 路径处理,添加创建 style 标签, 以及将 require 引用的代码赋值给 innerHtml 中,这样,得到一段 js 代码,代码中包含了经过 style-loader 添加的 创建 style 标签内容,标签的内容是经过 css-loader 处理的将 css 解析成 js 代码, 同时 less-loader 将 less 文件解析成了 css。然后就将 less 模块解析成了 js 模块,webpack 就会后续的统一管理了。


这就是 webpack 处理 less 文件成 js 文件的过程, 但是这才是一小部分,如果能够真的可以使用还需要很多的路要走,不过不是这篇文章的重点了。到此应该大概的了解了 webpack 中 loader 是什么作用以及为什么需要 laoder 了。简单的来说,loader 就是处理module(模块、文件)的,能够将 module 处理成 webpack 能够解析的样子,同时还可以对解析的文件做一些再加工


接下来主要介绍在 webpack 中如何配置 loader;从宏观层面上聊一聊 loader 的工作原理是什么样的;同时带着一起实现一下 loader 中关键的模块 loader-runner。最后带领导大家一起手动编写上述讲到的 style-loader, css-loader, less-loader。

如何配置 loader


如下是 webpack 中对于 loader 的基本配置:

module.exports = {
   
  resolveLoader: {
   
    // 从根目录下那个文件中寻找 loader
    modules: ['node_modules', path.join(__dirname, 'loaders')],
  },
  module: {
   
    rules: [{
   
        enforce: 'normal',
        test: /\.js$/,
        use: [{
   
          loader: 'babel-loader',
          options: {
   
            presets: [
              "@babel/preset-env"
            ]
          }
        }]
      },
      {
   
        enforce: 'pre',
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      }
    ]
  }
};

具体可以参考 https://webpack.docschina.org/configuration/module/#rule 对于 rule 的文档详细介绍。其中比较重要的字段是 enforce。将 loader分为了: post(后置)、normal(普通)、pre(前置)类型。


除了可以在配置文件中设置 loader 之外,由于 loader 是对任意一个文件或者模块的处理。所以也可以在引用每一个模块的地方引用 loader,比如说:

import style from 'style-loader!css-loader?modules!less-loader!./index.less'

在文件地址 ./index.less 前可以添加 loader 多个 loader 使用 !分割, 同时再每个 loader 中后面可以添加 ?作为 loader 的 options。这种添加 loader 的方式是 inline(内联)类型的 loader。同时还可以加上特殊标记前缀,来表示某个特定的 model 要使用什么类型的 loader,分别为如下:

符号 变量 含义
-! noPreAutoLoaders 不要前置和普通 loader
! noAutoLoaders 不要普通 loader
!! noPrePostAutoLoaders 不要前后置和普通 loader,只要内联 loader

比如说对于如下:

import style from '-!style-loader!css-loader?modules!less-loader!./index.less'

对于 ./index.less 这个模块来说,不能使用配置文件中配置的前置普通的 loader,只能使用后置的以及内联的 loader 处理本模块。


所以说对于处理模块的 loader 来说,一共有四种类型: post(后置)、normal(普通)、inline(内联)、 pre(前置)。一共有三种标记可以标记某个特定模块具体使用什么类型的 loader, 接下来通过源码的角度来看看具体是怎么实现的。

loader 怎么工作


假设有如下文件和 rules:

const request = 'inline-loader1!inline-loader2!./src/index.js';
const rules = [
  {
   
    enforce: 'pre',
    test: /\.js$/,
    use: ['pre-loader1', 'pre-loader2'],
  },
  {
   
    enforce: 'normal',
    test: /\.js$/,
    use: ['normal-loader1', 'normal-loader2'],
  },
  {
   
    enforce: 'post',
    test: /\.js$/,
    use: ['post-loader1', 'post-loader2'],
  }
];

这里 request 也就是模块为./src/index.js, 同时该模块被 async-loader1 以及 async-loader2 这两个内联的 loader 处理。 同时还有一个 webpack 配置文件中的 rules,其中有 前置 loader pre-loader1、pre-loader2,普通的 loader normal-loader1、normal-loader2,当然对于 enforce 没有被赋值的情况下就是默认的 normal。还有 post 后置 loader post-loader1、post-loader2。


首先我们需要获取出这四种 loader:

const preLoaders = [];
const normalLoaders = [];
const postLoaders = [];
const inlineLoaders = request.replace(/^-?!+/, "").replace(/!!+/g, '!').split('!');

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 {
    // normal
      normalLoaders.push(...rule.use);
    }
  }
}


为了获取 内联的 loader,需要将引用的地址用 !分割获取, 但是在这之前,需要将 -?! 特殊标记前缀置位空,同时对于连续的 !也需要置为空避免出现空的 loader。这样就能获取到 [ ‘async-loader1’, ‘async-loader2’, ‘./src/index.js’ ], 已经能够获取到内联的 loader 了,同时通过循环遍历 rules 能够获取到其他的 loader。到此我们已经拿到四种 loader 了。值得注意的是, 在引用地址中和 rules 中 loader 的顺序就是定义的顺序内有发生改变的。


接下来我们需要获取 loader 执行的顺序列表 loaders 了。默认情况下也就是没有特殊标记的情况下,loaders 会是如下的顺序生成:

loaders = [
  ...postLoaders,
  ...inlineLoaders,
  ...normalLoaders,
  ...preLoaders,
 ];

默认情况下,分别按照 post inline normal pre 的顺序以及每一种 loader 定义的顺序排列生成 loaders。


对于带有特殊标记的引用来说也会影响到 loaders 中的内容:

if(request.startsWith('!')) {
    // 不要 normal
  loaders = [
    ...postLoaders,
    ...inlineLoaders,
    ...preLoaders,
  ];
} else if(request.startsWith('-!')) {
    // 不要 normal、pre
  loaders = [
    ...postLoaders,
    ...inlineLoaders
  ];
} else if(request.startsWith('!!')) {
    // 不要 post、normal、pre
  loaders = [
    ...inlineLoaders,
  ];
} else {
    // post、inline、normal、pre
  loaders = [
    ...postLoaders,
    ...inlineLoaders,
    ...normalLoaders,
    ...preLoaders,
  ];
}

对于 引用地址 request 仅仅是以 ! 开头 是不需要 normal 类型的 loader 的, 但是其他的类型的 loader 顺序依然保持。同理,-! 不需要 normal、pre loader, !! 不需要 post、normal、pre loader。


到此,对于引用文件 request 的 loader处理列表 loaders 已经拿到了,接下来需要经过 loader-runner 对 loader 列表中的 loader 按照一定的规则对 资源文件进行加工处理。

runLoaders({
   
  resource: path.join(__dirname, resource),
  loaders
}, (err, data) => {
   
  console.log(data);
});

loader 获取 loaders 列表的完整代码如下:

const {
    runLoaders } = require('loader-runner');
const fs = require('fs');
const path = require('path');

const loadDir = path.resolve(__dirname,'loaders', 'runner');
const request = 'inline-loader1!inline-loader2!./src/index.js';

let preLoaders = [];
let normalLoaders = [];
let postLoaders = [];
let inlineLoaders = request.replace(/^-?!+/, "").replace(/!!+/g, '!').split('!');

const resource = inlineLoaders.pop();

const resolveLoader = loader => path.resolve(loadDir, loader);

const rules = [
  {
   
    enforce: 'pre',
    test: /\.js$/,
    use: ['pre-loader1', 'pre-loader2'],
  },
  {
   
    enforce: 'normal',
    test: /\.js$/,
    use: ['normal-loader1', 'normal-loader2'],
  },
  {
   
    enforce: 'post',
    test: /\.js$/,
    use: ['post-loader1', 'post-loader2'],
  }
];



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
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值