webpack——babel简单实现

331 阅读9分钟

babel解析过程(ES6——ast-ES5-graph(依赖图)-build.js)

  • 首先加载fs然后读取入口文件,获取入口的文件内容
import fs = require('fs');

function createAsset(filename){
    const content = fs.readFileSync(入口文件,'utf-8')
}


//content打印为入口文件的代码
createAsset('./src/index.js')
  • 使用babel/parser(解析器),解析成ast
import fs = require('fs');
+ import parser = require('@/babel/paser')

function createAsset(filename){
    const content = fs.readFileSync(入口文件,'utf-8')
    
    //一参为要解析的内容
+    const ast = parser.parse(content,{
+       sourceType: 'module'//模块化解析
+    })
}

//content打印为入口文件的代码
createAsset('./src/index.js')

+使用babel/traverse(遍历ast),进行对ast树进行遍历import节点获取导入的文件内容,例如:

import fs = require('fs');
import parser = require('@/babel/paser')
+ import traverse = require('@/babel/traverse').default

function createAsset(filename){
    const content = fs.readFileSync(入口文件,'utf-8')
    
    //一参为要解析的内容
    const ast = parser.parse(content,{
      sourceType: 'module'//模块化解析
    })
    
    
+    const dependencied = [];//因为不只有一个import
+   //遍历ast,获取import节点,获取对应内容,即文件路径,例如import './info.js',会获取到'./info.js'这个内容
+   traverse(ast,{
+        ImportDeclaration: ({node}) => {
+            //向数组存储import的内容
+            dependencied.push(node.source.value);
+        }
+   })
    
}

//content打印为入口文件的代码
createAsset('./src/index.js')
  • 使用babel/core进行把es6变成es5语法
import fs = require('fs');
import parser = require('@/babel/paser')
import traverse = require('@/babel/traverse').default
+ import babel = require('@/babel/core')

function createAsset(filename){
    const content = fs.readFileSync(入口文件,'utf-8')
    
    //一参为要解析的内容
    const ast = parser.parse(content,{
      sourceType: 'module'//模块化解析
    })
    
    
    const dependencied = [];//因为不只有一个import
   //遍历ast,获取import节点,获取对应内容,即文件路径,例如import './info.js',会获取到'./info.js'这个内容
   traverse(ast,{
        ImportDeclaration: ({node}) => {
            //向数组存储import的内容
            dependencied.push(node.source.value);
        }
    })
    
+    //ast--ES5 code
+   const {code} = babel.transformFromAstSync(ast,null,{
+      presets: ['@/babel/preset-env'] 
+    })
    
    
}

//content打印为入口文件的代码
createAsset('./src/index.js')

image.png

  • 最后返回对象:
import fs = require('fs');
import parser = require('@/babel/paser')
import traverse = require('@/babel/traverse').default
import babel = require('@/babel/core')

+ let ID = 0;

function createAsset(filename){
    const content = fs.readFileSync(入口文件,'utf-8')
    
    //一参为要解析的内容
    const ast = parser.parse(content,{
      sourceType: 'module'//模块化解析
    })
    
    
    const dependencied = [];//因为不只有一个import
   //遍历ast,获取import节点,获取对应内容,即文件路径,例如import './info.js',会获取到'./info.js'这个内容
   traverse(ast,{
        ImportDeclaration: ({node}) => {
            //向数组存储import的内容
            dependencied.push(node.source.value);
        }
    })
    
    //ast--ES5 code
   const {code} = babel.transformFromAstSync(ast,null,{
      presets: ['@/babel/preset-env'] 
   })
   
+   let id = ID;
   
+   return {
+       id,
+       filename,//'./src/index.js'
+       dependencied,'['./info.js']'
+       code//es5代码
+   }
    
    
}

//content打印为入口文件的代码
createAsset('./src/index.js')
  • 构建文件依赖图:,根据获取到的入口文件依赖,然后获取逐层引入的依赖,例如入口文件引入的依赖为info.js,而info.js引入了consts.js,现在把它们都扁平化的存储在一个数组上。
+ const path = require('path')
...

funtion createGrap(entry){
    const mainAsset = createAsset(entry)//获取到一个文件的对象而已{id: 1,filename:'./index.js',decepenice: ['./info.js'],code: 当前index.js文件转换后的ES5code}
    
    const queue = [mainAsset]
    //使用递归也可以
    for(const asset of queue){
        const dirname = path.dirname(asset.filename)
        
        asset.mapping = {};//添加映射,后面可以直接根据id找到该模块依赖的模块
        
        asset.dependencies.forEach(realtivePath => {
            const absolutePath = path.join(dirname,relativePath)
            const child = createAsset(absolutePath)
         
            asset.mapping[realtivePath] = child.id;//该默认的依赖映射
            queue.push(child)
        })
    }
    return queue

}

- createAsset('./src/index.js')
+ const graph = createGrap('./src/index.js')

最后的queue形式为如图所示: image.png

  • 实现CMD API整合模块化代码(graph(依赖图)->bundle.js)
...
function bundle(graph){
    //转换为['function(){ es5 code }',{'依赖路径': 该依赖id}]形式
    const modules = ''
    graph.forEach(mo=>{
        modules+ = `
        ${mod.id}: [
            //因为code中的es5代码包含了require,module,exports这些所以要作为参数传入,后面就实现这些参数即可
            function (require,module,exports){
                $(mod.code}
            },
            ${JSON.stringify(mod.mapping)}
        ]
        `
    })


    //构造立即调用函数表达式,因为打包出去的就是字符串
    //module其实就是改造好的graph,更加直观的可以知道依赖关系
    const result  = `(function(modules){
        //实现require
        fuction require(id){
            const [fn,mapping] = modules[id];
            
            //把该模块的es5代码使用require导入的相对路径转为替换id,然而可以作为fn的第一个参数require传入
            function loaclRequire(relativePath){
                return require(mapping[relativePath]);
            }
            
            //二三参数
            const module = {
                exports: {}
            }
            
            fn(loaclRequire,module,module.exports)//实现fn的三个方法require,module,exports
        }
    
        require(0);//执行到入口函数,拿到key为0的值,即入口文件转换后的数组([fun,mapping])
    })({${modules}})`
    
}

const graph = createGrap('./src/index.js')
+ const result = bundle(graph)

最后验证把result打印出来,在console运行,如果可以进行对应的代码输出说明成功。 module大概为如下图所示作为参数传入: image.png

webpack打包过程

  • 打包后之后的文件
    • 为一个自执行函数

    • 参数通过对象的形式传递

      • key为文件路径 value为一个使用eval函数包裹的es5code的函数
      • 如果存在多个相互依赖的文件,参数则为多个key/value对象
    • 怎么把所有文件打包之后形成一个文件,通过_webpack_require_

    • 原理通过递归方式不停的调用自己

webpack 构建流程

Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程 :

  1. 初始化参数:从配置文件和 Shell 语句(即命令行)中读取与合并参数,得出最终的参数。
  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译。而run方法就是使用@babel/parser解析成AST语法树。
  3. 确定入口:根据配置中的 entry 找出所有的入口文件。
  4. 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理。
  5. 完成模块编译:在经过第 4 步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系。
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会。
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

简易实现webpack

我们打包后看到的bundle.js内容大概为如图所示:

// 定义一个立即执行函数,传入生成的依赖关系图
;(function(graph) {
  // 重写require函数
  function require(moduleId) {
    // 找到对应moduleId的依赖对象,调用require函数,eval执行,拿到exports对象
    function localRequire(relativePath) {
      return require(graph[moduleId].dependecies[relativePath]) // {__esModule: true, say: ƒ say(name)}
    }
    // 定义exports对象
    var exports = {}
    ;(function(require, exports, code) {
      // commonjs语法使用module.exports暴露实现,我们传入的exports对象会捕获依赖对象(hello.js)暴露的实现(exports.say = say)并写入
      eval(code)
    })(localRequire, exports, graph[moduleId].code)
    // 暴露exports对象,即暴露依赖对象对应的实现
    return exports
  }
  // 从入口文件开始执行
  require('./src/index.js')
})({
  './src/index.js': {
    dependecies: { './hello.js': './src/hello.js' },
    code: '"use strict";\n\nvar _hello = require("./hello.js");\n\ndocument.write((0, _hello.say)("webpack"));'
  },
  './src/hello.js': {
    dependecies: {},
    code:
      '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nexports.say = say;\n\nfunction say(name) {\n  return "hello ".concat(name);\n}'
  }
})

下面代码为从compiler.run()=>ast树的解析=>ast树遍历获取收集所需依赖=>转换为es5代码=>遍历解析所有依赖项,用一维数组形式表现依赖关系图=>重写require函数,输出bundle方法

const fs = require('fs')
const path = require('path')
const options = require('./webpack.config')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const { transformFromAst } = require('@babel/core')

const Parser = {
  getAst: path => {
    // 读取入口文件
    const content = fs.readFileSync(path, 'utf-8')
    // 将文件内容转为AST抽象语法树
    return parser.parse(content, {
      sourceType: 'module'
    })
  },
  getDependecies: (ast, filename) => {
    const dependecies = {}
    // 遍历所有的 import 模块,存入dependecies
    traverse(ast, {
      // 类型为 ImportDeclaration 的 AST 节点 (即为import 语句)
      ImportDeclaration({ node }) {
        const dirname = path.dirname(filename)
        // 保存依赖模块路径,之后生成依赖关系图需要用到
        const filepath = './' + path.join(dirname, node.source.value)
        dependecies[node.source.value] = filepath
      }
    })
    return dependecies
  },
  getCode: ast => {
    // AST转换为code
    const { code } = transformFromAst(ast, null, {
      presets: ['@babel/preset-env']
    })
    return code
  }
}

