多Web Components按关注点分离的组织方案及相关问题
关于无打包工具的Web Components架构问题
我正在搭建一个小型个人网站,计划使用Web Components而非UI库,且不打算使用任何打包工具。我希望将每个Web Components存储在独立的ES6 JS模块文件中,以下是我的配置示例:
代码示例
hello-planet.mjs
export class HelloPlanet extends HTMLElement { constructor() { super(); } connectedCallback(){ // Attach a shadow root to the element. let shadowRoot = this.attachShadow({mode: 'open'}); let planet = this.getAttribute('planet') shadowRoot.innerHTML = `<p>hello ${planet}</p>`; } }
hello-world.mjs
export class HelloWorld extends HTMLElement { constructor(){ super() } connectedCallback(){ // Attach a shadow root to the element. let shadowRoot = this.attachShadow({mode: 'open'}); ['Mercury','Venus','Earth','Mars','Jupiter','Saturn','Uranus','Neptune'].forEach(planet=>{ let el = document.createElement('hello-planet') el.setAttribute('planet', planet); shadowRoot.appendChild(el) }) } }
main.mjs
// ordering of imports does not matter import {HelloPlanet} from './hello-planet.mjs'; import {HelloWorld} from './hello-world.mjs'; customElements.define('hello-world', HelloWorld); customElements.define('hello-planet', HelloPlanet); // this will typically be handled by a client side router (e.g. Navigo) let el = document.createElement('hello-world'); let body = document.querySelector('body'); body.appendChild(el);
index.html
(仅调用main.mjs,浏览器会下载其余脚本)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <script src="main.mjs" type="module"></script> <title>Web components test</title> </head> <body> </body> </html>
问题
- 我在已查阅的示例中未见过这种方案,请问该方案是否合理?有没有更优的Web Components组织方式?
- 当需要模板和样式时,如何扩展该方案将它们分离到单独的HTML和CSS文件中以实现关注点分离?我看过相关资料但不知如何适配当前架构。
回答
问题1:方案合理性与更优组织方式
你的方案完全合理,这是无打包工具场景下Web Components的标准模块化实践,很多轻量项目都会采用这种方式,只是部分示例可能会把组件注册逻辑分散到各自模块中,而非集中到main.mjs。
更优的组织方式建议:
- 组件自注册:把
customElements.define逻辑放到每个组件自己的模块中,比如在hello-planet.mjs末尾添加:
这样if (!customElements.get('hello-planet')) { customElements.define('hello-planet', HelloPlanet); }main.mjs只需负责导入组件,不用管注册,降低耦合度,也方便单个组件在其他页面复用。 - 按功能分组目录:如果后续组件增多,可以按功能划分目录,比如
/components/ui/放通用UI组件,/components/pages/放页面级组件,让结构更清晰。 - 懒加载组件:结合路由(比如你提到的Navigo),在路由切换时动态导入组件,减少初始页面的加载资源,比如:
router.on('/planets', async () => { await import('./hello-world.mjs'); const el = document.createElement('hello-world'); document.body.appendChild(el); });
问题2:分离模板与样式到单独文件
要实现关注点分离,你可以通过ES模块导入静态资源的方式,结合fetch或者原生的import assertions(现代浏览器支持)来加载单独的HTML和CSS文件:
步骤1:分离模板和样式文件
- 创建
hello-planet.template.html:<p>hello <span class="planet-name"></span></p> - 创建
hello-planet.style.css:.planet-name { color: #2c3e50; font-weight: bold; }
步骤2:在组件模块中加载资源
修改hello-planet.mjs,可以选择两种实现方式:
// 方式1:使用fetch(兼容性更好) export class HelloPlanet extends HTMLElement { constructor() { super(); } async connectedCallback() { const shadowRoot = this.attachShadow({ mode: 'open' }); // 加载样式 const styleRes = await fetch('./hello-planet.style.css'); const styleText = await styleRes.text(); const styleEl = document.createElement('style'); styleEl.textContent = styleText; // 加载模板 const templateRes = await fetch('./hello-planet.template.html'); const templateText = await templateRes.text(); const templateEl = document.createElement('template'); templateEl.innerHTML = templateText; // 填充数据并挂载到shadowRoot const content = templateEl.content.cloneNode(true); content.querySelector('.planet-name').textContent = this.getAttribute('planet'); shadowRoot.appendChild(styleEl); shadowRoot.appendChild(content); } } // 自注册组件 if (!customElements.get('hello-planet')) { customElements.define('hello-planet', HelloPlanet); }
// 方式2:使用import断言(Chrome 91+,Firefox 102+支持) import templateText from './hello-planet.template.html' assert { type: 'text' }; import styleText from './hello-planet.style.css' assert { type: 'text' }; export class HelloPlanet extends HTMLElement { constructor() { super(); } connectedCallback() { const shadowRoot = this.attachShadow({ mode: 'open' }); const styleEl = document.createElement('style'); styleEl.textContent = styleText; const templateEl = document.createElement('template'); templateEl.innerHTML = templateText; const content = templateEl.content.cloneNode(true); content.querySelector('.planet-name').textContent = this.getAttribute('planet'); shadowRoot.appendChild(styleEl); shadowRoot.appendChild(content); } } if (!customElements.get('hello-planet')) { customElements.define('hello-planet', HelloPlanet); }
注意事项:
- 如果你使用
import断言,需要确保服务器正确返回text/plain或对应的MIME类型,否则浏览器会报错。 - 对于复用性高的模板,也可以把模板文件放到
/templates/目录,样式放到/styles/components/目录,保持结构规整。
内容的提问来源于stack exchange,提问作者mrbrahman
相关产品推荐
相关产品推荐

