腳手架源碼 ivue-clicss
模板配置 webpack前端
繼續上一篇文章的講解,讓咱們繼續來看如何實現 init 功能。(如您想閱讀上一篇內容能夠點擊這裏)vue
讓咱們先新建一個腳手架的配置文件scaffold-config-dev.json
node
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
命令的代碼實現首先檢查當前網絡環境, 建立檢查網絡環境方法isNetworkConnect
vue-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個步驟
locals->zh_CN->index.js
module.exports = {
.....
LOADING_FROM_CLOUD: '正在拉取雲端數據,請稍候',
.....
};
複製代碼
// 下載中動畫效果
"ora": "^1.3.0"
複製代碼
使用後效果如圖:
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();
複製代碼
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();
....
}
複製代碼
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;
}
複製代碼
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",
複製代碼
formQuestion.js
用於用戶表單選擇questionInput
、questionYesOrNo
、questionList
、questionCheckboxPlus
、getGitInfo
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
}
}
複製代碼
/**
* 解析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
模板後返回可配置的選項
這一步是讓腳手架去下載用戶選擇的模板
添加提示
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"
複製代碼
lib->scaffold->index.js
中新建download
方法/**
* 經過指定的 meta 參數下載模版,下載成功後返回模板的 Schema 信息
*
* @param {Object} metaParams 導出參數
* @return {*} 下載的臨時路徑 或者 報錯對象
*/
exports.download = async function (metaParams = {}) {
}
複製代碼
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);
}
複製代碼
schema.js
中增長getSchema
方法用於生成用戶輸入的表單lib->scaffold->schema.js
/**
* 獲取 Schema, 用於生成用戶輸入的表單
*
* @param {Object} templateConf 每一個模版的 config
* @return {Object} 返回的 JSON Schema
*/
exports.getSchema = function (templateConf = {}) {
return parseConfToSchema(templateConf);
}
複製代碼
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
輸出的是什麼
如下輸出包含了咱們須要在哪裏建立文件的路徑,和模板名稱、類型
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
方法輸出的信息:
輸出包含了模板的倉庫地址,描述,等信息
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;
};
複製代碼
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));
}
}
複製代碼
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);
.....
複製代碼
下載後如圖:
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;
......
複製代碼
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);
}
複製代碼
download
方法commander->scaffold->action.js
// 第三步:經過用戶選擇的框架和模板,下載模板
spinner.start();
let templateConf = await scaffold.download(metaParams, checkboxParams);
spinner.stop();
複製代碼
// 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
module.exports = async function (conf) {
......
// 設置用戶選擇的參數
// 只有基礎模板才能夠自定義選項
if (metaParams.template === 'Basic') {
await scaffold.setCheckboxParams(checkboxParams.checkbox);
// 是否選擇了css
if (cssParams) {
await scaffold.setCssParams(cssParams.csssProcessors);
}
}
......
}
複製代碼
模板下載完成後咱們須要用戶對模板的字段進行設置,如設置做者名稱、做者郵箱、項目描述、項目名稱。因此咱們須要獲取模板中的meta.json 文件。知道那些字段是須要用戶去設置的。
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);
}
複製代碼
commander->scaffold->action.js
添加一下代碼,輸出獲取到的配置commander->scaffold->action.js
module.exports = async function (conf) {
......
// 第四步:根據下載的模板的 meta.json 獲取當前模板所須要用戶輸入的字段 schema
let schema = await scaffold.getSchema(templateConf);
......
}
複製代碼
到了這裏咱們須要用上一步的配置去讓用戶進行輸入
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'
],
複製代碼
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 對象描述常規文件,則返回 true。
if (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);
})
}
複製代碼
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);
}
});
}
複製代碼
> lib->scaffold->index.js
中管理template
的render
方法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);
}
複製代碼
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
。
🦆🦆🦆🦆🦆🦆🦆🦆🦆🦆🦆🦆🦆🦆🦆
新的一年也要加油呀💪💪💪💪💪💪