5

前端工程化体系

 2 years ago
source link: https://roshanca.com/2017/front-end-engineering-system/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

前端工程化体系

何谓前端工程化?即根据业务特点,将前端开发流程规范化、标准化。它主要包含不同业务场景的技术选型、代码规范、构建发布方案等。主要目地是为了提升前端开发工程师的开发效率与代码质量,降低前后端联调的沟通成本,使得前后端工程师更加专注于自身擅长领域。

本人从业这几年有积累了一些产品和项目经验,对前端工程体系的设计有一些自己的见解:

  • 前端开发应该是“自成体系”的,包括运维布署、日志监控等
  • 针对不同的场景有不同的解决方案,并不是一套大而全的框架体系。比如针对以产品宣传展示为主的网页(Site),采用多页模式和响应式设计开发;以用户交互为主的且无强烈 SEO 要求的应用(Application),采用单页模式开发
  • 产品组件化,为提高复用性尽量将组件的颗粒度分细一些,且低耦合高内聚
  • 避免重复造轮子,引入一些优秀的开源资源,取长补短

根据以上思考,大致将自己理解的前端工程体系分为三大块:

  • Node 服务层:主要做数据的代理和 Mock,url 的路由分发,还有模板渲染
  • Web 应用开发层:主要专注 Web 交互体验
  • 前端运维层:构建布署、日志监控等

图1: 点击查看原图


Node 服务层

一般在 web 应用中,数据的来源分为两类:

  • 用户交互产生的 ajax 请求(客户端发起)
  • 服务端模板渲染所需初始数据

191209_50j4je49cc5hk95kf52c012kc46b6_1259x734.png 图2: 页面数据的两种来源

前者来说,传统的做法是后端直接提供接口给供客户端调用,但面临微服务化逐渐成为主流的今天,后端系统也趋于拆分为众多后端服务,提供不同的 api,直接调用面临请求认证和跨域等众多问题,Node 做为中转站利用 http-proxy 将 http 请求和响应传输于前后两端,起到桥梁作用。

有人说直接请求后端 api 岂不是性能更佳?跨域问题直接用 CORS(Cross-origin resource sharing)不是也能解决?

我们先来看跨域问题:首先 CORS 有兼容性问题(不支持 IE10 及以下),其次还有一些限制或者说是自身局限:

需要注意的是,如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。

针对性能问题,我拿了公司项目的 api 几个接口做了些测试,不是非常精确,但也能看个大概。分别在 No throtting 和 Regular 4G (模拟移动端网络)的模式下,用 proxy(http-proxy 代理请求),direct(直接请求后端地址)和 node(利用 request 模块发起请求)三种模式,做了 GET 和 POST 请求的延迟比对。在比对之前心里也大概有数,肯定是直接请求后端地址的延迟最低,毕竟加了 Node 一层,性能会由损耗。以下是测试结果:

191209_31020jdgacaida759g78jkl6dif32_1280x733.png No throtting GET

191209_03gk9h936b8d6keh3l0ljaa878j9i_1280x695.png No throtting POST

191209_5c016g94bl21df74l1hd9d49ejfj5_1280x726.png Regular 4G GET

191209_1dbf8cc9ec26517ihgc15jgc79e1b_1280x727.png Regular 4G POST

可以看出,GET 请求中 direct 是最快的,proxy 次之,node request 垫底,POST 则相差不明显。但延迟差距基本在 30ms 左右,是否可以接受,就要看自己的系统架构对性能的要求有多高了。对于我司目前的规模体量用户数来说,我认为基本是可以忽略的。

但为什么一定要用 Node 来做 http 请求的代理转发?我的理由:

  1. 前期前端开发过程中 mock 可与后期后端接口做平滑过渡,客户端可做到无感知。甚至还能在 mock 数据和真实数据之间来回切换,十分灵活
  2. 当后端服务增多时,特别是内部系统,跨域调用很成问题,Node 代理转发从根本上解决跨域问题,不用担心 Cookie 丢失等问题。而且在 Node 层针对 http 的请求和相应能做进一步拦截和根据业务需求定制这些二次加工,适用于更加广泛的场景

我在之前做公司的模拟炒股项目时,大概就是这样用滴:

账户系统 行情系统 交易系统 /api/account /api/stock /api/trade

var url = require('url');

// 账户系统 API
app.use('/api/account', proxy(config.api.accout, {
  forwardPath: function (req, res) {
    return url.parse(req.url).path;
  }
}));

// 股票行情 API
app.use('/api/stock', proxy(config.api.stock, {
  forwardPath: function (req, res) {
    return url.parse(req.url).path;
  }
}));

// 股票交易 API 
app.use('/api/trade', proxy(config.api.trade, {
  forwardPath: function (req, res) {
    return url.parse(req.url).path;
  }
}));

至于服务端渲染,之前一直是用 JSP or PHP 等后端模板来做。现在又多了一种选择,Node 本身就是个服务端,而且模板本身是属于视觉层和数据层的结合品。在前后端分离的比较合理的条件下,让前端工程师来写服务端模板更加适合,因为此时 Node 服务层应该不包含复杂的数据运算(CPU 密集型非 Node 擅长场景),这里的数据来源都是比较直接和清晰的,顶多要对这些数据做些业务加工处理而已。但对前端人员来说,了解业务逻辑是必备的,也是必需的,我们倡导的。

还有一点是因为下文中将要介绍的 Web 应用开发中,开发构建的工具本身就是基于 Node 的,构建后的静态资源如何在模板页面中引入,还是前端工程师最为清楚。所以虽模板属于服务端范畴,但与前端是结合的更加紧密的。

数据 Mock

一般来说,项目开发前期,后端接口实现的任务未完成时,前端写好了页面只能等待。此时,如果后端有空可以造一些假数据,让前端可以继续开发调试。但我们鼓励前端工程师自己 mock 数据,为什么呢?

  • 更好的了解业务
  • 做为数据接口的第一级消费者,前端应该更清楚怎样的数据结构,适合页面展示。比如某个字段,用数组好,还是用字符串拼接好等等
  • mock 效率更高,实现简单:json-servermockjs

但目前这两种简单的 mock 工具都不是太适合当前公司的项目场景,因为我们公司大部分接口都不是 restful 的,所以用下来遇到两个问题:

  • json-server 只支持 restful,而且只能生成 json 文件不够灵活
  • mockjs 是比较灵活高效,但做不到数据的持久化

比较完美的方案是两者结合再加上支持非 restful 的 mock,这个可能需要以后自己定制了。PS. 最近 github 上看到了一个以 service workers 为出发点创造出来的一个 mock 服务: service-mocker,不管是否适用,要为这样的思路点赞。

url 路由

前端来制定路由,也促使前端工程师更全面深入地了解业务。这属于设计范畴,需要经验加持。

因为具体路由下,基本就是业务逻辑 controller 了:

router.route('/:catalogId')
  .get(catalogController.findOne)
  .put(validate(paramSchema.updateCatalog), catalogController.update)
  .delete(catalogController.remove);

还有一些简单的路由,就是负责页面渲染而已(特别是单页应用,基本上走客户端路由的路子了):

app.get('/', (req, res) => {
  res.render('index', {
    ip: req.connection.remoteAddress
  });
});

服务端模板渲染

对于单页应用来说,服务端模板只是一个壳(shell)而已:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8"/>
    <title>Single Page Application</title>
    <script>window.serverData={foo: '来自服务端数据'}</script>
  </head>
  <body>
    <div id="app"></div>
    <script src="//cdn/file-5917b08e4c7569d461b1.js"></script>
  </body>
</html>

我们看到这里只通过 window.serverData 提供了简单的服务端数据。

对于多页应用来说,我们会引入 layout, include 等模板方案提取公共部分页面以达到最大复用:

./views
├── layout
│   ├── default.jade
│   ├── bootstrap.jade
│   └── ...
├── include
│   ├── topnav.jade
│   ├── header.jade
│   ├── footer.jade
│   └── ...
├── templates
│   └── ...
└── index.jade

上文已提到过 Node 来做服务端模板渲染的有一个好处就是方便与静态资源衔接,是如何做到的呢?首先 web 应用经过前端构建工具 build 之后会生成静态资源映射表:asset-manifest.json,串联起来大概是这样:

{
  "main.css": "static/css/main.ad87bbd6.css",
  "main.css.map": "static/css/main.ad87bbd6.css.map",
  "main.js": "static/js/main.a3907cec.js",
  "main.js.map": "static/js/main.a3907cec.js.map",
  "static/media/yay.jpg": "static/media/yay.44dd3333.jpg"
}
const manifest = require('./asset-manifest.json');

app.locals.assets = {
  mainCss: manifest['main.css'],
  mainJs: manifest['main.js'],
  ...
};
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8"/>
    <title>Single Page Application</title>
    <link rel="stylesheet" href="<%= assets.mainCss %>">
    <script>window.serverData={foo: '来自服务端数据'}</script>
  </head>
  <body>
    <div id="app"></div>
    <script src="<%= assets.mainJs %>"></script>
  </body>
</html>

Web 应用开发层

这一部分应该是前端工程师的专长部分,也是其核心价值的体现之处。

组件化和工程化

web 应用开发主要包含三个部分:html,css 和 js,三者之中,除了 js 在近几年才拥有了模块化机制以外,html 和 css 都是需要手工维护(难以维护),伴随着复制粘贴,时间一久其产生的代码冗余就像是 windows 上的 C 盘空间一样,不断发胖。

所以现代的 web 应用有这样的开发意识:everything in js is the better way. 正是 js 的模块化的可能性,成为了 web 功能模块(包含界面与交互)的组件化实现的基石。可以将 html 和 css 写在 js 之中(JSX),也可以以模板的形式它们三者写一起(Vue):

JSX 写法:

import React, { PropTypes } from 'react';
import { Profile } from '../components/Profile';
import styles from './Preview.css';

const Preview = ({url, user}) => {
  return (
    <div className={styles.card}>
      <img src={url} alt="" className={styles.normal}/>
      <Profile user={user} />
    </div>
  );
};

Preview.propTypes = {
  url: PropTypes.string,
  user: PropTypes.object
};

export default Preview;

Vue 的单文件形式:

<template>
  <div class="card">
    <img :src="url" alt="" class="normal">
    <Profile :user="user" />
  </div>
</template>

<script>
export default {
  name: 'card',
  props: {
    url: String,
    user: {
      type: Object,
      default: () => {}
    }
  }
}
</script>

<style>
.card {
  padding: 10px 15px;
  border: 1px solid #333;
}

.normal {
  color: #666;
}
</style>

无论哪种形式,组件都不能直接在浏览器中使用(将来的 Web Components 可以,但可构建的组件绝对是更加有优势)。如果我们将浏览器看作是 web 应用的 runtime 环境,对比于 jvm 是 java 的 runtime,那前端的组件代码都是 java 级别的源码,要类似 java 那样通过编译打包成 war 方可使用。

191209_7dkdhib588ke63cc76djac4h0c60h_1280x525.png

所以,为提升开发效率和代码质量,我们一般不直接写浏览器可读的 html, css 和 js,而是通过可模块化的“高级版” js,将一切资源进行串联,形成一系列业务或非业务组件,它们通过倚赖注入与模块输出、通过编译转换、通过合并或重组,最终生成浏览器可解析的代码。

如何更好的“串联”,这些都是工程问题,也是做为现代前端工程师的必备素质。

MDV = 模型驱动视图。从 2010 Google 推出 Angular 开始,到 2013 年 Facebook 推出 React,再到最近国人自创的小而美的 Vue,都一路贯穿着 web 开发新的思路:从手动操作 DOM数据绑定 + Virtual DOM 的转变。这也是 everything in js 极致体现:把 DOM 操作都干掉了,直接用基于 js plain object 的虚拟 DOM:

var a = React.createElement('a', {
  className: 'link',
  href: 'https://github.com/facebook/react'
}, 'React');

React 首创的虚拟 DOM 有两大杀手锏大大提升了原生 DOM 操作效率低下的问题(特别是在移动端):batching 和 diff。batching 把所有的 DOM 操作搜集起来,一次性提交给真实的 DOM;diff 算法时间复杂度也从标准的 diff 算法的 O(n^3 ) 降到了 O(n)1

除了性能提升之外,由于思路的转变,在开发效率上也有很大提升:MDV 模式下很自然的就将 web 页面看做是一台状态机(State Machine),UI = f(state), 界面上的变化皆由状态变化所导致,状态的变化来源一定为 M,即数据模型。我们将手动操作 DOM 的工作交给 MVVM 框架的数据绑定来做,界面的改变由数据的变化而自动完成,不仅十分高效,而且我们对于数据的流向更加清晰可控。在引入严格的函数式编程与不可变数据(immutable.js)后,还能使得结果可预测,方便做单元测试。

当然,除了许多新的概念需要学习之外,组件间如何通信,异步数据如何管理等一系列问题也是随之而来的一些挑战。

这里的轮子就是指组件。必须要根据公司的业务特点打造前端组件库,这里的指的组件,可以是无关业务的界面组件,也可能是功能模块,或者两者皆有。其目的就是为了方便复用,便于管理。涉及公司业务的我们定义为 private components,不涉及业务的我们如果做的优秀也可以开源,开源有什么好处?大家都懂的。

另外我们也拥抱业界优秀的开源方案,比如蚂蚁金服的 ant-design,谷歌的 Material-UI 等,在巨人的肩膀上,我们可以更高效更快速地开发我们的项目,迭代我们的产品。

多终端和跨平台

在这里要稍微强调一下我们的主战场也就是浏览器(可以看作是我们前端代码的运行环境)。可以想像的到,js 这个在短短 10 天之内创造出来的解释性语言之所以能够如此流行而且经久不衰,完全是托了浏览器这个全球数量最庞大分部最广的天然 js 解析器的福。它遍布世界上的每一个角落,你不得不用。所以了解语言宿主,当然也是至关重要的。

根据浏览器内核来分,前端应该关注的浏览器环境应该包括:

  • 移动端:
    • 微信 X5 内核
    • Android: Webview
    • iOS: WK, UIWebview
  • PC 端:
    • Trident(IE)
    • Webkit(Safari),Blink(Chrome)
    • Gecko(FireFox,已放弃)
    • Presto(Opera,已放弃)

可以看出来,最主要的战场,是 Webkit(移动端上的 Webkit 主要看系统 SDK)。在国内来说,由于微信的关系,我们还需要比较关注的是 X5(特别是近期的微信小程序推出)。

根据平台来分:

  • 移动端:Cordova,React Native,Weex
  • 桌面端:node-webkit(NW.js), Electron
  • 穿戴设备:WebVR

关于终端适配和跨平台的挑战,前者在于一些兼容性的问题,这个根据之前的工程化的方案,将 html, css, js 视作“编译”资源,在通过“编译工具”构建后,让工具自动地根据不同内核生成相应的适配代码,降低开发成本。后者对前端工程师的要求较高,需要复合性的知识和能力,但带来的收益巨大。值得一提的是 React 的目标宗旨就是:write once,run everywhere,它也正朝着这个方向在不断前进。

总体来说,Web 应用的开发主要技术特点就是(盗了某熊的全栈之路的图,哈哈):

191209_4l52ah044bfj23b251494f3l781kk_715x225.png

前端运维层

在代码 build 之前有两部操作要做:

  • 代码层面的 Lint
  • 功能层面的 Unit Test

Lint 是根据代码规范的规则来制定的,不同的场景有不同的规则。比如是 node 环境还是浏览器环境,采用的模块化机制是 commonjs 还是 amd 等,都要分别配置。这里主要是为了保障代码没有明显的错误和拥有一致的风格。

191209_4cf4a069e83j8g73l7hhi4bg24c20_1120x586.jpg

Unit Test 在以往只重界面不重交互(页面状态管理)的传统 web 项目下并不常见,但在一些稍微有点复杂程度的前端项目中,单元测试就显得比较重要了。我之前的公司对项目开发的速度要求比较高,所以这一块比较少做。不过目前一些主流的前端框架都附带测试库与简单例子,比如 create-react-app 就自带了 jest

CI/CD

前端的布署主要分静态和动态两部分:

  • 静态主要是指一些静态资源,布署也比较简单,就是往 CDN 服务器上放即可
  • 动态就是 Node 服务层的东西,关于 Node App 的布署,可参考我之前写的这篇文章

至于如何做到自动化以提升效率,主要利用 git hooks 通知到 CI 服务器,执行对应的脚本来实现。这里有许多方案可供选择。由于我在公司一直用的是 gitlab,所以之前只尝试过 gitlab-ci,它能与 Docker 相结合,用起来还是比较爽的。

其它比较常见的如 Jenkins, Travis CI 等。

扩展与稳定性

遇到程序 crash 怎么办?现在 Node 上已经有非常多的进程重启模块,比如 pm2,forever 等。

为了更好的利用 CPU 内核资源,我们还有在单台服务器上启用多个应用实例。大家知道,nginx 或 haproxy 等集群都是一主多从,主机的端口通过负载均衡算法,将请求转发到 slave 机器上。以上说的是多台机器的情形,那在一台机器上起多个实例,对这些实例进行集群管理也是一样的原理。

这里以 pm2 为例:

191209_7e13icblef0241jj9d2i0ki5bel5e_1280x442.png

然后我们手动制造一个崩溃,可以看到,可以看到第一个线程,restart显示为1,也就是说当崩溃的时候它会自动创建新的线程来继续服务。

191209_8g908a83kb22ak55j85il60db6g2f_1280x578.png

性能日志监控

监控可大可小,往小了说,可以是一个工具,往大了讲,也可以是一套系统,不仅仅可以有可视化的图表,还支持一系列的报警规则。如果公司的人力有限,估计投入产出比不高的,初期可以考虑一些优秀的在线服务,比如:keymetrics.io, Sentry.io 等,这些服务功能强大基本涵盖所有需求,而且易于集成。当然,好用的基本上都是需要付费的。

文中没有提到的 Private NPM 和 Private Docker Registry 我用虚线框起来,是就目前来看很难做到,因为我们的代码库是内网的,而且公司目前是阿里云的重度使用者,Docker 要如要投入使用,要是没有自身的 IAAS 基础设施做支持估计也是玩不转。当然,这些都是没几个小公司能玩转的,有机会就尝试,但做为技术人,首要任务还是方案落地和解决实际的业务问题。

  1. React 只会逐层对比两颗随机树,这大大降低了 diff 算法的复杂度。并且在 web 组件中很少会将节点移动到不同的层级,经常只会在同一层级中移动。 


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK