moztw.org

如何在 Firefox OS 上建立瀏覽器 APP

Firefox OS 上的瀏覽器 APP 利用 Gecko 提供的 Browser API 來管理網頁瀏覽,並利用 HTML5 技術提供使用者界面。根據不同的實作它可以實現分頁,瀏覽歷史,書籤… 等等功能。這篇文章示範如何建立一個可以在 Firefox OS 裝置上使用的瀏覽器 APP,它包含了最基本的網誌列跟上一頁/下一頁功能。

文章範例的原始碼可以在這裡下載:

https://github.com/begeeben/firefox-os-browser-sample

基本工具: WebIDE

Firefox 34 以及之後的版本都包含了 WebIDE。開發瀏覽器 APP 的並不一定需要一台 Firefox OS 手機,我們可以在 WebIDE 上用 Firefox OS simulator 來執行這個 APP。使用 WebIDE 可以很容易的從範本建立一個 APP,並做 HTML/CSS/JS 編輯。要開啟 WebIDE,從上面的標題列選 Tools > Web Developer > WebIDE :

open webide

使用範本建立 APP:

首先,從 WebIDE 的空白範本建立一個 APP。Privileged APP 才能有 browser 權限使用 Browser API,它提供了一些在 Firefox OS 上才能用的 method 和 event 來管理
iframe。

new priviledged app

建好的 APP 看起來像下面這張圖,接下來我們要開始寫 code 做瀏覽器 APP 需要的功能。

mybrowser empty

設定 manifest.webapp:

從範本建立好的 APP 已經在 manifest.webapp 裡設定 
type  為
privileged:

"type": "privileged",

1

"type": "privileged",

要使用 Browser API 我們需要加上 
browser 權限:

"permissions": {
"browser": {}
},

123

"permissions": { "browser": {} },

HTML 結構:

這個瀏覽器 APP 上面會有一條工具列,畫面中央會拿來放用來瀏覽網頁用的
iframe,之後將用 javascript 來產生這個
iframe。這個 APP 剛開啟的時候預設顯示 “Hello myBrowser!"。

<!-- browser toolbar -->
<div id="toolbar">
<button id="goback-button" type="button" disabled><</button>
<button id="goforward-button" type="button" disabled>></button>
<!-- url bar displays page url or page title -->
<form id="url-bar">
<input id="url-input" type="text"></input>
</form>
<button id="url-button" form="url-bar">GO</button>
</div>
<!-- the container for the browser frame -->
<div id="frame-container" class="empty">
<div id="empty-frame">
<h1>Hello myBrowser!</h1>
</div>
</div>

12345678910111213141516

<!-- browser toolbar --><div id="toolbar"> <button id="goback-button" type="button" disabled><</button> <button id="goforward-button" type="button" disabled>></button> <!-- url bar displays page url or page title --> <form id="url-bar"> <input id="url-input" type="text"></input> </form> <button id="url-button" form="url-bar">GO</button></div><!-- the container for the browser frame --><div id="frame-container" class="empty"> <div id="empty-frame"> <h1>Hello myBrowser!</h1> </div></div>

預設看起來長這樣:

browser startup

管理瀏覽網頁的 iframe:

要提供 iframe 裡面網頁的資訊到使用者界面上,需要接收處理 
mozbrowser event。在 
iframe 上加 
mozbrowser 參數,就可以在這 
iframe 上使用 Browser API。在這裡為了要把 UI 和 mozbrowser 的處理邏輯分開,我們建立一個 tab.js,這樣也預留了未來可以做多分頁瀏覽的彈性。

/**

  • Returns an iframe which runs in a child process with Browser API enabled
  • and fullscreen is allowed

*

  • @param {String} [url] Optional URL
  • @return {iframe} An OOP mozbrowser iframe

*/
function createIFrame (url) {
var iframe = document.createElement('iframe');
iframe.setAttribute('mozbrowser', true);
iframe.setAttribute('mozallowfullscreen', true);
iframe.setAttribute('remote', true);

if (url) {
iframe.src = url;
}

return iframe;
}

12345678910111213141516171819

/* Returns an iframe which runs in a child process with Browser API enabled and fullscreen is allowed @param {String} [url] Optional URL @return {iframe} An OOP mozbrowser iframe */function createIFrame (url) { var iframe = document.createElement('iframe'); iframe.setAttribute('mozbrowser', true); iframe.setAttribute('mozallowfullscreen', true); iframe.setAttribute('remote', true); if (url) { iframe.src = url; } return iframe;}

在 
iframe 加上 
mozallowfullscreen 參數可以允許網頁使用全螢幕。在網頁裡面在要顯示全螢幕的 element 上呼叫
Element.mozRequestFullscreen 。

加了 
remote 參數讓 
iframe 跑在另一個獨立的 child process 上。這是為了避免惡意的網站直接存取瀏覽器 APP 本身。(目前的 Firefox OS 版本還沒有允許 Nested OOP,因此這個參數在我們的範例還沒有作用,需要等 Bug 1020135 – (nested-oop) [meta] Allow nested oop <iframe mozbrowser>)

Tab object 在建構的時候會建立一個
iframe ,並且開始聽這個 
iframe 上的 
mozbrowser event。

/**

  • The browser tab constructor.

*

  • Creates an iframe and attaches mozbrowser events for web browsing.

*

  • Implements EventListener Interface.

*

  • @param {String} url An optional plaintext URL

*/
function Tab (url) {
this.iframe = createIFrame(url);
this.title = null;
this.url = url;

this.iframe.addEventListener('mozbrowserloadstart', this);
this.iframe.addEventListener('mozbrowserlocationchange', this);
this.iframe.addEventListener('mozbrowsertitlechange', this);
this.iframe.addEventListener('mozbrowserloadend', this);
this.iframe.addEventListener('mozbrowsererror', this);
};

1234567891011121314151617181920

/* The browser tab constructor. Creates an iframe and attaches mozbrowser events for web browsing. Implements EventListener Interface. @param {String} url An optional plaintext URL */function Tab (url) { this.iframe = createIFrame(url); this.title = null; this.url = url; this.iframe.addEventListener('mozbrowserloadstart', this); this.iframe.addEventListener('mozbrowserlocationchange', this); this.iframe.addEventListener('mozbrowsertitlechange', this); this.iframe.addEventListener('mozbrowserloadend', this); this.iframe.addEventListener('mozbrowsererror', this);};

除了這五個 event 以外,這個簡單的 APP 還有很多 
mozbrowser event 沒用到。這些 event 提供很多 
iframe 有用的資訊,比如說
title ,URL,讀取進度,context menu 等等。

例如,我們可以用 
mozbrowsertitlechange event 來收到 
iframe 裡面網頁的
title 。在 tab.js 裡面當收到 
title 的時候,發出一個 
CustomEvent  ‘
tab:titlechange’ 通知其他組件更新頁面的 title。

Tab.prototype.mozbrowsertitlechange = function _mozbrowsertitlechange (e) {
if (e.detail) {
this.title = e.detail;
}

var event = new CustomEvent('tab:titlechange', { detail: this });
window.dispatchEvent(event);
};

12345678

Tab.prototype.mozbrowsertitlechange = function _mozbrowsertitlechange (e) { if (e.detail) { this.title = e.detail; } var event = new CustomEvent('tab:titlechange', { detail: this }); window.dispatchEvent(event);};

這裡示範在 ‘
tab:titlechange ‘ event 的時候更新網頁
title。如果要做分頁總覽功能,也要用一樣的方式更新各個頁面的
title。

/**

  • Display the title of the currentTab on titlechange event.

*/
window.addEventListener('tab:titlechange', function (e) {
if (currentTab === e.detail) {
urlInput.value = currentTab.title;
}
});

12345678

/* Display the title of the currentTab on titlechange event. */window.addEventListener('tab:titlechange', function (e) { if (currentTab === e.detail) { urlInput.value = currentTab.title; }});

在使用者送出 input 的時候瀏覽網頁:

在網址列裡面直接打搜尋條件很普遍而且很方便。當使用者送出 URL 的時候,先判斷這是不是一個有效的 URL,如果不是,就當做搜尋條件,使用搜尋引擎 URI 來搜尋。

/**

  • The default search engine URI

*

  • @type {String}

*/
var searchEngineUri = 'https://search.yahoo.com/search?p={searchTerms}';

/**

  • Using an input element to check the validity of the input URL. If the input
  • is not valid, returns a search URL.

*

  • @param {String} input A plaintext URL or search terms
  • @param {String} searchEngineUri The search engine to be used
  • @return {String} A valid URL

*/
function getUrlFromInput(input, searchEngineUri) {
var urlValidate = document.createElement('input');
urlValidate.setAttribute('type', 'url');
urlValidate.setAttribute('value', input);

if (!urlValidate.validity.valid) {
var uri = searchEngineUri.replace('{searchTerms}', input);
return uri;
}

return input;
}

123456789101112131415161718192021222324252627

