资讯专栏INFORMATION COLUMN

手把手带你撸一个cli工具

xingqiba / 2710人阅读

摘要:最近,笔者就在为组里的框架去做一套基本的工具。通过这边文章,笔者希望大家都能简单的去实现一个属于自己的脚手架工具。我们在下新增文件,这个文件导出一个的类。结语到此,一个简单的就制作完成了,大家可以参考等优秀的适当的扩展自己的工具。

你有没有遇到过在没有vue-cli、create-react-app这样子的脚手架的时候一个文件一个文件的去拷贝老项目的配置文件。最近,笔者就在为组里的框架去做一套基本的cli工具。通过这边文章,笔者希望大家都能简单的去实现一个属于自己的脚手架工具。

</>复制代码

  1. 原文链接: https://juejin.im/user/57ac15...
做好准备工作

首先,我们需要去新建一个项目并初始化package.json

</>复制代码

  1. mkdir my-cli && cd my-cli
  2. npm init

然后我们需要在项目中新建bin文件夹,并将package.json中提供一个bin字段并指向我们的bin文件夹下,这样通过npm我们就可以实现指令的软链了。

</>复制代码

  1. "bin": {
  2. "mycli": "bin/mycli"
  3. },

在mycli中,我们要在头部增加这样一句注释,作用是"指定由哪个解释器来执行脚本"

</>复制代码

  1. #!/usr/bin/env node
  2. console.log("hello world");

接下来,全局安装我们这个包,这样我们就可以直接在本地使用mycli这个指令了。

</>复制代码

  1. sudo npm install -g
提供基本模版

既然我们要去做一个初始化项目的cli,那么项目模版就必不可少了,笔者在这里提前准备了一个demo的项目目录模版,这里就不展开赘述了。

编写逻辑

其实核心逻辑很简单,就是通过控制台获取到用户的一些自定义选项,然后根据选项去从本地或者远程仓库拿到我们提前准备好的模版,将配置写入模版并最后拷贝模版到本地就行了。

我们在src下新增creator.js文件,这个文件导出一个Creator的类。在这个类中现在仅需要三个简单的方法:init用于初始化、ask用于和命令行交互获取用户选择输入的数据、write用于调用模版的构建方法去执行拷贝文件写数据的任务。

</>复制代码

  1. class Creator {
  2. constructor() {
  3. // 存储命令行获取的数据,作为demo这里只要这两个;
  4. this.options = {
  5. name: "",
  6. description: "",
  7. };
  8. }
  9. // 初始化;
  10. init() {}
  11. // 和命令行交互;
  12. ask() {}
  13. // 拷贝&写数据;
  14. write() {}
  15. }
  16. module.exports = Creator;

先去完善init方法,这个方法里我们仅需要调用ask方法和命令行交互并做一些提示即可(可以通过chalk这个库去丰富我们的命令行交互色彩)

</>复制代码

  1. // ...
  2. init() {
  3. console.log(chalk.green("my cli 开始"));
  4. console.log();
  5. this.ask();
  6. }
  7. // ...

接下来是ask方法,在这个方法中,我们需要根据提示引导用户输入问题并获取用户的输入,这里用到inquirer这个库来和命令行交互。

</>复制代码

  1. // ...
  2. ask() {
  3. // 问题
  4. const prompt = [];
  5. prompt.push({
  6. type: "input",
  7. name: "name",
  8. message: "请输入项目名称",
  9. validate(input) {
  10. if (!input) {
  11. return "请输入项目名称!";
  12. }
  13. if (fs.existsSync(input)) {
  14. return "项目名已重复!"
  15. }
  16. return true;
  17. }
  18. });
  19. prompt.push({
  20. type: "input",
  21. name: "description",
  22. message: "请输入项目描述",
  23. });
  24. // 返回promise
  25. return inquirer.prompt(prompt);
  26. }
  27. // ...

修改刚才的init方法,将ask方法改为Promise调用。

</>复制代码

  1. init() {
  2. console.log(chalk.green("my cli 开始"));
  3. console.log();
  4. this.ask().then((answers) => {
  5. this.options = Object.assign({}, this.options, answers);
  6. console.log(this.options);
  7. });
  8. }

现在我们去命令行试一下,修改bin/mycli文件,然后去运行mycli命令。

</>复制代码

  1. #!/usr/bin/env node
  2. const Creator = require("../src/creator.js");
  3. const project = new Creator();
  4. project.init();

在和用户交互完毕并获取到数据后,我们要做的就是去调用write方法执行拷贝构建了。考虑到日后可能增加很多的模版目录,不妨我们将每一类的模版拷贝构建工作放到模版中的脚本去做,从而增大可扩展性,新增template/index.js文件

接下来首先根据项目目录结构创建文件夹(注意区分项目的执行目录和项目目录的关系)。

</>复制代码

  1. module.exports = function(creator, options, callback) {
  2. const { name, description } = options;
  3. // 获取当前命令的执行目录,注意和项目目录区分
  4. const cwd = process.cwd();
  5. // 项目目录
  6. const projectPath = path.join(cwd, name);
  7. const buildPath = path.join(projectPath, "build");
  8. const pagePath = path.join(projectPath, "page");
  9. const srcPath = path.join(projectPath, "src");
  10. // 新建项目目录
  11. // 同步创建目录,以免文件目录不对齐
  12. fs.mkdirSync(projectPath);
  13. fs.mkdirSync(buildPath);
  14. fs.mkdirSync(pagePath);
  15. fs.mkdirSync(srcPath);
  16. callback();
  17. }

然后回到creator.js文件,在Creator中的write调用这个方法。

</>复制代码

  1. // ...
  2. init() {
  3. console.log(chalk.green("my cli 开始"));
  4. console.log();
  5. this.ask().then((answers) => {
  6. this.options = Object.assign({}, this.options, answers);
  7. this.write();
  8. });
  9. }
  10. // ...
  11. write() {
  12. console.log(chalk.green("my cli 构建开始"));
  13. const tplBuilder = require("../template/index.js");
  14. tplBuilder(this, this.options, () => {
  15. console.log(chalk.green("my cli 构建完成"));
  16. console.log();
  17. console.log(chalk.grey(`开始项目: cd ${this.options.name } && npm install`));
  18. });
  19. }
  20. // ...

在开启文件拷贝写数据之前,我们需要用到两个库mem-fsmem-fs-editor,前者可以帮助我们在内存中创建一个临时的文件store,后者可以以ejs的形式去编辑我们的文件。

现在constructor中初始化store。

</>复制代码

  1. constructor() {
  2. // 创建内存store
  3. const store = memFs.create();
  4. this.fs = memFsEditor.create(store);
  5. this.options = {
  6. name: "",
  7. description: "",
  8. };
  9. this.rootPath = path.resolve(__dirname, "../");
  10. this.tplDirPath = path.join(this.rootPath, "template");
  11. }

接下来在Creator中增加两个方法copy和copyTpl分别用于直接拷贝文件和拷贝文件并注入数据。

</>复制代码

  1. getTplPath(file) {
  2. return path.join(this.tplDirPath, file);
  3. }
  4. copyTpl(file, to, data = {}) {
  5. const tplPath = this.getTplPath(file);
  6. this.fs.copyTpl(tplPath, to, data);
  7. }
  8. copy(file, to) {
  9. const tplPath = this.getTplPath(file);
  10. this.fs.copy(tplPath, to);
  11. }

然后我们根据ejs的语法修改模版中的package.json文件以实现数据注入的功能

</>复制代码

  1. {
  2. "name": "<%= name %>",
  3. "version": "1.0.0",
  4. "description": "<%= description %>",
  5. "main": "index.js",
  6. "scripts": {},
  7. "author": "",
  8. "license": "ISC"
  9. }

回到template/index.js中,对模版中的文件进行相应的拷贝和数据注入操作,最后打印一些可视化的信息。

