码上问学问代码之理,学编程之道

第 1 章 · 前端工程化与 Vue 起步

写一个网页不难,难的是几十个页面、上百个组件、几个人同时改还能不乱。这一章先把「地基」打好:弄懂 Node.js、npm、Vite 各自管什么,然后用一条命令创建出本课程的前端项目 hivue,看懂它的目录结构,认识 单文件组件(SFC) 和 组合式 API,最后改一个组件、亲眼看到 热更新(HMR) 秒级生效。学完这一章,你就有了一个能跑起来的前端工地。

为什么前端需要「工程化」

从「能跑」到「能维护」
从「三件套」到「工程化」:文件多了、人多了,规矩就得跟上原始开发:引三个文件index.htmlstyle.cssmain.js上手快,但:全局变量满天飞 · 没有组件复用· 改一处怕碰坏另一处工程化:Vue + Vite 脚手架组件化页面拆成 .vue 积木模块化import / export 管依赖热更新改完立刻看到,不用刷新构建打包发布时压缩优化工程化不是把简单事变复杂,而是让「复杂项目」仍然可控
先讲为什么自己在家做饭,一口锅一把刀就够了;开一家餐厅,就得有洗菜区、灶台、冷库、出餐口,每样东西放在固定位置,两三个厨师才能同时干活不打架。
前端也一样:写一个页面用记事本都行,但写一个网站不行。当你把十几次 <script> 堆进同一个 HTML、把函数名起成 a1、data2,改一处炸三处时,「工程化」就不是高级话题,而是活命的必需品。
讲透:「古典文学网」为什么非工程化不可这个项目有首页、列表页、详情页、登录注册、个人中心、收藏页;后端已经给了 18 个接口(见 docs/API接口文档.md)。如果不用工程化,你要在每个 HTML 里复制一遍请求逻辑、登录判断、头像展示——改一次接口地址,就得改十几处。工程化买的是「改一处、处处生效」的能力。

Node.js / npm / pnpm:前端工具链的地基

装 18 或 20 的 LTS 版
先讲为什么JavaScript 原本只能在浏览器里跑,一关掉浏览器它就没活了。Node.js 做的事很简单:把 Chrome 的 JS 引擎(V8)单独拎出来,让 JS 能在你的电脑上直接运行——可以读文件、开服务器、跑命令行工具。
这件事的意义在于:前端由此长出了一整套「在本地先把代码加工好」的工具链。没有 Node,就没有 npm,也就没有 Vite 和 Vue 脚手架——所以它是我们所有工具的地基。
动手做去 nodejs.org 下载 LTS(长期支持版),选 18 或 20 都行,Windows 装 .msi、macOS 装 .pkg,一路下一步。装完打开一个新的终端窗口,验证一下。
check-version.shbash
# 装完一定要「重新打开」终端,否则 PATH 还没生效
node -v
npm -v
终端输出命令行执行结果
运行结果: $ node -v v20.19.0 $ npm -v 10.8.2 -- 只要两行都能打印出版本号,环境就装好了

本课程统一用 pnpm 装依赖。它是 npm 的替代品,命令几乎一一对应,但装得更快、更省磁盘(同样的包在所有项目间共享一份,而不是每个项目都拷一份)。Node 16.13 以后自带 corepack,一条命令就能启用。

setup-pnpm.shbash
# 启用 Node 自带的 corepack,再用它激活 pnpm 9
corepack enable
corepack prepare pnpm@9 --activate
pnpm -v
终端输出命令行执行结果
运行结果: $ corepack enable $ corepack prepare pnpm@9 --activate Preparing pnpm@9 for immediate activation... $ pnpm -v 9.15.4 -- 之后的项目里,npm install 都可以换成 pnpm install
讲透:这几个命令到底谁管谁
  • node:运行环境,负责「把 JS 跑起来」。版本看 node -v。
  • npm:Node 自带的包管理器(Node Package Manager),负责「从网上下载别人写好的代码库到你的项目里」。它自己的版本用 npm -v 看,和 Node 版本是两回事。
  • pnpm:另一款包管理器,读的是同一份 package.json,命令基本能互换:install / run dev / add。团队里选一个用就行,别在一个项目里混着用。
  • 为什么推荐偶数版本:Node 的奇数版(19、21)是尝鲜版,生命周期短;偶数版的 LTS(18、20、22)才会长期维护,正好对应课程要求的「Node 18 / 20 LTS」。
