原生微信小程序

微信小程序

https://mp.weixin.qq.com/
在这里插入图片描述

小程序代码构成

  • JSON 配置
  • WXML 模版
  • WXSS 样式
  • JS 逻辑交互

JSON 配置

在小程序中,JSON扮演的静态配置的角色。

小程序配置 app.json

    {
        "pages": ["pages/index/index", "pages/logs/logs"],
        "window": {
            "backgroundTextStyle": "light",
            "navigationBarBackgroundColor": "#fff",
            "navigationBarTitleText": "WeChat",
            "navigationBarTextStyle": "black"
        }
    }
  • pages字段 —— 用于描述当前小程序所有页面路径,这是为了让微信客户端知道当前你的小程序页面定义在哪个目录。
  • window字段 —— 定义小程序所有页面的顶部背景颜色,文字颜色定义等。

全局配置

https://developers.weixin.qq.com/miniprogram/dev/reference/configuration/app.html

tabBar

如果小程序是一个多 tab 应用(客户端窗口的底部或顶部有 tab 栏可以切换页面),可以通过 tabBar 配置项指定 tab 栏的表现,以及 tab 切换时显示的对应页面。

其中 list 接受一个数组,只能配置最少 2 个、最多 5 个 tab。tab 按数组的顺序排序,每个项都是一个对象

   {
        "pages": ["pages/index/index", "pages/logs/index"],
        "tabBar": {
            "list": [
                {
                    "iconPath": "assets/fonts/首页.png",
                    "selectedIconPath": "assets/fonts/home.png",
                    "pagePath": "pages/index/index",
                    "text": "首页"
                },
                {
                    "pagePath": "pages/logs/logs",
                    "text": "日志"
                }
            ]
        }
    }

tabBar 还有其他属性

颜色仅支持十六进制,定位仅支持top和bottom,其中top时不支持图标。

{
        "tabBar": {
            "color": "#ff00ff",
            "selectedColor": "#0000ff",
            "backgroundColor": "#00ff00",
            "position":"bottom"  
        }
    }

页面配置

每一个小程序页面也可以使用 .json 文件来对本页面的窗口表现进行配置。页面中配置项在当前页面会覆盖 app.json 的 window 中相同的配置项

页面配置中只能设置 app.json 中 window 对应的配置项,以决定本页面的窗口表现,所以无需写 window 这个属性。

WXML 模版

WXML 充当的就是类似 HTML 的角色。

    <view class="container">
        <view class="userinfo">
            <button wx:if="{{!hasUserInfo && canIUse}}">获取头像昵称</button>
            <block wx:else>
                <image src="{{userInfo.avatarUrl}}" background-size="cover"></image>
                <text class="userinfo-nickname">{{userInfo.nickName}}</text>
            </block>
        </view>
        
        <view class="usermotto">
            <text class="user-motto">{{motto}}</text>
        </view>
    </view>

block标签的作用是直接解析里面的内容,不解析自身block标签。

WXSS 样式

WXSS 具有 CSS 大部分的特性,小程序在 WXSS 也做了一些扩充和修改。

  • 新增了尺寸单位。在写 CSS 样式时,开发者需要考虑到手机设备的屏幕会有不同的宽度和设备像素比,采用一些技巧来换算一些像素单位。WXSS 在底层支持新的尺寸单位 rpx ,开发者可以免去换算的烦恼,只要交给小程序底层来换算即可,由于换算采用的浮点数运算,所以运算结果会和预期结果有一点点偏差。
  • 提供了全局的样式和局部样式。和前边 app.json, page.json 的概念相同,你可以写一个 app.wxss 作为全局样式,会作用于当前小程序的所有页面,局部页面样式 page.wxss 仅对当前页面生效。
  • 此外 WXSS 仅支持部分 CSS 选择器

https://developers.weixin.qq.com/miniprogram/dev/framework/view/wxss.html
iPhone6 1rpx = 0.5px 1px = 2rpx

JS 逻辑交互

    <view>{{ msg }}</view>
    <button bindtap="clickMe">点击我</button>
    Page({
        data:{
            msg: '小程序'
        },
        clickMe() {
            this.setData({msg: 'Hello World'})
        }
    })

更详细的资料:https://developers.weixin.qq.com/miniprogram/dev/framework/view/wxml/event.html

