先来看一下注册Components的源码:

+ (void)_registerDefaultComponents
{
[self registerComponent:@"container" withClass:NSClassFromString(@"WXDivComponent") withProperties:nil];
[self registerComponent:@"div" withClass:NSClassFromString(@"WXComponent") withProperties:nil];
[self registerComponent:@"text" withClass:NSClassFromString(@"WXTextComponent") withProperties:nil];
[self registerComponent:@"image" withClass:NSClassFromString(@"WXImageComponent") withProperties:nil];
[self registerComponent:@"scroller" withClass:NSClassFromString(@"WXScrollerComponent") withProperties:nil];
[self registerComponent:@"list" withClass:NSClassFromString(@"WXListComponent") withProperties:nil];
[self registerComponent:@"recycler" withClass:NSClassFromString(@"WXRecyclerComponent") withProperties:nil];
[self registerComponent:@"waterfall" withClass:NSClassFromString(@"WXRecyclerComponent") withProperties:nil]; [self registerComponent:@"header" withClass:NSClassFromString(@"WXHeaderComponent")];
[self registerComponent:@"cell" withClass:NSClassFromString(@"WXCellComponent")];
[self registerComponent:@"embed" withClass:NSClassFromString(@"WXEmbedComponent")];
[self registerComponent:@"a" withClass:NSClassFromString(@"WXAComponent")]; [self registerComponent:@"select" withClass:NSClassFromString(@"WXSelectComponent")];
[self registerComponent:@"switch" withClass:NSClassFromString(@"WXSwitchComponent")];
[self registerComponent:@"input" withClass:NSClassFromString(@"WXTextInputComponent")];
[self registerComponent:@"video" withClass:NSClassFromString(@"WXVideoComponent")];
[self registerComponent:@"indicator" withClass:NSClassFromString(@"WXIndicatorComponent")];
[self registerComponent:@"slider" withClass:NSClassFromString(@"WXCycleSliderComponent")];
[self registerComponent:@"cycleslider" withClass:NSClassFromString(@"WXCycleSliderComponent")];
[self registerComponent:@"web" withClass:NSClassFromString(@"WXWebComponent")];
[self registerComponent:@"loading" withClass:NSClassFromString(@"WXLoadingComponent")];
[self registerComponent:@"loading-indicator" withClass:NSClassFromString(@"WXLoadingIndicator")];
[self registerComponent:@"refresh" withClass:NSClassFromString(@"WXRefreshComponent")];
[self registerComponent:@"textarea" withClass:NSClassFromString(@"WXTextAreaComponent")];
[self registerComponent:@"canvas" withClass:NSClassFromString(@"WXCanvasComponent")];
[self registerComponent:@"slider-neighbor" withClass:NSClassFromString(@"WXSliderNeighborComponent")]; [self registerComponent:@"recycle-list" withClass:NSClassFromString(@"WXRecycleListComponent")];
[self registerComponent:@"cell-slot" withClass:NSClassFromString(@"WXCellSlotComponent") withProperties: @{@"append":@"tree", @"isTemplate":@YES}]; }

从源码可以看到,WeexSDK会默认注册这28个组件。这里以WXWebComponent组件注册为例,来分析组件注册的过程。

【说明】:上面标红可以看到,有两个注册组件的方法,区别在于最后一个入参是否传@{@"append":@"tree"}。如果被标记成了@"tree",那么在syncQueue堆积了很多任务的时候,会被强制执行一次layout。在上面的28个组件中,只有前8种没有被标记成@"tree",剩下的20种都有可能强制执行一次layout。

+ (void)registerComponent:(NSString *)name withClass:(Class)clazz
{
[self registerComponent:name withClass:clazz withProperties: @{@"append":@"tree"}];
} + (void)registerComponent:(NSString *)name withClass:(Class)clazz withProperties:(NSDictionary *)properties
{
if (!name || !clazz) {
return;
} WXAssert(name && clazz, @"Fail to register the component, please check if the parameters are correct !"); // 1.WXComponentFactory注册组件的方法
[WXComponentFactory registerComponent:name withClass:clazz withPros:properties];
// 2.遍历出所有异步的方法
NSMutableDictionary *dict = [WXComponentFactory componentMethodMapsWithName:name];
dict[@"type"] = name;
// 3.把组件注册到WXBridgeManager中
if (properties) {
NSMutableDictionary *props = [properties mutableCopy];
if ([dict[@"methods"] count]) {
[props addEntriesFromDictionary:dict];
}
[[WXSDKManager bridgeMgr] registerComponents:@[props]];
} else {
[[WXSDKManager bridgeMgr] registerComponents:@[dict]];
}
}

注册组件全部都是通过WXComponentFactory完成的,WXComponentFactory是一个单例。

@interface WXComponentFactory : NSObject
{
NSMutableDictionary *_componentConfigs;
NSLock *_configLock;
} @end

在WXComponentFactory中,_componentConfigs会存储所有的组件配置,注册的过程也是生成_componentConfigs的过程。

- (void)registerComponent:(NSString *)name withClass:(Class)clazz withPros:(NSDictionary *)pros
{
WXAssert(name && clazz, @"name or clazz must not be nil for registering component."); WXComponentConfig *config = nil;
[_configLock lock];
config = [_componentConfigs objectForKey:name]; // 如果组件已经注册过,会提示重复注册,并且覆盖原先的注册行为
if(config){
WXLogInfo(@"Overrider component name:%@ class:%@, to name:%@ class:%@",
config.name, config.class, name, clazz);
} config = [[WXComponentConfig alloc] initWithName:name class:NSStringFromClass(clazz) pros:pros];
[_componentConfigs setValue:config forKey:name];
// 注册类方法
[config registerMethods]; [_configLock unlock];
}

在WXComponentFactory的_componentConfigs字典中会按照组件的名字作为key,WXComponentConfig作为value存储各个组件的配置。

@interface WXComponentConfig : WXInvocationConfig

@property (nonatomic, strong) NSDictionary *properties;

@end

@interface WXInvocationConfig : NSObject

@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *clazz;
/**
* The methods map
**/
@property (nonatomic, strong) NSMutableDictionary *asyncMethods;
@property (nonatomic, strong) NSMutableDictionary *syncMethods; @end

WXComponentConfig继承自WXInvocationConfig,在WXInvocationConfig中存储了组件名name、类名clazz、类里面的同步方法字典syncMethods和异步方法字典asyncMethods。

组件注册比较关键的一点是注册类方法

- (void)registerMethods
{
Class currentClass = NSClassFromString(_clazz); if (!currentClass) {
WXLogWarning(@"The module class [%@] doesn't exit!", _clazz);
return;
} while (currentClass != [NSObject class]) {
unsigned int methodCount = ;
// 获取类的方法列表
Method *methodList = class_copyMethodList(object_getClass(currentClass), &methodCount);
for (unsigned int i = ; i < methodCount; i++) {
// 获取SEL的字符串名称
NSString *selStr = [NSString stringWithCString:sel_getName(method_getName(methodList[i])) encoding:NSUTF8StringEncoding];
BOOL isSyncMethod = NO;
// 如果是SEL名字带sync,就是同步方法
if ([selStr hasPrefix:@"wx_export_method_sync_"]) {
isSyncMethod = YES;
}
// 如果是SEL名字不带sync,就是异步方法
else if ([selStr hasPrefix:@"wx_export_method_"]) {
isSyncMethod = NO;
} else {
// 如果名字里面不带wx_export_method_前缀的方法,那么都不算是暴露出来的方法,直接continue,进行下一轮的筛选
continue;
} NSString *name = nil, *method = nil;
SEL selector = NSSelectorFromString(selStr);
if ([currentClass respondsToSelector:selector]) {
method = ((NSString* (*)(id, SEL))[currentClass methodForSelector:selector])(currentClass, selector);
} if (method.length <= ) {
WXLogWarning(@"The module class [%@] doesn't has any method!", _clazz);
continue;
}
// 去掉方法名里面带的:号
NSRange range = [method rangeOfString:@":"];
if (range.location != NSNotFound) {
name = [method substringToIndex:range.location];
} else {
name = method;
}
// 最终字典里面会按照异步方法和同步方法保存到最终的方法字典里
NSMutableDictionary *methods = isSyncMethod ? _syncMethods : _asyncMethods;
[methods setObject:method forKey:name];
} free(methodList);
currentClass = class_getSuperclass(currentClass);
} }

上面的代码理解起来比较容易,找到对应的类方法,判断名字里面是否带有“sync”来判断方法是同步还是异步。重点需要解析的是组件的方法是如何转换成类方法暴露出去的。

Weex是通过WX_EXPORT_METHOD宏做到对外暴露类方法的。

#define WX_EXPORT_METHOD(method) WX_EXPORT_METHOD_INTERNAL(method,wx_export_method_)

#define WX_EXPORT_METHOD_INTERNAL(method, token) \
+ (NSString *)WX_CONCAT_WRAPPER(token, __LINE__) { \
return NSStringFromSelector(method); \
} #define WX_CONCAT_WRAPPER(a, b) WX_CONCAT(a, b) #define WX_CONCAT(a, b) a ## b

从宏定义可以看到,完全展开之后就是这个样子:

#define WX_EXPORT_METHOD(method)

+ (NSString *)wx_export_method_ __LINE__ { \
return NSStringFromSelector(method); \
}

举个例子,在WXWebComponent的64行有如下代码:

WX_EXPORT_METHOD(@selector(goBack))

那么这个宏在预编译的时候就会被展开成下面这个样子:

+ (NSString *)wx_export_method_64 {
return NSStringFromSelector(@selector(goBack));
}
在WXWebComponent的类方法里面就多了一个wx_export_method_64的方法。由于在同一个文件里面,WX_EXPORT_METHOD宏是不允许写在同一行的,所以转换出来的方法名字肯定不会相同。但是不同类里面行数就没有规定,行数是可能相同的,从而不同类里面可能就有相同的方法名。但是完全不会有什么影响,因为获取类方法的时候是通过class_copyMethodList,保证这个list里面都是唯一的名字即可。
这里还有一点需要说明,用class_copyMethodList会获取所有的类方法(+号方法),包括不通过WX_EXPORT_METHOD宏对外暴露的普通的+号方法。但是这里有一个判断条件,会避开这些不通过WX_EXPORT_METHOD宏对外暴露的普通的+号类方法。

如果不通过WX_EXPORT_METHOD宏来申明对外暴露的普通的+号类方法,那么名字里面就不会带wx_export_method_的前缀的方法,那么都不算是暴露出来的方法,上面筛选的代码里面会直接continue,进行下一轮的筛选,所以不必担心那些普通的+号类方法会进来干扰。

这样通过上面的筛选之后,字典里面就会存储如下信息:

methods = {
goBack = goBack;
goForward = goForward;
reload = reload;
}

这就完成了组件注册的第一步,完成了注册配置WXComponentConfig。

组件注册的第二步,是遍历所有的异步方法。

- (NSMutableDictionary *)_componentMethodMapsWithName:(NSString *)name
{
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
NSMutableArray *methods = [NSMutableArray array]; [_configLock lock];
[dict setValue:methods forKey:@"methods"]; WXComponentConfig *config = _componentConfigs[name];
void (^mBlock)(id, id, BOOL *) = ^(id mKey, id mObj, BOOL * mStop) {
[methods addObject:mKey];
};
[config.asyncMethods enumerateKeysAndObjectsUsingBlock:mBlock];
[_configLock unlock]; return dict;
}

上面的方法也是在WXComponentFactory中,该方法遍历出异步方法,并放入字典中,返回异步方法的字典。

以WXWebComponent,这里会返回如下的异步方法字典:

{
methods = (
goForward,
goBack,
reload
);
}

【注意】:大部分 Component 并没有wx_export前缀的 method,所以这里拿到的方法,很多Component都为空。

注册组件的最后一步:在JSFrame中注册组件。

    if (properties) {
NSMutableDictionary *props = [properties mutableCopy];
if ([dict[@"methods"] count]) {
[props addEntriesFromDictionary:dict];
}
[[WXSDKManager bridgeMgr] registerComponents:@[props]];
} else {
[[WXSDKManager bridgeMgr] registerComponents:@[dict]];
}

我们先来看一下WXSDKManager和bridgeMgr对应的WXBridgeManager:

@interface WXSDKManager ()

@property (nonatomic, strong) WXBridgeManager *bridgeMgr;

@property (nonatomic, strong) WXThreadSafeMutableDictionary *instanceDict;

@end

@interface WXBridgeManager ()

@property (nonatomic, weak, readonly) WXSDKInstance *topInstance;
@property (nonatomic, strong) WXBridgeContext *bridgeCtx;
@property (nonatomic, assign) BOOL stopRunning;
@property (nonatomic, strong) NSMutableArray *instanceIdStack; @end

WXBridgeManager中会弱引用WXSDKInstance实例,是为了能调用WXSDKInstance的一些属性和方法。WXBridgeManager里面最重要的一个属性就是WXBridgeContext。

@interface WXBridgeContext : NSObject

@property (nonatomic, weak, readonly) WXSDKInstance *topInstance;
@property (nonatomic, strong) id<WXBridgeProtocol> jsBridge;
@property (nonatomic, strong) id<WXBridgeProtocol> devToolSocketBridge;
@property (nonatomic, assign) BOOL debugJS;
//store the methods which will be executed from native to js
@property (nonatomic, strong) NSMutableDictionary *sendQueue; // 存储native要即将调用js的一些方法
//the instance stack
@property (nonatomic, strong) WXThreadSafeMutableArray *insStack; // 实例堆栈
//identify if the JSFramework has been loaded
@property (nonatomic) BOOL frameworkLoadFinished; // 标识JSFramework是否已经加载完成
//store some methods temporarily before JSFramework is loaded
@property (nonatomic, strong) NSMutableArray *methodQueue; // 在JSFramework加载完成之前,临时存储一些方法
// store service
@property (nonatomic, strong) NSMutableArray *jsServiceQueue; // 存储js模板的service @end

在WXBridgeContext中强持有了一个jsBridge,这个属性就是用来和js进行交互的Bridge。

现在我们回到注册的最后一步:

[[WXSDKManager bridgeMgr] registerComponents:@[dict]];

WXBridgeManager调用registerComponents方法:

- (void)registerComponents:(NSArray *)components
{
if (!components) return; __weak typeof(self) weakSelf = self;
WXPerformBlockOnBridgeThread(^(){
[weakSelf.bridgeCtx registerComponents:components];
});
}

从上面可以看到,最终是WXBridgeManager里面的WXBridgeContext 调用registerComponents,进行组件的注册。该注册过程是在一个特殊的线程中进行的:

void WXPerformBlockOnBridgeThread(void (^block)(void))
{
[WXBridgeManager _performBlockOnBridgeThread:block];
} + (void)_performBlockOnBridgeThread:(void (^)(void))block
{
if ([NSThread currentThread] == [self jsThread]) {
block();
} else {
[self performSelector:@selector(_performBlockOnBridgeThread:)
onThread:[self jsThread]
withObject:[block copy]
waitUntilDone:NO];
}
}

所有的组件注册都在jsThread这个子线程中执行,这个jsThread也是一个单例,全局唯一。

+ (NSThread *)jsThread
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{ WXBridgeThread = [[NSThread alloc] initWithTarget:[[self class]sharedManager] selector:@selector(_runLoopThread) object:nil];
[WXBridgeThread setName:WX_BRIDGE_THREAD_NAME];
if(WX_SYS_VERSION_GREATER_THAN_OR_EQUAL_TO(@"8.0")) {
[WXBridgeThread setQualityOfService:[[NSThread mainThread] qualityOfService]];
} else {
[WXBridgeThread setThreadPriority:[[NSThread mainThread] threadPriority]];
} [WXBridgeThread start];
}); return WXBridgeThread;
}

