阶段一:Vue 2 单页应用基础

从零构建带AI流式对话的Vue2全栈应用-CSDN博客这个链接里面的阶段一详细操作以及知识点讲解

我们先只学习阶段一。这个阶段完成后,你将能够:

  1. 理解前端、Vue、Node.js、npm分别是什么。
  2. 创建并运行一个 Vue 2 项目。
  3. 看懂 .vue 文件的基本结构。
  4. 使用 datamethodscomputedwatch 和生命周期。
  5. 使用 v-modelv-ifv-forv-bindv-on
  6. 创建页面路由。
  7. 创建子组件,并实现父子组件通信。
  8. 使用 Vuex 保存简单的全局状态。

这对应原文阶段一的学习目标和实操任务。


一、开始前需要知道的技术现状

Vue 2 已经结束官方生命周期,Vue CLI 也已经进入维护模式。现在创建全新商业项目,通常会优先使用 Vue 3 和 Vite;不过你正在学习的文章以及很多旧项目使用 Vue 2,因此学习 Vue 2 对维护旧系统、理解 Vue 基础仍然有价值。

原文建议安装 Node.js 18 或 20,但截至 2026 年 7 月,这两个版本都已经结束官方支持。Node.js 官方当前仍支持 Node.js 22 和 Node.js 24。为了减少 Vue 2 旧工具链的兼容问题,本教程建议你使用:

Node.js 22 LTS + npm + Vue CLI 5 + Vue 2

Node.js 24 是目前较新的 LTS,但 Vue 2 和 Vue CLI 都属于旧技术栈,因此 Node.js 22 通常是更稳妥的教学选择。Node.js 官方当前将 22 和 24 都列为 LTS版本。


二、先理解几个基本概念

1. 什么是前端

用户打开一个网站后,浏览器里看见和操作的部分,就是前端。

前端最基础的三项技术是:

技术 作用 类比
HTML 决定页面里有什么 房子的结构
CSS 决定页面长什么样 房子的装修
JavaScript 决定页面怎么交互 房子的电器和控制系统

例如:

html 复制代码
<button>点击我</button>

这是 HTML,表示页面上有一个按钮。

css 复制代码
button {
  color: white;
  background: blue;
}

这是 CSS,负责改变按钮的颜色。

javascript 复制代码
button.onclick = function () {
  alert('你点击了按钮')
}

这是 JavaScript,负责处理用户点击按钮后的行为。


2. Vue 是什么

Vue 是一个帮助我们开发前端页面的 JavaScript 框架。

不使用 Vue 时,我们可能需要手动找到页面元素,再修改元素:

javascript 复制代码
document.querySelector('#message').innerText = '你好'

使用 Vue 后,我们主要修改数据:

javascript 复制代码
this.message = '你好'

Vue 会自动把新的数据更新到页面上。

这个过程称为:

数据驱动视图

可以简单理解为:

复制代码
修改 JavaScript 数据
        ↓
Vue 检测到数据变化
        ↓
Vue 自动更新页面

Vue 实例创建时,会把 data 中的数据加入响应式系统;这些数据发生变化后,页面会相应更新。


3. Node.js 是什么

浏览器能够执行 JavaScript,但创建 Vue 项目还需要:

  • 下载 Vue 依赖;
  • 启动开发服务器;
  • 编译 .vue 文件;
  • 压缩和打包代码。

这些工作需要 Node.js。

在目前这个阶段,Node.js 主要是我们的前端开发工具运行环境,还不是项目后端。


4. npm 是什么

npm 会随着 Node.js 一起安装。

它是一个包管理工具,负责帮我们下载项目所需的第三方代码。

例如:

bash 复制代码
npm install axios

意思是:

在当前项目中安装 Axios。

后面的阶段三会用 Axios 请求后端接口。


5. 什么是单页应用 SPA

SPA 是 Single Page Application 的缩写,中文叫"单页应用"。

传统网站切换页面时,浏览器通常会重新加载整个 HTML 页面。

Vue 单页应用一般只加载一次基础页面,之后通过 JavaScript 切换页面区域。例如:

复制代码
首页 → 用户管理 → AI对话

地址会发生变化,但浏览器不会每次都完整刷新整个网站。

Vue Router 就负责管理这种页面切换。


三、安装开发环境

下面以 Windows 为主。

第一步:安装 Node.js 22

安装 Node.js 22 LTS。

安装时通常保持默认选项,一直点击下一步即可。

安装结束后,打开:

复制代码
开始菜单 → 搜索"命令提示符" → 打开 CMD

初学阶段建议先使用 CMD,不要使用 PowerShell,以避免执行策略等额外问题。

输入:

bash 复制代码
node -v

应该看到类似:

复制代码
v22.x.x

再输入:

bash 复制代码
npm -v

应该看到类似:

复制代码
10.x.x

具体小版本号不同没有关系。

这两个命令是什么意思

bash 复制代码
node -v
  • node:运行 Node.js。
  • -v:查看 version,也就是版本号。
