德飞 發表於 2021-3-16 09:14:00

uni-app全栈仿微信开源项目系列(一)

<h2 id="叮咚项目参考文档-v10">叮咚项目参考文档 v1.0</h2>
<p>项目技术栈:</p>
<p>​前端:uni-app + nvue 实现原生页面渲染、同时兼容多端。</p>
<p>后端:Egg.js + MySQL + Redis 实现后端API服务。</p>
<p>不使用第三方组件库,自己写一套。</p>
<h2 id="nvue需要注意的点">NVUE需要注意的点</h2>
<ol>
<li>
<p>在NVUE中引入字体图标需要参考Weex的引入规则 点我查看</p>
</li>
<li>
<p>NVUE模式下的页面默认是Flex布局</p>
</li>
<li>
<p>iconfont图标应该放在<code>text</code>标签中包裹,不能直接使用<code>view</code>标签包裹</p>
</li>
<li>
<p>NVUE中的屏幕都是以750像素为基准</p>
</li>
<li>
<p>text组件换行问题,text组件中的内容如果有换行,显示的效果也会换行</p>
</li>
<li>
<p>Weex是从上到下进行渲染的,如果你的元素有定位之类的脱离文档流的需求,元素最好按顺序写,否则<code>z-index</code>可能也救不了你。</p>
</li>
<li>
<p>目前仅 iOS 支持 <code>box-shadow</code> 属性,Android 暂不支持,可以使用图片代替。每个元素只支持设置一个阴影效果,不支持多个阴影同时作用于一个元素。</p>
</li>
</ol>
<p><code>uni-app</code>中 vue 和 nvue 的区别:</p>
<p>uni-app是逻辑和渲染分离的。渲染层,在app端提供了两套排版引擎:小程序方式的webview渲染,和weex方式的原生渲染。<br>
两种渲染引擎可以自己根据需要选。vue文件走的webview渲染,nvue走的原生渲染。<br>
组件和js写法是一样的,css不一样,原生排版的能用的css必须是flex布局,这是web的css的子集。当然什么界面都可以用flex布出来。不懂flex布局就自己学。</p>
<p>一般情况下用vue就可以了。如果是app且有部分场景vue页面的性能不满足你的需求时,这个页面可以改用nvue页面。如果app里同时存在同名的vue和nvue页面,在app端会优先执行nvue页面,而其他端仍然优先vue页面。</p>
<p>区别和适用场景这文档里写的很清楚:https://uniapp.dcloud.io/nvue-outline</p>
<h2 id="记录一些踩过的坑">记录一些踩过的坑</h2>
<ol>
<li>【报Bug】2.2.5版本纯nvue的uniapp模式子组件使用插槽报错问题</li>
</ol>
<h2 id="1环境搭建和项目创建">1.环境搭建和项目创建</h2>
<p>需要安装的插件:</p>
<ol>
<li>内置浏览器</li>
<li>App真机运行</li>
<li>uni-app App调试</li>
<li>less编译</li>
<li>scss/sass编译</li>
<li>stylus编译</li>
<li>es6编译</li>
</ol>
<p>创建项目:</p>
<p>项目类型为:uni-app,使用默认模版。</p>
<p>开启原生渲染:</p>
<p>uni-app在App端,支持vue页面和nvue页面混搭、互相跳转。也支持纯nvue原生渲染。</p>
<p>启用纯原生渲染模式,可以减少App端的包体积、减少使用时的内存占用。因为webview渲染模式的相关模块将被移除。</p>
<p>在manifest.json源码视图的<code>"app-plus"</code>下配置<code>"renderer":"native"</code>,即代表App端启用纯原生渲染模式。此时pages.json注册的vue页面将被忽略,vue组件也将被原生渲染引擎来渲染。</p>
<p>如果不指定该值,默认是不启动纯原生渲染的。</p>
<pre><code class="language-json"> // manifest.json   
    {   
         // ...   
      /* App平台特有配置 */   
      "app-plus": {   
            "renderer": "native", //App端纯原生渲染模式
      }   
    }
</code></pre>
<p>使用uni-app编译模式:</p>
<pre><code class="language-json"> // manifest.json   
    {   
         // ...   
      /* App平台特有配置 */   
      "app-plus": {   
            "renderer": "native", //App端纯原生渲染模式
            "nvueCompiler" : "uni-app",
      }   
    }
</code></pre>
<h2 id="2全局配置">2.全局配置</h2>
<h3 id="21-引入全局样式">2.1 引入全局样式</h3>
<p>将封装好的<code>free.css</code>库引入到项目中。</p>
<h3 id="22-引入自定义图标库">2.2 引入自定义图标库</h3>
<p>全局加载自己的字体图标库并且做多端适配:</p>
<pre><code class="language-html">&lt;script&gt;
        export default {
                onLaunch: function() {
                        // #ifdef APP-NVUE
                        // 加载公共图标库 只有在NVUE环境下才加载
                        const domModule = weex.requireModule('dom')
                        domModule.addRule('fontFace', {
                                'fontFamily': "iconfont",
                                src: "url('https://at.alicdn.com/t/font_1365296_2ijcbdrmsg.ttf')"
                        });
                        // #endif
                },
                onShow: function() {
                        console.log('App Show')
                },
                onHide: function() {
                        console.log('App Hide')
                }
        }
&lt;/script&gt;

&lt;style&gt;
        /*每个页面公共css */
        @import url("./common/free.css");
        @import url("./common/common.css");
        /* #ifndef APP-PLUS */
        @import url("./common/free-icon.css");
        /* #endif */
&lt;/style&gt;

</code></pre>
<blockquote>
<p>如果对跨端兼容和条件编译语法不熟悉,可以参考官方文档</p>
</blockquote>
<h3 id="23-配置tabbar底部导航">2.3 配置tabbar底部导航</h3>
<p>修改<code>package.json</code>配置文件、添加<code>tabbar</code>配置。</p>
<p>这里的 tabbar 的 icon 图标大小为 81*81。</p>
<pre><code class="language-json">{
        "pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
                {
                        "path": "pages/tabbar/index/index",
                        "style": {}
                },
                {
                        "path": "pages/tabbar/mail/mail",
                        "style": {}
                },
                {
                        "path": "pages/tabbar/find/find",
                        "style": {}
                },
                {
                        "path": "pages/tabbar/my/my",
                        "style": {}
                }
        ],
        "globalStyle": {
                "navigationBarTextStyle": "black",
                "navigationBarTitleText": "叮咚",
                "navigationBarBackgroundColor": "#F8F8F8",
                "backgroundColor": "#F8F8F8"
        },
        "tabBar": {
                "color": "#000000",
                "selectedColor": "#08C261",
                "borderStyle": "black",
                "backgroundColor": "#F7F7F7",
                "list": [{
                                "iconPath": "static/tabbar/index.png",
                                "selectedIconPath": "static/tabbar/index-select.png",
                                "pagePath": "pages/tabbar/index/index",
                                "text": "首页"
                        },
                        {
                                "iconPath": "static/tabbar/mail.png",
                                "selectedIconPath": "static/tabbar/mail-select.png",
                                "pagePath": "pages/tabbar/mail/mail",
                                "text": "通讯录"
                        },
                        {
                                "iconPath": "static/tabbar/find.png",
                                "selectedIconPath": "static/tabbar/find-select.png",
                                "pagePath": "pages/tabbar/find/find",
                                "text": "发现"
                        },
                        {
                                "iconPath": "static/tabbar/my.png",
                                "selectedIconPath": "static/tabbar/my-select.png",
                                "pagePath": "pages/tabbar/my/my",
                                "text": "我的"
                        }
                ]
        }
}

</code></pre>
<p>配置细节参考官方文档</p>
<h3 id="24-配置globalstyle">2.4 配置globalStyle</h3>
<p>取消APP端下的原生导航栏、滚动条。</p>
<pre><code class="language-json">"globalStyle": {
    "navigationBarTextStyle": "black",
    "navigationBarTitleText": "叮咚",
    "navigationBarBackgroundColor": "#F8F8F8",
    "backgroundColor": "#F8F8F8",
    "app-plus":{
      "titleNView":false,
      "scrollIndicator":"none"
    }
}
</code></pre>
<h2 id="3聊天列表页开发">3.聊天列表页开发</h2>
<h3 id="31-头部导航栏组件开发">3.1 头部导航栏组件开发</h3>
<pre><code class="language-html">&lt;template&gt;
        &lt;view&gt;
                &lt;!-- 导航栏 --&gt;
                &lt;view class="bg-light"&gt;
                        &lt;!-- 状态栏 --&gt;
                        &lt;view :style="'height:'+statusBarHeight+'px'"&gt;&lt;/view&gt;
                        &lt;!-- 导航 --&gt;
                        &lt;view class="w-100 flex align-center justify-between border" style="height: 90rpx"&gt;
                                &lt;!-- 左边标题部分 --&gt;
                                &lt;view class="flex align-center"&gt;
                                        &lt;text class="font-md ml-3"&gt;叮咚(10)&lt;/text&gt;
                                &lt;/view&gt;
                                &lt;!-- 右边图标部分 --&gt;
                                &lt;view class="flex align-center"&gt;
                                        &lt;view class="flex align-center justify-center border" style="height: 90rpx;width: 90rpx;"&gt;
                                                &lt;text class="iconfont font-md"&gt;&amp;#xe6e3;&lt;/text&gt;
                                        &lt;/view&gt;

                                        &lt;view class="flex align-center justify-center border" style="height: 90rpx;width: 90rpx;"&gt;
                                                &lt;text class="iconfont font-md"&gt;&amp;#xe682;&lt;/text&gt;
                                        &lt;/view&gt;
                                &lt;/view&gt;
                        &lt;/view&gt;
                &lt;/view&gt;
        &lt;/view&gt;
&lt;/template&gt;

&lt;script&gt;
        export default {
                data() {
                        return {
                                statusBarHeight: 0 // 状态栏高度
                        }
                },
                onLoad() {
                        this.statusBarHeight = plus.navigator.getStatusbarHeight()
                },
                methods: {

                }
        }
&lt;/script&gt;

&lt;style lang="less"&gt;
&lt;/style&gt;

</code></pre>
<h3 id="32-图标按钮组件封装">3.2 [*]图标按钮组件封装</h3>
<p>这个地方有一个坑:</p>
<blockquote>
<p>NVUE 中如果使用 iconfont 的话就必须使用<code>text</code>标签进行包裹,如果要封装成组件,通过<code>slot</code>动态传递iconfont 的 16 进制值的话就会报错,因为 slot 会转换成<code>text</code>标签,又因为<code>text</code>标签里面不能再次嵌套<code>text</code>标签,所以报错,这个也是近期才发现的,以前看别人写没有问题,怎么解决呢?通过 props 传参。</p>
</blockquote>
<p><code>封装的组件: free-icon-button.vue</code></p>
<pre><code class="language-html">&lt;template&gt;
&lt;view
      class="flex align-center justify-center"
      hover-class="bg-hover-light" @click="$emit('click')"
      style="height: 90rpx;width: 90rpx;"&gt;
    &lt;text class="iconfont font-md"&gt;{{iconValue}}&lt;/text&gt;
    &lt;/view&gt;
&lt;/template&gt;

&lt;script&gt;
    export default {
      name: '',
      components: {},
      props: {
            iconValue: {
                required: true
            }
      },
      data () {
            return {}
      },
      computed: {},
      watch: {},
      created () {
            console.log(this.iconValue)
      },
      mounted () {},
      methods: {}
    }
&lt;/script&gt;

&lt;style scoped lang="less"&gt;&lt;/style&gt;

