從零開始搭建 Vue 腳手架工具(二)

新的一年新的開始

github倉庫地址

腳手架源碼 ivue-clicss

模板配置 webpack前端

開始

繼續上一篇文章的講解,讓咱們繼續來看如何實現 init 功能。(如您想閱讀上一篇內容能夠點擊這裏)vue

讓咱們先新建一個腳手架的配置文件scaffold-config-dev.jsonnode

lib->scaffold->templates->scaffold-config-dev.jsonwebpack

{
    "version": "0.1.0",
    "defaults": {
        "framework": "Vue",
        "template": "Basic"
    },
    "frameworks": [
        {
            "value": "Vue",
            "name": "Vue2",
            "subList": {
                "template": [
                    {
                        "value": "Basic",
                        "name": "Basic",
                        "git": "https://github.com/lavas-project/lavas-template-vue.git",
                        "branch": "release-basic",
                        "desc": "基礎模版,包含 Ivue Material Ui",
                        "locals": {
                            "zh_CN": {
                                "desc": "基礎模版,包含 Ivue Material Ui \n包含額外配置選項 (默認包含 Babel)"
                            },
                            "en": {
                                "desc": "Basic Template, contains Ivue Material Ui \nIncludes additional configuration options (default Babel)"
                            }
                        }
                    },
                    {
                        "value": "Basic-MPA",
                        "name": "Basic-MPA",
                        "git": "https://github.com/lavas-project/lavas-template-vue.git",
                        "branch": "release-basic-mpa",
                        "desc": "多頁面模版,包含 Ivue Material Ui 和 PWA 工程化相關必需內容",
                        "locals": {
                            "zh_CN": {
                                "desc": "多頁面模版,包含 Ivue Material Ui \n(默認包含 Babel, Router,Sass)"
                            },
                            "en": {
                                "desc": "Mpa Template, contains Ivue Material Ui \n(default Babel,Router,Sass)"
                            }
                        }
                    },
                    {
                        "value": "PWA-SPA",
                        "name": "PWA-SPA",
                        "git": "https://github.com/lavas-project/lavas-template-vue.git",
                        "branch": "release-pwa-spa",
                        "desc": "PWA 單頁面模版,包含 Ivue Material Ui 和 PWA 工程化相關必需內容",
                        "locals": {
                            "zh_CN": {
                                "desc": "PWA 單頁面模版,包含 Ivue Material Ui 和 PWA 工程化相關必需內容 \n(默認包含 Babel,Router,Sass)"
                            },
                            "en": {
                                "desc": "PWA Basic Template, contains Ivue Material Ui and PWA \n(default Babel,Router,Sass)"
                            }
                        }
                    },
                    {
                        "value": "PWA-MPA",
                        "name": "PWA-MPA",
                        "git": "https://github.com/lavas-project/lavas-template-vue.git",
                        "branch": "release-pwa-mpa",
                        "desc": "PWA 多頁面模版,包含 Ivue Material Ui 和 PWA 工程化相關必需內容",
                        "locals": {
                            "zh_CN": {
                                "desc": "PWA 多頁面模版,包含 Ivue Material Ui 和 PWA 工程化相關必需內容 \n(默認包含 Babel,Router,Vuex,Sass)"
                            },
                            "en": {
                                "desc": "PWA Mpa Template, contains Ivue Material Ui and PWA \n(default Babel,Router,Vuex,Sass)"
                            }
                        }
                    }
                ]
            }
        }
    ],
    "schema": {
        "framework": {
            "type": "list",
            "name": "前端框架",
            "description": "項目所選擇的基礎框架",
            "locals": {
                "zh_CN": {
                    "name": "前端框架",
                    "description": "項目所選擇的基礎框架"
                },
                "en": {
                    "name": "framework",
                    "description": "The framework chosen for the project"
                }
            },
            "required": true,
            "link": "frameworks",
            "default": "vue",
            "checkbox": false,
            "disable": true,
            "depLevel": 0,
            "list": [],
            "jsonType": "string"
        },
        "template": {
            "type": "list",
            "name": "模版類型",
            "description": "初始化項目時選中的模版類型",
            "locals": {
                "zh_CN": {
                    "name": "模版類型",
                    "description": "初始化項目時選中的模版類型"
                },
                "en": {
                    "name": "template",
                    "description": "The type of template selected when initializing the project"
                }
            },
            "dependence": "framework",
            "default": "Basic",
            "ref": "template",
            "depLevel": 1,
            "checkbox": false,
            "required": true,
            "list": [],
            "jsonType": "string"
        },
        "checkbox": {
            "type": "checkbox",
            "key": "checkbox",
            "name": "選擇選項",
            "description": "檢查項目所需的功能",
            "required": true,
            "checkbox": true,
            "list": [
                {
                    "value": "router",
                    "name": "Router",
                    "checked": false
                },
                {
                    "value": "vuex",
                    "name": "Vuex",
                    "checked": false
                },
                {
                    "value": "css",
                    "name": "CSS Pre-processors",
                    "checked": false
                },
                {
                    "value": "typescript",
                    "name": "Typescript",
                    "checked": false
                }
            ],
            "depLevel": 0,
            "jsonType": "string"
        },
        "csssProcessors": {
            "type": "list",
            "key": "csssProcessors",
            "name": "選擇CSS預處理器",
            "description": "(支持PostCSS,Autoprefixer和CSS模塊默認狀況下)",
            "required": true,
            "checkbox": true,
            "list": [
                {
                    "value": "scss",
                    "name": "Sass/SCSS"
                },
                {
                    "value": "less",
                    "name": "Less"
                },
                {
                    "value": "stylus",
                    "name": "Stylus"
                }
            ],
            "depLevel": 0,
            "jsonType": "string"
        }
    }
}
複製代碼

而後在commander 下新建文件在該目錄下管理主邏輯代碼ios

commander->scaffold->index.jsgit

'use strict';

// init 安裝腳手架命令
const init = require('./action');
// 提示文件
const locals = require('../../locals')();

module.exports = function (program) {

    // define init command
    program
        .command('init')
        .description(locals.INIT_DESC)
        .option('-f, --force', locals.INIT_OPTION_FORCE)
        .action(options => init({
            force: options.force
        }));
};
複製代碼

locals.js 文件中添加提示github

module.exports = {
  .....
  INIT_DESC: '初始化 ivue-cli 項目',
  INIT_OPTION_FORCE: '是否覆蓋已有項目',
  .....
};
複製代碼

以上建立了 init 命令的運行web

接下來讓咱們來看看 init 命令的代碼實現

首先檢查當前網絡環境, 建立檢查網絡環境方法isNetworkConnectvue-router

lib->utils->index.js

const dns = require('dns');

/**
 * 檢測當前網絡環境
 *
 * @return {Boolean} 是否聯網
 */
exports.isNetworkConnect = function () {
    return new Promise((reslove) => {
        dns.lookup('baidu.com', (err) => reslove(!(err && err.code === 'ENOTFOUND')));
    });
}