bash 复制代码
npm -v
  • npm:运行 npm 包管理器。
  • -v:查看 npm 版本。

如果提示:

复制代码
'node' 不是内部或外部命令

通常表示 Node.js 没有正确安装,或者安装完成后没有重新打开 CMD。


第二步:安装 VS Code

VS Code 是代码编辑器。

安装完成后打开 VS Code,在左侧扩展商店中安装:

  • Vue - Official
  • ESLint
  • Prettier - Code formatter

Vue - Official 提供 .vue 文件的语法高亮、代码提示和错误检查,目前是官方 Vue 编辑器扩展。

暂时不需要安装很多扩展。扩展过多反而可能产生格式化和代码检查冲突。


第三步:创建专门的代码目录

打开 CMD,执行:

bash 复制代码
mkdir %USERPROFILE%\code

含义是:

  • mkdir:创建文件夹;
  • %USERPROFILE%:你当前 Windows 用户的主目录;
  • code:新文件夹名称。

接着进入这个文件夹:

bash 复制代码
cd /d %USERPROFILE%\code

含义是:

  • cd:切换目录;
  • /d:允许同时切换盘符;
  • %USERPROFILE%\code:目标目录。

建议项目路径中暂时不要包含:

  • 中文;
  • 空格;
  • 特殊符号。

这样可以减少旧工具出现路径兼容问题。


四、安装 Vue CLI

在 CMD 中执行:

bash 复制代码
npm install -g @vue/cli

各部分的含义是:

复制代码
npm        使用 npm
install    安装软件包
-g         全局安装
@vue/cli   要安装的 Vue CLI 软件包

全局安装后,我们可以在任何目录使用 vue 命令。

安装完成后执行:

bash 复制代码
vue --version

正常情况下会显示 Vue CLI 的版本。

Vue CLI 官方仍然保留了 npm install -g @vue/clivue create 的使用方式,但它目前已经处于维护模式。


五、创建第一个 Vue 2 项目

确保 CMD 当前位于:

复制代码
你的用户目录\code

执行:

bash 复制代码
vue create my-ai-web

这里:

  • vue create:使用 Vue CLI 创建项目;
  • my-ai-web:项目文件夹名称。

创建过程中会出现一系列选项。使用键盘上下方向键移动,使用空格选择,使用回车确认。

推荐选择

1. 选择配置方式

选择:

复制代码
Manually select features

意思是手动选择项目功能。

不要直接选择默认配置,因为我们需要 Router 和 Vuex。


2. 选择项目功能

建议选中:

复制代码
Choose Vue version
Babel
Router
Vuex
CSS Pre-processors
Linter / Formatter

暂时不选:

复制代码
TypeScript
PWA
Unit Testing
E2E Testing

因为你现在刚开始学习,先使用普通 JavaScript,避免一次引入太多概念。

各功能含义:

功能 作用
Choose Vue version 允许选择 Vue 2
Babel 把较新的 JavaScript 转换成兼容代码
Router 管理首页、关于页等页面切换
Vuex 管理多个组件共享的数据
CSS Pre-processors 为阶段二的 SCSS 做准备
Linter / Formatter 检查代码中的常见问题

3. 选择 Vue 版本

选择:

复制代码
2.x

一定不要选 3.x,因为当前课程按照原文学习 Vue 2。


4. 是否使用 history mode

可能会出现:

复制代码
Use history mode for router?

选择:

复制代码
No

这样地址可能长这样:

复制代码
http://localhost:8080/#/about

中间会有一个 #

这种模式配置简单,初学阶段也不容易遇到服务器刷新页面后返回 404 的问题。


5. 选择 CSS 预处理器

选择:

复制代码
Sass/SCSS

当前阶段暂时主要使用普通 CSS,阶段二会开始学习 SCSS。


6. 选择代码检查规则

选择相对简单的:

复制代码
ESLint with error prevention only

再选择:

复制代码
Lint on save

意思是每次保存文件时检查代码。


7. 配置文件放在哪里

选择:

复制代码
In dedicated config files

表示把各项配置放进独立文件。


8. 是否保存为预设

选择:

复制代码
No

等待项目安装完成。


六、启动项目

进入项目目录:

bash 复制代码
cd my-ai-web

启动开发服务器:

bash 复制代码
npm run serve

正常情况下,终端会显示类似:

复制代码
App running at:
- Local: http://localhost:8080/

在浏览器中访问:

复制代码
http://localhost:8080

看到 Vue 默认页面,就说明项目创建成功了。

npm run serve 是什么意思

项目的 package.json 文件里定义了一个名为 serve 的命令。

执行:

bash 复制代码
npm run serve

相当于告诉 npm:

请启动这个项目的开发服务器。

开发服务器启动后:

  • 浏览器可以访问项目;
  • 保存代码后,页面通常会自动更新;
  • 终端会显示编译错误;
  • 终端窗口必须保持运行。

停止服务器时,在终端按:

复制代码
Ctrl + C

然后输入:

复制代码
Y

再次启动时,回到项目目录重新执行:

bash 复制代码
npm run serve

七、用 VS Code 打开项目

