Nuxt.js小试牛刀—用SSR框架Nuxt.js+koa开发官网

5,416 阅读10分钟

前段时间需要开发HybridAPP中的H5部分,在编写H5的过程中遇到了让我印象深刻的首屏加载白屏的问题。白屏问题受网络影响,众所周知Vue是SPA框架,就是单页面应用,简单来说就是可以把整个项目看成是一个页面,在首屏加载的时候,是加载的整个项目。在项目比较大的时候,网络越差,需要拉取单页面应用的时间就会长,在等待的过程中就会产生白屏。在寻找解决思路的过程中了解到,要想很好解决这个问题,将项目采用SSR方式(即服务端渲染)来开发是个很不错的选择,由此便接触到了基于Vue的SSR框架NuxtJs。在重构官网时,我便用上了NuxtJs。

一、什么是Nuxt.js

简单介绍

简单说就是Vue的SEO化。Vue开发的SPA(单页应用)不利于搜索引擎的SEO优化,所以在开发需要搜索引擎提供流量的项目时就比较尴尬,对于公司官网、新闻、博客、电影、咨询等项目来说,Nuxt.js不仅可以带来巨大的 SEO 提升,而且可以做到服务端拼接好html后直接返回,首屏可以做到无需发起ajax、axios请求。

运行原理

1、用户打开浏览器,输入网址请求到Node.js

2、部署在Node.js的应用Nuxt.js接收浏览器请求,并请求服务端获取数据

3、Nuxt.js获取到数据后进行服务端渲染

4、Nuxt.js将html网页响应给浏览器

img

流程图

下图阐述了 Nuxt.js 应用一个完整的服务器请求到渲染(或用户通过 切换路由渲染页面)的流程:

img

二、Nuxt.js实战

1、创建项目

直接使用脚手架工具 create-nuxt-app进行安装:

npx create-nuxt-app <项目名>

集成服务器端选择koa,UI框架选择ElementUI

2、项目目录结构

项目创建成功之后,生成统一目录结构:

.
├── assets # 资源目录,存放图片、封装工具等
├── components # 组件目录,全局定义的组件
├── layouts # 布局目录,用于组织应用的布局组件
├── middleware # 中间件目录,目录用于存放应用的中间件
├── pages # 页面目录,用于组织应用的路由及视图
├── plugins # 插件目录,用于组织那些需要在实例化之前需要运行的JS插件。
├── server # 服务端目录,定义集成的服务器端文件
├── static # 静态资源目录,存放应用的静态文件
├── store # 仓库目录,用于组织应用的 Vuex 状态树文件
├── styles # 样式目录,存放页面样式文件
├── .eslintrc.js # eslint配置文件
├── .gitignore.js # git 忽略文件配置
├── nuxt.config.js # 应用的配置文件
├── package-lock.json # 版本快照,跑npm install自动生成
├── package.json # 用于描述应用的依赖关系和对外暴露的脚本接口
├── README.md

3、项目配置

项目配置文件是nuxt.config.js,在项目初始化之后,文件中会有一些默认的基础配置,当你的项目需要实现一些定制化功能时,就需要重新进行配置。

  • 配置网页信息。

以往配置网页的一些标题啊,图标什么的都是直接修改根文件index.html,但在这里不一样,因为项目中没有暴露出根文件以供修改,所以需要在配置文件中进行配置。比如说官网的一些配置如下:

module.exports = {
  // ...前面的代码
  head: {
    title: '小马立行科技',
    meta: [
      { charset: 'utf-8' },
      { name: 'renderer', content: 'webkit' },
      { name: 'force-rendering', content: 'webkit' },
      { 'http-equiv': 'X-UA-Compatible', content: 'IE=Edge,chrome=1' },
      { name: 'keyword', content: '小马立行,汽车互联网科技企业,车联网,大数据' },
      { hid: 'description', name: 'description', content: '小马立行-让用户真正的享受到车联网的便利' }
    ],
    link: [
      { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
    ],
    script: []
  }
  // ...后面的代码
}
  • 引入样式文件

如果是直接引入css文件,在css属性中写入相应文件路径。 如果需要在项目中使用预处理css语言文件,比如scss文件,则需要先导入一些第三方依赖包:

cnpm install --save node-sass sass-loader @nuxtjs/style-resources

然后添加style-resource模块,并定义全局的样式变量定义文件variables.scss,如下所示

module.exports = {
  // ...前面的代码
   loading: { color: '#fff' }, // 配置页面加载的颜色
   css: [
    'element-ui/lib/theme-chalk/index.css' // 直接引入css文件
   ]

   styleResources: {
    scss: './styles/variables.scss' // 定义全局的样式变量定义文件
   },
   
   /*
   ** Plugins to load before mounting the App
   ** 该配置项用于配置那些需要在 根vue.js应用 实例化之前需要运行的 Javascript 插件。
   */
   plugins: [
    '@/plugins/element-ui'
   ],
   
   /*
   ** Nuxt.js modules 该配置项允许您将Nuxt模块添加到项目中。
   */
   modules: [
    '@nuxtjs/style-resources' // 添加style-resource模块
   ],
  // ...后面的代码
}

配置完这些之后,便能在项目中使用sass来编写样式。

  • 引入 Javascript 插件

Javascript 插件的引入,可以采取两种方案。 一是通过在项目配置中的plugins熟悉,来引入相应的JS文件: img

/**
* plugins/element-ui.js
*/
import Vue from 'vue'
import locale from 'element-ui/lib/locale/lang/en'
import {
  Carousel,
  CarouselItem,
  Drawer
} from 'element-ui'

Vue.use(Carousel, { locale })
Vue.use(CarouselItem, { locale })
Vue.use(Drawer, { locale })

二是直接在head属性中直接引入。 img

两种方式的差别在于,引入顺序的不同。方法一会在页面渲染后引入,而方法二是在页面渲染时引入。对于一些需要在项目渲染完成前就加载的JS文件,需要采用第二种方式进行引入。例如引入用来解决H5页面终端适配的flexible.js时,就需要采用第二种方式引入,用方法一引入时,在页面渲染完成之前会有一段时间处于布局混乱阶段。

  • 响应式配置 为了让页面在不同分辨率下能够保持良好的布局,需要引入px2rem和flexible来对项目进行改造,在vue项目中引入这两个插件十分简单,添加相应依赖即可,但在nuxt中稍有不同。

导入一些第三方依赖包:

cnpm install --save postcss-plugin-px2rem px2rem-loader

在项目配置文件中进行配置:

删除head中的缩放设定:

 head: {
 meta: [
  //删除的内容
  { name: 'viewport', content: 'width=device-width, initial-scale=1' }
  // 
 ],
}
  // ...前面的代码
  /*
  ** Build configuration 第三方模块
  */
  build: {
    transpile: [/^element-ui/],
    postcss: {
      plugins: {
        'postcss-plugin-px2rem': {
          rootValue: 54, // 换算基数,默认100,自行根据效果调整。
          mediaQuery: false, // (布尔值)允许在媒体查询中转换px。
          minPixelValue: 3 // 设置要替换的最小像素值默认0,这里表示大于3px会被转rem。
        }
      },
      preset: {
        // 更改postcss-preset-env 设置
        autoprefixer: {}
      }
    }
  // ...后面的代码

3、路由配置

不同于vue中可以自由定义的方式,在这里,项目路由和pages目录绑定了,Nuxt.js 依据 pages 目录结构,自动生成 vue-router 模块的路由配置。

比如项目中, pages 目录如下: img

那么你就可以通过http://项目域名/http://项目域名/pagehttp://项目域名/joinhttp://项目域名/project来访问相应的页面了。

4、使用中间件

在开头初始化项目的时候,我们引入了koa,koa是NodeJs的一个服务端集成框架,引入这个框架意味着我们可以在项目中定义中间件来在页面渲染之前定义函数,来执行一些特定的操作。

比如说我在项目中使用了中间件,来对终端进行判断,从而在页面渲染之前判断用户终端是移动端还是Pc端,进而选择展示移动端页面还是PC端页面。

/**
* middleware/device.js
*/

export default function(context) {
  const isMobile = (ua) => {
    return !!ua.match(/AppleWebKit.*Mobile.*/)
  }
  const reg = /\/m|\/m\//
  const hasMPath = reg.test(context.route.fullPath)
  const userAgent = context.req ? context.req.headers['user-agent'] : ''

  if (!isMobile(userAgent)) {
  // 若终端为pc但是路径中包含/m/或者/m的
    if (hasMPath) {
      // ...
    }
  } else {
    // 若终端为mobile但是路径中不包含/m/或者/m的
    // 重定向到mobile
    if (!hasMPath) {
      // ...
    }
  }
}

在项目配置文件中进行配置:

  // ...前面的代码
  router: {
    // 在每页渲染前运行中间件判断终端,是移动端还是pc端
    middleware: ['device']
  },
  // ...后面的代码

5、项目热更新

为了开发便利,需要给项目进行热更新配置,保存代码不用刷新页面便可看到修改效果。 引入依赖包:

cnpm install --save cross-env nodemon

修改webpack配置package.json:

将开发命令

"scripts": {
    "dev": "nuxt",
    "build": "nuxt build",
    "start": "nuxt start",
    "generate": "nuxt generate"
  }

改为

"scripts": {
    "dev": "cross-env NODE_ENV=development nodemon server/index.js --watch server",
    "build": "nuxt build",
    "start": "cross-env NODE_ENV=production node server/index.js",
    "generate": "nuxt generate"
  }

接下来在运行npm run start或是npm run dev时便会启动热更新。

项目基本配置完成,接下来就可以进行开发啦

三、遇到的问题

1、怎么通过判断用户终端,来达到为其显示PC端布局还是移动端布局。

UI设计师给的设计稿显示,官网PC端和移动端的是同一风格的两套不同布局。

由于便于后期维护,我并不想单独写两个项目来区分PC端和移动端。

所以我最终选择了在一个项目中将PC端和移动端的页面中的设计元素拆分封装成一个个组件,如果设计相似,那其实组件就可复用了。不同页面就组合需要的组件,像搭积木一样组成完整页面,再根据中间件给的终端标识值,选择显示页面的PC端组件还是移动端组件。

2、存储中间件中的终端标识值。

通过添加中间件,在中间件业务逻辑中判断终端是移动端还是pc端,将判断结果用自定义常量标识值表示。为了方便统一冠以标识值的同步和存储,我在项目中引入了vuex,通过koa+vuex结合,管理终端标识值。

在上述提到的中间件定义逻辑中,添加更新vuex状态的逻辑:

export default function(context) {
  const isMobile = (ua) => {
    return !!ua.match(/AppleWebKit.*Mobile.*/)
  }
  const reg = /\/m|\/m\//
  const hasMPath = reg.test(context.route.fullPath)
  const userAgent = context.req ? context.req.headers['user-agent'] : ''

  if (!isMobile(userAgent)) {
    context.store.commit('SET_DEVICE_TYPE', 1)
    // 若终端为pc但是路径中包含/m/或者/m的
    if (hasMPath) {
      context.store.commit('SET_DEVICE_TYPE', 2)
      // 重定向到pc
      const url = context.route.fullPath.substring(2)
      context.redirect(url)
    }
  } else {
    context.store.commit('SET_DEVICE_TYPE', 2)
    context.store.commit('SET_PAGE_TYPE', 2)
    // 若终端为mobile但是路径中不包含/m/或者/m的
    // 重定向到mobile
    if (!hasMPath) {
      context.store.commit('SET_DEVICE_TYPE', 1)
      context.redirect(`/m${context.route.fullPath}`)
    }
  }
}

3、移动端点击事件响应延迟的问题

在部分老机型上,存在网页点击事件响应延迟的问题,这是因为移动端有个两次连续“轻触”是“放大”的操作,在你第一次被“轻触”后,浏览器需要先等一段时间,若有“连续的第二次轻触”,则进行“放大”操作,否则就执行click事件,这就导致了移动端所谓的300ms click延迟。

一般会在项目中引入fastclick.js来解决这个问题,但是在该项目中按照官方文档的方式引入该插件,发现了页面刚开始加载出来时,有一小段时间是依旧存在击事件响应延迟问题的,一段时间后问题消失。

后来发现是按照官方方法在项目中引入插件,有一个加载插件的过程。这个过程中,fastclick.js还未生效,所以问题还是存在。

后来改成直接在head属性中直接引入,先行加载必要的css以及js文件,很好的解决了等待加载页面事件的布局混乱和点击事件响应延迟的问题。

四、总结

在面对未知的框架或语言时,我总是会对其有一份畏惧之情,不论是本文的中心Nuxt.js,还是正在吐血学习中的React和TypeScript,想学和怕学的情绪在脑子里像两个小人在干架~[doge头],但是当真正静下心来去研究每一份新的知识,并将其运用到项目中,那种充实感和喜悦感怕是只有正在学习和前进的你我才能体会到啦~

上一家公司的官网我就是使用了Nuxt+koa来实现的。

这里贴上地址,比较简单的几个静态页面:前公司官网