webpack-手写loader

本文探讨了如何手写webpack中的loader,包括babel-loader、banner-loader、file-loader、url-loader以及less-loader、css-loader和style-loader。强调了loader的组成部分(pitchLoader和normalLoader),以及loader执行的特点,如最后一个loader需返回JS脚本,且每个loader保持无状态。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

手写loader

// webpack.config.js
const path = require('path')
module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  resolveLoader: {
    // 方法一: webpack会先去node_modules下找,找不到再去指定目录下找
    modules: ['node_modules', path.resolve(__dirname, 'loaders')],
    // 方法二: 配置别名
    alias: {
      loader1: path.resolve(__dirname, 'loaders', 'loader1')
    }
  },
  // 写法一: 
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          'loader1',
          'loader2',
          'loader3'   // 从下往上执行
        ]
      }
    ]
  }
  // 写法二: 
  // loader的分类  pre nromal inline post
  module: {
    rules: [
      {
        test: /\.js$/,
        use: ['loader1'],
        enforce: 'pre'
      },
      {
        test: /\.js$/,
        use: ['loader2'],
        enforce: 'normal'
      },
      {
        test: /\.js$/,
        use: ['loader3'],
        enforce: 'port'
      }
    ] // 同样是从上往下执行(可以通过enforce: pre  post 改变执行顺序)
  }
}
// /loaders/loader1.js
function loader(source){
  // 参数是源代码
  console.log(source)
  return source
}

module.exports = loader
// inline loader(在引入文件的时候执行loader)
// index.js
// 正常情况下会按照pre normal inline post执行loader
// -! 不会让文件再去执行pre normal loader
// ! 不会让文件执行normal loader
// !! 不会让文件执行其他loader 只执行inline loader
const str = require('!inline-loader(loader文件名)!./test.js')  // 写多个loader也是从后往前执行
  • loader默认由两部分组成 pitchLoader normalLoader
// loader1.js
function loader(source){
  console.log('loader1-normal')
  return source
}
loader.pitch = function(){
  console.log('loader1-pitch')
}

// loader2.js
function loader(source){
  console.log('loader2-normal')
  return source
}
loader.pitch = function(){
  console.log('loader2-pitch')
}

// webpack.config.js
module = {
  rules: [
    'loader1',
    'loader2'
  ]
}
// 输出 loader1-pitch loader2-pitch loader2-normal loader1-normal
  • loader的特点
    1. 最后一个执行的loader要返回js脚本
    2. 每个loader都是无状态的,确保loader在不用模块转换之间不保存状态
手写babel-loader
// babel-loader.js
const babel = require('@babel/core') // babel核心包
function loader(source){
  // this指向loader上下文 loaderContext
  const cb = this.async();  // 告诉 loader-runner 这个 loader 将会异步地回调
  babel.transform(source, {
    ...this.query, // 获取loader的options选项 options: {presets: ['@babel/preset-env']}
    sourceMap: true, // 开启sourceMap
    filename: this.resourcePath.split('/').pop() // sourceMap的文件名
  }, function(err, result){
    // babel的转化过程是异步的
    cb(null, result.code, result.map)
  })
}
module.exports = loader
手写banner-loader
const {validate} = require('schema-utils')
const fs = require('fs')

function loader(source){
  const options = this.query;
  const cb = this.async()
  // 校验options的骨架
  let schema = {
    type: 'object',
    properties: {
      text: {
        type: 'string'
      },
      filename: {
        type: 'string'
      }
    }
  }
  validate(schema,options, 'banner-loader');
  if(options.filename){
    this.addDependency(options.filename) // 监听文件变化,如果变化了重新打包,前提是webpack中设置watch: true
    fs.readFile(options.filename, 'utf-8', function(err, data){
      cb(err, `/**${data}**/${source}`)
    })
  }else{
    cb(null, `/**${options.text}**/${source}`)
  }
}
手写file-loader url-loader
// file-loader
const loaderUtils = require('loader-utils');
function loader(source){
  // 根据文件内容生成文件名(md5格式)
  const filename = loaderUtils.interpolateName(this, '[hash].[ext]', {
    content: source
  })
  this.emitFile(filename, source) // 生成文件
  return `module.exports="${filename}"` // 注意这里需要加"",导出的是字符串
}
loader.raw = true; // 获取原生的source(二进制流)
module.exports = loader
// url-loader
const mime = require('mime')
function loader(source){
  const options = this.query
  if(options.limit && options.limit > source.limit){
    return `module.exports = "data:/${mime.getType(this.resourcePath)};base64,${source.toString('base64')}"`
  }else{
    return require('./file-loader').call(this, source)
  }
}
loader.raw = true // 获取原生的source(二进制流)
手写less-loader css-loader style-loader
  • 实际执行顺序: style-loader(pitch), less-loader(normal), css-loader(normal)
// less-loader
// 解析less,转换成css
const less = require('less')
function loader(source){
  let css;
  less.render(source, function(err, result){
    css = result.css // 返回css对象
  })
  return css
}
module.exports = loader
// css-loader
// 把引用的路径替换成require('xxx')的形式
function loader(source){
  // ps: 
  // 1.贪婪匹配是先看整个字符串是否匹配,如果不匹配,它会去掉字符串的最后一个字符,并再次尝试,如果还不匹配,那么再去掉当前最后一个,直到发现匹配或不剩任何字符。 eg: /(.+)/g
  // 2.惰性匹配是从左侧第一个字符开始向右匹配, 先看第一个字符是不是一个匹配, 如果不匹配就加入下一个字符再尝式匹配, 直到发现匹配
  const reg = /url\((.+?)\)/g; // 惰性匹配
  let pos = 0;
  let current;
  let arr = ['let list = []'];
  while(current = reg.exec(source)){
    let [matchUrl, group] = current;
    let last = reg.lastIndex - matchUrl.length;
    arr.push(`list.push(${JSON.stringify(source.slice(pos, last))})`)
    arr.push(`list.push('url('+require(${group})+')')`)
    pos = reg.lastIndex
  }
  arr.push(`list.push(${JSON.stringify(source.slice(pos))})`)
  arr.push(`module.exports = list.join('')`)
  return arr.join('\r\n')
}
module.exports = loader;
// style-loader (最后一个loader要返回可执行的js脚本)
function loader(source){
  let str = `
    let style = document.createElement('style')
    style.innerHTML = ${JSON.stringify(source)}
    document.head.appendChild(style)
  ` // 注意这里用JSON.stringify,可以识别换行,替换成\r\n, ""不能识别换行
  return str
}
loader.pitch = function(remainingRequest){
  // remainingRequest 剩余未执行的loader和源文件
  let str = `
    let style = document.createElement('style');
    style.innerHTML = require("${'!!' + 'css-loader.js!less-loader!./index.less'}")
    document.head.appendChild(style)
  ` // 注意这里用JSON.stringify,可以识别换行,替换成\r\n, ""不能识别换行
  return str
}
module.exports = loader
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值