04. Vue3组合式API:代码组织的艺术

Vue3组合式API:代码组织的艺术

Composition API

如果你写过大型Vue2项目,一定遇到过这样的困扰:

  • 一个组件几千行,data、methods、computed分散各处
  • 想复用一段逻辑,只能用mixin,然后命名冲突
  • TypeScript支持不完善,类型推断总有问题

组合式API(Composition API)就是为了解决这些问题而生的。它让你按功能组织代码,而不是按选项组织。

选项式API vs 组合式API

选项式API(Options API)


export default {
  data() {
    return {
      count: 0,
      user: null
    }
  },
  computed: {
    doubleCount() {
      return this.count * 2
    }
  },
  methods: {
    increment() {
      this.count++
    },
    fetchUser() {
      // ...
    }
  },
  mounted() {
    this.fetchUser()
  }
}

问题:

  • 相关代码分散在不同选项中
  • 逻辑复用困难(mixin有命名冲突)
  • TypeScript类型推断不好

组合式API(Composition API)


import { ref, computed, onMounted } from 'vue'

// 计数器逻辑
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
  count.value++
}

// 用户逻辑
const user = ref(null)
async function fetchUser() {
  user.value = await fetch('/api/user').then(r => r.json())
}
onMounted(fetchUser)

优势:

  • 相关代码放在一起
  • 逻辑可以提取成独立函数
  • 完美的TypeScript支持

setup函数

``是setup函数的语法糖:

<!-- 等价于 -->

export default {
  setup() {
    const count = ref(0)
    return { count }
  }
}

<!-- 语法糖 -->

const count = ref(0)

所有顶层绑定自动暴露给模板。

Props和Emits


// 定义props
const props = defineProps({
  title: String
})

// 定义emits
const emit = defineEmits(['update'])

// 使用
console.log(props.title)
emit('update', data)

响应式转换

Vue3提供响应式语法糖(实验性):


// 启用后,ref可以省略.value
let count = $ref(0)
count++  // 自动处理

// 解构保持响应性
const { x, y } = $(useMouse())

组合式函数

组合式函数(Composables)是Vue3复用逻辑的核心方式。

基础示例

// useCounter.js
import { ref, computed } from 'vue'

export function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  const doubleCount = computed(() => count.value * 2)

  function increment() {
    count.value++
  }

  function decrement() {
    count.value--
  }

  function reset() {
    count.value = initialValue
  }

  return {
    count,
    doubleCount,
    increment,
    decrement,
    reset
  }
}
<!-- 使用 -->

import { useCounter } from './useCounter'

const { count, doubleCount, increment } = useCounter(10)

  <p>Count: {{ count }}</p>
  <p>Double: {{ doubleCount }}</p>
  <button>+1</button>

实用组合式函数

useMouse - 追踪鼠标位置:

import { ref, onMounted, onUnmounted } from 'vue'

export function useMouse() {
  const x = ref(0)
  const y = ref(0)

  function update(event) {
    x.value = event.pageX
    y.value = event.pageY
  }

  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))

  return { x, y }
}

useFetch - 数据获取:

import { ref, watchEffect, toValue } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)
  const loading = ref(false)

  async function fetch() {
    loading.value = true
    error.value = null

    try {
      const response = await fetch(toValue(url))
      data.value = await response.json()
    } catch (e) {
      error.value = e
    } finally {
      loading.value = false
    }
  }

  // 支持响应式URL
  watchEffect(fetch)

  return { data, error, loading, refetch: fetch }
}

import { useFetch } from './useFetch'

const { data, loading, error } = useFetch('/api/users')

  <div>Loading...</div>
  <div>Error: {{ error.message }}</div>
  <div>
    <ul>
      <li>
        {{ user.name }}
      </li>
    </ul>
  </div>

useLocalStorage - 本地存储:

import { ref, watch } from 'vue'

export function useLocalStorage(key, defaultValue) {
  const stored = localStorage.getItem(key)
  const data = ref(stored ? JSON.parse(stored) : defaultValue)

  watch(
    data,
    (newVal) => {
      if (newVal === null) {
        localStorage.removeItem(key)
      } else {
        localStorage.setItem(key, JSON.stringify(newVal))
      }
    },
    { deep: true }
  )

  return data
}

import { useLocalStorage } from './useLocalStorage'

const theme = useLocalStorage('theme', 'light')

  <button>
    Toggle Theme
  </button>

provide/inject

跨层级组件通信:

<!-- 祖先组件 -->

import { provide, ref } from 'vue'

const theme = ref('dark')
provide('theme', theme)
provide('toggleTheme', () => {
  theme.value = theme.value === 'light' ? 'dark' : 'light'
})
<!-- 后代组件(任意层级) -->

import { inject } from 'vue'

const theme = inject('theme')
const toggleTheme = inject('toggleTheme')

  <div>
    <button>Toggle</button>
  </div>

响应式provide

