路由器组件
路由组件是一种特殊的内容类型,当我们使用 component
或 componentUrl
属性指定路由内容时,路由可以加载它。
它应该有助于更好地构建我们的应用程序,将内容保存在适当的位置,并以更清晰、更舒适的方式更快地完成许多事情。
组件函数
组件是一个接收 props
和 context
并应返回渲染函数的函数。
组件渲染函数应该返回带有组件 HTML 内容的 标记模板文字。
例如
const MyComponent = (props, context) => {
// some component logic
let value = 'foo';
// return render function
return () => context.$h`
<div class="page">
<p>Value is ${value}</p>
</div>
`;
}
组件模板
如上所述,组件渲染函数应返回带有组件 HTML 内容的 标记模板文字。它有一些需要注意的重要事项。
所有自闭合标签都必须闭合!。如果您没有关闭自闭合标签,例如 <br>
、<img src="">
、<input ...>
,则编译器将抛出错误。
所有空元素都可以自闭合:
<div class="my-div"></div>
<!-- also valid as -->
<div class="my-div" />
组件属性
组件函数接收的第一个参数是 props
。此对象将包含您将在 navigate 方法中传递的所有 props,以及所有路由参数。
例如,如果我们有以下路线
{
path: '/blog/:id',
component: MyComponent
}
当我们通过 /blog/34/
URL 导航到路由时,它将具有等于 '34'
的 props.id
。
并且当我们使用如下 API 导航到组件时
router.navigate('/blog/34/', {
props: {
foo: 'bar'
}
})
那么 props
将是以下对象:{ id: '34', foo: 'bar' }
此外,props 还将包含作为属性传递给自定义组件的属性。如果自定义组件具有以下属性
<my-component foo="bar" id="25" user=${{name: 'John'}} number=${30}></my-component>
那么 $props
将是
{
foo: 'bar',
id: '25',
user: {
name: 'John'
},
number: 30
}
组件上下文
context
对象包含许多有用的助手
属性 | 描述 |
---|---|
$h | 特殊的 标记模板文字,必须用于包装组件渲染函数结果和内部的所有 HTML 条目
|
$el |
|
$ | Dom7 库
|
$f7 | Framework7 应用程序实例
|
$store | 存储实例。有关更多详细信息和示例,请参阅存储文档。 |
$f7route | 当前路由。包含带有路由 query 、hash 、params 、path 和 url 的对象 |
$f7router | 相关路由器实例
|
$theme | 具有
|
$update(回调) | 此方法表明该组件及其子组件需要使用更新后的状态重新渲染。
不保证 DOM 更改会立即应用,因此如果您依赖 DOM(例如,需要在状态更改后获取 HTML 内容或属性值),请将 |
$ref(initialValue) | 此方法创建反应式“变量”,更新后会自动更新组件,而无需调用 它返回一个带有
不保证 DOM 更改会立即应用,因此如果您依赖 DOM(例如,需要在状态更改后获取 HTML 内容或属性值),请将 |
$useState(initialValue) | 此方法创建反应式“状态”。
对于
对于
对于
例如
|
$tick(callback) | 如果您依赖 DOM 并需要确保在调用 传递的回调将在 DOM 更新时执行。 此方法返回一个 Promise,该 Promise 也将在 DOM 更新时解析。 所以你可以这样使用它
|
$f7ready(callback) | 仅当您使用主应用程序组件以确保在应用程序初始化时调用 Framework7 API 时,才需要使用此方法。
|
事件 | |
$on | 将 DOM 事件处理程序附加到组件根元素的函数
当组件销毁时,此类事件处理程序将自动分离 |
$once | 将 DOM 事件处理程序附加到组件根元素的函数。 与 |
$emit(event, data) | 在可重用的自定义组件中发出自定义 DOM 事件的函数
以及在其他父组件中
|
生命周期钩子 | |
$onBeforeMount | 在组件被添加到 DOM 之前调用 |
$onMounted | 在组件被添加到 DOM 之后立即调用
|
$onBeforeUpdate | 在组件 VDOM 被修补/更新之前立即调用 |
$onUpdated | 在组件 VDOM 被修补/更新之后立即调用 |
$onBeforeUnmount | 在组件被卸载(从 DOM 中分离)之前立即调用 |
$onUnmounted | 在组件被卸载和销毁时调用 |
因此,带有页面组件的示例路由可能如下所示
routes = [
// ...
{
path: '/some-page/',
// Component
component: (props, { $h, $f7, $on }) => {
const title = 'Component Page';
const names = ['John', 'Vladimir', 'Timo'];
const openAlert = () => {
$f7.dialog.alert('Hello world!');
}
$on('pageInit', (e, page) => {
// do something on page init
});
$on('pageAfterOut', (e, page) => {
// page has left the view
});
return () => $h`
<div class="page">
<div class="navbar">
<div class="navbar-bg"></div>
<div class="navbar-inner">
<div class="title">${title}</div>
</div>
</div>
<div class="page-content">
<a @click=${openAlert} class="red-link">Open Alert</a>
<div class="list simple-list">
<ul>
${names.map((name) => $h`
<li>${name}</li>
`)}
</ul>
</div>
</div>
</div>
`;
},
},
// ...
]
组件页面事件
组件页面事件处理程序可以在 $on
组件事件处理程序中传递。它们是常见的 DOM 页面事件。因为它们是 DOM 事件,所以它们接受 event
作为第一个参数,页面数据 作为第二个参数。它们与普通 DOM 事件的唯一区别是事件处理程序名称必须以驼峰式格式指定(page:init
-> pageInit
)。
const MyComponent = (props, { $on }) => {
$on('pageMounted', (e, page) => {
console.log('page mounted');
});
$on('pageInit', (e, page) => {
console.log('page init');
});
$on('pageBeforeIn', (e, page) => {
console.log('page before in');
});
$on('pageAfterIn', (e, page) => {
console.log('page after in');
});
$on('pageBeforeOut', (e, page) => {
console.log('page before out');
});
$on('pageAfterOut', (e, page) => {
console.log('page after out');
});
$on('pageBeforeUnmount', (e, page) => {
console.log('page before unmount');
});
$on('pageBeforeRemove', (e, page) => {
console.log('page before remove');
});
}
DOM 事件处理
请注意组件模板中的附加 @
属性。这是一种将事件监听器分配给指定元素的简写方法。将在组件作用域中搜索指定的事件处理程序。
此类事件处理程序属性值必须是一个函数。
const MyComponent = (props, { $h, $update }) => {
let value = 10;
const addValue = (number) => {
value += number;
$update();
}
const onClick = () => {
console.log('click');
}
return () => $h`
<div class="page">
<!-- pass function to attribute -->
<button @click=${onClick}>Button</button>
<!-- also work -->
<button @click=${() => onClick()}>Button</button>
<!-- will not work, attribute value "onClick" is just a string -->
<button @click="onClick">Button</button>
<!-- passing dynamic data will work as expected -->
<button @click=${() => addValue(15)}>Button</button>
</div>
`
}
事件处理程序仅在初始渲染时或针对使用 VDOM 修补的元素进行处理。如果您手动将此类元素添加到 DOM,它将不起作用!
const MyComponent = (props, { $h, $on }) => {
const onClick = () => {
console.log('click');
}
$on('pageInit', (e, page) => {
// this won't work
page.$el.append('<a @click="onClick">Link</a>');
});
return () => $h`
<div class="page">
</div>
`
}
组件根元素
组件模板或渲染函数必须只返回单个 HTML 元素。并且它必须是路由器支持的元素。
如果您将页面作为路由器组件加载,则路由器组件必须返回页面元素。
<template> <div class="page"> ... </div> </template>
如果您将模态框(可路由模态框)作为路由器组件加载,则路由器组件必须返回该模态框元素。
<template> <div class="popup"> ... </div> </template>
如果您将面板(可路由面板)作为路由器组件加载,则路由器组件必须返回面板元素。
<template> <div class="panel panel-left panel-cover"> ... </div> </template>
如果您将选项卡内容(可路由选项卡)作为路由器组件加载,则路由器组件必须返回将插入可路由选项卡内部的选项卡子元素。
<template> <div class="some-element"> ... </div> </template>
单文件组件
在同一个路由数组下指定所有组件路由不是很方便,尤其是在我们有很多这样的路由的情况下。这就是为什么我们可以使用 componentUrl
并将组件放入单个文件中的原因。
routes = [
...
{
path: '/some-page/',
componentUrl: './some-page.f7',
},
..
];
在 some-page.f7
中
<!-- component template, uses same tagged template literals -->
<template>
<div class="page">
<div class="navbar">
<div class="navbar-bg"></div>
<div class="navbar-inner">
<div class="title">${title}</div>
</div>
</div>
<div class="page-content">
<a @click=${openAlert}>Open Alert</a>
<div class="list simple-list">
<ul>
${names.map((name) => $h`
<li>${name}</li>
`)}
</ul>
</div>
</div>
</div>
</template>
<!-- component styles -->
<style>
.red-link {
color: red;
}
</style>
<!-- rest of component logic -->
<script>
// script must return/export component function
export default (props, { $f7, $on }) => {
const title = 'Component Page';
const names = ['John', 'Vladimir', 'Timo'];
const openAlert = () => {
$f7.dialog.alert('Hello world!');
}
$on('pageInit', () => {
// do something on page init
});
$on('pageAfterOut', () => {
// page has left the view
});
// component function must return render function
return $render;
}
</script>
嗯,现在它更干净了。<template>
和 <style>
标签将自动转换为导出组件的相同属性。
必须在组件函数的末尾使用 return $render
,因为它将被解析器替换为 <template>
标签的内容。
与 Webpack 和 Vite 一起使用
对于 Webpack,有一个特殊的 framework7-loader 插件,它允许将单文件组件捆绑到主包中,而不是使用 XHR(例如 componentUrl
)每次都加载和解析组件文件。
对于 Vite.js,也有一个特殊的 rollup-plugin-framework7 插件来捆绑单文件组件。
这些插件在捆绑过程中解析单文件组件的文件并将其转换为普通的 JS 对象。因此,它可能会提高应用程序性能,因为不会有运行时解析和编译。
配置插件后,我们需要将单文件组件存储在 .f7
(或 Webpack 中的 .f7.html
)文件中,并使用 export default
导出组件。
<template>
<div class="page">
...
</div>
</template>
<script>
export default () => {
let foo = 'bar';
const doThis = () => {
// ...
}
return $render;
}
</script>
也可以导入所需的依赖项和样式。
<template>
<div class="page">
...
</div>
</template>
<script>
import './path/to/some-styles.css';
import utils from './path/to/utils.js';
export default () => {
let foo = 'bar';
let now = utils.now();
const doThis = () => {
// ...
}
return $render;
}
</script>
然后我们可以导入它并将其添加到路由中。
// routes.js
import NewsPage from './path/to/news.f7';
import ServicesPage from './path/to/services.f7';
export default [
{
path: '/news/',
component: NewsPage,
},
{
path: '/services/',
component: ServicesPage,
}
]
JSX
模板字面量在 HTML 文档中没有良好的语法高亮显示。但是,当与 webpack 或 Vite 一起使用时,也可以使用 JSX 语法编写组件。
为此,我们需要将组件存储在 .f7.jsx
文件中,并使用 JSX 编写它们。
export default (props, { $update }) => {
let value = 10;
const items = ['Item 1', 'Item 2'];
const addValue = (number) => {
value += number;
$update();
}
//- render function should returns JSX
return () => (
<div class="page">
<p>The value is {value}</p>
<p>
{/* JSX doesn't support @ in attribute name so event handlers should start from "on" */}
<button onClick={() => addValue(10)}>Add Value</button>
</p>
<ul>
{items.map((item) => (
<li>{item}</li>
))}
</ul>
</div>
)
}
并在 routes.js
中以相同的方式导入它们。
import NewsPage from './path/to/news.f7.jsx';
import ServicesPage from './path/to/services.f7.jsx';
export default [
{
path: '/news/',
component: NewsPage,
},
{
path: '/services/',
component: ServicesPage,
}
]
虚拟 DOM
虚拟 DOM 和所有与 VDOM 相关的功能从 Framework7 3.1.0 版本开始可用。
虚拟 DOM (VDOM) 是一种编程概念,其中 UI 的理想或“虚拟”表示保存在内存中,并与“真实”DOM 同步。它允许我们将应用程序的视图表示为其状态的函数。
VDOM 库称为 Snabbdom,因为它非常轻量级、快速,并且非常适合 Framework7 环境。
那么 Framework7 路由器组件 VDOM 渲染是如何工作的呢?组件模板被转换为 VDOM,而不是直接插入到 DOM 中。之后,当组件状态发生变化时,它会创建新的 VDOM 并将其与之前的 VDOM 进行比较。然后,它会根据差异来修补真实的 DOM,只更改需要更改的元素和属性。所有这一切都是自动发生的!
让我们看一下用户资料组件示例,它将在我们请求用户数据时自动更新布局。
<template>
<div class="page">
<div class="navbar">
<div class="navbar-bg"></div>
<div class="navbar-inner">
<div class="title">Profile</div>
</div>
</div>
<div class="page-content">
${user && $h`
<!-- Show user list when it is loaded -->
<div class="list simple-list">
<ul>
<li>First Name: ${user.firstName}</li>
<li>Last Name: ${user.lastName}</li>
<li>Age: ${user.age}</li>
</ul>
</div>
`}
${!user && $h`
<!-- Otherwise show preloader -->
<div class="block block-strong text-align-center">
<div class="preloader"></div>
</div>
`}
</div>
</div>
</template>
<script>
export default (props, { $on, $f7, $update }) => {
// empty initial user data
let user = null;
$on('pageInit', () => {
// request user data on page init
fetch('https://api.website.com/get-user-profile')
.then((res) => res.json())
.then((data) => {
// update user with new data
user = data;
// trigger re-render
$update();
});
})
return $render;
}
</script>
请注意,直接赋值给组件状态不会触发布局更新。每当您需要更新组件布局时,请使用 $update
!
列表和自动初始化组件中的键
当 VDOM 更新元素列表时,默认情况下它使用“就地修补”策略。如果数据项的顺序发生了变化,它不会移动 DOM 元素以匹配项的顺序,而是会就地修补每个元素,并确保它反映了应该在该特定索引处呈现的内容。
这种默认模式很有效,但只适合于当您的渲染输出不依赖于子组件状态或临时 DOM 状态(例如表单输入值)时。
要向 VDOM 提供提示,以便它能够跟踪每个节点的身份,从而重用和重新排序现有元素,您需要为每个项提供唯一的 key
属性。
渲染列表时,key
的理想值是每个项的唯一 ID。
<template>
...
<ul>
${items.map((item) => $h`
<li key=${item.id}>...</li>
`)}
</ul>
...
</template>
<script>
export default () => {
const items = [
{
id: 1,
title: 'Item A'
},
{
id: 2,
title: 'Item B'
},
];
return $render;
}
</script>
与自动初始化的组件(如 范围滑块、仪表 和其他应该在添加到 DOM 时自动初始化(如果它们具有 range-slider-init
、gauge-init
)并在从 DOM 中删除时自动销毁的组件)相同。因此,此类元素也必须使用唯一键进行标识。
<template>
<div class="page">
...
<div class="page-content">
${gaugeVisible && $h`
<!-- must have unique key -->
<div key="gauge" class="gauge gauge-init" data-type="circle"
data-value="0.60"
data-value-text="60%"
data-value-text-color="#ff9800"
data-border-color="#ff9800"
></div>
`}
...
<a href="#" class="button" @click=${showGauge}>Show Gauge</a>
</div>
</div>
</template>
<script>
export default (props, { $update }) => {
let gaugeVisible = false;
const showGauge = () => {
gaugeVisible = true;
$update();
}
return $render;
}
</script>
- 注意,
key
属性在单个组件中必须是唯一的。 - 如果未指定
key
属性且元素具有id
属性,则id
属性将用作虚拟节点唯一键。
innerHTML
如果我们需要插入 HTML 字符串(例如,从 API 端点接收的字符串),我们需要使用特殊的 innerHTML
元素属性。
<template>
<div class="page">
...
<div class="block" innerHTML=${customHTML}></div>
</div>
</template>
<script>
export default (props) => {
const customHTML = '<p>Hello <b>World!</b></p>';
return $render;
}
</script>
在元素上使用 innerHTML
将覆盖其所有子元素。
在 innerHTML
中传递的 HTML 内容只是一个字符串,例如,组件事件处理程序(如 @click
属性)将不起作用。
主应用程序组件
可以将整个应用程序布局制作成一个组件。
请注意,由于 VDOM 的实现,强烈建议为每个自动初始化的视图(具有 view-init
类的视图)添加唯一的 id
或 key
属性。
要启用它,首先,我们应该在 index.html
中保留应用程序根元素为空。
<body>
<!-- empty app root element -->
<div id="app"></div>
</body>
然后我们需要创建主应用程序组件,例如,使用 Vite 的单文件组件。
<!-- app.f7 -->
<template>
<div id="app">
${loggedIn.value && $h`
<div class="panel panel-left panel-reveal panel-init">
<!-- every View has unique ID attribute -->
<div class="view view-init" id="view-panel" data-url="/panel/"></div>
</div>
<div class="view view-main view-init" id="view-main" data-url="/"></div>
`}
${!loggedIn.value && $h`
<div class="login-screen modal-in">
<div class="view view-init" id="view-auth" data-url="/auth/"></div>
</div>
`}
</div>
</template>
<script>
export default (props, { $store }) => {
const loggedIn = $store.getters.loggedIn;
return $render;
}
</script>
最后,当我们初始化 Framework7 时,我们需要在初始化时指定应用程序组件。
// import main app component
import App from './path/to/app.f7';
var app = new Framework7({
// specify main app component
component: App,
})
或者,如果我们不使用 webpack,我们也可以通过 XHR 加载它。
var app = new Framework7({
// load main app component
componentUrl: './path/to/app.f7',
})
另请注意,主应用程序组件将在应用程序初始化过程完成之前挂载(添加到 DOM)。因此,如果您需要立即调用 Framework7 API,请使用 $f7ready
回调。
<template>
<div id="app">
...
</div>
</template>
<script>
export default (props, { $f7ready, $f7 }) => {
$f7ready(() => {
// now it is safe to call Framework7 APIs
$f7.dialog.alert('Hello!');
})
}
</script>
自定义组件
注册组件
可以创建自定义可复用组件。我们需要在 Framework7 初始化之前使用以下方法执行此操作:
Framework7.registerComponent(tagName, component)- 注册自定义组件
- tagName - string。组件标签名称,例如
my-component
(将用作<my-component>
)。自定义组件标签名称必须包含连字符/短划线字符“
-
”。 - component - object 或 class。组件函数。
请注意,目前,只能在路由器组件(由路由器加载的组件)中使用自定义组件。
Framework7.registerComponent(
// component name
'my-list-item',
// component function
(props, { $h }) => {
let foo = 'bar';
return () => $h`
<li class="item-content" id="${props.id}">...</li>
`
}
)
并在其他组件中使用它,如:
<div class="list">
<ul>
<my-list-item id="item-1"></my-list-item>
</ul>
</div>
请注意,传递给自定义组件元素的属性在组件 props
中可用。
局部组件
可以在组件中创建局部自定义组件。
<template>
<ul>
<!-- use tag names as variables -->
<${ListItem} title="Item 1" />
<${ListItem} title="Item 2" />
<${ListItem} title="Item 3" />
</ul>
</template>
<script>
// create local component
const ListItem = (props, { $h }) => {
return () => $h`<li>${props.title}</li>`;
}
// export main component
export default () => {
return $render;
}
</script>
或者可以导入它们。
<template>
<ul>
<!-- use tag names as variables -->
<${ListItem} title="Item 1" />
<${ListItem} title="Item 2" />
<${ListItem} title="Item 3" />
</ul>
</template>
<script>
// import component
import ListItem from 'path/to/list-item.f7';
// export main component
export default () => {
return $render;
}
</script>
使用 JSX
const ListItem = (props) => {
return (
<li>{props.title}</li>
)
}
/* or
import ListItem from 'path/to/list-item.f7.jsx'
*/
export default () => {
return () => (
<ul>
<ListItem title="Item 1" />
<ListItem title="Item 2" />
<ListItem title="Item 3" />
</ul>
)
}
在 JSX 中,它可以在主组件内部创建。
export default () => {
const ListItem = (props) => {
return (
<li>{props.title}</li>
)
}
return () => (
<ul>
<ListItem title="Item 1" />
<ListItem title="Item 2" />
<ListItem title="Item 3" />
</ul>
)
}
事件
您可以使用相同的 @{event}
语法为模板中的自定义组件分配 DOM 事件。事件处理程序实际上将附加到自定义组件的根元素。
<template>
<div class="page">
...
<my-button @click="onClick">Click Me</my-button>
</div>
</template>
<script>
return {
// ...
methods: {
onClick: function(e) {
console.log('clicked');
}
},
// ...
}
</script>
插槽
如果我们需要将子元素(或文本)传递给自定义组件,我们需要使用插槽。这里的插槽实现类似于 Web Components 插槽。
我们使用 slot
标签来指定组件子元素应该放置的位置。例如 my-button
组件模板
<a class="button button-fill">
<slot></slot>
</a>
然后可以像这样使用
<my-button>Click Me</my-button>
要指定插槽的默认值(当没有传入子元素时),我们只需将其放在 <slot>
标签内
<a class="button button-fill">
<slot>Default Button Text</slot>
</a>
要将元素分布在组件布局中,我们可以使用命名插槽。例如,my-container
组件的模板
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
我们可以像下面这样使用它
<my-container>
<h1 slot="header">Title</h1>
<p>Text for main content.</p>
<p>More text for main content.</p>
<p slot="footer">Footer content</p>
</my-container>
组件的输出结果将是
<div class="container">
<header>
<h1>Title</h1>
</header>
<main>
<p>Text for main content.</p>
<p>More text for main content.</p>
</main>
<footer>
<p>Footer content</p>
</footer>
</div>
模板技巧
条件渲染
要在 JavaScript 中实现条件,我们通常使用 if
(if-else
) 语句。在模板和 JSX 中,我们不能直接使用它们,而应该使用 JavaScript 运算符。
if
对于 if
语句,我们应该使用逻辑与 (&&
) 运算符
<template>
<div class="page">
${someVar && $h`
<p>Text will be visible when "someVar" is truthy</p>
`}
${someVar === 1 && $h`
<p>Text will be visible when "someVar" equals to 1</p>
`}
</div>
</template>
<script>
export default () => {
const someVar = 1;
return $render;
}
</script>
使用 JSX 也一样
export default () => {
const someVar = 1;
return () => (
<div class="page">
{someVar && (
<p>Text will be visible when "someVar" is truthy</p>
)}
{someVar === 1 && (
<p>Text will be visible when "someVar" equals to 1</p>
)}
</div>
)
}
if-else
对于 if-else
,我们可以使用三元运算符 (?:
) 或 &&
和 !
运算符的组合
<template>
<div class="page">
${someVar ? $h`
<p>Text will be visible when "someVar" is truthy</p>
` : $h`
<p>Text will be visible when "someVar" is falsy</p>
`}
{someVar && (
<p>Text will be visible when "someVar" is truthy</p>
)}
{!someVar && (
<p>Text will be visible when "someVar" is falsy</p>
)}
</div>
</template>
<script>
export default () => {
const someVar = 1;
return $render;
}
</script>
使用 JSX 也一样
export default () => {
const someVar = 1;
return () => (
<div class="page">
{someVar ? (
<p>Text will be visible when "someVar" is truthy</p>
) : (
<p>Text will be visible when "someVar" is falsy</p>
)}
{someVar && (
<p>Text will be visible when "someVar" is truthy</p>
)}
{!someVar && (
<p>Text will be visible when "someVar" is falsy</p>
)}
</div>
)
}
将数组映射到元素
要将数组映射到元素,我们使用数组的 .map()
方法
<template>
<div class="page">
<ul>
${items.map((item) => $h`
<li>${item}</li>
`)}
</ul>
</div>
</template>
<script>
export default () => {
const items = [
'item 1',
'item 2',
'item 3',
];
return $render;
}
</script>
使用 JSX 也一样
export default () => {
const items = [
'item 1',
'item 2',
'item 3',
];
return () => (
<div class="page">
<ul>
{items.map((item) => (
<li>{item}</li>
))}
</ul>
</div>
)
}