path-to-regexp使用及源碼解析


一.使用

該方法的作用是把字符串轉為正則表達式。

我們在vue-router中,react-router或koa-router中,我們經常做路由匹配像這種格式的 /foo/:id 這樣的,或者其他更復雜的路由匹配,都能支持,那么這些路由背后是怎么做的呢?其實它就是依賴於 path-to-regexp.js的。下面我們先來了解下 path-to-regexp.js的基本用法

1.1. pathToRegexp()

作用:這里這個方法可以類比於 js 中 new RegExp('xxx')

var pathToRegexp = require('path-to-regexp')

var re = pathToRegexp('/foo/:bar')
console.log(re);    

打印結果如下:

/^\/foo\/((?:[^\/]+?))(?:\/(?=$))?$/i

  

要注意兩點,一點是我們自己的 url 地址,一條是匹配規則。

1.2. exec()

作用:匹配 url 地址與規則是否相符。

var pathToRegexp = require('path-to-regexp')

var re = pathToRegexp('/foo/:bar');     // 匹配規則
var match1 = re.exec('/test/route');    // url 路徑
var match2 = re.exec('/foo/route');     // url 路徑

console.log(match1);
console.log(match2);

  

打印結果如下:

null
[ '/foo/route', 'route', index: 0, input: '/foo/route' ]

  

說明:

上述代碼中,第一個 url 路徑與匹配規則不相符返回 null,第二個 url 路徑與匹配規則相符,返回一個數組。

1.3. parse()

作用:解析 url 字符串中的參數部分(:id)。

var pathToRegexp = require('path-to-regexp');

var url = '/user/:id';
console.log(pathToRegexp.parse(url));

  

打印結果如下:

[ '/user',
  { name: 'id',
    prefix: '/',
    delimiter: '/',
    optional: false,
    repeat: false,
    partial: false,
    pattern: '[^\\/]+?' } ]

  

說明:返回一個數組,從第二個數據可以就可以得到 url 地址攜帶參數的屬性名稱(item.name)。

當然,url 中攜帶參數出了 :id 這種形式,還有 /user?id=11 這種,使用 parse() 方法解析自行查看結果。

1.4. compile()

作用:快速填充 url 字符串的參數值。

var pathToRegexp = require('path-to-regexp')

var url = '/user/:id/:name'
var data = {id: 10001, name: 'bob'}
console.log(pathToRegexp.compile(url)(data))

  

打印結果:

/user/10001/bob

  

復制代碼
const pathToRegExp = require('path-to-regexp');

const t1 = pathToRegExp('/foo/:id');
console.log(t1); // /^\/foo\/([^\/]+?)(?:\/)?$/i

console.log(t1.exec('/foo/barrrr')); 
/* 
[
  0: "/foo/barrrr"
  1: "barrrr"
  groups: undefined
  index: 0
  input: "/foo/barrrr"
]
*/
console.log(t1.exec('/ccccc')); // null

const t2 = pathToRegExp('aaa');
console.log(t2); // /^aaa(?:\/)?$/i
復制代碼

如上代碼中的字符串 '/foo/:id', 中的 '/' 為分隔符,它把多個匹配模式分割開,因此會分成 foo 和 :id, 因此我們正則匹配 foo的時候是完全匹配的,因此正則 是 /^\/foo$/ 這樣的。但是 :id 是命名參數,它可以匹配任何請求路徑字符串。
在命名參數上,我們也可以使用一些修飾符,比如?, + , * 等

1.5. 在字符串后面加上 * 號

復制代碼
const pathToRegExp = require('path-to-regexp');

const t1 = pathToRegExp('/foo/:id*');

console.log(t1.exec('/foo/a/b/c/d'));
// 輸出如下:
/*
  [
    0: "/foo/a/b/c/d"
    1: "a/b/c/d"
    groups: undefined
    index: 0
    input: "/foo/a/b/c/d"
  ]
*/

console.log(t1.exec('/foo'));
/*
 輸出如下:
 [
   0: "/foo"
   1: undefined
   groups: undefined
   index: 0
   input: "/foo"
 ]
*/
復制代碼

*表示我這個命名參數:id可以接收0個或多個匹配模式

1.6 在字符串后面加上 + 號
如下代碼:

復制代碼
const pathToRegExp = require('path-to-regexp');
const t1 = pathToRegExp('/foo/:id+');
console.log(t1.exec('/foo/a/b/c/d'));
// 輸出如下:
/*
  [
    0: "/foo/a/b/c/d"
    1: "a/b/c/d"
    groups: undefined
    index: 0
    input: "/foo/a/b/c/d"
  ]
*/

console.log(t1.exec('/foo')); // null
復制代碼

+ 表示命名參數至少要接收一個匹配模式,也就是說 '/foo/' 后至少有一個匹配模式,如果沒有的話,就會匹配失敗。

1.7 在字符串后面加上 ? 號

復制代碼
const pathToRegExp = require('path-to-regexp');

const t1 = pathToRegExp('/foo/:id?');

console.log(t1.exec('/foo/a/b/c/d')); // null

console.log(t1.exec('/foo/a'));
/*
 輸出為 
 [
   0: "/foo/a"
   1: "a"
   groups: undefined
   index: 0
   input: "/foo/a"
 ]
*/

console.log(t1.exec('/foo')); // null
/*
 輸出為:
 [
   0: "/foo"
   1: undefined
   groups: undefined
   index: 0
   input: "/foo"
 ]
*/
復制代碼

? 表示命名參數可以接收0個或1個匹配模式,如果為多個匹配模式的話,就會返回null.

1.8 pathToRegexp 方法的第二個參數keys,默認我們可以傳入一個數組,默認為 []; 我們來看下

復制代碼
const pathToRegExp = require('path-to-regexp');

const keys = []; 
var t1 = pathToRegExp('/:foo/icon-(\\d+).png',keys)
const t11 = t1.exec('/home/icon-123.png');
const t12 = t1.exec('/about/icon-abc.png');

console.log(t11);
/*
 打印輸出為:
 [
   0: "/home/icon-123.png"
   1: "home"
   2: "123"
   groups: undefined
   index: 0
   input: "/home/icon-123.png"
 ]
*/

console.log(t12); // 輸出為null

console.log(keys);

/*
 輸出值為:
 [
   {
     delimiter: "/"
     name: "foo"
     optional: false
     pattern: "[^\/]+?"
     prefix: "/"
     repeat: false
   },
   {
     delimiter: "-"
     name: 0
     optional: false
     pattern: "\d+"
     prefix: "-"
     repeat: false
   }
 ]
*/
復制代碼

注意如上:未命名參數的keys.name為0。

1.9 第三個參數options,為一個對象,包含如下對應的key值:

復制代碼
options = {
  delimiter: '',
  whitelist: '',
  strict: '',
  start: '',
  end: '',
  endsWith: ''
}
復制代碼

我們可以看到官網對這些字段的含義解析如下:

更多請看github上的使用方式 (https://github.com/pillarjs/path-to-regexp

注意:至於其他的 Parse方法使用及 Compile 方法的使用,tokensToRegExp 和 tokensToFunction 請看 github上的demo。

github上的源碼及使用(https://github.com/pillarjs/path-to-regexp

二.源碼解析

1:path-to-regexp.js 源碼分析如下:

首先從源碼中該js文件對外暴露了5個方法,源碼如下:

module.exports = pathToRegexp
module.exports.parse = parse
module.exports.compile = compile
module.exports.tokensToFunction = tokensToFunction
module.exports.tokensToRegExp = tokensToRegExp

首先要說明下的是:分析源碼的最好的方式是:做個demo,然后在頁面上執行結果打上斷點一步步調式。就能理解代碼的基本含義了。

首先path-to-regexp.js源碼如下初始化一些數據:

復制代碼
/**
 * Default configs.
 默認的配置項在 '/' 下
 */
var DEFAULT_DELIMITER = '/'; 

/**
 * The main path matching regexp utility.
 *
 * @type {RegExp}
 */
var PATH_REGEXP = new RegExp([
  // Match escaped characters that would otherwise appear in future matches.
  // This allows the user to escape special characters that won't transform.
  '(\\\\.)',
  // Match Express-style parameters and un-named parameters with a prefix
  // and optional suffixes. Matches appear as:
  //
  // ":test(\\d+)?" => ["test", "\d+", undefined, "?"]
  // "(\\d+)"  => [undefined, undefined, "\d+", undefined]
  '(?:\\:(\\w+)(?:\\(((?:\\\\.|[^\\\\()])+)\\))?|\\(((?:\\\\.|[^\\\\()])+)\\))([+*?])?'
].join('|'), 'g');
復制代碼

然后看源碼中看到一個很復雜的正則。我們來分析下該正則的含義,因為下面會使用到該正則返回的 PATH_REGEXP 來匹配的。

1. new RegExp('\\\\.', 'g'); 的含義是:在 new RegExp對象后,會返回 /\\./g, 然后是匹配字符串 \\ , 點號(.) 是元字符匹配任意的字符。因此如下測試代碼可以理解具體的作用了:

復制代碼
var reg12 = new RegExp('\\\\.', 'g');

console.log(reg12); // 輸出 /\\./g

console.log(reg12.test('.')); // false
console.log(reg12.test('\.')); // false
console.log(reg12.test('\a')); // false
console.log(reg12.test('.a')); // false
console.log(reg12.test('n.a')); // false

console.log(reg12.test('\\a')); // true
console.log(reg12.test('\\aaaa')); // false

console.log(reg12.test('\\.')); // true
復制代碼

想要詳細了解相關的知識點,請看這篇文章

2. (?:\\:(\\w+)(?:\\(((?:\\\\.|[^\\\\()])+)\\))?|\\(((?:\\\\.|[^\\\\()])+)\\))([+*?])?

如上復雜的正則表達式可以分解為如下:

復制代碼
(?:\\:
  (\\w+)
  (?:\\
    (
      (
        (?:\\\\.|[^\\\\()])+
      )\\
    )
  )? | \\ 
  (
    (
      (?:\\\\.|[^\\\\()])+
    )\\
  )
)([+*?])?
復制代碼

如上正則表達式,我們把它分解成如上所示,俗話說,正則表達式不管它有多復雜,我們可以學會一步步分解出來。

1. (?:\\:)的含義:?:以這樣的開頭,我們可以理解為 非捕獲性分組。非捕獲性分組的含義可以理解:子表達式可以作為被整體修飾但是子表達式匹配的結果不會被存儲;什么意思呢?比如如下demo:

復制代碼
// 非捕獲性分組
var num2 = "11 22";
/#(?:\d+)/.test(num2);
console.log(RegExp.$1); // 輸出:""

var num2 = "11aa22";
console.log(/(?:\d+)/.test(num2)); // 返回 true
復制代碼

具體理解非捕獲性分組我們可以看這篇文章.

因此 (?:\\:) 的含義是 匹配 \\: 這樣的字符串。比如如下測試demo:

/(?:\\:)/g.test("\\:"); // 返回true
/(?:\\:)/g.test("\\:a"); // 返回true

因此我們可以總結 (?:\\:) 的具體含義是:使用非捕獲性分組,只要能匹配到字符串中含有 \\: 就返回true.

2. (\\w+) 的含義:

\w; 查找任意一個字母或數字或下划線,等價於A_Za_z0_9,_ 那么 \\w+ 呢?請看如下demo

const t1 = new RegExp('\\w+', 'g');
console.log(t1); // 輸出 /\w+/g

console.log(t1.test('11')); // true

也就是說 \\w+ 在 RegExp中實列化后,變成了 /\w+/g, 、\w+ 的含義就是匹配任意一個或多個字母、數字、下划線。

3. (?:\\ 和 第一點是一樣的。就是匹配 字符串中包含的 '\\' 這個的字符。

4. (?:\\\\.|[^\\\\()])+ 中的 ?:\\\\. , 上面介紹了 (?:)這是非捕獲性分組,\\\\.的含義就是匹配字符串中 "\\" , 點號(.) 是元字符匹配任意的字符。然后元字符+ 就是匹配至少一個或多個。比如如下demo。

復制代碼
const t1 = new RegExp('(?:\\\\.)+', 'g');
console.log(t1); // 輸出 /(?:\\.)+/g

/(?:\\.)+/g.test("\\\\\aaaa"); // true
/(?:\\.)+/g.test("\\\\\bbbb"); // true
/(?:\\.)+/g.test("\\..."); // true
復制代碼

([^\\\\()])+ 的含義是:分組匹配 ([^\\()])+

const t1 = new RegExp('([^\\\\()])+', 'g');
console.log(t1); // 輸出 /([^\\()])+/g;

如下圖所示:

應該就是任意一個字符吧,如果是空字符串的話,返回false.

后面的正則表達式也是差不多的意思。

一: pathToRegExp

該方法的作用是將路徑字符串轉換為正則表達式。
如下基本代碼:

復制代碼
/**
 * Normalize the given path string, returning a regular expression.
 *
 * An empty array can be passed in for the keys, which will hold the
 * placeholder key descriptions. For example, using `/user/:id`, `keys` will
 * contain `[{ name: 'id', delimiter: '/', optional: false, repeat: false }]`.
 *
 * @param  {(string|RegExp|Array)} path
 * @param  {Array=}                keys
 * @param  {Object=}               options
 * @return {!RegExp}
 */
function pathToRegexp (path, keys, options) {
  if (path instanceof RegExp) {
    return regexpToRegexp(path, keys)
  }

  if (Array.isArray(path)) {
    return arrayToRegexp(/** @type {!Array} */ (path), keys, options)
  }

  return stringToRegexp(/** @type {string} */ (path), keys, options)
}
復制代碼

該方法有三個參數:
@param path {string|RegExp|Array} 為url路徑,它的類型為一個字符串、正則表達式或一個數組.
@param keys {Array} 默認為空數組 []
@param options {Object} 為一個對象。
@return 返回的是一個正則表達式

pathToRegexp代碼的基本含義如下:
1. 判斷該路徑是否是正則表達式的實列,if (path instanceof RegExp) {}, 如果是的話,就直接返回正則表達式 return regexpToRegexp(path, keys);

2. 判斷該路徑是否是一個數組,如果是一個數組的話,if (Array.isArray(path)) {}, 那么就把數組轉換為 正則表達式,如代碼:return arrayToRegexp((path), keys, options);

3. 如果即不是正則表達式的實列,也不是一個數組的話,那就是字符串了,因此使用把字符串轉換為正則表達式,如代碼: return stringToRegexp((path), keys, options);

比如如下demo,傳入的path是一個字符串路徑,它返回的是一個正則表達式。

3.1 只有第一個參數字符串。

復制代碼
const pathToRegExp = require('path-to-regexp');

const t1 = pathToRegExp('/foo/:id');
console.log(t1); // /^\/foo\/([^\/]+?)(?:\/)?$/i

// 普通的字符串
const t2 = pathToRegExp('aaa');
console.log(t2); // /^aaa(?:\/)?$/i
復制代碼

我們先打個斷點看看,它代碼是如何執行的:

如下圖所示:

可以看到,斷點會先進入 pathToRegExp 方法內部,代碼如上,先判斷第一個參數path是否是一個字符串,還是是一個正則表達式,或者是一個數組,由於我們傳入的是一個字符串,因此會調用 return stringToRegexp((path), keys, options); 這個方法內部執行。如下 stringToRegexp 函數方法內部,如下所示:

我們可以看到,第一個參數path傳入的是一個字符串 "/foo/:id", 然后第二個參數和第三個參數我們都沒有傳遞,因此他們都為undefined。最后他們會調用 tokensToRegExp 這個函數,但是在調用該函數之前,會先調用 parse這個方法:parse(path, options);我們先進入 parse這個方法內部看看情況。

parse函數代碼如下:

復制代碼
/**
 * Parse a string for the raw tokens.
 *
 * @param  {string}  str
 * @param  {Object=} options
 * @return {!Array}
 */
function parse (str, options) {
  var tokens = []
  var key = 0
  var index = 0
  var path = ''
  var defaultDelimiter = (options && options.delimiter) || DEFAULT_DELIMITER
  var whitelist = (options && options.whitelist) || undefined
  var pathEscaped = false
  var res

  while ((res = PATH_REGEXP.exec(str)) !== null) {
    var m = res[0]
    var escaped = res[1]
    var offset = res.index
    path += str.slice(index, offset)
    index = offset + m.length

    // Ignore already escaped sequences.
    if (escaped) {
      path += escaped[1]
      pathEscaped = true
      continue
    }

    var prev = ''
    var name = res[2]
    var capture = res[3]
    var group = res[4]
    var modifier = res[5]

    if (!pathEscaped && path.length) {
      var k = path.length - 1
      var c = path[k]
      var matches = whitelist ? whitelist.indexOf(c) > -1 : true

      if (matches) {
        prev = c
        path = path.slice(0, k)
      }
    }

    // Push the current path onto the tokens.
    if (path) {
      tokens.push(path)
      path = ''
      pathEscaped = false
    }

    var repeat = modifier === '+' || modifier === '*'
    var optional = modifier === '?' || modifier === '*'
    var pattern = capture || group
    var delimiter = prev || defaultDelimiter

    tokens.push({
      name: name || key++,
      prefix: prev,
      delimiter: delimiter,
      optional: optional,
      repeat: repeat,
      pattern: pattern
        ? escapeGroup(pattern)
        : '[^' + escapeString(delimiter === defaultDelimiter ? delimiter : (delimiter + defaultDelimiter)) + ']+?'
    })
  }

  // Push any remaining characters.
  if (path || index < str.length) {
    tokens.push(path + str.substr(index))
  }

  return tokens
}
復制代碼

該方法同樣我們傳入了兩個參數,第一個是 path這個路徑,第二個是 options,該參數是一個對象,看這句代碼:

var defaultDelimiter = (options && options.delimiter) || DEFAULT_DELIMITER;

由此可見,該options對象有一個參數 delimiter, 我們可以先理解為一個分隔符吧。默認為 DEFAULT_DELIMITER = '/' 這樣的。

var whitelist = (options && options.whitelist) || undefined 這句代碼的時候,我們也可以看到options對象也有一個 whitelist 該key。具體做什么用的,我們現在還未知,不過沒有關系,我們一步步先走下去。

while ((res = PATH_REGEXP.exec(str)) !== null) {} 當代碼執行到這句的時候,使用while循環,如果res = PATH_REGEXP.exec(str)) !== null, 當res不是null的時候,就執行下面的代碼,我們先來看下使用正則中的exec方法執行完成后,一般會返回什么,如下基本的測試 exec代碼:

復制代碼
var reg = new RegExp([
  // Match escaped characters that would otherwise appear in future matches.
  // This allows the user to escape special characters that won't transform.
  '(\\\\.)',
  // Match Express-style parameters and un-named parameters with a prefix
  // and optional suffixes. Matches appear as:
  //
  // ":test(\\d+)?" => ["test", "\d+", undefined, "?"]
  // "(\\d+)"  => [undefined, undefined, "\d+", undefined]
  '(?:\\:(\\w+)(?:\\(((?:\\\\.|[^\\\\()])+)\\))?|\\(((?:\\\\.|[^\\\\()])+)\\))([+*?])?'
].join('|'), 'g');

var str = "/foo/:id";
console.log(reg.exec(str));
復制代碼

如下圖是 console.log(reg.exec(str)); 這句代碼輸出的數據;

exec() 該方法如果找到了匹配的文本的話,則會返回一個結果數組,否則的話,會返回一個null。

該數組的第0個元素的含義是:它是與正則相匹配的文本。

第1個元素是與RegExpObject的第1個子表達式相匹配的文本。如果沒有的話,就返回undefined.
第2個元素是與RegExpObject的第2個子表達式相匹配的文本,如果沒有的話,就返回undefined。
.... 依次類推。

除了這些返回之外,exec方法還反回了兩個屬性,
index: 該屬性是聲明的匹配文本的第一個字符的位置。
input: 該屬性是存放的是被檢索的字符串。

因此exec方法返回的是一個數組,具體的對應的含義就是上面的解釋的哦。

我們使用斷點可以看到如下代碼的截取的數據,如下圖所示:

如上就是 parse 函數返回的數據了,現在我們繼續進入 tokensToRegExp 函數,看如何轉為正則表達式了。

tokensToRegExp 函數第一個參數 tokens 如下值就是執行完 parse函數返回的值了。

我們繼續走可以看到如下所示:

注意:我們現在就能明白 第三個參數 options 傳進來的是一個對象,它有哪些key呢?從上面我們分析可知:
有如下key配置項:

復制代碼
options = {
  delimiter: '',
  whitelist: '',
  strict: '',
  start: '',
  end: '',
  endsWith: ''
}
復制代碼

如上是目前知道的配置項,該配置項具體是什么作用,我們目前還未知,我們可以繼續走下代碼看看;

我們接下來是遍歷傳進來的tokens了。tokens它是一個數組,具體的可以看如上所示。

tokens 第一個參數為'/foo'; 因此進入 route += escapeString(token); 因此會調用 escapeString 函數,然后把值返回回來給 route; escapeString 函數代碼如下:

復制代碼
/**
 * Escape a regular expression string.
 *
 * @param  {string} str
 * @return {string}
 */
function escapeString (str) {
  return str.replace(/([.+*?=^!:${}()[\]|/\\])/g, '\\$1')
}
復制代碼

因此最后返回給 route 的值為 "\/foo", 代碼執行如下所示:

執行完成后,如上所示,我們會繼續循環tokens, 因此會獲取到第二個元素了,第二個元素是一個對象,該值為如下:

因此我們會進入 else語句代碼內部了,首先判斷:

var capture = token.repeat
    ? '(?:' + token.pattern + ')(?:' + escapeString(token.delimiter) + '(?:' + token.pattern + '))*'
    : token.pattern

判斷 token的屬性 repeat 是否為true,如果為true的話,就使用 ? 后面的表達式,否則的是 token.pattern的值
了。

如下圖所示:

最后我們返回的 route就是如下的正則表達式了;

最后就是代碼繼續判斷了,執行結果為如下:

我們可以看到,執行結果后就是返回的我們的正則表達式了:

const pathToRegExp = require('path-to-regexp');

const t1 = pathToRegExp('/foo/:id');
console.log(t1); // /^\/foo\/([^\/]+?)(?:\/)?$/i

和上面打印的是類似的。

總結:當我們使用 pathToRegexp 將字符串轉換為正則表達式的時候,第一個參數為字符串,第二個參數和第三個參數為undefined的時候,首先會調用

function pathToRegexp (path, keys, options) {
  return stringToRegexp(/** @type {string} */ (path), keys, options);
} 

這個函數,然后接着調用 stringToRegexp 這個函數,會將字符串轉化為正則表達式,該函數傳入三個參數,第一個參數為字符串,第二個參數和第三個參數目前為undefined。現在我們來看下stringToRegexp函數代碼如下:

復制代碼
/**
 * Create a path regexp from string input.
 *
 * @param  {string}  path
 * @param  {Array=}  keys
 * @param  {Object=} options
 * @return {!RegExp}
 */
function stringToRegexp (path, keys, options) {
  return tokensToRegExp(parse(path, options), keys, options)
}
復制代碼

接着會調用 tokensToRegExp 函數,將字符串轉換為真正的正則,在調用該方法之前,會先調用 parse 方法,會將字符串使用exec方法匹配,如果匹配成功的話,就返回exec匹配成功后的一個數組,里面會包含很多字段。如下代碼:

復制代碼
function parse (str, options) {
  var tokens = []
  var key = 0
  var index = 0
  var path = ''
  var defaultDelimiter = (options && options.delimiter) || DEFAULT_DELIMITER
  var whitelist = (options && options.whitelist) || undefined
  var pathEscaped = false
  var res

  while ((res = PATH_REGEXP.exec(str)) !== null) {
    var m = res[0]
    var escaped = res[1]
    var offset = res.index
    path += str.slice(index, offset)
    index = offset + m.length

    // Ignore already escaped sequences.
    if (escaped) {
      path += escaped[1]
      pathEscaped = true
      continue
    }

    var prev = ''
    var name = res[2]
    var capture = res[3]
    var group = res[4]
    var modifier = res[5]

    if (!pathEscaped && path.length) {
      var k = path.length - 1
      var c = path[k]
      var matches = whitelist ? whitelist.indexOf(c) > -1 : true

      if (matches) {
        prev = c
        path = path.slice(0, k)
      }
    }

    // Push the current path onto the tokens.
    if (path) {
      tokens.push(path)
      path = ''
      pathEscaped = false
    }

    var repeat = modifier === '+' || modifier === '*'
    var optional = modifier === '?' || modifier === '*'
    var pattern = capture || group
    var delimiter = prev || defaultDelimiter

    tokens.push({
      name: name || key++,
      prefix: prev,
      delimiter: delimiter,
      optional: optional,
      repeat: repeat,
      pattern: pattern
        ? escapeGroup(pattern)
        : '[^' + escapeString(delimiter === defaultDelimiter ? delimiter : (delimiter + defaultDelimiter)) + ']+?'
    })
  }

  // Push any remaining characters.
  if (path || index < str.length) {
    tokens.push(path + str.substr(index))
  }

  return tokens
}
復制代碼

如上代碼,首先會 while ((res = PATH_REGEXP.exec(str)) !== null) {} 匹配'/foo/:id',將結果保存到res中,再判斷res是否為null,如果沒有匹配到的話,就返回null,如果匹配到了,就返回匹配后的結果。
因此匹配到了 :id, 因此res匹配的結果如下:

復制代碼
[
  ":id",
  undefined,
  "id",
  undefined,
  undefined,
  undefined,
  groups: undefined,
  index: 5,
  input: '/foo/:id' 
]
復制代碼

如下圖所示:

接着執行這段代碼:

復制代碼
var m = res[0]
var escaped = res[1]
var offset = res.index
path += str.slice(index, offset)
index = offset + m.length

// Ignore already escaped sequences.
if (escaped) {
  path += escaped[1]
  pathEscaped = true
  continue
}

var prev = ''
var name = res[2]
var capture = res[3]
var group = res[4]
var modifier = res[5]
復制代碼

因此 m = ":id", escaped = undefined, offset = 5, path += str.slice(index, offset); 因此 path = '/foo/:id'.slice(0, 5); path = '/foo/'; 接着 index = offset + m.length = 5 + 3 = 8; 接着判斷 有沒有 escaped,上面可知為undefined,因此不會進入if語句內部,接着 prev = ''; name = res[2] = 'id'; capture = res[3] = undefined, group = undefined, modifier = res[5] = undefined;

再接着執行下面的代碼:

復制代碼
if (!pathEscaped && path.length) {
var k = path.length - 1
var c = path[k]
var matches = whitelist ? whitelist.indexOf(c) > -1 : true

if (matches) {
  prev = c
  path = path.slice(0, k)
}
}

// Push the current path onto the tokens.
if (path) {
tokens.push(path)
path = ''
pathEscaped = false
}

var repeat = modifier === '+' || modifier === '*'
var optional = modifier === '?' || modifier === '*'
var pattern = capture || group
var delimiter = prev || defaultDelimiter
復制代碼

首先 pathEscaped 為false, !pathEscaped 所以為true,path = '/foo/', 因此 也有 path.length 的長度了,所以會進入if語句內部,var k = path.length - 1 = 4; var c = path[4] = '/'; var matches = whitelist ? whitelist.indexOf(c) > -1 : true; whitelist 是第三個參數 options對象的key, 由於第三個參數傳了undefined進來,因此whitelist就為undefined; 因此 matches = true; if (matches) {} 這個判斷語句會進入,prev = 4; path = '/foo/'.slice(0, 4) = '/foo';
再接着執行代碼:

if (path) {
  tokens.push(path)
  path = ''
  pathEscaped = false
}

因此 tokens = ['/foo']; 然后置空 path = ''; pathEscaped 設置為false; 繼續定義如下:

var repeat = modifier === '+' || modifier === '*'
var optional = modifier === '?' || modifier === '*'
var pattern = capture || group
var delimiter = prev || defaultDelimiter

從上面的代碼分析可知 modifier = res[5] = undefined; 因此 repeat = false; optional = false; pattern = capture || group; capture 和 group 上面也是為undefined, 因此 pattern = undefined; var delimiter = prev || defaultDelimiter = '/';

最后執行

復制代碼
tokens.push({
  name: name || key++,
  prefix: prev,
  delimiter: delimiter,
  optional: optional,
  repeat: repeat,
  pattern: pattern
    ? escapeGroup(pattern)
    : '[^' + escapeString(delimiter === defaultDelimiter ? delimiter : (delimiter + defaultDelimiter)) + ']+?'
})
復制代碼

因此 tokens 最后變成如下數據:

復制代碼
tokens = [
  "/foo",
  {
    name: 'id',
    delimiter: '/',
    optional: false,
    pattern: "[^\/]+?"
    prefix: "/"
    repeat: false
  }
]
復制代碼

最后代碼,再如下:

復制代碼
// Push any remaining characters.
if (path || index < str.length) {
  tokens.push(path + str.substr(index))
}

return tokens
復制代碼

tokens最終的值變成如下:

var p = path + str.substr(index); p = '' + str.substr(8) = '/foo/:id'.substr(8) + '' = '';
復制代碼
最終 tokens = [
  "/foo",
  {
    name: 'id',
    delimiter: '/',
    optional: false,
    pattern: "[^\/]+?"
    prefix: "/"
    repeat: false
  }
]
復制代碼

執行完 path 函數后,我們返回了 tokens的值了,接着我們繼續調用 tokensToRegExp(parse(path, options), keys, options) 這個函數,我們會進入該函數的內部了。

首先代碼初始化如下:

復制代碼
options = options || {}

var strict = options.strict
var start = options.start !== false
var end = options.end !== false
var delimiter = options.delimiter || DEFAULT_DELIMITER
var endsWith = [].concat(options.endsWith || []).map(escapeString).concat('$').join('|')
var route = start ? '^' : ''
復制代碼

首先我們傳進來的 options = undefined; 因此 strict = undefined; start = true; end = true;

delimiter = DEFAULT_DELIMITER = '/';
endsWith = [].concat([]).map(escapeString).concat('$').join('|'). escapeString 代碼如下:

function escapeString (str) {
  return str.replace(/([.+*?=^!:${}()[\]|/\\])/g, '\\$1')
}

最后 endsWith = '$';

接着就是代碼for循環了,如下代碼:

復制代碼
for (var i = 0; i < tokens.length; i++) {
  var token = tokens[i]

  if (typeof token === 'string') {
    route += escapeString(token)
  } else {
    var capture = token.repeat
      ? '(?:' + token.pattern + ')(?:' + escapeString(token.delimiter) + '(?:' + token.pattern + '))*'
      : token.pattern

    if (keys) keys.push(token)

    if (token.optional) {
      if (!token.prefix) {
        route += '(' + capture + ')?'
      } else {
        route += '(?:' + escapeString(token.prefix) + '(' + capture + '))?'
      }
    } else {
      route += escapeString(token.prefix) + '(' + capture + ')'
    }
  }
}
復制代碼

我們從上面可知 tokens 值返回的是如下:

復制代碼
tokens = [
  "/foo",
  {
    name: 'id',
    delimiter: '/',
    optional: false,
    pattern: "[^\/]+?"
    prefix: "/"
    repeat: false
  }
]
復制代碼

因此第一次循環,判斷tokens[0] 是否是一個字符串,是字符串的話,就直接進入了第一個if語句代碼內部,因此route = escapeString(tokens[0]); 就調用escapeString 函數內部代碼了,因此最終調用的代碼:

'/foo'.replace(/([.+*?=^!:()[\]|/])/g,()[\]|/])/g,′1'); 最后 route = "\/foo";

接着第二次循環,該第二個參數是一個對象,就會else語句代碼內部,就會執行下面這段代碼:

復制代碼
var capture = token.repeat
? '(?:' + token.pattern + ')(?:' + escapeString(token.delimiter) + '(?:' + token.pattern + '))*'
: token.pattern

if (keys) keys.push(token)
復制代碼

token.repeat 它的值為false的,因此 capture = token.pattern = "[^\/]+?";

token.optional = false, 因此也就進入else語句代碼內部了,執行代碼:

route += escapeString(token.prefix) + '(' + capture + ')';

最后route = escapeString('/') = "\/foo" + "\/" + '(' + capture + ')'; = "\/foo" + "\/" + "[^\/]+?"
= "^\/foo\/([^\/]+?)"

最后代碼如下:

復制代碼
if (end) {
  if (!strict) route += '(?:' + escapeString(delimiter) + ')?'
  route += endsWith === '$' ? '$' : '(?=' + endsWith + ')'
} else {
  var endToken = tokens[tokens.length - 1]
  var isEndDelimited = typeof endToken === 'string'
    ? endToken[endToken.length - 1] === delimiter
    : endToken === undefined

  if (!strict) route += '(?:' + escapeString(delimiter) + '(?=' + endsWith + '))?'
  if (!isEndDelimited) route += '(?=' + escapeString(delimiter) + '|' + endsWith + ')'
}

return new RegExp(route, flags(options))
復制代碼

如上可知 end 為true,因此會進入if語句,strict 為undefined,因此 !strict 就為true了,所以 route = '(?:' + escapeString(delimiter) + ')?' = "^\/foo\/([^\/]+?)(?:\/)?";

再接着 route += endsWith === '?′?′' : '(?=' + endsWith + ')'; 由上面可知 endsWith 就是等於 ;;因此會在尾部再加上 符號,最后 route的值變為 "^\/foo\/([^\/]+?)(?:\/)?$";

最后會調用 return new RegExp(route, flags(options)); flags代碼如下:

function flags (options) {
  return options && options.sensitive ? '' : 'i'
}

因為 options 傳入的參數為 undefined, 因此 最終 返回的是 i 了; 因此轉為正則的話 new RegExp = (route, 'i') = "^\/foo\/([^\/]+?)(?:\/)?$/i"; i 的含義是不區分大小寫。

如上就是 pathToRegExp 對字符串轉換為正則表達式的全部過程,可以看到設計的復雜性及設計該代碼的人的厲害。

注意:其他的方法源碼我就不一一分析了,大家有空自己可以看下,目前的基本的公用函數都已經分析到了,最主要的公用函數就是:parse, tokensToRegExp等,當然里面還有類似這個函數 tokensToFunction 有興趣的也可以分析下,發現分析源碼很耗時。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM