浅聊前端依赖管理及优化(上)

1、引子

在npm、yarn等包管理工具的帮助下,前端工程化模块化的进程日益加快,项目的依赖包也日益增大,特别是若项目采用webpack来构建用到许多webpack的插件、一些辅助开发如(eslint、postcss、dev-server之类的库)以及一些单元测试(jest、mocha、enzyme)可能须要用到的插件,项目中的node_module就会变的十分庞大。javascript

如图:css

若是搭配这种状况是否是很绝望:前端

所以团队开发者每次从新初始化项目进行npm install会十分缓慢,而且若占用大量空间,更进一步来讲项目中每每不少前端团队会用到一台构建服务器,对前端项目的代码获取、打包、构建、部署进行管理(例如笔者本身所在的团队就在使用jenkins对前端项目的打包、构建、部署进行自动化托管)。npm这样的项目级依赖管理的特性就会形成大量的时间以及资源的消耗。vue

同时笔者也想在文章开始以前代表一个态度,npm自己portable的模块化设计,使每一个项目互不干系,也是一种它设计的精华所在,本文仅是针对实际使用中遇到的一些小困扰进行解读,但愿提供一个新的思路。java

补充一下,实际yarn已经有了解决方案 - workspaces:node

Workspaces in Yarnreact

本文暂不把yarn容纳到讨论范围内(实际思路很类似,笔者也是写完以后才发现的)。webpack

此本篇文章就在这样的背景下诞生了。纯粹是笔者在项目中积累的一些经验,如有不足望指出。git

2、先来讲说示例

github仓库地址:github.com/Roxyhuang/n…github

一般现代前端的标准工程每每具有如下功能,为了让这个讨论更贴近实际开发,而且更直观,所以规定示例项目基本具有如下这些能力:

(1) 提供dev-server

(2) 提供语法转译(babel)

(3) 可以解析样式

(4) js代码风格插件以及样式风格检查

(5) 单元测试

(6) 提供样式兼容处理

固然以上部分只体现的依赖层面。所以可能有些依赖的引入可能出现错误或者不合理。

并假设目前团队使用了webpack和parcel两套前端构建方案,团队项目中有使用了React和Vue。

3、问题的总结

若不对项目作任何优化以及设置正常项目的依赖的结构应该是这样的:

如图:

而后咱们再看一下node_modules总体所占的空间:

状况一:多个依赖模块彻底相同的项目

这种状况相对很常见,实际就是相同依赖的项目有多个

如图:

能够想象这样模块的容量即是162.2mb x 3,同时文件数也会是1072 x 3。

因而可知实际咱们对容量的消耗是是十分大的,笔者作了非精确统计实际:

生产中所需的模块容量与文件数(dependencies):

本地开发和测试所需的模块容量与文件数(devDependencies):

实际能够看到devDependencies才是咱们总体依赖体积与数量如此庞大的“元凶”。

固然有部分生产中全局所需的库(如:React Vue)能够采用外部化,经过CDN来引入。(即图上CDN模块的复用)

状况二:仅生产中所需的模块依赖依赖重复 (dependencies)

如图:

生产中所需的模块容量与文件数(dependencies):

仅图中dependencies中红色标注的react-redux,redux版本以及使用了mobx react-mobx更换

本地开发和测试所需的模块容量与文件数(devDependencies):

无变化

实际这种状况和状况一相似只有dependencies中部分模块的依赖有差别或者版本不一样。固然再进一步,这个部分每一个项目确实可能出现较大的差别,同时着部分差别可能会引起devDependencies也出现变化(如:React Vue 可能用到webpack Loader eslint plugins等,以后会说起这块)。

实际依然devDependencies才是咱们总体依赖体积与数量如此庞大的“元凶”。

状况三:本地开发和测试所需的包依赖重复且,但不存在版本差别 (devDependencies)

如图:

这种状况咱们也囊括了状况二的差别(因为dependencies产生的差别,同时也会影响devDependencies的)

生产中所需的模块容量与文件数(dependencies):

如图所示,除了状况中出现的react-redux,redux版本有差别,因为出现了webpack + vue的项目,项目中dependencies的依赖须要引入vue或vue-router,除此都是一致的。

本地开发和测试所需的模块容量与文件数(devDependencies):

因为咱们须要引入vue,致使咱们devDependencies须要产生一些变化包括移除一些webpack与react相关的插件,eslint与react的插件等,变动为webpack与vue相关的插件,eslint与vue的插件等。

如图笔者非精确的统计一下这种状况仅devDependencies下的容量与文件数:

仍是依然很大,而且实际仍是存在大量的依赖重叠(例子中有17个依赖重叠)。

状况四:多个构件工具,存在本地开发和测试所需的包依赖重复,且存在在版本差别, (devDependencies)

这种是目前最复杂的状况,存在下列几个状况

(1) 前端构件方案存在多个, 例子中为webpack 和 parcel

(2) 生产中所需的模块存在差别,甚至技术栈不一样

(3) 本地开发和测试所需的包依赖重复,但有略微不一样,且存在版本不一样

如图:

这种状况咱们也囊括了状况二的差别,能够看到同一个构件工具下存在本地开发和测试所需的包依赖重叠的状况依然是较多的。

所以实际若是出现这种状况,咱们仍须要考虑以及解决。

小小的总结一下:

结合状况一状况二状况三状况四的例子能够得出几点小小的“规律”:

(1) devDependencies不管从模块所占体积仍是文件数量都在总体依赖模块中所占的比较较大

(2) dependencies下的包,可能会由于项目不一样差别较大,同时也会致使devDependencies下模块有差别

(3) 即便devDependencies可能项目存在差别,可是仍有可能一部分重叠

(4) 多个模块化方案,可能会致使项目devDependencies差别较大

咱们能够发现其中无论前三种状况的哪种devDependencies都会有一部分的依赖模块是重叠的。那既然存在重复,那咱们就有必要考虑“复用”

同时咱们发现多个模块化方案(webpack, parcel)devDependencies下的模块差别会较大,这也是咱们以后须要解决的。

锁定依赖模块的能力:

非本文重点,仅简述一下:

(1) lock or dont'lock

这个问题社区探讨的老问题,锁不锁都有支持的一方,然而这边笔者不代表态度,可是同时也但愿咱们以后的依赖管理的优化和“瘦身”可以具有锁定依赖的能力。

(2) npm 锁定依赖以及锁定依赖的内部循环的方式

  • 使用语义版本控制来锁定版本:

    关于semantic-versioning,这里咱们不赘述,有兴趣能够看一下npm文档:

    docs.npmjs.com/about-seman…

    可是经过semantic-versioning来锁定版本,没法锁定依赖的内部循环(即依赖的依赖)。

  • npm-package-locks来锁定

    关于npm-package-locks,这里咱们不赘述,有兴趣的能够看一下:

    docs.npmjs.com/files/packa…

咱们关注的重点是咱们须要依赖package-lock.json,来协助锁定依赖的版本以及依赖内部循环的版本。

再总结一下:

(1) devDependencies不管从模块所占体积仍是文件数量都在总体依赖模块中所占的比较较大

(2) 项目的dependencies可能会由于项目不一样差别较大,同时一部分能够外部化,所以不在本文想要探讨“瘦身管理”的范围内,列出主要是以便更接近现实情况

(3) 即便devDependencies可能项目存在差别,可是仍有大量的依赖重复与重叠,根据实际包的分析他们是目前依赖“瘦身管理”的主要目标

(4) 即便dependencies有差别的项目,devDependencies大部分可能类似(如状况三所示),所以咱们仍能够提炼出一部分相同的devDependencies进行“瘦身管理”。

(5) 多个模块化方案,可能会致使项目devDependencie差别较大(如例子中webpack和parcel)可能会有一部分devDependencies可能版本也会不一样,不建议两个项目差别极大的node_modules共享。

(6) 同一个模块可能会存在版本差别,所以咱们可能会面对同时面对一个库存在多版本的状况

(7) 不管依赖锁定是否必要,可是但愿提供的方案可以具有。

(8) 依赖锁定,须要依靠package-lock.json实现(shrinkwrap.json亦可)

4、优化方案以及存在的一些问题

下面的优化方案都依赖了NodeJS模块加载机制,所以先粗略的聊一下,这里不描述完整的过程,感兴趣的朋友能够查询官方文档或能够看一下朴灵大大的深刻浅出NodeJS 。

如何利用NodeJS模块加载机制

简单来讲 - 在NodeJS中引入模块,须要经历以下3个步骤:

  • 路径分析
  • 扩展名分析
  • 编译执行

咱们利用的则是在路径分析中自定义文件模块(第三方npm包)的查找:

自定义文件模块查找顺序为:

  • 当前目录下node_modules目录
  • 父目录下node_modules目录
  • 向上逐级递归直到根目录下下node_modules目录
  • 递归至根目录

有点相似于JS的原型链查找,文件路径越深,模块查找越耗时,同时也是它慢的缘由。

固然这是默认状况下,实际通畅咱们能够经过配置NODE_PATH的方式,在递归至根目录后,若依然没法找到,给到一个路径或多个路径找到具体模块。

1. 配置NODE_PATH(方案一)

方案一实际就是利用了配置NODE_PATH,一般通常会将其配制成npm i -g所在的全局模块目录(实际能够改,本例暂定这个目录)。

结构变更

状况1、状况2、状况三均可以转化成以下结构:

状况三下为例子 - 如图:

具体实现:

在对应系统的环境变量配置文件中增长环境变量NODE_PATH,例如在MacOS中

vi /ect/profile  
# 或/etc/bashrc或~/.bashrc 
# 此处不赘述配置问题、加载顺序以及原理
复制代码
-> export PATH=$PATH: 
# 将 /usr/bin 追加到 PATH 变量中

-> export NODE_PATH="/usr/lib/node_modules;/usr/local/lib/node_modules"
# 指定 NODE_PATH 变量
复制代码

那 NODE_PATH 就是NODE中用来寻找模块所提供的路径注册环境变量。咱们可使用上面的方法指定NODE_PATH环境变量。而且用;分割多个不一样的目录。

关于 node 的包加载机制我就不在这里赘述了。NODE_PATH中的路径被遍历是发生,从项目的根位置递归搜寻 node_modules 目录,直到文件系统根目录的 node_modules,若是尚未查找到指定模块的话,就会去 NODE_PATH中注册的路径中查找。

存在问题:

(1) 不会生成package.json,所以对依赖管理比较繁琐,实现增量安装比较繁琐

  • 固然其实能够不须要npm i -g去处理,能够手动本身维护一个目录包括package.json,以及package-lock.json,但若是这么只会造成只是一个线性关系,而非一个树状关系。

(2) 不支持同一模块同时存在不一样版本,所以若是依赖出现版本差别,没有解决方案

  • 由于造成的是一个线性关系,而非一个树状关系实际项目若存同一依赖版本差别,就会有一个优先级的问题存在。

(3) 全局安装模块,没法生成package-lock.json没法锁定依赖内部循环(依赖模块的依赖)

  • 默认若是使用npm i -g安装,一样不会生成package-lock.json,所以没法锁定依赖内部循环。

2. 提高node_modules目录(方案二)

结合以前问题分析的咱们得出的结论,所以实际上咱们把项目中devDependencies依赖重叠的模块,在项目的父目录存放node_modules便可将依赖提高。便可以进行项目间的共享。

目前笔者的理想方案应该可以达到下列几个目的:

  • 能够将项目构建类似技术栈的项目统一在一块儿,共享依赖

  • 能够实现对公共模块的维护和管理,并实现增量安装

  • 对原来npm install流程改动不会太大

(1)结构变化后的依赖结构图

实际须要实现只要将项目目录改成如下结构

----|---wwwroot/ # 工做目录或部署目录
 |
 |---webpack/--|---webpack-react/---|---package.json
 | | |---package-lock.json
 | | |---node_modules/
 | | |---src/
 | | |
 | |--- webpack-vue/--- |---package.json
 | | |---package-lock.json
 | | |---node_modules/
 | | |---src/
 | |---node_modules/ |
 | |---package.json |
 | |---package.json-lock|
 | | |
 |---parcel/---|--- parcel-react/---|---package.json
 | | |---package-lock.json
 | | |---node_modules/
 | | |---src/
 | | |
 | |--- parcel-vue/ --- |---package.json
 | | |---package-lock.json
 | | |---node_modules/
 | | |---src/
复制代码

状况1、状况2、状况3、状况四能够转化成以下结构:

状况四下为例子 - 如图:

说说目前笔者探索的两个实践:

(2)手工管理package.json

下一步提及来也十分简单:

实际只要在,维护一个package.json便可,实际经常只须要devDependencies,而后实际须要安装时只须要进行一次npm install便可,而且会生成package-lock.json。

即上面说到的这几个位置

|---webpack/--|---webpack-react/---|---package.json
|             |--- webpack-vue/--- |---package.json

|---parcel/---|--- parcel-react/---|---package.json
|             |--- parcel-vue/ --- |---package.json
复制代码

实际只要在devDependencies按正常package.json中的内容维护便可。

(3)利用 preinstall

“对原来npm install流程改动不会太大”,这一点上面对于手工管理方式实际并并不知足。而且手工进行管理十分繁琐,所以咱们是否能够在npm install以前把devDependencies内重叠模块的node_modules进行提高呢?

答案是:能够的,主要利用的是npm script中的preinstall,关于npm script以及preinstall能够经过:docs.npmjs.com/misc/script…来了解。

npm的preinstall这个hook实际会在安装软件包以前运行。实际咱们就能够经过preinstall去执行一些node.js的代码让咱们的devDependencies内重叠模块的node_modules提高。

那具体来看一下步骤:

a.在项目package.json中增长preinstall
"scripts": {
    "preinstall": "node build/scripts/preinstall.js",
    "start": "webpack-dev-server --mode development --hot --progress --colors --port 3000 --open"
  }
复制代码

这里我会在项目的build目录下的scripts执行preinstall.js这个文件内的代码

b.切换至项目父级目录(即约定的技术栈目录)

这里我仅进行了一个最简单的示范,其原理是在项目的package.json里增长devDependenciesGlobal 项:

实际能够看一下我提供的实现具体代码

实际最为关键的为:

const execPath = path.resolve('../');
const preListObj = preList.devDependenciesGlobal; // 项目package.json本身维护的devDependencies那些重叠依赖
const currentMd5 = md5(JSON.stringify(preListObj)); // 简单举例生成一个md5,实际应用中能够搭配其余机制
...

fs.writeFileSync(`${execPath}/package.json`, JSON.stringify({"md5": currentMd5,dependencies: preListObj})); // 在上级目录建立一个package.json文件并,写入md5以及devDependenciesGlobal的内容

let script = `cd ${execPath} && npm i`;

 // 切换至父级目录并执行npm install
 
exec(script, function (err, stdout, stderr) {
  console.log(stdout);
  if (err) {
   console.log('error:' + stderr);
  } else {
   console.log(stdout);
   console.log('package init success');
   console.log(`The global install in ${execPath}`);
  }
});
复制代码
c.实际所占空间的变化:

(此处就不贴图片了,你们能够clone实例本身试一下)

|---webpack/--|---webpack-react/---|---node_modules/ # 共21.1 MB,369项
|             |--- webpack-vue/--- |---node_modules/ # 共17.8 MB, 336项
|             |---node_modules/ # 共153.3 MB,994项
|             |
|---parcel/---|--- parcel-react/---|---node_modules/ # 89.4 MB,540项
|             |--- parcel-vue/ --- |---node_modules/ # 71.5 MB,491项
|             |---node_modules/ # 共21.1 MB,118项
复制代码

实际很是简单,即得执行目录的上级目录,此处为了举例个人例子里会在运行preinstall时根据devDependenciesGlobal生成一个md5,以便下次install时比对,若一致不执行preinstall内的流程。固然这并非一个最佳实践(以后的各类实践方案在下篇中进一步给出)

固然实际这部分存在“无限”的可能性,能够根据本身的需求来完善(好比服务端获取devDependenciesGlobal以及md5)。

这仅仅是一个简单示范,实际能够经过配合一些服务端以及docker进一步提高。

存在问题:

(1) windows兼容性

笔者所在的团队在使用的过程当中,有遇到windows的开发环境下的各类问题,好比:

  • win10下自定义的preinstall流程中的install执行十分缓慢,或没法执行
  • babel或抛出一些错误(这里不具体说明)

(2) 使用者没法作到无感知

也是最大的缺点,即便用的时候使用者会有感知,开发项目的时候要求使用必定是主动必须按特定的目录结构来安排本身的workspace。

(3) 本地npm版本,不一致,也可能对依赖形成的影响较大

(4) npm script相关的必须在devDependencies引入 (固然也能够经过一些方式避开,好比dev-server不直接使用npm script启动)

这仅仅是一个开始...待续

利用node模块加载机制,咱们其实已经能够很大程度改进咱们的依赖了,但如上所述咱们还存在这些问题:

(1) windows兼容性

上文有提到过windows下,笔者实际实践发现会出现一些问题,是否是有办法能够统一环境呢?

(2) 没法作到让使用者到无感知

(3) 本地npm版本,对依赖形成的影响较大

(5) 管理公共依赖没有标准化和自动化

所以下篇笔者可能会主要进一步经过其余方案来配合这个解决方案

(1) 本文举例的完善版本 (经过shell远程下载公共依赖的package.json,也是笔者目前团队在使用的)

(2) 经过搭配docker进一步完善方案 (是笔者想进一步加强的)

(3) 搭配私有npm仓库

(4) 如何更标准化的管理公共依赖,使其能够自动化标准化

这里挖个坑下篇会更新这部份内容

结语以及本文不足之处

通过上述操做,咱们即可以将前端项目根据框架进行依赖管理以及划分了,其实如此改进仍并不是最完美以及优化,不足之处仍很是之多,期待更加优雅的解决方案,使咱们前端项目的架构更佳健壮和灵活。也欢迎你们和我探讨和讨论,谢谢各位大佬能耐心看完。

相关文章
相关标签/搜索