后端通过 strapi
一波操作下来就搞定了,接下来就进入到 nuxt3
前端部分的开发
创建项目并迁移布局
按文档创建项目
npx nuxi@latest init blog-by-nuxt
cd blog-by-nuxt
yarn install
创建项目后,根据文档,我们开始将布局迁移至nuxt3
<!-- app.ts -->
<template>
<NuxtLayout> </NuxtLayout>
</template>
创建布局文件layouts/default.vue
,components文件夹中的组件将会按规则自动注册。
<!-- layouts/default.vue -->
<template>
<div>
<!-- components/layout/Header.vue -->
<LayoutHeader />
<div class="container-mask"></div>
<ClientOnly>
<!-- components/layout/Minecreaft.vue -->
<LayoutMinecreaft />
</ClientOnly>
<div class="container-wrap">
<main class="main-container">
<NuxtPage />
</main>
</div>
<!-- components/layout/Footer.vue -->
<LayoutFooter />
<!-- components/BackTop.vue -->
<BackTop />
</div>
</template>
<script lang="ts" setup>
</script>
引入stylus相关依赖,并迁移样式后
yarn add stylus stylus-loader -D
# 引入自己写的stylus-shortcut快捷样式
yarn add stylus-shortcut
在配置中全局引用,并配置部分meta
标签
export default defineNuxtConfig({
// ...
app: {
head: {
title: "超能郭工",
htmlAttrs: {
lang: 'zh-CN'
},
meta: [
{ name: "viewport", content: "width=device-width,initial-scale=1" }
],
link: [
{ rel: "apple-touch-icon", sizes: "180x180", href: "/apple-touch-icon.png" },
{ rel: "icon", type: "image/png", sizes: "32x32", href: "/favicon-32x32.png" },
{ rel: "icon", type: "image/png", sizes: "16x16", href: "/favicon-16x16.png" },
{ rel: "manifest", href: "/site.webmanifest" },
{ rel: "preconnect", href: "www.kwokronny.com" },
{ rel: "preconnect", href: "cdn.bootcdn.net" },
{ rel: "dns-prefetch", href: "www.kwokronny.com" },
{ rel: "dns-prefetch", href: "cdn.bootcdn.net" },
],
script: [
{ async: true, src: "//hm.baidu.com/hm.js?1762b014e1a457fdc04e1ea7bc1026fa" }
]
}
},
css: ["~/styles/index.styl"],
// ...
})
以上布局的迁移工作就基本完成了。
对接后端
在 nuxt3
的Modules
列表找到了@nuxtjs/strapi模块
,已帮我们写好了基础的接口实例,包括 ts 的声明,非常好用。
安装文档 · Nuxt Strapi (nuxtjs.org)
yarn add -D @nuxtjs/strapi
export default defineNuxtConfig({
// ...
modules: [
'@nuxtjs/strapi',
],
strapi: {
url: 'http://api.example.com',
prefix: '/api',
version: 'v4',
}
// ...
})
完成以上配置后,我们登录到 strapi
内容管理平台创建内容类型。
创建 Tag标签、Category分类、Post文章 三个内容类型
-
Post文章与Tag标签创建
has and belongs to many
关系 -
Post文章与Category分类创建
has many
关系
‼️重要,完成内容类型的创建后,还需要为这个内容放开公共的权限,否则也将无法请求到数据
创建完成后可增加一些测试数据后就可以开始前后端数据的对接了。
// types/index.d.ts
// @nuxtjs/strapi 缺失Media的声明
declare interface StrapiMedia {
ext: string;
formats: { thumbnail: StrapiMedia };
hash: string;
height: number;
mime: string;
name: string;
previewUrl?: string;
provider?: string;
size: number;
url: string;
width: number
}
declare interface DataType {
createdAt: string;
updatedAt: string;
}
// 内容类型开启了 Draft&publish 功能将会多一个 publishedAt 属性
declare interface PublishedDataType extends DataType {
publishedAt: string;
}
// SEO组件的数据结构,后续再细说
declare interface SEO {
ogTitle: string;
ogDescription: string;
ogImage: Strapi4ResponseSingle<StrapiMedia>;
description: string;
}
// 文章数据结构声明
declare interface Post extends PublishedDataType {
title: string;
cover: Strapi4ResponseSingle<StrapiMedia>;
content: string;
category: Strapi4ResponseSingle<Category>;
tags: Strapi4ResponseMany<Tag>
seo: SEO;
top: number;
}
// 标签数据结构声明
declare interface Tag extends DataType {
name: string;
}
// 分类数据结构声明
declare interface Category extends PublishedDataType {
name: string;
}
创建composables/strapi.ts
文件,给所有接口写为组合式API。
import { Strapi4RequestParams, Strapi4ResponseMany } from "@nuxtjs/strapi/dist/runtime/types"
import { Post, Category, Tag } from "~/types"
export const usePosts = async (params: Strapi4RequestParams = {}): Promise<Strapi4ResponseMany<Post>> => {
const { find } = useStrapi<Post>()
return await find("posts", Object.assign({
populate: ["cover", "category"],
}, params))
}
export const usePost = async (id: number, preview: boolean = false) => {
const { findOne } = useStrapi<Post>()
return await findOne("posts", id, {
populate: ["cover", "category", "tags", "seo", "seo.ogImage"],
publicationState: preview ? 'preview' : 'live' //预览未发布的文章
})
}
export const useCategories = async (id: number) => {
const { findOne } = useStrapi<Category>()
let categories = await find("categories", {
populate: "pId"
})
return categories
}
export const useTags = async () => {
const { find } = useStrapi<Tag>()
return await find("tags")
}
创建components/PostList.vue
文件实现文章展示页
<!-- components/PostList.vue -->
<template>
<div class="post-list row gutter-s spac-mh_gs">
<div v-for="(item, idx) in posts" :key="idx" class="post-item" :data-idx="idx"
:class="props.postItemClass || `col-d-12 col-dm-8 col-6`">
<NuxtLink :to="`/post/${item.id}`" :aria-label="item.attributes.title">
<AspectRatio ratio="4/3">
<img class="cover" style="object-fit: cover;" :src="mediaUrl(item.attributes.cover.data?.attributes.url)"
:alt="`封面Cover: ${item.attributes.title}`" />
</AspectRatio>
</NuxtLink>
<div class="info">
<NuxtLink :to="`/post/${item.id}/${item.attributes.title}`">
<strong class="title">
{{ item.attributes.title }}
</strong>
</NuxtLink>
<div class="mark">
<span>
<Icon name="material-symbols:tag-rounded"></Icon>
{{ item.attributes.category.data?.attributes.name || "未分类" }}
</span>
<span>
<Icon class="spac-mr_n" name="material-symbols:calendar-month-sharp"></Icon>
{{ dateUtil.now(item.attributes.createdAt) }}
</span>
</div>
</div>
</div>
</div>
<!-- 数据为空提示 -->
<InnerTip v-if="!posts.length && !loading" type="empty"></InnerTip>
<div v-if="!props.top">
<!-- 用于滚动加载的观察对象 -->
<div class="load-observer" ref="LoadObserver"></div>
<!-- 全部加载完成提示 -->
<InnerTip v-if="loaded && posts.length > 0" type="loaded"></InnerTip>
<!-- 加载中提示 -->
<div class="text-a_c spac-mt_s5" v-if="loading">
<Icon name="svg-spinners:pulse-rings-multiple" size="50px"></Icon>
</div>
</div>
</template>
<script lang="ts" setup>
import { Strapi4RequestParams, Strapi4ResponseData } from '@nuxtjs/strapi/dist/runtime/types';
import { Post } from '~/types';
interface IProp {
params?: Strapi4RequestParams // 自定义post请求参数
top?: boolean,
postItemClass?: string // 自定义postItem样式
}
const props = defineProps<IProp>()
const posts = ref<Strapi4ResponseData<Post>[]>([]);
const loading = ref(false);
const loaded = ref(false);
const page = ref(1);
const LoadObserver = ref(null);
useAsyncData(async () => {
await loadNextPage();
})
onMounted(() => {
if (LoadObserver.value) {
// 观察者实现滚动加载
const observer = new IntersectionObserver((entries) => !loaded.value && !loading.value && entries[0].isIntersecting && loadNextPage(), {
rootMargin: '0px',
threshold: 1.0
})
observer.observe(LoadObserver.value)
}
})
async function loadNextPage() {
if (loading.value || loaded.value) return;
loading.value = true;
let response = await usePosts(Object.assign(props.top ? {
sort: ["top:desc", "createdAt:desc"],
pagination: {
limit: 4,
start: 0
}
} : {
sort: ["top:desc", "createdAt:desc"],
pagination: {
page: page.value,
pageSize: 12,
}
}, props.params) || {})
loading.value = false;
if (!response.data.length) {
loaded.value = true;
return false;
}
posts.value = posts.value.concat(response.data)
page.value += 1;
}
</script>
创建pages/index.vue
实现首页
<template>
<div class="index-page">
<h2>最新文章 Latest Articles</h2>
<div class="spac-mb_s2">
<NuxtLink class="link-btn spac-mr_s1 spac-mb_s1" :to="`/category/${item.id}`" v-for="item in categories?.data"
:key="item.id">
<Icon name="material-symbols:tag-rounded" size="14px"></Icon>{{ item.attributes.name }}
</NuxtLink>
</div>
<PostList />
</div>
</template>
<script lang="ts" setup>
const categories = await useCategories()
</script>
以上就基本完成了关于项目的配置与布局的迁移啦,搞完收工