精华内容
参与话题
问答
  • 什么是组件化?   在看了很多其他的方案之后,首先对组件化思想上有一个小分歧。我认为很多人对于 iOS 中组件化的理解其实是有误区的。我刚工作的第一年就是在做 Flex 开发,其中就有很多组件化的思想,加上最近在...

    什么是组件化?

      在看了很多其他的方案之后,首先对组件化思想上有一个小分歧。我认为很多人对于 iOS 中组件化的理解其实是有误区的。做 Flex 开发,其中就有很多组件化的思想,加上最近在用 Vue 做 web 项目之后,更为意识到大家在 iOS 开发上说的组件化有点不合适。
      首先我认为组件是一个相对比较小的功能模块,不需要与外界有太多的通信,更不能依赖其他第三方,这一点尤为重要。比如说几乎所有 iOS 开发者知道的 MJRefresh,几乎不依赖业务,并且提供了良好的调用接口和使用体验的才能称为组件。而看了很多方案,大部分都是在讲 App 里面的业务能组件之间的通信和解耦,其实我更愿意将这些东西称为 “模块”。那如何区分这两种呢,我觉得这句话比较好理解:核心业务模块化,通用功能组件化
      打比方说你的 App 是一个电商项目,name 你的产品详情页、列表页、购物车、搜索等页面肯定就是调用频次非常高的 VC 了,这些界面之间跳转都会非常频繁。这就造成了互相依赖并且高度耦合。如下图所示:
    在这里插入图片描述
      像商品详情页这些通常外部调入只需要传入一个 productID 就可以,而且高度依赖自己的业务功能的模块就可以将这些当成一个模块维护。后面需要修改里面的活动的显示、业务的增删都可以单独在详情模块里面改动而不需要改动别的代码。
      而对于组件,比方说我上面提到的 IM 类型的 App 中用到的聊天键盘,或者集成支付宝、微信等支付功能的支付插件。这些可以在多个不同的项目小组内部共享。甚至可以开源到社区中提供所有开发者使用的小插件,用组件来形容更贴切。在 Flex、Vue、angular 等前端开发中提现尤为突出。

    为什么要有组件化(模块化)

      客户端在公司业务发展的过程中体积越来越庞大,其中堆叠了大量的业务逻辑代码,不同业务模块的代码相互调用,相互嵌套,代码之间的耦合性越来越高,调用逻辑会越来越混乱。当某个模块需要升级的时候,改动代码的时候往往会有牵一发而动全身的感觉。特别是如果工程量出设计的时候没有考虑接口的封装,而将大量的业务代码与功能模块代码混在一起时,将来的升级就需要对代码进行大量修改及调整,带来的功工作量是非常巨大的。这就需要设计一套符合要求的组件之间通信的中间件。模块化可以将代码的功能逻辑尽量封装在一起,对外只提供接口,业务逻辑代码与功能模块通过接口进行弱耦合。

    我的模块化架构思路

    如何优化模块化之间的通信

      封装模块的工作只要你对面向对象思想有所理解,实现起来应该不难,确保写好调用接口就行,这里不再赘述。而模块化最重要的就是各个模块之间的通信。比如在商品搜索列表页面,需要查看购物车功能和查看商品详情功能,购物车的商品列表也能点击商品到商品详情页。等等这些页面之间都会相互调用,相互依赖。通常我们会怎么实现呢?比如这样:

    #import "ProductDetailViewController.h"
    #import "CartViewController.h"
    
    @implementation ProductListViewController
    
    - (void)gotoDetail {
    	ProductDetailViewController *detailVC = [[ProductDetailViewController alloc] initWithProId:self.proId];
    	[self.navigationController pushViewController:detailVC animated:YES];
    }
    
    - (void)gotoCart {
    	CartViewController *cartVC = [[CartViewController alloc] init];
    	[self.navigationController pushViewController:cartVC animated:YES];
    }
    
    @end
    

      相信这样的代码大家都不陌生,基本都是这样做。而且这样也并没有问题。但是,项目一旦大起来问题就来了。各个模块只要有相互调用的情况,都会相互产生依赖。每次跳转都需要 import 对应的控制器,重写一次代码。如果某个地方做了一点点需求改动,比如商品详情页需要多传入一个参数,这个时候就要找到各个调用的地方逐一修改,这显然不是高效的办法。

    运用中间件

      于是很简单的就想到了一个方法,提供一个中间层:Router。在 router 里面定义好每次跳转的方法,然后再需要用的界面调用 router 函数,传入对应的参数。比如这样:
    在这里插入图片描述

    // Router.m
    #import "ProductDetailViewController.h"
    #import "CartViewController.h"
    
    @implementation Router
    
    + (UIViewController *)getDetailWithParam:(NSString *)param {
    	ProductDetailViewController *detailVC = [[ProductDetailViewController alloc] initWithProId:self.proId];
    	return detailVC;
    }
    
    + (UIVIewController *)getCart {
    	CartViewController *cartVC = [[CartViewController alloc]init];
    	return cartVC;
    }
    
    @end
    

      其他界面中这样使用:

    #import "Router.m"
    
    UIViewController *detailVC = [[Router instance] jumpToDetailWithParam:param];
    [self.navigationController pushViewController:detailVC];
    
    运行 runtime

      但是这样写的话也有一个问题,每个 vc 都会依赖 Router,而 Router 里面会依赖所有的 VC。name如何打破这层循环引用呢?OC 里有个法宝可以用到:runtime。

    - (UIViewController *)getViewController:(NSString *)stringVCName {
    	Class class = NSClassFromString(stringVCName);
    	UIViewController *controller = [[class alloc]init];
    	if (controller == nil) {
    		NSLog("未找到此类:%@",stringVCName);
    		controller = [[RouterError sharedInstance] getErrorController];
    	}
    	return controller;
    }
    

      这样上面的图就是这样的:
    在这里插入图片描述
      这样 Router 里面不需要 import 任何 VC 了,代码也就数十行而已,看起来非常的简便。而且做了异常处理,如果找不到此类,会返回预先设置的错误界面。是不是有点类似于 web 开发中的 404 界面呢?

    UIViewController *controller = [[Router shaedInstance] getViewController:@"ProductDetailViewController"];
    [self.navigationController pushViewController:controller];
    
    如何传参数

      很多人肯定都发现了,这样写的话如何传递参数呢。比如商品详情页至少要传一个 productID 吧。别急,我们可以将上面的方法稍微处理,传入一个 dict 做了参数:

    - (UIViewController *)getViewController:(NSString *)stringVCName  {
    	Class class = NSClassFromString(stringVCName);
    	UIViewController *controller = [[class alloc]init];
    	return controller;
    }
    
    - (UIViewController *)getViewController:(NSString *)stringVCName witParam:(NSDictionary *)paramdic {
    	UIViewController *controller = [self getViewController:stringVCName];
    	if (controller != nil) {
    		controller = [self controller:controller withParam:paramdict andVCname:stringVCName];
    	} else {
    		NSLog(@"未找到此类:%@",stringVCName);
    		// EXCEPTION Push 啊 Normal Error VC
    		controller = [[RouterError sharedInstance] getErrorController];
    	}
    	return controller;
    }
    
    /**
    	此方法用来初始化参数(控制器初始化方法默认为 initViewControllerParam。初始化方法可以自定义,前提是 VC 必须实现它。要想灵活一点,也可以添加一个参数 actionName,当做参数传入。不过这样你就需要修改此方法了)。
    	@param controller 获取到的实例 VC
    	@param paramdic 实例化参数
    	@param vcName 控制器名字
    	@return 初始化之后的VC
    */
    - (UIViewController *)controller:(UIViewController *)controller withParam:(NSDictionary *)paradic andVCname:(NSString *)vcName {
    	SEL selector = NSSelectorFromString(@"initViewControllerParam:");
    	if(![controller respondsToSelector:selector]) { // 如果没定义初始化参数方法,直接返回,没必要在往下做设置参数的方法
    		NSLog(@"目标类:%@ 未定义:%@方法",controller,@"initViewControllerParam:");
    		return controller;
    	}
    	// 在初始化参数里面添加 key 信息,方便控制器中检验路由信息
    	if (paradic == nil) {
    		paramdic = [[NSMutableDictionary alloc] init];
    		[paradic setValue:vcName forKey:@"URLKEY"];
    		SuppressPerformSlelctorLeakWarning([controller performSelector:selector withObject:paramdic]);
    	} else {
    		[paramdic setValue:vcName forKey:@"URLKEY"];
    	}
    	SuppressPerformSelecorLeakWarning([controller performSelector:selector withObject:paramdic]);
    	return controller;
    }
    

      我们默认在业务控制器里面有个 initViewControllerParam 方法,然后再 router 里面可以用 respondsToSelector 手动触发这个方法,传入参数 paramdic。当然如果你想要更加灵活一点,那就将 initViewControllerParam 初始化方法当做一个 actionName 参数传到 router 里面。类似于这样:

    - (UIViewController *)controller:(UIViewController *)controller withParam:(NSDictionary *)paramdic andVCName:(NSString *)vcName actionName:(NSString *)actionName {
    	SEL selector = NSSelectorFromString(actionName);
    	... 后面就是一样的代码了
    }
    

      到这里基本上模块化就是可以实现了。基本上通过超过 100 行的代码解决各个复杂业务模块之间的通信和高度解耦。

    总结

      模块化的实现方法在 iOS 开发汇总算是比较好实现的,主要是 OC 本身就是一门动态的语言。对象类型是加上是在运行时中确定的,而调用方法在 OC 中是以发消息的形式实现。这就增加了很多可以操作的可能性。这种方法在大部分的 App 中都可能很好的应用,并且解决大部分的业务需求。

    展开全文
  • Vue 组件化开发

    万次阅读 2018-12-20 15:50:21
    Vue 组件化开发 提示: 本次分享知识点基于 vue.js,需要对 vue.js 有一定的了解。 什么叫做组件化 vue.js 有两大法宝,一个是数据驱动,另一个就是组件化,那么问题来了,什么叫做组件化,为什么要组件化?接下来我...

    Vue 组件化开发

    提示: 本次分享知识点基于 vue.js,需要对 vue.js 有一定的了解。

    什么叫做组件化

    vue.js 有两大法宝,一个是数据驱动,另一个就是组件化,那么问题来了,什么叫做组件化,为什么要组件化?接下来我就针对这两个问题一一解答,所谓组件化,就是把页面拆分成多个组件,每个组件依赖的 CSS、JS、模板、图片等资源放在一起开发和维护。 因为组件是资源独立的,所以组件在系统内部可复用,组件和组件之间可以嵌套,如果项目比较复杂,可以极大简化代码量,并且对后期的需求变更和维护也更加友好。

    image.png

    参考: 组件基础

    如何进行组件化开发

    先看下图:

    image.png

    这是 vue.js 中的一个报错,原因是使用了一个未经注册的组件 lx-xxx ,这个报错告诉我们一个道理:使用自定义组件之前必须注册。  
    那么如何注册一个组件呢? Vue.js 提供了 2 种组件的注册方式,全局注册局部注册

    1. 全局注册

    在 vue.js 中我们可以使用 Vue.component(tagName, options) 进行全局注册,例如

    Vue.component('my-component', {
      // 选项
    })
    

    2. 局部注册

    Vue.js 也同样支持局部注册,我们可以在一个组件内部使用 components 选项做组件的局部注册,例如:

    import HelloWorld from './components/HelloWorld'
    
    export default {
      components: {
        HelloWorld
      }
    }
    

    区别:全局组件是挂载在 Vue.options.components 下,而局部组件是挂载在 vm.$options.components 下,这也是全局注册的组件能被任意使用的原因。

    组件化开发必备知识

    所谓工欲善其事,必先利其器,在正式开发一个组件之前,我们先要掌握一些必备的知识,这里我只会简单介绍一下,详情参考官网。

    name

    组件的名称,必填

    <lx-niu/>
    <lx-niu></lx-niu/>
    
    name: 'lxNiu'
    

    js 中使用驼峰式命令,HTML 使用kebab-case命名。

    props

    组件属性,用于父子组件通信,可通过this.msg访问

    <div>{{msg}}</div>
    
    props: {
      msg: {
        type: String,
        default: ''
      }
    }
    
    show: Boolean // 默认false
    
    msg: [String, Boolean]  // 多种类型
    

    computed

    处理data或者props中的属性,并返回一个新属性

    <div>{{newMsg}}</div>
    
    computed: {
      newMsg() {
        return 'hello ' + this.msg
      }
    },
    

    注:因为props,data和computed在编译阶段都会作为vm的属性合并,所以不可重名

    render

    用render函数描述template

    <lx-niu tag='button'>hello world</lx-niu>
    
    <script type="text/javascript">
      export default {
        name: 'lxNiu',
        props: {
          tag: {
            type: String,
            default: 'div'
          },
        },
        // h: createElement
        render(h) {
          return h(this.tag,
            {class: 'demo'}, 
            this.$slots.default)
        }
      }
    </script>
    

    render 中的 h 其实就是 createElement,它接受三个参数,返回一个 vnode  
    h 参数解释:  
    args1: {string | Function | Object} 用于提供DOM的html内容  
    args2: {Object} 设置DOM样式、属性、绑定事件之类  
    args3: {array} 用于设置分发的内容

    注:vue编译顺序: template–> compile --> render --> vnode --> patch --> DOM

    slot

    分发内容,有传入时显示,无传入 DOM 时显示默认,分为无名和具名两种,this.slots.defaultslotthis.slots.default 默认指向无名插槽,多个 slot 时用法 this.slots.name

    <lx-niu>
      <div slot='header'>header</div>
      <div class="body" slot='body'>
        <input type="text">
      </div>
      <div slot='footer'>footer</div>
    
      <button class='btn'>button</button>
    </lx-niu>
    
    <template>
      <div>
        <slot name='header'></slot>
        <slot name='body'></slot>
        <slot name='footer'></slot>
        <slot></slot>
      </div>
    </template>
    
    <script>
      export default {
        name: 'lxNiu',
        mounted() {
          this.$slots.header // 包含了slot="foo"的内容
          this.$slots.default // 得到一个vnode,没有被包含在具名插槽中的节点,这里是button
        }
      }
    </script>
    

    class

    定义子组件的类名

    // 父组件
    <lx-niu round type='big'/>
    
    // 子组件
    <div :class="[
      type ? 'lx-niu__' + type : '',
      {'is-round': round},
    ]">控制</div>
    
    //真实DOM
    <div class='lx-niu__big is-round'>hello</div>
    

    style

    向子组件传递样式

    // 父组件
    <lx-niu :bodyStyle='{color: "red"}'/>
    
    
    // 子组件
    <template>
      <div :style='bodyStyle'>hello world</div>
    </template>
    
    <script>
      export default {
        name: 'lxNiu',
        props: {
          bodyStyle: {},
        },
      }
    </script>
    

    其他属性

    $attrs

    v-bind="$attrs" 将除class和style外的属性添加到父组件上,如定义input:

    <input v-bind="$attrs">
    

    v-once

    组件只渲染一次,后面即使数据发生变化也不会重新渲染,比如例子中val不会变成456

    <template>
      <div>
        <button @click="show = !show">button</button>
        <button @click="val = '456'">button</button>
        <div v-once v-if="show">
          <span>{{val}}</span>
        </div>
      </div>
    </template>
    
    <script>
    export default {
      data() {
        return{
          show: false,
          val: '123'
        }
      },
    };
    </script>
    

    mixins

    // mixin.js
    export default {
      data() {
        return{
           msg: 'hello world'
        }
      },
      methods: {
        clickBtn() {
          console.log(this.msg)
        }
      },
    }
    
    // index.vue
    <button @click="clickBtn">button</button>
    
    import actionMixin from "./mixin.js";
    export default {
      methods: {},
      mixins: [actionMixin]
    }
    

    实例演示

    比如我们要注册一个 lx-button 这样一个组件,那么目录和伪代码如下:

    image.png

    index.vue

    <template>
      <button>lxButton</button>
    </template>
    
    <script>
    export default {
      name: 'lxButton'
    }
    </script>
    

    index.js

    import lxButton from './src/index'
    
    lxButton.install = (Vue) => {
      Vue.component(lxButton.name, lxButton)
    }
    
    export default lxButton
    

    其中 install 是 Vue.js 提供了一个公开方法,这个方法的第一个参数是 Vue 构造器,第二个参数是一个可选的选项对象。
    MyPlugin.install = function (Vue, options){}

    参考: 开发插件

    watch-弹窗实现原理

    <button @click="dialogVisible = true">显示</button>
    <lx-niu :visible.sync="dialogVisible"></lx-niu>
    
    <script>
      export default {
        data() {
          return {
            dialogVisible: false
          }
        },
        watch: {
          dialogVisible(val) {
            console.log('father change', val)
          }
        }
      }
    </script>
    

    定义组件

    <template>
      <div v-show="visible">
        <button @click="hide">关闭</button>
      </div>
    </template>
    
    <script>
      export default {
        name: 'lxNiu',
        props: {
          visible: Boolean
        },
        watch: {
          visible(val) {
            console.log('child change:', val)
          }
        },
        methods: {
          hide() {
            this.$emit('update:visible', false);
          }
        },
      }
    </script>
    

    点击父组件中的显示按钮,改变传入子组件中的值,点击子组件中的关闭,改变父组件中值。

    注:@click=“dialogVisible = true” 点击时将dialogVisible的值改为true  
    注::visible.sync: 双向数据绑定,配合update:visible使用,实现子组件修改父组件中的值

    官网解释: sync

    col组件实例

    export default {
      name: 'ElCol',
    
      props: {
        span: {
          type: Number,
          default: 24
        },
        tag: {
          type: String,
          default: 'div'
        },
        offset: Number,
        pull: Number,
        push: Number,
        xs: [Number, Object],
        sm: [Number, Object],
        md: [Number, Object],
        lg: [Number, Object],
        xl: [Number, Object]
      },
    
      computed: {
        gutter() {
          let parent = this.$parent;
          while (parent && parent.$options.componentName !== 'ElRow') {
            parent = parent.$parent;
          }
          return parent ? parent.gutter : 0;
        }
      },
      render(h) {
        let classList = [];
        let style = {};
    
        if (this.gutter) {
          style.paddingLeft = this.gutter / 2 + 'px';
          style.paddingRight = style.paddingLeft;
        }
    
        ['span', 'offset', 'pull', 'push'].forEach(prop => {
          if (this[prop] || this[prop] === 0) {
            classList.push(
              prop !== 'span'
                ? `el-col-${prop}-${this[prop]}`
                : `el-col-${this[prop]}`
            );
          }
        });
    
        ['xs', 'sm', 'md', 'lg', 'xl'].forEach(size => {
          if (typeof this[size] === 'number') {
            classList.push(`el-col-${size}-${this[size]}`);
          } else if (typeof this[size] === 'object') {
            let props = this[size];
            Object.keys(props).forEach(prop => {
              classList.push(
                prop !== 'span'
                  ? `el-col-${size}-${prop}-${props[prop]}`
                  : `el-col-${size}-${props[prop]}`
              );
            });
          }
        });
    
        return h(this.tag, {
          class: ['el-col', classList],
          style
        }, this.$slots.default);
      }
    };
    

    col组件使用render函数,而不是template来实现组件,原因有两个:

    1. 该组件有大量的类判断,如果采用template代码比较冗余,使用js代码更加简洁
    2. 直接render描述性能更好

    官网解释: render-function

    button组件实例

    <template>
      <button
        class="el-button"
        @click="handleClick"
        :disabled="buttonDisabled || loading"
        :autofocus="autofocus"
        :type="nativeType"
        :class="[
          type ? 'el-button--' + type : '',
          buttonSize ? 'el-button--' + buttonSize : '',
          {
            'is-disabled': buttonDisabled,
            'is-loading': loading,
            'is-plain': plain,
            'is-round': round,
            'is-circle': circle
          }
        ]"
      >
        <i class="el-icon-loading" v-if="loading"></i>
        <i :class="icon" v-if="icon && !loading"></i>
        <span v-if="$slots.default"><slot></slot></span>
      </button>
    </template>
    <script>
      export default {
        name: 'ElButton',
    
        inject: {
          elForm: {
            default: ''
          },
          elFormItem: {
            default: ''
          }
        },
    
        props: {
          type: {
            type: String,
            default: 'default'
          },
          size: String,
          icon: {
            type: String,
            default: ''
          },
          nativeType: {
            type: String,
            default: 'button'
          },
          loading: Boolean,
          disabled: Boolean,
          plain: Boolean,
          autofocus: Boolean,
          round: Boolean,
          circle: Boolean
        },
    
        computed: {
          _elFormItemSize() {
            return (this.elFormItem || {}).elFormItemSize;
          },
          buttonSize() {
            return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
          },
          buttonDisabled() {
            return this.disabled || (this.elForm || {}).disabled;
          }
        },
    
        methods: {
          handleClick(evt) {
            this.$emit('click', evt);
          }
        }
      };
    </script>
    

    局部组件实例

    <template>
      <div class="login">
        <login-header />
        <login-request />
        <login-footer />
      </div>
    </template>
    
    <script>
    import loginHeader from './login-header';
    import loginRequest from './login-request';
    import loginFooter from './login-footer';
    
    export default {
      components: {
        [loginHeader.name]: loginHeader,
        [loginRequest.name]: loginRequest,
        [loginFooter.name]: loginFooter
      }
    };
    </script>
    

    8. 分享总结

    • 首先介绍了什么是组件化开发,以及为什么要进行组件化
    • 其次介绍了组件开发的两种方式和适用场景,以及进行组件化开发的必备知识
    • 最后实例演示了全局组件和局部组件的开发

    参考:  
    Vue.js 技术揭秘  
    element-ui

    展开全文
  • Android组件化方案

    万次阅读 多人点赞 2017-02-15 19:01:52
    随着APP版本不断的迭代,新功能的不断增加,业务也会变的越来越复杂,APP业务模块的数量有可能还会继续增加,而且每个模块的代码也变的越来越多,这样发展下去单一工程下的APP架构势必会影响开发效率,增加项目的...

    版权声明:本文为博主原创文章,欢迎大家转载!
    转载请标明出处: http://blog.csdn.net/guiying712/article/details/55213884 ,本文出自:【张华洋的博客】


    Android组件化项目地址:Android组件化项目AndroidModulePattern

    Android组件化之终极方案地址:http://blog.csdn.net/guiying712/article/details/78057120

    1、为什么要项目组件化

    随着APP版本不断的迭代,新功能的不断增加,业务也会变的越来越复杂,APP业务模块的数量有可能还会继续增加,而且每个模块的代码也变的越来越多,这样发展下去单一工程下的APP架构势必会影响开发效率,增加项目的维护成本,每个工程师都要熟悉如此之多的代码,将很难进行多人协作开发,而且Android项目在编译代码的时候电脑会非常卡,又因为单一工程下代码耦合严重,每修改一处代码后都要重新编译打包测试,导致非常耗时,最重要的是这样的代码想要做单元测试根本无从下手,所以必须要有更灵活的架构代替过去单一的工程架构。

    单一工程模型

    上图是目前比较普遍使用的Android APP技术架构,往往是在一个界面中存在大量的业务逻辑,而业务逻辑中充斥着各种网络请求、数据操作等行为,整个项目中也没有模块的概念,只有简单的以业务逻辑划分的文件夹,并且业务之间也是直接相互调用、高度耦合在一起的;

    单一工程模型下的业务关系

    上图单一工程模型下的业务关系,总的来说就是:你中有我,我中有你,相互依赖,无法分离。
    然而随着产品的迭代,业务越来越复杂,随之带来的是项目结构复杂度的极度增加,此时我们会面临如下几个问题:

    1、实际业务变化非常快,但是单一工程的业务模块耦合度太高,牵一发而动全身;
    2、对工程所做的任何修改都必须要编译整个工程;
    3、功能测试和系统测试每次都要进行;
    4、团队协同开发存在较多的冲突.不得不花费更多的时间去沟通和协调,并且在开发过程中,任何一位成员没办法专注于自己的功能点,影响开发效率;
    5、不能灵活的对业务模块进行配置和组装;

    为了满足各个业务模块的迭代而彼此不受影响,更好的解决上面这种让人头疼的依赖关系,就需要整改App的架构。


    2、如何组件化

    组件化工程模型

    上图是组件化工程模型,为了方便理解这张架构图,下面会列举一些组件化工程中用到的名词的含义:

    名词 含义
    集成模式 所有的业务组件被“app壳工程”依赖,组成一个完整的APP;
    组件模式 可以独立开发业务组件,每一个业务组件就是一个APP;
    app壳工程 负责管理各个业务组件,和打包apk,没有具体的业务功能;
    业务组件 根据公司具体业务而独立形成一个的工程;
    功能组件 提供开发APP的某些基础功能,例如打印日志、树状图等;
    Main组件 属于业务组件,指定APP启动页面、主界面;
    Common组件 属于功能组件,支撑业务组件的基础,提供多数业务组件需要的功能,例如提供网络请求功能;

    **
    Android APP组件化架构的目标是告别结构臃肿,让各个业务变得相对独立,业务组件在组件模式下可以独立开发,而在集成模式下又可以变为arr包集成到“app壳工程”中,组成一个完整功能的APP;
    从组件化工程模型中可以看到,业务组件之间是独立的,没有关联的,这些业务组件在集成模式下是一个个library,被app壳工程所依赖,组成一个具有完整业务功能的APP应用,但是在组件开发模式下,业务组件又变成了一个个application,它们可以独立开发和调试,由于在组件开发模式下,业务组件们的代码量相比于完整的项目差了很远,因此在运行时可以显著减少编译时间。

    组件化工程下的业务关系

    这是组件化工程模型下的业务关系,业务之间将不再直接引用和依赖,而是通过“路由”这样一个中转站间接产生联系,而Android中的路由实际就是对URL Scheme的封装;
    如此规模大的架构整改需要付出更高的成本,还会涉及一些潜在的风险,但是整改后的架构能够带来很多好处:

    1、加快业务迭代速度,各个业务模块组件更加独立,不再出现业务耦合情况;
    2、稳定的公共模块采用依赖库方式,提供给各个业务线使用,减少重复开发和维护工作量;
    3、迭代频繁的业务模块采用组件方式,各业务研发可以互不干扰、提升协作效率,并控制产品质量;
    4、为新业务随时集成提供了基础,所有业务可上可下,灵活多变;
    5、降低团队成员熟悉项目的成本,降低项目的维护难度;
    6、加快编译速度,提高开发效率;
    7、控制代码权限,将代码的权限细分到更小的粒度;


    3、组件化实施流程

    1)组件模式和集成模式的转换

    Android Studio中的Module主要有两种属性,分别为:

    1、application属性,可以独立运行的Android程序,也就是我们的APP;

    apply plugin: ‘com.android.application’

    2、library属性,不可以独立运行,一般是Android程序依赖的库文件;

    apply plugin: ‘com.android.library’

    Module的属性是在每个组件的 build.gradle 文件中配置的,当我们在组件模式开发时,业务组件应处于application属性,这时的业务组件就是一个 Android App,可以独立开发和调试;而当我们转换到集成模式开发时,业务组件应该处于 library 属性,这样才能被我们的“app壳工程”所依赖,组成一个具有完整功能的APP;

    但是我们如何让组件在这两种模式之间自动转换呢?总不能每次需要转换模式的时候去每个业务组件的 Gralde 文件中去手动把 Application 改成 library 吧?如果我们的项目只有两三个组件那么这个办法肯定是可行的,手动去改一遍也用不了多久,但是在大型项目中我们可能会有十几个业务组件,再去手动改一遍必定费时费力,这时候就需要程序员发挥下懒的本质了。

    试想,我们经常在写代码的时候定义静态常量,那么定义静态常量的目的什么呢?当一个常量需要被好几处代码引用的时候,把这个常量定义为静态常量的好处是当这个常量的值需要改变时我们只需要改变静态常量的值,其他引用了这个静态常量的地方都会被改变,做到了一次改变,到处生效;根据这个思想,那么我们就可以在我们的代码中的某处定义一个决定业务组件属性的常量,然后让所有业务组件的build.gradle都引用这个常量,这样当我们改变了常量值的时候,所有引用了这个常量值的业务组件就会根据值的变化改变自己的属性;可是问题来了?静态常量是用Java代码定义的,而改变组件属性是需要在Gradle中定义的,Gradle能做到吗?

    Gradle自动构建工具有一个重要属性,可以帮助我们完成这个事情。每当我们用AndroidStudio创建一个Android项目后,就会在项目的根目录中生成一个文件 gradle.properties,我们将使用这个文件的一个重要属性:在Android项目中的任何一个build.gradle文件中都可以把gradle.properties中的常量读取出来;那么我们在上面提到解决办法就有了实际行动的方法,首先我们在gradle.properties中定义一个常量值 isModule(是否是组件开发模式,true为是,false为否)

    # 每次更改“isModule”的值后,需要点击 "Sync Project" 按钮
    isModule=false

    然后我们在业务组件的build.gradle中读取 isModule,但是 gradle.properties 还有一个重要属性: gradle.properties 中的数据类型都是String类型,使用其他数据类型需要自行转换;也就是说我们读到 isModule 是个String类型的值,而我们需要的是Boolean值,代码如下:

    if (isModule.toBoolean()) {
        apply plugin: 'com.android.application'
    } else {
        apply plugin: 'com.android.library'
    }

    这样我们第一个问题就解决了,当然了 每次改变isModule的值后,都要同步项目才能生效;

    2)组件之间AndroidManifest合并问题

    在 AndroidStudio 中每一个组件都会有对应的 AndroidManifest.xml,用于声明需要的权限、Application、Activity、Service、Broadcast等,当项目处于组件模式时,业务组件的 AndroidManifest.xml 应该具有一个 Android APP 所具有的的所有属性,尤其是声明 Application 和要 launch的Activity,但是当项目处于集成模式的时候,每一个业务组件的 AndroidManifest.xml 都要合并到“app壳工程”中,要是每一个业务组件都有自己的 Application 和 launch的Activity,那么合并的时候肯定会冲突,试想一个APP怎么可能会有多个 Application 和 launch 的Activity呢?

    但是大家应该注意到这个问题是在组件开发模式和集成开发模式之间转换引起的问题,而在上一节中我们已经解决了组件模式和集成模式转换的问题,另外大家应该都经历过将 Android 项目从 Eclipse 切换到 AndroidStudio 的过程,由于 Android 项目在 Eclipse 和 AndroidStudio开发时 AndroidManifest.xml 文件的位置是不一样的,我们需要在build.gradle 中指定下 AndroidManifest.xml 的位置,AndroidStudio 才能读取到 AndroidManifest.xml,这样解决办法也就有了,我们可以为组件开发模式下的业务组件再创建一个 AndroidManifest.xml,然后根据isModule指定AndroidManifest.xml的文件路径,让业务组件在集成模式和组件模式下使用不同的AndroidManifest.xml,这样表单冲突的问题就可以规避了。

    业务组件的目录结构

    上图是组件化项目中一个标准的业务组件目录结构,首先我们在main文件夹下创建一个module文件夹用于存放组件开发模式下业务组件的 AndroidManifest.xml,而 AndroidStudio 生成的 AndroidManifest.xml 则依然保留,并用于集成开发模式下业务组件的表单;然后我们需要在业务组件的 build.gradle 中指定表单的路径,代码如下:

      sourceSets {
            main {
                if (isModule.toBoolean()) {
                    manifest.srcFile 'src/main/module/AndroidManifest.xml'
                } else {
                    manifest.srcFile 'src/main/AndroidManifest.xml'
                }
            }
        }

    这样在不同的开发模式下就会读取到不同的 AndroidManifest.xml ,然后我们需要修改这两个表单的内容以为我们不同的开发模式服务。

    首先是集成开发模式下的 AndroidManifest.xml,前面我们说过集成模式下,业务组件的表单是绝对不能拥有自己的 Application 和 launch 的 Activity的,也不能声明APP名称、图标等属性,总之app壳工程有的属性,业务组件都不能有,下面是一份标准的集成开发模式下业务组件的 AndroidManifest.xml:

    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.guiying.girls">
    
        <application android:theme="@style/AppTheme">
            <activity
                android:name=".main.GirlsActivity"
                android:screenOrientation="portrait" />
            <activity
                android:name=".girl.GirlActivity"
                android:screenOrientation="portrait"
                android:theme="@style/AppTheme.NoActionBar" />
        </application>
    
    </manifest>
    

    我在这个表单中只声明了应用的主题,而且这个主题还是跟app壳工程中的主题是一致的,都引用了common组件中的资源文件,在这里声明主题是为了方便这个业务组件中有使用默认主题的Activity时就不用再给Activity单独声明theme了。

    然后是组件开发模式下的表单文件:

    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.guiying.girls">
    
        <application
            android:name="debug.GirlsApplication"
            android:allowBackup="true"
            android:icon="@mipmap/ic_launcher"
            android:label="@string/girls_name"
            android:supportsRtl="true"
            android:theme="@style/AppTheme">
            <activity
                android:name=".main.GirlsActivity"
                android:screenOrientation="portrait">
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
    
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
            </activity>
            <activity
                android:name=".girl.GirlActivity"
                android:screenOrientation="portrait"
                android:theme="@style/AppTheme.NoActionBar" />
        </application>
    
    </manifest>

    组件模式下的业务组件表单就是一个Android项目普通的AndroidManifest.xml,这里就不在过多介绍了。


    3)全局Context的获取及组件数据初始化

    当Android程序启动时,Android系统会为每个程序创建一个 Application 类的对象,并且只创建一个,application对象的生命周期是整个程序中最长的,它的生命周期就等于这个程序的生命周期。在默认情况下应用系统会自动生成 Application 对象,但是如果我们自定义了 Application,那就需要在 AndroidManifest.xml 中声明告知系统,实例化的时候,是实例化我们自定义的,而非默认的。

    但是我们在组件化开发的时候,可能为了数据的问题每一个组件都会自定义一个Application类,如果我们在自己的组件中开发时需要获取 全局的Context,一般都会直接获取 application 对象,但是当所有组件要打包合并在一起的时候就会出现问题,因为最后程序只有一个 Application,我们组件中自己定义的 Application 肯定是没法使用的,因此我们需要想办法再任何一个业务组件中都能获取到全局的 Context,而且这个 Context 不管是在组件开发模式还是在集成开发模式都是生效的。

    在 组件化工程模型图中,功能组件集合中有一个 Common 组件, Common 有公共、公用、共同的意思,所以这个组件中主要封装了项目中需要的基础功能,并且每一个业务组件都要依赖Common组件,Common 组件就像是万丈高楼的地基,而业务组件就是在 Common 组件这个地基上搭建起来我们的APP的,Common 组件会专门在一个章节中讲解,这里只讲 Common组件中的一个功能,在Common组件中我们封装了项目中用到的各种Base类,这些基类中就有BaseApplication 类

    BaseApplication 主要用于各个业务组件和app壳工程中声明的 Application 类继承用的,只要各个业务组件和app壳工程中声明的Application类继承了 BaseApplication,当应用启动时 BaseApplication 就会被动实例化,这样从 BaseApplication 获取的 Context 就会生效,也就从根本上解决了我们不能直接从各个组件获取全局 Context 的问题;

    这时候大家肯定都会有个疑问?不是说了业务组件不能有自己的 Application 吗,怎么还让他们继承 BaseApplication 呢?其实我前面说的是业务组件不能在集成模式下拥有自己的 Application,但是这不代表业务组件也不能在组件开发模式下拥有自己的Application,其实业务组件在组件开发模式下必须要有自己的 Application 类,一方面是为了让 BaseApplication 被实例化从而获取 Context,还有一个作用是,业务组件自己的 Application 可以在组件开发模式下初始化一些数据,例如在组件开发模式下,A组件没有登录页面也没法登录,因此就无法获取到 Token,这样请求网络就无法成功,因此我们需要在A组件这个 APP 启动后就应该已经登录了,这时候组件自己的 Application 类就有了用武之地,我们在组件的 Application的 onCreate 方法中模拟一个登陆接口,在登陆成功后将数据保存到本地,这样就可以处理A组件中的数据业务了;另外我们也可以在组件Application中初始化一些第三方库

    但是,实际上业务组件中的Application在最终的集成项目中是没有什么实际作用的,组件自己的 Application 仅限于在组件模式下发挥功能,因此我们需要在将项目从组件模式转换到集成模式后将组件自己的Application剔除出我们的项目;在 AndroidManifest 合并问题小节中介绍了如何在不同开发模式下让 Gradle 识别组件表单的路径,这个方法也同样适用于Java代码;

    业务组件的java目录结构

    我们在Java文件夹下创建一个 debug 文件夹,用于存放不会在业务组件中引用的类,例如上图中的 NewsApplication ,你甚至可以在 debug 文件夹中创建一个Activity,然后组件表单中声明启动这个Activity,在这个Activity中不用setContentView,只需要在启动你的目标Activity的时候传递参数就行,这样就就可以解决组件模式下某些Activity需要getIntent数据而没有办法拿到的情况,代码如下;

    public class LauncherActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            request();
            Intent intent = new Intent(this, TargetActivity.class);
            intent.putExtra("name", "avcd");
            intent.putExtra("syscode", "023e2e12ed");
            startActivity(intent);
            finish();
        }
    
        //申请读写权限
        private void request() {
            AndPermission.with(this)
                    .requestCode(110)
                    .permission(Manifest.permission.WRITE_EXTERNAL_STORAGE,
                            Manifest.permission.CAMERA, Manifest.permission.READ_PHONE_STATE)
                    .callback(this)
                    .start();
        }
    
    }

    接下来在业务组件的 build.gradle 中,根据 isModule 是否是集成模式将 debug 这个 Java代码文件夹排除:

    
        sourceSets {
            main {
                if (isModule.toBoolean()) {
                    manifest.srcFile 'src/main/module/AndroidManifest.xml'
                } else {
                    manifest.srcFile 'src/main/AndroidManifest.xml'
                    //集成开发模式下排除debug文件夹中的所有Java文件
                    java {
                        exclude 'debug/**'
                    }
                }
            }
        }

    4)library依赖问题

    在介绍这一节的时候,先说一个问题,在组件化工程模型图中,多媒体组件和Common组件都依赖了日志组件,而A业务组件有同时依赖了多媒体组件和Common组件,这时候就会有人问,你这样搞岂不是日志组件要被重复依赖了,而且Common组件也被每一个业务组件依赖了,这样不出问题吗?

    其实大家完全没有必要担心这个问题,如果真有重复依赖的问题,在你编译打包的时候就会报错,如果你还是不相信的话可以反编译下最后打包出来的APP,看看里面的代码你就知道了。组件只是我们在代码开发阶段中为了方便叫的一个术语,在组件被打包进APP的时候是没有这个概念的,这些组件最后都会被打包成arr包,然后被app壳工程所依赖,在构建APP的过程中Gradle会自动将重复的arr包排除,APP中也就不会存在相同的代码了;

    但是虽然组件是不会重复了,但是我们还是要考虑另一个情况,我们在build.gradle中compile的第三方库,例如AndroidSupport库经常会被一些开源的控件所依赖,而我们自己一定也会compile AndroidSupport库 ,这就会造成第三方包和我们自己的包存在重复加载,解决办法就是找出那个多出来的库,并将多出来的库给排除掉,而且Gradle也是支持这样做的,分别有两种方式:根据组件名排除或者根据包名排除,下面以排除support-v4库为例:

    dependencies {
        compile fileTree(dir: 'libs', include: ['*.jar'])
        compile("com.jude:easyrecyclerview:$rootProject.easyRecyclerVersion") {
            exclude module: 'support-v4'//根据组件名排除
            exclude group: 'android.support.v4'//根据包名排除
        }
    }

    library重复依赖的问题算是都解决了,但是我们在开发项目的时候会依赖很多开源库,而这些库每个组件都需要用到,要是每个组件都去依赖一遍也是很麻烦的,尤其是给这些库升级的时候,为了方便我们统一管理第三方库,我们将给给整个工程提供统一的依赖第三方库的入口,前面介绍的Common库的作用之一就是统一依赖开源库,因为其他业务组件都依赖了Common库,所以这些业务组件也就间接依赖了Common所依赖的开源库。

    dependencies {
        compile fileTree(dir: 'libs', include: ['*.jar'])
        //Android Support
        compile "com.android.support:appcompat-v7:$rootProject.supportLibraryVersion"
        compile "com.android.support:design:$rootProject.supportLibraryVersion"
        compile "com.android.support:percent:$rootProject.supportLibraryVersion"
        //网络请求相关
        compile "com.squareup.retrofit2:retrofit:$rootProject.retrofitVersion"
        compile "com.squareup.retrofit2:retrofit-mock:$rootProject.retrofitVersion"
        compile "com.github.franmontiel:PersistentCookieJar:$rootProject.cookieVersion"
        //稳定的
        compile "com.github.bumptech.glide:glide:$rootProject.glideVersion"
        compile "com.orhanobut:logger:$rootProject.loggerVersion"
        compile "org.greenrobot:eventbus:$rootProject.eventbusVersion"
        compile "com.google.code.gson:gson:$rootProject.gsonVersion"
        compile "com.github.chrisbanes:PhotoView:$rootProject.photoViewVersion"
    
        compile "com.jude:easyrecyclerview:$rootProject.easyRecyclerVersion"
        compile "com.github.GrenderG:Toasty:$rootProject.toastyVersion"
    
        //router
        compile "com.github.mzule.activityrouter:activityrouter:$rootProject.routerVersion"
    }

    5)组件之间调用和通信

    在组件化开发的时候,组件之间是没有依赖关系,我们不能在使用显示调用来跳转页面了,因为我们组件化的目的之一就是解决模块间的强依赖问题,假如现在要从A业务组件跳转到业务B组件,并且要携带参数跳转,这时候怎么办呢?而且组件这么多怎么管理也是个问题,这时候就需要引入“路由”的概念了,由本文开始的组件化模型下的业务关系图可知路由就是起到一个转发的作用。

    这里我将介绍开源库的“ActivityRouter” ,有兴趣的同学情直接去ActivityRouter的Github主页学习:ActivityRouter,ActivityRouter支持给Activity定义 URL,这样就可以通过 URL 跳转到Activity,并且支持从浏览器以及 APP 中跳入我们的Activity,而且还支持通过 url 调用方法。下面将介绍如何将ActivityRouter集成到组件化项目中以实现组件之间的调用;

    1、首先我们需要在 Common 组件中的 build.gradle 将ActivityRouter 依赖进来,方便我们在业务组件中调用:

    dependencies {
        compile fileTree(dir: 'libs', include: ['*.jar'])
        //router
        compile "com.github.mzule.activityrouter:activityrouter:$rootProject.routerVersion"
    }

    2、这一步我们需要先了解 APT这个概念,APT(Annotation Processing Tool)是一种处理注解的工具,它对源代码文件进行检测找出其中的Annotation,使用Annotation进行额外的处理。 Annotation处理器在处理Annotation时可以根据源文件中的Annotation生成额外的源文件和其它的文件(文件具体内容由Annotation处理器的编写者决定),APT还会编译生成的源文件和原来的源文件,将它们一起生成class文件。在这里我们将在每一个业务组件的 build.gradle 都引入ActivityRouter 的 Annotation处理器,我们将会在声明组件和Url的时候使用,annotationProcessor是Android官方提供的Annotation处理器插件,代码如下:

    
    dependencies {
        compile fileTree(dir: 'libs', include: ['*.jar'])
        annotationProcessor "com.github.mzule.activityrouter:compiler:$rootProject.annotationProcessor"
    }

    3、接下来需要在 app壳工程的 AndroidManifest.xml 配置,到这里ActivityRouter配置就算完成了:

     <!--声明整个应用程序的路由协议-->
            <activity
                android:name="com.github.mzule.activityrouter.router.RouterActivity"
                android:theme="@android:style/Theme.NoDisplay">
                <intent-filter>
                    <action android:name="android.intent.action.VIEW" />
    
                    <category android:name="android.intent.category.DEFAULT" />
                    <category android:name="android.intent.category.BROWSABLE" />
    
                    <data android:scheme="@string/global_scheme" /> <!-- 改成自己的scheme -->
                </intent-filter>
            </activity>
            <!--发送崩溃日志界面-->

    4、接下来我们将声明项目中的业务组件,声明方法如下:

    @Module("girls")
    public class Girls {
    }

    在每一个业务组件的java文件的根目录下创建一个类,用 注解@Module 声明这个业务组件;
    然后在“app壳工程”的 应用Application 中使用 注解@Modules 管理我们声明的所有业务组件,方法如下:

    @Modules({"main", "girls", "news"})
    public class MyApplication extends BaseApplication {
    }

    到这里组件化项目中的所有业务组件就被声明和管理起来了,组件之间的也就可以互相调用了,当然前提是要给业务组件中的Activity定义 URL。

    5、例如我们给 Girls组件 中的 GirlsActivity 使用 注解@Router 定义一个 URL:“news”,方法如下:

    @Router("girls")
    public class GirlsActivity extends BaseActionBarActivity {
    
        private GirlsView mView;
        private GirlsContract.Presenter mPresenter;
    
        @Override
        protected int setTitleId() {
            return R.string.girls_activity_title;
        }
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            mView = new GirlsView(this);
            setContentView(mView);
            mPresenter = new GirlsPresenter(mView);
            mPresenter.start();
        }
    }

    然后我们就可以在项目中的任何一个地方通过 URL地址 : module://girls, 调用 GirlsActivity,方法如下:

           Routers.open(MainActivity.this, "module://girls");

    组件之间的调用解决后,另外需要解决的就是组件之间的通信,例如A业务组件中有消息列表,而用户在B组件中操作某个事件后会产生一条新消息,需要通知A组件刷新消息列表,这样业务场景需求可以使用Android广播来解决,也可以使用第三方的事件总线来实现,比如EventBus


    6)组件之间资源名冲突

    因为我们拆分出了很多业务组件和功能组件,在把这些组件合并到“app壳工程”时候就有可能会出现资源名冲突问题,例如A组件和B组件都有一张叫做“ic_back”的图标,这时候在集成模式下打包APP就会编译出错,解决这个问题最简单的办法就是在项目中约定资源文件命名规约,比如强制使每个资源文件的名称以组件名开始,这个可以根据实际情况和开发人员制定规则。当然了万能的Gradle构建工具也提供了解决方法,通过在在组件的build.gradle中添加如下的代码:

        //设置了resourcePrefix值后,所有的资源名必须以指定的字符串做前缀,否则会报错。
        //但是resourcePrefix这个值只能限定xml里面的资源,并不能限定图片资源,所有图片资源仍然需要手动去修改资源名。
        resourcePrefix "girls_"

    但是设置了这个属性后有个问题,所有的资源名必须以指定的字符串做前缀,否则会报错,而且resourcePrefix这个值只能限定xml里面的资源,并不能限定图片资源,所有图片资源仍然需要手动去修改资源名;所以我并不推荐使用这种方法来解决资源名冲突。


    4、组件化项目的工程类型

    在组件化工程模型中主要有:app壳工程、业务组件和功能组件3种类型,而业务组件中的Main组件和功能组件中的Common组件比较特殊,下面将分别介绍。

    1)app壳工程

    app壳工程是从名称来解释就是一个空壳工程,没有任何的业务代码,也不能有Activity,但它又必须被单独划分成一个组件,而不能融合到其他组件中,是因为它有如下几点重要功能:

    1、app壳工程中声明了我们Android应用的 Application,这个 Application 必须继承自 Common组件中的 BaseApplication(如果你无需实现自己的Application可以直接在表单声明BaseApplication),因为只有这样,在打包应用后才能让BaseApplication中的Context生效,当然你还可以在这个 Application中初始化我们工程中使用到的库文件,还可以在这里解决Android引用方法数不能超过 65535 的限制,对崩溃事件的捕获和发送也可以在这里声明。

    2、app壳工程的 AndroidManifest.xml 是我Android应用的根表单,应用的名称、图标以及是否支持备份等等属性都是在这份表单中配置的,其他组件中的表单最终在集成开发模式下都被合并到这份 AndroidManifest.xml 中。

    3、app壳工程的 build.gradle 是比较特殊的,app壳不管是在集成开发模式还是组件开发模式,它的属性始终都是:com.android.application,因为最终其他的组件都要被app壳工程所依赖,被打包进app壳工程中,这一点从组件化工程模型图中就能体现出来,所以app壳工程是不需要单独调试单独开发的。另外Android应用的打包签名,以及buildTypes和defaultConfig都需要在这里配置,而它的dependencies则需要根据isModule的值分别依赖不同的组件,在组件开发模式下app壳工程只需要依赖Common组件,或者为了防止报错也可以根据实际情况依赖其他功能组件,而在集成模式下app壳工程必须依赖所有在应用Application中声明的业务组件,并且不需要再依赖任何功能组件。

    下面是一份 app壳工程 的 build.gradle文件

    apply plugin: 'com.android.application'
    
    static def buildTime() {
        return new Date().format("yyyyMMdd");
    }
    
    android {
        signingConfigs {
            release {
                keyAlias 'guiying712'
                keyPassword 'guiying712'
                storeFile file('/mykey.jks')
                storePassword 'guiying712'
            }
        }
    
        compileSdkVersion rootProject.ext.compileSdkVersion
        buildToolsVersion rootProject.ext.buildToolsVersion
        defaultConfig {
            applicationId "com.guiying.androidmodulepattern"
            minSdkVersion rootProject.ext.minSdkVersion
            targetSdkVersion rootProject.ext.targetSdkVersion
            versionCode rootProject.ext.versionCode
            versionName rootProject.ext.versionName
            multiDexEnabled false
            //打包时间
            resValue "string", "build_time", buildTime()
        }
    
        buildTypes {
            release {
                //更改AndroidManifest.xml中预先定义好占位符信息
                //manifestPlaceholders = [app_icon: "@drawable/icon"]
                // 不显示Log
                buildConfigField "boolean", "LEO_DEBUG", "false"
                //是否zip对齐
                zipAlignEnabled true
                // 缩减resource文件
                shrinkResources true
                //Proguard
                minifyEnabled true
                proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
                //签名
                signingConfig signingConfigs.release
            }
    
            debug {
                //给applicationId添加后缀“.debug”
                applicationIdSuffix ".debug"
                //manifestPlaceholders = [app_icon: "@drawable/launch_beta"]
                buildConfigField "boolean", "LOG_DEBUG", "true"
                zipAlignEnabled false
                shrinkResources false
                minifyEnabled false
                debuggable true
            }
        }
    
    
    }
    
    dependencies {
        compile fileTree(dir: 'libs', include: ['*.jar'])
        annotationProcessor "com.github.mzule.activityrouter:compiler:$rootProject.annotationProcessor"
        if (isModule.toBoolean()) {
            compile project(':lib_common')
        } else {
            compile project(':module_main')
            compile project(':module_girls')
            compile project(':module_news')
        }
    }

    2)功能组件和Common组件

    功能组件是为了支撑业务组件的某些功能而独立划分出来的组件,功能实质上跟项目中引入的第三方库是一样的,功能组件的特征如下:

    1、功能组件的 AndroidManifest.xml 是一张空表,这张表中只有功能组件的包名;

    2、功能组件不管是在集成开发模式下还是组件开发模式下属性始终是: com.android.library,所以功能组件是不需要读取 gradle.properties 中的 isModule 值的;另外功能组件的 build.gradle 也无需设置 buildTypes ,只需要 dependencies 这个功能组件需要的jar包和开源库。

    下面是一份 普通 的功能组件的 build.gradle文件

    apply plugin: 'com.android.library'
    
    android {
        compileSdkVersion rootProject.ext.compileSdkVersion
        buildToolsVersion rootProject.ext.buildToolsVersion
    
        defaultConfig {
            minSdkVersion rootProject.ext.minSdkVersion
            targetSdkVersion rootProject.ext.targetSdkVersion
            versionCode rootProject.ext.versionCode
            versionName rootProject.ext.versionName
        }
    
    }
    
    dependencies {
        compile fileTree(dir: 'libs', include: ['*.jar'])
    }

    Common组件除了有功能组件的普遍属性外,还具有其他功能

    1、Common组件的 AndroidManifest.xml 不是一张空表,这张表中声明了我们 Android应用用到的所有使用权限 uses-permission 和 uses-feature,放到这里是因为在组件开发模式下,所有业务组件就无需在自己的 AndroidManifest.xm 声明自己要用到的权限了。

    2、Common组件的 build.gradle 需要统一依赖业务组件中用到的 第三方依赖库和jar包,例如我们用到的ActivityRouter、Okhttp等等。

    3、Common组件中封装了Android应用的 Base类和网络请求工具、图片加载工具等等,公用的 widget控件也应该放在Common 组件中;业务组件中都用到的数据也应放于Common组件中,例如保存到 SharedPreferences 和 DataBase 中的登陆数据;

    4、Common组件的资源文件中需要放置项目公用的 Drawable、layout、sting、dimen、color和style 等等,另外项目中的 Activity 主题必须定义在 Common中,方便和 BaseActivity 配合保持整个Android应用的界面风格统一。

    下面是一份 Common功能组件的 build.gradle文件

    apply plugin: 'com.android.library'
    
    android {
        compileSdkVersion rootProject.ext.compileSdkVersion
        buildToolsVersion rootProject.ext.buildToolsVersion
    
        defaultConfig {
            minSdkVersion rootProject.ext.minSdkVersion
            targetSdkVersion rootProject.ext.targetSdkVersion
            versionCode rootProject.ext.versionCode
            versionName rootProject.ext.versionName
        }
    
    }
    
    dependencies {
        compile fileTree(dir: 'libs', include: ['*.jar'])
        //Android Support
        compile "com.android.support:appcompat-v7:$rootProject.supportLibraryVersion"
        compile "com.android.support:design:$rootProject.supportLibraryVersion"
        compile "com.android.support:percent:$rootProject.supportLibraryVersion"
        //网络请求相关
        compile "com.squareup.retrofit2:retrofit:$rootProject.retrofitVersion"
        compile "com.squareup.retrofit2:retrofit-mock:$rootProject.retrofitVersion"
        compile "com.github.franmontiel:PersistentCookieJar:$rootProject.cookieVersion"
        //稳定的
        compile "com.github.bumptech.glide:glide:$rootProject.glideVersion"
        compile "com.orhanobut:logger:$rootProject.loggerVersion"
        compile "org.greenrobot:eventbus:$rootProject.eventbusVersion"
        compile "com.google.code.gson:gson:$rootProject.gsonVersion"
        compile "com.github.chrisbanes:PhotoView:$rootProject.photoViewVersion"
    
        compile "com.jude:easyrecyclerview:$rootProject.easyRecyclerVersion"
        compile "com.github.GrenderG:Toasty:$rootProject.toastyVersion"
    
        //router
        compile "com.github.mzule.activityrouter:activityrouter:$rootProject.routerVersion"
    }
    

    2)业务组件和Main组件

    业务组件就是根据业务逻辑的不同拆分出来的组件,业务组件的特征如下:

    1、业务组件中要有两张AndroidManifest.xml,分别对应组件开发模式和集成开发模式,这两张表的区别请查看 组件之间AndroidManifest合并问题 小节。

    2、业务组件在集成模式下是不能有自己的Application的,但在组件开发模式下又必须实现自己的Application并且要继承自Common组件的BaseApplication,并且这个Application不能被业务组件中的代码引用,因为它的功能就是为了使业务组件从BaseApplication中获取的全局Context生效,还有初始化数据之用。

    3、业务组件有debug文件夹,这个文件夹在集成模式下会从业务组件的代码中排除掉,所以debug文件夹中的类不能被业务组件强引用,例如组件模式下的 Application 就是置于这个文件夹中,还有组件模式下开发给目标 Activity 传递参数的用的 launch Activity 也应该置于 debug 文件夹中;

    4、业务组件必须在自己的 Java文件夹中创建业务组件声明类,以使 app壳工程 中的 应用Application能够引用,实现组件跳转,具体请查看 组件之间调用和通信 小节;

    5、业务组件必须在自己的 build.gradle 中根据 isModule 值的不同改变自己的属性,在组件模式下是:com.android.application,而在集成模式下com.android.library;同时还需要在build.gradle配置资源文件,如 指定不同开发模式下的AndroidManifest.xml文件路径,排除debug文件夹等;业务组件还必须在dependencies中依赖Common组件,并且引入ActivityRouter的注解处理器annotationProcessor,以及依赖其他用到的功能组件。

    下面是一份普通业务组件的 build.gradle文件

    if (isModule.toBoolean()) {
        apply plugin: 'com.android.application'
    } else {
        apply plugin: 'com.android.library'
    }
    
    android {
        compileSdkVersion rootProject.ext.compileSdkVersion
        buildToolsVersion rootProject.ext.buildToolsVersion
    
        defaultConfig {
            minSdkVersion rootProject.ext.minSdkVersion
            targetSdkVersion rootProject.ext.targetSdkVersion
            versionCode rootProject.ext.versionCode
            versionName rootProject.ext.versionName
        }
    
        sourceSets {
            main {
                if (isModule.toBoolean()) {
                    manifest.srcFile 'src/main/module/AndroidManifest.xml'
                } else {
                    manifest.srcFile 'src/main/AndroidManifest.xml'
                    //集成开发模式下排除debug文件夹中的所有Java文件
                    java {
                        exclude 'debug/**'
                    }
                }
            }
        }
    
        //设置了resourcePrefix值后,所有的资源名必须以指定的字符串做前缀,否则会报错。
        //但是resourcePrefix这个值只能限定xml里面的资源,并不能限定图片资源,所有图片资源仍然需要手动去修改资源名。
        //resourcePrefix "girls_"
    
    
    }
    
    dependencies {
        compile fileTree(dir: 'libs', include: ['*.jar'])
        annotationProcessor "com.github.mzule.activityrouter:compiler:$rootProject.annotationProcessor"
        compile project(':lib_common')
    }

    Main组件除了有业务组件的普遍属性外,还有一项重要功能

    1、Main组件集成模式下的AndroidManifest.xml是跟其他业务组件不一样的,Main组件的表单中声明了我们整个Android应用的launch Activity,这就是Main组件的独特之处;所以我建议SplashActivity、登陆Activity以及主界面都应属于Main组件,也就是说Android应用启动后要调用的页面应置于Main组件。

            <activity
                android:name=".splash.SplashActivity"
                android:launchMode="singleTop"
                android:screenOrientation="portrait"
                android:theme="@style/SplashTheme">
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
    
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
            </activity>

    5、组件化项目的混淆方案

    组件化项目的Java代码混淆方案采用在集成模式下集中在app壳工程中混淆,各个业务组件不配置混淆文件。集成开发模式下在app壳工程中build.gradle文件的release构建类型中开启混淆属性,其他buildTypes配置方案跟普通项目保持一致,Java混淆配置文件也放置在app壳工程中,各个业务组件的混淆配置规则都应该在app壳工程中的混淆配置文件中添加和修改。

    之所以不采用在每个业务组件中开启混淆的方案,是因为 组件在集成模式下都被 Gradle 构建成了 release 类型的arr包,一旦业务组件的代码被混淆,而这时候代码中又出现了bug,将很难根据日志找出导致bug的原因;另外每个业务组件中都保留一份混淆配置文件非常不便于修改和管理,这也是不推荐在业务组件的 build.gradle 文件中配置 buildTypes (构建类型)的原因。


    6、工程的build.gradle和gradle.properties文件

    1)组件化工程的build.gradle文件

    在组件化项目中因为每个组件的 build.gradle 都需要配置 compileSdkVersion、buildToolsVersion和defaultConfig 等的版本号,而且每个组件都需要用到 annotationProcessor,为了能够使组件化项目中的所有组件的 build.gradle 中的这些配置都能保持统一,并且也是为了方便修改版本号,我们统一在Android工程根目录下的build.gradle中定义这些版本号,当然为了方便管理Common组件中的第三方开源库的版本号,最好也在这里定义这些开源库的版本号,然后在各个组件的build.gradle中引用Android工程根目录下的build.gradle定义的版本号,组件化工程的 build.gradle 文件代码如下:

    buildscript {
        repositories {
            jcenter()
            mavenCentral()
        }
    
        dependencies {
            //classpath "com.android.tools.build:gradle:$localGradlePluginVersion"
            //$localGradlePluginVersion是gradle.properties中的数据
            classpath "com.android.tools.build:gradle:$localGradlePluginVersion"
        }
    }
    
    allprojects {
        repositories {
            jcenter()
            mavenCentral()
            //Add the JitPack repository
            maven { url "https://jitpack.io" }
            //支持arr包
            flatDir {
                dirs 'libs'
            }
        }
    }
    
    task clean(type: Delete) {
        delete rootProject.buildDir
    }
    
    // Define versions in a single place
    //时间:2017.2.13;每次修改版本号都要添加修改时间
    ext {
        // Sdk and tools
        //localBuildToolsVersion是gradle.properties中的数据
        buildToolsVersion = localBuildToolsVersion
        compileSdkVersion = 25
        minSdkVersion = 16
        targetSdkVersion = 25
        versionCode = 1
        versionName = "1.0"
        javaVersion = JavaVersion.VERSION_1_8
    
        // App dependencies version
        supportLibraryVersion = "25.3.1"
        retrofitVersion = "2.1.0"
        glideVersion = "3.7.0"
        loggerVersion = "1.15"
        eventbusVersion = "3.0.0"
        gsonVersion = "2.8.0"
        photoViewVersion = "2.0.0"
    
        //需检查升级版本
        annotationProcessor = "1.1.7"
        routerVersion = "1.2.2"
        easyRecyclerVersion = "4.4.0"
        cookieVersion = "v1.0.1"
        toastyVersion = "1.1.3"
    }
    

    2)组件化工程的gradle.properties文件

    在组件化实施流程中我们了解到gradle.properties有两个属性对我们非常有用:

    1、在Android项目中的任何一个build.gradle文件中都可以把gradle.properties中的常量读取出来,不管这个build.gradle是组件的还是整个项目工程的build.gradle;

    2、gradle.properties中的数据类型都是String类型,使用其他数据类型需要自行转换;

    利用gradle.properties的属性不仅可以解决集成开发模式和组件开发模式的转换,而且还可以解决在多人协同开发Android项目的时候,因为开发团队成员的Android开发环境(开发环境指Android SDK和AndroidStudio)不一致而导致频繁改变线上项目的build.gradle配置。

    在每个Android组件的 build.gradle 中有一个属性:buildToolsVersion,表示构建工具的版本号,这个属性值对应 AndroidSDK 中的 Android SDK Build-tools,正常情况下 build.gradle 中的 buildToolsVersion 跟你电脑中 Android SDK Build-tools 的最新版本是一致的,比如现在 Android SDK Build-tools 的最新的版本是:25.0.3,那么我的Android项目中 build.gradle 中的 buildToolsVersion 版本号也是 25.0.3,但是一旦一个Android项目是由好几个人同时开发,总会出现每个人的开发环境 Android SDK Build-tools 是都是不一样的,并不是所有人都会经常升级更新 Android SDK,而且代码是保存到线上环境的(例如使用 SVN/Git 等工具),某个开发人员提交代码后线上Android项目中 build.gradle 中的 buildToolsVersion 也会被不断地改变。

    另外一个原因是因为Android工程的根目录下的 build.gradle 声明了 Android Gradle 构建工具,而这个工具也是有版本号的,而且 Gradle Build Tools 的版本号跟 AndroidStudio 版本号一致的,但是有些开发人员基本很久都不会升级自己的 AndroidStudio 版本,导致团队中每个开发人员的 Gradle Build Tools 的版本号也不一致。

    如果每次同步代码后这两个工具的版本号被改变了,开发人员可以自己手动改回来,并且不要把改动工具版本号的代码提交到线上环境,这样还可以勉强继续开发;但是很多公司都会使用持续集成工具(例如Jenkins)用于持续的软件版本发布,而Android出包是需要 Android SDK Build-tools 和 Gradle Build Tools 配合的,一旦提交到线上的版本跟持续集成工具所依赖的Android环境构建工具版本号不一致就会导致Android打包失败。

    为了解决上面问题就必须将Android项目中 build.gradle 中的 buildToolsVersion 和 GradleBuildTools 版本号从线上代码隔离出来,保证线上代码的 buildToolsVersion 和 Gradle Build Tools 版本号不会被人为改变。

    具体的实施流程大家可以查看我的这篇博文 AndroidStudio本地化配置gradle的buildToolsVersion和gradleBuildTools


    7、组件化项目Router的其他方案-ARouter

    在组件化项目中使用到的跨组件跳转库ActivityRouter可以使用阿里巴巴的开源路由项目:阿里巴巴ARouter

    ActivityRouter和ARouter的接入组件化项目的方式是一样的,ActivityRouter提供的功能目前ARouter也全部支持,但是ARouter还支持依赖注入解耦,页面、拦截器、服务等组件均会自动注册到框架。对于大家来说,没有最好的只有最适合的,大家可以根据自己的项目选择合适的Router。

    下面将介绍ARouter的基础使用方法,更多功能还需大家去Github自己学习;

    1、首先 ARouter 这个框架是需要初始化SDK的,所以你需要在“app壳工程”中的应用Application中加入下面的代码,注意:在 debug 模式下一定要 openDebug

        if (BuildConfig.DEBUG) {
                //一定要在ARouter.init之前调用openDebug
                ARouter.openDebug();
                ARouter.openLog();
           }
           ARouter.init(this);

    2、首先我们依然需要在 Common 组件中的 build.gradle 将ARouter 依赖进来,方便我们在业务组件中调用:

    dependencies {
        compile fileTree(dir: 'libs', include: ['*.jar'])
        //router
        compile 'com.alibaba:arouter-api:1.2.1.1'
    }

    3、然后在每一个业务组件的 build.gradle 都引入ARouter 的 Annotation处理器,代码如下:

    
    android {
        defaultConfig {
        ...
        javaCompileOptions {
            annotationProcessorOptions {
            arguments = [ moduleName : project.getName() ]
            }
        }
        }
    }
    
    dependencies {
        compile fileTree(dir: 'libs', include: ['*.jar'])
        annotationProcessor 'com.alibaba:arouter-compiler:1.0.3'
    }

    4、由于ARouter支持自动注册到框架,所以我们不用像ActivityRouter那样在各个组件中声明组件,当然更不需要在Application中管理组件了。 我们给 Girls组件 中的 GirlsActivity 添加注解:@Route(path = “/girls/list”),需要注意的是这里的路径至少需要有两级,/xx/xx,之所以这样是因为ARouter使用了路径中第一段字符串(/*/)作为分组,比如像上面的”girls”,而分组这个概念就有点类似于ActivityRouter中的组件声明 @Module ,代码如下:

    @Route(path = "/girls/list")
    public class GirlsActivity extends BaseActionBarActivity {
    
        private GirlsView mView;
        private GirlsContract.Presenter mPresenter;
    
        @Override
        protected int setTitleId() {
            return R.string.girls_activity_title;
        }
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            mView = new GirlsView(this);
            setContentView(mView);
            mPresenter = new GirlsPresenter(mView);
            mPresenter.start();
        }
    }

    然后我们就可以在项目中的任何一个地方通过 URL地址 : /girls/list, 调用 GirlsActivity,方法如下:

          ARouter.getInstance().build("/girls/list").navigation();

    8、结束语

    组件化相比于单一工程优势是显而易见的:

    1. 组件模式下可以加快编译速度,提高开发效率;
    2. 自由选择开发框架(MVC /MVP / MVVM /);
    3. 方便做单元测试;
    4. 代码架构更加清晰,降低项目的维护难度;
    5. 适合于团队开发;

    最后贴出Android组件化Demo地址:Android组件化项目AndroidModulePattern

    想要学习更多Android组件化知识,请查看 :Android组件化之终极方案


    感谢以下文章提供的帮助
    1. http://www.cnblogs.com/chenxibobo/p/6187954.html
    2. https://kymjs.com/code/2016/10/18/01/
    3. https://zhuanlan.zhihu.com/p/23388989
    4. https://zhuanlan.zhihu.com/p/23147164?refer=moduth

    感谢以下开源项目
    1. https://github.com/mzule/ActivityRouter
    2. https://github.com/alibaba/ARouter

    展开全文
  • 前端组件化思想

    万次阅读 多人点赞 2018-02-27 09:59:14
    1.开篇   先说说为什么要写这篇文章吧:不知从什么时候开始,大家相信前端摩尔定律:“每18个月,前端难度会增加一倍”。我并不完全认可这个数字的可靠性,但是这句话的本意我还是非常肯定的。...

    1.开篇

      先说说为什么要写这篇文章吧:不知从什么时候开始,大家相信前端摩尔定律:“每18个月,前端难度会增加一倍”。我并不完全认可这个数字的可靠性,但是这句话的本意我还是非常肯定的。

      是的,前端越来越简单了,但也越来越复杂了—简单到你可以用一个Githubstarter搭建一个框架,集成所有的全家桶,涵盖单元测试和功能测试,包括部署以及发布,甚至你开发时使用的UI库都让你写不了几行css;可又复杂到如此多的框架和库层出不穷,你还没来得及学会官网的doc呢,就已经有新的替代品了,那就更别提静下心去学习其中的源码或推敲原理了,跟不上脚步强行搬砖自然略显疲惫。

      正是前端飞速的发展使得前端看似简单,但若想深入却实属不易。顺便提一句,去年6月底,ES8已经发布了,没错,你没看错,是不感觉学不动了(开玩笑了,其实也没更新啥,不会再有ES5->ES6这种跨度了)。

      所以,我近期觉得使用的框架有些多了,得静下心来沉淀沉淀—为什么要说写组件化思想呢?因为我觉得它是伴随着前端发展的一个不可或缺的设计思想,目前几大流行框架也都非常好的实现了组件化,比如ReactVueReact之前用得算是比较多了,所以本篇我决定以Vue作为基础,去谈一谈前端模块化,组件化,可维护化的设计细想。

    2.什么是组件化

      组件化并不是前端所特有的,一些其他的语言或者桌面程序等,都具有组件化的先例。确切的说,只要有UI层的展示,就必定有可以组件化的地方。简单来说,组件就是将一段UI样式和其对应的功能作为独立的整体去看待,无论这个整体放在哪里去使用,它都具有一样的功能和样式,从而实现复用,这种整体化的细想就是组件化。不难看出,组件化设计就是为了增加复用性,灵活性,提高系统设计,从而提高开发效率。

    3.组件化的演变

      如果你对JS的理解还停留在jQuery的话(jQuery本身是一个非常优秀的库),那么请跳过此文(开个玩笑)。在那个时候,大部分的前端开发应该都是十分过程式的开发:操作DOM,发起ajax请求,刷新数据,局部更新页面。这样的动作反反复复,甚至在同一个项目里同样的流程也许还要重复,其实jQuery本身也有有自己模块化的设计,有时我们也会用到类似jQuery UI等不错的库来减少工作量,但请注意,这里我只认为它是模块化的。

      频繁操作DOM,过程式的开发方式的确不怎么样。这时开始流行MV*,比如MVC,前端开始学习后端的思想,讲业务逻辑,UI,功能,可以按照不同的文件去划分,结构清晰,设计明了,开发起来也不错。在这个基础上,又有了更加不错的MVVM框架,它的出现,更加简化了前端的操作,并且将前端的UI赋予了真实意义:你所看到的任何UI,应该都对应其相应的ViewModel,即你看到的view就是真实的数据,并且实现了双向绑定,只要UI改变,UI所对应的数据也改变,反之亦然。这的确很方便,但大部分的MVVM框架,并没有实现组件化,或者说没有很好的实现组件化,因为MVVM最大的问题就是:

    • 1.执行效率,只要数据改变,它下面所有监测数据上绑定的UI一般都会去更新,效率很低,如果你操作频繁,很可能调了几十万遍(有可能层次太深或者监测了太多的数据变化)。

    • 2.由于MVVM一般需要严格的ViewModel的作用域,因此大部分情况不支持多次绑定,或者只允许绑定一个根节点做为顶层DOM渲染,这就给组件化带来了困难(不能独立的去绑定部分UI)。

      而后,在此基础上,一些新的前端框架“取其精华,去其糟粕”,开始大力推广前端组件化的开发方式,从这一点来说,ReactVue是类似的。

      但从框架本身来说,ReactVue是完全不同的,前者是单向数据流管理设计的先驱,如果非让我做一个不恰当的比较的话,我觉得React+Redux是将MVC做到了极致(action->request, reducer->controller);而后者则是后起之秀,既吸取了React的数据流管理方式(Vue本身也可以用类似React的方式去开发,但难度比较大而已,不是很Vue)的设计理念,也实现了MVVM的双向绑定和数据监控(这应该是Vue的核心了),所以Vue是比较灵活的,可以按需扩展,它才敢称自己是渐进式框架。

    PS1: 并非讨论孰好孰坏,两大框架我都很喜欢,因为都非常好的实现了组件化。

    PS2: 上面有提到模块化,个人觉得如果更广义的来讲,模块化和组件化并不在一个维度上,模块化往往是代码的设计和项目结构的设计;但很多时候在狭义的场景中,比如一个很通用的功能,也完全能够将其组件化或模块化,这两者此时十分相似,最大的区别就是组件必定是模块化的,并且往往需要实例化,也应当赋有生命周期,而模块化往往是直接引用。

    4.如何实现组件化

      我就以搜房网为例(最近房价居高不下,各个大佬还在吹各种牛x说房价不久后将白菜价,我顺便mark下看以后打谁的脸)进行demo分析。随手截图如下:

    demo1.png

    4.1分析页面布局

    demo2.png

      从大体上来看,可以分为顶部搜索,中间内容展示。而中间内容又分为part1,2,3三种类型。由于篇幅问题,本文只分析part1,2,3

      每一个part中又可以分为header(title + link)和content(每个part不一样)

    demo3.png

    4.2初步开发

    如果没有经过任何设计,也许会出现下面的代码:

    
    <template>
      <div id="app">
        <div class="nav-search">...</div>
        <div class="panel">
          <div class="part1 left">
            <div>
              <span>万科城润园楼盘动态</span>
              <a href="">更多动态>></a>
            </div>
            <div>这里是每个part里面的具体内容</div>
          </div>
          <div class="part2 right">
            <div>
              <span>楼盘故事</span>
              <a href="">更多>></a>
            </div>
            <div>这里是每个part里面的具体内容</div>
          </div>
          <div class="part3">
            <div>
              <span>万科城润园户型</span>
              <a href="">二居(1)</a>
              <a href="">三居(4)</a>
              <a href="">四居(3)</a>
              <a href="">更多>></a>
            </div>
            <div>这里是每个part里面的具体内容</div>
          </div>
        </div>
      </div>
    </template>
    
    

    其中我省略了大部分的细节实现,实际代码量应该是这里的数倍。

    这段代码有几个问题:

    • 1.part1,2,3的结构很类似,有些许重复

    • 2.实际的代码量将会很多,很难快速定位问题,维护难度较大

    4.3化繁为简

    首先我们可以将part1,2,3进行分离,这样就独立出来三个文件,那么结构上将会非常清晰

    
    <template>
      <div id="app">
        <div class="nav-search">...</div>
        <div class="panel">
          <part1 />
          <part2 />
          <part3 /> 
      </div>
    </template>
    

    这有些类似将一个大函数逐步拆解成几部分的过程,不难想象part1,2,3中的代码,必然是适用性很差,确切的说只有这里能够引用。(但我看过很多项目的代码,就是这么干的,认为自己做了组件化,抽象还不错(@_@))

    4.4组件抽象

    仔细观察part1,2,3,正如我上面所说,它们其实是很相似的:都具有相同的外层border并附有shadow,都具有抬头和显示更多,各自内容部分暂不细说的话,这三个完全就是一模一样。

    如此,我们将具有高度相似的业务数据进行抽离,实现组件的抽象。

    part.vue

    
    <template>
      <div class="part">
        <div class="hearder">
          <span>{{ title }}</span>
          <a :href="linkForMore">{{ showMore || '更多>>' }}</a>
        </div>
        <slot name="content" />
      </div>
    </template>
    

    我们将part内可以抽象的数据都做成了props,包括利用slot去做模版,同时showMore || '更多>>'也考虑到了part1的link名字和其他几个part不一致的情况。

    这样一来app.vue就更加清晰化

    
    <template>
      <div id="app">
        <div class="nav-search">...</div>
        <div class="panel">
          <part
            title="万科城润园楼盘动态"
            linkForMore="#1"
            showMore="更多动态>>"
          >
            <div slot="content">这里是part1里面的具体内容</div>
          </part>
          <part
            title="楼盘故事"
            linkForMore="#2"
          >
            <div slot="content">这里是part2里面的具体内容</div>
          </part>
          <part
            title="万科城润园户型"
            linkForMore="#3"
          >
            <div slot="content">这里是part3里面的具体内容</div>
          </part>
      </div>
    </template>
    

    这里有几点需要说明一下:

    • 1.三个part中部分UI差异应该在哪里定义?

    比如三个part的宽度都不一样,并且part1和part2可能要需要进行浮动。

    必须要记住,这种差异并不是组件本身的,<part />的设计本身应该是无浮动并且宽度占100%的,至于占谁的100%,那就取决于谁引用它,至于向左还是向右浮动,同样也取决于引用它的container需要自己去定义,在上面的代码中,app.vue就应该是<part />的container,app想要的是一个左浮动且宽度为80%的part(part1),右浮动且宽度为20%的part(part2)和一个宽度为100%的part(part3),但它们都是part,所以应该由app来设置这些差异。

    记住这一点,将给你的抽象和扩展但来事半功倍的效果。

    • 2.三个part中的数据差异应该在哪里定义?

    比如part3中,其他的part只有一个类似更多>>的link,但是它却有多个(一居,二居...)。

    这里我推荐将这种差异体现在组件内部,设计方法也很多:

    比如可以将link数组化为links;

    比如可以将更多>>看作是一个default的link,而多余的部分则是用户自定义的特殊link,这两者合并组成了links。用户自定义的默认是没有的,需要引用组件时进行传入。

    总之,只要有数据差异化,就应该结合组件本身和业务上下文将差异合理的消除在内部。

    • 3.注意组件内数据的命名方式

    一个通用的,可扩展性高的组件,必然是有非常合理的命名的,比如观察一些组件库的命名,总会出现类似list,data,content,name,key,callback,className等名词,绝对不会出现我们系统中的类似iterationList, projectName等业务名词,这些名词和任一产品和应用都无关,它与自身抽象的组件有关,只表明组件内部的数据含义,偶尔也会代表其结构,所以只有这样,才能让用户通用。

    我们在组件化时,也需要遵循这种设计原则,但库往往是想让广大开发者通用,而我们可以降低scope,做到在整个app内通用即可。所以从这个角度来说,好的组件化必然有好的BA和UX,这是大实话

    5.写在最后

    你也许会认为这样抽象没有太大的必要性,毕竟它只是一段静态UI(pure component),但任何的设计都是基于一定的复杂度才衍生出来的,其实大部分情况下这种设计都是需要将功能逻辑代码也纳入其中的,并不光只是UI(如antd, element-ui等),我这里举的例子也相对比较简单,并不想有太多的代码。

    个人认为在一个大型前端项目中,这种组件化的抽象设计是很重要的,不仅增加了复用性提高了工作效率,从某种程度上来说也反应了程序员对业务和产品设计的理解,一旦有问题或者需要功能扩展时,你就会发现之前的设计是多么的make sense(毕竟需求总是在变哪)。

    展开全文
  • 什么叫组件化开发

    万次阅读 2018-02-24 21:38:46
    转载:什么叫组件化开发? - aloo的回答 - 知乎 https://www.zhihu.com/question/29735633/answer/90873592  从第一代码农写下第一行代码开始到上个世纪的80年代的软件危机,码农一直在考虑一个问题,怎么让写代码...
  • 组件化之路

    2019-07-07 15:12:24
    于是查阅了很多的学习资料,看了几个开源的组件化实战例子与组件化方案的开源库,总结后尝试去对之前做过的一个项目进行组件化,也是对学习到的进行实践与检验。在实践的过程中很感谢板神对我的指导。 开始 ...
  • 什么是 Android 组件化

    千次阅读 2019-06-17 11:22:47
    我希望阅读本片博客的童鞋都是有一定的 Android 基础的,并且近期想实施组件化方案的.希望这篇文章能帮助到你,让你知道什么是组件化,有哪些可选的方案 什么是组件化 下面这幅图相信大家平常多多少少都能看见类似的. ...
  • 组件化

    2020-10-16 13:48:51
    文章目录组件化1.模块化2.组件化3.模块化与组件化的区别4.Calces配置组件化5.Calces如何使用项目build.gradle子模块(user)配置管理依赖及版本号1.创建config.gradle2.引入配置文件3.修改各模块内容并指定清单文件4....
  • 组件化开发

    2020-11-09 20:38:12
    vue-组件化组件的使用分为三个步骤代码 组件的使用分为三个步骤 创建组件构造器 调用Vue.extend()方法创建组件构造器 注册组件 调用Vue.component()方法注册组件 使用组件 在Vue实例的作用范围内使用组件 代码 ...
  • 阿里4轮面试,两轮面试都被问到组件化问题,面试的点各不相同,有组件化架构设计、插件化框架设计、路由架构设计、热修复设计等问题,但是最终都是殊途同归,所有的问题都汇集在这,如何对手机淘宝组架构设计?...
  • 组件化学习

    2019-09-20 14:55:20
    组件化 网络上讨论的组件化,有多种方式,记录下自己理解的内容 基于Mediator模式和Target-Action模式 该模式来自于文章iOS应用架构谈 组件化方案,主要是基于Mediator模式和Target-Action模式,中间采用了runtime...
  • 从模块化到组件化再到插件化

    万次阅读 多人点赞 2017-04-17 22:02:35
    从模块化到组件化再到插件化 参考: http://blog.xiaohansong.com/2015/10/21/IoC-and-DI/ http://blog.csdn.net/dd864140130/article/details/53645290 ... ...
  • 组件化开发和模块化开发概念辨析

    万次阅读 多人点赞 2018-01-29 00:57:06
    组件化开发和模块化开发概念辨析 网上有许多讲组件化开发、模块化开发的文章,但大家一般都是将这两个概念混为一谈的,并没有加以区分。而且实际上许多人对于组件、模块的区别也不甚明了,甚至于许多博客文章专门...
  • 前端组件化开发

    万次阅读 2016-11-28 16:40:54
    Component,中文称为组件,或者构件。使用非常比较广泛,它的核心意义在于复用,相对模块,对于依赖性有更高的要求。 Module, 中文为模块或模组。它的核心意义是分离职责,属于代码级模块的产出。它本身是提供...
  • Android组件化开发

    千次阅读 2020-02-26 14:23:21
    https://www.cnblogs.com/ldq2016/p/9073105.html 一.是什么 二.为什么 三.怎么做
  • 前端组件化开发实践

    千次阅读 2016-06-28 17:00:23
    随着前端开发复杂度的日益提升,组件化开发应运而生,并随着 FIS、React 等优秀框架的出现遍地开花。这一过程同样发生在美团,面临业务规模的快速发展和工程师团队的不断扩张,我们历经引入组件化解决资源整合问题、...
  • 教你打造一个Android组件化开发框架

    千次阅读 2017-12-03 23:21:51
    无需注解,支持任意功能调用&回调的android组件化开发框架。兼容同步&异步调用及同步&异步实现,并做到调用方式和实现方式解耦。
  • 为什么会需要组件化 随着项目的发展,业务逻辑越来越复杂,代码量越来越多,会带来: 各种业务模块耦合在一起,不利于维护,不利于新入职成员的阅读 改动一个小功能,就要重新编译整个工程,编译时间较长 代码量...
  • iOS App组件化开发实践

    千次阅读 2016-09-26 11:52:41
    iOS App组件化开发实践前因其实我们这个7人iOS开发团队并不适合组件化开发。原因是因为性价比低,需要花很多时间和经历去做这件事,带来的收益并不能彻底改变什么。但是因为有2~3个星期的空档期,并不是很忙;另外...
  • 组件化介绍 需求来源 随着项目规模不断扩大,业务模块增多,开发过程中会有多条产品线(多人或多小组开发不同的功能);如果用传统的开发模式,会导致代码臃肿,编译速度越来越慢,开发效率低下,代码维护成本越来越高. ...

空空如也

1 2 3 4 5 ... 20
收藏数 1,053,110
精华内容 421,244
关键字:

组件化