bootstrap-table-pipeline.js 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. /**
  2. * @author doug-the-guy
  3. * @update zhixin wen <wenzhixin2010@gmail.com>
  4. *
  5. * Bootstrap Table Pipeline
  6. * -----------------------
  7. *
  8. * This plugin enables client side data caching for server side requests which will
  9. * eliminate the need to issue a new request every page change. This will allow
  10. * for a performance balance for a large data set between returning all data at once
  11. * (client side paging) and a new server side request (server side paging).
  12. *
  13. * There are two new options:
  14. * - usePipeline: enables this feature
  15. * - pipelineSize: the size of each cache window
  16. *
  17. * The size of the pipeline must be evenly divisible by the current page size. This is
  18. * assured by rounding up to the nearest evenly divisible value. For example, if
  19. * the pipeline size is 4990 and the current page size is 25, then pipeline size will
  20. * be dynamically set to 5000.
  21. *
  22. * The cache windows are computed based on the pipeline size and the total number of rows
  23. * returned by the server side query. For example, with pipeline size 500 and total rows
  24. * 1300, the cache windows will be:
  25. *
  26. * [{'lower': 0, 'upper': 499}, {'lower': 500, 'upper': 999}, {'lower': 1000, 'upper': 1499}]
  27. *
  28. * Using the limit (i.e. the pipelineSize) and offset parameters, the server side request
  29. * **MUST** return only the data in the requested cache window **AND** the total number of rows.
  30. * To wit, the server side code must use the offset and limit parameters to prepare the response
  31. * data.
  32. *
  33. * On a page change, the new offset is checked if it is within the current cache window. If so,
  34. * the requested page data is returned from the cached data set. Otherwise, a new server side
  35. * request will be issued for the new cache window.
  36. *
  37. * The current cached data is only invalidated on these events:
  38. * * sorting
  39. * * searching
  40. * * page size change
  41. * * page change moves into a new cache window
  42. *
  43. * There are two new events:
  44. * - cached-data-hit.bs.table: issued when cached data is used on a page change
  45. * - cached-data-reset.bs.table: issued when the cached data is invalidated and a
  46. * new server side request is issued
  47. *
  48. **/
  49. const Utils = $.fn.bootstrapTable.utils
  50. Object.assign($.fn.bootstrapTable.defaults, {
  51. usePipeline: false,
  52. pipelineSize: 1000,
  53. // eslint-disable-next-line no-unused-vars
  54. onCachedDataHit (data) {
  55. return false
  56. },
  57. // eslint-disable-next-line no-unused-vars
  58. onCachedDataReset (data) {
  59. return false
  60. }
  61. })
  62. Object.assign($.fn.bootstrapTable.events, {
  63. 'cached-data-hit.bs.table': 'onCachedDataHit',
  64. 'cached-data-reset.bs.table': 'onCachedDataReset'
  65. })
  66. $.BootstrapTable = class extends $.BootstrapTable {
  67. // needs to be called before initServer
  68. init (...args) {
  69. if (this.options.usePipeline) {
  70. this.initPipeline()
  71. }
  72. super.init(...args)
  73. }
  74. initPipeline () {
  75. this.cacheRequestJSON = {}
  76. this.cacheWindows = []
  77. this.currWindow = 0
  78. this.resetCache = true
  79. }
  80. // force a cache reset on search
  81. onSearch (...args) {
  82. if (this.options.usePipeline) {
  83. this.resetCache = true
  84. }
  85. super.onSearch(...args)
  86. }
  87. // force a cache reset on sort
  88. onSort (...args) {
  89. if (this.options.usePipeline) {
  90. this.resetCache = true
  91. }
  92. super.onSort(...args)
  93. }
  94. // rebuild cache window on page size change
  95. onPageListChange (event) {
  96. const target = $(event.currentTarget)
  97. const newPageSize = parseInt(target.text(), 10)
  98. this.options.pipelineSize = this.calculatePipelineSize(this.options.pipelineSize, newPageSize)
  99. this.resetCache = true
  100. super.onPageListChange(event)
  101. }
  102. // calculate pipeline size by rounding up to
  103. // the nearest value evenly divisible by the pageSize
  104. calculatePipelineSize (pipelineSize, pageSize) {
  105. if (pageSize === 0) {
  106. return 0
  107. }
  108. return Math.ceil(pipelineSize / pageSize) * pageSize
  109. }
  110. // set cache windows based on the total number of rows returned
  111. // by server side request and the pipelineSize
  112. setCacheWindows () {
  113. this.cacheWindows = []
  114. for (let i = 0; i <= this.options.totalRows / this.options.pipelineSize; i++) {
  115. const lower = i * this.options.pipelineSize
  116. this.cacheWindows[i] = { lower, upper: lower + this.options.pipelineSize - 1 }
  117. }
  118. }
  119. // set the current cache window index, based on where the current offset falls
  120. setCurrWindow (offset) {
  121. this.currWindow = 0
  122. for (let i = 0; i < this.cacheWindows.length; i++) {
  123. if (this.cacheWindows[i].lower <= offset && offset <= this.cacheWindows[i].upper) {
  124. this.currWindow = i
  125. break
  126. }
  127. }
  128. }
  129. // draw rows from the cache using offset and limit
  130. drawFromCache (offset, limit) {
  131. const res = Utils.extend(true, {}, this.cacheRequestJSON)
  132. const drawStart = offset - this.cacheWindows[this.currWindow].lower
  133. const drawEnd = drawStart + limit
  134. res.rows = res.rows.slice(drawStart, drawEnd)
  135. return res
  136. }
  137. /*
  138. * determine if requested data is in cache (on paging) or if
  139. * a new ajax request needs to be issued (sorting, searching, paging
  140. * moving outside of cached data, page size change)
  141. * initial version of this extension will entirely override base initServer
  142. */
  143. initServer (silent, query) {
  144. if (!this.options.usePipeline) {
  145. return super.initServer(silent, query)
  146. }
  147. let useAjax = true
  148. const params = {}
  149. if (
  150. this.options.queryParamsType === 'limit' &&
  151. this.options.pagination &&
  152. this.options.sidePagination === 'server'
  153. ) {
  154. // same as parent initServer: params.offset
  155. params.offset = this.options.pageSize === this.options.formatAllRows() ?
  156. 0 : this.options.pageSize * (this.options.pageNumber - 1)
  157. params.limit = this.options.pageSize
  158. // if cacheWindows is empty, this is the initial request
  159. if (!this.cacheWindows.length) {
  160. useAjax = true
  161. params.drawOffset = params.offset
  162. // cache exists: determine if the page request is entirely within the current cached window
  163. } else {
  164. const w = this.cacheWindows[this.currWindow]
  165. // case 1: reset cache but stay within current window (e.g. column sort)
  166. // case 2: move outside of the current window (e.g. search or paging)
  167. // since each cache window is aligned with the current page size
  168. // checking if params.offset is outside the current window is sufficient.
  169. // need to re-query for preceding or succeeding cache window
  170. // also handle case
  171. if (this.resetCache || (params.offset < w.lower || params.offset > w.upper)) {
  172. useAjax = true
  173. this.setCurrWindow(params.offset)
  174. // store the relative offset for drawing the page data afterwards
  175. params.drawOffset = params.offset
  176. // now set params.offset to the lower bound of the new cache window
  177. // the server will return that whole cache window
  178. params.offset = this.cacheWindows[this.currWindow].lower
  179. // within current cache window
  180. } else {
  181. useAjax = false
  182. }
  183. }
  184. }
  185. // force an ajax call - this is on search, sort or page size change
  186. if (this.resetCache) {
  187. useAjax = true
  188. this.resetCache = false
  189. }
  190. if (useAjax) {
  191. // in this scenario limit is used on the server to get the cache window
  192. // and drawLimit is used to get the page data afterwards
  193. params.drawLimit = params.limit
  194. params.limit = this.options.pipelineSize
  195. }
  196. // cached results can be used
  197. if (!useAjax) {
  198. const res = this.drawFromCache(params.offset, params.limit)
  199. this.load(res)
  200. this.trigger('load-success', res)
  201. this.trigger('cached-data-hit', res)
  202. return
  203. }
  204. if (!this.pipelineResponseHandler) {
  205. this.pipelineResponseHandler = this.options.responseHandler
  206. this.options.responseHandler = (_res, jqXHR) => {
  207. let res = Utils.calculateObjectValue(this.options, this.pipelineResponseHandler, [_res, jqXHR], _res)
  208. // store entire request in cache
  209. this.cacheRequestJSON = Utils.extend(true, {}, res)
  210. // this gets set in load() also but needs to be set before
  211. // setting cacheWindows
  212. this.options.totalRows = res[this.options.totalField]
  213. // if this is a search, potentially less results will be returned
  214. // so cache windows need to be rebuilt. Otherwise it
  215. // will come out the same
  216. this.setCacheWindows()
  217. // just load data for the page
  218. res = this.drawFromCache(params.drawOffset, params.drawLimit)
  219. this.trigger('cached-data-reset', res)
  220. return res
  221. }
  222. }
  223. return super.initServer(silent, { ...query, ...params })
  224. }
  225. destroy (...args) {
  226. this.options.responseHandler = this.pipelineResponseHandler
  227. this.pipelineResponseHandler = null
  228. super.destroy(...args)
  229. }
  230. }