uniapp 是一个使用 vue.js 开发前端应用的框架,实现一套代码多端运行的效果,由 DCloud 团队开发维护。基于上半年都使用了 uniapp 开发小程序,这次来探讨下小程序的跨平台方案是怎么实现的

基础知识

node 包管理基础

npm 是 node 包管理工具,项目根目录下的 package.json 是 npm 的配置文件,且包都存放在 node_modules 下,结构如下图:

  1. .
  2. |-- dist
  3. |-- public
  4. | `-- index.html
  5. |-- node_modules
  6. | |-- .bin
  7. | | `-- vue-cli-service
  8. | `-- vue
  9. |-- src
  10. | |-- platforms
  11. | |-- `-- mp-weixin
  12. | |-- pages
  13. | |-- wxcomponents
  14. | |-- App.vue
  15. | |-- main.js
  16. | |-- manifest.json
  17. | |-- pages.json
  18. | `-- uni.scss
  19. |-- test
  20. | `-- usTimezone.test.js
  21. |-- README.md
  22. |-- package.json
  23. `-- vue.config.js

其中每个项目都会有独立的拥有一份依赖,运行 package.scripts 里面的命令则运行脚本

  1. {
  2. "name": "weniuprj",
  3. "version": "0.1.0",
  4. "private": true,
  5. "bin": {
  6. "vue-cli-service": "bin/vue-cli-service.js"
  7. },
  8. "scripts": {
  9. "serve": "npm run dev:mp-weixin",
  10. "build": "npm run build:mp-weixin",
  11. "lint": "vue-cli-service lint",
  12. "build:mp-qq": "cross-env NODE_ENV=production UNI_PLATFORM=mp-qq vue-cli-service uni-build",
  13. "build:mp-toutiao": "cross-env NODE_ENV=production UNI_PLATFORM=mp-toutiao vue-cli-service uni-build",
  14. "build:mp-weixin": "cross-env NODE_ENV=production UNI_PLATFORM=mp-weixin vue-cli-service uni-build",
  15. // ...
  16. "dev:mp-qq": "cross-env NODE_ENV=development UNI_PLATFORM=mp-qq vue-cli-service uni-build --watch",
  17. "dev:mp-toutiao": "cross-env NODE_ENV=development UNI_PLATFORM=mp-toutiao vue-cli-service uni-build --watch",
  18. "dev:mp-weixin": "cross-env NODE_ENV=development UNI_PLATFORM=mp-weixin vue-cli-service uni-build --watch",
  19. "info": "node node_modules/@dcloudio/vue-cli-plugin-uni/commands/info.js",
  20. "test:mp-weixin": "cross-env UNI_PLATFORM=mp-weixin jest -i"
  21. },
  22. "dependencies": {},
  23. "devDependencies": {}
  24. }

webpack 基本介绍

webpack 是一个用于现代 JavaScript 应用程序的 静态模块打包工具。当 webpack 处理应用程序时,它会在内部从一个或多个入口点构建一个 依赖图(dependency graph),然后将你项目中所需的每一个模块组合成一个或多个 bundles,它们均为静态资源,用于展示你的内容。

image.png

核心概念:

  • 入口(entry)
  • 输出(output)
  • loader
  • 插件(plugin)
  • 浏览器兼容(brower compatibility)

Uniapp

命令

通过执行不同的构建命令实现设置环境变量,实现配置和产物的不同

  1. cross-env NODE_ENV=production UNI_PLATFORM=h5 vue-cli-service uni-build
  2. cross-env NODE_ENV=development UNI_PLATFORM=mp-weixin vue-cli-service uni-build --watch

vue-cli

vue 框架的脚手架,提供 修改 webpack 配置、vue-cli-service 命令、创建项目等功能
image.png
执行 vue-cli-service 命令时,会自动加载 package.json 下所有名字为 vue-cli-plugin-<name> 的 server 插件,即该 npm 包下的 index.js,通过 server 插件去修改 webpack 配置、添加或修改 cli-service 命令,上面的
uni-build 命令就是通过改方法实现

  1. // packages/vue-cli-plugin-uni/index.js
  2. const initBuildCommand = require('./commands/build')
  3. const initServeCommand = require('./commands/serve')
  4. module.exports = (api, options) => {
  5. initServeCommand(api, options)
  6. // 注册 uni-build 命令
  7. initBuildCommand(api, options)
  8. // ...
  9. api.configureWebpack(require('./lib/configure-webpack')(platformOptions, manifestPlatformOptions, options, api))
  10. api.chainWebpack(require('./lib/chain-webpack')(platformOptions, options, api))
  11. // ...
  12. }
  13. // packages/vue-cli-plugin-uni/commands/build.js
  14. module.exports = (api, options) => {
  15. api.registerCommand('uni-build', {
  16. description: 'build for production',
  17. usage: 'vue-cli-service uni-build [options]',
  18. options: {
  19. '--watch': 'watch for changes',
  20. '--minimize': 'Tell webpack to minimize the bundle using the TerserPlugin.',
  21. '--auto-host': 'specify automator host',
  22. '--auto-port': 'specify automator port',
  23. '--subpackage': 'specify subpackage',
  24. '--plugin': 'specify plugin',
  25. '--manifest': 'build manifest.json'
  26. }
  27. }, async (args) => {
  28. // 命令执行内容,即打包小程序
  29. });
  30. };

构建入口

将通过不同命令,执行不同入口

  1. // @dcloudio/vue-cli-plugin-uni/lib/mp.js
  2. module.exports = {
  3. webpackConfig () {
  4. return {
  5. entry () {
  6. return process.UNI_ENTRY
  7. },
  8. // ... 其他配置
  9. module: {
  10. rules: [{
  11. // 自定义 main.js 解析 loader
  12. test: path.resolve(process.env.UNI_INPUT_DIR, getMainEntry()),
  13. use: [{
  14. loader: '@dcloudio/webpack-uni-mp-loader/lib/main'
  15. }]
  16. }],
  17. },
  18. };
  19. }
  20. }

image.png

配置文件解析

manifest.json

用来生成小程序配置文件 project.config.json ,uniapp 通过 webpack-uni-pages-loader 去读取对应 platforms 下配置参数兼容不同的小程序

pages.json

uniapp 所有页面配置都在 pages.json 里面,构建入口都是 src/main.js 文件,通过不同 webpack-uni-mp-loader 去实现不同页面的构建

  1. // packages/webpack-uni-pages-loader/lib/index.js
  2. module.exports = function (content, map) {
  3. // 获取 mainfest.json 和 pages 路径
  4. const manifestJsonPath = path.resolve(process.env.UNI_INPUT_DIR, 'manifest.json')
  5. const pagesJsonJsPath = path.resolve(process.env.UNI_INPUT_DIR, pagesJsonJsFileName)
  6. const manifestJson = parseManifestJson(fs.readFileSync(manifestJsonPath, 'utf8'))
  7. this.addDependency(manifestJsonPath)
  8. this.addDependency(pagesJsonJsPath)
  9. const pagesJson = parsePagesJson()
  10. // ... 其余代码
  11. // 各平台代码默认配置兼容
  12. const jsonFiles = require('./platforms/' + process.env.UNI_PLATFORM)(pagesJson, manifestJson)
  13. // 输出 json
  14. changedEmitFiles.forEach(name => {
  15. this.emitFile(name + '.json', emitFileCaches[name])
  16. })
  17. }

条件编译

通过使用 ifdef … endif 将不同代码打包到不同平台,使各端代码无冗余代码,如以下代码:

  1. // src/pages/index/index
  2. import units, { showTip } from '../../utils';
  3. export default {
  4. data() {
  5. return {
  6. title: 'Hello',
  7. };
  8. },
  9. onLoad() {
  10. uni.showToast({ title: 'test' });
  11. // #ifdef MP-WEIXIN
  12. setTimeout(() => units.showTip('一秒延时'), 1000);
  13. // #endif
  14. // #ifdef MP-ALIPAY
  15. setTimeout(() => showTip('两秒延时'), 2000);
  16. // #endif
  17. },
  18. methods: {},
  19. };

实现逻辑为 webpack 的一个 loader 插件,将所有文件内容都通过配置的正则规则将条件编译代码去除,代码会依次由:

  • main.js
  • app.vue#JavaScript
  • pages.vue#javaScript
  • app.vue#html
  • pages.vue#html
  • app.vue#css
  • pages.vue#css ```javascript // packages/vue-cli-plugin-uni/packages/webpack-preprocess-loader/index.js function() { try { content = preprocessor.preprocess(content, context, {
    1. type
    }) } catch (e) {
    1. console.error('异常提醒')
    } }

// packages/vue-cli-plugin-uni/packages/webpack-preprocess-loader/preprocess/lib/regexrules.js const regexrules = { html: { if: { start: ‘[ \t]<!—[ \t]#(ifndef|ifdef|if)[ \t]+(.?)[ \t](?:—>|!>)(?:[ \t]\n+)?’, end: ‘[ \t]<!(?:—)?[ \t]#endif[ \t](?:—>|!>)(?:[ \t]\n)?’ }, }, js: { if: { start: ‘[ \t](?://|/\)[ \t]#(ifndef|ifdef|if)[ \t]+([^\n])(?:\(?:\|/))?(?:[ \t]\n+)?’, end: ‘[ \t](?://|/\)[ \t]#endif[ \t](?:\(?:\|/))?(?:[ \t]\n)?’ }, } }

  1. 将上面代码编译为以下结果<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/139368/1638412766114-562be3f2-b0cf-46f4-bc34-0eb9a3117b07.png#clientId=u8f84b82c-ae77-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=205&id=uaeb19765&margin=%5Bobject%20Object%5D&name=image.png&originHeight=205&originWidth=763&originalType=binary&ratio=1&rotation=0&showTitle=false&size=27401&status=done&style=none&taskId=ud00c99a0-5a83-440a-b04c-ae92b178287&title=&width=763)
  2. <a name="R4joM"></a>
  3. ### 模板转换
  4. uniapp 通过 `mp-vue` `template` 代码解析为 AST 树,如 demo 代码
  5. ```html
  6. <view class="content">
  7. <image class="logo" src="/static/logo.png"></image>
  8. <view>
  9. <text class="title">{{ title }}</text>
  10. </view>
  11. <view>sdfasdfadsf</view>
  12. </view>

image.png
然后使用根据不同平台的环境变量生成不同的编译器,将 AST 树转为对应的小程序类型输出

  1. // uni-template-compiler/lib/index.js
  2. module.exports = {
  3. compile (source, options = {}) {
  4. // ...
  5. options.mp.platform = require('./mp')(options.mp.platform)
  6. const state = { options };
  7. // ...
  8. try {
  9. res.render = generateScript(traverseScript(ast, state), state)
  10. template = generateTemplate(traverseTemplate(ast, state), state)
  11. } catch (e) {
  12. console.error(e)
  13. throw new Error('Compile failed at ' + options.resourcePath.replace(
  14. path.extname(options.resourcePath),
  15. '.vue'
  16. ))
  17. }
  18. // ...
  19. }
  20. }
  21. // uni-template-compiler/lib/mp.js
  22. module.exports = function getCompilerOptions (platform) {
  23. let id = '@dcloudio/uni-' + platform
  24. return Object.assign({
  25. name: platform
  26. },
  27. // 基础类型及组件转换
  28. baseCompiler,
  29. require(id + '/lib/uni.compiler.js')
  30. )
  31. }
  32. // uni-mp-alipay/lib/uni.compiler.js
  33. module.exports = {
  34. // 事件处理机制,例如将 click 转为小程序 tap 事件
  35. getEventType(eventType) {},
  36. }

通过 WebpackUniMPPlugin 插件实现生成 WXML 代码

  1. // webpack-uni-mp-loader/lib/plugin/index-new.js
  2. class WebpackUniMPPlugin {
  3. apply (compiler) {
  4. // 生成 json
  5. generateJson(compilation)
  6. // app.js,app.wxss
  7. generateApp(compilation)
  8. .forEach(({
  9. file,
  10. source
  11. }) => emitFile(file, source, compilation))
  12. // 生成页面 将 div 转为 view
  13. generateComponent(compilation, compiler.options.output.jsonpFunction)
  14. }
  15. }

API 兼容

uniapp 以微信小程序的 API 为标准,将其他将其余平台的 API 通过代理的方式统一代理到 uni 这个全局对象上面,以支付宝小程序为例

  1. // @dcloudio/uni-mp-alipay/dist/index.js
  2. const protocols = {
  3. showToast ({
  4. icon = 'success'
  5. } = {}) {
  6. const args = {
  7. title: 'content',
  8. icon: 'type',
  9. duration: false,
  10. image: false,
  11. mask: false
  12. };
  13. if (icon === 'loading') {
  14. return {
  15. name: 'showLoading',
  16. args
  17. }
  18. }
  19. return {
  20. name: 'showToast',
  21. args
  22. }
  23. },
  24. };
  25. // 遍历代理到 uni 对象下
  26. Object.keys(my).forEach(name => {
  27. if (hasOwn(my, name) || hasOwn(protocols, name)) {
  28. // warper 函数做了错误判断及警告
  29. uni[name] = promisify(name, wrapper(name, my[name]));
  30. }
  31. });

自定义组件和静态资源

uniapp 指定工程的目录结构,约束用户通过不同平台的代码或者资源放到对应指定目录下

  • 微信小程序原生组件放到 src/wxcomponents 目录下
  • 不同代码放到 src/platforms/mp-weixin
  • 静态资源放到 src/static/mp-weixin

通过使用 copy-webpack-plugin 插件将代码复制到 dist

  1. // packages/vue-cli-plugin-uni/lib/copy-webpack-options.js
  2. function getCopyWebpackPluginOptions (platformOptions, vueOptions) {
  3. const copyOptions = getAssetsCopyOptions(assetsDir).concat(
  4. getUniModulesAssetsCopyOptions(assetsDir)
  5. )
  6. global.uniPlugin.copyWebpackOptions.forEach(copyWebpackOptions => {
  7. const platformCopyOptions =
  8. copyWebpackOptions(platformOptions, vueOptions, copyOptions) || []
  9. platformCopyOptions.forEach(copyOption => {
  10. if (typeof copyOption === 'string') {
  11. copyOption = getAssetsCopyOption(copyOption)
  12. }
  13. copyOption && copyOptions.push(copyOption)
  14. })
  15. })
  16. // ...
  17. return copyOptions
  18. }

总结

uniapp 基于 vue-cli 对多端构建的配置进行封装,通过条件编译实现代码分端处理,大大减少了小程序开发成本,代码逻辑结构也值得学习。