这篇文章将为大家详细讲解有关web开发中如何搭建前端脚手架,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。
脚手架的效果
这是一个基本的脚手架,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', '创建新项目') .parseprocess.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.basenameprocess.cwd)); if list.length) { // 如果当前目录不为空 if list.somen => { const fileName = path.resolveprocess.cwd), n); const isDir = fs.statSyncfileName).isDirectory); return projectName === n && isDir })) { console.log`项目${projectName}已经存在`); return; } rootName = projectName; next = Promise.resolveprojectName); } else if rootName === projectName) { rootName = '.'; next = inquirer.prompt[ { name: 'buildInCurrent', message: '当前目录为空,且目录名称和项目名称相同,是否直接在当前目录下创建新项目?', type: 'confirm', default: true } ]).thenanswer => { return Promise.resolveanswer.buildInCurrent ? '.' : projectName) }) } else { rootName = projectName; next = Promise.resolveprojectName) } next && go) function go) { next .thenprojectRoot => { if projectRoot !== '.') { fs.mkdirSyncprojectRoot) } return downloadprojectRoot).thentarget => { return { name: projectRoot, root: projectRoot, downloadTemp: target } }) }) .thencontext => { 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}` } ]).thenanswers => { return { ...context, metadata: { ...answers } } }) }) .thencontext => { //删除临时文件夹,将文件移动到目标目录下 return generatorcontext); }) .thencontext => { // 成功用绿色显示,给出积极的反馈 console.loglogSymbols.success, chalk.green'创建成功:)')) console.logchalk.green'cd ' + context.root + '\nnpm install\nnpm run dev')) }) .catcherr => { // 失败了用红色,增强提示 console.logerr); console.errorlogSymbols.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.jointarget || '.', '.download-temp'); return new Promisefunction res, rej) { // 这里可以根据具体的模板地址设置下载的url,注意,如果是git,url后面的branch不能忽略 let url='github:ZoeLeee/BaseLearnCli#bash'; const spinner = ora`正在下载项目模板,源地址:${url}`) spinner.start); downloadurl, target, { clone: true }, function err) { if err) { downloadurl, target, { clone: false }, function err) { if err) { spinner.fail); rejerr) } else { // 下载的模板存放在一个临时路径中,下载完成后,可以向下通知这个临时路径,以便后续处理 spinner.succeed) restarget) } }) } else { // 下载的模板存放在一个临时路径中,下载完成后,可以向下通知这个临时路径,以便后续处理 spinner.succeed) restarget) } }) }) }
这里注意下下载地址的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.rejectnew Error`无效的source:${src}`)) } return new Promiseresolve, reject) => { const metalsmith = Metalsmithprocess.cwd)) .metadatametadata) .cleanfalse) .sourcesrc) .destinationdest); // 判断下载的项目模板中是否有templates.ignore const ignoreFile = path.resolveprocess.cwd), path.joinsrc, 'templates.ignore')); const packjsonTemp = path.resolveprocess.cwd), path.joinsrc, 'package_temp.json')); let package_temp_content; if fs.existsSyncignoreFile)) { // 定义一个用于移除模板中被忽略文件的metalsmith插件 metalsmith.usefiles, metalsmith, done) => { const meta = metalsmith.metadata) // 先对ignore文件进行渲染,然后按行切割ignore文件的内容,拿到被忽略清单 const ignores = Handlebars .compilefs.readFileSyncignoreFile).toString))meta) .split'\n').maps => s.trim).replace/\//g, "\\")).filteritem => item.length); //删除被忽略的文件 for let ignorePattern of ignores) { if files.hasOwnPropertyignorePattern)) { delete files[ignorePattern]; } } done) }) } metalsmith.usefiles, metalsmith, done) => { const meta = metalsmith.metadata); package_temp_content = Handlebars.compilefs.readFileSyncpackjsonTemp).toString))meta); done); }) metalsmith.usefiles, metalsmith, done) => { const meta = metalsmith.metadata) Object.keysfiles).forEachfileName => { const t = files[fileName].contents.toString) if fileName === "package.json") files[fileName].contents = new Bufferpackage_temp_content); else files[fileName].contents = new BufferHandlebars.compilet)meta)); }) done) }).builderr => { removesrc); err ? rejecterr) : resolvecontext); }) }) }
通过Handlebars给我们的package_temp.json进行插值渲染,然后把渲染好的文件内容替换掉原先的package.json的内容
其中有时候我们也需要输入选择某些文件不下载,所以,我们在模板仓库加入一个文件,取名templates.ignore,
然后,跟处理package_temp.json类似,优先渲染这个文件内容,找出需要忽略的文件删掉。最后,删除临时文件夹,把文件移动到项目的文件。这样项目就差不多了。
加入删除文件夹得功能,在lib创建remove.js
const fs =require"fs"); const path=require"path"); function removeDirdir) { let files = fs.readdirSyncdir) forvar i=0;i<files.length;i++){ let newPath = path.joindir,files[i]); let stat = fs.statSyncnewPath) ifstat.isDirectory)){ //如果是文件夹就递归下去 removeDirnewPath); }else { //删除文件 fs.unlinkSyncnewPath); } } fs.rmdirSyncdir)//如果文件夹是空的,就将自己删除掉 } module.exports=removeDir;