Add blog post about extensions and shadow dom
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
parent
7d93c9d88b
commit
6c50ff1b84
28
.vscode/tasks.json
vendored
Normal file
28
.vscode/tasks.json
vendored
Normal file
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "dev",
|
||||
"problemMatcher": [],
|
||||
"label": "npm: dev",
|
||||
"detail": "vite dev"
|
||||
},
|
||||
{
|
||||
"type": "shell",
|
||||
"command": "npm",
|
||||
"args": ["run", "new-post", "${input:post-name}"],
|
||||
"problemMatcher": [],
|
||||
"label": "npm: new post",
|
||||
"detail": "new post"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "promptString",
|
||||
"id": "post-name",
|
||||
"description": "Post name",
|
||||
"default": "Incididunt aliqua adipisicing ipsum"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -12,7 +12,8 @@
|
|||
"format": "prettier --write .",
|
||||
"pagefind": "pagefind",
|
||||
"preview:pagefind": "pagefind --site .svelte-kit/output/prerendered/pages/",
|
||||
"build:pagefind": "pagefind --site build/"
|
||||
"build:pagefind": "pagefind --site build/",
|
||||
"new-post": "node scripts/new-post.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/kit": "^1.27.6",
|
||||
|
|
23
scripts/new-post.js
Normal file
23
scripts/new-post.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { format, formatISO } from 'date-fns';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const title = process.argv[2] ?? 'New Blog Post';
|
||||
const slug = title.toLowerCase().replaceAll(' ', '-').slice(0, 60);
|
||||
|
||||
const filename = `${format(new Date(), 'yyyy.MM.dd')}.${slug}.md`;
|
||||
|
||||
const contents = `---
|
||||
title: '${title}'
|
||||
date: ${formatISO(new Date())}
|
||||
description: 'Blog post description here'
|
||||
---
|
||||
`;
|
||||
|
||||
console.log('dirname', __dirname);
|
||||
console.log('filename', filename);
|
||||
|
||||
fs.writeFileSync(path.join(__dirname, '..', 'src', 'routes', 'posts', filename), contents);
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
title: Making Customizable Components in Svelte
|
||||
title: 'Making Customizable Components in Svelte'
|
||||
date: 2024-05-12T17:50:52+0000
|
||||
tags:
|
||||
- dev
|
||||
|
|
|
@ -0,0 +1,140 @@
|
|||
---
|
||||
title: 'Using Shadow DOM to isolate injected browser extension components'
|
||||
date: 2024-05-18T23:08:16-05:00
|
||||
description: 'I spent a weekend building a browser extension with React and TailwindCSS, but injected components can be a lot of trouble. This post explores how Shadow DOM saves the day (and your CSS sanity).'
|
||||
---
|
||||
|
||||
I recently started experimenting with building a browser extension. I'm using
|
||||
React and TailwindCSS --bundling it all with Vite-- creating some UI elements like buttons and injecting them
|
||||
into certain web pages. Setting this up was super easy, I used [a repository template](https://github.com/JohnBra/vite-web-extension)
|
||||
to get a PoC as quickly as possible, and a few minutes later I had something working!
|
||||
|
||||
One issue with injecting components into web pages though is that your
|
||||
components are injected into the regular page DOM. This means that your
|
||||
components can be affected by scripts or CSS on the page, and conversely your
|
||||
CSS can affect existing elements on the page should anything collide.
|
||||
|
||||
This is a bit of a problem when using TailwindCSS. On one hand, utility classes
|
||||
have the same meaning across websites. For example, `w-full` is always going to
|
||||
be `width: 100%;`, because what else would it be? On the other hand, websites
|
||||
_are_ always able to change what CSS these utility classes map to. `rounded`
|
||||
might mean different amounts of rounding on different websites, and
|
||||
`bg-color-primary` is going to map to different colors based on the color
|
||||
palette of the website. And nobody can stop you if you want to change `w-full`
|
||||
to set the width to 10 pixels.
|
||||
|
||||
Avoiding these collisions in the past would have involved prefixing all your IDs
|
||||
and classes so that they are distinct from any ones you might encounter on the
|
||||
page. Even then, Javascript code of the original page can traverse the
|
||||
components you inject on the page and inspect or modify them! Nowadays though,
|
||||
Shadow DOM allows us to easily isolate components on the page. Using Shadow DOM,
|
||||
the CSS of the injected components is isolated from the rest of the page. That
|
||||
isolation works in both directions, the CSS from the original page won't affect
|
||||
the elements in the Shadow DOM, and the CSS injected into the Shadow DOM won't
|
||||
affect the elements outside of it. Plus, Shadow DOM has an option to isolate the
|
||||
injected components from Javascript of the original page so they can't traverse
|
||||
into the Shadow DOM (the original page can still delete or move the root of the
|
||||
Shadow DOM, but can't look inside of it).
|
||||
|
||||
And the code for this is very simple! Shadow DOM might sound a bit scary, but
|
||||
it's actually very easy. Let's start with some basic code to inject a React
|
||||
component into a page:
|
||||
|
||||
```ts
|
||||
import styles from "./style.css";
|
||||
import { createRoot } from "react-dom/client";
|
||||
|
||||
const injectionTarget = document.querySelector<HTMLDivElement>(
|
||||
"#place-to-inject-my-component",
|
||||
);
|
||||
if (!injectionTarget) {
|
||||
throw new Error("Can't find injection target");
|
||||
}
|
||||
|
||||
const rootContainer = document.createElement("div");
|
||||
injectionTarget.appendChild(rootContainer);
|
||||
|
||||
const root = createRoot(rootContainer);
|
||||
root.render(<YourElement />);
|
||||
```
|
||||
|
||||
To use Shadow DOM, we need to create the Shadow DOM and attach it to the page,
|
||||
then inject our React root inside of the Shadow DOM. This is as easy as calling
|
||||
`attachShadow`! Let's modify the example to use the Shadow DOM:
|
||||
|
||||
```ts
|
||||
import styles from "./style.css";
|
||||
import { createRoot } from "react-dom/client";
|
||||
|
||||
const injectionTarget = document.querySelector<HTMLDivElement>(
|
||||
"#place-to-inject-my-component",
|
||||
);
|
||||
if (!injectionTarget) {
|
||||
throw new Error("Can't find injection target");
|
||||
}
|
||||
|
||||
const shadowDomRoot = document.createElement("div");
|
||||
injectionTarget.appendChild(shadowDomRoot);
|
||||
const shadow = shadowDomRoot.attachShadow({
|
||||
// closed means the contents of the shadow can't be accessed from the page
|
||||
mode: "closed"
|
||||
});
|
||||
|
||||
const rootContainer = document.createElement("div");
|
||||
shadow.appendChild(rootContainer);
|
||||
|
||||
const root = createRoot(rootContainer);
|
||||
root.render(<YourElement />);
|
||||
```
|
||||
|
||||
Nice! We just had to add about 3 lines of code, and now our components lives
|
||||
inside it's own bubble.
|
||||
|
||||
Now, if you are following along with this example and running the code, you
|
||||
might have noticed that your injected component suddenly has no styles applied
|
||||
to it now. Well, that's the CSS isolation the Shadow DOM gives you. How do we
|
||||
get around this? That's not too much work either, we just need to inject the CSS
|
||||
into the page too.
|
||||
|
||||
First, you can use the manifest file to inject CSS into the page declaratively.
|
||||
This is in fact what the repository template I linked above does. But that's not
|
||||
what we want here, because we don't want the CSS to be just injected into the
|
||||
page. We want it injected specifically into the Shadow DOM we just created. So,
|
||||
we'll have to disable this declarative CSS injection.
|
||||
|
||||
```json
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["http://*/*", "https://*/*", "<all_urls>"],
|
||||
"js": ["src/pages/content/index.tsx"],
|
||||
// Make sure this is empty
|
||||
"css": []
|
||||
}
|
||||
],
|
||||
```
|
||||
|
||||
Now that we got rid of that, we'll want to import the CSS into our script and
|
||||
inject that along too. The trick here is adding `?inline` to the end of the CSS
|
||||
import so that rather than injecting the CSS into the page, Vite understands that
|
||||
we want the contents of the CSS.
|
||||
|
||||
```ts
|
||||
// Note the `?inline`
|
||||
import styles from './style.css?inline';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
// ... same as example above
|
||||
|
||||
// Create the style element, put the CSS into it,
|
||||
// and inject it into the Shadow DOM.
|
||||
const style = document.createElement('style');
|
||||
style.innerHTML = styles;
|
||||
shadow.appendChild(style);
|
||||
|
||||
// ... same as example above
|
||||
```
|
||||
|
||||
Tada! Your component should now light up with all the fancy CSS --or utility
|
||||
classes-- you added to it. While the Shadow DOM example is slightly more complex
|
||||
than the most basic option I demonstrated first, the advantages you get in CSS
|
||||
and JS isolation are worth it considering it only takes a few lines of text.
|
Loading…
Reference in a new issue