vue3 源码学习,实现一个 mini-vue(十四):构建 compile 编译器(上)

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: vue3 源码学习,实现一个 mini-vue(十四):构建 compile 编译器(上)

前言

原文来自我的个人博客

接下来的几章中我们将要在 mini-vue 中 实现一个自己的编译器,在上一章我们了解了 compiler 的作用和大致流程。

它主要经历了以下三个大的步骤:

  1. 解析( parse ) template 模板,生成 AST
  2. 转化(transformAST,得到 JavaScript AST
  3. 生成(generaterender 函数

那么本章我们就先来实现编译器的第一步:依据模板生成 AST 抽象语法树

ps: compiler 是一个非常复杂的概念,我们不会实现一个完善的编译器,而是会像之前一样严格遵循: 没有使用就当做不存在 和  最少代码的实现逻辑 这两个标准。只关注  核心 和  当前业务 相关的内容,而忽略其他。

1. 扩展知识:JavaScript与有限自动状态机

想要实现 compiler 第一步是构建 AST 对象。那么想要构建 AST,就需要利用到 有限状态机 的概念。

有限状态机也被叫做 有限自动状态机,表示:有限个状态以及在这些状态之间的转移和动作等行为的数学计算模型)

光看概念,可能难以理解,那么下面我们来看一个具体的例子:

根据 packages/compiler-core/src/compile.ts 中的代码可知,ast 对象的生成是通过 baseParse 方法得到的。

const ast = isString(template) ? baseParse(template, options) : template
AI 代码解读

而对于 baseParse 方法而言,它接收一个 template 作为参数,返回一个 ast 对象。

即:通过 parse 方法,解析 template,得到 ast 对象。  中间解析的过程,就需要使用到 有限自动状态机。

比如,vue 想要把如下模板解析成 AST,那么就需要利用有限自动状态机对该模板进行分析

<div>hello world</div>
AI 代码解读

分析的过程中主要包含了三个特性:摘自阮一峰:JavaScript与有限状态机

  1. 状态总数(state)是有限的。

    1. 初始状态
    2. 标签开始状态
    3. 标签名称状态
    4. 文本状态
    5. 结束标签状态
    6. 结束标签名称状态
    7. ...
  2. 任一时刻,只处在一种状态之中。
  3. 某种条件下,会从一种状态转变(transition)到另一种状态。

如下图所示:
image.png

  1. 解析 <:由 初始状态 进入 标签开始状态
  2. 解析 div:由 标签开始状态 进入 标签名称状态
  3. 解析 >:由 标签名称状态 进入 初始状态
  4. 解析 hello world:由 初始状态 进入 文本状态
  5. 解析 <:由 文本状态 进入 标签开始状态
  6. 解析 /:由 标签开始状态 进入 结束标签状态
  7. 解析 div:由 结束标签状态 进入 结束标签名称状态
  8. 解析 >:由 结束标签名称状态 进入 初始状态

经过这样一些列的解析,对于:

<div>hello world</div>
AI 代码解读

而言,我们将得到三个 token

开始标签:<div>
文本节点:hello world
结束标签:</div>
AI 代码解读

而这样一个利用有限自动状态机的状态迁移,来获取 tokens 的过程,可以叫做:对模板的标记化

总结

这一小节,我们了解了什么是有限自动状态机,也知道了它的三个特性。

vue 利用它来实现了对模板的标记化,得到了对应的 token

关于这些 token 有什么用,我们下一小节说。

2. 扩展知识:扫描 tokens 构建 AST 结构的方案

在上一小节中,我们已经知道可以通过自动状态机解析模板为 tokens,而解析出来的 tokens 就是生成 AST 的关键。

生成 AST 的过程,就是 tokens 扫描的过程

我们以以下 html 结构为例:

<div>
  <p>hello</p>  
  <p>world</p>  
</div>
AI 代码解读

该 html 可以被解析为如下 tokens

开始标签:<div>
开始标签:<p>
文本节点:hello
结束标签:</p>
开始标签:<p>
文本节点:world
结束标签:</p>
结束标签:</div>
AI 代码解读

具体的扫描过程为:

1. 初始状态:

image.png

2. 扫描开始标签 <div>,在 AST 下 会生成 Element<div>

image.png

3. <p>hello</p> 标签依次入栈, AST 生成对应 Element<p>

image.png

  1. <p>hello</p> 标签依次出栈,<p>world</p> 标签依次入栈,AST 生成对应 Element<p>

image.png

5. </div>结束标签入栈再与<div>开始标签组合依次出栈。
image.png

6. 结束状态

image.png

在以上的图示中,我们通过 递归下降算法? 这样的一种扫描形式把 tokens 通过  解析成了 AST(抽象语法树)

3. 源码阅读:依据模板生成 AST 抽象语法树

前面做了这么多铺垫,我们中行与可以来看一下 vue 中生成 AST 的代码了。vue 的这部分代码全部放在了 packages/compiler-core/src/parse.ts 中:

image.png

从这个文件可以看出,整体 parse 的逻辑非常复杂,整体文件有 1175 行代码。

通过 packages/compiler-core/src/compile.ts 中的 baseCompile 方法可以知道,整个 parse 的过程是从 baseParse 开始的,所以我们可以直接从这个方法开始进行 debugger

创建测试实例:

<script>
  const { compile, h, render } = Vue
  // 创建 template
  const template = `<div>hello world</div>`

  // 生成 render 函数
  const renderFn = compile(template)
</script>
AI 代码解读

当前的 template 对应的目标极简 AST 为(我们不再关注其他的属性生成):

const ast = {
  "type": 0,
  "children": [
    {
      "type": 1,
      "tag": "div",
      "tagType": 0,
      // 属性,目前我们没有做任何处理。但是需要添加上,否则,生成的 ats 放到 vue 源码中会抛出错误
      "props": [],
      "children": [{ "type": 2, "content": " hello world " }]
    }
  ],
  // loc:位置,这个属性并不影响渲染,但是它必须存在,否则会报错。所以我们给了他一个 {}
  "loc": {}
}
AI 代码解读

模板解析的 token 流程为(以 <div>hello world</div> 为例):

1. <div
2. >
3. hello world
4. </div
5. >
AI 代码解读

明确好以上内容之后,我们开始。Here we go

  1. 进入 baseParse 方法:

image.png

  1. 上图描述的应该比较详细了,在 baseParse 方法中,程序首先会进入 createParserContext 方法中,这个方法会返回一个 ParserContext 类型的对象,这个对象比较复杂,我们只需要关注 source(模板源代码)属性即可
  2. 此时 context.source = "<div> hello world </div>",程序接着执行:

image.png

  1. createParserContext 方法跳出后,程序接着执行 getCursor 方法,这个方法主要获取 loc(即:location 位置),与我们的极简 `AST 无关,无需关注,接着调试:

image.png

  1. 接着,程序会先后执行 parseChildrengetSelectioncreateRoot 三个方法,最后返回 ast
  2. 执行 parseChildren 方法(解析子节点) ,这个方法 非常重要,是生成 AST 的核心方法:

image.png

  1. parseChildren 方法中的核心逻辑都在 while 循环中,它的目的就是循环解析模板数据,生成 AST 中的 children 的,最后生成的 nodes:(关于 while 循环中的逻辑是很复杂的,碍于篇幅这里做了点省略,具体逻辑可以参考我下面的框架实现)

image.png

  1. 接下来执行 getSelection 方法:

image.png

  1. 这个函数非常简单,也不是那么重要,我们只要知道是关于 location 的生成就行了。
  2. 最后,执行 createRoot 方法,这个方法也非常简单,就是整合了前两个函数生成的 childrenloc 成一个 RootNode 对象(最后的 AST)并返回

image.png

4. 框架实现

AST 对象的生成颇为复杂,我们把整个过程分为成三步进行处理。

  1. 构建 parse 方法,生成 context 实例
  2. 构建 parseChildren ,处理所有子节点(最复杂

    1. 构建有限自动状态机解析模板
    2. 扫描 token 生成 AST 结构
  3. 生成 AST,构建测试

4.1 构建 parse 函数,生成 context 实例

  1. 创建 packages/compiler-core/src/compile.ts 模块,写入如下代码:
export function baseCompile(template: string, options) {
  return {}
}
AI 代码解读
  1. 创建 packages/compiler-dom/src/index.ts 模块,导出 compile 方法:
import { baseCompile } from 'packages/compiler-core/src/compile'

export function compile(template: string, options) {
  return baseCompile(template, options)
}
AI 代码解读
  1. 在 packages/vue/src/index.ts 中,导出 compile 方法:
export { render, compile } from '@vue/runtime-dom'
AI 代码解读
  1. 创建 packages/compiler-core/src/parse.ts 模块下创建 baseParse 方法:
/**
 * 基础的 parse 方法,生成 AST
 * @param content tempalte 模板
 * @returns
 */
export function baseParse(content: string) {
  return {}
}
AI 代码解读
  1. 在 packages/compiler-core/src/compile.ts 模块下的 baseCompile 中,使用 baseParse 方法:
import { baseParse } from './parse'

export function baseCompile(template: string, options) {
  const ast = baseParse(template)
  console.log(JSON.stringify(ast))

  return {}
}
AI 代码解读

至此,我们就成功的触发了 baseParse。接下来我们去生成 context 上下文对象。

  1. 在 packages/compiler-core/src/parse.ts 中创建 createParserContext 方法,用来生成上下文对象:
/**
 * 创建解析器上下文
 */
function createParserContext(content: string): ParserContext {
  // 合成 context 上下文对象
  return {
    source: content
  }
}
AI 代码解读
  1. 创建 ParserContext 接口:
/**
 * 解析器上下文
 */
export interface ParserContext {
  // 模板数据源
  source: string
}
AI 代码解读
  1. 在 baseParse 中触发该方法:
export function baseParse(content: string) {
  // 创建 parser 对象,未解析器的上下文对象
  const context = createParserContext(content)
  console.log(context)
  return {}
}
AI 代码解读

至此我们成功得到了 context 上下文对象。

创建测试实例 packages/vue/examples/compiler/compiler-ast.html

<script>
  const { compile } = Vue
  // 创建 template
  const template = `<div> hello world </div>`

  // 生成 render 函数
  const renderFn = compile(template)
</script>
AI 代码解读

可以成功打印 context

image.png

4.2 构建有限自动状态机解析模板,扫描 token 生成 AST 结构

接下来我们通过 parseChildren 方法处理所有的子节点,整个处理的过程分为两大块:

  1. 构建有限自动状态机解析模板
  2. 扫描 token 生成 AST 结构

接下来我们来进行实现:

  1. 创建 parseChildren 方法:
/**
 * 解析子节点
 * @param context 上下文
 * @param mode 文本模型
 * @param ancestors 祖先节点
 * @returns
 */
function parseChildren(context: ParserContext, ancestors) {
  // 存放所有 node节点数据的数组
  const nodes = []

  /**
   * 循环解析所有 node 节点,可以理解为对 token 的处理。
   * 例如:<div>hello world</div>,此时的处理顺序为:
   * 1. <div
   * 2. >
   * 3. hello world
   * 4. </
   * 5. div>
   */
  while (!isEnd(context, ancestors)) {
    /**
     * 模板源
     */
    const s = context.source
    // 定义 node 节点
    let node

    if (startsWith(s, '{{')) {
    }
    // < 意味着一个标签的开始
    else if (s[0] === '<') {
      // 以 < 开始,后面跟a-z 表示,这是一个标签的开始
      if (/[a-z]/i.test(s[1])) {
        // 此时要处理 Element
        node = parseElement(context, ancestors)
      }
    }

    // node 不存在意味着上面的两个 if 都没有进入,那么我们就认为此时的 token 为文本节点
    if (!node) {
      node = parseText(context)
    }

    pushNode(nodes, node)
  }

  return nodes
}
AI 代码解读
  1. 以上代码中涉及到了 个方法:

    1. isEnd:判断是否为结束节点
    2. startsWith:判断是否以指定文本开头
    3. pushNode:为 array 执行 push 方法
    4. 复杂: parseElement:解析 element
    5. 复杂: parseText:解析 text
  2. 我们先实现前三个简单方法:
  3. 创建 startsWith 方法:
/**
 * 是否以指定文本开头
 */
function startsWith(source: string, searchString: string): boolean {
  return source.startsWith(searchString)
}
AI 代码解读
  1. 创建 isEnd 方法:
/**
 * 判断是否为结束节点
 */
function isEnd(context: ParserContext, ancestors): boolean {
  const s = context.source

  // 解析是否为结束标签
  if (startsWith(s, '</')) {
    for (let i = ancestors.length - 1; i >= 0; --i) {
      if (startsWithEndTagOpen(s, ancestors[i].tag)) {
        return true
      }
    }
  }
  return !s
}

/**
 * 判断当前是否为《标签结束的开始》。比如 </div> 就是 div 标签结束的开始
 * @param source 模板。例如:</div>
 * @param tag 标签。例如:div
 * @returns
 */
function startsWithEndTagOpen(source: string, tag: string): boolean {
  return (
    startsWith(source, '</') &&
    source.slice(2, 2 + tag.length).toLowerCase() === tag.toLowerCase() &&
    /[\t\r\n\f />]/.test(source[2 + tag.length] || '>')
  )
}
AI 代码解读
  1. 创建 pushNode 方法:
/**
 * nodes.push(node)
 */
function pushNode(nodes, node): void {
  nodes.push(node)
}
AI 代码解读

至此三个简单的方法都被构建完成。

接下来我们来处理 parseElement,在处理的过程中,我们需要使用到 NodeTypes 和 ElementTypes 这两个 enum 对象,所以我们需要先构建它们(直接从 vue 源码复制):

  1. 创建 packages/compiler-core/src/ast.ts 模块:
/**
 * 节点类型(我们这里复制了所有的节点类型,但是我们实际上只用到了极少的部分)
 */
export const enum NodeTypes {
  ROOT,
  ELEMENT,
  TEXT,
  COMMENT,
  SIMPLE_EXPRESSION,
  INTERPOLATION,
  ATTRIBUTE,
  DIRECTIVE,
  // containers
  COMPOUND_EXPRESSION,
  IF,
  IF_BRANCH,
  FOR,
  TEXT_CALL,
  // codegen
  VNODE_CALL,
  JS_CALL_EXPRESSION,
  JS_OBJECT_EXPRESSION,
  JS_PROPERTY,
  JS_ARRAY_EXPRESSION,
  JS_FUNCTION_EXPRESSION,
  JS_CONDITIONAL_EXPRESSION,
  JS_CACHE_EXPRESSION,

  // ssr codegen
  JS_BLOCK_STATEMENT,
  JS_TEMPLATE_LITERAL,
  JS_IF_STATEMENT,
  JS_ASSIGNMENT_EXPRESSION,
  JS_SEQUENCE_EXPRESSION,
  JS_RETURN_STATEMENT
}

/**
 * Element 标签类型
 */
export const enum ElementTypes {
  /**
   * element,例如:<div>
   */
  ELEMENT,
  /**
   * 组件
   */
  COMPONENT,
  /**
   * 插槽
   */
  SLOT,
  /**
   * template
   */
  TEMPLATE
}
AI 代码解读

下面就可以构建 parseElement 方法了 ,该方法的作用主要为了解析 Element 元素:

  1. 创建 parseElement
/**
 * 解析 Element 元素。例如:<div>
 */
function parseElement(context: ParserContext, ancestors) {
  // -- 先处理开始标签 --
  const element = parseTag(context, TagType.Start)

  //  -- 处理子节点 --
  ancestors.push(element)
  // 递归触发 parseChildren
  const children = parseChildren(context, ancestors)
  ancestors.pop()
  // 为子节点赋值
  element.children = children

  //  -- 最后处理结束标签 --
  if (startsWithEndTagOpen(context.source, element.tag)) {
    parseTag(context, TagType.End)
  }

  // 整个标签处理完成
  return element
}
AI 代码解读
  1. 构建 TagType enum
/**
 * 标签类型,包含:开始和结束
 */
const enum TagType {
  Start,
  End
}
AI 代码解读
  1. 处理开始标签,构建 parseTag
/**
 * 解析标签
 */
function parseTag(context: any, type: TagType): any {
  // -- 处理标签开始部分 --

  // 通过正则获取标签名
  const match: any = /^<\/?([a-z][^\r\n\t\f />]*)/i.exec(context.source)
  // 标签名字
  const tag = match[1]

  // 对模板进行解析处理
  advanceBy(context, match[0].length)

  // -- 处理标签结束部分 --

  // 判断是否为自关闭标签,例如 <img />
  let isSelfClosing = startsWith(context.source, '/>')
  // 《继续》对模板进行解析处理,是自动标签则处理两个字符 /> ,不是则处理一个字符 >
  advanceBy(context, isSelfClosing ? 2 : 1)

  // 标签类型
  let tagType = ElementTypes.ELEMENT

  return {
    type: NodeTypes.ELEMENT,
    tag,
    tagType,
    // 属性,目前我们没有做任何处理。但是需要添加上,否则,生成的 ats 放到 vue 源码中会抛出错误
    props: []
  }
}
AI 代码解读
  1. 解析标签的过程,其实就是一个自动状态机不断读取的过程,我们需要构建 advanceBy 方法,来标记进入下一步:
/**
 * 前进一步。多次调用,每次调用都会处理一部分的模板内容
 * 以 <div>hello world</div> 为例
 * 1. <div
 * 2. >
 * 3. hello world
 * 4. </div
 * 5. >
 */
function advanceBy(context: ParserContext, numberOfCharacters: number): void {
  // template 模板源
  const { source } = context
  // 去除开始部分的无效数据
  context.source = source.slice(numberOfCharacters)
}
AI 代码解读

至此 parseElement 构建完成。此处的代码虽然不多,但是逻辑非常复杂。在解析的过程中,会再次触发 parseChildren,这次触发表示触发 文本解析,所以下面我们要处理 parseText 方法。

  1. 创建 parseText 方法,解析文本:
/**
 * 解析文本。
 */
function parseText(context: ParserContext) {
  /**
   * 定义普通文本结束的标记
   * 例如:hello world </div>,那么文本结束的标记就为 <
   * PS:这也意味着如果你渲染了一个 <div> hell<o </div> 的标签,那么你将得到一个错误
   */
  const endTokens = ['<', '{{']
  // 计算普通文本结束的位置
  let endIndex = context.source.length

  // 计算精准的 endIndex,计算的逻辑为:从 context.source 中分别获取 '<', '{{' 的下标,取最小值为 endIndex
  for (let i = 0; i < endTokens.length; i++) {
    const index = context.source.indexOf(endTokens[i], 1)
    if (index !== -1 && endIndex > index) {
      endIndex = index
    }
  }

  // 获取处理的文本内容
  const content = parseTextData(context, endIndex)

  return {
    type: NodeTypes.TEXT,
    content
  }
}
AI 代码解读
  1. 解析文本的过程需要获取到文本内容,此时我们需要构建 parseTextData 方法:
/**
 * 从指定位置(length)获取给定长度的文本数据。
 */
function parseTextData(context: ParserContext, length: number): string {
  // 获取指定的文本数据
  const rawText = context.source.slice(0, length)
  // 《继续》对模板进行解析处理
  advanceBy(context, length)
  // 返回获取到的文本
  return rawText
}
AI 代码解读

最后在 baseParse 中触发 parseChildren 方法:

此时运行测试实例,打印出如下内容:

image.png

4.3 生成 AST,测试

当 parseChildren 处理完成之后,我们可以到 children,那么最后我们就只需要利用 createRoot 方法,把 children 放到 ROOT 节点之下即可。

  1. 创建 createRoot 方法:
/**
 * 生成 root 节点
 */
export function createRoot(children) {
  return {
    type: NodeTypes.ROOT,
    children,
    // loc:位置,这个属性并不影响渲染,但是它必须存在,否则会报错。所以我们给了他一个 {}
    loc: {}
  }
}
AI 代码解读
  1. 在 baseParse 中使用该方法:
/**
 * 基础的 parse 方法,生成 AST
 * @param content tempalte 模板
 * @returns
 */
export function baseParse(content: string) {
  // 创建 parser 对象,未解析器的上下文对象
  const context = createParserContext(content)
  const children = parseChildren(context, [])
  return createRoot(children)
}
AI 代码解读

至此整个 parse 解析流程完成。我们可以在 packages/compiler-core/src/compile.ts 中打印得到的 AST

export function baseCompile(template: string, options) {
  const ast = baseParse(template)
  console.log(JSON.stringify(ast), ast)

  return {}
}
AI 代码解读

得到的内容为:

{
  "type": 0,
  "children": [
    {
      "type": 1,
      "tag": "div",
      "tagType": 0,
      "props": [],
      "children": [{ "type": 2, "content": " hello world " }]
    }
  ],
  "loc": {}
}
AI 代码解读

我们可以把得到的该 AST 放入到 vue 的源码中进行解析,以此来验证是否正确。

在 vue 源码的 packages/compiler-core/src/compile.ts 模块下 baseCompile 方法中:

image.png

运行源码的 compile 方法,浏览器中依然可以渲染 hello world

<script>
  const { compile, h, render } = Vue
  // 创建 template
  const template = `<div>hello world</div>`

  // 生成 render 函数
  const renderFn = compile(template)

  // 创建组件
  const component = {
    render: renderFn
  }

  // 通过 h 函数,生成 vnode
  const vnode = h(component)

  // 通过 render 函数渲染组件
  render(vnode, document.querySelector('#app'))
</script>
AI 代码解读

成功运行,标记着我们的 AST 处理完成。

5. 扩展知识:AST 到 JavaScript AST 的转换策略和注意事项

目录
打赏
0
0
0
0
353
分享
相关文章
基于 ant-design-vue 和 Vue 3 封装的功能强大的表格组件
VTable 是一个基于 ant-design-vue 和 Vue 3 的多功能表格组件,支持列自定义、排序、本地化存储、行选择等功能。它继承了 Ant-Design-Vue Table 的所有特性并加以扩展,提供开箱即用的高性能体验。示例包括基础表格、可选择表格和自定义列渲染等。
vue2和vue3的响应式原理有何不同?
大家好,我是V哥。本文详细对比了Vue 2与Vue 3的响应式原理:Vue 2基于`Object.defineProperty()`,适合小型项目但存在性能瓶颈;Vue 3采用`Proxy`,大幅优化初始化、更新性能及内存占用,更高效稳定。此外,我建议前端开发者关注鸿蒙趋势,2025年将是国产化替代关键期,推荐《鸿蒙 HarmonyOS 开发之路》卷1助你入行。老项目用Vue 2?不妨升级到Vue 3,提升用户体验!关注V哥爱编程,全栈开发轻松上手。
高效工作流:用Mermaid绘制你的专属流程图;如何在Vue3中导入mermaid绘制流程图
mermaid是一款非常优秀的基于 JavaScript 的图表绘制工具,可渲染 Markdown 启发的文本定义以动态创建和修改图表。非常适合新手学习或者做一些弱交互且自定义要求不高的图表 除了流程图以外,mermaid还支持序列图、类图、状态图、实体关系图等图表可供探索。 博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
你真的会使用Vue3的onMounted钩子函数吗?Vue3中onMounted的用法详解
onMounted作为vue3中最常用的钩子函数之一,能够灵活、随心应手的使用是每个Vue开发者的必修课,同时根据其不同写法的特性,来选择最合适最有利于维护的写法。博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
Pinia 如何在 Vue 3 项目中进行安装和配置?
Pinia 如何在 Vue 3 项目中进行安装和配置?
142 4
管理数据必备;侦听器watch用法详解,vue2与vue3中watch的变化与差异
一篇文章同时搞定Vue2和Vue3的侦听器,是不是很棒?不要忘了Vue3中多了一个可选项watchEffect噢。 博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
GoView:Start14.6k,上车啦上车啦,Vue3低代码平台GoView,零代码+全栈框架
GoView 是一个Vue3搭建的低代码数据可视化开发平台,将图表或页面元素封装为基础组件,无需编写代码即可完成业务需求。 它的技术栈为:Vue3 + TypeScript4 + Vite2 + NaiveUI + ECharts5 +VChart + Axios + Pinia2 + PlopJS
Vue 组件化开发:构建高质量应用的核心
本文深入探讨了 Vue.js 组件化开发的核心概念与最佳实践。
194 1
创建vue3项目步骤以及安装第三方插件步骤【保姆级教程】
这是一篇关于创建Vue项目的详细指南,涵盖从环境搭建到项目部署的全过程。
463 1
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等