掘金是用什么写的?Nuxt就可以实现一模一样的效果

1,511 阅读12分钟

为了提高自己的全栈能力,学完Vue的我,又开始学习了Nuxt,以下就是我用Nuxt创建了一个简单的论坛。

1.Nuxt是什么?

Nuxt.js 是一个基于 Vue.js 的框架,主要用于构建服务端渲染(SSR)或静态网站生成(SSG)的应用。它提供了一些开箱即用的功能,简化了 Vue 应用的开发过程。

主要特点:

  1. 服务端渲染:Nuxt.js 可以让你的 Vue 应用在服务器上渲染,提升页面加载速度和 SEO 优化。
  2. 静态网站生成:可以将应用生成静态文件,便于部署和快速加载。
  3. 路由自动生成:Nuxt 会根据 pages 目录中的文件自动生成路由,不需要手动配置。
  4. 模块化架构:内置许多模块,支持状态管理(Vuex)、API 请求、PWA 等功能,方便扩展。
  5. 插件支持:可以在应用中轻松引入各种插件,增强功能。

生活中的例子:

想象一下,你想做一个电商网站。使用 Nuxt.js,你可以:

  • 提升 SEO:通过 SSR,让搜索引擎更容易抓取页面内容,增加曝光率。
  • 快速加载:生成静态页面,使用户在访问时能更快看到商品。
  • 简单路由:只需在 pages 文件夹中创建对应的文件,自动生成商品详情、购物车等页面。

帮助你更高效地开发现代化的 Vue 应用,特别是在 SEO 和性能方面表现优异。

2.ssr是什么?spa又是什么?

SSR(Server-Side Rendering)和 SPA(Single Page Application)是两种不同的前端渲染方式,各有优缺点。

1. SSR(服务端渲染)

服务端渲染指的是网页的内容在服务器端生成后,发送到客户端,浏览器直接显示完整的 HTML 页面。

工作流程:
  • 用户请求一个网页时,服务器先把页面的 HTML 生成好,带着数据一起返回给用户。
  • 浏览器拿到这个完整的页面后直接渲染出来,用户立即看到页面内容。
  • 当页面加载完成后,再通过 JavaScript 使页面变得“互动”。
优点:
  • SEO 友好:因为搜索引擎能抓取到完整的 HTML 内容。
  • 首屏加载快:用户可以快速看到页面内容,因为不需要先加载 JavaScript 再渲染页面。
缺点:
  • 服务器压力大:每次请求都需要服务器生成完整的页面。
  • 交互延迟:页面初始加载后,直到 JavaScript 完全加载,页面交互才能生效。

2. SPA(单页应用)

单页应用指的是整个应用只有一个 HTML 页面,所有页面内容和交互都是通过 JavaScript 动态加载和切换的。

工作流程:
  • 首次加载时,服务器只返回一个空的 HTML 壳和 JavaScript 文件。
  • JavaScript 运行后,通过 API 请求数据并渲染页面内容。
  • 后续页面切换时,只会加载部分内容,不会刷新整个页面。
优点:
  • 流畅的用户体验:页面切换无需刷新,用户体验更好。
  • 减少服务器负载:页面的大部分逻辑都在客户端处理,减少了服务器压力。
缺点:
  • 首屏加载慢:用户必须先下载 JavaScript 文件才能渲染页面内容。
  • SEO 不友好:因为初始 HTML 为空,搜索引擎可能抓取不到有价值的内容(可以通过 SSR 或其他方式解决)。
ssr和spa的对比:

ssr:就像去餐馆点菜,服务员会把所有菜肴准备好,一起送到你的桌上,你马上就能开始吃。

spa:就像吃自助餐,开始的时候拿一个空盘子,自己去拿食物,后续想吃什么就直接去取,不用每次都回到桌子上等服务员端上菜。

总结:

  • SSR 更适合需要快速加载和注重 SEO 的场景,如新闻网站、博客等。
  • SPA 适合需要高频交互和流畅体验的场景,如社交网络、后台管理系统等。

用Nuxt开发论坛

服务端渲染语言Nuxt + 强类型约束TS + 模拟后端接口json-server 实现一个简易的论坛。

如何使用 Nuxt.js

1. 安装 Nuxt 项目

首先,通过命令行工具创建 Nuxt.js 项目:

npx nuxi init <project-name>
cd <project-name>
npm install

这里 nuxi 是 Nuxt 3 的 CLI 工具,用来初始化和管理 Nuxt 项目。

2. 运行 Nuxt 项目

安装完所有依赖后,可以启动开发服务器:

npm run dev

默认情况下,Nuxt 开发服务器运行在 http://localhost:3000

3. 目录结构

Nuxt 有特定的目录结构,它基于文件系统来定义路由、布局、页面等。一个标准的 Nuxt 项目通常包含以下目录:

  • pages/:页面文件夹,文件名决定了路由。例如 pages/index.vue 对应 / 路由,pages/about.vue 对应 /about
  • layouts/:用于定义布局,页面可以继承不同的布局。
  • components/:用于存放 Vue 组件。
  • store/:用于定义 Vuex store 文件,用于管理全局状态。
  • static/:存放静态文件,如图片、字体等。
4. 创建页面和路由

Nuxt 自动根据 pages 目录下的文件生成路由。例如,创建以下文件结构:

pages/
 ├── index.vue
 ├── about.vue

index.vue 对应 / 路由,about.vue 对应 /about 路由。

5. 使用布局(layouts)

Nuxt 支持布局系统,可以为页面定义不同的布局。默认布局文件是 layouts/default.vue,每个页面会自动使用该布局。

例如,定义一个布局:

<!-- layouts/default.vue -->
<template>
  <div>
    <header>
      <h1>My Website Header</h1>
    </header>
    <nuxt />
    <footer>
      <p>Footer</p>
    </footer>
  </div>
</template>

页面的内容会被插入 <nuxt /> 的位置。

6. 使用 Vuex(store)

如果你需要全局状态管理,Nuxt 内置对 Vuex 的支持。创建 store/index.js 文件,Nuxt 会自动处理。

// store/index.js
export const state = () => ({
  counter: 0
})

export const mutations = {
  increment(state) {
    state.counter++
  }
}

然后可以在页面或组件中使用它:

<template>
  <div>
    <p>Counter: {{ $store.state.counter }}</p>
    <button @click="$store.commit('increment')">增加计数</button>
  </div>
</template>
7. API 请求

Nuxt 提供了 asyncData 方法,允许你在页面加载前获取数据。这个方法会在服务端或客户端执行,取决于页面的渲染模式。

<template>
  <div>
    <h1>{{ post.title }}</h1>
    <p>{{ post.content }}</p>
  </div>
</template>

<script>
export default {
  async asyncData({ $axios, params }) {
    const post = await $axios.$get(`/api/posts/${params.id}`)
    return { post }
  }
}
</script>

如何用json-server

1. 安装 json-server

首先,你需要在项目中安装 json-server。可以通过 npm 或 yarn 安装。

npm install -g json-server

全局安装后,你可以在任何地方使用 json-server

2. 创建一个 JSON 文件

接下来,你需要创建一个模拟数据库文件,通常叫做 db.json,这个文件会作为 json-server 的数据源。

{
    "posts": [
        {
            "id": 1,
            "title": "如何吸引野生画眉鸟到你的花园?",
            "content": "画眉鸟以其悦耳的鸣叫闻名。要吸引这些美丽的鸟儿到你的花园,可以在庭院中种植它们喜欢的植物,如灌木和果树,或者放置鸟食器,提供新鲜的水果和昆虫饲料。",
            "user": {
                "id": 2,
                "name": "鸟鸣之声"
            }
        },
        {
            "id": 2,
            "title": "识别常见的城市鸟类:麻雀与鸽子",
            "content": "在城市环境中,麻雀和鸽子是最常见的鸟类。麻雀通常小巧,身体棕色,喜欢成群结队,而鸽子体型较大,多见于公园和广场附近。通过观察它们的体型和行为,很容易区分这两种鸟类。",
            "user": {
                "id": 3,
                "name": "飞鸟观察家"
            }
        },
        {
            "id": 3,
            "title": "夏季如何为鸟类提供合适的饮水?",
            "content": "夏天鸟类需要充足的水分,尤其是在干旱地区。你可以在庭院中放置浅水盘,每天更换新鲜的水,并确保水盆安全、无害,让鸟类能安心饮水。",
            "user": {
                "id": 4,
                "name": "自然守护者"
            }
        },
        {
            "id": 4,
            "title": "鸟类迁徙的秘密:如何跟踪候鸟的迁徙路径?",
            "content": "通过安装GPS设备,科学家们能够跟踪候鸟的迁徙路径,了解它们的迁徙习惯和栖息地变化。这些数据对保护濒危鸟类具有重要意义。",
            "user": {
                "id": 5,
                "name": "迁徙研究员"
            }
        },
        {
            "id": 5,
            "title": "鹦鹉的饲养技巧:从食物到互动",
            "content": "饲养鹦鹉不仅仅是提供食物,还要注重与它们的互动。鹦鹉喜欢玩具和挑战,给它们提供丰富的精神刺激对健康有好处。食物方面,可以提供多种水果、种子和蔬菜。",
            "user": {
                "id": 6,
                "name": "鹦鹉达人"
            }
        },
        {
            "id": 6,
            "title": "夜莺:夜晚的歌手",
            "content": "夜莺以其夜晚优美的鸣唱而闻名。夜莺通常在春季开始鸣唱,主要为了吸引伴侣和划定领地。它们的鸣叫节奏复杂,频率多变,非常动听。",
            "user": {
                "id": 7,
                "name": "夜鸟迷"
            }
        },
        {
            "id": 7,
            "title": "如何搭建合适的鸟巢箱?",
            "content": "自制鸟巢箱可以吸引鸟类在你家附近筑巢。选择合适的材料如天然木材,确保鸟巢的通风和排水,并根据目标鸟类调整入口大小。放置在高处且不易被捕食者攻击的位置是关键。",
            "user": {
                "id": 8,
                "name": "DIY爱好者"
            }
        },
        {
            "id": 8,
            "title": "冬季如何帮助鸟类过冬?",
            "content": "冬天食物匮乏,鸟类需要额外的能量来抵御寒冷。你可以为它们提供高能量的种子和坚果,或者安装鸟食器,并在寒冷的日子定期补充食物。",
            "user": {
                "id": 9,
                "name": "冬日守护者"
            }
        },
        {
            "id": 9,
            "title": "金翅雀的饲养与繁殖注意事项",
            "content": "金翅雀是一种美丽的鸟类,饲养它们需要特别注意饮食搭配,多提供新鲜的蔬果和种子。繁殖期间要确保它们有安静的环境,避免过度打扰。",
            "user": {
                "id": 10,
                "name": "金翅爱好者"
            }
        },
        {
            "id": 10,
            "title": "发现稀有鸟类的乐趣:我在森林中的一次探险",
            "content": "上周,我在森林探险时意外发现了一只罕见的蓝翡翠!它的羽毛色彩斑斓,十分惊艳。这是一次令人难忘的经历,让我更加热爱观鸟。",
            "user": {
                "id": 11,
                "name": "森林探险家"
            }
        }
    ]
}

3. 启动 json-server

在包含 db.json 文件的目录下启动 json-server

json-server --watch db.json --port

默认情况下,它会在 http://localhost:3000 上运行

4. 自定义端口

如果你想在自定义端口上运行 json-server,可以使用 --port 选项。例如,运行在 5000 端口:

json-server --watch db.json --port 5000

项目

image.png

内容列表像掘金一样,点进去显示详细

image.png

用Nuxt能有很好SEO,利于网站被访问,浏览器能读取到数据,根据数据推荐给用户,手机端应用一般不会用Nuxt去写。

核心项目片段:

<template>
    <div>
        <h1>内容列表</h1>
        <div class="post-list" v-if="posts !== null">
            <div v-for="post in posts" :key="post.id">
                <div>
                    <div>
                        <NuxtLink :to="`/posts/${post.id}`">{{ post.title }}</NuxtLink>
                    </div>
                    <div>{{ post.content}}</div>
                    <div>
                        - <small>{{ post.user.name }}</small>
                    </div>
                </div>
            </div>
        </div>
    </div>
</template>

<script setup lang="ts">

const {
    data:posts,
    pending,
    refresh,
    error
} = await useApiFetch(()=> `posts`)

</script>

这里自定义了hooks函数,在useApiFetch中去写了useFetch()

<template>
    <div class="app">
        <header>
            <div>
                <span></span>
                <NuxtLink to="/">{{ name }}</NuxtLink>
            </div>
            <nav>
                <div>
                    <NuxtLink to="/about">关于</NuxtLink>
                </div>
                <div>
                    <NuxtLink to="/posts">内容</NuxtLink>
                </div>
                <div>
                    <NuxtLink to="/point">特点</NuxtLink>
                </div>
                <div>
                    <NuxtLink to="/culture">文化</NuxtLink>
                </div>
            </nav>
            <div>
                <div v-if="currentUser">
                    <NuxtLink to="/create">
                        <img src="/icons/add.svg" alt="添加内容">
                    </NuxtLink>
                </div>
                <div v-if="!currentUser">
                    <NuxtLink to="/login">
                        <img src="../static/icons/account.svg" alt="登录">
                    </NuxtLink>
                </div>
            </div>
        </header>
        <main>
            <slot></slot>
        </main>
        <footer class="footer">
            <div class="footer-container">
                <div class="footer-links">
                    <a href="#">联系我们</a>
                    <a href="#">隐私政策</a>
                    <a href="#">使用条款</a>
                </div>
                <div class="footer-copyright">
                    &copy; 2024 小鸟论坛 - 热爱自然,守护鸟类
                </div>
            </div>
        </footer>
    </div>
</template>

<script lang="ts" setup>
    const name = '小鸟论坛'
    const currentUser = false
    
</script>

<style>
@import '../assets/styles/default.css'
</style>

内置好的NuxtLink,其实和router-link一样的用法。

export const useApiFetch = (
    api : string | (() => string)
) => {
    return useFetch(api,{
        baseURL:'http://localhost:1234',
        onRequest:async(context) =>{
            
        }
    })
}