在项目目录中执行:

bash 复制代码
code .

这里的 . 表示当前文件夹。

如果 code . 无法使用,也可以:

复制代码
打开 VS Code
→ 文件
→ 打开文件夹
→ 选择 my-ai-web

八、认识项目目录

项目大致会包含:

复制代码
my-ai-web/
├── node_modules/
├── public/
├── src/
│   ├── assets/
│   ├── components/
│   ├── router/
│   ├── store/
│   ├── views/
│   ├── App.vue
│   └── main.js
├── package.json
└── package-lock.json

node_modules

存放项目安装的第三方依赖。

这里通常有大量文件。

不要手动修改里面的内容。


public

存放不需要经过 Vue 编译的公共文件。

其中的 index.html 是整个单页应用最基础的 HTML 文件。

初学阶段一般不需要修改它。


src

最重要的源代码目录。

以后大部分代码都写在这里。


src/main.js

项目入口文件。

它负责启动 Vue。

默认代码通常类似:

javascript 复制代码
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

Vue.config.productionTip = false

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

逐部分解释:

javascript 复制代码
import Vue from 'vue'

从已经安装的 vue 软件包中引入 Vue。

javascript 复制代码
import App from './App.vue'

引入根组件 App.vue

javascript 复制代码
import router from './router'

引入路由配置。

javascript 复制代码
import store from './store'

引入 Vuex 全局状态仓库。

javascript 复制代码
new Vue({
  router,
  store,
  render: h => h(App)
})

创建整个应用最外层的 Vue 实例。

Vue 应用通常由一个根实例和多个嵌套的子组件构成。

javascript 复制代码
.$mount('#app')

把 Vue 应用挂载到 HTML 中 id="app" 的元素上。

你现在不需要自己修改 main.js


App.vue

整个项目最外层的根组件。

其他页面会显示在它里面。


views

存放完整页面,例如:

复制代码
首页
关于页面
登录页面
AI对话页面

components

存放可以重复使用的小组件,例如:

复制代码
导航栏
用户卡片
消息气泡
输入框
确认按钮

九、理解 .vue 单文件组件

一个 .vue 文件通常包含三个部分:

javascript 复制代码
<template>
  <!-- HTML 页面结构 -->
</template>

<script>
  // JavaScript 数据和逻辑
</script>

<style>
  /* CSS 页面样式 */
</style>

这叫做:

Single File Component,单文件组件,简称 SFC。

例如:

复制代码
<template>
  <div>
    <h1>{{ message }}</h1>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: '你好,Vue'
    }
  }
}
</script>

<style>
h1 {
  color: blue;
}
</style>

三部分分别负责:

复制代码
template:页面有什么
script:页面的数据和行为
style:页面长什么样

十、编写根组件 App.vue

打开:

复制代码
src/App.vue

删除原有内容,替换成:

复制代码
<template>
  <div id="app">
    <header class="top-bar">
      <h1>我的 Vue 2 学习项目</h1>

      <nav class="nav">
        <router-link exact to="/">首页</router-link>
        <router-link to="/about">关于</router-link>
      </nav>
    </header>

    <main class="page-container">
      <router-view />
    </main>
  </div>
</template>

<style>
* {
  box-sizing: border-box;
}

body {
  margin: 0;
  background: #f5f7fa;
  color: #303133;
  font-family: Arial, "Microsoft YaHei", sans-serif;
}

.top-bar {
  padding: 20px;
  color: white;
  background: #409eff;
}

.top-bar h1 {
  margin: 0 0 16px;
}

.nav a {
  margin-right: 20px;
  color: white;
  text-decoration: none;
}

.nav a.router-link-exact-active {
  font-weight: bold;
  text-decoration: underline;
}

.page-container {
  width: 90%;
  max-width: 900px;
  margin: 30px auto;
}
</style>

代码说明

<router-link>

复制代码
<router-link to="/">首页</router-link>

它类似普通 HTML 的链接:

html 复制代码
<a href="/">首页</a>

router-link 会交给 Vue Router 处理,切换页面时通常不会完整刷新浏览器。

复制代码
<router-link to="/about">关于</router-link>

点击后进入 /about 路由。


<router-view />

复制代码
<router-view />

它是页面内容的占位区域。

访问首页时,这里显示 HomeView.vue

访问关于页时,这里显示 AboutView.vue

可以把它想象成电视屏幕:

复制代码
App.vue 是电视机外壳
router-view 是屏幕
不同页面是屏幕中播放的内容

CSS 选择器

css 复制代码
.top-bar {
  padding: 20px;
}

表示给 class="top-bar" 的元素设置样式。

css 复制代码
body {
  margin: 0;
}

清除浏览器默认的页面外边距。

css 复制代码
max-width: 900px;

表示内容区域最大不超过 900 像素,避免在大屏幕上过宽。


十一、配置路由

打开:

复制代码
src/router/index.js

替换成:

javascript 复制代码
import Vue from 'vue'
import VueRouter from 'vue-router'
import HomeView from '../views/HomeView.vue'
import AboutView from '../views/AboutView.vue'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'home',
    component: HomeView
  },
  {
    path: '/about',
    name: 'about',
    component: AboutView
  }
]

const router = new VueRouter({
  routes
})

export default router

代码说明

javascript 复制代码
import VueRouter from 'vue-router'

引入 Vue Router。

javascript 复制代码
Vue.use(VueRouter)

把 Vue Router 注册到 Vue 中。


路由数组

javascript 复制代码
const routes = [
  {
    path: '/',
    name: 'home',
    component: HomeView
  }
]

每个路由对象描述一条页面规则。

复制代码
path       浏览器地址
name       路由名称
component  这个地址显示哪个组件

因此:

javascript 复制代码
path: '/'

对应首页。

javascript 复制代码
component: HomeView

表示首页显示 HomeView.vue


导出路由

javascript 复制代码
export default router

把创建好的路由对象导出,供 main.js 使用。


十二、创建关于页面

打开或创建:

复制代码
src/views/AboutView.vue

写入:

复制代码
<template>
  <section class="about-page">
    <h2>关于这个项目</h2>

    <p>
      这是我学习 Vue 2 的第一个项目。
    </p>

    <p>
      后续我会在这个项目中继续学习 Element UI、Axios、登录认证和 AI 流式对话。
    </p>
  </section>
</template>

<script>
export default {
  name: 'AboutView'
}
</script>

<style scoped>
.about-page {
  padding: 24px;
  background: white;
  border-radius: 8px;
}

.about-page p {
  line-height: 1.8;
}
</style>

scoped 是什么意思

复制代码
<style scoped>

表示这里的 CSS 主要只作用于当前组件。

如果不写 scoped,样式可能影响其他页面。


name 是什么意思

javascript 复制代码
export default {
  name: 'AboutView'
}

这个对象就是当前 Vue 组件的配置。

name 是组件名称,主要方便:

  • 调试;
  • 浏览器开发工具识别;
  • 递归组件;
  • 缓存组件。

十三、创建子组件

在:

复制代码
src/components

中新建:

复制代码
StudentCard.vue

写入:

复制代码
<template>
  <article class="student-card">
    <h3>学习者卡片</h3>

    <p>姓名:{{ name }}</p>
    <p>已经添加的技能数量:{{ skillCount }}</p>

    <button @click="notifyParent">
      让子组件通知父组件
    </button>
  </article>
</template>

<script>
export default {
  name: 'StudentCard',

  props: {
    name: {
      type: String,
      required: true
    },

    skillCount: {
      type: Number,
      default: 0
    }
  },

  methods: {
    notifyParent() {
      this.$emit(
        'say-hello',
        `子组件说:${this.name},继续加油!`
      )
    }
  }
}
</script>

<style scoped>
.student-card {
  margin-top: 24px;
  padding: 20px;
  background: #ecf5ff;
  border: 1px solid #b3d8ff;
  border-radius: 8px;
}

button {
  padding: 8px 14px;
  cursor: pointer;
}
</style>

1. 什么是组件

组件是可以重复使用的一块页面。

例如一个用户卡片可能在多个页面显示:

复制代码
用户列表
用户详情
好友页面
搜索结果

我们不需要在每个页面重复写相同代码,只需要重复使用同一个组件。

Vue 组件本质上是可复用的 Vue 实例,可以拥有自己的 datamethodscomputedwatch 和生命周期。


2. 什么是 props

javascript 复制代码
props: {
  name: {
    type: String,
    required: true
  }
}

props 用于:

父组件向子组件传递数据。

这里规定:

复制代码
name 必须是字符串
name 必须传入
javascript 复制代码
skillCount: {
  type: Number,
  default: 0
}

表示:

复制代码
skillCount 应该是数字
如果父组件没传,默认是 0

Props 遵循单向数据流:父组件的数据可以传给子组件,但子组件不应该直接修改收到的 prop。


3. 什么是 $emit

javascript 复制代码
this.$emit('say-hello', '一段消息')

$emit 用来让子组件向父组件发出一个事件。

这里发出了:

复制代码
事件名称:say-hello
事件数据:子组件说......

父组件之后可以监听这个事件。

因此父子通信方向是:

复制代码
父组件 → props → 子组件
父组件 ← $emit ← 子组件

十四、编写首页并学习 Vue 核心语法

打开:

复制代码
src/views/HomeView.vue

删除原有代码,写入:

复制代码
<template>
  <section class="home-page">
    <h2>{{ pageTitle }}</h2>

    <p class="mounted-message">
      {{ mountedMessage }}
    </p>

    <div class="form-block">
      <label for="student-name">你的名字:</label>

      <input
        id="student-name"
        v-model.trim="studentName"
        placeholder="请输入名字"
      >

      <p v-if="studentName">
        你好,{{ studentName }}!
      </p>

      <p v-else>
        你还没有输入名字。
      </p>

      <p>
        当前名字共有 {{ nameLength }} 个字符。
      </p>
    </div>

    <div class="form-block">
      <label for="new-skill">新技能:</label>

      <input
        id="new-skill"
        v-model="newSkill"
        placeholder="例如:CSS"
        @keyup.enter="addSkill"
      >

      <button
        :disabled="!newSkill.trim()"
        @click="addSkill"
      >
        添加技能
      </button>
    </div>

    <div class="skill-block">
      <h3>我的技能列表</h3>

      <ul v-if="skills.length > 0">
        <li
          v-for="skill in skills"
          :key="skill.id"
        >
          <span>{{ skill.name }}</span>

          <button @click="removeSkill(skill.id)">
            删除
          </button>
        </li>
      </ul>

      <p v-else>
        目前还没有技能,请添加一个。
      </p>
    </div>

    <StudentCard
      :name="displayName"
      :skill-count="skills.length"
      @say-hello="handleChildMessage"
    />

    <p v-if="childMessage" class="child-message">
      {{ childMessage }}
    </p>

    <div class="vuex-block">
      <h3>Vuex 全局状态演示</h3>

      <p>
        全局学习次数:{{ studyCount }}
      </p>

      <button @click="increaseStudyCount">
        完成一次学习
      </button>
    </div>
  </section>
</template>

<script>
import StudentCard from '@/components/StudentCard.vue'

export default {
  name: 'HomeView',

  components: {
    StudentCard
  },

  data() {
    return {
      pageTitle: '阶段一:Vue 2 基础练习',
      studentName: '',
      newSkill: '',
      nextSkillId: 3,
      mountedMessage: '页面正在初始化......',
      childMessage: '',

      skills: [
        {
          id: 1,
          name: 'HTML'
        },
        {
          id: 2,
          name: 'JavaScript'
        }
      ]
    }
  },

  computed: {
    displayName() {
      return this.studentName || '未命名同学'
    },

    nameLength() {
      return this.studentName.length
    },

    studyCount() {
      return this.$store.state.studyCount
    }
  },

  watch: {
    studentName(newValue, oldValue) {
      console.log(
        `姓名从"${oldValue}"变成了"${newValue}"`
      )
    }
  },

  created() {
    console.log('created:组件数据已经准备完成')
  },

  mounted() {
    this.mountedMessage = '页面已经挂载完成,可以开始操作。'
    console.log('mounted:页面已经显示到浏览器中')
  },

  methods: {
    addSkill() {
      const skillName = this.newSkill.trim()

      if (!skillName) {
        return
      }

      this.skills.push({
        id: this.nextSkillId,
        name: skillName
      })

      this.nextSkillId += 1
      this.newSkill = ''
    },

    removeSkill(skillId) {
      this.skills = this.skills.filter(
        skill => skill.id !== skillId
      )
    },

    handleChildMessage(message) {
      this.childMessage = message
    },

    increaseStudyCount() {
      this.$store.commit('incrementStudyCount')
    }
  }
}
</script>

<style scoped>
.home-page {
  padding: 24px;
  background: white;
  border-radius: 8px;
}

.mounted-message {
  color: #909399;
}

.form-block,
.skill-block,
.vuex-block {
  margin-top: 24px;
  padding: 18px;
  border: 1px solid #dcdfe6;
  border-radius: 8px;
}

input {
  padding: 8px 10px;
  border: 1px solid #c0c4cc;
  border-radius: 4px;
}

button {
  margin-left: 8px;
  padding: 8px 14px;
  cursor: pointer;
}

button:disabled {
  cursor: not-allowed;
  opacity: 0.5;
}

li {
  margin-bottom: 10px;
}

.child-message {
  padding: 12px;
  color: #67c23a;
  background: #f0f9eb;
}
</style>

下面开始拆解这份代码。


十五、模板语法详解

1. 插值表达式 {``{ }}

复制代码
<h2>{{ pageTitle }}</h2>

双大括号会把 JavaScript 数据显示到页面。

pageTitle 来自:

javascript 复制代码
data() {
  return {
    pageTitle: '阶段一:Vue 2 基础练习'
  }
}

页面最终显示:

复制代码
阶段一:Vue 2 基础练习

数据变化时,页面也会自动变化。


2. v-model 双向绑定

复制代码
<input v-model.trim="studentName">

它建立了输入框与 studentName 之间的双向联系。

复制代码
用户在输入框输入内容
        ↓
studentName 自动变化
        ↓
使用 studentName 的页面自动更新

反过来,如果代码修改:

javascript 复制代码
this.studentName = '小王'

输入框也会显示"小王"。

.trim

javascript 复制代码
v-model.trim

.trim 会尽量去掉输入内容开头和结尾的空格。

例如:

复制代码
"   小王   "

会处理成:

复制代码
"小王"

3. v-ifv-else

复制代码
<p v-if="studentName">
  你好,{{ studentName }}!
</p>

<p v-else>
  你还没有输入名字。
</p>

studentName 有内容时,显示第一段。

studentName 为空时,显示第二段。

这里利用了 JavaScript 的真假判断:

复制代码
空字符串 ""        → false
非空字符串 "小王"  → true

4. v-for 列表渲染

