分类: web前端 | 标签: nuxt

nuxt2笔记整理

发表于: 2023-12-12 17:44:28 | 字数统计: 4.8k | 阅读时长预计: 23分钟

Nuxt2官网

简介

nuxt是基于vue语法来实现SSR(服务器渲染)的框架,解决单页应用不利于SEO的问题

创建项目

由于国内网络问题经常下载失败,推荐采用沙盒下载环境项目模板

https://stackblitz.com/github/nuxt/starter/tree/v2-stackblitz?file=README.md

1702344800164

新建一个nuxt2-study文件夹,将下载后的文件解压到这个文件夹中

1702345551102

然后安装依赖

npm i

启动

npm run dev

访问:localhost:3000

路由

每个页面组件都是一个Vue组件 ,pages目录中的每个文件都是一个页面。

静态路由

假设文件结构如下

pages/
--| user/
-----| index.vue
-----| one.vue
--| index.vue

那么生成的路由如下

router: {
  routes: [
    {
      name: 'index',
      path: '/',
      component: 'pages/index.vue'
    },
    {
      name: 'user',
      path: '/user',
      component: 'pages/user/index.vue'
    },
    {
      name: 'user-one',
      path: '/user/one',
      component: 'pages/user/one.vue'
    }
  ]
}

动态路由

假设文件结构如下

pages/
--| _slug/
-----| comments.vue
-----| index.vue
--| users/
-----| _id.vue
--| index.vue

那么生成的路由如下

router: {
  routes: [
    {
      name: 'index',
      path: '/',
      component: 'pages/index.vue'
    },
    {
      name: 'users-id',
      path: '/users/:id?',
      component: 'pages/users/_id.vue'
    },
    {
      name: 'slug',
      path: '/:slug',
      component: 'pages/_slug/index.vue'
    },
    {
      name: 'slug-comments',
      path: '/:slug/comments',
      component: 'pages/_slug/comments.vue'
    }
  ]
}

动态路由参数的获取和vue-router一致:this.$route.params.xxx

NuxtLink组件

和router-link类似,用于跳转页面。 对于指向站点内页面的所有链接,请使用< NuxtLink >。如果有其他网站的链接,应该使用< a >标签

<NuxtLink to="/book/10001">查看详情</NuxtLink>

布局

功能类似vue中的router-view组件,一个项目可以有多个布局。默认使用的布局是 layouts/default.vue

  • 定义新布局 layouts/myLayout.vue
<template>
    <div>
        <h1>自定义页面头部</h1>
        <!-- Nuxt功能类似router-view组件 -->
        <Nuxt />
        <h1>自定义页面底部</h1>
    </div>
</template>
  • 组件中使用布局
<template>
    <div>
        <NuxtLink to="/book/10001">查看详情</NuxtLink>
    </div>
</template>

<script>
export default {
    name: 'Nuxt2StudyIndex',
    layout: 'myLayout',//使用自定义的布局
    //...
};
</script>

错误页面

当404和500的时候显示一个错误页面

layouts/error.vue

<template>
    <!-- 自定义错误页面 -->
    <div class="error-container">
        <div class="info">
            <div class="code">{{ error.statusCode }}</div>
            <div v-if="error.statusCode == 404" class="desc">抱歉!页面走丢了</div>
            <div v-else class="desc">抱歉!服务器繁忙</div>
            <el-button @click="$router.replace('/home')">返回首页</el-button>
        </div>
    </div>
</template>
  
<script>
export default {
    props: ['error']
}
</script>
<style lang="scss" scoped>
.error-container {
    background-color: #55aafe;
    height: 100vh;
    display: flex;
    .info {
        margin: auto;
        color: #fff;
        text-align: center;
        .code {
            font-size: 200px;
            font-weight: bold;
        }
        .desc{
            font-size: 30px;
            margin: 20px 0;
        }
    }
}
</style>

生命周期

生命周期如下: 重点掌握asyncData() 和 mouted()

lifecycle

中间件

在请求到达之前做一些处理、比如重定向、鉴权等操作

路由中间件

新建 middleware/middleware.js

