简介
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