不想成为配置工程师的前端不是好架构,哈哈
我已经记不清这是第几次刷webpack的文档了,看了忘,忘了看,再看再忘…
这都不是重点。webpack官网文档中给我们推荐了https://github.com/ronami/minipack,可以一看。挺有意思的。
我用我蹩脚的英文对部分注释进行了翻译
我尝试总结一下思路吧:
从入口文件开始,使用babylon分析js代码,找到import语句,以此来找到依赖文件。然后又对依赖文件如此操作,构建出整个的依赖图(createGraph函数)。最后通过依赖图生成目标文件(bundle函数)。
AST开启了另一扇门,有时间要了解一下.
有用点赞哦:)
/**
* Module bundlers compile small pieces of code into something larger and more
* complex that can run in a web browser. These small pieces are just JavaScript
* files, and dependencies between them are expressed by a module system
* (https://webpack.js.org/concepts/modules).
*
* 模块打包器编译小片段代码到更到的更复杂的代码中,它们可以运行在浏览器中。这些小片段只是
* JavaScript文件,它们之间的依赖用模块系统表示。
*
* Module bundlers have this concept of an entry file. Instead of adding a few
* script tags in the browser and letting them run, we let the bundler know
* which file is the main file of our application. This is the file that should
* bootstrap our entire application.
*
* 模块打包器有入口文件的入口。我们让打包器知道哪一个文件是我们程序的主文件,而不是在
* 浏览器中添加script标签,让它们运行。这是启动我们整个应用的文件。
*
* Our bundler will start from that entry file, and it will try to understand
* which files it depends on. Then, it will try to understand which files its
* dependencies depend on. It will keep doing that until it figures out about
* every module in our application, and how they depend on one another.
*
* 我们的打包器会从入口文件开始,它会尝试理解这个文件依赖的文件。然后,它会尝试理解它
* 的依赖依赖的文件。它一直做这件事直到指出了每一个模块,和它们怎么依赖的。
*
* This understanding of a project is called the dependency graph.
*
* 项目的理解称为依赖图。
*
* In this example, we will create a dependency graph and use it to package
* all of its modules in one bundle.
*
* 这个例子中,我们创建一个依赖图,然后使用它把所有的模块打包到一个文件中。
*
* Let's begin :)
* 让我们开始:)
*
* Please note: This is a very simplified example. Handling cases such as
* circular dependencies, caching module exports, parsing each module just once
* and others are skipped to make this example as simple as possible.
*
* 注意:这是一个很简单的例子。处理循环依赖、缓存模块导出和解析每一个模块一次,别的都跳过了,
* 使得这个例子尽可能简单。
*/
const fs = require('fs'); // 文件系统模块
const path = require('path'); // 路径模块
const babylon = require('babylon'); // JavaScript解析器
const traverse = require('babel-traverse').default;
const {transformFromAst} = require('babel-core');
let ID = 0;
// We start by creating a function that will accept a path to a file, read
// its contents, and extract its dependencies.
// 我们通过创建一个接收文件路径、读取它的内容、提前它的依赖的函数开始。
function createAsset(filename) {
// Read the content of the file as a string.
// 读取文件的内容为一个字符串
const content = fs.readFileSync(filename, 'utf-8');
// Now we try to figure out which files this file depends on. We can do that
// by looking at its content for import strings. However, this is a pretty
// clunky approach, so instead, we will use a JavaScript parser.
//
// 现在我们尝试指出这个文件依赖的文件。我们可以通过查看import字符串。然而这是一个
// 本办法,我们使用JavaScript解析器,也就是babylon.
//
// JavaScript parsers are tools that can read and understand JavaScript code.
// They generate a more abstract model called an AST (abstract syntax tree).
// JavaScript解析器是可以读取并理解JavaScript代码的工具。它们生成一个更抽象的模型
// 称为AST 抽象语法树。
// I strongly suggest that you look at AST Explorer (https://astexplorer.net)
// to see how an AST looks like.
// 我强烈建议看一下AST Explorer了解AST像什么。
//
// The AST contains a lot of information about our code. We can query it to
// understand what our code is trying to do.
// AST包含你的代码的信息。我们能查询它来理解我们的代码要做什么。
const ast = babylon.parse(content, {
sourceType: 'module',
});
// This array will hold the relative paths of modules this module depends on.
// 这个数组用来存储这个模块依赖的模块的相对路径。
const dependencies = [];
// We traverse the AST to try and understand which modules this module depends
// on. To do that, we check every import declaration in the AST.
// 我们遍历AST尝试理解这个模块依赖的模块。为此我们检查每一个import声明。
traverse(ast, {
// EcmaScript modules are fairly easy because they are static. This means
// that you can't import a variable, or conditionally import another module.
// Every time we see an import statement we can just count its value as a
// dependency.
// EcmaScript模块相当简单,因为他们是静态的。这意味着你不能导入变量,或者条件导入别
// 的模块。每次我们看到一个import语句,我们仅仅获取它的值作为依赖。
ImportDeclaration: ({node}) => {
// We push the value that we import into the dependencies array.
// 我们把它的值添加到依赖数组中。
dependencies.push(node.source.value);
},
});
// We also assign a unique identifier to this module by incrementing a simple
// counter.
// 我们给这个模块分配一个唯一的标识符,通过自增长一个简单的计数器。
const id = ID++;
// We use EcmaScript modules and other JavaScript features that may not be
// supported on all browsers. To make sure our bundle runs in all browsers we
// will transpile it with Babel (see https://babeljs.io).
// 我们使用EcmaScript模块和别的JavaScript特性,这些在所有浏览器中不能被支持。为了让我们
// 的bundle在所有的浏览器中运行,我们用Babel转码。
//
// The `presets` option is a set of rules that tell Babel how to transpile
// our code. We use `babel-preset-env` to transpile our code to something
// that most browsers can run.
// presets选项是一系列规则,它告诉Babel怎么转码我们的代码。我们使用babel-preset-env来
// 转码。
const {code} = transformFromAst(ast, null, {
presets: ['env'],
});
// Return all information about this module.
// 返回这模块的所有信息
return {
id,
filename,
dependencies,
code,
};
}
// Now that we can extract the dependencies of a single module, we are going to
// start by extracting the dependencies of the entry file.
//
// 既然我们能提取一个模块的依赖,我们将要通过提取入口文件的依赖开始。
//
// Then, we are going to extract the dependencies of every one of its
// dependencies. We will keep that going until we figure out about every module
// in the application and how they depend on one another. This understanding of
// a project is called the dependency graph.
// 然后,我们将要提取它的每个依赖的依赖。我们将要持续这个,知道指出应用中的每个模块已经它们怎么相互依赖。
// 项目的理解称为依赖图。
function createGraph(entry) {
// Start by parsing the entry file.
// 通过解析入口文件开始
const mainAsset = createAsset(entry);
// We're going to use a queue to parse the dependencies of every asset. To do
// that we are defining an array with just the entry asset.
// 我们将要使用一个队列来解析每个资源的依赖。我们定义一个包含入口资源的数组来实现这个。
const queue = [mainAsset];
// We use a `for ... of` loop to iterate over the queue. Initially the queue
// only has one asset but as we iterate it we will push additional new assets
// into the queue. This loop will terminate when the queue is empty.
// 我们使用for...of循环来迭代队列。刚开始这个队列只有一个资源,但是我们迭代它,我们
// 会把新的资源加入到队列中。当队列空了,这个循环就结束了
for (const asset of queue) {
// Every one of our assets has a list of relative paths to the modules it
// depends on. We are going to iterate over them, parse them with our
// `createAsset()` function, and track the dependencies this module has in
// this object.
// 每一个资源有一个列表,保存了它依赖的相对路径。我们将会迭代它们,用我们的crateAsset来
// 解析它们。
asset.mapping = {};
// This is the directory this module is in.
// 这个模块所在的目录
const dirname = path.dirname(asset.filename);
// We iterate over the list of relative paths to its dependencies.
// 迭代依赖
asset.dependencies.forEach(relativePath => {
// Our `createAsset()` function expects an absolute filename. The
// dependencies array is an array of relative paths. These paths are
// relative to the file that imported them. We can turn the relative path
// into an absolute one by joining it with the path to the directory of
// the parent asset.
// 我们的creatAsset函数期望一个绝对文件名。依赖数组是一个相对路径的数组。这些路径
// 相对导入它们的文件。我们能够把相对路径转换成绝对路径,通过把它跟父资源的目录拼接。
const absolutePath = path.join(dirname, relativePath);
// 在已经解析的模块里面寻找
const c = queue.find(x => x.filename === absolutePath);
if (c) {
asset.mapping[relativePath] = c.id;
return;
}
// Parse the asset, read its content, and extract its dependencies.
const child = createAsset(absolutePath);
// It's essential for us to know that `asset` depends on `child`. We
// express that relationship by adding a new property to the `mapping`
// object with the id of the child.
asset.mapping[relativePath] = child.id;
// Finally, we push the child asset into the queue so its dependencies
// will also be iterated over and parsed.
queue.push(child);
});
}
// At this point the queue is just an array with every module in the target
// application: This is how we represent our graph.
return queue;
}
// Next, we define a function that will use our graph and return a bundle that
// 接下来,我们定义一个函数,它使用我们的依赖图来返回一个可以在浏览器中运行的bundle
// we can run in the browser.
//
// Our bundle will have just one self-invoking function:
// 我们的bundle将会只有一个自调用的函数
// (function() {})()
//
// That function will receive just one parameter: An object with information
// about every module in our graph.
// 那个函数将会接收一个参数,它是一个对象,带有依赖图中关于每个模块的信息。
function bundle(graph) {
let modules = '';
// Before we get to the body of that function, we'll construct the object that
// we'll pass to it as a parameter. Please note that this string that we're
// building gets wrapped by two curly braces ({}) so for every module, we add
// a string of this format: `key: value,`.
// 在我们说的函数体之前,我们先构建传给它的对象。请注意我们构建的字符串被花括号包围,所以对于每一个模块,我们增加一个key: value格式的字符串
graph.forEach(mod => {
// Every module in the graph has an entry in this object. We use the
// module's id as the key and an array for the value (we have 2 values for
// every module).
// 依赖图中的每个模块在对象中有一个记录。我们使用模块id作为key,数组作为值。
// The first value is the code of each module wrapped with a function. This
// is because modules should be scoped: Defining a variable in one module
// shouldn't affect others or the global scope.
// 第一个值是用函数包裹的每个模块的内容,这是因为每个模块必须有单独的作用域:在模块中定义的变量不应该影响别的模块或全局作用域。
// Our modules, after we transpiled them, use the CommonJS module system:
// They expect a `require`, a `module` and an `exports` objects to be
// available. Those are not normally available in the browser so we'll
// implement them and inject them into our function wrappers.
// 在我们转码后,我们的模块使用commonjs模块系统:他们期待一个require、module和exports。这些在浏览器中是不可用的,因此我们将会实现并注入它们到我们的包裹函数中。
// For the second value, we stringify the mapping between a module and its
// dependencies. This is an object that looks like this:
// { './relative/path': 1 }.
// 第二个值,我们把模块跟它的依赖的映射字符串化。
// This is because the transpiled code of our modules has calls to
// `require()` with relative paths. When this function is called, we should
// be able to know which module in the graph corresponds to that relative
// path for this module.
// 这是因为转过码的模块使用相对路径调用require函数。当这个函数被调用时,我么应该能够知道它是依赖图中的哪个模块。
modules += `${mod.id}: [
function (require, module, exports) {
${mod.code}
},
${JSON.stringify(mod.mapping)},
],`;
});
// Finally, we implement the body of the self-invoking function.
// 最后,我们实现自调用函数体。
// We start by creating a `require()` function: It accepts a module id and
// looks for it in the `modules` object we constructed previously. We
// destructure over the two-value array to get our function wrapper and the
// mapping object.
// 我们通过创建require函数来开始:它接收一个模块id,在我们前面构建的modules对象中寻找这个模块。我们解构两个元素的数组来获取我们的包装函数和影射对象。
// The code of our modules has calls to `require()` with relative file paths
// instead of module ids. Our require function expects module ids. Also, two
// modules might `require()` the same relative path but mean two different
// modules.
// 我们模块的代码使用相对路径而不是模块id来调用require。我们的require函数希望是模块id。并且,两个模块可能require两个相同的相对路径,但是却是不同的模块。
// To handle that, when a module is required we create a new, dedicated
// `require` function for it to use. It will be specific to that module and
// will know to turn its relative paths into ids by using the module's
// mapping object. The mapping object is exactly that, a mapping between
// relative paths and module ids for that specific module.
// 为了处理这种情况,当一个模块被require时,我们创建一个新的专用的require函数给它用。它将是特定给那个模块的,并且知道怎么通过模块的映射对象把相对路径转换成id。映射对象是针对具体模块的,相对路径和模块id的映射。
// Lastly, with CommonJs, when a module is required, it can expose values by
// mutating its `exports` object. The `exports` object, after it has been
// changed by the module's code, is returned from the `require()` function.
// 最后,在commonjs中,当一个模块被require时,它会通过exports对象导出值。exports对象再被修改之后,从require函数调用返回。
const result = `
(function(modules) {
function require(id) {
const [fn, mapping] = modules[id];
function localRequire(name) {
return require(mapping[name]);
}
const module = { exports : {} };
fn(localRequire, module, module.exports);
return module.exports;
}
require(0);
})({${modules}})
`;
// We simply return the result, hurray! :)
return result;
}
const graph = createGraph('./example/entry.js');
const result = bundle(graph);
console.log(result);