[译]Vue.js 教程:构建并提前渲染一个 SEO 友好的网站
原文地址: Vue.js Tutorial: A Prerendered, SEO-Friendly Example [Live Demo] - Snipcart原文作者:Maxime Laboissonniere
“受不了了,我们内部的报告面板太难用了”,我们的生产经理生气的说。他尝试下载数据的 app 简直是个灾难。
“Max ,我们需要让它更好用,你能解决吗”
“说实话,我宁愿做一个全新的 app”,我笑着说。
“好,按你说的做。那就交给你了,朋友”
我保持微笑的搓了搓手。终于有机会使用每个人都在讨论的 JS 框架:Vue.js 了。
------------------------------- 我是正文开始的分割线 ------------------------------
我刚写完上述的 app 而且我爱死它了。
由于最近经历的启发,我花了些时间为社区制作了一个 Vue.js 的教程。现在在这里我将主要讲两方面的主题:
- 如何用 Vue.js 构建一个简洁的网页 app
- 怎样使用 prerender-spa-plugin 做到 SEO 和提前渲染
具体来说,我将带大家创建一个 SEO 友好的简单产品页面。会有现场的 demo 和代码仓库给到大家。
我是通过 our latest headless CMS post 简单的接触到 Vue 的,但这次会是更深入的了解,所以我很兴奋。
我先简单介绍下这个渐进式的框架,因为可能有一些朋友还不了解。
Vue.js 到底是什么?
Vue 是一套帮你构建网页界面的轻量的渐进式 JavaScript 框架。
千万别被定义中的 “JS 框架” 欺骗了。Vue 和跟它对应的流行框架-- React.js 和 Angular.js 是非常不同的。对初学者来说,它不是 Google 和 Facebook 那样的商业巨头所有的开源产品。
尤雨溪在 2014 年第一次发布 Vue,目的是创造一个自底向上增量式的现代 JS 库。这是 Vue 最强大的特性之一:构建可插入的组件并可以在项目不重构的情况下添加。任何开发者都可以在项目中尝试 Vue 而不会对现有代码产生危害或负担。
模式和专业术语先抛开,我认为 Vue 的前提是:
- 在一开始你不能知道 app 的全部状态架构
- 你的数据肯定会在运行时改变
这些限制塑造了库本身:渐进式、基于组件和响应式。这种颗粒式的架构组件让你很容易在保持复用性的同时分离逻辑概念。从顶层来看,它通过原生方法与视图绑定数据,所以可以在必要的时候魔法般的更新(通过观察者)。尽管相同的定义会在很多响应式的前端框架里出现,我发现 Vue 更加优雅的实现了,而且对于我大多数的需求,它是更好的做法。
Vue 的学习曲线相比 React 更平稳,React 需要 JSX 模板知识。甚至有人说 Vue 是去掉了棘手部分的 React 。
最后,Vue 提供的 performance & insightful 开发工具给予了很棒的开发体验。 难怪选择 Vue 的开发者像火箭式地上升。
从开源项目( Laravel 和 PageKit )到企业( Gitlab 和 Codeship )(别提阿里巴巴和百度),非常多的组织在使用 Vue 。
现在,是时候看看我们要如何使用它。
我们的 Vue.js 例子:一个快速、SEO 友好的电子贸易 app
这个章节我将展示如何使用 Vue 2.0 和 Snipcart 构建一个简单的电子贸易 app ,开发人员编写购物平台的 HTML/JS 。我们同样可以看到如何让爬虫可以爬到产品页面。
预备知识
- 简单了解 Vue.js — 开始
- 基本理解 vuex 和 vue-router
- 一个 Snipcart 账户(测试模式永远免费)
如果你想深入了解 Vue 2.0 的一切, 请点击这里查看系列教程。
- 搭建开发环境
首先使用 vue-cli 生成一个基本的 Vue app 。在终端输入:
npm install -g vue-cli
vue init webpack-simple vue-snipcart
上面的命令将会新建一个名为 vue-snipcart 的新目录,里面包含使用 vue-loader 的基本配置。它也让我们可以编写单文件组件( template/js/css 在同一文件中 )。
我们希望这个示例更加像真实项目,所以我们添加两个在大型 Vue 单页应用中广泛使用的模块:vuex 和 vue-router 。
- vuex 是类似 flux 的状态管理模块—非常轻量但强大。它深受 Redux 的影响, 你可以前往这里学习。
- vue-router 让你定义路由来动态的导航到对应的组件。
进入 vue-snipcart 目录然后运行如下命令安装它们:
cd vue-snipcart
npm install --save vue-router vuex
另一个要安装的就是 prerender-spa-plugin,它可以让我们提前渲染爬虫需要的路由。
npm install --save prerender-spa-plugin
我们最后装以下四个包就可以了:
- pug — 用作模板,相比于 HTML 我更喜欢它。
- vuex-router-sync — 将一些路由信息直接注入 vuex 的 state 里。
- copy-webpack-plugin — 让我们可以简单的将 static 目录复制到 dist 目录下。
- babel-polyfill — 使 Vue 运行在 PhantomJS 内部(prerender-spa-plugin 要用到)。
运行下面命令:
npm install --save pug vuex-router-sync copy-webpack-plugin babel-polyfill
2. 配置架构
安装依赖:完成。接下来是配置使我们可以处理存储(store)的数据。
我们先从 vuex store 开始,用它来存储/获取产品信息。
在这个例子中,我们使用静态数据,当然如果我们从服务器获取(fetch)一样也是可以工作的。
注意:在 snipcart 里,我们用 一个 JS 基本片段注入 cart ,使用 简单 HTML 属性标记定义 products 。
2.1 构造 store
在 src 目录下新建 store 文件夹并在文件夹里新建以下三个文件:
- state.js 用来定义静态产品
- getter.js 用来定义一个通过 ID 来检索产品的 get 函数
- index.js 用来连接上面两个
//state.js
export const state = {
products: [
{
id: 1,
name: 'The Square Pair',
price: 100.00,
description: 'Bold & solid.',
image: 'https://snipcart.com/media/10171/glasses1.jpeg'
},
{
id: 2,
name: 'The Hip Pair',
price: 110.00,
description: 'Stylish & fancy.',
image: 'https://snipcart.com/media/10172/glasses2.jpeg'
},
{
id: 3,
name: 'The Science Pair',
price: 30,
description: 'Discreet & lightweight.',
image: 'https://snipcart.com/media/10173/glasses3.jpeg'
}
]
}
//getters.js
export const getters = {
getProductById: (state, getters) => (id) => {
return state.products.find(product => product.id == id)
}
}
//index.js
import Vue from 'vue'
import Vuex from 'vuex'
import { state } from './state.js'
import { getters } from './getters.js'
Vue.use(Vuex)
export default new Vuex.Store({
state,
getters
})
2.2 定义路由
我们保持基本的 store:首页列表展示产品 + 每个产品的详情页。所以我们需要注册两个路由。
import VueRouter from 'vue-router'
import Vue from 'vue'
import ProductDetails from './../components/productdetails.vue'
import Home from './../components/home.vue'
Vue.use(VueRouter)
export default new VueRouter({
mode: 'history',
routes: [
{ path: '/products/:id', component: ProductDetails },
{ path: '/', component: Home },
]
})
我们还没有创建上面的组件,别担心,我们就快了。
需要注意的是我们在 VueRouter 中使用的是 history 模式。这非常重要,不然我们的 prerender-spa-plugin 插件将不起作用。在这种模式下,router 将使用 history API 而不是 hash 来导航。
2.3 把一切连接在一起
现在我们已经有了 store 和 router ,我们需要把它们注册到 app,打开 src/main.js ,并将内容修改为:
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import { sync } from 'vuex-router-sync'
import store from './store'
sync(store, router)
new Vue({
store,
router,
render: h => h(App)
}).$mount('#app')
很简单,不是吗?提前说一下,vuex-router-sync 里的 sync 方法会注入在我们 store state 中一些当前路由的信息,我们一会将会用到。
3. 制作 Vue 组件
有了数据感觉棒极了,但把它显示出来就更棒了。我们将用三个组件来达到显示的目的:
- Home 用来显示产品列表
- Product 用来展示 Home 组件里的每个产品
- ProductDetails 用来显示产品详情
上述文件都新建在 src/components 目录下。
//Home.vue
<template lang="pug">
div(class="products")
div(v-for="product in products", class="product")
product(:product="product")
</template>
<script>
import Product from './../components/Product.vue'
export default {
name: 'home',
components: { Product },
computed: {
products(){
return this.$store.state.products
}
}
}
</script>
在上面的文件里,我们用 store.state 来获取产品并迭代它们来渲染各自的 Product 组件。
/Product.vue
<template lang="pug">
div(class="product")
router-link(v-bind:to="url").product
img(v-bind:src="product.image" v-bind:alt="product.name" class="thumbnail" height="200")
p {{ product.name }}
button(class="snipcart-add-item"
v-bind:data-item-name="product.name"
v-bind:data-item-id="product.id"
v-bind:data-item-image="product.image"
data-item-url="/"
v-bind:data-item-price="product.price")
| Buy it for {{ product.price }}$
</template>
<script>
export default {
name: 'Product',
props: ['product'],
computed: {
url(){
return `/products/${this.product.id}`
}
}
}
</script>
我们为每个产品添加链接,通过路由(router)将我们导航到最后的组件。
//ProductDetails.vue
<template lang="pug">
div(class="product-details")
img(v-bind:src="product.image" v-bind:alt="product.name" class="thumbnail" height="200")
div(class="product-description" v-bind:href="url")
p {{ product.name }}
p {{ product. description}}
button(class="snipcart-add-item"
v-bind:data-item-name="product.name"
v-bind:data-item-id="product.id"
v-bind:data-item-image="product.image"
data-item-url="/"
v-bind:data-item-price="product.price")
| Buy it for {{ product.price }}$
</template>
<script>
export default {
name: 'ProductDetails',
computed: {
id(){
return this.$store.state.route.params.id
},
product(){
return this.$store.getters.getProductById(this.id)
}
}
}
</script>
这个组件的逻辑比上面两个的稍微复杂点。我们从 route 中得到产品的 ID ,然后再通过之前定义的 getter 函数获取对应的产品信息。
4. 创建 app
让我们开始使用新组件吧。
打开 App.vue 文件,里面的内容依然还是一开始通过 vue init webpack-simple 生成的。
将内容替换成以下所示:
<template lang="pug">
div(id="app")
TopContext
router-view
</template>
<script>
import TopContext from './components/TopContext.vue'
export default {
name: 'app',
components: { TopContext }
}
</script>
TopContext 组件不是很重要,它只是用来显示头部。关键的是 router-view:它将会由 VueRouter 动态决定,我们之前定义的相关组件将会被注入来替代 router-view 。
最后要更新的是 index.html ,为了我们的需求,我们在 src 目录下新建 static 文件夹并将 index.html 移到该文件夹下并将内容更新为如下所示:
<!DOCTYPE html><html lang="en">
<head>
<meta charset="utf-8">
<title>vue-snipcart</title>
</head>
<body>
<div id="app">
</div>
<script src="/build.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.2/jquery.min.js"></script>
<script src="https://cdn.snipcart.com/scripts/2.0/snipcart.js" data-api-key="YjdiNWIyOTUtZTIyMy00MWMwLTkwNDUtMzI1M2M2NTgxYjE0" id="snipcart"></script>
<link href="https://cdn.snipcart.com/themes/2.0/base/snipcart.min.css" rel="stylesheet" type="text/css" />
</body>
</html>
可以看到我们添加了必要的 Snipcart 脚本。用一个小组件来包含它们可能会更简洁,但因为所有的视图都需要用到,我们选择这样做。
5. 用 Prerender plugin 来实现 Vue.js SEO
我们 app 的所有内容都是由 JS 动态生成的,是不支持 SEO的:页面中异步内容不能很好的被爬虫抓取,错过这些自然流露对我们的电商网站是非常不明智的。
让我们使用 prerendering 为 Vue.js app 带来更多的 SEO 机会。
与 Vue SSR(Server-Side Rendering)(服务端渲染)相比 ,prerendering 实现更简单且直接。前者更容易 使用过度,除非你处理的是非常多的路由,另外,两者都可以达到基本相同的 SEO 结果。
Prerendering 允许我们保持前端为快速、轻量的静态站,它更容易爬取。
我们看一下怎么使用它,打开 webpack.config.js 文件在 export 里添加:
plugins: [
new CopyWebpackPlugin([{
from: 'src/static'
}]),
new PrerenderSpaPlugin(
path.join(__dirname, 'dist'),
[ '/', '/products/1', '/products/2', '/products/3']
)
]
好了,那它是怎样工作的?
CopyWebpackPlugin 会复制 static 目录(仅仅包含指向 Vue App 的视图)到 dist 目录下。然后 PrerenderSpaPlugin 会调用 PhantomJS 来下载页面的内容并将结果当做静态文件。
哇!我们的 Vue app 已经可以提前渲染和 SEO 友好的产品页面。
你可以运行以下命令自己测试:
npm run build
它将会生成生产环境下我们需要的一切。
其他重要的 SEO 考量
- 考虑添加适当的 meta 标签和 app 页面的站点图。 你可以从这个学到更多关于“ postProcessHtml ” 的内容。
- 友好的内容在 SEO 中扮演着重要角色。建议 app 的内容能容易的创建、编辑和优化。为了使编辑器更强大,考虑将无头绪的 CMS 组合构建真正的 JAMstack 。
- 使用 HTTPS 连接在 Google 占头等因素。我们在 Netlify 部署这个demo,它提供免费的 SSL 认证。
- Mobile-first indexing 和 移动端友好是非常重要的因素,确保移动端的体验跟桌面端一样快速和完整。
Github 仓库和现场 Vue demo
点击下方的链接查看 demo 和源码仓库:
Github 仓库 Vue.js demo
总结
我之前使用过 Vue ,因此制作这个教程比较顺利。我肯定花了一个小时在这个 demo 。使 CopyWebpackPlugin 插件工作让我费了不少劲,好在最后在它的文档里找到了答案。
我希望这篇文章可以鼓励开发者在一些项目中开始使用 Vue ,就像我说的,你可以开始慢慢地用它在现有的项目中开发很小的部分。我觉得这绝对值得一试。我正在开发上面这个,我的领导正在开发商家面板的最新特性,而且他非常喜欢 Vue。另外,当你配置正确后,一个 Vue app 可以实现好的 SEO 结果。
如果你觉得被鼓舞了,查看 the Vue.js Awesome list ,里面有大量的 Vue 示例和项目。
如果你最后深入理解了 Vue , 买件纪念T恤 或者 赞助下作者!
PS:我们将尝试让尤雨溪用 Snipcart 来销售 Vue T恤,但不保证能办到。我们知道 Threadless 在T恤方面也是非常棒的。