精华内容
下载资源
问答
  • 前端组件化
    千次阅读 多人点赞
    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:峰华前端工程师
    公众号:峰华前端工程师

    更多相关内容
  • Vue.js是我在2014年2月开源的一个前端开发库,通过简洁的API提供高效的数据绑定和灵活的组件系统。在前端纷繁复杂的生态中,Vue.js有幸受到一定程度的关注,目前在GitHub上已经有5000+的star。本文将从各方面对Vue....
  • 前端组件化

    千次阅读 2020-12-21 10:40:47
    组件化 [外链图片转存失败,源站可能有... 什么是组件化? 前端组件化开发,就是将页面的某一部分独立出来,将这一部分的数据层(M)、视图层(V)和控制层(C)用黑盒的形式全部封装到一个组件内,暴露出一些开箱即用

    组件化

    在这里插入图片描述

    什么是组件化?

    前端组件化开发,就是将页面的某一部分独立出来,将这一部分的数据层(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 组件势必取代前端?

    展开全文
  • 主要介绍了深入理解Vue.js轻量高效的前端组件化方案 ,需要的朋友可以参考下
  • 前端组件化基础知识

    千次阅读 多人点赞 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获得证书

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

    展开全文
  • 基于React的前端组件化研究与设计,周兴宇,卞佳丽,随着互联网行业的快速发展,Web应用更加庞大和复杂,对用户体验和交互有着越来越高的需求,对前端技术不断提出新的要求。React是一�
  • 这一切,使得业界对前端开发方案的思考上多了很多,以React框架为代表推动的组件化开发方案就是目前业界比较认可的方案,本文将和大家一起探讨一下组件化开发方案能给我们带来什么,以及如何在ReactNative项目的运用...
  • 爱奇艺知识WEB前端组件化实践

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

    组件化作为一种开发模式,其在代码复用,提高开发效率上的效果被广泛认可。组件化思想适用于移动端、Web前端、PC端、TV端等多种类型的客户端和前端开发

    本文主要讲述爱奇艺知识 WEB 前端团队如何结合自身的业务特点,探索和实践了一套高效的前端组件化方案。

    组件化:前端解耦和提效利器

    前端业务发展过程中,代码体积会越来越大,业务的逻辑复杂程度也会随着迭代越来越高。

    组件化的意义在于提效,交付的产物是可用的、直观的、可组合的业务形态。

    目前行业内前端组件化按面向使用人群大致可分为3类:

    面向运营人员:运营人员主要进行页面的创建、设计,组件类似乐高积木一样,拼凑起来,针对特殊的组件,可交给开发人员定制实现,实现一些偏静态或者偏固定功能(如抽奖、问答、支付等)的页面,各个大厂一般都有自己 Low Code 平台下类似的产品。

    面向设计人员:设计人员主要参与页面设计,并直接将设计稿转换成前端代码,省去前端的开发过程,比如阿里的 imgcook,也可针对一些复杂逻辑,可交给开发人员进行二次开发。

    面向开发人员:这也是最传统的开发模式,主要针对的是动态的页面,需要后端接口配合的场景,各个页面的组件通用,所以抽离出来公共组件供各个页面调用。

    爱奇艺知识的组件化设计属于第三种,也即是面向开发人员而设计的。在开发层面,通过组件的复用,能够减少冗余代码、实现功能和业务逻辑的解耦,并且通过组件的拓展提高可拓展性、可维护性

    业务场景:项目多且独立、又要保持产品一致性

    具体到组件的拆分,目前在业内并没有统一的标准,需要结合具体的业务场景,因为不同场景下组件拆分的力度、程度可能都有所不同。

    爱奇艺知识 WEB 前端项目构成

    从爱奇艺知识WEB前端项目构成可以看到,知识Web前端的项目很多,我们把项目按类型分开,每个分类下的项目都是独立的项目,完成其相关的业务功能,各项目是独立开发、独立部署的,并且项目间没有直接的业务关联。项目虽然多,但项目中都有业务相关的课程、讲师、店铺等实体,而这些实体的UI样式在各个项目中要基本保持一致,另外一些通用的UI元素,比如弹框、loading等也要保证风格一致,因此为了产品体验一致性,同时提升开发效率,提高代码复用性,Web前端需要进行组件抽离

    在这里需要简单说明一下,我们Web端组件化思路和知识移动端团队在组件化的思路是不太一样的,移动端更多的是从业务和功能的纵向进行组件划分,也可以称之为模块化,每一个组件或模块既包括UI又包括业务逻辑,比如播放器组件播控播放控制和UI样式,分享组件包括分享功能和弹窗,支付组件包括支付流程和收银台支付结果页;而Web前端的组件化更偏重UI层面,逻辑更多的是放在页面中,这也符合Web开发的特点,UI元素复用性更高

    感兴趣的同学可以了解一下移动端组件化的设计思路,传送门在这里:爱奇艺知识移动端组件化探索和实践

    根据上述背景,知识Web前端进行了相应的组件化建设,目标如下:

    1. 代码复用,提升开发效率

    根据业务特点,横向和纵向划分组件,以组件为单位承接业务需求。虽然以UI组件为主,但对于相对独立的功能,也进行了纵向的组件抽取。

    2. 组件独立开发维护,与各项目解耦

    组件单独开发、调试、发布,供业务方调用,业务方只需专注业务本身。

    3. 更方便的组件调用,更合理的路由跳转

    组件调用需明确输入、输出参数,并对参数进行校验并给出合理错误提示;当各项目引用业务组件 涉及到跳转时,组件库自动决定是项目内(前端路由)跳转还是项目外(location.href)跳转。

    4. 组件文档化,支持在线调试,降低组件使用门槛

    组件库需要有完善的使用说明文档,支持在线调试,使业务方更加容易、便捷的使用。

    整体设计

    任何工具的设计都需要从实际的业务场景出发,回到我们的场景下,需要考虑如何管理项目间所依赖的公共组件资源变得非常重要,组件的设计既要符合业务方使用的便利性,同时也要满足组件自身开发的可维护性、可扩展性

    我们认为,组件化的关键在于组件的分层以及拆分,明确组件的层级以及对层级的定义对于组件化系统至关重要。下面是基于我们自身的业务,我们将组件进行如下分层(图示):

    为了更好的理解,我们将层级之间的关系进行如下展示(图示):

    由于 Card 组件由多个 Item 组件组成,所以 Card 组件与 Item 组件的关系如下:

    card组件

    基于以上设计,我们简单总结一下,组件按类别可分为:基础组件、业务组件。

    其中基础组件可分为:基础 UI 组件、基础工具组件;业务组件可分为:item 组件、Card 组件、区块组件、功能组件、页面组件。

    基础组件

    • 基础工具库:这是一组 JS 库,无 UI 界面,包含项目所使用的基础功能,如:网络请求、页面埋点、异常上报、与 Native 交互的 CallNative,以及一些常用utility工具等等。

    • 基础UI组件:包含了:文本、图片、按钮、布局、loading 加载、toast 提示等常用的UI组件。

    业务组件

    • Item 组件:业务组件的最小单元,比如:列表中的一项,宫格中的一项,金刚位中的一项、轮播图中的某张图片等等。业务方可引入所需的 item 组件或组合item组件来完成页面的展示。

    • Card 组件:这是一套服务于 Card 化所对应的一套组件,这里简单介绍一下 Card 化:后端返回的是一套基于 Card 的数据结构,每个card数据与前端某一个UI组件一一对应,通过对数据的配置来实现对前端展现的控制。Item 组件也可以属于 Card 的一部分,由 Card 组件组合多个 Item 组件来进行界面的展示。

    • 区块组件:是为了完成某一个区块的 UI 展示,而抽离出来的组件,供业务方使用,比如:评论组件、评价组件等。

    • 功能组件:是为了完成某一项具体的业务功能而抽离出的组件,比如:播放器组件、发布器组件、分享组件等。

    • 页面组件:也叫路由组件,这是基于微前端所抽离的组件,我们的某个页面也可以当做组件,从而被其他项目所使用。

    技术实现

    由于基础组件、区块组件、功能组件已经是非常通用的设计,这里我们重点介绍一下业务组件中的Card 组件、Item 组件。需要注意的是,不管是基础组件还是业务组件,我们在设计之初都需要让其符合以下设计原则。

    设计原则

    我们的组件设计原则需满足如下要求:

    • 需要统一技术栈,保证组件在同一技术生态。

    • 单一职责,一个组件只专注做一件事,且把这件事做好。

    • 追求无副作用,输入一但确定,输出就是固定的。

    • 可配置,一个组件要明确它的输入和输出分别是什么,同时入口处检查参数的有效性。

    • 粒度适中,划分粒度的大小需要根据实际情况权衡,太小会提升维护成本,太大又不够灵活和高复用性。每一个组件都应该有其独特的划分目的的,有的是为了实现复用,有的是为了封装复杂度 实现业务清晰的目的。

    • 适当的包体大小,便于页面快速加载。

    • 完善的使用说明文档。

    数据协议

    在项目初期,我们的 H5 移动端首页:https://zhishi.m.iqiyi.com 是基于后端的 Card 化数据,由后端数据选择前端的 Card 组件、Item 组件来进行页面的展示,在这种场景下,我们的 Card 组件、Item 组件首先被设计出来,与此同时我们设计了一套前端数据协议(可以理解为数据格式定义),后端 Card 数据经过前端协议后,转换成前端的 Card 数据结构,在此数据结构的基础上进而抽离出 Card 组件、Item 组件。

    后来发现我们大量的页面并非基于 Card 化数据,而是基于不同的 API 、不同的数据结构,如何让这些页面也复用我们的 Card 组件、Item 组件呢?为此我们需要不同的转换器,将后端的接口数据根据前端数据协议转换为 前端的数据格式(如下图),这样无论后端接口数据如何改变,我们只需更改转换器的逻辑,而无需更改前端组件代码,进而复用我们的 Card 组件、Item 组件。

    UI 实现

    在具体的 UI 实现方面,我们拿下图中我们的业务 UI 组件库内常用的两个Item举例:课程列表 Item、课程宫格 Item

    不难发现Item 组件主要是由 图片、文本组成的,而图片、文本恰好又属于我们的基础 UI 组件库范畴。

    • 图片组件,是在保留原生`img`的特性前提下,支持懒加载,自定义占位、加载失败占位、图片获取策略(webp or jpg);

    • 文本组件,支持 单行、多行截断。

    所以我们在编写 Item 组件的时候只需组合图片、文本组件即可,这样后期随着我们组件数量增长,当我们开发其他 Item 组件的时候会变得非常轻松,效率也得到极大提升。

    还有一点需要注意的是:可能我们的组件需要同时满足在移动端手机、平板、 PC端的正常展现,这样就会涉及到布局以及屏幕适配的问题,同时也涉及到设备旋转带来的屏幕尺寸动态变化的问题。所以在组件在设计的时候 宽、高不能定死,而是引用方设定的,同时我们需要在组件内兼容不同屏幕的适配样式,也可以分别导出不同屏幕的适配样式文件供引用方使用。

    构建工具

    由于基础工具库 是一组 JS 库,所以我们使用 rollup 构建,它是一个高效的 library 模块打包器,可以使我们的组件更加轻量、简洁,同时生成 es, umd 格式文件供前端调用。

    UI 组件库,我们使用 VUE 作为前端技术栈,使用 Webpack 进行构建,业务组件项目支持导出多个组件入口,支持业务组件按需加载(如下所示)

    import {
     item40000,
     item40001,
     item40010,
     item40011
    } from '@iqiyi-kpp/ui-biz-vue'

    组件支持按需加载就意味着,各组件入口需要单独打包,这会涉及到各入口的公共资源重复引用的问题。

    我们可以使用@babel/runtime,@babel/plugin-transform-runtime,设置 core-js false,做大限度减小 babel 转换的包体。

    使用 babel 命令打包 JS 文件(不使用 webpack),减小 JS 包体。

    使用 webpack externals 将第三方依赖,以及每个组件的公共引用提出来,通过 require 的方式去引用,而不是重复打包到每个组件内。

    技术文档

    我们的组件库开发完成,并不意味着组件化这件事就算做完了,一个良好的组件库往往需要有一个良好的文档说明,这样会降低团队成员的沟通成本,所以基于 storybook 我们搭建了 UI 组件的文档站点,比如下图的基础 UI 组件库文档,可动态展示最新的组件版本,同时支持 UI 组件在线调试,更能直观的表达组件样式与交互,增强了组件的易用性。

    总结

    前端组件库一般是放到公司的私有仓库上,通过 npm 进行维护的,使用方需要 引用并打包到自己的项目内才能使用。随着 webpack5 module federation 的流行,也可以将组件当作微模块单独发布,各使用方可通过动态 load module 的方式进行引用

    由此也可以看到前端组件化的优势:可以很大程度上降低系统各个功能的耦合性,这对前端工程化及降低代码的维护成本来说,是有很大的好处的。

    爱奇艺知识 WEB 前端组件库经过不断的完善和迭代,基本实现了组件化的全部目标,组件通过独立开发、测试、发布,使得各业务项目只需关注本身业务,组件得到充分复用,目前组件化已成功运用到各个业务项目中。

    组件化并非一蹴而就,而是一个持续的过程,比如:随着业务组件会变得很多,业务组件需要支持业务方按需加载,同时需要考虑组件的包体大小,不能因组件包体过大导致页面加载过慢;业务组件应该通过类似 Jenkins 工具统一发布,发布前应进行自动化测试,以完成很好的测试覆盖等等。

    当然在实际开发工作中会有许多不理想的情况,比如一个小项目没有那么多重复的代码,比如设计团队不认可组件化等等,所以组件化需要考虑具体的业务场景,在这个前提下设计出符合我们自身的组件化系统才有必要。

    也许你还想看

    优化无止境,爱奇艺中后台 Web 应用性能优化实践

    一切数据皆可配置:爱奇艺海外站的运营后台设计实践

     扫一扫下方二维码,更多精彩内容陪伴你!

    展开全文
  • 前端独立系统 后端独立系统 中间有类似 swagger 的接口约定机制 缺点: 形成这个问题的原因是整个研发过程两极分化,断裂得很厉害,前后端的唯一桥梁是接口,并且,一般来说,当一端产生了变更之后,很难有一种...
  • 基于Vue的前端组件化研究与实践,李曰斌,詹舒波,在这个互联网高速发展的时代,用户交互和用户体验变的越来越重要。前端技术的发展也呈现井喷的趋势,各种新技术,新框架不断涌现
  • 前端组件化思想及实践,黄思远,詹舒波,本文将通过总结前端组件化思想,将前后端的耦合分离,封装重复性的操作,从而实现一种包含数据绑定、逻辑运算等众多功能的模块化组件
  • 前端组件化PPT教案学习.pptx
  • 富途web前端负责⼈TooBug在GMTC2017全球移动技术大会上做了主题为《基于webpack和npm的前端组件化实践》的分享,就富途组件化的问题,组件化目标,跨项目复用概念进行了深入的分享
  • 前端组件化思想

    万次阅读 多人点赞 2018-02-27 09:59:14
      先说说为什么要写这篇文章吧:不知从什么时候开始,大家相信前端摩尔定律:“每18个月,前端难度会增加一倍”。我并不完全认可这个数字的可靠性,但是这句话的本意我还是非常肯定的。   是的,前端越来越简单...
  • 前端组件化开发在集装箱港口信息化中的应用.pdf
  • 例:弹窗内部的表单,tab页,单个select的远程搜索,页面上中下多层结构拆分多个组件等 如果对性能有要求需考虑 四.代码数目过500行 一般来说不会这么多代码,可以考虑拆分 五.常量配置 建议扔独立js文件 例:echart...
  • 前端组件化开发

    万次阅读 多人点赞 2016-11-28 16:40:54
    它的核心意义是分离职责,属于代码级模块的产出。它本身是提供服务的功能逻辑,是一组具有一定内聚性代码的组合,职责明确。 组件(Component)和模块(Module)又是一对容易混淆的名词,也常常被用来相互替换。个人...
  • 前端组件化开发和模块化开发的区别  首先,组件化和模块化的意义都在于实现了分治,目前我们开发的项目复杂度不断的上升,早已不是我们一个人能完成的工作,团队合作的话又会产生配合困难等各方面问题,组件化和...
  • 组件化的工作方式信奉独立、完整、自由组合。目标就是尽可能把设计与开发中的元素独立化,使它具备完整的局部功能,通过自由组合来构成整个产品。 从页面元素的可复用性角度考虑,我们将将组件按类型分为公众组件、...
  • 前端组件化框架之路

    千次阅读 2018-06-27 14:24:29
    但是在Web前端这个领域,并没有很通用的组件模式,因为缺少一个大家都能认同的实现方式,所以很多框架/库都实现了自己的组件化方式。前端圈最热衷于造轮子了,没有哪个别的领域能出现这么混乱而欣欣向荣的景象。这一...
  • 前端为什么要组件化开发?

    千次阅读 2021-11-05 11:02:39
    前端组件化: 指对具体的某个功能的封装,比如所有的分页可以封装为分页组件来统一使用,以此来达到组件复用,提高开发效率。 前段工程化: 概念:指使用软件工程的技术和方法来进行前端项目的
  • 包括课程概述、课程安排、学习前提等方面的介绍,让同学们对组件化有一个直观的了解。 1-1 课程导学 第2章 环境及知识准备 帮助大家梳理课程要用到的开发环境及知识储备 2-1 环境及知识准备 第3章 业务开发流程与...
  • 组件化与模块化已经深入体现到软件开发当中,也是为了让开发者更好的去解决软件上的高耦合、低内聚、无重用的3大代码问题。在写这篇文章之前,我也百度过很多相关介绍文章,各个作者都有自己的一些想法或者总结,...
  • 基于 Angular 的前端组件化

    千次阅读 2018-07-24 00:01:48
    其中组件化是最早在前端范围内展开应用的开发思想,比如 Bootstrap 对于标签样式的规范就是一种前端组件化。本篇文章是基于 Angular 框架对前端组件化的一些实践和思考,如有纰漏,还请谅解,欢迎指...
  • 前端组件化设计思路

    千次阅读 2019-01-05 14:14:35
    目前前端三大框架(Vue.js, Angular.js, React.js)都在引领着前端组件化开发方向,组件化前端开发方式的确为业务实现带来了前所未有的方便,其实组件化开发早已经具有(YUI),但如何封装一个优秀的组件,可能...
  • 目录前端的工程化、模块化和组件化一、工程化1、为什么要以工程化的思想来看待前端项目?2、 那我们又该怎样去做到前端的工程化呢?二、模块化1、模块2、为什么要模块化?3、作用三、组件化1、组件2、为什么要组件化...
  • 用 JSX 建立组件 Parser(解析器)- 前端组件化

    万次阅读 多人点赞 2021-01-08 17:16:56
    这里我们一起从 0 开始搭建一个组件系统。首先通过上一篇《前端组件化基础知识》中知道,一个组件可以通过 Markup 和 JavaScript 访问的一个环境。

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 243,771
精华内容 97,508
关键字:

前端组件化