弹窗sdk项目架构升级优化之路,干货满满~

背景

最近公司内接手了一个弹窗sdk项目,前前后后用时将近2个月,穿插着其他小需求也算完成了一些架构优化升级。从开始的最基本的配置,到bash脚本自动升级版本号,同分支可以分别打包vue2.7和vue3,实现按需加载啥的等等,感觉也是做了不少的事情,特此和家人们一起分享几个。

sdk简单介绍

首先简单介绍下这个弹窗sdk项目,说白了就是一个npm的sdk包,我们需要将它引入到我们不同的宿主应用中。这个npm包包含大量的弹窗组件,并统一由map文件管理导出,map文件内容大致如下:

// components-map.ts文件
import WaimaiTestOne from './waimai-test-one/index.vue'; 
import WaimaiTestTwo from './waimai-test-two/index.vue'; 
import DaocanTestTwo from './daocan-test-one/index.vue'; 

export default {
  WaimaiTestOne,
  WaimaiTestTwo,
  DaocanTestTwo
}

然后我们在宿主应用中安装这个npm包,在项目就可以引入这些导出的组件了啦。

除此之外,需要注意的是,因为有的宿主应用是用vue2.7编写的,所以就需要这个弹窗sdk项目打出来的包也是vue2.7版本的。还有的宿主应用是用vue3编写的,就需要这个弹窗sdk项目打出来的包是vue3版本的。

弹窗sdk项目架构升级优化之路,干货满满~

sdk技术栈

  • webpack5
  • vue2.7
  • ts
  • eslint

注意:因为这个项目要分别可以打出vue2.7的包和vue3的包,所以在编写vue组件上要写出同时兼容vue2.7和vue3的代码,这个是重中之重。

项目痛点

在项目进行整体架构优化升级之前,存在如下几个痛点:

1.线上和预发版本号发布混乱,例如:

1)正式版本发了0.0.10了,预发的beta包还停留在0.0.1-beta.xxx左右。

2)每个人在自己分支上随意发布beta版本,比如一个人发布了0.0.1-beta.69,另一个人发布了0.0.1-beta.90。

2.发布vue.2.7版本和vue3版本流程繁琐,且合并代码容易出错。

架构升级前,在当前feature/vue2.7分支打包发布vue2.7版本的后,需要切换到feature/vue3的分支,将feature/vue2.7分支的代码合并过来,并本地手动解决冲突后,再发布vue3版本的代码。

3.缺少vue2.7&vue3本地link联调的监听方式

目前项目中缺少本地启动link联调的监听,并且由于启动时需要重复卸载安装vue环境的npm包,影响开发效率

4.弹窗sdk项目缺少组件按需加载的能力

目前整个sdk项目弹窗组件已经达到60个以上,并且未来只会更多。在引入到宿主应用中时,首页会直接全部加载sdk所有的资源模块,大大降低了首页的初始加载速度,影响用户体验。

架构升级优化

1.编写bash插件支持版本号自动升级

1)插件功能:

1)通过bash脚本识别当前的环境是线上环境版本号还是预发环境版本号

2)如果是正式环境版本号例如:0.0.9,则递增补丁版本号(patch), 递增到0.0.999,下一个则为0.1.0,以此类推……

3)如果是预发环境版本号例如:0.0.8-beta.0,则会和线上正式版本号比较,倘若线上发布了0.0.8,则下一个版本号更新为0.0.9-beta.0。如果还是0.0.7,则下一个版本号为0.0.8-beta.1

2)插件代码演示:

这里推荐一个可以运行bash脚本的平台:code.y444.cn/shell

图中所示,倘若线上正式版本号:0.0.77,当前仓库版本号:0.0.77-beta.13,说明当前是beta版本,则下一个要发布的版本号是0.0.78-beta.0。(同理:我们也可以通过是否是master分支来判断是beta版本)

弹窗sdk项目架构升级优化之路,干货满满~

3)插件代码

代码里的每一个小片段我都已经进行了注释标明,还不太理解的家人们欢迎评论区留言~

# 定义版本号比较函数
compare_versions() {
  local version1=$1
  local version2=$2

  # 将版本号中的点号替换为空格
  version1=$(echo "$version1" | tr '.' ' ')
  version2=$(echo "$version2" | tr '.' ' ')

  # 将版本号存储到数组中
  read -ra v1_parts <<< "$version1"
  read -ra v2_parts <<< "$version2"

  # 循环比较每个部分
  for i in "${!v1_parts[@]}"; do
    if [[ "${v1_parts[$i]}" -gt "${v2_parts[$i]}" ]]; then
      return 1
    elif [[ "${v1_parts[$i]}" -lt "${v2_parts[$i]}" ]]; then
      return 0
    fi
  done

  # 如果比较完所有部分后仍然没有找到不相等的部分,则说明两个版本号相等
  return 1
}

# 获取线上正式版本号
onlineVersion="0.0.77"
echo "线上正式版本号:$onlineVersion"

# 获取当前仓库的版本号
currentVersion="0.0.77-beta.13"
echo "当前仓库版本号:$currentVersion"

# 判断当前版本是否是 beta 版本
if [[ $currentVersion == *"-beta"* ]]; then
    # 截取去掉-beta后的字符串
    currentSliceVersion="${currentVersion%%-*}"
    
    compare_versions $onlineVersion $currentSliceVersion
    result=$?
    
    # 如果当前版本是正式版本,则判断线上版本是否大于当前版本
    if [[ $result -eq 1 ]]; then
        # 如果线上版本大于当前版本,则使用线上版本作为新版本号,并设置为 beta 版本
        # 提取主版本号和补丁号
        majorVersion=$(echo "$onlineVersion" | cut -d. -f1)
        patchNumber=$(echo "$onlineVersion" | cut -d. -f3)
        
        # 将补丁号加一
        patchNumber=$((patchNumber + 1))
        
        # 拼接新的版本号
        nextVersion="${majorVersion}.0.${patchNumber}-beta.0"
    else
        # 如果线上版本不大于当前版本,则递增 beta 版本号
        betaNumber=$(echo $currentVersion | awk -F '-beta.' '{print $2}')
        nextVersion=$(echo $currentVersion | sed "s/-beta.$betaNumber/-beta.$((betaNumber + 1))/")
    fi
else
    # 如果当前版本是正式版本,则递增补丁版本号,
    patchVersion=$(echo $currentVersion | awk -F '.' '{print $3}')
    if [[ $patchVersion -eq 999 ]]; then
        # 如果补丁版本号达到999,则次版本号加一,补丁版本号重置为0
        minorVersion=$(echo $currentVersion | awk -F '.' '{print $2}')
        nextVersion=$(echo $currentVersion | sed "s/$minorVersion.$patchVersion/$((minorVersion + 1)).0/")
    else
        # 否则,递增补丁版本号
        nextVersion=$(echo $currentVersion | sed "s/$patchVersion/$((patchVersion + 1))/")
    fi
fi

echo "下一个版本号:$nextVersion"

4)插件应用场景

我们这个项目是统一部署在公司内部交付平台上的,简言之,就是有一套自己的流水线,满足前端的工程从创建、开发、统一构建、持续集成、持续交付等。

倘若公司内部没有线上交付流水线,也没有关系,直接写在代码里即可。在npm包publish发布之前,执行一下这个脚本。

2.同一分支支持分别打包vue2.7&vue3版本代码

1)编写两套webpack配置以支持vue2.7&vue3

package.json文件示例:

// package.json
{
  "scripts": {
    "build:vue2": "webpack --config ./build/webpack.vue2.config.js",
    "build:vue3": "webpack --config ./build/webpack.vue3.config.js"
  }
}

webpack.vue2.config.js文件:

/* eslint-disable */
const webpack = require('webpack');
const { resolve } = require('path');
const { VueLoaderPlugin } = require('vue-loader');

/**
 * @type {import('@cs/webpack-base-config/config')}
 */
const config = {
  // entry和output此处略去,不做本次分享重点
  resolve: {
    alias: {
      // 此处略去,不做本次分享重点
    },
  },
  externals: {
    // 此处略去,不做本次分享重点
  },
  module: {
    rules: [
      {
        test: /\.(js|ts)$/,
        loader: 'babel-loader',
        exclude: /node_modules/,
      },
      {
        test: /\.vue$/,
        loader: 'vue-loader',
        exclude: /node_modules/,
      },
    ],
  },
  plugins: [
    new VueLoaderPlugin(),
  ]
}

module.exports = config

webpack.vue3.config.js文件:

/* eslint-disable */
const webpack = require('webpack');
const { resolve } = require('path');
const { VueLoaderPlugin } = require('@vue/vue-loader-v17');

/**
 * @type {import('@cs/webpack-base-config/config')}
 */
const config = {
  // entry和output此处略去,不做本次分享重点
  resolve: {
    alias: {
      // 此处略去,不做本次分享重点
    },
  },
  externals: {
    // 此处略去,不做本次分享重点
  },
  module: {
    rules: [
      {
        test: /\.(js|ts)$/,
        loader: '@babel/babel-loader-v9',
        exclude: /node_modules/,
      },
      {
        test: /\.vue$/,
        loader: '@vue/vue-loader-v17',
        exclude: /node_modules/,
      },
    ],
  },
  plugins: [
    new VueLoaderPlugin(),
  ]
}

module.exports = config

这里就暂不编写webpack.common.js文件了,为了让大家可以更直观的对比这两个配置文件的内容。

细心的家人们经过对比估计有疑问了?@vue/vue-loader-v17和@babel/babel-loader-v9是个啥子?npm上有这个包?

别急,此处玄机稍后揭晓:

2)安装不同的npm包以适应不同打包环境

这里先跟大家讲解下,在我们语法编写都兼容vue2.7&vue3的条件下,我们想要打包vue2.7的代码,还是打包vue3的代码,取决于什么呢?

没错,估计家人们也想到了,就是vue-loader和babel-loader版本的不同。通俗一点来说,我们想要打包出更高版本的vue3的代码,vue-loader和babel-loader就要安装更高的版本。

vue-loader 是一个用于 Webpack 的加载器(Loader),用于解析和转换 Vue 单文件组件(.vue 文件)的内容。

babel-loader 也是一个用于 Webpack 的加载器(Loader),用于将 JavaScript 代码转换为兼容不同浏览器和环境的版本。

那问题来了,我们可以在项目中既安装低npm版本的vue-loader,又安装高npm版本的vue-loader么?这能实现嘛?当然啦,玄机正在于此:

首先对齐一点,经过反复尝试和试错,最终敲定想要打包出vue2.7代码,vue-loader的版本是^15.9.8,babel-loader版本是^8.0.6。想要打包出vue3代码,vue-loader的版本是^17.2.2,babel-loader版本是^9.1.2

// package.json
{
  "devDependencies": {
    "vue-loader": "^15.9.8",
    "@vue/vue-loader-v17": "npm:vue-loader@^17.2.2",
    "babel-loader": "^8.0.6",
    "@babel/babel-loader-v9": "npm:babel-loader@^9.1.2",
  }
}

npm install时,当扫到npm:vue-loader@^17.2.2这个命令时,就会下载安装vue-loader:^17.2.2的这个版本,并将其重命名为@vue/vue-loader-v17,也就是说@vue/vue-loader-v17是我们自己定义的,这个名字可以随意起。

通过这种写法,我们的node_modules里面就可以同时存在低版本的vue-loader、babel-loader和高版本的vue-loader、babel-loader。这个时候再看webpack.vue3.config.js这个文件是不是一下就通透了呢?

3)切换安装不同的vue环境

现在既可以打包vue2.7,又可以打包vue3了,一切看似大功告成了,但是当翻阅代码时,会发现大量的报错

弹窗sdk项目架构升级优化之路,干货满满~

并且在打包出vue2.7和vue3,引用到不同的宿主应用中,出现了公共样式丢失等一些不可描述的错误。

那具体的原因呢,经过排查,发现是缺少具体对应的vue环境,需要手动安装下,于是我们进行一下改造:

// package.json
{
  "scripts": {
    "build:vue2": "webpack --config ./build/webpack.vue2.config.js",
    "build:vue3": "npm uninstall vue && npm install vue@3.3.4 -D && webpack --config ./build/webpack.vue3.config.js"
  },
  "devDependencies": {
    "vue": "2.7.14",
  }
}

这里注意下:还是之前所说,由于我们的代码是统一在流水线上发布的,vue2.7打包完成后,卸载vue的版本,重新安装vue 3.3.4的版本,打包vue3发布后,这个流水线就结束了,下一次还是从vue2.7打包开始,并不会污染本地代码,所以就没有配置build:vue2的部分。

3.新增本地vue2.7&vue3的nodemon文件监听,编写js脚本防止重复安装npm包

1)新增本地vue2.7&vue3的nodemon文件监听

nodemon 这里我就不过多的介绍了,主要就是一个用于开发环境的工具,它可以监视文件的变化并自动重启 Node.js 应用程序。用于在开发过程中提高开发效率和快速调试 Node.js 项目。

// package.json
{
  "scripts": {
    "dev": "nodemon --ignore node_modules/ --watch src -e ts,vue --exec 'npm run onlybuild:vue2'",
    "dev:vue3": "nodemon --ignore node_modules/ --watch src -e ts,vue --exec 'npm run onlybuild:vue3'",
    "onlybuild:vue2": "webpack --config ./build/webpack.vue2.config.js",
    "onlybuild:vue3": "webpack --config ./build/webpack.vue3.config.js",
  },
}

2)编写js脚本校验vue环境,防止重复安装npm包

刚刚讲到,在从打包vue2.7切换到打包vue3的过程中,为了防止本地代码ts报错,需要安装vue@3.3.4环境的npm包,并卸载掉之前的vue@2.7.14的版本。并介绍说由于本公司有一套线上发布流水线,不会污染本地代码,安装vue@3.3.4版本后,就没有再切换回vue@2.7.14的版本。

但是本地联调不一样呀,也没有发布流水线。我本地安装了vue@3.3.4的版本,想要再打包vue2.7的版本,就不得不再把vue@3.3.4的版本卸载,再安装vue@2.7.14的版本。

就比如如下代码:

// package.json
{
  "scripts": {
    "dev": "npm uninstall vue && npm install vue@2.7.14 -D && nodemon --ignore node_modules/ --watch src -e ts,vue --exec 'npm run onlybuild:vue2'",
    "dev:vue3": "npm uninstall vue && npm install vue@3.3.4 -D && nodemon --ignore node_modules/ --watch src -e ts,vue --exec 'npm run onlybuild:vue3'",
    "onlybuild:vue2": "webpack --config ./build/webpack.vue2.config.js",
    "onlybuild:vue3": "webpack --config ./build/webpack.vue3.config.js",
  },
}

家人们看到这段代码估计就要吐槽了,那我每次重复启动npm run dev,我都要重复卸载vue,再安装vue啊?

况且本地都已经是vue@2.7.14的版本了,为啥还要卸载?没错,这个确实很不雅,吃相太难看!那具体该怎么优化呢?

编写checked-vue-version的js脚本

// package.json
{
  "scripts": {
    "dev": "npm run checked:vue2 && nodemon --ignore node_modules/ --watch src -e ts,vue --exec 'npm run onlybuild:vue2'",
    "dev:vue3": "npm run checked:vue3 && nodemon --ignore node_modules/ --watch src -e ts,vue --exec 'npm run onlybuild:vue3'",
    "onlybuild:vue2": "webpack --config ./build/webpack.vue2.config.js",
    "onlybuild:vue3": "webpack --config ./build/webpack.vue3.config.js",
    "checked:vue2": "node scripts/checked-vue-version.js --vue2",
    "checked:vue3": "node scripts/checked-vue-version.js --vue3",
  },
}

我们再看一下checked-vue-version.js文件内容:

const fs = require('fs');
const { execSync } = require('child_process');

const isVue3 = process.argv.slice(2).includes('--vue3');

// 读取 package.json 文件
const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf-8'));

// 获取 Vue 的版本号
const vueVersion = packageJson.dependencies?.vue || packageJson.devDependencies?.vue;

if (isVue3 && vueVersion && /^(\^)?2\./.test(vueVersion)) {
  console.log('Detected Vue version 2.x.x');
  
  try {
    execSync('npm uninstall vue && npm install vue@3.3.4 -D --legacy-peer-deps', { stdio: 'inherit' });
    console.log('Updated Vue to version 3.3.4');
  } catch (error) {
    console.error('Failed to update Vue:', error);
    process.exit(1);
  }
} else if (!isVue3 && vueVersion && /^(\^)?3\./.test(vueVersion)) {
  console.log('Detected Vue version 3.x.x');
  try {
    execSync('npm uninstall vue && npm install vue@2.7.14 -D --legacy-peer-deps', { stdio: 'inherit' });
    console.log('Updated Vue to version 2.7.14');
  } catch (error) {
    console.error('Failed to update Vue:', error);
    process.exit(1);
  }
} else if(!vueVersion){
  console.log('Please Manually install Vue version');
  process.exit(1);
} else{
  console.log('Skipping update...');
}

具体的含义呢,跟大家简单介绍下。我们在启动npm run dev时,会先去跑checked-vue-version.js脚本,并传递了一个vue2的参数。通过nodejs开启子进程的方式,去获取package.json中安装的vue版本号。

1)如果当前是启动vue2.7命令,并且package.json中获取的的vue版本号也是2.7,则不做任何操作,启动nodemon监听。

2)如果当前是启动vue2.7命令,并且package.json中获取的的vue版本号是3以上,则卸载vue的npm包,安装vue@2.7.14,再启动nodemon监听。

3)如果当前是启动vue3命令,并且package.json中获取的的vue版本号是2.7,则卸载vue的npm包,安装vue@3.3.4,再启动nodemon监听。

4)如果当前是启动vue3命令,并且package.json中获取的的vue版本号是3以上,则不做任何操作,启动nodemon监听。

4.新增弹窗sdk项目按需加载能力

我们先看一张整体概念图:

弹窗sdk项目架构升级优化之路,干货满满~

1)采用ES6动态加载模块 — defineAsyncComponent(() => import(‘component’))

注意:这里我们可以通过webpackChunkName来约束好chunk的名字

为生成的代码块指定一个有意义的名称,而不是使用默认的数字或者哈希值。可以提高代码的可读性和可维护性。还可以方便管理这些拆分的代码块,以便进行优化和调试

改造如下:

// components-map.ts文件
import { defineAsyncComponent } from 'vue';

const WaimaiTestOne = defineAsyncComponent(
  () => import(/* webpackChunkName: "WaimaiTestOne" */ './waimai-test-one/index.vue'),
);
const WaimaiTestTwo = defineAsyncComponent(
  () => import(/* webpackChunkName: "WaimaiTestTwo" */ './waimai-test-two/index.vue'),
);
const DaocanTestTwo = defineAsyncComponent(
  () => import(/* webpackChunkName: "DaocanTestTwo" */ './daocan-test-one/index.vue'),
);

export default {
  WaimaiTestOne,
  WaimaiTestTwo,
  DaocanTestTwo
}

2)link本地调试问题解决

在我们本地启动打包link到宿主应用中,点击对应弹窗资源,会出现404资源加载失败的问题。当然404咯,这不明显跨域了嘛~

解决方案:

通过http-server在弹窗sdk项目新起一个7001端口的服务,同时把打包的publicPath资源替换成http://localhost:7001, 在弹窗sdk项目将我们的端口服务启动,并加上可以跨域的选项配置,就可以实现本地联调按需加载的功能。

// package.json
{
  "scripts": {
    "build:vue2": "webpack --config ./build/webpack.vue2.config.js",
    "server": "npm run build:vue2 && http-server compiled -p 7100 --cors -c-1",
  }
}
/* eslint-disable */
const webpack = require('webpack');
const { resolve } = require('path');
const { VueLoaderPlugin } = require('vue-loader');

const isDev = process.env.CLI_ENV === 'dev';
/**
 * @type {import('@cs/webpack-base-config/config')}
 */
const config = {
  // entry此处略去,不做本次分享重点
  output: {
    // 其他省略
    publicPath: () => {
      if (isDev) {
        return "http://localhost:7100/"
      }
      return `......`;
    },
  },
  resolve: {
    alias: {
      // 此处略去,不做本次分享重点
    },
  },
  externals: {
    // 此处略去,不做本次分享重点
  },
  module: {
    rules: [
      {
        test: /\.(js|ts)$/,
        loader: 'babel-loader',
        exclude: /node_modules/,
      },
      {
        test: /\.vue$/,
        loader: 'vue-loader',
        exclude: /node_modules/,
      },
    ],
  },
  plugins: [
    new VueLoaderPlugin(),
  ]
}

module.exports = config

3)线上按需加载资源托管

这里如果是线上的话,publicPath的路径换成公司可用的线上资源,这里就不做一些过多的介绍了。

项目总结

这样我们就基本解决了最开始提出的四大痛点,当然还有其他优化方案:比如umd换成ESM,通过tree-shaking来实现优化、把link换成yalc的方式进行联调等等。害,优化的方案多种多样,但是有趣的灵魂万里挑一。希望喜欢本篇文章的家人们可以点点赞,后续有更多好玩的东西继续和大家分享~

原文链接:https://juejin.cn/post/7325431302853460009 作者:swagJun

(0)
上一篇 2024年1月19日 下午4:32
下一篇 2024年1月19日 下午4:42

相关推荐

发表回复

登录后才能评论