新的一年新的开始
github仓库地址
脚手架源码 ivue-cli
模板配置 webpack
开始
继续上一篇文章的讲解,让我们继续来看如何实现 init 功能。(如您想阅读上一篇内容可以点击这里)
新建一个脚手架的配置文件scaffold-config-dev.json
lib->scaffold->templates->scaffold-config-dev.json
{
"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"
}
}
}
创建 init
命令
commander
下新建文件在该目录下管理主逻辑代码
commander->scaffold->index.js
'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 文件中添加提示
module.exports = {
.....
INIT_DESC: '初始化 ivue-cli 项目',
INIT_OPTION_FORCE: '是否覆盖已有项目',
.....
};
以上创建了
init
命令的运行
init
命令的代码实现
首先检查当前网络环境, 创建检查网络环境方法
isNetworkConnect
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');
.....
}
当没有网络时会输出以下内容:
![37cd8b0c013c466e514da8d3a2666403.png](https://i-blog.csdnimg.cn/blog_migrate/c244e1b11e3672dfe54001a6ea79cca7.png)
否则输出如下内容:
![33f1203986f7a95237eb5721607fbc99.png](https://i-blog.csdnimg.cn/blog_migrate/6914c9cec0ab3eb3b4d0fc484904068a.png)
初始化过程的6个步骤
现在开始让我们来看看初始化过程的 初始化过程的6个步骤
第一步:从云端配置获取 Meta 配置。确定将要下载的框架和模板 lish
1.添加提示 locals->zh_CN->index.js
module.exports = {
.....
LOADING_FROM_CLOUD: '正在拉取云端数据,请稍候',
.....
};
2.安装包
// 下载中动画效果
"ora": "^1.3.0"
使用后效果如图:
![cc847ead5821ed6fc95364f27104211c.png](https://i-blog.csdnimg.cn/blog_migrate/8e489846f43e8319e10ebd310553b45a.jpeg)
3.引用下载配置的方法getMetaSchema
,下载完成后调用spinner.stop()
停止下载中效果 commander->scaffold->action.js
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
},
}
获取meta
配置,新建getMeta.js
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)
![63dc42789de02213d3f9dada869764ca.png](https://i-blog.csdnimg.cn/blog_migrate/9a41d98f916e9aecb17f7258eb1b9e59.jpeg)
以上就是获取到的模板配置内容
第二步:等待用户选择将要下载的框架和模板
到了这一步是我们需要让用户选择哪个模板的时候了
我们需要有一个表单让用户去选择 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
用于用户表单选择
新建公共方法questionInput
、questionYesOrNo
、questionList
、questionCheckboxPlus
、getGitInfo
commander->scaffold->formQuestion.js
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.公共方法新建完成后,让我们开始编写表单代码 commander->scaffold->formQuestion.js
/**
* 解析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
看看输出的是什么
![4992026bcba0578ec0ba4b7f750006a0.png](https://i-blog.csdnimg.cn/blog_migrate/137ca40e68e3becd307c032c6c182ecf.jpeg)
选择basic
模板后返回可配置的选项
![a62190f091d68372a1cc143372e40fe9.png](https://i-blog.csdnimg.cn/blog_migrate/2d5fc67350b7f569ffd3cc4e4da112b7.jpeg)
第三步:通过用户选择的框架和模板,下载模板
这一步是让脚手架去下载用户选择的模板
添加提示 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.新建download
方法,返回模板的Schema
信息 lib->scaffold->index.js
/**
* 通过指定的 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
输出的是什么
以下输出包含了我们需要在哪里创建文件的路径,和模板名称、类型
![461e3f85915b6cbb019adebe2917b878.png](https://i-blog.csdnimg.cn/blog_migrate/ce414a40cbc15d0921dcccb43b8f05f9.png)
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
方法输出的信息:
输出包含了模板的仓库地址,描述,等信息
![c4f11eaa2f1697e8553e33fed1f92037.png](https://i-blog.csdnimg.cn/blog_migrate/fee065eb83aae46b8be4457df7d2fac9.jpeg)
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);
.....
下载后如图:
![c1a117bc5bdd5654e3dc34a700bc16e5.png](https://i-blog.csdnimg.cn/blog_migrate/12827153fd471326ea0a31958f772258.jpeg)
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
包
由于customize
文件夹内容过多此处不进行展示,详情可以查看这里 lib->scaffold->index.js
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
看看输出的是什么
![e631bb70f6b3d8f27d60f4e65ee92c59.png](https://i-blog.csdnimg.cn/blog_migrate/bad09124d0073545910a9fcba9936565.jpeg)
如上输出使用户可以自定义自己的package.json
输入完成后如图:
![9434b72a24b03037c6b5c1e4098a84f5.png](https://i-blog.csdnimg.cn/blog_migrate/9690b5571d1750461ab4428eb5db4fef.jpeg)
由于字数过多,最后一步,请看下一章
如有错误欢迎提出issues
或者star
。
最后
新的一年也要加油呀
![d800e350b2fce7b09d77db0964473a48.png](https://i-blog.csdnimg.cn/blog_migrate/b758d96fea35d2e6f5977255e993aec6.jpeg)