之前咱们曾经实现了一个不带依赖预构建版本的vite, 在vite2.0中减少了一个代表性优化策略 依赖预构建
明天咱们也来手写一个。

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

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

代码分两局部

  1. 依赖预构建局部
  2. 本地服务局部

先开始编写依赖预构建局部

先把所须要的依赖引入

const http = require('http');const path = require('path');const url = require('url');const querystring = require('querystring');const glob = require('fast-glob'); // 在规定范畴内查问指定的文件,并返回绝对路径const { build } = require('esbuild'); // 打包编译esm模块const fs = require('fs');const os = require('os'); // 获取以后的零碎信息const { createHash } = require('crypto'); // 加密应用const { init, parse } = require('es-module-lexer'); // 查问出代码中应用import局部信息const MagicString = require('magic-string');// 替换代码中门路const compilerSfc = require('@vue/compiler-sfc');// 将sfc转化为json数据const compilerDom = require('@vue/compiler-dom');// 将template转化为render函数

编写依赖预构建主函数

async function optimizeDeps() {  // 第一步:设置缓存存储的地位  let cacheDir = 'node_modules/.vite';  const dataPath = path.join(cacheDir, '_metadata.json');  const mainHash = getDepHash();  const data = {    hash: mainHash,    browserHash: mainHash,    optimized: {},  };  if (fs.existsSync(dataPath)) {    let prevData;    try {      // 解析.vite下的_metadata.json文件      prevData = JSON.parse(fs.readFileSync(dataPath, 'utf-8'));    } catch (error) {      console.log(error);    }    // 哈希是统一的,不须要从新绑定    if (prevData && prevData.hash === data.hash) {      return prevData;    }  }  if (fs.existsSync(cacheDir)) {    // 如果node_modules/.vite这个文件存在则清空.vite下的所有文件    emptyDir(cacheDir);  } else {    fs.mkdirSync(cacheDir, { recursive: true });  }  // 第二步:收集依赖模块门路  const { deps, missing } = await scanImports();  // {  //   'vue': ''C:\\Users\\dftd\\desktop\\vite\\node-vue\\node_modules\\vue\\dist\\vue.runtime.esm-bundler.js''  // }  console.log('deps', deps);  // update browser hash  data.browserHash = createHash('sha256')    .update(data.hash + JSON.stringify(deps))    .digest('hex')    .substr(0, 8);  const qualifiedIds = Object.keys(deps);  // 如果没有找到任何依赖,那么间接把data数据写入.vite/_metadata.json  if (!qualifiedIds.length) {    fs.writeFileSync(dataPath, data);    return data;  }  // 第三步: 对收集的依赖进行解决  // 比方deps的数据是 { 'plamat/byte': 'C:\\Users\\dftd\\node_modules\\vue\\dist\\byte.js'  }  const flatIdDeps = {}; // 这个对象存储的是 { plamat_byte: 'path' } 的模式  const idToExports = {}; // 这个对象存储的是{ plamat/byte: 'souce'} 的模式  const flatIdToExports = {}; // 这个对象存储的是{ plamat/byte: 'souce'} 的模式  await init;  // 将 例如 node/example ==> node_example  const flattenId = (id) => id.replace(/[\/\.]/g, '_');  for (const id in deps) {    const flatId = flattenId(id);    flatIdDeps[flatId] = deps[id];    const entryContent = fs.readFileSync(deps[id], 'utf-8');    const exportsData = parse(entryContent);    for (const { ss, se } of exportsData[0]) {      const exp = entryContent.slice(ss, se);      if (/export\s+\*\s+from/.test(exp)) {        exportsData.hasReExports = true;      }    }    idToExports[id] = exportsData;    flatIdToExports[flatId] = exportsData;  }  const define = {    'process.env.NODE_ENV': 'development',  };  console.log('flatIdDeps', flatIdDeps);  function esbuildVitePlugin(flatIdDeps) {    return {      name: 'vite-scan',      setup(build) {        build.onResolve({ filter: /\*/ }, (args) => {          console.log('打印后果', args);        });      },    };  }  const result = await build({    absWorkingDir: process.cwd(),    // entryPoints: Object.keys(flatIdDeps),    entryPoints: Object.values(flatIdDeps),    bundle: true,    format: 'esm',    outdir: cacheDir, // 配置打完包的文件存储的地位 cacheDir默认为    treeShaking: true,    metafile: true,    define,    // plugins: [esbuildVitePlugin(flatIdDeps)],  });  // console.log('result', result);  const metafile = result.metafile;  // 将 _metadata.json 写入 .vite  const cacheDirOutputPath = path.relative(process.cwd(), cacheDir);  // console.log(cacheDirOutputPath);  for (const id in deps) {    // p ==> C:\Users\dftd\desktop\vite\node-vue\node_modules\.vite\vue.js    // normalizePath(p) ==> C:/Users/dftd/desktop/vite/node-vue/node_modules/.vite/vue.js    const p = path.resolve(cacheDir, flattenId(id) + '.js');    const entry = deps[id];    data.optimized[id] = {      file: normalizePath(p),      src: normalizePath(entry),      needsInterop: false,    };  }  fs.writeFileSync(dataPath, JSON.stringify(data, null, 2));  return data;}function normalizePath(id) {  const isWindows = os.platform() === 'win32';  return path.posix.normalize(isWindows ? id.replace(/\\/g, '/') : id);}// 将此我的项目的xxx.lock.json文件和局部vite配置内容生成一个hashfunction getDepHash() {  // 读取xxx.lock.json文件的内容  const content = lookupFile() || '';  const cryptographicStr = createHash('sha256')    .update(content)    .digest('hex')    .substring(0, 8);  return cryptographicStr;}// 读取xxx.lock.json文件的内容function lookupFile() {  const lockfileFormats = ['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml'];  let content = null;  for (let index = 0; index < lockfileFormats.length; index++) {    const lockPath = path.resolve(__dirname, lockfileFormats[index]);    const isExist = fs.existsSync(lockPath, 'utf-8');    if (isExist) {      content = fs.readFileSync(lockPath);      break;    }  }  // for (const index of lockfileFormats) {  //   console.log(index);  //   const lockPath = path.resolve(__dirname, index);  //   const isExist = fs.existsSync(lockPath, 'utf-8');  //   if (isExist) {  //     content = fs.readFileSync(lockPath);  //     return content;  //   }  // }  return content;}function emptyDir(dir) {  for (const file of fs.readdirSync(dir)) {    const abs = path.resolve(dir, file);    if (fs.lstatSync(abs).isDirectory()) {      emptyDir(abs);      fs.rmdirSync(abs);    } else {      fs.unlinkSync(abs);    }  }}// esbuild pluginfunction esbuildScanPlugin(deps) {  return {    name: 'dep-scan',    setup(build) {      // 解析index.html      build.onResolve({ filter: /\.(html|vue)$/ }, (args) => {        // console.log(args);        // const path1 = path.resolve(__dirname, args.path);        return {          path: args.path,          namespace: 'html',        };      });      // 加载以后index.html 文件 返回出 main.js      build.onLoad(        { filter: /\.(html|vue)$/, namespace: 'html' },        async ({ path: ids }) => {          const scriptModuleRE =            /(<script\b[^>]*type\s*=\s*(?:"module"|'module')[^>]*>)(.*?)<\/script>/gims;          const srcRE = /\bsrc\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s'">]+))/im;          const langRE = /\blang\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s'">]+))/im;          const scriptRE = /(<script\b(?:\s[^>]*>|>))(.*?)<\/script>/gims;          const importsRE =            /(?:^|;|\*\/)\s*import(?!\s+type)(?:[\w*{}\n\r\t, ]+from\s*)?\s*("[^"]+"|'[^']+')\s*(?:$|;|\/\/|\/\*)/gm;          let raw = fs.readFileSync(ids, 'utf-8');          raw = raw.replace(/<!--(.|[\r\n])*?-->/, '<!---->');          const isHtml = ids.endsWith('.html');          const regex = isHtml ? scriptModuleRE : scriptRE;          regex.lastIndex = 0;          let js = '';          let loader = 'js';          let match;          while ((match = regex.exec(raw))) {            const [, openTag, content] = match;            const srcMatch = openTag.match(srcRE);            const langMatch = openTag.match(langRE);            const lang =              langMatch && (langMatch[1] || langMatch[2] || langMatch[3]);            if (lang === 'ts' || lang === 'tsx' || lang === 'jsx') {              loader = lang;            }            if (srcMatch) {              const src = srcMatch[1] || srcMatch[2] || srcMatch[3];              js += `import ${JSON.stringify(src)}\n`;            } else if (content.trim()) {              js += content + '\n';            }          }          if (            loader.startsWith('ts') &&            (ids.endsWith('.svelte') ||              (ids.endsWith('.vue') && /<script\s+setup/.test(raw)))          ) {            // when using TS + (Vue + <script setup>) or Svelte, imports may seem            // unused to esbuild and dropped in the build output, which prevents            // esbuild from crawling further.            // the solution is to add `import 'x'` for every source to force            // esbuild to keep crawling due to potential side effects.            let m;            const original = js;            while ((m = importsRE.exec(original)) !== null) {              // This is necessary to avoid infinite loops with zero-width matches              if (m.index === importsRE.lastIndex) {                importsRE.lastIndex++;              }              js += `\nimport ${m[1]}`;            }          }          if (!js.includes(`export default`)) {            js += `\nexport default {}`;          }          return {            loader,            contents: js,          };        },      );      // 解析第三方库的esm js模块文件 间接走打包      build.onResolve(        {          // avoid matching windows volume          filter: /\.js\?v=1$/,        },        ({ path: id, importer }) => {          return {            path: id,            external: true,          };        },      );      // 解析.js 文件      build.onResolve(        {          // avoid matching windows volume          filter: /main\.js$/,        },        ({ path: id, importer }) => {          return {            path: id,            namespace: 'mianJs',          };        },      );      // 加载.js文件的内容      build.onLoad(        { filter: /main\.js$/, namespace: 'mianJs' },        ({ path: id }) => {          const c = fs.readFileSync(id, 'utf-8');          const magicString = new MagicString(c);          let imports = parse(c)[0];          imports.forEach((i) => {            const { s, e, n } = i;            let absolutePath = path.resolve(__dirname, 'node_modules', n);            const isExist = fs.existsSync(absolutePath);            if (isExist) {              const modulePath = require(absolutePath + '/package.json').module;              const esmPath = path.resolve(absolutePath, modulePath);              magicString.overwrite(s, e, `${esmPath}?v=1`);              deps[n] = esmPath;            } else {              // let aa = path.resolve(__dirname, n);              // magicString.overwrite(s, e, aa);            }          });          return {            loader: 'js',            contents: magicString.toString(),          };        },      );    },  };}// 收集依赖模块门路 返回一个依赖合集对象async function scanImports() {  const deps = {};  const missing = {};  let entries;  // 查问出当前目录下后缀为html的文件  entries = await glob('**/*.html', {    cwd: process.cwd(),    ignore: ['**/node_modules/**'],    absolute: true,  });  // entries => [ 'C:/Users/dftd/Desktop/vite/node-vue/index.html' ]  // console.log('entries', entries);  if (!entries.length) {    return {      deps: {},      missing: {},    };  }  const scanPath = esbuildScanPlugin(deps);  // 应用 esbuild 进行一次打包,打包过程中就找到了deps和missing,最初返回deps和missing  // Promise.all(  //   entries.map((entry) => {  //     console.log(222);  //     await build({  //       absWorkingDir: process.cwd(), // 工作的目录  //       entryPoints: [entry], // 入口文件(个别为index.html)  //       write: false, // build API能够间接写入文件系统 默认状况下,JavaScript API会写入文件系统  //       bundle: true, // 应用analyze性能生成一个对于bundle内容的易于浏览的报告  //       format: 'esm', // 输入的类型 (iife, cjs, esm)  //       plugins: [scanPath],  //     });  //   }),  // );  await build({    absWorkingDir: process.cwd(), // 工作的目录    entryPoints: entries, // 入口文件(个别为index.html)    write: false, // build API能够间接写入文件系统 默认状况下,JavaScript API会写入文件系统    bundle: true, // 应用analyze性能生成一个对于bundle内容的易于浏览的报告    format: 'esm', // 输入的类型 (iife, cjs, esm)    plugins: [scanPath],  });  return {    deps,    missing,  };}

依赖预构建总结:

依赖预构建产生在在本地服务启动之前执行, 大抵过程分为两步,第一步就是依赖的收集(这部分用到了esbuild build办法来收集所有的依赖门路),第二部是把这些收集的依赖门路再次用esbuild build办法进行编译,编译的后果存储到node_modules/.vite中, 并且还创立了一份_metadata.json文件对构建出的依赖做记录。

接下来编写本地服务局部

function createServer() {  optimizeDeps();  const serve = {};  let httpServe = http.createServer((req, res) => {    let pathName = url.parse(req.url).pathname;    // console.log(22, pathName);    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';    }    if (/.vite/.test(pathName)) {      // 间接加载预构建好的包      resolveViteModules(pathName, extType, res);    } else if (/\/@modules\//.test(pathName)) {      // 加载那些没有预构建好的包第三方包      resolveNodeModules(pathName, res);    } else {      // 加载非第三方包文件      resolveModules(pathName, extName, extType, res, req);    }  });  serve.listen = () => {    httpServe.listen(7777);  };  return serve;}// 重写门路function rewriteImports(source) {  let imports = parse(source)[0];  const magicString = new MagicString(source);  if (imports.length) {    for (let index = 0; index < imports.length; index++) {      const { s, e } = imports[index];      // 失去以后引入的第三方库的name      const id = source.substring(s, e);      if (/^[^\.\/]/.test(id)) {        // 依据id到node_modules/.vite/_metadata.json中查问以后这个包是否存在        const _metadataPath = path.resolve(          __dirname,          'node_modules/.vite/_metadata.json',        );        const _metaContent = require(_metadataPath).optimized;        console.log('_metaContent', _metaContent);        if (_metaContent[id]) {          magicString.overwrite(s, e, `/node_modules/.vite/${id}`);        } else {          magicString.overwrite(s, e, `/@modules/${id}`);        }      }    }  }  return magicString.toString();}// 解决加载非第三方包文件function resolveModules(pathName, extName, extType, res, req) {  fs.readFile(`.${pathName}`, 'utf-8', (err, data) => {    if (err) {      throw err;    }    console.log('extName', extName);    console.log('extType', extType);    res.writeHead(200, {      'Content-Type': `${extType}; charset=utf-8`,    });    if (extName == '.vue') {      const query = querystring.parse(url.parse(req.url).query);      const ret = compilerSfc.parse(data);      const { descriptor } = ret;      if (!query.type) {        // 解析出vue文件script局部        const scriptBlock = descriptor.script.content;        const newScriptBlock = rewriteImports(          scriptBlock.replace('export default', 'const __script = '),        );        const newRet = `        ${newScriptBlock}        import { render as __render } from '.${pathName}?type=template'        __script.render = __render        export default __script        `;        res.write(newRet);      } else {        // 解析出vue文件template局部 生成render函数        const templateBlock = descriptor.template.content;        const compilerTemplateBlockRender = rewriteImports(          compilerDom.compile(templateBlock, {            mode: 'module',          }).code,        );        res.write(compilerTemplateBlockRender);      }    } else if (extName == '.js') {      const r = rewriteImports(data);      res.write(r);    } else {      res.write(data);    }    res.end();  });}// 在node_modules 中读取没有预构建好的资源function resolveNodeModules(pathName, res) {  const id = pathName.replace(/\/@modules\//, '');  // 如果是加载的是第三方包  // 获取第三方包的相对地址  let absolutePath = path.resolve(__dirname, 'node_modules', id);  // console.log('absolutePath', absolutePath);  // 获取第三方包的esm的包地址  const modulePath = require(absolutePath + '/package.json').module;  const esmPath = path.resolve(absolutePath, modulePath);  // console.log('esmPath', esmPath);  // const pkgPath = `./node_modules/${id}/${modulePath}`;  fs.readFile(esmPath, 'utf-8', (err, data) => {    if (err) {      throw err;    }    res.writeHead(200, {      'Content-Type': `application/javascript; charset=utf-8`,    });    // const b = cjsEs6(data);    // console.log(b);    const r = rewriteImports(data);    // console.log(r);    res.write(r);    res.end();  });}// 加载.vite预构建好的包function resolveViteModules(pathName, extType, res) {  const id = pathName.replace(/\/node_modules\/.vite\//, '');  // 依据id到node_modules/.vite/_metadata.json中查问以后这个包是否存在  const _metadataPath = path.resolve(    __dirname,    'node_modules/.vite/_metadata.json',  );  const _metaContent = require(_metadataPath).optimized;  const { src, file } = _metaContent[id];  // 提取以后文件门路中的文件名称  const absFileName = path.basename(src);  const absFile = path.resolve(__dirname, 'node_modules/.vite', absFileName);  const content = fs.readFileSync(absFile, 'utf-8');  // console.log(1111, content);  // console.log(222, absFile);  res.writeHead(200, {    'Content-Type': `application/javascript; charset=utf-8`,  });  res.write(content);  res.end();}const serve = createServer();serve.listen();

本地服务局部总结:
我在上一篇中写过一篇不带预构建版本的vite,这个版本是在之前的根底重写了rewriteImports办法把门路重写改为/node_modules/.vite 能够间接加载预构建好的文件, 如果你还没有看我的不带预构建版本的vite倡议先看下,由浅入深能力更好的了解vite的实质原理。