温馨提示:内容很多很长,记录了本人的一步步思考步骤,如果各位大佬看不下去想尝试以下或者想看看代码请直接拖到最后查看
本人介绍
class BEON {
name: string;
sex: string;
ability: number;
constructor() {
this.name = 'BEON';
this.sex = '♂';
this.ability = 1;
}
}
为什么要做一个静态页面生成脚手架
在这个vue、react、angular三大前端框架横行的年代,我们对于前端页面的编写也是习惯了单页面的开发,但是殊不知有这么一群人在辛辛苦苦的写着静态页面做页面模板的工作。他们辛辛苦苦的在不同的静态页面中进行着ctrl + c 加 ctrl + v的工作,心里想着美滋滋啊,这个列表(或布局,或……)以前竟然写过,赶紧让我找找又可以不用写一段代码了。
但是我们既然都写过,为什么不能和三大框架一样用组件、指令之类的东西呢?一句话搞定毫无压力啊,为了给各位大佬们减少工作压力,本菜鸡就干活了。
脚手架的基础内容
在下也是个菜鸡,读了大佬的文章后才知道怎么制作一个脚手架<a href="https://juejin.im/post/5e7a22a8e51d4526d87c99ed" target="__black">接水怪大佬</a>
开始思路
我们还是来整理一下我们这个脚手架需要有哪些内容:
- 入口
- 创建项目create
- 运行dev
- 打包build
- 添加新项目add
emmm…………作为一个菜鸡来说好像很复杂的样子,想放弃- -
老板:小伙子加油,年底我准备换辆新车了
我:好的老板
还是让我们一步步来吧。
入口
功能需求
其实我们的主入口需要做的就是把指令进行注册,然后进行一些版本号的提示之类的事情,那么这里就先放一个最简单的注册。
脚手架模板
program
.command('create')
.description('用我可以创建一个项目哦')
.alias('c')
.action(() => {
console.log('我已经创建了一个项目!(假的)');
})
// 然后再加上输出
program
.version(require('./package.json').version, '-v --version')
.parse(process.argv);
if (!process.argv.slice(2).length) {
program.outputHelp();
}
执行beon后我们就得到了这么一个结果,美滋滋入口关键已经完成了呀!
但是有不熟悉的小伙伴可以不知道这个到底该在哪运行注册了,那么就要上基础脚手架的配置了,具体怎么配置我就不详细解释了,我就给大家一个小模版使用:https://gitee.com/missshen/model-cli。本地运行只需要把代码拉下来安装依赖后运行npm link!!然后指令是beon-test
其中package.json里面的几个配置说一下
{
"name": "cli-model", // 名字没啥用,只有进行npm发布的时候有用
"version": "1.0.0", // 版本号
"description": "cli脚手架模板", // 描述
"main": "main.js", // 入口(我们这没啥用)
"scripts": {},
"bin": {
"beon-test": "./bin/cmd" // !!重要,前面那个就是你的指令名称
},
"keywords": [
"cli-model" // 没啥用
],
"dependencies": {
"commander": "^5.0.0" // 包
},
"author": "wyx",
"license": "ISC"
}
代码功能
这哈只要动手能力强的大佬都成功的完成了自己的第一个小脚手架入口了,那么这就开始我们静态页面脚手架的编写了!,基本结构如下:
这样写对于我们后面的拓展可以方便很多
老板:还有抽分功能和数据的思想,小伙砸年底给你加鸡腿
import program from 'commander';
import create from './create'; // 项目创建
import dev from './dev'; // 项目启动
import build from './build'; //项目打包
import add from './add'; // 新建站点
let actionMap = {
// 项目创建
create: {
description: '创建一个新的项目', // 描述
usages: [// 使用方法
'beon create'
],
alias: 'c' // 命令简称
},
// 启动项目
dev: {
description: '本地启动项目',
usages: [
'beon dev'
],
alias: 'd'
},
//打包
build: {
description: '服务端项目打包',
usages: [
'beon build'
],
alias: 'b'
},
// 新建站点
add: {
description: '创建一个新的站点', // 描述
usages: [// 使用方法
'beon add'
],
alias: 'a' // 命令简称
}
}
Object.keys(actionMap).forEach(action => {
program
.command(action)
.description(actionMap[action].description)
.alias(actionMap[action].alias)
.action(() => {
switch (action) {
case 'create':
create();
break;
case 'dev':
dev();
break;
case 'build':
build();
break;
case 'add':
add();
break;
default:
break;
}
})
});
program
.version(require('../package.json').version, '-v --version')
.parse(process.argv);
if (!process.argv.slice(2).length) {
program.outputHelp();
}
这样就完成了我们入口的需求了
项目create
创建目标
当我们执行了create方法的时候,应该进行以下操作目标
那么一步步来构建我们的代码吧!
提问获取基础信息
对于提问器就需要用到inquirer这个包了,使用方法也很简单:
inquirer
.prompt({
type: 'input',
name: 'ProjectName',
message: '输入该项目名称'
})
.then(answer => {
console.log(answer);
})
这就是一个很简单的提问器了,然后我们包个promise就可以使用async await来写代码了,看起来就非常的舒服
const { ProjectName } = await new Promise(resolve => {
inquirer
.prompt({
type: 'input',
name: 'ProjectName',
message: '输入该项目名称'
})
.then(answer => {
resolve(answer);
})
});
接下来就是进行我们项目功能制作了,思路一样的利用数据循环来进行注册,后期扩展方便。
// 询问用户
let promptList = [{
type: 'list',
name: 'frame',
message: '选择模板或者现有测试项目',
choices: ['single', 'sites']
},
{
type: 'input',
name: 'description',
message: '输入该项目描述: '
},
{
type: 'input',
name: 'author',
message: '请输入您的大名: '
}
];
let prompt = () => {
return new Promise(resolve => {
inquirer
.prompt(promptList)
.then(answer => {
resolve(answer);
})
});
}
下载项目模板
这就需要网上大佬做的三方包来支持了 <span style="color: #e00000">download-git-repo</span> !
使用方式也是很简单粗暴直接引入使用就可以了,这里我们就直接放项目代码了
import downloadGit from 'download-git-repo';
// 项目模板远程下载
let downloadTemplate = async(ProjectName, api) => {
return new Promise((resolve, reject) => {
downloadGit(api, ProjectName, { clone: true }, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
})
});
};
更新package.json
文件???这就到readFileSync出马了呀
// 更新json配置文件
let updateJsonFile = (fileName, obj) => {
return new Promise(resolve => {
if (fs.existsSync(fileName)) {
const data = fs.readFileSync(fileName).toString();
let json = JSON.parse(data);
Object.keys(obj).forEach(key => {
json[key] = obj[key];
});
fs.writeFileSync(fileName, JSON.stringify(json, null, '\t'), 'utf-8');
resolve();
}
});
}
emmm...感觉这些东西网上一搜一大把也讲不出啥来,看看就完事了。
配置项目模式
到这就要好好说说项目模式的思想了,实现设计了以下两种模式:
这个地方的两种模式在使用的时候会有少许差异,在后面进行dev和build指令的时候会说明
简单来说就是:单项目没有sites文件夹,src内就是项目内容;多项目有sites文件夹,并且里面每一个文件就是一个项目,而src文件夹作为一个存放公共js、css、pug模板的地方了
结合上面的内容,那么我们就要来写代码了
if (answer.frame === 'sites') {
const siteName = 'test';
fs.mkdirSync(path.resolve(`${ProjectName}/sites`));
addSites(siteName, `${ProjectName}/`)
console.log(symbol.success, chalk.green('站点创建完成'));
await loadCmd(`rm -rf *`, '删除单站点文件', ProjectName + '/src/pug')
}
我们进行了创建sites文件夹的操作以及删除了src/pug的所有文件以免出现误解(<b style="color: red">所有内容都在模板基础上进行操作的,模板是单项目模式</b>)
在这里放出模板<a href="https://gitee.com/missshen/model" target="__blank">git地址</a>(最后会整理所有地址)
安装依赖运行
其实这个到没有多少写的,大家都是npm i、然后运行而已,只是我们是直接用node进行的,那么就需要使用exec和spawn了,
这就放一个执行器方法了。
const util = require("util");
const exec = util.promisify(require("child_process").exec);
const spawn = util.promisify(require("child_process").spawn);
let loadCmd = async(cmd, text, cd, check) => {
let loading = ora();
const runner = check ? spawn : exec;
loading.start(`${text}: 命令执行中...`);
if (cd) {
await runner(cmd, { cwd: path.resolve(process.cwd(), cd), detached: true, shell: true });
} else {
await runner(cmd);
}
loading.succeed(`${text}: 命令执行完成`);
}
可以看到我们还用promisify包装了一下,变成了promise使用起来更加方便。
个人思考
其实整个创建并不复杂,只是响对应处理多个模式的思想比较不那么容易想到,实现很简单也并没有采取很多脚手架的直接全部通过js来进行生成,而是采用了网上下载模板的方式。
dev跑起来!
开始进入船新阶段了,开始使用webpack了,还是一样我这边就不做过多的webpack介绍了,直接从了解webpack基础上开始!
分析功能
乍一看很复杂的样子,其实只是写的比较细致而已,这个流程也并不是代码执行的先后顺序,而是我们思路的先后顺序而已。
接下来让我们来开始进行<span style="color: #e22222">webpack</span>配置吧。
目录结构
这里不直接开始说代码而是提前说一下结构我觉得是很有必要的,这样可以简化我们构建的复杂度(一般webpack配置会比较长),这样可以让我们代码的易读性增加
可以看到我这边写的时候是吧整个webpack作为3部分来配置的,并且有一个主要的入口文件,叫dev.js 这样我们的功能划分就很明显。
- dev.js 负责运行时候的一些额外配置,例如端口检测等
- webpack.dev.js 就是一些只有在dev时会运行的配置
- webpack.config.js 是公共的配置
- webpack.build.js 是构建时的配置(在这说了之后后面不再讲解结构)
dev.js
这是这几个文件中最小的一个,就直接拿出来看就行了,主要了解一个插件portfinder可以帮我们进行端口占用检测。
await portfinder
.getPortPromise({
port: baseConfig.devServer.port || port || devPort
})
.then(port => {
devPort = port;
//
// `port` is guaranteed to be a free port
// in this scope.
//
})
.catch(err => {
console.log(err)
//
// Could not get a free port, `err` contains the reason.
//
});
new WebpackDevServer(webpack(baseConfig), baseConfig.devServer)
.listen(devPort, 'localhost', function (err, result) {
if (err) {
console.log(err);
}
});
这就是dev的功能了,检测端口然后运行webpack,功能简单单一
<p style="background-color: rgb(254, 67, 101);padding: 10px; color:#fff ">看着应该没啥问题了,接下来就是webpack配置了由于内容太多一句句讲解不如自己拿代码看官方文档,在这就说几个主要的内容了</p>
构建pug文件为html
在这里就又要用到插件了HtmlWebpackPlugin(这个菜鸡原来只会用插件啊)
然后我只需要进行一定的参数设置,就可以成功加载我们的pug文件了
plugins: [
new webpack.HotModuleReplacementPlugin(), // 热更新插件(偷偷留个注释不做说明)
...tool.getTpl(siteName).map(file => {
return new HtmlWebpackPlugin({
template: file.path,
filename: file.filename + '.html',
chunks: ['main', 'vendor' || null], //这里引入公共文件main.js
chunksSortMode: 'manual', //将chunks按引入的顺序排序
inject: true //所有JavaScript资源插入到body元素的底部
})
}),
]
我:我们只需要这样循环注册就美滋滋了
大佬: ???你这东西file文件天上掉下来的?
我:不是自动就有了么?☺
对于文件的获取我们只需要进行遍历文件夹下的pug后缀文件就行了,就像这样
const returnPath = siteName === false ? './src/pug/' : `./sites/${siteName}/pug/`
const files = glob
.sync(siteName === false ? './src/pug/*.pug' : `./sites/${siteName}/pug/*.pug`)
.map(filepath => {
const list = filepath.split(/[\/|\/\/|\\|\\\\]/g); // 斜杠分割文件目录
const fullname = list[list.length - 1].replace(/\.js/g, '');
// 拿到文件的 filename
const name = fullname.substring(0, fullname.lastIndexOf('.'));
return {
path: returnPath +
pathSeparator +
name +
'.pug',
filename: name
};
});
当然这段代码大家看着乐呵就行了,因为做了多项目和单项目区分处理所有有未知参数出现
路由入口html
就在上面那段看着乐呵的代码下面就是入口html模板的制作了,其实就是html代码片段生成了html文件而已
${files
.map(f => {
return ` <a href="${f.filename + '.html'}">${
f.filename
}</a><br>`;
})
.join('\n')}
</body>
</html>`;
这代码看起来就很简单粗暴
这样就构建了一个基础的dev运行器了,不赶紧运行一波?
这么简简单单的Hello World提现程序员不停追寻的结果(编不下去了)。
好的这样我们就成功的构建了我们的dev了
build打个包
既然我们都能成功的运行起来了那么build的配置也就差不多了,我们只需要稍稍的加一个output就完美了(当然不可能),配置关键还是得拉项目下来看。
今天我们就简简单单看看build.js就美滋滋了。
console.log( symbol.success , chalk.green('开始打包'));
webpack(baseConfig, async (error) => {
if(error !== null){
console.log(symbol.error, chalk.greenBright(error));
}else{
setTimeout(() => {
loadCmd(`rm main.js`, '删除main.js', './dist/js')
});
console.log(symbol.success, chalk.green('打包完成'));
}
// process.exit(1);
});
这里看我把main.js删了是因为一个奇怪的需求,让我打包后的项目看起来不像打包过的一样☺
add添加项目
把build水过了之后这个需要好好写了(中间水了也刚好没人看)
整理一下add结构
看整个流程关键点在于对于单项目到多项目的切换,最后觉得还是要删除掉单项目文件避免多人开发造成误会(当然选择的时候会进行提示)
整个流程代码如下
const site = await newPrompt();
const siteName = site.siteName;
// 站点名不能为空
if (siteName.length === 0) {
console.log(symbol.error, chalk.greenBright('新建站点的时候,请输入站点名'));
} else {
// 如果文件名不存在则继续执行,否则退出
await notExistFold(path.resolve('sites') + `/${siteName}`);
console.log(chalk.blueBright('开始创建新站点目录'));
try {
const isHave = await notExistFold(path.resolve('sites'), true);
if (isHave !== true) {
const answer = await new Promise(resolve => {
inquirer
.prompt([{
type: 'list',
name: 'warning',
message: '当前项目为单站点模式,是否确认转换为多站点(转换后无法对src内pug进行打包)',
choices: ['yes', 'no']
}])
.then(answer => {
resolve(answer.warning);
})
});
if (answer === 'yes') {
fs.mkdirSync(path.resolve('sites'));
} else {
return ;
}
}
addSites(siteName);
console.log(chalk.blueBright('新建站点成功!'));
} catch ( e ) {
console.log(symbol.error, chalk.greenBright('新建目录失败,请手动检测问题'));
}
}
其实切换更像对前面说过的内容的一次整合理解,如果有兴趣的大佬可以手动重新写一次add就可以把之前的内容复习一遍了。
结束语
至此脚手架的讲解基本上就算完成了,如果大家有什么问题可以评论提出来,鄙人会尽可能帮大家解答的。这也算是本人第一次比较详细的写文章了,当然会有很多不足代码肯定也会有bug(<span style="color: #dc5712">什么?我的代码没有bug!</span>),欢迎大家提出问题,如果大家有兴趣的话后期还会不定期进行更新的。
脚手架代码地址:https://gitee.com/missshen/beon-page-cli
模板项目地址:https://gitee.com/missshen/model
npm包安装命令:cnpm i -g beon-page-cli
运行指令:
- 创建:beon create 或者 beon c
- 运行:beon dev 或者 beon d
- 打包:beon build 或者 beon b
- 新增:beon add 或者 beon a
自己运行的时候一定记得运行npm run watch!!!这样才能实时生成babel后的代码,然后npm link就可以本地开发运行了。
最后上一张打包的目录结构(一点都看不出来是打包生成的项目)