前端脚手架,听起来玄乎,实际呢?

开篇

前端脚手架,听起来玄乎,实际呢?

然后我就装着知道的样子就去回答了一下,答案是这样的:

前端脚手架,听起来玄乎,实际呢?

粗狂的讲,这样的回答,好像没什么毛病。但既然是本着学习的态度,那这次就好好的讲一讲前端脚手架存在的意义,到底是个什么鬼,以及写一个脚手架到底有多难?所以接下来,文章将围绕下面几部分来讨论:

  • 前端脚手架存在的意义
  • 脚手架的实质
  • 写一个属于自己的脚手架有多难

前端脚手架存在的意义

随着前端工程化的概念越来越深入人心,脚手架的出现就是为减少重复性工作而引入的命令行工具,摆脱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的项目脚手架,其功能都是生成一个通用的目录结构,并配上构建、编译、检查等工程环境。大致流程如下:

  1. 解析用户输入的命令;
  2. 生成一些配置化文件,如package.json, 或webpack.config.js等;
  3. 根据用户的输入生成对应的模板项目;(高级一点的ctrl + c, ctrl + v)
  4. 安装该模板所需要的环境。

先以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。
综上,前端脚手架的实质包含两项,命令式的构建项目(解析命令,拷贝项目到本地),提供项目的配置(构建,编译,代码规范检查)。

写一个属于自己的脚手架有多难

整理思路

从上两节的描述,大致整理一下即将要实现的功能:

  1. 命令的解析,这个可以借助commander实现;
  2. 文件的操作,复制,粘贴,增加,删除,文件内容的新增,替换;这个可以借助fs-extra实现;
  3. 模板文件,就以自己对前端工程化粗浅的认识,写一个最牛(cu)逼(lou)的模板项目;
  4. 申请一个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(`git@github.com:closertb/${template}.git`);
      const result = spawn(
        'git',
        ['clone', `git@github.com: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

相关推荐