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": "返回"
}
}
}
}
}
配置中经常使用的是 path
和 navigationBarTitleText
, 就是我们页面的路径与标题, 对应我们上述配置中的 path
和 text
, 而其他配置我们并列放置, 因此我们可以编写一个解析方法如下:
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 保持一致了
type | name | path | query | params | object |
---|---|---|---|---|---|
String | String | String | Object | Object | Object |
跳转类型 | 路由别名 | 路由路径 | 带查询参数 | 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 方法
- 首先如果传入的是路由别名, 那么根据路由别名列表找出路径全名
- 然后根据 type 得出 uni 中 对应 API
- 将 query 解析成对应 URL 参数字符串 (bulidURL方法)
- 将 params 保存到对应路由别名下(vuex)
- 将其他参数拼接, 进行路由跳转
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
});
}
使用示例
有一个列表展示页
- 点击按钮进行跳转到筛选页面(将默认搜索条件带入)
- 选择筛选条件完毕, 确认后返回上一层页面, 并带回搜索条件, 触发搜索
<!--列表展示页面-->
<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>
- 列表页面点击查询进行路由跳转时, 通过
query
将搜索条件带到搜索页面 - 搜索页面通过 onLoad 页面生命周期获取上个页面传入参数, 并赋值给
searchForm
- 搜索页面点击确认后触发 列表页面
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
}
完结, 撒花!!!