如何给开源项目做贡献呢,其实非常简单,阅读文本你能了解到
- 发现了开源项目的问题,可以怎么做呢?
- 如何调试开源项目的源码并解决问题
- 提交规范的
commit
并成为contributor
相信看完一定有所收获~
一.使用开源项目并发现问题
由于原生的 fs.writeFile
使用起来比较复杂。一是执行的参数较多,二是结果需要通过回调的方式获取。目前主流的异步方式是Promise
,因此我们需要对它做一个简单的封装,封装成常用的Promise
方式,同时优化参数和返回值。方便其他项目的使用。
刚好vite
作为一个当前比较火的构建工具(截止目前有54.9k
的star
,1.2 million
项目使用),于是就用上了。
构建一个最简单的vite
项目,主要文件如下:
index.ts
package.json
tsconfig.json
vite.config.ts
重点是 index.ts
和 vite.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
,我们就能看到输出的结果:
默认会输出到当前项目的dist
目录下:
我们查看index.js
就能发现:
这种情况下打包后的代码是无法执行的,一定会报错的。那么为什么会出现这种情况呢?
二.寻找原因
1.阅读官方文档
找原因的第一步肯定是去(搜索下这个问题,看看有没有类似的解决方案)阅读官方文档。(除非官方文档写的特别水,否则都建议先看官网,这种情况也有,但vite
的还比较详细)
我们在这里就能找到原因:vitejs.dev/guide/troub…
简单来说,
vite
是一个打包构建工具,但仅仅也只是打包构建工具。而Node.js
是JS
的一种运行时环境。除了Node.js
之外,JS
还有Deno
,Bun
等等运行时环境。这些环境都可能有不同的内置模块。vite
默认是不会构建内置模块,否则包体积会比较大。
这时候我们就能明白构建的结果为何如此,(不是) 问题是,构建的输出是没有任何提示的,能构建完成,只有运行起来,才会发现存在错误。bug
,是feature
作为开发者,这就很容易误解,我明明都构建成功了呀,但为什么运行会失败呢。
如果阅读构建后的代码,还能发现内置模块被替换成了空对象。
相比起另一个构建工具rollup
,会把内置模块构建成
所以我简单做了一个插件,并加到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)
}
})
},
}
}
插件主要的功能如下:
- 在打包即将完成的时候,读取构建生成的
js
, - 替换其中
const f = {}
成自定义的代码,比如const f = require("fs")
。也可以是其他的。 - 再重新写回去
这个时候,我们构建的这个模块在node
环境就可以运行了。
正常情况就到此为止了,毕竟已经找到原因,还已经解决问题。
但我这个时候又比较有时间,想着去给vite
提一个优化issue
。但在提issue
之前,肯定需要检查下,是否已经有这种问题了。截止目前issue
列表有将近四百个问题。
结果一找还真有人提过这个问题:github.com/vitejs/vite…
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
到本地后,我们打开它,可以简单的看一下它的项目结构:
这是一个 Monorepo
项目
packages
目录是主要项目代码docs
目录主要是文档说明playground
目录下主要是测试用例script
目录下是一个执行的脚本
由于我们主要是去看vite
的源码,所以我们直奔packages/vite
核心代码在src
目录下,我们直接去看代码的话,是非常复杂的,更建议在调试中去看代码,比如上面的项目中,进行调试,然后根据构建的流程去阅读代码。启动命令我们去看package.json
:
那这就很明显了,直接进入vite
目录,执行pnpm dev
启动项目
这时候说明就启动完成了。
这时候我们需要把刚刚本地启动的vite
引入到其他项目中去使用。
还是在vite
的目录下:
pnpm link -g
然后再到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
的入口代码,打上断点。
我们在node_modules
目录下面修改的vite
就是我们本地启动的vite
,由于添加了debugger
,文件发生了改变,这时候它会重新编译。
我们切回需要构建的项目,切换 JavaScript Debug Terminal
终端
执行pnpm dev
,这时候,就会在我们的断点处停止了。(pnpm build
也可以,需要注意断点位置不同)
我们便可以愉快的开始断点调试了
四.解决问题
通过调试源码,判断如果引入的是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.需要在构建的什么阶段给出提示呢?
在模块加载阶段,判断了当前模块是内置模块,即可以抛出提示。
调试中,我们发现,所有的导入文件(导入文件指的是import foo from 'index.js
这种)都会经过这个resolveId
生命周期,其中id
是加载当前文件的路径,importer
是它父级文件的路径。有了这些以后,就可以添加warn
信息了。
到此为止就可以去提交pr
了。但这时候我们还可以更进一步,看一下为什么内置模块会被构建成空对象呢?
3.为什么内置模块会被构建成空对象呢?
文件packages/vite/src/node/plugins/resolve.ts
中,在resolveId
时期,内置模块最终会走到这里,返回一个标识__vite-browser-external
。
而在load
加载文件内容的时期,会将有这个标识的模块全部转成空模块,所以构建后就成了空对象
原因也找到了,开始提交commit
五.提交commit
首先fork
一份,修改fork
版本的,毕竟pr
的时候也可以提交fork
版本的。
提交pr
的时候,会有一些检查,如果有一些没有通过,就像下图所示,需要排查下代码原因。
提交前可以在本地执行pnpm test
,确保测试用例全部通过,再提交,减少维护者review
成本。
需要注意的是:commit
信息尽量全用小写,否则Semantic Pull Request
检查也会报错,比如我一开始把句子开头的单词大写了,后来修改了。
之后就注意邮箱消息,等待维护者的review
,可能需要修改,可能会直接合并,也可能会关闭。我遇到的维护者都比较好,一般关闭的情况,都会非常耐心的给出原因,或者修改的建议,甚至直接帮你修改。在此非常感谢大佬们。
截止目前,已经被合并进去啦,同时我的评论也带上了 Contributor
标识。
修复这个问题还获取的了一个标签p2-nice-to-have 🍰