nuxt3+pinia+vueuse+i18n+animate.css+element Plus 从零开始搭建

2,040 阅读6分钟

一、了解Nuxt

  • Nuxt是一个基于 Vue.js 的服务端渲染应用框架
  • Nuxt 3 带有稳定的、生产就绪的 API 和 50 多个由社区和 Nuxt 团队使用 Nuxt Kit 构建的支持模块

二、新特性

  • 更轻量:以现代浏览器为目标的服务器部署和客户端产物最多可缩小 75 倍
  • 更快:基于 nitro 提供动态代码分割能力,以优化冷启动性能
  • Hybrid:增量静态生成和其他的高级功能现在都成为可能
  • Suspense:在任意组件和导航前后都可以获取数据
  • Composition API:使用 Composition API 和 Nuxt 3 的 composables 实现真正的代码复用
  • Nuxt CLI:没有任何依赖,帮你轻松搭建项目和集成模块
  • Nuxt Devtools:通过直接在浏览器中查看信息和快速修复实现更快地工作
  • Nuxt Kit:具有 Typescript 和跨版本兼容性的全新模块开发
  • Webpack 5:更快的构建时间和更小的包大小,无需配置
  • Vite:使用 Vite 作为打包工具,体验闪电般快速的 HMR
  • Vue 3:Vue 3 是你下一个 Web 应用程序的坚实基础
  • TypeScript:使用原生 TypeScript 和 ESM 构建,无需额外步骤

更多新特性见中文官网文档 仓库模板见 GitHub仓库地址

三、初始化项目

node.js => 18.14.0
npm => 9.3.1

1. 初始化项目

npx nuxi init first-nuxt-app

2. 安装依赖

cd first-nuxt-app

# yarn
yarn install

# npm
npm install

3. 运行

npm run dev

四、新建常用目录

1. 新建 api 目录,此目录用来存放所以API请求

mkdir api

2. 新建 assets 目录,此目录用来存放静态资源,包含字体、css、image等

mkdir assets

3. 新建 components 目录,此目录用来存放所有VUE组件

mkdir components

4. 新建 composables 目录,此目录用来存放所有Vue组合式函数并自动导入

mkdir composables

5. 新建 constants 目录,此目录用来存放所有常量

mkdir constants

6. 新建 layouts 目录,此目录用来存放布局框架

mkdir layouts

7. 新建 locales 目录,此目录用来存放多语言配置文件

mkdir locales

8. 新建 middleware 目录,此目录用来存放路由中间件

mkdir middleware

9. 新建 pages 目录,此目录用来存放页面文件,文件名对应路由

mkdir pages

10. 新建 plugins 目录,此目录用来存放所有插件

mkdir plugins

11. 新建 stores 目录,此目录用来存放所有 pinia

mkdir stores

12. 新建 utils 目录,此目录用来存放所有工具函数

mkdir utils

五、element Plus 引入与使用

1. 安装 element Plus

npx nuxi@latest module add element-plus

2. 在 nuxt.config.ts 文件下配置如下:

export default defineNuxtConfig({
  modules: [
    '@element-plus/i18n'
  ],
})

六、i18n 引入与使用

1. 安装 i18n

npx nuxi@latest module add i18n

2. 在 nuxt.config.ts 文件下配置如下:

export default defineNuxtConfig({
  modules: [
    '@nuxtjs/i18n'
  ],
  i18n: {
    vueI18n: '~/locales/index.config.ts'
  },
})

2. 在 locales 目录下创建 en.json zh-tw.json index.config.ts,代码如下:

// index.config.ts
import en from './en.json'
import zhCn from './zh-cn.json'

export default defineI18nConfig(() => {
  return {
    legacy: false,
    messages: {
      cn: {
        ...zhCn
      },
      en: {
        ...en
      }
    }
  }
})
// zh-cn.json
{
  "welcome": "欢迎"
}
// en.json
{
  "welcome": "Welcome"
}

七、pinia 引入与使用

1. 安装 pinia

npm i pinia @pinia/nuxt

2. 在 nuxt.config.ts 文件下配置如下:

export default defineNuxtConfig({
  modules: [
    '@pinia/i18n'
  ],
})

3. 在 stores 目录下新建 useUserStore.ts 文件,用于配置用户信息相关pinia,代码如下:

import { defineStore } from "pinia";

