内容简介:我们需要借助最终的文件目录结构文件内容
我们需要借助 babel
来解析,先 npm init -y
npm i @babel/parser @babel/traverse @babel/core @babel/preset-env -D
最终的文件目录结构
|-- dist // 打包目标文件夹 | |-- 0.bundle.js | |-- 1.bundle.js | |-- result.js |-- src // 项目测试代码 | |-- entry.js | |-- messgae.js | |-- name.js | |-- a.js | |-- b.js |-- index.html // 加载文件打包出的文件 |-- app.js // 启动文件 |-- init.js // 打包项目需要的初始化代码 |-- babel-plugin.js // babel插件 |-- loader.js // loader |-- package.json
文件内容
entry.js
import message from "./message.js"; console.log(message); import("./a.js").then(() => { console.log("a done"); });
message.js
import { name } from "./name.js"; export default `hello ${name}!`; import("./a.js").then(() => { console.log("copy a done"); });
name.js
export const name = "world"; import("./b.js").then(() => { console.log("b done"); });
a.js
console.log("import a"); setTimeout(() => { document.body.style = "background:red;"; }, 3000);
b.js
console.log("import b");
编写
我在之前写的 webpack系列之输出文件分析
文章说过, webpack
打包出来的代码大致的样子是:point_down:
(function(modules) { function __webpack_require__(moduleId) { ... } ... return __webpack_require__(__webpack_require__.s = "./src/main.js"); })({ "./src/a.js": (function(module, __webpack_exports__, __webpack_require__) {} "./src/b.js": (function(module, __webpack_exports__, __webpack_require__) {} "./src/main.js": (function(module, __webpack_exports__, __webpack_require__) {} })
借鉴他的思路,我们也可以很快写出来一个简单的 webpack
,首先 (function(modules) {...})
内部的代码基本上可以写死,也就是我们等会需要写的 init.js
,接着看,这是一个自执行的函数, 传入的是一个对象,首先执行的是主入口的文件,然后再分别去找他们的依赖去执行相应的文件。
热身
我们这里借助bable来编译代码
先简单看一下:point_down:这个示例
const fs = require("fs"); const path = require("path"); const parser = require("@babel/parser"); const traverse = require("@babel/traverse").default; const babel = require("@babel/core"); let id = 0; const resolve = function(filename) { let content = ""; content = fs.readFileSync(path.resolve(__dirname, filename), "utf-8"); // 转ast树 const ast = parser.parse(content, { sourceType: "module", }); // 依赖 const dependencies = []; traverse(ast, { ImportDeclaration({ node }) { // import '' from '' dependencies.push(node.source.value); }, }); // ES6转成ES5 const { code } = babel.transformFromAstSync(ast, null, { presets: ["@babel/preset-env"], }); return { id: id++, dependencies, filename, code, }; }; const result = resolve("./src/entry.js"); console.log(result);
打印结果
{ id: 0, dependencies: [ './message.js' ], filename: './src/entry.js', code: '"use strict";\n\nvar _message = _interopRequireDefault(require( ....." }
我们这里解析了一个入口文件,然后通过 babel
转成 ast
, ImportDeclaration
拦截到 import
,将它添加到 dependencies
依赖内,处理完 import
后把代码转成 es5
,最后输出对象,包含当前的文件的 id
,依赖关系,文件名,以及编译后的源代码。这段代码是整篇的精髓,不过现在只处理了一个文件,我们刚刚找到了当前文件的依赖,接着需要递归查找下一个文件的依赖关系,最后把他们组合起来,跟之前看 webpack
输出的文件思想差不多。
递归查找所有依赖
在下面添加以下代码:point_down:,顺便删除最后两行 const result = resolve("./src/entry.js"); console.log(result);
const start = function(filename) { const entry = resolve(filename); const queue = [entry]; for (const asset of queue) { const dependencies = asset.dependencies; const dirname = path.dirname(asset.filename); asset.mapping = {}; dependencies.forEach((val) => { const result = resolve(path.join(dirname, val)); asset.mapping[val] = result.id; queue.push(result); }); } return queue; }; const fileDependenceList = start("./src/entry.js"); console.log(fileDependenceList);
执行后结果,我们捋一捋
入口entry.js
import :point_right: message.js
message.js
import :point_right: name.js
name.js
没有 import
别的文件所以依赖是空的
[ { id: 0, dependencies: [ './message.js' ], filename: './src/entry.js', code: '"use strict";\n\nvar _message = _interopRequireDefault(require( ....."' }, { id: 1, dependencies: [ './name.js' ], filename: 'src/message.js', code: '"..."' }, { id: 2, dependencies: [], filename: 'src/name.js', code: '"..."' }, ]
结果我们得到了,目前还不是之前想要的那个结构,继续添加以下代码
let moduleStr = ""; fileDependenceList.forEach((value) => { moduleStr += `${value.id}:[ function(require, module, exports) { ${value.code}; }, ${JSON.stringify(value.mapping)} ],`; }); const result = `(${fs.readFileSync("./init.js", "utf-8")})({${moduleStr}})`; fs.writeFileSync("./dist/result.js", result); // 注意这里需要有dist文件夹
这里把 init.js
引入了,内容如下
function init(modules) { function require(id) { var [fn, mapping] = modules[id]; function localRequire(relativePath) { return require(mapping[relativePath]); } var module = { exports: {} }; fn(localRequire, module, module.exports); return module.exports; } //执行入口文件, return require(0); }
执行之后在 dist/
下有一个 result
文件,我们放到浏览器去执行, index.html
加载
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>webpack</title> </head> <body> <script src="./dist/result.js"></script> </body> </html>
不出意外控制台输出 hello world
,接着会有三个报错,没错,因为我们没有处理 import().then()
这种代码,这个需要单独处理,如果你想把错误去掉,去 src
文件夹把 import()
都注释就完事了。
你去看 result
的代码内容,会发现代码我们首先执行 require(0)
,从入口触发,然后递归调用 require
来完成整个流程,看我们之前 moduleStr
输出的代码,结构跟 webpack
输入的有点区别,思路差不多
{ 0: [ function(require, module, exports) { var _message = _interopRequireDefault(require("./message.js")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } console.log(_message["default"]); }, { "./message.js": 1 }, ], 1: [function(require, module, exports) { ... }, { "./name.js": 2 }], 2: [function(require, module, exports) { ... }, {}], }
我们 require
都是当前文件的 id
,但是我们看内部有一段 require("./message.js")
,其实它执行的是 localRequire
方法,通过当前文件数组的第二个值 { "./message.js": 1 }
来定位它要执行的 id
是什么,这里的 id
是1,下面就是它的逻辑,通过文件名 filename
,去查找 mapping
对应的 id
。
var [fn, mapping] = modules[id]; function localRequire(relativePath) { return require(mapping[relativePath]); }
支持 import() 异步加载
首先先来解释以下如何异步加载,我们需要先生成 0.bundle.js
1.bundle.js
这样的文件,然后通过 document.createElement("script")
把它 push
到页面的 head
内完成加载。
修改 babel
部分
.... + let bundleId = 0; + const installedChunks = {}; const resolve = function(filename) { let content = ""; content = fs.readFileSync(path.resolve(__dirname, filename), "utf-8"); const ast = parser.parse(content, { sourceType: "module", }); const dependencies = []; traverse(ast, { ImportDeclaration({ node }) { // import '' from '' dependencies.push(node.source.value); }, + CallExpression({ node }) { + // import() + if (node.callee.type === "Import") { + const realPath = path.join( + path.dirname(filename), + node.arguments[0].value + ); + if (installedChunks[realPath] !== undefined) return; + let sourse = fs.readFileSync(realPath, "utf-8"); + sourse = `jsonp.load([${bundleId}, function(){${sourse}}])`; + fs.writeFileSync(`./dist/${bundleId}.bundle.js`, sourse); + installedChunks[realPath] = bundleId; + bundleId++; + process.installedChunks = { + nowPath: path.dirname(filename), + ...installedChunks, + }; + } + }, }); // ES6转成ES5 const { code } = babel.transformFromAstSync(ast, null, { + plugins: ["./babel-plugin.js"], presets: ["@babel/preset-env"], }); return { id: id++, dependencies, filename, code, }; }; ...
我们看到上面我们新增使用 babel
插件 plugins: ["./babel-plugin.js"]
,不懂的可以看 babel-handbook
babel-plugin.js
const nodePath = require("path"); module.exports = function({ types: t }) { return { visitor: { CallExpression(path) { if (path.node.callee.type === "Import") { path.replaceWith( t.callExpression( t.memberExpression( t.identifier("require"), t.identifier("import") ), [ t.numericLiteral( process.installedChunks[ nodePath.join( process.installedChunks["nowPath"], path.node.arguments[0].value ) ] ), ] ) ); } }, }, }; };
上面插件的功能就是把 import('./a.js')
转成 require.import(0)
修改 init.js
,主要是新增 import
方法,借鉴自 webpack
function init(modules) { function require(id) { var [fn, mapping] = modules[id]; function localRequire(relativePath) { return require(mapping[relativePath]); } var module = { exports: {} }; localRequire.import = require.import; // 新增 fn(localRequire, module, module.exports); return module.exports; } var installedChunks = {}; // 当前新增 require.import = function(chunkId) { // 当前新增 var promises = []; var installedChunkData = installedChunks[chunkId]; // 如果没有加载 if (installedChunkData !== 0) { if (installedChunkData) { promises.push(installedChunkData[2]); } else { var promise = new Promise(function(resolve, reject) { installedChunkData = installedChunks[chunkId] = [resolve, reject]; }); promises.push((installedChunkData[2] = promise)); // start chunk loading var script = document.createElement("script"); var onScriptComplete; script.charset = "utf-8"; script.src = "dist/" + chunkId + ".bundle.js"; var error = new Error(); onScriptComplete = function(event) { // avoid mem leaks in IE. script.onerror = script.onload = null; clearTimeout(timeout); var chunk = installedChunks[chunkId]; if (chunk !== 0) { if (chunk) { var errorType = event && (event.type === "load" ? "missing" : event.type); var realSrc = event && event.target && event.target.src; error.message = "Loading chunk " + chunkId + " failed.\n(" + errorType + ": " + realSrc + ")"; error.name = "ChunkLoadError"; error.type = errorType; error.request = realSrc; chunk[1](error); } installedChunks[chunkId] = undefined; } }; var timeout = setTimeout(function() { onScriptComplete({ type: "timeout", target: script }); }, 120000); script.onerror = script.onload = onScriptComplete; document.head.appendChild(script); } } return Promise.all(promises); }; window.jsonp = {}; // 当前新增 jsonp.load = function(bundle) { // 当前新增 var chunkId = bundle[0]; var fn = bundle[1]; var resolve = installedChunks[chunkId][0]; installedChunks[chunkId] = 0; // 执行异步加载文件代码 fn(); // 执行resolve resolve(); }; //执行入口文件, return require(0); }
我们异步加载的文件都会执行 jsonp.load
方法,,在生成文件 *.bunnd.js
之前都会把代码改装一下,得到下面的结构,这样就可以控制执行源代码及 .then() .catch()
等操作了
jsonp.load([ 0, function() { // 原文件代码 }, ]);
然后执行,你会发现 dist
多了两个文件, 0.bundle.js
1.bundle.js
,前提是你没有注释之前 import()
写的代码,然后去浏览器控制台查看,分别打印以下,接着3秒后页面背景变为红色
hello world! import b b done import a copy a done a done
等等,我们使用了三个 import
,为什么只有两个文件,因为有一个 import('./a.js')
使用了两次,这里我做了缓存,所以重复异步引入的文件会缓存利用
支持loader
loader
支持很简单,其实就是把文件的内容交给它单独处理返回新的结果,我们新建文件 loader.js
,内容如下:
module.exports = function(content) { return content + "; console.log('loader')"; };
在每个js文件后都加上打印loader的代码
接着修改resolve方法内的代码
+ const loader = require("./loader"); const resolve = function(filename) { let content = ""; content = fs.readFileSync(path.resolve(__dirname, filename), "utf-8"); + content = loader(content); const ast = parser.parse(content, { sourceType: "module", }); .... }
然后运行代码,浏览器控制台会打印是三个 loader
最后
至此,我们完成了 esModule
的支持,文件异步加载的支持、 loader
的支持,我们顺便还写了一个 babel
插件,整个流程没有什么难理解的地方,一个 webpack
就这样完成了,当然还可以再把功能完善。支持插件?把 tapable
加入?等等,时间有限,点到为止,如有错误还望指正
本章代码部分借鉴 webpack
输出的 bundle
及 You Gotta Love Frontend
的视频 Ronen Amiel - Build Your Own Webpack
代码已上传至 GitHub
: https://github.com/wclimb/my-webpack
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 基于顺丰同城接口编写sdk,java三方sdk编写思路
- 使用 Clojure 编写 OpenWhisk 操作,第 1 部分: 使用 Lisp 方言为 OpenWhisk 编写简明的代码
- 编写一个Locust文件
- 编写现代 JavaScript 代码
- 性能测试报告编写技巧
- 为vscode编写扩展
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Visual LISP程序设计
李学志 / 清华大学 / 2006-5 / 29.00元
本书系统地介绍了AutoCAD最新版本(2006)的Visual LISP程序设计技术。全书共分13章。前3章介绍AutoLISP语言的基础知识,第4章介绍Visual LISP的开发环境,第5~7章介绍程序的编辑、调试和设计的方法与技巧,第8章介绍如何定义新的AutoCAD命令及创建图层、线型、文字样式、剖面线、尺寸标注等各种AutoCAD对象,以及如何实现参数化图形设计的方法和技术,第9章介绍......一起来看看 《Visual LISP程序设计》 这本书的介绍吧!