// 使用Symbol避免命名冲突
export const ThemeKey = Symbol('theme')

// 提供默认值
const theme = inject(ThemeKey, 'light')

// 只读
provide(ThemeKey, readonly(theme))

模板引用


import { ref, onMounted } from 'vue'

const inputRef = ref(null)

onMounted(() => {
  inputRef.value.focus()
})

v-for中的ref


const listRef = ref([])

function setRef(el) {
  if (el) {
    listRef.value.push(el)
  }
}

  <div>
    {{ item }}
  </div>

组件ref


import ChildComponent from './Child.vue'

const childRef = ref(null)

function callChildMethod() {
  childRef.value.someMethod()
}

异步组件


import { defineAsyncComponent } from 'vue'

// 异步加载
const AsyncComponent = defineAsyncComponent(() =>
  import('./HeavyComponent.vue')
)

// 带加载状态
const AsyncComponentWithOptions = defineAsyncComponent({
  loader: () => import('./HeavyComponent.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorComponent,
  delay: 200,
  timeout: 3000
})

最佳实践

命名约定

// 组合式函数以use开头
useCounter()
useMouse()
useFetch()

// 私有函数以_开头或使用__
function _internalHelper() {}

返回值

// 返回响应式引用
export function useCounter() {
  const count = ref(0)
  return { count }  // 不要返回 count.value
}

// 使用时保持响应性
const { count } = useCounter()
// count是ref,仍然是响应式的

清理副作用

export function useEventListener(target, event, callback) {
  onMounted(() => target.addEventListener(event, callback))
  onUnmounted(() => target.removeEventListener(event, callback))

  // 或者返回清理函数
  return () => target.removeEventListener(event, callback)
}

// 使用
const cleanup = useEventListener(window, 'resize', handleResize)
cleanup()  // 手动清理

参数处理

// 支持ref和原始值
import { toValue } from 'vue'

export function useFetch(url) {
  // toValue会解包ref
  const finalUrl = toValue(url)

  watchEffect(() => {
    fetch(toValue(url))  // 响应式URL
  })
}

// 使用
const url = ref('/api/users')
const { data } = useFetch(url)  // url变化时自动重新fetch

小结

你现在掌握了:

  • 组合式API的核心思想(按功能组织代码)
  • 组合式函数的编写和使用
  • provide/inject跨层级通信
  • 模板引用和异步组件
  • 最佳实践和命名约定

下一期,我们进入Vue3生态,学习Vue Router路由和Pinia状态管理,完成一个实战项目。


练习任务

  1. 编写一个useCounter组合式函数
  2. 编写一个useDebounce防抖函数
  3. 使用provide/inject实现主题切换
  4. 实现一个异步加载的组件

下期预告:《Vue3生态:Router、Pinia与实战项目》—— 路由怎么配?状态怎么管?来做一个完整项目!

Views: 12

03. Vue3基础:响应式系统和组件入门

Vue3基础:响应式系统和组件入门

Vue Logo

终于进入Vue3的世界了!这一期,我们学习Vue3最核心的概念:响应式系统和组件。

理解这两个概念,你就掌握了Vue3的80%。

响应式系统

什么是响应式?

// 普通变量 - 不是响应式的
let count = 0
count++
// Vue不知道count变了,UI不会更新

// 响应式变量 - Vue会追踪变化
const count = ref(0)
count.value++
// Vue知道count变了,自动更新UI

响应式 = 数据变化时,UI自动更新。

ref vs reactive

Vue3提供两种响应式API:

ref:包装单个值

import { ref } from 'vue'

const count = ref(0)
const message = ref('Hello')
const user = ref({ name: 'Vue' })

// 读取和修改需要.value
console.log(count.value)  // 0
count.value++

reactive:包装对象/数组

import { reactive } from 'vue'

const state = reactive({
  count: 0,
  user: {
    name: 'Vue'
  }
})

// 直接访问属性
console.log(state.count)  // 0
state.count++

// 数组
const list = reactive([1, 2, 3])
list.push(4)

怎么选?

场景 推荐
基本类型(数字、字符串) ref
对象 都可以
需要整体替换 ref
解构友好 reactive

我的建议:默认用ref,对象很大时用reactive

模板中自动解包


import { ref } from 'vue'

const count = ref(0)

  <!-- 不需要.value,自动解包 -->
  <p>Count: {{ count }}</p>
  <button>+1</button>

响应式丢失问题

// ❌ 解构会丢失响应性
const state = reactive({ count: 0 })
let { count } = state  // count是普通数字
count++  // 不会触发更新!

// ✅ 使用toRefs
import { toRefs } from 'vue'
const { count } = toRefs(state)  // count是ref
count.value++  // 会触发更新

// ✅ 使用toRef(单个属性)
import { toRef } from 'vue'
const count = toRef(state, 'count')

computed

计算属性:基于响应式数据派生新值,自动缓存。


import { ref, computed } from 'vue'

const firstName = ref('Vue')
const lastName = ref('3')

// 只读计算属性
const fullName = computed(() => <code>${firstName.value} ${lastName.value})

// 可写计算属性
const fullName2 = computed({
  get: () => ${firstName.value} ${lastName.value},
  set: (val) => {
    [firstName.value, lastName.value] = val.split(' ')
  }
})

  

{{ fullName }}

watch

侦听数据变化:


import { ref, watch, watchEffect } from 'vue'

const count = ref(0)

// watch:明确指定侦听源
watch(count, (newVal, oldVal) => {
  console.log(<code>count changed: ${oldVal} -> ${newVal})
})

// watch多个源
watch([count, anotherRef], ([newCount, newAnother]) => {
  // ...
})

// watchEffect:自动追踪依赖
watchEffect(() => {
  console.log(count is ${count.value})
})

// 立即执行 + 可停止
const stop = watchEffect(() => {
  // ...
}, { immediate: true })

// 停止侦听
stop()

watch vs watchEffect

  • watch:明确指定侦听源,可访问新旧值
  • watchEffect:自动追踪,适合副作用

组件基础

定义组件


import { ref } from 'vue'

const count = ref(0)

  <button>
    Clicked {{ count }} times
  </button>

button {
  color: blue;
}

``是Vue3的语法糖,自动暴露变量给模板。

Props:父传子

<!-- Child.vue -->

// 定义props
const props = defineProps({
  title: {
    type: String,
    required: true
  },
  count: {
    type: Number,
    default: 0
  }
})

// 使用props
console.log(props.title)

  <h1>{{ title }}</h1>
  <p>Count: {{ count }}</p>
<!-- Parent.vue -->

import Child from './Child.vue'

Emits:子传父

<!-- Child.vue -->

const emit = defineEmits(['update', 'delete'])

function handleClick() {
  emit('update', { id: 1, name: 'Vue' })
}

  <button>Update</button>
<!-- Parent.vue -->

import Child from './Child.vue'

function onUpdate(data) {
  console.log('Received:', data)
}

v-model:双向绑定

<!-- 自定义组件支持v-model -->

const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])

const value = computed({
  get: () => props.modelValue,
  set: (val) => emit('update:modelValue', val)
})

Vue3.4+可以用defineModel


const modelValue = defineModel()

插槽

<!-- Card.vue -->

  <div class="card">
    <div class="header">

    </div>
    <div class="content">

    </div>
    <div class="footer">

    </div>
  </div>
<!-- 使用 -->

    <h1>Title</h1>

  <p>Content goes here</p>

    <button>OK</button>

作用域插槽:

<!-- List.vue -->

const items = ['Apple', 'Banana', 'Orange']

  <ul>
    <li>

    </li>
  </ul>
<!-- 使用 -->

    {{ index + 1 }}. {{ item }}

模板语法

文本插值


  <p>{{ message }}</p>
  <p>{{ count + 1 }}</p>
  <p>{{ formatDate(date) }}</p>

指令


  <!-- 条件渲染 -->
  <div>Type A</div>
  <div>Type B</div>
  <div>Other</div>

  <!-- 列表渲染 -->
  <ul>
    <li>
      {{ item.name }}
    </li>
  </ul>

  <!-- 事件绑定 -->
  <button>Click</button>
  <button>+1</button>
  ...

  <!-- 属性绑定 -->
  <img />
  <div>...</div>
  <div>...</div>

  <!-- 双向绑定 -->

  ...

  <!-- 显示/隐藏 -->
  <div>...</div>

v-if vs v-show

特性 v-if v-show
渲染方式 条件为假时不渲染 始终渲染,用CSS隐藏
切换成本 高(销毁/重建) 低(只改CSS)
初始成本 低(不渲染) 高(总是渲染)

建议:频繁切换用v-show,条件很少改变用v-if

Class和Style绑定


  <!-- 对象语法 -->
  <div></div>

  <!-- 数组语法 -->
  <div></div>

  <!-- 混合 -->
  <div></div>

  <!-- Style对象 -->
  <div></div>

  <!-- Style数组 -->
  <div></div>

生命周期


import { onMounted, onUpdated, onUnmounted } from 'vue'

onMounted(() => {
  console.log('组件已挂载')
})

onUpdated(() => {
  console.log('组件已更新')
})

onUnmounted(() => {
  console.log('组件已卸载')
})

生命周期钩子:

选项式API 组合式API
beforeCreate setup()
created setup()
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeUnmount onBeforeUnmount
unmounted onUnmounted

小结

你现在掌握了:

  • 响应式系统(ref、reactive、computed、watch)
  • 组件通信(props、emits、v-model、插槽)
  • 模板语法(指令、条件、循环)
  • 生命周期钩子

下一期,我们深入学习组合式API - Vue3最强大的特性。你会学会如何组织代码、复用逻辑、提取组合式函数。


