Skip to content

Commit 76e847a

Browse files
authored
feat(dev): add option for adding styles to global object instead of <head> element (for using in Shadow DOM) (#548)
* feat(dev): add option for adding styles to global object instead of <head> element (for using in Shadow DOM) * feat(dev): Add documentation for dontAppendStylesToHead option in README
1 parent 3ed4438 commit 76e847a

File tree

5 files changed

+65
-14
lines changed

5 files changed

+65
-14
lines changed

README.md

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -236,11 +236,36 @@ As the entry file of the remote module, not required, default is `remoteEntry.js
236236
```js
237237
exposes: {
238238
// 'externally exposed component name': 'externally exposed component address'
239-
'./remote-simple-button': './src/components/Button.vue',
240-
'./remote-simple-section': './src/components/Section.vue'
239+
'./remote-simple-button': './src/components/Button.vue',
240+
'./remote-simple-section': './src/components/Section.vue'
241241
},
242242
```
243243

244+
* If you need a more complex configuration
245+
```js
246+
exposes: {
247+
'./remote-simple-button': {
248+
import: './src/components/Button.vue',
249+
name: 'customChunkName',
250+
dontAppendStylesToHead: true
251+
},
252+
},
253+
```
254+
The `import` property is the address of the module. If you need to specify a custom chunk name for the module use the `name` property.
255+
256+
The `dontAppendStylesToHead` property is used if you don't want the plugin to automatically append all styles of the exposed component to the `<head>` element, which is the default behavior. It's useful if your component uses a ShadowDOM and the global styles wouldn't affect it anyway. The plugin will then expose the addresses of the CSS files in the global `window` object, so that your exposed component can append the styles inside the ShadowDOM itself. The key under the `window` object used for styles will be `css__{name_of_the_app}__{key_of_the_exposed_component}`. In the above example it would be `css__App__./remote-simple-button`, assuming that the global `name` option (not the one under exposed component configuration) is `App`. The value under this key is an array of strings, which contains the addresses of CSS files. In your exposed component you can iterate over this array and manually create `<link>` elements with `href` attribute set to the elements of the array like this:
257+
```js
258+
const styleContainer = document.createElement("div");
259+
const hrefs = window["css__App__./remote-simple-button"];
260+
261+
hrefs.forEach((href: string) => {
262+
const link = document.createElement('link')
263+
link.href = href
264+
link.rel = 'stylesheet'
265+
styleContainer.appendChild(link);
266+
});
267+
```
268+
244269
----
245270
### `remotes`
246271

packages/lib/__tests__/utils.spec.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,13 @@ test('get moduleMarker', () => {
4343
test('parse exposes options', () => {
4444
const normalizeSimple = (item) => ({
4545
import: item,
46-
name: undefined
46+
name: undefined,
47+
dontAppendStylesToHead: false
4748
})
4849
const normalizeOptions = (item) => ({
4950
import: item.import,
50-
name: item.name || undefined
51+
name: item.name || undefined,
52+
dontAppendStylesToHead: item.dontAppendStylesToHead || false
5153
})
5254
// string[]
5355
let exposes: (string | ExposesObject)[] | ExposesObject = [
@@ -81,7 +83,8 @@ test('parse exposes options', () => {
8183
exposes = {
8284
'./Content': {
8385
import: './src/components/Content.vue',
84-
name: 'content'
86+
name: 'content',
87+
dontAppendStylesToHead: true
8588
},
8689
'./Button': {
8790
import: './src/components/Button.js',
@@ -91,11 +94,19 @@ test('parse exposes options', () => {
9194
ret = parseOptions(exposes, normalizeSimple, normalizeOptions)
9295
expect(ret[0]).toMatchObject([
9396
'./Content',
94-
{ import: './src/components/Content.vue', name: 'content' }
97+
{
98+
import: './src/components/Content.vue',
99+
name: 'content',
100+
dontAppendStylesToHead: true
101+
}
95102
])
96103
expect(ret[1]).toMatchObject([
97104
'./Button',
98-
{ import: './src/components/Button.js', name: 'button' }
105+
{
106+
import: './src/components/Button.js',
107+
name: 'button',
108+
dontAppendStylesToHead: false
109+
}
99110
])
100111
// console.log(JSON.stringify(ret))
101112

packages/lib/src/prod/expose-production.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export function prodExposePlugin(
6363
`__federation_expose_${removeNonRegLetter(item[0], NAME_CHAR_REG)}`
6464
)
6565
moduleMap += `\n"${item[0]}":()=>{
66-
${DYNAMIC_LOADING_CSS}('${DYNAMIC_LOADING_CSS_PREFIX}${exposeFilepath}')
66+
${DYNAMIC_LOADING_CSS}('${DYNAMIC_LOADING_CSS_PREFIX}${exposeFilepath}', ${item[1].dontAppendStylesToHead}, '${item[0]}')
6767
return __federation_import('\${__federation_expose_${item[0]}}').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)},`
6868
}
6969

@@ -78,7 +78,7 @@ export function prodExposePlugin(
7878
const exportSet = new Set(['Module', '__esModule', 'default', '_export_sfc']);
7979
let moduleMap = {${moduleMap}}
8080
const seen = {}
81-
export const ${DYNAMIC_LOADING_CSS} = (cssFilePaths) => {
81+
export const ${DYNAMIC_LOADING_CSS} = (cssFilePaths, dontAppendStylesToHead, exposeItemName) => {
8282
const metaUrl = import.meta.url
8383
if (typeof metaUrl == 'undefined') {
8484
console.warn('The remote style takes effect only when the build.target option in the vite.config.ts file is higher than that of "es2020".')
@@ -90,9 +90,15 @@ export function prodExposePlugin(
9090
const href = curUrl + cssFilePath
9191
if (href in seen) return
9292
seen[href] = true
93-
const element = document.head.appendChild(document.createElement('link'))
94-
element.href = href
95-
element.rel = 'stylesheet'
93+
if (dontAppendStylesToHead) {
94+
const key = 'css__${options.name}__' + exposeItemName;
95+
if (window[key] == null) window[key] = []
96+
window[key].push(href);
97+
} else {
98+
const element = document.head.appendChild(document.createElement('link'))
99+
element.href = href
100+
element.rel = 'stylesheet'
101+
}
96102
})
97103
};
98104
async function __federation_import(name) {

packages/lib/src/utils/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,14 @@ export function parseExposeOptions(
8484
(item) => {
8585
return {
8686
import: item,
87-
name: undefined
87+
name: undefined,
88+
dontAppendStylesToHead: false
8889
}
8990
},
9091
(item) => ({
9192
import: item.import,
92-
name: item.name || undefined
93+
name: item.name || undefined,
94+
dontAppendStylesToHead: item.dontAppendStylesToHead || false
9395
})
9496
)
9597
}

packages/lib/types/index.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,13 @@ declare interface ExposesConfig {
121121
* Custom chunk name for the exposed module.
122122
*/
123123
name?: string
124+
125+
/**
126+
* If false, the link element with styles is put in <head> element. If true, the href argument of all links objects
127+
* are put under global window object and can be retrieved by the component. It's for using with ShadowDOM, when
128+
* the component must place the styles inside the ShadowDOM instead of the <head> element.
129+
*/
130+
dontAppendStylesToHead?: boolean
124131
}
125132

126133
/**

0 commit comments

Comments
 (0)