面试官:”Vite为什么快?“

前面一篇文章介绍了如何做 Vite 的插件开发 # 20分钟掌握 Vite 插件开发,这篇文章介绍清晰 Vite 的构建原理,为什么比 Webpack 构建速度快这么多。

1. 一句话解释 Webpack 的构建原理

前端之所有需要 类似于 Webpack 这样的构建工具,是为了提高项目的开发效率,Webpack 通过分析js中的 require 语句,分析出当前 js 文件所有的依赖文件,通过递归的方式层层分析后,得到整个项目的依赖关系图,对图中不同的文件执行不同的 loader,比如使用 css-loader 解析css代码,最后基于这个依赖关系图读取到整个项目中的所有文件代码,进行打包处理之后交给浏览器执行。

这个过程一气呵成,顺理成章。这里我们只用文字简单描述 Webpack 的工作原理。想要弄懂其中的详细过程,关注一点,我们下一篇文章再详细剖析。

这样的构建构过程,导致了在我们调试代码之前,需要等待 Webpack 的依赖收集过程,而当项目代码体量很大时,这个依赖收集的过程往往需要我们等待几十秒甚至一两分钟,程序员在这个时间段内只能摸鱼,开发体验非常差,有什么办法解决这个问题?

如果有办法能够做到更少的代码打包就好了!!!,于是 bundless 的打包思路就诞生了,Vite 便是这个这种思路的遥遥领先。

2. script 的模块化

需要打包工具的核心原因,就是浏览器在执行代码的时候,本身没有一个很好的方式去读懂我们的项目中的各个文件引入关系。

所以

Webpack 对浏览器说:"你别纠结了,我把所有的文件引入关系都梳理好了,并且将项目中所有文件的代码打包在了一起,你就去执行找一个文件吧!"

浏览器开开心的说:"好的大哥,谢谢大哥",然后毫无压力的哐哐运行

但是随着浏览器的进步,它开始能慢慢读的懂一些模块化的引入语法了,比如:

浏览器是可以正常那运行这份代码的:

再看一眼浏览器的是怎么处理这些文件引入关系的:

浏览器会将 import 语句处理成一个个HTTP网络请求,去获取 import 引入的各种模块, 就因为浏览器现在可以通过 type="module" 这种方式读懂项目中文件的模块化引入,所以,bundless 的思想得以发展

3. Vite 的一步一步实现

Vite 正是借助了刚刚我们说的浏览器的这一特性,将项目中的各种文件引入处理成网络请求,当浏览器执行到了模块依赖的代码,便自己去请求所需要的代码资源。这么简单?没错!那接下来我们一步一步自己打造一个 Vite。

首先,这里我们不使用构建工具,自己创建一个Vue的项目(参考vite的构建目录自己一个一个创建)

在index.html 中写入以下引入

但是现在浏览器是无法运行这份html的,因为这其中还有一个问题

来自于 node_modules 的模块浏览器无法读懂它的路径,进而无法请求到正确的资源,所以我们需要手动解决这一问题

在项目的根目录下创建 simple-vite.js 文件,通过 node 启动一个web服务,向浏览器响应资源

javascript 复制代码
const http = require('http')
const fs = require('fs')

const server = http.createServer((req, res) => {
  const { url, query } = req

  if (url === '/') {
    // 设置响应头的Content-Type是为了让浏览器以html的编码方式去加载这份资源
    res.writeHead(200, { 
      'Content-Type': 'text/html'
    })

    let content = fs.readFileSync('./index.html', 'utf8')
    res.end(content)
  }

})

server.listen(8080, () => {
  console.log('listening on port 8080');
})

用node运行这份js后:

这个应该就不用再详细解释了,node没有基础的看官只能自己先学习一下了

因为 html 代码中有 main.js 这个模块的引入,所以浏览器会自动发这个请求,但是浏览器访问的是我们自己的后端,我们没有写这个对应的响应,所以是看不到东西的

继续处理js文件的请求

javascript 复制代码
const http = require('http')
const fs = require('fs') 
const path = require('path') // ++++

const server = http.createServer((req, res) => {
  const { url, query } = req

  if (url === '/') {
    // 设置响应头的Content-Type是为了让浏览器以html的编码方式去加载这份资源
    res.writeHead(200, { 
      'Content-Type': 'text/html'
    })
    let content = fs.readFileSync('./index.html', 'utf8')
    res.end(content)
  } else if (url.endsWith('.js')) { // +++++++新增代码
    const p = path.resolve(__dirname, url.slice(1)) // '/main.js' ==> 'main.js'的绝对路径
    res.writeHead(200, {
      'Content-Type': 'application/javascript'
    })
    const content = fs.readFileSync(p, 'utf8')
    res.end(content)
  }

})

