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开发服务端渲染的应用所需要的各种配置。
vue-rouer
,自动依据 pages 目录结构生成对应路由配置为了便于大家快速使用,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 文档。
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.js
的 head
字段中将公用的一些 head
信息放在里面;所以我们是不是可以把它单独抽离出来作为一个配置文件,并和每个页面的路由名字($route.name
)关联起来,这样按照理想格式的 seo.config.js
就诞生了。
seo的配置文件写好了,接下来我们应该怎么才能注入这个配置呢,很简单,只需要在 default.vue
的 head
字段下将不同页面的配置,将他们关联起来。这样就达到了通过每个页面的路由名字($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}` }
]
}
}
}
...
}
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
像普通的 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
接着在项目下新建两个文件:
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.json
中 scripts
添加 gen
和 patch
两条命令:
"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 patch
和 npm run generate
的合并命令,就是说会先后执行这两个,方便本机第一次使用。如果本机执行过 npm run patch
可直接 npm run generate
,生成相关静态页。