Bootstrap

Nuxt.js 从入门到精通【为老弟拟定的前端学习视图】

课程简介

本课程专为零基础学员设计,涵盖从 Nuxt.js 的基本概念到高级应用的全面内容。通过理论讲解与实际项目相结合,学员将逐步掌握开发高性能、SEO 优化的现代 Web 应用的能力。课程遵循认知负荷理论,优化学习流程,确保学员高效吸收和应用知识。

教学原则依据认知负荷理论

  • 分段学习:将复杂内容拆分为小模块,逐步深入,避免一次性输入过多信息。
  • 多样化教学:结合视频、文字、代码示例和实践练习,满足不同学习风格。
  • 即时反馈:通过练习和项目提供即时反馈,帮助学员巩固知识,及时纠正错误。
  • 减少干扰:聚焦核心内容,避免不必要的信息,降低认知负荷。
  • 促进内在加工:通过实际操作和项目应用,增强理解和记忆,促进知识的内在化。

模块一:课程导论与开发环境搭建

学习目标

  • 理解课程结构和学习资源
  • 掌握 Nuxt.js 的基本概念和优势
  • 完成开发环境的搭建

详细内容

1.1 课程介绍
  • 课程目标与结构

    • 介绍课程的整体目标和各模块内容,帮助学员了解学习路径和预期成果。
    • 案例:展示一个成功完成课程的学员项目示例。
  • 学习资源与参考资料

    • 推荐书籍、网站和社区资源,指导学员如何利用官方文档和在线资源辅助学习。
    • 实战:如何高效使用 Nuxt.js 官方文档查找信息。
1.2 Nuxt.js 概述
  • 什么是 Nuxt.js?

    • Nuxt.js 的定义、起源及其与 Vue.js 的关系。
    • 案例分析:对比使用纯 Vue.js 和使用 Nuxt.js 开发同一个应用的差异。
    // 使用纯 Vue.js 创建一个简单页面
    import Vue from 'vue';
    import App from './App.vue';
    
    new Vue({
      render: h => h(App),
    }).$mount('#app');
    
    // 使用 Nuxt.js 创建相同的页面
    // pages/index.vue
    <template>
      <div>
        <h1>Hello, Nuxt.js!</h1>
      </div>
    </template>
    
    <script>
    export default {
      name: 'HomePage',
    };
    </script>
    
  • Nuxt.js 的优势与应用场景

    • 服务端渲染(SSR)与静态生成(SSG)的优势。
    • SEO 优化、性能提升和开发体验的具体体现。
    • 实战案例:分析一个使用 Nuxt.js 实现的高流量电商网站。
1.3 开发环境搭建
  • Node.js 和 npm 的安装

    • 下载与安装 Node.js(含 npm),验证安装成功的方法。
    • 实战:编写一个简单的 Node.js 脚本,确保环境配置正确。
    # 检查 Node.js 和 npm 是否安装成功
    node -v
    npm -v
    
    // 简单的 Node.js 脚本
    // hello.js
    console.log('Hello, Node.js!');
    
    # 运行脚本
    node hello.js
    
  • 创建第一个 Nuxt.js 应用

    • 使用 npx create-nuxt-app 创建项目,运行开发服务器并查看初始页面。
    • 案例:解读初始项目结构,理解各个文件的作用。
    npx create-nuxt-app my-nuxt-app
    cd my-nuxt-app
    npm run dev
    
    <!-- pages/index.vue -->
    <template>
      <div>
        <h1>Welcome to Nuxt.js!</h1>
        <p>This is your first Nuxt.js application.</p>
      </div>
    </template>
    
    <script>
    export default {
      name: 'HomePage',
    };
    </script>
    
  • 代码编辑器推荐及基本配置

    • 推荐使用 VS Code,安装必要的扩展(如 ESLint、Prettier)。
    • 实战:配置 VS Code 的开发环境,设置代码格式化和 lint 规则。
    // .eslintrc.js
    module.exports = {
      extends: ['@nuxtjs', 'plugin:prettier/recommended'],
      rules: {},
    };
    
    // .prettierrc
    {
      "semi": true,
      "singleQuote": true,
      "trailingComma": "es5"
    }
    
    # 安装 ESLint 和 Prettier
    npm install --save-dev eslint prettier eslint-config-prettier eslint-plugin-prettier
    

学习活动

  • 视频讲解:逐步演示环境搭建过程,确保每一步学员都能跟上。
  • 动手练习:学员自行搭建开发环境并创建第一个 Nuxt.js 项目。
  • 检查点:完成项目运行截图上传,导师进行反馈和指导。

资源与参考


模块二:Vue.js 基础回顾

学习目标

  • 回顾并巩固 Vue.js 的核心概念
  • 理解组件、Props、State 和生命周期
  • 掌握 Vue Hooks 的基本使用

详细内容

2.1 Vue.js 基础
  • 组件与模板

    • 组件的定义与使用,理解模板语法及其与 JavaScript 的关系。
    • 案例:创建一个可复用的按钮组件,并在不同页面中使用。
    <!-- components/Button.vue -->
    <template>
      <button @click="handleClick">{{ label }}</button>
    </template>
    
    <script>
    export default {
      name: 'Button',
      props: {
        label: {
          type: String,
          required: true,
        },
      },
      methods: {
        handleClick() {
          this.$emit('click');
        },
      },
    };
    </script>
    
    <style scoped>
    button {
      padding: 0.5rem 1rem;
      background-color: #007bff;
      color: white;
      border: none;
      border-radius: 5px;
      cursor: pointer;
    }
    
    button:hover {
      background-color: #0056b3;
    }
    </style>
    
    <!-- pages/index.vue -->
    <template>
      <div>
        <h1>Welcome to Nuxt.js!</h1>
        <Button label="Click Me" @click="handleClick" />
      </div>
    </template>
    
    <script>
    import Button from '~/components/Button.vue';
    
    export default {
      components: {
        Button,
      },
      methods: {
        handleClick() {
          alert('Button Clicked!');
        },
      },
    };
    </script>
    
  • Props 和 Data

    • Props 的传递与使用,Data 的定义与管理,理解 Props 与 Data 的区别与联系。
    • 实战:开发一个带有动态数据的用户卡片组件。
    <!-- components/UserCard.vue -->
    <template>
      <div class="card">
        <h2>{{ user.name }}</h2>
        <p>Email: {{ user.email }}</p>
        <button @click="like">Like ({{ likes }})</button>
      </div>
    </template>
    
    <script>
    export default {
      name: 'UserCard',
      props: {
        user: {
          type: Object,
          required: true,
        },
      },
      data() {
        return {
          likes: 0,
        };
      },
      methods: {
        like() {
          this.likes += 1;
        },
      },
    };
    </script>
    
    <style scoped>
    .card {
      border: 1px solid #ccc;
      padding: 1rem;
      margin: 1rem 0;
      border-radius: 5px;
      background-color: white;
    }
    
    button {
      padding: 0.3rem 0.6rem;
      background-color: #28a745;
      color: white;
      border: none;
      border-radius: 3px;
      cursor: pointer;
    }
    
    button:hover {
      background-color: #218838;
    }
    </style>
    
    <!-- pages/index.vue -->
    <template>
      <div>
        <h1>User Information</h1>
        <UserCard :user="user" />
      </div>
    </template>
    
    <script>
    import UserCard from '~/components/UserCard.vue';
    
    export default {
      components: {
        UserCard,
      },
      data() {
        return {
          user: { name: 'John Doe', email: '[email protected]' },
        };
      },
    };
    </script>
    
  • 生命周期钩子

    • 组件的生命周期概述,常用生命周期钩子介绍(如 createdmounteddestroyed)。
    • 案例:实现一个计时器组件,展示生命周期钩子的应用。
    <!-- components/Timer.vue -->
    <template>
      <div>Timer: {{ count }} seconds</div>
    </template>
    
    <script>
    export default {
      name: 'Timer',
      data() {
        return {
          count: 0,
          timer: null,
        };
      },
      mounted() {
        this.timer = setInterval(() => {
          this.count += 1;
        }, 1000);
      },
      beforeDestroy() {
        clearInterval(this.timer);
      },
    };
    </script>
    
    <!-- pages/index.vue -->
    <template>
      <div>
        <h1>Welcome to Nuxt.js!</h1>
        <Timer />
      </div>
    </template>
    
    <script>
    import Timer from '~/components/Timer.vue';
    
    export default {
      components: {
        Timer,
      },
    };
    </script>
    
2.2 Composition API 入门
  • useState 和 useEffect 等组合式 API

    • refreactive 的定义与使用场景,watchcomputed 的定义与副作用管理。
    • 实战:创建一个计数器应用,使用 Composition API 管理状态和副作用。
    <!-- components/Counter.vue -->
    <template>
      <div>
        <p>Current Count: {{ count }}</p>
        <button @click="increment">Increment</button>
        <button @click="decrement">Decrement</button>
      </div>
    </template>
    
    <script setup>
    import { ref, watch } from 'vue';
    
    const count = ref(0);
    
    const increment = () => {
      count.value += 1;
    };
    
    const decrement = () => {
      count.value -= 1;
    };
    
    watch(count, (newVal) => {
      document.title = `Count: ${newVal}`;
    });
    </script>
    
    <style scoped>
    button {
      padding: 0.3rem 0.6rem;
      margin-right: 0.5rem;
      background-color: #007bff;
      color: white;
      border: none;
      border-radius: 3px;
      cursor: pointer;
    }
    
    button:hover {
      background-color: #0056b3;
    }
    </style>
    
    <!-- pages/index.vue -->
    <template>
      <div>
        <h1>Counter Example</h1>
        <Counter />
      </div>
    </template>
    
    <script setup>
    import Counter from '~/components/Counter.vue';
    </script>
    
  • 自定义 Composables

    • 自定义 Composable 的概念与优势,创建和使用自定义 Composables 示例。
    • 案例:开发一个用于表单输入管理的自定义 Composable。
    // composables/useForm.js
    import { reactive } from 'vue';
    
    export default function useForm(initialValues) {
      const values = reactive({ ...initialValues });
    
      const handleChange = (e) => {
        const { name, value } = e.target;
        values[name] = value;
      };
    
      const resetForm = () => {
        Object.assign(values, initialValues);
      };
    
      return { values, handleChange, resetForm };
    }
    
    <!-- components/LoginForm.vue -->
    <template>
      <form @submit.prevent="handleSubmit">
        <div>
          <label>Email:</label>
          <input name="email" v-model="values.email" @input="handleChange" required />
        </div>
        <div>
          <label>Password:</label>
          <input name="password" type="password" v-model="values.password" @input="handleChange" required />
        </div>
        <button type="submit">Login</button>
      </form>
    </template>
    
    <script setup>
    import useForm from '~/composables/useForm';
    
    const { values, handleChange, resetForm } = useForm({ email: '', password: '' });
    
    const handleSubmit = () => {
      console.log('Form Submitted:', values);
      resetForm();
    };
    </script>
    
    <style scoped>
    form {
      display: flex;
      flex-direction: column;
      max-width: 300px;
    }
    
    div {
      margin-bottom: 1rem;
    }
    
    label {
      margin-bottom: 0.3rem;
    }
    
    input {
      padding: 0.5rem;
      border: 1px solid #ccc;
      border-radius: 3px;
    }
    
    button {
      padding: 0.5rem;
      background-color: #28a745;
      color: white;
      border: none;
      border-radius: 3px;
      cursor: pointer;
    }
    
    button:hover {
      background-color: #218838;
    }
    </style>
    

学习活动

  • 视频讲解:详细解释每个概念并演示代码示例,确保学员理解理论知识。
  • 动手练习:创建简单的 Vue 组件,使用 Props 和 Data 实现交互。
  • 练习项目:构建一个简单的计数器应用,应用 Composition API 管理状态和副作用。

资源与参考


模块三:Nuxt.js 基础

学习目标

  • 理解 Nuxt.js 的页面和路由机制
  • 掌握静态页面生成(SSG)与服务端渲染(SSR)的使用
  • 学会创建和使用 API 路由

详细内容

3.1 页面与路由
  • 文件系统路由机制

    • 基于文件结构的路由配置,页面组件的创建与组织。
    • 案例:创建多个页面(首页、关于页、联系页)并理解其路由路径。
    <!-- pages/index.vue -->
    <template>
      <div>
        <h1>Home Page</h1>
      </div>
    </template>
    
    <script>
    export default {
      name: 'HomePage',
    };
    </script>
    
    <!-- pages/about.vue -->
    <template>
      <div>
        <h1>About Page</h1>
      </div>
    </template>
    
    <script>
    export default {
      name: 'AboutPage',
    };
    </script>
    
    <!-- pages/contact.vue -->
    <template>
      <div>
        <h1>Contact Page</h1>
      </div>
    </template>
    
    <script>
    export default {
      name: 'ContactPage',
    };
    </script>
    
  • 动态路由与嵌套路由

    • 创建动态路由(如 _id.vue),嵌套路由的实现方法。
    • 实战:开发一个动态博客文章页面,路径如 /posts/_id.vue
    <!-- pages/posts/_id.vue -->
    <template>
      <div>
        <h1>Post ID: {{ id }}</h1>
      </div>
    </template>
    
    <script>
    export default {
      async asyncData({ params }) {
        return { id: params.id };
      },
    };
    </script>
    
    <!-- pages/posts/index.vue -->
    <template>
      <div>
        <h1>Posts</h1>
        <ul>
          <li v-for="post in posts" :key="post.id">
            <nuxt-link :to="`/posts/${post.id}`">{{ post.title }}</nuxt-link>
          </li>
        </ul>
      </div>
    </template>
    
    <script>
    export default {
      async asyncData() {
        const posts = [
          { id: 1, title: 'First Post' },
          { id: 2, title: 'Second Post' },
        ];
        return { posts };
      },
    };
    </script>
    
  • NuxtLink 组件的使用

    • 客户端导航与预加载,NuxtLink 组件的属性和最佳实践。
    • 案例:在多个页面之间实现无刷新导航,提升用户体验。
    <!-- components/NavBar.vue -->
    <template>
      <nav>
        <ul>
          <li>
            <NuxtLink to="/">Home</NuxtLink>
          </li>
          <li>
            <NuxtLink to="/about">About</NuxtLink>
          </li>
          <li>
            <NuxtLink to="/contact">Contact</NuxtLink>
          </li>
          <li>
            <NuxtLink to="/posts">Posts</NuxtLink>
          </li>
        </ul>
      </nav>
    </template>
    
    <style scoped>
    nav ul {
      list-style: none;
      display: flex;
      gap: 1rem;
      padding: 1rem;
      background-color: #f8f9fa;
    }
    
    nav a {
      text-decoration: none;
      color: #333;
    }
    
    nav a:hover {
      color: #007bff;
    }
    </style>
    
    <!-- layouts/default.vue -->
    <template>
      <div>
        <NavBar />
        <nuxt />
      </div>
    </template>
    
    <script>
    import NavBar from '~/components/NavBar.vue';
    
    export default {
      components: {
        NavBar,
      },
    };
    </script>
    
