为什么我们需要一套脚手架
为什么我们需要一套脚手架,它能帮助我们解决哪些痛点问题。
•前端项目配置越来越繁琐、耗时,重复无意义的工作•项目结构不统一、不规范 •前端项目类型繁多,不同项目不同配置,管理成本高•脚手架也可以是一套命令集,不只用来创建项目
那么为什么不用一些开源框架自身的 CLI 工具,需要自己开发呢,这里仁者见仁智者见智,我个人建议就是对于中型团队以上需要自己维护一套脚手架,因为可控性高,能满足团队特定需求的研发。
如何按照开源要求开发一个前端脚手架?
下面是我们常见的前端开源目录结构
脚手架的设计
思路
•解耦:脚手架与模板分离•脚手架负责构建流程,通过命令行与用户交互,获取项目信息 •模板负责统一项目结构、工作流程、依赖项管理•脚手架需要检测模板的版本是否有更新,支持模板的删除与新建 •……
流程图
代码讲解
目录结构
配置 Git hook
首先进行开发前的准备工作,来保证你代码的质量。
Husky + Lint-staged
通过 Git hook 完成 commitlint、ESLint、prettiter 等,具体配置我后面会给源码,有兴趣的可以自己搜索下。
// package.json"husky": { "hooks": { "pre-commit": "lint-staged", "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" } }, "lint-staged": { "**/*.js": [ "eslint --fix", "prettier --write", "git add" ] }
package.json 下的 bin 字段
bin:配置内部命令对应的可执行文件位置,配置命令后,npm 会寻找到对应的可执行文件,然后在 node_modules/.bin 目录下建立对应的符号链接。
由于 node_modules/.bin 会在运行时候加入到系统的环境变量,因此我们可以通过 npm 调用命令来执行脚本。
所有 node_modules/.bin 目录下的命令都可以通过 npm run [命令] 执行。
所以我们需要在 package.json 配置入口
"bin": { "easy": "bin/easy.js" }
npm link 本地调试
这里介绍下开发脚手架的调试方法。npm link 官网使用介绍。使用方法:
// cd 到你项目的bin目录(脚本)下$ npm link
去掉 link 也非常方便:
npm unlink linkname
bin 目录下的入口文件
#!/usr/bin/env nodeconst program = require('commander'); // 命令行工具const chalk = require('chalk'); // 命令行输出美化const didYouMean = require('didyoumean'); // 简易的智能匹配引擎const semver = require('semver'); // npm的语义版本包const enhanceErrorMessages = require('../lib/util/enhanceErrorMessages.js');const requiredNodeVersion = require('../package.json').engines.node;didYouMean.threshold = 0.6;function checkNodeVersion(wanted, cliName) { // 检测node版本是否符合要求范围 if (!semver.satisfies(process.version, wanted)) { console.log( chalk.red( 'You are using Node ' + process.version + ', but this version of ' + cliName + ' requires Node ' + wanted + '.\nPlease upgrade your Node version.' ) ); // 退出进程 process.exit(1); }}// 检测node版本checkNodeVersion(requiredNodeVersion, '@easy/cli');program .version(require('../package').version, '-v, --version') // 版本 .usage('<command> [options]'); // 使用信息// 初始化项目模板program .command('create <template-name> <project-name>') .description('create a new project from a template') .action((templateName, projectName, cmd) => { // 输入参数校验 validateArgsLen(process.argv.length, 5); require('../lib/easy-create')(lowercase(templateName), projectName); });// 添加一个项目模板program .command('add <template-name> <git-repo-address>') .description('add a project template') .action((templateName, gitRepoAddress, cmd) => { validateArgsLen(process.argv.length, 5); require('../lib/add-template')(lowercase(templateName), gitRepoAddress); });// 列出支持的项目模板program .command('list') .description('list all available project template') .action(cmd => { validateArgsLen(process.argv.length, 3); require('../lib/list-template')(); });// 删除一个项目模板program .command('delete <template-name>') .description('delete a project template') .action((templateName, cmd) => { validateArgsLen(process.argv.length, 4); require('../lib/delete-template')(templateName); });// 处理非法命令program.arguments('<command>').action(cmd => { // 不退出输出帮助信息 program.outputHelp(); console.log(` ` + chalk.red(`Unknown command ${chalk.yellow(cmd)}.`)); console.log(); suggestCommands(cmd);});// 重写commander某些事件enhanceErrorMessages('missingArgument', argsName => { return `Missing required argument ${chalk.yellow(`<${argsName}>`)}`;});program.parse(process.argv); // 把命令行参数传给commander解析// 输入easy显示帮助信息if (!process.argv.slice(2).length) { program.outputHelp();}// easy支持的命令function suggestCommands(cmd) { const avaliableCommands = program.commands.map(cmd => { return cmd._name; }); // 简易智能匹配用户命令 const suggestion = didYouMean(cmd, avaliableCommands); if (suggestion) { console.log(` ` + chalk.red(`Did you mean ${chalk.yellow(suggestion)}?`)); }}function lowercase(str) { return str.toLocaleLowerCase();}function validateArgsLen(argvLen, maxArgvLens) { if (argvLen > maxArgvLens) { console.log( chalk.yellow( '\n Info: You provided more than argument. the rest are ignored.' ) ); }}
其他代码就不贴了我会给出源码链接,下面分享一下几个有意思的点。建议大家有兴趣的跟着敲一遍,有很多小细节需要注意。
发布脚本
// script/release.jsconst { execSync } = require('child_process');const semver = require('semver');const inquirer = require('inquirer');const currentVerison = require('../package.json').version;const release = async () => { console.log(`Current easy cli version is ${currentVerison}`); const releaseActions = ['patch', 'minor', 'major']; const versions = {}; // 生成预发布版本标示 releaseActions.map(r => (versions[r] = semver.inc(currentVerison, r))); const releaseChoices = releaseActions.map(r => ({ name: `${r} (${versions[r]})`, value: r })); // 选择发布方式 const { release } = await inquirer.prompt([ { name: 'release', message: 'Select a release type', type: 'list', choices: [...releaseChoices] } ]); // 优先自定义版本 const version = versions[release]; // 二次确认发布 const { yes } = await inquirer.prompt([ { name: 'yes', message: `Confirm releasing ${version}`, type: 'confirm' } ]); if (yes) { execSync(`standard-version -r ${release}`, { stdio: 'inherit' }); }};release().catch(err => { console.error(err); process.exit(1);});
npm version 与 tag
官网关于 npm version 的介绍:
https://docs.npmjs.com/cli/version.html
如果不熟悉 Node 语义化版本可以阅读:
https://semver.org/lang/zh-CN/
npm version [<newversion> | major | minor | patch | premajor | preminor | prepatch | prerelease [--preid=<prerelease-id>] | from-git]'npm [-v | --version]' to print npm version'npm view <pkg> version' to view a package's published version'npm ls' to inspect current package/dependency versions
其实我们自己使用 npn publish,最终执行的还是 npm version 下命令。
官网关于 npm-dist-tag 的介绍:
npm-dist-tag[1]
npm install <name>@<tag>npm install --tag <tag>
npm 也有 tag 的概念,一般情况下我们不会指定 tag,这个时候默认使用的就是 latest 这个 tag,所有的发布与安装都是最新的正式版本,如果指定 tag 之后,我们可以在这个 tag 上发布一个新的版本,用户安装时候也可以指定这个 tag 来进行安装,你可以简单理解 tag 类型 git 中的 branch。
常用的一些关于 tag 的命令:
# 查看当前的tag和对应的version。npm dist-tag ls# 查看my-package发布过的所有版本号。npm view my-package versions# 给my-package设置tag,对应到版本version。npm dist-tag add my-package@version tag
如果一不小心把测试版发布成了正式版?发布之前我们是这样的:
latest: 1.0.0next: 1.0.0-alpha.0
错误的把 1.0.0-alpha.1 直接 npm publish:
latest: 1.0.0-alpha.1next: 1.0.0-alpha.0
解决方法:
# 把原来的1.0.0设置成最新的正式版$ npm dist-tag add my-package@1.0.0 latest# 把1.0.0-alpha.1更新到最新的测试版$ npm dist-tag add my-package@1.0.0-alpha.1 next
npm publish 一个包
1.创建一个 npm 账户2.cd 到你需要发布的 repo 仓库下, 记得切换到 npm 源(或者公司内网自建源)3.npm login,需要输入用户名、密码、邮箱 4.npm publish
集成 CI(Travis CI)自动发布
每次手动发布太 low 了,要是可以自动发布就好了。
Travis CI 提供的是持续集成服务(Continuous Integration,简称 CI)。它绑定 GitHub/GitLab 等上面的项目,只要有新的代码,就会自动抓取。然后,提供一个运行环境,执行测试,完成构建,还能部署到服务器。
持续集成指的是只要代码有变更,就自动运行构建和测试,反馈运行结果。确保符合预期以后,再将新代码“集成”到主干。
简单理解就是:它的作用是自动帮你做好从代码测试到发布的一系列流程,配合版本控制使用的话可以设置成每一次 push 都自动进行一次集成,保证代码的正确性。
注意现在 GitHub 也出了集成工具,感兴趣的可以去体验下。
如果你的项目是在 GitHub 并且是开源的,推荐使用这个 org[2]。
使用 GitHub 进行登录 Travis CI,完成一些授权工作,Travis CI 才能监听到你的 GitHub 项目代码的变化。
Travis CI 要求你项目的根目录必须有一个配置文件 .travis.yml 文件,这是一个配置文件,指定 travis 的行为,该文件还必须保存在 GitHub 的仓库。一旦有新的 push,travis 就会找到这个文件进行执行。
关于 travis 更多使用推荐阅读官网[3],这里主要讲下利用 travis 自动发布包到 npm[4], Continuous Integration environments[5]。
下面是一个 .travis.yml 配置文件:
language: node_jsnode_js: - '8'cache: directories: - node_modulesinstall: - npm installscript: - npm run lintdeploy: provider: npm email: "$NPM_EMAIL" api_key: "$AUTH_TOKEN" skip_cleanup: true on: branch: master# after_success:
然后在你的 travis 上选择需要开启 CI 的项目。
配置对应环境变量到该仓库下如:
环境变量名格式必须为“大写字母_大写字母”格式。
token 生成也非常简单,官网[6]介绍,可以直接在你的 npm 账户下的 tokens 页面手动生成或者通过 npm 命令行生成。
# 切换到npm源下, 登陆npmnpm login# 生成token, npm可以指定生成token的权限(只读或者可读可写)npm token create
然后配置一些脚本来执行 npm version,这样当你包版本有更新后 push 到 GitHub repo,就会触发 travis 自动发包到 npm。
DEMO
源码链接[7]
感兴趣的童鞋希望自己跟着写一遍,代码量适中,有问题的可以加我微信交流,微信号:xyzxiaozhongge,备注即可。对您有帮助的可以推荐给身边的童鞋哈,写的不好的地方欢迎斧正。
如果有点用处不妨关注一下。
References
[1] npm-dist-tag: https://docs.npmjs.com/cli/dist-tag[2] org: https://docs.travis-ci.com/[3] 官网: https://docs.travis-ci.com/ [4] travis
自动发布包到 npm: https://github.com/release-it/release-it/blob/master/docs/npm.md [5] Continuous Integration environments: https://github.com/release-it/release-it/blob/master/docs/ci.md#npm[6] 官网:
https://docs.npmjs.com/cli/token[7] 源码链接: https://github.com/NuoHui/easy-cli