webpack原理

写在前面:
本文是webpack的一个学习笔记,涉及webpack打包流程、plugin、loader的编写、以及实现一个简易版的webpack。
欢迎交流&有任何问题请大佬斧正

参考:
《深入浅出webpack》(写的很好,但是是基于webpack3+,tapable1.0以前的,跟最新的有些区别,但原理流程写的很清楚)
webpack原理与实战
webpack详解

本文基于webpack4+

webpack是一个打包模块化js的工具,可以通过loader转换文件,通过plugin扩展功能。
它本身结构精巧,基于tapable的插件架构,扩展性强,众多的loader或者plugin让webpack显得复杂。

一、webpack核心概念

一个常见的webpack配置长这样:

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: "./app/entry", // string | object | array
  // Webpack打包的入口
  output: {  // 定义webpack如何输出的选项
    path: path.resolve(__dirname, "dist"), // string
    // 所有输出文件的目标路径
    filename: "[chunkhash].js", // string
    // 「入口(entry chunk)」文件命名模版
    publicPath: "/assets/", // string
    // 构建文件的输出目录
    /* 其它高级配置 */
  },
  module: {  // 模块相关配置
    rules: [ // 配置模块loaders,解析规则
      {
        test: /\.jsx?$/,  // RegExp | string
        include: [ // 和test一样,必须匹配选项
          path.resolve(__dirname, "app")
        ],
        exclude: [ // 必不匹配选项(优先级高于test和include)
          path.resolve(__dirname, "app/demo-files")
        ],
        loader: "babel-loader", // 模块上下文解析
        options: { // loader的可选项
          presets: ["es2015"]
        },
      },
  },
  resolve: { //  解析模块的可选项
    modules: [ // 模块的查找目录
      "node_modules",
      path.resolve(__dirname, "app")
    ],
    extensions: [".js", ".json", ".jsx", ".css"], // 用到的文件的扩展
    alias: { // 模块别名列表
      "module": "new-module"
      },
  },
  devtool: "source-map", // enum
  // 为浏览器开发者工具添加元数据增强调试
  plugins: [ // 附加插件列表
    new HtmlWebpackPlugin({template: './src/index.html'})
  ],
}
  • Entry:指定webpack开始构建的入口模块,从该模块开始构建并计算出直接或间接依赖的模块或者库
  • Output:输出的文件名以及输出的目录
  • Loaders:文件转换器。Loaders将各类型的文件处理成webpack能够处理的模块,例如把es6转换为es5,scss转换为css
  • Plugins:用于扩展webpack的功能,在webpack构建生命周期的节点上加入扩展hook为webpack加入功能。
  • Chunk:coding split的产物,我们可以对一些代码打包成一个单独的chunk,比如某些公共模块,去重,更好的利用缓存。或者按需加载某些功能模块,优化加载时间。

二、webpack构建流程

从启动webpack构建到输出结果经历了一系列过程,它们是:

  1. 解析webpack配置参数,合并从shell传入和webpack.config.js文件里配置的参数,生产最后的配置结果。
  2. 注册所有配置的插件,好让插件监听webpack构建生命周期的事件节点,以做出对应的反应。
  3. 从配置的entry入口文件开始解析文件构建AST语法树,找出每个文件所依赖的文件,递归下去。
  4. 在解析文件递归的过程中根据文件类型和loader配置找出合适的loader用来对文件进行转换。
  5. 递归完后得到每个文件的最终结果,根据entry配置生成代码块chunk。
  6. 输出所有chunk到文件系统。

需要注意的是,在构建生命周期中有一系列插件在合适的时机做了合适的事情,比如UglifyJsPlugin会在loader转换递归完后对结果再使用UglifyJs压缩覆盖之前的结果。

三、webpack安装

如果你使用 webpack 4+ 版本,你还需要安装 CLI。

mkdir webpack-demo && cd webpack-demo
npm init -y
npm install webpack webpack-cli --save-dev

新建一个webpack.config.js文件
配置entry、output等必要内容

新建src文件夹,里面新建index.js文件

四、loader

对一个个单独的文件进行转换

常用loader:
https://webpack.docschina.org/loaders/

  • babel-loader把es6转换成es5
  • file-loader把文件替换成对应的URL
  • raw-loader注入文本文件内容到代码里去

写一个loader

写一个将js文件中的mll都替代成另一个词的loader

可以在webpack.config.js同级新建一个文件夹loaders
里面新建一个文件replaceLoader.js

// loader就是一个函数
// 不能写成箭头函数
// 因为要用到this指向

// source:引入文件的内容
module.exports = function(source) {
    return source.replace('mll', 'mll cool')
}

写一个loader其实很简单,将源代码处理后return就可以了

webpack中引入:
webpack.config.js

    module: {
        rules: [{
            test: /\.js/,
            use: [{
                loader: path.resolve(__dirname, './loaders/replaceLoader.js')
            }
            ]
        }]
    }

引入loader的位置

package.json

  "scripts": {
    "build": "webpack"
  },

执行npm run build
打包的时候就会把js文件里的mll替换成mll cool

1.options传参

webpack.config.js

    module: {
        rules: [{
            test: /\.js/,
            use: [{
                loader: path.resolve(__dirname, './loaders/replaceLoader.js'),
                options: {
                    name: 'wasabi'
                }
            }
            ]
        }]
    }

loaders/replaceLoader.js

module.exports = function(source) {
    console.log(this.query)
    return source.replace('mll', 'mll cool')
}

在loader中,this.query能取到options中的值
打印出来的结果为
{ name: 'wasabi' }

因此

module.exports = function(source) {
    return source.replace('mll', this.query.name)
}

可以通过外部传options来改变替换的内容


this.query的官方说法:

https://www.webpackjs.com/api/loaders/#this-query

如果这个 loader 配置了 options 对象的话,this.query 就指向这个 option 对象。
如果 loader 中没有 options,而是以 query 字符串作为参数调用时,this.query 就是一个以 ? 开头的字符串。
options 已取代 query,所以此属性废弃。使用 loader-utils 中的 getOptions 方法来提取给定 loader 的 option。

loader-utils

推荐使用loader-utils

npm install loader-utils --save-dev

举一个和callback一起使用的例子
https://www.webpackjs.com/api/loaders/#this-callback

this.callback(
  err: Error | null,
  content: string | Buffer,
  sourceMap?: SourceMap,
  meta?: any
);
module.exports = function(source) {
    const options = loaderUtils.getOptions(this)
    const result = source.replace('mll', options.name)
    this.callback(null, result) // 等价于return
    // this.callback(null, result, source, meta)
}

this.callback就相当于一个return的操作
但比return好的是,能传出去更多内容,例如源代码等等

2.异步操作

若loader中有异步操作

const loaderUtils = require('loader-utils')

module.exports = function(source) {
    const options = loaderUtils.getOptions(this)
    setTimeout(()=>{
        const result = source.replace('mll', options.name)
        return result
    }, 1000)
}

因为一开始调用loader的时候没有return东西,因此会报错

 Error: Final loader (./loaders/replaceLoader.js) didn't return a Buffer or String

解决:
this.async()

const loaderUtils = require('loader-utils')

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

然后打包,就能看到,打包时间5000+ms

Hash: aa999e77339b23001859
Version: webpack 4.39.3
Time: 5089ms

3.使用多个loader

loader的使用顺序是从下到上,从右到左

use: [{
    loader: path.resolve(__dirname, './loaders/replaceLoader.js'),
},{
    loader: path.resolve(__dirname, './loaders/replaceLoaderAsync.js'),
    options: {
        name: 'wasabi'
    }
}
]

因此是先做replaceLoaderAsync,replaceLoaderAsync处理后的代码再扔进replaceLoader做处理

每次写
path.resolve(__dirname, './loaders/replaceLoaderAsync.js'),
很麻烦,有什么简化方法吗?

可以这么写:

resolveLoader: {
    modules: ['node_modules', './loaders']
},
module: {
    rules: [{
        test: /\.js/,
        use: [{
            loader: 'replaceLoader'
        },{
            loader: 'replaceLoaderAsync',
            options: {
                name: 'wasabi'
            }
        }
        ]
    }]
}

可以用resolveLoader
表示loader先在node_modules中找,找不到的话在./loaders下找

4.loader用途

自己开发loader一般可用在哪里呢?
对源代码做一些包装

例如:

  1. 网页有中文版和英文版,可以最开始写的时候弄一个占位符,在打包的时候通过loader切换??
  2. 代码做异常监控
    在loader中对source中的function做try catch的包装。这样就可以不对业务源代码做改动了

五、plugin

常用plugin:https://webpack.docschina.org/plugins/

plugin编写

plugin 可以通过一些 hook 函数来拦截 webpack 的执行,甚至你可以运行一个子编译器和 loader 串联

a).Compilation、compiler

plugin 插件其实是一个含有 apply 方法的 class,而 apply 方法的参数就是 compiler 对象,compiler 对象里有各种钩子,这些钩子分别会在 webpack 的运行过程中触发,而实现这些钩子的核心是 tapable ,这个 tapable 还算好理解,可以把它看做是一个更高级的 发布-订阅。

总结:

  • Compiler 对象包含了 Webpack 环境所有的的配置信息,包含 options,loaders,plugins 这些信息,这个对象在 Webpack 启动时候被实例化,它是全局唯一的,可以简单地把它理解为 Webpack 实例;
  • Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等,代表了一次单一的版本构建和生成资源。当 Webpack 以开发模式运行时,每当检测到一个文件变化,一次新的 Compilation 将被创建。Compilation 对象也提供了很多事件回调供插件做扩展。通过 Compilation 也能读取到 Compiler 对象。
  • Compiler 和 Compilation 的区别在于:Compiler 代表了整个 Webpack 从启动到关闭的生命周期,而 Compilation 只是代表了一次新的编译。

可以通过 compilation 的 assets 对象来编写新的文件,或是修改已经创建的文件。

b).新建一个plugins

webpack.config.js同级下新建一个plugins文件夹

里面新建一个copyright-webpack-plugin.js文件

在原型上定义apply方法

// loader是个函数,plugin是个类

class CopyrightWebpackPlugin{
    constructor(options) {
        console.log('use plugin')
        console.log(options)
    }

    // 调插件时会调用apply方法
    // compiler 可以理解为webpack实例,存储了webpack各种各样的内容,打包的过程
    apply(compiler) {

    }
}

module.exports = CopyrightWebpackPlugin

webpack.config.js

const CopyrightWebpackPlugin = require('./plugins/copyright-webpack-plugin')

module.exports = {
    .....
    plugins: [
        new CopyrightWebpackPlugin({
            name: 'mll'
        })
    ]
}

在插件的constructor中可以拿到向插件中传的值
因此打印出来

use plugin
{name: 'mll'}

c).hooks

tapable

tapable 这个小型 library 是 webpack 的一个核心工具,但也可用于其他地方,以提供类似的插件接口。webpack 中许多对象扩展自 Tapable 类。这个类暴露 tap, tapAsync 和 tapPromise 方法,可以使用这些方法,注入自定义的构建步骤,这些步骤将在整个编译过程中不同时机触发。

webpack中最核心的负责编译的Compiler和负责创建bundles的Compilation都是Tapable的实例。

webpack3及其以前使用的是Tapable1.0之前的版本,提供了包括:

  • plugin(name:string, handler:function)注册插件到Tapable对象中
  • apply(…pluginInstances: (AnyPlugin|function)[])调用插件的定义,将事件监听器注册到Tapable实例注册表- 中
  • applyPlugins*(name:string, …)多种策略细致地控制事件的触发,包括applyPluginsAsync、- applyPluginsParallel等方法实现对事件触发的控制

之后使用的是1.0的Tapable

Tapable 1.0

暴露出很多的钩子,可以使用它们为插件创建钩子函数

const {
    SyncHook,
    SyncBailHook,
    SyncWaterfallHook,
    SyncLoopHook,
    AsyncParallelHook,
    AsyncParallelBailHook,
    AsyncSeriesHook,
    AsyncSeriesBailHook,
    AsyncSeriesWaterfallHook
 } = require("tapable");

1.0 Tapable使用方法:
https://github.com/webpack/tapable
https://juejin.im/post/5aa3d2056fb9a028c36868aa

Sync*类型的钩子:

  • 注册在该钩子下面的插件的执行顺序都是顺序执行。
  • 只能使用tap注册,不能使用tapPromise和tapAsync注册

对于Async*类型钩子:

  • 支持tap、tapPromise、tapAsync注册

webpack中的使用

https://www.webpackjs.com/api/compiler-hooks/#emit

生命周期钩子函数,是由 compiler 暴露,可以通过如下方式访问:

compiler.hooks.someHook.tap(/* ... */);

取决于不同的钩子类型,也可以在某些钩子上访问 tapAsync 和 tapPromise。

  • someHook为具体的钩子名称
  • tap 同步、tapAsync 异步、tapPromise
    异步可以使用 tap/tapAsync/tapPromise 方法触及。同步只能使用tap

以emit和compile的使用为例:

emit(异步hook):
AsyncSeriesHook
生成资源到 output 目录之前。
参数:compilation

compile(同步hook):
SyncHook
一个新的编译(compilation)创建之后,钩入(hook into) compiler。

写一个CopyrightWebpackPlugin

// loader是个函数,plugin是个类

class CopyrightWebpackPlugin{
    constructor(options) {
        console.log('use plugin')
        console.log(options)
    }

    // 调插件时会调用apply方法
    // compiler 可以理解为webpack实例,存储了webpack各种各样的内容,打包的过程
    apply(compiler) {
        // 同步,一个参数即可,不用传callback,后面也不用手动调用callback
        compiler.hooks.compile.tap('CopyrightWebpackPlugin', (compilation) => {
            console.log('compile')
        })

        // 异步时刻值(异步勾子)
        // compilation中只存放了和这次打包有关的内容
        // emit 时刻
        // 异步是两个参数
        compiler.hooks.emit.tapAsync('CopyrightWebpackPlugin',(compilation, cb) => {
            console.log(compilation.assets) // 打包生成的内容
            // 在我们即将把代码放到dist目录之前,又增加了一个文件
            compilation.assets['copyright.txt'] = {
                // 内容
                source: function() {
                    return 'copyright by mll'
                },
                size: function() {
                    return 16; // 内容长度
                }
            }
            cb() // 一定要在最后调用一下cb
        })
    }
}

module.exports = CopyrightWebpackPlugin

会在dist目录下多生成一个copyright.txt文件,里面有内容copyright by mll
除此之外,还有done等等时间钩子。查阅文档即可https://www.webpackjs.com/api/compiler-hooks/

注意下同步和异步的不同写法:
同步:

compiler.hooks.compile.tap('CopyrightWebpackPlugin', (compilation) => {
    console.log('compile')
})

同步只能使用tap触及
一个参数compilation

异步:

compiler.hooks.emit.tapAsync('CopyrightWebpackPlugin',(compilation, cb) => {
    console.log('test') 
    cb() // 一定要在最后调用一下cb
})

异步可以使用 tap/tapAsync/tapPromise 方法触及
两个参数(compilation, cb),第二个参数为回调函数cb,且最后一定要调用下这个回调函数cb()
(即在插件处理完任务时需要调用回调函数通知 Webpack,才会进入下一处理流程。 如果不执行 cb(),运行流程将会一直卡在这不往下执行 )

插件能够 钩入(hook) 到在每个编译(compilation)中触发的所有关键事件。在编译的每一步,插件都具备完全访问 compiler 对象的能力,如果情况合适,还可以访问当前 compilation 对象。

d).想知道compilation里面有些什么&& 调试 Webpack

由于 Webpack 运行在 Node.js 之上,调试 Webpack 就相对于调试 Node.js 程序。

在package.json中,加入

  "scripts": {
    "debug": "node --inspect --inspect-brk node_modules/webpack/bin/webpack.js",
  },

node在运行webpack.js的时候,传递些node的参数
--inspect 开启node的调试工具
--inspect-brk 在运行webpack做调试的时候,在运行webpack命令执行的时候,在第一行打一个断点

运行npm run debug

打开浏览器控制台,发现控制台上会多一个绿色的node标志,点击标志,会打开一个node调试框DevTools-Node.js

就会到第一行的断点位置,这个文件就是webpack的打包过程

在我们写的plugin上也打上断点debugger

compiler.hooks.emit.tapAsync('CopyrightWebpackPlugin',(compilation, cb) => {
    debugger
    ...
})

就可以开始调试了

调试的时候就可以看到compilation里面有哪些内容了
1.可以鼠标放在compilation变量上
2.也可以compilation加到调试台右边的watch里,就能展开看了

六、编写一个简易版webpack

写一个bundler,了解webpack在打包时都做了什么

a).准备工作

src/index.js

import message from './message.js'

console.log(message)

src/message.js

import {word} from './word.js'

const message = `say ${word}`

export default message

src/word.js

export const word = 'hello'

根目录下新建一个bundler.js文件,就是我们要写的打包文件

b).一些依赖包的安装

1.想要命令行中console出来的东西有颜色:安装cli-highlight

npm install cli-highlight -g

用法:
node bundler.js | highlight
(在原本的命令后加上 | highlight)
这样打印出的内容就有颜色标识

2.把源代码解析为抽象语法树ast
https://www.babeljs.cn/docs/babel-parser

npm i @babel/parser --save

3.遍历ast

npm install @babel/traverse --save

https://www.babeljs.cn/docs/babel-core

npm i @babel/core --save

5.es6转es5

npm i @babel/preset-env --save

c).编写bundler.js

1.对入口文件进行分析

首先对入口文件做一个分析:
bundler.js:

// 读取入口文件,分析入口文件里的代码

const fs = require('fs')
const path = require('path')
const paser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')

const moduleaAnalyser = (filename) => {
    const content = fs.readFileSync(filename, 'utf-8')
    const ast = paser.parse(content, {
        sourceType: 'module'
    }) // 抽象语法树 ast 是一个js对象
    const dependencies = {} // 入口文件的依赖
    // 遍历抽象语法树
    traverse(ast, {
        // 如果包含import引入语句,就会走这个函数
        ImportDeclaration({node}) { 
            // console.log(node) // 能分析出源代码中有多少依赖
            const dirname = path.dirname(filename) // 拿到filename的文件夹路径
            // console.log(dirname)
            const newFile = './' + path.join(dirname, node.source.value) // node.source.value为相对路径,这样可以拼成绝对路径
            dependencies[node.source.value] = newFile
            // console.log(dependencies) //{ './message.js': './src/message.js' } key为相对路径,value为绝对路径
        }
    })
    // 将ast转换为浏览器可以使用的代码
    const {code} = babel.transformFromAst(ast, null, {
        presets: ["@babel/preset-env"]// es6转为es5
    })
    // console.log(code)
    return {
        filename,
        dependencies,
        code
    }
    // console.log(dependencies)
    // console.log(ast.program.body)
}

const moduleInfo = moduleaAnalyser('./src/index.js')
console.log(moduleInfo)

执行node bundler.js | highlight ,会打印出来如下内容:

{ filename: './src/index.js',
  dependencies: { './message.js': './src/message.js' },
  code:
   '"use strict";\n\nvar _message = _interopRequireDefault(require("./message.js"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n\nconsole.log(_message["default"]);' }

filename:分析的模块路径(文件路径)
dependencies:该模块依赖的其他模块(文件路径)(key为相对路径,value为绝对路径,这样写是为了后面使用方便)
code:该模块转换后的代码

2.从入口文件开始,对所有其依赖的模块做相同的分析

// 读取入口文件,分析入口文件里的代码

const fs = require('fs')
const path = require('path')
const paser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')

// 对模块进行分析
const moduleaAnalyser = (filename) => {
    const content = fs.readFileSync(filename, 'utf-8')
    const ast = paser.parse(content, {
        sourceType: 'module'
    }) // 抽象语法树 ast 是一个js对象
    const dependencies = {} // 入口文件的依赖
    // 遍历抽象语法树
    traverse(ast, {
        // 如果包含import引入语句,就会走这个函数
        ImportDeclaration({node}) { 
            // console.log(node) // 能分析出源代码中有多少依赖
            const dirname = path.dirname(filename) // 拿到filename的文件夹路径
            // console.log(dirname)
            const newFile = './' + path.join(dirname, node.source.value) // node.source.value为相对路径,这样可以拼成绝对路径
            dependencies[node.source.value] = newFile
            // console.log(dependencies) //{ './message.js': './src/message.js' } key为相对路径,value为绝对路径
        }
    })
    // 将ast转换为浏览器可以使用的代码
    const {code} = babel.transformFromAst(ast, null, {
        presets: ["@babel/preset-env"]// es6转为es5
    })
    // console.log(code)
    return {
        filename,
        dependencies,
        code
    }
    // console.log(dependencies)
    // console.log(ast.program.body)
}

// 整个项目的依赖关系,依赖图谱
// entry 项目的入口文件
const makeDependenciesGraph = (entry) => {
    const entryModule = moduleaAnalyser(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(moduleaAnalyser(dependencies[j]))
            }
        }
    }
    console.log(graphArray)

    // 为了后面代码打包比较方便,对它做一个结构上的转换
    const graph = {}
    graphArray.forEach(item => {
        graph[item.filename] = {
            dependencies: item.dependencies,
            code: item.code
        }
    })
    //console.log(graph)
    return graph

}

// const moduleInfo = moduleaAnalyser('./src/index.js')
// console.log(moduleInfo)

const graphInfo = makeDependenciesGraph('./src/index.js')
console.log(graphInfo)

但是这里生成的代码还不能直接在浏览器上执行,因为浏览器上是没有require函数,exports对象的。
继续改造:

// 读取入口文件,分析入口文件里的代码

const fs = require('fs')
const path = require('path')
const paser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')

// 对模块进行分析
const moduleaAnalyser = (filename) => {
    const content = fs.readFileSync(filename, 'utf-8')
    const ast = paser.parse(content, {
        sourceType: 'module'
    }) // 抽象语法树 ast 是一个js对象
    const dependencies = {} // 入口文件的依赖
    // 遍历抽象语法树
    traverse(ast, {
        // 如果包含import引入语句,就会走这个函数
        ImportDeclaration({node}) { 
            // console.log(node) // 能分析出源代码中有多少依赖
            const dirname = path.dirname(filename) // 拿到filename的文件夹路径
            // console.log(dirname)
            const newFile = './' + path.join(dirname, node.source.value) // node.source.value为相对路径,这样可以拼成绝对路径
            dependencies[node.source.value] = newFile
            // console.log(dependencies) //{ './message.js': './src/message.js' } key为相对路径,value为绝对路径
        }
    })
    // 将ast转换为浏览器可以使用的代码
    const {code} = babel.transformFromAst(ast, null, {
        presets: ["@babel/preset-env"]// es6转为es5
    })
    // console.log(code)
    return {
        filename,
        dependencies,
        code
    }
    // console.log(dependencies)
    // console.log(ast.program.body)
}

// 整个项目的依赖关系,依赖图谱
// entry 项目的入口文件
const makeDependenciesGraph = (entry) => {
    const entryModule = moduleaAnalyser(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(moduleaAnalyser(dependencies[j]))
            }
        }
    }
    console.log(graphArray)

    // 为了后面代码打包比较方便,对它做一个结构上的转换
    const graph = {}
    graphArray.forEach(item => {
        graph[item.filename] = {
            dependencies: item.dependencies,
            code: item.code
        }
    })
    //console.log(graph)
    return graph

}

const generateCode = (entry) => {
    const graph = JSON.stringify(makeDependenciesGraph(entry))
    // 闭包 避免污染全局
    // 返回的是个字符串,最后在浏览器中运行
    // 浏览器上是没有require函数,exports对象的,所以代码直接放到浏览器上是不能直接运行的
    // 因此我们要构造这两个东西
    //  eval(code) // 执行代码
    // localRequire 相对路径转换绝对路径
    return `
        (function(graph){
            function require(module) {
                function localRequire(relativePath) {
                    return require(graph[module].dependencies[relativePath]);
                }
                var exports = {};
                (function(require, exports, code){
                    eval(code) 
                })(localRequire, exports, graph[module].code);
                return exports;
            };
            require('${entry}')
        })(${graph});
    `
}

// const moduleInfo = moduleaAnalyser('./src/index.js')
// console.log(moduleInfo)

const graphInfo = generateCode('./src/index.js')
console.log(graphInfo)

运行 node bundler.js | highlight 就会生成如下一段代码

        (function(graph){
            function require(module) {
                function localRequire(relativePath) {
                    return require(graph[module].dependencies[relativePath]);
                }
                var exports = {};
                (function(require, exports, code){
                    eval(code) 
                })(localRequire, exports, graph[module].code);
                return exports;
            };
            require('./src/index.js')
        })({"./src/index.js":{"dependencies":{"./message.js":"./src/message.js"},"code":"\"use strict\";\n\nvar _message = _interopRequireDefault(require(\"./message.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nconsole.log(_message[\"default\"]);"},"./src/message.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 = require(\"./word.js\");\n\nvar message = \"say \".concat(_word.word);\nvar _default = message;\nexports[\"default\"] = _default;"},"./src/word.js":{"dependencies":{},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n  value: true\n});\nexports.word = void 0;\nvar word = 'hello';\nexports.word = word;"}});

把这段代码复制到浏览器中,在console中执行,最后能输入say hello
这样,我们就把原本不能在浏览器端执行的代码转换为浏览器能执行的,webpack打包工具的原理也就是这样

七、生产环境和开发环境

生产环境和开发环境一般会配两个不同的webpack配置:
分开写配置文件就要涉及到使用命令执行不同的配置文件,我们可以使用npm的脚本命令,我们可以在package.json中找到scripts,添加如下命令

"build": "NODE_ENV=production webpack --config ./webpack.production.config.js --progress"
  • NODE_ENV=production 就是将运行环境设置成生产环境
  • webpack --config 就是运行webpack的配置文件
  • ./webpack.production.config.js 是要运行的指定位置的文件,这个路径是相对根目录来说的
  • --progress 是编译过程显示进程百分比的

开发环境一般不会配压缩代码、生成hash的插件,避免打包时间过长

推荐阅读更多精彩内容

  • 编写 Loader Loader就像是一个翻译员,能把源文件经过转化后输出新的结果,并且一个文件还可以链式的经过多...
    oWSQo阅读 5,730评论 0 6
  • webpack 核心 核心概述 entry 入口文件:js 代码文件,可执行的 node 模块或打包的入口文件。 ...
    coolheadedY阅读 2,790评论 1 6
  • Webpack原理与实践(一):打包流程 写在前面的话 在阅读 webpack4.x 源码的过程中,参考了《深入浅...
    DudleyLi阅读 321评论 0 2
  • Ba la la la ~ 读者朋友们,你们好啊,又到了冷锋时间,话不多说,发车! webpack是什么...
    冷锋_007阅读 360评论 0 9
  • 基本概念 本质上,webpack是JavaScript的静态模块打包工具。在webpack处理程序的时候,它会递归...
    古月丶阅读 312评论 0 1