学习《玩转 Vue3 全家桶》之实现迷你 Vite

232 阅读2分钟

Offer 驾到,掘友接招!我正在参与2022春招打卡活动,点击查看活动详情

本文源代码参考极客时间的《玩转 Vue3 全家桶》

1. 前置知识

1.1 koa

koa 是基于 node.js 的 web 开发框架。这里通过使用该框架,实现了 Vue 项目的开发访问。它拦截浏览器的请求,然后返回对应的内容。

2. 源码分析

首先,通过 koa 启动了一个服务器,通过该服务器建立浏览器请求和本地文件的关联。浏览器的请求分成了5种情况,rewriteImport 函数用于实现将引用的 npm 包变成相对路径。

2.1 rewriteImport

除了 . ../ / 开头的文件名称,其它导入的文件都会转化成 from '/@modules/${s1}'形式。比如将from 'vue' 转化成from '/@modules/vue'

// 导入文件替换,直接导入模块的变成 from /@modules/X
function rewriteImport(content) {
  return content.replace(/ from ['|"]([^'"]+)['|"]/g, function (s0, s1) {
    // s0:from 后面所有匹配的内容,s1:捕获括号中匹配到的内容
    // . ../ / 开头的,都是本地文件的相对路径
    if (s1[0] !== "." && s1[0] !== "/") {
      return `from '/@modules/${s1}'`;
    } else {
      return s0;
    }
  });
}

2.2 获取首页的内容

一般都是用于处理 index.html 文件,该部分代码会在原代码的基础上增加 process.env,用于标示运行的环境。

if (url == "/") {
  ctx.type = "text/html";
  let content = fs.readFileSync("./index.html", "utf-8");
  content = content.replace(
    "<script ",
    `
      <script>
        window.process = {env:{ NODE_ENV:'dev'}}
      </script>
      <script 
    `
  );
  ctx.body = content;
}

2.3 处理 JS 文件

根据浏览器中 url 部分的名称找到对应的本地文件,返回文件内容

if (url.endsWith(".js")) {
  const p = path.resolve(__dirname, url.slice(1));
  ctx.type = "application/javascript";
  const content = fs.readFileSync(p, "utf-8");
  ctx.body = rewriteImport(content);
}

2.4 处理 CSS 文件

如果是 CSS 文件,去掉换行,并把 CSS 内容转化成对应的 DOM 操作。

if (url.endsWith(".css")) {
  const p = path.resolve(__dirname, url.slice(1));
  const file = fs.readFileSync(p, "utf-8");
  const content = `
    const css = "${file.replace(/\n/g, "")}"
    let link = document.createElement('style')
    link.setAttribute('type', 'text/css')
    document.head.appendChild(link)
    link.innerHTML = css
    export default css
    `;
  ctx.type = "application/javascript";
  ctx.body = content;
}

2.5 处理导入的第三方模块

通过 rewriteImport 函数处理后,第三方模块变成了/@modules/${s1}形式。这里转化成去 node_modules 文件夹查找对应的文件,先找到对应第三方模块的文件夹,再找到该文件夹中package.json的 module,该 module 对应的文件就是第三方模块打包后的文件内容,可以直接使用

if (url.startsWith("/@modules/")) {
  const prefix = path.resolve(__dirname, "node_modules", url.replace("/@modules/", ""));
  const module = require(prefix + "/package.json").module;
  const p = path.resolve(prefix, module);
  const ret = fs.readFileSync(p, "utf-8");
  ctx.type = "application/javascript";
  ctx.body = rewriteImport(ret);
}

2.6 处理 Vue 文件

该部分代码处理了 Vue 文件的 script 部分和 template 部分,通过 @vue/compiler-sfc 包将 Vue 单文件进行拆分,通过 @vue/compiler-dom 包处理 template 部分的内容,将 template 里面的内容转化成 render