</code></pre>
<p><code>调用组件的文件index/index.nvue</code></p>
<pre><code class="language-html">&lt;free-icon-button @click="handleIconButtonClick" :iconValue="'\ue682'"/&gt;
</code></pre>
<blockquote>
<p>这里面还有一个细节:</p>
<p>通过props方式传参的话 iconfont 的 16 进制值就不能写成 <code>&amp;#xe682;</code>,必须写成<code>\ue682</code></p>
</blockquote>
<p>这个问题通过查资料 + 反复实践大约耗时 30 分钟,因此记录一下这个坑。</p>
<h3 id="33-封转头部导航组件">3.3 封转头部导航组件</h3>
<blockquote>
<p>uni-app的普通组件中使用onLoad、onShow不生效?,要用created、mounted,为什么?</p>
</blockquote>
<p>这个就要从<code>uni-app</code>的生命周期说起了。。。</p>
<p><code>uni-app</code>有 3 类生命周期:</p>
<ol>
<li>应用生命周期</li>
<li>页面生命周期</li>
<li>组件生命周期</li>
</ol>
<p>应用生命周期:</p>
<p><img src="https://img2020.cnblogs.com/blog/2217722/202105/2217722-20210528014051422-1595480633.png" alt="1615810364593" loading="lazy"></p>
<blockquote>
<p>重点是应用生命周期只能在<code>App.vue</code>中监听,其他页面监听无效,所以不要用错了。</p>
</blockquote>
<p>页面生命周期:</p>
<p><img src="https://img2020.cnblogs.com/blog/2217722/202105/2217722-20210528014051753-1408851910.png" alt="1615810725398" loading="lazy"></p>
<blockquote>
<p>需要注意的是:<code>onLoad</code>和<code>onReady</code>只会触发一次,这是官方没有说的,所以还是要多实践!</p>
</blockquote>
<p>以上是常用的几个,想了解全部的参考官方文档</p>
<p>组件生命周期:</p>
<p><code>uni-app</code> 组件支持的生命周期,与vue标准组件的生命周期相同。这里没有页面级的onLoad等生命周期:</p>
<p><img src="https://img2020.cnblogs.com/blog/2217722/202105/2217722-20210528014052016-2008590515.png" alt="1615811129046" loading="lazy"></p>
<blockquote>
<p>以后编写组件的时候就要细心点,页面组件就用页面的生命周期,普通组件就用组件的生命周期,别乱搞给自己挖坑。</p>
</blockquote>
<p><code>index/index.nvue 页面代码</code></p>
<pre><code class="language-html">&lt;template&gt;
        &lt;view&gt;
                &lt;!-- 导航栏 --&gt;
                &lt;free-nav-bar title titleValue="叮咚(99+)" fixed /&gt;

                &lt;!-- 列表 --&gt;
                &lt;view style="height: 2000rpx;"&gt;
                        &lt;text&gt;好好学习,天天向上!&lt;/text&gt;
                &lt;/view&gt;
        &lt;/view&gt;
&lt;/template&gt;

&lt;script&gt;
        import FreeNavBar from '@/components/free-ui/free-nav-bar.vue'
        export default {
                name: "IndexPage",
                components: {
                        FreeNavBar
                },
                data() {
                        return {}
                },
                methods: {}
        }
&lt;/script&gt;

&lt;style lang="less"&gt;
&lt;/style&gt;

</code></pre>
<p><code>free-nav-bar.vue 组件代码</code></p>
<pre><code class="language-html">&lt;template&gt;
        &lt;view&gt;
                &lt;!-- 导航栏 --&gt;
                &lt;view class="bg-light" :class="fixed?'fixed-top':''"&gt;
                        &lt;!-- 状态栏 --&gt;
                        &lt;view :style="'height:'+statusBarHeight+'px'"&gt;&lt;/view&gt;
                        &lt;!-- 导航 --&gt;
                        &lt;view class="w-100 flex align-center justify-between border" style="height: 90rpx"&gt;
                                &lt;!-- 左边标题部分 --&gt;
                                &lt;view class="flex align-center"&gt;
                                        &lt;text v-if="title" class="font-md ml-3"&gt;{{titleValue}}&lt;/text&gt;
                                &lt;/view&gt;
                                &lt;!-- 右边图标部分 --&gt;
                                &lt;view class="flex align-center"&gt;
                                        &lt;free-icon-button :iconValue="'\ue6e3'" /&gt;
                                        &lt;free-icon-button :iconValue="'\ue682'"/&gt;
                                &lt;/view&gt;
                        &lt;/view&gt;
                &lt;/view&gt;
                &lt;!-- 占位 --&gt;
                &lt;view v-if="fixed" :style="fixedStyle"&gt;&lt;/view&gt;
        &lt;/view&gt;
&lt;/template&gt;

&lt;script&gt;
import FreeIconButton from '@/components/free-ui/free-icon-button.vue'
export default {
name: 'FreeNavBar',
components: {
                FreeIconButton,
        },
props: {
                // 是否显示标题
                title: {
                        type: Boolean,
                        default: false
                },
                // 标题内容
                titleValue: {
                        type: String
                },
                // 是否固定导航栏
                fixed: {
                        type: Boolean,
                        default: true
                }
        },
data () {
    return {
                        navBarHeight: 0,                // 状态栏高度+导航栏高度
                        statusBarHeight: 0 // 状态栏高度
                }
},

computed: {
                fixedStyle() {
                        return `height: ${this.navBarHeight}px`
                }
        },
watch: {},
created () {},
mounted () {
                console.log("API获取:", uni.getSystemInfoSync().statusBarHeight)
                // NVUE环境下获取系统状态栏的高度
                // #ifdef APP-NVUE
                        this.statusBarHeight = plus.navigator.getStatusbarHeight()
                // #endif
                /*
                        这里使用uni.upx2px的原因是因为我们获取的statusBarHeight是px单位,要进行相加
                        需要转换成相同的单位才行.
               */
                this.navBarHeight = this.statusBarHeight + uni.upx2px(90)
        },
methods: {},
}
&lt;/script&gt;

&lt;style scoped lang="less"&gt;
&lt;/style&gt;

