Vite 的理解

843 阅读5分钟

Vite 是什么

官方给出含义:下一代前端开发与构建工具

Vite 与 其它打包工具有何不同

  • 在冷启动中,Vite 的启动速度比其它的快很多,因为它不需要将一些 ES6+ 的语法进行转义打包,直接引用依赖
  • 在开发过程中,因为现代化浏览器都支持 ES 模块,所以Vite在开发阶段只做包的引入
  • 使用 Vite 构建 Vue、React 等项目很简单,只需要 npm init @vitejs/app 构建一个 Vite 项目,在命令行中可以选择 Vue、React 等模板即可
  • 相对于 webpack 的优势:在开发环境中,Vite 是不做编译也不做打包,只对一些非 JS 文件进行 Loaded 转换成 JS 文件 所以加载速度快

原理

  • 在开发环境中 Vite 只开启一个静态服务器监听代码的变化,不需要将代码打包,由于在 Vite 项目中不需要打包,所以每一次修改更新都很快
  • 使用 Rellup 进行构建
  • 项目运行时可以看到很多个文件(.js、.vue、.jsx等等),虽然浏览器默认不会解析这些文件,但是服务器在返回的头部信息中把 Content-Type: application/javascript,所以浏览器在解析这些文件时是以 JavaScript 文件进行解析的
  • 语法使用 AMD 语法,不需要转成 CommonJS,因为现在主流的浏览器都支持 AMD 语法,只需要把项目中使用 Import 导入的 node_module 文件改成绝对路径即可
  • webpack 是将所有文件打包到一个 js 文件中,而 Vite 是直接加载这些文件(文件引入时修改路径)

手写 Vite

  • 创建文件 kvite 夹,进到文件夹并 npm init -y 初始化项目
  • 项目前的准备
  • 在 Vite 中是需要一个 node 服务器监听,所以引入一个 node 框架 koa(这里你可以使用 node 原生进行开启服务器,引入 http 模块,还可以使用其它 node 框架,例如 express、egg 等),然后运行服务器。
  • 为了监听代码的变化,还需要引入一个 nodemon 热更新
  • 当前项目以构建 vue3 为例,所以还需要引入 vue3 模块
mkdir kvite && cd kvite
npm init - y
// 下载文件由于使用 npm install 有些依赖会下载失败(如果有梯子的话就使用 npm install 吧),推荐用 yarn 进行安装
// yarn add 依赖 相当于 npm install 依赖 --save
yarn add koa
yarn add nodemon
yarn add vue @next // 这里说明一下,如果使用 yarn add vue 下载的是 vue2 版本
// 下载完后可以在 package.json 中查看下载后的依赖
{
  "dependencies": {
    "global": "^4.4.0",
    "koa": "^2.13.4",
    "nodemon": "^2.0.15",
    "vue": "^3.2.23"
  }
}
  • 前期准备好之后就开始进入正题
    • 在 kvite 文件夹中分别创建 index.js、index.html、src 文件夹、src文件夹/main.js
    • 在 index.html 中写一个常规 html 结构,在 body 中 创建一个 id 为 div 的节点
// kvite/index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Vite App</title>
</head>
<body>
  <div id="app"></div>
  <script type="module" src="/src/main.js"></script>
</body>
</html>
  • 在 kvite 中把 koa 引入,创建一个 node 服务器,并且运行文件,node index.js
// kvite/index.js
/**
 * @description 手写 Vite
 * @author oyzx
 * node 服务器,处理浏览器加载各种资源的请求
 */
const fs = require('fs')
const path = require('path')
const Koa = require('koa')
// 创建实例
const app = new Koa()
// 处理中间配置
// 处理路由
app.use(async ctx => {
  ctx.type = 'text/html'	// 浏览器根据类型解析文件
  ctx.body = fs.readFileSync(path.join(__dirname, './index.html'), 'utf-8')
})
app.listen(3000, () => {
  console.log('kvite start');
})
  • 打开浏览器,输入 http://localhost:3000 点击回车,我们就可以看到 /kvite/index.html 文件了,你可以在 /kvite/index.html 中添加一些元素再刷新一下就可以看到内容了 - 为了项目工程化,我在 package.json 中加入执行脚本,我们可以使用 yarn dev 或者 npm run dev 执行文件
// kvite/package.json
{
  "scripts": {
    "dev": "nodemon ./index.js"
  },
  "dependencies": {
    "global": "^4.4.0",
    "koa": "^2.13.4",
    "nodemon": "^2.0.15",
    "vue": "^3.2.23"
  }
}
  • 在浏览器中我们打开控制台,可以看到一个错误:Failed to load module script: Expected a JavaScript module script but the server responded with a MIME type of "text/html". Strict MIME type checking is enforced for module scripts per HTML spec. ,因为在 index.html 中引入一个 main.js,但是我们返回给浏览器解析的 type 类型却是 text/html,所以浏览器解析不了 js 文件,这时候我们就要根据路由来指定解析类型了
/**
 * @description 手写 Vite
 * @author oyzx
 * 
 * node 服务器,处理浏览器加载各种资源的请求
 */
const fs = require('fs')
const path = require('path')
const Koa = require('koa')
// 创建实例
const app = new Koa()
// 处理中间配置
// 处理路由
app.use(async ctx => {
  const { url } = ctx
  if (url === '/') {
    ctx.type = 'text/html'
    ctx.body = fs.readFileSync(path.join(__dirname, './index.html'), 'utf-8')
  } else if (url.endsWith('.js')) {
    ctx.type = 'application/javascript'
    ctx.body = fs.readFileSync(path.join(__dirname, url), 'utf-8')
  }
})
app.listen(3000, () => {
  console.log('kvite start');
})
  • 在 main.js 可以写一段代码,刷新页面后,在浏览器控制台中可以看到输出
// kvite/src/main.js
consol.log('hello world')
  • 接下来对引入文件的转换,将 import 文件的非相对路径转成相对路径进行一个文件导入,如果文件中还有 import 中非相对路径的引入文件继续转换导入
// kvite/index
/**
 * @description 手写 Vite
 * @author oyzx
 * 
 * node 服务器,处理浏览器加载各种资源的请求
 */
const fs = require('fs')
const path = require('path')
const Koa = require('koa')
// 创建实例
const app = new Koa()
/**
 * @description 将原来的裸地址改成相对地址
 */
const GRewriteImport = content => {
  return content.replace(/ from ['"](.*)["']/g, (s1, s2) => {
    if (s2.startsWith('.') || s2.startsWith('/') || s2.startsWith('../')) {
      return s1
    } else {
      // 替换
      return ` from '/@modules/${s2}'`
    }
  })
}
// 处理中间配置
// 处理路由
app.use(async ctx => {
  const { url } = ctx
  if (url === '/') {
    ctx.type = 'text/html'
    ctx.body = fs.readFileSync(path.join(__dirname, './index.html'), 'utf-8')
  } else if (url.endsWith('.js')) {
    ctx.type = 'application/javascript'
    ctx.body = GRewriteImport(fs.readFileSync(path.join(__dirname, url), 'utf-8'))
  } else if (url.startsWith('/@modules/')) {
    // 裸模块
    const vModuleName = url.replace('/@modules/', '')
    // 接下来去 node_modules 文件夹下面去找
    const vPrefix = path.join(__dirname, '/node_modules/', vModuleName)
    // 接下来查找文件中的 package.json,根据 package.json 中 module 读取文件
    const vModule = require(path.join(vPrefix, 'package.json')).module
    const vFilepath = path.join(vPrefix, vModule)
    // 获取具体文件
    const vResult = GRewriteImport(fs.readFileSync(vFilepath, 'utf-8'))
    ctx.type = 'application/javascript'
    ctx.body = vResult
  }
})
app.listen(3000, () => {
  console.log('kvite start');
})

