Skip to content

Commit

Permalink
feat(tag): tag该用jsx编写 (#1270)
Browse files Browse the repository at this point in the history
* feat(tag): tag该用jsx编写

* refactor(tag): 删除旧tag.vue文件
  • Loading branch information
dexterBo authored Apr 11, 2024
1 parent 023acde commit 303b631
Show file tree
Hide file tree
Showing 7 changed files with 347 additions and 100 deletions.
2 changes: 1 addition & 1 deletion babel.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@ module.exports = {
},
],
],
plugins: ['@babel/plugin-transform-runtime', '@babel/plugin-proposal-class-properties'],
plugins: ['@babel/plugin-transform-runtime', '@babel/plugin-proposal-class-properties', '@vue/babel-plugin-jsx'],
};
126 changes: 126 additions & 0 deletions src/hooks/render-tnode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { h, ComponentPublicInstance, VNode, isVNode } from 'vue';
import isEmpty from 'lodash/isEmpty';
import isString from 'lodash/isString';
import isFunction from 'lodash/isFunction';
import isObject from 'lodash/isObject';
import camelCase from 'lodash/camelCase';
import kebabCase from 'lodash/kebabCase';

export interface JSXRenderContext {
defaultNode?: VNode | string;
params?: Record<string, any>;
slotFirst?: boolean;
// 是否不打印 LOG
silent?: boolean;
}

export type OptionsType = VNode | JSXRenderContext | string;

export function getDefaultNode(options?: OptionsType) {
let defaultNode;
if (isObject(options) && 'defaultNode' in options) {
defaultNode = options.defaultNode;
} else if (isVNode(options) || isString(options)) {
defaultNode = options;
}

return defaultNode;
}

export function getParams(options?: OptionsType) {
return isObject(options) && 'params' in options ? options.params : {};
}

export function getSlotFirst(options?: OptionsType) {
return isObject(options) && 'slotFirst' in options ? options.slotFirst : {};
}

// 同时支持驼峰命名和中划线命名的插槽,示例:value-display 和 valueDisplay
export function handleSlots(instance: ComponentPublicInstance, params: Record<string, any>, name: string) {
// 检查是否存在 驼峰命名 的插槽
let node = instance.$slots[camelCase(name)]?.(params);
if (node) return node;
// 检查是否存在 中划线命名 的插槽
node = instance.$slots[kebabCase(name)]?.(params);
if (node) return node;
return null;
}

/**
* 通过JSX的方式渲染 TNode,props 和 插槽同时处理,也能处理默认值为 true 则渲染默认节点的情况
* @param vm 组件实例
* @param name 插槽和属性名称
* @param options 值可能为默认渲染节点,也可能是默认渲染节点和参数的集合
* @example renderTNodeJSX(this, 'closeBtn') 优先级 props function 大于 插槽
* @example renderTNodeJSX(this, 'closeBtn', <close-icon />)。 当属性值为 true 时则渲染 <close-icon />
* @example renderTNodeJSX(this, 'closeBtn', { defaultNode: <close-icon />, params })。 params 为渲染节点时所需的参数
*/
export const renderTNodeJSX = (instance: ComponentPublicInstance, name: string, options?: OptionsType) => {
// assemble params && defaultNode
const params = getParams(options);
const defaultNode = getDefaultNode(options);

// 处理 props 类型的Node
let propsNode;
if (name in instance) {
propsNode = instance[name];
}

// 是否静默日志
// const isSilent = Boolean(isObject(options) && 'silent' in options && options.silent);
// // 同名插槽和属性同时存在,则提醒用户只需要选择一种方式即可
// if (instance.$slots[name] && propsNode && propsNode !== true && !isSilent) {
// console.warn(`Both $slots.${name} and $props.${name} exist, $props.${name} is preferred`);
// }

// propsNode 为 false 不渲染
if (propsNode === false || propsNode === null) return;
if (propsNode === true && defaultNode) {
return handleSlots(instance, params, name) || defaultNode;
}

// 同名 props 和 slot 优先处理 props
if (isFunction(propsNode)) return propsNode(h, params);
const isPropsEmpty = [undefined, params, ''].includes(propsNode);
// Props 为空,但插槽存在
if (isPropsEmpty && (instance.$slots[camelCase(name)] || instance.$slots[kebabCase(name)])) {
return handleSlots(instance, params, name);
}
return propsNode;
};