jsThread会把@selector(_runLoopThread)作为selector:

- (void)_runLoopThread
{
[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; while (!_stopRunning) {
@autoreleasepool {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
}
}

在这里用[NSMachPort port]的方式开启了一个runloop,之后再也无法获取到这个port了,而且这个runloop不是CFRunloop,所以用官方文档上的那3个方法已经不能停止这个runloop了,只能自己通过while的方式来停止。

回到注册:

- (void)registerComponents:(NSArray *)components
{
WXAssertBridgeThread(); if(!components) return; [self callJSMethod:@"registerComponents" args:@[components]];
}

从上面可以看到,注册实际上就是调用js的方法"registerComponents"。这里需要注意的是:

由于是在子线程上注册组件,那么JSFramework如果没有加载完成,native去调用js的方法,必定调用失败。所以需要在JSFramework加载完成之前,把native调用JS的方法都缓存起来,一旦JSFramework加载完成,把缓存里面的方法都丢给JSFramework去加载。
- (void)callJSMethod:(NSString *)method args:(NSArray *)args
{
if (self.frameworkLoadFinished) {
[self.jsBridge callJSMethod:method args:args];
} else {
[_methodQueue addObject:@{@"method":method, @"args":args}];
}
}

所以在WXBridgeContext中需要一个NSMutableArray,用来缓存在JSFramework加载完成之前,调用JS的方法。这里是保存在_methodQueue里面。如果JSFramework加载完成,那么就会调用callJSMethod:args:方法。

- (JSValue *)callJSMethod:(NSString *)method args:(NSArray *)args
{
WXLogDebug(@"Calling JS... method:%@, args:%@", method, args);
return [[_jsContext globalObject] invokeMethod:method withArguments:args];
}

以WXWebComponent为例,注册组件的method就是@“registerComponents”,args参数就是:

(
{
append = tree;
methods = (
goForward,
goBack,
reload
);
type = web;
}
)

至此,组件注册的全部过程就结束了。 图示如下:

附录:Vue 标签是如何加载以及渲染到视图上的?

从刚才的注册过程中发现,最后一步是通过_jsBridge调用callJSMethod这个方法来注册的,而且从WXBridgeContext中可以看到,这个_jsBridge就是WXJSCoreBridge的实例。WXJSCoreBridge可以认为是 Weex 与 Vue 进行通信的最底层的部分。在调用callJSMethod方法之前,_jsBridge向 JavaScriptCore 中注册了很多全局 function,因为jsBridge是懒加载的,所以这些操作只会执行一次,具体可以看精简后的源码:

- (void)registerGlobalFunctions
{
[_jsBridge registerCallNative:^NSInteger(NSString *instance, NSArray *tasks, NSString *callback) {
//...
}];
[_jsBridge registerCallAddElement:^NSInteger(NSString *instanceId, NSString *parentRef, NSDictionary *elementData, NSInteger index) {
//...
}]; [_jsBridge registerCallCreateBody:^NSInteger(NSString *instanceId, NSDictionary *bodyData) {
//...
}]; [_jsBridge registerCallRemoveElement:^NSInteger(NSString *instanceId, NSString *ref) {
//...
}]; [_jsBridge registerCallMoveElement:^NSInteger(NSString *instanceId,NSString *ref,NSString *parentRef,NSInteger index) {
//...
}]; [_jsBridge registerCallUpdateAttrs:^NSInteger(NSString *instanceId,NSString *ref,NSDictionary *attrsData) {
//...
}]; [_jsBridge registerCallUpdateStyle:^NSInteger(NSString *instanceId,NSString *ref,NSDictionary *stylesData) {
//...
}]; [_jsBridge registerCallAddEvent:^NSInteger(NSString *instanceId,NSString *ref,NSString *event) {
//...
}]; [_jsBridge registerCallRemoveEvent:^NSInteger(NSString *instanceId,NSString *ref,NSString *event) {
//...
}]; [_jsBridge registerCallCreateFinish:^NSInteger(NSString *instanceId) {
//...
}]; [_jsBridge registerCallNativeModule:^NSInvocation*(NSString *instanceId, NSString *moduleName, NSString *methodName, NSArray *arguments, NSDictionary *options) {
//...
}]; [_jsBridge registerCallNativeComponent:^void(NSString *instanceId, NSString *componentRef, NSString *methodName, NSArray *args, NSDictionary *options) {
//...
}];
}

从这些方法名看,大多数都是一些与 Dom 更新相关的方法,我们在WXJSCoreBridge中更细致的看一下是怎么实现的:

- (void)registerCallAddElement:(WXJSCallAddElement)callAddElement
{
id callAddElementBlock = ^(JSValue *instanceId, JSValue *ref, JSValue *element, JSValue *index, JSValue *ifCallback) { NSString *instanceIdString = [instanceId toString];
NSDictionary *componentData = [element toDictionary];
NSString *parentRef = [ref toString];
NSInteger insertIndex = [[index toNumber] integerValue];
[WXTracingManager startTracingWithInstanceId:instanceIdString ref:componentData[@"ref"] className:nil name:WXTJSCall phase:WXTracingBegin functionName:@"addElement" options:@{@"threadName":WXTJSBridgeThread,@"componentData":componentData}];
WXLogDebug(@"callAddElement...%@, %@, %@, %ld", instanceIdString, parentRef, componentData, (long)insertIndex); return [JSValue valueWithInt32:(int32_t)callAddElement(instanceIdString, parentRef, componentData, insertIndex) inContext:[JSContext currentContext]];
}; _jsContext[@"callAddElement"] = callAddElementBlock;
}

这是一个更新 Dom 添加 UIView 的方法,这里需要把 Native 的方法暴露给 JS 调用。但是有一个问题:

OC 的方法参数格式和 JS 的不一样,不能直接提供给 JS 调用。

所以这里用了两个 Block 嵌套的方式,在 JS 中调用方法时会先 invoke 里层的 callAddElementBlock,这层 Block 将 JS 传进来的参数转换成 OC 的参数格式,再执行 callAddElement 并返回一个 JSValue 给 JS,callAddElement Block中是在WXComponentManager中完成的关于 Component 的一些操作。

至此,简单来说就是:Weex 的页面渲染是通过先向 JSCore 注入方法,Vue 加载完成就可以调用这些方法并传入相应的参数完成 Component 的渲染和视图的更新。

要注意,每一个 WXSDKInstance 对应一个 Vue 页面,Vue 加载之前就会创建对应的 WXSDKInstance,所有的 Component 都继承自WXComponent,它们的初始化方法都是:

- (instancetype)initWithRef:(NSString *)ref
type:(NSString*)type
styles:(nullable NSDictionary *)styles
attributes:(nullable NSDictionary *)attributes
events:(nullable NSArray *)events
weexInstance:(WXSDKInstance *)weexInstance;

这个方法会在 JS 调用callCreateBody时被 invoke(调用)。

WeexSDK之注册Components的更多相关文章

  1. WeexSDK之注册Modules

    注册Modules的流程和注册Components非常类似. + (void)_registerDefaultModules { [self registerModule:@"dom&quo ...

  2. WeexSDK之注册Handlers

    先看代码: + (void)_registerDefaultHandlers { [self registerHandler:[WXResourceRequestHandlerDefaultImpl ...

  3. WeexSDK源码分析(iOS)

    0.从工作原理谈起 Weex 表面上是一个客户端技术,但实际上它串联起了从本地开发.云端部署到分发的整个链路.开发者首先可在本地像编写 web 页面一样编写一个 app 的界面,然后通过命令行工具将之 ...

  4. 【原】vue单文件组件互相通讯

    在vue中,我们可以把一个页面各个部分单独封装起来,做成各种小组件,然后加载,这样的好处是代码维护起来比较容易,重复的代码多处调用! 在一个vue项目中,单文件组件以.vue形式文件命名 每个组件之间 ...

  5. vue2.0笔记《二》组件

    主要内容:如何注册组件.如何使用组件.父组件子组件之间值的传递 1.如何注册组件 第一步:通过import将子组件载入父组件的js中 // 第一步:通过import将子组件载入父组件的js中 impo ...

  6. vue.js学习系列-第一篇

    VUE系列一 简介    vue是一个兴起的前端js库,是一个精简的MVVM.从技术角度讲,Vue.js专注于 MVVM 模型的 ViewModel 层.它通过双向数据绑定把 View 层和 Mode ...

  7. vue 中给组建绑定原生事件@click.native=""

    <template>     <div class="div">  //组建使用          <v-header @click.native=& ...

  8. vue学习【第五篇】:Vue组件

    什么是组件 - 每一个组件都是一个vue实例 - 每个组件均具有自身的模板template,根组件的模板就是挂载点 - 每个组件模板只能拥有一个根标签 - 子组件的数据具有作用域,以达到组件的复用 根 ...

  9. ~Vue实现简单答题功能,主要包含单选框和复选框

    内容 实现简单答题效果 环境 Vue,webpack(自行安装) 实现方式 页面将答题列表传递给调用组件,组件将结果返回给调用页面(其它模式也ok,这只是例子) ------------------- ...

随机推荐

  1. 编写Servlet 实例 -Shopping网站时,遇到的几个问题

    问题一.在Web 上运行时,用JDBC链接MySQL总是出错,一直出现驱动加载失败 ------提示java.lang.ClassNotFoundException.解决方案:将数据库驱动jar文件导 ...

  2. awk选取制定行数,条件判断等

    awk '{if(NR%5==0){print}}' your_file 取出可以被5整除的数awk '{if(NR<=300){print}}' your_file 取出行数小于300的数据a ...

  3. 为Firefox浏览器安装Firebug插件

    一.确保联网 二.打开Firefox 三.菜单:工具 -> 附加组件 显示附加组件管理器界面,点扩展 在搜索框输入firebug,搜,在搜索结果列表中找到Firebug项,安装 安装进度 安装完 ...

  4. [剑指Offer]8-二叉树的下一个节点

    链接 https://www.nowcoder.com/practice/9023a0c988684a53960365b889ceaf5e?tpId=13&tqId=11210&tPa ...

  5. web项目局部打印

    window.print()方法是打印整个body,若想打印局部区域,网上出现了各种解决办法,我觉得都挺好的.我最推荐jquery.PrintArea.js插件形式 点击上述链接首先下载下来,我的是版 ...

  6. easyui combobox下拉框文字超出宽度有横向滚轮

    //下拉框显示横向滚轮 $(".combo").mouseenter(function(){ $(this).prev().combobox("showPanel&quo ...

  7. js判断是手机端还是pc端访问

    if(/Android|webOS|iPhone|iPod|BlackBerry/i.test(navigator.userAgent)) { window.location.href = " ...

  8. Pycharm中选择Python解释器

    新建项目后,有时候Pycharm找不到Python解释器,如果找不到的话,就会报错.报错信息: No python interpreter configured for the project 找到P ...

  9. React-router4 第八篇 ReactCSSTransitionGroup 动画转换

    https://reacttraining.com/react-router/web/example/animated-transitions 动画转换这么高级,其实是又引入了一个组件,没什么特别, ...

  10. Linux系统中的tar命令

    时间一长什么东西都容易忘记,尤其是一些不常用的东西忘记的更快,所以避免忘记,就记录下来,可以方面使用的时候查询.Tar命令在linux系统中算是一个比较重要的命令,今天就针对该命令进行总结一下. 1. ...