{"id":16434780,"url":"https://github.com/zce/vite-essentials","last_synced_at":"2025-03-21T04:33:04.990Z","repository":{"id":42759029,"uuid":"278518083","full_name":"zce/vite-essentials","owner":"zce","description":"Vite essentials","archived":false,"fork":false,"pushed_at":"2023-01-07T19:58:03.000Z","size":2171,"stargazers_count":42,"open_issues_count":9,"forks_count":14,"subscribers_count":3,"default_branch":"master","last_synced_at":"2024-10-12T08:50:04.139Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/zce.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-07-10T02:22:30.000Z","updated_at":"2023-03-12T08:38:46.000Z","dependencies_parsed_at":"2023-02-08T00:01:00.217Z","dependency_job_id":null,"html_url":"https://github.com/zce/vite-essentials","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zce%2Fvite-essentials","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zce%2Fvite-essentials/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zce%2Fvite-essentials/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zce%2Fvite-essentials/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/zce","download_url":"https://codeload.github.com/zce/vite-essentials/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":221811541,"owners_count":16884363,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-11T08:50:05.687Z","updated_at":"2024-10-28T09:25:33.206Z","avatar_url":"https://github.com/zce.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 前端新玩具：Vite\n\n## 知识储备\n\n- 掌握 ES Modules 特性\n- 了解 HTTP 2 标准\n\n## 相关介绍\n\n### Vite 的定义\n\n面向现代浏览器的一个更轻、更快的 Web 应用开发工具，\n\n基于 ECMAScript 标准原生模块系统（ES Modules）实现。\n\n### Vite 的由来\n\n如果应用比较复杂，使用 Webpack 的开发过程相对没有那么丝滑。\n\n- Webpack Dev Server 冷启动时间会比较长\n- Webpack HMR 热更新的反应速度比较慢\n\n## 快速上手\n\nVite 官方目前提供了一个比较简单的脚手架：create-vite-app，可以使用这个脚手架快速创建一个使用 Vite 构建的 Vue.js 应用\n\n```shell\n$ npm init vite-app \u003cproject-name\u003e\n$ cd \u003cproject-name\u003e\n$ npm install\n$ npm run dev\n```\n\n如果使用 yarn：\n\n```shell\n$ yarn create vite-app \u003cproject-name\u003e\n$ cd \u003cproject-name\u003e\n$ yarn\n$ yarn dev\n```\n\n\u003e P.S.\n\u003e `npm init` 或者 `yarn create` 是这两个包管理工具提供的新功能，\n\u003e 其内部就是自动去安装一个 `create-\u003cxxx\u003e` 的模块（临时），然后自动执行这个模块中的 bin。\n\u003e 例如:\n\u003e `yarn create react-app my-react-app` 就相当于先 `yarn global add create-react-app`，然后自动执行 `create-react-app my-react-app`\n\n### 对比差异点\n\n打开生成的项目过后，你会发现就是一个很普通的 Vue.js 应用，没有太多特殊的地方。\n\n不过相比于之前 vue-cli 创建的项目或者是基于 Webpack 搭建的 Vue.js 项目，这里的开发依赖非常简单，只有 vite 和 @vue/compiler-sfc。\n\nvite 就是我们今天要介绍的主角，而 @vue/compiler-sfc 就是用来编译我们项目中 .vue 结尾的单文件组件（SFC），它取代的就是 Vue.js 2.x 时使用的 vue-template-compiler。\n\n再者就是 Vue.js 的版本是 3.0。这里尤其需要注意：**Vite 目前只支持 Vue.js 3.0 版本。**\n\n\u003e 如果你想，在后面介绍完实现原理过后，你也可以改造 Vite 让它支持 Vue.js 2.0。\n\n### 基础体验\n\n这里我们所安装的 vite 模块提供了两个子命令：\n\n- serve：启动一个用于开发的服务器\n- build：构建整个项目（上线）\n\n当我们执行 `vite serve` 的时候，你会发现响应速度非常快，几乎就是秒开。\n\n可能单独体验你不会有太明显的感觉，你可以对比使用 `vue-cli-service`（内部还是 Webpack）启动开发服务器，\n\n当我们对比使用 `vue-cli-service serve` 的时候，你会有更明显的感觉。\n\n因为 Webpack Dev Server 在启动时，需要先 build 一遍，而 build 的过程是需要耗费很多时间的。\n\n![](media/webpack.png)\n\n而 Vite 则完全不同，当我们执行 `vite serve` 时，内部直接启动了 Web Server，并不会先编译所有的代码文件。\n\n那仅仅是启动 Web Server，速度上自然就快了很多。\n\n![](media/vite.png)\n\n但是像 Webpack 这类工具的做法是将所有模块提前编译、打包进 bundle 里，换句话说，不管模块是否会被执行，都要被编译和打包到 bundle 里。随着项目越来越大打包后的 bundle 也越来越大，打包的速度自然也就越来越慢。\n\nVite 利用现代浏览器原生支持 ESM 特性，省略了对模块的打包。\n\n对于需要编译的文件，Vite 采用的是另外一种模式：即时编译。\n\n也就是说，只有具体去请求某个文件时才会编译这个文件。\n\n所以，这种「即时编译」的好处主要体现在：按需编译。\n\n### Optimize\n\nVite 还提供了一个目前在帮助列表中并没有呈现的一个子命令：optimize。\n\n这个命令的作用就是单独的去「优化依赖」。\n\n所谓的「优化依赖」，指的就是自动去把代码中依赖的第三方模块提前编译出来。\n\n例如，我们在代码中通过 `import` 载入了 vue 这个模块，那通过这个命令就会自动将这个模块打包成一个单独的 ESM bundle, 放到 `node_modules/.vite_opt_cache` 目录中。\n\n这样后续请求这个文件时就不需要再即时去加载了。\n\n### HMR\n\n同样也是模式的问题，热更新的时候，Vite 只需要立即编译当前所修改的文件即可，所以响应速度非常快。\n\n而 Webpack 修改某个文件过后，会自动以这个文件为入口重写 build 一次，所有的涉及到的依赖也都会被加载一遍，所以反应速度会慢很多。\n\n### Build\n\nVite 在生产模式下打包，需要使用 `vite build` 命令。\n\n这个命令内部采用的是 Rollup 完成的应用打包，最终还是会把文件都提前编译并打包到一起。\n\n对于 Code Splitting 需求，Vite 内部采用的就是原生 Dynamic imports 特性实现的，所以打包结果还是只能够支持现代浏览器。\n\n不过好在 Dynamic imports 特性是可以有 Polyfill 的：https://github.com/GoogleChromeLabs/dynamic-import-polyfill，也就是说，只要你想，它也可以运行在相对低版本的浏览器中。\n\n### 打包 or 不打包\n\nVite 的出现，引发了另外一个值得我们思考的问题：究竟还有没有必要打包应用？\n\n之前我们使用 Webpack 打包应用代码，使之成为一个 bundle.js，主要有两个原因：\n\n1. 浏览器环境并不支持模块化\n2. 零散的模块文件会产生大量的 HTTP 请求\n\n随着浏览器的对 ES 标准支持的逐渐完善，第一个问题已经慢慢不存在了。现阶段绝大多数浏览器都是支持 ES Modules 的。\n\n![es-modules](media/es-modules.png)\n\n零散模块文件确实会产生大量的 HTTP 请求，而大量的 HTTP 请求在浏览器端就会并发请求资源的问题；\n\n![](media/http-1.1.png)\n\n_如上图所示，红色圈出来的请求就是并行请求，但是后面的请求就因为域名链接数已超过限制，而被挂起等待了一段时间。_\n\n在 HTTP 1.1 的标准下，每次请求都需要单独建立 TCP 链接，经过完整的通讯过程，非常耗时；\n\n![](media/http-1.1-connection.png)\n\n而且每次请求除了请求体中的内容，请求头中也会包含很多数据，大量请求的情况下也会浪费很多资源。\n\n但是这些问题随着 HTTP 2 的出现，也就不复存在了。\n\n![](media/http-2-connection.png)\n\n关于 HTTP 1.1 与 HTTP 2 之间的差异，可以通过这个链接体验：https://http2.akamai.com/demo，直观感受下 HTTP/2 比 HTTP/1 到底快了多少。\n\n而且不打包也有一个好处，就是可以把按需加载实现到极致。\n\n\u003e 关于 HTTP 2 的详细介绍，可以参考：\n\u003e\n\u003e - https://blog.fundebug.com/2019/03/07/understand-http2-and-http3/\n\u003e - https://www.digitalocean.com/community/tutorials/http-1-1-vs-http-2-what-s-the-difference\n\n### 开箱即用\n\n- TypeScript - 内置支持\n- less/sass/stylus/postcss - 内置支持（需要单独安装所对应的编译器）\n\n### 特性小结\n\nVite 带来的优势主要体现在提升开发者在开发过程中的体验。\n\n- Dev Server 无需等待，即时启动；\n- 几乎实时的模块热更新；\n- 所需文件按需编译，避免编译用不到的文件；\n- 开箱即用，避免各种 Loader 和 Plugin 的配置；\n\n## 实现原理\n\nVite 的核心功能：Static Server + Compile + HMR\n\n核心思路：\n\n1. 将当前项目目录作为静态文件服务器的根目录\n2. 拦截部分文件请求\n   1. 处理代码中 import node_modules 中的模块\n   2. 处理 vue 单文件组件（SFC）的编译\n3. 通过 WebSocket 实现 HMR\n\n### 手写实现\n\n详细参考 [my-vite](my-vite)\n\n```javascript\n#!/usr/bin/env node\n\nconst path = require('path')\nconst { Readable } = require('stream')\nconst Koa = require('koa')\nconst send = require('koa-send')\nconst compilerSfc = require('@vue/compiler-sfc')\n\nconst cwd = process.cwd()\n\nconst streamToString = stream =\u003e new Promise((resolve, reject) =\u003e {\n  const chunks = []\n  stream.on('data', chunk =\u003e chunks.push(chunk))\n  stream.on('end', () =\u003e resolve(Buffer.concat(chunks).toString('utf8')))\n  stream.on('error', reject)\n})\n\nconst app = new Koa()\n\n// 重写请求路径，/@modules/xxx =\u003e /node_modules/\napp.use(async (ctx, next) =\u003e {\n  if (ctx.path.startsWith('/@modules/')) {\n    const moduleName = ctx.path.substr(10) // =\u003e vue\n    const modulePkg = require(path.join(cwd, 'node_modules', moduleName, 'package.json'))\n    ctx.path = path.join('/node_modules', moduleName, modulePkg.module)\n  }\n  await next()\n})\n\n// 根据请求路径得到相应文件 /index.html\napp.use(async (ctx, next) =\u003e {\n  // ctx.path // http://localhost:3080/\n  // ctx.body = 'my-vite'\n  await send(ctx, ctx.path, { root: cwd, index: 'index.html' }) // 有可能还需要额外处理相应结果\n  await next()\n})\n\n// .vue 文件请求的处理，即时编译\napp.use(async (ctx, next) =\u003e {\n  if (ctx.path.endsWith('.vue')) {\n    const contents = await streamToString(ctx.body)\n    const { descriptor } = compilerSfc.parse(contents)\n    let code\n\n    if (ctx.query.type === undefined) {\n      code = descriptor.script.content\n      code = code.replace(/export\\s+default\\s+/, 'const __script = ')\n      code += `\n  import { render as __render } from \"${ctx.path}?type=template\"\n  __script.render = __render\n  export default __script`\n      // console.log(code)\n      ctx.type = 'application/javascript'\n      ctx.body = Readable.from(Buffer.from(code))\n    } else if (ctx.query.type === 'template') {\n      const templateRender = compilerSfc.compileTemplate({ source: descriptor.template.content })\n      code = templateRender.code\n    }\n\n    ctx.type = 'application/javascript'\n    ctx.body = Readable.from(Buffer.from(code))\n  }\n  await next()\n})\n\n// 替换代码中特殊位置\napp.use(async (ctx, next) =\u003e {\n  if (ctx.type === 'application/javascript') {\n    const contents = await streamToString(ctx.body)\n    ctx.body = contents\n      .replace(/(from\\s+['\"])(?![\\.\\/])/g, '$1/@modules/')\n      .replace(/process\\.env\\.NODE_ENV/g, '\"production\"')\n  }\n})\n\napp.listen(3080)\n\nconsole.log('Server running @ http://localhost:3080')\n```\n\n## License\n\n[MIT](LICENSE) \u0026copy; [汪磊](https://zce.me)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzce%2Fvite-essentials","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzce%2Fvite-essentials","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzce%2Fvite-essentials/lists"}