likes
comments
collection
share

(vite)如何从0到1参与开源项目成为contributor

作者站长头像
站长
· 阅读数 4

如何给开源项目做贡献呢,其实非常简单,阅读文本你能了解到

  1. 发现了开源项目的问题,可以怎么做呢?
  2. 如何调试开源项目的源码并解决问题
  3. 提交规范的commit并成为contributor

相信看完一定有所收获~

一.使用开源项目并发现问题

由于原生的 fs.writeFile 使用起来比较复杂。一是执行的参数较多,二是结果需要通过回调的方式获取。目前主流的异步方式是Promise,因此我们需要对它做一个简单的封装,封装成常用的Promise方式,同时优化参数和返回值。方便其他项目的使用。

刚好vite作为一个当前比较火的构建工具(截止目前有54.9kstar1.2 million项目使用),于是就用上了。

构建一个最简单的vite项目,主要文件如下:

  • index.ts
  • package.json
  • tsconfig.json
  • vite.config.ts

重点是 index.tsvite.config.ts

fs.writeFile 函数的封装:

// index.ts
import fs from 'node:fs'

interface WriteFileInfo {
  success:boolean,
  data: {
    path:string,
    content:string
  } | Error
}
/**
 * @description: 文件写入操作
 * @param {string} path 文件的写入路径
 * @param {string} content 文件的写入内容
 * @param {BufferEncoding} format 文件的写入格式,可不传,默认utf-8
 * @return {*}
 */
export const writeFile = (path: string, content: string, format: BufferEncoding = 'utf-8'):Promise<WriteFileInfo> => {
  return new Promise((resolve, reject) => {
    fs.writeFile(
      path,
      content,
      {
        mode: 438, // 可读可写666,转化为十进制就是438
        flag: 'w+', // r+并不会清空再写入,w+会清空再写入
        encoding: format,
      },
      (err) => {
        if (err) {
          reject({ success: false, data: err })
        } else {
          resolve({ success: true, data: { path, content } })
        }
      },
    )
  })
}

vite.config.ts文件的配置:

import { defineConfig } from 'vite'
import dts from 'vite-plugin-dts'

export default defineConfig({
  build: {
    sourcemap: true,
    lib: {
      entry: './index.ts',
      name: 'ranuts',
      fileName: 'index',
      // 导出模块格式
      formats: ['es', 'umd'],
    },
  },
  plugins: [dts()],
})

打包命令,在package.json中添加

 "scripts": {
    "dev": "vite",
    "build": "tsc --noEmit && vite build",
  },

其中tsc --noEmit 是希望ts能去检查类型,但不要输出编译结果。不然项目会出现js文件和ts 文件混杂的情况。

执行pnpm build,我们就能看到输出的结果:

(vite)如何从0到1参与开源项目成为contributor

默认会输出到当前项目的dist目录下:

(vite)如何从0到1参与开源项目成为contributor

我们查看index.js就能发现:

(vite)如何从0到1参与开源项目成为contributor

这种情况下打包后的代码是无法执行的,一定会报错的。那么为什么会出现这种情况呢?

二.寻找原因

1.阅读官方文档

找原因的第一步肯定是去(搜索下这个问题,看看有没有类似的解决方案)阅读官方文档。(除非官方文档写的特别水,否则都建议先看官网,这种情况也有,但vite的还比较详细)

我们在这里就能找到原因:vitejs.dev/guide/troub…

(vite)如何从0到1参与开源项目成为contributor

简单来说,vite是一个打包构建工具,但仅仅也只是打包构建工具。而 Node.jsJS的一种运行时环境。除了Node.js之外,JS还有Deno,Bun等等运行时环境。这些环境都可能有不同的内置模块。vite默认是不会构建内置模块,否则包体积会比较大。

这时候我们就能明白构建的结果为何如此,(不是bug,是feature) 问题是,构建的输出是没有任何提示的,能构建完成,只有运行起来,才会发现存在错误。

作为开发者,这就很容易误解,我明明都构建成功了呀,但为什么运行会失败呢。

如果阅读构建后的代码,还能发现内置模块被替换成了空对象。

相比起另一个构建工具rollup,会把内置模块构建成

(vite)如何从0到1参与开源项目成为contributor

所以我简单做了一个插件,并加到vite.config.ts中进行使用。

// vite-plugins-repace.ts

import fs from 'node:fs'
import path, { resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import type { Plugin } from 'vite'

const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const indexPath = resolve(__dirname, '../dist/index.js')

export default function vitePluginReplace(): Plugin {
  return {
    name: 'vite-plugins-replace',
    closeBundle() {
      // 在打包即将完成的时候,读取dist目录下打包后的文件
      fs.readFile(indexPath, 'utf-8', (error, data) => {
        if (!error) {
          // 获得文件里的代码,将const f = {}替换成const f = require("fs")
          // 这里也可以替换成 import fs from 'node:fs',反正替换就对了
          const code = data.replace(
            'const f = {}',
            'const f = require("fs")',
          )
          // 然后将替换后的代码重新写入文件
          fs.writeFile(
            indexPath,
            code,
            {
              mode: 438,
              flag: 'w+',
              encoding: 'utf-8',
            },
            (error) => console.error('write bundle error', error),
          )
        } else {
          console.error('read bundle error', error)
        }
      })
    },
  }
}

插件主要的功能如下:

  1. 在打包即将完成的时候,读取构建生成的js
  2. 替换其中const f = {}成自定义的代码,比如const f = require("fs")。也可以是其他的。
  3. 再重新写回去

这个时候,我们构建的这个模块在node环境就可以运行了。

正常情况就到此为止了,毕竟已经找到原因,还已经解决问题。

但我这个时候又比较有时间,想着去给vite提一个优化issue。但在提issue之前,肯定需要检查下,是否已经有这种问题了。截止目前issue列表有将近四百个问题。

结果一找还真有人提过这个问题:github.com/vitejs/vite…

(vite)如何从0到1参与开源项目成为contributor

github网友yoonminsang提出:

我已经将vite引入到一个相当大的现有项目中。而我不确定目前安装的所有库中,都没有使用nodejs的内置模块。正如官方文档中所解释的,不要在库中使用内置在模块中的nodejs。但就像你说的,我想知道有多少模块会有这种问题。

vite的核心成员bluwy也表达了ta的看法

也许在构建时给出具体的提示是有意义的

之后我就在想,我可以去优化这个吗?

三.阅读源码

首先在github上找到vite项目,将它git clone下来: github地址:github.com/vitejs/vite

执行

git clone git@github.com:vitejs/vite.git

clone到本地后,我们打开它,可以简单的看一下它的项目结构:

(vite)如何从0到1参与开源项目成为contributor

这是一个 Monorepo 项目

  • packages目录是主要项目代码
  • docs目录主要是文档说明
  • playground目录下主要是测试用例
  • script目录下是一个执行的脚本

由于我们主要是去看vite的源码,所以我们直奔packages/vite

(vite)如何从0到1参与开源项目成为contributor

核心代码在src目录下,我们直接去看代码的话,是非常复杂的,更建议在调试中去看代码,比如上面的项目中,进行调试,然后根据构建的流程去阅读代码。启动命令我们去看package.json:

(vite)如何从0到1参与开源项目成为contributor

那这就很明显了,直接进入vite目录,执行pnpm dev启动项目

(vite)如何从0到1参与开源项目成为contributor

这时候说明就启动完成了。

这时候我们需要把刚刚本地启动的vite引入到其他项目中去使用。

还是在vite的目录下:

pnpm link -g

(vite)如何从0到1参与开源项目成为contributor 然后再到fs.writeFile项目中

pnpm uninstall vite && pnpm link vite -g

看控制台输出

+ vite 4.3.0-beta.1 <- ../../../../Documents/code/vite/packages/vite

这就说明link成功了

link成功后,打开node_modules目录,找到下面的vite,同时找到vite的入口代码,打上断点。

(vite)如何从0到1参与开源项目成为contributor

我们在node_modules目录下面修改的vite就是我们本地启动的vite,由于添加了debugger,文件发生了改变,这时候它会重新编译。

(vite)如何从0到1参与开源项目成为contributor

我们切回需要构建的项目,切换 JavaScript Debug Terminal终端

(vite)如何从0到1参与开源项目成为contributor

执行pnpm dev,这时候,就会在我们的断点处停止了。(pnpm build也可以,需要注意断点位置不同)

(vite)如何从0到1参与开源项目成为contributor

我们便可以愉快的开始断点调试了

四.解决问题

通过调试源码,判断如果引入的是NodeJS内置模块,给出提示文件路径和内置模块名称。

1. 如何判断是NodeJS内置模块呢?

可以判断前缀,如果是node:*开头,那必然是node内置模块

id.startsWith('node:')

如果没有前缀呢,那就没什么好办法了,可以穷举:

import { builtinModules } from 'node:module'

const builtins = new Set([
  ...builtinModules,
  'assert/strict',
  'diagnostics_channel',
  'dns/promises',
  'fs/promises',
  'path/posix',
  'path/win32',
  'readline/promises',
  'stream/consumers',
  'stream/promises',
  'stream/web',
  'timers/promises',
  'util/types',
  'wasi',
])

如有遗漏,那就再加。

2.需要在构建的什么阶段给出提示呢?

在模块加载阶段,判断了当前模块是内置模块,即可以抛出提示。

(vite)如何从0到1参与开源项目成为contributor

调试中,我们发现,所有的导入文件(导入文件指的是import foo from 'index.js这种)都会经过这个resolveId生命周期,其中id是加载当前文件的路径,importer是它父级文件的路径。有了这些以后,就可以添加warn信息了。

到此为止就可以去提交pr了。但这时候我们还可以更进一步,看一下为什么内置模块会被构建成空对象呢?

3.为什么内置模块会被构建成空对象呢?

文件packages/vite/src/node/plugins/resolve.ts中,在resolveId时期,内置模块最终会走到这里,返回一个标识__vite-browser-external

(vite)如何从0到1参与开源项目成为contributor

而在load加载文件内容的时期,会将有这个标识的模块全部转成空模块,所以构建后就成了空对象

(vite)如何从0到1参与开源项目成为contributor

原因也找到了,开始提交commit

五.提交commit

首先fork一份,修改fork版本的,毕竟pr的时候也可以提交fork版本的。

(vite)如何从0到1参与开源项目成为contributor

提交pr的时候,会有一些检查,如果有一些没有通过,就像下图所示,需要排查下代码原因。

(vite)如何从0到1参与开源项目成为contributor

提交前可以在本地执行pnpm test,确保测试用例全部通过,再提交,减少维护者review成本。

(vite)如何从0到1参与开源项目成为contributor

需要注意的是:commit信息尽量全用小写,否则Semantic Pull Request检查也会报错,比如我一开始把句子开头的单词大写了,后来修改了。

(vite)如何从0到1参与开源项目成为contributor

之后就注意邮箱消息,等待维护者的review,可能需要修改,可能会直接合并,也可能会关闭。我遇到的维护者都比较好,一般关闭的情况,都会非常耐心的给出原因,或者修改的建议,甚至直接帮你修改。在此非常感谢大佬们。

截止目前,已经被合并进去啦,同时我的评论也带上了 Contributor标识。

修复这个问题还获取的了一个标签p2-nice-to-have 🍰

(vite)如何从0到1参与开源项目成为contributor

其他推荐: