webpack4 [ Loader实现原理 ]

514 阅读3分钟

前言

前章有说过 loader 相当于一个文件翻译员,主要是对不同类型的文件进行是识别与转换,由于 Webpack 是由 node 编写,所以只能识别 js 文件,那么今天我们就来探索 webpack 中的 loader 是如何实现文件识别与转换的

了解

通过一个简单的配置,来了解一下,最经典的就是 css-loader 因为 webpack 只能识别js文件,那 css 文件怎么办 ?

// 一个简单的 webpack 配置
const path = require('path');

module.exports = {
  entry: './main.js', 
  output: {
    filename: 'bundle[hash:6].js',
    path: path.resolve(__dirname, './dist'),
  },
  module: { // loader 主要使用的地方
    rules: [
        // 配置打包css文件的loader,use定义使用到的loader,注意顺序
        // style-loader 必须放在 css-loader 前面,因为loader的执行顺序是从右至左
        { 
            test: /\.css$/,  // 匹配后缀为 .css 的文件
            use: ['style-loader', 'css-loader'] 
        }
    ]
  }
};

职责与逻辑

Loader 的职责是单一的,只需要完成一种识别与转换。 如果一个源文件需要经过多次转换才能达到预期效果,那么久需要通过多个 Loader 进行转换, 在调用多个 Loader 去转换一个文件时,每个 Loader 会链式的从右向左顺序执行, 前一个Loader 处理后的结果会传给下一个 Loader 继续处理,最后的Loader 将处理后的结果返回给 Webpack

基础

通过上边的了解之后,我们可以知道 Loader 的职责单一性,其实它是一个由 node 编写的文件处理模块,实质就是一个函数,ok,看一下结构

function LoadrFunc (source) {
  // source 是参数,也就是要处理的文件实体内容
  
  // do something 
  
  // 相当于该`Loader`没有做任何转换
  return source;
};
module.exports = LoadrFunc

接收一个参数 source 源码,就是要做转换的文件源码,然后做转换,最后返回给 webpack

进阶

在了解过 loader 是如何实现的之后,那么我们还知道日常使用中,可能会这么写 es6 转化 es5

{
    test:/\.js$/, // 匹配后缀为 .js 的文件
    use:{ // 这里采用对象的形式编写
        loader: 'babel-loader',
        options: {
          presets: ['@babel/preset-env'], // es6 转化 es5
        }
      }
}
    

这里使用 es6 转换 es5 的例子来演示一下,loader 中 options 是如何实现的 ? 在介绍之前,我们需要知道一个包 loader-utils ,故名思意就是了 loader 的工具类包,里边有一些 loader 常用的方法,ok

// 引入loader-utils 包
const loaderUtils = require('loader-utils');

function LoadrFunc (source) {
  // source 是参数,也就是要处理的文件实体内容
  
  // 获取到用户给当前 Loader 传入的 options
  const options = loaderUtils.getOptions(this);
  
  // do something 
  
  // 相当于该`Loader`没有做任何转换
  return source;
};

module.exports = LoadrFunc

如何实现一个 loader

就 ES6 转换 es5 loader 来示例演进一下,看看如何实现一个类似的 loader

根据上述说法,首先安装一下 loader-utils 工具包

npm install loader-utils -D

使用相关配置

// 一个简单的 webpack 配置
const path = require('path');

module.exports = {
  entry: './main.js', 
  output: {
    filename: 'bundle[hash:6].js',
    path: path.resolve(__dirname, './dist'),
  },
  resolveLoader:{
  
  // 默认从 node_modules 查找,找不到则去 customLoaderDir 目录查找
   modules:["node_modules",path.resolve(__dirname,"customLoaderDir")], 
   
   // 别名的方式,绝对路径
   // alias:{
       // "es6ToEs5-loader":path.resolve(__dirname,"customLoaderDir","es6ToEs5-loader.js")
   // }
   
  },
  module: { // loader 主要使用的地方
    rules: [
       {
            test:/\.js$/, // 匹配后缀为 .js 的文件
            use:{ // 这里采用对象的形式编写
                loader: 'babel-loader',
                options: {
                  presets: ['@babel/preset-env'], // es6 转化 es5
                }
              }
        }
    ]
  }
};

下面我来编写一下 es6 转换 es5 的 loader, 这里命名为 es6ToEs5-loader , 用用常规 loader 的结构

loader 默认是从 node-modules 目录下查找, 根据上述相关配置,会从 node_modules 下查找,找不到的话,则会从 customLoaderDir 目录查找,loader 的名称就是自定义 loader js 文件的名称

// customLoaderDir 目录,下创建 es6ToEs5-loader.js

// 引入loader-utils 包
let loaderUtils = require('loader-utils');
// 引入babel的核心
let babel = require("@babel/core")

function LoadrFunc (source) {
  // source 是参数,也就是要处理的文件实体内容
  
  // 获取到用户给当前 Loader 传入的 options
  const options = loaderUtils.getOptions(this);
  
  // 这里是异步,所以需要调用异步 api async , 这里可以打印一下 this 看看还有哪些方法和属性
  let callback = this.async()
  
  // 通过 babel 核心准换语法
  babel.transform(source,{
      ...options,
      sourceMap:true, // 这个呢也可以在使用loader的配置 options 里
      fileNmae: this.resourcePath.split("/").pop() // 给map文件起一个昵称
  },function (err,result) {
         // 回调函数接受两个参数,一个失败原因,一个结果 result
         // result.code 转化后的代码
         // result.map 则是映射的源文件
         callback(err,result.code,result.map)
  })
  
  //  在这里这个 return 就没有任何作用了,因为上述是个回调函数,显然是异步处理的,所以核心在 callback 回调函数里
  // return source;
};

module.exports = LoadrFunc

ok,通过一个简单的示例,可以清楚的了解到 webpack 中 loader 具体是怎么实现的,下一章,我将来分享 webpack 中 plugin 插件又是怎样的原理

小小鼓励,大大成长,欢迎点赞