/* The default search engine URI @type {String} /var searchEngineUri = 'https://search.yahoo.com/search?p={searchTerms}'; / Using an input element to check the validity of the input URL. If the input is not valid, returns a search URL. @param {String} input A plaintext URL or search terms @param {String} searchEngineUri The search engine to be used @return {String} A valid URL /function getUrlFromInput(input, searchEngineUri) { var urlValidate = document.createElement('input'); urlValidate.setAttribute('type', 'url'); urlValidate.setAttribute('value', input); if (!urlValidate.validity.valid) { var uri = searchEngineUri.replace('{searchTerms}', input); return uri; } return input;}

接下來確認有沒有 Tab object,沒有的話就新建一個,把 URL 傳給 Tab 並設定成 
iframe 的
src 。

/**

  • Check the input and browse the address with a Tab object on url submit.

*/
window.addEventListener('submit', function (e) {
e.preventDefault();

if (!currentUrlInput.trim()) {
return;
}

if (frameContainer.classList.contains('empty')) {
frameContainer.classList.remove('empty');
}

var url = getUrlFromInput(currentUrlInput.trim(), searchEngineUri);

if (!currentTab) {
currentTab = new Tab(url);
frameContainer.appendChild(currentTab.iframe);
} else if (currentUrlInput === currentTab.title) {
currentTab.reload();
} else {
currentTab.goToUrl(url);
}
});

12345678910111213141516171819202122232425

/* Check the input and browse the address with a Tab object on url submit. */window.addEventListener('submit', function (e) { e.preventDefault(); if (!currentUrlInput.trim()) { return; } if (frameContainer.classList.contains('empty')) { frameContainer.classList.remove('empty'); } var url = getUrlFromInput(currentUrlInput.trim(), searchEngineUri); if (!currentTab) { currentTab = new Tab(url); frameContainer.appendChild(currentTab.iframe); } else if (currentUrlInput === currentTab.title) { currentTab.reload(); } else { currentTab.goToUrl(url); }});

currentTab.reload 裡面用了
iframe.reload 這個 Browser API 來重新讀取頁面。

/**

  • Reload the current page.

*/
Tab.prototype.reload = function _reload () {
this.iframe.reload();
};

123456

/* Reload the current page. */Tab.prototype.reload = function _reload () { this.iframe.reload();};

開啟或關閉上一頁或下一頁按鈕:

最後要用 Browser API 檢查網頁能不能回上一頁或到下一頁。
iframe.getCanGoBack 會回傳一個
DOMRequest,在 
onsuccess callback 裡面我們可以知道檢查的結果。這邊用 Promise 包起來讓他比較方便存取。

/**

  • Check if the iframe can go backward in the navigation history.

*

  • @return {Promise} Resolve with true if it can go backward.

*/
Tab.prototype.getCanGoBack = function _getCanGoBack () {
var self = this;

return new Promise(function (resolve, reject) {
var request = self.iframe.getCanGoBack();

request.onsuccess = function () {
if (this.result) {
resolve(true);
} else {
resolve(false);
}
};
});
};

1234567891011121314151617181920

/* Check if the iframe can go backward in the navigation history. @return {Promise} Resolve with true if it can go backward. */Tab.prototype.getCanGoBack = function _getCanGoBack () { var self = this; return new Promise(function (resolve, reject) { var request = self.iframe.getCanGoBack(); request.onsuccess = function () { if (this.result) { resolve(true); } else { resolve(false); } }; });};

在網頁讀取完以後檢查能不能回上一頁,也就是當 
mozbrowserloadend event 發生的時候。

/**

  • Enable/disable goback and goforward buttons accordingly when the
  • currentTab is loaded.

*/
window.addEventListener('tab:loadend', function (e) {
if (currentTab === e.detail) {
currentTab.getCanGoBack().then(function(canGoBack) {
gobackButton.disabled = !canGoBack;
});

currentTab.getCanGoForward().then(function(canGoForward) {
goforwardButton.disabled = !canGoForward;
});
}
});

123456789101112131415

/* Enable/disable goback and goforward buttons accordingly when the currentTab is loaded. /window.addEventListener('tab:loadend', function (e) { if (currentTab === e.detail) { currentTab.getCanGoBack().then(function(canGoBack) { gobackButton.disabled = !canGoBack; }); currentTab.getCanGoForward().then(function(canGoForward) { goforwardButton.disabled = !canGoForward; }); }});

執行 APP:

手邊可能沒有 Firefox OS 手機能用,不過你還是可以在 WebIDE 裡面用 Firefox OS simulator 來跑 APP:

Firefox_WebIDE_installing_simulator

按右上方的 Select Runtime 選一個版本:

Firefox_WebIDE_choose_simulator

當 Firefox OS 跑起來後,按上面中間的那些按鈕,你就可以執行和 debug 這個 APP。現在你有了一個新的 Firefox OS 瀏覽器 APP!

mybrowser finished

結語:

要讓這個簡單的瀏覽器 APP 好用還缺很多功能,比如說書籤,設定,搜尋建議,分享… 等。你將會需要 HTML storage 和 
mozActivity 來實現這些功能。歡迎實現你的點子,然後分享到 Firefox OS 的 Marketplace 上!

參考閱讀:

https://hacks.mozilla.org/2014/08/building-the-firefox-browser-for-firefox-os/

https://developer.mozilla.org/en-US/docs/Web/API/Using_the_Browser_API