</code></pre>
<blockquote>
<p>这里使用计算属性配合动态计算 状态栏 + 导航栏的高度,这个高度给占位的<code>view</code>标签用,防止列表被导航栏覆盖。</p>
</blockquote>
<h3 id="34-开发聊天列表组件">3.4 开发聊天列表组件</h3>
<p><code>index/index.nvue 文件代码</code></p>
<pre><code class="language-html">&lt;template&gt;
        &lt;view&gt;
                &lt;!-- 导航栏 --&gt;
                &lt;free-nav-bar title titleValue="叮咚(99+)" fixed /&gt;

                &lt;!-- 列表 --&gt;
                &lt;view class="flex align-center" v-for="(item, index) in list" :key="index"&gt;
                        &lt;!-- 左侧 --&gt;
                        &lt;view class="flex align-center justify-center" style="width: 145rpx;"&gt;
                                &lt;image :src="item.avatar" style="width: 92rpx;height:92rpx;" mode="widthFix" class="rounded"&gt;&lt;/image&gt;
                        &lt;/view&gt;
                        &lt;!-- 右侧 --&gt;
                        &lt;view class="flex flex-column border-bottom flex-1 py-3 pr-3 border-light-secondary"&gt;
                                &lt;view class="flex align-center justify-between mb-1"&gt;
                                        &lt;text class="font-md"&gt;{{item.nickname}}&lt;/text&gt;
                                        &lt;text class="font-sm text-light-muted"&gt;{{item.update_time | formatTime}}&lt;/text&gt;
                                &lt;/view&gt;
                                &lt;text class="font text-ellipsis text-light-muted"&gt;{{item.data}}&lt;/text&gt;
                        &lt;/view&gt;
                &lt;/view&gt;
        &lt;/view&gt;
&lt;/template&gt;

&lt;script&gt;
        import FreeNavBar from '@/components/free-ui/free-nav-bar.vue'
        import $Time from '@/common/free-lib/time.js'
        export default {
                name: "IndexPage",
                components: {
                        FreeNavBar
                },
                data() {
                        return {
                                list: [{
                                                avatar: "/static/avatar.jpg",
                                                nickname: '老婆',
                                                update_time: Date.now(),
                                                data: '今晚想吃什么都可以...'
                                        },
                                        {
                                                avatar: "/static/avatar.jpg",
                                                nickname: '老婆2',
                                                update_time: Date.now(),
                                                data: '今晚想吃什么都可以...'
                                        },
                                        {
                                                avatar: "/static/avatar.jpg",
                                                nickname: '老婆3',
                                                update_time: Date.now(),
                                                data: '今晚想吃什么都可以...'
                                        },
                                        {
                                                avatar: "/static/avatar.jpg",
                                                nickname: '老婆4',
                                                update_time: Date.now(),
                                                data: '今晚想吃什么都可以...'
                                        },
                                        {
                                                avatar: "/static/avatar.jpg",
                                                nickname: '老婆5',
                                                update_time: Date.now(),
                                                data: '今晚想吃什么都可以...'
                                        }
                                ]
                        }
                },
                methods: {},
                filters: {
                        formatTime(value) {
                                return $Time.gettime(value)
                        }
                }
        }
&lt;/script&gt;

&lt;style lang="less"&gt;
&lt;/style&gt;

</code></pre>
<h3 id="35-封装头像组件">3.5 封装头像组件</h3>
<p><code>free-avatar.vue 文件代码</code></p>
<pre><code class="language-html">&lt;template&gt;
&lt;image
                :src="src"
                mode="widthFix"
                :style="getStyle"
                :class="type"
        &gt;&lt;/image&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
name: 'FreeAvatar',
components: {},
props: {
                // 图像大小
                size: {
                        type: ,
                        default: 90
                },
                // 图像地址
                src: {
                        type: String,
                        default: ""
                },
                // 图像显示类型,是否圆角显示
                type: {
                        type: String,
                        default: "rounded"
                }
        },
data () {
    return {}
},
computed: {
                getStyle () {
                        return `width: ${this.size}rpx;height: ${this.size}rpx;`
                }
        },
watch: {},
created () {},
mounted () {},
methods: {}
}
&lt;/script&gt;

&lt;style scoped lang="less"&gt;&lt;/style&gt;

</code></pre>
<p><code>index/index.vue 文件中使用</code></p>
<pre><code class="language-html">&lt;free-avatar :src="item.avatar" size="92" /&gt;
</code></pre>
<h3 id="36-badge组件封装">3.6 badge组件封装</h3>
<p>封装前的代码:</p>
<p><code>index/index.nvue 角标元素代码</code></p>
<pre><code class="language-html">&lt;!-- 角标 --&gt;
&lt;text class="bg-danger text-white rounded-circle font-sm badge" &gt;9&lt;/text&gt;
</code></pre>
<p><code>角标css代码</code></p>
<pre><code class="language-css">.badge {
    padding-left: 14rpx;
    padding-right: 14rpx;
    padding-bottom: 6rpx;
    padding-top: 6rpx;
    position: absolute;
    right: 15rpx;
    top: 15rpx;
}
</code></pre>
<p>封装后的代码:</p>
<p><code>free-badge.vue</code></p>
<pre><code class="language-html">&lt;template&gt;
&lt;text
                class="bg-danger text-white rounded-circle font-sm free-badge"
                :class="badgeClass"
                :style="badgeStyle"
        &gt;{{value}}&lt;/text&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
name: 'FreeBadge',
components: {},
props: {
                // 角标样式类名
                badgeClass: {
                        type: String,
                        default: ""
                },
                // 角标行内样式
                badgeStyle: {
                        type: String,
                        default: ""
                },
                // 角标内容
                value: {
                        type: ,
                        default: ""
                }
        },
data () {
    return {}
},
computed: {},
watch: {},
created () {},
mounted () {},
methods: {}
}
&lt;/script&gt;

&lt;style scoped lang="less"&gt;
        .free-badge {
                padding: 6rpx 14rpx;
        }
&lt;/style&gt;

</code></pre>
<p>使用<code>badge</code></p>
<pre><code class="language-html">&lt;free-badge value="1" badgeClass="position-absolute" badgeStyle="top: 15rpx;right:15rpx" /&gt;
</code></pre>
<h3 id="37-封装聊天列表组件">3.7 封装聊天列表组件</h3>
<blockquote>
<p>需要注意view标签监听<code>@longpress</code>长按事件无法获取坐标值,此时换成 div 标签即可。</p>
</blockquote>
<p><code> free-media-list.vue 文件代码</code></p>
<pre><code class="language-html">&lt;template&gt;
        &lt;view hover-class="bg-hover-light" v-if="item"&gt;
                &lt;!-- 列表 --&gt;
                &lt;div class="flex" @click="onClick" @longpress="long"&gt;
                        &lt;!-- 左侧 --&gt;
                        &lt;view class="flex align-center justify-center position-relative" style="width: 145rpx"&gt;
                                &lt;free-avatar :src="item.avatar" size="92" /&gt;
                                &lt;!-- 角标 --&gt;
                                &lt;free-badge :value="item.noreadnum" badgeClass="position-absolute" badgeStyle="top: 15rpx;right:15rpx" /&gt;
                        &lt;/view&gt;
                        &lt;!-- 右侧 --&gt;
                        &lt;view class="flex flex-column border-bottom flex-1 py-3 pr-3 border-light-secondary"&gt;
                                &lt;view class="flex align-center justify-between mb-1"&gt;
                                        &lt;text class="font-md"&gt;{{item.nickname}}&lt;/text&gt;
                                        &lt;text class="font-sm text-light-muted"&gt;{{item.update_time | formatTime}}&lt;/text&gt;
                                &lt;/view&gt;
                                &lt;text class="font-sm text-ellipsis text-light-muted"&gt;{{item.data}}&lt;/text&gt;
                        &lt;/view&gt;
                &lt;/div&gt;
        &lt;/view&gt;
&lt;/template&gt;

&lt;script&gt;
        import FreeAvatar from '@/components/free-ui/free-avatar.vue'
        import FreeBadge from '@/components/free-ui/free-badge.vue'
        import $Time from '@/common/free-lib/time.js'
        export default {
                name: 'FreeMediaList',
                components: {
                        FreeAvatar,
                        FreeBadge
                },
                props: {
                        item: Object,
                        index: Number
                },
                data() {
                        return {}
                },
                computed: {},
                watch: {},
                created() {},
                mounted() {},
                methods: {
                        onClick() {
                                this.$emit('click')
                        },
                        long(e) {
                                console.log(e)
                        }
                },
                filters: {
                        formatTime(value) {
                                return $Time.gettime(value)
                        }
                }
        }
&lt;/script&gt;

&lt;style scoped lang="less"&gt;
&lt;/style&gt;

</code></pre>
<p>使用组件的代码</p>
<pre><code class="language-html">&lt;!-- 列表 --&gt;
&lt;block v-for="(item, index) in list" :key="index"&gt;
    &lt;free-media-list
        :item="item"
    :index="index"
        /&gt;
&lt;/block&gt;
</code></pre>
<h3 id="38-封装全局mixin">3.8 封装全局mixin</h3>
<blockquote>
<p>例如有以下场景:</p>
<p>多个组件内需要对日期时间进行格式化处理,这时我们已经在某个组件内定义了<code>filters</code>过滤器,其他组件也需要使用这个过滤器,难道我们一个个 CV 过去吗?显然太low,而且以后这个<code>filters</code>发生变动你其他引用了这个<code>filters</code>的组件代码也得跟着改,也就会造成大量的重复代码冗余,维护起来极其不方便,此时就可以用 Vue 的<code>mixin</code>特性来解决这个问题,当然有人会说 <code>"我用全局过滤器也可以啊"</code>,是的,但我就要用<code>mixin</code>。</p>
</blockquote>
<p>官方的解释和<code>demo</code>已经很详细了,参考官方文档</p>
<p><code>mixin/free-base.js 文件代码</code></p>
<pre><code class="language-js">import $Time from '@/common/free-lib/time.js'
export default {
        filters: {
                formatTime(value) {
                        return $Time.gettime(value)
                }
        }
}

</code></pre>
<p><code>使用mixin</code></p>
<pre><code class="language-js">import freeBase from '@/common/mixin/free-base.js'

export default {
    mixins: ,
    props: {
    },
    data() {
      return {}
    },
    computed: {},
    watch: {},
    created() {},
    mounted() {},
    methods: {}
}
</code></pre>
<blockquote>
<p>到这里就已经把处理时间的过滤器混入到组件中了,直接在模版中使用即可。</p>
</blockquote>
<h3 id="38-开发弹出层组件">3.8 开发弹出层组件</h3>
<blockquote>
<p>通过 API 动态获取的参数都为 px,如果需要与 rpx 单位进行计算,要提前使用 <code>uni.upx2px</code>方法将 rpx 转换成px。</p>
<pre><code class="language-js">mounted() {
    // 获取系统信息
    let info = uni.getSystemInfoSync()
    this.maxX = info.windowWidth - uni.upx2px(this.bodyWidth) - 30
    this.maxY = info.windowHeight - uni.upx2px(this.bodyHeight) - 30
},
</code></pre>
</blockquote>
<p>长按事件的跨端兼容问题。</p>
<blockquote>
<p><code>@longpress</code>事件在nuve的原生APP环境和微信小程序环境下获取的参数属性不同,因此需要写2套代码兼容多端。</p>
<pre><code class="language-js">long(e) {
    let x = 0
    let y = 0
    // #ifdef APP-NVUE
    if(Array.isArray(e.changedTouches) &amp;&amp; e.changedTouches.length &gt; 0) {
      x = e.changedTouches.screenX
      y = e.changedTouches.screenY
    }
    // #endif

    // #ifdef MP-WEIXIN
    x = e.target.x
    y = e.target.y
    // #endif

    this.$emit('long', {x,y})
}
</code></pre>
</blockquote>
<p>处理长按弹出菜单的边界问题。</p>
<blockquote>
<ol>
<li>计算屏幕宽高的边界最大值,通过API获取屏幕宽高 - 元素宽高</li>
<li>判断用户点击的x/y坐标是否大于该值,大于直接使用最大值,否则是否坐标值</li>
</ol>
</blockquote>
<p>给弹出层组件增加动画效果,代码结合上下文进行理解。</p>
<ol>
<li>
<p>引入 weex 的 <code>animation</code>模块</p>
<pre><code class="language-js">// #ifdef APP-NVUE
const animation = weex.requireModule('animation')
// #endif
</code></pre>
</li>
<li>
<p>使用 <code>animation</code> API 实现动画</p>
<pre><code class="language-js">show(x = -1, y = -1) {
    this.x = (x &gt; this.maxX) ? this.maxX : x
    this.y = (y &gt; this.maxY) ? this.maxY : y
    this.status = true
    // #ifdef APP-NVUE
    this.$nextTick(_=&gt;{
      animation.transition(this.$refs.popup, {
            styles: {
                transform: 'scale(1,1)',
                transformOrigin: 'left top',
                opacity: 1
            },
            duration: 200,        // 单位:ms
            timingFunction: 'ease'
      })
    })
    // #endif
},
</code></pre>
</li>
</ol>
<h3 id="39-开发导航栏的弹出菜单">3.9 开发导航栏的弹出菜单</h3>
<p>直接看 commit</p>
<h3 id="310-删除当前聊天会话">3.10 删除当前聊天会话</h3>
<p>直接看 commit</p>
<h3 id="311-设置和取消聊天置顶">3.11 设置和取消聊天置顶</h3>
<p>直接看 commit</p>
<h2 id="4通讯录页开发">4.通讯录页开发</h2>
<h3 id="41-通讯录列表组件开发">4.1 通讯录列表组件开发</h3>
<p><code>mail/mail.nvue</code></p>
<pre><code class="language-html">&lt;template&gt;
        &lt;view&gt;
                &lt;!-- 导航栏 --&gt;
                &lt;free-nav-bar title titleValue="通讯录" fixed @openPopup="handleOpenPopup" /&gt;
                &lt;free-media-list /&gt;

                &lt;!-- 通讯录列表组件 --&gt;
                &lt;view class="flex bg-white" v-for="item in 5"&gt;
                        &lt;!-- 左侧图片 --&gt;
                        &lt;view class="flex justify-center align-center py-2 px-3" style="width: 132rpx; height: 104rpx;"&gt;
                                &lt;image style="width: 76rpx; height: 76rpx;" src="/static/images/mail/friend.png" mode="widthFix"&gt;&lt;/image&gt;
                        &lt;/view&gt;

                        &lt;!-- 右侧内容 --&gt;
                        &lt;view class="flex-1 flex align-center border-bottom"&gt;
                                &lt;text class="font-md text-dark"&gt;新的朋友&lt;/text&gt;
                        &lt;/view&gt;
                &lt;/view&gt;
        &lt;/view&gt;
