如何使用 Nuxt.js 构建 Jamstack 多语言博客

2025-06-08

如何使用 Nuxt.js 构建 Jamstack 多语言博客

Jamstack(JavaScript、API 和标记堆栈)是一种围绕 Web 项目开发新方式的术语,它允许您无需每次构建网站时都自行托管后端,而是在构建时渲染一组静态页面,并将其部署到内容分发网络 (CDN)。这意味着更高的安全性、更高的可扩展性和更佳的网站性能。

在本教程中,您将学习如何使用Nuxt.js构建 Jamstack 多语言博客。Nuxt.js 是一个强大的 Vue 框架,支持 SPA、SSR 和静态渲染,并结合Strapi Headless CMS来存储和公开数据,从而生成静态博客。要本地设置 Strapi,您可以按照本指南操作,否则,您可以使用我们服务器https://strapi.lotrek.net/上运行的只读实例

👉🏻 您可以在此存储库中找到本教程的完整代码

后端结构

使用 Strapi,我构建了一个简单结构来支持翻译,其中包含一个Post表,其中包含与一个或多个TransPost包含翻译的元素链接的元素

       ____________                        ____________
      |    POST    |                      | TRANS_POST |
       ============                        ============
      | published  |                      | language   |
      | created_at | <--(1)-------(N)-->> | title      |
      |            |                      | content    |
      |            |                      | slug       |
       ============                        ============
Enter fullscreen mode Exit fullscreen mode

您可以使用GraphQL 游乐场进行体验并探索后端。请记住,本教程的重点是Nuxt.js,您可以使用任何您想要的后端来生成最终的静态网站。后端代码库可在此处获取

设置 Nuxt.js 项目

全局安装 Nuxt.js 并创建一个名为multilangblog

npx create-nuxt-app multilangblog
Enter fullscreen mode Exit fullscreen mode

记得选择axios选项(稍后您将需要它)并添加 UI 框架,例如Buefy

创建一个客户端来获取帖子

安装apollo-fetch客户端以从 Strapi 服务器获取帖子(我使用这个旧包来使客户端部分尽可能简单,请查看@nuxtjs/apollo以获得更结构化和更新的插件)

yarn add apollo-fetch
Enter fullscreen mode Exit fullscreen mode

并在文件夹下创建index.js文件services来包装所有查询。此客户端应实现以下三个方法:

  • getAllPostsHead:获取特定语言的所有帖子,显示slugtitle
  • getAllPosts:获取特定语言的所有帖子,显示slugtitlecontent其他语言的帖子 slug 以获取备用 url。
  • getSinglePost:获取具有特定 slug 和语言的单个帖子,显示所有属性和其他语言的帖子。
import { createApolloFetch } from 'apollo-fetch'

export default class BlogClient {
  constructor () {
    this.apolloFetch = createApolloFetch({ uri: `${process.env.NUXT_ENV_BACKEND_URL}/graphql` })
  }

  getAllPostsHead (lang) {
    const allPostsQuery = `
      query AllPosts($lang: String!) {
        transPosts(where: {lang: $lang}) {
          slug
          title
        }
      }
    `
    return this.apolloFetch({
      query: allPostsQuery,
      variables: {
        lang
      }
    })
  }

  getAllPosts (lang) {
    const allPostsQuery = `
      query AllPosts($lang: String!) {
        transPosts(where: {lang: $lang}) {
          slug
          title
          content
          post {
            published
            transPosts(where: {lang_ne: $lang}) {
              slug
              lang
            }
          }
        }
      }
    `
    return this.apolloFetch({
      query: allPostsQuery,
      variables: {
        lang
      }
    })
  }

  getSinglePost (slug, lang) {
    const simplePostQuery = `
      query Post($slug: String!, $lang: String!) {
        transPosts(where: {slug : $slug, lang: $lang}) {
          slug
          title
          content
          post {
            published
            transPosts(where: {lang_ne: $lang}) {
              slug
              lang
            }
          }
        }
      }
    `
    return this.apolloFetch({
      query: simplePostQuery,
      variables: {
        slug,
        lang
      }
    })
  }
}
Enter fullscreen mode Exit fullscreen mode

为了使BlogClient您可以随时访问上下文(例如在asyncData函数中)创建plugins/ctx-inject.js文件

import BlogClient from '~/services'

export default ({ app }, inject) => {
  app.$blogClient = new BlogClient()
}
Enter fullscreen mode Exit fullscreen mode

并将其添加pluginsnuxt.config.js

export default {
  // ...
  plugins: ['~/plugins/ctx-inject.js']
}
Enter fullscreen mode Exit fullscreen mode

创建主视图

这个博客的结构非常简单,在主页 ( /) 中会有一个文章列表,其中包含一个文章阅读链接 ( /blog/<postslug>)。现在您可以BlogClient从上下文中访问实例,开始重写HomePage组件 ( ),以便在名为asyncDatapages/index.vue的特殊方法中获取博客文章,并为每篇文章渲染标题和链接。接收上下文作为第一个参数,您的实例可以在asyncDataBlogClientcontext.app.$blogClient

<template>
  <section class="section">
    <div class="is-mobile">
      <div v-for="post in posts" :key="post.slug">
        <h2>{{ post.title }}</h2>
        <nuxt-link :to="{name: 'blog-slug', params:{slug: post.slug}}">Read more...</nuxt-link>
      </div>
    </div>
  </section>
</template>

<script>
export default {
  name: 'HomePage',
  async asyncData ({ app }) {
    const postsData = await app.$blogClient.getAllPostsHead('en')
    return { posts: postsData.data.transPosts }
  },
  data () {
    return {
      posts: []
    }
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

添加/blog/<postslug>创建组件的路由BlogPostpages/blog/_slug.vue)。安装Vue Markdown 组件以正确呈现文章(yarn add vue-markdown

<template>
  <section class="section">
    <div class="is-mobile">
      <h2>{{ post.title }}</h2>
      <vue-markdown>{{ post.content }}</vue-markdown>
    </div>
  </section>
</template>

<script>
export default {
  name: 'BlogPost',
  components: {
    'vue-markdown': VueMarkdown
  },
  async asyncData ({ app, route }) {
    const postsData = await app.$blogClient.getSinglePost(route.params.slug, 'en')
    return { post: postsData.data.transPosts[0] }
  },
  data () {
    return {
      post: null
    }
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

添加 i18n

要设置 i18n,请安装Nuxt i18n 模块

yarn add nuxt-i18n
Enter fullscreen mode Exit fullscreen mode

在文件module部分启用它nuxt.config.js

{
  modules: ['nuxt-i18n']
}
Enter fullscreen mode Exit fullscreen mode

并设置 i18n

const LOCALES = [
  {
    code: 'en',
    iso: 'en-US'
  },
  {
    code: 'es',
    iso: 'es-ES'
  },
  {
    code: 'it',
    iso: 'it-IT'
  }
]
const DEFAULT_LOCALE = 'en'

export default {
  // ...
  i18n: {
    locales: LOCALES,
    defaultLocale: DEFAULT_LOCALE,
    encodePaths: false,
    vueI18n: {
      fallbackLocale: DEFAULT_LOCALE,
      messages: {
        en: {
          readmore: 'Read more'
        },
        es: {
          readmore: 'Lee mas'
        },
        it: {
          readmore: 'Leggi di più'
        }
      }
    }
  }
  // ...
}
Enter fullscreen mode Exit fullscreen mode

现在您可以修改HomePage组件:nuxt-link您应该使用并渲染localePath翻译后的标签readmore$t

<nuxt-link :to="localePath({name: 'blog-slug', params:{slug: post.slug}})">{{ $t('readmore') }}</nuxt-link>
Enter fullscreen mode Exit fullscreen mode

您可以使用属性asyncData获取帖子列表以获取当前语言。store.$i18ncontext

// ....
async asyncData ({ app, store }) {
  const postsData = await app.$blogClient.getAllPostsHead(
    store.$i18n.locale
  )
  return { posts: postsData.data.transPosts }
},
// ....
Enter fullscreen mode Exit fullscreen mode

BlogPost在组件中执行相同操作route.params.slug以获取 slug 参数

// ....
async asyncData ({ app, route, store }) {
  const postsData = await app.$blogClient.getSinglePost(
    route.params.slug, store.$i18n.locale
  )
  return { post: postsData.data.transPosts[0] }
},
// ....
Enter fullscreen mode Exit fullscreen mode

现在是时候创建一个组件来切换当前语言了,LanguageSwitchercomponents/LanguageSwitcher.vue

<template>
  <b-navbar-dropdown :label="$i18n.locale">
    <nuxt-link v-for="locale in availableLocales" :key="locale.code" class="navbar-item" :to="switchLocalePath(locale.code)">
      {{ locale.code }}
    </nuxt-link>
  </b-navbar-dropdown>
</template>

<script>
export default {
  computed: {
    availableLocales () {
      return this.$i18n.locales.filter(locale => locale.code !== this.$i18n.locale)
    }
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

并将其添加到layouts/default.vue导航栏中。此组件调用switchLocalePath以获取指向其他语言当前页面的链接。要使语言切换器与动态路由配合使用,您需要使用store.dispatchslugBlogPost组件中设置参数

//...
async asyncData ({ app, route, store }) {
  const postsData = await app.$blogClient.getSinglePost(
    route.params.slug, store.$i18n.locale
  )
  await store.dispatch(
    'i18n/setRouteParams',
    Object.fromEntries(postsData.data.transPosts[0].post.transPosts.map(
      el => [el.lang, { slug: el.slug }])
    )
  )
  return { post: postsData.data.transPosts[0] }
},
//...
Enter fullscreen mode Exit fullscreen mode

👉🏻更多关于语言切换器的信息

NUXT_ENV_BACKEND_URL记得使用BlogClient.env 或直接( )设置环境变量export NUXT_ENV_BACKEND_URL=https://strapi.lotrek.net并启动开发服务器

yarn dev
Enter fullscreen mode Exit fullscreen mode

完全静态生成

👉🏻 请注意,本文是我使用 Nuxt.js 2.12.0 编写的,之后我将核心升级到 2.13.0 以使用完整静态生成,请务必运行最新版本。更多信息,请阅读Nuxt.js 官方博客中的“Going Full Static”并关注代码库中的更新

要使用 Nuxt.js 生成此博客的完整静态版本,请添加target: 'static'nuxt.config.js运行

nuxt build && nuxt export
Enter fullscreen mode Exit fullscreen mode

(您可以将其包装nuxt export在脚本部分中package.json

dist最终输出是文件夹内生成的路线列表

ℹ Generating output directory: dist/                                                                                       
ℹ Full static mode activated                                                                                               
ℹ Generating pages
✔ Generated /it/
✔ Generated /es/
✔ Generated /
ℹ Ready to run nuxt serve or deploy dist/ directory
✨  Done in 43.49s.
Enter fullscreen mode Exit fullscreen mode

👉🏻 从 2.13.0 版本开始,Nuxt.js 使用 来crawler检测每个相对链接并生成它。您可以禁用爬虫设置generate.crawler: false,但仍然可以自行添加动态路由,以提高性能(如本例所示),或者添加爬虫无法检测到的额外路由。

要手动添加动态路线,您必须在设置routes下实现功能并返回包含您要生成的和包含帖子的对象列表。generatenuxt.config.jsroutepayload

import BlogClient from './services'

// ...

export default {
  // ...
  crawler: false,
  generate: {
    routes: async () => {
      const client = new BlogClient()
      let routes = []
      let postsData = []
      for (const locale of LOCALES) {
        postsData = await client.getAllPosts(locale.code)
        routes = routes.concat(postsData.data.transPosts.map((post) => {
          return {
            route: `${locale.code === DEFAULT_LOCALE ? '' : '/' + locale.code}/blog/${post.slug}`,
            payload: post
          }
        }))
      }
      return routes
    }
  }
  //...
}
Enter fullscreen mode Exit fullscreen mode

由于payload在中可用context,您可以重构BlogPost组件中的 asyncData 函数以从中获取特定帖子context.payload

const getSinglePostFromContext = async ({ app, route, store, payload }) => {
  if (payload) {
    return payload
  }
  const postsData = await app.$blogClient.getSinglePost(
    route.params.slug, store.$i18n.locale
  )
  return postsData.data.transPosts[0]
}

export default {
  name: 'BlogPost',
  async asyncData (context) {
    const singlePost = await getSinglePostFromContext(context)
    await context.store.dispatch(
      'i18n/setRouteParams',
      Object.fromEntries(singlePost.post.transPosts.map(
        el => [el.lang, { slug: el.slug }])
      )
    )
    return { post: singlePost }
  },
  // ...
}
Enter fullscreen mode Exit fullscreen mode

nuxt build && nuxt export再次运行

ℹ Generating pages
✔ Generated /it/
✔ Generated /es/
✔ Generated /
✔ Generated /blog/hello-world
✔ Generated /it/blog/ciao-mondo
✔ Generated /es/blog/hola-mundo
ℹ Ready to run nuxt serve or deploy dist/ directory
✨  Done in 33.82s.
Enter fullscreen mode Exit fullscreen mode

现在 Nuxt.js 能够生成动态路由🎉

您可以使用以下方式测试静态站点的安装

nuxt serve
Enter fullscreen mode Exit fullscreen mode

有时您可能需要为动态路由配置自定义路径,例如,您可能希望保留/blog/:slug英语路径、/artículos/:slug西班牙语路由和/articoli/:slug意大利语路由。按照 nuxt-i18n 文档,您需要i18nnuxt.config.js

i18n {
  // ...
  parsePages: false,
  pages: {
    'blog/_slug': {
      it: '/articoli/:slug',
      es: '/artículos/:slug',
      en: '/blog/:slug'
    }
  },
  // ...
}
Enter fullscreen mode Exit fullscreen mode

为了使这些设置在i18n配置和generate功能上可重复使用,请将自定义路由移动到单独的文件中i18n.config.js

export default {
  pages: {
    'blog/_slug': {
      it: '/articoli/:slug',
      es: '/artículos/:slug',
      en: '/blog/:slug'
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

并将其导入nuxt.config.js

import i18nConfig from './i18n.config'

// ...

export default {
  // ...
  i18n: {
    locales: LOCALES,
    defaultLocale: DEFAULT_LOCALE,
    parsePages: false,
    pages: i18nConfig.pages,
    encodePaths: false,
    vueI18n: {
      fallbackLocale: DEFAULT_LOCALE,
      // ...
    }
  },
  // ...
Enter fullscreen mode Exit fullscreen mode

现在您可以重写generate函数从自定义配置中获取正确的路径

routes: async () => {
  const client = new BlogClient()
  let routes = []
  let postsData = []
  for (const locale of LOCALES) {
    postsData = await client.getAllPosts(locale.code)
    routes = routes.concat(postsData.data.transPosts.map((post) => {
      return {
        route: `${locale.code === DEFAULT_LOCALE ? '' : '/' + locale.code}${i18nConfig.pages['blog/_slug'][locale.code].replace(':slug', post.slug)}`,
        payload: post
      }
    }))
  }
  return routes
}
Enter fullscreen mode Exit fullscreen mode

再次构建并导出所有内容,您将获得

ℹ Generating pages
✔ Generated /blog/hello-world
✔ Generated /it/articoli/ciao-mondo
✔ Generated /es/artículos/hola-mundo
✔ Generated /es/
✔ Generated /it/
✔ Generated /
ℹ Ready to run nuxt serve or deploy dist/ directory
✨  Done in 33.82s.
Enter fullscreen mode Exit fullscreen mode

您的具有自定义路径的完整静态生成博客已准备就绪🎉

你可以做更多

这个仓库中,你可以看到本教程的完整代码,最终部署在Netlify CDN 的https://eager-shockley-a415b7.netlify.app/。Netlify 是我最喜欢的服务之一,它为静态网站提供云托管,提供持续部署、免费 SSL、无服务器功能等等……最终代码为网站添加了一些缺失的功能,例如,它增加了作者支持,使用了一些此处为简单起见省略的外部组件,并为项目启用了 SEO 选项,以便向页面添加元数据(请参阅nuxt-18n 文档中的 SEO 部分)。

最终代码中包含的另一个有用的东西是站点地图,它由Nuxt.js Sitemap 模块提供。站点地图的设置非常简单,因为它默认使用值,因此动态路由将自动包含在内。配置非常简单,只需在文件数组部分的末尾generate.routes添加即可。@nuxtjs/sitemapmodulesnuxt.config.js

  {
    modules: [
      // ...
      '@nuxtjs/sitemap'
    ],
  }
Enter fullscreen mode Exit fullscreen mode

并配置sitemap部分

export default {
  // ...
  sitemap: {
    hostname: BASE_URL,
    gzip: true,
    i18n: DEFAULT_LOCALE
  }
  // ...
}
Enter fullscreen mode Exit fullscreen mode

查看Github 上的 Nuxt 社区组织,了解更多精彩模块和项目!

祝你编码愉快!💚


封面图片由Marco Verch (CC BY 2.0)提供

鏂囩珷鏉ユ簮锛�https://dev.to/astagi/how-to-build-a-jamstack-multi-language-blog-with-nuxt-js-3gah
PREV
25 个开发者必去的杀手级网站
NEXT
React.memo(明智地使用我)