挑战21天手写前端框架 day19 依赖预打包是什么意思

1,573 阅读5分钟

先讲结论:依赖预打包就是将框架里面用到的第三方开源的库的代码,拷贝一份到你的框架中保存起来。然后定期升级维护,好处就是项目中足够稳定,比如现在 npm 上经常出现的底层包挂码事件等超高的安全风险问题,就可以通过这个方法完美的解决。更多关于这部分的内容,可以查看 umi 作者云谦的星球日更28 依赖版本锁不锁

原理

感觉原理没什么好讲的,就是将构建入口找到 node_modules 下的某个包指定的 main 入口,然后将它编译到我们指定的路径中。修改项目中的引用,从引用包的方式改成引用相对文件的方式。

用法

我们在需要预打包的子包目录下,执行 pnpm build:deps pkgName 来将子包依赖预编译到子包的 compiled

如:

cd packages/malita
pnpm build:deps express

malita 子包中的 express 进行预编译操作。

实现

这个实现也算是之前知识点的一个复习环节。首先我们要执行 build:deps 命令我们需要先定义它,在 malita 子包的 packages.json 中的 scripts 配置中定义。我们可以这么写。

{
    "name": "malita",
    "scripts": {
        "build:deps": "pnpm esno ../../scripts/bundleDeps.ts",
    },
}

使用 esno 是希望直接执行 ts 类型的脚本,而不用将脚本转化成 js 语法。因为这个操作会在很多个子包中执行,因此我们将这个脚本写在最外层,供任意子包使用。

// 最顶层目录下

pnpm i esno -w --D

获取到传入参数

还记得我们第四天的内容,如何编写一个 cli 吗?我们那时候是使用 commander 来实现的,用 program.parse(process.argv) 取到命令行的参数。

今天我们换一种用法,使用另一个编写命令行时常用的库:minimist

pnpm i minimist @types/minimist -w --D

新建 scripts/bundleDeps.ts 文件,写入


+ import minimist from 'minimist';

+ const argv = minimist(process.argv.slice(2));

+ console.log(argv);

运行测试

cd packages/malita
pnpm build:deps express

// 日志
{ _: [ 'express' ] }

取到入口和目标路径

如果你忘了接下去该做什么,注意回看我们的需求:“将构建入口找到 node_modules 下的某个包指定的 main 入口,然后将它编译到我们指定的路径中”。

import minimist from 'minimist';
import fs from 'fs-extra';
import path from 'path';

const argv = minimist(process.argv.slice(2));

// 找到 node_modules 下的
const nodeModulesPath = path.join(process.cwd(), 'node_modules');

// 某个包
const pkg = argv._[0];

// 的 main 入口
const entry = require.resolve(pkg, {
    paths: [nodeModulesPath],
});

// 将它编译
build()

// 到
writeFile()

// 我们指定的路径中
const target = `compiled/${pkg}`;

然后问题的重点就回到了 buildwriteFile 方法的实现上。

采用这样的分解步骤,可以让我们的整个代码逻辑变得非常的清晰。然后先把我们的实现完成了,再去考虑代码的整洁性,改改 callback 到 promise ,逻辑调整啊之类的优化工作。我的理念就是不想太多,先做,先拿基础分。

使用 @vercel/ncc 预编译代码

// 没带 cd 的就是表示在最顶成根目录

pnpm i @vercel/ncc -w --D
import minimist from 'minimist';
import fs from 'fs-extra';
import path from 'path';
// @ts-ignore
import ncc from '@vercel/ncc';

const argv = minimist(process.argv.slice(2));
const nodeModulesPath = path.join(process.cwd(), 'node_modules');
const pkg = argv._[0];
const entry = require.resolve(pkg, {
    paths: [nodeModulesPath],
});
const target = `compiled/${pkg}`;
ncc(entry, {
    minify: true,
    target: 'es5',
    assetBuilds: false,
}).then(({ code }: any) => {
    // entry code
    fs.ensureDirSync(target);
    fs.writeFileSync(path.join(target, 'index.js'), code, 'utf-8');
})

运行验证

cd packages/malita
pnpm build:deps express

// 日志
> malita@0.0.2 build:deps /Users/congxiaochen/Documents/malita/packages/malita
> pnpm esno ../../scripts/bundleDeps.ts "express"

ncc: Version 0.33.4
ncc: Compiling file index.js into CJS

查看目标文件是否正确生成 packages/malita/compiled/express/index.js

修改项目中的引用

- import express from 'express';
+ import express from '../compiled/express';
cd packages/malita
pnpm build
cd examples/app
pnpm dev

