webpack 路径查找-谁选择放弃 Webpack? ——vite原理解析

明天我会分享一篇关于vite的文章。

文章推荐:三元小伙伴最近也关注了Vue3,不过我更感兴趣的是游大新设计的一个小工具——vite。 大家都知道webpack打包的时候有两个阶段:编译和打包,但是打包之后就会出现一个问题,就是随着模块的增加,bundle的体积会过大,从而导致发热更新速度明显减慢。 vite的诞生就是为了解决这个问题。 当模块越来越多时,热更新速度不会减慢。 其实有一说一,这只是Vue项目开发阶段的一个工具,其他场景还是需要依赖强大的Webpack。 vite并不是万能的。

另外值得一提的是,vite也正式应用于博客搭建系统vuepress中,解决热更新慢的问题。 以下为裕达原话:

迫不及待地想了解这个工具是如何工作的? 最近看到一篇不错的文章,把原理解释的比较清楚。 不过需要说明的是,vite项目更新速度非常快,每天都在更新。 因此,本文涉及的代码与最新的代码会有些不同,但原理和思想还是一样的。 想了解更多细节变化,小伙伴们webpack 路径查找,我们直接看源代码吧。 源码地址:.

以下为文章具体内容:

本文同步于雄鹿博主个人博客shymean.com:《橙色时代》,欢迎关注。 鹈鹕原文链接:

三天前webpack 路径查找,友达在Vue3.0beta直播中提到了一个vite工具。 它的描述是:一个Vue单页面组件的非封装开发服务器,可以直接在浏览器中运行请求的Vue文件。 我对它的原理更感兴趣。 兴趣,所以体验了一下,写了这篇文章,主要包括vite的实现原理分析和一些思考。

初步知识

vite稍微依赖modulesciprt的特性,所以需要提前做好功课,参考:JavaScriptmodules module-MDN。

moduleciprt 允许本机支持的模块直接在浏览器中运行

    // index.js可以通过export导出模块,也可以在其中继续使用import加载其他依赖     import App from './index.js'

当遇到导入依赖时,会直接发起HTTP请求对应的模块文件。

开发环境

本文使用的版本是vite@0.3.2,附上github项目地址~这个项目其实每天都在更新

首先克隆存储库

git clone https://github.com/vuejs/vitecd vite && yarn

环境安装完毕后,在项目下创建examples目录,添加index.html和Comp.vue文件,这里直接使用README.md中的examples

第一个是 inindex.html

import { createApp } from 'vue'import Comp from './Comp.vue'
createApp(Comp).mount('#app')

之后是“Comp.vue”



export default { data: () => ({ count: 0 })}
button { color: red }

然后在examples目录下运行

../bin/vite.js

可以在浏览器中打开预览:3000,同时支持文件热更新~

如果需要调试源码,只要启动npmrundev,它就会打开tsc-w--p来窃听src目录下的变化,并实时输出到dist目录下,然后就可以启动快乐源码了打码时间~

入口文件

目前这个项目迭代非常频繁(中间件historyFallbackMiddleware今天还存在,明天好像就没有了),大概的实现思路应该基本确定了,所以先确定本次源码阅读的目标:了解如何打包而不用使用webpack等工具的前提下直接运行vue文件。 基于这个目的,主要是了解实现思路,理清整体结构,而不是屈服于具体细节。

从入口 bin/vite.js 开始

const server = require('../dist/server').createServer(argv)

可以看到createServer方法,直接定位到src/server/client.tx。 Vite使用Koa构建服务器。 createServer中主要通过中间件注册相关函数。

// src/index.ts// 提前预告这四个插件的作用const internalPlugins: Plugin[] = [  modulesPlugin, // 处理入口html文件script标签和每个vue文件的模块依赖  vuePlugin, // vue单页面组件解析,将template、script、style解析成不同的响应内容,可以理解为简易版的vue-loader  hmrPlugin, // 使用websocket实现文件热更新  servePlugin // koa配置插件,目前看来主要是配置协商缓存相关]
export function createServer({ root = process.cwd(), middlewares: userMiddlewares = []}: ServerConfig = {}): Server { const app = new Koa() const server = http.createServer(app.callback()) // 预留了userMiddlewares方便提供后续API ;[...userMiddlewares, ...middlewares].forEach((m) => m({ root, app, server }) )
return server}

Vite通过下面的中间件注册koa中间件,

