明确webpack实现的性能

实质上,webpack 是一个古代 JavaScript 应用程序的动态模块打包器(module bundler)。 当 webpack 解决应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中蕴含应用程序须要的每个模块,而后将所有这些模块打包成一个或多个 bundle。

咱们的代码次要实现以下四步

graph TD    A(1.找到入口文件) --> B(2.解析入口文件并提取出依赖)    B --> C{3.递归发明出依赖图}    C --> D(4.把所有文件打包成一个文件)

开始开发

1.目录下新建三个js源文件

  • entry.js
import message from './message.js'console.log(message)
  • message.js
import name from "./name.js"export default `${name} is a girl`
  • name.js
export const name = 'Yolanda'

梳理下依赖关系,咱们的入口文件是entry.js,entry依赖message,message依赖name。

2.新建一个mywebpack.js文件,首先读取一下entry入口文件中的内容。
读取文件须要用到node的一个根底api-fs(文件系统),fs.readFileSync能够同步拿到js文件中的代码内容。

  • mywebpack.js
const fs = require('fs')function creatAsset (filename){    const content = fs.readFileSync(filename,'utf-8')    console.log(content)}creatAsset('./source/entry.js')

在当前目录下运行一下命令, 看一下输入
node mywebpack.js

输入内容为entry.js中的代码:

import message from './message.js';console.log(message);

3. 剖析ast,思考如何解析出entry.js文件中的依赖
能够应用ast工具 https://astexplorer.net/
看一下entry.js文件的ast是什么?

3.1 能够看到最下级是一个File, File中蕴含换一个program, 就是咱们的程序
3.2 在program的body属性里, 就是咱们各种语法的形容
3.3 能够看到第一个就是 ImportDeclaration, 也就是引入的申明.
3.4 ImportDeclaration里有一个source属性, 它的value就是引入的文件地址 './message.js'

4.生成entry.js的ast
首先装置一个Babylon(基于Babel的js解析工具)
npm i babylon

  • mywebpack.js
const fs = require('fs');const babylon = require('babylon');function createAsset(filename) {    const content = fs.readFileSync(filename, 'utf-8');    const ast = babylon.parse(content, {        sourceType: 'module'    });    console.log(ast);}createAsset('./source/entry.js');

能够看到输入了一个Object, 这就是咱们entry.js的AST.

5.基于这个ast,找到entry.js的ImportDeclaration中的source.value
首先,须要遍历出ImportDeclaration节点,那就须要一个工具:babel-traverse(能够像遍历对象一样遍历ast中的节点)
npm i babel-traverse
而后利用它遍历并获取到对应节点,提供一个函数来操作此节点。

  • mywebpack.js
const fs = require('fs');const babylon = require('babylon');const traverse = require('babel-traverse').default;function createAsset(filename) {    const content = fs.readFileSync(filename, 'utf-8');    const ast = babylon.parse(content, {        sourceType: 'module'    });    traverse(ast, {        ImportDeclaration: ({            node        }) => {            console.log(node)        }    })}createAsset('./source/entry.js');

6.获取entry.js的依赖
可能会呈现多个依赖,这里须要建一个数组存储。

  • mywebpack.js
const fs = require('fs');const babylon = require('babylon');const traverse = require('babel-traverse').default;function createAsset(filename) {    const content = fs.readFileSync(filename, 'utf-8');    const ast = babylon.parse(content, {        sourceType: 'module'    });    const dependencies = [];    traverse(ast, {        ImportDeclaration: ({            node        }) => {            dependencies.push(node.source.value);        }    })    console.log(dependencies);}createAsset('./source/entry.js');

能够自行打印一下dependencies数组看看.这里输入的是一个包含ImportDeclaration.source.value的数组。

7.优化creatAsset函数,减少id用于辨别文件

  • mywebpack.js
const fs = require('fs');const babylon = require('babylon');const traverse = require('babel-traverse').default;let ID = 0;function createAsset(filename) {    const content = fs.readFileSync(filename, 'utf-8');    const ast = babylon.parse(content, {        sourceType: 'module'    });    const dependencies = [];    traverse(ast, {        ImportDeclaration: ({            node        }) => {            dependencies.push(node.source.value);        }    })    const id = ID++;    return {        id,        filename,        dependencies    }}const mainAsset = createAsset('./source/entry.js');console.log(mainAsset);

运行一下看看, 是不是返回了正确的后果.

8.当初咱们有了单个文件的依赖了,接下来尝试建设模块间的依赖关系
新建一个createGraph函数,在这个函数里援用createAsset。
同时entry这个参数是动静的,所以createGraph接管entry这个参数。

  • mywebpack.js - createGraph
function createGraph(entry) {    const mainAsset = createAsset(entry);}const graph = createGraph('./source/entry.js');console.log(graph);

申明一个数组:allAsset用于存储全副的asset

  • mywebpack.js - createGraph
function createGraph(entry) {    const mainAsset = createAsset(entry);}const graph = createGraph('./source/entry.js');console.log(graph);

9.把相对路径转换为绝对路径
咱们在dependencies中存储的都是相对路径,然而咱们须要绝对路径能力拿到模块的Asset,这个时候要想方法拿到每个模块的绝对路径。

这里用到了node的另一个根底API:path,其中用到了它的两个办法:
1.path.dirname(获取以后文件的路径名)
2.path.join(拼接门路)
  • mywebpack.js - createGraph
function createGraph(entry) {    const mainAsset = createAsset(entry);    const allAsset = [mainAsset];    for (let asset of allAsset) {        const dirname = path.dirname(asset.filename);        asset.dependencies.forEach(relativePath => {            const absoultePath = path.join(dirname, relativePath);            const childAsset = createAsset(absoultePath);        });    }}

10.咱们还须要一个map,用于记录dependencies和childAsset之间的关系
map能够存储模块间的依赖关系,不便后续的查找。

  • mywebpack.js - createGraph
function createGraph(entry) {    const mainAsset = createAsset(entry);    const allAsset = [mainAsset];    for (let asset of allAsset) {        const dirname = path.dirname(asset.filename);        asset.mapping = {};        asset.dependencies.forEach(relativePath => {            const absoultePath = path.join(dirname, relativePath);            const childAsset = createAsset(absoultePath);            asset.mapping[relativePath] = childAsset.id;        });    }}

11.遍历所有文件,造成最终的依赖图
外围的一步,就一行代码,实现所有模块的遍历。

  • mywebpack.js - createGraph
