提交 14da9502 编写于 作者: fxy060608's avatar fxy060608

feat(h5): api Tree-Shaking

上级 d374f346
...@@ -2,9 +2,21 @@ export * from './service/base/base64' ...@@ -2,9 +2,21 @@ export * from './service/base/base64'
export * from './service/base/upx2px' export * from './service/base/upx2px'
export * from './service/base/interceptor' export * from './service/base/interceptor'
export { isSyncApi, isContextApi, promisify } from './helpers/promise' export * from './service/ui/createIntersectionObserver'
export * from './service/ui/createSelectorQuery'
// protocols
export * from './protocols/base/canIUse' export * from './protocols/base/canIUse'
export * from './protocols/device/makePhoneCall' export * from './protocols/device/makePhoneCall'
export * from './protocols/device/setClipboardData' export * from './protocols/device/setClipboardData'
export * from './protocols/file/openDocument'
export * from './protocols/location/chooseLocation'
export * from './protocols/location/getLocation'
export * from './protocols/location/openLocation'
// helpers
export { createApi } from './helpers/api' export { createApi } from './helpers/api'
export { isSyncApi, isContextApi, promisify } from './helpers/promise'
...@@ -10,7 +10,7 @@ function getInt(name: string) { ...@@ -10,7 +10,7 @@ function getInt(name: string) {
} }
} }
export const CanvasGetImageDataOptions = { export const CanvasGetImageDataOptions: ApiOptions = {
formatArgs: { formatArgs: {
x: getInt('x'), x: getInt('x'),
y: getInt('y'), y: getInt('y'),
...@@ -19,7 +19,7 @@ export const CanvasGetImageDataOptions = { ...@@ -19,7 +19,7 @@ export const CanvasGetImageDataOptions = {
} }
} }
export const CanvasGetImageDataProtocol = { export const CanvasGetImageDataProtocol: ApiProtocol = {
canvasId: { canvasId: {
type: String, type: String,
required: true required: true
...@@ -44,7 +44,7 @@ export const CanvasGetImageDataProtocol = { ...@@ -44,7 +44,7 @@ export const CanvasGetImageDataProtocol = {
export const CanvasPutImageDataOptions = CanvasGetImageDataOptions export const CanvasPutImageDataOptions = CanvasGetImageDataOptions
export const CanvasPutImageDataProtocol = { export const CanvasPutImageDataProtocol: ApiProtocol = {
canvasId: { canvasId: {
type: String, type: String,
required: true required: true
...@@ -98,7 +98,7 @@ export const CanvasToTempFilePathOptions: ApiOptions = { ...@@ -98,7 +98,7 @@ export const CanvasToTempFilePathOptions: ApiOptions = {
) )
} }
export const CanvasToTempFilePathProtocol = { export const CanvasToTempFilePathProtocol: ApiProtocol = {
x: { x: {
type: Number, type: Number,
default: 0 default: 0
...@@ -121,11 +121,10 @@ export const CanvasToTempFilePathProtocol = { ...@@ -121,11 +121,10 @@ export const CanvasToTempFilePathProtocol = {
}, },
canvasId: { canvasId: {
type: String, type: String,
require: true required: true
}, },
fileType: { fileType: {
type: String, type: String
require: true
}, },
quality: { quality: {
type: Number type: Number
......
export const openDocument = { import { ApiProtocol } from '../type'
export const OpenDocumentProtocol: ApiProtocol = {
filePath: { filePath: {
type: String, type: String,
required: true required: true
......
export const chooseLocation = {
keyword: {
type: String
}
}
import { ApiProtocol } from '../type'
export const ChooseLocationProtocol: ApiProtocol = {
keyword: {
type: String
}
}
const type = {
WGS84: 'WGS84',
GCJ02: 'GCJ02'
}
export const getLocation = {
type: {
type: String,
validator(value, params) {
value = (value || '').toUpperCase()
params.type = Object.values(type).indexOf(value) < 0 ? type.WGS84 : value
},
default: type.WGS84
},
altitude: {
altitude: Boolean,
default: false
}
}
import { ApiProtocol, ApiOptions } from '../type'
const coordTypes = {
WGS84: 'WGS84',
GCJ02: 'GCJ02'
}
export const GetLocationOptions: ApiOptions = {
formatArgs: {
type(value, params) {
value = (value || '').toUpperCase()
let type = coordTypes[value as keyof typeof coordTypes]
if (!type) {
type = coordTypes.WGS84
}
params.type = type
}
}
}
export const GetLocationProtocol: ApiProtocol = {
type: {
type: String,
default: coordTypes.WGS84
},
altitude: {
type: Boolean,
default: false
}
}
export const openLocation = { import { ApiProtocol, ApiOptions } from '../type'
export const OpenLocationOptions: ApiOptions = {
formatArgs: {
type(value, params) {
value = Math.floor(value)
params.scale = value >= 5 && value <= 18 ? value : 18
}
}
}
export const OpenLocationProtocol: ApiProtocol = {
latitude: { latitude: {
type: Number, type: Number,
required: true required: true
...@@ -9,10 +20,6 @@ export const openLocation = { ...@@ -9,10 +20,6 @@ export const openLocation = {
}, },
scale: { scale: {
type: Number, type: Number,
validator(value, params) {
value = Math.floor(value)
params.scale = value >= 5 && value <= 18 ? value : 18
},
default: 18 default: 18
}, },
name: { name: {
......
import { createApi } from '../../helpers/api'
export const createIntersectionObserver = createApi(() => {})
import { createApi } from '../../helpers/api'
export const createSelectorQuery = createApi(() => {})
import { initBridge } from '../../helper/bridge' import { initBridge } from '../../helpers/bridge'
export const ServiceJSBridge = initBridge('service') export const ServiceJSBridge = initBridge('service')
import { initBridge } from '../../helper/bridge' import { initBridge } from '../../helpers/bridge'
export const ViewJSBridge = initBridge('view') export const ViewJSBridge = initBridge('view')
import { ComponentPublicInstance } from 'vue'
let appVm: ComponentPublicInstance
export function getApp() {
return appVm
}
export function getCurrentPages() {
return []
}
...@@ -3,6 +3,7 @@ export { plugin } ...@@ -3,6 +3,7 @@ export { plugin }
export * from '@dcloudio/uni-components' export * from '@dcloudio/uni-components'
export * from './service/api' export * from './service/api'
export * from './service/api/uni' export * from './service/api/uni'
export { getApp, getCurrentPages } from './framework'
export { default as PageComponent } from './framework/components/page/index.vue' export { default as PageComponent } from './framework/components/page/index.vue'
export { export {
default as AsyncErrorComponent default as AsyncErrorComponent
......
import { createApi, OpenDocumentProtocol } from '@dcloudio/uni-api'
interface OpenDocumentOption {
filePath: string
}
export const openDocument = createApi((option: OpenDocumentOption) => {
window.open(option.filePath)
return true
}, OpenDocumentProtocol)
export * from './base/canIUse' export * from './base/canIUse'
export * from './device/makePhoneCall' export * from './device/makePhoneCall'
export * from './device/getSystemInfo' export * from './device/getSystemInfo'
export * from './device/getSystemInfoSync' export * from './device/getSystemInfoSync'
export * from './file/openDocument'
export * from './route/navigateBack'
export * from './route/navigateTo'
export * from './route/redirectTo'
export * from './route/reLaunch'
export * from './route/switchTab'
export { export {
arrayBufferToBase64,
base64ToArrayBuffer,
upx2px, upx2px,
addInterceptor, addInterceptor,
removeInterceptor, removeInterceptor,
promiseInterceptor promiseInterceptor,
arrayBufferToBase64,
base64ToArrayBuffer,
createSelectorQuery,
createIntersectionObserver
} from '@dcloudio/uni-api' } from '@dcloudio/uni-api'
import { createApi } from '@dcloudio/uni-api'
export const navigateBack = createApi(() => {})
import { createApi } from '@dcloudio/uni-api'
export const navigateTo = createApi(() => {})
import { createApi } from '@dcloudio/uni-api'
export const reLaunch = createApi(() => {})
import { createApi } from '@dcloudio/uni-api'
export const redirectTo = createApi(() => {})
import { createApi } from '@dcloudio/uni-api'
export const switchTab = createApi(() => {})
...@@ -4,6 +4,6 @@ import { ServiceJSBridge } from '@dcloudio/uni-core' ...@@ -4,6 +4,6 @@ import { ServiceJSBridge } from '@dcloudio/uni-core'
export default extend(ServiceJSBridge, { export default extend(ServiceJSBridge, {
publishHandler(event: string, args: any, pageId: number) { publishHandler(event: string, args: any, pageId: number) {
global.UniViewJSBridge.subscribeHandler(event, args, pageId) window.UniViewJSBridge.subscribeHandler(event, args, pageId)
} }
}) })
...@@ -4,6 +4,6 @@ import { ViewJSBridge } from '@dcloudio/uni-core' ...@@ -4,6 +4,6 @@ import { ViewJSBridge } from '@dcloudio/uni-core'
export default extend(ViewJSBridge, { export default extend(ViewJSBridge, {
publishHandler(event: string, args: any, pageId: number) { publishHandler(event: string, args: any, pageId: number) {
global.UniServiceJSBridge.subscribeHandler(event, args, pageId) window.UniServiceJSBridge.subscribeHandler(event, args, pageId)
} }
}) })
import { import { isPlainObject } from '@vue/shared'
LocationQueryRaw,
stringifyQuery as stringifyLocationQuery const encode = encodeURIComponent
} from 'vue-router' export function stringifyQuery(obj?: Record<string, any>, encodeStr = encode) {
export function stringifyQuery(query?: LocationQueryRaw) { const res = obj
if (query) { ? Object.keys(obj)
const querystring = stringifyLocationQuery(query) .map(key => {
if (querystring) { let val = obj[key]
return '?' + querystring if (typeof val === undefined || val === null) {
} val = ''
} } else if (isPlainObject(val)) {
return '' val = JSON.stringify(val)
}
return encodeStr(key) + '=' + encodeStr(val)
})
.filter(x => x.length > 0)
.join('&')
: null
return res ? `?${res}` : ''
} }
...@@ -21,7 +21,9 @@ ...@@ -21,7 +21,9 @@
}, },
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@rollup/plugin-inject": "^4.0.2", "@rollup/pluginutils": "^4.0.0",
"estree-walker": "^2.0.1",
"magic-string": "^0.25.7",
"slash": "^3.0.0" "slash": "^3.0.0"
}, },
"peerDependencies": { "peerDependencies": {
......
import { Plugin } from 'rollup' import { sep } from 'path'
import { RollupInjectOptions } from '@rollup/plugin-inject' import { Plugin, AcornNode } from 'rollup'
import inject from '@rollup/plugin-inject' import {
BaseNode,
Program,
Property,
Identifier,
MemberExpression,
MethodDefinition,
ExportSpecifier
} from 'estree'
const APIS = [ import {
'upx2px', attachScopes,
'canIUse', createFilter,
'makePhoneCall', makeLegalIdentifier
'getSystemInfo', } from '@rollup/pluginutils'
'getSystemInfoSync',
'arrayBufferToBase64',
'base64ToArrayBuffer'
]
const injectOptions: RollupInjectOptions = { import { walk } from 'estree-walker'
exclude: /\.[n]?vue$/,
// @dcloudio/uni-api/src/service/base/upx2px->checkDeviceWidth import MagicString from 'magic-string'
'__GLOBAL__.getSystemInfoSync': ['@dcloudio/uni-h5', 'getSystemInfoSync']
interface Scope {
parent: Scope
contains: (name: string) => boolean
} }
APIS.forEach(api => { type Injectment = string | [string, string]
injectOptions['uni.' + api] = ['@dcloudio/uni-h5', api]
}) export interface InjectOptions {
include?: string | RegExp | ReadonlyArray<string | RegExp> | null
exclude?: string | RegExp | ReadonlyArray<string | RegExp> | null
sourceMap?: boolean
[str: string]: Injectment | InjectOptions['include'] | Boolean
}
const escape = (str: string) => str.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&')
const isProperty = (node: BaseNode): node is Property =>
node.type === 'Property'
const isIdentifier = (node: BaseNode): node is Identifier =>
node.type === 'Identifier'
const isMemberExpression = (node: BaseNode): node is MemberExpression =>
node.type === 'MemberExpression'
const isMethodDefinition = (node: BaseNode): node is MethodDefinition =>
node.type === 'MethodDefinition'
const isExportSpecifier = (node: BaseNode): node is ExportSpecifier =>
node.type === 'ExportSpecifier'
const isReference = (node: BaseNode, parent: BaseNode): boolean => {
if (isMemberExpression(node)) {
return !node.computed && isReference(node.object, node)
}
if (isIdentifier(node)) {
if (isMemberExpression(parent))
return parent.computed || node === parent.object
// `bar` in { bar: foo }
if (isProperty(parent) && node !== parent.value) return false
// `bar` in `class Foo { bar () {...} }`
if (isMethodDefinition(parent)) return false
// `bar` in `export { foo as bar }`
if (isExportSpecifier(parent) && node !== parent.local) return false
return true
}
return false
}
const flatten = (startNode: BaseNode) => {
const parts = []
let node = startNode
while (isMemberExpression(node)) {
parts.unshift((node.property as Identifier).name)
node = node.object
}
const { name } = node as Identifier
parts.unshift(name)
return { name, keypath: parts.join('.') }
}
function normalizeModulesMap(
modulesMap: Map<string, string | [string, string]>
) {
modulesMap.forEach((mod, key) => {
modulesMap.set(
key,
Array.isArray(mod)
? [mod[0].split(sep).join('/'), mod[1]]
: mod.split(sep).join('/')
)
})
}
function inject(options: InjectOptions) {
if (!options) throw new Error('Missing options')
const filter = createFilter(options.include, options.exclude)
const modules = Object.assign({}, options) as { [str: string]: Injectment }
delete modules.include
delete modules.exclude
delete modules.sourceMap
const modulesMap = new Map<string, string | [string, string]>()
const namespaceModulesMap = new Map<string, string | [string, string]>()
Object.keys(modules).forEach(name => {
if (name.endsWith('.')) {
namespaceModulesMap.set(name, modules[name])
}
modulesMap.set(name, modules[name])
})
const hasNamespace = namespaceModulesMap.size > 0
// Fix paths on Windows
if (sep !== '/') {
normalizeModulesMap(modulesMap)
normalizeModulesMap(namespaceModulesMap)
}
const firstpass = new RegExp(
`(?:${Array.from(modulesMap.keys())
.map(escape)
.join('|')})`,
'g'
)
const sourceMap = options.sourceMap !== false
export const buildPluginInject: Plugin = inject(injectOptions) return {
name: 'inject',
transform(code, id) {
if (!filter(id)) return null
if (code.search(firstpass) === -1) return null
if (sep !== '/') id = id.split(sep).join('/')
let ast = null
try {
ast = (this.parse(code) as unknown) as Program
} catch (err) {
this.warn({
code: 'PARSE_ERROR',
message: `plugin-inject: failed to parse ${id}. Consider restricting the plugin to particular files via options.include`
})
}
if (!ast) {
return null
}
const imports = new Set()
ast.body.forEach(node => {
if (node.type === 'ImportDeclaration') {
node.specifiers.forEach(specifier => {
imports.add(specifier.local.name)
})
}
})
// analyse scopes
let scope = attachScopes(ast, 'scope') as Scope
const magicString = new MagicString(code)
const newImports = new Map()
function handleReference(node: BaseNode, name: string, keypath: string) {
let mod = modulesMap.get(keypath)
if (!mod && hasNamespace) {
const mods = keypath.split('.')
if (mods.length === 2) {
mod = namespaceModulesMap.get(mods[0] + '.')
if (mod) {
mod = [mod as string, mods[1]]
}
}
}
if (mod && !imports.has(name) && !scope.contains(name)) {
if (typeof mod === 'string') mod = [mod, 'default']
if (mod[0] === id) return false
const hash = `${keypath}:${mod[0]}:${mod[1]}`
const importLocalName =
name === keypath ? name : makeLegalIdentifier(`$inject_${keypath}`)
if (!newImports.has(hash)) {
if (mod[1] === '*') {
newImports.set(
hash,
`import * as ${importLocalName} from '${mod[0]}';`
)
} else {
newImports.set(
hash,
`import { ${mod[1]} as ${importLocalName} } from '${mod[0]}';`
)
}
}
if (name !== keypath) {
magicString.overwrite(
(node as AcornNode).start,
(node as AcornNode).end,
importLocalName,
{
storeName: true
}
)
}
return true
}
return false
}
walk(ast, {
enter(node, parent) {
if (sourceMap) {
magicString.addSourcemapLocation((node as AcornNode).start)
magicString.addSourcemapLocation((node as AcornNode).end)
}
if ((node as any).scope) {
scope = (node as any).scope
}
if (isProperty(node) && node.shorthand) {
const { name } = node.key as Identifier
handleReference(node, name, name)
this.skip()
return
}
if (isReference(node, parent)) {
const { name, keypath } = flatten(node)
const handled = handleReference(node, name, keypath)
if (handled) {
this.skip()
}
}
},
leave(node) {
if ((node as any).scope) {
scope = scope.parent
}
}
})
if (newImports.size === 0) {
return {
code,
ast,
map: sourceMap ? magicString.generateMap({ hires: true }) : null
}
}
const importBlock = Array.from(newImports.values()).join('\n\n')
magicString.prepend(`${importBlock}\n\n`)
return {
code: magicString.toString(),
map: sourceMap ? magicString.generateMap({ hires: true }) : null
}
}
} as Plugin
}
export const buildPluginInject: Plugin = inject({
exclude: /\.[n]?vue$/,
'__GLOBAL__.': '@dcloudio/uni-h5',
'uni.': '@dcloudio/uni-h5'
})
import { DepOptimizationOptions } from 'vite' import { DepOptimizationOptions } from 'vite'
export const optimizeDeps: DepOptimizationOptions = { export const optimizeDeps: DepOptimizationOptions = {
exclude: ['vue-router'] exclude: [
'vue-router',
'@dcloudio/uni-components',
'@dcloudio/uni-h5',
'@dcloudio/uni-h5-vue',
'@dcloudio/uni-shared'
]
} }
...@@ -2,7 +2,9 @@ import { ServerPlugin } from 'vite' ...@@ -2,7 +2,9 @@ import { ServerPlugin } from 'vite'
import { readBody } from 'vite' import { readBody } from 'vite'
import { parsePagesJson } from '../utils' import { parsePagesJson } from '../utils'
const uniCode = `import {uni} from '@dcloudio/uni-h5' const uniCode = `import {uni,getCurrentPages,getApp} from '@dcloudio/uni-h5'
window.getApp = getApp
window.getCurrentPages = getCurrentPages
window.uni = window.__GLOBAL__ = uni window.uni = window.__GLOBAL__ = uni
` `
......
...@@ -46,6 +46,7 @@ function createConfig(entryFile, output, plugins = []) { ...@@ -46,6 +46,7 @@ function createConfig(entryFile, output, plugins = []) {
const external = [ const external = [
'@vue/shared', '@vue/shared',
'@dcloudio/uni-shared',
...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.dependencies || {}),
...Object.keys(pkg.peerDependencies || {}) ...Object.keys(pkg.peerDependencies || {})
] ]
......
...@@ -571,15 +571,6 @@ ...@@ -571,15 +571,6 @@
magic-string "^0.25.7" magic-string "^0.25.7"
resolve "^1.17.0" resolve "^1.17.0"
"@rollup/plugin-inject@^4.0.2":
version "4.0.2"
resolved "https://registry.npmjs.org/@rollup/plugin-inject/-/plugin-inject-4.0.2.tgz#55b21bb244a07675f7fdde577db929c82fc17395"
integrity sha512-TSLMA8waJ7Dmgmoc8JfPnwUwVZgLjjIAM6MqeIFqPO2ODK36JqE0Cf2F54UTgCUuW8da93Mvoj75a6KAVWgylw==
dependencies:
"@rollup/pluginutils" "^3.0.4"
estree-walker "^1.0.1"
magic-string "^0.25.5"
"@rollup/plugin-json@^4.0.0", "@rollup/plugin-json@^4.0.3": "@rollup/plugin-json@^4.0.0", "@rollup/plugin-json@^4.0.3":
version "4.1.0" version "4.1.0"
resolved "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-4.1.0.tgz#54e09867ae6963c593844d8bd7a9c718294496f3" resolved "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-4.1.0.tgz#54e09867ae6963c593844d8bd7a9c718294496f3"
...@@ -608,7 +599,7 @@ ...@@ -608,7 +599,7 @@
"@rollup/pluginutils" "^3.0.8" "@rollup/pluginutils" "^3.0.8"
magic-string "^0.25.5" magic-string "^0.25.5"
"@rollup/pluginutils@^3.0.4", "@rollup/pluginutils@^3.0.8", "@rollup/pluginutils@^3.0.9", "@rollup/pluginutils@^3.1.0": "@rollup/pluginutils@^3.0.8", "@rollup/pluginutils@^3.0.9", "@rollup/pluginutils@^3.1.0":
version "3.1.0" version "3.1.0"
resolved "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz#706b4524ee6dc8b103b3c995533e5ad680c02b9b" resolved "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz#706b4524ee6dc8b103b3c995533e5ad680c02b9b"
integrity sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg== integrity sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==
...@@ -617,6 +608,15 @@ ...@@ -617,6 +608,15 @@
estree-walker "^1.0.1" estree-walker "^1.0.1"
picomatch "^2.2.2" picomatch "^2.2.2"
"@rollup/pluginutils@^4.0.0":
version "4.0.0"
resolved "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.0.0.tgz#e18e9f5a3925779fc15209dd316c1bd260d195ef"
integrity sha512-b5QiJRye4JlSg29bKNEECoKbLuPXZkPEHSgEjjP1CJV1CPdDBybfYHfm6kyq8yK51h/Zsyl8OvWUrp0FUBukEQ==
dependencies:
"@types/estree" "0.0.45"
estree-walker "^2.0.1"
picomatch "^2.2.2"
"@rushstack/node-core-library@3.30.0": "@rushstack/node-core-library@3.30.0":
version "3.30.0" version "3.30.0"
resolved "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.30.0.tgz#a2b814a611a040ac69d6c31ffc92bf9155c983fb" resolved "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.30.0.tgz#a2b814a611a040ac69d6c31ffc92bf9155c983fb"
...@@ -741,7 +741,7 @@ ...@@ -741,7 +741,7 @@
resolved "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" resolved "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d"
integrity sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag== integrity sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==
"@types/estree@*": "@types/estree@*", "@types/estree@0.0.45":
version "0.0.45" version "0.0.45"
resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.45.tgz#e9387572998e5ecdac221950dab3e8c3b16af884" resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.45.tgz#e9387572998e5ecdac221950dab3e8c3b16af884"
integrity sha512-jnqIUKDUqJbDIUxm0Uj7bnlMnRm1T/eZ9N+AVMqhPgzrba2GhGG5o/jCTwmdPK709nEZsGoMzXEDUjcXHa3W0g== integrity sha512-jnqIUKDUqJbDIUxm0Uj7bnlMnRm1T/eZ9N+AVMqhPgzrba2GhGG5o/jCTwmdPK709nEZsGoMzXEDUjcXHa3W0g==
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册