这篇内容是在有道云旧笔记基础上重新整理的 Vue/Vite 工程实践 笔记。原始记录偏碎片化,这里补充了使用场景、阅读顺序和落地时需要注意的边界,方便以后在项目里快速复用。

适用场景 ​

  • Vue/Vite 项目中遇到配置、组件封装、构建或运行时问题时,作为排查入口。
  • 需要把一次性调试经验沉淀成团队内可复用的前端工程实践。
  • 迁移旧项目或升级依赖时,用来对照检查环境变量、插件和组件行为差异。

核心要点 ​

  • 正文以代码或命令片段为主,阅读时建议先确认目标问题,再挑选对应片段验证。
  • 保留了 39 段可直接参考的代码或命令,主要涉及 markdown、js、css、html,复制前需要按当前项目路径、版本和变量名做调整。
  • 涉及命令行操作时,建议先在测试目录或测试环境执行,确认输出符合预期后再应用到生产环境。
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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
# Webview环境下前端技术方案


🎯 一、Vue 适配模板(flexible、rem、字体、缓存)
下面是一个完整的 适配入口模板,你可以放在 src/utils/adapt.js 或 main.js 中。

1. flexible + rem + DPR 适配(推荐)
```js
// src/utils/adapt.js
export function setupFlexible() {
const dpr = window.devicePixelRatio || 1
// 解决的是“像素密度”问题
const scale = Number((1 / dpr).toFixed(6))
// 通过 scale = 1 / dpr 统一了 CSS 像素密度,再通过 font-size = 100 * dpr 统一了 rem 的视觉比例。这两者叠加后,字体在所有设备上几乎一致。
// 设置 viewport, 这里选哦注意scale是动态的所以 CSS 的 1px 会尽可能接近设备的 1 物理像素。
const meta = document.querySelector('meta[name="viewport"]')
meta.setAttribute(
'content',
`width=device-width, initial-scale=${scale}, maximum-scale=${scale}, minimum-scale=${scale}, user-scalable=no`
)

// 设置根字体大小,解决的是“rem 缩放”问题*
const baseFont = 100 * dpr
document.documentElement.style.fontSize = baseFont + 'px'
// 设置当前 DPR
document.documentElement.style.setProperty('--vp-dpr', window.devicePixelRatio || 1)
// 动态计算 ratio ratio → 解决“字体层级节奏不一致”, ratio 的作用不是补偿像素缩放,而是补偿 视觉节奏。
const width = window.innerWidth
let ratio = 1.2 + (dpr - 1) * 0.05

if (width >= 480) ratio += 0.05
if (width >= 600) ratio += 0.05
if (width >= 768) ratio += 0.05

document.documentElement.style.setProperty('--vp-ratio', ratio.toFixed(3))
// 监听窗口变化
window.addEventListener('resize', () => {
document.documentElement.style.fontSize = baseFont + 'px'
})
}
```
在 main.js 中调用:

```js
import { setupFlexible } from './utils/adapt'
setupFlexible()
```
2. 字体适配(统一字体 + 禁止系统缩放)
CSS 全局字体设置(App.vue 或 global.css)
```css
html, body {
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif;
-webkit-text-size-adjust: 100% !important; /* 禁止系统字体缩放 */
}

:root {
/* 字体 = 基础字体 × 比例因子 */
--font-base: calc(16px * var(--vp-dpr));
/* 字体层级比例(你动态计算的 ratio) */
--ratio: var(--vp-ratio);

/* 标题层级 */
--font-h1: calc(var(--font-base) * var(--ratio) * var(--ratio) * 1.1);
--font-h2: calc(var(--font-base) * var(--ratio) * var(--ratio));
--font-h3: calc(var(--font-base) * var(--ratio));
--font-h4: calc(var(--font-base) * (var(--ratio) * 0.85));
--font-h5: calc(var(--font-base) * (var(--ratio) * 0.75));
--font-h6: calc(var(--font-base) * (var(--ratio) * 0.65));

/* 正文体系 */
--font-body: var(--font-base);
--font-body-small: calc(var(--font-base) * 0.875);

/* 辅助文字 */
--font-caption: calc(var(--font-base) * 0.75);
--font-overline: calc(var(--font-base) * 0.65);
}

```
字体文件(可选)
```css
@font-face {
font-family: 'CustomFont';
src: url('/fonts/custom.woff2') format('woff2');
font-display: swap;
}
```
3. Vue 禁用缓存(避免 WebView 加载旧版本)
index.html 强制禁用缓存
```html
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
```
Vue 构建资源加 hash(vue.config.js)
```js
module.exports = {
filenameHashing: true,
productionSourceMap: false,
}
```
4. 图片与字体加载稳定性(避免白屏)
```js
export async function waitForAssets() {
await document.fonts.ready
await Promise.all(
[...document.images].map(img => img.decode().catch(() => {}))
)
}
```
🎯 二、JSBridge 统一封装(Android / iOS / 鸿蒙)
你需要一个 跨端统一 API 层,前端只调用统一方法,不关心平台差异。

1. 定义统一 JSBridge(前端)
```js
// src/utils/bridge.js
// JS → Native
interface BridgeRequest {
type: 'request'
method: string
callbackId?: string
payload?: any
}

// Native → JS
interface BridgeResponse {
type: 'response'
callbackId: string
error?: string | null
data?: any
}

// Native → JS 主动事件
interface BridgeEvent {
type: 'event'
event: string
data?: any
}


// src/bridge/nativeJSBridge.js
const UA = window.navigator.userAgent
const isAndroid = UA.includes('Android') || UA.includes('Adr')
const isIOS = /\(i[^;]+;( U;)? CPU.+Mac OS X/.test(UA)
const isHarmony = UA.includes('Harmony')

const genId = () =>
'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
const r = (Math.random() * 16) | 0
const v = c === 'x' ? r : (r & 0x3) | 0x8
return v.toString(16)
})

class JSBridge {
constructor(options = {}) {
this._callbacks = new Map()
this._events = new Map()
this._timeout = options.timeout || 10000
this._log = options.log ?? false

// 暴露给 Native 调用
window.__WebViewBridge__ = {
dispatchFromNative: this._dispatchFromNative.bind(this),
}
}

_debug(...args) {
if (this._log) console.log('[JSBridge]', ...args)
}

// Native 调用入口
_dispatchFromNative(messageJson) {
let msg
try {
msg = typeof messageJson === 'string' ? JSON.parse(messageJson) : messageJson
} catch (e) {
this._debug('Invalid JSON from native', messageJson)
return
}

if (msg.type === 'response') {
const { callbackId, error, data } = msg
const cb = this._callbacks.get(callbackId)
if (!cb) {
this._debug('Callback not found:', callbackId)
return
}
this._callbacks.delete(callbackId)
error ? cb.resolve([null ,error]) : cb.resolve([data, null])
} else if (msg.type === 'event') {
const { event, data } = msg
const handlers = this._events.get(event) || []
handlers.forEach(fn => {
try {
fn(data)
} catch (e) {
this._debug('Event handler error:', e)
}
})
} else {
this._debug('Unknown message type:', msg)
}
}

// JS → Native 调用
call(method, payload = {}) {
return new Promise((resolve, reject) => {
if (typeof method !== 'string') {
// return reject(new Error('method must be string'))
this._debug('method must be string')
return resolve([null, 'method must be string'])
}
if (payload && typeof payload !== 'object') {
// return reject(new Error('payload must be object'))
this._debug('payload must be object')
return resolve([null, 'payload must be object'])
}

const callbackId = genId()
const message = {
type: 'request',
method,
callbackId,
payload,
}

// 超时控制,防止 callback 泄漏
const timer = setTimeout(() => {
this._callbacks.delete(callbackId)
// reject(new Error(`Bridge call timeout: ${method}`))
this._debug(`Bridge call timeout: ${method}`)
return resolve([null, `Bridge call timeout: ${method}`])
}, this._timeout)

this._callbacks.set(callbackId, {
resolve: data => {
clearTimeout(timer)
resolve([data, null])
},
reject: err => {
clearTimeout(timer)
// reject(err)
this._debug(err)
return resolve([null, err])
},
})

const msgStr = JSON.stringify(message)

try {
if ((isAndroid || isHarmony) && window.NativeBridge && typeof window.NativeBridge.postMessage === 'function') {
window.NativeBridge.postMessage(msgStr)
} else if (isIOS && window.webkit?.messageHandlers?.NativeBridge) {
window.webkit.messageHandlers.NativeBridge.postMessage(msgStr)
// } else if (isHarmony && window.HarmonyBridge?.postMessage) {
// window.HarmonyBridge.postMessage(msgStr)
} else {
this._debug('No native bridge found')
this._callbacks.delete(callbackId)
// reject(new Error('Native bridge not available'))
resolve([null, 'Native bridge not available'])
}
} catch (e) {
this._callbacks.delete(callbackId)
// reject(e)
resolve([null, e])
}
})
}

// 事件订阅(Native 主动推送)
on(event, handler) {
if (!this._events.has(event)) {
this._events.set(event, [])
}
this._events.get(event).push(handler)
return () => {
const arr = this._events.get(event) || []
this._events.set(
event,
arr.filter(fn => fn !== handler),
)
}
}
}

export const nativeJSBridge = new JSBridge({ timeout: 10000, log: false })

```
JSBridge + Vue 统一通信框架
```js
// src/provides/nativeJSBridge.js
import { nativeJSBridge } from './nativeJSBridge'

export default {
install(app) {
app.config.globalProperties.$nativeJSBridge = nativeJSBridge
app.provide('nativeJSBridge', nativeJSBridge)
},
}

export function useNativeBridge() {
return nativeJSBridge
}

// 在 main.js 中:
import { createApp } from 'vue'
import App from './App.vue'
import VueNativeBridge from './provides/nativeJSBridge'

const app = createApp(App)
app.use(VueNativeBridge)
app.mount('#app')


// 业务使用方式(组合式 API)

// 上述已经全局依赖注入,无需组件再次引入
import { useNativeBridge } from '@/provides/nativeJSBridge'

const bridge = useNativeBridge()

const getToken = async () => {
const [token, err] = await bridge.call('getToken')
if (token) {
console.log('token:', token)
} else {
console.log('%c [ error ]-302', 'font-size:14px; background:#cf222e; color:#fff;', err)
}
}

const openPage = url => bridge.call('openPage', { url })

// 订阅 Native 事件
const off = bridge.on('loginStatusChanged', data => {
console.log('loginStatusChanged:', data)
})
// 你必须在组件卸载时:
onUnmounted(() => off())

```

《WebView JSBridge 通信协议文档》
📌 1. 总体说明
WebView 与 H5 通过统一的 JSBridge 通信,采用 JSON 消息协议,支持:

- JS → Native 方法调用(request)

- Native → JS 回调(response)

- Native → JS 主动事件推送(event)

所有消息均为 JSON 字符串。

📌 2. 消息协议格式
2.1 JS → Native(request)
```json
{
"type": "request",
"method": "getToken",
"callbackId": "cb_123456",
"payload": {
"forceRefresh": true
}
}
```
字段说明:

字段 | 类型 | 必填 | 说明
type string 是 固定为 "request"
method string 是 调用的 Native 方法名
callbackId string 否 JS 生成的唯一 ID,用于回调
payload object 否 传递给 Native 的参数
2.2 Native → JS(response)
```json
{
"type": "response",
"callbackId": "cb_123456",
"error": null,
"data": {
"token": "abcdefg"
}
}
```
字段说明:

字段 类型 必填 说明
type string 是 固定为 "response"
callbackId string 是 对应 JS 的 callbackId
error string/null 否 错误信息(失败时必填)
data any 否 返回数据
2.3 Native → JS(event)
```json
{
"type": "event",
"event": "loginStatusChanged",
"data": {
"isLogin": true
}
}
```
字段说明:

字段 类型 必填 说明
type string 是 固定为 "event"
event string 是 事件名
data any 否 事件数据
📌 3. Native 调用 JS 的入口
JS 暴露统一入口:

```js
window.__WebViewBridge__.dispatchFromNative(messageJson)
```
Native 侧只需调用:

Android(Kotlin)
```kotlin
webView.evaluateJavascript(
"window.__WebViewBridge__.dispatchFromNative('$json')",
null
)
```
iOS(Swift)
```swift
webView.evaluateJavaScript("window.__WebViewBridge__.dispatchFromNative('\(json)')", nil)
```
鸿蒙(ArkTS)
```ts
webController.runJavaScript(`window.__WebViewBridge__.dispatchFromNative('${json}')`)
```
📌 4. Native 侧方法注册规范
Native 需要统一一个入口,例如:

Android
```kotlin
class NativeBridge {
@JavascriptInterface
fun postMessage(message: String) {
// 解析 JSON
// 根据 method 分发
// 执行后通过 evaluateJavascript 回调 JS
}
}
```
iOS
```swift
class NativeBridge: NSObject, WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage) {
// message.body 为 JSON 字符串
}
}
```
鸿蒙
```ts
webController.registerJavaScriptProxy("NativeBridge", {
postMessage(message: string) {
// 解析 JSON
}
})
```
📌 5. 错误码规范(可选)

错误码 说明
- 1001 方法不存在
- 1002 参数格式错误
- 1003 Native 内部错误
- 1004 权限不足
- 1005 超时

实践检查清单 &#8203;

  • 先确认 Vue、Vite、Node、包管理器版本,再判断示例是否需要按当前工程结构调整。
  • 涉及构建或插件配置时,建议同时验证开发环境和生产构建结果。
  • 涉及 UI 或浏览器行为时,至少在桌面端和移动端各验证一次关键路径。
  • 涉及接口、服务端或权限逻辑时,补充失败分支和权限边界测试。

复盘小结 &#8203;

这篇笔记更适合作为问题排查或方案落地前的速查材料。后续如果在真实项目中再次遇到同类问题,可以继续把具体版本、报错信息、最终取舍和验证结果补到对应小节里,让它从代码片段逐步沉淀成完整方案。