小程序宿主环境

  • 渲染层和逻辑层
  • 程序与页面
  • 组件
  • API

渲染层和逻辑层

小程序的运行环境分成渲染层和逻辑层,其中 WXML 模板和 WXSS 样式工作在渲染层,JS 脚本工作在逻辑层。

小程序的渲染层和逻辑层分别由2个线程管理:渲染层的界面使用了WebView 进行渲染;逻辑层采用JsCore线程运行JS脚本。一个小程序存在多个界面,所以渲染层存在多个WebView线程,这两个线程的通信会经由微信客户端(Native)做中转,逻辑层发送网络请求也经由Native转发。

程序与页面

微信客户端在打开小程序之前,会把整个小程序的代码包下载到本地。
紧接着通过 app.json 的 pages 字段就可以知道你当前小程序的所有页面路径。
整个小程序只有一个 App 实例,是全部页面共享的,具体内容查看注册程序 App https://developers.weixin.qq.com/miniprogram/dev/framework/app-service/app.html

组件

小程序提供了丰富的组件

   <navigator
        url="/page/navigate/navigate?title=navigate"
        hover-class="navigator-hover"
    >
        跳转到新页面(有后退按钮),url不能为tabbar中list项
    </navigator>
    
    <navigator
        url="../../redirect/redirect/redirect?title=redirect"
        open-type="redirect"
        hover-class="other-navigator-hover"
    >
        在当前页打开(没有后退按钮),url不能为tabbar中list项
    </navigator>
    
    <navigator
        url="/page/index/index"
        open-type="switchTab"
        hover-class="other-navigator-hover"
    >
        切换 Tab,url必须是tabbar中list项
    </navigator>

API

为了让开发者可以很方便的调起微信提供的能力,例如获取用户信息、微信支付等等,小程序提供了很多 API 给开发者去使用。

    wx.scanCode({
        success: (res) => {
            console.log(res)
        }
    })

https://developers.weixin.qq.com/miniprogram/dev/api/index.html

视图层

  • WXML
  • WXS
  • 事件系统
  • 动画
  • 组件

WXML

WXML(WeiXin Markup Language)是框架设计的一套标签语言,结合基础组件、事件系统,可以构建出页面的结构。

数据绑定

<!--wxml-->
    <view>{{message}}</view>
// page.js
    Page({
        data: {
            message: 'Hello MINA!'
        }
    })

https://developers.weixin.qq.com/miniprogram/dev/framework/view/wxml/data.html

列表渲染

默认数组的当前项的下标变量名默认为 index,数组当前项的变量名默认为 item

<!--wxml-->
    <view wx:for="{{array}}">{{item}}</view>
// page.js
    Page({
        data: {
            array: [1, 2, 3, 4, 5]
        }
    })
    <view wx:for="{{array}}" wx:for-index="idx" wx:for-item="itemName">
      {{idx}}: {{itemName.message}}
    </view>

https://developers.weixin.qq.com/miniprogram/dev/framework/view/wxml/list.html

如不提供 wx:key,会报一个 warning

 <switch wx:for="{{objectArray}}" wx:key="unique" style="display: block;">
        {{item.id}}
    </switch>
    Page({
        data: {
            objectArray: [
                {id: 5, unique: 'unique_5'},
                {id: 4, unique: 'unique_4'},
                {id: 3, unique: 'unique_3'},
                {id: 2, unique: 'unique_2'},
                {id: 1, unique: 'unique_1'},
                {id: 0, unique: 'unique_0'}
            ]
        }
    }) 

条件渲染

<!--wxml-->
    <view wx:if="{{view == 'WEBVIEW'}}">WEBVIEW</view>
    <view wx:elif="{{view == 'APP'}}">APP</view>
    <view wx:else>MINA</view>
// page.js
    Page({
        data: {
            view: 'MINA'
        }
    })

https://developers.weixin.qq.com/miniprogram/dev/framework/view/wxml/conditional.html

wx:if vs hidden

wx:if 是惰性的,如果在初始渲染条件为 false,框架什么也不做,在条件第一次变成真的时候才开始局部渲染。

相比之下,hidden 就简单的多,组件始终会被渲染,只是简单的控制显示与隐藏。

一般来说,wx:if 有更高的切换消耗而 hidden 有更高的初始渲染消耗。因此,如果需要频繁切换的情景下,用 hidden 更好,如果在运行时条件不大可能改变则 wx:if 较好。

    <view hidden="{{hidden}}">
        内容
    </view>

模板

<!--wxml-->
    <template name="staffName">
        <view>
            FirstName: {{firstName}}, LastName: {{lastName}}
        </view>
    </template>
    
    <template is="staffName" data="{{...staffA}}"></template>
    <template is="staffName" data="{{...staffB}}"></template>
    <template is="staffName" data="{{...staffC}}"></template>
// page.js
    Page({
        data: {
            staffA: {firstName: 'Hulk', lastName: 'Hu'},
            staffB: {firstName: 'Shang', lastName: 'You'},
            staffC: {firstName: 'Gideon', lastName: 'Lin'}
        }
    })

https://developers.weixin.qq.com/miniprogram/dev/framework/view/wxml/template.html

引用

WXML 提供两种文件引用方式import和include。

https://developers.weixin.qq.com/miniprogram/dev/framework/view/wxml/import.html

<!-- item.wxml -->
    <template name="item">
        <text>{{text}}</text>
    </template>

在 index.wxml 中引用了 item.wxml,就可以使用item模板:

 <import src="item.wxml" />
    <template is="item" data="{{text: 'forbar'}}" />

WXS

WXS(WeiXin Script)是小程序的一套脚本语言,结合 WXML,可以构建出页面的结构。

https://developers.weixin.qq.com/miniprogram/dev/framework/view/wxs/

页面渲染

<!--wxml-->
    <wxs module="m1">
        var msg = "hello world"; module.exports.message = msg;
    </wxs>
<view>{{m1.message}}</view>

页面输出:

hello world

事件系统

https://developers.weixin.qq.com/miniprogram/dev/framework/view/wxml/event.html

什么是事件

  • 事件是视图层到逻辑层的通讯方式。
  • 事件可以将用户的行为反馈到逻辑层进行处理。
  • 事件可以绑定在组件上,当达到触发事件,就会执行逻辑层中对应的事件处理函数。
  • 事件对象可以携带额外信息,如 id, dataset, touches。

事件的使用方式

 <view id="tapTest" data-hi="WeChat" bindtap="tapName">Click me!</view>
    Page({
        tapName(event) {
            console.log(event)
        }
    })

事件绑定和冒泡

事件绑定的写法同组件的属性,以 key、value 的形式。

  • key 以bind或catch开头,然后跟上事件的类型,如bindtap、catchtouchstart。
  • value 是一个字符串,需要在对应的 Page 中定义同名的函数。不然当触发事件的时候会报错。

bind事件绑定不会阻止冒泡事件向上冒泡,catch事件绑定可以阻止冒泡事件向上冒泡。

如在下边这个例子中,点击 inner view 会先后调用handleTap3和handleTap2(因为tap事件会冒泡到 middle view,而 middle view 阻止了 tap 事件冒泡,不再向父节点传递),点击 middle view 会触发handleTap2,点击 outer view 会触发handleTap1。

<view id="outer" bindtap="handleTap1">
    outer view
    <view id="middle" catchtap="handleTap2">
        middle view
        <view id="inner" bindtap="handleTap3">
            inner view
        </view>
    </view>
</view>

事件的捕获阶段

需要在捕获阶段监听事件时,可以采用capture-bind、capture-catch关键字,后者将中断捕获阶段和取消冒泡阶段。

在下面的代码中,点击 inner view 会先后调用handleTap2、handleTap4、handleTap3、handleTap1。

<view
    id="outer"
    bind:touchstart="handleTap1"
    capture-bind:touchstart="handleTap2"
>
    outer view
    <view
        id="inner"
        bind:touchstart="handleTap3"
        capture-bind:touchstart="handleTap4"
    >
        inner view
    </view>
</view>

动画

方案1:写css样式

#box2{
    height: 100px;
    width: 100px;
    background: blue;
}

#box2:hover{
    transform: rotate(180deg) scale(.5, .5);
    background: red;
    transition: background 2s ease, transform 2s ease-in 1s;
}

方案2:animate.css 库

// app.wxss 在主页面引入,page页面就都可以使用了
@import './assets/animate.wxss';

<view class='{{a}}'></view>

this.setData({
    a : ['box', 'animated', 'fadeOutRight']
})

方案3:写js函数

https://developers.weixin.qq.com/miniprogram/dev/api/wx.createAnimation.html

    <button bindtap="fn">动画</button>
    
    <view
        animation="{{animationData}}"
        style="background:red;height:100rpx;width:100rpx;position: absolute;top:1000rpx;"
    ></view>
   data: {
        animationData: {}
    },
    fn() {
        const animation = wx.createAnimation({
            duration: 3000
        })
        
        animation.scale(2, 2).rotate(45).step().width(400).top(10).step()
        
        this.setData({
            animationData: animation.export()
        })
    }

想实现动画,记得给动画初始属性,以及结尾调用step()方法。

step方法表示动画完成。一组动画中的所有动画会同时开始,一组动画完成后才会进行下一组动画。

导出动画队列。export 方法每次调用后会清掉之前的动画操作。

group 动画

    <input value="{{val}}" bindinput="sync" />
    
    <button bindtap="add">添加</button>
    
    <view wx:for="{{todos}}" wx:key="id" animation="{{item.animation}}" style="opacity:0.1">
        {{item.val}}
    </view>
    Page({
        data: {
            val : 'hello',
            todos : []
        },
        
        sync(e){
            // 数据双向绑定
            var val = e.detail.value;
            this.setData({
                val
            })
        },
        
        add(){
            var val = this.data.val;
            var todos = this.data.todos;
            todos.push({
                id : new Date().getTime(),
                val,
                animation : {}
            });
            // 渲染当前页面
            this.setData({
                val: '',
                todos: [...todos]
            })
            // 创建动画
            const animationData = wx.createAnimation({
                duration: 1500
            });
            animationData.opacity(1).step();
            // 找到最新的数据
            todos[todos.length - 1].animation = animationData.export();
            // 重新渲染动画效果
            setTimeout(()=>{
                this.setData({
                    todos: [...todos]
                })
            }, 100)
        }    
    })

组件

定义组件

鼠标右键->新建组件,会生成一组文件。

// components/toast/toast.js
  Component({
        //组件的属性列表
        properties: {
            str : String
        },
        
        //组件的生命周期函数
        lifetimes: {
            attached() {
                // 在组件实例进入页面节点树时执行
                console.log('attached:', this.properties.str);
            },
            detached() {
                // 在组件实例被从页面节点树移除时执行
                console.log('删除')
            }
        },
        
        // 组件的初始数据
        data: {
        
        },
        
        // 组件的方法列表
        methods: {
            show(){
            }
        }
    })
    
// components/toast/toast.wxml
    <view>
        传过来的属性:{{str}}
    </view>

使用组件

// pages/mine/mine.json
   {
        "usingComponents": {
            "toast":"/components/toast/toast"
        }
    }
// pages/mine/mine.wxml
  <toast id="abc" str="hello"></toast>
// pages/mine/mine.js
    Page({
        data: {
        },
        fn(){
            this.toast = this.selectComponent("#abc");
            this.toast.show();
        }
    })

逻辑层

  • 生命周期
  • 路由
  • 模块化

生命周期

page页的生命周期:https://developers.weixin.qq.com/miniprogram/dev/guide/framework/page-life-cycle.html

page页生命周期钩子函数的示例代码:https://developers.weixin.qq.com/miniprogram/dev/reference/api/Page.html

component页的生命周期:https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/lifetimes.html

组件的生命周期,指的是组件自身的一些函数,这些函数在特殊的时间点或遇到一些特殊的框架事件时被自动触发。

其中,最重要的生命周期是 created attached detached ,包含一个组件实例生命流程的最主要时间点。

  • 组件实例刚刚被创建好时, created 生命周期被触发。此时,组件数据 this.data 就是在 Component 构造器中定义的数据 data 。 此时还不能调用 setData 。 通常情况下,这个生命周期只应该用于给组件 this 添加一些自定义属性字段。
  • 在组件完全初始化完毕、进入页面节点树后, attached 生命周期被触发。此时, this.data 已被初始化为组件的当前值。这个生命周期很有用,绝大多数初始化工作可以在这个时机进行。
  • 在组件离开页面节点树后, detached 生命周期被触发。退出一个页面时,如果组件还在页面节点树中,则 detached 会被触发。

