简介
nuxt是基于vue语法来实现SSR(服务器渲染)的框架,解决单页应用不利于SEO的问题
创建项目
由于国内网络问题经常下载失败,推荐采用沙盒下载环境项目模板
https://stackblitz.com/github/nuxt/starter/tree/v2-stackblitz?file=README.md

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

然后安装依赖
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()

中间件
在请求到达之前做一些处理、比如重定向、鉴权等操作
路由中间件
新建 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