/**
* 通过JSX的方式渲染 TNode,props 和 插槽同时处理。与 renderTNodeJSX 区别在于 属性值为 undefined 时会渲染默认节点
* @param vm 组件实例
* @param name 插槽和属性名称
* @example renderTNodeJSX(this, 'closeBtn')
* @example renderTNodeJSX(this, 'closeBtn', <close-icon />)。this.closeBtn 为空时,则兜底渲染 <close-icon />
* @example renderTNodeJSX(this, 'closeBtn', { defaultNode: <close-icon />, params }) 。params 为渲染节点时所需的参数
*/
export const renderTNodeJSXDefault = (vm: ComponentPublicInstance, name: string, options?: OptionsType) => {
const defaultNode = getDefaultNode(options);
return renderTNodeJSX(vm, name, options) || defaultNode;
};

/**
* 用于处理相同名称的 TNode 渲染
* @param vm 组件实例
* @param name1 第一个名称,优先级高于 name2
* @param name2 第二个名称
* @param defaultNode 默认渲染内容:当 name1 和 name2 都为空时会启动默认内容渲染
* @example renderContent(this, 'default', 'content')
* @example renderContent(this, 'default', 'content', '我是默认内容')
* @example renderContent(this, 'default', 'content', { defaultNode: '我是默认内容', params })
*/
export const renderContent = (vm: ComponentPublicInstance, name1: string, name2: string, options?: OptionsType) => {
const params = getParams(options);
const defaultNode = getDefaultNode(options);

const toParams = params ? { params } : undefined;

const node1 = renderTNodeJSX(vm, name1, toParams);
const node2 = renderTNodeJSX(vm, name2, toParams);

const res = isEmpty(node1) ? node2 : node1;
return isEmpty(res) ? defaultNode : res;
};
126 changes: 126 additions & 0 deletions src/hooks/tnode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { h, getCurrentInstance, ComponentInternalInstance, VNode } from 'vue';
import isFunction from 'lodash/isFunction';
import camelCase from 'lodash/camelCase';
import kebabCase from 'lodash/kebabCase';
import { getDefaultNode, getParams, OptionsType, JSXRenderContext, getSlotFirst } from './render-tnode';

// 兼容处理插槽名称,同时支持驼峰命名和中划线命名,示例:value-display 和 valueDisplay
function handleSlots(instance: ComponentInternalInstance, name: string, params: Record<string, any>) {
// 2023-08 new Function 触发部分使用场景安全策略问题(Chrome插件/eletron等)
// // 每个 slots 需要单独的 h 函数 否则直接assign会重复把不同 slots 的 params 都注入
// const finalParams = new Function('return ' + h.toString())();
// if (params) {
// Object.assign(finalParams, params);
// }

// 检查是否存在 驼峰命名 的插槽(过滤注释节点)
let node = instance.slots[camelCase(name)]?.(params);
if (node && node.filter((t) => t.type.toString() !== 'Symbol(v-cmt)').length) return node;
// 检查是否存在 中划线命名 的插槽
node = instance.slots[kebabCase(name)]?.(params);
if (node && node.filter((t) => t.type.toString() !== 'Symbol(v-cmt)').length) return node;
return null;
}

/**
* 是否为空节点,需要过滤掉注释节点。注释节点也会被认为是空节点
*/
function isEmptyNode(node: any) {
if ([undefined, null, ''].includes(node)) return true;
const innerNodes = node instanceof Array ? node : [node];
const r = innerNodes.filter((node) => node?.type?.toString() !== 'Symbol(Comment)');
return !r.length;
}

