AI 驱动的 Vue3 应用开发平台 深入探究(六):双向代码转换之DSL到Vue代码生成

DSL 到 Vue 代码生成

DSL 到 Vue 代码生成系统将 VTJ 基于块的 DSL 模式转换为生产就绪的 Vue 3 组件。此转换管道弥合了可视化设计器输出与可执行代码之间的差距,实现了与标准 Vue 开发工作流的无缝集成,同时保留了设计器的意图并保持了类型安全。

架构概览

代码生成架构遵循四阶段管道,通过收集、解析、模板编译和格式化阶段处理 DSL 模式。这种模块化设计允许在保持统一转换逻辑的同时进行特定于平台的变体(web、h5、uniapp)。

flowchart TD DSL[BlockSchema DSL] --> COL[Collecter] CM[ComponentMap] --> PAR[解析器模块] DEP[Dependencies] --> COL COL --> COL1[遍历 DSL 树] COL1 --> COL2[收集导入] COL2 --> COL3[构建上下文] COL3 --> PAR PAR --> TMPL[模板解析器] PAR --> STATE[状态解析器] PAR --> FUNC[函数解析器] PAR --> PROP[Props 解析器] PAR --> EVENT[事件解析器] PAR --> DIR[指令解析器] TMPL --> TK[Token 生成] STATE --> TK FUNC --> TK PROP --> TK EVENT --> TK DIR --> TK TK --> ST[脚本模板] TK --> VT[Vue 模板] ST --> FMT[格式化器] VT --> FMT FMT --> TSF[TS 格式化器] FMT --> CSSF[CSS 格式化器] FMT --> VUEF[Vue 格式化器] TSF --> VUE[Vue 组件] CSSF --> VUE VUEF --> VUE

该系统利用收集器-收集者模式,在转换之前提取元数据,从而实现上下文感知的代码生成,解析依赖关系,跟踪组件使用情况,并维护正确的导入语句。

核心生成管道

生成器函数入口点

主要的生成函数接受灵活的输入配置,支持传统的参数传递和现代的选项对象。这种双重兼容性既确保了向后兼容性,又为新实现提供了更清晰的 API。

typescript 复制代码
export async function generator(
  _dsl: BlockSchema | GeneratorOptions,
  _componentMap: Map<string, MaterialDescription> = new Map(),
  _dependencies: Dependencie[] = [],
  _platform: PlatformType = "web",
  _formatterDisabled?: boolean,
) {
  const maybeOptions: any = _dsl;
  const options: GeneratorOptions =
    typeof maybeOptions.dsl === "object" && arguments.length === 1
      ? maybeOptions
      : {
          dsl: _dsl,
          componentMap: _componentMap,
          dependencies: _dependencies,
          platform: _platform,
          formatterDisabled: _formatterDisabled,
        };
  const {
    dsl,
    componentMap = new Map(),
    dependencies = [],
    platform = "web",
    formatterDisabled = false,
    ts = true,
    scss = false,
  } = options;
  const collecter = new Collecter(cloneDeep(dsl), dependencies);
  const token = parser(collecter, componentMap, platform);
  const script = scriptCompiled(token);
  const vue = vueCompiled({
    template: token.template || `\n<!--组件模版内容-->\n`,
    css:
      (await cssFormatter(token.css, formatterDisabled)) ||
      `\n/* 组件样式内容 */\n`,
    script: await tsFormatter(script, formatterDisabled),
    style: await cssFormatter(token.style, formatterDisabled),
    scriptLang: ts ? "ts" : "js",
    styleLang: scss ? "scss" : "css",
  });
  return await vueFormatter(vue, formatterDisabled).catch((e) => {
    e.content = vue;
    return Promise.reject(e);
  });
}

生成器编排四个关键操作:用于隔离的 DSL 深度克隆、元数据收集、语义解析、模板编译和代码格式化。每个阶段都会生成中间产物,供后续转换使用。

收集阶段

Collecter 类遍历 DSL 树以提取代码生成所需的关键元数据。这包括库依赖、导入语句、上下文引用、URL 模式和块插件引用。

typescript 复制代码
export class Collecter {
  imports: Record<string, Set<string>> = {};
  context: Record<string, Set<string>> = {};
  style: Record<string, string> = {};
  urlSchemas: Record<string, NodeFromUrlSchema> = {};
  blockPlugins: Record<string, NodeFromPlugin> = {};
  members: Set<string> = new Set();

  constructor(
    public dsl: BlockSchema,
    public dependencies: Dependencie[],
  ) {
    this.collectLibrary();
    this.walk(dsl);
  }

