路由组件
路由器组件是一种特殊类型的内容,当我们使用component或componentUrl属性指定路由内容时,可以由路由器加载。
它应该有助于更好地组织我们的应用程序,将事物放在适当的位置,并以更清晰、更舒适的方式更快地完成许多事情。
Component Function
组件是一个接收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>
`;
}
Component Template
如上所述,组件的渲染函数应该返回一个带有组件HTML内容的标记模板字面量。有几个重要的事项需要注意。
所有自闭合标签必须关闭!如果你没有关闭自闭合标签,比如<br>,<img src="">,<input ...>
,编译器会报错。
所有空元素可以自闭合。:
<div class="my-div"></div>
<!-- also valid as -->
<div class="my-div" />
Component Props
第一个接收组件函数的参数是props。这个对象将包含您在导航方法中传递的所有props,以及所有的路由参数。
例如,如果我们有以下路由:
{
path: '/blog/:id',
component: MyComponent
}
当我们通过/blog/34/ URL导航到路由时,props.id将等于'34'。
当我们使用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>
then $props
will be:
{
foo: 'bar',
id: '25',
user: {
name: 'John'
},
number: 30
}
Component Context
context对象包含许多有用的辅助函数:
属性 | 描述 |
---|---|
$h | 特殊的标记模板字面量,必须用于包装组件渲染函数的结果以及所有HTML条目内部:
|
$el | 对象的.value属性包含具有组件HTML元素的Dom7实例。
|
$ | Dom7 库:
|
$f7 | Framework7 app 实例
|
$store | Store 实例. Check Store 文档 获取更多详情和实例. |
$f7route | 当前路由对象,包括如下键值 route query , hash , params , path and url |
$f7router | 关联的路由实例,可以操作路由事件
|
$theme | 具有
|
$update(callback) | 这个方法告诉我们需要使用更新后的状态重新渲染这个组件及其子组件。
不能保证DOM更改会立即生效,因此如果您依赖DOM(例如,在状态更改后需要获取HTML内容或属性值),请将“callback”函数作为参数传递。 |
$ref(初始化) | 这种方法创建了一个响应式的“变量”,在更新后会自动更新组件,无需调用 它返回一个带有
不能保证DOM更改会立即生效,因此如果您依赖DOM(例如,在状态更改后需要获取HTML内容或属性值),请将“回调”函数作为参数传递。 |
$useState(初始值) | 这种方法创建了响应式的 "state".
For
For
For
例如:
|
$tick(callback) | 你也可以使用这种方法,如果你依赖于DOM,并且需要确保在调用$update()方法你也可以使用这种方法,如果你依赖于DOM,并且需要确保在调用$update()方法后组件状态和DOM已经更新。 传递的回调函数将在DOM更新后执行。 这个方法返回一个Promise,在DOM更新后也会被解析 所以你可以这样使用它:
|
$f7ready(callback) | 这种方法只有在使用主应用组件时才需要使用,以确保在应用初始化时调用Framework7的API。
|
事件 | |
$on | 将DOM事件处理程序附加到组件根元素的功能
当组件销毁时,这样的事件处理程序将自动解除绑定。 |
$once | 函数用于将DOM事件处理程序附加到组件根元素。与$on相同,但这些处理程序只会执行一次。 |
$emit(事件, 数据) | 在可重用的自定义组件中发出自定义DOM事件的功能:
在其他父组件中:
|
声明周期的hooks | |
$onBeforeMount | 在组件添加到DOM之前调用 |
$onMounted | 在组件被添加到DOM后立即调用
|
$onBeforeUpdate | 在更新虚拟 DOM(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>
`;
},
},
// ...
]
Component Page Events
组件页面事件处理程序可以通过 $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 Events Handling
请注意组件模板中的额外@属性。这是一种将事件监听器分配给指定元素的简写方法。指定的事件处理程序将在组件范围内进行搜索。
这样的事件处理程序属性值必须是一个函数:
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>
`
}
事件处理程序仅在初始渲染时或对使用虚拟DOM进行修补的元素上进行处理。如果您手动将此类元素添加到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>
`
}
Component Root Element
组件模板或渲染函数必须只返回单个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>
Single File Component
在同一个路由数组中指定所有组件路由并不是很方便,特别是当我们有很多这样的路由时。这就是为什么我们可以使用componentUrl
,将组件放入单个文件中的原因。
routes = [
...
{
path: '/some-page/',
componentUrl: './some-page.f7',
},
..
];
And in 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>
标签的内容。
Usage With Webpack and 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,
}
]
Virtual DOM
Virtual 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-renderd
$update();
});
})
return $render;
}
</script>
请注意,直接将值赋给组件状态不会触发布局更新。在需要更新组件布局时,请使用 $update
!
Keys in Lists & Auto-Init Components
列表中的键和自动初始化组件
当 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
属性)不会起作用。
Main App Component
可以将整个应用的布局作为一个组件。
请注意,由于 VDOM 的实现,强烈建议为每个自动初始化的视图(具有 view-init
类的视图)添加唯一的 id
或 key
属性:
为了启用它,首先,我们应该在 index.html
中保持应用根元素为空。
<body>
<!-- empty app root element -->
<div id="app"></div>
</body>
Then we need to create main app component, for example, Single File Component using 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>
Finally, when we init Framework7, we need to specify app component on init:
// import main app component
import App from './path/to/app.f7';
var app = new Framework7({
// specify main app component
component: App,
})
Or, if we don't use webpack, we can also load it via XHR:
var app = new Framework7({
// load main app component
componentUrl: './path/to/app.f7',
})
Also note that main app component will be mounted (added to DOM) BEFORE app initialization process finished. So if you need to call Framework7 APIs immediately, use $f7ready
callback:
<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>
Custom Components
Register Components
可以创建自定义的可重用组件。我们需要在Framework7初始化之前使用以下方法来完成:
Framework7.registerComponent(tagName, component)- register custom component
- tagName - string. Component tag name, e.g.
my-component
(will be used as<my-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中可用。
Local Components
可以在组件中创建本地自定义组件:
<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>
With 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>
)
}
Events
您可以在模板中使用相同的@{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>
Slots
如果我们需要将子元素(或文本)传递给自定义组件,我们需要使用插槽(slots)。这里的插槽实现类似于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>
And we can use it like following:
<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>
And component result output will be:
<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>
Template Recepies
Conditional Rendering
在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>
Same using 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>
Same using 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>
)
}
Mapping Array To Elements
要将数组映射到元素,我们使用数组的.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>
Same using JSX:
export default () => {
const items = [
'item 1',
'item 2',
'item 3',
];
return () => (
<div class="page">
<ul>
{items.map((item) => (
<li>{item}</li>
))}
</ul>
</div>
)
}