使用nuxt

Nuxt.js简单介绍

2016 年 10 月 25 日,zeit.co 背后的团队对外发布了 Next.js,一个 React 的服务端渲染应用框架。几小时后,与 Next.js 异曲同工,一个基于 Vue.js 的服务端渲染应用框架应运而生,我们称之为:Nuxt.js。

Nuxt.js 是一个基于 Vue.js 的通用应用框架。通过对客户端/服务端基础架构的抽象组织,Nuxt.js 主要关注的是应用的 UI渲染。Nuxt.js 预设了利用Vue.js开发服务端渲染的应用所需要的各种配置。

为什么使用Nuxt.js?

  • SSR(服务端渲染)的页面初始加载时间显然优于单页首屏渲染
  • 可以方便的对 SEO 进行管理
  • 无需配置页面路由,内置 vue-rouer,自动依据 pages 目录结构生成对应路由配置
  • 便捷的 HTML 头部标签管理(vue-meta)
  • 项目结构自动代码分层
  • 支持静态化(本文将着重以此展开介绍)

项目创建

为了便于大家快速使用,Nuxt.js 提供了很多模板

starter-template: 基础Nuxt.js模板

typescript-template: 基于Typescript的Nuxt.js模板

express-template: Nuxt.js + Express

koa-template: Nuxt.js + Koa

adonuxt-template: Nuxt.js + AdonisJS

electron-template: Nuxt.js + Electron

等等,更多的可以在这里看到 nuxt-community

这里我们使用 starter-template,可以使用 vue-cli 安装:

$ npm install -g vue-cli
$ vue init nuxt-community/starter-template nuxt-demo
$ cd nuxt-demo
$ npm install

生成项目结构如下:

nuxt-demo/
├── assets/
├── components/
│   └── AppLogo.vue
├── layouts/
│   └── default.vue
├── middleware/
├── pages/
│   └── index.vue
├── plugins/
│   └── README.md
├── static/
│   └── favicon.ico
├── store/
├── nuxt.config.js
├── package.json
└── README.md

可以看出来项目结构还是比较清晰的,接着我们根据业务需求在 vue-cli 脚手架生成的项目基础上扩展和修改出来的目录结构如下(已隐去部分文件):

nuxt-demo/
├── api/                                  //- 接口
│   └── index.js
├── assets/                               //- 需要编译的静态资源,如 scss、less、stylus
│   ├── images/                           //- 图片
│   └── styles/                           //- 样式
├── build/                                //- 自定义的一些编译配置
├── components/                           //- 公用的组件
│   ├── dm-toast.vue                      //- 全局组件`dm-toast`
│   └── ...
├── data/                                 //- 静态数据
├── layouts/                              //- 布局
│   ├── components/
│   │   ├── dm-footer.vue                 //- 公用header
│   │   └── dm-header.vue                 //- 公用footer
│   └── default.vue                       //- 默认布局
├── middleware/                           //- 中间件
├── mixins/                               //- Vue mixins
├── pages/                                //- 页面
│   ├── index.vue                         //- 主页
│   └── ...
├── plugins/                              //- vue插件
│   └── dm-tracker.js/                    //- 挂载utils/tracker.js
├── static/                               //- 无需编译处理的静态资源
│   └── images/                           //- 这里存放了一些通过数据循环出来的图片
├── store/                                //- vuex
│   └── index.js
├── utils/                                //- 工具集
│   ├── index.js
│   ├── http.js                           //- axios
│   ├── tracker.js                        //- PV统计
│   └── tracker-uitl.js
├── vendor/                               //- 第三方的库和插件
│   └── index.js
├── nuxt.config.js                        //- Nuxt.js配置文件
├── seo.config.js                         //- SEO相关配置文件
├── package-lock.json                     //- npm的版本锁
├── package.json
└── README.md

项目配置

Nuxt.js 默认的配置涵盖了大部分使用情形,可通过 nuxt.config.js 来覆盖默认的配置,下面相关配置根据实际项目驱动讲解,未涉及到的配置项可查阅 Nuxt.js 文档。

nuxt.config.js 总览

module.exports = {
  //- Document Common 
  head: {
    meta: [
      title: '我是一个title',
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { name: 'renderer', content: 'webkit' },
      { name: 'applicable-device', content: 'pc' },
      { 'http-equiv': 'X-UA-Compatible', content: 'IE=edge,chrome=1' },
      { 'http-equiv': 'Cache-Control', content: 'no-transform' },
      { 'http-equiv': 'Cache-Control', content: 'no-siteapp' }
    ],

    link: [
      { rel: 'icon', type: 'image/x-icon', href: '你的icon地址' }
    ],

    //- 这里可以写一些每个页面需要额外引入的一些js代码,比如:百度统计
    //- `alert(1)` 仅为代码示例
    script: [{
      type: 'text/javascript',
      innerHTML: `alert(1)`
    }],

    //- __dangerouslyDisableSanitizers 设置

根据官方文档的描述,我们了解到页面里的 head 配置优先级高于 nuxt.config.js 中的 head,就是说同等的配置会覆盖 nuxt.config.js 中的 head 相关位置的配置。但是这个等同覆盖的条件是你为它设置了同一个 hid,它会以此作为等同替换的条件去查找相关 dom 元素进行替换。

因为项目生成的是多页面静态站点,很多页面需要配置的 meta 多少有些不一样,深入到每一个页面去写单独的配置信息不免繁琐了许多,所以我们可以在 nuxt.config.jshead 字段中将公用的一些 head 信息放在里面;所以我们是不是可以把它单独抽离出来作为一个配置文件,并和每个页面的路由名字($route.name)关联起来,这样按照理想格式的 seo.config.js 就诞生了。

seo的配置文件写好了,接下来我们应该怎么才能注入这个配置呢,很简单,只需要在 default.vuehead 字段下将不同页面的配置,将他们关联起来。这样就达到了通过每个页面的路由名字($route.name)来映射和渲染对应的 meta。

layout/default.vue




seo.config.js

//- 根据路由 `$route.name` 映射配置
//- path - 页面的访问路径
//- head - Document ,页面的元信息
module.exports = {
  'index': {
    head: {
      title: '我是首页',
      meta: [
        { hid: 'keywords', name: 'keywords', content: '' },
        { hid: 'description', name: 'description', content: '' },
        { name: 'mobile-agent', content: 'format=wml; url=//该页面对应的移动端网址/' },
        { name: 'mobile-agent', content: 'format=xhtml; url=//该页面对应的移动端网址/' },
        { name: 'mobile-agent', content: 'format=html5; url=//该页面对应的移动端网址/' }
      ],
      link: [
        { rel: 'alternate', type: 'applicationnd.wap.xhtml+xml', media: 'handheld', href: '//该页面对应的移动端网址/' }
      ]
    }
  },
  //- head 可以是一个 `function`
  //- `function` 中会接收过来一个参数 `route`,表示当前页面的路由信息
  //- 可以根据这个做一些动态的配置信息
  //- 比如动态路由生成的页面中的 meta 信息,可能会根据页面内容来决定
  'name': {
    head(route) {
      const titles = {
        '1': 'ofo小黄车',
        '2': '盒马生鲜',
        '3': '顺丰速运'
      }

      return {
        title: `${titles[route.params.id]}_客户案例-斗米网`,
        meta: [
          { name: 'mobile-agent', content: `format=wml; url=//该页面对应的移动端网址${route.fullPath}` },
          { name: 'mobile-agent', content: `format=xhtml; url=//该页面对应的移动端网址${route.fullPath}` },
          { name: 'mobile-agent', content: `format=html5; url=//该页面对应的移动端网址${route.fullPath}` }
        ],
        link: [
          { rel: 'alternate', type: 'applicationnd.wap.xhtml+xml', media: 'handheld', href: `//该页面对应的移动端网址${route.fullPath}` }
        ]
      }
    }
  }
  ...
}

页面布局(layout)

Nuxt.js 中,抽象出来一个新的概念:layout,这样将页面划分为三层:1. layout、2. page、3. component,很方便的在多种布局方案中切换。

+------------------------------+
|            layout            |
|                              |
|   +----------------------+   |
|   |         page         |   |
|   |                      |   |
|   |   +--------------+   |   |
|   |   |  component   |   |   |
|   |   +--------------+   |   |
|   |                      |   |
|   |   +--------------+   |   |
|   |   |  component   |   |   |
|   |   +--------------+   |   |
|   |                      |   |
|   |   +--------------+   |   |
|   |   |  component   |   |   |
|   |   +--------------+   |   |
|   +----------------------+   |
|                              |
+------------------------------+

在页面 pages/*.vue 文件中可以指定一种布局,不指定的时候会使用默认布局 default

比如以下目录结构:

├── layouts/                              //- 布局
│   ├── components/
│   │   ├── dm-footer.vue                 //- 公用header
│   │   └── dm-header.vue                 //- 公用footer
│   ├── box.vue
│   └── default.vue                       //- 默认布局
├── pages/                                //- 页面
│   └── index.vue

使用 box.vue 的布局, 对应页面部分,类似 Vue 的 slot

layouts/box.vue




pages/index.vue


状态管理(vuex)

像普通的 Vue 应用一样,在 Nuxt.js 中也可以使用 vuex,而且无需额外 npm install vuex --save 和配置,只要直接在项目根目录创建 store 文件夹,Nuxt.js 会自动去寻找下面的 .js 文件,并自动进行状态树的模块划分。

Nuxt.js 支持两种使用 store 的方式:

普通方式:返回一个 Vuex.Store 实例,感觉很眼熟有木有

store/index.js

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const store = () => new Vuex.Store({
  state: {
    counter: 0
  },

  mutations: {
    increment (state) {
      state.counter++
    }
  }
})

export default store

模块方式:store 目录下的每个 .js 文件会被转换成为状态树指定命名的子模块,index.js 会被作为根模块

store/index.js

export const state = () => ({
  counter: 0
})

export const mutations = {
  increment (state) {
    state.counter++
  }
}

store/todos.js

export const state = () => ({
  list: []
})

export const mutations = {
  toggle (state, todo) {
    todo.done = !todo.done
  }
}

最终渲染出来的状态树:

new Vuex.Store({
  state: { counter: 0 },
  mutations: {
    increment (state) {
      state.counter++
    }
  },
  modules: {
    todos: {
      state: {
        list: []
      },
      mutations: {
        toggle (state, { todo }) {
          todo.done = !todo.done
        }
      }
    }
  }
})

一键静态化

npm run generate

然后会在项目根目录生成 dist 目录,静态化后的资源都在其内(html、js、css…)

当然,在静态化的时候还是遇到了一些问题:我们有一个专门放置静态资源的 CDN 分发服务器,所以每次版本更新的时候需要版本智能更新(不然用户访问到的可能就是旧的资源),即对应的模块内容发生改变时才去更新这个版本号,虽然可以通过 js/[name].[chunkhash:7].js这样的配置实现,但是 CDN 访问到的静态资源是通过 git 控制上线的,所以这样生成的静态资源,文件名每次可能不一样,这样就会越来越多,git 需要定时清理,比较麻烦。于是才有了这样一个两全的方案:既控制了版本更新,也让文件名不产生变动。

就是如下配置:

module.exports = {
  ...
  build: {
    ...
    filenames: {
      ...
      chunk: 'js/[name].js?v=[chunkhash:7]'
    }
  }
}

想法很丰满,现实很骨感,在 nuxt.config.js 中使用这样的配置静态化编译的时候会报错 Cannot find module 'pages_index.js?v=6f7b904...,(⊙o⊙)…经过一系列的源码追踪发现是vue-server-renderer 这个模块的 BUG。

虽然向官方提了相关 issue ,但是因不可抗拒的因素暂时无法修复。

vue-server-renderer/server-plugin.js 78行 asset.name.match(/.js$/) 这个判断里的正则明显把我们设定的格式 js/[name].js?v=[chunkhash:7] 过滤掉了,不会走这个判断,所以我们只要想办法把这个判断条件改一下,让它走这里。发现他同文件上面有个 isJS 函数,这样就省事多了,我们可以直接调用。

所以我们如果改源码的话只需要把这里 asset.name.match(/.js$/) 替换为 isJS(asset.name) 就行了。但是改源码总归不好,因为并不能保证团队其他成员的机器上模块库一致。投机取巧,既然直接修改不好,那就间接修改呗。

开始动手,先安装一个 npm 的模块:

npm install shelljs -D # OR yarn add shelljs -D

接着在项目下新建两个文件:

  • build/nuxt-generate.js:用来执行静态化的一些命令
  • build/vue-server-renderer.patch.js:给 vue-server-renderer 模块打补丁

build/nuxt-generate.js

const shell = require('shelljs')
const { resolve } = require('path')
const nuxt = resolve(__dirname, '../node_modules/.bin/nuxt')
const logProvider = require('consola').withScope('nuxt:generate')

shell.exec(`npm run patch`, (code, stdout, stderr) => {
  if (code !== 0) {
    logProvider.error(stderr)
  }

  //- 上面的命令执行成功之后在执行下面的命令
  shell.exec(`${nuxt} generate`)
})

build/vue-server-renderer.patch.js

const { resolve } = require('path')
const fs = require('fs')
const SSRJSPath = resolve(__dirname, '../node_modules/vue-server-renderer/server-plugin.js')
const consola = require('consola')
const logProvider = consola.withScope('vue:patch')

module.exports = VueSSRPatch()

/**
 * 对 `vue-server-renderer/server-plugin.js` 源码内容进行替换
 * asset.name.match(/\.js$/)
 * =>
 * isJS(asset.name)
 */
function VueSSRPatch() {
  //- 检测该模块是否存在
  if (fs.existsSync(SSRJSPath)) {
    let regexp = /asset\.name\.match\(\/\\\.js\$\/\)/
    let SSRJSContent = fs.readFileSync(SSRJSPath, 'utf8')

    //- 检测是否存在需要替换的内容(通常是指该项目在本机第一次运行)
    if (regexp.test(SSRJSContent)) {
      logProvider.start(`发现vue-server-renderer模块,开始执行修补操作!`)

      SSRJSContent = SSRJSContent.replace(regexp, 'isJS(asset.name)')
      fs.writeFileSync(SSRJSPath, SSRJSContent, 'utf8')

      logProvider.ready(`修补完毕!`)
      return true
    }

    logProvider.warn(`该模块已修补过,无需再次修补,可直接运行\`npm run dev\` 或 \`npm run gen\``)
    return false
  }

  logProvider.warn(`未发现该模块,跳出本次修复!`)
  return false
}

最后在 package.jsonscripts 添加 genpatch 两条命令:

"scripts": {
  "dev": "nuxt",
  "generate": "nuxt generate",
  "patch": "node build/vue-server-renderer.patch",
  "gen": "node build/nuxt-generate"
}

patch:本机第一次运行或者更新相关模块(vue-server-renderer)时需要执行一次。

gen
npm run patchnpm run generate 的合并命令,就是说会先后执行这两个,方便本机第一次使用。如果本机执行过 npm run patch可直接 npm run generate,生成相关静态页。

你可能感兴趣的:(vue)