练习任务

  1. 创建一个计数器组件,用ref实现
  2. 创建一个父子组件,通过props和emits通信
  3. 实现一个自定义v-model组件
  4. 使用作用域插槽渲染列表

下期预告:《Vue3组合式API:代码组织的艺术》—— 组合式函数怎么写?如何复用逻辑?

Views: 14

02. Vite快速入门:秒级启动的开发体验

Vite快速入门:秒级启动的开发体验

Vite Logo

如果你用过Webpack,一定经历过这样的痛苦:

  • npm run dev启动要等30秒
  • 改一行代码,热更新要等3秒
  • 配置文件写得想砸键盘

Vite就是为了终结这些痛苦而生的。它的名字来自法语"快"(vite),快到让你怀疑人生。

为什么Vite这么快?

传统打包工具的问题

Webpack的工作方式:

  1. 启动时,从入口文件开始
  2. 递归分析所有依赖
  3. 打包成一个(或几个)大文件
  4. 启动开发服务器

项目越大,依赖越多,打包越慢。

Vite的革命性做法

Vite利用了浏览器的原生ES模块能力:

开发环境

  1. 启动时不打包
  2. 浏览器请求哪个文件,就编译哪个文件
  3. 源码按需编译,秒级启动

生产环境
使用Rollup打包,优化输出

graph LR
    A[浏览器请求] --> B[Vite Dev Server]
    B --> C{已编译?}
    C -->|是| D[返回缓存]
    C -->|否| E[即时编译]
    E --> D

创建第一个Vite项目

# 创建Vue3项目
npm create vite@latest my-vue-app -- --template vue

# 或者用pnpm(推荐)
pnpm create vite my-vue-app --template vue

# 进入项目
cd my-vue-app

# 安装依赖
pnpm install

# 启动开发服务器
pnpm dev

输出:

  VITE v5.1.0  ready in 234 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h + enter to show help

234毫秒!这就是Vite的速度。

项目结构

my-vue-app/
├── index.html          # 入口HTML
├── package.json        # 项目配置
├── vite.config.js      # Vite配置
├── public/             # 静态资源(不处理)
│   └── favicon.ico
└── src/                # 源代码
    ├── main.js         # 入口文件
    ├── App.vue         # 根组件
    └── assets/         # 资源文件
        └── vue.svg

index.html

Vite把index.html作为入口,而不是main.js


    <title>Vite + Vue</title>

    <div id="app"></div>

注意type="module",这告诉浏览器用ES模块方式加载。

main.js

import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')

App.vue


import { ref } from 'vue'

const count = ref(0)

  <div>
    <h1>Vite + Vue</h1>
    <button>
      Count: {{ count }}
    </button>
  </div>

h1 {
  color: #42b883;
}

Vite配置

基础配置

// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],

  // 开发服务器
  server: {
    port: 3000,          // 端口
    open: true,          // 自动打开浏览器
    host: '0.0.0.0',     // 允许局域网访问
    cors: true           // 启用CORS
  },

  // 构建配置
  build: {
    outDir: 'dist',      // 输出目录
    sourcemap: true      // 生成sourcemap
  }
})

路径别名

import { defineConfig } from 'vite'
import { fileURLToPath, URL } from 'node:url'

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  }
})

使用:

import MyComponent from '@/components/MyComponent.vue'
// 而不是
import MyComponent from '../../../components/MyComponent.vue'

环境变量

# .env.development
VITE_API_URL=http://localhost:3000/api

# .env.production
VITE_API_URL=https://api.example.com

使用:

const apiUrl = import.meta.env.VITE_API_URL

注意:只有VITE_前缀的变量才会暴露给客户端代码。

代理API请求

export default defineConfig({
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  }
})

CSS处理

CSS Modules


  <div>
    <h1>Hello</h1>
  </div>

.container {
  padding: 20px;
}
.title {
  color: blue;
}

预处理器

# 安装Sass
pnpm add -D sass

  <div class="container">
    <h1>Hello</h1>
  </div>

$primary-color: #42b883;

.container {
  padding: 20px;

  h1 {
    color: $primary-color;
  }
}

全局CSS

// vite.config.js
export default defineConfig({
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: <code>@import "@/styles/variables.scss";
      }
    }
  }
})

静态资源

引入方式

// 直接引入
import logo from '@/assets/logo.png'

// URL形式
const imgUrl = new URL('./assets/logo.png', import.meta.url).href

// 动态引入(Vite特有)
const modules = import.meta.glob('./assets/*.png')
// 返回: { './assets/a.png': () => import('./assets/a.png'), ... }

public目录

public/下的文件不会被处理,直接复制到输出目录:

<img src="/logo.png" />

访问:http://localhost:5173/logo.png

构建优化

代码分割

// vite.config.js
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          'vendor': ['vue', 'vue-router', 'pinia'],
          'ui': ['element-plus']
        }
      }
    }
  }
})

分析打包大小

