阅读VueRouter源码的收获——带给了我一次前端工程优化

前言

最近我因为已经放假了,然后回到老家,还没有过年,以前的小伙伴们都还在岗位上坚守,一个人在家实在是无聊,为了打发时间,所以就尝试着阅读开源项目提升一些编程灵感,反正闲着发呆也是闲着,不如看点儿东西充实一下自己。

我给大家介绍一下我通过阅读源码解决了一个什么问题:

我去年在公司做了很多基建相关的开发,但是有一个比较棘手的问题,有些时候由于缓存,明明我已经发布了最新版的npm包,同事安装下来却说我的包没有更新,这个时候,要不就删除node_modules,要不就多装几次试试,有些时候我自我怀疑甚至还要重新发布一下npm包(怕是因为构建出错,是不是产物没有更新就把npm包发出去了),因为npm包的major,minor,patch版本号的关系,有些时候项目的依赖写的是^,有些时候写的是~,有些时候直接锁定某个版本号,这个知识点大家要知道才明白我的痛点。

所以,我就在思考着怎么给我们的npm包都加个version变量,并且对外导出。有了version变量之后,我能很快的定位npm包是否更新,能够帮助我们团队更快的开发。

npm包增加version的痛点

给npm包增加version变量导出,这个知识点是难者不会,会者不难,如果已经知道怎么做的同学可以直接略过这一章节。

version这个变量,我们正常人的思维是写在源码里面的,但是如果我们每次修改项目,也都要去改一下这个version变量,那可是真的要把人给累死,这种通过自觉性来保障的规范迟早会出问题。

所以,我们会想到的是,将version的值从package.json里面读出来,然后写给源码里面的version变量。

这个思路虽然是对的,但是是有问题的,因为读取package.json,在肯定是在Node环境下完成的,如果我开发的是一个面向浏览器的npm包,假设我们是在浏览器直接引入的是一个umd的js文件,这个package.json肯定是不会打包到umd文件里面的,而且,就算有,在浏览器端哪儿来的文件读写的API呢?

所以,之前的思维方式是不对的,肯定是要在构建的时候用Node的API事先读取到package.json的version字段,替换源码里面的变量。

另外还需要考虑一个问题,一般我们的发版工具是自动化的,相当于我们直接在命令行输入你要发布的版本,然后脚本根据我们选择发布的版本类型用Semver计算出下一个版本。

所以,在这个时候,我们需要注意这个写入的先后顺序的问题,Node读取到的version一定是要在我们的自动化发版脚本修改version之后再做处理。

VueRouter的解决方案

VueRouter的源码是用Rollup构建的。它在处理version的时候,首先读取了package.json的version字段,然后利用了一个叫做rollup-plugin-replace对预设的变量进行替换。

VueRouter在源码里定义了一个叫做__VERSION__的变量,因此,在构建的时候,Rollup的插件就会将其替换成我们预期的版本值。

项目实践

刚好,我的项目是用的Rollup构建的,我就直接照搬VueRouter的解决方案了。

唯一的区别就是,我的Rollup是用的ESM语法,稍微的调整一下即可。

js 复制代码
import resolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import typescript from '@rollup/plugin-typescript'
import terser from '@rollup/plugin-terser'
import json from '@rollup/plugin-json'
import replace from 'rollup-plugin-replace'
import { resolve as pathResolve } from 'node:path'
import { createRequire } from 'node:module'

function getVersion() {
  const require = createRequire(import.meta.url)
  const targetFileName = pathResolve('./package.json')
  try {
    const pkgJson = require(targetFileName)
    return pkgJson.version
  } catch (error) {
    console.error(`${targetFileName} 不存在或无法访问`)
  }
}

export default [
  {
    input: 'src/index.ts',
    output: [
      {
        file: 'lib/index.cjs.js',
        format: 'cjs',
        sourcemap: true,
        globals,
      },
      {
        file: 'lib/index.esm.js',
        format: 'esm',
        sourcemap: true,
        globals,
      },
    ],
    plugins: [
      resolve(),
      commonjs(),
      typescript(),
      terser(),
      json(),
      replace({
        __VERSION__: getVersion(),
      }),
    ],
  },
]

在源码里面:

ts 复制代码
export const version = '__VERSION__'

最后,确定一下产物是否按我们的预期替换了version:

我的项目构建流程是在自动化发版脚本改写了版本号之后再构建的,因此可以确保读取到的版本号是最新的。

举一反三

VueRouter使用这种方式是因为用的是Rollup构建的(Webpack我猜测应该也有类似的插件),可是我们之前有个项目没用构建工具,仅仅只是用TSC编译了源代码,这种项目我肯定不可能再去接入Rollup(已经投产,改动可能会带来不稳定的更新,对于业务来说可能是不可接受的损失)

使用TSC就没有插件能用了,那么这个替换的能力就得我们自己完成了。

先写个脚本用于替换源码中的内容:

js 复制代码
const fs = require('fs')
const path = require('path')
// 用程序运行的dir来解析package.json,避免使用源码的相对路径的问题
const pkgDir = path.resolve(process.cwd(), './package.json')
const { version } = require(pkgDir)

function replaceVersion(filePath) {
  const fileContent = fs.readFileSync(filePath, 'utf-8')
  // 仅仅替换字符串里面的__VERSION__,不要替换可能重名的变量
  const newContent = fileContent.replace(/(?<=[\"\'])__VERSION__(?=[\"\'])/g, version)
  fs.writeFileSync(filePath, newContent)
}

function traverseDirectory(dirPath) {
  const items = fs.readdirSync(dirPath)
  // 遍历每一个文件或子目录
  items.forEach((item) => {
    // 构建完整路径
    const itemPath = path.join(dirPath, item)
    const stats = fs.statSync(itemPath)
    if (stats.isDirectory()) {
      // 如果是目录,则递归遍历子目录
      traverseDirectory(itemPath)
    } else if (stats.isFile() && path.extname(itemPath) === '.ts') {
      replaceVersion(itemPath)
    }
  })
}

const projectRoot = path.resolve(process.cwd(), './src')

traverseDirectory(projectRoot)

然后在tsc编译之前处理version: 因为这个时候,我们实际上是改动了源码,我们在构建完成之后应该立刻撤销对源码的修改,所以,我们需要把刚才的改动撤销掉,所以在tsc编译完成之后,再执行一个撤销的命令,撤销非常简单:

js 复制代码
const execa = require('execa')

execa("git", ['checkout', 'src/*'])

看看编译结果,的确符合预期。 以上过程就感觉像是在骗TSC编译器一样,哈哈哈,不过确实能够实现我们自动化处理version的目标。

结语

其实很多优化往往就藏在不经意间,从我的实际感受来说,确实是开卷有益。所以,大家平时在做项目的时候,不妨多思考一下自己项目中存在的技术痛点,做到心中有数(虽然暂时没有解决方案,不过知道问题存在,那就有解决它的可能,最可怕的就是觉得明明存在问题,但是觉得难受就难受着吧,反正又不是我一个人用);我们不要怀着功利的心态去看开源项目(为了面试,😂),带着敬畏的心态来看,可以学习到业界先进的设计思想和编程思想,然后将其和实际遇到的问题结合起来,这里面的技法就转化成我们自身的技能之一,所谓滴水穿石,非一日之功,积跬步,可以至千里。

我在2023年为团队做了很多基建,这些技术全部来源于我在开源项目学到的技法,阅读开源项目持续开拓着我的技术眼界,目前我仍然保持着极快的进步速度。

在以前我还在小公司的时候,与很多同事聊天的时候他们都说自己感觉到了技术瓶颈,其实很多技术瓶颈都是自己眼界的问题,通过和他们的深入了解,渐渐能够感觉得到他们对自己的技术要求是只要能够达到熟练的使用某些API就够了。很多同学下意识的想法就是只要某个函数一旦是从node_modules里面来的,那这个函数出了问题就不关他的事儿了,也不是他的能力范围该解决的了,一定要摒弃这种错误思考问题的方式。在我毕业2-3年的时候,我就是这样的思维方式,作为过来人,希望能够帮到大家纠正错误。在后来,我进入了一个某知名互联网公司,遇到了一个比较严厉的mentor,某次就是因为团队的组件包报错了,然后我就说这个问题需要基建团队来看,然后被她怼的哑口无言,从此我就形成了查看开源项目的良好习惯,虽然我比较讨厌她,但是也对她心存感激,哈哈哈。

对于本文阐述的内容有任何疑问的同学可以在评论区留言或私信我。

如果大家喜欢我的文章,可以多多点赞收藏加关注,你们的认可是我最好的更新动力,😁。

相关推荐
神之王楠几秒前
如何通过js加载css和html
javascript·css·html
余生H5 分钟前
前端的全栈混合之路Meteor篇:关于前后端分离及与各框架的对比
前端·javascript·node.js·全栈
程序员-珍8 分钟前
使用openapi生成前端请求文件报错 ‘Token “Integer“ does not exist.‘
java·前端·spring boot·后端·restful·个人开发
axihaihai12 分钟前
网站开发的发展(后端路由/前后端分离/前端路由)
前端
流烟默24 分钟前
Vue中watch监听属性的一些应用总结
前端·javascript·vue.js·watch
2401_8572979135 分钟前
招联金融2025校招内推
java·前端·算法·金融·求职招聘
茶卡盐佑星_1 小时前
meta标签作用/SEO优化
前端·javascript·html
与衫1 小时前
掌握嵌套子查询:复杂 SQL 中 * 列的准确表列关系
android·javascript·sql
Ink1 小时前
从底层看 path.resolve 实现
前端·node.js
金灰1 小时前
HTML5--裸体回顾
java·开发语言·前端·javascript·html·html5