家喻户晓,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.jsimport 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.jsimport 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.jsexport 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.jsexport 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.jsconst actions = { changeToken({ commit }, token) { commit('setToken', token) }, ...}export default actions// store/getters.jsexport const token = (state) => state.tokenexport const userInfo = (state) => state.userInfo...// store/mutations.jsconst mutations = { setToken(state, token) { state.token = token }, ...}export default mutations// store/state.jsconst state = () => ({ token: '', userInfo: null, ...})export default state// store/index.jsimport 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' }}
重新启动我的项目即可。