【QCon】精华内容上线92%,全面覆盖“人工智能+”的典型案例!>>> 了解详情
写点什么

QQ 音乐商业化 Web 团队:前端工程化实践总结(一)

  • 2019-10-31
  • 本文字数:6014 字

    阅读完需:约 20 分钟

QQ音乐商业化Web团队:前端工程化实践总结(一)

本文主要介绍在前端工程化的一些探索和实践,结合移动端的基础库重构和 UI 组件库开发这两个项目详细介绍工程化方案 。

随着业务的不断扩展,团队的项目越来越多,面对日益复杂的业务场景和代码逻辑,我们发现在前端工程化方面团队还有很多需要优化的地方。现有的解决方案已经无法满足各种复杂的场景,我们每天都在疲于应付很多重复的工作,为此我们基于移动端基础库重构和 UI 组件库的建设这两个项目对团队的项目构建流程进行了详细的分析和梳理,并制定了一套适用于团队的工程化方案。

浅谈前端工程化

前端工程化是一个非常广泛的议题,包含的技术和解决方案也是非常丰富的。一个前端工程的生命周期可以大致划分为这四个过程:



前端工程的生命周期


任何在这四个过程中应用的系统化、严格约束、可量化的方法都可以称之为工程化。工程化的程度越高,在工作中因人的个体差异性导致的缺陷或者短板就会越少,项目质量可以得到更有效的保障。对上面四个过程的工程化并不是完全分隔的,而是相辅相成,比如开发阶段的优化也会对测试、部署和维护产生很大的影响。


下面从模块化、组件化、规范化和自动化这四个方面进行具体介绍。

模块化

模块化可以对复杂逻辑进行有效分割,每个模块更关注自身的功能,模块内部的数据和实现是私有的,通过向外部暴露一些接口来实现各模块间的通信。开发阶段前端需要关注 JS、CSS 和 HTML,下面我们将分别对 JS、CSS、HTML 的模块化进行简单介绍。

1. JS 模块化

JS 模块化是一个逐渐演变的过程,开始的 namespace 概念实现了简单对象封装,约定私有属性使用_开头,到后来的 IIFE 模式,利用匿名函数闭包的原理解决模块的隔离与引用,下面介绍现在比较流行的几种模块化标准。

2. CommonJS

Nodejs 中的模块化方案,就是基于 CommonJS 规范实现的。一个文件就是一个模块,有自己的作用域,没有 export 的变量和方法都是私有的,不会污染全局作用域,模块的加载是运行时同步加载的。CommonJS 可以细分为 CommonJS1 和 CommonJS2,二者的模块导出方式不同,CommonJS2 兼容 CommonJS1,增加了 module.exports 的导出方式,现在一般所指的都是 CommonJS2。


  • 每个文件一个模块,有自己的作用域,不会污染全局;

  • 使用 require 同步加载依赖的其他模块,通过 module.exports 导出需要暴露的接口;

  • 多次 require 的同一模块只会在第一次加载时运行,并将运行结果缓存,后续直接读取缓存结果,如果需要重新执行,需要先清理缓存;

  • Nodejs 环境下可以直接运行,各个模块按引入顺序依次执行。


module.exports.add = function (a, b) {    return a + b;}
exports.add = function (a, b) { return a + b;}
const sum = require('sum');sum.add(1, 2);
复制代码


AMD


浏览器加载 js 文件需要进行网络请求,而网络请求的耗时是不可预期的,这使得 CommonJS 同步加载模块的机制在浏览器端并不适用,我们不能因为要加载某个模块 js 而一直阻塞浏览器继续执行下面的代码。AMD 规范则采用异步的方式加载模块,允许指定回调函数,这非常适合用于浏览器端的模块化场景。


  • 使用 define 定义一个模块,使用 require 加载模块;

  • 异步加载,可以并行请求依赖模块;

  • 原生 JavaScript 运行环境无法直接执行 AMD 规范的模块代码,需要引入第三方库支持,如 requirejs 等;


