uni-app 入门之路由自动构建

1,392 阅读8分钟

uni-app 所有页面路径用一个 pages.json 进行配置, 不利于维护和模块化管理, 我们是否可以通过模块划分然后自动构出配置文件呢, 这样维护起来就方便许多了

node.js

需要实现自动构建首先我们需要了解 node.js

Node是JavaScript语言的服务器运行环境。

所谓“运行环境”有两层意思:首先,JavaScript语言通过Node在服务器运行,在这个意义上,Node有点像JavaScript虚拟机;其次,Node提供大量工具库,使得JavaScript语言与操作系统互动(比如读写文件、新建子进程),在这个意义上,Node又是JavaScript的工具库。

Node内部采用Google公司的V8引擎,作为JavaScript语言解释器;通过自行开发的libuv库,调用操作系统资源。

fs 模块

fs是filesystem的缩写,该模块提供本地文件的读写能力,基本上是POSIX文件操作命令的简单包装。但是,这个模块几乎对所有操作提供异步和同步两种操作方式,供开发者选择。

readdirSync 方法

readdirSync 方法用于同步读取目录,返回一个所包含的文件和子目录的数组

readdirSync 方法接收一个参数是文件路径

例如:

const fs = require('fs')
var text = fs.readdirSync('./modules')
console.log(text)
  • 如果该文件夹不存在则运行报错
  • 如果该文件夹是空, 则返回一个空数组
  • 如下文件结构返回 [ 'home.js', 'user' ]
├─modules
  home.js
  └─user

writeFile 方法

writeFile方法用于写入文件

writeFile 方法接收三个参数,第一个是文件路径,第二个是写入内容,第三个是回调函数。

例如:在 home.js 中写入 var str = "Hello Node"

var fs = require('fs');

fs.writeFile('./modules/home.js', 'var str = "Hello Node"', function (err) {
  if (err) throw err;
  console.log('文件写入成功');
});

读取内容

要想实现模块化路由自动化构建, 我们需要依次读取 modules 文件夹中的路由文件名和路径, 并导出内容,然后将所有内容拼接成 pages.json

读取所有文件路径

我们规定 modules 文件夹下只能是文件, 如果需要多层模块嵌套, 需要构建递归

const getRouter = () => {
  const result = fs.readdirSync('./modules')
  let router = []
  result.forEach(r => {
    const route = require('./modules/' + r)
    router.push(route)
  })
  return router
}

我们删除 modules 文件夹下 user 文件夹, 并在 home.js 中写入

module.exports = {
  baseUrl: 'pages/home/',
  children: [
    {
      path: 'index',
      text: '主页',
      navigationBarBackgroundColor: '#5565DB',
      "app-plus": {
        titleNView: {
          backButton: {
            title: '返回'
          }
        }
      }
    }
  ]
}

然后我们执行 getRouter 方法可以得到 [ { baseUrl: 'pages/home', children: [ [Object] ] } ]

JSON.stringify

由于对象不能直接打印出来,导致数据丢失,此时我们需要使用 JSON.stringify 方法对 require 引入的内容进行转换

修改方法为:

const getRouter = () => {
  const result = fs.readdirSync('./modules')
  let router = []
  result.forEach(r => {
    const route = require('./modules/' + r)
    router.push(JSON.stringify(route))
  })
  return router
}

再次执行后结果为:

[
  '{"baseUrl":"pages/home/","children":[{"path":"index","text":"主页","navigationBarBackgroundColor":"#5565DB","app-plus":{"titleNView":{"backButton":{"title":"返回"}}}}]}'
]

现在我们将所有内容都成功获取

解析内容

我们所写的配置是和官方需要的配置有很大的出入, 那么我们就需要进行相应的解析, 得到符合 uni-app 标准的配置, 查阅文档

官方规定规定的配置形式如下:

{
  "path": "pages/home/index",
  "style": {
    "navigationBarTitleText": "主页",
    "navigationBarBackgroundColor": "#5565DB",
    "app-plus": {
      "titleNView": {
        "backButton": {
          "title": "返回"
        }
      }
    }
  }
}