自小程序基础库版本 2.2.3 起,组件的的生命周期也可以在 lifetimes 字段内进行声明(这是推荐的方式,其优先级最高)。

    Component({
        lifetimes: {
            attached() {
                // 在组件实例进入页面节点树时执行
            },
            detached() {
                // 在组件实例被从页面节点树移除时执行
            },
        },
        // 以下是旧式的定义方式,可以保持对 <2.2.3 版本基础库的兼容
        attached() {
            // 在组件实例进入页面节点树时执行
        },
        detached() {
            // 在组件实例被从页面节点树移除时执行
        },
        // ...
    })

页面路由

在小程序中所有页面的路由全部由框架进行管理。

页面栈

https://developers.weixin.qq.com/miniprogram/dev/framework/app-service/route.html

打开新页面

调用 API wx.navigateTo

保留当前页面,跳转到应用内的某个页面。但是不能跳到 tabbar 页面。

    wx.navigateTo({
        url: 'test?id=1'
    })
    
    // test.js
    Page({
        onLoad(query) {
            console.log(query)
        }
    })

使用组件

    <navigator open-type="navigateTo" url="test?id=1">跳转</navigator>

页面重定向

调用 API wx.redirectTo

关闭当前页面,跳转到应用内的某个页面。但是不允许跳转到 tabbar 页面。

    wx.redirectTo({
        url: 'test?id=1'
    })

使用组件

   <navigator open-type="redirectTo" url="test?id=1">跳转</navigator>

页面返回

调用 API wx.navigateBack

关闭当前页面,返回上一页面或多级页面。

// 注意:调用 navigateTo 跳转时,调用该方法的页面会被加入堆栈,而 redirectTo 方法则不会。见下方示例代码

// 此处是A页面
    wx.navigateTo({
        url: 'B?id=1'
    })
    
// 此处是B页面
    wx.navigateTo({
        url: 'C?id=1'
    })
    
// 在C页面内 navigateBack,将返回A页面
   wx.navigateBack({
        delta: 2
    })

使用组件

   <navigator open-type="navigateBack">返回</navigator>

用户按左上角返回按钮

Tab 切换

调用 API wx.switchTab

跳转到 tabBar 页面,并关闭其他所有非 tabBar 页面

   {
        "tabBar": {
            "list": [
                {
                    "pagePath": "index",
                    "text": "首页"
                },
                {
                    "pagePath": "other",
                    "text": "其他"
                }
            ]
        }
    }

    wx.switchTab({
        url: '/pages/index/index'
    })

使用组件

    <navigator open-type="switchTab" url="/pages/index/index">跳转</navigator>

用户切换 Tab

重启动

调用 API wx.reLaunch

关闭所有页面,打开到应用内的某个页面

    wx.reLaunch({
        url: 'test?id=1'
    })

使用组件

  <navigator open-type="reLaunch" url="test?id=1">跳转</navigator>

模块化

CommonJS规范

可以将一些公共的代码抽离成为一个单独的 js 文件,作为一个模块。模块通过 module.exports 能对外暴露接口。

// common.js
    function sayHello(name) {
        console.log(`Hello ${name} !`)
    }
    
    module.exports.sayHello = sayHello

    const common = require('common.js')
    Page({
        helloMINA() {
            common.sayHello('MINA')
        }
    })

文件作用域

在 JavaScript 文件中声明的变量和函数只在该文件中有效;不同的文件中可以声明相同名字的变量和函数,不会互相影响。

通过全局函数 getApp 可以获取全局的应用实例,如果需要全局的数据可以在 App 中设置,如:

// app.js
    App({
        globalData: 1
    })
// a.js
    // Get the app instance.
    const app = getApp()
    // Get the global data and change it.
    app.globalData++
// b.js
    // 如果是从a跳转到b,那么这次输出的是2
    console.log(getApp().globalData)

能力

  • 网络
  • 存储
  • 拍照
  • 地图
  • 扫码

网络

服务器域名配置

每个微信小程序需要事先设置一个通讯域名,小程序只可以跟指定的域名与进行网络通信。包括普通 HTTPS 请求(wx.request)、上传文件(wx.uploadFile)、下载文件(wx.downloadFile) 和 WebSocket 通信(wx.connectSocket)

跳过域名校验

在微信开发者工具中,可以临时开启 开发环境不校验请求域名、TLS版本及HTTPS证书 选项,跳过服务器域名的校验。此时,在微信开发者工具中及手机开启调试模式时,不会进行服务器域名的校验。

    wx.request({
        url: 'test.php', // 仅为示例,并非真实的接口地址
        data: {
            x: '',
            y: ''
        },
        header: {
            'content-type': 'application/json' // 默认值
        },
        success(res) {
            console.log(res.data)
        }
    })

https://developers.weixin.qq.com/miniprogram/dev/api/wx.request.html

存储

每个微信小程序都可以有自己的本地缓存

设置

单个 key 允许存储的最大数据长度为 1MB,所有数据存储上限为 10MB。

存储对象和数组这种数据时,获取时也是对象和数组这种数据。

    wx.setStorage({
        key: 'key',
        data: 'value'
    })

    try {
        wx.setStorageSync('key', 'value')
    } catch (e) { }

获取

    wx.getStorage({
        key: 'key',
        success(res) {
            console.log(res.data)
        }
    })

    const value = wx.getStorageSync('key')

清理本地所有数据

    wx.clearStorage({
        success(){}    
    })

    wx.clearStorageSync()

删除指定的key

    wx.removeStorage({
        key: 'key',
        success(res) {
            console.log(res.data)
        }
    })

    wx.removeStorageSync('key')

拍照

https://developers.weixin.qq.com/miniprogram/dev/api/media/image/wx.chooseImage.html

    wx.chooseImage({
        count: 1, // 默认9
        sizeType: ['original', 'compressed'], // 可以指定是原图还是压缩图,默认二者都有
        sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有
        success: function (res) {
            // 返回选定照片的本地文件路径列表,tempFilePath可以作为img标签的src属性显示图片
            var tempFilePaths = res.tempFilePaths
            console.log(res);
        }
    })

地图

https://developers.weixin.qq.com/miniprogram/dev/component/map.html

https://developers.weixin.qq.com/miniprogram/dev/api/media/map/wx.createMapContext.html

https://developers.weixin.qq.com/miniprogram/dev/api/location/wx.chooseLocation.html

   <button bindtap="fn1">设置收货地址</button>
    
    {{ map.address }}
    
    <map
        style="width:100%;height:400rpx"
        latitude="{{map.latitude}}"
        longitude="{{map.longitude}}"
        markers="{{[{latitude:map.latitude,longitude:map.longitude}]}}"
    >
    </map>
    data: {
        map : null
    },
    fn1(){
        // 打开地图,默认为手机当前位置,可以选择位置,确定后得到当前选择的地址。
        wx.chooseLocation({
            success:(res)=>{
                console.log(res)
                // 本地存储
                wx.setStorageSync('map', res);
                // 提示框
                wx.showToast({
                    title: '收货地址设置成功',
                })
            }
        })
    },
    onShow: function () {
        var obj = wx.getStorageSync('map');
        this.setData({
            map : obj
        })
    },

扫码

https://developers.weixin.qq.com/miniprogram/dev/api/device/scan/wx.scanCode.html

    wx.scanCode({
        success(res) {
            console.log(res)
        }
    })

界面交互

显示消息提示框
   wx.showToast({
        title: '成功',
        icon: 'success',
        duration: 2000
    })

模态对话框
   wx.showModal({
        title: '提示',
        content: '这是一个模态弹窗',
        success(res) {
            if (res.confirm) {
                console.log('用户点击确定')
            } else if (res.cancel) {
                console.log('用户点击取消')
            }
        }
    })

loading 提示框

显示 loading 提示框。需主动调用 wx.hideLoading 才能关闭提示框


    wx.showLoading({
        title: '加载中',
    })
    
    setTimeout(function () {
        wx.hideLoading()
    }, 2000)

显示操作菜单

    wx.showActionSheet({
        itemList: ['A', 'B', 'C'],
        success(res) {
            console.log(res.tapIndex)
        },
        fail(res) {
            console.log(res.errMsg)
        }
    })

结束

微信小程序当然还提供其他能力,具体应查看官方文档。

发布了60 篇原创文章 · 获赞 19 · 访问量 5889

猜你喜欢

转载自blog.csdn.net/lff18277233091/article/details/104438779