复制代码
<li
  v-for="skill in skills"
  :key="skill.id"
>
  {{ skill.name }}
</li>

含义是:

遍历 skills 数组,每次取出一个元素,暂时命名为 skill

数组:

javascript 复制代码
skills: [
  {
    id: 1,
    name: 'HTML'
  },
  {
    id: 2,
    name: 'JavaScript'
  }
]

会生成两个 <li>

Vue 官方推荐在 v-for 中尽量提供唯一的 key,帮助 Vue 正确识别、复用和重新排列页面节点。

因此这里使用:

复制代码
:key="skill.id"

不要把数组序号作为长期可变列表的首选 key。


5. v-bind

完整写法是:

复制代码
v-bind:key="skill.id"

简写是:

复制代码
:key="skill.id"

再例如:

复制代码
:disabled="!newSkill.trim()"

表示动态控制按钮的 disabled 属性。

当输入框为空时:

javascript 复制代码
!newSkill.trim()

结果是 true,按钮被禁用。

当输入框有内容时,结果是 false,按钮可以点击。


6. v-on

完整写法:

复制代码
v-on:click="addSkill"

简写:

复制代码
@click="addSkill"

表示:

用户点击按钮时,执行 addSkill 方法。

Vue 可以通过 v-on 监听浏览器事件并执行 JavaScript。

再例如:

复制代码
@keyup.enter="addSkill"

表示:

用户在输入框中松开回车键时,执行 addSkill

其中 .enter 是按键修饰符。


十六、data 详解

javascript 复制代码
data() {
  return {
    studentName: '',
    skills: []
  }
}

data 用于保存当前组件的数据。

组件中的 data 必须写成函数,并返回一个新对象:

javascript 复制代码
data() {
  return {
    count: 0
  }
}

不能简单写成:

javascript 复制代码
data: {
  count: 0
}

原因是组件可以重复使用。使用函数后,每个组件实例都能得到自己独立的数据副本。


十七、methods 详解

methods 用于保存方法,也就是可以执行的操作。

添加技能

javascript 复制代码
addSkill() {
  const skillName = this.newSkill.trim()

  if (!skillName) {
    return
  }

  this.skills.push({
    id: this.nextSkillId,
    name: skillName
  })

  this.nextSkillId += 1
  this.newSkill = ''
}

逐步解释。

读取输入内容

javascript 复制代码
const skillName = this.newSkill.trim()
  • const:定义一个不会重新赋值的变量;
  • this.newSkill:访问当前组件的 newSkill 数据;
  • .trim():去掉首尾空格。

阻止空内容

javascript 复制代码
if (!skillName) {
  return
}

如果 skillName 是空字符串,就直接结束方法。

return 在这里相当于:

后面的代码不要再执行。


向数组添加对象

javascript 复制代码
this.skills.push({
  id: this.nextSkillId,
  name: skillName
})

push() 会在数组末尾添加一个元素。

例如原数组:

javascript 复制代码
[
  { id: 1, name: 'HTML' }
]

添加 CSS 后变成:

javascript 复制代码
[
  { id: 1, name: 'HTML' },
  { id: 3, name: 'CSS' }
]

Vue 检测到数组变化后,会自动重新渲染列表。


更新下一个 ID

javascript 复制代码
this.nextSkillId += 1

等价于:

javascript 复制代码
this.nextSkillId = this.nextSkillId + 1

这样下一个技能会使用新的 ID,避免重复。


清空输入框

javascript 复制代码
this.newSkill = ''

因为输入框使用:

复制代码
v-model="newSkill"

所以数据清空后,输入框也会自动清空。


删除技能

javascript 复制代码
removeSkill(skillId) {
  this.skills = this.skills.filter(
    skill => skill.id !== skillId
  )
}

filter() 会创建一个新数组。

它保留:

javascript 复制代码
skill.id !== skillId

结果为 true 的元素。

假设要删除 ID 为 2 的技能:

复制代码
HTML:1 !== 2 → 保留
JavaScript:2 !== 2 → 删除
CSS:3 !== 2 → 保留

最后得到不包含目标技能的新数组。


十八、computed 计算属性

javascript 复制代码
computed: {
  nameLength() {
    return this.studentName.length
  }
}

计算属性用于:

根据已有数据,计算出一个新的结果。

这里的 nameLength 依赖 studentName

当名字是:

复制代码
小明

长度是:

复制代码
2

studentName 变化后,nameLength 会自动重新计算。


displayName

javascript 复制代码
displayName() {
  return this.studentName || '未命名同学'
}

|| 表示"或者"。

如果 studentName 有值,就返回名字。

如果没有值,就返回:

复制代码
未命名同学

computed 和 methods 的区别

下面也能得到名字长度:

javascript 复制代码
methods: {
  getNameLength() {
    return this.studentName.length
  }
}

模板中需要写:

复制代码
{{ getNameLength() }}

而计算属性写:

复制代码
{{ nameLength }}

主要区别是:

  • computed 会根据依赖进行缓存;
  • 依赖没有变化时,可以直接使用之前结果;
  • methods 在页面重新渲染时可能再次执行。

当一个结果由其他数据推导而来时,一般优先考虑 computed。Vue 官方也建议,不要在可以使用计算属性的场景中过度使用 watch


十九、watch 侦听器

javascript 复制代码
watch: {
  studentName(newValue, oldValue) {
    console.log(
      `姓名从"${oldValue}"变成了"${newValue}"`
    )
  }
}

watch 用来观察某个数据。

studentName 变化时,函数自动执行。

参数:

复制代码
newValue:变化后的新值
oldValue:变化前的旧值

打开浏览器开发者工具:

复制代码
F12 → Console

然后在名字输入框中输入文字,可以看到控制台输出。

watch 常用于:

  • 用户输入后请求搜索接口;
  • 表单字段变化时保存草稿;
  • 路由参数变化后重新加载数据;
  • 某项设置变化后执行额外操作。

普通数据计算优先用 computed;需要执行异步请求或额外副作用时,再考虑 watch


二十、生命周期

组件不是突然出现在页面上的,它会经历:

复制代码
创建
  ↓
准备数据
  ↓
生成页面结构
  ↓
挂载到浏览器
  ↓
数据更新
  ↓
销毁

created

javascript 复制代码
created() {
  console.log('created:组件数据已经准备完成')
}

created 执行时:

  • data 已经可以访问;
  • methods 已经可以调用;
  • computedwatch 已经初始化;
  • 但真实页面元素还没有挂载完成。

Vue 官方文档说明,created 阶段已经完成数据观察、计算属性、方法和侦听器的设置,但挂载阶段还没有开始。

常见用途:

  • 初始化普通数据;
  • 请求页面初始数据;
  • 读取路由参数。

mounted

javascript 复制代码
mounted() {
  this.mountedMessage = '页面已经挂载完成,可以开始操作。'
}

mounted 执行时,组件已经显示到浏览器页面上。

常见用途:

  • 读取页面元素尺寸;
  • 初始化图表;
  • 让输入框自动获得焦点;
  • 创建需要 DOM 元素的第三方插件。

现阶段记住:

复制代码
created:数据准备好了
mounted:页面已经显示出来了

二十一、父子组件通信

首页中使用子组件:

复制代码
<StudentCard
  :name="displayName"
  :skill-count="skills.length"
  @say-hello="handleChildMessage"
/>

父组件向子组件传值

复制代码
:name="displayName"

把父组件的 displayName 传给子组件的 name

复制代码
:skill-count="skills.length"

把技能数量传给子组件的 skillCount

HTML 属性中通常使用短横线:

复制代码
skill-count

JavaScript 中使用驼峰:

复制代码
skillCount

子组件通知父组件

子组件中:

javascript 复制代码
this.$emit('say-hello', '消息内容')

父组件监听:

复制代码
@say-hello="handleChildMessage"

事件触发后执行:

javascript 复制代码
handleChildMessage(message) {
  this.childMessage = message
}

然后页面通过:

复制代码
<p v-if="childMessage">
  {{ childMessage }}
</p>

显示消息。

完整过程:

复制代码
点击子组件按钮
      ↓
子组件执行 notifyParent
      ↓
子组件 $emit 发出 say-hello
      ↓
父组件监听到 say-hello
      ↓
执行 handleChildMessage
      ↓
更新 childMessage
      ↓
页面显示消息

二十二、配置 Vuex

打开:

复制代码
src/store/index.js

替换成:

javascript 复制代码
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    studyCount: 0
  },

  mutations: {
    incrementStudyCount(state) {
      state.studyCount += 1
    }
  }
})

什么是 Vuex

普通的 data 属于某个组件。

例如:

这份数据主要归当前组件使用。

但有些数据可能需要很多页面共享:

  • 当前用户信息;
  • 是否已经登录;
  • 用户权限;
  • 网站主题;
  • 全局购物车;
  • 未读消息数量。

这些数据可以放进 Vuex。


state

javascript 复制代码
state: {
  studyCount: 0
}

state 是全局数据仓库。

首页通过:

javascript 复制代码
this.$store.state.studyCount

读取它。

我们把它包装成计算属性:

javascript 复制代码
studyCount() {
  return this.$store.state.studyCount
}

模板中就可以写:

复制代码
{{ studyCount }}

mutations

javascript 复制代码
mutations: {
  incrementStudyCount(state) {
    state.studyCount += 1
  }
}

mutation 用于修改 Vuex 中的数据。

组件不能直接调用这个函数,而是提交 mutation:

javascript 复制代码
this.$store.commit('incrementStudyCount')

这里:

  • $store:访问 Vuex 仓库;
  • commit:提交 mutation;
  • 'incrementStudyCount':mutation 名称。

点击按钮后:

复制代码
组件 commit mutation
        ↓
mutation 修改 state
        ↓
使用 state 的页面自动更新

目前只需要理解 Vuex 的基本流程:

复制代码
state:存数据
mutation:修改数据
commit:触发修改