/**
* 通过 JSX 的方式渲染 TNode,props 和 插槽同时处理,也能处理默认值为 true 则渲染默认节点的情况
* 优先级:Props 大于插槽
* 如果 props 值为 true ,则使用插槽渲染。如果也没有插槽的情况下,则使用 defaultNode 渲染
* @example const renderTNodeJSX = useTNodeJSX()
* @return () => {}
* @param name 插槽和属性名称
* @param options 值可能为默认渲染节点,也可能是默认渲染节点和参数的集合
* @example renderTNodeJSX('closeBtn') 优先级 props function 大于 插槽
* @example renderTNodeJSX('closeBtn', <close-icon />)。 当属性值为 true 时则渲染 <close-icon />
* @example renderTNodeJSX('closeBtn', { defaultNode: <close-icon />, params })。 params 为渲染节点时所需的参数
*/
export const useTNodeJSX = () => {
const instance = getCurrentInstance();
return function (name: string, options?: OptionsType) {
// assemble params && defaultNode
const params = getParams(options);
const defaultNode = getDefaultNode(options);
const slotFirst = getSlotFirst(options);

// 处理 props 类型的Node
let propsNode;
if (Object.keys(instance.props).includes(name)) {
propsNode = instance.props[name];
}

// 是否静默日志
// const isSilent = Boolean(isObject(options) && 'silent' in options && options.silent);
// // 同名插槽和属性同时存在,则提醒用户只需要选择一种方式即可
// if (instance.slots[name] && propsNode && propsNode !== true && !isSilent) {
// log.warn('', `Both slots.${name} and props.${name} exist, props.${name} is preferred`);
// }
// propsNode 为 false 不渲染
if (propsNode === false || propsNode === null) return;
if (propsNode === true) {
return handleSlots(instance, name, params) || defaultNode;
}

// 同名 props 和 slot 优先处理 props
if (isFunction(propsNode)) return propsNode(h, params);
const isPropsEmpty = [undefined, params, ''].includes(propsNode);
if ((isPropsEmpty || slotFirst) && (instance.slots[camelCase(name)] || instance.slots[kebabCase(name)])) {
return handleSlots(instance, name, params);
}
return propsNode;
};
};

/**
* 在setup中,通过JSX的方式 TNode,props 和 插槽同时处理。与 renderTNodeJSX 区别在于属性值为 undefined 时会渲染默认节点
* @example const renderTNodeJSXDefault = useTNodeDefault()
* @return () => {}
* @param name 插槽和属性名称
* @example renderTNodeJSXDefault('closeBtn')
* @example renderTNodeJSXDefault('closeBtn', <close-icon />) closeBtn 为空时,则兜底渲染 <close-icon />
* @example renderTNodeJSXDefault('closeBtn', { defaultNode: <close-icon />, params }) 。params 为渲染节点时所需的参数
*/
export const useTNodeDefault = () => {
const renderTNodeJSX = useTNodeJSX();
return function (name: string, options?: VNode | JSXRenderContext) {
const defaultNode = getDefaultNode(options);
return renderTNodeJSX(name, options) || defaultNode;
};
};

/**
* 在setup中,用于处理相同名称的 TNode 渲染
* @example const renderContent = useContent()
* @return () => {}
* @param name1 第一个名称,优先级高于 name2
* @param name2 第二个名称
* @param defaultNode 默认渲染内容:当 name1 和 name2 都为空时会启动默认内容渲染
* @example renderContent('default', 'content')
* @example renderContent('default', 'content', '我是默认内容')
* @example renderContent('default', 'content', { defaultNode: '我是默认内容', params })
*/
export const useContent = () => {
const renderTNodeJSX = useTNodeJSX();
return function (name1: string, name2: string, options?: VNode | JSXRenderContext) {
// assemble params && defaultNode
const params = getParams(options);
const defaultNode = getDefaultNode(options);

const toParams = params ? { params } : undefined;

const node1 = renderTNodeJSX(name1, toParams);
const node2 = renderTNodeJSX(name2, toParams);

const res = isEmptyNode(node1) ? node2 : node1;
return isEmptyNode(res) ? defaultNode : res;
};
};
Loading

0 comments on commit 303b631

Please sign in to comment.