从零构建带AI流式对话的Vue2全栈应用-CSDN博客这个链接里面的阶段一详细操作以及知识点讲解
我们先只学习阶段一。这个阶段完成后,你将能够:
- 理解前端、Vue、Node.js、npm分别是什么。
- 创建并运行一个 Vue 2 项目。
- 看懂
.vue文件的基本结构。 - 使用
data、methods、computed、watch和生命周期。 - 使用
v-model、v-if、v-for、v-bind、v-on。 - 创建页面路由。
- 创建子组件,并实现父子组件通信。
- 使用 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/cli 和 vue 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 实例,可以拥有自己的 data、methods、computed、watch 和生命周期。
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-if 和 v-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已经可以调用;computed和watch已经初始化;- 但真实页面元素还没有挂载完成。
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 的 actions 和 getters。
二十三、运行并检查效果
确保所有文件保存,然后在项目目录运行:
bash
npm run serve
浏览器打开:
http://localhost:8080
依次测试:
- 首页是否正常显示。
- 输入名字后,欢迎语是否变化。
- 名字字符数是否变化。
- 输入新技能并点击"添加技能"。
- 按回车是否也能添加技能。
- 点击"删除"是否能删除技能。
- 点击子组件按钮,父组件是否出现绿色消息。
- 点击"完成一次学习",数字是否增加。
- 点击顶部"关于",是否进入关于页面。
- 再点击"首页",是否返回首页。
- 打开 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 -v、npm -v和vue --version。 - 能执行
npm run serve启动项目。 - 知道
template、script、style分别负责什么。 - 能解释
data和methods的区别。 - 会使用
v-model、v-if、v-for、:属性、@事件。 - 知道 computed 用来计算派生数据。
- 知道 watch 用来观察数据变化。
- 能区分
created和mounted。 - 能通过 props 把数据传给子组件。
- 能通过
$emit让子组件通知父组件。 - 能使用 Router 切换首页和关于页。
- 能通过 Vuex 保存一个全局计数。