原文自定义jsx解析工厂函数
问题
我们通常会在react项目中使用jsx,当我们习惯了jsx的语法之后,可能就脱离不开jsx了,如果我们不在react而是在一个原生js项目中也想使用jsx怎么办呢?
解决方案
react官方已经出了一个新的jsx解析器,剥离了jsx-runtime用来单独解析jsx,可以脱离react使用jsx,并且和babel、typescript官方合作,使用babel或者ts配置即可解析jsx,都提供了简洁的配置方案。或者,我们还可以自定义一个jsx解析函数。
方案一:Babel配置
- 先安装依赖包
npm update @babel/core @babel/preset-react
- 再配置
babel.config.json
文件
{
"presets": [
["@babel/preset-react", {
"runtime": "automatic"
}]
]
}
方案二:typescript配置
要在ts中使用jsx,即使用tsx。Typescript 4.1 支持 React 17的jsx和jsxs工厂函数的tsconfig.json
配置
- 开发环境下用
"jsx":"react-jsxdev"
- 生产环境下用
"jsx":"react-jsx"
比如:
// ./src/tsconfig.json
{
"compilerOptions": {
"module": "esnext",
"target": "es2015",
"jsx": "react-jsx",
"strict": true
},
"include": [
"./**/*"
]
}
方案三:自定义tsx工厂函数
自定义一套jsx解析工厂函数,可以了解jsx的解析过程。下面演示一个ts版本的tsx解析函数。
- 先定义一个
jsxFactory.ts
用作定义和导出工厂函数
// --- jsxFactory.ts ---
/* https://gist.github.com/borestad/eac42120613bc67a3714f115e8b485a7
* Custom jsx parser
* See: tsconfig.json
*
* {
* "jsx": "react",
* "jsxFactory": "h",
* "lib": [
* "es2017",
* "dom",
* "dom.iterable"
* ]
* }
*
*/
interface entityMapData {
[key: string]: string;
}
export const entityMap: entityMapData = {
"&": "amp",
"<": "lt",
">": "gt",
'"': "quot",
"'": "#39",
"/": "#x2F",
};
export const escapeHtml = (str: object[] | string) =>
String(str).replace(/[&<>"'\/\\]/g, (s) => `&${entityMap[s]};`);
// To keep some consistency with React DOM, lets use a mapper
// https://reactjs.org/docs/dom-elements.html
export const AttributeMapper = (val: string) =>
({
tabIndex: "tabindex",
className: "class",
readOnly: "readonly",
}[val] || val);
// tslint:disable-next-line:no-default-export
export function DOMcreateElement(
tag: Function | string,
attrs?: { [key: string]: any },
...children: (HTMLElement | string)[]
): HTMLElement {
attrs = attrs || {};
const stack: any[] = [...children];
// Support for components(ish)
if (typeof tag === "function") {
attrs.children = stack;
return tag(attrs);
}
const elm = document.createElement(tag);
// Add attributes
for (let [name, val] of Object.entries(attrs)) {
name = escapeHtml(AttributeMapper(name));
if (name.startsWith("on") && name.toLowerCase() in window) {
elm.addEventListener(name.toLowerCase().substr(2), val);
} else if (name === "ref") {
val(elm);
} else if (name === "style") {
Object.assign(elm.style, val);
} else if (val === true) {
elm.setAttribute(name, name);
} else if (val !== false && val != null) {
elm.setAttribute(name, escapeHtml(val));
} else if (val === false) {
elm.removeAttribute(name);
}
}
// Append children
while (stack.length) {
const child = stack.shift();
// Is child a leaf?
if (!Array.isArray(child)) {
elm.appendChild(
(child as HTMLElement).nodeType == null
? document.createTextNode(child.toString())
: child
);
} else {
stack.push(...child);
}
}
return elm;
}
export const DOMcreateFragment = (
attrs?: { [key: string]: any },
...children: (HTMLElement | string)[]
): (HTMLElement | string)[] => {
return children;
};
- 配套的工厂函数
d.ts
声明文件
// --- jsxFactory.d.ts ---
declare namespace JSX {
type Element = string;
interface IntrinsicElements {
[eleName: string]: any;
}
}
- 然后在
tsconfig.json
中加上jsx配置
{
"compilerOptions":{
// ...其他配置
"jsx": "preserve",
"jsxFactory": "DOMcreateElement",
"jsxFragmentFactory": "DOMcreateFragment",
}
}
比如下面这个参考
{
"compilerOptions": {
"rootDir": "src",
"outDir": "lib",
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ESNext", "dom", "dom.iterable"],
"moduleResolution": "Node",
"strict": true,
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"noEmit": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"declaration": true,
"declarationDir": "./lib",
"declarationMap": true,
"baseUrl": "./",
"jsx": "preserve",
"jsxFactory": "DOMcreateElement",
"jsxFragmentFactory": "DOMcreateFragment",
"allowJs": true
},
"include": ["./src"]
}
- 通常来说这样就可以使用了,在每一个.tsx结尾的文件中引入DOMcreateElement即可,但是如果你用的esbuild,你还可以在esbuild配置中自动注入DOMcreateElement和DOMcreateFragment
一个参考的esbuild配置,下面示例中@/helper/jsxFactory
是jsxFactory.ts所在目录
esbuild: {
jsxFactory: "DOMcreateElement",
jsxFragment: "DOMcreateFragment",
jsxInject: `import { DOMcreateElement, DOMcreateFragment } from '@/helper/jsxFactory';`,
}
方案四:简洁的jsx工厂函数
jsx简洁版本,可以依照这个简单版本做定制和扩展
版本一
const appendChild = (parent, child) => {
if (Array.isArray(child))
child.forEach((nestedChild) => appendChild(parent, nestedChild));
else
parent.appendChild(child.nodeType ? child : document.createTextNode(child));
};
export const DOMcreateElement = (tag, props, ...children) => {
if (typeof tag === 'function') return tag(props, children);
const element = document.createElement(tag);
Object.entries(props || {}).forEach(([name, value]) => {
if (name.startsWith('on') && name.toLowerCase() in window) {
element.addEventListener(name.toLowerCase().substr(2), value);
} else {
element[name] = value;
// element.setAttribute(name, value.toString());
}
});
children.forEach((child) => {
appendChild(element, child);
});
return element;
};
export const DOMcreateFragment = (props, ...children) => {
return children;
};
版本二
/**
* A helper function that ensures we won't work with null values
* @param val
* @param fallback
*/
function nonNull(val, fallback) {
return Boolean(val) ? val : fallback;
}
/**
* How do we handle children. Children can either be:
* 1. Calls to DOMcreateElement, return a Node
* 2. Text content, returns a Text
* @param children
*/
function DOMparseChildren(children) {
return children.map((child) => {
if (typeof child === 'string') {
return document.createTextNode(child);
}
return child;
});
}
/**
* How do we handle regular nodes.
* 1. We create an element
* 2. We apply all properties from JSX to this DOM node
* 3. If available,we append all children.
* @param element
* @param properties
* @param children
*/
function DOMparseNode(element, properties, children) {
const el = document.createElement(element);
Object.keys(nonNull(properties, {})).forEach((key) => {
el[key] = properties[key];
});
DOMparseChildren(children).forEach((child) => {
el.appendChild(child);
});
return el;
}
/**
* Our entry function.
* 1. Is the element a function,than it's a functional component.
* We call this function (pass props and children of course)
* and return the result.We expect a return value of type Node
* 2. If the element is a string, we parse a regular node
* @param element
* @param properties
* @param children
*/
export function DOMcreateElement(element, properties, ...children) {
if (typeof element === 'function') {
return element({
...nonNull(properties, {}),
children,
});
}
return DOMparseNode(element, properties, children);
}