export default function ({app,redirect,route }) {
    console.log('这个会在服务器的控制台打印')
    //默认跳转到 home
    if("/" == route.fullPath || "/zh" == route.fullPath){
        redirect('/home')
    }
}

nuxt.config.js 配置中间件

export default {
    router: {
        middleware: "middleware", //middleware对应middleware.js的文件名
    }
    //...
}

命名中间件

新建middleware/auth.js

//鉴权中间件
export default function ({ app,store, redirect, route }) {
  console.log("auth middleware", store.getters.token, "auth url", route.path);

  //白名单
  let whiteList = [
    "/sign-in", 
    '/register',
    '/home'
  ];
  //let whiteListZh = whiteList.map(m => app.localePath(m))
  //whiteList = [...whiteList,...whiteListZh] //生成中英

  if (process.client && !store.getters.token) {//客户端渲染的页面 没有token
    if (!(whiteList.includes(route.path) || whiteList.some(s => route.path.startsWith(s)))) {//不在白名单
       return redirect("/sign-in");//跳转登录
      //return redirect(app.localePath("/sign-in"));//跳转登录
    }
  }
}

在页面组件或者布局组件中

<template>

</template>

<script>
export default {
  name: 'MyLayout',
  middleware: ['auth'] //当前组件将要受到auth中间件控制
}
</script>

<style lang="scss" scoped>
</style>

数据获取

asyncData

asyncData只能用于页面级组件,asyncData不能访问组件实例(this)。它接收上下文作为其参数。Nuxt会自动将返回的对象与组件数据进行浅层合并。

<template>
    <div>
        <ul>
            <li v-for="car in carList">{{ car.id + '-' + car.name }}</li>
        </ul>
    </div>
</template>

<script>
export default {
    name: 'Nuxt2StudyIndex',
    data() {
        return {
            carList: []
        };
    },
    //服务端渲染:服务器请求并渲染数据 这个钩子内部不能使用this
    async asyncData(ctx) {
         //query 等价于 this.$route.query  params 等价于 this.$route.params
        console.log('ctx', ctx.query,ctx.params)
        //模拟发送请求
        let carList = await new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve([
                    { id: 1, name: '宝马' },
                    { id: 2, name: '奔驰' },
                    { id: 3, name: '五菱' },
                ])
            }, 1000);
        })
        return { carList } //这个数据将自动合并到组件data()
    }
};
</script>

mouted

用于客户端渲染,和vue中的一致

<template>
    <div>
        <ul>
            <li v-for="car in carList">{{ car.id + '-' + car.name }}</li>
        </ul>
    </div>
</template>

<script>
export default {
    name: 'Nuxt2StudyIndex',

    data() {
        return {
            carList: []
        };
    },

    //客户端渲染
    mounted() {
        let cartList = [
            {id: 1,name: '宝马'},
            {id: 2,name: '奔驰'},
            {id: 3,name: '五菱'},
        ] 
        this.carList = cartList
    }
};
</script>

集成sass

安装

npm i sass@1.32.13
npm i sass-loader@10.1.1

页面使用

<template>
  <div class="container">
    <h1>hello nuxt2</h1>
  </div>
</template>
<style lang="scss" scoped>
.container{
  h1{
    color: red;
  }
}
</style>

集成elementui

安装

npm i element-ui -S

新建plugins/elementui.js

import Vue from 'vue';
import ElementUI from 'element-ui';
Vue.use(ElementUI);

nuxt.config.js引入elementui.js插件和样式

export default {
    css: [
        "element-ui/lib/theme-chalk/index.css",
        'element-ui/lib/theme-chalk/display.css'
      ],
    plugins: [
        "~/plugins/elementui.js",
    ],
    //...
}

页面使用

<template>
  <div class="container">
    <el-button type="primary">test</el-button>
  </div>
</template>

国际化

基本使用

安装

npm i @nuxtjs/i18n@7.3.1

项目根目录新建lang/zh.js

export default {
    name: '姓名',
    age: '年龄'
}

项目根目录新建lang/en.js

export default {
    name: 'name',
    age: 'age'
}

项目根目录新建i18n.js

import en from "./lang/en.js";
import zh from "./lang/zh.js";
import Vue from "vue";
import VueI18n from "vue-i18n";
Vue.use(VueI18n);

const i18n = {
  locales: ["en", "zh"], //有多少地区的语言就添加多少种
  defaultLocale: "en", //默认的地区语言
  vueI18n: {
    fallbackLocale: "en", //回退策略,指定的locale中没有找到对应资源的情况下使用的locale
    messages: {
      //要渲染的信息,有多少语言就添加多少种
      en,
      zh,
    }
  },
}

nuxt.config.js

import i18n from "./i18n.js"; //导入上面的i18n配置文件
export default {
    //模块
  modules: [["@nuxtjs/i18n", i18n]],
    //...
}

页面中使用

this.$t(‘key’) 获取当前语言下对应的value值

this.localePath(url) 根据语言处理路径

this.$i18n.locale 获取当前语言

<template>
  <div class="container">
    <el-tag type="primary">{{ $t('name') }}</el-tag>
    <el-tag type="success">{{ $t('age') }}</el-tag>
    <nuxt-link :to="url">本地化路径</nuxt-link>
  </div>
</template>

<script>
export default {
  name: 'Nuxt2StudyIndex',
  data() {
    return {
      url: ''
    };
  },
  mounted() {
    //localePath方法可根据语言处理路径 英文:/book/1001 中文:/zh/book/1001
    this.url = this.localePath('/book/1001')
  }
};
</script>

访问

英文:http://localhost:3000/
中文:http://localhost:3000/zh

语言切换组件

components/Language.vue

<template>
    <a :href="webUrl">
        <img :src="require('@/assets/images/zh.svg')" v-if="$i18n.locale == 'en'" @click.prevent="changeLang('zh')"
            style="cursor: pointer" />
        <img :src="require('@/assets/images/en.svg')" v-if="$i18n.locale == 'zh'" @click.prevent="changeLang('en')"
            style="cursor: pointer" />
    </a>
</template>

<script>
export default {
    name: 'Language',

    data() {
        return {
        }
    },

    computed: {
        webUrl() {
            return this.$i18n.locale == 'en' ? "/zh" + this.$route.fullPath : this.$route.fullPath
        }
    },

    methods: {
        // 切换语言
        changeLang(lang) {
            let url = this.$route.fullPath
            url = url.includes('/zh') ? url.replace('/zh', '') : '/zh' + url
            url = url.replace('null', '')
            // location.href = url
            this.$router.replace(url)
        }
    },
};
</script>

<style lang="scss" scoped>
a:hover{
    background: #f2f4f7;
}
a{
    padding: 8px!important;
}
img {
    vertical-align: middle;
}
</style>

element ui 国际化

layouts/default.vue

<template>
  <!-- 默认布局组件 -->
  <div>
    <!-- 渲染内容 -->
    <Nuxt />
  </div>
</template>
<script>
import en from 'element-ui/lib/locale/lang/en'
import zh from 'element-ui/lib/locale/lang/zh-CN'
import locale from 'element-ui/lib/locale'
export default {
  name: 'Default',
  mounted() {
    //elementui国际化
    let lang = this.$i18n.locale == 'en'? en :zh
    locale.use(lang)
  }
};
</script>

SEO的处理

全局处理

在nuxt.config.js 中可以给所有页面配置

export default {
  //全局SEO设置
  head: {
    title: "Tranalysis system",
    htmlAttrs: {
      lang: "en",
    },
    meta: [
      { charset: "utf-8" },
      { name: "viewport", content: "width=device-width, initial-scale=1" },
      { hid: "description", name: "description", content: "Tranalysis system" },
    ],
    link: [{ rel: "icon", type: "image/x-icon", href: "/favicon.ico" }],
  },
}

单个页面或者布局组件中配置

这种方式更好,可以支持国际化

<template></template>
<script>
export default {
    head() {
        let title = '标题111'
        let description = '描述2222'
        return {
          title,//网页标题
          meta: [
            {
              hid: 'keywords',
              name: 'keywords', //网页关键字
              content: title
            },
            {
              hid: 'description',
              name: 'description',//网页描述
              content: description
            },
          ]
        }
    },
}
</script>
<style lang="scss" scoped></style>

vuex的使用

和原来的用法一致,nuxt中已经内置vuex,所以不需要安装vuex,只需要安装vuex持久化插件即可

安装vuex持久化插件

npm i vuex-persistedstate@4.1.0

新建 plugins/vuex-persistedstate.js

import createPersistedState from 'vuex-persistedstate'

//vuex持久化
export default ({ store }) => {
    createPersistedState({
        storage: localStorage
    })(store)
}

nuxt.config.js

export default {
  plugins: [
    //...
    { src: '~/plugins/vuex-persistedstate.js', ssr: false } //引入vuex-persistedstate插件
  ],
}

store/modules/user.js

const user = {
    state: {
        count: 0
    },
    mutations: {
        SET_COUNT: (state, data) => {
          state.count = data
        },
    },
    actions: {}
}
export default user;

store/getters.js

const getters = {
    count: state => state.user.count
}

export default getters

store/index.js

import Vue from "vue";
import Vuex from "vuex";
import user from "./modules/user";
import getters from "./getters";
Vue.use(Vuex);
//主要这里要用箭头函数的形式导出,否则会报错
export default () => new Vuex.Store({
  getters,
  modules: {
    user
  },
});

组件中使用vuex

<template>
    <div>
        <el-button @click="add" type="primary">{{ count }}</el-button>
    </div>
</template>
<script>
import {mapGetters,mapMutations} from 'vuex'
export default {
    name: 'Nuxt2StudyIndex',
    computed:{
        ...mapGetters(['count'])
    },
    methods: {
        ...mapMutations(['SET_COUNT']),
        add(){
            this.SET_COUNT(this.count+1)
        }
    },
}
</script>

配置环境变量

安装

npm i cross-env@7.0.3 -D

项目根目录新建env.js

export default {
    //开发环境
    dev:{
      NODE_ENV: 'dev',
      PROJECT_TITLE: 'NUXT-DEV',
      ENV_API:'http://dev.com/api',//API地址
    },
    //测试环境
    stage:{
      NODE_ENV: 'test',
      PROJECT_TITLE: 'NUXT-TEST',
      ENV_API:'http://test.com/api',//API地址
    },
    //正式环境
    prod:{
      NODE_ENV: 'prod',
      PROJECT_TITLE: 'NUXT-PRODUCTION',
      ENV_API:'http://prod.com/api',//API地址
    }
}

nuxt.config.js

import env from "./env.js"; //导入上面的环境变量
export default {
    env: env[process.env.NODE_ENV] //配置环境变量
    //...
}

package.json 配置

{
  "scripts": {
    "dev": "cross-env NODE_ENV=dev nuxt dev",
    "build:stage": "cross-env NODE_ENV=stage nuxt build",
    "start:stage": "cross-env NODE_ENV=stage nuxt start",
    "build:prod": "cross-env NODE_ENV=prod nuxt build",
    "start:prod": "cross-env NODE_ENV=prod nuxt start",
    "generate:prod": "cross-env NODE_ENV=prod nuxt generate",
    "generate:stage": "cross-env NODE_ENV=stage nuxt generate"
  },
}

页面中使用

<template>
    <div>
        {{ title }}-{{ baseAPI }}
    </div>
</template>
<script>
export default {
    name: 'Nuxt2StudyIndex',
    data() {
        return {
            title: process.env.PROJECT_TITLE,
            baseAPI: process.env.ENV_API
        }
    }
}

集成axios

安装

npm i @nuxtjs/axios@5.13.6

plugins/axios.js 参考下面按需修改即可

import { Message } from "element-ui";
import { getToken } from "@/utils/auth";
//get请求参数处理
function tansParams(params) {
  let result = "";
  for (const propName of Object.keys(params)) {
    const value = params[propName];
    var part = encodeURIComponent(propName) + "=";
    if (value !== null && value !== "" && typeof value !== "undefined") {
      if (typeof value === "object") {
        for (const key of Object.keys(value)) {
          if (
            value[key] !== null &&
            value[key] !== "" &&
            typeof value[key] !== "undefined"
          ) {
            let params = propName + "[" + key + "]";
            var subPart = encodeURIComponent(params) + "=";
            result += subPart + encodeURIComponent(value[key]) + "&";
          }
        }
      } else {
        result += part + encodeURIComponent(value) + "&";
      }
    }
  }
  return result;
}
export default function ({ $axios, redirect, app, store, $router, $route }) {
  //如果需要国际化可以:app.i18n.t("key")

  //请求拦截
  $axios.interceptors.request.use(
    (config) => {
      const isToken = (config.headers || {}).isToken === false;
      if (getToken() && !isToken) {
        config.headers["Authorization"] = "Bearer " + getToken(); // 让每个请求携带自定义token 请根据实际情况自行修改
      }
      // get请求映射params参数
      if (config.method === "get" && config.params) {
        let url = config.url + "?" + tansParams(config.params);
        url = url.slice(0, -1);
        config.params = {};
        config.url = url;
      }
      return config;
    },
    (error) => {
      console.log("axios request error", error);
      return Promise.reject(error);
    }
  );
  //响应拦截
  $axios.interceptors.response.use(
    (response) => {
      const code = response.data.code || 200;
      console.log("code", code, "axios response:" + response);
      const res = response.data;
      //文件下载
      if ("business/product/downloadProductData" == response.config.url) {
        return res;
      }
      if (code === 401) {
        //登录过期
        // if(app.i18n.t(res.msg)) Message({type: 'error', message: app.i18n.t(res.msg)})
        // store.dispatch("LogOut").then(() => {
        //   app.router.push(app.localePath("/"));
        // });
        Message({ type: "error", message: "登录已过期,请重新登录!" });
        app.router.push(app.localePath("/"));
      } else if (code === 200) {
        //普通请求
        return res;
      } else if (code === 500) {
        Message({ type: "error", message: app.i18n.t(res?.msg) });
        return Promise.reject(new Error(res?.msg || "Error"));
      } else {
        return res;
      }
    },
    (error) => {
      console.log("err" + error);
      let { message } = error;
      Message({ message, type: "error", duration: 5 * 1000 });
      return Promise.reject(error);
    }
  );
}

plugins/api.js

import login from '@/api/login'
import menu from '@/api/menu'
export default function({ $axios },inject) {
    inject('loginAPI',login($axios)) //组件中使用this.$loginAPI.xxx方法名调用
    inject('menuAPI',menu($axios)) //组件中使用this.$menuAPI.xxx方法名调用
}

api/menu.js

export default function menu(request) {
  /**
   * 获取菜单
   */
  const getRouters = (lang,menuSuit=1) => {
    return request({
      url: `/system/menu/getRouters/${lang}/${menuSuit}`,
      method: "get",
    });
  };

  return {
    getRouters,
  };
}

api/login.js

export default function login(request) {
    // 注册方法
      function register(data) {
        return request({
          url: "/auth/register",
          headers: {
            isToken: false, //无需携带token
          },
          method: "post",
          data: data,
        });
      }
        // 获取用户详细信息
      function getInfo() {
        return request({
          url: "/system/user/getInfo",
          method: "get",
        });
      }
    return {
        register,
        getInfo
      };
}

nuxt.config.js 中配置

export default {
    //第一步:配置axios请求前缀
    axios: {
        // proxyHeaders: false
        baseURL: env[process.env.NODE_ENV].ENV_API 
        //...
    }
    //第二步:增加axios相关的两个配置
    plugins: [
        "~/plugins/axios.js",
        "~/plugins/api.js", 
        //...
    ],
    //第三步:模块中增加axios
    modules: ["@nuxtjs/axios", ["@nuxtjs/i18n", i18n]],
    //...
}

页面中使用

export default {
    methods: {
        async test(){
          //调用方法:plugins/api.js中的 inject('loginAPI',login($axios)) 第一个参数前面+$调用
          const res = await this.$loginAPI.register({username: 'tom',password: 123456})
          //await this.$loginAPI.getInfo()
          //this.$menuAPI.getRouters()
        }
    }
}

配置组件自动导入

nuxt.config.js

export default {
    components: true
    //...
}

配置ip和端口

nuxt.config.js

export default {
    server: {
        port: 81, // default: 3000
        host: "0.0.0.0", // default: localhost
    }
    //...
}

配置src目录

默认不使用src目录,如果需要配置像下面这样

---| node_modules/
---| nuxt.config.js
---| package.json
---| src/
------| assets/
------| components/
------| layouts/
------| middleware/
------| pages/
------| plugins/
------| static/
------| store/

则需要在nuxt.config.js中配置

export default {
    srcDir: "./src"
    //...
}

svg组件封装

封装svg组件,方便后续svg图片的使用

首先安装

npm i svg-sprite-loader@6.0.11 -D

在nuxt.config.js中配置

const path = require('path'); //导入path模块
export default {
    build: {
        vendor: ["element-ui"],
        extend(config, ctx) {
          // ...
          const svgRule = config.module.rules.find((rule) =>
            rule.test.test(".svg")
          );
          //这里将svg图片拷贝到 》 根目录/static/svg文件夹中
          svgRule.exclude = [path.resolve(__dirname, "./static/svg")];
          config.module.rules.push({
            test: /\.svg$/,
            include: [path.resolve(__dirname, "./static/svg")],
            loader: "svg-sprite-loader",
            options: {
              symbolId: "icon-[name]",
            },
          });
        },
    },
    //...
}

新建 components/SvgIcon.vue 组件

<!-- nuxt中使用svg https://blog.csdn.net/qq_26373925/article/details/108405038 -->
<template>
    <svg :class="svgClass" aria-hidden="true">
        <use :xlink:href="iconName" />
    </svg>
</template>
  
<script>
export default {
    name: 'SvgIcon',
    props: {
        iconClass: {
            type: String,
            required: true
        },
        className: {
            type: String,
            default: ''
        }
    },
    computed: {
        iconName() {
            console.log('iconClass', this.iconClass)
            return `#icon-${this.iconClass}`
        },
        svgClass() {
            return this.className ? 'svg-icon ' + this.className : 'svg-icon'
        }
    }
}
</script>
  
<style scoped>
.svg-icon {
    width: 1em;
    height: 1em;
    font-size: 1.2em;
    vertical-align: -0.15em;
    fill: currentColor;
    overflow: hidden;
}
</style>

页面中使用

<template>
    <div>
        <!--dashboard 对应 static/svg/dashboard.svg-->
        <svg-icon icon-class="dashboard" /> <span slot="title">首页</span>
    </div>
</template>
<script>
export default {}
</script>
<style scoped></style>

静态资源引入

static目录和assets目录,都是用来存储静态文件的

assets中的资源会被webpack处理,打包后会在dist中合并成一个文件;static中的资源不会被webpack处理,打包后直接复制到dist(默认是dist/static)下

推荐assets中存放自己的资源(css、images、utils等),static中放第三方资源(pdf.js、iconfont等)

动态绑定中,assets的图片会加载失败,因为webpack使用commonJS规范,需要使用require引入图片(可以通过import的方式引入)

assets 需要 require引入

<!--assets/images/zh.svg-->
<img :src="require('@/assets/images/zh.svg')" />

static直接映射到服务器根目录,可以通过项目根URL访问

<!--static/images/pay/ali.png-->
<img src="/images/pay/ali.png" width="40">

集成tailwindcss

安装

npm i @nuxtjs/tailwindcss@6.10.1

项目根目录 创建 tailwind.config.js

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [],
  theme: {
    extend: {},
  },
  plugins: [],
}

项目根目录 创建 assets/css/tailwind.css

@tailwind base;
@tailwind components;
@tailwind utilities;

在nuxt.config.js中配置

export default {
    buildModules: ['@nuxtjs/tailwindcss'],
    tailwindcss: {
        cssPath: '~/assets/css/tailwind.css',
        configPath: 'tailwind.config.js',
        exposeConfig: false,
        config: {}
    },
    //...
}

nuxt2项目模板

自建nuxt2项目模板 nuxt2-template

------ 本文结束,感谢您的阅读 ------
本文作者: 贺刘芳
版权声明: 本文采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。