Webpack之核心——打包

为什么要打包

因为要解决两个关键问题:

  1. 因为某些浏览器不支持: import和export关键字。
  2. 防止文件过多。
import b from './b.js'
const a = {
  value: 'a',
  getB: () => b.value + ' from a.js'
}
export default a

1、浏览器不支持 import和export关键字

虽然谷歌浏览器支持 <script type="module"></script> 写法,可以支持import和export关键字,但是IE8~15不支持type=module的写法。所以要将import和export关键字转化成所有浏览器能识别的语法:

  • import 关键字变成 require 函数
  • export 关键字变成 exports 对象

import和export关键字属于ES Module 模块,require函数和exports对象属于CommonJS模块。

它们之间的区别:

  • ES Module 是 ES 标准里的模块定义语法,CommonJS2 则是 Node.js 社区的民间约定
  • ES Module 使用 import 和 export 关键字,CommonJS2 则使用 require 函数和 exports 对象
  • ES Modules 对模块采用了「动态只读引用」,而 CommonJS2 则是简单的浅复制

2、文件过多,需要打包成一个文件

虽然有的浏览器能支持type='module'写法,但是当我们真正在浏览器中开始使用一个项目的时候,整个项目可能会有成千上万个通过import、export关键字引用的文件,这会导致浏览器请求文件时间特别长。

所以我们需要将关键字转译为普通代码,并把所有文件打包成一个。

打包成什么样的文件?

肯定包含了所以模块,然后能执行所有模块。

 

打包过程

我们结合我另一篇文章的内容——《Webpack之AST、Babel、依赖》。将a.js 和 b.js打包。得出如下代码:

a.js 的变化:import 关键字不见了,变成了 require()。export 关键字不见了,变成了 exports['default']。

a.js 变成 ES5 之后的代码详解:

"use strict"; //严格模式

// 等于 exports[__esModule] = true。__esModule为true则表示为ES module,与之CommonJS区分开
Object.defineProperty(exports, "__esModule", {value: true});

//老JS技巧,清空值,将值变为undefined  
exports["default"] = void 0;

var _b = _interopRequireDefault(require("./b.js")); 
// _interopRequireDefault函数意图给模块添加'default'
// 如果有__esModule则输出obj,没有则加上default默认值
// 其他 _interop 开头的函数大多都是为了兼容旧代码
// function _interopRequireDefault(obj) {                     
//   return obj && obj.__esModule ? obj : { "default": obj }; 
// }

var a = {
  value: 'a',  
  getB: function getB() {
    return _b["default"].value + ' from a.js';  
  }
};

// 等于 exports["default"] = a   
var _default = a;                     
exports["default"] = _default; 

 

打包后的文件

简易的打包器:

import { parse } from "@babel/parser"
import traverse from "@babel/traverse"
import { writeFileSync, readFileSync } from 'fs'
import { resolve, relative, dirname } from 'path';
import * as babel from '@babel/core'

// 设置根目录
const projectRoot = resolve(__dirname, 'project_1')
// 类型声明
type DepRelation = { key: string, deps: string[], code: string }[]
// 初始化一个空的 depRelation,用于收集依赖
const depRelation: DepRelation = [] // 数组!

// 将入口文件的绝对路径传入函数,如 D:\demo\fixture_1\index.js
collectCodeAndDeps(resolve(projectRoot, 'index.js'))

writeFileSync('dist_2.js', generateCode())
console.log('done')

function generateCode() {
  // 在 code 字符串外面包一个 function(require, module, exports){ ... } *
  // 把 code 写到文件里,引号不会出现在文件中
  let code = ''
  code += 'var depRelation = [' + depRelation.map(item => {
    const { key, deps, code } = item
    return `{
      key: ${JSON.stringify(key)}, 
      deps: ${JSON.stringify(deps)},
      code: function(require, module, exports){
        ${code}
      }
    }`
  }).join(',') + '];\n'
  code += 'var modules = {};\n'
  code += `execute(depRelation[0].key)\n`
  code += `
  function execute(key) {
    if (modules[key]) { return modules[key] }
    var item = depRelation.find(i => i.key === key)
    if (!item) { throw new Error(\`\${item} is not found\`) }
    var pathToKey = (path) => {
      var dirname = key.substring(0, key.lastIndexOf('/') + 1)
      var projectPath = (dirname + path).replace(\/\\.\\\/\/g, '').replace(\/\\\/\\\/\/, '/')
      return projectPath
    }
    var require = (path) => {
      return execute(pathToKey(path))
    }
    modules[key] = { __esModule: true }
    var module = { exports: modules[key] }
    item.code(require, module, module.exports)
    return modules[key]
  }
  `
  return code
}

function collectCodeAndDeps(filepath: string) {
  const key = getProjectPath(filepath) // 文件的项目路径,如 index.js
  if (depRelation.find(i => i.key === key)) {
    // 注意,重复依赖不一定是循环依赖
    return
  }
  // 获取文件内容,将内容放至 depRelation
  const code = readFileSync(filepath).toString()
  const { code: es5Code } = babel.transform(code, {
    presets: ['@babel/preset-env']
  })
  // 初始化 depRelation[key]
  // depRelation[key] = { deps: [], code: es5Code }
  // 改为了
  // const item = { key, deps: [], code: es5Code }
  // depRelation.push(item)
  const item = { key, deps: [], code: es5Code }
  depRelation.push(item)
  // 将代码转为 AST
  const ast = parse(code, { sourceType: 'module' })
  // 分析文件依赖,将内容放至 depRelation
  traverse(ast, {
    enter: path => {
      if (path.node.type === 'ImportDeclaration') {
        // path.node.source.value 往往是一个相对路径,如 ./a.js,需要先把它转为一个绝对路径
        const depAbsolutePath = resolve(dirname(filepath), path.node.source.value)
        // 然后转为项目路径
        const depProjectPath = getProjectPath(depAbsolutePath)
        // 把依赖写进 depRelation
        item.deps.push(depProjectPath)
        collectCodeAndDeps(depAbsolutePath)
      }
    }
  })
}
// 获取文件相对于根目录的相对路径
function getProjectPath(path: string) {
  return relative(projectRoot, path).replace(/\\/g, '/')
}

简化版打包后的文件: 

// dist_2.js
var depRelation = [ 
  {key: 'index.js', deps: ['a.js', 'b.js'], code: function(require, module, exports) {...} },
  {key: 'a.js', deps: ['b.js'], code: function(require, module, exports) {...} },
  {key: 'b.js', deps: ['a.js'], code: function(require, module, exports) {...} }
] 
execute(depRelation[0].key)
var modules = {}

// 执行入口文件,把depRelation从对象改为数组。因为数组的第一项就是入口,而对象没有第一项的概念。
execute(depRelation[0].key)
function execute(key) {
  // 如果已经 require 过,就直接返回上次的结果
  if (modules[key]) { return modules[key] }
  // 找到要执行的项目
  var item = depRelation.find(i => i.key === key)
  // 找不到就报错,中断执行
  if (!item) { throw new Error(`${item} is not found`) }
  // 把相对路径变成项目路径
  var pathToKey = (path) => {
    var dirname = key.substring(0, key.lastIndexOf('/') + 1)
    var projectPath = (dirname + path).replace(/\.\//g, '').replace(/\/\//, '/')
    return projectPath
  }
  // 创建 require 函数
  var require = (path) => {
    return execute(pathToKey(path))
  }
  // 初始化当前模块
  modules[key] = { __esModule: true }
  // 初始化 module 方便 code 往 module.exports 上添加属性
  var module = { exports: modules[key] }
  // 调用 code 函数,往 module.exports 上添加导出属性
  // 第二个参数 module 大部分时候是无用的,主要用于兼容旧代码
  item.code(require, module, module.exports)
  // 返回当前模块
  return modules[key]
}

最终运行dist_2.js打印出来的的文件与转译前的内容一致:

 

总结

  • webpack核心原理就是转译和打包。
  • 把 code 由字符串改为函数。
  • depRelation 从对象改为数组,因为数组的第一项就是入口,而对象没有第一项的概念。
  • 一些转译后的源码无需花太多精力去理解,因为并没有多大实际意义,如:item.code(require, module, module.exports) 中的module,或者是var _default = a; exports["default"] = _default。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值