使用100 行代码 写一个支持 vue 和 react 的koa脚手架

445 阅读7分钟

用 100 行代码,用 koa 写一个支持 vue 和 react 的赛博脚手架 😼

如何有效的保证不挂科,当然是背知识点啦;😏 同理,如何去理解脚手架呢,我的理解是以最低限度的可用性,去搭建一个脚手架,让我们放弃那些反复的钩子,plugin,以及一些其他的配置。我会在一个中间件中,来完成最低限度的解析代码的工作,希望能帮助大家去理解脚手架,此项目参考于 mini-vite 和 vite-plugin-vue;

我们 vite 也有自己的垃圾佬

这个项目是我在写 miniVite 的时候产生的想法,所以用的是同一个库,认准npm run cyber,就好了;

这个是仓库地址,觉得有用的话给个赞吧

Readme Card

这里是目录结构,以下是和当前项目相关的,专注于这些文件就好

|-- koaMinivite,
      |-- .gitignore,
      |-- cyber.html,
      |-- ...,
      |-- src,
          |-- cyber.ts,
          |-- cyber,
          |   |-- a.ts,
          |   |-- App.vue,
          |   |-- help.js,
          |   |-- react.tsx,
          |   |-- vue.ts,
          |-- ...,
          |-- node,
              |-- ...,
              |-- optimizer,
              |   |-- index.ts,
              |   |-- preBundlePlugin.ts,
              |   |-- scanplugin.ts,


一、koa 搭建

首先来看我们的入口文件cyber.ts;这个文件是核心文件,我们来看看他做了什么;

import Koa from "koa";
import { optimize } from "./node/optimizer";
const app = new Koa();
app.use(async (ctx, next) => {
  //中间件区域
  return next();
});
app.listen(8954, async () => {
  await optimize(root, "src/cyber/react.tsx");
  await optimize(root, "src/cyber/vue.ts");
  console.log("http://localhost:8954 ");
});

二、预构建

我们看到app.listen开启服务的时候为什么会在回调中处理一个 optimize 的函数呢; 我们对代码会进行一个预构建;详细的解释请移步 从 koa 到 mini-vite ; 我现在对其进行一个通俗的解释 这里是 react 包的 index.js

"use strict";
if (process.env.NODE_ENV === "production") {
  module.exports = require("./cjs/react.production.min.js");
} else {
  module.exports = require("./cjs/react.development.js");
}

那么你有没有发现一个问题;我们常使用的引入方法是`import React from "react";`

这是两种模块类型,一个是esm,一个是cjs;就像天堂的白鸽不会亲吻田野的乌鸦,es模块也不会引入cjs模块; 你拿前朝的🗡,怎么能斩本朝的官

那我们要解决以下几个问题

  • 怎么让各个包的模块格式都转化为 esm 模块
  • node_modules 包里的文件这么多,难道我们要全部代理成静态资源服务器吗
  • 如何让外部资源包和相对路径文件进行区分

esbuild 可以通过打包来解决这些问题,朋克风的解释: 不打灰怎么浇筑混凝土; 我们的目的是想要将所有要用的包,可以理解成后面砌砖需要多少水泥,都拖到工地上来;找个地方,把他们放哪里让我们好使用,

这个文件是 nodemodule 包中的.vite 文件,你可以去任何一个 vite 项目中去寻找他,我们这里改一下名字,就叫".m-vite";

这里是 optimizer 的代码

import path from "path";
import { build } from "esbuild";
import { scanPlugin } from "./scanplugin";
import { preBundlePlugin } from "./preBundlePlugin";
import { PRE_BUNDLE_DIR } from "../contants";
export async function optimize(root: string, other?: string) {
  // 1. 确定入口
  // 2. 从入口处扫描依赖
  // 3. 预构建依赖;
  const deps = new Set<string>();
  const entry = path.resolve(root, other ? other : "src/client/main.tsx");
  await build({
    entryPoints: [entry],
    bundle: true,
    write: false,
    plugins: [scanPlugin(deps)]
  });
  await build({
    entryPoints: [...deps],
    write: true,
    bundle: true,
    format: "esm",
    splitting: true,
    outdir: path.resolve(root, PRE_BUNDLE_DIR)//PRE_BUNDLE_DIR = ".m-vite",
    plugins: [preBundlePlugin(deps)]
  });
  console.log("需要构建的依赖项", deps)
}

他经历了两次 build,第一次是扫描依赖不生成文件,第二次是预构建依赖构建依赖,输出至 node_modules/.m-vite,

至于插件的详解 请移步

从koa到mini-vite;

我们这里经历了两次预构建

await optimize(root, "src/cyber/react.tsx");
await optimize(root, "src/cyber/vue.ts");

因为这个项目会同时执行 react 和 vue,所以需要预构建两次,当然你可以自己选择,只需要在入口处写上对应的文件名即可;

这个是我们预构建后的目录

.m-vite.png

打包后的他们支持 es 模块语法了,我们接下来就会去其中提取相应的依赖

三、中间件

transformHtml 渲染初始页面

开始我们中间件的第一步,让我们获取到了 req.url 后该做什么,当页面没有任何路径的时候,我们会获取到req.url ="/",我们就在初始路径下,给他返回一个 html 文件,让页面去加载他

app.use(async (ctx, next) => {
  const { req, res } = ctx;
  if (req.url === "/") {
    const realPath = path.join(process.cwd(), "/cyber.html");
    //transformHtml
    if (await fs.pathExists(realPath)) {
      let code = await fs.readFile(realPath, "utf-8");
      res.setHeader("Content-Type", "text/html");
      res.end(code);
    }
  }
});

我们这里去读取了我们根目录下的 cyber.html 文件,我们使用了fs-extra库;这个库的 api 基本和 fs 原生的一致,但又更好的性能,和更加丰富的 api,我们检测是否存在该路径,如果存在则使用utf-8的格式去读取,并设置text/html的请求头返回给我们的服务器;不出意外的话,你应该可以看到一个html页面了

这里是cyber.html文件的内容

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>

  <body>
    <div id="react"></div>
    <script type="module" src="src/cyber/react.tsx"></script>

    <div id="vue"></div>
    <script type="module" src="src/cyber/vue.ts"></script>
  </body>
</html>

我们可以看到,他会引入一个 react.tsx 文件,可是我们的服务器还没又办法去处理他,别慌,我们来进行下一步中间件的书写

app.use(async (ctx, next) => {
  const { req, res } = ctx;
  if (req.url === "/") {
    const realPath = path.join(process.cwd(), "/cyber.html");
    //transformHtml
    if (await fs.pathExists(realPath)) {
      let code = await fs.readFile(realPath, "utf-8");
      res.setHeader("Content-Type", "text/html");
      res.end(code);
    }
  } else if (req.method === "GET" || /\.(?:j|t)sx?$|\.mjs$|\.vue$/.test(req.url as string)) {
    //resolve
    const realPath = path.join(process.cwd(), req.url + "");
    //loader
    if (await fs.pathExists(realPath)) {
      let code = await fs.readFile(realPath, "utf-8");
      let exname =
        path.extname(req.url as string).slice(1) === "vue"
          ? "js"
          : path.extname(req.url as string).slice(1);
      let { code: transformedCode, map } = await esbuild.transform(code, {
        target: "esnext",
        format: "esm",
        sourcemap: true,
        loader: exname as "js" | "ts" | "jsx" | "tsx",
      });
      //  transform
      const ms = new MagicString(transformedCode);
      await init;
      const [imports, exports] = parse(transformedCode);
      for (const importInfo of imports) {
        const { n: importedId, s: start, e: end } = importInfo;
        if (!importedId) continue;
        if (/^[\w@][^:]/.test(importedId)) {
          let realPath = path.join("/", "node_modules", ".m-vite", `${importedId}.js`);
          realPath = realPath.replace(/\\/g, "/");
          ms.overwrite(start, end, realPath);
          code = ms.toString();
        }
      }

      res.setHeader("Content-Type", "application/javascript");

      res.statusCode = 200;
      res.end(code);
    }
  }
  return next();
});

我们来进行下一步的处理,这个是 vite 的 load 阶段,去读取相应的文件

resolve 和 loader 解析文件路径并读取他

if (req.method === "GET" || /\.(?:j|t)sx?$|\.mjs$|\.vue$/.test(req.url as string)) {
  const realPath = path.join(process.cwd(), req.url + "");
}

这个阶段我们主要做的是筛选和拼接路径,我们只允许 js,ts,jsx,tsx,vue,mjs 文件传入,我们使用正则对其过滤 然后再拼接出他的真正路径出来;我们 vite 的 resolve 阶段 就是做的这么一项工作