  private walk(dsl: BlockSchema) {
    this.walkNodes(dsl);
    this.collectContext(dsl);
    this.collectStyle(dsl);
  }

  private walkNodes(dsl: BlockSchema) {
    if (dsl.nodes) {
      dsl.nodes.forEach((node) => {
        this.collectUrlSchema(node);
        this.collectBlockPlugin(node);
        if (node.children) {
          if (Array.isArray(node.children)) {
            node.children.forEach((child) => {
              this.walkNodes({ nodes: [child] } as BlockSchema);
            });
          } else if (typeof node.children !== "string") {
            // 处理 JSExpression 子节点
          }
        }
      });
    }
  }
}

收集器使用正则表达式模式(例如 this.$libs.ElementPlus.ElButton)识别外部库引用,提取库名称和特定组件路径。这些引用被存储以供后续生成导入语句使用,同时从生成的代码中移除以防止运行时错误。

解析阶段

解析器将 DSL 元素转换为统一的 token 结构,作为模板编译的基础。每个 DSL 方面------状态、props、方法、事件、指令------都有专门的解析器函数。

flowchart TD subgraph DSL[BlockSchema] S[State] P[Props] M[Methods] C[Computed] W[Watch] L[LifeCycles] E[Events] N[Nodes] end subgraph Parsers[解析器函数] PS[parseState] PP[parseProps] PF[parseFunctions] PW[parseWatch] PE[parseEvents] PT[parseTemplate] end subgraph Tokens[Token 对象] TS[state] TP[props] TM[methods] TC[computed] TW[watch] TL[lifeCycles] TE[events] TT[template] TI[imports] TCOM[components] end S --> PS P --> PP M --> PF C --> PF W --> PW L --> PF E --> PE N --> PT PS --> TS PP --> TP PF --> TM PF --> TC PW --> TW PF --> TL PE --> TE PT --> TT PT --> TI PT --> TCOM

解析器入口点协调这些单独的解析器:

typescript 复制代码
export function parser(
  collecter: Collecter,
  componentMap: Map<string, MaterialDescription>,
  platform: PlatformType = "web",
): Token {
  const { dsl } = collecter;
  const computedKeys = Object.keys(dsl.computed || {});
  const lifeCycles = parseFunctionMap(dsl.lifeCycles, computedKeys);
  const computed = parseFunctionMap(dsl.computed, computedKeys);
  const watch = parseWatch(dsl.watch, computedKeys);
  const dataSources = parseDataSources(dsl.dataSources);

  const { methods, nodes, components, importBlocks, directives } =
    parseTemplate(
      dsl.nodes || [],
      componentMap,
      computedKeys,
      collecter.context,
    );
  const mergeComputed = [...computed, ...watch.computed];

  const mergeMethods = parseFunctionMap(
    {
      ...methods,
      ...(dsl.methods || {}),
    },
    computedKeys,
  );

  const blocksImport = importBlocks.map((n: any) => {
    return `import ${n.name} from './${n.id}.vue';`;
  });

  let { imports, uniComponents } = parseImports(
    componentMap,
    components,
    blocksImport,
    collecter.imports,
    platform,
  );

  const asyncComponents = Object.keys({
    ...collecter.urlSchemas,
    ...collecter.blockPlugins,
  });

  const urlSchemas = parseUrlSchemas(collecter.urlSchemas);
  const blockPlugins = parseBlockPlugins(collecter.blockPlugins);

  return {
    id: dsl.id as string,
    version: dsl.__VERSION__ as string,
    name: dsl.name,
    state: parseState(dsl.state).join(","),
    inject: parseInject(dsl.inject).join(","),
    props: parseProps(dsl.props).join(","),
    emits: parseEmits(dsl.emits).join(","),
    expose:
      dsl.expose && dsl.expose.length ? JSON.stringify(dsl.expose || []) : "",
    watch: watch.watches.join(","),
    lifeCycles: lifeCycles.join(","),
    computed: mergeComputed.join(","),
    methods: [...dataSources, ...mergeMethods].join(","),
    imports: "\n" + imports.join("\n"),
    components: skipUniComponents(components, uniComponents).join(","),
    directives: directives.join(","),
    returns: collecter.members.join(","),
    template: nodes.join("\n"),
    css: dsl.css || "",
    style: parseStyle(collecter.style),
    urlSchemas: urlSchemas.join("\n"),
    blockPlugins: blockPlugins.join("\n"),
    asyncComponents: asyncComponents.join(","),
    uniComponents,
    renderer: platform === "uniapp" ? "@vtj/uni-app" : "@vtj/renderer",
  };
}

模板解析

模板解析是最复杂的转换,它将节点模式转换为 Vue 模板语法,同时处理 props、事件、指令和嵌套子节点。

typescript 复制代码
export function parseTemplate(
  children: NodeSchema[],
  componentMap: Map<string, MaterialDescription>,
  computedKeys: string[] = [],
  context: Record<string, Set<string>> = {},
  parent?: NodeSchema,
) {
  const nodes: string[] = [];
  let methods: Record<string, JSFunction> = {};
  let components: string[] = [];
  const defineDirectives: string[] = [];
  let importBlocks: { id: string; name: string }[] = [];
  const slots = groupBySlot(children);
  slots.forEach((item) => {
    const contents: string[] = [];
    for (const child of item.children) {
      let { id, name, invisible, from } = child;
      if (invisible) {
        continue;
      }
      // 收集组件名称
      const component = getComponentName(name, componentMap, from);
      if (component) {
        components.push(component);
      }

      // 收集块引用
      if (isFromSchema(from)) {
        importBlocks.push({ id: from.id, name });
      }

      // 收集 props 和 events
      const { props, events, handlers } = parsePropsAndEvents(
        child,
        id as string,
        child.props,
        child.events,
        context,
        computedKeys,
      );

      const directives = parseDirectives(
        child.directives,
        computedKeys,
        defineDirectives,
      ).join(" ");

      const nodeChildren = child.children
        ? parseNodeChildren(
            child.children,
            computedKeys,
            componentMap,
            context,
            child,
          )
        : "";
      Object.assign(methods, handlers);

      let childContent = "";
      if (typeof nodeChildren === "string") {
        childContent = nodeChildren;
      } else {
        childContent = (nodeChildren?.nodes || []).join("\n");
        Object.assign(methods, nodeChildren?.methods || {});
        components = components.concat(nodeChildren?.components || []);
        importBlocks = importBlocks.concat(nodeChildren?.importBlocks || []);
      }

      const tagName = ["@dcloudio/uni-h5", "@dcloudio/uni-ui"].includes(
        (from || componentMap.get(name)?.package) as string,
      )
        ? kebabCase(name)
        : isFromUrlSchema(from) || isFromPlugin(from)
          ? "component"
          : name;
      contents.push(
        NO_END_TAGS.includes(tagName)
          ? `<${tagName} ${directives} ${props} ${events} />`
          : `<${tagName} ${directives} ${props} ${events}>${childContent ? "\n" + childContent.trim() : ""}</${tagName}>`,
      );
    }
    const node = wrapSlot(item.slot, contents.join("\n"), parent?.id);
    nodes.push(node);
  });

  return {
    nodes,
    methods,
    directives: directivesRegister(defineDirectives),
    components: dedupArray(components) as string[],
    importBlocks: dedupArray<{ id: string; name: string }>(importBlocks, "id"),
  };
}

模板解析器会自动处理来自 @dcloudio 包的 uniapp 组件的组件名称短横线命名转换,同时为标准 web 组件保留大驼峰命名。这确保了与特定于平台的命名约定的兼容性。

事件和指令处理

事件在转换时具有上下文感知能力,当表达式直接引用 this.* 时生成内联处理程序,或者在需要上下文数据时创建包装方法:

typescript 复制代码
function bindNodeEvents(
  id: string,
  events: NodeEvents = {},
  context: Record<string, Set<string>> = {},
) {
  const handlers: Record<string, JSFunction> = {};
  const nodeContext = Array.from(context[id] || new Set([]));
  const eventParams = nodeContext.length
    ? `({${nodeContext.join(", ")}}, args)`
    : "";
  const binders = Object.entries(events).map(([name, value]) => {
    const isExp = value.handler.value.startsWith("this.");
    const binder = isExp
      ? replaceThis(value.handler.value)
      : `${camelCase(name)}_${id}${eventParams}`;
    if (!isExp) {
      handlers[binder] = nodeContext.length
        ? {
            type: "JSFunction",
            value: `{
          return (${value.handler.value}).apply(this, args);
        }`,
          }
        : value.handler;
    }
    return bindEvent(name, value, binder, nodeContext, isExp);
  });
  return {
    binders,
    handlers,
  };
}

指令会被自动解析和注册,内置指令将得到特殊处理:

