最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 带你入门前端工程(三):前端组件化

    正文概述 掘金(谭光志)   2021-01-26   388

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

    高内聚,低耦合

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

    仅看文字可能不太好理解,下面来看一个简单的示例。

    // math.js
    export function add(a, b) {
        return a + b
    }
    
    export function mul(a, b) {
        return a * b
    }
    
    // test.js
    import { 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.js
    export 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').content
    const 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,返回 shadowRoot
    document.querySelector('custom-button').shadowRoot
    // closed,返回 null
    document.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?

    起源地下载网 » 带你入门前端工程(三):前端组件化

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元