各位观众老爷们,晚上好!今天咱们不聊八卦,专攻Vue的SSR和SSG,保证各位听完之后,腰不酸了,腿不疼了,一口气能优化十个Vue项目!
开场白:为何SSR/SSG如此重要?
想象一下,你的Vue应用就像一个害羞的小姑娘,第一次见未来婆婆(搜索引擎爬虫)。如果她躲在房间里(客户端渲染),等精心打扮完才出来(JS执行完才渲染),婆婆可能等不及就走了,留下的印象分肯定不高。
而SSR/SSG就像是提前把小姑娘打扮好,直接端到婆婆面前,第一印象直接拉满!搜索引擎一看,哇,内容丰富,速度飞快,立马给个好评!
第一部分:SSR(服务端渲染) – 动态的魅力
SSR,Server-Side Rendering,就是把Vue组件在服务器上预先渲染成HTML,再发送给浏览器。浏览器拿到的是可以直接显示的HTML,而不是一堆需要JS解析的代码。
-
优点:
- SEO优化: 搜索引擎更容易抓取到完整的内容,提高排名。
- 首屏渲染加速: 用户更快看到页面内容,提升用户体验。
-
缺点:
- 服务器压力增大: 每次请求都需要服务器渲染,对服务器性能有要求。
- 开发复杂度增加: 需要考虑服务器环境和客户端环境的差异。
1.1 使用Nuxt.js简化SSR
Nuxt.js是一个基于Vue.js的框架,专门用于简化SSR应用的开发。它封装了很多底层细节,让我们能够更专注于业务逻辑。
-
安装Nuxt.js:
npx create-nuxt-app my-nuxt-app # 或者使用 yarn yarn create nuxt-app my-nuxt-app
按照提示选择配置,比如项目名称、UI框架、模块等等。
-
目录结构:
Nuxt.js有自己的一套目录结构,其中几个重要的目录包括:
目录 说明 pages/
存放Vue组件,自动生成路由。 layouts/
存放布局组件,定义页面结构。 store/
存放Vuex状态管理相关文件。 components/
存放可复用的Vue组件。 -
页面路由:
Nuxt.js会根据
pages/
目录下的文件自动生成路由。例如,pages/index.vue
对应根路由/
,pages/about.vue
对应路由/about
。// pages/index.vue <template> <div> <h1>欢迎来到我的 Nuxt.js 应用!</h1> <nuxt-link to="/about">关于我</nuxt-link> </div> </template> <script> export default { name: 'IndexPage' } </script>
-
异步数据获取:
asyncData
和fetch
在SSR中,通常需要在服务器端获取数据。Nuxt.js提供了
asyncData
和fetch
两个方法来实现这个功能。-
asyncData
: 在组件渲染之前调用,可以将数据返回给组件的data
。// pages/posts/_id.vue <template> <div> <h1>{{ post.title }}</h1> <p>{{ post.content }}</p> </div> </template> <script> export default { async asyncData({ params, $axios }) { // 使用 $axios 插件 const { data } = await $axios.$get(`https://api.example.com/posts/${params.id}`) return { post: data } } } </script>
-
fetch
: 类似于asyncData
,但不能直接修改组件的data
,通常用于更新Vuex store。// pages/index.vue <template> <div> <h1>最新文章</h1> <ul> <li v-for="post in posts" :key="post.id">{{ post.title }}</li> </ul> </div> </template> <script> import { mapState } from 'vuex' export default { computed: { ...mapState(['posts']) }, async fetch({ store, $axios }) { const { data } = await $axios.$get('https://api.example.com/posts') store.commit('setPosts', data) } } </script>
-
-
SEO优化:
nuxt.config.js
Nuxt.js提供了
nuxt.config.js
文件来配置应用的各种选项,包括SEO相关的元数据。// nuxt.config.js export default { head: { title: '我的Nuxt应用', meta: [ { charset: 'utf-8' }, { name: 'viewport', content: 'width=device-width, initial-scale=1' }, { hid: 'description', name: 'description', content: '我的Nuxt应用的描述' } ], link: [ { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' } ] } }
-
部署:
使用
npm run build
或yarn build
构建应用,然后使用npm run start
或yarn start
启动服务器。也可以部署到云服务器,比如阿里云、腾讯云等。
1.2 手动配置Vue SSR(难度提升!)
如果你想更深入地了解SSR的原理,可以尝试手动配置Vue SSR。这需要更多的配置和代码,但也能让你更灵活地控制整个流程。
-
环境准备:
- Node.js环境
- Vue CLI
-
安装依赖:
npm install vue vue-server-renderer express --save
-
创建服务器端入口:
server.js
// server.js const express = require('express') const Vue = require('vue') const { createRenderer } = require('vue-server-renderer') const app = express() const renderer = createRenderer() app.get('*', (req, res) => { const app = new Vue({ data: { url: req.url }, template: `<div>访问的 URL 是: {{ url }}</div>` }) renderer.renderToString(app, (err, html) => { if (err) { res.status(500).end('Internal Server Error') return } res.end(` <!DOCTYPE html> <html lang="en"> <head><title>Hello</title></head> <body>${html}</body> </html> `) }) }) app.listen(8080, () => { console.log('服务器已启动: http://localhost:8080') })
-
运行服务器:
node server.js
访问
http://localhost:8080
,就能看到服务端渲染的页面了。 -
更复杂的配置:
bundleRenderer
上面的例子只是一个简单的演示。在实际项目中,我们需要使用
bundleRenderer
来处理更复杂的组件和依赖关系。-
构建客户端和服务器端代码:
使用Vue CLI创建项目,并配置
vue.config.js
文件,分别构建客户端和服务器端的代码。// vue.config.js const VueSSRServerPlugin = require('vue-server-renderer/server-plugin') const VueSSRClientPlugin = require('vue-server-renderer/client-plugin') const nodeExternals = require('webpack-node-externals') const TARGET_NODE = process.env.WEBPACK_TARGET === 'node' const plugins = [ TARGET_NODE ? new VueSSRServerPlugin() : new VueSSRClientPlugin() ] module.exports = { configureWebpack: () => ({ entry: `./src/entry-${TARGET_NODE ? 'server' : 'client'}.js`, devtool: 'source-map', target: TARGET_NODE ? 'node' : 'web', node: TARGET_NODE ? undefined : false, output: { libraryTarget: TARGET_NODE ? 'commonjs2' : undefined }, externals: TARGET_NODE ? nodeExternals({ allowlist: [/.css$/] }) : undefined, optimization: { splitChunks: false }, plugins: plugins }) }
-
创建客户端入口:
src/entry-client.js
// src/entry-client.js import { createApp } from './main' const { app, router } = createApp() router.onReady(() => { app.$mount('#app') })
-
创建服务器端入口:
src/entry-server.js
// src/entry-server.js import { createApp } from './main' export default context => { return new Promise((resolve, reject) => { const { app, router } = createApp() router.push(context.url) router.onReady(() => { const matchedComponents = router.getMatchedComponents() if (!matchedComponents.length) { return reject({ code: 404 }) } resolve(app) }, reject) }) }
-
更新
server.js
:// server.js const express = require('express') const { createBundleRenderer } = require('vue-server-renderer') const fs = require('fs') const app = express() const template = fs.readFileSync('./public/index.template.html', 'utf-8') const serverBundle = require('./dist/vue-ssr-server-bundle.json') const clientManifest = require('./dist/vue-ssr-client-manifest.json') const renderer = createBundleRenderer(serverBundle, { runInNewContext: false, template, clientManifest }) app.use(express.static('./dist')) app.use(express.static('./public')) app.get('*', (req, res) => { const context = { url: req.url, title: 'Hello SSR' // 可以动态设置 title } renderer.renderToString(context, (err, html) => { if (err) { console.error(err) if (err.code === 404) { res.status(404).end('Page not found') } else { res.status(500).end('Internal Server Error') } } else { res.end(html) } }) }) app.listen(8080, () => { console.log('服务器已启动: http://localhost:8080') })
-
创建
public/index.template.html
:<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{{ title }}</title> <!-- 使用 context.title --> {{{ renderStyles() }}} </head> <body> <!--vue-ssr-outlet--> {{{ renderScripts() }}} </body> </html>
-
运行构建命令:
npm run build:ssr
-
运行服务器:
node server.js
这个过程比较复杂,需要仔细阅读Vue SSR的官方文档。
-
第二部分:SSG(静态站点生成) – 静态的极致
SSG,Static Site Generation,就是在构建时将Vue组件预先渲染成HTML文件,然后直接部署到服务器。每次请求都直接返回静态HTML文件,无需服务器动态渲染。
-
优点:
- 速度极快: 直接返回静态文件,无需服务器计算。
- 安全性高: 没有服务器端代码,减少安全风险。
- 易于部署: 可以部署到CDN等静态资源服务器。
-
缺点:
- 不适合动态内容: 每次更新都需要重新构建。
- 构建时间较长: 大型项目构建时间可能很长。
2.1 使用Gridsome构建SSG应用
Gridsome是一个基于Vue.js的静态站点生成器。它使用GraphQL来管理数据,并提供了丰富的插件来扩展功能。
-
安装Gridsome:
npm install --global @gridsome/cli gridsome create my-gridsome-site cd my-gridsome-site gridsome develop
-
目录结构:
Gridsome的目录结构也比较规范:
目录 说明 src/pages/
存放Vue组件,自动生成路由。 src/layouts/
存放布局组件,定义页面结构。 src/components/
存放可复用的Vue组件。 src/templates/
存放模板组件,用于动态生成页面(例如博客文章)。 gridsome.config.js
存放Gridsome的配置信息。 gridsome.server.js
存放服务器端的配置信息。 -
GraphQL数据源:
Gridsome使用GraphQL来查询数据。你需要配置数据源,例如本地文件、API接口等。
-
本地文件:
// gridsome.config.js module.exports = { plugins: [ { use: '@gridsome/source-filesystem', options: { path: 'content/**/*.md', // Markdown文件的路径 typeName: 'Post', // GraphQL类型名称 remark: { // Plugins options for remark-prismjs plugins: [ '@gridsome/remark-prismjs' ] } } } ] }
-
API接口:
// gridsome.config.js module.exports = { plugins: [ { use: '@gridsome/source-graphql', options: { url: 'https://api.example.com/graphql', typeName: 'MyAPI', fieldName: 'api' } } ] }
-
-
页面查询:
在Vue组件中使用GraphQL查询数据。
// src/pages/Index.vue <template> <div> <h1>最新文章</h1> <ul> <li v-for="post in $page.posts.edges" :key="post.node.id"> <g-link :to="post.node.path">{{ post.node.title }}</g-link> </li> </ul> </div> </template> <page-query> query { posts: allPost { edges { node { id title path } } } } </page-query>
-
模板组件:
使用模板组件动态生成页面。例如,为每篇博客文章生成一个页面。
// src/templates/Post.vue <template> <div> <h1>{{ $page.post.title }}</h1> <div v-html="$page.post.content"></div> </div> </template> <page-query> query Post ($path: String!) { post: post (path: $path) { title content } } </page-query>
-
构建和部署:
使用
gridsome build
构建应用,然后将dist/
目录下的文件部署到服务器。
2.2 使用VuePress构建文档站点
VuePress是一个由Vue驱动的静态网站生成器,专门用于构建文档站点。它使用Markdown编写内容,并提供了丰富的插件来扩展功能。
-
安装VuePress:
npm install -g vuepress
-
创建项目:
mkdir my-vuepress-site cd my-vuepress-site npm init -y mkdir .vuepress echo '# Hello VuePress' > README.md
-
配置
package.json
:// package.json { "scripts": { "docs:dev": "vuepress dev .", "docs:build": "vuepress build ." } }
-
编写文档:
使用Markdown编写文档,存放在项目根目录下。
# Hello VuePress This is my first VuePress site.
-
运行开发服务器:
npm run docs:dev
-
构建和部署:
使用
npm run docs:build
构建应用,然后将.vuepress/dist
目录下的文件部署到服务器。
第三部分:SSR vs SSG – 如何选择?
SSR和SSG各有优缺点,如何选择取决于你的项目需求。
特性 | SSR | SSG |
---|---|---|
内容更新频率 | 适合频繁更新的内容,例如新闻、电商 | 适合不经常更新的内容,例如博客、文档 |
数据来源 | 动态数据,例如API接口 | 静态数据,例如Markdown文件 |
性能 | 首屏渲染速度快,但服务器压力大 | 速度极快,无需服务器计算 |
SEO优化 | 搜索引擎容易抓取完整内容 | 搜索引擎容易抓取完整内容 |
开发复杂度 | 较高 | 较低 |
部署复杂度 | 较高,需要服务器环境 | 较低,可以部署到CDN等静态资源服务器 |
总结:
- 如果你的应用需要频繁更新内容,并且对SEO有较高要求,可以选择SSR。Nuxt.js可以大大简化SSR的开发过程。
- 如果你的应用内容不经常更新,并且对性能有极致要求,可以选择SSG。Gridsome和VuePress都是不错的选择。
最后的忠告:
不要盲目追求SSR或SSG,要根据实际情况选择最适合你的方案。记住,没有银弹,只有最适合你的工具!希望大家都能成为SSR/SSG大师,让你的Vue应用飞起来!