pnpm add -D rollup-plugin-visualizer
import { visualizer } from 'rollup-plugin-visualizer'

export default defineConfig({
  plugins: [
    vue(),
    visualizer({ open: true })
  ]
})

构建后会生成一个HTML文件,可视化显示每个模块的大小。

TypeScript支持

# 创建TS项目
pnpm create vite my-app --template vue-ts

Vite原生支持TypeScript,无需额外配置(但需要vue-tsc做类型检查)。

pnpm add -D vue-tsc
// package.json
{
  "scripts": {
    "build": "vue-tsc && vite build"
  }
}

常用插件

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'           // JSX支持
import vueDevTools from 'vite-plugin-vue-devtools'   // Vue DevTools
import compression from 'vite-plugin-compression'    // Gzip压缩
import { visualizer } from 'rollup-plugin-visualizer'

export default defineConfig({
  plugins: [
    vue(),
    vueJsx(),
    vueDevTools(),
    compression(),
    visualizer()
  ]
})

常见问题

Q: 为什么生产构建用Rollup?

A: Rollup生成的代码更干净、更小。开发时用原生ES模块追求速度,生产时用Rollup追求优化。

Q: 如何处理CommonJS模块?

A: Vite会自动转换,大多数情况无需处理。如果有问题:

export default defineConfig({
  optimizeDeps: {
    include: ['some-cjs-module']
  }
})

Q: 热更新不生效?

A: 检查文件是否被正确引入,确保没有语法错误。可以在浏览器控制台查看HMR状态。

Vite vs Webpack

特性 Vite Webpack
启动速度 ⚡ 秒级 🐢 项目大时慢
热更新 ⚡ 毫秒级 🐢 较慢
配置复杂度 📝 极简 😵 复杂
生态成熟度 🌱 快速发展 🌳 非常成熟
生产打包 Rollup Webpack

结论:新项目首选Vite,除非必须用Webpack生态的特定插件。

小结

你现在掌握了:

  • Vite为什么快(原生ES模块)
  • 创建和配置Vite项目
  • 路径别名、环境变量、代理
  • CSS处理(Modules、预处理器)
  • 静态资源引入
  • 构建优化技巧

下一期,我们正式进入Vue3基础。你会学到Vue3的核心概念:响应式、组件、模板语法。


练习任务

  1. 创建一个Vite + Vue3项目
  2. 配置@路径别名
  3. 配置开发服务器代理
  4. 使用Sass编写样式

下期预告:《Vue3基础:响应式系统和组件入门》—— ref和reactive怎么选?组件怎么通信?

Views: 20

01. 现代前端开发入门:从ES6到Vue3的第一步

现代前端开发入门:从ES6到Vue3的第一步

Modern Frontend

你刚决定学习前端开发,打开教程一看:ES6、Node.js、npm、Webpack、Vite、TypeScript...这些名词像天书一样砸过来。别慌,这篇文章帮你理清现代前端的"基础设施",为Vue3开发打下坚实基础。

现代前端开发三件套

在Vue3之前,你需要先掌握三个基础:

  1. ES6+语法 - 现代JavaScript的写法
  2. Node.js + npm - 包管理和构建工具的基础
  3. 模块化开发 - 代码组织方式

让我们一个个攻克。

ES6+必学语法

ES6(ECMAScript 2015)是JavaScript的重大升级,Vue3大量使用这些新特性。

let和const

// ❌ 旧写法
var name = 'Vue'
var name = 'React' // 可以重复声明,容易出bug

// ✅ 新写法
const app = 'Vue3' // 常量,不可重新赋值
let count = 0 // 变量,可以修改
count = 1 // OK
// app = 'React' // 报错!const不能重新赋值

规则:默认用const,需要修改时才用let,永远不用var

箭头函数

// ❌ 旧写法
const add = function(a, b) {
return a + b
}

// ✅ 新写法
const add = (a, b) => {
return a + b
}

// 更简洁:单行可以省略大括号和return
const add = (a, b) => a + b

// 单参数可以省略括号
const double = n => n * 2

关键区别:箭头函数没有自己的this,它会继承外层作用域的this。这在Vue中非常重要。

const obj = {
count: 0,
// ❌ 普通函数,this指向调用者
incrementOld: function() {
setTimeout(function() {
console.log(this.count) // undefined!
}, 1000)
},
// ✅ 箭头函数,this继承外层
incrementNew: function() {
setTimeout(() => {
console.log(this.count) // 0
}, 1000)
}
}

解构赋值

// 对象解构
const user = { name: 'Vue', version: 3, author: 'Evan' }
const { name, version } = user
console.log(name, version) // 'Vue' 3

// 重命名
const { name: framework } = user
console.log(framework) // 'Vue'

// 默认值
const { license = 'MIT' } = user
console.log(license) // 'MIT'

// 数组解构
const [first, second, ...rest] = [1, 2, 3, 4, 5]
console.log(first, second, rest) // 1 2 [3, 4, 5]

