webpack(2)

104 阅读8分钟

01 转换代码

// 首先下载这些包
// 但是这些包可能下载的时候名字有变化,已经更新了,最新版才是ts,去npm搜下吧
// 1. 引入
import { parse } from '@babel/parser'
import traverse from '@babel/traverse'
import generator from '@babel/generator'

// 2. 我们写一句代码
let code = `let a = 'let';let b = '33'`

// 3. 转为ast语法树
const ast = parse(code, {sourceType: 'module'})

// 4. 遍历ast语法树
traverse(ast, {
// item 是遍历的每一项
  enter: item => {
  // 找到遍历的每一项中的VariableDeclaration这个类型
    if (item.node.type === 'VariableDeclaration') {
    // 找到item中的kind,将let改为var
      if (item.node.kind === 'let') {
        item.node.kind = 'var'
      }
    }
  }
});

// 5. 将语法树转换回去
let code2 = generator(ast, {}, code) 

console.log(code2)

02 找出依赖文件 解决嵌套依赖 解决循环依赖

perject_02 和 02ts对应

import { parse } from "@babel/parser";
import traverse from '@babel/traverse'
import { readFileSync } from 'fs'
// resolve 拼接路径  /Users/didi/Desktop/test/webpack-code/project_2/index.js
// relative 得到最后  index.js
// dirname 得到前面的路径  /Users/didi/Desktop/test/webpack-code/project_2
import { resolve, relative, dirname } from 'path';

// 当前目录
const projectRoot = resolve(__dirname, 'project_2')

// 类型声明
/**
 * key 代表文件
 * deps 存储文件中依赖文件
 * code 存储代码
 */
type DepRelation = {
  [key: string]: {
    deps: string[],
    code: string
  }
}

// 初始化手机依赖的数组
const DepRelation: DepRelation = {}

// 将入口文件传入到函数中
collectionCodeAndDeps(resolve(projectRoot, 'index.js'))

console.log("~ DepRelation", DepRelation)

function collectionCodeAndDeps(failPath: string) {
  // 文件的项目
  const key = getProjectPath(failPath)
  /**
   * 解决循环依赖(DepRelation的key是所有index(索引)到的文件)
   */
  if (Object.keys(DepRelation).includes(key)) {
    return
  }
  // 获取文件内容
  const code = readFileSync(failPath).toString()
  // 初始化 depRealation[key]
  DepRelation[key] = {deps: [], code: code}
  // 将代码转为Ast
  const ast = parse(code, {sourceType: 'module'})
  // 分析遍历
  traverse(ast, {
    enter: item => { 
      if (item.node.type === 'ImportDeclaration') {
        // item.node.source.value 是一个相对路径 如: ./a.js
        const depAbsoutePath = resolve(dirname(failPath), item.node.source.value)
        
        // 然后转译为项目路径
        const depProjectPath = getProjectPath(depAbsoutePath)
        // 将依赖装入DepRelation
        DepRelation[key].deps.push(depProjectPath)
        /**
         * 解决嵌套依赖
         */
        collectionCodeAndDeps(depAbsoutePath)
      }
    }
  })
}

function getProjectPath(path: string) {
  return relative(projectRoot, path).replace(/\\/g, '/')
}

03 转译代码(平稳的兼容策略(解决办法): 转译代码,将所有文件打包为一个文件)

