从0到1构建跨平台Electron应用,这篇文章就够了

简介: Electron是一个可以直接开发构建跨平台应用的库,简单、快捷。《Electron从0到1构建跨平台应用》这篇文章,我摘录了我自己在真实项目中,从开发到生成安装包的要点。

上一篇介绍了《Electron快速入手,拥有自己的第一个桌面应用 》,刚开始学习Electron的小伙伴可以先看下上一篇文章,方便快速入手。

但是,Electron的原理及一些其他功能开发,是如何实现的呢?

简介

Electron 基于 Chromium 和 Node.js, 让你可以使用 HTML, CSS 和 JavaScript 构建桌面端应用。

这篇文章摘录了我自己在真实项目中的要点问题

可能有的小伙伴会问,为什么不说一下 Vue、React 或者其他的库,如何与 Electron 结合开发?

这是因为 Vue 或者 React 等其他库都是 UI 页面,可以单独独立开发,只需要在代码打包压缩后,使用 BrowserWindow 中的 loadURL 或者 loadFile 加载即可。

Electron核心

Electron实现跨平台桌面应用程序的原理

image.png

通过集成浏览器内核,使用前端的技术来实现不同平台下的渲染,并结合了 ChromiumNode.js 和用于调用系统本地功能的 API 3大板块:

  1. Chromium:提供强大的 UI 渲染能力,用于显示网页内容。
  2. Node.js:主要用于本地文件系统和操作系统,提供 GUI 的操作能力,如path、fs、crypto 等模块。
  3. Native API:为 Electron 提供原生系统的 GUI 支持,使用 Electron Api 可以调用原生应用程序接口。

Electron主要核心点

Electron其实很简单,基本都是api,我自己整理了主要核心点有进程间通信appBrowserWindowApplication 4个方面:

  1. 进程间通信:处理主进程与渲染进程的双向通信。

  2. app:应用及应用生命周期。贯穿桌面端应用整个生命周期,虽然用的不多,但是很重要。

  3. BrowserWindow:浏览器窗口。大家可以理解成每一个 BWindow 都是一个独立的浏览器,用于加载渲染我们的前端代码。

  4. Application:应用程序功能点。为应用提供更佳完善的功能点,如程序坞、菜单......

从这4个主要核心点入手,开发人员会更快进入开发状态。

进程间通信

Electron 中有2个类型的进程,一种是主进程,另外一种是渲染进程。

而 ipcMain 代表了从主进程到渲染进程的异步通信; ipcRenderer 则代表了从渲染器进程到主进程的异步通信。

ipcMain与ipcRenderer

Electron 会存在多个进程,那么多进程间如何实现通信呢?

image.png

注意:不建议使用同步通信,这会对应用整体性能产生影响。

注意:进程通信所传参数只能为原始类型数据和可被 JSON.stringify 的引用类型数据。故不建议使用 JSON.stringify。

多进程间的通信,无论有多少个渲染进程,都遵循“主进程与渲染进程通信”。

渲染进程之间不能直接通信。

同时 Electron 提供了异步同步Promise 通信方式。

异步

通过 event.reply(...) 将异步消息发送回发送者。

event.reply 会自动处理从非主 frame 发送的消息,建议使用 event.sender.send 或者 win.webContents.send 把消息发送到主 frame。

主进程Main

const {
   
    ipcMain } = require('electron')
// 接收renderer的数据
ipcMain.on('asynchronous-message', (event, arg) => {
   
    
    console.log(arg) // 传递的数据 ping
    // event.reply('asynchronous-reply', 'pong') // 发送数据到渲染进程
    event.sender.send('asynchronous-reply', 'pong')
})

渲染进程Renderer

const {
   
    ipcRenderer } = require('electron')
// 接收main的数据
ipcRenderer.on('asynchronous-reply', (event, arg) => {
   
    
    console.log(arg) // prints "pong"
})
// 发送数据到主进程
ipcRenderer.send('asynchronous-message', 'ping')

同步

也可以设置同步接收(不建议使用),通过 event.returnValue 设置

主进程main

ipcMain.on('synchronous-message', (event, arg) => {
   
   
    event.returnValue = 'sync'
})

渲染进程renderer

const syncMsg = ipcRenderer.sendSync('synchronous-message', 'ping');
console.log(syncMsg)

ipcMain.once(channel, listener)只监听实现一次。

ipcMain.removeListener(channel, listener)删除某一指定的监听。

ipcMain.removeAllListeners([channel])移除多个指定 channel 的监听器,无参移除所有。

Promise

进程发送消息,并异步等待结果

ipcMain 提供 handle,ipcRenderer 提供了 invoke。

// 渲染进程
ipcRenderer.invoke('some-name', someArgument).then((result) => {
   
   
    // ...
})

// 主进程
ipcMain.handle('some-name', async (event, someArgument) => {
   
   
    const result = await doSomeWork(someArgument)
    return result
})

app

Electron 中 app 模块,是用来控制应用程序的事件生命周期,同时也提供了管理应用程序的事件方法。

注意: 某些方法仅在特定的操作系统上可用,如 app.hide() 只在 macos 可用。

const {
   
   app, BrowserWindow} = require('electron')
const path = require('path')

function createWindow () {
   
   
  const mainWindow = new BrowserWindow({
   
   
    width: 800,
    height: 600,
    webPreferences: {
   
   
      preload: path.join(__dirname, 'preload.js')
    }
  })
  mainWindow.loadFile('index.html')
}

app.whenReady().then(() => {
   
   
  createWindow()
  app.on('activate', function () {
   
   
    if (BrowserWindow.getAllWindows().length === 0) createWindow()
  })
})

app.on('window-all-closed', function () {
   
   
  if (process.platform !== 'darwin') app.quit()
})

app的生命周期

可以让我们在每个事件生命周期做出相应的处理,如监听初始化完成后创建窗口、监听应用被激活后聚焦窗口等。

1、ready

应用完成初始化时触发,这个是重点。因为Electron中很多都是需要初始化完成后,才可以执行,如 menu 、窗口等。

为避免出现错误,建议所有的操作都在ready之后

const {
   
    app } = require('electron')
app.on('ready', () => {
   
   
    // create menu
    // create browserWindow
})

可以通过 app.isReady() 来检查该事件是否已被触发。

若希望通过Promise实现,使用 app.whenReady()

2、第二个实例创建

开发中需要特别注意第二个实例(即第二个应用)创建时的处理。

因为 Electron 在一些情况下会触发第二个实例的创建,如程序坞对激活的默认操作等。

所以在 Electron 的应用即将要运行第二个实例时,处理方案一般是聚焦到主窗口。

const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
   
   
    app.quit();
} else {
   
   
    app.on('second-instance', (event, commandLine, workingDirectory) => {
   
   
        let win = mainWindow; // 获取到主窗口mainWindow
        if (win) {
   
   
            if (win.isMinimized()) {
   
    // 窗口最小化
                win.restore(); // 窗口从最小化状态恢复
            }
            if (!win.isVisible()) {
   
    // 窗口不可见
                win.show(); // 窗口显示
            }
            win.focus(); // 窗口聚焦
        }
    });
}

第二个实例执行时,会在第二个实例中触发 requestSingleInstanceLock 事件。

requestSingleInstanceLock 是用来判断当前应用程序实例是否成功取得了锁。如果它取得锁失败,代表此时另一个应用实例已经取得了锁并且仍旧在运行,当前的应用程序实例就需要立即退出。

而此时 second-instance 这个事件将在你的应用程序的首个实例中触发。

3、当应用被激活时(macos)

app.on('activate', (event, webContents, details) => {
   
   
    // 聚焦到最近使用的窗口
});

触发此事件的情况很多:
首次启动应用程序、尝试在应用程序已运行时或单击应用程序的坞站任务栏图标时重新激活它。

4、当所有窗口关闭,退出应用

const {
   
    app } = require('electron')
app.on('window-all-closed', () => {
   
   
    app.quit()
})

5、渲染进程进程奔溃

app.on('render-process-gone', (event, webContents, details) => {
   
   
    // 定位原因,可通过 log 记录;也可以做重启 retry 操作,切记限制count
    // webContents.reloadIgnoringCache(); // 忽略缓存强制刷新页面
});

details Object

  • reason string - 渲染进程消失的原因。 可选值:

    • clean-exit - 以零为退出代码退出的进程
    • abnormal-exit - 以非零退出代码退出的进程
    • killed - 进程发送一个 SIGTERM,否则是被外部杀死的。
    • crashed - 进程崩溃
    • oom - 进程内存不足
    • launch-failed - 进程从未成功启动
    • integrity-failure - 窗口代码完整性检查失败
  • exitCode Integer - 进程的退出代码,除非在 reasonlaunch-failed 的情况下, exitCode 将是一个平台特定的启动失败错误代码。

6、应用退出

app.on('will-quit', (event, webContents, details) => {
   
   
    // timer 或者 关闭第三方的进程
});

app常用事件方法

通过操作 app 应用程序的事件方法,来控制应用程序,如重启、关闭等。

const {
   
    app } = require('electron')
app.relaunch({
   
    args: process.argv.slice(1).concat(['--relaunch']) }) 
app.exit(0)

1、重启应用

app.relaunch([options])

relaunch 被多次调用时,多个实例将会在当前实例退出后启动。

2、所有窗口关闭

app.exit(0)

所有窗口都将立即被关闭,而不询问用户

3、当前应用程序目录

app.getAppPath()

可以理解成获取应用启动(代码)目录

4、设置 "关于" 面板选项

image.png

app.setAboutPanelOptions({
   
   
    applicationName: 'demo',
    applicationVersion: '0.0.1',
    version: '0.0.1'
});

5、注意,部分事件存在着系统的区别

// 只适用于 macOS
app.hide() // 隐藏所有的应用窗口,不是最小化.

BrowserWindow

const {
   
    BrowserWindow } = require('electron')

const win = new BrowserWindow({
   
    width: 800, height: 600 })

// 加载地址
win.loadURL('https://github.com')

// 加载文件
win.loadFile('index.html')

自定义窗口

可以通过自定窗口 options,实现自定义样式,如桌面窗口页面、桌面通知、模态框。。。

const options = {
   
    
    width: 800, 
    height: 600 
}
new BrowserWindow(options)

父子窗口

拖动父窗口,子窗口跟随父窗口而动。

const top = new BrowserWindow()
const child = new BrowserWindow({
   
    parent: top })

注意:child 窗口将总是显示在 top 窗口的顶部。

win.setParentWindow(parent)给窗口设置父窗口,null取消父窗口
win.getParentWindow()获取窗口的父窗口
win.getChildWindows()获取所有子窗口

显示与隐藏

窗口可以直接展示也可以延后展示。但每次执行 show,都会将当前桌面的焦点聚焦到执行show的窗口上。

const win = new BrowserWindow({
   
   
    show: true, 
})

在加载页面时,渲染进程第一次完成绘制时,如果窗口还没有被显示,渲染进程会发出 ready-to-show 事件。在此事件后显示窗口将没有视觉闪烁:

const win = new BrowserWindow({
   
    show: false })
win.once('ready-to-show', () => {
   
   
    win.show()
})

win.hide()窗口隐藏,焦点消失

win.show()窗口显示,获取焦点

win.showInactive()窗口显示,但不聚焦于窗口(多用于当其他窗口需要显示时,但是不想中断当前窗口的操作)

win.isVisible()判断窗口是否显示

Bounds

1、设置窗口的size、position

setBounds(bounds[, animate]):同时设置 size、position,但是同时也会重置窗口。

setSize(width, height[, animate]): 调整窗口的宽度和高度。

setPosition(x, y[, animate]):将窗口移动到x 和 y。

注意:animate只在macOS上才会生效。

// 设置 bounds 边界属性
win.setBounds({
   
    x: 440, y: 225, width: 800, height: 600 })
// 设置单一 bounds 边界属性
win.setBounds({
   
    width: 100 })
win.setSize(800, 600)
win.setPosition(440, 225)

2、获取窗口size、position

getBounds()获取窗口的边界信息。

getSize()获取窗口的宽度和高度。

getPosition()返回一个包含当前窗口位置的数组

center()将窗口移动到屏幕中央(常用)。

3、常见问题

win.setSize如果 width 或 height 低于任何设定的最小尺寸约束,窗口将对齐到约束的最小尺寸。

win.setPosition有的电脑机型存在兼容问题,执行一次win.setPosition(x,y)不会生效,需要执行两次。

Application

应用包含了很多程序工具,如 Menu、Tray...

Menu

创建原生应用菜单和上下文菜单。

image.png

在mac中,菜单展示在应用内;而 windows 与 linux,菜单则会展示在各个窗口的顶部。

