前言
webpack
作为目前支流的前端构建工具,咱们简直每天都须要与它打交道。集体认为一个好的开源产品壮大的起因应该包含外围开发者的稳固输入以及对应生态的凋敝。对于生态来说,webpack
是一个足够凋谢的类库,提供了可插拔的形式去自定义一些配置,包含配置 loader
和 plugin
,本篇咱们次要探讨 loader。
loader
实质上是一个函数,webpack 在打包过程中会依照规定顺序调用解决某种文件的 loader
,而后将上一个 loader
产生的后果或者资源文件传入进去,以后 loader
解决实现后再交给下一个 loader
。
loader 的类型
开始之前,还是要先大略提一下 loader
的类型以及一些罕用的 api
,不感兴趣的同学能够间接跳过这一大节,更具体的指引请参阅官网文档。
loader
次要有以下几种类型:
- 同步
loader
:return
或调用this.callback
都是同步返回值 - 异步
loader
:是用this.async()
获取异步函数,是用this.callback()
返回值 raw loader
:默认状况下承受utf-8
类型的字符串作为入参,若标记raw
属性为true
,则入参的类型为二进制数据pitch loader
:loader
总是从右到左被调用。有些状况下,loader
只关怀 request 前面的 元数据 (metadata
),并且疏忽前一个loader
的后果。在理论(从右到左)执行loader
之前,会先从左到右调用loader
上的pitch
办法。
开发 loader
时罕用的 API
如下:
this.async
:获取一个callback
函数,解决异步this.callback
:同步loader
中,返回的办法this.emitFile
:产生一个文件this.getOptions
:依据传入的schema
获取对应参数this.importModule
:用于子编译器在构建时编译和执行申请this.resourcePath
:以后资源文件的门路
Hello Loader
当初假如咱们有这么一个需要:在每个文件的头部打上开发者的相干信息。比方打包之前的文件内容是这样的:
const name = 'jay'
打包之后文件内容可能是这样的:
/** * @Author:jay * @Email:[email protected] * /const name = 'jay'
那废话不多说,间接开整。首先把相干依赖装置一下:
//package.json
"webpack": "^5.0.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.10.0"
再简略的配置一下 webpack:
const HtmlWebpackPlugin = require('html-webpack-plugin')
const {resolve} = require('path')
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {path: resolve(__dirname, './dist'),
filename: 'js/[name].js',
clean: true
},
module: {
rules: [
{
test: /\.js$/,
loader: './loaders/hello-loader',
},
]
},
plugins: [
new HtmlWebpackPlugin({template: './public/index.html'})
]
}
接下来就能够开始实现这个 loader
了,首先每一个 loader
都是一个函数,这个函数的返回后果要么是二进制数据要么是字符串,字符串就是文件的具体内容,二进制数据就是资源文件比方图片的内容。在下面这个需要中,显然咱们只须要拿到文件的内容,做一些批改替换即可。所以能够比拟容易的写出上面的代码:
module.exports = function (content) {const newContent = ` /** * @Author:jay * @Email:[email protected] * */ ${content}
`
return newContent;
}
这样就会将对应的内容增加到打包后果中,如下图所示:
获取参数
下面对于文件的一些形容咱们是曾经写死了,但这样不太灵便,大多数时候是心愿能通过 loader
对应的配置去获取对应的参数,咱们能够在引入 loader
的时候这样革新一下:
{
test: /\.js$/,
loader: './loaders/hello-loader',
options: {
author: 'hello loader',
email: '[email protected]'
}
}
那么在具体的 loader 中,能够应用 this.getOptions(schema)
去获取传入的配置。这个 schema
是对 option
的格局校验,代码革新如下:
参考 webpack 视频解说:进入学习
const schema = {
type: 'object', //options 是一个对象
properties: {
//author 是一个字符串
author: {type: 'string'},
//email 是一个字符串
email: {type: 'string'}
}
}
const options = this.getOptions(schema) || {}
const {author = 'null', email = 'null'} = options
const newContent = ` /** * @Author:"${author}" * @Email:"${email}" * */ ${content}
`
这样就能够将用户自定义的参数传给解决的 loader
,对于一些须要提供可拓展能力的 loader
来说,获取参数这一步是必不可少的。webpack 配置如下,即可应用 loader 获取参数的能力。
异步回调
这时候有了一个新需要,心愿咱们把以后解决的文件内容信息与文件名通过网络传输,以便后续做一些剖析。那为了不便咱们还是在下面的 loader
进行拓展,理论开发中最好不要这样做,要保障 loader
的繁多职责。这时候就不能间接返回后果,而是要获取一个异步 callback
函数,应用这个函数把后果输入。代码实现如下:
const callback = this.async()
// 模仿网络申请
setTimeout(() => {callback(null, JSON.stringify(newContent), null, {})
console.log('net done');
}, 1000)
这里注意一下 callback
函数的用法:callback(error,content,map,meta)
,其中有四个参数,别离的作用是:
error
:错误信息,如果存在的话则会抛出异样,构建终止content
:解决后的内容map
:sourceMap
相干信息meta
:要传给下一个loader
的额定信息参数
JS 解决
下面大抵举例说明了同步 l loader
、异步 loader
以及如何获取 loader
的参数。在这一小结,次要实现开发过程中常常用到的三个 JS
解决相干的 loader
:
eslint-loader
:应用eslint
做代码检测babel-loader
:将ES6+
语法转换为ES5
语法uglify-loader
:对代码进行压缩混同
eslint-loader
首先先来实现 eslint-loader
,实现思路是对以后解决的文件调用 eslint
去扫描,如果无谬误则持续失常调用下一个 loader
去解决。具体代码实现如下:
const childProcess = require('child_process')
const exec = (command, cb) => {childProcess.exec(command, (error, stdout) => {cb && cb(error, stdout)
})
}
const schema = {
type: 'object',
properties: {fix: 'boolean'}
}
module.exports = function (content) {
const resourcePath = this.resourcePath
const callback = this.async()
const command = `npx eslint ${resourcePath}`
exec(command, (error, stdout) => {if (error) {console.log(stdout)
}
callback(null, content)
})
}
这里须要理清的是,loader
只是调用了具体工程中的 eslint
,也就是说用到的 eslint
相干的插件以及配置文件都是具体工程提供的,与这个 loader
无关。简略在我的项目内的 .eslintrc.js
文件下配置了一下引号类型为单引号
打包过程如下:
配置 fix
由上图能够看出,eslint
其实还反对一键修复的能力,由 fix
参数管制。实现 fix
的思路如下:
- 将文件内容写入到新文件中,对新文件进行
eslint
检测并fix
- 读取新文件的内容,返回
代码实现如下:
let id = 0
const schema = {
type: 'object',
properties: {
fix: {type: 'boolean'}
}
}
const fs = require('fs')
const path = require('path')
module.exports = function (content) {
const resourcePath = this.resourcePath
const callback = this.async()
const {fix} = this.getOptions(schema) || {fix: false}
if (fix) {const tempName = `./${id++}.js`
const fullPath = path.resolve(__dirname, tempName)
// 写入新文件
fs.writeFileSync(fullPath, content, { encoding: 'utf8'})
// 带 fix 检测新文件
const command = `npx eslint ${fullPath} --fix`
exec(command, (error, stdout) => {if (error) {console.log(stdout)
}
// 读取新文件
const newContent = fs.readFileSync(fullPath, { encoding: 'utf8'})
fs.unlinkSync(fullPath)
callback(null, newContent)
})
} else {
// 没有抉择 fix 则还是走旧逻辑,没必要读写文件
const command = `npx eslint ${resourcePath}`
exec(command, (error, stdout) => {if (error) {console.log(stdout)
}
callback(null, content)
})
}
}
这是打包前的文件,能够看到 eslint 也对它谬误标红提醒了
这是打包后的文件,能够看到配置的检测规定 eslint
曾经帮咱们修复
babel-loader
babel-loader
应该是绝大部份前端我的项目都会用到的 loader
之一了吧,ES6+
语法转 ES5
在打包这一步是必不可少的。这里也是依赖 babel
的提供的能力,loader
充当的角色只是一个 API
调用工程师罢了~。略微灵便一点的是,调用 babel
相干的参数能够从 loader
配置中传进来:
{
test: /\.js$/,
loader: './loaders/babel-loader',
options: {presets: ['@babel/preset-env']
}
}
loader 的具体代码实现如下:
const schema = {
"type": "object",
"properties": {
"presets": {"type": "array"}
}
}
const babel = require('@babel/core')
module.exports = function (content) {const options = this.getOptions(schema);
const callback = this.async();
babel.transform(content, options, function (err, result) {if (err) {callback(err)
} else {callback(null, result.code)
}
})
}
能够别离看一下打包前后的成果:
uglify-loader
最初要实现的一个是对代码进行压缩混同的 loader
,次要是用到的是 uglify-js 这个非常弱小的库,感兴趣的同学能够自行去理解下。咱们这里的实现非常简略,只是调用了一下这个库而已。
const uglifyJS = require('uglify-js');
module.exports = function (content) {const result = uglifyJS.minify(content)
const {error, code} = result
if (error) {this.callback(error)
} else {this.callback(null, code)
}
}
能够略微看一下打包后的代码:
小结
下面咱们大略理解与实现了 JS
相干的三个 loader
,这三个 loader
配合起来应用的程序如下,默认状况下,loader
的执行程序都是从下往上,从右往左的,留神理论状况下,个别会加一个 exclude
属性,让 loader
不去检测一些文件(比方 node_modules
目录下的文件)。
{
test: /\.js$/,
exclude:/node_modules/,
loader: './loaders/uglify-loader',
},
{
test: /\.js$/,
loader: './loaders/babel-loader',
exclude:/node_modules/,
options: {presets: ['@babel/preset-env']
}
},
{
test: /\.js$/,
loader: './loaders/eslint-loader',
exclude:/node_modules/,
options: {fix: true}
}
CSS 解决
解决完 JS
文件之后,咱们开始解决 CSS
文件。这里会实现两个罕用的 loader
:css-loader
和 style-loader
。先别离介绍一下它们的作用:
- css-loader:解决
CSS
文件的依赖以及资源的加载(因为webpack
默认只反对JS
的导入以及一些资源文件的导入,所以咱们须要实现对CSS
文件的导入) - style-loader:将
css-loader
解决后的后果输入到文档中
在源码的实现中,应用了 postcss
去剖析 CSS
文件,对 CSS
文件中的资源进行了分类解决加载。咱们实现没有源码实现那么全,这里实现次要实现两个性能:
@import
的实现- 图片资源的导入
其中图片资源的导入会放到下大节图片解决中介绍,所以本小结次要实现 @import
、CSS
文件的输入以及 style-loader
@import 与 CSS 代码输入
先写下一些测试的导入代码:
//main.js
import './style/index.css'
//index.css
@import './color.css';
.container {
width: 100px;
height: 100px;
}
//color.css
@import './bg.css';
.container {color: red;}
//bg.css
.container {background: url('../assets/img-168kb.jpeg');
border: 1px solid red;
}
在这里次要须要解决的应该是 @import
这个关键字,其余的内容就当字符串失常输入即可。由上图能够看出,@import
能够有限嵌套应用,实际上咱们要做的就是一棵树的深度优先遍历,递归是比拟浅显易懂的解决形式,能够配合正文大略看下上面的代码:
//css-loader.js
module.exports = async function (content) {const callback = this.async()
let newContent = content
try {newContent = await getImport(this, this.resourcePath, content)
// 这里留神须要这样导出,后续 webpack 的打包解决会默认当成一个 JS 文件来解决
callback(null, `module.exports = ${JSON.stringify(newContent)}`)
} catch (error) {callback(error, '')
}
}
// 匹配图片
const urlReg = /url\(['|"](.*)['|"]\)/g
// 匹配 @import 关键字
const importReg = /(@import ['"](.*)['"];?)/
const fs = require('fs')
const path = require('path')
async function getImport(context, originPath, content) {
let newContent = content
let regRes, imgRes
// 获取以后解决文件的父目录
let absolutePath = originPath.slice(0, originPath.lastIndexOf('/'))
// 如果以后文件中存在 @import 关键字,一直匹配解决
while (regRes = importReg.exec(newContent)) {const importExp = regRes[1]
const url = regRes[2]
// 获取 @import 导入的 css 文件的绝对路径
const fileAbsoluteUrl = url.startsWith('.') ? path.resolve(absolutePath, url) : url
// 读取指标文件的内容
const transformResult = fs.readFileSync(fileAbsoluteUrl, { encoding: 'utf8'})
// 将 @import 关键字替换成读取的文件内容
newContent = newContent.replaceAll(importExp, transformResult)
// 持续递归解决
newContent = await getImport(context, fileAbsoluteUrl, newContent)
// 解决图片
while (imgRes = urlReg.exec(newContent)) {const url = imgRes[1]
// 获取 url 形式引入图片的父目录
let absolutePath = fileAbsoluteUrl.slice(0, fileAbsoluteUrl.lastIndexOf('/'))
// 获取引入图片的绝对路径
const imgAbsoluteUrl = url.startsWith('.') ? path.resolve(absolutePath, url) : url
if (fs.existsSync(imgAbsoluteUrl)) {
// 调用图片相干 loader 解决(下一大节实现)const transformResult = await context.importModule(imgAbsoluteUrl, {})
// 将图片 loader 解决实现的内容替换 url
newContent = newContent.replaceAll(url, transformResult)
}
}
}
return newContent
}
总结来说,下面代码干了如下几点:
- 匹配
@import
,获取绝对路径 - 读取指标文件,将读取到的文件内容替换
@import
- 如果有图片,则调用相干
loader
解决 - 以后文件循环解决,援用的文件递归解决,直至
CSS
依赖树构建实现
能够大抵看一下打包后的后果:
/***/ ((module) => {module.exports = '.container {\n border: 1px solid red;\n background: url(\'../assets/img-168kb.jpeg\'); \n}\n.container {\n color: red;\n}\n.container {\n width: 100px;\n height: 100px;\n}\n\n'
/***/ })
style-loader
有了下面的打包后果之后,咱们只须要把打包后果插入到 DOM
中,就能够实现引入款式文件的性能。大抵实现如下:
let id = 0
const fs = require('fs')
const path = require('path')
module.exports = function (content) {const temp = path.resolve(__dirname,`./${id++}.js`)
// 将 css-loader 生成的字符串写入文件
fs.writeFileSync(temp,content)
// 读出 module.exports
const res = require(temp)
fs.unlinkSync(temp)
// 插入款式
const insertStyle = ` const style = document.createElement('style'); style.innerHTML = ${JSON.stringify(res)}; document.head.appendChild(style); `
return insertStyle
}
这里 style-loader
的实现绝对简略,做了如下几件事件:
- 提取
css-loader
的处理结果(这里取巧应用了读写文件的形式) - 插入款式
一起来看看款式失效了没有吧~
图片解决
下面咱们还没有实现图片的相干 loader
毫无疑问的这篇图片引入失败了,因为图片的门路不对。咱们接下来会实现三个 loader
去解决图片,别离是:
file-loader
解决图片的门路url-loader
将图片转成 base64image-loader
压缩图片
file-loader
file-loader
是一个 raw loader
,也就是它承受的内容应该是二进制的图片数据,它要做的有两件事:
- 将图片输入到打包后果中
- 将打包后果的图片门路替换代码的门路
代码实现如下:
const loaderUtils = require('loader-utils')
module.exports = function (content, map = null, meta = {}) {
// 是否被 url-loader 解决过,解决过的话返回 base74,url-loader 在上面小结具体实现
const {url,base64} = meta
if (url) {return `module.exports = "${base64}"`
} else {
// 依据以后的上下文,生成一个文件门路,基于 dist 打包目录,这里生成的文件地址就是:dist/assets/img.jpg
const interpolateName = loaderUtils.interpolateName(
this,
'assets/[name].[contenthash].[ext][query]',
{content}
}
// webpack 特有办法,生成一个文件
this.emitFile(interpolateName, content);
return `module.exports = "${interpolateName}"`
}
}
// 增加标记,示意这是一个 raw loader
module.exports.raw = true
有了 file-loader
之后,咱们来看看打包后果是怎么的:
能够看到文件曾经输入到 dist
目录,打包后的文件中引入文件也是正确的,能够失常显示文件了。到这里咱们才算把下面说的 CSS
文件导入致实现结束:
- 解决
@import
- 解决图片
image-loader
接下来就是一些精益求精的内容了,这里做的事件是压缩图片,用到的是 images 这个开源的图片解决库。能够大略看一下代码,这里没有什么特地的解决,都是下面曾经提到过的内容,就不过多赘述。
const images = require('images')
const fs = require('fs')
// 依据全门路取文件后缀
const {getExtByPath} = require('../utils')
const schema = {
type: 'object',
properties: {
quality: {type: 'number'}
}
}
module.exports = function (content) {const options = this.getOptions(schema) || {quality: 50}
const {quality} = options
const ext = getExtByPath(this)
const tempname = `./temp.${ext}`
// 依据传入的压缩水平,生成一张新图片
images(content).save(tempname, { quality})
// 读取新图片的内容
const newContent = fs.readFileSync(tempname)
fs.unlinkSync(tempname)
// 返回新图片的内容
return newContent
}
module.exports.raw = true
url-loader
对于一些小图片,咱们能够将它转成 base64
嵌入代码中,这样能够省去一些网络申请的工夫,然而转成 base64
之后文件的大小会增大,所以要依据本身的理论状况把控好这里的阈值。
const {getExtByPath} = require('../utils')
const schema = {
type: 'object',
properties: {
limit: {type: 'number'}
}
}
module.exports = function (content) {
// 默认值 500K
const options = this.getOptions(schema) || {limit: 1000 * 500}
const {limit} = options
// 超过阈值则返回原内容
const size = Buffer.byteLength(content)
if (size > limit) {return content}
// 读取 buffer
const buffer = Buffer.from(content)
const ext = getExtByPath(this)
// 将 buffer 转为 base64 字符串
const base64 = 'data: image/' + ext + ';base64,' + buffer.toString('base64');
// 这里返回了第四个参数——meta,示意这张图片曾经被 url-loader 解决过,下层的 file-loader 应该应用 base64 变量
this.callback(null, content, null, { url: true, base64})
}
module.exports.raw = true
来看看打包后的后果:
小结
这三个 loader
的应用程序个别如下:
- 先压缩图片
- 再判断是否能够转成
base64
- 最初应用
file-loader
输入
{test: /\.(png|jpe?g|gif)$/,
loader: './loaders/file-loader',
type: 'javascript/auto' // 让 webpack 把这些资源当成 js 解决,不要应用外部的资源处理程序去解决
},
{test: /\.(png|jpe?g|gif)$/,
loader: './loaders/url-loader',
type: 'javascript/auto',
options: {limit: 1000 * 500}
},
{test: /\.(png|jpe?g|gif)$/,
loader: './loaders/image-loader',
type: 'javascript/auto',
options: {quality: 50}
}
最初
在本文中次要实现了 8 个平时会常常接触到的 loader
,包含 JS
的解决、CSS
的解决、图片的解决。心愿可能加深你我对 loader
的了解,在脑海中大略晓得它能够做什么,或者有一天遇到问题的时候,你我能够通过这种开发 loader
的形式去解决。
最近在工作中多多少少接触到了一点开发工具的需要——相似于 babel
剖析文件、eslint
插件、stylelint
插件编写,感觉还是挺有意思的。还有 postcss
,他能够生成 CSS
的 AST
,之后也想去理解一下。当然,loader
都写了,plugin
必定也得整一篇。接下来应该会对这些相干的工具写一些分享的文章,感兴趣的同学能够注意一下下~