为什么要弄个脚手架
对于我个人,经常写些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 "color: #ff0000">用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 "htmlcode">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等,大概原来差不多,根据输入,选择加载哪些文件,大家自由扩展,谢谢阅读.
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
搭建前端脚手架,前端脚手架搭建
免责声明:本站文章均来自网站采集或用户投稿,网站不提供任何软件下载或自行开发的软件! 如有用户或公司发现本站内容信息存在侵权行为,请邮件告知! 858582#qq.com
《魔兽世界》大逃杀!60人新游玩模式《强袭风暴》3月21日上线
暴雪近日发布了《魔兽世界》10.2.6 更新内容,新游玩模式《强袭风暴》即将于3月21 日在亚服上线,届时玩家将前往阿拉希高地展开一场 60 人大逃杀对战。
艾泽拉斯的冒险者已经征服了艾泽拉斯的大地及遥远的彼岸。他们在对抗世界上最致命的敌人时展现出过人的手腕,并且成功阻止终结宇宙等级的威胁。当他们在为即将于《魔兽世界》资料片《地心之战》中来袭的萨拉塔斯势力做战斗准备时,他们还需要在熟悉的阿拉希高地面对一个全新的敌人──那就是彼此。在《巨龙崛起》10.2.6 更新的《强袭风暴》中,玩家将会进入一个全新的海盗主题大逃杀式限时活动,其中包含极高的风险和史诗级的奖励。
《强袭风暴》不是普通的战场,作为一个独立于主游戏之外的活动,玩家可以用大逃杀的风格来体验《魔兽世界》,不分职业、不分装备(除了你在赛局中捡到的),光是技巧和战略的强弱之分就能决定出谁才是能坚持到最后的赢家。本次活动将会开放单人和双人模式,玩家在加入海盗主题的预赛大厅区域前,可以从强袭风暴角色画面新增好友。游玩游戏将可以累计名望轨迹,《巨龙崛起》和《魔兽世界:巫妖王之怒 经典版》的玩家都可以获得奖励。