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

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 挑战21天手写前端框架 day19 依赖预打包是什么意思

image.png

先讲结论:依赖预打包就是将框架里面用到的第三方开源的库的代码,拷贝一份到你的框架中保存起来。然后定期升级维护,好处就是项目中足够稳定,比如现在 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 包的详细实现和设计,请加入作者云谦的星球


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

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


源码归档

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
资源调度 前端开发 算法
前端依赖版本重写指南
感谢神奇的 Semver 动态规则,npm 社区经常会发生依赖包更新后引入破坏变更的情况(应用没有使用依赖锁的话),而应用开发者就要在自己的依赖声明里先临时绕过,避免安装到有问题的版本,如果是一级依赖,只需要改 package.json 的声明就可以了,但如果是子依赖,就需要进行版本重写(overrides/resolution)了。本文是一篇针对版本重写功能的指南性文章,当你遇到如下的问题时,就可以按照对应的依赖重写语法,解决这些依赖问题了。
5806 1
前端依赖版本重写指南
|
26天前
|
缓存 前端开发 JavaScript
前端架构思考:代码复用带来的隐形耦合,可能让大模型造轮子是更好的选择-从 CDN 依赖包被删导致个站打不开到数年前因11 行代码导致上千项目崩溃谈谈npm黑洞 - 统计下你的项目有多少个依赖吧!
最近,我的个人网站因免费CDN上的Vue.js包路径变更导致无法访问,引发了我对前端依赖管理的深刻反思。文章探讨了NPM依赖陷阱、开源库所有权与维护压力、NPM生态问题,并提出减少不必要的依赖、重视模块设计等建议,以提升前端项目的稳定性和可控性。通过“left_pad”事件及个人经历,强调了依赖管理的重要性和让大模型代替人造轮子的潜在收益
|
6月前
|
资源调度 监控 前端开发
第七章(原理篇) 微前端技术之依赖管理与版本控制
第七章(原理篇) 微前端技术之依赖管理与版本控制
195 0
|
3月前
|
前端开发 Oracle Java
【前端学java】java开发的依赖安装与环境配置(1)
【8月更文挑战第8天】java开发的依赖安装与环境配置
49 1
【前端学java】java开发的依赖安装与环境配置(1)
|
3月前
|
前端开发 JavaScript 程序员
成功解决:尚硅谷中的谷粒商城前端项目运行依赖问题。【详细图解+问题说明+解决思路】
这篇文章介绍了如何解决尚硅谷谷粒商城前端项目中遇到的依赖问题,通过修改`package.json`和`package-lock.json`中的`node-sass`和`sass-loader`版本,成功解决了node版本与这些依赖的兼容性问题。
成功解决:尚硅谷中的谷粒商城前端项目运行依赖问题。【详细图解+问题说明+解决思路】
|
前端开发
前端学习笔记202305学习笔记第二十三天-vue3项目依赖安装
前端学习笔记202305学习笔记第二十三天-vue3项目依赖安装
44 0
前端学习笔记202305学习笔记第二十三天-vue3项目依赖安装
|
6月前
|
IDE 前端开发 搜索推荐
5款超好用的在线IDE,媲美vscode,可以直接编写前端构建化项目,而无需在本地下载依赖包,非常适合学习、demo、原型开发
5款超好用的在线IDE,媲美vscode,可以直接编写前端构建化项目,而无需在本地下载依赖包,非常适合学习、demo、原型开发
2005 0
|
6月前
|
JSON 前端开发 JavaScript
如何检查前端项目中未使用的依赖包?
如何检查前端项目中未使用的依赖包?
254 0
|
缓存 JSON 资源调度
前端包管理器的依赖管理原理
本文主要探究前端包管理器的依赖管理原理,希望对读者有所帮助。
679 0
前端包管理器的依赖管理原理
|
前端开发
前端学习笔记202305学习笔记第二十三天-重构项目依赖安装
前端学习笔记202305学习笔记第二十三天-重构项目依赖安装
67 0