小程序 foreach_【第2106期】小程序依赖分析实践

前言

这种可视化分析图还是很直观的,很有趣。今日早读文章由自然醒授权分享。

正文从这开始~~

用过 webpack 的同学肯定知道 webpack-bundle-analyzer ,可以用来分析当前项目 js 文件的依赖关系。

e2241aa023f6116c9daf6db562b8df37.png

webpack-bundle-analyzer

因为最近一直在做小程序业务,而且小程序对包体大小特别敏感,所以就想着能不能做一个类似的工具,用来查看当前小程序各个主包与分包之间的依赖关系。经过几天的折腾终于做出来了,效果如下:

01d8415c14d8c22ffcd3af66d67d6343.png

小程序依赖关系

今天的文章就带大家来实现这个工具。

小程序入口

小程序的页面通过 app.jsonpages 参数定义,用于指定小程序由哪些页面组成,每一项都对应一个页面的路径(含文件名) 信息。 pages 内的每个页面,小程序都会去寻找对应的 json, js, wxml, wxss 四个文件进行处理。

如开发目录为:

├── app.js

├── app.json

├── app.wxss

├── pages

│ │── index

│ │ ├── index.wxml

│ │ ├── index.js

│ │ ├── index.json

│ │ └── index.wxss

│ └── logs

│ ├── logs.wxml

│ └── logs.js

└── utils

则需要在 app.json 中写:

{

"pages": ["pages/index/index", "pages/logs/logs"]

}

为了方便演示,我们先 fork 一份小程序的官方demo,然后新建一个文件 depend.js,依赖分析相关的工作就在这个文件里面实现。

$ git clone git@github.com:wechat-miniprogram/miniprogram-demo.git

$ cd miniprogram-demo

$ touch depend.js

其大致的目录结构如下:

3fc9189f9ce6329092fe3ec468044cf7.png

目录结构

app.json 为入口,我们可以获取所有主包下的页面。

const fs = require('fs-extra')

const path = require('path')

const root = process.cwd()

class Depend {

constructor() {

this.context = path.join(root, 'miniprogram')

}

// 获取绝对地址

getAbsolute(file) {

return path.join(this.context, file)

}

run() {

const appPath = this.getAbsolute('app.json')

const appJson = fs.readJsonSync(appPath)

const { pages } = appJson // 主包的所有页面

}

}

每个页面会对应 json, js, wxml, wxss 四个文件:

const Extends = ['.js', '.json', '.wxml', '.wxss']

class Depend {

constructor() {

// 存储文件

this.files = new Set()

this.context = path.join(root, 'miniprogram')

}

// 修改文件后缀

replaceExt(filePath, ext = '') {

const dirName = path.dirname(filePath)

const extName = path.extname(filePath)

const fileName = path.basename(filePath, extName)

return path.join(dirName, fileName + ext)

}

run() {

// 省略获取 pages 过程

pages.forEach(page => {

// 获取绝对地址

const absPath = this.getAbsolute(page)

Extends.forEach(ext => {

// 每个页面都需要判断 js、json、wxml、wxss 是否存在

const filePath = this.replaceExt(absPath, ext)

if (fs.existsSync(filePath)) {

this.files.add(filePath)

}

})

})

}

}

现在 pages 内页面相关的文件都放到 files 字段存起来了。

构造树形结构

拿到文件后,我们需要依据各个文件构造一个树形结构的文件树,用于后续展示依赖关系。

假设我们有一个 pages 目录, pages 目录下有两个页面: detailindex ,这两个 页面文件夹下有四个对应的文件。

pages

├── detail

│ ├── detail.js

│ ├── detail.json

│ ├── detail.wxml

│ └── detail.wxss

└── index

├── index.js

├── index.json

├── index.wxml

└── index.wxss

依据上面的目录结构,我们构造一个如下的文件树结构, size 用于表示当前文件或文件夹的大小, children 存放文件夹下的文件,如果是文件则没有 children 属性。

pages = {

"size": 8,

"children": {

"detail": {

"size": 4,

"children": {

"detail.js": { "size": 1 },

"detail.json": { "size": 1 },

"detail.wxml": { "size": 1 },

"detail.wxss": { "size": 1 }

}

},

"index": {

"size": 4,

"children": {

"index.js": { "size": 1 },

"index.json": { "size": 1 },

"index.wxml": { "size": 1 },

"index.wxss": { "size": 1 }

}

}

}

}