3.2 静态页面生成(SSG)与服务端渲染(SSR)
  • asyncData 和 fetch 方法

    • 静态生成与服务端渲染的数据获取方法,使用 asyncData 获取静态数据,使用 fetch 实现实时数据渲染。
    • 实战:为博客应用生成静态文章页面,提升加载速度。
    <!-- pages/posts/_id.vue -->
    <template>
      <div>
        <h1>{{ post.title }}</h1>
        <p>{{ post.content }}</p>
      </div>
    </template>
    
    <script>
    export default {
      async asyncData({ params }) {
        const post = {
          id: params.id,
          title: `Post ${params.id}`,
          content: 'This is the post content.',
        };
        return { post };
      },
    };
    </script>
    
  • fetch 方法

    • 服务端渲染的概念与应用场景,使用 fetch 实现实时数据渲染。
    • 案例:开发一个实时天气信息页面,展示服务端渲染的优势。
    <!-- pages/weather.vue -->
    <template>
      <div>
        <h1>Weather in {{ weather.location.name }}</h1>
        <p>Temperature: {{ weather.current.temp_c }}°C</p>
        <p>Condition: {{ weather.current.condition.text }}</p>
      </div>
    </template>
    
    <script>
    export default {
      async fetch() {
        const res = await fetch('https://api.weatherapi.com/v1/current.json?key=YOUR_API_KEY&q=London');
        const data = await res.json();
        this.weather = data;
      },
      data() {
        return {
          weather: {},
        };
      },
    };
    </script>
    
3.3 API 路由
  • 创建 API 接口

    • server/api 目录下创建 API 路由,处理不同的 HTTP 请求方法(GET、POST 等)。
    • 实战:开发一个简单的留言板 API,实现留言的获取和提交。
    // server/api/messages.js
    let messages = [];
    
    export default function handler(req, res) {
      if (req.method === 'GET') {
        res.status(200).json(messages);
      } else if (req.method === 'POST') {
        const { name, message } = req.body;
        messages.push({ name, message, id: messages.length + 1 });
        res.status(201).json({ message: 'Message added successfully' });
      } else {
        res.setHeader('Allow', ['GET', 'POST']);
        res.status(405).end(`Method ${req.method} Not Allowed`);
      }
    }
    
    <!-- components/MessageBoard.vue -->
    <template>
      <div>
        <h2>Message Board</h2>
        <form @submit.prevent="handleSubmit">
          <input name="name" v-model="form.name" placeholder="Your Name" required />
          <textarea name="message" v-model="form.message" placeholder="Your Message" required></textarea>
          <button type="submit">Send</button>
        </form>
        <ul>
          <li v-for="msg in messages" :key="msg.id">
            <strong>{{ msg.name }}:</strong> {{ msg.message }}
          </li>
        </ul>
      </div>
    </template>
    
    <script>
    export default {
      data() {
        return {
          messages: [],
          form: { name: '', message: '' },
        };
      },
      async mounted() {
        await this.fetchMessages();
      },
      methods: {
        async fetchMessages() {
          const res = await fetch('/api/messages');
          const data = await res.json();
          this.messages = data;
        },
        async handleSubmit() {
          await fetch('/api/messages', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(this.form),
          });
          this.form = { name: '', message: '' };
          await this.fetchMessages();
        },
      },
    };
    </script>
    
    <style scoped>
    form {
      display: flex;
      flex-direction: column;
      max-width: 400px;
    }
    
    input, textarea {
      margin-bottom: 1rem;
      padding: 0.5rem;
      border: 1px solid #ccc;
      border-radius: 3px;
    }
    
    button {
      padding: 0.5rem;
      background-color: #007bff;
      color: white;
      border: none;
      border-radius: 3px;
      cursor: pointer;
    }
    
    button:hover {
      background-color: #0056b3;
    }
    
    ul {
      list-style: none;
      padding: 0;
    }
    
    li {
      padding: 0.5rem;
      border-bottom: 1px solid #ccc;
    }
    </style>
    

    注意:以上 API 路由示例中的数据存储在内存中,适用于教学和测试目的。在实际应用中,应使用数据库或其他持久化存储方式。

学习活动

  • 视频讲解:详细讲解页面与路由机制,演示静态与服务端渲染的实现。
  • 动手练习:创建多个页面,设置动态路由并使用 NuxtLink 组件导航。
  • 练习项目:构建一个简单的博客,使用 SSG 生成文章页面,使用 API 路由处理评论提交。

资源与参考


模块四:样式与布局

学习目标

  • 掌握在 Nuxt.js 中应用 CSS 的多种方法
  • 学会使用 CSS Modules 实现模块化样式
  • 理解并应用 Scoped CSS 进行内联样式
  • 了解并集成流行的 CSS 框架

详细内容

4.1 CSS 基础
  • 全局样式与组件级样式

    • assets/css 中应用全局样式,在组件中应用局部样式。
    • 案例:为整个应用设置统一的字体和颜色主题。
    /* assets/css/global.css */
    body {
      font-family: Arial, sans-serif;
      background-color: #f0f0f0;
      margin: 0;
      padding: 0;
    }
    
    nav ul {
      list-style: none;
      display: flex;
      gap: 1rem;
    }
    
    nav a {
      text-decoration: none;
      color: #333;
    }
    
    nav a:hover {
      color: #0070f3;
    }
    
    // nuxt.config.js
    export default {
      css: ['~/assets/css/global.css'],
      // 其他配置...
    };
    
    <!-- components/NavBar.vue -->
    <template>
      <nav>
        <ul>
          <li>
            <NuxtLink to="/">Home</NuxtLink>
          </li>
          <li>
            <NuxtLink to="/about">About</NuxtLink>
          </li>
          <li>
            <NuxtLink to="/contact">Contact</NuxtLink>
          </li>
          <li>
            <NuxtLink to="/posts">Posts</NuxtLink>
          </li>
        </ul>
      </nav>
    </template>
    
    <style scoped>
    nav ul {
      list-style: none;
      display: flex;
      gap: 1rem;
      padding: 1rem;
      background-color: #f8f9fa;
    }
    
    nav a {
      text-decoration: none;
      color: #333;
    }
    
    nav a:hover {
      color: #0070f3;
    }
    </style>
    
  • CSS 文件的导入与使用

    • 使用 @import 导入 CSS 文件,优化 CSS 加载顺序。
    • 实战:为不同组件应用不同的 CSS 文件,确保样式不冲突。
    /* assets/css/Button.css */
    .button {
      padding: 0.5rem 1rem;
      background-color: #0070f3;
      color: white;
      border: none;
      border-radius: 5px;
      cursor: pointer;
    }
    
    .button:hover {
      background-color: #005bb5;
    }
    
    <!-- components/Button.vue -->
    <template>
      <button class="button" @click="handleClick">{{ label }}</button>
    </template>
    
    <script>
    export default {
      name: 'Button',
      props: {
        label: {
          type: String,
          required: true,
        },
      },
      methods: {
        handleClick() {
          this.$emit('click');
        },
      },
    };
    </script>
    
    <style src="~/assets/css/Button.css" scoped></style>
    
4.2 CSS Modules
  • 使用 CSS Modules 实现模块化样式

    • CSS Modules 的概念与优势,创建和使用 CSS Module 文件(如 Component.module.css)。
    • 案例:为按钮组件创建独立的 CSS Module,实现样式隔离。
    /* components/Button.module.css */
    .button {
      padding: 0.5rem 1rem;
      background-color: #28a745;
      color: white;
      border: none;
      border-radius: 5px;
      cursor: pointer;
    }
    
    .button:hover {
      background-color: #218838;
    }
    
    <!-- components/Button.vue -->
    <template>
      <button :class="$style.button" @click="handleClick">{{ label }}</button>
    </template>
    
    <script>
    export default {
      name: 'Button',
      props: {
        label: {
          type: String,
          required: true,
        },
      },
      methods: {
        handleClick() {
          this.$emit('click');
        },
      },
    };
    </script>
    
    <style module>
    /* 可以在这里添加局部样式 */
    </style>
    
  • 类名命名与样式应用

    • 避免样式冲突的方法,动态应用类名。
    • 实战:使用动态类名实现主题切换功能。
    /* components/Card.module.css */
    .card {
      padding: 1rem;
      border: 1px solid #ccc;
      border-radius: 5px;
      background-color: white;
    }
    
    .dark {
      background-color: #333;
      color: white;
      border-color: #555;
    }
    
    <!-- components/Card.vue -->
    <template>
      <div :class="[$style.card, isDark ? $style.dark : '']">
        <slot></slot>
      </div>
    </template>
    
    <script>
    export default {
      name: 'Card',
      props: {
        isDark: {
          type: Boolean,
          default: false,
        },
      },
    };
    </script>
    
    <style module>
    /* 可以在这里添加局部样式 */
    </style>
    
    <!-- pages/index.vue -->
    <template>
      <div>
        <button @click="toggleTheme">Toggle Theme</button>
        <Card :isDark="isDark">
          <h2>Card Title</h2>
          <p>This is a card component.</p>
        </Card>
      </div>
    </template>
    
    <script>
    import Card from '~/components/Card.vue';
    import { ref } from '@vue/composition-api';
    
    export default {
      components: {
        Card,
      },
      setup() {
        const isDark = ref(false);
        const toggleTheme = () => {
          isDark.value = !isDark.value;
        };
        return { isDark, toggleTheme };
      },
    };
    </script>
    
    <style scoped>
    button {
      padding: 0.5rem 1rem;
      margin-bottom: 1rem;
      background-color: #007bff;
      color: white;
      border: none;
      border-radius: 3px;
      cursor: pointer;
    }
    
    button:hover {
      background-color: #0056b3;
    }
    </style>
    
4.3 Scoped CSS
  • 内联样式与动态样式

    • 使用 Scoped CSS 在组件中编写内联样式,动态样式的实现方法。
    • 案例:为动态颜色的卡片组件添加内联样式。
    <!-- components/DynamicCard.vue -->
    <template>
      <div class="card">
        <h2>{{ title }}</h2>
        <p>{{ content }}</p>
        <style scoped>
          .card {
            padding: 1rem;
            border: 1px solid #ccc;
            border-radius: 5px;
            background-color: {{ bgColor }};
            color: {{ textColor }};
          }
        </style>
      </div>
    </template>
    
    <script>
    export default {
      name: 'DynamicCard',
      props: {
        title: {
          type: String,
          required: true,
        },
        content: {
          type: String,
          required: true,
        },
        bgColor: {
          type: String,
          required: true,
        },
      },
      computed: {
        textColor() {
          return this.bgColor === '#333' ? 'white' : 'black';
        },
      },
    };
    </script>
    
    <!-- pages/index.vue -->
    <template>
      <div>
        <DynamicCard title="Dynamic Card" content="This card has dynamic styles." bgColor="#f9c74f" />
        <DynamicCard title="Dark Card" content="This card has dark background." bgColor="#333" />
      </div>
    </template>
    
    <script>
    import DynamicCard from '~/components/DynamicCard.vue';
    
    export default {
      components: {
        DynamicCard,
      },
    };
    </script>
    
  • 样式作用域与优先级

    • 确保样式仅应用于特定组件,管理样式优先级的问题。
    • 实战:解决样式冲突,确保组件样式独立性。
    <!-- components/StyledButton.vue -->
    <template>
      <button class="btn">
        {{ label }}
        <style scoped>
          .btn {
            padding: 0.5rem 1rem;
            background-color: #0070f3;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
          }
    
          .btn:hover {
            background-color: #005bb5;
          }
        </style>
      </button>
    </template>
    
    <script>
    export default {
      name: 'StyledButton',
      props: {
        label: {
          type: String,
          required: true,
        },
      },
    };
    </script>
    
    <!-- pages/index.vue -->
    <template>
      <div>
        <StyledButton label="Styled Button" />
        <button class="btn">Regular Button</button>
        <style scoped>
          .btn {
            padding: 0.5rem 1rem;
            background-color: #28a745;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
          }
    
          .btn:hover {
            background-color: #218838;
          }
        </style>
      </div>
    </template>
    
    <script>
    import StyledButton from '~/components/StyledButton.vue';
    
    export default {
      components: {
        StyledButton,
      },
    };
    </script>
    
4.4 集成 CSS 框架
  • 使用 Tailwind CSS

    • Tailwind CSS 的安装与配置,使用 Tailwind 工具类进行快速样式开发。
    • 案例:使用 Tailwind CSS 构建一个响应式导航栏。
    # 安装 Tailwind CSS
    npm install -D tailwindcss postcss autoprefixer
    npx tailwindcss init -p
    
    // tailwind.config.js
    module.exports = {
      content: ['./pages/**/*.{vue,js}', './components/**/*.{vue,js}'],
      theme: {
        extend: {},
      },
      plugins: [],
    };
    
    /* assets/css/tailwind.css */
    @tailwind base;
    @tailwind components;
    @tailwind utilities;
    
    // nuxt.config.js
    export default {
      css: ['~/assets/css/tailwind.css'],
      build: {
        postcss: {
          plugins: {
            tailwindcss: {},
            autoprefixer: {},
          },
        },
      },
      // 其他配置...
    };
    
    <!-- components/NavBarTailwind.vue -->
    <template>
      <nav class="bg-blue-500 p-4">
        <ul class="flex space-x-4">
          <li>
            <NuxtLink to="/" class="text-white hover:bg-blue-700 px-3 py-2 rounded">Home</NuxtLink>
          </li>
          <li>
            <NuxtLink to="/about" class="text-white hover:bg-blue-700 px-3 py-2 rounded">About</NuxtLink>
          </li>
          <li>
            <NuxtLink to="/contact" class="text-white hover:bg-blue-700 px-3 py-2 rounded">Contact</NuxtLink>
          </li>
        </ul>
      </nav>
    </template>
    
    <style scoped>
    /* 可以添加额外的样式 */
    </style>
    
    <!-- layouts/default.vue -->
    <template>
      <div>
        <NavBarTailwind />
        <Nuxt />
      </div>
    </template>
    
    <script>
    import NavBarTailwind from '~/components/NavBarTailwind.vue';
    
    export default {
      components: {
        NavBarTailwind,
      },
    };
    </script>
    
  • 使用 Bootstrap 或其他流行框架

    • Bootstrap 的集成方法,其他框架的选择与使用(如 Vuetify)。
    • 实战:在项目中集成 Vuetify,实现现代化界面设计。
    # 安装 Vuetify
    npm install vuetify
    
    // plugins/vuetify.js
    import Vue from 'vue';
    import Vuetify from 'vuetify';
    import 'vuetify/dist/vuetify.min.css';
    
    Vue.use(Vuetify);
    
    export default new Vuetify({});
    
    // nuxt.config.js
    export default {
      plugins: ['~/plugins/vuetify.js'],
      css: ['vuetify/dist/vuetify.min.css'],
      build: {
        transpile: ['vuetify/lib'],
      },
      // 其他配置...
    };
    
    <!-- components/VuetifyButton.vue -->
    <template>
      <v-btn color="primary">{{ label }}</v-btn>
    </template>
    
    <script>
    export default {
      name: 'VuetifyButton',
      props: {
        label: {
          type: String,
          required: true,
        },
      },
    };
    </script>
    
    <!-- pages/index.vue -->
    <template>
      <div>
        <h1>Vuetify Button</h1>
        <VuetifyButton label="Click Me" />
      </div>
    </template>
    
    <script>
    import VuetifyButton from '~/components/VuetifyButton.vue';
    
    export default {
      components: {
        VuetifyButton,
      },
    };
    </script>
    

学习活动

  • 视频讲解:演示不同的样式应用方法,展示 CSS Modules 和 Scoped CSS 的使用。
  • 动手练习:为现有项目添加全局样式和组件级样式,使用 CSS Modules 重构样式。
  • 练习项目:设计一个响应式布局,集成 Tailwind CSS 并应用于项目中。

资源与参考


模块五:数据获取与管理

学习目标

  • 掌握在 Nuxt.js 中进行数据获取的方法
  • 理解数据预取与缓存策略
  • 学会管理应用状态,使用 Vuex

详细内容

5.1 数据获取
  • Fetch API 与 Axios

    • 使用 Fetch API 进行数据请求,使用 Axios 的优势与基本用法。
    • 实战:在项目中集成 Axios,替代 Fetch API 进行数据请求。
    # 安装 Axios
    npm install axios
    
    // plugins/axios.js
    import axios from 'axios';
    
    export default ({ app }, inject) => {
      const instance = axios.create({
        baseURL: 'http://localhost:3000/api',
      });
    
      inject('axios', instance);
    };
    
    // nuxt.config.js
    export default {
      plugins: ['~/plugins/axios.js'],
      // 其他配置...
    };
    
    <!-- components/AxiosExample.vue -->
    <template>
      <div>
        <h2>Messages (Fetched with Axios)</h2>
        <ul>
          <li v-for="msg in messages" :key="msg.id">
            <strong>{{ msg.name }}:</strong> {{ msg.message }}
          </li>
        </ul>
      </div>
    </template>
    
    <script>
    export default {
      data() {
        return {
          messages: [],
        };
      },
      async mounted() {
        try {
          const res = await this.$axios.get('/messages');
          this.messages = res.data;
        } catch (error) {
          console.error(error);
        }
      },
    };
    </script>
    
    <!-- pages/index.vue -->
    <template>
      <div>
        <h1>Welcome to Nuxt.js!</h1>
        <AxiosExample />
      </div>
    </template>
    
    <script>
    import AxiosExample from '~/components/AxiosExample.vue';
    
    export default {
      components: {
        AxiosExample,
      },
    };
    </script>
    
  • 数据预取与缓存策略

    • 数据预取的概念与实现,缓存策略的选择与应用(如 SWR、vue-query)。
    • 案例:使用 SWR 实现数据预取和缓存,提高应用性能。
    # 安装 SWR
    npm install swr
    
    <!-- components/SWRExample.vue -->
    <template>
      <div>
        <h2>Messages (Fetched with SWR)</h2>
        <ul>
          <li v-for="msg in data" :key="msg.id">
            <strong>{{ msg.name }}:</strong> {{ msg.message }}
          </li>
        </ul>
      </div>
    </template>
    
    <script>
    import useSWR from 'swr';
    import axios from 'axios';
    
    export default {
      setup() {
        const fetcher = (url) => axios.get(url).then(res => res.data);
        const { data, error } = useSWR('/messages', fetcher);
    
        return { data, error };
      },
    };
    </script>
    
    <!-- pages/index.vue -->
    <template>
      <div>
        <h1>Welcome to Nuxt.js!</h1>
        <SWRExample />
      </div>
    </template>
    
    <script>
    import SWRExample from '~/components/SWRExample.vue';
    
    export default {
      components: {
        SWRExample,
      },
    };
    </script>
    
5.2 状态管理
  • 使用 Vuex

    • Vuex 的定义与使用场景,创建和使用 Vuex 仓库的基础方法。
    • 实战:开发一个全局主题切换功能,使用 Vuex 管理主题状态。
    // store/index.js
    export const state = () => ({
      theme: 'light',
    });
    
    export const mutations = {
      toggleTheme(state) {
        state.theme = state.theme === 'light' ? 'dark' : 'light';
      },
    };
    
    <!-- components/NavBar.vue -->
    <template>
      <nav :class="themeClass">
        <ul>
          <li>
            <NuxtLink to="/">Home</NuxtLink>
          </li>
          <li>
            <NuxtLink to="/about">About</NuxtLink>
          </li>
          <li>
            <NuxtLink to="/contact">Contact</NuxtLink>
          </li>
          <li>
            <button @click="toggleTheme">
              Switch to {{ theme === 'light' ? 'Dark' : 'Light' }} Theme
            </button>
          </li>
        </ul>
      </nav>
    </template>
    
    <script>
    import { mapState, mapMutations } from 'vuex';
    
    export default {
      computed: {
        ...mapState(['theme']),
        themeClass() {
          return this.theme;
        },
      },
      methods: {
        ...mapMutations(['toggleTheme']),
      },
    };
    </script>
    
    <style scoped>
    nav.light {
      background-color: #ffffff;
      color: #000000;
    }
    
    nav.dark {
      background-color: #333333;
      color: #ffffff;
    }
    
    nav ul {
      list-style: none;
      display: flex;
      gap: 1rem;
      padding: 1rem;
    }
    
    nav button {
      padding: 0.5rem 1rem;
      cursor: pointer;
    }
    </style>
    
  • 集成 Vuex

    • Vuex 的基本概念与配置,使用 Vuex 模块化管理复杂状态。
    • 案例:在项目中集成 Vuex,管理全局消息状态。
    // store/messages.js
    export const state = () => ({
      messages: [],
    });
    
    export const mutations = {
      setMessages(state, messages) {
        state.messages = messages;
      },
      addMessage(state, message) {
        state.messages.push(message);
      },
    };
    
    export const actions = {
      async fetchMessages({ commit }) {
        try {
          const res = await this.$axios.get('/messages');
          commit('setMessages', res.data);
        } catch (error) {
          console.error(error);
        }
      },
      async submitMessage({ commit }, form) {
        try {
          await this.$axios.post('/messages', form);
          commit('addMessage', { ...form, id: this.state.messages.length + 1 });
        } catch (error) {
          console.error(error);
        }
      },
    };
    
    <!-- components/VuexMessageBoard.vue -->
    <template>
      <div>
        <h2>Vuex Message Board</h2>
        <form @submit.prevent="handleSubmit">
          <input name="name" v-model="form.name" placeholder="Your Name" required />
          <textarea name="message" v-model="form.message" placeholder="Your Message" required></textarea>
          <button type="submit">Send</button>
        </form>
        <ul>
          <li v-for="msg in messages" :key="msg.id">
            <strong>{{ msg.name }}:</strong> {{ msg.message }}
          </li>
        </ul>
      </div>
    </template>
    
    <script>
    import { mapState, mapActions } from 'vuex';
    
    export default {
      data() {
        return {
          form: { name: '', message: '' },
        };
      },
      computed: {
        ...mapState(['messages']),
      },
      methods: {
        ...mapActions(['submitMessage', 'fetchMessages']),
        async handleSubmit() {
          await this.submitMessage(this.form);
          this.form = { name: '', message: '' };
        },
      },
      async mounted() {
        await this.fetchMessages();
      },
    };
    </script>
    
    <style scoped>
    form {
      display: flex;
      flex-direction: column;
      max-width: 400px;
    }
    
    input, textarea {
      margin-bottom: 1rem;
      padding: 0.5rem;
      border: 1px solid #ccc;
      border-radius: 3px;
    }
    
    button {
      padding: 0.5rem;
      background-color: #28a745;
      color: white;
      border: none;
      border-radius: 3px;
      cursor: pointer;
    }
    
    button:hover {
      background-color: #218838;
    }
    
    ul {
      list-style: none;
      padding: 0;
    }
    
    li {
      padding: 0.5rem;
      border-bottom: 1px solid #ccc;
    }
    </style>
    
    <!-- pages/index.vue -->
    <template>
      <div>
        <h1>Welcome to Nuxt.js!</h1>
        <VuexMessageBoard />
      </div>
    </template>
    
    <script>
    import VuexMessageBoard from '~/components/VuexMessageBoard.vue';
    
    export default {
      components: {
        VuexMessageBoard,
      },
    };
    </script>
    
5.3 数据更新与同步
  • 实时数据更新

    • 使用 WebSockets 或其他实时技术,实现数据的实时更新。
    • 实战:集成 Socket.io,实现实时消息更新。
    # 安装 Socket.io
    npm install socket.io socket.io-client
    
    // server/socket.js
    const { Server } = require('socket.io');
    
    let io;
    
    module.exports = function (server) {
      if (!io) {
        io = new Server(server, {
          cors: {
            origin: '*',
            methods: ['GET', 'POST'],
          },
        });
    
        io.on('connection', (socket) => {
          console.log('New client connected');
    
          socket.on('sendMessage', (msg) => {
            io.emit('newMessage', msg);
          });
    
          socket.on('disconnect', () => {
            console.log('Client disconnected');
          });
        });
      }
      return io;
    };
    
    // server/index.js
    const express = require('express');
    const http = require('http');
    const socketHandler = require('./socket');
    
    const app = express();
    const server = http.createServer(app);
    const io = socketHandler(server);
    
    app.use(express.json());
    
    app.get('/api/messages', (req, res) => {
      res.json([]);
    });
    
    app.post('/api/messages', (req, res) => {
      const { name, message } = req.body;
      io.emit('newMessage', { name, message, id: Date.now() });
      res.status(201).json({ message: 'Message added successfully' });
    });
    
    server.listen(3000, () => {
      console.log('Server is running on port 3000');
    });
    
    <!-- components/RealTimeMessageBoard.vue -->
    <template>
      <div>
        <h2>Real-Time Message Board</h2>
        <form @submit.prevent="handleSubmit">
          <input name="name" v-model="form.name" placeholder="Your Name" required />
          <textarea name="message" v-model="form.message" placeholder="Your Message" required></textarea>
          <button type="submit">Send</button>
        </form>
        <ul>
          <li v-for="msg in messages" :key="msg.id">
            <strong>{{ msg.name }}:</strong> {{ msg.message }}
          </li>
        </ul>
      </div>
    </template>
    
    <script>
    import io from 'socket.io-client';
    import { ref, onMounted, onUnmounted } from '@vue/composition-api';
    
    export default {
      setup() {
        const socket = io();
        const messages = ref([]);
        const form = ref({ name: '', message: '' });
    
        const handleSubmit = () => {
          socket.emit('sendMessage', form.value);
          form.value = { name: '', message: '' };
        };
    
        const addMessage = (msg) => {
          messages.value.push(msg);
        };
    
        onMounted(() => {
          socket.on('newMessage', addMessage);
        });
    
        onUnmounted(() => {
          socket.off('newMessage', addMessage);
          socket.disconnect();
        });
    
        return { messages, form, handleSubmit };
      },
    };
    </script>
    
    <style scoped>
    form {
      display: flex;
      flex-direction: column;
      max-width: 400px;
    }
    
    input, textarea {
      margin-bottom: 1rem;
      padding: 0.5rem;
      border: 1px solid #ccc;
      border-radius: 3px;
    }
    
    button {
      padding: 0.5rem;
      background-color: #007bff;
      color: white;
      border: none;
      border-radius: 3px;
      cursor: pointer;
    }
    
    button:hover {
      background-color: #0056b3;
    }
    
    ul {
      list-style: none;
      padding: 0;
    }
    
    li {
      padding: 0.5rem;
      border-bottom: 1px solid #ccc;
    }
    </style>
    
    <!-- pages/index.vue -->
    <template>
      <div>
        <h1>Welcome to Nuxt.js!</h1>
        <RealTimeMessageBoard />
      </div>
    </template>
    
    <script>
    import RealTimeMessageBoard from '~/components/RealTimeMessageBoard.vue';
    
    export default {
      components: {
        RealTimeMessageBoard,
      },
    };
    </script>
    

学习活动

  • 视频讲解:详细介绍数据获取方法和状态管理工具的使用。
  • 动手练习:在项目中集成 Axios 进行数据请求,使用 Vuex 管理简单状态。
  • 练习项目:构建一个用户信息展示页面,使用 Vuex 管理全局状态。

资源与参考


模块六:高级功能与优化

学习目标

  • 学会使用 Nuxt.js 的图像优化功能
  • 实现多语言支持(国际化)
  • 掌握 SEO 优化的方法
  • 进行性能优化,提升应用效率

详细内容

6.1 图像优化
  • Nuxt Image 组件

    • Image 组件的使用方法和优势,配置外部图片源。
    • 实战:在项目中使用 Image 组件替代传统的 <img> 标签,提升图片加载性能。
    # 安装 @nuxt/image
    npm install @nuxt/image
    
    // nuxt.config.js
    export default {
      modules: ['@nuxt/image'],
      image: {
        // 选项
      },
      // 其他配置...
    };
    
    <!-- components/OptimizedImage.vue -->
    <template>
      <div>
        <h2>Optimized Image</h2>
        <NuxtImage src="/images/sample.jpg" alt="Sample" width="500" height="300" />
      </div>
    </template>
    
    <script>
    export default {
      name: 'OptimizedImage',
    };
    </script>
    
    <!-- pages/index.vue -->
    <template>
      <div>
        <h1>Welcome to Nuxt.js!</h1>
        <OptimizedImage />
      </div>
    </template>
    
    <script>
    import OptimizedImage from '~/components/OptimizedImage.vue';
    
    export default {
      components: {
        OptimizedImage,
      },
    };
    </script>
    

    注意:确保在 nuxt.config.js 中配置了外部图片源(如果使用外部 URL)。

    // nuxt.config.js
    export default {
      modules: ['@nuxt/image'],
      image: {
        domains: ['example.com'],
      },
      // 其他配置...
    };
    
  • 图片懒加载与响应式图片

    • 实现图片懒加载提升页面性能,使用不同分辨率的图片实现响应式设计。
    • 案例:为博客文章添加响应式图片,优化加载时间和用户体验。
    <!-- components/ResponsiveImage.vue -->
    <template>
      <div>
        <h2>Responsive Image</h2>
        <NuxtImage
          src="/images/responsive.jpg"
          alt="Responsive Example"
          layout="responsive"
          sizes="(max-width: 600px) 480px,
                 (max-width: 1200px) 800px,
                 1200px"
        />
      </div>
    </template>
    
    <script>
    export default {
      name: 'ResponsiveImage',
    };
    </script>
    
    <!-- pages/index.vue -->
    <template>
      <div>
        <h1>Welcome to Nuxt.js!</h1>
        <ResponsiveImage />
      </div>
    </template>
    
    <script>
    import ResponsiveImage from '~/components/ResponsiveImage.vue';
    
    export default {
      components: {
        ResponsiveImage,
      },
    };
    </script>
    
6.2 国际化(i18n)
  • 多语言支持

    • Nuxt.js 内置国际化支持的配置,管理多语言资源文件。
    • 实战:配置 Nuxt.js 应用支持中文和英文两种语言。
    # 安装 @nuxtjs/i18n 模块
    npm install @nuxtjs/i18n
    
    // nuxt.config.js
    export default {
      modules: ['@nuxtjs/i18n'],
      i18n: {
        locales: [
          { code: 'en', name: 'English' },
          { code: 'zh', name: '中文' },
        ],
        defaultLocale: 'en',
        vueI18n: {
          fallbackLocale: 'en',
          messages: {
            en: {
              welcome: 'Welcome to Nuxt.js!',
              description: 'This is an internationalized application.',
            },
            zh: {
              welcome: '欢迎来到 Nuxt.js!',
              description: '这是一个国际化的应用程序。',
            },
          },
        },
      },
      // 其他配置...
    };
    
    <!-- pages/index.vue -->
    <template>
      <div>
        <LanguageSwitcher />
        <h1>{{ $t('welcome') }}</h1>
        <p>{{ $t('description') }}</p>
      </div>
    </template>
    
    <script>
    import LanguageSwitcher from '~/components/LanguageSwitcher.vue';
    
    export default {
      components: {
        LanguageSwitcher,
      },
    };
    </script>
    
  • 本地化配置

    • URL 路由中的语言标识,动态内容的本地化处理。
    • 案例:开发一个多语言切换功能,动态展示不同语言内容。
    <!-- components/LanguageSwitcher.vue -->
    <template>
      <div>
        <span v-for="locale in locales" :key="locale.code" class="mr-2">
          <NuxtLink :to="switchLocalePath(locale.code)" :class="{ active: locale.code === currentLocale }">
            {{ locale.name }}
          </NuxtLink>
        </span>
      </div>
    </template>
    
    <script>
    export default {
      computed: {
        locales() {
          return this.$i18n.locales;
        },
        currentLocale() {
          return this.$i18n.locale;
        },
      },
    };
    </script>
    
    <style scoped>
    .active {
      font-weight: bold;
      text-decoration: underline;
    }
    </style>
    
    <!-- pages/index.vue -->
    <template>
      <div>
        <LanguageSwitcher />
        <h1>{{ $t('welcome') }}</h1>
        <p>{{ $t('description') }}</p>
      </div>
    </template>
    
    <script>
    import LanguageSwitcher from '~/components/LanguageSwitcher.vue';
    
    export default {
      components: {
        LanguageSwitcher,
      },
    };
    </script>
    
6.3 SEO 优化
  • 使用 Head 组件

    • 配置页面的标题、描述和关键字,动态设置元标签。
    • 实战:为每个页面设置独特的 meta 标签,提升搜索引擎排名。
    <!-- components/SEO.vue -->
    <template>
      <head>
        <title>{{ title }}</title>
        <meta name="description" :content="description" />
      </head>
    </template>
    
    <script>
    export default {
      props: {
        title: {
          type: String,
          required: true,
        },
        description: {
          type: String,
          required: true,
        },
      },
    };
    </script>
    
    <!-- pages/about.vue -->
    <template>
      <div>
        <SEO title="About Us" description="Learn more about our company." />
        <h1>About Us</h1>
        <p>This is the about page.</p>
      </div>
    </template>
    
    <script>
    import SEO from '~/components/SEO.vue';
    
    export default {
      components: {
        SEO,
      },
    };
    </script>
    
  • 元数据管理与结构化数据

    • 使用 JSON-LD 添加结构化数据,提升搜索引擎可见性的方法。
    • 案例:为博客文章添加结构化数据,增强搜索引擎理解。
    <!-- components/BlogSEO.vue -->
    <template>
      <head>
        <title>{{ title }}</title>
        <meta name="description" :content="description" />
        <script type="application/ld+json" :innerHTML="jsonLd"></script>
      </head>
    </template>
    
    <script>
    export default {
      props: {
        title: {
          type: String,
          required: true,
        },
        description: {
          type: String,
          required: true,
        },
        author: {
          type: String,
          required: true,
        },
        date: {
          type: String,
          required: true,
        },
      },
      computed: {
        jsonLd() {
          return JSON.stringify({
            "@context": "https://schema.org",
            "@type": "BlogPosting",
            "headline": this.title,
            "description": this.description,
            "author": {
              "@type": "Person",
              "name": this.author,
            },
            "datePublished": this.date,
          });
        },
      },
    };
    </script>
    
    <!-- pages/posts/_id.vue -->
    <template>
      <div>
        <BlogSEO :title="post.title" :description="post.content" :author="post.author" :date="post.date" />
        <h1>{{ post.title }}</h1>
        <p>{{ post.content }}</p>
      </div>
    </template>
    
    <script>
    import BlogSEO from '~/components/BlogSEO.vue';
    
    export default {
      components: {
        BlogSEO,
      },
      async asyncData({ params }) {
        const post = {
          id: params.id,
          title: `Post ${params.id}`,
          content: 'This is the post content.',
          author: 'John Doe',
          date: '2024-01-01',
        };
        return { post };
      },
    };
    </script>
    
6.4 性能优化
  • 代码分割与懒加载

    • 动态导入实现代码分割,按需加载组件提升性能。
    • 实战:优化大型组件的加载方式,减少初始加载时间。
    <!-- pages/index.vue -->
    <template>
      <div>
        <h1>Welcome to Nuxt.js!</h1>
        <LazyComponent />
      </div>
    </template>
    
    <script>
    export default {
      components: {
        LazyComponent: () => import('~/components/LazyComponent.vue'),
      },
    };
    </script>
    
    <!-- components/LazyComponent.vue -->
    <template>
      <div>This is a lazily loaded component.</div>
    </template>
    
    <script>
    export default {
      name: 'LazyComponent',
    };
    </script>
    
  • Bundle 分析与优化

    • 使用 Nuxt.js 内置的 bundle 分析工具,优化依赖包和减少体积的方法。
    • 案例:通过分析工具识别和移除冗余依赖,提升应用性能。
    # 安装 @nuxtjs/webpack-bundle-analyzer
    npm install --save-dev @nuxtjs/webpack-bundle-analyzer
    
    // nuxt.config.js
    export default {
      buildModules: ['@nuxtjs/webpack-bundle-analyzer'],
      webpackBundleAnalyzer: {
        analyzerMode: 'static',
        openAnalyzer: false,
      },
      // 其他配置...
    };
    
    # 运行分析
    npm run build
    

    分析结果:查看生成的分析报告,识别大尺寸的依赖包,考虑替换或优化。

学习活动

  • 视频讲解:演示图像优化、国际化和 SEO 优化的实现过程。
  • 动手练习:在项目中集成 Image 组件,配置多语言支持。
  • 练习项目:优化现有项目的图像加载,添加多语言支持,并进行基本的 SEO 设置。

资源与参考


模块七:认证与安全

学习目标

  • 实现用户认证功能
  • 掌握常见的 Web 安全最佳实践
  • 确保应用的数据安全与防护

详细内容

7.1 用户认证
  • 基于 JWT 的认证

    • JWT 的基本概念与工作原理,在 Nuxt.js 中实现 JWT 认证,存储与管理 JWT(如 HttpOnly Cookies)。
    • 实战:开发一个用户登录系统,使用 JWT 进行认证和授权。
    # 安装必要的依赖
    npm install jsonwebtoken bcryptjs
    
    // server/api/login.js
    const jwt = require('jsonwebtoken');
    const bcrypt = require('bcryptjs');
    
    const SECRET_KEY = 'your-secret-key';
    const users = [
      { id: 1, username: 'user1', password: '$2a$10$E9JbFjR7g6JQPOlxLHBM2u3u2fT6mVZ6r4vUQ8Bvc7oGB0uVQqC1e' }, // password: pass123
    ];
    
    export default function handler(req, res) {
      if (req.method === 'POST') {
        const { username, password } = req.body;
        const user = users.find((u) => u.username === username);
    
        if (user && bcrypt.compareSync(password, user.password)) {
          const token = jwt.sign({ id: user.id, username: user.username }, SECRET_KEY, { expiresIn: '1h' });
          res.setHeader('Set-Cookie', `token=${token}; HttpOnly; Path=/; Max-Age=3600`);
          res.status(200).json({ message: 'Login successful' });
        } else {
          res.status(401).json({ message: 'Invalid credentials' });
        }
      } else {
        res.setHeader('Allow', ['POST']);
        res.status(405).end(`Method ${req.method} Not Allowed`);
      }
    }
    
    <!-- components/LoginForm.vue -->
    <template>
      <div>
        <h2>Login</h2>
        <form @submit.prevent="handleSubmit">
          <input name="username" v-model="form.username" placeholder="Username" required />
          <input name="password" type="password" v-model="form.password" placeholder="Password" required />
          <button type="submit">Login</button>
        </form>
        <p v-if="message">{{ message }}</p>
      </div>
    </template>
    
    <script>
    export default {
      data() {
        return {
          form: { username: '', password: '' },
          message: '',
        };
      },
      methods: {
        async handleSubmit() {
          try {
            const res = await this.$axios.post('/login', this.form);
            this.message = res.data.message;
          } catch (error) {
            this.message = error.response.data.message;
          }
        },
      },
    };
    </script>
    
    <style scoped>
    form {
      display: flex;
      flex-direction: column;
      max-width: 300px;
    }
    
    input {
      margin-bottom: 1rem;
      padding: 0.5rem;
      border: 1px solid #ccc;
      border-radius: 3px;
    }
    
    button {
      padding: 0.5rem;
      background-color: #007bff;
      color: white;
      border: none;
      border-radius: 3px;
      cursor: pointer;
    }
    
    button:hover {
      background-color: #0056b3;
    }
    
    p {
      color: red;
    }
    </style>
    
    <!-- pages/index.vue -->
    <template>
      <div>
        <h1>Welcome to Nuxt.js!</h1>
        <LoginForm />
      </div>
    </template>
    
    <script>
    import LoginForm from '~/components/LoginForm.vue';
    
    export default {
      components: {
        LoginForm,
      },
    };
    </script>
    
  • 使用 Nuxt Auth 模块实现社交登录

    • Nuxt Auth 模块的安装与配置,集成常见的社交登录提供商(如 Google、GitHub),自定义认证流程与回调。
    • 案例:为应用添加 GitHub 登录功能,提升用户注册的便捷性。
    # 安装 @nuxtjs/auth-next 和 @nuxtjs/axios
    npm install @nuxtjs/auth-next @nuxtjs/axios
    
    // nuxt.config.js
    export default {
      modules: ['@nuxtjs/axios', '@nuxtjs/auth-next'],
      auth: {
        strategies: {
          github: {
            clientId: 'your-github-client-id',
            clientSecret: 'your-github-client-secret',
            codeChallengeMethod: '',
            responseType: 'code',
          },
        },
      },
      axios: {
        baseURL: 'http://localhost:3000/api',
      },
      // 其他配置...
    };
    
    <!-- components/GitHubLogin.vue -->
    <template>
      <div>
        <button @click="loginWithGitHub">Sign in with GitHub</button>
        <button v-if="isAuthenticated" @click="logout">Sign out</button>
      </div>
    </template>
    
    <script>
    export default {
      computed: {
        isAuthenticated() {
          return this.$auth.loggedIn;
        },
      },
      methods: {
        loginWithGitHub() {
          this.$auth.loginWith('github');
        },
        logout() {
          this.$auth.logout();
        },
      },
    };
    </script>
    
    <style scoped>
    button {
      padding: 0.5rem 1rem;
      margin-right: 0.5rem;
      background-color: #24292e;
      color: white;
      border: none;
      border-radius: 3px;
      cursor: pointer;
    }
    
    button:hover {
      background-color: #444d56;
    }
    </style>
    
    <!-- pages/index.vue -->
    <template>
      <div>
        <h1>Welcome to Nuxt.js!</h1>
        <GitHubLogin />
        <p v-if="isAuthenticated">Hello, {{ user.name }}!</p>
      </div>
    </template>
    
    <script>
    export default {
      components: {
        GitHubLogin,
      },
      computed: {
        isAuthenticated() {
          return this.$auth.loggedIn;
        },
        user() {
          return this.$auth.user;
        },
      },
    };
    </script>
    
    // server/api/auth/[...auth].js
    import { handleAuth } from '@nuxtjs/auth-next/dist/runtime';
    
    export default handleAuth();
    

    环境变量配置.env):

    GITHUB_CLIENT_ID=your-github-client-id
    GITHUB_CLIENT_SECRET=your-github-client-secret
    AUTH_SECRET=your-auth-secret
    
