Skip to content

Global Configuration ​

In uni-app development, global configuration refers to application-level settings, including basic application information, page routing, window styles, permission requests, etc. This guide will detail global configuration methods and common configuration items in uni-app.

pages.json ​

pages.json is the global configuration file for uni-app projects, used to configure uni-app globally, determining page file paths, window styles, native navigation bars, native bottom tabbar, etc.

Basic Structure ​

A basic pages.json file structure looks like this:

json
{
  "pages": [{
    "path": "pages/index/index",
    "style": {
      "navigationBarTitleText": "Home"
    }
  }, {
    "path": "pages/detail/detail",
    "style": {
      "navigationBarTitleText": "Detail"
    }
  }],
  "globalStyle": {
    "navigationBarTextStyle": "white",
    "navigationBarTitleText": "uni-app",
    "navigationBarBackgroundColor": "#007AFF",
    "backgroundColor": "#F8F8F8"
  },
  "tabBar": {
    "color": "#7A7E83",
    "selectedColor": "#007AFF",
    "borderStyle": "black",
    "backgroundColor": "#ffffff",
    "list": [{
      "pagePath": "pages/index/index",
      "iconPath": "static/tab-home.png",
      "selectedIconPath": "static/tab-home-active.png",
      "text": "Home"
    }, {
      "pagePath": "pages/mine/mine",
      "iconPath": "static/tab-mine.png",
      "selectedIconPath": "static/tab-mine-active.png",
      "text": "Mine"
    }]
  }
}

Main Configuration Items ​

1. pages ​

The pages array is used to configure application page routes, with each item being an object corresponding to a page.

json
"pages": [{
  "path": "pages/index/index", // Page path
  "style": {                   // Page window appearance
    "navigationBarTitleText": "Home", // Navigation bar title
    "enablePullDownRefresh": true     // Enable pull-to-refresh
  }
}]

TIP

The first item in the pages array represents the application's default startup page.

2. globalStyle ​

globalStyle is used to set the application's global styles, which will apply to every page. If a page has specific style requirements, it can be configured separately in the page's style, which will override global styles.

json
"globalStyle": {
  "navigationBarTextStyle": "white",        // Navigation bar title color, only supports black/white
  "navigationBarTitleText": "uni-app",      // Navigation bar title text
  "navigationBarBackgroundColor": "#007AFF", // Navigation bar background color
  "backgroundColor": "#F8F8F8",             // Window background color
  "backgroundTextStyle": "light",           // Pull-to-refresh background font and loading icon style, only supports dark/light
  "enablePullDownRefresh": false,           // Enable pull-to-refresh
  "usingComponents": {}                     // Reference mini-program components
}

3. tabBar ​

tabBar is used to configure the application's bottom tab bar, with a minimum of 2 and maximum of 5 tabs.

json
"tabBar": {
  "color": "#7A7E83",            // Default text color on tabs
  "selectedColor": "#007AFF",    // Text color when tab is selected
  "borderStyle": "black",        // Color of tabbar top border, optional values: black/white
  "backgroundColor": "#ffffff",  // Tab background color
  "list": [{                     // Tab list, minimum 2, maximum 5
    "pagePath": "pages/index/index", // Page path
    "iconPath": "static/tab-home.png", // Image path, recommended size 81px * 81px
    "selectedIconPath": "static/tab-home-active.png", // Selected image path
    "text": "Home"               // Tab button text
  }],
  "position": "bottom"           // Optional values: bottom, top
}

4. condition ​

condition is used to enable specific page scenario values for development and debugging convenience.

json
"condition": {
  "current": 0,                  // Currently active mode (list index)
  "list": [{
    "name": "Detail Page",       // Mode name
    "path": "pages/detail/detail", // Startup page
    "query": "id=1"              // Startup parameters
  }]
}

5. subPackages ​

subPackages is used for subpackage loading to optimize mini-program startup speed.

json
"subPackages": [{
  "root": "pagesA",              // Subpackage root directory
  "pages": [{                    // Subpackage page paths
    "path": "list/list",
    "style": {
      "navigationBarTitleText": "List"
    }
  }]
}]

6. preloadRule ​

preloadRule is used to configure subpackage preload rules.

json
"preloadRule": {
  "pages/index/index": {
    "network": "all",            // Preload under specified network, optional values: all (no network limit), wifi (only under wifi)
    "packages": ["pagesA"]       // Subpackage root or name to preload after entering page
  }
}

7. easycom ​

easycom is used to configure component auto-import rules, eliminating the need to manually import components in pages.

json
"easycom": {
  "autoscan": true,              // Enable automatic scanning
  "custom": {                    // Custom scanning rules
    "^u-(.*)": "@/uview-ui/components/u-$1/u-$1.vue"
  }
}

manifest.json ​

manifest.json is the application configuration file used to specify application name, icon, permissions, and other information.

Basic Structure ​

json
{
  "name": "Application Name",
  "appid": "",
  "description": "Application Description",
  "versionName": "1.0.0",
  "versionCode": "100",
  "transformPx": false,
  "app-plus": {
    "usingComponents": true,
    "nvueCompiler": "uni-app",
    "splashscreen": {
      "alwaysShowBeforeRender": true,
      "waiting": true,
      "autoclose": true,
      "delay": 0
    },
    "modules": {},
    "distribute": {
      "android": {
        "permissions": [
          "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>"
        ]
      },
      "ios": {},
      "sdkConfigs": {}
    }
  },
  "quickapp": {},
  "mp-weixin": {
    "appid": "WeChat mini-program appid starting with wx",
    "setting": {
      "urlCheck": false
    },
    "usingComponents": true
  },
  "mp-alipay": {
    "usingComponents": true
  },
  "mp-baidu": {
    "usingComponents": true
  },
  "mp-toutiao": {
    "usingComponents": true
  },
  "h5": {
    "router": {
      "base": "/"
    }
  }
}

Main Configuration Items ​

1. Basic Information ​

json
{
  "name": "Application Name",
  "appid": "",
  "description": "Application Description",
  "versionName": "1.0.0",
  "versionCode": "100"
}

2. app-plus (App-specific Configuration) ​

json
"app-plus": {
  "usingComponents": true,
  "nvueCompiler": "uni-app",
  "splashscreen": {
    "alwaysShowBeforeRender": true,
    "waiting": true,
    "autoclose": true,
    "delay": 0
  },
  "modules": {
    "Payment": {},
    "Push": {},
    "Share": {},
    "OAuth": {}
  },
  "distribute": {
    "android": {
      "permissions": [
        "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>"
      ],
      "minSdkVersion": 21,
      "targetSdkVersion": 28,
      "abiFilters": ["armeabi-v7a", "arm64-v8a"]
    },
    "ios": {
      "dSYMs": false,
      "idfa": false
    },
    "sdkConfigs": {
      "ad": {},
      "maps": {},
      "oauth": {},
      "payment": {},
      "push": {},
      "share": {}
    },
    "icons": {
      "android": {
        "hdpi": "unpackage/res/icons/72x72.png",
        "xhdpi": "unpackage/res/icons/96x96.png",
        "xxhdpi": "unpackage/res/icons/144x144.png",
        "xxxhdpi": "unpackage/res/icons/192x192.png"
      },
      "ios": {
        "appstore": "unpackage/res/icons/1024x1024.png",
        "ipad": {
          "app": "unpackage/res/icons/76x76.png",
          "app@2x": "unpackage/res/icons/152x152.png",
          "notification": "unpackage/res/icons/20x20.png",
          "notification@2x": "unpackage/res/icons/40x40.png",
          "proapp@2x": "unpackage/res/icons/167x167.png",
          "settings": "unpackage/res/icons/29x29.png",
          "settings@2x": "unpackage/res/icons/58x58.png",
          "spotlight": "unpackage/res/icons/40x40.png",
          "spotlight@2x": "unpackage/res/icons/80x80.png"
        },
        "iphone": {
          "app@2x": "unpackage/res/icons/120x120.png",
          "app@3x": "unpackage/res/icons/180x180.png",
          "notification@2x": "unpackage/res/icons/40x40.png",
          "notification@3x": "unpackage/res/icons/60x60.png",
          "settings@2x": "unpackage/res/icons/58x58.png",
          "settings@3x": "unpackage/res/icons/87x87.png",
          "spotlight@2x": "unpackage/res/icons/80x80.png",
          "spotlight@3x": "unpackage/res/icons/120x120.png"
        }
      }
    }
  }
}

3. Mini-program Platform Configuration ​

json
"mp-weixin": {
  "appid": "WeChat mini-program appid starting with wx",
  "setting": {
    "urlCheck": false,
    "es6": true,
    "postcss": true,
    "minified": true
  },
  "usingComponents": true,
  "permission": {
    "scope.userLocation": {
      "desc": "Your location information will be used for location interface effects in the mini-program"
    }
  }
}

4. H5 Configuration ​

json
"h5": {
  "title": "Application Name",
  "router": {
    "mode": "history",
    "base": "/"
  },
  "devServer": {
    "https": false,
    "port": 8080,
    "disableHostCheck": true,
    "proxy": {
      "/api": {
        "target": "http://localhost:3000",
        "changeOrigin": true,
        "secure": false,
        "pathRewrite": {
          "^/api": ""
        }
      }
    }
  },
  "optimization": {
    "treeShaking": {
      "enable": true
    }
  },
  "template": "index.html"
}

uni.setStorage and getStorage ​

In addition to configuration files, uni-app provides runtime global data storage methods that can share data between different pages of the application.

Store Data ​

javascript
// Synchronous storage
try {
  uni.setStorageSync('key', 'value')
} catch (e) {
  console.error(e)
}

// Asynchronous storage
uni.setStorage({
  key: 'key',
  data: 'value',
  success: function() {
    console.log('Storage successful')
  },
  fail: function(err) {
    console.error('Storage failed', err)
  }
})

Get Data ​

javascript
// Synchronous get
try {
  const value = uni.getStorageSync('key')
  if (value) {
    console.log(value)
  }
} catch (e) {
  console.error(e)
}

// Asynchronous get
uni.getStorage({
  key: 'key',
  success: function(res) {
    console.log(res.data)
  },
  fail: function(err) {
    console.error('Get failed', err)
  }
})

Remove Data ​

javascript
// Synchronous remove
try {
  uni.removeStorageSync('key')
} catch (e) {
  console.error(e)
}

// Asynchronous remove
uni.removeStorage({
  key: 'key',
  success: function() {
    console.log('Remove successful')
  }
})

Clear All Data ​

javascript
// Synchronous clear
try {
  uni.clearStorageSync()
} catch (e) {
  console.error(e)
}

// Asynchronous clear
uni.clearStorage({
  success: function() {
    console.log('Clear successful')
  }
})

getApp() and getCurrentPages() ​

uni-app provides global application instance and page instance retrieval methods.

getApp() ​

getApp() is used to get the current application instance and can be used to get and set global data.

javascript
const app = getApp()

// Set global data
app.globalData = {
  userInfo: null
}

// Get global data
console.log(app.globalData.userInfo)

getCurrentPages() ​

getCurrentPages() is used to get the current page stack instance, where the first element in the array is the home page and the last element is the current page.

javascript
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const prevPage = pages[pages.length - 2]

// Get current page path
console.log(currentPage.route)

// Get current page parameters
console.log(currentPage.options)

// Call previous page method
if (prevPage) {
  prevPage.refreshData()
}

Vuex State Management ​

For complex applications, Vuex is recommended for state management.

Install Vuex ​

bash
npm install vuex --save

Create Store ​

javascript
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    count: 0,
    userInfo: null
  },
  mutations: {
    increment(state) {
      state.count++
    },
    setUserInfo(state, userInfo) {
      state.userInfo = userInfo
    }
  },
  actions: {
    login({ commit }, userInfo) {
      return new Promise((resolve, reject) => {
        // Simulate login request
        setTimeout(() => {
          commit('setUserInfo', userInfo)
          resolve()
        }, 1000)
      })
    }
  },
  getters: {
    isLoggedIn: state => !!state.userInfo
  }
})

Register in main.js ​

javascript
// main.js
import Vue from 'vue'
import App from './App'
import store from './store'

Vue.config.productionTip = false

const app = new Vue({
  store,
  ...App
})
app.$mount()

Use in Components ​

html
<template>
  <view class="container">
    <text>Count: {{ count }}</text>
    <button @tap="increment">+1</button>
    
    <view v-if="isLoggedIn">
      <text>Welcome, {{ userInfo.name }}</text>
      <button @tap="logout">Logout</button>
    </view>
    <view v-else>
      <button @tap="login">Login</button>
    </view>
  </view>
</template>

<script>
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'

export default {
  computed: {
    ...mapState(['count', 'userInfo']),
    ...mapGetters(['isLoggedIn'])
  },
  methods: {
    ...mapMutations(['increment']),
    ...mapActions(['login']),
    logout() {
      this.$store.commit('setUserInfo', null)
    },
    handleLogin() {
      const userInfo = {
        id: 1,
        name: 'John',
        avatar: '/static/avatar.png'
      }
      this.login(userInfo)
    }
  }
}
</script>

uni.getSystemInfo Get System Information ​

During development, it's often necessary to get device system information such as screen size, device model, etc.

javascript
// Synchronous get
try {
  const systemInfo = uni.getSystemInfoSync()
  console.log(systemInfo)
} catch (e) {
  console.error(e)
}

// Asynchronous get
uni.getSystemInfo({
  success: function(res) {
    console.log(res)
  },
  fail: function(err) {
    console.error(err)
  }
})

System information includes the following properties:

javascript
{
  brand: 'iPhone',              // Phone brand
  model: 'iPhone X',            // Phone model
  pixelRatio: 3,                // Device pixel ratio
  screenWidth: 375,             // Screen width
  screenHeight: 812,            // Screen height
  windowWidth: 375,             // Available window width
  windowHeight: 724,            // Available window height
  statusBarHeight: 44,          // Status bar height
  language: 'en-US',            // Application language setting
  system: 'iOS 11.0',           // Operating system version
  version: '1.0.0',             // Application version
  platform: 'ios',              // Client platform
  fontSizeSetting: 16,          // User font size setting
  SDKVersion: '2.0.4',          // Client base library version
  safeArea: {                   // Safe area
    left: 0,
    right: 375,
    top: 44,
    bottom: 778,
    width: 375,
    height: 734
  }
}

Global Styles and Themes ​

Global Style Files ​

In uni-app, you can define global styles through the <style> tag in App.vue.

html
<!-- App.vue -->
<style>
/* Global styles */
page {
  background-color: #f8f8f8;
  font-size: 16px;
  font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica, Segoe UI, Arial, Roboto, 'PingFang SC', 'miui', 'Hiragino Sans GB', 'Microsoft Yahei', sans-serif;
}

.container {
  padding: 20rpx;
}

.btn {
  padding: 20rpx 30rpx;
  background-color: #007AFF;
  color: #ffffff;
  border-radius: 8rpx;
  font-size: 28rpx;
  text-align: center;
}

.btn-danger {
  background-color: #dd524d;
}

.text-primary {
  color: #007AFF;
}

.text-danger {
  color: #dd524d;
}
</style>

Theme Switching ​

You can implement theme switching by dynamically switching CSS variables.

html
<!-- App.vue -->
<style>
/* Define CSS variables */
page {
  /* Light theme */
  --bg-color: #f8f8f8;
  --text-color: #333333;
  --border-color: #e5e5e5;
  --primary-color: #007AFF;
  --secondary-color: #6c757d;
  --success-color: #28a745;
  --danger-color: #dc3545;
  --warning-color: #ffc107;
  --info-color: #17a2b8;
}

/* Dark theme */
page.dark {
  --bg-color: #121212;
  --text-color: #e5e5e5;
  --border-color: #333333;
  --primary-color: #409eff;
  --secondary-color: #909399;
  --success-color: #67c23a;
  --danger-color: #f56c6c;
  --warning-color: #e6a23c;
  --info-color: #909399;
}

/* Use CSS variables */
.container {
  background-color: var(--bg-color);
  color: var(--text-color);
}

.btn-primary {
  background-color: var(--primary-color);
  color: #ffffff;
}

.border {
  border: 1px solid var(--border-color);
}
</style>

<script>
export default {
  onLaunch: function() {
    // Get theme setting from local storage
    try {
      const theme = uni.getStorageSync('theme')
      if (theme === 'dark') {
        this.setDarkTheme()
      } else {
        this.setLightTheme()
      }
    } catch (e) {
      console.error(e)
    }
  },
  methods: {
    setDarkTheme() {
      // Add dark theme class
      document.documentElement.classList.add('dark')
      // Store theme setting
      uni.setStorageSync('theme', 'dark')
    },
    setLightTheme() {
      // Remove dark theme class
      document.documentElement.classList.remove('dark')
      // Store theme setting
      uni.setStorageSync('theme', 'light')
    },
    toggleTheme() {
      // Toggle theme
      if (document.documentElement.classList.contains('dark')) {
        this.setLightTheme()
      } else {
        this.setDarkTheme()
      }
    }
  }
}
</script>

Global Mixins ​

For logic that needs to be reused across multiple components, you can use global mixins.

javascript
// mixins/global.js
export default {
  data() {
    return {
      globalData: 'This is global data'
    }
  },
  onLoad() {
    console.log('Global mixin onLoad')
  },
  methods: {
    globalMethod() {
      console.log('This is a global method')
    },
    showToast(title, icon = 'none') {
      uni.showToast({
        title,
        icon
      })
    }
  }
}

Register global mixin in main.js:

javascript
// main.js
import Vue from 'vue'
import App from './App'
import globalMixin from './mixins/global'

Vue.mixin(globalMixin)

const app = new Vue({
  ...App
})
app.$mount()

Global Filters ​

For data formatting logic that needs to be reused across multiple components, you can use global filters.

javascript
// filters/index.js
export function formatDate(date, fmt = 'yyyy-MM-dd') {
  if (!date) return ''
  if (typeof date === 'string') {
    date = new Date(date.replace(/-/g, '/'))
  }
  if (typeof date === 'number') {
    date = new Date(date)
  }
  
  const o = {
    'M+': date.getMonth() + 1,
    'd+': date.getDate(),
    'h+': date.getHours(),
    'm+': date.getMinutes(),
    's+': date.getSeconds(),
    'q+': Math.floor((date.getMonth() + 3) / 3),
    'S': date.getMilliseconds()
  }
  
  if (/(y+)/.test(fmt)) {
    fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length))
  }
  
  for (let k in o) {
    if (new RegExp('(' + k + ')').test(fmt)) {
      fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? (o[k]) : (('00' + o[k]).substr(('' + o[k]).length)))
    }
  }
  
  return fmt
}

export function formatPrice(price) {
  if (!price && price !== 0) return ''
  return '$' + parseFloat(price).toFixed(2)
}

export function formatFileSize(size) {
  if (!size) return '0 B'
  const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']
  let index = 0
  while (size >= 1024 && index < units.length - 1) {
    size /= 1024
    index++
  }
  return size.toFixed(2) + ' ' + units[index]
}

Register global filters in main.js:

javascript
// main.js
import Vue from 'vue'
import App from './App'
import * as filters from './filters'

// Register global filters
Object.keys(filters).forEach(key => {
  Vue.filter(key, filters[key])
})

const app = new Vue({
  ...App
})
app.$mount()

Global Error Handling ​

To catch global errors in the application, you can add a global error handler in main.js.

javascript
// main.js
import Vue from 'vue'
import App from './App'

// Global error handling
Vue.config.errorHandler = function(err, vm, info) {
  console.error('Vue error:', err)
  console.error('Error info:', info)
  
  // Can report error to server
  // reportErrorToServer(err, info)
}

// Catch Promise errors
window.addEventListener('unhandledrejection', event => {
  console.error('Unhandled Promise error:', event.reason)
  
  // Can report error to server
  // reportErrorToServer(event.reason)
  
  // Prevent default handling
  event.preventDefault()
})

const app = new Vue({
  ...App
})
app.$mount()

Global Event Bus ​

For cross-component communication, you can use a global event bus.

javascript
// main.js
import Vue from 'vue'
import App from './App'

// Create global event bus
Vue.prototype.$bus = new Vue()

const app = new Vue({
  ...App
})
app.$mount()

Global API Interceptors ​

For scenarios requiring unified API request handling, you can use interceptors.

javascript
// utils/request.js
import { baseURL } from '@/config'

// Request interceptor
uni.addInterceptor('request', {
  invoke(args) {
    // Pre-request processing
    console.log('Request interceptor:', args)
    
    // Add baseURL
    if (!args.url.startsWith('http')) {
      args.url = baseURL + args.url
    }
    
    // Add token
    const token = uni.getStorageSync('token')
    if (token) {
      args.header = {
        ...args.header,
        'Authorization': `Bearer ${token}`
      }
    }
    
    // Add timestamp to prevent caching
    if (args.method === 'GET') {
      args.url += (args.url.includes('?') ? '&' : '?') + `_t=${Date.now()}`
    }
  },
  success(args) {
    // Post-request success processing
    console.log('Request success:', args)
    
    // Handle business status codes
    if (args.data.code !== 0) {
      uni.showToast({
        title: args.data.message || 'Request failed',
        icon: 'none'
      })
      
      // Handle specific error codes
      if (args.data.code === 401) {
        // Token expired, redirect to login
        uni.navigateTo({
          url: '/pages/login/login'
        })
      }
      
      // Throw business error
      const error = new Error(args.data.message)
      error.code = args.data.code
      throw error
    }
    
    // Return business data
    return args.data.data
  },
  fail(err) {
    // Request failure handling
    console.error('Request failed:', err)
    
    uni.showToast({
      title: 'Network error, please try again later',
      icon: 'none'
    })
    
    return Promise.reject(err)
  },
  complete(res) {
    // Request completion handling
    console.log('Request complete:', res)
  }
})

Global Configuration Best Practices ​

  1. Modular Configuration: Split configuration by functionality into multiple modules for easier maintenance.
javascript
// config/index.js
import development from './env.development'
import production from './env.production'

// Choose configuration based on environment
const env = process.env.NODE_ENV
const config = env === 'development' ? development : production

export default {
  // Basic configuration
  appName: 'uni-app Example',
  appVersion: '1.0.0',
  
  // Environment configuration
  ...config,
  
  // Common configuration
  navBar: {
    backgroundColor: '#007AFF',
    titleColor: '#ffffff',
    backIconColor: '#ffffff'
  },
  tabBar: {
    color: '#7A7E83',
    selectedColor: '#007AFF',
    backgroundColor: '#ffffff'
  },
  
  // Cache keys
  storageKeys: {
    token: 'APP_TOKEN',
    userInfo: 'USER_INFO',
    theme: 'APP_THEME',
    language: 'APP_LANGUAGE'
  }
}
  1. Environment Configuration: Provide different configurations for different environments.
javascript
// config/env.development.js
export default {
  baseURL: 'http://localhost:3000/api',
  debug: true,
  mockData: true
}

// config/env.production.js
export default {
  baseURL: 'https://api.example.com',
  debug: false,
  mockData: false
}
  1. Unified API Management: Centrally manage API interfaces.
javascript
// api/user.js
import { get, post } from '@/utils/request'

export function login(data) {
  return post('/user/login', data)
}

export function getUserInfo() {
  return get('/user/info')
}

export function updateUserInfo(data) {
  return post('/user/update', data)
}

Summary ​

This guide detailed global configuration methods and common configuration items in uni-app, including:

  1. Configuration Files: Configuration items and usage of pages.json and manifest.json
  2. Data Storage: Usage methods of uni.setStorage and uni.getStorage
  3. Application Instance: Usage methods of getApp() and getCurrentPages()
  4. State Management: Vuex configuration and usage
  5. System Information: Usage methods of uni.getSystemInfo
  6. Global Styles and Themes: Global style definition and theme switching
  7. Global Mixins: Reusing component logic
  8. Global Filters: Data formatting
  9. Global Error Handling: Catching application errors
  10. Global Event Bus: Cross-component communication
  11. Global API Interceptors: Unified API request handling
  12. Global Configuration Best Practices: Modular configuration, environment configuration, API management, etc.

By properly using these global configuration methods, you can make uni-app applications more structured, easier to maintain, and improve development efficiency.

Develop once, deploy everywhere - Making cross-platform development easier