因为工作的时候,公司提供的是 Windows 台式机,因此一般都是在 Windows 环境下开发的。
但是最近在用 cnpm 安装脚本的时候,忽然发现一个很有意思的问题:用 cnpm 安装的全局脚本,比如 vue-cli 居然只能在 cmd 中运行,无法在 powerShell 中运行。
类似的问题,其实以前也碰到过,但是以前一般都是报着能用就用,不去深究原理的想法。
其实很多时候,自己内心深处还是有一个声音不断地在提醒自己:“知其然更知其所以然方能走的更远”!
但是奈何,自己实力不允许,对很多东西都是一知半解,没有形成一套体系,没有拥有看透问题本质的能力。
但是现在,开始找到感觉了,于是现在碰到问题,多花点时间,稍微深究一下,事后收获还是很大的。
闲话不多说,转回我们的正题吧。
至于为什么会用 npm,我想前端的同学应该都深有感触,特别是国内的前端同学。
我想或多或少都会经历过一下场景:
同事1:反正大家都说 npm 不好用! 同事2:npm 垃圾玩意儿,下东西太慢了! 同事3:cnpm 下东西快,比 npm 强一万倍! 同事4:你竟然还在用 npm,太老土了! 。。。
类似的情景对话,我想大家应该都经历过。
但是究竟为什么 npm 不好用,cnpm 好用呢?
这种踩 npm 的情况,是从什么时候开始的,现在还是这样吗?
npm 跟 cnpm 的差别是什么呢?
npm 可以通过什么方式变得跟 cnpm 一样好用吗?
其实这些问题,并没有多少人去深究,特别是对于很多其前端初级选手来说,估计很多人都下意识的认为,其实这两个是一个东西,都是下 nodejs 模块的嘛。
估计很多人都这样想:这些问题关我鸟事,我研究的那么透彻,老板也不会给我涨工资啊!我前端,写好页面就行了,这些个框架,能用好就行了,我开车,干嘛要了解汽车的原理呢!
如果你符合以上的思维,那么请及时中断往下看的念头吧,这篇文章不适合你。
那么究竟为什么大家都重口一词的说 npm 不好用 cnpm 好用呢?
原因是就是,以前 npm 真的挺不好用的!
这篇博主的文章分析的非常好,有兴趣的同学可以阅读一下:blog.xgheaven.com/2018/05/03/…
就因为 npm 不好用,所以才催生出像 cnpm、yarn 等第三方包管理系统。
但是随着 npm 6.x 版本的发布,这些问题已经被被解决了,已经被扫进了历史的垃圾堆里了。
所以结论就是:现在用 nodejs 的包管理系统,首选 npm,实在不能用 npm 的情况下,再考虑用第三方的包管理系统。
比如 create-react-app 这个脚手架,就是只支持 yarn 的,但是如果你非要用 npm,就很麻烦了。
但是有的童鞋会发问,npm 安装模块太慢了,不能忍。
说实话,我也忍不了,忍不了你就改个 npm 模块的源呗,从镜像站去下载模块不久快很多么。
这个问题其实不止是出现在 npm 上,很多包管理系统都会出现在这样的问题。
比如 python 的 pip,Ubuntu 的 apt-get,homebrew,centos 的 yum 等等,都会因为官方源服务器在国外,访问起来太慢。
但是这个问题其实是有解决方案的,换成国内的镜像源就能解决。
比如 npm 换成淘宝的国内镜像源就能解决下载过慢的问题了,下面是配置方式:
# 换源 npm config set registry https://registry.npm.taobao.org# 检查是否改成功了 npm config get registry 复制代码
再聊聊为什么会出现第三方包管理工具。
其实之前就说过了,因为 npm 在开始的时候不好用,所以后来社区就诞生出了更优秀的包管理工具。
但是 npm 也是在进步的,我们不能总是以一种陈旧的眼光去看待问题,以一种拥抱未来的姿态去剔除我们思想中的偏见成分。
npm 的这种历史,有点类似于 JavaScript 的发展历史。
以前 JavaScript 很多地方用起来不友好,所以催生出了很多优秀的 JavaScript 框架,十年前,风头最盛的大概就是 jQuery 了吧。
甚至一度,有人豪言壮语的宣称:不用学 JavaScript 了,学了 jQuery 就行了。
但是历史总是这么惊人的相似,随着 es6 以及后续版本的出现,jQuery、lodash 等很多增强 JavaScript 语言功能的框架都渐渐的开始退出了历史的舞台了。
至于原因,我想大家因该也知道,同样实现一种功能,自带的肯定是更好用的,如果不好用,只能说明他还有提升的空间。
这个定律,在很多时候都是比较符合现实的表现的。
所以,为什么说代码开源,有助于计算机行业的发展。
因为同样一个工具,总有人觉得不好用,觉得不好用,你拿出更好用的东西出来,大家都会学习你这种更加先进的理念。
正式因为有了这个不断循环往复的过程,才造就了近几十年以来,互联网行业的蓬勃发展。
说了太多对于这个话题的思考,还是让我们回到这个问题的本身来吧。
究竟是什么诱因,让我奋笔疾书的写下这篇文章的呢?
先来让我们检查一下 cnpm 的版本:
C:\Users\Administrator>cnpm --version cnpm@6.1.0 (C:\Users\Administrator\AppData\Roaming\npm\node_modules\cnpm\lib\parse_argv.js) npm@6.12.0 (C:\Users\Administrator\AppData\Roaming\npm\node_modules\cnpm\node_modules\npm\lib\npm.js) node@10.16.3 (C:\Program Files\nodejs\node.exe) npminstall@3.23.0 (C:\Users\Administrator\AppData\Roaming\npm\node_modules\cnpm\node_modules\npminstall\lib\index.js) prefix=C:\Users\Administrator\AppData\Roaming\npm win32 x64 10.0.18362 registry=https://r.npm.taobao.org 复制代码不得不说,这个命令显示的内容可真多,一下子将 nodejs、npm、cnpm 的版本都给暴露了,不过没关系,这正是我们想要看到的结果。
为了真实的情景再现,我接下来要安装 vue-cli 了:
C:\Users\Administrator>cnpm install vue-cli -g 复制代码为了文章的简介,安装过程的 log 就不黏贴上来了,反正没有报错,异常退出的话,vue-cli 就装成功了。
下面我来运行一下 vue :
C:\Users\Administrator>vueC:\Users\Administrator>“node” “C:\Users\Administrator\AppData\Roaming\npm\node_modules\vue-cli\bin\vue” Usage: vue <command> [options]
Options: -V, --version output the version number -h, --help output usage information
Commands: init generate a new project from a template list list available official templates build prototype a new project create (for v3 warning only) help [cmd] display help for [cmd] 复制代码
可以看到的是,我运行 vue 命令的时候,并没有直接执行 node vue 脚本 这样的命令,而是唤出了一串很字符来执行 vue:
"node" "C:\Users\Administrator\AppData\Roaming\npm\\node_modules\vue-cli\bin\vue" 复制代码为什么会这样呢?
相信跟我以前一样,没怎么思考过这个问题的人,肯定会误以为,我们安装了某个模块,是不是说我们就安装了某个直接可以执行的二进制文件呢?
这个答案是否定的,其实我们安装的 nodejs 模块都是一些 nodejs 脚本,我们在调用像 vue 这样的命令的时候,其实就是调用 nodejs 这个引擎,去执行对应的 nodejs 脚本。
这个问题,你往大了想,就能够看透计算机的本质了。
我们计算机其实不能识别我们的编程语言,不说高级编程语言,即使是汇编、机器码他也无法识别,他最原始的一面是,只能识别 0、1 两个不同的电压信号。
机器码的作用,就是让我们来驱动不同的电压信号组合,来使计算机产生对应的反应。
所以简而言之,我们写代码,其实都只是在按照编程语言提供给我们的规则,来创造一些复杂的组合逻辑,做一些看似很简单的事情。
这个问题往深了说,就说到计算机组成原理、操作系统的本质等等方面了,我目前也只是略知一二,所以就不往这方面展开了。
我们只要明白,其实无论是我们全局安装的模块还是局部安装的模块,运行起来都是同一套逻辑。
甚至就连 npm 本身也就是一个模块,这个模块和其他的第三方模块也没有什么本质方面的区别。
打开全局安装的模块的目录,我们可以看到,有个 node_modules 文件夹,然后目录里面有我们全局安装的模块的命令行运行的脚本。
可以看到的是,与 vue 相关的脚本就有 3 个,这是因为我用 cnpm 安装的缘故,如果你用 npm 安装的,应该就只有两个脚本。
我们分别打开这三个脚本,看看里面的内容:
首先是 vue:
#!/bin/sh basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")case uname in CYGWIN|MINGW|MSYS) basedir=cygpath -w <span class="hljs-string">"<span class="hljs-variable">$basedir</span>"</span>;; esac
if [ -x “ b a s e d i r < / s p a n > / n o d e " < / s p a n > ] ; < s p a n c l a s s = " h l j s − k e y w o r d " > t h e n < / s p a n > < s p a n c l a s s = " h l j s − s t r i n g " > " < s p a n c l a s s = " h l j s − v a r i a b l e " > basedir</span>/node"</span> ]; <span class="hljs-keyword">then</span> <span class="hljs-string">"<span class="hljs-variable"> basedir</span>/node"</span>];<spanclass="hljs−keyword">then</span><spanclass="hljs−string">"<spanclass="hljs−variable">basedir/node” “ b a s e d i r < / s p a n > / n o d e m o d u l e s / v u e − c l i / b i n / v u e " < / s p a n > < s p a n c l a s s = " h l j s − s t r i n g " > " < s p a n c l a s s = " h l j s − v a r i a b l e " > basedir</span>/node_modules/vue-cli/bin/vue"</span> <span class="hljs-string">"<span class="hljs-variable"> basedir</span>/nodemodules/vue−cli/bin/vue"</span><spanclass="hljs−string">"<spanclass="hljs−variable">@” ret= ? < s p a n c l a s s = " h l j s − k e y w o r d " > e l s e < / s p a n > n o d e < s p a n c l a s s = " h l j s − s t r i n g " > " < s p a n c l a s s = " h l j s − v a r i a b l e " > ? <span class="hljs-keyword">else</span> node <span class="hljs-string">"<span class="hljs-variable"> ?<spanclass="hljs−keyword">else</span>node<spanclass="hljs−string">"<spanclass="hljs−variable">basedir/node_modules/vue-cli/bin/vue" " @ < / s p a n > " < / s p a n > r e t = @</span>"</span> ret= @</span>"</span>ret=? fi exit $ret 复制代码
可以看到,这是一个 bash 脚本,可以直接在 Linux 或者 Mac 下运行的。
其次是 vue.cmd:
@SETLOCAL@IF EXIST “%~dp0\node.exe” ( @SET “_prog=%~dp0\node.exe” ) ELSE ( @SET “_prog=node” @SET PATHEXT=%PATHEXT:;.JS;=;% )
“%_prog%” “%~dp0\node_modules\vue-cli\bin\vue” %* @ENDLOCAL 复制代码
这是一个 Windows 批处理脚本,这个脚本也很简单,就是拿到 node 的路径,然后用 node 执行全局模块中的 vue。
最后是 vue.psl:
#!/usr/bin/env pwsh $basedir=Split-Path $MyInvocation.MyCommand.Definition -Parente x e < / s p a n > = < s p a n c l a s s = " h l j s − s t r i n g " > " " < / s p a n > < s p a n c l a s s = " h l j s − k e y w o r d " > i f < / s p a n > ( < s p a n c l a s s = " h l j s − v a r i a b l e " > exe</span>=<span class="hljs-string">""</span> <span class="hljs-keyword">if</span> (<span class="hljs-variable"> exe</span>=<spanclass="hljs−string">""</span><spanclass="hljs−keyword">if</span>(<spanclass="hljs−variable">PSVersionTable.PSVersion -lt “6.0” -or KaTeX parse error: Expected '}', got '#' at position 50: …"hljs-comment">#̲ Fix case when …exe=".exe" } r e t < / s p a n > = < s p a n c l a s s = " h l j s − n u m b e r " > 0 < / s p a n > < s p a n c l a s s = " h l j s − k e y w o r d " > i f < / s p a n > ( < s p a n c l a s s = " h l j s − b u i l t i n " > T e s t − P a t h < / s p a n > < s p a n c l a s s = " h l j s − s t r i n g " > " < s p a n c l a s s = " h l j s − v a r i a b l e " > ret</span>=<span class="hljs-number">0</span> <span class="hljs-keyword">if</span> (<span class="hljs-built_in">Test-Path</span> <span class="hljs-string">"<span class="hljs-variable"> ret</span>=<spanclass="hljs−number">0</span><spanclass="hljs−keyword">if</span>(<spanclass="hljs−builtin">Test−Path</span><spanclass="hljs−string">"<spanclass="hljs−variable">basedir/nodeKaTeX parse error: Expected '}', got '&' at position 25: …>"</span>) { &̲amp; <span clas…basedir/node e x e < / s p a n > " < / s p a n > < s p a n c l a s s = " h l j s − s t r i n g " > " < s p a n c l a s s = " h l j s − v a r i a b l e " > exe</span>"</span> <span class="hljs-string">"<span class="hljs-variable"> exe</span>"</span><spanclass="hljs−string">"<spanclass="hljs−variable">basedir/node_modules/vue-cli/bin/vue" a r g s < / s p a n > < s p a n c l a s s = " h l j s − v a r i a b l e " > args</span> <span class="hljs-variable"> args</span><spanclass="hljs−variable">ret=KaTeX parse error: Expected 'EOF', got '}' at position 21: …XITCODE</span> }̲ <span class="h…exe" " b a s e d i r < / s p a n > / n o d e m o d u l e s / v u e − c l i / b i n / v u e " < / s p a n > < s p a n c l a s s = " h l j s − v a r i a b l e " > basedir</span>/node_modules/vue-cli/bin/vue"</span> <span class="hljs-variable"> basedir</span>/nodemodules/vue−cli/bin/vue"</span><spanclass="hljs−variable">args r e t < / s p a n > = < s p a n c l a s s = " h l j s − v a r i a b l e " > ret</span>=<span class="hljs-variable"> ret</span>=<spanclass="hljs−variable">LASTEXITCODE } exit $ret 复制代码
这是一个 powerShell 脚本,同样也是拿到 node 的路径,然后执行 vue 脚本。
这个写法本身是没问题的,但是会造成在某些电脑上无法使用,比如我的电脑,执行的过程中,会报这样的错误:
接下来,让我们看看用 npm 安装的模块,生成的一键运行的脚本,有何不同呢?
为了公平起见,我们先将用 cnpm 安装的 vue-cli 删除掉:
E:\work2\caidademo>npm uninstall -g vue-cli 复制代码删除成功以后,再用 npm 安装一遍 vue-cli:
E:\work2\caidademo>npm install -g vue-cli 复制代码安装成功以后,我们会发现,这次只生成了两个脚本:
稍微想想就能明白,他们应该是分别运行在类 Unix 系统和 Windows 系统中的脚本。
vue 脚本我们就不看了,让我们来研究下 vue.cmd 脚本与之前的有何差异:
@IF EXIST "%~dp0\node.exe" ( "%~dp0\node.exe" "%~dp0\node_modules\vue-cli\bin\vue" %* ) ELSE ( @SETLOCAL @SET PATHEXT=%PATHEXT:;.JS;=;% node "%~dp0\node_modules\vue-cli\bin\vue" %* ) 复制代码可以看到,差异不大,唯一的差异就是,这个脚本里面直接用 node ,来执行 vue,不是用 "node.exe" ,因为这种写法,在 cmd 中是支持的,但是在 powerShell 中是不支持的。
所以其实我们也能用 cnpm 来管理我们的 node 模块,只是需要改一改 cnpm 给我们自动生成的脚本就行了。
又或许,这就是一个 bug,需要你给 npm 仓库去贡献代码,修改生成脚本的逻辑。
但是 cnpm 的问题肯定不仅仅只是这一个,这个问题只是其中的一个小问题而已。
所以,就像之前说的那样,如果可以的话,尽量用 npm 去管理 nodejs 模块吧。
对于某些曾经推动历史发展,后又淹没在历史的长河中的事务,我们同样保持敬意。