7.2 安全最佳实践
  • 防范 XSS 与 CSRF 攻击

    • 理解 XSS 和 CSRF 的攻击原理,在 Nuxt.js 中实施防护措施(如 Content Security Policy、CSRF Tokens)。
    • 实战:在项目中配置 Content Security Policy,防范 XSS 攻击。
    // nuxt.config.js
    export default {
      head: {
        meta: [
          { 'http-equiv': 'Content-Security-Policy', content: "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;" },
        ],
      },
      // 其他配置...
    };
    
  • 数据验证与错误处理

    • 输入数据的验证与清洗,统一的错误处理机制,使用库(如 Joi、Yup)进行验证。
    • 案例:为用户注册表单添加数据验证,确保数据的安全性和一致性。
    # 安装 Yup
    npm install yup
    
    // server/api/register.js
    const Yup = require('yup');
    
    const schema = Yup.object().shape({
      username: Yup.string().required(),
      email: Yup.string().email().required(),
      password: Yup.string().min(6).required(),
    });
    
    export default async function handler(req, res) {
      if (req.method === 'POST') {
        try {
          const validData = await schema.validate(req.body);
          // 处理注册逻辑,例如存储用户信息
          res.status(201).json({ message: 'User registered successfully', user: validData });
        } catch (error) {
          res.status(400).json({ message: error.errors });
        }
      } else {
        res.setHeader('Allow', ['POST']);
        res.status(405).end(`Method ${req.method} Not Allowed`);
      }
    }
    
    <!-- components/RegisterForm.vue -->
    <template>
      <div>
        <h2>Register</h2>
        <form @submit.prevent="handleSubmit">
          <input name="username" v-model="form.username" placeholder="Username" required />
          <input name="email" type="email" v-model="form.email" placeholder="Email" required />
          <input name="password" type="password" v-model="form.password" placeholder="Password" required />
          <button type="submit">Register</button>
        </form>
        <p v-if="message">{{ message }}</p>
      </div>
    </template>
    
    <script>
    export default {
      data() {
        return {
          form: { username: '', email: '', password: '' },
          message: '',
        };
      },
      methods: {
        async handleSubmit() {
          try {
            const res = await this.$axios.post('/register', this.form);
            this.message = res.data.message;
          } catch (error) {
            this.message = error.response.data.message;
          }
        },
      },
    };
    </script>
    
    <style scoped>
    form {
      display: flex;
      flex-direction: column;
      max-width: 300px;
    }
    
    input {
      margin-bottom: 1rem;
      padding: 0.5rem;
      border: 1px solid #ccc;
      border-radius: 3px;
    }
    
    button {
      padding: 0.5rem;
      background-color: #28a745;
      color: white;
      border: none;
      border-radius: 3px;
      cursor: pointer;
    }
    
    button:hover {
      background-color: #218838;
    }
    
    p {
      color: red;
    }
    </style>
    
    <!-- pages/index.vue -->
    <template>
      <div>
        <h1>Welcome to Nuxt.js!</h1>
        <RegisterForm />
      </div>
    </template>
    
    <script>
    import RegisterForm from '~/components/RegisterForm.vue';
    
    export default {
      components: {
        RegisterForm,
      },
    };
    </script>
    

学习活动

  • 视频讲解:介绍用户认证机制和常见的安全威胁。
  • 动手练习:在项目中集成 JWT 认证,配置 Nuxt Auth 模块实现社交登录。
  • 练习项目:为博客应用添加用户注册、登录功能,并实现基本的安全防护。

资源与参考


模块八:部署与运维

学习目标

  • 掌握将 Nuxt.js 应用部署到 Vercel 的流程
  • 了解其他常见的部署平台选项
  • 实现持续集成与持续部署(CI/CD)

详细内容

8.1 部署到 Vercel
  • Vercel 平台简介

    • Vercel 的特点与优势,Vercel 与 Nuxt.js 的深度集成。
    • 案例:分析一个成功部署在 Vercel 上的 Nuxt.js 应用。
  • 部署流程与配置

    • 连接 GitHub 仓库,配置项目设置(环境变量、构建命令),部署后的管理与监控。
    • 实战:将当前项目部署到 Vercel,配置必要的环境变量和设置。
    # 确保项目已初始化 Git 并推送到 GitHub
    git init
    git add .
    git commit -m "Initial commit"
    git branch -M main
    git remote add origin https://github.com/yourusername/your-repo.git
    git push -u origin main
    

    在 Vercel 上

    1. 登录 Vercel
    2. 点击 “New Project”。
    3. 选择 GitHub 仓库并授权。
    4. 配置项目设置(环境变量、构建命令)。
    5. 点击 “Deploy”。
8.2 其他部署选项
  • 部署到 Netlify

    • Netlify 的安装与配置,部署流程与注意事项。
    • 案例:对比 Vercel 和 Netlify 的部署流程,选择适合的部署平台。
    # 使用 Netlify CLI 部署
    npm install -g netlify-cli
    netlify login
    netlify init
    netlify deploy
    
  • 部署到 AWS、Heroku 等平台

    • 使用 AWS Amplify 部署 Nuxt.js 应用,Heroku 的部署步骤与配置,比较不同平台的优缺点。
    • 实战:将项目部署到 Heroku,体验不同部署平台的差异。
    # 在 Heroku 上部署
    heroku create your-app-name
    git push heroku main
    heroku open
    

    注意:确保配置环境变量和构建脚本,以适应不同平台的要求。

8.3 持续集成与持续部署(CI/CD)
  • 自动化部署流程

    • 配置 GitHub Actions 实现自动化构建与部署,使用其他 CI/CD 工具(如 GitLab CI、CircleCI)。
    • 案例:设计一个自动化部署流程,确保代码每次提交后自动部署到生产环境。
    # .github/workflows/deploy.yml
    name: Deploy to Vercel
    
    on:
      push:
        branches:
          - main
    
    jobs:
      deploy:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v2
          - name: Setup Node.js
            uses: actions/setup-node@v2
            with:
              node-version: '16'
          - name: Install Dependencies
            run: npm install
          - name: Build
            run: npm run build
          - name: Deploy
            uses: amondnet/vercel-action@v20
            with:
              vercel-token: ${{ secrets.VERCEL_TOKEN }}
              vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
              vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
              working-directory: ./
            env:
              VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
    
  • 使用 GitHub Actions

    • 创建和配置 GitHub Actions 工作流,实现代码测试、构建和部署的自动化。
    • 实战:在项目中配置 GitHub Actions,实现代码提交后的自动化测试和部署。

学习活动

  • 视频讲解:演示如何将 Nuxt.js 应用部署到 Vercel 和其他平台。
  • 动手练习:将项目部署到 Vercel,配置环境变量和部署选项。
  • 练习项目:配置 GitHub Actions 实现项目的自动化部署流程。

资源与参考


模块九:TypeScript 与 Nuxt.js

学习目标

  • 理解 TypeScript 的基本概念与类型系统
  • 学会在 Nuxt.js 项目中集成 TypeScript
  • 实现类型安全的开发,提升代码质量

详细内容

9.1 TypeScript 基础
  • 类型系统概述

    • TypeScript 的优势与基本概念,基本类型、接口和类型别名。
    • 案例:为一个简单的 Vue 组件添加类型注解,提升代码的可读性和安全性。
    // components/TypedButton.vue
    <template>
      <button @click="handleClick">{{ label }}</button>
    </template>
    
    <script lang="ts">
    import { defineComponent } from '@nuxtjs/composition-api';
    
    export default defineComponent({
      name: 'TypedButton',
      props: {
        label: {
          type: String,
          required: true,
        },
      },
      methods: {
        handleClick() {
          this.$emit('click');
        },
      },
    });
    </script>
    
    <style scoped>
    button {
      padding: 0.5rem 1rem;
      background-color: #007bff;
      color: white;
      border: none;
      border-radius: 5px;
      cursor: pointer;
    }
    
    button:hover {
      background-color: #0056b3;
    }
    </style>
    
    <!-- pages/index.vue -->
    <template>
      <div>
        <h1>Welcome to Nuxt.js with TypeScript!</h1>
        <TypedButton label="Click Me" @click="handleClick" />
      </div>
    </template>
    
    <script lang="ts">
    import { defineComponent } from '@nuxtjs/composition-api';
    import TypedButton from '~/components/TypedButton.vue';
    
    export default defineComponent({
      components: {
        TypedButton,
      },
      methods: {
        handleClick() {
          alert('Typed Button Clicked!');
        },
      },
    });
    </script>
    
  • 在 Nuxt.js 中集成 TypeScript

    • 安装和配置 TypeScript,创建 tsconfig.json 文件,将现有 JavaScript 文件迁移到 TypeScript。
    • 实战:将现有项目从 JavaScript 转换为 TypeScript,解决迁移过程中的常见问题。
    # 安装 TypeScript 和类型定义
    npm install --save-dev typescript @types/node @nuxt/types
    
    # 运行 Nuxt.js,自动生成 tsconfig.json
    npm run dev
    

    迁移示例

    // components/Button.js -> components/Button.vue(已经是 TypeScript 示例)
    
