UNPKG

@easytool/react-permission

Version:
355 lines (288 loc) 11.2 kB
import React, { useContext, useState, useEffect, Children } from 'react'; import _slicedToArray from '@babel/runtime/helpers/slicedToArray'; import _typeof from '@babel/runtime/helpers/typeof'; import PropTypes from 'prop-types'; var PermissionContext = /*#__PURE__*/React.createContext(null); /** * @desc 封装了一些项目常用方法. */ // 内部函数, 用于判断对象类型 function _getClass(object) { return Object.prototype.toString.call(object).match(/^\[object\s(.*)\]$/)[1]; } function isArray(obj) { return _getClass(obj).toLowerCase() === 'array'; } function isString(obj) { return _getClass(obj).toLowerCase() === 'string'; } function isObject(obj) { return _getClass(obj).toLowerCase() === 'object'; } function isNumber(obj) { return _getClass(obj).toLowerCase() === 'number'; } function isPromise(obj) { return !!obj && (_typeof(obj) === 'object' || typeof obj === 'function') && typeof obj.then === 'function'; } /** * @desc 判断参数是否为空, 包括null, undefined, [], '', {} * @param {object} obj 需判断的对象 */ function isEmpty(obj) { var empty = false; if (obj === null || obj === undefined) { // null and undefined empty = true; } else if ((isArray(obj) || isString(obj)) && obj.length === 0) { empty = true; } else if (isObject(obj)) { var hasProp = false; for (var prop in obj) { if (obj.hasOwnProperty(prop)) { hasProp = true; break; } } if (!hasProp) { empty = true; } } else if (isNumber(obj) && isNaN(obj)) { empty = true; } return empty; } function trim(value) { if (isString(value)) { return value.trim(); } return value; } /** * COPY from https://github1s.com/facebook/react/blob/HEAD/fixtures/legacy-jsx-runtimes/react-16/cjs/react-jsx-runtime.development.js * 参考 https://stackoverflow.com/questions/33199959/how-to-detect-a-react-component-vs-a-react-element */ // ATTENTION // When adding new symbols to this file, // Please consider also adding to 'react-devtools-shared/src/backend/ReactSymbols' // The Symbol used to tag the ReactElement-like types. If there is no native Symbol // nor polyfill, then a plain number is used for performance. // 老版本(兼容性处理) var REACT_ELEMENT_TYPE = 0xeac7; if (typeof Symbol === 'function' && Symbol.for) { var symbolFor = Symbol.for; REACT_ELEMENT_TYPE = symbolFor('react.element'); symbolFor('react.portal'); symbolFor('react.fragment'); symbolFor('react.strict_mode'); symbolFor('react.profiler'); symbolFor('react.provider'); symbolFor('react.context'); symbolFor('react.forward_ref'); symbolFor('react.suspense'); symbolFor('react.suspense_list'); symbolFor('react.memo'); symbolFor('react.lazy'); symbolFor('react.block'); symbolFor('react.server.block'); symbolFor('react.fundamental'); symbolFor('react.scope'); symbolFor('react.opaque.id'); symbolFor('react.debug_trace_mode'); symbolFor('react.offscreen'); symbolFor('react.legacy_hidden'); } /* $$typeof: Symbol(react.element) */ function isReactElement(node) { return /*#__PURE__*/React.isValidElement(node) && (node === null || node === void 0 ? void 0 : node.$$typeof) === REACT_ELEMENT_TYPE; } function formatPermission(value) { var permissions = []; if (isArray(value)) { permissions = value; } else if (isNumber(value)) { permissions.push(value); } else if (isString(value)) { if (value.includes(',')) { // TODO: 移除开头和结尾的 "," permissions = value.split(','); } else { permissions.push(value); } } return permissions; } function generateKey(element) { var index = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; // var key = ''; // if (isReactDOMElement(element)) { // key = element.type; // } else if (isReactComponentElement(element)) { // key = element.type.name; // } // TODO: key有点多余, 先这样吧 return "permission__".concat(index); } function comparePermission() { var elementPermission = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; var hasPermission = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; if (elementPermission.length === 0) { return true; } if (elementPermission.length > hasPermission.length) { return false; } var _loop = function _loop(i) { var requiredPermission = elementPermission[i]; // Compare permission var allow = hasPermission.some(function (userPermission) { return trim(requiredPermission) == trim(userPermission); }); if (!allow) { return { v: false }; } }; for (var i = 0; i < elementPermission.length; i++) { var _ret = _loop(i); if (_typeof(_ret) === "object") return _ret.v; } return true; } // 递归遍历 Virtual Tree, 首次是 Permission.props.children, 然后根据用户自定义来 function filterChildren(element, hasPermission, props) { var index = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 0; if (!element) { return null; } // isReactDOMElement: DOMElement. // isReactComponent: classComponent, functionComponent, portal. // isReactWrapper: fragment, suspense. // isReactHOC: memo, lazy, forward_ref. // if (isReactDOMElement(element) || isReactComponent(element) || isReactWrapper(element) || isReactHOC(element)) { if (isReactElement(element)) { if (checkElementPermission(element, hasPermission, props)) { // TODO: element没有子元素, 说明是通过 props 传递的. var children = element.props.children; if (Children.count(children) === 0) { return element; } var validChildren = filterChildren(children, hasPermission, props, index); var newChildren = handleChildren(validChildren, children); // cloneElement(element, props, children), 第二个, 第三个参数用于覆盖拷贝的 element 属性, 如果不输入默认使用原 element 的. // key and ref from the original element will be preserved. var newElement = /*#__PURE__*/React.cloneElement(element, { key: element.key || generateKey(element, index) }, newChildren); // 返回权限过滤后的元素. return newElement; } // 如果用户权限还未加载完成, 默认隐藏所有元素不显示, 不执行 onDeny 方法 // isEmpty(permission) && isPromise(hasPermission) 表示当前用户权限为 pending 状态 if (isEmpty(hasPermission) && isPromise(props.hasPermission)) { return null; } var onDeny = getPropertyValueByNames(element, ['onDeny', 'deny', 'data-ondeny', 'data-deny']); return handleDeny(element, onDeny || props.onDeny, index); // 处理 Array } else if (isArray(element)) { var _validChildren = []; // 有效的子元素 // 这里筛选出有效的子元素 Children.forEach(element, function (child, _index) { // checkedChild 已校验过的子元素 var checkedChild = filterChildren(child, hasPermission, props, _index); checkedChild && _validChildren.push(checkedChild); }); return _validChildren; } // TODO: 其他元素类型暴露方法让用户自己处理, 默认不处理 return element; } function checkElementPermission(element, hasPermission, props) { var comparePermission = props.comparePermission; var elementPermission = getPropertyValueByNames(element, ['permission', 'permissions', 'data-permission', 'data-permissions']); // 元素需要的权限, 空的表示不需要权限 if (isEmpty(elementPermission)) { return true; } // 用户的权限, 空的表示没有权限或还没获取到权限 if (isEmpty(hasPermission)) { return false; } return comparePermission(formatPermission(elementPermission), formatPermission(hasPermission)); } function getPropertyValueByNames() { var element = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var names = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; for (var i = 0; i < names.length; i++) { var value = element.props[names[i]]; // TODO: 方法优化 if (value || value === 0) { return value; } } return null; } function handleChildren(newChildren, oldChildren) { // children 为数组时 react会检测 key 是否为空, 为空会报警告. if (!newChildren || newChildren.length === 0) { newChildren = null; // 确保 newChildren 和 children 数据类型保持一致(object 或 array), 如果类型被修改, // 会导致 react 以为子组件被替换了, 将卸载已 render 的子组件(componentWillUnmount)并且重新渲染新的子组件(componentDidMount). } else if (newChildren.length === 1 && oldChildren.length === 1) { newChildren = newChildren[0]; } return newChildren; } function handleDeny(element, onDeny, index) { if (!onDeny) { return null; } if ( /*#__PURE__*/React.isValidElement(onDeny)) { return onDeny; } if (typeof onDeny === 'function') { // TODO: 这里可能会有性能问题, 如果 onDeny 方法直接执行页面跳转, 可能来不及清除内存占用. var newElement = onDeny(element, index); // 返回值类型必须是有效的React元素 if ( /*#__PURE__*/React.isValidElement(newElement)) { return newElement; } // 非 null, undefined, 0 if (newElement) { throw 'onDeny return a invalid react element.'; } } return null; } function render(props, permission) { var children = props.children, hasPermission = props.hasPermission, onLoad = props.onLoad; // isEmpty(permission) && isPromise(hasPermission) 表示当前用户权限为 pending 状态 if (isEmpty(permission) && isPromise(hasPermission) && /*#__PURE__*/React.isValidElement(onLoad)) { return onLoad; } var newChildren = filterChildren(children, permission, props); return newChildren; } function Permission(_props) { var props = Object.assign({ hasPermission: null, comparePermission: comparePermission, onLoad: null, onDeny: null, onError: null }, useContext(PermissionContext), _props); // TODO: 增加状态 pending, fulfill 状态. var _useState = useState(isPromise(props.hasPermission) ? null : props.hasPermission), _useState2 = _slicedToArray(_useState, 2), hasPermission = _useState2[0], setHasPermission = _useState2[1]; useEffect(function () { if (isPromise(props.hasPermission)) { props.hasPermission.then(function (permission) { return setHasPermission(permission); }, props.onError); } else { setHasPermission(props.hasPermission); } }, [props.hasPermission]); return render(props, hasPermission); } Permission.propTypes = { hasPermission: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.array, PropTypes.func, PropTypes.object]), comparePermission: PropTypes.func, onLoad: PropTypes.object, onDeny: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), onError: PropTypes.func }; export default Permission; export { PermissionContext };