什么是Loader?继前两篇文章(Part1,Part2)介绍了webpack的工作原理之后,我们了解到Loader:模块转换器,即根据需求加载模块的内容替换成新的内容,而每个Loader的职责单一,只能完成一次转换,所以我们一般会通过多个Loader链式处理源文件。然后得到我们想要的结果。那么Loader只需要关心输入输出即可。Loader实际上是一个Node.js模块,它导出了一个函数(也就是说,我们可以使用所有的node.jsAPI),如下:module.exports=function(source){//对source做一系列的转换下面先介绍下webpack提供的供Loader调用的API,对Loader有更深入的了解,然后分析babel-loader的源码,看看我们常用的loader是怎么写的。获取Loader的选项constloaderUtils=require('loader-utils');module.exports=function(source){//获取当前Loader用户传入的optionsconsole.log(loaderUtils.getOptions(this));返回来源;}返回其他结果如上,我们返回转换后的内容,但是在某些情况下,我们不仅需要返回转换后的内容,还需要返回一些其他的内容,比如sourceMap或者AST语法树,那么这个时候我们可以使用webpack提供的APIthis.callback。在使用this.callback的时候,我们必须在Loader函数中返回undefined,让webpack知道返回的结果在this.callback中。API详细参数如下:callback(//无法替换原内容时报错err:Error||null,//替换后的内容,如上面sourcecontent:string|Buffer,//用于获取替换后的内容的SourceMap原来的内容是为了方便调试//我们明白我们只在开发环境中使用SourceMap,所以会变得可控,//webpack也提供了this.sourceMap来告诉是否使用sourceMap,//当然你也可以使用loader的option做判断,比如css-loadersourceMap?:SourceMap,//如果这次转换同时生成了ast语法树,也可以返回这个ast,这样后面的loader需要重用ast,所以可以提高性能abstractSyntaxTree?AST);synchronous和asynchronous看this.asyncAPI下异步Loader是如何实现的,module.exports=asyncfunction(来源e){常量回调=this.async();const{err,content,sourceMap,AST}=awaitFunc();回调(错误,内容,sourceMap,AST);//与appeal`this.callback`参数相同}处理二进制数据像file-loader这样的Loader处理二进制数据,所以你需要告诉webpack将二进制格式的数据传递给加载器。代码可以如下:module.exports=function(source){if(sourceinstanceofBuffer){//一系列操作returnsource;//当然我也可以返回二进制数据给下一个loader}}moudle.exports.raw=true;//如果不设置,会通过moudle.exports获取字符串.raw=true;通知webpack需要二进制数据缓存的最佳点来加速优化。您可以使用this.cacheable(Boolen)来缓存加载程序转换的内容。当处理文件或依赖文件没有变化时,使用缓存转换内容更快!其他API说到学习,当然是越系统越好,介绍的API也多一些。除了上面常用的API,还有下面常用的API。this.context:当前转换文件所在目录this.resource:当前处理文件的完整请求路径,包含querystringthis.resourcePath:当前处理文件路径this.resourceQuery:当前处理文件的querystringfilethis.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:为当前处理的文件添加依赖文件so发生依赖文件时RecallLoader转换文件,this.addDependency(file:string)this.addContextDependency:为当前处理文件添加一个依赖文件目录,这样当依赖文件目录下的文件发生变化时,再次调用loader转换文件,this.addContextDependency(dir:string)this.clearDependencies:清除当前正在处理的文件的所有依赖this.emitFile:输出一个文件,使用的方法是this.emitFile(name:string,content:Buffer|string,sourceMap:{...})babel-loader源码简要分析源码第一行如下:letbabel;尝试{babel=require("@babel/core");}catch(err){if(err.code==="MODULE_NOT_FOUND"){err.message+="\nbabel-loader@8需要Babel7.x('@babel/core'包)。"+“如果你想使用Babel6.x(‘babel-core’),你应该安装‘babel-loader@7’。”;}抛出错误;}babel-loader依赖@babel/core,也就是说安装babel-loader需要同时安装@babel/core(通常是babel-preset-env,babel-plugin-transform-runtime,babel-runtime),我们看看src/index.js整个文件是不是按照我们前面说的写Loader的方式来组织代码的。//引入package.jsonconstpkg=require("../package.json");/*根据babel-loader是否配置了cacheDirectory属性,告诉babel-loader是否缓存loader的执行结果。如果为true,会使用cache方法来实现,`cache.js`文件有read、write、filename(文件命名方式)以及如何处理cache的handleCache方法(有则读,有则写、读)是没有),有兴趣的可以看看。*/constcache=require("./cache");/*transfrom.js用于转换内容,内部调用babel.transform方法进行转换。这里简单介绍一下babel的原理:babylon将es6/es7代码解析成ast,babel-traverse将ast翻译成新的ast,新的ast通过babel-generator转换成es5,核心方法是`runSync`@babel/core/lib/transformation/index.js中的方法,感兴趣的可以去了解一下。*/consttransform=require("./transform");constinjectCaller=require("./injectCaller");constpath=require("path");//获取Loader参数选项constloaderUtils=require("loader-utils");module.exports=makeLoader();module.exports.custom=makeLoader;functionmakeLoader(callback){constoverrides=callback?回调(通天塔):未定义;returnfunction(source,inputSourceMap){//上面介绍的api可以知道这是一个asynchronousLoader,做异步加载。constcallback=this.async();loader.call(this,source,inputSourceMap,overrides).then(args=>callback(null,...args),err=>callback(err));};}asyncfunctionloader(source,inputSourceMap,overrides){....}可以看到确实和我们的Loader写法一样,通过module.exports=makeLoader();导出一个函数,makeLoader()是一个高阶函数,返回一个函数,通过constcallback=this.async();可以知道这是一个异步加载器,不难看出最重要的这一步的实现是在函数加载器中,那么加载器函数中到底做了什么?让我们来看看。在阅读源码之前,最好看一下babel-loader的README,有个基本的了解。上面的代码表明loader(source,inputSourceMap,overrides)函数有三个入参一、source=>要转换的代码,inputSourceMap=>前面loader处理过的sourceMap,如果有的话,overrides=>customloader,整个源码可以分成几个部分,letloaderOptions=loaderUtils.getOptions(this)||{};,获取options,获取当前转换文件的路径this.resourcePath来判断是否自定义loader转换,这里会对options.customize做一系列的判断,options.customize一个相对路径,当参数loader函数的overrides为空,才会生效。执行letoverride=require(loaderOptions.customize);override可用后,后续的逻辑(如转换,option获取)override都会参与到流程中。合并函数输入参数和LoaderOptions得到programmaticOptions。调用babel.loadPartialConfig可以获取babel配置并赋值给config变量。其实就是为了让系统方便的操作和验证用户的配置。该功能解决了插件和预设生成cacheIdentifier来判断options.cacheDirectory是否需要缓存Loader转换内容的问题,如果为true则调用cache.js的module.exportCache方法(上面有介绍)如果config.babelrc不为空,会有一个.babelrc文件,根据.babelrc文件的变化,使用this.addDependency(config.babelrc);metadataSubscribers订阅Metadata,主要作用是在编译时订阅一些元数据过程。订阅后,这些元数据将被添加到webpack的上下文中。平时我们用不到,估计在一些babel-plugins中可能会用到。最后将处理后的结果返回一个summary。每个Loader的返回值其实都是一个Function,就是传入转换后的内容来获取转换后的内容。这就是它的作用。本文首先介绍Loader的基本概念,并了解到webpack为Loader的编写提供了一些常用的API。最后通过简单分析一下babel-loader的源码,我想我应该差不多知道怎么写一个简单的Loader了。原文地址——个人博客。
