yangrd

yangrd 查看完整档案

济南编辑济南大学  |  艺术 编辑三际  |  java高级工程师 编辑 www.sunxyz.cn/ 编辑
编辑

程序的目的是为了从人类手中接管现实世界

个人动态

yangrd 发布了文章 · 2020-03-18

记 Spring OAuth2 同时登录产生多个Token

在用户点击登录多次时会出现无法登录认证的情况 后台报错

org.springframework.security.oauth2.common.exceptions.OAuth2Exception: Incorrect result size: expected 1, actual 6

问题是创建Token的时候出现了并发,所导致的 github上也有相关的讨论
产生问题的代码

DefaultTokenServices createAccessToken的方法没有控制并发所导致的

public class DefaultTokenServices implements AuthorizationServerTokenServices, ResourceServerTokenServices, ConsumerTokenServices, InitializingBean {
    private int refreshTokenValiditySeconds = 2592000;
    private int accessTokenValiditySeconds = 43200;
    private boolean supportRefreshToken = false;
    private boolean reuseRefreshToken = true;
    private TokenStore tokenStore;
    private ClientDetailsService clientDetailsService;
    private TokenEnhancer accessTokenEnhancer;
    private AuthenticationManager authenticationManager;

    public DefaultTokenServices() {
    }

    public void afterPropertiesSet() throws Exception {
        Assert.notNull(this.tokenStore, "tokenStore must be set");
    }

    @Transactional
    public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
        OAuth2AccessToken existingAccessToken = this.tokenStore.getAccessToken(authentication);
        OAuth2RefreshToken refreshToken = null;
        if (existingAccessToken != null) {
            if (!existingAccessToken.isExpired()) {
                this.tokenStore.storeAccessToken(existingAccessToken, authentication);
                return existingAccessToken;
            }

            if (existingAccessToken.getRefreshToken() != null) {
                refreshToken = existingAccessToken.getRefreshToken();
                this.tokenStore.removeRefreshToken(refreshToken);
            }

            this.tokenStore.removeAccessToken(existingAccessToken);
        }

        if (refreshToken == null) {
            refreshToken = this.createRefreshToken(authentication);
        } else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
            ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken)refreshToken;
            if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
                refreshToken = this.createRefreshToken(authentication);
            }
        }

        OAuth2AccessToken accessToken = this.createAccessToken(authentication, refreshToken);
        this.tokenStore.storeAccessToken(accessToken, authentication);
        refreshToken = accessToken.getRefreshToken();
        if (refreshToken != null) {
            this.tokenStore.storeRefreshToken(refreshToken, authentication);
        }

        return accessToken;
    }
}

TokenStore的实现我选用的是JdbcTokenStore 然后

可以自定义一个CustomTokenServices, 实现createAccessToken方法 ,事务隔离级别设置成序列化.

class CustomTokenServices extends DefaultTokenServices {

    @Transactional(rollbackFor = Exception.class, isolation = Isolation.SERIALIZABLE)
    @Override
    public  OAuth2AccessToken createAccessToken(
            OAuth2Authentication authentication) throws AuthenticationException {
        return super.createAccessToken(authentication);
    }
}

当然也可以通过synchronized关键字

class CustomTokenServices extends DefaultTokenServices {
    @Override
    public synchronized OAuth2AccessToken createAccessToken(
            OAuth2Authentication authentication) throws AuthenticationException {
        return super.createAccessToken(authentication);
    }
}

最后选用的方案是

ALTER TABLE oauth_access_token
  ADD unique (authentication_id);

后记:

OAuth2版本

<dependency>
    <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
    <version>2.0.14.RELEASE</version>
</dependency>

JdbcTokenStore 的sql

Drop table  if exists oauth_access_token;
create table oauth_access_token (
  create_time timestamp default now(),
  token_id VARCHAR(255),
  token BLOB,
  authentication_id VARCHAR(255),
  user_name VARCHAR(255),
  client_id VARCHAR(255),
  authentication BLOB,
  refresh_token VARCHAR(255)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Drop table  if exists oauth_refresh_token;
create table oauth_refresh_token (
  create_time timestamp default now(),
  token_id VARCHAR(255),
  token BLOB,
  authentication BLOB
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

相关代码:
spring-cloud-oauth2

查看原文

赞 1 收藏 1 评论 1

yangrd 发布了文章 · 2020-03-12

记 synchronized 与 ReentrantLock 在spring 事务中失效了

导读:
最近发现某个项目的用户流水和账款金额出现了并发问题, 然后使用乐观锁解决了这个问题, 但是因为有跑批任务 在同一时刻 同一用户的账款 会增加多条流水于是就出现:

  • StaleObjectStateException,
  • ObjectOptimisticLockingFailureException,
  • CannotAcquireLockException

的异常, 虽然 账款的问题是解决了 (单笔没问题后面的批处理还是会把失败的流水继续推过来) 但还是不是一个比较好且简单的解, 开动脑筋,在不改动主要业务逻辑的情况下如何对其打补丁呢?
最简单的方式就是将并行改为串行 然后就在数据库查询修改的方法外面加了synchronized 关键字, 但加上之后并行查询的数据还是老数据 synchronized 居然失效了, 不应该啊, 然后想到了应该是事务未提交导致的,但是方法上有事务注解啊, 接着想到了 事务是通过动态代理实现的显然动态代理的方法并没有synchronized关键字修饰.
这是一个例子:

@Service
class WalletApplicationSerive{

    @Transactional(rollbackFor =  Exception.class)
    public synchronized void pay(accountId, amount, outerCode){
       // 数据库查询之后修改
    }
}

当我们使用事务的时候 其背后的实现是动态代理结合IOC容器获取出来的WalletApplicationSerive 的实例已经被Spring 换成了(spring 实现的更复杂, 为了方便理解这里以静态代理为例 ,这只是一个简单的示例)


class WalletApplicationSeriveProxy{

   private WalletApplicationSerivce tagert;
   
    public  void pay(accountId, amount, outerCode){
       tx.begin()
       try{
          tagert.pay(accountId, amount, outerCode)
       }catch(Exception e){
          tx.rollback()
          throw e;
       }
       tx.commit()
      
    }
}

动态代理:

   // 目标对象
    Object target ;

    Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), Main.class, new InvocationHandler() {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

           // 带有@Transcational注解的方法都会被拦截
           tx.begin();
           try{
               method.invoke(target, args);
           }catch(Exception e){
              tx.rollback();
              throw e;
           }
           tx.commit();
           return null;
        }
        
    });

一切都变得简单了 这也就是synchronized与ReentrantLock在spring 事务中失效了的原因.
要如何解决呢? 很简单 在 代理类外面增加上事务.

@Service
class WalletApplicationSerive{

    @Autowired
    InnerWalletApplicationSerive inner;
    
    public synchronized void pay(accountId, amount, outerCode){
       inner.pay(accountId, amount, outerCode)
    }
    
    @Service
    static class InnerWalletApplicationSerive{
        @Transactional(rollbackFor =  Exception.class)
        public void pay(accountId, amount, outerCode){
           // 数据库查询之后修改
        }
    }
}

问题解决, 但这里锁的粒度太粗了, 可以在对锁进行更细的粒度改造:

@Service
class WalletApplicationSerive{

    @Autowired
    InnerWalletApplicationSerive inner;
    
    public  void pay(accountId, amount, outerCode){
       synchronized(WalletLockManger.getLock(accountId)){
            inner.pay(accountId, amount, outerCode)
       }
      
    }
    
    @Service
    static class InnerWalletApplicationSerive{
        @Transactional(rollbackFor =  Exception.class)
        public void pay(accountId, amount, outerCode){
           // 数据库查询之后修改
        }
    }
}

class WalletLockManger {

    private static final Map<String, String> lockMap = new ConcurrentHashMap<>();

    public static String getLock(String accountId) {
        return lockMap.computeIfAbsent(accountId, Function.identity());
    }
}

synchronized(WalletLockManger.getLock(accountId)) 这个里有很大的改造空间, 后面如果 要部署多个实例的时候 可以将这里换成redis的锁.

查看原文

赞 0 收藏 0 评论 0

yangrd 关注了用户 · 2019-11-11

SOFAStack @sofastack

SOFAStack™(Scalable Open Financial Architecture Stack)是一套用于快速构建金融级分布式架构的中间件,也是在金融场景里锤炼出来的最佳实践。

关注 531

yangrd 关注了专栏 · 2019-11-11

金融级分布式架构SOFAStack

蚂蚁金服自主研发的分布式中间件(Scalable Open Financial Architecture)

关注 1047

yangrd 赞了回答 · 2019-11-05

解决Java:为何事务的实际执行结果与预期不符

从aop的原理 和简单了解的spring 类加载过程来讲,@Transactional 之所以起作用是因为aop动态代理 ,在方法进入时开启事务,方法结束时提交事务,spring在注入bean时 注入的是代理类。但是在相同类中 a()调用b(), 这个b 是 this.b() , 是原始方法 ,而不是代理生成的方法,所以注解不生效。

关注 2 回答 2

yangrd 发布了文章 · 2019-10-18

[微服务] spring cloud +docker 体系小节

技术选型

组件

  • 服务注册发现 - Cousl
  • 服务调用 - fegin
  • 客户端负载均衡 - Netflix Ribbon
  • 网关 - Spring Cloud Gateway
  • 断路器 - Netflix Hystrix
  • 配置中心 - Cousl
  • 链路跟踪 - zipkin+sleuth
  • 日志收集分析 - elkf
  • 应用监管 - spring boot admin / prometheus+grafana

容器

  • docker

扩展

安全相关

  • 服务无状态 - 前端服务(OAuth2) 后端服务 (JWT) 服务之间调用 通过JWT Token 增加安全性
  • 全站启用https 对外通信

高并发,高可用

  • 多节点多实例
  • 数据库 小库 读写分离
  • 缓存引入 redis 与 mongodb
  • cdn前端加速 (使用oss 静态资源)
  • 前端引入缓存

用户体验

  • 通过前面的手段使内容获取速度更快
  • UI 一致性与突出重点
  • 出现错误时友好的用户提示与引导
  • 最小操作步骤
  • 站在用户角度思考
查看原文

赞 0 收藏 0 评论 0

yangrd 发布了文章 · 2019-09-01

DDD在微服务中的应用

学习与应用DDD有一年半的时间了,今天用最简短的文字去记录一下我们在微服务中应用DDD的实践的经验,了解DDD与微服务的朋友也许听过一句话: 微服务与DDD相结合应用相得益彰,首先在讨论微服务之前,我们先了解一下什么是DDD,(这个系列会有三篇文章)DDD 全名叫做domain-driven design 翻译成中文叫做领域驱动设计。

DDD是什么?

那我们首先把这个词拆开来看:领域 驱动 设计
首先是领域 什么是领域?
领域是指某个范围,
什么是驱动?
驱动是指某物推进某事
什么是设计?
设计模式大家应该都了解过 这个设计与设计模式相类似。
连起来看就是范围推进设计或者说设计人员将领域专家的领域知识翻译成特定的设计 此处的设计人员是指计算机工程师,这是我个人见解,下面看看维基百科的定义:

领域驱动设计(英语:Domain-driven design,缩写 DDD)是一种通过将实现连接到持续进化的模型来满足复杂需求的软件开发方法。领域驱动设计的前提是:

  • 把项目的主要重点放在核心领域(core domain)和域逻辑
  • 把复杂的设计放在有界域(bounded context)的模型上
  • 发起一个创造性的合作之间的技术和域界专家以迭代地完善的概念模式,解决特定领域的问题

该词是由埃里克・埃文斯(Eric Evans)在其同名书中创造.

DDD的一些概念

领域专家 、通用语言 、领域模型、 战略建模、 战术建模。

领域专家: 领域专家,有时也称为主题专家,是非常重要的资源。他们对软件应用领域的了解程度对软件的成败有直接影响。

通用语言:领域专家与软件工程师之间沟通的语言 如: 用户这个名词, 在电商系统时可以是买家卖家管理员这些角色在特定的上下文中用户指的概念是不同的;还有订单号有时是指自有系统的订单号,有时是指天猫的订单号,有时是指WMS中的订单号,虽然都是订单号但在特定上下文中指代的是不同事物概念,而这里如何区分这些订单号就需要加上上下文也就是前文中的范围;如自有系统中的订单我们称其为系统订单,天猫订单我们称其为天猫订单;而这里的范围就是领域,领域可大可小,视其耦合紧密程度而定,而正是这些领域的划分是领域驱动中最困难的事物。

领域模型:描述某一事物或范围的一种模型或结构,可以是图文也可是UML或代码,通常对于软件开发工程师是代码 也就是描述某一事物的一组对象或服务,所以领域驱动相对于数据库驱动设计更加面向对象,编写也更困难一些,但其带来的好处也是显而易见的后期代码越来越多某件事物的某个动作与行为不会散布在各处,他们有自己专属的领域位置,项目越大效果越佳。

战略建模:是指领域对象与服务的划分与领域之间上下文的映射,是范围之间如何联系与划分的指导。

战术建模:是将领域模型通过代码实现的的一种模式与方法,在后面的文章中会仔细讲这一块。

DDD应用

了解与熟悉领域的边界与合理划分领域的职责,各司其职,前期领域知识的了解与领域的划分要和领域专家充分的交流沟通,技术手段通过进程或模块或类将不同的领域隔离开来。

欢迎大家评论!

参考资料

Domain-Driven Design

查看原文

赞 2 收藏 2 评论 0

yangrd 发布了文章 · 2019-08-17

Spring Boot Cloud CLI - 快速上手

导读

在日常开发与测试中有一些Spring Cloud 的相关的组件如 eureka、configserver、zipkin、hystrixdashboard等相对来说不容易发生变动,这里就介绍一种Spring 官方为我们提供的开箱即用的 Spring Boot Cloud CLI 只需要一条命令就可以启动这些相关的组件服务。

Spring Boot Cloud CLI 是什么?

Spring Boot Cloud CLI 官方是这样描述的:

Spring Boot CLI provides Spring Boot command line features for Spring Cloud. You can write Groovy scripts to run Spring Cloud component applications (e.g. @EnableEurekaServer). You can also easily do things like encryption and decryption to support Spring Cloud Config clients with secret configuration values. With the Launcher CLI you can launch services like Eureka, Zipkin, Config Server conveniently all at once from the command line (very useful at development time).

翻译之后:

Springbootcli为SpringCloud提供了Springboot命令行功能。您可以编写groovy脚本来运行Spring Cloud组件应用程序(例如@enableurekaserver)。您还可以轻松地执行加密和解密等操作,以支持具有机密配置值的SpringCloud配置客户机。使用启动器cli,您可以从命令行方便地同时启动诸如eureka、zipkin、config server等服务(在开发时非常有用)。

Spring Boot Cloud CLI 如何使用?

官方提供的最新版本是2.2.0.BUILD-SNAPSHOT,由于版本依赖的问题在运行时出了一些问题,然后将版本改为了:

1、安装:

1.1 需要先安装Spring CLI
已liunx 为例:

首先将刚才下载的Spring CLI v1.5.18.RELEASE 解压 ,然后命令设置如下:

export PATH=${PATH}:/spring-boot-cli-1.5.18.RELEASE/bin

windows:

set PATH=D:\spring-boot-cli-1.5.18.RELEASE\bin;%PATH%

更多安装方式参考官方文档

检查是否安装成功:

spring --version

1.2安装Spring Cloud CLI

命令如下:

spring install org.springframework.cloud:spring-cloud-cli:1.3.2.RELEASE

检查是否安装成功:

spring cloud --version

2、运行服务

在开发中运行Spring Cloud Services。
Launcher CLI可用于从命令行运行Eureka,Config Server等常用服务。列出您可以执行的可用服务spring cloud --list,并仅启动一组默认服务spring cloud。要选择要部署的服务,只需在命令行中列出它们,例如:

 spring cloud eureka configserver h2 zipkin

支持的可部署的服务摘要:

ServiceNameAddressDescription
eurekaEureka Serverhttp://localhost:8761Eureka服务器用于服务注册和发现
configserverConfig Serverhttp://localhost:8888配置服务并从本地目录./launcher提供配置
h2H2 Databasehttp://localhost:9095 (console), jdbc:h2:tcp://localhost:9096/{data}h2数据库
kafkaKafka Brokerhttp://localhost:9091 (actuator endpoints), localhost:9092
hystrixdashboardHystrix Dashboardhttp://localhost:7979断路器
dataflowDataflow Serverhttp://localhost:9393
zipkinZipkin Serverhttp://localhost:9411用于可视化跟踪
stubrunnerStub Runner Boothttp://localhost:8750

获取帮助

spring help cloud

可以使用具有相同名称的本地YAML文件(在当前工作目录或名为“config”或其中的子目录)中配置这些应用程序中的每一个~/.spring-cloud。例如,configserver.yml你可能想做这样的事情来为后端找到一个本地git存储库:
configserver.yml

spring:
  profiles:
    active: git
  cloud:
    config:
      server:
        git:
          uri: file://${user.home}/dev/demo/config-repo

3、 添加其他应用

可以在./config目录下添加自己定义的程序,例如:
./config/my-cloud.yml

spring:
  cloud:
    launcher:
      deployables:
        source:
          coordinates: maven://com.example:source:0.0.1-SNAPSHOT
          port: 7000
        sink:
          coordinates: maven://com.example:sink:0.0.1-SNAPSHOT
          port: 7001

当您使用

spring cloud --list

即可列出应用

source sink configserver dataflow eureka h2 hystrixdashboard kafka stubrunner zipkin

4、编写Groovy脚本和运行应用程序

Spring Cloud CLI支持大多数Spring Cloud声明性功能,例如@Enable*注释类。例如,这是一个功能齐全的Eureka服务器

app.groovy

@EnableEurekaServer
class Eureka {}

您可以从命令行运行,如下所示

spring run app.groovy

要包含其他依赖项,通常只需添加适当的启用特征的注释即可,例如@EnableConfigServer, @EnableOAuth2Sso或@EnableEurekaClient。要手动包含依赖项,您可以使用@Grab特殊的“Spring Boot”短样式工件坐标,即只使用工件ID(不需要组或版本信息),例如设置客户端应用程序以侦听AMQP来自Spring CLoud Bus的管理活动:
app.groovy

@Grab('spring-cloud-starter-bus-amqp')
@RestController
class Service {
  @RequestMapping('/')
  def home() { [message: 'Hello'] }
}

5、加密和解密

Spring Cloud CLI附带“加密”和“解密”命令。两者都接受相同形式的参数,并将键指定为必需的“--key”,例如

$ spring encrypt mysecret --key foo
682bc583f4641835fa2db009355293665d2647dade3375c0ee201de2a49f7bda
$ spring decrypt --key foo 682bc583f4641835fa2db009355293665d2647dade3375c0ee201de2a49f7bda
mysecret

要在文件中使用密钥(例如,用于加密的RSA公钥),请在密钥值前加上“@”并提供文件路径,例如

$ spring encrypt mysecret --key @ $ {HOME} 
/.ssh / id_rsa.pub AQAjPgt3eFZQXwt8tsHAVv / QHiY5sI2dRcR + ...

参考资料

getting-started-installing-the-cli
Spring Boot Cloud CLI

查看原文

赞 0 收藏 0 评论 0

yangrd 收藏了文章 · 2019-07-04

基于Vue SEO的四种方案

前言:众所周知,Vue SPA单页面应用对SEO不友好,当然也有相应的解决方案,下面列出几种最近研究和使用过的SEO方案,SSR和静态化基于Nuxt.js来说。

  • 1.SSR服务器渲染;
  • 2.静态化;
  • 3.预渲染prerender-spa-plugin;
  • 4.使用Phantomjs针对爬虫做处理。

1.SSR服务器渲染

关于服务器渲染:Vue官网介绍,对Vue版本有要求,对服务器也有一定要求,需要支持nodejs环境。

使用SSR权衡之处:

  • 开发条件所限,浏览器特定的代码,只能在某些生命周期钩子函数 (lifecycle hook) 中使用;一些外部扩展库 (external library) 可能需要特殊处理,才能在服务器渲染应用程序中运行;
  • 环境和部署要求更高,需要Node.js server 运行环境;
  • 高流量的情况下,请准备相应的服务器负载,并明智地采用缓存策略。

优势:

  • 更好的 SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面;
  • 更快的内容到达时间 (time-to-content),特别是对于缓慢的网络情况或运行缓慢的设备。

不足:(开发中遇到的坑)
1.一套代码两套执行环境,会引起各种问题,比如服务端没有window、document对象,处理方式是增加判断,如果是客户端才执行:

if(process.browser){
 console.log(window);
}

引用npm包,带有dom操作的,例如:wowjs,不能用import的方式,改用:

if (process.browser) {
     var { WOW } = require('wowjs');
     require('wowjs/css/libs/animate.css');
 }

2.Nuxt asyncData方法,初始化页面前先得到数据,但仅限于页面组件调用:

// 并发加载多个接口:
  async asyncData ({ app, query }) {
    let [resA, resB, resC] = await Promise.all([
      app.$axios.get('/api/a'),
      app.$axios.get('/api/b'),
      app.$axios.get('/api/c'),
     ])
     
     return {
       dataA: resA.data,
       dataB: resB.data,
       dataC: resC.data,
     }
  }

在asyncData中获取参数:

1.获取动态路由参数,如:

/list/:id' ==>  '/list/123

接收:

async asyncData ({ app, query }) {
  console.log(app.context.params.id) //123
}
2.获取url?获取参数,如:

/list?id=123

接收:

async asyncData ({ app, query }) {
  console.log(query.id) //123
}

3.如果你使用v-if语法,部署到线上大概也会遇到这个错误:

Error while initializing app DOMException: Failed to execute 'appendChild' on 'Node': This node type does not support this method.
    at Object.We [as appendChild]

根据github nuxt上的issue第1552条提示,要将v-if改为v-show语法。

4.坑太多,留坑,晚点更。

2.静态化

静态化是Nuxt.js打包的另一种方式,算是 Nuxt.js 的一个创新点,页面加载速度很快。
在 Nuxt.js 执行 generate 静态化打包时,动态路由会被忽略。

-| pages/
---| index.vue
---| users/
-----| _id.vue

需要动态路由先生成静态页面,你需要指定动态路由参数的值,并配置到 routes 数组中去。

// nuxt.config.js
module.exports = {
  generate: {
    routes: [
      '/users/1',
      '/users/2',
      '/users/3'
    ]
  }
}

运行打包,即可看见打包出来的页面。
但是如果路由动态参数的值是动态的而不是固定的,应该怎么做呢?

  • 使用一个返回 Promise 对象类型 的 函数;
  • 使用一个回调是 callback(err, params) 的 函数。
// nuxt.config.js
import axios from 'axios'

export default {
  generate: {
    routes: function () {
      return axios.get('https://my-api/users')
      .then((res) => {
        return res.data.map((user) => {
          return {
            route: '/users/' + user.id,
            payload: user
          }
        })
      })
    }
  }
}

现在我们可以从/users/_id.vue访问的payload,如下所示:

async asyncData ({ params, error, payload }) {
  if (payload) return { user: payload }
  else return { user: await backend.fetchUser(params.id) }
}

如果你的动态路由的参数很多,例如商品详情,可能高达几千几万个。需要一个接口返回所有id,然后打包时遍历id,打包到本地,如果某个商品修改了或者下架了,又要重新打包,数量多的情况下打包也是非常慢的,非常不现实。
优势:

  • 纯静态文件,访问速度超快;
  • 对比SSR,不涉及到服务器负载方面问题;
  • 静态网页不宜遭到黑客攻击,安全性更高。

不足:

  • 如果动态路由参数多的话不适用。

3.预渲染prerender-spa-plugin

如果你只是用来改善少数营销页面(例如 /, /about, /contact 等)的 SEO,那么你可能需要预渲染。无需使用 web 服务器实时动态编译 HTML,而是使用预渲染方式,在构建时 (build time) 简单地生成针对特定路由的静态 HTML 文件。优点是设置预渲染更简单,并可以将你的前端作为一个完全静态的站点。

$ cnpm install prerender-spa-plugin --save

vue cli 3 vue.config.js配置:

const PrerenderSPAPlugin = require('prerender-spa-plugin');
const Renderer = PrerenderSPAPlugin.PuppeteerRenderer;
const path = require('path');
module.exports = {
    configureWebpack: config => {
        if (process.env.NODE_ENV !== 'production') return;
        return {
            plugins: [
                new PrerenderSPAPlugin({
                    // 生成文件的路径,也可以与webpakc打包的一致。
                    // 下面这句话非常重要!!!
                    // 这个目录只能有一级,如果目录层次大于一级,在生成的时候不会有任何错误提示,在预渲染的时候只会卡着不动。
                    staticDir: path.join(__dirname,'dist'),
                    // 对应自己的路由文件,比如a有参数,就需要写成 /a/param1。
                    routes: ['/', '/product','/about'],
                    // 这个很重要,如果没有配置这段,也不会进行预编译
                    renderer: new Renderer({
                        inject: {
                            foo: 'bar'
                        },
                        headless: false,
                        // 在 main.js 中 document.dispatchEvent(new Event('render-event')),两者的事件名称要对应上。
                        renderAfterDocumentEvent: 'render-event'
                    })
                }),
            ],
        };
    }
}

在main.js中添加:

new Vue({
  router,
  render: h => h(App),
  mounted () {
    document.dispatchEvent(new Event('render-event'))
  }
}).$mount('#app')

注意:router中必须设置 mode: “history”

打包出来可以看见文件,打包出文件夹/index.html,例如:about => about/index.html,里面有html内容。

优势:

  • 改动小,引入个插件就完事;

不足:

  • 无法使用动态路由;
  • 只适用少量页面的项目,页面多达几百个的情况下,打包会很很很慢;

4.使用Phantomjs针对爬虫做处理

Phantomjs是一个基于webkit内核的无头浏览器,即没有UI界面,即它就是一个浏览器,只是其内的点击、翻页等人为相关操作需要程序设计实现。
虽然“PhantomJS宣布终止开发”,但是已经满足对Vue的SEO处理。
这种解决方案其实是一种旁路机制,原理就是通过Nginx配置,判断访问的来源UA是否是爬虫访问,如果是则将搜索引擎的爬虫请求转发到一个node server,再通过PhantomJS来解析完整的HTML,返回给爬虫。

图片描述

具体代码戳这里:vue-seo-phantomjs
要安装全局phantomjs,局部express,测试:

$ phantomjs spider.js 'https://www.baidu.com'

如果见到在命令行里出现了一推html,那恭喜你,你已经征服PhantomJS啦。
启动之后或者用postman在请求头增加User-Agent值为Baiduspider,效果一样的。

部署上线
线上要安装nodepm2phantomjs,nginx相关配置:

upstream spider_server {
  server localhost:3000;
}

server {
    listen       80;
    server_name  example.com;
    
    location / {
      proxy_set_header  Host            $host:$proxy_port;
      proxy_set_header  X-Real-IP       $remote_addr;
      proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;

      if ($http_user_agent ~* "Baiduspider|twitterbot|facebookexternalhit|rogerbot|linkedinbot|embedly|quora link preview|showyoubot|outbrain|pinterest|slackbot|vkShare|W3C_Validator|bingbot|Sosospider|Sogou Pic Spider|Googlebot|360Spider") {
        proxy_pass  http://spider_server;
      }
    }
}

优势:

  • 完全不用改动项目代码,按原本的SPA开发即可,对比开发SSR成本小不要太多;
  • 对已用SPA开发完成的项目,这是不二之选。

不足:

  • 部署需要node服务器支持;
  • 爬虫访问比网页访问要慢一些,因为定时要定时资源加载完成才返回给爬虫;
  • 如果被恶意模拟百度爬虫大量循环爬取,会造成服务器负载方面问题,解决方法是判断访问的IP,是否是百度官方爬虫的IP。

总结

如果构建大型网站,如商城类,别犹豫,直接上SSR服务器渲染,当然也有相应的坑等你,社区较成熟,英文好点,一切问题都迎刃而解。
如果只是个人博客、公司官网这类,其余三种都可以。
如果对已用SPA开发完成的项目进行SEO优化,而且支持node服务器,请使用Phantomjs

很少写文章,这是我这个月对Vue SEO方案的探索,写的不对的地方请指出,谢谢理解~

2020.4.8更
去年7月份上线改版的一呼百应商城,可以右键查看源代码看效果,就是用了服务器渲染SSR处理,本来由于项目进度以及对服务器渲染了解程度不够深入,前期是单页面,后来对SEO有要求,再把单页改造成SSR。当中遇到种种坑也一一解决了。

忙于工作,微信:16626412342,QQ:1058566903,欢迎沟通交流~

查看原文

yangrd 赞了文章 · 2019-07-04

基于Vue SEO的四种方案

前言:众所周知,Vue SPA单页面应用对SEO不友好,当然也有相应的解决方案,下面列出几种最近研究和使用过的SEO方案,SSR和静态化基于Nuxt.js来说。

  • 1.SSR服务器渲染;
  • 2.静态化;
  • 3.预渲染prerender-spa-plugin;
  • 4.使用Phantomjs针对爬虫做处理。

1.SSR服务器渲染

关于服务器渲染:Vue官网介绍,对Vue版本有要求,对服务器也有一定要求,需要支持nodejs环境。

使用SSR权衡之处:

  • 开发条件所限,浏览器特定的代码,只能在某些生命周期钩子函数 (lifecycle hook) 中使用;一些外部扩展库 (external library) 可能需要特殊处理,才能在服务器渲染应用程序中运行;
  • 环境和部署要求更高,需要Node.js server 运行环境;
  • 高流量的情况下,请准备相应的服务器负载,并明智地采用缓存策略。

优势:

  • 更好的 SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面;
  • 更快的内容到达时间 (time-to-content),特别是对于缓慢的网络情况或运行缓慢的设备。

不足:(开发中遇到的坑)
1.一套代码两套执行环境,会引起各种问题,比如服务端没有window、document对象,处理方式是增加判断,如果是客户端才执行:

if(process.browser){
 console.log(window);
}

引用npm包,带有dom操作的,例如:wowjs,不能用import的方式,改用:

if (process.browser) {
     var { WOW } = require('wowjs');
     require('wowjs/css/libs/animate.css');
 }

2.Nuxt asyncData方法,初始化页面前先得到数据,但仅限于页面组件调用:

// 并发加载多个接口:
  async asyncData ({ app, query }) {
    let [resA, resB, resC] = await Promise.all([
      app.$axios.get('/api/a'),
      app.$axios.get('/api/b'),
      app.$axios.get('/api/c'),
     ])
     
     return {
       dataA: resA.data,
       dataB: resB.data,
       dataC: resC.data,
     }
  }

在asyncData中获取参数:

1.获取动态路由参数,如:

/list/:id' ==>  '/list/123

接收:

async asyncData ({ app, query }) {
  console.log(app.context.params.id) //123
}
2.获取url?获取参数,如:

/list?id=123

接收:

async asyncData ({ app, query }) {
  console.log(query.id) //123
}

3.如果你使用v-if语法,部署到线上大概也会遇到这个错误:

Error while initializing app DOMException: Failed to execute 'appendChild' on 'Node': This node type does not support this method.
    at Object.We [as appendChild]

根据github nuxt上的issue第1552条提示,要将v-if改为v-show语法。

4.坑太多,留坑,晚点更。

2.静态化

静态化是Nuxt.js打包的另一种方式,算是 Nuxt.js 的一个创新点,页面加载速度很快。
在 Nuxt.js 执行 generate 静态化打包时,动态路由会被忽略。

-| pages/
---| index.vue
---| users/
-----| _id.vue

需要动态路由先生成静态页面,你需要指定动态路由参数的值,并配置到 routes 数组中去。

// nuxt.config.js
module.exports = {
  generate: {
    routes: [
      '/users/1',
      '/users/2',
      '/users/3'
    ]
  }
}

运行打包,即可看见打包出来的页面。
但是如果路由动态参数的值是动态的而不是固定的,应该怎么做呢?

  • 使用一个返回 Promise 对象类型 的 函数;
  • 使用一个回调是 callback(err, params) 的 函数。
// nuxt.config.js
import axios from 'axios'

export default {
  generate: {
    routes: function () {
      return axios.get('https://my-api/users')
      .then((res) => {
        return res.data.map((user) => {
          return {
            route: '/users/' + user.id,
            payload: user
          }
        })
      })
    }
  }
}

现在我们可以从/users/_id.vue访问的payload,如下所示:

async asyncData ({ params, error, payload }) {
  if (payload) return { user: payload }
  else return { user: await backend.fetchUser(params.id) }
}

如果你的动态路由的参数很多,例如商品详情,可能高达几千几万个。需要一个接口返回所有id,然后打包时遍历id,打包到本地,如果某个商品修改了或者下架了,又要重新打包,数量多的情况下打包也是非常慢的,非常不现实。
优势:

  • 纯静态文件,访问速度超快;
  • 对比SSR,不涉及到服务器负载方面问题;
  • 静态网页不宜遭到黑客攻击,安全性更高。

不足:

  • 如果动态路由参数多的话不适用。

3.预渲染prerender-spa-plugin

如果你只是用来改善少数营销页面(例如 /, /about, /contact 等)的 SEO,那么你可能需要预渲染。无需使用 web 服务器实时动态编译 HTML,而是使用预渲染方式,在构建时 (build time) 简单地生成针对特定路由的静态 HTML 文件。优点是设置预渲染更简单,并可以将你的前端作为一个完全静态的站点。

$ cnpm install prerender-spa-plugin --save

vue cli 3 vue.config.js配置:

const PrerenderSPAPlugin = require('prerender-spa-plugin');
const Renderer = PrerenderSPAPlugin.PuppeteerRenderer;
const path = require('path');
module.exports = {
    configureWebpack: config => {
        if (process.env.NODE_ENV !== 'production') return;
        return {
            plugins: [
                new PrerenderSPAPlugin({
                    // 生成文件的路径,也可以与webpakc打包的一致。
                    // 下面这句话非常重要!!!
                    // 这个目录只能有一级,如果目录层次大于一级,在生成的时候不会有任何错误提示,在预渲染的时候只会卡着不动。
                    staticDir: path.join(__dirname,'dist'),
                    // 对应自己的路由文件,比如a有参数,就需要写成 /a/param1。
                    routes: ['/', '/product','/about'],
                    // 这个很重要,如果没有配置这段,也不会进行预编译
                    renderer: new Renderer({
                        inject: {
                            foo: 'bar'
                        },
                        headless: false,
                        // 在 main.js 中 document.dispatchEvent(new Event('render-event')),两者的事件名称要对应上。
                        renderAfterDocumentEvent: 'render-event'
                    })
                }),
            ],
        };
    }
}

在main.js中添加:

new Vue({
  router,
  render: h => h(App),
  mounted () {
    document.dispatchEvent(new Event('render-event'))
  }
}).$mount('#app')

注意:router中必须设置 mode: “history”

打包出来可以看见文件,打包出文件夹/index.html,例如:about => about/index.html,里面有html内容。

优势:

  • 改动小,引入个插件就完事;

不足:

  • 无法使用动态路由;
  • 只适用少量页面的项目,页面多达几百个的情况下,打包会很很很慢;

4.使用Phantomjs针对爬虫做处理

Phantomjs是一个基于webkit内核的无头浏览器,即没有UI界面,即它就是一个浏览器,只是其内的点击、翻页等人为相关操作需要程序设计实现。
虽然“PhantomJS宣布终止开发”,但是已经满足对Vue的SEO处理。
这种解决方案其实是一种旁路机制,原理就是通过Nginx配置,判断访问的来源UA是否是爬虫访问,如果是则将搜索引擎的爬虫请求转发到一个node server,再通过PhantomJS来解析完整的HTML,返回给爬虫。

图片描述

具体代码戳这里:vue-seo-phantomjs
要安装全局phantomjs,局部express,测试:

$ phantomjs spider.js 'https://www.baidu.com'

如果见到在命令行里出现了一推html,那恭喜你,你已经征服PhantomJS啦。
启动之后或者用postman在请求头增加User-Agent值为Baiduspider,效果一样的。

部署上线
线上要安装nodepm2phantomjs,nginx相关配置:

upstream spider_server {
  server localhost:3000;
}

server {
    listen       80;
    server_name  example.com;
    
    location / {
      proxy_set_header  Host            $host:$proxy_port;
      proxy_set_header  X-Real-IP       $remote_addr;
      proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;

      if ($http_user_agent ~* "Baiduspider|twitterbot|facebookexternalhit|rogerbot|linkedinbot|embedly|quora link preview|showyoubot|outbrain|pinterest|slackbot|vkShare|W3C_Validator|bingbot|Sosospider|Sogou Pic Spider|Googlebot|360Spider") {
        proxy_pass  http://spider_server;
      }
    }
}

优势:

  • 完全不用改动项目代码,按原本的SPA开发即可,对比开发SSR成本小不要太多;
  • 对已用SPA开发完成的项目,这是不二之选。

不足:

  • 部署需要node服务器支持;
  • 爬虫访问比网页访问要慢一些,因为定时要定时资源加载完成才返回给爬虫;
  • 如果被恶意模拟百度爬虫大量循环爬取,会造成服务器负载方面问题,解决方法是判断访问的IP,是否是百度官方爬虫的IP。

总结

如果构建大型网站,如商城类,别犹豫,直接上SSR服务器渲染,当然也有相应的坑等你,社区较成熟,英文好点,一切问题都迎刃而解。
如果只是个人博客、公司官网这类,其余三种都可以。
如果对已用SPA开发完成的项目进行SEO优化,而且支持node服务器,请使用Phantomjs

很少写文章,这是我这个月对Vue SEO方案的探索,写的不对的地方请指出,谢谢理解~

2020.4.8更
去年7月份上线改版的一呼百应商城,可以右键查看源代码看效果,就是用了服务器渲染SSR处理,本来由于项目进度以及对服务器渲染了解程度不够深入,前期是单页面,后来对SEO有要求,再把单页改造成SSR。当中遇到种种坑也一一解决了。

忙于工作,微信:16626412342,QQ:1058566903,欢迎沟通交流~

查看原文

赞 108 收藏 74 评论 13

认证与成就

  • 获得 238 次点赞
  • 获得 6 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 6 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

  • sanji-boot

    基于Spring Boot 的网站脚手架 :=> 微核心 : 提供权限认证 用户管理 操作日志 等常用功能; 前端 使用 vue, bootstrap-table, nestable ,z-tree,jquery-confirm 等 支持响应式 Material Design 风格 ;后端使用 Spring MVC, Spring Data JPA, Spring security || Apache Shiro;

  • web-spider

    一个简单的爬虫框架

注册于 2017-03-02
个人主页被 3.7k 人浏览