升職加薪利器 - 帶你手把手構造一個基于pug的靜態(tài)頁面生成腳手架

溫馨提示:內容很多很長,記錄了本人的一步步思考步驟,如果各位大佬看不下去想嘗試以下或者想看看代碼請直接拖到最后查看

本人介紹

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

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

為什么要做一個靜態(tài)頁面生成腳手架

在這個vue、react、angular三大前端框架橫行的年代,我們對于前端頁面的編寫也是習慣了單頁面的開發(fā),但是殊不知有這么一群人在辛辛苦苦的寫著靜態(tài)頁面做頁面模板的工作。他們辛辛苦苦的在不同的靜態(tài)頁面中進行著ctrl + c 加 ctrl + v的工作,心里想著美滋滋啊,這個列表(或布局,或……)以前竟然寫過,趕緊讓我找找又可以不用寫一段代碼了。


image

但是我們既然都寫過,為什么不能和三大框架一樣用組件、指令之類的東西呢?一句話搞定毫無壓力啊,為了給各位大佬們減少工作壓力,本菜雞就干活了。

腳手架的基礎內容

在下也是個菜雞,讀了大佬的文章后才知道怎么制作一個腳手架<a target="__black">接水怪大佬</a>

image

開始思路

我們還是來整理一下我們這個腳手架需要有哪些內容:

  • 入口
  • 創(chuàng)建項目create
  • 運行dev
  • 打包build
  • 添加新項目add

emmm…………作為一個菜雞來說好像很復雜的樣子,想放棄- -

image

老板:小伙子加油,年底我準備換輛新車了

我:好的老板

還是讓我們一步步來吧。

入口

功能需求

image

其實我們的主入口需要做的就是把指令進行注冊,然后進行一些版本號的提示之類的事情,那么這里就先放一個最簡單的注冊。

腳手架模板

program
    .command('create')
    .description('用我可以創(chuàng)建一個項目哦')
    .alias('c')
    .action(() => {
        console.log('我已經(jīng)創(chuàng)建了一個項目?。俚模?);
    })
// 然后再加上輸出
program
    .version(require('./package.json').version, '-v --version')
    .parse(process.argv);
    
if (!process.argv.slice(2).length) {
    program.outputHelp();
}

執(zhí)行beon后我們就得到了這么一個結果,美滋滋入口關鍵已經(jīng)完成了呀!

image

但是有不熟悉的小伙伴可以不知道這個到底該在哪運行注冊了,那么就要上基礎腳手架的配置了,具體怎么配置我就不詳細解釋了,我就給大家一個小模版使用:https://gitee.com/missshen/model-cli。本地運行只需要把代碼拉下來安裝依賴后運行npm link??!然后指令是beon-test

image

其中package.json里面的幾個配置說一下

{
  "name": "cli-model", // 名字沒啥用,只有進行npm發(fā)布的時候有用
  "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"
}

代碼功能

這哈只要動手能力強的大佬都成功的完成了自己的第一個小腳手架入口了,那么這就開始我們靜態(tài)頁面腳手架的編寫了!,基本結構如下:


image

這樣寫對于我們后面的拓展可以方便很多

老板:還有抽分功能和數(shù)據(jù)的思想,小伙砸年底給你加雞腿

import program from 'commander';

import create from './create'; // 項目創(chuàng)建
import dev from './dev'; // 項目啟動
import build from './build'; //項目打包
import add from './add'; // 新建站點

let actionMap = {
    // 項目創(chuàng)建
    create: {
        description: '創(chuàng)建一個新的項目', // 描述
        usages: [// 使用方法
            'beon create'
        ],
        alias: 'c' // 命令簡稱
    },
    // 啟動項目
    dev: {
        description: '本地啟動項目',
        usages: [
            'beon dev'
        ],
        alias: 'd'
    },
    //打包
    build: {
        description: '服務端項目打包',
        usages: [
            'beon build'
        ],
        alias: 'b'
    },
    // 新建站點
    add: {
        description: '創(chuàng)建一個新的站點', // 描述
        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

創(chuàng)建目標

當我們執(zhí)行了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

接下來就是進行我們項目功能制作了,思路一樣的利用數(shù)據(jù)循環(huán)來進行注冊,后期擴展方便。

// 詢問用戶
let promptList = [{
        type: 'list',
        name: 'frame',
        message: '選擇模板或者現(xiàn)有測試項目',
        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);
            })
    });
}

