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

简介: 挑战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)了。本文是一篇针对版本重写功能的指南性文章,当你遇到如下的问题时,就可以按照对应的依赖重写语法,解决这些依赖问题了。
4208 1
前端依赖版本重写指南
|
4天前
|
资源调度 监控 前端开发
第七章(原理篇) 微前端技术之依赖管理与版本控制
第七章(原理篇) 微前端技术之依赖管理与版本控制
|
5天前
|
IDE 前端开发 搜索推荐
5款超好用的在线IDE,媲美vscode,可以直接编写前端构建化项目,而无需在本地下载依赖包,非常适合学习、demo、原型开发
5款超好用的在线IDE,媲美vscode,可以直接编写前端构建化项目,而无需在本地下载依赖包,非常适合学习、demo、原型开发
116 0
|
9月前
|
前端开发
前端学习笔记202305学习笔记第二十三天-vue3项目依赖安装
前端学习笔记202305学习笔记第二十三天-vue3项目依赖安装
32 0
前端学习笔记202305学习笔记第二十三天-vue3项目依赖安装
|
5天前
|
JSON 前端开发 JavaScript
如何检查前端项目中未使用的依赖包?
如何检查前端项目中未使用的依赖包?
|
9月前
|
前端开发
前端学习笔记202305学习笔记第二十三天-重构项目依赖安装
前端学习笔记202305学习笔记第二十三天-重构项目依赖安装
42 0
|
缓存 JSON 资源调度
前端包管理器的依赖管理原理
本文主要探究前端包管理器的依赖管理原理,希望对读者有所帮助。
594 0
前端包管理器的依赖管理原理
|
前端开发
前端知识案例4-依赖收集4-问题解决
前端知识案例4-依赖收集4-问题解决
36 0
前端知识案例4-依赖收集4-问题解决
|
前端开发
前端知识案例1-依赖收集1-问题解决 原
前端知识案例1-依赖收集1-问题解决 原
44 0
前端知识案例1-依赖收集1-问题解决 原
|
前端开发
前端知识案例3-依赖收集3-问题解决 原创
前端知识案例3-依赖收集3-问题解决 原创
39 0
前端知识案例3-依赖收集3-问题解决 原创