面试官:”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是开发环境的工具,绝大多数情况下我们不用不考虑兼容性,不会有人开发时还用老版本的浏览器吧>_<

完整代码在这里

相关推荐
王解36 分钟前
webpack loader全解析,从入门到精通(10)
前端·webpack·node.js
我不当帕鲁谁当帕鲁40 分钟前
arcgis for js实现FeatureLayer图层弹窗展示所有field字段
前端·javascript·arcgis
那一抹阳光多灿烂1 小时前
工程化实战内功修炼测试题
前端·javascript
放逐者-保持本心,方可放逐2 小时前
微信小程序=》基础=》常见问题=》性能总结
前端·微信小程序·小程序·前端框架
毋若成4 小时前
前端三大组件之CSS,三大选择器,游戏网页仿写
前端·css
红中马喽4 小时前
JS学习日记(webAPI—DOM)
开发语言·前端·javascript·笔记·vscode·学习
Black蜡笔小新5 小时前
网页直播/点播播放器EasyPlayer.js播放器OffscreenCanvas这个特性是否需要特殊的环境和硬件支持
前端·javascript·html
秦jh_6 小时前
【Linux】多线程(概念,控制)
linux·运维·前端
蜗牛快跑2136 小时前
面向对象编程 vs 函数式编程
前端·函数式编程·面向对象编程
Dread_lxy6 小时前
vue 依赖注入(Provide、Inject )和混入(mixins)
前端·javascript·vue.js