diff --git a/packages/inula-router/src/history/baseHistory.ts b/packages/inula-router/src/history/baseHistory.ts index 87451567..0b1bffa0 100644 --- a/packages/inula-router/src/history/baseHistory.ts +++ b/packages/inula-router/src/history/baseHistory.ts @@ -66,7 +66,9 @@ export function getBaseHistory( Object.assign(historyProps, nextState); } historyProps.length = browserHistory.length; - const args = { location: historyProps.location, action: historyProps.action }; + // 避免location饮用相同时setState不触发 + const location = Object.assign({}, historyProps.location); + const args = { location: location, action: historyProps.action }; transitionManager.notifyListeners(args); }; } diff --git a/packages/inula-router/src/history/hashHistory.ts b/packages/inula-router/src/history/hashHistory.ts index 6deb725e..8ce51b0a 100644 --- a/packages/inula-router/src/history/hashHistory.ts +++ b/packages/inula-router/src/history/hashHistory.ts @@ -57,6 +57,13 @@ export function createHashHistory(option: HashHistoryOptio const pathDecoder = addHeadSlash; const pathEncoder = hashType === 'slash' ? addHeadSlash : stripHeadSlash; + const startLocation = getHashContent(window.location.href); + const encodeLocation = pathEncoder(startLocation); + // 初始化hash格式不合法时会重定向 + if (startLocation !== encodeLocation) { + window.location.replace(stripHash(window.location.href) + '#' + encodeLocation); + } + function getLocation() { let hashPath = pathDecoder(getHashContent(window.location.hash)); if (basename) { diff --git a/packages/inula-router/src/router/matcher/__tests__/parser.test.ts b/packages/inula-router/src/router/matcher/__tests__/parser.test.ts index 91493cf6..e2a779d7 100644 --- a/packages/inula-router/src/router/matcher/__tests__/parser.test.ts +++ b/packages/inula-router/src/router/matcher/__tests__/parser.test.ts @@ -121,7 +121,13 @@ describe('parser test', () => { it('url without end slash match wildcard', function () { const parser = createPathParser('/about/', { strictMode: false }); const matched = parser.parse('/about'); - expect(matched).toBeNull(); + expect(matched).toStrictEqual({ + path: '/about/', + url: '/about', + score: [10], + isExact: true, + param: {}, + }); }); it('url without end slash match wildcard (strictMode)', function () { @@ -259,7 +265,7 @@ describe('parser test', () => { }); it('dynamic param with complex regexp pattern', () => { - const parser = createPathParser('/detail/:action([\\da-z]+)', { exact: true }); + const parser = createPathParser('/detail/:action([\\da-z]+)', { exact: true, caseSensitive: true }); const res = parser.parse('/detail/a123'); expect(res).toEqual({ isExact: true, diff --git a/packages/inula-router/src/router/matcher/parser.ts b/packages/inula-router/src/router/matcher/parser.ts index 9ea88fa7..1ddbbb5c 100644 --- a/packages/inula-router/src/router/matcher/parser.ts +++ b/packages/inula-router/src/router/matcher/parser.ts @@ -97,12 +97,14 @@ export function createPathParser

(pathname: string, option: ParserOp const token = tokens[tokenIdx]; const nextToken = tokens[tokenIdx + 1]; switch (token.type) { - case TokenType.Delimiter: - { - const hasOptional = lookToNextDelimiter(tokenIdx + 1); - pattern += `/${hasOptional ? '?' : ''}`; - } + case TokenType.Delimiter: { + // 该分隔符后有可选参数则该分割符在匹配时是可选的 + const hasOptional = lookToNextDelimiter(tokenIdx + 1); + // 该分割符为最后一个且strictMode===false时,该分隔符在匹配时是可选的 + const isSlashOptional = nextToken === undefined && !strictMode; + pattern += `/${hasOptional || isSlashOptional ? '?' : ''}`; break; + } case TokenType.Static: pattern += token.value.replace(REGEX_CHARS_RE, '\\$&'); if (nextToken && nextToken.type === TokenType.Pattern) { @@ -112,32 +114,31 @@ export function createPathParser

(pathname: string, option: ParserOp } scores.push(MatchScore.static); break; - case TokenType.Param: - { - // 动态参数支持形如/:param、/:param*、/:param?、/:param(\\d+)的形式 - let paramRegexp = ''; - if (nextToken) { - switch (nextToken.type) { - case TokenType.LBracket: - // 跳过当前Token和左括号 - tokenIdx += 2; - while (tokens[tokenIdx].type !== TokenType.RBracket) { - paramRegexp += tokens[tokenIdx].value; - tokenIdx++; - } - paramRegexp = `(${paramRegexp})`; - break; - case TokenType.Pattern: + case TokenType.Param: { + // 动态参数支持形如/:param、/:param*、/:param?、/:param(\\d+)的形式 + let paramRegexp = ''; + if (nextToken) { + switch (nextToken.type) { + case TokenType.LBracket: + // 跳过当前Token和左括号 + tokenIdx += 2; + while (tokens[tokenIdx].type !== TokenType.RBracket) { + paramRegexp += tokens[tokenIdx].value; tokenIdx++; - paramRegexp += `(${nextToken.value === '*' ? '.*' : BASE_PARAM_PATTERN})${nextToken.value}`; - break; - } + } + paramRegexp = `(${paramRegexp})`; + break; + case TokenType.Pattern: + tokenIdx++; + paramRegexp += `(${nextToken.value === '*' ? '.*' : BASE_PARAM_PATTERN})${nextToken.value}`; + break; } - pattern += paramRegexp ? `(?:${paramRegexp})` : `(${BASE_PARAM_PATTERN})`; - keys.push(token.value); - scores.push(MatchScore.param); } + pattern += paramRegexp ? `(?:${paramRegexp})` : `(${BASE_PARAM_PATTERN})`; + keys.push(token.value); + scores.push(MatchScore.param); break; + } case TokenType.WildCard: keys.push(token.value); pattern += `((?:${BASE_PARAM_PATTERN})${onlyHasWildCard ? '?' : ''}(?:/(?:${BASE_PARAM_PATTERN}))*)`; @@ -215,16 +216,15 @@ export function createPathParser

(pathname: string, option: ParserOp } path += params[token.value]; break; - case TokenType.WildCard: - { - const wildCard = params['*']; - if (wildCard instanceof Array) { - path += wildCard.join('/'); - } else { - path += wildCard; - } + case TokenType.WildCard: { + const wildCard = params['*']; + if (wildCard instanceof Array) { + path += wildCard.join('/'); + } else { + path += wildCard; } break; + } case TokenType.Delimiter: path += token.value; break; diff --git a/packages/inula/__tests__/DomTest/Attribute.test.js b/packages/inula/__tests__/DomTest/Attribute.test.js index 27e5becb..a216bc5b 100644 --- a/packages/inula/__tests__/DomTest/Attribute.test.js +++ b/packages/inula/__tests__/DomTest/Attribute.test.js @@ -95,4 +95,16 @@ describe('Dom Attribute', () => { Inula.render(

, container); }).not.toThrow(); }); + + it('dangerouslySetInnerHTML和children同时设置,只渲染children', () => { + Inula.act(() => { + Inula.render( +
+ 123 +
, + container + ); + }); + expect(container.innerHTML).toBe('
123
'); + }); }); diff --git a/packages/inula/src/dom/validators/ValidateProps.ts b/packages/inula/src/dom/validators/ValidateProps.ts index 2a6b27e1..18cf48c3 100644 --- a/packages/inula/src/dom/validators/ValidateProps.ts +++ b/packages/inula/src/dom/validators/ValidateProps.ts @@ -17,6 +17,25 @@ import { getPropDetails, PROPERTY_TYPE, PropDetails } from './PropertiesData'; const INVALID_EVENT_NAME_REGEX = /^on[^A-Z]/; +const voidTagElements = [ + 'area', + 'base', + 'br', + 'col', + 'embed', + 'hr', + 'img', + 'input', + 'keygen', + 'link', + 'meta', + 'param', + 'source', + 'track', + 'wbr', + 'menuitem', +]; + // 是内置元素 export function isNativeElement(tagName: string, props: Record) { return !tagName.includes('-') && props.is === undefined; @@ -108,6 +127,18 @@ export function validateProps(type, props) { throw new Error('style should be a object.'); } + // 对于没有children的元素,设置dangerouslySetInnerHTML不生效 + if (voidTagElements.includes(type)) { + if (props.dangerouslySetInnerHTML != null) { + delete props.dangerouslySetInnerHTML; + } + } + + // dangerouslySetInnerHTML和children同时设置,只渲染children + if (props.dangerouslySetInnerHTML != null && props.children != null) { + delete props.dangerouslySetInnerHTML; + } + if (isDev) { // 校验属性 const invalidProps = Object.keys(props).filter(key => !isValidProp(type, key, props[key])); diff --git a/packages/inula/src/index.ts b/packages/inula/src/index.ts index 116ab1f2..33a3eaba 100644 --- a/packages/inula/src/index.ts +++ b/packages/inula/src/index.ts @@ -72,6 +72,8 @@ import { import { syncUpdates as flushSync } from './renderer/TreeBuilder'; import { toRaw } from './inulax/proxy/ProxyHandler'; +const version = __VERSION__; + const Inula = { Children, createRef, @@ -122,9 +124,9 @@ const Inula = { Profiler, StrictMode, Suspense, + version, }; -export const version = __VERSION__; export { Children, createRef, @@ -178,6 +180,7 @@ export { Profiler, StrictMode, Suspense, + version, }; export * from './types'; diff --git a/packages/inula/src/renderer/TreeBuilder.ts b/packages/inula/src/renderer/TreeBuilder.ts index c29126fb..c7a76bee 100644 --- a/packages/inula/src/renderer/TreeBuilder.ts +++ b/packages/inula/src/renderer/TreeBuilder.ts @@ -200,6 +200,10 @@ function getChildByIndex(vNode: VNode, idx: number) { // 从多个更新节点中,计算出开始节点。即:找到最近的共同的父辈节点 export function calcStartUpdateVNode(treeRoot: VNode) { const toUpdateNodes = Array.from(treeRoot.toUpdateNodes!); + // 所有待更新元素的parent为null说明该node的父元素已经被卸载,应该从根节点发起更新 + if (toUpdateNodes.every(node => node.parent === null)) { + return treeRoot; + } if (toUpdateNodes.length === 0) { return treeRoot; diff --git a/packages/inula/src/renderer/render/ClassComponent.ts b/packages/inula/src/renderer/render/ClassComponent.ts index 2792425c..ec1a4857 100644 --- a/packages/inula/src/renderer/render/ClassComponent.ts +++ b/packages/inula/src/renderer/render/ClassComponent.ts @@ -145,7 +145,12 @@ export function captureRender(processing: VNode): VNode | null { processUpdates(processing, inst, nextProps); // 如果 props, state, context 都没有变化且 isForceUpdate 为 false则不需要更新 - shouldUpdate = oldProps !== processing.props || inst.state !== processing.state || processing.isForceUpdate; + shouldUpdate = + oldProps !== processing.props || + inst.state !== processing.state || + processing.isForceUpdate || + // 响应式状态管理器中的值变化,需要更新 + processing.isStoreChange; if (shouldUpdate) { // derivedStateFromProps会修改nextState,因此需要调用 @@ -167,7 +172,7 @@ export function captureRender(processing: VNode): VNode | null { } // 如果捕获了 error,必须更新 const isCatchError = (processing.flags & DidCapture) === DidCapture; - shouldUpdate = isCatchError || shouldUpdate || processing.isStoreChange; + shouldUpdate = isCatchError || shouldUpdate; // 更新ref markRef(processing);