介绍一下脚手架的作用:为了减少重复性工作而做的重复性工作是针对开发中的功能:编译es6、js模块化、压缩代码、热更新等功能。我们使用webpack等打包工具,但是带来了新的问题:初始化项目的麻烦,webpack配置复杂,配置文件繁多,所以有了一键生成项目,0配置开发脚手架本文项目代码地址本系列分为3篇,详细介绍如何实现一个脚手架:一键初始化项目0配置开发环境和打包一键上传服务器首先说一下我个人的开发习惯。在写函数之前,我会先写调用方法,然后站在用户的角度一步步写。现在基本功能写完了,慢慢完善一键初始化项目等功能。我期望的是在命令行执行输入my-clicreatetext-project,回车直接创建项目并生成模板,下载所有的依赖。下面我们从命令行开始创建项目my-cli,执行npminit-y快速初始化binmy-cli:添加到package.json:{"bin":{"my-cli":"bin.js"}}bin.js:#!/usr/bin/envnodeconsole.log(process.argv);#!/usr/bin/envnode,这一行是必须加的,就是让系统动态搜索PATH目录下的node到执行你的脚本文件。在命令行执行npmlink创建一个全局的软链接,这样我们就可以全局使用my-cli命令了。在开发npm包的前期,我们会在其他项目中使用link的方式进行测试开发,后期再发布到npm命令行。执行my-cli123output:['/usr/local/bin/node','/usr/local/bin/my-cli','1','2','3']这样我们就可以获取用户输入参数如my-clicreatetest-project后,我们可以通过数组的[2]位判断命令类型create,通过[3]位获取项目名称test-projectcommandernode。命令行解析最常用的是Commander库,简化复杂cli参数的操作(我们现在简单的参数可以不用commander直接用process.argv[3]取名,但是对于复杂的命令行在以后,我们也先在这里使用commander)#!/usr/bin/envnodeconstprogram=require("commander");constversion=require("./package.json").version;program.version(version,"-v,--version");program.command("create").description("Createanewprojectusingmy-cli").option("-d--dir
","Createdirectory").action((name,command)=>{constcreate=require("./create/index");create(name,command);});program.parse(process.argv);commander解析后会触发action回调方法command完成行执行:my-cli-v输出:1.0.0命令行执行:my-clicreatetest-project输出:test-project创建一个项目,得到用户传入的名字,就可以用这个名字创建一个项目我们的代码尽量保持bin.js干净,不要在bin.js中写下面的代码,创建create文件夹,创建index.js文件create/index.js:constpath=require("path");constmkdirp=require("mkdirp");module.exports=function(name){mkdirp(path.join(process.cwd(),name),function(err){if(err)console.error("失败create");elseconsole.log("createdsuccessfully");});};process.cwd()获取工作空间目录,并与用户传入的项目名称拼接(我们使用mkdirp包来创建文件夹,可以避免我们逐级创建目录)修改bin.js的action方法://bin.js.action(name=>{constcreate=require("./create")创建(名称)});命令行执行:my-clicreatetest-project输出:创建成功,在命令行所在目录下创建了一个test-project文件夹。列出我们的模板包含哪些文件一个基础版本的vue项目模板:|-src|-main.js|-App.vue|-components|-HelloWorld.vue|-index.html|-package.json这些文件我赢了一一介绍。我们需要的是生成这些文件并写入到目录中。编写模板的方法有很多种。以下是我的写法:模板目录:|-generator|-index-html.js|-package-json.js|-main.js|-App-vue.js|-HelloWorld-vue.jsgenerator/index-html.js模板示例:module.exports=function(name){consttemplate=`{"name":"${name}","version":"1.0.0","description":"","main":"index.js","scripts":{},"devDependencies":{},"author":"","license":"ISC","dependencies":{"vue":"^2.6.10"}}`;返回{模板,目录:“”,名称:“package.json”};};dir为目录,例如main.js的目录为srccreate/index.js在mkdirp中添加:constpath=require("path");constmkdirp=require("mkdirp");constfs=require("fs");module.exports=function(name){constprojectDir=path.join(process.cwd(),na我);mkdirp(projectDir,function(err){if(err)console.error("创建失败");else{console.log(`创建${name}文件夹成功`);const{template,dir,name:fileName}=require("../generator/package")(name);fs.writeFile(path.join(projectDir,dir,fileName),template.trim(),function(err){if(err)console.error(`未能创建${fileName}文件`);else{console.log(`成功创建${fileName}文件`);}});}});};这里只写模板的创建,我们可以使用readdir获取目录下的所有文件来遍历执行下载依赖。我们通常使用命令行npminstall/yarninstall来下载npm包。这时我们需要使用node的child_process.spawnapi来调用系统命令,因为考虑到跨平台兼容处理,所以使用cross-spawn库来帮助我们兼容操作命令我们创建utils文件夹并创建install.jsutils/install.js:constspawn=require("cross-spawn");module.exports=functioninstall(options){constcwd=options.cwd||}进程.cwd();returnnewPromise((resolve,reject)=>{constcommand=options.isYarn?"yarn":"npm";constargs=["install","--save","--save-exact","--loglevel","错误"];nstchild=spawn(command,args,{cwd,stdio:["pipe",process.stdout,process.stderr]});孩子。once("close",code=>{if(code!==0){reject({command:`${command}${args.join("")}`});return;}resolve();});孩子。一次(“错误”,拒绝);});};然后我们就可以在创建模板后调用install方法下载依赖了install({cwd:projectDir});要知道workspace就是我们项目的目录至此,解析cli,创建目录,创建模板,下载一组依赖。完成基本功能后,接下来就是对剩下的代码进行补全和优化。当代码写多了,我们看到上面create方法中的callback嵌套回调会很不爽。Node7已经支持async和await,所以我们将上面的代码改成Promise,在utils目录下创建,promisify.js:module.exports=functionpromisify(fn){returnfunction(...args){returnnewPromise(function(resolve,reject){fn(...args,function(err,...res){if(err)返回reject(err);if(res.length===1)returnresolve(res[0]);resolve(res);});});};};这个方法帮我们把Function的回调形式改成了Promise,在utils目录下创建,fs.js:constfs=require(fs);constpromisify=require("./promisify");常量mkdirp=require(“mkdirp”);exports.writeFile=promisify(fs.writeFile);exports.readdir=promisify(fs.readdir);exports.mkdirp=promisify(mkdirp);将fs和mkdirp方法转换为promise转换的create.js:constpath=require("path");constfs=require("../utils/fs-promise");constinstall=require("../utils/install");module.exports=asyncfunction(name){constprojectDir=path.join(process.cwd(),name);等待fs.mkdirp(projectDir);console.log(`成功创建${name}文件夹`);const{template,dir,name:fileName}=require("../generator/package")(name);等待fs.writeFile(path.join(projectDir,dir,fileName),template.trim());console.log(`创建${fileName}文件成功`);安装({cwd:projectDir});};结论关于进一步优化:更多的功能和健壮性,比如指定目录创建项目,目录不存在等。chalk和ora优化日志,给用户更好的反馈通过查询器查询用户,获得更多选择:templatevue-router、vuex等更多的初始化模板功能,eslint的更多功能:内置webpack配置一键发布服务器其实你要学会用好第三方库,你会发现每一个我们上面的模块有一个第三方库。我们只是把这些功能组装起来,再用我们的想法进一步封装起来。虽然已有vue-cli、create-react-app等脚手架,但我们还是有可能在某些情况下,需要自己实现脚手架的一些功能,根据公司业务进行封装,减少重复性工作,或了解内部部原则【团社】前端招聘:资深/资深/技术达人,欢迎投递lishixuan@qtshe.com