// 日志
> @examples/app@1.0.0 dev /Users/congxiaochen/Documents/malita/examples/app
> malita dev

App listening at http://127.0.0.1:8888
[HPM] Proxy created: /api  -> http://jsonplaceholder.typicode.com/
[HPM] Proxy rewrite rule created: "^/api" ~> ""

页面功能正常访问。

添加类型定义

从上面的操作,我们可以看出来 express 依赖已经预编译成功了。但是我们会收到一个类型错误。

无法找到模块“../compiled/express”的声明文件。“/Users/congxiaochen/Documents/malita/packages/malita/compiled/express/index.js”隐式拥有 "any" 类型。

pnpm i dts-packer -w --D
+ import { Package } from 'dts-packer';

+ new Package({
+     cwd: cwd,
+     name: pkg,
+     typesRoot: target,
+ });

遗留问题

cd packages/malita
pnpm build:deps express

// 日志
TypeError: Cannot read properties of undefined (reading 'uid')
    at isDirectory (/Users/congxiaochen/Documents/malita/node_modules/.pnpm/resolve@1.22.0/node_modules/resolve/lib/sync.js:31:23)
    at loadNodeModulesSync (/Users/congxiaochen/Documents/malita/node_modules/.pnpm/resolve@1.22.0/node_modules/resolve/lib/sync.js:200:17)
    at Function.resolveSync [as sync] (/Users/congxiaochen/Documents/malita/node_modules/.pnpm/resolve@1.22.0/node_modules/resolve/lib/sync.js:107:17)
    at Package.getEntryFile (/Users/congxiaochen/Documents/malita/node_modules/.pnpm/dts-packer@0.0.3/node_modules/dts-packer/dist/Package.js:88:56)
    at Package.init (/Users/congxiaochen/Documents/malita/node_modules/.pnpm/dts-packer@0.0.3/node_modules/dts-packer/dist/Package.js:37:32)
    at new Package (/Users/congxiaochen/Documents/malita/node_modules/.pnpm/dts-packer@0.0.3/node_modules/dts-packer/dist/Package.js:31:14)
    at null.build (/Users/congxiaochen/Documents/malita/scripts/bundleDeps.ts:52:5)

有一个很难懂的日志,我们可以根据报错堆栈,定位到是加载 @types/express 的时候找不到包。但是注释掉 ncc 单独执行 Package 又能顺利生成。从11点调试到12点半,太晚了,就没有继续定位这个问题,如果有知道原因的小伙伴,记得指导我一下。

pnpm build:deps express

> malita@0.0.2 build:deps /Users/congxiaochen/Documents/malita/packages/malita
> pnpm esno ../../scripts/bundleDeps.ts "express"

@types/express
>  index.d.ts /Users/congxiaochen/Documents/malita/node_modules/@types/express/index.d.ts
// 略
>> dep import express

类型文件找不到

完成了 express 的构建之后,我们继续构建其他的依赖,比如 commander

pnpm build:deps commander

> malita@0.0.2 build:deps /Users/congxiaochen/Documents/malita/packages/malita
> pnpm esno ../../scripts/bundleDeps.ts "commander"

ncc: Version 0.33.4
ncc: Compiling file index.js into CJS
>  typings/index.d.ts /Users/congxiaochen/Documents/malita/packages/malita/node_modules/commander/typings/index.d.ts

我们会发现,commander 自己有 types 定义并且是在 typings/index.d.ts

因此如果我们在项目中使用 import xx from '../compiled/commander'; 就会提示无法找到 commander 模块。

因此我们还需要在构建的包下面生成一个虚拟的包,这个实现很简单,将原有的package.json 拷贝到目标目录下就行。

    const pkgRoot = path.dirname(
        resolve.sync(`${pkg}/package.json`, {
            basedir: cwd,
        }),
    );
    if (fs.existsSync(path.join(pkgRoot, 'LICENSE'))) {
        fs.copyFileSync(path.join(pkgRoot, 'LICENSE'), path.join(target, 'LICENSE'))
    }
    fs.copyFileSync(path.join(pkgRoot, 'package.json'), path.join(target, 'package.json'))

接下来就是将我们的其他依赖都进行预打包就可以了。这里只是讲解了如何实现和一点点注意事项,如果你想对这个内容了解的更清楚一点,包括上面用到的 dts-packer 包的详细实现和设计,请加入作者云谦的星球

感谢阅读,如果你觉得本文对你有一点点帮助,别忘了给我点赞加关注哦,感激不尽。

如果文中有错漏的地方,欢迎指出,太晚了有点困了。一稿出了,抱歉抱歉。

源码归档