// 定义一个模块define(id ? , dependencies ? , factory); // 引用一个模块require([module], callback)
复制代码


CMD


类似于 AMD 规范,是应用在浏览器端的 JS 模块化方案,由 sea.js 提出,详见 https://www.zhihu.com/question/20351507


UMD


UMD 规范兼容 AMD 和 CommonJS,在浏览器和 Nodejs 中均可以运行。


(function (root, factory) {    if (typeof define === 'function' && define.amd) {                define(['jquery', 'underscore'], factory);    } else if (typeof exports === 'object') {                module.exports = factory(require('jquery'), require('underscore'));    } else {                root.returnExports = factory(root.jQuery, root._);    }}(this, function ($, _) {        function a() {};    function b() {};    function c() {};
return { b: b, c: c }}));
复制代码


ES6 Module


ES6 从语言标准的层面上实现了模块化,是 ECMA 提出的模块化标准,后续浏览器和 Nodejs 都宣布会原生支持,越来越受开发者青睐。


  • 使用 import 引入模块,export 导出模块;

  • 与 CommonJS 的执行时机不同,只是个只读引用,只会在真正调用的地方开始执行,而不是像 CommonJS 那样,在 require 的时候就会执行代码;

  • 支持度暂不完善,需要进行代码转换成上面介绍的某一种模块化规范。


在浏览器中可以通过下面的方式引入 es6 规范的模块 js:


<script type="module" src="foo.mjs"></script>
<script type="module" src="foo.mjs" defer></script>
复制代码


defer 和 async 不同,它会阻塞 DomContentLoaded 事件,每个模块 js 会根据引入的顺序依次执行。


随着更多浏览器对 ES6 的支持,现在有一些方案开始提出直接使用 ES2015+的代码在浏览器中直接执行来提高运行效果,这篇文章《Deploying ES2015+ Code in Production Today》中有详细的介绍,可以结合这份性能测试报告综合评估 ES6 在 node 以及各种浏览器环境下的执行效率对比。

3. CSS 模块化

CSS 自诞生以来,基本语法和核心机制一直没有本质上的变化,它的发展几乎全是表现力层面上的提升。不同于 JS,CSS 本身不具有高级编程属性,无法使用变量、运算、函数等,无法管理依赖,全局作用域使得在编写 CSS 样式的时候需要更多人工去处理优先级的问题,样式名还有压缩极限的问题,为此,出现了很多“编译工具”和“开发方案”为 CSS 赋予“编程能力”。


预处理器


随着页面越来越复杂,为了便于开发和维护,我们常常会将 CSS 文件进行切分,然后再将需要的文件进行合并。诸如 LESS、SASS、Stylus 等预处理器为 CSS 带来了编程能力,我们可以使用变量、运算、函数,@import 指令可以轻松合并文件。但各种预处理器并不能完全解决全局作用域的问题,需要结合 namespace 的思想去命名。


OOCSS & SMACSS


OOCSS 和 SMACSS 都是有关 css 的方法论。OOCSS(Object Oriented CSS)即面向对象的 CSS,旨在编写高可复用、低耦合和高扩展的 CSS 代码,有两个主要原则,它们都是用来规定应该把什么属性定义在什么样式类中。


  • Separate structure and skin(分离结构和主题)

  • Separate container and content(分离容器和内容)


SMACSS(Scalable and Modular Architecture for CSS)是可扩展模块化的 CSS,它的核心就是结构化 CSS 代码,则有三个主要规则:


  • Categorizing CSS Rules (CSS 分类规则):将 CSS 分成 Base、Layout、Module、State、Theme 这 5 类。

  • Naming Rules(命名规则):考虑用命名体现样式对应的类别,如 layout-这样的前缀。

  • Minimizing the Depth of Applicability(最小化适配深度):降低对特定 html 结构的依赖。


/* 依赖html结构,不提倡 */.sidebar ul h3 { }
/* 建议直接定义 */.sub-title { }
复制代码


BEM


BEM 是一种 CSS 命名规范,旨在解决样式名的全局冲突问题。BEM 是块(block)、元素(element)、修饰符(modifier)的简写,我们常用这三个实体开发组件。


  • 块(block):一种布局或者设计上的抽象,每一个块拥有一个命名空间(前缀)。

  • 元素(element):是.block 的后代,和块一起形成一个完整的实体。

  • 修饰符(modifier):代表一个块的状态,表示它持有的一个特定属性。


在选择器中,BEM 要求只使用类名,不允许使用 id,由以下三种符号来表示扩展的关系:


  • 中划线( - ) :仅作为连字符使用,表示某个块或者某个子元素的多单词之间的连接记号。

  • 双下划线( __ ):双下划线用来连接块和块的子元素。

  • 单下划线( _ ):单下划线用来描述一个块或者块的子元素的一种状态。


.type-block__element_modifier {}
复制代码


从上面 BEM 的命名要求可以看到,类名都很长,这就导致在对 CSS 文件进行压缩的时候,我们无法得到更大的优化空间。而且 BEM 仅仅是一种规范,需要团队中的开发者自行遵守,在可靠性上无法得到有效保障,而且还可能和第三方库的命名冲突。


CSS in JS


CSS in JS 是一种比较激进的方案,彻底抛弃了 CSS,完全使用 JS 来编写 CSS,又用起了行内样式(inline style),它的发展得益于 React 的出现,具体的原因可以参见组件化这部分内容。


  • 解决全局命名污染的问题;

  • 更贴近 Web 组件化的思想;

  • 可以在一些无法解析 CSS 的运行环境下执行,比如 React Native 等;

  • JS 赋予 CSS 更多的编程能力,实现了 CSS 和 JS 间的变量共享;

  • 支持 CSS 单元测试,提高 CSS 的安全性;

  • 原生 JS 编写 CSS 无法支持到很多特性,比如伪类、media query 等,需要引入额外的第三方库来支持,各种库的对比详见 css-in-js;

  • 有运行时损耗,性能比直接 class 要差一些;

  • 不容易 debug;


下面以 styled-components 为例:


import React from 'react';import styled from 'styled-components';
const Container = styled.div` text-align: center;`;

const App = () => ( <Container> It is a test! </Container>);
render(<App />, document.getElementById('content'));
复制代码


构建后的结果如下,我们发现不会再有.css 文件,一个.js 文件包含了组件相关的全部代码:


var _templateObject = _taggedTemplateLiteral(['\n  text-align: center;\n'], ['\n  text-align: center;\n']);
function _taggedTemplateLiteral(strings, raw) { return Object.freeze(Object.defineProperties(strings, { raw: { value: Object.freeze(raw) } }));}
var Container = _styledComponents2.default.div(_templateObject);
var App = function App() { return _react2.default.createElement( Container, null, 'It is a test!' );};
复制代码


CSS module


CSS module 则最大化地结合了现有 CSS 生态和 JS 模块化的能力,以前用于 CSS 的技术都可以继续使用。CSS module 最终会构建出两个文件:一个.css 文件和一个.js。


  • 解决全局命名污染的问题;

  • 默认是局部的,可以用:global 声明全局样式;

  • 受 CSS 的限制,只能一层嵌套,和 JS 无法共享变量;

  • 能支持现在所有的 CSS 技术。


以 webpack 为例,使用 css-loader 就可以实现 CSS module:


module.exports = {    ...    module: {        rules: [            ...            {                loader: 'css-loader',                options: {                    importLoaders: 1,                    modules: {                        localIdentName: "[name]__[local]--[hash:base64:5]"                    },
} } ... ] } ...}
复制代码


下面是一个组件开发的例子:


/* style.css */.color {    color: green;}
:local .className .subClass :global(.global-class-name) { color: blue;}/* component.js */import styles from './style.css';elem.outerHTML = `<h1 class=${styles.color}>It is a test title</h1>`;
复制代码


