脚手架源码 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
。
🦆🦆🦆🦆🦆🦆🦆🦆🦆🦆🦆🦆🦆🦆🦆
新的一年也要加油呀💪💪💪💪💪💪