feat(vue): add beforeRenderToNative hook to support computed style

This commit is contained in:
chestershen
2022-11-25 15:23:34 +08:00
committed by zoomchan-cxj
parent b28d02b010
commit 294e55e3cd
10 changed files with 198 additions and 63 deletions
+3
View File
@@ -32,6 +32,9 @@ export default {
babel: false,
extensions: false,
compileEnhancements: false,
nonSemVerExperiments: {
tryAssertion: true,
},
files: [
'packages/**/__tests__/*.test.js',
],
@@ -1,3 +1,23 @@
/*
* Tencent is pleased to support the open source community by making
* Hippy available.
*
* Copyright (C) 2017-2022 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import test from 'ava';
import translateColor from '../color-parser';
@@ -19,6 +39,7 @@ test('Test color translation', (t) => {
t.is(translateColor('transparent'), 0);
t.is(translateColor('blueviolet'), 4287245282);
t.is(translateColor(4287245282), 3808397867);
t.is(translateColor('var(-Bg)'), 'var(-Bg)');
});
test('Test color translation error handle NaN', (t) => {
@@ -20,7 +20,7 @@
/* eslint-disable no-mixed-operators */
const names = {
export const names = {
transparent: 0x00000000,
aliceblue: 0xf0f8ffff,
antiquewhite: 0xfaebd7ff,
@@ -346,6 +346,8 @@ function baseColor(color) {
* Translate the color to make sure native understood.
*/
function translateColor(color, options = {}) {
if (typeof color === 'string' && color.indexOf('var(') !== -1) return color;
let int32Color = baseColor(color);
if (int32Color === null) {
throw new Error(`Bad color value: ${color}`);
@@ -22,7 +22,7 @@ import crypto from 'crypto';
import { getOptions } from 'loader-utils';
import { GLOBAL_STYLE_NAME, GLOBAL_DISPOSE_STYLE_NAME } from '@vue/runtime/constants';
import parseCSS from './css-parser';
import translateColor from './color-parser';
import translateColor, { names as colorNames } from './color-parser';
let sourceId = 0;
@@ -43,7 +43,15 @@ function hippyVueCSSLoader(source) {
selectors: n.selectors,
declarations: n.declarations.map((dec) => {
let { value } = dec;
if (dec.property && dec.property.toLowerCase().indexOf('color') > -1) {
const isVariableColor = dec.property && dec.property.startsWith('-') && typeof value === 'string'
&& (
value.includes('#')
|| value.includes('rgb')
|| value.includes('hls')
|| value.includes('hue')
|| value.trim() in colorNames
);
if (dec.property && (dec.property.toLowerCase().indexOf('color') > -1 || isVariableColor)) {
value = translateColor(value, options);
}
return {
+16 -18
View File
@@ -450,27 +450,25 @@ function parseCSS(css, options) {
break;
}
case 'transform': {
const keyReg = /((\w+)\s*\()/;
const valueReg = /(?:\(['"]?)(.*?)(?:['"]?\))/;
const regex = /(\w+\s*)(?:\(['"]?)(.*?)(?:['"]?\))/g;
const oldValue = value;
value = [];
oldValue.split(' ').forEach((transformKeyValue) => {
if (keyReg.test(transformKeyValue)) {
const key = keyReg.exec(transformKeyValue)[2];
let v = valueReg.exec(transformKeyValue)[1];
if (v.indexOf('.') === 0) {
v = `0${v}`;
}
if (parseFloat(v).toString() === v) {
v = parseFloat(v);
}
const transform = {};
transform[key] = v;
value.push(transform);
} else {
error('missing \'(\'');
let group;
while (group = regex.exec(oldValue)) {
const key = group[1];
let v = group[2];
if (v.indexOf('.') === 0) {
v = `0${v}`;
}
});
if (parseFloat(v).toString() === v) {
v = parseFloat(v);
}
const transform = {};
transform[key] = v;
value.push(transform);
};
break;
}
case 'fontWeight':
@@ -0,0 +1,78 @@
/*
* Tencent is pleased to support the open source community by making
* Hippy available.
*
* Copyright (C) 2017-2022 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Most nodes test is executed in node-ops.test.js
// here just test the lacked testing for ViewNode for coverage.
import test, { before } from 'ava';
import ViewNode from '../view-node';
import ElementNode from '../element-node';
import * as util from '../../util';
before(() => {
global.__GLOBAL__ = {
nodeId: 101,
};
});
const logWhenFail = async (t, cb, err) => {
const tryTest = await t.try((t) => {
cb(t);
});
if (!tryTest.passed) {
t.log(err);
}
tryTest.commit();
};
test('ElementNode API', async (t) => {
await logWhenFail(t, (t) => {
const node = new ElementNode('div');
t.true(util.isFunction(node.appendChild));
t.true(util.isFunction(node.insertBefore));
t.true(util.isFunction(node.moveChild));
t.true(util.isFunction(node.removeChild));
t.true(util.isFunction(node.getBoundingClientRect));
t.true(node.classList instanceof Set);
t.true(node.style instanceof Object);
t.true(node.attributes instanceof Object);
}, 'ElementNode APIs have breaking changes, please update const variable \'BEFORE_RENDER_TO_NATIVE_HOOK_VERSION\'');
});
test('ViewNode API', async (t) => {
await logWhenFail(t, (t) => {
const node = new ViewNode();
t.true(util.isFunction(node.appendChild));
t.true(util.isFunction(node.insertBefore));
t.true(util.isFunction(node.moveChild));
t.true(util.isFunction(node.removeChild));
const childNode1 = new ViewNode();
const childNode2 = new ViewNode();
node.appendChild(childNode1);
node.appendChild(childNode2);
t.true(node.firstChild === childNode1);
t.true(node.lastChild === childNode2);
}, 'ViewNode APIs have breaking changes, please update const variable \'BEFORE_RENDER_TO_NATIVE_HOOK_VERSION\'');
});
+38 -17
View File
@@ -36,6 +36,7 @@ import {
isFunction,
capitalizeFirstLetter,
convertImageLocalPath,
getBeforeRenderToNative,
} from '../../util';
import {
isRTL,
@@ -279,11 +280,12 @@ function parseViewComponent(targetNode, nativeNode, style) {
}
// Change the ScrollView child collapsable attribute
if (nativeNode.name === 'ScrollView') {
if (targetNode.childNodes.length !== 1) {
const realNodes = targetNode.childNodes.filter(child => !child.meta.skipAddToDom);
if (realNodes.length !== 1) {
warn('Only one child node is acceptable for View with overflow');
}
if (targetNode.childNodes.length) {
targetNode.childNodes[0].setStyle('collapsable', false);
if (realNodes.length) {
realNodes[0].setStyle('collapsable', false);
}
}
if (style.backgroundImage) {
@@ -292,6 +294,27 @@ function parseViewComponent(targetNode, nativeNode, style) {
}
}
/**
* getElemCss
* @param {ElementNode} element
* @returns {{}}
*/
function getElemCss(element) {
const style = Object.create(null);
try {
getCssMap().query(element).selectors.forEach((matchedSelector) => {
if (!isStyleMatched(matchedSelector, element)) return;
matchedSelector.ruleSet.declarations.forEach((cssStyle) => {
style[cssStyle.property] = cssStyle.value;
});
});
} catch (err) {
console.error('getDomCss Error:', err);
}
return style;
};
/**
* Get target node attributes, use to chrome devTool tag attribute show while debugging
* @param targetNode
@@ -325,20 +348,8 @@ function renderToNative(rootViewId, targetNode) {
if (!targetNode.meta.component) {
throw new Error(`Specific tag is not supported yet: ${targetNode.tagName}`);
}
let style = {};
// Apply styles when the targetNode attach to document at first time.
if (targetNode.meta.component.defaultNativeStyle) {
style = { ...targetNode.meta.component.defaultNativeStyle };
}
// Apply styles from CSS
const matchedSelectors = getCssMap().query(targetNode);
matchedSelectors.selectors.forEach((matchedSelector) => {
if (!isStyleMatched(matchedSelector, targetNode)) return;
matchedSelector.ruleSet.declarations.forEach((cssStyle) => {
style[cssStyle.property] = cssStyle.value;
});
});
// Apply style from style attribute.
let style = getElemCss(targetNode);
style = { ...style, ...targetNode.style };
// Convert to real native event
@@ -359,6 +370,15 @@ function renderToNative(rootViewId, targetNode) {
}
});
}
getBeforeRenderToNative()(targetNode, style);
// use defaultNativeStyle later to avoid incorrect compute style from inherit node
// in beforeRenderToNative hook
if (targetNode.meta.component.defaultNativeStyle) {
style = { ...targetNode.meta.component.defaultNativeStyle, ...style };
}
// Translate to native node
const nativeNode = {
id: targetNode.nodeId,
@@ -516,4 +536,5 @@ export {
removeChild,
updateChild,
updateWithChildren,
getElemCss,
};
+17
View File
@@ -49,6 +49,7 @@ import {
isFunction,
trace,
setBeforeLoadStyle,
setBeforeRenderToNative,
} from '../util';
import DocumentNode from '../renderer/document-node';
import { Event } from '../renderer/native/event';
@@ -259,6 +260,22 @@ if (config.devtools && devtools) {
devtools.emit('init', Vue);
}
/*
* used to validate beforeRenderToNative hook
* when ElementNode or ViewNode have breaking changes, add version number to disable
* beforeRenderToNative hook
*/
const BEFORE_RENDER_TO_NATIVE_HOOK_VERSION = 1;
Vue.config._setBeforeRenderToNative = (hook, version) => {
if (isFunction(hook)) {
if (BEFORE_RENDER_TO_NATIVE_HOOK_VERSION === version) {
setBeforeRenderToNative(hook);
} else {
console.error('_setBeforeRenderToNative API had changed, the hook function will be ignored!');
}
}
};
// proxy Vue constructor to add Hippy Vue instance to global.__VUE_ROOT_INSTANCES__
const VueProxy = new Proxy(Vue, {
construct(Target, args) {
+1 -25
View File
@@ -26,11 +26,7 @@ import {
trace,
isFunction,
} from '../util';
import {
getCssMap,
} from '../renderer/native/index';
import { isStyleMatched } from '../util/node';
import { getElemCss } from '../renderer/native/index';
import BackAndroid from './backAndroid';
import * as NetInfo from './netInfo';
@@ -88,26 +84,6 @@ const measureInWindowByMethod = function measureInWindowByMethod(el, method) {
}));
};
/**
* getElemCss
* @param {ElementNode} element
* @returns {{}}
*/
const getElemCss = function getElemCss(element) {
const style = Object.create(null);
try {
getCssMap().query(element).selectors.forEach((matchedSelector) => {
if (!isStyleMatched(matchedSelector, element)) return;
matchedSelector.ruleSet.declarations.forEach((cssStyle) => {
style[cssStyle.property] = cssStyle.value;
});
});
} catch (err) {
console.error('getDomCss Error:', err);
}
return style;
};
/**
* Native communication module
*/
+11
View File
@@ -42,6 +42,7 @@ let _Vue;
* @returns {Object} decl - Processed declaration, original declaration by default.
*/
let _beforeLoadStyle = decl => decl;
let _beforeRenderToNative = () => {};
function setVue(Vue) {
_Vue = Vue;
@@ -67,6 +68,14 @@ function getBeforeLoadStyle() {
return _beforeLoadStyle;
}
function setBeforeRenderToNative(beforeRenderToNative) {
_beforeRenderToNative = beforeRenderToNative;
}
function getBeforeRenderToNative() {
return _beforeRenderToNative;
}
const infoTrace = once(() => {
console.log('Hippy-Vue has "Vue.config.silent" to control trace log output, to see output logs if set it to false.');
});
@@ -262,6 +271,8 @@ export {
getApp,
setBeforeLoadStyle,
getBeforeLoadStyle,
setBeforeRenderToNative,
getBeforeRenderToNative,
trace,
warn,
isTraceEnabled,