//webpack.config.jsmodule.exports={...module:{rules:[{test:/.vue$/,loader:'vue-loader'},{test:/.scss$/,//先经由 sass-loader,然后将结果传入 css-loader,末了再进入 style-loader。
use:['style-loader',//从JS字符串创建样式节点'css-loader',//把CSS翻译成CommonJS{loader:'sass-loader',options:{data:'$color:red;'//把Sass编译成CSS}}]}]}...}

方法2(右到左地被调用)

//moduleimportStylesfrom'style-loader!css-loader?modules!./styles.css';

当链式调用多个 loader 的时候,请记住它们会以相反的顺序实行。
取决于数组写法格式,从右向左或者从下向上实行。
像流水线一样,挨个处理每个loader,前一个loader的结果会通报给下一个loader,末了的 Loader 将处理后的结果以 String 或 Buffer 的形式返回给 compiler。

利用 loader-utils 能够编译 loader 的配置,还可以通过 schema-utils 进行验证

import{getOptions}from'loader-utils';import{validateOptions}from'schema-utils';constschema={//...}exportdefaultfunction(content){//获取optionsconstoptions=getOptions(this);//考验loader的options是否合法validateOptions(schema,options,'DemoLoader');//在这里写转换loader的逻辑//...returncontent;};content: 表示源文件字符串或者buffermap: 表示sourcemap工具meta: 表示元数据,赞助工具同步loader

同步 loader,我们可以通过return和this.callback返回输出的内容

webpack的htmlloaderwebpack的几个常见loader源码浅析着手实现一个md2htmlloader Java
(图片来自网络侵删)

module.exports=function(content,map,meta){//一些同步操作outputContent=someSyncOperation(content)returnoutputContent;}

如果返回结果只有一个,也可以直策应用 return 返回结果。
但是,如果有些情形下还须要返回其他内容,如sourceMap或是AST语法树,这个时候可以借助webpack供应的api this.callback

module.exports=function(content,map,meta){this.callback(err:Error|null,content:string|Buffer,sourceMap?:SourceMap,meta?:any);return;}

第一个参数必须是 Error 或者 null 第二个参数是一个 string 或者 Buffer。
可选的:第三个参数必须是一个可以被这个模块解析的 source map。
可选的:第四个选项,会被 webpack 忽略,可以是任何东西【可以将抽象语法树(abstract syntax tree - AST)(例如 ESTree)作为第四个参数(meta),如果你想在多个 loader 之间共享通用的 AST,这样做有助于加速编译韶光。
】。

异步loader

异步loader,利用 this.async 来获取 callback 函数。

//让Loader缓存module.exports=function(source){varcallback=this.async();//做异步的事doSomeAsyncOperation(content,function(err,result){if(err)returncallback(err);callback(null,result);});};

详情请参考官网API

开拓一个大略的md-loader

constmarked=require("marked");constloaderUtils=require("loader-utils");module.exports=function(content){this.cacheable&&this.cacheable();constoptions=loaderUtils.getOptions(this);try{marked.setOptions(options);returnmarked(content)}catch(err){this.emitError(err);returnnull}};

上述的例子是通过现成的插件把markdown文件里的content转成html字符串,但是如果没有这个插件,改怎么做呢?这个情形下,我们可以考虑其余一种解法,借助 AST 语法树,来帮忙我们更加便捷地操作转换。

利用 AST 作源码转换

markdown-ast是将markdown文件里的content转成数组形式的抽象语法树节点,操作 AST 语法树远比操作字符串要大略、方便得多:

//通过正则的方法把字符串处理成直不雅观的AST语法树constmd=require('markdown-ast');module.exports=function(content){this.cacheable&&this.cacheable();constoptions=loaderUtils.getOptions(this);try{console.log(md(content))constparser=newMdParser(content);returnparser.data}catch(err){console.log(err)returnnull}};

constmd=require('markdown-ast');consthljs=require('highlight.js');//代码高亮插件//利用AST作源码转换classMdParser{constructor(content){this.data=md(content);console.log(this.data)this.parse()}parse(){this.data=this.traverse(this.data);}traverse(ast){console.log("md转抽象语法树操作",ast)letbody='';ast.map(item=>{switch(item.type){case"bold":case"break":case"codeBlock":consthighlightedCode=hljs.highlight(item.syntax,item.code).valuebody+=highlightedCodebreak;case"codeSpan":case"image":case"italic":case"link":case"list":item.type=(item.bullet==='-')?'ul':'ol'if(item.type!=='-'){item.startatt=(`start=${item.indent.length}`)}else{item.startatt=''}body+='<'+item.type+item.startatt+'>\n'+this.traverse(item.block)+'</'+item.type+'>\n'break;case"quote":letquoteString=this.traverse(item.block)body+='<blockquote>\n'+quoteString+'</blockquote>\n';break;case"strike":case"text":case"title":body+=`<h${item.rank}>${item.text}</h${item.rank}>`break;default:throwError("error",`Nocorrespondingtreatmentwhenitem.typeequal${item.type}`);}})returnbody}}

md 转成抽象语树

ast抽象语法数转成html字符串

md2html-loader源码地址(https://github.com/6fedcom/fe-blog/blob/master/webpack-loader/loaders/md-loader.js)

loader的一些开拓技巧只管即便担保一个loader去做一件事情,然后可以用不同的loader组合不同的场景需求开拓的时候不应该在 loader 中保留状态。
loader必须是一个无任何副浸染的纯函数,loader支持异步,因此是可以在 loader 中有 I/O 操作的。
模块化:担保 loader 是模块化的。
loader 天生模块须要遵照和普通模块一样的设计原则。
合理的利用缓存 合理的缓存能够降落重复编译带来的本钱。
loader 实行时默认是开启缓存的,这样一来, webpack 在编译过程中实行到判断是否须要重编译 loader 实例的时候,会直接跳过 rebuild 环节,节省不必要重修带来的开销。
但是当且仅当有你的 loader 有其他不稳定的外部依赖(如 I/O 接口依赖)时,可以关闭缓存:

this.cacheable&&this.cacheable(false);loader-runner 是一个非常实用的工具,用来开拓、调试loader,它许可你不依赖 webpack 单独运行 loadernpm install loader-runner --save-dev

//创建run-loader.jsconstfs=require("fs");constpath=require("path");const{runLoaders}=require("loader-runner");runLoaders({resource:"./readme.md",loaders:[path.resolve(__dirname,"./loaders/md-loader")],readResource:fs.readFile.bind(fs),},(err,result)=>(err?console.error(err):console.log(result)));

实行 node run-loader

认识更多的loader

style-loader源码简析

浸染:把样式插入到DOM中,方法是在head中插入一个style标签,并把样式写入到这个标签的 innerHTML 里 看下源码。

先去掉option处理代码,这样就比较清晰明了了

返回一段js代码,通过require来获取css内容,再通过addStyle的方法把css插入到dom里 自己实现一个简陋的style-loader.js

module.exports.pitch=function(request){const{stringifyRequest}=loaderUtilsvarresult=[//1. 获取css内容。
2.//调用addStyle把CSS内容插入到DOM中(locals为true,默认导出css)'varcontent=require('+stringifyRequest(this,'!!'+request)+')’,'require('+stringifyRequest(this,'!'+path.join(__dirname,"addstyle.js"))+')(content)’,'if(content.locals)module.exports=content.locals’]returnresult.join(';')}

须要解释的是,正常我们都会用default的方法,这里用到pitch方法。
pitch 方法有一个官方的阐明在这里 pitching loader。
大略的阐明一下便是,默认的loader都是从右向左实行,用 pitching loader 是从左到右实行的。

{test:/\.css$/,use:[{loader:"style-loader"},{loader:"css-loader"}]}

为什么要先实行style-loader呢,由于我们要把css-loader拿到的内容终极输出成CSS样式中可以用的代码而不是字符串。

addstyle.js

module.exports=function(content){letstyle=document.createElement("style")style.innerHTML=contentdocument.head.appendChild(style)}babel-loader源码简析

首先看下跳过loader的配置处理,看下babel-loader输出

上图我们可以看到是输出transpile(source, options)的code和map 再来看下transpile方法做了啥

babel-loader是通过babel.transform来实现对代码的编译的, 这么看来,以是我们只须要几行代码就可以实现一个大略的babel-loader

constbabel=require("babel-core")module.exports=function(source){constbabelOptions={presets:['env']}returnbabel.transform(source,babelOptions).code}

vue-loader源码简析

vue单文件组件(简称sfc)

<template> <div class="text"> {{a}} </div></template><script>export default { data () { return { a: "vue demo" }; }};</script><style lang="scss" scope>.text { color: red;}</style>

webpack配置

constVueloaderPlugin=require('vue-loader/lib/plugin')module.exports={...module:{rules:[...{test:/\.vue$/,loader:'vue-loader'}]}plugins:[newVueloaderPlugin()]...}

VueLoaderPlugin浸染:将在webpack.config定义过的其它规则复制并运用到 .vue 文件里相应措辞的块中。
plugin-webpack4.js

constvueLoaderUse=vueUse[vueLoaderUseIndex]vueLoaderUse.ident='vue-loader-options'vueLoaderUse.options=vueLoaderUse.options||{}//cloneRule会修正原始rule的resource和resourceQuery配置,//携带分外query的文件路径将被运用对应ruleconstclonedRules=rules.filter(r=>r!==vueRule).map(cloneRule)//globalpitcher(responsibleforinjectingtemplatecompilerloader&CSS//postloader)constpitcher={loader:require.resolve('./loaders/pitcher'),resourceQuery:query=>{constparsed=qs.parse(query.slice(1))returnparsed.vue!=null},options:{cacheDirectory:vueLoaderUse.options.cacheDirectory,cacheIdentifier:vueLoaderUse.options.cacheIdentifier}}//更新webpack的rules配置,这样vue单文件中的各个标签可以运用clonedRules干系的配置compiler.options.module.rules=[pitcher,...clonedRules,...rules]

获取webpack.config.js的rules项,然后复制rules,为携带了?vue&lang=xx...query参数的文件依赖配置xx后缀文件同样的loader 为Vue文件配置一个公共的loader:pitcher 将[pitchLoder, ...clonedRules, ...rules]作为webapck新的rules。

再看一下vue-loader结果的输出

当引入一个vue文件后,vue-loader是将vue单文件组件进行parse,获取每个 block 的干系内容,将不同类型的 block 组件的 Vue SFC 转化成 js module 字符串。

// vue-loader利用`@vue/component-compiler-utils`将SFC源码解析成SFC描述符,,根据不同 module path 的类型(query 参数上的 type 字段)来抽离 SFC 当中不同类型的 block。
const{parse}=require('@vue/component-compiler-utils')//将单个.vue文件内容解析成一个descriptor工具,也称为SFC(Single-FileComponents)工具//descriptor包含template、script、style等标签的属性和内容,方便为每种标签做对应处理constdescriptor=parse({source,compiler:options.compiler||loadTemplateCompiler(loaderContext),filename,sourceRoot,needMap:sourceMap})//为单文件组件天生唯一哈希idconstid=hash(isProduction?(shortFilePath+'\n'+source):shortFilePath)//如果某个style标签包含scoped属性,则须要进行CSSScoped处理consthasScoped=descriptor.styles.some(s=>s.scoped)

然后下一步将新天生的 js module 加入到 webpack 的编译环节,即对这个 js module 进行 AST 的解析以及干系依赖的网络过程。

来看下源码是怎么操作不同type类型(template/script/style)的,selectBlock 方法内部紧张便是根据不同的 type 类型,来获取 descriptor 上对应类型的 content 内容并传入到下一个 loader 处理

这三段代码可以把不同type解析成一个import的字符串

import{render,staticRenderFns}from"./App.vue?vue&type=template&id=7ba5bd90&"importscriptfrom"./App.vue?vue&type=script&lang=js&"exportfrom"./App.vue?vue&type=script&lang=js&"importstyle0from"./App.vue?vue&type=style&index=0&lang=scss&scope=true&"

总结一下vue-loader的事情流程

注册VueLoaderPlugin 在插件中,会复制当前项目webpack配置中的rules项,当资源路径包含query.lang时通过resourceQuery匹配相同的rules并实行对应loader时 插入一个公共的loader,并在pitch阶段根据query.type插入对应的自定义loader加载.vue时会调用vue-loader .vue文件被解析成一个descriptor工具,包含template、script、styles等属性对应各个标签, 对付每个标签,会根据标签属性拼接src?vue&query引用代码,个中src为单页面组件路径,query为一些特性的参数,比较主要的有lang、type和scoped 如果包含lang属性,会匹配与该后缀相同的rules并运用对应的loaders 根据type实行对应的自定义loader,template将实行templateLoader、style将实行stylePostLoader在templateLoader中,会通过vue-template-compiler将template转换为render函数,在此过程中, 会将传入的scopeId追加到每个标签的segments上,末了作为vnode的配置属性通报给createElemenet方法, 在render函数调用并渲染页面时,会将scopeId属性作为原始属性渲染到页面上在stylePostLoader中,通过PostCSS解析style标签内容参考文献webpack官网loader api(https://www.webpackjs.com/api/loaders/)手把手教你写webpack yaml-loader(https://mp.weixin.qq.com/s/gTAq5K5pziPT4tmiGqw5_w)言川-webpack 源码解析系列(https://github.com/lihongxun945/diving-into-webpack)从vue-loader源码剖析CSS Scoped的实现(https://juejin.im/post/5d8627355188253f3a70c22c)