demand

The company decided to use a back-end system that had been maintained for more than a decade (originally back-end rendering) using a back-end separation approach. Due to the long duration of the process, the new and old systems need to coexist for a period of time. This coexistence is intended to be transparent to users, so we made the interface structure of the new system very similar to the old system, and we made the menus of the two systems exactly the same.

To achieve complete consistency, you first need to pull out the old system menu and change it into a configurable form. Second, because the new system is a front-end rendering form, so the new system menu needs to be obtained through THE API. Next, I’ll get into the specifics of the implementation, using the Rails 5 and vue.js 2 frameworks as examples, but the other frameworks are pretty much the same.

Request Routing Definition

The first is the route definition. Route definition Each system defines its own functions separately, assuming that there are now three functions in total, one of which has been refactored into the new system.

The old system (Rails) still has two routes left:

# config/routes.rb

Rails.application.routes.draw do
  get "function-A1"= >"function_a1#index"
  get "function-A2"= >"function_a2#index"
end
Copy the code

2, new system (vue.js) add a route:

// src/configs/routes.js

const routes = [  
  {
    path: '/function-B1'.component: () = > import('@/pages/function_b1'),},]Copy the code

Change the menu to configurable

Then there is the issue of menu configuration. The configuration format is YAML format, which uses the simplest tree structure with minimal configuration content.

1. Configuration menu:

# config/menus.yml

- name: The menu A
  icon_old: icon-a
  icon_new: mdi-a
  children:
    - name: Menu A1
      link: /function-A1
    - name: Menu A2
      link: /function-A2
- name: Menu B
  icon_old: icon-b
  icon_new: mdi-b
  children:
    - name: Menu B1
    - link: /frontend/function-B1
#...
Copy the code

It is assumed that the connections starting with /frontend will be serviced using the new system (vue.js) (proxy and Nginx are required here, not expanded in this article) and the other cases will be serviced using the old system (Rails).

2. Load menu configuration file:

# config/initializers/config_menus.rb

file_dir = Rails.root.join('config'.'menus.yml')
Rails.configuration.menus = YAML.load_file(file_dir)
After # can get: in the program so that Rails. The configuration. The menus
Copy the code

The Rails Routes Helper method can be loaded as follows:

# config/initializers/config_menus.rb

Rails.configuration.after_initialize do |application|
  # The development environment at this time the routing data is still empty, need to be actively reload
  # https://stackoverflow.com/a/8707687
  application.reload_routes!

  file = File.read(Rails.root.join('config'.'menus.yml'))
  The <%= xxx_path %> method can be used in yamL configuration
  bind = application.routes.url_helpers.instance_eval { binding }
  application.config.menus = YAML.load(ERB.new(file).result(bind))
end
Copy the code

Select the parent node from the child node. Add a tree_Nodes attribute to the menu automatically through the program, which makes the subsequent use more convenient:

# config/initializers/config_menus.rb

file_dir = Rails.root.join('config'.'menus.yml')
Rails.configuration.menus = YAML.load_file(file_dir)
A tree_nodes for # menu: [1], the menu A1: [1, 1], the menu A2: [1, 2], the menu B: [2]
add_tree_nodes = lambda { |menus, parent_tree_nodes = []|
  menus.each_with_index do |menu, index|
    tree_nodes = parent_tree_nodes + [index + 1] menu.merge! ("tree_nodes" => tree_nodes)
    add_tree_nodes.call(menu['children'], tree_nodes) if menu['children'].present?
  end
}
add_tree_nodes.call(Rails.configuration.menus)
Copy the code

Old system menu rendering

The old system was a back-end rendering, where you could just take the configuration and render it. Render is rendered recursively. Here are some core methods.

1. Render menu in layout:

# app/views/layouts/application.html.erb

# menu bar
<%= render_menus(Rails.configuration.menus) %>
Copy the code

2. Core method of menu rendering (UI framework is Bootstrap 2) :

# app/helpers/menus_helper.rb

module MenuHelper
  def render_menus(menus, depth = 1)
    doms = menus.map do |menu|
      if menu['children']
        <<~HTML # here we assume that the open style makes the parent menu open and collapse <li class="# {'open' ifmenu_active? (menu, depth)}">
            <a href="#" class="dropdown-toggle">
              <i class="#{menu['icon_old'] || 'icon-double-angle-right'}"></i>
              <span class="menu-text">#{menu['name']}</span>
              <b class="arrow icon-angle-down"></b>
            </a>
            <ul class="submenu">
              #{render_menus(menu['children'], depth + 1)}
            </ul>
          </li>
        HTML
      else
        <<~HTML # Here we assume that the active style makes the submenu highlight <li class="# {'active' ifmenu_active? (menu, depth)}">
            <a href="#{menu['link']}">
              <i class="#{menu['icon_old'] || 'icon-double-angle-right'}"></i>
              <span class="menu-text">#{menu['name']}</span>
            </a>
          </li>
        HTML
      end
    end
    doms.join.html_safe
  end

  private

  The tree_nodes property from the menu configuration step 2 above is used here. If not, there are other ways to solve this problem
  def menu_active?(menu, depth)
    current_active_menu&.dig('tree_nodes')&.first(depth) == menu['tree_nodes']
  end

  # deep traversal, get the first hit menu (hit algorithm here simply use the path of the current URL, the actual situation may be more complicated, not discussed here)
  def current_active_menu(menus = nil)
    @current_active_menu ||= begin
      menus ||= Rails.configuration.menus
      menus.each do |menu|
        if menu['children']
          active_menu = current_active_menu(menu['children'])
          return active_menu if active_menu
        elsif request.path == menu['link']
          return menu
        end
      end
      nil
    end
  end
end
Copy the code

New system menu rendering

The new system is the front-end rendering form, except to obtain the menu data through THE API form, other logic is similar to the above, most of it is Ruby code to JS code.

1. Define the API for retrieving the backend menu first:

# app/controllers/api/menus_controller.rb

module Api
  class MenusController < ApplicationController
    def index
      render json: { menus: Rails.configuration.menus }
    end
  end
end
Copy the code

2. Then the render menu in the front layout:

<! -- src/layouts/index.vue --> <template> <Menus :menus="menus" :currentActiveMenu="currentActiveMenu" /> </template> <script> import Menus from '@/components/Menus' export default { components: { Menus, }, data: function () { return { menus: [], currentActiveMenu: }}, mounted () {acis.get ('/ menus /menus.json'). Then (acis.menus => {this.menus =.data. Subsequent highlight this menu to use this. SetCurrentActiveMenu (enclosing menus)})}, method: { setCurrentActiveMenu (menus) { menus.forEach(menu => { if (menu.children) { const activeMenu = this.setCurrentActiveMenu(menu.children) if (activeMenu) { this.currentActiveMenu = activeMenu } } else if (location.pathname === menu.link) { this.currentActiveMenu = menu } }) }, }, </script>Copy the code

Finally, complete the core menu components (UI framework Vuetify 2) :

<! -- src/components/Menus.vue --> <template> <v-list> <template v-for="menu in menus"> <v-list-group v-if="menu.children" :key="menu.name" :sub-group="isSubmenu" :value="isMenuActive(menu)" > <template v-slot:activator> <v-list-item-icon v-if="! isSubmenu"> <v-icon v-text="menu.icon_new || 'mdi-notification-clear-all'" /> </v-list-item-icon> <v-list-item-content> <v-list-item-title v-text="menu.name" /> </v-list-item-content> </template> <Menus :menus="menu.children" :currentActiveMenu="currentActiveMenu" :depth="depth + 1" /> </v-list-group> <v-list-item v-else :key="menu.name" :href="menu.link" :class="{ 'v-list-item--active': isMenuActive(menu) }" > <v-list-item-icon v-if="! isSubmenu"> <v-icon v-text="menu.icon_new || 'mdi-notification-clear-all'" /> </v-list-item-icon> <v-list-item-content> <v-list-item-title v-text="menu.name" /> </v-list-item-content> </v-list-item> </template> </v-list> </template> <script> export default {name: 'Menus', // Use the recursive component to configure the name of the component: {Menus: {type: Array, required: true }, currentActiveMenu: { type: Object }, depth: { type: Number, default: 1 }, }, methods: { isMenuActive(menu) { if (this.currentActiveMenu) { return this.currentActiveMenu.tree_nodes.slice(0, this.depth).join(',') === menu.tree_nodes.join(',') } else { return false } }, }, computed: { isSubmenu () { return this.depth ! == 1 }, }, } </script>Copy the code

other

The above is the minimum core solution, which should be used in the actual project, but also to deal with whether the menu is visible, whether the menu opens a new window, whether the menu has permissions and a series of problems, most of which can be solved by adding configuration items in the configuration file.

In fact, it took the most time to configure the system menu into Menus.yml, which took more than 1,000 rows manually