尤大前端新玩具-vite 简介 对比Webpack 手写实现简单的vite实现

文章目录

  • Vite
    • 定义
    • 由来
    • vite创建项目
    • 对比差异点
    • vite速度体验
      • viet两个子命令:
      • webpack启动过程:
      • vite启动过程:
      • vite第一次启动时的编译
      • vite的按需请求
      • NPM 依赖解析和预构建:
      • HMR 模块热重载
    • 打包和不打包的问题
    • 实现原理
      • 核心思路
      • 手写实现vite
    • 小结

Vite

定义

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

Vite (法语意为 “快速的”,发音 /vit/) 是一种新型前端构建工具,能够显著提升前端开发体验,它主要由两部分组成:

  • 一个开发服务器,它利用 原生 ES 模块 提供了 丰富的内建功能,如速度快到惊人的 模块热更新(HMR)。
  • 一套构建指令,它使用 Rollup 打包你的代码,预配置输出高度优化的静态资源用于生产。

Vite 意在提供更开箱即用的配置,同时它的 插件 API 和 JavaScript API 带来了高度的可扩展性,并完全支持类型化

由来

如果应用比较复杂,使用Webpack的开发过程相对没有那么丝滑

  • Webpack Dev Server冷启动时间比较长
  • Webpack HMR 热更新的反应速度比较慢

对于冷启动和vite启动的区别,vite官网是这么说的(以下摘于官网):

当冷启动开发服务器时,基于打包器的方式是在提供服务前,急切地抓取和构建你整个应用

vite通过在一开始将应用中的模块区分为依赖源码两类,改进了开发服务器启动时间。

  • 依赖:大多为纯JavaScript 并在开发时不会变动。一些较大的依赖(例如有上百个模块的组件库)处理的代价也很高。依赖也通常会以某些方式(例如 ESM 或者 CommonJS)被拆分到大量小模块中。

    Vite 将会使用 esbuild 预构建依赖。Esbuild 使用 Go 编写,并且比以 JavaScript 编写的打包器预构建依赖快 10-100 倍。

  • 源码:通常包含一些并非直接是 JavaScript 的文件,需要转换(例如 JSX,CSS 或者 Vue/Svelte 组件),时常会被编辑。同时,并不是所有的源码都需要同时被加载。(例如基于路由拆分的代码模块)。

    Vite 以 原生 ESM 方式服务源码。这实际上是让浏览器接管了打包程序的部分工作:Vite 只需要在浏览器请求源码时进行转换并按需提供源码。根据情景动态导入的代码,即只在当前屏幕上实际使用时才会被处理。

当基于打包器启动时,编辑文件后将重新构建文件本身。显然我们不应该重新构建整个包,因为这样更新速度会随着应用体积增长而直线下降。

一些打包器的开发服务器将构建内容存入内存,这样它们只需要在文件更改时使模块图的一部分失活[1],但它也仍需要整个重新构建并重载页面。这样代价很高,并且重新加载页面会消除应用程序的当前状态,所以打包器支持了动态模块热重载(HMR):允许一个模块 “热替换” 它自己,而对页面其余部分没有影响。这大大改进了开发体验 - 然而,在实践中我们发现,即使是 HMR 更新速度也会随着应用程序规模的增长而显著下降。

在 Vite 中,HMR 是在原生 ESM 上执行的。当编辑一个文件时,Vite 只需要精确地使已编辑的模块与其最近的 HMR 边界之间的链失效(大多数时候只需要模块本身),使 HMR 更新始终快速,无论应用程序的大小。

Vite 同时利用 HTTP 头来加速整个页面的重新加载(再次让浏览器为我们做更多事情):源码模块的请求会根据 304 Not Modified 进行协商缓存,而依赖模块请求则会通过 Cache-Control: max-age=31536000,immutable 进行强缓存,因此一旦被缓存它们将不需要再次请求。

一旦你体验到 Vite 有多快,我们十分怀疑你是否愿意再忍受像曾经那样使用打包器开发。

vite创建项目

Vite官方目前提供了一个比较简单的脚手架 create-vite-app 可以使用这个脚手架快速创建一个使用vite构建的vue.js应用

npm:

创建项目

npm init vite-app

启动项目

cd

npm run dev

yarn:

创建项目

yarn create vite-app

启动项目

cd

yarn dev

npm init 或者 yarn create 是这两个包管理工具提供的新功能,其内部就是自动去安装一个create-的模块(临时),然后自动执行这个模块中的bin

你还可以通过附加的命令行选项直接指定项目名称和你想要使用的模板。例如,要构建一个 Vite + Vue 项目,运行:

# npm 6.x
npm init @vitejs/app my-vue-app --template vue

# npm 7+, 需要额外的双横线:
npm init @vitejs/app my-vue-app -- --template vue

# yarn
yarn create @vitejs/app my-vue-app --template vue

支持的模板预设包括:

  • vanilla
  • vue
  • vue-ts
  • react
  • react-ts
  • preact
  • preact-ts
  • lit-element
  • lit-element-ts

对比差异点

打开生成的项目过后,你会发现是一个很普通的vue.js应用,没有太多特殊的地方。

