VUE3学习第六天—-vite 实现原理

学习: https://github.com/zelixag/toyvite

1. vite 概念

  • Vite 是一个面向现代浏览器的一个更轻,更快的Web应用开发工具
  • 它基于ECMAScript标准原生模块系统(ES Modules)实现

Vite出现是为了解决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版本,在创建的时候也可以通过指令的选择不同的模板也可以支持其他的框架。

2. 基本使用

子命令

  • vite serve: 启动一个用于开发的开发web服务器,启动时不需要编译所有代码文件,启动的速度非常的快,我们看下图:

Vite运行机制

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

回顾vue-cli-service serve,看下图:

image.png

当运行vue-cli-service serve的时候,内部会使用webpack,先去打包所有的模块,如果模块比较多的时候,打包速度会非常的慢,把打包的结果存储到内存中,然后在启动开发的web服务器。浏览器请求服务器,服务器就会吧内存中存储的打包到额结果,返回给浏览器。webpack会将所有的模块提前编译打包放在服务器内存里,不管模块是否被执行或者被调用都会在先编译打包到bundle里面,随着项目越来越大,打包的后的bundle也会越来越大,打包的速度自然也会越来越慢。

Vite借用现代浏览器对ESModule原生支持的能力,省略了对模块的打包,对于需要编译的文件,比如单文件文件,sass样式模块等。vite采用的另一种模式即时编译,也就是说只有当浏览器请求按个文件才会编译这个文件。这种即时编译的好处就是按需编译,速度会更快。

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

可想而知,Vite HMR性能会更好

  • vite build:
    • Rollup: 使用的是Rollup去打包,还是会将文件提前编译并且打包在一起
    • Dynamic import 切割的需求Vite采用的是Dynamic import原生的动态导入特性实现的,所以打包结果还是支持现代浏览器
    • Polyfill: 不过动态导入特性还是有Polyfill进行处理

vite 的出现引发了我们一个值得思考的问题

思考: 打包 or 不打包

  • 使用Webpack打包的两个原因:
    • 浏览器环境并不支持模块化
      绝大多数浏览器都支持esmodule
    • 零散的模块文件会产生大量的HTTP请求
      http2可以帮我们解决,可以服用链接

ESModule的支持

image.png

如果还需要兼容IE,那就不能使用vite构建项目

开箱即用

  • TypeScript – 内置支持
  • less/sass/stylus/postcss – 内置支持(需要单独安装对应的编译器)
  • 支持 JSX
  • 支持 WebAssembly

….

Vite 特性

  • 快速冷启动
    需要谁请求谁,服务器编译返回
  • 模块热更新更快
    谁更新请求谁,服务器编译并返回
  • 按需编译
    需要谁或谁更新,服务器编译并返回
    避免编译使用不到的文件
  • 开箱即用
    避免各种loader以及各种plugin配置

Vite 核心功能

  • 静态 Web 服务器
    • 静态服务器会拦截部分请求
  • 编译单文件组件
    • 拦截浏览器不识别的模块,并处理
  • HMR

接下来我们来实现一个能够开启静态web服务器的命令行工具,vite内部使用的是koa来开启静态web服务器,我们也使用koa来开启静态web服务器,把当前运行vite目录作为静态web服务器的根目录,好,开始写代码。
创建一个node项目

image.png

初始化package.json文件,安装koa 和 koa-send

{
  "name": "vite-cli",
  "version": "1.0.0",
  "main": "index.js",
  "bin": "index.js",
  "license": "ISC",
  "dependencies": {
    "koa": "^2.13.1",
    "koa-send": "^5.0.1"
  }
}

复制代码

main.js文件

#!/usr/bin/env node

const Koa = require('koa');
const send = require('koa-send');


const app = new Koa()

// 1. 静态文件服务器
app.use(async (ctx, next) => {
  // 第一个参数上下文,当前请求的路径,当前运行node程序的目录,默认页面
  await send(ctx, ctx.path, {root: process.cwd(), index: 'index.html'})
  await next()
})

app.listen(3000);
console.log('Server running @ http://localhost:3000')

复制代码

我们将
这样就会启动一个一koa创建的一个静态服务器,我们写好上面代码之后,将这个node项目使用npm link进行全局软链接。然后使用官方vite 创建一个vue3的项目,在这个项目下执行vite-cli.之后每次代码更新,都需要在这个vue3项目中重新执行vite-cli,即可看到更新的效果

image.png

错误: 当我们解析vue模块的时候失败了,我们使用import导入模块的时候要求使用相对路径,’/’, ‘./’, ‘../’

image.png

如图我们发现vue的路劲没有上面说的三种相对路径,浏览器不识别,导致报错。我们必须要解决这个问题。我们可以查看vite是怎么解决这个问题的:

我们启动一个vite 创建的项目:

image.png

  • 发现vite在服务器将路径处理成一个不存在的路径,/@modules/vue.js这个路径在服务器上是根本不存在的,我们根本没有创建这个@modules文件夹。拿在服务器就应该去处理这个请求,当有@modules的时候,加载我们的vue模块,这是我们的思路。

image.png

  • 再看mian.js请求头,告诉浏览器返回的文件是javascript;

image.png

开启一个静态web服务器已经搞定,接下来考虑怎么加载一个第三方模块

加载第三方模块

这里需要创建两个中间件:

  • 第一个需要import中的路径改变 改成/@modules/第三方模块名称
  • 第二个当请求过来之后判断请求路径是否含有/@modules/第三方模块名称,如果有的话,去node_modules中去加载对应的第三方模块
  1. 实现第一个中间件,为第三方模块路径加上@modules
// 因为读取流是异步的,使用Promise来接收
const streamToString = stream => new Promise((resolve, reject) => {
  const chunks = []
  // 读取流,将流数据放入chunks中
  stream.on('data', chunk => chunks.push(chunk))
  // 读完之后将流数据返回
  stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')))
  // 发生错误的话
  stream.on('error', reject)
})
// 2. 修改第三方模块的路径
app.use(async (ctx, next) => {
  if(ctx.type === 'application/javascript') {
    // ctx.body的值是一个流,但是我们要把流文件转化为字符串。才能将第三方模块的路径发生改变,而且这个功能其他位置还要用我们抽成一个方法
    const contents = await streamToString(ctx.body);
    // import vue from 'vue'
    // import App from './App.vue' // 浏览器可识别就不处理了
    ctx.body = contents.replace(/(from\s+['"])(?![\.\/])/g, '$1/@modules/')
  }
})
复制代码

读取对应的请求内容ctx.type === 'application/javascript,然后将读取内容流转化为字符串,将对应地方模块使用正则匹配并替换contents.replace(/(from\s+['"])(?!\.\/)/g, '$1/@modules/')
最后拿到我们想要的,报四百没有关系,下一个中间件处理好这个404,我们第三方模块加载就成功了

image.png

  1. 第二个中间件,如果请求路径是 /@modules/开头的话,我们就请求路径修改成node_modules对应的文件路径,然后继续交给静态文件中间件处理
// 3. 加载第三方模块
app.use(async (ctx, next) => {
  // ctx.path --> /@modules/vue
  if(ctx.path.startsWith(`/@modules/`)) {
    // 拿到模块名称
    const moduleName = ctx.path.substr(10);
    // 我们要获取模块的入口文件。也就是esmodules的入口,使用path模块来拼接路径
    // 第一个参数当前项目跟路径,第二个参数node_modules 第三个参数找到模块的package.json文件路径
    const pkgPath = path.join(process.cwd(), 'node_modules', moduleName, 'package.json');
    // 使用require获取模块
    const pkg = require(pkgPath)
    // 第一个参数表示node_modules文件夹下,第二个参数是第三方模块文件夹下,第三个数第三方包的入口文件
    ctx.path = path.join('/node_modules', moduleName, pkg.module)
  }
  await next();
})

复制代码

通过以上代码,启动项目发现第三方模块vue启动成功,但是里面的第三方模块浏览器却没有加载,为什么呢?
image.png

我们观察,浏览器加载app.vue和index.css失败,是因为当加载组件的时候浏览器不能识别这个模块,加载css文件的时候也是一样,不能识别css模块。所以服务器上还需要处理浏览器不能识别的模块,编译完成之后再返回给浏览器可识别的文件。

image.png

编译单文件组件

main.js引入的单文件组件模块和样式模块浏览器都无法识别,浏览器只能处理js模块,所以其他模块必须在服务端处理,当请求单文件模块的时候,需要在服务器上把单文件组件编译成js模块然后返回给浏览器。下面我们可以打开浏览器,看vite中是如何处理单文件组件的。

vite 会将单文件组件编译成一个对象,先加载helloword组件,然后创建了一个组件选项对象,这个对象没有模板,因为模板最终需要转化为render函数,然后挂载到选项对象上
image.png

接下来又加载App.vue,传递一个参数type=template,这次请求是告诉服务器,去帮我绑定一下这个单文件组件的模板。然后返回一个render函数。然后把render函数挂载到上面创建的组件选项对象上。
下面还有两个选项 __hmrId, __file,最后导出组件可选对象

image.png

从这个过程我们可以看到,当浏览器请求单文件组件的时候,服务器会来编译单文件组件文件,并把相应的结果返回给浏览器。

vite中是如何处理单文件组件的:

  • 会发送两次请求
  • 第一次请求时把单文件编译成一个组件对象
  • 第二次请求时编译单文件组件模板,返回一个render函数,并把render函数挂载到前面生成的组件对象上

接下来我们用代码实现这个过程:

const stringToStream = text => {
  const stream = new Readable;
  stream.push(text);
  stream.push(null);
  return stream;
}
// 4. 处理单文件组件
app.use(async (ctx, next) => {
  // 分两次处理单文件组件
  //第一个判断是否是单文件组件
  if(ctx.path.endsWith('.vue')) {
    const contents = awit streamToString(ctx.body);
    // 返回一个对象,单文件组件描述对象
    const {descriptor} = compilerSFC.parse(contents)
    let code
    if(!ctx.query.type === 'template') {
      code = descriptor.script.content;

      code = code.replace(/export\s+default\s/g, 'const __script = ')
      code+= `
      import { render as __render } from "${ctx.path}?type=template"
      __script.render = __render
      console.log('haha')
      export default __script
            `
    }
    ctx.type = 'appliction/javascript'
    ctx.body = stringToStream(code)
  }
  await next()
})

复制代码

这个代码应该放在在加载第三方模块之前的一个中间间,处理完之后再加载第三方组件

image.png
我们可以看到进过以上代码的处理编译,服务器返回的内容确实是我们想要的。但是浏览器还是出现两个错误

image.png
一个是加载css模块报错,第二个是前面的报错回到值后面的import不在会 发起请求.为了不干扰第二次请求,我们将vite创建vue3项目中的css引入和img模块引入都注释先

处理第二次请求

如果query.type属性值为template则需要处理第二次请求

// 4. 处理单文件组件
app.use(async (ctx, next) => {
  // 分两次处理单文件组件
  //第一个判断是否是单文件组件
  if(ctx.path.endsWith('.vue')) {
    ··· 
    } else if(ctx.query.type === 'template') {
      // compilerSFC对象里面有一个方法,接受一个对象形式的参数就是编译模板的内容
      const templateRender = compilerSFC.compileTemplate({source: descriptor.template.content})
      code = templateRender.code
    }
    ctx.type = 'application/javascript';
    ctx.body = stringToStream(code);
  }
  await next()
})
// 2
复制代码

使用compilerSFC处理完之后,浏览器还是空白,而且报了一个错,process,这个值是node环境下使用的,服务器还没有处理就将这个变量返回,是不行的。

image.png

可以看出源码中process.env.NODE_ENV的判断生产环境还是开发环境打包操作的,但是我们现在没有用打包工具,所以这句话没有帮我们处理直接返回了浏览器,浏览器不认识所以报错了。服务器一定要将其处理才能返回。

image.png

所以服务器将返回的代码含有process.env.NODE_ENV的代码都转化为”development”即可

// 2. 修改第三方模块的路径
app.use(async (ctx, next) => {
  if(ctx.type === 'application/javascript') {
    // ctx.body的值是一个流,但是我们要把流文件转化为字符串。才能将第三方模块的路径发生改变,而且这个功能其他位置还要用我们抽成一个方法
    const contents = await streamToString(ctx.body);
    // import vue from 'vue'
    // import App from './App.vue' // 浏览器可识别就不处理了
    ctx.body = contents
      // 非获取匹配,正向否定预查,在任何不匹配pattern的字符串开始处匹配查找字符串,该匹配不需要获取供以后使用。例如“Windows(?!95|98|NT|2000)”能匹配“Windows3.1”中的“Windows”,但不能匹配“Windows2000”中的“Windows”。
      // import vue from 'vue' ---> import vue from '/@modules/vue'
      // import db from '../db/index' ---> import db from '../db/index'
      .replace(/(from\s+['"])(?![\.\/])/g, '$1/@modules/')
      .replace(/process\.env\.NODE_ENV/g, '"development"')
  }
})
复制代码

在再vite创建的项目中允许命令vite-cli,效果终于出来啦。
image.png

没有样式是正确的,因为css模块我们没有处理,也没有将css模块转化成js模块,所以如果想做可以更具单文件组件转化一样,将css模块先编译在返回浏览器应该也会很容易,我们知道vite大概原理即可,剩下的可以很容易的一步一步实现我相信,好了vue3学习的第三天,欢迎查看,关注,点赞,评论!!!

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享