class Compiler {
  constructor(options) {
    // webpack 配置
    const { entry, output } = options
    // 入口
    this.entry = entry
    // 出口
    this.output = output
    // 模块
    this.modules = []
  }
  // 构建启动
  run() {
    // 解析入口文件
    const info = this.build(this.entry)
    this.modules.push(info)
    this.modules.forEach(({ dependecies }) => {
      // 判断有依赖对象,递归解析所有依赖项
      if (dependecies) {
        for (const dependency in dependecies) {
          this.modules.push(this.build(dependecies[dependency]))
        }
      }
    })
    // 生成依赖关系图
    const dependencyGraph = this.modules.reduce(
      (graph, item) => ({
        ...graph,
        // 使用文件路径作为每个模块的唯一标识符,保存对应模块的依赖对象和文件内容
        [item.filename]: {
          dependecies: item.dependecies,
          code: item.code
        }
      }),
      {}
    )
    this.generate(dependencyGraph)
  }
  build(filename) {
    const { getAst, getDependecies, getCode } = Parser
    const ast = getAst(filename)
    const dependecies = getDependecies(ast, filename)
    const code = getCode(ast)
    return {
      // 文件路径,可以作为每个模块的唯一标识符
      filename,
      // 依赖对象,保存着依赖模块路径
      dependecies,
      // 文件内容
      code
    }
  }
  // 重写 require函数 (浏览器不能识别commonjs语法),输出bundle
  generate(code) {
    // 输出文件路径
    const filePath = path.join(this.output.path, this.output.filename)
    // 懵逼了吗? 没事,下一节我们捋一捋
    const bundle = `(function(graph){
      function require(module){
        function localRequire(relativePath){
          return require(graph[module].dependecies[relativePath])
        }
        var exports = {};
        (function(require,exports,code){
          eval(code)
        })(localRequire,exports,graph[module].code);
        return exports;
      }
      require('${this.entry}')
    })(${JSON.stringify(code)})`

    // 把文件内容写入到文件系统
    fs.writeFileSync(filePath, bundle, 'utf-8')
  }
}

new Compiler(options).run()

webpack优化

参考

  • webpack生成和开发环境使用插件差异 image.png

  • webpack开发和生产时候常用插件

    • 开发
      • decServe
    • 生产
      • HtmlWebpackPlugin:根据模板html生成index.html,也可以配置html压缩
      • mini-css-extract-plugin: 将css提取出来单独文件,原因为css要变成js,js文件最后在创建style标签进行内容填充,如果js文件就会过大加载就会变慢,会出现闪屏现象
      • postcss-loder、postcss-perset-env插件:进行兼容性处理,例如自动添加样式前缀
      • optimizeCssAssetsWebpackPlugin:css压缩
  • 自带优化

    • 函数名字简化
    • tree-sharking: 在生产环境下,依赖关系的解析时,会把无用的代码不进行打包,例如import {a} from 'aa.js',只会引入aa.js中的a代码,aa.js除了a的代码都不会被打包在dist的输出文件中。
    • scope-hositing: 作用域提升,如果代码为变量定义的也不会打包的代码中,例如var a = 1;var b=2; console.log(a+b)==>console.log(3).
    • source-map: 打包后文件和源文件的映射
  • 手动配置的优化

    • 速度
      • 多线程打包(happypack):下载loader,一般配合babel-loader使用。但是体积小的文件打包时间会比较慢
    • 体积
      • code split(代码分割):按需加载文件
        • 多入口拆分:node_module每个为一个文件,一个入口即为一个chunk同时也会是一个bundle
        module.exports = {
            //单入口
            //entry: './scr/index.js',
            //多入口
            entry: {
                //多入口:有一个入口,最终就输出一个bundle
                index: './src/js/index.js',
                test: './src/js/test.js'
            },
            output:{
                //多入口为生成多个输出文件,因此命名时候主要
                //使用contenthash原因为打包输出的时候,只有文件内容更改过才会生成一个新的hash值,内部不变生成的hash就一直不变
                filename: 'js/[name].[contenthash:10].js',
                path: resolve(__dirname,'build')
            }
        }
        
        • optiomization拆分
        • js使用import实现按需加载(vue路由也是使用这种方式实现)
      • PWA:
      • IgnorePlugin:moment时间插件该插件为很多语言包,我们可以使用IgnorePlugin把不需要的语言包删掉
      • externals:把一些第三方库不打包到压缩文件去,才用cdn形式在html上引入,例如jquery。然后配置:
      //不打包
      externals: {
          'jquery': '$'
      }
      
      //不解析
      modules: {
          noParse: /jquery/,
      }
      
      • dll: 如果按照所有优化配置,发现打包体积还是过大时,可以使用动态链接库(提取到一个单独文件打包,最后放在cdn上引入使用)
    • 其他需要引入的插件
      • autoPrefixer: 加载前缀
      • babel-loader: 解析es6语法