路由器的作用是什么?通俗的讲,路由器的作用就是一根网线满足多人上网的需求。而在开发中路由器模块的作用就是实现中转分发,也就是说将原来有关系的模块(有依赖的模块分开),产生一个中间的模块,让原来依赖的两个模块都去和路由模块交互,从而将原来两个有关系的模块拆分开,利如我现在在开发一个app,根据业务需求这里需要开发一个便民中心模块,但是进入这个模块肯定得先从主模块点击进去,这里如果有数据之间的传递的话,就产生了交互。而交互产生了的话,就会产生依赖,此时便民中心模块依赖主模块,而此时如果我们搭建路由器模块的话,那么主模块就和便民中心模块解耦了,主模块通过路由器模块将数据分发给便民中心模块,而不像以前我们在android端跳转的转的话直接通过Intent跳转,传值也通过Intent传,这里有个弊端就如定义传值的key值,在接受这个key随确定value值得时候,你必须得知道这个key是什么,后期维护你需要一个一个Activity的去找,跳转到那个Activity需要传什么值,甚是头疼,那么路由器的另一个优点来了,我们从一个模块跳到另一个模块需要传的值的key完全可以通过注解标记出来,一目了然,而不用那么费力的找了,而且具体怎么传的完全隔离出去,用户完全不用考虑实现的细节。

通俗一点的讲,这里我们先不讨论模块之间的隔离,只简单的讨论一下从Activity(A)跳转到Activity(B)的场景,首先我们第一点得确定是从哪一个Activity跳到哪一个Activity,最初的跳转是你要么直接显示跳转、要么隐式跳转,但是不管怎么跳转都需要知道具体是那个Activity(显示)、隐式(知道action什么的等),而采用路由器模式的时候,你完全不用关心他的Activity的具体名字是什么,或者他的action等是什么,你只需要给它造一个匹配规则,让路由器自己找到你想传值和跳转的Activity到底是哪一个,就好比我定义一个IP地址,这个Ip地址就是指向B(Activity)的,那么A(Activity)通过将ip地址传给路由器然后路由器帮你分析你想跳转到哪一个Activity中,最终锁定到B,,日后你想修改跳转规则或传值是怎么传的,只要查看路由表就好了,这就是路由模块的好处了。

如果你想自己实现一个路由框架的话,得做哪些准备呢?首先你得知道路由器到底是干嘛使的,通过上面的分析,你大体应该知道路由器在Android端的作用了,那么我们首先需要做的就是将所有的模块的主Activity制定他们匹配的ip,也就是说为A模块主(Activity)标记一个唯一的ip地址,也就是加个域名,例如A对应http://feiyu/,B模块对应http://zp/

然后将对应关系保存到缓存中,保存到缓存中后,我们通过路由器调用之后,路由器从路由表中取出对应关系,但是路由表必须知道这个关系的规则是什么,他需要根据规则将路由表中数据解析出来,然后实现跳转,那么在写路由器模块的时候我们必须为路由器写上解析器模块,那么这里我们已经想到要用至少到两个设计模式了,单利模式、解释器模式(专门用来解析规则例如正则表达式就是用的这种设计模式)。

如果我们要保存映射关系,是否将它固定,也就是说每写一个模块的话,将它手动添加到路由表中,很显然这是不科学的,因为我们写的路由器模块是给其他小伙伴用的,他不一定愿意看你的代码,那么怎么样让他写的模块映射出路由表里的数据,这里就用到了注解,我们在路由器模块中写出注解规则,和你合作的小伙伴只要按照你的注解规则,为他的模块主activity标记上注解,我们就能动态的将注解解析出来,然后将它放到路由表中。这样路由器解析的时候就会找到需要跳转的模块。

但是这里又有一个问题,该采用运行时注解还是编译器时注解呢,运行时注解,就是你动态通过反射将注解解析出来放在集合中,但是那需要在运行时解析,比较耗费点时间,那么采用编译器注解呢,就是说在编译的时候先检查有没有编译注解,如果有的话先通过注解生成java文件,然后才将新生成的java文件和你写的项目java文件一起编译成class文件,但是这么做的话会多出java文件,从而使apk包增大,还有可能遇到android的65536的限制,但是一点不影响运行时的速度,综合考虑还是采用编译时注解(apt技术)。

