Студенты, которые использовали 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"]
}
Чтобы облегчить демонстрацию, мы сначала разветвляем официальную демоверсию апплета, а затем создаем новый файл.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
содержание,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
Суффикс можно не указывать.Кроме того, если путь для запроса является папкой, файлы в папке будут импортированы по умолчанию.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, поэтому файл wxml можно проанализировать с помощью анализатора html. Принципы работы с анализатором html можно найти в статье, которую я написал ранее."Принцип компиляции шаблона 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 здесь повторяться не будет, просто следуйте демо на официальном сайте, нам нужно поставитьtree. json
Просто преобразуйте данные в формат, требуемый ECharts, поместите полный код в коды и bod и перейдите по указанному ниже онлайн-адресу, чтобы увидеть результат.
Интернет-адрес:код sandbox.io/is/cold-dawn…
Суммировать
Эта статья более практична, поэтому выложено много кода.Кроме того, в этой статье представлена идея получения зависимостей каждого файла, хотя здесь только дерево файлов для построения такого графа зависимостей.
В бизнес-разработке мини-программа IDE должна быть полностью скомпилирована при каждом запуске. Предварительный просмотр версии для разработки займет много времени. Теперь, когда у нас есть зависимости файлов, мы можем выбрать для упаковки только те страницы, которые в настоящее время разрабатываются. Это может значительно повысить эффективность нашей разработки. Если вам интересна эта часть, вы можете написать еще одну статью о том, как ее реализовать.