├── .gitignore ├── commitlint.config.js ├── index.js ├── package.json ├── README.zh.md ├── README.md └── Router.webpack.plugin.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'] 3 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const RouterWebpackPlugin = require('./Router.webpack.plugin') 2 | 3 | module.exports = (api, projectOptions) => { 4 | const options = projectOptions.pluginOptions.routeConfig 5 | api.configureWebpack(config => { 6 | config.plugins.push(new RouterWebpackPlugin(options ? options.route : {} || {})) 7 | }) 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-cli-plugin-route-generator", 3 | "version": "1.0.5", 4 | "description": "", 5 | "main": "index.js", 6 | "author": "GzhiYi", 7 | "license": "MIT", 8 | "keywords": ["vue", "vue-router", "vue-cli", "vue-cli-plugin", "router", "webpack"], 9 | "dependencies": { 10 | "chokidar": "^3.3.1", 11 | "colors": "^1.4.0", 12 | "lodash": "^4.17.15", 13 | "@commitlint/cli": "^8.3.5", 14 | "@commitlint/config-conventional": "^8.3.4", 15 | "husky": "^4.2.3", 16 | "lint-staged": "^10.0.9" 17 | }, 18 | "devDependencies": { 19 | "@commitlint/cli": "^8.3.5", 20 | "@commitlint/config-conventional": "^8.3.4", 21 | "husky": "^4.2.3", 22 | "lint-staged": "^10.0.9" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/GzhiYi/vue-cli-plugin-route-generator.git" 27 | }, 28 | "gitHooks": { 29 | "commit-msg": "commitlint -e $HUSKY_GIT_PARAMS", 30 | "pre-commit": "lint-staged" 31 | }, 32 | "lint-staged": { 33 | "*.js": [ 34 | "vue-cli-service lint", 35 | "git add" 36 | ], 37 | "*.vue": [ 38 | "vue-cli-service lint", 39 | "git add" 40 | ], 41 | "ignore": [ 42 | "node_modules", 43 | "dist", 44 | "package-lock.json" 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /README.zh.md: -------------------------------------------------------------------------------- 1 | # vue-cli-plugin-route-generator 2 | 3 | 基于Nuxtjs源码,分离出的一个vue路由自动生成插件。 4 | 5 | [English](./README.md) 6 | 7 | ## 安装 8 | 9 | ### yarn 10 | 11 | ```bash 12 | yarn add vue-cli-plugin-route-generator 13 | ``` 14 | ### npm 15 | 16 | ```bash 17 | npm install -D vue-cli-plugin-route-generator 18 | ``` 19 | 20 | ## 用法 21 | 22 | 在安装完插件之后,在`views`下的文件或者文件夹改动,将会自动对应生成路由`routes.js`。 23 | 24 | ### 例子 25 | 26 | views文件夹内: 27 | 28 | - 基础路由: 29 | 30 | ``` 31 | views/ 32 | --| user/ 33 | -----| index.vue 34 | -----| one.vue 35 | --| index.vue 36 | ``` 37 | 38 | 将生成如下: 39 | ```javascript 40 | router: { 41 | routes: [ 42 | { 43 | name: 'index', 44 | path: '/', 45 | component: 'views/index.vue' 46 | }, 47 | { 48 | name: 'user', 49 | path: '/user', 50 | component: 'views/user/index.vue' 51 | }, 52 | { 53 | name: 'user-one', 54 | path: '/user/one', 55 | component: 'views/user/one.vue' 56 | } 57 | ] 58 | } 59 | ``` 60 | 61 | - 动态路由 62 | 63 | ``` 64 | views/ 65 | --| _slug/ 66 | -----| comments.vue 67 | -----| index.vue 68 | --| users/ 69 | -----| _id.vue 70 | --| index.vue 71 | ``` 72 | 73 | 将生成如下: 74 | 75 | ```javascript 76 | router: { 77 | routes: [ 78 | { 79 | name: 'index', 80 | path: '/', 81 | component: 'views/index.vue' 82 | }, 83 | { 84 | name: 'users-id', 85 | path: '/users/:id?', 86 | component: 'views/users/_id.vue' 87 | }, 88 | { 89 | name: 'slug', 90 | path: '/:slug', 91 | component: 'views/_slug/index.vue' 92 | }, 93 | { 94 | name: 'slug-comments', 95 | path: '/:slug/comments', 96 | component: 'views/_slug/comments.vue' 97 | } 98 | ] 99 | } 100 | ``` 101 | 102 | - 嵌套路由 103 | 104 | ``` 105 | views/ 106 | --| users/ 107 | -----| _id.vue 108 | -----| index.vue 109 | --| users.vue 110 | ``` 111 | 112 | 将生成如下: 113 | ```javascript 114 | router: { 115 | routes: [ 116 | { 117 | path: '/users', 118 | component: 'views/users.vue', 119 | children: [ 120 | { 121 | path: '', 122 | component: 'views/users/index.vue', 123 | name: 'users' 124 | }, 125 | { 126 | path: ':id', 127 | component: 'views/users/_id.vue', 128 | name: 'users-id' 129 | } 130 | ] 131 | } 132 | ] 133 | } 134 | ``` 135 | - 动态嵌套路由 136 | 137 | ``` 138 | views/ 139 | --| _category/ 140 | -----| _subCategory/ 141 | --------| _id.vue 142 | --------| index.vue 143 | -----| _subCategory.vue 144 | -----| index.vue 145 | --| _category.vue 146 | --| index.vue 147 | ``` 148 | 149 | 将生成如下: 150 | ```javascript 151 | router: { 152 | routes: [ 153 | { 154 | path: '/', 155 | component: 'views/index.vue', 156 | name: 'index' 157 | }, 158 | { 159 | path: '/:category', 160 | component: 'views/_category.vue', 161 | children: [ 162 | { 163 | path: '', 164 | component: 'views/_category/index.vue', 165 | name: 'category' 166 | }, 167 | { 168 | path: ':subCategory', 169 | component: 'views/_category/_subCategory.vue', 170 | children: [ 171 | { 172 | path: '', 173 | component: 'views/_category/_subCategory/index.vue', 174 | name: 'category-subCategory' 175 | }, 176 | { 177 | path: ':id', 178 | component: 'views/_category/_subCategory/_id.vue', 179 | name: 'category-subCategory-id' 180 | } 181 | ] 182 | } 183 | ] 184 | } 185 | ] 186 | } 187 | ``` 188 | 189 | 👉 [更多](https://nuxtjs.org/guide/routing) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-cli-plugin-route-generator 2 | 3 | Based on Nuxtjs source code, a custom plug-in for generating routes is separated. 4 | 5 | [中文](./README.zh.md) 6 | 7 | ## Install 8 | 9 | ### Using yarn 10 | 11 | ```bash 12 | yarn add vue-cli-plugin-route-generator 13 | ``` 14 | ### Using npm 15 | 16 | ```bash 17 | npm install -D vue-cli-plugin-route-generator 18 | ``` 19 | 20 | ## Usage 21 | 22 | After installing this plugin, changes to files or folders under `views` will automatically regenerate `routes.js`. 23 | 24 | ### Example 25 | 26 | In views: 27 | 28 | - Basic Routes: 29 | 30 | ``` 31 | views/ 32 | --| user/ 33 | -----| index.vue 34 | -----| one.vue 35 | --| index.vue 36 | ``` 37 | 38 | will generate into: 39 | ```javascript 40 | router: { 41 | routes: [ 42 | { 43 | name: 'index', 44 | path: '/', 45 | component: 'views/index.vue' 46 | }, 47 | { 48 | name: 'user', 49 | path: '/user', 50 | component: 'views/user/index.vue' 51 | }, 52 | { 53 | name: 'user-one', 54 | path: '/user/one', 55 | component: 'views/user/one.vue' 56 | } 57 | ] 58 | } 59 | ``` 60 | 61 | - Dynamic Routes 62 | 63 | ``` 64 | views/ 65 | --| _slug/ 66 | -----| comments.vue 67 | -----| index.vue 68 | --| users/ 69 | -----| _id.vue 70 | --| index.vue 71 | ``` 72 | 73 | will generate into: 74 | ```javascript 75 | router: { 76 | routes: [ 77 | { 78 | name: 'index', 79 | path: '/', 80 | component: 'views/index.vue' 81 | }, 82 | { 83 | name: 'users-id', 84 | path: '/users/:id?', 85 | component: 'views/users/_id.vue' 86 | }, 87 | { 88 | name: 'slug', 89 | path: '/:slug', 90 | component: 'views/_slug/index.vue' 91 | }, 92 | { 93 | name: 'slug-comments', 94 | path: '/:slug/comments', 95 | component: 'views/_slug/comments.vue' 96 | } 97 | ] 98 | } 99 | ``` 100 | 101 | - Nested Routes 102 | 103 | ``` 104 | views/ 105 | --| users/ 106 | -----| _id.vue 107 | -----| index.vue 108 | --| users.vue 109 | ``` 110 | 111 | will generate into: 112 | ```javascript 113 | router: { 114 | routes: [ 115 | { 116 | path: '/users', 117 | component: 'views/users.vue', 118 | children: [ 119 | { 120 | path: '', 121 | component: 'views/users/index.vue', 122 | name: 'users' 123 | }, 124 | { 125 | path: ':id', 126 | component: 'views/users/_id.vue', 127 | name: 'users-id' 128 | } 129 | ] 130 | } 131 | ] 132 | } 133 | ``` 134 | - Dynamic Nested Routes 135 | 136 | ``` 137 | views/ 138 | --| _category/ 139 | -----| _subCategory/ 140 | --------| _id.vue 141 | --------| index.vue 142 | -----| _subCategory.vue 143 | -----| index.vue 144 | --| _category.vue 145 | --| index.vue 146 | ``` 147 | 148 | will generate into: 149 | ```javascript 150 | router: { 151 | routes: [ 152 | { 153 | path: '/', 154 | component: 'views/index.vue', 155 | name: 'index' 156 | }, 157 | { 158 | path: '/:category', 159 | component: 'views/_category.vue', 160 | children: [ 161 | { 162 | path: '', 163 | component: 'views/_category/index.vue', 164 | name: 'category' 165 | }, 166 | { 167 | path: ':subCategory', 168 | component: 'views/_category/_subCategory.vue', 169 | children: [ 170 | { 171 | path: '', 172 | component: 'views/_category/_subCategory/index.vue', 173 | name: 'category-subCategory' 174 | }, 175 | { 176 | path: ':id', 177 | component: 'views/_category/_subCategory/_id.vue', 178 | name: 'category-subCategory-id' 179 | } 180 | ] 181 | } 182 | ] 183 | } 184 | ] 185 | } 186 | ``` 187 | 188 | 👉 [view more](https://nuxtjs.org/guide/routing) -------------------------------------------------------------------------------- /Router.webpack.plugin.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const { 3 | camelCase, 4 | throttle 5 | } = require('lodash') 6 | const chokidar = require('chokidar') 7 | const Glob = require('glob') 8 | const pify = require('pify') 9 | const path = require('path') 10 | const fs = require('fs') 11 | const glob = pify(Glob) 12 | require('colors') 13 | 14 | /** 15 | * 主函数,生成路由表和文件入口 16 | * @param {object} config 配置 17 | */ 18 | const generateRoutesAndFiles = async (config) => { 19 | // ** 匹配任意级别目录 20 | const files = await glob(`${config.viewPath || 'views'}/**/*.{vue,js}`, { 21 | // cwd: path.resolve(process.cwd(), './'), 22 | ignore: ['**/*.test.*', '**/*.spec.*', '**/-*.*', '**/#*.*'] 23 | }) 24 | // 这里将文件名中带有"、'的替换为$ 25 | files.map(f => f.replace(/('|")/g, '\\$1')) 26 | return createRoutes(files, config.viewPath || 'views', '-') 27 | } 28 | 29 | /** 30 | * 31 | * @param {*} files 文件名数组 32 | * @param {*} viewsDir views的目录 33 | * @param {*} routeNameSplitter 路由分割符 34 | */ 35 | const createRoutes = (files, viewsDir = '', routeNameSplitter = '-') => { 36 | const supportedExtensions = ['vue', 'js'] 37 | const routes = [] 38 | const requireComponent = [] 39 | files.forEach((file) => { 40 | // keys 拿到每一级目录和文件名(不包括后缀) 41 | const keys = file 42 | // 去掉viewsDir前缀 43 | .replace(new RegExp(`^${viewsDir}`), '') 44 | // 去掉扩展后缀.js或.vue 45 | .replace(new RegExp(`\\.(${supportedExtensions.join('|')})$`), '') 46 | .replace(/\/{2,}/g, '/') 47 | .split('/') 48 | .slice(1) 49 | 50 | // 单个路由结构 51 | const route = { 52 | name: '', 53 | path: '', 54 | // 这里生成的component为用-连接的,比如 kol-index。最后转为驼峰kolIndex 55 | component: `${camelCase(keys.join('-').replace('_', ''))}` 56 | } 57 | // 引入组件的字符串 58 | requireComponent.push(`const ${route.component} = () => import(/* webpackChunkName: "${route.component}" */ '@/views/${keys.join('/')}')`) 59 | let parent = routes 60 | // keys 约为 ['kol', 'index'] 61 | keys.forEach((key, i) => { 62 | // remove underscore only, if its the prefix 63 | // 去掉开头的下划线 64 | const sanitizedKey = key.startsWith('_') ? key.substr(1) : key 65 | 66 | route.name = route.name ? 67 | route.name + routeNameSplitter + sanitizedKey : 68 | sanitizedKey 69 | // 如果文件名为 - 的话,则会生成一个 * 的路由 70 | route.name += key === '_' ? 'all' : '' 71 | // route.chunkName = file.replace(new RegExp(`\\.(${supportedExtensions.join('|')})$`), '') 72 | const child = parent.find(parentRoute => parentRoute.name === route.name) 73 | 74 | if (child) { 75 | child.children = child.children || [] 76 | parent = child.children 77 | route.path = '' 78 | // 这里判断如果结尾是index的话,则路径为空。path不会带有index 79 | } else if (key === 'index' && i + 1 === keys.length) { 80 | route.path += i > 0 ? '' : '/' 81 | } else { 82 | route.path += '/' + getRoutePathExtension(key) 83 | 84 | if (key.startsWith('_') && key.length > 1) { 85 | route.path += '?' 86 | } 87 | } 88 | }) 89 | parent.push(route) 90 | }) 91 | sortRoutes(routes) 92 | return { 93 | routes: cleanChildrenRoutes(routes), 94 | requireComponent 95 | } 96 | } 97 | const startsWithAlias = aliasArray => str => aliasArray.some(c => str.startsWith(c)) 98 | const startsWithSrcAlias = startsWithAlias(['@', '~']) 99 | const r = (...args) => { 100 | const lastArg = args[args.length - 1] 101 | if (startsWithSrcAlias(lastArg)) { 102 | return wp(lastArg) 103 | } 104 | return wp(path.resolve(...args.map(str => str.replace(/\//g, escapeRegExp(path.sep))))) 105 | } 106 | 107 | const baseToString = value => { 108 | if (typeof value == 'string') { 109 | return value 110 | } 111 | if (isArray_1(value)) { 112 | return _arrayMap(value, baseToString) + '' 113 | } 114 | if (isSymbol_1(value)) { 115 | return symbolToString ? symbolToString.call(value) : '' 116 | } 117 | let result = (value + '') 118 | return (result == '0' && (1 / value) == -INFINITY) ? '-0' : result 119 | } 120 | 121 | const toString = value => { 122 | return value == null ? '' : baseToString(value) 123 | } 124 | 125 | const escapeRegExp = string => { 126 | const reRegExpChar = /[\\^$.*+?()[\]{}|]/g, 127 | reHasRegExpChar = RegExp(reRegExpChar.source) 128 | string = toString(string) 129 | return (string && reHasRegExpChar.test(string)) ? 130 | string.replace(reRegExpChar, '\\$&') : 131 | string 132 | } 133 | const isWindows = /^win/.test(process.platform) 134 | const wp = (p = '') => { 135 | // windows 的特殊处理 136 | if (isWindows) { 137 | return p.replace(/\\/g, '\\\\') 138 | } 139 | return p 140 | } 141 | const getRoutePathExtension = (key) => { 142 | // 如果key 为 - 的话,则会生成一个 * 的路由 143 | if (key === '_') { 144 | return '*' 145 | } 146 | // 如果key 为 _ 开始的话,则会生成一个 :key 的路由 147 | if (key.startsWith('_')) { 148 | return `:${key.substr(1)}` 149 | } 150 | return key 151 | } 152 | 153 | const DYNAMIC_ROUTE_REGEX = /^\/(:|\*)/ 154 | 155 | // 这个函数是对routes数组进行一些默认规则的排序 156 | const sortRoutes = routes => { 157 | routes.sort((a, b) => { 158 | if (!a.path.length) { 159 | return -1 160 | } 161 | if (!b.path.length) { 162 | return 1 163 | } 164 | // Order: /static, /index, /:dynamic 165 | // Match exact route before index: /login before /index/_slug 166 | if (a.path === '/') { 167 | return DYNAMIC_ROUTE_REGEX.test(b.path) ? -1 : 1 168 | } 169 | if (b.path === '/') { 170 | return DYNAMIC_ROUTE_REGEX.test(a.path) ? 1 : -1 171 | } 172 | 173 | let i 174 | let res = 0 175 | let y = 0 176 | let z = 0 177 | const _a = a.path.split('/') 178 | const _b = b.path.split('/') 179 | for (i = 0; i < _a.length; i++) { 180 | if (res !== 0) { 181 | break 182 | } 183 | y = _a[i] === '*' ? 2 : _a[i].includes(':') ? 1 : 0 184 | z = _b[i] === '*' ? 2 : _b[i].includes(':') ? 1 : 0 185 | res = y - z 186 | // If a.length >= b.length 187 | if (i === _b.length - 1 && res === 0) { 188 | // unless * found sort by level, then alphabetically 189 | res = _a[i] === '*' ? -1 : ( 190 | _a.length === _b.length ? a.path.localeCompare(b.path) : (_a.length - _b.length) 191 | ) 192 | } 193 | } 194 | 195 | if (res === 0) { 196 | // unless * found sort by level, then alphabetically 197 | res = _a[i - 1] === '*' && _b[i] ? 1 : ( 198 | _a.length === _b.length ? a.path.localeCompare(b.path) : (_a.length - _b.length) 199 | ) 200 | } 201 | return res 202 | }) 203 | 204 | routes.forEach((route) => { 205 | if (route.children) { 206 | sortRoutes(route.children) 207 | } 208 | }) 209 | 210 | return routes 211 | } 212 | 213 | const cleanChildrenRoutes = (routes, isChild = false, routeNameSplitter = '-') => { 214 | let start = -1 215 | const regExpIndex = new RegExp(`${routeNameSplitter}index$`) 216 | const routesIndex = [] 217 | routes.forEach((route) => { 218 | if (regExpIndex.test(route.name) || route.name === 'index') { 219 | // Save indexOf 'index' key in name 220 | const res = route.name.split(routeNameSplitter) 221 | const s = res.indexOf('index') 222 | start = start === -1 || s < start ? s : start 223 | routesIndex.push(res) 224 | } 225 | }) 226 | routes.forEach((route) => { 227 | route.path = isChild ? route.path.replace('/', '') : route.path 228 | if (route.path.includes('?')) { 229 | const names = route.name.split(routeNameSplitter) 230 | const paths = route.path.split('/') 231 | if (!isChild) { 232 | paths.shift() 233 | } // clean first / for parents 234 | routesIndex.forEach((r) => { 235 | const i = r.indexOf('index') - start // children names 236 | if (i < paths.length) { 237 | for (let a = 0; a <= i; a++) { 238 | if (a === i) { 239 | paths[a] = paths[a].replace('?', '') 240 | } 241 | if (a < i && names[a] !== r[a]) { 242 | break 243 | } 244 | } 245 | } 246 | }) 247 | route.path = (isChild ? '' : '/') + paths.join('/') 248 | } 249 | route.name = route.name.replace(regExpIndex, '') 250 | if (route.children) { 251 | if (route.children.find(child => child.path === '')) { 252 | delete route.name 253 | } 254 | route.children = cleanChildrenRoutes(route.children, true, routeNameSplitter) 255 | } 256 | }) 257 | return routes 258 | } 259 | 260 | const creatRouter = (config) => { 261 | const { srcPath } = config 262 | generateRoutesAndFiles(config).then(res => { 263 | // add eslint disable for building lint 264 | let string = '/* eslint-disable */\n' 265 | res.requireComponent.forEach(res => { 266 | string += `${res}\n` 267 | }) 268 | string += `export default ${JSON.stringify(res.routes, null, 2)}` 269 | .replace(/"component": "(\w+?)"/g, `"component": $1`) 270 | .replace(/"(\w+?)":/g, '$1:').replace(/"/g, '\'') 271 | fs.writeFile(path.resolve(process.cwd(), `${srcPath || './src'}/router/routes.js`), `${string}\n`, () => { 272 | console.log('\n路由表已重新生成.'.green) 273 | }) 274 | }) 275 | } 276 | class RouterWebpackPlugin { 277 | constructor(config) { 278 | this.config = config 279 | } 280 | apply(compiler) { 281 | const _creatRouter = throttle(() => { 282 | creatRouter(this.config) 283 | }, 500) 284 | const { viewPath } = this.config 285 | compiler 286 | .hooks 287 | .entryOption 288 | .tap('RouterWebpackPlugin', () => { 289 | _creatRouter() 290 | }) 291 | compiler 292 | .hooks 293 | .environment 294 | .tap('RouterWebpackPlugin', () => { 295 | compiler.options.mode === 'development' && chokidar.watch(viewPath || 'views', { 296 | ignoreInitial: true, 297 | // cwd: path.resolve(process.cwd(), srcPath || './src'), 298 | ignore: ['**/*.test.*', '**/*.spec.*', '**/-*.*', '**/#*.*'] 299 | }).on('add', () => { 300 | _creatRouter() 301 | }).on('addDir', () => { 302 | _creatRouter() 303 | }).on('unlink', () => { 304 | _creatRouter() 305 | }).on('unlinkDir', () => { 306 | _creatRouter() 307 | }) 308 | }) 309 | } 310 | } 311 | 312 | module.exports = RouterWebpackPlugin 313 | --------------------------------------------------------------------------------