mirror of
https://github.com/Tencent/Hippy.git
synced 2026-10-17 04:41:51 +08:00
feat(vue): add beforeRenderToNative hook to support computed style
This commit is contained in:
committed by
zoomchan-cxj
parent
b28d02b010
commit
294e55e3cd
@@ -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 {
|
||||
|
||||
@@ -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\'');
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user