You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

多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>

问题

  1. 我在已查阅的示例中未见过这种方案,请问该方案是否合理?有没有更优的Web Components组织方式?
  2. 当需要模板和样式时,如何扩展该方案将它们分离到单独的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.18 01:25:30