function createGraph(entry) {    const mainAsset = createAsset(entry);    const allAsset = [mainAsset];    for (let asset of allAsset) {        const dirname = path.dirname(asset.filename);        asset.mapping = {};        asset.dependencies.forEach(relativePath => {            const absoultePath = path.join(dirname, relativePath);            const childAsset = createAsset(absoultePath);            asset.mapping[relativePath] = childAsset.id;            //⬇️要害一步,向数组中推动子依赖            allAsset.push(childAsset);        });    }    return allAsset;}

获取到了文件依赖图之后,咱们须要把所有文件打包成一个文件而后输入。

12.新增一个办法bundle
最初一个函数了!胜利在望~
bundle实现的是通过graph来发明一个立刻执行函数,立刻执行函数的参数有全副模块的代码体。

  • mywebpack.js - bundle
function bundle(graph) {    //自执行函数的参数    let modules = '';        //遍历所有模块,拼接modules字符串:每个id对应一个value,value里要包含该module的可执行代码    graph.forEach(module => {        modules += `${module.id}:[        ],`;    })    //返回的后果    const result = `        (function() {                    })({${modules}})    `;}

到当初咱们会发现,graph里只记录了每个模块对应的依赖关系,并没有包含可执行的代码体。
所以下一步,咱们须要获取每个模块的代码体。

批改createAsset函数,获取到代码体且编译。

首先咱们会用到babel-core,不便各个插件剖析语法进行相应的解决。

装置他!
npm i babel-core

还会用到 babel-preset-env,它能够依据开发者的配置,按需加载插件,能够作为预设来编译代码。

装置他!
npm i babel-preset-env

  • mywebpack.js - createAsset
function createAsset(filename) {    const content = fs.readFileSync(filename, 'utf-8');    const ast = babylon.parse(content, {        sourceType: 'module'    });    const dependencies = [];    traverse(ast, {        ImportDeclaration: ({            node        }) => {            dependencies.push(node.source.value);        }    })    const id = ID++;    const {        code    } = babel.transformFromAst(ast, null, {        // 第三个参数, 通知babel以什么形式编译咱们的代码. 这里咱们就用官网提供的preset-env, 编译es2015+的js代码.        // 当然还有其余的各种预设, 能够编译ts, react等等代码.        presets: ['env']    })    return {        id,        filename,        dependencies,        code    }}

13.把获取到的code放到result中
依据commonJs的标准,每个模块的代码函数须要接管三个参数:require,module,exports。

  • module变量代表以后的模块,它是一个变量,其中exports属性就是对外的模块。加载某个模块,其实就是加载该模块的module.exports属性。
  • mywebpack.js - bundle

    function bundle(graph) {  let modules = '';  graph.forEach(module => {      modules += `${module.id}:[          function(require, module, exports) {              ${module.code}          },      ],`;  })  const result = `      (function() {                })({${modules}})  `;  return result;}

接下来开始实现require函数,首先须要把mapping传到对应modules的value外面。

function bundle(graph) {    let modules = '';    graph.forEach(module => {        modules += `${module.id}:[            function(require, module, exports) {                ${module.code}            },            ${JSON.stringify(module.mapping)},        ],`;    })    const result = `        (function() {                    })({${modules}})    `;    return result;}

requier要接管一个参数,来示意引入了哪些代码,这个时候就能够用mapping的映射关系,应用id找到引入的代码。

function bundle(graph) {    let modules = '';    graph.forEach(module => {        modules += `${module.id}:[            function(require, module, exports) {                ${module.code}            },            ${JSON.stringify(module.mapping)},        ],`;    })    // 取出的fn是下面定义的modules里的function,mapping是${JSON.stringify(module.mapping)}    const result = `        (function(modules) {            function require(id) {                const [fn, mapping] = modules[id];                function localRequire(relativePath) {                    return require(mapping[relativePath]);                }                const module = { exports: {}};                fn(localRequire, module, module.exports);                return module.exports;            }            require(0);        })({${modules}})    `;    return result;}

当当当!到这里就根本实现了咱们的简易版webpack,这个时候能够node运行下mywebpack.js,把输入的后果代码复制到浏览器里看看能不能失常运行。
浏览器打印出:Yolanda is a girl 就意味着你的webpack曾经失常运行起来了!给本人鼓个掌吧~

最初能够优化一下流程,每次都复制到浏览器看输入后果有点吃力,能够配置下package.json用命令行操作。
npm init

  • package.json
"scripts": {    "build": "rm -rf dist.js && node mywebpack.js > dist.js"},

最初运行

npm run build.

功败垂成!

贴上mywebpack.js残缺代码:

// 步骤:// 1.找到一个入口文件;// 2.解析这个入口文件,提起入口文件的依赖;// 3.解析入口文件中依赖的依赖,递归的发明一个依赖图,能够形容文件间的依赖关系;// 4.将所有的文件打包成一个文件。//node外面的fs模块能够读取文件const fs = require('fs')//node的根底api 能够帮忙拿到绝对路径const path = require('path')//babel(语法转换)的解析工具const babylon = require('babylon')//babel-traverse :能够像遍历对象一样遍历ast中的节点const tranverse = require('babel-traverse').default;//babel-core:把js文件解析为astconst babel = require('babel-core')//记录IDlet ID = 0;//拿到该file的id,filename 以及 依赖的数组function createAsset(filename){    //同步的读取文件    const content =fs.readFileSync(filename,'utf-8')    //转化为ast找到相应节点    const ast = babylon.parse(content,{        sourceType:'module'    })    //定义一个数组用于贮存依赖    const dependencies = []    //定义一个id 每调用一次creatAsset函数id++    let id = ID++    //遍历拿到ImportDeclaration中的source中的value    tranverse(ast,{        ImportDeclaration:({node})=>{            //拿到node中的source的value push到贮存依赖的数组里            dependencies.push(node.source.value)        }    })    const{ code } = babel.transformFromAst(ast , null,{        presets:['env']    })    //输入一个对象,其中包含id,filename以及它所依赖的dependencies    return {        id,        filename,        dependencies,        code    }}//创立依赖图 外围!!function createGraph(entry){    const mainAsset = createAsset(entry)    const allAsset = [mainAsset]    for( let asset of allAsset){        //拿到以后的dirname        const dirname= path.dirname(asset.filename)        //给asset新增一个mapping对象,不便后续查找        asset.mapping = {}        //将dependencies中的相对路径转化为绝对路径        asset.dependencies.forEach(relativePath=>{            const absolutePath = path.join(dirname,relativePath)            //拿到绝对路径下的子元素的asset内容            const childAsset = createAsset(absolutePath)            //存储对应关系            asset.mapping[relativePath]=childAsset.id            //递归的外围一行代码!!            allAsset.push(childAsset)        })    }    return allAsset}//创立整体后果代码块(须要立刻执行)接管module作为参数function bundle (graph){    let modules = ''    graph.forEach(module=>{        modules+=`        ${module.id}:[            function(require,module,exports){                ${module.code}            },            ${JSON.stringify(module.mapping)}        ],        `    })    //输入的后果为字符串    const result = `(    function(modules){         function require(id){             const [fn,mapping] = modules[id];             function localRequire(relativePath){                 return require(mapping[relativePath])             }             const module = { exports:{}}             fn(localRequire,module,module.exports)                          return module.exports         }         require(0)    }    )({${modules}})`    return result}//传入相对路径const graph = createGraph('./source/entry.js')const res = bundle(graph)console.log(res)