手摸手从浅析vue-cli原理到定制前端开发模板

2020/3/29 11:02:31

本文主要是介绍手摸手从浅析vue-cli原理到定制前端开发模板,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

前言

从迷糊小生到职场菜鸟,遇到无数难以解决的问题。幸得职场路上有大佬手摸手悉心教导,终于学会独自开着手扶拖拉机出混迹江湖。遥想当时初入江湖,灵魂三问“听说你vue用得挺熟练,写个vue-cli吧?”、“这边需要定制化前端开发模板,你来加优化方案吧?”、“把vue-cli和webpack合在一起做个模板吧?” 。下面将手摸手从浅析vue-cli源码到webpack定制开发模板,文章内容比较多,所以分成了两部分。

1 背景

vue-cli是vue官方提供快速创建项目的辅助脚手架工具,可以帮助我们快速创建新项目。官方提供几个常用的模板。具体vue-cli怎么使用,相信大伙已经玩得贼溜,不懂的伙伴可以查阅官方文档。但是在日常开发中,我们需要根据脚手架增添各种不同的结构和常用工具,单纯的简单的模板已经满足不了我们的需求,所以想要定制化模板。“工欲善其事,必先利其器”,要想更好去定制模板,要先更了解vue-cli的原理,然后去定制前端模板。如有不妥之处,请轻喷,多多指教。

2 模板meta.js范例

单纯阅读vue-cli源码,在生成模板处理中需要对照着meta配置文件阅读比较清晰,这里贴了一份自己根据webpack模板配置的有关内容

依赖项

sortDependencies为项目模板的依赖排序,installDependencies是为模板渲染完毕之后安装项目依赖npm包,runLintFix是执行eslint格式化不合法的代码格式

const {
  sortDependencies,
  installDependencies,
  runLintFix,
  printMessage,
} = require('./utils')
const path = require('path')

const { addTestAnswers } = require('./scenarios')
复制代码

配置文件选项

addTestAnswers是在模板运行测试时,将为所选场景添加答案;helpers是模板文件预定义的模板渲染辅助函数;prompts是定义需要收集的依赖变量;filters是预定义的根据条件过滤的文件;complete是模板渲染完毕之后,执行的函数,这里的作用是根据用户选择是否需要自动安装npm包依赖和格式化代码风格。

辅助函数和为选项添加答案

module.exports = {
  metalsmith: {
    before: addTestAnswers
  },
  helpers: {
    if_or(v1, v2, options) {
      if (v1 || v2) {
        return options.fn(this);
      }
      return options.inverse(this);
    }
  },
  
复制代码

定义的prompts问题

  prompts: {
    name: {
      when: "isNotTest",
      type: "string",
      required: true,
      message: "项目名称"
    },
    router: {
      when: "isNotTest",
      type: "confirm",
      message: "安装vue-router?"
    },
    vuex: {
      when: "isNotTest",
      type: 'confirm',
      message: '安装vuex?'
    },
    autoInstall: {
      when: 'isNotTest',
      type: 'list',
      message:
        '创建项目后,我们应该为您运行`npm install`吗?',
      choices: [
        {
          name: '是的,使用npm',
          value: 'npm',
          short: 'npm',
        },
        {
          name: '是的,使用yarn',
          value: 'yarn',
          short: 'yarn',
        },
        {
          name: '不用了,我手动安装它',
          value: false,
          short: 'no',
        },
      ]
    }
  },
复制代码

filters和complete

filters定义了根据收集的变量是否为真来决定是否需要删除这些文件。complete是项目构建完毕之后,执行的钩子函数,根据脚手架vue-cli,如果complete不存在,可以另外定义一个completeMessage字符串。

filters: {
  'src/router/**/*': "router",
  'src/store/**/*': "vuex"
},
complete: function(data, { chalk }) {
  const green = chalk.green

  sortDependencies(data, green)

  const cwd = path.join(process.cwd(), data.inPlace ? '' : data.destDirName)

  if (data.autoInstall) {
    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)
  }
}
};
复制代码

定制项目内容

官方的webpack项目内容定义在webpack/template中,主要对package.jsonrouter以及readme.MD嵌入了handlerbars的语法进行条件渲染。对官方模板进行了删减,只定义部分变量,方便理解。

package.json

下面{{}}是占位,根据vue-cli脚手架收集变量值和依赖库来条件渲染来条件渲染啊内容

{
  "name": "{{name}}",
  "version": "{{version}}",
  "description": "{{description}}",
  "main": "./src/main.js",
  "dependencies": {
    {{#vuex}}
    "vuex": "^3.1.0",
    {{/vuex}}
    {{#router}}
    "vue-router": "^3.0.1",
    {{/router}}
    "vue": "^2.5.2"
  }
  // ... 省略部分内容
}
复制代码

main.js

项目入口文件也是根据收集的库依赖来动态增加或者删除内容达到定制项目

import Vue from 'vue'
import App from './App'
// {{#router}}
import router from './router'
// {{/router}}

Vue.config.productionTip = false

/* eslint-disable no-new */
new Vue({
  el: '#app',
  // {{#router}}
  router,
  // {{/router}}
  render: h => h(App)
})
复制代码

3 vue-cli目录设计

本文采用的是vue-cli的版本是 2.96,现在vue-cli已经更新到了4版本,但是内部原理还是一样,接下来先手摸手解析vue-cli的代码。

大体思路

vue-cli主要提供vue init、 vue list 两个主要命令; vue init 功能是根据提供的模板生成项目,而vue list 是罗列出官方提供可使用的模板。官方把脚手架和模板分离,个人认为是方便模板更新与脚手架解耦,另一方面也为提供第三方模板提供条件(膜拜大佬操作)。

第一次使用 vue init [模板名] [项目名]从远程安装模板时,会根据模板名称是否带有‘/’区分是第三方模板还是官方模板进行下载,将模板下载并存入计算机用户根目录。下次下载时可直接从缓存中读取模板,生成最终的开发目录结构。

目录结构

vue-cli主要命令有vue initvue list两个命令,第一个命令是我们这次主讲的一个命令,vue-list是通过请求github respo的api获取可使用的模板列表,比较简单。

+---bin
|   |--- vue
|   |--- vue-build
|   |--- vue-create
|   |--- vue-init
|   \--- vue-list
|
|
\---lib
     |--- ask.js
     |--- check-version.js
     |--- eval.js
     |--- filter.js
     |--- generate.js
     |--- git-user.js
     |--- local-path.js
     |--- logger.js
     |--- options.js
     \--- warnings.js
复制代码

bin目录中定义了vue 、init、create、build、list,其中vue-create已经在vue-cli3以上版本使用,vue-build已移除。所以会重点去了解init和list。其中lib目录是辅助函数目录,会在下面详细描述。

4 vue命令源码

终于进入正题,这里会按照命令vuevue listvue init顺序递进,来手摸手,手把手,冲 ─=≡Σ(((つ•̀ω•́)つ

vue命令源码定义在bin/vue文件中,源码中引入了commander包,commander是node.js 命令行接口的完整解决方案,这里是定义bash的命令,具体怎么使用请跳移tj大神commander仓库。里面文档比较齐全。

#!/usr/bin/env node
const program = require('commander')
// 定义了init list build create几个命令和版本号
program
  .version(require('../package').version)
  .usage('<command> [options]')
  .command('init', 'generate a new project from a template')
  .command('list', 'list available official templates')
  .command('build', 'prototype a new project')
  .command('create', '(for v3 warning only)')
program.parse(process.argv)
复制代码

当在bash中只输入vue指令时就会输出command定义的命令提示信息反馈。

5 vue list源码

vue list命令是通过请求Github的api获取当前组织仓库列表信息,展示到bash,这个命令用到几个常见的库:

request: 发起处理网络请求的库,这里用做请求github api
chalk: bash输出文字高亮美观
logger: 自定义函数,打印信息

代码解析

#!/usr/bin/env node

const logger = require('../lib/logger')
const request = require('request')
const chalk = require('chalk')

// 定义用户退出时事件
console.log()
process.on('exit', () => {
  console.log()
})


// 请求vue的官方仓库,这里可以看到是 https://api.github.com/users/vuejs-templates/repos
request({
  url: 'https://api.github.com/users/vuejs-templates/repos',
  headers: {
    'User-Agent': 'vue-cli'
  }
}, (err, res, body) => {
  if (err) logger.fatal(err)
  // 返回官方模板列表数据
  const requestBody = JSON.parse(body)
  if (Array.isArray(requestBody)) {
    console.log('  Available official templates:')
    console.log()
    requestBody.forEach(repo => {
      console.log(
        '  ' + chalk.yellow('★') +
        '  ' + chalk.blue(repo.name) +
        ' - ' + repo.description)
    })
  } else {
    console.error(requestBody.message)
  }
})
复制代码

执行结果

在输入vue list时,会执行上面的代码,请求https://api.github.com/users/vuejs-templates/repos获取到模板列表信息,如下面所示:

$ vue list

  Available official templates:

  ★  browserify - A full-featured Browserify + vueify setup with hot- reload, linting & unit testing.
  ★  browserify-simple - A simple Browserify + vueify setup for quick  prototyping.
  ★  pwa - PWA template for vue-cli based on the webpack template
  ★  simple - The simplest possible Vue setup in a single HTML file
  ★  webpack - A full-featured Webpack + vue-loader setup with hot re load, linting, testing & css extraction.
  ★  webpack-simple - A simple Webpack + vue-loader setup for quick p rototyping.
复制代码

自定义仓库信息

在自定义模板仓库时,如果不想通过请求获取仓库列表信息,可以通过在脚手架中定义一份关于仓库信息,每次输入vue list,读取本地文件进行展示,不过这样每次有模板更新,都要更新脚手架,所以为了解耦,建议通过网络请求读取的方式。如果内部团队使用,并且不日常更新可以采用下面的方式。

通过读取本地文件

#!/usr/bin/env node
const chalk = require("chalk");
const templates = require("../templates")
console.log(chalk.cyan('\nThere are some avaiable templates: \n'))
templates.forEach((item,index) => {
    console.log(chalk.yellow('★') + ` ${chalk.green(item.name)} - ${chalk.cyan(item.descriptions)}\n`)
    console.log(`   url: ${chalk.underline(item.url)} \n`)
})
复制代码

templates定义

下面是作者定制了两个模板,一个是React + typescriptvue的模板,可以通过vue init ly-templates/vue(或者react) 项目名进行加载。

const templates = [
  {
    name: "vue",
    url: "https://github.com/ly-templates/vue",
    descriptions: "a single page web project template for vue"
  },
  {
    name: "react",
    url: "https://github.com/ly-templates/react",
    descriptions: "a single page web project template for react"
  }
];
module.exports = templates
复制代码

6 vue init源码

终于到主菜了,一般最后都是难啃的骨头,也是最想去了解的地方,先缓解一下紧张的神经:

vue init依赖库

运行vue init命令首先执行vue-init文件,去拨开她神秘的..(Σσ(・Д・;)我我我只是揭开了面纱!!!)

下面会是依赖模块的作用解析:

// 下载github仓库的库,支持github、gitlab
const download = require('download-git-repo')
// 定义bash命令
const program = require('commander')
// nodejs内置模块fs判断文件目录是否存在函数
const exists = require('fs').existsSync
// nodejs内置模块,处理统一路径
const path = require('path')
// 提供动态loading视觉,生动形象
const ora = require('ora')
// 获取系统用户根目录路径
const home = require('user-home')
//将绝对路径转换成带波浪符的路径
const tildify = require('tildify')
// 将bash打印信息高亮
const chalk = require('chalk')
// 用作询问收集用户所需依赖库
const inquirer = require('inquirer')
// 用作删除文件作用
const rm = require('rimraf').sync
// 内部定义模块,打印信息
const 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')
// 判断是否本地路径
const isLocalPath = localPath.isLocalPath
// 获取本地模板路径
const getTemplatePath = localPath.getTemplatePath
复制代码

定义help命令

vue inti 命令提供通过git clone下载github仓库的方式,只要指定--clone或者-c的命令参数,也可以明确指定采用用户根目录的缓存模板生成项目.

// 指定下载仓库的方式和生成项目是否从缓存中读取
program
  .usage('<template-name> [project-name]')
  .option('-c, --clone', 'use git clone')
  .option('--offline', 'use cached template')
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()
})
// 如果只指定了vue init,没有指定模板名,调用help命令打印提示信息
function help () {
  program.parse(process.argv)
  if (program.args.length < 1) return program.help()
}
help()

复制代码

模板指定和项目名

通过vue init webpack demo命令生成项目目录结构,首先会先获取模板名和项目名称。如果当前没有指定项目名或者指定为'.',则会提示在当前目录生成项目和取消选择。

// 获取模板名称
let template = program.args[0]
// 判断是官方仓库还是三方仓库,比如自己定义的React模板,指定为
'ly-templates/react'
const hasSlash = template.indexOf('/') > -1
// 获取项目名称
const rawName = program.args[1]
// 是否指定项目名称,不存在或者指定为当前目录
const inPlace = !rawName || rawName === '.'
// 如果没有指定项目名称或者为'.',则以当前的目录的名称为目录名,否则以指定的目录名在当前目录生成项目
const name = inPlace ? path.relative('../', process.cwd()) : rawName
// 获取要生成目录的绝对路径
const to = path.resolve(rawName || '.')
// 是够是采用git clone方式下载
const clone = program.clone || false
// 设置缓存模板目录到用户根目录的路径
const tmp = path.join(home, '.vue-templates', template.replace(/[\/:]/g, '-'))
// 如果指定通过--offline方式,从本地缓存获取模板
if (program.offline) {
  console.log(`> Use cached template at ${chalk.yellow(tildify(tmp))}`)
  template = tmp
}
console.log()
// 监听用户退出事件
process.on('exit', () => {
  console.log()
})
//如果没指定项目名称或者要生成目录的绝对路径中已存在同名目录
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()
}
复制代码

下载模板逻辑

下载主要逻辑是判断是否从缓存中的模板生成目录,还是第三方或者官方远程仓库读取模板仓库并进行下载,中间对以往废弃的官方模板进行检测,并进行提示。

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 { // 通过网络安装方式
    //检查是否存在新版本的vue-cli,当然这里已经存在vue-cli更新
    checkVersion(() => {
      // 如果是官方模板
      if (!hasSlash) {
        // 指定官方模板git地址
        const officialTemplate = 'vuejs-templates/' + template
        // 模板名称不存在'#'
        if (template.indexOf('#') !== -1) {
          //下载生成模板
          downloadAndGenerate(officialTemplate)
        } else {
          //检查模板名是带有 '-2.0'
          if (template.indexOf('-2.0') !== -1) {
            //如果模板名存在`-2.0`,提示之前版本已废弃,不需指定-2.0
            warnings.v2SuffixTemplatesDeprecated(template, inPlace ? '' : name)
            return
          }
          downloadAndGenerate(officialTemplate)
        }
      } else {
        // 第三方模板下载
        downloadAndGenerate(template)
      }
    })
  }
}
复制代码

这里原先对于downloadAndGenerate对模板进行下载的时候对于本地缓存已存在的模板进行删除比较迷惑,后来也想通了。可能是作者想把脚手架和仓库模板解耦,方便对模板仓库随时进行更新。这样用户就可以每次通过网络下载的时候得到的都是最新的模板。

function downloadAndGenerate (template) {
  //设置下载模板中提示信息
  const spinner = ora('downloading template')
  //显示loading
  spinner.start()
  // 如果本地已存在模板,则进行删除
  if (exists(tmp)) rm(tmp)
  // 下载模板名
  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)
    })
  })
}
复制代码

