CommanderXL / Biu-blog

个人博客
432 stars 39 forks source link

【Mpx】template/script/style/json 模块单文件的生成 #44

Open CommanderXL opened 4 years ago

CommanderXL commented 4 years ago

不同于 Vue 借助 webpack 是将 Vue 单文件最终打包成单独的 js chunk 文件。而小程序的规范是每个页面/组件需要对应的 wxml/js/wxss/json 4个文件。因为 mpx 使用单文件的方式去组织代码,所以在编译环节所需要做的工作之一就是将 mpx 单文件当中不同 block 的内容拆解到对应文件类型当中。在动态入口编译的小节里面我们了解到 mpx 会分析每个 mpx 文件的引用依赖,从而去给这个文件创建一个 entry 依赖(SingleEntryPlugin)并加入到 webpack 的编译流程当中。我们还是继续看下 mpx loader 对于 mpx 单文件初步编译转化后的内容:

/* script */
export * from "!!babel-loader!../../node_modules/@mpxjs/webpack-plugin/lib/selector?type=script&index=0!./list.mpx"

/* styles */
require("!!../../node_modules/@mpxjs/webpack-plugin/lib/extractor?type=styles&index=0!../../node_modules/@mpxjs/webpack-plugin/lib/wxss/loader?root=&importLoaders=1&extract=true!../../node_modules/@mpxjs/webpack-plugin/lib/style-compiler/index?{\"id\":\"2271575d\",\"scoped\":false,\"sourceMap\":false,\"transRpx\":{\"mode\":\"only\",\"comment\":\"use rpx\",\"include\":\"/Users/XRene/demo/mpx-demo-source/src\"}}!stylus-loader!../../node_modules/@mpxjs/webpack-plugin/lib/selector?type=styles&index=0!./list.mpx")

/* json */
require("!!../../node_modules/@mpxjs/webpack-plugin/lib/extractor?type=json&index=0!../../node_modules/@mpxjs/webpack-plugin/lib/json-compiler/index?root=!../../node_modules/@mpxjs/webpack-plugin/lib/selector?type=json&index=0!./list.mpx")

/* template */
require("!!../../node_modules/@mpxjs/webpack-plugin/lib/extractor?type=template&index=0!../../node_modules/@mpxjs/webpack-plugin/lib/wxml/wxml-loader?root=!../../node_modules/@mpxjs/webpack-plugin/lib/template-compiler/index?{\"usingComponents\":[],\"hasScoped\":false,\"isNative\":false,\"moduleId\":\"2271575d\"}!../../node_modules/@mpxjs/webpack-plugin/lib/selector?type=template&index=0!./list.mpx")

接下来可以看下 styles/json/template 这3个 block 的处理流程是什么样。

首先来看下 json block 的处理流程:list.mpx -> json-compiler -> extractor。第一个阶段 list.mpx 文件经由 json-compiler 的处理流程在前面的章节已经讲过,主要就是分析依赖增加动态入口的编译过程。当所有的依赖分析完后,调用 json-compiler loader 的异步回调函数:

// lib/json-compiler/index.js

module.exports = function (content) {

  ...
  const nativeCallback = this.async()
  ...

  let callbacked = false
  const callback = (err, processOutput) => {
    checkEntryDeps(() => {
      callbacked = true
      if (err) return nativeCallback(err)
      let output = `var json = ${JSON.stringify(json, null, 2)};\n`
      if (processOutput) output = processOutput(output)
      output += `module.exports = JSON.stringify(json, null, 2);\n`
      nativeCallback(null, output)
    })
  }
}

这里我们可以看到经由 json-compiler 处理后,通过nativeCallback方法传入下一个 loader 的文本内容形如:

var json = {
  "usingComponents": {
    "list": "/components/list397512ea/list"
  }
}

module.exports = JSON.stringify(json, null, 2)

即这段文本内容会传递到下一个 loader 内部进行处理,即 extractor。接下来我们来看下 extractor 里面主要是实现了哪些功能:

// lib/extractor.js

module.exports = function (content) {
  ...
  const contentLoader = normalize.lib('content-loader')
  let request = `!!${contentLoader}?${JSON.stringify(options)}!${this.resource}` // 构建一个新的 resource,且这个 resource 只需要经过 content-loader
  let resultSource = defaultResultSource
  const childFilename = 'extractor-filename'
  const outputOptions = {
    filename: childFilename
  }
  // 创建一个 child compiler
  const childCompiler = mainCompilation.createChildCompiler(request, outputOptions, [
    new NodeTemplatePlugin(outputOptions),
    new LibraryTemplatePlugin(null, 'commonjs2'), // 最终输出的 chunk 内容遵循 commonjs 规范的可执行的模块代码 module.exports = (function(modules) {})([modules])
    new NodeTargetPlugin(),
    new SingleEntryPlugin(this.context, request, resourcePath),
    new LimitChunkCountPlugin({ maxChunks: 1 })
  ])

  ...
  childCompiler.hooks.thisCompilation.tap('MpxWebpackPlugin ', (compilation) => {
    // 创建 loaderContext 时触发的 hook,在这个 hook 触发的时候,将原本从 json-compiler 传递过来的 content 内容挂载至 loaderContext.__mpx__ 属性上面以供接下来的 content -loader 来进行使用
    compilation.hooks.normalModuleLoader.tap('MpxWebpackPlugin', (loaderContext, module) => {
      // 传递编译结果,子编译器进入content-loader后直接输出
      loaderContext.__mpx__ = {
        content,
        fileDependencies: this.getDependencies(),
        contextDependencies: this.getContextDependencies()
      }
    })
  })

  let source

  childCompiler.hooks.afterCompile.tapAsync('MpxWebpackPlugin', (compilation, callback) => {
    // 这里 afterCompile 产出的 assets 的代码当中是包含 webpack runtime bootstrap 的代码,不过需要注意的是这个 source 模块的产出形式
    // 因为使用了 new LibraryTemplatePlugin(null, 'commonjs2') 等插件。所以产出的 source 是可以在 node 环境下执行的 module
    // 因为在 loaderContext 上部署了 exec 方法,即可以直接执行 commonjs 规范的 module 代码,这样就最终完成了 mpx 单文件当中不同模块的抽离工作
    source = compilation.assets[childFilename] && compilation.assets[childFilename].source()

    // Remove all chunk assets
    compilation.chunks.forEach((chunk) => {
      chunk.files.forEach((file) => {
        delete compilation.assets[file]
      })
    })

    callback()
  })

  childCompiler.runAsChild((err, entries, compilation) => {
    ...
    try {
      // exec 是 loaderContext 上提供的一个方法,在其内部会构建原生的 node.js module,并执行这个 module 的代码
      // 执行这个 module 代码后获取的内容就是通过 module.exports 导出的内容
      let text = this.exec(source, request)
      if (Array.isArray(text)) {
        text = text.map((item) => {
          return item[1]
        }).join('\n')
      }

      let extracted = extract(text, options.type, resourcePath, +options.index, selfResourcePath)
      if (extracted) {
        resultSource = `module.exports = __webpack_public_path__ + ${JSON.stringify(extracted)};`
      }
    } catch (err) {
      return nativeCallback(err)
    }
    if (resultSource) {
      nativeCallback(null, resultSource)
    } else {
      nativeCallback()
    }
  })
}

稍微总结下上面的处理流程:

  1. 构建一个以当前模块路径及 content-loader 的 resource 路径
  2. 以这个 resource 路径作为入口模块,创建一个 childCompiler
  3. childCompiler 启动后,创建 loaderContext 的过程中,将 content 文本内容挂载至 loaderContext.mpx 上,这样在 content-loader 在处理入口模块的时候仅仅就是取出这个 content 文本内容并返回。实际上这个入口模块经过 loader 的过程不会做任何的处理工作,仅仅是将父 compilation 传入的 content 返回出去。
  4. loader 处理模块的环节结束后,进入到 module.build 阶段,这个阶段对 content 内容没有太多的处理
  5. createAssets 阶段,输出 chunk。
  6. 将输出的 chunk 构建为一个原生的 node.js 模块并执行,获取从这个 chunk 导出的内容。也就是模块通过module.exports导出的内容。

所以上面的示例 demo 最终会输出一个 json 文件,里面包含的内容即为:

{
  "usingComponents": {
    "list": "/components/list397512ea/list"
  }
}