既然知道地址了,我们就可以和上文中的一样,去读取他

if (await fs.pathExists(realPath)) {
  let code = await fs.readFile(realPath, "utf-8");
  let { code: transformedCode, map } = await esbuild.transform(code, {
    target: "esnext",
    format: "esm",
    sourcemap: true,
    loader: path.extname(req.url as string).slice(1) as "js" | "ts" | "jsx" | "tsx",
  });
}

我们可以看到,我们读取他后,使用 esbuild 对其进行了转换,我们这一步是将 ts 文件或者 tsx 文件转换为 esm 模块;其中 loader 是我们目前文件的后缀,react的优势就是可以直接被 esbuild 解析,可以帮我们节省很多功夫,vue则需要花费大量的精力进行书写了

我们得到的transformedCode就是我们处理好的tsx文件了,这时候他是一个原生的 es6 模块;但问题随之而来

transform 转换

bare.png 我们的浏览器可不认识bare引入的包;这时候我们需要对其进行改写,上面的预构建的文件就有了用武之地;

const ms = new MagicString(transformedCode);
await init;
const [imports, exports] = parse(transformedCode);
for (const importInfo of imports) {
  const { n: importedId, s: start, e: end } = importInfo;

  if (!importedId) continue;
  if (/^[\w@][^:]/.test(importedId)) {
    let realPath = path.join("/", "node_modules", ".m-vite", `${importedId}.js`);
    realPath = realPath.replace(/\\/g, "/");
    // ms.overwrite(start, end, realPath);
    code = ms.toString();
  }
}

我们使用magic-string这个库import MagicString from "magic-string";new一个ms;出来,他的功能是去修改我们的代码;这个库是vite官方使用的 然后使用 await init 创建引入和导出的分析环境;import { init, parse } from "es-module-lexer";这个工具库可以帮助我们分析一个模块的导入导出,并返回相应的ast;

{ n: 'react', t: 1, s: 43, e: 48, ss: 24, se: 49, d: -1, a: -1 }
{
  n: 'react-dom/client',
  t: 1,
  s: 79,
  e: 95,
  ss: 51,
  se: 96,
  d: -1,
  a: -1
}

这个是我们分析出的 imports 的值,我们只需要关注三点;nsen是包名 s是开始位置 e是结束位置;知道这几个值我们就能改写原来的代码;

let realPath = path.join("/", "node_modules", ".m-vite", `${importedId}.js`);

将代码改为我们想要的路径,然后返回即可;这是包的位置就指向的了.m-vite文件中了

import a from "./a.ts";
import React from "/node_modules/.m-vite/react.js";
import { createRoot } from "/node_modules/.m-vite/react-dom/client.js";
console.log(React, a);
const App = () => /* @__PURE__ */ React.createElement("div", null, "react");
const root = createRoot(document.getElementById("react"));
root.render(/* @__PURE__ */ React.createElement(App, null));

这个就是最终返回的文件;esbuild 原生支持reactvue与之相比又很反人类了;

res.setHeader("Content-Type", "application/javascript");
res.statusCode = 200;
res.end(code);

最后将其作为 js 文件返回,你的 react 就成了,但是我们是否还遗忘了一个东西,vue该怎么处理?

vue-loader

vue的处理是一把心酸,一把泪,为了处理vue;跑去读了vue-plugin的源码;

vue-plugin

其中难点就在于 sfc 怎么处理;怎么讲单文件拆分成三块进行处理,当然,我只拆了两块,华生,你发现了盲点。整个项目没有处理css的东西,哈哈 😄;当然我们先学会处理 js,css 会好处理很多

先叠个甲,这个vue-plugin的风格也是垃圾佬风格 😏

第一步,我们需要拆分 sfc 单文件组件

该怎么拆分呢,从 load 开始读

vue-plugin/index
load(id, opt) {
//...
      if (query.vue) {
        if (query.src) {
          return fs.readFileSync(filename, 'utf-8')
        }
        const descriptor = getDescriptor(filename, options.value)!
        let block: SFCBlock | null | undefined
        if (query.type === 'script') {
          // handle <script> + <script setup> merge via compileScript()
          block = resolveScript(
            descriptor,
            options.value,
            ssr,
            customElementFilter.value(filename),
          )
        } else if (query.type === 'template') {
          block = descriptor.template!
        } else if (query.type === 'style') {
          block = descriptor.styles[query.index!]
        } else if (query.index != null) {
          block = descriptor.customBlocks[query.index]
        }
        if (block) {
          return {
            code: block.content,
            map: block.map as any,
          }
        }
      }
    },

getDescriptor我找到了以下两个位置 代码位置:packages/plugin-vue/src/utils/descriptorCache.ts

parse.png

注意这个parse ;为了寻找complier的位置,我寻找到了这个位置

代码位置:packages/plugin-vue/src/compiler.ts

compiler.png 我们拿到了两个信息 一个是vue/compiler-sfc 一个是 parse; 既然如此 我们就import {parse as vueParse,compileTemplate,compileScript} from "vue/compiler-sfc"引入 parse 对其进行解析

const vueAst = vueParse(code);

我们得到了 vue 的 ast 语法树,代码成功的被我们拆分了;以下的代码块都经过简化

{
  descriptor: {
    filename: 'anonymous.vue',
    source: `...`
    template: {
      type: 'template',
      content:  `...`,
      loc: [Object],
      attrs: {},
      ast: [Object],
      map: [Object]
    },
    script: {
      type: 'script',
      content:  `...`,
      loc: [Object],
      attrs: [Object],
      lang: 'ts',
      map: [Object]
    },
    scriptSetup: {
      type: 'script',
      content:  `...`,
      loc: [Object],
      attrs: [Object],
      setup: true,
      lang: 'ts'
    },
    styles: [],
    customBlocks: [],
    cssVars: [],
    slotted: false,
    shouldForceReload: [Function: shouldForceReload]
  },
  errors: []
}

接下来就是转换代码了;我们接着 load 钩子开始读

resolveScript我又找到了以下位置

compiler.png

我们引入compileScript对 script 标签进行处理;

let scriptRes = compileScript(vueAst.descriptor, { id: req.url }); 得到结果

{
  type: 'script',
  content: 'import { defineComponent as _defineComponent } from 'vue'
import { ref } from "vue";

import { defineComponent } from "vue";

const __default__ = defineComponent({
  name: "App",
});

export default /*@__PURE__*/_defineComponent({
  ...__default__,
  setup(__props, { expose: __expose }) {
  __expose();

let count = ref(0);
const add = () => {
  count.value++;
};

const __returned__ = { get count() { return count }, set count(v) { count = v }, add }
Object.defineProperty(__returned__, '__isScriptSetup', { enumerable: false, value: true })
return __returned__
}

})',
  loc: {
  ....
  },
  attrs: { setup: true, lang: 'ts' },
  setup: true,
  lang: 'ts',
  bindings: {
    defineComponent: 'setup-const',
    ref: 'setup-const',
    count: 'setup-let',
    add: 'setup-const'
  },
  imports: [Object: null prototype] {
    defineComponent: {
      isType: false,
      imported: 'defineComponent',
      local: 'defineComponent',
      source: 'vue',
      isFromSetup: false,
      isUsedInTemplate: false
    },
    ref: {
      isType: false,
      imported: 'ref',
      local: 'ref',
      source: 'vue',
      isFromSetup: true,
      isUsedInTemplate: false
    }
  },
  map: []
    ,
    names: [],
    mappings: '...'
  },
  scriptAst: [
    Node {
      type: 'ImportDeclaration',
      start: 2,
      end: 40,
      loc: [SourceLocation],
      importKind: 'value',
      specifiers: [Array],
      source: [Node],
      attributes: []
    },
    Node {
      type: 'ExportDefaultDeclaration',
      start: 44,
      end: 222,
      loc: [SourceLocation],
      exportKind: 'value',
      declaration: [Node]
    }
  ],
  scriptSetupAst: [
    Node {
      type: 'ImportDeclaration',
      start: 2,
      end: 28,
      loc: [SourceLocation],
      importKind: 'value',
      specifiers: [Array],
      source: [Node],
      attributes: []
    },
    Node {
      type: 'VariableDeclaration',
      start: 30,
      end: 49,
      loc: [SourceLocation],
      declarations: [Array],
      kind: 'let'
    },
    Node {
      type: 'VariableDeclaration',
      start: 51,
      end: 92,
      loc: [SourceLocation],
      declarations: [Array],
      kind: 'const'
    }
  ],
  dep

如法炮制 找到compileTemplate函数,对模板代码进行处理

   let temp = compileTemplate({ source: vueAst.descriptor.template?.content as string, filename: realPath, id: req.url,  });

tempSfc.png

这里是template 的编译结果

{
  code: 'import { toDisplayString as _toDisplayString, createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"\n' +
    '\n' +
    'export function render(_ctx, _cache) {\n' +
    '  return (_openBlock(), _createElementBlock("div", null, [\n' +
    '    _createElementVNode("div", null, "hello Vue " + _toDisplayString(_ctx.count), 1 /* TEXT */),\n' +
    '    _createElementVNode("button", {\n' +
    '      onClick: _cache[0] || (_cache[0] = (...args) => (_ctx.add &#x26;&#x26; _ctx.add(...args)))\n' +
    '    }, "add")\n' +
    '  ]))\n' +
    '}',
  ast: {
    type: 0,
    source: '\r\n' +
      '  <div>\r\n' +
      '    <div>hello Vue {{ count }}</div>\r\n' +
      '    <button @click="add">add</button>\r\n' +
      '  </div>\r\n',
    children: [ [Object] ],
    helpers: Set(4) {
      Symbol(toDisplayString),
      Symbol(createElementVNode),
      Symbol(openBlock),
      Symbol(createElementBlock)
    },
    components: [],
    directives: [],
    hoists: [],
    imports: [],
    cached: [ [Object] ],
    temps: 0,
    codegenNode: {
      type: 13,
      tag: '"div"',
      props: undefined,
      children: [Array],
      patchFlag: undefined,
      dynamicProps: undefined,
      directives: undefined,
      isBlock: true,
      disableTracking: false,
      isComponent: false,
      loc: [Object]
    },
    loc: {
      start: [Object],
      end: [Object],
      source: '\r\n' +
        '  <div>\r\n' +
        '    <div>hello Vue {{ count }}</div>\r\n' +
        '    <button @click="add">add</button>\r\n' +
        '  </div>\r\n'
    },
    transformed: true,
    filters: []
  },
  preamble: '',
  source: '\r\n' +
    '  <div>\r\n' +
    '    <div>hello Vue {{ count }}</div>\r\n' +
    '    <button @click="add">add</button>\r\n' +
    '  </div>\r\n',
  errors: [],
  tips: [],
  map: {
    version: 3,
    sources: [ 'D:\\code\\study\\vite\\minivite\\koaVite\\src\\cyber\\App.vue' ],
    names: [ 'count' ],
    mappings: ';;;wBACE,oBAGM;IAFJ,oBAAgC,aAA3B,YAAU,oBAAGA,UAAK;IACvB,oBAAkC;MAAxB,OAAK,0CAAE,6BAAG;OAAE,KAAG',
    sourcesContent: [
      '\r\n' +
        '  <div>\r\n' +
        '    <div>hello Vue {{ count }}</div>\r\n' +
        '    <button @click="add">add</button>\r\n' +
        '  </div>\r\n'
    ]
  }
}

我们分割了其sfc的代码后,我们该怎么做呢;我们来看看一个vue3的项目,服务器返回的vue文件是什么样子

import { createHotContext as __vite__createHotContext } from "/@vite/client";import.meta.hot = __vite__createHotContext("/src/views/index.vue");import { defineComponent as _defineComponent } from "/node_modules/.vite/deps/vue.js?v=eaf793ab";
import { ElButton } from "/node_modules/.vite/deps/element-plus.js?v=eaf793ab";
import { testApi } from "/src/api/index.js";
import { defineComponent } from "/node_modules/.vite/deps/vue.js?v=eaf793ab";
const __default__ = defineComponent({
  name: ""
});
const _sfc_main = /* @__PURE__ */ _defineComponent({
  ...__default__,
  setup(__props, { expose: __expose }) {
    __expose();
    const sendTEst = async () => {
      let res = await testApi();
      console.log("ok", res);
    };
    const __returned__ = { sendTEst, get ElButton() {
      return ElButton;
    } };
    Object.defineProperty(__returned__, "__isScriptSetup", { enumerable: false, value: true });
    return __returned__;
  }
});
import { createTextVNode as _createTextVNode, withCtx as _withCtx, createVNode as _createVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "/node_modules/.vite/deps/vue.js?v=eaf793ab";
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
  return _openBlock(), _createElementBlock("div", null, [
    _createVNode($setup["ElButton"], { onClick: $setup.sendTEst }, {
      default: _withCtx(() => _cache[0] || (_cache[0] = [
        _createTextVNode("hello world")
      ])),
      _: 1
      /* STABLE */
    }),
    _cache[1] || (_cache[1] = _createTextVNode(" 1111111 "))
  ]);
}
_sfc_main.__hmrId = "b301384e";
typeof __VUE_HMR_RUNTIME__ !== "undefined" && __VUE_HMR_RUNTIME__.createRecord(_sfc_main.__hmrId, _sfc_main);
import.meta.hot.on("file-changed", ({ file }) => {
  __VUE_HMR_RUNTIME__.CHANGED_FILE = file;
});
import.meta.hot.accept((mod) => {
  if (!mod) return;
  const { default: updated, _rerender_only } = mod;
  if (_rerender_only) {
    __VUE_HMR_RUNTIME__.rerender(updated.__hmrId, updated.render);
  } else {
    __VUE_HMR_RUNTIME__.reload(updated.__hmrId, updated);
  }
});
import _export_sfc from "/@id/__x00__plugin-vue:export-helper";
export default /* @__PURE__ */ _export_sfc(_sfc_main, [["render", _sfc_render], ["__file", "D:/code/work/project/vite-project/src/views/index.vue"]]);

export default (sfc, props) => {
    const target = sfc.__vccOpts || sfc;
    for (const [key,val] of props) {
        target[key] = val;
    }
    return target;
}

我在阅读源码的时候,发现vue3实际上是通过默认暴露一个vue对象,这个vue对象就是其组件;我从上面源码获取到的信息就是vue文件时暴露一个default,这个default会根据改写其render和__file属性;将其视为vue组件,我们做的第一步就是将_sfc_mainrender替换为compileTemplate处理过后的render方法;要想将其合并,我们先将其拼接

     let arr = [scriptRes.content, temp.code]
        code = arr.join("\n");

最终我们得到混合的代码块;我们的vue-loader算是初步完成了,接下来我们去处理他的code

vue-plugin transform

因为我们主要处理的是导出模块,我们可以通过 const [imports, exports] = parse(transformedCode);对他的export进行改写


 if (req.url?.endsWith(".vue")) {
        for (const exportInfo of exports) {
          const { n: exportId, s: start, e: end } = exportInfo;
          if (!exportInfo) continue;

          if (exportId === "default") ms.overwrite(start, end, "other");
          // console.log(realPath)
          code = ms.toString() + `let sfc_main =stdin_default.__vccOpts || stdin_default;sfc_main.render =render};\n sfc_main.__file = "${realPath}";\nexport default /* @__PURE__ */sfc_main`
        }
      }

我们可以看到,我将default进行了处理;这是为什么呢;因为他解析出来的代码默认就是一个default导出;我将其改写为非default导出,这个位置需要留给vue实例;然后我们对其进行改写,将stdin_default(这个是script解析的结果)的属性改写,并提供新的名字sfc_main;sfc_main.render =render将基本信息改写完我们将其导出 export default /* @__PURE__ */sfc_main; 这是App.vue的代码

<template>
  <div>
    <div>hello Vue {{ count }}</div>
    <button  @click="add">add</button>
  </div>
</template>
<script lang="ts">
import { defineComponent } from "vue";

export default defineComponent({
  name: "App",
});
</script>
<script setup lang="ts">
import { ref } from "vue";
let count = ref(0);
const add = () => {
  count.value++;
};
</script>

我们来看看页面

noref.png

🥜,你又发现了忙点,怎么响应式数据没有

我对其sfc_main的render参数进行打印

render_arg.png

我发现其是有值的,排除法得到应该是我的render有问题;我在重新去检查compileTemplate的参数,并去查询其源码 vite-plugin-vue/packages/plugin-vue/src /template.ts

我发现其bindingMetadata;在vue3的源码的阅读中,bind我们一般知道含有这个词的一般为传递参数;我刚好发现在script返回值的ts类型中刚好有这个值

export interface SFCScriptBlock extends SFCBlock {
    type: 'script';
    setup?: string | boolean;
    bindings?: BindingMetadata$1;
    imports?: Record<string, ImportBinding>;
    scriptAst?: _babel_types.Statement[];
    scriptSetupAst?: _babel_types.Statement[];
    warnings?: string[];
    /**
     * Fully resolved dependency file paths (unix slashes) with imported types
     * used in macros, used for HMR cache busting in @vitejs/plugin-vue and
     * vue-loader.
     */
    deps?: string[];
}

那我们把他加进去

       let temp = compileTemplate({ source: vueAst.descriptor.template?.content as string, filename: realPath, id: req.url, slotted: vueAst.descriptor.slotted, compilerOptions: { bindingMetadata: scriptRes.bindings } });
    

我们的代码就能正常工作了,甚至可以使用vue的开发者工具;

res.gif

这里是完整的代码

import Koa from "koa";
import { optimize } from "./node/optimizer";
import { init, parse } from "es-module-lexer";
import MagicString from "magic-string";
import fs from "fs-extra";
import esbuild from "esbuild"
import path from "path";
import {
  parse as vueParse,
  compileTemplate,
  compileScript,
} from "vue/compiler-sfc"
const root = process.cwd();


const app = new Koa();

app.use(async (ctx, next) => {
  const { req, res } = ctx;
  if (req.url === "/") {
    const realPath = path.join(process.cwd(), "/cyber.html");
    //transformHtml
    if (await fs.pathExists(realPath)) {
      let code = await fs.readFile(realPath, "utf-8");
      res.setHeader("Content-Type", "text/html");
      res.end(code)
    }
  } else if ((req.method === "GET" || /\.(?:j|t)sx?$|\.mjs$|\.vue$/.test(req.url as string))) {
    //resolve
    const realPath = path.join(process.cwd(), req.url + "");
    //loader
    if (await fs.pathExists(realPath)) {

      let code = await fs.readFile(realPath, "utf-8");
      //vue-plugin loader
      if (req.url?.endsWith(".vue")) {
        const vueAst = vueParse(code);
        let scriptRes = compileScript(vueAst.descriptor, { id: req.url });
        let temp = compileTemplate({ source: vueAst.descriptor.template?.content as string, filename: realPath, id: req.url, slotted: vueAst.descriptor.slotted, compilerOptions: { bindingMetadata: scriptRes.bindings } });
    
 
        let arr = [scriptRes.content, temp.code]
        code = arr.join("\n");

      }
      let exname = path.extname(req.url as string).slice(1) === "vue" ? "js" :path.extname(req.url as string).slice(1)
      let { code: transformedCode, map } = await esbuild.transform(code, {
        target: "esnext",
        format: "esm",
        sourcemap: true,
        loader: exname as "js" | "ts" | "jsx" | "tsx",
      });
  
      //  transform
      const ms = new MagicString(transformedCode);
      await init;
      const [imports, exports] = parse(transformedCode);

      for (const importInfo of imports) {
        const { n: importedId, s: start, e: end } = importInfo;

        if (!importedId) continue;
        if (/^[\w@][^:]/.test(importedId)) {

          let realPath = path.join('/', "node_modules", ".m-vite", `${importedId}.js`);
          realPath = realPath.replace(/\\/g, "/");
         
          ms.overwrite(start, end, realPath);
          code = ms.toString();
        }

      }
      // vue-plugin transform
      if (req.url?.endsWith(".vue")) {
        for (const exportInfo of exports) {
          const { n: exportId, s: start, e: end } = exportInfo;
          if (!exportInfo) continue;

          if (exportId === "default") ms.overwrite(start, end, "other");
          // console.log(realPath)
          code = ms.toString() + `let sfc_main =stdin_default.__vccOpts || stdin_default;sfc_main.render = function(...arg){console.log(arg);return render(...arg)};\n sfc_main.__file = "${realPath}";\nexport default /* @__PURE__ */sfc_main`
        }
      }
      res.setHeader("Content-Type", "application/javascript");


      res.statusCode = 200;
      res.end(code)
    }
  }
  return next()
})


app.listen(8954, async () => {
  await optimize(root, "src/cyber/react.tsx")
  await optimize(root, "src/cyber/vue.ts")
  console.log("http://localhost:8954 ")
})

同时支持模块化

image.png

什么叫做赛博朋克?这就叫赛博朋克;垃圾佬的哲学观,又不是不能跑😎;

靓仔;都看到这里了,点个赞再走呗