zoukankan      html  css  js  c++  java
  • webpack的loader的原理和实现

    想要实现一个loader,需要首先了解loader的基本原理和用法。

    1. 使用

    loader是处理模块的解析器。

      module: {
        rules: [
          {
            test: /.css$/,
            use: [ // 多个loader,从右向左解析,即css-loader开始
              MiniCssExtractPlugin.loader,
              'css-loader'
            ]
          }
    }

    2.自定义loader的查找规则

    很多时候,我们可以自己定义loader, 比如在根目录下新建一个loaders的文件夹,文件夹内实现各个loader的代码。但是webpack不识别这些loader,我们需要配置使webpack识别这些自定义的loader。

    有四种方式:

    1. resolveLoader.moduels

      resolveLoader: {
        modules: ['node_modules', 'loaders'] // 先从node_modules中查找,没有从loaders文件夹中查找loader1.js
      },
      module: {
        rules: [
          {
            test: /.js/,
            use: ['loader1']
          }
        ]
      }

    2.resolveLoader.alias

      resolveLoader: {
        alias: {// 绝对路径
          loader1: path.resolve(__dirname, 'loaders', 'loader1.js')
        }
      },

    3.loader的绝对路径

      module: {
        rules: [
          {
            test: /.js/,
            use: [path.resolve(__dirname, 'loaders', 'loader1.js')]
          }
        ]
      }

    4.npm link(待解决)

    3. loader的标准

    1.  一个loader只实现一个功能,复合设计的单一功能原则。

    2.  loader的处理顺序。

    当一个文件需要多个loader时,从最后的loader开始执行,其传入的参数是文件的原始内容。返回结果传入倒数第二个loader, 作为其入参,依次处理,直到第一个loader。

    3. loaders 处理的最终结果(最后一个loader返回值)是一个字符串/Buffer。

    4. loader类型

    loader的加载顺序是按照pre->normal->inline->post的顺序执行

    1.pre-前置loader

    rule.enforce = pre;

          { 
            test: /test.js$/,
            loader: 'loader3',
            enforce: 'pre'
          },

    2.normal-正常loader

     没有任何特征的loader都是普通loader

    3.inline-行内loader

    // 对test.js使用loader1和loader2
    import 'loader1!loader2!./test.js'; // 按照从右到左,先执行loader2

    行内loader的一个应用场景是,loader中pitch的参数remainingRequest。其通过loaderUtils.stringifyRequest(this, XXXX)后,变为

    "../loaders/css-loader.js!./style.css"

    对于正常的.css文件,会根据webpack中的规则,从右向左加载。但是对于上面的行内loader,有三个标志符号指定哪些loader。

    1)!  忽略普通loader

    // 表示忽略webpack配置中的正常loader,然后按照loader类型的顺序加载
    require("!" + "../loaders/css-loader.js!./style.css")

    2. -!  忽略普通和前置loader

    // 表示忽略webpack配置中的正常和前置loader
    require("-!" + "../loaders/css-loader.js!./style.css")

    3. !! 只使用行内loader 忽略普通,前置,后置loader;

    // 表示只使用行内loader, 忽略webpack配置中的loader
    require("!!" + "../loaders/css-loader.js!./style.css")

    4.post-后置loader

          { 
            test: /test.js$/,
            loader: 'loader5',
            enforce: 'post'
          },

    6. loaders的常见API

    1. this.callback

    当loader有单个返回值时可以直接使用return返回。当需要返回多个结果时,需要使用this.callback。

    其预期参数如下:

    this.callback(
        err: Error | null,
        content: string | Buffer,
        sourceMap?:SourceMap, // 可选传参
        meta?:any //元数据,可以是任意值;当将AST作为参数传递时,可以提高编译速度
    }

    ⚠️: 使用该方法时,loader必须返回undefined。

    2. 越过loader(Pitching loader)

    含义:

    Pitching loader指的是loader上的pitch方法。

    语法:

    module.exports = function (content) {
      console.log(this.data); // {value: 42}
      return stringorBuffer;
    }
    /**
     * 对于请求index.js的rule
     * use: ['loader1','loader2', 'loader3']
     *
     * @param {*} remainingRequest 
     * 剩余的请求。
     * 如果返回undefined,则按照remainingRequest的顺序访问下一个loader的pitch
     * 对于第一个被调用的pitch方法来说,其值为: loader2!loader3!index.js
     * 
     * @param {*} precedingRequest 
     * 前一个请求。
     * 1. 如果返回一个非undefined值,则直接进入precedingRequest所在的loader方法,
     * 并且将pitch的返回值作为该loader方法的参数。
     * 如果该loader不是FinalLoader,按照从右到左顺序依次执行
     * 2. 有一个特殊情况,如果第一个pitch方法返回一个非undefined值,
     * 它必须是string|Buffer,因为它将作为该FinalLoader的返回值
     * 
     * @param {*} data 
     * pitch中的数据。
     * 初始值是空对象{},可以给其赋值,然后通过loader方法中的this.date共享该数据
     */
     module.exports.pitch = function(remainingRequest, precedingRequest, data) {
      data.value = 42;
      // 此处可以返回数据;但是如果是第一个pitch,只能返回string|Buffer,它就是最终结果
    }

    作用:

    正常的loader加载顺序是从右到左。但是在执行loader之前,会从左到右的调用loader上的pitch方法,可以根据该方法的返回值,决定后续的loader要跳过不执行。其方法中传入的data数据可以通过loader方法中的this.data进行共享。

    应用场景:

    1 )最左侧的两个loader之间有关联关系;手动加载loader。

    如:style-loader和css-loader

    2 ) pitch阶段给data赋值,在执行阶段从this.data取值

    3)通过pitch可以跳过某些loader

    执行顺序

    use: [
      'a-loader',
      'b-loader',
      'c-loader'
    ]
    // 当所有的loader的pitch方法都返回undefined时,正确的执行顺序如下
    |- 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

    如果某个loader的pitch方法返回一个非undefined的值,将会跳过剩余的loader。

    //  如果上面的b-loader返回一个结果,则执行顺序为
    |- a-loader `pitch`
      |- b-loader `pitch` returns a module
    |- a-loader normal execution

    3. raw

    设置loader的raw属性为true,则内容变为二进制形式。针对图片,文件等。

    此时content.length就是文件的大小

    7. loader工具库中常见方法

    loader-utils: 内含各种处理loader的options的各种工具函数

    schema-utils:  用于校验loader和plugin的数据结构

    我们根据上面的要求,可以自己完成常见loader的实现。

    1. loaderUtils.stringifyRequest(this, itemUrl)

    将URL转为适合loader的相对路径

    /Users/lyralee/Desktop/MyStudy/React/loaders/loaders/css-loader.js!/Users/lyralee/Desktop/MyStudy/React/loaders/src/style.css
    // 使用了loaderUtils.stringifyRequest(this, XXXX)方法后
    "../loaders/css-loader.js!./style.css"

    2. loaderUtils.getOptions(this)

    获取loader的options对象

    3. schemaUtils(schema, options)

    校验options的格式

    8.自模拟实现loader

    1. babel-loader

    简单的模拟实现babel-loader。它本身是基于@babel/core和其他插件和预设。

    const babel = require('@babel/core');
    const loaderUtils = require('loader-utils');
    const path = require('path');
    
    function loader(inputSource) {
      const loaderOptions = loaderUtils.getOptions(this);
      const options = {
        ...options,
        sourceMap: true, //是否生成映射
        filename: path.basename(this.resourcePath) //从路径中获取目标文件名
      }
      const {code, map, ast} = babel.transform(inputSource, loaderOptions);
      // 将内容传递给webpack
      /**
       * code: 处理后的字符串
       * map: 代码的source-map
       * ast: 生成的AST
       */
      this.callback(null, code, map, ast);
    }
    module.exports = loader;

    2. banner-loader

    给解析的模块添加注释信息。该loader主要用于学习schema-utils的用法。

    const babel = require('@babel/core');
    // 获取loader的options
    const loaderUtils = require('loader-utils');
    // 校验loader的options
    const validationOptions = require('schema-utils');
    const fs = require('fs');
    
    /**
     * 
     * @param {*} inputSource 
     * 该方法只接受内容作为入参,要注意使用该插件的顺序,
     * 如果在其他返回多个参数的loader之后接受参数,会丢失内容
     */
    function loader(inputSource) {
      // 该loader启用缓存
      this.cacheable(); 
      // 用于异步操作中
      const callback = this.async();
      const schema = {
        type: 'object',
        properties: {
          text: { type: 'string' },
          filename: { type: 'string'}
        }
      }
      const options = loaderUtils.getOptions(this);
      // 校验options格式是否符合自定义的格式schema
      validationOptions(schema, options);
      const { code } = babel.transform(inputSource);
      // 读取外部文件,作为注释的内容
      fs.readFile(options.filename, 'utf8', (err, text) => {
        callback(null, options.text + text + code);
      })
    }
    module.exports = loader;

    按照loader中的要求,options必须含有两个字段,filename和text,否则会报错

              {
                loader: 'banner-loader',
                options: {
                  text: '/***lyra code ***/',
                  filename: path.resolve(__dirname, 'banner.txt')
                }
              }

    3. less-loader

    const less = require('less');
    
    module.exports = function(content) {
      const callback = this.async();
      less.render(content, {filename: this.resource}, (err, result) => {
        callback(null, result.css)
      })
    }

    4. css-loader

    /**
     * 主要实现处理@import 和 url() 语法,基于postcss
     */
     //通过js插件处理样式
    const postcss = require('postcss');
    // css选择器的词法分析器,用于解析和序列化css选择器
    const Tokenizer = require("css-selector-tokenizer");
    
    module.exports = function(content) {
      const callback = this.async();
      const options = {
        importItems: [],
        urlItems: []
      };
      postcss([createPlugin(options)]).process(content).then(result => {
        const {importItems, urlItems} = options;
        let requires = importItems.map(itemUrl => (
          `require(${itemUrl});`
          )
        ).join('');  
        // require(url)返回一个打包后的绝对路径
        let cssstring = JSON.stringify(result.css).replace(/_CSS_URL_(d+)/g, function(match, g1) {
          // "background-image: url('" + require('" + url + "')";
          return '"+ require("' + urlItems[+g1] + '").default + "';
        });
    
        cssstring = cssstring.replace(/@imports+['"][^'"]+['"];/g, '');
        callback(null, `${requires}module.exports=${cssstring}`);
      })
    }
    // 自定义的js插件
    function createPlugin({urlItems, importItems}) {
      return function(css) {
        // 遍历@import规则
        css.walkAtRules(/^import$/, function(result) {
          importItems.push(result.params);
        })
        // 遍历每一条样式
        css.walkDecls(function(decl) {
          // 解析样式属性的值
          const values = Tokenizer.parseValues(decl.value);
          values.nodes.forEach(value => {
            value.nodes.forEach(item => {
              if(item.type === 'url') {
                let url = item.url;
                item.url = "_CSS_URL_" + urlItems.length;
                urlItems.push(url);
              }
            })  
          })
          // 将解析后值返回序列化
          decl.value = Tokenizer.stringifyValues(values);
        })
      }
    }

    5.style-loader

    const loaderUtils = require('loader-utils');
    
    module.exports.pitch = function(remainingRquest, precedingRequest, data){
      const script = (
        `
          const style = document.createElement('style');
          style.innerHTML = require(${loaderUtils.stringifyRequest(this, '!!' + remainingRquest)});
          document.head.appendChild(style);
        `
      )
      return script;
    }

    6. file-loader

    /**
     * 获取内容了;修改名称;在打包文件夹中输出
     */
    const { interpolateName, getOptions } = require('loader-utils');
    
    module.exports = function(content) {
      const { name='[name].[hahs].[ext]' } = getOptions(this) || {};
      const outFilename = interpolateName(this, name, {content});
      this.emitFile(outFilename, content);
      return `module.exports=${JSON.stringify(outFilename)}`
    }
    // 内容二进制形式
    module.exports.raw = true;

    7.url-loader 

    /**
     * 当小于limit时,使用base64;
     * 当大于limit时,根据file-loader处理
     */
    const { getOptions } = require('loader-utils');
    const fileLoader = require('file-loader');
    const mime = require('mime');
    
    module.exports = function(content) { 
      const { limit=10*1024 } = getOptions(this) || {};
      if (content.length < limit) {
        const base64 = `data:${mime.getType(this.resourcePath)};base64,${content.toString('base64')}`
        return `module.exports = "${base64}"`
      }
      return fileLoader.call(this, content)
    }
    module.exports.raw = true;
  • 相关阅读:
    Head First设计模式-单例模式
    mahout算法源码分析之Itembased Collaborative Filtering(四)共生矩阵乘法
    “非常PHP学习网”(www.veryphp.cn)一期上线
    IOS深入学习(19)之View object
    POJ 1005(累加)
    Plan04.学习与提升
    一个python
    【tcl脚本】改变输出字符格式
    为什么寄存器比内存快?
    我觉得epoll和select最大的区别
  • 原文地址:https://www.cnblogs.com/lyraLee/p/12050811.html
Copyright © 2011-2022 走看看