构建运行后生成的 dom 结构如下:


<h1 class="style__color--rUMvq">It is a test title</h1>
复制代码


component.js 中 styles 变量的值如下,我们看到声明成:global 的类名.global-class-name 没有被转换,具有全局作用域。


const styles = {    "color": "style__color--rUMvq",    "className": "style__className--3n_7c",    "subClass": "style__subClass--1lYnt"}
复制代码


说明:React 对样式如何定义并没有明确态度,无论是 BEM 规范,还是 CSS in JS 或者 CSS module 都是支持的,选择何种方案是开发者自行决定的。

组件化

最初,网页开发一般都会遵循一个原则”关注点分离”,各个技术只负责自己的领域,不能混合在一起,形成耦合。HTML 只负责结构,CSS 负责样式,JS 负责逻辑和交互,三者完全隔离,不提倡写行内样式(inline style)和行内脚本(inline script)。React 的出现打破了这种原则,它的考虑维度变成了一个组件,要求把组件相关的 HTML、CSS 和 JS 写在一起,这种思想可以很好地解决隔离的问题,每个组件相关的代码都在一起,便于维护和管理。


我们回想一下原有引用组件的步骤:


1.引入这个组件的 JS;


2.引入这个组件的样式 CSS(如果有);


3.在页面中引入这个组件的;


4.最后是编写初始化组件的代码。


这种引入方式很繁琐,一个组件的代码分布在多个文件里面,而且作用域暴露在全局,缺乏内聚性容易产生冲突。


组件化就是将页面进行模块拆分,将某一部分独立出来,多个组件可以自由组合形成一个更复杂的组件。组件将数据、视图和逻辑封装起来,仅仅暴露出需要的接口和属性,第三方可以完全黑盒调用,不需要去关注组件内部的实现,很大程度上降低了系统各个功能的耦合性,并且提高了功能内部的聚合性。

1.React、Vue、Angular…

React、Vue、Angular 等框架的流行推动了 Web 组件化的进程。它们都是数据驱动型,不同于 DOM 操作是碎片的命令式,它允许将两个组件通过声明式编程建立内在联系。


<!-- 数据驱动的声明式Declarative--><pagination     current={current} total={maxCount/20}     on-nav={this.nav(1)}></pagination>
<!-- DOM操作的命令式Imprective --><pagination id='pagination'></pagination><script>// 获取元素var pagination = document.querySelector('#pagination');
// 绑定事件pagination.addEventListener('pagination-nav', function(event){ ...})
// 设置属性$.ajax('/blogs').then(function( json ){ pagination.setAttribute('current', 0) pagination.setAttribute('total', json.length / 20)})</script>
复制代码


从上面的例子可以看到,声明式编程让组件更简单了,我们不需要去记住各种 DOM 相关的 API,这些全部交给框架来实现,开发者仅仅需要声明每个组件“想要画成什么样子”。


  • JSX vs 模板 DSL


React 使用 JSX,非常灵活,与 JS 的作用域一致。Vue、Angular 采用模板 DSL,可编程性受到限制,作用域和 JS 是隔离的,但也是这个缺点使得我们可以在构建期间对模板做更多的事情,比如静态分析、更好地代码检查、性能优化等等。二者都没有浏览器原生支持,需要经过 Transform 才能运行。

2.Web Component

Web Component 是 W3C 专门为组件化创建的标准,一些 Shadow DOM 等特性将彻底的、从浏览器的层面解决掉一些作用域的问题,而且写法一致,它有几个概念:


  • Custom Element: 带有特定行为且用户自命名的 HTML 元素,扩展 HTML 语义;


<x-foo>Custom Element</x-foo>
复制代码


/* 定义新元素 */var XFooProto = Object.create(HTMLElement.prototype);
// 生命周期相关XFooProto.readyCallback = function() { this.textContent = "I'm an x-foo!";};
// 设置 JS 方法XFooProto.foo = function() { alert('foo() called'); };
var XFoo = document.register('x-foo', { prototype: XFooProto });
// 创建元素var xFoo = document.createElement('x-foo');
复制代码