</>复制代码

  1. module.exports = function(creator, options, callback) {
  2. const { name, description } = options;
  3. // 获取当前命令的执行目录,注意和项目目录区分
  4. const cwd = process.cwd();
  5. // 项目目录
  6. const projectPath = path.join(cwd, name);
  7. const buildPath = path.join(projectPath, "build");
  8. const pagePath = path.join(projectPath, "page");
  9. const srcPath = path.join(projectPath, "src");
  10. // 新建项目目录
  11. // 同步创建目录,以免文件目录不对齐
  12. fs.mkdirSync(projectPath);
  13. fs.mkdirSync(buildPath);
  14. fs.mkdirSync(pagePath);
  15. fs.mkdirSync(srcPath);
  16. creator.copyTpl("packagejson", path.join(projectPath, "package.json"), {
  17. name,
  18. description,
  19. });
  20. creator.copy("build/build.js", path.join(buildPath, "build.js"));
  21. creator.copy("page/index.html", path.join(pagePath, "index.html"));
  22. creator.copy("src/index.js", path.join(srcPath, "index.js"));
  23. creator.fs.commit(() => {
  24. console.log();
  25. console.log(`${chalk.grey(`创建项目: ${name}`)} ${chalk.green("✔ ")}`);
  26. console.log(`${chalk.grey(`创建目录: ${name}/build`)} ${chalk.green("✔ ")}`);
  27. console.log(`${chalk.grey(`创建目录: ${name}/page`)} ${chalk.green("✔ ")}`);
  28. console.log(`${chalk.grey(`创建目录: ${name}/src`)} ${chalk.green("✔ ")}`);
  29. console.log(`${chalk.grey(`创建文件: ${name}/build/build.js`)} ${chalk.green("✔ ")}`);
  30. console.log(`${chalk.grey(`创建文件: ${name}/page/index.html`)} ${chalk.green("✔ ")}`);
  31. console.log(`${chalk.grey(`创建文件: ${name}/src/index.js`)} ${chalk.green("✔ ")}`);
  32. callback();
  33. });
  34. }

执行mycli指令创建项目,一个简单的cli就完成了。

结语

到此,一个简单的cli就制作完成了,大家可以参考vue-cli、create-react-app等优秀的cli适当的扩展自己的cli工具。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/102989.html

相关文章

  • 了解MVVM及Vue实现原理,把手你撸源码。

    摘要:方法实现将所有属性挂载在观察对象,将每一项做一个数据劫持就是将中每一项用定义新属性并返回这个对象。当和发生变化时,自动会触发视图更新,获取得到的也就是最新值。 MVVM及Vue实现原理 Github源码地址:https://github.com/wyj2443573... mvvm 双向数据绑定数据影响视图,视图影响数据angular 脏值检测 vue数据劫持+发布订阅模式vue 不...

    cooxer 评论0 收藏0
  • 把手你撸个vue2.0弹窗组件

    摘要:组件结构同组件结构通过方法获取元素的大小及其相对于视口的位置,之后对提示信息进行定位。可以用来进行一些复杂带校验的弹窗信息展示,也可以只用于简单信息的展示。可以通过属性来显示任意标题,通过属性来修改显示区域的宽度。 手把手教你撸个vue2.0弹窗组件 在开始之前需要了解一下开发vue插件的前置知识,推荐先看一下vue官网的插件介绍 预览地址 http://haogewudi.me/k...

    mrli2016 评论0 收藏0
  • 把手你撸个vue2.0弹窗组件

    摘要:组件结构同组件结构通过方法获取元素的大小及其相对于视口的位置,之后对提示信息进行定位。可以用来进行一些复杂带校验的弹窗信息展示,也可以只用于简单信息的展示。可以通过属性来显示任意标题,通过属性来修改显示区域的宽度。 手把手教你撸个vue2.0弹窗组件 在开始之前需要了解一下开发vue插件的前置知识,推荐先看一下vue官网的插件介绍 预览地址 http://haogewudi.me/k...

    taoszu 评论0 收藏0

发表评论

0条评论

xingqiba

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<