複製代碼

而後咱們去建立一個文件管理錯誤提示

locals->zh_CN->index.js

module.exports = {
    .....
    NETWORK_DISCONNECT: '建立工程須要下載雲端模版',
    NETWORK_DISCONNECT_SUG: '請確認您的設備處於網絡可訪問的環境中',
    WELECOME: `歡迎使用`,
    GREETING_GUIDE: '開始新建一個項目',
    .....
};

複製代碼

新建 action.js用於init命令核心代碼文件,同時引用 isNetworkConnect 檢測網絡

commander->scaffold->action.js

const utils = require('../../lib/utils')
const log = require('../../lib/utils/log');
const locals = require('../../locals')();

module.exports = async function (conf) {
    // 檢測當前網絡環境
    let isNetWorkOk = await utils.isNetworkConnect();

    // 離線提示
    if (!isNetWorkOk) {
        log.error(locals.NETWORK_DISCONNECT);
        log.error(locals.NETWORK_DISCONNECT_SUG);
        return;
    }
    
    log.info(locals.WELECOME);
    log.info(locals.GREETING_GUIDE + '\n');

    .....
}

複製代碼

當沒有網絡時會輸出如下內容:

不然輸出以下內容:

初始化過程的6個步驟

如今開始讓咱們來看看初始化過程的初始化過程的6個步驟

第一步:從雲端配置獲取 Meta 配置。肯定將要下載的框架和模板 lish

locals->zh_CN->index.js

1.添加提示

module.exports = {
    .....
    LOADING_FROM_CLOUD: '正在拉取雲端數據,請稍候',
    .....
};

複製代碼

2.安裝包

// 下載中動畫效果
"ora": "^1.3.0"
複製代碼

使用後效果如圖:

3.接下來引用下載配置的方法 getMetaSchema,下載完成後調用spinner.stop() 中止下載中效果

const scaffold = require('../../lib/scaffold');

    // 第一步:從雲端配置獲取 Meta 配置。肯定將要下載的框架和模板 lish
    let spinner = ora(locals.LOADING_FROM_CLOUD + '...');
    spinner.start();
    let metaSchema = await scaffold.getMetaSchema();
    spinner.stop();
複製代碼

4.接下來讓咱們看看getMetaSchema()方法是如何實現的。

  • 新建一個store.js用於緩存數據

lib/scaffold

/**
 * @file 簡單的 store
 */

'use strict';

const store = {};

module.exports = {
    /**
     * setter
     *
     * @param {String} name store key
     * @param {Any} value store value
     */
    set (name, value) {
        store[name] = value;
    },

    /**
     * getter
     *
     * @param {String} name store key
     * @return {[type]} store value
     */
    get (name) {
        return store[name];
    }
}
複製代碼
  • 新建一個config.js用於設置公共配置
/**
 * @file  scaffold 相關配置
 */
'use strict';
const jsonP = require('./templates/scaffold-config-dev.json');

module.exports = {
    /**
     * 全局的配置文件地址
     *
     * @type {String}
     */
    GLOBAL_CONF_URL: {
        production: jsonP,
        development: jsonP
    },
}

複製代碼
  • 新建getMeta.js獲取meta配置
const store = require('./store');
const conf = require('./config');

// 若是是開發環境就使用開發環境的 CONF 數據,避免污染線上的 CONF 數據
const confUrl = conf.GLOBAL_CONF_URL[
    process.env.NODE_ENV === 'development'
        ? 'development'
        : 'production'
];

/**
 * 請求全局的配置 JOSN 數據
 *
 * @return {Object}   JSON 數據
 */
module.exports = async function () {
    let data = store.get('data');

    // 若是 store 中已經存在了,2s 後再嘗試更新下是否是有最新的數據
    if (data) {
        let timer = setTimeout(async () => {
            let json = await confUrl;

            store.set('data', json);
            clearTimeout(timer);
        }, 2000);

        return data;
    }

    // 若是 store 裏面沒有,咱們立刻就獲取一份最新的數據
    data = await confUrl;
    store.set('data', data);

    return data;
}

複製代碼
  • 以上新建了獲取配置的方法接下來

  • 而後在 lib/scaffold中新建文件schema.js

  • 獲取meta配置項

const getMeta = require('./getMeta');

/**
 * 獲取元 Schema, 即模板選擇的 Schema
 *
 * @return {Object} 元 Schema
 */
exports.getMetaSchema = async function () {
    // 獲取整個配置文件 scaffold-config-dev.json
    let meta = await getMeta();
    ....
}
複製代碼
  • 咱們還須要去獲 scaffold-config-dev.json 中的 schema字段的內容因此咱們須要
  • 新建parseConfToSchema方法整理schema字段
/**
 * 把約定的 JSON CONF 內容解析成可自動化處理的 schema
 *
 * @param {Object}  conf 按照約定格式的配置 json 文件
 * @return {Object} schema
 */
function parseConfToSchema (conf = {}) {

    let properties = conf.schema || {};

    Object.keys(properties).forEach((key) => {
        let item = properties[key];

        if (item.type === 'list') {
            if (item.link && !item.dependence) {
                properties[key].list = conf[item.link];
            }
            else if (item.dependence) {
                properties[item.dependence].list.forEach((depItem) => {
                    if (depItem.value === conf.defaults[item.dependence]) {
                        properties[key].list = depItem.subList ?
                            (depItem.subList[key] || [])
                            : [];
                    }
                });
            }
        }
    });

    return properties;
}
複製代碼
  • 新建parseConfToSchema後,引用parseConfToSchema方法獲取schema字段裏的配置,而且把配置保存到store裏面緩存起來減小請求次數
/**
 * 獲取元 Schema, 即模板選擇的 Schema
 *
 * @return {Object} 元 Schema
 */
exports.getMetaSchema = async function () {
    // 獲取整個配置文件 scaffold-config-dev.json
    let meta = await getMeta();
    
    // 獲取配置文件 scaffold-config-dev.json 的 schema
    let metaSchema = parseConfToSchema(meta);

    store.set('metaSchema', metaSchema);

    return metaSchema;
}
複製代碼

5.到這來咱們已經完成了meta配置的獲取了,而後咱們須要把方法暴露給index.js進行代碼管理

index.js

const store = require('./store');

/**
 * 獲取元 Schema - 涉及模版下載的 Schema
 *
 * @return {Promise<*>}   Meta Schema
 */
exports.getMetaSchema = async function () {
    return store.get('metaSchema') || await Schema.getMetaSchema();
}

複製代碼
  • 讓咱們運行init看看輸出的內容
// 第一步:從雲端配置獲取 Meta 配置。肯定將要下載的框架和模板 lish
    let spinner = ora(locals.LOADING_FROM_CLOUD + '...');
    spinner.start();
    let metaSchema = await scaffold.getMetaSchema();
    spinner.stop();

    console.log(metaSchema)
