Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

关于前端开发中“模块”和“组件”概念的思考 #21

Open
hax opened this issue Apr 8, 2015 · 29 comments
Open

关于前端开发中“模块”和“组件”概念的思考 #21

hax opened this issue Apr 8, 2015 · 29 comments

Comments

@hax
Copy link
Owner

hax commented Apr 8, 2015

本文基于与 @fouber 讨论时的记录整理。

术语的重要性

首要是澄清术语。同事平时交流的时候,有比较多的上下文信息是双方已经预先知道的,所以容易推断对方要表达的意思,一定程度的术语混淆关系不大。但是和其他人交流的时候,如果不明确术语的内涵和外延,经常变成鸡同鸭讲的状况。

举个例子来说,上次看到有位同学老是骂别人的文章里哪里哪里不对,进而演变为完全否定他人。我后来发现他对某些术语的理解有诸多“与众不同”,即他自己从概念和定义上就否定了别人。而有这样问题的人常常不自知,或者被指出其术语运用存在的问题仍坚持认为是别人概念错误,跟这样的同学交流起来就特别令人痛苦和恼火。

所以让我们先明确定义。

模块(对应英文“module”)

通常所指“模块”是指编程语言所提供的代码组织机制,利用此机制可将程序拆解为独立且通用的代码单元。

对于JavaScript来说,在ES6之前,并没有语言内置的模块机制,但我们用一些方式自制了某种模块机制,像CommonJS / AMD甚至建立了普遍接受的社区标准。虽然它们都是模块机制,但会有一些重大或微妙的差异。故当我们提到JS模块时,如果没有足够的上下文,有时需要明确是CommonJS module或AMD module或ES6 module。

对于CSS来说,并没有普遍接受的“CSS模块”概念。一个CSS样式表里可以通过@import来引入其他样式表,但我们通常并不称之为“模块”。多份样式表以cascade机制结合,这和我们一般编程语言中模块互相调用的方式相当不同。且CSS的@import语义基本上就是最简单的include,也就是将@import语句替换为导入样式表的内容。而编程语言中的导入模块会在当前作用域导入命名空间、符号等,比简单的include要复杂许多。

有关“CSS模块”的问题,我们后面还会讨论。

注:在Web标准中,“CSS module”其实指CSS spec本身的模块化。这也是我们应该避免采用“CSS模块”来指代CSS代码的组织结构的重要原因。

其实我公司里对“模块”的用法也比较随便。比如我们有/static/js/modules/目录,其实下面就是一些脚本,并没有采用任何一种module规范。再如我们有/src/modules/目录,下面每个子目录是业务模块,里面包含了view、controller和相关的各种类。

这里一个是历史因素——目录结构不是我建立的,大家习惯如此,都知道我们讲的“module”是指业务模块,跟具体编程语言里的module没有直接的关系,只要沟通没有什么障碍,那也不必改了。不过当我们完成引入JS module loader和相关设施之后,很可能还是需要重新调整文档和目录命名,以避免可能的理解错位。

回到关于“模块”的定义讨论上,我建议运用此术语时尽量避免扩张性解释——即避免在脱离特定机制的general的“模块化”的意义上使用“模块”这个词。

比如,传统的JS代码组织方法之一,是挂在global上的层级命名空间。此严格上不好称之为“模块”。原因是namespace只提供逻辑划分,不解决代码本身的划分。如果没有其他机制,代码划分仍然是文件为单位,并由开发者自己指定script加载。同理,我们通常认为C++里没有模块(尽管有namespace和include),但是PHP我们认为有模块(因为它有autoloader可以根据namespace映射到目录去加载文件)。

当然,即便编程语言没有模块,我们仍然可以通过一些方式进行“模块化”编程,但这种模糊的用法有可能造成误解。在JS这边因为我们已经有很成熟的CommonJS / AMD / ES6 module了,更应避免模糊用法。

组件(对应英文“component”)

另一个概念是“组件”。大体上“组件”和“模块”的概念是类似的,只是“组件”通常指更high-level的东西。

我个人体会,“模块”指代码单元,其意义偏向静态的代码结构。而“组件”指功能单元,其意义偏向运行时的结构,并有更复杂的控制(如组件实例的生命周期管理)。

举例来说,在组件系统中,你应该可以比较容易的做到在运行时查找某种组件并替换为另一种组件(热插拔)。而这通常并不作为模块系统的需求——即使模块系统支持动态加载,通常也不支持注销旧模块;即使支持注销旧模块,通常也不支持替换所有旧模块的引用(意味着需要重新 实例化/初始化 模块依赖树上所有直接或间接引用此模块的模块)。

注:的确有某Node.js平台下的游戏框架设计以class作为模块单元,通过替换prototype来做到模块的热插拔。不过这其实要求非常多的编程方式约定,实际上可被视为使用的是JS的一个裁剪的特性子集,因而不具有普遍性。

组件与模块的关系

网页本身导入脚本、样式表、图片、组件等,继而组件导入其自己所需的脚本、样式表、图片、其他组件之类。这样的组件机制比较符合我们对于网页构成的一贯认知。Web Components相关规范中的HTML Imports大体就是这么个东西。

注意,(Web Components的)组件机制跟(JS的)模块机制是正交的。

所谓正交,就是两者并不互相依赖对方的机制——至少目前是这样的。HTML Imports导入的作为组件的HTML文件里,引入脚本(目前)仍然用的是script标签,并不需要ES6 module。

但是我们有两个问题。

第一,实践中的组件方案不止Web Components一种。(现在的情况实际是大多数人还没有用上Web Components。)

其他组件框架绝大多数基于JS,它们的代码本身需要被加载,那就有一个模块机制的问题——因为组件框架通常都足够复杂,不太可能用裸脚本方式。既然怎么样都需要某种模块加载器,那么组件框架很可能直接利用模块加载器来加载asset。这样模块机制就变成了组件机制的基础了。

另一方面,组件框架如何定义组件呢?无论过去还是现在,看下来大多数组件框架就以一个class来定义一个组件。最常见的代码组织惯例是,一个class对应一个模块,于是组件就变成了符合某种模式(如继承自某基类)的JS模块。

我们比较一下。原生的Web Components方案,开发者需要在document里加link rel="import",然后引用的组件HTML文件里写script/style/link标签,script里声明自定义标签和相关组件行为。比起直接document里加载JS模块,然后在JS模块里import / require其他的JS模块 / HTML template / CSS样式表的方式,好像后者反而更简单点?对此我们稍后再讨论。

前面讨论“模块”定义问题时,我们讲过要避免扩张性解释。“组件”可以被称为“模块”(通常会加限定词以区别于普通的JS模块,如“UI模块”)的原因只在于组件本身以JS来表达,因而可以对应到一个具体的JS模块。假如组件本身并不以JS来表达——像Web Components的组件的形式是一个特殊的HTML文件,则称之为“XX模块”就是对“模块”的扩张性解释。就算是前一种情况,为了概念清晰和保持一致性,我仍然会建议用“组件”一词。

第二,回到Web Components规范,尽管组件机制和模块机制可以是正交的,但是实际情况是资源的依赖、加载、执行(应用)等是两者共性的问题。当前相关的各项标准在这点上其实还未协调,故而标准社区有讨论是否需要统一以及如何统一的问题,而Firefox也因此暂未实现HTML Imports。怎么样才对,我现在也还没想清楚,社区也还没有一致的意见。

通过JS Module Loader加载CSS等资源

HTML Imports使用和传统网页较为一致的模型。与此相对的,从历史到现在一直有以JS为中心的方案。

之前我们讨论过JS的模块。语法上以import "a"require("a")来引入其他模块。但是到底这里的"a"表示什么,如何加载,如何执行,是由具体的loader(及其hook/plugin)处理的。这里就提供了从JS module loader加载其他资源的可能。比如RequireJS、Sea.js、SystemJS均(通过插件)支持加载CSS。

我们是否可以把被加载的CSS资源叫做“CSS模块”?我觉得是有问题的。现有loader的这些插件的实现实际上只是简单的创建link[rel=stylesheet]元素插入到document中。这种行为和通常引入JS模块非常不同。引入另一个JS模块是为了调用它所提供的接口,但引入一个CSS却并不“调用”CSS。所以引入CSS本身对于JS程序来说并不存在“模块化”意义,纯粹只是表达了一种资源依赖——即本JS模块所要完成的功能还需要某些asset。

loader其实可以加载任何东西。如果看loader的另一些插件,如允许import "a.png"的图片资源插件,它只是起到preload作用。字体插件亦然。所以没有人称其为图片模块和字体模块,而只是称之为资源。

CSS介于图片/字体和JS之间。CSS像JS的地方是在于其复杂性,现代Web应用的CSS的复杂度已经有点接近编程了。但是从loader的角度,它更像图片/字体。