不过相比于之前的vue-cli构建的项目或者是基于webpack搭建的vue.js项目,这里开发依赖非常简单,只有vite@vue/compiler-sfc

vite就是我们今天要说的主角,而@vue/compiler-sfc就是用来编译我们项目中.vue结尾的单文件组件(SFC),它取代的就是vue.js 2.x时使用的vue-template-compiler

再者就是Vite只支持Vue3.0版本 (实现原理了解以后,还可以改造vite支持vue2.0)

vite速度体验

viet两个子命令:

  • serve:启动一个用于开发的服务器
  • build:构建整个项目(上线)

当我们执行vite serve的时候,你会发现响应速度非常快,几乎就是秒开

可能单独体验你不会有太明显的感觉,你可以对比使用vue-cli-serive(内部还是webpack)启动开发服务器

当我们对比使用vue-cli-service serve的时候,你会有更明显的感觉。

因为Webpack Dev Server在启动的时候,需要先build一遍,而build的过程是需要耗费很多时间的。

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

webpack启动过程:

尤大前端新玩具-vite 简介 对比Webpack 手写实现简单的vite实现_第1张图片

而vite完全是不同的,当我们执行了vite serve时,内部直接启动了Web server并不会先编译所有的代码文件,仅仅是启动了web server服务

vite启动过程:

尤大前端新玩具-vite 简介 对比Webpack 手写实现简单的vite实现_第2张图片

对于vite来说,它利用现代浏览器原生支持ESM特性,省略了对模块的打包,对于需要编译的文件,vite采用的是另外一种模式:即时编译也就是说只有具体去请求某个文件时才会编译这个文件。

vite第一次启动时的编译

对于vite第一次启动的时候,vite也会进行一次简短的编译。这是由于我们的main.js引入了vue的模块,而vue的模块是存在于node_modules中的,而node_modules里面的模块会有个问题就是一个模块依赖另外一个模块,而我们一般不会去动里面的东西,vite会在node_modules\.vite_opt_cache下生成一个依赖的缓存,而这个文件第一次被创建以后,只要配置没有发生变化是不会改变的,下次启动就会很迅速。

Vite 会将预构建的依赖缓存到 node_modules/.vite

它根据几个源来决定是否需要重新运行预构建步骤:

  • package.json 中的 dependencies 列表
  • 包管理器的 lockfile,例如 package-lock.json, yarn.lock,或者 pnpm-lock.yaml
  • 可能在 vite.config.js 相关字段中配置过的

只有当上面的一个步骤发生变化时,才需要重新运行预构建步骤。

如果出于某些原因,你想要强制 Vite 重新绑定依赖,你可以用 --force 命令行选项启动开发服务器,或者手动删除 node_modules/.vite 目录。

vite的按需请求

vite启动的时候,会将项目的根目录会作为静态文件服务器的根目录。

vite构建的项目目录:

尤大前端新玩具-vite 简介 对比Webpack 手写实现简单的vite实现_第3张图片

index.html:

尤大前端新玩具-vite 简介 对比Webpack 手写实现简单的vite实现_第4张图片

index.html文件请求

尤大前端新玩具-vite 简介 对比Webpack 手写实现简单的vite实现_第5张图片

我们请求的html文件就是根目录下的index.html文件,我们可以看到请求过来的index.html文件中的script标签的type属性为module 这个就是ESM定义的一个标准,一旦这么写的时候,那么它加载的时候,它可以使用ESM的标准,这样就可以使用import去组织模块化,而这块不是vite所实现的,这是浏览器自带的。这样main.js就可以去加载一些模块文件。

NPM 依赖解析和预构建:

原生 ES 引入不支持下面这样的裸模块导入:

import { someMethod } from 'my-dep'

上面的操作将在浏览器中抛出一个错误。Vite 将在服务的所有源文件中检测此类裸模块导入,并执行以下操作:

  1. 预构建 他们以提升页面重载速度,并将 CommonJS / UMD 转换为 ESM 格式。预构建这一步由 esbuild 执行,这使得 Vite 的冷启动时间比任何基于 javascript 的打包程序都要快得多。
  2. 重写导入为合法的 URL,例如 /node_modules/.vite/my-dep.js?v=f3sf2ebd 以便浏览器能够正确导入它们。

HMR 模块热重载

同样也是模式的问题,热更新的时候,Vite只需要立即编译当前所修改的文件即可,然后传递给页面,所以响应速度非常快

而Webpack修改某个文件过后,会自动以这个文件为入口重写build一次,所有涉及到的依赖也会被重新加载一边,所以反应速度会非常慢。

打包和不打包的问题

vite的出现,引发了另外一个值得我们思考的问题:究竟还有没有必要打包应用

之前我们使用Webpack打包应用代码,使之称为一个bundle.js,主要有两个原因:

  1. 浏览器环境并不支持模块化
  2. 零散的模块文件会产生大量的HTTP请求

随着浏览器对ES标准支持的逐渐完善,第一个问题已经慢慢不存在了,大多数浏览器都是支持ES modules的

尤大前端新玩具-vite 简介 对比Webpack 手写实现简单的vite实现_第6张图片

零散模块文件确实会产生大量的HTTP请求,而大量的HTTP请求在浏览器端就会并发请求资源的问题

并行请求就会产生因为域名连接数超限而被挂起等待一段时间。

在HTTP 1.1的标准下,每次请求都是单独建立TCP连接,经过完整的通讯过程,非常耗时。而且每次请求除了请求体中的内容,请求头也会包含很多数据,大量请求会耗费很多的资源。而在HTTP2.0也不复存在了,但是vite为了兼容性,还是选择了打包的方式。

实现原理

vite的核心功能:Static Server + Compiler + HMR

核心思路

  1. 将当前项目目录作为静态文件服务器的根目录
  2. 拦截部分文件请求
    1. 处理代码中import node_modules中的模块
    2. 处理vue单文件组件(SFC)的编译
  3. 通过WebSocket实现HMR

手写实现vite

  1. 删除vite项目中的vite依赖

尤大前端新玩具-vite 简介 对比Webpack 手写实现简单的vite实现_第7张图片

  1. 创建一个文件夹,里面创建js文件模拟实现vite

尤大前端新玩具-vite 简介 对比Webpack 手写实现简单的vite实现_第8张图片

  1. 创建js文件和package.json文件

    npm init

    创建cli.js文件

    给webpack.json文件中增加bin配置指向cli.js

尤大前端新玩具-vite 简介 对比Webpack 手写实现简单的vite实现_第9张图片

  1. 编写cli.js文件.(等会vite启动就使用cli.js文件里面编写实现vite的代码。)

  2. 安装koa依赖

    npm install koa --save // 类似express

    npm install koa-send --save //用于项目目录作为静态文件服务器的根目录

  3. 简单配置koa先让服务跑起来,并且能够访问到目录下的index.html

    #!/usr/bin/env node
    // 文件头,给linux系统用的,意思就是用node去执行当前这个文件脚本
    
    // 1. 将当前项目目录作为静态文件服务器的根目录
    // 用koa-send 去实现将当前目录作为静态文件服务器的根目录,因为它会有一些中间件。static封装的太彻底,没法简单修改
    const send = require('koa-send');
    // 这里web服务器用koa去做,vite也用的koa,类似express
    const Koa = require('koa');
    const app = new Koa();
    app.use(async (ctx, next) =>{
           
      ctx.body = 'my-vite';
      // 将项目目录作为静态文件服务器的根目录,并自动请求index.html文件
      // 因为有的路径为不完整的路径,所以通过send的第三个参数做一些处理
      await send(ctx, ctx.path, {
            root: process.cwd(), index: 'index.html'});// 有可能还要额外处理相应结果
      await next();
    })
    
    app.listen(3080);
    
    console.log('Server-running-@-http://localhost:3080');
    
  4. 在删除vite依赖的项目中将my-vite(我们编写的实现vite)文件夹作为本地npm依赖进行安装,并更改启动命令配置

    npm install …/my-vite

    此时node_modules/.bin文件夹下有了一个my-vite的命令文件

尤大前端新玩具-vite 简介 对比Webpack 手写实现简单的vite实现_第10张图片

更改package.json配置

尤大前端新玩具-vite 简介 对比Webpack 手写实现简单的vite实现_第11张图片

  1. 测试

    这里页面请求成功了,但是报错:
    Uncaught TypeError: Failed to resolve module specifier “vue”. Relative references must start with either “/”, “./”, or “…/”.
    这就是因为请求的这个页面它里面调用了main.js这个文件这个文件里面又importvue这个文件import { createApp } from 'vue',原生 ES 引入不支持下面这样的裸模块导入,所以它会报错,它会找不到这个模块。我们接下来可以使用Koa提供的中间件去处理这个问题。

尤大前端新玩具-vite 简介 对比Webpack 手写实现简单的vite实现_第12张图片

​ HTML页面请求成功

尤大前端新玩具-vite 简介 对比Webpack 手写实现简单的vite实现_第13张图片

​ 分析这个报错的原因,因为我们一般开发当中,如果使用import { createApp } from 'vue', 我们是知道它会去项目目录里面的node_module/vue/package.json文件里面去找对应的 module配置,然后在dist里面调相应的js文件。
尤大前端新玩具-vite 简介 对比Webpack 手写实现简单的vite实现_第14张图片

所以如果人工去修改这个问题的话,只需要将dist下对应文件的路径配置在Import语句里即可,比如:import { createApp } from '../node_modules/vue/dist/vue.runtime.esm-bundler.js',当然这是不行的,我们更希望开发者能够更友好和简单的去引入模块

  1. 替换代码中的特殊位置

    我们可以看到vite内部对于引入vue做的操作是将引入的目录改为@modules/vue.js我们也来实现这样的操作,我们可以简单用正则表达式来实现。

尤大前端新玩具-vite 简介 对比Webpack 手写实现简单的vite实现_第15张图片

因为send的时候,它已经把那个文件读出来了,而且将结果放在了ctx.body当中,我们可以输出一下ctx.body看一下,它输出了一个ReadStream文件读取流。我们就要把这个读取流里面的内容都拿出来。这里我们可以用一个处理函数去处理一下,然后转成字符串

app.use(async (ctx,next) => {
     
  console.log(ctx.body)
})
// 输出结果
ReadStream {
     
  _readableState: ReadableState {
     
    objectMode: false,
    highWaterMark: 65536,
    buffer: BufferList {
      head: null, tail: null, length: 0 },      
    length: 0,
    pipes: [],
    flowing: null,
    ended: false,
    endEmitted: false,
    reading: false,
    sync: true,
    needReadable: false,
    emittedReadable: false,
    readableListening: false,
    resumeScheduled: false,
    errorEmitted: false,
    emitClose: true,
    autoDestroy: false,
    destroyed: false,
    errored: null,
    closed: false,
    closeEmitted: false,
    defaultEncoding: 'utf8',
    awaitDrainWriters: null,
    multiAwaitDrain: false,
    readingMore: false,
    decoder: null,
    encoding: null,
    [Symbol(kPaused)]: null
  },
  _events: [Object: null prototype] {
     
    end: [Function (anonymous)],
    error: [Function: bound onceWrapper] {
      listener: [Function (anonymous)] }
  },
  _eventsCount: 2,
  _maxListeners: undefined,
  path: 'D:\\Iprogram\\programs\\vite\\vite-myself\\index.html',   
  fd: null,
  flags: 'r',
  mode: 438,
  start: undefined,
  end: Infinity,
  autoClose: true,
  pos: undefined,
  bytesRead: 0,
  closed: false,
  [Symbol(kFs)]: {
     
    appendFile: [Function: appendFile],
    appendFileSync: [Function: appendFileSync],
    access: [Function: access],
    accessSync: [Function: accessSync],
    chown: [Function: chown],
    chownSync: [Function: chownSync],
    chmod: [Function: chmod],
    chmodSync: [Function: chmodSync],
    close: [Function: close],
    closeSync: [Function: closeSync],
    copyFile: [Function: copyFile],
    copyFileSync: [Function: copyFileSync],
    createReadStream: [Function: createReadStream],
    createWriteStream: [Function: createWriteStream],
    exists: [Function: exists],
    existsSync: [Function: existsSync],
    fchown: [Function: fchown],
    fchownSync: [Function: fchownSync],
    fchmod: [Function: fchmod],
    fchmodSync: [Function: fchmodSync],
    fdatasync: [Function: fdatasync],
    fdatasyncSync: [Function: fdatasyncSync],
    fstat: [Function: fstat],
    fstatSync: [Function: fstatSync],
    fsync: [Function: fsync],
    fsyncSync: [Function: fsyncSync],
    ftruncate: [Function: ftruncate],
    ftruncateSync: [Function: ftruncateSync],
    futimes: [Function: futimes],
    futimesSync: [Function: futimesSync],
    lchown: [Function: lchown],
    lchownSync: [Function: lchownSync],
    lchmod: undefined,
    lchmodSync: undefined,
    link: [Function: link],
    linkSync: [Function: linkSync],
    lstat: [Function: lstat],
    lstatSync: [Function: lstatSync],
    lutimes: [Function: lutimes],
    lutimesSync: [Function: lutimesSync],
    mkdir: [Function: mkdir],
    mkdirSync: [Function: mkdirSync],
    mkdtemp: [Function: mkdtemp],
    mkdtempSync: [Function: mkdtempSync],
    open: [Function: open],
    openSync: [Function: openSync],
    opendir: [Function: opendir],
    opendirSync: [Function: opendirSync],
    readdir: [Function: readdir],
    readdirSync: [Function: readdirSync],
    read: [Function: read],
    readSync: [Function: readSync],
    readv: [Function: readv],
    readvSync: [Function: readvSync],
    readFile: [Function: readFile],
    readFileSync: [Function: readFileSync],
    readlink: [Function: readlink],
    readlinkSync: [Function: readlinkSync],
    realpath: [Function: realpath] {
      native: [Function (anonymous)] },
    realpathSync: [Function: realpathSync] {
      native: [Function (anonymous)] },
    rename: [Function: rename],
    renameSync: [Function: renameSync],
    rm: [Function: rm],
    rmSync: [Function: rmSync],
    rmdir: [Function: rmdir],
    rmdirSync: [Function: rmdirSync],
    stat: [Function: stat],
    statSync: [Function: statSync],
    symlink: [Function: symlink],
    symlinkSync: [Function: symlinkSync],
    truncate: [Function: truncate],
    truncateSync: [Function: truncateSync],
    unwatchFile: [Function: unwatchFile],
    unlink: [Function: unlink],
    unlinkSync: [Function: unlinkSync],
    utimes: [Function: utimes],
    utimesSync: [Function: utimesSync],
    watch: [Function: watch],
    watchFile: [Function: watchFile],
    writeFile: [Function: writeFile],
    writeFileSync: [Function: writeFileSync],
    write: [Function: write],
    writeSync: [Function: writeSync],
    writev: [Function: writev],
    writevSync: [Function: writevSync],
    Dir: [class Dir],
    Dirent: [class Dirent],
    Stats: [Function: Stats],
    ReadStream: [Getter/Setter],
    WriteStream: [Getter/Setter],
    FileReadStream: [Getter/Setter],
    FileWriteStream: [Getter/Setter],
    _toUnixTimestamp: [Function: toUnixTimestamp],
    F_OK: 0,
    R_OK: 4,
    W_OK: 2,
    X_OK: 1,
    constants: [Object: null prototype] {
     
      UV_FS_SYMLINK_DIR: 1,
      UV_FS_SYMLINK_JUNCTION: 2,
      O_RDONLY: 0,
      O_WRONLY: 1,
      O_RDWR: 2,
      UV_DIRENT_UNKNOWN: 0,
      UV_DIRENT_FILE: 1,
      UV_DIRENT_DIR: 2,
      UV_DIRENT_LINK: 3,
      UV_DIRENT_FIFO: 4,
      UV_DIRENT_SOCKET: 5,
      UV_DIRENT_CHAR: 6,
      UV_DIRENT_BLOCK: 7,
      S_IFMT: 61440,
      S_IFREG: 32768,
      S_IFDIR: 16384,
      S_IFCHR: 8192,
      S_IFLNK: 40960,
      O_CREAT: 256,
      O_EXCL: 1024,
      UV_FS_O_FILEMAP: 536870912,
      O_TRUNC: 512,
      O_APPEND: 8,
      F_OK: 0,
      R_OK: 4,
      W_OK: 2,
      X_OK: 1,
      UV_FS_COPYFILE_EXCL: 1,
      COPYFILE_EXCL: 1,
      UV_FS_COPYFILE_FICLONE: 2,
      COPYFILE_FICLONE: 2,
      UV_FS_COPYFILE_FICLONE_FORCE: 4,
      COPYFILE_FICLONE_FORCE: 4
    },
    promises: [Getter]
  },
  [Symbol(kCapture)]: false,
  [Symbol(kIsPerformingIO)]: false
}
ReadStream {
     
  _readableState: ReadableState {
     
    objectMode: false,
    highWaterMark: 65536,
    buffer: BufferList {
      head: null, tail: null, length: 0 },      
    length: 0,
    pipes: [],
    flowing: null,
    ended: false,
    endEmitted: false,
    reading: false,
    sync: true,
    needReadable: false,
    emittedReadable: false,
    readableListening: false,
    resumeScheduled: false,
    errorEmitted: false,
    emitClose: true,
    autoDestroy: false,
    destroyed: false,
    errored: null,
    closed: false,
    closeEmitted: false,
    defaultEncoding: 'utf8',
    awaitDrainWriters: null,
    multiAwaitDrain: false,
    readingMore: false,
    decoder: null,
    encoding: null,
    [Symbol(kPaused)]: null
  },
  _events: [Object: null prototype] {
     
    end: [Function (anonymous)],
    error: [Function: bound onceWrapper] {
      listener: [Function (anonymous)] }
  },
  _eventsCount: 2,
  _maxListeners: undefined,
  path: 'D:\\Iprogram\\programs\\vite\\vite-myself\\src\\main.js', 
  fd: null,
  flags: 'r',
  mode: 438,
  start: undefined,
  end: Infinity,
  autoClose: true,
  pos: undefined,
  bytesRead: 0,
  closed: false,
  [Symbol(kFs)]: {
     
    appendFile: [Function: appendFile],
    appendFileSync: [Function: appendFileSync],
    access: [Function: access],
    accessSync: [Function: accessSync],
    chown: [Function: chown],
    chownSync: [Function: chownSync],
    chmod: [Function: chmod],
    chmodSync: [Function: chmodSync],
    close: [Function: close],
    closeSync: [Function: closeSync],
    copyFile: [Function: copyFile],
    copyFileSync: [Function: copyFileSync],
    createReadStream: [Function: createReadStream],
    createWriteStream: [Function: createWriteStream],
    exists: [Function: exists],
    existsSync: [Function: existsSync],
    fchown: [Function: fchown],
    fchownSync: [Function: fchownSync],
    fchmod: [Function: fchmod],
    fchmodSync: [Function: fchmodSync],
    fdatasync: [Function: fdatasync],
    fdatasyncSync: [Function: fdatasyncSync],
    fstat: [Function: fstat],
    fstatSync: [Function: fstatSync],
    fsync: [Function: fsync],
    fsyncSync: [Function: fsyncSync],
    ftruncate: [Function: ftruncate],
    ftruncateSync: [Function: ftruncateSync],
    futimes: [Function: futimes],
    futimesSync: [Function: futimesSync],
    lchown: [Function: lchown],
    lchownSync: [Function: lchownSync],
    lchmod: undefined,
    lchmodSync: undefined,
    link: [Function: link],
    linkSync: [Function: linkSync],
    lstat: [Function: lstat],
    lstatSync: [Function: lstatSync],
    lutimes: [Function: lutimes],
    lutimesSync: [Function: lutimesSync],
    mkdir: [Function: mkdir],
    mkdirSync: [Function: mkdirSync],
    mkdtemp: [Function: mkdtemp],
    mkdtempSync: [Function: mkdtempSync],
    open: [Function: open],
    openSync: [Function: openSync],
    opendir: [Function: opendir],
    opendirSync: [Function: opendirSync],
    readdir: [Function: readdir],
    readdirSync: [Function: readdirSync],
    read: [Function: read],
    readSync: [Function: readSync],
    readv: [Function: readv],
    readvSync: [Function: readvSync],
    readFile: [Function: readFile],
    readFileSync: [Function: readFileSync],
    readlink: [Function: readlink],
    readlinkSync: [Function: readlinkSync],
    realpath: [Function: realpath] {
      native: [Function (anonymous)] },
    realpathSync: [Function: realpathSync] {
      native: [Function (anonymous)] },
    rename: [Function: rename],
    renameSync: [Function: renameSync],
    rm: [Function: rm],
    rmSync: [Function: rmSync],
    rmdir: [Function: rmdir],
    rmdirSync: [Function: rmdirSync],
    stat: [Function: stat],
    statSync: [Function: statSync],
    symlink: [Function: symlink],
    symlinkSync: [Function: symlinkSync],
    truncate: [Function: truncate],
    truncateSync: [Function: truncateSync],
    unwatchFile: [Function: unwatchFile],
    unlink: [Function: unlink],
    unlinkSync: [Function: unlinkSync],
    utimes: [Function: utimes],
    utimesSync: [Function: utimesSync],
    watch: [Function: watch],
    watchFile: [Function: watchFile],
    writeFile: [Function: writeFile],
    writeFileSync: [Function: writeFileSync],
    write: [Function: write],
    writeSync: [Function: writeSync],
    writev: [Function: writev],
    writevSync: [Function: writevSync],
    Dir: [class Dir],
    Dirent: [class Dirent],
    Stats: [Function: Stats],
    ReadStream: [Getter/Setter],
    WriteStream: [Getter/Setter],
    FileReadStream: [Getter/Setter],
    FileWriteStream: [Getter/Setter],
    _toUnixTimestamp: [Function: toUnixTimestamp],
    F_OK: 0,
    R_OK: 4,
    W_OK: 2,
    X_OK: 1,
    constants: [Object: null prototype] {
     
      UV_FS_SYMLINK_DIR: 1,
      UV_FS_SYMLINK_JUNCTION: 2,
      O_RDONLY: 0,
      O_WRONLY: 1,
      O_RDWR: 2,
      UV_DIRENT_UNKNOWN: 0,
      UV_DIRENT_FILE: 1,
      UV_DIRENT_DIR: 2,
      UV_DIRENT_LINK: 3,
      UV_DIRENT_FIFO: 4,
      UV_DIRENT_SOCKET: 5,
      UV_DIRENT_CHAR: 6,
      UV_DIRENT_BLOCK: 7,
      S_IFMT: 61440,
      S_IFREG: 32768,
      S_IFDIR: 16384,
      S_IFCHR: 8192,
      S_IFLNK: 40960,
      O_CREAT: 256,
      O_EXCL: 1024,
      UV_FS_O_FILEMAP: 536870912,
      O_TRUNC: 512,
      O_APPEND: 8,
      F_OK: 0,
      R_OK: 4,
      W_OK: 2,
      X_OK: 1,
      UV_FS_COPYFILE_EXCL: 1,
      COPYFILE_EXCL: 1,
      UV_FS_COPYFILE_FICLONE: 2,
      COPYFILE_FICLONE: 2,
      UV_FS_COPYFILE_FICLONE_FORCE: 4,
      COPYFILE_FICLONE_FORCE: 4
    },
    promises: [Getter]
  },
  [Symbol(kCapture)]: false,
  [Symbol(kIsPerformingIO)]: false
}

