最近公司立了一个新项目,需要做SEO。做过半分离项目(有的数据是后台模板渲染,有的是AJAX获取)的同学都知道和后台人员在同一个页面写代码是多么的难受。项目不易维护,开发体验差,所以大家都知道使用Nuxt.jsSEO,网页可以在搜索引擎中被搜索到,因为他们有自己的爬虫,可以爬取并保存网页内容,但是他们只会保存网页中第一个请求并返回的内容,即HTML文档。在前后端不分离的传统网站中,在浏览器上查看源代码,就可以看到网页上有什么内容,源代码中包含相应的代码。随着前后端分离的开发越来越流行,也衍生出越来越多的单页面应用(SPA)。当你在浏览器中打开一个SPA的源代码时,你只能看到几个标签,其余的内容全部由JS渲染,也就是客户端渲染。因此,单页应用的一大痛点就是搜索引擎无法对其进行索引。有痛点就会产生相应的解决方案,就会出现SSR。SSRSSR表示服务器端渲染。乍一看,你可能会觉得又回到了之前没有前后台分离的开发模式。实际上,与之前相比,客户端和服务器之间多了一个中间层,用于接收数据并将页面渲染回客户端。通过中间层帮助客户端渲染页面,当搜索引擎爬取到页面时,解决了前后端分离页面不收录的痛点。不仅如此,因为中间层也是服务器,前端工程师可以做更多的事情,比如简单的处理数据,充当代理等等。Nuxt.jsVue官方提供了vue-server-renderer模块来实现SSR,Nuxt.js是一个封装的框架,提供开箱即用的NodeJSSSR服务。生命周期在地址栏输入一个链接,或者点击其他网站的链接跳转到Nuxt站点的页面(首屏访问),呈现的第一个页面的数据会被中间层请求到服务器,然后Vue会渲染组件,然后将渲染后生成的HTML文档返回给客户端。然后点击页面的组件或者通过vue-router的push、replace等方式跳转到页面,都是在客户端完成的,除非刷新浏览器,否则不会再次访问中间层服务器.因此,页面中对服务器端数据的请求可能是由中间层发起的,也可能是由客户端发起的。在访问Nuxt站点的页面时,会经历以下生命周期。我们可以处理站点在不同节点的行为,按照nuxt.config.js中插件配置的顺序执行插件。nuxtServerInit:如果Nuxt应用中使用了Vuex,进入页面前会运行actions中的nuxtServerInit函数向Vuex中填充数据。中间件:依次执行nuxt.config.js中配置的路由中间件、布局中间件、页面中间件,可用于验证用户权限。validate():返回true、false或Promise。返回false时会跳转到错误页面,主要用于验证动态路由的参数。asyncData():返回一个对象。Nuxt执行后会将返回的对象合并到Vue实例的data属性中,用于在访问页面前调用接口获取数据。这个钩子取代了SPA中常用的created钩子。beforeCreate:Vue组件生命周期钩子。创建:Vue组件生命周期钩子。fetch():和asyncData一样,也是用来调用接口获取数据。不同之处在于它不会将返回值合并到数据中。一般用于处理本地数据,比如Vuex。值得一提的是,这个钩子是在Vue实例创建之后执行的,所以可以通过这个来访问Vue实例。执行其他Vue生命周期钩子,例如beforeMount和mounted。在上面的生命周期中,只有前两个完全在中间层执行,其余可能在中间层执行,也可能在客户端执行,主要看页面是首屏渲染还是跳入站点,因此了解哪些代码在什么环境中以及何时运行非常重要。插件vant-ui项目使用vantui作为组件库。Nuxt项目中没有类似main.js的入口文件。如果需要安装依赖,需要在插件中声明。每次访问Nuxt站点的第一个屏幕时,nuxt.config.js中声明的插件将被再次执行,以便开发者可以将他们需要的插件注册到应用程序中//nuxt.config.jsmodule.exports={...,plugins:['@/plugins/vant-ui'],...}需要在根目录的plugins目录下创建对应的文件///plugins/vant-ui.jsimportVuefrom'vue'import{Button,Search,Toast}from'vant'Vue.use(Button)Vue.use(Search)Vue.use(Toast)为了按需导入,还需要安装babel-plugin-import并制作相关配置//nuxt.config.jsmodule.exports={build:{analyze:true,transpile:[/vant.*?less/],babel:{//根据需要导入配置插件:[['import',{libraryName:'vant',style:name=>`${name}/style/less.js`},'vant']]},loaders:{less:{lessOptions:{modifyVars:{//这里可以修改less在vantstyle中使用变量来自定义ui风格'@button-primary-background-color':'#000'}}}}}}AxiosNuxt官方提供了一个专门封装的@nuxtjs/axios模块。看了文档发现和普通用法略有不同,麻烦还是用原来的axios,毕竟之前也用过。做了进一步的封装(官方强烈推荐的模块尽量使用,像我这种不听话的人会默默踩坑,摸索后实现了下面这段话)但是和SPA不同的是,axios发起的请求可能是在中间层server上还是client上,取决于是否是首屏渲染(上面生命周期中提到),所以需要在代码中使用process.client和process.server来匹配相应的环境来制作判断并处理它们。importaxiosfrom'axios'import{Toast}from'vant'const{CancelToken}=axios//用于获取nuxt.config.js配置中的baseURLnuxtConfig=require('../nuxt.config')//让requestfunctionhaveaxioscancelTokenexportconstcancelToken=fn=>{constnewFn=(...arg)=>{//每个cancelToken只能使用一次,之后会保持状态//所以每次请求都创建一个是新的cancelTokenconstsource=CancelToken.source()newFn.token=source.tokennewFn.cancel=source.cancelreturnfn(...arg)}returnnewFn}//创建实例functioncreateAxios(){constinstance=axios.create({baseURL:'/api',//中间层代理地址timeout:10000})//请求拦截instance.interceptors.request.use(config=>{//服务器不需要代理if(process.server){config.baseURL=nuxtConfig.env.baseUrl}returnconfig},err=>{returnPromise.reject(err)})//响应拦截instance.interceptors.response.use(res=>{const{data}=resif(data.code!==200){if(process.server){//在插件中,我把context挂载在axios实例上//当服务端发起请求出错时,可以跳转到错误页面instance.nuxtContext.error({statusCode:500,message:''})}else{Toast(data.msg)}returnPromise.reject(res)}returndata.data},err=>{const{response}=err//服务器和客户端有不同的错误处理方式if(process.client){if(response){switch(response.status){case401://未登录Toast('请先登录')breakcase403://操作被拒绝(无相应权限)Toast('您的操作被拒绝')breakcase404:Toast('找不到资源')breakcase500:Toast('Systemerror')break}returnPromise.reject(response)}//取消请求if(axios.isCancel(err)){console.log('requestCancel')returnPromise.reject(err)}//请求超时if(err.message?.includes('timeout')){Toast('网络超时,请重试')returnPromise.reeject(err)}if(!window.navigator.onLine){//断开连接Toast('请检查网络')returnPromise.reject(err)}else{Toast('未知错误')returnPromise.reject(err)}}else{console.log(response)instance.nuxtContext.error({statusCode:response.status,message:''})}})returninstance}exportdefaultcreateAxios()添加插件挂载nuxt上下文到Onaxios实例,用于发生错误时跳转到错误页面。另外,需要记得在nuxt.config.js中声明这个插件///plugins/axios.jsimportaxiosfrom'@/utils/request'//将context对象挂载到axios实例中exportdefaultcontext=>{axios.nuxtContext=context}Vuex在SPA中是持久化的,token通常存储在localStorage中,但是在访问SSR站点首屏时,登录等权限验证必须在中间层进行,数据存储在获取不到客户端的localStorage。但是可以保存在cookie中,利用cookie的特性,在访问站点的时候带到中间层,这样中间层就可以获取cookie中的数据进行授权验证,填充到Vuex中供客户端使用。先安装vuex-persistedstate和js-cookie,将Vuex中的数据同步到cookie中,然后写一个插件将vuex-persistedstate注入到Vuex中(别忘了在nuxt.config.js中声明)。///plugins/vuex-persistedstate.jsimportcreatePersistedStatefrom'vuex-persistedstate'importCookiesfrom'js-cookie'exportdefault({store})=>{//"SynchronizedatainVuextocookie"只有客户端会做if(!process.client){return}createPersistedState({//通过配置修改vuex-persistedstate的读写行为//将操作目标改为cookie存储:{getItem:key=>{returnCookies.get(key)},setItem:(key,value)=>{Cookies.set(key,value,{expires:365})},removeItem:key=>{Cookies.remove(key)}}})(store)}和然后在nuxtServerInit()生命周期钩子中访问期间读取cookie。///store/index.jsimport{SET_USER_INFO}from'./mutation-types'exportconststate=()=>({token:''})exportconstmutations={[SET_TOKEN](state,token){state...(!vuex){return}commit(SET_TOKEN,JSON.parse(vuex).token)}}这样就可以在后续的Middleware、validate()等hook中通过context.store获取需要的数据。另外为了方便管理,我在文件中引入了/store/mutation-types.js。默认情况下,Nuxt会将/store目录下的.js文件视为一个Vuex模块,因此需要配置Nuxt忽略文件,使其忽略mutation-types.js。#/.nuxtignore#ignorestoremutationtypesstore/mutation-types.js使用SCSS为了更舒服地编写CSS,当然需要一个CSS预处理器!为了让common.scss(变量、mixins等)可以在任何组件中使用,需要用@nuxtjs/style-resources模块来实现,第一步是安装,第二步是配置。//nuxt.config.jsmodule.exports={...,buildModules:['@nuxtjs/style-resources'],//配置全局函数,mixin,变量styleResources:{scss:[//注意顺序!以下文件所依赖的文件需要放在前面'@/assets/scss/_variables.scss','@/assets/scss/_functions.scss','@/assets/scss/_mixins.scss']//sass:[],//less:[],//stylus:[]}...}由于浏览器的安全机制,代理会拦截跨域的AJAX请求。为了解决跨域问题,可以设置一个代理,而这个时候服务端的代理不需要是Nginx,可以直接使用中间层作为代理,为客户端请求数据。安装@nuxtjs/proxy后,进行配置。//nuxt.config.jsmodule.exports={...,modules:['@nuxtjs/proxy'],proxy:{'/api':{target:'https://xxx.com',pathRewrite:{'^/api':'/'}}}...}自定义入口文件入口文件主要作用是启动服务和初始化Nuxt核心类。Nuxt对入口文件进行了封装和隐藏。如果看不到,可以更改它。不是,但是我们可以自己做这两件事,这样入口文件就在我们的控制之下,变得更具可扩展性。//在根目录下创建server.jsconst{loadNuxt,build}=require('nuxt')constExpress=require('express')//使用express启动服务,这样就可以在上面做更多的处理中间层,比如使用More中间件等constcookieParser=require('cookie-parser')//之前Vuex持久化使用的cookie-parser就在这里constapp=Express()//自定义入口文件后,nuxt.config.js的server选项会失效,需要手动调用constconfig=require('./nuxt.config')constisDev=process.env.NODE_ENV!=='production'const{host,port}=config.serverasyncfunctionstart(){//我们得到Nuxt实例constnuxt=awaitloadNuxt(isDev?'dev':'start')app.use(cookieParser())//使用Nuxt.jsapp渲染每个路由.use(nuxt.render)//仅在开发模式下使用热重载构建if(isDev){build(nuxt)}//监听服务器app.listen(port,host)console.log('Serverlisteningon`localhost:'+port+'`.')}start()然后修改package.json中的启动命令。{"scripts":{...,"start":"nodeserver.js",...}}结论由于项目还处于早期阶段,能否继续下去还不得而知,所以项目规模非常有限。还有很多点没有讲到。如果大家有什么要补充的或者发现不对的地方,请帮我改正。感谢您的收看。