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