当前位置:   article > 正文

vite学习——原理剖析+手写实现+插件_vite transform

vite transform

在这里插入图片描述
vite是一个开发构建工具,开发过程中它利用浏览器native ES Module特性按需导入源码,预打包依赖。

特点: 启动快,更新快

一、按需加载

工作原理

1.浏览器利用es module imports,关键变化是index.html中的入口导入方式(开发阶段需要考虑浏览器兼容
在这里插入图片描述
所以,开发阶段不需要进行打包操作
本地文件使用 type=module,不支持本地路径,需要开启一个本地serve
2.第三方依赖模块预打包,并将导入地址修改为相对地址,从而统一所有请求为相对地址
预打包:第三方依赖提前打包存在node_modules中,程序启动时,直接从node_modules中去下载(浏览器只能加载相对路径)
在这里插入图片描述

二、解析

vite需要根据请求资源类型的不同做不同的解析工作,解析的最后结果是js对象
比如app.vue 文件

import HelloWorld from "./components/HelloWorld.vue";

解析为
const _script = {
	name: "App",
	components: {
		HelloWorld
	}
}
// template 部分转换为了一个模板请求,解析结果是一个渲染函数
import {render as _render} from "src/App.vue?type=template"
// 将解析得到的render函数设置到组件配置对象上
_script.render=_render
_script._hmrId="src/App.vue""
_script._file="/Users/...../src/App.vue"
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

vue文件 在服务端提前被vite编译之后,直接输出给浏览器,直接使用

三、vite手写实现

1、基础实现
基于koa框架,创建node服务器
基本配置:

// 一个node服务器,相当于devServer  基于koa
const Koa = require("koa");
const app = new Koa();
const fs = require("fs");

const path = require("path");

const compilerSfc = require("@vue/compiler-sfc"); // 编译器,得到一个组件配置对象,解析vue文件
const compilerDom = require("@vue/compiler-dom"); // 编译模板


app.use(async (ctx) => {
	// 处理文件解析
});

// 设置端口监听
app.listen(4002, () => {
  console.log("vite start");
});

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

2、解析html
利用fs对文件进行读取

// 首页
    ctx.type = "text/html";
    ctx.body = fs.readFileSync("./index.html", "utf8");
  • 1
  • 2
  • 3

3、解析js
跟html解析区别在意文本类型的不同

// 响应js请求
    const p = path.join(__dirname, url);
    ctx.type = "text/javascript";
    ctx.body = fs.readFileSync(p, "utf-8");
  • 1
  • 2
  • 3
  • 4

4、第三方库的支持,比如vue
我们在代码里一般这样引入
import { createApp, h } from “vue”;
这里引入会报错
表示希望路径是绝对路径或者相对路径
表示期望路径是绝对路径或者相对路径
第三方的依赖我们都会安装在node_modules中,所以需要去node_modules中去寻找对应的js文件,因此我们需要对路径进行重写

比如vue,我们可以给它加一个别名----/@module/vue,
一个改写函数

// 重写导入,让引用的第三方文件,变成相对地址
function rewriteImport(content) {
  return content.replace(/ from ['|"](.*)['|"]/g, function (s0, s1) {
    // s0 - 匹配字符串
    // s1 - 分组内容
    // 看看是不是相对地址
    if (s1.startsWith(".") || s1.startsWith("../") || s1.startsWith("/")) {
      // 不处理
      return s0;
    } else {
      // 是外部文件
      return ` from '/@module/${s1}'`;
    }
  });
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

名称改写好之后,我们要去node_modules里面对应的js文件
比如vue,可以先看一下vue的位置
在这里插入图片描述
dist/vue.runtime.esm-bundler.js 是vue文件真正的路径
所以我们需要把
import XX from “vue” 变成 … from “baseurl + /node_moudle/dist/vue.runtime.esm-bundler.js”

// 获取模块名称,就是@module/ 后面的内容
    const moduleName = url.replace("/@module/", "");
    // 在node_module 中找到对应的模块
    const prefix = path.join(__dirname, "../node_modules", moduleName);
    // 要加载文件的地址
    const module = require(prefix + "/package.json").module;
    const filePath = path.join(prefix, module);
    const ret = fs.readFileSync(filePath, "utf-8");
    ctx.type = "text/javascript";
    ctx.body = rewriteImport(ret);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

看一下处理后的路径地址,对filePath的打印
在这里插入图片描述
如果要保证运行,还需要在index.html添加

// 这是vue的第三方包中的某个配置,这里相当于欺骗了浏览器
 <script>
      window.process = {
        env: {
          NODE_ENV: 'dev'
        }
      }
    </script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

5、单文件组件支持,.vue文件(sfc)
实现思路:*.vue文件 => template 模板 => render函数

  1. *.vue文件 => template 模板 需要借助 compiler-sfc
    这里主要提取js代码,生产render函数
  2. template => render函数 需要借助 compiler-dom
// 读取vue文件的内容
    const p = path.join(__dirname, url.split("?")[0]);
    const ret = compilerSfc.parse(fs.readFileSync(p, "utf-8")); // 将vue文件中的块进行解析,得到一个ast
    if (!query.type) {
      // 没有type说明是sfc(单文件组件)
      // 解析sfc,处理内部的js
      console.log(ret);
      // 获取脚本内容
      const scriptContent = ret.descriptor.script.content;
      ctx.type = "text/javascript";
      ctx.body = `
      ${rewriteImport(scriptContent)}
      // template解析转换为另一个请求单独做
      import { render as  __render } from '${url}?type=template'
      // __script.render = __render
      export default __render
      `;
    } else if (query.type === "template") {
      // 2.template => render 函数
      const tpl = ret.descriptor.template.content;
      // 编译为包含render的模块
      const render = compilerDom.compile(tpl, { mode: "module" }).code;
      ctx.type = "text/javascript";
      ctx.body = rewriteImport(render);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

对ret进行打印,看一下script和template
在这里插入图片描述
对其中的content部分进行处理
6、处理.css文件
思路:创建style便签,将css文件的内容进行读取,添加到style标签中

const p = path.join(__dirname, url.split("?")[0]);
    const file = fs.readFileSync(p, "utf-8");
    // css 转化为js代码
    // 利用js创建style标签
    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;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

四、vite插件

vite插件可以扩展vite能力,比如解析用户自定义的文件输入,在打包代码前转义代码,或者查找第三方模块

1、 插件钩子

开发时,vite dev server 创建一个插件容器,按照Rollup调用创建钩子的规则请求各个钩子函数

在服务启动时调用一次:

optipns  替换或操控rollup选项(只在打包时有用,开发时为空)
buildStart 开始创建(只是一个信号)
  • 1
  • 2

每次有模块请求时都会调用:

resolveId 创建自定义确认函数,常用语句定位第三方依赖
load 创建自定义加载函数,可用于返回自定义的内容
transform 可用于装换已加载的模块内容
  • 1
  • 2
  • 3

在服务器关闭时调用一次:

buildEnd
closeBundle
  • 1
  • 2

vite 特有钩子

config:修改Vite配置(可以配置别名)
configResolved :vite配置确认
configureServer:用于配置dev server
transformIndexHtml: 用于转换宿主页(可以注入或者删除内容)
handleHotUpdate:自定义HMR更新时调用
  • 1
  • 2
  • 3
  • 4
  • 5

2、钩子调用顺序
config → configResolved → optipns → configureServer → buildStart → transform → load → resolveId → transformIndexHtml → vite dev server

3、插件顺序
别名处理 Alias
用户插件执行(如果设置 enforce : ‘pre’)
Vite 核心插件(plugin-vue)
用户插件执行(如果未设置 enforce)
Vite 构建插件
用户插件执行(如果设置 enforce : ‘post’)
Vite 构建后置插件

4、实现一个mock服务器—vite-plugin-mock

实现思路:

给开发服务器实例(concent)配置一个中间件,这个中间件可以存储用户配置接口映射信息,并提前处理输入请求,如果请求的url和路由表匹配则接管,按用户配置的handler返回结果

import path from "path";

let mockRouteMap = {};

function matchRoute(req) {
  let url = req.url;
  let method = req.method.toLowerCase();
  let routeList = mockRouteMap[method];

  return routeList && routeList.find((item) => item.path === url);
}

// 默认导出的插件工厂函数
export default function (options = {}) {
  // 获取mock文件入口,默认是index
  options.entry = options.entry || "./mock/index.js";
  // 转换为绝对路径
  if (!path.isAbsolute(options.entry)) {
    options.entry = path.resolve(process.cwd(), options.entry);
  }
  // 返回的插件
  return {
    configureServer: function ({ app }) {
      // 定义路由表
      const mockObj = require(options.entry);
      // 创建路由表
      createRoute(mockObj);

      // 定义中间件:路由匹配
      const middleware = (req, res, next) => {
        // 1.执行匹配过程
        let route = matchRoute(req);
        //2. 存在匹配,是一个mock请求
        if (route) {
          console.log("mock req", route.method, route.path);
          res.send = send;
          route.handler(req, res);
        } else {
          next();
        }
      };

      // 最终目标,给app注册一个中间件
      app.use(middleware);
    },
  };
}

function createRoute(mockConfList) {
  mockConfList.forEach((mockConf) => {
    let method = mockConf.method || "get";
    let path = mockConf.url;
    let handler = mockConf.response;
    // 路由对象
    let route = { path, method: method.toLowerCase(), handler };
    if (!mockRouteMap[method]) {
      mockRouteMap[method] = [];
    }
    console.log("create mock api");
    // 存入映射对象中
    mockRouteMap[method].push(route);
  });
}

// 实现一个send方法
function send(body) {
  let chunk = JSON.stringify(body);
  if (chunk) {
    chunk = Buffer.from(chunk, "utf-8");
    this.setHeader("Content-Length", chunk.length);
  }
  this.setHeader("Content-Type", "application/json");
  this.statusCode = 200;
  this.end(chunk, "utf8");
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76

demo

demo地址:https://github.com/wkh1234/vite-demo

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/盐析白兔/article/detail/390328
推荐阅读
相关标签
  

闽ICP备14008679号