logo

San DevTools 技术解析(下)

作者:九十九2021.01.11 23:24浏览量:444

简介:除Backend、Frontend、Message Channel和Debugging Protocol这些技术外,今天来讲下:DevTools 扩展开发和项目中其他比较有价值的技术点。

前言

我们已经连续分享了《San DevTools 技术解析》上篇与中篇,讲了其中四大模块:BackendFrontendMessage ChannelDebugging Protocol,基本完成了San DevTools整体架构与核心技术讲解,除这些技术外,我们还有哪些有意思的技术?今天来讲下:DevTools 扩展开发和项目中其他比较有价值的技术点。

DevTools 扩展开发

简介

概念:我们正在说的应该叫Chrome扩展(Chrome Extension),真正意义上的Chrome插件是浏览器底层的功能扩展,需要在了解浏览器底层技术的基础上利用 C++ 去开发。鉴于Chrome插件的叫法已经习惯,本文所说的插件也是Chrome扩展。

Chrome插件是一个用Web技术开发、用来增强浏览器功能的软件,它其实就是一个由HTML、CSS、JS、图片等资源组成的一个.crx后缀的压缩包。

DevTools 扩展

什么是DevTools扩展?它是为 Chrome DevTools 添加新功能的插件,功能上可新增加Panel UI面板和侧边栏,与被检查的页面交互,获取类似网络请求、Dom等信息。与其他扩展类似:有Backgroud、Content Script和其他项。DevTools 扩展都有一个 DevTools 页面,可以访问Chrome提供的 DevTools Api:
  • devtools.inspectedWindow:获取被审查窗口的有关信息

  • devtools.network:获取有关网络请求的信息

  • devtools.panels:面板相关
大家可以对比一下官方提供的DevTools扩展架构图与中篇中提供的图,结合两者,加深一下理解。

DevTools 扩展实例

实现扩展可通过简单的三步实现:

1.Manifest.json:指向HTML文件

   {
        "devtools_page": "devtools.html"
   }

2.HTML只引入JS文件

<!DOCTYPE html>
   <html>
       <head>
           <meta charset="utf-8" />
           <title>DevTools</title>
           <meta name="viewport" content="width=device-width,initial-scale=1" />
       </head>
       <body>
           <script src="/js/devtools.js"></script>
       </body>
   </html>

3.调用 API 创建自定义面板,同一个插件可以创建多个自定义面板

chrome.devtools.panels.create(
        'San',
        '/icons/logo128.png',
        'panel.html'
   );

关键技术:

  • 至少提供两个HTML,分别为devtools和panel

  • 入口:manifest 的 devtools_page

  • 核心API :chrome.devtools.panels.create

DevTools 扩展调试方式

因扩展需要多个页面与JS,调试时各模块方式也不同,这里做个汇总:

  1. Content Script 调试:F12或右键->检查,包括Inject Script

  2. Devtools Panel 调试:panel 面板里右键 -> 检查

  3. Backgroud 调试:chrome://extensions/ -> 背景页

总结:

  1. Backgroud 是常驻的,与浏览器生命周期相同

  2. Content Script 是页面级的,与页面生命周期相同

  3. DevTools 与调试工具生命周期相同

其他技术

Monorepo 项目管理

Monorepo 是管理项目代码的一种方式,指在一个项目仓库(repo)中管理多个模块/包(packages),不同于常见的每个模块建立一个repo。适合于大型前端项目的代码管理。San DevTools采用这种代码组织方式,通过yarn workspace管理依赖和运行项目。比传统git-submodule的多个repo管理上方便很多。

san-devtools
├── packages
│      ├─ shared
│      │    ├─ src
│      │    └─ package.json
│      └─ backend
│           ├─ src
│           └─ package.json
├── tsconfig.json             # 配置文件,对整个项目生效
├── .eslintrc                 # 配置文件,对整个项目生效
├── node_modules              # 整个项目只有一个外层 node_modules
└── package.json              # 包含整个项目所有依赖
// package.json
{
     ...
     "scripts": {    # workspace 彼此独立运行
        "start": "yarn workspace san-devtools start",
        "build:standalone": "yarn workspace san-devtools build",
        "start:extensions": "yarn workspace extensions start",
        "build:extensions": "yarn workspace extensions build"
    },
    "workspaces": [    # Yarn 命令使用的工作空间
        "packages/*"  
    ],
    "files": [
        "packages"    # 配置项目包含的文件名数组
    ],
    ...
}


模块解析

代码里经常可以看到如下的引入:

import Bridge from '@shared/Bridge';
import {install, DevToolsHook} from '@backend/hook';
@shared@backend的路径是如何解析的呢,只需要配置两个文件:
// tsconfig.json
{
     ...
  "paths": {
            "@backend/*": ["packages/backend/src/*"],
            "@frontend/*": ["packages/frontend/src/*"],
            "@shared/*": ["packages/shared/src/*"]
     }
     ...
}
// packages/build-tools/createConfig.js
const baseConfig = {
     ...
     resolve: {
        alias: {
            '@backend': resolve('backend/src/'),
            '@shared': resolve('shared/src/'),
            '@frontend': resolve('frontend/src/')
        }
    }
    ...
};


简易中间件服务器

San DevTools里我们简易实现一个中间件服务器。

class Server {
    constructor(options) {
        this._middlewares = [];

        // 添加中间件
        this.use(...);

        this.createServer();
    }

    createServer() {
        this._server = http.createServer((req, res) => {
            this._requestHandler(req, res, err => {
                ...
            });
        });
    }
    _requestHandler(req, res, errorHandler) {
        let idx = 0;
        const middlewares = this._middlewares;
        const firstHandler = middlewares[idx];

        run(firstHandler);

        function next() {
            idx++;
            if (idx < middlewares.length) {
                run(middlewares[idx]);
            }
        }

        function run(fn) {
            fn(req, res, next);
        }
    }
    use(fn) {
        this._middlewares.push(fn);
    }
};


消息合并

做移动端Hybird等通信方案的同学,可能知道消息发送过于频繁时,不能立即发送所有事件,需要合并消息后发送,降低发送频次。在San DevTools中,消息不是立即发送,先放入堆栈中,通过requestAnimationFrame自动根据系统的帧频来发送事件。简化后的示例代码如下:

const BATCH_DURATION = 100;

interface Wall {
    listen: (fn: Function) => void;
    send: (data: any) => void;
}

export default class Bridge extends EventEmitter {
    constructor(wall: Wall) {
        super();
        this.wall = wall;
        wall.listen((messages: Message | Message[]) => {
            this._emit(messages);
        });
    }

    /**
     * Send an event.
     *
     * @param {String} event
     * @param {*} payload
     */
    send(event: string, payload: any) {
        this._batchingQueue.push({
            event,
            payload
        });

        const now = Date.now();
        if (now - this._time > BATCH_DURATION) {
            this._flush();
        }
        else {
            this._timer = setTimeout(() => this._flush(), BATCH_DURATION);
        }
    }

    _emit(message: Message) {
        if (typeof message === 'string') {
            this.emit(message);
        }
        else if (message.chunk) {
            // chunk 模式时,合并数据
            this._receivingQueue.push(message.chunk);
            if (message.isLast) {
                this.emit(message.event, this._receivingQueue);
                this._receivingQueue.length = 0;
            }
        }
        else {
            this.emit(message.event, message.payload);
        }
    }

    _send(messages: Message | Message[]) {
        this._sendingQueue.push(messages);
        this._nextSend();
    }

    _nextSend() {
        // 如果没有消息或正在发送中,停止发送,等待 requestAnimationFrame
        if (!this._sendingQueue.length || this._sending) {
            return;
        }
        this._sending = true;
        const messages = this._sendingQueue.shift();

        // 真正发送
        this.wall.send(messages);

        this._sending = false;

        // 根据系统帧频调用发送事件
        requestAnimationFrame(() => this._nextSend());
    }
}

