# nuxtExample
**Repository Path**: adherent996/nuxtExample
## Basic Information
- **Project Name**: nuxtExample
- **Description**: vue-ssr样例
- **Primary Language**: JavaScript
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 1
- **Created**: 2024-07-23
- **Last Updated**: 2024-07-23
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# nuxt
## 参考链接
* [Vue SSR 指南](https://ssr.vuejs.org/zh/guide/build-config.html)
* [Nuxt源码精读](https://juejin.cn/post/6917247127315808270)
* [nuxt缓存实践](https://juejin.cn/post/6844903623483195399)
* [Nuxt 性能优化-页面缓存](https://my.oschina.net/u/158675/blog/4304453)
* [异步数据](https://www.nuxtjs.cn/guide/async-data)
* [SplitChunks & Lodash & Vuetify tree shaking](https://ithelp.ithome.com.tw/articles/10207669)
* [分享:nuxt中间件](https://blog.csdn.net/awseda/article/details/106227729)
* [官网文档webpack配置](https://zh.nuxtjs.org/docs/2.x/configuration-glossary/configuration-build)
* [nuxtjs 全局变量添加 asyncData 也可访问](http://quanzhan.applemei.com/webStack/TXpnNE5RPT0=)
* [Nuxt 基础:面向源码研究Nuxt.js](https://www.cnblogs.com/laozhang-is-phi/p/9687504.html)
* [Vue SSR深度剖析](https://juejin.cn/post/6844903812700831757)
* [服务端渲染SSR及实现原理](https://juejin.cn/post/7046898330000949285)
* [NuxtJS的 AsyncData 和 Fetch 使用详解](https://www.cnblogs.com/China-Dream/p/15667561.html)
## 目录
* [什么是服务器端渲染SSR](#什么是服务器端渲染SSR)
* [为什么使用服务器端渲染SSR](#为什么使用服务器端渲染SSR)
* [服务器端渲染与预渲染](#服务器端渲染与预渲染)
* [简单通过vue-server-renderer实现ssr](#简单通过vue-server-renderer实现ssr)
* [ssr代码约束](#ssr代码约束)
* [使用vue-router的路由](#使用vue-router的路由)
* [数据预取和状态](#数据预取和状态)
* [store](#store)
* [客户端激活](#客户端激活)
* [什么是nuxt?](#什么是nuxt?)
* [nuxt源码架构](#nuxt源码架构)
* [ssr核心原理](#ssr核心原理)
* [nuxt源码与原理](#nuxt源码与原理)
* [nuxt中间件](#nuxt中间件)
* [nuxt路由生成策略](#nuxt路由生成策略)
* [nuxt缓存](#nuxt缓存)
* [nuxt请求到渲染](#nuxt请求到渲染)
* [nuxt预渲染数据](#nuxt预渲染数据)
* [nuxt的api属性和方法](#nuxt的api属性和方法)
* [nuxt优化](#nuxt优化)
* [nuxt插件](#nuxt插件)
* [nuxt全局变量](#nuxt全局变量)
* [nuxt后端restfulApi](#nuxt后端restfulApi)
* [nuxt部署](#nuxt部署)
* [其它](#其它)
---
## 什么是服务器端渲染SSR
Vue.js 是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作 DOM。然而,也可以将同一个组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序。
服务器渲染的 Vue.js 应用程序也可以被认为是"同构"或"通用",因为应用程序的大部分代码都可以在服务器和客户端上运行。
## 为什么使用服务器端渲染SSR
* 与传统 SPA (单页应用程序 (Single-Page Application)) 相比,服务器端渲染 (SSR) 的优势主要在于:
* 更好的 SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面。
* 更快的内容到达时间 (time-to-content),特别是对于缓慢的网络情况或运行缓慢的设备。
* 使用服务器端渲染 (SSR) 时还需要有一些权衡之处:
* 开发条件所限。浏览器特定的代码,只能在某些生命周期钩子函数 (lifecycle hook) 中使用;一些外部扩展库 (external library) 可能需要特殊处理,才能在服务器渲染应用程序中运行。
* 涉及构建设置和部署的更多要求。与可以部署在任何静态文件服务器上的完全静态单页面应用程序 (SPA) 不同,服务器渲染应用程序,需要处于 Node.js server 运行环境。
* 更多的服务器端负载。在 Node.js 中渲染完整的应用程序,显然会比仅仅提供静态文件的 server 更加大量占用 CPU 资源 (CPU-intensive - CPU 密集),因此如果你预料在高流量环境 (high traffic) 下使用,请准备相应的服务器负载,并明智地采用缓存策略。
## 服务器端渲染与预渲染
服务器端渲染 (SSR) 只是用来改善少数营销页面的 SEO,那么你可能需要预渲染。
无需使用 web 服务器实时动态编译 HTML,而是使用预渲染方式,在构建时 (build time) 简单地生成针对特定路由的静态 HTML 文件。
优点是设置预渲染更简单,并可以将你的前端作为一个完全静态的站点。
可以使用 prerender-spa-plugin 轻松地添加预渲染。
## 简单通过vue-server-renderer实现ssr
```js
const Vue = require('vue');
const server = require('express')();
const template = require('fs').readFileSync('./index.template.html', 'utf-8');
const renderer = require('vue-server-renderer').createRenderer({
template,
});
const context = {
title: 'vue ssr',
metas: `
`,
};
server.get('*', (req, res) => {
const app = new Vue({
data: {
url: req.url
},
template: `
```
data-server-rendered 特殊属性,让客户端 Vue 知道这部分 HTML 是由 Vue 在服务端渲染的,并且应该以激活模式进行挂载。注意,这里并没有添加 id="app",而是添加 data-server-rendered 属性:你需要自行添加 ID 或其他能够选取到应用程序根元素的选择器,否则应用程序将无法正常激活。
在没有 data-server-rendered 属性的元素上,还可以向 $mount 函数的 hydrating 参数位置传入 true,来强制使用激活模式(hydration):
```js
// true强制使用应用程序的激活模式
app.$mount('#app', true)
```
## 什么是nuxt?
Nuxt是Vue开源社区提供的一整套基于Vue生态的SSR解决方案,包含脚手架、初始化工程目录、调试/构建服务等,其中SSR功能底层依旧依赖的是vue-server-renderer这个模块。
## nuxt源码架构
```txt
// 工程核心目录结构
├─ distributions
├─ nuxt // nuxt指令入口,同时对外暴露@nuxt/core、@nuxt/builder、@nuxt/generator、getWebpackConfig
├─ nuxt-start // nuxt start指令,同时对外暴露@nuxt/core
├─ lerna.json // lerna配置文件
├─ package.json
├─ packages // 工作目录
├─ babel-preset-app // babel初始预设
├─ builder // 根据路由构建动态当前页ssr资源,产出.nuxt资源
├─ cli // 脚手架命令入口
├─ config // 提供加载nuxt配置相关的方法
├─ core // Nuxt实例,加载nuxt配置,初始化应用模版,渲染页面,启动SSR服务
├─ generator // Generato实例,生成前端静态资源(非SSR)
├─ server // Server实例,基于Connect封装开发/生产环境http服务,管理Middleware
├─ types // ts类型
├─ utils // 工具类
├─ vue-app // 存放Nuxt应用构建模版,即.nuxt文件内容
├─ vue-renderer // 根据构建的SSR资源渲染html
└─ webpack // webpack相关配置、构建实例
├─ scripts
├─ test
└─ yarn.lock
```
package.json指令
```js
"scripts": {
"dev": "nuxt", // 开启一个监听3000端口的服务器,同时提供hot-reloading功能
"build": "nuxt build", //构建整个应用,压缩合并JS和CSS文件(用于生产环境)
"start": "nuxt start", // 开启一个生产模式的服务器(必须先运行nuxt build命令)
"generate": "nuxt generate" //构建整个应用,并为每一个路由生成一个静态页面(用于静态服务器)
}
```
## ssr核心原理
nuxt底层调用了vue-server-renderer这个方法库渲染html资源
html渲染的过程从本质上看非常简洁,假设我们有一个模版+资源映射表:
1. 读取资源映射表,在node环境加载并执行js bundle,创建Vue实例渲染得到html片段
2. 读取html模版,匹配{{{}}}中的方法
3. 实现renderResourceHints、renderStyles、renderScripts方法,核心是根据资源映射表创建对应的link、style、script标签。
4. 将html片段及上面创建的标签插入到html模版中。
```html
{{{ renderResourceHints() }}}
{{{ renderStyles() }}}
{{{ renderScripts() }}}
```
```json
{
publicPath: "xx/xx/",
initial: [
"css/xxx.css"
"js/xxx.js"
],
async: [
"css/xxx.css"
"js/xxx.js"
]
}
```
* 详细
```txt
src
├── components
├── App.vue
├── app.js ----通用 entry
├── entry-client.js ----仅运行于浏览器
└── entry-server.js ----仅运行于服务器
```
经过 webpack 打包之后会有两个 bundle 产物:
1. server bundle 用于生成 vue-ssr-server-bundle.json,我们熟悉的 sourceMap 和需要在服务端运行的代码列表都在这个产物中。
```json
vue-SSR-server-bundle.json
{
"entry": "static/js/app.80f0e94fe005dfb1b2d7.js",
"files": {
"static/js/app.80f0e94fe005dfb1b2d7.js": "module.exports=function(t...",
"static/js/xxx.29dba471385af57c280c.js": "module.exports=function(t..."
}
}
```
2. client Bundle 用于生成 vue-SSR-client-manifest.json,包含所有的静态资源,首次渲染需要加载的 script 标签,以及需要在客户端运行的代码。
```json
vue-SSR-client-manifest.json
{
"publicPath": "//cdn.xxx.cn/xxx/",
"all": [
"static/js/app.80f0e94fe005dfb1b2d7.js",
"static/css/app.d3f8a9a55d0c0be68be0.css"
],
"initial": [
"static/js/app.80f0e94fe005dfb1b2d7.js",
"static/css/app.d3f8a9a55d0c0be68be0.css"
],
"async": [
"static/js/xxx.29dba471385af57c280c.js"
],
"modules": {
"00f0587d": [ 0, 1 ]
...
}
}
```
## nuxt源码与原理
* 过程简述
1. 初始化
* 获取到serverBundle的入口文件代码并解析为入口函数,每次执行实例化vue对象
* 实例化了render和templateRenderer对象,负责渲染vue组件和组装html
2. 渲染
* 用户发起请求时,通过执行serverBundle后得到的应用入口函数,实例化vue对象。
* renderer对象负责把vue对象递归转为vnode,并把vnode根据不同node类型调用不同渲染函数最终组装为html。
* 在渲染组件的过程中如果组件定义了serverPrefetch钩子,则等待serverPrefetch执行完成之后再渲染页面(serverPrefetch生成的数据不会应用于父组件)
3. 输出
* 在编译阶段通过插件生成应用的文件和模块moduleIdentifier
* 在同一次编译过程中通过vue-loader把moduleIdentifier注入到每个模块的hook钩子中
* 在渲染阶段创建组件时调用hook钩子,把每个模块的moduleIdentifier添加到用户上下文context的_registeredComponents数组中
TemplateRenderer在获取依赖文件时读取_registeredComponents根据moduleIdentifier在clientManifest文件的映射关系找到,页面所需要引入的文件。
* 防止交叉污染
Node.js 服务器是一个长期运行的进程,在客户端编写的代码在进入进程时,变量的上下文将会被保留,导致交叉请求状态污染。 因此不可共享一个实例,所以说 createApp 是一个可被重复执行的函数。其实在包内部,变量之间也存在防止交叉污染的能力。
nodejs创建vm沙盒,并返回了run函数作为每次实例化vue组件的入口函数
```js
var run = createBundleRunner(
entry,
files,
basedir,
rendererOptions.runInNewContext
);
```
防止交叉污染的能力是由 rendererOptions.runInNewContext 这个配置项来提供的,这个配置支持 true, false,和 once 三种配置项传入。
```txt
// rendererOptions.runInNewContext 可配置项如下
true,每次创建vue实例时都创建一个全新的v8上下文环境并重新执行bundle代码,好处是每次渲染的环境状态是隔离的,不存在状态单例问题,也不存在状态污染问题。但是,缺点是每次创建v8上下文的性能代价很高。
false,创建在当前global运行上下文中运行的bundle代码环境,bundle代码将可以获取到当前运行环境的global对象,运行环境是单例的
once ,会在初始化时单例创建与global隔离的运行上下文
```
当runInNewContext设置为false或者once时,在初始化之后的用户每次请求将会在同一个沙盒环境中运行,所以在实例化vue实例或者一些状态存储必须通过闭包创建独立的作用域才不会被不同请求产生的数据相互污染
```js
export function createApp(context) {
const app = new Vue({
render: h => h(App)
});
return {app};
}
```
* 渲染
* templateRenderer,它负责html的组装,它的主要原型方法有:bindRenderFns、render、renderStyles、renderResourceHints、renderState、renderScripts,renderStyles、renderResourceHints、renderState、renderScripts分别是生成页面需要加载的样式、preload和prefetch资源、页面state(比如vuex的状态state,需要在服务端给context.state赋值才能输出)、脚本文件引用的内容。
* templateRenderer.bindRenderFns 中绑定的 state, script , styles 等资源。
* render: 函数会被递归调用按照从父到子的顺序,将组件全部转化为 html。
1. 创建RenderContext 渲染上下文对象,这个对象将贯穿整个递归过程,它主要负责在递归过程中闭合组件标签和渲染可缓存组件的存储工作。
2. 在组件中编译template,解析语法树,然后转成render函数。
```ts
template:
{{value}}
render:
with(this){return _c('span',[_ssrNode("
"+_ssrEscape(_s(value))+"
")])}
```
3. 检查组件是否定义了serverPrefetch钩子,如果定义了,则等待钩子执行完毕后才继续resolve回调,返回的是该vue组件的vnode,传递给renderNode函数递归解析。
4. 根据不同的vnode类型执行不同的render函数,六种不同类型的节点渲染方法
1. renderStringNode$1:负责处理通过vue编译template字符串模板生成的StringNode简单节点的渲染工作
* 如果没有子节点则直接调用写函数,其中el.open和el.close是节点开始和闭合标签。
* 如果有子节点则把子节点添加到渲染上下文的renderStates数组中,写入开始标签并传入渲染上下文的next函数,写函数在拼接完成后调用next,在渲染上下文的next函数中继续解析该节点的子节点,并且在解析这个节点树之后写入闭合标签
```js
function renderStringNode$1 (el, context) {
var write = context.write;
var next = context.next;
if (isUndef(el.children) || el.children.length === 0) {
write(el.open + (el.close || ''), next);
} else {
var children = el.children;
context.renderStates.push({
type: 'Element',
children: children,
rendered: 0,
total: children.length,
endTag: el.close
});
write(el.open, next);
}
}
```
2. renderComponent:负责处理vue的组件类型的节点
* 如果组件设置了serverCacheKey并且缓存中存在该key的渲染结果,则直接写入缓存的html结果。
在写入html之前我们看到代码中循环调用了res.components并且传入了用户上下文userContext,循环调用的函数其实是在vue-loader注入的一个hook。这个hook会在执行时把当前这个组件的moduleIdentifier(webpack中编译时生成的模块标识)添加到用户上下文userContext的_registeredComponents数组中,vue会通过这个数组查找组件的引用资源文件。
* 如果没有命中缓存或者根本就没有缓存,则分别执行:renderComponentWithCache和renderComponentInner
这两个函数的区别是renderComponentWithCache会在组件渲染完成时,通过渲染上下文把结果写入缓存。
在renderComponentInner函数中通过vnode创建组件对象,等待组件的serverPrefetch钩子执行完成之后,调用组件对象的_render生成子节点的vnode后再渲染。
serverPrefetch钩子中获取的数据只会被渲染到当前组件或者子组件中,因为在执行这个组件的serverPrefetch之前父组件已经被渲染完成了。
3. renderElement:负责渲染dom组件
处理自定义指令、show指令和组件的scoped CSS ID生成还有给标签加上data-server-rendered属性(表示这是经过服务端渲染的标签),最后组装好dom的开始标签startTag。
如果组件是自闭合标签或者没有子节点,则直接写入标签节点内容。否则通过渲染上下文在渲染子节点后再写入结束标签。
4. renderAsyncComponent:负责针对异步函数的加载和解析
vnode的asyncFactory是加载函数,因为我们的serverBundle已经包含所有脚本包含异步脚本了,所以在这一步的asyncFactory几乎就相当于一次Promise.resolve返回异步模块,不发起任何请求。拿到组件内容后创建vnode节点,调用renderComponent、renderNode。如果函数式组件的话可能返回多个vnode,直接通过渲染上下文渲染。
* 内容输出
1. 没有定义template模板
渲染结果会被直接返回给renderToString的回调函数,而页面所需要的脚本依赖我们通过用户上下文context的renderStyles、renderResourceHints、renderState、renderScripts这些函数分别获得(因为context在开始渲染之前就已经被templateRenderer.bindRenderFns(context)注入这些函数了)。
```js
renderer.renderToString(context, (err, html) => {
if (err) {
return handlerError(err, req, res, next);
}
const styles = context.renderStyles();
const scripts = context.renderScripts();
const resources = context.renderResourceHints();
const states = context.renderState();
const result = template({
html,
styles,
scripts,
resources,
states
});
return res.send(result);
});
```
```html
{{{resources}}}
{{{styles}}}
{{{html}}}
{{{states}}}
{{{scripts}}}
```
2. 定义了template模板
在创建TemplateRenderer实例的构造函数中,会对提供的template字符串做一个解析。解析的规则很简单,把模板分为三个部分:html文档开头到head标签是head部分,head之后到内容占位符是neck部分,最后tail部分是内容占位符到最后。
利用lodash.template函数将这三个字符串包装为一个渲染函数,可以在template模板中自定义一些占位符,然后通过用户上下文context上面的数据渲染。
把准备好的各个部分按照顺序拼接。
如果设置了inject为false,则preload、style、state、script的引用都需要自己在模板中自行渲染。
* 运行过程
1. 提取nuxt.config.js
2. 实例化nuxt
3. 运行nuxt.build
4. 开启node服务器并监听端口
* nuxt类
* 默认配置项default={},读取config时覆盖
* 判断运行环境
* 定义静态资源路径
* 判断store是否存在
* 读取template并缓存
* build/render/renderRoute/renderAndGetWindow/generate方法,实例化后执行
* 读取util方法
* close关闭服务器方法
* build生成.nuxt过程
* 判断是否满足运行条件
* generateRoutesAndFiles方法生成路由并注入模板(.nuxt,.nuxt/components)
* 读取layout内容
* 根据pages目录生成路由
* 解析模板文件
* 判断是否有默认布局,无则新建
* 判断是否有store,有则插入路由模板
* 解析好的文件生成到.nuxt目录下
* buildFiles生成文件到.nuxt/dist目录
* 对于dev环境,添加watchPage方法,监听文件变化,热更新
* 对于pro环境,构建client和server
* render相关
* render()
* nuxt递归挂载render
* dev模式调用webpack中间件
* 处理默认路由
* 处理static文件
* pro模式启动server处理.nuxt/dist文件
* 定义404,调用renderRoute方法
* 接收renderRoute产生的html进行响应
* renderRoute()
* 服务端渲染:生成完整html内容
* 非服务端渲染:产生id为_nuxt的div给客户端渲染
* 结合其它依赖文件内容输出html
* renderAndGetWindow()
* 检查生成的html的DOM结构,有异常则抛出,无则正常通知
## nuxt中间件
可以在nuxt.config.js中追加serverMiddleware属性来配置服务端中间件
nuxt中间件调用过程可以方便我们处理:代理、缓存、日志、监控、数据处理等等。
middleware/stats.js
```js
export default function (context) {
context.userAgent = process.server ? context.req.headers['user-agent'] : navigator.userAgent
}
```
pages/index.vue
```js
export default {
middleware: 'stats'
}
```
关于页面重定向,服务器端重定向防页面闪烁
middleware/redirect.js
```js
export default function ({ isServer, req, redirect, route }) {
let pcOrigin = 'xxxx'
let mobileOrigin = 'xxxx'
let isMobile = (ua) => {
return !!ua.match(/AppleWebKit.*Mobile.*/)
}
let userAgent = req ? req.headers['user-agent'] : navigator.userAgent || ''
console.log(isMobile(userAgent))
return isMobile(userAgent) ? redirect(mobileOrigin + route.fullPath) : redirect(pcOrigin + route.fullPath)
// 使用redirect 重定向到外链需要加上前缀:http / https
}
```
nuxt.config.js
```js
router:{
middleware: 'midd'
},
```
## nuxt路由生成策略
@nuxt/builder
```js
class Builder {
async build() {
...
// 检查文件目录是否存在
await this.validatePages()
// 生成路由并且产出模版文件
await this.generateRoutesAndFiles()
}
async generateRoutesAndFiles() {
...
// 根据文件结构生成路由表
await this.resolveRoutes(templateContext)
// 产出模版文件
await this.compileTemplates(templateContext)
}
}
```
```txt
pages/
--| _slug/
-----| comments.vue
-----| index.vue
--| user/
-----| index.vue
-----| one.vue
--| index.vue
```
```js
router: {
routes: [
{
name: 'index',
path: '/',
component: 'pages/index.vue'
},
{
name: 'slug',
path: '/:slug',
component: 'pages/_slug/index.vue'
},
{
name: 'slug-comments',
path: '/:slug/comments',
component: 'pages/_slug/comments.vue'
},
{
name: 'user',
path: '/user',
component: 'pages/user/index.vue'
},
{
name: 'user-one',
path: '/user/one',
component: 'pages/user/one.vue'
}
]
}
```
动态路由组件中定义参数校验方法,见[nuxt的api属性和方法](#nuxt的api属性和方法)第8点
## nuxt缓存
1. 组件级别的缓存
nuxt.config.js
```js
const LRU = require('lru-cache')
module.exports = {
render: {
bundleRenderer: {
cache: LRU({
max: 1000, // 最大的缓存个数
maxAge: 1000 * 60 * 15 // 缓存15分钟
})
}
}
}
```
在需做缓存的vue组件上增加name以及serverCacheKey字段,以确定缓存的唯一键值
```js
export default {
name: 'AppHeader',
props: ['type'],
serverCacheKey: props => props.type
}
```
新的请求到来时,只要父组件传下来的type属性之前处理过,就可以复用之前的渲染缓存结果,以增进性能
如果该组件除了依赖父组件的type属性,还依赖于别的属性,serverCacheKey这里也要做出相应的改变,因此,如果组件依赖于很多的全局状态,或者,依赖的状态取值非常多,意味需要缓存会被频繁被设置而导致溢出,其实就没有多大意义了,在lru-cache的配置中,设置的最大缓存个数是1000,超出部分就会被清掉
何时使用组件缓存?
在以下情况,你不应该缓存组件:
* 它具有可能依赖于全局状态的子组件。
* 它具有对渲染上下文产生副作用(side effect)的子组件。
2. API级别的缓存
在服务端渲染的场景中,往往会将请求放在服务端去做,渲染完页面再返回给浏览器,而有些接口是可以去做缓存的,比如单纯获取配置数据的接口,对接口的缓存可以加快每个请求的处理速度,更快地释放掉请求,从而增进性能
```js
import axios from 'axios'
import md5 from 'md5'
import LRU from 'lru-cache'
// 给api加3秒缓存
const CACHED = LRU({
max: 1000,
maxAge: 1000 * 3
})
function request (config) {
let key
// 服务端才加缓存,浏览器端就不管了
if (config.cache && !process.browser) {
const { params = {}, data = {} } = config
key = md5(config.url + JSON.stringify(params) + JSON.stringify(data))
if (CACHED.has(key)) {
// 缓存命中
return Promise.resolve(CACHED.get(key))
}
}
return axios(config)
.then(rsp => {
if (config.cache && !process.browser) {
// 返回结果前先设置缓存
CACHED.set(key, rsp.data)
}
return rsp.data
})
}
const api = {
getGames: params => request({
url: '/gameInfo/gatGames',
params,
cache: true
})
}
```
3. 页面级别的缓存
在不依赖于登录态以及过多参数的情况下,如果并发量很大,可以考虑使用页面级别的缓存, 在nuxt.config.js增加serverMiddleware属性
```js
const nuxtPageCache = require('nuxt-page-cache')
module.exports = {
serverMiddleware: [
nuxtPageCache.cacheSeconds(1, req => {
if (req.query && req.query.pageType) {
return req.query.pageType
}
return false
})
]
}
```
上面的例子根据链接后面的参数pageType去做缓存,如果链接后面有pageType参数,就做缓存,缓存时间为1秒,也就是说在1秒内相同的pageType请求,服务端只会执行一次完整的渲染
重新封装:如果缓存命中,直接将原先的计算结果返回,大大提供了性能
```js
const LRU = require('lru-cache')
let cacheStore = new LRU({
max: 100, // 设置最大的缓存个数
maxAge: 200
})
module.exports.cacheSeconds = function (secondsTTL, cacheKey) {
// 设置缓存的时间
const ttl = secondsTTL * 1000
return function (req, res, next) {
// 获取缓存的key值
let key = req.originalUrl
if (typeof cacheKey === 'function') {
key = cacheKey(req, res)
if (!key) { return next() }
} else if (typeof cacheKey === 'string') {
key = cacheKey
}
// 如果缓存命中,直接返回
const value = cacheStore.get(key)
if (value) {
return res.end(value, 'utf-8')
}
// 缓存原先的end方案
res.original_end = res.end
// 重写res.end方案,由此nuxt调用res.end实际上是调用该方法,
res.end = function () {
if (res.statusCode === 200) {
// 设置缓存
cacheStore.set(key, data, ttl)
}
// 最终返回结果
res.original_end(data, 'utf-8')
}
}
}
```
另一种写法:
1. 新建服务器端中间件处理缓存文件 ~/utils/server-middleware/pageCache.js(可结合上方代码修改)
```js
const LRU = require('lru-cache');
export const cachePage = new LRU({
max: 100, // 缓存队列长度 最大缓存数量
maxAge: 1000 * 60 * 180, // 缓存时间 单位:毫秒
});
export default function(req, res, next) {
const url = req._parsedOriginalUrl;
const pathname = url.pathname;
// 本地开发环境不做页面缓存(也可开启开发环境进行调试)
if (process.env.NODE_ENV !== 'development') {
// 只有首页才进行缓存
if (['/'].indexOf(pathname) > -1) {
const existsHtml = cachePage.get('indexPage');
if (existsHtml) {
// 如果没有Content-Type:text/html 的 header,gtmetrix网站无法做测评
res.setHeader('Content-Type', ' text/html; charset=utf-8');
return res.end(existsHtml.html, 'utf-8');
} else {
res.original_end = res.end;
res.end = function(data) {
if (res.statusCode === 200) {
// 设置缓存
cachePage.set('indexPage', {
html: data,
});
}
res.original_end(data, 'utf-8');
};
}
}
}
next();
}
```
2. nuxt.config.js
```js
serverMiddleware: [
{
path: '/',
handler: '~/utils/server-middleware/pageCache.js',
},
]
```
3. 清理缓存
pageCahe.js
```js
if (pathname === '/cleancache') {
cachePage.reset();
res.statusCode = 200;
}
```
nuxt.config.js中
```js
{
path: '/cleancache',
handler: '~/utils/server-middleware/pageCache.js',
},
```
## nuxt请求到渲染
请求->服务器初始化->中间件运行(nuxt.config.js/匹配layout/匹配页面)->校验参数->异步数据处理(asyncData->beforeCreate->created->fetch)->客户端渲染->路由跳转nuxt-link回到中间件运行
客户端渲染->vue生命周期(beforeMount后续)
## nuxt预渲染数据
1. asyncData:可以在服务端执行async Data获取数据
方法是在组件初始化前被调用的,并且是在服务端调用所以该方法是没有办法通过 this 来引用的,但可以传入第一个参数context来引用上下文
```html
```
使用回调函数
```js
export default {
asyncData({ params }, callback) {
axios.get(`https://my-api/posts/${params.id}`).then(res => {
callback(null, { title: res.data.title })
})
}
}
```
返回对象:如果组件的数据不需要异步获取或处理,可以直接返回指定的字面对象作为组件的数据。
```js
export default {
data() {
return { foo: 'bar' }
}
}
```
使用 req/res(request/response) 对象:在服务器端调用asyncData时,可以访问用户请求的req和res对象。
```js
export default {
async asyncData({ req, res }) {
// 请检查您是否在服务器端
// 使用 req 和 res
if (process.server) {
return { host: req.headers.host }
}
return {}
}
}
```
访问动态路由数据:如果你定义一个名为_slug.vue的文件,您可以通过context.params.slug来访问它。
```js
export default {
async asyncData({ params }) {
const slug = params.slug // When calling /abc the slug will be "abc"
return { slug }
}
}
```
错误处理
```js
export default {
asyncData({ params, error }) {
return axios
.get(`https://my-api/posts/${params.id}`)
.then(res => {
return { title: res.data.title }
})
.catch(e => {
error({ statusCode: 404, message: 'Post not found' })
})
}
}
```
```js
export default {
asyncData({ params }, callback) {
axios
.get(`https://my-api/posts/${params.id}`)
.then(res => {
callback(null, { title: res.data.title })
})
.catch(e => {
callback({ statusCode: 404, message: 'Post not found' })
})
}
}
```
2. fetch应对的是vuex
fetch 是在组件实例化之后被调用的,可以访问到 this
```html
Stars: {{ $store.state.stars }}
Stars: {{ $store.state.stars }}
//store/index.js
export const actions = {
async GET_STARS({ commit }) {
const { data } = await axios.get('http://my-api/stars')
commit('SET_STARS', data)
}
}
```
可直接修改本地数据
```js
export default {
data() {
return {
todos: []
};
},
async fetch() {
const { data } = await axios.get(
`https://jsonplaceholder.typicode.com/todos`
);
// `todos` has to be declared in data()
this.todos = data;
}
};
```
可以通过 methods 来调用
```html
```
## nuxt的api属性和方法
1. 使用 head 方法设置当前页面的头部标签
```html
{{ title }}
```
2. 设置内部router-view组件的key属性
```js
export default {
key(route) {
return route.fullPath
}
}
```
3. layouts 根目录下的所有文件都属于个性化布局文件,可以在页面组件中利用 layout 属性来引用。
```js
export default {
layout: 'blog',
// 或
layout(context) {
return 'blog'
}
}
```
4. loading:loading 属性提供了禁用特定页面上的默认加载进度条的选项。
默认情况下,Nuxt.js 使用自己的组件来显示路由跳转之间的进度条。
```html
My page
```
5. 在应用中的特定页面设置中间件
pages/secret.vue
```html
Secret page
```
middleware/authenticated.js
```js
export default function ({ store, redirect }) {
// If the user is not authenticated
if (!store.state.authenticated) {
return redirect('/login')
}
}
```
6. scrollToTop 属性用于控制页面渲染前是否滚动至页面顶部
默认情况下,从当前页面切换至目标页面时,Nuxt.js 会让目标页面滚动至顶部。但是在嵌套子路由的场景下,Nuxt.js 会保持当前页面的滚动位置,除非在子路由的页面组件中将 scrollToTop 设置为 true。
```html
子页面组件
```
7. 路由切换时的过渡动效
更多配置参考:[API: transition 属性](https://www.nuxtjs.cn/api/pages-transition)
```html
```
8. validate 方法可以在动态路由对应的页面组件中配置一个校验方法用于校验动态路由参数的有效性。
```js
validate({ params, query }) {
return true // 如果参数有效
return false // 参数无效,Nuxt.js 停止渲染当前页面并显示错误页面
}
```
```js
async validate({ params, query, store }) {
// await operations
return true // 如果参数有效
return false // 将停止Nuxt.js呈现页面并显示错误页面
}
```
```js
validate({ params, query, store }) {
return new Promise((resolve) => setTimeout(() => resolve()))
}
```
```js
export default {
validate({ params }) {
// Must be a number
return /^\d+$/.test(params.id)
}
}
```
```js
export default {
validate({ params, store }) {
// 校验 `params.id` 是否存在
return store.state.categories.some(category => category.id === params.id)
}
}
```
```js
export default {
async validate({ params, store }) {
// 使用自定义消息触发内部服务器500错误
throw new Error('Under Construction!')
}
}
```
9. watchQuery属性,监听参数字符串更改并在更改时执行组件方法 (asyncData, fetch, validate, layout, ...)
```js
export default {
//要为所有参数字符串设置监听,设置:watchQuery: true
watchQuery: ['page']
}
```
## nuxt优化
1. splitChunks分包:chunk被切分,按需加载
nuxt.config.js
```js
module.exports = {
build: {
optimization: {
splitChunks: {
minSize: 10000,
maxSize: 250000
}
}
}
}
```
2. LodashModuleReplacementPlugin:lodash tree-shaking缩减大小
npm i -D lodash-webpack-plugin
nuxt.config.js
```js
const LodashModuleReplacementPlugin = require('lodash-webpack-plugin')
module.exports = {
build: {
extend(config, ctx) {
config.plugins.unshift(new LodashModuleReplacementPlugin)
// rules[2].use[0] is babel-loader
config.module.rules[2].use[0].options.plugins = ['lodash']
}
}
}
```
3. 缓存见[nuxt缓存](#nuxt缓存)
4. 其它webpack配置
由于有eslint检测,因此看不出优化后文件大小变化
```js
build: {
transpile: [/^element-ui/], // babel编译指定模块
// analyze: true,
optimization: {
minimize: true, // tree-shaking
splitChunks: {
chunks: 'all'
} // 分包
},
parallel: true, // 多线程build,实验属性
sourceMap: false, // 索源文件
collapseBooleanAttributes: true,
decodeEntities: true,
minifyCSS: true,
minifyJS: true,
processConditionalComments: true,
removeEmptyAttributes: true,
removeRedundantAttributes: true,
trimCustomFragments: true,
useShortDoctype: true,
/*
** You can extend webpack config here
*/
extend (config, ctx) {
}
}
```
## nuxt插件
/plugins/request-cache.js
```js
import axios from 'axios'
import LRU from 'lru-cache'
// 给api加3秒缓存
const CACHED = new LRU({
max: 1000,
maxAge: 1000
})
const request = (config) => {
// 服务端才加缓存,浏览器端就不管了
// if (config.cache && !process.browser) {}
const { params = {}, data = {} } = config
const key = config.method + config.url + JSON.stringify(params) + JSON.stringify(data)
if (CACHED.has(key)) {
// 缓存命中
return Promise.resolve(CACHED.get(key))
}
return axios(config)
.then((rsp) => {
CACHED.set(key, rsp.data)
return rsp.data
})
}
// const callback = (string) => {
// console.log('That was easy!', string)
// }
// 只注入服务端
// export default ({ app }, inject) => {
// // Set the function directly on the context.app object
// app.requestCache = callback
// }
// 注入服务端和浏览器端
export default ({ app }, inject) => {
inject('requestCache', request)
}
```
vue页面使用
```js
async asyncData (context) {
// called every time before loading the component
let result = 'test'
result = await context.app.$requestCache({
method: 'get',
url: 'https://***/api/values'
})
return { result }
}
```
## nuxt全局变量
/plugins/webconfig.js
```js
import Vue from 'vue' // vue 文件引入 - 方便在vue方法内容直接 this 调取
const urlPrefix = 'http://localhost:6666/'
const main = {
install (Vue) {
Vue.prototype.urlPrefix = urlPrefix // 变量的内容 后期可以在vue中 this->$api.xxx 使用
}
}
Vue.use(main) // 这里不能丢
// 这里是 为了在 asyncData 方法中使用
export default ({ app }, inject) => {
// Set the function directly on the context.app object
app.$api = urlPrefix // 名称
}
```
nuxt.config.js
```js
{ src: '@/plugins/webconfig.js', ssr: false },
```
页面
```js
mounted () {
console.log('mounted')
this.prefix = (this as any).urlPrefix
},
```
## nuxt后端restfulApi
写法同koa2,在nuxt代码中穿插即可
错误日志样例:
/plugins/errorHandler.js
```js
import Vue from 'vue'
import axios from 'axios'
// 系统错误捕获
const getErr = async (err, _this, info) => {
await axios.post('/api/getErr', { err: err.stack, hook: info })
}
const errorHandler = (error, vm, info) => {
getErr(error, vm, info)
}
Vue.config.errorHandler = errorHandler
Vue.prototype.$throw = (error, vm, info) => errorHandler(error, vm, info)
```
nuxt.config.js
```js
plugins: [
{ src: '@/plugins/errorHandler.js', ssr: false }
],
```
index.vue
```js
created () {
console.log('created')
this.$axios.post('/api/getErr', { err: 'err.stack', hook: 'info' })
},
```
服务端index.js
```js
const json = require('koa-json')
const bodyparser = require('koa-bodyparser')
const getErr = require('../utils/routes/api/getErr.js')
async function start () {
...
//必须加入bodyparser、json中间件,才能获取ctx.request.body
app.use(bodyparser())
app.use(json())
app.use(getErr.routes()).use(getErr.allowedMethods())
app.use((ctx) => {
ctx.status = 200
ctx.respond = false // Bypass Koa's built-in response handling
ctx.req.ctx = ctx // This might be useful later on, e.g. in nuxtServerInit or with nuxt-stash
nuxt.render(ctx.req, ctx.res)
})
...
}
```
路由逻辑:/utils/routes/api/getErr.js
```js
const fs = require('fs')
const router = require('koa-router')()
router.post('/api/getErr', async (ctx, next) => {
const time = new Date()
fs.writeFile(
'./logs/' + time.getTime() + '.txt',
'报错内容:' + ctx.request.body.err + '\r\n' +
'所在钩子:' + ctx.request.body.hook + '\r\n' +
'报错时间:' + time.toLocaleString() + '\r\n',
(err) => {
if (err) { throw err }
})
ctx.body = {
e: ctx.request.body
}
})
module.exports = router
```
## nuxt部署
生产环境文件:.nuxt node_modules server/ static/(看是否有资源文件引用,assets同理) middleware/(如在/server/index.js有引用) nuxt.config.js package.json
npm run build
npm run start 成功后改用pm2
pm2 start npm --name "nuxtExample" -- run start --watch
如果更新后不生效,可能pm2程序没彻底关闭,可执行如下命令:
lsof -i tcp:20001 查看pm2的pid,其中20001是我们配置的端口号,下面的PID为1037
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
node 1037
kill -9 {pid} pid是上面看到的pid
## 其它
* 教程
https://www.nuxtjs.cn
* ts bug 临时解决方案
Interface 'NuxtApp' incorrectly extends interface 'Vue'
tsconfig.json
compilerOptions :{"skipLibCheck": true}
* nuxt的pages自动路由原理
https://segmentfault.com/a/1190000014839730
https://www.nuxtjs.cn/guide/routing
* Nuxt配置sass或者less
https://www.jianshu.com/p/7a1d11c7d895
* vue typescript支持
https://cn.vuejs.org/v2/guide/typescript.html
* 使用nuxt封装的axios
https://axios.nuxtjs.org/
https://www.cnblogs.com/yddzyy/p/13269887.html
* 全局组件
https://segmentfault.com/a/1190000020452519?utm_source=tag-newest
* 生命周期
https://blog.csdn.net/webjhh/article/details/94460595
* 项目部署
https://segmentfault.com/a/1190000020452519?utm_source=tag-newest
https://blog.csdn.net/sophie_u/article/details/86690053
生产环境文件:.nuxt node_modules server/ static/(看是否有资源文件引用,assets同理) middleware/(如在/server/index.js有引用) nuxt.config.js package.json
npm run build
npm run start 成功后改用pm2
pm2 start npm --name "nuxtExample" -- run start --watch
* Classic mode for store/ is deprecated and will be removed in Nuxt 3.
https://www.bootschool.net/article/5c50895df60a310b0e6f33ee/nuxt.js-error-classic-mode-for-store-is-deprecated
* Build failing with Error "cross-env: Permission denied"
https://github.com/travis-ci/travis-ci/issues/7044
* 缓存
https://www.jianshu.com/p/421a3cf22415
https://my.oschina.net/u/158675/blog/4304453
* 前端渲染中间件
https://blog.csdn.net/awseda/article/details/106227729
* webpack优化和配置
https://zh.nuxtjs.org/docs/2.x/configuration-glossary/configuration-build
* nuxt.js使用v-for动态绑定img的src的本地路径,图片不能渲染
https://blog.csdn.net/qq_42231156/article/details/100071551
* typescript
https://segmentfault.com/a/1190000011744210
https://blog.csdn.net/sllailcp/article/details/102542796/
* pdf在线预览
https://blog.csdn.net/weixin_38312502/article/details/105577457
https://www.npmjs.com/package/vue-pdf
* nuxt.js项目中全局捕获异常并生成错误日志全过程
https://www.cnblogs.com/xinyan-666/p/9337742.html
https://www.cnblogs.com/antyhouse/p/12553073.html