Webpack的loader的原理
上一篇文章中,我们讲到了webpack的执行原理Webpack5的打包分析,这篇文章我们讲解一下webpack是如何处理一些非js文件的内容的。本系列一共分为三篇
Loader的原理
我们知道在webpack内部是一切皆模块的概念,那我们如果处理不是js文件的内容呢?
/*
目录结构
webpack-loader-study/src
├── a.js
├── src
├── index.js
├── index.css
├── public
├── index.html
*/
// index.js
import "./index.css"
console.log('hello world')
我们的webpack配置更加简单
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode: 'development',
entry: "./src/index.js",
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'build.js'
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html'
})
]
}
当我们运行npx webpack
的时候,会发现会抱一个错误, 发现webpack并不认识我们的css文件,所以这就需要一定的loader来处理不是js的文件内容
ERROR in ./src/index.css 1:5 Module parse failed: Unexpected token (1:5) You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. 在这个例子中我们可以通过
css-loader
来处理,使得我们的webpack可以处理css文件, 我们这样处理了后,发现css文件确实不报错了,但是并没有产生效果,这是因为css-loader
只是单纯的把css文件转换成字符串,并没有作其他的逻辑,所以我们还需要把字符串提取出来到style
中或者单独的文件中
module: {
rules: [
{
test: /\.css$/,
loader: 'css-loader'
}
]
}
## 修改成
+ module: {
+ rules: [
+ {
+ test: /\.css$/,
+ use: ['style-loader', 'css-loader']
+ }
+ ]
+ }
自定义loader的开发
- loader是一个函数,接受一个源代码为参数
- loader不能是一个箭头函数,因为需要通过this上下文传递参数
- loader同步处理源码的时候必须要有返回值, 返回值必须是一个
buffer
或者string
,异步的需要调用特定的函数
// 同步的loader 直接返回内容
module.exports = function (source) {
console.log(source)
return source.replace('hello', 'hcc')
}
// 通过this.callback实现同步的loader, 可以增加一些参数(错误信息,资源,sourcemap)
module.exports = function (source) {
console.log(source)
let newSource = source.replace('hello', 'hcc-1-2')
return this.callback(null, newSource)
}
如果有一些异步的操作,去替换源代码,那么如果我们直接这样的话,会报错,因为它认为loader执行完了,没有返回值(这里和jest测试异步的原理很像)
module.exports = function (source) {
let newSource = source.replace('hello', 'hcc-1-2')
setTimeout(() => {
return this.callback(null, newSource)
}, 2000)
}
所以我们需要让它知道我们的处理没有完,可以通过this.async
返回的callback
进行处理,主动的去通知loader处理是否完成
module.exports = function (source) {
let callback = this.async()
let newSource = source.replace('hello', 'hcc-2-2')
setTimeout(() => {
return callback(null, newSource)
}, 2000)
}
手写常用的loader
上面我们大致可以知道loader的用途和使用,接下来我们将手写一些loader来处理一些文件,方便大家加深对loader的理解
- 基于js的
babel-loader
- 基于图片和文件的
file-loader
,url-loader
- 基于样式的
style-loader
,css-loader
,less-loader
基于js的兼容处理
我们知道当我们书写ES7等高级语法的时候,低端浏览器存在兼容问题,所以我们需要通过babel-loader
进行处理,将高端语法转换成低端语法
// index.js 中我们写一个class类
class A {
getName() {
console.log('name')
}
}
接下来,我们自己来实现一个babel-loader
, 平时我们使用的时候,会直接执行下面一句话,但是有很多疑问
- 为什么我没有使用到
@babel/core
但是也要安装依赖呢? @babel/preset-env
的作用是什么,为什么要在options中使用
npm install -D babel-loader @babel/core @babel/preset-env
原生的效果
- 在
webpack.config.js
中配置loader去处理js文件
+ {
+ test: /\.jsx?$/,
+ use: {
+ loader: 'babel-loader',
+ options: {
+ presets: [
+ '@babel/preset-env'
+ ]
+ }
+ }
+ }
当我们运行npx webpack
的时候,查看输出,发现class被转义成了下面这样
function A() {
_classCallCheck(this, A);
}
实现babel-loader
- 我们在loader目录下创建一个
hcc-babel-loader.js
文件 - 在
webpack.config.js
中同样的配置loader去处理js文件
{
test: /\.jsx?$/,
use: {
_ loader: 'babel-loader',
+ loader: 'hcc-babel-loader',
options: {
presets: [
'@babel/preset-env'
]
}
}
}
- 在
hcc-babel-laoder
中通过Babel的核心模块(@babel/core
)去处理代码
let babel = require('@babel/core')
let loaderUtils = require('loader-utils')
module.exports = function(source) {
let cb = this.async()
let options = loaderUtils.getOptions(this) // 获取到传入的options
babel.transform(source, { // babel.transform是一个异步操作
...options,
}, (err, result) => {
cb(err, result.code)
})
}
- 这样打包后的代码就是经过babel转义后的代码,但是我们打包后没有
source-map
的配置,需要处理一下可以输出源码映射,再webpack.config.js
中需要打开devtool: source-map
// 通过@babel/core 来进行代码转换
let babel = require('@babel/core')
let loaderUtils = require('loader-utils')
module.exports = function(source) {
let cb = this.async()
let options = loaderUtils.getOptions(this) // 获取到传入的options
babel.transform(source, { // babel.transform是一个异步操作
...options,
+ sourceMaps: true
}, (err, result) => {
+ cb(err, result.code, result.map)
})
}
实现file-loader 和 url-loader
webpack.config.js 中增加图片的处理
{
test: /\.(png|jpe?g|gif)(\?.*)?$/,
loader: 'hcc-file-loader'
}
入口文件中添加如下代码
import pic from './pic/hcc.jpg'
let image = new Image()
image.src = pic
document.body.appendChild(image)
在写file-loader
和 url-loader
之前有几个概念需要说明下
url-loader
包括file-loader
, 图片必须转换成二进制之后才可以进行处理**loader.raw = true;
**。file-laoder
的原理是,当webpack
发现处理的是图片等后缀的时候,处理图片的内容,然后导出一个打包后的地址,当我们引入图片的时候,就是引入了打包后地址- 我们新建一个
hcc-file-loader
的文件,
const loaderUtils = require('loader-utils')
function loader(source) {
let filename = loaderUtils.interpolateName(this, '[hash].[ext]', {
content:source
})
console.log(filename)
this.emitFile(filename, source) // 发送文件到dist目录下
return `module.exports = "${filename}"`
}
// 把图片转换成二进制
loader.raw = true;
module.exports = loader
在这里我们需要导出文件(module.export = ...
),因为在index.js中 有一个pic变量需要引入转换后的内容, 这里不注意很可能会出问题。
上面我们已经处理完了file-loader
, 接下来我们基于file-loader
来实现url-loader
- 增加url-loader的配置
{
test: /\.(png|jpe?g|gif)(\?.*)?$/,
use: {
loader: 'url-loader',
options: {
limit: 20000
}
}
}
- 创建一个
hcc-url-loader
来进行文件大小的处理,如果文件大于2k就利用file-loader
单独创建一个文件,如果小于20k的话转换成base64的文件
const loaderUtils = require('loader-utils')
const mime = require('mime');
function loader(source) {
const { limit } = loaderUtils.getOptions(this);
console.log(source.length)
let size = source.length // 获取文件大小
// 使用file-laoder 处理文件
if(limit && size > limit) {
return require('./hcc-file-loader').call(this, source) // 注意传递上下文
} else {
// 返回base64
return `module.exports = "data:${mime.getType(this.resourcePath)};base64,${source.toString('base64')}"`;
}
}
loader.raw = true
module.exports = loader
处理样式
基于样式处理分为主要下面三个loaderstyle-loader
, css-loader
, less-loader
- 一个less文件会首先经过
less-loader
, 把less语法转换成css语法 - 然后通过
css-loader
进行css 处理,遇到css中使用图片需要通过url-loader
来处理 - 通过
style-loader
插入到模板的style标签中
实现
- 我们现在
webpack.config.js
中创建对应的loader来处理less文件
{
test: /\.less$/,
use: {
loader: ['hcc-style-loader', 'hcc-css-loader', 'hcc-less-loader']
}
}
- 创建一个
index.less
文件,然后在入口文件index.js
中引入
body {
background: red;
#app{
color: skyblue;
}
}
index.js
文件
import './index.less'
hcc-less-loader
来利用less的render将less内容转换成css内容, 所以我们之前使用的less-loader
的时候, 需要安装less依赖,是因为less-loader
中使用了它来转换语法
let less = require('less')
module.exports = function(source) {
let result = ""
less.render(source, null, (err, output) => {
result = output.css
})
return result
}
hcc-css-loader
来将css输出,这里进行简单的处理,之后再详细介绍问题
module.exports = function(source) {
return source
}
hcc-style-loader
来将css存放在新建的style
的标签中
module.exports = function (source) {
return `let style = document.createElement('style')
style.innerHTML = ${JSON.stringify(source)} // 注意格式化代码,换行符保留
document.head.appendChild(style)`
}
问题
如果我们把index.less中引入图片的内容,这样就会出现图片加载不到的问题,因为css-loader
没有做任何处理,当传递给style-loader
的时候打包后,在dist目录找不到对应的图片位置
body {
background: url("./pic/hcc.jpg");
#app{
color: skyblue;
}
}
所以我们需要对css-loader进行改造,需要把上面的url的图片地址通过require来执行,这样在打包的时候,就会通过
url-loader
来处理图片的内容,然后发送到dist目录下
body {
background: url(require('./pic/hcc.jpg'))
}
body #app {
color: skyblue;
}
但是我们这样修改了,还有一个问题,require
需要在运行时执行,但是style-loader
的执行后是在编译阶段,所以会有一个问题,css-loader
处理后的值,在style-loader
中没用这里我们就需要一些额外的处理。首先我们需要了解loader的执行机制
loader的执行机制
- 如果我们这样的书写
use: ['loader1', 'loader2', 'loader3']
, 正常情况下真是的loader是这样执行的,会优先进过loader的pitch
, 然后依次的向后执行,但是我们一般都省略了pitch
,所以loader在我们理解中,就是从左到右开始执行 - 当我们书写了
pitch
的话,loader的执行机制会发生变化,如果pitch
有返回值的话,它会跳过之后的执行直接跳入上一个的正常的loader,loader链就会中断。例如loader2
中我们使用了loader2.pitch
并返回了内容的话,就会变成下面的执行过程有了这个知识的话,我们可以通过动态引入
css-loader
来实现
最后的实现
- 在
style-loader
中增加一个pitch
,跳过之后的loader处理pitch
有一个参数,里面有剩余没有执行的loader
和源文件- 在
pitch
处理后需要跳过所以的loader,直接运行文件 (!!
跳过 pre、 normal 和 post loader) require()
需要一个相对路径,需要通过loader-utils
来进行处理
let loaderUtils = require('loader-utils')
module.exports = function (source) {
console.log('normal-style-loader', source)
return ""
}
module.exports.pitch = function(remainingRequest) {
// E:\hcc\hcc-webpack\webpack-loader-study\loader\hcc-css-loader.js!E:\hcc\hcc-webpack\webpack-loader-study\loader\hcc-less-loader.js!E:\hcc\hcc-webpack\webpack-loader-study\src\index.less
console.log(remainingRequest)
console.log('pitch-style-loader')
// 通过!!跳过剩下的 loader, 相当于直接执行require('index.less')
return `
let style = document.createElement('style')
style.innerHTML = require(${loaderUtils.stringifyRequest(this, '!!' + remainingRequest)})
document.head.appendChild(style)`
}
css-loader
中的需要处理图片的require拼接
module.exports = function(source) {
console.log('css-normal-loader', source)
let arr = ['let list = []']
let pos = 0
let reg = /url\((.+?)\)/g
let current;
while(current = reg.exec(source)) {
let [matchUrl, group] = current
// console.log(matchUrl, group)
let last = reg.lastIndex - matchUrl.length;
arr.push(`list.push(${JSON.stringify(source.slice(pos, last))})`)
pos = reg.lastIndex
// 注意这里的字符串拼接
arr.push(`list.push('url(' + require(${group}) +')')`)
}
arr.push(`list.push(${JSON.stringify(source.slice(pos))})`)
arr.push(`module.exports = list.join('')`)
console.log(arr.join('\r\n'))
return arr.join('\r\n')
}
less-loader
不需要有任何改变还是之前的内容
let less = require('less')
module.exports = function(source) {
let result = ""
less.render(source, null, (err, output) => {
result = output.css
})
return result
}
- 修改之后的运行路径就是这样的
- 首先执行
hcc-style-loader
的pitch
,然后执行require(index.less)
进入webpack的less文件的处理 css-loader
执行后通过module.exports 导出经过require处理后的模块style-loadr
中添加到style标签的innerHTML中,然后渲染到页面样式
- 首先执行