從零搭一個(gè)自用的前端腳手架的方法步驟
為什么要弄個(gè)腳手架
對(duì)于我個(gè)人,經(jīng)常寫些demo,或者寫一個(gè)新項(xiàng)目的時(shí)候,要么就是把以前的項(xiàng)目模板復(fù)制一份,要么就是重新搭建一份,顯得比較麻煩,浪費(fèi)時(shí)間,所以就有了搭建一個(gè)能滿足自己需要的腳手架。
腳手架的效果
這是一個(gè)基本的腳手架,init一個(gè)項(xiàng)目,輸入項(xiàng)目名稱,版本號(hào)等信息,然后從git倉(cāng)庫(kù)拷貝一份自己需要的項(xiàng)目模板。類似vue的vue-cli或者react的create-react-app,只是這個(gè)比較簡(jiǎn)單.
基本思路參考下圖
這部分參考了掘金@張國(guó)鈺大佬的思路.
項(xiàng)目結(jié)構(gòu)
主要3個(gè),一個(gè)bin文件夾,放執(zhí)行命令的入口文件
lib文件夾,放項(xiàng)目的主要文件,package.json不多說(shuō)
這項(xiàng)目主要用到的幾個(gè)包
- commander: 命令行工具
- download-git-repo: 用來(lái)下載遠(yuǎn)程模板
- ora: 顯示loading動(dòng)畫
- chalk: 修改控制臺(tái)輸出內(nèi)容樣式
- log-symbols: 顯示出 √ 或 × 等的圖標(biāo)
- inquirer.js:命令交互
- metalsmith:處理項(xiàng)目模板
- handlebars:模板引擎
使用commander.js命令行工具
修改package.json的bin執(zhí)行入口,
"bin": { "lz": "./bin/www" },
"lz"這個(gè)命令可以自己選擇,然后在bin文件加創(chuàng)建名為www的文件,
#! /usr/bin/env node require('../lib/index.js');
其中
#! /usr/bin/env node
不能少,這個(gè)主要指定當(dāng)前腳本由node.js進(jìn)行解析
在lib創(chuàng)建一個(gè)index.js文件,
const program = require('commander') program.version('1.0.0') .usage('<command> [項(xiàng)目名稱]') .command('init', '創(chuàng)建新項(xiàng)目') .parse(process.argv);
為方便測(cè)試,先鏈接到全局環(huán)境
npm link
執(zhí)行下命令感受下
lz init hello
正常來(lái)說(shuō),應(yīng)該就報(bào)錯(cuò)了,錯(cuò)誤堆棧大概就是確實(shí)www-init文件,
這是因?yàn)?br /> commander支持git風(fēng)格的子命令處理,可以根據(jù)子命令自動(dòng)引導(dǎo)到以特定格式命名的命令執(zhí)行文件,文件名的格式是[command]-[subcommand],例如:
macaw hello => macaw-hello
macaw init => macaw-init
所以我們 執(zhí)行www文件的init,所以要在bin創(chuàng)建一個(gè)www-init文件,在lib創(chuàng)建個(gè)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>') // 根據(jù)輸入,獲取項(xiàng)目名稱 let projectName = process.argv[2]; if (!projectName) { // project-name 必填 // 相當(dāng)于執(zhí)行命令的--help選項(xiàng),顯示help信息,這是commander內(nèi)置的一個(gè)命令選項(xiàng) program.help() return } const list = glob.sync('*') // 遍歷當(dāng)前目錄 let next = undefined; let rootName = path.basename(process.cwd()); if (list.length) { // 如果當(dāng)前目錄不為空 if (list.some(n => { const fileName = path.resolve(process.cwd(), n); const isDir = fs.statSync(fileName).isDirectory(); return projectName === n && isDir })) { console.log(`項(xiàng)目${projectName}已經(jīng)存在`); return; } rootName = projectName; next = Promise.resolve(projectName); } else if (rootName === projectName) { rootName = '.'; next = inquirer.prompt([ { name: 'buildInCurrent', message: '當(dāng)前目錄為空,且目錄名稱和項(xiàng)目名稱相同,是否直接在當(dāng)前目錄下創(chuàng)建新項(xiàng)目?', 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: '項(xiàng)目的名稱', default: context.name }, { name: 'projectVersion', message: '項(xiàng)目的版本號(hào)', default: '1.0.0' }, { name: 'projectDescription', message: '項(xiàng)目的簡(jiǎn)介', default: `A project named ${context.name}` } ]).then(answers => { return { ...context, metadata: { ...answers } } }) }) .then(context => { //刪除臨時(shí)文件夾,將文件移動(dòng)到目標(biāo)目錄下 return generator(context); }) .then(context => { // 成功用綠色顯示,給出積極的反饋 console.log(logSymbols.success, chalk.green('創(chuàng)建成功:)')) console.log(chalk.green('cd ' + context.root + '\nnpm install\nnpm run dev')) }) .catch(err => { // 失敗了用紅色,增強(qiáng)提示 console.log(err); console.error(logSymbols.error, chalk.red(`創(chuàng)建失?。?{err.message}`)) }) }
init.js都做了什么呢?
首先,獲得 init 后面輸入的參數(shù),作為項(xiàng)目名稱,當(dāng)然判斷這個(gè)項(xiàng)目名稱是否存在,然后進(jìn)行對(duì)應(yīng)的邏輯操作,通過(guò)download-git-repo工具,下載倉(cāng)庫(kù)的模板,然后通過(guò)inquirer.js 處理命令行交互,獲得輸入的名稱,版本號(hào)能信息,最后在根據(jù)這些信息,處理模板文件。
用download-git-repo下載模板文件
在lib下創(chuàng)建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) { // 這里可以根據(jù)具體的模板地址設(shè)置下載的url,注意,如果是git,url后面的branch不能忽略 let url='github:ZoeLeee/BaseLearnCli#bash'; const spinner = ora(`正在下載項(xiàng)目模板,源地址:${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 { // 下載的模板存放在一個(gè)臨時(shí)路徑中,下載完成后,可以向下通知這個(gè)臨時(shí)路徑,以便后續(xù)處理 spinner.succeed() res(target) } }) } else { // 下載的模板存放在一個(gè)臨時(shí)路徑中,下載完成后,可以向下通知這個(gè)臨時(shí)路徑,以便后續(xù)處理 spinner.succeed() res(target) } }) }) }
這里注意下下載地址的url,注意url的格式,不是git clone 的那個(gè)地址。其中有個(gè)clone:false這個(gè)參數(shù),如果只是個(gè)人用,可以為true,這樣就相當(dāng)于執(zhí)行的git clone的操作,如果給其他人,可能會(huì)出錯(cuò),用false的話,那個(gè)就是直接用http協(xié)議去下載這個(gè)模板,具體可以去看官網(wǎng)的文檔.
inquirer.js 處理命令交互
比較簡(jiǎn)單,可以看init.js
這里把獲取到的輸入信息在往下傳遞去處理。
metalsmith
接著,要根據(jù)獲取到的信息,渲染模板。
首先,未不影響原來(lái)的模板運(yùn)行,我們?cè)趃it倉(cāng)庫(kù)上創(chuàng)建一個(gè)package_temp.json,對(duì)應(yīng)上我們要交互的變量名
{ "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下創(chuàng)建generator.js文件,用來(lái)處理模板
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(`無(wú)效的source:${src}`)) } return new Promise((resolve, reject) => { const metalsmith = Metalsmith(process.cwd()) .metadata(metadata) .clean(false) .source(src) .destination(dest); // 判斷下載的項(xiàng)目模板中是否有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)) { // 定義一個(gè)用于移除模板中被忽略文件的metalsmith插件 metalsmith.use((files, metalsmith, done) => { const meta = metalsmith.metadata() // 先對(duì)ignore文件進(jìn)行渲染,然后按行切割ignore文件的內(nèi)容,拿到被忽略清單 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); }) }) }
通過(guò)Handlebars給我們的package_temp.json進(jìn)行插值渲染,然后把渲染好的文件內(nèi)容替換掉原先的package.json的內(nèi)容
其中有時(shí)候我們也需要輸入選擇某些文件不下載,所以,我們?cè)谀0鍌}(cāng)庫(kù)加入一個(gè)文件,取名templates.ignore,
然后,跟處理package_temp.json類似,優(yōu)先渲染這個(gè)文件內(nèi)容,找出需要忽略的文件刪掉。最后,刪除臨時(shí)文件夾,把文件移動(dòng)到項(xiàng)目的文件。這樣項(xiàng)目就差不多了。
加入刪除文件夾得功能,在lib創(chuàng)建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;
結(jié)尾
關(guān)于美化得就不說(shuō)了,大概的腳手架就以上這些內(nèi)容,當(dāng)然這些功能太過(guò)于簡(jiǎn)單,我們還需要根據(jù)自己的需要,添加功能,比如說(shuō),是否要啟用Typescript,要less還是sass等,大概原來(lái)差不多,根據(jù)輸入,選擇加載哪些文件,大家自由擴(kuò)展,謝謝閱讀.
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
原生js實(shí)現(xiàn)數(shù)字字母混合驗(yàn)證碼的簡(jiǎn)單實(shí)例
這篇文章主要介紹了原生js實(shí)現(xiàn)數(shù)字字母混合驗(yàn)證碼的簡(jiǎn)單實(shí)例,注釋很詳細(xì),感興趣的小伙伴們可以參考一下2015-12-12javascript charAt() arr[i]數(shù)組實(shí)例代碼
實(shí)例區(qū)別一下charAt()和arr[i].toString()的使用方法2008-08-08vue+ts下對(duì)axios的封裝實(shí)現(xiàn)
這篇文章主要介紹了vue+ts下對(duì)axios的封裝實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-02-02JavaScript Spread Syntax (...)的十種使用方法
這篇文章主要介紹了JavaScript Spread Syntax (...)的十個(gè)強(qiáng)大用途,擴(kuò)展語(yǔ)法即Spread Syntax(…) 是 ES6 中引入的一個(gè)新特性,它允許我們從可迭代對(duì)象中快速提取元素2022-07-07ES6基礎(chǔ)之?dāng)?shù)組和對(duì)象的拓展實(shí)例詳解
這篇文章主要介紹了ES6基礎(chǔ)之?dāng)?shù)組和對(duì)象的拓展,結(jié)合實(shí)例形式詳細(xì)分析了ES6數(shù)組和對(duì)象拓展運(yùn)算符、拓展方法的使用及相關(guān)操作技巧,需要的朋友可以參考下2019-08-08js 動(dòng)態(tài)生成json對(duì)象、時(shí)時(shí)更新json對(duì)象的方法
下面小編就為大家?guī)?lái)一篇js 動(dòng)態(tài)生成json對(duì)象、時(shí)時(shí)更新json對(duì)象的方法。小編覺的挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-12-12