需求分析
丁香医生小程序中的图片大多通过imgmap引入
目前的图片链接为http,通过静态资源库上传
const host = 'http://special.dxycdn.com/topic/lizy/resource/dxy-doctor-weapp';const imgMap = {close2: `${host}/ic_close2@2x.png`}
需要更换cdn的地址为https,通过管理后台上传
const imgMap = {close2: 'https://img1.dxycdn.com/2019/0125/899/3325176078032906703-22.png',}
一共有151张图片需要处理
方案选择
程序的实现思路大致如下,输入是imgMap.js源文件,输出是一份json文件,格式如下
{noMatchObj: {},noExistObj: {},successObj: {},failObj: {}}
程序的过程大致如下:
关键在于如何用管理后台实现图片的上传
如果是人工来做这个事情,需要有如下的步骤
- 管理后台登录
- 使用文件上传工具进行上传
- 将cdn地址粘贴到
imgMap.js中对应的位置
程序实现的两种思路
- 第一种是模拟登录,模拟请求,需要将管理后台上传图片的逻辑使用再node实现一遍
- 第二种是本次采用的puppeteer,使用headless Chrome去模拟上传图片的操作
Puppeteer
就跟名字一样,使用Node操作Chrome
这是官方的介绍
Puppeteer is a Node library which provides a high-level API to control Chrome or Chromium over the DevTools Protocol. Puppeteer runs headless by default, but can be configured to run full (non-headless) Chrome or Chromium.
官方举了一些🌰,可以使用Puppeteer做下面的事情
- 生成网页的PDF
- 爬虫,做服务端渲染
- 自动化的表单提交,UI测试,键盘输入
- 提供了一整套测试环境,可以测试最新的Javascript和浏览器特性
- 测试网页的Performance
- 测试Chrome插件
总的来说官方文档的引导部分还是不错的,照着简单的例子实现一个demo,后续就可以自己查API实现自己想要的功能了。
关键的API
这里的说明非常不offical!!!,只是用通俗语言便于大家理解
puppeteer.launch启动Chromebrowser.newPage打开一个新的tab页面page.goto页面跳转page.click(selector[, options])这里模拟点击传入选择器匹配的元素page.waitForNavigation([options])等待页面跳转page.waitForSelector等待选择器出现page.waitForResponse等待请求返回page.evaluate传入函数,执行一段javascript
参考阅读:
开发历程
将整个开发阶段分为几个步骤
- 初始数据处理
- 登录
- 上传
初始数据处理
简单的编辑器文件替换将imgMap.js做一些简单的处理,改为commonjs的模块规范,直接require引入即可使用。
const imgMap = require('./origin/imgMapTest')
正则校验
一眼扫过去发现大多链接的共性,使用正则表达式将少部分刁民t出来
/^(.+@.+(png|jpg))(?:.*)$/
文件存在性校验
管理后台上传图片的前提是本地要有这张图片,需要做一个文件本地存在性检验
在 puppeteer 中官方都是使用 async await 来实现异步逻辑, node 中的 fs 模块返回的不是 Promise ,还是 err-first 的 callback 形式,个人有两种方式来处理
- Node8提供了
util.promisify来将模块包装成 Promise - 使用开源的
node-fs-extra模块来处理文件系统相关操作,提供了更多易用的API
本次选择使用 fs-extra 来做文件存在性检验与写文件等操作
登录
这里是主函数
调用puppeteer.launch({ headless: false })启动一个Chrome,这里的headless参数用来控制以无头方式启动。
const main = async () => {const browser = await puppeteer.launch({ headless: false })const page = await browser.newPage()await page.goto('https://asktest.dxy.net/admin#/devtools/upload')}
很好,不让我们上传,直接跳转到了登录页面,我这里的登录方案是微信扫码。
使用puppeteer就是要分析页面的dom结构,通过选择器来找到需要触发事件的dom节点。
我们需要模拟的操作是
- 【点击 —- 返回电脑登录】
- 【点击 —- 微信图标】
通过分析页面的dom节点,一步一步试,传入一个唯一的选择器
// 模拟登录const login = async (page) => {try {// 点击返回电脑登录await page.click('[paneid=j_loginTab2]')// 点击微信登录await page.click('.third__content a:first-child')await page.waitForNavigation({waitUntil: 'load'});console.log('登录成功')} catch (error) {console.error(error);console.error('登录失败')return Promise.reject()}}
很好,扫完码我们就进来了,登录大功告成,注意一下这里的page.waitForNavigation调用,等到页面加载完成再进行后续的操作。
上传
找到文件上传入口
上传之前我们需要找到上传的入口,做以下操作的模拟
- 【点击 —- 内部工具】
- 【点击 —- 文件上传】
想想简直太简单了有木有!!!结果点开dom结构发现这选择器太难选中了吧。。。。。。。

