先讲结论:依赖预打包就是将框架里面用到的第三方开源的库的代码,拷贝一份到你的框架中保存起来。然后定期升级维护,好处就是项目中足够稳定,比如现在 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}`; 复制代码
然后问题的重点就回到了 build
和 writeFile
方法的实现上。
采用这样的分解步骤,可以让我们的整个代码逻辑变得非常的清晰。然后先把我们的实现完成了,再去考虑代码的整洁性,改改 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
包的详细实现和设计,请加入作者云谦的星球。
感谢阅读,如果你觉得本文对你有一点点帮助,别忘了给我点赞加关注哦,感激不尽。
如果文中有错漏的地方,欢迎指出,太晚了有点困了。一稿出了,抱歉抱歉。