Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

【译】Vue.js 教程:构建并提前渲染一个 SEO 友好的网站 #9

Open
Hugo-seth opened this issue Apr 22, 2017 · 10 comments

Comments

@Hugo-seth
Copy link
Owner

Hugo-seth commented Apr 22, 2017

原文地址:Vue.js Tutorial: A Prerendered, SEO-Friendly Example [Live Demo] - Snipcart

原文作者:Maxime Laboissonniere

“受不了了,我们内部的报告面板太难用了”,我们的生产经理生气的说。他尝试下载数据的 app 简直是个灾难。

“Max ,我们需要让它更好用,你能解决吗”,

“说实话,我宁愿做一个全新的 app”,我笑着说。

“好,按你说的做。那就交给你了,朋友”,

我保持微笑的搓了搓手。终于有机会使用每个人都在讨论的 JS 框架:Vue.js 了。

------------------------------- 我是正文开始的分割线 ------------------------------

我刚写完上述的 app 而且我爱死它了。

由于最近经历的启发,我花了些时间为社区制作了一个 Vue.js 的教程。现在在这里我将主要讲两方面的主题:

  1. 如何用 Vue.js 构建一个简洁的网页 app

  2. 怎样使用 prerender-spa-plugin 做到 SEO 和提前渲染

具体来说,我将带大家创建一个 SEO 友好的简单产品页面。会有现场的 demo 和代码仓库给到大家。

我是通过 our latest headless CMS post 简单的接触到 Vue 的,但这次会是更深入的了解,所以我很兴奋。

我先简单介绍下这个渐进式的框架,因为可能有一些朋友还不了解。

Vue.js 到底是什么?

what-is-vuejs-definition

Vue 是一套帮你构建网页界面的轻量的渐进式 JavaScript 框架。

千万别被定义中的 “JS 框架” 欺骗了。Vue 和跟它对应的流行框架-- React.jsAngular.js 是非常不同的。对初学者来说,它不是 Google 和 Facebook 那样的商业巨头所有的开源产品。

尤雨溪在 2014 年第一次发布 Vue,目的是创造一个自底向上增量式的现代 JS 库。这是 Vue 最强大的特性之一:构建可插入的组件并可以在项目不重构的情况下添加。任何开发者都可以在项目中尝试 Vue 而不会对现有代码产生危害或负担。

模式和专业术语先抛开,我认为 Vue 的前提是:

1. 在一开始你不能知道 app 的全部状态架构

2. 你的数据肯定会在运行时改变

这些限制塑造了库本身:渐进式、基于组件和响应式。这种颗粒式的架构组件让你很容易在保持复用性的同时分离逻辑概念。从顶层来看,它通过原生方法与视图绑定数据,所以可以在必要的时候魔法般的更新(通过观察者)。尽管相同的定义会在很多响应式的前端框架里出现,我发现 Vue 更加优雅的实现了,而且对于我大多数的需求,它是更好的做法。

Vue 的学习曲线相比 React 更平稳,React 需要 JSX 模板知识。甚至有人说 Vue 是去掉了棘手部分的 React 。

最后,Vue 提供的 performance & insightful 开发工具给予了很棒的开发体验。难怪选择 Vue 的开发者像火箭式地上升

vuejs-popularity

从开源项目( Laravel 和 PageKit )到企业( Gitlab 和 Codeship )(别提阿里巴巴和百度),非常多的组织在使用 Vue 。

现在,是时候看看我们要如何使用它。

我们的 Vue.js 例子:一个快速、SEO 友好的电子贸易 app

这个章节我将展示如何使用 Vue 2.0 和 Snipcart 构建一个简单的电子贸易 app ,开发人员编写购物平台的 HTML/JS 。我们同样可以看到如何让爬虫可以爬到产品页面。

预备知识

如果你想深入了解 Vue 2.0 的一切,请点击这里查看系列教程

1. 搭建开发环境

首先使用 vue-cli 生成一个基本的 Vue app 。在终端输入:

npm install -g vue-cli
vue init webpack-simple vue-snipcart

