能跑就行的跨项目共享文件实践

544 阅读6分钟

背景

随着我们的业务不断的发展,我们的前端工程也变得越来越大,能否管理好这么一个庞大的前端项目,决定了后期维护以及迭代的成本。相信很多接触过大型前端项目的同学应该也接触过不少解决方案了,比如mononrepo、微前端、微服务、多仓库git管理,这些在社区都有比较成熟的方法了。这里我们就不进行细细的套路,今天是要解决的是怎么在这样的多项目中进行代码复用。

常见方案解决

复制粘贴

image.png

这是自己开发的时候最喜闻乐见的形式了,业务初期,起步快,业务呈现小步快跑的趋势,我就是直接cv一把梭,这种方式优点很明显,就一个字:快!但是缺点也很明显那就是代码大量的冗余,对于一个有代码洁癖的人是不能接受的(如果你让我写,那就另当别论)。同时随着后续业务的迭代,多个项目中的代码难以保持一致,如果中途换了人进行维护,那么口口相传的方式很容易就引起bug,当然如果本来就在两个项目中有差异化的开发,那么这样的复制粘贴是一种上添花的方式。

npm发包

image.png

将对应需要复用的代码进行抽离成npm包,然后在需要进行使用的项目中进行安装,这种方式相信大家肯定也不陌生,优点也很明显:代码不冗余,同步管理好。缺点也是不少,比如需要有专门的人来进行专门的包管理,每次进行简单的修改就要进行重新的发包流程,你和热跟新已经告别了,别以为这样的时间可以忽略不记,当你遇到一个反复修改的产品经历时候,你今天的时间算是就是栽在这里了。

workspace

如果你有接触过monorepo的话,那么你对这一种方法一定不会陌生,workspace工作空间,最开始是yarn提供的一种monorepo依赖管理机制,可以在项目的根目录管理多个项目的依赖,随着技术的发展,现在yarn workspace已经退出舞台了,现在新的项目用pnpm workspace,这种方法可以说是非常的好用了,优点很明显:代码不冗余,同步管理好,需要热更新的话我们只需要设置好package.json的main字段即可:

{
  "main": "src/index.tsx"
}

这样我们每次组件进行更改的时候,对应使用项目也可以进行热更新了,当然如果你这个包需要进行发布的话,就只需要改变路径为构建产物可以说是相当好用了。

{
  "main": "dist/index.tsx"
}

说完了优点,我们来说一下缺点:如果复用的代码不在同一个仓库的话你就需要进行发包处理走方法二的老路。

module federation

image.png

首先来简单介绍一下什么是module federation(这也是本文需要重点介绍的方法,毕竟总是要讲出一点新东西才能骗到大家的赞嘛),这是webpack5推出的新特性:"一个应用可以由多个独立的构建组成,这些独立的构建之间没有依赖关系,他们可以进行独立的开发部署,这就是通常被认为的微前端,但是又不仅限于此",可以看出mf要做的是和微前端做的类似的东西,我这里不是对mf的进行深入的介绍,而是对我在使用mf时候遇到的痛点的对应解决办法进行一个分享。

mf主要分为host和remote也就是我们的消费方和提供方,对应的消费方通过配置将需要暴露的部分进行默认暴露,消费方通过合适的配置就可以进行消费使用。这样讲有点抽象,我给个抽象的例子来抽象你们一下啊:

提供方
new ModuleFederationPlugin({  
  name'app2',  
  ...  
  exposes: {  
    './Hello''./src/Hello',  
  },  
}
消费方
const { ModuleFederationPlugin } = require('webpack').container;  
  
module.exports = {  
  ...  
  plugins: [  
    new ModuleFederationPlugin({  
      name'app1',  
      filename'app1RemoteEntry.js',  
      remotes: {  
        'app2''app2@http://127.0.0.1:8002/app2RemoteEntry.js',  
      },  
      shared: { react: { singletontrue }, 'react-dom': { singletontrue } },  
    })  
  ]  
}

经过上面的配置我们就可以在消费方使用提供方暴露的代码片段了像这样:

import React from 'react';  
import App2Hello from 'app2/Hello';  
  
const RootComponent = () => {  
  return (  
    <div>  
      <div>app1</div>  
      <App2Hello />  
    </div>  
  );  
};  
  
export default RootComponent;

对比前面的方案我们来看一下这种方式的优点:代码不冗余,同步管理好,如果消费方不在同一个monorepo依旧可以使用,同时在使用的时候我们也不用创建那么多个package了,无论是啥我们只需要在expose中进行填写。缺点也是有不少:

1. 消费方和供应方的类型统一困难,无法获取正确的声明文件。
2. 问题排查source-map在消费时候无法正常使用。

针对以上的问题社区有给出相应的解决方案:

方法一: 类型问题,通过手动复制对应的类型声明文件挂载在CDN服务供消费侧拉取使用,也就是手写.d.ts文件,当然手写的方法太low了我们也可以通过解析对应的暴露片段的ast来提取对应的类型声明文件来上传到CDN中,在实践的过程中,针对ast的使用我觉得还是有难度的,对一些自定义类型不好提取。同时这样的方式下source-map依旧是不可以使用的。
方法二:通过tsconfig.json来进行路径修改。这是今天要分享的一种自己实践出来的方法。首先我们知道,类型无法统一就是声明文件无法进行很好的统一,如果我们在使用组件的时候已经能够找到组件的定义,那么其实没有类型声明文件也是不影响的。tsconfig文件配置中给了我们一个很有用的配置项,那就是path,如果你玩过路径别名的配置,那么一定对这个功能十分的熟悉了。想象一下,你的消费侧通过路径别名拿到供给侧的组件定义,这样我们是不是也实现了跨项目的代码片段共享了呢?同时由于我们找到的是组件的定义位置而不是通过类型声明文件来进行使用,我们的source-map也就可以正常使用了,而这里提到的组件定义位置其实在我们写module federation的exposes时候就已经有了,我们只需要进行一个遍历就可以批量的实现路径别名设置,当然这种方式只适合两个项目在你本地都有,因为ts最终是不会影响你的开发的,所以只要两个项目你都拥有,那么你就可以使用这种方法了。