您好,欢迎来到三六零分类信息网!老站,搜索引擎当天收录,欢迎发信息

介绍小程序依赖分析实践

2026/2/25 7:56:00发布14次查看
微信小程序开发教程介绍小程序依赖分析实践。
用过 webpack 的同学肯定知道 webpack-bundle-analyzer ,可以用来分析当前项目 js 文件的依赖关系。
因为最近一直在做小程序业务,而且小程序对包体大小特别敏感,所以就想着能不能做一个类似的工具,用来查看当前小程序各个主包与分包之间的依赖关系。经过几天的折腾终于做出来了,效果如下:
今天的文章就带大家来实现这个工具。
小程序入口小程序的页面通过 app.json 的 pages 参数定义,用于指定小程序由哪些页面组成,每一项都对应一个页面的路径(含文件名) 信息。 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复制代码
其大致的目录结构如下:
以 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 目录下有两个页面:detail、index ,这两个 页面文件夹下有四个对应的文件。
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 }) }复制代码
获取依赖关系上面的步骤看起来没什么问题,但是我们缺少了重要的一环,那就是我们在构造文件树之前,还需要得到每个文件的依赖项,这样输出的才是小程序完整的文件树。文件的依赖关系需要分成四部分来讲,分别是  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')复制代码
获取 .json 文件依赖json 文件本身是不支持模块化的,但是小程序可以通过 json 文件导入自定义组件,只需要在页面的 json 文件通过 usingcomponents 进行引用声明。usingcomponents 为一个对象,键为自定义组件的标签名,值为自定义组件文件路径:
{  usingcomponents: {    component-tag-name: path/to/the/custom/component  }}复制代码
自定义组件与小程序页面一样,也会对应四个文件,所以我们需要获取 json 中 usingcomponents 内的所有依赖项,并判断每个组件对应的那四个文件是否存在,然后添加到依赖项内。
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 提供两种文件引用方式 import 和 include。
<import src="a.wxml"/><include 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)          })  }}复制代码
获取分包依赖熟悉小程序的同学肯定知道,小程序提供了分包机制。使用分包后,分包内的文件会被打包成一个单独的包,在用到的时候才会加载,而其他的文件则会放在主包,小程序打开的时候就会加载。subpackages 中,每个分包的配置有以下几项:
字段类型说明
root string 分包根目录
name string 分包别名,分包预下载时可以使用
pages stringarray 分包页面路径,相对与分包根目录
independent boolean 分包是否是独立分包
所以我们在运行的时候,除了要拿到 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 文件:
接下来,我们利用 echarts 的画图能力,将这个 json 数据以图表的形式展现出来。我们可以在 echarts 提供的实例中看到一个 disk usage 的案例,很符合我们的预期。
echarts 的配置这里就不再赘述,按照官网的 demo 即可,我们需要把 tree. json 的数据转化为 echarts 需要的格式就行了,完整的代码放到 codesandbod 了,去下面的线上地址就能看到效果了。
线上地址:https://codesandbox.io/s/cold-dawn-kufc9
总结这篇文章比较偏实践,所以贴了很多的代码,另外本文对各个文件的依赖获取提供了一个思路,虽然这里只是用文件树构造了一个这样的依赖图。
在业务开发中,小程序 ide 每次启动都需要进行全量的编译,开发版预览的时候会等待较长的时间,我们现在有文件依赖关系后,就可以只选取目前正在开发的页面进行打包,这样就能大大提高我们的开发效率。如果有对这部分内容感兴趣的,可以另外写一篇文章介绍下如何实现。
相关免费学习推荐:微信小程序开发教程
以上就是介绍小程序依赖分析实践的详细内容。
该用户其它信息

VIP推荐

免费发布信息,免费发布B2B信息网站平台 - 三六零分类信息网 沪ICP备09012988号-2
企业名录 Product