上面的命令将会新建一个名为 vue-snipcart 的新目录,里面包含使用 vue-loader 的基本配置。它也让我们可以编写单文件组件( template/js/css 在同一文件中 )。

我们希望这个示例更加像真实项目,所以我们添加两个在大型 Vue 单页应用中广泛使用的模块:vuexvue-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

vuejs-seo-prerendering

我们 app 的所有内容都是由 JS 动态生成的,是不支持 SEO的:页面中异步内容不能很好的被爬虫抓取,错过这些自然流露对我们的电商网站是非常不明智的。

让我们使用 prerenderingVue.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 考量

  1. 考虑添加适当的 meta 标签和 app 页面的站点图。你可以从这个学到更多关于“ postProcessHtml ” 的内容

  2. 友好的内容在 SEO 中扮演着重要角色。建议 app 的内容能容易的创建、编辑和优化。为了使编辑器更强大,考虑将无头绪的 CMS 组合构建真正的 JAMstack 。

  3. 使用 HTTPS 连接在 Google 占头等因素。我们在 Netlify 部署这个demo,它提供免费的 SSL 认证。

  4. Mobile-first indexing移动端友好是非常重要的因素,确保移动端的体验跟桌面端一样快速和完整。

Github 仓库和现场 Vue demo

vuejs-tutorial-live-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恤方面也是非常棒的。

@Hugo-seth Hugo-seth changed the title [译]Vue.js 教程:构建并提前渲染一个 SEO 友好的网站 [译] Vue.js 教程:构建并提前渲染一个 SEO 友好的网站 Apr 22, 2017
@Hugo-seth Hugo-seth added the Vue label Apr 26, 2017
@Hugo-seth Hugo-seth changed the title [译] Vue.js 教程:构建并提前渲染一个 SEO 友好的网站 【译】Vue.js 教程:构建并提前渲染一个 SEO 友好的网站 Sep 14, 2017
@whatwg6
Copy link

whatwg6 commented Sep 18, 2017

m

@MingOf
Copy link

MingOf commented Oct 27, 2017

同款头像... prerender-spa-plugin原理是什么呢? 目前看来只是一个webpack的插件,prerender不是应该在后台做的吗?我有点懵

@Hugo-seth
Copy link
Owner Author

@MingOf 原理文章有讲到:

CopyWebpackPlugin 会复制 static 目录(仅仅包含指向 Vue App 的视图)到 dist 目录下。然后 PrerenderSpaPlugin 会调用 PhantomJS 来下载页面的内容并将结果当做静态文件

意思就是插件会帮你执行 js 把页面先渲染成有内容的静态页,这样搜索引擎就可以抓到内容,SSR(Server-Side Rendering)是要后台配合的,这个不需要。

@Chaoyingz
Copy link

预渲染后JS全部失效可能是什么原因?

@Hugo-seth
Copy link
Owner Author

@caoyiii 这应该不可能吧,我肯定没办法猜原因的。插件只会执行一遍你的 js 并不会改你的 js ,可能的原因是你的打包逻辑有问题,打包后的 js 有问题

@Hugo-seth
Copy link
Owner Author

@datianjiao 差不多。文章里的 demo 生成的结构如下:

vue

你可以把项目 clone 下来跑一下看看。

@Maplesog
Copy link

Maplesog commented Mar 1, 2018

@Hugo-seth 你好, @caoyiii 说的问题确实存在。已弃坑。

@Hugo-seth
Copy link
Owner Author

@Maplesog 好吧。。。我只跑过文章的 demo

@Chaoyingz
Copy link

已上nuxt

@whatwg6
Copy link

whatwg6 commented Mar 2, 2018

不太喜欢nuxt自动创建路由的机制,还有这个预渲染只能用在history模式,我用没有发生js全部失效的问题,虽然在打包后的文件夹里有对应的静态html但是访问对应的地址即使刷新仍然没有访问静态html,官方的demo没问题,所以挺坑。

@github-staff github-staff deleted a comment Apr 26, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants