puppeteer在开发过程中的实践

卞龙亭
上篇文章我们谈了一下puppeteer是什么,以及具体能做什么,但文中谈到的在我们日常开发中并不常用到,截图我们有截图工具、前端自动化测试有phantomjs、selenium。不仅如此,为了SEO,前端基本都会做SSR,爬取这样的页面的话python得益于强大的模块,具有得天独道的优势。也许有人会问,那对于某些后台管理系统,是没有做SSR的,此时puppeteer的优势不就体现出来了吗?对于此种情况,首先,爬管理系统的数据的话需要有账号密码,我们没有。那如果是我们自己的系统的话,数据都是自己的,我还需要爬吗?

额。。。。

pupueteer看来似乎并没有想象中的那么强大。但实际情况下并非如此,虽然有些功能之前存在的一些框架也能做到,但毕竟对于我们前端开发来说,要想掌握新的技术就需要去学习新的语言,这样的话成本就很大了。

pupueteer提供了简洁的API和丰富的接口,以及其是一个nodejs库,所以从学习角度来讲对前端很友好,上手快。接下来就来具体描述一个在实际开发过程中使用puppeteer的场景。

在我们公司前端开发的过程中,大部分项目要去接CAS,其目的是为了获取COOKIE,以便于与后端进行交互。目前常见的场景存在三种形式。

1.前端网关nodejs,主要用于转换dubbo协议,获取数据,其项目中会用nodejs去接cas

2.有些应用,服务端接口需要登录cas后拿到回调的cookie去请求数据,而此时前端也要配代理,模拟cas登录,拿到cookie后写在header里

3.有些应用,直接登录相关应用的开发环境,然后把cookie拿过来通过document.cookie写到本地环境进行开发

第一种情况因为线上需要cas登录,暂且不表。

但对于后面两种情况,我们可以简单分析下,其实2和3都是在本地开发的时候拿到登录cas后的cookie,然后在请求数据的时候把cookie带回去拿到数据。但要放到服务器的时候实际上是前端把静态资源全部打包成某一个或若干个js文件(一般不会超过3个)。所以问题就在于我们怎么方便的拿到cookie,如果对于一个熟悉cas sso原理的话,其对接一下cas可能会很快,但弊端就是要新搭一个node服务,写一些登录cas,拿cookie的流程,而且如果针对不同权限角色的话,每次登录新的角色,都要重新用不同角色去登录系统,去拿cookie(之前我们在开发robert就是如此,涉及admin,运维,开发,测试,测试经理等不同角色,每次功能有掺和都是一个痛苦的切换过程),如果对于一个不明其理的开发来说,这无异于一枚张榜炸弹,你接口跨域我可以很简单的做下代理,你**还让我去接cas。。。。

(敲黑板) 划重点啦。

接下来我们就用puppeteer,来实现一个超级简单的获取cookie的过程,简单到令人发指。以下示例以robert为例,如有需要可根据相关需求自行修改。

以我负责开发的运维发布平台为例, 其实现思路如下

puppeteer

代码如下:

const puppeteer = require("puppeteer");

let cookie = {  
  name: "JSESSIONID",
  value: "",
  domain: "localhost",
  url: "http://localhost:3000/",
  path: "/",
  httpOnly: true,
  secure: false
};
const role = process.argv.pop();

const getRole = role => {  
  return {
    a: { //系统管理员
      username: "admin",
      password: "123456"
    },
    qa1: { //测试经理
      username: "04688",
      password: "123456"
    },
    qa: { //测试
      username: "01522",
      password: "123456"
    },
    dev: { //开发
      username: "04588",
      password: "123456"
    },
    ops: { //运维
      username: "04141",
      password: "123456"
    }
  }[role];
};

class Launch {  
  constructor(username, password) {
    this.username = username;
    this.password = password;
    this.flag = true;   //用来判断拦截第一次302
  }
  async init(page, browser) {
    // <!--  模拟cas登录 -->
    const casBrowser = await puppeteer.launch();    
    const loginPage = await casBrowser.newPage();
    await loginPage.goto("CAS开发环境地址");
    await loginPage.type("#username", this.username);
    await loginPage.type("#password", this.password);
    await loginPage.click("input[type=submit]");
    await loginPage.waitFor(1000);
    const cookies = await loginPage.cookies();
    cookies.map(v => {
      if (v.name === cookie.name) {
        cookie.value = v.value;
      }
    });
    await casBrowser.close();
    // <!--  拿到cookie后,关闭该实例 -->

    // <!-- 打开本地环境流程 -->
    let appBrowser = browser, appPage = page;
    if (appPage) {  // 如果cookie过期,直接在该实例上setCookie,无需新开实例
      await appPage.setCookie(cookie);  
      await appPage.reload()
      this.flag = true;
    } else {
      appBrowser = await puppeteer.launch({
        headless: false,
      });
      appPage = (await appBrowser.pages())[0];  
      await appPage.setCookie(cookie); //设置cookie
      const {
        width,
        height
      } = await appPage.evaluate(() => {
        return {
          width: window.outerWidth,
          height: window.outerHeight
        };
      });
      await appPage.setViewport({
        width,
        height
      });
      await appPage.goto("http://localhost:3000/");
    }
    // <!-- 本地流程结束 -->

    //监听请求
    await appPage.on("response", async (res) => {
      const status = res.status();
      if (status === 302 && this.flag) {    //cookie过期后,服务接口会重定向到cas登录页,所以只需拦截302即可知道cookie是否过期,当然也可和后端约定一个状态,如401
        this.flag = false;
        this.init(appPage, appBrowser);  //cookie过期,重新模拟登录cas获取新的cookie
        }
    })
  }
}

const {  
  username = "admin", password = "123456"
} = getRole(role) || {}  //默认登录admin账号

const launch = new Launch(username, password);

launch.init()

看过上篇的应该对基础用法有基本了解,接下来我们以此例顺便谈下新的知识点。 首先生成一个新的浏览器实例,不明其理的人会发现里面我们定义了一个getRole函数,而且里面有一坨不明所以的username和password。哈哈,这里就是我刚刚提到的关于不同权限的用户角色。当跳转成功后,通过page.cookies来获取界面的cookies,在我们robert中,我们只需拿到cookieJSESSIONID的值就行,拿到之后,关闭这个浏览器实例。

接下来新建一个浏览器实例,用来跳转本地环境, 然后把上面拿到的cookie设置到该实例,然后跳转到我们本地的环境即可。

简单明了的完成了拿cookie的过程。至此,我们完成了puppeteer在实际开发中的应用,大大减少了登录,切换账号的过程。我已经在robert项目中加入该流程,确实挺爽的。