ホームページ > 記事 > ウェブフロントエンド > Vue.js 開発実践:絶妙な無限ロードとページング機能の実現_html/css_WEB-ITnose
この記事は Vue.js のチュートリアルであり、一般的なビジネス シナリオであるページ分割と無限ロードを使用して、読者が Vue.js の設計アイデアをより深く理解できるようにすることを目的としています。 Todo List の多くの入門チュートリアルと比較して、Vue.js を使用して要件を完了するための思考プロセスをより包括的に示しています。大規模なアプリケーションの構築に関するいくつかの高度なチュートリアルと比較して、いくつかの断片的な詳細の実装に重点を置いています。読者にとってはより便利です。すぐにマスターして使用できます。
ページ内の情報量が多すぎる場合 (たとえば、ニュース リストに表示する必要のあるニュース項目が 200 件ある場合)、問題が発生します。
データ量が多すぎるため、読み込み速度に影響します
ユーザーエクスペリエンスが低く、以前に読んだ記事を見つけるのが難しい
200 項目が 2000 以上になるとスケーラビリティが低下する
したがって、一般的な解決策は次のとおりです。最後にデータをロードするか、ページ単位で表示します。無限ロードの実装プロセスは次のようになります。
データを取得する Ajax クラス メソッド
データはローカル配列に格納されます
配列内の各データは HTML テンプレート フラグメントに挿入されます
HTML フラグメントをノードに追加します
フロントエンド ページング 実装プロセスは次のようなものです:
データを取得するための ajax クラス メソッド
データはローカル配列を置き換えます
配列内の各データは HTML テンプレート フラグメントに挿入されます
ノードをクリアした後、HTML フラグメントをノードに追加します
頻繁に変更される または、コードを保守するときに、HTML のレンダリングやパーツの挿入が煩わしいと感じることもあります。 HTML を文字列に結合し、対応する位置にデータを挿入する必要があるため、これは非常に長い文字列になることが多く、後からクラスを追加するのが困難だからです。 es6 のテンプレート文字列はこの状況を改善しましたが、まだ欠陥があります (たとえば、実際の記述中に HTML コードをハイライト表示できないなど)。同時に、配列をループするために多くの for または forEach を記述し、このコード スニペットに複雑な相互作用がある場合は、イベント プロキシを介して多数のメソッドをバインドする必要がある場合もあります。
この種のビジネスを完了するときに上記の問題に遭遇した場合は、Vue がとても素晴らしいことに気づくでしょう。Vue を使ってみましょう!
新しいプロジェクトを作成するには vue-cli を使用することを強くお勧めします。
最初は、node.js と npm を使用して多くのライブラリをインストールすると、よくわからないディレクトリや設定ファイルが生成され、次のような多数の eslint プロンプトが表示されると思うかもしれません。コードを書いたらすぐに。しかし、お金を払う価値は間違いなくあります。なぜなら、このようなテンプレートは、ファイルを整理するための Vue.js の考え方をよりよく理解するのに役立ちます。また、適応すると、これらのルールや規制によって開発効率が大幅に向上することがわかります。
このチュートリアルでは、loadmore という新しいプロジェクトを作成しました。具体的な新しいプロジェクトのプロセスについては、公式 Web サイトのチュートリアルのインストールセクションを参照してください。
チュートリアルの段階的な深化に協力するために、まず Load more 機能を完了します。後続のページングと一貫性を保つために、私のページは 2 つの部分で構成されるように準備されています。1 つは情報リストで、もう 1 つは下部にある「さらに読み込む」ボタンです。両方とも App.vue のルート コンポーネントに配置します。 。
<template> <div id="app"> <list></list> <a class="button" @click="next" >GO NEXT</a> </div></template><script>import List from './components/List'export default { components: { List }, data () { return { ... } }, methods: { next () { ... } }}</script><style scoped> .button { display: block; width: 100%; background: #212121; color: #fff; font-weight: bold; text-align: center; padding: 1em; cursor: pointer; text-decoration: none; } .button span { margin-left: 2em; font-size: .5rem; color: #d6d6d6; }</style>
このプロセスでは、Vue の設計アイデアに基づいて次のアイデアがあります:
情報リストで、上で述べたことを完了します いくつかのステップこれらの手順は、情報リスト自体にのみ関連します。[次へ] ボタンとの唯一の関係は、[次へ] をクリックした後に情報リストを取得するためにトリガーする必要があり、これは props を介して渡すことができることです。そこで、リストとその独自のビジネス ロジックとスタイルを List.vue コンポーネントに配置します。
ボタンの基本的なスタイルをいくつか定義しましたが、使用する CSS セレクターは .button クラス名であり、他のコンポーネントの .button スタイルとは異なる場合があります。 競合そこで、App.vue のスタイルがこのコンポーネント内でのみ機能するように、スコープ付き属性を追加しました。
注: スコープ付きは CSS の優先順位に影響しません。スコープ付きを使用しても、外部スタイル シートによってオーバーライドされないという意味ではありません。
reset.css などの基本的なスタイルをいくつか紹介します。プロジェクトで sass などの言語が使用されている場合、対応する外部 sass ファイルをアセット フォルダーに配置し、インポートを通じて導入できます。通常の CSS は、スコープ属性を使用せずにコンポーネントに直接記述することができますが、このスタイル シートが頻繁に変更されないことが確実な場合は、サードパーティの静的リソースとしてindex.html に導入することもできます。たとえば、この例では、
<link rel="stylesheet" href="./static/reset.css">
効果:
を追加しました。目前我们主要的业务逻辑都是围绕信息列表展开的,也就是我们创建的List.vue。首先,我们需要获取目标数据,我选用了cnodejs.org社区的API作为例子进行编写。如果你也想用一个封装好的ajax库的话,应该这么做:
将目标JS库文件放在static文件夹中,例如我选择的是reqwest.js,然后在index.html先引入。
<script src="./static/reqwest.min.js"></script>
然后在build配置文件夹中,修改webpack.base.conf.js,export externals属性:
externals: { 'reqwest': 'reqwest'}
这样我们在我们的项目中,就可以随时加载第三方库了。
import reqwest from 'reqwest'
在这个例子中,我们只需要调用文章列表这一个接口,但是实际项目中,可能你需要调用很多接口,而这些接口又会在多个组件中被用到。那么调用接口的逻辑四散在各个组件中肯定是不好的,想象一下对方的url发生了变化,你就得在无数个组件中一个个检查是否要修改。
所以我在src文件夹中新建了一个api文件夹,用于存放各类API接口。当前例子中,要获取的是新闻列表,所以新建一个news.js文件:
import reqwest from 'reqwest'const domain = 'https://cnodejs.org/api/v1/topics'export default { getList (data, callback) { reqwest({ url: domain, data: data }) .then(val => callback(null, val)) .catch(e => callback(e)) }}
这样我们就拥有了一个获取新闻列表的API:getList。
我们用一个c34106e0b4e09414b63b2ea253ff83d6作为新闻列表,内部的每一个25edfb22a4f469ecb59f1190150159c6就是一条新闻,其中包括标题、时间和作者3个信息。
在data中,我们用一个名为list的数组来储存新闻列表的数据,一开始当然是空的。我们再在data中设置一个名为limit的值,用来控制每页加载多少条数据,作为参数传给getList这个API。
因此我们的template部分是这样的(加入了一些style美化样式):
<template> <ol> <li v-for="news of list"> <p class="title">{{ news.title }}</p> <p class="date">{{ news.create_at }}</p> <p class="author">By: {{ news.author.loginname }}</p> </li> </ol></template><style scoped> ol { margin-left: 2rem; list-style: outside decimal; } li { line-height: 1.5; padding: 1rem; border-bottom: 1px solid #b6b6b6; } .title { font-weight: bold; font-size: 1.3rem; } .date { font-size: .8rem; color: #d6d6d6; }</style>
之后我们显然需要使用getList来获取数据,不过先想想我们会在哪几个地方使用呢?首先,我们需要在组件开始渲染时自动获取一次列表,填充基础内容。其次,我们在每次点击APP.vue中的Next按钮时也需要获取新的列表。
所以我们在methods中定义一个get方法,成功获取到数据后,就把获取的数组拼接到当前list数组后,从而实现了加载更多。
沿着这个思路,再想想get方法需要的参数,一个是包含了page和limit两个属性的对象,另一个是回调函数。回调函数我们已经说过,只需要拼接数组即可,因此只剩下最后一个page参数还没设置。
在初始化的时候,page的值应该为1,默认是第一页内容。之后page的值只由Next按钮改变,所以我们让page通过props获取App.vue中传来的page值。
最后则是补充get方法触发的条件。一是在组件的生命周期函数created中调用this.get()获取初始内容,另一是在page值变化时对应获取,所以我们watch了page属性,当其变化时,调用this.get()。
最后List.vue的script长这样:
<script>import news from '../api/news'export default { data () { return { list: [], limit: 10 } }, props: { page: { type: Number, default: 1 } }, created () { this.get() }, watch: { page (val) { this.get() } }, methods: { get () { news.getList({ page: this.page, limit: this.limit }, (err, list) => { if (err) { console.log(err) } else { list.data.forEach((data) => { const d = new Date(data.create_at) data.create_at = `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()}` }) this.list = this.list.concat(list.data) } }) } }}</script>
同时我们将App.vue中的4309a73696dbaeac0ddd115cebb6f9b7修改为:
<list :page="page"></list>
再为page在App.vue中添加一个初始值以及对应的方法next:
data () { return { page: 1 }},methods: { next () { this.page++ }}
这样我们就已经完成了加载更多的功能。
因为之前我们的思路非常清晰,代码结构也很明了,所以改写起来会非常简单,只需要将List.vue中拼接数组改为赋值数组就可以了:
// 常规loadmore// this.list = this.list.concat(list.data)// 分页this.list = list.data
就这么简单的一行就完成了功能的改变,这就是Vue.js中核心的数据驱动视图的威力。当然,接下来我们还要做点更cooooool的。
因为分页替换了原来的数组,所以仅仅一个Next按钮不够用了,我们还需要一个Previous按钮返回上一页。同样的,也给Previous按钮绑定一个previous方法,除了用this.page--改变page的值以外,还需要对this.page === 1的边界条件进行一个判断。
同时为了方便知道我们当前的页数,在按钮中,加入{{ page }}显示页数。
<a class="button" @click="next" >GO NEXT<span>CURRENT:{{page}}</span></a>
编写和完善功能的过程中,已经充分体现了Vue.js清晰和便利的一面,接下来继续看看其它好用的功能,首先就是transition动画。
为了展示transition的威力,首先我找到了一个模仿的对象:lavalamp.js( Demo地址 )。
在Demo中可以看到页面以一种非常优雅的动画过渡完成了切换内容的过程,其本身是用JQuery+CSS动画完成的,我准备用Vue.js进行改写。
首先学习了一下原作者的实现思路以后,发现是将一个div作为loader,position设定为fixed。当翻页时,根据点击的按钮不同,loader从顶部或者底部扩展高度,达到100%。数据加载完毕后,再折叠高度,最终隐藏。
那么初步的思路如下:
添加一个loader,最小高度与按钮一致,背景同为黑色,让过渡显得更自然。
loader高度需要达到一个屏幕的高度,所以设置html和body的height为100%。
需要有一个值,作为loader是否显示的依据,我定为finish,其默认值值为true,通过给loader添加v-show="!finish"来控制其显示。
在next和previous方法中添加this.finish = false触发loader的显示。
在App.vue和List.vue建立一个双向的props属性绑定至finish,当List.vue中的get方法执行完毕后,通过props将App.vue中的finish设定为true,隐藏loader。
给loader添加一个transition。由于动画分为顶部展开和底部展开两种,所以使用动态的transition为其指定正确的transition名称。
新增一个值up,用于判断动画从哪个方向开始,其默认值为false。在previous方法中,执行this.up = true,反之在next方法中,则执行this.up = false。
根据思路,写出的loader应该是这样的(style等样式设定在最后统一展示):
<div id="loader" v-show="!finish" :transition="up? 'up-start':'down-start'"> <span>Loading</span></div>
可以看到我设定了up-start和down-start两种transition方式,对应的css动画代码如下:
.down-start-transition { bottom: 0; height: 100%; } .down-start-enter { animation: expand .5s 1 cubic-bezier(0, 1, 0, 1) both; } .down-start-leave { animation: collapse .5s 1 cubic-bezier(0, 1, 0, 1) both; top: 0; bottom: auto; } .up-start-transition { top: 0; height: 100%; } .up-start-enter { animation: expand .5s 1 cubic-bezier(0, 1, 0, 1) both; } .up-start-leave { animation: collapse .5s 1 cubic-bezier(0, 1, 0, 1) both; top: auto; bottom: 0; } @keyframes expand { 0% { height: 3em; transform: translate3d(0, 0, 0); } 100% { height: 100%; transform: translate3d(0, 0, 0); } } @keyframes collapse { 0% { height: 100%; transform: translate3d(0, 0, 0); } 100% { height: 3em; transform: translate3d(0, 0, 0); } }
设置了expand和collapse两个animation,再在transition的各个生命周期钩子中做对应的绑定,就达到了和lavalamp.js相接近的效果。
为了保证动画能执行完整,在List.vue的get方法执行完之后,还使用了一个setTimeout定时器让finish延时0.5秒变为true。
动画效果完成之后,实际使用时发现lavalamp.js还有个巧妙地设计,就是点击Previous后,页面前往底部,反之点击Next后则前往顶部。
实现后者并不复杂,在next方法中加入以下一行代码调整位置即可:
document.body.scrollTop = 0
previous前往底部则略微复杂一点,因为获取到数据之后,页面高度会发生改变,如果在previous中执行scrollTop的改变,有可能会出现新的内容填充后高度变长,页面不到底的情况。所以我watch了finish的值,仅当点击按钮为previous且finish变化为false至true时前往底部,代码如下:
watch: { finish (val, oldVal) { if (!oldVal && val && this.up) { document.body.scrollTop = document.body.scrollHeight } }}
完成以上内容之后,发现不论翻到第几页,一旦刷新,就会回到第一页。vue-router就是为解决这类问题而生的。
首先我们引入VueRouter,方式可以参考上文中的“引入第三方JS库”。然后在main.js对路由规则进行一些配置。
我们的思路包括:
我们需要在url上反映出当前所处的页数。
url中的页数应该与所有组件中的page值保持一致。
点击Next和Previous按钮要跳转到对应的url去。
在这个例子中我们没有router-view。
因此main.js的配置如下:
import Vue from 'vue'import App from './App'import VueRouter from 'VueRouter'Vue.use(VueRouter)const router = new VueRouter()router.map({ '/page/:pageNum': { name: 'page', component: {} }})router.redirect({ '/': '/page/1'})router.beforeEach((transition) => { if (transition.to.path !== '/page/0') { transition.next() } else { transition.abort() }})router.start(App, 'app')
首先定义了一个名为page的具名路径。之后将所有目标路径为'/',也就是初始页的请求,重定向到'/page/1'上保证一致性。最后再在每次路由执行之前做一个判断,如果到了'/page/0'这样的非法路径上,就不执行transition.next()。
根据之前的思路,在App.vue中,获取路由对象的参数值,赋值给page。同时给两个按钮添加对应的v-link。