导航菜单

  • 0.api
  • 0.Async
  • 0.module
  • 1.ES2015
  • 2.Promise
  • 3.Node
  • 4.NodeInstall
  • 5.REPL
  • 6.NodeCore
  • 7.module&NPM
  • 8.Encoding
  • 9.Buffer
  • 10.fs
  • 11.Stream-1
  • 11.Stream-2
  • 11.Stream-3
  • 11.Stream-4
  • 12-Network-2
  • 12.NetWork-3
  • 12.Network-1
  • 13.tcp
  • 14.http-1
  • 14.http-2
  • 15.compress
  • 16.crypto
  • 17.process
  • 18.yargs
  • 19.cache
  • 20.action
  • 21.https
  • 22.cookie
  • 23.session
  • 24.express-1
  • 24.express-2
  • 24.express-3
  • 24.express-4
  • 25.koa-1
  • 26.webpack-1-basic
  • 26.webpack-2-optimize
  • 26.webpack-3-file
  • 26.webpack-4.tapable
  • 26.webpack-5-AST
  • 26.webpack-6-sources
  • 26.webpack-7-loader
  • 26.webpack-8-plugin
  • 26.webpack-9-hand
  • 26.webpack-10-prepare
  • 28.redux
  • 28.redux-jwt-back
  • 28.redux-jwt-front
  • 29.mongodb-1
  • 29.mongodb-2
  • 29.mongodb-3
  • 29.mongodb-4
  • 29.mongodb-5
  • 29.mongodb-6
  • 30.cms-1-mysql
  • 30.cms-2-mysql
  • 30.cms-3-mysql
  • 30.cms-4-nunjucks
  • 30.cms-5-mock
  • 30.cms-6-egg
  • 30.cms-7-api
  • 30.cms-8-roadhog
  • 30.cms-9-yaml
  • 30.cms-10-umi
  • 30.cms-12-dva
  • 30.cms-13-dva-ant
  • 30.cms-14-front
  • 30.cms-15-deploy
  • 31.dva
  • 31.cms-13-dva-antdesign
  • 33.redis
  • 34.unittest
  • 35.jwt
  • 36.websocket-1
  • 36.websocket-2
  • 38.chat-api-1
  • 38.chat-api-2
  • 38.chat-3
  • 38.chat-api-3
  • 38.chat
  • 38.chat2
  • 38.chat2
  • 39.crawl-0
  • 39.crawl-1
  • 39.crawl-2
  • 40.deploy
  • 41.safe
  • 42.test
  • 43.nginx
  • 44.enzyme
  • 45.docker
  • 46.elastic
  • 47.oauth
  • 48.wxpay
  • index
  • 52.UML
  • 53.design
  • index
  • 54.linux
  • 57.ts
  • 56.react-ssr
  • 58.ts_react
  • 59.ketang
  • 59.ketang2
  • 61.1.devops-linux
  • 61.2.devops-vi
  • 61.3.devops-user
  • 61.4.devops-auth
  • 61.5.devops-shell
  • 61.6.devops-install
  • 61.7.devops-system
  • 61.8.devops-service
  • 61.9.devops-network
  • 61.10.devops-nginx
  • 61.11.devops-docker
  • 61.12.devops-jekins
  • 61.13.devops-groovy
  • 61.14.devops-php
  • 61.15.devops-java
  • 61.16.devops-node
  • 61.17.devops-k8s
  • 62.1.react-basic
  • 62.2.react-state
  • 62.3.react-high
  • 62.4.react-optimize
  • 62.5.react-hooks
  • 62.6.react-immutable
  • 62.7.react-mobx
  • 62.8.react-source
  • 63.1.redux
  • 63.2.redux-middleware
  • 63.3.redux-hooks
  • 63.4.redux-saga
  • 63.5.redux-saga-hand
  • 64.1.router
  • 64.2.router-connected
  • 65.1.typescript
  • 65.2.typescript
  • 65.3.typescript
  • 65.4.antd
  • 65.4.definition
  • 66-1.vue-base
  • 66-2.vue-component
  • 66-3.vue-cli3.0
  • 66-4.$message组件
  • 66-5.Form组件
  • 66-6.tree
  • 66-7.vue-router-apply
  • 66-8.axios-apply
  • 66-9.vuex-apply
  • 66-10.jwt-vue
  • 66-11.vue-ssr
  • 66-12.nuxt-apply
  • 66-13.pwa
  • 66-14.vue单元测试
  • 66-15.权限校验
  • 67-1-network
  • 68-2-wireshark
  • 7.npm2
  • 69-hooks
  • 70-deploy
  • 71-hmr
  • 72.deploy
  • 73.import
  • 74.mobile
  • 75.webpack-1.文件分析
  • 75.webpack-2.loader
  • 75.webpack-3.源码流程
  • 75.webpack-4.tapable
  • 75.webpack-5.prepare
  • 75.webpack-6.resolve
  • 75.webpack-7.loader
  • 75.webpack-8.module
  • 75.webpack-9.chunk
  • 75.webpack-10.asset
  • 75.webpack-11.实现
  • 76.react_optimize
  • 77.ts_ketang_back
  • 77.ts_ketang_front
  • 78.vue-domdiff
  • 79.grammar
  • 80.tree
  • 81.axios
  • 82.1.react
  • 82.2.react-high
  • 82.3.react-router
  • 82.4.redux
  • 82.5.redux_middleware
  • 82.6.connected
  • 82.7.saga
  • 82.8.dva
  • 82.8.dva-source
  • 82.9.roadhog
  • 82.10.umi
  • 82.11.antdesign
  • 82.12.ketang-front
  • 82.12.ketang-back
  • 83.upload
  • 84.graphql
  • 85.antpro
  • 86.1.uml
  • 86.2.design
  • 87.postcss
  • 88.react16-1
  • 89.nextjs
  • 90.react-test
  • 91.react-ts
  • 92.rbac
  • 93.tsnode
  • 94.1.JavaScript
  • 94.2.JavaScript
  • 94.3.MODULE
  • 94.4.EventLoop
  • 94.5.文件上传
  • 94.6.https
  • 94.7. nginx
  • 95.1. react
  • 95.2.react
  • 96.1.react16
  • 96.2.fiber
  • 96.3.fiber
  • 97.serverless
  • 98.websocket
  • 100.1.react-basic
  • 101.1.monitor
  • 101.2.monitor
  • 102.java
  • 103.1.webpack-usage
  • 103.2.webpack-bundle
  • 103.3.webpack-ast
  • 103.4.webpack-flow
  • 103.5.webpack-loader
  • 103.6.webpack-tapable
  • 103.7.webpack-plugin
  • 103.8.webpack-optimize1
  • 103.9.webpack-optimize2
  • 103.10.webpack-hand
  • 103.11.webpack-hmr
  • 103.11.webpack5
  • 103.13.splitChunks
  • 103.14.webpack-sourcemap
  • 103.15.webpack-compiler1
  • 103.15.webpack-compiler2
  • 103.16.rollup.1
  • 103.16.rollup.2
  • 103.16.rollup.3
  • 103.16.vite.basic
  • 103.16.vite.source
  • 103.16.vite.plugin
  • 103.16.vite.1
  • 103.16.vite.2
  • 103.17.polyfill
  • 104.1.binary
  • 104.2.binary
  • 105.skeleton
  • 106.1.react
  • 106.2.react_hooks
  • 106.3.react_router
  • 106.4.redux
  • 106.5.redux_middleware
  • 106.6.connected-react-router
  • 106.6.redux-first-history
  • 106.7.redux-saga
  • 106.8.dva
  • 106.9.umi
  • 106.10.ketang
  • 106.11.antdesign
  • 106.12.antpro
  • 106.13.router-6
  • 106.14.ssr
  • 106.15.nextjs
  • 106.16.1.cms
  • 106.16.2.cms
  • 106.16.3.cms
  • 106.16.4.cms
  • 106.16.mobx
  • 106.17.fomily
  • 107.fiber
  • 108.http
  • 109.1.webpack_usage
  • 109.2.webpack_source
  • 109.3.dll
  • 110.nest.js
  • 111.xstate
  • 112.Form
  • 113.redux-saga
  • 114.react+typescript
  • 115.immer
  • 116.pro5
  • 117.css-loader
  • 118.1.umi-core
  • 119.2.module-federation
  • 119.1.module-federation
  • 120.create-react-app
  • 121.react-scripts
  • 122.react-optimize
  • 123.jsx-runtime
  • 124.next.js
  • 125.1.linux
  • 125.2.linux-vi
  • 125.3.linux-user
  • 125.4.linux-auth
  • 125.5.linux-shell
  • 125.6.linux-install
  • 125.7.linux-system
  • 125.8.linux-service
  • 125.9.linux-network
  • 125.10.nginx
  • 125.11.docker
  • 125.12.ci
  • 125.13.k8s
  • 125.14.k8s
  • 125.15.k8s
  • 125.16.k8s
  • 126.11.react-1
  • 126.12.react-2
  • 126.12.react-3
  • 126.12.react-4
  • 126.12.react-5
  • 126.12.react-6
  • 126.12.react-7
  • 126.12.react-8
  • 127.frontend
  • 128.rollup
  • 129.px2rem-loader
  • 130.health
  • 131.hooks
  • 132.keepalive
  • 133.vue-cli
  • 134.react18
  • 134.2.react18
  • 134.3.react18
  • 135.function
  • 136.toolkit
  • 137.lerna
  • 138.create-vite
  • 139.cli
  • 140.antd
  • 141.react-dnd
  • 142.1.link
  • 143.1.gulp
  • 143.2.stream
  • 143.3.gulp
  • 144.1.closure
  • 144.2.v8
  • 144.3.gc
  • 145.react-router-v6
  • 146.browser
  • 147.lighthouse
  • 148.1.basic
  • 148.2.basic
  • 148.3.basic
  • 148.4.basic
  • 148.5.basic
  • 149.1.vite
  • 149.2.vite
  • 149.3.vite
  • 149.4.vite
  • 150.react-window
  • 151.react-query
  • 152.useRequest
  • 153.transition
  • 154.emotion
  • 155.1.formily
  • 155.2.formily
  • 155.3.formily
  • 155.3.1.mobx.usage
  • 155.3.2.mobx.source
  • 156.vue-loader
  • 103.11.mf
  • 157.1.react18
  • 158.umi4
  • 159.rxjs
  • 159.rxjs2
  • 160.bff
  • 161.zustand
  • 162.vscode
  • 163.emp
  • 164.cors
  • 1.核心知识
    • 1.1 安装依赖
    • 1.2 Connect
    • 1.3 serve-static
    • 1.4 es-module-lexer
    • 1.5 resolve
    • 1.6 fast-glob
    • 1.7 magic-string
  • 2.实现命令行
    • 2.1 package.json
    • 2.2 vite3.js
    • 2.3 cli.js
  • 3.实现 http 服务器
    • 3.1 cli.js
    • 3.2 server\index.js
  • 4.实现静态文件中间件
    • 4.1 server\index.js
    • 4.2 static.js
    • 4.3 config.js
    • 4.4 utils.js
    • 4.5 index.html
    • 4.6 main.js
  • 5.分析第三方依赖
    • 5.1 main.js
    • 5.2 server\index.js
    • 5.3 optimizer\index.js
    • 5.4 scan.js
    • 5.5 esbuildScanPlugin.js
    • 5.6 pluginContainer.js
    • 5.7 resolve.js
  • 6. 预编译并保存 metadata
    • 6.1 server\index.js
    • 6.2 config.js
    • 6.3 optimizer\index.js
  • 7.修改导入路径
    • 7.1 server\index.js
    • 7.2 transform.js
    • 7.3 utils.js
    • 7.4 transformRequest.js
    • 7.5 pluginContainer.js
    • 7.6 send.js
    • 7.7 plugins\index.js
    • 7.8 importAnalysis.js
    • 7.9 preAlias.js
    • 7.10 config.js
  • 8.支持 vue 插件
    • 8.1 esbuildDepPlugin.js
    • 8.2 plugins\index.js
    • 8.3 utils.js
    • 8.4 config.js
    • 8.5 index.html
    • 8.6 main.js
    • 8.7 App.vue
    • 8.8 vue.js
    • 8.9 vite.config.js
  • 9.支持 style
    • 9.1 config.js
    • 9.2 App.vue
    • 9.3 vue.js
  • 10.支持环境变量
    • 10.1 plugins\index.js
    • 10.2 define.js
    • 10.3 plugins\vue.js
  • 11.HMR
    • 11.1.创建项目
      • 11.1.1 安装
      • 11.1.2.package.json
      • 11.1.3.src\main.js
      • 11.1.4.index.html
    • 11.2.封装模块
      • 11.2.1 src\render.js
      • 12.2.2 src\main.js
    • 11.3.销毁副作用
      • 11.3.1 render.js
    • 11.4.保留状态
      • 11.4.1 render.js
    • 11.5.拒绝更新
      • 11.5.1 render.js
      • 11.5.2 src\main.js
  • 12.支持 HMR
    • 12.1 server\index.js
    • 12.2 ws.js
    • 12.3 hmr.js
    • 12.4 transformRequest.js
    • 12.5 moduleGraph.js
    • 12.6 importAnalysis.js
    • 12.7 index.html
    • 12.8 main.js
    • 12.9 render.js
    • 12.10 client.js

1.核心知识 #

1.1 安装依赖 #

npm install connect es-module-lexer resolve check-is-array esbuild fast-glob fs-extra serve-static magic-string chokidar ws  hash-sum --save

1.2 Connect #

  • Connect是一个框架,它使用被称为中间件的模块化组件,以可重用的方式实现 web 程序的逻辑
  • 在 Connect 中,中间件组件是一个函数,它拦截 HTTP 服务器提供的请求和响应,执行逻辑,然后,或者结束响应,或者把它传递给下一个中间件组件
  • Connect 用分配器把中间件连接在一起
  • Express 构建在 Connect 之上的更高层的框架
const connect = require("connect");
const http = require("http");

const middlewares = connect();
middlewares.use(function (req, res, next) {
  console.log("middleware1");
  next();
});
middlewares.use(function (req, res, next) {
  console.log("middleware2");
  next();
});
middlewares.use(function (req, res, next) {
  res.end("Hello from Connect!");
});
http.createServer(middlewares).listen(3000);

1.3 serve-static #

  • serve-static是一个静态文件中中间件
const connect = require("connect");
const static = require("serve-static");
const http = require("http");

const middlewares = connect();
middlewares.use(static(__dirname));
http.createServer(middlewares).listen(3001);

1.4 es-module-lexer #

  • es-module-lexer是一个 JS 模块语法解析器
const { init, parse } = require("es-module-lexer");
(async () => {
  await init;
  const [imports, exports] = parse(`import _ from 'lodash';\nexport var p = 5`);
  console.log(imports);
  console.log(exports);
})();

1.5 resolve #

  • resolve实现了 node 的require.resolve()算法
const resolve = require("resolve");
const res = resolve.sync("check-is-array", { basedir: __dirname });
console.log(res);

1.6 fast-glob #

  • fast-glob该包提供了一些方法,用于遍历文件系统,并根据Unix Bash shell使用的规则返回与指定模式的定义集匹配的路径名
const fg = require("fast-glob");
(async () => {
  const entries = await fg(["**/*.js"]);
  console.log(entries);
})();

1.7 magic-string #

  • magic-string是一个用来操作字符串的库
const MagicString = require("magic-string");
const ms = new MagicString("var age = 10");
ms.overwrite(10, 12, "11");
console.log(ms.toString());

2.实现命令行 #

2.1 package.json #

{
  "bin": {
    "vite3": "./bin/vite3.js"
  }
}

2.2 vite3.js #

bin\vite3.js

#!/usr/bin/env node
require("../lib/cli");

2.3 cli.js #

lib\cli.js

console.log("vite3");

3.实现 http 服务器 #

3.1 cli.js #

lib\cli.js

+let { createServer } = require('./server');
+(async function () {
+  const server = await createServer();
+  server.listen(9999);
+})();

3.2 server\index.js #

lib\server\index.js

const connect = require("connect");
async function createServer() {
  const middlewares = connect();
  const server = {
    async listen(port) {
      require("http")
        .createServer(middlewares)
        .listen(port, async () => {
          console.log(`dev server running at: http://localhost:${port}`);
        });
    },
  };
  return server;
}
exports.createServer = createServer;

4.实现静态文件中间件 #

4.1 server\index.js #

lib\server\index.js

