需求分析
丁香医生小程序中的图片大多通过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: {} // 上传失败
}