2019-10-31 16:371731

评论

发布
暂无评论
发现更多内容

【安全运维】小微企业的安全运维工具用哪款好?

行云管家

运维 安全运维

SpringBoot2.x系列教程——整合使用JPA

会踢球的程序源

Java

阿里内部 SpringCloud Alibaba(全彩版)开源,P8 大牛纯手打造

架构师之道

Java 微服务

【七千字】教你如何用MySQL分析查询语句Explain

会踢球的程序源

Java MySQL

开源正当时,共赢新未来 OpenHarmony开发者大会成功召开

科技汇

Alibaba最新神作!耗时182天肝出来1015页分布式全栈手册太香了

Java你猿哥

Java 分布式 SSM框架 分布式核心原理解析 分布式开发

测试工程师为什么要关注研发效能?

思码逸研发效能

软件工程 研发效能 测试工程师

互联网工程师Java面试八股文及答案整理(2023最新版)

会踢球的程序源

Java springboot java面试

Oracle 23c 新特性实操体验优质文章汇总

墨天轮

数据库 oracle sql 新版本/特性解读

3月寒窗!啃透美团保姆级分布式进阶技术手册,4月终入美团定L8

Java你猿哥

Java 分布式 SSM框架 分布式数据 分布式消息

关于ChatGPT,我们请小红书技术人和NLP专家聊聊原理和潜力

小红书技术REDtech

自然语言处理 openai ChatGPT

【FAQ】关于华为推送服务因营销消息频次管控导致服务通讯类消息下发失败的解决方案

HMS Core

HMS Core

多云转晴:Databend 的天空计算之路

Databend

小红书2024届实习生招聘一直在等着你!

小红书技术REDtech

招聘 实习 小红书

从「搭子」文化,看融云如何助力垂类社交应用增长

融云 RongCloud

融云 Z世代 通讯 交友 搭子

小红书社区反作弊探索与实践

小红书技术REDtech

防作弊 小红书

开屏广告=让用户等?小红书如何兼顾用户体验和广告投放效果

小红书技术REDtech

推荐 广告 小红书

准备2023金三银四的Java程序员注意:40+文档5000+页面试资料来啦

会踢球的程序源

Java java面试 面试资料 Java大厂面试

如何一招搞定PCB阻焊过孔问题?

华秋PCB

工具 电路 阻抗 PCB PCB设计

LED显示屏如何做到节能环保?

Dylan

经济 设备 LED显示屏

把脉分布式事务的模型、协议和方案

小小怪下士

Java 分布式 分布式事务 后端

python统计程序耗时 | python小知识

AIWeker

Python python小知识 三周年连更

字节跳动正式开源分布式训练调度框架 Primus

字节跳动开源

开源 算法 流批一体

我在 20 年的软件工程师生涯中学到的 20 件事

宇宙之一粟

翻译 软技能

揭秘云原生时代企业可观测体系落地实践

嘉为蓝鲸

云原生应用 云原生(Cloud Native) 可观测宇宙

Redis删除键命令: 新手用del,老手用unlink,有何区别?

Java你猿哥

Java redis SSM框架 Java工程师 delete

Unity 之 月签到累计签到代码实现(ScriptableObject应用 | DoTween入场动画)

陈言必行

Unity 三周年连更

90%的Java开发人员都会犯的5个错误

做梦都在改BUG

阿里全新推出:微服务突击手册,把所有操作都写出来了

Java你猿哥

微服务 微服务架构 Spring Cloud SSM框架

FastAPI 快速开发 Web API 项目: 连接 MySQL 数据库

宇宙之一粟

Python FastApi 三周年连更

小红书广告智能创意能力构建过程详解

小红书技术REDtech

人工智能 广告 小红书

QQ音乐商业化Web团队:前端工程化实践总结(一)_行业深度_sara_InfoQ精选文章