const connect = require('connect');
+const serveStaticMiddleware = require('./middlewares/static');
+const resolveConfig = require('../config');
async function createServer() {
+ const config = await resolveConfig()
  const middlewares = connect();
  const server = {
    async listen(port) {
      require('http').createServer(middlewares)
        .listen(port, async () => {
          console.log(`dev server running at: http://localhost:${port}`)
        })
    }
  }
+ middlewares.use(serveStaticMiddleware(config))
  return server;
}
exports.createServer = createServer;

4.2 static.js #

lib\server\middlewares\static.js

const static = require("serve-static");
function serveStaticMiddleware({ root }) {
  return static(root);
}
module.exports = serveStaticMiddleware;

4.3 config.js #

lib\config.js

const { normalizePath } = require("./utils");
async function resolveConfig() {
  const root = normalizePath(process.cwd());
  let config = {
    root,
  };
  return config;
}
module.exports = resolveConfig;

4.4 utils.js #

lib\utils.js

function normalizePath(id) {
  return id.replace(/\\/g, "/");
}
exports.normalizePath = normalizePath;

4.5 index.html #

viteuse\index.html

<!DOCTYPE html>
<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>vite2</title>
  </head>

  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>

4.6 main.js #

viteuse\main.js

console.log("main");

5.分析第三方依赖 #

5.1 main.js #

src\main.js

+import { createApp } from 'vue'
+console.log(createApp);

5.2 server\index.js #

lib\server\index.js

const connect = require('connect');
const serveStaticMiddleware = require('./middlewares/static');
const resolveConfig = require('../config');
+const { createOptimizeDepsRun } = require('../optimizer');
async function createServer() {
  const config = await resolveConfig()
  const middlewares = connect();
  const server = {
    async listen(port) {
+     await runOptimize(config, server)
      require('http').createServer(middlewares)
        .listen(port, async () => {
          console.log(`dev server running at: http://localhost:${port}`)
        })
    }
  }
  middlewares.use(serveStaticMiddleware(config))
  return server;
}
+async function runOptimize(config, server) {
+  await createOptimizeDepsRun(config)
+}
exports.createServer = createServer;

5.3 optimizer\index.js #

lib\optimizer\index.js

const scanImports = require("./scan");
async function createOptimizeDepsRun(config) {
  const deps = await scanImports(config);
  console.log(deps);
}
exports.createOptimizeDepsRun = createOptimizeDepsRun;

5.4 scan.js #

lib\optimizer\scan.js

const { build } = require("esbuild");
const esbuildScanPlugin = require("./esbuildScanPlugin");
const path = require("path");
async function scanImports(config) {
  const depImports = {};
  const esPlugin = await esbuildScanPlugin(config, depImports);
  await build({
    absWorkingDir: config.root,
    entryPoints: [path.resolve("./index.html")],
    bundle: true,
    format: "esm",
    outfile: "dist/index.js",
    write: true,
    plugins: [esPlugin],
  });
  return depImports;
}
module.exports = scanImports;

5.5 esbuildScanPlugin.js #

lib\optimizer\esbuildScanPlugin.js

const fs = require("fs-extra");
const path = require("path");
const { createPluginContainer } = require("../server/pluginContainer");
const resolvePlugin = require("../plugins/resolve");
const { normalizePath } = require("../utils");
const htmlTypesRE = /\.html$/;
const scriptModuleRE = /<script type="module" src\="(.+?)"><\/script>/;
const JS_TYPES_RE = /\.js$/;
async function esbuildScanPlugin(config, depImports) {
  config.plugins = [resolvePlugin(config)];
  const container = await createPluginContainer(config);
  const resolve = async (id, importer) => {
    return await container.resolveId(id, importer);
  };
  return {
    name: "vite:dep-scan",
    setup(build) {
      build.onResolve({ filter: htmlTypesRE }, async ({ path, importer }) => {
        const resolved = await resolve(path, importer);
        if (resolved) {
          return {
            path: resolved.id || resolved,
            namespace: "html",
          };
        }
      });
      build.onResolve({ filter: /.*/ }, async ({ path, importer }) => {
        const resolved = await resolve(path, importer);
        if (resolved) {
          const id = resolved.id || resolved;
          const included = id.includes("node_modules");
          if (included) {
            depImports[path] = normalizePath(id);
            return {
              path: id,
              external: true,
            };
          }
          return {
            path: id,
          };
        }
        return { path };
      });
      build.onLoad(
        { filter: htmlTypesRE, namespace: "html" },
        async ({ path }) => {
          let html = fs.readFileSync(path, "utf-8");
          let [, scriptSrc] = html.match(scriptModuleRE);
          let js = `import ${JSON.stringify(scriptSrc)};\n`;
          return {
            loader: "js",
            contents: js,
          };
        }
      );
      build.onLoad({ filter: JS_TYPES_RE }, ({ path: id }) => {
        let ext = path.extname(id).slice(1);
        let contents = fs.readFileSync(id, "utf-8");
        return {
          loader: ext,
          contents,
        };
      });
    },
  };
}
module.exports = esbuildScanPlugin;

5.6 pluginContainer.js #

lib\server\pluginContainer.js

const { normalizePath } = require("../utils");
const path = require("path");
async function createPluginContainer({ plugins, root }) {
  class PluginContext {}
  const container = {
    async resolveId(id, importer) {
      let ctx = new PluginContext();
      let resolveId = id;
      for (const plugin of plugins) {
        if (!plugin.resolveId) continue;
        const result = await plugin.resolveId.call(ctx, id, importer);
        if (result) {
          resolveId = result.id || result;
          break;
        }
      }
      return { id: normalizePath(resolveId) };
    },
  };
  return container;
}
exports.createPluginContainer = createPluginContainer;

5.7 resolve.js #

lib\plugins\resolve.js

const fs = require("fs");
const path = require("path");
const resolve = require("resolve");
function resolvePlugin(config) {
  return {
    name: "vite:resolve",
    resolveId(id, importer) {
      //如果/开头表示是绝对路径
      if (id.startsWith("/")) {
        return { id: path.resolve(config.root, id.slice(1)) };
      }
      //如果是绝对路径
      if (path.isAbsolute(id)) {
        return { id };
      }
      //如果是相对路径
      if (id.startsWith(".")) {
        const basedir = path.dirname(importer);
        const fsPath = path.resolve(basedir, id);
        return { id: fsPath };
      }
      //如果是第三方包
      let res = tryNodeResolve(id, importer, config);
      if (res) {
        return res;
      }
    },
  };
}

function tryNodeResolve(id, importer, config) {
  const pkgPath = resolve.sync(`${id}/package.json`, { basedir: config.root });
  const pkgDir = path.dirname(pkgPath);
  const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
  const entryPoint = pkg.module;
  const entryPointPath = path.join(pkgDir, entryPoint);
  return { id: entryPointPath };
}
module.exports = resolvePlugin;

6. 预编译并保存 metadata #

6.1 server\index.js #

lib\server\index.js

const connect = require('connect');
const http = require('http');
const serveStaticMiddleware = require('./middlewares/static');
const resolveConfig = require('../config');
const { createOptimizeDepsRun } = require('../optimizer');
async function createServer() {
  const config = await resolveConfig();
  const middlewares = connect();
  const server = {
    async listen(port) {
+     await runOptimize(config, server)
      http.createServer(middlewares).listen(port, async () => {
        console.log(`server running at http://localhost:${port}`);
      });
    }
  }
  middlewares.use(serveStaticMiddleware(config));
  return server;
}
+async function runOptimize(config, server) {
+  const optimizeDeps = await createOptimizeDepsRun(config);
+  server._optimizeDepsMetadata = optimizeDeps.metadata
}
exports.createServer = createServer;

6.2 config.js #

lib\config.js

+const path = require('path');
const { normalizePath } = require('./utils');
async function resolveConfig() {
  //当前的根目录 window \\  linux /
  const root = normalizePath(process.cwd());
+ const cacheDir = normalizePath(path.resolve(`node_modules/.vite7`))
  let config = {
    root,
+   cacheDir
  }
  return config;
}
module.exports = resolveConfig;

6.3 optimizer\index.js #

lib\optimizer\index.js

