uni-app

简介

  • 官网
  • uni-app-x
    • uni-app x,是下一代 uni-app。他没有使用js和webview,它基于 uts 语言。在App端,uts在iOS编译为swift、在Android编译为kotlin、web/小程序平台编译为JavaScript
    • uts替代的是js,而uvue替代的就是html和css。可以理解为uts类似dart,而uvue类似flutter
    • uts和ts很相似,但为了跨端,uts进行了一些约束和特定平台的增补;uvue是一套基于uts的、兼容vue语法的、跨iOS和Android的、原生渲染引擎

项目初始化运行及发布

  • 可使用HBuilder创建项目或vue-cli创建项目
    • 发布app必须通过HBuilder,vue-cli可以发布h5/小程序。基于vue-cli创建项目时默认安装了cross-env插件,基于此插件在启动命令前增加了NODE_ENV等参数的配置
    • HBuilder创建的项目默认无package.json,可手动创建或npm init创建,之后可通过npm安装插件
    • HBuilder创建的项目代码对应vue-cli创建项目的src代码
    • vue-cli创建的项目还需手动安装less相关依赖(cnpm install less less-loader -D)或sass(cnpm install sass-loader node-sass -D)
  • 使用vscode进行开发
    • vue create -p dcloudio/uni-preset-vue my-project 创建临时项目(迁移原HBuilder项目也建议先创建一个临时项目进行操作)
    • 选择 Hello uni-app
    • 生成后删除/src下的文件,复制原先项目的文件到/src下
    • 其他参考:https://ask.dcloud.net.cn/article/36286
  • 发布

    • 小程序
      • 发行 - 发行到微信小程序(此时process.env.NODE_ENV才等于production)
      • 配置小程序ID
    • H5

      • 发行 - 网站PC/手机H5
      • 可在manifest.json - Web配置中对路由模式和运行基础路径进行配置(一般留空或者配置成./)
      • 打包后可在unpackage/dist/build/h5找到打包产物
      • 配置nginx

        1
        2
        3
        4
        5
        6
        7
        8
        # 默认路由模式为history. ** 注意: 此时路由会带#,不能去掉 **
        server {
        listen 80;
        server_name www.aezo.cn;
        # 无需配置 location / 和 try_files
        root /home/aezocn/h5;
        index index.html;
        }

文件结构

https://uniapp.dcloud.net.cn/collocation/pages

  • pages.json 文件用来对 uni-app 进行全局配置,决定页面文件的路径、窗口样式、原生的导航栏、底部的原生tabbar 等
    • 它类似微信小程序中app.json的页面管理部分
  • manifest.json 文件是应用的配置文件,用于指定应用的名称、图标、权限(如定位)等

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    // 对应微信小程序的app.json
    "mp-weixin" : {
    "appid" : "wx111111111111111",
    // 部分场景需要增加授权申明 参考: https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/authorize.html
    // 部分涉及到用户隐私的授权(如麦克风获取、相册写入等权限), 还需要在小程序后台添加《用户隐私保护指引》且通过审核, 否则调用权限获取时报错 "fail appid privacy api banned"
    // 否则用户拒绝后在设置中无法找到开启选项
    "permission" : {
    "scope.record" : {
    "desc" : "你的麦克风将用于语音对话"
    }
    },
    },
    // h5模式相关配置(非必须)
    "h5" : {
    // 项目发布在/test-demo1/端点下,只支持发行的时候(开发时运行无效)
    "publicPath" : "/test-demo1/", // 可不用设置
    "router" : {
    "base" : "/test-demo1/"
    },
    // 本地开发配置
    "devServer" : {
    // 防止出现Invalid Host header问题(使用反向代理时出现)
    "disableHostCheck" : true,
    "port" : 80,
    "publicPath": "/test-demo1/", // 本地访问http://localhost/test-demo1/#/
    "proxy" : {
    // 匹配/api-sq/xxx开头的请求,并将其转发到8080/xxx上(pathRewrite去掉了前缀)。可解决跨域问题
    "/api-sq/" : {
    "target" : "http://127.0.0.1:8080/", // 请求的目标地址
    "changeOrigin" : true,
    "secure" : false,
    "pathRewrite" : {
    "^/api-sq" : ""
    }
    }
    }
    },
    "optimization" : {
    "treeShaking" : {
    "enable" : true
    }
    },
    "uniStatistics" : {
    "enable" : false
    }
    }
  • package.json 使用HBuilder编辑创建的项目默认无此文件,可手动创建或npm init创建

    • uni-app在文件中增加uni-app扩展节点,可实现自定义条件编译平台
    • 增加编译时的环境变量,可在package.json文件中增加以下节点,然后再发行-自定义发行菜单中可看到此标题

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      // 方式一
      "scripts": {
      // 使用VUE_APP_开头定义环境类型;此处UNI_PLATFORM等环境变量在代码中无法获取,但是uni-app可以获取进行判断;NODE_ENV不同编译方式不同(production才会进行代码压缩)
      "h5-test": "D: && cd D:/software/HBuilderX/plugins/uniapp-cli && cross-env UNI_INPUT_DIR=$INIT_CWD/ UNI_OUTPUT_DIR=$INIT_CWD/unpackage/dist/build/h5 UNI_PLATFORM=h5 NODE_ENV=testing VUE_APP_NODE_ENV=testing node bin/uniapp-cli.js",
      "h5-prod": "D: && cd D:/software/HBuilderX/plugins/uniapp-cli && cross-env UNI_INPUT_DIR=$INIT_CWD/ UNI_OUTPUT_DIR=$INIT_CWD/unpackage/dist/build/h5 UNI_PLATFORM=h5 NODE_ENV=production VUE_APP_NODE_ENV=production node bin/uniapp-cli.js"
      },
      // 方式二(实测无效)
      "uni-app": {
      "scripts": {
      // 自定义脚本(启动方式),对于内置启动方式可修改 manifest.json
      "build-h5-api1": {
      "title":"生产环境API地址一(H5)",
      "env": {
      "UNI_PLATFORM": "h5",
      "NODE_ENV": "production",
      "APP_ENV": "api1" // 自定义环境变量,实测无效
      }
      }
      }
      }
  • App.vue 是uni-app的主组件,所有页面都是在App.vue下进行切换的,是页面入口文件

    • 作用包括:调用应用生命周期函数、配置全局样式、配置全局的存储globalData。如根据不同访问模式,跳转不同入口页

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
        <script>  
      export default {
      onLaunch: function() {
      console.log('App Launch. 当uni-app 初始化完成时触发,全局只触发一次');
      },
      onShow () {
      console.log('App Show. 当 uni-app 启动,或从后台进入前台显示')

      // #ifdef MP-WEIXIN
      // 微信方式访问,判断是否绑定,若未绑定,跳转欢迎页(进行账号绑定)
      login().then(res => {
      if (res == 'loginUnBind') {
      uni.navigateTo({
      url: '../index/index'
      })
      }
      });
      // #endif

      // #ifdef H5
      // H5模式访问,没有登录则条登录页面
      let token = uni.getStorageSync('token');
      if(token == null || token == "") {
      uni.navigateTo({
      url: './pages/h5Login'
      })
      }
      // #endif
      }
      }
      </script>

