vite1流程分析

1. 从入口出发

1. package.json

// 根据package.json中bin字段找到bin下面的vite.js
"bin": {
  "vite": "bin/vite.js"
},
// bin/vite.js 指定使用node执行
#!/usr/bin/env node
require('../dist/node/cli')
复制代码

2. cli

// 注册命令
cli.command('[root]').action(async(root, argv) => {
  const options = await resolveOptions({ argv, defaultMode: 'development' })
  return runServe(options)
})
cli..command('build [root]').action(async(root, argv) => {
  const options = await resolveOptions({ argv, defaultMode: 'production' })
  return runBuild(options)
})
复制代码

2.1 resolveOptions

function resolveOptions() {
  // 读取用户的配置 可以通过 -c指定配置文件
  // require('vite.config.js')
  const userConfig = await resolveConfig()
}
复制代码

2.2 runServe

function runServer() {
  // vite1启动一个koa服务器
  const server = require('./server').createServer(options)
  server.listen(port, () => {})
}
复制代码

2.3 runBuild

function runBuild() {
  // 还是使用 rollup来打包
  await require('./build')[options.ssr ? 'ssrBuild' : 'build'](options)
}
复制代码

2. build

// vite打包最终还是使用的rollup来处理的
function build() {
  // 使用rollup打包
  const rollup = require("rollup").rollup;
  const bundle = await rollup();
  await bundle.write();
}
复制代码

3. server

// 启动一个koa服务器
function createServer(config) {
  const { root = process.cwd() } = config;
  const app = new Koa();
  const server = resolveServer(config, app.callback());
  // HMRWatcher 监听文件的变化
  const watcher = chokidar.watch(root, {});
  // resolveAlias
  // const resolver = createResolver();
  // 构建上下文对象 给中间件的
  const context = {
    root,
    app,
    server,
    watcher,
    config,
    port: config.port || 3000,
  };
  // 扩展ctx属性
  app.use((ctx, next) => {
    Object.assign(ctx, context);
    return next();
  });
  // 一些列的中间件
  const resolvedPlugins = [
    // 这里是各种中间件
    serveStaticPlugin, // 1. 让koa提供静态文件服务 koa-static
  ];
  resolvedPlugins.forEach((m) => m && m(context));
  const listen = server.listen.bind(server);
  server.listen = async (port, ...args) => {
    // 函数劫持 重写listen方法 runOptimize
    // await require("../optimizer").optimizeDeps(config);
    return listen(port, ...args);
  };
  return server;
}
复制代码

3.1 serveStaticPlugin

// 使用 koa-static 提供静态服务 这样我们就可以访问到 index.html 文件 会请求 /src/index.js 资源
// 下一步我们需要处理 index.js 这个文件
function serveStaticPlugin({ root, app, resolver, config }) {
  app.use(async (ctx, next) => {
    await next();
  });
  app.use(require("koa-etag")());
  app.use(require("koa-static")(root));
  app.use(require("koa-static")(path.join(root, "public")));
  // history api fallback
}
复制代码

3.2 moduleRewritePlugin

// index.js 浏览器不知道如何处理这些
// 改写路径 import { createApp } from "/@modules/vue"
import { createApp } from "vue";
import App from "./App.vue";
createApp(App).mount("#app");

const moduleRewritePlugin = ({ root, app, watcher, resolver }) => {
  app.use(async (ctx, next) => {
    await next();
    const publicPath = ctx.path;
    // 处理 js 类型的文件 .vue文件单独处理
    if (ctx.body && ctx.response.is("js") && !ctx.url.endsWith(".map")) {
      // 读取文件内容 stream.toString() 会判断是否为stream流
      const content = await readBody(ctx.body);
      // 动态 import
      // const importer = removeUnRelatedHmrQuery(
      //   resolver.normalizePublicPath(ctx.url)
      // );
      const importer = {};
      // 重写 import
      ctx.body = rewriteImports(root, content, importer, resolver);
    }
  });
  // watcher.on("change", async (filePath) => {});
};

// 重写 import
function rewriteImports(root, source, importer, resolver) {
  let imports = [];
  // es-module-lexer const [imports, exports] = parse(source, {})
  imports = parse(source)[0];
  if (imports.length) {
    // magic-string 操作字符串
    const s = new MagicString(source);
    for (let i = 0; i < imports.length; i++) {
      // 不考虑 dynamic 的情况
      const { s: start, e: end } = imports[i];
      // if (isExternalUrl(id)) {
      //   continue
      // }
      // 加上 /@modules/ 还会处理相对路径 ./foo -> /some/path/foo 等
      // const resolved = resolveImport(root, importer, id, resolver);
      // s.overwrite(start, end);
      let id = source.substring(start, end);
      if (/^[^\/\.]/.test(id)) {
        id = `/@modules/${id}`;
        // 修改路径增加 /@modules 前缀
        s.overwrite(start, end, id);
      }
    }
    return s.toString();
  } else {
    return source;
  }
}
复制代码

3.3 moduleResolvePlugin

// 请求 /@modules/vue 的时候找不到 解析 @modules 的模块
const moduleRE = /^\/@modules\//;
const moduleResolvePlugin = ({ root, app, resolver }) => {
  // 处理 vue 找到 @vue/ 下内容 esm模块的
  const vueResolved = resolveVue(root);
  app.use(async (ctx, next) => {
    // 不是 @modules 的模块 直接next
    if (!moduleRE.test(ctx.path)) return next();
    // 去掉 @modules 的前缀
    const id = decodeURIComponent(ctx.path.replace(moduleRE, ""));
    ctx.type = "js";
    const serve = async (id, file) => {
      //  moduleIdToFileMap.set(id, file) // cache缓存
      // ctx.read = cachedRead.bind(null, ctx) fs.readFile
      await ctx.read(file);
      return next();
    };
    if (id in vueResolved) {
      return serve(id, vueResolved[id]);
    }
    // 第三方模块的处理?
    // const referer = ctx.get("referer");
    // let importer;
    // // map文件
    // const isMapFile = ctx.path.endsWith(".map");
    // const importerFilePath = importer ? resolver.requestToFile(importer) : root;
    // const nodeModuleInfo = resolveNodeModule(root, id, resolver);
    // return serve(id, nodeModuleFilePath, "node_modules");
  });
};
// 处理vue的
const resolveVue = (root) => {
  let vueVersion;
  let vueBasePath;
  let compilerPath;
  // 找到package.json文件
  const projectPkg = JSON.parse(lookupFile(root, ["package.json"]) || `{}`);
  // 找到vue
  let isLocal = !!(projectPkg.dependencies && projectPkg.dependencies.vue);
  // 找到vue的 package resolve.sync()
  const userVuePkg = resolveFrom(root, "vue/package.json");
  vueBasePath = path.dirname(userVuePkg);
  vueVersion = fs.readJSONSync(userVuePkg).version;
  // 解析vue单文件的包
  const compilerPkgPath = resolveFrom(root, "@vue/compiler-sfc/package.json");
  const compilerPkg = require(compilerPkgPath);
  compilerPath = path.join(path.dirname(compilerPkgPath), compilerPkg.main);
  // vue的各个文件 vue3的各个模块
  const resolvePath = (name, from) =>
    resolveFrom(from, `@vue/${name}/dist/${name}.esm-bundler.js`);
  const runtimeDomPath = resolvePath("runtime-dom", vueBasePath);
  const runtimeCorePath = resolvePath("runtime-core", runtimeDomPath);
  const reactivityPath = resolvePath("reactivity", runtimeCorePath);
  const sharedPath = resolvePath("shared", runtimeCorePath);
  return {
    version: vueVersion,
    vue: runtimeDomPath,
    "@vue/runtime-dom": runtimeDomPath,
    "@vue/runtime-core": runtimeCorePath,
    "@vue/reactivity": reactivityPath,
    "@vue/shared": sharedPath,
    compiler: compilerPath,
    isLocal,
  };
};
复制代码

3.4 vuePlugin

// 处理 App.vue vue的单文件
// 1. 改写app.vue的内容
// 2. 处理 template文件
// 3. 处理style的内容
const vuePlugin = () => {
  // 只处理vue文件
  if (!ctx.path.endsWith(".vue") && !ctx.vue) { return next(); }
  // vue内容会改写 引入 template和style都是通过query来指定的
  const query = ctx.query;
  const publicPath = ctx.path;
  const filePath = path.join(root, publicPath);
  // 解析单文件组件
  // {
  //   filename: '',
  //   source: '',
  //   template: {
  //     type: 'template',
  //     content: '',
  //     ast: ''
  //   },
  //   script: {
  //     type: 'script',
  //   },
  //   styles: [
  //     { type: 'style', content: ''}
  //   ]
  // }
  const descriptor = await parseSFC(root, filePath, ctx.body);
  if(query.type) {
    ctx.type = "js";
    const {code} = await compileSFCMain()
    ctx.body = code
  }
  if (query.type === "template") {
    const templateBlock = descriptor.template;
    ctx.type = "js";
    const { code } = compileSFCTemplate()
    ctx.body = code;
  }
  if(query.type === "style") {
    const styleBlock = descriptor.styles[index];
    const result = await compileSFCStyle()
    ctx.type = "js";
    ctx.body = codegenCss()
  }
  if (query.type === "custom") {
    }
  // watcher.on('change', file => {handleVueReload()})
}
复制代码

3.4.1 parseSFC

// 读取文件内容 通过 @vue/compiler-sfc 解析单文件解析
async function parseSFC(root, filePath, content) {
  // 读取文件的内容
  content = await cachedRead(null, filePath, true /* poll */);
  if (typeof content !== "string") {
    content = content.toString();
  }
  // @vue/compiler-sfc 解析单文件
  const { parse } = resolveCompiler(root);
  const { descriptor } = parse(content, {
    filename: filePath,
    sourceMap: true,
  });
  return descriptor;
}
复制代码

3.4.2 compileSFCMain

// 我们会讲 .vue单文件的内容做修改
async function compileSFCMain(descriptor, filepath, publicPath, root) {
  // creates unique hashes
  const id = hash_sum(publicPath)
  let code = ``;
  let content = ``;
  let script = descriptor.script;
  const compiler = resolveCompiler(root);
  // 解析script内容
  if ((descriptor.script || descriptor.scriptSetup) && compiler.compileScript) {
    script = compiler.compileScript(descriptor, { id });
  }
  if (script) {
    content = script.content;
    // ts
    // if (script.lang === "ts") {
    //   // esbuild的transform
    //   const res = await transform(content, publicPath, {
    //     loader: "ts",
    //   });
    //   content = res.code;
    // }
  }
  // @vue/compiler-sfc
  code += rewriteDefault(content, "__script");
  // 样式的scoped和css-module
  let hasScoped = false;
  // let hasCSSModules = false;
  if (descriptor.styles) {
    descriptor.styles.forEach((s, i) => {
      const styleRequest = publicPath + `?type=style&index=${i}`;
      if (s.scoped) hasScoped = true;
      if (s.module) {
      } else {
        // 样式文件是通过 import的方式 然后会有 type属性
        code += `\nimport ${JSON.stringify(styleRequest)}`;
      }
      if (hasScoped) {
        // import "/src/App.vue?type=style&index=0"
        code += `\n__script.__scopeId = "data-v-${id}"`;
      }
    });
  }
  // if(descriptor.customBlocks) {}

  // template模版会变成render函数
  if (descriptor.template) {
    const templateRequest = publicPath + `?type=template`;
    // import { render as __render } from "/src/App.vue?type=template"
    code += `\nimport { render as __render } from ${JSON.stringify(
      templateRequest
    )}`;
    // __script.render = __render
    code += `\n__script.render = __render`;
  }
  code += `\n__script.__hmrId = ${JSON.stringify(publicPath)}`;
  code += `\ntypeof __VUE_HMR_RUNTIME__ !== 'undefined' && __VUE_HMR_RUNTIME__.createRecord(__script.__hmrId, __script)`;
  // __script.__file = "/Users/liu/vitejs/hello-vite/src/App.vue"
  code += `\n__script.__file = ${JSON.stringify(filepath)}`;
  code += `\nexport default __script`;
  return { code };
}
复制代码