&lt;/template&gt;

&lt;script&gt;
        import FreeNavBar from '@/components/free-ui/free-nav-bar.vue'
        export default {
                components: {
                        FreeNavBar
                },
                data() {
                        return {

                        }
                },
                onLoad() {

                },
                methods: {

                }
        }
&lt;/script&gt;

&lt;style&gt;

&lt;/style&gt;

</code></pre>
<h3 id="42-封装公共列表组件">4.2 封装公共列表组件</h3>
<p><code>free-list-item.vue</code></p>
<pre><code class="language-html">&lt;template&gt;
        &lt;view class="flex bg-white" hover-class="bg-light" @click="$emit('click')"&gt;
                &lt;!-- 左侧图片 --&gt;
                &lt;view
                        class="flex justify-center align-center py-2 px-3"
                        style="width: 132rpx; height: 104rpx;"
                &gt;
                        &lt;image v-if="cover" style="width: 76rpx; height: 76rpx;" :src="cover" mode="widthFix"&gt;&lt;/image&gt;
                &lt;/view&gt;

                &lt;!-- 右侧内容 --&gt;
                &lt;view class="flex-1 flex align-center border-bottom"&gt;
                        &lt;text class="font-md text-dark"&gt;{{title}}&lt;/text&gt;
                &lt;/view&gt;
        &lt;/view&gt;
&lt;/template&gt;

&lt;script&gt;
        export default {
                name: 'FreeListItem',
                components: {},
                props: {
                        // 封面
                        cover: {
                                type: String,
                                default: ""
                        },
                        // 标题
                        title: {
                                type: String,
                                default: ""
                        }
                },
                data() {
                        return {}
                },
                computed: {},
                watch: {},
                created() {},
                mounted() {
                        console.log(123)
                },
                methods: {}
        }
&lt;/script&gt;

&lt;style scoped lang="less"&gt;&lt;/style&gt;

</code></pre>
<p>页面组件中使用</p>
<pre><code class="language-html">&lt;!-- 通讯录列表组件 --&gt;
&lt;free-list-item v-for="(item, index) in list" :key="index" :cover="item.cover" :title="item.title" /&gt;
</code></pre>
<h3 id="43-完善通讯录列表">4.3 完善通讯录列表</h3>
<p>效果图:</p>
<p><img src="https://img2020.cnblogs.com/blog/2217722/202105/2217722-20210528014052289-376831396.png" alt="1616180499592" loading="lazy"></p>
<p><code>mail/mail.nvue</code></p>
<pre><code class="language-html">&lt;template&gt;
        &lt;view&gt;
                &lt;!-- 导航栏 --&gt;
                &lt;free-nav-bar title titleValue="通讯录" fixed @openPopup="handleOpenPopup" /&gt;

                &lt;!-- 通讯录列表组件 --&gt;
                &lt;free-list-item v-for="(item, index) in topList" :key="index" :cover="item.cover" :title="item.title" /&gt;
               
                &lt;!-- 通讯录列表 --&gt;
                &lt;block v-for="(item, index) in list" :key="index"&gt;
                        &lt;view v-if="item.data.length"&gt;
                                &lt;view class="py-2 px-3 border-bottom bg-light"&gt;
                                        &lt;text class="font-sm text-dark"&gt;{{item.letter}}&lt;/text&gt;
                                &lt;/view&gt;
                                &lt;free-list-item v-for="(item2, index2) in item.data" :key="index2" :title="item2" cover="/static/avatar.jpg" /&gt;
                        &lt;/view&gt;
                &lt;/block&gt;
        &lt;/view&gt;
&lt;/template&gt;

