关于前端: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的原理了解更加粗浅,同时也心愿能帮忙到你们! 前面我也一直的更新其余源码解析文章!

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理