在项目中渐进式的升级组件库

1,512 阅读5分钟

前言

众所周知,在前端项目不断的更新迭代的过程中,总会遇到所依赖的库升级的问题,对于一些已经有了一定规模的项目来说,对所依赖的库进行升级,往往是一个令人头痛的事情,特别是一些库大版本升级时,进行了很多破坏性更新的情况下。随着项目的越来越大,升级的成本也就越来越高。

为什么要升级

对项目中依赖的库进行升级,主要是因为旧的版本官方基本都不会再新增功能,只是修复一些bug,当你遇到问题时,也很难获得官方的支持,特别是对于公司内部的库来说,一方面公司层面可能会要求各个项目进行升级,另一方面则是当你需要公司库的某个功能,但它又只在新版本提供了后,你就会面临究竟是自己写一套,还是升级版本的局面,而基础库的部门,大概率不会为了你的功能,而在旧版本新增功能。

如何升级

最近在为自己负责的项目进行组件库的升级,组件库时公司内部的组件库,项目依赖的版本时2.x版本,而最新版本已经到了4.x。这里就只分享一下升级方案

基本思路

在项目中共存两个版本的库,然后根据路由,一个路由一个路由的进行升级,但这里涉及到两个问题,一个是如何让一个项目存在两个版本的库,另一个则是样式问题,众所周知,css样式是会互相覆盖的,这就导致即使两个版本,如果这两个版本采用的css类名基本一致,就会样式冲突,很不幸,我的项目就是这样的。

如何让两个版本共存

我这里采用的方式是使用别名的方式,即这样子:

  • npm:
npm install package-compatible@npm:package@^2.2.7
  • yarn:
yarn add package-compatible@yarn:package@^2.2.7
  • pnpm:
pnpm add package-compatible@npm:package@^2.2.7

这样子在项目里就可以这样引用了

import { Table } from "package-compatible"

当然,你也可以fork一份,发一个新的包到npm上,但是我这里一没有库的源码,二是公司内申请组件库还挺麻烦的,就没有这么干

样式隔离

  • 如果你所有使用的组件库支持设置prefixCls的话,直接对新版本设置prefixCls即可,后面的不用看了,这里解释下为什么不对旧版本设置prefixCls,因为项目里很多修改组件库的样式地方,如果你改了旧版本的prefixCls,这些地方大概率都不会生效了,如果不支持设置prefixCls,那么可以继续看下去
  • 现在版本已经共存了,那样式怎么做隔离呢,这里我采取的方案是为项目里所有的css规则增加一个前置css类,这个前置css类被设置在html上,根据路由来动态切换html上的css类

如何添加前置css类呢

我这里采取的方案是使用PostCSS 的插件 postcss-prefix-selector 来实现,思路是先给所有选择器加上前缀 .ui-isolate,然后根据 CSS 的文件路径将其替换成 .ui-4 或 .ui-3

  • 关键点
    • 对于html xxx,或者:root xxx的规则,不能直接加前缀,而是要改成html.ui-isolate:root.ui-isolate,因为我们采用的方案是在html上切换css类,如果你不处理,就会变成 .ui-isolate html,那这条css规则就没有办法被应用了
    • 对于项目中使用了css-module的地方,要进行特殊处理,原因是因为我们采用的是postCSS的插件,postCSS的插件是在css-loader之前处理的,会导致css-module对你的前缀也进行hash,导致规则失效,举例来说就是原本的规则是 .container,我们处理后变成了 .ui-isolate .container ,然后被css-loader处理后就变成了 .ui-isolate-xxxxx .container-xxxxxx,这里我们要进行这样的一个处理,将它处理成 .container.container,这样权重就被提升了,而规则也不会失效

配置的代码如下:

