Skip to content

Latest commit

 

History

History
326 lines (267 loc) · 12.1 KB

2019-05-10-bundler.md

File metadata and controls

326 lines (267 loc) · 12.1 KB

利用 babel 实现简易版打包工具函数

简要唠几句

webpack4.0+相比webpack2.0+不但是在打包性能上提高了不少,而且在配置打包参数上也简化了很多,例如webpack4+就废弃了CommonsChunkPluginapi,转而新增加了optimization配置项,专门解决 chunk 相关打包配置的问题;NoEmitOnErrorsPluginModuleConcatenationPlugin早期都是通过插件的形式进行使用,在webpack4+都改成optimization对象下的一个子属性配置项来解决,并且optimization有默认配置项,就是如果在使用中不配置这个,默认也能进行压缩打包,极大的方便了开发者的使用。

那么使用简单了,它具体是如何实现的呢?比如说,loaderplugins是如何实现的呢?,该如何写一个自己的loaderplugins呢?webpack具体是怎么讲相互依赖的代码打包成浏览器能够识别的 es5 代码呢?下面就来简要的介绍下如果完成以上几个问题。

以下示例代码可以访问源码查看

自定义 loader

loader 可以理解就是对 javascript 做定制化打包需要做的事情,比如常见的将 es6 转化成 es5、批量删除代码中的注释代码块等等操作,通过在 webpack.config 文件中定义相应 loader 的 options 可以作为参数传入自定义的 loader 函数中进行处理,具体使用也可以参考官方文档

同步 loader

实现一个基本的 try,catch 代码块,通过 laoder 统一加上,基本代码形式如下:

注意:这里在编写 loader 不能使用箭头函数

// loader代码
const loaderUtils = require('loader-utils')
module.exports = function(source) {
  const options = loaderUtils.getOptions(this)
  let result = ''
  if (options.tryCatch) {
    result = `try { ${source} } catch(err) {
      console.log(err.name)
      console.log(err.message)
      console.log(err.stack)
    }`
  }
  return result || source
}

// config 代码
resolveLoader: {
    modules: ['node_modules', './loaders/']
  },
module: {
  rules: [
    {
      test: /\.js$/,
      use: [
        {
          loader: 'loaderDemo',
          options: {
            tryCatch: true
          }
        }
      ]
    }
  ]
}
  • source 就是需要打包的代码块
  • 可以通过一个 loader-utils 工具来快速获取外层传进来的 options 上的参数,例如上面获取 tryCatch 配置
  • resolveLoader 主要是为了方便在 module 中使用自定义的 loader 写法,具体含义是会在'node_modules'和'./loaders/'文件夹下去寻找定义的 loader

异步 loader

异步 loader 其实就是使用了官方提供的一个 this 上的 async 方法,含义就是等待执行结果,然后在会返回以 callback,其实这个 callback 也就是调用了 this.callback,所以得保证参数传入一致,基本代码如下:

// this.async返回的callback其实就是this.callback
// this.callback(
//   err: Error | null,
//   content: string | Buffer,
//   sourceMap?: SourceMap,
//   meta?: any
// )

module.exports = function(source) {
  const options = loaderUtils.getOptions(this)
  const callback = this.async()
  setTimeout(() => {
    const result = source.replace('wq', options.flag)
    callback(null, result)
  }, 1000)
}

自定义 plugins

插件其实就是可以理解在 js 打包过程中需要执行的任务,例如将某些打包好的文件插入的页面模版中,在最终打包的项目中增加一个额外的静态文件,在打包项目之前先删除上一次打包的文件等等这些操作。也会有同步执行和异步执行的,具体可以参考插件编写规范例如下面的代码就是实现在最终打包文件夹中生成一个 md 文件,代码如下:

插件就是一个类,在使用的时候需要通过 new 关键字来实例化 apply 方法来触发 同步方法使用 compiler.hooks.compile.tap 写法 异步方法使用 compiler.hooks.emit.tapAsync 写法

class ExampleWebpackPlugin {
  constructor(opts) {}

  apply(compiler) {
    compiler.hooks.compile.tap('ExampleWebpackPlugin', compilation => {
      console.log('同步complie时刻')
    })

    compiler.hooks.emit.tapAsync('ExampleWebpackPlugin', (compilation, cb) => {
      let filelist = 'In this build:\n\n'

      for (const filename in compilation.assets) {
        filelist += '- ' + filename + '\n'
      }

      // 新定义一个filelist.md文件,并且插入到最终打包的目录中
      compilation.assets['filelist.md'] = {
        source() {
          // 返回文件的内容
          return filelist
        },
        size() {
          // 返回文件的大小
          return filelist.length
        }
      }
      cb()
    })
  }
}

module.exports = ExampleWebpackPlugin

自定义打包函数

以上只是简单的介绍下 loader 和 plugin 基本写法的规范,具体其实还有很多官方提供的 api 可以尝试去使用,写出更加强大的 loader 及 plugin,下面主要介绍下如果利用 babel 编写一个打包 js 的函数,需要打包的 js 源码如下:

// index.js
import msg from './msg.js'
console.log(msg)

// msg.js
import word from './word.js'
const msg = `say ${word}`
export default msg

// word.js
const word = 'wangqi'
export default word

以上代码中,假设 index.js 是入口文件,msg.js 和 word.js 是其他模块代码,如果打包以上这些代码,必须得解决以下几个问题:

  • imort 引入浏览器不识别,export default 浏览器不识别,es6 语法糖解析
  • import 嵌套层数过深怎么解决依赖问题

1、分析单文件

需要用到以下几个库来配合处理:

  • @babel/parser,将 javascript 生成 AST 树结构
  • @babel/traverse ,遍历生成好的 AST 数结构
  • @babel/core,其中的transformFromAstSync方法就是将 AST 数按照@babel/preset-env这个最新的 javascript 准则去解析

通过 node 原生fsAPI 来读取入口文件,并且配合以上说的几个 babel 的插件,最终输出单个文件解析的对象。片段代码如下:

  const content = fs.readFileSync(filename, 'utf-8')
  const ast = parser.parse(content, {
    sourceType: 'module'
  })
  const dependencies = {}
  traverse(ast, {
    ImportDeclaration({ node }) {
      const dirname = path.dirname(filename)
      const newFile = './' + path.join(dirname, node.source.value)
      dependencies[node.source.value] = newFile
    }
  })
  const { code } = babel.transformFromAstSync(ast, null, {
    presets: ['@babel/preset-env']
  })

  return {
    filename,
    dependencies,
    code
  }
}

2、生成所有依赖的对象树

定义一个 graphArray 收集 moduleAnalyser 函数分析文件输出的对象树结构,这里在遍历 graphArray 数组的时候,会通过判断是否存在 dependencies 则继续遍历对象树的子依赖,并且将子依赖对应的 js 文件再次交给 moduleAnalyser 函数处理,最后又被 push 到 graphArray 数组中,然后跳出内层循环,继续外层循环,直到 dependencies 不存在了,就把所有的依赖都遍历完成了。

将第一次遍历得到的 graphArray 结果,转变成对象的形式输出,方便最终将代码转成浏览器识别做准备,基本代码如下:

const makeDependenciesGraph = entry => {
  const entryModule = moduleAnalyser(entry)
  const graphArray = [entryModule]
  for (let i = 0; i < graphArray.length; i++) {
    const item = graphArray[i]
    const { dependencies } = item
    if (dependencies) {
      for (let j in dependencies) {
        graphArray.push(moduleAnalyser(dependencies[j]))
      }
    }
  }
  const graph = {}
  graphArray.forEach(item => {
    graph[item.filename] = {
      dependencies: item.dependencies,
      code: item.code
    }
  })
  return graph
}

3、所有依赖对象树生成浏览器执行代码

在完成以上两步,可以尝试打包,看看运行结果,片段代码如下:

var _msg = _interopRequireDefault(require(\"./msg.js\"));
exports[\"default\"] = _default;

你会发现会出现requireexports,这两个都不是浏览器全局提供的 api,因此在解析这段代码需要自行实现这两个 api,否则浏览器无法直接运行,并且为了打包后的函数块不会影响到全局的环境,因此可以使用闭包将需要打包的代码块包裹住,最终使用 eval 来执行字符串代码块,代码如下:

const generateCode = entry => {
  const graph = JSON.stringify(makeDependenciesGraph(entry))

  return `
    (function(graph){
      function require(module) {
        function localRequire(path) {
          return require(graph[module].dependencies[path])
        };
        var exports = {};
        (function(require, exports, code){
          eval(code)
        })(localRequire, exports, graph[module].code);
        return exports;
      }
      require('${entry}')
    })(${graph})
  `
}

4、生成文件夹及 dist 文件

其实完成以上三步就能将最开始定义的三个文件打包成浏览器识别的代码,这一步主要是能够自动的生成文件夹并且生成最终打包的文件,主要是使用了 node 原生的操作文件的 api,代码如下:

const mkDist = (path, name, codeInfo) => {
  fs.readdir(path, (error, data) => {
    if (error) {
      fs.mkdirSync(path)
    }
    if (data && data.length) {
      fs.unlink(`${path}/${name}`, error => {
        if (error) {
          console.log(error)
          return false
        }
      })
    }
    fs.writeFile(`${path}/${name}`, codeInfo, 'utf8', error => {
      if (error) {
        console.log(error)
        return false
      }
    })
  })
}

5、具体调用

const codeInfo = generateCode('./src/index.js')
mkDist('./dist', 'dist.js', codeInfo)

最终打包的完整结果如下:

;(function(graph) {
  function require(module) {
    function localRequire(path) {
      return require(graph[module].dependencies[path])
    }
    var exports = {}
    ;(function(require, exports, code) {
      eval(code)
    })(localRequire, exports, graph[module].code)
    return exports
  }
  require('./src/index.js')
})({
  './src/index.js': {
    dependencies: { './msg.js': './src/msg.js' },
    code:
      '"use strict";\n\nvar _msg = _interopRequireDefault(require("./msg.js"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n\nconsole.log(_msg["default"]);'
  },
  './src/msg.js': {
    dependencies: { './word.js': './src/word.js' },
    code:
      '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nexports["default"] = void 0;\n\nvar _word = _interopRequireDefault(require("./word.js"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n\nvar msg = "say ".concat(_word["default"]);\nvar _default = msg;\nexports["default"] = _default;'
  },
  './src/word.js': {
    dependencies: {},
    code:
      '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nexports["default"] = void 0;\nvar word = \'wangqi\';\nvar _default = word;\nexports["default"] = _default;'
  }
})

总结

其实 webpack 官网对于 loader 和 plugin 都有很详细的分析,自定义一个属于自己的 loader 和 plugin 并不难,再一个就是 babel 的强大,省去了我们对 ast 树的生成以及 ast 树的分析工作,其实现在市面上很多这种代码转换工具都是依托于 babel 的强大工具函数,例如 Taro 肯定是使用了。以上介绍的内容只是简要的介绍了下相关的内容,如果需要深入还得时刻关注官方网站的更新及变化。以上的内容都附有源码

以上就是全部的内容,如果有什么不对的地方,欢迎提issues