升职加薪利器 - 带你手把手构造一个基于pug的静态页面生成脚手架

温馨提示:内容很多很长,记录了本人的一步步思考步骤,如果各位大佬看不下去想尝试以下或者想看看代码请直接拖到最后查看

本人介绍

class BEON {
    name: string;
    sex: string;
    ability: number;

    constructor() {
        this.name = 'BEON';
        this.sex = '♂';
        this.ability = 1;
    }
}

为什么要做一个静态页面生成脚手架

在这个vue、react、angular三大前端框架横行的年代,我们对于前端页面的编写也是习惯了单页面的开发,但是殊不知有这么一群人在辛辛苦苦的写着静态页面做页面模板的工作。他们辛辛苦苦的在不同的静态页面中进行着ctrl + c 加 ctrl + v的工作,心里想着美滋滋啊,这个列表(或布局,或……)以前竟然写过,赶紧让我找找又可以不用写一段代码了。


image

但是我们既然都写过,为什么不能和三大框架一样用组件、指令之类的东西呢?一句话搞定毫无压力啊,为了给各位大佬们减少工作压力,本菜鸡就干活了。

脚手架的基础内容

在下也是个菜鸡,读了大佬的文章后才知道怎么制作一个脚手架<a href="https://juejin.im/post/5e7a22a8e51d4526d87c99ed" target="__black">接水怪大佬</a>

image

开始思路

我们还是来整理一下我们这个脚手架需要有哪些内容:

  • 入口
  • 创建项目create
  • 运行dev
  • 打包build
  • 添加新项目add

emmm…………作为一个菜鸡来说好像很复杂的样子,想放弃- -

image

老板:小伙子加油,年底我准备换辆新车了

我:好的老板

还是让我们一步步来吧。

入口

功能需求

image

其实我们的主入口需要做的就是把指令进行注册,然后进行一些版本号的提示之类的事情,那么这里就先放一个最简单的注册。

脚手架模板

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后我们就得到了这么一个结果,美滋滋入口关键已经完成了呀!

image

但是有不熟悉的小伙伴可以不知道这个到底该在哪运行注册了,那么就要上基础脚手架的配置了,具体怎么配置我就不详细解释了,我就给大家一个小模版使用:https://gitee.com/missshen/model-cli。本地运行只需要把代码拉下来安装依赖后运行npm link!!然后指令是beon-test

image

其中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"
}

代码功能

这哈只要动手能力强的大佬都成功的完成了自己的第一个小脚手架入口了,那么这就开始我们静态页面脚手架的编写了!,基本结构如下:


image

这样写对于我们后面的拓展可以方便很多

老板:还有抽分功能和数据的思想,小伙砸年底给你加鸡腿

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();
}
image

这样就完成了我们入口的需求了

项目create

创建目标

当我们执行了create方法的时候,应该进行以下操作目标

image

那么一步步来构建我们的代码吧!

提问获取基础信息

对于提问器就需要用到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);
        })
});
image

接下来就是进行我们项目功能制作了,思路一样的利用数据循环来进行注册,后期扩展方便。

// 询问用户
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...感觉这些东西网上一搜一大把也讲不出啥来,看看就完事了。

配置项目模式

到这就要好好说说项目模式的思想了,实现设计了以下两种模式:

image

这个地方的两种模式在使用的时候会有少许差异,在后面进行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来进行生成,而是采用了网上下载模板的方式。

image

dev跑起来!

开始进入船新阶段了,开始使用webpack了,还是一样我这边就不做过多的webpack介绍了,直接从了解webpack基础上开始!

分析功能

image

乍一看很复杂的样子,其实只是写的比较细致而已,这个流程也并不是代码执行的先后顺序,而是我们思路的先后顺序而已。

接下来让我们来开始进行<span style="color: #e22222">webpack</span>配置吧。

目录结构

这里不直接开始说代码而是提前说一下结构我觉得是很有必要的,这样可以简化我们构建的复杂度(一般webpack配置会比较长),这样可以让我们代码的易读性增加

image

可以看到我这边写的时候是吧整个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,功能简单单一

image

<p style="background-color: rgb(254, 67, 101);padding: 10px; color:#fff ">看着应该没啥问题了,接下来就是webpack配置了由于内容太多一句句讲解不如自己拿代码看官方文档,在这就说几个主要的内容了</p>

构建pug文件为html

在这里就又要用到插件了HtmlWebpackPlugin(这个菜鸡原来只会用插件啊)

image

然后我只需要进行一定的参数设置,就可以成功加载我们的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运行器了,不赶紧运行一波?

image

这么简简单单的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结构

image

看整个流程关键点在于对于单项目到多项目的切换,最后觉得还是要删除掉单项目文件避免多人开发造成误会(当然选择的时候会进行提示)

整个流程代码如下

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就可以本地开发运行了。

image

最后上一张打包的目录结构(一点都看不出来是打包生成的项目)

image
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350