Nuxt.js 是一个基于 Vue 可用来创建服务端渲染(SSR) Web 应用的框架。
为什么要服务器端渲染?
Web 前端的服务器端渲染(SSR)主要有以下好处:
- 更好的 SEO,搜索引擎可以爬取完全渲染的 HTML
- 更快的内容到达时间(减少访问的白屏时间)
相比非 SSR 的 Web 应用它也会带来一些缺点:
- 消耗服务器资源
- 对前端开发人员能力要求更高
Nuxt.js SSR 的流程
要做到上述优点还能保证用户交互体验,要做到在首次加载页面的时候才进行 SSR。下面的 Nuxt.js 的简要流程图反应的何时才会进行 SSR。
在浏览器进行站内导航时,无需 SSR。
完整的生命周期:https://nuxtjs.org/docs/concepts/nuxt-lifecycle/
开启服务器端渲染
Nuxt.js 也可以以单页应用模式和静态站点模式运行,默认以 SSR 模式运行,可在项目根目录下的配置文件 nuxt.config.js
中做修改
1 2 3 4 5
|
export default { ssr: true, };
|
路由 和 Pages 文件夹
Nuxt.js 框架会 根据 pages 文件夹的结构自动生成路由的规则:
1 2 3 4 5 6
| pages/ --| users.vue /users 路径下的页面的父组件,子组件渲染的地方使用 <NuxtChild/> | users/ -----| index.vue 访问路径: /users | search.vue 访问路径: /users/search _.vue 访问路径: 未匹配的路径
|
以 _
开头命名 vue 文件以定义 params, 可用在组件中以 this.$route.params
取得:
1 2 3 4
| pages/ --| users.vue --| users/ -----| _id.vue 访问路径: /users/123 获取参数 this.$route.params.id
|
⚠️ 使用 NuxtLink 组件在站内路由导航。
⚠ pages 文件夹不要写非页面组件,会生成不必要的路由配置。非页组件应写在 components 文件夹内。
问答: pages/user/list.vue 的访问路径是什么?
路程由的 base 配置
如果项目不是部署在域名根目录下,需要配置 router.base
1 2 3 4 5 6 7
|
export default { router: { base: process.env.ROUTER_BASE, }, };
|
接口请求
由于 SSR,Nuxt.js 的请求库是在服务器端实例化的。所以不能简单的像单页应用一样,封装一个请求模块导出使用。这样使 Nuxt.js 在不同用户访问时共用一个请求实例,会出现用户数据泄漏的问题。
社区提供了 @nuxt/asiox 模块来处理这个问题。
使用:
安装 @nuxt/asiox
包
配置
1 2 3 4 5 6 7 8
|
export default { modules: ["@nuxtjs/axios"], axios: { }, };
|
使用方法
https://axios.nuxtjs.org/usage
在 plugin 中添加拦截器:https://axios.nuxtjs.org/extend
Cookie 操作
为了方便在服务器和浏览器中方便和有统一的方法对 cookie 进行操作,我们可以使用社区提供的 cookie-universal-nuxt
包。
NPM
1 2 3 4 5 6 7 8 9
|
export default { modules: [ ['cookie-universal-nuxt', { } ] }
|
它能在 Nuxt.js 的上下文 context
的 app
和 Vue 组件实例中添加 $cookies
对象,其中有对 cookie 的操作方法:
1 2 3 4 5 6 7 8 9 10 11 12 13
| export default ({ app }) => { app.$cookies.set("cookie-name", "cookie-value", { path: "/", maxAge: 60 * 60 * 24 * 7, }); };
this.$cookies.set("cookie-name", "cookie-value", { path: "/", maxAge: 60 * 60 * 24 * 7, });
|
问答: cookit 设置成 httpOnly 要在哪一端获取 cookit , nodejs or 浏览器?
asyncData() 和 fetch()
Nuxt.js 为 Vue 添加了两个钩子
asyncData()
- 限于页面组件
- 它可以在服务端或浏览器端渲染页面前端调用,渲染组件前获取数据,返回的对象合并到前组件的 data
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| <template> <div> <h1>{{ post.title }}</h1> <p>{{ post.description }}</p> </div> </template>
<script> export default { async asyncData(context) { // 服务器端 if (process.server) { const { req, res, beforeNuxtRender } = context; } // 浏览器端 if (process.client) { const { from, nuxtState } = context; }
const post = await axios.get(`https://api.nuxtjs.dev/posts/${params.id}`); return { post }; }, }; </script>
|
fetch()
- 页面组件和非页面组件可用
- 什么时候被调用:
- ⚠️ 组件每次加载前被调用(在服务端或切换至目标路由之前),渲染组件前填充应用的状态树(store)数据
- 同 methods 一样被调用,这时可以于改变组件的 data
fetch 方法来获取数据填充应用的状态树(vuex)。为了让获取过程可以异步,你需要返回一个 Promise,Nuxt.js 会等这个 promise 完成后再渲染组件,如:
1 2 3 4 5 6 7 8 9 10 11 12
| <template> <h1>Stars: {{ $store.state.stars }}</h1> </template>
<script> export default { async fetch({ store, params }) { let { data } = await axios.get("http://my-api/stars"); store.commit("setStars", data); }, }; </script>
|
让 query 变更也触发 asyncData 和 fetch
1 2 3 4 5 6 7 8 9 10 11
| // pages/somepage.vue
<template> <h1>A Page</h1> </template>
<script> export default { watchQuery: true, }; </script>
|
问答:asyncData 在什类型的组件中使用,页面组件 / 非页面组件?
Store(VUEX)
state
使用 function
设置默认值。 Object
是引用类型数据,在服务器端会在多个用户访问时使用同一个对象引用来初始化 store。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
export const state = () => ({ list: [], });
export const mutations = { add(state, text) { state.list.push({ text, done: false, }); }, remove(state, { todo }) { state.list.splice(state.list.indexOf(todo), 1); }, toggle(state, todo) { todo.done = !todo.done; }, };
|
其他最佳实践
UI 库的按需加载
减少静态资源的 size。
Element UI 和 Vant UI 的示范:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
|
export default { build: { transpile: [/^element-ui/, /vant.*?less/], babel: { plugins: [ [ "import", { libraryName: "vant", style: (name) => `${name}/style/less`, }, "vant", ],
[ "component", { libraryName: "element-ui", styleLibraryName: "theme-chalk", }, ], ], }, }, };
|
1 2 3 4 5 6 7 8 9
| <template> <el-button>Button</el-button> </template>
<script> import Vue from "vue"; import { Button } from "element-ui"; Vue.use(Button); </script>
|
媒体资源 懒加载
使用 nuxt-lazy-load 在用户需要看见媒体时才加载资源。
https://gitlab.com/broj42/nuxt-lazy-load
1 2 3 4 5
|
export default { modules: ["nuxt-lazy-load"], };
|
默认配置所有媒体开启懒加载。
背景图片的懒加载:
1
| <div lazy-background="~/assets/images/background-image.jpg">Content</div>
|
非懒加载的 img 标签加 data-not-lazy,避免出现一些 bug,比如图片裁切组件等等:
1 2 3
| <audio controls="controls" data-not-lazy> <source type="audio/mpeg" src="audio.mp3" /> </audio>
|
服务器端日志
添加服务器端日志,方便排查日志。
使用 nuxt-winson-log
配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
|
export default { modules: [["nuxt-winston-log"]], winstonLog: { useDefaultLogger: false, loggerOptions: { format: combine( label({ label: "Nuxt Server" }), timestamp(), printf(({ level, message, label, timestamp }) => { return `${timestamp} [${label}] ${level}: ${message}`; }) ), transports: [ new transports.Console(), new transports.File({ filename: path.resolve( process.cwd(), "./logs", `${process.env.NODE_ENV}.log` ), maxsize: 5 * 1024 * 1024, maxFiles: 20, }), ], }, }, };
|
使用
1 2 3 4 5 6 7 8 9
|
export default PageA { asyncData(context) { if(process.server) { context.$winstonLog.info('Hello from asyncData on server') } } }
|