上次和大家分享了VUE同构赋能 VUE SSR 篇,链接在这里
juejin.cn/post/684490…
今天是下篇:NUXT
上篇有可能讲的太复杂了😂,这次咋们直奔主题
还是上次的 DEMO,我们用NUXT来实现这个页面
github地址在文章末尾

Nuxt
Nuxt是基于Vue ssr之上,集成了Vue-Router,Vuex,webpack等框架、组件的一个服务端渲染框架
在我看来,Nuxt就是一个升级版的Vue ssr,为我们预设了服务端渲染的应用所需要的各种配置
但是相应的,Nuxt的入侵性是特别高的,我们需要理解Nuxt的思路,才能发挥它的优势。
目录结构
首先我们来看下Nuxt项目的目录结构

没有了前后端入口的配置,没了webpack配置
我们需要做的仅仅是"专注业务"
具体文件夹可以看到
assets - 前端资源文件
components - 前端通用组件
layouts - 前端布局组件
store - vuex
static - 静态文件
plugins - 插件
middleware - 中间件
pages - 前端VUE页面
server - 后端
其实这么多文件,可以分为三个:前端,后端,资源
约定大于配置
Nuxt中,到处体现着"约定大于配置"的思想,这一点我们要注意
比如最简单的,上面目录文件夹的名字,如果没有额外配置,是不能更改名字的
还有例如:layouts布局文件中
default.vue是默认布局页
error.vue是默认错误页面
比如pages前端页面文件夹中,约定了自动生成router的规则
还有store文件夹中,默认index.js是默认状态树,
todos.js 文件则会自动生成todos 模块
等等……
这些具体的下面会具体讲到
布局/路由
现在开始编写我们的demo,
先从layout布局页开始,我们需要写两个页面default.vue和error.vue
布局页就可以看作我们平时 vue 的 app.vue 入口页面
但是他们有不同之处,Nuxt的子页面内,可以指定布局页(未指定就是default.vue)
default.vue
<template>
<div>
<header>
<div class="head-content">
<section class="head-left">
一个帮助开发者成长的社区 |
<nuxt-link to="/"> 首页</nuxt-link>
</section>
<section class="head-right">
<nuxt-link to="/user/login"> 登录</nuxt-link>
|
<nuxt-link to="/user/register"> 注册</nuxt-link>
</section>
</div>
</header>
<nuxt />
</div>
</template>
子页面设置layout
export default {
// 默认就是default
layout: "default",
}
<nuxt>与<nuxt-link>
我们在布局页内可以看到<nuxt>与<nuxt-link> 这两个组件,有点似曾相识的感觉
其实就是对应的<router-view><router-link>,
所以布局页或者我们的嵌套路由的页面,都需要用到<nuxt>组件
我们可以看到源码内:
// nuxt
let routerView = [
h('router-view', data)
]
if (props.keepAlive) {
routerView = [
h('keep-alive', { props: props.keepAliveProps }, routerView)
]
}
return h('transition', {
props: transitionProps,
on: listeners
}, routerView)
// nuxt-link
export default {
name: 'NuxtLink',
extends: Vue.component('RouterLink'),
}
是扩展了vue-router
其中<nuxt>可接受接收 keep-alive 和 keep-alive-props
<nuxt-link>则是帮我们扩展了自动预获取代码分割页面
可以使用 no-prefetch属性 禁用
<nuxt-link to="/user" no-prefetch>个人中心</nuxt-link>
自动构建路由
路由在Nuxt中全是自动构建的,这点非常有用,会帮我们减少很多配置工作
在所有的SSR项目中,其实要做到前后端同构,都是依赖路由来判定前后端的入口,从而进行输出的
我们来看下Nuxt构建路由的规则
简单来说,我总结了三点:
- 根据目录和文件名自动构建同名路由
- 根据_下划线构建动态路由
- 根据vue文件同名文件夹来构建嵌套路由
我们根据我们的demo来看看,我们需要个人中心和列表页
所以我们的目录结构是这样的

最终编译的router.js如下:
routes: [{
path: "/user/login",
component: _fade0492,
name: "user-login"
}, {
path: "/user/register",
component: _35d3a876,
name: "user-register"
}, {
path: "/info/:pageIndex?",
component: _287821ee,
name: "info-pageIndex"
}, {
path: "/",
component: _48b188ea,
name: "index"
}]
可以看到,列表页生成了/info/:pageIndex? 这个路由
我们通过 /info/1或者/info/2 可以访问到对应页码的数据
我们再来看看 _pageIndex.vue
<template>
<div>
<vmenu type="info" />
<div class="info-container">
<div class="info-title">热门信息</div>
<ul class="info-content">
<li v-for="item in list" :key="item.title">
<section class="info-content-r">{{ item.publishDate }}</section>
<section class="info-content-l">{{ item.title }}</section>
</li>
</ul>
<vpage :count="count" url="info" :pageIndex="pageIndex" />
</div>
</div>
</template>
page.vue
<template>
<div class="page-container">
<section>
<router-link :to="{ name: toUrl, params: { pageIndex: 1 } }">
第一页
</router-link>
</section>
<section
v-for="(i, ix) in pageCount"
:key="i"
:class="{ current: pageIndex == ix + 1 }"
>
<router-link :to="{ name: toUrl, params: { pageIndex: ix + 1 } }">
{{ ix + 1 }}
</router-link>
</section>
<section>
<router-link :to="{ name: toUrl, params: { pageIndex: pageCount } }">
最后一页
</router-link>
</section>
</div>
</template>
<script>
export default {
name: "page",
props: {
pageSize: { default: 10 },
pageIndex: { default: 1 },
count: { default: 100 },
url: { default: "" }
},
computed: {
toUrl() {
return this.url + "-pageIndex";
},
pageCount() {
return this.count % this.pageSize == 0
? this.count / this.pageSize
: Math.floor(this.count / this.pageSize) + 1;
}
},
mounted() {},
created() {}
};
</script>
可以看到因为自动构建的路由名称是 info-pageIndex
所以我们分页跳转的时候也需要对应的拼接好路由
异步数据
异步数据获取是SSR项目必须的功能,在Nuxt中相当简便
asyncData
asyncData很眼熟,VUR SSR中也有同样的方法用来加载异步数据
但是,但是
在Nuxt中的asyncData方法是完全不一样的
在这里,不仅能获取异步的数据,而且还会将最终的数据挂载到data上
我们看下demo这里,从后台获取一下新闻列表:
export default {
async asyncData({
isDev,
route,
store,
env,
params,
query,
req,
res,
redirect,
error,
$axios
}) {
let data = await $axios.post(`/api/news/list`, {
page: params.pageIndex || 1
});
return { ...data.data };
},
components: {
vmenu: Menu,
vpage: page
},
// router.path是不一样的,不用监听
// watchQuery: ["pageIndex"],
data() {
return {
pageIndex: this.$route.params.pageIndex || 1
};
}
};
这里axios一定要使用Nuxt官方模块@nuxt/axios,会自动进行header填充等其他优化
总之,就是这么简单,我们再async内获取数据,然后return出来就都OK了
之后组件内就和我们平时在data内使用数据一样了
fetch
再来看看fetch,其实fetch就和Vue SSR中的asyncData很像了
它不会绑定在data内,只是用来操作store的
用法如下:
<template>
<h1>Stars: {{ $store.state.stars }}</h1>
</template>
<script>
export default {
fetch ({ store, params }) {
return axios.get('http://my-api/stars')
.then((res) => {
store.commit('setStars', res.data)
})
}
}
</script>
相较VUESSR,Nuxt中的异步数据更加纯粹,不需要借助VUEX就可以实现
store回归状态共享的本质
vuex
我们再来看看在Nuxt中vuex是怎么使用的
其实开始也说了,Nuxt约定在store目录中index.js是默认状态树
其他名称的文件,会生成对应的store模块
所以我们来写一个保存登录状态的store
store/user.js
export const state = () => {
user: null
};
export const mutations = {
// 设置登录用户
set(state, user) {
state.user = user;
},
loginOut(state) {
state.user = null;
}
}
nuxtServerInit
有个问题,我们什么时候把登录状态写进store呢?
想想平时我们的应用,写进cookie或者localstorage是比较好的方案
但是我们SSR项目有个优势,我们可以紧密的和后端结合起来
我们可以使用 nuxtServerInit 将客户的登录状态写进sotre
注意,nuxtServerInit只有服务端调用时才会执行
store/index.js
export const actions = {
nuxtServerInit({ commit }, { req }) {
if (req.session.user) {
commit("user/set", req.session.user);
}
}
}
这样就能一直维护客户的最新登录状态(当然前端登陆成功后,也要写入store)
middleware
那么前端页面怎么判断登录状态呢?
在每个路由切换的时候判断比较好,比如在router.beforeEach内
但是Nuxt路由是自动构建的,也没有相关接口
但是别急,我们有更好用的 middleware
中间件可以在我们指定的页面加载之前执行,并且可以执行异步执行
布局页内可以设置中间件,这样所有该布局的页面,都会执行该中间件
来看看demo的例子 middleware/default.js
export default function ({ route, store, redirect }) {
// 如果用户不存在,跳到登录页面
if (!store.state.user.user)
redirect('/user/login?redirect=' + route.path);
}
_pageIndex.vue页面内引用一下该中间件
middleware: "default"
引用之后,如果未登录就会自动跳转到登录页
模块/插件
Nuxt有丰富的模块可以供我们使用
官方模块有:
@nuxt/http: 基于ky-universal的轻量级和通用的HTTP请求
@nuxtjs/axios: 安全和使用简单Axios与Nuxt.js集成用来请求HTTP
@nuxtjs/pwa: 使用经过严格测试,更新且稳定的PWA解决方案来增强Nuxt
@nuxtjs/auth: Nuxt.js的身份验证模块,提供不同的方案和验证策略
社区模块就更多了:
github.com/topics/nuxt…
使用方法也很简单,直接在nuxt.config.js内 比如:
modules: [
['@nuxtjs/axios', { baseURL: 'http://localhost:3000' }],
],
当然也还还是会自己引用一些插件的,我们可以使用plugins配置
plugins: [
{ src: '@/plugins/element-ui', ssr: false }
],
注意,如果我们的插件只能在服务端运行的话,可以配置ssr:false,然后前端也可以配合<no-ssr>组件来使用
接着在plugins目录内进行扩展,这里就暴露了Vue对象给我们
import Vue from 'vue'
import Element from 'element-ui'
import locale from 'element-ui/lib/locale/lang/en'
Vue.use(Element, { locale })
生命周期
到这里,我们的demo页面就写完了
要值得注意的是Nuxt生命周期的差异
Nuxt扩展的asyncData,fetch,validate,middleware会在前后端内都执行
nuxtServerInit,serverMiddleware会在服务端执行
暴露给我们的redirect,error等方法前后端都可以使用,
但是诸如req,res之类的,前端则为undefined,这也是需要时刻注意的

缓存
最后咱们再来看看缓存
组件缓存和VUESSR一样,我们在nuxt.config.js内配置缓存项
const LRU = require('lru-cache')
module.exports = {
render: {
bundleRenderer: {
cache: LRU({
max: 1000,
maxAge: 1000 * 60 * 15
})
}
}
}
接着组件内指定好serverCacheKey就OK
export default {
props: ['type'],
serverCacheKey: props => props.type
}
serverMiddleware
页面级别的缓存的话,有点特殊,我们需要使用Nuxt的服务端中间件
这里我们再以上次讲的redis-lru来做页面缓存
const config = require('../nuxt.config.js')
module.exports = async function (req, res, next) {
if (process.env.NODE_ENV === "development") {
next();
return;
}
const redis = require('redis').createClient(config.redis);
const lru = require('redis-lru');
const cache = lru(redis, 100);
// 获取缓存的key值
let key = req.originalUrl
const value = await cache.get(key)
if (value) {
console.log('cached output', key);
return res.end(value, 'utf-8')
}
res._end = res.end
res.end = async function (data) {
if (res._headers['content-type'].startsWith('text/html')
&& res.statusCode === 200) {
await cache.set(key, data)
}
res._end(data, 'utf-8')
}
next();
}
值得注意的是服务端中间件中的req和res是 Node.js http request 对象
最后输出需要调用res的原始end方法(就算你Nuxt配置的后端服务器为koa)
结语
好了,我的VUE SSR系列上下篇都讲完了
Nuxt相较VUESSR是一个升级版,如果你是新启动项目,建议直接从Nuxt开始
如果想体验一下SSR全流程,可以从头开始配置VUESSR,会有不一样的体验
本次Nuxt的demo地址在这里: github.com/kungithub/s…