乐趣区

关于前端:vite系列手写一个简易版的vite

家喻户晓 vite 曾经是以后 web 新一代构建工具的佼佼者, 次要是让前端开发者在本地调试阶段批改代码后无需打包间接疾速失去一个响应,这让咱们在开发阶段的效率失去一个晋升, 接下来为了深刻理解 vite 原理,咱们由浅入深先本人实现一个不带预构建版本的 vite。

首先咱们要晓得的是 vite 现在能够做到不必打包间接在浏览器上执行 ESM 代码,是因为目前支流的浏览器都曾经反对了 ESM 模块加载即 import 能够间接被浏览器辨认,辨认的前提咱们只须要在 script 标签上增加 type=”module” 即可。

咱们要实现的性能如下图所示:

  1. 实现对 vue3 语法的解析
  2. 实现对 SFC 的解析
  3. 实现对 html 和 js 的解析
  4. 最初实现一个对数字的加减操作性能

第一步咱们先创立一个文件夹命名(node-vite)
第二步咱们在文件夹内创立一个 index.html 文件和一个入口文件 mian.js 以及一个 App.vue 文件

<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app"></div>
  </body>
  <script type="module" src="./main.js"></script>
  <script>
    // 提前申明这些值 是因为在解析第三方库的源码中有间接应用以下变量的,所以提前先申明进去
    const process = {
      env: {NODE_ENV: 'development',},
    };
    window.process = process;
    window.development = 'development';
  </script>
</html>
import * as Vue from 'vue';
import App from './App.vue';
Vue.createApp(App).mount('#app');
<template>
  <div> 我当初是数字:{{message}}</div>
  <button v-on:click="handleClick1"> 减少 </button>
  <button v-on:click="handleClick2"> 减小 </button>
</template>
<script>
export default {data() {
    return {message: 0,};
  },
  methods: {handleClick1() {this.message += 1;},
    handleClick2() {this.message -= 1;},
  },
};
</script>

这三个文件曾经是咱们要实现的加减数字操作性能的代码了,剩下的就是咱们要实现一个 vite 把这三个文件能够失常运行起来并在浏览器上看到成果。

咱们命令上进入到 node-vite 文件夹后执行 npm init, 接下来咱们装置以下包:

  1. nodemon ————– node 过程治理
  2. vue ——————- 我是装置的 3.2.20
  3. @vue/compiler-dom—— 版本和 vue 的版本统一(将 template 编译成 render 函数)
  4. @vue/compiler-sfc—— 版本和 vue 的版本统一(将 SFC 文件编译成 json 数据)
  5. es-module-lexer ——- 获取文件中 import 语句的信息用来获取通过 import 加载的包
  6. magic-string ———- 用来替换第三方包的门路

咱们再 node-vite 目录中创立 server.js 用来实现简略版的 vite 性能
package.json 代码如下

{
  "name": "node-vite",
  "version": "1.0.0",
  "description": "","main":"",
  "scripts": {"dev": "nodemon ./server.js",},
  "author": "","license":"ISC","dependencies": {"@vue/compiler-dom":"3.2.20","@vue/compiler-sfc":"3.2.20","es-module-lexer":"^0.9.3","magic-string":"^0.25.7","vue":"3.2.20"},"devDependencies": {"nodemon":"^2.0.15"}
}

进入到 server.js 咱们先创立一个本地的 http 服务器

const http = require('http');
const server = http.createServer((req, res) => {});
server.listen(9999);

咱们的目标是 当浏览器拜访 localhost:9999 的时候让浏览器默认拜访 index.html, 从浏览器外面加载 index.html 后默认解析 main.js, 当浏览器再解析到 import * as Vue from 'vue'; 必定解析不出 vue 的文件,咱们要做的是批改 vue 的门路而后从而被动找到 vue 的源码地位后取出 vue 的 esm 版本的 js 文件而后再被动返回给浏览器, 接下来咱们开始解决

const http = require('http');
const url = require('url');
const path = require('path');
const server = http.createServer((req, res) => {
    // 解析出拜访地址 url 的文件名
    let pathName = url.parse(req.url).pathname;
    // 当浏览器间接拜访 localhost:9999 的时候 间接返回 index.html 文件
    if(pathName === '/'){pathName = '/index.html'}
    // 解析出文件的后缀类型
    let extName = path.extname(pathName);
    let extType = '';
    switch (extName) {
      case '.html':
        extType = 'text/html';
        break;
      case '.js':
        extType = 'application/javascript';
        break;
      case '.css':
        extType = 'text/css';
        break;
      case '.ico':
        extType = 'image/x-icon';
        break;
      case '.vue':
        extType = 'application/javascript';
        break;
      default:
        extType = 'text/html';
    }
    // 咱们会把 import * as Vue from 'vue' 这样的语句变成 import * as Vue from '/@modules/vue';
    // 这样咱们会晓得带 `/@modules` 标识的门路须要到 node_modules 中找源代码
    if (/\/@modules\//.test(pathName)) {
        // 如果解析到门路有 `/@modules` 则到 node_modules 中找对应库的源代而后返回给浏览器
        resolveNodeModules(pathName, res);
    } else {
        // 否则解析惯例的 js 文件、.vue 文件、html 文件
        resolveModules(pathName, extName, extType, res, req);
    }
});
server.listen(9999);

到目前为止咱们的 server.js 文件构造架子搭建好了, 接下来咱们开始实现 resolveModules() 函数

const compilerSfc = require('@vue/compiler-sfc');
const compilerDom = require('@vue/compiler-dom');
const fs = require('fs');
function resolveModules(pathName, extName, extType, res, req){fs.readFile(`.${pathName}`, 'utf-8', (err, data) => {
        // 如果文件解析出错则间接抛处谬误
        if(err){throw err;}
        // 设置 Content-Type
        res.writeHead(200, {'Content-Type': `${extType}; charset=utf-8`})

       // 对申请的过去的不同类型的文件做出响应的解决
       if(extName === '.js'){
        // 对后缀为.js 的文件解决
        // rewriteImports 函数作用将替换 import 引入第三方包的门路, 一会咱们实现这个函数
        const r = rewriteImports(data);
        res.write(r);
       } else if (extName === '.vue') {// 对后缀为.vue 的文件解决(即 SFC)
          // 解析出申请 url 的参数对象
          const query = querystring.parse(url.parse(req.url).query);
          // 通过 @vue/compiler-sfc 库把 sfc 解析成 json 数据
          const ret = compilerSfc.parse(data);
          const {descriptor} = ret;
          if (!query.type) {
            // 解析出 sfc 文件 script 局部
            const scriptBlock = descriptor.script.content;
            // 在 sfc 文件中咱们也可能应用 import 引入文件所以须要 rewriteImports 函数把外面的门路进行替换
            const newScriptBlock = rewriteImports(scriptBlock.replace('export default', 'const __script ='),
            );
            // 将替换好的 js 局部和动静引入 render 函数(template 编译而成)组合再一起而后返回到浏览器
            const newRet = `
            ${newScriptBlock}
            import {render as __render} from '.${pathName}?type=template'
            __script.render = __render
            export default __script
            `;
            res.write(newRet);
          } else {// 浏览器再次解析到 `import { render as __render} from './App.vue?type=template'` 会加载 render 函数
            // 解析出 vue 文件通过 @vue/compiler-dom 库将 template 局部变为 render 函数
            const templateBlock = descriptor.template.content;
            const compilerTemplateBlockRender = rewriteImports(
              compilerDom.compile(templateBlock, {mode: 'module',}).code,
            );
            res.write(compilerTemplateBlockRender);
         }
       } else {
        // 对其余后缀比方.html、ico 的文件解决
        // 不须要做任何解决间接返回
        res.write(data);
       }
       res.end();})
}

咱们开始实现 rewriteImports()函数

// es-module-lexer 参数解析
// n 示意模块的名称
// s 示意模块名称在导入语句中的开始地位
// e 示意模块名称在导入语句中的完结地位
// ss 示意导入语句在源代码中的开始地位
// se 示意导入语句在源代码中的完结地位
// d 示意导入语句是否为动静导入,如果是则为对应的开始地位,否则默认为 -1
const {init, parse} = require('es-module-lexer');
const MagicString = require('magic-string');
function rewriteImports(soure) {const imports = parse(soure)[0];
    const magicString = new MagicString(soure);
    if (imports.length) {for (let i = 0; i < imports.length; i++) {const { s, e} = imports[i];
      let id = soure.substring(s, e);
      if (/^[^\/\.]/.test(id)) {// id = `/@modules/${id}`;
        // 批改门路减少 /@modules 前缀
        // magicString.overwrite(s, e, id);
        magicString.overwrite(s, e, `/@modules/${id}`);
      }
    }
    return magicString.toString();}
}

rewriteImports 函数中应用了 es-module-lexer 和 magic-string 两个黑魔法库来达到替换门路的性能 也是 vite 中所应用的
接下来咱们实现最初一个函数 resolveNodeModules 在 node_modules 中获取第三方包的资源并返回给浏览器

function resolveNodeModules(pathName, res){
    // 获取 `/@modules/vue` 中的 vue   
    const id = pathName.replace(/\/@modules\//, '');
    // 获取第三方包的相对地址
    let absolutePath = path.resolve(__dirname, 'node_modules', id);
    // 获取第三方包的 package.json 的 module 字段解析出 esm 的包地址
    const modulePath = require(absolutePath + '/package.json').module;
    const esmPath = path.resolve(absolutePath, modulePath);
    // 读取 esm 模块的 js 内容
    fs.readFile(esmPath, 'utf-8', (err, data) => {if (err) {throw err;}
      res.writeHead(200, {'Content-Type': `application/javascript; charset=utf-8`,})
      // 应用 rewriteImports 函数替换资源中引入的第三方包的门路
      const r = rewriteImports(data);
      res.write(r);
      res.end(););
}

以上就是实现了一个简易版的 vite, 咱们能够执行 npm run dev 来启动下服务看下成果。
从 vite2.0 后尤大神对 vite 进行了一系列的性能优化其中最为有代表性的优化是依赖预构建,我会在下一期中手写一个带有预构建版本的繁难 vite。写文章也让我对 vite 的原理了解更加粗浅,同时也心愿能帮忙到你们! 前面我也一直的更新其余源码解析文章!

退出移动版