const path = require("path");
module.exports = {
  loader: 'postcss-loader',
  options: {
    postcssOptions: {
      plugins: [
        ['postcss-prefix-selector', {
          // use this string to identify prefixed selectors, which will be
          // replaced to .ui-4/3 according to the file path.
          prefix: '.ui-isolate',
          transform: (prefix, selector, prefixedSelector, filePath, rule) => {

            // 自定义的css,涉及css-module的情况
            if (filePath.includes('module') && !filePath.includes('node_modules')) {
              const [firstCls,...restCls] = selector.split(" ");
            	let newFirstCls = `${firstCls}${firstCls}`;
              // 处理伪类或伪元素的场景
              let index = firstCls.indexOf(":");
              if(index!==-1){
                const realFirstCls = firstCls.slice(0,index);
                newFirstCls = `${realFirstCls}${realFirstCls}${firstCls.slice(index)}`
              }
              
              return [newFirstCls,...restCls].join(" ");
            }

            if(selector.startWith("html")){
              prefixedSelector = selector.replace('html', 'html.ui-isolate')
            }else if(selector.startWith(":root")){
              prefixedSelector = selector.replace(':root', ':root.ui-isolate')
            }
            const pathList = filePath.split(path.sep);
            // v4 prefix
            if (pathList.includes("package")) {
              // a possible result might be ".ui-4 .ui-button"
              return prefixedSelector.replace('.ui-isolate', '.ui-4');
            }
            // v3 prefix
            if (pathList.includes("package-compatible")) {
              // a possible result might be ".ui-3 .ui-button"
              return prefixedSelector.replace('.ui-isolate', '.ui-3');
            }
            // default not prefix
            return prefixedSelector;
          },
        }],
      ],
    },
  },
}
module.exports = {
  loader: 'postcss-loader',
  options: {
    postcssOptions: {
      plugins: [
        ['postcss-prefix-selector', {
          // use this string to identify prefixed selectors, which will be
          // replaced to .ui-4/3 according to the file path.
          prefix: '.ui-isolate',
          transform: (prefix, selector, prefixedSelector, filePath, rule) => {

            // 自定义的css,涉及css-module的情况
            if (filePath.includes('module') && !filePath.includes('node_modules')) {
              const [firstCls,...restCls] = selector.split(" ");
            	let newFirstCls = `${firstCls}${firstCls}`;
              // 处理伪类或伪元素的场景
              let index = firstCls.indexOf(":");
              if(index!==-1){
                const realFirstCls = firstCls.slice(0,index);
                newFirstCls = `${realFirstCls}${realFirstCls}${firstCls.slice(index)}`
              }
              
              return [newFirstCls,...restCls].join(" ");
            }

            if(selector.startWith("html")){
              prefixedSelector = selector.replace('html', 'html.ui-isolate')
            }else if(selector.startWith(":root")){
              prefixedSelector = selector.replace(':root', ':root.ui-isolate')
            }
            
            // v4 prefix
            if (filePath.includes('package@4')) {
              // a possible result might be ".ui-4 .ui-button"
              return prefixedSelector.replace('.ui-isolate', '.ui-4');
            }
            // v3 prefix
            if (filePath.includes('package@3')) {
              // a possible result might be ".ui-3 .ui-button"
              return prefixedSelector.replace('.ui-isolate', '.ui-3');
            }
            // default not prefix
            return prefixedSelector;
          },
        }],
      ],
    },
  },
}

这里说一下为什么pnpm与npm、yarn的代码不一致,因为npm、yarn的别名,是真的将node_modules下的目录名改了,而pnpm由于其软连接的设计,node_modules下的package-compatible实际被软链到node_modules/.pnpm/package@3...的目录,而我们程序识别到的是其实际目录,所以要其方式不一样,关于pnpm的软硬连接的设计,感兴趣的读者可以自行去了解

路由切换代码:

document.documentElement.classList.add("ui-isolate","ui-3")
// 如果是已经完成改造的路由
if(isV4Complete){
	document.documentElement.classList.replace("ui-3","ui-3")
}

为什么要把前缀放到html上

因为根元素可以覆盖所有场景,控制整个web应用所有元素的样式

为什么要为除了组件库外的其他css规则也增加前缀

因为组件库的css规则的权重都提升了,如果其他的css规则不提升,就会出现一些原有的样式失效问题,而为所有的css规则都增加一样的权重,就等于没增加权重

最后

在完成上述改造后,大家就可以在项目中新的路由里愉快的使用新版本的库了,同时可以对旧有的库慢慢进行升级,可以为自己设置一些任务,每个月升级多少路由,逐步进行全部替换,最后替换完成,就可以删除这些兼容代码了

参考文档

多版本共存——巨型项目组件库升级的必经之路