客户端渲染和服务器端渲染 简述服务器端渲染、客户端渲染、静态站点生成
CSR CSR => client-side-render,即客户端渲染。具体过程如下:
用户请求页面,返回页面。此时页面只是模版页面
浏览器解析页面代码,读到js代码时,会根据我们所写的接口去请求数据
得到返回数据后使用模版(vue/react/ng/art-template)进行渲染
网站举例
SSR SSR => server-side-render,即服务器端渲染。具体过程如下:
用户请求页面
后端取到准备好的数据,渲染到我们自己写的服务器模版(next/nuxt/ejs)中,准备好html结构与相应数据后返回给浏览器
CSR & SSR 优缺点对比
优点
缺点
CSR
减轻服务器压力,前后端分离
对seo不友好(不利于爬虫爬取)
SSR
对seo友好
对服务器性能有一定要求,不利于前后端分离
其实在真正开发中通常是 csr 与 ssr 相结合使用,前端使用cdn缓存,后端使用nginx缓存。这样是最优的解决方案。上两张图大家对比理解:
Vue SSR 官网参考文章
基本用法 安装
1 npm install vue@2 vue-server-renderer --save
渲染一个Vue实例 server.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const Vue = require ('vue' )const app = new Vue ({ template : `<div>Hello World</div>` })const renderer = require ('vue-server-renderer' ).createRenderer () renderer.renderToString (app, (err, html ) => { if (err) throw err console .log (html) }) renderer.renderToString (app).then (html => { console .log (html) }).catch (err => { console .log (err) })
package.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 { "name" : "vue-server-renderer" , "version" : "1.0.0" , "description" : "" , "main" : "index.js" , "scripts" : { "dev" : "nodemon ./server.js" } , "keywords" : [ ] , "author" : "" , "license" : "ISC" , "dependencies" : { "vue" : "^2.7.14" , "vue-server-renderer" : "^2.7.14" } }
与服务器端集成 1 npm install express --save
server2.js
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 30 31 32 33 34 35 36 37 38 const Vue = require ('vue' )const server = require ('express' )()const renderer = require ('vue-server-renderer' ).createRenderer () server.get ('*' , (req,res ) => { const app = new Vue ({ data : { url : req.url }, template : `<h1>你访问的 URL 是: {{ url }}</h1>` }) renderer.renderToString (app, (err, html ) => { if (err) { res.status (500 ).end ('服务器端发生错误' ) return } res.end (` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> ${html} </body> </html> ` ) }) }) server.listen (8080 )
访问:http://localhost:8080/
使用一个页面模板 当你在渲染 Vue 应用程序时,renderer 只从应用程序生成 HTML 标记 (markup)。在这个示例中,我们必须用一个额外的 HTML 页面包裹容器,来包裹生成的 HTML 标记。
为了简化这些,你可以直接在创建 renderer 时提供一个页面模板。多数时候,我们会将页面模板放在特有的文件中,例如 index.html
:
1 2 3 4 5 6 7 8 9 10 11 12 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta http-equiv ="X-UA-Compatible" content ="IE=edge" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > Document</title > </head > <body > </body > </html >
注意 <!--vue-ssr-outlet-->
注释 – 这里将是应用程序 HTML 标记注入的地方。
<!--vue-ssr-outlet-->
这个注释要加上,否则会报以下错误
然后,我们可以读取和传输文件到 Vue renderer 中:
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 const Vue = require ('vue' )const server = require ('express' )()const renderer = require ('vue-server-renderer' ).createRenderer ({ template : require ('fs' ).readFileSync ('./index.html' , 'utf-8' ) }) server.get ('*' , (req,res ) => { const app = new Vue ({ data : { url : req.url }, template : `<h1>你访问的 URL 是: {{ url }}</h1>` }) renderer.renderToString (app, (err, html ) => { if (err) { res.status (500 ).end ('服务器端发生错误' ) return } res.end (html) }) }) server.listen (8080 )
模板插值
index.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta http-equiv ="X-UA-Compatible" content ="IE=edge" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > {{ title }}</title > {{{ meta }}}</head > <body > </body > </html >
我们可以通过传入一个”渲染上下文对象”,作为 renderToString
函数的第二个参数,来提供插值数据:
1 2 3 4 5 6 7 8 9 10 11 12 const context = { title : 'hello' , meta : ` <meta ...> <meta ...> ` } renderer.renderToString (app, context, (err, html ) => { })
也可以与 Vue 应用程序实例共享 context
对象,允许模板插值中的组件动态地注册数据。
server2.js
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 30 31 32 33 34 35 36 37 const Vue = require ('vue' )const server = require ('express' )()const template = require ('fs' ).readFileSync ('./index.html' , 'utf-8' )const renderer = require ('vue-server-renderer' ).createRenderer ({ template })const context = { title : '自定义标题' , meta : ` <meta name="keyword" content="vue,ssr"> <meta name="description" content="vue srr demo"> ` } server.get ('*' , (req,res ) => { const app = new Vue ({ data : { url : req.url }, template : `<h1>你访问的 URL 是: {{ url }}</h1>` }) renderer.renderToString (app, context, (err, html ) => { if (err) { res.status (500 ).end ('服务器端发生错误' ) return } res.end (html) }) }) server.listen (8080 )
通用代码 服务器上的数据响应 在纯客户端应用程序 (client-only app) 中,每个用户会在他们各自的浏览器中使用新的应用程序实例。对于服务器端渲染,我们也希望如此:每个请求应该都是全新的、独立的应用程序实例,以便不会有交叉请求造成的状态污染 (cross-request state pollution)。
因为实际的渲染过程需要确定性,所以我们也将在服务器上“预取”数据 (“pre-fetching” data) - 这意味着在我们开始渲染时,我们的应用程序就已经解析完成其状态。也就是说,将数据进行响应式的过程在服务器上是多余的,所以默认情况下禁用。禁用响应式数据,还可以避免将「数据」转换为「响应式对象」的性能开销。
组件生命周期钩子函数 由于没有动态更新,所有的生命周期钩子函数中,只有 beforeCreate
和 created
会在服务器端渲染 (SSR) 过程中被调用。这就是说任何其他生命周期钩子函数中的代码(例如 beforeMount
或 mounted
),只会在客户端执行。
此外还需要注意的是,你应该避免在 beforeCreate
和 created
生命周期时产生全局副作用的代码,例如在其中使用 setInterval
设置 timer。在纯客户端 (client-side only) 的代码中,我们可以设置一个 timer,然后在 beforeDestroy
或 destroyed
生命周期时将其销毁。但是,由于在 SSR 期间并不会调用销毁钩子函数,所以 timer 将永远保留下来。为了避免这种情况,请将副作用代码移动到 beforeMount
或 mounted
生命周期中。
通用代码不可接受特定平台的 API,因此如果你的代码中,直接使用了像 window
或 document
,这种仅浏览器可用的全局变量,则会在 Node.js 中执行时抛出错误,反之也是如此。
对于共享于服务器和客户端,但用于不同平台 API 的任务(task),建议将平台特定实现包含在通用 API 中,或者使用为你执行此操作的 library。例如,axios (opens new window) 是一个 HTTP 客户端,可以向服务器和客户端都暴露相同的 API。
对于仅浏览器可用的 API,通常方式是,在「纯客户端 (client-only)」的生命周期钩子函数中惰性访问 (lazily access) 它们。
请注意,考虑到如果第三方 library 不是以上面的通用用法编写,则将其集成到服务器渲染的应用程序中,可能会很棘手。你可能 要通过模拟 (mock) 一些全局变量来使其正常运行,但这只是 hack 的做法,并且可能会干扰到其他 library 的环境检测代码。
自定义指令 大多数自定义指令直接操作 DOM,因此会在服务器端渲染 (SSR) 过程中导致错误。有两种方法可以解决这个问题:
推荐使用组件作为抽象机制,并运行在「虚拟 DOM 层级(Virtual-DOM level)」(例如,使用渲染函数(render function))。
如果你有一个自定义指令,但是不是很容易替换为组件,则可以在创建服务器 renderer 时,使用 directives
选项所提供”服务器端版本(server-side version)”。
源码结构 避免状态单例 当编写纯客户端 (client-only) 代码时,我们习惯于每次在新的上下文中对代码进行取值。但是,Node.js 服务器是一个长期运行的进程。当我们的代码进入该进程时,它将进行一次取值并留存在内存中。这意味着如果创建一个单例对象,它将在每个传入的请求之间共享。
如基本示例所示,我们为每个请求创建一个新的根 Vue 实例 。这与每个用户在自己的浏览器中使用新应用程序的实例类似。如果我们在多个请求之间使用一个共享的实例,很容易导致交叉请求状态污染 (cross-request state pollution)。
因此,我们不应该直接创建一个应用程序实例,而是应该暴露一个可以重复执行的工厂函数 ,为每个请求创建新的应用程序实例:
src/app.js
1 2 3 4 5 6 7 8 9 10 const Vue = require ('vue' )module .exports = function createApp (context ) { return new Vue ({ data : { url : context.url }, template : `<h1>你访问的 URL 是: {{ url }}</h1>` }) }
src/server.js
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 30 31 `const createApp = require('./app')` const server = require ('express' )()const template = require ('fs' ).readFileSync ('./index.html' , 'utf-8' )const renderer = require ('vue-server-renderer' ).createRenderer ({ template })const context = { title : '自定义标题' , meta : ` <meta name="keyword" content="vue,ssr"> <meta name="description" content="vue srr demo"> ` } server.get ('*' , (req,res ) => {` const param = { url: req.url } const app = createApp(param)` renderer.renderToString (app, context, (err, html ) => { if (err) { res.status (500 ).end ('服务器端发生错误' ) return } res.end (html) }) }) server.listen (8080 )
介绍构建步骤 到目前为止,我们还没有讨论过如何将相同的 Vue 应用程序提供给客户端。为了做到这一点,我们需要使用 webpack 来打包我们的 Vue 应用程序。事实上,我们可能需要在服务器上使用 webpack 打包 Vue 应用程序,因为:
通常 Vue 应用程序是由 webpack 和 vue-loader
构建,并且许多 webpack 特定功能不能直接在 Node.js 中运行(例如通过 file-loader
导入文件,通过 css-loader
导入 CSS)。
尽管 Node.js 最新版本能够完全支持 ES2015 特性,我们还是需要转译客户端代码以适应老版浏览器。这也会涉及到构建步骤。
所以基本看法是,对于客户端应用程序和服务器应用程序,我们都要使用 webpack 打包 - 服务器需要「服务器 bundle」然后用于服务器端渲染(SSR),而「客户端 bundle」会发送给浏览器,用于混合静态标记。
使用 webpack 的源码结构 现在我们正在使用 webpack 来处理服务器和客户端的应用程序,大部分源码可以使用通用方式编写,可以使用 webpack 支持的所有功能。
一个基本项目可能像是这样:
1 2 3 4 5 6 7 8 9 src ├── components │ ├── Foo.vue │ ├── Bar.vue │ └── Baz.vue ├── App.vue ├── app.js ├── entry-client.js └── entry-server.js
app.js
app.js
是我们应用程序的「通用 entry」。在纯客户端应用程序中,我们将在此文件中创建根 Vue 实例,并直接挂载到 DOM。但是,对于服务器端渲染(SSR),责任转移到纯客户端 entry 文件。app.js
简单地使用 export 导出一个 createApp
函数:
1 2 3 4 5 6 7 8 9 10 11 12 import Vue from 'vue' import App from './App.vue' export function createApp () { const app = new Vue ({ render : h => h (App ) }) return { app } }
entry-client.js
:
客户端 entry 只需创建应用程序,并且将其挂载到 DOM 中:
1 2 3 4 5 6 7 8 import { createApp } from './app' const { app } = createApp () app.$mount('#app' )
entry-server.js
:
服务器 entry 使用 default export 导出函数,并在每次渲染中重复调用此函数。此时,除了创建和返回应用程序实例之外,它不会做太多事情 - 但是稍后我们将在此执行服务器端路由匹配 (server-side route matching) 和数据预取逻辑 (data pre-fetching logic)。
1 2 3 4 5 6 import { createApp } from './app' export default context => { const { app } = createApp () return app }
Nuxt 官方文档:https://www.nuxtjs.cn/guide
简单来说,Nuxt 就是基于Vue 的一个应用框架,采用服务端渲染 ,让你的SPA应用(Vue)也可以拥有 SEO
Vue 开发一个单页面应用,相信很多前端工程师都已经学会了,但是单页面应用有一个致命的缺点,就是 SEO 极不友好。除非,vue 能在服务端渲染(ssr)并直接返回已经渲染好的页面,而并非只是一个单纯的 <div id="app"></div>
。
Nuxt.js 就是一个极简的 vue 版的 ssr 框架。基于它,我们可以快速开发一个基于 vue 的 ssr 单页面应用。
Nuxt.js简单的说是Vue.js的通用框架,最常用的就是用来作SSR(服务器端渲染)。再直白点说,就是Vue.js原来是开发SPA(单页应用)的,但是随着技术的普及,很多人想用Vue开发多页应用,并在服务端完成渲染。这时候就出现了Nuxt.js这个框架,她它简化了SSR的开发难度。还可以直接用命令把我们制作的vue项目生成为静态html。
安装 Nuxt.js 官方提供了一个模板,可以使用 vue-cli 直接安装。
1 vue init nuxt-community/starter-template project-name
或者
创建成功后
🎉 Successfully created project nuxt01-start
To get started:
cd nuxt01-start
npm run dev
To build & start for production:
cd nuxt01-start
npm run build
npm run start
创建的目录结构
目录结构介绍 1 2 3 4 5 6 7 8 9 10 11 12 13 14 . ├── README.md ├── assets ├── components ├── layouts ├── middleware ├── node_modules ├── nuxt.config .js ├── package.json ├── pages ├── plugins ├── static ├── store └── yarn.lock
其中:
assets : 资源文件。放置需要经过 webpack 打包处理的资源文件,如 scss,图片,字体等。
components : vue组件。这里存放在页面中,可以复用的组件,不支持服务器端的钩子。
layouts : 布局。页面都需要有一个布局,默认为 default。它规定了一个页面如何布局页面。所有页面都会加载在布局页面中的 <nuxt />
标签中。如果需要在普通页面中使用下级路由,则需要在页面中添加 <nuxt-child />
。该目录名为Nuxt.js保留的,不可更改。在 layout 中我们可以放入一些每个页面都会以用到的组件,比如 header & footer。当然如果你不想使用已生成的 layout 组件,你可以重新创建一个,比如 blank.vue 一般不需要引入 header&footer 的页面可以使用 blank.vue 这个 layout 组件。代码如下:
middleware : 中间件。存放中间件。可以在页面中调用: middleware: 'middlewareName'
。
pages : 页面。一个 vue 文件即为一个页面。index.vue 为根页面。
若需要二级页面,则添加文件夹即可。
如果页面的名称类似于 _id.vue
(以 _
开头),则为动态路由页面,_
后为匹配的变量(params)。
若变量是必须的,则在文件夹下建立空文件 index.vue
。更多的配置请移步至 官网 。
plugin : 插件。用于组织和配置,那些需要在 根vue.js应用
实例化之前需要运行的 Javascript 插件,需要配合nuxt.config.js
static : 静态文件。放置不需要经过 webpack 打包的静态资源。如一些 js, css 库。
store : Nuxt.js 框架集成了 Vuex 状态树 的相关功能配置,在 store
目录下创建一个 index.js
文件可激活这些配置。
nuxt.config.js : nuxt.config.js
文件用于组织Nuxt.js 应用的个性化配置,以便覆盖默认配置。具体配置请移步至 官网 。
Nuxt生命周期 参考文章:https://www.cnblogs.com/XF-eng/p/14611496.html
众所周知,Vue 的生命周期全都跑在客户端(浏览器),而Nuxt的生命周期有些在服务端(Node)、客户端,甚至两边都在:
以上是 nuxt.js 的生命周期流程图,红框内的是Nuxt 的生命周期(运行在服务端),黄框内同时运行在服务端&&客户端上,绿框内则运行在客户端。
因为 红框、黄框内的周期都不存在Window对象,所以不能直接使用window
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <script> export default { asyncData() { console.log(window) // 服务端报错 }, fetch() { console.log(window) // 服务端报错 }, created () { console.log(window) // undefined }, mounted () { console.log(window) // Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, frames: Window, …} } } </script>
nuxtServerInit (SSR) nuxtServerInit 对stroe操作
1.服务器初始化 2.只能够在store/index.js中使用 3.用于在渲染页面之前存储数据到vuex中
第一个参数是:vuex上下文 第二个参数是:Nuxt上下文
store/index.js
1 2 3 4 5 export const actions = { nuxtServerInit (store, context ) { console .log ('nuxtServerInit' , store, context) } }
1 2 3 4 5 6 7 8 9 10 export const actions = { nuxtServerInit (store, {app:{$cookies},route,$axios,req,res,redirect} ) { let user = $cookies.get ('user' ) ? $cookies.get ('user' ) : {err :2 ,msg :'未登录' ,token :'' }; store.commit ('user/M_UPDATE_USER' ,user) } }
middleware(SSR)
中间件执行流程顺序:
nuxt.config.js
->匹配布局->匹配页面
middleware nuxt.config outside
->middleware layouts
->middleware pages
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 export default ({app :{$cookies},store,redirect,route,$axios,params,query,req,res})=>{ }router : { middleware : 'auth' }middleware ( ){..}, middleware :'auth' , middleware ( ){..}, middleware :'auth' ,
nuxt.config.js 新建middleware/auth.js
1 2 3 4 5 export default ({store, route, redirect, params, query, req, res}) => { console .log ('middleware/auth.js' ) }
此外还需要在nuxt.config.js中新增router配置
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 export default { head : { title : 'nuxt01-start' , htmlAttrs : { lang : 'en' }, meta : [ { charset : 'utf-8' }, { name : 'viewport' , content : 'width=device-width, initial-scale=1' }, { hid : 'description' , name : 'description' , content : '' }, { name : 'format-detection' , content : 'telephone=no' } ], link : [ { rel : 'icon' , type : 'image/x-icon' , href : '/favicon.ico' } ] },` router: { middleware: 'auth' },` css : [ ], plugins : [ ], components : true , buildModules : [ ], modules : [ ], build : { } }
布局 新建layouts/default.vue文件
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 class="d-layout"> <nuxt/> </div> </template> <script> export default { ` // middleware: 'auth' // 页面层级中间件定义 middleware() { console.log('middleware->layouts/default.vue') }` } </script> <style scoped> .d-layout { width: 100%; height: 100%; border: 1px solid red; background-color: #ccc; } </style>
修改pages/index.vue的布局
1 2 3 4 5 6 7 8 9 10 11 <template> <Tutorial/> </template> <script> export default { name: 'IndexPage', ` layout: 'default'` } </script>
页面 pages/index.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <template> <Tutorial/> </template> <script> export default { name: 'IndexPage', layout: 'default', ` // middleware: 'auth' // 页面层级中间件定义 middleware() { console.log('middleware->pages/index.vue') }` } </script>
validate(SSR) 下来请求到达 validate 方法,在这里可以对 page 组件 component 组件 进行动态路参数的有效性。返回 true
说明路由有效,则进入路由页面。返回不是 true
则显示 404 页面。
只能在页面组件使用(pages/xx.vue)
可以让你在动态路由对应的页面组件中配置一个校验方法用于校验动态路由参数的有效性。
pages/index.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <template> <Tutorial/> </template> <script> export default { name: 'IndexPage', layout: 'default', // middleware: 'auth' // 页面层级中间件定义 middleware() { console.log('middleware->pages/index.vue') }, ` // 参数的有效性 validate({params, query}) { console.log('validate', params, query) return true; // true页面才会显示 }` } </script>
asyncData(SSR) 这个方法可以使得你能够在渲染组件之前异步获取数据。好比你在vue组件中用created获取数据一样,不同的是asyncData是在服务端执行的 还有要注意的是:asyncData只是在首屏的时候调用一次(即页面渲染之前,所以事件触发不了它)
pages/index.vue
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 30 31 32 33 34 35 36 37 38 39 <template> <Tutorial/> </template> <script> export default { name: 'IndexPage', layout: 'default', // middleware: 'auth' // 页面层级中间件定义 middleware() { console.log('middleware->pages/index.vue') }, // 参数的有效性 validate({params, query}) { console.log('validate', params, query) return true; // true页面才会显示 }, ` // 读数据,返回给组件 asyncData(context) { // 异步业务逻辑,读取服务器数据 console.log('asyncData') // 返回的数据会和data里面的数据合并 return { b: 2 } },` data() { return { a: 1, b: 1111 } } } </script>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 async asyncData (context ){ let res = await context.$axios({url :'/api/goods/home' }) return {msg2 :'oo' ,data :res.data } } asyncData ({ params }) { return axios.get (`https://my-api/posts/${params.id} ` ) .then ((res ) => { return { title : res.data .title } }) }async fetch (context ){ let res = await context.$axios({url :'/api/goods/home' }) context.store .commit ('XXX' ,res.data ); }
fetch(SSR) fetch 方法用于在渲染页面前填充应用的状态树(store)数据, 与 asyncData 方法类似,不同的是它不会设置组件的数据。 如果页面组件设置了 fetch 方法,它会在组件每次加载前被调用(在服务端或切换至目标路由之前)。 fetch 方法的第一个参数 是页面组件的上下文对象 context,我们可以用 fetch 方法来获取数据填充应用的状态树。为了让获取过程可以异步,你需要返回一个 Promise ,Nuxt.js 会等这个 promise 完成后再渲染组件。注意 ,在fetch阶段是无法使用this获取组件实例,fetch是在组件初始化之前被调用(好像fetch函数也会在created和beforeMount之间执行一次)
pages/index.vue
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 <template> <Tutorial/> </template> <script> export default { name: 'IndexPage', layout: 'default', // middleware: 'auth' // 页面层级中间件定义 middleware() { console.log('middleware->pages/index.vue') }, // 参数的有效性 validate({params, query}) { console.log('validate', params, query) return true; // true页面才会显示 }, // 读数据,返回给组件 asyncData(context) { // 异步业务逻辑,读取服务器数据 console.log('asyncData') // 返回的数据会和data里面的数据合并 return { b: 2 } }, ` // 读数据,返回给vuex fetch({store}) { // 异步业务逻辑,读取服务器数据提交给vuex console.log('fetch') },` data() { return { a: 1, b: 1111 } } } </script>
1 2 3 4 5 6 7 8 9 10 11 12 13 <template> <h1>Stars: {{ $store.state.stars }}</h1> </template> <script> export default { fetch({ store, params }) { return axios.get('http://my-api/stars').then(res => { store.commit('setStars', res.data) }) } } </script>
如果要在fetch中调用并操作store,请使用store.dispatch,但是要确保在内部使用async / await等待操作结束:
1 2 3 4 5 6 7 <script> export default { async fetch({ store, params }) { await store.dispatch('GET_STARS') } } </script>
beforeCreat和created阶段(SSR && CSR) pages/index.vue
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 <template> <Tutorial/> </template> <script> export default { name: 'IndexPage', layout: 'default', // middleware: 'auth' // 页面层级中间件定义 middleware() { console.log('middleware->pages/index.vue') }, // 参数的有效性 validate({params, query}) { console.log('validate', params, query) return true; // true页面才会显示 }, // 读数据,返回给组件 asyncData(context) { // 异步业务逻辑,读取服务器数据 console.log('asyncData') // 返回的数据会和data里面的数据合并 return { b: 2 } }, // 读数据,返回给vuex fetch({store}) { // 异步业务逻辑,读取服务器数据提交给vuex console.log('fetch') }, ` //SSR && CSR beforeCreate(){ console.log('beforeCreate') }, created(){ // console.log('created',this) console.log('created') },` data() { return { a: 1, b: 1111 } } } </script>
其他vue生命周期函数(CSR) pages/index.vue
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 <template> <Tutorial /> </template> <script> export default { name: "IndexPage", layout: "default", // middleware: 'auth' // 页面层级中间件定义 middleware() { console.log("middleware->pages/index.vue"); }, // 参数的有效性 validate({ params, query }) { console.log("validate", params, query); return true; // true页面才会显示 }, // 读数据,返回给组件 asyncData(context) { // 异步业务逻辑,读取服务器数据 console.log("asyncData"); // 返回的数据会和data里面的数据合并 return { b: 2, }; }, // 读数据,返回给vuex fetch({ store }) { // 异步业务逻辑,读取服务器数据提交给vuex console.log("fetch"); }, //SSR && CSR beforeCreate() { console.log("beforeCreate"); }, created() { // console.log('created',this) console.log("created"); }, //CSR window this指向组件 beforeMount() { console.log('beforeMount') }, mounted() { console.log("mounted") }, beforeUpdate() { console.log('beforeUpdate') }, updated() { console.log("updated") }, beforeDestroy() { console.log('beforeDestroy') }, destroyed() { console.log('destroyed') }, ///服务端渲染 激活、失活 不存在 // activated(){}, // deactivated(){}, data() { return { a: 1, b: 1111, }; }, }; </script>
路由 约定式路由 展示区: name:路由名目录名-其他目录-文件名 params: key要对等文件名 子路由:目录代表子路由,子路由内部同级的文件,代表是同级一级路由 配置 声明式跳转:<nuxt-link :to=" name: 'product-id' ,params:{id:3] ,query:{a:111,b:222}}">商品e3</nuxt-link>
动态路由,_名称 ,加下划线,代表变量
路径从pages出发为依据
layouts/default.vue
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 <template> <div class="d-layout"> <!-- 跳转 声明式跳转 router-link --> <nuxt-link to="/">首页</nuxt-link> <nuxt-link to="/goods">商品页面</nuxt-link> <nuxt-link to="/userinfo">用户信息</nuxt-link> <nuxt-link :to="{name:'login',query:{a:11,b:22}}">登陆页面</nuxt-link> <!-- 展示区 ~~ router-view --> <nuxt /> </div> </template> <script> export default { // middleware: 'auth' // 页面层级中间件定义 middleware() { console.log('middleware->layouts/default.vue') } } </script> <style scoped> .d-layout { width: 100%; height: 100%; border: 1px solid red; background-color: #ccc; } </style>
pages/index.vue
1 2 3 4 5 <template> <div> <h1>首页</h1> </div> </template>
pages/UserInfo.vue
1 2 3 4 5 <template> <div> 用户信息页面 </div> </template>
pages/goods.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <template> <div> 商品页面 <hr> <nuxt-link to="/goods/1?a=1&b=2">商品01</nuxt-link> <nuxt-link :to="{name:'goods-id',params:{id:2},query:{a:111,b:222}}">商品02</nuxt-link> <nuxt-link :to="{name:'goods-id',params:{id:3},query:{a:111,b:222}}">商品03</nuxt-link> <nuxt-link to="/goods/comment">评论</nuxt-link> <nuxt/> </div> </template> <script> export default { } </script> <style> </style>
pages/goods/_id.vue
1 2 3 4 5 <template> <div> 商品详情页面{{ $route.params }} </div> </template>
pages/goods/comment.vue
1 2 3 4 5 6 7 8 9 10 <template> <div> 评论页面 <hr> <nuxt-link :to="{name: 'goods-comment-uid', params: {uid: 1}, query: {a: 111, b: 2222}}">评论01</nuxt-link> <nuxt-link to="/goods/comment/2?a=111&b=222">评论02</nuxt-link> <nuxt/> </div> </template>
pages/goods/comment/_uid.vue
1 2 3 4 5 <template> <div> 评论页面{{ $route.params }} </div> </template>
展示区层级控制
Path
File
/
index.vue
/goods
goods/index.vue
/goods/123
goods/_id.vue
/goods/comment
goods/comment.vue
/about
_.vue
/about/careers
_.vue
/about/careers/chicago
_.vue
pages/一级展示/二级展示 /index.vue会在一级展示 /index.vue空文档代表有默认页,不会找寻其他_详情.vue
处理 404 页面,现在符合_.vue
页面的逻辑
比如说想要商品详情页面一级展示就需要,把pages/goods.vue移至pages/goods/index.vue
扩展性路由 先将跳转抽取到app-header文件中
exact-active-class="xxx"
,xxx样式,加上exact,代表严格匹配
layouts/app-header.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <template> <div> <!-- 跳转 声明式跳转 router-link --> <nuxt-link to="/index" exact-active-class="app_header--active">首页</nuxt-link> <nuxt-link to="/goods" active-class="app_header--active">商品页面</nuxt-link> <nuxt-link to="/userinfo" active-class="app_header--active">用户信息</nuxt-link> <nuxt-link :to="{name:'login',query:{a:11,b:22}}" active-class="app_header--active">登陆页面</nuxt-link> </div> </template> <script> export default { } </script> <style scoped> .app_header--active { color: red; background-color: blueviolet; } </style>
nuxt.config.js
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 export default { head : { title : 'nuxt01-start' , htmlAttrs : { lang : 'en' }, meta : [ { charset : 'utf-8' }, { name : 'viewport' , content : 'width=device-width, initial-scale=1' }, { hid : 'description' , name : 'description' , content : '' }, { name : 'format-detection' , content : 'telephone=no' } ], link : [ { rel : 'icon' , type : 'image/x-icon' , href : '/favicon.ico' } ] }, router : { middleware : 'auth' , ` // 扩展路由 extendRoutes(routes, resolve) { console.log(routes) routes.push({ name: 'home', path: '/index', component: resolve(__dirname, 'pages/index.vue') }) }` }, css : [ ], plugins : [ ], components : true , buildModules : [ ], modules : [ ], build : { } }
参数校验 pages/goods/_id.vue
validate函数返回true才可以访问该页面,返回false会跳转到error页面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <template> <div> 商品详情页面{{ $route.params }} </div> </template> <script> export default { // 参数有效性判断 validate({ params, query }) { return typeof params.id === 'number' } } </script> <style> </style>
错误页面定制 预定式,错误页面为error.vue
layouts/error.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <template> <div> <h1 v-if="error.statusCode">{{error.message}}</h1> <h1 v-else>应用发生异常</h1> <button @click="$router.replace('/index')">跳转到首页</button> </div> </template> <script> export default { // 接受错误信息 error: {statusCode, message } props: ['error'] } </script> <style> </style>
路由统一动效 assets/css/transition.css
1 2 3 4 5 6 7 8 9 10 .page-enter-active , .page-leave-active { transition : opticity .5s ; }.page-enter , .page-leave-active { opacity : 0 ; }
nuxt.config.js
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 export default { head : { title : 'nuxt01-start' , htmlAttrs : { lang : 'en' }, meta : [ { charset : 'utf-8' }, { name : 'viewport' , content : 'width=device-width, initial-scale=1' }, { hid : 'description' , name : 'description' , content : '' }, { name : 'format-detection' , content : 'telephone=no' } ], link : [ { rel : 'icon' , type : 'image/x-icon' , href : '/favicon.ico' } ] }, router : { middleware : 'auth' , extendRoutes (routes, resolve ) { console .log (routes) routes.push ({ name : 'home' , path : '/index' , component : resolve (__dirname, 'pages/index.vue' ) }) } }, ` css: [ 'assets/css/transition.css' ],` plugins : [ ], components : true , buildModules : [ ], modules : [ ], build : { } }
路由独享动效 assets/transition.css
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 .page-enter-active , .page-leave-active { transition : opticity .5s ; }.page-enter , .page-leave-active { opacity : 0 ; }.test-enter-active , .test-leave-active { transition : .5s ease all; }.test-enter , .test-leave-active { margin-left : -1000px ; }
pages/goods/_id.vue
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 <template> <div> 商品详情页面{{ $route.params }} </div> </template> <script> export default { // 参数有效性判断 validate({ params, query }) { return typeof params.id === 'number' }, // transition: '动画名' transition: 'test' } </script> <style scoped> /** .test-enter-active, .test-leave-active { transition: .5s ease all; } .test-enter, .test-leave-active { margin-left: -1000px; } */ </style>
路由守卫 前置:依赖中间件middlware,插件
全局守卫: nuxt.config指向middleware
layouts定义中间件
组件独享守卫: middleware(写法同layouts)
插件全局前置守卫
后置:组件独享后置守卫,使用vue的beforeRouteLeave钩子
插件全局后置守卫
前置守卫 第一种方式: nuxt.config.js
在middleware中指定auth.js文件(middleware/auth.js)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 export default { router : { `middleware: 'auth',` extendRoutes (routes, resolve ) { console .log (routes) routes.push ({ name : 'home' , path : '/index' , component : resolve (__dirname, 'pages/index.vue' ) }) } } }
middleware/auth.js
1 2 3 4 5 6 7 8 9 10 export default ({store, route, redirect, params, query, req, res}) => { console .log ('middleware/auth.js' ) redirect ('/login' ) }
第二种方式:
layouts/default.vue
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 30 31 32 33 34 35 <template> <div class="d-layout"> <app-header></app-header> <!-- 展示区 ~~ router-view --> <nuxt /> </div> </template> <script> import appHeader from './app-header.vue' export default { components: { appHeader }, // middleware: 'auth' // 页面层级中间件定义 ` middleware({store,route,redirect,params,query}) { //context 服务端上下文 //全局守卫前置业务 //store 状态树信息 //route 一条目标路由信息 // redirect 强制跳转 //params,query 校验参数合理性 console.log('middleware layouts 全局守卫前置业务') console.log('middleware->layouts/default.vue') redirect('/login') }` } </script> <style scoped> .d-layout { width: 100%; height: 100%; border: 1px solid red; background-color: #ccc; } </style>
第三种方式:组件独享守卫
pages/index.vue
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 30 31 <template> <div> <h1>首页</h1> </div> </template> <script> export default { name: "IndexPage", layout: "default", // middleware: 'auth' // 页面层级中间件定义 middleware({store,route,redirect,params,query}) { //context 服务端上下文 //全局守卫前置业务 //store 状态树信息 //route 一条目标路由信息 // redirect 强制跳转 //params,query 校验参数合理性 console.log('middleware layouts 全局守卫前置业务') console.log("middleware->pages/index.vue"); redirect('/userinfo') }, // 参数的有效性 validate({ params, query }) { console.log("validate", params, query); return true; // true页面才会显示 } }; </script>
点击首页,会跳转到用户信息页面
第四种方式: 插件全局前置守卫
nuxt.config.js
//nuxt.config文件,~代表根目录
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 export default { head : { title : 'nuxt01-start' , htmlAttrs : { lang : 'en' }, meta : [ { charset : 'utf-8' }, { name : 'viewport' , content : 'width=device-width, initial-scale=1' }, { hid : 'description' , name : 'description' , content : '' }, { name : 'format-detection' , content : 'telephone=no' } ], link : [ { rel : 'icon' , type : 'image/x-icon' , href : '/favicon.ico' } ] }, router : { middleware : 'auth' , extendRoutes (routes, resolve ) { console .log (routes) routes.push ({ name : 'home' , path : '/index' , component : resolve (__dirname, 'pages/index.vue' ) }) } }, css : [ 'assets/css/transition.css' ], ` plugins: [ //nuxt.config文件,~代表根目录 '~/plugins/router' ],` }
plugins/router.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 export default ({app,redirect,params,query,store})=>{ console .log ('插件前置守卫' ) app.router .beforeEach ((to,from ,next )=> { if (to.path === '/login' ) { next () } else { redirect ('/login' ) } }) }
后置守卫 第一种方式:
插件
plugins/router.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 export default ({app,redirect,params,query,store})=>{ console .log ('插件前置守卫' ) app.router .beforeEach ((to,from ,next )=> { if (to.path === '/login' ) { next () } else { redirect ('/login' ) } }) ` //插件全局后置守卫 app.router.afterEach((to,from)=>{ console.log('插件全局后置守卫') }) ` }
第二种方式:组件独享后置守卫
pages/index.vue
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 30 31 32 33 34 35 36 <template> <div> <h1>首页</h1> </div> </template> <script> export default { name: "IndexPage", layout: "default", // middleware: 'auth' // 页面层级中间件定义 middleware({store,route,redirect,params,query}) { //context 服务端上下文 //全局守卫前置业务 //store 状态树信息 //route 一条目标路由信息 // redirect 强制跳转 //params,query 校验参数合理性 // console.log('middleware layouts 全局守卫前置业务') // console.log("middleware->pages/index.vue"); // redirect('/userinfo') }, // 参数的有效性 validate({ params, query }) { console.log("validate", params, query); return true; // true页面才会显示 }, ` // 组件独享后置守卫 beforeRouteLeave(to,from,next){ let b1 = window.confirm('是否要离开') next(b1) }` }; </script>
数据交互、跨域 安装axios、proxy
1 npm i @nuxtjs/axios @nuxtjs/proxy --save
数据交互 配置nuxt.config.js
1 2 3 4 5 6 7 8 9 export default { ` modules: [ '@nuxtjs/axios' ],` }
先模拟数据
static/data/list.json
发送请求获取数据
pages/index.vue
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 30 31 <template> <div> <h1>首页</h1> <h3>{{title}}</h3> </div> </template> <script> export default { name: "IndexPage", layout: "default", // 读数据,返回给组件 ` async asyncData({$axios}) { // 异步业务逻辑,读取服务器数据 let res = await $axios({url: '/data/list.json'}) console.log('读取到的静态数据', res.data) // 返回的数据会和data里面的数据合并 return { title: res.data.title } },` // 读数据,返回给vuex fetch({ store }) { // 异步业务逻辑,读取服务器数据提交给vuex console.log("fetch"); } }; </script>
跨域 解决跨域
nuxt.config.js
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 module .exports = { modules : [ '@nuxtjs/axios' ], ` axios:{ proxy:true,//开启axios跨域 // prefix:'/api',//baseUrl }, proxy:{ '/api/':{ target:'http://localhost:3001',//代理转发的地址 changeOrigin:true, pathRewrite:{ // '^/api':'' } } }` }
拦截器配置与token携带 nuxt.config.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 export default { plugins : [ '~/plugins/router' , ` { src: '~/plugins/axios', ssr: true // 服务器 }` ], modules : [ '@nuxtjs/axios' ], }
plugins/axios.js
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 30 31 export default function ({$axios,redirect,route,store} ){ $axios.defaults .timeout =10000 ; $axios.onRequest (config => { console .log ('请求拦截' ) config.headers .token = 'eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4YTZlNDk0MWFhZTI0OTViOGViYjllNzc1ZDQ5MWEzYiIsInN1YiI6IjEiLCJpc3MiOiJkZXAiLCJpYXQiOjE2OTI2MTQ5NDQsImV4cCI6MTY5MjcwMTM0NH0.GqanFI_WzYAfiaTFMcyk9xgNv5Aw2M0PlAiRF_bEyhY' return config }) $axios.onResponse (res => { console .log ('响应拦截' ) if (res.code !== 200 ) { consolelog ('认证失败' ) if (res.code === 401 ) { store.dispatch ('user/logout' ) redirect ('/login?path=' +route.fullPath ) } } else { return res } }) $axios.onError (error => { return error; }) }
pages/index.vue
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 30 31 32 33 <template> <div> <h1>首页</h1> <h3>{{title}}</h3> </div> </template> <script> export default { name: "IndexPage", layout: "default", // 读数据,返回给组件 async asyncData({$axios}) { // 异步业务逻辑,读取服务器数据 let res = await $axios({url: '/data/list.json'}) console.log('读取到的静态数据', res.data) // 返回的数据会和data里面的数据合并 return { title: res.data.title } }, // 读数据,返回给vuex async fetch({ store, $axios}) { // 异步业务逻辑,读取服务器数据提交给vuex // 测试跨域数据 ` let res = await $axios({url: '/api/sys/user'}) console.log('读取的跨域数据', res.data) console.log("fetch");` } }; </script>
loading页配置与定制 nuxt.config.js
1 2 3 4 5 6 7 export default { loading : {color :'#399' , height : ' 3px' }, }
当然我们也可以使用自定义组件
components/loadding.vue
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 <template> <div v-if="loading" class="spinner"></div> </template> <script> export default { data: () => ({ loading: false, }), methods: { // 这两个函数使nuxt为我们提供的,一个是请求开始,一个是结束 start() { this.loading = true; }, finish() { this.loading = false; }, }, }; </script> <style scoped> .spinner { width: 60px; height: 60px; background-color: #399; position: fixed; left: 50%; top: 50%; margin-left: -30px; margin-top: -30px; -webkit-animation: rotateplane 1.2s infinite ease-in-out; animation: rotateplane 1.2s infinite ease-in-out; } @-webkit-keyframes rotateplane { 0% { -webkit-transform: perspective(120px); } 50% { -webkit-transform: perspective(120px) rotateY(180deg); } 100% { -webkit-transform: perspective(120px) rotateY(180deg) rotateX(180deg); } } @keyframes rotateplane { 0% { transform: perspective(120px) rotateX(0deg) rotateY(0deg); -webkit-transform: perspective(120px) rotateX(0deg) rotateY(0deg); } 50% { transform: perspective(120px) rotateX(-180.1deg) rotateY(0deg); -webkit-transform: perspective(120px) rotateX(-180.1deg) rotateY(0deg); } 100% { transform: perspective(120px) rotateX(-180deg) rotateY(-179.9deg); -webkit-transform: perspective(120px) rotateX(-180deg) rotateY(-179.9deg); } } </style>
然后修改nuxt.config.js配置文件
1 2 3 4 5 6 7 8 export default { loading : '~/components/loadding.vue' , }
vuex定义和使用 模块方式: store目录下的每个.js
文件会被转换成为状态树指定命名的子模块 (当然,index
是根模块)
Classic(不建议使用): store/index.js返回创建Vuex.store实例的方法。
state必须是一个函数(规定),其他可以为对象,然后批量导出index内的state,mutations,actions,getters
基本使用 store/index.js
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 30 31 32 33 export const state = ( ) => ({ bNav : false , bLoading : false });export const mutations = { M_UPDATE_NAV (state, payload ) { state.bNav = payload; }, M_UPDATE_LOADING (state, payload ) { state.bLoading = payload; } }export const actions = { nuxtServerInit (store, { app: { $cookies } } ) { let user = { err : 2 , msg : '未登录' , token : '' } store.commit ('user/M_UPDATE_USER' , user) } }export const getters = { getNav (state ) { return state.bNav ? '显示' : '隐藏' } }
store/user.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 export const state = ( ) => ({ err : 1 , msg : '未登录' , token : '' , data : {} })export const mutations = { M_UPDATE_USER (user, payload ) { user.err = payload.err ; user.msg = payload.msg ; user.data = payload.data ; user.token = payload.token ; } }export const actions = { A_UPDATE_USER ({ commit, user }, payload ) { commit ('M_UPDATE_USER' , payload) } }
store/home.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 export const state = ( ) => ({ err : 1 , data : {} })export const mutations = { M_UPDATE_HOME (state, payload ) { state.err = payload.err ; state.data = payload.data ; } }export const actions = { A_UPDATE_HOME ({ commit, state }, payload ) { commit ('M_UPDATE_HOME' , { err : 0 , data : { title : "home 模块 actions所传递的数据" } }) } }
pages/index.vue
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 30 31 32 33 34 35 36 37 38 <template> <div> <h1>首页</h1> <button @click="getStore">点击修改vuex中的数据</button> </div> </template> <script> import {mapActions, mapGetters, mapState, mapMutations} from 'vuex' export default { name: "IndexPage", layout: "default", methods:{ getStore(){ //编程式访问vuex //发出actions请求给user模块 // this.$store.dispatch('user/A_UPDATE_USER',{err:0,msg:'登录成功',token:'假token',data:{title:"user模块的actions提交过来的数据"}}) // this.A_UPDATE_USER({err:0,msg:'登录成功',token:'假token',data:{title:"123"}}) //发出mutations请求给user模块 // this.$store.commit('user/M_UPDATE_USER',{err:0,msg:'登录成功',token:'假token',data:{title:"组件携带过去的数据"}}) this.M_UPDATE_USER({err:0,msg:'登录成功',token:'假token',data:{title:"456"}}) }, ...mapActions('user',['A_UPDATE_USER']), ...mapMutations('user',['M_UPDATE_USER']), }, computed:{ xx(){}, ...mapGetters(['getNav']), ...mapState(['bNav']), ...mapState('user',['data']), ...mapState({home:state=>state.home.data}), } }; </script>
状态持久化与token校验 安装cookie-universal-nuxt:状态持久化,需要到配置文件的mudules内添加一下,请求自动携带cookie
1 npm i cookie-universal-nuxt --save
思想:登录时,
同步vuex 8& cookie,
强制刷新后(vuex失效),用nuxtServerInit钩子,取出cookies,同步vuex,
axios拦截器读取vuex(vuex存在内存上,读取速度更快;cookies存在磁盘,读取速度慢)
1.同步vuex && cookie
1 2 3 4 5 6 7 this .$cookies .set ('user' ,res.data )this .$store .commit ('user/M_UPDATE_USER ,res.data) //跳转: 1.登录或注册跳转到用户页,2.哪里来回哪里 if(!this.$route.query.path || /login|reg/.test(this.$route.query.path)){ this.$router.replace( ' /user') }else{ this.$router.replace(this.$route.query.path)
2.强刷后,利用nuxtServerInit取出cookies同步vuex
store/index.js
1 2 3 4 5 6 7 8 export const actions = { nuxtServerInit (store,{app:{$cookies}} ) { let user = $cookies.get ('user' )?$cookies.get ('user' ):{err :2 ,msg :'未登录' ,token :'' } store.commit ('user/M_UPDATE_USER' ,user) } }
3.axios拦截器读取vuex
1 2 3 4 5 6 7 8 9 10 11 12 $axios.onRequest (config => { config.headers .token = store.state .user .token return config; }) $axios.onResponse (res => { if (res.data .err === 2 && route.fullPath !== '/login' ){redirect ('/login?path= ' +route.fullPath )return res })
修改配置文件nuxt.config.js
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 30 31 export default { plugins : [ '~/plugins/router' , { src : '~/plugins/axios' , ssr : true } ], axios :{ proxy :true , }, proxy :{ '/api/' :{ target :'http://www.006969.xyz:8000' , changeOrigin :true , pathRewrite :{ '^/api' :'' } } } modules : [ '@nuxtjs/axios' , ` 'cookie-universal-nuxt'` ] }
store/user.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 export const state = ( ) => ({ token : '' })export const mutations = { M_UPDATE_USER (user, payload ) { user.token = payload.token ; } }export const actions = { A_UPDATE_USER ({ commit, user }, payload ) { commit ('M_UPDATE_USER' , payload) } }
// 对token进行持久化
store/index.js
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 30 31 32 33 export const state = ( ) => ({ bNav : false , bLoading : false });export const mutations = { M_UPDATE_NAV (state, payload ) { state.bNav = payload; }, M_UPDATE_LOADING (state, payload ) { state.bLoading = payload; } }export const actions = { nuxtServerInit (store, { app: { $cookies } } ) { ` //初始化token东西到store当中 let user = $cookies.get('token') ? {token: $cookies.get('token')} : { token: '' } store.commit('user/M_UPDATE_USER', user)` } }export const getters = { getNav (state ) { return state.bNav ? '显示' : '隐藏' } }
登陆页面模拟登陆操作
pages/login.vue
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 <template> <div> 登陆页面 <button @click="login">登陆</button> </div> </template> <script> export default { name: 'login', methods: { login(){ this.$axios({ url:'/api/login', method:'post', data:{ username: 'admin', password: '123456' } }).then( res=>{ console.log(res) console.log(res.code) // 登陆成功,token在vuex和cookie中各存一份 if(res.code == 200){ //同步vuex && cookie this.$cookies.set('token',res.data.token) this.$store.commit('user/M_UPDATE_USER',res.data) if(!this.$route.query.path || /login|reg/.test(this.$route.query.path)){ this.$router.replace('/index') }else{ this.$router.replace(this.$route.query.path) } }else{ this.message=res.message } } ) } } } </script> <style> </style>
plugins/axios.js
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 30 31 32 33 34 35 36 37 38 39 import { Message } from 'element-ui' export default function ({$axios,redirect,route,store} ){ $axios.defaults .timeout =10000 ; $axios.onRequest (config => { console .log ('请求拦截' ) console .log (store.state .user .token ) config.headers .token = store.state .user .token return config }) $axios.onResponse (res => { console .log ('响应拦截' ) if (res.data .code !== 200 ) { Message ({ message : res.data .message , type : 'error' }) console .log ('认证失败' ) if (res.data .code === 401 || res.data .code === 403 ) { store.commit ('user/M_UPDATE_USER' , {token : '' }) redirect ('/login?path=' +route.fullPath ) } } else { return res.data } }) $axios.onError (error => { return error; }) }
element-ui使用 基本使用 安装
plugins/element-ui.js
1 2 3 4 5 6 7 8 import Vue from 'vue' import ElementUI from 'element-ui' Vue .use (ElementUI )
nuxt.config.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 export default { css : [ 'assets/css/transition.css' , `'element-ui/lib/theme-chalk/index.css'` ], plugins : [ '~/plugins/router' , { src : '~/plugins/axios' , ssr : true }, ` { src: "~/plugins/element-ui", ssr:true, // 不支持ssr的插件只会在客户端运行不要给true //mode: 'server'// client // v2.44 }` ], build : { `transpile:[ /^element-ui/] ` } }
测试
1 <el-button type="primary">按钮</el-button>
修改导航栏 layouts/app-header.vue
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 <template> <el-menu :default-active="activeIndex" @select="handleSelect" active-text-color="#399" mode="horizontal" > <el-menu-item v-for="(item,index) of navs" :key="index" :index="index+''" >{{item.title}}</el-menu-item> </el-menu> </template> <script> export default { data(){ return { activeIndex:"-1", navs:[ {path:'/index',title:'首页'}, {path:'/goods',title:'商品'}, {path:'/userinfo',title:'用户信息'}, ] } }, methods:{ handleSelect(key,keyPath){ this.$router.push(this.navs[key].path) } }, watch:{ $route:{ immediate:true, handler(route){ let find=false; this.navs.map((item,index)=>{ if(item.path=='/') this.$router.push({name:'root'}) if(route.path==item.path) { // console.log('true') this.activeIndex=index+''; find=true; } }) if(!find) this.activeIndex="-1"; } } } } </script> <style scoped> /* .app_header--active{ background: #399; color:#fff } */ </style>
解决登陆注册页面不需要导航栏问题,也需要修改layputs/default.vue文件
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 <template> <div class="d-layout"> <app-header v-if="bNav"></app-header> <!-- 展示区 ~~ router-view --> <nuxt /> </div> </template> <script> import appHeader from './app-header.vue' export default { components: { appHeader }, // middleware: 'auth' // 页面层级中间件定义 middleware({store,route,redirect,params,query}) { //context 服务端上下文 //全局守卫前置业务 //store 状态树信息 //route 一条目标路由信息 // redirect 强制跳转 //params,query 校验参数合理性 // console.log('middleware layouts 全局守卫前置业务') // console.log('middleware->layouts/default.vue') // redirect('/login') }, data(){ return { bNav:true } }, watch:{ $route:{ immediate:true, handler(route){ if(/login|reg/.test(route.path)){ this.bNav=false; }else{ this.bNav=true; } } } }, } </script> <style scoped> </style>
登陆与注销 pages/login.vue
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 <template> <div class="login"> <h3>登录</h3> <el-divider></el-divider> <el-input v-model="username" placeholder="请输入" class="mb"> <template slot="prepend">用户</template> </el-input> <el-input type="password" v-model="password" placeholder="请输入" class="mb"> <template slot="prepend">密码</template> </el-input> <div class="error">{{message}}</div> <el-button type="primary" @click="login">登录</el-button> <el-button @click="$router.push('/reg')">注册</el-button> </div> </template> <script> export default { name: 'login', data:()=>({ username:'', password:'', message:'' }), methods: { login(){ this.$axios({ url:'/api/login', method:'post', data:{ username: this.username, password: this.password } }).then( res=>{ console.log(res) console.log(res.code) // 登陆成功,token在vuex和cookie中各存一份 if(res.code == 200){ //同步vuex && cookie this.$cookies.set('token',res.data.token) this.$store.commit('user/M_UPDATE_USER',res.data) if(!this.$route.query.path || /login|reg/.test(this.$route.query.path)){ this.$router.replace('/index') }else{ this.$router.replace(this.$route.query.path) } }else{ this.message=res.message } } ) } } } </script> <style> .login{ width:35%; height:auto; position: absolute; left:50%;top:50%; margin-left:-17%; transform: translateY(-50%) } .mb{ margin-bottom: 20px; } .error{ color:red } </style>
pages/userinfo.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <template> <div class="user"> <h3>用户</h3> <el-button @click="logout">注销</el-button> </div> </template> <script> export default { methods:{ logout(){ //删除cookie,情况vuex this.$cookies.remove('token') this.$store.commit('user/M_UPDATE_USER',{token:''}) this.$router.push('/login') } } } </script>
全局方法、过滤器、组件、指令 全局方法 plugins/mixins.js定义全局方法
1 2 3 4 import Vue from 'vue' let show = ( )=>console .log ('全局方法' )Vue .prototype .$show = show
nuxt.config.js配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 export default { plugins : [ '~/plugins/router' , { src : '~/plugins/axios' , ssr : true }, { src : "~/plugins/element-ui" , ssr :true , }, ` '~/plugins/mixins'` ], }
组件调用
1 2 3 mounted ( ) { this .$show() },
全局过滤器 assets/script/filter.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 export function fillzero (n ) { return n < 10 ? '0' + n : '' + n; }export const date = time => { let d = new Date (); d.setTime (time); let year = d.getFullYear (); let month = d.getMonth () + 1 ; let date = d.getDate (); let hour = d.getHours (); let min = d.getMinutes (); let sec = d.getSeconds (); return `${year} 年${fillzero(month)} 月${fillzero(date)} 日 ${fillzero(hour)} :${fillzero(min)} :${fillzero(sec)} ` }
plugins/mixins.js
1 2 3 4 5 6 7 8 9 import Vue from 'vue' let show = ( ) => console .log ('全局方法' )Vue .prototype .$show =show`import * as filters from '../assets/script/filters'; Object.keys(filters).forEach(key=>Vue.filter(key,filters[key]));`
组件使用
1 2 3 4 5 6 7 8 9 10 <template> <div class="user"> <h3>用户</h3> <el-button @click="logout">注销</el-button> <hr> <h3>全局过滤器使用</h3> <p>{{4 | fillzero}}</p> <span>{{1692779835608 | date}}</span> </div> </template>
全局指令 bind绑定的时候触发,inserted插入的时候触发,componentUpdated更新的时候触发
assets/script/directives/direc1.js
1 2 3 4 5 6 7 8 9 function direc1 (el, binding, vnode ) { console .log ('全局指令1' , el, binding, vnode) }export default { bind (el, binding, vnode ) { direc1 (el, binding, vnode) } }
assets/script/directives/direc2.js
1 2 3 4 5 6 7 8 9 10 11 function direc2 (el, binding, vnode ) { console .log ('全局指令2' ) }export default { inserted (el, binding, vnode ) { direc2 (el, binding, vnode) }, componentUpdated (el, binding, vnode ) { direc2 (el, binding, vnode) } }
plugins/mixins.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import Vue from 'vue' let show = ( ) => console .log ('全局方法' )Vue .prototype .$show =showimport * as filters from '../assets/script/filter' ;Object .keys (filters).forEach (key =>Vue .filter (key,filters[key]));import direc1 from '../assets/script/directives/direc1' import direc2 from '../assets/script/directives/direc2' Vue .directive ('direc1' ,direc1)Vue .directive ('direc2' ,direc2)
组件内使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <template> <div class="user"> <h3>全局过滤器使用</h3> <p>{{4 | fillzero}}</p> <span>{{1692779835608 | date}}</span> <hr> <h3>全局指令使用</h3> <div v-direc1="'red'">direc1</div> <div v-direc2>{{title}}</div> </div> </template> <script> export default { data() { return { title: '全局指令' } }, mounted() { this.$show() //打印全局方法 } } </script>
全局组件 components/global/mybutton/index.vue
1 2 3 4 5 6 7 8 <template> <button>全局按钮</button> </template> <script> export default { name: "my-button", }; </script>
plugins/mixins.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import Vue from 'vue' let show = ( ) => console .log ('全局方法' )Vue .prototype .$show =showimport * as filters from '../assets/script/filter' ;Object .keys (filters).forEach (key =>Vue .filter (key,filters[key]));import direc1 from '../assets/script/directives/direc1' import direc2 from '../assets/script/directives/direc2' Vue .directive ('direc1' ,direc1)Vue .directive ('direc2' ,direc2)import myButton from '../ components/global/mybutton' ;Vue .component ('my-button' , myButton)
组件使用
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 <template> <div class="user"> <h3>全局过滤器使用</h3> <p>{{4 | fillzero}}</p> <span>{{1692779835608 | date}}</span> <hr> <h3>全局指令使用</h3> <div v-direc1="'red'">direc1</div> <div v-direc2>{{title}}</div> <hr> <h3>全局组件使用</h3> <my-button></my-button> </div> </template> <script> export default { data() { return { title: '全局指令' } }, mounted() { this.$show() //打印全局方法 } } </script>
网站描述头部信息,优化seo
nuxt.config.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 export default { head : { title : '自定义标题' , htmlAttrs : { lang : 'en' }, meta : [ { charset : 'utf-8' }, { name : 'viewport' , content : 'width=device-width, initial-scale=1' }, { hid : 'description' , name : 'description' , content : '' }, { name : 'format-detection' , content : 'telephone=no' } ], link : [ { rel : 'icon' , type : 'image/x-icon' , href : '/favicon.ico' } ] } }
pages/userInfo.vue
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 <template> <div class ="user" > <h3 > 用户</h3 > <el-button @click ="logout" > 注销</el-button > <hr /> <h3 > 全局过滤器使用</h3 > <p > {{ 4 | fillzero }}</p > <span > {{ 1692779835608 | date }}</span > <hr /> <h3 > 全局指令使用</h3 > <div v-direc1 ="'red'" > direc1</div > <div v-direc2 > {{ title }}</div > <hr /> <h3 > 全局组件使用</h3 > <my-button > </my-button > </div > </template><script > export default { head ( ) { return { title : '商品信息' , meta : [{ name : "keywords" , content : '商品信息内容' }], }; }, data ( ) { return { title : "全局指令" , }; }, mounted ( ) { this .$show(); }, methods : { logout ( ) { this .$cookies .remove ("token" ); this .$store .commit ("user/M_UPDATE_USER" , { token : "" }); this .$router .push ("/login" ); }, }, }; </script >
Vue.mixin mixins.js内定义混入方法
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 30 31 32 33 34 35 36 37 import Vue from 'vue' let show = ( ) => console .log ('全局方法' )Vue .prototype .$show = showimport * as filters from '../assets/script/filter' ;Object .keys (filters).forEach (key => Vue .filter (key, filters[key]));import direc1 from '../assets/script/directives/direc1' import direc2 from '../assets/script/directives/direc2' Vue .directive ('direc1' , direc1)Vue .directive ('direc2' , direc2)import myButton from '../components/global/mybutton' ;Vue .component ('my-button' , myButton)Vue .mixin ({ methods : { $seo(title, content, payload = []) { return { title, meta : [{ hid : 'description' , name : 'keywords' , content }].concat (payload) } } } })
组件使用
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 <template> <div class ="user" > <h3 > 用户</h3 > <el-button @click ="logout" > 注销</el-button > <hr /> <h3 > 全局过滤器使用</h3 > <p > {{ 4 | fillzero }}</p > <span > {{ 1692779835608 | date }}</span > <hr /> <h3 > 全局指令使用</h3 > <div v-direc1 ="'red'" > direc1</div > <div v-direc2 > {{ title }}</div > <hr /> <h3 > 全局组件使用</h3 > <my-button > </my-button > </div > </template><script > export default { head ( ) { return this .$seo('自定义商品标题' ,'自定义商品描述信息' ,[]) }, data ( ) { return { title : "全局指令" , }; }, mounted ( ) { this .$show(); }, methods : { logout ( ) { this .$cookies .remove ("token" ); this .$store .commit ("user/M_UPDATE_USER" , { token : "" }); this .$router .push ("/login" ); }, }, }; </script >
scss使用 下载node-sass sass-loader
1 npm i node-sass sass-loader --save
使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <template> <div class="comment-detail"> <h3>评论详情</h3> <div class="box">box</div> <div class="box2">box2</div> </div> </template> <style lang="scss" scoped> $bg: #399; .box{ background: $bg } .box2{ background: $theme-bg } </style>
全局主题导入
下载@nuxtjs/style-resources,需要配置modules,还需要指定styleResources内的scss文件
安装
1 npm install @nuxtjs/style-resources --save
nuxt.config.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 module .exports = { modules : [ '@nuxtjs/axios' , 'cookie-universal-nuxt' , `'@nuxtjs/style-resources'` ],` styleResources:{ scss:[ './assets/scss/global.scss' ] },` }
assets/scss/gloabl.scss
使用
1 2 3 .box2 { background : $theme-bg }
定义化html模板 项目根路径下创建app.html文件,约定模板
1 2 3 4 5 6 7 8 9 10 11 <!DOCTYPE html > <html {{HTML_ATTRS }}> <head {{HEAD_ATTRS }}> {{HEAD}} </head > <body {{BODY_ATTRS }}> {{APP}}</body > </html >
资源指向与引入 ~
代表根路径
static无优化,不参与打包
accets打包优化,转base64
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <template> <div class="container"> <h4>内部资源指向</h4> <!-- 相对路径找到一些需要压缩的资源 assets --> <!-- <img src="../assets/img/btns.png" alt=""> --> <img src="~assets/img/btns.png" alt=""> <!-- 绝对路径找到无需压缩的资源static --> <img src="/img/bg.jpg" alt=""> <div class="bgimg">css指向需要压缩的资源</div> </div> </template> <script> </script> <style scoped> .bgimg { /* background: url('../assets/img/btns.png') no-repeat; */ background: url('~/assets/img/btns.png') no-repeat; } </style>
全局引入资源
公共文件可以在app.html通过src引入
可以通过nuxt.config.js内的script:[{src:'...'}]
进行添加,或者用link链接
nuxt.config.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 export default { head : { title : '自定义标题' , htmlAttrs : { lang : 'en' }, meta : [ { charset : 'utf-8' }, { name : 'viewport' , content : 'width=device-width, initial-scale=1' }, { hid : 'description' , name : 'description' , content : '' }, { name : 'format-detection' , content : 'telephone=no' } ], ` script:[ {src:'https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js'} ],` link : [ { rel : 'icon' , type : 'image/x-icon' , href : '/favicon.ico' }, ` {rel:'stylesheet',href:'https://fonts.googleapis.com/css?family=Roboto'}` ] } }
局部引入 pages/userinfo.vue
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 class="user"> <el-button onclick="alert($)">测试外部引入资源</el-button> </div> </template> <script> export default { //head选项,页面meta配置 // head() { // return { // title: '商品信息', // meta: [{ name: "keywords", content: '商品信息内容' }], // }; // }, // head() { // return this.$seo('自定义商品标题','自定义商品描述信息',[]) // }, head: { script:[ {src:'https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js'} ] } }; </script>
改装成ts 创建一个新项目
1 npx create-nuxt-app nuxt-ts
项目重写
之前项目都是用javascript来写的,现在我们用typescript来重写
下载@nuxt/typescript-build
1 yarn add --dev @nuxt/typescript-build @nuxt/types typescript@4
配置nuxt.config.js
1 2 3 4 5 6 export default { buildModules : [ '@nuxt/typescript-build' ] }
添加tsconfig.json,types/vue-shim.d.ts配置文件,文件内容在https://typescript.nuxtjs.org/guide/setup.html#configuration
新建tsconfig.json
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 30 31 32 33 34 { "compilerOptions" : { "target" : "ES2018" , "module" : "ESNext" , "moduleResolution" : "Node" , "lib" : [ "ESNext" , "ESNext.AsyncIterable" , "DOM" ] , "esModuleInterop" : true , "allowJs" : true , "sourceMap" : true , "strict" : true , "noEmit" : true , "baseUrl" : "." , "paths" : { "~/*" : [ "./*" ] , "@/*" : [ "./*" ] } , "types" : [ "@nuxt/types" , "@nuxt/typescript-build" , "@types/node" ] } , "exclude" : [ "node_modules" ] }
新建vue-shim.d.ts
1 2 3 4 declare module "*.vue" { import Vue from 'vue' export default Vue }
在tsconfig.json内加上
1 2 3 4 { "skipLibCheck" : true , "experimentalDecorators" : true , }
下载vue-property-decorator和vue-class-component,用类的方式定义vue组件
https://typescript.nuxtjs.org/zh-hant/cookbook/components/
1 npm install vue-property-decorator vue-class-component --save-dev
ps: 项目必须在工作区首个,vscode的bug
组件内的ts
components/loadding.vue
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 <template> <div v-if="loading" class="spinner"></div> </template> <script lang="ts"> import {Vue,Component} from 'vue-property-decorator' @Component export default class Loading extends Vue{ //data元数据 == 实例属性 loading: boolean = false //methods的方法 == 类内的实例方法 start():void{ this.loading = true; } finish():void{ this.loading = false; } } </script> <!-- <script> export default { data: () => ({ loading: false, }), methods: { // 这两个函数使nuxt为我们提供的,一个是请求开始,一个是结束 start() { this.loading = true; }, finish() { this.loading = false; }, }, }; </script> --> <style scoped> .spinner { width: 60px; height: 60px; background-color: #399; position: fixed; left: 50%; top: 50%; margin-left: -30px; margin-top: -30px; -webkit-animation: rotateplane 1.2s infinite ease-in-out; animation: rotateplane 1.2s infinite ease-in-out; } @-webkit-keyframes rotateplane { 0% { -webkit-transform: perspective(120px); } 50% { -webkit-transform: perspective(120px) rotateY(180deg); } 100% { -webkit-transform: perspective(120px) rotateY(180deg) rotateX(180deg); } } @keyframes rotateplane { 0% { transform: perspective(120px) rotateX(0deg) rotateY(0deg); -webkit-transform: perspective(120px) rotateX(0deg) rotateY(0deg); } 50% { transform: perspective(120px) rotateX(-180.1deg) rotateY(0deg); -webkit-transform: perspective(120px) rotateX(-180.1deg) rotateY(0deg); } 100% { transform: perspective(120px) rotateX(-180deg) rotateY(-179.9deg); -webkit-transform: perspective(120px) rotateX(-180deg) rotateY(-179.9deg); } } </style>
components/mybutton/index.vue
1 2 3 4 5 6 7 8 9 10 <template> <button>全局按钮</button> </template> <script lang="ts"> import {Vue,Component} from 'vue-property-decorator' @Component export default class MyButton extends Vue{ } </script>
layouts/app-header.vue
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 <template> <el-menu :default-active="activeIndex" @select="handleSelect" active-text-color="#399" mode="horizontal" > <el-menu-item v-for="(item,index) of navs" :key="index" :index="index+''" >{{item.title}}</el-menu-item> </el-menu> </template> <script lang="ts"> import {Vue,Component,Watch} from 'vue-property-decorator' import {Route} from 'vue-router' type TNavs = {path:string,title:string} @Component export default class AppHeader extends Vue { activeIndex:string = "-1"; navs:TNavs[] = [ {path:'/index',title:'首页'}, {path:'/goods',title:'商品'}, {path:'/userinfo',title:'用户信息'}, ]; handleSelect(key:number):void{ this.$router.push(this.navs[key].path) } @Watch('$route',{immediate:true,deep:true}) onRouteChange(route:Route){ let find=false; this.navs.map((item,index)=>{ if(item.path=='/') this.$router.push({name:'root'}) if(route.path==item.path) { // console.log('true') this.activeIndex=index+''; find=true; } }) if(!find) this.activeIndex="-1"; } } // export default { // data(){ // return { // activeIndex:"-1", // navs:[ // {path:'/index',title:'首页'}, // {path:'/goods',title:'商品'}, // {path:'/userinfo',title:'用户信息'}, // ] // } // }, // methods:{ // handleSelect(key,keyPath){ // this.$router.push(this.navs[key].path) // } // }, // watch:{ // $route:{ // immediate:true, // handler(route){ // let find=false; // this.navs.map((item,index)=>{ // if(item.path=='/') this.$router.push({name:'root'}) // if(route.path==item.path) { // // console.log('true') // this.activeIndex=index+''; // find=true; // } // }) // if(!find) this.activeIndex="-1"; // } // } // } // } </script> <style scoped> /* .app_header--active{ background: #399; color:#fff } */ </style>
layouts/default.vue
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 <template> <div class="d-layout"> <app-header v-if="bNav"></app-header> <!-- 展示区 ~~ router-view --> <nuxt /> </div> </template> <script lang="ts"> import {Vue,Component,Watch} from 'vue-property-decorator' import AppHeader from './app-header.vue' import { Route } from 'vue-router'; @Component({ middleware({store,route,redirect,params,query}){ //context 服务端上下文 //全局守卫前置业务 //store 状态树信息 //route 一条目标路由信息 // redirect 强制跳转 //params,query 校验参数合理性 console.log('middleware layouts 全局守卫前置业务') // redirect('/reg') // console.log('middleware layouts') }, components:{AppHeader} }) export default class Default extends Vue{ bNav:boolean = true; @Watch('$route',{immediate:true}) onRouteChange(route:Route){ if(/login|reg/.test(route.path)){ this.bNav=false; }else{ this.bNav=true; } } } // import appHeader from './app-header.vue' // export default { // components: { appHeader }, // // middleware: 'auth' // 页面层级中间件定义 // middleware({store,route,redirect,params,query}) { // //context 服务端上下文 // //全局守卫前置业务 // //store 状态树信息 // //route 一条目标路由信息 // // redirect 强制跳转 // //params,query 校验参数合理性 // // console.log('middleware layouts 全局守卫前置业务') // // console.log('middleware->layouts/default.vue') // // redirect('/login') // }, // data(){ // return { // bNav:true // } // }, // watch:{ // $route:{ // immediate:true, // handler(route){ // if(/login|reg/.test(route.path)){ // this.bNav=false; // }else{ // this.bNav=true; // } // } // } // }, // } </script> <style scoped> </style>
layouts/error.vue
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 v-if="error.statusCode">{{error.message}}</h1> <h1 v-else>应用发生异常</h1> <button @click="$router.replace('/index')">跳转到首页</button> </div> </template> <script lang="ts"> import {Vue,Component,Prop} from 'vue-property-decorator' @Component export default class Error extends Vue{ @Prop() readonly error:string|undefined } // export default { // // 接受错误信息 error: {statusCode, message } // props: ['error'] // } </script> <style> </style>
新增types/vue.d.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import Vue from 'vue' ;import {NuxtAxiosInstance } from '@nuxtjs/axios' import {NuxtCookies } from 'cookie-universal-nuxt' declare module 'vue/types/vue' { interface Vue { $axios : NuxtAxiosInstance ; $seo :Function ; detail :{title :string ,des :string }; $show :()=> void ; collectionName :string ; $cookies : NuxtCookies ; username :string ; password :string ; } }
同时也把vue-shim.d.ts移到types目录下
pages/login.vue
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 <template> <div class="login"> <h3>登录</h3> <el-divider></el-divider> <el-input v-model="username" placeholder="请输入" class="mb"> <template slot="prepend">用户</template> </el-input> <el-input type="password" v-model="password" placeholder="请输入" class="mb"> <template slot="prepend">密码</template> </el-input> <div class="error">{{message}}</div> <el-button type="primary" @click="login">登录</el-button> <el-button @click="$router.push('/reg')">注册</el-button> </div> </template> <script lang="ts"> import { Vue, Component } from "vue-property-decorator"; @Component export default class Login extends Vue { username:string = ''; password:string = ''; message:string = ''; login(){ this.$axios({ url:'/api/login', method:'post', data:{ username: this.username, password: this.password } }).then( res=>{ console.log(res) console.log(res.code) // 登陆成功,token在vuex和cookie中各存一份 if(res.code == 200){ //同步vuex && cookie this.$cookies.set('token',res.data.token) this.$store.commit('user/M_UPDATE_USER',res.data) if(!this.$route.query.path || /login|reg/.test(this.$route.query.path)){ this.$router.replace('/index') }else{ this.$router.replace(this.$route.query.path) } }else{ this.message=res.message } } ) } } // export default { // name: 'login', // data:()=>({ // username:'', // password:'', // message:'' // }), // methods: { // login(){ // this.$axios({ // url:'/api/login', // method:'post', // data:{ // username: this.username, // password: this.password // } // }).then( // res=>{ // console.log(res) // console.log(res.code) // // 登陆成功,token在vuex和cookie中各存一份 // if(res.code == 200){ // //同步vuex && cookie // this.$cookies.set('token',res.data.token) // this.$store.commit('user/M_UPDATE_USER',res.data) // if(!this.$route.query.path || /login|reg/.test(this.$route.query.path)){ // this.$router.replace('/index') // }else{ // this.$router.replace(this.$route.query.path) // } // }else{ // this.message=res.message // } // } // ) // } // } // } </script> <style> .login{ width:35%; height:auto; position: absolute; left:50%;top:50%; margin-left:-17%; transform: translateY(-50%) } .mb{ margin-bottom: 20px; } .error{ color:red } </style>
pages/UserInfo.vue
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 <template> <div class="user"> <h3>用户</h3> <el-button @click="logout">注销</el-button> <hr /> <h3>全局过滤器使用</h3> <p>{{ 4 | fillzero }}</p> <span>{{ 1692779835608 | date }}</span> <hr /> <h3>全局指令使用</h3> <div v-direc1="'red'">direc1</div> <div v-direc2>{{ title }}</div> <hr /> <h3>全局组件使用</h3> <my-button></my-button> <el-button onclick="alert($)">测试外部引入资源</el-button> </div> </template> <script lang="ts"> import { Vue, Component } from "vue-property-decorator"; @Component({ head:{ script:[ {src:'https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js'} ] } }) export default class UserInfo extends Vue { logout() { //删除cookie,情况vuex this.$cookies.remove("token"); this.$store.commit("user/M_UPDATE_USER", { token: "" }); this.$router.push("/login"); } } // export default { // //head选项,页面meta配置 // // head() { // // return { // // title: '商品信息', // // meta: [{ name: "keywords", content: '商品信息内容' }], // // }; // // }, // // head() { // // return this.$seo('自定义商品标题','自定义商品描述信息',[]) // // }, // head: { // script:[ // {src:'https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js'} // ] // }, // data() { // return { // title: "全局指令", // }; // }, // mounted() { // this.$show(); //打印全局方法 // }, // methods: { // logout() { // //删除cookie,情况vuex // this.$cookies.remove("token"); // this.$store.commit("user/M_UPDATE_USER", { token: "" }); // this.$router.push("/login"); // }, // }, // }; </script>
vuex加入装饰器 安装
pages/index.vue
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 <template> <div class="container"> <el-carousel> <el-carousel-item v-for="item of banner" :key="item._id"> <nuxt-link :to="{name:'goods-id',params:{id:item._id},query:{collectionName:'banner'}}"> <img :src="item.banner" class="img"> <div class="title_bg"> <h3 class="title">{{item.title}}</h3> <h4 class="sub_title">{{item.sub_title}}</h4> </div> </nuxt-link> </el-carousel-item> </el-carousel> <el-row class="home" :gutter="20"> <el-col :span="6" v-for="item of home" :key="item._id"> <el-card :body-style="{padding:'0px'}" style="margin-bottom:20px"> <nuxt-link :to="{name:'goods-id',params:{id:item._id},query:{collectionName:'home'}}"> <img :src="item.detail.auth_icon" alt="" class="image"> </nuxt-link> <div style="padding:14px;"> <span>{{item.title}}</span> <div class="bottom clearfix"> <time class="time">{{item.time}}</time> <el-button class="button" type="warning" icon="el-icon-star-off" circle></el-button> </div> </div> </el-card> </el-col> </el-row> <h4>vuex操作</h4> <button @click="getStore">编程式操作</button> <div>inddex getters:{{getNav}}</div> <div>inddex state:{{bNav}}</div> <div>user state: {{data}}</div> <h4>内部资源指向</h4> <!-- 相对路径找到一些需要压缩的资源 assets --> <!-- <img src="../assets/img/btns.png" alt=""> --> <img src="~assets/img/btns.png" alt=""> <!-- 绝对路径找到无需压缩的资源static --> <img src="/img/bg.jpg" alt=""> <div class="bgimg">css指向需要压缩的资源</div> <h4>外部资源</h4> <el-button onclick="alert($)">测试外部js脚本</el-button> </div> </template> <script lang="ts"> import { Vue, Component, Prop } from "vue-property-decorator"; import { State, Getter, Action, Mutation } from "vuex-class"; @Component({ async asyncData({ app: { $axios } }) { let res = await $axios({ url: "/data/list.json" }); // console.log('读取到的静态资源',res.data) //读取跨域数据 try { let res2 = await $axios({ url: "/api/goods/banner", params: { _limit: 3 } }); // console.log('读取到的跨域资源',res2.data) return { // title:res.data.title, banner: res2.data.data }; } catch (e) {} }, async fetch({ app: { $axios }, store, error }) { let res2 = await $axios({ url: "/api/goods/home", params: { _limit: 20 } }); // console.log('res',res.data.title) res2.data && store.commit("home/M_UPDATE_HOME", { err: 0, data: res2.data.data }); } }) export default class Index extends Vue { @State bNav: boolean | undefined; //装饰一个实例属性bNav引用到state.bNav @State(state => state.home.data) home?: object[]; @State("user") data!: object; //外部state.user 做 组件内的data使用 @Getter getNav!: string; //抓取getters的key,作为组件实例属性使用 //...mapActions('user',['A_UPDATE_USER']), @Action("user/A_UPDATE_USER") A_UPDATE_USER!: (payload: object) => void; //...mapMutations('user',['M_UPDATE_USER']), @Mutation("user/M_UPDATE_USER") M_UPDATE_USER!: (payload: object) => void; getStore() { //编程式访问vuex //发出actions请求给user模块 // this.$store.dispatch('user/A_UPDATE_USER',{err:0,msg:'登录成功',token:'假token',data:{title:"user模块的actions提交过来的数据"}}) // this.A_UPDATE_USER({err:0,msg:'登录成功',token:'假token',data:{title:"123"}}) //发出mutations请求给user模块 // this.$store.commit('user/M_UPDATE_USER',{err:0,msg:'登录成功',token:'假token',data:{title:"组件携带过去的数据"}}) this.M_UPDATE_USER({err:0,msg:'登录成功',token:'假token',data:{title:"456"}}) } //计算属性定义 get xx(): string { return this.bNav ? "真" : "假"; } } </script> <style scoped> .el-carousel__item .img { width: 100%; height: auto; } .el-carousel__item .title_bg { background: rgba(0, 0, 0, 0.5); color: #fff; height: 60px; width: 100%; position: absolute; left: 0; bottom: 0px; z-index: 999; } .home { margin-top: 20px; } .time { font-size: 13px; color: #999; } .bottom { margin-top: 13px; line-height: 12px; } .image { width: 100%; display: block; } .button { padding: 0; float: right; } .bgimg { height: 50px; /* background: url('../assets/img/takeSbmComment.png') no-repeat */ background: url("~assets/img/takeSbmComment.png") no-repeat; } </style>
types/index.ts
类型主题模块
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 30 31 32 33 34 35 36 37 interface IStoreState { bNav :boolean ; bLoading :boolean ; }interface IStoreHome { err :number ; msg?:string ; data :Array <{ _id :string ; des :string ; time :number ; title :string ; detail?:{ auth :string ; auth_icon :string ; content :string ; } }> }interface IStoreUser { err :number ; msg :string ; token :string ; data?:Partial <{ _id :string ; nikename :string ; fans :number ; follow :number ; time :number ; icon :string ; }> }export {IStoreUser , IStoreState , IStoreHome }
stroe/index.ts
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 30 31 32 33 34 35 36 import {IStoreState } from '@/types' export const state = function ( ):IStoreState { return { bNav : false , bLoading : false } };export const mutations = { M_UPDATE_NAV (state:IStoreState, payload:boolean ) { state.bNav = payload; }, M_UPDATE_LOADING (state:IStoreState, payload:boolean ) { state.bLoading = payload; } }export const actions = { nuxtServerInit (store:any, context:any ) { let user = context.app .$cookies .get ('user' ) ? context.app .$cookies .get ('user' ) : {err :2 ,msg :'未登录' ,token :'' }; store.commit ('user/M_UPDATE_USER' ,user) } }export const getters = { getNav (state:IStoreState ){ return state.bNav ? '显示' : '隐藏' } }
store/home.ts
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 import { IStoreHome } from "~/types" ; type TAcion = { commit :(type:string,payload:object )=> void ; state :IStoreHome }export const state=function ( ):IStoreHome { return { err :1 , data :[] } }export const mutations = { M_UPDATE_HOME (state:IStoreHome,payload:IStoreHome ){ state.err =payload.err ; state.data =payload.data ; } }export const actions = { A_UPDATE_HOME ({commit,state}:TAcion,payload:IStoreHome ){ commit ('M_UPDATE_HOME' ,{err :0 ,data :{title :"home 模块 actions所传递的数据" }}) } }
store/user.ts
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 import { IStoreUser } from "~/types" ;type TAction ={ user :IStoreUser ; commit :(type :string ,payload:object )=> void ; }export const state=function ( ):IStoreUser { return { err :1 , msg :'未登录' , token :'' , data :{} } }export const mutations = { M_UPDATE_USER (user:IStoreUser,payload:IStoreUser ){ user.err = payload.err ; user.msg = payload.msg ; user.data = payload.data ; user.token = payload.token ; } }export const actions = { A_UPDATE_USER ({commit,user}:TAction,payload:{} ){ commit ('M_UPDATE_USER' ,payload) } }
部署 nuxt通过代理,将请求转发带真实服务器,部署时,nuxt项目即前端工程3000和真实服务器即后端工程9001都要部署。
nuxt通过server/index.js开启自身端口服务3000
nuxt先打包npm run build,放到服务器3000端口的有:.nuxt /server /static /package-lock.json /package.json /nuxt.config.js
真实服务器即后端工程9001
先配置一下nuxt.config.js
1 2 3 4 5 6 7 module .exports = { mode : 'universal' , server :1 port : 3000 , host :'0.0.0.0' ... }
1 2 3 4 5 6 7 8 9 10 nuxt npm run build 打包 我们需要复制到(阿里云)服务器的文件 3000 目录下 .nuxt package-lock.json package.json nuxt.config.json static server 反向代理
启动node服务
启动前端命令
1 2 3 pm2 start /usr/local/9001 /bin/www --name=node9001cd /usr/local/3000 / pm2 --name=nuxt3000 start npm -- run start
vue-server-renderer和Nuxt区别 vue-server-renderer
和 Nuxt.js
都涉及在服务器端渲染 (Server-Side Rendering, SSR) 上的 Vue.js 应用程序,但它们有不同的作用和使用方式。
vue-server-renderer : vue-server-renderer
是 Vue.js 官方提供的一个库,用于将 Vue 组件渲染为字符串,以便在服务器上进行预渲染或在客户端进行激活。它的主要目的是将 Vue 组件渲染为 HTML 字符串,然后将这些字符串发送给客户端,客户端接收后再激活这些组件。你可以使用它来实现自己的服务器端渲染解决方案,但是需要手动处理路由、数据获取等方面的逻辑。
Nuxt.js : Nuxt.js
是一个基于 Vue.js 的框架,专注于简化 Vue 应用程序的服务器端渲染。它提供了一系列约定和默认配置,使得你可以更容易地构建 SSR 应用程序。Nuxt.js 自动处理路由、数据获取、页面布局等方面的细节,大大减轻了开发者的工作量。Nuxt.js 还提供了一些其他功能,如自动生成路由、代码分割、静态站点生成等。
总的来说,vue-server-renderer
更像是一个底层的库,允许你以自定义的方式处理服务器端渲染,而 Nuxt.js
则是一个高级框架,为你提供了一整套的工具和约定来构建 SSR 应用程序,使得开发过程更加简单和快速。
如果你只是需要在现有的 Vue.js 应用程序中实现一些服务器端渲染的功能,你可以使用 vue-server-renderer
。如果你想从头开始构建一个服务器端渲染的 Vue.js 应用程序,并希望有更多的自动化和便利性,那么 Nuxt.js
是一个更好的选择。