├── example ├── src │ ├── router │ │ └── .gitkeep │ ├── main.js │ └── views │ │ ├── goods │ │ ├── list.vue │ │ └── detail │ │ │ └── index.vue │ │ ├── user │ │ ├── detail │ │ │ └── index.vue │ │ └── list.vue │ │ └── demo.vue ├── package.json └── webpack.config.js ├── .editorconfig ├── package.json ├── .gitignore ├── src ├── index.js └── utils │ └── parse-route.js └── README.md /example/src/router/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/src/main.js: -------------------------------------------------------------------------------- 1 | import demo from './views/demo.vue'; 2 | 3 | -------------------------------------------------------------------------------- /example/src/views/goods/list.vue: -------------------------------------------------------------------------------- 1 | 4 | 13 | 16 | -------------------------------------------------------------------------------- /example/src/views/user/detail/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 13 | 16 | -------------------------------------------------------------------------------- /example/src/views/goods/detail/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 13 | 16 | -------------------------------------------------------------------------------- /example/src/views/user/list.vue: -------------------------------------------------------------------------------- 1 | 4 | 16 | 19 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /example/src/views/demo.vue: -------------------------------------------------------------------------------- 1 | 4 | 20 | 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xiyun/vue-route-webpack-plugin", 3 | "version": "2.1.0", 4 | "main": "./src/index.js", 5 | "repository": "git@github.com:xiyun-international/vue-route-webpack-plugin.git", 6 | "author": "zhaoliang ", 7 | "license": "MIT", 8 | "dependencies": { 9 | "chokidar": "^3.0.2", 10 | "glob": "^7.1.5", 11 | "shelljs": "^0.8.3" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "main": "src/main.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "build": "webpack", 8 | "start": "webpack-dev-server" 9 | }, 10 | "devDependencies": { 11 | "@babel/core": "^7.5.5", 12 | "babel-loader": "^8.0.6", 13 | "babel-preset-vue": "^2.0.2", 14 | "vue-loader": "^15.7.1", 15 | "vue-template-compiler": "^2.6.10", 16 | "webpack": "^4.39.1", 17 | "webpack-cli": "^3.3.6", 18 | "webpack-dev-server": "^3.8.0" 19 | }, 20 | "dependencies": { 21 | "shelljs": "^0.8.3" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const VueLoaderPlugin = require('vue-loader/lib/plugin'); 3 | const VueRouteWebpackPlugin = require('../src/index'); 4 | 5 | module.exports = { 6 | mode: 'development', 7 | entry: './src/main.js', 8 | output: { 9 | filename: 'main.js', 10 | path: path.resolve(__dirname, 'dist'), 11 | }, 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.vue$/, 16 | loader: 'vue-loader', 17 | }, 18 | { 19 | test: /\.js$/, 20 | loader: 'babel-loader', 21 | }, 22 | ] 23 | }, 24 | plugins: [ 25 | new VueLoaderPlugin(), 26 | new VueRouteWebpackPlugin(), 27 | ] 28 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | 4 | yarn.lock 5 | .DS_Store 6 | children.js 7 | 8 | example/dist/ 9 | example/node_modules/ 10 | 11 | # Logs 12 | logs 13 | *.log 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | lerna-debug.log* 18 | 19 | # Diagnostic reports (https://nodejs.org/api/report.html) 20 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 21 | 22 | # Runtime data 23 | pids 24 | *.pid 25 | *.seed 26 | *.pid.lock 27 | 28 | # Directory for instrumented libs generated by jscoverage/JSCover 29 | lib-cov 30 | 31 | # Coverage directory used by tools like istanbul 32 | coverage 33 | *.lcov 34 | 35 | # nyc test coverage 36 | .nyc_output 37 | 38 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 39 | .grunt 40 | 41 | # Bower dependency directory (https://bower.io/) 42 | bower_components 43 | 44 | # node-waf configuration 45 | .lock-wscript 46 | 47 | # Compiled binary addons (https://nodejs.org/api/addons.html) 48 | build/Release 49 | 50 | # Dependency directories 51 | node_modules/ 52 | jspm_packages/ 53 | 54 | # TypeScript v1 declaration files 55 | typings/ 56 | 57 | # TypeScript cache 58 | *.tsbuildinfo 59 | 60 | # Optional npm cache directory 61 | .npm 62 | 63 | # Optional eslint cache 64 | .eslintcache 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variables file 76 | .env 77 | .env.test 78 | 79 | # parcel-bundler cache (https://parceljs.org/) 80 | .cache 81 | 82 | # next.js build output 83 | .next 84 | 85 | # nuxt.js build output 86 | .nuxt 87 | 88 | # vuepress build output 89 | .vuepress/dist 90 | 91 | # Serverless directories 92 | .serverless/ 93 | 94 | # FuseBox cache 95 | .fusebox/ 96 | 97 | # DynamoDB Local files 98 | .dynamodb/ 99 | 100 | # IntelliJ project files 101 | .idea 102 | *.iml 103 | out 104 | gen 105 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const chokidar = require('chokidar'); 4 | const glob = require('glob'); 5 | const shelljs = require('shelljs'); 6 | 7 | // 获取子目录路径 8 | function getSubDirectory(dir) { 9 | if (dir.indexOf('./') !== -1) { 10 | dir = dir.substring(dir.indexOf('.') + 2); 11 | } 12 | return dir.substring(dir.indexOf('/') + 1); 13 | } 14 | 15 | class VueRouteWebpackPlugin { 16 | constructor(options = {}) { 17 | // import 的路径前缀 18 | this.prefix = options.prefix || '../'; 19 | if (this.prefix.lastIndexOf('/') === -1) { 20 | this.prefix += '/'; 21 | } 22 | // 扫描目录 23 | this.directory = options.directory || `src/views`; 24 | // 路由文件存放路径 25 | this.routeFilePath = options.routeFilePath || `src/router/children.js`; 26 | // 生成的文件中是否使用双引号规范,默认使用 27 | this.doubleQoute = options.doubleQoute === undefined ? true : !!options.doubleQoute; 28 | this.qoute = this.doubleQoute ? '"' : "'"; 29 | } 30 | 31 | apply(compiler) { 32 | compiler.hooks.afterPlugins.tap('VueRouteWebpackPlugin', () => { 33 | const allData = this.parseRouteData(); 34 | this.writeRouteFile(allData); 35 | console.log('路由文件生成成功'); 36 | if (process.env.NODE_ENV === 'development') { 37 | const watcher = chokidar.watch(path.resolve(this.directory), { 38 | ignored: /(^|[\/\\])\../, 39 | persistent: true 40 | }); 41 | 42 | watcher.on('change', () => { 43 | const allData = this.parseRouteData(); 44 | this.writeRouteFile(allData); 45 | console.log('路由文件生成成功'); 46 | }) 47 | } 48 | }) 49 | } 50 | 51 | parseRouteData() { 52 | const files = glob.sync(path.join('.', this.directory) + '/**/*.vue'); 53 | const routeData = []; 54 | const importData = new Set; 55 | files.forEach(filePath => { 56 | let content = fs.readFileSync(path.resolve(filePath), 'utf8'); 57 | content = content.substring(content.indexOf(' 139 | ``` 140 | 141 | **默认情况下**,当你启动开发服务或执行构建的时候,就会在`src/router`目录下生成`children.js`这个路由文件。 142 | 143 | 假设你的页面文件路径是:`src/views/user/list.vue`,那么生成的路由文件的内容看起来就会是这样的: 144 | ```js 145 | import userlist from "../views/user/list.vue"; 146 | 147 | export default [ 148 | { 149 | path: "user/list/:type", 150 | component: userlist, 151 | }, 152 | { 153 | path: "user/list", 154 | alias: "user", 155 | component: userlist, 156 | }, 157 | { 158 | path: "user/list", 159 | alias: "user", 160 | name: "user-list", 161 | component: userlist, 162 | meta: { 163 | requiresAuth: true, 164 | userType: "member", 165 | } 166 | }, 167 | ] 168 | ``` 169 | 170 | *因为这个路由文件是由插件自动生成的,所以你可以在 .gitignore 文件中把它在版本库中忽略掉,避免多人协同开发时**因频繁改动发生冲突**。* 171 | 172 | **如果使用了 eslint,同时忽略了路由文件,那么需要在 `.eslintrs.js` 中禁用掉这两个检查规则:** 173 | ```js 174 | "import/no-unresolved": "off", 175 | "import/extensions": "off", 176 | ``` 177 | 178 | #### 默认目录约定 179 | 180 | ``` 181 | src/ 182 | |-views/ (项目文件,插件会扫描该目录下所有 .vue 文件的路由配置) 183 | |-... 184 | |-router/ (路由目录) 185 | |-index.js (主路由文件,需要引入 children.js 作为子路由来使用) 186 | |-children.js (路由文件,由插件自动生成) 187 | ``` 188 | 189 | #### 选项参考 190 | 191 | 插件提供了以下这些选项供自定义配置 192 | ```js 193 | new VueRouteWebpackPlugin({ 194 | // 配置 import 路径前缀,默认是:"../",因为路由文件会默认放在 src/router/ 目录下 195 | prefix: "../", 196 | // 插件扫描的项目目录,默认会扫描 "src/views" 目录 197 | directory: "src/views", 198 | // 生成的路由文件存放地址,默认存放到 "src/router/children.js" 199 | routeFilePath: "src/router/children.js", 200 | // 生成的文件中的 import 路径是否使用双引号规范,默认使用 201 | // 注意:生成的路由文件中的 path 的引号是原封不动使用用户的 202 | doubleQoute: true, 203 | }) 204 | ``` 205 | -------------------------------------------------------------------------------- /src/utils/parse-route.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 解析路由配置 3 | */ 4 | module.exports = function parseRoute(contentArr) { 5 | let matchOption = ''; 6 | // 路由总配置项 7 | const routeConfigs = []; 8 | // 记录对象配置的 key 9 | let configKey = ''; 10 | let index = 0; 11 | // 用来记录上一次匹配的结果 12 | let beforeCharacter = ''; 13 | 14 | // 记录子配置项数据 15 | let normalObjectData = {}; 16 | let normalObjectKey = ''; 17 | let normalObjectValue = ''; 18 | 19 | // 触发器 20 | function emit(type, data) { 21 | switch (type) { 22 | case 'matchOption': 23 | matchOption += data; 24 | break; 25 | case 'resetMatchOption': 26 | matchOption = ''; 27 | break; 28 | case 'setPath': 29 | routeConfigs[index] = routeConfigs[index] || {}; 30 | routeConfigs[index].path = routeConfigs[index].path || ''; 31 | routeConfigs[index].path += data; 32 | beforeCharacter = data; 33 | break; 34 | case 'setAlias': 35 | routeConfigs[index].alias = routeConfigs[index].alias || ''; 36 | routeConfigs[index].alias += data; 37 | beforeCharacter = data; 38 | break; 39 | case 'endStringConfig': 40 | index++; 41 | matchOption = ''; 42 | beforeCharacter = ''; 43 | break; 44 | case 'addConfigKey': 45 | configKey += data; 46 | break; 47 | case 'setConfigKey': 48 | routeConfigs[index] = routeConfigs[index] || {}; 49 | routeConfigs[index][configKey] = ''; 50 | break; 51 | case 'resetConfigKey': 52 | configKey = ''; 53 | break; 54 | case 'setConfigValue': 55 | routeConfigs[index][configKey] += data; 56 | break; 57 | case 'endObjectConfigOfString': 58 | index++; 59 | matchOption = ''; 60 | break; 61 | case 'addNormalObjectKey': 62 | normalObjectKey += data; 63 | break; 64 | case 'addNormalObjectValue': 65 | normalObjectValue += data; 66 | break; 67 | case 'resetNormalObjectKey': 68 | normalObjectKey = ''; 69 | break; 70 | case 'resetNormalObjectValue': 71 | normalObjectValue = ''; 72 | break; 73 | case 'setNormalObject': 74 | normalObjectData[normalObjectKey] = normalObjectValue; 75 | normalObjectKey = ''; 76 | normalObjectValue = ''; 77 | break; 78 | case 'resetNormalObject': 79 | normalObjectData = {}; 80 | normalObjectKey = ''; 81 | normalObjectValue = ''; 82 | break; 83 | case 'endNormalObject': 84 | routeConfigs[index][configKey] = { ...normalObjectData } 85 | normalObjectData = {}; 86 | normalObjectKey = ''; 87 | normalObjectValue = ''; 88 | index++; 89 | configKey = ''; 90 | matchOption = ''; 91 | break; 92 | } 93 | } 94 | 95 | // 状态开始 96 | function start(c) { 97 | if (c === "@") { 98 | // console.log('start', c); 99 | return optionStart; 100 | } else { 101 | // // console.log('start return:', c); 102 | return start; 103 | } 104 | } 105 | 106 | // 选项开始状态 107 | function optionStart(c) { 108 | // console.log('optionStart', c); 109 | if (c.match(/[route]/)) { 110 | // 如果等于 route 后还进这来,就不对了 111 | if (matchOption === 'route') { 112 | return start; 113 | } 114 | emit('matchOption', c); 115 | return optionStart; 116 | } else if (c === '(') { 117 | return beforeOption(c); 118 | } else { 119 | emit('resetMatchOption'); 120 | return start; 121 | } 122 | } 123 | 124 | // 选项开始前 125 | function beforeOption(c) { 126 | // console.log('beforeOption', c); 127 | if (matchOption !== 'route') { 128 | return start; 129 | } else if (c.match(/[\s\t\f\n]/)) { 130 | return beforeOption; 131 | } else { 132 | return intoOption; 133 | } 134 | } 135 | 136 | // 进入解析选项的状态 137 | function intoOption(c) { 138 | // console.log('intoOption', c); 139 | if (c === '\'' || c === '"') { 140 | // 如果解析到引号,就是简单的行级配置 141 | return stringPath(c); 142 | } else if (c === "{") { 143 | // 如果解析到大括号,就是对象配置的方式 144 | return beforeObjectConfig; 145 | } else { 146 | return start; 147 | } 148 | } 149 | 150 | // 对象配置前的状态 151 | function beforeObjectConfig(c) { 152 | // console.log('beforeObjectConfig', c); 153 | if (c.match(/[\n\s\t\f\*\/]/)) { 154 | return beforeObjectConfig; 155 | } else { 156 | return objectConfig(c); 157 | } 158 | } 159 | 160 | // 进入到对象配置状态 161 | function objectConfig(c) { 162 | // console.log('objectConfig', c); 163 | if (c.match(/[a-zA-Z0-9]/)) { 164 | emit('addConfigKey', c); 165 | return objectConfig; 166 | } else if (c.match(/[\s\t]/)) { 167 | return objectConfig; 168 | } else if (c === ':') { 169 | emit('setConfigKey', c); 170 | return beforeObjectConfigValue; 171 | } else { 172 | emit('resetConfigKey'); 173 | // TODO 174 | return start; 175 | } 176 | } 177 | 178 | // 开始对象值状态之前 179 | function beforeObjectConfigValue(c) { 180 | // console.log('beforeObjectConfigValue', c); 181 | if (c.match(/[\s\t]/)) { 182 | return beforeObjectConfigValue; 183 | } else { 184 | return objectConfigValue(c); 185 | } 186 | } 187 | 188 | // 对象值的状态 189 | function objectConfigValue(c) { 190 | // console.log('objectConfigValue', c); 191 | if (c === '\'' || c === '"') { 192 | return objectConfigStringValue(c); 193 | } else if (c === '{') { 194 | return beforeNormalObject; 195 | } else { 196 | // TODO 197 | return start; 198 | } 199 | } 200 | 201 | function beforeNormalObject(c) { 202 | // console.log('beforeNormalObject', c); 203 | if (c.match(/[\s\n\t\f\/*]/)) { 204 | return beforeNormalObject; 205 | } else if (c === '}') { 206 | return endNormalObject; 207 | } else { 208 | return normalObjectState(c); 209 | } 210 | } 211 | 212 | function normalObjectState(c) { 213 | // console.log('normalObjectState', c); 214 | if (c.match(/[a-zA-Z0-9_]/)) { 215 | emit('addNormalObjectKey', c) 216 | return normalObjectState; 217 | } else if (c === ':') { 218 | return beforeNormalObjectValue; 219 | } else { 220 | return endNormalObject(c); 221 | } 222 | } 223 | 224 | function beforeNormalObjectValue(c) { 225 | // console.log('beforeNormalObjectValue', c); 226 | if (c.match(/[\s\t\f]/)) { 227 | return beforeNormalObjectValue; 228 | } else { 229 | return normalObjectValueState(c); 230 | } 231 | } 232 | 233 | function normalObjectValueState(c) { 234 | // console.log('normalObjectValueState', c); 235 | if (c.match(/[a-zA-Z\-_'"]/)) { 236 | emit('addNormalObjectValue', c) 237 | return normalObjectValueState; 238 | } else if (c.match(/[,\s\n\t\f\/*]/)) { 239 | // 继续进行下一次匹配 240 | emit('setNormalObject'); 241 | return beforeNormalObject; 242 | } else if (c === '}') { 243 | emit('setNormalObject'); 244 | return endNormalObject; 245 | } else { 246 | return beforeEndOption; 247 | } 248 | } 249 | 250 | function endNormalObject(c) { 251 | // console.log('endNormalObject', c); 252 | if (c === ',') { 253 | // 继续解析对象的配置 254 | emit('endNormalObject') 255 | return beforeObjectConfig; 256 | } else { 257 | return beforeEndOption(c); 258 | } 259 | } 260 | 261 | function beforeEndOption(c) { 262 | // console.log('beforeEndOption', c); 263 | if (c.match(/[\n\s\t\f\/*]/)) { 264 | return beforeEndOption; 265 | } else { 266 | emit('endNormalObject') 267 | return endOption; 268 | } 269 | } 270 | 271 | function endOption(c) { 272 | return start; 273 | } 274 | 275 | // 进入到配置字符串值的状态 276 | function objectConfigStringValue(c) { 277 | // console.log('objectConfigStringValue', c); 278 | if (c.match(/[0-9a-zA-Z\/\-_:?*\(\|\)\[\]'"]/)) { 279 | emit('setConfigValue', c); 280 | return objectConfigStringValue; 281 | } else { 282 | // 进行下一次匹配的时候,还原 283 | emit('resetConfigKey'); 284 | return endObjectConfigStringValue(c); 285 | } 286 | } 287 | 288 | // 字符串值结束后,会继续转到上一个状态执行 289 | function endObjectConfigStringValue(c) { 290 | // console.log('endObjectConfigStringValue', c); 291 | if (c.match(/[,\s\n\f\t\*\/]/)) { 292 | return endObjectConfigStringValue; 293 | } else if (c.match(/[a-zA-Z]/)) { 294 | return objectConfig(c); 295 | } else if (c === '}') { 296 | return beforeEndConfigOfString; 297 | } else { 298 | emit('endObjectConfigOfString'); 299 | return start; 300 | } 301 | } 302 | 303 | function beforeEndConfigOfString(c) { 304 | // console.log('beforeEndConfigOfString', c); 305 | if (c.match(/[\s\n\t\f]/)) { 306 | return beforeEndConfigOfString; 307 | } else { 308 | // 包括 if (c === ')') 309 | emit('endObjectConfigOfString'); 310 | return start; 311 | } 312 | } 313 | 314 | // 字符串路由状态,字符串也要一同拼接 315 | function stringPath(c) { 316 | // console.log('stringPath', c); 317 | // vue 路由支持正则配置 318 | if (c.match(/[0-9a-zA-Z\/\-_:?*\(\|\)\[\]'"]/)) { 319 | if (!((beforeCharacter === '\'' || beforeCharacter === '"') && c === ')')) { 320 | emit('setPath', c); 321 | } 322 | return stringPath; 323 | } else { 324 | return endStringPath(c); 325 | } 326 | } 327 | 328 | // 结束行级 path 状态 329 | function endStringPath(c) { 330 | // console.log('endStringPath', c); 331 | if (c.match(/[\s\t]/)) { 332 | return endStringPath; 333 | } else if (c === ',') { 334 | return beforeStringAlias; 335 | } else { 336 | // 包括了 if (c === ')') 337 | emit('endStringConfig'); 338 | return start; 339 | } 340 | } 341 | 342 | // 进入 alias 前 343 | function beforeStringAlias(c) { 344 | // console.log('beforeStringAlias', c); 345 | if (c.match(/[\s\t]/)) { 346 | return beforeStringAlias; 347 | } else if (c === '\'' || c === '"') { 348 | // 进入到行级 alias 349 | return stringAlias(c); 350 | } else { 351 | // 包括了 if (c === ')') 352 | emit('endStringConfig'); 353 | return start; 354 | } 355 | } 356 | 357 | // 进入到行级 alias 358 | function stringAlias(c) { 359 | // console.log('stringAlias', c); 360 | // alias 也假装支持正则配置吧 361 | if (c.match(/[0-9a-zA-Z\/\-_:?*\(\|\)\[\]'"]/)) { 362 | if (!((beforeCharacter === '\'' || beforeCharacter === '"') && c === ')')) { 363 | emit('setAlias', c); 364 | } 365 | return stringAlias; 366 | } else { 367 | // 包括了 if (c === ')') 和 alias 匹配失败,还需要保留 path 368 | emit('endStringConfig'); 369 | return start; 370 | } 371 | } 372 | 373 | // 设置初始状态 374 | let state = start; 375 | 376 | for (let c of contentArr) { 377 | // // console.log('loop', c); 378 | state = state(c); 379 | } 380 | return routeConfigs; 381 | } 382 | --------------------------------------------------------------------------------