路由组件
路由组件是一种特殊类型的内容,当我们在使用属性指定路由内容时,路由可以在加载时加载它。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
。该对象将包含您在导航方法中传递的所有属性,以及所有路由参数。
例如,如果我们有以下路由:
{
path: '/blog/:id',
component: MyComponent
}
当我们通过/blog/34/
URL 导航到路由时,它将具有props.id
等于'34'
.
。此外,当我们使用 API 如此导航到组件时:
router.navigate('/blog/34/', {
props: {
foo: 'bar'
}
})
然后props
将是以下对象:{ id: '34', foo: 'bar' }
此外,属性将包含传递给自定义组件作为属性的属性。如果自定义组件有这样的属性:
<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 | Store 实例。查看Store 文档获取更多详细信息和示例。 |
$f7route | 当前路由。包含路由对象query , hash , params , path 和url |
$f7router | 相关路由实例
|
$theme | 带有
|
$update(回调) | 此方法表示此组件及其子组件需要使用更新后的状态重新渲染
它不保证 DOM 更改立即应用,因此如果您依赖于 DOM(例如,需要在状态更改后获取 HTML 内容或属性值),则将 |
$ref(initialValue) | 此方法创建响应式“变量”,在更新后自动更新组件,而无需调用 它返回一个对象,其中
它不保证 DOM 更改立即应用,因此如果您依赖于 DOM(例如,需要在状态更改后获取 HTML 内容或属性值),则将 |
$useState(initialValue) | 此方法创建响应式“状态”。
对于
对于
对于
例如:
|
$tick(回调) | 如果您依赖于 DOM 并需要在调用 传递的回调将在 DOM 更新时执行。 此方法返回在 DOM 更新时也会解析的 Promise。 因此,您可以使用它作为:
|
$f7ready(回调) | 只有在使用主应用组件时才需要使用此方法,以确保在应用程序初始化时调用 Framework7 API。
|
事件 | |
$on | 用于将 DOM 事件处理程序附加到组件根元素的函数
这样的事件处理程序将在组件销毁时自动分离 |
$once | 用于将 DOM 事件处理程序附加到组件根元素的函数。与 |
$emit(事件上触发, 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 元素。并且它必须是路由支持的一个元素:
如果你将页面加载为路由组件,则路由组件必须返回Page元素:
<template> <div class="page"> ... </div> </template>
如果你将模态框(可路由模态框)加载为路由组件,则路由组件必须返回该模态框元素:
<template> <div class="popup"> ... </div> </template>
如果你将面板(可路由面板)加载为路由组件,则路由组件必须返回Panel元素:
<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>
在组件函数的末尾必须有
It is mandatory to have 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
虚拟DOM和所有与VDOM相关的功能从Framework7版本3.1.0开始可用。
虚拟DOM(VDOM)是一种编程概念,其中UI的理想或“虚拟”表示形式保存在内存中,并与“真实”DOM同步。它允许我们将应用程序的视图表示为其状态的一个函数。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>
与自动初始化的组件(如范围滑块, 计量表和其他应该在使用时自动初始化(如果它们有range-slider-init
, gauge-init
)的组件)相同,当它们添加到DOM时自动初始化,当它们从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
元素prop/属性:
<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实现,强烈建议为每个自动初始化的视图(具有id
或key
属性:view-init
类的视图)添加唯一的
要启用它,首先,我们应该在index.html
:
<body>
<!-- empty app root element -->
<div id="app"></div>
</body>
中保持应用根元素为空:
<!-- 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>
然后,我们需要创建主应用程序组件,例如使用Vite的单文件组件:
// import main app component
import App from './path/to/app.f7';
var app = new Framework7({
// specify main app component
component: App,
})
最后,在初始化Framework7时,我们需要指定应用程序组件:
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, 组件上传递单独的属性)- 注册自定义组件
- tagName - 字符串. 组件标签名,例如
my-component
(将用作<my-component>
)中自定义组件标签名必须包含连字符/短划线字符 "
-
" - 组件上传递单独的属性 - 对象或类. 组件函数
注意,目前,自定义组件只能在路由组件中使用(由路由加载的组件)。
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>
)
}
事件
您可以在模板中为自定义组件分配 DOM 事件,使用相同的语法。事件处理程序实际上会附加到自定义组件的根元素。@{event}
syntax. Event handler will be actually attached to custom component root element.
<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>
)
}
将数组映射到元素
要将数组映射到元素,我们使用 Array 的.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>
)
}