我们进一步仔细分析可以发现,JS模块对其他JS模块的依赖是一种强依赖——在依赖项加载和执行完后才能执行自己,而其对加载的CSS、图片等的依赖是一种弱依赖——我们只是表达额外需要某种资源,但是加载顺序甚至是否加载成功且应用完毕都可能是不重要的。

所以我们或许应该认为存在一个更高阶的组件(即使它直接以这个JS模块本身表达),它同时需要这些JS代码逻辑和一些CSS资源。另一方面,现有的使用JS module loader来加载CSS、图片等的实践也许存在滥用和误用的状况。

BTW,hixie有一份草案是通过needs等属性表示资源的依赖关系和优先级,其中包含了延迟加载或空闲时才加载等特性(均可视为弱依赖关系)。抛开声明性和不依赖JS的优势不说,基于JS module loader的方案能否优雅的支持弱依赖关系,是有很大的疑问的。(当然needs提案也面临跟HTML Imports和ES6 module一样的问题,其底层的依赖处理机制需要协调统一。)

注意:loader可以支持import a from "a.png"然后a返回一个HTMLImageElement对象,import b from "b.css"然后b返回一个CSSStyleSheet对象。这样导出一些可以被JS操作的对象似乎使其更像JS模块一样具有强依赖的特征,这也许是一种合理的用法。不过这时我们可以注意到另一个行为上的差异——image插件其实并没有把HTMLImageElement插入到document中,而按照通常CSS插件的意图,却需要把CSSStyleSheet对象插入到document.styleSheets中。这反映了CSS不同寻常之处——它直接是全局生效的,与“模块化”的要求是正好抵触的。我们后面还会详细讨论这一点。

此外,loader不会多次加载和执行(应用)相同CSS——这是module loader的要点之一。而CSS自己的@import语义则正好相反,多次引入相同URL的样式表,都会在导入位置上应用。使用JS module loader的import的语义和CSS自己的@import语义不一致,这也许是个问题。

CSS的@import也支持media query和supports condition等特性,这是目前的JS module loader插件不支持的(至少我没见过支持的)。带有media query的CSS@import声明会在运行时根据media query是否匹配而动态应用,也就是除了依赖关系以外,还有其他因素共同决定是否加载,这和前面谈到的弱依赖是类似的。

要基于JS module loader实现@import "a.css" (min-width:500px)的效果,可能得这样写:

matchMedia('min-width:500px').addEventListener(mediaQueryList => if (mediaQueryList.matches) System.import('a.css').then(() => ...));

或者

import a from 'a.css'
assert(a instanceof CSSStyleSheet)
a.media.appendMedium('min-width:500px')

前者实在难看,且其依赖关系已经不是声明性的了(从而相当麻烦)。后者则可能在还未加上条件时已经开始下载了(从而不满足需求)。

总结一下。JS module loader虽然可以被利用来加载各种资源,但本质上就是一个dependencies tree和注册在其上的一些纯粹由依赖来驱动的callback / promise。对于JS模块来说,这样的设计恰如其分,但是对于其他种类的资源来说——它们可能具有比单纯依赖(即强依赖)更复杂的如优先级、动态条件、可fallback等需求,直接把JS module loader用作组件系统的基础可能并非合适方案。其实就算是加载JS,对于polyfill / shim,loader系统都可能是要开外挂而不在标准机制内。

回顾之前讨论过的“模块”概念,我们可以增加一个认识:“模块”术语暗示了强依赖——因为编程语言的模块都是强依赖的——即使许多人没有明确意识到这一点。

CSS局域化问题

我们对于CSS当然也有分而治之的需求。但是简单用“模块化”来表述可能是有问题的。

如前所述,传统上,CSS被插入文档中,其包含的样式规则是文档全局有效的,这和模块化本身是相抵牾的。

当然我们可以通过某种开发规则来达到效果的局部化。比如以特定id/class限定所有CSS rule的应用范围。

另一种似乎更常见的方式是:所有rule本身就只包含class选择器。从某种角度上说,可被视为这个样式表定义(导出)了一些可复用的样式,并以class来命名。是否能称这样一个样式表为一个“CSS模块”?

当我们讲“A模块依赖B模块”的时候,其实暗含A要使用B所导出的接口的意思。假如我们认为“CSS模块”暴露的是class钩子,可是一个CSS模块依赖其他CSS模块并不存在需要调用它的class钩子的情况;覆写和扩展class钩子或可类比为某种接口使用,但实际运行时并没有任何约束,我们也很难进行静态检查(比如我们无从判断A的代码中有一个B所不包含的class名字是有意扩展还仅仅是拼写错误)。JS依赖CSS的情况也是类似的。

