@mpx/cli 脚手架源码解析

回眸只為那壹抹淺笑 提交于 2019-12-19 14:33:12

前言

Mpx是一款致力于提高小程序开发体验的增强型小程序框架,通过Mpx,我们能够以最先进的web开发体验(Vue +
Webpack)来开发生产性能深度优化的小程序。

下面说说mpx脚手架的源码:
源码地址:https://github.com/didi/mpx/tree/master/packages/cli
目录结构:

.
├── README.md 
├── bin
│   ├── mpx-init.js // init命令开始执行的内容
│   └── mpx.js // 命令入口文件
├── lib
│   ├── ask.js // 自定义工具-用于询问开发者
│   ├── check-version.js // 检查本地node和npm包版本
│   ├── eval.js // 在data的作用域执行exp表达式并返回其执行得到的值
│   ├── filter.js // 配合metalsmith删除过滤多余的文件
│   ├── generate.js // 模板下载后根据用户选择生成指定模板
│   ├── git-user.js // 用于获取本地的git配置的用户名和邮件,并返回格式 姓名<邮箱> 的字符串
│   ├── local-path.js // 判断本地文件是否存在
│   ├── logger.js // 记录日志
│   └── options.js // 获取模板的选项配置信息并初始化默认值
├── package-lock.json
└── package.json

文件分析

package.json

{
  "name": "@mpxjs/cli",
  "version": "2.1.0",
  "description": "mpx脚手架",
  "bin": {
    "mpx": "bin/mpx.js",
    "mpx-init": "bin/mpx-init.js"
  },
  "dependencies": {
    "async": "^2.4.0",
    "chalk": "^2.1.0",
    "commander": "^2.9.0",
    "download-git-repo": "^1.0.1",
    "inquirer": "6.3.1",
    "metalsmith": "^2.1.0",
    "minimatch": "^3.0.0",
    "multimatch": "^2.1.0",
    "nunjucks": "^3.1.2",
    "ora": "^1.3.0",
    "read-metadata": "^1.0.0",
    "request": "^2.67.0",
    "rimraf": "^2.5.0",
    "semver": "^5.1.0",
    "tildify": "^1.2.0",
    "update-notifier": "^2.5.0",
    "user-home": "^2.0.0",
    "validate-npm-package-name": "^3.0.0"
  },
  "keywords": [
    "mpx",
    "cli"
  ],
  "author": "donghongping",
  "license": "Apache",
  "main": "bin/mpx-init.js",
  "directories": {
    "bin": "bin",
    "lib": "lib"
  },
  "files": [
    "bin",
    "lib"
  ],
  "publishConfig": {
    "registry": "https://registry.npmjs.org"
  },
  "repository": {
    "type": "git",
    "url": "git@github.com:didi/mpx.git"
  },
  "homepage": "https://didi.github.io/mpx/",
  "bugs": {
    "url": "https://github.com/didi/mpx/issues"
  },
  "engines": {
    "node": ">=8.0.0"
  },
  "scripts": {
    "test": "echo \"Error: run tests from root\" && exit 1"
  }
}

安装依赖包

  • async:异步处理工具
  • chalk :用于高亮终端打印出来的信息
  • commander: 命令行处理工具
  • download-git-repo: 用于下载远程仓库至本地 支持GitHub、GitLab、Bitbucket
  • inquirer:用于命令行与开发者交互
  • metalsmith:静态网站生成器
  • minimatch:字符匹配工具
  • multimatch:可以支持多个条件的匹配
  • nunjucks:js模板引擎 ora:用于命令行上的加载效果
  • read-metadata:用于读取json或者yaml元数据文件并返回一个对象
  • request :发送http请求的工具
  • rimraf:相当于UNIX的“rm -rf”命令
  • semver:版本号处理工具
  • tildify:将绝对路径转换成带波浪符的路径
  • update-notifier:更新node和npm包
  • user-home: 用于获取用户的根目录
  • validate-npm-package-name:用于npm包的名字是否是合法的

入口文件

mpx.js
#!/usr/bin/env node

require('commander')
  .version(require('../package').version)
  .usage('<command> [options]')
  .command('init', 'generate a new project from a template')
  .parse(process.argv)

主要是根据init命令执行max-init.js文件内容;

mpx-init.js

引入依赖包:忽略

指令引导提示:

// 指令引导
program
  .usage('[project-name]')
  .option('-c, --clone', 'use git clone')
  .option('--offline [value]', 'use cached template or specific a local path to mpx-template')
  .on('--help', () => {
    console.log()
    console.log('  Examples:')
    console.log()
    console.log(chalk.gray('    # create a new project with the specified dirname'))
    console.log('    $ mpx init awesome-project')
    console.log()
    console.log(chalk.gray('    # create a new project in current directory'))
    console.log('    $ mpx init')
    console.log()
  })
  .parse(process.argv)

判断是否在当前目录创建项目

if (inPlace || exists(to)) {
  inquirer.prompt([{
    type: 'confirm',
    message: inPlace
      ? 'Generate project in current directory?'
      : 'Target directory exists. Continue?',
    name: 'ok'
  }]).then(answers => {
    if (answers.ok) {
      run()
    }
  }).catch(logger.fatal)
} else {
  run()
}

判断inPlace和exists(to),true则询问开发者,当开发者回答“yes”的时候执行run函数,否则直接执行run函数。这里询问开发者的问题有如下两个:
Generate project in current directory? //是否在当前目录下构建项目
Target directory exists. Continue? //构建目录已存在,是否继续

重要的两个函数
run函数: 主要逻辑如下
在这里插入图片描述
可以选择选用本地自定义的模板或者是官方模板,如果是需要使用远程仓库的自定义模板,则可以在下载的时候配置自己指定的模板地址;

function run () {
  // check if template is local 模板是否在本地
  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(() => {
      // use official templates
      // 如果需要下载自己的模板即可在此处修改自定义模板的路径
      const officialTemplate = 'mpx-ecology/' + template
      downloadAndGenerate(officialTemplate)
    })
  }
}

downloadAndGenerate函数:下载函数

function downloadAndGenerate (template) {
  const spinner = ora('downloading template') // loading 动画
  spinner.start() // 动画开始
  // Remove if local template exists
  if (exists(tmp)) rm(tmp)
  // 下载克隆
  download(template, tmp, { clone }, err => {
    spinner.stop() // 下载完成后,动画暂停
    if (err) logger.fatal('Failed to download repo ' + template + ': ' + err.message.trim())
    // 执行参数可选配置等操作,主要是执行meta.js里面的各项配置
    generate(name, tmp, to, err => {
      if (err) logger.fatal(err)
      logger.success('Generated "%s".', name)
    })
  })
}

lib文件

generate.js文件

该文件主要处理模本的配置和生成
主要函数为generate函数,即上面mpx-init.js里面的generate函数

/**
 * Generate a template given a `src` and `dest`.
 *
 * @param {String} name
 * @param {String} src
 * @param {String} dest
 * @param {Function} done
 */

module.exports = function generate (name, src, dest, done) {
  const opts = getOptions(name, src) // 获取到配置参数
  const metalsmith = Metalsmith(path.join(src, 'template')) // 静态网站生成器
  const data = Object.assign(metalsmith.metadata(), {
    destDirName: name,
    inPlace: dest === process.cwd(),
    noEscape: true
  }) // 添加一些变量至metalsmith中,并获取metalsmith中全部变量
  const helpers = { chalk, logger }
  //配置对象是否有before函数,是则执行
  if (opts.metalsmith && typeof opts.metalsmith.before === 'function') {
    opts.metalsmith.before(metalsmith, opts, helpers)
  }

  (opts.mock
    ? metalsmith.use(mock(opts.mock))
    : metalsmith.use(askQuestions(opts.prompts))) // 询问问题
    .use(computed(opts.computed)) // 处理关键词
    .use(filterFiles(opts.filters)) // 过滤文件
    .use(renderTemplateFiles(opts.skipInterpolation)) // 渲染模板文件

  // 配置对象是否有after函数,是则执行
  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)
  }

  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') {
        // 配置对象有complete函数则执行
        const helpers = { chalk, logger, files }
        opts.complete(data, helpers)
      } else {
        // 配置对象有completeMessage,执行logMessage函数
        logMessage(opts.completeMessage, data)
      }
    })

  return data
}

generate的主要分为以下几步:

  1. 获取模板配置
  2. 初始化Metalsmith
  3. 添加一些变量至Metalsmith
  4. 配置对象中是否有before函数,有则执行
  5. 询问问题
  6. 处理变量
  7. 过滤文件
  8. 渲染模板文件
  9. 配置对象中是否有after函数,有则执行
  10. 最后构建项目内容
  11. 构建完成,成功若配置对象中有complete函数则执行,否则打印配置对象中的completeMessage信息,如果有错误,执行回调函数done(err)

另外与vue-cli不同的是,mpx的脚手架添加了配置变量插值标签,以免渲染时能与小程序动态绑定语法冲突

nunjucks.configure({
  tags: {
    variableStart: '<$',
    variableEnd: '$>'
  },
  autoescape: false,
  trimBlocks: true,
  lstripBlocks: true
})
const render = nunjucks.renderString

其他文件:正如上面目录介绍里面一样,主要是封装的一些工具函数,在上述方法中使用到,这里不做详细解释;

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!