现在的前端领域, 随着JS框架, UI框架和各种库的丰富, 前端架构也变得十分的重要. 如果一个大型项目没有合理的前端架构设计, 那么前端代码可能因为不同的开发人员随意的引入各种库和UI框架, 导致代码量变得异常臃肿, 最终结果可能是代码变得无法维护, 页面性能低下,不得已只能推翻重构. 所以我们需要在项目开始前, 同样的需要对前端代码进行架构, 一旦前端架构师设计出所有前端开发人员都要遵循的检验机制, 建立起系统设计的规范, 那么项目就拥有了可以衡量代码质量的标准, 前端开发人员也能享受到更高效的工作流. 所以, 前端架构的定义可以用以下一句话来总结:
前端架构是一系列工具和流程的集合, 旨在提升前端代码的质量, 并实现高效, 可持续的工作流.
本系列的前端架构文章, 将分别围绕前端架构的四个核心展开, 分别是代码, 流程, 测试, 文档.
前端架构的四个核心
(一) 代码
归根到底, 所有的网站都是由一堆文本文件和资源文件组成的. 当我们面对制作网站所产生的大量代码时, 就会发现为代码和资源设定一个期望是多么重要. 在代码部分, 我们会专注于如果实现系统架构中的HTML, CSS, JavaScript.
(二) 流程
现在早已过了FTP上传文件的时代, 那么现在重要的是思考怎么用工具和流程构建一个高效且避免出错的工作流. 工作流变得越来越复杂, 那些用于它们的工具也同样如此. 这些工具在提高生产力, 加快效率和保持代码一致性上带来了惊人的效果, 但也伴随着过度工程化和抽象化的风险. 所以, 现有的工作流是需要改变的.
(三) 测试
要构建一个可扩展和可持续优化的系统, 必须保证新代码和老代码能够很好的兼容. 我们的代码不会独立存在, 它们都是大型系统中的一部分. 创建覆盖面广泛的测试方案, 能确保老代码还能正常运作.
(四) 文档
一般而言, 如果不是团队中的重要成员要离开, 我们几乎都不会意识到文档的重要性. 等到那个时候, 大家将不得不停下手头的工作, 优先编写所有的文档. 作为前端机构师, 你要善于在项目开发的同时编写良好的文档.
代码核心
(一) HTML
在前端的架构中, HTML作为页面的基础是十分重要的. 如果初始的HTML写得很烂, 将要写出很多不必要的CSS和JavaScript来弥补. 反之, 如果如果初始的HTML写得足够好, 就能写出根据可扩展性和可维护的CSS和JavsScript.
首先我们来看一些初级的前端工程师可能写出的HTML代码:
<div id="header" class="clearfix"> <div id="header-screen" class="clearfix"> <div id="header-inner" class="container-12 clearfix"> <div id="nav-header" role="navigation"> <div class="region" region-navigation> <div class="block block-system block-menu"> <div class="block-inner"> <div class="content"> <ul class="menu"> <li class="first leaf"> <a href="#">菜单1</a> </li> <li class="second leaf"> <a href="#">菜单2</a> </li> </ul> </div> </div> </div> </div> </div> </div> </div>
这类"div
乱炖"的代码, 是很多初级的前端为应付切页面的工作写出来的. 只是单纯为了还原psd图, 而完全你不考虑HTML的可读性和可维护性.
随后, 在HTML5之后, 标签的语义化受到了大家的重视, 采用语义化的标签, 不仅增加了代码的可读性, 也有利于SEO. HTML语义化标签的使用,这也是在前端架构中需要考虑到的,下面我们来看一下使用语言化标签写的这段代码:
<header><section><nav> <ul> <li> <a href="#"> 菜单1 </a> </li> <li> <a href="#"> 菜单2 </a> </li> </ul> </nav> </section> </header>
但是如果我们的页面的菜单有数10项的时候, 就会额外添加<li><a href="#">菜单N</a></li>
, 这类重复的工作量完全可以交给Mustache
这类模板引擎来解决, 已Vue中的模板引擎语法来写HTML, 会减少很多的工作量 :
<template><header><section> <nav> <ul> <li v-for="(item, index) in navList" :key="index"> <a href="#"> {item} </a> </li> </ul> </nav> </section> </header> </template> <script> export default { data() { navList:['菜单1','菜单2','菜单3','菜单4','菜单5','菜单6','菜单7','菜单8','菜单9','菜单10'] } } </script>
你也可以使用Handlebars, Jade, artTemplate各种模板引擎到你的项目中, 当然这些都是需要取决于前端架构师前期的所选择的技术选型. 做为前端架构师, 需要评估HTML产生的过程, 你对内容的顺序, 使用的元素和CSS类名有多大的控制权? 这些元素在将来改动起来会有多大难度? 模板的易用性? 你可以通过系统做出更改, 还是需要手动处理? 通过回答这些问题, 可能会颠覆你自己构建HTML和CSS的方法.
(二) CSS
构建CSS现在有很多成熟的方法, 例如使用新的命名空间, 扩充数据属性或在JavaScript里面定义CSS. 这些方法你可以从BootStrap, ElementUI这类UI框架中找到影子. 下面, 介绍3种比较常用的方法.
1.OOCSS方法(Object-Oriented CSS 面向对象的CSS)
<div class="toggle simple"><div class="toggle-control open"> <h1 class="toggle-title">标题</h1> </div> <div class="toggle-details open"> 详细内容 </div> </div>
上面这段代码就展示了如何使用OOCSS方法创建一个可切换的HTML代码, OOCSS有两个主要的原则:
- 分离结构和外观
- 分离容器和内容
分离结构和外观
这里的toggle
用来控制结构, simple
用来控制外观,这就是分离结构和外观的表现. 这样可以实现外观的复用, 例如当前的simple
皮肤使用直角, 而complex
皮肤可能使用圆角, 还加了阴影.
分离容器和内容
这里使用toggle-title
就是分离容器和内容的表现, 无论toggle-title
的容器是用的<h1>
还是<h2>
或者是<div>
, 一旦加上了toggle-title
这个类名, 那么该容器均已该类名所定义的样式呈现内容.
2.SMACSS方法(Scalable and Modular Architecture for CSS 模块化架构的可扩展的CSS)
<div class="toggle toggle-simple"><div class="toggle-control is-active"> <h2 class="toggle-title">标题2</h2> </div> <div class="toggle-detail is-active"> 详细内容 </div> </div>
上面的这段代码基本展示了如何使用SMACSS方法,在我个人的理解中, OOCSS更多的其实是提供了一种CSS构建思想, 该思想要求将结构和外观分离, 将容器和内容分离. 但是并没有提供一套完整的CSS构建规范, 而SMACSS是提供了一套样式系统, 该样式系统有5个具体类别:
- 基础: 如果不添加CSS类名, 标记会以什么外观呈现
- 布局: 把页面分成一些区域
- 模块: 设计中的模块化, 可复用的单元
- 状态: 描述在特定的状态或情况下, 模块或布局的的显示方法
- 主题: 一个可选的视觉外观层, 可以让你更换不同主题
基础
//base.css
body, form { margin: 0; padding: 0; } a { color: #039; } a:hover { color: #03F; }
在基础代码中, 应该规定的是页面中的一些通用样式,例如将body
的margin
和padding
设置为0 , 设置a
标签的颜色等. 类似于某些人常用的initial.css
文件.
布局
//layout.css
#header, #article, #footer { width: 960px; margin: auto; } #article { border: solid #CCC; border-width: 1px 0 0; }
这里的布局指的是页面中一些通用的布局组件, 例如头部, 侧边栏, 主体和底部这些. 这些布局组件会在多个页面通用, 所以最好把其放入到一个css文件中. 方便复用. 在SMACSS中, 推荐将布局容器的顶级标签设置为id
, 这样确保了每个页面中拥有唯一持有该样式的布局容器, 也方便其css和js选择器的使用. 当然, 你也可以使用一个唯一的类名替代id
.
模块
//module.css//module1
.module1 > h2 { padding: 5px; } .module1 span { padding: 5px; } //module2 .module2 > h2 { padding: 10px; } .module2 span { padding: 10px; }
模块是指页面中可以单独分离并提取出来复用的部分, 例如导航条, 侧边栏, 对话框或一些widget等. 所以, 模块禁止使用id
, 而应该采用类名的方式.
状态
<div id="header" class="is-collapsed"> <form> <div class="msg is-error"> There is an error! </div> <label for="searchbox" class="is-hidden">Search</label> <input type="search" id="searchbox"> </form> </div>
State 负责定义元素不同的状态下,所呈现的样式. 上面的一段代码中,已is-
开头的就是表示状态的类名, is-collapsed
, is-error
等类名不会单独使用, 而是和前面的布局和模块一起使用. 下面的代码, 就是在tab栏模块和状态一起使用:
//state.css
.tab {background-color: purple; color: white; } .is-tab-active { background-color: white; color: black; }
主题
// module-name.css
.mod {border: 1px solid; } //theme.css .mod { border-color: blue; }
这里的主题理解为皮肤更加合适, 已上面的代码为例, 在module-name.css
中定义了边框除颜色之外的样式, 在theme.css
文件中定义了该边框的颜色, 这样的好处就是, 如果定义其他颜色的类名去覆盖这些有颜色的样式, 那么就可以通过类名去切换皮肤的颜色. 达到更换主题的效果.
更多关于SMACSS的方法, 请参考: https://smacss.com/book
3.BEM方法(Block Element Modifier 块元素修饰符)
<div class="toggle toggle--simple"><div class="toggle__control toggle__control--active"> <h2 class="toggle__title">标题3</h2> </div> <div class="toggle__details toggle__details--active"> ... </div> ... </div>
BEM是由Yandex提出的给一个CSS命名方法, 该方法要求使用一个CSS类名, 尽可能使用以下三者组成:
- 块名: 所属组件的名称
- 元素: 元素在块里面的名称
- 修饰符: 任何与块或元素相关联的的修饰符
块名
这里的块名很多初学者会以为是inline-block
中的块, 其实这里的块名指的是一个独立的模块或组件. 例如一个<header>
可以用做一个模块, <header>
中的<nav>
可以用作一个模块. 模块之间是可以相互嵌套的. 上面的示例代码中 ,toggle
就是一个独立的模块
元素
元素是指无法用在其他块名中的部分, 在BEM方法中, 元素跟在块名后面使用__
连接, 之所以约定使用双下划线是因为方便在块名中使用单下划线命名. 上面示例代码中的toggle__control
, toggle__title
就是块名+元素的命名方式.
修饰符
修饰符与SMACSS中的状态类似, 在BEM方法中, 修饰符需要跟在元素后面使用--
连接. 有的人会觉得这种写法会使得代码冗余, SMACSS使用is-active
同样可以表示同样的作用, 为什么上面的代码要使用toggle__details--active
呢? 其实, 如果单独看open
和is-active
这两个名字, 我们并不知道它们的含义是什么, 但是当看到一个toggle__details--active
的类名, 我们就知道它是表示: 这个元素的名称是details
, 位置在toggle
组件里, 状态为active
.
(三) JavaScript
1.框架的选择
这里我不想陷入Angular, React, Vue三大框架之争. 我是一个Vue的开发者, 我深知MVVM框架给我们开发者带了极大的便利, 不用再以jQuery不停的操作DOM的形式去开发, 而是只关注数据的改变, 以数据去驱动DOM的改变. 这能够把更多的时间放入到业务逻辑的处理上.
就目前三大框架的生态系统来看, 大部分业务三大框架实现起来其实并没有什么大的差别,框架的选择更多的取决于项目中团队人员的偏好和学习成本. 比如Vue的学习成本就相比于Angular要小太多. 虽然我是一个Vue的开发者, 但我不得不说在React中使用JSX的语法让写代码变得很愉快.
这里我还想说的是: 其实你很可能不需要任何的框架!
有很多成功的网站只不过是采用了一些模板语法, 加上少量手动创建的Sass文件和几十个Javascript函数创建而成. 当项目的规模足够庞大, 需要牺牲代码文件体积大小去换取框架所带来的开发效率的提高时, 再考虑评估引入哪类JS框架和UI框架, 否则不要轻易放弃精简方案.
2.选择一套JavaScript代码规范
每个人写代码的方式是不同的, 有些人可能喜欢用==
, 但有的喜欢用===
; 有的人可能习惯给每个变量使用var
去声明, 但有的喜欢使用一个var
加逗号运算符去同时声明多个变量. 这些代码习惯可能并不会对程序运行造成影响. 但是在大型业务中, 面临多个开发者共同开发时, 如果没有一套代码规范, 那么就会出现代码难以维护, 难以阅读的情况. 为了让新加入的团队成员也能够快速熟悉相关的代码, 并且让代码可以维护, 一套Javascript代码规范不论是开发大型项目和小型项目, 都是必须的.
如果公司没有代码定制自己的代码规范, 可以使用大公司所制定的代码规范, 这里向大家推荐以下三个代码规范:
-
Airbnb JavaScript Style Guide
Airbnb的Javascript号称是"最合理的编写JavaScript代码的方式", 也是互联网中最流行的JavaScript代码规范, 它在Github上足有6万star, 几乎覆盖了JavaScript的每一项语言特性. -
Google JavaScript Style Guide
Google的JavaScript代码规范相比于Airbnb代码规范更加全面, 它不仅从代码美感,性能角度和代码特性对编写Js代码进行了规范, 同时也对Js的命名, 导入方式, Js代码文档进行了规范. 在Introduction中, Google团队表明, 在项目中全部使用了Google的Js规范, 才能被叫做Google Style的代码! -
JavaScript Standard Style Guide
standard JS是一个功能强大的 JavaScript 代码规范, 自带 linter 和自动代码纠正, 无需配置, 自动格式化代码. 可以在编码早期就发现代码中的低级错误. 这个代码规范被很多知名公司所采用, 比如 NPM、GitHub、mongoDB 等.
下面截取部分airbnb的ES5规范, 来对比一下使用了规范和未使用规范的区别:
数组
- 使用直接量创建数组
//bad
var items = new Array() //good var items = [];
- 拷贝数组时, 使用
slice
var len = items.length;
var itemsCopy = [];
var i;// bad for (i = 0; i < len; i++) { itemsCopy[i] = items[i]; } // good itemsCopy = items.slice();
- 使用
slice
将类数组对象转换成数组
function trigger() { var args = Array.prototype.slice.call(arguments); }
字符串
- 使用单引号
''
包裹字符串
//bad
var name = "LITANGHUI"//good var name = 'LITANGHUI'
- 程序化生成的字符串使用
join
连接而不是使用连接符。尤其是 IE 下
var items;
var messages;
var length;
var i; messages = [{ state: 'success', message: 'This one worked.' }, { state: 'success', message: 'This one worked as well.' }, { state: 'error', message: 'This one did not work.' }]; length = messages.length; // bad function inbox(messages) { items = '<ul>'; for (i = 0; i < length; i++) { items += '<li>' + messages[i].message + '</li>'; } return items + '</ul>'; } // good function inbox(messages) { items = []; for (i = 0; i < length; i++) { // use direct assignment in this case because we're micro-optimizing. items[i] = '<li>' + messages[i].message + '</li>'; } return '<ul>' + items.join('') + '</ul>'; }
比较运算符 & 等号
- 优先使用
===
和!==
而不是==
和!=
- 使用快捷方式
// bad
if (name !== '') {// ...stuff... } // good if (name) { // ...stuff... } // bad if (collection.length > 0) { // ...stuff... } // good if (collection.length) { // ...stuff... }
空白
- 使用 2 个空格作为缩进
// bad
function () { ∙∙∙∙var name; } // bad function () { ∙var name; } // good function () { ∙∙var name; }
- 大括号前放一个空格
// bad
function test(){ console.log('test'); } // good function test() { console.log('test'); } // bad dog.set('attr',{ age: '1 year', breed: 'Bernese Mountain Dog' }); // good dog.set('attr', { age: '1 year', breed: 'Bernese Mountain Dog' });
- 使用空格把运算符隔开
// bad
var x=y+5;// good var x = y + 5;