本文共--字 阅读约--分钟 | 浏览: -- Last Updated: 2021-05-10
注:该文章为网络公开课笔记 原链接
Webpack 是为了支持前端模块化开发,最终打包生成能跑在浏览器上的代码文件的工具。
在模块化开发时,常有以下工程结构:
.
├── src
│ ├─── index.js
│ └─── add.js
├── index.html
// add.js
exports.default = function (a, b) {
return a + b;
}
var add = require('./add.js').default;
console.log(add(1, 2));
这是一个简单的应用,要想将其跑在浏览器上,则需要引入到 html 文档中。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="./src/index.js"></script>
</body>
</html>
很显然,这个程序不会正常执行,因为在浏览器环境,没有 exports
、require
语法。
所以,需要借助一些工具,将我们模块化编写的代码,处理成浏览器支持的代码。 webpack 就用来解决这些问题。
该如何解决呢? 无疑就是读取代码、分析代码、转换代码;那么让我一步一步的实现 简易版的 webpack 吧!
首先,我们如果想让上面 html 引入的 index.js
能够执行,最终要做哪些处理呢?最终打包之后的代码有是什么样的呢?
// 造一个假的 exports 对象,将 require 的 add.js 代码 读出来 通过 eval 执行这个代码字符串
var exports = {};
eval('exports.default = function (a, b) { return a + b }')
console.log(exports.default(1, 2));
为了避免全局污染(上述代码中使用 eval 可以直接修改、声明全局变量,而我们读出来的模块代码是不应该影响全局或其他包括的)的问题,我们进行将其改造成一个自执行函数。
var exports = {};
(function (exports, code) {
eval(code)
})(exports, 'exports.default = function (a, b) { return a + b }')
console.log(exports.default(1, 2));
接下来的问题是,我们在 src/index.js
中使用模块化开发会出现模块之间的依赖,在我们不改变 src/index.js
的模块化写法的前提下,我们需要造一个 require
函数,来支持模块化。
function require(file) {
var exports = {};
(function (exports, code) {
eval(code)
})(exports, 'exports.default = function (a, b) { return a + b }')
return exports;
}
// 原src/indx.js代码
var add = require('./add.js').default;
console.log(add(1, 2));
这样,我们就可以在 index.html
引入这个 require
函数,然后直接引入 src/index.js
就可以执行了。现在的问题是,我们的代码字符串是写死的,上面的 require 函数也不是真正的读取代码,
而当我们需要依赖多个模块化代码的时候,就需要 require
多个模块。那么最终的webpack处理后的代码结构应该是怎样的呢?
(function (list) {
function require(file) {
var exports = {};
(function (exports, code) {
eval(code)
})(exports, list[file])
return exports;
}
require('index.js')
})(
{
"index.js": `
var add = require('add.js').default;
console.log(add(1, 2));
`,
"add.js": `
exports.default = function (a, b) {
return a + b;
}
`
}
)
这样,我们就可以分析出最终要做的事情了,需要将各模块文件读取成代码字符串,然后组成一个图结构传递给主自执行函数,然后通过伪造的 require 去将这些代码字符串取出来并执行。这样我们开发的时候就可以使用模块化开发,而 webpack 最终就会将代码打包组织在一起,最终生成一套代码可直接跑在浏览器上。
而我们在开发Web应用时,是使用 ES6 的模块化语法:
// src/index.js
import add from './add.js';
console.log(add(1, 2));
// src/add.js
export default function (a, b) {
return a + b;
}
同样的,浏览器在之前不支持 script type module
的情况下,是无法支持 es6 模块化语法的,那么最终我们要做的就是以下三个步骤:
那们我们就可以开始 webpack 简易版的编码工作了。
1、安装依赖
# 转换语法树
yarn add @babel/parser
# 依赖分析
yarn add @babel/traverse
# 核心包 es6 => es5
yarn add @babel/core
# babel 转换时需要的插件
yarn add @babel/preset-env
2、编码
编码 webpack.js
// webpack.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')
const objectKeys = require('object-keys')
// 获取模块信息的方法
function getModuleInfo(file) {
// 1.读取文件
const body = fs.readFileSync(file, 'utf-8');
// 2.转换AST语法树,便于之后分析依赖、代码转换
const ast = parser.parse(body, {
sourceType: 'module'
})
// 3.分析和收集当前模块的依赖
// 这个函数最终的返回应该是这样的
// {
// 'file': './src/index.js',
// deps: {
// './add.js': './src/add.js'
// },
// code: '...'
// }
const deps = {};
traverse(ast, {
ImportDeclaration({ node }) {
// 当前执行目录
const dirname = path.dirname(file); // dirname => ./src
// node.source.value 就是当前模块依赖的模块的文件名称,即 import xx from 'abc' 中的 abc
// 获取依赖模块的绝对路径
const abspath = './' + path.join(dirname, node.source.value) // abspath => ./src/add.js
deps[node.source.value] = abspath; // deps => { './add.js': './src/add.js' }
}
})
// 4.es6转es5
const { code } = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"]
})
// console.log('code: ' + code + '\n'); // 这个code就是转换出来的当前模块的代码
// 5.组装成moduleInfo
return {
file,
deps,
code
}
}
// 从入口开始层层递归处理模块信息
function parseModules(file) {
// 获取入口模块的模块信息
const entry = getModuleInfo(file);
// 用来存储整个模块链的各个模块信息
const temp = [entry];
// 通过入口文件递归生成依赖关系图
getDeps(temp, entry)
// 组装成最终的依赖关系图
const depsGraph = {};
temp.forEach(info => {
depsGraph[info.file] = {
deps: info.deps,
code: info.code
}
})
return depsGraph;
// depsGraph 最终的结构是这样的,即符合我们上面封装成的自执行函数中的传参 list 的样子了
// {
// './src/index.js': {
// 'deps': {'./add.js' : './src/index.js'},
// 'code': '...'
// },
// './src/add.js': {
// 'deps': {},
// 'code': '...'
// }
// }
}
function getDeps(temp, { deps }) {
// 拿到模块的 deps,当deps为空时,则不会执行forEach了
Object.keys(deps).forEach(key => {
// 依赖的模块信息
const child = getModuleInfo(deps[key])
// 收集
temp.push(child)
// 递归获取依赖的依赖
getDeps(temp, child)
})
}
// console.log(parseModules('./src/index.js'));
// 通过 parseModules 我们得到了我们最终想要的自执行函数的 list 参数,那么现在需要的就是将list组装进我们的 require函数中就可以了
// 需要注意的是,depsGraph 的 key 值都是绝对路径,那么就需要做点修改
// 当递归执行依赖的时候,需要 require 一个模块的时候,代码常写的是相对路径
// 所以 提供一个 absRequire 来覆盖 eval 中依赖代码执行时的 require
// 作了一层封装,即代码层面写的仍然是相对路径,通过 deps 找到绝对路径,然后传给真正的require
function bundle(file) {
const depsGraph = JSON.stringify(parseModules('./src/index.js'))
return `(function (list) {
function require (file) {
function absRequire(relPath) {
return require(list[file].deps[relPath])
}
var exports = {};
(function (require, exports, code) {
eval(code)
})(absRequire, exports, list[file].code)
return exports;
}
require('${file}')
})(${depsGraph})`
}
const content = bundle('./src/index.js');
// 生成 dist
// 不存在就创建
!fs.existsSync('./dist') && fs.mkdirSync('./dist')
fs.writeFileSync('./dist/bundle.js', content)
3、运行 node webpack.js
即可以得到我们想要的处理后的代码 ./dist/bundle.js
。这样在 index.html
中引用 ./dist/bundle.js
就可以了,对于应用代码也可以使用模块化开发了。
如果我们采用es6 模块化的写法,那么在 index.html
这样使用。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="./src/index.js" type="module"></script>
</body>
</html>
通过 live server
打开就可以正常运作了, 而 vite 就是采用了 script type module 的方式,则大大加快了编译过程,无需打包处理使用了es6模块化开发的js代码,浏览器将直接支持 export 和 import 模块化语法。
自己编写代码实现一个 live server
// server.js
const Koa = require('koa')
const path = require('path')
const fs = require('fs')
const app = new Koa();
app.use(async (ctx) => {
let {
request: { url }
} = ctx;
// 首页
if (url === '/') {
ctx.type = "text/html"
let content = fs.readFileSync('./index.html', 'utf-8')
ctx.body = content
} else if (url.endsWith('.js')) {
console.log('aaa', url);
// js文件
const p = path.resolve(__dirname, url.slice(1));
ctx.type = "application/javascript"
const content = fs.readFileSync(p, 'utf-8')
ctx.body = content
}
})
app.listen(3000, () => {
console.log('vite start...');
})
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!-- <script src="./src/index.js"></script> -->
<!-- <script src="./version4.js"></script> -->
<!-- <script src="./dist/bundle.js"></script> -->
<script src="./src/index.js" type="module"></script>
</body>
</html>
这样我们运行 node server.js
就会启动本地服务,我们访问 localhost:3000
的时候,就会把 index.html
的文件返回给浏览器,而在渲染 html 的时候,碰到了 script type module
时浏览器就会像web服务器请求对应的 ./src/index.js
文件,那么 web服务器就将 js 文件返回给浏览器。