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(/@import\s+['"][^'"]+['"];/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;

复制代码

原文地址:https://www.cnblogs.com/ygunoil/p/14340269.html

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


一准备工作umi为react的一个封装比较nice的脚手架。优点当然就是安装方便,开箱即用,集成了诸多好用功能,详见官网。当然缺点就是无法暴露原始的webpack.config.json文件,只能按照官方文档上特定的方法进行修改,而文档却并不完善。项目顺利进行大家笑嘻嘻,一旦遇到偏难怪需求,
webpack在引入两个依赖的包时,可能需要使用shimming,意思是处理代码上的兼容1、在main.js中引入jqueryimport$from'jquery'importappendfrom'./append'//下面的给页面添加元素文件append()2、append.js使用main.js的$向页面中添加元素functionappDomFunc(){
  Happypack (plugin)多线程使用loader编译文件treeshaking删除无用JS代码,依赖ESM规范source-map生产环境一定去掉process.env.XXX配置环境变量,区分各种编译环境splitchunks 代码分离去重DynamicImport动态加载js文件Hot-module-replacement开发环境热更新w
一目录结构├──build//构建相关├──config//配置相关├──src//源代码│├──api//所有请求│├──assets//主题字体等静态资源│
接着第一节的demo现在我们尝试整合一些其他资源,比如图像,看看webpack如何处理。在webpack出现之前,前端开发人员会使用grunt和gulp等工具来处理资源,并将它们从 /src 文件夹移动到 /dist 或 /build 目录中。同样方式也被用于JavaScript模块,但是,像webpack这样的工
webpack的打包原理识别入口文件通过逐层识别模块依赖(Commonjs、amd或者es6的import,webpack都会对其进行分析,来获取代码的依赖)webpack做的就是分析代码,转换代码,编译代码,输出代码最终形成打包后的代码什么是loaderloader是文件加载器,能够加载资源文件,并对这些文件进行
上一篇文章我们在一个demo中见到了webpack的身影,如果从未接触过webpack的同学学完了上一篇文章可能会觉得webpack只是一个“翻译官”,但事实上webpack可不仅仅可以‘翻译’代码,事实上,webpack是一个‘打包’工具,‘打包‘才是webpack的核心任务。打开webpack的官网:webpack中
注:本章将配合Vue详细讲解webpack前提条件1.在开始之前,请确保安装了Node.js和npm的最新版本。使用旧版本,你可能遇到各种问题,因为它们可能缺少webpack功能以及/或者缺少相关package包。在cmd中node-v,npm-v可查看对应的版本和安装情况。2.这里会用到ES6的模块化,如果你
前言、之前我认为对于项目的优化无非是从代码上去优化一些东西,比如循环呀函数式调用呀让你的代码看起来更加的简洁容易懂后来我在面试过程中不断有面试官不断地问了我这些问题所以自己就去研究了一下发现并不是我之前想的那样,一个好的webapck优化的胜过于你在整体代码上
##一、组件component###1.什么是组件?组件(Component)是Vue.js最强大的功能之一。组件可以扩展HTML元素,封装可重用的代码组件是自定义元素(对象)###2.定义组件的方式方式1:先创建组件构造器,然后由组件构造器创建组件方式2:直接创建组件###3.组件的分
 #webpack5概述>webpack是一个现代javascript应用程序的**静态模块打包器(modulebundler)**>>vue-cli脚手架环境,集成了webpack,所以才能对各类文件进行打包处理[webpack官网](https://webpack.js.org/) ##webpack能做什么webpack是一个静态模块打包
//这里导入webpack配置,我用对象的形式表示,当然entry属性上定义的文件要有//constconfig=require("./webpack.config");constconfig={entry:'./src/index.js'};const{join,dirname}=require("path");const{readFileSync,writeFileSync}=requi
安装cnpminstallwebpack-bundle-analyzer-D 修改vue.config.jsmodule.exports={chainWebpack:config=>{if(process.env.use_analyzer){//分析config.plugin('webpack-bundle-analyzer')
webpack打包html插件html-webpack-plugin的使用1.创建这样的测试目录结构1)index.html是即将被打包的文件,你可以随便写一些内容2)index.js是打包的入口文件,你可以写或不写内容2.webpack.config.js的代码如下:/***压缩html需要插件:html-webpack=plugin*插件使用方法:
vueinitwebpacktabbar创建vue项目时出现问题vue:无法加载文件C:\Users\dengqian\AppData\Roaming\npm\vue.ps1,因为在此系统上禁止运行脚本。C:\Windows\System32\WindowsPowerShell\v1.0,找到如上路径,以管理员身份运行powershell.exe即可输入set-ExecutionPolicyRemo
在webpack中使用ECharts【官网教程】1、使用如下命令通过npm 安装EChartsnpminstallecharts--save2、全局引用在main.jsimportechartsfrom'echarts'Vue.prototype.$echarts=echarts;3、运用<divstyle="width:60vw;height:280px;"ref="chart"&
如果没有看过我的上一篇文章,请先移步去看一下哈!时隔一年多,之前写的文章感觉大家还挺喜欢的,一直说要更新下一篇。但是一直没有机会,没时间,还有就是,感觉自己的技术,可能不足以支持我继续往下走。但是经过这个一年多的历练,感觉很多东西考虑的会更周全,不负期待!废话说一堆,步入正题
dist文件配置对应的路由名展示这样的效果可以创建一个本地的服务还可以进行代理项目开发是在src里进行代码逻辑编写./表示本地路径/表示绝对路径(nginx需要配置)
 1、用命令创建webpack模板项目vueinitwebpack 项目名,需要选是否的统一选否,所以的依赖手动添加  2、安装依赖vue-router,elementui,sass-loader,node-sass,axios插件安装路由:npminstallvue-router安装elementu
最近想把蘑菇博客部署到k8s上,作为一名java搬砖工,搬砖人不讲码德,biu一下就把后端各模块的dockerfile和chart包copy过来了,很快啊!接下来就可以愉快的使用helm管理蘑菇后端服务部署了。部署完后端服务后,准备制作前端镜像,发现前端打包后,无法动态读取系统环境变量,这很头疼,难不