9.2 类型安全开发
  • 定义接口与类型

    • 为组件定义 Props 接口,定义 API 数据的类型。
    • 案例:为博客文章组件定义类型,确保数据的一致性和安全性。
    // types/index.ts
    export interface Post {
      id: string;
      title: string;
      content: string;
      author: string;
      date: string;
    }
    
    // components/PostCard.vue
    <template>
      <div class="card">
        <h2>{{ post.title }}</h2>
        <p>{{ post.content }}</p>
        <small>
          By {{ post.author }} on {{ post.date }}
        </small>
      </div>
    </template>
    
    <script lang="ts">
    import { defineComponent } from '@nuxtjs/composition-api';
    import { Post } from '~/types';
    
    export default defineComponent({
      name: 'PostCard',
      props: {
        post: {
          type: Object as () => Post,
          required: true,
        },
      },
    });
    </script>
    
    <style scoped>
    .card {
      border: 1px solid #ccc;
      padding: 1rem;
      margin: 1rem 0;
      border-radius: 5px;
      background-color: white;
    }
    </style>
    
    // pages/posts/_id.vue
    <template>
      <div>
        <PostCard :post="post" />
      </div>
    </template>
    
    <script lang="ts">
    import { defineComponent } from '@nuxtjs/composition-api';
    import { Post } from '~/types';
    import PostCard from '~/components/PostCard.vue';
    
    export default defineComponent({
      components: {
        PostCard,
      },
      async asyncData({ params }): Promise<{ post: Post }> {
        const post: Post = {
          id: params.id as string,
          title: `Post ${params.id}`,
          content: 'This is the post content.',
          author: 'John Doe',
          date: '2024-01-01',
        };
        return { post };
      },
    });
    </script>
    
  • 使用泛型与高级类型

    • 理解泛型的概念与应用,使用高级类型(如联合类型、交叉类型)。
    • 实战:开发一个通用的数据请求 Composable,使用泛型提升灵活性和类型安全。
    // composables/useFetch.ts
    import useSWR from 'swr';
    import axios from 'axios';
    
    export default function useFetch<T>(url: string) {
      const fetcher = (url: string): Promise<T> => axios.get(url).then(res => res.data);
      const { data, error } = useSWR<T>(url, fetcher);
    
      return {
        data,
        isLoading: !error && !data,
        isError: error,
      };
    }
    
    <!-- components/PostsList.vue -->
    <template>
      <div>
        <h2>Blog Posts</h2>
        <ul>
          <li v-for="post in data" :key="post.id">
            <strong>{{ post.title }}</strong> by {{ post.author }}
          </li>
        </ul>
      </div>
    </template>
    
    <script lang="ts">
    import { defineComponent } from '@nuxtjs/composition-api';
    import useFetch from '~/composables/useFetch';
    import { Post } from '~/types';
    
    export default defineComponent({
      setup() {
        const { data, isLoading, isError } = useFetch<Post[]>('/api/messages');
    
        return { data, isLoading, isError };
      },
    });
    </script>
    
  • 与第三方库的类型集成

    • 安装和使用类型定义文件(@types),处理自定义类型声明。
    • 案例:为一个未提供类型定义的第三方库添加自定义类型声明。
    # 安装 Lodash 类型定义文件
    npm install --save-dev @types/lodash
    
    // components/LodashExample.vue
    <template>
      <div>
        <h2>Lodash Example</h2>
        <ul>
          <li v-for="num in doubled" :key="num">{{ num }}</li>
        </ul>
      </div>
    </template>
    
    <script lang="ts">
    import { defineComponent } from '@nuxtjs/composition-api';
    import _ from 'lodash';
    
    export default defineComponent({
      setup() {
        const numbers = [1, 2, 3, 4, 5];
        const doubled = _.map(numbers, (num) => num * 2);
        return { doubled };
      },
    });
    </script>
    
    // 创建自定义类型声明(如果第三方库没有提供)
    // types/custom-lib.d.ts
    declare module 'custom-lib' {
      export function customFunction(arg: string): string;
    }
    

学习活动

  • 视频讲解:介绍 TypeScript 的基本概念和在 Nuxt.js 中的集成方法。
  • 动手练习:在现有项目中引入 TypeScript,重构组件以使用类型。
  • 练习项目:为博客应用添加 TypeScript 支持,定义 API 数据类型和组件 Props 接口。

资源与参考


模块十:实战项目开发

学习目标

  • 综合应用所学知识,独立开发一个完整的 Nuxt.js 项目
  • 练习项目规划、需求分析、功能实现和部署
  • 提升项目管理和问题解决能力

详细内容

10.1 项目规划
  • 项目选型与需求分析

    • 确定项目类型(如博客、电子商务、社交平台),编写项目需求文档。
    • 案例:选择开发一个博客应用,详细分析其功能需求和用户故事。

    项目需求文档示例

    • 用户可以注册和登录。
    • 用户可以创建、编辑和删除博客文章。
    • 用户可以评论和点赞文章。
    • 管理员可以管理所有用户和文章。
    • 网站支持多语言(英文和中文)。
  • 技术栈选择

    • 确定使用的库和工具(如 Vuex、Tailwind CSS),规划项目结构与模块划分。
    • 实战:制定项目技术栈和目录结构,确保模块化和可扩展性。

    技术栈

    • 前端框架: Nuxt.js
    • 状态管理: Vuex
    • 样式框架: Tailwind CSS
    • 数据库: MongoDB
    • 认证: @nuxtjs/auth-next
    • 部署平台: Vercel

    项目目录结构

    my-blog-app/
    ├── assets/
    │   └── css/
    ├── components/
    ├── composables/
    ├── layouts/
    ├── middleware/
    ├── pages/
    │   ├── api/
    │   ├── posts/
    │   ├── _id.vue
    │   ├── index.vue
    │   └── about.vue
    ├── plugins/
    ├── static/
    ├── store/
    ├── types/
    ├── nuxt.config.js
    ├── tsconfig.json
    └── package.json
    
10.2 项目搭建
  • 初始化项目结构

    • 设置目录结构和文件组织,配置开发环境和工具(如 ESLint、Prettier)。
    • 实战:创建项目骨架,配置代码格式化和 lint 规则。
    npx create-nuxt-app my-blog-app
    cd my-blog-app
    npm install -D tailwindcss postcss autoprefixer
    npx tailwindcss init -p
    npm install @nuxtjs/auth-next @nuxtjs/axios vuex
    
    /* assets/css/tailwind.css */
    @tailwind base;
    @tailwind components;
    @tailwind utilities;
    
    // nuxt.config.js
    export default {
      modules: ['@nuxtjs/axios', '@nuxtjs/auth-next', '@nuxtjs/tailwindcss'],
      axios: {
        baseURL: 'http://localhost:3000/api',
      },
      auth: {
        strategies: {
          github: {
            clientId: 'your-github-client-id',
            clientSecret: 'your-github-client-secret',
            codeChallengeMethod: '',
            responseType: 'code',
          },
        },
      },
      css: ['~/assets/css/tailwind.css'],
      build: {
        postcss: {
          plugins: {
            tailwindcss: {},
            autoprefixer: {},
          },
        },
      },
      // 其他配置...
    };
    
10.3 功能实现
  • 用户认证模块

    • 实现用户注册、登录、登出功能,保护受限页面和资源。
    • 实战:开发一个用户认证系统,确保只有登录用户才能访问特定页面。
    <!-- components/ProtectedRoute.vue -->
    <template>
      <div v-if="isAuthenticated">
        <slot></slot>
      </div>
      <div v-else>
        <nuxt-link to="/login">Please login to access this page.</nuxt-link>
      </div>
    </template>
    
    <script>
    export default {
      computed: {
        isAuthenticated() {
          return this.$auth.loggedIn;
        },
      },
    };
    </script>
    
    <style scoped>
    /* 可以添加额外的样式 */
    </style>
    
    <!-- pages/dashboard.vue -->
    <template>
      <ProtectedRoute>
        <div>
          <h1>Dashboard</h1>
          <p>This is a protected page.</p>
        </div>
      </ProtectedRoute>
    </template>
    
    <script>
    import ProtectedRoute from '~/components/ProtectedRoute.vue';
    
    export default {
      components: {
        ProtectedRoute,
      },
    };
    </script>
    
  • 数据展示与交互

    • 实现数据列表展示、分页、搜索功能,增加用户交互(如评论、点赞)。
    • 案例:为博客应用添加文章列表、分页和搜索功能,提升用户体验。
    <!-- components/PostsList.vue -->
    <template>
      <div>
        <h2>Blog Posts</h2>
        <input
          type="text"
          v-model="searchTerm"
          @input="handleSearch"
          placeholder="Search posts..."
          class="mb-4 p-2 border"
        />
        <ul>
          <li v-for="post in filteredPosts" :key="post.id">
            <nuxt-link :to="`/posts/${post.id}`">{{ post.title }}</nuxt-link> by {{ post.author }}
          </li>
        </ul>
        <div class="mt-4">
          <button @click="prevPage" :disabled="currentPage === 1">Previous</button>
          <span class="mx-2">Page {{ currentPage }}</span>
          <button @click="nextPage">Next</button>
        </div>
      </div>
    </template>
    
    <script>
    import { mapState, mapActions } from 'vuex';
    
    export default {
      data() {
        return {
          currentPage: 1,
          pageSize: 5,
          searchTerm: '',
        };
      },
      computed: {
        ...mapState(['posts']),
        filteredPosts() {
          let filtered = this.posts;
          if (this.searchTerm) {
            filtered = filtered.filter(post =>
              post.title.toLowerCase().includes(this.searchTerm.toLowerCase())
            );
          }
          const start = (this.currentPage - 1) * this.pageSize;
          return filtered.slice(start, start + this.pageSize);
        },
      },
      methods: {
        ...mapActions(['fetchPosts']),
        handleSearch() {
          this.currentPage = 1;
        },
        nextPage() {
          this.currentPage += 1;
        },
        prevPage() {
          if (this.currentPage > 1) this.currentPage -= 1;
        },
      },
      async mounted() {
        await this.fetchPosts();
      },
    };
    </script>
    
    <style scoped>
    button {
      padding: 0.5rem 1rem;
      margin-right: 0.5rem;
      background-color: #007bff;
      color: white;
      border: none;
      border-radius: 3px;
      cursor: pointer;
    }
    
    button:disabled {
      background-color: #cccccc;
      cursor: not-allowed;
    }
    
    button:hover:not(:disabled) {
      background-color: #0056b3;
    }
    
    ul {
      list-style: none;
      padding: 0;
    }
    
    li {
      padding: 0.5rem 0;
      border-bottom: 1px solid #ccc;
    }
    </style>
    
    // store/messages.js
    export const state = () => ({
      posts: [],
    });
    
    export const mutations = {
      setPosts(state, posts) {
        state.posts = posts;
      },
    };
    
    export const actions = {
      async fetchPosts({ commit }) {
        try {
          const res = await this.$axios.get('/messages');
          commit('setPosts', res.data);
        } catch (error) {
          console.error(error);
        }
      },
    };
    
    <!-- pages/posts/index.vue -->
    <template>
      <div>
        <h1>Blog Posts</h1>
        <PostsList />
      </div>
    </template>
    
    <script>
    import PostsList from '~/components/PostsList.vue';
    
    export default {
      components: {
        PostsList,
      },
    };
    </script>
    
  • 优化与测试

    • 应用性能优化和 SEO 设置,编写基本的单元测试和集成测试。
    • 实战:优化项目的性能,确保 SEO 设置符合最佳实践,并编写关键功能的测试用例。

    性能优化

    • 使用 Nuxt Image 组件优化图片加载。
    • 实现代码分割和懒加载。
    • 使用 SWR 或 vue-query 进行数据缓存。

    测试示例

    # 安装 Jest 和 Vue Testing Library
    npm install --save-dev jest @vue/test-utils @testing-library/vue @testing-library/jest-dom @testing-library/user-event @types/jest
    
    // jest.config.js
    module.exports = {
      preset: 'ts-jest',
      testEnvironment: 'jsdom',
      setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
      moduleNameMapper: {
        '\\.(css|less)$': 'identity-obj-proxy',
      },
    };
    
    // jest.setup.js
    import '@testing-library/jest-dom';
    
    // tests/unit/Button.test.ts
    import { render, screen, fireEvent } from '@testing-library/vue';
    import Button from '~/components/Button.vue';
    
    test('renders button with label and handles click', async () => {
      const handleClick = jest.fn();
      render(Button, {
        props: { label: 'Test Button' },
        listeners: { click: handleClick },
      });
    
      const buttonElement = screen.getByText(/Test Button/i);
      expect(buttonElement).toBeInTheDocument();
    
      await fireEvent.click(buttonElement);
      expect(handleClick).toHaveBeenCalledTimes(1);
    });
    
    // tests/unit/PostCard.test.ts
    import { render, screen } from '@testing-library/vue';
    import PostCard from '~/components/PostCard.vue';
    import { Post } from '~/types';
    
    test('renders post card with correct data', () => {
      const post: Post = {
        id: '1',
        title: 'Test Post',
        content: 'This is a test post.',
        author: 'Jane Doe',
        date: '2024-01-01',
      };
    
      render(PostCard, {
        props: { post },
      });
    
      expect(screen.getByText(/Test Post/i)).toBeInTheDocument();
      expect(screen.getByText(/This is a test post./i)).toBeInTheDocument();
      expect(screen.getByText(/By Jane Doe on 2024-01-01/i)).toBeInTheDocument();
    });
    
    // package.json
    {
      "scripts": {
        "test": "jest"
      },
      "jest": {
        "testEnvironment": "jsdom",
        "setupFilesAfterEnv": ["<rootDir>/jest.setup.js"],
        "moduleNameMapper": {
          "\\.(css|less)$": "identity-obj-proxy"
        }
      }
    }
    
    # 运行测试
    npm run test
    

    覆盖率报告:查看 coverage/lcov-report/index.html,识别未覆盖的代码区域。

学习活动

  • 项目讲解:介绍项目需求和实现步骤,确保学员理解项目目标。
  • 动手开发:分阶段完成项目的各个功能模块,逐步实现项目目标。
  • 导师指导:定期进行项目进度检查和答疑,确保学员顺利完成项目。
  • 项目评审:完成项目后进行代码评审和优化建议,提升代码质量和项目可维护性。

资源与参考


模块十一:测试与质量保障

学习目标

  • 理解测试的重要性,掌握测试的基本类型
  • 学会使用 Jest 和 Vue Testing Library 进行测试
  • 实现持续测试与质量保障

详细内容

11.1 测试基础
  • 单元测试与集成测试

    • 理解单元测试和集成测试的区别与应用场景,测试金字塔的概念。
    • 案例:分析一个功能模块的单元测试和集成测试的不同点。

    单元测试:测试组件或函数的单个部分,确保其独立工作。

    集成测试:测试多个组件或模块的集成,确保它们协同工作。

11.2 测试工具
  • 使用 Jest 进行单元测试

    • 安装和配置 Jest,编写和运行基本的单元测试。
    • 实战:为用户认证模块编写单元测试,确保功能正确性。
    # 安装 Jest 及相关依赖
    npm install --save-dev jest @vue/test-utils @testing-library/vue @testing-library/jest-dom @testing-library/user-event @types/jest
    
    // jest.config.js
    module.exports = {
      preset: 'ts-jest',
      testEnvironment: 'jsdom',
      setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
      moduleNameMapper: {
        '\\.(css|less)$': 'identity-obj-proxy',
      },
    };
    
    // jest.setup.js
    import '@testing-library/jest-dom';
    
    // tests/unit/LoginForm.test.ts
    import { render, screen, fireEvent } from '@testing-library/vue';
    import LoginForm from '~/components/LoginForm.vue';
    import axios from 'axios';
    
    jest.mock('axios');
    const mockedAxios = axios as jest.Mocked<typeof axios>;
    
    test('renders login form and handles login', async () => {
      mockedAxios.post.mockResolvedValueOnce({ data: { message: 'Login successful' } });
    
      render(LoginForm);
    
      const usernameInput = screen.getByPlaceholderText(/Username/i);
      const passwordInput = screen.getByPlaceholderText(/Password/i);
      const submitButton = screen.getByText(/Login/i);
    
      await fireEvent.update(usernameInput, 'user1');
      await fireEvent.update(passwordInput, 'pass123');
      await fireEvent.click(submitButton);
    
      const successMessage = await screen.findByText(/Login successful/i);
      expect(successMessage).toBeInTheDocument();
      expect(mockedAxios.post).toHaveBeenCalledWith('/login', { username: 'user1', password: 'pass123' });
    });
    
  • 使用 Vue Testing Library 进行组件测试

    • 安装和配置 Vue Testing Library,编写和运行组件测试,模拟用户交互。
    • 案例:为博客文章组件编写测试,确保数据正确渲染和交互功能正常。
    // tests/unit/PostCard.test.ts
    import { render, screen } from '@testing-library/vue';
    import PostCard from '~/components/PostCard.vue';
    import { Post } from '~/types';
    
    test('renders post card with correct data', () => {
      const post: Post = {
        id: '1',
        title: 'Test Post',
        content: 'This is a test post.',
        author: 'Jane Doe',
        date: '2024-01-01',
      };
    
      render(PostCard, {
        props: { post },
      });
    
      expect(screen.getByText(/Test Post/i)).toBeInTheDocument();
      expect(screen.getByText(/This is a test post./i)).toBeInTheDocument();
      expect(screen.getByText(/By Jane Doe on 2024-01-01/i)).toBeInTheDocument();
    });
    
    // tests/unit/ProtectedRoute.test.ts
    import { render, screen } from '@testing-library/vue';
    import ProtectedRoute from '~/components/ProtectedRoute.vue';
    import { createStore } from 'vuex';
    import { auth } from '@nuxtjs/auth-next';
    
    test('renders children when authenticated', () => {
      const store = createStore({
        modules: {
          auth: {
            namespaced: true,
            state: () => ({
              loggedIn: true,
              user: { name: 'John Doe', email: '[email protected]' },
            }),
            getters: {
              loggedIn: (state) => state.loggedIn,
              user: (state) => state.user,
            },
          },
        },
      });
    
      render(ProtectedRoute, {
        slots: {
          default: '<div>Protected Content</div>',
        },
        global: {
          plugins: [store],
        },
      });
    
      expect(screen.getByText(/Protected Content/i)).toBeInTheDocument();
    });
    
    test('does not render children when not authenticated', () => {
      const store = createStore({
        modules: {
          auth: {
            namespaced: true,
            state: () => ({
              loggedIn: false,
              user: null,
            }),
            getters: {
              loggedIn: (state) => state.loggedIn,
              user: (state) => state.user,
            },
          },
        },
      });
    
      render(ProtectedRoute, {
        slots: {
          default: '<div>Protected Content</div>',
        },
        global: {
          plugins: [store],
        },
      });
    
      expect(screen.queryByText(/Protected Content/i)).not.toBeInTheDocument();
      expect(screen.getByText(/Please login to access this page./i)).toBeInTheDocument();
    });
    
11.3 持续测试
  • 集成测试到 CI 流程

    • 在 GitHub Actions 中配置测试工作流,实现代码提交后的自动化测试。
    • 实战:为项目配置 GitHub Actions,使每次代码提交后自动运行测试用例。
    # .github/workflows/test.yml
    name: Run Tests
    
    on: [push, pull_request]
    
    jobs:
      build:
        runs-on: ubuntu-latest
    
        steps:
          - uses: actions/checkout@v2
          - name: Setup Node.js
            uses: actions/setup-node@v2
            with:
              node-version: '16'
          - name: Install Dependencies
            run: npm install
          - name: Run Tests
            run: npm run test
    
  • 实现代码覆盖率

    • 配置 Jest 生成代码覆盖率报告,分析和提升测试覆盖率。
    • 案例:通过代码覆盖率报告识别未覆盖的代码区域,完善测试用例。
    // package.json
    {
      "scripts": {
        "test": "jest --coverage"
      },
      "jest": {
        "preset": "ts-jest",
        "testEnvironment": "jsdom",
        "setupFilesAfterEnv": ["<rootDir>/jest.setup.js"],
        "moduleNameMapper": {
          "\\.(css|less)$": "identity-obj-proxy"
        },
        "collectCoverage": true,
        "collectCoverageFrom": ["components/**/*.{vue,ts}", "pages/**/*.{vue,ts}"],
        "coverageReporters": ["json", "lcov", "text", "clover"]
      }
    }
    
    // 使用 Jest 进行代码覆盖率
    // 运行测试并生成覆盖率报告
    npm run test
    

    覆盖率报告:查看 coverage/lcov-report/index.html,识别未覆盖的代码区域。

学习活动

  • 视频讲解:介绍测试的基本概念和工具的使用方法。
  • 动手练习:为项目中的组件编写单元测试和集成测试。
  • 练习项目:实现一个简单的测试覆盖率,确保关键功能的正确性。

资源与参考


模块十二:扩展与高级应用

学习目标

  • 掌握无服务器架构,使用 Serverless Functions
  • 实现实时功能,如聊天或通知
  • 集成 GraphQL,构建高效的数据接口

详细内容

12.1 Serverless Functions
  • 无服务器架构简介

    • 理解无服务器架构的概念与优势,常见的无服务器平台(如 Vercel Functions、AWS Lambda)。
    • 案例:分析无服务器架构在实际项目中的应用场景。
  • 使用 Nuxt.js 的 Serverless Functions

    • 创建和部署 Serverless Functions,处理异步任务和后台处理。
    • 实战:开发一个图像处理的 Serverless Function,实现用户上传图片后自动调整大小。
    # 安装 Sharp 进行图像处理
    npm install sharp
    
    // server/api/resize-image.js
    const sharp = require('sharp');
    
    export default async function handler(req, res) {
      if (req.method === 'POST') {
        try {
          const { image, width, height } = req.body;
          const buffer = Buffer.from(image, 'base64');
    
          const resizedImage = await sharp(buffer)
            .resize(width, height)
            .toBuffer();
    
          res.status(200).json({ resizedImage: resizedImage.toString('base64') });
        } catch (error) {
          res.status(500).json({ message: 'Image processing failed' });
        }
      } else {
        res.setHeader('Allow', ['POST']);
        res.status(405).end(`Method ${req.method} Not Allowed`);
      }
    }
    
    <!-- components/ImageUploader.vue -->
    <template>
      <div>
        <h2>Image Uploader and Resizer</h2>
        <input type="file" accept="image/*" @change="handleImageChange" />
        <div v-if="image">
          <img :src="image" alt="Original" width="100" />
          <div>
            <label>
              Width:
              <input type="number" v-model.number="width" />
            </label>
            <label>
              Height:
              <input type="number" v-model.number="height" />
            </label>
          </div>
          <button @click="handleResize">Resize Image</button>
        </div>
        <div v-if="resizedImage">
          <h3>Resized Image</h3>
          <img :src="resizedImage" alt="Resized" />
        </div>
      </div>
    </template>
    
    <script>
    export default {
      data() {
        return {
          image: null,
          resizedImage: null,
          width: 300,
          height: 300,
        };
      },
      methods: {
        handleImageChange(e) {
          const file = e.target.files[0];
          if (file) {
            const reader = new FileReader();
            reader.onload = () => {
              this.image = reader.result;
            };
            reader.readAsDataURL(file);
          }
        },
        async handleResize() {
          if (this.image) {
            const base64Image = this.image.split(',')[1];
            const res = await this.$axios.post('/resize-image', {
              image: base64Image,
              width: this.width,
              height: this.height,
            });
            this.resizedImage = `data:image/jpeg;base64,${res.data.resizedImage}`;
          }
        },
      },
    };
    </script>
    
    <style scoped>
    input {
      margin-bottom: 1rem;
      padding: 0.5rem;
      border: 1px solid #ccc;
      border-radius: 3px;
    }
    
    button {
      padding: 0.5rem 1rem;
      background-color: #ffc107;
      color: black;
      border: none;
      border-radius: 3px;
      cursor: pointer;
    }
    
    button:hover {
      background-color: #e0a800;
    }
    
    img {
      margin-top: 1rem;
      border: 1px solid #ccc;
      border-radius: 5px;
    }
    </style>
    
    <!-- pages/index.vue -->
    <template>
      <div>
        <h1>Welcome to Nuxt.js!</h1>
        <ImageUploader />
      </div>
    </template>
    
    <script>
    import ImageUploader from '~/components/ImageUploader.vue';
    
    export default {
      components: {
        ImageUploader,
      },
    };
    </script>
    
12.2 实时功能
  • 集成 WebSockets

    • WebSockets 的基本概念与使用场景,在 Nuxt.js 中集成 WebSocket 服务(如 Socket.io)。
    • 案例:开发一个实时聊天应用,理解 WebSocket 的通信机制。
  • 实现实时聊天或通知功能

    • 构建实时聊天应用的基础功能,实现实时通知系统。
    • 实战:为博客应用添加实时评论通知,提升用户互动体验。
    # 安装 Socket.io
    npm install socket.io socket.io-client
    
    // server/socket.js
    const { Server } = require('socket.io');
    
    let io;
    
    module.exports = function (server) {
      if (!io) {
        io = new Server(server, {
          cors: {
            origin: '*',
            methods: ['GET', 'POST'],
          },
        });
    
        io.on('connection', (socket) => {
          console.log('New client connected');
    
          socket.on('sendComment', (comment) => {
            io.emit('newComment', comment);
          });
    
          socket.on('disconnect', () => {
            console.log('Client disconnected');
          });
        });
      }
      return io;
    };
    
    // server/index.js
    const express = require('express');
    const http = require('http');
    const socketHandler = require('./socket');
    
    const app = express();
    const server = http.createServer(app);
    const io = socketHandler(server);
    
    app.use(express.json());
    
    app.get('/api/messages', (req, res) => {
      res.json([]);
    });
    
    app.post('/api/messages', (req, res) => {
      const { name, message } = req.body;
      io.emit('newComment', { name, message, id: Date.now() });
      res.status(201).json({ message: 'Comment added successfully' });
    });
    
    server.listen(3000, () => {
      console.log('Server is running on port 3000');
    });
    
    <!-- components/RealTimeComments.vue -->
    <template>
      <div>
        <h2>Real-Time Comments</h2>
        <form @submit.prevent="handleSubmit">
          <input name="name" v-model="form.name" placeholder="Your Name" required />
          <textarea name="message" v-model="form.message" placeholder="Your Comment" required></textarea>
          <button type="submit">Send</button>
        </form>
        <ul>
          <li v-for="msg in messages" :key="msg.id">
            <strong>{{ msg.name }}:</strong> {{ msg.message }}
          </li>
        </ul>
      </div>
    </template>
    
    <script>
    import io from 'socket.io-client';
    import { ref, onMounted, onUnmounted } from '@nuxtjs/composition-api';
    
    export default {
      setup() {
        const socket = io();
        const messages = ref([]);
        const form = ref({ name: '', message: '' });
    
        const handleSubmit = () => {
          socket.emit('sendComment', form.value);
          form.value = { name: '', message: '' };
        };
    
        const addMessage = (msg) => {
          messages.value.push(msg);
        };
    
        onMounted(() => {
          socket.on('newComment', addMessage);
        });
    
        onUnmounted(() => {
          socket.off('newComment', addMessage);
          socket.disconnect();
        });
    
        return { messages, form, handleSubmit };
      },
    };
    </script>
    
    <style scoped>
    form {
      display: flex;
      flex-direction: column;
      max-width: 400px;
    }
    
    input, textarea {
      margin-bottom: 1rem;
      padding: 0.5rem;
      border: 1px solid #ccc;
      border-radius: 3px;
    }
    
    button {
      padding: 0.5rem 1rem;
      background-color: #007bff;
      color: white;
      border: none;
      border-radius: 3px;
      cursor: pointer;
    }
    
    button:hover {
      background-color: #0056b3;
    }
    
    ul {
      list-style: none;
      padding: 0;
    }
    
    li {
      padding: 0.5rem;
      border-bottom: 1px solid #ccc;
    }
    </style>
    
    <!-- pages/index.vue -->
    <template>
      <div>
        <h1>Welcome to Nuxt.js!</h1>
        <RealTimeComments />
      </div>
    </template>
    
    <script>
    import RealTimeComments from '~/components/RealTimeComments.vue';
    
    export default {
      components: {
        RealTimeComments,
      },
    };
    </script>
    
