mpvue微信小程序vue.wxml太多导致主包过大的分包解决方案

405 阅读6分钟

优化前

主包将近2M了,我还用了一些插件,上传的时候也会打进主包导致上传超过2M不成功.本次优化主要是优化主包中的wxml页面,mpvue打包时会把所有vue单文件模式下的vue模板打包到主包中,如果你的分包页面或者vue组件有很多主包就会变得很大. image.png

下图可以看到pageA分包中的页面有313Kb,这些都应该打包到分包中.不能占用主包的空间.commponents下是全局组件打包到主包中也没什么问题 image.png

开始优化

先来看看打包出来的目录结构,我的项目有1个主包pages和2个分包pagesA,pagesCharts. 优化后应该像下图这样vue.wxml打包到自己的包里去.

image.png

1.修改页面引入

需要在打包过程中修改这里的引入路径,修改成/pagesA/valueAddService/bill-list.vue.wxml,引入路径到各个自己的包里.

image.png

经过一番查找最后在mpvue-loader中找到了编译代码.做如下修改

image.png之后的修改都是在这一个文件中完成.

function createPageMPML (emitFile, resourcePath, rootComponent, context, fileExt) {
  const { src } = getFileInfo(resourcePath) || {}
  const { name, filePath } = getCompInfo(context, rootComponent, fileExt)
  // 分包优化1 修改page.wxml中引入的路径 修改为从各个主包或分包中引入
  let newPath = filePath.replace('/xcx-xhr-co.ibanbu.com/src', '') // 这里根据你的项目打包出来的结果自行修改
  const MPMLContent = genPageML(name, newPath, fileExt)
  emitFile(`${src}.${fileExt.template}`, MPMLContent)
}

2.修改为优化1中的文件位置 编译到分包中减少主包的大小

function createComponentMPML ({ emitWarning, emitError, emitFile, resourcePath, context, compiled, fileExt }) {
  cacheCreateMPMLFns[resourcePath] = arguments
  const { pageType, moduleId, components } = getFileInfo(resourcePath) || {}
  const { name, filePath } = getCompInfo(context, resourcePath, fileExt)
  const options = { components, pageType, name, moduleId }
  const MPMLContent = genComponentMPML(compiled, options, emitFile, emitError, emitWarning, fileExt)

  // 分包优化2 修改为优化1中的文件位置 编译到分包中减少主包的大小
  let newPath = filePath
  if (filePath.startsWith('/xcx-xhr-co.ibanbu.com/src/pages')) { // 因为的我分包都是这个前缀,所以这么写,你们的情况可能不同就要多加判断了
    newPath = filePath.replace('/xcx-xhr-co.ibanbu.com/src', '')
  }

  emitFile(newPath, MPMLContent)
}

这样修改后基本完成差不多了.但如果你的vue页面里引入其他vue组件的话还需要下一步修改

3.修改单vue组件中引入分包中的组件路径

还是以我的这个页面为例,页面中引入了1个components下的全局vue组件和3个pagesA下的分包组件,全局组件我们不动,只需要修改分包组件的引入路径就可以了

image.png

对优化2的createComponentMPML函数做如下添加

function createComponentMPML ({ emitWarning, emitError, emitFile, resourcePath, context, compiled, fileExt }) {
  cacheCreateMPMLFns[resourcePath] = arguments
  const { pageType, moduleId, components } = getFileInfo(resourcePath) || {}

  // 分包优化3 修改单vue组建中引入分包中的组件路径
  for (let key in components) {
    if (typeof components[key] === 'object' && components[key].src) {
      if (components[key].src.startsWith('/xcx-xhr-co.ibanbu.com/src/pages')) {
        components[key].src = components[key].src.replace('/xcx-xhr-co.ibanbu.com/src', '')
      }
    }
  }

  const { name, filePath } = getCompInfo(context, resourcePath, fileExt)
  const options = { components, pageType, name, moduleId }
  const MPMLContent = genComponentMPML(compiled, options, emitFile, emitError, emitWarning, fileExt)

  // 分包优化2 修改为优化1中的文件位置 编译到分包中减少主包的大小
  let newPath = filePath
  if (filePath.startsWith('/xcx-xhr-co.ibanbu.com/src/pages')) {
    newPath = filePath.replace('/xcx-xhr-co.ibanbu.com/src', '')
  }

  emitFile(newPath, MPMLContent)
}

4.修改完成, 重新打包!

打包后目录结构,看到只有全局组件打包到这个文件夹下

image.png

其他的vue组件都到了自己的相应包中了

image.png

打包后代码包结构,从1.8M下降到了1.4M,之后再怎么增加页面都不会影响主包大小了.我的项目也能完美运行了~

image.png

完整修改文件

mpvue-loader@2.0.0 /node_modules/mpvue-loader/lib/mp-compiler/index.js

const babel = require('babel-core')
const path = require('path')
const fs = require('fs')
const deepEqual = require('deep-equal')
const compiler = require('mpvue-template-compiler')

const { parseConfig, parseComponentsDeps, parseGlobalComponents, clearGlobalComponents } = require('./parse')
const { parseComponentsDeps: parseComponentsDepsTs } = require('./parse-ts')
const { genPageML } = require('./templates')

const {
  cacheFileInfo,
  getFileInfo,
  getCompInfo,
  resolveTarget,
  covertCCVar,
  cacheSlots,
  getSlots,
  htmlBeautify,
  getBabelrc
} = require('./util')

function genComponentMPML (compiled, options, emitFile, emitError, emitWarning, fileExt) {
  options.components['slots'] = { src: '/components/slots', name: 'slots' }
  const { code: mpmlContent, compiled: compiledResult, slots, importCode } = compiler.compileToMPML(compiled, options, fileExt)
  const { mpErrors, mpTips } = compiledResult
  // 缓存 slots,延迟编译
  cacheSlots(slots, importCode)

  if (mpErrors && mpErrors.length) {
    emitError('\n  Error compiling template:\n' + mpErrors.map(e => ` - ${e}`).join('\n') + '\n')
  }
  if (mpTips && mpTips.length) {
    emitWarning(mpTips.map(e => ` - ${e}`).join('\n') + '\n')
  }
  return htmlBeautify(mpmlContent)
}

function createPageMPML (emitFile, resourcePath, rootComponent, context, fileExt) {
  const { src } = getFileInfo(resourcePath) || {}
  const { name, filePath } = getCompInfo(context, rootComponent, fileExt)
  // 分包优化1 修改page.wxml中引入的路径 修改为从各个主包或分包中引入
  let newPath = filePath.replace('/xcx-xhr-co.ibanbu.com/src', '')
  const MPMLContent = genPageML(name, newPath, fileExt)
  emitFile(`${src}.${fileExt.template}`, MPMLContent)
}

// 更新全局组件时,需要重新生成 mpml,用这个字段保存所有需要更新的页面及其参数
const cacheCreateMPMLFns = {}

function createComponentMPML ({ emitWarning, emitError, emitFile, resourcePath, context, compiled, fileExt }) {
  cacheCreateMPMLFns[resourcePath] = arguments
  const { pageType, moduleId, components } = getFileInfo(resourcePath) || {}

  // 分包优化3 修改单vue组建中引入分包中的组件路径
  for (let key in components) {
    if (typeof components[key] === 'object' && components[key].src) {
      if (components[key].src.startsWith('/xcx-xhr-co.ibanbu.com/src/pages')) {
        components[key].src = components[key].src.replace('/xcx-xhr-co.ibanbu.com/src', '')
      }
    }
  }

  const { name, filePath } = getCompInfo(context, resourcePath, fileExt)
  const options = { components, pageType, name, moduleId }
  const MPMLContent = genComponentMPML(compiled, options, emitFile, emitError, emitWarning, fileExt)

  // 分包优化2 修改为优化1中的文件位置 编译到分包中减少主包的大小
  let newPath = filePath
  if (filePath.startsWith('/xcx-xhr-co.ibanbu.com/src/pages')) {
    newPath = filePath.replace('/xcx-xhr-co.ibanbu.com/src', '')
  }

  emitFile(newPath, MPMLContent)
}

let slotsHookAdded = false
function compileMPML (compiled, html, options) {
  const fileExt = options.fileExt
  if (!slotsHookAdded) {
    // avoid add hook several times during compilation
    slotsHookAdded = true
    // TODO: support webpack4
    this._compilation.plugin('seal', () => {
      const content = getSlots()
      if (content.trim()) {
        this.emitFile(`components/slots.${fileExt.template}`, htmlBeautify(content))
      }
      slotsHookAdded = false
    })
  }

  return new Promise(resolve => {
    const pollComponentsStatus = () => {
      const { pageType, components } = getFileInfo(this.resourcePath) || {}
      if (!pageType || (components && !components.isCompleted)) {
        setTimeout(pollComponentsStatus, 20)
      } else {
        resolve()
      }
    }
    pollComponentsStatus()
  }).then(() => {
    createComponentMPML({
      emitWarning: this.emitWarning,
      emitError: this.emitError,
      emitFile: this.emitFile,
      resourcePath: this.resourcePath,
      context: this.options.context,
      rootComponent: null,
      compiled, html,
      fileExt
    })
  })
}

// 针对 .vue 单文件的脚本逻辑的处理
// 处理出当前单文件组件的子组件依赖
function compileMPScript (script, mpOptioins, moduleId) {
  const { resourcePath, options, resolve, context } = this
  const babelrc = getBabelrc(mpOptioins.globalBabelrc)

  let scriptContent = script.content
  const babelOptions = { extends: babelrc, plugins: [parseComponentsDeps] }
  if (script.src) {
    const scriptpath = path.join(path.dirname(resourcePath), script.src)
    scriptContent = fs.readFileSync(scriptpath).toString()
  }

  let metadata
  if (script.lang === 'ts') {
    metadata = parseComponentsDepsTs(scriptContent)
  } else {
    const result = babel.transform(scriptContent, babelOptions)
    metadata = result.metadata
  }
  // metadata: importsMap, components
  const { importsMap, components: originComponents } = metadata

  // 处理子组件的信息
  const components = {}
  const fileInfo = resolveTarget(resourcePath, options.entry)

  const callback = () => resolveComponent(resourcePath, fileInfo, importsMap, components, moduleId)
  if (originComponents) {
    resolveSrc(originComponents, components, resolve, context, options.context, mpOptioins.fileExt)
      .then(() => callback())
      .catch(err => {
        console.error(err)
        callback()
      })
  } else {
    callback()
  }

  return script
}

// checkMPEntry 针对 entry main.js 的入口处理: 编译出 app, page 的入口js、mpml、json
let globalComponents
function compileMP (content, mpOptioins) {
  const { resourcePath, emitFile, resolve, context, options } = this
  const fileInfo = resolveTarget(resourcePath, options.entry)
  cacheFileInfo(resourcePath, fileInfo)
  const { isApp, isPage } = fileInfo
  if (isApp) {
    // 解析前将可能存在的全局组件清空
    clearGlobalComponents()
  }

  const babelrc = getBabelrc(mpOptioins.globalBabelrc)
  // app入口进行全局component解析
  const { metadata } = babel.transform(content, { extends: babelrc, plugins: isApp ? [parseConfig, parseGlobalComponents] : [parseConfig] })

  // metadata: config
  const { rootComponent, globalComponents: globalComps } = metadata
  if (isApp) {
    // 保存旧数据,用于对比
    const oldGlobalComponents = globalComponents
    // 开始解析组件路径时把全局组件清空,解析完成后再进行赋值,标志全局组件解析完成
    globalComponents = null

    // 解析全局组件的路径
    const components = {}
    resolveSrc(globalComps, components, resolve, context, options.context, mpOptioins.fileExt).then(() => {
      handleResult(components)
    }).catch(err => {
      console.error(err)
      handleResult(components)
    })
    const handleResult = components => {
      globalComponents = components
      // 热更时,如果全局组件更新,需要重新生成所有的 mpml
      if (oldGlobalComponents && !deepEqual(oldGlobalComponents, globalComponents)) {
        // 更新所有页面的组件
        Object.keys(cacheResolveComponents).forEach(k => {
          resolveComponent(...cacheResolveComponents[k])
        })
        // 重新生成所有 mpml
        Object.keys(cacheCreateMPMLFns).forEach(k => {
          createComponentMPML(...cacheCreateMPMLFns[k])
        })
      }
    }
  }

  if (isApp || isPage) {
    // 这儿应该异步在所有的模块都清晰后再生成
    // 生成入口 mpml
    if (isPage && rootComponent) {
      resolve(context, rootComponent, (err, rootComponentSrc) => {
        if (err) return
        // 这儿需要搞定 根组件的 路径
        // resourcePath =     C:\ibanbu.com\xcx-xhr-co.ibanbu.com\node_modules\mpvue-entry\dist\pagesIndex.js
        // rootComponentSrc = C:\ibanbu.com\xcx-xhr-co.ibanbu.com\src\pages\index.vue
        createPageMPML(emitFile, resourcePath, rootComponentSrc, this.options.context, mpOptioins.fileExt)
      })
    }
  }

  return content
}

function resolveSrc (originComponents, components, resolveFn, context, projectRoot, fileExt) {
  return Promise.all(Object.keys(originComponents).map(k => {
    return new Promise((resolve, reject) => {
      resolveFn(context, originComponents[k], (err, realSrc) => {
        if (err) return reject(err)
        const com = covertCCVar(k)
        const { filePath, name } = getCompInfo(projectRoot, realSrc, fileExt)
        components[com] = { src: filePath, name }
        resolve()
      })
    })
  }))
}

const cacheResolveComponents = {}
function resolveComponent (resourcePath, fileInfo, importsMap, localComponents, moduleId) {
  // 需要等待全局组件解析完成
  if (!globalComponents) {
    setTimeout(resolveComponent, 20, ...arguments)
  } else {
    // 保存当前所有参数,在热更时如果全局组件发生变化,需要进行组件更新
    cacheResolveComponents[resourcePath] = arguments
    const components = Object.assign({}, globalComponents, localComponents)
    components.isCompleted = true
    cacheFileInfo(resourcePath, fileInfo, { importsMap, components, moduleId })
  }
}

module.exports = {
  compileMP,
  compileMPML,
  compileMPScript
}