阅读 1380

社招的我,在酷家乐的野蛮生长

前言

前段时间发过一篇文章,在酷家乐做面试官的日子,获得了很多小伙伴们的点赞和阅读,这大概是我第一次较为正式的在技术社区发文章,所以还是挺惊讶也挺开心大家愿意花时间看我写的文章。

其实作者本人的文笔很烂,是个严重偏科的人,高考语文不及格,而且离及格线还很远,小时候最怕写作文之类的,之前不太喜欢写文章,但还是觉得不管什么行业,训练总结和归纳能力是有必要的,过一段时间做了一件事情之后,回过头来总结一下,有助于自己看到这段时间成长了什么,还有待提升的能力,有什么可以分享给其他人的,一些错误的理解甚至还能被纠正,其实挺好。

我在酷家乐是用户平台业务组的一枚小前端,之前被挺多小伙伴误以为是大佬,其实感觉还是沾了公司和工具平台大佬们的光,哈哈。。其实我们组做的业务很简单,就是产品运营导向的导流展示性质的网站,比如:酷家乐主站m 站桌面客户端,算是比较传统的 toC web 业务,包括一些移动 h5 和基于 electron 的本地客户端软件的一些业务,业务上没有什么难点也没有亮点,强行要说的话可能对样式和体验的要求会高一些(=。=)

所以如果光做业务没有思考(技术或业务)的话,做了几年各方面能力还是原地踏步一点也不奇怪,(肯定要有小伙伴吐槽,面试造火箭进去拧螺丝了)其实成长完全是看个人,公司平台只是给你机会,你要抓不住的话也没有人可以帮你带你,持续努力真的很重要,谁都喜欢和能力强的人做队友,老板也喜欢那种点一下方向就能做好所有事情的人,所以我其实工作了快 4 年在技术上一直都没有敢懈怠过。

时间线

17 年下

上面说了我们的业务本身没有什么技术壁垒,但技术债却很多,基础设施比较落后,我 17 年中刚来的时候这个业务组的技术栈还是 ftl + jquery 的形式,开发前端需要把 jetty 起起来。。。我相信每个公司都有这种技术债,只不过我司在互联网工程界当时算是落后其他公司一大截,不仅仅是我们组,其他前端组甚至后端的技术栈也是一样老旧,但这种局面也一定是有客观原因的,可能是资金有限,改造陈本巨大,业务多人少,有体系化改造经验的人也少等等。

所以,当时我进来就是负责和我的老板“胖胖”(比我早来一个月多点)一起改造用户平台前端组的技术架构和基础设施,然而起初那半年,我真正能花在技术上的时间真的很少,因为业务不等人,但我们仍然不得不“在高速公路上给飞驰的汽车换轮胎”,一边忍受极低的开发效率,几乎每个需求过来都是硬编码,还时不时要清理前人留下的坑,一边利用晚上和周末的时间做技术方案、技术任务(然而那段时间我家里也是最忙的时候,结婚老婆怀孕都赶在了一起,公司和医院就是我的家)。

第一步我们就开始尝试做前后端分离,当然我们需要真正的“前端基架组”的大佬们帮我们做掉一些事情,比如服务端渲染层,这层在前端和后端之间。而纯前端部分就得靠我们自己了,当时我的老板“胖胖”给了我两件事情,一件是前端工程化,一件是前端架构,后来我其实并没有选择的机会,因为工程化被交给了另一个更年轻的同事,所以自然我就负责起了更偏“业务”的前端架构。

说是说前端架构,但很多“后端思维”的人认为“架构师”并不是这种业务上的架构,真正的架构师视角应该是系统级的,包括了很多应用,而且前端能有啥架构?

我表示不太能认同,我自己就是计算机软件工程毕业的,js 本身就是面向对象的语言,实际上随着现在前端业务的复杂性提升,前端不过是在走后端的老路,以前后端碰到的问题,在前端领域也碰到了,没有设计的前端代码在这个年代显然是站不住脚的,犹如 10 年前一次性“网页脚本”般的存在,难不成我们一套系统用了不到半年一年就要重做迁移一次?