typescript 复制代码
export const BUILT_IN_DIRECTIVES = [
  "show",
  "if",
  "else-if",
  "else",
  "for",
  "model",
  "on",
  "bind",
  "text",
  "html",
  "once",
  "pre",
  "cloak",
  "slot",
  "is",
];

来源: template.ts

状态和 Props 转换

状态属性被转换为 Vue 3 响应式引用,并自动移除 this. 前缀以使代码更整洁:

typescript 复制代码
export function parseState(state: BlockState = {}) {
  return Object.entries(state).map(([key, val]) => {
    const value = parseValue(val);
    return `${key}: ref(${value})`;
  });
}

Props 被转换为带有类型推断的 Vue 组件 prop 定义:

typescript 复制代码
export function parseProps(props: Array<string | BlockProp> = []) {
  return toArray(props).map((item) => {
    const prop = typeof item === "string" ? { name: item } : item;
    const { name, defaultValue, type = "String", required = false } = prop;
    const defVal = parseValue(defaultValue, true, false);
    const result = [`${name}: { type: ${type}, required: ${required} }`];
    if (defaultValue !== undefined) {
      result[0] = `${name}: { type: ${type}, required: ${required}, default: ${defVal} }`;
    }
    return result.join(",");
  });
}

模板编译阶段

生成的 token 对象被注入到模板字符串中,以生成最终的脚本和 Vue 组件结构。

脚本模板

typescript 复制代码
const scriptTemplate = `
// @ts-nocheck

<%= imports %>
import { useProvider } from '<%= renderer %>';
export default defineComponent({
  name: '<%= name %>', 
  <% if(inject) { %> inject: { <%= inject %>}, <% } %>
  <% if(components) { %> components: { <%= components %> }, <% } %>
  <% if(directives) { %> directives: { <%= directives %> }, <% } %>
  props: { <%= props %> },
  emits: [<%= emits %>],
  <% if(expose) { %> expose: <%= expose %>, <% } %>
  setup(props, { expose }) {
    const { state, methods, computed, lifeCycles, watch } = useProvider();
    const { <%= returns %> } = methods;
    return {
      <%= returns %>,
      ...toRefs(state)
    };
  }
});
`;

Vue 模板

typescript 复制代码
const vueTemplate = `
<template>
<%= template %>
</template>
<script lang="<%= scriptLang %>">
<%= script %>
</script>
<style lang="<%= styleLang %>" scoped>
<%= css %>
<%= style %>
</style>
`;

模板编译使用 @vtj/base 的模板函数进行高效的字符串插值,并进行正确的转义。

代码格式化阶段

最后阶段应用 Prettier 格式化以确保所有生成文件的代码风格一致。单独的格式化器处理不同的代码类型:

typescript 复制代码
export async function vueFormatter(content: string, disabled?: boolean) {
  if (disabled) return content;
  return format(content, {
    ...prettierOptions,
    parser: "vue",
    plugins: [htmlParser, babelParser, cssParser, estree],
  });
}

export async function tsFormatter(content: string, disabled?: boolean) {
  if (disabled) return content;
  return format(content, {
    ...prettierOptions,
    parser: "babel-ts",
    plugins: [babelParser, estree],
  });
}

export async function cssFormatter(content: string, disabled?: boolean) {
  if (disabled) return content;
  return format(content, {
    ...prettierOptions,
    parser: "css",
    plugins: [cssParser],
  });
}

格式化器选项强制执行一致的格式化规则:

  • 箭头函数括号:始终
  • 括号间距:启用
  • 行宽:80 个字符
  • 单引号:JSX 启用
  • 行尾:LF

DSL 到 Vue 映射

转换遵循 DSL 模式元素与 Vue 组件结构之间的系统映射规则:

DSL 元素 Vue 输出 示例转换
state 属性 ref() 响应式引用 list: { type: 'JSExpression', value: '[]' }list: ref([])
computed 计算属性 total: { type: 'JSFunction', value: '() => this.state.count * 2' }total: computed(() => state.count * 2)
methods 方法对象 fetchData: { type: 'JSFunction', value: 'async () => {}' }fetchData: async () => {}
lifeCycles 生命周期钩子 mounted: { type: 'JSFunction', value: '() => {}' }mounted: () => {}
nodes Vue 模板 HTML 将节点树转换为类 JSX 的 Vue 模板语法
events 事件处理程序 @click 绑定与包装方法以进行上下文访问
directives Vue 指令 v-if, v-for, v-model 带有正确的语法
props 组件 props props: { title: { type: String, required: true } }
watch 监听选项 watch: { count: { immediate: true, deep: false, handler: '() => {}' } }

