手写loader相关
loader是什么
一种用于加载、解析和转换文件的工具,用于在构建过程中对源代码进行预处理或转换
- loader 就是一个导出为函数的 JS 模块,接收上一个 loader 产生的结果或者资源文件(source) 作为入参,也可以用多个 loader 函数组成 loader chain
- compiler 需要得到最后一个 loader 产生的处理结果,这个处理结果应该是 buffer(被转为string) 或者 string
loader 执行
- 初始化参数:从配置文件和 shell 语句中读取与合并参数,得出最终的参数
- 开始编译:用参数初始化compiler,加载所有配置插件,执行run方法开始编译,确定入口,依据入口找到所有“入口”文件
- 编译模块:从入口文件出发,调用所有配置当中的 loader 编译模块,再找出该模块的依赖模块,再递归进行处理
- 完成编译:经过 loader 编译之后的模块得到编译之后的内容,及它们间的依赖关系
- 输出资源:依据入口和依赖关系,组装成一个个包含多个模块的 chunk,再将 chunk 转换成独立的文件加入输出列表
- 写入文件:在确定好输出内容后,依据配置中的输出路径和文件名称,将内容最终写入到文件系统
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- a!b!c!module, 正常的调用顺序应该是 c b a ,但是真正调用顺序是 a(pitch) b(pitch) c(pitch) c b a ,如果其中任何一个 pitching loader 有返回值就相当它和它右边的 loader 已经执行完毕
- 如果 b 返回了字符串
result b, 接下来只有 a 会被系统执行,且 a 的loader收到的参数就是 result b - 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 = runLoaderscreateLoaderObject 实现
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- 通过 loader-utils里的 interpolateName 方法可以配合 options.filename 及文件内容生成一个唯一的文件名
- 通过 this.emitFile(uri, content) 让 webpack依据参数创建对应的文件,放在指定目录下
- 返回 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