前端脚手架,听起来玄乎,实际呢?
开篇
然后我就装着知道的样子就去回答了一下,答案是这样的:
粗狂的讲,这样的回答,好像没什么毛病。但既然是本着学习的态度,那这次就好好的讲一讲前端脚手架存在的意义,到底是个什么鬼,以及写一个脚手架到底有多难?所以接下来,文章将围绕下面几部分来讨论:
- 前端脚手架存在的意义
- 脚手架的实质
- 写一个属于自己的脚手架有多难
前端脚手架存在的意义
随着前端工程化的概念越来越深入人心,脚手架的出现就是为减少重复性工作而引入的命令行工具,摆脱ctrl + c, ctrl + v,此话zenjiang? 现在新建一个前端项目,已经不是在html头部引入css,尾部引入js那么简单的事了,css都是采用Sass或则Less编写,在js中引入,然后动态构建注入到html中;除了学习基本的js,css语法和热门框架,还需要学习构建工具webpack,babel这些怎么配置,怎么起前端服务,怎么热更新;为了在编写过程中让编辑器帮我们查错以及更加规范,我们还需要引入ESlint;甚至,有些项目还需要引入单元测试(Jest)。对于一个更入门的人来说,这无疑会让人望而却步。而前端脚手架的出现,就让事情简单化,一键命令,新建一个工程,再执行两个npm命令,跑起一个项目。在入门时,无需关注配置什么的,只需要开心的写代码;另外,对于很多系统,他们的页面相似度非常高,所以就可以基于一套模板来搭建,虽然是不同的人开发,但用脚手架来搭建,相同的项目结构与代码书写规范,是很利于项目的后期维护的;以上就是为什么脚手架存在的意义, 让项目从"搭建-开发-部署"更加快速以及规范 (出自于某乎达人)。
脚手架的实质
现在流行的前端脚手架都是基于NodeJs编写,比如前面提到的Vue-CLI,比较火的create-react-app,还有Dva-CLI和我司自己curie,都是热门框架react和vue的项目脚手架,其功能都是生成一个通用的目录结构,并配上构建、编译、检查等工程环境。大致流程如下:
- 解析用户输入的命令;
- 生成一些配置化文件,如package.json, 或webpack.config.js等;
- 根据用户的输入生成对应的模板项目;(高级一点的ctrl + c, ctrl + v)
- 安装该模板所需要的环境。
先以create-react-app为例,网上有很多读create-react-app源码的文章,可以网上搜索一下,推荐一篇。它的代码不多,读起来也比较容易,最主要的是create-react-app在某种程度上来讲它只做了1,2步的事情(当然4也做了一些),第3步是由react-scripts完成的,当然其还有一个重要作用,就是作为这个项目的构建编译工具,所以你在package.json中还能看到下图靠右这样的命令(熟悉的nmp start, 有木有):
所以我们知道,create-react-app主要解析命令,执行文件的操作,react-scripts主要提供模板与模板所需要的项目工程化配置,如上图左侧所示,我们能看到其包含了webpack与jest测试的相关配置文件。
而vue-cli的实现与create-react-app稍微有点不一样。首先vue-cli新建工程是那种一步一步问答式命令来进行个性化定制的,而后者是一键搞定的;vue-cli的项目模板来源于github,支持多种模板(可通过vue list查看),通过git下载的,具体模板配置参照这里,而后者模板来自于react-scripts目录下的两个文件夹(上图中的template与template-typescript);vue-cli构建的项目其构建编译是直接依赖于browserify或webpack这样的构建工具,配置是完全暴露给使用者的,可以再次自定义,而后者是基于webpack进行了一层封装,然后将其暴露为一种新的构建命令,当然也可以运行npm run eject将配置暴露出来。dva是一个比较成熟的react解决方案,比较适合中后台系统,dva-cli与前两者具有较强的相似性,采用roadhog作为其构建编译工具,开发人员,无须关心构建配置,只需关心业务代码的实现(所以大厂的码农自己不注意的话容易发展成码畜)。其还有一个扩展功能,就是项目生成后,可以采用命令添加一个页面,这样确实也能减少一部分的ctrl + c, ctrl + v。
综上,前端脚手架的实质包含两项,命令式的构建项目(解析命令,拷贝项目到本地),提供项目的配置(构建,编译,代码规范检查)。
写一个属于自己的脚手架有多难
整理思路
从上两节的描述,大致整理一下即将要实现的功能:
- 命令的解析,这个可以借助commander实现;
- 文件的操作,复制,粘贴,增加,删除,文件内容的新增,替换;这个可以借助fs-extra实现;
- 模板文件,就以自己对前端工程化粗浅的认识,写一个最牛(cu)逼(lou)的模板项目;
- 申请一个npm账号,这个不算实现的功能,算附属工作;
以上,感觉是不是实现特简单。不是感觉,是确实很简单。流程如下:
实现代码
命令解析:
// 四种模板。对应我git仓库四个仓库地址 const tempIndex = { react: 'reactTemplate', // react 模板 vue: 'vueTemplate', // vue 模板 h5: 'h5Template', // h5模板 dva: 'dvaTemplate', // dva模板 }; let projectName; // 存储目录 let templateName; // 模板名称 let inputIndex; // 除了拷贝模板,还支持自定义模板路径下载,但感觉有点多此一举 const program = new commander.Command(packageJson.name) .version('v' + packageJson.version, '-v, --version') .arguments('<templateName>') .arguments('<projectName>') .option('-f, --force', 'force delete the exist director') .option('-d, --directly', 'copy the not specified template') .alias('cp') .description('create-doddle react myProject') .action(function (index,name) { inputIndex = index; // 允许目标项目名和要复制的模板类型名顺序颠倒 if (tempIndex[index] || tempIndex[name]) { if (tempIndex[index]) { templateName = tempIndex[index]; projectName = name; } else { templateName = tempIndex[name]; projectName = index; } } if (program.directly) { templateName = index; } }); program.parse(process.argv) // 没有输入任何参数,报语法错误,并打印help if (program.args.length === 0) { console.log(chalk.red('syntax error')); program.help() } if (templateName) { excute(templateName, projectName, program.force); } else { console.log(`the template ${inputIndex} you want download do not exist`); }
文件拷贝:
async function create(temp, project, force = false) { tempName = temp; projectName = project; forceDel = force; const file = currentPath + projectName; try { // 检测项目文件夹是否已存在, 若存在,抛出错误 const res = await fs.pathExists(file); if (res) { if (forceDel) { console.log(green('force remove the exist directory')); await fs.remove(file); downloadByGit(renameFile, tempName); } else { // 抛出错误,并提示可以使用-f参数来强制删除已存在的项目 console.log(chalk.red('Error, In this directory, the project name already exsits !')); console.log(chalk.green('you can use option -f to force delete the directory !')); } return; } // 若不存在,直接从git下载 downloadByGit(renameFile, tempName); } catch (err) { console.error(red(err)); } }
关于chalk,这是一款颜色标记插件,将要打印的文字用不同的颜色标记出来,像下面这样:
git文件下载:
function downloadByGit(callback, template) { console.log(green('start download')); console.log(`[email protected]:closertb/${template}.git`); const result = spawn( 'git', ['clone', `[email protected]:closertb/${template}.git`], { stdio: 'inherit' } ); const error = result.error; if (error) { console.log(red(error)); return; } // 定义回调; callback && callback(); }
主要就这三段代码,就实现了命令的解析,和从git源端拷贝模板到本地。
创建可执行命令
稍微对前端工程化了解的就知道,对于项目,想创建npm run start或npm run dev这样的可执行命令,只需要在package.json的scripts进行定义。而想要创建vue init webpack myapp或dva init myapp这样的命令又怎样做呢?广告之后,马上揭晓(自娱自乐中,请忽视O(∩~∩)O!)
第一步:在你项目的package.json中填上一个bin属性,表明它是可执行的, 并配置好可执行命令和入口文件
第二步: 在你的入口文件(我这里是根目录的index.js)首行加上一段代码: #!/usr/bin/env node,告诉操作系统执行这个脚本的时候,调用/usr/bin下的node解释器;
第三步:登录你的npm账号,运行npm publish发布你的npm包,参考过的链接
到此,一个简易的脚手架就写好了。
最后
人老了,总喜欢最后唠叨两句,其实不论vue-cli,还是create-react-app,或则dva,其核心功能(最牛逼的)不是命令的解析或者模板的拷贝,这部分只占了它脚手架很小的部分,看起来多,是因为它做了很多兼容,比如帮助、错误检测、系统网络环境检测、回滚这些操作,但这也不是核心。个人觉得核心还是是react-scripts或则roadhog这些构建编译脚本,但归根接地还是得对babel和webpack这些库的深入理解,送给观看这篇文章到这里的你,也勉励一下自己,继续加油。
项目源码地址: create-doddle
npm包地址:create-doddle
首发地址:Denzel Blog