06-nestjs基础实践,创建nuxt前端项目,部署

193 阅读4分钟

参考文档

初始化项目

创建新的项目:npx nuxi@latest init 01-nestjs-front

修改项目端口号, 修改文件nuxt.config.ts

export default defineNuxtConfig({
  + devServer: { port: 8010 },
})

修改app.vue

<template>
    <NuxtPage />
</template>

创建pages/index.vue文件, 安装依赖npm i sass -D

<template>
  <div class="test-div">{{ hello }}</div>
</template>

<script lang="ts" setup>
  import { ref } from 'vue'
  const hello = ref('hello world')
</script>
<style lang="scss" scoped>
  .test-div {
    font-size: 40px;
  }
</style>

命令行运行项目npm run dev, 在浏览器访问http://localhost:8010/

使用组件库

使用tailwindcss

安装组件库:npm install --save-dev @nuxtjs/tailwindcss

nuxt.config.ts添加配置

export default defineNuxtConfig({  
  + modules: ['@nuxtjs/tailwindcss']
})

解决没有class样式提示的问题

npx tailwindcss init

使用Icon

安装图标库:npm install @heroicons/vue @headlessui/vue

使用

</template>
    <ArrowPathIcon/>
    <ChevronDownIcon/>
<template>
<script setup lang="ts">
  import { ArrowPathIcon} from '@heroicons/vue/24/outline'
  import { ChevronDownIcon} from '@heroicons/vue/20/solid'
</script>

Element-plus

安装插件:npm i element-plus @element-plus/nuxt -D

nuxt.config.ts中配置

export default defineNuxtConfig({  
  modules: [    '@element-plus/nuxt'  ],  
  elementPlus: { /** Options */ }
})

修改默认主题

创建文件\assets\styles\element\theme.scss

:root {
  --el-color-primary: red;
  --el-bg-color-overlay: transparent;
}

nuxt.config.ts中增加配置

export default defineNuxtConfig({  
  vite: {
    css: {
      preprocessorOptions: {
        scss: { additionalData: `@use "~/assets/styles/element/theme.scss" as element;` },
      },
    },
  },
  elementPlus: {
    // icon: 'ElIcon',
    importStyle: 'scss', // themes: ['dark'],
  },
})

使用默认布局

改造app.vue

<template>
  + <NuxtLayout>
    <NuxtPage />
  + </NuxtLayout>
</template>

创建layouts/default.vue

<template>
  <div class="default-layout">
    <div class="default-header">
      header
    </div>
    <div class="default-body">
      <slot />
    </div>
    <div class="default-footer">
      footer
    </div>
  </div>
</template>
<script lang="ts" setup>
</script>
<style lang="scss" scoped>
</style>

状态管理pinia

Pinia 中文文档

安装插件:npm i pinia @pinia/nuxt

修改配置文件nuxt.config.ts

modules: ['@pinia/nuxt'],

创建目录和文件:stores/counter.ts

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  function increment() {
    count.value++
  }
  return { count, increment }
})

在项目中使用

<template>
  <div class="py-3">
    <div color="primary" size="xs" variant="solid" @click="counterStore.increment">+</div>
    <p class="text-teal-400 font-bold my-2">count: {{ counterStore.count }}</p>
  </div>
</template>
<script lang="ts" setup>
  import { useCounterStore } from '../stores/counter'
  const counterStore = useCounterStore()
</script>

状态持久化

pinia中有个下载量较多的插件pinia-plugin-persistedstate,这是插件文档

安装插件:npm i pinia-plugin-persistedstate @pinia-plugin-persistedstate/nuxt

然后配置nuxt.config.ts

export default defineNuxtConfig({
  devServer: {
    port: 8010,
  },
  devtools: { enabled: true },
  modules: ['@nuxt/ui', '@pinia/nuxt', '@pinia-plugin-persistedstate/nuxt'],
  // 全局设置pinia持久化存储策略
  piniaPersistedstate: {
    cookieOptions: { sameSite: 'strict' },
    // 1. sessionStorage和localStorage服务端渲染不会持久化, cookies客户端和服务端都可以持久化
    storage: 'cookies', // 可选值sessionStorage|localStorage|cookies
  },
})

编辑文件:stores/counter.ts

export const useCounterStore = defineStore(
  'counter',
  () => {
    ...
  },
  // 如果不想缓存就不要配置
  { 
    // 方案1: 如果想使用cookies缓存(客户端和服务端都可以持久化)
    + persist: true,
    // 方案2: 如果想配置sessionStorage|localStorage缓存(只有客户端可以持久化)
    + persist: process.client ? {
    +     key: 'aaaa', // 自定义缓存的键名, 默认使用store的名称,本例中默认是'counter'
    +     storage: localStorage, // localStorage|sessionStorage
    + }: false
  }, 
)

请求接口

axios

安装插件:npm i axios中文文档

创建api/common/request.ts


import axios from 'axios'
import { ElMessage } from 'element-plus'

const instance = axios.create({
  baseURL: '/api',
  timeout: 80000,
})
// 添加请求拦截器
instance.interceptors.request.use(
  function (config) {
    if (process.env.NODE_ENV === 'development') {
      // 本地开发环境,baseURL修正(服务端没法做本地代理,需要完整的请求路径)
      config.baseURL = `${process.server ? process.env.VITE_API_BASEURL : ''}${config.baseURL}`
    }
    // 在发送请求之前做些什么
    return config
  },
  function (error) {
    // 对请求错误做些什么
    return Promise.reject(error)
  },
)

// 添加响应拦截器
instance.interceptors.response.use(
  function (response) {
    // 2xx 范围内的状态码都会触发该函数。
    // 对响应数据做点什么
    const resData = response.data
    if (resData) {
      if (resData.code !== 0) {
        if (process.client && resData.msg) {
          ElMessage({ showClose: true, message: resData.msg, type: 'error' }) // 错误提示
        }
      }
      return resData //成功返回数据
    } else {
      if (process.client) {
        ElMessage({ showClose: true, message: response.data.message, type: 'error' }) // 错误提示
      }
      return Promise.reject(response)
    }
  },
  function (error) {
    if (process.client) {
      ElMessage({ showClose: true, message: error.message, type: 'error' }) // 错误提示
    }
    // 超出 2xx 范围的状态码都会触发该函数。
    // 对响应错误做点什么
    return Promise.reject(error)
  },
)
export default instance

配置tsconfig.json, 解决axios引用错误

{
  // https://nuxt.com/docs/guide/concepts/typescript
  "extends": "./.nuxt/tsconfig.json",
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/stores/*": ["./stores/*"],
      + "@/utils/*": ["./utils/*"]
    }
  }
}

nuxt.config.ts配置本地接口请求跨域

export default defineNuxtConfig({
 nitro: {
    // 配置请求跨域
    devProxy: {
      '/api': {
        target: 'http://127.0.0.1:9010/api',
        changeOrigin: true,
      },
    },
  },
})

.vue文件中请求接口

<template>
    <div @click="handleRequest">Login</div>
</template>
<script setup lang="ts">
  import axios from '@/api/common/request'
  
  const requestUserById = (id: number): Promise<Record<string, any>> => {
    return request.get(`/user/${id}`)
  }
  const handleRequest = async () => {
    const res = await requestUserById(1)
    console.log(res)
  }
</script>
<style scoped></style>

设置环境变量

我们会在项目中配置两个变量VITE_MODE_ENVVITE_API_BASEURL

调用方式

<script setup lang="ts">
  // 方式1:在访问不到nuxt实例的地方不可用
  const config = useRuntimeConfig()
  console.log(config.public.VITE_MODE_ENV) // 服务端和客户端都可以获取
  console.log(config.VITE_MODE_ENV) // 只有服务端才能获取到
  // 方式2:处处可用
  console.log(process.env.VITE_MODE_ENV)
</script>
  1. 创建.env.development,.env.test,.env.production等配置文件,按需调整配置
# .env.development
VITE_MODE_ENV = development # 开发环境
VITE_API_BASEURL = http://127.0.0.1:9010 # 接口请求地址
# .env.test
VITE_MODE_ENV = test # 开发环境
VITE_API_BASEURL = http://127.0.0.1:9010 # 接口请求地址
# .env.production
VITE_MODE_ENV = production # 开发环境
VITE_API_BASEURL = http://127.0.0.1:9010 # 接口请求地址
  1. 创建类型typings/env.d.ts, 支持process.env.XXX的代码提示
declare namespace NodeJS {
  // process.env.XXX 的类型提示
  interface ProcessEnv {
    NODE_ENV: string
    VITE_MODE_ENV: 'development' | 'test' | 'production'
    VITE_API_BASEURL: string
  }
}
  1. 修改package.jsonscript脚本,指向对应的配置文件
"dev": "nuxt dev --dotenv .env.development",
"dev:test": "nuxt dev --dotenv .env.test",
"dev:prod": "nuxt dev --dotenv .env.production",
"build": "nuxt build --dotenv .env.development",
"build:test": "nuxt build --dotenv .env.test",
"build:prod": "nuxt build --dotenv .env.production",
  1. 修改配置文件nuxt.config.ts
import { loadEnv } from 'vite'

// 通过启动命令区分环境`development|test|production`
const envScript = (process.env as any).npm_lifecycle_script.split(' ')
// 通过启动命令区分环境
const envName = envScript[envScript.length - 1] || '.env.development'
// 拿到了.env文件配置的信息
const envData = loadEnv(envName, 'env') as unknown as NodeJS.ProcessEnv
// 定义变量,能通过`process.env.XXX`获取值
const defineEnv: Record<string, string> = {}
Object.keys(envData).forEach((key) => (defineEnv[`process.env.${key}`] = JSON.stringify((envData as any)[key])))
// 配置接口代理地址
const proxyApiTarget = `${envData.VITE_API_BASEURL}/api`
console.log('环境变量 ======= process.env.XXXX:', defineEnv)
console.log('运行变量 ======= useRuntimeConfig:', envData)

export default defineNuxtConfig({
  ...
  vite: {
   // 指定env文件夹
    envDir: '~',
    // 这定义的变量通过`process.env.VITE_MODE_ENV`获取到值
    define: {
      // 如:'process.env.VITE_MODE_ENV': JSON.stringify(envData.VITE_MODE_ENV),
      ...defineEnv, 
    },
  }
  runtimeConfig: {
    ...envData,
    // 把env放入这个里面,通过useRuntimeConfig获取
    public: { ...envData },
  },
  nitro: {
    // 配置客户端跨域,用于客户端转发
    devProxy: {
      '/api': { target: proxyApiTarget, changeOrigin: true },
    },
  },
})

修改api/common/request.ts

// 在请求拦截器中,用配置数据修正baseURL
// 本地开发环境,baseURL修正(服务端没法做本地代理,需要完整的请求路径)
- config.baseURL = `${process.server ? 'http://localhost:9010' : ''}${config.baseURL}`
+ config.baseURL = `${process.server ? process.env.VITE_API_BASEURL : ''}${config.baseURL}`

用户登录

src/api/user.ts中添加接口

import type { IUserInfo } from '@/stores/user'
import request from './common/request'
import type { IResPromise } from './common/request.d'

/** 用户登录, 假设存在接口`/auth/signin.ts`, */
export const requestSingin = (data: { username: string; password: string }): IResPromise<{ access_token: string; userInfo: IUserInfo }> => {
  return request.post('/auth/signin', data)
}

创建文件src/stores/user.ts

因为pinia使用了持久化缓存插件,所以userStore会把accessTokenuserInfo保存在cookie中

import { requestSingin } from '@/api/user'

/** 登录的用户信息 */
export interface IUserInfo {
  id?: number
  username?: string
  password?: string
  profile?: {
    id: number
    gender: 1 | 2
    photo: string
    address: string
  }
}

export const useUserStore = defineStore(
  'userStore',
  () => {
    // 因为`pinia`使用了持久化缓存插件,所以userStore会把`accessToken`和`userInfo`保存在cookie中
    const accessToken = ref<string>('')
    const userInfo = ref<IUserInfo>({})

    // 用户是否登录
    const isLogin = computed(() => !!accessToken.value)

    // 用户登录
    const handleLoginIn = async (params: { username: string; password: string; [p: string]: any }) => {
      const { username, password } = params
      const res = await requestSingin({ username, password })
      if (res?.code === 0 && res.data) {
        const { access_token, userInfo: info } = res.data
        accessToken.value = access_token
        userInfo.value = info
        return info
      }
    }
    // 退出登录,清空缓存
    const handleLoginOut = () => {
      accessToken.value = ''
      userInfo.value = {}
    }
    return { accessToken, userInfo, isLogin, handleLoginIn, handleLoginOut }
  },
  // 配置持久化缓存
  { persist: true },
)

编辑src/api/common/request.ts, 配置Authorization头信息

每次接口请求都会携带Authorization头信息,服务端会自动读取Authorization并做用户权限校验

+ import { useUserStore } from '@/stores/user'
// 添加请求拦截器
instance.interceptors.request.use(
  function (config) {
    // 添加Authorization头信息
    + const userStore = useUserStore()
    + if (userStore.accessToken) {
    +   config.headers.Authorization = `Bearer ${userStore.accessToken}`
    + }
    ...
    return config
  },
  function (error) {
    // 对请求错误做些什么
    return Promise.reject(error)
  },
)

在centos系统中部署

在centos系统安装docker,配置gitee的密钥(参考之前的文章)

在nuxt3项目中创建文件:.dockerignore

*.md
node_modules/
.git/
.DS_Store
.vscode/
.dockerignore

在nuxt3项目中创建文件:Dockerfile

# build stage
FROM node:18.0-alpine3.14 as build-stage

WORKDIR /app

COPY package.json .

RUN npm config set registry https://registry.npmmirror.com/

RUN npm install

COPY . .

#3、参数,node的环境为生产环境
ENV NODE_ENV=production
#4、任意ip
ENV HOST 0.0.0.0

# 执行生产打包
RUN npm run build:prod 

# production stage
FROM node:18.0-alpine3.14 as production-stage

COPY --from=build-stage /app/.output /app/.output
COPY --from=build-stage /app/package.json /app/package.json

WORKDIR /app

#3、参数,node的环境为生产环境
ENV NODE_ENV=production
#4、任意ip
ENV HOST 0.0.0.0

RUN npm config set registry https://registry.npmmirror.com/

RUN npm install --production

RUN npm install pm2 -g

EXPOSE 3000

# CMD ["node", "/app/.output/server/index.mjs"]
CMD ["pm2-runtime", "/app/.output/server/index.mjs"]

在nuxt3项目中创建文件:deploy.sh

#!/bin/bash
# 在centos系统中的项目路径
WORK_PATH='/usr/__project__/englist-novel-front'
cd $WORK_PATH

echo "清除老代码-----------------------------"
git reset --hard origin/master
git clean -f

echo "拉取新代码-----------------------------"
git pull origin master

echo "停止旧容器并删除旧容器------------------"
# docker stop englist-novel-front
# docker rm englist-novel-front

echo "开始执行构建新容器---------------------------"
docker build -t englist-novel-front .

# 构建Docker镜像:
echo "构建Docker镜像-----------------------------"
docker build -t englist-novel-front .

# 运行Docker容器:
echo "运行Docker容器-----------------------------"
docker run -p 8010:3000 englist-novel-front

# 阿里云发布 ---------------------------------
# - 执行部署:sh deploy.sh

在阿里云centos系统中创建目录/usr/__project__,用git通过ssh方式拉取项目englist-novel-front仓库,执行shell脚本部署:sh deploy.sh