複製代碼

以上就是獲取到的模板配置內容

第二步:等待用戶選擇將要下載的框架和模板

到了這一步是咱們須要讓用戶選擇哪一個模板的時候了

咱們須要有一個表單讓用戶去選擇

commander->scaffold->action.js

const formQ = require('./formQuestion');
    
    // 第二步:等待用戶選擇將要下載的框架和模板
    let metaParams = await formQ(metaSchema);
複製代碼

添加提示

locals->zh_CN->index.js

module.exports = {
  .....
  INPUT_INVALID: '輸入不符合規範',
  PLEASE_INPUT: '請輸入', 
  PLEASE_INPUT_NUM_DESC: '請選擇一個數字指定',
  PLEASE_INPUT_NUM: '請輸入數字',
  PLEASE_INPUT_RIGHR_NUM: '請輸入正確的數字',
  PLEASE_SELECT: '請選擇一個',
  PLEASE_SELECT_DESC: '按上下鍵選擇',
  .....
};

複製代碼

安裝須要的包

// 將node.js現代化爲當前ECMAScript規範
 "mz": "^2.7.0",
 // node fs 方法添加promise支持
 "fs-extra": "^4.0.1",
 // 常見的交互式命令行用戶界面的集合
 "inquirer": "^6.2.0",
複製代碼

1.首先新建formQuestion.js用於用戶表單選擇

  • 新建公共方法questionInputquestionYesOrNoquestionListquestionCheckboxPlusgetGitInfo
const exec = require('mz/child_process').exec;
const fs = require('fs-extra');
const os = require('os');
const inquirer = require('inquirer');
const path = require('path');

const locals = require('../../locals')();
const log = require('../../lib/utils/log');

'use strict';

/**
 * 獲取當前用戶的 git 帳號信息
 *
 * @return {Promise} promise 對象
 */
async function getGitInfo () {
    let author;
    let email;

    try {
        // 嘗試從 git 配置中獲取
        author = await exec('git config --get user.name');
        email = await exec('git config --get user.email');
    }
    catch (e) {
    }
    
    author = author && author[0] && author[0].toString().trim();
    email = email && email[0] && email[0].toString().trim();
    
    return { author, email };
}

/**
 * 詢問 input 類型的參數
 *
 * @param  {string} key    參數的 key
 * @param  {Object} schema schema 內容
 * @param  {Object} params 當前已有的參數
 * @return {Object}        question 須要的參數
 */
async function questionInput (key, schema, params) {
    let con = schema[key];
    let { name, invalidate } = con;
    let defaultVal = con.default;
    // 語言 locals - zh_CN
    let itemLocals = con.locals && con.locals[locals.LANG];

    if (itemLocals) {
        // locals - zh_CN - name
        name = itemLocals.name || name;
        // 模板類型
        defaultVal = itemLocals.default || defaultVal;
        invalidate = itemLocals.invalidate || invalidate;
    }

    con.validate = () => !!1;

    // 若是輸入項是 author 或者 email 的,嘗試去 git config 中拿默認內容
    if (key === 'author' || key === 'email') {
        let userInfo = await getGitInfo();
        defaultVal = userInfo[key] || con.default;
    }

    if (key === 'dirPath') {
        defaultVal = path.resolve(process.cwd(), con.default || '');
        con.validate = value => {
            let nowPath = path.resolve(process.cwd(), value || '');

            if (!fs.existsSync(nowPath)) {
                return invalidate || locals.INPUT_INVALID;
            }
            else {
            }

            return true;
        }
    }
    
    // 匹配輸入是否符合規範
    if (con.regExp) {
        let reg = new RegExp(con.regExp);

        con.validate = value => {
            if (!reg.test(value)) {
                return invalidate || locals.INPUT_INVALID;
            }
            return true;
        }
    }

    return {
        // 密碼
        'type': con.type === 'password' ? 'password' : 'input',
        'name': key,
        // 提示信息
        'message': `${locals.PLEASE_INPUT}${name}: `,
        // 默認值
        'default': defaultVal,
        // 驗證
        'validate': con.validate
    }
}

/**
 * 詢問 boolean 類型的參數
 *
 * @param  {string} key    參數的 key
 * @param  {Object} schema schema 內容
 * @param  {Object} params 當前已有的參數
 * @return {Object}        question 須要的參數
 */
async function questionYesOrNo (key, schema, params) {
    let con = schema[key];
    // 名稱
    let name = con.name;
    // 語言
    let itemLocals = con.locals && con.locals[locals.LANG];
    
    // 獲取相應語言的提示
    if (itemLocals) {
        name = itemLocals.name || name;
    }

    return {
        'type': 'confirm',
        'name': key,
        'default': false,
        'message': `${name}? :`
    }
}

/**
 * 詢問 list 類型的參數 (多選或者單選)
 *
 * @param  {string} key    參數的 key
 * @param  {Object} schema schema 內容
 * @param  {Object} params 當前已有的參數
 * @return {Object}        question 須要的參數
 */
function questionList (key, schema, params) {
    let con = schema[key];

    // 來源列表
    let sourceLish = [];
    // 選擇列表
    let choiceList = [];
    let text = '';
    let valueList = [];
    let listName = con.name;
    // 模板類型
    let listLocals = con.locals && con.locals[locals.LANG];
    
    // 獲取相應語言的提示
    if (listLocals) {
        listName = listLocals.name;
    }

    // 依賴
    if (!con.dependence) {
        sourceLish = con.list;
    }
    // 層級
    else if (con.depLevel > 0) {
        // 表示是級聯的操做
        let dependence = con.dependence;
        // 類型 template
        let ref = con.ref;
        let depList = schema[dependence].list;
        let depValue = params[dependence] || schema[dependence].list[0];

        depList.forEach((depItem) => {
            if (depItem.value === depValue) {
                sourceLish = (depItem.subList && depItem.subList[ref]);
            }
        });
    }

    sourceLish.forEach((item, index) => {
        let url = '';
        let { desc, name } = item;
        let itemLocals = item.locals && item.locals[locals.LANG];
        
        // 相應語言的提示
        if (itemLocals) {
            desc = itemLocals.desc || desc;
            name = itemLocals.name || name;
        }

        desc = log.chalk.gray('\n ' + desc);

        choiceList.push({
            value: item.value,
            name: `${name}${desc}${url}`,
            short: item.value
        });
        valueList.push(item.value);
        text += ''
            + log.chalk.blue('\n [' + log.chalk.yellow(index + 1) + '] ' + name)
            + desc;
    });

    // 若是是 windows 下的 git bash 環境,因爲沒有交互 GUI,因此就採用文本輸入的方式來解決
    if (os.platform() === 'win32' && process.env.ORIGINAL_PATH) {
        return {
            'type': 'input',
            'name': key,
            'message': locals.PLEASE_INPUT_NUM_DESC + ' ' + listName + ':' + text
                + '\n' + log.chalk.green('?') + ' ' + locals.PLEASE_INPUT_NUM + ':',
            'default': 1,
            'valueList': valueList,
            // 驗證
            'validate' () {
                if (!/\d+/.test(value) || +value > valueList.length || +value <= 0) {
                    return locals.PLEASE_INPUT_RIGHR_NUM;
                }
                return true;
            }
        };
    }

    return {
        'type': 'list',
        'name': key,
        'message': `${locals.PLEASE_SELECT}${listName} (${log.chalk.green(locals.PLEASE_SELECT_DESC)}):`,
        'choices': choiceList,
        'default': choiceList[0].value || '',
        'checked': !!con.checkbox,
        'pageSize': 1000
    }
}


/**
 * 詢問 checkbox-plus 類型的參數 (多選或者單選)
 *
 * @param  {string} key    參數的 key
 * @param  {Object} schema schema 內容
 * @param  {Object} params 當前已有的參數
 * @return {Object}        question 須要的參數
 */
function questionCheckboxPlus (key, schema, params) {
    let con = schema[key];

    // 來源列表
    let sourceLish = con.list;
    // 選擇列表
    let choiceList = [];

    sourceLish.forEach((item, index) => {
        let { name } = item;
        let itemLocals = item.locals && item.locals[locals.LANG];

        if (itemLocals) {
            name = itemLocals.name || name;
        }

        choiceList.push({
            value: item.value,
            name: name,

            checked: item.checked
        });

    });

    return {
        'type': con.type,
        'name': key,
        'message': con.name,
        'choices': choiceList
    }
}

複製代碼

2.公共方法新建完成後,讓咱們開始編寫表單代碼

/**
 * 解析schme, 生成 form 表單
 *
 * @param  {Object} schema  傳入的 schema 規則
 * @return {Object}         獲取的 form 參數
 */
module.exports = async function (schema) {
    let params = {};
    
    // 只有basic模板才能夠進行配置定製    
    if (schema.key) {
        let opts = {};
        let data = {};
        
        // 配置選擇,複選框
        opts = await questionCheckboxPlus(schema.key,
            {
                [schema.key]: schema
            }, params);
        
        // 輸出選擇的配置
        data = await inquirer.prompt([opts]).then(function (answers) {
            return {
                [schema.key]: answers[schema.key]
            };
        });

        params = Object.assign({}, params, data);

        return params
    }
    else {
        for (let key of Object.keys(schema)) {
            let con = schema[key];
            let type = con.type;
            let opts = {};
            let data = {};

            switch (type) {
                case 'string':
                case 'number':
                case 'password':
                    // 輸入密碼
                    opts = await questionInput(key, schema, params);
                    break;
                case 'boolean':
                    // 確認
                    opts = await questionYesOrNo(key, schema, params);
                    break;
                case 'list':
                    // 列表
                    opts = await questionList(key, schema, params);
                    break;
            }

            // 若是 list 只有一個 item 的時候,就不須要用戶選擇了,直接給定當前的值就行
            if (type === 'list' && con.list.length === 1) {
                data[key] = con.list[0].value;
            }
            else if (!con.disable && !con.key) {
                data = await inquirer.prompt([opts]);
                if (opts.valueList) {
                    data[key] = opts.valueList[+data[key] - 1];
                }
            }

            params = Object.assign({}, params, data);

        }
    }

    return params;
};

複製代碼

因爲咱們能夠對Basic模板進行配置的定製因此如今須要修改commander->scaffold->action.js裏面的代碼

const formQ = require('./formQuestion');
    
    // 第二步:等待用戶選擇將要下載的框架和模板
    let metaParams = await formQ(metaSchema);

    let checkboxParams;
    let cssParams;
    // 只有基礎模板才能夠自定義選項
    if (metaParams.template === 'Basic') {

        // 獲取用戶選擇的參數
        checkboxParams = await formQ(metaSchema.checkbox);

        // 是否選擇了css
        if (checkboxParams.checkbox.indexOf('css') > -1) {
            cssParams = await formQ(metaSchema.csssProcessors);
        }
    }
複製代碼

修改後如上

到了這裏再讓咱們運行init看看輸出的是什麼

選擇basic模板後返回可配置的選項

第三步:經過用戶選擇的框架和模板,下載模板

這一步是讓腳手架去下載用戶選擇的模板

添加提示

locals->zh_CN->index.js
module.exports = {
    ......
    META_TEMPLATE_ERROR: '獲取模版 Meta 信息出錯',
    DOWNLOAD_TEMPLATE_ERROR: '下載模版出錯,請檢查當前網絡',
    ......
};

複製代碼

安裝須要的包

"lodash": "^4.17.4",
"ajv": "^5.1.3",
"axios": "^0.17.1"
"compressing": "^1.3.1"
複製代碼

1.首先在lib->scaffold->index.js中新建download方法

/**
 * 經過指定的 meta 參數下載模版,下載成功後返回模板的 Schema 信息
 *
 * @param {Object} metaParams 導出參數
 * @return {*} 下載的臨時路徑 或者 報錯對象
 */
exports.download = async function (metaParams = {}) {
    
}

複製代碼

2.咱們先要有一個導出全部文件路徑的方法extendsDefaultFields去導出全部的文件

lib->scaffold->index.js

const store = require('./store');
const Schema = require('./schema');

const path = require('path');
const _ = require('lodash');

/**
 * 獲取導出的全部的 fields (包含 default 參數)
 *
 * @param  {Object} fields  傳入的 fields
 * @param  {Obejct} templateConf    模版的配置
 * @return {Object}         輸出的 fields
 */
async function extendsDefaultFields (fields = {}, templateConf = {}) {
    let defaultFields = {};
    let schema = store.get('schema') || await Schema.getSchema(templateConf)

    Object.keys(schema).forEach((key) => (defaultFields[key] = schema[key].default))

    /* eslint-disable fecs-use-computed-property */
    // defaultFields.name = fields.name || 'ivue-cli'
    defaultFields.name = fields.name || 'ivue-cli';

    defaultFields.dirPath = path.resolve(process.cwd(), fields.dirPath || '', defaultFields.name);

    return _.merge({}, defaultFields, fields);
}

複製代碼

3.而後咱們在schema.js中增長getSchema方法用於生成用戶輸入的表單

lib->scaffold->schema.js

/**
 * 獲取 Schema, 用於生成用戶輸入的表單
 *
 * @param {Object} templateConf 每一個模版的 config
 * @return {Object} 返回的 JSON Schema
 */
exports.getSchema = function (templateConf = {}) {
    return parseConfToSchema(templateConf);
}

複製代碼

4.而後回到lib->scaffold->index.js文件建立download方法下載成功後返回模板的 schema字段的信息

lib->scaffold->index.js

/**
 * 經過指定的 meta 參數下載模版,下載成功後返回模板的 Schema 信息
 *
 * @param {Object} metaParams 導出參數
 * @return {*} 下載的臨時路徑 或者 報錯對象
 */
