跳到主要内容

前端项目结构演进

· 阅读需 15 分钟
Jeii Zou

组件框架分析

前言

前端组件库一直是提升开发效率的利器. 尤其在React,Vue等前端UI框架大行其道以来, 各种层出不穷的UI组件库给前端工程师提供了各种各样的选择. 或者选择构建自己的组件库. 从框架结构方面来说, 这些年的演化令人眼花缭乱, 不知不觉已经2020年了. 回顾这些年, 前端框架的结构有哪些变化呢. 下面我列举和介绍一些典型的UI框架的项目结构来看看前端组件框架是如何设计的, 在这些年发生了什么样的变化.

对于UI库来说, 大致分成两种类型的UI框架, 不依赖前端框架的UI框架, 在早些年比较流行, 以及现在比较流行的依赖于各大前端框架的组件框架. (这两者在)

BootStrap

说到前端的组件库, BootStrap永远是绕不过去的一块路标. 早在jQuery的时代, Bootstrap就开始流行了. 在那个时候JqueryBootstrap几乎是前端必备的知识点.

到目前为止, Bootstrap的GithubStar已经高达139K之巨. 目前最新的版本是4.4.1, 其中组件的代码, 使用的工程工具都和过去的版本有了巨大的变化.

而今天主要的内容不会聚焦在组件和使用方式的变化上, 而是框架结构的设计和变化

我们来看Bootstrap3.x4.x发生的变化

好了, 我知道我截取的比较小, 不要仔细去对比了, 我提取几个最主要的变化给大家分析一下.

一般来说, 我阅读一个前端的开源代码的时候, package.json绝对是首选的文件. 而对于了解一个框架的从开发到部署的流程来说, 阅读脚本再配合依赖的查阅就差不多能知道个大概了.

所以首先可以来简单的对比一下两个版本之间的script命令:

emmm, 建议大家自己去代码里面看一看. 这种密度的script其实也算比较多的了. 这里的截图肯定是看不清楚了

左边的话, 其实主要的编译工具还是grunt, 而其运行的内容大概就是把src中的js文件都拼起来. 是的, 没有听错, 由于在3.x时代没有成熟import机制的存在, bootstrap虽然将每个组件的js代码都分割成不同的文件, 但是在最终的打包过程中, 做的仅仅是将文件简单的concat并且uglify

/* ========================================================================
* Bootstrap: affix.js v3.4.1
* https://getbootstrap.com/docs/3.4/javascript/#affix
* ========================================================================
* Copyright 2011-2019 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */


+function ($) {
'use strict';

// AFFIX CLASS DEFINITION
// ======================

var Affix = function (element, options) {

....

/* ========================================================================
* Bootstrap: alert.js v3.4.1
* https://getbootstrap.com/docs/3.4/javascript/#alerts
* ========================================================================
* Copyright 2011-2019 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */


+function ($) {
'use strict';

// ALERT CLASS DEFINITION
// ======================

var dismiss = '[data-dismiss="alert"]'
var Alert = function (el) {

....

最终的打包结果:

/*!
* Bootstrap v3.4.1 (https://getbootstrap.com/)
* Copyright 2011-2019 Twitter, Inc.
* Licensed under the MIT license
*/
if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");!function(t){"use strict";var e=jQuery.fn.jquery.split(" ")[0].split(".");if(e[0]<2&&e[1]<9||1==e[0]&&9==e[1]&&e[2]<1||3<e[0])throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher, but lower than version 4")}(),function(n){"use strict";n.fn.emulateTransitionEnd=function(t){var e=!1,i=this;n(this).one("bsTransitionEnd",function(){e=!0});return setTimeout(function(){e||n(i).trigger(n.support.transition.end)},t),this},n(function(){n.support.transition=function o(){var t=document.createElement("bootstrap"),e={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",

...

并且在3.X版本的时候, Bootstrap采用的还是less, 后来在4.x版本的时候更换为了sass.

那么4.x是如何处理代码的呢? 查阅4.4.1版本的package.json, 我们发现, bootstrap对js的处理工具换成了rollup:

"script":{
"js-compile-standalone": "rollup --environment BUNDLE:false --config build/rollup.config.js --sourcemap",
"js-compile-bundle": "rollup --environment BUNDLE:true --config build/rollup.config.js --sourcemap",
"js-compile-plugins": "node build/build-plugins.js",
"js-compile-plugins-coverage": "cross-env NODE_ENV=test node build/build-plugins.js",
}

相对来说rollup是一个比较主流的打包工具, 不比webpack的配置复杂, 比较适合用来打包一些工具型(library)的js库, 当然也可以用来打包一般的web项目. 特点是打包配置相对于webpack来说更加简单, 并且自动进行tree-shacking.

至于sass的打包, 我看到代码中只要还是依赖node-sass进行手动的打包:

"css-compile-main": "node-sass --output-style expanded --source-map true --source-map-contents true --precision 6 scss/ -o dist/css/ && npm run css-copy",

至于其他的命令其实没有那么重要, 主要是一些代码可视化和兼容性的优化以及测试的部分.

剩余应当说一说的, 应该是文档相关的操作.

Bootstrap通过jekyll这个静态站点生成器吧位于site中的文档编译成html

    "docs": "npm-run-all css-docs js-docs docs-compile docs-lint",
"docs-compile": "bundle exec jekyll build",
"docs-production": "cross-env JEKYLL_ENV=production npm run docs-compile",
"docs-netlify": "cross-env JEKYLL_ENV=netlify npm run docs-compile",
"docs-lint": "node build/vnu-jar.js",
"docs-serve": "bundle exec jekyll serve",
"docs-serve-only": "npm run docs-serve -- --skip-initial-build --no-watch",

所以, 最终bootstrap的处理流程大致是这样的:

大体上来说, 传统的UI组件库只需要控制jscss, 使用方式也是对特定的标签添加类名. 简单并且朴素.

实际上直到现在还是有很多网站依赖于这些传统的技术方案. 因为他们足够使用了.

Element 组件库

不过从到2014年以后, 情况发生了一些变化. facebook团队开源他们的内部工具react, 再后来, 尤雨溪退出了vue. 前端的世界发生了一点变化. (额外说一嘴, angular早在2009年就出现了, 不过一直没有在国内火起来).

那以后, 由于前端组件化概念的演进, UI框架的形态发生了变化.

新的形态, 叫做组件库, 相比UI框架来说, 组件库集成程度要更高一点, 包含了一些逻辑的处理. 在众多的UI框架中, Element是一个比较优秀的组件库. 并且对比ant design源码的阅读更加清晰(antd design把框架的操作封装到antd-tool中, 读起来不是很爽)

同样的, 我们先来看项目目录以及package.json文件,

Elemnet依赖于vue框架, 因此打包单位也是一个个的vue组件. 其组件目录在packages中, 每个组件都有一个index.js和一个src文件夹存放对应的vue模板文件.

还在2.2.2版本左右的时候, 部分组件还有package.json文件, 用于独立发布组件和版本控制. 后来可能处于实际的考虑, 把package.json文件都删除了.

在具体的编译过程上面, Element底层的打包工具已经是webpack了, 基本的逻辑是遍历package中的文件夹, 在src里面生成一个index.js文件, 然后调用配置好的webpack文件进行打包.

对于样式的处理上, element使用的是sass预编译语言, 在packages文件夹中有一个theme-chalk文件, 里面存放了所有组件的scss文件, 会用过npm调用build:theme, 激活gulp中的任务, 把scss文件编译到lib文件夹中.

对于编译产物, 一个是生成了一个包含全部组件的index.js和在theme文件夹中的index.css. 另一个是对每个组件都生成了独立样式文件和js入口文件.

Element 大概就是这样了, 可以看到整体的项目架构没有变化, 文档, 组件库, 测试. 而使用的工程化工具则发生了天翻地覆的变化. 当然这些工具并没有说哪个一定比哪个好. 也仅仅是技术选型上的不同.

下面我想介绍一种另一个风格的组件库

Material Component Design

好的, 现在我要介绍一个md设计的组件库: Material-Component-Web-Compoment. 这个库看起来和element的目录接口差不多. 不过不同的地方在他在每个组件的目录下面塞了一个package.json文件.

这些package.json文件实际上是一种多package.json组件的版本管理方案

这种方案依赖于lerna这个版本管理工具. 当然这个版本管理工具实际上历史也比较就远了

lerna这个工具主要可以帮助你解决组件之间相互依赖的问题, 并且可以自动的遍历文件夹下面的所有组件, 升级相关的依赖. lerna提供一个配置文件帮助你配置某些你需要的项目:

{
"version": "1.1.3",
"npmClient": "npm",
"command": {
"publish": {
"ignoreChanges": ["ignored-file", "*.md"],
"message": "chore(release): publish",
"registry": "https://npm.pkg.github.com"
},
"bootstrap": {
"ignore": "component-*",
"npmClientArgs": ["--no-package-lock"]
}
},
"packages": ["packages/*"]
}

lerna这个库的使用, 我这里就不具体的展开讲解了, 文档上都有. 回到md-web-component这个库来讲, 其和element的区别主要有两个, 一个是它是基于web-compnent实现的组件库, 另一个是它将所有的组件零散的发布到npm库上, 通过组织名称进行管理.

这是两种不同的模式. 一般来说, 现在主流的ant designelement是保持一致的, 并且根据我们的使用场景来说是比较合适的. 那么为什么md-web-component这个库会采用这种方式来管理不同的组件呢?

个人认为, 这和web-component这个基础技术可能有一定的关系.

我们知道web-component这是一项web标准基础技术. 和PWA一样, 他不是一个独立的技术栈, 而是一些标准组成的. 主要是由以下四个:

  • window.customElements: 自定义元素, 可以想vue/react那样自定义html的标签.
window.customElements.define('my-element', MyElement);

class MyElement extends HTMLElement {
constructor() {
super();
}

connectedCallback() {
// here the element has been inserted into the DOM
}
}

还提供了一些生命周期:

constructor -> attributeChangedCallback -> connectedCallback
  • shadowDOM: 提供不可见的HTML/CSS, 相当于组件, 本质上实现了htmlcss的组件化
<template id="userCardTemplate">
<img src="https://semantic-ui.com/images/avatar2/large/kristy.png" class="image">
<div class="container">
<p class="name">User Name</p>
<p class="email">yourmail@some-email.com</p>
<button class="button">Follow</button>
</div>
</template>

这是一种原生的, 真正意义上的组件化.

  • html imports: 可以再一个html文章中包含另一个html, 可以用来做外部内容的引入
  • template: 模板元素, 可以保存客户端的内容机制. 可以用来做动态DOM生成

这些特定, 决定了一个web-component组件可以独立的. 自由的在web网页中存在. 也就是说, web-component不同于react/vue这种框架下的组件, 需要一个runtime, 而是可以在原生环境中独立运行.

可惜, 直到今天, 浏览器的支持程度还不不够.

再回头来看md-component这个库的设计, 从我理解的角度就可以解释这种独立分发的设计了.

再说说其他

关于微前端

UI框架从单纯的css到框架的组件层面, 实际上集成度是越来越高的. 那更进一步是什么? 我想大概是微前端了.

不仅包含了完整的一个前端, 还包含了对应的后端逻辑. 当前, 代价就是通用性降低.

实际上, web-component这项技术很适合作为微前端体系的载体, 能够动态加载html, css和js, 当然就可以动态的加载一个完整的微前端.

这也是事实上的一种微前端的设计方案.

关于web技术

最后一小部分我提到了web-component, 这使得我不得不想到现有的react/vue这样的框架. react退出来有7年了. 而vue也差不多6年了.

未来前端技术的发展方向会在什么地方, 微前端还是移动端, 物联网还是人工智能.