MIP Shell

    这里列举几个比较常见的通过个性化 Shell 可以实现的需求,供大家参考:

    • 对于默认的头部标题栏样式或者 DOM 结构不满意,有修改的需求。

    • 除了头部,还有底部栏或者侧边栏需要额外渲染和绑定事件。例如下图:

    • 开发者需要控制站点的 Shell 配置,修改/禁用/忽略某些选项。

      例如开发者希望忽略 HTML 中的配置项而固定选择某些按钮,或者希望在配置之外增加某些按钮等。

    全局的 MIP 对象会暴露一个 MIP Shell 基类供大家继承。例如我们要创建一个 MIP Shell Example 组件,我们可以写如下代码:

    类名使用驼峰命名,组件平台会自动把驼峰转化为符合 HTML 规范的短划线连接形式,如 。

    个性化 Shell 的编写规范和普通组件相同,同样在 mip2-extensions 项目中编写,如下:

    MIP Shell Folder

    个性化 Shell 的使用和内置的 MIP Shell 基本类似。唯一的区别是为标签增加一个属性 mip-shell,例子如下:

    1. <mip-shell-example mip-shell>
    2. <script type="application/json">
    3. {
    4. "routes": [
    5. {
    6. "pattern": "*",
    7. "meta": {
    8. "header": {
    9. "show": true,
    10. "title": "MIP Index",
    11. "logo": "https://boscdn.baidu.com/assets/mip/codelab/shell/mashroom.jpg"
    12. },
    13. }
    14. }
    15. ],
    16. "exampleUserId": 1
    17. }
    18. </script>
    19. </mip-shell>

    可以看到这个例子中在 routes 平级增加了一个 exampleUserId,将会在后续继承父类方法中使用到。个性化 Shell 就可以通过传入自定义数据来处理额外的逻辑。

    构造函数中有两个属性可以被子类修改,他们分别是:

    • alwaysReadConfigOnLoad, 默认值 true
      因为每个页面都有全部的配置,因此在页面切换时,目标页面的配置同时存在于当前页面和目标页面两处(正常情况下两处配置应该相同)。这个属性可以控制以哪一份配置为准。

      如果确认每个页面的配置是严格相同的,或者为了性能考虑,则应该使用 false,从而保证只有第一次读入配置,后续均不读取。反之如果需要每次均读取覆盖,则应该使用 true

    开发者可以在构造函数中修改这两个属性,也可以初始化自己之后将要使用的其他属性和变量。注意在初始化时,必须要调用 super 并且带上参数,如下:

    1. constructor (...args) {
    2. super(...args)
    3. this.alwaysReadConfigOnLoad = false
    4. this.transitionContainsHeader = false
    5. }

    showHeaderCloseButton

    • 参数: 无。
    • 返回值boolean,默认 true

    MIP Shell 的头部标题栏右侧的按钮区域会根据 MIP 页面当前所处的状态来决定是否展示关闭按钮。当处于百度搜索结果页中(即拥有 SuperFrame 环境时)会额外渲染一个关闭按钮,点击效果用以通知 SuperFrame 关闭自身,如下图所示:

    MIP 页面判断当前是否处于 SuperFrame 环境的判断依据是 window.MIP.standalone 值等于 false

    如果开发者有特殊需求,要求即便在 window.MIP.standalone === false 成立时依然 不展现 关闭按钮,可以继承这个方法并返回 false。这个方法在 standalone 的判断 之后 生效,因此即便它返回 true,只要 standalone 也为 true 则关闭按钮依然不展现。

    1. showHeaderCloseButton () {
    2. if (location.href.indexOf('main') !== -1) {
    3. return true;
    4. } else {
    5. return false;
    6. }
    7. }

    handleShellCustomButton

    • 参数buttonName, string, 按钮配置时的 name 属性。
    • 返回值:无。

    MIP Shell 的头部标题栏上所有的按钮(如默认的后退,关闭,更多以及用户配置的 buttonGroup)在点击时都会调用这个方法。

    虽然默认的后退(back), 关闭(close)和更多(more)按钮已有其对应的处理方法(如点击更多展现浮层,点击后退路由后退等),但开发者依然可以在这里接到这些值,以添加可能存在的额外操作。

    buttonGroup 配置时,每一个按钮均有一个 name 属性,这个 name 属性也会当做参数传入这个方法。

    1. handleShellCustomButton (buttonName) {
    2. if (buttonName === 'back') {
    3. // 默认头部已经包含 name 为 back 的按钮,并已有默认处理(路由后退)
    4. // 如果需要,这里可以再进行一些额外的处理
    5. console.log('click on back')
    6. } else if (buttonName === 'about') {
    7. // 假设 HTML 中配置了 name 为 about 的按钮,这里定义它的响应
    8. console.log('click on about')
    9. // 实际上跳转页面可以通过在 buttonGroup 中的 link 属性进行配置。这里仅仅是做一个示例
    10. window.MIP.viewer.open('./about.html')
    11. }
    12. }

    特别地,在 MIP Shell 基类逻辑中还定义了一个名为 cancel 的按钮的点击响应,作用是关闭更多按钮的浮层。因此如果开发者在 buttonGroup 中配置了名为 cancel 的按钮,可以不必自行实现关闭浮层的响应即可获得相同的效果。

    • 参数shellConfig, Object, 经过处理的 Shell 配置对象。
    • 返回值:无。

    Shell 子类通过这个方法对 MIP Shell 初步处理后的配置对象进行修改,再进行后续的渲染和绑定,从而可以对 HTML 中的配置进行统一的操作。

    MIP Shell 进行的所谓“初步处理”包括:

    1. 读取 HTML 中对应标签内的 JSON,并通过 JSON.parse() 进行转义。
    2. 遍历 routes 数组的每个元素,进行如下操作:
      1. 获取 meta 值,和默认 meta 进行合并并写会。HTML 中的 meta 优先级更高。
      2. 获取 pattern 值,将字符串转化为正则表达式(采用 new RegExp() 进行转化)。特别的,'*' 被转化为 /.*/ 以匹配任意字符
      3. 如果无法获取到 route.meta.header.title 的值,则从当前页面的 <title> 标签获取。

    在这些操作之后,MIP Shell 将 整个 JSON 对象 当做参数传递给开发者(不单单是 routes 数组)。开发者可以在 processShellConfig 方法内对参数进行修改,不必返回。这里还分为同步和异步两种情况。

    • 同步修改

      即方法内容不涉及异步操作,直接对参数进行修改即可。示例如下:

    • 异步修改

      1. processShellConfig (shellConfig) {
      2. // 设置默认值
      3. shellConfig.routes.forEach(routeConfig => {
      4. routeConfig.meta.header.title = '极速服务'
      5. routeConfig.meta.header.logo = 'https://www.baidu.com/favicon.ico'
      6. })
      7. // 获取 HTML 配置好的 exampleUserId
      8. let isId = shellConfig.exampleUserId
      9. // 使用 setTimeout 模拟异步发送请求
      10. setTimeout(() => {
      11. // 通过 exampleUserId 获取到目标用户的标题和 LOGO,并固定按钮
      12. shellConfig.routes[0].meta.header.title = '蓝犀牛搬家'
      13. shellConfig.routes[0].meta.header.logo = 'https://boscdn.baidu.com/assets/mip2/lanxiniu/logo.png'
      14. shellConfig.routes[0].meta.header.buttonGroup = [
      15. {
      16. name: 'share',
      17. text: '分享'
      18. },
      19. {
      20. name: 'indexPage',
      21. text: '首页'
      22. },
      23. {
      24. name: 'about',
      25. text: '关于蓝犀牛'
      26. },
      27. {
      28. name: 'cancel',
      29. text: '取消'
      30. }
      31. ]
      32. shellConfig.routes[1].meta.header.title = '红犀牛搬家'
      33. // 异步操作,需要更新 Shell 配置缓存
      34. this.updateShellConfig(shellConfig)
      35. // 异步操作,需要更新页面上的 Shell DOM
      36. // window.MIP.viewer.page.pageId 表示当前页面的 pageId,由 MIP Shell 负责更新
      37. this.refreshShell({pageId: window.MIP.viewer.page.pageId})
      38. }, 1000)
      39. }

    renderOtherParts

    • 参数:无。
    • 返回值:无。

    默认的 MIP Shell 只渲染头部标题栏。如果开发者希望渲染其他部分(如底部菜单栏),可以通过继承 renderOtherParts 方法来实现。

    这个方法没有参数,但可以通过获取 this.currentPageMeta 来获取当前页面的 meta 信息(即 MIP Shell 所有配置中匹配当前页面的 meta,其中包括当前页面的标题,LOGO,按钮等所有信息)。

    需要注意的是,如果需要创建 position: fixed 的 DOM 元素(如底部菜单栏),应当使用 <mip-fixed> 作为标签名,而非其他如 <div> 等 HTML 标准标签。这主要是为了解决 iOS 的 iframe 中 fixed 元素滚动抖动的 BUG。

    1. renderOtherParts () {
    2. this.$footerWrapper = document.createElement('mip-fixed')
    3. this.$footerWrapper.setAttribute('type', 'bottom')
    4. this.$footerWrapper.classList.add('mip-shell-footer-wrapper')
    5. this.$footer = document.createElement('div')
    6. this.$footer.classList.add('mip-shell-footer', 'mip-border', 'mip-border-top')
    7. this.$footer.innerHTML = this.renderFooter()
    8. this.$footerWrapper.appendChild(this.$footer)
    9. document.body.appendChild(this.$footerWrapper)
    10. }
    11. renderFooter() {
    12. let pageMeta = this.currentPageMeta
    13. return 'hello ${pageMeta.header.title}!'
    14. }

    建议把 this.renderFooter() 抽象成一个单独的方法,因为这个方法也会在后面 update 时被调用。

    updateOtherParts

    • 参数:无。
    • 返回值:无。

    MIP 页面首次进入时会调用 renderOtherParts() 方法进行初始渲染。而后续切换页面时,MIP Page 会将目标页面的 meta 信息设置为 this.currentPageMeta 并调用 方法以更新自定义部件。

    updateOtherParts() 方法中,开发者仅需要更新 HTML 即可,不需要像 renderOtherParts() 那样创建 DOM 并插入到页面中。也因此,将 renderFooter() 独立出来有利于这里继续调用。示例如下:

    1. updateOtherParts() {
    2. this.$footer.innerHTML = this.renderFooter()
    3. }
    • 参数options, Object, 路由切换时的配置项。
      • targetPageId, string, 目标页面的 pageId
      • targetPageMeta, Object, 目标页面的 pageMeta,结构和 <mip-shell> 中的 meta 对象相同
      • sourcePageId, string, 当前页面的 pageId
      • sourcePageMeta, Object, 当前页面的 pageMeta,结构和 <mip-shell> 中的 meta 对象相同
      • newPage, boolean, 是否需要创建 iframe
      • isForward, boolean, 动画是否为前进方向
    • 返回值:无。

    MIP 在页面切换之前,会调用此方法。如果子类需要在动画之前进行一些操作(例如加入自己的动画元素),可以继承并实现这个方法。示例如下:

    1. // 固定动画切换方向为前进方向
    2. options.isForward = true
    3. }

    afterSwitchPage

    • 参数options, Object, 路由切换时的配置项。
      • targetPageId, string, 目标页面的 pageId
      • targetPageMeta, Object, 目标页面的 pageMeta,结构和 <mip-shell> 中的 meta 对象相同
      • sourcePageId, string, 当前页面的 pageId
      • sourcePageMeta, Object, 当前页面的 pageMeta,结构和 <mip-shell> 中的 meta 对象相同
      • newPage, boolean, 是否需要创建 iframe
      • isForward, boolean, 动画是否为前进方向
    • 返回值:无。

    MIP 在页面切换之后,会调用此方法。如果子类需要在动画之后进行一些操作(例如要通知一些消息),可以继承并实现这个方法。示例如下:

    这里列出两个个性化 Shell 的实例(均为实际线上代码,但隐去了敏感信息和复杂的业务逻辑)

    极速服务 Shell

    命名为 <mip-shell-is>,主要工作有:

    1. 增加额外的 isId 配置项
    2. 根据 isId 通过接口 异步 获取站点的标题,LOGO 和按钮配置
    3. 为添加的按钮增加点击响应
    4. 因为涉及异步获取站点 meta 信息,因此首屏请求之后不再重复获取信息
    • mip-shell-is.js

      1. export default class MIPShellIS extends window.MIP.builtinComponents.MIPShell {
      2. constructor (...args) {
      3. super(...args)
      4. this.alwaysReadConfigOnLoad = false
      5. this.transitionContainsHeader = false
      6. }
      7. processShellConfig (shellConfig) {
      8. // 设置默认属性
      9. shellConfig.routes.forEach(routeConfig => {
      10. routeConfig.meta.header.title = '极速服务'
      11. routeConfig.meta.header.logo = 'https://www.baidu.com/favicon.ico'
      12. routeConfig.meta.header.bouncy = false
      13. })
      14. let isId = shellConfig.isId
      15. console.log('Simulate async request with isId:', isId)
      16. setTimeout(() => {
      17. shellConfig.routes[0].meta.header.title = '蓝犀牛搬家'
      18. shellConfig.routes[0].meta.header.logo = 'https://boscdn.baidu.com/assets/mip2/lanxiniu/logo.png'
      19. shellConfig.routes[0].meta.header.buttonGroup = [
      20. {
      21. name: 'share',
      22. text: '分享'
      23. },
      24. {
      25. name: 'indexPage',
      26. text: '首页'
      27. },
      28. {
      29. name: 'about',
      30. text: '关于蓝犀牛'
      31. },
      32. {
      33. name: 'cancel',
      34. text: '取消'
      35. }
      36. ]
      37. shellConfig.routes[1].meta.header.title = '红犀牛搬家'
      38. this.updateShellConfig(shellConfig)
      39. this.refreshShell({pageId: window.MIP.viewer.page.pageId})
      40. }, 1000)
      41. }
      42. handleShellCustomButton (buttonName) {
      43. if (buttonName === 'share') {
      44. console.log('click on share')
      45. this.toggleDropdown(false)
      46. } else if (buttonName === 'indexPage') {
      47. console.log('click on indexPage')
      48. this.toggleDropdown(false)
      49. } else if (buttonName === 'about') {
      50. console.log('click on about')
      51. this.toggleDropdown(false)
      52. }
      53. }
      54. }
    • mip-shell-is.html

      只列出 <body> 部分。

      1. <body>
      2. <mip-shell-is mip-shell>
      3. <script type="application/json">
      4. {
      5. "routes": [
      6. {
      7. "pattern": "*",
      8. "meta": {
      9. "header": {
      10. "show": true
      11. }
      12. }
      13. }
      14. ],
      15. "isId": 123
      16. }
      17. </script>
      18. </mip-shell-is>
      19. <p>This is MIP SHELL IS</p>
      20. <a class="link" href="./mip-shell-is-2.html" mip-link>Go to MIP SHELL IS 2</a>
      21. <div id="button">By viewer.open</div>
      22. <script src="../../dist/mip.js"></script>
      23. <script src="./components/mip-shell-is.js"></script>
      24. </body>

    命名为 <mip-shell-novel>,主要工作有:

    1. 增加额外的 catalog 配置项用以记录小说目录
    2. 额外渲染底部菜单栏
    3. 为底部菜单栏绑定点击事件,并提供解绑函数
    4. 每个页面都包含目录信息,为性能考虑,只读取第一个页面的信息。
    • mip-shell-novel.js

      1. export default class MIPShellNovel extends window.MIP.builtinComponents.MIPShell {
      2. constructor (...args) {
      3. super(...args)
      4. this.alwaysReadConfigOnLoad = false
      5. this.transitionContainsHeader = false
      6. }
      7. processShellConfig (shellConfig) {
      8. this.catalog = shellConfig.catalog
      9. }
      10. renderOtherParts () {
      11. this.$footerWrapper = document.createElement('mip-fixed')
      12. this.$footerWrapper.setAttribute('type', 'bottom')
      13. this.$footerWrapper.classList.add('mip-shell-footer-wrapper')
      14. this.$footer = document.createElement('div')
      15. this.$footer.classList.add('mip-shell-footer', 'mip-border', 'mip-border-top')
      16. this.$footer.innerHTML = this.renderFooter()
      17. this.$footerWrapper.appendChild(this.$footer)
      18. document.body.appendChild(this.$footerWrapper)
      19. }
      20. updateOtherParts () {
      21. this.$footer.innerHTML = this.renderFooter()
      22. }
      23. let pageMeta = this.currentPageMeta
      24. let {buttonGroup} = pageMeta.footer
      25. let renderFooterButtonGroup = buttonGroup => buttonGroup.map(buttonConfig => `
      26. <div class="button" mip-footer-btn data-button-name="${buttonConfig.name}">${buttonConfig.text}</div>
      27. `).join('')
      28. let footerHTML = `
      29. <div class="upper mip-border mip-border-bottom">
      30. <div class="switch switch-left" mip-footer-btn data-button-name="previous">&lt;上一章</div>
      31. <div class="switch switch-right" mip-footer-btn data-button-name="next">下一章&gt;</div>
      32. </div>
      33. ${renderFooterButtonGroup(buttonGroup)}
      34. </div>
      35. `
      36. return footerHTML
      37. }
      38. bindHeaderEvents () {
      39. super.bindHeaderEvents()
      40. let me = this
      41. let event = window.MIP.util.event
      42. // 代理底部菜单栏的点击事件
      43. this.footEventHandler = event.delegate(this.$footerWrapper, '[mip-footer-btn]', 'click', function (e) {
      44. let buttonName = this.dataset.buttonName
      45. me.handleFooterButton(buttonName)
      46. })
      47. if (this.$buttonMask) {
      48. this.$buttonMask.onclick = () => {
      49. this.toggleDropdown(false)
      50. this.toggleDOM(this.$footerWrapper, false, {transitionName: 'slide'})
      51. }
      52. }
      53. }
      54. unbindHeaderEvents () {
      55. super.unbindHeaderEvents()
      56. if (this.footEventHandler) {
      57. this.footEventHandler()
      58. this.footEventHandler = undefined
      59. }
      60. }
      61. handleShellCustomButton (buttonName) {
      62. if (buttonName === 'share') {
      63. console.log('share')
      64. this.toggleDropdown(false)
      65. } else if (buttonName === 'setting') {
      66. this.toggleDOM(this.$buttonWrapper, false, {transitionName: 'slide'})
      67. this.toggleDOM(this.$footerWrapper, true, {transitionName: 'slide'})
      68. }
      69. }
      70. handleFooterButton (buttonName) {
      71. console.log('click on footer:', buttonName)
      72. this.toggleDOM(this.$buttonMask, false)
      73. this.toggleDOM(this.$footerWrapper, false, {transitionName: 'slide'})
      74. }
      75. }
    • 只列出 <body> 部分。

      1. <body>
      2. <h2>第1章 灵魂重生</h2>
      3. <p>“贱人,你竟敢背叛我!”</p>
      4. <p>“宋凌云,你这个畜生,我视你如手足,当你如兄弟,是我亲手把你培育成无双战神,可你竟然与那贱人勾搭成奸,还要置我于死路,我做鬼都不会放过你。” </p>
      5. <p>陆宇猛然睁开眼睛,一下子坐起,双眼之中充满了愤怒与杀气,拳头握得死紧! </p>
      6. <p>“不对,这是哪里?我明明在黑狱中灰飞烟灭,怎么可能还未死?” </p>
      7. <p>“难道说,我重生了?” </p>
      8. <p>陌生的环境让陆宇迅速清醒,过往的记忆逐一呈现在脑海里。 </p>
      9. <p>陆宇原本是神武天域的圣魂天师,开创了史无前例的武魂进化之术,将一个不起眼的辅助职业魂天师推到了巅峰极境,成为了神武天域有史以来第一个圣帝级魂天师,简称圣魂天师! </p>
      10. <p>那是至高荣誉,堪称魂天师领域的万古第一人。 </p>
      11. <p>然而就在陆宇最风光,最得意,站在人生巅峰之际,一场背叛彻底将他摧毁。 </p>
      12. <p>陆宇这一生有三大引以为傲的事情,貌美无双的娇妻,神勇无敌的兄弟,功成名就的事业,那是无数人都梦寐以求的东西,他都得到了,可他却没有猜到结局。 </p>
      13. <p>陆宇的成长并不顺利,但是开创武魂进化之术改变了他的一生,让他娶到了神武天域十大美女之一的马灵月为妻,曾羡煞无数人。 </p>
      14. <p>后来,陆宇又结识了宋凌云,两人肝胆相照,成为了好兄弟。 </p>
      15. <p>身为魂天师,陆宇致力于研究武魂进化之术,并在娇妻与兄弟身上耗费了半生精力。 </p>
      16. <p>原本,马灵月和宋凌云的武魂都只是地级三品以下,注定成就有限。 </p>
      17. <p>但是陆宇却利用自己独创的武魂进化之术,让两人的武魂等级从地级三品提升到了天级八品,一跃成为了神武天域的至强者。 </p>
      18. <mip-shell-novel mip-shell>
      19. <script type="application/json">
      20. {
      21. "routes": [
      22. {
      23. "pattern": "/novel-\\d",
      24. "meta": {
      25. "header": {
      26. "show": true,
      27. "title": "神武天帝",
      28. "buttonGroup": [{
      29. "name": "share",
      30. "text": "分享"
      31. },{
      32. "name": "setting",
      33. "text": "设置"
      34. },{
      35. "name": "cancel",
      36. "text": "取消"
      37. }]
      38. },
      39. "footer": {
      40. "buttonGroup": [{
      41. "name": "catalog",
      42. "text": "目录"
      43. },{
      44. "name": "night",
      45. "text": "夜间模式"
      46. },{
      47. "name": "setting",
      48. "text": "设置"
      49. }]
      50. }
      51. }
      52. }
      53. ],
      54. "catalog": [
      55. {
      56. "name": "第1章 灵魂重生",
      57. "link":"novel-1.html"
      58. },
      59. {
      60. "name": "第2章 武魂提升",
      61. "link":"novel-2.html"
      62. },
      63. {
      64. "name": "第3章 牛刀小试",
      65. "link":"novel-3.html"
      66. },
      67. {
      68. "name": "第4章 笑里藏刀",
      69. "link":"novel-4.html"
      70. },
      71. {
      72. "name": "第5章 云月儿",
      73. "link":"novel-5.html"
      74. }
      75. ]
      76. }
      77. </script>
      78. </mip-shell-novel>
      79. <script src="../../dist/mip.js"></script>
      80. <script src="./components/mip-shell-novel.js"></script>