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


    我们知道通过

    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 ')
    3. .description('create a new project powered by vue-cli-service')
    4. .option('-p, --preset ', 'Skip prompts and use saved or remote preset')
    5. .option('-d, --default', 'Skip prompts and use default preset')
    6. .option('-i, --inlinePreset ', 'Skip prompts and use inline JSON string as preset')
    7. .option('-m, --packageManager ', 'Use specified npm client when installing dependencies')
    8. .option('-r, --registry ', '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 ', '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

    欢迎下载使用。

  • 相关阅读:
    npm的配置文件及其路径问题
    Redis集群高可用配置
    安装并设置linux虚拟机ubuntu20.04.6 LTS
    解决postSticky多次收到通知问题
    Linux桌面溯源
    哈希应用: 位图 + 布隆过滤器
    基于变化点 copula 优化算法中的贝叶斯研究(Matlab代码实现)
    Linux下vim的简单使用方式
    如何获取淘宝商品类目API数据接口
    C语言中static关键字用法
  • 原文地址:https://blog.csdn.net/liubangbo/article/details/126546406