exports.download = async function (metaParams = {}) {
   // 輸出導出路徑相關配置
    metaParams = await extendsDefaultFields(metaParams);
}

複製代碼

到了這裏咱們看看metaParams輸出的是什麼

如下輸出包含了咱們須要在哪裏建立文件的路徑,和模板名稱、類型

5.接下來咱們去建立真正的 download 方法下載一個指定的模版

  • 新建文件lib->scaffold->template.js

lib->scaffold->template.js

/**
 * 下載一個指定的模版
 *
 * @param  {Object} metaParams  導出模版所需字段, 從 mataSchema 中得出
 * @return {Objecy}             導出的結果
 */
exports.download = async function (metaParams = {}) {

}
複製代碼
  • 咱們先來獲取模板的信息,建立getTemplateInfo方法獲取模版信息

lib->scaffold->template.js

const getMeta = require('./getMeta');
const store = require('./store');

/**
 * 獲取模版信息
 *
 * @param  {Object} metaParam 元參數
 * @return {Object} framework 和 template 信息
 */
async function getTemplateInfo (metaParam) {
    try {
        // 獲取所有配置
        let meta = await getMeta();
        let frameworkValue = metaParam.framework || meta.defaults.framework || 'vue';
        let templateValue = metaParam.template || meta.defaults.template || 'template'

        // 對應的模板信息
        let framework = meta.frameworks.filter(item => item.value === frameworkValue)[0];
        // 倉庫地址等信息
        let template = framework.subList.template.filter(item => item.value === templateValue)[0];
        // 版本號
        let version = meta.version;


        store.set('framework', framework);
        store.set('template', template);
        store.set('version', version);

        return {
            framework,
            template,
            version
        };
    }
    catch (e) {
        // 若是這一步出錯了,只能說明是 BOS 上的 Meta 配置格式錯誤。。
        throw new Error(locals.META_TEMPLATE_ERROR);
    }
}

複製代碼
  • 而後咱們在download方法中添加getTemplateInfo 方法輸出模板詳細信息

lib->scaffold->template.js

/**
 * 下載一個指定的模版
 *
 * @param  {Object} metaParams  導出模版所需字段, 從 mataSchema 中得出
 * @return {Objecy}             導出的結果
 */
exports.download = async function (metaParams = {}) {
    let { framework, template, version } = await getTemplateInfo(metaParams);
}
複製代碼

接下來咱們來看看getTemplateInfo方法輸出的信息:

輸出包含了模板的倉庫地址,描述,等信息

6.接下來設置從雲端下載到本地的路徑

  • download方法中添加以下代碼

lib->scaffold->template.js

const conf = require('./config');
const path = require('path');

/**
 * 下載一個指定的模版
 *
 * @param  {Object} metaParams  導出模版所需字段, 從 mataSchema 中得出
 * @return {Objecy}             導出的結果
 */
exports.download = async function (metaParams = {}) {
    let { framework, template, version } = await getTemplateInfo(metaParams);
    
    // 下載到本地的路徑
    let storeDir = path.resolve(
        conf.LOCAL_TEMPLATES_DIR,
        framework.value, template.value + '_' + version
    )
}

複製代碼
  • 其中conf.LOCAL_TEMPLATES_DIR 爲本地模版存放路徑

lib->scaffold->config.js

const path = require('path');
const utils = require('../utils');

module.exports = {
    /**
     * 本地模版存放路徑
     *
     * @type {String}
     */
    LOCAL_TEMPLATES_DIR: path.resolve(utils.getHome(), 'tmp'),
    .....
}
複製代碼
  • utils.getHome() 爲獲取雲端倉庫的跟目錄

lib->utils->index.js

const os = require('os');
const path = require('path');
const fs = require('fs-extra');

/**
 * 獲取項目根目錄
 *
 * @return {string} 目錄 Path
 */
exports.getHome = function () {
    let dir = process.env[
        os.platform() === 'win32'
            ? 'APPDATA'
            : 'HOME'
    ] + path.sep + '.ivue-project'

    // 若是這個目錄不存在,則建立這個目錄
    !fs.existsSync(dir) && fs.mkdirSync(dir);

    return dir;
};
複製代碼

7.接下來咱們須要去驗證用戶的輸入是否與配置中的驗證規則相匹配

  • schema.js 中添加getMetaJsonSchema方法

lib->scaffold->schema.js

/**
 * 獲取 meta JSON Schema, 用於驗證 json 表單
 *
 * @return {Object} 返回的 JSON Schema
 */
exports.getMetaJsonSchema = async function () {
    let meta = await getMeta();
    let metaSchema = parseConfToSchema(meta);

    store.set('metaSchema', metaSchema);

    return metaSchema;
}
複製代碼
  • 而後咱們回到download方法中添加驗證用戶輸入

lib->scaffold->template.js

/**
 * 下載一個指定的模版
 *
 * @param  {Object} metaParams  導出模版所需字段, 從 mataSchema 中得出
 * @return {Objecy}             導出的結果
 */
exports.download = async function (metaParams = {}) {
    let { framework, template, version } = await getTemplateInfo(metaParams);

    let storeDir = path.resolve(
        conf.LOCAL_TEMPLATES_DIR,
        framework.value, template.value + '_' + version
    )
    
    // 驗證是不是json字符串
    let ajv = new Ajv({ allErrors: true });
    let metaJsonSchema = store.get('metaJsonSchema') || await schema.getMetaJsonSchema();
    
    // 驗證用戶輸入   
    let validate = ajv.compile(metaJsonSchema);
    let valid = validate(metaParams);
    
    if (!valid) {
        throw new Error(JSON.stringify(validate.errors));
    }
}
複製代碼

8.驗證經過後咱們開始從服務器上拉取模板

  • 新增downloadTemplateFromCloud方法用於下載模板,從服務器上拉取模版

lib->scaffold->template.js

/**
 * 經過指定框架名和模版名從服務器上拉取模版(要求在模版 relase 的時候注意上傳的 CDN 路徑)
 *
 * @param {string} framework 框架名稱
 * @param {string} template 模版名稱
 * @param {string} targetPath 模版下載後存放路徑
 */

async function downloadTemplateFromCloud (framework, template, targetPath) {
    const outputFilename = path.resolve(targetPath, 'template.zip');

    // existsSync:  若是路徑存在,則返回 true,不然返回 false。
    // removeSync 刪除文件、目錄
    fs.existsSync(targetPath) && fs.removeSync(targetPath);
    // 確保目錄存在。若是目錄結構不存在,則建立它
    fs.mkdirsSync(targetPath);

    framework = (framework || 'vue').toLowerCase();
    template = (template || 'basic').toLowerCase().replace(/\s/, '-');

    try {
        // 請求模板
        let result = await axios.request({
            responseType: 'arraybuffer',
            url: 'https://codeload.github.com/qq282126990/webpack/zip/release-' + template,
            method: 'get',
            headers: {
                'Content-Type': 'application/zip'
            }
        });

        fs.writeFileSync(outputFilename, result.data);

        // 解壓縮是反響過程,接口都統一爲 uncompress
        await compressing.zip.uncompress(outputFilename, targetPath);
        fs.removeSync(outputFilename);
    }
    catch (e) {
        throw new Error(locals.DOWNLOAD_TEMPLATE_ERROR);
    }

複製代碼
  • 而後咱們回到download方法,添加方法downloadTemplateFromCloud經過指定框架名和模版名從服務器上拉取模版,模板內容將下載到storeDir路徑下

lib->scaffold->template.js

......
//  經過指定框架名和模版名從服務器上拉取模版
await downloadTemplateFromCloud(framework.value, template.value, storeDir);
.....

複製代碼

下載後如圖:

9.接下來咱們去獲取模板下載後的meta.json文件

download方法中添加代碼,獲取文件夾名稱以及下載後的meta.json的內容

lib->scaffold->template.js

......
// 獲取文件夾名稱
const files = fs.readdirSync(storeDir);

store.set('storeDir', `${storeDir}/${files}`);

let templateConfigContent = fs.readFileSync(path.resolve(`${storeDir}/${files}`, 'meta.json'), 'utf-8');

let templateConfig = JSON.parse(templateConfigContent);

store.set('templateConfig', templateConfig);

return templateConfig;
......

複製代碼

10.而後咱們須要吧代碼統一管理到index.js

lib->scaffold->index.js

const template = require('./template');

/**
 * 經過指定的 meta 參數下載模版,下載成功後返回模板的 Schema 信息
 *
 * @param {Object} metaParams 導出參數
 * @return {*} 下載的臨時路徑 或者 報錯對象
 */
exports.download = async function (metaParams = {}) {
    metaParams = await extendsDefaultFields(metaParams);

    return await template.download(metaParams);
}
複製代碼

11.最後咱們導出download方法

commander->scaffold->action.js

  • 新增如下代碼
// 第三步:經過用戶選擇的框架和模板,下載模板
spinner.start();
let templateConf = await scaffold.download(metaParams, checkboxParams);
spinner.stop();

複製代碼

12.因爲咱們能夠對基礎模本進行自定義選項因此還須要增長如下代碼來下載對應的選項配置

  • 新增包
// ETPL是一個強複用,靈活,高性能的JavaScript的模板引擎,適用於瀏覽器端或節點環境中視圖的生成
"etpl": "^3.2.0"
複製代碼
  • lib->scaffold->index.js中新增方法setMainJs設置webpack模板的main.js文件,setCheckboxParams經過指定的參數渲染下載成功的模板,setCssParams配置css

lib->scaffold->index.js

因爲customize文件夾內容過多此處不進行展現,詳情能夠查看這裏

const etpl = require('etpl');
const fs = require('fs-extra');
const path = require('path');

// 設置router 配置
const routerConfig = require('../../../../customize/router');
// 設置 vuex 配置
const vuexConfig = require('../../../../customize/vuex');
// 設置 typescriptConfig 配置
const typescriptConfig = require('../../../../customize/typescript');

/**
 * main.js
 *
 * @param {String} storeDir 文件根目錄
 * @param {String} currentDir 當前文件目錄
 * @param {Function} etplCompile 字符串轉換
 * @param {Array} params 須要設置的參數
 */
function setMainJs (storeDir, currentDir, etplCompile, params) {

    // 模塊
    let nodeModules = '';
    // 路徑列表
    let urls = '';
    // 配置
    let configs = '';
    // 名字列表
    let names = '';


    params.forEach((key) => {
        // 插入路由配置
        if (key === 'router') {
            nodeModules += `${nodeModules.length === 0 ? '' : '\n'}import VueRouter from 'vue-router'${nodeModules.length === 0 ? '\n' : ''}`;

            urls += `${urls.length === 0 ? '' : '\n'}import router from './router'`;

            configs += `\nVue.use(VueRouter)`;

            names += `${names.length === 0 ? '' : '\n'}    router,`;
        }

        // 插入vuex配置
        if (key === 'vuex') {
            urls += `${urls.length === 0 ? '' : '\n'}import store from './store'`;

            names += `${names.length === 0 ? '' : '\n'}    store,`;
        }
    });

    // main.js
    let mainJs =
        `import Vue from 'vue'
${nodeModules}
import App from './App.vue'
${urls}${urls.length > 0 ? '\n' : ''}
import IvueMaterial from 'ivue-material'
import 'ivue-material/dist/styles/ivue.css'
${configs}
Vue.use(IvueMaterial)

Vue.config.productionTip = false

new Vue({
${names}${names.length > 0 ? '\n' : ''}    render: h => h(App),
}).$mount('#app')
`;

    mainJs = etplCompile.compile(mainJs)();

    let name
    if (params.indexOf('typescript') > -1) {
        name = 'main.ts';
    }
    else {
        name = 'main.js';
    }

    // 從新寫入文件
    fs.writeFileSync(path.resolve(`${storeDir}/src`, name), mainJs);
}

/**
 * 經過指定的參數渲染下載成功的模板
 *
 * @param {Array} params 須要設置的參數
 */
exports.setCheckboxParams = async function (params = []) {
    const storeDir = store.get('storeDir');
    const templateConfig = store.get('templateConfig');
    const etplCompile = new etpl.Engine(templateConfig.etpl);
    const currentDir = './packages/customize/router/code'

    params.forEach((key) => {
        // 插入路由配置
        if (key === 'router') {
            routerConfig.setFile(storeDir, etplCompile,params);
        }

        // 插入 vuex 配置
        if (key === 'vuex') {
            vuexConfig.setFile(storeDir, etplCompile, params);
        }

        // 插入 typescript 配置
        if (key === 'typescript') {
            typescriptConfig.setFile(storeDir, etplCompile);
        }
    });

    // 修改 main.js
    setMainJs(storeDir, currentDir, etplCompile, params);

    // 設置 shims-vue.d.ts
    if (params.indexOf('typescript') > -1) {
        setShimsVueDTs(storeDir, currentDir, etplCompile, params);
    }
}


/**
 * 配置css參數
 *
 * @param {Array} params 須要設置的參數
 */
exports.setCssParams = async function (params = '') {
    const storeDir = store.get('storeDir');
    const templateConfig = store.get('templateConfig');
    const etplCompile = new etpl.Engine(templateConfig.etpl);

    let nodeModules = {};

    // scss
    if (params === 'scss') {
        nodeModules = {
            'node-sass': '^4.12.0',
            'sass-loader': '^7.2.0'
        };
    }
    // less
    else if (params === 'less') {
        nodeModules = {
            'less': '^3.0.4',
            'less-loader': '^7.2.0'
        };
    }
    // stylus
    else if (params === 'stylus') {
        nodeModules = {
            'stylus': '^0.54.5',
            'stylus-loader': '^3.0.2'
        };
    }

    // 設置css版本號
    setCssPackConfig(storeDir, etplCompile, nodeModules);
}
複製代碼
  • 而後在 commander->scaffold->action.js 中添加以下代碼:

commander->scaffold->action.js

module.exports = async function (conf) {
    ......
    // 設置用戶選擇的參數
    // 只有基礎模板才能夠自定義選項
    if (metaParams.template === 'Basic') {
        await scaffold.setCheckboxParams(checkboxParams.checkbox);
    
        // 是否選擇了css
        if (cssParams) {
            await scaffold.setCssParams(cssParams.csssProcessors);
        }
    }
    ......
}
複製代碼

第四步:根據下載的模板的 meta.json 獲取當前模板所須要用戶輸入的字段 schema

模板下載完成後咱們須要用戶對模板的字段進行設置,如設置做者名稱、做者郵箱、項目描述、項目名稱。因此咱們須要獲取模板中的meta.json 文件。知道那些字段是須要用戶去設置的。

1.新增getSchema方法,獲取meta.json配置

lib->scaffold->index.js

/**
 * 獲取 Schema - 涉及模版渲染的 Schema
 *
 * @param {Object} templateConf 模版本身的配置
 * @return {Promise<*>}   Schema
 */
exports.getSchema = async function (templateConf = {}) {
    if (!templateConf) {
        // 若是實在沒有提早下載模板,就現用默認的參數下載一個
        templateConf = await Schema.download();
    }
    return Schema.getSchema(templateConf);
}
複製代碼

2.而後在 commander->scaffold->action.js 添加一下代碼,輸出獲取到的配置

commander->scaffold->action.js

module.exports = async function (conf) {
    ......
    // 第四步:根據下載的模板的 meta.json 獲取當前模板所須要用戶輸入的字段 schema
    let schema = await scaffold.getSchema(templateConf);
    ......
}
複製代碼

第五步:等待用戶輸入 schema 所預設的字段信息

到了這裏咱們須要用上一步的配置去讓用戶進行輸入

commander->scaffold->action.js

// 第五步:等待用戶輸入 schema 所預設的字段信息
let params = await formQ(schema);
複製代碼

咱們再次運行init看看輸出的是什麼

如上輸出使用戶能夠自定義本身的package.json

輸入完成後如圖:

第六步:渲染模板,並導出到指定的文件夾(當前文件夾)

新增提示

LOADING_EXPORT_PROJECT: '正在導出工程',
INIT_SUCCESS: '項目已建立成功',
INIT_NEXT_GUIDE: '您能夠操做以下命令快速開始開發工程',
RENDER_TEMPLATE_ERROR: '模板渲染出錯',
複製代碼

安裝包

"glob": "^7.1.2",
"archiver": "^1.3.0"
複製代碼

config.js 新增配置

/**
* 渲染模版時默認忽略的文件或文件夾
*
* @type {Arrag<string>}
*/
DEFAULT_RENDER_IGNORES: [
    'node_modules',
    '**/*.tmp', '**/*.log',
    '**/*.png', '**/*.jpg', '**/*.jpeg', '**/*.bmp', '**/*.gif', '**/*.ico',
    '**/*.svg', '**/*.woff', '**/*.ttf', '**/*.woff2'
],
/**
 * 默認的 etpl 配置
 *
 * @type {Object}
 */
ETPL: {
    commandOpen: '{%',
    commandClose: '%}',
    variableOpen: '*__',
    variableClose: '__*'
},
 /**
 * render common data 渲染時間
 *
 * @type {Object}
 */
COMMON_DATA: {
    year: (new Date()).getFullYear(),
    time: Date.now()
},
/**
 * 導出時默認忽略的文件或文件夾
 *
 * @type {Array<string>}
 */
DEFAULT_EXPORTS_IGNORES: [
    '.git',
    'meta.js',
    'meta.json'
],
複製代碼

1.咱們先在lib->scaffold->template.js中新增方法renderTemplate渲染template 裏面的全部文件

lib->scaffold->template.js

const conf = require('./config');
// ETPL是一個強複用,靈活,高性能的JavaScript的模板引擎,適用於瀏覽器端或節點環境中視圖的生成
const etpl = require('etpl');
// Match files using the patterns the shell uses, like stars and stuff.
const glob = require('glob');
// 用於存檔生成的流式界面
const archiver = require('archiver');
const fs = require('fs-extra');

/**
 * 渲染 template 裏面的全部文件
 *
 * @param  {Object} params    收集的用戶輸入字段
 * @param  {string} tmpStoreDir  臨時文件夾存儲路徑
 * @return {Promise}          渲染 promise
 */
function renderTemplate (params, tmpStoreDir) {
    let templateConfig = store.get('templateConfig');
    let dirPath = params.dirPath || process.cwd();
    // 模板文件渲染
    let etplCompile = new etpl.Engine(templateConfig.etpl || conf.ETPL);

    // 把指定的開發者不須要的文件和文件夾都刪掉
    deleteFilter(tmpStoreDir, templateConfig.exportsIgnores);

    return new Promise((resolve, reject) => glob(
        '**/*',
        {
            // 要搜索的當前工做目錄
            cwd: tmpStoreDir,
            // 添加模式或glob模式數組以排除匹配。注意:不管其餘設置如何,ignore模式始終處於dot:true模式狀態。
            ignore: (templateConfig.renderIgnores || []).concat(...conf.DEFAULT_RENDER_IGNORES)
        },
        (err, files) => {
            files.forEach((file) => {
                // 文件路徑
                let filePath = path.resolve(tmpStoreDir, file);
                // 對象提供有關文件的信息。
                // 若是 fs.Stats 對象描述常規文件,則返回 trueif (fs.statSync(filePath).isFile()) {
                    let content = fs.readFileSync(filePath, 'utf8');

                    // 這裏能夠直接經過外界配置的規則,從新計算出一份數據,只要和 template 裏面的字段對應上就行了
                    let extDataTpls = templateConfig.extData || {};
                    let extData = {};
                    let commonData = conf.COMMON_DATA;

                    Object.keys(extDataTpls).forEach((key) => {
                        extData[key] = etplCompile.compile(`${extDataTpls[key]}`)(params);
                    });

                    let renderData = Object.assign({}, params, extData, commonData);
                    
                    let afterCon = etplCompile.compile(content)(renderData);

                    fs.writeFileSync(filePath, afterCon);
                }
            });

            // addPackageJson(tmpStoreDir, params);

            if (params.isStream) {
                //  設置壓縮級別
                let archive = archiver('zip', { zlib: { level: 9 } });
                let tmpZipPath = path.resolve(tmpStoreDir, '..', 'zip');
                // 建立一個文件以將歸檔數據流式傳輸到。
                let output = fs.createWriteStream(tmpZipPath);

                // 將 歸檔數據管道傳輸到文件
                archiver.pipe(output);
                // 從子目錄追加文件並在歸檔中命名爲  params.name
                archive.directory(tmpStoreDir, params.name);
                //  完成歸檔(即咱們已完成附加文件,但流必須完成)
                //  'close''end''finish'可能在調用此方法後當即觸發,所以請事先註冊
                archive.finalize().on('finish', () => resolve(fs.createReadStream(tmpZipPath)));
            }
            else {
                fs.copySync(tmpStoreDir, dirPath);
                resolve(dirPath);
            }
        }
    ));
}