if (url.indexOf(".vue") > -1) {
  const p = path.resolve(__dirname, url.split("?")[0].slice(1));
  const { descriptor } = compilerSfc.parse(fs.readFileSync(p, "utf-8"));

  if (!query.type) {
    // 1 处理 script 部分的内容
    ctx.type = "application/javascript";
    // 借用 vue 自带的 compile 框架 解析单文件组件,其实相当于 vue-loader 做的事情
    ctx.body = `
  ${rewriteImport(descriptor.script.content.replace("export default ", "const __script = "))}
  import { render as __render } from "${url}?type=template"
  __script.render = __render
  export default __script
      `;
  } else if (query.type === "template") {
    // 2 编译 template 部分的内容
    const template = descriptor.template;
    // 服务端将 template 转化成 render
    const render = compilerDom.compile(template.content, { mode: "module" }).code;
    ctx.type = "application/javascript";
    ctx.body = rewriteImport(render);
  }
}

3. 源码

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();
// 导入文件替换,直接导入模块的变成 from /@modules/X
function rewriteImport(content) {
  return content.replace(/ from ['|"]([^'"]+)['|"]/g, function (s0, s1) {
    // s0:from 后面所有匹配的内容,s1:捕获括号中匹配到的内容
    // . ../ / 开头的,都是本地文件的相对路径
    if (s1[0] !== "." && s1[0] !== "/") {
      return `from '/@modules/${s1}'`;
    } else {
      return s0;
    }
  });
}

app.use(async (ctx) => {
  const {
    request: { url, query },
  } = ctx;

  if (url == "/") {
    // 1. 首页获取内容
    ctx.type = "text/html";
    let content = fs.readFileSync("./index.html", "utf-8");
    content = content.replace(
      "<script ",
      `
      <script>
        window.process = {env:{ NODE_ENV:'dev'}}
      </script>
      <script 
    `
    );
    ctx.body = content;
  } else if (url.endsWith(".js")) {
    // 2. 处理 JS 文件
    const p = path.resolve(__dirname, url.slice(1));
    ctx.type = "application/javascript";
    const content = fs.readFileSync(p, "utf-8");
    ctx.body = rewriteImport(content);
  } else if (url.endsWith(".css")) {
    // 3. 处理 CSS 文件。将 css 文件内容变成 js 形式
    const p = path.resolve(__dirname, url.slice(1));
    const file = fs.readFileSync(p, "utf-8");
    const content = `
    const css = "${file.replace(/\n/g, "")}"
    let link = document.createElement('style')
    link.setAttribute('type', 'text/css')
    document.head.appendChild(link)
    link.innerHTML = css
    export default css
    `;
    ctx.type = "application/javascript";
    ctx.body = content;
  } else if (url.startsWith("/@modules/")) {
    // 4. 处理 node_modules。
    const prefix = path.resolve(__dirname, "node_modules", url.replace("/@modules/", ""));
    const module = require(prefix + "/package.json").module;
    const p = path.resolve(prefix, module);
    const ret = fs.readFileSync(p, "utf-8");
    ctx.type = "application/javascript";
    ctx.body = rewriteImport(ret);
  } else if (url.indexOf(".vue") > -1) {
    // 5. 处理 vue 单文件
    const p = path.resolve(__dirname, url.split("?")[0].slice(1));
    const { descriptor } = compilerSfc.parse(fs.readFileSync(p, "utf-8"));

    if (!query.type) {
      // 5.1 处理 script 部分的内容
      ctx.type = "application/javascript";
      // 借用 vue 自带的 compile 框架 解析单文件组件,其实相当于 vue-loader 做的事情
      ctx.body = `
  ${rewriteImport(descriptor.script.content.replace("export default ", "const __script = "))}
  import { render as __render } from "${url}?type=template"
  __script.render = __render
  export default __script
      `;
    } else if (query.type === "template") {
      // 5.2 编译 template 部分的内容
      const template = descriptor.template;
      // 服务端将 template 转化成 render
      const render = compilerDom.compile(template.content, { mode: "module" }).code;
      ctx.type = "application/javascript";
      ctx.body = rewriteImport(render);
    }
  }
});

app.listen(8008, () => {
  console.log("服务已启动,端口是 8008");
});

4. 总结

  1. 总的来说,迷你 vite 的实现思路很清晰,它通过启动一个 web 服务器,拦截浏览器的请求,实现首页文件的按需加载,从而使项目启动时间和项目复杂解耦,提高启动效率。
  2. 本文耗时4个小时