美文网首页
Bit 组件共享

Bit 组件共享

作者: 说叁两事 | 来源:发表于2021-11-19 00:58 被阅读0次

    Think in Component

    Bit是组件驱动架构,基于组件的现代应用开发。在Bit的世界里,一切皆组件。

    组件可以组合成其他组件,最终组成一个应用APP,即APP也是组件的一种。

    这为我们开发提供一个新的思路:我们构建可以整合成不同应用的组件,而不是构建包含组件的应用。

    Bit帮我们构建模块化、稳固的、可测试、可复用的代码。

    Bit Cloud是组件的云托管服务。它为开发人员和团队提供端到端的解决方案,用于托管、组织、检索、使用、更新和协作处理组件。

    image

    Bit优势

    • 以组件架构的思想帮助我们构建模块化、稳固的、可测试、可复用的代码。
    • 从现有代码结构中分离组件,无需更改结构,或维护新的项目。
    • 可更改依赖组件,并创建自己的版本独立管理,无需担忧污染其它环境。

    初始化Bit工作区

    安装BVM & Bit

    BVM是Bit版本管理工具,雷同NVM

    // node版本12.22.0以上
    npm i -g @teambit/bvm
    
    

    执行bvm -h检验是否安装成功,若提醒bvm命令不可用,需要设置环境变量:

    # MacOs Bash
    echo 'export PATH=$HOME/bin:$PATH' >> ~/.bashrc && source ~/.bashrc
    
    # zsh
    echo 'export PATH=$HOME/bin:$PATH' >> ~/.zshrc && source ~/.zshrc
    
    # windows
    setx path "%path%;%LocalAppData%\.bvm"
    
    

    安装最新版bit:

    bvm install
    
    

    执行bit -h检验是否安装成功,若提醒bit命令不可用,需要按上述流程设置一下环境变量。

    bit new命令初始化工作区

    适用于新建项目

    $ bit new  <env> <project>
    $ cd <project>
    $ bit install
    
    

    bit init命令初始化工作区

    适用于已有项目

    1. 先初始化环境
    $ cd <project>
    $ bit init --harmony
    
    
    1. 手动配置开发环境

    以react环境为例,修改workspace.jsonc文件:

    "teambit.workspace/variants": {
      "*": {
        "teambit.react/react": { }
      }
    }
    
    
    1. 安装必要的peer依赖
    $ bit install react --type peer
    $ bit install react-dom --type peer
    
    

    初始化Git

    需要将workspace.jsonc和.bitmap 上传到Git。

    创建组件

    使用内置组件创建

    以react为例:

    1. 以内置模版创建组件bit create <built-in-template> <component>
    $ bit templates # 查看所有的内置模版
    $ bit create react-component ui/button     # TypeScript
    $ bit create react-component-js ui/button  # JavaScript
    
    

    注意:其中,<component>可以是个路径,前置路径为命名空间,上述示例等同于bit create react-component button --namespace ui。

    1. 添加测试用例
    $ bit install @testing-library/react
    
    
    1. 编译并起服务
    $ bit compile
    $ bit start
    
    

    自定义组件

    1. 已有组件结构与代码
    2. 通过bit add <relative-path> --namespace <namespace>添加组件

    查看组件信息

    可以查看组件编译环境、包含文件、依赖等所有信息。

    $ bit show <component-id>
    
    

    输出信息示例:

      ┌───────────────┬────────────────────────────────────────────────────────────────────┐
      │ id            │ my-scope/ui/button                                                 │
      ├───────────────┼────────────────────────────────────────────────────────────────────┤
      │ scope         │ my-scope                                                           │
      ├───────────────┼────────────────────────────────────────────────────────────────────┤
      │ name          │ ui/button                                                          │
      ├───────────────┼────────────────────────────────────────────────────────────────────┤
      │ env           │ teambit.react/react                                                │
      ├───────────────┼────────────────────────────────────────────────────────────────────┤
      │ package name  │ @my-scope/ui.button                                                │
      ├───────────────┼────────────────────────────────────────────────────────────────────┤
      │ main file     │ index.ts                                                           │
      ├───────────────┼────────────────────────────────────────────────────────────────────┤
      │ files         │ button.composition.tsx                                             │
      │               │ button.docs.mdx                                                    │
      │               │ button.tsx                                                         │
      │               │ button.spec.tsx                                                    │
      │               │ index.ts                                                           │
      ├───────────────┼────────────────────────────────────────────────────────────────────┤
      │ dev files     │ button.docs.mdx (teambit.docs/docs)                                │
      │               │ button.spec.tsx (teambit.defender/tester)                          │
      │               │ button.composition.tsx (teambit.compositions/compositions)         │
      ├───────────────┼────────────────────────────────────────────────────────────────────┤
      │ extensions    │ teambit.react/react                                                │
      │               │ teambit.component/dev-files                                        │
      │               │ teambit.compositions/compositions                                  │
      │               │ teambit.pkg/pkg                                                    │
      │               │ teambit.docs/docs                                                  │
      │               │ teambit.envs/envs                                                  │
      │               │ teambit.dependencies/dependency-resolver                           │
      ├───────────────┼────────────────────────────────────────────────────────────────────┤
      │ dependencies  │ core-js@3.8.3- (package)                                           │
      ├───────────────┼────────────────────────────────────────────────────────────────────┤
      │ dev           │ @testing-library/react@11.2.6- (package)                           │
      │ dependencies  │ @babel/runtime@7.12.18-------- (package)                           │
      │               │ @types/react-router-dom@5.1.7- (package)                           │
      │               │ @types/jest@26.0.20----------- (package)                           │
      │               │ @types/react@16.9.43---------- (package)                           │
      │               │ @types/node@12.20.4----------- (package)                           │
      ├───────────────┼────────────────────────────────────────────────────────────────────┤
      │ peer          │ react@16.13.1----- (package)                                       │
      │ dependencies  │ react-dom@16.13.1- (package)                                       │
      └───────────────┴────────────────────────────────────────────────────────────────────┘
    
    

    查看组件状态

    $ bit status
    
    

    查看组件所有版本

    $ bit log <component-id>
    
    

    查看本地所有组件列表

    $ bit list
    
    

    启动测试服务器

    通过 worker 运行不同的工作区任务,例如测试、linter 和由组件定义的任何工作区任务。

    $ bit compile
    $ bit start
    
    

    使用组件

    在导入另一个组件作为依赖时,Bit不允许使用相对路径导入。因为这会耦合项目特定的目录结构,请使用包名替代。

    要将组件作为依赖项导入,必须使用模块链接。

    Bit 为工作区中的每个组件创建一个模块,这些模块链接在 node_modules 目录中,并包含它的构建输出和自动生成的 package.json。

    要为组件重新生成模块链接,请运行该bit link命令。

    将组件安装为NPM包

    install命令安装组件,以NPM包的形式使用。

    作为Vendor组件

    Bit工作区获取组件并管理该组件,就好像它是自定义组件一样

    通过import命令安装组件,示例如下:

    $ bit import <component-id>
    
    

    更新import的组件到最新版本

    $ bit import
    
    

    将Vendor组件转为NPM包依赖

    $ bit eject <component-id>
    
    

    Scope

    Scope是组件的虚拟存储。

    Bit 使用Scope保存Bit组件的版本并根据需要访问它们。

    Remote Scope

    托管组件及其版本的Bit服务器。

    特色

    在远程服务器上设置Scope以共享组件,如Bit.dev或自托管 Bit 服务器。

    将组件存储在Remote Scope上,可以使它们在其他项目中重复使用。

    • 使用import命令从Remote Scope获取组件。
    • 使用export命令将组件推送到Remote Scope。

    注意:Remote Scope会缓存组件依赖,例如其他Scope的组件。这样做的好处是,即使依赖组件不可用,还能确保当前组件可执行。

    使用

    在Bit Server创建Remote Scope后,需要更改workspace.jsonc文件:

    {
      "teambit.workspace/workspace": {
        "defaultScope": "<bit-username>.<remote-scope-name>"
      }
    }
    
    

    workspace.jsonc文件中的任何更改都需要重新启动本地开发服务器。

    $ bit start
    
    

    Workspace Scope

    工作区组件的本地存储。

    特色

    开发人员的工作区都在本地 Scope 中保存了组件及其历史记录的工作副本。这允许我们浏览历史记录、比较版本和检查组件的过去修订。

    Workspace Scope也可能包含来自各异Remote Scope的组件。

    共享组件

    1. 为已修改的组件更新版本号
    $ bit tag --all --message "first version"
    
    
    1. 共享组件
    $ bit export
    
    

    注意:当共享上传流程结束,.bitmap文件将更新以反映该新状态。

    安装组件

    注册Scope源

    $ npm config set '@YourUserName:registry' https://node.bit.dev
    
    

    安装依赖

    $ npm install @orgName/componentScopeName.componentID
    
    

    Bit Component vs. NPM包

    Bit 专注于基于组件的工作流,npm 包关注编译后的输出。

    • 生成NPM包只是Bit Component构建流程的部分,Bit称之为版本工件。

    Configuration

    每个组件都必须配置一个环境,好让Bit 就“知道”如何构建、测试、lint 和document组件。

    teambit.workspace/variants提供一个统一的方式,可以为每个组件设置不同的配置项,而无需修改每个组件文件下的 package.json 。

    {
      "teambit.workspace/variants": {
        "design/theme": {
          "defaultScope": "acme.theme",
        },
        "cart": {
          "defaultScope": "acme.cart",
          "teambit.react/react": {}
        }
      }
    }
    
    

    查看配置

    • bit env - 打印一个简单的表格,其中包含工作区中的所有组件及其环境
    • bit show <component> - 打印组件的所有信息,包括环境
    • bit start- 通过浏览器可视化浏览组件树以查看组件的环境

    移除组件

    移除本地组建

    $ bit remove <component-id>
    
    

    产生的影响:

    • 一个未追踪的组件依赖 删除组件 —— 没有影响
      • 因为Bit还没有隔离未追踪的组件,不会检测其依赖
    • 一个已追踪的组件依赖 删除组件 —— 会警告,使用--force强制删除
    • 引入的远程组件依赖 删除组件 —— 没有影响
      • 因为远程组件是已经隔离且不可更改的
      • 本地引入远程组件且更改会创建另一个版本

    移除远程组件

    $ bit remove <username.your-scope/ui/button> --remote
    
    

    以一个例子描述产生的影响:

    • button组件在远程uiScope中
    • card组件依赖button组件,也在uiScope中
    • login组件依赖button组件,在adminScope中

    删除button组件后的影响:

    • 因为card组件与button组件在同一个Scope中,因此删除button组件会有个警告。
      • 可追加---force强制删除
      • 删除后,card组件缺少依赖,为保证其正常工作需要重构
    • login组件没有影响
      • Bit会在Scope中维护依赖
    • 其他项目依赖login组件时,安装会报错
      • 溯源button组件,缺失

    编译组件

    大多数现代框架都需要一个编译或转译项目来将源代码转换为可以在多个浏览器或 Nodejs 中运行的可执行代码。

    而Bit 的编译器是一个环境服务。

    编译器的选择(Babel、TypeScript 等)及其配置由其服务的各种环境决定。

    编译器永远不会直接运行,而只能通过 Compiler 服务运行。

    单个工作区可能会针对不同的组件运行不同的编译器,每个编译器都根据自己的环境。

    $ bit compile <component-id> # 编译特定组件
    $ bit compile # 编译工作区全部组件
    
    

    组件依赖关系图

    Bit 的一个关键特性是能够根据组件的源代码自动创建依赖关系图。

    Javascript 可以使用 require 或 import 声明依赖两种类型的依赖项:

    • 作为 node_modules 安装的软件包
    • 项目内部的文件和目录,或在装饰器中引用(例如在 Angular 中)

    node_modules依赖

    Bit解析包(即node_modules)的流程:

    image
    • 可以通过bit show <component-id>来检查 Bit 为每个包解析的依赖项(Packages):
    $ bit show hello/world
    ┌───────────────────┬─────────────────────────────────────────────────────────────────────┐
    │        ID         │                            hello/world                              │
    ├───────────────────┼─────────────────────────────────────────────────────────────────────┤
    │     Language      │                             javascript                              │
    ├───────────────────┼─────────────────────────────────────────────────────────────────────┤
    │     Main File     │                      src/hello-world/index.js                       │
    ├───────────────────┼─────────────────────────────────────────────────────────────────────┤
    │     Packages      │                           left-pad@^2.1.0                           │
    ├───────────────────┼─────────────────────────────────────────────────────────────────────┤
    │       Files       │       src/hello-world/hello-world.js, src/hello-world/index.js      │
    └───────────────────┴─────────────────────────────────────────────────────────────────────┘
    
    

    如果 Bit 无法解析所有包的依赖项,它会提示missing package dependencies。我们需要验证 package.json 中是否确实存在所有包。

    文件依赖

    组件可以依赖于其他文件,例如import ./utils.js。

    为了隔离这些依赖其它文件的组件,我们还需要跟踪该组件依赖的其它文件。这是因为如果我们想在另一个项目中使用这个组件,该组件必须要有它的依赖文件。

    注意:Bit 使用静态代码分析,因此仅支持静态导入import,不支持require。

    Bit解析文件依赖的流程

    image

    当 Bit 遇到需要跟踪的文件时,它会尝试检查该文件是否已经在另一个组件中进行了跟踪,在这种情况下,Bit 将使另一个组件成为该组件的依赖项。

    如果文件未被跟踪,Bit 将untracked file dependencies在检查组件状态时发出警告。

    隔离问题

    要解决隔离问题,您可以:

    • 将未跟踪的文件依赖项添加到现有组件
    • 将文件作为新组件进行跟踪

    采取以上何种方法基于文件的上下文。如果该文件被多个其他组件使用,则将其放入一个单独的组件中是有意义的。

    但是,如果此文件仅仅是被跟踪文件的内部文件,则可以将其添加为组件的文件。

    文件添加到现有组件

    运行bit add指向要添加文件的组件的 Id:

    // 示例
    $ bit add src/utils/noop.js --id hello/world
    
    

    运行bit status ,检查是否成功:

    $ bit status
    new components
        > component/hello-world... ok
    
    

    文件作为新组件进行跟踪

    可以bit add添加新组件

    // 示例
    $ bit add src/utils/noop.js --namespace utils
    
    

    执行结果是一个新组件。

    私有化部署v15

    image

    硬件条件

    • Linux/Mac系统
    • 内存4G+

    前置条件

    • Docker
    • Git
    # 卸载旧版docker
    $ yum remove docker  docker-common docker-selinux docker-engine
    # 安装docker依赖
    $ yum install -y yum-utils device-mapper-persistent-data lvm2
    # 设置docker源
    $ yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
    $ yum install docker-ce
    # 启动docker
    $ systemctl start docker
    # 加入开机启动
    $ systemctl enable docker
    
    
    # 安装Git
    $ yum install git
    
    

    部署流程

    $ git clone https://github.com/teambit/bit.git
    $ cd bit/scripts/docker-teambit-bit
    $ docker build -f ./Dockerfile-bit -t bitcli/bit:latest .
    $ docker build -f ./Dockerfile-bit-server -t bitcli/bit-server:latest .
    $ docker run -dit bitcli/bit:latest /bin/bash # 运行
    $ docker run -dit -p <port>:3000 bitcli/bit-server:latest
    
    
    • Dockerfile-bit:
      • 安装 bvm 然后使用 bvm 安装 bit 的 docker 文件。
      • 这个 docker 通常对在 CI 机器上运行像 tag 和 export 这样的Bit命令很有用
    • Dockerfile-bit-server:
      • 一个基于Dockerfile-bit(使用 from)的docker 文件
      • 该docker文件创建一个空白Scope,并在其上通过bit start初始化Bit服务器
    • Dockerfile-symphony:
      • 仅供内部使用

    相关问题

    Mac电脑ssh链接: Permission denied

    $ sudo ssh root@<ip>
    
    

    ssh链接时报警告WARNING: REMOTE HOST IDENTIFICATION HAS CHANGE

    $ sudo ssh-keygen -R <ip>
    
    

    bvm install安装不了Bit

    临时更改terminal代理

    $ export http_proxy=http://127.0.0.1:1087
    $ export https_proxy=$http_proxy
    
    

    注意:

    • 需要有VPN
    • 保证浏览器可以访问外网
    • 开启VPN,即使全局,终端也是无法被代理

    永久修改代理

    # 修改~/.bashrc设置永久管理脚本
    function proxy_on() {
        export http_proxy=http://127.0.0.1:1087
        export https_proxy=$http_proxy
        echo -e "终端代理已开启。"
    }
    
    function proxy_off(){
        unset http_proxy https_proxy
        echo -e "终端代理已关闭。"
    }
    
    

    注意:修改后,通过source ~/.bashrc立即生效。

    通过proxy_on启动代理,proxy_off关闭代理。

    发布

    注册远程Scope

    # 客户端
    $ cd <my-project>
    $ bit init
    $ bit remote add http://<host>:<port>
    
    

    workspace.jsonc

    配置teambit.workspace/workspace

      "teambit.workspace/workspace": {
        /**
         * the name of the component workspace. used for development purposes.
         **/
        "name": "my-workspace-name",
        /**
         * set the icon to be shown on the Bit server.
         **/
        "icon": "https://static.bit.dev/bit-logo.svg",
        /**
         * default directory to place a component during `bit import` and `bit create`.
         * the following placeholders are available:
         * name - component name includes namespace, e.g. 'ui/button'.
         * scopeId - full scope-id includes the owner, e.g. 'teambit.compilation'.
         * scope - scope name only, e.g. 'compilation'.
         * owner - owner name in bit.dev, e.g. 'teambit'.
         **/
        "defaultDirectory": "{scope}/{name}",
        /**
         * default scope for all components in workspace.
         **/
        "defaultScope": "remote-scope"
      },
    
    

    打Tag

    监听文件变化,才能标识。

    若文件无变化,无法进行标识。

    $ bit tag --all  --message "first version"
    
    

    注意: 独立的组件通过独立的“远程范围”、远程组件托管形成依赖关系网络。

    这种依赖关系网络使更改能够让一个组件的改变传播到它的所有依赖组件。也就是说,一个组件的更改以级联的方式触发了组件的CI及依赖它的组件的CI。

    举例:

    组件B依赖组件A,二者初始版本皆为0.0.1。

    若组件A由0.0.1进行更改,通过bit tag --all会A0.0.1 —> 0.0.2,B0.0.1 —> 0.0.2。

    若继续更改组件B,通过bit tag --all,B0.0.2 —> 0.0.3,A的版本不变。

    Bit部署

    部署的前提是有新的标识,否则,无法部署。

    $ bit export
    
    
    image

    扩展Bit

    我们通过创建Aspect和接入Bit的API来扩展Bit。

    扩展Workspace UI

    以新增Tab为例:

    初始化Bit环境

    $ bit init
    
    

    会自动新建.bit/、.bitmap、workspace.jsonc文件(夹)。

    修改DefaultScope

    {
      ...
      "teambit.workspace/workspace": {
        /**
         * the name of the component workspace. used for development purposes.
         **/
        "name": "my-workspace-name",
        /**
         * set the icon to be shown on the Bit server.
         **/
        "icon": "https://static.bit.dev/bit-logo.svg",
        /**
         * default directory to place a component during `bit import` and `bit create`.
         * the following placeholders are available:
         * name - component name includes namespace, e.g. 'ui/button'.
         * scopeId - full scope-id includes the owner, e.g. 'teambit.compilation'.
         * scope - scope name only, e.g. 'compilation'.
         * owner - owner name in bit.dev, e.g. 'teambit'.
         **/
        "defaultDirectory": "{scope}/{name}",
        /**
         * default scope for all components in workspace.
         **/
        "defaultScope": "me"
      },
      ...
    }
    
    

    新建Aspect

    $ bit create aspect aspects/hello-world
    
    

    生成目录结构:

    .
    └──me
      └── aspects
        └── hello-world
          ├── hello-world.aspect.ts
          ├── hello-world.main.runtime.ts
          └── index.ts
    
    

    其中,hello-world.main.runtime.ts代码如下:

    // hello-world.main.runtime.ts
    import { MainRuntime } from '@teambit/cli';
    import { HelloWorldAspect } from './hello-world.aspect';
    
    export class HelloWorldMain {
    
      static slots = [];
      static dependencies = [];
      static runtime = MainRuntime;
      static async provider() {
        return new HelloWorldMain();
      }
    }
    
    HelloWorldAspect.addRuntime(HelloWorldMain);
    
    

    注意:hello-world.main.runtime是负责扩展workspace CLI和 workspace Server的。

    为了在组件详情页创建一个新的菜单,我们需要参考hello-world.main.runtime.ts文件新建hello-world.ui.runtime.tsx文件:

    // hello-world.ui.runtime.tsx
    import React, { useContext } from 'react';
    import { UIRuntime } from '@teambit/ui';
    import { ComponentUI, ComponentAspect } from '@teambit/component';
    import { HelloWorldAspect } from './hello-world.aspect';
    
    export class HelloWorldUI extends React.Component<any> {
      static slots = [];
      static dependencies = [ComponentAspect];
      static runtime = UIRuntime;
      static async provider([component]: [ComponentUI]) {
        return new HelloWorldUI();
      }
    }
    
    HelloWorldAspect.addRuntime(HelloWorldUI);
    
    

    注意:这里引入了ComponentAspect,它是Bit核心Aspect,负责组建页面所有的组件和操作。将ComponentAspect作为依赖,我们能在provider中获取到它并使用它提供的API。

    // 更新hello-world.ui.runtime.tsx
    // 注册registerNavigation导航
    import React, { useContext } from 'react';
    import { UIRuntime } from '@teambit/ui';
    import { ComponentUI, ComponentAspect } from '@teambit/component';
    import { HelloWorldAspect } from './hello-world.aspect';
    
    export class HelloWorldUI extends React.Component<any> {
      static slots = [];
      static dependencies = [ComponentAspect];
      static runtime = UIRuntime;
      static async provider([component]: [ComponentUI]) {
         component.registerNavigation({  
           href: '~hello',  
           children: 'Hello'
         });
        return new HelloWorldUI();
      }
    }
    
    HelloWorldAspect.addRuntime(HelloWorldUI);
    
    

    这里,我们通过ComponentAspect依赖提供的registerNavigation注册了导航,手动切换导航会渲染Hello。

    // 更新hello-world.ui.runtime.tsx
    // 注册registerRoute路由
    import React, { useContext } from 'react';
    import { UIRuntime } from '@teambit/ui';
    import { ComponentUI, ComponentAspect } from '@teambit/component';
    import { HelloWorldAspect } from './hello-world.aspect';
    
    export class HelloWorldUI extends React.Component<any> {
      static slots = [];
      static dependencies = [ComponentAspect];
      static runtime = UIRuntime;
      static async provider([component]: [ComponentUI]) {
         component.registerRoute({      
           children: () => <div>hello world</div>,      
           path: '~hello'    
         });
         component.registerNavigation({  
           href: '~hello',  
           children: 'Hello'
         });
        return new HelloWorldUI();
      }
    }
    
    HelloWorldAspect.addRuntime(HelloWorldUI);
    
    

    这里,我们通过ComponentAspect依赖提供的registerRoute注册了路由,该路由会承接上述注册的导航,简单的渲染了hello world。

    注册自定义Aspect

    在执行Aspect之前,要为其配置解析环境,该环境会将Aspect最终转译为浏览器、nodejs可识别的代码。

    {
      ...
      "teambit.workspace/variants": {
        "{me/aspects/*}": {
          "teambit.harmony/aspect":{}
        }
      },
      "me/aspects/hello-world": {}
      ...
    }
    
    

    安装依赖

    $ bit install
    
    

    不安装依赖,bit start也是正常运行的,只是看不到增加的UI。

    效果展示

    运行bit start查看效果~

    image

    注意:若要更新展示,则要删除.bit/、node_modules、public/,再次执行bit install和bit start。

    查看Aspect信息

    中途可通过bit show me/aspects/hello-world查看信息

    通过内置模板创建扩展

    • 通过bit templates查看内置模板
    • 通过`bit create <template> <custom-name> [--scope scope-name]
    • 通过bit install安装模板相关依赖
    • 通过bit status查看自定义扩展状态
    • 若有依赖缺失报错,将缺失依赖添加到:
      "teambit.dependencies/dependency-resolver": {
        /**
         * choose the package manager for Bit to use. you can choose between 'yarn', 'pnpm'
         */
        "packageManager": "teambit.dependencies/pnpm",
        "policy": {
          "dependencies": {},
          "peerDependencies": {
            "react": "~17.0.2",
            "@testing-library/react": "~12.1.2"
          }
        }
      },
    
    

    bit install补充安装依赖。

    • 通过bit start --dev测试。

    相关文章

      网友评论

          本文标题:Bit 组件共享

          本文链接:https://www.haomeiwen.com/subject/afhstrtx.html