app.png

3.4.3 compileSFCTemplate

// 处理模版还是  @vue/compiler-sfc
function compileSFCTemplate(root, template, filePath, publicPath, scoped) {
  const { compileTemplate } = resolveCompiler(root);
  const id = hash_sum(publicPath);
  const { code } = compileTemplate({
    source: template.content,
    id,
  });
  return { code };
}
复制代码

template.png

3.4.4 compileSFCStyle

// 处理样式
async function compileSFCStyle(root, style, index, filePath, publicPath) {
  // const { generateCodeFrame } = resolveCompiler(root)
  const resource = filePath + `?type=style&index=${index}`
  // 编译css postcss ? 还是使用的 @vue/compiler-sfc 内部使用的postcss
  const result = await compileCss(root, publicPath, {
    source: style.content, 
    filename: resource,
    id: ``, // will be computed in compileCss
    scoped: style.scoped != null,
    modules: style.module != null,
    preprocessLang: style.lang // css less scss stylus
  })
  // recordCssImportChain(result.dependencies, resource)
  // result.code = await rewriteCssUrls(result.code, publicPath)
  return result
}
复制代码

style.png

3.4.5 codegenCss

function codegenCss(id, css, modules) {
  let code =
    // clientPublicPath: /vite/client
    `import { updateStyle } from "${clientPublicPath}"\n` +
    `const css = ${JSON.stringify(css)}\n` +
    `updateStyle(${JSON.stringify(id)}, css)\n`
  if (modules) {
    code += dataToEsm(modules, { namedExports: true })
  } else {
    code += `export default css`
  }
  return code 
}
复制代码

3.5 clientPlugin

//  我们在处理样式的时候 codegenCss import { updateStyle } from  ‘/vite/client’
const clientPlugin = ({app, config}) => {
  // 就是读 client/client.js 内容
  const clientCode = fs.readFileSync(clientFilePath, 'utf-8')
  app.use(async(ctx, next) => {
    if (ctx.path === clientPublicPath) { // /vite/client
      // if (config.hmr){} // socket
      ctx.type = 'js'
      ctx.status = 200
      ctx.body = clientCode
    } else {
      return next()
    }
  })
}
复制代码

3.6 htmlRewritePlugin

// 其实在 htmlRewritePlugin中间件中我们插入了 一段script脚本
// 修改了我们index.html的内容
const htmlRewritePlugin = ({ root, app, watcher, resolver, config}) => {
  // 注入code <script type="module">import "/vite/client"</script>
  const devInjectionCode = `\n<script type="module">import "${clientPublicPath}"</script>\n`
  return injectScriptToHtml(html, devInjectionCode)
  app.use(async(ctx, next) => {
    await next()
    if (ctx.response.is('html') && ctx.body) {
      const importer = ctx.path
      const html = await readBody(ctx.body)
      if (!html) return
      ctx.body = await rewriteHtml(importer, html)
      return
    }
  })
  watcher.on('change', (file) => {})
}
复制代码

3.7 client/client.js

// 处理process环境变量的问题
const __MODE__ = ''
;(window).process = (window).process || {}
;(window).process.env = (window).process.env || {}
;(window).process.env.NODE_ENV = __MODE__
// 处理样式
function updateStyle(id, content) {
  let style = document.createElement('style')
  style.setAttribute('type', 'text/css')
  style.innerHTML = content
  document.head.appendChild(style)
}
// 还有很多其他的功能
复制代码

3.8 总结

1. 我们需要一个静态文件服务器 访问我们的页面
2. esModule无法识别 import {} from 'vue' 我们需要改写路径
3. 当引入 @module/xxx的模块时候 我们要对他们进行解析
4. 访问App.vue的时候我们需要对单文件处理
 4.1 重写app.vue的内容
 4.2 处理template render函数
 4.3 处理style
5. 我们访问index.html的时候会注入script <script type="module">import "/vite/client"</script>
6. 在client.js中出处理 process的问题
复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享