我和女友之间的鬼故事,告诉你框架与组件化的爱恨情仇

806 阅读19分钟

只要一提到组件化,我相信大多数同学,必然会想到 react, vue, angular... 这样的前端框架

然而我心中始终有这样一些疑问,是这些框架成就了组件化?还是组件化成就了这些框架?

原谅我,为了吸引大家进来,做了标题党。想看鬼故事的,请直接跳转到第二节

目录:

  1. 为啥要组件化
  2. 为啥不用框架
  3. 无框架 组件化 理论基础
  4. 无框架 组件化 实战经验

文章总字数 6000+,整体阅读时长 15 分钟,文章篇幅较长,干货在最后一节

为啥要组件化

我们都知道 HTML,CSS,JS 分别对应网页中的 结构样式行为

组件化能减少关注点

现在有个功能是:一个红色按钮,点击这个按钮输出alert-text 属性上的文案。

按照最原始的方式实现:

原始的方式,我在使用这个功能的时候,必须了解:

  1. css 文件路径 ./component/button.css
  2. js 文件路径 ./component/button.js
  3. 两个 css 选择器 btn, j_btn_alert (以及它们的命名规则)
  4. 标签名称 button
  5. 按钮类型 type="button"
  6. 文案属性名 data-alert-text="你点击了我"
  7. 按钮文案 click

按照组件化逻辑(这里以React为例)大概会是这样:

组件化的方式,我在使用这个功能,必须了解:

  1. 组件文件路径 ./Button/index.js
  2. 组件名称 Button
  3. 文案属性名 alertText="你点击了我"
  4. 按钮文案 click

可以很明显的看到,如果站在使用方的角度,组件化的关注点是比原始的方案少很多的。

并且我们还仅仅只是举了一个非常简单的按钮的例子。如果是一个复杂逻辑,这个差别就更大了。

原文链接

HTML JS CSS 相互制衡

在我眼里,其实写网页和建房子是一样一样的。每个页面则对应着每个房间,每个房间里面的元素又对应不同的模块

我们一个网站传统的架构和这个逻辑也是非常像的。

通常 js 和 css 会放到我们静态服务器中,所以会用一个 static 的文件来独立维护。

然后 views 中存放着我们的 html 或者 ejs 模版。

这些元素之间彼此通过 文件引用 以及 选择器 关联着。

如果我们想把桌子从 A 房间搬到 B 房间。我们不仅要搬桌子的 HTML,JS, CSS。

还得知道这个桌子它上面放了什么东西。比如桌子上面有一个台灯,那同时还得把台灯的 HTML,JS,CSS 也都搬到 B 房间。

并且桌子上有可能还放了很多其它乱七八糟的东西,这些逻辑关系,都得人工一个个找出来,对于习惯了组件化的开发的同学来说,简直就是噩梦

我们再来看看同样的比喻,我们在组件化的开发模式中是什么样的?

可以看到我们组件之间是通过接口关联的。

我在复用一个组件的时候,完全不需要关心其内部的逻辑实现(哪怕里面乱的不忍直视)。

有了组件,甚至都不用感知到 HTML,JS 和 CSS 本身。眼里一切都是组件,我们就只是在堆砌组件,以及对接他们的接口

而且删除一个组件是非常放心的,并不用担心我可能只是删掉了一个选择器,我的页面就崩溃的问题。

组件化省钱

组件化,最吸引我的点就在于它将所有逻辑都黑盒化了,不管它内部是好是坏,我作为使用方只需要做组件的搬运工就好。

要知道,组件搬运工这件事情,其实是不需要什么技术含量的。

在之前,我可能需要招 4 个高级工程师(并且这四个人还必须要对我们整站的架构,规范,了如指掌)。

有了组件化,我可能只需要 2 个高级工程师写组件,再顺带 2 个初级工程师做做搬运工。

到后期,组件库完善之后,我高级工程师完全可以去做其它项目,然后兼职做做这个项目的技术顾问

如果多个项目之间,技术架构相同,这些组件还可以直接拿来就用,这不知道省了多少人力。

当然,实际项目中远没有上述这么简单。这里只是按照 组件化 能更容易实现代码里面高内聚低耦合这个思路,来阐述,理论上组件化是可以省人力的。毕竟组件如何封装本身还是一门很大的学问。

为啥不用框架

在解释这个原因之前,我们先来听一个,你要过河找你女友的鬼故事

有一天,你和你女友约好周末去河边看星星,结果到了才发现,河上并没有桥。

然而你们并不是一起去的,只是约在了河边碰头,而此时恰好这个河将你和你的女友分割两方。

你女友要求你过河去找她,你要是做不到她就和你分手

在这焦急万分的时刻,你告诉自己不能慌,整个场面你要 hold 住。于是你仔细的观察河周围的情况。

  1. 河旁边恰好有一颗树,并且树上刚好有一根绳子,你可以抓着这个绳子荡过去;
  2. 河旁边有几块板子,你可以用这些拼一个大的板子,搭在桥上过河;
  3. 可能你本人就是木匠,你直接用这些零件造了简单可以称为桥的东西;
  4. 河旁边有几个造桥的工厂(为了成本他们像宜家一样只提供零件和运输)。你买回桥的零件,按照说明说可以直接拼成一座完整的桥;

这是个鬼故事?

为什么说是鬼故事呢?如果把这个女友看做产品经理过河这件事看作产品给你的项目需求。产品经理可能要求不只是你,而是你整个团队都要过河,并且可能就给你半天,过不来那就是你们团队技术不行,这一切听起来是不是像每天都会发生的鬼故事

这里纯属为了营造戏剧效果,产品大大我是爱你们的。

回到项目问题上,上面的四种状态其实就分别对应着我们解决问题的四种方法。

  1. 原始:管它怎样,先完成需求过去再说;
  2. 模块化:临时搭建了一些简陋的模块,但是能解决问题,并且别人也能用;
  3. 组件化:你很专业,封装了一个完整的组件,别人也可能安心的过去;
  4. 框架: 这个框架,能让没有什么专业的小伙伴,也能按照说明书,造出过河的桥;

原始

<>
 <绳子/> 
</>
< />

<script>
/**
 * 这个方法不要删,
 * 不要问我为什么要用这个骚操作过河
 * 可以去问产品为啥只给了半天时间完成需求
 */
const 过河 =()=> 树.绳子;
过河('荡');
</script>

原始的方案最大的优点就是快,可能 5 分钟你就完成了需求。然而,用绳子荡过去这件事,可能你团队里面并不是每个人都能做到。并且很有可能别人并不知道你这个绳子是用来过河的,还以为是什么冗余代码直接删掉。

模块化

<!-- 要注意这个木板拼接结构, 拼错了木板容易塌 -->
<>
  <木板3>
    <木板1 />
    <木板2 />
  </木板3>
</>

<script>
/**
 * 用的时候要小心,别掉下去
 */
const 过河 = ()=> 木板3.放到河上;
过河('小心的走过去');
</script>

相对于原始的方案就好了很多,对于使用方来说,只要了解拼装规则,和使用方法,就能过河。并且团队里面的人都可以过去。

不过这个难点就在于每个使用方都得明确知道规则和方法,一旦用错就容易掉河里。

组件化

importfrom "@/components/桥";

<河>
  <桥 方式="大步的走过去" />
</河>

上一节,我们其实有讲到非常多的组件化的优点。这里我只要知道桥怎么来,以及桥怎么用我就能很容易过河。当然造这个桥本身,是需要技术含量的,且每个人造桥的方式是不一样的。

框架

这里安利一下 张老师写的 《框架带来了什么?》 这篇文章

框架和组件化的使用姿势是一样的。然而,使用框架本身是需要的成本的。

需要选择: 比如旁边有两家工厂一个叫 React,一个叫 Vue 你怎么选?

学习成本: 比如你团队里面的人之前都没有接触过 React 和 Vue,就得从 0 开始学习;

不确定因素: 工厂随时可能被替代,今天可能这个工厂流行,明天就有可能是另外一个工厂;

排他性: 当我们选择了一个工厂之后,一切逻辑都得按照这个工厂的来。是很难在同一个项目中,同时使用两个工厂的设计理念的。

全盘接受:工厂也有好有坏,你接受一个工厂带给你的好处的同时,你同时也全盘接受了这个工厂的缺点;

当然,你们团队如果都是 facebook 的员工,那么选择使用 React 框架应该是又快又好的

无框架 组件化

现在组件化框架非常的流行。仿佛就给人一种错觉,现在前端只有 React 和 Vue等框架。

现实工作中很多项目我们接手都并不是从 0 开始的。有可能这个项目用了某种框架,也有可能这个项目什么框架也都没有使用。

不能是说因为我想要推进组件化,并且我很熟悉 React,我就给项目组说,你们的需求先停一停,给我半年时间,让我重构一下整站之前的代码。

无框架 组件化,不是框架不好,往往是项目为大的无奈之举

那现在怎么办呢?我们又想组件化,又不能用框架。此时我们就需要分析一下,框架在组件化这一层都做了哪些事情。

构建

.
├── static
│   ├── css
│   │   ├── common
│   │   │   └── global.css
│   │   ├── component
│   │   │   └── button.css
│   │   └── pages
│   │       ├── about.css
│   │       └── home.css
│   └── js
│       ├── common
│       │   └── global.js
│       ├── component
│       │   └── button.js
│       ├── pages
│       │   ├── about.js
│       │   └── home.js
│       └── plugins
│           └── jquery.js
└── views
    ├── about.html
    └── home.html

传统的基于文件的构建,仅仅只是对文件压缩打包合并处理。所以通常的做法是,一种类型的文件放到一个文件夹下,因为这边便于统一写处理逻辑。

但是这个方式需要人工建立文件之间的引用关系,这就回到之前我们提到的,将椅子从 A 房间搬到 B房间的问题,这不符合组件化内聚的逻辑。

.
└── src
    ├── components
    │   ├── Button
    │   │   ├── index.css
    │   │   ├── index.html
    │   │   └── index.js
    │   └── Global
    │       ├── index.css
    │       └── index.js
    ├── pages
    │   ├── about
    │   │   ├── index.css
    │   │   ├── index.html
    │   │   └── index.js
    │   └── home
    │       ├── index.css
    │       ├── index.html
    │       └── index.js
    └── plugin
        └── jquery.js

组件化比较推荐的是将一个完整功能内聚在一起。框架的做法是以 JS 文件作为组件的入口文件。并用这个入口 JS 文件来组件其内部依赖关系。这样组件之间的依赖就会比较的小了。

按照这种文件组织方式,如果还是基于文件的构建,实现起来难度就大大的提升了。

因为我们写组件的时候要内聚,但是我们在看到页面的时候,这些组件实际还是被拆开,平铺在了 HTML 中(通过文件引用和各种“钩子”联系起来),此时又要重新组织的逻辑,这个对于构建的依赖分析要求会非常的高。

所以如果你想做组件化,你需要让你的构建支持到 依赖分析。目前比较容易做到这件事的方案是 webpack

牛人,同样是可以用 Gulp 实现依赖分析

JS 优先

不管是 React,VUE,都是以 JS 为切入点,去描述一个组件以及组织这个组件内部,结构,样式,逻辑,的关系。

这个其实很好理解,HTML 擅长描述结构,CSS 擅长描述样式,而依赖关系以及逻辑关系,明显 JS 更胜一筹。并且,这个也更加的利于构建工具去做依赖分析

所以随着框架的流行,你会听到诸如 CSS in js, JSX(HTML in js)... 这样打破传统观念的思想方案杀出来了。

那传统方式又是怎样的呢?可能因为用户看到的页面是以 HTML 为入口的,所以传统的方案是以 HTML 优先 的方式处理的。

<!-- home.ejs -->
<html>
  <body>
    <h1><%= data.title %></h1>
    <p><%= data.description %></p>
  </body>
</html>
/** home.js **/
import ejsHome from "./home.ejs";
import EJS from "ejs";

const data={
  title:'首页',
  description:'这是首页'
};
const html = EJS.render(ejsHome, {data});

return html;
<!-- home.html -->
<html>
  <body>
    <h1>首页</h1>
    <p>这是首页</p>
  </body>
</html>

通常我们会用 HTML 模版引擎,去描述组件的结构。当拿到数据的时候,再借用模版引擎的 Render 方法去拿到实际需要渲染的 HTML。

这里使用 HTML 优先 这个方案,蹩脚的地方就出来了。因为 HTML 并不能做完所有的事情,即使 HTML 优先,我们仍然需要模版引擎的加持。拼接 HTML模版和数据这件事情,仍然需要 JS 参与。

明明 JS 一个人可以搞定的事情,非要让 HTML 抗起来,关键他又没扛住,还请来 JS 帮一把。不知道 HTML 或者是 JS 本人会作何感想?

所以想要做组件化,目前比较推荐的是使用 JS First 的方案。

同构实战

前面几节分别讲了,组件化的好处,为了推进组件化而选择框架的一些代价,以及想要实现,无框架,组件化的两个理论基础(构建加持,JS First)。这一节我们讲讲实战。

这里以我们 webnovel(需要科学上网) 为例,给大家介绍

  1. PC 站(传统jquery项目):www.webnovel.com
  2. M 站(React 框架):m.wenovel.com

这两个站点分别代表着,前面提到的传统派框架派

比较有趣的是,我们团队的小伙伴都会觉得 PC 站的开发体验相比 M 站要糟糕。但是从页面加载速度,SEO 和页面性能等技术角度来说,PC 站又明显胜过 M 站。

为了不捡芝麻丢西瓜。我们在传统和框架之间选择了尝试无框架组件化这个思路,当然还有一个原因是我们也不能将项目停下来整站重构。

在我们看来,只要能在 PC 站实现组件化,就能非常大的提升我们的开发体验

升级构建

当我们将构建从 gulp 升级到 webpack,对于开发角度最直观的体感是说,我们的文件结构变自由了。

当然这不仅是说我们可以组件化的去划分我们的文件夹,更可喜的是组件化的文件夹可以和原始的文件结构兼容。因为构建本身并不关心文件夹结构是怎样,只要依赖正确就不会有影响。组件化的方式存放文件,是为了让开发者更方便管理。

而这也就是说,我们可以渐进增强的去做网站的组件化。旧的模块和页面,就等它们在那里,岁月静好。新的页面,新的功能,我们组件化开发,开心。

当然,这其中还是有一些坑的。

比如,之前的代码是用 ES5 的语法写的,升级构建之后,可以用 ES6 了。虽然构建工具能完美兼容这两种语法,但是在使用 ES5 的老代码里,webpack 构建的 tree shaking 是不能用的。

JS First

当我们升级构建之后,现在就已经可以直接用 index.js 这个入口文件去引用 index.css_index.ejs 文件。然后使用方,只需要引用 index.js 然后初始化一下即可,这是不是离前面提到的组件化已经非常接近了?

前面已经提到的 JS First 的概念,简单的说就是让 HTML in JS,可以让组件更加的内聚。比如上图这个例子,我们直接可以用 JS 的字符串模版替换掉原始的 EJS HTML 模版引擎。

然而当我们正式在项目中用字符串模版替换 EJS 模版的时候,我们发现字符串模版终究还是字符串。用这种非结构化的语言去描述结构本身,看起来是非常吃力的(左图),终于明白了为啥描述结构是要用 HTML。

此时,不得不佩服 React,为了让 HTML in JS 发明的 JSX 这个语法糖(右图),可以在 JS 里以类似 HTML 的形式,结构化的描述组件之间的关系,完美的解决了这个痛点。

然而,说好的无框架,组件化,难道我们又要用 React?

JSX without React

介绍全新的 JSX 转换,跳转链接

于此同时,React 官网发来喜讯。简单的说,就是你想使用 JSX 并不需要 React ,只需要 Babel 即可。

本来在升级构建的时候,为了使用 ES6 的语法,我们就已经使用了 Babel。有种天助我也的感觉。

这时我们只需要在 Babel 的配置中添加 @babel/plugin-transfrom-react-jsx 这个 Babel 插件,即可将左边这样的 JSX 结构化语法,转成右边这样 function 语法。

等等!function 语法?不对啊,我要的不是这个 function ,我要的是 HTML 啊!此时 JSX 转 HTML 成为了我们新的难题。

我们想这样的问题估计早就有人做过了。然后我们就去 github 上找到了以上两个看起来最符合我们需求的方案。

然而,当我在他们的源码中看到了 document.createElement(tagName) 这行代码的时候,我整个人都蒙了。原来这两个方案都是运行时的(就是要在浏览器里面运行)。而我要的这个转换是在构件时。没法此时只能自己造轮子了。

jsx2string: 点我查看npm包

这里的 JSX,只是借用了 React 中的 JSX 的结构化语法,本身和 React 没有任何关系,自然也不能用 React 里面除开模版之外的所有逻辑。

简单梳理一下这整个流程,其实很简单。就是你在 JS 中写了 JSX 语法,Babel 在构建的时候会将其转换成 function 的形式,然后利用 jsx2string 执行就可以拿到你想要的 HTML 字符串。

服务端渲染

到这里非服务端渲染的逻辑,我们已经可以实现组件化开发。此时就差服务端渲染了。

我们目前 PC 站的 服务端渲染是 koa + EJS 的逻辑。又是熟悉的 HTML First 的味道。

以前是用 EJS 拼凑,HTML 模版和数据,得到渲染的 HTML。现在我们实现了 JS First,所以只需要将数据作为参数传递给 Render 方法,然后执行一下就能拿到 HTML。这里我们只需要写一个简单的 if else 逻辑,如果是.html 的文件我们就走原始逻辑,如果是 .js 文件我们就执行一下。这几行代码的改动,对整个对服务端的改造是非常小的。

.
└── Home
    ├── index.js          // 浏览器端 js 入口文件 
    └── index.node.js     // 服务端渲染的 js 文件

因为服务端渲染的 js 文件,只是需要拿到服务端数据之后,执行吐出 HTML 文件。所以它和浏览器端的 JS 要处理的事情是不完全一样的。所以在本地开发,我们实际是起了一个两个构建,一个是构建浏览器端需要的文件index.js, 一个是服务端渲染的构建 index.node.js

此时有一个比较难的点,就是在于 index.node.js 中需要拿到 index.js 所有的依赖关系放到 link 和 script 标签(webpack build 出来一个页面可能有多个 js 和 css 文件的依赖关系)中在浏览器中运行。并且 index.node.js 文件自身也是有依赖关系的。为了减少复杂度,我们默认让index.node.js 是不做依赖拆分的,全部都打包成一个文件(webpack 默认就是打包在一起的)。

new HtmlWebpackPlugin({
  inject: true,
  minify: false,
  filename: "./home/index.config.js",
  chunks: [ "Home/index" ],
  // 这个里面会直接包含 home/index.js 所有的依赖关系
  htmlPluginOption:({ htmlWebpackPlugin }) =>`module.exports = ${JSON.stringify(htmlWebpackPlugin)};`
});

这样我们只需要处理 index.js 依赖注入的处理。我们这里是用了一个比较 Hack 的方式拿到的。我们知道 webpack 插件 html-webpack-plugin 是可以根据模版生成 HTML 文件的,并且自动将 chunks 里的所有依赖都自动放进去。因为我们并不需要 HTML, 所以我们让 html-webpack-plugin 生成的是一个包含当前所有依赖关系的 js 文件。然后我们只需要在我们的 index.nodex.js 中拿到这个配置文件,然后将这些所有的依赖转换成 link 和 script 标签渲染即可。

.
└── Home
    ├── index.config.js   // index.js 之后所有的依赖文件(webpack build 自动生成)
    ├── index.js          // 浏览器端,js 入口文件 
    └── index.node.js     // 服务端渲染的 js 文件

所以我们是先启动 index.js 文件的构建,然后会分析出所有的依赖关系,并同时输出包含依赖关系的配置文件 index.config.js

index.nodex.js 的构建会打包成最后服务端渲染的一个单文件。然后在服务端渲染的时候,我们把 index.config.js 文件,同服务端请求的数据,一同注入到 index.nodex.js 然后运行输出 HTML 文件。

这虽然实现了我们想要的效果,但是总感觉有点丑陋,不知道小伙伴们有没有更好的建议。

结语

到这里我们算是将我们 PC 整站的组件化推进了一大步。值得一提的是,我们这个组件化是并没有附加任何框架的代价的。

因为我落点是在最后的项目组件化推进上。所以我刻意回避了,组件化本身的问题。比如之前提到的组件化封装本身也是一个复杂的学问。太内聚往往不够灵活,太灵活使用方需要关注的点就会变多。这还是需要基于团队能力找到一个平衡点的。

没有绝对好的方案,绝对坏的方案,一切都看是否适合

最后,为自己的标题党再次道歉。这篇文章多少能让大家有收获,那就算作我的道歉礼了。在这里也提前祝大家新年快乐。