大家好,我是码农小余。
今天…… 女同事问小余:你平时是怎么高效地积累这么多好用的第三方包? 小余:就没事多逛 github,多点开 package.json 看看。
今天我们就“单纯”地从 VueUse scripts 入手,从中探索我们后续也许会用上的第三方包库。
先总的看一下 VueUse 的 script:
"scripts": {
"build": "nr update && esno scripts/build.ts",
"build:redirects": "esno scripts/redirects.ts",
"build:rollup": "cross-env NODE_OPTIONS=\"--max-old-space-size=6144\" rollup -c",
"build:types": "tsc --emitDeclarationOnly && nr types:fix",
"clean": "rimraf dist types packages/*/dist",
"dev": "nr update && nr docs",
"docs": "vitepress dev packages --open",
"docs:build": "nr update:full && vitepress build packages && nr build:redirects && esno scripts/post-docs.ts",
"docs:serve": "vitepress serve packages",
"lint": "eslint .",
"lint:fix": "nr lint --fix",
"publish:ci": "esno scripts/publish.ts",
"install-fonts": "gfi install Inter && gfi install Fira Code",
"release": "esno scripts/release.ts && git push --follow-tags",
"size": "esno scripts/export-size.ts",
"test": "nr test:3",
"test:2": "vue-demi-switch 2 vue2 && vitest run --silent",
"test:3": "vue-demi-switch 3 && vitest run",
"test:all": "nr test:3 && nr test:2 && vue-demi-switch 3",
"test:watch": "vitest --watch",
"typecheck": "tsc --noEmit",
"types:fix": "esno scripts/fix-types.ts",
"update": "nr -C packages/metadata update && esno scripts/update.ts",
"update:full": "nr update && nr build:types",
"watch": "esno scripts/build.ts --watch"
}
简述每一条脚本的职责:
接下来我们重点了解一些常用和复杂的脚本——update、build、release、size。
update
脚本执行的是
nr -C packages/metadata update && esno scripts/update.ts
nr[1] 也许你之前没有接触过,它主要的作用:
前半部分 nr -C packages/metadata update
表示切换到 packages/metadata
目录然后执行 update
脚本。(补充:也可以通过 pnpm run update --filter @vueuse/metadata 完成一样的动作,项目中使用了 @antfu/ni[2] 所以通过 nr -C 包管理更加一致)。
回到 metadata 包,我们详细了解 update:
import { ecosystemFunctions } from '../../../meta/ecosystem-functions'
import { packages } from '../../../meta/packages'
// 读取整个 VueUse 的元数据
export async function readMetadata() {
// indexes 包括 packages、categories、functions 3个属性
const indexes: PackageIndexes = {
packages: {},
categories: [],
functions: [
...ecosystemFunctions,
],
}
// ... 省略一堆处理代码
for (const info of packages) {
// ...
}
return indexes
}
async function run() {
const indexes = await readMetadata()
await fs.writeJSON(join(DIR_PACKAGE, 'index.json'), indexes, { spaces: 2 })
}
run()
readMetadata 方法比较简单,从 meta/ecosystem-functions
和 meta/packages
读取元数据,生成 indexes 结果并写入 index.json 文件,打开 index.json 瞄一眼:
{
"packages": {
"shared": {
"name": "shared",
"display": "Shared utilities",
"dir": "packages/shared"
},
"core": {
"name": "core",
"display": "VueUse",
"description": "Collection of essential Vue Composition Utilities",
"dir": "packages/core"
},
// ...
},
"categories": [
// ...
],
"functions": [
{
"name": "computedAsync",
"package": "core",
"lastUpdated": 1651597361000,
"docs": "https://vueuse.org/core/computedAsync/",
"category": "Utilities",
"description": "computed for async functions",
"alias": [
"asyncComputed"
]
},
{
"name": "computedEager",
"package": "shared",
"lastUpdated": 1645956777000,
"docs": "https://vueuse.org/shared/computedEager/",
"category": "Utilities",
"description": "eager computed without lazy evaluation",
"alias": [
"eagerComputed"
]
}
// ...
]
}
上述 json 就跟 VueUse 文档中的信息[3]对应上了。抛开这些,我们看看 readMeta 中那些好用的包:
git log -1 --format=%at xx
获取的结果;写入信息之后,update 会执行 esno scripts/update.ts
:
import fs from 'fs-extra'
import { metadata } from '../packages/metadata/metadata'
import { updateContributors, updateCountBadge, updateFunctionREADME, updateFunctionsMD, updateImport, updateIndexREADME, updatePackageJSON, updatePackageREADME } from './utils'
async function run() {
await Promise.all([
// 更新各个包的入口文件 index.ts
updateImport(metadata),
// 更新每个包的 README
updatePackageREADME(metadata),
// 更新根目录的 README
updateIndexREADME(metadata),
// 更新附加组件
updateFunctionsMD(metadata),
// 更新方法的 README
updateFunctionREADME(metadata),
// 更新每个包的 package.json
updatePackageJSON(metadata),
// 更新计数徽章
updateCountBadge(metadata),
// 更新贡献者
updateContributors(),
])
await fs.copy('./CONTRIBUTING.md', './packages/contributing.md')
}
run()
本文主要是借助 VueUse 去了解 monorepo 项目的脚本框架,不会深入每个操作细节,有需求再回来查看每个函数逻辑即可。
至此,就完成了整个 update 操作,我们接着看 build 过程。
build 脚本执行以下命令:
nr update && esno scripts/build.ts
build 依赖 update 的执行,我们进入 scripts/build.ts
:
if (require.main === module)
cli()
这个判断给你 3s 思考是想表达什么?1-2-3,时间到,请看答案:
当文件直接从 Node.js 运行时,则
require.main
被设置为其module
。这意味着可以通过测试require.main === module
来确定文件是否被直接运行。 对于文件foo.js
,如果通过node foo.js
运行,则为true
,如果通过require('./foo')
运行,则为false
。 当入口点不是 CommonJS 模块时,则require.main
为undefined
,且主模块不可达。
然后我们进入 build 流程:
import { metadata } from '../packages/metadata/metadata'
async function build() {
consola.info('Clean up')
exec('pnpm run clean', { stdio: 'inherit' })
consola.info('Generate Imports')
// 从上述 update 过程生成的 index.json 中获取,所以脚本顺序上是依赖 nr update 的
await updateImport(metadata)
consola.info('Rollup')
exec(`pnpm run build:rollup${watch ? ' -- --watch' : ''}`, { stdio: 'inherit' })
consola.info('Fix types')
exec('pnpm run types:fix', { stdio: 'inherit' })
await buildMetaFiles()
}
build 逻辑也很清晰:
pnpm run clean
清除上一次构建结果,也就是删除 types、dist 等目录;等等,这里为什么不用 nr
而用了 pnpm run
?建议给 antfu[8] 提个 PR~await updateImport(metadata)
生成每个包的入口文件(index.ts);pnpm run build:rollup
通过 rollup 完成构建动作,构建配置待会再看;pnpm run types:fix
完成 @vue/composition-api、vue 到 vue-demi 的类型修复;整个流程清楚之后,我们来深入学习 build:rollup:
/* eslint-disable no-global-assign */
// 使用 esbuild 即时转换 JSX、TypeScript 和 esnext 功能
require('esbuild-register')
module.exports = require('./scripts/rollup.config.ts')
查阅 ./scripts/rollup.config.ts
:
const esbuildPlugin = esbuild()
const esbuildMinifer = (options: ESBuildOptions) => {
const { renderChunk } = esbuild(options)
return {
name: 'esbuild-minifer',
renderChunk,
}
}
// 96-108 行
{
file: `packages/${name}/dist/${fn}.iife.min.js`,
format: 'iife',
name: iifeName,
extend: true,
globals: iifeGlobals,
plugins: [
injectVueDemi,
esbuildMinifer({
minify: true,
}),
],
}
// 112-125
configs.push({
input,
output,
plugins: [
target
? esbuild({ target })
: esbuildPlugin,
json(),
],
external: [
...externals,
...(external || []),
],
})
rollup 的配置只截取了部分配置,重点学习 rollup 构建中使用 esbuild 能力的过程。在插件部分,iife 输出格式上使用了 esbuild[9] 的 minify [10] 配置,对于指定 target 的构建需求上,使用了 esbuild 的 target[11] 配置。
构建完了,接下来就看看发版(release)、发布(build)流程
release 流程执行的是:
esno scripts/release.ts && git push --follow-tags
查阅 scripts/release.ts
:
import { execSync } from 'child_process'
import { readJSONSync } from 'fs-extra'
// 读取 package.json 中的 version 字段
const { version: oldVersion } = readJSONSync('package.json')
// 自动化发布过程
execSync('npx bumpp', { stdio: 'inherit' })
// 再次读取 version 字段
const { version } = readJSONSync('package.json')
// 比较新旧版本、如果一致就退出进程
if (oldVersion === version) {
console.log('canceled')
process.exit()
}
// 类型声明构建
execSync('npm run build:types', { stdio: 'inherit' })
// 执行更新
execSync('npm run update', { stdio: 'inherit' })
// add
execSync('git add .', { stdio: 'inherit' })
// commit
execSync(`git commit -m "chore: release v${version}"`, { stdio: 'inherit' })
// 打tag
execSync(`git tag -a v${version} -m "v${version}"`, { stdio: 'inherit' })
release 流程非常清晰,上述代码中有一个包引起我的注意力:bumpp[12] 基于 `version-bump-prompt`[13] 添加了以下特性:
npx bumpp
;--execute
以在提交前执行命令;最后来看 publish 过程:
esno scripts/publish.ts
流程如下:
import { packages } from '../meta/packages'
// 执行包的构建
execSync('npm run build', { stdio: 'inherit' })
let command = 'npm publish --access public'
// 如果 version 包含 beta 就在发布命令上添加 tag
if (version.includes('beta'))
command += ' --tag beta'
// 依次发布每一个包
for (const { name } of packages) {
execSync(command, { stdio: 'inherit', cwd: path.join('packages', name, 'dist') })
consola.success(`Published @vueuse/${name}`)
}
publish 先执行包的构建,然后定义发布命令,如果 version 中包含 beta 字段,就在发布参数上打上 tag,可以在终端执行 npm info @vueuse/core 查看 beta 包:
最后循环包数据 packages 依次执行 npm publish --access public
进行发包。整个流程可以通过 vueuse 的 publish action[14] 进一步查看。
VueUse 有一个完全 tree shaking 的特性,并展示了每个函数构建后的体积。export-size[15] 页面可以查看到每个函数的大小:
这是怎么实现的呢?答案是在 scripts 中可以看到一条 esno scripts/export-size.ts
的脚本:
import { markdownTable } from 'markdown-table'
import { getExportsSize } from 'export-size'
import filesize from 'filesize'
import fs from 'fs-extra'
import { version } from '../package.json'
import { packages } from '../meta/packages'
async function run() {
// ...
for (const pkg of [...packages.slice(1), packages[0]]) {
const { exports, packageJSON } = await getExportsSize({
pkg: `./packages/${pkg.name}/dist`,
output: false,
bundler: 'rollup',
external: ['vue-demi', ...(pkg.external || [])],
includes: ['@vueuse/shared'],
})
// ...
md += markdownTable([
['Function', 'min+gzipped'],
...exports.map((i) => {
return [`\`${i.name}\``, filesize(i.minzipped)]
}),
])
md += '\n\n'
}
await fs.writeFile('packages/export-size.md', md, 'utf-8')
}
遍历 packages 列表,调用 getExportsSize
依次生成每个包下函数的信息:
然后通过以下两个包的处理
最终将 md 字符串写入 export-size.md。
本文从 VueUse 项目的 scripts 作为切入点,通过阅读 update、build、release、publish、size 等脚本的源码,不仅接触了大量好用的包,而且后续如果要自定义构建、更新版本、发包等流程,也能轻松应对~
[1]
nr: https://www.npmjs.com/package/@antfu/ni
[2]
@antfu/ni: https://www.npmjs.com/package/@antfu/ni
[3]
VueUse 文档中的信息: https://vueuse.org/functions.html
[4]
gray-matter: https://www.npmjs.com/package/gray-matter
[5]
front matter: https://vuepress.vuejs.org/zh/guide/frontmatter.html#%E5%85%B6%E4%BB%96%E6%A0%BC%E5%BC%8F%E7%9A%84-front-matter
[6]
simple-git: https://www.npmjs.com/package/simple-git
[7]
fast-glob: https://www.npmjs.com/package/fast-glob
[8]
antfu: https://github.com/antfu
[9]
esbuild: https://esbuild.github.io/
[10]
minify : https://esbuild.github.io/api/#minify
[11]
target: https://esbuild.github.io/api/#target
[12]
bumpp: https://www.npmjs.com/package/bumpp
[13]
version-bump-prompt
: https://github.com/JS-DevTools/version-bump-prompt
[14]
publish action: https://github.com/vueuse/vueuse/runs/6670930037?check_suite_focus=true
[15]
export-size: https://vueuse.org/export-size.html
[16]
filesize: https://www.npmjs.com/package/filesize
[17]
markdownTable: https://www.npmjs.com/package/markdown-table