乐趣区

服务端预渲染之Nuxt – (爬坑篇)

Nuxt 是解决 SEO 的比较常用的解决方案,随着 Nuxt 也有很多坑,每当突破一个小技术点的时候,都有很大的成就感,在这段时间里着实让我痛并快乐着。在这里根据个人学习情况,所踩过的坑做了一个汇总和总结。
Nuxt 开发跨域
项目可以使用 Nginx 来反向代理,将外来的请求(这里也注意下将 Linux 的防火墙放行相应端口)转发的内部 Nuxt 默认的 3000 端口上,最简单的配置文件如下:
nuxtjs.config.js
{
modules: [
‘@nuxtjs/axios’,
‘@nuxtjs/proxy’
],
proxy: [
[
‘/api’,
{
target: ‘http://localhost:3001’, // api 主机
pathRewrite: {‘^/api’ : ‘/’}
}
]
]
}
@nuxtjs/proxy 需要手动单独安装。
Nuxt Store 使用
在 Nuxt 中使用 Vuex 跟传统在 Vue 中使用 Vuex 还不太一样,首先 Nuxt 已经集成了 Vuex,不需要我们进行二次安装,直接引用就好,在默认 Nuxt 的框架模板下有一个 Store 的文件夹,就是我们用来存放 Vuex 的地方。
Nuxt 官方也提供了相关文档, 可以简单的过一下,但是官方文档我看来比较潦草。
根据官方文档在 store 文件下面创建两个.js 文件,分别是 index.js 和 todo.js。并在 pages 文件夹下面创建 index.vue。
store – index.js
export const state = () => ({
counter: 0
})
export const mutations = {
increment (state) {
state.counter++
}
}
store – todo.js
export const state = () => ({
list: []
})
export const mutations = {
add (state, text) {
state.list.push({
text: text,
done: false
})
},
remove (state, { todo}) {
state.list.splice(state.list.indexOf(todo), 1)
},
toggle (state, todo) {
todo.done = !todo.done
}
}
pages – index.vue
<template>
<section class=”container”>
<div>
<h2 @click=”$store.commit(‘increment’)”>{{counter}}</h2>
<ul>
<li v-for=”(item,index) of list”
:key=”index”>{{item.text}}</li>
</ul>
</div>
</section>
</template>

<script>
import Logo from ‘~/components/Logo.vue’
import {mapState} from “vuex”;
export default {
components: {
Logo
},
computed:{
…mapState([“counter”]),
…mapState(“todos”,{
list:state => state.list
})
},
created(){
for(let i =0;i<10;i++){
this.$store.commit(“todos/add”,i);
}
console.log(this.list)
}
}
</script>
在 Nuxt 中可以直接使用 this.$store,并且是默认启用命名空间的。再看一下 computed 中的代码,在使用 mapState 的时候,counter 属性是直接获取出来的,然而 todos 属性则是通过命名空间才获取到的。这又是怎么回事?
Nuxt 把 store 中的 index.js 文件中所有的 state、mutations、actions、getters 都作为其公共属性挂载到了,store 实例上,然而其他的文件则是使用的是命名空间,其对应的命名空间的名字就是其文件名。
运行项目的时候可以在.nuxt 文件夹内找到 store.js 看下是怎么完成的。简单的解释一下代码作用,以及做什么用的。
.nuxt – store.js
// 引入 vue
import Vue from ‘vue’
// 引入 vuex
import Vuex from ‘vuex’
// 作为中间件
Vue.use(Vuex)
// 保存 console 函数
const log = console
// vuex 的属性
const VUEX_PROPERTIES = [‘state’, ‘getters’, ‘actions’, ‘mutations’]
// store 属性容器
let store = {}
// 没有返回值的自执行函数
void (function updateModules() {
// 初始化根数据,也就是上面所说的 index 文件做为共有数据
store = normalizeRoot(require(‘@/store/index.js’), ‘store/index.js’)
// 如果 store 是函数,提示异常,停止执行
if (typeof store === ‘function’) {
// 警告:经典模式的商店是不赞成的,并将删除在 Nuxt 3。
return log.warn(‘Classic mode for store is deprecated and will be removed in Nuxt 3.’)
}
// 执行存储模块
// store – 模块化
store.modules = store.modules || {}
// 解决存储模块方法
// 引入 todos.js 文件,即数据
// ‘todos.js’ 文件名
resolveStoreModules(require(‘@/store/todos.js’), ‘todos.js’)

// 如果环境支持热重载
if (process.client && module.hot) {
// 无论何时更新 Vuex 模块
module.hot.accept([
‘@/store/index.js’,
‘@/store/todos.js’,
], () => {
// 更新的根。模块的最新定义。
updateModules()
// 在 store 中触发热更新。
window.$nuxt.$store.hotUpdate(store)
})
}
})()

// 创建 store 实例
// – 如果 store 是 function 则使用 store
// – 否则创建一个新的实例
export const createStore = store instanceof Function ? store : () => {
// 返回实例
return new Vuex.Store(Object.assign({
strict: (process.env.NODE_ENV !== ‘production’)
}, store))
}
// 解决存储模块方法
// moduleData – 导出数据
// filename – 文件名
function resolveStoreModules(moduleData, filename) {
// 获取导出数据,为了解决 es6(export default)导出
moduleData = moduleData.default || moduleData
// 远程 store src + 扩展 (./foo/index.js -> foo/index)
const namespace = filename.replace(/\.(js|mjs|ts)$/, ”)
// 空间名称
const namespaces = namespace.split(‘/’)
// 模块名称(state,getters 等)
let moduleName = namespaces[namespaces.length – 1]
// 文件路径
const filePath = `store/${filename}`
// 如果 moduleName === ‘state’
// – 执行 normalizeState – 正常状态
// – 执行 normalizeModule – 标准化模块
moduleData = moduleName === ‘state’
? normalizeState(moduleData, filePath)
: normalizeModule(moduleData, filePath)
// 如果是(state,getters 等)执行
if (VUEX_PROPERTIES.includes(moduleName)) {
// module 名称
const property = moduleName
// 存储模块 // 获取存储模块
const storeModule = getStoreModule(store, namespaces, { isProperty: true})
// 合并属性
mergeProperty(storeModule, moduleData, property)
// 取消后续代码执行
return
}
// 特殊处理 index.js
// 模块名称等于 index
const isIndexModule = (moduleName === ‘index’)
// 如果等于
if (isIndexModule) {
// 名称空间弹出最后一个
namespaces.pop()
// 获取模块名称
moduleName = namespaces[namespaces.length – 1]
}
// 获取存储模块
const storeModule = getStoreModule(store, namespaces)
// 遍历 VUEX_PROPERTIES
for (const property of VUEX_PROPERTIES) {
// 合并属性
// storeModule – 存储模块
// moduleData[property] – 存储模块中的某个属性数据
// property – 模块名称
mergeProperty(storeModule, moduleData[property], property)
}
// 如果 moduleData.namespaced === false
if (moduleData.namespaced === false) {
// 删除命名空间
delete storeModule.namespaced
}
}
// 初始化根数据
// moduleData – 导出数据
// filePath – 文件路径
function normalizeRoot(moduleData, filePath) {
// 获取导出数据,为了解决 es6(export default)导出
moduleData = moduleData.default || moduleData
// 如果导入的数据中存在 commit 方法,则抛出异常
// – 应该导出一个返回 Vuex 实例的方法。
if (moduleData.commit) {
throw new Error(`[nuxt] ${filePath} should export a method that returns a Vuex instance.`)
}
// 如果 moduleData 不是函数,则使用空队形进行合并处理
if (typeof moduleData !== ‘function’) {
// 避免键入错误: 设置在覆盖顶级键时只有 getter 的属性
moduleData = Object.assign({}, moduleData)
}
// 对模块化进行处理后返回
return normalizeModule(moduleData, filePath)
}
// 正常状态
// – 模块数据
// – 文件路径
function normalizeState(moduleData, filePath) {
// 如果 moduleData 不是 function
if (typeof moduleData !== ‘function’) {
// 警告提示
// ${filePath} 应该导出一个返回对象的方法
log.warn(`${filePath} should export a method that returns an object`)
// 合并 state
const state = Object.assign({}, moduleData)
// 以函数形式导出 state
return () => state
}
// 对模块化进行处理
return normalizeModule(moduleData, filePath)
}
// 对模块化进行处理
// moduleData – 导出数据
// filePath – 文件路径
function normalizeModule(moduleData, filePath) {
// 如果 module 数据的 state 存在并且不是 function 警告提示
if (moduleData.state && typeof moduleData.state !== ‘function’) {
//“state”应该是返回 ${filePath} 中的对象的方法
log.warn(`’state’ should be a method that returns an object in ${filePath}`)
// 合并 state
const state = Object.assign({}, moduleData.state)
// 覆盖原有 state 使用函数返回
moduleData = Object.assign({}, moduleData, { state: () => state })
}
// 返回初始化数据
return moduleData
}
// 获取 store 的 Model
// – storeModule store 数据模型
// – namespaces 命名空间名称数组
// – 是否使用命名空间 默认值 为 false
function getStoreModule(storeModule, namespaces, { isProperty = false} = {}) {
// 如果 namespaces 不存在,启动命名空间,命名空间名称长度 1
if (!namespaces.length || (isProperty && namespaces.length === 1)) {
// 返回 model
return storeModule
}
// 获取命名空间名称
const namespace = namespaces.shift()
// 保存命名空间中的数据
storeModule.modules[namespace] = storeModule.modules[namespace] || {}
// 启用命名空间
storeModule.modules[namespace].namespaced = true
// 添加命名数据
storeModule.modules[namespace].modules = storeModule.modules[namespace].modules || {}
// 递归
return getStoreModule(storeModule.modules[namespace], namespaces, {isProperty})
}
// 合并属性
// storeModule – 存储模块
// moduleData – 存储模属性数据
// property – 模块名称
function mergeProperty(storeModule, moduleData, property) {
// 如果 moduleData 不存在推出程序
if (!moduleData) return
// 如果 模块名称 是 state
if (property === ‘state’) {
// 把 state 数据分到模块空间内
storeModule.state = moduleData || storeModule.state
} else {
// 其他模块
// 合并到对应的模块空间内
storeModule[property] = Object.assign({}, storeModule[property], moduleData)
}
}
以上就是编译后的 store 文件,大致的意思就是对 store 文件进行遍历处理,根据不同的文件使用不同的解决方案,使用命名空间挂载 model。
页面 loading
Nuxt 有提供加载 Loading 组件,一下是配置。
nuxtjs.config.js
module.exports = {
loading: {color: ‘#3B8070’}
}
Nuxt 提供的 loading 不能满足项目需求,可能有的项目不需要这样加载动画,so~,就需要自己手动配置一个。添加一个 loading 组件 (官方示例如下,详情可看官方文档) 引用该组件。
nuxtjs.config.js
module.exports = {
loading: ‘~components/loading.vue’
}
一个小插曲在 Nuxt 中,~ 与 @都指向的是根目录。
components/loading.vue
<template lang=”html”>
<div class=”loading-page” v-if=”loading”>
<p>Loading…</p>
</div>
</template>

<script>
export default {
data: () => ({
loading: false
}),
methods: {
start () {
this.loading = true
},
finish () {
this.loading = false
}
}
}
</script>
第三方组件库
项目开发过程中,难免会用到组件库,与在 Vue 中使用的时候是太一样的,需要添加一些依赖才能正常使用。
plugins – element-ui.js
import Vue from ‘vue’;
import Element from ‘element-ui’;
import locale from ‘element-ui/lib/locale/lang/en’;
export default () => {
Vue.use(Element, { locale})
};
nuxtjs.config.js
module.exports = {
css: [
‘element-ui/lib/theme-chalk/index.css’
],
plugins: [
‘@/plugins/element-ui’,
‘@/plugins/router’
]
};
使用中间件
中间件 Nuxt 没有给出具体的使用文档,而是放入了一个编辑器。这一点我感觉到了一丝丝的 差异。为什么要这样。。。简单的研究了一下,弄明白了大概。
在 middleware 中创建想要的中间件。这里借用一下官网的例子。
middleware – visits.js
export default function ({store, route, redirect}) {
store.commit(‘ADD_VISIT’, route.path)
}
向上面这样就创建好了一个中间件,但是应该怎么使用呢?在使用的时候有两种方式,一种是全局使用,另一种是在页面中单独使用,文件名会作为其中间件的名称。
++ 全局使用 ++
nuxtjs.config.js
export default {
router: {
middleware: [‘visits’]
}
}
页面中单独使用
export default {
middleware: ‘auth’
}
官网中在页面中的 asyncData 中有一段这样的代码。
export default {
asyncData({store, route, userAgent}) {
return {
userAgent
}
}
}
持续更新。。。
总结
Nuxt 的学习曲线非常小,就像 Vue 框架一样,已经是一个开箱即用的状态,我们可以直接跨过配置直接开发。对配置有兴趣的可以在 Vue 官方文档找到 SSR 渲染文档。

退出移动版