脚手架的做用:为减小重复性工做而作的重复性工做javascript
即为了开发中的:编译 es6,js 模块化,压缩代码,热更新等功能,咱们使用webpack
等打包工具,可是又带来了新的问题:初始化工程的麻烦,复杂的webpack
配置,以及各类配置文件,因此就有了一键生成项目,0 配置开发的脚手架html
本文项目代码地址前端
本系列分 3 篇,详细介绍如何实现一个脚手架:vue
首先说一下我的的开发习惯java
在写功能前我会先把调用方式写出了,而后一步一步的从使用者的角度写,现将基础功能写好后,慢慢完善node
例如一键初始化项目功能react
我指望的就是 在命令行执行输入 my-cli create text-project
,回车后直接建立项目并生成模板,还会把依赖都下载好webpack
咱们下面就从命令行开始入手git
建立项目 my-cli
,执行 npm init -y
快速初始化es6
my-cli
:
在 package.json
中加入:
{
"bin": {
"my-cli": "bin.js"
}
}
复制代码
bin.js
:
#!/usr/bin/env node
console.log(process.argv);
复制代码
#!/usr/bin/env node
,这一行是必须加的,就是让系统动态的去PATH
目录中查找node
来执行你的脚本文件。
命令行执行 npm link
,建立软连接至全局,这样咱们就能够全局使用my-cli
命令了,在开发 npm
包的前期都会使用link
方式在其余项目中测试来开发,后期再发布到npm
上
命令行执行 my-cli 1 2 3
输出:[ '/usr/local/bin/node', '/usr/local/bin/my-cli', '1', '2', '3' ]
这样咱们就能够获取到用户的输入参数
例如my-cli create test-project
咱们就能够经过数组第 [2] 位判断命令类型create
,经过第 [3] 位拿到项目名称test-project
node
的命令行解析最经常使用的就是commander
库,来简化复杂cli
参数操做
(咱们如今的参数简单能够不使用commander
,直接用process.argv[3]
获取名称,可是为了以后会复杂的命令行,这里也先使用commander
)
#!/usr/bin/env node
const program = require("commander");
const version = require("./package.json").version;
program.version(version, "-v, --version");
program
.command("create <app-name>")
.description("使用 my-cli 建立一个新的项目")
.option("-d --dir <dir>", "建立目录")
.action((name, command) => {
const create = require("./create/index");
create(name, command);
});
program.parse(process.argv);
复制代码
commander
解析完成后会触发action
回调方法
命令行执行:my-cli -v
输出:1.0.0
命令行执行: my-cli create test-project
输出:test-project
拿到了用户传入的名称,就能够用这么名字建立项目 咱们的代码尽可能保持bin.js
整洁,不将接下来的代码写在bin.js
里,建立create
文件夹,建立index.js
文件
create/index.js
中:
const path = require("path");
const mkdirp = require("mkdirp");
module.exports = function(name) {
mkdirp(path.join(process.cwd(), name), function(err) {
if (err) console.error("建立失败");
else console.log("建立成功");
});
};
复制代码
process.cwd()
获取工做区目录,和用户传入项目名称拼接起来
(建立文件夹咱们使用mkdirp
包,能够避免咱们一级一级的建立目录)
修改bin.js
的action
方法:
// bin.js
.action(name => {
const create = require("./create")
create(name)
});
复制代码
命令行执行: my-cli create test-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.js
复制代码
generator/index-html.js
模板示例:
module.exports = function(name) {
const template = ` { "name": "${name}", "version": "1.0.0", "description": "", "main": "index.js", "scripts": {}, "devDependencies": { }, "author": "", "license": "ISC", "dependencies": { "vue": "^2.6.10" } } `;
return { template, dir: "", name: "package.json" };
};
复制代码
dir
就是目录,例如main.js
的dir
就是src
create/index.js
在mkdirp
中新增:
const path = require("path");
const mkdirp = require("mkdirp");
const fs = require("fs");
module.exports = function(name) {
const projectDir = path.join(process.cwd(), name);
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
来获取目录下全部文件来遍历执行
咱们日常下载npm
包都是使用命令行 npm install / yarn install
这时就须要用到 node
的 child_process.spawn
api 来调用系统命令
由于考虑到跨平台兼容处理,因此使用 cross-spawn 库,来帮咱们兼容的操做命令
咱们建立utils
文件夹,建立install.js
utils/install.js
:
const spawn = require("cross-spawn");
module.exports = function install(options) {
const cwd = options.cwd || process.cwd();
return new Promise((resolve, reject) => {
const command = options.isYarn ? "yarn" : "npm";
const args = ["install", "--save", "--save-exact", "--loglevel", "error"];
const child = spawn(command, args, { cwd, stdio: ["pipe", process.stdout, process.stderr] });
child.once("close", code => {
if (code !== 0) {
reject({
command: `${command} ${args.join(" ")}`
});
return;
}
resolve();
});
child.once("error", reject);
});
};
复制代码
而后咱们就能够在建立完模板后调用install
方法下载依赖
install({ cwd: projectDir });
复制代码
要知道工做区为咱们项目的目录
至此,解析 cli,建立目录,建立模板,下载依赖一套流程已经完成
基本功能都跑通以后下面就是要填充剩余代码和优化
当代码写的多了以后,咱们看上面create
方法内的回调嵌套回调会很是难受
node 7
已经支持async,await
,因此咱们将上面代码改为Promise
在utils
目录下建立,promisify.js
:
module.exports = function promisify(fn) {
return function(...args) {
return new Promise(function(resolve, reject) {
fn(...args, function(err, ...res) {
if (err) return reject(err);
if (res.length === 1) return resolve(res[0]);
resolve(res);
});
});
};
};
复制代码
这个方法帮咱们把回调形式的Function
改为Promise
在utils
目录下建立,fs.js
:
const fs = require(fs);
const promisify = require("./promisify");
const mkdirp = require("mkdirp");
exports.writeFile = promisify(fs.writeFile);
exports.readdir = promisify(fs.readdir);
exports.mkdirp = promisify(mkdirp);
复制代码
将fs
和mkdirp
方法改形成promise
改造后的create.js
:
const path = require("path");
const fs = require("../utils/fs-promise");
const install = require("../utils/install");
module.exports = async function(name) {
const projectDir = path.join(process.cwd(), name);
await fs.mkdirp(projectDir);
console.log(`建立${name}文件夹成功`);
const { template, dir, name: fileName } = require("../generator/package")(name);
await fs.writeFile(path.join(projectDir, dir, fileName), template.trim());
console.log(`建立${fileName}文件成功`);
install({ cwd: projectDir });
};
复制代码
关于进一步优化:
chalk
和ora
优化log
,给用户更好的反馈inquirer
问询用户获得更多的选择:模板vue-router
,vuex
等更多初始化模板功能,eslint
更多的功能:
其实要学会善用第三方库,你会发现咱们上面的每一个模块都有第三方库的身影,咱们只是将这些功能组装起来,再结合咱们的想法进一步封装
虽然有vue-cli
,create-react-app
这些已有的脚手架,可是咱们仍是可能在某些状况下须要本身实现脚手架部分功能,根据公司的业务来封装,减小重复性工做,或者了解一下内部原理
【青团社】招聘前端方面: 高级/资深/技术专家,欢迎投递 lishixuan@qtshe.com