特定于平台的变体

生成器支持三个平台,并具有特定于平台的适配:

Web 平台

  • 使用 @vtj/renderer 作为运行时提供程序
  • 保留大驼峰组件名称
  • 完整的 Element Plus、Ant Design Vue 和 Vant 支持
  • 启用 CSS 作用域样式

H5 平台

  • 移动端优化的组件选择
  • 触摸事件处理优化
  • 响应式视口配置

UniApp 平台

  • 使用 @vtj/uni-app 作为运行时提供程序
  • uni-app 组件的短横线命名转换
  • 特定于平台的条件导入
  • 支持使用 rpx 单位进行响应式布局

平台检测发生在生成时,而不是运行时,确保仅包含平台相关的导入和依赖项,从而优化包大小。

值转换的实用函数

转换依赖于处理不同值类型和上下文替换的实用函数:

typescript 复制代码
export function parseValue(
  val: unknown,
  stringify: boolean = true,
  noThis: boolean = true,
  computedKeys: string[] = [],
) {
  let value = isJSCode(val)
    ? val.value.trim().replace(/;$/, "")
    : stringify
      ? JSON.stringify(val)
      : val;
  value = replaceComputedValue(value as string, computedKeys);
  return noThis
    ? replaceThis(replaceContext(value as string))
    : replaceContext(value as string);
}

export function replaceThis(content: string) {
  return content.replace(new RegExp("this.", "g"), "");
}

export function replaceContext(content: string) {
  return content.replace(/this\.context\??\./g, "");
}

export function replaceComputedValue(
  content: string = "",
  keys: string[] = [],
) {
  let result = content;
  for (const key of keys) {
    result = result.replace(
      new RegExp(`this.${key}.value`, "g"),
      `this.${key}`,
    );
  }
  return result;
}

完整生成工作流

sequenceDiagram participant User participant Gen as "generator()" participant Coll as Collecter participant Pars as "parser()" participant Temp as templates participant Fmt as formatters User ->> Gen: DSL + componentMap + dependencies Gen ->> Coll: new Collecter(dsl, dependencies) Coll -->> Coll: 遍历 DSL 树 Coll -->> Coll: 收集导入和上下文 Coll -->> Gen: Collecter 实例 Gen ->> Pars: "parser(collecter, componentMap)" Pars ->> Pars: "parseState()" Pars ->> Pars: "parseTemplate()" Pars ->> Pars: "parsePropsAndEvents()" Pars ->> Pars: "parseImports()" Pars -->> Gen: Token 对象 Gen ->> Temp: "scriptCompiled(token)" Temp -->> Gen: 脚本字符串 Gen ->> Temp: "vueCompiled(template + script + css)" Temp -->> Gen: Vue 模板字符串 Gen ->> Fmt: "tsFormatter()" Gen ->> Fmt: "cssFormatter()" Gen ->> Fmt: "vueFormatter()" Fmt -->> Gen: 格式化的 Vue 组件 Gen -->> User: 完整的 .vue 文件

导入管理

系统根据组件使用情况和依赖项智能地管理导入:

typescript 复制代码
export function parseImports(
  componentMap: Map<string, MaterialDescription>,
  components: string[] = [],
  importBlocks: string[] = [],
  collectImports: Record<string, Set<string>> = {},
  platform: PlatformType = "web",
) {
  const imports: string[] = [];

  // 处理来自收集器的库导入
  for (const [library, members] of Object.entries(collectImports)) {
    if (library === "Vue") continue;
    const memberArray = Array.from(members);
    if (memberArray.length > 0) {
      imports.push(`import { ${memberArray.join(", ")} } from '${library}';`);
    }
  }

  // 处理组件导入
  components.forEach((name) => {
    const material = componentMap.get(name);
    if (material) {
      const { library, package: pkg } = material;
      if (library) {
        imports.push(`import { ${name} } from '${library}';`);
      } else if (pkg && pkg.startsWith("uni-")) {
        // UniApp 组件使用运行时注册
      } else {
        imports.push(`import { ${name} } from '${pkg}';`);
      }
    }
  });

  // 添加块导入
  imports.push(...importBlocks);

  return { imports, uniComponents: [] };
}

配置选项

GeneratorOptions 接口提供对代码生成的全面控制:

选项 类型 默认值 描述
dsl BlockSchema 必需 用于生成代码的 DSL 模式
componentMap Map<string, MaterialDescription> new Map() 用于导入解析的组件元数据
dependencies Dependencie[] [] 用于库导入的外部依赖项
platform PlatformType 'web' 目标平台:'web'、'h5' 或 'uniapp'
formatterDisabled boolean false 跳过 Prettier 格式化以加快生成速度
ts boolean true 生成 TypeScript 代码(JavaScript 则为 false)
scss boolean false 使用 SCSS 作为样式(CSS 则为 false)

错误处理和验证

生成器在每个阶段都包含内置的错误处理。格式化阶段在保留生成内容用于调试的同时捕获错误:

typescript 复制代码
return await vueFormatter(vue, formatterDisabled).catch((e) => {
  e.content = vue;
  return Promise.reject(e);
});

这确保了即使格式化失败(例如由于语法无效),未格式化的生成代码也可用于检查和调试。

与设计器集成

代码生成系统旨在与 VTJ 的可视化设计器无缝集成。当设计器在可视化编辑器中保存更改时,DSL 会自动重新生成,并可作为生产就绪的 Vue 组件导出。这种往返工作流实现了:

  • 具有即时代码生成的可视化开发
  • 与设计器同步的手动代码编辑
  • 通过版本控制的 DSL 文件进行团队协作
  • 从单个 DSL 源生成多个目标

有关从 Vue 源代码到 DSL 的反向转换的更多信息,请参阅 Vue 源代码到 DSL 解析。

高级用法

自定义组件映射集成

要在生成的代码中使用自定义组件,请提供具有正确元数据的组件映射:

typescript 复制代码
const componentMap = new Map([
  [
    "MyButton",
    {
      name: "MyButton",
      package: "@my-company/ui",
      library: "MyUI",
      categoryId: "components",
      props: [{ name: "label", label: "Label", setters: "InputSetter" }],
    },
  ],
]);

const code = await generator({
  dsl: blockSchema,
  componentMap,
  platform: "web",
});

处理数据源

DSL 中定义的数据源被转换为响应式数据获取方法:

typescript 复制代码
export const dataSources = {
  userApi: {
    type: "JSFunction",
    value: `async () => {
      return await fetch('/api/user').then(res => res.json());
    }`,
  },
};

生成为:

typescript 复制代码
const userApi: () => Promise<any> = async () => {
  return await fetch("/api/user").then((res) => res.json());
};

监听配置

监听选项在转换时会正确处理 deep 和 immediate:

typescript 复制代码
export const watch = {
  count: {
    type: "JSFunction",
    value: "(val, oldVal) => { console.log(val, oldVal); }",
    deep: false,
    immediate: true,
  },
};

DSL 到 Vue 代码生成系统提供了一个强大、生产就绪的转换管道,在保持设计器意图的同时生成干净、可维护的 Vue 代码。模块化架构允许进行特定于平台的调整,并易于扩展以满足自定义需求。

后续步骤

  • 探索 Vue 源代码到 DSL 解析 以了解反向转换
  • 查看 模板编译和 AST 转换 以获取更深入的编译详细信息
  • 了解关于 内置组件库 的组件集成
  • 查看 项目结构和文件组织 以了解生成输出的位置

参考资料

开源项目仓库:gitee.com/newgateway/...

相关推荐
前端老兵AI1 小时前
React vs Vue 2026年怎么选?9年前端的真实建议
vue.js·react.js
Wect1 小时前
React 中的双缓存 Fiber 树机制
前端·react.js·面试
天才熊猫君1 小时前
Vue 3 中 Watch 的陷阱:为什么异步操作后创建的监听会泄漏?
前端·javascript
梵得儿SHI1 小时前
Vue3 生态工具实战进阶:API 请求封装 + 样式解决方案全攻略(Axios/Sass/CSS Modules)
前端·css·vue3·sass·api请求·样式解决方案·组合式api管理
有梦想的咸鱼还是咸鱼吗1 小时前
前端必会|防抖与节流从原理到实战,解决90%高频事件卡顿问题
前端
阿诺木1 小时前
Node.js 局域网设备发现:mDNS、UDP 广播和子网扫描
前端
盐焗乳鸽还要砂锅1 小时前
亲手造一只有灵魂的 AI 小龙虾是种什么体验?
前端·llm·agent
猫头虎1 小时前
Docker 安装 OpenClaw 报错排查完全手册(续):如何解决pairing required,`EACCES: permission denied`Docker 拉取镜像提示 `denied
运维·docker·容器·开源·github·aigc·ai编程
YimWu1 小时前
Opencode 核心设计-Session会话机制
前端·agent·ai编程