背景
在用车saas化推广兼容小品牌用车的过程中,由于用户交互、接口数据、业务流程在主品牌与小品牌之间存在差异性,导致代码分叉过多,影响可读性与可编辑性;两侧用车能力存在部分混入,造成运行时代码过大;单一场景迭代容易干扰其他端侧用车能力;后续还会拓展到端外用车,上述问题会继续放大。
什么是静态化编译
简单来说,本文中「静态化编译」等同于程序运行中的「编译时」,与「运行时」是相对的。也就是说主要在程序编译阶段,就得把相关代码打包进去产物中,来降低运行时的压力。业务静态化编译,也就是在开发阶段就把不同端口难以融合的业务逻辑以不同文件的形式进行拆分,这里的不同文件是指不同的文件名后缀,文件名是相同的。来到编译环节,通过webpack resolve plugin来对文件名及其后缀进行分叉与打包。
目标与价值
- 实现差异化场景隔离,共享核心用车能力;
- 多场景业务逻辑拆分,降低测试回归工作量;
- 降低研发、维护阶段对于其他端额外影响和风险;
- 实现差异化打包构建,降低包体积,提高秒开率;
- 提高业务代码整洁性和独立性;
- 为实现业务能力编排提供技术可能性。
使用场景
业务隔离
营销能力、全局通知栏、临时锁车、头盔能力、响铃寻车、更多业务...
样式隔离
开锁页底部交互卡片、骑行中底部交互卡片、lottie动画能力、popup弹窗能力
基础能力隔离
蓝牙能力 @hb/hb/bluetooth-taro、车码识别能力 @hb/taro-core-monorepo/scan-parser、webView承载页(主品牌使用平台webView承载页;小品牌独立)
如何实现业务静态化编译
对于解决静态化编译,首先需要解决业务逻辑分叉问题;其次开发resolve插件系统,对于分叉代码按照文件名后缀进行提取与编译。
古早时期(未使用多场景静态化编译方案前)

上述图片是业务代码中分别引入小品牌与主品牌全局通知栏业务hook,只有在运行时才能根据当前环境是小品牌或主品牌来进行区分端口。这种方案的缺陷很明显:运行时过大,直接影响包体积;代码分叉太多,影响可读性。

上述图片是构建后产物,不管当前环境是小品牌还是主品牌,其他场景逻辑都在在产物中,影响其体积。
当下及以后(使用多场景静态化编译方案后)

引入方式终结在文件夹之后,不用指定当前的具体文件名及其后缀。

这是构建小品牌场景后的产物,如果是主品牌的话,产物中后缀则为.../*.index.oho.ts。
目录结构

上述图片是老的目录结构:
只有通过文件名来进行区分不同业务类型:
/mini.ts:小品牌;/oho.ts:主品牌

上述图片是新的目录结构:
通过文件名后缀区分不同业务类型:
.mini.ts:小品牌包含了 .mini.alipay.ts,.mini.weapp.ts;
.oho.ts:主品牌, .oho.alipay.ts,.oho.weapp.ts基础
require的局限性

- 只限于node环境;
- 只会解析.js .json .node文件;
- 只会去解析文件的完整路径,不会解析文件夹;
- 解析文件成功后返回值仅仅是一个路径,没有包含描述文件等较为丰富的数据。
创建插件
在创建插件时一般会传入source和target两个参数:
- source:插件拿到Resolver.hooks['source']钩子,并调tap或tapAsync添加处理函数事件。当解析器接收到了source事件时,会执行注册的处理函数;
- target:在处理完毕后,调用doResolve触发一个target事件,交由下一个监听target事件的插件处理。

有了注册事件tapAsync和触发事件doResolve,各个插件就可以像积木一样链接起来。
Tapable介绍
简介
Tapable 是一个类似于 Node.js 中的 EventEmitter 的库,但它更专注于自定义事件的触发和处理。通过 Tapable 我们可以注册自定义事件,然后在适当的时机去执行自定义事件。这个和我们所熟知的生命周期函数类似,在特定的时机去触发。
Tapable是一个由Webpack团队维护的事件库,它基于发布订阅模式实现事件处理。在Webpack中,Compiler和Compilation是Webpack的内置对象,它们都继承于Tapable。通过使用Tapable,这些对象可以触发事件,从而将不同的插件串联起来。这种机制使得Webpack可以在不同的编译阶段调用不同的插件,从而影响编译结果。
更具体地说,Tapable提供了一系列事件的发布订阅API,允许注册事件,然后在适当的时机触发这些注册的事件进行执行。这些注册的事件可以分为同步和异步两种执行方式。同步钩子可以使用tap方法进行注册,并通过call方法触发执行。
总的来说,Tapable在Webpack中起到了一个核心的作用,它连接了Webpack的各个插件,使它们能够按照设定的逻辑和时机进行交互和执行,从而实现了Webpack的灵活性和扩展性。
分类

简易demo
AsyncSeriesBailHook 是一个异步串行、熔断类型的 Hook。在串行的执行过程中,只要其中一个有返回值,后面的就不会执行了。这里列举该hook是因为后续resolve众多plugin之中对于插件系统中hook的注册,全部都是使用该钩子。


enhanced-resolve介绍
enhanced-resolve是webpack的一个核心包,用于增强webpack的模块解析能力,使其更容易找到所需的模块,从而提高webpack的性能和可维护性。
具体来说,enhanced-resolve可以为webpack解析器添加额外的搜索路径以及解析规则,让webpack更好地解释路径和文件,进而让webpack更加专心地做模块打包相关的事情。
总的来说,enhanced-resolve对于webpack的作用是增强模块解析能力,提高性能和可维护性,使webpack更加专注于模块打包相关的工作。
流水线



不同的hooks之间通过Resolver中的doResolve串联起来,一个hook中可以包含多个plugin。
插件流转

这里描述了插件在各个hooks上的注册过程。可以将hooks理解成弹夹,plugin理解成子弹,插件的注册过程也就是给多个弹夹装子弹的过程。
Resolver实例创建与解析

- ResolverFactory.createResolver 根据 Resolver 类创建实例:myResolve (吃了配置,吐出对象myResolve)
- myResolve 上 注册并订阅 大量的 hook (枪支弹药贮备好,一刻激发)
- 调用 myResolver.resolve 方法开始进行 文件解析 的主流程
- 内部通过 resolve.doResolve方法,开始调用第一个 hook: this.hooks.resolve
- 找到之前 订阅 hook 的 plugin:ParsePlugin
- ParsePlugin 进行初步解析,然后 通过doResolve 执行下一个 hook parsed-resolve,前期准备工作结束,链式调用开始,真正的解析文件的流程也开始。
插件装配


集成静态化编译能力
- 安装@hb/multi-scene-resolve-plugin_自定义编译文件npm包;

- 项目配置文件中引入上述npm包;

- 替换taro内置multiPlatformPlugin插件。

使用提示
统一接口的多场景文件这一跨平台兼容写法有如下三个使用要点:
- 不同端的对应文件一定要统一接口和调用方式。
- 引用文件的时候,只需要写默认文件名,不用带文件后缀。
- 最好有一个平台无关的默认文件,这样在使用 TS 的时候也不会出现报错。
Q&A
1.为啥必须用MultiSceneResolvePlugin插件覆盖Taro MultiPlatformPlugin自带插件,而不是并行处理?
由于MultiPlatformPlugin解析不了以下文件路径:/pages/riding/useHelmet.oho.weapp.ts,所以只能覆盖Taro自身插件;并且重复解析会提高编译时间。

2.插件注册的时机or钩子一定是described-resolve和resolve嘛?
不一定,只要是在文件名或后缀完成绑定前都可以,比如before-file,file等。
3.为啥不能调整MultiSceneResolvePlugin插件与MultiPlatformPlugin的顺序,使其先执行前者?
MultiPlatformPlugin插件Taro自带插件,集成在@tarojs/mini-runner和@tarojs/webpack-runner中,通过chain.merge合并自定义配置项,执行优先级高于自定义。而且如果重置了,解析出带有后缀的完整路径后会影响MultiPlatformPlugin中对于处理文件的筛选条件。
4.为啥不编写taro插件而编写webpack插件?
taro插件系统从3.6.3版本才系统性的支持,而且webpack插件在系统中更为通用。
(本文作者:刘广永)
关注公众号「哈啰技术」,第一时间收到最新技术推文。