视频链接:www.bilibili.com/video/BV18k…
github代码仓库:github.com/dmokel/GoRa… npm package链接:www.npmjs.com/package/gor… 一些技术博客: 1、redfin.engineering/node-module… 2、gist.github.com/sindresorhu… 3、transang.me/how-to-publ… 4、dev.to/mbarzeev/hy… 5、www.sensedeep.com/blog/posts/… 6、2ality.com/2019/10/hyb… 7、antfu.me/posts/publi… w3c文档:www.w3schools.com/js/js_versi… tsconfig bases仓库:github.com/tsconfig/ba… ts模块系统文档:www.typescriptlang.org/docs/handbo… nodejs团队付出巨大努力来尝试自动解析CJS文件以自动检测可能的命名导出:github.com/nodejs/cjs-… ts模块解析中有关文件拓展名的文档: 1、www.typescriptlang.org/docs/handbo… 2、www.typescriptlang.org/docs/handbo… module output format:www.typescriptlang.org/docs/handbo… cjs与esm互操作性:www.typescriptlang.org/docs/handbo… package.json中字段介绍: 1、nodejs.org/dist/latest… 2、www.typescriptlang.org/docs/handbo… 3、www.typescriptlang.org/docs/handbo…
嗨,大家好,我是Mokel,好久不见,欢迎回来~
在上一期视频里,我们创建了一个local package,即address-discrimination,在需要的时候,我们可以将local package发布到npm,即npm发包,那么这期视频我们来演示一下github发版与npm发包。
我将使用一个独立的仓库来演示github发版与npm发包,在这里我将使用我之前开发的一个工具包,也就是这个GoRarity,它是一个计算NFT稀有度的工具包,稀有度可以理解为是一个NFT的稀有属性评分,分数越高则表示该NFT在其所在的NFT系列中,即项目中,越稀有。它的最终结果如此所示,这里是之前用团队的账号发布的内容,在这期视频里,我会用我个人账号重新演示一下完整的发布过程和最终的发布结果。
我们先来捋一下具体要做哪些事情,首先是github发版,它相对而言会比较简单,我们只需要在仓库内编写workflow文件,从而利用github action自动执行发版操作,而关于发版操作,我们可以复用社区开发者编写好的一些actions,并向它提供github-token以及设置一些参数,如此,github发版即可完成。
然后是npm发包,它相对而言会繁琐一些,整体可以分为三个部分,其一是使用tsc将ts代码编译为js产物和d.ts类型文件,其二是更新package.json文件来定义包的属性,其三是编写workflow文件从而利用github action来自动执行脚本将包发布到npm。这里要补充一下,我之所以没有用上webpack、rollup和esbuild等编译打包工具,一方面是因为我确实还不太熟悉它们,只浅浅的使用过,另一方面是我认为虽然tsc有被诟病,但多熟悉一下tsc对于学习其他的编译打包工具也会有所帮助。
而谈到发包,CommonJS 和 ECMAScript 这两个Module概念必然是无法绕过的,回顾我们过去的几期视频,会发现,我们都没有关注过这两个模块概念,而在这期视频,我们将要对这两者有一定的探究。
简单地说,CommonJS 和 ECMAScript 是 NodeJS 生态中的两个模块系统,原先 NodeJS 生态系统的基础是构建在CommonJS模块系统之上,而后的几年时间里,NodeJS一直致力于支持运行 ECMAScript 模块系统,即 ESModule,现在,NodeJS中对ESM的支持已经实现,尘埃落定。
而ts的模块系统也随着nodejs对ESModule的原生支持而更新,主要体现在module和moduleResolution配置项上,这两个tsconfig的字段连同nodejs所支持的package.json中的新字段type,共同构成了当下我们日常开发中常用的与模块系统相关的配置项。
打开代码仓库,这个仓库是克隆的GoRarity,但我将一些代码和文件删除了,从而重新演示一下github发版和npm发包的过程。打开tsconfig.json文件,可以看到当前该文件中配置项的设置与我们的startup monorepo仓库的tsconfig.option.json的配置项的设置一致,然后进行一些修改,如我所演示:
根据ts文档,我们可以知道,target不受其他配置项的影响与控制,修改target的值也会修改lib的默认值,而module的默认值也会受到target的影响,moduleResolution的默认值则受到module值的影响。
target 决定了由ts代码编译出来的js代码中哪些js特性被降级,哪些js特性被保留,例如,如果我们的ts代码中有箭头函数 () => this ,而我们的target设置为 es5 或更低即es3,那么箭头函数会被转换为等价的函数表达式,如我所演示:
target的默认值为es3,即javascript es3,但这显然是一个过于老旧的javascript版本,你可以查看w3c文档www.w3schools.com/js/js_versi… 了解迄今为止所有的javascript版本。项目中target配置值的选择逻辑可以概述为:将target值设置为你打算支持的最低ES版本可以确保tsc发出的js代码不会使用更高版本中引入的语言功能;由于target值还影响了lib的默认值,这也确保你不会错误访问旧环境中可能已经不可用的全局变量。
tsconfig提供了一个bases仓库,其中提供了不同场景下可供借鉴的配置项的参考设置值,你可以查看github文档了解更多详细信息github.com/tsconfig/ba…
而lib的作用是指定项目内所包含的ts类型定义,也就是编写ts代码时的类型提示,根据上述描述可知,我们可以不显式指明lib的配置值,其默认值会自动根据target的值调整,在我个人的实际开发中,也经常选择不显式指定lib的配置值,这样能够避免可能出现的因限制lib而导致缺少部分ts类型文件的问题,如果再遇到其他问题则再根据实际情况调整。
module和moduleResolution决定了ts代码编译的js代码的模块类型和对应的模块解析策略,在最新的ts模块系统方案中,当module和moduleResolution均设置为node16时,结合package.json中的type字段,会决定ts代码编译的js代码的模块类型,以及决定d.ts类型文件被解释为CommonJS模块还是ES模块,需要注意的是,ts为d.ts类型文件检测到的模块类型需要与nodejs为相应的js文件检测到的模块类型相匹配,这个一般都能被满足,但这意味着,CommonJS入口点和ESModule入口点都需要自己的声明文件,即使它们之间的内容相同。
关于ts模块系统的更详细更深入的信息,你可以阅读ts文档获取更多www.typescriptlang.org/docs/handbo…
简单了解了ts中有关模块系统的配置项及其作用后,回到将ts源码编译为js产物和d.ts类型文件并发包的操作过程,我们可以分为以下几种不同的情况:
1、CommonJS module Only,即仅输出CommonJS模块系统的js产物,以CommonJS模块npm包发布
2、ES module Only,即仅输出ES模块系统的js产物,以纯ES模块npm包发布
3、Hybrid npm module for ES and CommonJS,即同时输出CommonJS和ES模块系统的js产物,以混合模式npm包发布
上述三种做法我个人认为并没有纯技术上的绝对优劣之分,如何选择更多是工程、边界情况与生态系统的综合考量,我在简介中提供了一些技术博客, 其中有不同工程师或团队的一些差异化的观点,你可以阅读这些博客了解更多,同时,ts模块系统的文档也是你深入了解研究此问题的非常重要的资料之一。
顺带一提,如果你并不是需要维护一个npm包,而仅仅是开发一个项目,那么在项目中使用ESModule是非常值得被采纳的方式。
接下来,我将依次粗略演示上述三种做法并对npm包进行一定的本地测试来进一步探究ts编译、npm发包和模块系统的相关内容。
首先是CommonJS module Only,更新package.json文件,如我所演示:
然后在终端中执行pnpm run build命令编译js产物和d.ts文件,我们来比较一下ts源代码和编译后的js产物以及d.ts类型文件:
可以看到,tsc对我们的ts代码进行了一定的编译转译,并且转译结果是CommonJS模块系统的js代码。
我们新建一个temp-test项目来对该package进行本地测试,创建并填充package.json和tsconfig.json文件,tsconfig.json文件延续我们之前的内容,package.json文件简单填充一些,然后在src文件夹下新建index.ts文件,我们先通过pnpm link ../GoRarity-Temp命令在本地link一下gorarity包,这样就可以在测试项目内使用gorarity包了,然后我们在index.ts文件中编写用于测试我们的gorarity包的代码,如我所演示:
回到该测试项目的package.json文件中,将type字段设置为commonjs,这表明我们的测试项目使用CommonJS模块系统,其含义为当我们利用tsc对该测试项目的ts代码进行编译转译时,它会转译输出commonjs模块系统的js代码。我们在终端执行pnpm run build编译ts代码,注意此时dist文件夹下的js代码,它是commonjs module格式,然后通过node ./dist/index.js运行代码,可以看到代码正常运行,输出了预期的结果。
然后我们将type字段变更为module,这表明我们的测试项目改为使用ES模块系统,再次通过pnpm run build编译ts代码,编译完成后,查看此时dist文件夹下的js代码,它是ES module格式,然后通过node ./dist/index.js运行代码,可以看到代码也是正常运行,输出了预期的结果。
此时,你是否产生了一些疑惑?当前,该gorarity包是CommonJS npm package,我们的测试项目是ESModule,根据常规认知,ESM是无法以命名导入方式导入CJS的,主要因为CJS和ESM对它们各自命名导出的计算是在不同阶段进行的,CSJ在执行阶段,ESM在解析阶段,以lodash package为例,你可以使用default import方式import _ from './lodash.cjs',但无法执行命名导入import { shuffle } from './lodash.cjs',但在我们的测试项目中却发生了意外,我们在ESM中以命名方式导入了CJS的gorarity包的类,这是因为“nodejs的static analysis,从而有时nodejs可以从CJS合成命名导出”,你可以查看github文档了解更多有关nodejs团队付出巨大努力来尝试自动解析CJS文件以自动检测可能的命名导出的信息github.com/nodejs/cjs-…
至此,在通过撰写ts代码并使用tsc编译js产物和d.ts类型文件来创建npm包的情况下,我们完成了仅输出CommonJS模块系统的js产物,以CommonJS module创建npm包并进行本地测试的探究。CommonJS module Only是一种观点,它可以阐述为:如果你必须发布CommonJS module,那么你就只发布CommonJS module,并且测试你的CJS library能否按照你的预期在ESModule下正常工作,如果不能,则为你的CJS library提供一个轻量级ESM 包装器,使其能够按照你的预期正常工作;但请注意,以我们的演示项目为例,你无法简单地利用tsc从ts代码转译输出ESM包装器,它的创建你将需要手动进行操作,或者开发命令行工具来自动化操作,但这两者都不是简单而轻松的工作,所以,是否选择CommonJS module Only方式,是一个你需要综合考量的问题。
另一方面,我们也可以看到一些有关Pure ESM package的技术博客,这是ES module Only的观点,它可以概述为:如果你完全不需要提供CommonJS module 的npm包,那么仅输出ES模块系统的js产物,以纯ES模块npm包发布就是非常被推荐的方式,因为ESM是未来,开发者们正在进行一场运动,旨在让每个人都只编写ESM代码,与此同时,社区也正在逐渐迁移到ESM。
更新我们的GoRarity项目,只需要将pacage.json中的type字段变更为module即可,然后我们执行pnpm run build编译ts代码,会发现ts抛出了非常多Relative import paths need explicit file extensions in EcmaScript imports when '--moduleResolution' is 'node16' or 'nodenext'的报错信息,这是ts最新模块系统针对ESM中解析相对路径导入的模块的新规则,本质也是nodejs针对ESM中解析相对路径导入的模块的新规则。这里,我们根据报错信息的提示,对所有涉及到的文件进行更新。可以看到,ts对此都有智能提示,注意文件拓展名是.js。你可以查看ts文档了解更多关于模块解析中的文件拓展名的信息。
www.typescriptlang.org/docs/handbo…
www.typescriptlang.org/docs/handbo…
更新完成后,执行pnpm run build构建js产物和d.ts文件,我们来比较一下ts源代码和编译后的js产物以及d.ts类型文件:
从格式上看,它们显然是 相同的,而且,相较于CommomJS module下的js代码,它们干净简洁直观,这正是在nodejs对ESModule完全支持后给JsTs生态带来的一些变化 。你可以查看ts的文档了解更多关于moudle output format 的信息。
www.typescriptlang.org/docs/handbo…
切换到我们的temp-test测试项目,此时测试项目是使用ES模块系统,即package.json中的type字段设置为module代码中没有任何错误信息提示,在终端中执行pnpm run build命令编译js产物,编译出来的js代码简洁直观,是ES module格式,然后通过node ./dist/index.js运行代码,可以看到代码也是正常运行,输出了预期的结果。
将我们的测试项目改为使用CommonJS模块系统,即更新package.json文件中的type字段,将其设置为commonJS,可以看到代码中立马有了ts反馈的错误信息:The current file is a CommonJS module whose imports will produce 'require' calls; however, the referenced file is an ECMAScript module and cannot be imported with 'require'. Consider writing a dynamic 'import("@nftgo/gorarity")' call instead.
(当前文件是CommonJS module,该文件的 import语句会被转译为 require 调用,但是,我们依赖的文件,即gorarity包的代码文件,是ES module,ESModule的文件不能通过require导入。你可以考虑使用动态import语句来导入gorarity这个包)
这个报错源于ESM和CJS之间的互操作性相关的问题,报错信息已经清晰明了地描述了问题所在,所以我们不再进行CJS 导入 ESM 的测试,你可以查看ts的文档了解更多关于两者互操作性的内容。www.typescriptlang.org/docs/handbo…
至此,我们回顾一下:如果你完全不需要提供CommonJS module 的npm包,那么仅输出ES模块系统的js产物,以纯ES模块npm包发布就是非常被推荐的方式,并且,这将促使你的npm包的下游用户也转向纯ESM包或纯ESM项目,当然,对于纯ESM模块,得益于ESM和CJS之间的互操作性,它也仍然能够以特定但不完美的方式导入 CommonJS module,这是对于逐步迁移到ESM生态非常有帮助的一个特性。
最后一种发布方式我们将其称为混合模式,在一些技术博客中,它是被推荐的方式,你可以查看简介中的文章或者搜索更多的相关信息详细了解,但需要注意的是,混合模式存在至少两个显而易见的限制或问题:
其一是混合模式发布的npm包的所有依赖都必须支持CommonJS,否则我们将需要通过dynamic import()的方式来导入纯ESM包,这是一种带有传染性污染的编码方式,会给下游用户带来不便。
其二是用户可能会意外地既import你的ESM脚本又require你的CJS,这是非常危险的不可控情况。具体地说,例如,假设一个库 omg.mjs 依赖于 index.mjs,而另一个库 bbq.cjs 依赖于 index.cjs,然后另外一个项目同时依赖 omg.mjs 和 bbq.cjs,nodejs 通常会删除重复的模块,但 nodejs 不知道你的 CJS 和 ESM 是“相同”的文件,因此你的代码将运行两次,保留库状态的两个副本,这可能会导致各种奇怪的错误。
需要声明的是,我并非在视频中提供或传递某种倾向性的选择,而是以我个人实际开发经历来尽可能贴近实际去阐述和演示上述三种模式,如果你想要进一步更深入地探究这个主题,我强烈地建议你阅读我在简介中提供的技术博客,并且持续关注nodejs、ts以及其他编译器或运行时的官方文档和技术动态。我在视频中阐述的所有内容尽可能做到没有重大错误和严重误导,但无法保证完全正确和一直正确。
回到我们的GoRarity项目,我们需要对它进行一定的更新,将tsconfig.json重命名为tsconfig-base.json,注意需要删除module、moduleResolution和outDir配置,然后分别创建tsconfig-cjs.json和tsconfig.json文件,并向其中填入内容,如我所演示:
我们在这里创建了两个ts配置文件,它们用于生成不同的编译产物。
然后更新package.json中的compile脚本,更新为"compile": "tsc -p tsconfig.json && tsc -p tsconfig-cjs.json && ./fixup",其含义是使用tsc工具分别应用两个tsconfig文件从而编译输出两组产物,compile脚本还执行了一个fixup shell脚本,该shell脚本的作用是向dist/mjs 和 dist/cjs 文件夹内分别写入一个package.json文件,用于控制对应文件夹下的代码文件的模块类型,这里分别为CommonJS Module 和 ES Module。此外,我们还需要更新和新增package.json中的这几项配置项:
更新好后,我们在终端中执行pnpm run build命令编译ts代码,编译完成后,可以看到dist/文件夹下分别有cjs和mjs子文件夹,分别查看 一下它们的index.js和package.json文件,都符合我们的预期。
切换到temp-test测试项目,我们来测试一下以混合模式发布的gorarity,测试的方式比较简单,我们只需要更新package.json中的type字段即可,先设置为module,然后在index.ts文件中试着跳转到gorarity包,可以看到,跳转到了gorarity的dist/mjs文件夹下的类型文件。然后在终端中执行pnpm run build编译ts代码,编译结果如我所示, 然后执行node ./dist/index.js运行脚本,正常执行,正确输出。
再将type字段设置为commonjs,然后再次在index.ts文件中跳转到gorarity包,此时,跳转到了gorarity的dist/cjs文件夹下的类型文件,结合一下刚刚的跳转情况,这意味着我们在gorarity的package.json中设置的exports字段生效了。然后在终端中执行pnpm run build编译ts代码,编译结果如我所示, 然后执行node ./dist/index.js运行脚本,可以看到,也是正常执行,正确输出。
显而易见,以混合模式发布的gorarity包,通过了我们的temp-test项目的测试,我们可以一定程度上地认为,在忽略之前提到的混合模式的限制或问题的情况下,以混合模式发布的gorarity包可以正常工作了。
最后我们补充说明一下刚刚在gorarity项目的package.json中更新和新增的字段,其中exports是nodejs中新的用于替代main字段的新字段,它以更严格的方式指示了该package的入口点,我们定义exports字段的方式称为conditional exports,关于exports字段的更详细的解释以及其他字段的信息,你可以查看nodejs文档和ts文档。
www.typescriptlang.org/docs/handbo…
www.typescriptlang.org/docs/handbo…
至此,我们完成了三种npm包发布模式的演示与测试,接下来我们来编写github workflow文件从而利用github action来最终完成github发版和npm发包的操作。
回到GoRarity项目,可以看到,这里已经有我之前写好并且测试过的workflow文件了,在此之前,我们还需要更新一下package.json文件,在其中新增一些内容,如我所演示:
然后看我之前写好的的pre-release.yml文件和release.yml文件:
它们本质是定义了一个github workflow,该workflow用于执行github发版操作和npm发包操作,两者的区别在于github发版步骤里对prerelease配置项的设置,以及执行pnpm脚本步骤中是否添加了beta tag。我们先来实操演示一下。
把整理好的代码推送到github,然后我们需要在仓库内新增一个npm的sceret token,你需要登陆npmjs,然后在access Token这里生成一个token,我目前使用的还是classic Token生成模式,生成后复制并添加到github仓库中,命名为NPM_TOKEN。
添加好后,我们回到vscode中,由于npmjs报错v1.0.0版本号已经被使用了,这是因为我之前测试npm package的发布时使用了v1.0.0版本号,实际上我已经在很久之前删除了测试发布的npm package,不确定是什么原因导致npmjs始终没有正确清理我已经删除的package,所以在这里我将package.json中的version字段更新为v2.0.0,然后提交一个commit推到github;接下来,我们先在本地给仓库打tag,这里我们不演示pre-release,所以直接打正式的tag v2.0.0,同时我们还需要给第一个commit打一个tag v0.0.1,因为我所使用的marvinpinto/action-automatic-releases@latestaction需要获取两个tag对应的commit之间的commit信息来生成release note。
然后把tag推送到github,从而触发Rlease action;在github仓库actions页面可以看到有一个Release action正在运行;等它运行成功,我们可以看到github release 有了一个 latest version,同时npmjs中有我们刚刚发布的gorarity package,这意味着我们的Release已经执行成功了。
我们来具体看一下latest version,其中有根据commit message自动生成的release note,符合我们的预期。然后回到npmjs中,具体看一下发布的gorarity package。
首先是gorarity相较于@nftgo/gorarity有一个TS的标识,这是因为我们在项目的package.json中提供了types字段,缺少该字段则没有TS标识;然后则是常规的README 和底部的 keywords,以及Repository和Homepage跳转链接;在code页面,则是我们在package.json中的files字段指定的文件夹下的内容会上传发布在此处;其他则是一些常规信息。
至此,我们完成了github发版与npm发包的所有内容,其中较为关键的是对ts和nodejs的模块系统及模块解析策略的初步的整体的认识,但我所能分享的内容也非常有限,希望大家在评论区分享讨论探讨,从而更进一步准确的深入的理解模块系统和模块解析策略,我是Mokel,我们下期视频不见不散!