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;
  • 相关阅读:
    Codeforces 1316B String Modification
    Codeforces 1305C Kuroni and Impossible Calculation
    Codeforces 1305B Kuroni and Simple Strings
    Codeforces 1321D Navigation System
    Codeforces 1321C Remove Adjacent
    Codeforces 1321B Journey Planning
    Operating systems Chapter 6
    Operating systems Chapter 5
    Abandoned country HDU
    Computer HDU
  • 原文地址:https://www.cnblogs.com/lyraLee/p/12050811.html
Copyright © 2011-2022 走看看