12.3 GraphQL 与 Nuxt.js
  • 使用 Apollo Client 集成 GraphQL

    • Apollo Client 的安装与配置,查询和变更数据的基本操作。
    • 案例:在项目中集成 Apollo Client,实现高效的数据查询。
    # 安装 Apollo Client
    npm install @nuxtjs/apollo
    
    // nuxt.config.js
    export default {
      modules: ['@nuxtjs/apollo'],
      apollo: {
        clientConfigs: {
          default: {
            httpEndpoint: 'http://localhost:3000/api/graphql',
          },
        },
      },
      // 其他配置...
    };
    
    <!-- components/ApolloPosts.vue -->
    <template>
      <div>
        <h2>GraphQL Posts</h2>
        <ul>
          <li v-for="post in posts" :key="post.id">
            <strong>{{ post.title }}</strong> by {{ post.author }}
          </li>
        </ul>
      </div>
    </template>
    
    <script>
    import { gql } from 'graphql-tag';
    import { useQuery } from '@vue/apollo-composable';
    
    export default {
      setup() {
        const GET_POSTS = gql`
          query GetPosts {
            posts {
              id
              title
              content
              author
              date
            }
          }
        `;
    
        const { result, loading, error } = useQuery(GET_POSTS);
    
        return {
          posts: result,
          loading,
          error,
        };
      },
    };
    </script>
    
    <!-- pages/index.vue -->
    <template>
      <div>
        <h1>Welcome to Nuxt.js!</h1>
        <ApolloPosts />
      </div>
    </template>
    
    <script>
    import ApolloPosts from '~/components/ApolloPosts.vue';
    
    export default {
      components: {
        ApolloPosts,
      },
    };
    </script>
    
  • 构建 GraphQL API

    • 使用 Apollo Server 创建 GraphQL API,定义 Schema 和 Resolvers。
    • 实战:开发一个 GraphQL API,支持文章的查询和创建操作。
    # 安装 Apollo Server 和相关依赖
    npm install apollo-server-micro graphql
    
    // server/api/graphql.js
    const { ApolloServer, gql } = require('apollo-server-micro');
    const Cors = require('micro-cors');
    
    const cors = Cors();
    
    const typeDefs = gql`
      type Post {
        id: ID!
        title: String!
        content: String!
        author: String!
        date: String!
      }
    
      type Query {
        posts: [Post!]!
      }
    
      type Mutation {
        addPost(title: String!, content: String!, author: String!): Post!
      }
    `;
    
    let posts = [
      { id: '1', title: 'First Post', content: 'Content of first post', author: 'John Doe', date: '2024-01-01' },
      { id: '2', title: 'Second Post', content: 'Content of second post', author: 'Jane Smith', date: '2024-01-02' },
    ];
    
    const resolvers = {
      Query: {
        posts: () => posts,
      },
      Mutation: {
        addPost: (_, { title, content, author }) => {
          const newPost = { id: (posts.length + 1).toString(), title, content, author, date: new Date().toISOString() };
          posts.push(newPost);
          return newPost;
        },
      },
    };
    
    const apolloServer = new ApolloServer({ typeDefs, resolvers });
    const startServer = apolloServer.start();
    
    export default cors(async function handler(req, res) {
      if (req.method === 'OPTIONS') {
        res.end();
        return false;
      }
      await startServer;
      await apolloServer.createHandler({
        path: '/api/graphql',
      })(req, res);
    });
    
    export const config = {
      api: {
        bodyParser: false,
      },
    };
    
    <!-- components/AddPost.vue -->
    <template>
      <div>
        <h2>Add New Post</h2>
        <form @submit.prevent="handleSubmit">
          <input name="title" v-model="form.title" placeholder="Title" required />
          <textarea name="content" v-model="form.content" placeholder="Content" required></textarea>
          <input name="author" v-model="form.author" placeholder="Author" required />
          <button type="submit" :disabled="loading">Add Post</button>
        </form>
        <p v-if="error">Error adding post.</p>
        <p v-if="data">Post added successfully!</p>
      </div>
    </template>
    
    <script>
    import { gql } from 'graphql-tag';
    import { useMutation } from '@vue/apollo-composable';
    import { ref } from '@nuxtjs/composition-api';
    
    export default {
      setup() {
        const ADD_POST = gql`
          mutation AddPost($title: String!, $content: String!, $author: String!) {
            addPost(title: $title, content: $content, author: $author) {
              id
              title
              content
              author
              date
            }
          }
        `;
    
        const form = ref({ title: '', content: '', author: '' });
        const { mutate, onDone, onError, loading, error, data } = useMutation(ADD_POST);
    
        const handleSubmit = () => {
          mutate(form.value);
        };
    
        return { form, handleSubmit, loading, error, data };
      },
    };
    </script>
    
    <style scoped>
    form {
      display: flex;
      flex-direction: column;
      max-width: 400px;
    }
    
    input, textarea {
      margin-bottom: 1rem;
      padding: 0.5rem;
      border: 1px solid #ccc;
      border-radius: 3px;
    }
    
    button {
      padding: 0.5rem 1rem;
      background-color: #ffc107;
      color: black;
      border: none;
      border-radius: 3px;
      cursor: pointer;
    }
    
    button:disabled {
      background-color: #e0a800;
      cursor: not-allowed;
    }
    
    p {
      color: red;
    }
    </style>
    
    <!-- pages/index.vue -->
    <template>
      <div>
        <h1>Welcome to Nuxt.js with GraphQL!</h1>
        <AddPost />
        <ApolloPosts />
      </div>
    </template>
    
    <script>
    import AddPost from '~/components/AddPost.vue';
    import ApolloPosts from '~/components/ApolloPosts.vue';
    
    export default {
      components: {
        AddPost,
        ApolloPosts,
      },
    };
    </script>
    
12.4 集成第三方服务
  • 支付集成

    • 使用 Stripe 或其他支付服务,实现支付功能。
    • 案例:为博客应用添加付费订阅功能,允许用户订阅以获取高级内容。
    # 安装 Stripe
    npm install stripe @types/stripe
    
    // server/api/stripe.js
    const Stripe = require('stripe');
    const stripe = Stripe(process.env.STRIPE_SECRET_KEY);
    
    export default async function handler(req, res) {
      if (req.method === 'POST') {
        try {
          const session = await stripe.checkout.sessions.create({
            payment_method_types: ['card'],
            line_items: [
              {
                price_data: {
                  currency: 'usd',
                  product_data: {
                    name: 'Premium Subscription',
                  },
                  unit_amount: 999,
                },
                quantity: 1,
              },
            ],
            mode: 'payment',
            success_url: `${req.headers.origin}/success?session_id={CHECKOUT_SESSION_ID}`,
            cancel_url: `${req.headers.origin}/cancel`,
          });
          res.status(200).json({ sessionId: session.id });
        } catch (err) {
          res.status(500).json({ error: 'Failed to create session' });
        }
      } else {
        res.setHeader('Allow', 'POST');
        res.status(405).end('Method Not Allowed');
      }
    }
    
    <!-- components/CheckoutButton.vue -->
    <template>
      <button @click="handleCheckout" :disabled="loading">
        {{ loading ? 'Processing...' : 'Subscribe for $9.99/month' }}
      </button>
    </template>
    
    <script>
    export default {
      data() {
        return {
          loading: false,
        };
      },
      methods: {
        async handleCheckout() {
          this.loading = true;
          try {
            const res = await this.$axios.post('/stripe');
            const stripe = Stripe('your-publishable-key');
            await stripe.redirectToCheckout({ sessionId: res.data.sessionId });
          } catch (error) {
            console.error(error);
          } finally {
            this.loading = false;
          }
        },
      },
    };
    </script>
    
    <style scoped>
    button {
      padding: 0.5rem 1rem;
      background-color: #ffc107;
      color: black;
      border: none;
      border-radius: 3px;
      cursor: pointer;
    }
    
    button:disabled {
      background-color: #e0a800;
      cursor: not-allowed;
    }
    
    button:hover:not(:disabled) {
      background-color: #d39e00;
    }
    </style>
    
    <!-- pages/index.vue -->
    <template>
      <div>
        <h1>Welcome to Nuxt.js!</h1>
        <CheckoutButton />
      </div>
    </template>
    
    <script>
    import CheckoutButton from '~/components/CheckoutButton.vue';
    
    export default {
      components: {
        CheckoutButton,
      },
    };
    </script>
    

    注意:确保在 .env 文件中配置 Stripe 的密钥。

    STRIPE_SECRET_KEY=your-stripe-secret-key
    NEXTAUTH_SECRET=your-auth-secret
    
12.5 监控与日志
  • 集成监控工具

    • 使用 Sentry 或其他监控工具,监控应用错误和性能。
    • 实战:为项目集成 Sentry,实时监控和报告错误。
    # 安装 Sentry
    npm install @nuxtjs/sentry
    
    // nuxt.config.js
    export default {
      modules: ['@nuxtjs/sentry'],
      sentry: {
        dsn: 'your-sentry-dsn',
        config: {},
      },
      // 其他配置...
    };
    
    <!-- components/ErrorBoundary.vue -->
    <template>
      <nuxt-error v-if="hasError" :error="error" />
      <div v-else>
        <slot></slot>
      </div>
    </template>
    
    <script>
    export default {
      data() {
        return {
          hasError: false,
          error: null,
        };
      },
      errorCaptured(err, vm, info) {
        this.hasError = true;
        this.error = err;
        this.$sentry.captureException(err);
        return false;
      },
    };
    </script>
    
    <!-- layouts/default.vue -->
    <template>
      <div>
        <NavBar />
        <ErrorBoundary>
          <nuxt />
        </ErrorBoundary>
      </div>
    </template>
    
    <script>
    import NavBar from '~/components/NavBar.vue';
    import ErrorBoundary from '~/components/ErrorBoundary.vue';
    
    export default {
      components: {
        NavBar,
        ErrorBoundary,
      },
    };
    </script>
    
    SENTRY_DSN=your-sentry-dsn
    

学习活动

  • 视频讲解:演示 Serverless Functions 和实时功能的实现过程。
  • 动手练习:在项目中添加 Serverless Functions,集成 WebSockets 实现实时聊天。
  • 练习项目:为博客应用添加实时评论功能,使用 GraphQL 构建数据接口。

资源与参考


模块十三:课程总结与展望

学习目标

  • 回顾并巩固课程中学到的核心概念和技能
  • 指导学员制定进一步学习和发展的路径
  • 提供就业指导和项目展示机会

详细内容

13.1 知识回顾
  • 复习核心概念与技能
    • 总结 Nuxt.js 的关键功能和最佳实践,回顾各模块的重要知识点。
    • 案例:通过问答和讨论,帮助学员巩固所学知识。
13.2 进一步学习路径
  • 深入学习 Nuxt.js 高级特性

    • 探索 Nuxt.js 的高级功能,如 Middleware、Server Middleware,学习性能优化的深层技巧。
    • 实战:为项目添加 Middleware,实现请求的拦截和处理。
    // middleware/auth.ts
    export default function ({ store, redirect }) {
      if (!store.state.auth.loggedIn) {
        return redirect('/login');
      }
    }
    
    <!-- pages/dashboard.vue -->
    <template>
      <div>
        <h1>Dashboard</h1>
        <p>This is a protected page.</p>
      </div>
    </template>
    
    <script>
    export default {
      middleware: 'auth',
    };
    </script>
    
  • 探索相关技术栈

    • 了解 Nuxt.js 与其他框架的集成,如 Vue Native,构建跨平台移动应用。
    • 案例:展示一个使用 Vue Native 开发的移动应用示例。
13.3 就业指导与项目展示
  • 项目作品集制作

    • 如何展示和组织项目作品,编写项目文档和 README 文件。
    • 实战:指导学员整理和优化个人项目,准备作品集。
    # My Blog App
    
    ## Description
    A full-stack blog application built with Nuxt.js, TypeScript, Vuex, Tailwind CSS, and GraphQL. Features include user authentication, real-time comments, and SEO optimization.
    
    ## Features
    - User registration and login with @nuxtjs/auth-next
    - Create, edit, and delete blog posts
    - Real-time comments with Socket.io
    - GraphQL API with Apollo Server
    - Responsive design with Tailwind CSS
    
    ## Installation
    1. Clone the repository:
       ```bash
       git clone https://github.com/yourusername/my-blog-app.git
    
    1. Navigate to the project directory:
      cd my-blog-app
      
    2. Install dependencies:
      npm install
      
    3. Create a .env file and add the necessary environment variables:
      GITHUB_CLIENT_ID=your-github-client-id
      GITHUB_CLIENT_SECRET=your-github-client-secret
      STRIPE_SECRET_KEY=your-stripe-secret-key
      SENTRY_DSN=your-sentry-dsn
      

    Usage

    npm run dev
    

    License

    MIT

    
    
  • 求职建议与面试技巧

    • 准备技术面试的问题和答案,简历优化和求职渠道推荐。
    • 案例:模拟技术面试,提供面试问题和答题技巧。

    常见面试问题

    1. 解释 Nuxt.js 的静态生成(SSG)和服务端渲染(SSR)的区别。
    2. 如何在 Nuxt.js 中实现 API 路由?
    3. 什么是 Vue Composition API?举例说明其使用。
    4. 如何在 Nuxt.js 应用中进行国际化?
    5. 解释 JWT 的工作原理及其在认证中的应用。

    答题技巧

    • 清晰简洁:简明扼要地回答问题,避免冗长。
    • 示例说明:使用具体的代码示例或项目经验来支持回答。
    • 结构化思维:逻辑清晰地组织答案,分点阐述。

学习活动

  • 视频讲解:总结课程内容,分享进一步学习的建议。
  • 动手活动:整理和完善个人项目,准备项目展示。
  • 互动讨论:参与在线讨论,分享学习心得和求职经验。

资源与参考


教学方式

视频讲解

每个模块包含详细的视频教程,分步骤讲解理论知识和实际操作,结合示范代码演示关键概念。视频内容结构清晰,便于学员跟随学习。

实战项目

通过实际项目开发,帮助学员将所学知识应用于真实场景,提升项目开发和问题解决能力。每个模块都有对应的实战项目,确保学员在实践中巩固所学知识。

课后练习

每个模块配有练习题和编程任务,巩固学习效果,确保学员能够理解并应用所学内容。练习题涵盖理论知识和实际编码,全面提升学员的综合能力。

讨论与答疑

提供在线论坛或社群,学员可以互相交流,分享学习经验,导师实时答疑,解决学习中的疑问。通过互动讨论,促进学员之间的合作与学习。


推荐学习资源

;