避坑装完 Node 后终端里敲 node -v 提示 command not found / 不是内部或外部命令,九成不是装失败,而是当前这个终端是装之前打开的,环境变量没加载。关掉窗口重新开一个就好;如果还不行,重启一下电脑再看。

Vite:为什么它比 Webpack 快

开发时不打包
先讲为什么老一代工具 Webpack 的思路是「先把所有菜都做好再一起端上桌」——你按下启动键,它要把整个项目几百个文件全部读一遍、打包成一个 bundle,才开始给你预览。项目一大,冷启动就要几十秒,改一个字的反馈也要等一两秒。
Vite(法语里「快」的意思)反过来做:开发阶段根本不打包。浏览器要哪个文件,它就按需编译哪个文件给你;只有真正要上线时才打包。所以冷启动通常在 1 秒内。
动手做先感受一下它的启动速度。等第 4 节建完项目我们再正式跑,这里先看输出长什么样(注意 ready in 后面那个数字)。
启动开发服务器.shbash
pnpm dev
终端输出命令行执行结果
运行结果: $ pnpm dev > hivue@0.0.0 dev /Users/you/code/hivue > vite VITE v6.0.7 ready in 312 ms ➜ Local: http://localhost:5173/ ➜ Network: use --host to expose ➜ press h + enter to show help -- 312 毫秒就起来了。换 Webpack,同规模项目通常要 5 秒以上
讲透:快在哪三件事上
  • 不打包,靠浏览器原生的 ES Module。现代浏览器本身就会 import 别的文件,Vite 只是站在中间做实时编译和转发,省掉了「全量打包」这一步。
  • 依赖预构建用 esbuild。esbuild 是用 Go 写的,比用 JS 写的老打包器快几十倍。Vite 只用它处理 node_modules 里那些不常变的第三方库,预先编译好缓存起来。
  • 改一个文件只重编译那一个文件。这就是热更新(HMR)按毫秒计的原因——第 8 节你会亲眼看到。
  • 上线还是要打包:pnpm build 时 Vite 会切换到 Rollup 做生产构建,产物在 dist/ 目录,这一步在第 12 章讲。

创建项目:npm create vue@latest hivue

一条命令,一个工地
动手做官方脚手架叫 create-vue。在你想放代码的目录下打开终端,跑下面这条命令(hivue 是项目名,也是它要创建的文件夹名)。它不会一口气装完,而是一步步问你问题——用 ↑ ↓ 移动、空格勾选、回车确认。
create-project.shbash
npm create vue@latest hivue
终端输出命令行执行结果
运行结果: $ npm create vue@latest hivue Need to install the following packages: create-vue@3.16.4 Ok to proceed? (y) y > npx > create-vue hivue | Vue.js - The Progressive JavaScript Framework o Select features to include in your project: (↑/↓ to navigate, space to select, enter to confirm) | [ ] TypeScript | [ ] JSX Support | [ ] Router (SPA development) | [ ] Pinia (state management) | [ ] Vitest (unit testing) | [ ] End-to-End Testing | [ ] ESLint (error prevention) | [ ] Prettier (code formatting) o Select features to include in your project: | [x] Router (SPA development) | [x] Pinia (state management) | [x] ESLint (error prevention) o Installed. Now run: | cd hivue | npm install | npm run dev -- 因为命令里已经写了 hivue,所以跳过了「项目名」那一步 -- 本课程只勾这 3 项:Router、Pinia、ESLint
install-and-run.shbash
cd hivue
pnpm install
pnpm dev
终端输出命令行执行结果
运行结果: $ cd hivue $ pnpm install Packages: +34 Progress: resolved 34, reused 34, downloaded 0, added 34, done dependencies: + pinia 2.3.0 + vue 3.5.13 + vue-router 4.5.0 devDependencies: + eslint 9.17.0 + vite 6.0.7 + @vitejs/plugin-vue 5.2.1 Done in 3.2s $ pnpm dev VITE v6.0.7 ready in 322 ms ➜ Local: http://localhost:5173/ -- 浏览器打开这个地址,能看到 Vue 的欢迎页就成功了
讲透:那三个勾是干什么的
  • Router:单页应用的路由。「古典文学网」有 /list、/detail/1、/user 这些地址,全靠它(第 7 章)。
  • Pinia:全局状态仓库。登录后拿到的 token 和用户信息,得存起来给所有页面用,就放这里(第 8 章)。
  • ESLint:代码检查器。写了未使用的变量、漏了 :key、用了 ==,它会在编辑器里直接标红。宁可一开始被它烦,也别等上线才发现错。
  • 没勾的那几项:TypeScript 会给初学阶段增加语法负担,本课程不勾;Vitest / E2E 属于测试,项目做起来再说;Prettier 是格式化工具,可以和 ESLint 二选一,避免两套规则打架。
  • 为什么不手写:脚手架已经配好了 Vite、ESLint、路由、目录规范,手写要花半天还容易漏。能用官方脚手架就别自己造。
避坑项目路径里不要出现中文和空格,比如别建在「我的文档/前端 项目」下。日常 pnpm dev 也许能跑,但一旦遇到要本地编译的依赖(node-gyp 那一类),中文和空格路径会让它报一堆看不懂的错。统一放在 ~/code/hivue、D:\code\hivue 这种纯英文、无空格的路径下最省心。

认识 SFC:一个组件一个文件

template + script setup + style
SFC(.vue 文件)三段式:结构、行为、样式各管一段,合成一个组件<template>页面长什么样HTML + 模板语法<script setup>数据与逻辑ref、函数、请求<style scoped>只管自己这块的样式scoped = 样式不外泄scoped 的意思:这份样式只作用于本组件的元素,不会污染别的组件——组件之间互不打架
先讲为什么传统做法里,一个「作品卡片」要拆成三个文件:card.html 写结构、card.js 写交互、card.css 写样式。改一句话就得在三个文件之间来回跳,而且 card.css 里的 .title 很可能会把别处的标题也一起改了——样式串味是 CSS 的老毛病。
Vue 的答案是 单文件组件(SFC,Single-File Component):把结构、逻辑、样式装进同一个 .vue 文件,各占一个标签块。文件即组件,组件即文件。
动手做在 src/components/ 下新建 HelloCard.vue,手写一遍下面的内容,感受「一个文件就是一个组件」。
src/components/HelloCard.vuevue
<script setup>
// 组合式 API:这里定义的变量和函数,模板里可以直接用
import { ref } from 'vue'

const title = ref('静夜思')
const author = ref('李白')
const views = ref(328)
</script>

<template>
  <div class="card">
    <h3 class="title">{{ title }}</h3>
    <p>作者:{{ author }} 浏览量:{{ views }}</p>
  </div>
</template>

<style scoped>
.card {
  padding: 16px;
  border: 1px solid #e5e7eb;
  border-radius: 8px;
}
.title {
  margin: 0 0 6px;
  color: #8b5a2b;
}
</style>
页面效果组件渲染结果
运行结果: (页面上渲染出一个带浅灰边框、圆角、内边距的小卡片) 静夜思 作者:李白 浏览量:328 -- 浏览器 Elements 面板里能看到 Vue 自动加的唯一属性,如 -- <div data-v-7ba5bd90 class="card">
讲透:三个块各管一件事
  • <template>:管「长什么样」。里面写的是 HTML 加上 Vue 的模板语法({{ }} 插值、v- 指令),下一章专门讲。
  • <script setup>:管「怎么动」。setup 是组合式 API 的写法标志,在这里声明的变量和函数,模板里直接就能用,不需要 return,也没有 this。
  • <style scoped>:管「好不好看」。scoped 是重点——它给这个组件的每个元素加一个唯一属性,样式选择器也只匹配这个属性,所以这里的 .title 绝不会影响别的组件。
  • 文件命名用大驼峰:HelloCard.vue、ArticleItem.vue。因为组件在模板里是当标签用的,大写开头才能和原生 HTML 标签区分开。
  • 一个 SFC 里最多一个 <template>、一个 <script setup>;<style> 可以有好几个(比如一个 scoped 一个全局)。
避坑只写了 <script>、漏掉 setup,模板里就会报 Property "title" was accessed during render but is not defined。setup 这个后缀决定了「不写 return 也能在模板里用」,本课程所有组件都写 <script setup>。另外别把变量写在 <template> 里——那里只能放表达式,放不了声明。

组合式 API 与选项式 API 的对比

Vue 3 为什么推荐组合式
先讲为什么同一个组件,Vue 允许你用两种方式写。老写法叫选项式 API:按「选项」分块,数据放 data、计算属性放 computed、方法放 methods。
问题来了——一个「作品搜索」功能,它的关键词变量在 data、搜索方法在 methods、结果条数在 computed,同一个功能的代码被劈成了三块。功能一多,你就要在几百行里上上下下地跳。新写法组合式 API 换了个组织方式:按功能聚合,一个功能用到的数据、方法、计算属性写在一起。
动手做先看老写法(了解即可,本课程不这么写):一个计数器,数据、计算属性、方法分布在三个选项里。
CounterOld.vue(选项式,仅作对比)vue
<script>
export default {
  data() {
    return { likes: 0 }
  },
  computed: {
    text() {
      return '点赞 ' + this.likes
    }
  },
  methods: {
    addLike() {
      this.likes++
    }
  }
}
</script>
页面效果组件渲染结果
运行结果: (这段代码本身没有输出) 它描述的行为是:页面上显示「点赞 0」,点按钮后依次变成「点赞 1」「点赞 2」。 -- 注意三个选项之间靠 this 互相访问:this.likes、this.addLike
动手做再看组合式:同一个功能,数据、计算属性、方法挨在一起,一眼看完。
CounterNew.vue(组合式)vue
<script setup>
import { ref, computed } from 'vue'

// ---- 点赞功能:相关的东西全放在一起 ----
const likes = ref(0)
const text = computed(() => '点赞 ' + likes.value)

function addLike() {
  likes.value++
}
</script>

<template>
  <button @click="addLike">{{ text }}</button>
</template>
页面效果组件渲染结果
运行结果: (页面上出现一个按钮) 点赞 0 -- 点一次后变成「点赞 1」,再点变成「点赞 2」 -- 和选项式写法的运行效果完全一样,只是代码组织方式不同
讲透:为什么 Vue 3 推荐组合式
  • 代码按功能聚合,而不是按选项分散。一个功能一块,几百行的组件也能一眼定位。
  • 逻辑能复用。ref、computed 这些就是普通函数,可以把「搜索」整套逻辑抽到一个 useSearch.js 里,多个页面共享(第 5 章的组合式函数)。选项式的复用要靠 mixin,来源难追踪、命名易冲突。
  • 没有 this。不用再纠结 this 指向谁、箭头函数里为什么拿不到,对新手最友好。
  • ref 的规矩:ref(0) 返回的是一个「响应式盒子」,在脚本里读写要加 .value(likes.value++),在模板里不用加(Vue 自动帮你拆开)。
  • 两种写法可以混用,但本课程从第 1 章到第 12 章统一用组合式 + <script setup>。

项目目录结构逐个说明

先认路,再干活
项目目录:先认路hivue/├─ src/│ ├─ views/ 页面级组件│ ├─ components/ 可复用小组件│ ├─ composables/ 逻辑积木│ ├─ router/ 路由表│ ├─ stores/ Pinia 仓库│ ├─ api/ 接口函数│ ├─ assets/ 图片等静态资源│ └─ App.vue 根组件└─ index.html 唯一入口为什么这样分?views vs components页面(路由用)与零件(复用)分开composables逻辑复用,不复制粘贴api/ 集中接口地址只写一处,改后端只改这里stores/ 集中登录态等全局数据有唯一出处
动手做在 hivue 目录下看看脚手架给你生成了什么(node_modules 里是几万个依赖文件,先跳过)。
查看目录.shbash
ls -1
ls -1 src
终端输出命令行执行结果
运行结果: $ ls -1 README.md eslint.config.js index.html jsconfig.json package.json public src vite.config.js $ ls -1 src App.vue assets components main.js router stores views -- components 和 views 的区别在第 6 章、第 7 章展开
动手做打开 src/main.js,这是整个应用的入口文件,只有 10 行左右,但它是「启动开关」。
src/main.jsjavascript
import { createApp } from 'vue'
import { createPinia } from 'pinia'

import App from './App.vue'
import router from './router'
import './assets/main.css'

const app = createApp(App)

app.use(createPinia())   // 装上状态仓库
app.use(router)          // 装上路由

app.mount('#app')        // 挂载到 index.html 里的 <div id="app">
页面效果浏览器运行结果
运行结果: (这段代码没有控制台输出) 执行到 app.mount('#app') 时,Vue 会把 App.vue 的内容渲染进 index.html 的 <div id="app"></div> 里。浏览器打开 http://localhost:5173/ 能看到页面, 就说明挂载成功了。
讲透:每个文件/目录的职责
  • index.html:全站唯一的 HTML 入口,里面只有一个空的 <div id="app"></div>。别在这儿写内容——内容都归 App.vue 管。
  • src/main.js:入口 js。创建 app、装上插件(Pinia、Router)、挂载到 #app。全项目只有它干这件事。
  • src/App.vue:根组件。所有页面都是它的子节点,通常只放一个 <RouterView /> 加公共的头尾。
  • src/components/:可复用的小组件,如作品卡片、书评项、分页条。它们不直接对应某个网址。
  • src/views/:页面级组件,一个文件对应一个路由地址,如 HomeView.vue、ArticleListView.vue。判断标准:能被地址栏直接打开吗——能,就放 views。
  • src/router/:路由表,把 /list 这样的地址映射到某个 view 组件(第 7 章)。
  • src/stores/:Pinia 的状态仓库,放跨页面共享的数据,比如登录用户信息(第 8 章)。
  • src/assets/ 与 public/:都是放静态资源的,区别在 assets 里的文件会被构建工具处理(改名、压缩,用 import 引用),public 里的会原样复制到输出目录(用绝对路径 /xxx.png 引用)。
  • node_modules/ 三条纪律:不手改、不提交到 git(.gitignore 已忽略)、删了重装没风险——它随时可以用 pnpm install 照着 package.json 重新生成。

改第一个组件,看热更新(HMR)

改完不用手动刷新
先讲为什么以前调 CSS 是这样:改一行 → 切到浏览器 → 按 F5 → 页面重载 → 滚动条回到顶部 → 重新找到刚才调的那个元素。一次两秒钟,一天两百次。HMR(Hot Module Replacement,模块热替换)把这一串全砍掉了:文件一保存,Vite 只把改动的那一个模块推到浏览器替换掉,页面不刷新、状态不丢。
动手做把 App.vue 清空重写,引入刚才的 HelloCard.vue。保存,看浏览器。
src/App.vuevue
<script setup>
import HelloCard from './components/HelloCard.vue'
</script>

<template>
  <main>
    <h1>古典文学网</h1>
    <HelloCard />
  </main>
</template>
页面效果组件渲染结果
运行结果: (浏览器保持不刷新,下半秒内变成:) 古典文学网 ┌──────────────────────────┐ │ 静夜思 │ │ 作者:李白 浏览量:328 │ └──────────────────────────┘ -- 保存文件的一瞬间就变了,没有按刷新
动手做再把 HelloCard.vue 里的 const title = ref('静夜思') 改成 '春望',保存,然后回头看跑着 pnpm dev 的那个终端窗口。
dev-server.log(保存文件后的终端)bash
pnpm dev
终端输出命令行执行结果
运行结果: $ pnpm dev VITE v6.0.7 ready in 322 ms ➜ Local: http://localhost:5173/ 10:32:15 [vite] hmr update /src/components/HelloCard.vue 10:33:02 [vite] hmr update /src/components/HelloCard.vue -- 每保存一次就多一行 hmr update,只更新这一个文件
讲透:HMR 到底保留了什么
  • 组件状态不丢。在搜索框里输入一半的关键词,改个标题样式,输入框里的内容还在——因为组件没被销毁重建,只是被替换了 render 部分。
  • 什么时候会退化成整页刷新:改了 vite.config.js、改了路由表结构、改了 main.js 这类「启动级别」的文件,Vite 会直接给你重载页面。这是正常的,不是你的代码坏了。
  • 页面报错不一定是代码坏了:先在终端看有没有编译错误,再看浏览器控制台。终端里的红字通常是语法错,浏览器控制台里的红字才多半是运行期错误。
  • 这就是工具链带来的体验:你只负责写 .vue 文件,编译、替换、刷新全是 Vite 在背后跑。这一章开头讲的「工程化」,到这里就变成了实实在在的「省时间」。

Vue 与其他前端框架对比

React / Angular / Svelte 各擅长什么

前端框架也不是「越新越好」。Vue、React、Angular、Svelte 各有侧重,选哪个往往取决于团队习惯和项目规模。下面按常用维度对比,重点看「什么时候该用谁」。

维度Vue 3(本课)ReactAngularSvelte
设计理念渐进式:模板 + 组合式 API函数式:一切皆组件与 Hook一体化平台,强约定编译时框架,无虚拟 DOM
学习曲线平缓:模板写法接近 HTML中等:JSX 与 Hook 规则要适应陡:概念与 CLI 多平缓:写法最接近原生
模板写法单文件组件 .vue(template / script / style)JSX(在 JS 里写标签)HTML 模板 + TypeScript类 HTML 的 .svelte 文件
响应式原理基于 Proxy 的细粒度追踪手动 setState / 不可变更新Zone.js 脏检查编译期收集依赖
生态(路由 / 状态)Vue Router / Pinia,官方维护React Router / Redux,社区多Router / NgRx,官方全套轻量,官方库较少
性能好(编译优化 + 细粒度)好(虚拟 DOM 成熟)稳,但包体偏大极佳(无运行时开销)
典型适用场景教学、中后台、渐进改造旧站大型 SPA、跨端(React Native)企业级大项目轻量应用、极致体积

横向看每一列:React 灵活但要自己定规矩,Angular 强但重,Svelte 快但生态新——Vue 在「好上手」和「能长大」之间找了平衡点。

讲透:怎么选,以及为什么是本课的「Vue」
  • 想最快做出看得见的页面、又不想被 JSX 和 Hook 规则绊住 → Vue。单文件组件把结构、逻辑、样式装在一个文件里,模板语法接近普通 HTML,初学者一眼能懂。
  • 要做跨端 App、或团队已重度使用 React → React。它生态最大、招人最容易,但「状态管理怎么选」这类决策要你自己拍板。
  • 大型企业项目、强类型、统一规范优先 → Angular。它把路由、状态、表单全套给你定好,代价是概念多、包体大、入门陡。
  • 「古典文学网」为什么选 Vue:它是教学项目,前面课程一、二已经把「JSON 接口」讲透了,Vue 的 ref / computed 能让学生把「接口数据 → 页面」这条链路看得清清楚楚;而且 Vue 中文资料和社区极活跃,遇到问题好查。
动手练习
1用一句话说清 Node.js、npm、pnpm、Vite、Vue 各自负责什么,并说出「pnpm dev 这条命令到底启动了哪个程序」。
2在本地按第 4 节创建 hivue 项目(勾 Router + Pinia + ESLint),启动开发服务器,把访问地址写下来。然后在 src/components/ 下新建 MovieTag.vue,用 <script setup> 定义片名和年份两个变量,在模板里渲染出来,并在 App.vue 里用上它。
3迁移练习:「红影赏析」也要做一个前端站点,你打算用同一条命令创建 himovie 项目。请回答两个问题:① 首页要展示的「电影卡片」应该放 components/ 还是 views/?② 电影详情页组件呢?说明你的判断依据。
参考答案
src/components/MovieTag.vuevue
<script setup>
import { ref } from 'vue'

const name = ref('流浪地球')
const year = ref(2019)
</script>

<template>
  <p class="movie">{{ name }}({{ year }})</p>
</template>

<style scoped>
.movie {
  font-size: 14px;
  color: #555;
}
</style>
页面效果组件渲染结果
运行结果: (在 App.vue 里写成 <MovieTag /> 之后,页面上显示) 流浪地球(2019) -- 终端同时打印一行: 10:41:07 [vite] hmr update /src/components/MovieTag.vue

第 1 题的关键:Node 是运行环境,npm/pnpm 是包管理器(读 package.json 下依赖),Vite 是构建工具(pnpm dev 真正启动的就是它),Vue 是我们写的框架。第 3 题的判断依据只有一条:这个组件能不能被地址栏直接打开——电影卡片不能,放 components/;电影详情页要对应 /movie/1 这样的地址,放 views/。

本章小结

📝 本节自测

做完点「检查本节」看解析与得分;做错可重做。