2021SC@SDUSC
简述
上一期简单的开始了hippy-vue源码的分析,并且分析完了util模块。 回顾hippy-vue的入口文件index.js中,除了使用了util模块,另一个,也是目前最重要的就是runtime模块了。 入口文件index.js回顾↓: 所以这一期进行runtime模块源代码的分析。 先看一下目录结构:
各个文件的具体功能此处暂时不作分析。 单独提一下node-ops.js:
hippy-vue 其实是基于官方 Vue 2.x 源代码,通过改写 node-ops 外挂实现的自定义渲染层,但不仅仅是个到终端的渲染层,还同时实现前端组件到终端的映射、CSS 语法解析,和其它跨端框架不同,它尽力将 Web 端的开发体验带到终端上来,同时保持了对 Web 生态的兼容。
由此可见node-ops是hippy的关键点,实现了自定义渲染层,以支持vue的组件等可以映射到终端。
再放一张hippy-vue官网里的一张架构图:
这期先分析runtime模块的index.js文件。从index.js逐渐分析其余代码。
代码分析
先上runtime/index.js 源代码:
import Vue from 'core/index';
import { defineComputed, proxy } from 'core/instance/state';
import { ASSET_TYPES } from 'shared/constants';
import { mountComponent } from 'core/instance/lifecycle';
import { compileToFunctions } from 'web/compiler/index';
import {
warn,
isPlainObject,
mergeOptions,
extend,
} from 'core/util/index';
import {
registerBuiltinElements,
registerElement,
getElementMap,
mustUseProp,
isReservedTag,
isUnknownElement,
} from '../elements';
import {
getApp,
setApp,
isFunction,
trace,
setBeforeLoadStyle,
} from '../util';
import DocumentNode from '../renderer/document-node';
import { Event } from '../renderer/native/event';
import { patch } from './patch';
import Native, { HippyRegister } from './native';
import * as iPhone from './iphone';
import * as platformDirectives from './directives';
const componentName = ['%c[Hippy-Vue process.env.HIPPY_VUE_VERSION]%c', 'color: #4fc08d; font-weight: bold', 'color: auto; font-weight: auto'];
const documentNode = new DocumentNode();
Vue.$document = documentNode;
Vue.prototype.$document = documentNode;
Vue.$Document = DocumentNode;
Vue.$Event = Event;
Vue.config.mustUseProp = mustUseProp;
Vue.config.isReservedTag = isReservedTag;
Vue.config.isUnknownElement = isUnknownElement;
Vue.compile = compileToFunctions;
Vue.registerElement = registerElement;
extend(Vue.options.directives, platformDirectives);
Vue.prototype.__patch__ = patch;
Vue.prototype.$mount = function $mount(el, hydrating) {
const options = this.$options;
if (!options.render) {
const { template } = options;
if (template && typeof template !== 'string') {
warn(`invalid template option: ${template}`, this);
return this;
}
if (template) {
const { render, staticRenderFns } = compileToFunctions(
template,
{
delimiters: options.delimiters,
comments: options.comments,
},
this,
);
options.render = render;
options.staticRenderFns = staticRenderFns;
}
}
return mountComponent(this, el, hydrating);
};
Vue.prototype.$start = function $start(afterCallback, beforeCallback) {
setApp(this);
if (isFunction(this.$options.beforeLoadStyle)) {
setBeforeLoadStyle(this.$options.beforeLoadStyle);
}
getElementMap().forEach((entry) => {
Vue.component(entry.meta.component.name, entry.meta.component);
});
HippyRegister.regist(this.$options.appName, (superProps) => {
const { __instanceId__: rootViewId } = superProps;
this.$options.$superProps = superProps;
this.$options.rootViewId = rootViewId;
trace(...componentName, 'Start', this.$options.appName, 'with rootViewId', rootViewId, superProps);
if (this.$el) {
this.$destroy();
const AppConstructor = Vue.extend(this.$options);
const newApp = new AppConstructor(this.$options);
setApp(newApp);
}
if (isFunction(beforeCallback)) {
beforeCallback(this, superProps);
}
this.$mount();
if (Native.Platform === 'ios') {
const statusBar = iPhone.drawStatusBar(this.$options);
if (statusBar) {
if (!this.$el.childNodes.length) {
this.$el.appendChild(statusBar);
} else {
this.$el.insertBefore(statusBar, this.$el.childNodes[0]);
}
}
}
if (isFunction(afterCallback)) {
afterCallback(this, superProps);
}
});
};
let cid = 1;
function initProps(Comp) {
const { props } = Comp.options;
Object.keys(props).forEach(key => proxy(Comp.prototype, '_props', key));
}
function initComputed(Comp) {
const { computed } = Comp.options;
Object.keys(computed).forEach(key => defineComputed(Comp.prototype, key, computed[key]));
}
Vue.component = function component(id, definition) {
if (!definition) {
return this.options.components[id];
}
if (isPlainObject(definition)) {
definition.name = definition.name || id;
definition = this.options._base.extend(definition);
}
this.options.components[id] = definition;
return definition;
};
Vue.extend = function hippyExtend(extendOptions) {
extendOptions = extendOptions || {};
const Super = this;
const SuperId = Super.cid;
const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {});
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId];
}
const name = extendOptions.name || Super.options.name;
const Sub = function VueComponent(options) {
this._init(options);
};
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
cid += 1;
Sub.cid = cid;
Sub.options = mergeOptions(Super.options, extendOptions);
Sub.super = Super;
if (Sub.options.props) {
initProps(Sub);
}
if (Sub.options.computed) {
initComputed(Sub);
}
Sub.extend = Super.extend;
Sub.mixin = Super.mixin;
Sub.use = Super.use;
ASSET_TYPES.forEach((type) => {
Sub[type] = Super[type];
});
if (name) {
Sub.options.components[name] = Sub;
}
Sub.superOptions = Super.options;
Sub.extendOptions = extendOptions;
Sub.sealedOptions = extend({}, Sub.options);
cachedCtors[SuperId] = Sub;
return Sub;
};
Vue.Native = Native;
Vue.getApp = getApp;
Vue.use(registerBuiltinElements);
export default Vue;
文件最后向外输出了Vue 对象。 代码主要干了以下事情:
- 安装初始化文档和事件类
- 安装初始化平台工具和运行时指令组件
- 重写mount、扩展以避免组件内置警告
- 绑定Native属性
// Install document部分是new了一个documentNode对象并且赋给Vue的$document属性以及原型上对应的$document。 documentNode 函数引自于render 模块的document-node.js 。 render模块目录结构:(这一模块控制着渲染,定义、生成DOM节点等。)
render/document-node.js 代码:
import CommentNode from './comment-node';
import ElementNode from './element-node';
import ViewNode from './view-node';
import TextNode from './text-node';
import InputNode from './input-node';
import ListNode from './list-node';
import ListItemNode from './list-item-node';
import { Event } from './native/event';
class DocumentNode extends ViewNode {
constructor() {
super();
this.documentElement = new ElementNode('document');
this.createComment = this.constructor.createComment;
this.createElement = this.constructor.createElement;
this.createElementNS = this.constructor.createElementNS;
this.createTextNode = this.constructor.createTextNode;
}
static createComment(text) {
return new CommentNode(text);
}
static createElement(tagName) {
switch (tagName) {
case 'input':
case 'textarea':
return new InputNode(tagName);
case 'ul':
return new ListNode(tagName);
case 'li':
return new ListItemNode(tagName);
default:
return new ElementNode(tagName);
}
}
static createElementNS(namespace, tagName) {
return new ElementNode(`${namespace}:${tagName}`);
}
static createTextNode(text) {
return new TextNode(text);
}
static createEvent(eventName) {
return new Event(eventName);
}
}
export default DocumentNode;
这一部分比较简单直接,文件直接向外输出了DocumentNode 类。 而这个类是继承于view-node.js 文件的ViewNode 类。 view-node.js 代码:
import { insertChild, removeChild } from './native';
const ROOT_VIEW_ID = 0;
let currentNodeId = 0;
if (global.__GLOBAL__ && Number.isInteger(global.__GLOBAL__.nodeId)) {
currentNodeId = global.__GLOBAL__.nodeId;
}
function getNodeId() {
currentNodeId += 1;
if (currentNodeId % 10 === 0) {
currentNodeId += 1;
}
if (currentNodeId % 10 === ROOT_VIEW_ID) {
currentNodeId += 1;
}
return currentNodeId;
}
class ViewNode {
constructor() {
this._ownerDocument = null;
this._meta = null;
this._isMounted = false;
this.nodeId = getNodeId();
this.index = 0;
this.childNodes = [];
this.parentNode = null;
this.prevSibling = null;
this.nextSibling = null;
}
toString() {
return this.constructor.name;
}
get firstChild() {
return this.childNodes.length ? this.childNodes[0] : null;
}
get lastChild() {
return this.childNodes.length
? this.childNodes[this.childNodes.length - 1]
: null;
}
get meta() {
if (!this._meta) {
return {};
}
return this._meta;
}
get ownerDocument() {
if (this._ownerDocument) {
return this._ownerDocument;
}
let el = this;
while (el.constructor.name !== 'DocumentNode') {
el = el.parentNode;
if (!el) {
break;
}
}
this._ownerDocument = el;
return el;
}
get isMounted() {
return this._isMounted;
}
set isMounted(isMounted) {
this._isMounted = isMounted;
}
insertBefore(childNode, referenceNode) {
if (!childNode) {
throw new Error('Can\'t insert child.');
}
if (!referenceNode) {
return this.appendChild(childNode);
}
if (referenceNode.parentNode !== this) {
throw new Error('Can\'t insert child, because the reference node has a different parent.');
}
if (childNode.parentNode && childNode.parentNode !== this) {
throw new Error('Can\'t insert child, because it already has a different parent.');
}
const index = this.childNodes.indexOf(referenceNode);
childNode.parentNode = this;
childNode.nextSibling = referenceNode;
childNode.prevSibling = this.childNodes[index - 1];
if (this.childNodes[index - 1]) {
this.childNodes[index - 1].nextSibling = childNode;
}
referenceNode.prevSibling = childNode;
this.childNodes.splice(index, 0, childNode);
return insertChild(this, childNode, index);
}
moveChild(childNode, referenceNode) {
if (!childNode) {
throw new Error('Can\'t move child.');
}
if (!referenceNode) {
return this.appendChild(childNode);
}
if (referenceNode.parentNode !== this) {
throw new Error('Can\'t move child, because the reference node has a different parent.');
}
if (childNode.parentNode && childNode.parentNode !== this) {
throw new Error('Can\'t move child, because it already has a different parent.');
}
const oldIndex = this.childNodes.indexOf(childNode);
const newIndex = this.childNodes.indexOf(referenceNode);
if (newIndex === oldIndex) {
return childNode;
}
childNode.nextSibling = referenceNode;
childNode.prevSibling = referenceNode.prevSibling;
referenceNode.prevSibling = childNode;
if (this.childNodes[newIndex - 1]) {
this.childNodes[newIndex - 1].nextSibling = childNode;
}
if (this.childNodes[newIndex + 1]) {
this.childNodes[newIndex + 1].prevSibling = childNode;
}
if (this.childNodes[oldIndex - 1]) {
this.childNodes[oldIndex - 1].nextSibling = this.childNodes[oldIndex + 1];
}
if (this.childNodes[oldIndex + 1]) {
this.childNodes[oldIndex + 1].prevSibling = this.childNodes[oldIndex - 1];
}
removeChild(this, childNode);
this.childNodes.splice(newIndex, 0, childNode);
this.childNodes.splice(oldIndex + (newIndex < oldIndex ? 1 : 0), 1);
const atIndex = this.childNodes.filter(ch => ch.index > -1).indexOf(childNode);
return insertChild(this, childNode, atIndex);
}
appendChild(childNode) {
if (!childNode) {
throw new Error('Can\'t append child.');
}
if (childNode.parentNode && childNode.parentNode !== this) {
throw new Error('Can\'t append child, because it already has a different parent.');
}
if (childNode.isMounted) {
this.removeChild(childNode);
}
childNode.parentNode = this;
if (this.lastChild) {
childNode.prevSibling = this.lastChild;
this.lastChild.nextSibling = childNode;
}
this.childNodes.push(childNode);
insertChild(this, childNode, this.childNodes.length - 1);
}
removeChild(childNode) {
if (!childNode) {
throw new Error('Can\'t remove child.');
}
if (!childNode.parentNode) {
throw new Error('Can\'t remove child, because it has no parent.');
}
if (childNode.parentNode !== this) {
throw new Error('Can\'t remove child, because it has a different parent.');
}
if (childNode.meta.skipAddToDom) {
return;
}
removeChild(this, childNode);
if (childNode.prevSibling) {
childNode.prevSibling.nextSibling = childNode.nextSibling;
}
if (childNode.nextSibling) {
childNode.nextSibling.prevSibling = childNode.prevSibling;
}
childNode.prevSibling = null;
childNode.nextSibling = null;
this.childNodes = this.childNodes.filter(node => node !== childNode);
}
findChild(condition) {
const yes = condition(this);
if (yes) {
return this;
}
if (this.childNodes.length) {
for (let i = 0; i < this.childNodes.length; i += 1) {
const childNode = this.childNodes[i];
const targetChild = this.findChild.call(childNode, condition);
if (targetChild) {
return targetChild;
}
}
}
return null;
}
traverseChildren(callback) {
let index;
if (this.parentNode) {
index = this.parentNode.childNodes.filter(node => !node.meta.skipAddToDom).indexOf(this);
} else {
index = 0;
}
this.index = index;
callback(this);
if (this.childNodes.length) {
this.childNodes.forEach((childNode) => {
this.traverseChildren.call(childNode, callback);
});
}
}
}
export default ViewNode;
ViewNode 类是视图节点。 其中除了构造器,主要函数还有各种get 和set 函数用以获取子节点以及自己的渲染状态,insertBefore ,moveChild ,appendChild ,removeChild 等函数可以修改节点布局,其中removeChild 等函数调用了./native/index.js 的相应函数。
再看document-node.js代码。 含有一个构造器及createComment ,createElement ,createElementNS 等函数。 其中createElement 函数可以根据标签名(例如"li"),new一个相应的节点对象。 这一部分的对象都是引自同一目录下的其他文件代码。有的类继承了ViewNode ,如TextNode 。
import ViewNode from './view-node';
import { Text } from './native/components';
export default class TextNode extends ViewNode {
constructor(text) {
super();
this.text = text;
this._meta = {
symbol: Text,
skipAddToDom: true,
};
}
setText(text) {
this.text = text;
this.parentNode.setText(text);
}
}
有的节点类除了展示还需要其他一些功能,继承的是ElementNode 。 具体代码结构如上面的TestNode 类似。 一个构造器,如果有需要则含有其他一些可以设置这个节点的函数。比如TestNode 的setText 函数。
总结
到此为止,这一期从runtime 模块入手,按照代码路线基本把涉及到的render 模块分析完了(native文件夹里的代码还暂时没有分析到,主要就是和移动终端设备的兼容问题,里面有css选择器、解析器、终端组件映射等。),render 模块涉及到了dom节点的定义与生成等,为了和移动终端兼容,实现了部分终端常用的节点。 下一期可以继续深入分析runtime 的入口文件index.js 。
|