自己写一个简单的 Vite


什么是 Vite

官网上已经说明啦:下一代前端开发与构建工具,提高开发者的开发体验在这里插入图片描述


分析

首先看一下空项目里都有些什么吧,项目的根目录下会有个 index.html ,叫宿主页,页面内会有一个 script 标签
在这里插入图片描述
这里使用浏览器新支持的 type="module",这样写有一个好处,就是在 main.js 中可以以 ESModule 的方式编写,且不需要打包工具进行打包,浏览器会发送相应请求,去寻找 vue 以及 ./App.vue,如下如所示

在这里插入图片描述

等待加载完这两个资源之后,在进行 createApp() 创建应用程序,在页面上显示

这样的处理方法有以下好处

  • 在开发阶段不需要打包,这样不管我的项目有多大,加载的速度快
  • 按需加载,用到相关的模块就去加载相关的模块,不相关的不加载,提高速度

需要知道的知识点

  • 浏览器只知道相对地址,它根据当前的 index.html 的地址,去发送请求,获取资源
  • 浏览器只认识 htmljscss,如果给我一个vue 文件,浏览器是不知道怎么处理,所以 vite 需要额外去处理 vue 文件,(也就是把 vue 转化成 js)其他的文件格式同理,把特殊的文件类型进行解析和转换。

思路

vite 相当于一个服务器,处理一下宿主文件,以及 vue 文件等


目标

自己实现一个基础的 vite ,包括一下几个功能

  • 返回宿主页面
  • 处理裸模块
  • 处理单文件组件(SFC)

开发

先写一个 Node 服务

在这里插入图片描述


返回宿主页面

我们需要有个宿主页面,来当我们的单页面的宿主

之后,当我们请求 / 时,就需要返回宿主页,那我们可以用 fs 模块
在这里插入图片描述
但是这样也不对,因为请求的 main 也是 html 了,那是因为我们没写路由

在这里插入图片描述
写完路由之后如下,main.js 会找不到,之后再来处理 js 文件


处理 js 文件

大概的处理方式就是找到文件之后,返回即可

在这里插入图片描述

演示如下
在这里插入图片描述

这里需要说明一下,ctx.type 默认是 text/plain 也就是纯文本,我们需要设置成 application/javascript ,告诉浏览器这是个 js 代码
在这里插入图片描述

path.join 的使用与否,不影响结果,不过输出的处理后的内容的话,还是能看出区别的,使用 path.join 拼接的路径会更规范一些

在这里插入图片描述


裸模块的路径重写

浏览器是不认识绝对路径的,只知道相对路径
在这里插入图片描述

比方说入口文件中的 'vue‘ ,它就是一个裸模块,浏览器发送请求,是找不到这个文件的,需要 vite 来做一下处理,帮我们找到这个模块然后返回

其中,我们需要处理一下导入的文字,在前面加上些标志,说明她是裸模块,此时需要一个函数来批量处理这些东西

处理完之后,浏览器会发送相关依赖的请求,从而请求资源

在这里插入图片描述

之后浏览器就可以请求资源了

在这里插入图片描述

// 重写导入 的函数如下
function rewriteImport(content) {
  return content.replace(/ from ['"](.*)['"]/g, function (s1, s2) {
    const startsWithResult =
      s2.startsWith("/") || s2.startsWith("./") || s2.startsWith("../");
    if (startsWithResult === true) {
      return s1;
    } else {
      return ` from '/@modules/${s2}'`;
    }
  });
}

加载裸模块

大概的处理逻辑就是这样的
先到 package.json 中找到真正的入口位置路径,然后 fs 一下,返回

在这里插入图片描述

在这里插入图片描述

process 报错

如果遇到如下报错,那么需要在页面上声明一下变量

模拟一下
在这里插入图片描述
可以稍微查看一下报错的位置
在这里插入图片描述

因为这些代码里有些是 node 的代码,而浏览器环境中是没有 process 变量的,所以我们需要在页面上设置一下

// 在页面上
<script>
  // 模拟一下环境,否则不会渲染
  window.process = {
    env: {
      NODE_ENV: "dev",
    },
  };
</script>
// main.js
import { createApp, h } from "vue";
// import App from "./App.vue";

createApp(h("div", "我是内容")).mount("#app");


解析 sfc

就是解析单文件的 app.vue

主要是处理 app.vue 这个请求,然后把他变成 js 代码

思路还是一样的,只不过这里引入个模块 compiler-sfc,此模块是专门分析 vue 文件,把他转化成 js 对象

const compilerSFC = require("@vue/compiler-sfc");

打印一下分析后的内容,如下图所示
在这里插入图片描述

我们需要先拿到组件中的 script 的内容,然后返回,其中 template 转换成 渲染函数 需要另外 一个请求去处理

const p = path.join(__dirname, url.split("?")[0]); // 找到文件位置
const content = fs.readFileSync(p, "utf-8"); // 以纯文本读取
const compileContent = compilerSFC.parse(content); // vue 词法分析
const { type } = query;
if (!type) {
  // 获取组件中的脚本
  const { content: scriptContent } = compileContent.descriptor.script;
  const script = scriptContent.replace(
    "export default ",
    "const __script = "
  ); // 将组件中的脚本变成变量
  ctx.type = "application/javascript";
  ctx.body = `
  ${rewriteImport(script)}
  import { render as __render } from '${url}?type=template'   // 请求 html 获取,render 函数
  __script.render = __render    // 将 render 函数 挂载到 脚本变量 中
  export default __script   // 导出脚本变量
  `;

模板编译

模板编译有另外一个模块去处理 @vue/compiler-dom,它可以将 template 转换成渲染函数

在这里插入图片描述

else if (type === "template") {
    const { content: templateContent } = compileContent.descriptor.template;
    const render = compilerDOM.compile(templateContent, {
      mode: "module",
    }).code;
    console.log("render:>>", render);
    ctx.type = "application/javascript";
    ctx.body = rewriteImport(render);
  }
}

整体代码

const Koa = require("koa");
const fs = require("fs");
const path = require("path");
const compilerSFC = require("@vue/compiler-sfc");
const compilerDOM = require("@vue/compiler-dom");

const app = new Koa();

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")) {
    const p = path.join(__dirname, url); // 首先找到文件的位置
    ctx.type = "application/javascript"; // 这里如果不设置的话,默认是 text/plain
    ctx.body = rewriteImport(fs.readFileSync(p, "utf-8"));
  } else if (url.startsWith("/@modules/")) {
    // console.log("裸模块的加载");
    const moduleName = url.replace("/@modules/", "");
    const p = path.join(__dirname, "node_modules", moduleName);
    const modulePath = require(path.join(p, "/package.json")).module; // 这里的 json 是 require 引入
    const realPath = path.join(p, modulePath);
    ctx.type = "application/javascript";
    ctx.body = rewriteImport(fs.readFileSync(realPath, "utf-8"));
  } else if (url.indexOf(".vue") > -1) {
    const p = path.join(__dirname, url.split("?")[0]); // 找到文件位置
    const content = fs.readFileSync(p, "utf-8"); // 以纯文本读取
    const compileContent = compilerSFC.parse(content); // vue 词法分析
    const { type } = query;
    if (!type) {
      // 获取组件中的脚本
      const { content: scriptContent } = compileContent.descriptor.script;
      const script = scriptContent.replace(
        "export default ",
        "const __script = "
      ); // 将组件中的脚本变成变量
      ctx.type = "application/javascript";
      ctx.body = `
        ${rewriteImport(script)}
        import { render as __render } from '${url}?type=template'   // 请求 html 获取,render 函数
        __script.render = __render    // 将 render 函数 挂载到 脚本变量 中
        export default __script   // 导出脚本变量
      `;
    } else if (type === "template") {
      const { content: templateContent } = compileContent.descriptor.template;
      const render = compilerDOM.compile(templateContent, {
        mode: "module",
      }).code;
      console.log("render:>>", render);
      ctx.type = "application/javascript";
      ctx.body = rewriteImport(render);
    }
  }
});

function rewriteImport(content) {
  return content.replace(/ from ['"](.*)['"]/g, function (s1, s2) {
    const startsWithResult =
      s2.startsWith("/") || s2.startsWith("./") || s2.startsWith("../");
    if (startsWithResult === true) {
      return s1;
    } else {
      return ` from "/@modules/${s2}"`;
    }
  });
}

app.listen(3000);


gitee 地址

仓库地址

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值