日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

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

發(fā)布時間:2023/12/1 编程问答 38 豆豆
生活随笔 收集整理的這篇文章主要介紹了 小程序 foreach_【第2106期】小程序依赖分析实践 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

前言

這種可視化分析圖還是很直觀的,很有趣。今日早讀文章由自然醒授權(quán)分享。

正文從這開始~~

用過 webpack 的同學(xué)肯定知道 webpack-bundle-analyzer ,可以用來分析當(dāng)前項目 js 文件的依賴關(guān)系。

webpack-bundle-analyzer

因為最近一直在做小程序業(yè)務(wù),而且小程序?qū)Πw大小特別敏感,所以就想著能不能做一個類似的工具,用來查看當(dāng)前小程序各個主包與分包之間的依賴關(guān)系。經(jīng)過幾天的折騰終于做出來了,效果如下:

小程序依賴關(guān)系

今天的文章就帶大家來實現(xiàn)這個工具。

小程序入口

小程序的頁面通過 app.json 的 pages 參數(shù)定義,用于指定小程序由哪些頁面組成,每一項都對應(yīng)一個頁面的路徑(含文件名) 信息。 pages 內(nèi)的每個頁面,小程序都會去尋找對應(yīng)的 json, js, wxml, wxss 四個文件進(jìn)行處理。

如開發(fā)目錄為:

├── 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,依賴分析相關(guān)的工作就在這個文件里面實現(xiàn)。

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

$ cd miniprogram-demo

$ touch depend.js

其大致的目錄結(jié)構(gòu)如下:

目錄結(jié)構(gòu)

以 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 // 主包的所有頁面

}

}

每個頁面會對應(yīng) 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)

}

})

})

}

}

現(xiàn)在 pages 內(nèi)頁面相關(guān)的文件都放到 files 字段存起來了。

構(gòu)造樹形結(jié)構(gòu)

拿到文件后,我們需要依據(jù)各個文件構(gòu)造一個樹形結(jié)構(gòu)的文件樹,用于后續(xù)展示依賴關(guān)系。

假設(shè)我們有一個 pages 目錄, pages 目錄下有兩個頁面: detail、 index ,這兩個 頁面文件夾下有四個對應(yīng)的文件。

pages

├── detail

│ ├── detail.js

│ ├── detail.json

│ ├── detail.wxml

│ └── detail.wxss

└── index

├── index.js

├── index.json

├── index.wxml

└── index.wxss

依據(jù)上面的目錄結(jié)構(gòu),我們構(gòu)造一個如下的文件樹結(jié)構(gòu), size 用于表示當(dāng)前文件或文件夾的大小, 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 }

}

}

}

}

我們先在構(gòu)造函數(shù)構(gòu)造一個 tree 字段用來存儲文件樹的數(shù)據(jù),然后我們將每個文件都傳入 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)) {

// 調(diào)用 addToTree

this.addToTree(filePath)

}

})

})

}

}

接下來實現(xiàn) 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)) {

// 如果該文件已經(jīng)添加過,則不再添加到文件樹中

return

}

const size = this.getSize(filePath)

const relPath = this.getRelative(filePath)

// 將文件路徑轉(zhuǎn)化成數(shù)組

// '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 })

}

tree.json

獲取依賴關(guān)系

上面的步驟看起來沒什么問題,但是我們?nèi)鄙倭酥匾囊画h(huán),那就是我們在構(gòu)造文件樹之前,還需要得到每個文件的依賴項,這樣輸出的才是小程序完整的文件樹。文件的依賴關(guān)系需要分成四部分來講,分別是 js, json, wxml, wxss 這四種類型文件獲取依賴的方式。

獲取 .js 文件依賴

小程序支持 CommonJS 的方式進(jìn)行模塊化,如果開啟了 es6,也能支持 ESM 進(jìn)行模塊化。我們?nèi)绻@得一個 js 文件的依賴,首先要明確,js 文件導(dǎo)入模塊的三種寫法,針對下面三種語法,我們可以引入 Babel 來獲取依賴。

import a from './a.js'

export b from './b.js'

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

通過 @babel/parser 將代碼轉(zhuǎn)化為 AST,然后通過 @babel/traverse 遍歷 AST 節(jié)點,獲取上面三種導(dǎo)入方式的值,放到數(shù)組。

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

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

class Depend {

// ...

jsDeps(file) {

const deps = []

const dirName = path.dirname(file)

// 讀取 js 文件內(nèi)容

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

// 將代碼轉(zhuǎn)化為 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

}

}

在獲取依賴模塊的路徑后,還不能立即將路徑添加到依賴數(shù)組內(nèi),因為根據(jù)模塊語法 js 后綴是可以省略的,另外 require 的路徑是一個文件夾的時候,默認(rèn)會導(dǎo)入該文件夾下的 index.js 。

class Depend {

// 獲取某個路徑的腳本文件

transformScript(url) {

const ext = path.extname(url)

// 如果存在后綴,表示當(dāng)前已經(jīng)是一個文件

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) {...}

}

我們可以創(chuàng)建一個 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 文件導(dǎo)入自定義組件,只需要在頁面的 json 文件通過 usingComponents 進(jìn)行引用聲明。 usingComponents 為一個對象,鍵為自定義組件的標(biāo)簽名,值為自定義組件文件路徑:

{

"usingComponents": {

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

}

}

自定義組件與小程序頁面一樣,也會對應(yīng)四個文件,所以我們需要獲取 json 中 usingComponents 內(nèi)的所有依賴項,并判斷每個組件對應(yīng)的那四個文件是否存在,然后添加到依賴項內(nèi)。

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。

src="a.wxml"/>

src="b.wxml"/>

wxml 文件本質(zhì)上還是一個 html 文件,所以可以通過 html parser 對 wxml 文件進(jìn)行解析,關(guān)于 html parser 相關(guān)的原理可以看我之前寫過的文章 《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 文件導(dǎo)入樣式和 css 語法一致,使用 @import 語句可以導(dǎo)入外聯(lián)樣式表。

@import "common.wxss";

可以通過 postcss 解析 wxss 文件,然后獲取導(dǎo)入文件的地址,但是這里我們偷個懶,直接通過簡單的正則匹配來做。

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

}

}

將依賴添加到樹結(jié)構(gòu)中

現(xiàn)在我們需要修改 addToTree 方法。

class Depend {

addToTree(filePath) {

// 如果該文件已經(jīng)添加過,則不再添加到文件樹中

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)

})

}

}

獲取分包依賴

熟悉小程序的同學(xué)肯定知道,小程序提供了分包機制。使用分包后,分包內(nèi)的文件會被打包成一個單獨的包,在用到的時候才會加載,而其他的文件則會放在主包,小程序打開的時候就會加載。 subpackages 中,每個分包的配置有以下幾項:

字段類型說明
rootString分包根目錄
nameString分包別名,分包預(yù)下載時可以使用
pagesStringArray分包頁面路徑,相對與分包根目錄
independentBoolean分包是否是獨立分包

所以我們在運行的時候,除了要拿到 pages 下的所有頁面,還需拿到 subpackages 中所有的頁面。由于之前只關(guān)心主包的內(nèi)容, this.tree 下面只有一顆文件樹,現(xiàn)在我們需要在 this.tree 下掛載多顆文件樹,我們需要先為主包創(chuàng)建一個單獨的文件樹,然后為每個分包創(chuàng)建一個文件樹。

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') // 為主包創(chuàng)建文件樹

pages.forEach(page => {

this.addPage(page, 'main')

})

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

// 所以我們兩個屬性都獲取,哪個存在就用哪個

const subPkgs = subPackages || subpackages

// 分包存在的時候才進(jìn)行遍歷

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

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

this.createTree(root) // 為分包創(chuàng)建文件樹

pages.forEach(page => {

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

})

})

// 輸出文件樹

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

}

}

addToTree 方法也需要進(jìn)行修改,根據(jù)傳入的 pkg 來判斷將當(dāng)前文件添加到哪個樹。

class Depend {

addToTree(filePath, pkg = 'main') {

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

// 如果該文件已經(jīng)添加過,則不再添加到文件樹中

return

}

let relPath = this.getRelative(filePath)

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

// 如果該文件不是以分包名開頭,證明該文件不在分包內(nèi),

// 需要將文件添加到主包的文件樹內(nèi)

pkg = 'main'

}

const tree = this.tree[pkg] // 依據(jù) pkg 取到對應(yīng)的樹

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 文件夾下,則該文件需要放入主包的文件樹內(nèi)。

通過 EChart 畫圖

經(jīng)過上面的流程后,最終我們可以得到如下的一個 json 文件:

tree.json

接下來,我們利用 ECharts 的畫圖能力,將這個 json 數(shù)據(jù)以圖表的形式展現(xiàn)出來。我們可以在 ECharts 提供的實例中看到一個 Disk Usage 的案例,很符合我們的預(yù)期。

ECharts

ECharts 的配置這里就不再贅述,按照官網(wǎng)的 demo 即可,我們需要把 tree.json 的數(shù)據(jù)轉(zhuǎn)化為 ECharts 需要的格式就行了,完整的代碼放到 codesandbod 了,去下面的線上地址就能看到效果了。

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

最后效果

總結(jié)

這篇文章比較偏實踐,所以貼了很多的代碼,另外本文對各個文件的依賴獲取提供了一個思路,雖然這里只是用文件樹構(gòu)造了一個這樣的依賴圖。

在業(yè)務(wù)開發(fā)中,小程序 IDE 每次啟動都需要進(jìn)行全量的編譯,開發(fā)版預(yù)覽的時候會等待較長的時間,我們現(xiàn)在有文件依賴關(guān)系后,就可以只選取目前正在開發(fā)的頁面進(jìn)行打包,這樣就能大大提高我們的開發(fā)效率。如果有對這部分內(nèi)容感興趣的,可以另外寫一篇文章介紹下如何實現(xiàn)。

關(guān)于本文 作者:@自然醒 原文:https://blog.shenfq.com/2020/小程序依賴分析/

為你推薦

【第1806期】高德JS依賴分析工程及關(guān)鍵原理

【第2030期】JavaScript 啟動性能瓶頸分析與解決方案

歡迎自薦投稿,前端早讀課等你來

總結(jié)

以上是生活随笔為你收集整理的小程序 foreach_【第2106期】小程序依赖分析实践的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。