Skip to content

手写loader相关

loader是什么

一种用于加载、解析和转换文件的工具,用于在构建过程中对源代码进行预处理或转换

  1. loader 就是一个导出为函数的 JS 模块,接收上一个 loader 产生的结果或者资源文件(source) 作为入参,也可以用多个 loader 函数组成 loader chain
  2. compiler 需要得到最后一个 loader 产生的处理结果,这个处理结果应该是 buffer(被转为string) 或者 string

loader 执行

  1. 初始化参数:从配置文件和 shell 语句中读取与合并参数,得出最终的参数
  2. 开始编译:用参数初始化compiler,加载所有配置插件,执行run方法开始编译,确定入口,依据入口找到所有“入口”文件
  3. 编译模块:从入口文件出发,调用所有配置当中的 loader 编译模块,再找出该模块的依赖模块,再递归进行处理
  4. 完成编译:经过 loader 编译之后的模块得到编译之后的内容,及它们间的依赖关系
  5. 输出资源:依据入口和依赖关系,组装成一个个包含多个模块的 chunk,再将 chunk 转换成独立的文件加入输出列表
  6. 写入文件:在确定好输出内容后,依据配置中的输出路径和文件名称,将内容最终写入到文件系统

loader 分类

pre 前置 -- normal 正常、普通 -- inline 行内 -- post 后置

不同的 loader 并不是说 loader 本身具有什么属性,而是在使用时使用了什么样的 enforce 来进行修饰

javascript
const path = require('path')
const fs = require('fs')

let filePath = path.resolve(__dirname, 'src', 'index.js') // 入口模块 

//! 此处定义行内 loader 
let request = `inline1-loader!inline2-loader!${filePath}`

//! 通过 rules 定义
let rules = [
  {
    test: /\.js$/,
    use: ['normal1-loader', 'normal2-loader']
  },
  {
    test: /\.js$/,
    enforce: 'post',
    use: ['post1-loader', 'post2-loader']
  },
  {
    test: /\.js$/,
    enforce: 'pre',
    use: ['pre1-loader', 'pre2-loader']
  }
]

console.log(request)

loader 组合

提取行内 loader

js
//* 01 提取行内 loader 与 需要被处理的文件
let parts = request.split('!')
let resource = parts.pop()

自定义函数获取 loader 绝对路径

js
//* 02 将 loader 处理成绝对路径
let resolveLoader = loader => path.resolve(__dirname, 'loaders', loader)
let inlineLoader = parts.map(resolveLoader)

提取非行内 loader

js
//* 03 将非行内 loader 提取出来
let preLoaders = [], normalLoaders = [], postLoaders = []
for (let i = 0; i < rules.length; i++) {
  let rule = rules[i]
  if (rule.test.test(resource)) {
    if (rule.enforce === 'pre') {
      preLoaders.push(...rule.use)
    } else if (rule.enforce === 'post') {
      postLoaders.push(...rule.use)
    } else {
      normalLoaders.push(...rule.use)
    }
  }
}
preLoaders = preLoaders.map(resolveLoader)
normalLoaders = normalLoaders.map(resolveLoader)
postLoaders = postLoaders.map(resolveLoader)

前置符号

前置符号使用

在使用 loader 的时候可以设置符号来控制最终执行哪个

  • ! 禁用所有的普通 loader (post inline pre)
  • !! 跳过 pre normal post ,只保留 inline
  • -! 禁用 pre 和 normal
js
// 符号使用
const title = require('!!inline1-loader!./title') // 只有 inline
const title = require('!inline1-loader!./title') // 只禁 normal
const title = require('-!inline1-loader!./title') // 禁用normal 与 pre

const path = require('path')

module.exports = {
  mode: 'development',
  devtool: false,
  entry: './src/index.js',
  resolveLoader: {
    modules: [path.resolve(__dirname, 'loaders'), 'node_modules']
  },
  output: {
    filename: 'build.js',
    path: path.resolve('dist')
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        // use: path.resolve(__dirname, 'loaders', 'normal1-loader')
        use: ['normal1-loader', 'normal2-loader']
      },
      {
        test: /\.js$/,
        enforce: 'post',
        use: ['post1-loader', 'post2-loader']
      },
      {
        test: /\.js$/,
        enforce: 'pre',
        use: ['pre1-loader', 'pre2-loader']
      }
    ]
  }
}

前置符号处理

js
// 此时的 loader 处理
let parts = request.replace(/^-?!+/, '').split('!')

//* 04 前置 loader 处理
let finalLoaders = []
if (request.startsWith('!!')) {
  finalLoaders = [...inlineLoaders] // 只有 inlineLoader 生效
} else if (request.startsWith('-!')) {
  finalLoaders = [...postLoaders, ...inlineLoaders] // 去除pre 和 normal
} else if (request.startsWith('!')) {
  finalLoaders = [...postLoaders, ...inlineLoaders, ...preLoaders] // 去除 normal 
} else {
  finalLoaders = [...postLoaders, ...inlineLoaders, ...normalLoaders, ...preLoaders]
}

console.log(finalLoaders)

runLoaders

原生 runLoaders方法

获取到所有的入口之后就使用配置当中的 loader 对入口文件进行处理,此时会调用 runloader 方法

js
// 使用原生的 ruLoaders 方法
const { runLoaders } = require('loader-runner')

//* 05 调用 runLoader 方法
runLoaders({
  resource, // 需要加载和转换的模块
  loaders,  // 保存loader们的数组
  context: { minimize: true }, // 上下文对象
  readResource: fs.readFile.bind(fs)  // 读取文件时使用到的方法
}, (err, result) => {
  console.log(err)
  console.log(result)
})

pitch 含义

js
function loader(source) {
  console.log('inline1执行了')
  return source + '//inline1'
}
loader.pitch = () => {
  console.log('inline1-pitch')
}

module.exports = loader
  1. a!b!c!module, 正常的调用顺序应该是 c b a ,但是真正调用顺序是 a(pitch) b(pitch) c(pitch) c b a ,如果其中任何一个 pitching loader 有返回值就相当它和它右边的 loader 已经执行完毕
  2. 如果 b 返回了字符串 result b , 接下来只有 a 会被系统执行,且 a 的loader收到的参数就是 result b
  3. loader 返回值有两种

runLoaders 初始化

js
function runLoaders(options, callback) {
  let resource = options.resource  // 获取要加载的资源 src/index.js
  let loaders = options.loaders || [] // 需要经过哪些 loader 进行处理
  let loaderContext = options.context || {} // loader 执行上下文
  let readResource = options.readResource || fs.readFile  // 读取文件内容的方法

  // 将每一个 loader 从loader的绝对路径处理成一个 loader 对象
  loaders = loaders.map(createLoaderObject)
}

module.exports = runLoaders

createLoaderObject 实现

js
function createLoaderObject(request) {
  let loaderObj = {
    request,
    normal: null, //loader函数本身
    pitch: null, // loader的pitch
    raw: false, //是否需要转为字符串,设置为true表示处理 buffer
    data: {}, // 每一个 loader 都会有一个自定义对象来存储自定义信息
    pitchExecuted: false,  // 当前loader的pitch函数是否已经执行过
    normalExecuted: false, // 当前 loader的 normal方法是否已经执行过
  }
  let normal = require(loaderObj.request)  // 加载这个 loader 模块
  loaderObj.normal = normal
  loaderObj.pitch = normal.pitch
  loaderObj.raw = normal.raw
  return loaderObj
}

runLoaders 补充

js
let loaderObjects = loaders.map(createLoaderObject)
loaderContext.resource = resource // 将来通过 this 可以找到当前 loader 的资源
loaderContext.index = 0 // 当前正在执行的 loader 的索引
loaderContext.loaders = loaderObjects  // loader对象数组
loaderContext.callback = null  // loader回调函数
loaderContext.async = null //设置是否为异步操作

file-loader

生成一个新的文件名,让 webpack 将当前文件拷贝到指定的路径

js
const imgSrc = require('./img/t1.jpg')
const oImg = document.createElement('img')
oImg.src = imgSrc
oImg.width = 180
document.body.appendChild(oImg)
---------------------------------------------------
const { getOptions, interpolateName } = require('loader-utils')

function loader(source) {
  const options = getOptions(this) || {}

  // 生成打包后输出的文件名 
  let filename = interpolateName(this, options.filename, { content: source })

  // 利用 webpack 内部实现的方法将上述文件名所对应的文件拷贝至指定的目录
  this.emitFile(filename, source)

  // 最终返回一个 buffer 或者字符串直接给 compiler 进行使用
  return `module.exports = ${JSON.stringify(filename)}`
}
loader.raw = true
module.exports = loader
  1. 通过 loader-utils里的 interpolateName 方法可以配合 options.filename 及文件内容生成一个唯一的文件名
  2. 通过 this.emitFile(uri, content) 让 webpack依据参数创建对应的文件,放在指定目录下
  3. 返回 module.exports=$(JSON.stringify(uri)), 这样就把原来的文件路径替换为编译后的路径

url-loader

建立在 file-loader 之上的一个 loader

js
rules: [
    {
        test: /\.(jpg|png|gif|jpeg)$/,
        // use: ['file-loader']
        use: [
            {
                loader: 'url-loader',
                options: {
                    limit: 100 * 1024
                }
            }
        ]
    }
]
js
const mime = require('mime')
const { getOptions } = require('loader-utils')

function loader(content) {
  const options = getOptions(this) || {}
  let { limit, fallback = 'lg-file-loader' } = options
  // 判断是否存在 limit 
  if (limit) {
    limit = parseInt(limit, 10)
  }

  if (!limit || content.length < limit) {
    let mimeType = mime.getType(this.resourcePath)   // resourcePath就是需要加载的文件路径 

    // 按着规则将图片数据处理为 base64 
    let base64Str = `data:${mimeType};base64,${content.toString('base64')}`
    return `module.exports=${JSON.stringify(base64Str)}`
  } else {
    // 这里的 require 不会自动加载配置文件,需要手动设置
    let fileLoader = require(fallback)
    return fileLoader.call(this, content)
  }
}

loader.raw = true
module.exports = loader

// data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ

Released under the MIT License.