Table of Contents
之前有写过用 puppeteer 作为 ssr 解决方案的文章。那个对于旧项目适配很合适,新项目还是一开始就采用 ssr 框架比较好。所以,我把“品词轩”用 nuxt 重新实现了一下。
项目介绍
用 nuxt 实现的诗词网站。功能简单,主要就是为了了解一下 nuxt。(vapper 好像比 nuxt 好用些。)
项目地址:https://github.com/tonyzhou1890/poem-v4-nuxt
网站地址:https://poem-nuxt.tony93.top
实现解析
技术栈
nuxt
Nuxt 简介
nuxt 是基于 vue 的 ssr 框架。方便了 ssr 开发,但也有诸多约束。nuxt 的路由可以是约定式,也可以自定义。约定式路由模式下,pages 下面就是各个页面,layouts 下面是布局。其在 vue 原有的生命周期上添加了 asyncData 和 fetch。asyncData 在页面创建前调用,是阻塞的。所以可以在 asyncData 里请求 api。fetch 是在 created 之后调用,主要用来填充 store 数据,非阻塞。
篇幅有限,就不完整介绍 nuxt 了。反正 nuxt 文档挺长的。
项目结构
/. │ .editorconfig │ .eslintrc.js │ .gitignore │ .prettierrc │ ecosystem.config.js // pm2 配置文件 │ jsconfig.json │ nuxt.config.js // nuxt 配置文件 │ package.json │ README.md │ tsconfig.json │ yarn.lock │ ├─assets // 需要打包引用的资源文件 │ │ global.ts // 这个我随便放的 │ │ README.md │ │ │ ├─icons │ │ └─svg │ │ xxx.svg │ │ │ ├─images │ │ background.png │ │ │ └─style │ global.less │ variables.less │ ├─components │ │ Header.vue │ │ README.md │ │ │ ├─Pagination │ │ index.vue │ │ │ ├─PoemItemCom │ │ index.vue │ │ │ ├─PoemListCom │ │ index.vue │ │ │ ├─SvgIcon │ │ index.vue │ │ │ └─Tabs │ index.vue │ ├─layouts │ default.vue │ README.md │ ├─middleware │ README.md │ ├─pages │ article.vue │ author.vue │ authorList.vue │ collection.vue │ index.vue │ poemList.vue │ README.md │ search.vue │ subject.vue │ subjectPoem.vue │ ├─plugins │ axios.js │ icon.js │ README.md │ svgo.yml │ ├─services │ api.ts │ ├─static │ favicon.ico │ README.md │ robots.txt // 练习项目,不需要搜索引擎抓取 │ └─store index.js README.md
Axios 的使用
在有 this 的生命周期里,使用 this.$axios,在 asyncData 里使用解构参数里的 $axios。
// pages/index.vue export default Vue.extend({ name: 'Home', asyncData({ $axios }) { return Promise.all([ $axios.get(urls.home, { params: { home: true, }, }), $axios.get(urls.poemListRandom), ]).then((res) => { return { authors: res[0].data.authors, poems: res[1].data, } }) }, data() { return { poems: [], authors: [], } }, }
但在使用之前需要配置一下。axios 是作为插件使用的,所以需要在 plugins 文件夹下编写 axios.js 文件。
// plugins/axios.js export default function ({ app: { $axios } }) { // 数据访问前缀 $axios.defaults.baseURL = 'https://xxx/xxx' $axios.interceptors.request.use( (config) => config, (error) => { Promise.reject(error) } ) $axios.interceptors.response.use( (response) => { if (response.data.code !== 0) { return Promise.reject(response.data) } else { return response.data } }, (error) => { return Promise.reject(error) } ) }
// nuxt.config.js export default { // Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins plugins: ['@/plugins/axios'], // Modules: https://go.nuxtjs.dev/config-modules modules: [ // https://go.nuxtjs.dev/axios '@nuxtjs/axios', ], // Axios module configuration: https://go.nuxtjs.dev/config-axios axios: {}, }
Svg-Icon 的封装
为了方便地使用 svg,需要封装一个 svg-icon 组件。
// components/SvgIcon/index.vue <template> <svg :class="svgClass" aria-hidden="true"> <use :xlink:href="iconName" /> </svg> </template> <script> export default { name: 'SvgIcon', props: { iconClass: { type: String, required: true, }, className: { type: String, default: '', }, }, computed: { iconName() { return `#icon-${this.iconClass}` }, svgClass() { if (this.className) { return 'svg-icon ' + this.className } else { return 'svg-icon' } }, }, } </script> <style scoped> .svg-icon { width: 1em; height: 1em; vertical-align: -0.15em; fill: currentColor; overflow: hidden; } </style>
// plugins/icon.js import Vue from 'vue' import SvgIcon from '@/components/SvgIcon' // svg组件 // register globally Vue.component('SvgIcon', SvgIcon) const req = require.context('@/assets/icons/svg', false, /\.svg$/) const requireAll = (requireContext) => requireContext.keys().map(requireContext) requireAll(req)
// nuxt.config.js import path from 'path' export default { // Global CSS: https://go.nuxtjs.dev/config-css css: ['@/assets/style/variables.less', '@/assets/style/global.less'], // Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins plugins: ['@/plugins/icon', '@/plugins/axios'], // Build Configuration: https://go.nuxtjs.dev/config-build build: { extend(config, _) { const svgRule = config.module.rules.find((rule) => rule.test.test('.svg')) svgRule.exclude = [path.resolve(__dirname, 'assets/icons/svg')] // Includes /assets/icons/svg for svg-sprite-loader config.module.rules.push({ test: /\.svg$/, include: [path.resolve(__dirname, 'assets/icons/svg')], loader: 'svg-sprite-loader', options: { symbolId: 'icon-[name]', }, }) }, }, }
less 的使用和全局样式
安装完 less 和 less-loader 就可以使用 less 了。至于全局样式,创建好文件后 nuxt.config.js 里配置一下就可以了。
// nuxt.config.js export default { // Global CSS: https://go.nuxtjs.dev/config-css css: ['@/assets/style/variables.less', '@/assets/style/global.less'], }
keep-alive 的使用
因为首页的诗词是随机的,所以首页需要使用 keep-alive 属性,避免每次回到首页都是不一样的内容。
// layouts/default.vue <Nuxt keep-alive :keep-alive-props="{ include: ['Home'] }" />
include 数组里的组件名称就是组件的 name 属性。如果没写 name,就是 nuxt 自动生成的组件名称。
head 的配置
既然是针对 seo 的改版,metaInfo 自然是需要的。在 nuxt 里,只需要配置页面的 head 属性就可以。
// pages/author.vue export default Vue.extend({ name: 'Author', head() { return { title: `作者--${this.$route.query.author}`, meta: [ { hid: 'description', name: 'description', content: `${this.$route.query.author}的诗词`, }, ], } }, })
除了各个页面单独配置,最好也设置一个全局的。
// nuxt.config.js export default { // Global page headers: https://go.nuxtjs.dev/config-head head: { title: '品词轩', htmlAttrs: { lang: 'zh', }, meta: [ { charset: 'utf-8' }, { name: 'viewport', content: 'width=device-width, initial-scale=1' }, { hid: 'description', name: 'description', content: '' }, ], link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }], }, }
路由参数变化
nuxt 并没有默认根据参数变化重新渲染页面。路由相同的页面,不管参数怎么变,nuxt 都视为同一个页面。这在实际应用中肯定是不合适的。所以,需要打开参数监听——watchQuery。
// nuxt.config.js export default { // https://www.nuxtjs.cn/api/pages-watchquery watchQuery: true, }
然后,在页面里设置需要监听的参数。
// pages/search.vue export default Vue.extend({ watchQuery: ['ap', 'tp', 'cp', 'type', 'keyword'], })
部署
本地开发完后提交 git,然后服务器上拉取项目,安装依赖。然后 yarn build 打包。
我是 nginx + node 的形式。nginx 根据域名代理到 nuxt 服务端口。
按照 nuxt 文档的指引,建立的 ecosystem.config.js 文件,但 pm2 start 启动后怎么也无法访问。用 netstat -tunlp 命令也看不到 3000 端口。
// ecosystem.config.js module.exports = { apps: [ { name: 'NuxtAppName', exec_mode: 'cluster', instances: 'max', // Or a number of instances // script: './node_modules/nuxt/bin/nuxt.js', script: 'yarn', args: 'start' } ] }
后来我把 script 部分换成 yarn,package.json 里的命令加上端口就好了。
// package.json "scripts": { "start": "nuxt-ts start --port 8100", },
本来以为部署会很顺利,没想到折腾了好久~~~~~~
性能
相比于 puppeteer 方案,nuxt 性能要好很多。在不使用缓存的情况下,浏览器端测试 qps 可以达到 30。配置为 1C2G1M 的 ecs。
总结
相比于传统服务端渲染,nuxt 和 puppeteer 都是首次加载服务端渲染,后续操作客户端渲染。所以虽然性能稍差,但可以满足需求。特别是 nuxt 这一类的方案,其性能优于 puppeteer,结合 redis 一类的内存缓存技术应该可以达到较高的 qps。但 puppeteer 泛用性强,不限定技术栈,并且还可以用于爬虫、海报渲染等场景。所以具体用什么方案还是得看场景。