/**
 * 因为浏览器不能识别 import export
 * 所以我们进行打包 转换为 require
 */
 import * as babel from '@babel/core';
 import { parse } from "@babel/parser";
 import traverse from '@babel/traverse'
 import { readFileSync } from 'fs'
 // resolve 拼接路径  /Users/didi/Desktop/test/webpack-code/project_2/index.js
 // relative 得到最后  index.js
 // dirname 得到前面的路径  /Users/didi/Desktop/test/webpack-code/project_2
 import { resolve, relative, dirname } from 'path';
 



 // 当前目录
 const projectRoot = resolve(__dirname, 'project_2')
 
 // 类型声明
 /**
  * key 代表文件
  * deps 存储文件中依赖文件
  * code 存储代码
  */
 type DepRelation = {
   [key: string]: {
     deps: string[],
     code: string
   }
 }
 
 // 初始化手机依赖的数组
 const DepRelation: DepRelation = {}
 
 // 将入口文件传入到函数中
 collectionCodeAndDeps(resolve(projectRoot, 'index.js'))
 
 console.log("~ DepRelation", DepRelation)
 



 function collectionCodeAndDeps(failPath: string) {
   // 文件的项目
   const key = getProjectPath(failPath)
   /**
    * 解决循环依赖(DepRelation的key是所有index(索引)到的文件)
    */
   if (Object.keys(DepRelation).includes(key)) {
     return
   }
   // 获取文件内容
   const code = readFileSync(failPath).toString()
   // 转换为es5(本次改动点)
   const { code: es5Code }: any = babel.transform(code, {
     presets: ['@babel/preset-env']
   })
   
   // 初始化 depRealation[key]
   DepRelation[key] = {deps: [], code: es5Code as string}
   // 将代码转为Ast
   const ast = parse(code, {sourceType: 'module'})
   // 分析遍历
   traverse(ast, {
     enter: item => {
       if (item.node.type === 'ImportDeclaration') {
         // item.node.source.value 是一个相对路径 如: ./a.js
         const depAbsoutePath = resolve(dirname(failPath), item.node.source.value)
          
         // 然后转译为项目路径
         const depProjectPath = getProjectPath(depAbsoutePath)
         // 将依赖装入DepRelation
         DepRelation[key].deps.push(depProjectPath)
         /**
          * 解决嵌套依赖
          */
         collectionCodeAndDeps(depAbsoutePath)
       }
     }
   })
 }
 
 function getProjectPath(path: string) {
   return relative(projectRoot, path).replace(/\\/g, '/')
 } 

04 (打包器)将所有文件打包为一个文件(平稳的兼容策略(解决办法): 转译代码,将所有文件打包为一个文件)

  • 这里的文件将会打包为一个文件, 然后去执行他
/**
 * 本次解决问题:
 * 1. 打包为dist文件, dist文件就是将所有文件包含在内, 
 * 2. 不在需要像index.js需要依赖其他文件, 而在dist文件中式全部包含所有文件的, 
 * 3. 然后执行所有模块, 所有文件放到一个文件,再执行, 这个就是打包bundel
 * 运行 ts-node code/04.bundle.ts
 */
import * as babel from '@babel/core';
import { parse } from "@babel/parser";
import traverse from '@babel/traverse'
import { readFileSync } from 'fs'
// resolve 拼接路径  /Users/didi/Desktop/test/webpack-code/project_2/index.js
// relative 得到最后  index.js
// dirname 得到前面的路径  /Users/didi/Desktop/test/webpack-code/project_2
import { resolve, relative, dirname } from 'path';

// 当前目录
const projectRoot = resolve(__dirname, 'project_3')

// 类型声明
/**
* key 代表文件
* deps 存储文件中依赖文件
* code 存储代码
*/
type DepRelation = {
  key: string,
  deps: string[],
  code: string
}[]

type Item = {
key: string,
deps: string[],
code: string
}

// 初始化手机依赖的数组
const depRelation: DepRelation = []

// 将入口文件传入到函数中
collectionCodeAndDeps(resolve(projectRoot, 'index.js'))

console.log("~ DepRelation", depRelation)

function collectionCodeAndDeps(failPath: string) {
  // 文件的项目
  const key = getProjectPath(failPath)
  /**
  * 解决循环依赖(DepRelation的key是所有index(索引)到的文件)
  */
  if (depRelation.find(i => i.key === key)) {
    return
  }
  // 获取文件内容
  const code = readFileSync(failPath).toString()
  // 转换为es5(本次改动点)
  const { code: es5Code }: any = babel.transform(code, {
    presets: ['@babel/preset-env']
  })
  
  // 初始化 depRealation[key]
  const item: Item = {key, deps: [], code: es5Code}

  depRelation.push(item)

  // 将代码转为Ast
  const ast = parse(code, {sourceType: 'module'})
  // 分析遍历
  traverse(ast, {
    enter: path => {
      if (path.node.type === 'ImportDeclaration') {
        // item.node.source.value 是一个相对路径 如: ./a.js
        const depAbsoutePath = resolve(dirname(failPath), path.node.source.value)
        
        // 然后转译为项目路径
        const depProjectPath = getProjectPath(depAbsoutePath)
        // 将依赖装入DepRelation
        item.deps.push(depProjectPath)
        /**
        * 解决嵌套依赖
        */
        collectionCodeAndDeps(depAbsoutePath)
      }
    }
  })
}

function getProjectPath(path: string) {
  return relative(projectRoot, path).replace(/\\/g, '/')
} 

05 手写一个dist文件 (dist文件就是包含所有打包后的代码,里面的code是函数然后执行函数得到结果

/**
 * 这个文件是模仿dist打包文件(dist文件就是将project_3的index.js去转化降级打包为一个文件)
 * depRelation是babel转译后的代码
 * https://www.dvy.com.cn/2020/12/06/6976.html 这个博客有babel的转译后代码的讲解
 */
var depRelation =[
  {
    key: 'index.js',
    deps: ['a.js', 'b.js'],
    code: function(require, module, exports) {
      "use strict";
      // 这里就是bable转译的代码,inport转化为require  _interopRequireDefault是为了做转化
      var _a = _interopRequireDefault(require("./a.js"))
      var _b = _interopRequireDefault(require("./b.js"))
      // _interopRequireDefault的作用就是判断require的模块是否是已经被babel编译过的模块,如果是,则当前require的引用一定存在一个default属性;否则为他加一个default属性,这样便不会调用模块的default为undefined的情况了
      function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj}}
      // _a["default"]就是调用a文件默认导出default
      console.log(_a["default"].getB());
      console.log(_b["default"].getA());
    }
  },
  {
    key: 'a.js',
    deps: ['b.js'],
    code: function(require, module, exports) {
      "use strict";
      // 这里就是bable转译的代码,inport转化为require  _interopRequireDefault是为了做转化
      Object.defineProperty(exports, "__esModule", {
        value: true
      })
      exports["default"] = void 0;
      var _b = _interopRequireDefault(require("./b.js"))
      // _interopRequireDefault的作用就是判断require的模块是否是已经被babel编译过的模块,如果是,则当前require的引用一定存在一个default属性;否则为他加一个default属性,这样便不会调用模块的default为undefined的情况了
      function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj}}

      var a = {
        value: 'a',
        getB: function () {
          return _b["default"].value + 'from a.js'
        }
      }
      var _default = a
      exports['default'] = _default
    }
  },
  {
    key: 'b.js',
    deps: ['a.js'],
    code: function(require, module, exports) {
      "use strict";

      Object.defineProperty(exports, "__esModule", {
        value: true
      })
      exports["default"] = void 0;
      // 这里就是bable转译的代码,inport转化为require  _interopRequireDefault是为了做转化
      var _a = _interopRequireDefault(require("./a.js"))
      // _interopRequireDefault的作用就是判断require的模块是否是已经被babel编译过的模块,如果是,则当前require的引用一定存在一个default属性;否则为他加一个default属性,这样便不会调用模块的default为undefined的情况了
      function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj}}

      var b = {
        value: 'b',
        getA: function () {
          return _a["default"].value + 'from b.js'
        }
      }
      var _default = b
      exports['default'] = _default
    }
  }
]
var moudles = {}
execute(depRelation[0].key)
function execute(key) {
  // 缓存 如果已经 require 过,直接返回上次的结果
  if (moudles[key]) { return moudles[key]}
  // 找到要执行的项目
  var item = depRelation.find(i => i.key === key)
  // 找不到就报错,中断执行
  if (!item) { throw new Error(`${item} is not found`) }
  // 把相对路径变为项目路径
  var pathToley = (path) => {
    var dirname = key.substring(0, key.lastIndexOf('/') + 1)
    var projectPath = (dirname + path).replace(/\.\//g, '').replace(/\/\//, '/')
    return projectPath
  }
  // require 函数
  var require = (path) => {
    return execute(pathToley(path))
  }
  // 初始化当前模块
  moudles[key] = { __esModule: true }
  var moudle = { exports: moudles[key]}
  // 初始化 moudel 方便 code 在 moudel.exports 导出属性
  // 第二个参数moudel ,大部分无用,用于兼容旧代码
  item.code(require, moudle, moudle.exports)
  // 返回当前模块
  return moudles[key]
}

07 这个文件作用是生成dist文件,加上04文件的打包器,就可以输出自动输出dist

  • 那么这里发现只可以解决打包js文件,css呢? 那么就是去将css转化为js,那么就可以加载了
  • 我们建立了一个loader的文件夹,实现了css-loader,就是将css,变为js引入
/**
 * 运行 ts-node code/06.bundleFile.ts
 */
import { writeFileSync, writevSync } from "fs"
import { resolve, relative, dirname, join } from 'path';
import { mkdir } from 'shelljs'
import * as babel from '@babel/core';
import { parse } from "@babel/parser";
import traverse from '@babel/traverse'
import { readFileSync } from 'fs'

const projectName = 'project_3'
const projectRoot = resolve(__dirname, projectName)

type DepRelation = {
  key: string,
  deps: string[],
  code: string
}[]

type Item = {
  key: string,
  deps: string[],
  code: string
}

// 初始化手机依赖的数组
// 类型声明
// 将入口文件的绝对路径传入函数,如 D://faiel/index.js
const depRelation: DepRelation = []
collectionCodeAndDeps(resolve(projectRoot, 'index.js'))
console.log("~ depRelation", depRelation)

const dir = `${projectName}/dist`
mkdir('-p', dir)
writeFileSync(join(dir, 'dist.js'), generatorCode())

function generatorCode() {
  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, moudle, exports) {
        ${code}
      }
    }`
  }).join(',') + '];\n'
  code += 'var moudles = {}\n'
  code += 'execute(depRelation[0].key)\n'
  code += `
    function execute(key) {
      if (moudles[key]) { return moudles[key]}
      var item = depRelation.find(i => i.key === key)
      if (!item) { throw new Error(\`\${item} is not found\`) }
      var pathToley = (path) => {
        var dirname = key.substring(0, key.lastIndexOf('/') + 1)
        var projectPath = (dirname + path).replace(\/\\.\\\/\/g, '').replace(\/\\\/\\\/\/, '/')
        return projectPath
      }
      var require = (path) => {
        return execute(pathToley(path))
      }
      moudles[key] = { __esModule: true }
      var moudle = { exports: moudles[key]}
      item.code(require, moudle, moudle.exports)
      return moudles[key]
    }
  `
  return code
}

// 打包器
function collectionCodeAndDeps(failPath: string) {
  // 文件的项目
  const key = getProjectPath(failPath)
  /**
  * 解决循环依赖(DepRelation的key是所有index(索引)到的文件)
  */
  if (depRelation.find(i => i.key === key)) {
    return
  }
  // 获取文件内容
  const code = readFileSync(failPath).toString()
  // 转换为es5(本次改动点)
  const { code: es5Code }: any = babel.transform(code, {
    presets: ['@babel/preset-env']
  })
  
  // 初始化 depRealation[key]
  const item: Item = {key, deps: [], code: es5Code}

  depRelation.push(item)

  // 将代码转为Ast
  const ast = parse(code, {sourceType: 'module'})
  // 分析遍历
  traverse(ast, {
    enter: path => {
      if (path.node.type === 'ImportDeclaration') {
        // item.node.source.value 是一个相对路径 如: ./a.js
        const depAbsoutePath = resolve(dirname(failPath), path.node.source.value)
        
        // 然后转译为项目路径
        const depProjectPath = getProjectPath(depAbsoutePath)
        // 将依赖装入DepRelation
        item.deps.push(depProjectPath)
        /**
        * 解决嵌套依赖
        */
        collectionCodeAndDeps(depAbsoutePath)
      }
    }
  })
}

function getProjectPath(path: string) {
  return relative(projectRoot, path).replace(/\\/g, '/')
} 

08 对前面的loader进行优化 loader2(单一职责原则)

  • webpack 里每个loader只做一件事情 现在loader文件中做了两件事 1.转字符串 和 2.代码扔到dom中
  • 目前我的loader做了两件事
    1. 将css转化为js字符串
  1. webpack-cli 是如何调用 webpack的? 答案: webpack = require('webpack') compiler = webpack(option, callback)
  2. webpack 如何分析 index.js? 答案: 学习前面我知道打包器首先分析收集依赖,然后打包成一个文件,并且webapck肯定也是去分析ast匹配语法树而不是用正则,

09 plugin (imagemin-webpack-plugin)

/**
 * 运行 ts-node code/06.bundleFile.ts
 */
import { writeFileSync, writevSync } from "fs"
import { resolve, relative, dirname, join } from 'path';
import { mkdir } from 'shelljs'
import * as babel from '@babel/core';
import { parse } from "@babel/parser";
import traverse from '@babel/traverse'
import { readFileSync } from 'fs'

const projectName = 'project_3'
const projectRoot = resolve(__dirname, projectName)

type DepRelation = {
  key: string,
  deps: string[],
  code: string
}[]

type Item = {
  key: string,
  deps: string[],
  code: string
}

// 初始化手机依赖的数组
// 类型声明
// 将入口文件的绝对路径传入函数,如 D://faiel/index.js
const depRelation: DepRelation = []
collectionCodeAndDeps(resolve(projectRoot, 'index.js'))
console.log("~ depRelation", depRelation)

const dir = `${projectName}/dist`
mkdir('-p', dir)
writeFileSync(join(dir, 'dist.js'), generatorCode())

function generatorCode() {
  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, moudle, exports) {
        ${code}
      }
    }`
  }).join(',') + '];\n'
  code += 'var moudles = {}\n'
  code += 'execute(depRelation[0].key)\n'
  code += `
    function execute(key) {
      if (moudles[key]) { return moudles[key]}
      var item = depRelation.find(i => i.key === key)
      if (!item) { throw new Error(\`\${item} is not found\`) }
      var pathToley = (path) => {
        var dirname = key.substring(0, key.lastIndexOf('/') + 1)
        var projectPath = (dirname + path).replace(\/\\.\\\/\/g, '').replace(\/\\\/\\\/\/, '/')
        return projectPath
      }
      var require = (path) => {
        return execute(pathToley(path))
      }
      moudles[key] = { __esModule: true }
      var moudle = { exports: moudles[key]}
      item.code(require, moudle, moudle.exports)
      return moudles[key]
    }
  `
  return code
}

// 打包器
function collectionCodeAndDeps(failPath: string) {
  // 文件的项目
  const key = getProjectPath(failPath)
  /**
 * 解决循环依赖(DepRelation的key是所有index(索引)到的文件)
 */
  if (depRelation.find(i => i.key === key)) {
    return
  }
  // 获取文件内容
  let code = readFileSync(failPath).toString()
  /**
  * 这里是我们本次文件新加的处理css文件代码
  */
  if (/\.css$/.test(failPath)) {
    code = require('./loader2/css-loader.js')(code)
    code = require('./loader2/style-loader.js')(code)
  }
  // 转换为es5(本次改动点)
  const { code: es5Code }: any = babel.transform(code, {
    presets: ['@babel/preset-env']
  })
  
  // 初始化 depRealation[key]
  const item: Item = {key, deps: [], code: es5Code}

  depRelation.push(item)

  // 将代码转为Ast
  const ast = parse(code, {sourceType: 'module'})
  // 分析遍历
  traverse(ast, {
    enter: path => {
      if (path.node.type === 'ImportDeclaration') {
        // item.node.source.value 是一个相对路径 如: ./a.js
        const depAbsoutePath = resolve(dirname(failPath), path.node.source.value)
        
        // 然后转译为项目路径
        const depProjectPath = getProjectPath(depAbsoutePath)
        // 将依赖装入DepRelation
        item.deps.push(depProjectPath)
        /**
       * 解决嵌套依赖
       */
        collectionCodeAndDeps(depAbsoutePath)
      }
    }
  })
}

function getProjectPath(path: string) {
  return relative(projectRoot, path).replace(/\\/g, '/')
} 

利用node, 通过浏览器调试代码:

eg: node --inspect-brk ./node_modules/webpack-cli/bin/cli.js