&lt;script&gt;
        import FreeNavBar from '@/components/free-ui/free-nav-bar.vue'
        import FreeListItem from '@/components/free-ui/free-list-item.vue'
        export default {
                components: {
                        FreeNavBar,
                        FreeListItem
                },
                data() {
                        return {
                                topList: [
                                        {
                                                title: "新的朋友",
                                                cover: "/static/images/mail/friend.png",
                                                event: ""
                                        },
                                        {
                                                title: "群聊",
                                                cover: "/static/images/mail/group.png",
                                                event: ""
                                        },
                                        {
                                                title: "标签",
                                                cover: "/static/images/mail/tag.png",
                                                event: ""
                                        }
                                ],
                                list: [{
                                        "letter": "A",
                                        "data": [
                                                "阿苏",
                                                "阿拉",
                                                "阿勒",
                                                "阿里",
                                                "安庆",
                                                "澳机"
                                        ]
                                }, {
                                        "letter": "B",
                                        "data": [
                                                "保罗",
                                                "包头",
                                                "北海福成",
                                                "北南苑",
                                                "北都国际"
                                        ]
                                }, {
                                        "letter": "C",
                                        "data": [
                                                "长山场",
                                                "长国",
                                                "常德",
                                                "长花~",
                                                "长王",
                                                "常机",
                                                "成双",
                                                "赤峰"
                                        ]
                                }, {
                                        "letter": "D",
                                        "data": [
                                                "大理爱你",
                                                "大周哈哈",
                                                "达河",
                                                "丹浪",
                                                "德芒",
                                                "迪香里拉",
                                        ]
                                }, {
                                        "letter": "E",
                                        "data": [
                                                "鄂多斯",
                                        ]
                                }, {
                                        "letter": "F",
                                        "data": [
                                                "福乐",
                                        ]
                                }, {
                                        "letter": "G",
                                        "data": [
                                                "固原六盘",
                                                "广盘龙",
                                                "广白机",
                                                "桂江",
                                        ]
                                }, {
                                        "letter": "H",
                                        "data": [
                                                "哈尔滨太平",
                                                "哈密",
                                                "海美兰",
                                                "海拉尔",
                                                "邯郸",
                                        ]
                                }, {
                                        "letter": "I",
                                        "data": []
                                }, {
                                        "letter": "J",
                                        "data": [
                                                "鸡西兴",
                                                "佳木斯",
                                                "嘉峪",
                                        ]
                                }, {
                                        "letter": "K",
                                        "data": [
                                                "克拉玛依",
                                                "库车龟兹",
                                                "库尔勒",
                                        ]
                                }, {
                                        "letter": "L",
                                        "data": [
                                                "拉萨贡嘎",
                                                "黎平",
                                                "林芝米",
                                                "柳州白莲",
                                        ]
                                }, {
                                        "letter": "M",
                                        "data": [

                                        ]
                                }, {
                                        "letter": "N",
                                        "data": [

                                        ]
                                }, {
                                        "letter": "O",
                                        "data": []
                                }, {
                                        "letter": "P",
                                        "data": [
                                                "普洱思茅"
                                        ]
                                }, {
                                        "letter": "Q",
                                        "data": [
                                                "齐齐哈尔",
                                                "秦皇岛山",
                                                "青岛流亭",
                                                "衢州机场",
                                                "泉州晋江机场"
                                        ]
                                }, {
                                        "letter": "R",
                                        "data": [
                                                "日喀则和"
                                        ]
                                }, {
                                        "letter": "S",
                                        "data": [
                                                "三亚凤凰",
                                                "汕头",
                                                "上海虹桥",
                                                "上海浦东",
                                                "深圳宝安",
                                                "沈阳桃仙",
                                                "石家庄正定",
                                                "苏南硕放"
                                        ]
                                }, {
                                        "letter": "T",
                                        "data": [
                                                "塔城",
                                        ]
                                }, {
                                        "letter": "U",
                                        "data": []
                                }, {
                                        "letter": "V",
                                        "data": []
                                }, {
                                        "letter": "W",
                                        "data": [
                                                "文山",
                                                "温永强",
                                                "乌海",
                                                "武汉天",
                                        ]
                                }, {
                                        "letter": "X",
                                        "data": []
                                }, {
                                        "letter": "Y",
                                        "data": []
                                }, {
                                        "letter": "Z",
                                        "data": [
                                                "昭通",
                                                "芷江",
                                                "中卫",
                                                "舟山",
                                        ]
                                }]
                        }
                },
                onLoad() {

                },
                methods: {

                }
        }
&lt;/script&gt;

&lt;style&gt;

&lt;/style&gt;

</code></pre>
<h2 id="5发现页开发">5.发现页开发</h2>
<p><code>find/find.nvue</code></p>
<pre><code class="language-html">&lt;template&gt;
        &lt;view class="page"&gt;
                &lt;free-nav-bar title titleValue="发现" /&gt;
               
                &lt;!-- 列表 --&gt;
                &lt;free-list-item title="朋友圈" rightIconShow&gt;
                        &lt;text slot="icon" class="iconfont font-lg main-text-color"&gt;&amp;#58983;&lt;/text&gt;
                        &lt;view slot="right" class="position-relative p-1"&gt;
                                &lt;free-avatar src="/static/avatar.jpg" size="55"/&gt;
                                &lt;text
                                  class="rounded-circle bg-danger position-absolute"
                                  style="width: 20rpx;height: 20rpx;top: 0;right: 0;"
                                &gt;&lt;/text&gt;
                        &lt;/view&gt;
                &lt;/free-list-item&gt;
                &lt;free-divider /&gt;
               
                &lt;free-list-item title="扫一扫" rightIconShow&gt;
                        &lt;text slot="icon" class="iconfont font-lg"&gt;&amp;#58900;&lt;/text&gt;
                &lt;/free-list-item&gt;
               
                &lt;free-list-item title="摇一摇" rightIconShow&gt;
                        &lt;text slot="icon" class="iconfont font-lg"&gt;&amp;#58941;&lt;/text&gt;
                &lt;/free-list-item&gt;
                &lt;free-divider /&gt;
               
                &lt;free-list-item title="看一看" rightIconShow&gt;
                        &lt;text slot="icon" class="iconfont font-lg"&gt;&amp;#58896;&lt;/text&gt;
                &lt;/free-list-item&gt;
               
                &lt;free-list-item title="搜一搜" rightIconShow&gt;
                        &lt;text slot="icon" class="iconfont font-lg"&gt;&amp;#58897;&lt;/text&gt;
                &lt;/free-list-item&gt;
                &lt;free-divider /&gt;
               
                &lt;free-list-item title="购物" rightIconShow&gt;
                        &lt;text slot="icon" class="iconfont font-lg"&gt;&amp;#58968;&lt;/text&gt;
                &lt;/free-list-item&gt;
        &lt;/view&gt;
