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


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 相关文章推荐
setInterval 和 setTimeout会产生内存溢出
Feb 15 Javascript
js直接编辑当前cookie的脚本
Sep 14 Javascript
jquery Validation表单验证使用详解
Sep 12 Javascript
JavaScript读二进制文件并用ajax传输二进制流的方法
Jul 18 Javascript
用JS中split方法实现彩色文字背景效果实例
Aug 24 Javascript
js改变透明度实现轮播图的算法
Aug 24 Javascript
JS中Array数组学习总结
Jan 18 Javascript
vue-cli 项目打包完成后运行文件路径报错问题
Jul 19 Javascript
Vue项目打包部署到iis服务器的配置方法
Oct 14 Javascript
CountUp.js数字滚动插件使用方法详解
Oct 17 Javascript
node.js使用net模块创建服务器和客户端示例【基于TCP协议】
Feb 14 Javascript
解决vue页面渲染但dom没渲染的操作
Jul 27 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
PHP 数组入门教程小结
2009/05/20 PHP
php函数间的参数传递(值传递/引用传递)
2013/09/23 PHP
PHP使用ob_start生成html页面的方法
2014/11/07 PHP
PHP实现的简单缓存类
2015/07/29 PHP
php封装的pdo数据库操作工具类与用法示例
2019/05/08 PHP
javascript编程起步(第四课)
2007/02/27 Javascript
JAVASCRIPT下判断IE与FF的比较简单的方式
2008/10/17 Javascript
jQuery select控制插件
2009/08/17 Javascript
半角全角相互转换的js函数
2009/10/16 Javascript
浏览器图片选择预览、旋转、批量上传的JS代码实现
2013/12/04 Javascript
Html5的placeholder属性(IE兼容)实现代码
2014/08/30 Javascript
jQuery中html()方法用法实例
2014/12/25 Javascript
jQuery使用之处理页面元素用法实例
2015/01/19 Javascript
基于MVC4+EasyUI的Web开发框架形成之旅之界面控件的使用
2015/12/16 Javascript
基于JavaScript实现根据手机定位获取当前具体位置(X省X市X县X街道X号)
2015/12/29 Javascript
详解JS对象封装的常用方式
2016/12/30 Javascript
Node接收电子邮件的实例代码
2017/07/21 Javascript
基于Vue实现页面切换左右滑动效果
2020/06/29 Javascript
Vue 页面跳转不用router-link的实现代码
2018/04/12 Javascript
使用vue点击li,获取当前点击li父辈元素的属性值方法
2018/09/12 Javascript
详解js模板引擎art template数组渲染的方法
2018/10/09 Javascript
node-red File读取好保存实例讲解
2019/09/11 Javascript
[42:25]2018DOTA2亚洲邀请赛 4.5 淘汰赛 LGD vs Liquid 第三场
2018/04/06 DOTA
尝试用最短的Python代码来实现服务器和代理服务器
2016/06/23 Python
Django2.1.3 中间件使用详解
2018/11/26 Python
python模块导入的细节详解
2018/12/10 Python
python 获取utc时间转化为本地时间的方法
2018/12/31 Python
美国儿童玩具、装扮和玩偶商店:Magic Cabin
2018/09/02 全球购物
Pottery Barn阿联酋:购买家具、家居装饰及更多
2019/12/08 全球购物
汽车检测与维修专业求职信
2013/10/30 职场文书
社区志愿者心得体会
2014/01/03 职场文书
家长学校培训材料
2014/08/20 职场文书
幼儿园语言教学反思
2016/02/23 职场文书
导游词之苏州盘门景区
2019/11/12 职场文书
CSS3实现的文字弹出特效
2021/04/16 HTML / CSS
SpringCloud Alibaba项目实战之nacos-server服务搭建过程
2021/06/21 Java/Android