展开运算符

// 数组展开
const arr1 = [1, 2, 3]
const arr2 = [...arr1, 4, 5]
console.log(arr2) // [1, 2, 3, 4, 5]

// 对象展开(浅拷贝)
const defaults = { theme: 'light', lang: 'zh' }
const settings = { ...defaults, theme: 'dark' }
console.log(settings) // { theme: 'dark', lang: 'zh' }

// 合并对象
const merged = { ...obj1, ...obj2 }

模板字符串

const name = 'Vue'
const version = 3

// ❌ 旧写法
const msg = 'Welcome to ' + name + ' ' + version

// ✅ 新写法
const msg = <code>Welcome to ${name} ${version}

// 多行字符串
const html = `

${name}

`

简写属性

const name = 'Vue'
const version = 3

// ❌ 旧写法
const app = {
name: name,
version: version,
sayHello: function() {
return 'Hello'
}
}

// ✅ 新写法
const app = {
name, // 等同于 name: name
version, // 等同于 version: version
sayHello() { // 方法简写
return 'Hello'
}
}

Promise和async/await

// Promise
fetch('/api/users')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error))

// async/await(推荐)
async function getUsers() {
try {
const response = await fetch('/api/users')
const data = await response.json()
console.log(data)
} catch (error) {
console.error(error)
}
}

// 并行请求
async function getAll() {
const [users, posts] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/posts').then(r => r.json())
])
return { users, posts }
}

数组方法

const numbers = [1, 2, 3, 4, 5]

// map:转换每个元素
const doubled = numbers.map(n => n * 2) // [2, 4, 6, 8, 10]

// filter:筛选元素
const evens = numbers.filter(n => n % 2 === 0) // [2, 4]

// find:查找元素
const found = numbers.find(n => n > 3) // 4

// reduce:累积计算
const sum = numbers.reduce((acc, n) => acc + n, 0) // 15

// some/every:判断
const hasEven = numbers.some(n => n % 2 === 0) // true
const allPositive = numbers.every(n => n > 0) // true

// 链式调用
const result = numbers
.filter(n => n > 2)
.map(n => n * 10)
.reduce((acc, n) => acc + n, 0) // 120

可选链和空值合并

const user = {
profile: {
name: 'Vue'
// avatar 可能不存在
}
}

// ❌ 旧写法(容易报错)
const avatar = user && user.profile && user.profile.avatar

// ✅ 可选链
const avatar = user?.profile?.avatar // undefined,不会报错

// ❌ 旧写法
const name = user.name || 'Anonymous' // 空字符串也会用默认值

// ✅ 空值合并
const name = user.name ?? 'Anonymous' // 只有null/undefined才用默认值

Node.js和npm

什么是Node.js?

Node.js让JavaScript可以脱离浏览器运行。它主要用于:

  1. 运行构建工具(Vite、Webpack)
  2. 服务端开发(Express、Nest)
  3. 运行脚本

下载安装:https://nodejs.org/

npm基础命令

# 初始化项目
npm init -y

# 安装依赖
npm install vue # 安装到dependencies
npm install -D vite # 安装到devDependencies
npm install -g pnpm # 全局安装

# 版本管理
npm install vue@3 # 指定版本
npm install vue@latest # 最新版本

# 运行脚本
npm run dev # 开发服务器
npm run build # 构建生产版本

# 其他
npm update # 更新依赖
npm outdated # 检查过时依赖
npm audit # 安全检查

package.json

{
"name": "my-vue-app",
"version": "1.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.4.0"
},
"devDependencies": {
"vite": "^5.0.0"
}
}

pnpm:更快的替代品

# 安装pnpm
npm install -g pnpm

# 使用(命令基本相同)
pnpm install
pnpm add vue
pnpm dev

pnpm优点:

  • 更快(硬链接,节省磁盘空间)
  • 更严格(避免幽灵依赖)
  • 更安全(更好的依赖管理)

ES模块

import/export

// utils.js - 导出
export const PI = 3.14159

export function add(a, b) {
return a + b
}

export default class Calculator {
// ...
}

// main.js - 导入
import Calculator, { PI, add } from './utils.js'
import * as utils from './utils.js'

动态导入

// 懒加载
const module = await import('./heavy-module.js')

// Vue路由懒加载
const routes = [
{
path: '/about',
component: () => import('./views/About.vue')
}
]

开发环境推荐

VSCode必备插件

  1. Vue - Official - Vue语法高亮和智能提示
  2. ESLint - 代码规范检查
  3. Prettier - 代码格式化
  4. Volar - Vue3专用(已集成到Vue - Official)

推荐配置

// .vscode/settings.json
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
}

小结

你现在掌握了:

  • ES6+核心语法(箭头函数、解构、展开、async/await)
  • Node.js和npm基础操作
  • ES模块的import/export
  • 开发环境配置

