当前位置:   article > 正文

模仿dcloudio/uni-preset-vue 自定义基于uni-app的小程序框架

uni-preset-vue

我们知道通过

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文件中添加一行打印:

  1. program
  2. .command('create <app-name>')
  3. .description('create a new project powered by vue-cli-service')
  4. .option('-p, --preset <presetName>', 'Skip prompts and use saved or remote preset')
  5. .option('-d, --default', 'Skip prompts and use default preset')
  6. .option('-i, --inlinePreset <json>', 'Skip prompts and use inline JSON string as preset')
  7. .option('-m, --packageManager <command>', 'Use specified npm client when installing dependencies')
  8. .option('-r, --registry <url>', 'Use specified npm registry when installing dependencies (only for npm)')
  9. .option('-g, --git [message]', 'Force git initialization with initial commit message')
  10. .option('-n, --no-git', 'Skip git initialization')
  11. .option('-f, --force', 'Overwrite target directory if it exists')
  12. .option('--merge', 'Merge target directory if it exists')
  13. .option('-c, --clone', 'Use git clone when fetching remote preset')
  14. .option('-x, --proxy <proxyUrl>', 'Use specified proxy when creating project')
  15. .option('-b, --bare', 'Scaffold project without beginner instructions')
  16. .option('--skipGetStarted', 'Skip displaying "Get started" instructions')
  17. .action((name, options) => {
  18. if (minimist(process.argv.slice(3))._.length > 1) {
  19. 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.'))
  20. }
  21. // --git makes commander to default git to true
  22. if (process.argv.includes('-g') || process.argv.includes('--git')) {
  23. options.forceGit = true
  24. }
  25. console.log("liubbc name: ", name, "; options: ", options)
  26. return
  27. require('../lib/create')(name, options)
  28. })

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

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

就看到了加的打印:

1.1 处理preset.json

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

  1. const creator = new Creator(name, targetDir, getPromptModules())
  2. await creator.create(options)
  3. async create (cliOptions = {}, preset = null){
  4. if (!preset) {
  5. if (cliOptions.preset) {
  6. //因为vue-create 命令 加了 -p 参数 所以走这里,进行解析预设模板
  7. // vue create foo --preset bar
  8. preset = await this.resolvePreset(cliOptions.preset, cliOptions.clone)
  9. } else if (cliOptions.default) {
  10. // vue create foo --default
  11. preset = defaults.presets['Default (Vue 3)']
  12. } else if (cliOptions.inlinePreset) {
  13. // vue create foo --inlinePreset {...}
  14. try {
  15. preset = JSON.parse(cliOptions.inlinePreset)
  16. } catch (e) {
  17. error(`CLI inline preset is not valid JSON: ${cliOptions.inlinePreset}`)
  18. exit(1)
  19. }
  20. } else {
  21. preset = await this.promptAndResolvePreset()
  22. }
  23. }
  24. .......
  25. }

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

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

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

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

  1. module.exports = async function loadPresetFromDir (dir) {
  2. const presetPath = path.join(dir, 'preset.json')
  3. if (!fs.existsSync(presetPath)) {
  4. throw new Error('remote / local preset does not contain preset.json!')
  5. }
  6. const preset = await fs.readJson(presetPath)
  7. // if the preset dir contains generator.js or generator/index.js, we will inject it as a hidden
  8. // plugin so it will be invoked by the generator.
  9. const hasGenerator = fs.existsSync(path.join(dir, 'generator.js')) || fs.existsSync(path.join(dir, 'generator/index.js'))
  10. if (hasGenerator) {
  11. (preset.plugins || (preset.plugins = {}))[dir.replace(/[/]$/, '')] = {
  12. _isPreset: true,
  13. prompts: true
  14. }
  15. }
  16. return preset
  17. }

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

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 预设:

接着往下走:

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

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

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

  1. // generate package.json with plugin dependencies
  2. const pkg = {
  3. name,
  4. version: '0.1.0',
  5. private: true,
  6. devDependencies: {},
  7. ...resolvePkg(context)
  8. }
  9. const deps = Object.keys(preset.plugins)
  10. deps.forEach(dep => {
  11. if (preset.plugins[dep]._isPreset) {
  12. //在这里把预设中的包含_isPreset的插件给过滤掉了
  13. return
  14. }
  15. let { version } = preset.plugins[dep]
  16. if (!version) {
  17. if (isOfficialPlugin(dep) || dep === '@vue/cli-service' || dep === '@vue/babel-preset-env') {
  18. version = isTestOrDebug ? `latest` : `~${latestMinor}`
  19. } else {
  20. version = 'latest'
  21. }
  22. }
  23. pkg.devDependencies[dep] = version
  24. })
  25. // write package.json
  26. await writeFileTree(context, {
  27. 'package.json': JSON.stringify(pkg, null, 2)
  28. })

生成的package.json如下:

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

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

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

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

1.2 加载generator.js

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

  1. // { id: options } => [{ id, apply, options }]
  2. async resolvePlugins (rawPlugins, pkg) {
  3. // ensure cli-service is invoked first
  4. rawPlugins = sortObject(rawPlugins, ['@vue/cli-service'], true)
  5. const plugins = []
  6. for (const id of Object.keys(rawPlugins)) {
  7. const apply = loadModule(`${id}/generator`, this.context) || (() => {})
  8. let options = rawPlugins[id] || {}
  9. console.log("liubbc id: ", id, "; options: ", options)
  10. if (options.prompts) {
  11. let pluginPrompts = loadModule(`${id}/prompts`, this.context)
  12. if (pluginPrompts) {
  13. const prompt = inquirer.createPromptModule()
  14. if (typeof pluginPrompts === 'function') {
  15. pluginPrompts = pluginPrompts(pkg, prompt)
  16. }
  17. if (typeof pluginPrompts.getPrompts === 'function') {
  18. pluginPrompts = pluginPrompts.getPrompts(pkg, prompt)
  19. }
  20. log()
  21. log(`${chalk.cyan(options._isPreset ? `Preset options:` : id)}`)
  22. options = await prompt(pluginPrompts)
  23. }
  24. }
  25. plugins.push({ id, apply, options })
  26. }
  27. return plugins
  28. }

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

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

1.3 执行generator.js

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

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

  1. const generator = new Generator(context, {
  2. pkg,
  3. plugins,
  4. afterInvokeCbs,
  5. afterAnyInvokeCbs
  6. })
  7. await generator.generate({
  8. extractConfigFiles: preset.useConfigFiles
  9. })

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

  1. async generate ({
  2. extractConfigFiles = false,
  3. checkExisting = false,
  4. sortPackageJson = true
  5. } = {}) {
  6. await this.initPlugins()
  7. // save the file system before applying plugin for comparison
  8. const initialFiles = Object.assign({}, this.files)
  9. // extract configs from package.json into dedicated files.
  10. this.extractConfigFiles(extractConfigFiles, checkExisting)
  11. // wait for file resolve
  12. await this.resolveFiles()
  13. // set package.json
  14. if (sortPackageJson) {
  15. this.sortPkg()
  16. }
  17. this.files['package.json'] = JSON.stringify(this.pkg, null, 2) + '\n'
  18. // write/update file tree to disk
  19. await writeFileTree(this.context, this.files, initialFiles, this.filesModifyRecord)
  20. }
  1. async initPlugins () {
  2. const { rootOptions, invoking } = this
  3. const pluginIds = this.plugins.map(p => p.id)
  4. // avoid modifying the passed afterInvokes, because we want to ignore them from other plugins
  5. const passedAfterInvokeCbs = this.afterInvokeCbs
  6. this.afterInvokeCbs = []
  7. // apply hooks from all plugins to collect 'afterAnyHooks'
  8. for (const plugin of this.allPlugins) {
  9. const { id, apply } = plugin
  10. const api = new GeneratorAPI(id, this, {}, rootOptions)
  11. if (apply.hooks) {
  12. await apply.hooks(api, {}, rootOptions, pluginIds)
  13. }
  14. }
  15. // We are doing save/load to make the hook order deterministic
  16. // save "any" hooks
  17. const afterAnyInvokeCbsFromPlugins = this.afterAnyInvokeCbs
  18. // reset hooks
  19. this.afterInvokeCbs = passedAfterInvokeCbs
  20. this.afterAnyInvokeCbs = []
  21. this.postProcessFilesCbs = []
  22. // apply generators from plugins
  23. for (const plugin of this.plugins) {
  24. const { id, apply, options } = plugin
  25. const api = new GeneratorAPI(id, this, options, rootOptions)
  26. await apply(api, options, rootOptions, invoking)
  27. if (apply.hooks) {
  28. // while we execute the entire `hooks` function,
  29. // only the `afterInvoke` hook is respected
  30. // because `afterAnyHooks` is already determined by the `allPlugins` loop above
  31. await apply.hooks(api, options, rootOptions, pluginIds)
  32. }
  33. }
  34. // restore "any" hooks
  35. this.afterAnyInvokeCbs = afterAnyInvokeCbsFromPlugins
  36. }