&lt;/template&gt;

&lt;script&gt;
        import FreeNavBar from '@/components/free-ui/free-nav-bar.vue'
        import FreeListItem from '@/components/free-ui/free-list-item.vue'
        import FreeAvatar from '@/components/free-ui/free-avatar.vue'
        import FreeDivider from '@/components/free-ui/free-divider.vue'
        export default {
                name: "FindIndex",
                components: {
                        FreeNavBar,
                        FreeListItem,
                        FreeDivider,
                        FreeAvatar
                },
                data() {
                        return {
                               
                        }
                },
                onLoad() {
                       
                },
                methods: {

                }
        }
&lt;/script&gt;

&lt;style lang="less"&gt;

&lt;/style&gt;

</code></pre>
<p>该业务针对<code>free-list-item</code>组件做了部分修改,添加了插槽,具体细节参考commit。</p>
<h2 id="6个人中心页开发">6.个人中心页开发</h2>
<h3 id="61-优化自定义导航栏功能">6.1 优化自定义导航栏功能</h3>
<p><code>my.nvue</code></p>
<pre><code class="language-html">&lt;template&gt;
&lt;view class="page"&gt;
        &lt;free-nav-bar bgColor="bg-white" &gt;
                &lt;!-- &lt;text slot="right" :iconValue="'\ue59117'"&gt;&lt;/text&gt; --&gt;
                &lt;free-icon-button slot="right" :iconValue="'\ue6ed'" /&gt;
        &lt;/free-nav-bar&gt;
        &lt;/view&gt;
&lt;/template&gt;

&lt;script&gt;
import FreeNavBar from '@/components/free-ui/free-nav-bar.vue'
import FreeIconButton from '@/components/free-ui/free-icon-button.vue'
export default {
name: 'MyIndex',
components: {
                FreeNavBar,
                FreeIconButton
        },
props: {},
data () {
    return {}
},
computed: {},
watch: {},
created () {},
mounted () {},
methods: {}
}
&lt;/script&gt;

&lt;style scoped lang="less"&gt;&lt;/style&gt;

</code></pre>
<blockquote>
<p>使用<code>iconfont</code>图标的<code>unicode</code>编码动态赋值,需要进行格式转换:</p>
<p>例如将<code>&amp;#xe601;</code>转换成<code>\ue601</code>,注意这里去掉了分号!</p>
</blockquote>
<h3 id="62-完善个人中心页">6.2 完善个人中心页</h3>
<p>效果图:</p>
<p><img src="https://img2020.cnblogs.com/blog/2217722/202105/2217722-20210528014052547-1475704036.png" alt="1616275944396" loading="lazy"></p>
<p><code>my.nvue</code></p>
<pre><code class="language-html">&lt;template&gt;
&lt;view class="page"&gt;
                &lt;free-nav-bar bgColor="bg-white" &gt;
                        &lt;free-icon-button slot="right" :iconValue="'\ue6ed'" /&gt;
                &lt;/free-nav-bar&gt;
               
                &lt;free-list-item cover="/static/images/demo/demo6.jpg" coverSize="120" title="张波" rightIconShow&gt;
                        &lt;!-- 中间文本内容 --&gt;
                        &lt;view class="flex flex-column"&gt;
                                &lt;text class="text-dark font-lg font-weight-bold"&gt;伤心的瘦子&lt;/text&gt;
                                &lt;text class="text-light-muted font mt-2"&gt;叮咚号: Alexander3714&lt;/text&gt;
                        &lt;/view&gt;
                       
                        &lt;!-- 右侧图标 --&gt;
                        &lt;view slot="right"&gt;
                                &lt;text class="iconfont font-md text-light-muted"&gt;&amp;#xe614;&lt;/text&gt;
                        &lt;/view&gt;
                &lt;/free-list-item&gt;
               
                &lt;free-divider&gt;&lt;/free-divider&gt;
                &lt;free-list-item title="支付" rightIconShow&gt;
                        &lt;text slot="icon" class="iconfont font-lg py-1"&gt;&amp;#xe66c;&lt;/text&gt;
                &lt;/free-list-item&gt;
                &lt;free-divider&gt;&lt;/free-divider&gt;
                &lt;free-list-item title="收藏" rightIconShow&gt;
                        &lt;text slot="icon" class="iconfont font-lg py-1"&gt;&amp;#xea2d;&lt;/text&gt;
                &lt;/free-list-item&gt;
                &lt;free-list-item title="相册" rightIconShow&gt;
                        &lt;text slot="icon" class="iconfont font-lg py-1"&gt;&amp;#xe608;&lt;/text&gt;
                &lt;/free-list-item&gt;
                &lt;free-list-item title="表情" rightIconShow&gt;
                        &lt;text slot="icon" class="iconfont font-lg py-1"&gt;&amp;#xe605;&lt;/text&gt;
                &lt;/free-list-item&gt;
                &lt;free-divider&gt;&lt;/free-divider&gt;
                &lt;free-list-item title="设置" rightIconShow&gt;
                        &lt;text slot="icon" class="iconfont font-lg py-1"&gt;&amp;#xe612;&lt;/text&gt;
                &lt;/free-list-item&gt;
        &lt;/view&gt;
&lt;/template&gt;

&lt;script&gt;
import FreeNavBar from '@/components/free-ui/free-nav-bar.vue'
import FreeIconButton from '@/components/free-ui/free-icon-button.vue'
import FreeListItem from '@/components/free-ui/free-list-item.vue'
import FreeDivider from '@/components/free-ui/free-divider.vue'
export default {
name: 'MyIndex',
components: {
                FreeNavBar,
                FreeIconButton,
                FreeListItem,
                FreeDivider
        },
props: {},
data () {
    return {}
},
computed: {},
watch: {},
created () {},
mounted () {},
methods: {}
}
&lt;/script&gt;

&lt;style scoped lang="less"&gt;&lt;/style&gt;

</code></pre>
<p>Typora太卡了。。。换一个文件写。。。</p><br><br>
来源:https://www.cnblogs.com/alexander3714/p/14541481.html
頁: [1]
查看完整版本: uni-app全栈仿微信开源项目系列(一)