十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Meteor 应用 URL 路由实战:基于 Flow Router Extra 的客户端路由、Blaze 页面渲染与按路由动态加载

Meteor 应用 URL 路由实战:基于 Flow Router Extra 的客户端路由、Blaze 页面渲染与按路由动态加载 Meteor 应用 URL 路由实战基于 Flow Router Extra 的客户端路由、Blaze 页面渲染与按路由动态加载【免费下载链接】meteorMeteor, the JavaScript App Platform项目地址: https://gitcode.com/gh_mirrors/me/meteor本指南以 Meteor 官方指南《URLs and Routing》为骨架系统讲解如何在 Meteor 客户端渲染应用中用 URL 驱动 UI包括 URL 在 data on the wire 架构下的角色定位、使用ostrio:flow-router-extra定义路由与访问路由信息、结合 Blaze Layout 渲染页面、按路由动态加载模块以及重定向、404 与服务端路由等高级主题。读完本文你将能够为自己的 Meteor 应用实现完整、可书签、可分享、可被搜索引擎理解的 URL 路由体系并结合仓库源码理解 Flow Router Extra 与 Meteor 动态导入dynamic import底层的运行机制。客户端路由URL 在 Meteor 应用中的角色在 Web 应用中路由routing指的是用 URL 来驱动用户界面UI的过程。URL 是每个浏览器中醒目的存在对用户而言主要有三个功能书签Bookmarking——用户可以把 URL 收藏在浏览器中以便日后回到想保存的内容分享Sharing——用户可以通过把某个页面的链接发给他人来分享内容导航Navigation——URL 驱动浏览器的前进/后退功能。在传统的 Web 应用技术栈中服务器逐页渲染 HTMLURL 是用户访问应用的根本入口用户点击 URL请求经 HTTP 发送到服务器服务器通过服务端路由器做出响应。Meteor 则遵循data on the wire数据在线原则服务器不关心 URL也不关心 HTML 页面客户端应用通过DDP与服务器通信。通常应用加载时会初始化一系列订阅subscriptions来拉取渲染所需的数据随着用户与应用交互不同的订阅被加载——这个过程在技术上完全不需要 URL 参与你甚至可以做一个 URL 从不变化的 Meteor 应用。但 URL 对用户友好的三个功能在典型 Meteor 应用中仍然重要。由于服务器不随 URL 驱动URL 就变成用户当前正在查看的客户端状态的一种有用表示。与服务器渲染应用不同的是它不必描述用户当前状态的全部只需要包含你想要可链接linkable的那部分。例如URL 中应包含页面上的搜索过滤条件但不必包含某个下拉菜单或弹窗的开合状态。核心结论在 Meteor 中URL 是对客户端部分状态的序列化。选择哪些状态放进 URL本质上是产品设计决策——它决定用户能否把当前视图分享给他人。选择路由包Flow Router 与 Flow Router ExtraMeteor 社区中客户端路由的主流方案是 Flow Router 及其增强分支 Flow Router Extra。Flow Router是kadira团队发布的 Meteor 社区路由包以轻量、与框架无关著称专注于 URL 匹配与路由信息访问。Flow Router Extra包名ostrio:flow-router-extra是在原版 Flow Router 基础上精心扩展的维护版本完整保留了原版 API并额外提供waitOn路由级等待订阅/资源就绪、模板上下文template context以及内置的.render()方法。此外arillo:flow-router-helpers与zimme:active-route这两个常用辅助包已内置其中并持续适配最新的 Meteor 版本——这正是本文推荐使用 Flow Router Extra 的原因。安装命令meteor add ostrio:flow-router-extra定义路由第一个 FlowRouter.route路由器的基本职责是匹配特定 URL 并据此执行动作。这一切都发生在客户端——用户的浏览器或移动应用容器中。以 Meteor Todos 示例应用为例FlowRouter.route(/lists/:_id, { name: Lists.show, action(params, queryParams) { console.log(Looking at a list?); } });这个路由处理器会在两种情况下运行页面最初加载时 URL 恰好匹配该模式页面打开期间 URL 变为匹配该模式的地址。注意与服务器端渲染应用不同URL 的变化不需要向服务器发送任何额外请求——这正是客户端路由的本质。当路由被匹配时action方法执行你可以在此完成任何需要的动作。name属性是可选的但它让我们可以更便捷地引用该路由例如用FlowRouter.pathFor(Lists.show, ...)或FlowRouter.go(Lists.show, ...)。URL 模式匹配路径参数与查询串以/lists/:_id为例路径中某一段以:为前缀表示这是一个URL 参数url parameter它将匹配该路径段中的任意字符串。Flow Router 会把这部分 URL 通过当前路由的params属性暴露出来。此外URL 还可以包含 HTTP查询字符串query string?之后的部分。Flow Router 会将其拆分成语义化的命名参数称为queryParams。以下是示例 URL 及其对应的params和queryParamsURL是否匹配模式paramsqueryParams/否/about否/lists/否/lists/eMtGij5AFESbTKfkT是{ _id: eMtGij5AFESbTKfkT }{ }/lists/1是{ _id: 1 }{ }/lists/1?todoSorttop是{ _id: 1 }{ todoSort: top }注意params和queryParams中的值永远是字符串因为 URL 本身无法编码数据类型。例如想让参数代表一个数字访问时可能需要用parseInt(value, 10)做转换。访问当前路由信息除了把参数作为action函数的入参传递Flow Router 还通过全局单例FlowRouter暴露了一系列响应式或非响应式的函数。用户在你的应用中导航时这些函数的值会部分响应式地随之变化。与其他全局单例一样参见数据结构与 Stores 相关说明最好限制对FlowRouter的访问范围让应用的各部分是模块化、相互独立的。具体来说最好只在组件层级的最顶端访问FlowRouter——即在页面page组件或包裹它的布局layout组件中访问更多内容参见 UI/UX 文章。当前路由的响应式 API以下是在代码中访问当前路由信息的常用响应式函数FlowRouter.getRouteName()——获取路由的 nameFlowRouter.getParam(paramName)——返回单个 URL 参数的值FlowRouter.getQueryParam(paramName)——返回单个 URL 查询参数的值。在 Todos 示例应用的列表页中我们用FlowRouter.getParam(_id)获取当前列表的 id下文会看到更多用法。高亮当前激活路由有一个场景适合在组件层级深处访问全局FlowRouter单例通过导航组件渲染链接时通常需要以某种方式高亮当前激活的路由即用户正在浏览的站点区域。在 Todos 示例应用中我们在App_body模板中为每个用户已知的列表生成链接{{#each list in lists}} a classlist-todo {{activeListClass list}} ... {{list.name}} /a {{/each}}再通过activeListClass辅助函数判断用户当前是否正在查看该列表Template.App_body.helpers({ activeListClass(list) { const active ActiveRoute.name(Lists.show) FlowRouter.getParam(_id) list._id; return active active; } });这里ActiveRoute.name(Lists.show)用于判断当前路由是否为Lists.show同时用FlowRouter.getParam(_id)比对列表 id。ActiveRoute是 Flow Router Extra 内置的zimme:active-route辅助能力无需额外安装。根据路由渲染页面理解了如何定义路由、如何访问当前路由信息之后就可以实现路由最常见的用途——为用户的访问渲染对应的用户界面。本节以 Blaze 作为 UI 引擎讲解。如果你用 React 或 Angular 构建应用思路类似只是代码略有不同。使用 Flow Router 时为不同 URL 展示不同视图的最简单方式是搭配Blaze Layout包meteor add kadira:blaze-layout使用该包需要先定义一个布局layout组件。Todos 示例应用中的布局组件叫App_bodytemplate nameApp_body ... {{ Template.dynamic templatemain}} ... /template这里并非完整的App_body组件只突出最关键的部分。此处使用了 Blaze 的Template.dynamic特性来渲染一个绑定到数据上下文main属性的模板。借助 Blaze Layout我们可以在路由被访问时改变main属性的值。在Lists.show路由定义的action函数中完成这一操作FlowRouter.route(/lists/:_id, { name: Lists.show, action() { BlazeLayout.render(App_body, {main: Lists_show_page}); } });这意味着每当用户访问形如/lists/X的 URL 时Lists.show路由触发BlazeLayout调用把App_body组件的main属性设为Lists_show_page页面随即切换。组件即页面Components as pages注意我们命名为Lists_show_page而不是Lists_show的组件这表示该模板由 Flow Router 的 action 直接渲染构成该 URL 渲染层级的最顶端。Lists_show_page模板无参数渲染——它自身负责从当前路由收集信息再把这些信息传给它的子模板。相应地Lists_show_page与该路由强耦合因此它是一个智能组件smart component关于智能组件与可复用组件的讨论见 UI/UX 文章。一个像Lists_show_page这样的页面智能组件应当承担四项职责收集路由信息订阅相关的订阅subscription从这些订阅中获取数据把数据传给子组件。因此Lists_show_page的 HTML 模板非常简洁大部分逻辑放在 JavaScript 中template nameLists_show_page {{#each listId in listIdArray}} {{ Lists_show (listArgs listId)}} {{else}} {{ App_notFound}} {{/each}} /template{{#each listId in listIdArray}}是一种用于页面切换动画的技巧参见 UI/UX 文章中的页面切换动画。Template.Lists_show_page.helpers({ // 我们对一个元素的数组使用 #each这样在切换列表时list模板会被 // 移除并新建一份副本这对动画效果很重要。 listIdArray() { const instance Template.instance(); const listId instance.getListId(); return Lists.findOne(listId) ? [listId] : []; }, listArgs(listId) { const instance Template.instance(); return { todosReady: instance.subscriptionsReady(), // 我们以函数形式传递 list包含完整 list 及其所有字段 // 因为我们要控制响应性。勾选一个 todo 项时list.incompleteCount // 会变化。如果不这样做每次勾选都会重渲染整个 list。 // 通过把 list 的响应性隔离到真正关心它的区域可以避免这种情况。 list() { return Lists.findOne(listId); }, // 只带 _id 字段查找 list避免对 list.incompleteCount 建立依赖 // 从而在它变化时不会重渲染 todos。 todos: Lists.findOne(listId, {fields: {_id: true}}).todos() }; } });真正负责渲染页面内容的是listShow这个可复用组件。页面组件把参数传给可复用组件因此它可以非常机械——与路由器对话和渲染页面这两类关注点被清晰分离了。登录态变化时的渲染逻辑在组件层做授权有些渲染逻辑既与路由相关又与 UI 渲染相关典型例子是授权authorization例如对于部分页面如果用户尚未登录你可能想渲染一个登录表单。最佳实践是所有该渲染什么的逻辑都放在组件层级即渲染组件的树中因此授权应发生在组件内部。假设我们想把它加进上面的Lists_show_pagetemplate nameLists_show_page {{#if currentUser}} {{#each listId in listIdArray}} {{ Lists_show (listArgs listId)}} {{else}} {{ App_notFound}} {{/each}} {{else}} Please log in to edit posts. {{/if}} /template当多个需要访问控制的页面都要复用这一行为时可以通过把模板包进一个包含所需行为的包装wrapper布局组件来共享功能。利用 Blaze 的 template as block helper 能力可以创建包装组件参见 Blaze 指南中的 Block Helperstemplate nameApp_forceLoggedIn {{#if currentUser}} {{ Template.contentBlock}} {{else}} Please log in see this page. {{/if}} /template随后包裹我们的Lists_show_pagetemplate nameLists_show_page {{#App_forceLoggedIn}} {{#each listId in listIdArray}} {{ Lists_show (listArgs listId)}} {{else}} {{ App_notFound}} {{/each}} {{/App_forceLoggedIn}} /template这种做法的最大优势是查看Lists_show_page时能立刻看清用户访问该页面将发生什么行为。多种此类行为可以通过多层包裹组合或者创建一个组合多个包装模板的元包装meta-wrapper模板。路由切换与编程式导航为用户提供到达新路由的方式才有意义。最直接的方式是a标签加 URL。你可以借助FlowRouter.pathFor之类的辅助函数自行生成 URL。Todos 示例应用中的导航链接如下a href{{pathFor Lists.show _idlist._id}} title{{list.name}} classlist-todo {{activeListClass list}}pathFor的第一个参数是路由名对应定义路由时的name后续参数是 URL 参数键值对它会自动生成匹配该路由的 URL 字符串。编程式路由跳转有些场景需要在用户操作后不以点击链接的方式切换路由。例如在示例应用中用户创建新列表后我们想把用户带到刚创建的列表页。一旦得知新列表的 id就调用FlowRouter.go()import { insert } from ../../api/lists/methods.js; Template.App_body.events({ click .js-new-list() { const listId insert.call(); FlowRouter.go(Lists.show, { _id: listId }); } });如果只想改变 URL 的一部分可以使用FlowRouter.setParams()和FlowRouter.setQueryParams()。例如从查看一个列表切换到另一个列表FlowRouter.setParams({_id: newList._id});当然FlowRouter.go()永远可行因此除非在特定场景下做针对性优化否则优先使用它。在 URL 中存储数据如开篇所述URL 本质上是用户当前查看的客户端状态的部分序列化。虽然参数只能是字符串但任何类型的数据都可以通过序列化转换成字符串。一般地若想在 URL 参数中存储任意可序列化数据可以用EJSON.stringify()将其转为字符串再用encodeURIComponent进行 URL 编码去掉对 URL 有特殊含义的字符FlowRouter.setQueryParams({data: encodeURIComponent(EJSON.stringify(data))});取回数据时用EJSON.parse()。注意Flow Router 会自动帮你做 URL 解码const data EJSON.parse(FlowRouter.getQueryParam(data));EJSON是 Meteor 内置的扩展 JSON 序列化格式支持Date、ObjectID、二进制数据等扩展类型因此在 URL 中序列化复杂客户端状态非常顺手其实现位于仓库 packages/ejson/ejson.js。重定向有时用户会到达一个并不合适停留的页面数据被移走了、在管理后台登出了、或者刚创建了新对象而你希望用户落到新对象的页面。通常可以在用户动作的响应中调用FlowRouter.go()及其同类函数完成重定向如上文的建列表示例但当用户直接浏览到一个不存在的 URL 时则需要知道如何立即重定向。静态重定向如果 URL 已过时比如应用修改了 URL 方案可以在路由的action函数内部重定向FlowRouter.route(/old-list-route/:_id, { action(params) { FlowRouter.go(Lists.show, params); } });动态重定向上述方式只适用于静态重定向。有时你需要先加载一些数据才能决定重定向去向。这时需要渲染部分组件层级来订阅所需数据。例如 Todos 示例应用想让根路由/重定向到第一个已知列表先渲染一个专门的App_rootRedirector路由FlowRouter.route(/, { name: App.home, action() { BlazeLayout.render(App_body, {main: App_rootRedirector}); } });App_rootRedirector组件被渲染在App_body布局内而该布局负责在渲染其子组件之前订阅用户已知的列表集合且我们保证至少存在一个这样的列表。这意味着只要App_rootRedirector被创建必然有列表已加载于是可以Template.App_rootRedirector.onCreated(function rootRedirectorOnCreated() { // 这里需要设置一个 timeout以免在重定向过程中再次触发重定向 // 这是当前 FR 版本的一个限制。 Meteor.setTimeout(() { FlowRouter.go(Lists.show, Lists.findOne()); }); });如果你需要等待一个创建时尚未订阅的特定数据可以用autorun配合subscriptionsReady()等待订阅完成Template.App_rootRedirector.onCreated(function rootRedirectorOnCreated() { // 如果我们需要在这里打开订阅 this.subscribe(lists.public); // 现在需要等待上面的订阅。等待期间模板也需要渲染某种加载状态。 this.autorun(() { if (this.subscriptionsReady()) { FlowRouter.go(Lists.show, Lists.findOne()); } }); });用户操作后的重定向通常当用户完成某个动作后你只想以编程方式进入新路由。上文建列表的例子是**乐观式optimistically**重定向——即在收到服务器 Method 成功的响应之前就跳转。这样做是合理的因为我们有理由预期该方法在绝大多数情况下会成功关于乐观 UI 的进一步讨论见 UI/UX 文章。但如果想等待 Method 从服务器返回后再跳转可以把重定向放进 Method 的回调中Template.App_body.events({ click .js-new-list() { lists.insert.call((err, listId) { if (!err) { FlowRouter.go(Lists.show, { _id: listId }); } }); } });同时在用户点击按钮到重定向完成之间应给出某种操作进行中的指示若 Method 返回错误也别忘了给出反馈。高级路由按路由动态加载模块动态导入Dynamic Import的原理与配置动态导入dynamic imports最早在Meteor 1.5引入。该技术能显著缩小客户端的 bundle 体积模块及其依赖按需加载——在本场景下是按当前 URI即当前路由加载。假设我们有index.html和index.js其中包含index模板的代码且这是应用中唯一依赖体积庞大的moment包的地方。这意味着moment在应用其他部分并不需要塞进初始 bundle 只会浪费带宽、拖慢加载。!-- /imports/client/index.html -- template nameindex h1Current time is: {{time}}/h1 /template// /imports/client/index.js import moment from moment; import { Template } from meteor/templating; import ./index.html; Template.index.helpers({ time() { return moment().format(LTS); } });// /imports/lib/routes.js import { FlowRouter } from meteor/ostrio:flow-router-extra; FlowRouter.route(/, { name: index, waitOn() { // 等待 index.js 通过网络加载完成 return import(/imports/client/index.js); }, action() { BlazeLayout.render(App_body, {main: index}); } });这里的waitOn是 Flow Router Extra 相对原版 Flow Router 的关键扩展它让路由在进入action之前等待异步资源就绪订阅或动态 import 的 Promise从而避免路由已匹配但模块尚未加载的空白渲染。从 Meteor 仓库源码看import()的底层由 packages/dynamic-import 包实现。其 README 说明该包实现了Module.prototype.dynamicImport(id)运行时 API用于从服务器按需取回模块使这些模块不必包含在初始 JavaScript bundle 中任何已取回的模块版本都会被永久缓存即使关闭窗口或重启浏览器同一客户端也无需再次取回。其核心机制是编译期把import(...)编译为module.dynamicImport(...)运行时 client.js 定义Module.prototype.dynamicImport function (id) { return module.prefetch(id).then(...) }先prefetch缺失模块再求值命名空间缺失模块通过一次POST请求批量取回client.js 的 fetchMissing服务器端在 server.js 中提供middleware处理OPTIONS/POST并设置Access-Control-Allow-Origin: *允许跨源获取同时用 40 位随机密钥randomId(40)见 server.js校验平台归属。几个值得注意的实现细节IndexedDB 缓存cache.js 中canUseCache仅在客户端、非 Cordova、且Meteor.isProduction时开启——开发模式下禁用缓存以避免混淆生产环境则是透明优化缓存数据库名为MeteorDynamicImportCache以版本号为 key 存储模块源码cache.js并通过延迟 100ms 的flushSetMany批量写入避免拖慢首次动态导入cache.js。版本树dynamic-versions.js 中__DYNAMIC_VERSIONS__是构建期由tools/isobuild/bundler.js注入的全部动态模块的哈希树客户端据此判断哪些模块已是最新、无需重新取回。可配置项client.js 读取Meteor.settings.public.packages[dynamic-import]支持useLocationOrigin用location.origin拼接取回地址避免跨源预检延迟见 client.js与disableLocationOriginIframeiframe 环境下禁用前者。404 页面路由级与数据级用户输入错误 URL 时通常应展示某种有趣的 not-found 页面。这里其实有两类 not-found第一类URL 不匹配任何路由定义。用FlowRouter.notFound处理// App_notFound 模板同时用于未知路由和缺失的列表 FlowRouter.notFound { action() { BlazeLayout.render(App_body, {main: App_notFound}); } };第二类URL 合法匹配了某个路由但匹配不到任何数据。此时路由匹配成功但订阅完成后发现没有数据。通常合理的做法是让页面组件负责订阅与取数的组件渲染 not-found 模板而不是常规页面模板template nameLists_show_page {{#each listId in listIdArray}} {{ Lists_show (listArgs listId)}} {{else}} {{ App_notFound}} {{/each}} /template分析与路由了解应用哪些页面最常被访问、用户从何处而来是很常见的需求。基于 Flow Router 的分析方案配置详见 部署指南中的监控用户一节典型做法是安装okgrow:analytics包把 Google Analytics 等提供商的 key 通过Meteor.settings传入该包会自动挂钩 Flow Router为你记录所有页面事件——无需在路由中手动埋点。服务端路由如前所述Meteor 是客户端渲染框架但这并不总能消除对服务器渲染路由的需求。服务端路由主要有三种应用场景。1. API 访问的服务端路由。Meteor 允许通过底层 connect 处理器WebApp构建任意 API如果只是想把 Methods 和 Publications 暴露成 RESTful 接口通常用simple:rest包即可参见数据加载指南与 Methods 文章。如果需要更强的控制力可以用功能全面的nimble:restivus包按你需要的本体论ontology自由创建接口。2. 服务端渲染SSR。Blaze 不支持服务端渲染因此用 Blaze 无法在服务器渲染页面而 React 支持。这意味着用 React 作为渲染框架时可以在服务器端渲染 HTML。Flow Router 大体上也可以像上文 Blaze 那样渲染 React 组件但截至撰写本文时Flow Router 的 SSR 支持仍是实验性的——不过在当前 Meteor 生态下若要做 SSR它仍可能是最合适的方案。3. 附加资源的服务端路由。有时你可能要在服务器提供额外资源或接收 webhook。若涉及更复杂的动态 URL 部分可实现 Picker——一个支持动态路由的简易服务端路由器。而如果要在提供 PDF、XLSX 等附加服务端资源时验证用户身份可以使用mhagmajer:server-router包轻松完成。小结围绕 URL 驱动的客户端渲染应用本文完整覆盖了 Meteor 路由实战的完整链路定位URL 是可链接的客户端状态序列化而非服务器入口定义用ostrio:flow-router-extra的FlowRouter.route匹配路径参数与查询串通过name命名路由读取用响应式 APIgetRouteName/getParam/getQueryParam与内置ActiveRoute读取当前路由、高亮导航渲染用kadira:blaze-layout的BlazeLayout.render配合Template.dynamic切换页面页面组件负责收集路由信息、订阅取数并传给可复用子组件授权等渲染逻辑收敛在组件层级导航用pathFor生成链接用FlowRouter.go/setParams/setQueryParams编程式跳转用EJSON序列化任意数据进 URL重定向静态重定向写在action内动态重定向交给渲染在布局中的重定向组件用户操作后可按乐观或回调方式跳转进阶用waitOn 动态import()按路由按需加载模块底层由仓库中的 packages/dynamic-import 包以Module.prototype.dynamicImport IndexedDB 缓存实现区分路由级与数据级 404并在需要时引入服务端路由。建议在动手实现时将FlowRouter的访问限制在页面/布局组件的顶端、把路由相关与渲染相关逻辑解耦、善用waitOn与动态导入控制首屏 bundle 体积这套模式即可支撑从中小应用到大型 Meteor 项目的一致路由体验。【免费下载链接】meteorMeteor, the JavaScript App Platform项目地址: https://gitcode.com/gh_mirrors/me/meteor创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表