另一方面,这导出的class及其样式声明,也未被限定于只能被声明依赖者使用,其效果仍然是全局性的。

所以不建议管这样的东西叫“CSS模块”,这在沟通中很容易造成误解。(虽然公司内部沟通的话可能问题不大。)

题外话:这种方式实际上滥用了class属性。因为CSS没有复用机制,所以只好拿class属性来充数,通过class来作为应用样式的钩子。这违背了HTML规范和CSS规范的要求。除了对规范的实质性违背之外,这种方式在工程上的一个后果是,将内容和样式的耦合点从样式表的selector转移到了HTML文档的元素属性上。这对于页面开发流程、分工协作方式和长期可维护性会有巨大的影响。此外和通常认知的不同,这样的开发方式其实对页面性能有负面作用。具体就不展开了,可另行讨论。

组件框架在CSS这块的需求我认为“局域化样式”(scoped style)是比“CSS模块”更准确的称呼。目前的具体实现方案除了class样式钩子外,更靠谱的方式是:

  1. shadow dom天生样式就是局域化的
  2. style元素的scoped属性
  3. 以特定id/class限定单个样式表中CSS rule的应用范围,并配合css3增加的all属性和unset值来确保不被其他样式表污染。

前两者目前都有浏览器支持的问题。但第三种方式配合CSS预处理器是完全可行的。

特别是如果讲CSS预处理器,因为它们是真的可以以mixin、函数等来进行抽象,因此讲“SASS模块”、“Stylus模块”、基于预处理器的“样式库/框架”,倒是可以接受的。

补充

上述思考来自和云龙讨论的整理。

云龙想对“模块”进行扩张性解释的原因,可能在于当我们描述依赖树的时候,每个单元叫什么好。标准社区里目前还没确定怎么统一(ES6 module、HTML Imports、needs属性等规范的)依赖机制。我翻了一下es-discuss里hixie与tc39的成员们关于loader的讨论,发现在讨论到loader作为统一设施时,基本上module是特指ES6 module,而用来加载其他东西会称为non-module,大多数时候是直接说stylesheet、image、font或统称resource。而HTML Imports规范里,只有import link tree,并不包含style和script。也就是暂时找不到对完整的依赖树上的单元有统一的称谓。我觉得参照标准社区如何运用这些词汇会比较妥当。这意味着也许并不需要一定要找一个词汇来统称它。如果在讨论依赖树的时候我们需要统一称呼,称之为“依赖项”或许就足够了。

@fouber
Copy link

fouber commented Apr 8, 2015

@hax

这些整理对我的写作至关重要,给出了很多指导意见,真的非常非常感谢!

@liusong1111
Copy link

hax写的css局域化问题跟facebook的这个ppt说的很像:
https://speakerdeck.com/vjeux/react-css-in-js

他的库:
https://github.com/js-next/react-style

没有深入玩,呵呵

@tiye
Copy link

tiye commented Apr 9, 2015

@liusong1111
Copy link

@jiyinyiyong 我看了下css-layout,它的介绍说它是css的子集,专门做layout的,实现了flexbox云云,难道是专为移动端搞的?

这个跟"css局域化"的话题还不是一回事,很高兴在github上看到hax,希望能跟着学习。

@fouber
Copy link

fouber commented Apr 9, 2015

@jiyinyiyong @liusong1111

我和 @hax 的讨论,源自我要描述一种通用的前端模块化开发模型:

modular

如图,整个前端应用由 UI模块 组合而成,UI模块可以嵌套组合其他UI模块,此外UI模块还会依赖 JS模块CSS模块,我对三类模块的的定义是:

  • UI模块:也称UI组件,是页面展现和可交互单元,封装的是展现和交换,暴露的是属性、方法和事件。比如搜索模块(Search)、导航模块(Navigator)、页脚模块(Footer)等
  • JS模块:是JS实现的算法和数据单元,封装的是算法和数据,暴露的是接口。比如网络请模块(Ajax)、配置数据模块(Config)、一些常用函数模块(Utils)等
  • CSS模块:是CSS实现的样式单元,封装的是样式规则,暴露的是选择器。比如字体图标模块(IconFonts)、动画模块(Animate)、栅格系统(Grid)等

UI模块内维护自己独有的HTML、CSS和JS,如果依赖了其他CSS模块,可以在自己的CSS文件中声明依赖,JS如果依赖其他JS模块,也可以用JS的require类语法标记依赖,三种模块将组成一颗完整的依赖树,用统一的loader来以模块为单位加载资源(注意,这个loader其实可以在后端实现,模板引擎中)。所有前端应用都能被拆分成这样的模块系统来维护。

@hax 觉得三者使用的概念名称有些不妥,会引起一些误解,因为:

通常所指“模块”是指编程语言所提供的代码组织机制,利用此机制可将程序拆解为独立且通用的代码单元。

所以,@hax 的指导是:

  • UI模块应该称为 UI组件,因为这是业界标准称呼;
  • JS模块还是JS模块,因为模块是代码的单元,是编程语言所提供的代码组织机制;
  • CSS模块称为 局域化样式 一方面契合webcomponents的设计理念,另一方面因为CSS没有语言层面的模块机制,所以不属于模块的概念;

我很赞同 @hax 对技术词汇和概念的见解,只是写作上有些困难,要想办法克服一下

@switer
Copy link

switer commented Apr 10, 2015

@fouber 有个错别字:

CSS模块称为 局域化样式 一方面契合webcomponents的设计理念,另一方面因为CSS没有语音层面的模块机制,所以不属于模块的概念;

语音改为语言

@qushuangru
Copy link

复杂到看不懂是在说什么。(。・`ω´・)

@lifesinger
Copy link

云龙那张图,在 react 的全页面组件化的开发模式写,都是 component

webpack 里,Input 可以是 css、js、img 等,Output 统一为 js + png(图片),css 可以转换为 js,这样 css 资源就变成了一个用 js 表示的 css 模块

赞同 hax 的梳理,component 是功能单元,module 是存储单元(不一定是文件),这两者是正交的。从开发者的角度来看,接触的是 components,一个页面是一个 component,里面包含若干 component(有 ui 的,也有与 ui 不相关的,比如一个 PageComponent 可以由 HeadComponent + ContentComponent + EventCenterComponent 组成),这些 component 在 react 里最终以 js 代码呈现,这些 js (含转换成 js 的 css)代码的存储方式,涉及 module system,一个好的 module system,可以做到让上层组件开发者透明。

越来越认可 React 的全页面组件化的开发思路,用 component 的简单模型,解决了之前很多厘不清的概念。在 React 的架构下,感觉 Web Components 甚至有些狭隘了。

@xufei
Copy link

xufei commented Apr 10, 2015

本文的观点完全赞同。

@lifesinger 我也觉得Web Components狭隘了,在现在这个时代,除了带来样式的局部作用域,其他价值都已经不大了。

全组件化从管理上说是非常有利的,但实践阶段还是会有不少坑。我因为之前搞过几年Adobe Flex,所以对这里面的坑有不少认识,主要是并不能如预期那样减少维护工作量。另外,在最近这些年UI爆发性的多样化场景下,UI组件面临封装程度的两难选择。

封装过度的话,随意一点不一样的展示都导致无数配置,封装太少的话,又失去封装价值。所以我去年那次在携程讲MVVM时代的Web控件,重点就是谈的这方面的问题:xufei/ng-control#2

所以我支持的是UI层轻量化,模板化,不把你例子里面EventCenterComponent这类放到跟他们同一层级。

@hax
Copy link
Owner Author

hax commented Apr 10, 2015

@army8735 @ustccjw @chenjunxyf @lwbjing @OllivanderMe @barretlee @SAWSAWSAW

占位或mark可以点页面上的 subscribe 按钮。用于占位、单纯点赞等无营养的comment恕删。

@Justineo
Copy link

@liusong1111
css-layout 这个库用 JS 实现了 flex 布局,在 Web 上你可以看成是 CSS flex 布局的 shim,而我觉得把它结合在 React Native 这样的框架中用才是最有价值的地方。

@hax
避免 class 污染我觉得是很难的。有的时候为了复用样式而且避免歧义和冲突,难免为了样式而添加 class,哪怕这个类名是语义良好的。甚至有的时候得先有一套通用的样式再去按样式规定的类名和 DOM 结构去编写 HTML(保证多个页面甚至多个系统的样式一致)。说穿了「写干净的 HTML → 选择节点编写样式」这样的顺序不符合目前大多数开发场景,因为写 HTML 的时候脑中已经考虑到样式的钩子了(即使命名是语义合理的)。

关于 scoped CSS,我们现在的 SPA 系统都是在每个页面的 HTML 容器上带上唯一的 class 然后多一层嵌套来实现“scoped CSS”的,甚至在有多级业务模块时会带上所有 parent 模块的名字(其实用属性选择器就可以不用带 parent 了,属于历史遗留问题吧)。

比如一个用户列表的代码可能是这样:

<!-- system/user_list.tpl.html -->
<div class="system system-user-list">
  <!-- content -->
</div>
// system.less
.system {
  // styles
}

// system/user_list.less
.system-user-list {
  // styles
}

通过脚手架生成代码框架,保证开发者编辑的所有样式代码都会在某个「命名空间」下。

关于模块的入口未来究竟应该是 Web Components 还是 JS module,暂时不置可否。但 JS module 目前肯定是更切实际的方案。

@fouber
Copy link

fouber commented Apr 10, 2015

@lifesinger 在React的component描述中 CSS功能单元 也是一个组件么?

@sapjax
Copy link

sapjax commented Apr 10, 2015

不赞同“Web Components有些狭隘”的说法

“样式的局部作用域”只是Web Components为解决组件冲突/组件和页面冲突的一个组成部分
Web Components考虑的不仅仅是一个站点(项目)下的组件化,也考虑组件/页面和第三方的组件如何更好的共存、配合。

想象一下,浏览器对WEB新特性都支持非常好的时候,完全可以抛弃dom库,写原生代码,第三方的原生代码组件可以直接import进来用,不用担心和自己项目的组件/页面有冲突,或许可能用一种撘积木的方式来开发页面。

我对React不怎么了解,感觉传统的网站开发和MV*的WEB应用还是有很大区别的,不知道React是不是都能胜任?

但是相比把css转成js,把模板写在jsx中,Web Components处理组件自身的/组件和组件之间的依赖的方式显然更加的原始、直观、具有一致性,并且无需编译。

@fouber
Copy link

fouber commented Apr 10, 2015

各位不要跑题,组件和模块的具体实现方案不在本讨论之中,这个实现方案非常多样,有前端方案也有后端方案,前端方案又包括全部转移到JS中管理和由HTML Imports管理,后端方案设计思路是在模板引擎中实现一个模块化框架。

不讨论以上方案的优劣,三者都是一个目的,就是以模块/组件为单位组织前端系统,我想求证的是,这样一个组织形式中,各个单元的命名该怎么描述。

很明显,UI组件是必然存在的一种单元,无论是React还是WebComponents,但是UI组件之外,还有两类单元不可忽视,一类是JS模块,它们是算法和数据的单元,另外一类,安装hax的说法,叫“局域化样式”,我想讨论的是,这个“局域化样式”能不能称之为“CSS模块”

在组件化开发思想下,JS模块和“CSS模块”始终是不可忽视的存在。

我要举例的局域化样式案例最常见的有:

  • 栅格系统(grid)
  • 图标字体(icon-fonts)
  • 动画过渡(animate)

这些css单元封装的是 样式规则,暴露的是 选择器 “接口”

我们的UI组件极有可能会依赖这类CSS单元,在组件中使用它们暴露的选择器,比如最常见的字体图标库 font-awesome,我们可能在系统中各个UI组件的HTML内用到它,只要将UI组件和这个css建立依赖关系,就能复用了:

<!-- 顶部导航组件HTML结构 -->
<div class="header">
    <nav>
        <a href="/list"> <i class="fa fa-list">列表 </a>
        <a href="/comment"> <i class="fa fa-comment">聊天 </a>
        ...
    </nav>
</div>
/**
 * 顶部导航组件局域化样式,依赖font-awesome这个“CSS模块”
 * @require font-awesome
 */
.header { ... }
.header .fa {
    font-size: 30px; /* 覆写样式 */
}

当然,这类CSS单元在“导入”之后,它所暴露的选择器“接口”是直接暴露在全局环境下的,它不符合我们对“模块”的定义么?

我之所以想求解这个概念,是因为我想描述这种开发模型,许多年前我们已经在工程中这样实践了,但我一直都将三者统一描述为“模块”,这样我很容易向其他人阐述这是一个完全的模块化系统。但如果要是划分为其他概念去描述,这个系统的称呼就不那么好定义了,试想一下:

模块化开发 就是一种将整个前端应用拆解成 UI模块JS模块CSS模块 来维护的前端架构设计

对比:

XX化开发 就是一种将整个前端应用拆解成 UI组件JS模块局域化样式 来维护的前端架构设计。

这个XX我就很难给出一个大家都认可的称呼了。。。。

当然,如果都描述为“组件”更加不合理,因为 @hax 的说法我也很认同,组件是高级别的模块,有着生命周期的概念,而我想描述的概念中CSS和JS单元都不具备生命周期。

@zack-lin
Copy link

谈谈自己一拙劣的理解:
JS 模块的结束:算法和数据的单元;再抽象点理解,就是具有一定规则的代码单元;如果从这点考虑,叫 CSS 模块是没问题的。
但是我觉得 module 这东西,应该是相互不干扰的,有独立性的,而选择器的全局变量特点导致无法隔离多个 CSS 单元的相互影响,所以我认为不能叫 CSS 模块,也更不能叫局域化样式。 云龙举的三个例子,动画其实是有生命周期的,系统跟模块不等同吧,图标字体具有一定的独立性,所以叫模块还过得去

@sapjax
Copy link

sapjax commented Apr 10, 2015

@fouber 你所举的例子都是全局性的css,这和局域化的样式感觉概念正好反了,这种样式称为整站样式全局通用样式更多一点吧,称做css模块感觉也不太合适
@hax说的局域化样式是强调css的作用域,作用范围在UI组件的内部,组件下的css叫css模块当然是不合适的。

@hax
Copy link
Owner Author

hax commented Apr 10, 2015

@qiangspecial @dolymood @byphper @ColdXu
需要mark请点subscribe按钮。没有实质内容的comment恕删。

@xufei
Copy link

xufei commented Apr 10, 2015

@fouber css不应作为组件,只应作为资源,因为界面组件能体现界面功能,逻辑组件能体现逻辑功能,但是纯粹的一块css,它什么都不能表达,只有依附于html才能表达自己的含义。

在单页应用中,刚才 @Justineo 提到的那种,在组件容器上带一个样式名当识别码,这是一种方式,相对比较优雅点,但这里面还有更深的问题要解决。

当单页应用逐渐大型化,巨型化,逻辑功能和子界面必然都是动态加载了,他们所依赖的样式,在不同规模的产品中,有不同的加载策略:

  • 比较小型的产品,可以整个样式不切分,随首页载入,界面组件不带自己的样式
  • 中大型产品,可以在载入某个组件的同时,把它对应的样式,也就是刚才10说的那个识别码往下部分单独存放的样式文件载入

再继续优化,就变成了把这段样式都嵌入到界面组件中了,或者更彻底的inline化。所以,这个时候样式怎么还会是组件呢?他已经成为某个组件的一部分了。

所以这时候,构建的工作量就大了,意味着界面组件必须声明对某一块样式的依赖。这一块我一直没有想清楚怎么处理,所以之前写《Web应用的组件化(二)》的时候,只写了界面片段的包含关系需要管理,没有写界面片段对样式片段的依赖关系。

总的来说,在组件化体系中,除了人所共知的javascript模块间依赖,还包括:

  • 界面组件对样式块的依赖
  • 界面组件对逻辑的依赖

然后,还存在界面组件对界面组件的依赖(包含),至于说样式块之间是否还需要依赖管理,这个一直没有想明白。

虽然样式显得像组件,也需要管理一些依赖关系,但它只能算特殊资源,而不能算组件。

刚才 @fouber 提到的三个东西:

  • grid,这个应该是全局性的基础设施,不是某种组件。
  • 图标字体,在未来也不会是某种组件,只会是资源,他将来应该会被inline svg这样的东西替代,直接嵌入到某个html模板片段内部。
  • 动画过渡,这个比较纠结,但很大程度上还是依附于某个界面组件存在的,很少会用于描述多个东西……

@fouber
Copy link

fouber commented Apr 10, 2015

@xufei

最后指出的这些,说法可能不太准确:

  1. grid不仅仅是全局性的,每个组件内部都可能有自己的grid,它需要复用grid的样式
  2. 字体图标不能内嵌到html中,因为同一个图标会出现多次,在不同组件内都有可能被复用,也是不能每个组件都内嵌的
  3. 动画过渡也具备以上复用的可能

@VaJoy
Copy link

VaJoy commented Apr 10, 2015

css的模块化(不谈及组件),回到设计过程的层面来说,例如通过sass在一个sass文件中按需引入另一个sass文件,从而在多个sass页面中复用所引入sass中封装好的方法,IMO,兴许也算得上是样式模块化的一种形式,当然这种形式可能太小众了也不能解决大方向上的问题。

@cyjake
Copy link

cyjake commented Apr 10, 2015

比较赞同 @sapjax 的说法。

单纯的 CSS “模块化”没什么好说的,用 webpack 还是直接写 JS 对象字面量还是用其他预编译手段都能把 CSS 当成 JS 处理,各个模块的依赖关系也都好办。但是 CSS 级联 的特征决定就目前而言没办法很好地将其组件化,自己去 shim <style scoped> 代价可小不了。

我们通常考虑 class 或者 id 的唯一性,但别忘了组件嵌套的时候,父组件样式穿透到子组件,导致子组件自身样式不足以正常显示,还得写补丁代码的情况。所以 web component 这一套在我看来是必须的,而 CSS 代码写在哪里,只是见仁见智的东西。

@FrankFang
Copy link

有没有想过放弃 CSS 的『组件』/『模块』化。这样一来也许我们的 JS 组件也许更通用了。

@fouber
Copy link

fouber commented Apr 30, 2015

@FrankFang
没有css的『组件』/『模块』化,我们很难写出一个通用的“JS组件”

@FrankFang
Copy link

@fouber JS 组件 + 皮肤(可选),提供默认皮肤,但每个业务可以自己做皮肤,规则不限。这样呢?有些样式确是是必要的,比如 dialog 的 position: fixed,这样的直接内联就好了。
我主要是想把 CSS 从 JS 的模块中分离,让 CSS 和组织形式与 JS 的模块规则正交。毕竟,CSS太容易「反模块化」了。

@fouber
Copy link

fouber commented May 4, 2015

@FrankFang

组件化开发,第一目标是分治,第二目标才是复用。

我们划分组件更多的是为了把复杂系统拆分成独立的组件单元来开发和维护,以提高研发效率,降低维护成本。将复杂系统划分成组件来开发的时候,我们要解决组件的内聚问题,因为就近维护也是工程上的重要管理手段,所以才有了上述的讨论。

至于dialog这种可复用组件,我觉得例子举得不是很合理,毕竟你的系统不能完全由这种单元组织而成。可复用的组件为了可复用性,可以做CSS的内联,不过从维护的角度来说,更合理的做法是组件的css是独立维护的,只是在发布组件的时候将其内嵌到js而已。

关于CSS的【反模块化】是存在,但我觉得这个问题并没有想象中的那么严重,我举个非常常见的例子——Button,一般前端应用中,Button通常都是作为一种CSS样式被描述的,可以用一个.btn的className让一个a或者button元素展示为button效果,我觉得这种工程诉求并没有错,是非常合理的,只要把握得当,不要将css单元滥用为metacss,是没有什么问题的。如果彻底消除掉这种开发方式,我反倒觉得有些矫枉过正。

以下是我要描述的组件化开发体系:

site
  ├─ components
  │   ├─ nav
  │   ├─ btn
  │   │    └─ btn.css
  │   ├─ header
  │   │    ├─ header.js
  │   │    ├─ header.css
  │   │    └─ header.html
  │   └─ ...
  └─ pages
      ├─ index
      │    ├─ index.html
      │    ├─ index.css
      │    └─ index.js
      └─ ...

UI组件化、JS模块化、必要的CSS单元、UI组件的最外围容器(页面),以上这4项应该是前端组件化开发中不可避免的四种开发概念吧。

@hax
Copy link
Owner Author

hax commented Jan 6, 2016

最近 <script type=module>接近合并入html规范。也有新的标准提案尝试基于 ES2015 module 重新考虑 HTML imports: https://github.com/dglazkov/webcomponents/blob/html-modules/proposals/HTML-Imports-and-ES-Modules.md 。预计2016年年中我们可重新review这个话题。

@xuzhijun
Copy link

xuzhijun commented Feb 6, 2016

我的想法下,可能只有UI组件与JS模块,而UI组件内部则包含了CSS片段,其可以看作是UI组件的一个配置项来用。
然而,CSS片段内部是可以继承或者依赖于其他CSS片段。

@airen
Copy link

airen commented Feb 2, 2018

@hax 贺老,请教一下,如果将模块、组件和Atomic Design(原子设计)应该怎么更好的去结合理解,以及Vue中组件相关的概念,是否结合Atomic Design更易于理解。不知道您对这方面有何看法或建解。

@hax
Copy link
Owner Author

hax commented Feb 5, 2018

@airen 我对 Atomic Design 不太了解,但看上去这是一个设计方法论,超出了这里所讨论的纯技术范畴。多说一句,我对于采用比喻(如原子、分子、有机体……)作为基本概念的系统(无论是设计方法论还是技术体系或者其他什么东西)都抱有警惕。因为它规避了真正的下定义。你可以注意到本篇的很多篇幅都是在辨析定义和厘清概念。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests