我们知道通过

vue create -p dcloudio/uni-preset-vue my-project

可以创建小程序框架。这个是DCloud 基于vue-cli 来实现的uni-app框架。使用过的都知道,有好几个模板进行选择。如果我们有自己的项目框架,如何做成预设模板,一行命令一键生成项目框架呢?例如提供给其他团队项目框架,只需要告诉一下命令就可以了,方便不少。

这个功能是通过vue-cli  create 命令, -p 参数实现的,下面我们先分析一下整个流程。

1. vue create -p dcloudio/uni-preset-vue my-project 流程

要分析清楚这个流程,肯定需要调试,分享一个笨方法。局部安装vue-cli 然后在node-module/@vue/cli目录下添加log 即可。例如:新建vue-cli-local 目录 在此目录下 执行

npm install @vue/cli

局部安装vue-cli 。我们用vscode 加载vue-cli-local 目录 在node-module/@vue/cli/bin/vue.js文件中添加一行打印:

program
  .command('create <app-name>')
  .description('create a new project powered by vue-cli-service')
  .option('-p, --preset <presetName>', 'Skip prompts and use saved or remote preset')
  .option('-d, --default', 'Skip prompts and use default preset')
  .option('-i, --inlinePreset <json>', 'Skip prompts and use inline JSON string as preset')
  .option('-m, --packageManager <command>', 'Use specified npm client when installing dependencies')
  .option('-r, --registry <url>', 'Use specified npm registry when installing dependencies (only for npm)')
  .option('-g, --git [message]', 'Force git initialization with initial commit message')
  .option('-n, --no-git', 'Skip git initialization')
  .option('-f, --force', 'Overwrite target directory if it exists')
  .option('--merge', 'Merge target directory if it exists')
  .option('-c, --clone', 'Use git clone when fetching remote preset')
  .option('-x, --proxy <proxyUrl>', 'Use specified proxy when creating project')
  .option('-b, --bare', 'Scaffold project without beginner instructions')
  .option('--skipGetStarted', 'Skip displaying "Get started" instructions')
  .action((name, options) => {
    if (minimist(process.argv.slice(3))._.length > 1) {
      console.log(chalk.yellow('\n Info: You provided more than one argument. The first one will be used as the app\'s name, the rest are ignored.'))
    }
    // --git makes commander to default git to true
    if (process.argv.includes('-g') || process.argv.includes('--git')) {
      options.forceGit = true
    }
    console.log("liubbc name: ", name, "; options: ", options)
    return
    require('../lib/create')(name, options)
  })

在vue-cli-local目录下执行如下命令:

node node_modules/@vue/cli/bin/vue.js create -p  dcloudio/uni-preset-vue  my-project

就看到了加的打印:

1.1 处理preset.json

 按此方法继续往下看代码:

  const creator = new Creator(name, targetDir, getPromptModules())   
  await creator.create(options)

  async create (cliOptions = {}, preset = null){
    if (!preset) {
      if (cliOptions.preset) {
        //因为vue-create 命令 加了 -p 参数 所以走这里,进行解析预设模板
        // vue create foo --preset bar
        preset = await this.resolvePreset(cliOptions.preset, cliOptions.clone)
      } else if (cliOptions.default) {
        // vue create foo --default
        preset = defaults.presets['Default (Vue 3)']
      } else if (cliOptions.inlinePreset) {
        // vue create foo --inlinePreset {...}
        try {
          preset = JSON.parse(cliOptions.inlinePreset)
        } catch (e) {
          error(`CLI inline preset is not valid JSON: ${cliOptions.inlinePreset}`)
          exit(1)
        }
      } else {
        preset = await this.promptAndResolvePreset()
      }
    }
    .......
  }

如代码中的注释所示加了vue create 命令加了-p 参数所以去解析预设模板。

解析预设模板代码调用如下:

resolvePreset -> loadRemotePreset -> require('download-git-repo') -> loadPresetFromDir

通过download-git-repo 库去下载远程模板,下载的模板放在一个临时文件夹中。模板必须包含preset.json文件,否则会报错退出。还检查是否包含generator.js 文件,如果包含则为preset的plugins字段再添加两个字段: _isPreset: true,  prompts: true 代码如下:

module.exports = async function loadPresetFromDir (dir) {
  const presetPath = path.join(dir, 'preset.json')
  if (!fs.existsSync(presetPath)) {
    throw new Error('remote / local preset does not contain preset.json!')
  }
  const preset = await fs.readJson(presetPath)

  // if the preset dir contains generator.js or generator/index.js, we will inject it as a hidden
  // plugin so it will be invoked by the generator.
  const hasGenerator = fs.existsSync(path.join(dir, 'generator.js')) || fs.existsSync(path.join(dir, 'generator/index.js'))
  if (hasGenerator) {
    (preset.plugins || (preset.plugins = {}))[dir.replace(/[/]$/, '')] = {
      _isPreset: true,
      prompts: true
    }
  }

  return preset
}

从字面意思看,这两个字段说明这个插件是预设模板,且有提示符供选择。

download-git-repo 支持gitlab github,这次我们预设模板放在gitee上,所以不能像

vue create -p dcloudio/uni-preset-vue my-project  这样使用。但可以使用原始URL的方式,加direct前缀,--clone选项,如下所示:

vue create -p direct:https://gitee.com/liubangbo/mp-d-tab-preset.git  --clone my-project

走到这里,我们就可以新建一个空预设模板放到gitee上,当然要包含preset.json文件,否则会报错。

我们打印看一下 dcloudio/uni-preset-vue 预设:

接着往下走:

    // clone before mutating
    preset = cloneDeep(preset)
    // inject core service
    preset.plugins['@vue/cli-service'] = Object.assign({
      projectName: name
    }, preset)

给preset plugins字段注入@vue/cli-service字段。注意vue-cli 有2大部分,一部分就是cli  另一部分就是cli-service,这里就不详细描述了。

接下来就是生成就是创建项目目录,并生成package.json

    // generate package.json with plugin dependencies
    const pkg = {
      name,
      version: '0.1.0',
      private: true,
      devDependencies: {},
      ...resolvePkg(context)
    }
    const deps = Object.keys(preset.plugins)
    deps.forEach(dep => {
      if (preset.plugins[dep]._isPreset) {
        //在这里把预设中的包含_isPreset的插件给过滤掉了
        return
      }

      let { version } = preset.plugins[dep]

      if (!version) {
        if (isOfficialPlugin(dep) || dep === '@vue/cli-service' || dep === '@vue/babel-preset-env') {
          version = isTestOrDebug ? `latest` : `~${latestMinor}`
        } else {
          version = 'latest'
        }
      }

      pkg.devDependencies[dep] = version
    })    

    // write package.json
    await writeFileTree(context, {
      'package.json': JSON.stringify(pkg, null, 2)
    })

生成的package.json如下:

 后面就是Initializing git repository, install 这些依赖,这样就把cli-service 给安装上了。

走到这里注意一下,下载的预设只处理了preset.json文件,并没有处理generator.js文件。

--------------------------------------------------------------------------------------------------------------------------------

下面就进入复杂的部分,开始处理generator.js了。

1.2 加载generator.js

resolvePlugins处理了generator.js,我们添加了打印,把id, 和options都打印出来看下。

  // { id: options } => [{ id, apply, options }]
  async resolvePlugins (rawPlugins, pkg) {
    // ensure cli-service is invoked first
    rawPlugins = sortObject(rawPlugins, ['@vue/cli-service'], true)
    const plugins = []
    for (const id of Object.keys(rawPlugins)) {      
      const apply = loadModule(`${id}/generator`, this.context) || (() => {})
      let options = rawPlugins[id] || {}
      console.log("liubbc id: ", id, "; options: ", options)
      if (options.prompts) {
        let pluginPrompts = loadModule(`${id}/prompts`, this.context)

        if (pluginPrompts) {
          const prompt = inquirer.createPromptModule()

          if (typeof pluginPrompts === 'function') {
            pluginPrompts = pluginPrompts(pkg, prompt)
          }
          if (typeof pluginPrompts.getPrompts === 'function') {
            pluginPrompts = pluginPrompts.getPrompts(pkg, prompt)
          }

          log()
          log(`${chalk.cyan(options._isPreset ? `Preset options:` : id)}`)
          options = await prompt(pluginPrompts)
        }
      }

      plugins.push({ id, apply, options })
    }
    return plugins
  }

 由于dcloudio/uni-preset-vue 预设 有prompts文件,所以会弹出提示供用户选择。我们一般只有一个项目框架,不需要prompts,这里就不分析了。

注意从代码看这里只是load generator.js 赋值给了plugins,并没有执行generator.js。

1.3 执行generator.js

看下处理过的plugins长什么样:

有3个字段,id, apply, options 。 下面就要分析一下 apply在哪里执行的。

    const generator = new Generator(context, {
      pkg,
      plugins,
      afterInvokeCbs,
      afterAnyInvokeCbs
    })
    
    await generator.generate({
      extractConfigFiles: preset.useConfigFiles
    })

创建Generator实例,并执行generate方法。

  async generate ({
    extractConfigFiles = false,
    checkExisting = false,
    sortPackageJson = true
  } = {}) {
    await this.initPlugins()

    // save the file system before applying plugin for comparison
    const initialFiles = Object.assign({}, this.files)
    // extract configs from package.json into dedicated files.
    this.extractConfigFiles(extractConfigFiles, checkExisting)
    // wait for file resolve
    await this.resolveFiles()
    // set package.json
    if (sortPackageJson) {
      this.sortPkg()
    }
    this.files['package.json'] = JSON.stringify(this.pkg, null, 2) + '\n'
    // write/update file tree to disk
    await writeFileTree(this.context, this.files, initialFiles, this.filesModifyRecord)
  }
  async initPlugins () {
    const { rootOptions, invoking } = this
    const pluginIds = this.plugins.map(p => p.id)

    // avoid modifying the passed afterInvokes, because we want to ignore them from other plugins
    const passedAfterInvokeCbs = this.afterInvokeCbs
    this.afterInvokeCbs = []
    // apply hooks from all plugins to collect 'afterAnyHooks'
    for (const plugin of this.allPlugins) {
      const { id, apply } = plugin
      const api = new GeneratorAPI(id, this, {}, rootOptions)

      if (apply.hooks) {
        await apply.hooks(api, {}, rootOptions, pluginIds)
      }
    }

    // We are doing save/load to make the hook order deterministic
    // save "any" hooks
    const afterAnyInvokeCbsFromPlugins = this.afterAnyInvokeCbs

    // reset hooks
    this.afterInvokeCbs = passedAfterInvokeCbs
    this.afterAnyInvokeCbs = []
    this.postProcessFilesCbs = []

    // apply generators from plugins
    for (const plugin of this.plugins) {
      const { id, apply, options } = plugin
      const api = new GeneratorAPI(id, this, options, rootOptions)      
      await apply(api, options, rootOptions, invoking)

      if (apply.hooks) {
        // while we execute the entire `hooks` function,
        // only the `afterInvoke` hook is respected
        // because `afterAnyHooks` is already determined by the `allPlugins` loop above
        await apply.hooks(api, options, rootOptions, pluginIds)
      }
    }
    // restore "any" hooks
    this.afterAnyInvokeCbs = afterAnyInvokeCbsFromPlugins
  }

最终在initPlugins方法中执行了apply方法,也就是预设模板中generator.js中导出的方法。

走到这里执行generator.js的地方就找到了。分析dcloudio/uni-preset-vue (

GitHub - dcloudio/uni-preset-vue: uni-app preset for vue)可以看出,基本上就是扩展package.json中的内容,以及拷贝项目框架文件。

在这里注意3点:

1)要新建一个node 工程,在package.json中安装用到的node 库 例如,glob等。并且需要把node_modules上传

2)在放项目框架文件的时候要注意,/template/common  里面的文件是和src文件平级的

     src/目录下的文件要放到/template/default/目录下

3).env .ignore文件要修改一下名称 改为 _env  _ignore

我们仿照generator.js 如下所示:

const fs = require('fs')
const path = require('path')

const isBinary = require('isbinaryfile')

async function generate(dir, files, base = '', rootOptions = {}) {  
  const glob = require('glob')
  glob.sync('**/*', {
    cwd: dir,
    nodir: true
  }).forEach(rawPath => {    
    const sourcePath = path.resolve(dir, rawPath)
    const filename = path.join(base, rawPath)    
    if (isBinary.sync(sourcePath)) {
      files[filename] = fs.readFileSync(sourcePath) // return buffer
    } else {
      let content = fs.readFileSync(sourcePath, 'utf-8')
      if (path.basename(filename) === 'manifest.json') {
        content = content.replace('{{name}}', rootOptions.projectName || '')
      }
      if (filename.charAt(0) === '_' && filename.charAt(1) !== '_') {
        files[`.${filename.slice(1)}`] = content
      } else if (filename.charAt(0) === '_' && filename.charAt(1) === '_') {
        files[`${filename.slice(1)}`] = content
      } else {
        files[filename] = content
      }
    }
  })
}

module.exports = (api, options, rootOptions) => {
  api.extendPackage(pkg => {
    return {
      dependencies: {
        'regenerator-runtime': '^0.12.1',// 锁定版本,避免高版本在小程序中出错
        '@dcloudio/uni-helper-json': '*',
        "@dcloudio/uni-ui": "^1.4.19",
        "core-js": "^3.6.5",
        "flyio": "^0.6.2",
        "uview-ui": "^2.0.31",
        "vue": "^2.6.11",
        "vuex": "^3.2.0"
      },
      devDependencies: {
        "@babel/runtime": "~7.17.9",// 临时指定版本,7.13.x 会报错
        'postcss-comment': '^2.0.0',
        '@dcloudio/types': '^3.0.4',
        'miniprogram-api-typings': '*',
        'mini-types': '*',
        "cross-env": "^7.0.2",
        "glob": "^8.0.3",
        "jest": "^25.4.0",
        "open-devtools": "^0.2.1",
        "sass": "^1.53.0",
        "sass-loader": "^10.3.1",
        "vue-template-compiler": "^2.6.11"
      },
      resolutions: {
        "@babel/runtime": "~7.17.9"
      }
    }
  })
  
  api.render(async function (files) {
    Object.keys(files).forEach(name => {
      delete files[name]
    })

    const base = 'src'
    await generate(path.resolve(__dirname, './template/common'), files)
    await generate(path.resolve(__dirname, './template/default'), files, base, rootOptions)    
  })
}

最后在说一下使用方式:

vue create -p direct:https://gitee.com/liubangbo/mp-d-tab-preset.git  --clone my-project

欢迎下载使用。

Logo

前往低代码交流专区

更多推荐