我们先在构造函数构造一个 tree 字段用来存储文件树的数据,然后我们将每个文件都传入 addToTree 方法,将文件添加到树中 。

class Depend {

constructor() {

this.tree = {

size: 0,

children: {}

}

this.files = new Set()

this.context = path.join(root, 'miniprogram')

}

run() {

// 省略获取 pages 过程

pages.forEach(page => {

const absPath = this.getAbsolute(page)

Extends.forEach(ext => {

const filePath = this.replaceExt(absPath, ext)

if (fs.existsSync(filePath)) {

// 调用 addToTree

this.addToTree(filePath)

}

})

})

}

}

接下来实现 addToTree 方法:

class Depend {

// 省略之前的部分代码

// 获取相对地址

getRelative(file) {

return path.relative(this.context, file)

}

// 获取文件大小,单位 KB

getSize(file) {

const stats = fs.statSync(file)

return stats.size / 1024

}

// 将文件添加到树中

addToTree(filePath) {

if (this.files.has(filePath)) {

// 如果该文件已经添加过,则不再添加到文件树中

return

}

const size = this.getSize(filePath)

const relPath = this.getRelative(filePath)

// 将文件路径转化成数组

// 'pages/index/index.js' =>

// ['pages', 'index', 'index.js']

const names = relPath.split(path.sep)

const lastIdx = names.length - 1

this.tree.size += size

let point = this.tree.children

names.forEach((name, idx) => {

if (idx === lastIdx) {

point[name] = { size }

return

}

if (!point[name]) {

point[name] = {

size, children: {}

}

} else {

point[name].size += size

}

point = point[name].children

})

// 将文件添加的 files

this.files.add(filePath)

}

}

我们可以在运行之后,将文件输出到 tree.json 看看。

run() {

// ...

pages.forEach(page => {

//...

})

fs.writeJSONSync('tree.json', this.tree, { spaces: 2 })

}

aea8580a6bb9b168968e187e07b215b0.png

tree.json

获取依赖关系

上面的步骤看起来没什么问题,但是我们缺少了重要的一环,那就是我们在构造文件树之前,还需要得到每个文件的依赖项,这样输出的才是小程序完整的文件树。文件的依赖关系需要分成四部分来讲,分别是 js, json, wxml, wxss 这四种类型文件获取依赖的方式。

获取 .js 文件依赖

小程序支持 CommonJS 的方式进行模块化,如果开启了 es6,也能支持 ESM 进行模块化。我们如果要获得一个 js 文件的依赖,首先要明确,js 文件导入模块的三种写法,针对下面三种语法,我们可以引入 Babel 来获取依赖。

import a from './a.js'

export b from './b.js'

const c = require('./c.js')

通过 @babel/parser 将代码转化为 AST,然后通过 @babel/traverse 遍历 AST 节点,获取上面三种导入方式的值,放到数组。

const { parse } = require('@babel/parser')

const { default: traverse } = require('@babel/traverse')

class Depend {

// ...

jsDeps(file) {

const deps = []

const dirName = path.dirname(file)

// 读取 js 文件内容

const content = fs.readFileSync(file, 'utf-8')

// 将代码转化为 AST

const ast = parse(content, {

sourceType: 'module',

plugins: ['exportDefaultFrom']

})

// 遍历 AST

traverse(ast, {

ImportDeclaration: ({ node }) => {

// 获取 import from 地址

const { value } = node.source

const jsFile = this.transformScript(dirName, value)

if (jsFile) {

deps.push(jsFile)

}

},

ExportNamedDeclaration: ({ node }) => {

// 获取 export from 地址

const { value } = node.source

const jsFile = this.transformScript(dirName, value)

if (jsFile) {

deps.push(jsFile)

}

},

CallExpression: ({ node }) => {

if (

(node.callee.name && node.callee.name === 'require') &&

node.arguments.length >= 1

) {

// 获取 require 地址

const [{ value }] = node.arguments

const jsFile = this.transformScript(dirName, value)

if (jsFile) {

deps.push(jsFile)

}

}

}

})

return deps

}

}

在获取依赖模块的路径后,还不能立即将路径添加到依赖数组内,因为根据模块语法 js 后缀是可以省略的,另外 require 的路径是一个文件夹的时候,默认会导入该文件夹下的 index.js

class Depend {

// 获取某个路径的脚本文件

transformScript(url) {

const ext = path.extname(url)

// 如果存在后缀,表示当前已经是一个文件

if (ext === '.js' && fs.existsSync(url)) {

return url

}

// a/b/c => a/b/c.js

const jsFile = url + '.js'

if (fs.existsSync(jsFile)) {

return jsFile

}

// a/b/c => a/b/c/index.js

const jsIndexFile = path.join(url, 'index.js')

if (fs.existsSync(jsIndexFile)) {

return jsIndexFile

}

return null

}

jsDeps(file) {...}

}

我们可以创建一个 js,看看输出的 deps 是否正确:

// 文件路径:/Users/shenfq/Code/fork/miniprogram-demo/

import a from './a.js'

export b from '../b.js'

const c = require('../../c.js')

da953e411d32f483d22cfdb130347626.png

获取 .json 文件依赖

json 文件本身是不支持模块化的,但是小程序可以通过 json 文件导入自定义组件,只需要在页面的 json 文件通过 usingComponents 进行引用声明。 usingComponents 为一个对象,键为自定义组件的标签名,值为自定义组件文件路径:

{

"usingComponents": {

"component-tag-name": "path/to/the/custom/component"

}

}

自定义组件与小程序页面一样,也会对应四个文件,所以我们需要获取 jsonusingComponents 内的所有依赖项,并判断每个组件对应的那四个文件是否存在,然后添加到依赖项内。

class Depend {

// ...

jsonDeps(file) {

const deps = []

const dirName = path.dirname(file)

const { usingComponents } = fs.readJsonSync(file)

if (usingComponents && typeof usingComponents === 'object') {

Object.values(usingComponents).forEach((component) => {

component = path.resolve(dirName, component)

// 每个组件都需要判断 js/json/wxml/wxss 文件是否存在

Extends.forEach((ext) => {

const file = this.replaceExt(component, ext)

if (fs.existsSync(file)) {

deps.push(file)

}

})

})

}

return deps

}

}

获取 .wxml 文件依赖

wxml 提供两种文件引用方式 importinclude

src="a.wxml"/>

src="b.wxml"/>

wxml 文件本质上还是一个 html 文件,所以可以通过 html parser 对 wxml 文件进行解析,关于 html parser 相关的原理可以看我之前写过的文章 《Vue 模板编译原理》。

const htmlparser2 = require('htmlparser2')

class Depend {

// ...

wxmlDeps(file) {

const deps = []

const dirName = path.dirname(file)

const content = fs.readFileSync(file, 'utf-8')

const htmlParser = new htmlparser2.Parser({

onopentag(name, attribs = {}) {

if (name !== 'import' && name !== 'require') {

return

}

const { src } = attribs

if (src) {

return

}

const wxmlFile = path.resolve(dirName, src)

if (fs.existsSync(wxmlFile)) {

deps.push(wxmlFile)

}

}

})

htmlParser.write(content)

htmlParser.end()

return deps

}

}

获取 .wxss 文件依赖

最后 wxss 文件导入样式和 css 语法一致,使用 @import 语句可以导入外联样式表。

@import "common.wxss";

可以通过 postcss 解析 wxss 文件,然后获取导入文件的地址,但是这里我们偷个懒,直接通过简单的正则匹配来做。

class Depend {

// ...

wxssDeps(file) {

const deps = []

const dirName = path.dirname(file)

const content = fs.readFileSync(file, 'utf-8')

const importRegExp = /@import\s*['"](.+)['"];*/g

let matched

while ((matched = importRegExp.exec(content)) !== null) {

if (!matched[1]) {

continue

}

const wxssFile = path.resolve(dirName, matched[1])

if (fs.existsSync(wxmlFile)) {

deps.push(wxssFile)

}

}

return deps

}

}

将依赖添加到树结构中

现在我们需要修改 addToTree 方法。

class Depend {

addToTree(filePath) {

// 如果该文件已经添加过,则不再添加到文件树中

if (this.files.has(filePath)) {

return

}

const relPath = this.getRelative(filePath)

const names = relPath.split(path.sep)

names.forEach((name, idx) => {

// ... 添加到树中

})

this.files.add(filePath)

// ===== 获取文件依赖,并添加到树中 =====

const deps = this.getDeps(filePath)

deps.forEach(dep => {

this.addToTree(dep)

})

}

}

363e705dc85bbe8555c33ea506441187.png

获取分包依赖

熟悉小程序的同学肯定知道,小程序提供了分包机制。使用分包后,分包内的文件会被打包成一个单独的包,在用到的时候才会加载,而其他的文件则会放在主包,小程序打开的时候就会加载。 subpackages 中,每个分包的配置有以下几项:

字段类型说明
rootString分包根目录
nameString分包别名,分包预下载时可以使用
pagesStringArray分包页面路径,相对与分包根目录
independentBoolean分包是否是独立分包

所以我们在运行的时候,除了要拿到 pages 下的所有页面,还需拿到 subpackages 中所有的页面。由于之前只关心主包的内容, this.tree 下面只有一颗文件树,现在我们需要在 this.tree 下挂载多颗文件树,我们需要先为主包创建一个单独的文件树,然后为每个分包创建一个文件树。

class Depend {

constructor() {

this.tree = {}

this.files = new Set()

this.context = path.join(root, 'miniprogram')

}

createTree(pkg) {

this.tree[pkg] = {

size: 0,

children: {}

}

}

addPage(page, pkg) {

const absPath = this.getAbsolute(page)

Extends.forEach(ext => {

const filePath = this.replaceExt(absPath, ext)

if (fs.existsSync(filePath)) {

this.addToTree(filePath, pkg)

}

})

}

run() {

const appPath = this.getAbsolute('app.json')

const appJson = fs.readJsonSync(appPath)

const { pages, subPackages, subpackages } = appJson

this.createTree('main') // 为主包创建文件树

pages.forEach(page => {

this.addPage(page, 'main')

})

// 由于 app.json 中 subPackages、subpackages 都能生效

// 所以我们两个属性都获取,哪个存在就用哪个

const subPkgs = subPackages || subpackages

// 分包存在的时候才进行遍历

subPkgs && subPkgs.forEach(({ root, pages }) => {

root = root.split('/').join(path.sep)

this.createTree(root) // 为分包创建文件树

pages.forEach(page => {

this.addPage(`${root}${path.sep}${page}`, pkg)

})

})

// 输出文件树

fs.writeJSONSync('tree.json', this.tree, { spaces: 2 })

}

}

addToTree 方法也需要进行修改,根据传入的 pkg 来判断将当前文件添加到哪个树。

class Depend {

addToTree(filePath, pkg = 'main') {

if (this.files.has(filePath)) {

// 如果该文件已经添加过,则不再添加到文件树中

return

}

let relPath = this.getRelative(filePath)

if (pkg !== 'main' && relPath.indexOf(pkg) !== 0) {

// 如果该文件不是以分包名开头,证明该文件不在分包内,

// 需要将文件添加到主包的文件树内

pkg = 'main'

}

const tree = this.tree[pkg] // 依据 pkg 取到对应的树

const size = this.getSize(filePath)

const names = relPath.split(path.sep)

const lastIdx = names.length - 1

tree.size += size

let point = tree.children

names.forEach((name, idx) => {

// ... 添加到树中

})

this.files.add(filePath)

// ===== 获取文件依赖,并添加到树中 =====

const deps = this.getDeps(filePath)

deps.forEach(dep => {

this.addToTree(dep)

})

}

}

这里有一点需要注意,如果 package/a 分包下的文件依赖的文件不在 package/a 文件夹下,则该文件需要放入主包的文件树内。

通过 EChart 画图

经过上面的流程后,最终我们可以得到如下的一个 json 文件:

4c9eb05f6406a74212e1737cabd2b9bf.png

tree.json

接下来,我们利用 ECharts 的画图能力,将这个 json 数据以图表的形式展现出来。我们可以在 ECharts 提供的实例中看到一个 Disk Usage 的案例,很符合我们的预期。

829a123433abb716e7d79b66f15443c5.png

ECharts

ECharts 的配置这里就不再赘述,按照官网的 demo 即可,我们需要把 tree.json 的数据转化为 ECharts 需要的格式就行了,完整的代码放到 codesandbod 了,去下面的线上地址就能看到效果了。

线上地址:https://codesandbox.io/s/cold-dawn-kufc9

15b74ac039d991b86b90a151febc3c7b.png

最后效果

总结

这篇文章比较偏实践,所以贴了很多的代码,另外本文对各个文件的依赖获取提供了一个思路,虽然这里只是用文件树构造了一个这样的依赖图。

在业务开发中,小程序 IDE 每次启动都需要进行全量的编译,开发版预览的时候会等待较长的时间,我们现在有文件依赖关系后,就可以只选取目前正在开发的页面进行打包,这样就能大大提高我们的开发效率。如果有对这部分内容感兴趣的,可以另外写一篇文章介绍下如何实现。

关于本文 作者:@自然醒 原文:https://blog.shenfq.com/2020/小程序依赖分析/

2487faa484481365055eaf45eb5a7f07.png

为你推荐

【第1806期】高德JS依赖分析工程及关键原理

【第2030期】JavaScript 启动性能瓶颈分析与解决方案

欢迎自荐投稿,前端早读课等你来

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/369759.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

python----模块

collections---------------------------------------->扩展数据类型 re-------------------------------------------------->正则相关操作,正则 匹配字符串 time----------------------------------------------->时间相关 三种格式:时间戳&…

css渲染(二) 文本

一、文本样式 首行缩进  text-indent 首行缩进是将段落的第一行缩进&#xff0c;这是常用的文本格式化效果。一般地&#xff0c;中文写作时开头空两格。[注意]该属性可以为负值&#xff1b;应用于: 块级元素(包括block和inline-block)  <div><p style"text-in…

28岁学python转行_28岁转行程序员,学Java还是Python?码农:想快点月薪过万就选它...

为什么要学Java&#xff1f;Python给人的印象简单是因为我们在用Python的时候&#xff0c;可以直接调用别人已经写好的代码接口就可以&#xff0c;相对于傻瓜模式&#xff0c;Java的许多处理都要原生很多&#xff0c;写的代码可能会多一些&#xff0c;但一旦完成封装&#xff0…

CSS布局(五) 网页布局方式

网页实质是块与块之间的位置&#xff0c;块挨着块&#xff0c;块嵌套块&#xff0c;块叠着块。 三种关系&#xff1a;相邻&#xff0c;嵌套&#xff0c;重叠。 下面介绍网页布局的常用几种方式 1.一列布局&#xff1a; 一般都是固定的宽高&#xff0c;设置margin : 0 auto来水…

变量声明declare,简单运算符运算,变量测试与内容替换

declare -/ 选项 变量名 - 设类型 取消类型 -i 设为整型 -x 设为环境变量 -p 显示类型属性&#xff08;property&#xff09; [rootlocalhost ~]# a1 [rootlocalhost ~]# declare -p a declare -- a"1" [rootlocalhost ~]# export a [rootlocalhost ~]# declare -p …

如何水平居中一个元素

在项目中经常会遇到居中问题&#xff0c;这里借鉴度娘的众多答案&#xff0c;做一个总结&#xff1a; 一、元素的水平居中 1、行级元素的水平居中 <div style"width: 200px;height: 100px;border: 1px solid cyan; text-align: center;"><span>行级元素…

Yammer Metrics,一种监视应用程序的新方法

当您运行诸如Web应用程序之类的长期应用程序时&#xff0c;最好了解一些关于它们的统计信息&#xff0c;例如&#xff0c;服务的请求数&#xff0c;请求持续时间或活动请求数。 但是还有一些更通用的信息&#xff0c;例如内部集合的状态&#xff0c;代码的某些部分被执行了多少…

mysql教程目录_MySql目录(二)

MySql索引(二) 转自&#xff1a; http://www.cnblogs.com/dreamhome/archive/2013/04/16/3025304.html 所有MySQL列类型可以被索引。根据存储引擎定义每个表的最大索引数和最大索引长度。 所有存储引擎支持每个表至少16个索引&#xff0c;总索引长度至少为256字节。大多数存储引…

solr和Lucene的配置方式和应用

solr字段类型 类说明BinaryField二进制数据BoolField布尔值&#xff0c;其中’t’/’T’/’1’都是trueCollationFiled支持Unicode排序CurrencyField支持货币和汇率DateRangeFiled支持索引date rangeExternamFiledFiledpull磁盘上的文件EnumField支持定义枚举值ICUCollationFie…

CSS——清除浮动的六种解决方案

内容的高度撑起父元素容器的高度&#xff0c;效果图如下HTML和CSS代码如下给&#xff50;标签加上浮动以后&#xff0c;&#xff50;&#xff5b;float&#xff1a;left&#xff1b;&#xff5d;&#xff0c;此时DIV塌陷&#xff0c;两段内容同行显示&#xff0c;效果如下&…

40个Java Collections面试问答

Java Collections Framework是Java编程语言的基本方面。 这是Java面试问题的重要主题之一。 在这里&#xff0c;我列出了Java集合框架的一些重要问题和解答。 什么是Java Collections Framework&#xff1f; 列出Collections框架的一些好处&#xff1f; 集合框架中泛型的好处…

AtCoder Beginner Contest 084(AB)

A - New Year 题目链接&#xff1a;https://abc084.contest.atcoder.jp/tasks/abc084_a Time limit : 2sec / Memory limit : 256MB Score : 100 points Problem Statement How many hours do we have until New Year at M oclock (24-hour notation) on 30th, December? Cons…

等待正确的时刻–集成测试

当您必须测试多线程程序时&#xff0c;总是需要等到系统达到特定状态后&#xff0c;测试才能验证是否达到了正确的状态。 这样做的通常方法是在系统中插入一个“探针”&#xff0c;该探针将向同步原语发出信号 &#xff08;例如Semaphore &#xff09;&#xff0c;并且测试将一…

网络编程---黏包

基于UDP协议的socket udp的server 不需要进行监听也不需要建立连接&#xff0c;在启动服务之后只能被动的等待客户端发送消息过来。 客户端发送消息的同时还会 自带地址信息&#xff0c;消息回复的时候 不仅需要发送消息 还需把对方的地址填上。 udp的client 不需要connect 因为…

CSS布局(二) 盒子模型属性

盒子模型的属性 宽高width/height 在CSS中&#xff0c;可以对任何块级元素设置显式高度。 如果指定高度大于显示内容所需高度&#xff0c;多余的高度会产生一个视觉效果&#xff0c;就好像有额外的内边距一样&#xff1b; 如果指定高度小于显示内容所需高度&#xff0c;取决于…

Extjs 下拉框

刚刚熟练了easyui控件的使用&#xff0c;又開始了如今的这个项目。这个项目是个半成品。前端使用的是Extjs控件&#xff0c;jsp中没有代码。就引用了非常多的js。。。于是乎有种不知所措了呀。。。 说实话特别的不想去看那些代码&#xff0c;第一是不熟悉&#xff0c;第二是太乱…

python输入代码界面通常_vscode写python时的代码错误提醒和自动格式化的方法

python的代码错误检查通常用pep8、pylint和flake8&#xff0c;自动格式化代码通常用autopep8、yapf、black。这些工具均可以利用pip进行安装&#xff0c;这里介绍传统的利用pip.exe安装和在VScode中安装两种方式。【温馨提醒】要使用flake8或要想flake8等工具起作用&#xff0c…

java编译找不到符号_关于久违的Javac,编译出现“找不到符号”

参考文档&#xff1a;http://blog.csdn.net/qq369201191/article/details/49946609工作以来习惯了maven编译&#xff0c;已经忘记了javac这个东东&#xff0c;以至于遇到javac问题时困惑了&#xff0c;下面总结一下&#xff0c;以便后者参考。一、使用javac进行项目java文件编译…

某些小时后MySql连接自动掉线

MySql配置为删除任何闲置超过8小时的连接。 这意味着什么&#xff1f; 在8个小时的间隔后返回到已部署的应用程序之后&#xff08;如果未更改默认SQL参数&#xff09;&#xff0c;将会遇到异常情况。 如何解决这个问题&#xff1f; 增加wait_time参数-不是一个好主意&#xff…

shopxx 阿里云OSS设置

shopxx 使用文档没有啊&#xff0c;只能自己看了 数据中心 字段其实是 EndPoint字段 URL前缀 是 图片服务器的主机地址。这个在阿里云回传的时候是不带的。 对应 阿里OSS 外网域名 转载于:https://www.cnblogs.com/nanahome/p/7346641.html