【JSX配置及DIM】(二)JSX运行时DIM

123 阅读2分钟

继续上一篇 目前先记录过程,后续补充说明

自定义运行时

src 目录, 新增 es-runtime.ts 文件,用于自定义 jsx 运行时相关的方法,完整如下:

/**
 * 创建 DOM 元素
 * @param tag
 * @param props
 * @param children
 * @returns
 */
export function createElement(
  tag: string | Function,
  props: Record<string, any>,
  ...children: (HTMLElement | HTMLElement[] | string)[]
): HTMLElement | string {
  // 支持函数组件
  if (typeof tag === "function") {
    return tag(props, children);
  }

  const element = document.createElement(tag);

  // 添加属性
  for (let [name, val] of Object.entries(props ?? {})) {
    if (name.startsWith("on") && name.toLowerCase() in window) {
      element.addEventListener(name.toLowerCase().substring(2), val);
    } else if (name === "ref") {
      val(element);
    } else if (name === "style") {
      Object.assign(element.style, val);
    } else if (val === true) {
      element.setAttribute(name, name);
    } else if (val !== false && val != null) {
      element.setAttribute(name, val);
    } else if (val === false) {
      element.removeAttribute(name);
    }
  }

  // 添加子元素
  children.forEach((child) => {
    appendChild(element, child);
  });

  return element;
}

/**
 * 创建元素片段
 * @param _tag
 * @param children
 * @returns
 */
export function createFragment(
  _tag: string,
  ...children: (HTMLElement | string)[]
): (HTMLElement | string)[] {
  return children;
}

/**
 * 挂载子元素
 * @param parent
 * @param child
 * @returns
 */
export function appendChild(
  parent: HTMLElement,
  child: JSX.Children | (() => JSX.Children)
): any {
  if (typeof child === "function") return appendChild(parent, child());
  if (Array.isArray(child)) {
    return child.forEach((nestedChild) => appendChild(parent, nestedChild));
  }
  if (typeof child === "string") {
    return parent.appendChild(document.createTextNode(child));
  }
  return parent.appendChild(
    (child as HTMLElement).nodeType === null
      ? document.createTextNode(child.toString())
      : (child as Node)
  );
}

export function initRuntime() {
  window.$createElement = createElement;
  window.$createFragment = createFragment;
}

配置使用运行时

JSX 全局接口定义

src/vite-env.d.ts

/// <reference types="vite/client" />
declare interface Window {
  $createElement(
    tag: string | Function,
    props: Record<string, any>,
    ...children: (HTMLElement | HTMLElement[] | string)[]
  ): HTMLElement | string;

  $createFragment(
    tag: string,
    ...children: (HTMLElement | HTMLElement[] | string)[]
  ): (HTMLElement | string)[];
}

declare namespace JSX {
  interface IntrinsicElements {
    div: Element;
  }

  type Element = HTMLElement;

  type Children = HTMLElement | HTMLElement[] | string;
}

使用 tsc 编译时的配置

完整 tsconfig.json,修改项见注释

{
  "compilerOptions": {
    "target": "ESNext",
    "useDefineForClassFields": true,
    "module": "ESNext",
    "lib": [
      "ESNext",
      "DOM"
    ],
    "moduleResolution": "Node",
    "strict": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "esModuleInterop": true,
    "noEmit": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "skipLibCheck": true,
    "jsx": "react", // 如果不配置这里不会触发编译
    "jsxFactory": "$createElement", // 运行时自定义的方法
    "jsxFragmentFactory": "$createFragment" // 运行时自定义的方法
  },
  "include": [
    "src"
  ]
}

使用 Vite(esbuild) 编译时的配置

待补充

示例的TSX文件

src/components/SXEample.tsx

const SXExample = () => (
  <>
    <div>
      Hi SX
      <>
        <p>row one</p>
        <p>row two</p>
      </>
    </div>
  </>
);

export default SXExample;

main.ts

import "./style.css";
import typescriptLogo from "./typescript.svg";
import { setupCounter } from "./counter";
import SXExample from "./components/SXExample";
import { appendChild, initRuntime } from "./sx-runtime";

// 初始化运行时
initRuntime();

const app = document.querySelector<HTMLDivElement>("#app");
app!.innerHTML = `
  <div>
    <a href="https://vitejs.dev" target="_blank">
      <img src="/vite.svg" class="logo" alt="Vite logo" />
    </a>
    <a href="https://www.typescriptlang.org/" target="_blank">
      <img src="${typescriptLogo}" class="logo vanilla" alt="TypeScript logo" />
    </a>
    <h1>Vite + TypeScript</h1>
    <div class="card">
      <button id="counter" type="button"></button>
    </div>
    <p class="read-the-docs">
      Click on the Vite and TypeScript logos to learn more
    </p>
  </div>
`;

// 挂载 dom
appendChild(app!, SXExample);

setupCounter(document.querySelector<HTMLButtonElement>("#counter")!);

运行

jsx基本解析正常

开始 DIM runtime

以下待完善

支持语句模板,例如

/// jsx
const ShowExample = ()=>(
    <show when={true} fallback={<div>这是隐藏时的内容</div>}>
        <div>这是显示的内容</div>
    </show>
)
const EachEaxmple = ()=>(
    <each src={dataList}>
        <Item ...data />
    </each>
)

支持自动移除 DOM 事件监听

支持响应式数据

观察数据/生命周期