2016- 我的前端之路
2016- 我的前端之路: 工具化与工程化从属于笔者的Web 前端入门与工程实践,本文承接自笔者去年的年度总结:2015- 我的前端之路: 数据流驱动的界面。另外如果对整体编程技术体系与思维感兴趣的推荐阅读另一篇盘点文章:2016- 我的技术之路:编程知识体系结构图 。
2016- 我的前端之路:工具化与工程化
二十载光辉岁月
近年来,随着浏览器性能的提升与移动互联网浪潮的汹涌而来,
纷扰之虹
笔者在前两天看到了 Thomas Fuchs 的一则
分久必合,合久必分啊,无论是前端开发中各个模块的分割还是所谓的前后端分离,都不能形式化的单纯按照语言或者模块来划分,还是需要兼顾功能,合理划分。笔者在
总结而言,目前前端工具化已经进入到了非常繁荣的时代,随之而来很多前端开发者也甚为苦恼,疲于学习。工具的变革会非常迅速,很多优秀的工具可能都只是历史长河中的一朵浪花,而蕴藏其中的工程化思维则会恒久长存。无论你现在使用的是
引言的最后,我还想提及一个词,算是今年我在前端领域看到的出镜率最高的一个单词:
工具化
月盈而亏,过犹不及。相信很多人都看过了
工具化的意义
工具化是有意义的。笔者在这里非常赞同尤雨溪:Vue 2.0,渐进式前端解决方案的思想,工具的存在是为了帮助我们应对复杂度,在技术选型的时候我们面临的抽象问题就是应用的复杂度与所使用的工具复杂度的对比。工具的复杂度是可以理解为是我们为了处理问题内在复杂度所做的投资。为什么叫投资?那是因为如果投的太少,就起不到规模的效应,不会有合理的回报。这就像创业公司拿风投,投多少是很重要的问题。如果要解决的问题本身是非常复杂的,那么你用一个过于简陋的工具应付它,就会遇到工具太弱而使得生产力受影响的问题。反之,是如果所要解决的问题并不复杂,但你却用了很复杂的框架,那么就相当于杀鸡用牛刀,会遇到工具复杂度所带来的副作用,不仅会失去工具本身所带来优势,还会增加各种问题,例如培训成本、上手成本,以及实际开发效率等。
笔者在MV*
到
工具化的不足:抽象漏洞定理
抽象漏洞定理是
谈到这里我们就会明白,不同的项目具备不同的内在复杂度,一刀切的方式评论工具的好坏与适用简直耍流氓,而且我们不能忽略项目开发人员的素质、客户或者产品经理的素质对于项目内在复杂度的影响。对于典型的小型活动页,譬如某个微信
React?Vue?Angular 2?
笔者最近翻译过几篇盘点文,发现很有趣的一点,若文中不提或没夸
小而美的视图层
函数式思维:抽象与直观
近年来随着应用业务逻辑的日益复杂与并发编程的大规模应用,函数式编程在前后端都大放异彩。软件开发领域有一句名言:可变的状态是万恶之源,函数式编程即是避免使用共享状态而避免了面向对象编程中的一些常见痛处。不过老实说笔者并不想一味的推崇函数式编程,在下文关于
<script>
export default {
components: {},
data() {
return {
notes: [],
};
},
created() {
this.fetchNotes();
},
methods: {
addNote(title, body, createdAt, flagged) {
return database('notes').insert({ title, body, created_at: createdAt, flagged });
},
};
</script>
<template>
<div class="app">
<header-menu
:addNote='addNote'
>
</div>
</template>
<style scoped>
.app {
width: 100%;
height: 100%;
postion: relative;
}
</style>
当我们将视角转回到
View = f(Data)
这种对用户界面的抽象方式确实令笔者耳目一新,这样我们对于界面的组合搭配就可以抽象为对于函数的组合,某个复杂的界面可以解构为数个不同的函数调用的组合变换。
很多人第一次学习
在现代浏览器中,对于
前后端分离与全栈: 技术与人
前后端分离与全栈并不是什么新鲜的名词,都曾引领一时风骚。五年前笔者初接触到前后端分离的思想与全栈工程师的定义时,感觉醍醐灌顶,当时的自我定位也是希望成为一名优秀的全栈工程师,不过现在想来当时的自己冠以这个名头更多的是为了给什么都了解一点但是都谈不上精通,碰到稍微深入点的问题就手足无措的自己的心理安慰罢了。
- 将原本由服务端负责的数据渲染工作交由前端进行,并且规定前端与服务端之间只能通过标准化协议进行通信。
- 组织架构上的分离,由早期的服务端开发人员顺手去写个界面转变为完整的前端团队构建工程化的前端架构。
前后端分离本质上是前端与后端适用不同的技术选型与项目架构,不过二者很多思想上也是可以融会贯通,譬如无论是响应式编程还是函数式编程等等思想在前后端皆有体现。而全栈则无论从技术还是组织架构的划分上似乎又回到了按照需求分割的状态。不过呢,我们必须要面对现实,很大程度的工程师并没有能力做到全栈,这一点不在于具体的代码技术,而是对于前后端各自的理解,对于系统业务逻辑的理解。如果我们分配给一个完整的业务块,同时,那么最终得到的是无数个碎片化相互独立的系统。
相辅相成的客户端渲染与服务端渲染
- Tradeoffs in server side and client side rendering
Roy Thomas Fielding博士的Architectural Styles andthe Design of Network-based Software Architectures
笔者在
上文描述的即是前后端分离思想的发展之路,而近两年来随着
-
对浏览器兼容性的提升,目前
React 、Angular、Vue 等现代Web 框架纷纷放弃了对于旧版本浏览器的支持,引入服务端渲染之后至少对于使用旧版本浏览器的用户能够提供更加友好的首屏展示,虽然后续功能依然不能使用。 -
对搜索引擎更加友好,客户端渲染意味着整体的渲染用脚本完成,这一点对于爬虫并不友好。虽然现代爬虫往往也会通过内置自动化浏览器等方式支持脚本执行,但是这样无形会加重很多爬虫服务器的负载,因此
Google 这样的大型搜索引擎在进行网页索引的时候还是依赖于文档本身。如果你希望提升在搜索引擎上的排行,让你的网站更方便地被搜索到,那么支持服务端渲染是个不错的选择。 -
整体加载速度与用户体验优化,在首屏渲染的时候,服务端渲染的性能是远快于客户端渲染的。不过在后续的页面响应更新与子视图渲染时,受限于网络带宽与重渲染的范畴,服务端渲染是会弱于客户端渲染。另外在服务端渲染的同时,我们也会在服务端抓取部分应用数据附加到文档中,在目前
HTTP/1.1 仍为主流的情况下可以减少客户端的请求连接数与时延,让用户更快地接触到所需要的应用数据。
总结而言,服务端渲染与客户端渲染是相辅相成的,在
项目中的全栈工程师: 技术全栈,需求隔离,合理分配
全栈工程师对于个人发展有很大的意义,对于实际的项目开发,特别是中小创公司中以进度为第一指挥棒的项目而言更具有非常积极的意义。但是全栈往往意味着一定的
今年年末的时候,不少技术交流平台上掀起了对于全栈工程师的声讨,以知乎上全栈工程师为什么会招黑这个讨论为例,大家对于全栈工程师的黑点主要在于:
- Leon-Ready:全栈工程师越来越难以存在,很多人不过滥竽充数。随着互联网的发展,为了应对不同的挑战,不同的方向都需要花费大量的时间精力解决问题,岗位细分是必然的。这么多年来每个方向的专家经验和技能的积累都不是白来的,人的精力和时间都是有限的,越往后发展,真正意义上的全栈越没机会出现了。
- 轮子哥:一个人追求全栈可以,那是他个人的自由。但是如果一个工作岗位追求全栈,然后还来鼓吹这种东西的话,那证明这个公司是不健康的、效率底下的。
现代经济发展的一个重要特征就是社会分工日益精细明确,想要成为无所不知的通才不过南柯一梦。不过在上面的声讨中我们也可以看出全栈工程师对于个人的发展是及其有意义的,它山之石,可以攻玉,融会贯通方能举一反三。笔者在自己的小团队中很提倡职位轮替,一般某个项目周期完成后会调换部分前后端工程师的位置,一方面是为了避免繁杂的事务性开发让大家过于疲惫。另一方面也是希望每个人都了解对方的工作,这样以后出
工程化
断断续续写到这里有点疲累了,本部分应该会是最重要的章节,不过再不写毕业论文估计就要被打死了
T ,T,笔者会在以后的文章中进行补充完善。
何谓工程化
技术需要分三步走,支持业务,驱动业务,再到引领业务。所谓工程化,即是面向某个产品需求的技术架构与项目组织,工程化的根本目标即是以尽可能快的速度实现可信赖的产品。尽可能短的时间包括开发速度、部署速度与重构速度,而可信赖又在于产品的可测试性、可变性以及
- 开发速度:开发速度是最为直观、明显的工程化衡量指标,也是其他部门与程序员、程序员之间的核心矛盾。绝大部分优秀的工程化方案首要解决的就是开发速度,不过笔者一直也会强调一句话,磨刀不误砍材工,我们在追寻局部速度最快的同时不能忽略整体最优,初期单纯的追求速度而带来的技术负债会为以后阶段造成不可弥补的损害。
- 部署速度:笔者在日常工作中,最长对测试或者产品经理说的一句话就是,我本地改好了,还没有推送到线上测试环境呢。在
DevOps 概念深入人心,各种CI 工具流行的今天,自动化编译与部署帮我们省去了很多的麻烦。但是部署速度仍然是不可忽视的重要衡量指标,特别是以NPM 为代表的难以捉摸的包管理工具与不知道什么时候会抽个风的服务器都会对我们的编译部署过程造成很大的威胁,往往项目依赖数目的增多、结构划分的混乱也会加大部署速度的不可控性。
- 重构速度:听产品经理说我们的需求又要变了,听技术
Leader 说最近又出了新的技术栈,甩现在的十万八千里。
- 可测试性:现在很多团队都会提倡测试驱动开发,这对于提升代码质量有非常重要的意义。而工程方案的选项也会对代码的可测试性造成很大的影响,可能没有无法测试的代码,但是我们要尽量减少代码的测试代价,鼓励程序员能够更加积极地主动地写测试代码。
- 可变性:程序员说:这个需求没法改啊!
Bug 的重现与定位:没有不出Bug 的程序,特别是在初期需求不明确的情况下,Bug 的出现是必然而无法避免的,优秀的工程化方案应该考虑如何能更快速地辅助程序员定位Bug 。
无论是前后端分离,还是后端流行的
-
功能的模块化与界面的组件化
-
统一的开发规范与代码样式风格,能够在遵循
SRP 单一职责原则的前提下以最少的代码实现所需要的功能,即保证合理的关注点分离。 -
代码的可测试性
-
方便共享的代码库与依赖管理工具
-
持续集成与部署
-
项目的线上质量保障
前端的工程化需求
当我们落地到前端时,笔者在历年的实践中感受到以下几个突出的问题:
- 前后端业务逻辑衔接:在前后端分离的情况下,前后端是各成体系与团队,那么前后端的沟通也就成了项目开发中的主要矛盾之一。前端在开发的时候往往是根据界面来划分模块,命名变量,而后端是习惯根据抽象的业务逻辑来划分模块,根据数据库定义来命名变量。最简单而是最常见的问题譬如二者可能对于同意义的变量命名不同,并且考虑到业务需求的经常变更,后台接口也会发生频繁变动。此时就需要前端能够建立专门的接口层对上屏蔽这种变化,保证界面层的稳定性。
- 多业务系统的组件复用:当我们面临新的开发需求,或者具有多个业务系统时,我们希望能够尽量复用已有代码,不仅是为了提高开发效率,还是为了能够保证公司内部应用风格的一致性。
- 多平台适配与代码复用:在移动化浪潮面前,我们的应用不仅需要考虑到
PC 端的支持,还需要考虑微信小程序、微信内H5 、WAP、ReactNative、Weex、Cordova 等等平台内的支持。这里我们希望能够尽量的复用代码来保证开发速度与重构速度,这里需要强调的是,笔者觉得移动端和PC 端本身是不同的设计风格,笔者不赞同过多的考虑所谓的响应式开发来复用界面组件,更多的应该是着眼于逻辑代码的复用,虽然这样不可避免的会影响效率。鱼与熊掌,不可兼得,这一点需要因地制宜,也是不能一概而论。
归纳到具体的技术点,我们可以得出如下衍化图:
声明式的渲染或者说可变的命令式操作是任何情况下都需要的,从以
var options = $("#options");
$.each(result, function() {
options.append($("<option />").val(this.id).text(this.name));
});
<div ng-repeat="item in items" ng-click="select(item)">{{item.name}}
</div>
目前
- 大型
Web 应用:业务功能极其复杂,使用Vue ,React,Angular 这种MVVM 的框架后,在开发过程中,组件必然越来越多,父子组件之间的通信,子组件之间的通信频率都会大大增加。如何管理这些组件之间的数据流动就会成为这类WebApp 的最大难点。 - Hybrid Web APP:矛盾点在于性能与用户验证等。
- 活动页面
- 游戏
MicroFrontend:微前端
微服务为构建可扩展、可维护的大规模服务集群带来的便利已是毋庸置疑,而现在随着前端应用复杂度的日渐提升,所谓的巨石型的前端应用也是层出不穷。而与服务端应用程序一样,大型笨重的
回归现实的前端开发计划
本文的最后一个部分着眼于笔者一年中实践规划出的前端开发计划,估计本文只是提纲挈领的说一下,未来会有专门的文章进行详细介绍。缘何称之为回归现实的前端开发计划?是因为笔者感觉遇见的最大的问题在于需求的不明确、接口的不稳定与开发人员素质的参差不齐。先不论技术层面,项目开发中我们在组织层面的希望能让每个参与的人无论水平高低都能最大限度的发挥其价值,每个人都会写组件,都会写实体类,但是他们不一定能写出合适的优质的代码。另一方面,好的架构都是衍化而来,不同的行业领域、应用场景、界面交互的需求都会引发架构的衍化。我们需要抱着开放的心态,不断地提取公共代码,保证合适的复用程度。同时也要避免过度抽象而带来的一系列问题。笔者提倡的团队合理搭配方式如下,这个更多的是面向于小型公司,人手不足,一个当两个用,恨不得所有人都是全栈:
声明式编程与数据流驱动:有得有失
渐进的状态管理
在不同的时间段做不同的事情,当我们在编写纯组件阶段,我们需要显式声明所有的状态
- 原型:Local State
这个阶段我们可能直接将数据获取的函数放置到
// component
<button onClick={() => store.users.push(user)} />
这里的
- 项目增长:External State
随着项目逐渐复杂化,我们需要寻找专门的状态管理工具来进行外部状态的管理了
// component
<button onClick={() => store.addUser(user)} />
// store
@action addUser = (user) => {
this.users.push(user);
}
这个时候你也可以直接在组件内部修改状态,即还是使用第一个阶段的代码风格,直接操作
// root file
import { useStrict } from 'mobx';
useStrict(true);
- 多人协作
/ 严格规范/ 复杂交互:Redux
随着项目体量进一步的增加与参与者的增加,这时候使用声明式的
// reducer
(state, action) => newState
渐进的前端架构
笔者心中的前端架构如下所示,这里分别按照项目的流程与不同的开发时间应该开发的模块进行说明:
解构设计稿
纯组件
在解构设计稿之后,我们需要总结出其中的纯组件,此时所谓的
实体类
实体类其实就是静态类型语言,从工程上的意义而言就是可以统一数据规范,笔者在上文中提及过康威定律,设计系统的组织,其产生的设计等同于组织之内、组织之间的沟通结构。实体类,再辅以类似于
//零件关联的图纸信息
models: [ModelEntity] = [];
cover: string = '';
/**
* @function 根据推导出的零件封面地址
*/
get cover() {
//判断是否存在图纸信息
if (this.models && this.models.length > 0 && this.models[0].image) {
return this.models[0].image;
}
return 'https://coding.net/u/hoteam/p/Cache/git/raw/master/2016/10/3/demo.png';
}
同时在实体基类中,我们还可以定义些常用方法
/**
* @function 所有实体类的基类,命名为EntityBase以防与DOM Core中的Entity重名
*/
export default class EntityBase {
//实体类名
name: string = "defaultName"; //默认构造函数,将数据添加到当前类中
constructor(data, self) {
//判断是否传入了self,如果为空则默认为当前值
self = self || this;
} // 过滤值为null undefined '' 的属性
filtration() {
const newObj = {};
for (let key in this) {
if (
this.hasOwnProperty(key) &&
this[key] !== null &&
this[key] !== void 0 &&
this[key] !== ""
) {
newObj[key] = this[key];
}
}
return newObj;
}
/**
* @function 仅仅将类中声明存在的属性复制进来
* @param data
*/
assignProperties(data = {}) {
let properties = Object.keys(this);
for (let key in data) {
if (properties.indexOf(key) > -1) {
this[[key]] = data[[key]];
}
}
}
/**
* @function 统一处理时间与日期对象
* @param data
*/
parseDateProperty(data) {
if (!data) {
return;
} //统一处理created_at、updated_at
if (data.created_at) {
if (data.created_at.date) {
data.created_at.date = parseStringToDate(data.created_at.date);
} else {
data.created_at = parseStringToDate(data.created_at);
}
}
if (data.updated_at) {
if (data.updated_at.date) {
data.updated_at.date = parseStringToDate(data.updated_at.date);
} else {
data.updated_at = parseStringToDate(data.updated_at);
}
}
if (data.completed_at) {
if (data.completed_at.date) {
data.completed_at.date = parseStringToDate(data.completed_at.date);
} else {
data.completed_at = parseStringToDate(data.completed_at);
}
}
if (data.expiration_at) {
if (data.expiration_at.date) {
data.expiration_at.date = parseStringToDate(data.expiration_at.date);
} else {
data.expiration_at = parseStringToDate(data.expiration_at);
}
}
}
/**
* @function 将类以JSON字符串形式输出
*/
toString() {
return JSON.stringify(Object.keys(this));
}
/**
* @function 生成随机数
* @return {string}
* @private
*/
_randomNumber() {
let result = "";
for (let i = 0; i < 6; i++) {
result += Math.floor(Math.random() * 10);
}
return result;
}
}
接口
接口主要是负责进行数据获取,同时接口层还有一个职责就是对上层屏蔽服务端接口细节,进行接口组装合并等。笔者主要是使用总结出的Fluent Fetcher,譬如我们要定义一个最常见的登录接口:
/**
* 通过邮箱或手机号登录
* @param account 邮箱或手机号
* @param password 密码
* @returns {UserEntity}
*/
async loginByAccount({account,password}){
let result = await this.post('/login',{
account,
password
});
return {
user: new UserEntity(result.user),
token: result.token
};
}
建议开发人员接口写好后,直接简单测试下:
let accountAPI = new AccountAPI(testUserToken);
accountAPI
.loginByAccount({ account: "wyk@1001hao.com", password: "1234567" })
.then((data) => {
console.log(data);
});
这里直接使用
容器/ 高阶组件
容器往往用于连接状态管理与纯组件,笔者挺喜欢
// @flow
import React, { Component, PropTypes } from 'react';
import { push } from 'react-router-redux';
import { connect } from 'react-redux';
/**
* 组件ContainerName,用于展示
*/
@connect(null, {
pushState: push,
})
export default class ContainerName extends Component {
static propTypes = {};
static defaultProps = {};
/**
* @function 默认构造函数
* @param props
*/
constructor(props) {
super(props);
}
/**
* @function 组件挂载完成回调
*/
componentDidMount() {
}
/**
* @function 默认渲染函数
*/
render() {
return <section className="">
</section>
}
}
服务端渲染与路由
服务端渲染与路由可以参考Webpack2-React-Redux-Boilerplate。
线上质量保障:前端之难,不在前端
前端开发完成并不意味着万事大吉,笔者在一份周报中写道,我们目前所谓的
线上质量保障,往往面对的是很多不可控因素,譬如公司邮件服务欠费而导致注册邮件无法发出等问题,笔者建立了frontend-guardian,希望在明年一年内予以完善:
- 实时反馈产品是否可用
- 如果不可用,即时通知维护人员
- 如果不可用,能够迅速辅助定位错误