文件读取流处理函数:

// 文件读取流处理函数
const streamToString = stream => {
     
  // 读取操作涉及到异步,所以使用Promise
  return new Promise((resolve, reject) => {
     
    const chunks = [];
    // 流读取因为是一个片段一个片段的,每次片段会触发一个data事件
    // 每次事件里我们会拿到一个chunk,我们push到chunks里面
    stream.on('data', chunk =>{
     
      chunks.push(chunk)
    });
    // 读取结束触发end事件,将Buffer处理过后chunks的转成字符串,resolve返回出去
    stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
    // 如果出错直接传递reject
    stream.on('error', reject);
  })
}

我们可以输出一下使用函数处理过后的ctx.body,我们就可以拿到正确的信息,我们如果要替换里面的内容,可以做一些操作。

尤大前端新玩具-vite 简介 对比Webpack 手写实现简单的vite实现_第16张图片

但是每次请求可能是CSS,HTML,JS等等,我们不能每次都转成字符串来处理,实际上我们只需要将JS转一下再做处理即可。这里我们使用正则去处理一下

app.use(async (ctx, next) =>{
     
  if(ctx.type === 'application/javascript') {
     
    const contexts = await streamToString(ctx.body);
    // 使用正则表达式处理成@module/xxx.js
    // import { createApp } from 'vue'
    // import { createApp } from '/@modules/vue'
    ctx.body = contexts.replace(/(from\s+['"])(?![\.\/])/g, '$1/@modules/')
  }
})

输出:

尤大前端新玩具-vite 简介 对比Webpack 手写实现简单的vite实现_第17张图片

但是这样会导致更多的错误,因为这样去找@modules下的vue是找不到的,我们就要去做请求路径的重写

  1. 重写请求路径

    // 重写请求路径 /@modules/xxx => /node_modules/
    app.use(async (ctx, next) => {
           
      // 只要开头是/@modules/的我们要重写
      if (ctx.path.startsWith('/@modules/')) {
           
        const moduleName = ctx.path.substr(10);  // => 这步只是处理了vue
        const modulePkg = require(path.join(cwd, 'node_modules', moduleName, 'package.json')); // 拿到package.json
        // 拿到真实路径
        ctx.path = path.join('/node_modules', moduleName, modulePkg.module);
      }
    })
    

    这样vue就可以正常访问了

尤大前端新玩具-vite 简介 对比Webpack 手写实现简单的vite实现_第18张图片

但是报了其他的错误,这是因为vue文件去掉了其他的模块。
  1. .vue文件请求的处理,即时编译

    实现即时编译肯定不能自己去实现,我们用到vue的@vue/compiler-sfc

    给my-vite安装

    npm i @vue/compiler-sfc

    // .vue文件请求的处理,即时编译
    app.use(async (ctx, next) =>{
           
      if(ctx.path.endsWith('.vue')) {
           
        // 编译我们肯定不能自己去写,我们要用到@vue/compiler-sfc这个模块
        // parse方法接收单文件组件的内容--源代码
        const contents = await streamToString(ctx.body);
        // 它里面有个descriptor其实就是解析过后的代码
        const {
            descriptor } = compilerSfc.parse(contents);
        // 输出一下descriptor
        console.log(descriptor);
      }
      await next();
    })
    // 输出结果
    {
           
      filename: 'anonymous.vue',
      source: '\n' +
        '\n' +
        '\n',
      template: {
           
        type: 'template',
        content: '\n' +
          '  Vue logo\n' +       
          '  \n',
        loc: {
           
          source: '\n' +
            '  Vue logo\n' +     
            '  \n',
          start: [Object],
          end: [Object]
        },
        attrs: {
           },
        ast: {
           
          type: 1,
          ns: 0,
          tag: 'template',
          tagType: 0,
          props: [],
          isSelfClosing: false,
          children: [Array],
          loc: [Object],
          codegenNode: undefined
        },
        map: {
           
          version: 3,
          sources: [Array],
          names: [],
          mappings: ';EACE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;EAC7C,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC',
          file: 'anonymous.vue',
          sourceRoot: '',
          sourcesContent: [Array]
        }
      },
      script: {
           
        type: 'script',
        content: '\n' +
          "import HelloWorld from './components/HelloWorld.vue'\n" +   
          '\n' +
          'export default {\n' +
          "  name: 'App',\n" +
          '  components: {\n' +
          '    HelloWorld\n' +
          '  }\n' +
          '}\n',
        loc: {
           
          source: '\n' +
            "import HelloWorld from './components/HelloWorld.vue'\n" + 
            '\n' +
            'export default {\n' +
            "  name: 'App',\n" +
            '  components: {\n' +
            '    HelloWorld\n' +
            '  }\n' +
            '}\n',
          start: [Object],
          end: [Object]
        },
        attrs: {
           },
        map: {
           
          version: 3,
          sources: [Array],
          names: [],
          mappings: ';AAMA,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;;AAEnD,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE;EACb,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;EACX,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE;IACV,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;EACX;AACF',
          file: 'anonymous.vue',
          sourceRoot: '',
          sourcesContent: [Array]
        }
      },
      scriptSetup: null,
      styles: [],
      customBlocks: [],
      cssVars: [],
      slotted: false
    }
    
    

    它打印了一个大对象,里面的template其实就是我们在单文件组件的内容,还有个script是一个纯js的函数,我们只需要将script返回回去就好了

    // .vue文件请求的处理,即时编译
    app.use(async (ctx, next) =>{
           
      if(ctx.path.endsWith('.vue')) {
           
        // 编译我们肯定不能自己去写,我们要用到@vue/compiler-sfc这个模块
        // parse方法接收单文件组件的内容--源代码
        const contents = await streamToString(ctx.body);
        // 它里面有个descriptor其实就是解析过后的代码
        const {
            descriptor } = compilerSfc.parse(contents);
        // 这里把ctx.body设置为js脚本,你就要去设置一下contentType
        // 这个contentType是服务端告诉客户端我给你返回的是什么类型的资源
        ctx.type = 'application/javascript';
        // 这样就和下面的中间件产生了冲突,现在的ctx.body是一个字符串而不是一个流,做一个处理
        // 必须先转成Buffer,再转成读取流
        ctx.body = Readable.from(Buffer.from(descriptor.script.content));
      }
      await next();
    })
    

    这样App.vue就是我们返回的script里面的内容
    尤大前端新玩具-vite 简介 对比Webpack 手写实现简单的vite实现_第19张图片
    但是这样它缺少了很重要的组成部分template来作为我们单文件组件的render函数
    这里我们可以看一下vite的处理方法,他就是将export default替换为了一个变量的定义,然后在底下拿了一下这个render

尤大前端新玩具-vite 简介 对比Webpack 手写实现简单的vite实现_第20张图片
我们来实现一下替换(直接暴力替换)

let code = descriptor.script.content;
    code = code.replace(/export\s+default\s+/, 'const __script = ');
    code += `
import { render as __render } from "${ctx.path}?type=template"
__script.render = __render
export default __script`
最后再使用一下code
ctx.body = Readable.from(Buffer.from(code));

尤大前端新玩具-vite 简介 对比Webpack 手写实现简单的vite实现_第21张图片
这里我们要做一下判断,如果请求是template的时候,我们要返回render函数

if(ctx.path.endsWith('.vue')) {
    // 编译我们肯定不能自己去写,我们要用到@vue/compiler-sfc这个模块
    // parse方法接收单文件组件的内容--源代码
    const contents = await streamToString(ctx.body);
    // 它里面有个descriptor其实就是解析过后的代码
    const { descriptor } = compilerSfc.parse(contents);
    let code;
    if (ctx.query.type === undefined) {
      code = descriptor.script.content;
      code = code.replace(/export\s+default\s+/, 'const __script = ');
      code += `
  import { render as __render } from "${ctx.path}?type=template"
  __script.render = __render
  export default __script`
    } else if (ctx.query.type === 'template') {
      // 如果请求是template的话
      // source就是我们要传递的模板内容
      const templateRender = compilerSfc.compileTemplate({ source: descriptor.template.content });
      code = templateRender.code;
    }
    // 这里把ctx.body设置为js脚本,你就要去设置一下contentType
    // 这个contentType是服务端告诉客户端我给你返回的是什么类型的资源
    ctx.type = 'application/javascript';
    // 这样就和下面的中间件产生了冲突,现在的ctx.body是一个字符串而不是一个流,做一个处理
    // 必须先转成Buffer,再转成读取流
    ctx.body = Readable.from(Buffer.from(code));
  }

.
这样页面就可以请求到这个模板函数,拿到这个render

  1. 移除掉css调用和img图片的调用,程序已经能跑起来了

    因为没做css的处理和img的处理,在vite示例项目中,它是调用了css和img

尤大前端新玩具-vite 简介 对比Webpack 手写实现简单的vite实现_第22张图片

尤大前端新玩具-vite 简介 对比Webpack 手写实现简单的vite实现_第23张图片
但是我们一旦移除掉的话,程序是不报错的跑起来了
尤大前端新玩具-vite 简介 对比Webpack 手写实现简单的vite实现_第24张图片

  1. 整体cli.js的展示

    #!/usr/bin/env node
    
    const send = require('koa-send');
    const Koa = require('koa');
    const app = new Koa();
    const cwd = process.cwd();
    const path = require('path');
    const compilerSfc = require('@vue/compiler-sfc');
    const {
            Readable } = require('stream');
    
    // 重写请求路径 /@modules/xxx => /node_modules/
    app.use(async (ctx, next) => {
           
      if (ctx.path.startsWith('/@modules/')) {
           
        const moduleName = ctx.path.substr(10); 
        const modulePkg = require(path.join(cwd, 'node_modules', moduleName, 'package.json'));
        ctx.path = path.join('/node_modules', moduleName, modulePkg.module);
      }
      await next();
    })
    
    
    // 根据请求路径得到相应文件
    app.use(async (ctx, next) =>{
           
      // ctx.body = 'my-vite';
      await send(ctx, ctx.path, {
            root: cwd, index: 'index.html'}); 
      await next();
    })
    
    
    // .vue文件请求的处理,即时编译
    app.use(async (ctx, next) =>{
           
      if(ctx.path.endsWith('.vue')) {
           
        const contents = await streamToString(ctx.body);
        const {
            descriptor } = compilerSfc.parse(contents);
        let code;
        if (ctx.query.type === undefined) {
           
          code = descriptor.script.content;
          code = code.replace(/export\s+default\s+/, 'const __script = ');
          code += `
      import { render as __render } from "${
             ctx.path}?type=template"
      __script.render = __render
      export default __script`
        } else if (ctx.query.type === 'template') {
           
          const templateRender = compilerSfc.compileTemplate({
            source: descriptor.template.content });
          code = templateRender.code;
        }
        ctx.type = 'application/javascript';
        ctx.body = Readable.from(Buffer.from(code));
      }
      await next();
    })
    
    // 替换代码中的特殊位置
    app.use(async (ctx, next) =>{
           
      if(ctx.type === 'application/javascript') {
           
        const contents = await streamToString(ctx.body);
        ctx.body = contents
        .replace(/(from\s+['"])(?![\.\/])/g, '$1/@modules/')
        .replace(/process.env.NODE_ENV/g, '"production"');
      }
      await next();
    })
    
    // 文件读取流处理函数
    const streamToString = stream => {
           
      return new Promise((resolve, reject) => {
           
        const chunks = [];
        stream.on('data', chunk =>{
           
          chunks.push(chunk);
        });
        stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
        stream.on('error', reject);
      })
    }
    
    
    
    app.listen(3080);
    
    console.log('Server-running-@-http://localhost:3080');
    

小结

vite带来的优势主要体现在提升开发者在开发过程中的体验

  • Dev Server 无需等待,即时启动。
  • 几乎实时的模块热更新
  • 所需文件按需编译,避免编译用不到的文件
  • 开箱即用,避免各种Loader和Plugin的配置

你可能感兴趣的:(VUE,vue,javascript,js,webpack)