vite简要解析以及模拟一个简单的vite

文章内容输出来源:拉勾教育前端高薪训练营

  • Vite是一个面向现代浏览器的一个更轻、更快的Web应用开发工具
  • 它基于ECMAScript标准原生模块系统(ES Moudule)实现,解决webpack-dev-server冷启动时间过长,HMR热更新反应速度慢

Vite和Vue-CLI

  • Vite使用浏览器原生支持的ES Module加载模块,在开发模式下不需要打包可以直接运行
  • Vite在生产环境下使用Rollup打包(基于ES Module的方式打包,不需要基于babel将import转换成require),打包体积比Webpack小
  • Vue-CLI开发模式下必须对项目打包才可以运行
  • Vue-CLI使用Webpack打包

Vite的特点

  • 快速冷启动
  • 代码按需编译,只有代码当前需要加载时才会编译
  • 模块热更新
  • 开箱即用

核心功能

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

与webpack的HMR区别

  • Vite HMR:立即编译当前所修改的文件
  • Webpack HMR:会自动以这个文件为入口重写build一次,所有的涉及到的依赖也都会被加载一遍
    注:使用Webpack打包的两个原因,浏览器环境并不支持模块化和零散的模块文件会产生大量的HTTP请求

Vite项目依赖

  • Vite
  • @vue/compiler-sfc

Vite创建项目

  • 基于Vue3的项目
$ npm init vite-app <project-name>
$ cd <project-name>
$ npm install
$ npm run dev
  • 基于模板创建项目
$ npm init vite-app --template react
$ npm init vite-app --template preact

关于Vite是如何让浏览器识别.vue文件
Vite开启的web服务器会劫持.vue请求,把.vue文件解析成js文件并把响应头中的content-type设置为application/javascript

模拟Vite实现

<!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>Document</title>
</head>
<body>
  <div id="app"></div>
  <script type="module" src="/src/main.js"></script>
</body>
</html>

package.json

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

index.js

#!/usr/bin/env node
const Koa = require('koa')
const send = require('koa-send')

const app = new Koa()

// 1. 静态文件服务器
app.use(async (ctx, next) => {
  await send(ctx, ctx.path, { root: process.cwd(), index: 'index.html' })
  await next()
})

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

在vite-cli目录下执行将命令链接到全局

$ npm link

在vue3项目中执行

$ vite-cli

修改第三方模块的路径

上面的代码因为main.js不是相对路径引入,会爆出

Uncaught TypeError: Failed to resolve module specifier “vue”. Relative references must start with either “/”, “./”, or “…/”.
在index.js处理

const streamToString = stream => new Promise((resolve, reject) => {
  const 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') {
    const contents = await streamToString(ctx.body)
    // import vue from 'vue'
    // import App from './App.vue'
    ctx.body = contents
      .replace(/(from\s+['"])(?![\.\/])/g, '$1/@modules/')
      .replace(/process\.env\.NODE_ENV/g, '"development"')
  }
})

加载第三方模块

经过上方的路径处理,浏览器会去 http://localhost:3000/@modules/vue 下寻找文件并加载但此时并不存在文件,此时需要匹配@modules并将其修改为node_modules中对应的路径进行加载
index.js

const path = require('path')

// 3. 加载第三方模块
app.use(async (ctx, next) => {
  // ctx.path --> /@modules/vue
  if (ctx.path.startsWith('/@modules/')) {
    const moduleName = ctx.path.substr(10)
    const pkgPath = path.join(process.cwd(), 'node_modules', moduleName, 'package.json')
    const pkg = require(pkgPath)
    ctx.path = path.join('/node_modules', moduleName, pkg.module)
  }
  await next()
})

编译单文件组件

$ yarn add @vue/compiler-sfc

index.js

const { Readable } = require('stream')
const compilerSFC = require('@vue/compiler-sfc')

const stringToStream = text => {
  const stream = new Readable()
  stream.push(text)
  stream.push(null)
  return stream
}

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) {
      code = descriptor.script.content
      // console.log(code)
      code = code.replace(/export\s+default\s+/g, '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 = stringToStream(code)
  }
  await next()
})

完整代码

#!/usr/bin/env node
const path = require('path')
const { Readable } = require('stream')
const Koa = require('koa')
const send = require('koa-send')
const compilerSFC = require('@vue/compiler-sfc')

const app = new Koa()

const streamToString = stream => new Promise((resolve, reject) => {
  const chunks = []
  stream.on('data', chunk => chunks.push(chunk))
  stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')))
  stream.on('error', reject)
})

const stringToStream = text => {
  const stream = new Readable()
  stream.push(text)
  stream.push(null)
  return stream
}

// 3. 加载第三方模块
app.use(async (ctx, next) => {
  // ctx.path --> /@modules/vue
  if (ctx.path.startsWith('/@modules/')) {
    const moduleName = ctx.path.substr(10)
    const pkgPath = path.join(process.cwd(), 'node_modules', moduleName, 'package.json')
    const pkg = require(pkgPath)
    ctx.path = path.join('/node_modules', moduleName, pkg.module)
  }
  await next()
})

// 1. 静态文件服务器
app.use(async (ctx, next) => {
  await send(ctx, ctx.path, { root: process.cwd(), index: 'index.html' })
  await next()
})

// 4. 处理单文件组件
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) {
      code = descriptor.script.content
      // console.log(code)
      code = code.replace(/export\s+default\s+/g, '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 = stringToStream(code)
  }
  await next()
})

// 2. 修改第三方模块的路径
app.use(async (ctx, next) => {
  if (ctx.type === 'application/javascript') {
    const contents = await streamToString(ctx.body)
    // import vue from 'vue'
    // import App from './App.vue'
    ctx.body = contents
      .replace(/(from\s+['"])(?![\.\/])/g, '$1/@modules/')
      .replace(/process\.env\.NODE_ENV/g, '"development"')
  }
})

app.listen(3000)
console.log('Server running @ http://localhost:3000')
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值