配置中经常使用的是 pathnavigationBarTitleText, 就是我们页面的路径与标题, 对应我们上述配置中的 pathtext, 而其他配置我们并列放置, 因此我们可以编写一个解析方法如下:

const buildRouter = route => {
  const { baseUrl, children } = route
  return children.map(item => {
    const { path, text, ...otherConfig } = item
    return {
      path: baseUrl + path,
      style: {
        navigationBarTitleText: text,
        ...otherConfig
      }
    }
  })
}

然后我们也需要改造 getRouter 方法, 因为 buildRouter 方法得到是一个数组, 而 pages.json 所有页面配置都是平级结构, 所以我们要把所有模块下的路由数组拼接在一起, 修改如下:

const getRouter = () => {
  const result = fs.readdirSync('./modules')
  let router = []
  result.forEach(r => {
    const route = require('./modules/' + r)
    const routeList = buildRouter(route)
    router = router.concat(routeList)
  })
  return JSON.stringify(router)
}

打印结果为:

[{"path":"pages/home/index","style":{"navigationBarTitleText":"主页","navigationBarBackgroundColor":"#5565DB","app-plus":{"titleNView":{"backButton":{"title":"返回"}}}}}]

格式化后和 uni-app 所需配置一致, 至此我们已经完成页面的模块化解析

公共配置

pages.json 除了页面配置外还需要公共配置, 如下:

{
    "easycom": {
        "^u-(.*)": "@/uview-ui/components/u-$1/u-$1.vue"
    },
    "globalStyle": {
        "navigationBarTextStyle": "black",
        "navigationBarTitleText": "uni-app",
        "navigationBarBackgroundColor": "#F8F8F8",
        "backgroundColor": "#F8F8F8"
    }
}

这个就较为简单了, 只需要单独建立一个文件配置引入就好了,在 modules 同级目录下创建一个 config.js 文件, 内容如下:

module.exports = {
  easycom: {
    "^u-(.*)": "@/uview-ui/components/u-$1/u-$1.vue"
  },
  globalStyle: {
    "navigationBarTextStyle": "black",
    "navigationBarTitleText": "uni-app",
    "navigationBarBackgroundColor": "#F8F8F8",
    "backgroundColor": "#F8F8F8"
  },
}

最后将公共配置信息与页面路径配置信息合并:

const config = require('./config')
const pages = {
  ...config,
  pages: getRouter()
}

pages 就是我们最后所需要的配置了

写入内容

最后我们要将得到的配置写入 pages.json 中

const writeRouter = () => {
  const pages = {
    ...config,
    pages: getRouter()
  }
  fs.writeFile(
    __dirname + '/../pages.json',
    JSON.stringify(pages, null, '  '),
    e => e ? console.error(e) : console.log('pages.json 配置文件更新成功')
  )
}

好了, 大功告成!!!

注意: 每次修改完路由配置后都需要手动执行 build.js 文件, 什么? 你问怎么执行, 请自行百度如何执行 js 文件

功能拓展

上述内容简单的实现了模块化路由自动构建, 再此基础上我们可以扩展出一些符合业务需求的功能

统一返回按钮

对于返回按钮, 一个APP应该统一, 我们业务统一把所有返回按钮都设定为:“< 返回”

那么根据文档我们需要每个路径 style 中都写上

"app-plus": {
  "titleNView": {
    "backButton": {
      "title": "返回"
    }
  }
}

程序员的天职就是偷懒, 能全局配置的问题为什么要手动每个页面进行配置呢?

如果该页面没有配置按钮 title, 那么我们自动配置, 如果有配置我们取该页面自己的配置, 编写 setBackChar 方法如下:

const setBackChar = (otherConfig = {}, title = '返回') => {
  if (otherConfig.hasOwnProperty('app-plus')) {
    const appPlus = otherConfig['app-plus']
    if (appPlus.hasOwnProperty('titleNView')) {
      const titleNView = appPlus.titleNView
      if (titleNView.hasOwnProperty('backButton')) {
        const backButton = titleNView.backButton
        if (backButton.hasOwnProperty('title')) {
          // 此时页面设置了返回按钮或者为空以设置的为主
        } else {
          backButton.title = title
        }
      } else {
        titleNView.backButton = { title }
      }
    } else {
      appPlus.titleNView = {
        backButton: { title }
      }
    }
  } else {
    otherConfig['app-plus'] = {
      titleNView: {
        backButton: { title }
      }
    }
  }
}

在 buildRouter 方法执行 map 循环时执行 setBackChar 方法

简写配置参数名

官方配置中参数名真是又臭又长, 我们可以根据自己的需求简化参数名, 具体实现可以参照 navigationBarTitleText 简化为: text

封装公共配置

整个APP应该有一个统一样式风格, 如果涉及需要修改导航栏字体样式、背景或者右侧导航菜单,此时可以封装成公共方法进行生成, 例:

// utils.js
const navRightButton = (text, type = 'none') => {
  return {
    text: text,
    type: type,
    fontSize: "16px",
    fontWeight: "bold",
    width: "40px"
  }
}

对应路由配置

const { navRightButton } = require('../utils')
module.exports = {
  baseUrl: 'pages/form/',
  children: [
    {
      path: 'index',
      text: '表单提交',
      "app-plus": {
        titleNView: {
          buttons: [
            navRightButton('保存')
          ]
        }
      }
    },
  ]
}

路由跳转优化

官方提供了六个方法供我们使用, 方法如下:

  • navigateTo 保留当前页面,跳转到应用内的某个页面,使用uni.navigateBack可以返回到原页面。
  • redirectTo 关闭当前页面,跳转到应用内的某个页面。
  • reLaunch 关闭所有页面,打开到应用内的某个页面。
  • switchTab 跳转到 tabBar 页面,并关闭其他所有非 tabBar 页面。
  • navigateBack 关闭当前页面,返回上一页面或多级页面。可通过 getCurrentPages() 获取当前的页面栈,决定需要返回几层。
  • preloadPage 预加载页面,是一种性能优化技术。被预载的页面,在打开时速度更快。

习惯使用 vue 的路由跳转方法 this.$router.push('/user-admin') , 对于 uni-app 中路由跳转方法还是不习惯, 尤其是无法使用快捷名称, 一定要使用全路径, 这样在每次路由跳转时都非常麻烦

插件开发

在 common/plugins 文件夹中创建 router.js

class Router {
  push() {}
}

export default {
  install(Vue, options) {
    Vue.prototype.$router = new Router()
  }
}

然后在 main.js 中进行注册

import router from '@/common/plugin/router'
Vue.use(router)

之后我们就可以按照 vue 的方式进行路由跳转了, 妈妈再也不用担心我记不住 uni-app 提供的路由跳转API名称了

$router.push

方法入参

既然是模仿 vue , 那入参形式当然要和 vue 保持一致了

typenamepathqueryparamsobject
StringStringStringObjectObjectObject
跳转类型路由别名路由路径带查询参数vuex参数其他配置

跳转类型解析

const navigate = (type) => {
    let navigateMethods
    switch (type) {
      case 'go':
        navigateMethods = uni.navigateTo
        break
      case 'tab':
        navigateMethods = uni.switchTab
        break
      case 'reLaunch':
        navigateMethods = uni.reLaunch
        break
      case 'redirectTo':
        navigateMethods = uni.redirectTo
        break
      default:
        navigateMethods = uni.navigateTo
        break
    }
    return navigateMethods
}

缺少了 preloadPage 方法,我业务中暂时未用到, 如果需要可以自行封装

路由别名

要实现路由别名就需要一份别名与完整路径对应表,这个时候就可以用到上面的模块化路由自动构建了

首先我们在配置单个页面路径的时候需要配置一个 name 属性, 如:

module.exports = {
  baseUrl: 'pages/home/',
  children: [
    {
      path: 'index',
      name: 'home',
      text: '主页',
      navigationBarBackgroundColor: '#5565DB',
    }
  ]
}

然后在解析路由 buildRouter 方法中,我们需要把 name 提取出来, 全局创建一个变量 nameList 用于保存 name 列表, buildRouter修改如下:

const nameList = []
const buildRouter = route => {
  const { baseUrl, children } = route
  return children.map(item => {
    const { name, path, text, ...otherConfig } = item
    if (!nameList.some(item => item.name === name)) {
      nameList.push({
        path: '/' + baseUrl + path,
        name
      })
    } else {
      console.error(`${name}命名重复, 请修改后重新编译`)
    }
    // 所有页面返回按钮加上中文汉字返回
    setBackChar(otherConfig)
    return {
      path: baseUrl + path,
      style: {
        navigationBarTitleText: text,
        ...otherConfig
      }
    }
  })
}

name 别名是不允许重复的, 如果重复打印错误信息提示修改

name 列表信息保存好后需要写入一个配置文件中, 于是写入方法中添加写入路由别名列表文件 aliasRouter.js

fs.writeFile(
  __dirname + '/aliasRouter.js',
  'export default ' +
  JSON.stringify(nameList, null, '  '),
  e => e ? console.error(e) : console.log('pages.json 路由别名列表文件更新成功')
)

push 方法

  1. 首先如果传入的是路由别名, 那么根据路由别名列表找出路径全名
  2. 然后根据 type 得出 uni 中 对应 API
  3. 将 query 解析成对应 URL 参数字符串 (bulidURL方法
  4. 将 params 保存到对应路由别名下(vuex)
  5. 将其他参数拼接, 进行路由跳转
const push = ({ type, name, path, query, params, object }) => {
    type = type || 'go'
    let pathFind
    if(name) pathFind = aliasRouter.find(item => item.name === name)
    const navigateMethods = this.navigate(type)
    let url = pathFind ? pathFind.path : path
    if (query) {
      url = bulidURL(url, query)
    }
    const navigateData = {
      url,
      fail(err) {
        console.error(err)
      },
      animationType: 'pop-in',
      animationDuration: 200,
      ...object
    }
    if (params) {
      if (!name) {
        console.error('请使用 name 跳转方式')
      } else {
        store.dispatch('router/addParamsData', { name, params }).then(() => {
          navigateMethods(navigateData)
        })
        return
      }
    }
    navigateMethods(navigateData)
  }

$router.back

返回时, 如果传入 callbackName, 那么将执行返回的那个页面 页面组件中 callbackName 方法, 并且将携带的参数带入

const back = ({ callbackName, data, object }) => {
   if (callbackName && isObject(callbackName)) {
     // 如果第一个参数是对象, 那么就直接返回无需回调
     object = callbackName
   } else if (callbackName && typeof callbackName === 'string') {
     const pages = getCurrentPages();  //获取所有页面栈实例列表
     const prevPage = pages[ pages.length - 2 ];  //上一页页面实例
     if (prevPage.$vm[callbackName]) prevPage.$vm[callbackName](data)
   }
   uni.navigateBack({
     delta: 1,
     animationType: 'pop-out',
     animationDuration: 200,
     fail(err) {
       console.error(err)
     },
     ...object
   });
}

使用示例

有一个列表展示页

  1. 点击按钮进行跳转到筛选页面(将默认搜索条件带入)
  2. 选择筛选条件完毕, 确认后返回上一层页面, 并带回搜索条件, 触发搜索
<!--列表展示页面-->
<template>
   <view class="content">
       <u-button @click="goSearchPage">查询</u-button>
       <view>
         <!--  列表展示    -->
       </view>
   </view>
</template>

<script>
export default {
 data() {
       return {
           setSearchForm: {
             name: '张三'
           }
       }
 },
 methods: {
   goSearchPage() {
       this.$router.push({
           name: 'search',
           query: this.setSearchForm
       })
   },
   setSearchForm(form) {
       this.setSearchForm = form
       this.getList()
   },
   getList() {
     // 根据搜索条件进行列表查询
   }
}
}
</script>
<!--列表搜索页面-->
<template>
 <view>
   <u-button @click="goBackSearch">确认</u-button>
 </view>
</template>

<script>
export default {
 data() {
   return {
     searchForm: {}
   }
 },
 onLoad(options) {
   this.searchForm = options
 },
 methods: {
   goBackSearch() {
     this.$router.back({
       callbackName: 'setSearchForm',
       data: this.searchForm
     })
   }
 }
}
</script>
  1. 列表页面点击查询进行路由跳转时, 通过 query 将搜索条件带到搜索页面
  2. 搜索页面通过 onLoad 页面生命周期获取上个页面传入参数, 并赋值给 searchForm
  3. 搜索页面点击确认后触发 列表页面 setSearchForm 方法, 并带入参数 searchForm

完整代码

import aliasRouter from '@/router/router'
import store from '@/store'

const toString = Object.prototype.toString

export function isDate (val) {
 return toString.call(val) === '[object Date]'
}

export function isObject (val) {
 return val !== null && typeof val === 'object'
}

function encode (val) {
 return encodeURIComponent(val)
   .replace(/%40/g, '@')
   .replace(/%3A/gi, ':')
   .replace(/%24/g, '$')
   .replace(/%2C/gi, ',')
   .replace(/%20/g, '+')
   .replace(/%5B/gi, '[')
   .replace(/%5D/gi, ']')
}

function bulidURL(url, params) {
 if (!params) {
   return url
 }

 const parts = []

 Object.keys(params).forEach((key) => {
   let val = params[key]
   if (val === null || typeof val === 'undefined') {
     return
   }
   let values
   if (Array.isArray(val)) {
     values = val
     key += '[]'
   } else {
     values = [val]
   }
   values.forEach((val) => {
     if (isDate(val)) {
       val = val.toISOString()
     } else if (isObject(val)) {
       val = JSON.stringify(val)
     }
     parts.push(`${encode(key)}=${encode(val)}`)
   })
 })

 let serializedParams = parts.join('&')

 if (serializedParams) {
   const markIndex = url.indexOf('#')
   if (markIndex !== -1) {
     url = url.slice(0, markIndex)
   }

   url += (url.indexOf('?') === -1 ? '?' : '&') + serializedParams
 }

 return url
}

class Router {
 navigate(type) {
   let navigateMethods
   switch (type) {
     case 'go':
       navigateMethods = uni.navigateTo
       break
     case 'tab':
       navigateMethods = uni.switchTab
       break
     case 'reLaunch':
       navigateMethods = uni.reLaunch
       break
     default:
       navigateMethods = uni.navigateTo
       break
   }
   return navigateMethods
 }
 push({ type, name, path, query, params, object }) {
   type = type || 'go'
   let pathFind
   if(name) pathFind = aliasRouter.find(item => item.name === name)
   const navigateMethods = this.navigate(type)
   let url = pathFind ? pathFind.path : path
   if (query) {
     url = bulidURL(url, query)
   }
   const navigateData = {
     url,
     fail(err) {
       console.error(err)
     },
     animationType: 'pop-in',
     animationDuration: 200,
     ...object
   }
   if (params) {
     if (!name) {
       console.error('请使用 name 跳转方式')
     } else {
       store.dispatch('router/addParamsData', { name, params }).then(() => {
         navigateMethods(navigateData)
       })
       return
     }
   }
   navigateMethods(navigateData)
 }
 back({ callbackName, data, object }) {
   if (callbackName && isObject(callbackName)) {
     // 如果第一个参数是对象, 那么就直接返回无需回调
     object = callbackName
   } else if (callbackName && typeof callbackName === 'string') {
     const pages = getCurrentPages();  //获取所有页面栈实例列表
     const prevPage = pages[ pages.length - 2 ];  //上一页页面实例
     if (prevPage.$vm[callbackName]) prevPage.$vm[callbackName](data)
   }
   uni.navigateBack({
     delta: 1,
     animationType: 'pop-out',
     animationDuration: 200,
     fail(err) {
       console.error(err)
     },
     ...object
   });
 }
}

export default {
 install(Vue, options) {
   Vue.prototype.$routers = new Router()
 }
}

export {
 Router
}

完结, 撒花!!!