可以对某一窗口单独设置或者删除Menu,但是这只是针对 windows、linux 生效。

win.setMenu(menu)设置为窗口的菜单栏 menu(只对 windows、linux 生效)。

win.removeMenu()删除窗口的菜单栏(只对 windows、linux 生效)。

如何全局设置Menu

const {
   
    Menu } = require('electron')

const template = [
{
   
   
  label: 'Electron',
  submenu: [
    {
   
    role: 'about', label: '关于' },
    {
   
    type: 'separator' },
    {
   
    role: 'services', label: '偏好设置' },
    {
   
    type: 'separator' },
    {
   
    role: 'hide', label: '隐藏' },
    {
   
    role: 'hideOthers', label: '隐藏其他' },
    {
   
    type: 'separator' },
    {
   
    role: 'quit', label: '退出' }
  ]
},
{
   
   
  label: '编辑',
  submenu: [
    {
   
    role: 'undo', label: '撤销' },
    {
   
    type: 'separator' },
    {
   
    role: 'menu_copy', label: '复制' },
    {
   
    role: 'menu_paste', label: '粘贴' }
  ]
},
{
   
   
  label: '窗口',
  submenu: [
    {
   
    
      role: 'minimize',
      label: '最小化',
      click: function (event, focusedWindow, focusedWebContents) {
   
   } 
    },
    {
   
    role: 'close', label: '关闭' },
    {
   
    role: 'togglefullscreen', label: '全屏', accelerator: 'Cmd+,OrCtrl+,'}
  ]
}];

let menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);

role:可以理解为官方命名好的指令,详见官方menuitemrole

label:我们对指令自定义的展示文字。

click:触发该指令的接受的函数

Tray

添加图标和上下文菜单到系统通知区

image.png

const {
   
   ipcMain, app, Menu, Tray} = require('electron')

const iconPath = path.join(__dirname, './iconTemplate.png')
const tray = new Tray(iconPath)
const contextMenu = Menu.buildFromTemplate([{
   
   
    label: 'tray 1',
    click: () => {
   
   }
}, {
   
   
    label: 'tray 2',
    click: () => {
   
   }
}])
tray.setToolTip('Electron Demo in the tray.')
tray.setContextMenu(contextMenu)

setToolTip设置鼠标指针在托盘图标上悬停时显示的文本

setContextMenu设置图标的内容菜单(支持动态添加多个内容)

image.png

dialog

系统对话框

image.png

1.支持多选、默认路径

const {
   
    dialog } = require('electron')

const options = {
   
   
    title: '标题',
    defaultPath: '默认地址',
    properties: [  
        openFile, // 容许选择文件
        openDirectory, // 容许选择目录
        multiSelections, // 容许多选
        showHiddenFiles, // 显示隐藏文件
        createDirectory, // 创建新的文件,只在mac生效
        promptToCreate,// 文件目录不存在,生成新文件夹,只在windows生效
    ]
}

dialog.showOpenDialog(win, options); // win是窗口

2.支持过滤文件

过滤文件后缀名 gif 的文件,显示所有文件用 * 代替

options.filters = [
    {
   
    name: 'Images', extensions: ['gif'] }
]

globalShortcut

键盘事件

需要先注册globalShortcut.register(accelerator, callback)

const {
   
    globalShortcut } = require('electron')

globalShortcut.register('CommandOrControl+F', () => {
   
   
    // 注册键盘事件是全局性质的,各个窗口都可以触发
})

globalShortcut.register(accelerator, callback)注册全局快捷键
globalShortcut.isRegistered(accelerator)判断是否注册
globalShortcut.unregister(accelerator)取消注册

注意:应用程序退出时,注销键盘事件

app.on('will-quit', () => {
   
   
    globalShortcut.unregisterAll()
})

Notification

系统通知

这个受限于当前系统是否支持桌面通知,在 mac 或 windows 电脑的设置中,需特别注意是否容许通知。

const {
   
    Notification } = require('electron');

const isAllowed = Notification.isSupported();
if (isAllowed) {
   
   
    const options = {
   
   
        title: '标题',
        body: '正文文本,显示在标题下方',
        silent: true, // 系统默认的通知声音
        icon: '', // 通知图标
    }
    const notification = new Notification(argConig);
    notification.on('click', () => {
   
     });
    notification.on('show', () => {
   
     });
    notification.on('close', () => {
   
     });
    notification.show();
}

notification.close()关闭通知

session

管理浏览器会话、cookie、缓存、代理设置等。

1、全局

const {
   
    session, BrowserWindow } = require('electron')

// 拦截下载
session.defaultSession.on('will-download', (event, item, webContents) => {
   
   
    event.preventDefault() // 阻止默认行为下载或做指定目录下载
})

2、单独窗口

const win = new BrowserWindow({
   
    width: 800, height: 600 })
win.loadURL('http://github.com')
const ses = win.webContents.session

ses.setProxy(config):设置代理

ses.cookies:设置cookies或获取cookies

3、浏览器UserAgent

设置 UserAgent 可以通过 app.userAgentFallback 全局设置,也可以通过ses.setUserAgent 设置。

screen

检索有关屏幕大小、显示器、光标位置等的信息。

const primaryDisplay = screen.getPrimaryDisplay() // 获取光标所在屏幕的屏幕信息
const {
   
    width, height } = primaryDisplay.workAreaSize // 获取光标下的屏幕尺寸
const allDisplay = screen.getAllDisplays() // 返回数组,所有的屏幕

screen.getPrimaryDisplay()返回主窗口 Display

screen.getAllDisplays()返回所有的窗口 Display[] 数组

screen.getDisplayNearestPoint离光标最近的窗口

Node

项目中会用到 Node.js,下面是我整理的常用方法。

fs

本地文件读写

读取目录的内容

fs.readdirSync(path[, options])

fs.readdir(path[, options])

读取文件的内容

fs.readFileSync(path[, options])

fs.readFile(path[, options])

文件的信息

const stats = fs.statSync(path);

stats.isDirectory() // 是否为系统目录
stats.isFile() // 是否为文件
stats.size // 文件大小
。。。

路径是否存在

fs.existsSync(path)

写入文件

file 是文件名时,将数据写入文件,如果文件已存在则替换该文件。

fs.writeFile(file, data[, options], callback)

fs.writeFileSync(file, data[, options], callback)

移除文件

fs.rmSync(path[, options])

fs.rmdirSync(path[, options])

更改文件的权限

fs.chmod(path, mode, callback)

fs.chmodSync(path, mode, callback)

注意:mode是8进制,可通过parseInt(mode, 8)转化

修改文件夹名

fs.renameSync

拷贝文件到指定地址

fs.copySync

path

拼接绝对路径

path.resolve([...paths])

拼接路径

path.join([...paths])

路径文件夹名字

path.dirname(path)

获取文件名

path.basename(path)

都是属于 Nodejs,整理到最后懒得整理了~~,小伙伴们想研究的话,具体看Node.js

生成安装包

Electron打包生成跨平台软件,见我的另一篇文章
使用Electron生成安装包并打包为不同平台的应用格式

存在的问题

Electron还是会存在部分坑~~

我整理了一些,已经添加到我个人的Electron专题集,小伙伴们可以借鉴一下。

我也会持续更新。。。

有些问题我可能遇到没写,或者我写的是错误的,欢迎大家积极指正沟通,共同学习,共同进步~~。

结语

感谢您的阅读!希望本文带给您有价值的信息。

如果对您有帮助,请「点赞」支持,并「关注」我的主页获取更多后续相关文章。同时,也欢迎「收藏」本文,方便以后查阅。

写作不易,我会继续努力,提供有意义的内容。感谢您的支持和关注!

后期分享

《Electron开发自定义通知 & 多并发下接收消息处理》

《Electron仿浏览器多窗口多进程管理》

《Electron性能优化》

目录
相关文章
|
2月前
|
缓存 JavaScript 前端开发
高效打造跨平台桌面应用:Electron加载服务器端JS
【9月更文挑战第17天】Electron 是一个基于 Chromium 和 Node.js 的开源框架,允许使用 HTML、CSS 和 JavaScript 构建跨平台桌面应用。加载服务器端 JS 可增强应用灵活性,实现代码复用、动态更新及实时通信。通过 HTTP 请求、WebSocket 或文件系统可实现加载,但需注意安全性、性能和兼容性问题。开发者应根据需求选择合适方法并谨慎实施。
116 3
|
2天前
|
安全 前端开发 Windows
Windows Electron 应用更新的原理是什么?揭秘 NsisUpdater
本文介绍了 Electron 应用在 Windows 中的更新原理,重点分析了 `NsisUpdater` 类的实现。该类利用 NSIS 脚本,通过初始化、检查更新、下载更新、验证签名和安装更新等步骤,确保应用的更新过程安全可靠。核心功能包括差异下载、签名验证和管理员权限处理,确保更新高效且安全。
11 4
Windows Electron 应用更新的原理是什么?揭秘 NsisUpdater
|
25天前
|
JavaScript API
使用vue3+vite+electron构建小项目介绍Electron进程间通信
使用vue3+vite+electron构建小项目介绍Electron进程间通信
212 3
|
1月前
|
安全 前端开发 iOS开发
揭秘 electron-builder:macOS 应用打包背后到底发生了什么?
本文详细介绍了 Electron 应用在 macOS 平台上的打包流程,涵盖配置文件、打包步骤、签名及 notarization 等关键环节。通过剖析 `electron-builder` 的源码,展示了如何处理多架构应用、执行签名,并解决常见问题。适合希望深入了解 macOS 打包细节的开发者。
39 2
|
1月前
|
开发框架 缓存 前端开发
electron-builder 解析:你了解其背后的构建原理吗?
本文首发于微信公众号“前端徐徐”,详细解析了 electron-builder 的工作原理。electron-builder 是一个专为整合前端项目与 Electron 应用的打包工具,负责管理依赖、生成配置文件及多平台构建。文章介绍了前端项目的构建流程、配置信息收集、依赖处理、asar 打包、附加资源准备、Electron 打包、代码签名、资源压缩、卸载程序生成、安装程序生成及最终安装包输出等环节。通过剖析 electron-builder 的原理,帮助开发者更好地理解和掌握跨端桌面应用的构建流程。
65 2
|
1月前
|
监控 前端开发 安全
谈谈我做 Electron 应用的这一两年
本文首发于微信公众号“前端徐徐”,作者徐徐分享了过去一两年间开发Electron桌面应用的经验与心得。文章详细介绍了从项目启动、技术选型到具体实施的过程,并探讨了桌面端开发面临的挑战及解决方案,如软件更新、任务队列设计、性能优化等。此外,还列举了一些特殊需求的实现方法,如静默安装、进程禁用等。作者认为,尽管桌面端开发有其独特性,但通过不断探索与实践,仍能显著提升用户体验和技术水平。
97 0
谈谈我做 Electron 应用的这一两年
|
24天前
|
开发框架 JavaScript 前端开发
Electron技术深度解析:构建跨平台桌面应用的利器
【10月更文挑战第13天】Electron技术深度解析:构建跨平台桌面应用的利器
131 0
|
24天前
|
XML 缓存 前端开发
Electron-builder 是如何打包 Windows 应用的?
本文首发于微信公众号“前端徐徐”,作者徐徐深入解析了 electron-builder 在 Windows 平台上的打包流程。文章详细介绍了 `winPackager.ts`、`AppxTarget.ts`、`MsiTarget.ts` 和 `NsisTarget.ts` 等核心文件,涵盖了目标创建、图标处理、代码签名、资源编辑、应用签名、性能优化等内容,并分别讲解了 AppX/MSIX、MSI 和 NSIS 安装程序的生成过程。通过这些内容,读者可以更好地理解和使用 electron-builder 进行 Windows 应用的打包和发布。
94 0
|
3月前
|
JavaScript 开发工具 git
Electron V8排查问题之构建时报错 "user32.lib is not found in LIB"如何解决
Electron V8排查问题之构建时报错 "user32.lib is not found in LIB"如何解决
41 1
|
3月前
|
容器 iOS开发 Linux
震惊!Uno Platform 响应式 UI 构建秘籍大公开!从布局容器到自适应设计,带你轻松打造跨平台完美界面
【8月更文挑战第31天】Uno Platform 是一款强大的跨平台应用开发框架,支持 Web、桌面(Windows、macOS、Linux)及移动(iOS、Android)等平台,仅需单一代码库。本文分享了四个构建响应式用户界面的最佳实践:利用布局容器(如 Grid)适配不同屏幕尺寸;采用自适应布局调整 UI;使用媒体查询定制样式;遵循响应式设计原则确保 UI 元素自适应调整。通过这些方法,开发者可以为用户提供一致且优秀的多设备体验。
119 0
下一篇
无影云桌面