webpack 核心原理

1. 怎样才能运行 import / export

  • 不同浏览器功能不同
    现代浏览器可以通过

    运行 http-server project_1/

    它会把所有的文件都请求一遍

    2). 平稳的兼容策略:把关键字转译为普通代码,并把所有文件打包成一个文件
    缺点:需要写复杂的代码来完成这件事情

    2. 把关键字转移成普通代码

    将 import / export 转成函数

    使用 @babel/core ,在上一节的 deps_4.ts 里添加下面几行代码

    import * as babel from '@babel/core';
     const code = readFileSync(filepath).toString()
    + const { code: es5Code } = babel.transform(code, {
    +    presets: ['@babel/preset-env']
    +  })
      // 初始化 depRelation[key]
    +  depRelation[key] = { deps: [], code: es5Code }
    

    运行 node -r ts-node/register bundler_1.ts

    a.js 的变化
    1). import 关键字 -> 变成了 require()
    2). export 关键字 -> 变成 exports['default']

    伏笔
    这里的 code 是字符串

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

    疑惑1

    Object.defineProperty(exports, "__esModule", {value: true});
    这是在做啥?

    • 解惑
      给当前模块添加 __esModule: true 属性,方便跟 CommonJS 模块区分开
      那为什么不直接用 exports.__esModule = true;
      两种区别不大,上面写法功能更强,exports.__esModule 兼容性更好
    疑惑2

    exports["default"] = void 0;
    这是在做啥?
    解惑
    void 0 等价于 undefined,老 JSer 的常见过时技巧
    这句话是为了强制清空 exports['default'] 的值

    细节1

    import b from './b.js' 变成了
    var _b = _interopRequireDefault(require("./b.js"))
    b.value 变成了
    _b['default'].value
    解释: _interopRequireDefault(module)

    _ 下划线前缀是为了避免与其他变量重名
    该函数的意图是给模块添加 'default'

    为什么要加 default?
    CommonJS 模块没有默认导出,加上方便兼容

    内部实现:return m && m.__esModule ? m : { "default": m }
    其他 _interop 开头的函数大多都是为了兼容旧代码

    细节2

    export default a 变成了
    var _default = a; exports["default"] = _default;
    简化一下就是 exports["default"] = a

    const x = 'x'; export {x} 会变成 var x = 'x'; exports.x = x
    解释
    这个 _default 中间变量有什么意义我也没看出来,也许后面有用
    其他部分都挺好理解的

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

    • 本质:ESModule 语法变成了 CommonJS 规则

    3. 把所有的文件打包成一个

    • 打包成一个什么样的文件?
      包含了所有模块,然后能执行所有模块
      比如:
    var depRelation = [ 
      {key: 'index.js', deps: ['a.js', 'b.js'], code: function... },
      {key: 'a.js', deps: ['b.js'], code: function... },
      {key: 'b.js', deps: ['a.js'], code: function... }
    ] // 为什么把 depRelation 从对象改为数组?
    // 因为数组的第一项就是入口,而对象没有第一项的概念
    execute(depRelation[0].key) // 执行入口文件
    function execute(key){
      var item = depRelation.find(i => i.key === key)
      item.code(???) // 执行 item 的代码,因此 code 最好是个函数,方便执行
      // 但是目前还不知道要传什么参数给 code 
      // 代码待完善
    }
    
    

    现在有三个问题还没解决
    1). depRelation 是对象,需要变成一个数组
    2). code 是字符串,需要变成一个函数
    3). execute 函数待完善

    3.1 把 depRelation 改为一个数组

    复制 bundle_1.ts 修改如下代码

    type DepRelation = { key: string,  deps: string[], code: string }[];
    // 初始化一个空的 depRelation,用于收集依赖
    const depRelation: DepRelation = [];
    const item = { deps: [], code: es5Code }
    
    traverse(ast, {
        enter: path => {
          if (path.node.type === 'ImportDeclaration') {
            item.deps.push(depProjectPath)
          }
        }
      })
    

    3.2 把 code 由字符串改为函数

    上面代码 code2 加上${code2}后就是一个函数了
    require, module, exports 这三个参数是 CommonJS 2 规范规定的

    3.3 完善 execute 函数(主体思路)

    const modules = {} // modules 用于缓存所有模块�function execute(key) { 
      if (modules[key]) { return modules[key] }
      var item = depRelation.find(i => i.key === key)
      var require = (path) => {
        return execute(pathToKey(path)) // 把相对路径变成 key 比如./b.js => b.js
      }
      modules[key] = { __esModule: true } // modules['a.js'] 给 a.js 准备一个空对象方便它去挂载
      var module = { exports: modules[key] }
      item.code(require, module, module.exports)  // 执行 a.js 的代码执行后就会挂载到 module.exports 上面
      return module.exports
    }
    
    

    3.4 最终文件主要内容

    var depRelation = [ 
      {key: 'index.js', deps: ['a.js', 'b.js'], code: function... },
      {key: 'a.js', deps: ['b.js'], code: function... },
      {key: 'b.js', deps: ['a.js'], code: function... }
    ] 
    var modules = {} // modules 用于缓存所有模块
    execute(depRelation[0].key)
    function execute(key){
      var require = ...
      var module = ...
      item.code(require, module, module.exports)
      ...
    }
    // 详见 dist.js
    

    dist.js 代码
    https://github.com/wanglifa/webapck-demo-2/blob/main/dist.js

    虽然我们已经知道了最终文件的主要内容,但是怎么才能得到这个最终文件那?
    答:拼凑出字符串,然后写入文件

    var dist = ""; 
    dist += content; 
    writeFileSync('dist.js', dist)
    

    3.5 自动创建最终文件

    • bundler_3.ts(基于 bundler_2.ts 复制修改的)
    + import { writeFileSync } from 'fs'
    + writeFileSync('dist_2.js', generateCode())
    
    + function generateCode() {
      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
    + }
    

    运行 node -r ts-node/register bundler_3.ts
    得到新文件 dist_2.js,与 dist.js 相差无几

    3.6 目前还存在的问题

    问题列表
    1). 生成的代码中有多个重复的 _interopXXX 函数
    2). 只能引入和运行 JS 文件
    3). 只能理解 import,无法理解 require
    4). 不支持插件
    5). 不支持配置入口文件和 dist 文件名

你可能感兴趣的:(webpack 核心原理)