赞
踩
我们知道通过
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
欢迎下载使用。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。