搭建一个通用的脚手架
在16年年底的时候,同事聊起脚手架。由于公司业务的多样性
,前端的灵活性
,让我们不得不思考更通用的脚手架。而不是伴随着前端技术的发展,不断的把时间花在配置
上。于是chef-cli诞生了。 18年年初,把过往一年的东西整理和总结下,重新增强了原有的脚手架project-next-cli, 不单单满足我们团队的需求,也可以满足其他人的需求。
<!--more-->
project-next-cli
面向的目标用户:
- 公司业务杂,但有一定的积累
- 爱折腾的同学和团队
- 借助github大量开发模板开发
发展
前端这几年(13年-15年)处于高速发展,主要表现:
备注:以下发展过程出现,请不要纠结出现顺序 [捂脸]
- 库/框架:jQuery, backbone, angular,react,vue
- 模块化:commonjs, AMD(CMD), UMD, es module
- 任务管理器:npm scripts, grunt, gulp
- 模块打包工具: r.js, webpack, rollup, browserify
- css预处理器:Sass, Less, Stylus, Postcss
- 静态检查器:flow/typescript
- 测试工具:mocha,jasmine,jest,ava
- 代码检测工具:eslint,jslint
开发
当我们真实开发中,会遇到各种各样的业务需求(场景),根据需求和场景选用不同的技术栈,由于技术的进步和不同浏览器运行时的限制,不得不配置对应的环境等,导致我们从而满足业务需求。
画了一张图来表示,业务,配置(环境),技术之间的关系
前端配置工程师
于是明见流传了一个新的职业,前端配置工程师 O(∩_∩)O~
社区现状
专一的脚手架
社区中存在着大量的专一型框架,主要针对一个目标任务做定制。比如下列脚手架
vue-cli
提供利用vue开发webpack
, 以及 远程克隆生成文件等 pwa
等模板,本文脚手架参考了vue-cli
的实现。
dva-cli
针对dva开发使用的脚手架
think-cli
针对 thinkjs项目创建项目
通用脚手架
yeoman
是一款强壮的且有一系列工具的通用型脚手架,但yeoman发布指定package名称,和用其开发工具。具体可点击这里查看yeoman添加生成器规则
开发初衷和目标
由于公司形态决定了,业务类型多样,前端技术发展迭代,为了跟进社区发展,更好的完成下列目标而诞生。
- 完成业务:专心,稳定,快速
- 团队规范:代码规范,测试流程,发布流程
- 沉淀:专人做专事,持续稳定的迭代更新,跟进时代
- 效益:少加班,少造轮子,完成kpi,做更有意义的事儿
实现准备
依托于Github,根据Github API
来实现,如下:
- 获取项目
curl -i https://api.github.com/orgs/project-scaffold/repos
- 获取版本
curl -i https://api.github.com/repos/project-scaffold/cli/tags
实现逻辑
根据Github API
获取到项目列表和版本号之后,根据输入的名称,选择对应的版本下载到本地私有仓库
,生成到执行目录下。核心流程图如下:。
总体设计
- 规范
- 使用Node进行脚手架开发,版本选择
>=6.0.0
- 选用async/await开发,解决异步回调问题
- 使用babel编译
- 使用ESLint规范代码
- 功能
遵守单一职责原则
,每个文件为一个单独模块,解决独立的问题。可以自由组合,从而实现复用。以下是最终的目录结构:
├── LICENSE ├── README.md ├── bin │ └── project ├── package.json ├── src │ ├── clear.js │ ├── config.js │ ├── helper │ │ ├── metalAsk.js │ │ ├── metalsimth.js │ │ └── render.js │ ├── index.js │ ├── init.js │ ├── install.js │ ├── list.js │ ├── project.js │ ├── search.js │ ├── uninstall.js │ ├── update.js │ └── utils │ ├── betterRequire.js │ ├── check.js │ ├── copy.js │ ├── defs.js │ ├── git.js │ ├── loading.js │ └── rc.js └── yarn.lock
配置和主框架
使用babel-preset-env保证版本兼容
{ "presets": [ ["env", { "targets": { "node": "6.0.0" } }] ] }
使用eslint管理代码
{ "parserOptions": { "ecmaVersion": 7, "sourceType": "module", "ecmaFeatures": { "jsx": true } }, "extends": "airbnb-base/legacy", "rules": { "consistent-return": 1, "prefer-destructuring": 0, "no-mixed-spaces-and-tabs": 0, "no-console": 0, "no-tabs": 0, "one-var":0, "no-unused-vars": 2, "no-multi-spaces": 2, "key-spacing": [ 2, { "beforeColon": false, "afterColon": true, "align": { "on": "colon" } } ], "no-return-await": 0 }, "env": { "node": true, "es6": true } }
使用husky检测提交
使用husky, 来定义git-hooks, 规范git代码提交流程,这里只做 commit
校验
在package.json
配置如下:
"husky": { "hooks": { "pre-commit": "npm run lint" } }
入口
统一配置和入口,分发到不同单一文件,执行输出。核心代码
function registerAction(command, type, typeMap) { command .command(type) .description(typeMap[type].desc) .alias(typeMap[type].alias) .action(async () => { try { if (type === 'help') { help(); } else if (type === 'config') { await project('config', ...process.argv.slice(3)); } else { await project(type); } } catch (e) { console.log(e); help(); } }); return command; }
本地配置读和写
配置用来获取脚手架的基本设置, 如registry, type等基本信息。
- 使用
project config set registry koajs # 设置本地仓库下载源 project config get registry # 获取本地仓库设置的属性 project config delete registry # 删除本地设置的属性
- 逻辑
判定本地设置文件存在 ===> 读/写
本地配置文件, 格式是 .ini
若中间每一步 数据为空/文件不存在 则给予提示
- 核心代码
switch (action) { case 'get': console.log(await rc(k)); console.log(''); return true; case 'set': await rc(k, v); return true; case 'remove': await rc(k, v, true); return true; default: console.log(await rc());
下面每个命令的实现逻辑。
下载
- 使用
project i
- 逻辑
Github API ===> 获取项目列表 ===> 选择一个项目 ===> 获取项目版本号 ===> 选择一个版本号 ===> 下载到本地仓库
获取项目列表
获取tag列表
若中间每一步 数据为空/文件不存在 则给予提示
请求代码
function fetch(api) { return new Promise((resolve, reject) => { request({ url : api, method : 'GET', headers: { 'User-Agent': `${ua}` } }, (err, res, body) => { if (err) { reject(err); return; } const data = JSON.parse(body); if (data.message === 'Not Found') { reject(new Error(`${api} is not found`)); } else { resolve(data); } }); }); }
下载代码
export const download = async (repo) => { const { url, scaffold } = await getGitInfo(repo); return new Promise((resolve, reject) => { downloadGit(url, `${dirs.download}/${scaffold}`, (err) => { if (err) { reject(err); return; } resolve(); }); }); };
- 核心代码
// 获取github项目列表 const repos = await repoList(); choices = repos.map(({ name }) => name); answers = await inquirer.prompt([ { type : 'list', name : 'repo', message: 'which repo do you want to install?', choices } ]); // 选择的项目 const repo = answers.repo; // 项目的版本号劣币爱哦 const tags = await tagList(repo); if (tags.length === 0) { version = ''; } else { choices = tags.map(({ name }) => name); answers = await inquirer.prompt([ { type : 'list', name : 'version', message: 'which version do you want to install?', choices } ]); version = answers.version; } // 下载 await download([repo, version].join('@'));
生成项目
- 使用
project init
- 逻辑
获取本地仓库列表 ===> 选择一个本地项目 ===> 输入基本信息 ===> 编译生成到临时文件 ===> 复制并重名到目标目录
若中间每一步 数据为空/文件不存在/生成目录已重复 则给予提示
- 核心代码
// 获取本地仓库项目 const list = await readdir(dirs.download); // 基本信息 const answers = await inquirer.prompt([ { type : 'list', name : 'scaffold', message: 'which scaffold do you want to init?', choices: list }, { type : 'input', name : 'dir', message: 'project name', // 必要的验证 async validate(input) { const done = this.async(); if (input.length === 0) { done('You must input project name'); return; } const dir = resolve(process.cwd(), input); if (await exists(dir)) { done('The project name is already existed. Please change another name'); } done(null, true); } } ]); const metalsmith = await rc('metalsmith'); if (metalsmith) { const tmp = `${dirs.tmp}/${answers.scaffold}`; // 复制一份到临时目录,在临时目录编译生成 await copy(`${dirs.download}/${answers.scaffold}`, tmp); await metal(answers.scaffold); await copy(`${tmp}/${dirs.metalsmith}`, answers.dir); // 删除临时目录 await rmfr(tmp); } else { await copy(`${dirs.download}/${answers.scaffold}`, answers.dir); }
其中模板引擎编译实现核心代码如下:
// metalsmith逻辑 function metal(answers, tmpBuildDir) { return new Promise((resolve, reject) => { metalsmith .metadata(answers) .source('./') .destination(tmpBuildDir) .clean(false) .use(render()) .build((err) => { if (err) { reject(err); return; } resolve(true); }); }); } // metalsmith render中间件实现 function render() { return function _render(files, metalsmith, next) { const meta = metalsmith.metadata(); /* eslint-disable */ Object.keys(files).forEach(function(file){ const str = files[file].contents.toString(); consolidate.swig.render(str, meta, (err, res) => { if (err) { return next(err); } files[file].contents = new Buffer(res); next(); }); }) } }
升级/降级版本
- 使用
project update
- 逻辑
获取本地仓库列表 ===> 选择一个本地项目 ===> 获取版本信息列表 ===> 选择一个版本 ===> 覆盖原有的版本文件
若中间每一步 数据为空/文件不存在 则给予提示
- 核心代码
// 获取本地仓库列表 const list = await readdir(dirs.download); // 选择一个要升级的项目 answers = await inquirer.prompt([ { type : 'list', name : 'scaffold', message: 'which scaffold do you want to update?', choices: list, async validate(input) { const done = this.async(); if (input.length === 0) { done('You must choice one scaffold to update the version. If not update, Ctrl+C'); return; } done(null, true); } } ]); const repo = answers.scaffold; // 获取该项目的版本信息 const tags = await tagList(repo); if (tags.length === 0) { version = ''; } else { choices = tags.map(({ name }) => name); answers = await inquirer.prompt([ { type : 'list', name : 'version', message: 'which version do you want to install?', choices } ]); version = answers.version; } // 下载覆盖文件 await download([repo, version].join('@'))
搜索
搜索远程的github仓库有哪些项目列表
- 使用
project search
- 逻辑
获取github项目列表 ===> 输入搜索的内容 ===> 返回匹配的列表
若中间每一步 数据为空 则给予提示
- 核心代码
const answers = await inquirer.prompt([ { type : 'input', name : 'search', message: 'search repo' } ]); if (answers.search) { let list = await searchList(); list = list .filter(item => item.name.indexOf(answers.search) > -1) .map(({ name }) => name); console.log(''); if (list.length === 0) { console.log(`${answers.search} is not found`); } console.log(list.join('\n')); console.log(''); }
总结
以上是这款通用脚手架产生的背景,针对用户以及具体实现,该脚手架目前还有一些可以优化的地方:
- 不同源,存储不同的文件
- 支持离线功能
硬广:如果您觉得project-next-cli好用,欢迎star,也欢迎fork一块维护。