// kvite/src/main.js
import { createApp, h } from 'vue'
createApp({
  render() {
    return h('div', 'hello world')
  }
}).mount('#app')
  • 接下来对 vue 文件进行转义引入,引入 @vue/compiler-sfc、@vue/compiler-dom 两个转义文件,
    • @vue/compiler-sfc 将 vue 文件中 script 标签包含的 vue 语法代码进行转义,
    • @vue/compiler-dom 将 vue 文件中 tempalte 标签包含的 vue 语法代码进行转义,
// 在 src 下创建一个 App.vue
// kvite/src/App.vue
<template>
  <div>
    <button @click="pReduceState">减一</button>
  hello {{ title }} world
  <button @click="pAddState">加一</button>
  </div >
</template >
  <script>
    import {reactive, toRefs} from "vue";
    export default {
      setup() {
    const state = reactive({
      title: 0,
    });
    const pAddState = () => {
      state.title += 1;
    };
    const pReduceState = () => {
      state.title -= 1;
    };
    return {
      ...toRefs(state),
      pAddState,
      pReduceState,
    };
  },
};
  </script>

/**
 * @description 手写 Vite
 * @author oyzx
 * 
 * node 服务器,处理浏览器加载各种资源的请求
 */
const fs = require('fs')
const path = require('path')
const Koa = require('koa')

const compilerSFC = require('@vue/compiler-sfc')
const compilerDOM = require('@vue/compiler-dom')
// 创建实例
const app = new Koa()
/**
 * @description 将原来的裸地址改成相对地址
 */
const GRewriteImport = content => {
  return content.replace(/ from ['"](.*)["']/g, (s1, s2) => {
    if (s2.startsWith('.') || s2.startsWith('/') || s2.startsWith('../')) {
      return s1
    } else {
      // 替换
      return ` from '/@modules/${s2}'`
    }
  })
}
// 处理中间配置
// 处理路由
app.use(async ctx => {
  const { url, query } = ctx
  if (url === '/') {
    ctx.type = 'text/html'
    ctx.body = fs.readFileSync(path.join(__dirname, './index.html'), 'utf-8')
  } else if (url.endsWith('.js')) {
    ctx.type = 'application/javascript'
    ctx.body = GRewriteImport(fs.readFileSync(path.join(__dirname, url), 'utf-8'))
  } else if (url.startsWith('/@modules/')) {
    // 裸模块
    const vModuleName = url.replace('/@modules/', '')
    // 接下来去 node_modules 文件夹下面去找
    const vPrefix = path.join(__dirname, '/node_modules/', vModuleName)
    // 接下来查找文件中的 package.json,根据 package.json 中 module 读取文件
    const vModule = require(path.join(vPrefix, 'package.json')).module
    const vFilepath = path.join(vPrefix, vModule)
    // 获取具体文件
    const vResult = GRewriteImport(fs.readFileSync(vFilepath, 'utf-8'))
    ctx.type = 'application/javascript'
    ctx.body = vResult
  } else if (url.indexOf('.vue') > -1) {
    // 获取加载文件路径
    const vPath = path.join(__dirname, url.split('?')[0])
    // 编译解析vue文件
    const vResult = compilerSFC.parse(fs.readFileSync(vPath, 'utf-8'))
    if (!query.type) {
      // 将 .vue 文件中的 script信息 解析成 js 文件
      // 获取脚本部分的内容
      const vScriptContent = vResult.descriptor.script.content
      // 替换文件中的 export default 导出文件,将其改成一个变量
      const vScript = vScriptContent.replace('export default ', 'const __script = ')
      ctx.type = 'application/javascript'
      ctx.body = `
        ${GRewriteImport(vScript)}
        import {render as __render} from '${url}?type=template'
        __script.render = __render
        export default __script
      `
    } else if (query.type === 'template') {
      // 将 vue 中的 template 模板信息进行编译转义
      const vTpl = vResult.descriptor.template.content
      // 编译 render
      const vRender = compilerDOM.compile(vTpl, { mode: 'module' }).code
      ctx.type = 'application/javascript'
      ctx.body = GRewriteImport(vRender)
    }
  }
})
app.listen(3000, () => {
  console.log('kvite start');
})
  • 保存,刷新浏览器即可看到信息