├── 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 |
2 |
3 |
4 |
13 |
16 |
--------------------------------------------------------------------------------
/example/src/views/user/detail/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
13 |
16 |
--------------------------------------------------------------------------------
/example/src/views/goods/detail/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
13 |
16 |
--------------------------------------------------------------------------------
/example/src/views/user/list.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 |
2 |
3 |
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 |
--------------------------------------------------------------------------------