从零搭一个自用的前端脚手架的方法步骤


Posted in Javascript onSeptember 23, 2019

为什么要弄个脚手架

对于我个人,经常写些demo,或者写一个新项目的时候,要么就是把以前的项目模板复制一份,要么就是重新搭建一份,显得比较麻烦,浪费时间,所以就有了搭建一个能满足自己需要的脚手架。

脚手架的效果

从零搭一个自用的前端脚手架的方法步骤

这是一个基本的脚手架,init一个项目,输入项目名称,版本号等信息,然后从git仓库拷贝一份自己需要的项目模板。类似vue的vue-cli或者react的create-react-app,只是这个比较简单.

基本思路参考下图

从零搭一个自用的前端脚手架的方法步骤

这部分参考了掘金@张国钰大佬的思路.

项目结构

从零搭一个自用的前端脚手架的方法步骤

主要3个,一个bin文件夹,放执行命令的入口文件

lib文件夹,放项目的主要文件,package.json不多说

这项目主要用到的几个包

  • commander: 命令行工具
  • download-git-repo: 用来下载远程模板
  • ora: 显示loading动画
  • chalk: 修改控制台输出内容样式
  • log-symbols: 显示出 √ 或 × 等的图标
  • inquirer.js:命令交互
  • metalsmith:处理项目模板
  • handlebars:模板引擎

使用commander.js命令行工具

修改package.json的bin执行入口,

"bin": {
  "lz": "./bin/www"
 },

"lz"这个命令可以自己选择,然后在bin文件加创建名为www的文件,

#! /usr/bin/env node
require('../lib/index.js');

其中

#! /usr/bin/env node

不能少,这个主要指定当前脚本由node.js进行解析

在lib创建一个index.js文件,

const program = require('commander')

program.version('1.0.0')
  .usage('<command> [项目名称]')
  .command('init', '创建新项目')
 .parse(process.argv);

为方便测试,先链接到全局环境

npm link

执行下命令感受下

lz init hello

正常来说,应该就报错了,错误堆栈大概就是确实www-init文件,

这是因为
commander支持git风格的子命令处理,可以根据子命令自动引导到以特定格式命名的命令执行文件,文件名的格式是[command]-[subcommand],例如:

macaw hello => macaw-hello
macaw init => macaw-init

所以我们 执行www文件的init,所以要在bin创建一个www-init文件,在lib创建个init.js文件
www-init

#! /usr/bin/env node
require('../lib/init.js');

init.js 完整代码

const program = require('commander')
const path = require('path')
const fs = require('fs')
const glob = require('glob') // npm i glob -D
const download = require('../lib/download.js')
const inquirer = require('inquirer')
const chalk = require('chalk')
const generator = require('../lib/generator')
const logSymbols = require("log-symbols");

 
program.usage('<project-name>')

// 根据输入,获取项目名称
let projectName = process.argv[2];

if (!projectName) { // project-name 必填
 // 相当于执行命令的--help选项,显示help信息,这是commander内置的一个命令选项
 program.help()
 return
}

const list = glob.sync('*') // 遍历当前目录

let next = undefined;

let rootName = path.basename(process.cwd());
if (list.length) { // 如果当前目录不为空
 if (list.some(n => {
  const fileName = path.resolve(process.cwd(), n);
  const isDir = fs.statSync(fileName).isDirectory();
  return projectName === n && isDir
 })) {
  console.log(`项目${projectName}已经存在`);
  return;
 }
 rootName = projectName;
 next = Promise.resolve(projectName);
} else if (rootName === projectName) {
 rootName = '.';
 next = inquirer.prompt([
  {
   name: 'buildInCurrent',
   message: '当前目录为空,且目录名称和项目名称相同,是否直接在当前目录下创建新项目?',
   type: 'confirm',
   default: true
  }
 ]).then(answer => {
  return Promise.resolve(answer.buildInCurrent ? '.' : projectName)
 })
} else {
 rootName = projectName;
 next = Promise.resolve(projectName)
}

next && go()

function go() {
 next
  .then(projectRoot => {
   if (projectRoot !== '.') {
    fs.mkdirSync(projectRoot)
   }
   return download(projectRoot).then(target => {
    return {
     name: projectRoot,
     root: projectRoot,
     downloadTemp: target
    }
   })
  })
  .then(context => {
   return inquirer.prompt([
    {
     name: 'projectName',
     message: '项目的名称',
     default: context.name
    }, {
     name: 'projectVersion',
     message: '项目的版本号',
     default: '1.0.0'
    }, {
     name: 'projectDescription',
     message: '项目的简介',
     default: `A project named ${context.name}`
    }
   ]).then(answers => {
    return {
     ...context,
     metadata: {
      ...answers
     }
    }
   })
  })
  .then(context => {
   //删除临时文件夹,将文件移动到目标目录下
   return generator(context);
  })
  .then(context => {
   // 成功用绿色显示,给出积极的反馈
   console.log(logSymbols.success, chalk.green('创建成功:)'))
   console.log(chalk.green('cd ' + context.root + '\nnpm install\nnpm run dev'))
  })
  .catch(err => {
   // 失败了用红色,增强提示
   console.log(err);
   console.error(logSymbols.error, chalk.red(`创建失败:${err.message}`))
  })
}

init.js都做了什么呢?

首先,获得 init 后面输入的参数,作为项目名称,当然判断这个项目名称是否存在,然后进行对应的逻辑操作,通过download-git-repo工具,下载仓库的模板,然后通过inquirer.js 处理命令行交互,获得输入的名称,版本号能信息,最后在根据这些信息,处理模板文件。

用download-git-repo下载模板文件

在lib下创建download.js文件

const download = require('download-git-repo')
const path = require("path")
const ora = require('ora')

module.exports = function (target) {
 target = path.join(target || '.', '.download-temp');
 return new Promise(function (res, rej) {
  // 这里可以根据具体的模板地址设置下载的url,注意,如果是git,url后面的branch不能忽略
  let url='github:ZoeLeee/BaseLearnCli#bash';
  const spinner = ora(`正在下载项目模板,源地址:${url}`)
  spinner.start();

  download(url, target, { clone: true }, function (err)
  {
    if (err) {
      download(url, target, { clone: false }, function (err)
      {
        if (err) {
          spinner.fail();
          rej(err)
        }
        else {
          // 下载的模板存放在一个临时路径中,下载完成后,可以向下通知这个临时路径,以便后续处理
          spinner.succeed()
          res(target)
        }
      })
    }
    else {
      // 下载的模板存放在一个临时路径中,下载完成后,可以向下通知这个临时路径,以便后续处理
      spinner.succeed()
      res(target)
    }
  })
 })
}

这里注意下下载地址的url,注意url的格式,不是git clone 的那个地址。其中有个clone:false这个参数,如果只是个人用,可以为true,这样就相当于执行的git clone的操作,如果给其他人,可能会出错,用false的话,那个就是直接用http协议去下载这个模板,具体可以去看官网的文档.

inquirer.js 处理命令交互

比较简单,可以看init.js

从零搭一个自用的前端脚手架的方法步骤

这里把获取到的输入信息在往下传递去处理。

metalsmith

接着,要根据获取到的信息,渲染模板。

首先,未不影响原来的模板运行,我们在git仓库上创建一个package_temp.json,对应上我们要交互的变量名

{
 "name": "{{projectName}}",
 "version": "{{projectVersion}}",
 "description": "{{projectDescription}}",
 "main": "./src/index.js",
 "scripts": {
  "dev": "webpack-dev-server --config ./config/webpack.config.js",
  "build": "webpack --config ./config/webpack.config.js --mode production"
 },
 "author": "{{author}}",
 "license": "ISC",
 "devDependencies": {
  "@babel/core": "^7.3.3",
  "@babel/preset-env": "^7.3.1",
  "@babel/preset-react": "^7.0.0",
  "babel-loader": "^8.0.5",
  "clean-webpack-plugin": "^1.0.1",
  "css-loader": "^2.1.0",
  "html-webpack-plugin": "^3.2.0",
  "style-loader": "^0.23.1",
  "webpack": "^4.28.2",
  "webpack-cli": "^3.1.2",
  "webpack-dev-server": "^3.2.0"
 },
 "dependencies": {
  "react": "^16.8.2",
  "react-dom": "^16.8.2"
 }
}

