家喻户晓,Vue SPA 单页面利用对 SEO 不敌对,当然也有相应的解决方案。服务端渲染 (SSR) 就是罕用的一种。SSR 有利于 搜索引擎优化 (SEO, Search Engine Optimization),并且 内容达到工夫 (time-to-content)(或称之为 首屏渲染时长)也有很大的优化空间。
Nuxt.js 是一个基于 Vue.js
的轻量级利用框架, 可用来创立 服务端渲染 (SSR)
利用, 也可充当动态站点引擎生成动态站点利用, 具备优雅的代码构造分层和热加载等个性。
我的项目地址:明么的博客
初始化我的项目
运行 create-nuxt-app
通过 Nuxt 官网提供的脚手架工具 create-nuxt-app 初始化我的项目:
$ npx create-nuxt-app < 我的项目名 >
// 或者
$ yarn create nuxt-app < 我的项目名 >
我的项目配置
我的项目创立的时候会让你进行一些配置的抉择, 可依据本人须要进行抉择。
我的项目运行
运行完后,它将装置所有依赖项,下一步是启动我的项目:
$ cd <project-name>
$ yarn dev
在浏览器中,关上 http://localhost:3000
目录构造
.
├── assets // 用于组织未编译的动态资源
├── components // 用于组织利用的 Vue.js 组件
├── layouts // 用于组织利用的布局组件
├── middleware // 用于寄存利用的中间件
├── node_modules
├── pages // 用于组织利用的路由及视图
├── plugins // 组织插件。├── static // 用于寄存利用的动态文件
├── store // 状态治理
├── nuxt.config.js // 配置文件
├── package.json
├── jsconfig.json
├── stylelint.config.js
├── README.md
└── yarn.lock
我的项目开发
我的项目启动之后,咱们就能够进行开发阶段了。
创立页面
在 pages
创立页面文件:
pages/
└── article/
├── index.vue
├── _category/
│ └── index.vue
└── detail/
└── _articleId.vue
Nuxt.js 预设了利用 Vue.js 开发服务端渲染的利用所须要的各种配置。所以不须要再装置 vue-router
了,他会根据 pages
目录构造主动生成 vue-router
模块的路由配置。页面之间应用路由,官网举荐应用 <nuxt-link>
标签,与 <router-link>
的应用形式是一样的。下面创立的目录构造将会生成对应的路由配置表:
router: {
routes: [
{
name: 'article',
path: '/article',
component: 'pages/article/index.vue'
},
{
name: "article-category"
path: "/article/:category",
component: 'pages/article/_category/index.vue',
},
{
name: "article-detail-articleId"
path: "/article/detail/:articleId",
component: 'pages/article/detail/_articleId.vue'
}
]
}
组件局部
组件这一块划分为 base
、framework
、page
三个目录:
components/
├── base 根本组件
├── framework 布局相干组件
└── page/ 各个页面下的组件
├── home
└── ...
这里须要留神在开发 VUE SPA
利用时咱们有时候会把页面组件放在 pages
下,我将页面下的组件全副放到了 components
下,因为 Nuxt.js
框架会读取 pages
目录下所有的 .vue
文件并主动生成对应的路由配置。
资源的寄存
官网介绍的很具体,资源的寄存有两个目录:static
、assets
static
: 用于寄存利用的动态文件,此类文件不会调用 Webpack
进行构建编译解决。服务器启动的时候,该目录下的文件会映射至利用的根门路 /
下。
举个例子: /static/banner.png
映射至 /banner.png
assets
: 用于组织未编译的动态资源如 LESS
、SASS
或 JS
。
别名
别名 | 目录 |
---|---|
~ 或 @ | srcDir |
~~ 或 @@ | rootDir |
为了不便援用,nuxt 提供了两个别名,如果你须要引入 assets
或者 static
目录, 应用 ~/assets/your_image.png
和 ~/static/your_image.png
形式。
全局款式
这里我选用 LESS
预处理语言,装置:
$ yarn add less less-loader -D
在 assets/css/
创立 .less
文件, 通过一个文件引入:
// assets/css/index.less
@import './normalize.less';
@import './reset.less';
@import './variables.less';
@import './common.less'
在 nuxt.config.js
中引入
export default {
...
css: ['~/assets/css/index.less'],
...
}
LESS 全局变量
在应用预处理语言的时候,咱们必定会应用到变量,以不便对立治理色彩、字体大小等。
首先定义好变量文件 variables.less
/* ===== 主题色配置 ===== */
@colorPrimary: #6bc30d;
@colorAssist: #2db7f5;
@colorSuccess: #67c23a;
@colorWarning: #e6a23c;
@colorError: #f56c6c;
@colorInfo: #909399;
装置:
$ yarn add @nuxtjs/style-resources -D
在 nuxt.config.js
中减少配置:
export default {
...
modules: [
// https://go.nuxtjs.dev/axios
'@nuxtjs/axios',
'@nuxtjs/style-resources',
],
styleResources: {
// your settings here
// sass: [],
// scss: [],
// stylus: [],
less: ['~/assets/css/variables.less'],
},
...
}
布局 layouts
我的博客大略分为这几种布局形式:
在这里我创立了三种布局组件:
layouts/
├── admin.vue // 上图第四个
├── default.vue // 上图第一个和第三个只蕴含 nav 和 footer
└── user.vue // 上图第二个
admin.vue
: 后盾治理模块的布局user.vue
: 集体核心模块的布局default.vue
: 默认的布局
拿 default.vue
举例,我把 导航 和 页脚 放到了一个组件 AppLayout
中:
<!-- layouts/default.vue -->
<template>
<app-layout>
<nuxt />
</app-layout>
</template>
<script>
import AppLayout from '@/components/framework/AppLayout/AppLayout'
export default {
name: 'AppLayoutDefault',
components: {AppLayout}
}
</script>
而后在页面中应用:
<!-- pages/index.vue -->
<template>
<!-- Your template -->
</template>
<script>
export default {
layout: 'default'
// 指定布局, 不指定的话将会应用默认布局:layouts/default.vue
// 其实我这里指不指定都能够哈哈。}
</script>
对于页面上路由的跳转,官网举荐应用 <nuxt-link>
,这里 <nuxt-link>
和 <a>
还是有区别的,nuxt-link
走的是 vue-router 的路由,即页面为单页面,浏览器不会重定向。而 <a>
标签走的是 window.location.href
,每次点击 a
标签后页面,都会进行一次服务端渲染。
全局过滤器
在 plugins/
目录下,新建 filters.js
,比如说咱们要对工夫进行一个格式化解决 :
Day.js:一个轻量的解决工夫和日期的 JavaScript 库
$ yarn add dayjs
import Vue from 'vue'
import dayjs from 'dayjs'
// 工夫格式化
export function dateFormatFilter(date, fmt) {if (!date) {return ''} else {return dayjs(date).format(fmt)
}
}
const filters = {dateFormatFilter}
Object.keys(filters).forEach((key) => {Vue.filter(key, filters[key])
})
export default filters
而后,在 nuxt.config.js
中配置,
export default {
...
plugins: ['~/plugins/filters.js']
...
}
自定义指令
在 plugins/directive/focus
目录下,增加 index.js
:
import Vue from 'vue';
const focus = Vue.directive('focus', {inserted(el) {el.focus();
},
});
export default focus;
自定义指令和全局过滤器一样,都须要在 nuxt.config.js
增加配置:
export default {
...
plugins: [
'~/plugins/filters.js',{src: '~/plugins/directive/focus/index.js', ssr: false},
],
...
}
head 配置 SEO
通过应用 head
办法设置以后页面的头部标签。
<template>
<h1>{{title}}</h1></template>
<script>
export default {
...
head() {
return {
title: '明么的博客',
meta: [
{
hid: 'description',
name: 'description',
content: 'My custom description'
}
]
}
}
}
</script>
留神:为了防止子组件中的 meta 标签不能正确笼罩父组件中雷同的标签而产生反复的景象,倡议利用 hid 键为 meta 标签配一个惟一的标识编号。
如果页面比拟多的话,每个页面都须要写 head 对象,就会有些的繁琐。能够借助 nuxt
的 plugin
机制,将其封装成一个函数,并注入到每一个页面当中:
// plugins/head.js
import Vue from 'vue'
Vue.mixin({
methods: {$seo(title, content) {
return {
title,
meta: [{
hid: 'description',
name: 'description',
content
}]
}
}
}
})
在 nuxt.config.js
中减少配置:
export default {
...
plugins: [
'~/plugins/filters.js',{src: '~/plugins/directive/focus/index.js', ssr: false},
'~/plugins/head.js'
],
...
}
在页面中应用:
head() {return this.$seo(this.detail.title, this.detail.summary)
}
axios 申请数据
申请数据,在初始化我的项目的时候曾经抉择了 Axios,就不须要再另行装置了,能够查看 nuxt.config.js
中曾经配置好了:
export default {
...
modules: [
// https://go.nuxtjs.dev/axios
'@nuxtjs/axios',
...
],
...
}
页面中通过 this.$axios.$get
来获取数据,不须要在每个页面都独自引入 axios
.
然而一般来说咱们会对 axios
做一下封装,集中处理一些数据或者是错误信息。
在 plugins
目录下新建 axios.js
和 api-repositories.js
, 上面是我的一些简略的配置:
// plugins/axios.js
import qs from 'qs'
export default function(ctx) {const { $axios, store, app} = ctx
// $axios.defaults.timeout = 0;
$axios.transformRequest = [(data, header) => {if (header['Content-Type'] && header['Content-Type'].includes('json')) {return JSON.stringify(data)
}
return qs.stringify(data, { arrayFormat: 'repeat'})
}
]
$axios.onRequest((config) => {
const token = store.getters.token
if (token) {config.headers.Authorization = `Bearer ${token}`
}
// 如果是 get 申请,参数序列化
if (config.method === 'get') {config.paramsSerializer = function(params) {return qs.stringify(params, { arrayFormat: 'repeat'}) // params 是数组类型如 arr=[1,2],则转换成 arr=1&arr=2
}
}
return config
})
$axios.onRequestError((error) => {console.log('onRequestError', error)
})
$axios.onResponse((res) => {// ["data", "status", "statusText", "headers", "config", "request"]
// 如果 后端返回的码失常 则 将 res.data 返回
if (res && res.data) {if (res.headers['content-type'] === 'text/html') {return res}
if (res.data.code === 'success') {return res} else {return Promise.reject(res.data)
}
}
})
$axios.onResponseError((error) => {console.log('onResponseError', error)
})
$axios.onError((error) => {console.log('onError', error)
if (error && error.message.indexOf('401') > 1) {app.$toast.error('登录过期了,请从新登录!')
sessionStorage.clear()
store.dispatch('changeUserInfo', null)
store.dispatch('changeToken', '')
} else {app.$toast.show(error.message)
}
})
}
// plugins/api-repositories.js
export default ({$axios}, inject) => {
const repositories = {GetCategory: (params, options) => $axios.get('/categories', params, options),
PostCategory: (params, options) => $axios.post('/categories', params, options),
PutCategory: (params, options) => $axios.put(`/categories/${params.categoryId}`, params, options),
DeleteCategory: (params, options) => $axios.delete(`/categories/${params.categoryId}`, params, options)
...
}
inject('myApi', repositories)
}
而后在 nuxt.config.js
中减少配置:
export default {
...
plugins: [
...
{src: '~/plugins/axios.js', ssr: true},
{src: '~/plugins/api-repositories.js', ssr: true},
],
/*
** Axios module configuration
** See https://axios.nuxtjs.org/options
*/
axios: {baseURL: 'http://localhost:5000/'},
}
这样就能够间接在页面中应用了:
this.$myApi.GetCategory()
proxy 代理
应用 proxy 解决跨域问题:
$ yarn add @nuxtjs/proxy
在 nuxt.config.js
中减少配置, 上面是我的配置:
export default {
...
modules: [
...
'@nuxtjs/proxy',
...
],
axios: {
proxy: true,
headers: {
'Access-Control-Allow-Origin': '*',
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': 'application/json; charset=UTF-8'
},
prefix: '/api',
credentials: true
},
/*
** 配置代理
*/
proxy: {
'/api': {
target: process.env.NODE_ENV === 'development' ? 'http://localhost:5000/' : 'http://localhost:5000/',
changeOrigin: true,
pathRewrite: {'^/api': ''}
},
'/douban/': {
target: 'http://api.douban.com/v2',
changeOrigin: true,
pathRewrite: {'^/douban': ''}
},
...
},
}
在单页面开发中,打包公布上线还须要 nginx
代理能力实现跨域,在 nuxt
中,打包公布上线之后,申请是在服务端发动的,不存在跨域问题,所以不须要在另外再做 nginx
代理。
asyncData
该办法是 Nuxt
一大卖点, asyncData
办法会在组件(限于页面组件)每次加载之前被调用。它能够在服务端或路由更新之前被调用,服务端渲染的能力就在这里。
留神:因为
asyncData
办法是在组件 初始化 前被调用的,所以在办法内是没有方法通过this
来援用组件的实例对象。
另外提及一点,当 asyncData
在服务端执行时,是没有 document
和 window
对象的。
asyncData
第一个参数被设定为以后页面的上下文对象,能够利用 asyncData
办法来获取数据,Nuxt.js
会将 asyncData
返回的数据交融组件 data
办法返回的数据一并返回给以后组件。
export default {asyncData (ctx) {
ctx.app // 根实例
ctx.route // 路由实例
ctx.params // 路由参数
ctx.query // 路由问号前面的参数
ctx.error // 错误处理办法
}
}
服务端渲染:
export default {data () {return { categoryList: [] };
},
async asyncData({app}) {const res = await app.$myApi.GetCategory();
return {categoryList: res.result.list};
},
}
asyncData 渲染出错
在应用 asyncData
时可能因为服务器谬误或 api 谬误导致页面无奈渲染,针对这种状况的呈现,咱们还须要做一下解决。nuxt
提供了 context.error
办法用于错误处理,在 asyncData
中调用该办法即可跳转到谬误页面。
export default {async asyncData({ app, error}) {app.$myApi.GetCategory()
.then(res => {return { categoryList: res.result.list}
})
.catch(e => {error({ statusCode: 500, message: '服务器出错了啦~'})
})
},
}
当出现异常时会跳转到默认的谬误页,谬误页面能够通过 /layout/error.vue
自定义。
context.error
的参数必须是相似{statusCode: 500, message: '服务器开了个小差~'}
,statusCode
必须是http
状态码
为了不便,全局对立处理错误办法,在 plugins
目录下创立 ctx-inject.js
:
// plugins/ctx-inject.js
export default (ctx, inject) => {ctx.$errorHandler = (err) => {
try {
const res = err.data
if (res) {
// 因为 nuxt 的谬误页面只能辨认 http 的状态码,因而 statusCode 对立传 500,示意服务器异样。ctx.error({statusCode: 500, message: res.resultInfo})
} else {ctx.error({ statusCode: 500, message: '服务器出错了啦~'})
}
} catch {ctx.error({ statusCode: 500, message: '服务器出错了啦~'})
}
}
}
而后,在 nuxt.config.js
中减少配置:
export default {
...
plugins: [
...
'~/plugins/ctx-inject.js',
...
],
...
}
在页面中应用:
export default {data() {return { categoryList: [] }
},
async asyncData(ctx) {const { app} = ctx
// 尽量应用 try catch 的写法,将所有异样都捕捉到
try {const res = await app.$myApi.GetCategory()
return {categoryList: res.result.list,}
} catch (err) {ctx.$errorHandler(err)
}
},
}
fetch
fetch
办法用于在渲染页背后填充利用的状态树(store)数据,与 asyncData
办法相似,不同的是它不会设置组件的数据。它会在组件每次加载前被调用(在服务端或切换至指标路由之前)。和 asyncData
一样,第一个参数也是页面的上下文对象,同样无奈在外部应用 this
来获取组件实例。
<template>
...
</template>
<script>
export default {async fetch({ app, store, params}) {let res = await app.$myApi.GetCategory()
store.commit('setCategory', res.result.list)
}
}
</script>
store
在 nuxt
中应用状态治理,只须要在 store/
目录下创立文件即可。
store/
├── actions.js
├── getters.js
├── index.js
├── mutations.js
└── state.js
// store/actions.js
const actions = {changeToken({ commit}, token) {commit('setToken', token)
},
...
}
export default actions
// store/getters.js
export const token = (state) => state.token
export const userInfo = (state) => state.userInfo
...
// store/mutations.js
const mutations = {setToken(state, token) {state.token = token},
...
}
export default mutations
// store/state.js
const state = () => ({
token: '',
userInfo: null,
...
})
export default state
// store/index.js
import state from './state'
import * as getters from './getters'
import actions from './actions'
import mutations from './mutations'
export default {
state,
getters,
actions,
mutations
}
无论应用那种模式,您的
state
的值应该始终是function
,为了防止返回援用类型,会导致多个实例相互影响。
构建部署
开发结束后,就能够进行打包部署了,一般来说先在本地测试一下:
$ yarn build
$ yarn start
而后,云服务器装置 node
环境 和 pm2
。
减少 pm2
配置,在 server/
目录下,新建 pm2.config.json
文件:
{
"apps": [
{
"name": "my-blog",
"script": "./server/index.js",
"instances": 0,
"watch": false,
"exec_mode": "cluster_mode"
}
]
}
而后,在 package.json
中 scripts
配置命令:
{
"scripts": {
...
"pm2": "cross-env NODE_ENV=production pm2 start ./server/pm2.config.json",
}
}
把咱们我的项目中 .nuxt
, static
, package.json
, nuxt.config.js
, yarn.lock
或者是 package.lock
上传到服务器。进入上传的服务器目录,装置依赖:
$ yarn install
而后,运行:
$ npm run pm2
在设置服务器凋谢 3000 端口后,就能够通过端口拜访了。前面加个端口号总归是不适合,还须要应用 nginx
代理到默认端口 80(http) 或 433(https)。
记录一个小问题:3000 端口没问题,我的项目启动也失常,通过
http://60.***.***.110:3000
就是拜访不了。在nuxt.config.js
减少:
{
...
server: {
port: 3000,
host: '0.0.0.0'
}
}
重新启动我的项目即可。