server.listen(8080, () => {
  console.log('listening on port 8080');
})

我们将js文件的请求都读取到资源代码返回给浏览器后:

main.js 的资源请求正常了,但是现在项目还是无法运行,因为mian.js中有浏览器无法处理的模块路径 'vue',所以我们需要实现,当浏览器加载到来自 node_modules 中的模块时,我们告诉浏览器该模块的正常路径是什么

javascript 复制代码
const http = require('http')
const fs = require('fs') 
const path = require('path') 

function rewriteImport(content) { // ++++新增
  return content.replace(/ from ['|"]([^'"]+)['|"]/g, function(s0, s1) { // 找到  from 'vue' 中的  'vue'
    if (s1[0] !== '.' && s1[1] !== '/') {
      return ` from '/@modules/${s1}'`
    } else {
      return s0
    }
  })
}

const server = http.createServer((req, res) => {
  const { url, query } = req

  if (url === '/') {
    // 设置响应头的Content-Type是为了让浏览器以html的编码方式去加载这份资源
    res.writeHead(200, { 
      'Content-Type': 'text/html'
    })

    let content = fs.readFileSync('./index.html', 'utf8')
    res.end(content)
  } else if (url.endsWith('.js')) {
    const p = path.resolve(__dirname, url.slice(1))
    res.writeHead(200, {
      'Content-Type': 'application/javascript'
    })
    const content = fs.readFileSync(p, 'utf8')
    res.end(rewriteImport(content))  // +++++修改
  }

})

server.listen(8080, () => {
  console.log('listening on port 8080');
})

再次启动后端,访问浏览器后:

我们给所有的从 node_modules 中引入的模块,路径中打上一个特殊标记 @modules ,让浏览器知道这是个路径,因为在浏览器眼里, "./", "/", "../" 这种才叫路径。同时顺势浏览器便执行 main.js 中的代码,并发起了新的请求

但是

这个路径依然不是最终的有效路径,不过现在我们可以在node中进行判断,如果出现请求的url是 /@modules/xxx 这种类型的,我们就去 node_modules 中为其读取源代码

javascript 复制代码
const http = require('http')
const fs = require('fs') 
const path = require('path') 

function rewriteImport(content) { // ++++新增
  return content.replace(/ from ['|"]([^'"]+)['|"]/g, function(s0, s1) { // 找到  from 'vue' 中的  'vue'
    if (s1[0] !== '.' && s1[1] !== '/') {
      return ` from '/@modules/${s1}'`
    } else {
      return s0
    }
  })
}

const server = http.createServer((req, res) => {
  const { url, query } = req

  if (url === '/') {
    res.writeHead(200, { 
      'Content-Type': 'text/html'
    })

    let content = fs.readFileSync('./index.html', 'utf8')
    res.end(content)
  } else if (url.endsWith('.js')) {
    const p = path.resolve(__dirname, url.slice(1))
    res.writeHead(200, {
      'Content-Type': 'application/javascript'
    })
    const content = fs.readFileSync(p, 'utf8')
    res.end(rewriteImport(content))
    
  } else if (url.startsWith('/@modules/')) { // ++++++ 新增代码
  
    const prefix = path.resolve(__dirname, 'node_modules', url.replace('/@modules/', ''))
    const module = require(prefix + '/package.json').module
    const p = path.resolve(prefix, module)
    const content = fs.readFileSync(p, 'utf8')
    res.writeHead(200, {
      'Content-Type': 'application/javascript'
    })
    res.end(rewriteImport(content))
  }

})

server.listen(8080, () => {
  console.log('listening on port 8080');
})

小知识:要读取到一个公共库有效的源码,可以在该库的 package.json 文件中找到源码有效地址

比如 node_modules中的 Vue 的源码:

如此一来,我们就帮浏览器解决了第三方模块源码资源请求不到的问题:

Vue 源码中涉及到的各种其他的源码资源也顺势被请求到了

现在还剩最后一个问题:

关于 .vue 后缀的文件资源,浏览器确实请求到了,比如上图的 App.vue,但是,浏览器是读不懂vue的语法的这我们都知道,所以在 Response 响应中啥也看不见,就是因为浏览器拿到了代码解析不出来,它懵了,我们还得帮它读懂这份代码

众所周知,vue是有自己的编译器的,它能将自己的vue独有的语法编译成js代码,所以我们只需要在浏览器请求 .vue 后缀的文件时用上 vue 自己的编译器就好了

php 复制代码
const http = require('http')
const fs = require('fs') 
const path = require('path') 
const { URL } = require('url');
const compilerSfc = require('@vue/compiler-sfc') // +++++新增
const compilerDom = require('@vue/compiler-dom')

function rewriteImport(content) { 
  return content.replace(/ from ['|"]([^'"]+)['|"]/g, function(s0, s1) { // 找到  from 'vue' 中的  'vue'
    if (s1[0] !== '.' && s1[1] !== '/') {
      return ` from '/@modules/${s1}'`
    } else {
      return s0
    }
  })
}

const server = http.createServer((req, res) => {
  const { url } = req
  const query = new URL(req.url, `http://${req.headers.host}`).searchParams;  // ++++ node 新版本,读取get请求的参数写法

   // 省略部分代码... 
   else if (url.indexOf('.vue') !== -1) { // +++++ 返回.vue文件的js部分
    const p = path.resolve(__dirname, url.split('?')[0].slice(1))
    const { descriptor } = compilerSfc.parse(fs.readFileSync(p, 'utf8'))
    if (!query.get('type')) {
      res.writeHead(200, {'Content-Type': 'application/javascript'})
      const content = `
        ${rewriteImport(descriptor.script.content.replace('export default', 'const __script = '))} 
        import { render as __render } from "${url}?type=template" 
        __script.render = __render 
        export default __script
      `
      res.end(content)
    } else if (query.get('type') === 'template') { // 返回.vue文件的html部分
      const template = descriptor.template
      const render = compilerDom.compile(template.content, {mode: 'module'}).code
      res.writeHead(200, {'Content-Type': 'application/javascript'})
      res.end(rewriteImport(render))
    }
  }

})

server.listen(8080, () => {
  console.log('listening on port 8080');
})

我们都知道,vue的组件由 tempalte,script,style,三部分构成,以上代码主要是将浏览器请求的 App.vue 中的template部分打造成新的请求,让浏览器再多请求一次,而js部分是不需要的

注意App.vue 中不要使用setup的语法糖,用语法糖需要额外的编译手段,上述代码没有包含该功能

到这里还剩一个css部分的请求,目前是这样的:

添加css的响应

dart 复制代码
// 省略部分代码...
else if (url.endsWith('.css')) {
    const p = path.resolve(__dirname, url.slice(1))
    const file = fs.readFileSync(p, 'utf8')
    const content = `
      const css = "${file.replace(/\n/g, '')}"
      let link = document.createElement('style')
      link.setAttribute('type', 'text/css')
      document.head.appendChild(link)
      link.innerHTML = css
      export default css
    `
    res.writeHead(200, {'Content-Type': 'application/javascript'})
    res.end(content)
  }
// 省略部分代码...

将css代码改写成js响应给浏览器执行,最后我们试一试运行项目

别慌!这是因为在浏览器环境中找不到 process 进程(这是node中的),我们去最外面的index.html文件中加一点声明就好了

xml 复制代码
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + Vue</title>
  </head>
  <body>
    <div id="app"></div>

    <script> // ++++
      window.process = {
        env: {
          NODE_ENV: 'dev'
        }
      }
    </script>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>

再次访问 http:localhost:8080 看一眼效果

长这样子是因为我的 App.vue 中写了个demo

终于,整个项目在不使用构建工具的情况下,我们自己将它运行起来了,也处理好了各个模块引入的问题

4. 总结一下

Vite 相比于 Webpack 之所以构建快是因为,Vite 借助新版本浏览器可以读懂模块化语法的特点,将项目中的模块化引入统一以一个又一个http请求的方式响应给浏览器,这样做的好处就是省去了 Webpack 构建过程中递归做依赖收集的耗时步骤,又因为Vite是开发环境的工具,绝大多数情况下我们不用不考虑兼容性,不会有人开发时还用老版本的浏览器吧>_<

完整代码在这里

相关推荐
满怀10151 分钟前
【Django全栈开发实战】从零构建企业级Web应用
前端·python·django·orm·web开发·前后端分离
胡斌附体14 分钟前
微服务调试问题总结
java·微服务·架构·调试·本地·夸微服务联调
Darling02zjh41 分钟前
GUI图形化演示
前端
Channing Lewis43 分钟前
如何判断一个网站后端是用什么语言写的
前端·数据库·python
互联网搬砖老肖1 小时前
Web 架构之状态码全解
前端·架构
showmethetime1 小时前
matlab提取脑电数据的五种频域特征指标数值
前端·人工智能·matlab
zc.z2 小时前
微服务如何实现服务的高并发
数据库·微服务·架构
why1512 小时前
微服务商城(1)开篇、服务划分
微服务·云原生·架构
左钦杨2 小时前
IOS CSS3 right transformX 动画卡顿 回弹
前端·ios·css3
NaclarbCSDN3 小时前
Java集合框架
java·开发语言·前端