写点什么

图解 VueLoader : .vue 文件是如何被打包的?

2021 年 6 月 20 日

图解 VueLoader : .vue 文件是如何被打包的?

一、整体概述


使用过 Vue 的同学,对于 .vue 单文件文件组件类型的文件(下文简称 SFC)应该不会陌生。SFC 文件需要通过构建工具(本文以 Webpack4 为例)打包成一个 Bundle,才能被识别和使用。那么这中间经历了什么、不同的代码块是如何被其他规则识别的、最终生成了什么?带着这些问题,且看下文一一道来。



▲图 1. SFC 经过 Webpack 打包后的产物是什么


1. SFC 的输入和输出


Webpack 需要增加 vue-loader 【1】和 vueLoaderPlugin 对 SFC 进行支持。我们首先聚焦到 vue-loader 的代码:入口文件为 lib/index.js ,入参 source 是 SFC 源码,经过处理逻辑后,输出 export default 的代码字符串。



【1】:vue-loader:

https://github.com/vuejs/vue-loader/blob/master/lib/index.js#L32




// vue-loader lib/index.js
// source 是我们写的 SFC 源码module.exports = function (source) { ... // 返回值 code 是一段 ESModule 代码字符串 let code = `...`; code += `\n export default component.exports`; return code;};
复制代码


用实际的例子操作一下, demo.vue 经过运行后得到下图的输出。经过观察可以发现,原有的 template ,变成了 import ... from './demo.vue?vue&type=template' ,其他代码块也发生了类似的变化。 ?vue&type=template 新增的 vue 和 type 参数的作用是什么?我们继续往下看。



▲图 2. SFC 被 vue-loader 转化后的结果


2. template、script、style 代码块切分


上个小节中,可以看到 template、script、style 代码块在输出结果中已经转化为对应的 import 逻辑。这一步是 vue-loader 调用了 @vue/component-compiler-utils 的 parse 函数进行解析后,分别生成了对应的 import 逻辑,相关源码如下:



// vue-loader lib/index.jsconst { parse } = require('@vue/component-compiler-utils');
module.exports = function (source) {
// 解析源码,得到描述符 const descriptor = parse({ source, ... });
// 如果 template 块存在 if (descriptor.template) { ... } // 如果 script 块存在 if (descriptor.script) { ... } // 如果 style 块存在(支持多 style 块) if (descriptor.styles.length) { ... } // Vue 还支持自定义块 if (descriptor.customBlocks && descriptor.customBlocks.length) { ... }
}
复制代码

下图是 demo.vue 被转化的流程图解:



▲图 3. 各个代码块被分别转化为相应的 import 逻辑


二、VueLoaderPlugin 的作用


前面的章节留了一个疑问, ?vue&type=template 的作用是什么?可以从 VueLoaderPlugin 【2】中找出答案,首先我们先了解 Webpack 中的 Plugin 能做什么。


【2】:VueLoaderPlugin:

https://github.com/vuejs/vue-loader/blob/master/lib/plugin-webpack4.js


Plugin 的特性


Plugin 的作用,主要有以下两条:


  • 能够 hook 到在每个编译(compilation)中触发的所有关键事件。

  • 在插件实例的 apply 方法中,可以通过 compiler.options 获取 Webpack 配置,并进行修改


VueLoaderPlugin 通过第二个特性,在初始化阶段,对 module.rules 进行动态修改。


VueLoaderPlugin 预处理


VueLoaderPlugin 的处理流程中,修改了 module.rules,在原来的基础上加入了 pitcher 和 cloneRules 。这一步的作用是:新增的 rule ,能识别形如 ?vue&type=template 的 querystring,让不同语言的代码块匹配到对应的 rule。



class VueLoaderPlugin { apply (compiler) { // 标识 VueLoaderPlugin 已被加载 // 可用于 vue-loader 运行时,检测 VueLoaderPlugin 的加载信息 if (webpack4) { ... } esle { ... }
// 重头戏,对 Webpack 配置进行修改 const rawRules = compiler.options.module.rules; const { rules } = new RuleSet(rawRules); ... // 替换初始 module.rules,在原有 rule 上,增加 pitcher、clonedRules compiler.options.module.rules = [ pitcher, ...clonedRules, ...rules ]; }}
复制代码



【3】:对资源的 querystring 进行匹配:

https://v4.webpack.docschina.org/configuration/module/#rule-resourcequery




▲图 4. VueLoaderPlugin 对 module.rules 的修改


三、回到 Loader


上一节梳理了 VueLoaderPlugin 在初始化阶段的预处理,这一节我们继续回到构建阶段中,看看以 VueLoader 为中心如何协调其它 Loader ,得到每个代码块的构建结果。同样地,我们先了解一下 Webpack 的 loader 特性。


  1. Webpack 的 loader 运行顺序


对于 loader ,我们知道它们的执行是有顺序的,如果是这样的配置,运行的顺序将是 c-loader -> b-loader -> a-loader。


module.exports = {  module: {    rules: [{      ...      use: ['a-loader', 'b-loader', 'c-loader'],    }],  },};
复制代码


不过,在实际(从右到左)执行 loader 之前,会先从左到右调用 loader 上的 pitch 方法。


|- 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 方法中,如果有实际的返回值,将会跳过后续的 loader,比如在 b-loader 的 pitch 中,如果返回了实际值,将会产生下面的执行顺序。



# 注意 a-loader 依然会正常执行,跳过的是 c-loader|- a-loader `pitch` |- b-loader `pitch` returns a module|- a-loader normal execution
复制代码


知道这个特性,有利于我们理解 SFC 中各代码块在 loader 中的处理顺序。


2. SFC 转化流程


还记得第一节生成的编译结果吗?每个代码块都导出了对应逻辑,我们以 script 块为例,结合第二节的 PitcherLoader 再次进行转化,转化后的结果为:



▲图 5. script 块的转化流程


最后的 import 语句,使用了内联方式的 import 语法【4】,我们拆分一下便于理解。



【4】:import 语法:

https://webpack.docschina.org/concepts/loaders/#inline



# 原import-!../../node_modules/babel-loader/lib/index.js??ref--2-0!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./demo.vue?vue&type=script&lang=js&";
# Part1 -! # 将禁用所有已配置的 preLoader 和 loader,但是不禁用 postLoaders
# Part2 ../../node_modules/babel-loader/lib/index.js??ref--2-0# 参考小节:VueLoaderPlugin 的预处理,demo.vue 会自动添加 .js 后缀,以匹配所有 js 的 Rule,这里使用 babel-loader 处理 js 模块
# Part3 ../../node_modules/vue-loader/lib/index.js??vue-loader-options # /\.vue$/ 规则匹配到 demo.vue,并使用 vue-loader 处理 .vue 后缀
# Part4 ./demo.vue?vue&type=script&lang=js&
复制代码


3. PitchLoader


上述的转化发生在 PitchLoader 中,对 PitchLoader 的实现逻辑感兴趣的同学,可以阅读 loader/pitcher.js 的源码:


// vue-loader lib/loaders/pitcher.js
// PitcherLoader.pitch 方法,所有带 ?vue 的模块请求,都会走到这里module.exports.pitch = function (remainingRequest) { // 如 ./demo?vue&type=script&lang=js // 此时,loaders 是所有能处理 .vue 和 .xxx 的 loader 列表 let loaders = this.loaders; ... // 得到 -!babel-loader!vue-loader! const genRequest = loaders => { ... };
// 处理 style 块 和 template 块,支持 if (query.type === 'style') { ... } if (query.type === 'template') { ... }
// 处理 script 块和 custom 块 return `import mod from ${request}; export default mod; export * from ${request}`;}
复制代码


4. 再次执行 VueLoader


细心的同学可能发现了,在 PitchLoader 的转化结果中,还是会以 vue-loader 作为第一个处理的 loader,但 vue-loader 不是一开始就转化过了吗 ?与第一次不同的是,这次 vue-loader 的作用,仅仅是把 SFC 中语法块的源码提取出来,并交给后面的 loader 进行处理



▲图 6. 第二次进入 vue-loader



// vue-loader lib/index.jsconst { parse } = require('@vue/component-compiler-utils');
module.exports = function (source) { // 如果querystring 包含了 type 参数,则直接返回该块的代码 if (incomingQuery.type) { return selectBlock( ... ); }};
// vue-loader lib/select.jsmodule.exports = function selectBlock (...) { // 选择不同语法块的内容进行返回}
复制代码


至此,vue-loader 里面的处理逻辑基本已经梳理完成。各部分代码块也传入后续的 loader 中进行解析和转化。


四、总结


我们再用一张完整的处理流程图总结一下 SFC 构建流程吧:




头图:Unsplash

作者:尹佳

原文:https://mp.weixin.qq.com/s/FJzDRLchG_DWA80Wp141Vg

原文:图解 VueLoader : .vue 文件是如何被打包的?

来源:云加社区 - 微信公众号 [ID:QcloudCommunity]

转载:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

2021 年 6 月 20 日 08:001

评论

发布
暂无评论
发现更多内容

面试无忧:源码+实践,讲到MySQL调优的底层算法实现

小Q

Java 数据库 学习 面试 算法

区块链开发落地,联盟链系统平台搭建

t13823115967

区块链 区块链开发落地 联盟链系统平台搭建

架构师训练营第十一周作业

邓昀垚

架构师训练营第十一周总结

邓昀垚

免费下载O’Reilly出版社全新之作《建立机器学习流水线》

计算机与AI

学习

CPU飙高问题排查

程序猿玄微子

利用 Arthas 解决启动 StandbyNameNode 加载 EditLog 慢的问题

阿里巴巴云原生

阿里云 开源 云原生 中间件 Java 25 周年

Nginx的反向代理与负载均衡--配置Nginx

Linux服务器开发

nginx 负载均衡 反向代理 后端开发 Linux服务器

Spock单元测试框架实战指南四 - 异常测试

Java老k

单元测试 spock

第六周学习总结

Griffenliu

我是如何使计算提速>150倍的

Lart

Python 代码优化 Numpy

架构师训练营第二周框架设计学习总结

Geek_xq

磁盘到底是怎样工作的?一文理解硬盘结构

Guanngxu

操作系统

Scala语法特性(三):面向对象的独特点

正向成长

特质 样例类 case class Traits

第六周作业

Griffenliu

JVM调优不知道怎么回答,阿里总结四大模块,学不会就背过来

小Q

Java 学习 架构 面试 JVM

使用 Go 实现 Async/Await 模式

Roc

go golang channel goroutines Async

Vim - 可能是投资回报率最高的 Editor

star_fx

vim

RocketMQ 很慢?引出了一个未解之谜

阿里巴巴云原生

开源 云原生 中间件 Java 25 周年 Arthas

架构师训练营第 1 期 - 第 10 周 - 学习总结

wgl

极客大学架构师训练营

三万字无坑搭建基于Docker+K8S+GitLab/SVN+Jenkins+Harbor持续集成交付环境!!

冰河

Docker 云原生 k8s

数字货币——货币的第四次革命

CECBC区块链专委会

数字货币

一枚程序猿的MacBook M1详细体验报告

Zhendong

《华为数据之道》读书笔记:第 6 章 面向“自助消费”的数据服务建设

方志

数据中台 数据仓库 数字化转型 数据治理

《华为数据之道》读书笔记:第 7章 打造“数字孪生”的数据全量感知能力

方志

数据中台 数字化转型

区块链政务系统开发解决方案

t13823115967

区块链+ 区块链开发落地 政务系统开发解决方案

Arthas 实践——生产环境排查 CPU 飚高问题

阿里巴巴云原生

开源 云原生 中间件 Java 25 周年 Arthas

顶层设计已基本完备 数字货币将进入加速推进阶段

CECBC区块链专委会

数字货币

甲方日常 59

句子

工作 随笔杂谈 日常

区块链如何助力精准扶贫?

CECBC区块链专委会

区块链 扶贫

阿里架构师花近三个月时间整理出来的Java独家面试题(Java岗)

Crud的程序员

Java 编程 架构 java面试

「中国技术开放日·长沙站」现场直播

「中国技术开放日·长沙站」现场直播

图解 VueLoader : .vue 文件是如何被打包的?-InfoQ