精华内容
下载资源
问答
  • 前端组件化开发实践总结 千次阅读 多人点赞
    2021-10-13 12:46:52

    自从 2010 年第一份工作接触了前后端半分离的开发方式之后,在后面的这些年里,对前端的组件化开发有了更全面一点的认识,组件化在我们的前端开发中,对提高开发效率、代码的可维护性和可复用性有很大帮助,甚至对跟设计师沟通的效率和企业的品牌形象都有着深刻的影响。这篇文章就把我在开发中总结的一些组件化开发经验分享一下。示例中的所有代码都是伪代码,你可以按照实际情况应用到 React 或 Vue 的项目中。

    前端组件化发展历史

    在讨论组件化开发之前,我们先看看前端组件化开发的发展历史。网页开发刚起步时,并没有『前端』这个概念,更不用提组件化了。当时,网页只是作为可以在浏览器中浏览的富文本文件,开发网页就是使用一些标签让浏览器解析并显示。受制于 HTML 只是描述式的语言,网页中的代码没有办法复用,即使是类似的页面,都需要复制粘贴大量的重复代码:

    <!-- index.html -->
    <nav>
      <!-- 导航 -->
    </nav>
    <main>
      <!-- 内容 -->
    </main>
    <footer>
      <!-- 页脚 -->
    </footer>
    
    <!-- blogPost.html -->
    <nav>
      <!-- 相同的导航 -->
    </nav>
    <main>
      <!-- 不同的内容 -->
    </main>
    <footer>
      <!-- 相同的页脚 -->
    </footer>
    

    后来随着模板引擎的出现,可以把网页的代码分割成片段(Fragments)或模板(Templates),例如导航,内容,页脚等等,之后在需要的地方,使用引入(Include)语法,把这些片段引入进来,从而避免重复代码,这样形成了组件化的雏形,常见的动态脚本语言都有自己的模板引擎,例如 PHP、ASP 和 JSP:

    <!-- nav.jsp -->
    <nav>
      <!-- 导航 -->
    </nav>
    
    <!-- index.jsp -->
    <jsp:include page="nav.jsp" />
    

    只是这些片段在数据管理方面仍然会比较麻烦,还是需要使用客户端 JavaScript 脚本,手动控制数据和 HTML 视图的同步。而对于样式,也还是存在全局污染的问题。
    再后来,有些高级的开发语言,例如 Java, 推出了基于服务器的组件化开发技术,例如 JSF (JSP 的后继),基于 MVC 设计模式,通过 Java Class (POJO) 定义数据模型(Model),为 JSF 页面模板提供数据。JSF 的数据模型中有事件和生命周期相关的方法,而模板和数据模型通信的方式是 Ajax,通过使用 JSF facelets 组件的方式,可以直接发送 Ajax 请求,调用模型中的方法:

    <!-- index.xhtml -->
    <html
      xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://xmlns.jcp.org/jsf/html"
      xmlns:f="http://xmlns.jcp.org/jsf/core"
    >
      <h:commandButton id="submit" value="Submit">
        <f:ajax event="click" />
      </h:commandButton>
      <h:outputText id="result" value="#{userNumberBean.response}" />
    </html>
    
    // UserNumberBean.java
    @Named
    @RequestScoped
    public class UserNumberBean implements Serializable {
    	/* 其它代码省略 */
    
        public String getResponse() {
            if ((userNumber != null)
                    && (userNumber.compareTo(dukesNumberBean.getRandomInt()) == 0)) {
                return "Yay! You got it!";
            }
            if (userNumber == null) {
                return null;
            } else {
                return "Sorry, " + userNumber + " is incorrect.";
            }
        }
    }
    
    

    代码来源:Jarkarta EE 官方示例
    不过这种方式严格限定了编程语言,例如要用 JSF 技术就必须要使用 Java,并且样式污染的问题和客户端数据管理的问题仍然没有解决。
    随着 Node.js 的普及,JavaScript 可以脱离浏览器运行了,并且带来了 npm 包管理器,基于 npm 庞大的工具链,可以 Node.js 的代码打包成浏览器中能够运行的代码。而这期间,产生了像 React、Vue 和 Angular 这样的、适合大型前端项目开发的库和框架。

    在这里插入图片描述

    React 和 Vue 目前的流行程度比较高一些,它们都是纯客户端的、组件化的 JS 库,不依赖任何后端编程语言,让程序员都能够快速上手。不过,随着项目规模的扩大,如何利用好这些库进行组件化开发,成了前端工程师必须要掌握的课题。

    什么是组件

    那么到底什么是组件呢?你想一下日常生活中,电脑上的硬件,例如显卡、内存、CPU,或者汽车的零部件:轮胎、发动机、方向盘等,这些都属于组件,它们组合起来能形成一个完整可用的产品,对于遵循了设计规范的组件,只要型号匹配,无论品牌、样式,都可以互相兼容。
    我们前端工程师其实就当于是一个工厂,我们需要按照一定的规范,产生出合格的网页组件,利用它们组装成完整的页面。组件一般包含 HTML 模板、CSS 样式和 JavaScript 数据逻辑,自成一体,可以直接在其它组件中使用,组件本身的模板、样式和数据不会影响到其它组件。组件还包含一系列可配置的属性,动态的产生内容。

    常见的基础组件有按钮、导航、提示框、表单输入控件、对话框、表格、列表等,我们在它们的基础上又能组合出更复杂的组件,那么对于前端中的组件的定义就非常广泛了,小到一个按钮,大到一个页面都可以形成一个组件,例如两个相似的页面,可以复用一个页面组件,只需要通过修改组件的属性,来形成一个新的页面。

    在这里插入图片描述

    为什么要组件化开发

    你可能也知道,React 和 Vue 其实也可以完全按照传统的网页开发方式进行开发,最多是把网页大体分成几个部分,放到单独的几个文件里就好了,对于简单的网站来说没什么问题,但是如果开发的是大型的应用,例如网页版的 QQ 音乐、微信、邮箱等,它们有大量的、细小的组件,并且重复出现,这时如果针对每个页面编写 HTML 和样式,那么就会造成太多冗余代码了。

    <!-- playlist.html -->
    <div class="card">
      <div class="card__title"></div>
      <div class="card_content"></div>
    </div>
    
    <!-- homepage.html -->
    <div class="card">
      <div class="card__title"></div>
      <div class="card_content"></div>
    </div>
    
    <!-- user.html -->
    <div class="card">
      <div class="card__title"></div>
      <div class="card_content"></div>
    </div>
    

    如果使用组件化的方式,我们可以把重复出现的页面内容定义成组件,这样就不用复制粘贴同样的代码了,减少代码的体积:

    <!-- 伪组件 -->
    <!-- Card.comp -->
    <div class="card">
      <div class="card__title"></div>
      <div class="card_content"></div>
    </div>
    
    <!-- playlist.html -->
    <Card />
    
    <!-- homepage.html -->
    <Card />
    
    <!-- user.html -->
    <Card />
    

    当某个页面内容是复合组件时,那么可以直接把基础组件直接拿过来应用:

    <Card>
      <Title></Title>
      <Button></Button>
    </Card>
    

    利用这种方式,甚至可以达到 1 分钟产生一个新页面的恐怖速度,这对于你来说,节省时间去摸鱼,岂不美哉?

    <Page title="页面1" data="{...}" /> <Page title="页面2" data="{...}" />
    

    对于大型的公司来说,公司的网站、APP、桌面应用、Web 端应用等的设计风格都是一样的,同样的组件会在不同平台中使用,这个时候团队之间可以共享一套组件库,复用到各端平台上,减少重复开发的成本。React 和 Vue 都支持创建跨平台的应用,这时组件化开发就显得更重要了:

    在这里插入图片描述

    使用组件化开发之后,代码也容易维护了,如果页面某个部分出了问题,那么我们可以针对出现问题的组件进行修复,多次用到这个组件的地方也都会一起修复了。
    最后,设计师在设计产品界面的时候,会把重复出现的 UI 也做成『组件』,这个组件的概念几乎和前端中的组件一模一样,这时可以跟设计师交流想法,看看他/她是怎么规划组件的,这样也减少了在开发时,设计规划组件的时间。

    接下来分享一下我在组件化开发中总结的经验:

    一、组件的规划

    在正式进行前端应用开发之前,需要有充分的时间来分析,看看页面上的哪些内容需要做成组件。这一步只需要大致规划一下:

    • 如果公司设计师提供了详细的设计规范,那么直接按照规范中的组件来开发就可以了。

    在这里插入图片描述

    • 如果只有设计稿,那么就看看哪些内容有 2 次或更多次在其它页面中用到了,这些大概率需要设计为组件。
    • 如果连设计稿都没有,那么可以用市面上现成的组件库,例如 ant design。
    • 如果没有设计稿,还要自己从零开发,那么可以根据通用的套路,把基础组件,例如按钮、菜单等,规划出来,并把可能会多次用到的页面内容,也规划出来,例如导航,底部信息、联系方式区域,这样可以只改动需要变化的部分,不影响其它部分。

    这一步不用太过详细,浪费太多时间,后续开发的时候如果遇到不确定是否要写成组件的部分,可以直接把这部分代码写到大的组件里,如果其它组件又用到了,再把它抽离成组件。

    二、组件代码管理

    组件的代码应该遵循就近原则,也就是说:

    • 和组件有关的 HTML、CSS 、JS 代码和图片等静态资源应该放在同一个目录下,方便引用。
    • 组件里的代码应该只包括跟本组件相关的 HTML 模板、CSS 样式和 JS 数据逻辑。
    • 所有的组件应放到一个统一的『组件文件夹中』。

    如果组件之间有可以复用的 HTML 和 CSS,那么这个复用的部分可以直接定义成一个新的组件。

    如果组件之间有可以复用的 JS 数据逻辑,那么可以把公用的数据逻辑抽离出来,放到公共的业务逻辑目录下,使用到该逻辑的组件,统一从这个目录中导入。

    如果项目中使用到了全局状态管理,那么状态管理的数据应放在独立的目录里,这个目录还会存放分割好的状态片段 reducer,之后统一在 store 中合并。

    对于项目中的** API 处理**,可以把它们单独放到一个文件夹里,对于同一个数据类型的操作,可以放到同一个 js 文件里,例如对 user 用户数据的增删改查,这样能尽最大可能进行复用,之后在组件里可以直接引入相关 api 进行调用。如果直接写在组件里,那么使用相同 API 的组件就会造成代码重复。
    例如下面是一个组件化开发的目录结构示例(框架无关):

    project
    |-- components                # 所有组件
          |-- Card                # Card 组件
              |-- index.js        # Card 组件 js 代码
              |-- Card.html       # Card 组件 html 模板
              |-- Card.css        # Card 组件 css 样式
              |-- icon.svg        # Card 组件用到的 icon
    |-- logics                    # 公共业务逻辑
          |-- getUserInfo.js      # 例如获取用户信息
    |-- data                      # 全局状态
          |-- store.js            # 全局状态管理 store
          |-- user.reducer.js     # user reducer
          |-- blogPost.reducer.js # blogPost reducer
    |-- apis                      # 远程请求 API 业务逻辑
          |-- user.js             # user API
          |-- blogPost.js         # blogPost API
    

    三、组件样式管理

    在编写组件样式的时候,应只设置组件 CSS 盒子内部的样式,影响外部布局的样式要尽可能的避免,而应该由使用该组件的父组件去设置。例如,如果有一个组件设置了外边距,但是这个组件经常会用于 grid 或 flex 布局中,那么这个额外的边距会对布局造成影响,只能通过重置外边距的方式取消边距,这就不如组件不设置外边距,由父组件的布局决定它的位置,或者外边距。

    在这里插入图片描述

    类似的还有定位相关的样式,绝对定位、固定定位等对文档流有影响,应交由父组件决定,除非这个组件只有绝对定位这一种情况,例如对话框。
    组件中的 CSS 要局部化,以避免影响全局样式。传统的 CSS 样式是全局的,如果有两个不同的组件,使用了相同的 class 名字,那么后定义的样式会覆盖之前定义的。一般前端库中都有定义局部样式的功能,例如通过 CSS Modules。

    <!-- Vue -->
    <style scoped></style>
    
    <!-- React,通过文件名 -->
    Button.module.css
    

    修改子组件的样式时,优先使用选择器特异性(CSS Specificity)策略选定特定元素,而行内样式(inline-style)在设置简单样式的时候使用,尽一切可能避免 !important,因为如果再有上一层组件(爷爷组件)需要修改这个组件的样式时,就会很困难了。

    四、组件属性管理

    组件属性的命名要与它实际展示的内容相符。例如一个博客文章组件,title 属性表示标题,content 代表内容,showFullArticle 表示是否显示全文。

    <!-- 不推荐,full 表示什么? -->
    <BlogPost title="" content="" full="" />
    <!-- 推荐 -->
    <BlogPost title="" content="" showFullArticle="" />
    

    组件的属性应有必要的类型检查,避免在使用属性时出现异常,例如在需要数组的地方传递了布尔类型,那么如果代码有遍历数组的逻辑,就会出错。

    props: {
    	title: String,
      content: String,
      showFullArticle: Boolean
    }
    

    代表事件的属性,应该和现有的 HTML 事件名称规范保持一致,以 on 开头,后面用英文表示会触发的事件,例如 onEdit 表示会触发编辑事件。

    <BlogPost onEdit="" />
    

    组件的属性不能直接和状态进行捆绑,而是应该只作为状态的初始值。如果把属性值作为状态值,那么就破坏了单一数据流向机制,此时该组件可以通过自身修改状态,也能通过父组件的属性变化修改状态,很容易造成数据的不一致。推荐的做法是,设置一个初始值属性,可以通过父组件传递进来,当作状态的初始值,然后丢弃,后续只通过该组件修改状态值。

    const { initialTitle } = props;
    const title = state(initialTitle);
    
    // 修改
    updateTitle() {
     title = "...";
    }
    

    五、组件状态管理

    组件的状态分为全局状态和局部状态两种。
    局部状态是定义在组件本身的状态,应由组件本身内部管理,当子组件需要该组件的状态值时,通过属性传递给子组件,此时状态变化时,子组件也会自动刷新。

    // 父组件
    const someState = state("");
    
    <ChildComponent prop1="someState"></ChildComponent>;
    

    当子组件需要给父组件传递状态的值时,要通过事件的方式传递给父组件,尽最大可能避免使用 ref

    // 父组件
    function getStateVal(val) {
      console.log(val);
    }
    <ChildComponent onChange="getStateVal" />
    
    // 子组件
    <input onChange="e => getStateVal(e.target.value)" />
    

    状态的变化还应只通过事件或生命周期来进行,不能在其它同步执行的代码中进行,这样不会引起组件的刷新。

    const someState = state();
    
    // 错误
    const state = "newState";
    
    // 正确
    handleButtonClick() {
      someState = "newState";
    }
    

    全局状态是多个组件共享的,如果有多个组件共享某个状态,那么应该把状态定义在这些组件统一的、最接近的父组件中,或者使用全局状态管理库。

    在这里插入图片描述

    全局状态的修改,应该由类似于 actions 的行为触发,然后使用 reducer 修改状态,这样能追溯状态的变化路径,方便调试和打印日志。

    // 组件
    function updateState() {
      emitAction("changeState");
    }
    
    // reducer
    function someState(state, action) {
    	switch(action) {
        "changeState": someState => {
          	log(someState);
        		return newState
        }
      }
    }
    

    全局状态推荐按领域(Domain)来拆分 reducer,而不是按页面。例如按 user 用户、文章 posts、评论 comments 来拆分,而不是以 HomePage 主页、BlogPostListPage 博客列表页,BlogPostPage 博客详情页这样来拆分。这样做的好处是,能够最大限度的复用 reducer 中的逻辑。

    // userReducer
    
    {
    	"updateUser": ...
      "deleteUser": ...
      /* ... */
    }
    

    六、组件的组合

    前端开发就是组合不同的组件。
    在组合组件的时候,尽量使用『插槽』的形式(例如 React 的 children/render props,Vue 的 slot),这样可以减少组件的嵌套,避免多层传递属性或事件监听。
    使用 slot:

    // Layout 组件
    <div>
      <!-- nav slot -->
      <!-- content slot -->
      <!-- footer slot -->
    </div>
    
    // 首页 function handleNavChange() {}
    
    <Layout>
      <nav onChange="handleNavChange"></nav>
      <main></main>
      <footer></footer>
    </Layout>
    

    不使用 slot:

    // Layout props: { onNavChange: function }
    <Layout>
      <!-- 还需再传递一层 -->
      <nav onChange="onNavChange"></nav>
      <main></main>
      <footer></footer>
    </Layout>
    
    // 首页 function handleNavChange() {}
    <Layout onNavChange="handleNavChange" />
    

    如果有循环展示列表的地方,需要对循环中最外层的组件设置 key,这样在列表发生变化时,能帮助前端库判断是否可以通过排序的方式,重复利用现有的组件,来更新视图,而不是销毁重建。

    let todos = [{id: 1, content: "todo1"}, {id:2, content: "todo2"}, {id:3,
    content: "todo3"}];
    
    <List>
      for todo in todos:
      <item content="todo.content" key="todo.id" />
    </List>
    
    // todos 顺序变化,列表也只是根据 id 调整顺序,不会销毁重建 todos = [{id: 3,
    content: "todo3"}, {id:2, content: "todo2"}, {id:1, content: "todo1"}];
    

    如果有按条件展示组件的地方,且切换频率高,或有动画需求,要使用设置 css display 的方式,例如 vue 中的 v-show,如果切换频率低,可以使用加载、销毁的方式,例如 vue 中的 v-if,React 中使用 && 逻辑判断。

    七、组件的复用

    在复用组件的时候,可以通过改变组件的属性来修改一些简单的组件内容。

    <Card title="" content="<ContentComp />" />
    

    如果组件结构类似,但是有些内部的组件不一样,可以考虑通过『插槽』来复用。

    <!-- Comp -->
    <div>
      <h1></h1>
      <!-- slot -->
    </div>
    
    <!-- 其它组件 -->
    <Comp>
      <p>替换 slot</p>
    </Comp>
    

    如果有业务逻辑需要复用,尤其是涉及到状态变化的,那么可以把它抽离为公共的业务逻辑,利用 Hooks(React)或 Composables (Vue)来复用。

    // 公共逻辑 (/logic/useSomeLogic.js)
    function useSomeLogic() {
      const someState = state();
      const updateState = (v) => (someState = v);
      return {
        someState,
        updateState,
      };
    }
    
    // 组件1复用
    import userSomeLoginc from "/logic/useSomeLogic.js";
    const { someState, updateState } = useSomeLogic();
    
    // 组件2复用
    import userSomeLoginc from "/logic/useSomeLogic.js";
    const { someState, updateState } = useSomeLogic();
    

    如果有视图样式需要复用,那么可以直接把这部分再抽离成一个新的组件。

    小结

    这篇文章从组件的代码、样式、属性、状态、组合和复用的这几个场景上,总结了一些我在前端开发中的一些经验和个人看法,可能并不适用所有项目,如果你有更好的最佳实践和经验,欢迎分享!如果觉得本文有帮助,请分享给其它人,感谢!

    原文地址:https://zxuqian.cn/7-ways-to-organize-frontend-components,欢迎访问查看更多前端开发教程!
    Bilibili:峰华前端工程师
    公众号:峰华前端工程师

    更多相关内容
  • 该文档来自MDCC 2016中国移动开发者大会。冯森林发表了题为“回归初心,从容器化到组件化”的主题演讲,欢迎下载!
  • Android组件化原理

    千次阅读 2022-01-07 14:33:49
    Android组件化原理什么是组件化?为什么使用组件化?一步步搭建组件化组件化开发要注意的几点问题1.新建模块2.统一Gradle版本号3.创建基础库4.组件模式和集成模式转换5.AndroidManifest的切换 什么是组件化? 一个...

    什么是组件化?

    一个大型APP版本一定会不断的迭代,APP里的功能也会随之增加,项目的业务也会变的越来越复杂,这样导致项目代码也变的越来越多,开发效率也会随之下降。并且单一工程下代码耦合严重,每修改一处代码后都要重新编译,非常耗时,单独修改的一个模块无法单独测试。

    组件化架构的目的是让各个业务变得相对独立,各个组件在组件模式下可以独立开发调试,集成模式下又可以集成到“app壳工程”中,从而得到一个具有完整功能的APP。

    组件化每一个组件都可以是一个APP可以单独修改调试,而不影响总项目。
    组件化基础架构图

    为什么使用组件化?

    编译速度: 可以但需测试单一模块,极大提高了开发速度
    超级解耦: 极度降低了模块间的耦合,便于后期的维护和更新
    功能重用: 某一块的功能在另外的组件化项目中使用只需要单独依赖这一模块即可
    便于团队开发: 组件化架构是团队开发必然会选择的一种开发方式,它能有效的使团队更好的协作

    一步步搭建组件化

    这里以演示为例,只设置登录这一个功能组件

    组件化开发要注意的几点问题

    • 要注意包名和资源文件命名冲突问题
    • Gradle中的版本号的统一管理
    • 组件在AppIicationLibrary之间如何做到随意切换
    • AndroidManifest. xml文件的区分
    • Library不能在Gradle文件中有applicationId

    这里以演示为例,只设置登录个人中心这两个功能组件

    1.新建模块

    在这里插入图片描述
    并且在module里新建一个activity
    在这里插入图片描述
    到这里我们看到login和我们的app都在有一个绿点证明创建成功
    在这里插入图片描述
    个人中心member模块创建同理,并且每个模块目前都可以独立运行。
    在这里插入图片描述

    2.统一Gradle版本号

    每一个模块都是一个application,所以每个模块都会有一个build.gradle,各个模块里面的配置不同,我们需要重新统一Gradle
    在主模块创建config.gradle
    在这里插入图片描述
    config.gradle里去添加一些版本号

    ext{
    
        android = [
                compileSdkVersion :30,
                buildToolsVersion: "30.0.2",
                applicationId :"activitytest.com.example.moduletest",
                minSdkVersion: 29,
                targetSdkVersion :30,
                versionCode :1,
                versionName :"1.0",
        ]
    
        androidxDeps = [
                "appcompat": 'androidx.appcompat:appcompat:1.1.0',
                "material": 'com.google.android.material:material:1.1.0',
                "constaraintlayout": 'androidx.constraintlayout:constraintlayout:1.1.3',
        ]
    
        commonDeps = [
                "arouter_api"          : 'com.alibaba:arouter-api:1.5.1',
                "glide"                : 'com.github.bumptech.glide:glide:4.11.0'
    
        ]
    
        annotationDeps = [
                "arouter_compiler" : 'com.alibaba:arouter-compiler:1.5.1'
        ]
    
        retrofitDeps = [
                "retrofit"  : 'com.squareup.retrofit2:retrofit:2.9.0',
                "converter" : 'com.squareup.retrofit2:converter-gson:2.9.0',
                "rxjava"    : 'io.reactivex.rxjava2:rxjava:2.2.20',
                "rxandroid" : 'io.reactivex.rxjava2:rxandroid:2.1.1',
                "adapter"   : 'com.squareup.retrofit2:adapter-rxjava2:2.9.0'
        ]
    
        androidxLibs = androidxDeps.values()
        commonLibs = commonDeps.values()
        annotationLibs = annotationDeps.values()
        retrofitLibs = retrofitDeps.values()
    }
    

    在主模块的build.gradle里添加

    apply from: "config.gradle"
    

    在这里插入图片描述
    在各模块中去引用这些版本号
    引用格式如下,两种写法均可

    compileSdkVersion rootProject.ext.android["compileSdkVersion"]
    buildToolsVersion rootProject.ext.android.buildToolsVersion
    

    引用前
    在这里插入图片描述
    引用后
    在这里插入图片描述
    并且使用同样的方法,我们还可以统一我们的依赖库在config.gradle里去添加我们要依赖的库,并在各个模块中去添加依赖

     implementation  rootProject.ext.dependencies.publicImplementation
    

    也可以采用第二种写法

    dependencies = [
    
                "appcompat"             : 'androidx.appcompat:appcompat:1.2.0',
    
                "material"               : 'com.google.android.material:material:1.2.1',
                "constraintLayout"       : 'androidx.constraintlayout:constraintlayout:2.0.4',//约束性布局
    
                //test
                "junit"                  : "junit:junit:4.13.1",
                "testExtJunit"           : 'androidx.test.ext:junit:1.1.2',//测试依赖,新建项目时会默认添加,一般不建议添加
                "espressoCore"           : 'androidx.test.espresso:espresso-core:3.3.0',//测试依赖,新建项目时会默认添加,一般不建议添加
    
        ]
    

    添加依赖

    dependencies {
    
        implementation rootProject.ext.dependencies.appcompat
        implementation  rootProject.ext.dependencies["constraintLayout"]
        testImplementation rootProject.ext.dependencies["junit"]
        androidTestImplementation rootProject.ext.dependencies["testExtJunit"]
        androidTestImplementation rootProject.ext.dependencies["espressoCore"]
    
    }
    

    3.创建基础库

    和新建module一样,这里需要新建一个library我们把它命名为Baselibs
    在这里插入图片描述

    同样需要统一版本号,由于这是一个library模块,所以它不需要applicationId
    在这里插入图片描述
    我们一样可以把它写进config.gradle

    other:[path:':Baselibs']
    

    在每个模块去调用

    implementation  project(rootProject.ext.dependencies.other)
    

    同理,当本地库为单独所用,我们可以直接调用,而不需要将其写入config.gradle,两种方法选择合适使用即可。

    implementation project(':Baselibs')
    

    但有时因为gradle版本问题,我们可能无法依赖到这些公共库,因为我们在config.gradle里是以数组形式定义的,这时我们可以同for-each循环的方法将其依次导入
    config.gradle

    dependencies = [
              ......
            other:[':Baselibs']
        ]
    

    其他模块的build.gradle

    dependencies {
    ......
        rootProject.ext.dependencies.other.each{
            implementation project(it)
        }
    

    4.组件模式和集成模式转换

    在主模块gradle.properties里添加布尔类型选项。
    在这里插入图片描述

    在各个模块的build.gradle里添加更改语句

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

    每个模块的applicationId也需要处理

    if(is_Module.toBoolean()){
                applicationId "activitytest.com.example.login"
            }
    

    在这里插入图片描述

    当我们将is_module改为false时,再次运行编译器我们的模块都不能单独运行了
    在这里插入图片描述
    在app模块中添加判断依赖就可以在集成模式下将各模块添加到app主模块中

    // 每加入一个新的模块,就需要在下面对应的添加一行
        if (is_Module.toBoolean())]) {
            implementation project(path:':login')
            implementation project(path:':member')
        }
    

    5.AndroidManifest的切换

    为了单独开发加载不同的AndroidManifest这里需要重新区分下。
    在组件模块里的main文件里新建manifest文件夹
    在这里插入图片描述

    并且重写一个AndroidManifest.xml文件,集成模式下,业务组件的表单是绝对不能拥有自己的 Application 和 launch 的 Activity的,也不能声明APP名称、图标等属性,总之app壳工程有的属性,业务组件都不能有,在这个表单中只声明了应用的主题,而且这个主题还是跟app壳工程中的主题是一致的

    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.example.login">
    
        <application
            android:theme="@style/Theme.MoudleTest">
            <activity android:name=".LoginActivity">
           
            </activity>
        </application>
    
    </manifest>
    

    并且我们还要使其在不同的模式下加载不同的AndroidManifest只需在各模块的build.gradle里添加更改语句

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

    6.*业务Application切换

    每个模块在运行时都会有自己的application,而在组件化开发过程中,我们的主模块只能有一个application,但在单独运行时又需要自己的application这里就需要配置一下。
    在业务模块添加新文件夹命名module
    在这里插入图片描述
    在里面建一个application文件
    在这里插入图片描述
    并且我们在build.gradle文件里配置module文件夹使其在单独运行时能够运行单独的application
    在配置manifest的语句中添加java.srcDir 'src/main/module'

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

    同时我们在basic基础层内新建application,用于加载一些数据的初始化

    public class BaseApplication extends Application {
        @Override
        public void onCreate() {
            super.onCreate();
            Log.e("fff","baseapplication");
        }
    }
    

    在业务模块内module里重写该模块的application

    public class LoginApplication extends BaseApplication {
        @Override
        public void onCreate() {
            super.onCreate();
        }
    }
    

    至此,组件化框架搭建结束

    组件之间的跳转

    这里采用阿里巴巴的开源库ARouter来实现跳转功能,我会在以后的文章单独拿出一篇来一步步去解读Arouter源码,让我们自己去搭建一个自己的路由

    一个用于帮助 Android App 进行组件化改造的框架 —— 支持模块间的路由、通信、解耦

    由 github 上 ARouter 的介绍可以知道,它可以实现组件间的路由功能。路由是指从一个接口上收到数据包,根据数据路由包的目的地址进行定向并转发到另一个接口的过程。这里可以体现出路由跳转的特点,非常适合组件化解耦。

    要使用 ARouter 进行界面跳转,需要我们的组件对 Arouter 添加依赖,因为所有的组件都依赖了 Baselibs模块,所以我们在 Baselibs 模块中添加 ARouter 的依赖即可。其它组件共同依赖的库也最好都放到 Baselibs中统一依赖。

    这里需要注意的是,arouter-compiler 的依赖需要所有使用到 ARouter 的模块和组件中都单独添加,不然无法在 apt 中生成索引文件,也就无法跳转成功。并且在每一个使用到 ARouter 的模块和组件的 build.gradle 文件中,其 android{} 中的 javaCompileOptions 中也需要添加特定配置。

    1.添加依赖

    Baselibs里的build.gradle添加依赖

    dependencies {
        api 'com.alibaba:arouter-api:1.3.1'
        // arouter-compiler 的注解依赖需要所有使用 ARouter 的 module 都添加依赖
        annotationProcessor 'com.alibaba:arouter-compiler:1.1.4'
    }
    
    // 所有使用到 ARouter 的组件和模块的 build.gradle
    android {
        defaultConfig {
            ...
            javaCompileOptions {
                annotationProcessorOptions {
                    arguments = [ moduleName : project.getName() ]
                }
            }
        }
    }
    dependencies {
        ...
        implementation project (':base')
        annotationProcessor 'com.alibaba:arouter-compiler:1.1.4'
    }
    

    主模块需要对跳转模块进行依赖

    // 主项目的 build.gradle 需要添加对 login 组件和 share 组件的依赖
    dependencies {
        // ... 其他
        implementation project(':login')
        implementation project(':share')
    }
    

    2.初始化ARouter

    添加了对 ARouter 的依赖后,还需要在项目的 Application 中将 ARouter 初始化,我们这里将 ARouter 的初始化工作放到主模块Application 的 onCreate()方法中,在应用启动的同时将 ARouter 初始化。

    public class MainApplication extends Application {
        @Override
        public void onCreate() {
            super.onCreate();
    
            // 初始化 ARouter
            if (isDebug()) {           
                // 这两行必须写在init之前,否则这些配置在init过程中将无效
                
                // 打印日志
                ARouter.openLog();     
                // 开启调试模式(如果在InstantRun模式下运行,必须开启调试模式!线上版本需要关闭,否则有安全风险)
                ARouter.openDebug();   
            }
            
            // 初始化 ARouter
            ARouter.init(this);
    
        }
    
        private boolean isDebug() {
            return BuildConfig.DEBUG;
        }
    
    }
    

    3.添加跳转

    这里我们在首页添加登录分享两个跳转页面。

    login.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            ARouter.getInstance().build("/login/login").navigation();
        }
    });
    
    share.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            ARouter.getInstance().build("/share/share").navigation();
        }
    });
    

    然后,需要在登录和分享组件中分别添加 LoginActivityShareActivity ,然后分别为两个 Activity 添加注解 Route,其中path 是跳转的路径,这里的路径需要注意的是至少需要有两级,/xx/xx

    @Route(path = "/login/login")
    public class Login extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_login);
    
        }
    }
    
    @Route(path = "/share/share")
    public class Share extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_share);
      }
    }
    

    这样就可以实现跳转了。

    组件之间的数据传递

    由于主项目与组件,组件与组件之间都是不可以直接使用类的相互引用来进行数据传递的,那么在开发过程中如果有组件间的数据传递时应该如何解决呢,这里我们可以采用 [接口 + 实现] 的方式来解决。

    Baselibs基础库里定义组件可以对外提供访问自身数据的抽象方法的 Service。并且提供了一个 ServiceFactory,每个组件中都要提供一个类实现自己对应的 Service 中的抽象方法。在组件加载后,需要创建一个实现类的对象,然后将实现了 Service 的类的对象添加到ServiceFactory 中。这样在不同组件交互时就可以通过 ServiceFactory 获取想要调用的组件的接口实现,然后调用其中的特定方法就可以实现组件间的数据传递与方法调用。

    当然,ServiceFactory 中也会提供所有的 Service 的空实现,在组件单独调试或部分集成调试时避免出现由于实现类对象为空引起的空指针异常。

    下面我们就按照这个方法来解决组件间数据传递与方法的相互调用这个问题,这里我们通过分享组件 中调用 登录组件 中的方法来获取登录状态是否登录这个场景来演示。

    1.定义接口

    其中 service文件夹中定义接口,LoginService 接口中定义了 Login 组件向外提供的数据传递的接口方法,EmptyService 中是 service 中定义的接口的空实现,ServiceFactory 接收组件中实现的接口对象的注册以及向外提供特定组件的接口实现。
    在这里插入图片描述
    LoginService

    public interface LoginService {
    
        /**
         * 是否已经登录
         * @return
         */
        boolean isLogin();
    
        /**
         * 获取登录用户的 Password
         * @return
         */
        String getPassword();
    }
    

    EmptyService

    public class EmptyService implements LoginService {
        @Override
        public boolean isLogin() {
            return false;
        }
    
        @Override
        public String getPassword() {
            return null;
        }
    }
    

    ServiceFactory

    public class ServiceFactory {
        private LoginService loginService;
        private ServiceFactory(){
     /**
         * 禁止外部创建 ServiceFactory 对象
         */
        private ServiceFactory() {
        }
    
        /**
         * 通过静态内部类方式实现 ServiceFactory 的单例
         */
        public static ServiceFactory getInstance() {
            return Inner.serviceFactory;
        }
    
        private static class Inner {
            private static ServiceFactory serviceFactory = new ServiceFactory();
        }
     /**
         * 接收 Login 组件实现的 Service 实例
         */
        public void setLoginService(LoginService loginService){
            this.loginService = loginService;
        }
           /**
         * 返回 Login 组件的 Service 实例
         */
        public LoginService getLoginService(){
            if(loginService == null){
                return new EmptyService();
            }else{
                return loginService;
            }
        }
    }
    

    2.实现接口

    login模块

    public class AccountService implements LoginService {
    
        private boolean login;
        private String password;
    
        public AccountService(boolean login, String password) {
            this.login = login;
            this.password = password;
        }
    
        @Override
        public boolean isLogin() {
            return login;
        }
    
        @Override
        public String getPassword() {
            return password;
        }
    }
    

    这里新建一个Util类用来存储登录数据

    public class LoginUtil {
        static boolean isLogin = false;
        static String password = null;
    }
    

    实现一下登录操作

    login = (Button)findViewById(R.id.login);
            login.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    LoginUtil.isLogin = true;
                    LoginUtil.password = "admin";
                    ServiceFactory.getInstance().setLoginService(new AccountService(LoginUtil.isLogin,LoginUtil.password));
                }
            });
    

    login模块的application里定义ServiceFactory类

    public class LoginApplication extends Application {
        @Override
        public void onCreate() {
            super.onCreate();
            ServiceFactory.getInstance().setLoginService(new AccountService(LoginUtil.isLogin,LoginUtil.password));
        }
    }
    

    在分享模块获取登录信息

    share = (Button)findViewById(R.id.share);
            share.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if(ServiceFactory.getInstance().getLoginService().isLogin()){
                        Toast.makeText(ShareActivity.this,"分享成功!",Toast.LENGTH_SHORT).show();
                    }else{
                        Toast.makeText(ShareActivity.this,"分享失败,请先登录!",Toast.LENGTH_SHORT).show();
                    }
                }
            });
    

    一个项目时只能有一个 Application 的,Login 作为组件时,主模块的 Application 类会初始化,而 Login 组件中的 Applicaiton 不会初始化。确实是存在这个问题的,我们这里先将 Service 的注册放到其活动里,稍后我们会解决 Login 作为组件时 Appliaciton 不会初始化的问题。

    组件Application的动态切换

    在主模块中有 Application 等情况下,组件在集中调试时其 Applicaiton 不会初始化的问题。而我们组件的 Service 在 ServiceFactory 的注册又必须放到组件初始化的地方。

    为了解决这个问题可以将组件的 Service 类强引用到主 Module 的 Application 中进行初始化,这就必须要求主模块可以直接访问组件中的类。而我们又不想在开发过程中主模块能访问组件中的类,这里可以通过反射来实现组件 Application 的初始化。

    1.定义抽象类 BaseApplication 继承 Application

    Baselibs基础库模块

    public abstract class BaseApplication extends Application {
        /**
         * Application 初始化
         */
        public abstract void initModuleApp(Application application);
    
        /**
         * 所有 Application 初始化后的自定义操作
         */
        public abstract void initModuleData(Application application);              //其他需要调用的方法
    }
    

    2.所有的组件的 Application 都继承 BaseApplication

    这里我们以Login模块为例

    public class LoginApplication extends BaseApplication{
    
        @Override
        public void onCreate() {
            super.onCreate();
            initModuleApp(this);
            initModuleData(this);
        }
    
        @Override
        public void initModuleApp(Application application) {
            ServiceFactory.getInstance().setLoginService(new AccountService(LoginUtil.isLogin,LoginUtil.password));
        }
    
        @Override
        public void initModuleData(Application application) {
    
        }
    }
    

    3.定义 AppConfig 类

    Baselibs模块定义一个静态的 String 数组,我们将需要初始化的组件的 Application 的完整类名放入到这个数组中。

    public class AppConfig {
        private static final String LoginApp = "com.example.login.LoginApplication";
    
        public static String[] moduleApps = {
                LoginApp
        };
    }
    

    4.主模块application实现两个初始化方法

    // 主 Module 的 Applicaiton
    public class MainApplication extends BaseApp {
        @Override
        public void onCreate() {
            super.onCreate();
            
            // 初始化组件 Application
            initModuleApp(this);
            
            // 其他操作
            
            // 所有 Application 初始化后的操作
            initModuleData(this);
            
        }
    
        @Override
        public void initModuleApp(Application application) {
            for (String moduleApp : AppConfig.moduleApps) {
                try {
                    Class clazz = Class.forName(moduleApp);
                    BaseApp baseApp = (BaseApp) clazz.newInstance();
                    baseApp.initModuleApp(this);
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InstantiationException e) {
                    e.printStackTrace();
                }
            }
        }
    
        @Override
        public void initModuleData(Application application) {
            for (String moduleApp : AppConfig.moduleApps) {
                try {
                    Class clazz = Class.forName(moduleApp);
                    BaseApp baseApp = (BaseApp) clazz.newInstance();
                    baseApp.initModuleData(this);
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InstantiationException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    

    到这里我们就通过反射,完成了组件 Application 的初始化操作,也实现了组件与化中的解耦需求。

    主模块使用其他组件的 Fragment

    我们在开发过程中经常使用 Fragment。一般情况下,我们都是直接通过访问具体 Fragment 类的方式实现 Fragment 的实例化,但是现在为了实现模块与组件间的解耦,在移除组件时不会由于引用的 Fragment 不存在而编译失败,我们就不能模块中直接访问组件的 Fragment 类。
    这里介绍两种方法

    1.ARouter

    这里可以采用ARouter直接调用

    fragment = (Fragment) ARouter.getInstance().build("/login/fragment").navigation();
    

    2.反射

    我们还是以Login模块为例,假如在该模块创建一个用户界面,命名为UserFragment
    首先,在 Login组件中创建 UserFragment,然后在 LoginService 接口中添加newUserFragment方法返回一个Fragment,在Login组件中的 AccountServiceBaselibsLoginService 的空实现类中实现这个方法,然后在主模块中通过 ServiceFactory 获取 LoginService 的实现类对象,调用其 newUserFragment 即可获取到 UserFragment 的实例。

    // Baselibs 模块的 LoginService 
    public interface LoginService {
    //其他代码...
        Fragment newUserFragment(Activity activity, int containerId, FragmentManager manager, Bundle bundle, String tag);
    }
    
    // Login 组件中的 AccountService
    public class AccountService implements LoginService {
        // 其他代码 ...
    
        @Override
        public Fragment newUserFragment(Activity activity, int containerId, FragmentManager manager, Bundle bundle, String tag) {
            FragmentTransaction transaction = manager.beginTransaction();
            // 创建 UserFragment 实例,并添加到 Activity 中
            Fragment userFragment = new UserFragment();
            transaction.add(containerId, userFragment, tag);
            transaction.commit();
            return userFragment;
        }
    }
    
    // 主模块的 FragmentActivity
    public class FragmentActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_fragment);
            
            // 通过组件提供的 Service 实现 Fragment 的实例化
            ServiceFactory.getInstance().getAccountService().newUserFragment(this, R.id.layout_fragment, getSupportFragmentManager(), null, "");
        }
    }
    
    展开全文
  • 前端组件化

    千次阅读 2020-12-21 10:40:47
    组件化 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fEVcmrj4-1608518366216)...

    组件化

    在这里插入图片描述

    什么是组件化?

    前端组件化开发,就是将页面的某一部分独立出来,将这一部分的数据层(M)、视图层(V)和控制层(C)用黑盒的形式全部封装到一个组件内,暴露出一些开箱即用的函数和属性供外部调用。无论这个组件放到哪里去使用,它都具有一样的功能和样式,从而实现复用(只写一处,处处复用),这种整体化的思想就是组件化。


    每个组件都是独立的个体,都只负责一块功能。组件之间互相独立,通过特定的方式进行沟通。外部完全不用考虑组件的内部实现逻辑。一个好的前端组件,必须要把维护性,复用性,扩展性,性能做到极致。

    组件化与模块化的区别

    从历史发展角度来讲
    随着前端开发越来越复杂、对效率要求越来越高,由项目级模块化开发,进一步提升到通用功能组件化开发,模块化是组件化的前提,组件化是模块化的演进


    从整体概念来讲

    • 模块化是一种分治的思想,述求是解耦,一般指的是 JavaScript 模块,比如用来格式化时间的模块
    • 组件化是模块化思想的实现手段,述求是复用,包含了 template,style,script,script又可以由各种模块组成


    从复用的角度来讲

    • 模块一般是项目范围内按照项目业务内容来划分的,比如一个项目划分为子系统、模块、子模块,代码分开就是模块,位于架构业务框架层,横向分块
    • 组件是按照一些小功能的通用性和可复用性抽象出来的,可以跨项目的,是可复用的模块,通常位于架构底层,被其他层所依赖
      在这里插入图片描述


    **从划分的角度来讲**
    • 模块是从代码逻辑的角度进行划分,方便代码分层开发,保证每个功能模块的职能单一
    • 组件时从 UI 界面的角度进行划分,前端的组件化,方便 UI 组件的重用

    为什么要前端组件化

    随着前端项目复杂度的急剧增加,我们很容易遇到以下这些场景:

    • 页面逻辑越来越多,代码越写越庞大,容易牵一发而动全身
    • 同样的逻辑在多个地方重复编写,改一个问题要在多个地方进行同样的修改


    以上场景带来的问题就是:

    • 项目复杂度增加
    • 重复性劳动多,效率低
    • 代码质量差,不可控


    因此前端组件化可以给我们带来:

    • 增加代码的复用性,灵活性
    • 提高开发效率,降低开发成本
    • 便于各个开发者之间分工协作、同步开发
    • 降低系统各个功能的耦合性,提高了功能内部的聚合性
    • 降低代码的维护成本

    应用组件化需要考虑的问题

    1. **如何分成各个模块?**我们可以根据业务来进行划分,对于比较大的功能模块可以作为应用的一个模块来使用,但是也应该注意,划分出来的模块不要过多,否则可能会降低编译的速度并且增加维护的难度。
    2. 如何解决组件之间的隔离?
    3. 各个模块之间如何进行数据共享和数据通信?
    4. **如何防止资源名冲突问题?**遵守命名规约就能规避资源名冲突问题。

    组件的划分

    划分方法

    尽可能抽象和解耦。不断抽象出一个跟业务没有关系的模块,它是可以继承的,这就是组件化设计的思维转换。
    划分粒度:需要根据实际情况权衡,太小会提升维护成本,太大又不够灵活。
    目前还没有一套原则和方法论来指导组件的划分,我们只能根据前人的经验再结合实际情况来进行组件的划分。
    关于组件划分的一些建议:

    • 组件之间的依赖应该尽可能的少。
    • 单个组件代码量最好不要超过1000行。
    • 组件划分的依据通常是业务逻辑、功能,要考虑各组件之间的关系是否明确,以及组件的可复用度。
    • 每一个组件都应该有其独特的划分目的,有的是为了复用实现,有的是为了封装复杂度、清晰业务实现。


    我经常的做法是:如果看到有多个页面都出现了这个重复元素,则抽取成一个组件。还有在开发之中发现结构相似的也可以考虑抽取成一个组件。没有必要在一开始就把所有都抽取成一个个组件。

    组件分类

    基础UI组件

    这是最小化的组件,它们不依赖于其他组件。作为页面中最少的元素而存在,比如按钮、下拉菜单、对话框等。其中大部分是对原生 Web 元素的封装,例如:<input><select><button> ,它们以简单的形式存在。
    在创建基础组件的过程中,要遵循一个基本原则:基础组件是独立存在的。它们可以共享配置,但是不能相互依赖,依赖意味着它不是基础组件。
    像 antd、iview、element-ui 里提供的基本都是基础 UI 组件。

    复合组件

    复合组件是在多个基础的 UI 组件上的进一步结合。大部分复合组件,包含了一些复杂的组件,往往需要花很长的时间,才能变成一个可稳定使用的版本。复合组件包含以下几个部分:

    • 表格。表格往往带有复杂的交互,比如固定行、固定列、可编辑、虚拟滚动等。由于其数据量大,往往又对性能有很高的要求。
    • 图表。图表的门槛相对比较高,并且种类繁多,对于显示、交互的要求也高。
    • 富文本编辑器。几乎是最复杂的组件,其功能需求往往与 Word 进行对比,其代码量可能接近 Word 的数量级。

    业务组件

    业务组件是我们在实现业务功能的过程中抽象出来的组件,其作用是在应用中复用业务逻辑。当它们涉及一些更复杂的业务情形时,就要考虑是否将这些组件放入组件库中。
    通常是根据最小业务状态抽象而出,有些业务组件也具有一定的复用性,但大多数是一次性组件。
    特点:UI可配置,业务逻辑完整。有完整的后台流程,数据结构。

    组件的隔离

    由于前端基础技术栈自身的原因,html 、css 、js 运行在一个页面上时是没有隔离的,也就是说 js 可以根据选择器获取到任意的 dom 节点,一条 css 规则也会应用在文档中所有满足规则的节点, js 代码中可以随意的创建和使用全局变量。


    因此,要想实现组件化,我们应该尽可能的去实现每个组件的隔离。
    组件隔离其实就是模块化,这里我们需要实现 CSS 模块化和 JS 模块化。


    在 vue 中,我们可以为组件中 style 标签增加一个 scoped 的标识, vue-loader 在编译的过程中会为组件每一个元素节点增加 scopeId 作为属性,同时为所有的样式类加上属性选择器 scopeId ,从而达到隔离的效果。如下图:

    在这里插入图片描述


    组件间通信

    高内聚低耦合必然会带来数据流动上的壁垒,所以隔离后的组件就要解决组件之间的通信处理。组件通信分为父子组件通信和非父子组件通信,这就涉及到接口设计、事件处理和状态管理三块内容。


    在 vue 中,可以使用 props ,事件监听 ,EventBus 的方式来实现组件间的通信。

    组件的按需加载

    iview 、antd 使用的都是 babel-plugin-import 插件,可以实现组件的按需加载。
    本质上就是将对整个库的引用,变为具体模块的引用。这样 webpack 收集依赖模块时就不是整个组件库,而是具体的某个模块了。


    如果所用 ui 组件库不符合 babel-plugin-import 的转换规则,可以通过 babel-plugin-import 提供的 customName 字段来自定义转换后的路径。通过 style 字段,来进一步自定义转换后的 style 路径。

    怎么设计一个组件

    组件的设计原则

    1. 标准性:任何一个组件都应该遵守一套标准,可以使得不同区域的开发人员据此标准开发出一套标准统一的组件。
    2. 单一职责原则:一个组件只专注做一件事,且把这件事做好。一个功能如果可以拆分成多个功能点,那就可以将每个功能点封装成一个组件,当然也不是组件的颗粒度越小越好,只要将一个组件内的功能逻辑控制在一个可控的范围内即可。
    3. 开闭原则:对扩展开放,对修改关闭。属性配置等 API 对外开放,组件内部状态对外封闭。
    4. 追求短小精悍
    5. 避免太多参数**,扁平化参数**:除了数据,避免复杂的对象,尽量只接收原始类型的值。
    6. 合理的依赖关系:父组件不依赖子组件,删除某个子组件不会造成功能异常
    7. 适用SPOT(Single Point of Truth)法则尽量不要重复代码
    8. 追求无副作用
    9. 复用与易用
    10. 避免暴露组件内部实现
    11. 入口处检查参数的有效性,出口处检查返回的正确性
    12. 稳定抽象原则(SAP)
      1. 组件的抽象程度与其稳定程度成正比
      2. 一个稳定的组件应该是抽象的(逻辑无关的)
      3. 一个不稳定的组件应该是具体的(逻辑相关的)
      4. 为降低组件之间的耦合度,我们要针对抽象组件编程,而不是针对业务实现编程
    13. 良好的接口设计,API 尽量和已知概念保持一致

    可配置性

    一个组件,要明确它的输入和输出分别是什么。
    组件除了要展示默认的内容,还需要做一些动态的适配。
    要做可配置性,最基本的方式是通过属性向组件传递配置的值,而在组件初始化的声明周期内,通过读取属性的值做出对应的显示修改。还有一些方法,通过调用组件暴露出来的函数,向函数传递有效的值;修改全局 CSS 样式;向组件传递特定事件,并在组件内监听该事件来执行函数等。
    在做可配置性时,为了让组件更加健壮,保证组件接收到的是有效的属性、函数接收到的是有效的参数,需要做一些校验。

    属性的值的校验

    1. 属性值的类型是否是有效的
    2. 属性是否是必填的

    函数的参数的校验

    函数的参数校验,校验函数的入参和出参。

    生命周期

    一个组件,需要明确知道在生命周期的不同阶段做该做的事。
    初始化阶段,读取属性的值,如果需要做数据和逻辑处理的话,在这个阶段进行。
    属性值变化时,如果属性发生变化,且需要对变化后的数据进行处理的话,在这个阶段进行处理。
    组件销毁阶段,如果组件已经创建了一些可能会对系统产生一些副作用的东西,可以在这个阶段进行清除。

    在这里插入图片描述


    事件传递

    组件接收用户的输入后,需要反馈给外部。
    例如一个输入框组件,用户输入数字后,组件需要告诉外部自己接收到了用户的输入,以及输入内容。
    输出一般有两种方式:

    1. 执行回调方法:直接执行 attribute 、 property 传入的 onXXX 方法,并且把数据通过函数传参的方式。大部分开源类库都使用这种方式。
    2. 事件触发器:使用 EventEmitter ,来触发约定好的事件名。调用方则需要对该事件名进行监听,数据对传到事件监听的回调方法里。

    Web Component

    在近几年里,Web Components 也被叫做 Custom Elements ,已经变成一个标准让开发者可以仅使用 HTML , CSS 和 JavaScript 来实现一个可复用的组件。这个概念最初于 2011 年提出。最低能在 IE11 上实现 Web Component ,通过 polyfill 的方式。


    使用 web component 改变了我们 UI 的架构:

    在这里插入图片描述



    你可以创建自定义的 HTML 标签,它能够从被扩展的 HTML 元素那里继承所有的属性,然后只需要简单地导入一段脚本,就可以在任何支持 Web Component 的浏览器中使用。组件中定义的所有 HTML 、 CSS 和 JavaScript 的定义域都仅限于组件内部。不需要框架,也不需要编译。

    组成 web components 技术的四部分:
    • Template Element
    • Custom Element
    • Shadow DOM
    • HTML Imports

    Template Element

    定义组件的 HTML 模板。
    本身不会被 html 解析影响文档,只有它的结构被附加到真实的节点上才会影响文档,里面可以写 style 还有 script ,style 里面的 css 不会影响布局, script 里面的脚本不会被执行,并且因为惰性,只能是内联的,不能是外部引入的。

    <template>
      <style>
        button {
        	display: block;
          padding: 0 16px;
          font-size: 16px;
          width: 100%;
          height: 40px;
          cursor: pointer;
        }
      </style>
      <button>Label</button>
    </template>
    

    template 是用标签包裹着模板内容,不同之处在于获取模板内容的方式,
    获取模板内容:

    console.log(document.querySelector('template').content);
    

    Custom Element

    对外提供组件的标签。
    通过 document.createElement 方法来创建自定义元素。w3c 规范规定必须以连字符(-)分隔。

    class Button extends HTMLElement {
      constructor() {
        super();
        //...
      }
    }
    window.customElements.define('my-button', Button);
    
    <my-button></my-button>
    

    浏览器兼容性:

    在这里插入图片描述

    Shadow DOM

    通过 Shadow DOM 封装组件的内部结构。


    什么是 Shadow DOM。
    封装使程序员能够限制对某些对象组件的未授权访问。在此定义下,对象以公共访问方法的形式提供接口作为与其数据交互的方式。这样对象的内部表示不能直接被对象的外部访问。
    Shdow DOM 将此概念引入 HTML 。它允许你将隐藏的,分离的 DOM 链接到元素。
    Shadow Root 是 Shadow 树中最顶层的节点,是在创建 shadow DOM 时被附加到常规 DOM 节点的内容。具有与之关联的 Shadow Root 的节点称为 Shadow Host。我们可以像使用普通 DOM 一样将元素附加到 Shadow Root。链接到 Shadow Root 的节点形成 Shdow 树。如下图:

    在这里插入图片描述


    可以使用 Element.attachShadow() 方法来将一个 shadow root 附加到任何一个元素上。它接受一个配置对象作为参数,该对象有一个 mode 属性,值可以是 open 或者 closed
    open 表示可以通过页面内的 JavaScript 方法来获取 Shadow DOM 。比如使用 Element.shadowRoot 属性。

    class Button extends HTMLElement {
      constructor() {
        super();
        
        // Shadow DOM:将 Shadow Root 附加到 custom element 上。
        this._shadowRoot = this.attachShadow({ mode: "open" });
        const para = document.createElement('p');
        this._shadowRoot.appendChild(para);
      }
    }
    

    Html imports

    控制组件的依赖加载。
    使引入组件不再麻烦,传统的引入需要单独引入 css 和 js , html 引入用 link 标签直接引入 html ,一个标签就可以引入一个组件,不管你有多少 css 和 js 文件。
    HTML import 为原生 HTML 提供了导入 HTML 文件的功能,使用 link 标签, rel 设置为 import , href 为被导入文件路径。

    <link rel="import" href="header.html">
    

    HTML 导入之后不会立即被浏览器解析并渲染,需要手动插入到 DOM 节点,这点跟 CSS 不同
    不过很遗憾,现在这个功能得兼容度很不友好。暂不考虑使用。

    在这里插入图片描述


    生命周期回调函数

    生命周期方法的顺序:
    constructor → attributeChangedCallback → connectedCallback

    • connectedCallback:当自定义元素第一次被连接到文档 DOM 时被调用。
    • disconnectedCallback:当自定义元素与文档 DOM 断开连接时被调用。
    • attributeChangedCallback:当自定义元素的一个属性被增加、移除或更改时被调用。


    constructorconnectedCallback 的区别在于, constructor 在元素被创建时调用,而 connectedCallback 是在元素真正被插入到 DOM 中时调用。
    connectedCallback 相对的是 disconnectedCallback ,当元素从 DOM 中移除时会调用该方法。在这个方法中可以进行必要的清理工作,但要记住这个方法不一定会被调用,比如用户关闭浏览器或关闭浏览器标签页的时候。
    另一个常用的生命周期方法是 attributeChangedCallback 。当属性被添加到 observedAttributes 数组时该方法会被调用。该回调函数仅在属性存在于 observedAttributes 数组中时才会被调用。该方法调用时的参数为属性的名称、属性的旧值和新值:

    class MyElement extends HTMLElement {
      constructor() {
        super();
      }
      
      static get observedAttributes {
        return ['foo', 'bar'];
      }
    
    	attributeChangedCallback(attr, oldVal, newVal) {
        switch(attr) {
          case 'foo':
            // do something
          case 'bar':
            // do something
        }
      }
    }
    

    总结

    前端组件化我认为更多的还是要先有组件化的思想才能更好的实践。实现组件化的手段工具有很多,重要的还是要有组件化的思想,编码前多思考。但是又要注意不要过度设计组件。

    相关链接

    https://juejin.im/post/5dd696765188254dfe47c74a
    漫谈Web前端的『组件化』
    Web Components(MDN)
    谈谈组件设计
    javascript组件化
    前端架构之路(6) - 组件化
    微软官方的 Web Components 组件库
    Web Components 入门实例教程 —— 阮一峰
    深入理解Shadow DOM v1
    Web 组件势必取代前端?

    展开全文
  • 前端组件化基础知识

    千次阅读 多人点赞 2020-12-31 22:45:17
    这里我们一起来学习前端组件化的知识,而组件化在前端架构里面是最重要的一个部分。

    同学们好,我是来自《技术银河》 的 三钻

    这里我们一起来学习前端组件化的知识,而组件化在前端架构里面是最重要的一个部分。

    讲到前端架构,其实前端架构中最热门的就有两个话题,一个就是组件化,另一个就是架构模式。组件化的概念是从开始研究如何扩展 HTML 标签开始的,最后延伸出来的一套前端架构体系。而它最重要的作用就是提高前端代码的复用性

    架构模式就是大家特别熟悉的 MVC, MVVM 等设计模式,这个话题主要关心的就是前端跟数据逻辑层之间的交互。

    所以说,前端架构当中,组件化可以说是重中之重。在实际工程当中,其实组件化往往会比架构模式要更重要一些。因为组件化直接决定了一个前端团队代码的复用率,而一个好的组件化体系是可以帮助一个前端团队提升他们代码的复用率,从而也提升了团队的整体效率

    因为复用率提高了,大家重复编写的代码量就会降低,效率就会提高,从而团队中的成员的心理和心智负担就会少很多。

    所以学习组件化可以是说是非常重要的

    这里我们先从了解什么是组件化和一个组件的基本组成部分开始。

    组件的基本概念

    组件都会区分为模块和对象,组件是与 UI 强相关的,所以某种意义上我们可以认为组件是特殊的模块或者是特殊的对象。

    组件化既是对象也是模块

    组件化的特点是可以使用树形结构来进行组合,并且有一定的模版化的配置能力。这个就是我们组件的一个基本概念。

    对象与组件的区别

    首先我们来看对象,它有三大要素

    1. 属性 —— Properties
    2. 方法 —— Methods
    3. 继承关系 —— Inherit

    在 JavaScript 中的普通对象可以用它的属性,方法和继承关系来描述。而这里面的继承,在 JavaScript 中是使用原型继承的。

    这里说的 “普通对象” 不包含复杂的函数对象或者是其他的特殊对象,而在 JavaScript 当中,属性和方法是一体的。

    相对比组件,组件里面包含的语义要素会更丰富一点,组件中的要素有:

    • 属性 —— Properties
    • 方法 —— Methods
    • 继承 —— Inherit
    • 特性 —— Attribute
    • 配置与状态 —— Config & State
    • 事件 —— Event
    • 生命周期 —— Lifecycle
    • 子组件 —— Children

    PropertiesAttribute 在英语的含义中是有很大的区别的,但是往往都会翻译成 “属性”。 如果遇到两个单词都出现的时候,就会把 Attribute 翻译为 “特性”,把 Properties 翻译成 “属性”。这两个要素要怎么区分呢?这里在文章的后面会和大家一起详细了解。

    接下来就是组件的 Config,它就是对组件的一种配置。我们经常会在一个构造函数创建一个对象的时候用到 Config ,我们传入这个构造函数的参数就叫 “Config(配置 )”。

    同时组件也会有 state(状态)。当用户去操作或者是一些方法被调用的时候,一个 state 就会发生变化。这种就是组件的状态,是会随着一些行为而改变的。而 statepropertiesattributesconfig 都有可能是相识或者相同的。

    event 就是 “事件” 的意识,而一个事件是组件往外传递的。我们的组件主要是用来描述 UI 这样的东西,基本上它都会有这种事件来实现它的某种类型的交互。

    每一个组件都会有生命周期 lifecycle,这个一会儿在文章的后面会详细的展开学习。

    组件的 children 是非常重要的一部分,children 也是组件当中一个必要的条件,因为没有 children 组件就不可能形成树形结构,那么描述界面的能力就会差很多。

    之前有一些比较流行的拖拽系统,我们可以把一些写好的 UI 组件拖到页面上,从而建立我们的系统界面。但是后面发现除了可以拖拽在某些区域之外,还需要一些自动排序,组件嵌套组件的功能需求。这个时候组件与组件之间没有树形结构就不好使了。

    最后组件在对象的基础上添加了很多语义相关的概念,也是这样使得组件变成了一种非常适合描述 UI 的概念。

    组件 Component

    我们用一张图来更深入的了解组件。

    组件最直接产生变化的来源就是用户的输入和操作,比如说当一个用户在我们的选择框组件中选中了一个选项时,这个时候我们的状态 state,甚至是我们的子组件 children 都会发生变化。

    图中右边的这几种情况就是组件的开发者与组件的关系。其中一种就是开发者使用了组件的标记代码 Markup Code,来对组件产生影响。其实,也就是开发者通过组件特性 Attribute 来更改组件的一些特征或者是特性。

    Attribute 是一种声明型的语言,也是标记型代码 Markup Code。而 Markup Code 也不一定是我们的 HTML 这种 XML 类的语言。在标记语言的大生态中,其实有非常多的语言可以用来描述一个界面的结构。但是最主流的就是基于 XML 体系的。在我们 Web 领域里面最常见的就是 XML 。而 JSX 也可以理解为一种嵌入在编程语言里面的 XML 结构。

    开发者除了可以用 Attribute,也可以用 Property 来影响组件。这个组件本身是有 Property(属性) 的,当开发者去修改一个组件的属性时,这个组件就会发生变化。而这个就是与对象中的 属性 Property 是一样的概念。

    AttributeProperty 是不是一样的呢?有的时候是,有的时候也不是,这个完全取决于组件体系的设计者。组件的实现者或者是设计者可以让 attributeproperty 统一。甚至我们把 stateconfigattributeproperty 四者都全部统一也是可以的。

    然后就是 方法 method,它是用于描述一个复杂的过程,但是在 JavaScript 当中的 Property 是允许有 getset 这样的方法的,所以最终 methodproperty 两者的作用也是差不多的。

    那么这里我们可以确定一个概念,使用组件的开发者会使用到 methodproperty,这些组件的要素。但是如果一个开发组件的开发者需要传递一个消息给到使用组件的程序员,这个时候就需要用到 事件 event。当一个组件内部因为某种行为或者事件触发到了变化时,组件就会给使用者发送 event 消息。所以这里的 event 的方向就是反过来的,从组件往外传输的。

    通过这张图我们就可以清楚知道组件的各个要素的作用,以及他们的信息流转方向

    特性 Attribute

    在所有组件的要素中,最复杂的无非就是 AttributeProperty

    我们从 Attribute 这个英文单词的理解上,更多是在强调描述性。比如,说我们描述一个人,头发很多、长相很帅、皮肤很白,这些都是属于 Attribute,也可以说是某一样东西的特性和特征方面的描述。

    Property 跟多的是一种从属关系。比如我们在开发中经常会发现一个对象,它有一个 Property 是另外一个对象,那么大概率它们之间是有一个从属关系的,子对象是从属于父对象。但是这里也有一种特殊情况,如果我们是弱引用的话,一个对象引用了另外一个对象,这样就是完全是另一个概念了。

    上面讲的就是这两个词在英文中的区别,但是在实际运用场景里面他们也是有区别的。

    因为 Property 是从属关系的,所以经常会在我们面向对象里面使用。而 Attribute 最初就是在我们 XML 里面中使用。它们有些时候是相同的,有些时候又是不同的。

    Attribute 对比 Property

    这里我们用一些例子来看看 Attribute 和 Property 的区别。我们可以看看它们在 HTML 当中不等效的场景。

    Attribute:

    <my-component attribute="v" />
    <script>
      myComponent.getAttribute('a')
      myComponent.setAttribute('a', value)
    </script>
    
    • HTML 中的 Attribute 是可以通过 HTML 属性去设置的
    • 同时也可以通过 JavaScript 去设置的

    Property:

    myComponent.a = 'value';
    
    • 这里就是定义某一个元素的 a = ‘value’
    • 这个就不是 attribute 了,而是 property

    很多同学都认为这只是两种不同的写法,其实它们的行为是有区别的。

    Class 属性

    <div class="class1 class2"></div>
    
    <script>
      var div = document.getElementByTagName('div');
      div.className // 输出就是 class1 class2	
    </script>
    

    早年 JavaScript 的 Class 是一个关键字,所以早期 class 作为关键词是不允许做为属性名的。但是现在这个已经被改过来了,关键字也是可以做属性名的。

    为了让这个关键字可以这么用,HTML 里面就做了一个妥协的设计。在 HTML 中属性仍然叫做 class 但是在 DOM 对象中的 property 就变成了 className。但是两者还是一个互相反射的关系的,这个神奇的关系会经常让大家掉一些坑里面。

    比如说在 React 里面,我们写 className它自动就把 Class 给设置了。

    Style 属性

    现在 JavaScript 语言中,已经没有 class 和 className 两者不一致的问题了。我们是可以使用 div.class 这样的写法的。但是 HTML 中就还是不支持 class 这个名字的,这个也就是一些历史包袱导致的问题。

    有些时候 Attribute 是一个字符串,而在 Property 中就是一个字符串语义化之后的对象。最典型的就是 Style

    <div class="class1 class2" style="color:blue"></div>
    
    <script>
      var div = document.getElementByTagName('div');
      div.style // 这里就是一个对象
    </script>
    

    在 HTML 里面的 Style 属性他是一个字符串,同时我们可以使用 getAttribute 和 setAttribute 去取得和设置这个属性。但是如果我们用这个 Style 属性,我们就会得到一个 key 和 vaule 的结构。

    Href 属性

    在 HTML 中 href 的 attribute 和 property 的意思就是非常相似的。但是它的 property 是经过 resolve 过的 url。

    比如我们的 href 的值输入的是 “//m.taobao.com”。这个时候前面的 http 或者是 https 协议是根据当前的页面做的,所以这里的 href 就需要编译一遍才能响应当前页面的协议。

    做过 http 到 https 改造的同学应该都知道,在让我们的网站使用 https 协议的时候,我们需要把所有写死的 http 或者 https 的 url 都要改成使用 //

    所以在我们 href 里面写了什么就出来什么的,就是 attribute。如果是经过 resolve 的就是我们的 property 了。

    <a href="//m.taobao.com"></a>
    <script>
      var a = document.getElementByTagName('a');
      // 这个获得的结果就是 "http://m.taobao.com", 这个 url 是 resolve 过的结果
      // 所以这个是 Property
      a.href;
      // 而这个获得的是 "//m.taobao.com", 跟 HTML 代码中完全一致
      // 所以这个是 Attribute
      a.getAttribute('href');
    </script>
    

    在上面的代码中我们也可以看到,我们可以同时访问 property 和 attribute。它们的语义虽然非常的接近,但是它们不是一样的东西。

    不过如果我们更改了任何一方,都会让另外一方发生改变。这个是需要我们去注意的现象。

    Input 和 value

    这个是最神奇的一对,而 value 也是特别的坑。

    我们很多都以为 property 和 attribute 中的 value 都是完全等效的。其实不是的,这个 attribute 中的 input 的 value 相当于一个 value 的默认值。不论是用户在 input 中输入了值,还是开发者使用 JavaScript 对 input 的 value 进行赋值,这个 input 的 attribute 是不会跟着变的。

    而在 input 的显示上是会优先显示 property,所以 attribute 中的 value 值就相当于一个默认值而已。这就是一个非常著名的坑,早期同学们有使用过 JQuery 的话,我们会觉得里面的 prop 和 attr 是一样的,没想到在 value 这里就会踩坑。

    所以后来 JQuery 库就出了一个叫 val 的方法,这样我们就不需要去想 attribute 还是 property 的 value,直接用它提供的 val 取值即可。

    这里一方面是一起增强一下 HTML 的 property 和 attribute 的知识。另一方面就是让我们认识到,就算是非常顶级的计算机专家设计的标签系统,也出现两个差不多的属性不等效的问题。那么如果让我们去设计一个标签系统,我们会让 property 和 attribute 等效还是不等效呢? 等学习完整个组件化的知识后,我们一起来回答一下这个问题。

    如何设计组件状态

    这里我们来分析一下,propertyattributestateconfig 在组件设计中都有什么区别。

    这里 Winer 老师给我们整理了一个表格,分成了四个场景:

    • Markup set —— 用标签去设置
    • JavaScript Set —— 使用 JavaScript 代码去设置
    • JavaScript Change —— 使用 JavaScript 代码去改变
    • User Input Change —— 终端用户的输入而改变
    Markup setJavaScript setJavaSscript ChangeUser Input Change
    property
    attribute
    state
    config

    那么我们一个一个来讲述一下:

    • Property
      • ❌ 它是不能够被 markup 这种静态的声明语言去设置的
      • ✅ 但是它是可以被 JavaScript 设置和改变的
      • ❓ 大部分情况下 property 是不应该由用户的输入去改变的,但是小数情况下,可能是来源于我们的业务逻辑,才有可能会接受用户输入的改变
    • Attribute
      • ❓ 用户的输入就不一定会改变它,与 Property 同理
      • ✅ 是可以由 markup,JavaScript 去设置的,同时也是可以被 JavaScript 所改变的
    • State
      • ❌ 状态是会由组件内部去改变的,它不会从组件的外部进行改变。如果我们想设计一个组件是从外部去改变组件的状态的话,那么我们组件内部的 state 就失控了。因为我们不知道组件外部什么时候会改变我们组件的 state,导致我们 state 的一致性无法保证。
      • ✅ 但是作为一个组件的设计者和实践者,我们一定要保证用户输入是能改变我们组件的 state 的。比如说用户点击了一个 tab,然后点中的 tab 就会被激活,这种交互一般都会用 state 去控制的。
    • Config
      • ✅ Config 在组件中是一个一次性生效的东西,它只会在我们组件构造的时候触发。所以它是不可更改的。也是因为它的不可更改性,所以我们通常会把 config 留给全局。通常每个页面都会有一份 config,然后拿着这个在页面内去使用。

    组件生命周期 Lifecycle

    讲到生命周期,我们最容易想到的会有两个,一个是 created 一个是 destroy。世界万物的生命必定会有 出生死亡,这两个生命周期。

    那么在这两个开始与结束之间有什么生命周期呢?我们就需要想一下,一个组件在构造到销毁之间都会发生什么事情。

    一个组件有一个非常重要的事情,就是它被创建之后,它有没有被显示出来。这里就涉及生命周期中的 mount,也就是组件有没有被挂載到 “屏幕的这棵树上”。这个生命周期我们可以在 React 和 Vue 里面看到,我们经常会使用这个生命周期,在组件被挂載后做一些相应的初始化操作。

    有挂載那必然就会有卸载,所以组件中的 mountunmount 是一组生命周期。而这个挂載与卸载的整个生命周期是可以反复的发生的,我们可以挂上去然后卸下来,然后再挂上去,这样反复又反复的走这个生命周期。

    所以在 unmount 之后,我们是可以回到 created 构建组件的这个生命周期的状态。

    那么组件还会在什么时候发生状态更变呢?这里我们就有两种情况:

    • 程序员使用代码去改变或者设置这个组件的状态
    • 用户输入时影响了组件的状态

    比如说我们用户点了一下按钮或者 Tab,这个时候就会触发这个组件的状态更变。同时也会产生一个组件的生命周期,而这个生命周期就是 Render 渲染或者 Update 更新。

    所有这些生命周期加在一起就是我们一个组件完整的生命周期。我们看到的所谓 willMountdidMount 无非就是这个生命周期之中更细节的位置。下面我给大家附上一张完整的生命周期的图。


    Children

    最后我们来讲一下 Children (子组件)的概念。Children 是构建组件树最重要的一个组件特性,并且在使用中其实有两种类型的 Children:

    • Content 型 Children —— 我们有几个 Children,但是最终就能显示出来几个 Children。这种类型的 Children,它的组件树是非常简单的。
    • Template 型 Children —— 这个时候整个 Children 它充当了一个模版的作用。比如说我们设计一个 list,但是最后的结果不一定就与我们 Children 代码中写的一致。因为我们 List 肯定是用于多个列表数据的,所以 list 的表示数量是与我们传入组件的 data 数据所相关的。如果我们有 100 个实际的 children 时,我们的 list 模版就会被复制 100 份。

    在设计我们的组件树的 children 的时候,一定要考虑到这两种不同的场景。比如我们在 React中,它没有 template 型的 children,但是它的 children 可以传函数,然后这个函数可以返回一个 children。这个时候它就充当了一个模版型 children 的作用了。那么在 Vue 里面当我们去做一些无尽的滚动列表的时候,这个对 Vue 的模版型 children 就有一定的要求。

    结束语

    这里我们就学习完了整个组件的概念和知识了,下一篇文章我们就会一起来设计和搭建一个组件系统,并且了解到它的各方各面的实践知识。我们还会用一些典型的组件和典型的功能来让大家对组件的实现有一定的了解。



    博主开始在B站直播学习,欢迎过来《直播间》一起学习。

    我们在这里互相监督,互相鼓励,互相努力走上人生学习之路,让学习改变我们生活!

    学习的路上,很枯燥,很寂寞,但是希望这样可以给我们彼此带来多一点陪伴,多一点鼓励。我们一起加油吧! (๑ •̀ㅂ•́)و


    我是来自《技术银河》的三钻,一位正在重塑知识的技术人。下期再见。


    推荐专栏

    小伙伴们可以查看或者订阅相关的专栏,从而集中阅读相关知识的文章哦。

    • 📖 《前端进阶》 — 这里包含的文章学习内容需要我们拥有 1-2 年前端开发经验后,选择让自己升级到高级前端工程师的学习内容(这里学习的内容是对应阿里 P6 级别的内容)。

    • 📖 《数据结构与算法》 — 到了如今,如果想成为一个高级开发工程师或者进入大厂,不论岗位是前端、后端还是AI,算法都是重中之重。也无论我们需要进入的公司的岗位是否最后是做算法工程师,前提面试就需要考算法。

    • 📖 《FCC前端集训营》 — 根据FreeCodeCamp的学习课程,一起深入浅出学习前端。稳固前端知识,一起在FreeCodeCamp获得证书

    • 📖 《前端星球》 — 以实战为线索,深入浅出前端多维度的知识点。内含有多方面的前端知识文章,带领不懂前端的童鞋一起学习前端,在前端开发路上童鞋一起燃起心中那团火🔥

    展开全文
  • Android组件化方案

    万次阅读 多人点赞 2017-02-15 19:01:52
    上图是组件化工程模型,为了方便理解这张架构图,下面会列举一些组件化工程中用到的名词的含义: 名词 含义 集成模式 所有的业务组件被“app壳工程”依赖,组成一个完整的APP; ...
  • 组件化开发

    千次阅读 2022-03-31 15:56:10
    业务越来越多,代码量也越来越多,耦合严重,层次混乱,页面互相之间的跳转有着极强的关联性,所有代码都写在app module中,编译一次都要5-6分钟,为了方便以后项目的开发/测试以及提高编译性能就需要进行组件化了。...
  • Android 组件化,从入门到不可自拔

    万次阅读 多人点赞 2019-04-19 02:54:33
    组件化能够显著提高Android项目开发效率,支持不同业务组件单独打包或者组合打包,可以说是Android开发者必备技能。 本文通过一个极其简单的实践案例,梳理了组件化的配置过程,并辅以全部源码,希望对还没有应用...
  • Android 组件化架构-简谈

    千次阅读 2022-03-14 11:11:24
    了解组件化 在了解组件化之前,我们需要先了解模块化。 模块化就是将整体业务集合按照功能的不同,抽离到不同的模块中,这样做的好处就是在团队协助中能够较好的区分各自负责的功能模块、也能使得整个工程显得不是...
  • 前端组件化思想

    万次阅读 多人点赞 2018-02-27 09:59:14
      组件化并不是前端所特有的,一些其他的语言或者桌面程序等,都具有组件化的先例。确切的说,只要有UI层的展示,就必定有可以组件化的地方。简单来说,组件就是将一段UI样式和其对应的功能作为独立的整体去看待,...
  • 组件化优化
  • 前端为什么要组件化开发?

    千次阅读 2021-11-05 11:02:39
    文章目录什么是前端模块化,组件化,工程化?为什么要组件化组件化和模块化的区别插槽组件传值组件库 什么是前端模块化,组件化,工程化? 前端模块化: 可以理解为一组自定义业务的抽象封装,是根据项目的情况...
  • Android组件化实现

    千次阅读 2022-03-16 21:28:27
    为什么需要组件化组件化顾名思义就是将代码按功能或者业务划分成一个个组件,小的项目一般不需要,只有当项目大到一定程度,代码量足够多的时候我们就需要用到组件化组件化总体来说有下面几个收益点: 代码...
  • 一、组件化的意义 随着Android 项目代码和结构逐渐复杂,维护成本会指数型上升,通常我们会利用Android Studio自带的Module去拆分项目代码。但这种拆分显然需要基于一定逻辑和结构,目前主流的拆分思路有两种:分别...
  • Android组件化开发简单示例

    千次阅读 多人点赞 2020-11-21 15:01:52
    一、组件化初始模型 1、通过一个简单的android项目初始架构图来了解组件化,如下图: 打个生动的比喻,把我们的APP当成一个电脑主机,那么app外壳就是主机外壳,main组件就是主板,其他各个组件就类似于硬盘、...
  • Android组件化开发

    千次阅读 多人点赞 2020-02-26 14:23:21
    什么是组件化开发 所谓组件化,就是将整个庞大的项目以业务逻辑进行拆分成多个模块,并且各个模块之间相互独立,相互解耦,每一个模块可以单独进行开发调试,各个模块调试完,以library的形式依赖在壳App中组合成一...
  • 面试官:谈一下你对Vue组件化的理解

    千次阅读 多人点赞 2022-03-20 21:27:52
    假设说采取传统开发网页的方式去开发一个系统,在一定程度上,会...为了避免这些弊端可以采取组件化设计去开发一个多人协作、功能又较多的项目,组件化开发带来的可维护性和可复用性可以提升开发效率、降低代码耦合度。
  • 安卓一步一步搭建组件化

    千次阅读 多人点赞 2020-11-09 20:20:05
    安卓组件化的搭建和基本功能的实现1.组件化是什么?1.1 了解组件化:1.2 组件化的基本结构:1.3 组件化的优点:2.组件化框架的搭建:2.1 第一步:搭建基础层2.1.1 创建config.gradle2.1.2 建立一个library模块作为...
  • 前言组件化对于任何一个业务场景复杂的APP以及经过多次迭代之后的产品来说都是必经之路,组件化是指解耦复杂系统时将多个功能模块拆分、重组的过程。组件化要做的不仅仅是表面上看到的模块拆分解耦...
  • Vue基础知识总结 4:vue组件化开发

    千次阅读 多人点赞 2021-07-21 23:37:50
    和过程编程相比,函数式编程里函数的计算可随时调用。 filter函数自动过滤对象的所有元素,返回true才会存入指定对象; Reduce函数对数组内部的所有元素进行汇总; 2、代码实例 <!DOCTYPE
  • iOS组件化开发从开始到完整总结

    千次阅读 2019-09-19 17:23:16
    组件化介绍 需求来源 随着项目规模不断扩大,业务模块增多,开发过程中会有多条产品线(多人或多小组开发不同的功能);如果用传统的开发模式,会导致代码臃肿,编译速度越来越慢,开发效率低下,代码维护成本越来越高. ...
  • iOS组件化开发流程

    千次阅读 2021-11-18 15:12:39
    组件化开发之前,我们先了解一下,什么是组件化,为什么要组件化开发 举个很简单的例子,我们平时在开发的时候用的第三方库,我们直接pod下来就可以使用,跟项目是分开的独立模块,就可以理解为一个组件,为什么要...
  • 【Android 插件化】插件化简介 ( 组件化与插件化 )

    千次阅读 多人点赞 2021-05-29 21:27:36
    一、组件化与插件化、 二、插件化示例、 三、插件化标准引入、
  • Android - 组件化、模块化开发

    千次阅读 2020-07-07 16:36:54
    一、组件化与模块化介绍 组件化 组件:最初的目的是代码重用,功能相对单一或者独立。在整个系统的代码层次上位于最底层,被其他代码所依赖,所以说组件化是纵向分层。 特点:把重复的代码提取出来合并成为一个...
  • 二、组件化 1、概念 2、使用 3、版本管理 4、模块间跳转 5、资源命名问题 三、插件化 四、热修复 1、概述 2、流派 3、原理 五、总结 前言 谈到热修复相信大家应该比较熟悉,因为它是目前比较重要的...
  • Vue全家桶之组件化开发

    千次阅读 多人点赞 2020-01-06 07:56:26
    学习组件化开发,首先掌握组件化的开发思想,组件的注册方式,组件间的数据交互方式,组件插槽的用法,vue调式工具的用法,组件的方式来实现业务逻辑功能。 组件化开发思想,组件注册,组件调式,组件间的数据交互,...
  • 爱奇艺知识WEB前端组件化实践

    千次阅读 2021-01-29 11:42:26
    组件化作为一种开发模式,其在代码复用,提高开发效率上的效果被广泛认可。组件化思想适用于移动端、Web前端、PC端、TV端等多种类型的客户端和前端开发。本文主要讲述爱奇艺知识 WEB 前端...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 1,382,089
精华内容 552,835
关键字:

组件化