electron中重写js 内置函数 需要重新内核编译

一,electron用typescript实现了函数。比如重写了 windows.history.back go等js 内置函数。不走blink的js binding。

 D:\dev\electron7\src\electron\lib\renderer\window-setup.ts

window.open

  if (!usesNativeWindowOpen) {
    // TODO(MarshallOfSound): Make compatible with ctx isolation without hole-punch
    // Make the browser window or guest view emit "new-window" event.
    (window as any).open = function (url?: string, frameName?: string, features?: string) {
      if (url != null && url !== '') {
        url = resolveURL(url, location.href);
      }
      const guestId = ipcRendererInternal.sendSync('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_OPEN', url, toString(frameName), toString(features));
      if (guestId != null) {
        return getOrCreateProxy(guestId);
      } else {
        return null;
      }
    };
    if (shouldUseContextBridge) internalContextBridge.overrideGlobalValueWithDynamicPropsFromIsolatedWorld(['open'], window.open);
  }

history接口实现

    window.history.forward = function () {
      ipcRendererInternal.send('ELECTRON_NAVIGATION_CONTROLLER_GO_FORWARD');
    };
    if (shouldUseContextBridge) internalContextBridge.overrideGlobalValueFromIsolatedWorld(['history', 'forward'], window.history.forward);

    window.history.go = function (offset: number) {
      ipcRendererInternal.send('ELECTRON_NAVIGATION_CONTROLLER_GO_TO_OFFSET', +offset);
    };
    if (shouldUseContextBridge) internalContextBridge.overrideGlobalValueFromIsolatedWorld(['history', 'go'], window.history.go);

    const getHistoryLength = () => ipcRendererInternal.sendSync('ELECTRON_NAVIGATION_CONTROLLER_LENGTH');
    Object.defineProperty(window.history, 'length', {
      get: getHistoryLength,
      set () {}
    });

 

在browser进程中接收到back事件,真正做back的又传回给了chromium内核,通过web contents的loadURL接口。

D:\dev\electron7\src\electron\lib\browser\navigation-controller.js

'use strict';

const { ipcMainInternal } = require('@electron/internal/browser/ipc-main-internal');

// The history operation in renderer is redirected to browser.
ipcMainInternal.on('ELECTRON_NAVIGATION_CONTROLLER_GO_BACK', function (event) {
  event.sender.goBack();
});
event.sender是谁:D:\dev\electron7\src\electron\lib\browser\navigation-controller.js
  NavigationController.prototype.goBack = function () {
    if (!this.canGoBack()) {
      return;
    }
    this.pendingIndex = this.getActiveIndex() - 1;
    if (this.inPageIndex > -1 && this.pendingIndex >= this.inPageIndex) {
      return this.webContents._goBack();
    } else {
      return this.webContents._loadURL(this.history[this.pendingIndex], {}); //调用electron的绑定的webContents
    }
  };

声明:

export const syncMethods = new Set([
  'getURL',
  'getTitle',
  'isLoading',
  'isLoadingMainFrame',
  'isWaitingForResponse',
  'stop',
  'reload',
  'reloadIgnoringCache',
  'canGoBack',
  'canGoForward',
  'canGoToOffset',
  'clearHistory',
  'goBack',

 

_loadURL的绑定定义在:D:\dev\electron7\src\electron\shell\browser\api\atom_api_web_contents.cc

// static
void WebContents::BuildPrototype(v8::Isolate* isolate,
                                 v8::Local<v8::FunctionTemplate> prototype) {
  prototype->SetClassName(mate::StringToV8(isolate, "WebContents"));
  mate::ObjectTemplateBuilder(isolate, prototype->PrototypeTemplate())
      .MakeDestroyable()
      .SetMethod("setBackgroundThrottling",
                 &WebContents::SetBackgroundThrottling)
      .SetMethod("getProcessId", &WebContents::GetProcessID)
      .SetMethod("getOSProcessId", &WebContents::GetOSProcessID)
      .SetMethod("_getOSProcessIdForFrame",
                 &WebContents::GetOSProcessIdForFrame)
      .SetMethod("equal", &WebContents::Equal)
      .SetMethod("_loadURL", &WebContents::LoadURL)

 这里在同文件中又实现:

void WebContents::LoadURL(const GURL& url, const mate::Dictionary& options) {
  if (!url.is_valid() || url.spec().size() > url::kMaxURLChars) {
    Emit("did-fail-load", static_cast<int>(net::ERR_INVALID_URL),
         net::ErrorToShortString(net::ERR_INVALID_URL),
         url.possibly_invalid_spec(), true);
    return;
  }

  content::NavigationController::LoadURLParams params(url);

  if (!options.Get("httpReferrer", &params.referrer)) {
    GURL http_referrer;
    if (options.Get("httpReferrer", &http_referrer))
      params.referrer =
          content::Referrer(http_referrer.GetAsReferrer(),
                            network::mojom::ReferrerPolicy::kDefault);
  }

  std::string user_agent;
  if (options.Get("userAgent", &user_agent))
    web_contents()->SetUserAgentOverride(user_agent, false);

  std::string extra_headers;
  if (options.Get("extraHeaders", &extra_headers))
    params.extra_headers = extra_headers;

  scoped_refptr<network::ResourceRequestBody> body;
  if (options.Get("postData", &body)) {
    params.post_data = body;
    params.load_type = content::NavigationController::LOAD_TYPE_HTTP_POST;
  }

  GURL base_url_for_data_url;
  if (options.Get("baseURLForDataURL", &base_url_for_data_url)) {
    params.base_url_for_data_url = base_url_for_data_url;
    params.load_type = content::NavigationController::LOAD_TYPE_DATA;
  }

  bool reload_ignoring_cache = false;
  if (options.Get("reloadIgnoringCache", &reload_ignoring_cache) &&
      reload_ignoring_cache) {
    params.reload_type = content::ReloadType::BYPASSING_CACHE;
  }

  params.transition_type = ui::PAGE_TRANSITION_TYPED;
  params.should_clear_history_list = true;
  params.override_user_agent = content::NavigationController::UA_OVERRIDE_TRUE;
  // Discord non-committed entries to ensure that we don't re-use a pending
  // entry
  web_contents()->GetController().DiscardNonCommittedEntries();
  web_contents()->GetController().LoadURLWithParams(params);//这里到跑了下面位置:
//D:\dev\electron7\src\content\browser\frame_host\navigation_controller_impl.cc
// void NavigationControllerImpl::LoadURLWithParams(const LoadURLParams& params)
// Set the background color of RenderWidgetHostView. // We have to call it right after LoadURL because the RenderViewHost is only // created after loading a page. auto* const view = web_contents()->GetRenderWidgetHostView(); if (view) { auto* web_preferences = WebContentsPreferences::From(web_contents()); std::string color_name; if (web_preferences->GetPreference(options::kBackgroundColor, &color_name)) { view->SetBackgroundColor(ParseHexColor(color_name)); } else { view->SetBackgroundColor(SK_ColorTRANSPARENT); } } }

跑到了内核的

web_contents()->GetController().LoadURLWithParams(params);
 

二、window.open的测试代码。其中调用了内部ipc,用js找到了ipcRenderer通道。spec里面是测试代码。

w.webContents.executeJavaScript

D:\dev\electron7\src\electron\spec-main\chromium-spec.ts

 describe('window.open', () => {
    it('denies custom open when nativeWindowOpen: true', async () => {
      const w = new BrowserWindow({
        show: false,
        webPreferences: {
          contextIsolation: false,
          nodeIntegration: true,
          nativeWindowOpen: true
        }
      });
      w.loadURL('about:blank');

      const previousListeners = process.listeners('uncaughtException');
      process.removeAllListeners('uncaughtException');
      try {
        const uncaughtException = new Promise<Error>(resolve => {
          process.once('uncaughtException', resolve);
        });
        expect(await w.webContents.executeJavaScript(`(${function () {
          const ipc = process.electronBinding('ipc').ipc;
          return ipc.sendSync(true, 'ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_OPEN', ['', '', ''])[0];
        }})()`)).to.be.null('null');
        const exception = await uncaughtException;
        expect(exception.message).to.match(/denied: expected native window\.open/);
      } finally {
        previousListeners.forEach(l => process.on('uncaughtException', l));
      }
    });
  });
})

 

三、electron定义自定义的接口:D:\dev\electron7\src\electron\electron.d.ts 如IpcRenderer, ipcMain

  interface IpcRenderer extends NodeJS.EventEmitter {

    // Docs: http://electronjs.org/docs\api\ipc-renderer

    /**
     * Resolves with the response from the main process.
     *
     * Send a message to the main process asynchronously via `channel` and expect an
     * asynchronous result. Arguments will be serialized as JSON internally and hence
     * no functions or prototype chain will be included.
     *
     * The main process should listen for `channel` with `ipcMain.handle()`.
     * 
For example:
     */
    invoke(channel: string, ...args: any[]): Promise<any>;
    /**
     * Listens to `channel`, when a new message arrives `listener` would be called with
     * `listener(event, args...)`.
     */
    on(channel: string, listener: (event: IpcRendererEvent, ...args: any[]) => void): this;
    /**
     * Adds a one time `listener` function for the event. This `listener` is invoked
     * only the next time a message is sent to `channel`, after which it is removed.
     */
    once(channel: string, listener: (event: IpcRendererEvent, ...args: any[]) => void): this;
    /**
     * Removes all listeners, or those of the specified `channel`.
     */
    removeAllListeners(channel: string): this;
    /**
     * Removes the specified `listener` from the listener array for the specified
     * `channel`.
     */
    removeListener(channel: string, listener: (...args: any[]) => void): this;
    /**
     * Send a message to the main process asynchronously via `channel`, you can also
     * send arbitrary arguments. Arguments will be serialized as JSON internally and
     * hence no functions or prototype chain will be included.
     *
     * The main process handles it by listening for `channel` with the `ipcMain`
     * module.
     */
    send(channel: string, ...args: any[]): void;
    /**
     * The value sent back by the `ipcMain` handler.
     *
     * Send a message to the main process synchronously via `channel`, you can also
     * send arbitrary arguments. Arguments will be serialized in JSON internally and
     * hence no functions or prototype chain will be included.
     *
     * The main process handles it by listening for `channel` with `ipcMain` module,
     * and replies by setting `event.returnValue`.
     *
     * **Note:** Sending a synchronous message will block the whole renderer process,
     * unless you know what you are doing you should never use it.
     */
    sendSync(channel: string, ...args: any[]): any;
    sendSyncEx(channel: string, ...args: any[]): any;
    /**
     * Sends a message to a window with `webContentsId` via `channel`.
     */
    sendTo(webContentsId: number, channel: string, ...args: any[]): void;
    /**
     * Like `ipcRenderer.send` but the event will be sent to the `<webview>` element in
     * the host page instead of the main process.
     */
    sendToHost(channel: string, ...args: any[]): void;
  }
View Code

 这个electron.d.ts声明是自动生成的。electron会根据json格式的说明文档,自己分析生成。

比如ipcRenderer的文档在:D:\dev\electron7\src\electron\docs\api\ipc-renderer.md,在文档中添加新的方法声明:

### `ipcRenderer.sendSyncEx(channel, ...args)`

* `channel` String
* `...args` any[]

Returns `any` - The value sent back by the [`ipcMain`](ipc-main.md) handler.

Send a message to the main process synchronously via `channel`, you can also
send arbitrary arguments. Arguments will be serialized in JSON internally and
hence no functions or prototype chain will be included.

The main process handles it by listening for `channel` with [`ipcMain`](ipc-main.md) module,
and replies by setting `event.returnValue`.

**Note:** Sending a synchronous message will block the whole renderer process,
unless you know what you are doing you should never use it.

electron借助doc parser(https://github.com/electron/docs-parser),生成,filenames.auto.gni ,electron-api.json。

借助构建脚本 electron/build.gn

# We geneate the definitions twice here, once in //electron/electron.d.ts
# and once in $target_gen_dir
# The one in $target_gen_dir is used for the actual TSC build later one
# and the one in //electron/electron.d.ts is used by your IDE (vscode)
# for typescript prompting
npm_action("build_electron_definitions") {
  script = "gn-typescript-definitions"
  args = [ rebase_path("$target_gen_dir/tsc/typings/electron.d.ts") ]
  inputs = auto_filenames.api_docs + [ "yarn.lock" ]

  outputs = [
    "$target_gen_dir/tsc/typings/electron.d.ts",
  ]
}

根据输入文件 inputs = auto_filenames.api_docs

它找的是filename.auto.gni 里面的定义的模块,最终生成

electron.d.ts

https://github.com/electron/typescript-definitions

四、ipcRendererInternal 是electron内部的消息传递,它和对外提供的 其实用的一个ipc通道,通过第一个参数internal的boolean值控制是否发往外部。

D:\dev\electron7\src\electron\lib\renderer\ipc-renderer-internal.ts

const binding = process.electronBinding('ipc');
const v8Util = process.electronBinding('v8_util');

// Created by init.js.
export const ipcRendererInternal: Electron.IpcRendererInternal = v8Util.getHiddenValue(global, 'ipc-internal');
const internal = true;

if (!ipcRendererInternal.send) {
  ipcRendererInternal.send = function (channel, ...args) {
    return binding.ipc.send(internal, channel, args);
  };

  ipcRendererInternal.sendSync = function (channel, ...args) {
    return binding.ipc.sendSync(internal, channel, args)[0];
  };

  ipcRendererInternal.sendSyncEx = function (channel, ...args) {
    return binding.ipc.sendSync(false, channel, args)[0];
  };

  ipcRendererInternal.sendTo = function (webContentsId, channel, ...args) {
    return binding.ipc.sendTo(internal, false, webContentsId, channel, args);
  };

  ipcRendererInternal.sendToAll = function (webContentsId, channel, ...args) {
    return binding.ipc.sendTo(internal, true, webContentsId, channel, args);
  };
}

供外部的ipc调用的实现

D:\dev\electron7\src\electron\lib\renderer\api\ipc-renderer.js

'use strict';

const { ipc } = process.electronBinding('ipc');
const v8Util = process.electronBinding('v8_util');

// Created by init.js.
const ipcRenderer = v8Util.getHiddenValue(global, 'ipc');//都来源于ipc
const internal = false; //发送时的标志,不是发往内部

if (!ipcRenderer.send) {
  ipcRenderer.send = function (channel, ...args) {
    return ipc.send(internal, channel, args);
  };

  ipcRenderer.sendSync = function (channel, ...args) {
    const result = ipc.sendSync(internal, channel, args);
    if (!Array.isArray(result) || result.length !== 1) {
      throw new Error(`Unexpected return value from ipcRenderer.sendSync: ${result}`);
    }
    return result[0];
  };

  ipcRenderer.sendToHost = function (channel, ...args) {
    return ipc.sendToHost(channel, args);
  };

  ipcRenderer.sendTo = function (webContentsId, channel, ...args) {
    return ipc.sendTo(internal, false, webContentsId, channel, args);
  };

  ipcRenderer.sendToAll = function (webContentsId, channel, ...args) {
    return ipc.sendTo(internal, true, webContentsId, channel, args);
  };

  ipcRenderer.invoke = function (channel, ...args) {
    return ipc.invoke(channel, args).then(({ error, result }) => {
      if (error) { throw new Error(`Error invoking remote method '${channel}': ${error}`); }
      return result;
    });
  };
}

module.exports = ipcRenderer; //可以看到倒出给了外部。

 

 

electron/internal/browser/ipc-main-internal

 

const webContentsMethods = new Set([
  'getURL',
  'loadURL',
  'executeJavaScript',
  'print'
]);

 ipc的c++实现:D:\dev\electron7\src\electron\shell\renderer\api\atom_api_renderer_ipc.cc

  static void BuildPrototype(v8::Isolate* isolate,
                             v8::Local<v8::FunctionTemplate> prototype) {
    prototype->SetClassName(mate::StringToV8(isolate, "IPCRenderer"));
    mate::ObjectTemplateBuilder(isolate, prototype->PrototypeTemplate())
        .SetMethod("send", &IPCRenderer::Send)
        .SetMethod("sendSync", &IPCRenderer::SendSync)
        .SetMethod("sendTo", &IPCRenderer::SendTo)
        .SetMethod("sendToHost", &IPCRenderer::SendToHost)
        .SetMethod("invoke", &IPCRenderer::Invoke);
  }

......

base::Value SendSync(bool internal,
                       const std::string& channel,
                       base::ListValue arguments) {
    base::ListValue result;

    electron_browser_ptr_->MessageSync(internal, channel, std::move(arguments),
                                       &result);

    if (!result.is_list() || result.GetList().empty())
      return base::Value{};

    return std::move(result.GetList().at(0));
  }

 

posted @ 2020-08-07 15:38  Bigben  阅读(1321)  评论(0编辑  收藏  举报