关于前端工程化的一些思考

2,097 阅读8分钟

就目前的时间点来说(2018年),Web业务日益复杂化和多元化,多年以前在职业的细分领域中,主要还是以网页重构为主,也就是利用 HTML, CSS 将网页的布局结构以及整体视觉色彩实现出来,高级一点的可能会说,网页支持移动端的适配(媒体查询),在那个年代能把Flex写的很滑溜,就已经走在了大多数前端的前面。而今天开发模式早已从之前的 WebPage 转变成了 Web App,代码量,人数,协作也从之前的单打独斗到如今的团队协作。那么我们所谈论的复杂和多元,又是什么呢?

复杂和多元

如今的前端领域早已扩展到了服务端领域(Node.js),移动端领域(Hybrid 模式 & JS To Native 模式),桌面应用,各种浏览器的 Extension ,而 JS 引擎,除了 Google V8 之外还有 JavaScriptCore,微软的 ChakraCore 等等。代码量可能从以前的千行到如今的万行,甚至十万行。人数从一个人变成了N个一起协作开发,于是历史上的“我们”随着这些需求的增加,对JS的改造有了很多不同的方案,如:早期的 CoffeeScript 对JS的语法糖进行增强,AMD的模块化( require.js),打包工具(Grunt.js)等。业务上,我们越来越从中游的承上启下,变成了去承接从点到点业务的全面,我们会去思考这些复杂和多元的场景,而产生的问题,如:

  • 如何进行高效的多人协作
  • 如何保证项目的可维护性
  • 如何提高项目的开发质量

工程化要素

在软件工程领域,一个很标准的软件工程,它会有很多组织设计。而前端我想,它也应该有它自己的组织设计,如:

  • 生态丰富的 Web 框架库
  • 静态类型系统
  • 代码的模块化和 NPM 生态
  • 代码的规范化
  • 编辑器的规范化
  • 业务的规范化
  • 业务的组件化
  • 自动化测试
  • 工具的构建

站在巨人的肩膀上为我们添砖加瓦

我所思考的工程化都会围绕着 TypeScript 展开,选择它的原因很简单:“它足够强大和成熟”。

生态丰富的 Web 框架库

我选择了 React 而不是 Vue,React 赋予了 Web应用 一种全新的开发概念: Web Component ,我们在设计一个 Web应用时,我们可以很灵活的从组件的角度去设计,通过组装不同的组件来拼接页面。

React 丰富的生态体系中,给予了我们足够的选择,当我们的业务场景是简单时,我们可以只使用 React 做为 View,如:

当你的业务场景足够复杂时,你就可以选择 Route 和 Redux 去管理你们的数据流和路由,如 Redux :

如果我们真的需要,我们可以想办法把我们写的组件转换成 React Native 给予移动端中的 Web App 等同 Native的体验,甚至转换成微信小程序(这些转换都是依赖于一个优秀的工具 Webpack 和 Virtual DOM 这样的程序设计)。

静态类型系统

对于松散的 JavaScript 在大型系统中的应用,有着非常不友好的体验,错误可能无法提示,参数的传入和传出没有显示的标明(要么去问人,要么自己打印再处理),以及很让我不爽的 JS 写法千奇百怪。对于这一块,我们选择了微软的 TypeScript 做为我们前端的基石,我们的工程化也是围绕着 TypeScript 去展开,TS给了我们既有 JS 的既视感又有强类型的约束,对于保证工程质量有着很大的帮助。

代码的模块化和 NPM 生态

自从 es015 制定了模块化的标准后,基本上前端的模块实现都是遵从着 es module 的标准,只不过我们的工具在构建阶段为了优雅降级,而转译成了通过 Object.defineProperty(exports, "__esModule", { value: true }); 来定义的 commonjs 格式,在 TS 中模块化的定义与此基本相同。有了模块化在大型工程中,我们可以对代码进行隔离封装,去有序的组织我们的代码。

// 如果模块 使用了 export default 导出
  
  
import * as RT from 'r';
  
  
const R = RT.default;
  
  
// 如果模块 使用了 module.exports 导出
  
  
import * as RT from 'r';
  
  
// RT = module.exports
  
  
// 如果模块 使用了 exports = {} 导出
  
  
import * as RT from 'r'
  
  
// RT = exports
  
  
// 如果模块 使用了 export 导出
  
  
import * as RT from 'r';
  
  
// RT = 所有export导出的对象
  
  
// 如果模块 使用了 umd 导出
  
  
import * as RT from 'r';
  
  
// RT = 可能是module.exports 或 exports  的导出

CSS的模块化方案比如:CSS Module

对于我们的模块化生态(有工具可以下载,安装模块,有依赖项的配置),很多年以前前端是没有这样的基础生态的,自从 Node.js 横空出世随之而来的 NPM ,最先是应用在 Node.js 领域中的解决方案,随后扩充到了整个前端。因此,我们有 package.json 这样的配置文件,去定义模块的依赖,也有 NPM 这样的工具去下载,安装模块。如果你有私有化的需求,也可以直接购买 NPM 的企业服务,在这个上面还是很便捷的。

代码的规范化

当我们有了TS的强约束之后,我们还需要风格上的规范化,如:() => () => () => {} 这种风格,一看真的很想“打人”,还有一些属于个人喜好上的,如:var d ="xxx" 超过 120 个字符的长度(脑补一下,你根本看不到结尾,还要自己在编辑器中往后拖很久。)于是,我们很愉悦的接受了社区中最好的 TS React lint 方案:tslint-react,并且根据我们的一些行为习惯做了优化配置:

{
  "extends": ["tslint:recommended", "tslint-react"],
  "rules": {
      "jsx-alignment": true,
      "jsx-wrap-multiline": true,
      "jsx-self-close": true,
      "jsx-space-before-trailing-slash": true,
      "jsx-curly-spacing": "always",
      "jsx-boolean-value": false,
      "jsx-no-multiline-js": false,
      "object-literal-sort-keys": false,
      "ordered-imports": false,
      "no-implicit-dependencies": false,
      "no-submodule-imports": false,
      "no-var-requires": false
  }
}

编辑器的规范化

当你们公司将要超过三人时,编辑器的规范就无比重要了,如:其中一人的编辑器是4个空格,其中一人是2tab,有人喜欢这么用,有人喜欢那样用,于是在代码阅读上很可能造成障碍,于是 editorconfig 就是为了保证编辑器的规范统一,我们会用统一的风格去 commit 的代码:

# top-most EditorConfig file
root = true
 
 
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
 
# Matches multiple files with brace expansion notation
# Set default charset
[*.{ts,tsx}]
charset = utf-8
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
 
# Matches the exact files either package.json or .travis.yml
[{package.json}]
indent_style = space
indent_size = 2

业务的规范化

这里是指一些通配的文档,如:接口文档,Code Review 原则,gitlab的CI,Mock Server 等。

业务的组件化

由于我所选择的基础架构是围绕着 TS + React ,因此我们的业务都将进行组件化的设计,一个页面是由多个不同的小组件拼凑而成:

import * as React from "react";
import { connect } from "react-redux";
import * as actions from "./flow/actions";
import * as TYPES from "./types";
import { IStoreState } from "../../global/types";
import { Header } from "./components/Header";
import "./style.less";

const localImage = require("@/assets/welearnmore.png");
const onLineImage: string = "http://images.w3crange.com/welearnmore.png";

class HomeComponent extends React.Component<TYPES.IHomePageProps, TYPES.IHomePageState> {
  constructor(props: TYPES.IHomePageProps) {
    super(props);
    this.state = {
      name: "",
    };
  }

  public actionDataSync = () => {
    this.props.dataSync();
  }

  public actionDataAsync = () => {
    this.props.dataAsync("icepy");
  }

  public setName = () => {
    this.setState({
      name: "icepy",
    });
  }

  public render() {
    const { homePage, global } = this.props;
    const { syncId, asyncId } = homePage;
    const { globalSyncId } = global;
    const { name } = this.state;
    return (
      <div className="container">
        <Header localImageSrc={localImage} onLineImageSrc={onLineImage} />
        <div className="buttons">
          <button onClick={this.actionDataSync}> dataSync action </button>
          <button onClick={this.actionDataAsync}> dataAsync action </button>
          <button onClick={this.setName}> setState name </button>
        </div>
        <div className="contents">
          <p>
            syncId: {syncId}
          </p>
          <p>
            asyncId: {asyncId}
          </p>
          <p>
            setState name: {name}
          </p>
          <p>
            global Sync Id: {globalSyncId}
          </p>
        </div>
      </div>
    );
  }
}

const mapStateToProps = (state: IStoreState) => {
  const { homePage, global } = state;
  return {
    homePage,
    global,
  };
};

const HomePage = connect(mapStateToProps, actions)(HomeComponent);
export default HomePage;

自动化测试

由于我们选择的基础架构天然的可数据化驱动,因此在某种程度上来说,已经满足了UI自动化测试,只是在设计上我们需要去完善很多不同的类型,举例:假设我们有一个输入框,它的基础色是蓝色的,点击之后是红色的,对于自动化测试来说,就是模拟这个行为,并且输出数据(React 的渲染依赖的关键数据),我们可以定义 baseStyle="蓝色",clickStyle="红色",通过获取值来执行自动化测试,目前市面上的测试方案是:jest enzyme。

工具的构建

既然我们选择了TS + React 的基础架构,由于这是无法直接跑在浏览器中的代码,因此我们要经过一系列的转换,而这个过程我们选择了使用 Webpack + NPM Scripts 做为我们基础的构建工具和编译脚本:

"scripts": {
    "dev": "MODE=development webpack-dev-server --color --hot --open --mode=development",
    "build": "MODE=production webpack --mode=production",
    "start": "npm run dev",
    "precommit": "lint-staged"
 },
 "lint-staged": {
    "src/*/**.{ts,tsx}": "tslint --project tsconfig.json"
 }

这些脚本,我们都已经写好,对于开发而言(包括下一个项目),我们只需要下载已经搞好的 Starter 项目,输入 npm start 就能 Run 起来一个工程,而不需要去关心这些配置,这些脚本。

总结

工程化主要的意义是在于减少人为的干预与业务无关的代码或者实现,尽可能的在某种程度上去提高工程实现的效率。我想,这可能就是前端工程化的意义所在。今天,我们所面对的日益复杂的 Web 应用,这些手段是提高工作效率的有效途径之一。

我们提炼的工程化样本:

welearnmore/WLM-TypeScript-React-Starter

最后,欢迎讨论 ...