所以我就开始尝试在我老板“胖胖”搭建的 pug + jquery 的纯前端项目下开始做代码迁移(这时候我们的工程化也是同步进行的,实际上整个前端项目的开发体验好了太多了,有 mock,有 dev,有 build,也不用起后端服务),这个项目当时继续用 jquery 其实也是为了好迁移,毕竟新的工程化工具开发效率体验要好很多,所以很多遗留代码只是整理了一下就迁移过去了(过程并没有想的那么轻松),并且新的需求采用了新的架构去写,去掉了早期继承式的代码风格,采用了工厂模式和面向切面的思想,跟 react 一样把对应的生命周期钩子抛出来,开发效率相比以前要高了很多,也更加容易阅读和拆分。

工厂模式和面向切面的写法

但迁移这种活其实需要的并不是技术,而是耐心和细心,可以说是吃力不讨好吧,至少当时我是这么认为的。

直到遇到一些交互比较多的需求,需要频繁的做 reRender,这时候老板搭建的架构就出现了瓶颈,对于 reRender 没有很好的支持,也比较麻烦,所以我就突发奇想,把 react 中数据驱动视图的概念带到了当前 pug + jquery 的体系下,取名叫 store,让开发者聚焦在数据的更改上,数据改变自动触发视图的 reRender,还开发了一个简易版的 router,可以根据链接定位到对应的视图模块,改变路由也能自动切换绑定好的视图,虽然这些都是很小的技术方案,也就一百来行代码,但给我后面的工作其实做了一些铺垫。

引入 store 后的写法

引入 router 后的写法

除了前端架构,我还做过一段时间前端性能分析的项目,以虚拟小组(类似兴趣小组)的方式运作,我是组长,用空余时间带几个人一起来做一些技术项目,主要是利用 puppeteer 来离线对一些页面进行性能跑分并给出对应的优化建议(跟监控不是一回事),不过后来这个项目由于大家的业务时间不可控,拖了比较久被砍了。这个经历对我还是挺伤的,虽然有客观原因,但也意识到自己的组织能力是个短板。

总结

客观的说,17 年下半年这段时间,如果不是当时担子比较重,还真的有离职的想法,感觉自己在做别人不愿意做的“杂活”,如果更功利有的选的话,我想我不会选择这条路,因为跟我之前的经验完全不搭边,我之前做的比较深入的是爬虫和数据库相关的内部系统,虽然有过心理准备,但也没想到是这种情况,所以如果不想把时间浪费在这种我认为“可以节省的时间”上,那就只能“先苦后甜”了。

当时其实和我的老板“胖胖”也沟通过,我很真实地告诉了他我的想法和目前遇到的情况,他也客观的说过做架构做框架的人在任何一家公司其实都差不多(老板以前在淘宝三年也做过类似我的事情),很难拿到好的结果,述职的时候都会比较难说,而且设计上的东西很难说你的就是好的,又很难有量化指标,但这种事情又很重要不能不做,只说不想让做这样事情的人再吃亏,会尽力帮我争取一个好的结果(至此,我觉得我老板真的很好,他不是那种掌控型的领导,是会给建议的领导,并且对每个下属的心理、成长都很关心,如果我没记错的话,业务团队里面我们组的离职率应该是最低的,近两年时间内个人原因离职的仅有 1 人,而实际上,我们组的业务属性应该离职率是最高的才比较正常,这就很能体现他在管理上的一些才能了)。

18 年上

机会之所以称之为机会,就是并不是每个人都有的,对于大部分互联网公司,其实对社招都并不是很友好,当下就是需要一些熟练工消化业务,而且这些熟练工本身还不能太差,要 cover 一个或者多个项目的开发和维护,还要带来一些经验输出给团队的其他成员,至于特别培养什么的基本就别奢求了,可遇不可求,对社招的要求只会更高,但手上的事情其实很难做得超出预期。