const scanImports = require('./scan');
+const fs = require('fs-extra');
+const path = require('path');
+const { build } = require('esbuild');
+const { normalizePath } = require('../utils');
async function createOptimizeDepsRun(config) {
  const deps = await scanImports(config);
+ const { cacheDir } = config;
+ const depsCacheDir = path.resolve(cacheDir, 'deps')
+ const metadataPath = path.join(depsCacheDir, '_metadata.json');
+ const metadata = {
+   optimized: {}
+ }
+ for (const id in deps) {
+   const entry = deps[id]
+   metadata.optimized[id] = {
+     file: normalizePath(path.resolve(depsCacheDir, id + '.js')),
+     src: entry
+   }
+   await build({
+     absWorkingDir: process.cwd(),
+     entryPoints: [deps[id]],
+     outfile: path.resolve(depsCacheDir, id + '.js'),
+     bundle: true,
+     write: true,
+     format: 'esm'
+   })
+ }
+ await fs.ensureDir(depsCacheDir);
+ await fs.writeFile(metadataPath, JSON.stringify(metadata, (key, value) => {
+   if (key === 'file' || key === 'src') {
+     //optimized里存的是绝对路径,此处写入硬盘的是相对于缓存目录的相对路径
+     console.log(depsCacheDir, value);
+     return normalizePath(path.relative(depsCacheDir, value));
+   }
+   return value
+ }, 2));
+ return { metadata };
}
exports.createOptimizeDepsRun = createOptimizeDepsRun;

7.修改导入路径 #

  • 修改返回的 main.js 中的 vue 的路径

    • import { createApp } from 'vue' 变为
    • import { createApp } from '/node_modules/.vite/deps/vue.js'
  • 请求/src/main.js,此请求先由transformMiddleware中间件处理,通过isJSRequest判断是 js 请求,走transformRequest

  • 在transformRequest里执行pluginContainer.resolveId(url)方法,方法内会由resolvePlugin返回/src/main.js的绝对路径
  • 再调用pluginContainer.load(id)方法返回 JS 文件内容,此处返回null
  • 再调用pluginContainer.transform(code, id)方法,执行importAnalysisPlugin里的transform方法,里面会分析依赖的模块,获取依赖的vue,重新执行PluginContext.resolve,执行preAliasPlugin,获取vue相对路径/node_modules/.vite/deps/vue.js,把vue变为/node_modules/.vite/deps/vue.js

7.1 server\index.js #

  • 使用转换请求的transformMiddleware中间件
  • 执行transformRequest
    • pluginContainer.resolveId

lib\server\index.js

const connect = require('connect');
const http = require('http');
const serveStaticMiddleware = require('./middlewares/static');
const resolveConfig = require('../config');
const { createOptimizeDepsRun } = require('../optimizer');
+const transformMiddleware = require('./middlewares/transform');
+const { createPluginContainer } = require('./pluginContainer');
async function createServer() {
  const config = await resolveConfig();
  const middlewares = connect();
+ const pluginContainer = await createPluginContainer(config)
  const server = {
+   pluginContainer,
    async listen(port) {
      await runOptimize(config, server)
      http.createServer(middlewares).listen(port, async () => {
        console.log(`server running at http://localhost:${port}`);
      });
    }
  }
+ for (const plugin of config.plugins) {
+   if (plugin.configureServer) {
+     await plugin.configureServer(server)
+   }
+ }
+ middlewares.use(transformMiddleware(server))
  middlewares.use(serveStaticMiddleware(config));
  return server;
}
async function runOptimize(config, server) {
  const optimizeDeps = await createOptimizeDepsRun(config);
  server._optimizeDepsMetadata = optimizeDeps.metadata
}
exports.createServer = createServer;

7.2 transform.js #

  • 判断如果请求的是 JS 文件的请就进行 JS 内容转换

lib\server\middlewares\transform.js

const { isJSRequest } = require("../../utils");
const send = require("../send");
const transformRequest = require("../transformRequest");
const { parse } = require("url");
function transformMiddleware(server) {
  return async function (req, res, next) {
    if (req.method !== "GET") {
      return next();
    }
    let url = parse(req.url).pathname;
    if (isJSRequest(url)) {
      //切记这个地方要把req.url传给transformRequest,不是url,否则会丢失query
      const result = await transformRequest(req.url, server);
      if (result) {
        const type = "js";
        return send(req, res, result.code, type);
      }
    } else {
      return next();
    }
  };
}
module.exports = transformMiddleware;

7.3 utils.js #

lib\utils.js

function normalizePath(id) {
  return id.replace(/\\/g, '/')
}
exports.normalizePath = normalizePath;

+const knownJsSrcRE = /\.js/
+const isJSRequest = (url) => {
+  if (knownJsSrcRE.test(url)) {
+    return true
+  }
+  return false
+}
+exports.isJSRequest = isJSRequest;

7.4 transformRequest.js #

lib\server\transformRequest.js

const fs = require("fs-extra");
async function transformRequest(url, server) {
  const { pluginContainer } = server;
  const { id } = await pluginContainer.resolveId(url); //获取此文件的绝对路径
  const loadResult = await pluginContainer.load(id); //加载此文件的内容
  let code;
  if (loadResult) {
    code = loadResult.code;
  } else {
    code = await fs.readFile(id, "utf-8");
  }
  //转换文件内容
  const transformResult = await pluginContainer.transform(code, id);
  return transformResult;
}
module.exports = transformRequest;

7.5 pluginContainer.js #

lib\server\pluginContainer.js

const { normalizePath } = require("../utils");
const path = require('path');
async function createPluginContainer({ plugins,root }) {
  class PluginContext {
+   async resolve(id, importer = path.join(root, "index.html")) {
+     return await container.resolveId(id, importer);
+   }
  }
  //插件容器是一个用来执行插件的容器
  const container = {
    //resolve是一个方法,是一个根据标记符计算路径的方法
    //vue=>vue在硬盘上对应路径
    async resolveId(id, importer) {
      let ctx = new PluginContext();
      let resolveId = id;
      for (const plugin of plugins) {
        if (!plugin.resolveId) continue;
        const result = await plugin.resolveId.call(ctx, id, importer);
        if (result) {
          resolveId = result.id || result;
          break;
        }
      }
      return { id: normalizePath(resolveId) }
    },
+   async load(id) {
+     const ctx = new PluginContext()
+     for (const plugin of plugins) {
+       if (!plugin.load) continue
+       const result = await plugin.load.call(ctx, id)
+       if (result !== null) {
+         return result
+       }
+     }
+     return null
+   },
+   async transform(code, id) {
+     for (const plugin of plugins) {
+       if (!plugin.transform) continue
+       const ctx = new PluginContext()
+       const result = await plugin.transform.call(ctx, code, id)
+       if (!result) continue
+       code = result.code || result;
+     }
+     return { code }
+   }
  }
  return container;
}
exports.createPluginContainer = createPluginContainer;

7.6 send.js #

lib\server\send.js

const alias = {
  js: "application/javascript",
  css: "text/css",
  html: "text/html",
  json: "application/json",
};
function send(_req, res, content, type) {
  res.setHeader("Content-Type", alias[type] || type);
  res.statusCode = 200;
  return res.end(content);
}
module.exports = send;

7.7 plugins\index.js #

lib\plugins\index.js

const importAnalysisPlugin = require("./importAnalysis");
const preAliasPlugin = require("./preAlias");
const resolvePlugin = require("./resolve");
async function resolvePlugins(config) {
  return [
    preAliasPlugin(config), //把vue=>vue.js
    resolvePlugin(config),
    importAnalysisPlugin(config),
  ];
}
exports.resolvePlugins = resolvePlugins;

7.8 importAnalysis.js #

  • 导入文件分析 lib\plugins\importAnalysis.js
const { init, parse } = require("es-module-lexer");
const MagicString = require("magic-string");
function importAnalysisPlugin(config) {
  const { root } = config;
  return {
    name: "vite:import-analysis",
    async transform(source, importer) {
      await init;
      let imports = parse(source)[0];
      if (!imports.length) {
        return source;
      }
      let ms = new MagicString(source);
      const normalizeUrl = async (url) => {
        //解析此导入的模块的路径
        const resolved = await this.resolve(url, importer);
        if (resolved.id.startsWith(root + "/")) {
          //把绝对路径变成相对路径
          url = resolved.id.slice(root.length);
        }
        return url;
      };
      for (let index = 0; index < imports.length; index++) {
        const { s: start, e: end, n: specifier } = imports[index];
        if (specifier) {
          const normalizedUrl = await normalizeUrl(specifier);
          if (normalizedUrl !== specifier) {
            ms.overwrite(start, end, normalizedUrl);
          }
        }
      }
      return ms.toString();
    },
  };
}
module.exports = importAnalysisPlugin;

7.9 preAlias.js #

  • 看看是否是经过预构建的路径,如果是直接取预构建后的路径

lib\plugins\preAlias.js

function preAliasPlugin() {
  let server;
  return {
    name: "vite:pre-alias",
    configureServer(_server) {
      server = _server;
    },
    resolveId(id) {
      //把vue=>vue.js
      const metadata = server._optimizeDepsMetadata;
      const isOptimized = metadata.optimized[id];
      if (isOptimized) {
        return {
          id: isOptimized.file, //// vue => c:/node_modules/.vite/deps/vue.js
        };
      }
    },
  };
}
module.exports = preAliasPlugin;

7.10 config.js #

lib\config.js

const path = require('path');
const { normalizePath } = require('./utils');
+const { resolvePlugins } = require('./plugins');
async function resolveConfig() {
  const root = normalizePath(process.cwd());
  const cacheDir = normalizePath(path.resolve(`node_modules/.vite`))
  let config = {
    root,
    cacheDir
  }
+ const plugins = await resolvePlugins(config);
+ config.plugins = plugins;
  return config;
}
module.exports = resolveConfig;

8.支持 vue 插件 #

8.1 esbuildDepPlugin.js #

lib\optimizer\esbuildDepPlugin.js

const path = require('path');
const fs = require('fs-extra');
const htmlTypesRE = /\.html$/;
const scriptModuleRE = /<script type="module" src\="(.+?)"><\/script>/;
const { createPluginContainer } = require('../server/pluginContainer');
const resolvePlugin = require('../plugins/resolve');
const jsRE = /\.js$/;
async function esBuildScanPlugin(config, deps) {
  //在此处其实调用的vite插件系统
  config.plugins = [resolvePlugin(config)];
  const container = await createPluginContainer(config);
  const resolve = async (id, importer) => {
    return await container.resolveId(id, importer);
  }
  return {
    name: 'vite:dep-scan',
    setup(build) {
      //X [ERROR] No loader is configured for ".vue" files: src/App.vue
+     build.onResolve(
+       {
+         filter: /\.vue$/
+       },
+       async ({ path: id, importer }) => {
+         const resolved = await resolve(id, importer)
+         if (resolved) {
+           return {
+             path: resolved.id,
+             external: true
+           }
+         }
+       }
+     )
      //用来处理路径的
      build.onResolve({ filter: htmlTypesRE }, async ({ path, importer }) => {
        //path=C:\aproject\vite5\doc\index.html importer 空
        const resolved = await resolve(path, importer);
        if (resolved) {
          return {
            path: resolved.id || resolved,
            namespace: 'html' //为了更细化区分不同的文件类型,我可以给文件添加一个命名空间
          }
        }
      });
      //对于其它所有的类型文件我们也进行处理
      build.onResolve({ filter: /.*/ }, async ({ path, importer }) => {
        const resolved = await resolve(path, importer);
        //返回值可能是 {id:xx} 或 xx
        //C:\aproject\vite5\doc\main.js
        if (resolved) {
          const id = resolved.id || resolved;
          const included = id.includes('node_modules');
          if (included) {
            //deps.vue = "C:/aproject/viteproject/node_modules/vue/dist/vue.runtime.esm-bundler.js"
            deps[path] = id;
            return {
              path,
              external: true //external设置为true的话说明这是一个外部模块,不会进行后续的打包分析,直接返回了
            }
          }
          return { path: id }
        }
        return { path }
      });
      //用来处理读取内容 自定义读取器
      build.onLoad({ filter: htmlTypesRE, namespace: 'html' }, async ({ path }) => {
        let html = fs.readFileSync(path, 'utf8');
        let [, scriptSrc] = html.match(scriptModuleRE);
        let js = `import ${JSON.stringify(scriptSrc)}`;//import "/main.js"
        return {
          loader: 'js',
          contents: js
        }
      })
      build.onLoad({ filter: jsRE }, async ({ path: id }) => {
        let ext = path.extname(id).slice(1);// .js  js
        const contents = fs.readFileSync(id, 'utf8');
        return {
          loader: ext,
          contents
        }
      })
    }
  }
}
module.exports = esBuildScanPlugin;

8.2 plugins\index.js #

lib\plugins\index.js

const importAnalysisPlugin = require('./importAnalysis');
const preAliasPlugin = require('./preAlias');
const resolvePlugin = require('./resolve');
+async function resolvePlugins(config, userPlugins) {
  return [
    preAliasPlugin(config),
    resolvePlugin(config),
+   ...userPlugins,
    importAnalysisPlugin(config)
  ]
}
exports.resolvePlugins = resolvePlugins;

8.3 utils.js #

lib\utils.js

function normalizePath(id) {
  return id.replace(/\\/g, '/')
}
exports.normalizePath = normalizePath;
+const knownJsSrcRE = /\.(js|vue)/
const isJSRequest = (url) => {
  if (knownJsSrcRE.test(url)) {
    return true
  }
  return false
}
exports.isJSRequest = isJSRequest;

8.4 config.js #

lib\config.js

const path = require('path');
const { normalizePath } = require('./utils');
const { resolvePlugins } = require('./plugins');
+const fs = require('fs-extra');
async function resolveConfig() {
  //当前的根目录 window \\  linux /
  const root = normalizePath(process.cwd());
  const cacheDir = normalizePath(path.resolve(`node_modules/.vite7`))
  let config = {
    root,
    cacheDir
  }
+ const jsconfigFile = path.resolve(root, 'vite.config.js')
+ const exists = await fs.pathExists(jsconfigFile)
+ if (exists) {
+   const userConfig = require(jsconfigFile);
+   config = { ...config, ...userConfig };
+ }
+ const userPlugins = config.plugins || [];
+ const plugins = await resolvePlugins(config, userPlugins);
  config.plugins = plugins;
  return config;
}
module.exports = resolveConfig;

8.5 index.html #

index.html

<!DOCTYPE html>
<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>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>

8.6 main.js #

src\main.js

import { createApp } from "vue";
import App from "/src/App.vue";
createApp(App).mount("#app");

8.7 App.vue #

src\App.vue

<template>
  <h1>App</h1>
</template>
<script>
export default {
  name: 'App'
}
</script>

8.8 vue.js #

plugins\vue.js

const {
  parse,
  compileScript,
  rewriteDefault,
  compileTemplate,
} = require("vue/compiler-sfc");
const fs = require("fs");
const descriptorCache = new Map();
function vue() {
  return {
    name: "vue",
    async transform(code, id) {
      const { filename } = parseVueRequest(id);
      if (filename.endsWith(".vue")) {
        let result = await transformMain(code, filename);
        return result;
      }
      return null;
    },
  };
}

async function getDescriptor(filename) {
  let descriptor = descriptorCache.get(filename);
  if (descriptor) return descriptor;
  const content = await fs.promises.readFile(filename, "utf8");
  const result = parse(content, { filename });
  descriptor = result.descriptor;
  descriptorCache.set(filename, descriptor);
  return descriptor;
}
async function transformMain(source, filename) {
  const descriptor = await getDescriptor(filename);
  const scriptCode = genScriptCode(descriptor, filename);
  const templateCode = genTemplateCode(descriptor, filename);
  let resolvedCode = [
    templateCode,
    scriptCode,
    `_sfc_main['render'] = render`,
    `export default _sfc_main`,
  ].join("\n");
  return { code: resolvedCode };
}

function genScriptCode(descriptor, id) {
  let scriptCode = "";
  let script = compileScript(descriptor, { id });
  if (!script.lang) {
    scriptCode = rewriteDefault(script.content, "_sfc_main");
  }
  return scriptCode;
}
function genTemplateCode(descriptor, id) {
  let content = descriptor.template.content;
  const result = compileTemplate({ source: content, id });
  return result.code;
}
function parseVueRequest(id) {
  const [filename, querystring = ""] = id.split("?");
  let query = new URLSearchParams(querystring);
  return {
    filename,
    query,
  };
}
module.exports = vue;

8.9 vite.config.js #

vite.config.js

const vue = require("./plugins/vue");
module.exports = {
  plugins: [vue()],
};

9.支持 style #

9.1 config.js #

lib\config.js

const path = require('path');
const { normalizePath } = require('./utils');
const { resolvePlugins } = require('./plugins');
const fs = require('fs-extra');
async function resolveConfig() {
  //当前的根目录 window \\  linux /
  const root = normalizePath(process.cwd());
  const cacheDir = normalizePath(path.resolve(`node_modules/.vite7`))
  let config = {
    root,
    cacheDir
  }
  const jsconfigFile = path.resolve(root, 'vite.config.js')
  const exists = await fs.pathExists(jsconfigFile)
  if (exists) {
    const userConfig = require(jsconfigFile);
    config = { ...config, ...userConfig };
  }
  const userPlugins = config.plugins || [];
+ for (const plugin of userPlugins) {
+   if (plugin.config) {
+     const res = await plugin.config(config)
+     if (res) {
+       config = { ...config, ...res }
+     }
+   }
+ }
  const plugins = await resolvePlugins(config, userPlugins);
  config.plugins = plugins;
  return config;
}
module.exports = resolveConfig;

9.2 App.vue #

src\App.vue

<template>
  <h1>App</h1>
</template>
<script>
export default {
  name: 'App'
}
</script>
+<style>
+h1 {
+  color: red;
+}
+</style>

9.3 vue.js #

plugins\vue.js

npm install hash-sum --save
+const { parse, compileScript, rewriteDefault, compileTemplate, compileStyleAsync } = require('vue/compiler-sfc');
const fs = require('fs');
+const path = require('path');
+const hash = require('hash-sum');
+const descriptorCache = new Map();
function vue() {
+ let root;
  return {
    name: 'vue',
+   config(config) {
+     root = config.root;
+   },
+   async load(id) {
+     const { filename, query } = parseVueRequest(id);
+     if (query.has('vue')) {
+       const descriptor = await getDescriptor(filename, root);
+       if (query.get('type') === 'style') {
+         let block = descriptor.styles[Number(query.get('index'))];
+         if (block) {
+           return { code: block.content };
+         }
+       }
+     }
+   },
    async transform(code, id) {
+     const { filename, query } = parseVueRequest(id);
+     if (filename.endsWith('.vue')) {
+       if (query.get('type') === 'style') {
+         const descriptor = await getDescriptor(filename, root);
+         let result = await transformStyle(code, descriptor, query.get('index'));
+         return result;
+       } else {
+         let result = await transformMain(code, filename,root);
+         return result;
+       }
+     }
      return null;
    }
  }
}
+async function transformStyle(code, descriptor, index) {
+  const block = descriptor.styles[index];
+  //如果是CSS,其实翻译之后和翻译之前内容是一样的
+  const result = await compileStyleAsync({
+    filename: descriptor.filename,
+    source: code,
+    id: `data-v-${descriptor.id}`,//必须传递,不然报错
+    scoped: block.scoped
+  });
+  let styleCode = result.code;
+  const injectCode =
+    `\nvar  style = document.createElement('style');` +
+    `\nstyle.innerHTML = ${JSON.stringify(styleCode)};` +
+    `\ndocument.head.appendChild(style);`
+  return {
+    code: injectCode
+  };
+}
+async function getDescriptor(filename, root) {
+  let descriptor = descriptorCache.get(filename);
+  if (descriptor) return descriptor;
+  const content = await fs.promises.readFile(filename, 'utf8');
+  const result = parse(content, { filename });
+  descriptor = result.descriptor;
+  descriptor.id = hash(path.relative(root, filename));
+  descriptorCache.set(filename, descriptor);
+  return descriptor;
+}
async function transformMain(source, filename,root) {
  const descriptor = await getDescriptor(filename, root);
  const scriptCode = genScriptCode(descriptor, filename)
  const templateCode = genTemplateCode(descriptor, filename);
+ const stylesCode = genStyleCode(descriptor, filename);
  let resolvedCode = [
+   stylesCode,
    templateCode,
    scriptCode,
    `_sfc_main['render'] = render`,
    `export default _sfc_main`
  ].join('\n');
  return { code: resolvedCode }
}
+function genStyleCode(descriptor, filename) {
+  let styleCode = '';
+  if (descriptor.styles.length) {
+    descriptor.styles.forEach((style, index) => {
+      const query = `?vue&type=style&index=${index}&lang=css`;
+      const styleRequest = (filename + query).replace(/\\/g, '/');
+      styleCode += `\nimport ${JSON.stringify(styleRequest)}`;
+    });
+    return styleCode;
+  }
+}
function genScriptCode(descriptor, id) {
  let scriptCode = ''
  let script = compileScript(descriptor, { id });
  if (!script.lang) {
    scriptCode = rewriteDefault(
      script.content,
      '_sfc_main',
    )
  }
  return scriptCode;
}
function genTemplateCode(descriptor, id) {
  let content = descriptor.template.content;
  const result = compileTemplate({ source: content, id });
  return result.code;
}
+function parseVueRequest(id) {
+  const [filename, querystring = ''] = id.split('?');
+  let query = new URLSearchParams(querystring);
+  return {
+    filename, query
+  };
+}
module.exports = vue;

10.支持环境变量 #

10.1 plugins\index.js #

lib\plugins\index.js

const importAnalysisPlugin = require('./importAnalysis');
const preAliasPlugin = require('./preAlias');
const resolvePlugin = require('./resolve');
+const definePlugin = require('./define');
async function resolvePlugins(config, userPlugins) {
  return [
    preAliasPlugin(config),
    resolvePlugin(config),
    ...userPlugins,
+   definePlugin(config),
    importAnalysisPlugin(config)
  ]
}
exports.resolvePlugins = resolvePlugins;

10.2 define.js #

lib\plugins\define.js

const MagicString = require("magic-string");
function definePlugin(config) {
  return {
    name: "vite:define",
    transform(code) {
      const replacements = config.define || {};
      const replacementsKeys = Object.keys(replacements);
      const pattern = new RegExp(
        "(" + replacementsKeys.map((str) => str).join("|") + ")",
        "g"
      );
      const s = new MagicString(code);
      let hasReplaced = false;
      let match;
      while ((match = pattern.exec(code))) {
        hasReplaced = true;
        const start = match.index;
        const end = start + match[0].length;
        const replacement = "" + replacements[match[1]];
        s.overwrite(start, end, replacement);
      }
      if (!hasReplaced) {
        return null;
      }
      return { code: s.toString() };
    },
  };
}
module.exports = definePlugin;

10.3 plugins\vue.js #

plugins\vue.js

const { parse, compileScript, rewriteDefault, compileTemplate, compileStyleAsync } = require('vue/compiler-sfc');
const fs = require('fs');
const path = require('path');
const hash = require('hash-sum');
const descriptorCache = new Map();
function vue() {
  let root;
  return {
    name: 'vue',
    config(config) {
      root = config.root;
+     return {
+       define: {
+         __VUE_OPTIONS_API__: true,
+         __VUE_PROD_DEVTOOLS__: false
+       }
+     }
    },
    async load(id) {
      const { filename, query } = parseVueRequest(id);
      if (query.has('vue')) {
        const descriptor = await getDescriptor(filename, root);
        if (query.get('type') === 'style') {
          let block = descriptor.styles[Number(query.get('index'))];
          if (block) {
            return { code: block.content };
          }
        }
      }
    },
    async transform(code, id) {
      const { filename, query } = parseVueRequest(id);
      if (filename.endsWith('.vue')) {
        if (query.get('type') === 'style') {
          const descriptor = await getDescriptor(filename, root);
          let result = await transformStyle(code, descriptor, query.get('index'));
          return result;
        } else {
          let result = await transformMain(code, filename,root);
          return result;
        }
      }
      return null;
    }
  }
}
async function transformStyle(code, descriptor, index) {
  const block = descriptor.styles[index];
  //如果是CSS,其实翻译之后和翻译之前内容是一样的,最终返回的JS靠packages\vite\src\node\plugins\css.ts
  const result = await compileStyleAsync({
    filename: descriptor.filename,
    source: code,
    id: `data-v-${descriptor.id}`,//必须传递,不然报错
    scoped: block.scoped
  });
  let styleCode = result.code;
  const injectCode =
    `\nvar  style = document.createElement('style');` +
    `\nstyle.innerHTML = ${JSON.stringify(styleCode)};` +
    `\ndocument.head.appendChild(style);`
  return {
    code: injectCode
  };
}
async function getDescriptor(filename, root) {
  let descriptor = descriptorCache.get(filename);
  if (descriptor) return descriptor;
  const content = await fs.promises.readFile(filename, 'utf8');
  const result = parse(content, { filename });
  descriptor = result.descriptor;
  descriptor.id = hash(path.relative(root, filename));
  descriptorCache.set(filename, descriptor);
  return descriptor;
}
async function transformMain(source, filename,root) {
  const { descriptor } = parse(source, { filename });
  const scriptCode = genScriptCode(descriptor, filename)
  const templateCode = genTemplateCode(descriptor, filename);
  const stylesCode = genStyleCode(descriptor, filename);
  let resolvedCode = [
    stylesCode,
    templateCode,
    scriptCode,
    `_sfc_main['render'] = render`,
    `export default _sfc_main`
  ].join('\n');
  return { code: resolvedCode }
}
function genStyleCode(descriptor, filename) {
  let styleCode = '';
  if (descriptor.styles.length) {
    descriptor.styles.forEach((style, index) => {
      const query = `?vue&type=style&index=${index}&lang=css`;
      const styleRequest = (filename + query).replace(/\\/g, '/');
      styleCode += `\nimport ${JSON.stringify(styleRequest)}`;
    });
    return styleCode;
  }
}
function genScriptCode(descriptor, id) {
  let scriptCode = ''
  let script = compileScript(descriptor, { id });
  if (!script.lang) {
    scriptCode = rewriteDefault(
      script.content,
      '_sfc_main',
    )
  }
  return scriptCode;
}
function genTemplateCode(descriptor, id) {
  let content = descriptor.template.content;
  const result = compileTemplate({ source: content, id });
  return result.code;
}
function parseVueRequest(id) {
  const [filename, querystring = ''] = id.split('?');
  let query = new URLSearchParams(querystring);
  return {
    filename, query
  };
}
module.exports = vue;

11.HMR #

  • HMR

11.1.创建项目 #

11.1.1 安装 #

pnpm install vite -D

11.1.2.package.json #

{
  "scripts": {
    "dev": "vite"
  }
}

11.1.3.src\main.js #

src\main.js

export function render() {
  app.innerHTML = 'title';
}
render();
if (import.meta.hot) {
  import.meta.hot.accept((newModule) => {
    newModule.render();
  });
}

11.1.4.index.html #

index.html

<!DOCTYPE html>
<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>hmr</title>
</head>

<body>
  <div id="app"></div>
  <script type="module" src="/src/main.js"></script>
</body>

</html>

11.2.封装模块 #

11.2.1 src\render.js #

src\render.js

export function render() {
  app.innerHTML = 'title1';
}

12.2.2 src\main.js #

src\main.js

import { render } from './render';
render();
if (import.meta.hot) {
  import.meta.hot.accept(['./render'], ([renderMod]) => {
    renderMod.render();
  });
}

11.3.销毁副作用 #

11.3.1 render.js #

src\render.js

+let counter = { number: 0 };
+let timer = setInterval(() => {
+  console.log(counter.number++);
+}, 1000);

export function render() {
  app.innerHTML = 'title';
}

+if (import.meta.hot) {
+  import.meta.hot.dispose(() => {
+    console.log('dispose render.js');
+    clearInterval(timer);
+  });
+}

11.4.保留状态 #

11.4.1 render.js #

src\render.js

+let counter = import.meta.hot.data.counter || { number: 0 };
let timer;

export function render() {
  timer = setInterval(() => {
    app.innerHTML = counter.number++;
  }, 1000);
}

if (import.meta.hot) {
//每个模块有一个data属性,保存热更新前的状态  
+ import.meta.hot.data.counter = counter;
  import.meta.hot.dispose(() => {
    console.log('dispose render.js');
    clearInterval(timer);
  });
}

11.5.拒绝更新 #

11.5.1 render.js #

src\render.js

+export let counter = import.meta.hot.data.counter || { number: 0 };
let timer;

export function render() {
  timer = setInterval(() => {
    app.innerHTML = counter.number++;
  }, 1000);
}

if (import.meta.hot) {
  import.meta.hot.data.counter = counter;
  import.meta.hot.dispose(() => {
    console.log('dispose render.js');
    clearInterval(timer);
  });
}

11.5.2 src\main.js #

src\main.js

import { render } from './render';
render();
if (import.meta.hot) {
  import.meta.hot.accept(['./render'], ([renderMod]) => {
+    if (renderMod.counter.number < 10) {
+      renderMod.render();
+    } else {
+      //强制刷新
+      import.meta.hot.invalidate();
    }
  });
+  //import.meta.hot.accept();
+  import.meta.hot.decline();
}

12.支持 HMR #

12.1 server\index.js #

lib\server\index.js

const connect = require('connect');
const http = require('http');
const serveStaticMiddleware = require('./middlewares/static');
const resolveConfig = require('../config');
const { createOptimizeDepsRun } = require('../optimizer');
const transformMiddleware = require('./middlewares/transform');
const { createPluginContainer } = require('./pluginContainer');
+const { handleHMRUpdate } = require('./hmr');
+const { createWebSocketServer } = require('./ws');
+const { normalizePath } = require('../utils');
+const chokidar = require('chokidar');
+const { ModuleGraph } = require('./moduleGraph')
+const path = require('path');
async function createServer() {
  const config = await resolveConfig();
  const middlewares = connect();
+ const httpServer = require('http').createServer(middlewares)
+ const ws = createWebSocketServer(httpServer, config)
+ const watcher = chokidar.watch(path.resolve(config.root), {
+   ignored: [
+     '**/node_modules/**',
+     '**/.git/**'
+   ]
+ });
+ const moduleGraph = new ModuleGraph((url) =>
+   pluginContainer.resolveId(url)
+ )
+ const pluginContainer = await createPluginContainer(config)
  const server = {
+   config,
+   ws,
+   watcher,
+   moduleGraph,
+   httpServer,
    pluginContainer,
    async listen(port) {
      await runOptimize(config, server)
+     httpServer.listen(port, async () => {
        console.log(`server running at http://localhost:${port}`);
      });
    }
  }
+ watcher.on('change', async (file) => {
+   file = normalizePath(file)
+   await handleHMRUpdate(file, server)
+ })
  for (const plugin of config.plugins) {
    if (plugin.configureServer) {
      await plugin.configureServer(server)
    }
  }
  middlewares.use(transformMiddleware(server))
  middlewares.use(serveStaticMiddleware(config));
  return server;
}
async function runOptimize(config, server) {
  const optimizeDeps = await createOptimizeDepsRun(config);
  server._optimizeDepsMetadata = optimizeDeps.metadata
}
exports.createServer = createServer;

12.2 ws.js #

lib\server\ws.js

const { WebSocketServer } = require("ws");
const HMR_HEADER = "vite-hmr";
function createWebSocketServer(httpServer) {
  const wss = new WebSocketServer({ noServer: true });
  httpServer.on("upgrade", (req, socket, head) => {
    if (req.headers["sec-websocket-protocol"] === HMR_HEADER) {
      wss.handleUpgrade(req, socket, head, (ws) => {
        wss.emit("connection", ws, req);
      });
    }
  });
  wss.on("connection", (socket) => {
    socket.send(JSON.stringify({ type: "connected" }));
  });
  return {
    on: wss.on.bind(wss),
    off: wss.off.bind(wss),
    send(payload) {
      const stringified = JSON.stringify(payload);
      wss.clients.forEach((client) => {
        if (client.readyState === 1) {
          client.send(stringified);
        }
      });
    },
  };
}
exports.createWebSocketServer = createWebSocketServer;

12.3 hmr.js #

lib\server\hmr.js

const LexerState = {
  inCall: 0,
  inQuoteString: 1,
};

async function handleHMRUpdate(file, server) {
  const { moduleGraph, ws } = server;
  //根据文件获取模块
  const module = moduleGraph.getModuleById(file);
  if (module) {
    const updates = [];
    const boundaries = new Set();
    propagateUpdate(module, boundaries);
    updates.push(
      ...[...boundaries].map(({ boundary, acceptedVia }) => ({
        type: `${boundary.type}-update`,
        path: boundary.url,
        acceptedPath: acceptedVia.url,
      }))
    );
    ws.send({
      type: "update",
      updates,
    });
  }
}

function updateModules(file, modules, { ws }) {}
function propagateUpdate(node, boundaries) {
  if (!node.importers.size) {
    return true;
  }
  for (const importer of node.importers) {
    if (importer.acceptedHmrDeps.has(node)) {
      boundaries.add({
        boundary: importer,
        acceptedVia: node,
      });
      continue;
    }
  }
  return false;
}
function lexAcceptedHmrDeps(code, start, urls) {
  let state = LexerState.inCall;
  let prevState = LexerState.inCall;
  let currentDep = "";
  function addDep(index) {
    urls.add({
      url: currentDep,
      start: index - currentDep.length - 1,
      end: index + 1,
    });
    currentDep = "";
  }
  for (let i = start; i < code.length; i++) {
    const char = code.charAt(i);
    switch (state) {
      case LexerState.inCall:
        if (char === `'` || char === `"`) {
          prevState = state;
          state = LexerState.inQuoteString;
        }
        break;
      case LexerState.inQuoteString:
        if (char === `'` || char === `"`) {
          addDep(i);
          return false;
        } else {
          currentDep += char;
        }
        break;
      default:
        break;
    }
  }
  return false;
}
exports.handleHMRUpdate = handleHMRUpdate;
exports.updateModules = updateModules;
exports.lexAcceptedHmrDeps = lexAcceptedHmrDeps;

12.4 transformRequest.js #

lib\server\transformRequest.js

const fs = require('fs-extra');
+const { parse } = require("url");
async function transformRequest(url, server) {
  const { pluginContainer } = server
  const { id } = await pluginContainer.resolveId(url);
  const loadResult = await pluginContainer.load(id)
  let code;
  if (loadResult) {
    code = loadResult.code;;
  } else {
+   let fsPath = parse(id).pathname;
+   code = await fs.readFile(fsPath, 'utf-8')
  }
+ await server.moduleGraph.ensureEntryFromUrl(url)
  const transformResult = await pluginContainer.transform(code, id)
  return transformResult;
}
module.exports = transformRequest;

12.5 moduleGraph.js #

lib\server\moduleGraph.js

class ModuleNode {
  //哪些模块导入了自己
  importers = new Set();
  //接收哪些子模块的修改
  acceptedHmrDeps = new Set();
  constructor(url) {
    this.url = url;
    this.type = "js";
  }
}
class ModuleGraph {
  constructor(resolveId) {
    this.resolveId = resolveId;
  }
  idToModuleMap = new Map();
  //通过ID查找模块
  getModuleById(id) {
    return this.idToModuleMap.get(id);
  }
  //把原始的URL添加到Map
  async ensureEntryFromUrl(rawUrl) {
    const [url, resolvedId] = await this.resolveUrl(rawUrl);
    let mod = this.idToModuleMap.get(resolvedId); //通过文件URL查找模块
    if (!mod) {
      this.idToModuleMap.set(resolvedId, new ModuleNode(url)); //把绝对路径和模块的对应关系保存在idToModuleMap中
    }
    return mod;
  }
  async resolveUrl(url) {
    const resolved = await this.resolveId(url);
    const resolvedId = resolved.id || url;
    return [url, resolvedId];
  }
  async updateModuleInfo(mod, importedModules, acceptedModules) {
    for (const imported of importedModules) {
      const dep = await this.ensureEntryFromUrl(imported);
      dep.importers.add(mod); //render.js importerts main.js
    }
    const deps = (mod.acceptedHmrDeps = new Set()); //main.js acceptedHmrDeps render.js
    for (const accepted of acceptedModules) {
      const dep = await this.ensureEntryFromUrl(accepted);
      deps.add(dep);
    }
  }
}
exports.ModuleGraph = ModuleGraph;

12.6 importAnalysis.js #

lib\plugins\importAnalysis.js

const { init, parse } = require('es-module-lexer')
const MagicString = require('magic-string');
+const { lexAcceptedHmrDeps } = require('../server/hmr');
+const path = require('path');
function importAnalysisPlugin(config) {
  const { root } = config
+ let server
  return {
    name: 'vite:import-analysis',
+   configureServer(_server) {
+     server = _server
+   },
    async transform(source, importer) {
      await init
      let imports = parse(source)[0]
      if (!imports.length) {
        return source
      }
+     const { moduleGraph } = server
+     const importerModule = moduleGraph.getModuleById(importer)
+     const importedUrls = new Set()
+     const acceptedUrls = new Set()
      let ms = new MagicString(source);
      const normalizeUrl = async (url) => {
        const resolved = await this.resolve(url, importer)
        if (resolved.id.startsWith(root + '/')) {
          url = resolved.id.slice(root.length)
        }
+       await moduleGraph.ensureEntryFromUrl(url)
        return url;
      }
      for (let index = 0; index < imports.length; index++) {
        const { s: start, e: end, n: specifier } = imports[index]
+       const rawUrl = source.slice(start, end)
+       if (rawUrl === 'import.meta') {
+         const prop = source.slice(end, end + 4)
+         if (prop === '.hot') {
+           if (source.slice(end + 4, end + 11) === '.accept') {
+             lexAcceptedHmrDeps(source, source.indexOf('(', end + 11) + 1, acceptedUrls)
+           }
+         }
+       }
        if (specifier) {
          const normalizedUrl = await normalizeUrl(specifier)
          if (normalizedUrl !== specifier) {
            ms.overwrite(start, end, normalizedUrl)
          }
+         importedUrls.add(normalizedUrl)
        }
      }
+     const normalizedAcceptedUrls = new Set()
+     const toAbsoluteUrl = (url) =>
+       path.posix.resolve(path.posix.dirname(importerModule.url), url)
+     for (const { url, start, end } of acceptedUrls) {
+       const [normalized] = await moduleGraph.resolveUrl(toAbsoluteUrl(url),)
+       normalizedAcceptedUrls.add(normalized)
+       ms.overwrite(start, end, JSON.stringify(normalized))
+     }
+     await moduleGraph.updateModuleInfo(
+       importerModule,
+       importedUrls,
+       normalizedAcceptedUrls
+     )
      return ms.toString()
    }
  }
}
module.exports = importAnalysisPlugin;

12.7 index.html #

index.html

<!DOCTYPE html>
<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>
    <script type="module" src="/src/main.js"></script>
    <script type="module" src="/src/client.js"></script>
  </body>
</html>

12.8 main.js #

src\main.js

import { render } from "./render.js";
render();
window.hotModulesMap = new Map();
var ownerPath = "/src/main.js";
import.meta.hot = {
  accept(deps, callback) {
    acceptDeps(deps, callback);
  },
};
function acceptDeps(deps, callback) {
  const mod = hotModulesMap.get(ownerPath) || {
    id: ownerPath,
    callbacks: [],
  };
  mod.callbacks.push({
    deps,
    fn: callback,
  });
  hotModulesMap.set(ownerPath, mod);
}
if (import.meta.hot) {
  import.meta.hot.accept(["./render.js"], ([renderMod]) => {
    renderMod.render();
  });
}

12.9 render.js #

src\render.js

export function render() {
  app.innerHTML = "title1";
}

12.10 client.js #

src\client.js

console.log("[vite] connecting...");
var socket = new WebSocket(`ws://${window.location.host}`, "vite-hmr");
socket.addEventListener("message", async ({ data }) => {
  handleMessage(JSON.parse(data));
});
async function handleMessage(payload) {
  switch (payload.type) {
    case "connected":
      console.log(`[vite] connected.`);
      break;
    case "update":
      payload.updates.forEach((update) => {
        if (update.type === "js-update") {
          fetchUpdate(update);
        }
      });
      break;
    case "full-reload":
      location.reload();
    default:
      break;
  }
}

async function fetchUpdate({ path, acceptedPath }) {
  const mod = window.hotModulesMap.get(path);
  if (!mod) {
    return;
  }
  const moduleMap = new Map();
  const modulesToUpdate = new Set();
  for (const { deps } of mod.callbacks) {
    deps.forEach((dep) => {
      if (acceptedPath === dep) {
        modulesToUpdate.add(dep);
      }
    });
  }
  await Promise.all(
    Array.from(modulesToUpdate).map(async (dep) => {
      const newMod = await import(dep + "?ts=" + Date.now());
      moduleMap.set(dep, newMod);
    })
  );
  for (const { deps, fn } of mod.callbacks) {
    fn(deps.map((dep) => moduleMap.get(dep)));
  }
  const loggedPath = `${acceptedPath} via ${path}`;
  console.log(`[vite] hot updated: ${loggedPath}`);
}

访问验证

请输入访问令牌

Token不正确,请重新输入