/**
 * 刪除某個目錄中的指定文件或文件夾
 *
 * @param {string} dir 根目錄
 * @param {*} ignores 過濾的文件或文件夾數組
 * @param {*} checkboxParams 須要插入的文件
 */
function deleteFilter (dir, ignores = [], checkboxFile) {
    ignores.concat(...conf.DEFAULT_EXPORTS_IGNORES).forEach((target) => {
        let targetPath = path.resolve(dir, target);
        // 若是路徑存在,則返回 true,不然返回 false。

        //  刪除文件
        fs.existsSync(targetPath) && fs.removeSync(targetPath);
    })
}
複製代碼

2.而後在lib->scaffold->template.js中新增方法render渲染指定的模板模版

lib->scaffold->template.js

const locals = require('../../locals')();

/**
 * 渲染指定的模板模版
 *
 * @param {Object} params 收集到的用戶輸入的參數
 * @return {*} 導出的結果
 */
exports.render = async function (params) {
    // 模板配置
    let templateConfig = store.get('templateConfig') || await this.download(params);
    // 模板路徑
    let tmpStoreDir = path.resolve(conf.LOCAL_TEMPLATES_DIR, `${Date.now()}`);
    let storeDir = store.get('storeDir');
    // 驗證json
    let ajv = new Ajv({ allErrors: true });

    let jsonSchema = schema.getMetaJsonSchema(templateConfig);
    jsonSchema.then(async (res) => {
        let validate = ajv.compile(res);

        let valid = validate(params);

        if (!valid) {
            throw new Error(JSON.stringify(validate.errors));
        }

        try {
            // 若是路徑存在,則返回 true,不然返回 false
            if (!fs.existsSync(storeDir)) {
                await this.download(params);
            }
            else {
            }

            // 將建立的目錄路徑
            fs.mkdirSync(tmpStoreDir);

            // 拷貝文件
            fs.copySync(storeDir, tmpStoreDir);

            //  渲染 template 裏面的全部文件
            let renderResult = await renderTemplate(params, tmpStoreDir);

            // 刪除文件
            fs.removeSync(tmpStoreDir);

            return renderResult;
        }
        catch (e) {
            throw new Error(locals.RENDER_TEMPLATE_ERROR);
        }
    });
}
複製代碼

