一. 现状
现在我接手一个项目”好货网”, 该项目已经上线两年, 我的 title 是前端技术负责人, 团队现有三名前端工程师, 其中一人刚入职.
项目用原生 JS+CSS 编写, 没有使用任何框架, 整个网站共有 10 个一级页面, 20 个子页面, 页面风格一致.
当有新需求时, 开发人员会新建一个页面, 如果新页面的部分模块和已有页面雷同, 会拷贝代码到新页面中, 基本不考虑复用和抽取.
二. 问题
- 新页面开发周期长, 产品经理痛苦不堪
- 页面难以维护, 比如有一个模块是商品模块, 当商品结构调整时,所有的商品信息展示都要更改, 开发和测试要对 15 个页面进行调整和测试
- 页面可读性差, 因为已经换了很多工程师, 每个人的风格可能都不一样, 导致代码风格千奇百怪
- 重复代码非常多, 而且同一种业务逻辑会有多种实现方式, 但是效果是一样的
- 不敢重构, 动一发牵全身
三. 目标
为了解决上述五个问题, 我决定开展一个技术项目, 即实现项目组件化
四. 方案
4.1 框架的选择 有两种方案可以选择, 一种是原生 JS 组件化, 另一种是引入 react,vue 等框架. 考虑到团队成员的水平, react 和 vue 基本没用过, 这样会有学习成本, 而且这些框架入门容易,用好难, 考虑到团队成员的 js 基础还不错, 保险起见, 决定采用 js 实现组件化.
4.2 制定方案 分析所有页面后发现, 30 个页面的风格一致, 按钮,下拉框,弹出层等都一致,商品展示模块 ui 一致, 用户信息模块 ui 一致, 所以打算分为基础组件和业务组件两部分进行封装, 为了保证线上功能稳定性和迭代的正常开发, 按如下步骤完成最终的上线:
- 跟 UI 和产品沟通, 确认网站风格, UI 设计基础组件和业务组件
- 封装基础组件
- 替换线上页面的基础组件, 一个个上线, 保证稳定性
- 封装业务组件
- 替换页面的业务组件, 一个个页面上线, 保证稳定性
- 重构页面, 一个个页面上线
4.3 组件的实现过程 组件分为两大类, 一类是基础组件, 一类是业务组件, 业务组件使用基础组件+业务逻辑进行封装, 除了一些简单的基础组件,比如 button 等, 其他组件都继承一个组件基类 Component
4.3.1 基类 Component 的实现 1. Component 有三个最重要的生命周期: init: 组件实例被创建, 我们在这里去创建一个 div mounted: 在 div 挂在到 dom 树上, 该方法会触发, 供子类去重写 unmounted: 把 div 移除 dom 树 2. 提供一个 attributes 数组, 用于存放组件的属性值, 比如 style 属性,data 属性, 由使用者传递进来 3. 组件之间需要互相挂载, 所以提供两个方法 appendTo 和 appendChild. 考虑到大多数组件都是被挂载到 Component 上, 提供一个 render 方法供重写 4. 很多时候, 组件的逻辑很复杂, 不同状态下展示不同的 div,这时候需要有状态去标识, 所以我们可以封装一个 states 数组到 components 中 5. Component 要暴露出去 div 的事件供子类设置, 封装 addEventListener, removeEventListener, triggerEvent 方法, 子类可重写
4.3.2 基础组件的实现
基础组件包括 TabContainer, ScrollContainer 组件等, 带有业务特性, 需继承 Component 并按组件要求重写某些方法, 举例
1. TabContainer 组件会重写 render 实现 tab 头的切换效果.
2. ScrollContainer 组件的最外层 div 要设置为可滚动, 一种方法是每次使用 ScrollContainer 时传入 style 属性, 但对 ScrollContainer 来说这种属性是固定的, 应该封装到 ScrollContainer 更合适, 所以我采用重写 init 方法,里边(先调用super.init())
4.3.3 业务组件的实现 业务组件继承 Component, 可以使用基础组件, 一般是用于通用业务模块的抽取, 比如商品模块,ui 和交互都一样,就抽取出来, 商品信息作为属性传递进来, 根据需求去展示商品信息, 内部交互自己处理,有的交互需要通知父组件时通过事件向外传递
4.3.4 页面 页面由各个业务组件+基础组件+业务逻辑封装, 页面一方面负责取服务端数据, 并把部分数据传递到业务组件中, 另一方面需要处理子组件之间的交互
4.3.5 过程中的点
1. 应该会有不少人在子类中直接调用 component 创建的 div, 这种是否合理呢? 我的想法是不太合理, div 应该由 component 自己维护, 但是开发的过程中发现有的子类真的需要调用,比如 scrollContainer 需要知道 div 的高度, 所以在 Component 中提供了一个 get 方法供子类调用, 纠结… 其实也可以考虑不能被子类调用, scrollContainer 自己定义一个最大的 div, 挂载到 component 的 div 上,然后 scrollContainer 取自己的 div 高度
2. 如果有埋点的需求, 比如大部分组件的 mounted 后都需要发送后端埋点, 可以考虑封装到 component 的 mounted 中, 提供一个 attribute 作为是否发送埋点的开关, 控制 mounted 里要不要加载
3. 子类组件封装自己能提供的事件, 对于使用者来说只需要知道事件的名称, 然后把事件函数传入. 比如 scrollContainer 在滚动条触底时要对外抛出一个事件, 不具备通用性, 需封装到 scrollContainer 中, 所以它需要提供一个 scrollEnd 事件. 一开始我直接调用this.container.addEventListener(‘scroll’)去做的, 但是思考了一下觉得应该通过重写 addEventListener 方法去实现比较合理
4.4 组件化的工具 组件化的过程中, 会发现很多问题, 比如:
- css 样式都写在 js 中, css 维护起来非常不方便, 而且代码可读性变差. 解决方式有很多种, 比如 npm 去引入 css-loader, 让 css 文件可以 import 到 js 中
- 用 js 代码去生成 dom 树比较繁琐, 不够清晰和直观, 可以引入 JSX 的写法, npm 上找现成儿的, 或者可以自己去实现解析 jsx
4.5 保证项目可维护性的其他措施
- 项目大了, 无论新老员工都需要知道项目中目前有哪些组件, 省的去找代码看, 开源的 storyBook 非常好, 可以很方便安装到项目中, 项目启动后, 在浏览器中就能看到目前项目的所有组件了.
- 确定一种好的代码风格, 通过 codeReview 的方式让团队的代码风格趋于一致,大家也能互相学习各自的好的编程风格, 提高个人的编程能力
- 我觉得不停的重构是保证团队质量的唯一方式, 而如何保证重构又能完成迭代任务呢, 搭建自动化测试是一种好的方式
五. 结果
这是个计划, 我还没做完 4.3.2, 没有结果, 中间肯定还有很多坑…. , 这是个漫长的过程, 但是会有完成的那天的, 毕竟时间如白驹过隙, 岁月又曾绕过谁呢