新手一枚,权当是学习。同时也了解app-inspector的实现原理。
一. 从入口说起
我们在使用app-inspector时候通过命令行 类似于: app-inspector -u 192.168.56.101:5555
进行启动,那我们就来看看这个过程是怎么进行的。
我们从app-inspector 看起
co(update, {
pkg: pkg,
callback: init
});
这里使用了npm-update的库,主要是获取到当前npm库的最新版本。{pkg: pkg, callback: init}
即为update的参数,同时callback用于后续的回调使用。再回到init方法中:
function init(error, data) {
if (data && data.version && pkg.version !== data.version) {
printInfo([`version ${pkg.version} is outdate`, `run: npm i -g ${pkg.name}@${data.version}`]);
}
if (program.versions) {
console.info(`${EOL} ${chalk.grey(pkg.version)} ${EOL}`);
process.exit(0);
}
if (!program.udid) {
program.help();
process.exit(0);
}
_.merge(options, _.getConfig(program));
co(Inspector, options).catch(e => {
console.log(e);
});
}
首先判断获取的最新的版本是否与当前的版本一致了。如果不通就如下图所示:
再来合并默认的options参数以及我们通过命令行传参的内容,传递给inspector方法做为参数。
module.exports = function *(options) {
try {
yield parseOptions(options);
//初始化以及启动服务端
const server = new Server(options);
yield server.start();
const url = `http://${_.ipv4}:${options.port}`;
logger.debug(`server start at: ${url}`);
// 初始化设备
yield initDevice(options);
global.serverStarted = true;
logger.info(`inspector start at: ${chalk.white(url)}`);
if (!options.silent) {
//初始化完成后,若参数中未带有slient的话,则打开浏览器
yield openBrowser(url);
}
} catch (e) {
console.log(e);
}
}
这里的初始化设备的过程,主要是通过我们传参的uuid进行判断是android还是iOS的设备通过判断来启动对应不同的wd。
function *initDevice(options) {
const udid = options.udid;
const isIOS = _.getDeviceInfo(udid).isIOS;
if (isIOS) {
yield require('./ios').initDevice(udid);
} else {
yield require('./android').initDevice(udid);
}
}
到这里,服务就算启动完成了,下来就是具体的路由的处理了。
二. 从路由看
module.exports = function(app) {
rootRouter
.get('/', function *(next) {
const isIOS = _.getDeviceInfo(this._options.udid).isIOS;
if (global.serverStarted) {
if (isIOS) {
yield require('./ios').dumpXMLAndScreenShot();
} else {
yield require('./android').dumpXMLAndScreenShot();
}
}
this.body = yield render('index.html', {
data: {
title: pgk.name,
version: pgk.version,
isIOS: isIOS,
serverStarted: global.serverStarted
}
});
});
app
.use(rootRouter.routes());
};
从package.json可以看出nodejs的后台使用的是koa的框架。虽然对这个不太了解。但是至少能够看出来。这个业务逻辑的一些处理,以上的判断逻辑其实也差不多。当浏览器进行访问首页的时候,判断是否为iOS设备,来获取到对应设备的xml的文件, 再返回对应的页面回去。上边的this._options
是在服务端初始化的时候就赋值好的,可以理解是个成员变量吧。
这里主要再来看看android端的dumpXMLAndScreenShot()
的处理,因为对iOS不对了解,所以暂时先分析android这边吧。
exports.dumpXMLAndScreenShot = function *() {
yield adb.shell(`touch ${ADB.ANDROID_TMP_DIR}/macaca-dump.xml`);
var result = yield uiautomator.sendCommand('/wd/hub/session/:sessionId/source', 'get', null);
var xml = result.value;
xml = xml.replace(/content-desc=\"\"/g, 'content-desc="null"');
const tempDir = path.join(__dirname, '..', '.temp');
_.mkdir(tempDir);
const xmlFilePath = path.join(tempDir, 'android.json');
const hierarchy = xml2map.tojson(xml).hierarchy;
logger.info(`Dump Android XML success, save to ${xmlFilePath}`);
//这个方法的主要作用就是给最外层的节点增杰一个bounds,以及将遍历里面所有的节点将bounds全部转换为数组类型
const adaptor = function(node) {
if (node.bounds) {
const bounds = node.bounds.match(/[\d\.]+/g);
// [ x, y, width, height]
node.bounds = [
~~bounds[0],
~~bounds[1],
bounds[2] - bounds[0],
bounds[3] - bounds[1],
];
} else {
var width = 0;
var height = 0;
node.node = node.node.length ? node.node : [node.node];
node.node.forEach(item => {
const bounds = item.bounds.match(/[\d\.]+/g);
if (bounds[2] - bounds[0] > width) {
width = bounds[2] - bounds[0];
}
if (bounds[3] - bounds[1] > height) {
height = bounds[3] - bounds[1];
}
});
node.class = 'MacacaAppInspectorRoot';
node.bounds = [0, 0, width, height];
}
if (node.node) {
node.nodes = node.node.length ? node.node : [node.node];
node.nodes.forEach(adaptor);
delete node.node;
}
return node;
};
var data = adaptor(hierarchy);
//将结果数据写入到android.json文件中
fs.writeFileSync(xmlFilePath, JSON.stringify(data), 'utf8');
//删除原先的图片,截屏并拉取到本地
const remoteFile = `${ADB.ANDROID_TMP_DIR}/screenshot.png`;
const cmd = `/system/bin/rm ${remoteFile}; /system/bin/screencap -p ${remoteFile}`;
yield adb.shell(cmd);
const localPath = path.join(tempDir, 'android-screenshot.png');
yield adb.pull(remoteFile, localPath);
};
这里的逻辑有点复杂,这里主要的作用就是将android端那边传送的XML进行解析,接着由浏览器进行渲染。
浏览器部分还没细看 ,下来重新写一篇。