3.而後在> lib->scaffold->index.js中管理templaterender方法

  • 新增extendsDefaultFields方法用於導出全部的模板文件

lib->scaffold->index.js

/**
 * 獲取導出的全部的 fields (包含 default 參數)
 *
 * @param  {Object} fields  傳入的 fields
 * @param  {Obejct} templateConf    模版的配置
 * @return {Object}         輸出的 fields
 */
async function extendsDefaultFields (fields = {}, templateConf = {}) {
    let defaultFields = {};
    let schema = store.get('schema') || await Schema.getSchema(templateConf)

    Object.keys(schema).forEach((key) => (defaultFields[key] = schema[key].default))

    /* eslint-disable fecs-use-computed-property */
    // defaultFields.name = fields.name || 'ivue-cli'
    defaultFields.name = fields.name || 'ivue-cli';

    defaultFields.dirPath = path.resolve(process.cwd(), fields.dirPath || '', defaultFields.name);

    return _.merge({}, defaultFields, fields);
}

複製代碼
  • 而後新增render方法用於經過指定的參數渲染下載成功的模板
/**
 * 經過指定的參數渲染下載成功的模板
 *
 * @param {Object} params 導出參數
 * @param {Object} templateConf 模版的配置
 * @return {Promise<*>}   導出的結果
 */
exports.render = async function (params = {}, templateConf) {
    if (!templateConf) {
        // 若是實在沒有提早下載模板,就現用默認的參數下載一個(這個模板是默認的)
        templateConf = await Schema.download();
    }
    
    // 獲取導出的全部的 fields
    params = await extendsDefaultFields(params, templateConf);

    return await template.render(params);
}
複製代碼

4.咱們再回到commander->scaffold->action.js對導出的文件進行邏輯判斷

  • 新增exportProject方法輸出模板內容

commander->scaffold->action.js

/**
 * 輸出項目
 *
 * @param  {Object} params 輸出項目的參數
 * @param  {Object} templateConf  項目的配置內容
 * @param  {Object} checkboxParams  選框選擇選項
 */
async function exportProject (params, templateConf, checkboxParams) {
    let spinner = ora(locals.LOADING_EXPORT_PROJECT + '...');

    spinner.start();
    await scaffold.render(params, templateConf, checkboxParams);
    spinner.stop();

    console.log(params)

    // for log beautify
    console.log('');
    log.info(locals.INIT_SUCCESS);
    log.info(locals.INIT_NEXT_GUIDE + ':\n\n'
        + log.chalk.green('cd ' + params.name + '\n'
            + 'npm install\n'
            + 'npm run serve'
        ));
    try {
        await axios('https://lavas.baidu.com/api/logger/send?action=cli&commander=init');
    }
    catch (e) { }
}
複製代碼
  • 而後咱們在調用exportProject方法導出全部的文件
// 當前執行node命令時候的文件夾地址
let cwd = process.cwd();

module.exports = async function (conf) {
    .....
    // 第六步:渲染模板,並導出到指定的文件夾(當前文件夾)
    let projectTargetPath = path.resolve(params.dirPath || cwd, params.name);
    params = Object.assign({}, metaParams, params);
    
    // 測試某個路徑下的文件是否存在
    let isPathExist = await fs.pathExists(projectTargetPath);
    if (isPathExist) {
        // 錯誤提示項目已存在,避免覆蓋原有項目
        console.log(symbols.error, chalk.red('項目已存在'));
        return;
    }
    else {
        // 導出文件
        await exportProject(params, templateConf, checkboxParams);
    }
    .....
}
複製代碼

以上也正是 ivue-cli 腳手架的所有源碼。

到了這裏也算的講解完成了,若是須要更相信的代碼同窗們能夠自行查看倉庫源碼去閱讀,全部源碼已標註了註釋。

最後

新的一年終於吧這篇文章寫完了,算是膩補上一年的遺憾~~😂😂😂

因爲篇幅有點長,若有錯誤歡迎提出 issues 或者 star

🦆🦆🦆🦆🦆🦆🦆🦆🦆🦆🦆🦆🦆🦆🦆

新的一年也要加油呀💪💪💪💪💪💪

相關文章
相關標籤/搜索