vue3源码解析:编译之编译器代码生成过程

上文我们讲到了编译时vue如何转换标准AST中vue独有的属性、指令(例如v-for#slot)等内容。本文我们来分析一下转换后的AST最终如何生成代码。

一、示例模板

让我们以一个具体的模板为例,分析代码生成的完整过程:

vue 复制代码
<template>
  <div class="container">
    <h1>{{ title }}</h1>
    <button @click="handleClick">Count: {{ count }}</button>
  </div>
</template>

二、转换后的 AST 结构

经过解析和转换后,这个模板会生成如下的 AST:

js 复制代码
const ast = {
  type: NodeTypes.ROOT,
  children: [
    {
      type: NodeTypes.ELEMENT,
      tag: "div",
      props: [
        {
          type: NodeTypes.ATTRIBUTE,
          name: "class",
          value: {
            type: NodeTypes.TEXT,
            content: "container",
          },
        },
      ],
      children: [
        {
          type: NodeTypes.ELEMENT,
          tag: "h1",
          children: [
            {
              type: NodeTypes.INTERPOLATION,
              content: {
                type: NodeTypes.SIMPLE_EXPRESSION,
                content: "title",
              },
            },
          ],
          codegenNode: {
            type: NodeTypes.VNODE_CALL,
            tag: '"h1"',
            props: null,
            children: {
              type: NodeTypes.INTERPOLATION,
              content: {
                type: NodeTypes.SIMPLE_EXPRESSION,
                content: "title",
              },
            },
            patchFlag: PatchFlags.TEXT, // 1 表示动态文本内容
            dynamicProps: null,
            isBlock: false,
            disableTracking: false,
            isComponent: false,
          },
        },
        {
          type: NodeTypes.ELEMENT,
          tag: "button",
          props: [
            {
              type: NodeTypes.DIRECTIVE,
              name: "on",
              arg: {
                type: NodeTypes.SIMPLE_EXPRESSION,
                content: "click",
                isStatic: true,
              },
              exp: {
                type: NodeTypes.SIMPLE_EXPRESSION,
                content: "handleClick",
              },
            },
          ],
          children: [
            {
              type: NodeTypes.TEXT,
              content: "Count: ",
            },
            {
              type: NodeTypes.INTERPOLATION,
              content: {
                type: NodeTypes.SIMPLE_EXPRESSION,
                content: "count",
              },
            },
          ],
          codegenNode: {
            type: NodeTypes.VNODE_CALL,
            tag: '"button"',
            props: {
              type: NodeTypes.JS_OBJECT_EXPRESSION,
              properties: [
                {
                  key: "onClick",
                  value: {
                    type: NodeTypes.SIMPLE_EXPRESSION,
                    content:
                      "_cache[0] || (_cache[0] = (...args) => _ctx.handleClick && _ctx.handleClick(...args))",
                    isStatic: false,
                  },
                },
              ],
            },
            children: [
              {
                type: NodeTypes.TEXT,
                content: "Count: ",
              },
              {
                type: NodeTypes.INTERPOLATION,
                content: {
                  type: NodeTypes.SIMPLE_EXPRESSION,
                  content: "count",
                },
              },
            ],
            patchFlag: PatchFlags.TEXT, // 1 表示动态文本内容
            dynamicProps: null,
            isBlock: false,
            disableTracking: false,
            isComponent: false,
          },
        },
      ],
      codegenNode: {
        type: NodeTypes.VNODE_CALL,
        tag: '"div"',
        props: {
          type: NodeTypes.JS_OBJECT_EXPRESSION,
          properties: [
            {
              key: "class",
              value: {
                type: NodeTypes.SIMPLE_EXPRESSION,
                content: "container",
                isStatic: true,
              },
            },
          ],
        },
        children: [
          // h1 的 codegenNode
          // button 的 codegenNode
        ],
        patchFlag: 0, // 0 表示没有动态内容
        dynamicProps: null,
        isBlock: true,
        disableTracking: false,
        isComponent: false,
      },
    },
  ],
  codegenNode: {
    type: NodeTypes.VNODE_CALL,
    tag: FRAGMENT,
    props: null,
    children: [
      /* div 的 codegenNode */
    ],
    patchFlag: PatchFlags.STABLE_FRAGMENT,
    dynamicProps: null,
    isBlock: true,
    disableTracking: false,
    isComponent: false,
  },
  helpers: new Set([
    CREATE_ELEMENT_VNODE,
    TO_DISPLAY_STRING,
    CREATE_ELEMENT_BLOCK,
    OPEN_BLOCK,
  ]),
  components: [],
  directives: [],
  hoists: [
    {
      type: NodeTypes.JS_OBJECT_EXPRESSION,
      properties: [
        {
          key: "class",
          value: {
            type: NodeTypes.SIMPLE_EXPRESSION,
            content: "container",
            isStatic: true,
          },
        },
      ],
    },
  ],
  imports: [],
  temps: 0,
  cached: 1,
};

这个 AST 结构展示了转换后的完整状态,包括:

  1. 每个元素节点都添加了 codegenNode,包含生成代码所需的所有信息
  2. 根节点的 codegenNode 使用 Fragment 包装
  3. 静态内容(如 class="container")被提升到 hoists 数组
  4. 事件处理器被缓存(cached 计数为 1)
  5. 添加了所需的辅助函数(helpers)
  6. 为动态内容添加了 patch flags 标记

三、AST 生成的最终代码

让我们看看转换后的 AST 结构生成的最终代码

1. 生成的代码

js 复制代码
// 1. 导入辅助函数
import {
  createElementVNode as _createElementVNode,
  toDisplayString as _toDisplayString,
  openBlock as _openBlock,
  createElementBlock as _createElementBlock,
} from "vue";

// 2. 静态提升的内容
const _hoisted_1 = { class: "container" };

// 3. 渲染函数
export function render(_ctx, _cache) {
  return (
    _openBlock(),
    _createElementBlock("div", _hoisted_1, [
      _createElementVNode(
        "h1",
        null,
        _toDisplayString(_ctx.title),
        1 /* TEXT */
      ),
      _createElementVNode(
        "button",
        {
          onClick:
            _cache[0] ||
            (_cache[0] = (...args) =>
              _ctx.handleClick && _ctx.handleClick(...args)),
        },
        "Count: " + _toDisplayString(_ctx.count),
        1 /* TEXT */
      ),
    ])
  );
}

2. 代码生成说明

  1. 辅助函数导入

    • createElementVNode:创建普通元素节点
    • toDisplayString:转换插值表达式内容
    • openBlock:开启块级作用域
    • createElementBlock:创建块级元素
  2. 静态提升

    • 将静态的 class 属性提升为常量 _hoisted_1
    • 避免重复创建静态内容
  3. 事件处理

js 复制代码
onClick: _cache[0] ||
  (_cache[0] = (...args) => _ctx.handleClick && _ctx.handleClick(...args));
  • 使用事件缓存优化
  • 处理可能不存在的方法调用
  1. 动态内容处理
js 复制代码
_toDisplayString(_ctx.title); // 处理 {{ title }}
"Count: " + _toDisplayString(_ctx.count); // 处理 Count: {{ count }}
  1. 补丁标记

    • 1 /* TEXT */:表示节点包含动态文本内容
    • 用于优化更新性能
  2. 块级树优化

    • 使用 _openBlock_createElementBlock 创建块级树
    • 优化更新性能
    • 跟踪动态节点

3. 优化说明

  1. 静态内容优化

    • 静态的 class 属性被提升到渲染函数外部
    • 减少每次渲染时的对象创建
  2. 事件处理优化

    • 使用事件处理器缓存
    • 避免不必要的函数重建
  3. 动态内容标记

    • 使用补丁标记(patch flags)标识动态内容
    • 优化更新性能
  4. 块级树优化

    • 创建块级树结构
    • 精确跟踪动态节点
    • 提高更新性能

这个生成的代码展示了 Vue 编译器的多项优化策略:

  • 静态内容提升
  • 事件处理器缓存
  • 补丁标记
  • 块级树优化
  • 动态节点追踪

四、举例说明生成代码的详细过程

让我们按照 generate 函数的执行流程,详细分析代码生成的每个步骤:

4.1 创建上下文和初始设置

源码分析

js// 复制代码
const context = createCodegenContext(ast, options);
if (options.onContextCreated) options.onContextCreated(context);

// 2. 从上下文中解构需要的工具函数和状态
const {
  mode, // 编译模式:module/function
  push, // 用于向代码字符串追加内容
  prefixIdentifiers, // 是否添加标识符前缀
  indent, // 增加缩进级别
  deindent, // 减少缩进级别
  newline, // 插入换行
  scopeId, // 作用域ID
  ssr, // 是否服务端渲染
} = context;

// 3. 获取辅助函数信息
const helpers = Array.from(ast.helpers);
const hasHelpers = helpers.length > 0;
const useWithBlock = !prefixIdentifiers && mode !== "module";

context 对象变化

js 复制代码
// 初始状态
context = {
  mode: "module",
  code: "",
  column: 1,
  line: 1,
  offset: 0,
  indentLevel: 0,
  pure: false,
  map: undefined,
  source: ast.source,
  // ... 其他属性
};

// helpers 数组内容(基于我们的示例)
helpers = [
  CREATE_ELEMENT_VNODE,
  TO_DISPLAY_STRING,
  OPEN_BLOCK,
  CREATE_ELEMENT_BLOCK,
];

4.2 生成前导代码

源码分析

js 复制代码
// 1. 创建前导代码上下文
const preambleContext = isSetupInlined
  ? createCodegenContext(ast, options)
  : context;

// 2. 根据模式生成不同的前导代码
if (!__BROWSER__ && mode === "module") {
  genModulePreamble(ast, preambleContext, genScopeId, isSetupInlined);
} else {
  genFunctionPreamble(ast, preambleContext);
}

模块模式下的前导代码生成

js 复制代码
function genModulePreamble(ast, context, genScopeId, inline) {
  const { push, newline, runtimeModuleName } = context;

  // 1. 生成辅助函数导入
  if (ast.helpers.size) {
    push(
      `import { ${helpers
        .map((s) => `${helperNameMap[s]} as _${helperNameMap[s]}`)
        .join(", ")} } from ${JSON.stringify(runtimeModuleName)}\n`
    );
  }

  // 2. 生成静态提升的变量
  genHoists(ast.hoists, context);
  newline();

  // 3. 添加 export 关键字
  if (!inline) {
    push(`export `);
  }
}

代码生成过程

js 复制代码
// context.code 的变化
context.code = ''
↓
// 添加导入语句
context.code = `import {
  createElementVNode as _createElementVNode,
  toDisplayString as _toDisplayString,
  openBlock as _openBlock,
  createElementBlock as _createElementBlock
} from "vue"\n`
↓
// 添加静态提升的内容
context.code += `\nconst _hoisted_1 = { class: "container" }\n`
↓
// 添加 export 关键字
context.code += `\nexport `

4.3 生成渲染函数签名

源码分析

js 复制代码
// 1. 确定函数名和参数
const functionName = ssr ? `ssrRender` : `render`;
const args = ssr ? ["_ctx", "_push", "_parent", "_attrs"] : ["_ctx", "_cache"];

// 2. 生成函数声明
if (isSetupInlined) {
  push(`(${signature}) => {`);
} else {
  push(`function ${functionName}(${args.join(", ")}) {`);
}
indent();

// 3. 处理 with 块(如果需要)
if (useWithBlock) {
  push(`with (_ctx) {`);
  indent();
  // 在 with 块内声明辅助函数
  if (hasHelpers) {
    push(
      `const { ${helpers.map(aliasHelper).join(", ")} } = _Vue\n`,
      NewlineType.End
    );
    newline();
  }
}

代码生成过程

js 复制代码
// context.code 的变化
context.code += `function render(_ctx, _cache) {`
↓
context.code += `\n  `  // 缩进增加

4.4 生成 VNode 树

源码分析

js 复制代码
// 1. 生成返回语句
if (!ssr) {
  push(`return `);
}

// 2. 生成根节点
if (ast.codegenNode) {
  genNode(ast.codegenNode, context);
} else {
  push(`null`);
}

节点生成的核心函数

js 复制代码
// genNode 是代码生成的核心函数,负责处理所有类型的节点
function genNode(node: CodegenNode | symbol | string, context: CodegenContext) {
  // 1. 处理字符串节点
  if (isString(node)) {
    context.push(node, NewlineType.Unknown);
    return;
  }
  // 2. 处理符号节点(通常是辅助函数)
  if (isSymbol(node)) {
    context.push(context.helper(node));
    return;
  }

  // 3. 根据节点类型分发到不同的处理函数
  switch (node.type) {
    // 3.1 元素、条件和循环节点
    case NodeTypes.ELEMENT:
    case NodeTypes.IF:
    case NodeTypes.FOR:
      // 确保节点已经过转换
      genNode(node.codegenNode!, context);
      break;

    // 3.2 文本节点
    case NodeTypes.TEXT:
      genText(node, context);
      break;

    // 3.3 简单表达式
    case NodeTypes.SIMPLE_EXPRESSION:
      genExpression(node, context);
      break;

    // 3.4 插值表达式 {{ xxx }}
    case NodeTypes.INTERPOLATION:
      genInterpolation(node, context);
      break;

    // 3.5 虚拟节点调用
    case NodeTypes.VNODE_CALL:
      genVNodeCall(node, context);
      break;

    // 3.6 复合表达式
    case NodeTypes.COMPOUND_EXPRESSION:
      genCompoundExpression(node, context);
      break;

    // ... 其他类型的处理
  }
}

// 处理文本节点
function genText(node: TextNode, context: CodegenContext) {
  context.push(JSON.stringify(node.content), NewlineType.Unknown, node);
}

// 处理表达式节点
function genExpression(node: SimpleExpressionNode, context: CodegenContext) {
  const { content, isStatic } = node;
  context.push(
    isStatic ? JSON.stringify(content) : content,
    NewlineType.Unknown,
    node
  );
}

// 处理插值表达式
function genInterpolation(node: InterpolationNode, context: CodegenContext) {
  const { push, helper, pure } = context;
  if (pure) push(PURE_ANNOTATION);
  push(`${helper(TO_DISPLAY_STRING)}(`);
  genNode(node.content, context);
  push(`)`);
}

// 处理虚拟节点调用
function genVNodeCall(node: VNodeCall, context: CodegenContext) {
  const { push, helper, pure } = context;
  const {
    tag,
    props,
    children,
    patchFlag,
    dynamicProps,
    directives,
    isBlock,
    disableTracking,
    isComponent,
  } = node;

  // 1. 处理指令
  if (directives) {
    push(helper(WITH_DIRECTIVES) + `(`);
  }

  // 2. 处理块级节点
  if (isBlock) {
    push(`(${helper(OPEN_BLOCK)}(${disableTracking ? `true` : ``}), `);
  }

  // 3. 添加纯注释标记
  if (pure) {
    push(PURE_ANNOTATION);
  }

  // 4. 选择创建节点的辅助函数
  const callHelper = isBlock
    ? getVNodeBlockHelper(context.inSSR, isComponent)
    : getVNodeHelper(context.inSSR, isComponent);

  push(helper(callHelper) + `(`, NewlineType.None, node);

  // 5. 生成参数列表
  genNodeList(
    genNullableArgs([tag, props, children, patchFlag, dynamicProps]),
    context
  );

  push(`)`);

  // 6. 处理块级和指令的结束
  if (isBlock) {
    push(`)`);
  }
  if (directives) {
    push(`, `);
    genNode(directives, context);
    push(`)`);
  }
}

// 处理节点列表生成
function genNodeList(
  nodes: (string | symbol | CodegenNode)[],
  context: CodegenContext
) {
  const { push, newline } = context;

  for (let i = 0; i < nodes.length; i++) {
    const node = nodes[i];
    if (isString(node)) {
      push(node);
    } else if (isSymbol(node)) {
      push(context.helper(node));
    } else {
      genNode(node, context);
    }
    if (i < nodes.length - 1) {
      push(", ");
    }
  }
}

// 处理可能为空的参数列表
function genNullableArgs(args: any[]): any[] {
  // 从后往前寻找第一个非空参数
  let i = args.length;
  while (i--) {
    if (args[i] != null) break;
  }
  // 返回截至最后一个非空参数的列表
  return args.slice(0, i + 1);
}

代码生成过程

js 复制代码
// 1. 初始状态
context = {
  code: `function render(_ctx, _cache) {\n  return `,
  indentLevel: 1,
  line: 2,
  column: 9,
};

// 2. 生成根节点开始
context.code += `(_openBlock(), _createElementBlock("div", _hoisted_1, [`;
context.column += 52;

// 3. 生成 h1 节点
context.code += `\n  _createElementVNode("h1", null, _toDisplayString(_ctx.title), 1 /* TEXT */),`;
context.line += 1;
context.column = 78;

// 4. 生成 button 节点
context.code += `\n  _createElementVNode("button", {
    onClick: _cache[0] || (_cache[0] = (...args) => (_ctx.handleClick && _ctx.handleClick(...args)))
  }, "Count: " + _toDisplayString(_ctx.count), 1 /* TEXT */)`;
context.line += 3;
context.column = 63;

// 5. 完成节点生成
context.code += `\n]))`;
context.line += 1;
context.column = 4;

在这个过程中,genNode 函数通过递归遍历 AST 节点树,为每种类型的节点生成相应的代码:

  1. 元素节点 :生成 createElementVNodecreateElementBlock 调用
  2. 文本节点:直接生成字符串字面量
  3. 插值表达式 :生成 toDisplayString 调用
  4. 指令:生成相应的指令处理代码
  5. 事件处理器:生成带缓存的事件处理函数

每个节点的生成都会考虑:

  • 静态/动态内容的处理
  • 优化标记的添加
  • 缓存策略的应用
  • 块级树的构建

4.5 生成函数结束

源码分析

js 复制代码
// 1. 处理 with 块结束
if (useWithBlock) {
  deindent();
  push(`}`);
}

// 2. 结束渲染函数
deindent();
push(`}`);

// 3. 返回生成结果
return {
  ast,
  code: context.code,
  preamble: isSetupInlined ? preambleContext.code : ``,
  map: context.map ? context.map.toJSON() : undefined,
};

最终生成的完整代码

js 复制代码
import {
  createElementVNode as _createElementVNode,
  toDisplayString as _toDisplayString,
  openBlock as _openBlock,
  createElementBlock as _createElementBlock,
} from "vue";

const _hoisted_1 = { class: "container" };

export function render(_ctx, _cache) {
  return (
    _openBlock(),
    _createElementBlock("div", _hoisted_1, [
      _createElementVNode(
        "h1",
        null,
        _toDisplayString(_ctx.title),
        1 /* TEXT */
      ),
      _createElementVNode(
        "button",
        {
          onClick:
            _cache[0] ||
            (_cache[0] = (...args) =>
              _ctx.handleClick && _ctx.handleClick(...args)),
        },
        "Count: " + _toDisplayString(_ctx.count),
        1 /* TEXT */
      ),
    ])
  );
}

下面通过一个简单的流程图将整个代码生成过程过一遍。

js 复制代码
模板 Template
    ↓
解析 Parse
    ↓
AST 生成
    ↓
转换 Transform
    ↓
代码生成 CodeGen
    |
    ├── 1. 创建上下文
    |     ├── 初始化 context 对象
    |     └── 收集 helpers
    |
    ├── 2. 生成前导代码
    |     ├── 导入辅助函数
    |     ├── 静态提升
    |     └── 导出声明
    |
    ├── 3. 生成渲染函数
    |     ├── 函数签名
    |     └── with 块处理
    |
    ├── 4. 生成 VNode 树
    |     ├── genNode
    |     |    ├── 文本节点
    |     |    ├── 表达式节点
    |     |    ├── 插值节点
    |     |    └── VNode 调用
    |     |
    |     └── 优化处理
    |          ├── 静态提升
    |          ├── 补丁标记
    |          ├── 块级树
    |          └── 事件缓存
    |
    └── 5. 完成生成
          ├── 闭合函数
          └── 返回结果

五、优化说明

让我们基于前面的示例模板来分析各项优化策略:

vue 复制代码
<template>
  <div class="container">
    <h1>{{ title }}</h1>
    <button @click="handleClick">Count: {{ count }}</button>
  </div>
</template>

1. 静态提升 (Static Hoisting)

在我们的示例中,静态的 class 属性被提升:

js 复制代码
// 静态提升的内容
const _hoisted_1 = { class: "container" };

// 渲染函数中直接使用提升的内容
return _createElementBlock("div", _hoisted_1, [
  // ... 子节点
]);

这种优化的效果:

  • 将静态的 class="container" 提升到渲染函数外部
  • 避免在每次渲染时重新创建对象
  • 减少内存分配和垃圾回收的开销

2. 动态内容处理 (Dynamic Content)

示例中有两处动态内容:

js 复制代码
// 1. h1 中的动态文本
_createElementVNode(
  "h1",
  null,
  _toDisplayString(_ctx.title), // 动态内容
  1 /* TEXT */  // 补丁标记
);

// 2. button 中的动态文本
_createElementVNode(
  "button",
  { onClick: /*...*/ },
  "Count: " + _toDisplayString(_ctx.count), // 动态内容
  1 /* TEXT */  // 补丁标记
);

优化效果:

  • 使用 _toDisplayString 处理动态文本
  • 通过补丁标记 1 /* TEXT */ 标识动态内容
  • 更新时只处理文本部分,不影响节点本身

3. 事件处理优化 (Event Handling)

示例中的点击事件处理:

js 复制代码
_createElementVNode(
  "button",
  {
    onClick:
      _cache[0] ||
      (_cache[0] = (...args) => _ctx.handleClick && _ctx.handleClick(...args)),
  }
  // ... 内容
);

优化效果:

  • 使用 _cache[0] 缓存事件处理函数
  • 避免每次渲染时重新创建函数
  • 处理方法可能不存在的情况 (_ctx.handleClick && _ctx.handleClick)

4. 块级树优化 (Block Tree)

示例中的块级树结构:

js 复制代码
// 1. 开启块级作用域
_openBlock();

// 2. 创建块级元素
_createElementBlock("div", _hoisted_1, [
  // 子节点被收集到 block 的 dynamicChildren 中
  _createElementVNode("h1", null, _toDisplayString(_ctx.title), 1),
  _createElementVNode("button", /*...*/, /*...*/, 1)
]);

优化效果:

  • 根节点 div 被标记为块级节点
  • 动态子节点 (h1 和 button) 被收集到 dynamicChildren 数组
  • 更新时可以直接定位到动态节点,跳过静态内容

5. 优化策略的综合应用

让我们看看这些优化策略如何在示例中协同工作:

js 复制代码
import /*...*/ "vue";

// 1. 静态提升
const _hoisted_1 = { class: "container" };

// 2. 渲染函数
export function render(_ctx, _cache) {
  return (
    _openBlock(),
    _createElementBlock("div", _hoisted_1, [
      // 动态文本节点
      _createElementVNode(
        "h1",
        null,
        _toDisplayString(_ctx.title),
        1 /* TEXT */
      ),
      // 带缓存的事件处理器和动态文本
      _createElementVNode(
        "button",
        {
          onClick:
            _cache[0] ||
            (_cache[0] = (...args) =>
              _ctx.handleClick && _ctx.handleClick(...args)),
        },
        "Count: " + _toDisplayString(_ctx.count),
        1 /* TEXT */
      ),
    ])
  );
}

这个示例展示了优化策略的综合效果:

  • 静态的 class 属性被提升
  • 动态文本内容使用补丁标记
  • 事件处理器被缓存
  • 使用块级树结构追踪动态节点
  • 生成的代码既简洁又高效

六、总结

代码生成是vue编译过程的最后一步,它将优化后的 AST 转换为可执行的渲染函数,在这个过程中,编译器实现了多种优化策略,如静态提升、动态内容标记、事件缓存、块树优化。至此,我们完成了整个编译流程的分析。

相关推荐
黄智勇16 分钟前
xlsx-handlebars 一个用于处理 XLSX 文件 Handlebars 模板的 Rust 库,支持多平台使
前端
brzhang1 小时前
为什么 OpenAI 不让 LLM 生成 UI?深度解析 OpenAI Apps SDK 背后的新一代交互范式
前端·后端·架构
brzhang2 小时前
OpenAI Apps SDK ,一个好的 App,不是让用户知道它该怎么用,而是让用户自然地知道自己在做什么。
前端·后端·架构
程序员王天2 小时前
【开发AGIC】Vue3+NestJS+DeepSeek AI作业批改系统(已开源)
vue.js·ai编程·nestjs
井柏然3 小时前
前端工程化—实战npm包深入理解 external 及实例唯一性
前端·javascript·前端工程化
昔冰_G3 小时前
Vue内置组件KeepAlive——缓存组件实例
vue.js·缓存·vue3·vue2·keep-alive·vue组件缓存·vue内置组件
IT_陈寒3 小时前
Redis 高性能缓存设计:7个核心优化策略让你的QPS提升300%
前端·人工智能·后端
aklry3 小时前
elpis之动态组件机制
javascript·vue.js·架构
井柏然4 小时前
从 npm 包实战深入理解 external 及实例唯一性
前端·javascript·前端工程化
羊锦磊4 小时前
[ vue 前端框架 ] 基本用法和vue.cli脚手架搭建
前端·vue.js·前端框架