在lib下创建generator.js文件,用来处理模板

const Metalsmith = require('metalsmith')
const Handlebars = require('handlebars')
const remove = require("../lib/remove")
const fs = require("fs")
const path = require("path")

module.exports = function (context) {
 let metadata = context.metadata;
 let src = context.downloadTemp;
 let dest = './' + context.root;
 if (!src) {
  return Promise.reject(new Error(`无效的source:${src}`))
 }

 return new Promise((resolve, reject) => {
  const metalsmith = Metalsmith(process.cwd())
   .metadata(metadata)
   .clean(false)
   .source(src)
   .destination(dest);
  // 判断下载的项目模板中是否有templates.ignore
  const ignoreFile = path.resolve(process.cwd(), path.join(src, 'templates.ignore'));

  const packjsonTemp = path.resolve(process.cwd(), path.join(src, 'package_temp.json'));

  let package_temp_content;

  if (fs.existsSync(ignoreFile)) {
   // 定义一个用于移除模板中被忽略文件的metalsmith插件
   metalsmith.use((files, metalsmith, done) => {
    const meta = metalsmith.metadata()
    // 先对ignore文件进行渲染,然后按行切割ignore文件的内容,拿到被忽略清单
    const ignores = Handlebars
     .compile(fs.readFileSync(ignoreFile).toString())(meta)
     .split('\n').map(s => s.trim().replace(/\//g, "\\")).filter(item => item.length);
    //删除被忽略的文件
    for (let ignorePattern of ignores) {
     if (files.hasOwnProperty(ignorePattern)) {
      delete files[ignorePattern];
     }
    }
    done()
   })
  }
  metalsmith.use((files, metalsmith, done) => {
   const meta = metalsmith.metadata();
   package_temp_content = Handlebars.compile(fs.readFileSync(packjsonTemp).toString())(meta);
   done();
  })

  metalsmith.use((files, metalsmith, done) => {
   const meta = metalsmith.metadata()
   Object.keys(files).forEach(fileName => {
    const t = files[fileName].contents.toString()
    if (fileName === "package.json")
     files[fileName].contents = new Buffer(package_temp_content);
    else
     files[fileName].contents = new Buffer(Handlebars.compile(t)(meta));
   })
   done()
  }).build(err => {
   remove(src);
   err ? reject(err) : resolve(context);
  })
 })
}

通过Handlebars给我们的package_temp.json进行插值渲染,然后把渲染好的文件内容替换掉原先的package.json的内容

其中有时候我们也需要输入选择某些文件不下载,所以,我们在模板仓库加入一个文件,取名templates.ignore,
然后,跟处理package_temp.json类似,优先渲染这个文件内容,找出需要忽略的文件删掉。最后,删除临时文件夹,把文件移动到项目的文件。这样项目就差不多了。
加入删除文件夹得功能,在lib创建remove.js

const fs =require("fs");
const path=require("path");

function removeDir(dir) {
 let files = fs.readdirSync(dir)
 for(var i=0;i<files.length;i++){
  let newPath = path.join(dir,files[i]);
  let stat = fs.statSync(newPath)
  if(stat.isDirectory()){
   //如果是文件夹就递归下去
   removeDir(newPath);
  }else {
   //删除文件
   fs.unlinkSync(newPath);
  }
 }
 fs.rmdirSync(dir)//如果文件夹是空的,就将自己删除掉
}

module.exports=removeDir;

结尾

关于美化得就不说了,大概的脚手架就以上这些内容,当然这些功能太过于简单,我们还需要根据自己的需要,添加功能,比如说,是否要启用Typescript,要less还是sass等,大概原来差不多,根据输入,选择加载哪些文件,大家自由扩展,谢谢阅读.

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
Jquery css函数用法(判断标签是否拥有某属性)
May 28 Javascript
基于jquery实现的表格分页实现代码
Jun 21 Javascript
解析javascript 实用函数的使用详解
May 10 Javascript
详解JavaScript正则表达式中的global属性的使用
Jun 16 Javascript
jQuery实现购物车表单自动结算效果实例
Aug 10 Javascript
AngularJS入门教程之Select(选择框)详解
Jul 27 Javascript
jQuery图片查看插件Magnify开发详解
Dec 25 jQuery
详解JavaScript 中 if / if...else...替换方式
Jul 15 Javascript
基于vue实现移动端圆形旋钮插件效果
Nov 28 Javascript
JS 5种遍历对象的方式
Jun 16 Javascript
Openlayers学习之加载鹰眼控件
Sep 28 Javascript
JavaScript实现网页下拉菜单效果
Nov 20 Javascript
layui 实现加载动画以及非真实加载进度的方法
Sep 23 #Javascript
layui加载数据显示loading加载完成loading消失的实例代码
Sep 23 #Javascript
ES10的13个新特性示例(小结)
Sep 23 #Javascript
layui-tree实现Ajax异步请求后动态添加节点的方法
Sep 23 #Javascript
vue多页面项目中路由使用history模式的方法
Sep 23 #Javascript
JS随机密码生成算法
Sep 23 #Javascript
详解mpvue开发微信小程序基础知识
Sep 23 #Javascript
You might like
Session的工作方式
2006/10/09 PHP
PHP 创建文件(文件夹)以及目录操作代码
2010/03/04 PHP
Laravel 5 学习笔记
2015/03/06 PHP
解决form中action属性后面?传递参数 获取不到的问题
2017/07/21 PHP
Ubuntu中支持PHP5与PHP7双版本的简单实现
2018/08/19 PHP
javascript动画之圆形运动,环绕鼠标运动作小球
2010/07/20 Javascript
jQuery中的read和JavaScript中的onload函数的区别
2014/08/27 Javascript
JS实现的网页倒计时数字时钟效果
2015/03/02 Javascript
JavaScript中字符串(string)转json的2种方法
2015/06/25 Javascript
jquery操作angularjs对象
2015/06/26 Javascript
js实现无限级树形导航列表效果代码
2015/09/23 Javascript
jQuery简单实现input文本框内灰色提示文本效果的方法
2015/12/02 Javascript
移动端js触摸事件详解
2016/09/18 Javascript
JQuery控制DIV的选取实现方法
2016/09/18 Javascript
Three.js实现浏览器变动时进行自适应的方法
2017/09/26 Javascript
jQuery实现鼠标移到某个对象时弹出显示层功能
2018/08/23 jQuery
elementUI多选框反选的实现代码
2019/04/03 Javascript
JS学习笔记之贪吃蛇小游戏demo实例详解
2019/05/29 Javascript
videocapture库制作python视频高速传输程序
2013/12/23 Python
Python批量更改文件名的实现方法
2017/10/29 Python
解决python写入带有中文的字符到文件错误的问题
2019/01/31 Python
python  文件的基本操作 菜中菜功能的实例代码
2019/07/17 Python
python中删除某个元素的方法解析
2019/11/05 Python
python实现删除列表中某个元素的3种方法
2020/01/15 Python
在主流系统之上安装Pygame的方法
2020/05/20 Python
Selenium及python实现滚动操作多种方法
2020/07/21 Python
HTML5+lufylegend实现游戏中的卷轴
2016/02/29 HTML / CSS
澳大利亚工具仓库:Tools Warehouse
2018/10/15 全球购物
怎样写演讲稿
2014/01/04 职场文书
高效课堂标语
2014/06/26 职场文书
三八妇女节趣味活动方案
2014/08/23 职场文书
乡镇党委书记第三阶段个人整改措施
2014/09/16 职场文书
2015元旦文艺汇演主持稿(开场白+结束语)
2014/12/14 职场文书
课文《燕子》教学反思
2016/02/17 职场文书
PHP 技巧 * SVG 保存为图片(分享图生成)
2021/04/02 PHP
mysql主从复制的实现步骤
2021/10/24 MySQL