自定义的hooks函数,useApiFetch

body {
    margin: 0;
    padding: 0;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
  }
  
  main {
    font-size: 18px;
    line-height: 1.8;
  }
  
  h1 {
    font-size: 32px;
  }
  
  a {
    color: black;
    text-decoration: none;
  }
  
  button {
    border: none;
    outline: none;
    background: #ededed;
    padding: 6px 16px;
    font-size: 14px;
    border-radius: 5px;
    color: #666;
    cursor: pointer;
    border: 1px solid transparent;
    margin-bottom: 16px;
    margin-top: 8px;
  }
  
  button:hover {
    color: #000;
  }
  
  button ~ button {
    margin-left: 8px;
  }
  
  button.primary {
    background: #000;
    color: #ededed;
  }
  
  button.primary:hover {
    color: #fff;
  }
  
  button.bordered {
    background: none;
    border: 1px solid #eaeaea;
  }
  
  input,
  textarea {
    margin-bottom: 16px;
    margin-right: 16px;
  }
  
  pre {
    font-size: 16px;
    padding: 8px;
  }
  
  label {
    margin-right: 16px;
  }
  
  input[type='submit'] {
    border: none;
    outline: none;
    background: #ededed;
    padding: 6px 16px;
    font-size: 14px;
    border-radius: 5px;
    color: #666;
    cursor: pointer;
    border: 1px solid transparent;
  }
  
  input[type='submit']:active {
    color: #000;
  }
  
  input[type='text'],
  input[type='password'],
  textarea {
    min-width: 360px;
  }
  
  input[type='text'],
  input[type='password'],
  textarea,
  select {
    border: 1px solid #eaeaea;
    padding: 8px;
    border-radius: 5px;
    font-size: 14px;
  }
  
  input[type='checkbox'] {
    transform: scale(1.3);
  }
  
  /* App */
  .app {
    display: flex;
    flex-direction: column;
    min-height: 100vh;
  }
  
  /* Header */
  .app > header {
    display: flex;
    align-items: center;
    padding: 8px 16px;
    border-bottom: 1px solid #eaeaea;
    margin-bottom: 24px;
  }
  
  .app > header > div:nth-child(1) {
    display: flex;
    align-items: center;
    font-size: 22px;
    font-weight: bold;
    margin-right: 56px;
  }
  
  .app > header > div:nth-child(1) > span {
    font-size: 32px;
    padding-right: 8px;
  }
  
  .app > header > nav {
    display: flex;
    flex-grow: 1;
    color: #666;
  }
  
  .app > header > nav > div {
    margin: 0 16px;
  }
  
  .app > header > nav ~ div {
    display: flex;
  }
  
  .app > header > nav ~ div > div {
    margin-left: 8px;
  }
  
  /* Main */
  .app > main {
    flex-grow: 1;
    padding: 8px 16px;
    display: flex;
    flex-direction: column;
  }
  
  .post-list > div {
    display: flex;
    margin-bottom: 24px;
  }
  
  .post-list > div > div {
    margin-right: 16px;
  }
  
  .post-list > div > div > img {
    width: 64px;
    height: 64px;
    object-fit: cover;
    border-radius: 50%;
  }
  
  .post-list > div > div > div:nth-child(1) {
    font-weight: bold;
  }
  
  .post-list > div > div > div:nth-child(3) {
    font-size: 14px;
  }
  
  .post-list > div > div > div:last-child {
    display: flex;
    margin-top: 8px;
  }
  
  .post-list > div > div > div:last-child img {
    width: 20px;
  }
  
  .post-list > div > div > div:last-child > div {
    margin-right: 8px;
  }
  
  .router-link-active {
    font-weight: bold;
    color: black;
  }


  
  .footer {
    background-color: #2c3e50; /* 深色背景 */
    color: #ecf0f1; /* 浅色字体 */
    padding: 20px 0; /* 上下内边距 */
    text-align: center; /* 文字居中 */
  }
  
  .footer-container {
    max-width: 1200px; /* 限制内容最大宽度 */
    margin: 0 auto; /* 居中对齐 */
    display: flex;
    flex-direction: column;
    align-items: center; /* 垂直居中 */
  }
  
  .footer-links {
    margin-bottom: 10px;
  }
  
  .footer-links a {
    color: #ecf0f1; /* 链接颜色 */
    margin: 0 15px; /* 链接之间的间距 */
    text-decoration: none; /* 去除下划线 */
    font-size: 14px;
  }

全局样式,layouts->defaults.css

总结

当你学完Vue的生态,再学Nuxt的时候,会发现Nuxt会方便很多,很多东西都是直接用的,相关的好处也是显而易见的。服务端渲染(SSR)与静态生成支持,提升 SEO,文件系统路由,开发更高效,灵活的布局系统,统一设计风格,自动化的 SEO 优化。