• Vite实现原理


    Vite实现原理

    1.Vite

    接下来再来回顾一下Vite,之前已经演示过Vite的基本使用,这里的重点是来了解Vite的核心实现原理。先来回顾一下Vite的概念,Vite是一个面向现代浏览器的一个更轻、更快的web应用开发工具,它基于ECMAScript标准的原生模块系统ES Module来实现的。

    它的出现是为了解决外webpack在开发阶段使用webpack-dev-server冷启动时间过长,另外webpack-HMR热更新反应速度慢的问题。使用Vite创建的项目就是一个普通的Vue3的应用,没有太多特殊的地方,相比基于Vue cli创建的项目也少了很多的配置文件和依赖。

    Vite创建的项目开发依赖非常的简单,只有vite和@vue/compiler-sfc。Vite就是接下来要模拟实现的命令航工具,vue/compiler-sfc就是用来编译项目中的.vue结尾的单文件组件,vue2中使用的是vue/template-compiler。这里需要注意的是,Vite目前只支持Vue3.0的版本,在创建项目的时候通过指定使用不同的模板,也可以支持其他的框架。

    Vite项目中提供了两个子命令,vite servevite buildvite serve开启一个用于开发的web服务器,在启动服务器的时候不需要编译所有的代码文件,启动速度非常的快。

    在运行vite serve的时候,不需要打包,直接开启一个web服务器,当浏览器请求服务器,比如请求一个单文件组件,这个时候在服务器端编译单文件组件,然后把编译的结果返回给浏览器。注意,这里的编译是在服务器端,另外模块的处理是在请求到服务器端处理的。

    再来回顾一下vue-cli创建的应用,它在启动开发的web服务器的时候使用的是vue-cli-service-serve,当运行vue-cli-service-serve的时候,它内部会使用webpack首先去打包所有的模块。如果模块比较多的话,打包速度会非常的慢,把打包的结果存储到内存中,然后才会开启开发的外部服务器浏览器,请求外部服务器把内存中打包的结果直接返回给浏览器。

    webpack这类工具,它的做法是将所有的模块提前编译打包进bundle里。换句话说,不管模块是否被执行,是否使用到,都要被编译和打包到bundle里。随着项目越来越大,打包后的帮也越来越大,打包的速度自然也就越来越慢。

    Vite利用现代浏览器原生支持的ES Module模块化的特性,省略了对模块的打包,对于需要编译的文件,比如单文件、组件、样式模块等为采用的另一种模式,即时编译。也就是说只有具体去请求某个文件的时候,才会在服务端编译这个文件。所以这种即时编译的好处主要体现在按需编译速度会更快。

    Vite默认也支持HMR模块热更新,相对于webpack中的HMR,性能更好,因为Vite只需要立即编译当前所修改的文件即可,所以响应速度非常快。而webpack修改某个文件过后会自动以这个文件为入口重新build一次,所有涉及到的依赖也会被重新加载一次,所以反应速度稍微会慢一些。

    • Vite HMR
      • 立即编译当前所修改的文件
    • Webpack HMR
      • 会自动以这个文件为入口重写build一次,所有的涉及到的依赖也都会被加载一遍

    Vite在生产模式下打包需要使用vite build的命令,这个命令内部采用的是rollup进行打包,最终还是会把文件都提前编译并打包到一起。对于代码切割的需求,vite它内部采用的是原生的动态导入的特性实现的,所以打包结果还是只能够支持现代浏览器,不过动态导入特性是有相应的Polyfill的。

    那随着Vite的出现,引发了另外一个值得思考的问题,究竟有没有必要去打包应用呢?之前使用webpack进行打包,会把所有的模块打包到一个bundle.js中,主要有两个原因,第一是浏览器环境并不支持模块化,第二是零散的模块文件会产生大量的http请求。

    先来看第一个问题,浏览器环境并不支持模块化。随着现在浏览器对ES Module标准支持的逐渐完善,第一个问题已经慢慢不存在了,现阶段绝大多数浏览器都是支持ES Module特性的。

    再来看第二个问题,之前打包还有一个目的,就是当JS文件比较多的时候,每个JS文件都要发送一次请求,每个请求都要创建一个连接,那为了减少请求服务器的次数,所以打包成一个文件。这个问题HTTP已经帮我们解决,它可以复用链接。

    下面看一下浏览器对ES Module的支持情况。

    Vite创建的项目几乎不需要额外的配置,默认就支持TypeScript以及CSS的预编译器,但是需要单独安装对应的编译器以及支持JSX和Web Assembly。其他的future你可以打开官网来查看。

    Vite带来的优势主要体现在提升开发者在开发过程中的体验。外部开发服务器不需要等待,可以立即启动。另外模块热更新几乎是实时的,所需的文件是按需编译,避免编译用不到的文件,还有开箱即用,避免各种loader以及plugin的配置。

    2.Vite 实现原理-静态Web服务器

    接下来通过实现一个自己的Vite工具来深入了解Vite的工作原理。先来回顾一下Vite的核心功能。Vite的核心功能包括开启一个静态的web服务器,并且能够编译单文件组件,而且提供HMR的功能。

    当启动Vite的时候,首先会将当前项目目录作为静态文件服务器的根目录。静态文件服务器会拦截部分请求,例如当请求单文件组件的时候,会实施编译以及处理其他浏览器不能识别的模块。通过webSocket实现HMR,这个功能会跳过。下面首先来实现一个能够开启静态web服务器的命令行工具。Vite内部使用的是koa来开启静态web服务器。

    项目目录:

    index.js

    #!/usr/bin/env node
    const Koa = require('koa')
    const send = require("koa-send")
    
    const app = new Koa()
    
    // 1.静态文件服务器
    app.use(async (ctx, next) => {
      await send(ctx, ctx.path, { root: process.cwd(), index: 'index.html' })
      await next()
    })
    
    app.listen(3000)
    console.log('Server 3000')
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    要基于koa来开发一个静态的we部服务器,所以index.js这里先来导入所需要的两个模块,koakoa-send。然后再来创建一个koa的实例,接下来使用koa开发静态web服务器,默认返回根目录中的index.html

    先来创建一个中间键,它负责去处理静态文件,默认加载当前目录下,也就是运行该命令行工具目录中的index.html

    接下来直接调用send,把index.html面返回给浏览器,当然返回的是当前目录,也就是运行该命令行工具的这个目录下的index.html。直接调用send的函数,第一个参数是ctx上下文,第二个参数是ctx.path当前请求的路径。第三个参数要去配置根目录,当前web服务器的根目录是root: process.cwd(),接着要设置默认的页面index,默认的页面就是index.html。返回静态页面就写完了,因为是中间键,所以还要调用一下next

    接下来需要去监听端口3000

    到这里静态服务器就暂时写完了来测试一下,测试之前我们先来npm link一下,它会把当前这个项目链接到npm这个安装目录里边来,下面打开一个基于Vue3开发的项目,然后在终端里边只需要输入my-vite-cli,也就是开发的这个命令行工具。

    因为还没处理完,页面上什么都没看到。打开console控制台,这里有个错误,它告诉我们解析vue模块的时候失败了。我们使用import导入模块的时候,这里要求使用相对路径。

    再切换到network,刷新一下来看main.js,这次请求这里导入了vue,但导入vue的时候,它后边的路径没有/、./、../,所以浏览器不识别,我们希望他去node_modules文件夹去加载vue,这是打包工具的默认行为,但是浏览器不支持。想要解决这个问题的话,来看一下Vite中是如何处理的。

    当浏览器从Vite开启的外部服务器加载main.js的时候,首先会去处理加载第三方模块的路径,所以在服务器端要手动来处理这个路径的问题。当请求一个模块的时候,比如当前请求的main.js这个模块,要把该模块中加载第三方模块的import中的路径做一个处理,让它加载一个不存在的路径,这里的/@modules/vue.js

    再来看一下这次请求的headers,在响应头中可以找到contentType,它的值是application/javascript,它的作用是告诉浏览器返还的文件是一个JavaScript文件。所以一会可以在web服务器输出之前先判断一下当前返回的文件是否是js文件,如果是的话,再来处理里边的第三方模块的路径,然后再去请求/@modules/vue.js的时候,这个路径在服务器上是根本不存在的,没有@modules文件夹,需要在服务器上要去处理这个请求,去node_modules去加载的vue模块。

    好,这是我们的思路,那到这里我们加载index.html的静态服务器其实就搞定了,稍后再来处理加载第三方模块的问题。

    3.Vite 实现原理-修改第三方模块的路径

    接下来处理请求第三方模块的问题,需要来创建两个中间键,一个中间键是把加载第三方模块的import中的路径改变,改成加载/@modules/模块的名称。另一个中间件,当请求过来之后,判断请求路径中是否有/@modules/模块的名称,如果有的话,取node_modules目录中加载对应的模块。下面实现第一个中间键,修改第三方模块的路径

    #!/usr/bin/env node
    const Koa = require('koa')
    const send = require("koa-send")
    
    const app = new Koa()
    
    const streamToString = stream => new Promise((resolve, reject) => {
      const chunks = []
      stream.on('data', chunk => chunks.push(chunk))
      stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')))
      stream.on('error', reject)
    })
    
    // 1.静态文件服务器
    app.use(async (ctx, next) => {
      await send(ctx, ctx.path, { root: process.cwd(), index: 'index.html' })
      await next()
    })
    
    // 2.修改第三方模块的路径
    app.use(async (ctx, next) => {
      if (ctx.type === 'application/javascript') {
        const contents = await streamToString(ctx.body)
        // import vue from 'vue
        // import App from './App.vue
        // 这里需要将【from '】替换成【from '@modules】
        ctx.body = contents.replace(/(from\s+['"])(?!\.\/)/g, '$1/@modules/')
      }
    })
    
    app.listen(3000)
    console.log('Server 3000')
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    第一个中间件中加载了静态文件,当把静态文件返回给浏览器之前,要判断一下当前返回的文件是否是javascript模块,如果是的话,来修改第三方模块的路径,修改成/@modules/模块的名称

    在把文件返回给浏览器之前,需要判断一下当前返回给浏览器的文件是否是javascript,在这里只需要判断响应头中的contentType是否为application/javascript,如果是,那需要找到这个文件中的内容,然后处理import中的路径。

    ctx.body就是要返回给浏览器的js文件。注意ctx.body是一个流,要修改字符串中的路径,所以需要把流转换成字符串。这件事情其他位置还有用,所以在最上面来定一个函数streamToString,这个函数的作用是把流转换成字符串。

    const streamToString = stream => new Promise((resolve, reject) => {
      const chunks = []
      stream.on('data', chunk => chunks.push(chunk))
      stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')))
      stream.on('error', reject)
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    因为读取流是一个异步的过程,所以这里要返回一个promise对象。在这个函数里,首先要定一个chunks数组,它存储一会读取到的buffer,然后来注册streamdata事件,去监听读取到的buffer,把读取到的buffer存储到chunks数组中。当数据读取完毕之后,把获取到的结果传递给resolve。这里先要把数组中的buffer合并,然后再转换成字符串。把以上流程注册到end事件中。最后如果出错的话,调用reject,再注册出错的事件error,这个时候直接执行reject。

    // 2.修改第三方模块的路径
    app.use(async (ctx, next) => {
      if (ctx.type === 'application/javascript') {
        const contents = await streamToString(ctx.body)
        // import vue from 'vue'
        // import App from './App.vue
        // 这里需要将【from '】替换成【from '@modules】
        ctx.body = contents.replace(/(from\s+['"])(?!\.\/)/g, '$1/@modules/')
      }
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    现在可以把ctx.body转换成字符串,来接收一下它的返回结果const contents = await streamToString(ctx.body)。接下来把contents中加载第三方模块的路径修改一下,然后把结果重新赋值给ctx.body输出。这里需要通过正则表达式把第三方模块匹配出来,然后替换成/@modules/模块的名称

    下面打开浏览器来测试一下,打开浏览器之前我们还需要重启一下服务器,因为这里代码修改了。

    先来看一下main.js请求,找到response来看这次请求的结果,注意这里的vue的路径已经被修改了,修改成了/@modules/vue,这个路径是不存在的。下一个请求是/@modules/vue这个文件,而现在返回的是404,这个路径根本是不存在的,所以获取不到文件。所以在静态的web服务器中,当请求过来之后,还要首先去判断一下当前请求的路径中是否以/@modules/开头,如果是的话就去node_module找对应的模块,下面实现这个功能。

    4.Vite 实现原理-加载第三方模块

    上一小节中创建了一个中间件,负责把加载的第三方模块的路径修改成/@modules/的形式。现在再来创建一个中间键,当请求过来以后,判断请求的路径是否以/@modules/开头,如果是的话,去node_modules目录中加载对应的模块。

    接下来在处理静态文件之前再来创建一个中间键。注意这里是在处理静态文件之前。这里要做的事情是当请求的路径是以/@modules/开头的话,把请求的路径修改成node_modules中对应的文件路径,然后继续交给处理静态文件的中间件去处理,所以这个中间界应该写在第一个位置。

    #!/usr/bin/env node
    const path = require('path')
    const Koa = require('koa')
    const send = require("koa-send")
    
    const app = new Koa()
    
    const streamToString = stream => new Promise((resolve, reject) => {
      const chunks = []
      stream.on('data', chunk => chunks.push(chunk))
      stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')))
      stream.on('error', reject)
    })
    
    // 3. 加载第三方模块
    app.use(async (ctx, next) => {
      // ctx.path --> /@modules/vue
      if (ctx.path.startsWith('/@modules/')) {
        const moduleName = ctx.path.substr(10)
        const pkgPath = path.join(process.cwd(), 'node_modules', moduleName, 'package.json')
        const pkg = require(pkgPath)
        ctx.path = path.join('/node_modules', moduleName, pkg.module)
      }
      await next()
    })
    
    // 1.静态文件服务器
    app.use(async (ctx, next) => {
      await send(ctx, ctx.path, { root: process.cwd(), index: 'index.html' })
      await next()
    })
    
    // 2.修改第三方模块的路径
    app.use(async (ctx, next) => {
      if (ctx.type === 'application/javascript') {
        const contents = await streamToString(ctx.body)
        // import vue from 'vue
        // import App from './App.vue
        // 这里需要将【from '】替换成【from '@modules】
        ctx.body = contents.replace(/(from\s+['"])(?!\.\/)/g, '$1/@modules/')
      }
    })
    
    app.listen(3000)
    console.log('Server 3000')
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    在这个函数里首先要获取当前的路径,使用的是ctx.path,当前希望处理的路径是这个样子的ctx.path --> /@modules/vue。所以先来判断一下ctx.path属性是否是以/@modules/开头的,如果是的话,那再从这个路径中截取模块的名字vue。注意这里的/@modules/总共是10个字符,以直接来截取。

    拿到模块的名称之后,要获取这个模块的入口文件,这里要获取的是ES Module模块的入口文件。需要先找到这个模块的package.json,然后再获取package.jsonmodule字段的值,也就是ES Module模块的入口,需要先来拼接模块的package.json文件的路径。这里用到node中的path模块,可以调用path.join()来拼接路径。

    先定一个常量来接收拼接好的路径,现在要拼接的是package.json的路径。当前项目的路径是process.cwd(),然后是node_modules文件夹,在node_modules文件夹里边来找模块moduleName,然后再去找这个模块下面的package.json。有了这个路径之后,再使用require来加载package.json。最后重新给cpx.path赋值,因为之前请求的这个路径是不存在的,需要重新给它设置一个存在的路径,让它去加载这个文件。

    到这里加载第三方模块就写完了,然后重启一下服务器,再打开浏览器来测试一下,看看是否OK。

    但是这还有一个问题,加载的vuebundlevue版本,也就是需要打包的vue,但这个vue里边它又去加载了vue源码中的runtime-dom模块,还去加载了vue中的shared的模块,但是看一下请求这个位置,它根本就没有请求到这两个模块,是不是加载这两个模块出了问题呢?好,我们来看一下在console里面有两个错误。

    这两个错误告诉我们加载模块失败。第一个是App.vue,第二个是index.css,也就是当前去加载App.vueindex.css这l两个个模块时,浏览器不能识别,所以在服务器上还要去处理浏览器不能够识别的模块。

    5.Vite 实现原理-编译单文件组件

    刚刚演示过浏览器无法处理在main.js中使用import加载的单文件组件模块和样式模块,浏览器只能够处理js模块,所以通过import加载的其他模块都需要在服务器端处理。当请求单文件组件的时候,需要在服务器上把单文件组件编译成js模块,然后返回给浏览器。下面先打开浏览器来观察一下Vite中是如何去处理单文件组件的,然后再来自己实现。这里只演示单文件组件的处理过程,其他通过import导入的模块你可以参考我们的实现思路自己来写。

    当第一次请求这个文件的时候,服务器会把单文件组件编译成一个对象。这里先加载HelloWorld这个组件,然后创建了一个组件的选项对象。注意这里没有模板,因为模板最终要被编译成render函数,然后挂载到选项对象上。那下面又去加载了App.vue?type=template

    这次请求是告诉服务器,你去帮我编一下这个单文件组件的模板,然后返回一个render函数,注意现在下载的就是它的render函数,然后把render函数挂载到刚刚创建的组件选项对象上来。从这里可以看到,当请求单文件组件的时候,服务器会来编译单文件组件,并把相应的结果返回给浏览器。

    App.vue?type=template这次请求是告诉服务器,你帮我把单文件组件编译成render函数,可以找到这个render函数这块,通过export把这个render函数导出了。在app.vue里边,去加载这个组件返回的这个render函数,并且把它挂载到script对象的render方法上来。

    这是vite中是如何去处理单文件组件的,它会发送两次请求,第一次请求是把单文件组件编译成一个对象,第二次请求是编译单文件组件的模板返回一个render函数,并且把这个render函数挂载到刚刚创建的那个对象的render方法上。

    下面实现一下第一次请求的过程:把单文件组件编译成一个对象

    这次请求需要把单文件组件编译成一个组件的选项对象,这里需要写一个中间件来处理单文件组件。现在需要确定这个中间件书写的位置。当请求到单文件组件并把单文件组件读取完成之后。接下来需要对单文件组件进行编译,并且把编译的结果返回给浏览器,这里的核心是把单文件组件读取完成之后再处理,所以是在处理完静态文件之后,并且单文件组件也有可能会加载第三方模块,所以是在处理第三方模块之前,1和3中间。

    #!/usr/bin/env node
    const path = require('path')
    
    const Koa = require('koa')
    const send = require("koa-send")
    const compilerSFC = require('@vue/compiler-sfc')
    const { Readable } = require("stream");
    
    const app = new Koa()
    
    const streamToString = stream => new Promise((resolve, reject) => {
      const chunks = []
      stream.on('data', chunk => chunks.push(chunk))
      stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')))
      stream.on('error', reject)
    })
    
    const stringToStream = text => {
      const stream = new Readable()
      stream.push(text)
      stream.push(null)
      return stream
    }
    
    // 3. 加载第三方模块
    app.use(async (ctx, next) => {
      // ctx.path --> /@modules/vue
      if (ctx.path.startsWith('/@modules/')) {
        const moduleName = ctx.path.substr(10)
        const pkgPath = path.join(process.cwd(), 'node_modules', moduleName, 'package.json')
        const pkg = require(pkgPath)
        ctx.path = path.join('/node_modules', moduleName, pkg.module)
      }
      await next()
    })
    
    // 1.静态文件服务器
    app.use(async (ctx, next) => {
      await send(ctx, ctx.path, { root: process.cwd(), index: 'index.html' })
      await next()
    })
    
    // 4.处理单文件组件
    app.use(async (ctx, next) => {
      if (ctx.path.endsWith('.vue')) {
        // 把ctx.body转换成字符串
        const contents = await streamToString(ctx.body)
        const { descriptor } = compilerSFC.parse(contents)
        let code
        if (!ctx.query.type) {
          code = descriptor.script.content
          // console.log(code)
    
          code = code.replace(/export\s+default\s+/g, 'const __script = ')
    
          code += `
            import { render as __render } from "${ctx.path}?type=template"
            __script.render = __render
            export default __script
          `
        }
        ctx.type = 'application/javascript'
        ctx.body = stringToStream(code)
      }
    
      await next()
    })
    
    
    // 2.修改第三方模块的路径
    app.use(async (ctx, next) => {
      if (ctx.type === 'application/javascript') {
        const contents = await streamToString(ctx.body)
        // import vue from 'vue
        // import App from './App.vue
        // 这里需要将【from '】替换成【from '@modules】
        ctx.body = contents.replace(/(from\s+['"])(?![\.\/])/g, '$1/@modules/')
      }
    })
    
    app.listen(3000)
    console.log('Server 3000')
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82

    当请求的文件是单文件组件的时候,需要对单文件组件进行编译,编译的工作比较复杂,因为Vue3里的每一个模块都可以拿出来单独使用,而Vue3中又提供了编译单文件组件的模块@vue/compiler-sfc,所以可以直接使用这个模块来对单文件组件进行编译。

    接下来要分两次来处理单文件组件,首先要去判断当前请求的是否是单文件组件,也就是请求的路径的后缀是否是以.vue结尾ctx.path.endsWith('.vue')。如果是以.vue结尾的话,需要把ctx.body转换成字符串,因为ctx.body中现在就是单文件组件中的内容,而在编译单文件组件的时候是需要单文件组件的内容的。const contents = await streamToString(ctx.body)

    这个单文件组件的内容就获取到了。下面要调用compilerSFC.parse()方法编译单文件组件,const { descriptor } = compilerSFC.parse(contents)

    然后在下面再来定一个code变量,这是最终要返回给浏览器的代码。之前看过vite中的处理过程,单文件组件的请求有两次,第一次是不带type参数,第二次是带着type=template参数。

    先来处理不带参数的情况,也就是第一次的请求。第一次请求的时候需要返回这个单文件组件编译之后的选项对象。先来判断一下当前的查询字符串中是否有type,如果当前query中没有type属性的时候,说明是第一次请求,先去接收到当前单文件组件编译之后的js代码。code = descriptor.script.content,可以打印一下

    import HelloWorld from './components/HelloWorld.vue'
                                                        
    export default {                                    
      name: 'App',
      components: {                                     
        HelloWorld                                      
      }
    }                                                   
    
    
    export default {     
      name: 'HelloWorld',
      props: {           
        msg: String      
      },
      data() {           
        return {         
          count: 0       
        }                
      }                  
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    下面来改造一下这里的code,刚刚看过code的形式了,下面需要先把code中的export default方替换成const __script =code = code.replace(/export\s+default\s+/g, 'const __script = ')

    import {render as __render} from "/src/App.vue?type=template"
    __script.render = __render
    __script.__hmrId = "/src/App.vue"
    typeof __VUE_HMR_RUNTIME__ !== 'undefined' && __VUE_HMR_RUNTIME__.createRecord(__script.__hmrId, __script)
    __script.__file = "F:\\Web\\lagou\\03-05-Vue.js3.0\\code\\02-vite-demo\\src\\App.vue"
    export default __script
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    然后后面还要去拼接上一段代码,

    code += `
            import { render as __render } from "${ctx.path}?type=template"
            __script.render = __render
            export default __script
          `
    
    • 1
    • 2
    • 3
    • 4
    • 5

    接下来设置响应头中的contentType,把它设置为application/javascript,需要告诉浏览器,现在发给你的是javascript模块,让浏览器去执行javascript模块。

    最后还要把code输出给浏览器,code是字符串,需要把code转换成只读流设置给ctx.body,因为下一个中间键中要去处理的这个body是流的形式。

    所以下面我们要来写一个辅助的函数,把字符串转换成流。

    const { Readable } = require("stream");
    
    const stringToStream = text => {
      const stream = new Readable()
      stream.push(text)
      stream.push(null)
      return stream
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    ctx.body去赋值,ctx.body = stringToStream(code)。重启测试

    刷新一下浏览器,先找到app.vue,请求的这个路径已经OK了,第二次请求App.vue?type=template此时还没有返回的内容是因为还没有处理第二次请求。

    6.Vite 实现原理-编译单文件组件

    我们已经把单文件组件的第一次请求处理完毕,第一次请求会把单文件组件定义成组件的选项对象,然后返回给浏览器,但是这个选项对象中没有模板或者render函数,接下来再来处理单文件组件的第二次请求,第二次请求url中会带着参数type=template。第二次请求中,要把单文件组件的模板编译成render函数。

    ctx.query.type === 'template'
    
    • 1

    ctx.query.type === 'template'的时候是对单文件组件的第二次请求。在这里要去编译模板,compilerSFC对象下有一个专门编译模板的方法compileTemplate,它需要接受一个对象形式的参数,设置要编译的模板的内容。

    const templateRender = compilerSFC.compileTemplate({ source: descriptor.template.content })
    
    • 1

    组件模板中的内容可以通过descriptor.template.content获取,返回的对象中有code属性,code就是的render函数。

    但是console中有一个报错,它指向shared文件。

    注意这个位置出错,它访问不到process。当前的代码是在浏览器环境下执行的,而processnode的环境中的对象,而现在是在浏览器环境中执行的,浏览器环境中没有process对象,所以报错,因此需要在返回的js模块中处理这部分。

    ctx.body = contents
          .replace(/(from\s+['"])(?![\.\/])/g, '$1/@modules/')
          .replace(/process\.env\.NODE_ENV/g, '"development"')
    
    • 1
    • 2
    • 3

    在返回js模块之前,要把js模块中的所有process.env.NODE_ENV都替换成development,表示当前是开发环境。

    打开浏览器来测试一下。

    刷新浏览器,终于可以看到这个结果了。这块没有样式是因为我们之前把样式模块以及图片都给它注释起来了。点击这个按钮组件也可以正常工作。

    到这里,模拟vite的实现就写完了。这里的核心是在开发阶段不需要本地打包,根据需要请求服务器编译单文件组件。当然这里还没有去处理样式和图片模块,可以根据实现单文件组件的思路来尝试实现处理样式模块和图片模块,可以观察Vite返回的结果,自己来模拟。

  • 相关阅读:
    STM32 | 独立看门狗 | RTC(实时时钟)
    C++中类的运算符重载教程(一),内附完整代码与解析
    Ruby on Rails 实践:更换 aloe 首页
    《WebGIS快速开发教程第四版》重磅更新
    Net6 用imagesharp 实现跨平台图片处理并存入oss
    第四章——DQL查询数据(最重点)
    shell脚本判断语句
    win环境安装Node.js的多种方式
    MySQL 主要线程
    Vue3+TypeScript+Vite如何使用require动态引入类似于图片等静态资源
  • 原文地址:https://blog.csdn.net/weixin_42122355/article/details/128124151