阶段一不需要深入学习 Vuex 的 actionsgetters


二十三、运行并检查效果

确保所有文件保存,然后在项目目录运行:

bash 复制代码
npm run serve

浏览器打开:

复制代码
http://localhost:8080

依次测试:

  1. 首页是否正常显示。
  2. 输入名字后,欢迎语是否变化。
  3. 名字字符数是否变化。
  4. 输入新技能并点击"添加技能"。
  5. 按回车是否也能添加技能。
  6. 点击"删除"是否能删除技能。
  7. 点击子组件按钮,父组件是否出现绿色消息。
  8. 点击"完成一次学习",数字是否增加。
  9. 点击顶部"关于",是否进入关于页面。
  10. 再点击"首页",是否返回首页。
  11. 打开 F12 控制台,修改名字时是否出现 watch 日志。

二十四、可能遇到的错误

1. vue 不是内部或外部命令

先关闭 CMD,重新打开。

然后执行:

bash 复制代码
vue --version

仍然失败时重新安装:

bash 复制代码
npm install -g @vue/cli

2. npm run serve 提示找不到 package.json

说明你不在项目目录。

执行:

bash 复制代码
cd /d %USERPROFILE%\code\my-ai-web
npm run serve

3. 页面空白

检查两个位置。

终端是否有红色错误。

浏览器打开:

复制代码
F12 → Console

查看是否有红色错误。

最常见原因包括:

  • 文件名写错;
  • 引入路径写错;
  • 标签没有闭合;
  • 少写逗号;
  • 使用了中文标点;
  • StudentCard.vue 没有放在正确目录。

4. 出现组件未定义错误

确认首页有:

javascript 复制代码
import StudentCard from '@/components/StudentCard.vue'

并且有:

javascript 复制代码
components: {
  StudentCard
}

只有引入还不够,还需要在 components 中注册。


5. 代码保存后页面没有变化

确认开发服务器仍然运行。

终端应该保持在:

bash 复制代码
npm run serve

的运行状态。

必要时停止后重新启动:

复制代码
Ctrl + C
npm run serve

二十五、阶段一练习

完成基础代码后,独立尝试以下修改。

练习一:增加学习目标

data 中增加:

javascript 复制代码
learningGoal: '完成 AI 流式对话项目'

在页面中显示:

复制代码
<p>我的目标:{{ learningGoal }}</p>

练习二:显示技能总数

新建计算属性:

javascript 复制代码
skillCount() {
  return this.skills.length
}

页面显示:

复制代码
<p>技能总数:{{ skillCount }}</p>

练习三:增加清空列表按钮

模板增加:

复制代码
<button @click="clearSkills">
  清空全部技能
</button>

methods 中增加:

javascript 复制代码
clearSkills() {
  this.skills = []
}

清空后,已有的 v-else 应该显示:

复制代码
目前还没有技能,请添加一个。

练习四:禁止添加重复技能

addSkill 中,向数组添加前增加:

javascript 复制代码
const alreadyExists = this.skills.some(
  skill => skill.name === skillName
)

if (alreadyExists) {
  alert('这个技能已经存在')
  return
}

some() 会检查数组中是否至少有一个元素满足条件。


阶段一完成标准

达到下面这些条件,就说明阶段一基本完成:

  • 能运行 node -vnpm -vvue --version
  • 能执行 npm run serve 启动项目。
  • 知道 templatescriptstyle 分别负责什么。
  • 能解释 datamethods 的区别。
  • 会使用 v-modelv-ifv-for:属性@事件
  • 知道 computed 用来计算派生数据。
  • 知道 watch 用来观察数据变化。
  • 能区分 createdmounted
  • 能通过 props 把数据传给子组件。
  • 能通过 $emit 让子组件通知父组件。
  • 能使用 Router 切换首页和关于页。
  • 能通过 Vuex 保存一个全局计数。
相关推荐
大模型丫丫19 小时前
Skill-Agent 如何实践:从概念到落地的完整指南
java·大数据·人工智能
工业HMI实战笔记19 小时前
【拯救HMI】:边缘计算在工业自动化中的落地:低延迟控制的实现路径
人工智能·自动化·边缘计算
Ivanqhz19 小时前
预训练 Embedding + 轻量级线上模型
人工智能·机器学习·embedding
问商十三载19 小时前
2026大模型GEO优化体系:3层链路提收录,零成本提34%引用率附工具包
大数据·前端·人工智能
小码哥哥19 小时前
企业AI知识库本地部署的安全真相
人工智能·安全
qiten_00719 小时前
LangChain核心组件深入理解第四篇 -- Tool Agent运行的手和脚
大数据·人工智能·langchain
IT_陈寒19 小时前
Redis过期key的坑,差点让线上服务崩了
前端·人工智能·后端
恋猫de小郭19 小时前
AI 又又造词,Graph 就又要替代 Loop 了?
前端·人工智能·ai编程
数智化管理手记19 小时前
全面预算管理执行偏差大?全面预算管理全流程落地步骤是什么
大数据·网络·数据库·人工智能·数据挖掘