vite加载文件原理简析

532 阅读1分钟

1.加载入口实现

es6模块都是通过type设置为module来实现的,浏览器加载 ES6 模块,也使用<script>标签,但是要加入type="module"属性,可参考浏览器加载规则,所以在项目中需要设置index.html里面的文件为type="module" 即可完成浏览器加载。

<script src="/src/main.ts" type="module"></script>

2.文件的加载

首先在把 type= "module"放到<script>标签中来声明这是一个模块,这样通过srcimport导入的文件将会发起http请求;vite会拦截这些请求,并将请求文件进行特别处理。

我们通过起一个koa服务器来解析数据,当访问/时即把index.html返回即可

if (url === "/") {
    const home = fs.readFileSync('./index.html', 'utf-8')
    ctx.type = "text/html"
    ctx.body = home
  }

返回之后会去加载main.ts模块然后发起一个http请求到服务端

3.解析ts/js文件

服务端拿到ts文件后会读取文件信息,发现下面这段代码

import { createApp } from "vue";

之间返回读取的file后就会发现有如下的报错信息,没办法解析vue模块,所以需要对vue模块进行模块替换

vite中会存在裸模块替换,当试图请求node_modules文件夹的文件时,会进行裸模块替换(路径转换为相对路径),浏览器中只识别相对路径绝对路径

import Vue from 'vue' 会转换成 import Vue from '/@modules/vue'

接下里将/@modules解析为真正的文件地址,并返回给浏览器。其它打包工具,如webpack打包时帮我们做了这件事情。通过 import 导入的文件 webpack 会去 node_modules/包名/package.json 文件内找 moduel 属性,如下例子。

{ 
  "license": "MIT",
  "main": "index.js",
  "module": "dist/vue.runtime.esm-bundler.js",
  "name": "vue",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/vuejs/vue-next.git"
  },
  "types": "dist/vue.d.ts",
  "unpkg": "dist/vue.global.js",
  "version": "3.2.20"
}

只要将这个dist/vue.runtime.esm-bundler.js这个地址文件返回就好了。所以需要进行模块替换解析,下面使用了rewriteImport函数实现,vite是用的es-module-lexer来解析成ast拿到import的地址

 else if (url.endsWith(".ts")) {
    // 2. 返回ts
    const filePath = path.join(__dirname,url) // 获取绝对路劲
    const file = fs.readFileSync(filePath, 'utf-8')
    ctx.type = "application/javascript"
    ctx.body = rewriteImport(file)
  }
function rewriteImport(content) {
  return content.replace(/ from ['"](.*)['"]/g, (s1, s2) => {
    // s1, 匹配部分, s2: 匹配分组内容
    if (s2.startsWith("./") || s2.startsWith("/") || s2.startsWith("../")) {
      // 相对路劲直接返回
      return s1;
    } else {
      return ` from "/@modules/${s2}"`
    }
  }).replace(/process.env.NODE_ENV/g, '"development"')
}

替换模块之后发现返回的内容,并且并没有报错信息

同时也请求了一个http://localhost:3003/@modules/vue请求,接下来我们对这个请求进行处理

4.解析第三方模块

按照上面的分析我们只需将请求路劲以/@modules/开头的请求返回 node_modules/包名/package.json里面的module属性值文件即可,因为在node_modules里面有对应的模块存在,只需要将其解析出来就行,同时需要rewriteImport,因为模块可能也加载了其他模块

 else if (url.startsWith("/@modules/")) {
    const filePrefix = path.resolve(__dirname, 'node_modules', url.replace('/@modules/', ''))
    // /Users/yugangcao/Downloads/mini-vite/node_modules/vue
    const module = require(filePrefix + '/package.json').module
    // dist/vue.runtime.esm-bundler.js
    const file = fs.readFileSync(filePrefix + '/' + module, 'utf-8')
    // 读取文件地址 /Users/yugangcao/Downloads/mini-vite/node_modules/vue/dist/vue.runtime.esm-bundler.js
    ctx.type = "text/javascript"
    ctx.body = rewriteImport(file)
  }

5.解析.vue文件

main.ts里面会发出http://localhost:3003/src/App.vue 请求,加载App.vue文件,在vite中,单文件处理是使用的是@vue/compiler-sfc模块进行编译处理的,因此咱们也借助该模块完成Vue文件的处理。 它解析的结果大概如下:

同时在vue中我们使用@vue/compiler-dom 来编译 template,由于我们返回的vueruntime版本的,是没有编译器的,我们应该将编译好的template返回回去,下面我们返回一个渲染(render)函数即可 ,体验地址

rewriteImport(compile(templateContent, { mode: "module" }).code

export function render(_ctx, _cache) {
  const _component_HelloWorld = _resolveComponent("HelloWorld")

  return (_openBlock(), _createElementBlock(_Fragment, null, [
    _createElementVNode("img", {
      alt: "Vue logo",
      src: "./assets/logo.png"
    }),
    _createVNode(_component_HelloWorld, { msg: "Hello Vue 3 + TypeScript + Vite" })
  ], 64 /* STABLE_FRAGMENT */))
}

同时把解析的scrip内容进行rewriteImport后返回去加载其他模块,如果有style则需要加载一个style文件,解析代码如下

else if (url.endsWith(".vue")) {
    // 解析单文件组件相当于vue-loader做的事情
    // 转换script部分:将默认导出的组件对象转换为常量
    const p = path.resolve(__dirname, url.slice(1));
    const ret = parse(fs.readFileSync(p, "utf-8"));
    console.log('vue', ret)
    const scriptContent = ret.descriptor.script?.content ?? ret.descriptor.scriptSetup?.content;
    const script = scriptContent.replace(
      "export default ",
      "const __script = "
    );
    const templateContent = ret.descriptor.template.content;

    // 转换template为模板请求
    // 将转换获得的渲染函数设置到__script上
    // 最后重新导出__script
    ctx.type = "text/javascript";
    ctx.body = `
${rewriteImport(compile(templateContent, { mode: "module" }).code)}
${rewriteImport(script)}
// 如果有 style 就发送请求获取 style 的部分
${ret.descriptor.styles.length ? `import "${url}?type=style"` : ''}
__script.render = render
export default __script
    `;
  }

6.解析style

从.vue文件里返回的信息里会发出一个http://localhost:3003/src/App.vue?type=style 请求,Vite对style的处理比较特殊,处于热更新模块中,由于我们没有实现热更新,咱们这儿就模拟实现一下,将style content返回,在客户端实现该方法。

else if (url.endsWith('?type=style')) {
    const p = path.resolve(__dirname, url.split("?")[0].slice(1));
    const ret = parse(fs.readFileSync(p, "utf-8"));
    const styleBlock = ret.descriptor.styles[0];
    // console.log('style', styleBlock)
    ctx.type = "application/javascript";
    ctx.body = `
      const css = ${JSON.stringify(styleBlock.content)};
      updateStyle(css);
      export default css;
    `;
  }

index.html中添加updateStyle代码:

// ...
<body>
    <div id="app"></div>
    <script>
     function updateStyle(content) {
       const isExist = typeof CSSStyleSheet !== undefined
       if(isExist) {
         console.log('CSSStyleSheet is exist')
          // 方法1,使用可构造样式表
        let cssStyleSheet = new CSSStyleSheet()
        cssStyleSheet.replaceSync(content)
        document.adoptedStyleSheets = [
          ...document.adoptedStyleSheets,
          cssStyleSheet
        ]
       } else {
         // 方法2
        let style = document.createElement('style')
        style.setAttribute('type', 'text/css')
        style.innerHTML = content        document.head.appendChild(style)
       }
      }
    </script>
    <script src="/src/main.ts" type="module"></script>
</body>
// ...

7.解析静态文件

以png图片为例,当拦截到.png时,直接将读取到的文件信息返回即可

else if (url.endsWith(".png") | url.endsWith('.svg')) {
    const imgPath = path.join(__dirname,'src', url) // 获取绝对路劲
    ctx.body = fs.readFileSync(imgPath);
  }

8.解析.css文件

判断.css文件然后读取文件,加上插入代码后返回渲染即可

else 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,'')}"
      updateStyle(css);
      export default css
    `
    ctx.type = 'application/javascript'
    ctx.body = content
  }

9.解析错误问题

当解析node_modules中代码中会出现一个小错,就是vue源码里有用process.ENV判断环境的,我们浏览器client里设置以下即可,

错误信息如下

Uncaught ReferenceError: process is not defined
    at shared:442

我们在rewriteImport 中配置replace(/process.env.NODE_ENV/g, '"development"') 即可,当然也可以在index.html中添加这个环境变量也可以

最终页面的渲染效果图: