谈一谈 webpack 打包

用户头像
林浩
关注
发布于: 17 小时前
谈一谈webpack打包



前言

每天反反复复在iTerm敲下webpack打包命令, 默认配置下dist目录下就会很听话的生成一大串代码, 那它是怎么把做到的呢? 下面是一个toy系列webpack打包原理来展示项目中打包各个模块是怎么一个流程。这篇文章想写很久了一直拖着, 昨天上课看到有同学又说起了webpack的打包原理, 下午整理了一下, 于是就有了这篇文章。



实现思路

  1. 分析入口文件, 读取文件内容, 将代码字符串转为js对象(ast)

  2. 分析各个模块的依赖并存起来

  3. 将AST对象翻译成可运行的代码

  4. 处理所有依赖生成的代码, 让其可以在浏览器中正常运行



准备工作



先安装江湖上赫赫有名的babel家族老大 @babel/core 以及跟他征战多年的小弟们 @babel/parser @babel/trasverse @/babel/preset-env



人物简介



@babel/core: 家族的核心,一把手, 决定家族的各项工作





@babel/parser: 干些脏累活, 将目标代码解析成AST对象





@babel/trasverse: 打杂, 服务于整个家族, 对代码进行遍历并提供处理策略





@/babel/preset-env: 外交官, 让代码某些特性可以在低版本目标浏览器中得到支持(polyfill)



正文

创建a.test.js b.test.js, index.js, 三角恋关系如代码所示, 最后再创建一个bundler-merge.js去当和事老



a.test.js

import b from './b.test.js'
let result = b() + ' world'
export default result



b.test.js

export default () => {
return 'hello'
}

index.js

import result from './a.test.js';
console.log(result)



后面代码都是在bundler-merge.js中进行



创建一个函数来处理入口文件,读取代码后转为字符串对象(AST)

const fs = require('fs');
const parser = require('@babel/parser');

const analzy = (filename) => {
const content = fs.readFileSync(filename, 'utf-8');
const ast = parser.parse(content, {
sourceType: 'module'
})
}



将ast打印出来,可以看到AST对象的结构





program下面的body就是我们需要处理的内容,输出program.body可以看到 ImportDeclarationExpressionStatement 分别对应 index.js文件中的两行代码



紧接着, 遍历ast对象, 对ImportDeclaration进行处理, 将他们的依赖缓存起来,这里dependencies存了他们的绝对路径和相对路径,后面会用得到

...
const dependencies = {};
for(let name of ast.program.body) {
if (name.type === 'ImportDeclaration') {
const dirname = path.dirname(filename)
const absolutePath = "./" + path.join(dirname, name.source.value)
dependencies.push({
dependencies.absPath = absolutePath,
dependencies.realPath = name.source.value
});
}
}



@babel/traverse 举手说这一步的脏累活可以交给他, 好吧



traverse(ast, {
ImportDeclaration({node}) {
const dirname = path.dirname(filename)
const absolutePath = "./" + path.join(dirname, name.source.value)
dependencies.push({
...
});
}
})





到这一步,我们已经拿到入口文件一些必要信息,如filename, dependencies, 还差把ast对象翻译成编译运行的代码



const { code} = core.transformFromAst(ast, null, { presets: ["@babel/preset-env"]});



babel.transformFromAst(ast: Object, code?: string, options?: Object, callback: Function): FileNode | null



最终翻译的代码长这个样子



到目前为止我们只分析了入口文件里面的代码, 下面我们会对所有依赖也进行同样的操作



const makeDependenciesCollection = (entry) => {
let graphs = [analyze(entry)];
for(let graph of graphs) {
const {dependencies} = graph;
if (dependencies) {
for(let item in dependencies) {
graphs.push(analyze(dependencies[item].absPath));
}
}
}
}



这里创建了一个makeDependenciesCollection函数, 管理了graph队列用于缓存依赖经过翻译后的具体信息(filename, code, dependencies)。将数组对象转成一个对象,方便打包



const makeDependenciesCollection = (entry) => {
let graphs = [analyze(entry)];
let graphObj = {};
...
graphs.forEach(item => {
graphObj[item.filename] = {
dependencies: item.dependencies,
code: item.code
}
})
return graphObj;
}





最后,我们需要把每个模块里面的代码运行到浏览器上, 创建一个新函数, 并返回一个IIFE函数



const generateCode = (entry) => {
const graph = JSON.stringify(makeDependenciesCollection(entry));
// 写一个闭包, 浏览器不认识require, exports, 所以需要对require, exports进行处理
return `
(function(graph){
...
})(${graph});
`;
}



到这一步很关键了, 前面编译好的各个模块,都是都通过require和exports进行关联,但浏览器并不认识这两个东西,所以需要对require和exports进行实现



下面两段代码分别是前面翻译好的代码中存在require和exports的代码片段

  1. 存在require

"use strict";
var _aTest = _interopRequireDefault(
'require("./a.test.js")
);
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { "default": obj };
}
console.log(_aTest["default"]);
  1. 存在exports

"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports["default"] = void 0;
var _bTest = _interopRequireDefault(
require("./b.test.js")
);
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { "default": obj };
}
var result = _bTest["default"] + 'world';
var _default = result;
exports["default\"] = _default;



require函数实现



(function(graph){
// 构建一个require
function require(module) {
function _require() {
return require(graph[module].dependencies.absPath)
}
// 每个模块用一个闭包执行, 不污染全局
(function(require, code) {
// 引入模块是相对路径, 只有转为根目录路径才能找到
eval(code)
})(_require, graph[module].code);
}
require('${entry}')
})(${graph});



这个IIFE函数传入通过graph[module] 去匹配对应的code, 但从上面那段包含require的代码, 会看到里面有一个require函数, 传入的是被引用文件的相对路径, 这里的思路是给每一个模块创建一个闭包, 通过传入一个require参数, 当code里面的require被调用时, 会去执行我们传入的require函数, _require函数作用是根据 require函数传入的相对路径去匹配对应的绝对路径,最后通过eval去执行code



exports实现比较简单,只需要传入一个对象,并且在每次运行结束后return 出去 给下一个依赖使用



(function(graph){
// 构建一个require
function require(module) {
function _require() {
return require(graph[module].dependencies.absPath)
}
let exports = {};
(function(require, exports, code) {
eval(code)
})(_require, exports, graph[module].code);
return exports
}
require('${entry}')
})(${graph});



最终打包完成的代码长这个模样



最后, Ctr + C + V + \r !





参考

https://webpack.js.org/concepts/

https://babeljs.io/docs/en/

https://www.google.com/



发布于: 17 小时前 阅读数: 13
用户头像

林浩

关注

还未添加个人签名 2019.01.14 加入

还未添加个人简介

评论

发布
暂无评论
谈一谈webpack打包