Vue3+Nuxt3 从0到1搭建官网项目
想开发一个官网,并且支持SEO搜索,当然离不开我们的 Nuxt ,Nuxt2 我们刚刚可以熟练运用,现在有出现了Nuxt3,那通过本篇文章让我们一起了解一下。
安装 Nuxt3,创建项目
安装nuxt3, 需要node v18.10.0以上,大家记得查看自己的node版本。
升级node,可以参考使用nvm 切换不同node版本~
// node v18.10.0
// npx nuxi@latest init <project-name>
npx nuxi@latest init nuxt3-demo
cd nuxt3-demo
初始化的 package.json
这是项目刚创建后的package.json文件
{
"name": "nuxt3-demo",
"private": true,
"type": "module",
"scripts": {
"dev": "nuxt dev",
"build": "nuxt build",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"dependencies": {
"nuxt": "3.8.2",
"vue": "3.3.10",
"vue-router": "4.2.5"
}
}
项目结构
├── app.vue // 主文件
├── assets // 静态资源
├── components // 公共vue组件
├── composables // 将你的Vue组合式函数自动导入到你的应用程序中
├── error.vue // 路由匹配不到时
├── i18n.config.ts // 语言切换配置文件
├── lang // 语言JSON
├── nuxt.config.ts // nuxt 配置文件
├── package.json
├── pages // pages文件夹下面的页面名,默认为 路由地址
├── plugins // 公共插件
├── public // 提供网站的静态资源
├── server
├── tsconfig.json
└── yarn.lock // 包含了应用程序的所有依赖项和脚本
初始化项目
我们首先创建一个首页,将项目运行起来,这样一会儿讲到SEO、语音切换时,方便查看效果
pages 文件下创建index.vue
<template>
<div class="home-wrap">
Nuxt3--这是我们的首页
</div>
</template>
<script>
export default {
}
</script>
<style lang="scss" scoped>
.home-wrap{
height: 500px;
display: flex;
justify-content: center;
align-items: center;
font-size: 50px;
}
</style>
我们发现页面出现如下错误,提示我们需要引入 sass
)
引入sass
// 如果下载失败。记得比对package.json 中依赖的版本号
// node_modules 和 yarn.lock 记得也删除一下
yarn add string-width@7.1.0 sass@1.69.5 sass-loader@13.3.2 --save
修改 app.vue 文件
<template>
<div>
<NuxtPage />
</div>
</template>
查看效果
配置公共的css、meta
在我们的项目中,UI风格肯定是有规范(统一)的,因此我们可以将 css 重置文件、公共的css文件,以及Meta提前引入
assets下的css
在assets 文件夹下,我们可以创建css(样式)、img(图片)、fonts(字体库)等文件夹
reset.scss 重置文件
因为UI主体区域为1200px,因此body 我们设置了最大宽度是1350px,这样主体区域两侧有一点占位空间,使内容不至于紧贴设备边界
/* CSS reset */
// /assets/css/reset.scss
html,body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,form,fieldset,input,textarea,p,blockquote,th,td {
margin:0;
padding:0;
}
body{
font-family:"思源黑体","Microsoft YaHei","微软雅黑",Arial,sans-serif;
font-size: 14px;
color: #333;
min-width: 1350px;
}
*{
-webkit-text-size-adjust: none;
}
*,*:after,*:before{
box-sizing:border-box;
margin: 0;
padding:0;
}
table {
border-collapse:separate;
border-spacing:0;
}
fieldset,img {
border:0;
}
img{
display: block;
-webkit-user-drag: none;
}
img[src=""],img:not([src]){
opacity: 0;
border:none;
visibility: hidden;
max-width: none;
}
address,caption,cite,code,dfn,th,var {
font-style:normal;
font-weight:normal;
}
ol,ul ,li{
list-style:none;
}
caption,th {
text-align:left;
}
h1,h2,h3,h4,h5,h6 {
font-size:100%;
font-weight:normal;
}
abbr,acronym { border:0;
}
a{
text-decoration:none;
}
/* 解决兼容而加的样式 */
a, img {
-webkit-touch-callout: none; /*禁止长按链接与图片弹出菜单*/}
a,button,input{-webkit-tap-highlight-color:rgba(255,0,0,0);}
img{
display: block;
}
button,input,optgroup,select,textarea {
outline:none;
/*-webkit-appearance:none; /*去掉webkit默认的表单样式*/}
a,button,input,optgroup,select,textarea {
-webkit-tap-highlight-color:rgba(0,0,0,0); /*去掉a、input和button点击时的蓝色外边框和灰色半透明背景*/
}
input:-ms-input-placeholder{color:#b3b7c0;}
input::-webkit-input-placeholder{color:#b3b7c0;}
input:-moz-placeholder{color:#b3b7c0;}
input::-moz-placeholder{color:#b3b7c0;}
common.scss
/* CSS common */
// 这里大家可以写一些公共的css样式,我这里主要是举例
// /assets/css/common.scss
.g-border{position: relative;}
.g-border1{position: relative;}
.g-border:after{content:'';position: absolute;bottom:0;width:100%;height:1px;background:#e8e8e8;overflow: hidden;left:0;transform:translate(0%,0) scale(1,0.5);}
.g-border1::before{content:'';position: absolute;top:0;width:100%;height:1px;background:#e8e8e8;overflow: hidden;left:0;transform:translate(0%,0) scale(1,0.5);}
.g-border-on::before,.g-border-on:after{
background: #dcdcdc!important;
}
.g-text-ove2{display: -webkit-box;-webkit-box-orient: vertical; -webkit-line-clamp: 2;overflow: hidden;}
.g-text-ove1{overflow: hidden;text-overflow:ellipsis;white-space: nowrap;}
配置nuxt.config.ts
// 熟悉我的小伙伴可能注意到,我非常喜欢在项目中使用 address,哈哈哈
yarn add address@2.0.1 --save
// https://nuxt.com/docs/api/configuration/nuxt-config
const address = require('address')
const localhost = address.ip() || 'localhost'
export default defineNuxtConfig({
ssr:true,
app:{
head: {
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'viewport', name: 'viewport', content:"width=1350, user-scalable=no,viewport-fit=cover"},
{ hid: 'description', name: 'description', content: 'CSDN 作者:拿回忆下酒,介绍Vue3+Nuxt3 从0到1搭建官网项目(SEO搜索、中英文切换、图片懒加载)的dome' },
{ hid: 'keywords', name: 'keywords', content: 'Vue3,Nuxt3,CSDN 拿回忆下酒' },
{ name: 'format-detection', content: 'telephone=no' }
],
link:[
{
rel:'icon',
type:'image/x-icon',
href:'/favicon.ico'
}
]
},
},
css: [
'@/assets/css/reset.scss',
// 公共class
'@/assets/css/common.scss'
],
devtools: {
enabled: true,
ssr:false
},
devServer:{
host: localhost,
port:8303
}
})
现在的package.json
防止小伙伴下错版本号,咱们确定一下 现在的依赖包的版本号
大家也可以复制一下内容,将node_modules 和 yarn.lock 删除,重新执行 yarn install
{
"name": "nuxt3-demo",
"private": true,
"type": "module",
"scripts": {
"dev": "nuxt dev --open",
"build": "nuxt build",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"dependencies": {
"@nuxt/devtools": "latest",
"@nuxtjs/i18n": "8.0.0",
"address": "2.0.1",
"nuxt": "3.8.2",
"sass": "1.69.5",
"sass-loader": "13.3.2",
"string-width": "7.1.0",
"vue": "3.3.10",
"vue-router": "4.2.5"
}
}
查看效果
我们可以看到 IP、端口、meta、css 已经都改变了
创建新页面
创建新页面,演示目录结构、路由跳转、多页面TDK配置等等
pages目录结构
我们在pages文件夹下,新建 首页(index)、关于我们(about)、订单(product/order)、管家(product/steward)等页面,用来演示 目录结构 和 路由 的 对应关系
// pages 目录结构
├── nuxt.config.ts
├── package.json
├── pages
│ ├── abouut.vue
│ ├── index.vue
│ └── product
│ ├── order.vue
│ └── steward.vue
components 创建组件
components/ 目录是你放置所有 Vue 组件的地方。我们可以把公共组件放到这里
Header 组件
<template>
<section class="c-head-wrap" >
<div class="content-box">
<div class="head-box">
<section class="main g-width-box g-cen-y">
<div class="logo-box" >
演示logo
<!-- <img class="img" src="@/assets/img/logo-top.png" /> -->
</div>
<div
class="nav-btn-box "
>
<div class="g-dis">
<div
v-for="(m, i) in navArr"
:key="i"
class="btn"
ref="btnId"
@click="routerFn(m)"
:class="[isPageFn(m,i)]"
>
<h6>{{ m.name }}</h6>
<div v-show="m.children" class="children-box">
<div
class="item"
v-for="(n,ind) in m.children"
:key="i+'-'+ind"
@click.stop="routerFn(n)"
>
{{ n.name }}
</div>
</div>
</div>
</div>
<div class="border" :style="isStyle"></div>
</div>
</section>
</div>
</div>
</section>
</template>
<script setup>
import { ref } from "vue";
const router = useRouter()
const route = useRoute()
const navArr = ref([
{ path: "/",name:'首页' },
{
path: "/product",
name:'产品业务',
children:[
{ path: "/product/order", name:'订单'},
{ path: "/product/steward", name:'管家'}
]
},
{ path: "/about", name:'关于我们' }
]);
let btnId = ref(null)
let isStyle = ref('')
const routerFn = (e) => {
if(e.children) return
router.push(e.path)
};
const isPageFn =(e,ind)=>{
let async = false
if(route.path == e.path){
async = true
} else if(e.children){
async = !!e.children.filter(m => m.path == route.path).length
}
return async ? 'on':''
}
</script>
<style lang="scss" scoped>
.c-head-wrap{
min-width: 1350px;
position: absolute;
left: 0;
width: 100%;
z-index: 22;
}
.content-box{
width: 100%;
height: 70px;
background: transparent;
border-bottom: 1px solid #E0E4E8;
}
.main{
height: 70px;
}
.logo-box{
height: 100%;
display: flex;
align-items: center;
margin-right: 102px;
font-size: 20px;
font-weight: 500;
}
.nav-btn-box{
flex: 1;
position: relative;
.border{
width: 72px;
height: 2px;
background: #0044FF;
position: absolute;
bottom: 0;
transition: left 0.28s;
opacity: 0;
}
.btn{
font-weight: 500;
font-size: 18px;
color: #4F587F;
line-height: 70px;
padding: 0 18px;
cursor: pointer;
position: relative;
font-weight: 400;
z-index: 2;
&.on{
color: #0044FF;
position: relative;
}
&:hover{
.children-box{
display: block;
}
}
}
}
.children-box{
display: none;
position: absolute;
background-image: linear-gradient(197deg, rgba(246,246,246,1) , rgba(246,246,246,.65));
backdrop-filter: blur(5px);
box-shadow: 0 20px 40px 0 rgba(74,91,130,.16);
border: 2px solid #fff;
min-width: 100%;
left: 50%;
top: 70px;
line-height: 50px;
text-align: center;
white-space: nowrap;
transform: translate(-50%,0);
font-size: 16px;
font-weight: 400;
color: #4F587F;
z-index: 2;
.item{
border-bottom: 1px solid #E0E4E8;
padding: 0 14px;
&:last-child{
border-bottom: 0;
}
&.on,&:hover{
color: #0044FF;
}
}
}
</style>
Footer 组件
<template>
<div class="footer-box" >
<div class="box g-width-box">
<div class="left">
<div class="icon">演示logo</div>
<div class="phone">咨询电话:+86-136xxxx8899</div>
<div class="btn-box g-cen-y">
<span>版权 © 演示xxxx科技有限公司</span>
<i></i>
<a href="https://beian.miit.gov.cn/" target="_blank" class="btn">京ICP备2010xxxx88号</a>
<i></i>
<a href="https://beian.mps.gov.cn/#/query/webSearch?code=1010xxxxx3434" target="_blank" class="btn sprite-btn">
<img class="sprite" src="assets/img/sprite.png"/>
<span>京公网安备1010xxxxx3434号</span>
</a>
</div>
</div>
<div class="right">
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.footer-box{
height: 200px;
background: #242933;
.box{
display: flex;
justify-content:space-between;
padding-top: 40px;
.left{
font-size: 14px;
.icon{
height: 32px;
display: block;
margin-bottom: 40px;
color:#fff;
font-size: 30px;
}
.phone{
line-height: 22px;
color: #FFFFFF;
}
.btn-box{
line-height: 24px;
color:#878FB4 ;
&>i{
height: 14px;
width: 2px;
background: #878FB4 ;
margin: 0 10px;
}
.btn{
color:#878FB4 ;
font-size: 14px;
cursor: pointer;
&:hover{
color: #fff;
}
}
.sprite-btn{
display: flex;
align-items: center;
}
.sprite{
width: 16px;
height: 17px;
margin-right: 5px;
}
}
}
.right{
display: flex;
flex-direction:column;
align-items: center;
.qrcode1{
width: 100px;
height: 100px;
}
.txt{
line-height: 32px;
font-size: 12rpx;
color: #E9EDFF;
text-align: center;
}
}
}
}
</style>
引入组件
官网一般header(头部)、footer(底部)都是复用的,因此我们在app.vue文件中引用它们
// app.vue
<template>
<div>
<Header />
<NuxtPage />
<Footer />
</div>
</template>
组件效果
多语音系统
现在好多公司的官网都是多语言,因此我们一起了解一下 i18n
引入i18n
yarn add @nuxtjs/i18n@8.0.0 --save
新建lang文件夹
这是一个自创建的文件夹,用来存放 语言切换 需要用到的数据,我们来了解一下它的目录结构
├── nuxt.config.ts
├── i18n.config.ts
├── lang
│ ├── cn
│ │ ├── about.js
│ │ ├── home.js
│ │ ├── index.js // 入口文件
│ │ ├── order.js
│ │ └── steward.js
│ └── en
│ ├── about.js
│ ├── home.js
│ ├── index.js // 入口文件
│ ├── order.js
│ └── steward.js
入口文件(index.js )
index.js 的代码是一致的,主要作用是 将同一种语言下的文件合并到一起, 我以其中一个为例
// 获取同一目录下 的 js 文件
const files = import.meta.globEager('./*.js');
const modules = Object.keys(files).reduce((prev, cur) => {
// 获取js 文件的 文件名 并转 大写 (这里很重要!!!!)
// 这里 大写的文件名,是日后 dom 中日后替换的关键字
let key = cur.split('/')[1].split('.')[0].toUpperCase()
// 将内容合并在一起,为了方便理解,下面有合并后的JSON图
return {...prev,...{[key]:files[cur]?.default}}
}, {});
export default {...modules};
中英js文件 对比
每个js文件,JSON结构都是一致的,我们以home、about 为例对比一下
// cn/home.js
export default {
'b1': {
'title': '我们的首页',
'txt': '这是一段尝试,json结构可以任意定义,只要保持统一规范即可'
}
}
// en/home.js
export default {
'b1': {
'title': 'Our homepage',
'txt': 'China\'s leading fintech platform for automobiles empowering supply chain ecosystem partners. '
}
}
***********************
// cn/about.js
export default {
'b1': {
'title': '关于我们'
}
}
// en/about.js
export default {
'b1': {
'title': 'About Us'
}
}
合并后的JSON图
i18n.config.ts
import en from './lang/en/index.js'
import cn from './lang/cn/index.js'
export default defineI18nConfig(() => ({
legacy: false, // 是否兼容之前
fallbackLocale: 'cn', // 区配不到的语言就用en
messages: {
en,
cn
}
}))
nuxt.config.ts
export default defineNuxtConfig({
...
modules:[
'@nuxtjs/i18n'
],
i18n: {
strategy: 'prefix_and_default', // 添加路由前缀的方式
locales: ["en", "cn"], //配置语种
defaultLocale: 'cn', // 默认语种
vueI18n: './i18n.config.ts', // 通过vueI18n配置
},
...
})
修改index.vue 文件
我们以index.vue 文件为例,主要看 $t(HOME.b1.title
) 部分,HOME 便是当时 lang中的文件名
<template>
<div class="home-wrap">
Nuxt3--{{ $t(`HOME.b1.title`) }}
</div>
</template>
<script setup>
export default {
}
</script>
<style lang="scss" scoped>
.home-wrap{
min-height: calc(100vh - 200px);
display: flex;
justify-content: center;
align-items: center;
font-size: 50px;
}
</style>
查看效果
中英文切换,主要是通过不同的路由进行区分的,大家注意看浏览器URL地址
中文效果
英文效果
动态切换语言
我们虽然实现了语言替换,但是官网中并没有实现语言动态切换的功能,因此我们需要进一步完善,首先我们需要引入pinia
yarn add @pinia/nuxt@0.5.1 pinia@2.0.23 --save
nuxt.config.ts
// 添加 @pinia/nuxt
export default defineNuxtConfig({
...
modules:[
'@pinia/nuxt',
'@nuxtjs/i18n'
]
...
})
head文件
lang 文件夹下增加 head.js 文件,用来替换 头部的语言
// lang/cn/head.js
export default {
'home':'首页',
'product':'产品业务',
'order':'订单',
'steward':'管家',
'aboutUs':'关于我们'
}
// lang/en/head.js
export default {
'home':'Home',
'product':'Product',
'order':'Order',
'steward':'Steward',
'aboutUs':'About Us'
}
composables
使用composables/目录将你的Vue组合式函数自动导入到你的应用程序中。
├── components
├── composables
│ ├── global.ts
│ └── store.ts
store.ts
export const useStateStore = defineStore('nuxtStore', () => {
const locale = ref('')
let localeName = ref('')
const localeArr = ref([
{id:'cn',name:'中文'},
{id:'en',name:'English'}
])
const setState = (name: any) => {
let arr = localeArr.value.filter(m=>m.id ==name)
let obj = localeArr.value[0]
if(arr.length>0){
obj = arr[0]
}
locale.value = obj.id
localeName.value = obj.name
}
return {
locale,
localeArr,
localeName,
setState
}
})
global.ts
export default function () {
const routerFn = (fullPath: any) => {
const route = useRoute()
const localeRoute = useLocaleRoute()
const store = useStateStore()
// 你的公共逻辑
let arr = ['cn','en']
let a = arr.filter((m) => fullPath.includes(m))
if(a.length){
let paths = fullPath.split(a[0])
fullPath = '/'+store.locale +paths[1]
} else {
fullPath = '/'+store.locale +fullPath
}
// 用于把当前页面生成对应的语言前缀的路由,例如:/zh/,/zh/about
const routePath = localeRoute({ path:fullPath, query: { ...route.query } })
if (routePath) {
return navigateTo(fullPath) // 路由跳转
}
};
return {
routerFn
};
}
header.vue
我们需要重新改造一下header文件,右上角增加了语言切换按钮,整个导航栏 增加了滚动超出页面后悬浮,按钮切换高亮动画效果等
<template>
<section class="c-head-wrap" :class="[{'on':scrollY >=70},store.isIPhone?'m':'pc']">
<div class="content-box">
<div class="back-box"></div>
<div class="head-box">
<section class="main g-width-box g-cen-y">
<div class="logo-box" >
<img class="img" src="@/assets/img/logo-top.png" />
</div>
<div
class="nav-btn-box "
>
<div class="g-dis">
<div
v-for="(m, i) in navArr"
:key="i"
class="btn"
ref="btnId"
@click="routerFn(m)"
:class="[isPageFn(m,i),{'hover':isChildren == m.path}]"
>
<h6>{{ $t(`HEAD.${m.name}`) }}</h6>
<div v-show="m.children && async" class="children-box">
<div
class="item"
v-for="(n,ind) in m.children"
:key="i+'-'+ind"
:class="[isPath == n.path && 'on']"
@click.stop="routerFn(n)"
>
{{ $t(`HEAD.${n.name}`) }}
</div>
</div>
</div>
</div>
<div class="border" :style="isStyle"></div>
</div>
<div
class="locale-box g-cen-y"
@click="localeFn"
:class="[{'hover':isChildren == 'locale'}]"
>
<i class="icon1"></i>
<span>{{ store.localeName}}</span>
<i class="icon2"></i>
<div class="children-box" v-show="async ">
<div
class="item"
v-for="n in store.localeArr"
:key="n.id"
:class="[store.locale == n.id && 'on']"
@click.stop="change(n.id)"
>
{{ n.name }}
</div>
</div>
</div>
</section>
</div>
</div>
</section>
</template>
<script setup>
import { ref } from "vue";
import useGlobal from '@/composables/global.ts'
const route = useRoute()
const global = useGlobal()
const store = useStateStore()
defineProps({
scrollY: {
type: [String,Number],
default: 0
}
});
let async =ref(true)
const navArr = ref([
{ path: "/",name:'home' },
{
path: "/product",
name:'product',
children:[
{ path: "/product/order", name:'order'},
{ path: "/product/steward", name:'steward'}
]
},
{ path: "/about", name:'aboutUs' }
]);
let btnId = ref(null)
let isStyle = ref('')
let isChildren = ref('')
let isPath = computed(() => {
let name = 'cn'
if(route.path.split('/')[1]){
name = route.path.split('/')[1]
}
if(name==404){
name = 'cn'
}
store.setState(name)
return route.path.split(store.locale)[1] || '/'
})
const routerFn = (e) => {
if(e.children) {
if(store.isIPhone){
isChildren.value = e.path
}
return
}
let fullPath = route.path.split( store.locale )[1]
// 去重 路径无变化,不需要跳转
if(fullPath === e.path)return
global.routerFn(e.path)
asyncFn()
};
const localeFn =()=>{
if(store.isIPhone){
isChildren.value = 'locale'
}
}
const isPageFn =(e,ind)=>{
let async = false
if(isPath.value == e.path){
async = true
} else if(e.children){
async = !!e.children.filter(m => m.path == isPath.value).length
}
let arr = btnId.value
if(async && arr){
let w = 0;
for(let i=0;i<ind;i++){
w += arr[i].offsetWidth
}
isStyle.value = `left:${w}px;opacity:1;width:${arr[ind].offsetWidth}px`
}
return async ? 'on':''
}
// 切换中英文
const change = (e)=>{
store.setState(e)
let fullPath = route.fullPath
global.routerFn(fullPath)
asyncFn()
}
const asyncFn = ()=>{
async.value = false;
isChildren.value = ''
setTimeout(()=>{
async.value = true;
},100)
}
</script>
<style lang="scss" scoped>
.c-head-wrap{
min-width: 1350px;
position: absolute;
left: 0;
width: 100%;
z-index: 22;
&.on{
height: 0;
animation: boxShow .5s forwards ;
position: sticky;
.content-box{
box-shadow: 0 20px 40px 0 rgba(74,91,130,.16);
position: relative;
}
.head-box{
position: relative;
z-index: 2;
}
.back-box{
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 70px;
display: block;
background: rgba(255,255,255,.9);
backdrop-filter: blur(3px);
}
}
&.pc{
.nav-btn-box .btn:hover,.locale-box:hover{
.children-box{
display: block;
}
}
}
&.m{
.nav-btn-box .btn.hover,.locale-box.hover{
.children-box{
display: block;
}
}
}
}
.content-box{
width: 100%;
height: 70px;
background: transparent;
border-bottom: 1px solid #E0E4E8;
.back-box{
display: none;
}
}
.main{
height: 70px;
}
.logo-box{
height: 100%;
display: flex;
align-items: center;
margin-right: 102px;
.img{
width: 300px;
height: 32px;
object-fit: cover;
}
}
.nav-btn-box{
flex: 1;
position: relative;
.border{
width: 72px;
height: 2px;
background: #0044FF;
position: absolute;
bottom: 0;
transition: left 0.28s;
opacity: 0;
}
.btn{
font-weight: 500;
font-size: 18px;
color: #4F587F;
line-height: 70px;
padding: 0 18px;
cursor: pointer;
position: relative;
font-weight: 400;
z-index: 2;
&.on{
color: #0044FF;
position: relative;
}
}
}
.locale-box{
font-weight: 500;
font-size: 18px;
color: #1C1E20;
height: 100%;
display: flex;
align-items: center;
cursor: pointer;
position: relative;
.icon1{
width: 22px;
height: 22px;
margin-right: 9px;
background: url('@/assets/img/icon1.png') no-repeat center;
background-size: cover;
}
.icon2{
width: 24px;
height: 24px;
background: url('@/assets/img/icon2.png') no-repeat center;
background-size: cover;
}
}
.children-box{
display: none;
position: absolute;
background-image: linear-gradient(197deg, rgba(246,246,246,1) , rgba(246,246,246,.65));
backdrop-filter: blur(5px);
box-shadow: 0 20px 40px 0 rgba(74,91,130,.16);
border: 2px solid #fff;
min-width: 100%;
left: 50%;
top: 70px;
line-height: 50px;
text-align: center;
white-space: nowrap;
transform: translate(-50%,0);
font-size: 16px;
font-weight: 400;
color: #4F587F;
z-index: 2;
.item{
border-bottom: 1px solid #E0E4E8;
padding: 0 14px;
&:last-child{
border-bottom: 0;
}
&.on,&:hover{
color: #0044FF;
}
}
}
@keyframes boxShow {
0% {
top: -70px;
}
100% {
top:0px
}
}
</style>
error.vue
和 app.vue 同一级,增加error.vue 文件
<template>
<div />
</template>
<script>
export default {
created () {
this.$router.replace('/404')
}
}
</script>
<style>
</style>
404.vue
和index.vue同一级,增加404.vue 文件,文件内容可以自行编辑。 我这里直接重置到了首页
<template>
<div>
</div>
</template>
<script setup>
import { onMounted } from "vue";
// const route = useRoute()
onMounted(()=>{
navigateTo('/')
})
</script>
查看效果
内容有限,我们以首页为例,对比一下 中英文切换效果
中文效果
英文效果
配置每个页面的TDK
TDK指的是搜索引擎优化(SEO)中的页面Title、Description、Keywords
- Title:
- 页面的主要内容,对搜索引擎而言至关重要。
- 简洁、准确地描述页面内容。
- 不超过70个汉字或160个英文字符。
- Description:
-
简要描述页面内容,提供页面主要信息。
-
不超过200个汉字或~300个英文字符。
- Keywords:
-
关键词应该是与页面内容相关的,用于提升搜索结果的相关性。
-
不超过1000个汉字或~1600个英文字符。
-
使用对搜索引擎友好的逗号、分号或空格分隔关键词。
plugins 文件夹
Nuxt拥有一个插件系统,可以在创建Vue应用程序时使用Vue插件和其他功能。
在plugins 文件夹下,增加global.ts
global.ts
//global.ts
export default defineNuxtPlugin((nuxtApp) => {
return {
provide: {
setHead: (page: String) => {
const store = useStateStore()
let {title,keywords,description} = store.headJson[store.locale][page]
return {
title,
meta:[
{hid: 'keywords', name: 'keywords', content: keywords},
{hid: 'description', name: 'description', content: description}
]
}
}
}
};
})
index.vue
在首页中引用 global.ts 中 创建的 $setHead 方法
<template>
<div class="home-wrap">
<div >
Nuxt3--{{ $t(`HOME.b1.title`) }}
</div>
<div>
{{ $t(`HOME.b1.txt`) }}
</div>
</div>
</template>
<script setup>
const nuxtApp = useNuxtApp();
useHead({...nuxtApp.$setHead('home')})
</script>
<style lang="scss" scoped>
...
...
</style>
演示效果
中文效果
英文效果
图片懒加载
官网项目一直有一个弊端,就是项目中 图片资源比较多,导致打开速度慢。因此我们需要对图片资源做一下优化。
常规的优化一般针对图片压缩、图片格式、图片标签属性 和 图片懒加载
我们这里主要讨论一下 ,nuxt项目中如何进行图片懒加载 处理
这里首先声明一下,市面上其实有很多懒加载插件,但是不知道是不是因为nuxt3 和 lazy相关插件版本有兼容的问题,一致配置的不太好使
因此这个懒加载是我自己写的一个 自定义指令(本来也不复杂,所以干脆自己写了)
懒加载指令
在 plugins文件夹下,增加index.ts 文件,申明 v-Mlazy 指令
// plugins/index.js
// 该指令 arg 用来 区分 img src 还是 标签 background
import {useIntersectionObserver} from "@vueuse/core";
export default defineNuxtPlugin((nuxtApp) => {
// 不再是 import Vue from 'vue'的写法了
nuxtApp.vueApp.directive('Mlazy', {
// 不用mounted,在mounted时用户权限集还是空的
mounted(el,{
arg,
value
}) {
if (typeof value != 'string') {
return
} else if(arg == 'show') {
el.style.background = `url(${value}) no-repeat center`
el.style.backgroundSize = `cover`
return
} else if(arg == 'imgShow') {
el.src = value
return
}
const { stop } = useIntersectionObserver(
el,
([{ isIntersecting }]) => {
if (isIntersecting) {
if(arg == 'img'){
el.src = value
} else if(!arg){
el.style.background = `url(${value}) no-repeat center`
el.style.backgroundSize = `cover`
}
// 当组件卸载时,停止监听
stop();
}
}
)
}
})
})
页面中引用
nuxt3.0后 assets 里面的资源已经不允许动态拼接引入。
懒加载的地址,本地需要放到根目录下的 public文件下,网络图片不受限制
<template>
<div class="home-wrap">
<div >
Nuxt3--{{ $t(`HOME.b1.title`) }}
</div>
<div >
{{ $t(`HOME.b1.txt`) }}
</div>
<div>
这张图是 img
<img v-Mlazy:img="'/back.png'" />
</div>
<div v-Mlazy="'/back.png'">
这张图是 背景图
</div>
</div>
</template>
<script setup>
const nuxtApp = useNuxtApp();
useHead({...nuxtApp.$setHead('home')})
</script>
<style lang="scss" scoped>
.home-wrap{
min-height: calc(100vh - 200px);
&>div{
width: 1200px;
margin: 0 auto;
min-height: 200px;
display: flex;
justify-content: center;
align-items: center;
font-size: 50px;
position: relative;
}
img{
height: 200px;
width: 100%;
position: absolute;
z-index: -1;
}
}
</style>
演示效果
fonts 字体库
api请求
动画
文章正在努力完善中。。。。。