最终在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 如下所示:

  1. const fs = require('fs')
  2. const path = require('path')
  3. const isBinary = require('isbinaryfile')
  4. async function generate(dir, files, base = '', rootOptions = {}) {
  5. const glob = require('glob')
  6. glob.sync('**/*', {
  7. cwd: dir,
  8. nodir: true
  9. }).forEach(rawPath => {
  10. const sourcePath = path.resolve(dir, rawPath)
  11. const filename = path.join(base, rawPath)
  12. if (isBinary.sync(sourcePath)) {
  13. files[filename] = fs.readFileSync(sourcePath) // return buffer
  14. } else {
  15. let content = fs.readFileSync(sourcePath, 'utf-8')
  16. if (path.basename(filename) === 'manifest.json') {
  17. content = content.replace('{{name}}', rootOptions.projectName || '')
  18. }
  19. if (filename.charAt(0) === '_' && filename.charAt(1) !== '_') {
  20. files[`.${filename.slice(1)}`] = content
  21. } else if (filename.charAt(0) === '_' && filename.charAt(1) === '_') {
  22. files[`${filename.slice(1)}`] = content
  23. } else {
  24. files[filename] = content
  25. }
  26. }
  27. })
  28. }
  29. module.exports = (api, options, rootOptions) => {
  30. api.extendPackage(pkg => {
  31. return {
  32. dependencies: {
  33. 'regenerator-runtime': '^0.12.1',// 锁定版本,避免高版本在小程序中出错
  34. '@dcloudio/uni-helper-json': '*',
  35. "@dcloudio/uni-ui": "^1.4.19",
  36. "core-js": "^3.6.5",
  37. "flyio": "^0.6.2",
  38. "uview-ui": "^2.0.31",
  39. "vue": "^2.6.11",
  40. "vuex": "^3.2.0"
  41. },
  42. devDependencies: {
  43. "@babel/runtime": "~7.17.9",// 临时指定版本,7.13.x 会报错
  44. 'postcss-comment': '^2.0.0',
  45. '@dcloudio/types': '^3.0.4',
  46. 'miniprogram-api-typings': '*',
  47. 'mini-types': '*',
  48. "cross-env": "^7.0.2",
  49. "glob": "^8.0.3",
  50. "jest": "^25.4.0",
  51. "open-devtools": "^0.2.1",
  52. "sass": "^1.53.0",
  53. "sass-loader": "^10.3.1",
  54. "vue-template-compiler": "^2.6.11"
  55. },
  56. resolutions: {
  57. "@babel/runtime": "~7.17.9"
  58. }
  59. }
  60. })
  61. api.render(async function (files) {
  62. Object.keys(files).forEach(name => {
  63. delete files[name]
  64. })
  65. const base = 'src'
  66. await generate(path.resolve(__dirname, './template/common'), files)
  67. await generate(path.resolve(__dirname, './template/default'), files, base, rootOptions)
  68. })
  69. }

最后在说一下使用方式:

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

欢迎下载使用。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Cpp五条/article/detail/557139
推荐阅读
相关标签
  

闽ICP备14008679号