在理解模块化、组件化之前,最好先理解一下什么是高内聚,低耦合。它能更好的帮忙你了解模块化、组件化。

高内聚,低耦合

高内聚,低耦合是软件工程中的概念,它是判断代码好坏的一个重要指标。高内聚,就是指一个函数尽量只做一件事。低耦合,就是两个模块之间的关联水平低。

仅看文字可能不太好了解,上面来看一个简略的示例。

// math.jsexport function add(a, b) {    return a + b}export function mul(a, b) {    return a * b}
// test.jsimport { add, mul } from 'math'add(1, 2)mul(1, 2)mul(add(1, 2), add(1, 2))

下面的 math.js 就是高内聚,低耦合的典型示例。add()mul() 一个函数只做一件事,它们之间也没有间接分割。如果要将这两个函数分割在一起,也只能通过传参和返回值来实现。

既然有好的示例,那就有坏的示例,上面再看一个不好的示例。

// 母公司class Parent {    getProfit(...subs) {        let profit = 0        subs.forEach(sub => {            profit += sub.revenue - sub.cost        })        return profit    }}// 子公司class Sub {    constructor(revenue, cost) {        this.revenue = revenue        this.cost = cost    }}const p = new Parent()const s1 = new Sub(100, 10)const s2 = new Sub(200, 150)console.log(p.getProfit(s1, s2)) // 140

下面的代码是一个不太好的示例,因为母公司在计算利润时,间接操作了子公司的数据。更好的做法是,子公司间接将利润返回给母公司,而后母公司做一个汇总。

class Parent {    getProfit(...subs) {        let profit = 0        subs.forEach(sub => {            profit += sub.getProfit()        })        return profit    }}class Sub {    constructor(revenue, cost) {        this.revenue = revenue        this.cost = cost    }    getProfit() {        return this.revenue - this.cost    }}const p = new Parent()const s1 = new Sub(100, 10)const s2 = new Sub(200, 150)console.log(p.getProfit(s1, s2)) // 140

这样改就好多了,子公司减少了一个 getProfit() 办法,母公司在做汇总时间接调用这个办法。

高内聚,低耦合在业务场景中的使用

现实很美妙,事实很残暴。方才的示例是高内聚、低耦合比拟经典的例子。但在业务场景中写代码不可能做到这么完满,很多时候会呈现一个函数要解决多个逻辑的状况。

举个例子,用户注册。个别注册会在按钮上绑定一个点击事件回调函数 register(),用于解决注册逻辑。

function register(data) {    // 1. 验证用户数据是否非法    /**    * 验证账号    * 验证明码    * 验证短信验证码    * 验证身份证    * 验证邮箱    */    // 省略一大堆串 if 判断语句...    // 2. 如果用户上传了头像,则将用户头像转成 base64 码保留    /**    * 新建 FileReader 对象    * 将图片转换成 base64 码    */    // 省略转换代码...    // 3. 调用注册接口    // 省略注册代码...}

这个示例属于很常见的需要,点击一个按钮解决多个逻辑。从代码中也能够发现,这样写的后果就是三个性能耦合在一起。

依照高内聚、低耦合的要求,一个函数应该尽量只做一件事。所以咱们能够将函数中的另外两个性能:验证和转换独自提取进去,封装成一个函数。

function register(data) {    // 1. 验证用户数据是否非法    verifyUserData()    // 2. 如果用户上传了头像,则将用户头像转成 base64 码保留    toBase64()    // 3. 调用注册接口    // 省略注册代码...}function verifyUserData() {    /**    * 验证账号    * 验证明码    * 验证短信验证码    * 验证身份证    * 验证邮箱    */    // 省略一大堆串 if 判断语句...}function toBase64() {    /**    * 新建 FileReader 对象    * 将图片转换成 base64 码    */    // 省略转换代码...}

这样批改当前,就比拟合乎高内聚、低耦合的要求了。当前即便要批改或移除、新增性能,也十分不便。

模块化、组件化

模块化

模块化,就是把一个个文件看成一个模块,它们之间作用域互相隔离,互不烦扰。一个模块就是一个性能,它们能够被屡次复用。另外,模块化的设计也体现了分治的思维。什么是分治?维基百科的定义如下:

字面上的解释是“分而治之”,就是把一个简单的问题分成两个或更多的雷同或类似的子问题,直到最初子问题能够简略的间接求解,原问题的解即子问题的解的合并。

从前端方面来看,独自的 JavaScript 文件、CSS 文件都算是一个模块。

例如一个 math.js 文件,它就是一个数学模块,蕴含了和数学运算相干的函数:

// math.jsexport function add(a, b) {    return a + b}export function mul(a, b) {    return a * b}export function abs() { ... }...

一个 button.css 文件,蕴含了按钮相干的款式:

/* 按钮款式 */button {    ...}

组件化

那什么是组件化呢?咱们能够认为组件就是页面里的 UI 组件,一个页面能够由很多组件形成。例如一个后盾管理系统页面,可能蕴含了 HeaderSidebarMain 等各种组件。

一个组件又蕴含了 template(html)scriptstyle 三局部,其中 scriptstyle 能够由一个或多个模块组成。

从上图能够看到,一个页面能够分解成一个个组件,每个组件又能够分解成一个个模块,充分体现了分治的思维(如果忘了分治的定义,请回头再看一遍)。

由此可见,页面成为了一个容器,组件是这个容器的根本元素。组件与组件之间能够自在切换、屡次复用,批改页面只需批改对应的组件即可,大大的晋升了开发效率。

最现实的状况就是一个页面元素全副由组件形成,这样前端只须要写一些交互逻辑代码。尽管这种状况很难齐全实现,但咱们要尽量往这个方向下来做,争取实现全面组件化。

Web Components

得益于技术的倒退,目前三大框架在构建工具(例如 webpack、vite...)的配合下都能够很好的实现组件化。例如 Vue,应用 *.vue 文件就能够把 templatescriptstyle 写在一起,一个 *.vue 文件就是一个组件。

<template>    <div>        {{ msg }}    </div></template><script>export default {    data() {        return {            msg: 'Hello World!'        }    }}</script><style>body {    font-size: 14px;}</style>

如果不应用框架和构建工具,还能实现组件化吗?

答案是能够的,组件化是前端将来的倒退方向,Web Components 就是浏览器原生反对的组件化规范。应用 Web Components API,浏览器能够在不引入第三方代码的状况下实现组件化。

实战

当初咱们来创立一个 Web Components 按钮组件,点击它将会弹出一个音讯 Hello World!。点击这能够看到 DEMO 成果。

Custom elements(自定义元素)

浏览器提供了一个 customElements.define() 办法,容许咱们定义一个自定义元素和它的行为,而后在页面中应用。

class CustomButton extends HTMLElement {    constructor() {        // 必须首先调用 super办法         super()        // 元素的性能代码写在这里        const templateContent = document.getElementById('custom-button').content        const shadowRoot = this.attachShadow({ mode: 'open' })        shadowRoot.appendChild(templateContent.cloneNode(true))        shadowRoot.querySelector('button').onclick = () => {            alert('Hello World!')        }    }    connectedCallback() {        console.log('connected')    }}customElements.define('custom-button', CustomButton)

下面的代码应用 customElements.define() 办法注册了一个新的元素,并向其传递了元素的名称 custom-button、指定元素性能的类 CustomButton。而后咱们能够在页面中这样应用:

<custom-button></custom-button>

这个自定义元素继承自 HTMLElement(HTMLElement 接口示意所有的 HTML 元素),表明这个自定义元素具备 HTML 元素的个性。

应用 <template> 设置自定义元素内容

<template id="custom-button">    <button>自定义按钮</button>    <style>        button {            display: inline-block;            line-height: 1;            white-space: nowrap;            cursor: pointer;            text-align: center;            box-sizing: border-box;            outline: none;            margin: 0;            transition: .1s;            font-weight: 500;            padding: 12px 20px;            font-size: 14px;            border-radius: 4px;            color: #fff;            background-color: #409eff;            border-color: #409eff;            border: 0;        }        button:active {            background: #3a8ee6;            border-color: #3a8ee6;            color: #fff;        }      </style></template>

从下面的代码能够发现,咱们为这个自定义元素设置了内容 <button>自定义按钮</button> 以及款式,款式放在 <style> 标签里。能够说 <template> 其实就是一个 HTML 模板。

Shadow DOM(影子DOM)

设置了自定义元素的名称、内容以及款式,当初就差最初一步了:将内容、款式挂载到自定义元素上。

// 元素的性能代码写在这里const templateContent = document.getElementById('custom-button').contentconst shadowRoot = this.attachShadow({ mode: 'open' })shadowRoot.appendChild(templateContent.cloneNode(true))shadowRoot.querySelector('button').onclick = () => {    alert('Hello World!')}

元素的性能代码中有一个 attachShadow() 办法,它的作用是将影子 DOM 挂到自定义元素上。DOM 咱们晓得是什么意思,就是指页面元素。那“影子”是什么意思呢?“影子”的意思就是附加到自定义元素上的 DOM 性能是公有的,不会与页面其余元素发生冲突。

attachShadow() 办法还有一个参数 mode,它有两个值:

  1. open 代表能够从内部拜访影子 DOM。
  2. closed 代表不能够从内部拜访影子 DOM。
// open,返回 shadowRootdocument.querySelector('custom-button').shadowRoot// closed,返回 nulldocument.querySelector('custom-button').shadowRoot

生命周期

自定义元素有四个生命周期:

  1. connectedCallback: 当自定义元素第一次被连贯到文档 DOM 时被调用。
  2. disconnectedCallback: 当自定义元素与文档 DOM 断开连接时被调用。
  3. adoptedCallback: 当自定义元素被挪动到新文档时被调用。
  4. attributeChangedCallback: 当自定义元素的一个属性被减少、移除或更改时被调用。

生命周期在触发时会主动调用对应的回调函数,例如本次示例中就设置了 connectedCallback() 钩子。

最初附上残缺代码:

<!DOCTYPE html><html><head>    <meta charset="utf-8">    <title>Web Components</title></head><body>    <custom-button></custom-button>    <template id="custom-button">        <button>自定义按钮</button>        <style>            button {                display: inline-block;                line-height: 1;                white-space: nowrap;                cursor: pointer;                text-align: center;                box-sizing: border-box;                outline: none;                margin: 0;                transition: .1s;                font-weight: 500;                padding: 12px 20px;                font-size: 14px;                border-radius: 4px;                color: #fff;                background-color: #409eff;                border-color: #409eff;                border: 0;            }            button:active {                background: #3a8ee6;                border-color: #3a8ee6;                color: #fff;            }          </style>    </template>    <script>        class CustomButton extends HTMLElement {            constructor() {                // 必须首先调用 super办法                 super()                // 元素的性能代码写在这里                const templateContent = document.getElementById('custom-button').content                const shadowRoot = this.attachShadow({ mode: 'open' })                shadowRoot.appendChild(templateContent.cloneNode(true))                shadowRoot.querySelector('button').onclick = () => {                    alert('Hello World!')                }            }            connectedCallback() {                console.log('connected')            }        }        customElements.define('custom-button', CustomButton)    </script></body></html>

小结

用过 Vue 的同学可能会发现,Web Components 规范和 Vue 十分像。我预计 Vue 在设计时有参考过 Web Components(集体猜测,未考据)。

如果你想理解更多 Web Components 的信息,请参考 MDN 文档。

参考资料

  • 前端工程——根底篇
  • Web Components

带你入门前端工程 全文目录:

  1. 技术选型:如何进行技术选型?
  2. 对立标准:如何制订标准并利用工具保障标准被严格执行?
  3. 前端组件化:什么是模块化、组件化?
  4. 测试:如何写单元测试和 E2E(端到端) 测试?
  5. 构建工具:构建工具有哪些?都有哪些性能和劣势?
  6. 自动化部署:如何利用 Jenkins、Github Actions 自动化部署我的项目?
  7. 前端监控:解说前端监控原理及如何利用 sentry 对我的项目履行监控。
  8. 性能优化(一):如何检测网站性能?有哪些实用的性能优化规定?
  9. 性能优化(二):如何检测网站性能?有哪些实用的性能优化规定?
  10. 重构:为什么做重构?重构有哪些手法?
  11. 微服务:微服务是什么?如何搭建微服务项目?
  12. Severless:Severless 是什么?如何应用 Severless?