看了这么多,是不是有点累了,先欣赏下美女休息一下

好了,进入正题,这里我们来一起分析一下ActivityRouter源码,参观一下别人是怎么实现的,ActivityRouter源码 至于为什么要通过这个框架分析,因为它虽然有点缺点,但是它小巧并且已经能将路由框架的原理思想大体的表现出来。

前面提到编译时期的注解,那么先从这个框架的apt部分开始说起,如果想在android studio中实现apt功能只需要在app下build.gradle配置文件中加入

compile 'com.google.auto.service:auto-service:1.0-rc3'

然后在你的编译处理类上加入这个注解

@AutoService(Processor.class)
public class RouterProcessor extends AbstractProcessor 
先来看下一下实现这个编译处理类需要实现哪些方法:
public synchronized void init(ProcessingEnvironment processingEnv) {super.init(processingEnv);messager = processingEnv.getMessager();filer = processingEnv.getFiler();}

初始化方法,在这里你需要获得你需要使用的工具类,例如Messager(日志相关的辅助类),Filter(文件相关的辅助类(用它辅助生成新的java文件))

具体的其它辅助类,小伙伴们请查阅相关文档。
 public Set<String> getSupportedAnnotationTypes() {Set<String> ret = new HashSet<>();ret.add(Modules.class.getCanonicalName());ret.add(Module.class.getCanonicalName());ret.add(Router.class.getCanonicalName());return ret;}

这个方法用来告诉注解处理器那些注解需要处理。这里Module、Modules和Router处理注解需要处理,也就是说这个框架只声明了这

三种注解
@Retention(RetentionPolicy.CLASS)
public @interface Module {String value();
}
@Retention(RetentionPolicy.CLASS)
public @interface Modules {String[] value();
}

@Retention(RetentionPolicy.CLASS)这个注解用来标记是在编译时的注解,没有标记要注解的类型的话,默认为类注解

Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.CLASS)
public @interface Router {String[] value();String[] stringParams() default "";String[] intParams() default "";String[] longParams() default "";String[] booleanParams() default "";String[] shortParams() default "";String[] floatParams() default "";String[] doubleParams() default "";String[] byteParams() default "";String[] charParams() default "";String[] transfer() default "";
}

这个注解即可以标记类,又可以标记方法,其中带params是传递的参数

接下来是下面这个方法

public SourceVersion getSupportedSourceVersion() {return SourceVersion.latestSupported();}

返回支持的java版本


最重要的是实现下面这个方法,只要捕捉到你设置的注解最终就会回调这个方法供你生成java文件,如下:
 public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {debug("process apt with " + annotations.toString());if (annotations.isEmpty()) {return false;}boolean hasModule = false;boolean hasModules = false;// moduleString moduleName = "RouterMapping";Set<? extends Element> moduleList = roundEnv.getElementsAnnotatedWith(Module.class);if (moduleList != null && moduleList.size() > 0) {Module annotation = moduleList.iterator().next().getAnnotation(Module.class);moduleName = moduleName + "_" + annotation.value();hasModule = true;}// modulesString[] moduleNames = null;Set<? extends Element> modulesList = roundEnv.getElementsAnnotatedWith(Modules.class);if (modulesList != null && modulesList.size() > 0) {Element modules = modulesList.iterator().next();moduleNames = modules.getAnnotation(Modules.class).value();hasModules = true;}// RouterInitif (hasModules) {debug("generate modules RouterInit");generateModulesRouterInit(moduleNames);} else if (!hasModule) {debug("generate default RouterInit");generateDefaultRouterInit();}// RouterMappingreturn handleRouter(moduleName, roundEnv);}

在这个方法里面处理所有被注解了的元素,这里需要弄懂元素类型总共有多少种,如下:

ackage com.example;    // PackageElementpublic class Foo {        // TypeElement 类型元素private int a;      // VariableElement代表成员变量private Foo other;  // VariableElementpublic Foo () {}    // ExecuteableElement 匹配方法元素public void setA (  // ExecuteableElementint newA   // TypeElement 参数也代表TypeElement ) {}
}

这个方法首先获得Module和Modules注解的元素,然后创建RouterInit这个类,来看一下创建的这个类中有哪些方法和变量


MethodSpec.Builder initMethod = MethodSpec.methodBuilder("init").addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC);for (String module : moduleNames) {initMethod.addStatement("RouterMapping_" + module + ".map()");}TypeSpec routerInit = TypeSpec.classBuilder("RouterInit").addModifiers(Modifier.PUBLIC, Modifier.FINAL).addMethod(initMethod.build()).build();try {JavaFile.builder("com.github.mzule.activityrouter.router", routerInit).build().writeTo(filer);} catch (Exception e) {e.printStackTrace();}}

这个方法用了javaPoet来生成java文件,javaPoet是啥?javaPoet是JakeWharton大神编写的用于辅助生成java文件的框架。

javaPoet
这个方法的意思就是创建包名为com.github.mzule.activityrouter.router的类,并在这个类中创建静态init方法,并在init方法中调用
RouterMapping_module(注解module的名字被添加注解的activity)类的map方法,当然RouterMapping_module类也是动态生成的,来看一下
它生成的代码
private boolean handleRouter(String genClassName, RoundEnvironment roundEnv) {Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Router.class);MethodSpec.Builder mapMethod = MethodSpec.methodBuilder("map").addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC).addStatement("java.util.Map<String,String> transfer = null").addStatement("com.github.mzule.activityrouter.router.ExtraTypes extraTypes").addCode("\n")for (Element element : elements) {Router router = element.getAnnotation(Router.class);String[] transfer = router.transfer();if (transfer.length > 0 && !"".equals(transfer[0])) {mapMethod.addStatement("transfer = new java.util.HashMap<String, String>()");for (String s : transfer) {String[] components = s.split("=>");if (components.length != 2) {error("transfer `" + s + "` not match a=>b format");break;}mapMethod.addStatement("transfer.put($S, $S)", components[0], components[1]);}} else {mapMethod.addStatement("transfer = null");}mapMethod.addStatement("extraTypes = new com.github.mzule.activityrouter.router.ExtraTypes()");mapMethod.addStatement("extraTypes.setTransfer(transfer)");addStatement(mapMethod, int.class, router.intParams());addStatement(mapMethod, long.class, router.longParams());addStatement(mapMethod, boolean.class, router.booleanParams());addStatement(mapMethod, short.class, router.shortParams());addStatement(mapMethod, float.class, router.floatParams());addStatement(mapMethod, double.class, router.doubleParams());addStatement(mapMethod, byte.class, router.byteParams());addStatement(mapMethod, char.class, router.charParams());for (String format : router.value()) {ClassName className;Name methodName = null;if (element.getKind() == ElementKind.CLASS) {className = ClassName.get((TypeElement) element);} else if (element.getKind() == ElementKind.METHOD) {className = ClassName.get((TypeElement) element.getEnclosingElement());methodName = element.getSimpleName();} else {throw new IllegalArgumentException("unknow type");}if (format.startsWith("/")) {error("Router#value can not start with '/'. at [" + className + "]@Router(\"" + format + "\")");return false;}if (format.endsWith("/")) {error("Router#value can not end with '/'. at [" + className + "]@Router(\"" + format + "\")");return false;}if (element.getKind() == ElementKind.CLASS) {mapMethod.addStatement("com.github.mzule.activityrouter.router.Routers.map($S, $T.class, null, extraTypes)", format, className);} else {mapMethod.addStatement("com.github.mzule.activityrouter.router.Routers.map($S, null, " +"new MethodInvoker() {\n" +"   public void invoke(android.content.Context context, android.os.Bundle bundle) {\n" +"       $T.$N(context, bundle);\n" +"   }\n" +"}, " +"extraTypes)", format, className, methodName);}}mapMethod.addCode("\n");}TypeSpec routerMapping = TypeSpec.classBuilder(genClassName).addModifiers(Modifier.PUBLIC, Modifier.FINAL).addMethod(mapMethod.build()).build();try {JavaFile.builder("com.github.mzule.activityrouter.router", routerMapping).build().writeTo(filer);} catch (Throwable e) {e.printStackTrace();}return true;}

这个方法就是生成RouterMapping_module类,并生成静态的map()方法,那么在map方法里都添加了哪些操作呢?

java.util.Map<String,String> transfer = null

添加转化的对象(顾名思义就是将一个名字转化为另一个名字),介绍完生成java的对象的时候会详细讨论


extraTypes = new com.github.mzule.activityrouter.router.ExtraTypes()

创建传值类型的类,用来标记所传的值是什么类型的


 mapMethod.addStatement("com.github.mzule.activityrouter.router.Routers.map($S, $T.class, null, extraTypes)", format, className);

调用Routers类的map方法,将映射存到路由表中


讨论到这里,大体的可以看出这个框架利用apt技术动态的生成两个java类,这两个java类的主要作用就是将注解交给路由器的
解析类解析映射关系,最终将,映射关系存到路由器缓存中。只要在app启动时调用这两个java类,就可以将Activity和ip的映射
关系保存到路由器表中,在路由器中转中,就可以找到合适的模块的主Activity进行分发跳转了。

  现在来简单的运用这个框架,如果想把某个模块的主Activity加入到路由表中,直接在这个Activity上添加这个注解:
@Router("user/collection")
public class UserCollectionActivity extends DumpExtrasActivity {

这个注解相当于为这个Activity在路由器表中添加了user/collection这个域名映射记录,接下来在想要跳转的地方加上这么一句话:


Routers.open(context, "router://user/collection")

只是跳转到该Activity,该Activity结束的时候不需要传值给上一个Activity


  Routers.openForResult(context,"router://user/collection" ,requestCode);
该Activity结束的时候需要传值给上一个Activity

用起来确实很简单,那么如果要传值的话,需要加上

Router(value = {"main", "home"},longParams = {"id", "updateTime"},booleanParams = "web")

这里声明了域名是main或者home,定义了三个参数long类型:id,updateTime,boolean类型:web,那么再调用这个Activity的时候,

完整的url为router://main?id=1103&updateTime=537896&web=true,是不是类似于get方式传值,其它的方式小伙伴们请看作者的介绍
ActivityRouter介绍

好,继续看跳转的流程,当调用open或者openForResult的方法时都会走到下面这个方法
private static boolean open(Context context, Uri uri, int requestCode, RouterCallback callback) {boolean success = false;if (callback != null) {if (callback.beforeOpen(context, uri)) {return false;}}try {success = doOpen(context, uri, requestCode);} catch (Throwable e) {e.printStackTrace();if (callback != null) {callback.error(context, uri, e);}}if (callback != null) {if (success) {callback.afterOpen(context, uri);} else {callback.notFound(context, uri);}}return success;}

这里有一个回调函数,用户在自己定义的时候可以监听跳转之前和跳转之后(可以做一些事情,比如拦截、打印等等,自定义默认跳转等等),如下:

public interface RouterCallback {void notFound(Context context, Uri uri);boolean beforeOpen(Context context, Uri uri);void afterOpen(Context context, Uri uri);void error(Context context, Uri uri, Throwable e);

接下来进入下面这个方法实现真正的跳转,

 private static boolean doOpen(Context context, Uri uri, int requestCode) {initIfNeed();Path path = Path.create(uri);for (Mapping mapping : mappings) {if (mapping.match(path)) {if (mapping.getActivity() == null) {mapping.getMethod().invoke(context, mapping.parseExtras(uri));return true;}Intent intent = new Intent(context, mapping.getActivity());intent.putExtras(mapping.parseExtras(uri));intent.putExtra(KEY_RAW_URL, uri.toString());if (!(context instanceof Activity)) {intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);}if (requestCode >= 0) {if (context instanceof Activity) {((Activity) context).startActivityForResult(intent, requestCode);} else {throw new RuntimeException("can not startActivityForResult context " + context);}} else {context.startActivity(intent);}return true;}}return false;}

可以看到最终还是通过startActivity或StartActivityForResult实现跳转,但是在此之前需要从路由表中查询是否能找到用跳转的Activity的路由信息,找到的话将参数都解析出来,用intent进行传值,这个方法的第一行有initIfNeed()这个方法,这个方法是用来调用apt产生的java对象的的方法的,也就是说将有注解标记的java类收集起来注册进路由表中。

先来看一看这个框架是怎么将注解信息注册到路由表中的?