7 gernarate文件

Metalsmith是一个静态网站生成器,主要的执行步骤包括读取前端模板的meta.js或者meta.json模板配置(其中包括是否定义询问的问题和文件过滤的配置),然后根据模板配置过滤不需要的类库和文件,最后生成我们想要的项目文件。

generate依赖

  • chalk: bash打印颜色高亮类库
  • Metalsmith: 静态网站生成器
  • Handlebars: 前端模板引擎,类似的还有ejs
  • async:支持异步处理的模块
  • consolidate:支持各种模板引擎的渲染
  • multimatch:支持各种条件匹配的类库
  • getOptions: 用于读取meta.js中的模板配置
  • ask: 处理metajs中定义的询问问题
  • filter:过滤文件
  • logger: 打印信息
const chalk = require('chalk')
const Metalsmith = require('metalsmith')
const Handlebars = require('handlebars')
const async = require('async')
const render = require('consolidate').handlebars.render
const path = require('path')
const multimatch = require('multimatch')
const getOptions = require('./options')
const ask = require('./ask')
const filter = require('./filter')
const logger = require('./logger')
复制代码

模板辅助函数和生成函数

在generate.js中为模板定义模板渲染的辅助函数,并注入到模板配置中,这样就可以使用询问收集的所需类库来条件式渲染模板变量和过滤文件。

辅助函数

辅助函数可以接受多个参数,最后一个参数 options 为辅助函数的钩子,调用 options.fn(this) 即输出该辅助函数运算结果为真时的内容,反之调用 options.inverse(this) 。

  // 如果a和b两个变量相等
  Handlebars.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)
  })

复制代码

生成函数

传入的参数name是代表创建的目录名,src是模板的路径,dest是项目生成的路径,done是vue-init中定义的错误处理函数(如果发生错误就打印错误信息,成功就打印成功的信息)。

module.exports = function generate (name, src, dest, done) {
  // 读取模板的配置变量
  const opts = getOptions(name, src)
  // 初始化Metalsmith,这里可以看出vue模板定制的项目主要在template目录
  const metalsmith = Metalsmith(path.join(src, 'template'))
  // 获取metalsmith中的变量并注入一些自定义的变量
  const data = Object.assign(metalsmith.metadata(), {
    destDirName: name,
    inPlace: dest === process.cwd(),
    noEscape: true
  })
  // 模板文件中是否存在定义的辅助函数,如果存在就遍历注册辅助函数
  opts.helpers && Object.keys(opts.helpers).map(key => {
    Handlebars.registerHelper(key, opts.helpers[key])
  })

  const helpers = { chalk, logger }
  // 如果模板配置中存在metalsmith.before函数,那么执行该函数
  if (opts.metalsmith && typeof opts.metalsmith.before === 'function') {
    opts.metalsmith.before(metalsmith, opts, helpers)
  }
  // 询问meta.js配置文件中定义的prompts定义好的问题
  metalsmith.use(askQuestions(opts.prompts))
    // 根据上一步收集好是否需要类库的变量,根据条件过滤文件
    .use(filterFiles(opts.filters))
    // 渲染模板文件
    .use(renderTemplateFiles(opts.skipInterpolation))
  // meta.js中是否存在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)
    // 从模板根目录开始构建而不是`./ src`开始,后者是Metalsmith的`source`默认设置
    .source('.') 
    // 在dest路径生成项目文件
    .destination(dest)
    .build((err, files) => {
      done(err)
      // 如果配置文件中存在compltete函数就执行
      if (typeof opts.complete === 'function') {
        const helpers = { chalk, logger, files }
        opts.complete(data, helpers)
      } else {
        //否则打印配置中定义的completeMessage函数
        logMessage(opts.completeMessage, data)
      }
    })

  return data
}
复制代码

自定义metalsmith中间件

在generate中定义了两个metalsmith中间件,一个是askQuestions (用于询问问题),一个是filterFiles(用于根据条件过滤文件)

askQuestions

(files,metalsmith,done)是metalsmith中间件(插件)所需参数和格式,其中ask是自定义的函数,主要处理模板配置文件中的prompts问题。

function askQuestions (prompts) {
  return (files, metalsmith, done) => {
    ask(prompts, metalsmith.metadata(), done)
  }
}
复制代码

下面是ask.js内容

// 支持询问问题的类型
const promptMapping = {
  string: 'input',
  boolean: 'confirm'
}
// prompts是配置中的询问数组,data是metalsmith变量,done是处理函数(同上)
module.exports = function ask (prompts, data, done) {
  async.eachSeries(Object.keys(prompts), (key, next) => {
    prompt(data, key, prompts[key], next)
  }, done)
}


function prompt (data, key, prompt, done) {
  // 跳过不符合条件的提示,evaluate函数是在data的作用域执行exp表达式并返回其执行得到的值
  if (prompt.when && !evaluate(prompt.when, data)) {
    return done()
  }

  let promptDefault = prompt.default
  // 如果prompt的default值是一个函数,执行函数
  if (typeof prompt.default === 'function') {
    promptDefault = function () {
      return prompt.default.bind(this)(data)
    }
  }
  // inquirer收集问题答案
  inquirer.prompt([{
    type: promptMapping[prompt.type] || prompt.type,
    name: key,
    message: prompt.message || prompt.label || key,
    default: promptDefault,
    choices: prompt.choices || [],
    validate: prompt.validate || (() => true)
  }]).then(answers => {
    if (Array.isArray(answers[key])) {
      data[key] = {}
      answers[key].forEach(multiChoiceAnswer => {
        data[key][multiChoiceAnswer] = true
      })
    } else if (typeof answers[key] === 'string') {
      data[key] = answers[key].replace(/"/g, '\\"')
    } else {
      data[key] = answers[key]
    }
    done()
  }).catch(done)
}
复制代码

filterFiles

filterFiles是根据ask.js收集的依赖变量,然后根据meta.js配置文件中定义的filter文件数组,判断是否所需,来过滤没必要的文件。

function filterFiles (filters) {
  return (files, metalsmith, done) => {
    filter(files, filters, metalsmith.metadata(), done)
  }
}

// 用于条件匹配
const match = require('minimatch')
evaluate函数是在data的作用域执行exp表达式并返回其执行得到的值
const evaluate = require('./eval')

module.exports = (files, filters, data, done) => {
  // 如果没有定义filters,直接执行done
  if (!filters) {
    return done()
  }
  // 获取files的路径
  const fileNames = Object.keys(files)
  //获取filters数组定义的路径key并遍历
  Object.keys(filters).forEach(glob => {
    fileNames.forEach(file => {
      // 如果files的路径key和配置文件中定义的路径key值匹配
      if (match(file, glob, { dot: true })) {
        // 获取配置文件中定义的条件判断value
        const condition = filters[glob]
        // 如果条件不匹配,就删除metalsmith遍历的file
        if (!evaluate(condition, data)) {
          delete files[file]
        }
      }
    })
  })
  done()
}
复制代码

renderTemplateFiles 渲染模板文件

renderTemplateFiles函数中定义了renderTemplateFiles,这个我在vue官方的webpack模板中没有发现这个变量的定义,这里主要用作定义需要跳过不处理的文件,renderTemplateFiles可以是一个字符串路径,也可以是一个数组

function renderTemplateFiles (skipInterpolation) {
  skipInterpolation = typeof skipInterpolation === 'string'
    ? [skipInterpolation]
    : skipInterpolation
  return (files, metalsmith, done) => {
    const keys = Object.keys(files)
    //获取metalsmith的变量
    const metalsmithMetadata = metalsmith.metadata()
    // 异步处理,如果有定义需要跳过的文件,并且和当前遍历的文件相匹配,则执行下一个
    async.each(keys, (file, next) => {
      // skipping files with skipInterpolation option
      if (skipInterpolation && multimatch([file], skipInterpolation, { dot: true }).length) {
        return next()
      }
      //获取当前遍历文件file的内容(字符串形式)
      const str = files[file].contents.toString()
      // 如果当前文件内容不含有handlerbars的变量定义语法,则跳过该文件
      if (!/{{([^{}]+)}}/g.test(str)) {
        return next()
      }
      // 把文件中定义的变量替换成metalsmith的变量值
      render(str, metalsmithMetadata, (err, res) => {
        if (err) {
          err.message = `[${file}] ${err.message}`
          return next(err)
        }
        // 然后对文件内容重新赋值,这里new Buffer内容已经被nodejs舍弃,可以采用new Buffer.from(res)的用法
        files[file].contents = new Buffer(res)
        next()
      })
    }, done)
  }
}
复制代码

8 options读取配置

  • read-metadata: 读取meta.js的配置的库
  • getGitUser: 获取本地的git配置
  • validate-npm-package-name: 验证npm包名
  • fs.existsSync: 判断文件是否存在
// 读取meta.js的配置
module.exports = function options (name, dir) {
  const opts = getMetadata(dir)
  // 设置prompt选项的默认值
  setDefault(opts, 'name', name)
  // 验证name字段是否正确
  setValidateName(opts)
  // 获取本地的git配置
  const author = getGitUser()
  // 如果git中定义了author,则设定作者的选项为author提示
  if (author) {
    setDefault(opts, 'author', author)
  }

  return opts
}

function getMetadata (dir) {
// 配置文件可以命名为meta.json或者meta.js
  const json = path.join(dir, 'meta.json')
  const js = path.join(dir, 'meta.js')
  let opts = {}

  if (exists(json)) {
    opts = metadata.sync(json)
  } else if (exists(js)) {
    const req = require(path.resolve(js))
    if (req !== Object(req)) {
      throw new Error('meta.js needs to expose an object')
    }
    opts = req
  }

  return opts
}
复制代码

9 git-user

git-user主要使用了nodejs的exec运行本地git命令获取git设置中的emailuser

const exec = require('child_process').execSync

module.exports = () => {
  let name
  let email

  try {
    name = exec('git config --get user.name')
    email = exec('git config --get user.email')
  } catch (e) {}

  name = name && JSON.stringify(name.toString().trim()).slice(1, -1)
  email = email && (' <' + email.toString().trim() + '>')
  return (name || '') + (email || '')
}
复制代码

总结上面介绍,主要对部分主要的代码进行了解释,对于一些感觉比较好懂和对主流程不是太重要的代码进行了删减。大家在读完之后,建议自己动手定制一个项目模板,自己手动定制了一个react + typescriptvue的项目模板。

点赞加关注

  1. 看到这里了就点个赞支持下啦~,你的点赞,我的创作动力
  2. 关注公众号「前端好talk」,好好说说前端经历的故事

本文使用 mdnice 排版



这篇关于手摸手从浅析vue-cli原理到定制前端开发模板的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程