一个改善开发体验的小脚本"自动新建组件并且打开浏览器",主要功能是:
- 以已有组件为原型,新建组件,其目录结构保持一致(源码、单元测试、README 等),内容替换成新组件。
- 创建成功后,自动开启 dev server。server ready 后浏览器自动打开组件所在页面。
这个脚本主要目的是解决重复劳动减轻每次新建组件的工作量,以及规范团队的目录结构,还有一个好处是保障单元测试的编写。功能点1主要是复制文件和和替换内容,本文着重第二个功能。
这里有几个难点,如何设计一个通用的控制台输出监控脚本,从输出中获取想要的信息比如 host,难点在通用,即可以是任何框架不依赖特定参数或事件。
先看看效果:
Before

After

可以看到保持了原来的输出以及颜色,加入了自定义日志,并先后成功检测到了 host 启动和编译完成。整个过程没有依赖任何构建工具提供的事件。
接下来我们将设计一个通用的方案。
思路:执行命令
=> 拦截输出
=> 匹配输出
=> 执行回调
执行命令
用 spawn 而非 exec,因为构建命令是流式输出的,我们需要随时监听并做出想要的业务动作,而 exec 有 buffer 会批量输出。其次通过 spawn 我们可以很方便执行重定向等操作。
js
const child = spawn(cmd, cmdArgs, {
stdio: 'pipe',
// 显式启用 shell,否则 Error: spawn npm ENOENT
shell: true,
env: {
...process.env,
// 强制启用 真彩色(TrueColor) 或 16 万色(24-bit RGB)支持。
// 否则没有颜色
FORCE_COLOR: '3',
},
})
接下来我们将输出重定向到终端,以及重定向到我们的拦截逻辑。
ts
// 手动将子进程的 stdout/stderr 转发到父进程,这样终端才能实时输出构建日志
child.stdout.pipe(process.stdout)
child.stderr.pipe(process.stderr)
child.stdout.on('data', (data) => {
const output = data.toString()
// 这里监听输出执行业务逻辑
console.log('[output]:', output)
})
真正核心内容就这些,我们仅靠 Node.js 的能力,没有使用构建工具提供事件,做到了监听输出内容并且仍然保留原日志输出的内容和色彩,而且相对更灵活我们可以做任何事情。
接下来就是针对输出内容截取有用信息然后"反应"即可。
封装以便使用:
ts
import { spawn } from 'node:child_process'
export function chooseOpenCmd(): string {
// 根据不同平台打开浏览器
const openCommand =
process.platform === 'win32' ? 'start' : process.platform === 'darwin' ? 'open' : 'xdg-open'
return openCommand
}
type IExecuteParams = {
cmd: string
cmdArgs?: string[]
onOutput: (output: string) => void
}
export function execute({ cmd, cmdArgs = [], onOutput }: IExecuteParams) {
const devCmd = cmd + ' ' + cmdArgs.join(' ')
console.log('exec: `' + devCmd + '`')
// 启动 npm run dev 并捕获输出
const child = spawn(cmd, cmdArgs, {
stdio: 'pipe',
// 显式启用 shell,否则 Error: spawn npm ENOENT
shell: true,
env: {
...process.env,
// 强制启用 真彩色(TrueColor) 或 16 万色(24-bit RGB)支持。
// 否则没有颜色
FORCE_COLOR: '3',
},
})
// 手动将子进程的 stdout/stderr 转发到父进程
child.stdout.pipe(process.stdout)
child.stderr.pipe(process.stderr)
child.stdout.on('data', (data) => {
const output = data.toString()
onOutput(output)
// console.log('[output]:', output)
})
}
监听并获取 host
ts
let url: string | undefined
execute({
cmd: `npm`,
cmdArgs: ['run', 'dev'],
onOutput: (output) => {
if (output.includes('http://localhost')) {
// match url from output `> Local: http://localhost:8001`
url = output.match(/(http:\/\/localhost:\d+)/)?.[1]
log(
chalk.green(
'本地服务器已启动,正在等待编译完成,编译完成将自动打开该组件对应的浏览器地址...',
),
)
}
},
})
逻辑很直接:从输出匹配 host 然后保存到 url 变量中,打印一行日志,等待编译完成再浏览器打开该地址,当然还会拼接上组件名字,这样不用我们每次从主页进入点击多次才能进入组件文档页。
小问题:如果立即输出日志,将打乱原来输出,我们需要延迟下:
ts
// 防止输出打断 npm run dev 日志
setTimeout(() => {
console.log()
log(
chalk.green(
'本地服务器已启动,正在等待编译完成,编译完成将自动打开该组件对应的浏览器地址...',
),
'\n',
)
}, 200)
监听构建完成事件
同理,从实时输出的日志匹配 [Webpack] Compiled in
即构建成功日志(你可以灵活调整成自己构建工具表示构建成功的的字符串),然后给第一步获取的 url 拼接组件名浏览器打开:
ts
execute({
cmd: `npm`,
cmdArgs: ['run', 'dev'],
onOutput: (output) => {
if (output.includes('[Webpack] Compiled in ')) {
// 这里没有考虑非 Windows 系统。如果需要则使用 open npm 包
openInBrowserCmd = `start ${url}/components/${hyphenatedComponentName}`
console.log()
log(chalk.green('检测到编译完成,打开浏览器', openInBrowserCmd))
if (dryRun) {
log('execSync:', openInBrowserCmd)
return
}
execSync(openInBrowserCmd, { stdio: 'inherit' })
}
},
})
这里直接使用 start
打开浏览器,没有考虑非 Windows 系统。其次我们无需实时获取打开浏览器的日志,简单起见使用 execSync,传入 inherit 让其日志能如实显示到终端,当然这里可以删除该参数,因为没有日志。
有些问题需要处理,比如如果第一步没有匹配到 url 怎么办?修改文件导致二次触发构建完成会重复打开浏览器,不想安装 npm 包能否做到兼容非 Windows 系统。下面是比较完整的脚本:
ts
const open = chooseOpenCmd()
let opened = false
execute({
cmd: `npm`,
cmdArgs: ['run', 'dev'],
onOutput: (output) => {
if (output.includes('[Webpack] Compiled in ')) {
if (!url) {
throw new Error('无法获取本地服务器地址')
}
// 防止 hot reload 二次编译重复打开浏览器
if (opened) {
return
}
opened = true
openInBrowserCmd = `${open} ${url}/components/${hyphenatedComponentName}`
console.log()
log(chalk.green('检测到编译完成,打开浏览器', openInBrowserCmd))
if (dryRun) {
log('execSync:', openInBrowserCmd)
return
}
execSync(openInBrowserCmd, { stdio: 'inherit' })
}
},
})
ts
export function chooseOpenCmd(): string {
// 根据不同平台打开浏览器
const openCommand =
process.platform === 'win32' ? 'start' : process.platform === 'darwin' ? 'open' : 'xdg-open'
return openCommand
}
完整代码详见 github。
效果
ts
❯ npm run new Triangle -- --category=dataDisplay --title='三角形'
> @neural/[email protected] new
> node --experimental-strip-types scripts/create-component.ts Triangle --category=dataDisplay --title=三角形
[create-component] Create component "Triangle" from template "JSONInput" success: 16.631ms
[create-component] execSync: `npm run dev`
> @neural/[email protected] dev
> npm run check-node-version && dumi dev
info - dumi v2.2.17
info - Umi v4.1.5
Browserslist: caniuse-lite is outdated. Please run:
npx update-browserslist-db@latest
Why you should do it regularly: https://github.com/browserslist/update-db#readme
╔════════════════════════════════════════════════════╗
║ App listening at: ║
║ > Local: http://localhost:8001 ║
ready - ║ > Network: http://10.88.103.87:8001 ║
║ ║
║ Now you can open browser with the above addresses↑ ║
╚════════════════════════════════════════════════════╝
[create-component] 本地服务器已启动,正在等待编译完成,编译完成将自动打开该组件对应的浏览器地址...
event - [Webpack] Compiled in 21389 ms (7453 modules)
[create-component] 检测到编译完成,打开浏览器 start http://localhost:8001/components/triangle
可以看到我们拿到了 url 以及打开了浏览器。