webpack的loader的原理和实现
想要实现一个loader,需要首先了解loader的基本原理和用法。
1. 使用
loader是处理模块的解析器。
module: {
rules: [
{
test: /\.css$/,
use: [ // 多个loader,从右向左解析,即css-loader开始
MiniCssExtractPlugin.loader,
'css-loader'
]
}
}
2.自定义loader的查找规则
很多时候,我们可以自己定义loader, 比如在根目录下新建一个loaders的文件夹,文件夹内实现各个loader的代码。但是webpack不识别这些loader,我们需要配置使webpack识别这些自定义的loader。
有四种方式:
1. resolveLoader.moduels
resolveLoader: {
modules: ['node_modules', 'loaders'] // 先从node_modules中查找,没有从loaders文件夹中查找loader1.js
},
module: {
rules: [
{
test: /\.js/,
use: ['loader1']
}
]
}
2.resolveLoader.alias
resolveLoader: {
alias: {// 绝对路径
loader1: path.resolve(__dirname, 'loaders', 'loader1.js')
}
},
3.loader的绝对路径
module: {
rules: [
{
test: /\.js/,
use: [path.resolve(__dirname, 'loaders', 'loader1.js')]
}
]
}
4.npm link(待解决)
3. loader的标准
1. 一个loader只实现一个功能,复合设计的单一功能原则。
2. loader的处理顺序。
当一个文件需要多个loader时,从最后的loader开始执行,其传入的参数是文件的原始内容。返回结果传入倒数第二个loader, 作为其入参,依次处理,直到第一个loader。
3. loaders 处理的最终结果(最后一个loader返回值)是一个字符串/Buffer。
4. loader类型
loader的加载顺序是按照pre->normal->inline->post的顺序执行
1.pre-前置loader
rule.enforce = pre;
{
test: /test\.js$/,
loader: 'loader3',
enforce: 'pre'
},
2.normal-正常loader
没有任何特征的loader都是普通loader
3.inline-行内loader
// 对test.js使用loader1和loader2
import 'loader1!loader2!./test.js'; // 按照从右到左,先执行loader2
行内loader的一个应用场景是,loader中pitch的参数remainingRequest。其通过loaderUtils.stringifyRequest(this, XXXX)后,变为
"../loaders/css-loader.js!./style.css"
对于正常的.css文件,会根据webpack中的规则,从右向左加载。但是对于上面的行内loader,有三个标志符号指定哪些loader。
1)! 忽略普通loader
// 表示忽略webpack配置中的正常loader,然后按照loader类型的顺序加载
require("!" + "../loaders/css-loader.js!./style.css")
2. -! 忽略普通和前置loader
// 表示忽略webpack配置中的正常和前置loader
require("-!" + "../loaders/css-loader.js!./style.css")
3. !! 只使用行内loader 忽略普通,前置,后置loader;
// 表示只使用行内loader, 忽略webpack配置中的loader
require("!!" + "../loaders/css-loader.js!./style.css")
4.post-后置loader
{
test: /test\.js$/,
loader: 'loader5',
enforce: 'post'
},
6. loaders的常见API
1. this.callback
当loader有单个返回值时可以直接使用return返回。当需要返回多个结果时,需要使用this.callback。
其预期参数如下:
this.callback(
err: Error | null,
content: string | Buffer,
sourceMap?:SourceMap, // 可选传参
meta?:any //元数据,可以是任意值;当将AST作为参数传递时,可以提高编译速度
}
⚠️: 使用该方法时,loader必须返回undefined。
2. 越过loader(Pitching loader)
含义:
Pitching loader指的是loader上的pitch方法。
语法:
module.exports = function (content) {
console.log(this.data); // {value: 42}
return stringorBuffer;
}
/**
* 对于请求index.js的rule
* use: ['loader1','loader2', 'loader3']
*
* @param {*} remainingRequest
* 剩余的请求。
* 如果返回undefined,则按照remainingRequest的顺序访问下一个loader的pitch
* 对于第一个被调用的pitch方法来说,其值为: loader2!loader3!index.js
*
* @param {*} precedingRequest
* 前一个请求。
* 1. 如果返回一个非undefined值,则直接进入precedingRequest所在的loader方法,
* 并且将pitch的返回值作为该loader方法的参数。
* 如果该loader不是FinalLoader,按照从右到左顺序依次执行
* 2. 有一个特殊情况,如果第一个pitch方法返回一个非undefined值,
* 它必须是string|Buffer,因为它将作为该FinalLoader的返回值
*
* @param {*} data
* pitch中的数据。
* 初始值是空对象{},可以给其赋值,然后通过loader方法中的this.date共享该数据
*/
module.exports.pitch = function(remainingRequest, precedingRequest, data) {
data.value = 42;
// 此处可以返回数据;但是如果是第一个pitch,只能返回string|Buffer,它就是最终结果
}
作用:
正常的loader加载顺序是从右到左。但是在执行loader之前,会从左到右的调用loader上的pitch方法,可以根据该方法的返回值,决定后续的loader要跳过不执行。其方法中传入的data数据可以通过loader方法中的this.data进行共享。
应用场景:
1 )最左侧的两个loader之间有关联关系;手动加载loader。
如:style-loader和css-loader
2 ) pitch阶段给data赋值,在执行阶段从this.data取值
3)通过pitch可以跳过某些loader
执行顺序:
use: [
'a-loader',
'b-loader',
'c-loader'
]
// 当所有的loader的pitch方法都返回undefined时,正确的执行顺序如下
|- a-loader `pitch`
|- b-loader `pitch`
|- c-loader `pitch`
|- requested module is picked up as a dependency
|- c-loader normal execution
|- b-loader normal execution
|- a-loader normal execution
如果某个loader的pitch方法返回一个非undefined的值,将会跳过剩余的loader。
// 如果上面的b-loader返回一个结果,则执行顺序为
|- a-loader `pitch`
|- b-loader `pitch` returns a module
|- a-loader normal execution
3. raw
设置loader的raw属性为true,则内容变为二进制形式。针对图片,文件等。
此时content.length就是文件的大小
7. loader工具库中常见方法
loader-utils: 内含各种处理loader的options的各种工具函数
schema-utils: 用于校验loader和plugin的数据结构
我们根据上面的要求,可以自己完成常见loader的实现。
1. loaderUtils.stringifyRequest(this, itemUrl)
将URL转为适合loader的相对路径
/Users/lyralee/Desktop/MyStudy/React/loaders/loaders/css-loader.js!/Users/lyralee/Desktop/MyStudy/React/loaders/src/style.css
// 使用了loaderUtils.stringifyRequest(this, XXXX)方法后
"../loaders/css-loader.js!./style.css"
2. loaderUtils.getOptions(this)
获取loader的options对象
3. schemaUtils(schema, options)
校验options的格式
8.自模拟实现loader
1. babel-loader
简单的模拟实现babel-loader。它本身是基于@babel/core和其他插件和预设。
const babel = require('@babel/core');
const loaderUtils = require('loader-utils');
const path = require('path');
function loader(inputSource) {
const loaderOptions = loaderUtils.getOptions(this);
const options = {
...options,
sourceMap: true, //是否生成映射
filename: path.basename(this.resourcePath) //从路径中获取目标文件名
}
const {code, map, ast} = babel.transform(inputSource, loaderOptions);
// 将内容传递给webpack
/**
* code: 处理后的字符串
* map: 代码的source-map
* ast: 生成的AST
*/
this.callback(null, code, map, ast);
}
module.exports = loader;
2. banner-loader
给解析的模块添加注释信息。该loader主要用于学习schema-utils的用法。
const babel = require('@babel/core');
// 获取loader的options
const loaderUtils = require('loader-utils');
// 校验loader的options
const validationOptions = require('schema-utils');
const fs = require('fs');
/**
*
* @param {*} inputSource
* 该方法只接受内容作为入参,要注意使用该插件的顺序,
* 如果在其他返回多个参数的loader之后接受参数,会丢失内容
*/
function loader(inputSource) {
// 该loader启用缓存
this.cacheable();
// 用于异步操作中
const callback = this.async();
const schema = {
type: 'object',
properties: {
text: { type: 'string' },
filename: { type: 'string'}
}
}
const options = loaderUtils.getOptions(this);
// 校验options格式是否符合自定义的格式schema
validationOptions(schema, options);
const { code } = babel.transform(inputSource);
// 读取外部文件,作为注释的内容
fs.readFile(options.filename, 'utf8', (err, text) => {
callback(null, options.text + text + code);
})
}
module.exports = loader;
按照loader中的要求,options必须含有两个字段,filename和text,否则会报错
{
loader: 'banner-loader',
options: {
text: '/***lyra code ***/',
filename: path.resolve(__dirname, 'banner.txt')
}
}
3. less-loader
const less = require('less');
module.exports = function(content) {
const callback = this.async();
less.render(content, {filename: this.resource}, (err, result) => {
callback(null, result.css)
})
}
4. css-loader
/**
* 主要实现处理@import 和 url() 语法,基于postcss
*/
//通过js插件处理样式
const postcss = require('postcss');
// css选择器的词法分析器,用于解析和序列化css选择器
const Tokenizer = require("css-selector-tokenizer"); module.exports = function(content) {
const callback = this.async();
const options = {
importItems: [],
urlItems: []
};
postcss([createPlugin(options)]).process(content).then(result => {
const {importItems, urlItems} = options;
let requires = importItems.map(itemUrl => (
`require(${itemUrl});`
)
).join('');
// require(url)返回一个打包后的绝对路径
let cssstring = JSON.stringify(result.css).replace(/_CSS_URL_(\d+)/g, function(match, g1) {
// "background-image: url('" + require('" + url + "')";
return '"+ require("' + urlItems[+g1] + '").default + "';
}); cssstring = cssstring.replace(/@import\s+['"][^'"]+['"];/g, '');
callback(null, `${requires}module.exports=${cssstring}`);
})
}
// 自定义的js插件
function createPlugin({urlItems, importItems}) {
return function(css) {
// 遍历@import规则
css.walkAtRules(/^import$/, function(result) {
importItems.push(result.params);
})
// 遍历每一条样式
css.walkDecls(function(decl) {
// 解析样式属性的值
const values = Tokenizer.parseValues(decl.value);
values.nodes.forEach(value => {
value.nodes.forEach(item => {
if(item.type === 'url') {
let url = item.url;
item.url = "_CSS_URL_" + urlItems.length;
urlItems.push(url);
}
})
})
// 将解析后值返回序列化
decl.value = Tokenizer.stringifyValues(values);
})
}
}
5.style-loader
const loaderUtils = require('loader-utils');
module.exports.pitch = function(remainingRquest, precedingRequest, data){
const script = (
`
const style = document.createElement('style');
style.innerHTML = require(${loaderUtils.stringifyRequest(this, '!!' + remainingRquest)});
document.head.appendChild(style);
`
)
return script;
}
6. file-loader
/**
* 获取内容了;修改名称;在打包文件夹中输出
*/
const { interpolateName, getOptions } = require('loader-utils'); module.exports = function(content) {
const { name='[name].[hahs].[ext]' } = getOptions(this) || {};
const outFilename = interpolateName(this, name, {content});
this.emitFile(outFilename, content);
return `module.exports=${JSON.stringify(outFilename)}`
}
// 内容二进制形式
module.exports.raw = true;
7.url-loader
/**
* 当小于limit时,使用base64;
* 当大于limit时,根据file-loader处理
*/
const { getOptions } = require('loader-utils');
const fileLoader = require('file-loader');
const mime = require('mime'); module.exports = function(content) {
const { limit=10*1024 } = getOptions(this) || {};
if (content.length < limit) {
const base64 = `data:${mime.getType(this.resourcePath)};base64,${content.toString('base64')}`
return `module.exports = "${base64}"`
}
return fileLoader.call(this, content)
}
module.exports.raw = true;
webpack的loader的原理和实现的更多相关文章
- webpack之loader实践
初识前端模板概念的开发者,通常都使用过underscore的template方法,非常简单好用,支持赋值,条件判断,循环等,基本可以满足我们的需求. 在使用Webpack搭建开发环境的时候,如果要使用 ...
- webpack之loader和plugin简介
webpack之loader和plugin简介 webpack入门和实战(二):全面理解和运用loader和plugins webpack入门(四)——webpack loader 和plugin w ...
- Vue(基础七)_webpack(webpack异步加载原理)
---恢复内容开始--- 一.前言 1.webpack异步加载原理’ 2.webpack.ensure原理 ...
- webpack之Loader
我们知道webpack的优点之一就是专注于处理模块化的项目,能做到开箱即用,但同时这也是webpack的缺点,只能用于模块化开发的项目,例如:Vue,React,Angular.Webpack在进行打 ...
- webpack源码-loader的原理
版本 webpack :"version": "3.12.0", webpack配置中的loaders配置是如何传递的 webpack/lib/NormalMo ...
- Webpack学习-工作原理(上)
对于webpack的认识始终停留在对脚手架的使用,不得不说脚手架既方便又好用,修改起来也方便,只需要知道webpack中各个配置项的功能,于是对于我们来说,webpack始终就是一个黑盒子,我们完全不 ...
- webpack的loader和plugin的区别
[Loader]:用于对模块源码的转换,loader描述了webpack如何处理非javascript模块,并且在buld中引入这些依赖.loader可以将文件从不同的语言(如TypeScript)转 ...
- webpack基本用法及原理(10000+)
1 webpack是什么 所有工具的出现,都是为了解决特定的问题,那么前端熟悉的webpack是为了解决什么问题呢? 1.1 为什么会出现webpack js模块化: 浏览器认识的语言是HTML,CS ...
- 第五十六篇:webpack的loader(四) -打包js中的高级语法
好家伙, 1.打包处理js文件中的高级语法 webpack只能打包处理一部分高级的JavaScript 语法.对于那些webpack无法处理的高级js 语法,需要借 助于 babel-loader 进 ...
随机推荐
- JavaWeb项目之多条件过滤
相信很多同学在学习java基础之后,面对各种项目还是相当头疼,那今天我将手把手教你学会JavaWeb项目中的多条件过滤,希望你能在与我实战的过程中积累经验,更进一步. 分页查询 需求分析:在列表页面中 ...
- SVN客户端教程
.SVN是一个自由/开源的版本控制系统,一组文件存放在中心版本库,记录每一次文件和目录的修改,Subversion允许把数据恢复到早期版本,或是检查数据修改的历史,Subversion可以通过网络访问 ...
- ElasticSearch 429 Too Many Requests circuit_breaking_exception
错误提示 { "statusCode": 429, "error": "Too Many Requests", "message& ...
- Unity - LayerMask简析
本文简述了LayerMask的定义,后通过项目实战充分解析 LayerMask中的GetMask.LayerToName.NameToLayer 等函数的使用方法及其注意事项. 项目地址:3D坦克大战 ...
- java之 代理设计模式
1. 设计一个案例来实现租房功能.分析:在租房的过程中涉及到了3个对象,房东,中介,房客. 中介和房客具有相同的功能--租房. 可以设计如下: 2.上图的设计实际上就是一个代理设计模式---静态代理设 ...
- vim打开多个文件、同时显示多个文件、在文件之间切换
打开多个文件: 1.vim还没有启动的时候: 在终端里输入 vim file1 file2 ... filen便可以打开所有想要打开的文件 2.vim已经启动 输入 :open file 可以再打开 ...
- JavaScript数值类型保留显示小数方法
<script type="text/javascript"> //保留两位小数 //功能:将浮点数四舍五入,取小数点后2位 function toDecimal(x) ...
- Linux下which、whereis、locate、find命令作用
1 which 查看可执行文件的位置,也可以找到命令别名 2 whereis 查看文件的位置 3 locate 系统数据库查找文件位置,数据库大约每天更新一次 4 find 根据查找条件,搜寻硬盘查询 ...
- Java框架之MyBatis框架(二)
Mybatis框架是相对于优化dao层的框架,其有效的减少了频繁的连接数据库(在配置文件xml中进行配置),将sql语句与java代码进行分离(写在XXXXmapper.xml文件中,一个表对应一个x ...
- php学习笔记——学习路线图记录
PHP学习路线图 最全PHP自学指南 W3Cschool小编 2018-04-24 15:23:51 浏览数 (5381) 分享 收录专辑 对于广大零基础的PHP自学者,往往不知道如何系统的学习PHP ...