知识点

条件编译

  • 条件编译
  • MP为小程序,MP-WEIXIN、MP-ALIPAY
  • APP-PLUS基于HTML5+的JS引擎渲染的,APP-NVUE为App nvue 页面,APP-ANDROID为UTS原生编译方式,APP指所有App平台

生命周期

onLaunch等同步写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// onLaunch: function() {} // 这种写法不能在onLaunch前面使用async, 需写成 onLaunch: async function() {}
async onLaunch() {
await this.initData();
},
async onShow() {
await this.initData();
},
methods: {
// onLaunch onShow 同时调用,会执行多次
async initData() {
await login()
}
}

// 改写 uni.getProvider 等回调函数。uni.getProvider 为回调函数(接收回调,但返回对象不是Promise,无法使用await等特性)
export const login = () => {
// 返回 Promise 对象,从而外部可使用 await login()
return new Promise(resolve => {
uni.getProvider({
service: 'oauth',
success: function(res) {
if (~res.provider.indexOf('weixin')) {
uni.login({
provider: 'weixin',
success: async function(loginRes) {
let code = loginRes.code;
let res = await wxLogin(code)
// 释放,标识执行完成
resolve(res)
}
// 或在then中释放
// success: function(loginRes) {
// let code = loginRes.code;
// wxLogin(code).then(res => {
// resolve(res)
// });
// }
});
}
}
});
})
}
  • onLaunch的执行和所有页面的onLoad/onShow(App.vue和其他任何页面)执行没有先后顺序,有可能onLaunch没执行完,页面级别的onShow/onLoad就执行了。需要达到 onLaunch 中进行同步执行后,再执行 onShow/onLoad 的需求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// 参考:https://www.lervor.com/archives/128/
// main.js
Vue.prototype.$ready = new Promise(resolve => {
Vue.prototype.$emitReady = resolve
})

// App.vue
const login = () => {
postRequest('/m/auth/wxLogin').then(response => {
console.log(response)
})
}

export default {
async onLaunch() {
// #ifdef H5
if(this.$utils.isWeiXinH5()) {
wechat.weChatJsSdkSignature('/m/auth/weChatJsSdkSignature').then(() => {
// await this.h5Login() // 错误写法. 由于 h5Login 并没有返回 Promise 对象,且内部 login 方法并没有同步执行
// this.h5Login2() // 错误写法. 此时 h5Login2 内部 login 方法是同步执行了,但是 async h5Login2 表示 h5Login2 为一个异步函数,返回 Promise,导致当前行和之后代码并没有同步执行
await this.h5Login2() // 正确写法
this.$emitReady() // 释放
})
} else {
this.$emitReady() // else的情况一定也要释放,否则页面可能会卡在await
}
// #endif
},
methods: {
h5Login() {
console.log(1.1)
login()
console.log(1.2) // 不会等login执行完之后再执行
},
async h5Login2() {
console.log(2.1)
await login()
console.log(2.2) // 会等login执行完之后再执行
}
}
}

// Index.vue
export default {
// onLoad 和 onShow需要分别设置,会调用完onLoad就调用onShow(此处的调用完并不等于onLoad要执行完成)
async onLoad(options) {
// options.jsonStr 可获取url中的参数jsonStr(JSON字符串传递时需要encodeURIComponent,此处读取后需要decodeURIComponent再解析成JSON对象)
// switchTab时,options无法获取url中的参数,解决参考:https://segmentfault.com/a/1190000038993623
await this.$ready // 等待释放
// do somthing

// 可使用此方式让onShow中的代码优先执行
// setTimeout(() => {}, 0) // 或者设置成100毫秒
},
async onShow() {
await this.$ready // 等待释放
// do somthing
}
}

路由相关

  • 路由相关: https://uniapp.dcloud.net.cn/tutorial/page.html#%E8%B7%AF%E7%94%B1
    • 页面跳转
      • uni.navigateTo 不关闭之前页面(之前页面代码还会继续执行,导航条显示返回按钮)
      • uni.redirectTo 关闭之前页面,打开新页面(未自定义导航条的情况下,此时首页左上角会显示返回首页按钮,可通过API进行影藏此按钮;没有通过js控制显示返回首页按钮的API)
      • uni.switchTab 关闭之前页面并显示Tab主页
      • uni.reLaunch 关闭之前页面并打开某个页面
      • uni.navigateBack 关闭当前页面,返回上一页面或多级页面
        • H5模式下,浏览器刷新后无法通过uni.navigateBack返回上一页;可使用history对象解决,参考squni.js
    • 路径问题
      • uni.navigateTo可以使用相对路径或绝对路径,就算最终访问路径增加了publicPath等前缀也可使用/pages/xxx的绝对路径
      • 所有的路由都是基于page.json中的路径,注意如果路径中无.vue后缀,则路由时的路径也不能有.vue后缀
    • navigator标签问题
      • 当登录后,通过uni.switchTab进入到首页,首页此时如果是使用<navigator url="../hello">,会导致第一次进入时无法路由
      • 解决方法使用绝对路径<navigator url="/pages/hello">,且不能带.vue后缀
    • 路由挂载:需要跳转的页面必须在page.json中注册过。如需采用 Vue Router 方式管理路由,可在uni-app插件市场找Vue-Router相关插件
  • 跳转小程序(第三方小程序):https://uniapp.dcloud.net.cn/api/other/open-miniprogram.html#navigatetominiprogram
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 跳转小程序
uni.navigateToMiniProgram({
appId: '',
path: 'pages/index/index?id=123',
extraData: {
'data1': 'test'
},
success(res) {
// 打开成功
}
}

// 跳转会上一个小程序: 只有当另一个小程序跳转到当前小程序时才会能调用成功
uni.navigateBackMiniProgram()

// 跳转半屏小程序. 不支持跳转到个人小程序,且绑定的目标小程序最多10个,绑定时需要目标小程序审核
// 官方文档说还需将目标appId配置到 manifest.json -> mp-weixin -> embeddedAppIdList 数组中(跳转兔小巢时没配置也可以)
uni.openEmbeddedMiniProgram({
appId: '',
path: 'pages/main/index'
})

easycom组件规范

  • easycom
  • uni_modules
  • 只要组件安装在项目根目录下的uni_modules或components目录下,并符合components/组件名称/组件名称.vueuni_modules/组件名称/组件名称.vueuni_modules/插件ID/components/组件名称/组件名称.vue目录结构。就可以不用引用(import)、注册,直接在页面中使用

vue/nvue/uvue文件

  • uni-app App 端内置了一个基于 weex 改进的原生渲染引擎,提供了原生渲染能力
    • 在 App 端,如果使用 vue 页面,则使用 webview 渲染;如果使用 nvue 页面(native vue 的缩写),则使用原生渲染
    • 虽然 nvue 也可以多端编译,输出 H5 和小程序,但 nvue 的 css 写法受限,所以如果你不开发 App,那么不需要使用 nvue
    • nvue
  • uvue为uni-app-x的渲染引擎,结合uts可实现原生App的编译,类似Flutter,参考:https://doc.dcloud.net.cn/uni-app-x/

APP

  • 官网原生开发支持文档:https://nativesupport.dcloud.net.cn/
    • uni小程序 SDK:支持在原生App上扩展宿主App的小程序能力,或者用小程序替换原生App的部分功能模块
    • App离线sdk:使用场景是你没有原生App,用DCloud的工具来开发App,又不想使用云打包,则可以使用App离线sdk打包发布为原生App,App离线sdk支持5+ App、uni-app
    • 原生插件开发
  • 打包App的方式
    • 将uni-app(一般只没使用5+的特性,当然如果最终打包成APP也可使用5+特性)或5+项目通过HBuilderX云端打包
    • 将uni-app或5+项目或者离线打包,此时将uni-app项目打包出H5资源再嵌入到安卓项目的资源中去进行AS本地打包
  • HTML5+或plusHTML5+
    • plus不能在浏览器环境下使用,它必须在手机APP上才能使用,因为以安卓为例,他是操纵webview的API
    • WebView是android中一个非常重要的控件,它的作用是用来展示一个web页面,4.4版本之后,直接使用chrome作为内置网页浏览器
    • HTML5+是中国HTML5产业联盟的扩展规范,基于HTML5扩展了大量调用设备的能力,使得web语言可以像原生语言一样强大
  • 调试模拟器网页
    • chrome://inspect/devices可进入设备调试页面,找到对应页面点击inspect进行调试
  • App文件结构
    • /内部存储/Android/data/uni.UNI55836E9
      • /apps/__UNI__55836E9
        • /doc
          • /uniapp_temp 每次启动应用会清空
          • /uniapp_temp_1701010313392 每次启动应用会清空
            • /camera 拍照临时目录
      • /files

App离线sdk模式

  • 官网文档:https://nativesupport.dcloud.net.cn/AppDocs/
    • 支持uni-app、5+ App等项目发行为原生App
    • 提供扩展原生能力
      • uni-app项目扩展原生能力需开发uni原生插件,支持云端打包,有完善的开发者生态插件市场
      • 5+ App项目扩展原生能力需开发5+原生插件,仅支持本地离线打包
      • 5+ 原生插件已不再继续维护,建议开发者升级应用为uni-app项目并使用uni原生插件
    • 官网SDK中的Demo包含了一个集成了uni-app的原生项目(将uni-app打包成h5的模式放到原生项目资源中进行互相调用)
  • 可执行自定义的applicaiton与activity

组件

scroll-view组件

样式

API

多媒体处理

拍照与照片选择

  • APP照片选择返回路径如:file:///storage/emulated/0/Android/data/io.dcloud.HBuilder/apps/HBuilder/doc/uniapp_temp/compressed/1701010324957_1701007865482.png
  • APP拍照返回路径如:_doc/uniapp_temp_1701010313392/camera/1701010368878.jpg
  • 如果将以上路径传到hybrid(APP嵌入的手机本地H5项目)中,则拍照的这种路径无法读取到图片,可使用plus接口获取绝对路径url = plus.io.convertLocalFileSystemURL('_doc/') + url.substring(4)
  • chooseimage
1
2
3
4
5
6
7
8
uni.chooseImage({
count: 9, // 默认9
sizeType: ['compressed'], // original compressed
sourceType: ['camera'], // camera album
success: (res) => {
let tempFilePaths = res.tempFilePaths.map(x => ({ url: x }))
}
});

文件上传(小程序文件选择)

  • uni.uploadFile 文件上传到服务器(本质是做了一个POST请求到后台)。小程序只能从聊天列表中选择文件进行上传,参考下文
    • uni.uploadFile 提交到后台需要使用路径模式(微信小程序为http://tmp/..., App如_doc/…)
    • 不能使用base64或App本地路径 file:///storage/emulated/0/…
  • uni.chooseFile 只支持H5文件选择
  • wx.chooseMessageFile 只支持微信和QQ文件选择
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
wx.chooseMessageFile({
count: 10,
type: 'file', // 默认all包含图片
success: (res) => {
/**
* {
* errMsg: 'chooseMessageFile:ok',
* tempFiles: [{
* name: '测试.docx',
* path: 'http://tmp/jx68w6IX2lTod77e05411a81fce9e7a9086b2352e6e9.xlsx',
* size: 8619,
* time: 1690364448,
* type: 'file'
* }]
* }
*/
}
})

语音处理

语音录制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// manifest.json增加授权申明, 参考[文件结构](#文件结构)
"mp-weixin" : {
// 对应微信小程序的app.json
"permission" : {
"scope.record" : {
"desc" : "你的麦克风将用于语音对话"
}
},
}

async sendAudioMsg() {
let flag = await this.checkRecordAuth()
if (!flag) return
this.recorderManager = wx.getRecorderManager()
this.recorderManager.start({
duration: 60000,
sampleRate: 16000,
numberOfChannels: 1,
format: 'wav'
})
this.recorderManager.onStop((res) => {
// 超时或手动停止都会进入此方法
console.log('recorder stop', res)
// 上传 tempFilePath 文件到后台. 后端接收文件即可
const { tempFilePath } = res
})
},
checkRecordAuth() {
let that = this
return new Promise((resolve, reject) => {
uni.getSetting({
success(res) {
if (!res.authSetting['scope.record']) {
uni.authorize({
scope: 'scope.record',
success() {
that.$squni.toast('授权成功,请重新录音', 'success')
resolve(false)
},
fail(err) {
// authorize:fail auth deny
// 用户拒绝授权后,一段时间是不会跳授权弹框的
console.log(err)
uni.openSetting({
success(res2) {
console.log(res2)
resolve(false)
},
fail(err2) {
// openSetting:fail can only be invoked by user TAP gesture.
console.log(err2)
that.$squni.toast('请在右上角胶囊(···)的设置中开启麦克风权限')
}
})
}
})
} else if (res.authSetting['scope.record'] == true) {
resolve(true)
}
}
})
})
},

语音(合成)播放

  • 案例参考: aezo-chat-gpt(sqt-qingxingyigou)/index.vue
  • 可结合后端调用如ai-soft.md#阿里-语音合成
  • 由于小程序无法实现流式播放,可将后端多个ByteBuffer合成为几个大的ByteBuffer传到小程序端,从而小程序端进行多个ByteBuffer依次播放来实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 语音处理上下文(如果需要播放多个语音,可一个语音一个上下文)
const webAudioContext = wx.createWebAudioContext()
webAudioContext.suspend() // 暂停上下文(暂停播放)
webAudioContext.resume().then(() => {}) // 开始播放
// (页面关闭前)关闭上下文
webAudioContext.close()

// 此处 msg 即为后端返回的ByteBuffer(多个小的合并后的)
const audioBufferArr = []
webAudioContext.decodeAudioData(msg, buffer => {
audioBufferArr.push(buffer)
}, err => {
console.error('decodeAudioData fail', err)
})

// 创建 AudioBufferSourceNode 节点, 可连接到播放节点(播放器)
const audioBufferSource = webAudioContext.createBufferSource()
audioBufferSource.buffer = audioBufferArr[0]
audioBufferSource.onended = (res) => {
// 当前 audioBufferSource 播放结束后执行
// 但是暂停重新播放后,特别是多个音频切换播放的时候,小程序真机onended函数会自动被置空(小程序开发工具正常)
// 解决方案: 记录audioBufferSource播放的位置,下次重新创建audioBufferSource,并结合start offset还原到原播放位置 (由于需要手动计时, 可能会有一点误差)
// 并且在 webAudioContext.suspend() 的时候需要将原 audioBufferSource 清除(disconnect、stop、buffer = null、onended = null), 否则 webAudioContext.resume 的时候可能会出现语音重叠播放的情况
}
// 连接到播放节点并进行播放. IOS需要关闭静音模式(解决方式参考下文)
audioBufferSource.connect(webAudioContext.destination)
audioBufferSource.start() // 支持设置播放位置offset(如小音频文件3s, 可设置从1s处开始播放)
  • 解决(微信小程序)ISO需要关闭静音模式问题
1
2
3
4
5
6
7
8
9
10
const that = this
wx.setInnerAudioOption({
obeyMuteSwitch: false,
success: function (res) {
console.log('开启静音模式下播放音乐的功能', res)
},
fail: function (err) {
// that.$squni.toast('请先关闭手机静音')
}
})

位置

设备

振动

1
2
3
4
// 短振. IOS需要开启声音与触感-系统触感反馈
uni.vibrateShort({})
// 长振
uni.vibrateLong({})

常见业务

web-view开发

多环境编译问题

  • 使用XBuilder开发(一些依赖安装在HBuilder中)
    • 点击运行获取到的process.env.NODE_ENV = 'development',点击发行获取到的是production
      • HBuilder中点击运行或发行便会把此环境对应的API地址编译成微信小程序代码,之后小程序上传的便是此时编译的地址(微信小程序中无法获取process.env.NODE_ENV)。因此发布线上小程序需要点击XBuilder发行
    • 增加编译时的环境变量或使用cross-env定义script,参考上文文件-package.json
  • 使用vscode等开发(vue-cli创建项目,依赖全部安装在项目中)
    • 使用cross-env定义script

微信小程序联系客服

  • 使用button属性: https://uniapp.dcloud.net.cn/component/button.html#button
  • <button open-type="contact">联系客服</button>

    • 必须使用button组件,样式比较丑,可参考

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      <button class="cu-btn cuIcon sm feedback" open-type="contact">
      <view class="cuIcon-service text-green button-icon"></view>
      <text>联系客服</text>
      </button>

      <style>
      .cu-btn {
      display: inline-block;
      background: transparent;
      height: 164upx;
      margin-top: 0;
      border-radius: 0;
      .button-icon {
      margin-top: 42upx;
      }
      }

      button.feedback {
      height: 100rpx;
      font-size: 26rpx;
      width: 50%;
      line-height: 100rpx;
      /* 除去边框 */
      &::after {
      border: none;
      }
      }
      </style>
  • 然后在公众平台->功能->客服绑定对应客户人员微信

引入微信小程序插件

  • 参考:https://uniapp.dcloud.net.cn/tutorial/mp-weixin-plugin.html
  • 案例: 添加快递100小程序进行物流详情查看(插件免费接入,只能显示最新物流信息,详细信息会跳转到快递100小程序),参考文档

    • 需要先在小程序后台添加插件: 第三方设置 - 插件管理 - 添加插件,搜索插件wx6885acbedba59c14添加即可(开发者也可操作添加)
    • manifest.json 增加声明,可能需要重启小程序才能生效

      1
      2
      3
      4
      5
      6
      7
      8
      "mp-weixin": {
      "plugins": {
      "kd100Plugin": {
      "version": "1.0.0",
      "provider": "wx6885acbedba59c14"
      }
      }
      }
    • 调用实例

      1
      2
      3
      4
      5
      6
      uni.navigateTo({
      url: "plugin://kd100Plugin/index?num=SF12345678&appName=测试小程序",
      })

      <!-- 组件调用 -->
      <navigator url="plugin://kd100Plugin/index?num=xxx&appName=xxx"></navigator>
  • 案例: 在小程序中加入企业微信群聊

分包

uni-app和原生小程序混合开发或转换

  • 老项目(原生小程序/原生App)引入uni-app
  • 其他项目转uni-app
  • 老项目引入uni-app实践(uni-app以分包嵌入到微信原生小程序)

    • 将uni-app发行 - 发行为混合分包 - 输入分包名称如sub-demo
    • 在原生小程序的app.json中增加分包配置

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      // pages, 全局usingComponents, permission等配置可参考项目运行到微信小程序编辑器中的app.json(将里面需要的配置复制过来进行部分修改)
      "subpackages": [
      {
      "root": "sub-demo",
      "name": "sub-demo",
      "pages": [
      "pages/main/index",
      "pages/main/history/index"
      ],
      // 是否为独立分包: 独立分包可以独立于主包和其他分包运行,从独立分包中的页面进入小程序时,不需要下载主包。此时主包不存在,App 也不存在,调用 getApp() 获取到的是 undefined
      "independent": false
      }
      ],
      // 全局组件 (在uni-app项目的main.js中使用 Vue.component 注册到全局的组件会自动生成到app.json, 需要复制到原生小程序的app.json并增加分包路径前缀)
      // 这种配置原生小程序控制台会报错, 但是不影响使用. 如果是使用 easycom 模式的组件是无需此步骤的
      "usingComponents": {
      "cu-custom": "/sub-demo/uni_modules/colorui/components/cu-custom"
      },
      // 复制其他如权限等配置
      "permission": {}
    • uni-app项目开发说明

      • 由于发行分包后,uni-app项目的app.json不会生成,因此page.json中的globalStyle不会生成,为了减少对原生小程序的影响,可将globalStyle中的属性写到每个page的style中,如"navigationStyle": "custom"自定义顶部
      • 发行混合分包后,App.vue中的onLaunch会在首次进入分包时触发,此时(第一次)不会触发onShow(正常的uni-app项目第一次onShow也会触发),之后onShow和onHide可正常触发
      • 开发时需要将资源(图片,css,js等)的绝对路径调整为相对路径,使用如../static/logo.png@/common/config.js的模式
      • 页面的绝对路径调整为相对路径,或者使用如this.$squni.navigateTo的自定义跳转方法(自动增加/sub-demo的分包前缀)
      • 页面如果没有自定义样式,也最好增加一下如<style lang="scss">page {}</style>带css内容的style标签,这样才会生成页面wxss文件并且引入项目全局样式common/main.wxss
      • <button class="cu-btn text-blue"> 原生button标签配合colorui类时样式无法生效,改成view标签即可(但是像必须使用原生button标签的场景可能会存在样式问题)

其他

常见问题

  • Hbuilder启动小程序时报错Error: Fail to open IDE
    • 检查启动的小程序AppId, 已经登录的微信号是否有权限访问

兼容性问题

Vue相关语法问题

  • 使用Vue.js注意事项
    • Vue特性支持表
  • 小程序模板中不能直接使用$store和$config等自定义全局属性

    • $store需要通过computed属性映射一次,如果数据比较多可以使用mapState和mapGetters。参考: https://uniapp.dcloud.net.cn/tutorial/vue-vuex.html
    • $config可重新定义到data,从而模板中可进行使用(可放到mixin中)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      import { mapGetters } from 'vuex'

      data() {
      return {
      $config: this.$config,
      }
      },
      computed: {
      ...mapState({
      text: state => state.moduleA.text,
      timestamp: state => state.moduleB.timestamp
      }),
      // 使用 this.timeString
      ...mapGetters([
      'timeString'
      ]),
      // 数组不支持重命名,只能使用map模式重命名. 使用this.len
      mapGetters({
      len: 'childListLen'
      }),
      }
  • 监控路由属性及参数获取

    • uni-app不能watch $route属性,只能通过onShow函数来控制每次显示页面时的动作
    • this.$route.query只能在H5模式下获取到参数,微信小程序无法获取(从onLoad(options)中获取)。兼容性获取方法参考squni.js

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      /**
      * 获取当前页面请求路径
      */
      export const getCurPage = () => {
      // uni-app内置函数
      const pages = getCurrentPages()
      return (pages && pages.length > 0) ? pages[pages.length - 1] : {}
      }
      /**
      * 获取当前页面请求路径所有参数
      */
      export const getCurQueryAll = () => {
      const curPage = getCurPage()
      // 在微信小程序或是app中,通过curPage.options;如果是H5,则需要curPage.$route.query
      return curPage.options || (curPage.$route && curPage.$route.query)
      }

      export const getUrQuery = (name) => {
      return (
      decodeURIComponent(
      (new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(location.href) || [, ''])[1].replace(
      /\+/g, '%20')
      ) || null
      )
      }
  • uniapp编译的微信支持$slots.default的匿名插槽;编译的支付宝不支持,必须定义插槽名称

机型兼容问题

  • IOS 和 Android 对时间的解析有区别 ^1
    • new Date('2018-03-30 12:00:00') IOS 中对于中划线无法解析,Android 可正常解析
      • 解决方案:Date.parse(new Date('2018/03/30 12:00:00')) || Date.parse(new Date('2018-03-30 12:00:00'))
    • uniapp日期选择器在手机上不能选择日期问题(需要设置 start 和 end的属性值)
  • IOS 和 Android 对PDF文件预览的区别
  • uni.setStorageSync 部分(安卓)机型不能同步生效
    • 当设置后返回到上一个页面onShow中读取此数据拿到的仍然是之前的数据,可设置timeout延迟跳转页面
    • 参考:https://ask.dcloud.net.cn/question/88497
    • 也可试下同步存储异步获取
  • 华为输入法输入英文时可能带下划线,导致输入abc结果传到后台只有a
  • input等表单元素的v-model/@input(e.target.value和e.detail.value)都取不到值的问题
    • 小程序中有时候会出现不开调试模式,或者调试模式开启失败(只有性能按钮,没有vConsole按钮)
    • 有时候开启成功也会遇到,生产环境暂未遇到

支付宝和微信小程序兼容问题

  • VUE语法
    • uniapp编译的微信支持$slots.default的匿名插槽;编译的支付宝不支持,必须定义插槽名称
  • 样式问题
    • css单位使用rpx/upx (rem不兼容)
    • 支付宝在 input 组件设置 disabled:true 后组件会被禁用组件颜色会变灰。解决:可使用view代替
    • 支付宝使用伪元素好像有问题(如colorui的picker样式,但是colorui的图片伪元素正常)。解决:通过其他css样式解决
    • 支付宝不支持css的attr方法(如colorui的cu-steps类)
  • colorui插件样式问题

    • 支付宝中picker缺少右侧箭头样式。解决如下

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      <view class="cu-form-group">
      <view class="title">主体名称</view>
      <!-- 在外层套一个flex -->
      <view class="flex justify-end">
      <picker @change="companyChange" :value="companyIndex" :range="companyList" :range-key="'name'">
      <view class="picker">
      {{companyIndex > -1 ? companyList[companyIndex].name : '请选择主体名称'}}
      </view>
      </picker>
      <!-- #ifdef MP-ALIPAY -->
      <view class="cuIcon-right"></view>
      <!-- #endif -->
      </view>
      </view>
    • cu-steps类无效。解决如下

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      <view class="cu-steps">
      <view class="cu-item" v-for="(item,index) in stepList" :key="index">
      <text class="num" :class="['num-' + (index + 1)]" :data-index="index + 1"></text> {{item.name}}
      </view>
      </view>

      <style>
      /* 增加样式如 */
      .cu-steps .cu-item .num.num-1::before,
      .cu-steps .cu-item .num.num-1::after {
      content: "1";
      }
      </style>

样式常见问题

1
2
3
4
5
<!-- 支持小尺寸图片 -->
<image class="cu-avatar round" src="/static/logo.png">

<!-- 此方式仅开发环境有效,如果写成js导入的方式小程序真机是可以的 -->
<view class="cu-avatar round" style="background-image:url('/static/logo.png');"></view>
  • css变量单位
  • 单位换算说明
    • https://uniapp.dcloud.net.cn/tutorial/syntax-css.html#%E5%B0%BA%E5%AF%B8%E5%8D%95%E4%BD%8D
    • em 表示相对尺寸,其相对于当前对象内 (父级元素) 文本的字体尺寸 font-size(如当前对行内文本的字体尺寸未被设置,则相对于浏览器的默认字体尺寸。 任意浏览器的默认字体高都是16px。所有未经调整的浏览器都符合:1em = 16px),如果设置默认尺寸为12px,则1em = 12px
    • rem 为css3新增的一个相对单位,使用rem为元素设定字体大小时,仍然是相对大小,但是rem只相对于HTML根元素的font-size,因此只需要确定这一个font-size
    • 说明
      • rem: 微信小程序和支付宝小程序不兼容
      • 设计稿使用设备宽度750px比较容易计算750px的话1rpx=1px, 这样的话,设计图上量出来的尺寸是多少px就是多少rpx, 至于在不同的设备上实际上要换算成多少个rem就交给小程序自己换算
      • 为了简化font-size的换算,我们通常将rem与em的换算基准设置为 font-size : 62.5%; ,则此时1rem=1em = 16px * 62.5% = 10px, 这样10px = 1em=1rem,方便于我们使用
1
2
3
4
5
6
7
8
9
10
ios: pt
android: dp
web: px、rem、em
H5: rpx (建议)
uniapp: upx (动态绑定的 style 不支持直接使用 upx)

单位换算(正常情况下)
1pt = 1dp = 2px
2rpx = 2upx = 1px
1rem = (750/20)rpx = 37.5rpx = 37.5upx 可适当微调
  • 单位换算案例
1
2
3
4
5
<!-- 此处padding为30rpx,在小程序开发工具里面会变成15px(单数从而导致两张图片中间有间隙),此处改成28rpx就可以了 -->
<view style="padding: 0rpx 30rpx 30rpx 30rpx; width: 100%;">
<image style="width: 100%;display: block;" mode="widthFix" src="https://ossweb-img.qq.com/images/lol/web201310/skin/big10006.jpg"></image>
<image style="width: 100%;display: block;" mode="widthFix" src="https://ossweb-img.qq.com/images/lol/web201310/skin/big10006.jpg"></image>
</view>
  • uni.showToast 被遮盖:全局增加样式uni-toast {z-index: 999999;}
  • 不支持<br/>换行
1
2
3
4
5
6
7
8
<!-- 使用\n的时候,一定是在<text>标签内,如果在<view>标签中,\n并没有折行左右,只是显示一个空格 -->
<text>欢迎\n使用</text>
<!-- 会按照当前看到的排版 -->
<text>
欢迎

使用
</text>
1
2
3
4
5
6
<!-- 不能直接写成 {{ '&ensp;' }} -->
<text decode>{{ blank }}</text>

{
blank: '&ensp;'
}
  • 电脑版文字点击问题
1
2
3
4
5
6
<!-- 最外层的view如果改成text则会导致电脑端小程序无法触发点击事件(手机版是可以的) -->
<view class="inline">
我已阅读并同意
<text class="text-main" style="cursor: pointer;" @tap="navToDetail('用户协议')">用户协议</text>
<text class="text-main" style="cursor: pointer;" @tap="navToDetail('隐私政策')">《隐私政策》</text>
</view>

微信小程序包反编译

1
2
3
4
5
## 参考:https://blog.csdn.net/huagangwang/article/details/135013405
# 先使用windows微信打开小程序,然后通过小程序ID进行解码。提示解密成功,得到 dec.wxapkg
pc_wxapkg_decrypt.exe -wxid wxa04daf3912b3d61e -in "C:\Users\test\Documents\WeChat Files\Applet\wxa04daf3912b3d61e\1\__APP__.wxapkg"
# 反编译
node wuWxapkg.js ../decrypt/dec.wxapkg

其他

  • 小程序图片不显示问题,参考weixin.md
  • 访问出现Invalid Host header问题(使用反向代理时出现,如使用花生壳)
    • 修改uni-app的manifest.json文件 - 源码视图,增加"devServer": {"disableHostCheck" : true}
  • 真机调试
    • IOS需要开放微信访问本地网络

UI框架

ColorUI插件

1
2
3
4
5
6
7
8
9
10
11
12
13
<button class="cu-btn round shadow block cuIcon sm line-red lines-red bg-red bg-gradual-red" loading>
图标: <text class="cuIcon-upload"></text>
加载: <text class="cuIcon-loading2 cuIconfont-spin"></text>
默认: cu-btn
圆角: round
阴影: shadow
无效状态: block
图标按钮: cuIcon(配合图标)
尺寸: sm/默认/lg
镂空边框: line-red | lines-red(线条更粗)
背景: bg-red | bg-gradual-red(渐变色)
原生加载: loading | 图标(如果要防止重复点击需要结合disabled属性). 或者使用uni.showLoading函数
</button>
  • 常用布局
    • flex 水平浮动排列
    • flex flex-direction 垂直浮动排列
    • justify- 左右两边浮动
    • align- 中线对齐
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!--
左右元素顶格,中间元素居中 justify-content: space-between
元素间距相等,整体居中 justify-content: center + gap: 20px
元素及两侧留白均相等 justify-content: space-around 或 padding + gap
-->

<!-- 水平居中 -->
<span class="flex justify-center">
<div>
<span>文字1</span><span>文字2</span>
</div>
</span>

<!-- 左右两边浮动,并中线对齐. 如果没有两边对齐, 可以检查下父元素是否有 style="width: 100%;" -->
<span class="flex justify-between align-center">
<div>显示在左边</div> <span>显示在右边</span>
</span>

<!-- 垂直对齐 -->
<span class="flex flex-direction align-center">
<div>显示在上面</div>
<span>显示在下面</span>
</span>
  • 固定底部按钮
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<view style="margin-bottom: 200rpx;">上方元素设置margin防止遮挡</view>
<view class="flex flex-direction align-center margin-top-sm footer">
<view class="flex text-lg text-bold" style="line-height: 32rpx;">原价 <view style="text-decoration: line-through;">29.9</view></view>
<button class="bg-cyan shadow margin-top-sm" style="width: 100%;" @tap="gotoPay">9.9元(付费开通)</button>
</view>

.footer {
position: fixed;
left: 0;
bottom: 0;
width: 694rpx;
background: #FFFFFF;
padding: 16rpx 30rpx 16rpx 30rpx;
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
box-sizing: content-box;
border-top: 1rpx solid #efefef;
z-index: 999;
}

uView插件

  • uView
  • 安装

    • 通过 https://ext.dcloud.net.cn/plugin?id=1593 导入插件到项目 (uni_modules/uview-ui)
    • 代码配置

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      // main.js
      import uView from '@/uni_modules/uview-ui'
      Vue.use(uView)

      // App.vue 注意 lang="scss"
      <style lang="scss">
      @import "@/uni_modules/uview-ui/index.scss";
      </style>

      // 根目录创建 uni.scss (必须是这个文件名)
      @import '@/uni_modules/uview-ui/theme.scss';
  • u-cell-item使用slot时标题无法增加空格(使用padding解决)

1
2
3
4
5
6
<u-cell-group>
<u-cell-item>
<span slot="title" class="u-p-l-10">退出</span>
<uni-icons slot="icon" type="close" size="17" style="color: #606266;"></uni-icons>
</u-cell-item>
</u-cell-group>

图鸟

插件

自定义插件

富文本/markdown解析

源码解析

Vue3-H5案例解析

  • 先基于cli创建项目

如何解析pages.json文件

  • 根据此流程找到相关原理
    • 如何解析pages.json文件
    • 如何解析manifest.json文件
    • 为啥uniapp cli模板项目中main.ts没有出现app.mount('#app');也可以正常挂载
  • npm run dev:h5 本质执行的是uni命令(可改成uni --debug查看debug日志)
  • vite-plugin-uni/src/index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
export default function uniPlugin(
rawOptions: VitePluginUniOptions = {}
): Plugin[] {
// 初始化环境变量,如:process.env.UNI_INPUT_DIR
initEnv('unknown', { platform: process.env.UNI_PLATFORM || 'h5' })

const options: VitePluginUniResolvedOptions = {
...rawOptions,
base: '/',
assetsDir: 'assets',
inputDir: '',
outputDir: '',
command: 'serve',
platform: 'h5',
}
// ...

// 在vscode中启动h5,因此走 createPlugins(options)
return process.env.UNI_APP_X === 'true' && process.env.UNI_PLATFORM === 'app'
? createUVuePlugins(options)
: createPlugins(options)
}

function createPlugins(options: VitePluginUniResolvedOptions) {
const plugins: Plugin[] = []

// 增加处理 uni-module 目录插件
const injects = parseUniExtApis(
true,
process.env.UNI_UTS_PLATFORM,
'javascript'
)
if (Object.keys(injects).length) {
plugins.push(
uniViteInjectPlugin('uni:ext-api-inject', injects as InjectOptions)
)
}

// 检索uni相关扩展插件,参考: vite-plugin-uni/src/util/plugin.ts#initExtraPlugins
const uniPlugins = initExtraPlugins(
process.env.UNI_CLI_CONTEXT || process.cwd(),
(process.env.UNI_PLATFORM as UniApp.PLATFORM) || 'h5',
options
)
// 打印debug日志. debugUni日志以 uni:plugin 开头
debugUni(uniPlugins)

// 继续包装插件,处理vueJsxPlugin等
// ...
return plugins
}
  • vite-plugin-uni/src/util/plugin.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
export function initExtraPlugins(
cliRoot: string,
platform: UniApp.PLATFORM,
options: VitePluginUniResolvedOptions
) {
// initPlugins 根据检索到的每个插件信息一次调用 initPlugin 方法
return initPlugins(
cliRoot,
// 根据项目package.json的dependencies和devDependencies找到相关依赖列表
// 检索每个依赖的自身的package.json配置,读取package.json中的uni-app节点信息
// 如: 主项目依赖 @dcloudio/uni-h5,而 node_modules/@dcloudio/uni-h5/package.json中存在uni-app节点信息
/*
"uni-app": {
"name": "uni-h5",
"apply": [
"h5"
],
"main": "dist/uni.compiler.js"
}
*/
resolvePlugins(cliRoot, platform, options.uvue),
options
)
}

function initPlugin(
cliRoot: string,
{ id, config: { main } }: PluginConfig,
options: VitePluginUniResolvedOptions
): Plugin | void {
// 导入插件,如对应文件为 node_modules/@dcloudio/uni-h5/dist/uni.compiler.js
// 而此文件引入 @dcloudio/uni-h5-vite 模块
let plugin = require(require.resolve(
path.join(id, main || '/lib/uni.plugin.js'),
{ paths: [cliRoot] }
))
plugin = plugin.default || plugin
if (isFunction(plugin)) {
plugin = plugin(options)
}
return plugin
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// ...
import { uniMainJsPlugin } from './plugins/mainJs'
import { uniManifestJsonPlugin } from './plugins/manifestJson'
import { uniPagesJsonPlugin } from './plugins/pagesJson'
// ...

export default [
// ...
uniMainJsPlugin(),
uniManifestJsonPlugin(),
uniPagesJsonPlugin(),
// ...
]
  • uni-h5-vite/src/plugins/mainJs.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import {
defineUniMainJsPlugin,
isSsr,
PAGES_JSON_JS, // 定义了pages.json的处理器文件 pages-json-js
} from '@dcloudio/uni-cli-shared' // 另外一个包,一些通用方法
import { isSSR, isSsrManifest } from '../utils'

export function uniMainJsPlugin() {
// 返回插件定义
return defineUniMainJsPlugin((opts) => {
let runSSR = false
return {
name: 'uni:h5-main-js', // 插件名称
enforce: 'pre',
configResolved(config) {
runSSR =
isSsr(config.command, config) || isSsrManifest(config.command, config)
},
transform(code, id, options) {
// 如解析到 <script type="module" src="./main.ts"></script> 此时id就是 index.html所在目录+main.ts
// 而opts中判断的是 id == 项目目录/src/main.js | main.ts | main.uts
if (opts.filter(id)) {
if (!runSSR) {
code = code.includes('createSSRApp')
// 如默认模板的main.ts中包含`import { createSSRApp } from "vue";`,因此走的此此方法
? createApp(code)
// 如果自定义成 `import { createApp } from "vue";` 则走此方法
: createLegacyApp(code)
} else {
code = isSSR(options)
? createSSRServerApp(code)
: createSSRClientApp(code)
}
// 导入pages.json文件的执行语句
code = `import './${PAGES_JSON_JS}';${code}`
return {
code,
map: this.getCombinedSourcemap(),
}
}
},
}
})
}

function createApp(code: string) {
// uniapp cli模板项目中main.ts中就是通过createSSRApp进行创建示例的
// 此处可以看出来本质还是调用的 createVueApp
// 并通过 createApp 来进行挂载实例
return `import { plugin as __plugin } from '@dcloudio/uni-h5';${code.replace(
'createSSRApp',
'createVueApp as createSSRApp'
)};createApp().app.use(__plugin).mount("#app");`
}
  • uni-h5-vite/src/plugins/pagesJson.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
import {
API_DEPS_CSS,
FEATURE_DEFINES,
H5_FRAMEWORK_STYLE_PATH,
BASE_COMPONENTS_STYLE_PATH,
normalizeIdentifier,
normalizePagesJson,
defineUniPagesJsonPlugin, // 里面根据 jsonPath = normalizePath(path.join(process.env.UNI_INPUT_DIR, 'pages.json')) 获取文件
normalizePagesRoute,
normalizePagePath,
isEnableTreeShaking,
parseManifestJsonOnce,
MANIFEST_JSON_JS,
} from '@dcloudio/uni-cli-shared'

export function uniPagesJsonPlugin(): Plugin {
return defineUniPagesJsonPlugin((opts) => {
return {
name: 'uni:h5-pages-json',
enforce: 'pre',
transform(code, id, opt) {
if (opts.filter(id)) {
const { resolvedConfig } = opts
const ssr = isSSR(opt)
return {
code:
registerGlobalCode(resolvedConfig, ssr) +
// 根据pages.json生成代码,此处code即为对应文件json字符串
generatePagesJsonCode(ssr, code, resolvedConfig),
map: { mappings: '' },
}
}
},
}
})
}

// 根据pages.json拼接代码
function generatePagesJsonCode(
ssr: boolean | undefined,
jsonStr: string,
config: ResolvedConfig
) {
const globalName = getGlobal(ssr)
const pagesJson = normalizePagesJson(jsonStr, process.env.UNI_PLATFORM)
const { importLayoutComponentsCode, defineLayoutComponentsCode } =
generateLayoutComponentsCode(globalName, pagesJson)
const definePagesCode = generatePagesDefineCode(pagesJson, config)
const uniRoutesCode = generateRoutes(globalName, pagesJson, config)
const uniConfigCode = generateConfig(globalName, pagesJson, config)
const cssCode = generateCssCode(config)

return `
import { defineAsyncComponent, resolveComponent, createVNode, withCtx, openBlock, createBlock } from 'vue'
import { PageComponent, useI18n, setupWindow, setupPage } from '@dcloudio/uni-h5'
import { appId, appName, appVersion, appVersionCode, debug, networkTimeout, router, async, sdkConfigs, qqMapKey, googleMapKey, aMapKey, aMapSecurityJsCode, aMapServiceHost, nvue, locale, fallbackLocale, darkmode, themeConfig } from './${MANIFEST_JSON_JS}'
const locales = import.meta.globEager('./locale/*.json')
${importLayoutComponentsCode}
const extend = Object.assign
${cssCode}
${uniConfigCode}
${defineLayoutComponentsCode}
${definePagesCode}
${uniRoutesCode}
${config.command === 'serve' ? hmrCode : ''}
export {}
`
}

// 设置window.__uniConfig
function generateConfig(globalName, pagesJson, config) {
delete pagesJson.pages;
delete pagesJson.subPackages;
delete pagesJson.subpackages;
pagesJson.compilerVersion = process.env.UNI_COMPILER_VERSION;
return `${globalName}.__uniConfig=extend(${JSON.stringify(pagesJson)},{
appId,
appName,
appVersion,
appVersionCode,
async,
debug,
networkTimeout,
sdkConfigs,
qqMapKey,
googleMapKey,
aMapKey,
aMapSecurityJsCode,
aMapServiceHost,
nvue,
locale,
fallbackLocale,
locales:Object.keys(locales).reduce((res,name)=>{const locale=name.replace(/\\.\\/locale\\/(uni-app.)?(.*).json/,'$2');extend(res[locale]||(res[locale]={}),locales[name].default);return res},{}),
router,
darkmode,
themeConfig,
})
`;
}

参考文章

ChatGPT开源小程序