export const modulesPlugin: Plugin = ({ root, app }) => {  // 每个插件实际上是注册koa中间件  app.use(async (ctx, next) => {})}

看起来和Vue2的源码结构类似,通过装饰器逐步添加功能~目前只需要明确这四个插件的功能即可。

// vue2源码结构initMixin(Vue)stateMixin(Vue)eventsMixin(Vue)lifecycleMixin(Vue)renderMixin(Vue)

模块解析器中间件

这个中间件的作用是编译index.html和SFC文件的内容,并处理相关的依赖关系。

比如前面的html文件的script标签内容会被编译成

import { createApp } from '/__modules/vue'// 之前是import { createApp } from 'vue'import Comp from './Comp.vue'
createApp(Comp).mount('#app')

这样,当浏览器解析并运行该模块类型的script标签时,就会请求对应的模块文件,其中

对于入口文件,需要script标签下的相关依赖。 对于单页面组件,vue-loader中,tmplate、script、style标签也需要处理; 实际上,此类依赖项将作为 css 和 js 文件请求加载。

单页组件主要包括template、script、style标签,script标签中代码的导入会被编译成

// 加载热更新模块客户端,后面会提到import "/__hmrClient"
let __script; export default (__script = { data: () => ({ count: 0 })})// 根据type进行区分,样式文件type=styleimport "/Comp.vue?type=style&index=0"// 保留css scopeID__script.__scopeId = "data-v-92a6df80"// render函数文件type=templateimport { render as __render } from "/Comp.vue?type=template"__script.render = __render__script.__hmrId = "/Comp.vue"

style 和 template 标签将被重写为 /Comp.vue?type=xxx,并且将重新发送 http 请求。 这种通过query参数来区分并加载SFC文件各模块内容的方法,与vue-loader中webpack中的resourceQuery配置处理方式与前三种相同。 如果了解vue-loader运行原理的朋友听到这里,可能已经意识到了。 我从vue-loader的源码中写了一篇关于CSS-Scoped的实现的文章。 上面也介绍了vue-loader的大致原理。

回到vite,现在我们知道了moduleResolverMiddleware的作用,主要是重画模块路径,通过查询参数区分SFC文件的依赖关系,方便浏览器通过url加载实际模块。打开浏览器控制台查看特定文件请求

Vue插件

后面提到,单页面组件的模板和样式会被处理成单独的导入路径,通过query.type来区分,那么当服务器收到对应的url请求时,如何返回正确的资源内容呢? 答案就在第二个插件VuePlugin中。

单页文件的请求有一个特点,就是以*.vue结尾作为请求路径。 服务器收到具有这些特征的http请求时,主要进行处理

{ filename: '/Users/Txm/source_code/vite/examples/Comp.vue', template: {   type: 'template',   content: 'n  n',   loc: {     source: 'n  n',     start: [Object],     end: [Object]   },   attrs: {},   map: {     version: 3,     sources: [Array],     names: [],     mappings: ';AACA',     file: '/Users/Txm/source_code/vite/examples/Comp.vue',     sourceRoot: '',     sourcesContent: [Array]   } }, script: {   type: 'script',   content: 'nexport default {n  data: () => ({ count: 0 })n}n',   loc: {     source: 'nexport default {n  data: () => ({ count: 0 })n}n',     start: [Object],     end: [Object]   },   attrs: {},   map: {     version: 3,     sources: [Array],     names: [],     mappings: ';AAKA;AACA;AACA',     file: '/Users/Txm/source_code/vite/examples/Comp.vue',     sourceRoot: '',     sourcesContent: [Array]   } }, styles: [   {     type: 'style',     content: 'nbutton { color: red }n',     loc: [Object],     attrs: [Object],     scoped: true,     map: [Object]   } ], customBlocks: []}

回去整理一下流程

// Comp.vue返回的文件内容,可以看见跟入口文件的script标签内容比较相似import { updateStyle } from "/__hmrClient"
const __script = { data: () => ({ count: 0 })}// style标签内容解析后的css代码updateStyle("92a6df80-0", "/Comp.vue?type=style&index=0")__script.__scopeId = "data-v-92a6df80"// temlpate标签内容解析后的renderimport { render as __render } from "/Comp.vue?type=template"__script.render = __render__script.__hmrId = "/Comp.vue"export default __script

每个标签的内容解析完毕后,会通过LRUCache进行缓存,方便最后复用

export const vueCache = new LRUCache({  max: 65535})

至此,我们对vite如何通过koa直接运行vue文件有了一个大概的了解。 它的思想和vue-loader类似。 它使用modulescript来处理文件依赖关系,然后在解析单个页面文件后拼接不同的query.types来处理每个资源。 文件,最后响应浏览器进行渲染。

hmr插件

后面会提到,vite还支持文件的热更新。 既然没有使用webpack,那么应该怎么做呢? 答案是自己实现一个哈哈哈~

热更新主要通过webSocket实现,包括ws服务器和ws客户端,hmrPlugin主要负责ws服务器部分,ws客户端在src/client中实现。 当导入“/__hmrClient”时,服务器和客户端关联起来。

目前主要定义了以下消息类型

当文件发生变化时,服务器会在handleVueSFCReload方法中根据变化的类型推送不同的消息。 当客户端收到相应的消息后,会结合vue.HMRRuntime进行处理或者重新加载新的资源。

这里还有很多TODO需要热更新。 我觉得这是学习热更新原理的一个很好的案例。 先编码一下,然后再回去看一遍。

关于热更新的原理,社区里有很多原理分析,不妨一步步看下去

服务插件

该插件主要用于实现一些koa请求和响应配置。

经过前面的分析,每次请求时,都会从入口文件开始,依次分析各个依赖关系。

在上一步中,很明显,对于每个vue文件,都会发送多个http请求,然后搜索和解析的操作非常频繁。 如果不配置缓存的话,服务器的性能负担会比较大,koa-conditional -get 和 koa-etag 应该是解决这个问题的,不过好像还没有实现。

概括

至此,vite源码的基本阅读已经完成,因为本地阅读源码的主要目的是了解整个工具的实现原理和大致功能,所以还没有深入理解各个实现细节功能。 几个重要的方法包括rewriteImports、compileSFCMain、compileSFCTemplate、compileSFCStyle、updateStyle等没有展示具体代码实现,主要收获是理解

当我第一次看到vite的介绍时,我就觉得这会是一个非常有趣的工具。 虽然还没上映,但我还是不忍心看。我觉得主要作用是

目前看来,vite 还是缺少打包等重要功能,应该无法替代 webpack 等工具。 但我觉得vite不应该用来替代现有的开发工具,所以我大概不会添加打包之类的功能~