一 目录

不折腾的前端,和咸鱼有什么区别

目录
一 目录
二 前言
三 第一步 转换代码、生成依赖
四 第二步 生成依赖图谱
五 第三步 生成代码字符串

二 前言

返回目录

参考文章:实现一个简略的Webpack

Webpack 的实质就是一个模块打包器,工作就是将每个模块打包成相应的 bundle

首先,咱们须要筹备目录:

+ 我的项目根门路 || 文件夹  - index.js      - 主入口  - message.js    - 主入口依赖文件  - word.js       - 主入口依赖文件的依赖文件  - bundler.js    - 打包器  - bundle.js     - 打包后寄存代码的文件

最终的我的项目地址:all-for-one - 031-手写 Webpack

如果小伙伴懒得敲,那能够看下面仓库的最终代码。

而后,咱们 index.jsmessage.jsword.js 内容如下:

index.js
// index.jsimport message from "./message.js";console.log(message);
message.js
// message.jsimport { word } from "./word.js";const message = `say ${word}`;export default message;
word.js
// word.jsexport const word = "hello";

最初,咱们实现一个 bundler.js 文件,将 index.js 当成入口,将外面牵扯的文件都本义并执行即可!

实现思路:

  1. 利用 babel 实现代码转换,并生成单个文件的依赖
  2. 生成依赖图谱
  3. 生成最初打包代码

上面分 3 章尝试这个内容。

三 第一步 转换代码、生成依赖

返回目录

这一步须要利用 babel 帮忙咱们进行转换,所以先装包:

npm i @babel/parser @babel/traverse @babel/core @babel/preset-env -D

转换代码须要:

  1. 利用 @babel/parser 生成 AST 形象语法树
  2. 利用 @babel/traverse 进行 AST 遍历,记录依赖关系
  3. 通过 @babel/core@babel/preset-env 进行代码的转换

而后增加内容:

bundler.js
const fs = require("fs");const path = require("path");const parser = require("@babel/parser");const traverse = require("@babel/traverse").default;const babel = require("@babel/core");// 第一步:转换代码、生成依赖function stepOne(filename) {  // 读入文件  const content = fs.readFileSync(filename, "utf-8");  const ast = parser.parse(content, {    sourceType: "module", // babel 官网规定必须加这个参数,不然无奈辨认 ES Module  });  const dependencies = {};  // 遍历 AST 形象语法树  traverse(ast, {    // 获取通过 import 引入的模块    ImportDeclaration({ node }) {      const dirname = path.dirname(filename);      const newFile = "./" + path.join(dirname, node.source.value);      // 保留所依赖的模块      dependencies[node.source.value] = newFile;    },  });  //通过 @babel/core 和 @babel/preset-env 进行代码的转换  const { code } = babel.transformFromAst(ast, null, {    presets: ["@babel/preset-env"],  });  return {    filename, // 该文件名    dependencies, // 该文件所依赖的模块汇合(键值对存储)    code, // 转换后的代码  };}console.log('--- step one ---');const one = stepOne('./index.js');console.log(one);fs.writeFile('bundle.js', one.code, () => {  console.log('写入胜利');});

通过 Node 的形式运行这段代码:node bundler.js

--- step one ---{  filename: './index.js',  dependencies: { './message.js': './message.js' },  code:`    "use strict";    var _message = _interopRequireDefault(require("./message.js"));    function _interopRequireDefault(obj) {      return obj && obj.__esModule ? obj : { "default": obj };    }    // index.js    console.log(_message["default"]);`,}
  1. 入口 filenameindex.js
  2. 依赖 message.js
  3. 本义代码 code

所以 jsliangcode 提取到 bundle.js 中进行查看:

bundler.js
// ...代码省略fs.writeFile('bundle.js', one.code, () => {  console.log('写入胜利');});
bundle.js
"use strict";var _message = _interopRequireDefault(require("./message.js"));function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }// index.jsconsole.log(_message["default"]);

解读下这个文件内容:

  • use strict:应用严格模式
  • _interopRequireDefault:对不合乎 babel 规范的模块增加 default 属性,并指向本身对象以防止 exports.default 出错

所以当初这份文件的内容是能够运行的了,然而你运行的时候会报错,报错内容如下:

import { word } from "./word.js";       ^SyntaxError: Unexpected token {

也就是说咱们执行到 message.js,然而它外面的内容没法运行,因为 importES6 内容嘛。

咋整,持续看上面内容。

四 第二步 生成依赖图谱

返回目录

既然咱们只生成了一份本义后的文件:

--- step one ---{  filename: './index.js',  dependencies: { './message.js': './message.js' },  code:`    "use strict";    var _message = _interopRequireDefault(require("./message.js"));    function _interopRequireDefault(obj) {      return obj && obj.__esModule ? obj : { "default": obj };    }    // index.js    console.log(_message["default"]);`,}

那么咱们能够依据其中的 dependencies 进行递归,将整个依赖图谱都找进去:

bundler.js
// ...省略后面内容// 第二步:生成依赖图谱// entry 为入口文件function stepTwo(entry) {  const entryModule = stepOne(entry);  // 这个数组是外围,尽管当初只有一个元素,往后看你就会明确  const graphArray = [entryModule];  for (let i = 0; i < graphArray.length; i++) {    const item = graphArray[i];    const { dependencies } = item; // 拿到文件所依赖的模块汇合(键值对存储)    for (let j in dependencies) {      graphArray.push(stepOne(dependencies[j])); // 敲黑板!要害代码,目标是将入口模块及其所有相干的模块放入数组    }  }  // 接下来生成图谱  const graph = {};  graphArray.forEach((item) => {    graph[item.filename] = {      dependencies: item.dependencies,      code: item.code,    };  });  return graph;}console.log('--- step two ---');const two = stepTwo('./index.js');console.log(two);let word = '';for (let i in two) {  word = word + two[i].code + '\n\n';}fs.writeFile('bundle.js', word, () => {  console.log('写入胜利');});

所以当咱们 node bundler.js 的时候,会打印内容进去:

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

能够看到咱们将整个依赖关系中的文件都搜寻进去,并通过 babel 进行了转换,而后 jsliang 通过 Nodefs 模块将其写进了 bundle.js 中:

bundler.js
let word = '';for (let i in two) {  word = word + two[i].code + '\n\n';}fs.writeFile('bundle.js', word, () => {  console.log('写入胜利');});

再来看 bundle.js 内容:

bundle.js
"use strict";var _message = _interopRequireDefault(require("./message.js"));function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }// index.jsconsole.log(_message["default"]);"use strict";Object.defineProperty(exports, "__esModule", {  value: true});exports["default"] = void 0;var _word = require("./word.js");// message.jsvar message = "say ".concat(_word.word);var _default = message;exports["default"] = _default;"use strict";Object.defineProperty(exports, "__esModule", {  value: true});exports.word = void 0;// word.jsvar word = "hello";exports.word = word;

跟步骤一的解析差不多,不过这样子的内容是没法运行的,毕竟咱们塞到同一个文件中了,所以须要步骤三咯。

五 第三步 生成代码字符串

返回目录

最初一步咱们实现上面代码:

bundler.js
// 上面是生成代码字符串的操作function stepThree(entry){  // 要先把对象转换为字符串,不然在上面的模板字符串中会默认调取对象的 toString 办法,参数变成 [Object object],显然不行  const graph = JSON.stringify(stepTwo(entry))  return `(function(graph) {  // require 函数的实质是执行一个模块的代码,而后将相应变量挂载到 exports 对象上  function require(module) {    // localRequire 的实质是拿到依赖包的 exports 变量    function localRequire(relativePath) {      return require(graph[module].dependencies[relativePath]);    }    var exports = {};    (function(require, exports, code) {      eval(code);    })(localRequire, exports, graph[module].code);    return exports; // 函数返回指向局部变量,造成闭包,exports 变量在函数执行后不会被捣毁  }  require('${entry}')})(${graph})`;};console.log('--- step three ---');const three = stepThree('./index.js');console.log(three);fs.writeFile('bundle.js', three, () => {  console.log('写入胜利');});

能够看到,stepThree 返回的是一个立刻执行函数,须要传递 graph

(function(graph) {  // 具体内容})(graph)

那么图谱(graph)怎么来?须要通过 stepTwo(entry) 拿到了依赖图谱。

然而,因为步骤二返回的是对象啊,如果间接传进去对象,那么就会被本义,所以须要 JSON.stringify()

const graph = JSON.stringify(stepTwo(entry));(function(graph) {  // 具体内容})(graph)

那为什么这个函数(stepThree)须要传递 entry?起因在于咱们须要一个主入口,就好比 Webpack 单入口模式:

转变前后
// 转变前const graph = JSON.stringify(stepTwo(entry));(function(graph) {  function require(module) {    // ...具体内容  }  require('${entry}')})(graph)/* --- 分界线 --- */// 转变后const graph = JSON.stringify(stepTwo(entry));(function(graph) {  function require(module) {    // ...具体内容  }  require('./index.js')})(graph)

这样咱们就分明了,从 index.js 动手,而后再看外面具体内容:

function require(module) {  // localRequire 的实质是拿到依赖包的 exports 变量  function localRequire(relativePath) {    return require(graph[module].dependencies[relativePath]);  }  var exports = {};  (function(require, exports, code) {    eval(code);  })(localRequire, exports, graph[module].code);  return exports; // 函数返回指向局部变量,造成闭包,exports 变量在函数执行后不会被捣毁}require('./index.js')

eval 是指 JavaScript 能够运行外面的字符串代码,eval('2 + 2') 会进去后果 4,所以 eval(code) 就跟咱们第一步的时候,node bundle.js 一样,执行 code 外面的代码。

所以咱们执行 require(module) 外面的代码,先走:

(function(require, exports, code) {  eval(code);})(localRequire, exports, graph[module].code);

此刻这个代码中,传递的参数有 3 个:

  • require:如果在 eval(code) 执行代码期间,碰到 require 就调用 localRequire 办法
  • exports:如果在 eval(code) 执行代码期间,碰到 exports 就将外面内容设置到对象 exports
  • graph[module].code:一开始 module'./index.js',所以查找 graph'./index.js' 对应的 code,将其传递进 eval(code) 外面

有的小伙伴会好奇这代码怎么走的,咱们能够先看上面一段代码:

const localRequire = (abc) => {  console.log(abc);};const code = `  console.log(456);  doRequire(123)`;(function(doRequire, code) {  eval(code);})(localRequire, code);

这段代码中,执行的 doRequire 其实就是传入进来的 localRequire 办法,最终输入 456123

当初,再回头来看:

区块一:bundle.js
function require(module) {  // localRequire 的实质是拿到依赖包的 exports 变量  function localRequire(relativePath) {    return require(graph[module].dependencies[relativePath]);  }  var exports = {};  (function (require, exports, code) {    eval(code);  })(localRequire, exports, graph[module].code);  return exports; // 函数返回指向局部变量,造成闭包,exports 变量在函数执行后不会被捣毁}require("./index.js");

它先执行 立刻执行函数 (function (require, exports, code) {})(),再到 eval(code),从而执行上面代码:

区块二:graph['./index.js'].code
"use strict";var _message = _interopRequireDefault(require("./message.js"));function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }// index.jsconsole.log(_message["default"]);

在碰到 require("./message.js") 的时候,持续进去下面【区块一】的代码,因为此刻的 require 是:

function localRequire(relativePath) {  return require(graph[module].dependencies[relativePath]);}

所以咱们再调用本人的 require() 办法,将内容传递进去,变成:require('./message.js')

……以此类推,直到 './word.js' 外面没有 require() 办法体了,咱们再执行上面内容,将 exports 导出去。

这就是这段内容的运行流程。

至于其中细节咱们就不一一赘述了,小伙伴们如果还没看懂能够自行断点调试,这外面的代码口头形容的话 jsliang 讲得不是分明。

最初咱们看看输入整顿后的 bundle.js

bundle.js
(function (graph) {  // require 函数的实质是执行一个模块的代码,而后将相应变量挂载到 exports 对象上  function require(module) {    // localRequire 的实质是拿到依赖包的 exports 变量    function localRequire(relativePath) {      return require(graph[module].dependencies[relativePath]);    }    var exports = {};    (function (require, exports, code) {      eval(code);    })(localRequire, exports, graph[module].code);    return exports; // 函数返回指向局部变量,造成闭包,exports 变量在函数执行后不会被捣毁  }  require("./index.js");})({  "./index.js": {    dependencies: { "./message.js": "./message.js" },    code: `      "use strict";            var _message = _interopRequireDefault(require("./message.js"));            function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }            // index.js            console.log(_message["default"]);    `,  },  "./message.js": {    dependencies: { "./word.js": "./word.js" },    code: `      "use strict";            Object.defineProperty(exports, "__esModule", {        value: true      });            exports["default"] = void 0;            var _word = require("./word.js");            // message.js            var message = "say ".concat(_word.word);            var _default = message;            exports["default"] = _default;    `,  },  "./word.js": {    dependencies: {},    code: `      "use strict";            Object.defineProperty(exports, "__esModule", {        value: true      });            exports.word = void 0;            // word.js            var word = "hello";      exports.word = word;',  },});

此时咱们 node bundle.js,就能够获取到:

say hello

这样咱们就手撸实现了单入口的 Webpack 简略实现。


jsliang 的文档库由 梁峻荣 采纳 常识共享 署名-非商业性应用-雷同形式共享 4.0 国内 许可协定 进行许可。<br/>基于 https://github.com/LiangJunrong/document-library 上的作品创作。<br/>本许可协定受权之外的应用权限能够从 https://creativecommons.org/licenses/by-nc-sa/2.5/cn/ 处取得。