Vue CLI can easily create a Vue project, but it is not enough for the actual project. Therefore, some common capabilities will be added based on the business situation to reduce some repetitive operations when creating a new project. For the purpose of learning and sharing, this paper will introduce the front-end architecture design of our Vue project. Of course, Some places may not be the best way. After all, everyone's business is different. What suits you is the best.
In addition to the basic architecture design, this article will also introduce how to develop a Vue CLI plug-in and preset.
ps.Based on Vue2.x edition, node Version 16.5.0
Create a basic project
First create a basic project using Vue CLI:
vue create hello-world
Then select the Vue2 option to create, and the initial project structure is as follows:
The next step is to add bricks and tiles on this basis.
route
Routing is essential. Install Vue Router:
npm install vue-router
Modify app Vue file:
<template> <div id="app"> <router-view /> </div> </template> <script> export default { name: 'App', } </script> <style> * { padding: 0; margin: 0; border: 0; outline: none; } html, body { width: 100%; height: 100%; } </style> <style scoped> #app { width: 100%; height: 100%; display: flex; } </style>
Add a routing exit and simply set the page style.
Next, add the pages directory to place the page and put the original app Vue's content moved to hello vue:
For routing configuration, we choose to configure based on the file, and create a new / src / router in the src directory config. js:
export default [ { path: '/', redirect: '/hello', }, { name: 'hello', path: '/hello/', component: 'Hello', } ]
Property supports the Vue router build option routes The component attribute passes the component path under the pages directory, which stipulates that routing components can only be placed under the pages directory, and then create a new / SRC / router JS file:
import Vue from 'vue' import Router from 'vue-router' import routes from './router.config.js' Vue.use(Router) const createRoute = (routes) => { if (!routes) { return [] } return routes.map((item) => { return { ...item, component: () => { return import('./pages/' + item.component) }, children: createRoute(item.children) } }) } const router = new Router({ mode: 'history', routes: createRoute(routes), }) export default router
Using factory functions and import methods to define dynamic components requires recursive processing of sub routes. Finally, in main Introducing routes into JS:
// main.js // ... import router from './router'// ++ // ... new Vue({ router,// ++ render: h => h(App), }).$mount('#app')
menu
Our business basically needs a menu, which is displayed on the left side of the page by default. We have an internal component library, but there is no external open source, so this paper uses Element instead, and the menu is also configured through files, creating / SRC / NAV config. JS file:
export default [{ title: 'hello', router: '/hello', icon: 'el-icon-menu' }]
Then modify the app Vue file:
<template> <div id="app"> <el-menu style="width: 250px; height: 100%" :router="true" :default-active="defaultActive" > <el-menu-item v-for="(item, index) in navList" :key="index" :index="item.router" > <i :class="item.icon"></i> <span slot="title">{{ item.title }}</span> </el-menu-item> </el-menu> <router-view /> </div> </template> <script> import navList from './nav.config.js' export default { name: 'App', data() { return { navList, } }, computed: { defaultActive() { let path = this.$route.path // Check for an exact match let fullMatch = navList.find((item) => { return item.router === path }) // If not, check whether there is a partial match if (!fullMatch) { fullMatch = navList.find((item) => { return new RegExp('^' + item.router + '/').test(path) }) } return fullMatch ? fullMatch.router : '' }, }, } </script>
The effect is as follows:
Of course, the above is just a hint. The actual situation is more complicated. After all, the nested menu is not considered here.
jurisdiction
Our permission granularity is relatively large, which is only controlled to the routing level. The specific implementation is to add a code field to each item in the menu configuration and routing configuration, and then obtain the code that the current user has permission through request. The menu without permission is not displayed by default, and the access to the route without permission will be redirected to page 403.
Get permission data
The permission data is returned together with the user information interface and then stored in vuex, so first configure vuex and install:
npm install vuex --save
Add / SRC / store js:
import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) export default new Vuex.Store({ state: { userInfo: null, }, actions: { // Request user information async getUserInfo(ctx) { let userInfo = { // ... code: ['001'] // User owned permissions } ctx.commit('setUserInfo', userInfo) } }, mutations: { setUserInfo(state, userInfo) { state.userInfo = userInfo } }, })
In main JS, and then initialize Vue:
// ... import store from './store' // ... const initApp = async () => { await store.dispatch('getUserInfo') new Vue({ router, store, render: h => h(App), }).$mount('#app') } initApp()
menu
Modify NAV config. JS new code field:
// nav.config.js export default [{ title: 'hello', router: '/hello', icon: 'el-icon-menu' code: '001', }]
Then in app Filter out unauthorized menus in Vue:
export default { name: 'App', data() { return { navList,// -- } }, computed: { navList() {// ++ const { userInfo } = this.$store.state if (!userInfo || !userInfo.code || userInfo.code.length <= 0) return [] return navList.filter((item) => { return userInfo.code.includes(item.code) }) } } }
In this way, the menu without permission will not be displayed.
route
Modify router config. JS, add code field:
export default [{ path: '/', redirect: '/hello', }, { name: 'hello', path: '/hello/', component: 'Hello', code: '001', } ]
code is a custom field and needs to be saved in the meta field of the routing record, otherwise it will be lost. Modify the createRoute method:
// router.js // ... const createRoute = (routes) => { // ... return routes.map((item) => { return { ...item, component: () => { return import('./pages/' + item.component) }, children: createRoute(item.children), meta: {// ++ code: item.code } } }) } // ...
Then you need to intercept the route jump and judge whether you have permission. If you don't have permission, go to page 403:
// router.js // ... import store from './store' // ... router.beforeEach((to, from, next) => { const userInfo = store.state.userInfo const code = userInfo && userInfo.code && userInfo.code.length > 0 ? userInfo.code : [] // Go to the error page and jump directly, otherwise it will cause a dead cycle if (/^\/error\//.test(to.path)) { return next() } // Have permission to jump directly if (code.includes(to.meta.code)) { next() } else if (to.meta.code) { // The route exists and has no permission. Jump to page 403 next({ path: '/error/403' }) } else { // If there is no code, it means it is an illegal path. Jump to page 404 next({ path: '/error/404' }) } })
The error component has not been added yet. Add the following:
// pages/Error.vue <template> <div class="container">{{ errorText }}</div> </template> <script> const map = { 403: 'No permission', 404: 'Page does not exist', } export default { name: 'Error', computed: { errorText() { return map[this.$route.params.type] || 'unknown error' }, }, } </script> <style scoped> .container { width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; font-size: 50px; } </style>
Next, modify the router config. JS, add the route of the error page, and add a route without permission for testing:
// router.config.js export default [ // ... { name: 'Error', path: '/error/:type', component: 'Error', }, { name: 'hi', path: '/hi/', code: 'No permission test, please enter hi', component: 'Hello', } ]
Because this code user does not have a password, now we open the / hi route and directly jump to route 403:
crumbs
Similar to the menu, bread crumbs are also required by most pages. The composition of bread crumbs is divided into two parts: one is the position in the current menu, and the other is the path generated in page operation. Because the path of the first part may change dynamically, it is generally obtained along with the user information through the interface, and then stored in vuex to modify the store js:
// ... async getUserInfo(ctx) { let userInfo = { code: ['001'], breadcrumb: {// Add breadcrumb data '001': ['Hello'], }, } ctx.commit('setUserInfo', userInfo) } // ...
The second part is in router config. JS configuration:
export default [ //... { name: 'hello', path: '/hello/', component: 'Hello', code: '001', breadcrumb: ['world'],// ++ } ]
The breadcrumb field, like the code field, is a user-defined field, but the data of this field is used by the component. The component needs to obtain the data of this field and then render the breadcrumb menu on the page. Therefore, although it is OK to save it to the meta field, it is troublesome to obtain it in the component, so we can set it to the props field of the routing record, Directly inject props into components, which is much more convenient to use. Modify router js:
// router.js // ... const createRoute = (routes) => { // ... return routes.map((item) => { return { ...item, component: () => { return import('./pages/' + item.component) }, children: createRoute(item.children), meta: { code: item.code }, props: {// ++ breadcrumbObj: { breadcrumb: item.breadcrumb, code: item.code } } } }) } // ...
In this way, a breadcrumbObj attribute is declared in the component to get the breadcrumb data. It is seen that code is passed along with each other. This is simultaneous interpreting the breadcrumb data of the code from the bread data obtained from the user interface according to the current routing code, and then merging the two parts, in order to avoid making every component do it again. We can write in a global mixin and modify main js:
// ... Vue.mixin({ props: { breadcrumbObj: { type: Object, default: () => null } }, computed: { breadcrumb() { if (!this.breadcrumbObj) { return [] } let { code, breadcrumb } = this.breadcrumbObj // Bread crumb data obtained by user interface let breadcrumbData = this.$store.state.userInfo.breadcrumb // Is there bread crumb data in the current route let firstBreadcrumb = breadcrumbData && Array.isArray(breadcrumbData[code]) ? breadcrumbData[code] : [] // Merge the crumb data of the two parts return firstBreadcrumb.concat(breadcrumb || []) } } }) // ... initApp()
Finally, we are at hello The Vue component renders the following crumbs:
<template> <div class="container"> <el-breadcrumb separator="/"> <el-breadcrumb-item v-for="(item, index) in breadcrumb" :key="index">{{item}}</el-breadcrumb-item> </el-breadcrumb> // ... </div> </template>
Of course, our bread crumbs do not need to support clicking. If necessary, we can modify the data structure of the following bread crumbs.
Interface request
The interface request uses axios, but some basic configuration, request interception and response will be done. Because there are still some scenarios that need to directly use unconfigured axios, we create a new instance by default and install it first:
npm install axios
Then create a new / src/api / directory and add an httpinstance JS file:
import axios from 'axios' // Create a new instance const http = axios.create({ timeout: 10000,// The timeout is set to 10 seconds withCredentials: true,// Whether vouchers need to be used in cross domain requests is set to required headers: { 'X-Requested-With': 'XMLHttpRequest'// Indicates that it is an ajax request }, }) export default http
Then add a request Interceptor:
// ... // request interceptor http.interceptors.request.use(function (config) { // What to do before sending the request return config; }, function (error) { // What to do about request errors return Promise.reject(error); }); // ...
In fact, I didn't do anything. I wrote it first and left different items to modify as needed.
Add last response Interceptor:
// ... import { Message } from 'element-ui' // ... // Response interceptor http.interceptors.response.use( function (response) { // Unified handling of errors if (response.data.code !== '0') { // Pop up error prompt if (!response.config.noMsg && response.data.msg) { Message.error(response.data.msg) } return Promise.reject(response) } else if (response.data.code === '0' && response.config.successNotify && response.data.msg) { // Pop up success prompt Message.success(response.data.msg) } return Promise.resolve({ code: response.data.code, msg: response.data.msg, data: response.data.data, }) }, function (error) { // Login expiration if (error.status === 403) { location.reload() return } // Timeout prompt if (error.message.indexOf('timeout') > -1) { Message.error('Request timed out, please try again!') } return Promise.reject(error) }, ) // ...
We agree on a successful response (status code 200) with the following structure:
{ code: '0', msg: 'xxx', data: xxx }
If the code is not 0, even if the status code is 200, the request error message prompt box will pop up. If you don't want to pop up the prompt box automatically for a request, you can also prohibit it. Just add the configuration parameter noMsg: true when requesting, for example:
axios.get('/xxx', { noMsg: true })
If the request is successful, the prompt will not pop up by default. If necessary, you can set the configuration parameter successNotify: true.
There are only two kinds of errors with status code between non [200300], login expiration and request timeout. Other situations can be modified according to the project.
Multilingual
Multilingual use vue-i18n To implement, install:
npm install vue-i18n@8
vue-i18n 9 The X version supports Vue3, so we use 8 X version.
Then create a directory / src/i18n /, and create a new index JS file is used to create i18n instances:
import Vue from 'vue' import VueI18n from 'vue-i18n' Vue.use(VueI18n) const i18n = new VueI18n() export default i18n
We didn't do anything except create an instance. Don't worry. Let's take it step by step.
Our general idea is that the multi language source data is in / src/i18n /, and then compiled into json files and placed in the / public/i18n / directory of the project. The initial default language of the page is also returned together with the user information interface. The page uses ajax to request the corresponding json file in the public directory according to the default language type, and calls the method of VueI18n for dynamic setting.
The purpose of this is to facilitate the modification of the default language of the page. Secondly, multilingual files are not packaged with the project code, so as to reduce the packaging time, request on demand and unnecessary resource requests.
Next, we will create the Chinese and English data of the new page. The directory structure is as follows:
For example, Chinese hello The JSON file reads as follows (ignoring the author's low-level translation ~):
In index Import hello. JS file JSON file and ElementUI language file, and merge and export:
import hello from './hello.json' import elementLocale from 'element-ui/lib/locale/lang/zh-CN' export default { hello, ...elementLocale }
Why elementLocale, because the multilingual data structure passed to Vue-i18n is as follows:
We put the index JS as vue-i18n's multilingual data, and the multilingual file of ElementUI is as follows:
So we need to combine the attributes and hello attributes of this object into one object.
Next, we need to write the exported data into a json file and output it to the public directory. This can be done by directly writing a js script file. However, in order to separate from the source code of the project, we write an npm package.
Create an npm Toolkit
We create a package directory at the same level of the project and initialize it with npm init:
The reason why it is named - tool is that there may be similar requirements for compiling multiple languages in the future, so it takes a common name to facilitate the addition of other functions later.
Use of command line interactive tools Commander.js , installation:
npm install commander
Then create a new entry file index js:
#!/usr/bin/env node const { program } = require('commander'); // Compiling multilingual files const buildI18n = () => { console.log('Compiling multilingual files'); } program .command('i18n') // Add i18n command .action(buildI18n) program.parse(process.argv);
Because our package is to be used as a command-line tool, the first line of the file needs to specify the interpreter of the script as node, and then use the commander to configure an i18n command to compile multilingual files. Later, if you want to add other functions and add commands, if you have the executable file, we need to add it in the package Add a bin field in the JSON file to indicate that there is an executable file in our package. Let npm create a symbolic link to map the command to the file when installing the package.
// hello-tool/package.json { "bin": { "hello": "./index.js" } }
We haven't published the package directly to the directory of Hello tool, so we haven't published it directly under Hello tool:
npm link
Then go to our hello world Directory and execute:
npm link hello-tool
Now try typing hello i18n on the command line:
Compiling multilingual files
Next, improve the logic of buildI18n function, mainly in three steps:
1. Clear the target directory, i.e. / public/i18n directory
2. Obtain the data exported from various multilingual files under / src/i18n
3. Write to json file and output to / public/i18n directory
The code is as follows:
const path = require('path') const fs = require('fs') // Compiling multilingual files const buildI18n = () => { // Multilingual source directory let srcDir = path.join(process.cwd(), 'src/i18n') // Target directory let destDir = path.join(process.cwd(), 'public/i18n') // 1. Clear the target directory. clearDir is a user-defined method that recursively traverses the directory for deletion clearDir(destDir) // 2. Obtain source multilingual export data let data = {} let langDirs = fs.readdirSync(srcDir) langDirs.forEach((dir) => { let dirPath = path.join(srcDir, dir) // Read / SRC / I18N / xxx / index JS file, get the exported multilingual object and store it on the data object let indexPath = path.join(dirPath, 'index.js') if (fs.statSync(dirPath).isDirectory() && fs.existsSync(indexPath)) { // Use require to load the file module and obtain the exported data data[dir] = require(indexPath) } }) // 3. Write to target directory Object.keys(data).forEach((lang) => { // Create public/i18n directory if (!fs.existsSync(destDir)) { fs.mkdirSync(destDir) } let dirPath = path.join(destDir, lang) let filePath = path.join(dirPath, 'index.json') // Create a multilingual directory if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath) } // Create json file fs.writeFileSync(filePath, JSON.stringify(data[lang], null, 4)) }) console.log('Multilingual compilation completed'); }
The code is very simple. Next, let's run the command:
An error is reported, indicating that import cannot be used outside the module. In fact, the new version of nodejs already supports the module syntax of ES6. You can replace the file suffix with mjs, or in package The type=module field is added to the JSON file, but many modifications have to be made. What should I do? Is there a simpler method? Replace multilingual files with commonjs module syntax? It's OK, but it's not very elegant, but fortunately babel provides one @babel/register Package, babel can be bound to the require module of node, and then it can be compiled immediately at runtime. That is, babel will compile when require('/src/i18n/xxx/index.js'). After compiling, of course, there will be no import statement. First install:
npm install @babel/core @babel/register @babel/preset-env
Then create a new babel configuration file:
// hello-tool/babel.config.js module.exports = { 'presets': ['@babel/preset-env'] }
Finally, in Hello tool / index JS file:
const path = require('path') const { program } = require('commander'); const fs = require('fs') require("@babel/register")({ configFile: path.resolve(__dirname, './babel.config.js'), }) // ...
Next, run the command again:
It can be seen that the compilation is completed and the file is also output to the public directory, but there is a default attribute in the json file. Obviously, we don't need this layer, so when we require('i18n/xxx/index.js'), we can store the exported default object and modify the Hello tool / index js:
const buildI18n = () => { // ... langDirs.forEach((dir) => { let dirPath = path.join(srcDir, dir) let indexPath = path.join(dirPath, 'index.js') if (fs.statSync(dirPath).isDirectory() && fs.existsSync(indexPath)) { data[dir] = require(indexPath).default// ++ } }) // ... }
The effect is as follows:
Use multilingual files
First, modify the return data of the user interface and add the default language field:
// /src/store.js // ... async getUserInfo(ctx) { let userInfo = { // ... language: 'zh_CN'// default language } ctx.commit('setUserInfo', userInfo) } // ...
Then in main After obtaining the user information in JS, request and set multilingual immediately:
// /src/main.js import { setLanguage } from './utils'// ++ import i18n from './i18n'// ++ const initApp = async () => { await store.dispatch('getUserInfo') await setLanguage(store.state.userInfo.language)// ++ new Vue({ i18n,// ++ router, store, render: h => h(App), }).$mount('#app') }
The setLanguage method requests multilingual files and switches:
// /src/utils/index.js import axios from 'axios' import i18n from '../i18n' // Request and set multilingual data const languageCache = {} export const setLanguage = async (language = 'zh_CN') => { let languageData = null // With cache, use cache data if (languageCache[language]) { languageData = languageCache[language] } else { // No cache, request initiated const { data } = await axios.get(`/i18n/${language}/index.json`) languageCache[language] = languageData = data } // Set locale information for locale i18n.setLocaleMessage(language, languageData) // Modify locale i18n.locale = language }
Then change the information displayed in each component into the form of $t('xxx '). Of course, the menu and route need to be modified accordingly. The effect is as follows:
It can be found that the language of ElementUI component has not changed, of course, because we haven't processed it yet. The modification is very simple. ElementUI supports custom i18n processing methods:
// /src/main.js // ... Vue.use(ElementUI, { i18n: (key, value) => i18n.t(key, value) }) // ...
Generate the initial multilingual file through the CLI plug-in
Finally, there is another problem, that is, what if there is no multilingual file when the project is initialized? Do you have to manually run the command to compile multilingual files after the project is created? There are several solutions:
1. Generally, a project scaffold will be provided in the end, so we can directly add the initial multilingual file to the default template;
2. When starting the service and packaging, compile the multilingual files first, like this:
"scripts": { "serve": "hello i18n && vue-cli-service serve", "build": "hello i18n && vue-cli-service build" }
3. Develop a Vue CLI plug-in to help us automatically run a multilingual compilation command when the project is created;
Next, simply implement the third method. Similarly, create a new plug-in directory at the same level of the project and create corresponding files (pay attention to the naming specification of plug-ins):
According to the plug-in development specification, index JS is the entry file of the Service plug-in. The Service plug-in can modify the webpack configuration, create a new Vue cli Service command or modify an existing command. We can't use it. Our logic is in generator JS, this file will be called in two scenarios:
1. During project creation, when the CLI plug-in is installed as part of the project creation preset
2. Called when the plug-in is installed separately through vue add or vue invoke when the project is created
What we need is to automatically run the multilingual compilation command for us when the project is created or the plug-in is installed JS needs to export a function as follows:
const { exec } = require('child_process'); module.exports = (api) => { // In order to see the command of compiling multiple languages in the project, we add hello i18n to the package of the project JSON file, modify package JSON files can use the provided API Extendpackage method api.extendPackage({ scripts: { buildI18n: 'hello i18n' } }) // The hook will be called after the file is written to the hard disk. api.afterInvoke(() => { // Get the full path of the project let targetDir = api.generator.context // Enter the project folder and run the command exec(`cd ${targetDir} && npm run buildI18n`, (error, stdout, stderr) => { if (error) { console.error(error); return; } console.log(stdout); console.error(stderr); }); }) }
We run the compile command in the afterInvoke hook because it runs too early and the dependencies may not have been installed. In addition, we also get the full path of the project. This is because when configuring the plug-in through preset, the plug-in may not be in the actual project folder when it is called. For example, we create project b through this command in folder a:
vue create b
When the plug-in is called, it is in the a directory. Obviously, the hello-i18n package is installed in the b directory, so we should first enter the actual directory of the project and then run the compilation command.
Next, test it. First install the plug-in under the project:
npm install --save-dev file:Full path\vue-cli-plugin-i18n
Then call the generator of the plug-in through the following command:
vue invoke vue-cli-plugin-i18n
The effect is as follows:
You can see the package of the project The JSON file has been injected with compilation commands, and the commands are automatically executed to generate multilingual files.
Mock data
Mock data is recommended Mock , it is easy to use. Create a mock data file:
Then at / API / index JS:
In this simple way, the request can be intercepted:
Standardization
For standardized configuration, such as code style check and git submission specification, the author previously wrote an article on the construction of component library, in which a section introduces the configuration process in detail, which can be moved: [ten thousand words long text] configure a vue component library from zero - standardized configuration section.
other
Request agent
It is inevitable to encounter cross domain problems when developing and testing interface requests locally. You can configure the proxy option of webpack dev server and create a Vue config. JS file:
module.exports = { devServer: { proxy: { '^/api/': { target: 'http://xxx:xxx', changeOrigin: true } } } }
Compile node_ Dependencies in modules
By default, Babel loader ignores all nodes_ The files in modules, but some dependencies may not have been compiled. For example, some packages written by ourselves will not be compiled in order to save trouble. If the latest syntax is used, they may not run in lower version browsers. Therefore, they also need to be compiled when packaging, and a dependency should be explicitly translated through Babel, You can configure this transpileDependencies option and modify Vue config. js:
module.exports = { // ... transpileDependencies: ['your-package-name'] }
environment variable
If you need environment variables, you can create them in the root directory of the project env file, it should be noted that if you want to render through the plug-in The template file starting with_ Instead, that is_ env, which will eventually render as File at the beginning.
Scaffolding
When we have designed a set of project structure, we must use it as a template to quickly create a project. Generally, we will create a scaffold tool to generate it. However, Vue CLI provides the ability of preset. Preset refers to a JSON object containing predefined options and plug-ins required to create a new project, so we can create a CLI plug-in to create a template, Then create a preset and configure the plug-in into the preset, so that our custom preset can be used when using vue create command to create a project.
Create a CLI plug-in that generates templates
The new plug-in directory is as follows:
You can see that this time we created a generator directory, because we need to render templates, and the template files will be placed in this directory. Create a template directory, and then copy the project structure we configured earlier (excluding package.json):
Now let's finish / generator / index JS file contents:
1. Because package is not included JSON, so we need to modify the vue project's default package JSON, add what we need, and use the API mentioned above Extendpackage method:
// generator/index.js module.exports = (api) => { // Extended package json api.extendPackage({ "dependencies": { "axios": "^0.25.0", "element-ui": "^2.15.6", "vue-i18n": "^8.27.0", "vue-router": "^3.5.3", "vuex": "^3.6.2" }, "devDependencies": { "mockjs": "^1.1.0", "sass": "^1.49.7", "sass-loader": "^8.0.2", "hello-tool": "^1.0.0"// Pay attention here and don't forget to add our toolkit } }) }
Added some additional dependencies, including the Hello tool we developed earlier.
2. Rendering template
module.exports = (api) => { // ... api.render('./template') }
The render method renders all files in the template directory.
Create a custom preset
We have all the plug-ins. Finally, let's create a custom preset and create a new preset JSON file to configure the template plug-in and i18n plug-in we wrote earlier:
{ "plugins": { "vue-cli-plugin-template": { "version": "^1.0.0" }, "vue-cli-plugin-i18n": { "version": "^1.0.0" } } }
At the same time, in order to test this preset, we create an empty directory:
Then enter the test preset directory and specify our preset path when running vue create command:
vue create --preset ../preset.json my-project
The effect is as follows:
Remote use preset
If the preset local test is OK, it can be uploaded to the warehouse, and then it can be used by others. For example, the author uploaded it to the warehouse: https://github.com/wanglin2/Vue_project_design , you can use this:
vue create --preset wanglin2/Vue_project_design project-name
summary
If there is anything wrong or better, see you in the comment area~