怎么样?上面的话听起来就像一个 loser 在抱怨,客观的现象讲出来其实就没意思了,不然还能怎么样呢?但我认为优秀的人即使不需要铺垫一个完美开局,也应该把当下的每一件事做到你自己的极致,我来酷家乐学到最多的就是适应、没有机会自己创造机会。

所以 18 年上这半年,是我搞事搞的最多的半年,结果也确实这半年的绩效是我最好的一次。这半年因为业务上比较轻松,所以做技术项目的时间会比较宽裕。这个阶段,我的 OKR 其实有部分是我自己定的,我的老板只是帮我衡量一下可不可以做,或者商量一下怎么样的形式去做。

这半年我做了公司内部类似 gio 的全埋点采集 SDK,这个需求其实是来自于我们组的产品,所以能争取到一些业务上的时间来换取做技术项目的时间,实现我就不细说了,主要还是借鉴淘宝 spm 的一些做法。虽然这个项目后来被大数据组接走了,不过对我来说,我觉得还挺有意思的,这个项目也让我第一次意识到,产品们也是可以沟通的,即使是业务组,只要对业务有价值的项目其实也是可以做的,只不过要考虑一下 ROI。

有了全埋点系统还不够,产品们希望能对一个需求的各种形态做实验,针对不同的用户,不同的灰度,看哪种方式带来的数据增长和回报更好,所以我们又开发了一个分桶实验管理系统来支持,这个系统主要责任方是后端,所以前端只是打辅助的角色,开发周期也比较短,不过因为没有产品交互,所以都是自己在想什么样的展现形式比较合理。

另外还参加了基架组的一小部分细碎工作,不过也没帮上什么大忙,自己倒是系统学习了 docker 和 k8s 相关的知识。

上面也说了,几个项目不是被接走、被砍、就是主要责任方不是自己,觉得还是达不到我对自己的要求。

然后这时候有个契机是,公司希望统一使用 react 技术栈,虽然 react 在 C 端业务开发效率并不算很高,但统一的好处其实是大于坏处的,尤其我们很多组之间的组件有可能是通用的,很多其他组已经在使用 react 了,长远来看是值得切换的。

借着这个机会,我自己提出想开发一款状态管理框架,当时比较流行的是 redux 和 mobx,redux 的好处是可预测,比较稳定,但从一些已有的项目和我自己的经验来看,开发和维护体验并不好,心智负担很多,我希望有更加简洁和可追溯的 API,对 TS 的支持也希望更好。mobx 的问题是难测试,难调试,有很多 magic,老项目里面的一堆 reducer 也很难迁移,多人协作也会导致一些公用 store 逐渐变成“胖球”模型,虽然使用上更加受到 vuer 的青睐。

为何不能结合一下他们的优点呢?当时就想自己开发一个状态管理框架,统一一下公司内部的各种框架,再加入一些常用的功能和扩展插槽,融入一些业务架构的理念,附上一个最佳实践,既达到了收拢、规范效果,又可以及时添加功能、应对各种场景。

所以就尝试开始写这个状态管理框架,我取名叫 sticky。在写这篇文章之前,我也看到过社区很多人都折腾过状态管理框架,还有很多人去对标 redux、mobx、dva 等,其实这是有原因的,因为状态管理在 react 生态是最偏“业务架构”属性的,所以可玩性会很强。

我觉得,个人开发者其实很难和一个团队一个社区去抗衡,各种各样的框架其实理念都差不多,很多人都想到一块去了,对于普通的框架开发者,框架只是在设计上的抉择不同,不存在什么特别厉害的“黑科技”,所以,框架“统一、舒服”远远比“优雅、黑科技”要重要的多,好像 vue 的初衷就是这样吧,当时尤大大也只是觉得市面上的框架总有用的不舒服的地方,所以才自己开发了一个。

由于框架还在开源审核流程中,暂时还没向外推广,我可以先稍微介绍一些 sticky 框架的特点。由于我们的业务属性不存在类似后端的 model,所以我借鉴了 DDD 中的领域模型的概念,我把下面这样的 class 称之为一个领域模型:

import { state, mutation, effect, computed, reducer } from 'sticky';
import { fetchColumnList } from '../api';

class DesignColumnDomain {
  @state() columnList = []; // 需要状态管理的属性
  @state() pageSize = 18;
  @state() totalPage = 0;
  @state() current = 1;
  @state('localStorage', 1000) status = 1; // 在 localStorage 中也存一份,并设置过期时间
  
  @computed // 计算属性,属性没变化会缓存计算值,不会重复计算
  get computedVal() {
    return this.pageSize + this.totalPage;
  }

  @mutation // 类似 mobx、vuex 的 mutation、action 写法,只做同步操作
  columnListLoaded = ({ columnList, totalPage }) => {
    if (columnList) {
      this.columnList = columnList;
      this.totalPage = totalPage;
    }
  }

  @reducer // redux 的 reducer 写法
  updateColumnListPage = (state, page) => {
    return Immutable.fromJS(state).setIn(['current'], page).toJSON();
  }

  @effect // 支持异步操作,提供更新语法糖
  async fetchColumnListFromRemote({ page, tagId }) {
    this.$merge(
        this.updateColumnListPage,
        this.columnListLoaded,
    ); // 合并多个同步操作
    const { columnList, totalPage } = await fetchColumnList({
      page,
      num: this.pageSize,
      tagId,
    });
    this.columnListLoaded({
      columnList,
      totalPage,
    });
    // 提供简单的单属性更新的语法糖
    this.$update({
        columnList,
        totalPage,
    });
    /**
     * 这个 effect 之所以不让直接通过 this 赋值就是为了防止写出过多过程式,无法复用的代码
     * 希望隔离流程和状态更新操作,职责分离,这点我认为 redux 的形式更好
     **/
  }
}
// 让用户自己去实例化的好处是灵活性高,即使不用 ts,vscode 也会自动提示这个 model 中的属性和方法
// 如果用工厂模式去帮助产生实例,ts 下类型的推导是正确的没什么问题,但 js 就无法享受这个好处了
export default new DesignColumnDomain();
复制代码

下面这个 class 其实也只是一个普通的 class,它的作用是用来分发流程,我把它叫做 processor,注意:在上面的 domain class 中是不建议互相引用的,所有的方法都只操作本模块的状态,如果要结合起来做一些计算,应该提升到 processor class 中来做,processor class 也可以做成复用的,它描述的是一组操作流程:

import { getQuery, updateQuery } from '@util/url-tool';
import $column from '../@domain/column';
import $tag from '../@domain/tag';

class ListProcessor {
  @computed
  get computedVal() {
    return this.$column.pageSize + this.$tag.currentTagId;
  }
  
  async changePage(page) {
    $column.updateColumnListPage(page);
    updateQuery({
      page,
    });
    $column.fetchColumnListFromRemote({
      page,
      tagId: $tag.currentTagId
    });
  }

  async changeTag(id) {
    $column.updateColumnListPage(1);
    $tag.updateCurrentTagId(id);
    updateQuery({
      page: 1,
      tagid: id
    });
    $column.fetchColumnListFromRemote({
      page: 1,
      tagId: id
    });
  }

  async initLayoutState() {
    const { page = 1, tagid = '' } = getQuery();
    $column.updateColumnListPage(page);
    $tag.updateCurrentTagId(tagid);
    $tag.fetchTagsFromRemote();
    $column.fetchColumnListFromRemote({ page, tagId: tagid });
  }
  
  // 将来的一些想法,想补足一些竞态的功能
  @effect('takeLatest')
  async processorFuture() {
    return this.$pipe(
        this.$merge(
            $column.updateColumnListPage(page),
            $tag.updateCurrentTagId(tagid),
        ),
        //this.$call($tag.fetchTagsFromRemote()),
        this.$cancel($column.current === 0, [
            $tag.fetchTagsFromRemote()
        ]),
        this.$update({
            computedVal,
        })
    );
  }
}

export default new ListProcessor();
复制代码

怎么使用,sticky 把一些细节都给隐藏起来了,整个初始化只需要一条代码就搞定了:

Sticky.render(<Layout />, '#app');
复制代码

jsx 或 tsx 中又该如何使用呢?

import React from 'react';
import { stick } from 'sticky';
import {
  Breadcrumb,
  Radio,
  Pagination,
  Skeleton,
} from 'penrose';

import './index.scss';
import Columns from './columns';
import $list from '../../@processor/list';
import $tag from '../../@domain/tag';
import $column from '../../@domain/column';

@stick() // 标记这是一个需要被自动触发更新的组件
export default class List extends React.Component {
  componentDidMount() {
    // 直接触发 processor class 中的流程
    $list.initLayoutState();
  }

  render() {
    // 模板中直接使用 class 中的属性或方法
    return (
      <>
        <Breadcrumb styleName="bread-crumb">
          <Breadcrumb.Item href="/design">
            优秀设计
          </Breadcrumb.Item>
          <Breadcrumb.Item>
            设计专栏
          </Breadcrumb.Item>
        </Breadcrumb>
        <Radio.Group value={$tag.currentTagId} onChange={$list.changeTag}>
          <Radio value="">热门推荐</Radio>
          <For
            each="item"
            index="idx"
            of={$tag.tags}
          >
            <Radio key={idx} value={item.id}>{item.tagName}</Radio>
          </For>
        </Radio.Group>
        <Skeleton
          styleName="column-list"
          when={$column.columnList.length > 0}
          render={<Columns showTag={$tag.currentTagId === ''} data={$column.columnList} />}
        />
        <Pagination
          current={$column.current}
          defaultPageSize={$column.pageSize}
          totalPage={$column.totalPage}
          onChange={$list.changePage}
          hideOnSinglePage={true}
        />
      </>
    );
  }
}
复制代码

看到这里应该是有一些疑问,为什么直接用 es module 来取代像 mobx 那样的 store key 的形式挂载到 props 上,这样主要还是希望编辑器能在 js 下直接就有提示,如果通过 props 来传递,一方面会断开引用,形成了声明的心智负担,另一方面也会与父组件传过来的 props 混在一起。

这样做有缺陷吗?当然有,import domain 的语句会和其他 import 语句混在一起,貌似还不太符合 react 组件设计原则,但其实普通的基础组件我们不会用状态管理,业务组件用到状态管理,大部分情况下使用者并不想关心你的 domain class 里有什么东西,只关心传进去的 props 参数而已,所以只是数据源从哪里获取的问题,外部获取用 forceUpdate(), 走 props 的话可以直接用 setState(),由于每个 domain class 自带隐形的命名空间,更不存在冲突的情况,所以也不需要 redux 那种一一映射的麻烦写法。

其他的一些功能:

// 全局配置开关
Sticky.config({
    // devTool: true,
    middleware: {
      logger: false,
      effect: true,
    }
});

// 使用一个自定义中间件,中间件形式和 redux 的一样
Sticky.use(middleware);
复制代码

sticky 也支持时间旅行,自带 logger 和 effect 中间件,基本算是抹平了 redux 和 mobx 的差异性吧,至于合理性我觉得见仁见智,反正公司内部已经小范围在尝试使用了,后面可能会进入维护期并开源出来。

总结

这个半年做的事情比较多,成长的速度也是最快的,比较精读的框架源码应该也有 5 个以上,也为下半年的晋升答辩润色了不少,我司新人入职必须要满 1 年才能进行晋升,所以我算是刚解冻就晋升了,虽然挺险的,我答辩的时候因为演讲者注释没调出来脑子一片空白全程脱稿裸讲,讲得特别烂,感谢工具和算法的几个大佬最终给过了。。。不过整个过程感觉还是挺严格的,被怼的很惨,确实还是有挺多人被卡掉了。

18 年下

每年的下半年业务总是很多,所以这半年其实也没什么时间做技术项目,主要的精力都花在了 react 化改造和几个大型业务的任务拆分和技术方案制定上。

react 化改造主要包括了不断的优化 sticky 框架,搭建新项目新仓库,并跑通整个开发构建部署流程,老的页面或活动页我们仍然采用 jquery 开发和维护,新的主频道页缓慢迁移到了 react 技术栈,除了这些,还和 UED 合作,建设了公司内部的 C 端通用 react 组件库,初期我也贡献了比较多的代码,开发效率相比以前要高了很多,整个过程算是我一直在协调和跟进的,这半年在技术上的组织能力要比以前更游刃有余了,但其实技术上的亮点可能不如上半年。

而且这半年在业务上的沟通合作还是出了点问题,因为一些原因,在会议中直接和产品暴露了情绪,其实显得自己挺不专业的,我并不是不懂这个道理,我工作那么久其实也是第一次那么生气,气到要直接怼回去,生气其实是因为对方先把激烈的言辞甩了给你,还对你的辛苦不屑一顾,本质其实是因为她不理解你或者不想理解你,“我理解你的难处,但是对不起,我就要明天上线”,“视觉周末加班出稿子,但还是没有一次性到位,导致前端延期,但我还是明天就要上线”等等,毫无人情味和委婉的表达,而且我也不可能把锅全部甩给视觉,我不是一个喜欢推卸责任或者主动攻击别人的人,生活中我就是这种性格,但要太过分了,可能宁愿丢了工作也要跟你干到底的典型三傻之狮子座脾气。。。当时就觉得同事也是人,你们合起伙来怼弱势群体实在是有点太尴尬了,何况人家没犯什么错误,职场再尔虞我诈也稍微在乎一下对方的感受吧,说话完全不先考虑一下合不合适说。

结果最后还是我被老板和平台前端大老板给约谈了,他们也没有跟我上纲上线,也客观分析了事情的情况和我的问题,事情过了之后其实想想也没什么大不了的事情,对方是个说话比较冲的人,但也不是那种不可以沟通的,所以其实跟当时业务压力大也有关系,突然就点着了。

总结

这半年基本上功过抵消了,所以就拿了个很普通的绩效,算是戾气最重的一段时间,有一个多月也是经常工作带到家里做,天天做到凌晨一二点,还在和视觉互相回企业微信的消息。因为晋升了,所以其实好绩效变得更加难拿了,对于资深来说不做点有影响力的事情其实很难拿好绩效。

19 年上

今年上半年给自己定的目标是开源 sticky 框架,并且把前端物料平台做起来。对应给自己的要求是独立负责一整个比较大型的技术项目并更好的支撑业务拿到好的结果,并且把运营和推进事情的软技能给提升起来,不管结果怎么样,这是我的方向。

感想

回过头来,感觉自己做了很多东西,又感觉什么也没做,没什么拿得出手的亮点项目,所以我觉得后面可能得更聚焦一些,只做好 1-2 件事,虽然这在业务组真的很难,但只要有点时间做技术,我觉得就已经很幸福了,剩下的就得看管理层和公司层面的用人方式了。

不过说真的,以我的背景,可能到哪都只能写业务,我相信大部分人跟我是一样的,所以其实写业务也没什么不好,写简单的业务也没什么不好,重要的是业务给你带来了什么,你给业务带来了什么,不排斥但也不懈怠,业务写的不咋样的人怎么放心给你做技术呢?

如果你觉得你的成长比平台慢,那你应该抓点紧了,如果你的成长已经远远超过了平台,换个平台也不一定是件坏事,但不要因为一点困难就轻易的逃避,一件事情还没做好就想着做别的事情,有时候要正视自己的问题,不要总是怪别人怪平台。

以上全部,是我自己的看法,不代表公司不代表任何。

我们还有很多 HC,如果你有兴趣加入一起做建设,我的邮箱:feifan@qunhemail.com

关注下面的标签,发现更多相似文章
评论