从零实现一个mini版Vue Cli脚手架

1,322 阅读4分钟

1. 脚手架定义

    脚手架是为了保证各施工过程顺利进行而搭设的工作平台。对我们前端程序员来说,可以帮我们更好,更高效地进行编码工作。

image.png

2. 脚手架的常用命令

  1. 首先我们先建一个工程:bob_cli,进行bob_cli文件夹执行npm init 进行工程初始化,再创建一个lib文件夹和index.js文件

image.png

  1. 安装commander包,用来解析控制台输入的指令,去npm 里面进行搜素,有详细说明

image.png

  1. 自定义执行入口文件,如下图,以后可以用bob来代替index.js。然后npm link 使bob能进行相关映射

image.png

4.在index.js里面编写如下代码

                                    index.js
  #!/usr/bin/env node

const { program } = require("commander");
const { helpOptions } = require("./lib/core/help");
//获取版本号
program.version(require("./package.json").version);
helpOptions();
//解析控制台的指令
program.parse(process.argv);
                                ./lib/core/help.js                             
 const { program } = require("commander");
 //自定义选项
const helpOptions = () => {
  program.option("-s, --small", "small pizza size");
  program.option(
    "-d --dest <dest>",
    "a destination folder, like: -d /src/components"
  );
  //还可以自定义事件监听
  program.on("--help", function () {
    console.log("");
    console.log("Other:");
    console.log("  other options~");
  });
};
  1. 在控制台输入 bob --help, bob --V看到下面输出信息,就成功了。有些选项还没做,后面有需求可以完善。
Options:
  -V, --version                 output the version number
  -s, --small                   small pizza size
  -d --dest <dest>              a destination folder, like: -d /src/components
  -h, --help                    display help for command
  Other:
  other options~
  commaner -V
1.0.0

3. 脚手架做的事情

 比如我们常用的vue-cli。当我们敲下 vue-cli create project,它会帮我们做好哪些事情呢?   
  1. 用户选择需要的配置项,像常用的 vue-router,vuex,css 预处理器等。
  2. 安装所需依赖到工程环境。
  3. 进入npm run serve 启动项目。
  4. 打开浏览器输入网址。

现在我们从这4个步骤一个个来配置

  1. 现在我们省去用户交互选择配置的过程,直接定制一个配置好的通用的模板,此时需要安装download-git-repo包。地址:gitlab.com/flippidippi…
  2. 解析控制台命令,例如bob create demo01,我们要创建一个demo01文件夹,并将下载好的模板文件放进去
                            bob_cli\lib\core\action.js
                            
  //create project
  program
    .command("create <project> [others...]")
    .description("clone a repository into a newly created directory")
    .action(createProjectAction);
    
    
                           bob_cli\lib\core\action.js
                   
  //callBack ==> promisify ===>promise ==> async/await
const createProjectAction = async (project) => {
  //1.克隆项目
  await download(vueRepo, project, { clone: true });

  // 2.执行npm install
  //兼容windows
  const command = process.platform === "win32" ? "npm.cmd" : "npm";
  await commandOptions(command, ["install"], { cwd: `${project}` });

  //3.启动serve
  await commandOptions(command, ["run", "serve"], { cwd: `${project}` });

  //4.打开浏览器
  open("http://localhost:8080");
};

                            bob_cli\lib\config\repo-config.js
                            
 //注意这里分为三段:direct+git http的地址+#分支名称  
 //这里拿coderwhy老师的vue模板来演示下
let vueRepo = 'direct:https://github.com/coderwhy/hy-vue-temp.git#master';
module.exports = {
  vueRepo,
};                   

                            bob_cli\lib\utils\terminal.js
/**
 * 执行终端命令相关的代码
 */
//开启子进程
const { spawn } = require("child_process");

const commandOptions = (...args) => {
  return new Promise((resolve, reject) => {
     console.log("ready to create project ");
    const childProcess = spawn(...args);

    //将子进程的输出信息输出到当前控制台
    childProcess.stdout.pipe(process.stdout);
    childProcess.stderr.pipe(process.stderr);
    //监听子进程执行完了
    childProcess.on("close", (code) => {
      console.log(`child process exited with code ${code}`);
      resolve()
    });
  });
};
  1. 打开浏览器所需的包执行 npm install open就可以了。
  2. 需要注意 启动serve那段代码,需要根据package.json里的script 运行命令一致 image.png
  3. 最后可以看到拉取下来的demo01工程目录,浏览器也自动打开

4. 自动生成组件和路由文件

4.1 生成组件

  1. 此时我们要用到ejs包,来解析ejs语法。npm i ejs
  2. 准备好组件模板vue-component.ejs
                                 bob_cli\lib\templates\vue-component.ejs 
<template>
  <div class="<%= data.lowerName %>">
    <h1>{{ msg }}</h1>
  </div>
</template>

<script>
export default {
  name: '<%= data.name %>',
  props: {
    msg: String
  },
  components: {
  },
  mixins: [],
  data: function() {
    return {
      message: "<%= data.name %>"
    }
  },
  created: function() {

  },
  mounted: function() {

  },
  computed: {

  },
  methods: {

  }
}
</script>
<style scoped>
  .<%= data.lowerName %> {
    
  }
</style>
                                 bob_cli\lib\core\create.js

program
    .command("addcpn <name>")
    .description(
      "add vue component, like:addcpn HelloWorld [-d src/components]"
    )
    .action((name) => {
     const options = program.opts();
      addCpnAction(name, options.dest || "src/components");
    });
                                 bob_cli\lib\core\action.js       
    
    const addCpnAction = async (name, dest) => {
  //1.编译ejs模板 result
  const result = await compile("vue-component.ejs", {
    name,
    lowerName: name.toLowerCase(),
  });
  console.log(result);

  //2.写入文件夹
  //没有就创建。
  
  if (createDirSync(dest)) {
  const targetPath = path.resolve(dest, `${name}.vue`);
  writeToFile(targetPath, result);
  }
};
    
                               bob_cli\lib\utils\utils.js
                                                           
//解析组件模板 成字符串,然后写入目标文件夹中
const compile = (templateName, data) => {
  const fileOption = `../templates/${templateName}`;
  const filePath = path.resolve(__dirname, fileOption);

  return new Promise((resolve, reject) => {
    ejs.renderFile(filePath, { data }, {}, (err, str) => {
      if (err) {
        reject(err);
        return;
      }
      resolve(str);
    });
  });
};

const writeToFile = (path, content) => {
  return fs.promises.writeFile(path, content);
};

  /**
 * 递归创建不存在的文件夹
 * @param {路径名 包含后缀的文件} pathName 
 * @returns 
 */
const createDirSync = (pathName) => {
    if (fs.existsSync(pathName)) {
      return true;
    } else {
      if (createDirSync(path.dirname(pathName))) {
        fs.mkdirSync(pathName);
        return true;
      }
    }
  }
                    
  1. 进入demo01文件夹,运行bob addcpn goods -d pages/goods,可以看到demo01下生成pages,并且里面的ejs模板全部替换为了组件名字 image.png

4.2 生成模块文件夹及模块路由文件

  1. 思路跟上面生成组件差不多。先准备router的ejs模板
                                bob_cli\lib\templates\vue-router.ejs
                        
// 普通加载路由
// import <%= data.name %> from './<%= data.name %>.vue'
// 懒加载路由
const <%= data.name %> = () => import('./<%= data.name %>.vue')
export default {
  path: '/<%= data.lowerName %>',
  name: '<%= data.name %>',
  component: <%= data.name %>,
  children: [
  ]
}
                               bob_cli\lib\core\create.js
                               
program
    .command("addpage <page>")
    .description(
      "add vue page and router config, like: why addpage Home [-d src/pages]"
    )
    .action((page) => {
    const options = program.opts();
      addPageRouteAction(page, options.dest || "src/pages");
    });
    
                               bob_cli\lib\core\action.js
    
    //b.创建文件加及路由
const addPageRouteAction = async (name, dest) => {
  //1.编译ejs模板 result
  const pageResult = await compile("vue-component.ejs", {
    name,
    lowerName: name.toLowerCase(),
  });

  const routerResult = await compile("vue-router.ejs", {
    name,
    lowerName: name.toLowerCase(),
  });
  //将目标文件夹和组件文件夹做一个拼接。
  const targetPath = path.resolve(dest, name.toLowerCase());
  //先判断是否有文件夹,没有就创建、、
  if (createDirSync(targetPath)) {
    //2.写入文件夹
    //program.dest可以获取到终端的<dest>
    const targetPagePath = path.resolve(targetPath, `${name}.vue`);

    const targetRouterPath = path.resolve(targetPath, "router.js");

    writeToFile(targetPagePath, pageResult);
    writeToFile(targetRouterPath, routerResult);
  }
};

                              bob_cli\lib\utils\utils.js
                              
//解析组件模板 成字符串,然后写入目标文件夹中
const compile = (templateName, data) => {
  const fileOption = `../templates/${templateName}`;
  const filePath = path.resolve(__dirname, fileOption);

  return new Promise((resolve, reject) => {
    ejs.renderFile(filePath, { data }, {}, (err, str) => {
      if (err) {
        reject(err);
        return;
      }
      resolve(str);
    });
  });
};

/**
 * 递归创建不存在的文件夹
 * @param {路径名 包含后缀的文件} pathName 
 * @returns 
 */
const createDirSync = (pathName) => {
    if (fs.existsSync(pathName)) {
      return true;
    } else {
      if (createDirSync(path.dirname(pathName))) {
        fs.mkdirSync(pathName);
        return true;
      }
    }
  }
  
const writeToFile = (path, content) => {
  return fs.promises.writeFile(path, content);
};
    
  1. 控制台输入命令bob addpage register -d src/views/pages 看输出结果,这样不用再一个个模块再手动注册路由和组件 image.png

4.3 生成vuex模块

过程如4.2一致。就不重复粘贴代码。

5. 打包发布到NPM

  1. 先到官网注册 npm账号

image.png

  1. 在vscode中对发布的包进行配置
//发布的包名
"name": "bobvue_cli",
  "version": "1.0.0",
  "description": "a ",
  "main": "index.js",
  "bin": {
  //脚手架命令
    "bobvue_cli": "index.js"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "bob",
  //算所算法关键字
  "keywords": ["vue-cli","vue","vue-template","vue3"],
  //github所在仓库
  "homepage": "https://github.com/xiaoboRao/bobvue_cli",
  "repository": {
    "type": "git",
    "url": "https://github.com/xiaoboRao/bobvue_cli"
  },
  1. 在vscode发布包,执行npm login,输入注册的账号,密码,邮箱,第一次登陆时会提示OTP,npm会发送一个验证码到注册邮箱,输入即可登陆了

image.png

  1. 接下来去npm下载这个包,验证下,最终结果在预想之中。

6. 附上GitHub地址和npm地址

   GitHub:[https://github.com/xiaoboRao/bobvue_cli](bobvue_cli)
   npm:[https://www.npmjs.com/package/bobvue_cli](bobvue_cli)