众所周知,Vue SPA单页面应用对SEO不友好,固然也有相应的解决方案。 服务端渲染 (SSR) 就是经常使用的一种。 SSR 有利于 搜索引擎优化(SEO, Search Engine Optimization) ,而且 内容到达时间(time-to-content) (或称之为首屏渲染时长)也有很大的优化空间。css
Nuxt.js 是一个基于 Vue.js
的轻量级应用框架,可用来建立 服务端渲染 (SSR)
应用,也可充当静态站点引擎生成静态站点应用,具备优雅的代码结构分层和热加载等特性。html
项目地址:明么的博客vue
经过 Nuxt 官方提供的脚手架工具 create-nuxt-app 初始化项目:node
$ npx create-nuxt-app <项目名>
// 或者ios
$ yarn create nuxt-app <项目名>
项目建立的时候会让你进行一些配置的选择,可根据本身须要进行选择。nginx
运行完后,它将安装全部依赖项,下一步是启动项目:git
$ cd <project-name> $ yarn dev
在浏览器中,打开 http://localhost:3000github
. ├── 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
项目启动以后,咱们就能够进行开发阶段了。vue-router
在pages
建立页面文件:npm
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'], ... }
在使用预处理语言的时候,咱们确定会使用到变量,以方便统一管理颜色、字体大小等。
首先定义好变量文件 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/ ├── 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
方法设置当前页面的头部标签。
<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,就不须要再另行安装了,能够查看 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 解决跨域问题:
$ 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
代理。
该方法是 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
时可能因为服务器错误或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
方法用于在渲染页面前填充应用的状态树(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>
在 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' } }
从新启动项目便可。