下載項目模板

這就需要網(wǎng)上大佬做的三方包來支持了 <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...感覺這些東西網(wǎng)上一搜一大把也講不出啥來,看看就完事了。

配置項目模式

到這就要好好說說項目模式的思想了,實現(xiàn)設計了以下兩種模式:

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('站點創(chuàng)建完成'));
    await loadCmd(`rm -rf *`, '刪除單站點文件', ProjectName + '/src/pug')
}

我們進行了創(chuàng)建sites文件夾的操作以及刪除了src/pug的所有文件以免出現(xiàn)誤解(<b style="color: red">所有內容都在模板基礎上進行操作的,模板是單項目模式</b>)

在這里放出模板<a target="__blank">git地址</a>(最后會整理所有地址)

安裝依賴運行

其實這個到?jīng)]有多少寫的,大家都是npm i、然后運行而已,只是我們是直接用node進行的,那么就需要使用exec和spawn了,
這就放一個執(zhí)行器方法了。

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}: 命令執(zhí)行中...`);
    if (cd) {
        await runner(cmd, { cwd: path.resolve(process.cwd(), cd), detached: true, shell: true });
    } else {
        await runner(cmd);
    }
    loading.succeed(`${text}: 命令執(zhí)行完成`);
}

可以看到我們還用promisify包裝了一下,變成了promise使用起來更加方便。

個人思考

其實整個創(chuàng)建并不復雜,只是響對應處理多個模式的思想比較不那么容易想到,實現(xiàn)很簡單也并沒有采取很多腳手架的直接全部通過js來進行生成,而是采用了網(wǎng)上下載模板的方式。

image

dev跑起來!

開始進入船新階段了,開始使用webpack了,還是一樣我這邊就不做過多的webpack介紹了,直接從了解webpack基礎上開始!

分析功能

image

乍一看很復雜的樣子,其實只是寫的比較細致而已,這個流程也并不是代碼執(zhí)行的先后順序,而是我們思路的先后順序而已。

接下來讓我們來開始進行<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

然后我只需要進行一定的參數(shù)設置,就可以成功加載我們的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元素的底部
        })
    }),
]

我:我們只需要這樣循環(huán)注冊就美滋滋了

大佬: ???你這東西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
        };
    });

當然這段代碼大家看著樂呵就行了,因為做了多項目和單項目區(qū)分處理所有有未知參數(shù)出現(xiàn)

路由入口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提現(xiàn)程序員不停追尋的結果(編不下去了)。

好的這樣我們就成功的構建了我們的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

看整個流程關鍵點在于對于單項目到多項目的切換,最后覺得還是要刪除掉單項目文件避免多人開發(fā)造成誤會(當然選擇的時候會進行提示)

整個流程代碼如下

const site = await newPrompt();
const siteName = site.siteName;
// 站點名不能為空
if (siteName.length === 0) {
    console.log(symbol.error, chalk.greenBright('新建站點的時候,請輸入站點名'));
} else {
    // 如果文件名不存在則繼續(xù)執(zhí)行,否則退出
    await notExistFold(path.resolve('sites') + `/${siteName}`);
    console.log(chalk.blueBright('開始創(chuàng)建新站點目錄'));
    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

運行指令:

  • 創(chuàng)建:beon create 或者 beon c
  • 運行:beon dev 或者 beon d
  • 打包:beon build 或者 beon b
  • 新增:beon add 或者 beon a

自己運行的時候一定記得運行npm run watch!!!這樣才能實時生成babel后的代碼,然后npm link就可以本地開發(fā)運行了。

image

最后上一張打包的目錄結構(一點都看不出來是打包生成的項目)

image
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容