 private static void initIfNeed() {if (!mappings.isEmpty()) {return;}RouterInit.init();sort();}

这里的路由表就是一个private staticList<Mapping> mappings= newArrayList<>()(List集合),如果这个集合有元素的话,则表明已经注册了,否则这个调用RouterInit.init()j进行注册,RouterInit类是用apt生成的,如果在没有编译之前,这个引用肯定会报错,作者这里采用了取巧的方式,就是先写死两个类

public class RouterInit {public static void init() {}
}
public final class RouterMapping {public static final void map() {}

起到骗过编译器检查的效果,正常的使用还没有被编译注解处理器生成的java类的时候是利用反射,这个欺骗反而增加了那么点速度,编译器完成之后,新生成的java类将覆盖掉那两个写死的java类(占坑java类),在前面的APT生成java类时已经提到过,最终生成的类最后是调用了这个框架已经存在的类方法,也就是Routers.map的方法

来看一下这个方法,如下:

 mappings.add(new Mapping(format, activity, method, extraTypes));

直接向表中装填mapping对象,这里注意一下第三个参数,如果Router注解标记的是类,那么第三个参数为null,如果标记的是方法,那么第三个参数为MethodInvoker引用,第二个参数为null。

下面是路由表信息对象Mapping的构造函数:

 public Mapping(String format, Class<? extends Activity> activity, MethodInvoker method, ExtraTypes extraTypes) {if (format == null) {throw new NullPointerException("format can not be null");}this.format = format;this.activity = activity;this.method = method;this.extraTypes = extraTypes;if (format.toLowerCase().startsWith("http://") || format.toLowerCase().startsWith("https://")) {this.formatPath = Path.create(Uri.parse(format));} else {this.formatPath = Path.create(Uri.parse("helper://".concat(format)));}}

这个构造函数主要做的工作就是将域名解析为Path类,如果域名以http或https开头那表示是在配置文件中配置了下面这些东东

<action android:name="android.intent.action.VIEW" /><category android:name="android.intent.category.DEFAULT" /><category android:name="android.intent.category.BROWSABLE" /><dataandroid:host="mzule.com"android:scheme="http" />

接下来看一下 Path.create的方法干了些什么?如下:

 public static Path create(Uri uri) {//鍒涘缓helperPath path = new Path(uri.getScheme().concat("://"));//得到路径String urlPath = uri.getPath();if (urlPath == null) {urlPath = "";}//截取掉最后一个/if (urlPath.endsWith("/")) {urlPath = urlPath.substring(0, urlPath.length() - 1);}parse(path, uri.getHost() + urlPath);return path;}//有多少种路径可以找到它就用多少种pathprivate static void parse(Path scheme, String s) {String[] components = s.split("/");Path curPath = scheme;for (String component : components) {Path temp = new Path(component);curPath.next = temp;curPath = temp;}}

这个方法根据host和path将当前的path创建了一个链表,表头的path持有Scheme,然后依次链接host和path以"/"分开的字符串,打个比方,如果Uri是zp://www.zp.com/path/zp的话那么表头的Path为value为zp://,而后一个Path的value为www.zp.com,第三个Path的value为path,第四个Path的value为zp,也就是说总共产生了四个path的链表。

ok,将Activity的映射信息注册到路由表后,那么又回到doOpen方法,遍历路由表信息看看有没有匹配的Path有的话跳转,没有的话回调notFound方法,接下来假设要跳转的url为router://main?id=1103&updateTime=537896&web=true,这里会创建一个拥有两个Path的链表,然后遍历Mapping,调用下面这个方法看Path是否匹配

 public boolean match(Path fullLink) {if (formatPath.isHttp()) {return Path.match(formatPath, fullLink);} else {// fullLink without hostboolean match = Path.match(formatPath.next(), fullLink.next());if (!match && fullLink.next() != null) {// fullLink with hostmatch = Path.match(formatPath.next(), fullLink.next().next());}return match;}}

这个方法也很简单,循环判断链表下面是否所有的Path的value都相等(这里要排除掉带:的参数),如果相等那就说明匹配到了,注意这里去掉表头的Scheme的比较

public static boolean match(final Path format, final Path link) {if (format == null || link == null) {return false;}if (format.length() != link.length()) {return false;}Path x = format;Path y = link;while (x != null) {if (!x.match(y)) {return false;}x = x.next;y = y.next;}return true;}

假设此时路由表中已经找到匹配的Activity的映射信息了,那么接下来就需要将Uri里面的传的参数截取出来,将参数传递转化为bundle传递,如下方法所示:

public Bundle parseExtras(Uri uri) {Bundle bundle = new Bundle();// path segments // ignore schemePath p = formatPath.next();Path y = Path.create(uri).next();while (p != null) {if (p.isArgument()) {put(bundle, p.argument(), y.value());}p = p.next();y = y.next();}// parameterSet<String> names = UriCompact.getQueryParameterNames(uri);for (String name : names) {String value = uri.getQueryParameter(name);put(bundle, name, value);}return bundle;}

这里的router://main?id=1103&updateTime=537896&web=true参数为id=1103&updateTime=537896&web=true,三个参数,这里需要做的就是将这些字符串取出来,按参数的key名字和value截取出来分别存在集合中,如下所示:

 public static Set<String> getQueryParameterNames(Uri uri) {String query = uri.getEncodedQuery();if (query == null) {return Collections.emptySet();}Set<String> names = new LinkedHashSet<String>();int start = 0;do {int next = query.indexOf('&', start);int end = (next == -1) ? query.length() : next;int separator = query.indexOf('=', start);if (separator > end || separator == -1) {separator = end;}String name = query.substring(start, separator);names.add(Uri.decode(name));// Move start to end of name.start = end + 1;} while (start < query.length());return Collections.unmodifiableSet(names);}

最后一步就是将参数转化为不同的类型

 private void put(Bundle bundle, String name, String value) {int type = extraTypes.getType(name);name = extraTypes.transfer(name);if (type == ExtraTypes.STRING) {type = extraTypes.getType(name);}switch (type) {case ExtraTypes.INT:bundle.putInt(name, Integer.parseInt(value));break;case ExtraTypes.LONG:bundle.putLong(name, Long.parseLong(value));break;case ExtraTypes.BOOL:bundle.putBoolean(name, Boolean.parseBoolean(value));break;case ExtraTypes.SHORT:bundle.putShort(name, Short.parseShort(value));break;case ExtraTypes.FLOAT:bundle.putFloat(name, Float.parseFloat(value));break;case ExtraTypes.DOUBLE:bundle.putDouble(name, Double.parseDouble(value));break;case ExtraTypes.BYTE:bundle.putByte(name, Byte.parseByte(value));break;case ExtraTypes.CHAR:bundle.putChar(name, value.charAt(0));break;default:bundle.putString(name, value);break;}}

在APT中解析参数类型的时候,每一个mapping都有一个唯一的ExtraTypes类来储存不同的参数类型,以参数的名字标记之

 public int getType(String name) {if (arrayContain(intExtra, name)) {return INT;}if (arrayContain(longExtra, name)) {return LONG;}if (arrayContain(booleanExtra, name)) {return BOOL;}if (arrayContain(shortExtra, name)) {return SHORT;}if (arrayContain(floatExtra, name)) {return FLOAT;}if (arrayContain(doubleExtra, name)) {return DOUBLE;}if (arrayContain(byteExtra, name)) {return BYTE;}if (arrayContain(charExtra, name)) {return CHAR;}return STRING;}

其实这个方法就是直接根据参数的名字去ExtraTypes集合中去找有没有这个名字的存储,如果有这个名字的话,那么将类型提出出来,也就是已经确定类型了

  private String[] intExtra;private String[] longExtra;private String[] booleanExtra;private String[] shortExtra;private String[] floatExtra;private String[] doubleExtra;private String[] byteExtra;private String[] charExtra;

ExtraTypes类总共有这么多集合,在编译期已经将参数名字保存到ExtraTypes中,最后通过新生成的java文件,调用RouterInit.init()方法将ExtraTypes和mapping绑定,从而达到在传参数时的动态转化。好了,在android的实现一个路由架构的原理基本就介绍完了,欢迎小伙伴点赞和留言。

组件化开发之路由器模块详解(ActivityRouter源码详解)相关推荐

  1. Vue-Watcher观察者源码详解

    源码调试地址 https://github.com/KingComedy/vue-debugger 什么是Watcher Watcher是Vue中的观察者类,主要任务是:观察Vue组件中的属性,当属性 ...

  2. Android组件化开发详解

    学习目标: 熟练使用组件化开发,路由配置 学习内容: 在使用组件化开发前首先要明确项目整体框架,划分模块及业务(重点),好的开始才会有好的结果.模块划分明确后开始配置Module. 如图我们要完成以下 ...

  3. react基础 - 模块与组件 - 组件化开发

    一,模块与组件 1. 模块:       理解: 向外提供特定功能的js程序, 一般就是一个js文件       为什么: js代码更多更复杂       作用: 复用js, 简化js的编写, 提高j ...

  4. 微信小程序 基础3【组件化开发、自定义组件、全栈开发、使用Express】

    视频地址: https://www.bilibili.com/video/BV1cW411T7t6  [2018]学做小程序- 清华大学 https://www.bilibili.com/video/ ...

  5. React心得之降龙十八掌:第二式-飞龙在天( React组件化开发及相关概念)

    引言 (乾卦九五)<彖>曰:"'飞龙在天',大人造也." 学习了上一章,想必我们对于React有了一个初步的认识,了解了什么是React.什么是JSX.模块与组件.组件 ...

  6. 【流媒体开发】VLC Media Player - Android 平台源码编译 与 二次开发详解 (提供详细800M下载好的编译源码及eclipse可调试播放器源码下载)

    作者 : 韩曙亮  博客地址 : http://blog.csdn.net/shulianghan/article/details/42707293 转载请注明出处 : http://blog.csd ...

  7. Vue.js组件化开发实践

    Vue.js组件化开发实践 前言 公司目前制作一个H5活动,特别是有一定统一结构的活动,都要码一个重复的轮子.后来接到一个基于模板的活动设计系统的需求,便有了一下的内容.首先会对使用Vue进行开发的一 ...

  8. VUE.JS 组件化开发实践

    前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家.点击跳转到教程. 前言 公司目前制作一个H5活动,特别是有一定统一结构的活动,都要码一个重复的轮子.后来接到一个基于模 ...

  9. [Vue.js] 深入 -- 组件化开发

    组件化开发思想 现实中的组件化思想体现 标准 分治 重用 组合 组件注册 全局组件注册语法 Vue.component(组件名称,{data:组件数据,template:组件模板内容 }) 组件用法 ...

最新文章

  1. 李开复「预见2021」:自动化成企业升级转型刚需 | AI日报
  2. asp.net gridview 模板列 弹出窗口编辑_连云港各种新型铝模板设计软件,哪家强_威尔达建材...
  3. Java 进阶 ——2019 计划要读的书
  4. Python第一天学习---基础语法
  5. php 获取key的位置,PHP获取当前所在目录位置的方法
  6. 信息学奥赛一本通 2055:【例3.5】收费
  7. 信息学奥赛一本通C++语言——1050:骑车与走路
  8. [2018.09.08 T2] 最大土地面积
  9. 使用GMM进行语音性别检测(入门)
  10. matlab autocad选哪个,cad哪个版本最好用,如何选择?
  11. 学会 Python 到底能干嘛?我们整理出了 7 大工作方向……
  12. 国外计算机论文范文精选,国外计算机论文参考范文.doc
  13. linux 清除dns缓存
  14. 大数据兼云计算(王明龙)讲师-JAVA-DAY05-基本数据类型
  15. 使用Typora编辑器编写md文档插入图片方法
  16. 阿里P1到P10,你的能力能拿多少年薪?
  17. 小企业如何利用区块链和大数据获利?
  18. nz-select 选择器
  19. nginx代理常见问题
  20. 【NLP】第12章 检测客户情绪以做出预测

热门文章

  1. java中实现word(doc、docx)中完美提取文字、表格为结构化数据
  2. 大脑的默认网络有哪些脑区组成,其具有那些功能?The Brain’s Default Mode Network
  3. jsp/java mysql图书馆管理系统毕业设计网站成品论文
  4. 全国主要城市交通卡芯片一览,看看有没有你的家乡……
  5. dive into python 3_对象方法Dive into Python读书笔记3
  6. 步态识别新论文学习——《Gait Recognition from a Single Image using a Phase-Aware Gait Cycle Reconstruction Netw》
  7. 中国oracle考试认证考点查询网站
  8. 闲鱼卖家待发货在哪看
  9. python大数据培训班学费
  10. 全球社交软件月活排行 微信排第五