• Vue-cli 原理实现


    背景

    在平时工作中会有遇到许多以相同模板定制的小程序,因此想自己建立一个生成模板的脚手架工具,以模板为基础构建对应的小程序,而平时的小程序都是用mpvue框架来写的,因此首先先参考一下Vue-cli的原理。知道原理之后,再定制自己的模板脚手架肯定是事半功倍的。


    在说代码之前我们首先回顾一下Vue-cli的使用,我们通常使用的是webpack模板包,输入的是以下代码。

    vue init webpack [project-name]

    在执行这段代码之后,系统会自动下载模板包,随后会询问我们一些问题,比如模板名称,作者,是否需要使用eslint,使用npm或者yarn进行构建等等,当所有问题我们回答之后,就开始生成脚手架项目。

    我们将源码下载下来,源码仓库点击这里,平时用的脚手架还是2.0版本,要注意,默认的分支是在dev上,dev上是3.0版本。

    我们首先看一下package.json,在文件当中有这么一段话

    1.  
      {
    2.  
      "bin": {
    3.  
      "vue": "bin/vue",
    4.  
      "vue-init": "bin/vue-init",
    5.  
      "vue-list": "bin/vue-list"
    6.  
      }
    7.  
      }

    由此可见,我们使用的命令 vue init,应该是来自bin/vue-init这个文件,我们接下来看一下这个文件中的内容


    bin/vue-init

    1.  
      const download = require('download-git-repo')
    2.  
      const program = require('commander')
    3.  
      const exists = require('fs').existsSync
    4.  
      const path = require('path')
    5.  
      const ora = require('ora')
    6.  
      const home = require('user-home')
    7.  
      const tildify = require('tildify')
    8.  
      const chalk = require('chalk')
    9.  
      const inquirer = require('inquirer')
    10.  
      const rm = require('rimraf').sync
    11.  
      const logger = require('../lib/logger')
    12.  
      const generate = require('../lib/generate')
    13.  
      const checkVersion = require('../lib/check-version')
    14.  
      const warnings = require('../lib/warnings')
    15.  
      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的模块
    剩下的本地路径的模块其实都是一些工具类,等用到的时候我们再来讲


    1.  
      // 是否为本地路径的方法 主要是判断模板路径当中是否存在 `./`
    2.  
      const isLocalPath = localPath.isLocalPath
    3.  
      // 获取模板路径的方法 如果路径参数是绝对路径 则直接返回 如果是相对的 则根据当前路径拼接
    4.  
      const getTemplatePath = localPath.getTemplatePath
    1.  
      /**
    2.  
      * Usage.
    3.  
      */
    4.  
       
    5.  
      program
    6.  
      .usage('<template-name> [project-name]')
    7.  
      .option('-c, --clone', 'use git clone')
    8.  
      .option('--offline', 'use cached template')
    9.  
       
    10.  
      /**
    11.  
      * Help.
    12.  
      */
    13.  
       
    14.  
      program.on('--help', () => {
    15.  
      console.log(' Examples:')
    16.  
      console.log()
    17.  
      console.log(chalk.gray(' # create a new project with an official template'))
    18.  
      console.log(' $ vue init webpack my-project')
    19.  
      console.log()
    20.  
      console.log(chalk.gray(' # create a new project straight from a github template'))
    21.  
      console.log(' $ vue init username/repo my-project')
    22.  
      console.log()
    23.  
      })
    24.  
       
    25.  
      /**
    26.  
      * Help.
    27.  
      */
    28.  
      function help () {
    29.  
      program.parse(process.argv)
    30.  
      if (program.args.length < 1) return program.help()
    31.  
      }
    32.  
      help()

    这部分代码声明了vue init用法,如果在终端当中 输入 vue init --help或者跟在vue init 后面的参数长度小于1,也会输出下面的描述

    1.  
      Usage: vue-init <template-name> [project-name]
    2.  
       
    3.  
      Options:
    4.  
       
    5.  
      -c, --clone use git clone
    6.  
      --offline use cached template
    7.  
      -h, --help output usage information
    8.  
      Examples:
    9.  
       
    10.  
      # create a new project with an official template
    11.  
      $ vue init webpack my-project
    12.  
       
    13.  
      # create a new project straight from a github template
    14.  
      $ vue init username/repo my-project

    接下来是一些变量的获取

    1.  
      /**
    2.  
      * Settings.
    3.  
      */
    4.  
      // 模板路径
    5.  
      let template = program.args[0]
    6.  
      const hasSlash = template.indexOf('/') > -1
    7.  
      // 项目名称
    8.  
      const rawName = program.args[1]
    9.  
      const inPlace = !rawName || rawName === '.'
    10.  
      // 如果不存在项目名称或项目名称输入的'.' 则name取的是 当前文件夹的名称
    11.  
      const name = inPlace ? path.relative('../', process.cwd()) : rawName
    12.  
      // 输出路径
    13.  
      const to = path.resolve(rawName || '.')
    14.  
      // 是否需要用到 git clone
    15.  
      const clone = program.clone || false
    16.  
       
    17.  
      // tmp为本地模板路径 如果 是离线状态 那么模板路径取本地的
    18.  
      const tmp = path.join(home, '.vue-templates', template.replace(/[/:]/g, '-'))
    19.  
      if (program.offline) {
    20.  
      console.log(`> Use cached template at ${chalk.yellow(tildify(tmp))}`)
    21.  
      template = tmp
    22.  
      }

    接下来主要是根据模板名称,来下载并生产模板,如果是本地的模板路径,就直接生成。

    1.  
      /**
    2.  
      * Check, download and generate the project.
    3.  
      */
    4.  
       
    5.  
      function run () {
    6.  
      // 判断是否是本地模板路径
    7.  
      if (isLocalPath(template)) {
    8.  
      // 获取模板地址
    9.  
      const templatePath = getTemplatePath(template)
    10.  
      // 如果本地模板路径存在 则开始生成模板
    11.  
      if (exists(templatePath)) {
    12.  
      generate(name, templatePath, to, err => {
    13.  
      if (err) logger.fatal(err)
    14.  
      console.log()
    15.  
      logger.success('Generated "%s".', name)
    16.  
      })
    17.  
      } else {
    18.  
      logger.fatal('Local template "%s" not found.', template)
    19.  
      }
    20.  
      } else {
    21.  
      // 非本地模板路径 则先检查版本
    22.  
      checkVersion(() => {
    23.  
      // 路径中是否 包含'/'
    24.  
      // 如果没有 则进入这个逻辑
    25.  
      if (!hasSlash) {
    26.  
      // 拼接路径 'vuejs-tempalte'下的都是官方的模板包
    27.  
      const officialTemplate = 'vuejs-templates/' + template
    28.  
      // 如果路径当中存在 '#'则直接下载
    29.  
      if (template.indexOf('#') !== -1) {
    30.  
      downloadAndGenerate(officialTemplate)
    31.  
      } else {
    32.  
      // 如果不存在 -2.0的字符串 则会输出 模板废弃的相关提示
    33.  
      if (template.indexOf('-2.0') !== -1) {
    34.  
      warnings.v2SuffixTemplatesDeprecated(template, inPlace ? '' : name)
    35.  
      return
    36.  
      }
    37.  
       
    38.  
      // 下载并生产模板
    39.  
      downloadAndGenerate(officialTemplate)
    40.  
      }
    41.  
      } else {
    42.  
      // 下载并生生成模板
    43.  
      downloadAndGenerate(template)
    44.  
      }
    45.  
      })
    46.  
      }
    47.  
      }

    我们来看下 downloadAndGenerate这个方法

    1.  
      /**
    2.  
      * Download a generate from a template repo.
    3.  
      *
    4.  
      * @param {String} template
    5.  
      */
    6.  
       
    7.  
      function downloadAndGenerate (template) {
    8.  
      // 执行加载动画
    9.  
      const spinner = ora('downloading template')
    10.  
      spinner.start()
    11.  
      // Remove if local template exists
    12.  
      // 删除本地存在的模板
    13.  
      if (exists(tmp)) rm(tmp)
    14.  
      // template参数为目标地址 tmp为下载地址 clone参数代表是否需要clone
    15.  
      download(template, tmp, { clone }, err => {
    16.  
      // 结束加载动画
    17.  
      spinner.stop()
    18.  
      // 如果下载出错 输出日志
    19.  
      if (err) logger.fatal('Failed to download repo ' + template + ': ' + err.message.trim())
    20.  
      // 模板下载成功之后进入生产模板的方法中 这里我们再进一步讲
    21.  
      generate(name, tmp, to, err => {
    22.  
      if (err) logger.fatal(err)
    23.  
      console.log()
    24.  
      logger.success('Generated "%s".', name)
    25.  
      })
    26.  
      })
    27.  
      }

    到这里为止,bin/vue-init就讲完了,该文件做的最主要的一件事情,就是根据模板名称,来下载生成模板,但是具体下载和生成的模板的方法并不在里面。

    下载模板

    下载模板用的download方法是属于download-git-repo模块的。

    最基础的用法为如下用法,这里的参数很好理解,第一个参数为仓库地址,第二个为输出地址,第三个是否需要 git clone,带四个为回调参数

    1.  
      download('flipxfx/download-git-repo-fixture', 'test/tmp',{ clone: true }, function (err) {
    2.  
      console.log(err ? 'Error' : 'Success')
    3.  
      })

    在上面的run方法中有提到一个#的字符串实际就是这个模块下载分支模块的用法

    1.  
      download('bitbucket:flipxfx/download-git-repo-fixture#my-branch', 'test/tmp', { clone: true }, function (err) {
    2.  
      console.log(err ? 'Error' : 'Success')
    3.  
      })

    生成模板

    模板生成generate方法在generate.js当中,我们继续来看一下


    generate.js

    1.  
      const chalk = require('chalk')
    2.  
      const Metalsmith = require('metalsmith')
    3.  
      const Handlebars = require('handlebars')
    4.  
      const async = require('async')
    5.  
      const render = require('consolidate').handlebars.render
    6.  
      const path = require('path')
    7.  
      const multimatch = require('multimatch')
    8.  
      const getOptions = require('./options')
    9.  
      const ask = require('./ask')
    10.  
      const filter = require('./filter')
    11.  
      const logger = require('./logger')
    12.  
       

    chalk 是一个可以让终端输出内容变色的模块
    Metalsmith是一个静态网站(博客,项目)的生成库
    handlerbars 是一个模板编译器,通过templatejson,输出一个html
    async 异步处理模块,有点类似让方法变成一个线程
    consolidate 模板引擎整合库
    multimatch 一个字符串数组匹配的库
    options 是一个自己定义的配置项文件

    随后注册了2个渲染器,类似于vue中的 vif velse的条件渲染

    1.  
      // register handlebars helper
    2.  
      Handlebars.registerHelper('if_eq', function (a, b, opts) {
    3.  
      return a === b
    4.  
      ? opts.fn(this)
    5.  
      : opts.inverse(this)
    6.  
      })
    7.  
       
    8.  
      Handlebars.registerHelper('unless_eq', function (a, b, opts) {
    9.  
      return a === b
    10.  
      ? opts.inverse(this)
    11.  
      : opts.fn(this)
    12.  
      })

    接下来看关键的generate方法

    1.  
      module.exports = function generate (name, src, dest, done) {
    2.  
      // 读取了src目录下的 配置文件信息, 同时将 name auther(当前git用户) 赋值到了 opts 当中
    3.  
      const opts = getOptions(name, src)
    4.  
      // 拼接了目录 src/{template} 要在这个目录下生产静态文件
    5.  
      const metalsmith = Metalsmith(path.join(src, 'template'))
    6.  
      // 将metalsmitch中的meta 与 三个属性合并起来 形成 data
    7.  
      const data = Object.assign(metalsmith.metadata(), {
    8.  
      destDirName: name,
    9.  
      inPlace: dest === process.cwd(),
    10.  
      noEscape: true
    11.  
      })
    12.  
      // 遍历 meta.js元数据中的helpers对象,注册渲染模板数据
    13.  
      // 分别指定了 if_or 和 template_version内容
    14.  
      opts.helpers && Object.keys(opts.helpers).map(key => {
    15.  
      Handlebars.registerHelper(key, opts.helpers[key])
    16.  
      })
    17.  
       
    18.  
      const helpers = { chalk, logger }
    19.  
       
    20.  
      // 将metalsmith metadata 数据 和 { isNotTest, isTest 合并 }
    21.  
      if (opts.metalsmith && typeof opts.metalsmith.before === 'function') {
    22.  
      opts.metalsmith.before(metalsmith, opts, helpers)
    23.  
      }
    24.  
       
    25.  
      // askQuestions是会在终端里询问一些问题
    26.  
      // 名称 描述 作者 是要什么构建 在meta.js 的opts.prompts当中
    27.  
      // filterFiles 是用来过滤文件
    28.  
      // renderTemplateFiles 是一个渲染插件
    29.  
      metalsmith.use(askQuestions(opts.prompts))
    30.  
      .use(filterFiles(opts.filters))
    31.  
      .use(renderTemplateFiles(opts.skipInterpolation))
    32.  
       
    33.  
      if (typeof opts.metalsmith === 'function') {
    34.  
      opts.metalsmith(metalsmith, opts, helpers)
    35.  
      } else if (opts.metalsmith && typeof opts.metalsmith.after === 'function') {
    36.  
      opts.metalsmith.after(metalsmith, opts, helpers)
    37.  
      }
    38.  
       
    39.  
      // clean方法是设置在写入之前是否删除原先目标目录 默认为true
    40.  
      // source方法是设置原路径
    41.  
      // destination方法就是设置输出的目录
    42.  
      // build方法执行构建
    43.  
      metalsmith.clean(false)
    44.  
      .source('.') // start from template root instead of `./src` which is Metalsmith's default for `source`
    45.  
      .destination(dest)
    46.  
      .build((err, files) => {
    47.  
      done(err)
    48.  
      if (typeof opts.complete === 'function') {
    49.  
      // 当生成完毕之后执行 meta.js当中的 opts.complete方法
    50.  
      const helpers = { chalk, logger, files }
    51.  
      opts.complete(data, helpers)
    52.  
      } else {
    53.  
      logMessage(opts.completeMessage, data)
    54.  
      }
    55.  
      })
    56.  
       
    57.  
      return data
    58.  
      }

    meta.js

    接下来看以下complete方法

    1.  
      complete: function(data, { chalk }) {
    2.  
      const green = chalk.green
    3.  
      // 会将已有的packagejoson 依赖声明重新排序
    4.  
      sortDependencies(data, green)
    5.  
       
    6.  
      const cwd = path.join(process.cwd(), data.inPlace ? '' : data.destDirName)
    7.  
      // 是否需要自动安装 这个在之前构建前的询问当中 是我们自己选择的
    8.  
      if (data.autoInstall) {
    9.  
      // 在终端中执行 install 命令
    10.  
      installDependencies(cwd, data.autoInstall, green)
    11.  
      .then(() => {
    12.  
      return runLintFix(cwd, data, green)
    13.  
      })
    14.  
      .then(() => {
    15.  
      printMessage(data, green)
    16.  
      })
    17.  
      .catch(e => {
    18.  
      console.log(chalk.red('Error:'), e)
    19.  
      })
    20.  
      } else {
    21.  
      printMessage(data, chalk)
    22.  
      }
    23.  
      }

    构建自定义模板

    在看完vue-init命令的原理之后,其实定制自定义的模板是很简单的事情,我们只要做2件事

    • 首先我们需要有一个自己模板项目
    • 如果需要自定义一些变量,就需要在模板的meta.js当中定制

    由于下载模块使用的是download-git-repo模块,它本身是支持在github,gitlab,bitucket上下载的,到时候我们只需要将定制好的模板项目放到git远程仓库上即可。

    由于我需要定义的是小程序的开发模板,mpvue本身也有一个quickstart的模板,那么我们就在它的基础上进行定制,首先我们将它fork下来,新建一个custom分支,在这个分支上进行定制。

    我们需要定制的地方有用到的依赖库,需要额外用到less以及wxparse
    因此我们在 template/package.json当中进行添加

    1.  
      {
    2.  
      // ... 部分省略
    3.  
      "dependencies": {
    4.  
      "mpvue": "^1.0.11"{{#vuex}},
    5.  
      "vuex": "^3.0.1"{{/vuex}}
    6.  
      },
    7.  
      "devDependencies": {
    8.  
      // ... 省略
    9.  
      // 这是添加的包
    10.  
      "less": "^3.0.4",
    11.  
      "less-loader": "^4.1.0",
    12.  
      "mpvue-wxparse": "^0.6.5"
    13.  
      }
    14.  
      }

    除此之外,我们还需要定制一下eslint规则,由于只用到standard,因此我们在meta.js当中 可以将 airbnb风格的提问删除

    1.  
      "lintConfig": {
    2.  
      "when": "lint",
    3.  
      "type": "list",
    4.  
      "message": "Pick an ESLint preset",
    5.  
      "choices": [
    6.  
      {
    7.  
      "name": "Standard (https://github.com/feross/standard)",
    8.  
      "value": "standard",
    9.  
      "short": "Standard"
    10.  
      },
    11.  
      {
    12.  
      "name": "none (configure it yourself)",
    13.  
      "value": "none",
    14.  
      "short": "none"
    15.  
      }
    16.  
      ]
    17.  
      }

    .eslinttrc.js

    1.  
      'rules': {
    2.  
      {{#if_eq lintConfig "standard"}}
    3.  
      "camelcase": 0,
    4.  
      // allow paren-less arrow functions
    5.  
      "arrow-parens": 0,
    6.  
      "space-before-function-paren": 0,
    7.  
      // allow async-await
    8.  
      "generator-star-spacing": 0,
    9.  
      {{/if_eq}}
    10.  
      {{#if_eq lintConfig "airbnb"}}
    11.  
      // don't require .vue extension when importing
    12.  
      'import/extensions': ['error', 'always', {
    13.  
      'js': 'never',
    14.  
      'vue': 'never'
    15.  
      }],
    16.  
      // allow optionalDependencies
    17.  
      'import/no-extraneous-dependencies': ['error', {
    18.  
      'optionalDependencies': ['test/unit/index.js']
    19.  
      }],
    20.  
      {{/if_eq}}
    21.  
      // allow debugger during development
    22.  
      'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
    23.  
      }

    最后我们在构建时的提问当中,再设置一个小程序名称的提问,而这个名称会设置到导航的标题当中。
    提问是在meta.js当中添加

    1.  
      "prompts": {
    2.  
      "name": {
    3.  
      "type": "string",
    4.  
      "required": true,
    5.  
      "message": "Project name"
    6.  
      },
    7.  
      // 新增提问
    8.  
      "appName": {
    9.  
      "type": "string",
    10.  
      "required": true,
    11.  
      "message": "App name"
    12.  
      }
    13.  
      }

    main.json

    1.  
      {
    2.  
      "pages": [
    3.  
      "pages/index/main",
    4.  
      "pages/counter/main",
    5.  
      "pages/logs/main"
    6.  
      ],
    7.  
      "window": {
    8.  
      "backgroundTextStyle": "light",
    9.  
      "navigationBarBackgroundColor": "#fff",
    10.  
      // 根据提问设置标题
    11.  
      "navigationBarTitleText": "{{appName}}",
    12.  
      "navigationBarTextStyle": "black"
    13.  
      }
    14.  
      }
    15.  
       

    最后我们来尝试一下我们自己的模板

    vue init Baifann/mpvue-quickstart#custom min-app-project
    

    image_1cj0ikq141je51ii31eek25t18il19.png-31.4kB

    总结

    以上模板的定制是十分简单的,在实际项目上肯定更为复杂,但是按照这个思路应该都是可行的。比如说将一些自行封装的组件也放置到项目当中等等,这里就不再细说。原理解析都是基于vue-cli 2.0的,但实际上 3.0也已经整装待发,如果后续有机会,深入了解之后,再和大家分享,谢谢大家。

  • 相关阅读:
    在同一台机器上让Microsoft SQL Server 2000/ SQL2005/ SQL2008共存
    HTML 中<style>中</style>里面<!-- -->标签是干嘛的
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    怎样清除td和input之间空隙
    aspx页面中, <%= % > 与 <%# % > 的区别
    python之day13(ORM,paramiko模块,堡垒机)
    python之day12(线程池,redis,rabbitMQ)
    RabbitMQ与SQLAlchemy(预习)
    redis操作篇
    python之day11(线程,进程,协程,memcache,redis)
  • 原文地址:https://www.cnblogs.com/dillonmei/p/12570860.html
Copyright © 2020-2023  润新知