上一个版本,我们做到了通过简单的路由匹配来加载一级页面组件。在这个版本中,我们逐步完善了以下功能:
- 路由匹配更多模式
- 可以无限添加子组件
一,路由匹配更多模式
通过vue-router,我们可以发现路由匹配会遇到很多种情况,比较基本的大概有三种情况:
- 短路由,比如 /welcom
- 长路由,比如/welcome/aa/bb
- 带有参数的路由,比如/welcome/:name/:id
说到这里,其实这个路由匹配我已经做了三个版本了。
第一个版本,思路是把当前的path进行数组分割,比如把 /pro/a分割成 [pro,a] ,然后去路由配置中找组件,最终找到类似[component1, component2]数据,从而加载每个组件。
不过很快就发现,这种思路不行,比如,如果碰到 /pro 这样的path,路由配置的实际情况是 /pro 会有自己的子路由,子路由的path等于"",通过这种方式只能找到 /pro 的组件,并不能找到默认子路由。而/pro的path,需要找到子路由的组件。如果不明白见vue-router文件嵌套路由那章。
第二个版本,为了解决子路由的默认组件情况,我修改了方案,这个方案可以理解为顺藤摸瓜,刚开始也是分割path(比如 :[pro, a]),但是分割后不再各自去路由配置文件中去匹配组件,而是,通过先找到数组中的第一个路径(比如:pro)匹配到根组件,再继续看数组是否有第二个路径(比如:a),如果有则在当前的已经找到的根组件配置中找子组件路由去匹配。如果没有则在当前子路由找有没有path等于“”的默认路由进行加载,如果连默认路由都没有,就加载404.
按照这种方案也有问题,比如遇到了 /welcome/a 这样路径,而在我们的配置文件中,我们会有两个相同名字的路由文件,但是有些区别。比如第一个路由 /welcome 下面有个子路由 path 等于 /b,第二个路由/welcome/:id 下面有个子路由 path 等于 ""。按照顺藤摸瓜的模式,我们匹配到了第一个路由,但是没有找到 /a的,所以,404了,其实,我们预期的结果是能匹配到第二个路由。当然,造成这个问题,是因为我在匹配到第一路由的时候就break掉了。并没有继续循环下一个,因为我也是特意这么做的,如果全部循环可能会找到想要的,但是也可能会出现极端情况,出现三个/welcome路由,中间的那个是我想要的,但是因为没有break,我得继续向下找,找到第三个错误的路由,就错过了第二个正确的。
第三版本,这个是全匹配模式,思路是,刚开始对路由配置文件进行处理,把子路由的path和父路由的path进行组合,形成一个完整路由然后再进行匹配。这个方式目前用的比较好。下面详细介绍下这种方式。
1.1 路由转换
let routersMap = [];
/**
* 收集路由配置的所有path
*/
const createAllRouterPath = (routers, lastPath, lastComponent) => {
if (!routers || routers.length <= 0) return;
for(let i = 0, len = routers.length; i < len; i++){
(function(i, lastPath, lastComponent){
let {path, children, component} = routers[i];
if (component == undefined) return;
// 如果当前路径前面没有 / ,则给他加上
if (path && path.indexOf('/') !== 0) path = '/' + path;
if (path.indexOf(':') >= 0) path = path.replace(/\/:[\w]*/g, '/:?[\\w]*')
lastPath += path;
lastComponent += ',' + component
if (children){
createAllRouterPath(children, lastPath, lastComponent);
} else {
routersMap.push({path: lastPath, components: lastComponent.split(',').filter(Boolean)});
}
})(i, lastPath, lastComponent)
}
}
上面的代码我们用使用递归去获取组织完整的path和path对于的全部组件,再用闭包解决for循环的时候数据丢失的问题。最终我们会把路由的配置信息转换成新的集合存在 routersMap中,下图就是源配置信息(左图)转最终结果(右图)。
1.2 路由匹配
/**
* 匹配路由,加载组件
* */
const matchingRouterComponent = async (path, callback) => {
if (!path) return;
let _components = [];
for(let i = 0, len = routersMap.length; i < len; i++){
const {path: _path, components} = routersMap[i];
// 添加上开始和结束标记,可精准匹配
const reg = new RegExp('^' + _path + '$', 'g');
if (path === _path || (_path.indexOf(':') >= 0 && reg.test(path))){
_components = components
break
}
}
for(let i = 0, len = _components.length; i < len; i++){
await callback(_components[i], i);
}
}
通过以上代码,只要提供了当前path和回调函数(加载组件或者销毁组件函数),在这里就通过循环 routersMap 中的path,去做全等于匹配和正则匹配。如果匹配成功就会跳出循环,加载当前path的所有组件。
二,加载无限子组件
加载子路由的组件,需要在父组件中设定一个容器,如果是只加载一个子组件只需要设定 这样的容器,然后把子组件插入到这个容器中即可。但是,在无限子路由中,我们就需要辨别下每个容器之间有什么不同。辨别具有相同class的div标签,我们需要给标签添加一个唯一标识。
/**
* 重新加载组件
* */
const reloadPageComponent = (component, loadPathIndex) => {
return new Promise(resolve=>{
const defaultFiles = ['index.html', 'index.js', 'index.css'];
let html_path = `path1/${component}/${defaultFiles[0]}`;
let css_path = `path1/${component}/${defaultFiles[2]}`;
let js_path = `path1/${component}/${defaultFiles[1]}`;
// https://github.com/seajs/seajs/issues/1135
// seajs的书写规范,使用require,其参数不能有变量,必须是直接字符串,否则需要使用.async方式加载
require.async(html_path, (_html)=>{
const _uuid = _createUUID('data-v');
// 给路由容器添加唯一标识
const {res, template} = _compile_routerView_add_uuid(_html, _uuid);
if (res) temp_uuid.push(_uuid);
if (loadPathIndex === 0){
document.getElementById('app').innerHTML = template;
// 当重新开始加载根目录的时候,需要重置临时uuid,再根据状态判断保存
temp_uuid = [];
if (res) temp_uuid.push(_uuid);
} else{
const uuid = temp_uuid.shift();
mounted(template, uuid);
}
require.async(js_path);
require.async(css_path);
resolve();
});
})
}
加载组件的代码逻辑大概是,我们通过seajs加载html,并且查看html代码中是否有 class='router-view' 的关键词,如果有就生成 uuid并添加到html中,然后返回新的html和匹配结果。
通过 loadPathIndex 区分当前加载的组件是不是根组件,如果是直接插入到 app 的容器中。否则就插入到子组件容器里。并根据 uuid 来区分应该插入到哪个子组件容器,从而形成无限子路由的加载。
三,总结
以上,完成了更多了路由匹配和无限加载子路由,后面我们将要完善路由的history模式和路由的一些基本对外使用接口,比如push()。