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

feat(h5): api Tree-Shaking

上级 d374f346
......@@ -2,9 +2,21 @@ export * from './service/base/base64'
export * from './service/base/upx2px'
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/device/makePhoneCall'
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 { isSyncApi, isContextApi, promisify } from './helpers/promise'
......@@ -10,7 +10,7 @@ function getInt(name: string) {
}
}
export const CanvasGetImageDataOptions = {
export const CanvasGetImageDataOptions: ApiOptions = {
formatArgs: {
x: getInt('x'),
y: getInt('y'),
......@@ -19,7 +19,7 @@ export const CanvasGetImageDataOptions = {
}
}
export const CanvasGetImageDataProtocol = {
export const CanvasGetImageDataProtocol: ApiProtocol = {
canvasId: {
type: String,
required: true
......@@ -44,7 +44,7 @@ export const CanvasGetImageDataProtocol = {
export const CanvasPutImageDataOptions = CanvasGetImageDataOptions
export const CanvasPutImageDataProtocol = {
export const CanvasPutImageDataProtocol: ApiProtocol = {
canvasId: {
type: String,
required: true
......@@ -98,7 +98,7 @@ export const CanvasToTempFilePathOptions: ApiOptions = {
)
}
export const CanvasToTempFilePathProtocol = {
export const CanvasToTempFilePathProtocol: ApiProtocol = {
x: {
type: Number,
default: 0
......@@ -121,11 +121,10 @@ export const CanvasToTempFilePathProtocol = {
},
canvasId: {
type: String,
require: true
required: true
},
fileType: {
type: String,
require: true
type: String
},
quality: {
type: Number
......
export const openDocument = {
import { ApiProtocol } from '../type'
export const OpenDocumentProtocol: ApiProtocol = {
filePath: {
type: String,
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: {
type: Number,
required: true
......@@ -9,10 +20,6 @@ export const openLocation = {
},
scale: {
type: Number,
validator(value, params) {
value = Math.floor(value)
params.scale = value >= 5 && value <= 18 ? value : 18
},
default: 18
},
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')
import { initBridge } from '../../helper/bridge'
import { initBridge } from '../../helpers/bridge'
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 }
export * from '@dcloudio/uni-components'
export * from './service/api'
export * from './service/api/uni'
export { getApp, getCurrentPages } from './framework'
export { default as PageComponent } from './framework/components/page/index.vue'
export {
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 './device/makePhoneCall'
export * from './device/getSystemInfo'
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 {
arrayBufferToBase64,
base64ToArrayBuffer,
upx2px,
addInterceptor,
removeInterceptor,
promiseInterceptor
promiseInterceptor,
arrayBufferToBase64,
base64ToArrayBuffer,
createSelectorQuery,
createIntersectionObserver
} 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'
export default extend(ServiceJSBridge, {
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'
export default extend(ViewJSBridge, {
publishHandler(event: string, args: any, pageId: number) {
global.UniServiceJSBridge.subscribeHandler(event, args, pageId)
window.UniServiceJSBridge.subscribeHandler(event, args, pageId)
}
})
import {
LocationQueryRaw,
stringifyQuery as stringifyLocationQuery
} from 'vue-router'
export function stringifyQuery(query?: LocationQueryRaw) {
if (query) {
const querystring = stringifyLocationQuery(query)
if (querystring) {
return '?' + querystring
}
}
return ''
import { isPlainObject } from '@vue/shared'
const encode = encodeURIComponent
export function stringifyQuery(obj?: Record<string, any>, encodeStr = encode) {
const res = obj
? Object.keys(obj)
.map(key => {
let val = obj[key]
if (typeof val === undefined || val === null) {
val = ''
} else if (isPlainObject(val)) {
val = JSON.stringify(val)
}
return encodeStr(key) + '=' + encodeStr(val)
})
.filter(x => x.length > 0)
.join('&')
: null
return res ? `?${res}` : ''
}
......@@ -21,7 +21,9 @@
},
"license": "Apache-2.0",
"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"
},
"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 = [
'upx2px',
'canIUse',
'makePhoneCall',
'getSystemInfo',
'getSystemInfoSync',
'arrayBufferToBase64',
'base64ToArrayBuffer'
]
import {
attachScopes,
createFilter,
makeLegalIdentifier
} from '@rollup/pluginutils'
const injectOptions: RollupInjectOptions = {
exclude: /\.[n]?vue$/,
// @dcloudio/uni-api/src/service/base/upx2px->checkDeviceWidth
'__GLOBAL__.getSystemInfoSync': ['@dcloudio/uni-h5', 'getSystemInfoSync']
import { walk } from 'estree-walker'
import MagicString from 'magic-string'
interface Scope {
parent: Scope
contains: (name: string) => boolean
}
APIS.forEach(api => {
injectOptions['uni.' + api] = ['@dcloudio/uni-h5', api]
})
type Injectment = string | [string, string]
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'
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'
import { readBody } from 'vite'
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
`
......
......@@ -46,6 +46,7 @@ function createConfig(entryFile, output, plugins = []) {
const external = [
'@vue/shared',
'@dcloudio/uni-shared',
...Object.keys(pkg.dependencies || {}),
...Object.keys(pkg.peerDependencies || {})
]
......
......@@ -571,15 +571,6 @@
magic-string "^0.25.7"
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":
version "4.1.0"
resolved "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-4.1.0.tgz#54e09867ae6963c593844d8bd7a9c718294496f3"
......@@ -608,7 +599,7 @@
"@rollup/pluginutils" "^3.0.8"
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"
resolved "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz#706b4524ee6dc8b103b3c995533e5ad680c02b9b"
integrity sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==
......@@ -617,6 +608,15 @@
estree-walker "^1.0.1"
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":
version "3.30.0"
resolved "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.30.0.tgz#a2b814a611a040ac69d6c31ffc92bf9155c983fb"
......@@ -741,7 +741,7 @@
resolved "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d"
integrity sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==
"@types/estree@*":
"@types/estree@*", "@types/estree@0.0.45":
version "0.0.45"
resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.45.tgz#e9387572998e5ecdac221950dab3e8c3b16af884"
integrity sha512-jnqIUKDUqJbDIUxm0Uj7bnlMnRm1T/eZ9N+AVMqhPgzrba2GhGG5o/jCTwmdPK709nEZsGoMzXEDUjcXHa3W0g==
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册