├── .browserslistrc ├── .prettierrc ├── babel.config.js ├── assets └── img │ ├── demo.png │ ├── h5.png │ ├── pc.png │ ├── qr.png │ ├── header.gif │ ├── component.jpg │ └── content.gif ├── public ├── favicon.ico └── index.html ├── examples ├── assets │ ├── loading.gif │ └── refresh.gif ├── store.js ├── router.js ├── main.js ├── utils │ ├── api.js │ └── item-factory.js ├── components │ ├── Refresher.vue │ ├── SingleList.vue │ ├── Waterfall.vue │ ├── Recommended.vue │ └── Item.vue └── App.vue ├── .editorconfig ├── vue.config.js ├── store └── flow.js ├── .eslintrc.js ├── deploy.sh ├── plugins └── import.js ├── nuxt.config.js ├── layouts └── default.vue ├── LICENSE ├── package.json ├── .gitignore ├── README.md └── pages └── index.vue /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /assets/img/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/falstack/vue-hybrid-best-practices/HEAD/assets/img/demo.png -------------------------------------------------------------------------------- /assets/img/h5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/falstack/vue-hybrid-best-practices/HEAD/assets/img/h5.png -------------------------------------------------------------------------------- /assets/img/pc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/falstack/vue-hybrid-best-practices/HEAD/assets/img/pc.png -------------------------------------------------------------------------------- /assets/img/qr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/falstack/vue-hybrid-best-practices/HEAD/assets/img/qr.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/falstack/vue-hybrid-best-practices/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /assets/img/header.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/falstack/vue-hybrid-best-practices/HEAD/assets/img/header.gif -------------------------------------------------------------------------------- /assets/img/component.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/falstack/vue-hybrid-best-practices/HEAD/assets/img/component.jpg -------------------------------------------------------------------------------- /assets/img/content.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/falstack/vue-hybrid-best-practices/HEAD/assets/img/content.gif -------------------------------------------------------------------------------- /examples/assets/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/falstack/vue-hybrid-best-practices/HEAD/examples/assets/loading.gif -------------------------------------------------------------------------------- /examples/assets/refresh.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/falstack/vue-hybrid-best-practices/HEAD/examples/assets/refresh.gif -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | productionSourceMap: false, 3 | pages: { 4 | index: { 5 | entry: 'examples/main.js', 6 | template: 'public/index.html', 7 | filename: 'index.html' 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/store.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import VueMixinStore from 'vue-mixin-store' 4 | import * as api from './utils/api' 5 | 6 | const flow = VueMixinStore.FlowStore(api) 7 | 8 | Vue.use(Vuex) 9 | 10 | export default new Vuex.Store({ 11 | modules: { 12 | flow 13 | } 14 | }) 15 | -------------------------------------------------------------------------------- /examples/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | 4 | Vue.use(VueRouter) 5 | 6 | export default new VueRouter({ 7 | mode: 'history', 8 | routes: [ 9 | { 10 | path: '/', 11 | name: 'index', 12 | component: () => import('../pages/index') 13 | } 14 | ] 15 | }) 16 | -------------------------------------------------------------------------------- /store/flow.js: -------------------------------------------------------------------------------- 1 | import VueMixinStore from 'vue-mixin-store' 2 | import * as api from '../examples/utils/api' 3 | 4 | const flow = VueMixinStore.FlowStore(api) 5 | 6 | export const state = flow.state 7 | 8 | export const mutations = flow.mutations 9 | 10 | export const actions = flow.actions 11 | 12 | export const getters = flow.getters 13 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | 'extends': [ 7 | 'plugin:vue/essential', 8 | '@vue/standard' 9 | ], 10 | rules: { 11 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 12 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' 13 | }, 14 | parserOptions: { 15 | parser: 'babel-eslint' 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 当发生错误时中止脚本 4 | set -e 5 | 6 | # 构建 7 | npm run generate 8 | 9 | # cd 到构建输出的目录下 10 | cd dist 11 | 12 | git init 13 | git add -A 14 | git commit -m 'update dependencies' 15 | 16 | # 部署到 https://.github.io/ 17 | git push -f git@github.com:falstack/vue-hybird-best-practices.git master:gh-pages 18 | 19 | cd - 20 | git add -A 21 | git commit -m 'update' 22 | git push origin master 23 | -------------------------------------------------------------------------------- /plugins/import.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueMixinStore from 'vue-mixin-store' 3 | import VueFlowRender from 'vue-flow-render' 4 | import VScroller from 'h5-vue-scroller' 5 | import VSwitcher from 'v-switcher' 6 | import 'v-switcher/dist/v-switcher.css' 7 | import 'normalize.css' 8 | 9 | Vue.component(VSwitcher.name, VSwitcher) 10 | Vue.component(VScroller.name, VScroller) 11 | Vue.component(VueFlowRender.name, VueFlowRender) 12 | Vue.component(VueMixinStore.FlowLoader.name, VueMixinStore.FlowLoader) 13 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | vue-hybird-best-practices 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import store from './store' 5 | import VueMixinStore from 'vue-mixin-store' 6 | import VueFlowRender from 'vue-flow-render' 7 | import VScroller from 'h5-vue-scroller' 8 | import VSwitcher from 'v-switcher' 9 | import 'v-switcher/dist/v-switcher.css' 10 | import 'normalize.css' 11 | 12 | Vue.component(VSwitcher.name, VSwitcher) 13 | Vue.component(VScroller.name, VScroller) 14 | Vue.component(VueFlowRender.name, VueFlowRender) 15 | Vue.component(VueMixinStore.FlowLoader.name, VueMixinStore.FlowLoader) 16 | 17 | // eslint-disable-next-line no-new 18 | new Vue({ 19 | el: '#app', 20 | router, 21 | store, 22 | render: h => h(App) 23 | }) 24 | -------------------------------------------------------------------------------- /nuxt.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mode: 'spa', 3 | 4 | head: { 5 | meta: [ 6 | { charset: 'utf-8' }, 7 | { 8 | name: 'viewport', 9 | content: 'width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover' 10 | }, 11 | { 12 | name: 'format-detection', 13 | content: 'telephone=no,email=no,address=no' 14 | }, 15 | { 16 | name: 'applicable-device', 17 | content: 'mobile' 18 | }, 19 | { name: 'renderer', content: 'webkit|ie-comp|ie-stand' }, 20 | { name: 'force-rendering', content: 'webkit' }, 21 | { 'http-equiv': 'X-UA-Compatible', content: 'IE=edge,chrome=1' } 22 | ], 23 | }, 24 | 25 | router: { 26 | base: '/vue-hybird-best-practices/' 27 | }, 28 | 29 | plugins: [ 30 | '~plugins/import.js' 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /examples/utils/api.js: -------------------------------------------------------------------------------- 1 | import ItemFactory from './item-factory' 2 | 3 | export const getListByPage = ({ page, count }) => { 4 | return new Promise(resolve => { 5 | const total = 1024 6 | const hasFetch = (page - 1) * count 7 | const getLength = total - hasFetch >= count ? count : total - hasFetch 8 | setTimeout(() => { 9 | const result = ItemFactory.get(getLength) 10 | resolve({ 11 | result, 12 | no_more: getLength + hasFetch >= total, 13 | total 14 | }) 15 | }, 500) 16 | }) 17 | } 18 | 19 | export const getCarousel = () => { 20 | return new Promise(resolve => { 21 | setTimeout(() => { 22 | const result = ItemFactory.get(5) 23 | resolve(result) 24 | }, 500) 25 | }) 26 | } 27 | 28 | export const getRecommended = () => { 29 | return new Promise(resolve => { 30 | setTimeout(() => { 31 | const result = ItemFactory.get(10) 32 | resolve(result) 33 | }, 500) 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /layouts/default.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 45 | 46 | 51 | -------------------------------------------------------------------------------- /examples/components/Refresher.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 21 | 22 | 54 | -------------------------------------------------------------------------------- /examples/App.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 40 | 41 | 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013-present, Yuxi (Evan) You 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /examples/utils/item-factory.js: -------------------------------------------------------------------------------- 1 | const faker = require('faker') 2 | 3 | let GLOBAL_ID = 0 4 | 5 | export default new class { 6 | get (count) { 7 | let items = []; let i 8 | for (i = 0; i < count; i++) { 9 | items[i] = { 10 | id: ++GLOBAL_ID, 11 | background: this.getRandomColor(), 12 | poster: `${faker.image.animals()}?id=${faker.random.number()}`, 13 | width: 100, 14 | height: 100 + ~~(Math.random() * 50), 15 | words: faker.lorem.sentence(), 16 | user: { 17 | id: faker.random.number(), 18 | avatar: `${faker.image.animals()}?id=${faker.random.number()}`, 19 | nickname: faker.name.findName() 20 | }, 21 | meta: { 22 | play: faker.random.number(), 23 | mark: faker.random.number(), 24 | like: faker.random.number() 25 | } 26 | } 27 | } 28 | return count === 1 ? items[0] : items 29 | } 30 | 31 | getRandomColor () { 32 | var colors = [ 33 | 'rgba(21,174,103,.5)', 34 | 'rgba(245,163,59,.5)', 35 | 'rgba(255,230,135,.5)', 36 | 'rgba(194,217,78,.5)', 37 | 'rgba(195,123,177,.5)', 38 | 'rgba(125,205,244,.5)' 39 | ] 40 | return colors[~~(Math.random() * colors.length)] 41 | } 42 | }() 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-hybird-best-practices", 3 | "version": "0.1.1", 4 | "author": "falstack ", 5 | "license": "MIT", 6 | "bugs": { 7 | "url": "https://github.com/falstack/vue-hybird-best-practices/issues" 8 | }, 9 | "homepage": "https://github.com/falstack/vue-hybird-best-practices", 10 | "scripts": { 11 | "dev": "vue-cli-service serve", 12 | "build": "vue-cli-service build", 13 | "lint": "eslint --ext .js,.vue --fix examples", 14 | "generate": "nuxt generate", 15 | "deploy": "bash deploy.sh" 16 | }, 17 | "dependencies": { 18 | "h5-vue-scroller": "^1.0.2", 19 | "normalize.css": "^8.0.1", 20 | "v-switcher": "^1.0.80", 21 | "vue": "^2.6.11", 22 | "vue-flow-render": "^1.0.5", 23 | "vue-mixin-store": "^1.1.71", 24 | "vue-router": "^3.1.3", 25 | "vuex": "^3.1.2" 26 | }, 27 | "devDependencies": { 28 | "@vue/cli-plugin-babel": "^4.1.1", 29 | "@vue/cli-plugin-eslint": "^4.1.1", 30 | "@vue/cli-service": "^4.1.1", 31 | "@vue/eslint-config-standard": "^5.0.1", 32 | "babel-eslint": "^10.0.3", 33 | "core-js": "3.5.0", 34 | "eslint": "^6.7.2", 35 | "eslint-plugin-vue": "^6.0.1", 36 | "faker": "^4.1.0", 37 | "node-sass": "^4.13.0", 38 | "nuxt": "^2.11.0", 39 | "sass": "^1.23.7", 40 | "sass-loader": "^8.0.0", 41 | "vue-template-compiler": "^2.6.11" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/components/SingleList.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 32 | 33 | 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # parcel-bundler cache (https://parceljs.org/) 63 | .cache 64 | 65 | # next.js build output 66 | .next 67 | 68 | # nuxt.js build output 69 | .nuxt 70 | 71 | # Nuxt generate 72 | dist 73 | 74 | # vuepress build output 75 | .vuepress/dist 76 | 77 | # Serverless directories 78 | .serverless 79 | 80 | # IDE 81 | .idea 82 | .DS_Store 83 | .env.js 84 | -------------------------------------------------------------------------------- /examples/components/Waterfall.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 26 | 27 | 84 | -------------------------------------------------------------------------------- /examples/components/Recommended.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 65 | 66 | 89 | -------------------------------------------------------------------------------- /examples/components/Item.vue: -------------------------------------------------------------------------------- 1 | 66 | 67 | 84 | 85 | 113 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vue.js 在复杂信息流场景下的最佳实践 2 | 3 | 经常做业务的前端同学肯定遇到过这样的业务场景: 4 | 5 | ##### 常见的 hybird 页面 6 | 7 | 8 | ##### 常见的 UGC 类的 PC 网页 9 | 10 | 11 | 这类页面都会**承载着多个信息流列表**,本文就针对这类复杂信息流页面进行梳理,给出我在做了无数次这类页面后的最佳实践总结。 12 | 13 | 你可以直接戳这个地址查看最终 [demo 效果](https://falstack.github.io/vue-hybrid-best-practices/)(请在手机模式下浏览) 14 | 15 | 16 | 17 | demo 的代码仓库:[vue-hybird-best-practices](https://github.com/falstack/vue-hybird-best-practices) 18 | 19 | ### 第一步:我们需要一个 Tab 组件作为承载信息流的容器 20 | 21 | 这个 tab 组件至少要满足以下两个场景: 22 | 23 | ##### 切换 Tab 时的渐变动画 24 | 25 | 26 | ##### 滑动时的手指跟随 27 | 28 | 29 | 因为这些 feature 能够让我们的页面效果尽可能的接近原生,因此专门把它抽象成一个组件: 30 | 31 | [v-switcher:一个强大的 Tab 组件](https://github.com/falstack/v-switcher) 32 | 33 | 我们注意到页面顶部的这个图片轮播组件与 Tab 组件的区别仅仅是:「能否自动滑动」。因此我们在 v-switcher 里增加了几行代码来兼容图片轮播功能。 34 | 35 | > v-switcher 的实现还兼容了很多场景,可以查看 github 的 readme 来了解。 36 | 37 | ### 第二步:我们需要一个方便的状态管理工具来存储信息流的数据和状态 38 | 39 | 什么意思呢?仔细思考一下对于每一个信息流列表,它都会有以下这几个状态: 40 | 41 | - loading(加载中) 42 | - nothing(列表为空) 43 | - error(列表加载出错) 44 | - fetched(已经向服务端发起过请求的列表) 45 | - noMore(列表已加载完,没有更多了) 46 | 47 | 除此之外我们还要根据列表的长度在第一屏展示特殊的 loading(通常为骨架屏)或 error (通常为大图),而在非第一屏的情况下展示其它 UI 样式。 48 | 49 | 当我们点击 Tab 切换到另一个列表的时候,上一个列表的状态要正确的维持,下一个列表和上一个列表的状态要分离开,如果还需要下拉刷新、筛选排序等,如果不抽象一个数据层出来,那这个代码是真的难看,相信经常做这类页面的同学都深有体会。 50 | 51 | 为了对信息流的数据和状态进行一个完美的管理,我们又提供了另一个组件: 52 | 53 | [vue-mixin-store:一个专门做列表状态管理的组件](https://github.com/falstack/vue-mixin-store) 54 | 55 | `vue-mixin-store`是依赖于 vuex 的,它提供了很多的 API 让列表的 CURD 变的非常方便,通过它你可以让绝大多数信息流的开发都变成复制粘贴,文档:[vue-mixin-store](https://falstack.github.io/vue-mixin-store/) 56 | 57 | ### 第三步:我们需要对信息流列表做惰性渲染 58 | 59 | > 所谓惰性渲染,就是指在列表里只保留视口内的 DOM,视口之外的 DOM 不展示,在列表滚动的时候,我们通过 JS 来计算要渲染哪些 DOM,达到优化内存的目的。 60 | 61 | 一开始做这个优化,我是想使用 [vue-virtual-scroll-list](https://github.com/tangbc/vue-virtual-scroll-list) 这个库的,但这个库有几个地方无法满足我的需求: 62 | 63 | 1. 无法兼容 [better-scroll](https://github.com/ustbhuangyi/better-scroll) 64 | 2. 仅支持单列的列表,无法支持瀑布流 65 | 66 | 因此这个库虽然很好,但对我来说它的应用场景“受限”,所以没办法只好自己写了一个: 67 | 68 | [vue-flow-render:一个列表惰性加载组件](https://github.com/falstack/vue-flow-render) 69 | 70 | 其实和 vue-virtual-scroll-list 对比区别不是很大,只是我将“滚动”的**行为**从组件中剥离出去,只保留了滚动的**结果**,因此使用该组件的时候它需要一个父容器来分发`scroll`事件。 71 | 72 | 支持瀑布流也只是在计算 DOM 位置的时候根据 props 做了不同的 case 处理,都很简单。 73 | 74 | 但它可以应用在更广的场景中。 75 | 76 | -------- 77 | 78 | > 至此我们的页面无论是代码整洁度还是性能都有了一定的保障(`v-switcher`针对性能也做了很多优化、`vue-flow-render`也是尽可能通过少的计算量来实现的惰性加载) 79 | 80 | 于是我们就开心的写完代码提交给了测试的同事验收,但发现好像还有点问题??? 81 | 82 | 具体是什么问题呢,相信经常做移动端开发的同学都有遇到过这个问题:[-webkit-overflow-scrolling: touch 导致的页面锁死](https://www.baidu.com/s?wd=-webkit-overflow-scrolling%20%E9%A1%B5%E9%9D%A2%E9%94%81%E6%AD%BB&rsv_spt=1&rsv_iqid=0xe9a920ca001bb6af&issp=1&f=8&rsv_bp=1&rsv_idx=2&ie=utf-8&tn=baiduhome_pg&rsv_enter=1&rsv_dl=ib&rsv_sug3=26&rsv_sug1=12&rsv_sug7=101&rsv_t=ed46ZgkjtBjoinygD4sGRFYA%2Bv68%2BIUztl6hHQNBqFqChsyY5kDTEkawll8DTU%2FK9Rq0) 83 | 84 | 遇到这个问题真是难受,网上的解决方法五花八门都达不到效果。 85 | 86 | 因为毕竟做了很久的前端开发,所以我对这个 bug 是早就知道的,因此在一开始寻找惰性加载组件时就希望能够与 better-scroll 搭配使用,因为 better-scroll 是可以解决页面锁死问题的。 87 | 88 | 于是我们愉快的使用起了 better-scroll 又提测了。 89 | 90 | 然后发现又有点不对: 91 | 92 | better-scroll 在 Android 设备上的体验,真的很不好,特别是当页面里的数据量很大,并且有很多个 tab(需要很多个 better-scroll 实例)的时候。 93 | 94 | 而且在其他项目中尝试使用 better-scroll 会在复杂场景下(路由切换 + 异步请求等)会导致一些机型(iPhoneX)的偶现 bug。 95 | 96 | 因此在测试同学的强烈要求下,我们也无法使用 better-scroll 了。 97 | 98 | 但这个问题还是得解决啊,于是我们在寻找专门解决页面锁死的库,终于是找到了一个:[iNoBounce](https://github.com/lazd/iNoBounce),尝试了一下真的解决了问题。 99 | 100 | 但怎么说呢,这个库用起来并不怎么舒服,把它加到我们的最佳实践里好像还缺了一些分量,我们需要加强它。 101 | 102 | ### 第四步:我们需要一个专门处理滚动行为的组件 103 | 104 | 需要这个组件的原因是: 105 | 106 | 1. 我们需要解决 iOS 页面锁死的问题 107 | 2. 我们需要分发 scroll 事件给`vue-flow-render` 108 | 3. 我们有更多的场景需要`onTop`、`onBottom`、`onRefresh`等事件 109 | 110 | 因此我们提供了另一个组件: 111 | 112 | [h5-vue-scroller:一个处理 iOS 页面锁死和分发滚动事件的组件](https://github.com/falstack/h5-vue-scroller) 113 | 114 | 至此,我们的「Vue.js 在复杂信息流场景下的最佳实践」所需要的组件都已经给出来了,接下来就简单讲一下为什么要这么做。 115 | 116 | -------- 117 | 118 | ### 需求是千遍万化的,一个组件无法解决所有问题 119 | 120 | 虽然这篇文章介绍了四个组件搭配的结果,但并不代表着你需要在所有地方都同时使用它们。 121 | 122 | 在其他场景的最佳实践下会需要其他组件,这其中有一点是可以肯定的:我们的代码不能杂糅起来,必须要很好的分层,再去组件化。 123 | 124 | 我相信很多学习 Vue.js 的同学都看过下面这种图: 125 | 126 | ##### 组件化开发: 127 | 128 | 129 | 但在复杂场景下,又有多少人能把代码合理的抽象与组件化呢? 130 | 131 | `vue-mixin-store`的实现初衷就是因为我之前有一个同事列表从来不写 loading,也不做异常处理,页面代码里到处都是 DOM 操作,我实在是受不了了才写了这么个组件出来。 132 | 133 | 希望这篇文章能让你的开发变得简单,感谢阅读(记得 star 和遇到问题提 issue 给我 :D) 134 | 135 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 148 | 149 | 222 | 223 | 304 | --------------------------------------------------------------------------------