继上两篇文章webpack工做原理介绍(上篇、下篇),咱们了解到Loader:模块转换器,也就是将模块的内容按照需求装换成新内容
,并且每一个Loader的职责都是单一,只会完成一种转换,因此咱们通常对源文件的处理,也是由多个Loader以链式顺序执行的方式来进行屡次装换,而后获得咱们要的结果。javascript
那么这样Loader只须要关心输入和输出,Loader实际上是一个Node.js模块,该模块导出的是一个函数(意味着,全部node.js的api咱们均可以使用),以下:css
module.exports = function (source) { // 对source作一系列的转换 return source; }
下面咱们介绍一下webpack提供了哪些供Loader调用的api,对Loader有个比较深入的理解,而后来分析babel-loader
的源码,看看咱们经常使用的loader是怎么编写出来的。java
const loaderUtils = require('loader-utils'); module.exports = function(source) { // 获取用户为当前Loader传入的options console.log(loaderUtils.getOptions(this)); return source; }
如上,咱们返回的是转换后的内容,可是有些状况下,咱们不只仅须要返回转换后的内容,还须要返回一些其余的内容,如sourceMap或是AST语法树,那么这时候咱们可使用webpack提供的APIthis.callback
,当使用this.callback
了,那么咱们就必须须要在Loader函数返回undefined
,以此来让webpack知道返回的结果在this.callback
中,API详细参数以下:node
this.callback( // 没法装换原内容时的Error err: Error || null, // 装换后的的内容,如上述的source content: string | Buffer, // 用于经过装换后的内容得出原内容的Source Map,方便调试 // 咱们了解到,SourceMap咱们只是会在开发环境去使用,因而就会变成可控制的, // webpack也提供了this.sourceMap去告诉是否须要使用sourceMap, // 固然也可使用loader的option来作判断,如css-loader sourceMap?: SourceMap, // 若是本次转换同时生成ast语法树,也能够将这个ast返回,方便后续loader须要复用该ast,这样能够提升性能 abstractSyntaxTree? AST );
看看异步Loader在this.async
API下如何实现,webpack
module.exports = async function (source) { const callback = this.async(); const { err, content, sourceMap, AST } = await Func(); callback(err, content, sourceMap, AST); // 如上诉`this.callback`参数同样 }
像file-loader
这样的Loader,处理的是二进制数据,那么就须要告诉webpack给loader传入二进制格式的数据,代码能够以下:git
module.exports = function(source) { if (source instanceof Buffer) { // 一系列操做 return source; //固然我自己也能够返回二进制数据提供给下一个loader } } moudle.exports.raw = true; //不设置,就会拿到字符串
经过moudle.exports.raw = true;
告知webpack,本身自己须要二进制数据。es6
优化的最佳点,可使用this.cacheable(Boolen)
,缓存loader转换后的内容,当处理文件或依赖文件没有发生变化时,使用缓存的转换内容,以此提速!github
说到学习,固然越系统越好了,api多介绍 ,除了上面经常使用的api以外,还存在如下经常使用的api。web
this.context
: 当前处理转换的文件所在的目录this.resource
: 当前处理转换的文件完整请求路径,包括querystringthis.resourcePath
: 当前处理转换的文件的路径this.resourceQuery
: 当前处理文件的querystringthis.target
: webpack配置的targetthis.loadMoudle
: 处理文件时,须要依赖其余文件的处理结果时,可使用this.loadMoudle(request: string, callback: function(err, source, sourceMap, module))去获取到依赖文件的处理结果。this.resolve
: 获取指定文件的完整路径,this.resolve(context: string, request: string, callback: function(err, result: string))this.addDependency
: 为当前处理文件添加依赖文件,以便依赖文件发生变化时从新调用Loader转换该文件,this.addDependency(file: string)this.addContextDependency
: 为当前处理文件添加依赖文件目录,以便依赖文件目录里文件发生变化时从新调用Loader转换该文件,this.addContextDependency(dir: string)this.clearDependencies
: 清除当前正在处理的文件的全部依赖this.emitFile
: 输出一个文件,使用的方法为this.emitFile(name: string, content: Buffer | string, sourceMap: {...})
源码第一行以下:json
let babel; try { babel = require("@babel/core"); } catch (err) { if (err.code === "MODULE_NOT_FOUND") { err.message += "\n babel-loader@8 requires Babel 7.x (the package '@babel/core'). " + "If you'd like to use Babel 6.x ('babel-core'), you should install 'babel-loader@7'."; } throw err; }
babel-loader依赖了@babel/core
,这就是安装babel-loader须要同时安装@babel/core
(一般会再安装babel-preset-env
、babel-plugin-transform-runtime
、babel-runtime
)的缘由。咱们接下去看,src/index.js
整个文件是否是按照咱们前面所讲编写Loader的方法来组织代码的。
//引入package.json const pkg = require("../package.json"); /* 根据babel-loader是否配置cacheDirectory属性来告诉 babel-loader是否缓存loader的执行结果,若是true, 便会使用cache方法去实现,`cache.js`文件有着read、write、filename(文件命名方法) 以及如何处理缓存的handleCache方法(有则读,无则写再读),有兴趣能够去看看。 */ const cache = require("./cache"); /* transfrom.js用来转换内容,内部调用了babel.transform方法进行转换,这里简单介绍一下babel的原理: babylon将es6/es7代码解析成ast,babel-traverse对ast进行转译,获得新的ast,新的ast经过 babel-generator转换成es5,核心方法在@babel/core/lib/transformation/index.js中的`runSync` 方法,有兴趣能够去了解一下。 */ const transform = require("./transform"); const injectCaller = require("./injectCaller"); const path = require("path"); // 获取Loader参数options const loaderUtils = require("loader-utils"); module.exports = makeLoader(); module.exports.custom = makeLoader; function makeLoader(callback) { const overrides = callback ? callback(babel) : undefined; return function(source, inputSourceMap) { // 上面介绍过的api能够得知,这是个异步Loader,作的是异步装换的工做 const callback = this.async(); loader .call(this, source, inputSourceMap, overrides) .then(args => callback(null, ...args), err => callback(err)); }; } async function loader(source, inputSourceMap, overrides) { .... }
能够看到确实和咱们Loader编写方式是同样的,经过module.exports = makeLoader();
导出一个函数,makeLoader()
是一个高阶函数,又返回了一个函数,经过const callback = this.async();
能够知道,这是一个异步的loader,不难看出最重要的实现都在这一步函数loader里面了,那么到底在loader函数里面究竟作了些什么呢?咱们来看看,在阅读源码前,最好先看看babel-loader
的README,先作个基本了解.
上面代码能够看出loader(source, inputSourceMap, overrides)
函数入参有三个,分别是source=>待转换的code
,inputSourceMap=>上一个loader处理后的sourceMap,有的话
,overrides=>自定义加载器
,整块源码能够分红几部分,
let loaderOptions = loaderUtils.getOptions(this) || {};
,获取options,而且获取当前处理转换的文件的路径this.resourcePath
。let override = require(loaderOptions.customize);
,有了override以后,后续逻辑(如转换、获取option)override都会进行介入处理。babel.loadPartialConfig
能够拿到babel配置并赋值给config变量,其实就是为了容许系统轻松操做和验证用户的配置,此功能解决了插件和预设 config.babelrc
不为空,则有.babelrc文件,依赖.babelrc文件变化,使用this.addDependency(config.babelrc);
每个Loader其实返回值就是一个Function,并且就是把带转换内容传入,获得转换后的内容,作的事情就是这样,这篇文章先对Loader的基本概念进行介绍,而且了解webpack为Loader的编写提供一些经常使用的API,最后经过简析babel-loader的源码,我以为应该差很少知道如何去写一个简单的Loader了,原文地址-我的博客。