web前端领域技术日新月异,技术栈也不断丰富,在日常工作中涉及到的内容也不断增加,一个前端项目从开发到发布涉及的步骤也很多,很多重复工作内容,因此我们需要开发一些工作来减少这些工作量---工作流。工作流现在也存在很多解决方案,大都是采用GUI方式+自定义脚本方式,相比GUI的方式很多人更爱命令行的的方式,轻量化,可以方便自定义开发,更好适应现有业务的情况。
本文章目的,基于一个命令行模板工具,循序渐进的告诉读者,开发一个命令行工具,会用到哪些现有的轮子,如何让你的工具变得丰满起来。同时我也会简要介绍这些轮子是用来做什么的,以及在实际操作中具体的基本用法。
关于web前端工作流 我计划分为 采用三个篇文章来介绍其他两个主题:
我们在做这件事的时候 主要是基于以下三个痛点:
项目初始化
通常来说一个团队涉及到的项目都会很多,以我们 企鹅辅导 来说,前端项目非常之多, 并且我们团队也有自己的一些开发规范和要求,对于新开发一个项目,同样需要遵循规范和约束,以前我们的做法大都是复制现有项目,然后删减其中我们觉得不需要的内容,然后基于此继续开发。
那么问题来了,复制的项目通常有太多新项目用不上的内容,并且我们很难区分哪些是需要删除的、保留的。 这时候就出现了我们的模板工具,通过开发一个模板工具,通过交互是的命令行初始化项目。 但是如果仅仅做一个命令行工具,就为了初始化项目(低频操作),是很难用起来的。
构建工具
现在的前端项目几乎都有构建工具,涉及到构建,又需要做构建优化,这包括:构建本身速度优化,构建的静态内容优化等等,其次构建本身的升级也需要跟上时代步伐,通常的做法都是每个项目独立维护一份构建配置,这就造成优化无法快速应用到项目中,需要在每个项目重复这些工作,如何让这些能力通用,这也可以纳入工作流工具中。
项目发布
以我们 企鹅辅导 产品来说,发布一个需求大致需要经历如下过程:
显而易见,发布一个需求涉及到的步骤非常多,并且每个不同的步骤都可能是在不同系统中完成,对我们开发者也增加了很多不必要的工作。从实际情况来看,任何一个修改,就算是小到一个wording的修改,都需要经历这些步骤,这就造成任何一个发布没有数小时,基本没法完成。
基于这些原因,才有了团队工具流开发的必要性,将一个项目从初始化到发布上线的所有过程都集中到工具中,简化工作内容,保证发布流程。对于不同的团队情况不同,部分内容也不一定适用,具体是否有必要依赖具体业务情况。
接下来我将演示,如何看法自己的模板项目命令行工具
创建命令行项目,基本目录如下
imt/
├── bin/
│ └── cli.js
├── LICENSE
├── package.json
├── README.md
├── src/
│ ├── commands/
│ │ ├── create.js
│ ├── const.js
│ ├── index.js
│ ├── plugins/
│ │ └── index.js
│ ├── Service.js
│ ├── utils/
│ │ ├── index.js
│ │ ├── logger.js
│ │ ├── spinner.js
处理命令行参数,解析参数并执行不同操作, 下面这个脚本就是一个nodejs脚本,其中第一行是为了告诉bash 使用 node 执行脚本
./bin/cli.js
#!/usr/bin/env nodeconst program = require('commander'); // 一个命令行参数解析工具const pkg = require('../package.json'); // 这里是为了自动设置代理,有的仓库在外网,需要设置代理才能够下载program .version(pkg.version)
.option('-P, --proxy <proxy>', '设置代理')
.option('-x, --defaultProxy', '使用内网代理', false);// 定义一个action,匹配后会执行指定的函数program .command('create [template] [dir]')
.description('初始化项目')
.action((template, dir, options) => {
require('../src/commands/create')(template, dir, options);
});
program.parse(process.argv); // 解析命令行参数if (!process.argv.slice(2).length) { // 没有参数直接输出帮助信息
program.outputHelp();} else if (program.proxy) { // 将proxy设置到环境变量
process.env.proxy = program.proxy;}
配置可执行命令 让我们工具连接到全局并且可直接执行, 配置package.json
"bin": {
"imt": "./bin/cli.js"
},
执行 npm link
,将命令软连接到全局命令搜索目录下, 执行完毕后,直接在terminal中输入imt
然后回车键,会看到如下信息帮助信息:
Usage: imt [options] [command]
Options:
-V, --version output the version number
-P, --proxy <proxy> 设置代理
-x, --defaultProxy 使用内网代理
-h, --help output usage information
Commands:
create [template] [dir] 初始化项目
模板代码下载和初始化
现在你的的命令行工具已经安装成功,接下来就是根据命令行输入的参数,执行函数。我们看如何实现模板功能,模板通常我们不止一种,因此具体模板我们不会放在这个工具中,是通过imt create gitName/projectName
中的参数gitName/projectName
获取具体模板所在仓库位置。
共需要做三件事情:
当用户没有输入任何模板地址时,你需要提供给默认的选择,交互式选择项目模板
const inquirer = require('inquirer');// 内置模板const defaultTemplates = [
{
name: 'React应用',
value: 'hxfdarling/imt-react-template'
},
{
name: '微信小程序',
value: 'hxfdarling/imt-mp-template'
}];// ... 省略其他代码
async getTemplate() {
let { template } = this;
if (!template) {
({ template } = await inquirer.prompt([
{
name: 'template',
message: '选择内置模板',
type: 'list',
choices: defaultTemplates,
},
]));
}
if (/\.zip$/.test(template)) {
template = `direct:${template}`;
}
this.template = template;
}
// ... 省略其他代码
我们通过inquirer
模块 快速的生成交互式的选择界面,该工具支持多种不通过的交互式输入方式。这里使用到了他的list
列表选择能力,具体效果如下:
得到目标模板地址后,需要下载模板的仓库代码:
const download = require('download-git-repo');const chalk = require('chalk');const ora = require('ora');//... 省去部分代码
downloadTemplate() {
const { template } = this;
const spinner = ora('模板下载中').start();
return new Promise(resolve => {
download(template, this.templateDir, {}, error => {
if (error) {
console.log(chalk.red(`下载模板失败:${template},请确认网络是否正常`));
console.error(error);
process.exit(1);
} else {
spinner.stop();
console.log(chalk.green('模板下载成功'));
resolve();
}
});
});
}
这里下载仓库代码,我使用了download-git-repo
快速实现地址解析和下载,下载过程我们需要美化一下,通过ora
工具支持添加一个loading图标,表示正在处理。具体效果如下:
接下来就是初始化我们的模板项目并执行模板项目中的代码,以初始化项目,具体代码如下:
//... 省略其他代码
await this.confirmDir(); // 提示用户当前目录不为空(如果不为空)
await this.getTemplate();// 获取仓库地址
await this.downloadTemplate(); // 下载模板
console.log(chalk.yellow('初始化模板'));
shell.cd(templateDir); // 切换命令行执行目录到模板目录
// 由于内网,检测用户是否安装了tnpm,安装了就使用tnpm命令
let npmCmd = 'npm';
if (!shell.exec('tnpm -v', { silent: true }).stderr) {
npmCmd = 'tnpm';
}
// 执行模板项目的node_modules初始化
shell.exec(`${npmCmd} i`, { silent: true });
const main = require(`${templateDir}/package.json`).main || 'index.js';
// 运行模板项目的代码
spawnSync('node', [`.template/${main}`], {
cwd: this.projectDir,
// 由于是在child_process中执行,可能有交互命令,需要继承父进程的标准输入,否则子进程无法获取到键盘输入
stdio: 'inherit',
});
// 清理模板项目目录
fs.emptyDirSync(templateDir);
fs.removeSync(templateDir);//... 省略其他代码
上面这段代码,也用到了一个高度封装的轮子shelljs。
至此我们一个完整的命令行工具搞定
具体如何初始化模板代码,我们继续看模板项目中的实现:
通常来说项目的大多数技术栈都统一,但是具体到不同业务中,实际用到的框架、库可能也不尽相同。因此模板项目并非一个简单的复制仓库代码搞定,我们需要让模板在初始化时,可以选择功能。
这里以React应用模板为例,具体代码地址imt-react-template,这个模板代码支持初始化多页面应用和单页面引用,是否使用rem,是否初始化index.html内容等可选项。
项目目录
imt-react-template/
├── bin/
│ └── cli.js
├── index.js // 模板执行脚本
├── package.json
├── README.md
└── templates/
├── common/ 两种不同模板共享文件
├── multi/ 多页面模板
└── single/ 单页面模板
我们看一下具体模板如何初始化, 这里诺列具体代码了,说一下具体步骤:
单页面应用
,多页面应用
复制文件需要用到一个工具yeoman-generator
,该工具,提供模板变量和逻辑的能力,在复制文件时,通过传入对象,中的所有属性可以直接在模板代码的全局内访问,具体使用如下:
复制模板,第三个参数是模板执行的上下文对象
this.fs.copyTpl(
this.templatePath('./common/.imtrc.js'),
this.destinationPath('.imtrc.js'),
{
app:"single",
webapp:true
});
.imtrc.js文件模板代码如下:
module.exports = {
// 应用类型
mode:"<%= app %>",
<% if (webapp) { %>
// 支持webapp插件
webappConfig:{
},
<% } %>
};
这里通过<%= app %>
实现访问this.props
中的属性,直接打印到当前位置,通过<% if (webapp) { %> xxx <% } %>
实现条件判断,是否需要输出到最终文件,其他更多的语法参见 yeoman文档
最终效果我们看一张动图看完整效果
当然这只是一个初级的模板初始化工具,我们还可以再丰富它,例如:是否使用某些库、支持添加空页面模板等。
介绍一些用于开发命令行工具会用到的工具,下面这些工具都可直接在github中搜索,都是开源项目。
工具名称 | 介绍 |
---|---|
lint-staged | 可以用于实现提交前代码格式化,eslint等处理 |
husky | git钩子,例如提交前的一些脚本处理,提交消息检测等 |
commitlint | 用于git仓库提交的message规范检测,统一团队项目提交规范,这里有一个简单的库,能够快速接入这个能力到项目中commitlint-config-imt |
chalk | 命令行颜色工具,命令行工具输入日志时,带有颜色 |
commander | 命令行工具,必备工具,简化参数解析和帮助信息输出 |
inquirer | 交互式命令行工具,让你可以再命令行中实现可交互输入 |
semver | 版本工具,可以用于提示用户你的命令行支持版本的nodejs |
yeoman-generator | 快速项目初始化的模板工具,功能相当强大,具体能力参考官方文档 |
debug | 很好用的日志工具,可以给不同日志设置标题,能够快速调整日志打印策略 |
shelljs | shell执行工具,非常方便的在js代码中执行shell命令,甚至直接在js代码中使用shell命令! |
tapable | webpack插件基础库,提供了多种插件执行的模式,并行、异步、同步等等,如果需要让你的工具支持插件机制,使用这个库将这个事儿变得非常方便 |
ora | 简单来说,提供命令行中loading图标,和步骤标示能力,具体效果看git仓库 |
当然,这些工具只是冰山一角。
--------------------------------------------------------------------------
原文作者:腾讯高级工程师刘华
来源:腾讯内部KM论坛
你也想成为腾讯工程师?
也想年终奖人手一部 Iphone X?
那就快加入NEXT学院吧!
NEXT学院课程「Web前端工程师NEXT学位完整课程」火热招生中!
感兴趣的同学赶紧点击原文了解详情吧~