有三个点可以关注下:

  1. _emit方法接收数据时有个chunk模式,先把数据接收到_receivingQueue,当标志为最后一条数据时,把_receivingQueue整体emit出去;

  2. send方法里有个BATCH_DURATION的变量,控制事件的最小时间;

  3.  _nextSend中通过requestAnimationFrame根据系统帧频发送事件;

字体图标Iconfont

阿里的iconfont提供了非常丰富的字体图标,可以通过项目管理的方式,在平台管理一系列图标,并做为整体下载提供一个字体文件,包含了所有的图标。对于多人维护的项目,管理起来不是很方便,也不便于确认哪些图标没有使用。DevTools中我们通过一定的技巧,通过代码的方式维护所有的图标,使用起来非常方便。

    1. 首先实现icon.san基础字体组件,核心是通过type属性,渲染不同的字体图标   

<template>
      <svg
          xmlns="http://www.w3.org/2000/svg"
          class="{{['Icon', className]}}"
          width="24"
          height="24"
          viewBox="0 0 1024 1024">
          <template s-for="item in paths">
              <path fill="currentColor" d="{{item}}" />
          </template>
      </svg>
  </template>
  <script>
  import icons from './icon.ts';
  export default {
      initData() {
          return {
              className: '',
          }
      },
  
      computed: {
          paths() {
              let type = this.data.get('type');
              return icons[type] || null;
          }
      }
  
  }
</script>
  <style lang="less">
  .Icon {
      width: 1em;
      height: 1em;
      fill: currentColor;
  }
</style>


   2. 在 icons.ts 定义/维护全部的字体图标

  /* eslint-disable */
  const PATH_START_RECORD = ['M920.833 281.025a37 37 0 0 0-36.959-0.071L728.47 370.148V233.041c0-20.435-16.565-37-37-37H121.708c-20.435 0-37 16.565-37 37V790.96c0 20.435 16.565 37 37 37H691.47c20.435 0 37-16.565 37-37V653.865l155.406 89.182a36.975 36.975 0 0 0 18.416 4.909 37 37 0 0 0 37-37V313.044a36.998 36.998 0 0 0-18.459-32.019zM525.402 541.971L385.664 651.059a36.993 36.993 0 0 1-38.929 4.118 37 37 0 0 1-20.839-33.139l-0.857-219.66a37.002 37.002 0 0 1 59.873-29.228l140.597 110.572a37 37 0 0 1-0.107 58.249z m339.89 105.092L728.47 568.546V455.47l136.822-78.529v270.122z'];
  
  const ICONS = {
      'start-record': PATH_START_RECORD
  };
  
  export default ICONS;


   3. 最后使用时就非常简单了,指定type即可

<template>
      <san-custom-icon type="start-record"></san-custom-icon>
  </template>
  <script>
  import CustomIcon from '@components/icon.san';
  export default {
      components: {
          'san-custom-icon': CustomIcon,
      }
  }
</script>


最后

San生态全景,繁荣的生态建设,落地在Feed、搜索、小程序等百度核心主航道业务,已经成为百度大前端的基础设施。

参考资料

[1]Extending DevTools https://developer.chrome.com/docs/extensions/mv2/devtools/

[2]Chrome DevTools Protocol https://github.com/chromedevtools/devtools-protocol

[3]Chrome Browser Protocol https://github.com/ChromeDevTools/devtools-protocol/blob/master/json/browser_protocol.json

[4]Chrome Js Protocol https://github.com/ChromeDevTools/devtools-protocol/blob/master/json/js_protocol.json

[5]深入理解 Chrome DevTools https://zhaomenghuan.js.org/blog/chrome-devtools.html

[6]Chrome DevTools Frontend 运行原理浅析 https://zhaomenghuan.js.org/blog/chrome-devtools-frontend-analysis-of-principle.html

[7]Chrome DevTools 远程调试协议分析及实战 https://cloud.tencent.com/developer/article/1620907

[8]Chrome插件开发全攻略 https://www.cnblogs.com/liuxianan/p/chrome-plugin-develop.html

相关文章推荐

发表评论