mini-vite

140 阅读1分钟

前言

目前 Vite 3.0 已经正式发布,也被称为是下一代前端构建工具。

Vite 作为构建工具最基础的功能就是在入口文件 index.html 中引入相关的内容。

原生 ESM 语法

总所周知,原生 ESM 语法<script type="module" src="...">是 script 标签支持 type 属性值为 module,这样可以通过模块的方式引入 JavaScript 的代码。Vite 使用的就是这种方式来引入 JavaScript 的代码。

构建 mini-vite 服务

使用 express 来构建服务,初始化工程。 代码仓地址mini-vite: vite 基础版 (gitee.com)

响应 html 文件

index.html 文件准备。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + Vue</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/index.js"></script>
  </body>
</html>
app.get("/", (_, res) => {
  let content = fs.readFileSync(
    path.resolve(__dirname, "./src/index.html"),
    "utf-8"
  )
  // window下process缺失会导致报错,所以动态设置process
  content = content.replace(
    "<script",
    `<script>
          window.process = {
              env: {
                  NODE_ENV: 'dev'
              }
          }
         </script>
         <script`
  )
  res.type("html").send(content)
})

响应 js 文件

index.js 文件准备。

import { createApp } from "vue"

import App from "/src/app.vue"

import "/src/index.css"

createApp(App).mount("#app")
app.get("/*.js", (req, res) => {
  let content = fs.readFileSync(
    path.resolve(__dirname, req.url.slice(1)),
    "utf-8"
  )
  res.type("application/javascript").send(rewriteImport(content))
})
// 替换第三方库并发送请求 from 'vue' => from '/@modules/vue'
function rewriteImport(content) {
  return content.replace(/ from ['|"]([^'"]+)['|"]/g, (s0, s1) => {
    if (s1[0] !== "." && s1[0] !== "/") {
      return ` from '/@modules/${s1}'`
    } else {
      return s0
    }
  })
}

响应第三方库

app.get("/@modules/*", (req, res) => {
  const prefix = path.resolve(
    __dirname,
    "node_modules",
    req.url.replace("/@modules/", "")
  )
  const module = require(prefix + "/package.json").module
  let content = fs.readFileSync(path.resolve(prefix, module), "utf-8")
  res.type("application/javascript").send(rewriteImport(content))
})

响应 Vue 单文件组件

app.vue 文件准备。

<template>
  <div>{{ count }}</div>
  <button type="button" @click="add">count++</button>
</template>

<script>
import { ref } from "vue"
export default {
  setup() {
    const count = ref(0)
    const add = () => {
      count.value++
    }
    return { count, add }
  },
}
</script>

<style>
button {
  font-size: 16px;
}
</style>

<style>
button {
  font-size: 20px;
}
</style>
// .vue单文件编译 => complier-sfc => template + script + styles
const complierSfc = require("@vue/compiler-sfc")
const complierDom = require("@vue/compiler-dom")
app.get("/*.vue", (req, res) => {
  const p = path.resolve(__dirname, req.url.split("?")[0].slice(1))
  const { descriptor } = complierSfc.parse(fs.readFileSync(p, "utf-8"))
  let body = ""
  if (!req.query.type) {
    body = `
        ${rewriteImport(
          descriptor.script.content.replace(
            "export default",
            "const __script = "
          )
        )}
import { render as __render } from "${req.url}?type=template"
__script.render = __render
export default __script
    `
    if (descriptor.styles.length > 0) {
      body += resolveStyle(
        descriptor.styles.map((item) => item.content).join("")
      )
    }
  } else {
    // template => complier-dom => render函数
    const { template } = descriptor
    const render = complierDom.compile(template.content, { mode: "module" })
    body = rewriteImport(render.code)
  }
  res.type("application/javascript").send(body)
})
// 处理 style
function resolveStyle(content) {
  return `
    let link = document.createElement('style')
    link.setAttribute('type','text/css')
    document.head.appendChild(link)
    link.innerHTML = "${content.replace(/\s/g, "")}"
    `
}

响应 css 文件

index.css 文件准备。

div {
  color: skyblue;
}
app.get("/*.css", (req, res) => {
  const file = fs.readFileSync(
    path.resolve(__dirname, req.url.slice(1)),
    "utf-8"
  )
  res.type("application/javascript").send(resolveStyle(file))
})