-
Créez des extensions web pour Safari
Démarrez avec les extensions web Safari en créant et en testant une de A à Z, sans utiliser Xcode. Découvrez comment le blocage de contenu, la modification des pages, la messagerie native et le modèle Autorisations fonctionnent ensemble pour créer une expérience de navigation puissante et respectueuse de la vie privée sur toutes les plateformes.
Chapitres
- 0:00 - Introduction
- 3:23 - Pour commencer
- 7:23 - Bloquer du contenu
- 14:40 - Modifier des pages web
- 19:53 - Conditionner et distribuer
- 22:33 - Communiquer avec votre app
- 26:04 - Étapes suivantes
Ressources
- w3.org — W3C WebExtensions Community Group
- Packaging and distributing Safari Web Extensions with App Store Connect
- WebKit.org – Report issues to the WebKit open-source project
- Submit feedback
- MDN Web Docs - Web Extensions API
Vidéos connexes
WWDC26
-
Rechercher dans cette vidéo…
-
-
3:44 - Manifest file
{ "manifest_version": 3, "name": "Shiny OnTrack", "description": "Stay on track while you browse the web", "version": 1.0 } -
4:29 - Adding an extension icon
{ "manifest_version": 3, "name": "Shiny OnTrack", "description": "Stay on track while you browse the web", "version": 1.0, "icons": { "512": "images/icon.svg" } } -
5:30 - Adding an action button
{ "manifest_version": 3, "name": "Shiny OnTrack", "description": "Stay on track while you browse the web", "version": 1.0, "action": { "default_popup": "popup.html" } } -
6:17 - Adding custom UI to your extension
{ "manifest_version": 3, "name": "Shiny OnTrack", "description": "Stay on track while you browse the web", "version": 1.0, "options_ui": { "page": "options.html" } } -
6:30 - Including the UI in the extension manifest
{ "manifest_version": 3, "name": "Shiny OnTrack", "description": "Stay on track while you browse the web", "version": 1.0, "icons": { "512": "images/icon.svg" }, "options_ui": { "page": "options.html" } } -
6:40 - Hello World
<!DOCTYPE html> <html> <body> <p>Hello World</p> </body> </html> -
8:18 - Adding declarativeNetRequest permission
{ "manifest_version": 3, "name": "Shiny OnTrack", "description": "Stay on track while you browse the web", "version": 1.0, "icons": { "512": "images/icon.svg" }, "options_ui": { "page": "options.html" }, "permissions": [ "declarativeNetRequest" ] } -
8:22 - Blocking network requests
// block rule { id: 1, priority: 1, action: { type: "block" }, condition: { urlFilter: "||webkit.org", resourceTypes: [ "main_frame" ] } } -
8:41 - Modifying network requests
{ "manifest_version": 3, "name": "Shiny OnTrack", "description": "Stay on track while you browse the web", "version": 1.0, "icons": { "512": "images/icon.svg" }, "options_ui": { "page": "options.html" }, "permissions": [ "declarativeNetRequest" ], "declarativeNetRequest": { "rule_resources": [ { "id": "ruleset_id", "enabled": true, "path": "rules.json" } ] } } -
8:50 - Updating dynamic rules
await browser.declarativeNetRequest.updateDynamicRules({ addRules: [ rule ] }) -
9:19 - Wiring up the static declarativeNetRequest rules
{ "manifest_version": 3, "name": "Shiny OnTrack", "description": "Stay on track while you browse the web", "version": 1.0, "icons": { "512": "images/icon.svg" }, "options_ui": { "page": "options.html" }, "permissions": [ "declarativeNetRequest" ] } -
9:40 - Adding block rules dynamically
// A helper function to map the host to the declarative net request rule ID. export function hostToRuleID(host) { let hash = 0; for (let i = 0; i < host.length; i++) { hash = ((hash << 5) + hash) + host.charCodeAt(i); hash |= 0; } return Math.abs(hash) || 1; } function createBlockRule(host) { return { id: hostToRuleID(host), priority: 1, action: { type: "block" }, condition: { urlFilter: `||${host}`, resourceTypes: ["main_frame"] } } } export async function createRules(hosts) { try { await browser.declarativeNetRequest.updateDynamicRules({ addRules: hosts.map(createBlockRule) }) } catch { console.log("Failed to create declarative net request rules") } } -
10:10 - Handling adding hosts to the settings
import { createRules, removeAllRules, removeRule } from './rules.js' export async function addHost(host, blockingMode) { if (!host) return if (blockingMode === "full") await createRules([host]) } -
10:48 - Redirecting network requests
{ id: 1, priority: 1, action: { type: "redirect", redirect: { extensionPath: "/blocked.html" } }, condition: { urlFilter: "||webkit.org", resourceTypes: [ "main_frame" ] } } -
11:17 - Declaring optional host permissions
{ "manifest_version": 3, "name": "Shiny OnTrack", "description": "Stay on track while you browse the web", "version": 1.0, "icons": { "512": "images/icon.svg" }, "options_ui": { "page": "options.html" }, "permissions": [ "declarativeNetRequestWithHostAccess" ], "optional_host_permissions": [ "https://webkit.org/*" ] } -
11:54 - Declaring optional host permissions for all sites
{ "manifest_version": 3, "name": "Shiny OnTrack", "description": "Stay on track while you browse the web", "version": 1.0, "icons": { "512": "images/icon.svg" }, "options_ui": { "page": "options.html" }, "permissions": [ "declarativeNetRequestWithHostAccess" ], "optional_host_permissions": [ "*://*/*" ] } -
13:12 - Add the redirect rule
// A helper function to map the host to the declarative net request rule ID. export function hostToRuleID(host) { let hash = 0; for (let i = 0; i < host.length; i++) { hash = ((hash << 5) + hash) + host.charCodeAt(i); hash |= 0; } return Math.abs(hash) || 1; } function createBlockRule(host) { return { id: hostToRuleID(host), priority: 1, action: { type: "block" }, condition: { urlFilter: `||${host}`, resourceTypes: ["main_frame"] } } } function createRedirectRule(host) { return { id: hostToRuleID(host), priority: 1, action: { type: "redirect", redirect: { extensionPath: "/blocked.html" } }, condition: { urlFilter: `||${host}`, resourceTypes: ["main_frame"] } } } export async function createRules(hosts) { try { await browser.declarativeNetRequest.updateDynamicRules({ addRules: hosts.map(createRedirectRule) }) } catch { console.log("Failed to create declarative net request rules") } } -
13:42 - Dynamically ask for host permissions
import { createRules, removeAllRules, removeRule } from './rules.js' export async function addHost(host, blockingMode) { if (!host) return const granted = await browser.permissions.request({ origins: [`*://${host}/*`, `*://*.${host}/*`] }) if (!granted) return if (blockingMode === "full") await createRules([host]) } -
14:55 - Defining content scripts
{ "manifest_version": 3, "name": "Shiny OnTrack", "description": "Stay on track while you browse the web", "version": 1.0, "icons": { "512": "images/icon.svg" }, "options_ui": { "page": "options.html" }, "permissions": [ "declarativeNetRequestWithHostAccess" ], "optional_host_permissions": [ "*://*/*" ], "content_scripts": [ { "js": [ "content.js" ], "css": [ "content.css" ], "matches": [ "*://*.webkit.org/*" ] } ] } -
15:13 - Dynamically registering content scripts
let script = { id: "id", js: [ "content.js" ], css: [ "content.css" ], matches: [ "*://*.webkit.org/*" ], persistAcrossSessions: true } await browser.scripting.registerContentScripts([ script ]) -
15:31 - Adding the scripting permission
{ "manifest_version": 3, "name": "Shiny OnTrack", "description": "Stay on track while you browse the web", "version": 1.0, "icons": { "512": "images/icon.svg" }, "options_page": "options.html", "permissions": [ "declarativeNetRequestWithHostAccess", "scripting" ], "optional_host_permissions": [ "*://*/*" ] } -
15:41 - Registering content scripts
// scripting.js function contentScript(host) { return { id: `cs-${host}`, js: [ "content.js" ], css: [ "content.css" ], matches: [ `*://${host}/*`, `*://*.${host}/*` ], persistAcrossSessions: true } } export function registerScripts(hosts) { const scripts = hosts.map(contentScript) try { await browser.scripting.registerContentScripts(scripts) } catch { console.log("Failed to register content scripts") } } -
16:02 - Adding a host
// host.js export async function addHost(host, blockMode) { if (!host) return const granted = await browser.permissions.request({ origins: [`*://${host}/*`, `*://*.${host}/*`] }) if (!granted) return if (blockingMode === "full") await createRules([ host ]) await registerScripts([ host ]) } -
17:06 - Web extensions storage APIs
await browser.session.storage.set({ key: value }) await browser.local.storage.set({ key: value }) -
17:21 - Adding storage permission to the web extension manifest.json
{ "manifest_version": 3, "name": "Shiny OnTrack", "description": "Stay on track while you browse the web", "version": 1.0, "icons": { "512": "images/icon.svg" }, "options_page": "options.html", "permissions": [ "declarativeNetRequestWithHostAccess", "scripting", "storage" ], "optional_host_permissions": [ "*://*/*" ] } -
17:30 - Saving data with storage
// storage.js export async function updateHosts(hosts) { await browser.storage.local.set({ hosts: hosts }) } export async function getHosts() { const { hosts = [] } = await browser.storage.local.get("hosts") return hosts } export async function saveBlockMode(mode) { await browser.storage.local.set({ blockMode: mode }) } export async function getBlockMode() { const { blockMode = "full" } = await browser.storage.local.get("blockMode") return blockMode } -
17:41 - Persisting hosts to storage
// host.js export async function addHost(host, blockMode) { if (!host) return const granted = await browser.permissions.request({ origins: [`*://${host}/*`, `*://*.${host}/*`] }) if (!granted) return if (blockingMode === "full") await createRules([ host ]) await registerScripts([ host ]) let existingHosts = await getHosts() let updatedHosts = [ ...existingHosts, host ] await updateHosts(updatedHosts) } -
17:51 - Reading from storage
// options.js let existingHosts = await getHosts() let blockMode = await getBlockMode() displayBlocklist(existingHosts) -
18:00 - Switching block modes
// host.js export async function userDidSwitchMode(blockMode) { await saveBlockMode(blockMode) if (blockMode === "full") { let hosts = await getHosts() await createRules(hosts) } else await removeAllRules() } -
19:01 - Adding a background script
{ "manifest_version": 3, "name": "Shiny OnTrack", "description": "Stay on track while you browse the web", "version": 1.0, "icons": { "512": "images/icon.svg" }, "options_page": "options.html", "permissions": [ "declarativeNetRequestWithHostAccess", "scripting", "storage" ], "optional_host_permissions": [ "*://*/*" ], "background": { "scripts": [ "background.js" ], "type": "module" } } -
19:39 - Background script
// background.js import { registerScripts } from "./utilities/scripting.js" import { getHosts } from "./utilities/storage.js" browser.runtime.onInstalled.addListener(async (details) => { if (details.reason !== "update") return const hosts = await getHosts() await registerScripts(hosts) }) -
22:49 - Package your web extension into an app for Xcode
xcrun safari-web-extension-packager --copy-resources /path/to/ShinyOnTrack -
23:32 - Adding the nativeMessaging permission
{ "manifest_version": 3, "name": "Shiny OnTrack", "description": "Stay on track while you browse the web", "version": 1.0, "icons": { "512": "images/icon.svg" }, "options_page": "options.html", "permissions": [ "declarativeNetRequestWithHostAccess", "scripting", "storage", "nativeMessaging" ], "optional_host_permissions": [ "*://*/*" ], "background": { "scripts": [ "background.js" ], "type": "module" } } -
23:40 - Sending a native message
// background.js import { registerScripts } from "./utilities/scripting.js" import { getHosts } from "./utilities/storage.js" browser.runtime.onInstalled.addListener(async (details) => { if (details.reason !== "update") return const hosts = await getHosts() await registerScripts(hosts) }) export async function requestBioAuth() { const message = { message: "requestBioAuth" } const response = await browser.runtime.sendNativeMessage(message) return response?.success } -
23:55 - Handling native messages
// SafariWebExtensionHandler.swift import LocalAuthentication class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { func beginRequest(with context: NSExtensionContext) { let request = context.inputItems.first as? NSExtensionItem let message = request?.userInfo?[SFExtensionMessageKey] as? [String: Any] if message?["message"] as? String == "requestBioAuth" { let lAContext = LAContext() Task { do { let success = try await lAContext.evaluatePolicy( .deviceOwnerAuthenticationWithBiometrics, localizedReason: "Authenticate to change blocked sites" ) self.reply(context: context, success: success) } catch { self.reply(context: context, success: false) } } } } } -
24:25 - Replying to a native message
// SafariWebExtensionHandler.swift import LocalAuthentication class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { func beginRequest(with context: NSExtensionContext) { let request = context.inputItems.first as? NSExtensionItem let message = request?.userInfo?[SFExtensionMessageKey] as? [String: Any] if message?["message"] as? String == "requestBioAuth" { let lAContext = LAContext() Task { do { let success = try await lAContext.evaluatePolicy( .deviceOwnerAuthenticationWithBiometrics, localizedReason: "Authenticate to change blocked sites" ) self.reply(context: context, success: success) } catch { self.reply(context: context, success: false) } } } } private func reply(context: NSExtensionContext, success: Bool) { let response = NSExtensionItem() response.userInfo = [SFExtensionMessageKey: ["success": success]] context.completeRequest(returningItems: [response], completionHandler: nil) } }
-