最近把项目中沉淀出来的一些工具函数整理出来编写成一个代码仓库,准备先在本地把每个函数都调试一遍之后再发布,需要使用yarn link 或者npm link 在测试的项目中link 指定的本地依赖库,在这过程中遇到一些问题

问题

在React Native 项目下无法使用yarn link 或者npm link 来link 本地的依赖,原因是RN 使用的打包工具Metro 不支持symlinks

逛了一番issues 后,找到了一个可行的解决方案

解决方案

使用第三方库metro-symlinked-deps

这个库可以用来自定义metro 的打包配置,使用方法可以参考metro-symlinked-deps 的文档

这里贴一下metro-symlinked-deps 给出的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// metro.config.js

const {
applyConfigForLinkedDependencies,
} = require('@carimus/metro-symlinked-deps');

module.exports = applyConfigForLinkedDependencies(
{
transformer: {
getTransformOptions: async () => ({
transform: {
experimentalImportSupport: false,
inlineRequires: false,
},
}),
},
},
{
projectRoot: __dirname,
blacklistLinkedModules: ['react-native'],
},
);

但是在使用这个库的时候也遇到了一些问题

另一个问题

一开始使用这个库并没有解决yarn link 无效的问题。确定自己配置没错之后,就开始寻找问题。因为看到有人说这个库有用,有人说没用。就开始看这个库的源码,看看它到底干了啥

于是,发现这个库会拿到项目下node_modules 里面使用symlink 的依赖的真实地址。

那么按理来说我用yarn link 是符合他的这个逻辑的, 并且我使用ls -l 命令查看我link 的本地依赖也是正确返回了软链的地址。于是带着疑问我查看了这个库用来收集link 的依赖的方法,最终发现这个库使用了get-dev-paths 来检查link 的依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* Resolve all detected linked directories to unique absolute paths without a trailing slash.
*
* @param {string} projectRoot
*/
function resolveDevPaths(projectRoot) {
return Array.from(
new Set(
getDevPaths(projectRoot)
.map((linkPath) => {
return `${fs.realpathSync(linkPath)}`.replace(/\/+$/, '');
})
.filter((absLinkPath) => !!absLinkPath),
),
);
}

所以目光来到了get-dev-paths

首先看看它是如何返回link 的依赖的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
addPath = function(dep) {
var target;
if (!fs.lstatSync(dep).isSymbolicLink()) {
return;
}
try {
target = realpath.sync(dep);
} catch (error) {
err = error;
return typeof opts.onError === "function" ? opts.onError(err) : void 0;
}
// Skip target paths with "/node_modules/" in them.
if (nodeModulesRE.test(target)) {
return typeof opts.onError === "function" ? opts.onError(new Error(`Symlink leads to nothing: '${dep}'`)) : void 0;
}
if (opts.preserveLinks) {
paths.push(dep);
}
if (!visited.has(target)) {
visited.add(target);
if (!opts.preserveLinks) {
paths.push(target);
}
queue.push(dep);
}
};

addPath 方法通过判断一个文件路径是否是软链,并文件的真实路径添加最终的输出结果中

那么addPath 在哪调用的呢,在这个库的源码里发现下面这段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
return fs.readdirSync(depsDir).forEach(function(name) {
var scope;
if (name[0] === '.') { // Skip hidden directories.
return;
}
if (name[0] === '@') {
scope = name;
fs.readdirSync(path.join(depsDir, name)).forEach(function(name) {
if (deps[name = scope + '/' + name]) {
return addPath(path.join(depsDir, name));
}
});
} else if (deps[name]) {
addPath(path.join(depsDir, name));
}
});

上面代码中deps[name]deps是获取了项目目录下的package.json中的dependencies 里的依赖,并且与fs.readdirSync方法获取的文件名来比对,如果文件名出现在了dependencies 就执行addPath方法来获取真实路径并添加到最终结果中。

于是我发现,我使用yarn link 的依赖的名字并没有出现在我的package.json中的dependencies 里,于是我在package.json 中的dependencies 里添加了本地依赖的名字

1
2
3
"dependencies": {
"myPackageName" : "*"
},

再次尝试react-native start ,终于编译成功。

One more thing

到这里就可以开始测试自己的本地依赖了,但是还有一个小小问题,可能会报错。本地依赖的依赖找不到了。这时候需要改一下配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// metro.config.js

const {
applyConfigForLinkedDependencies,
} = require('@carimus/metro-symlinked-deps');

module.exports = applyConfigForLinkedDependencies(
{
transformer: {
getTransformOptions: async () => ({
transform: {
experimentalImportSupport: false,
inlineRequires: false,
},
}),
},
},
{
projectRoot: __dirname,
blacklistLinkedModules: ['react-native'],
// 新增这个
resolveNodeModulesAtRoot: true
},
);

终于可以在React Native 项目中调试本地依赖了~

最后

以上是我寻找在React Native 项目中使用yarn link的解决办法的全过程。

参考

本文作者: Roy Luo

本文链接: 在react-native 项目中使用yarn link