背景
在平时工作中会有遇到许多以相同模板定制的小程序,因此想自己建立一个生成模板的脚手架工具,以模板为基础构建对应的小程序,而平时的小程序都是用mpvue框架来写的,因此首先先参考一下Vue-cli的原理。知道原理之后,再定制自己的模板脚手架肯定是事半功倍的。
在说代码之前我们首先回顾一下Vue-cli的使用,我们通常使用的是webpack模板包,输入的是以下代码。
vue init webpack [project-name]复制代码
在执行这段代码之后,系统会自动下载模板包,随后会询问我们一些问题,比如模板名称,作者,是否需要使用eslint,使用npm或者yarn进行构建等等,当所有问题我们回答之后,就开始生成脚手架项目。
我们将源码下来,源码仓库点击这里,平时用的脚手架还是2.0版本,要注意,默认的分支是在dev上,dev上是3.0版本。
我们首先看一下package.json,在文件当中有这么一段话
{ "bin": { "vue": "bin/vue", "vue-init": "bin/vue-init", "vue-list": "bin/vue-list" } } 复制代码
由此可见,我们使用的命令 vue init,应该是来自bin/vue-init这个文件,我们接下来看一下这个文件中的内容
bin/vue-init
const download = require('download-git-repo')const program = require('commander')const exists = require('fs').existsSyncconst path = require('path')const ora = require('ora')const home = require('user-home')const tildify = require('tildify')const chalk = require('chalk')const inquirer = require('inquirer')const rm = require('rimraf').syncconst logger = require('../lib/logger')const generate = require('../lib/generate')const checkVersion = require('../lib/check-version')const warnings = require('../lib/warnings')const localPath = require('../lib/local-path') 复制代码
download-git-repo 一个用于下载git仓库的项目的模块 commander 可以将文字输出到终端当中 fs 是node的文件读写的模块 path 模块提供了一些工具函数,用于处理文件与目录的路径 ora 这个模块用于在终端里有显示载入动画 user-home 获取用户主目录的路径 tildify 将绝对路径转换为波形路径 比如/Users/sindresorhus/dev → ~/dev inquirer 是一个命令行的回答的模块,你可以自己设定终端的问题,然后对这些回答给出相应的处理 rimraf 是一个可以使用 UNIX 命令 rm -rf的模块 剩下的本地路径的模块其实都是一些工具类,等用到的时候我们再来讲
// 是否为本地路径的方法 主要是判断模板路径当中是否存在 `./`const isLocalPath = localPath.isLocalPath// 获取模板路径的方法 如果路径参数是绝对路径 则直接返回 如果是相对的 则根据当前路径拼接const getTemplatePath = localPath.getTemplatePath 复制代码/** * Usage. */program .usage('<template-name> [project-name]') .option('-c, --clone', 'use git clone') .option('--offline', 'use cached template')/** * Help. */program.on('--help', () => { console.log(' Examples:') console.log() console.log(chalk.gray(' # create a new project with an official template')) console.log(' $ vue init webpack my-project') console.log() console.log(chalk.gray(' # create a new project straight from a github template')) console.log(' $ vue init username/repo my-project') console.log() })/** * Help. */function help () { program.parse(process.argv) if (program.args.length < 1) return program.help() }help() 复制代码
这部分代码声明了vue init用法,如果在终端当中 输入 vue init --help或者跟在vue init 后面的参数长度小于1,也会输出下面的描述
Usage: vue-init <template-name> [project-name] Options: -c, --clone use git clone --offline use cached template -h, --help output usage information Examples: # create a new project with an official template $ vue init webpack my-project # create a new project straight from a github template $ vue init username/repo my-project 复制代码
接下来是一些变量的获取
/** * Settings. */// 模板路径let template = program.args[0]const hasSlash = template.indexOf('/') > -1// 项目名称const rawName = program.args[1]const inPlace = !rawName || rawName === '.'// 如果不存在项目名称或项目名称输入的'.' 则name取的是 当前文件夹的名称const name = inPlace ? path.relative('../', process.cwd()) : rawName// 输出路径const to = path.resolve(rawName || '.')// 是否需要用到 git cloneconst clone = program.clone || false// tmp为本地模板路径 如果 是离线状态 那么模板路径取本地的const tmp = path.join(home, '.vue-templates', template.replace(/[\/:]/g, '-'))if (program.offline) { console.log(`> Use cached template at ${chalk.yellow(tildify(tmp))}`) template = tmp } 复制代码
接下来主要是根据模板名称,来下载并生产模板,如果是本地的模板路径,就直接生成。
/** * Check, download and generate the project. */function run () { // 判断是否是本地模板路径 if (isLocalPath(template)) { // 获取模板地址 const templatePath = getTemplatePath(template) // 如果本地模板路径存在 则开始生成模板 if (exists(templatePath)) { generate(name, templatePath, to, err => { if (err) logger.fatal(err) console.log() logger.success('Generated "%s".', name) }) } else { logger.fatal('Local template "%s" not found.', template) } } else { // 非本地模板路径 则先检查版本 checkVersion(() => { // 路径中是否 包含'/' // 如果没有 则进入这个逻辑 if (!hasSlash) { // 拼接路径 'vuejs-tempalte'下的都是官方的模板包 const officialTemplate = 'vuejs-templates/' + template // 如果路径当中存在 '#'则直接下载 if (template.indexOf('#') !== -1) { downloadAndGenerate(officialTemplate) } else { // 如果不存在 -2.0的字符串 则会输出 模板废弃的相关提示 if (template.indexOf('-2.0') !== -1) { warnings.v2SuffixTemplatesDeprecated(template, inPlace ? '' : name) return } // 下载并生产模板 downloadAndGenerate(officialTemplate) } } else { // 下载并生生成模板 downloadAndGenerate(template) } }) } } 复制代码
我们来看下 downloadAndGenerate这个方法
/** * Download a generate from a template repo. * * @param {String} template */function downloadAndGenerate (template) { // 执行加载动画 const spinner = ora('downloading template') spinner.start() // Remove if local template exists // 删除本地存在的模板 if (exists(tmp)) rm(tmp) // template参数为目标地址 tmp为下载地址 clone参数代表是否需要clone download(template, tmp, { clone }, err => { // 结束加载动画 spinner.stop() // 如果下载出错 输出日志 if (err) logger.fatal('Failed to download repo ' + template + ': ' + err.message.trim()) // 模板下载成功之后进入生产模板的方法中 这里我们再进一步讲 generate(name, tmp, to, err => { if (err) logger.fatal(err) console.log() logger.success('Generated "%s".', name) }) }) } 复制代码
到这里为止,bin/vue-init就讲完了,该文件做的最主要的一件事情,就是根据模板名称,来下载生成模板,但是具体下载和生成的模板的方法并不在里面。
下载模板
下载模板用的download方法是属于download-git-repo模块的。
最基础的用法为如下用法,这里的参数很好理解,第一个参数为仓库地址,第二个为输出地址,第三个是否需要 git clone,带四个为回调参数
download('flipxfx/download-git-repo-fixture', 'test/tmp',{ clone: true }, function (err) { console.log(err ? 'Error' : 'Success') }) 复制代码
在上面的run方法中有提到一个#的字符串实际就是这个模块下载分支模块的用法
download('bitbucket:flipxfx/download-git-repo-fixture#my-branch', 'test/tmp', { clone: true }, function (err) { console.log(err ? 'Error' : 'Success') }) 复制代码
生成模板
模板生成generate方法在generate.js当中,我们继续来看一下
generate.js
const chalk = require('chalk')const Metalsmith = require('metalsmith')const Handlebars = require('handlebars')const async = require('async')const render = require('consolidate').handlebars.renderconst path = require('path')const multimatch = require('multimatch')const getOptions = require('./options')const ask = require('./ask')const filter = require('./filter')const logger = require('./logger') 复制代码
chalk 是一个可以让终端输出内容变色的模块 Metalsmith是一个静态网站(博客,项目)的生成库 handlerbars 是一个模板编译器,通过template和json,输出一个html async 异步处理模块,有点类似让方法变成一个线程 consolidate 模板引擎整合库 multimatch 一个字符串数组匹配的库 options 是一个自己定义的配置项文件
随后注册了2个渲染器,类似于vue中的 vif velse的条件渲染
// register handlebars helperHandlebars.registerHelper('if_eq', function (a, b, opts) { return a === b ? opts.fn(this) : opts.inverse(this) }) Handlebars.registerHelper('unless_eq', function (a, b, opts) { return a === b ? opts.inverse(this) : opts.fn(this) }) 复制代码
接下来看关键的generate方法
module.exports = function generate (name, src, dest, done) { // 读取了src目录下的 配置文件信息, 同时将 name auther(当前git用户) 赋值到了 opts 当中 const opts = getOptions(name, src) // 拼接了目录 src/{template} 要在这个目录下生产静态文件 const metalsmith = Metalsmith(path.join(src, 'template')) // 将metalsmitch中的meta 与 三个属性合并起来 形成 data const data = Object.assign(metalsmith.metadata(), { destDirName: name, inPlace: dest === process.cwd(), noEscape: true }) // 遍历 meta.js元数据中的helpers对象,注册渲染模板数据 // 分别指定了 if_or 和 template_version内容 opts.helpers && Object.keys(opts.helpers).map(key => { Handlebars.registerHelper(key, opts.helpers[key]) }) const helpers = { chalk, logger } // 将metalsmith metadata 数据 和 { isNotTest, isTest 合并 } if (opts.metalsmith && typeof opts.metalsmith.before === 'function') { opts.metalsmith.before(metalsmith, opts, helpers) } // askQuestions是会在终端里询问一些问题 // 名称 描述 作者 是要什么构建 在meta.js 的opts.prompts当中 // filterFiles 是用来过滤文件 // renderTemplateFiles 是一个渲染插件 metalsmith.use(askQuestions(opts.prompts)) .use(filterFiles(opts.filters)) .use(renderTemplateFiles(opts.skipInterpolation)) if (typeof opts.metalsmith === 'function') { opts.metalsmith(metalsmith, opts, helpers) } else if (opts.metalsmith && typeof opts.metalsmith.after === 'function') { opts.metalsmith.after(metalsmith, opts, helpers) } // clean方法是设置在写入之前是否删除原先目标目录 默认为true // source方法是设置原路径 // destination方法就是设置输出的目录 // build方法执行构建 metalsmith.clean(false) .source('.') // start from template root instead of `./src` which is Metalsmith's default for `source` .destination(dest) .build((err, files) => { done(err) if (typeof opts.complete === 'function') { // 当生成完毕之后执行 meta.js当中的 opts.complete方法 const helpers = { chalk, logger, files } opts.complete(data, helpers) } else { logMessage(opts.completeMessage, data) } }) return data } 复制代码
meta.js
接下来看以下complete方法
complete: function(data, { chalk }) { const green = chalk.green // 会将已有的packagejoson 依赖声明重新排序 sortDependencies(data, green) const cwd = path.join(process.cwd(), data.inPlace ? '' : data.destDirName) // 是否需要自动安装 这个在之前构建前的询问当中 是我们自己选择的 if (data.autoInstall) { // 在终端中执行 install 命令 installDependencies(cwd, data.autoInstall, green) .then(() => { return runLintFix(cwd, data, green) }) .then(() => { printMessage(data, green) }) .catch(e => { console.log(chalk.red('Error:'), e) }) } else { printMessage(data, chalk) } } 复制代码
构建自定义模板
在看完vue-init命令的原理之后,其实定制自定义的模板是很简单的事情,我们只要做2件事
首先我们需要有一个自己模板项目
如果需要自定义一些变量,就需要在模板的meta.js当中定制
由于下载模块使用的是download-git-repo模块,它本身是支持在github,gitlab,bitucket上下载的,到时候我们只需要将定制好的模板项目放到git远程仓库上即可。
由于我需要定义的是小程序的开发模板,mpvue本身也有一个quickstart的模板,那么我们就在它的基础上进行定制,首先我们将它fork下来,新建一个custom分支,在这个分支上进行定制。
我们需要定制的地方有用到的依赖库,需要额外用到less以及wxparse 因此我们在 template/package.json当中进行添加
{ // ... 部分省略 "dependencies": { "mpvue": "^1.0.11"{{#vuex}}, "vuex": "^3.0.1"{{/vuex}} }, "devDependencies": { // ... 省略 // 这是添加的包 "less": "^3.0.4", "less-loader": "^4.1.0", "mpvue-wxparse": "^0.6.5" } } 复制代码
除此之外,我们还需要定制一下eslint规则,由于只用到standard,因此我们在meta.js当中 可以将 airbnb风格的提问删除
"lintConfig": { "when": "lint", "type": "list", "message": "Pick an ESLint preset", "choices": [ { "name": "Standard (https://github.com/feross/standard)", "value": "standard", "short": "Standard" }, { "name": "none (configure it yourself)", "value": "none", "short": "none" } ] } 复制代码
.eslinttrc.js
'rules': { {{#if_eq lintConfig "standard"}} "camelcase": 0, // allow paren-less arrow functions "arrow-parens": 0, "space-before-function-paren": 0, // allow async-await "generator-star-spacing": 0, {{/if_eq}} {{#if_eq lintConfig "airbnb"}} // don't require .vue extension when importing 'import/extensions': ['error', 'always', { 'js': 'never', 'vue': 'never' }], // allow optionalDependencies 'import/no-extraneous-dependencies': ['error', { 'optionalDependencies': ['test/unit/index.js'] }], {{/if_eq}} // allow debugger during development 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0 } 复制代码
最后我们在构建时的提问当中,再设置一个小程序名称的提问,而这个名称会设置到导航的标题当中。 提问是在meta.js当中添加
"prompts": { "name": { "type": "string", "required": true, "message": "Project name" }, // 新增提问 "appName": { "type": "string", "required": true, "message": "App name" } } 复制代码
main.json
{ "pages": [ "pages/index/main", "pages/counter/main", "pages/logs/main" ], "window": { "backgroundTextStyle": "light", "navigationBarBackgroundColor": "#fff", // 根据提问设置标题 "navigationBarTitleText": "{{appName}}", "navigationBarTextStyle": "black" } } 复制代码
最后我们来尝试一下我们自己的模板
vue init Baifann/mpvue-quickstart#custom min-app-project复制代码
总结
以上模板的定制是十分简单的,在实际项目上肯定更为复杂,但是按照这个思路应该都是可行的。比如说将一些自行封装的组件也放置到项目当中等等,这里就不再细说。原理解析都是基于vue-cli 2.0的,但实际上 3.0也已经整装待发,如果后续有机会,深入了解之后,再和大家分享,谢谢大家。
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。