好在有个神奇的东西,是一个chrome插件,将页面的操作记录为puppeteer的代码,
点击 Record,之后在页面依次做内部工具,文件上传操作
点击 Stop
将记录的代码复制下来跑一跑
await page.waitForSelector('.scroll-side-inner > .side-menu-content > ul > .is-active > .el-submenu__title')await page.click('.scroll-side-inner > .side-menu-content > ul > .is-active > .el-submenu__title')await page.waitForSelector('ul > .el-submenu > .el-menu > .submenu-wrapper > .is-active')await page.click('ul > .el-submenu > .el-menu > .submenu-wrapper > .is-active')
竟然跑不通???进到了用户管理里面???咱们的dom结构都是for循环出来的,class名都一样,这个recorder只是记录一层层的结构关系,怪不得进到了错误的菜单里,咱们简单修改一下,加上 nth-child
// 找到上传图片的入口const findUpload = async (page) => {try {// 点击内部工具await page.waitForSelector('.scroll-side-inner > .side-menu-content > ul > .el-submenu:nth-child(10)')await page.click('.scroll-side-inner > .side-menu-content > ul > .el-submenu:nth-child(10)')// 点击文件上传await page.waitForSelector('ul > .el-submenu.is-opened > .el-menu > .submenu-wrapper > .el-menu-item:nth-child(5)')await page.click('ul > .el-submenu.is-opened > .el-menu > .submenu-wrapper > .el-menu-item:nth-child(5)')} catch (error) {console.error(error)console.error('上传图片入口寻找失败')return Promise.reject()}}
折腾这么久才进到文件上传菜单!!!
上传核心逻辑
首先去puppeteer的文档查阅上传实现
只有这么一点
管理后台的上传组件基于element UI组件,puppeteer的原理则是直接去拿到input节点
看来又是一波DOM操作
await page.waitForFunction(() => {return location.href.indexOf('/devtools/upload')})// 找到文件上传的节点,并将其设为可见await page.waitForSelector('input[type=file].el-upload__input')await page.evaluate(() => {document.querySelector('input[type=file].el-upload__input').style.display = 'block'})const inputElement = await page.$('input[name=uploadFile]')
成功拿到上传的节点,开始上传单个文件的操作,这里使用page.waitForResponse来监听单个请求,也可以使用全局的请求拦截器Response去处理
// 上传单个文件const uploadSingleFile = async (page, inputElement, key, src, imgObj) => {try {await inputElement.uploadFile(src);await page.waitForResponse(res => ~res.url().indexOf('i/att?center_file_id'))const url = await page.evaluate(() => {return Promise.resolve(document.querySelector('input[type=text].el-input__inner[readonly=readonly]').value)})return url} catch (error) {console.error(`${key}:${src}上传失败`)console.error(error)}}
上传完成之后通过获取反显链接的input框拿到cdn地址,批量上传之前还有一个检验文件存在性的功能要实现,使用之前提到的fs-extra模块,以及最后的写文件操作也一并实现,使用Date.now()命名可以清晰看到文件的生成时间先后顺序。
const fs = require('fs-extra')async function exist(file) {try {const exists = await fs.pathExists(file)return exists} catch (error) {console.error(error);}}// 将JSON写成文件输出const writeJsonToFile = async (json) => {try {const fileName = `./${Date.now()}.json`await fs.writeJson(fileName, json, { space: 2 })} catch (error) {console.error(error)console.error('写文件失败')}}
成果
按照如下数据格式输出:
清晰的看到图片们的状态,successObj中的对象直接拿来使用,放入imgMap.js即可,剩下三种情况的图片手动处理即可。
{matchObj: {}, // 正则匹配成功noMatchObj: {}, // 正则匹配成功existObj: {}, // dropbox存在noExistObj: {}, // dropbox不存在,successObj: {}, // 上传成功,failObj: {} // 上传失败}
