= RequiredKeysextends never ? { data?: Q } : { data: Q }') 39 | writer.writeLine('type Params= RequiredKeys
extends never ? { params?: P } : { params: P }') 40 | writer.writeLine('class Router {') 41 | 42 | writer.write(this.generateMethods()) 43 | 44 | writer.writeLine('}') 45 | }) 46 | 47 | const routerClass = this.routerSourceFile.getClass('Router')! 48 | const staticMembers = tempSourceFile.getClass('Router')!.getStaticMembers() 49 | this.routerSourceFile.addTypeAliases(tempSourceFile.getTypeAliases().map((m) => m.getStructure())) 50 | routerClass.addMembers(staticMembers.map((m) => m.getStructure() as any)) 51 | 52 | this.routerSourceFile.emitSync() 53 | tempSourceFile.delete() 54 | this.root.log(processTypeEnum.REMIND, '👋 已成功生成') 55 | } 56 | 57 | if (force) { 58 | _emit() 59 | } else { 60 | this.emitTimer = setTimeout(_emit, 300) 61 | } 62 | } 63 | 64 | generateMethods() { 65 | let methodText = '' 66 | let packages = this.root.pages.reduce((store, page) => { 67 | let pages = store.get(page.packageName) 68 | if (!pages) { 69 | pages = [] 70 | store.set(page.packageName, pages) 71 | } 72 | pages.push(page) 73 | return store 74 | }, new Map
()) 75 | 76 | for (const packageName of packages.keys()) { 77 | const pages = packages.get(packageName) 78 | if (packageName === 'main') { 79 | methodText += pages 80 | ?.map((page) => { 81 | return `static ${page.method?.name}: ${page.method?.type} = ${page.method?.value}` 82 | }) 83 | .join('\n\n') 84 | } else { 85 | methodText += ` 86 | static ${packageName}: { 87 | ${pages 88 | ?.map((page) => { 89 | return `${page.method?.name}: ${page.method?.type}` 90 | }) 91 | .join(';\n')} 92 | } = { 93 | ${pages 94 | ?.map((page) => { 95 | return `${page.method?.name}: ${page.method?.value}` 96 | }) 97 | .join(',\n')} 98 | } 99 | ` 100 | } 101 | } 102 | 103 | return methodText 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /packages/tarojs-router-next/src/router/index.ts: -------------------------------------------------------------------------------- 1 | import Taro, { Current, getCurrentInstance } from '@tarojs/taro' 2 | import { ROUTE_KEY } from '../constants' 3 | import { NoPageException } from '../exception/no-page' 4 | import { formatPath, isNil } from '../func' 5 | import { execMiddlewares, getMiddlewares } from '../middleware' 6 | import { PageData } from '../page-data' 7 | import { execRouterBackListener } from '../router-back-listener' 8 | import { NavigateOptions, NavigateType, Route } from './type' 9 | 10 | export { NavigateOptions, NavigateType, Route } from './type' 11 | 12 | export class Router { 13 | /** 14 | * 页面跳转 15 | * @param route 目标路由对象 16 | * @param options 跳转选项 17 | */ 18 | static async navigate (route: Route, options?: NavigateOptions): Promise { 19 | options = { ...{ type: NavigateType.navigateTo, params: {} }, ...options } 20 | options.params = Object.assign({}, options.params) 21 | const route_key = Date.now() + '' 22 | 23 | Current['_page'] = Current.page 24 | Object.defineProperties(Current, { 25 | page: { 26 | set: function (page) { 27 | if (page === undefined || page === null) { 28 | this._page = page 29 | return 30 | } 31 | if (!page[ROUTE_KEY]) { 32 | const originOnUnload = page.onUnload 33 | page.onUnload = function () { 34 | originOnUnload && originOnUnload.apply(this) 35 | PageData.emitBack(route_key) 36 | setTimeout(() => execRouterBackListener(route)) 37 | } 38 | page[ROUTE_KEY] = route_key 39 | } 40 | this._page = page 41 | }, 42 | get: function () { 43 | return this._page 44 | }, 45 | }, 46 | }) 47 | 48 | if (options.data) { 49 | PageData.setPageData(route_key, options.data) 50 | } 51 | 52 | const context = { route, type: options.type!, params: options.params, data: options.data } 53 | 54 | const middlewares = getMiddlewares(context) 55 | const url = formatPath(route, options!.params!) 56 | middlewares.push(async (ctx, next) => { 57 | switch (options!.type) { 58 | case NavigateType.reLaunch: 59 | await Taro.reLaunch({ 60 | url, 61 | complete: options?.complete, 62 | fail: options?.fail, 63 | success: options?.success, 64 | }) 65 | break 66 | case NavigateType.redirectTo: 67 | await Taro.redirectTo({ 68 | url, 69 | complete: options?.complete, 70 | fail: options?.fail, 71 | success: options?.success, 72 | }) 73 | break 74 | case NavigateType.switchTab: 75 | await Taro.switchTab({ 76 | url, 77 | complete: options?.complete, 78 | fail: options?.fail, 79 | success: options?.success, 80 | }) 81 | break 82 | default: 83 | await Taro.navigateTo({ 84 | url, 85 | complete: options?.complete, 86 | fail: options?.fail, 87 | success: options?.success, 88 | }) 89 | break 90 | } 91 | next() 92 | }) 93 | 94 | return new Promise(async (res, rej) => { 95 | try { 96 | PageData.setPagePromise(route_key, { res, rej }) 97 | await execMiddlewares(middlewares, context) 98 | } catch (err) { 99 | rej(err) 100 | } 101 | }) 102 | } 103 | 104 | /** 105 | * 返回上一个页面 106 | * @param result 返回给上一个页面的数据,如果 result 是 Error 的实例,则是抛出异常给上一个页面 107 | * @param options 其他选项 108 | */ 109 | static back( 110 | result?: unknown, 111 | options?: { 112 | /** 返回的页面数,如果 delta 大于现有页面数,则返回到首页。 */ 113 | delta?: number 114 | } 115 | ) { 116 | if (!isNil(result)) { 117 | PageData.setBackResult(result) 118 | } 119 | 120 | const currentPages = Taro.getCurrentPages() 121 | if (currentPages.length > 1) { 122 | return Taro.navigateBack(options) 123 | } 124 | 125 | throw new NoPageException() 126 | } 127 | 128 | /** 129 | * 设置页面返回的数据 130 | * 当物理键返回和左上角返回也需要带数据时会使用到 131 | */ 132 | static setBackResult(result: any) { 133 | PageData.setBackResult(result) 134 | } 135 | 136 | /** 137 | * 获取上一个页面携带过来的数据 138 | * @param default_value 默认数据 139 | */ 140 | static getData (default_value?: T): T | undefined { 141 | return PageData.getPageData(default_value) 142 | } 143 | 144 | /** 获取上一个页面携带过来的参数 */ 145 | static getParams >>(): T { 146 | const instance = getCurrentInstance() 147 | return Object.assign({}, instance.router?.params) as T 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /docs/guide/quike/middleware.md: -------------------------------------------------------------------------------- 1 | # 路由中间件 2 | 3 | 路由中间件将在跳转到目标页面之前执行,中间件的执行流程参考 koa 的洋葱模型: 4 | 5 |  6 | 7 | koa 是一个后端框架,在 koa 中,用户发起一个 http 请求给 koa 启动的服务,请求一层层进入 koa 的中间件,最终进入到一段 `具体的逻辑`(数据库操作或其他),然后再原路返回一段响应给用户。 8 | 9 | 换到这里就是,用户发起一个请求(进入页面的请求,包含目标页面的 url,ext 数据),请求一层层进入注册的中间件,然后进入到最后一个中间件:`跳转到目标页面的中间件`。然后再原路返回。 10 | 11 | ## 跳转到目标页面的中间件 12 | 13 | 在 tarojs-router-next 的路由跳转中,有一个隐藏的中间件:`跳转到目标页面的中间件`,它会默认添加在当前路由要执行的中间件的最后一个 `[...你的中间件, 跳转到目标页面的中间件]`,它即代表了 `目标页面`。 14 | 15 | ## 通过一个示例理解 16 | 17 | 注册几个路由中间件 18 | 19 | ```typescript 20 | import Taro from '@tarojs/taro' 21 | import { Middleware, registerMiddlewares } from 'tarojs-router-next' 22 | 23 | export const M1: Middleware = async (ctx, next) => { 24 | console.log('第一个中间件执行:', ctx.route.url) 25 | await next() // 执行下一个中间件 26 | } 27 | 28 | export const M2: Middleware = async (ctx, next) => { 29 | console.log('第二个中间件执行:', ctx.route.url) 30 | await next() // 执行下一个中间件 31 | } 32 | 33 | export const M3: Middleware = async (ctx, next) => { 34 | console.log('第三个中间件执行:', ctx.route.url) 35 | await next() // 执行下一个中间件 36 | } 37 | 38 | // 注册路由中间件 39 | registerMiddlewares([M1, M2, M3]) 40 | 41 | // 其实会执行四个中间件 [M1, M2, M3, 跳转到目标页面的中间件] 42 | ``` 43 | 44 | 然后在 `/pages/home/index` 页进行跳转到 `/pages/me/index` 页面 45 | 46 | ```typescript 47 | // pages/home/index.tsx 48 | import { Router } from 'tarojs-router-next' 49 | Router.toMe() // 进行页面跳转 50 | ``` 51 | 52 | 在 `/pages/me/index` 页面打印内容 53 | 54 | ```typescript 55 | // pages/me/index.tsx 56 | export default function Page() { 57 | console.log('成功进入了页面:me') 58 | return 59 | } 60 | ``` 61 | 62 | 输出: 63 | 64 | ```shell 65 | 第一个中间件执行:/pages/me/index 66 | 第二个中间件执行:/pages/me/index 67 | 第三个中间件执行:/pages/me/index 68 | 成功进入了页面:me 69 | ``` 70 | 71 | 现在我们想要在第二个中间件中判断用户是否登录,如果未登录就不要进入 me 页面,则只需要进行一些判断即可: 72 | 73 | ```typescript 74 | export const M2: Middleware = async (ctx, next) => { 75 | console.log('第二个中间件执行:', ctx.route.url) 76 | if (hasLogin()) { 77 | await next() // 执行下一个中间件 78 | } else { 79 | // 只要不执行 next,不进入后面的中间件即可 80 | console.log('请登录') 81 | } 82 | } 83 | ``` 84 | 85 | ## 注册路由中间件 86 | 87 | 上面的例子中我们注册了三个中间件,用的是 [registerMiddlewares](/api/register-middlewares),注册单个中间件可以使用 [registerMiddleware](/api/register-middleware) 88 | 89 | ```typescript 90 | import Taro from '@tarojs/taro' 91 | import { Middleware, registerMiddleware } from 'tarojs-router-next' 92 | 93 | export const M1: Middleware = async (ctx, next) => { 94 | console.log('中间件执行:', ctx.route.url) 95 | await next() 96 | console.log('中间件执行结束') 97 | } 98 | 99 | registerMiddleware(M1) 100 | ``` 101 | 102 | ## 动态注册路由中间件 103 | 104 | 有的时候我们希望某个中间件只为特定的页面工作,这个需求可以在中间件中增加判断条件来实现,但在中间件中做这些判断会使中间件的职能不够专一,并且这些判断逻辑无法在多个中间件中复用 105 | 106 | 怎么解决呢,我们可以在注册中间件时传递一个方法,将本来要写到中间件中的判断逻辑抽取到该方法中。在路由进入时该方法会被调用并传入当前路由的上下文,若方法返回 `true` 则为当前路由执行这些中间件 107 | 108 | ```typescript 109 | // 仅为 me 和 home 页面注册该路由中间件 110 | registerMiddleware(Logger, (ctx) => { 111 | return ['/pages/me/index', '/pages/home/index'].indexOf(ctx.route.url) !== -1 112 | }) 113 | 114 | // 注册多个中间件 115 | registerMiddlewares([Logger, Auth], (ctx) => { 116 | return ['/pages/me/index', '/pages/home/index'].indexOf(ctx.route.url) !== -1 117 | }) 118 | ``` 119 | 120 | ## 路由附加数据 121 | 122 | vue 开发者一定知道,我们使用 vue-router 定义路由时可以通过 [路由元信息](https://router.vuejs.org/zh/guide/advanced/meta.html) 携带一些数据告知导航守卫对该路由做一些特殊的处理: 123 | 124 | 比如告诉导航守卫该页面是要登陆的 125 | 126 | ```typescript 127 | const router = new VueRouter({ 128 | routes: [ 129 | { 130 | path: '/me', 131 | ... 132 | meta: { mustLogin: true } 133 | } 134 | ] 135 | }) 136 | ``` 137 | 138 | 或者是某些权限才可以访问, 139 | 140 | ```typescript 141 | { 142 | ... 143 | meta: { roles: [1, 2, 3] } 144 | } 145 | ``` 146 | 147 | 然后我们就可以在导航守卫中获取到 `meta` 和 `route` 来进行一些判断和处理 148 | 149 | #### 在 tarojs-router-next 中这样实现: 150 | 151 | 首先,我们要定义附加数据,在页面文件夹下面新建 [route.config.ts](/guide/quike/route-config) 文件,然后导出 [Ext](/guide/quike/route-config#导出附加数据-ext): 152 | 153 |  154 | 155 | 然后就可以在中间件中访问并使用: 156 | 157 | ```typescript 158 | import Taro from '@tarojs/taro' 159 | import { Middleware, Router } from 'tarojs-router-next' 160 | 161 | export const AuthCheck: Middleware<{ mustLogin: boolean }> = async (ctx, next) => { 162 | if (ctx.route.ext?.mustLogin) { 163 | const token = Taro.getStorageSync('token') 164 | if (!token) { 165 | const { confirm } = await Taro.showModal({ 166 | title: '提示', 167 | content: '请先登录', 168 | }) 169 | 170 | if (confirm) Router.toLogin() 171 | 172 | // 直接返回,不执行 next 即可打断中间件向下执行 173 | return 174 | } 175 | } 176 | 177 | await next() 178 | } 179 | ``` 180 | 181 | 但是请注意的是,通过 [route.config.ts](/guide/quike/route-config) 这种方式定义的附加数据,只有通过 [Router.to\*\*](/api/router#to-options-) 跳转时才会携带,通过 [Router.navigate](/api/router#navigate-route-options-) 跳转时,请通过 route.ext 参数携带 182 | 183 | ```typescript 184 | import { Router } from 'tarojs-router-next' 185 | Router.navigate({ url: '/pages/article-detail/index', ext: { mustLogin: true } }) 186 | ``` 187 | -------------------------------------------------------------------------------- /packages/tarojs-router-next-plugin/src/loader.ts: -------------------------------------------------------------------------------- 1 | import { processTypeEnum } from '@tarojs/helper/dist/constants' 2 | import fs from 'fs' 3 | import normalize from 'normalize-path' 4 | import path from 'path' 5 | import { Project, SourceFile } from 'ts-morph' 6 | import { IConfigPackage } from './config' 7 | import { ConfigPage, Page, RouteConfig } from './entitys' 8 | import { Plugin } from './plugin' 9 | import { extractValue, formatPageDir, isNil } from './utils' 10 | 11 | export class Loader { 12 | project = new Project() 13 | configPages: ConfigPage[] = [] 14 | appConfigPath: string 15 | appConfig: { 16 | pages: string[] 17 | subpackages?: any[] 18 | subPackages?: any[] 19 | window: any 20 | } 21 | 22 | constructor(private readonly root: Plugin) { 23 | // 非开发模式则读取 app.config.ts 中的配置,用于过滤未配置的页面 24 | if (!this.root.isWatch) this.loadConfigPages() 25 | } 26 | 27 | loadConfigPages(dynamic = false) { 28 | if (!dynamic && this.appConfig) return 29 | 30 | this.appConfigPath = this.root.helper.resolveMainFilePath(path.resolve(this.root.paths.sourcePath, './app.config')) 31 | this.appConfig = this.root.helper.readConfig(this.appConfigPath) 32 | 33 | for (const page of this.appConfig.pages) { 34 | this.configPages.push({ 35 | path: page, 36 | packageRoot: '', 37 | fullPath: path.resolve(this.root.paths.sourcePath, page), 38 | }) 39 | } 40 | 41 | for (const pkg of this.appConfig.subpackages || this.appConfig.subPackages || []) { 42 | for (const page of pkg.pages) { 43 | this.configPages.push({ 44 | path: page, 45 | packageRoot: pkg.root, 46 | fullPath: path.resolve(this.root.paths.sourcePath, pkg.root, page), 47 | }) 48 | } 49 | } 50 | } 51 | 52 | loadPages() { 53 | this.root.pages = [] 54 | for (const pkg of this.root.config.packages) { 55 | const routeConfigSourceFiles = this.project.addSourceFilesAtPaths(pkg.pagePath + '/**/route.config.ts') 56 | 57 | fs.readdirSync(pkg.pagePath) 58 | // 过滤一些非页面文件夹 59 | .filter((pageDirName) => this.root.config.ignore.indexOf(pageDirName) === -1) 60 | .forEach((pageDirName) => { 61 | const fullPath = path.resolve(pkg.pagePath, pageDirName, 'index') 62 | if ( 63 | !this.root.isWatch && 64 | this.configPages.findIndex((configPage) => configPage.fullPath === fullPath) === -1 65 | ) { 66 | return 67 | } 68 | 69 | const page = new Page() 70 | page.packageName = pkg.name 71 | page.dirName = pageDirName 72 | page.dirPath = path.resolve(pkg.pagePath, pageDirName) 73 | // 生成跳转路径 pages/xx/xx 74 | page.path = normalize(path.join(pkg.pagePath.replace(this.root.paths.sourcePath, ''), pageDirName, 'index')) 75 | page.fullPath = fullPath 76 | 77 | const sourceFile = routeConfigSourceFiles.find((sourceFile) => { 78 | return path.normalize(sourceFile.compilerNode.fileName) === path.resolve(page.dirPath, 'route.config.ts') 79 | }) 80 | 81 | if (sourceFile) { 82 | this.loadRouteConfig(page, sourceFile) 83 | } 84 | 85 | this.loadMethod(page) 86 | 87 | this.root.pages.push(page) 88 | this.root.log( 89 | processTypeEnum.GENERATE, 90 | `Router.${page.packageName === 'main' ? '' : page.packageName + '.'}${page.method?.name}` 91 | ) 92 | }) 93 | } 94 | } 95 | 96 | loadPage(pageDirPath: string, pkg: IConfigPackage) { 97 | const index = this.root.pages.findIndex((page) => page.dirPath === pageDirPath) 98 | 99 | const isExist = fs.existsSync(pageDirPath) 100 | if (isExist) { 101 | if (index !== -1) { 102 | const page = this.root.pages[index] 103 | this.loadRouteConfig(page) 104 | this.loadMethod(page) 105 | this.root.log( 106 | processTypeEnum.MODIFY, 107 | `Router.${page.packageName === 'main' ? '' : page.packageName + '.'}${page.method?.name}` 108 | ) 109 | } else { 110 | const page = new Page() 111 | page.packageName = pkg.name 112 | page.dirName = path.parse(pageDirPath).name 113 | page.dirPath = pageDirPath 114 | page.path = path.resolve(pageDirPath.replace(this.root.paths.sourcePath, ''), 'index') 115 | page.fullPath = path.resolve(pageDirPath, 'index') 116 | this.loadRouteConfig(page) 117 | this.loadMethod(page) 118 | this.root.pages.push(page) 119 | this.root.log( 120 | processTypeEnum.GENERATE, 121 | `Router.${page.packageName === 'main' ? '' : page.packageName + '.'}${page.method?.name}` 122 | ) 123 | } 124 | return true 125 | } else { 126 | if (index !== -1) { 127 | const [page] = this.root.pages.splice(index, 1) 128 | this.root.log( 129 | processTypeEnum.UNLINK, 130 | `Router.${page.packageName === 'main' ? '' : page.packageName + '.'}${page.method?.name}` 131 | ) 132 | return true 133 | } else { 134 | return false 135 | } 136 | } 137 | } 138 | 139 | loadRouteConfig(page: Page, configSourceFile?: SourceFile) { 140 | page.routeConfig = undefined 141 | const routeConfig: RouteConfig = {} 142 | 143 | if (!configSourceFile) { 144 | const configPath = path.resolve(page.dirPath, 'route.config.ts') 145 | if (!fs.existsSync(configPath)) return 146 | configSourceFile = this.project.getSourceFile(configPath) 147 | if (configSourceFile) { 148 | configSourceFile.refreshFromFileSystemSync() 149 | } else { 150 | configSourceFile = this.project.addSourceFileAtPath(configPath) 151 | } 152 | } 153 | 154 | configSourceFile.getExportedDeclarations().forEach((declarations, name) => { 155 | if (declarations.length > 1) return 156 | const declaration = declarations[0] as any 157 | switch (name) { 158 | case 'Params': 159 | routeConfig.params = `import('${path.resolve(page.dirPath, 'route.config').replace(/\\/g, '/')}').Params` 160 | break 161 | case 'Data': 162 | routeConfig.data = `import('${path.resolve(page.dirPath, 'route.config').replace(/\\/g, '/')}').Data` 163 | break 164 | case 'BackData': 165 | routeConfig.backData = `import('${path.resolve(page.dirPath, 'route.config').replace(/\\/g, '/')}').BackData` 166 | break 167 | case 'Ext': 168 | routeConfig.ext = extractValue({ 169 | name, 170 | declaration, 171 | }) 172 | break 173 | } 174 | }) 175 | 176 | page.routeConfig = routeConfig 177 | } 178 | 179 | loadMethod(page: Page) { 180 | const { routeConfig, dirName } = page 181 | 182 | let methodName = 'to' + formatPageDir(dirName) 183 | const methodBody = `return Router.navigate({ url: "${page.path}"${ 184 | routeConfig?.ext ? ', ext: ' + routeConfig.ext : '' 185 | } }, options)` 186 | 187 | let method = `function (options) {${methodBody}}` 188 | 189 | let methodType: string 190 | 191 | let ReturnType = 'any' 192 | if (routeConfig?.backData) { 193 | ReturnType = routeConfig.backData 194 | } 195 | 196 | if (!routeConfig || (isNil(routeConfig.params) && isNil(routeConfig.data))) { 197 | methodType = ` ` + 198 | "(options?: NavigateOptions & Params > & Data >) => Promise "; 199 | page.method = { 200 | name: methodName, 201 | type: methodType, 202 | value: method, 203 | } 204 | return 205 | } 206 | 207 | methodType = ` ` + 208 | "(...options: RequiredKeys > & Data >> extends never " + 209 | "? [options?: NavigateOptions & Params > & Data >] : [options: NavigateOptions & Params > & Data >]) => Promise "; 210 | page.method = { 211 | name: methodName, 212 | type: methodType, 213 | value: method, 214 | } 215 | } 216 | } 217 | --------------------------------------------------------------------------------