下一期,我们将学习Vite - Vue3官方推荐的构建工具。它快得让你怀疑人生,配置简单到让你感动流泪。


练习任务

  1. 用解构赋值提取对象属性
  2. 用map/filter/reduce处理数组
  3. 用async/await改写Promise链式调用
  4. 创建一个npm项目,安装vue依赖

下期预告:《Vite快速入门:秒级启动的开发体验》—— 为什么Vite这么快?如何配置Vite项目?

Views: 11

Git实战宝典:从翻车现场到绝地求生

Git实战宝典:从翻车现场到绝地求生

学了前四期,你已经掌握了Git的理论和最佳实践。但现实世界不是童话,翻车才是常态。

这一期,我们直面那些让新手崩溃、让老手头秃的真实场景。每个问题都有救,只要你知道正确的方法。

紧急救援手册

场景1:commit后发现写错了

情况A:还没push

# 修改最后一次提交
git commit --amend -m "正确的提交信息"

# 或者补充文件
git add forgotten-file.txt
git commit --amend --no-edit

情况B:已经push了

# 如果只有你在这个分支工作
git commit --amend -m "正确的提交信息"
git push --force-with-lease

# 如果有别人也在用这个分支,不要用amend!
# 而是创建新的修复提交
git revert HEAD
git push

场景2:提交到错误的分支

# 你在main分支提交了,但应该在feature分支

# 1. 撤销当前提交,保留改动
git reset HEAD~1 --soft

# 2. 切换到正确的分支
git checkout feature-branch

# 3. 重新提交
git commit -m "feat: xxx"

场景3:需要撤回多个提交

# 撤回最近3个提交,保留改动在工作区
git reset HEAD~3

# 撤回最近3个提交,保留改动在暂存区
git reset HEAD~3 --soft

# 撤回最近3个提交,丢弃所有改动(危险!)
git reset HEAD~3 --hard

场景4:误删分支

# 查找被删分支的最后一个提交
git reflog

# 找到类似这样的记录
# abc1234 HEAD@{5}: checkout: moving from feature-xxx to main

# 重建分支
git checkout -b feature-xxx abc1234

场景5:误删文件

# 删除了工作区文件,想恢复
git checkout -- deleted-file.txt
# 或
git restore deleted-file.txt

# 删除了文件并已提交
git checkout HEAD~1 -- deleted-file.txt
git commit -m "restore: 恢复误删文件"

场景6:需要撤销已push的提交

# 方法1:revert(推荐,不改写历史)
git revert 
git push

# 方法2:reset + force push(危险,仅限个人分支)
git reset --hard 
git push --force-with-lease

revert创建一个新提交来撤销改动,不改写历史,适合已push的提交。reset回退指针,会改写历史。

场景7:merge出错想重来

# 合并过程中发现冲突太多,想放弃
git merge --abort

# 已经合并完成但想撤销
git reset --hard HEAD~1

场景8:需要合并特定的几个提交

# 方法1:cherry-pick
git cherry-pick   

# 方法2:交互式rebase
git rebase -i 
# 在编辑器中把不需要的提交标记为drop

历史改写

修改历史提交信息

# 修改最近3个提交的信息
git rebase -i HEAD~3

# 把要修改的提交前面的pick改成reword
# 保存后Git会逐个让你编辑提交信息

合并历史提交

git rebase -i HEAD~3

# 把后面几个提交的pick改成squash
# 保存后编辑合并后的提交信息

删除历史提交

git rebase -i HEAD~5

# 把要删除的提交改成drop

从历史中删除敏感文件

# 使用BFG Repo-Cleaner(快)
bfg --delete-files secrets.yml
git reflog expire --expire=now --all
git gc --prune=now --aggressive
git push --force

# 使用git filter-repo(现代推荐)
pip install git-filter-repo
git filter-repo --path secrets.yml --invert-paths

# 使用git filter-branch(慢,已废弃)
git filter-branch --force --index-filter \
  'git rm --cached --ignore-unmatch secrets.yml' \
  --prune-empty --tag-name-filter cat -- --all

重要:改写历史后通知所有协作者重新clone!

大文件处理

问题:仓库太大

# 查看哪些文件占用空间
git rev-list --objects --all | \
  git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' | \
  sed -n 's/^blob //p' | \
  sort --numeric-sort --key=2 | \
  tail -20

解决方案1:Git LFS

# 安装Git LFS
brew install git-lfs  # Mac
apt install git-lfs   # Ubuntu

# 初始化
git lfs install

# 跟踪大文件
git lfs track "*.psd"
git lfs track "*.zip"
git lfs track "assets/**"

# 查看跟踪规则
git lfs track

# 提交跟踪规则
git add .gitattributes
git commit -m "chore: 配置Git LFS"

解决方案2:清理历史

# 从历史中彻底删除大文件
git filter-repo --path huge-file.zip --invert-paths

# 清理垃圾
git reflog expire --expire=now --all
git gc --prune=now --aggressive

性能优化

clone太慢

# 浅克隆(只克隆最近的历史)
git clone --depth 1 https://github.com/user/repo.git

# 单分支克隆
git clone --single-branch --branch main https://github.com/user/repo.git

# 后续获取完整历史
git fetch --unshallow

仓库太大

# 清理无用对象
git gc

# 更激进的清理
git gc --aggressive --prune=now

# 清理reflog
git reflog expire --expire=now --all
git gc --prune=now

status太慢

# 禁用文件监控(大仓库)
git config core.fsmonitor false
git config core.untrackedCache false

子模块

添加子模块

git submodule add https://github.com/user/lib.git libs/lib

克隆包含子模块的仓库

# 方法1:克隆时自动初始化
git clone --recursive https://github.com/user/repo.git

# 方法2:克隆后手动初始化
git clone https://github.com/user/repo.git
cd repo
git submodule init
git submodule update

更新子模块

# 更新到最新
git submodule update --remote

# 更新所有子模块
git submodule foreach git pull origin main

删除子模块

# 删除子模块(Git 1.8.3+)
git submodule deinit libs/lib
git rm libs/lib
rm -rf .git/modules/libs/lib

常见错误

error: Your local changes would be overwritten

# 情况1:想保留本地修改
git stash
git pull
git stash pop

# 情况2:丢弃本地修改
git reset --hard
git pull

fatal: refusing to merge unrelated histories

# 两个仓库历史不相关,需要强制合并
git pull origin main --allow-unrelated-histories

! [rejected] main -> main (non-fast-forward)

# 远程有新提交,先拉取
git pull --rebase origin main
git push origin main

fatal: Authentication failed

# 检查SSH密钥
ssh -T git@github.com

# 检查远程URL
git remote -v

# 切换到SSH
git remote set-url origin git@github.com:user/repo.git

# 或使用token
git remote set-url origin https://@github.com/user/repo.git

fatal: not a git repository

# 当前目录不在Git仓库中
# 初始化仓库
git init

# 或进入正确的目录
cd /path/to/repo

调试技巧

查看某行代码的修改历史

# 查看文件的每一行是谁在什么时候改的
git blame filename.txt

# 只看某几行
git blame -L 10,20 filename.txt

# 查看某行代码的完整历史
git log -p -S "特定代码" filename.txt

找出引入bug的提交

# 二分查找
git bisect start
git bisect bad          # 当前版本有bug
git bisect good v1.0    # v1.0版本正常
# Git会自动跳到中间提交,你测试后标记
git bisect good/bad
# 重复直到找到
git bisect reset

查看reflog(操作日志)

# 查看所有操作记录
git reflog

# 找到误删的提交
git checkout 

最佳实践总结

DO

  1. 频繁提交:小步快跑,每个逻辑改动一个提交
  2. 写好提交信息:让未来的自己和队友能看懂
  3. 先pull再push:避免不必要的冲突
  4. 使用分支:不要在main上直接开发
  5. 定期备份:重要分支推送到远程

DON’T

  1. 不要push --force:除非你确定自己在做什么
  2. 不要commit敏感信息:密码、密钥一旦push就很难彻底清除
  3. 不要在公共分支rebase:会改写历史,坑队友
  4. 不要忽略.gitignore:node_modules、.env必须忽略
  5. 不要提交大文件:使用Git LFS

Git速查表

# 初始化
git init                          # 初始化仓库
git clone                    # 克隆仓库

# 日常操作
git status                        # 查看状态
git add                     # 添加到暂存区
git commit -m "message"           # 提交
git push                          # 推送
git pull                          # 拉取

# 分支操作
git branch                  # 创建分支
git checkout              # 切换分支
git checkout -b           # 创建并切换
git merge                 # 合并分支
git branch -d             # 删除分支

# 撤销操作
git checkout --             # 撤销工作区修改
git reset HEAD              # 撤销暂存
git commit --amend                # 修改最后一次提交
git revert                # 撤销提交(安全)
git reset --hard          # 回退到指定提交(危险)

# 历史查看
git log --oneline --graph         # 图形化日志
git blame                   # 查看每行修改记录
git reflog                        # 操作历史

# 暂存工作
git stash                         # 暂存当前工作
git stash pop                     # 恢复暂存

系列总结

恭喜你完成了整个Git系列!

第一期:Git基础 - 从安装到日常操作
第二期:分支管理 - 在平行宇宙中开发
第三期:团队协作 - Pull Request与代码审查
第四期:自动化 - 钩子、CI/CD与效率工具
第五期:实战宝典 - 从翻车到救命

你现在已经是Git高手了。但记住:Git只是工具,真正重要的是团队协作的规范和习惯。工具再好,用不好也是白搭。

保持学习,保持实践,保持分享。Git的世界还有很多宝藏等你探索。


进阶资源


恭喜!你已经掌握了Git的全部核心技能。去征服代码世界吧!

Views: 48