export const useUserStore = defineStore('user', {
  state: () => ({
    name: '张三',
    age: 18,
    sex: '男'
  }),
  getters: {
    ageAdd: (state) => state.age++
  },
  actions: {
    ageChange() {
      this.age++
    }
  }
})

八、vueuse 引入与使用

1. 安装 vueuse

npm i @vueuse/nuxt @vueuse/core

2. 在 nuxt.config.ts 文件下配置如下:

export default defineNuxtConfig({
  modules: [
    '@vueuse/i18n'
  ],
})

九、animate.css 引入与使用

1. 安装 animate.css

npm install animate.css --save

2. 在 app.vue 文件引入,代码如下:

import "animate.css"

十、封装接口请求

1. 在 composables 目录下新建 useDollarFetchRequest.ts 文件,用于封装$fetch,代码如下:

import { $fetch } from 'ofetch';
import { NEED_LOGIN, SUCCESS, ERROR } from '~/constants/service'
import { ElMessage } from 'element-plus'
 
interface RequestOptions {
  customBaseURL?: string;
  [key: string]: any;
}
 
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
 
// 请求拦截器
function handleRequest(options: RequestOptions) {
  options.headers = {
    'Content-Type': 'application/json',
    Authorization: 'Bearer 767|OAp1M52Akq7os139Brs8KPR4m5K9El3J57sGCQSta3aed4bb',
    ...options.headers,
  };
}
 
// 响应拦截器
function handleResponse(response: any) {
  const { code, message } = response

  // 请求成功
  if (code === SUCCESS) {
    return response
  }
  
  ElMessage({
    message,
    type: 'error'
  })

  if (code === NEED_LOGIN) {
    // 此处做【登录失败】处理
  }

  return Promise.reject(new Error(message))
}
 
/**
 * 创建请求方法
 * @param method
 */
function createDollarFetchRequest(method: HttpMethod) {
  return async function (
    url: string,
    data?: any,
    options: RequestOptions = {}
  ) {
    const baseURL = import.meta.env.PROD ? import.meta.env.VITE_APP_BASE_API : '/useApi' 
    const requestUrl = baseURL + url
    try {
      handleRequest(options);
      const response = await $fetch(requestUrl, {
        method,
        body: data,
        ...options,
      });
      return handleResponse(response);
    } catch (error) {
      console.error('请求错误:', error);
      throw error;
    }
  };
}

export const useDollarGet = createDollarFetchRequest('GET');
export const useDollarPost = createDollarFetchRequest('POST');
export const useDollarPut = createDollarFetchRequest('PUT');
export const useDollarDelete = createDollarFetchRequest('DELETE');

2. 在 composables 目录下新建 useFetchRequest.ts 文件,用于封装useFetch,代码如下:

import { useFetch } from '#app';
import type { UseFetchOptions } from 'nuxt/app';
import { NEED_LOGIN, SUCCESS, ERROR } from '~/constants/service'
import { ElMessage } from 'element-plus'
 
interface RequestOptions extends UseFetchOptions<any> {
  customBaseURL?: string;
}
 
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type HandleRequestOptions = { request: Request; options: RequestOptions };
type HandleResponseOptions = { response: any };
 
// 请求拦截器
function handleRequest({ options }: HandleRequestOptions) {
  options.headers = {
    'Content-Type': 'application/json',
    Authorization: 'Bearer 767|OAp1M52Akq7os139Brs8KPR4m5K9El3J57sGCQSta3aed4bb',
    ...options.headers,
  };
}
 
// 响应拦截器
function handleResponse({ response }: HandleResponseOptions) {
  const { code, message } = response._data
  
  // 请求成功
  if (code === SUCCESS) {
    return response._data
  }
  
  ElMessage({
    message,
    type: 'error'
  })

  if (code === NEED_LOGIN) {
    // 此处做【登录失败】处理
  }

  return Promise.reject(new Error(message))
}
 
/**
 * 创建请求方法
 * @param method
 */
function createUseFetchRequest(method: HttpMethod) {
  return async function (
    url: string,
    data?: any,
    options: RequestOptions = {}
  ) {
    const baseURL = import.meta.env.VITE_APP_BASE_API
    const requestUrl = baseURL + url
    return await useFetch(requestUrl, {
      ...options,
      method,
      body: data,
      onRequest: handleRequest,
      onResponse: handleResponse
    });
  };
}

export const useFetchGet = createUseFetchRequest('GET');
export const useFetchPost = createUseFetchRequest('POST');
export const useFetchPut = createUseFetchRequest('PUT');
export const useFetchDelete = createUseFetchRequest('DELETE');

十一、定义全局常量

1. 在 constants 目录下新建 common.ts 文件,定义公用常量,代码如下:

export class COMMON_STATUS {
  static readonly true = 1 // 启用
  static readonly false = 2 // 停用

  static readonly options = [
    { value: this.true, label: '启用', type: 'success' },
    { value: this.false, label: '停用', type: 'info' }
  ]
}

2. 在 constants 目录下新建 service.ts 文件,定义请求服务常量,代码如下:

export const SUCCESS = 200
export const ERROR = 201
export const NEED_LOGIN = 202

十二、全局路由守卫

1. 在 middleware 目录下新建 index.global.ts 文件,代码如下:

export default defineNuxtRouteMiddleware((to, from) => {
  console.log('路由守卫:', to, from)
})

十三、环境变量配置

1. 在根目录下新建 .env 运行环境文件,对应运行命令 npm run dev ,代码如下:

# 环境类型
VITE_ENV_TYPE = 'dev'
# API路径
VITE_APP_BASE_API = 'https://api.dev.com/'

2. 在根目录下新建 .env.stage 运行环境文件,对应运行命令 npm run build:stage ,代码如下:

# 环境类型
VITE_ENV_TYPE = 'stage'
# API路径
VITE_APP_BASE_API = 'https://api.stage.com/'

3. 在根目录下新建 .env.prod 运行环境文件,对应运行命令 npm run build:prod ,代码如下:

# 环境类型
VITE_ENV_TYPE = 'prod'
# API路径
VITE_APP_BASE_API = 'https://api.prod.com/'

4.在 package.json 配置对应命令,代码如下:

{
  "scripts": {
    "dev": "nuxt dev --dotenv .env.development",
    "serve": "nuxt dev --dotenv .env.development",
    "build": "nuxt build --dotenv .env.production",
    "build:stage": "nuxt build --dotenv .env.staging",
    "build:prod": "nuxt build --dotenv .env.production",
    "generate": "nuxt generate",
    "preview": "nuxt preview",
    "postinstall": "nuxt prepare"
  },
}

十四、全局错误页面(404,500等)封装处理

1. 在根目录下新建 error.vue 文件,代码如下:

<template>
  <div class="error">
    <div class="error-box">
      <h1 :data-t="error.statusCode " class="h1">{{ error.statusCode }}</h1>
    </div>
  </div>
</template>

<script>
export default {
  props: ["error"],
  layout: "error", // you can set a custom layout for the error page
};
</script>

<style lang="scss" scoped>
.error {
  width: 100%;
  height: 100vh;
  min-width: 500px;
  min-height: 500px;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  overflow: hidden;;
  .error-box {
    width: 400px;
    height: 300px;
    display: flex;
    align-items: center;
    justify-content: center;
  }
}
h1 {
  text-align: center;
  font-size: 6rem;
  animation: shake 0.6s ease-in-out infinite alternate;
  position: absolute;
  margin: 0 auto;
}
h1:before {
  content: attr(data-t);
  position: absolute;
  left: 50%;
  transform: translate(-50%, 0.34em);
  height: 0.1em;
  line-height: 0.5em;
  width: 100%;
  animation: scan 0.5s ease-in-out 275ms infinite alternate,
    glitch-anim 0.3s ease-in-out infinite alternate;
  overflow: hidden;
  opacity: 0.7;
}
h1:after {
  content: attr(data-t);
  position: absolute;
  top: -8px;
  left: 50%;
  transform: translate(-50%, 0.34em);
  height: 0.5em;
  line-height: 0.1em;
  width: 100%;
  animation: scan 665ms ease-in-out 0.59s infinite alternate,
    glitch-anim 0.3s ease-in-out infinite alternate;
  overflow: hidden;
  opacity: 0.8;
}
@keyframes glitch-anim {
  0% {
    clip: rect(32px, 9999px, 28px, 0);
  }

  10% {
    clip: rect(13px, 9999px, 37px, 0);
  }

  20% {
    clip: rect(45px, 9999px, 33px, 0);
  }

  30% {
    clip: rect(31px, 9999px, 94px, 0);
  }

  40% {
    clip: rect(88px, 9999px, 98px, 0);
  }

  50% {
    clip: rect(9px, 9999px, 98px, 0);
  }

  60% {
    clip: rect(37px, 9999px, 17px, 0);
  }

  70% {
    clip: rect(77px, 9999px, 34px, 0);
  }

  80% {
    clip: rect(55px, 9999px, 49px, 0);
  }

  90% {
    clip: rect(10px, 9999px, 2px, 0);
  }

  to {
    clip: rect(35px, 9999px, 53px, 0);
  }
}
@keyframes scan {
  0%,
  20%,
  to {
    height: 0;
    transform: translate(-50%, 0.44em);
  }

  10%,
  15% {
    height: 1em;
    line-height: 0.2em;
    transform: translate(-55%, 0.09em);
  }
}
@keyframes shake {
  0% {
    transform: translate(-1px);
  }

  10% {
    transform: translate(2px, 1px);
  }

  30% {
    transform: translate(-3px, 2px);
  }

  35% {
    transform: translate(2px, -3px);
    filter: blur(4px);
  }

  45% {
    transform: translate(2px, 2px) skewY(-8deg) scaleX(0.96);
    filter: blur(0);
  }

  50% {
    transform: translate(-3px, 1px);
  }
}
</style>

十五、全局loading动画

1. 在 components 目录下新建 FullLoading/index.vue 文件,文章出处,代码如下:

<template>
  <div class="loader">
    <div class="logo">
      <div class="white"></div>
      <div class="orange"></div>
      <div class="red"></div>
    </div>
    <p>Loading</p>
  </div>
</template>
<style lang="scss" scoped>
$color: black;
$size: 100px;
$borderWidth: 4px;
$totalTime: 1.5s;
$redWidth: 27%;
$orangeHeight: 50%;
$whiteWidth: 23%;
$backgroundColor1: white;
$backgroundColor2: #F3B93F;
$backgroundColor3: #EA5664;
$backgroundColor4: orange;
$backgroundColor5: red;

div.loader {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: #fff;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  z-index: 100;

  p {
    font-family: 'Helvetica', 'Arial', sans-serif;
    margin: 1em 0 0 0;
    font-size: 16px;
  }
}

div.logo {
  width: $size;
  height: $size;
  box-sizing: border-box;
  position: relative;
  background-color: white;
  &::before,
  &::after {
    z-index: 1;
    box-sizing: border-box;
    content: '';
    position: absolute;
    border: $borderWidth solid transparent;
    width: 0;
    height: 0;
    animation-direction: alternate;
    animation-timing-function: linear;
  }
  &::before {
    top: 0;
    left: 0;
    animation: border-before $totalTime infinite;
    animation-direction: alternate;
  }
  &::after {
    bottom: 0;
    right: 0;
    animation: border-after $totalTime infinite;
    animation-direction: alternate;
  }
  & > div {
    position: absolute;
    opacity: 0;
  }
  div.white {
    border-left: $borderWidth solid $color;
    top: 0;
    bottom: 0;
    right: 0;
    width: 0;
    animation: $backgroundColor1 $totalTime infinite;
    animation-direction: alternate;
  }
  div.orange {
    border-top: $borderWidth solid $color;
    left: 0;
    bottom: 0;
    right: 0;
    height: 0;
    background-color: $backgroundColor2;
    animation: $backgroundColor4 $totalTime infinite;
    animation-direction: alternate;
  }
  div.red {
    border-right: $borderWidth solid $color;
    top: 0;
    bottom: 0;
    left: 0;
    width: 0;
    background-color: $backgroundColor3;
    animation: $backgroundColor5 $totalTime infinite;
    animation-direction: alternate;
  }
}

@keyframes border-before {
  0% {
    width: 0;
    height: 0;
    border-top-color: $color;
    border-right-color: transparent;
  }
  12.49% {
    border-right-color: transparent;
  }
  12.5% {
    height: 0;
    width: 100%;
    border-top-color: $color;
    border-right-color: $color;
  }
  25%,
  100% {
    width: 100%;
    height: 100%;
    border-top-color: $color;
    border-right-color: $color;
  }
}

@keyframes border-after {
  0%,
  24.99% {
    width: 0;
    height: 0;
    border-left-color: transparent;
    border-bottom-color: transparent;
  }
  25% {
    width: 0;
    height: 0;
    border-left-color: transparent;
    border-bottom-color: $color;
  }
  37.49% {
    border-left-color: transparent;
    border-bottom-color: $color;
  }
  37.5% {
    height: 0;
    width: 100%;
    border-left-color: $color;
    border-bottom-color: $color;
  }
  50%,
  100% {
    width: 100%;
    height: 100%;
    border-left-color: $color;
    border-bottom-color: $color;
  }
}

@keyframes red {
  0%,
  50% {
    width: 0;
    opacity: 0;
  }
  50.01% {
    opacity: 1;
  }
  65%,
  100% {
    opacity: 1;
    width: $redWidth;
  }
}

@keyframes orange {
  0%,
  65% {
    height: 0;
    opacity: 0;
  }
  65.01% {
    opacity: 1;
  }
  80%,
  100% {
    opacity: 1;
    height: $orangeHeight;
  }
}

@keyframes white {
  0%,
  75% {
    width: 0;
    opacity: 0;
  }
  75.01% {
    opacity: 1;
  }
  90%,
  100% {
    opacity: 1;
    width: $whiteWidth;
  }
}
</style>

2. 全局使用,首屏加载渲染时显示动画,在 app.vue 文件写入以下代码:

<script setup lang="ts">
  // 是否首次加載
  const isFullLoading = ref(true)

  nuxtApp.hook('page:start', () => {
    isFullLoading.value = true
  })

  nuxtApp.hook('page:finish', () => {
    isFullLoading.value = false
  })
</script>

<template>
  <div>
    <!-- 首屏加载动画 -->
    <FullLoading v-if="isFullLoading" />
    <NuxtLayout>
      <NuxtPage />
    </NuxtLayout>
  </div>
</template>

十六、页面导航之间的进度条

1. 在 app.vue 文件下增加以下代码:

<template>
  <div>
    <NuxtLayout>
      <!--
        进度条
        color 进度条颜色,设置false可以关闭显色的颜色样式
        height 进度条高度,单位像素,默认3
        duration 进度条持续时间,单位毫秒,默认2000
        throttle 进度条出现隐藏的接流时间,单位毫秒,默认200
      -->
      <NuxtLoadingIndicator
        color="#000000"
        :height="3"
        :duration="2000"
        :throttle="200"
      />
      <NuxtPage />
    </NuxtLayout>
  </div>
</template>

十七、完整目录结构图

# 文件目录结构
├── .nuxt 
├── api 请求接口
├── assets 需要经过构建工具处理的静态资源
│   ├── fonts 字体
│   ├── images 图片
│   └── styles 公共样式
│       ├── index.scss 样式文件主入口
│       ├── mixins.scss 样式函数
│       ├── reset.scss 样式重置
│       ├── transition.scss 样式动画
│       └── variables.scss 样式变量
├── components 所有组件
│   └── FullLoading 首屏加载动画
│       └── index.vue
│   └── Layout layout布局
│       ├── Footer 公共底部
│       └── Header 公共头部
├── composables 组合式函数
│   ├── useDollarFetchRequest.ts $Fetch接口封装
│   └── useFetchRequest.ts useFetch接口封装
├── constants 公共常量
│   ├── common.ts 公共常量
│   └── service.ts 接口常量
├── layouts 可复用布局框架
│   ├── default 默认布局(带头部底部)
│   └── nolayout 不带头部底部布局
├── locales 语言包
│   ├── en.json 英语语言包
│   ├── index.config.ts 多语言配置文件
│   └── zh-tw.json 中文繁体语言包
├── middleware 路由中间件
│   └── index.global.ts 全局路由守卫
├── node_modules 项目依赖包
├── pages 文件路由 对应页面
├── plugins 注册的插件
│   ├── vue-lazyload.ts 自定义指令 图片懒加载
│   └── aos.client.ts aos.js屏幕滚动动画
├── public 不经过处理的静态资源
│   ├── favicon.ico 网页标签上的小图标
│   └── robots.txt 测试环境防止 Google 爬虫抓取(正式环境忽略该文件)
├── server 服务端处理程序
├── utils 通用函数
├── .env 运行环境常量配置文件
├── .env.prod 正式环境常量配置文件
├── .env.stage 测试环境常量配置文件
├── .gitignore Git版本控制下忽略文件目录
├── .nuxtignore 构建阶段忽略文件
├── app.config.ts 项目公开响应式配置
├── app.vue 主入口文件
├── error.vue 报错页面
├── nuxt.config.ts nuxt配置文件
├── package-lock.json 记录 `npm install` 时安装的各个 npm package 的来源和版本号
├── package.json 项目的描述、组件版本等
├── README.md 说明文件
└── tsconfig.json