(function(modules) { // webpackBootstrap
var installedModules = {}; // 先定义一个缓存池,如果有重复模块,不需要再次加载,直接从缓存中拿
// 实现一个require方法
function __webpack_require__(moduleId) { // moduleId是入口模块,./src/index.js
if(installedModules[moduleId]) { // 检查模块是否在缓存中
return installedModules[moduleId].exports;
}
// 新建一个模块
var module = installedModules[moduleId] = {
i: moduleId,
l: false, // 模块是否加载
exports: {}
};
// 执行模块函数
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// 将模块标记为已加载
module.l = true;
// 返回导出模块
return module.exports;
}
return __webpack_require__(__webpack_require__.s = "./src/index.js"); // 入口模块
})({
"./src/content.js": // key -> 模块的路径
(function(module, exports) { // 函数
eval("function Content() {\n\tvar dom = document.getElementById('root');\n\tvar content = document.createElement('div');\n\tcontent.innerText = 'content';\n\tdom.append(content);\n}\n\nmodule.exports = Content;\n\n//# sourceURL=webpack:///./src/content.js?");
}),
"./src/header.js":
(function(module, exports) {
eval("function Header() {\n\tvar dom = document.getElementById('root');\n\tvar header = document.createElement('div');\n\theader.innerText = 'header';\n\tdom.append(header);\n}\n\nmodule.exports = Header;\n\n//# sourceURL=webpack:///./src/header.js?");
}),
"./src/index.js":
(function(module, exports, __webpack_require__) {
eval("var Header = __webpack_require__(/*! ./header.js */ \"./src/header.js\");\nvar Sidebar = __webpack_require__(/*! ./sidebar.js */ \"./src/sidebar.js\");\nvar Content = __webpack_require__(/*! ./content.js */ \"./src/content.js\");\n\nnew Header();\nnew Sidebar();\nnew Content();\n\n//# sourceURL=webpack:///./src/index.js?");
}),
"./src/sidebar.js":
(function(module, exports) {
eval("function Sidebar() {\n\tvar dom = document.getElementById('root');\n\tvar sidebar = document.createElement('div');\n\tsidebar.innerText = 'sidebar';\n\tdom.append(sidebar);\n}\n\nmodule.exports = Sidebar;\n\n//# sourceURL=webpack:///./src/sidebar.js?");
})
});
Tapable
低配版webpack实现
- 配置webpack.config.js
let path = require('path')
class P{ // plugin
apply(compiler) {
compiler.hooks.emit.tap('emit', function() {
console.log('emit')
})
}
}
class P1{
apply(compiler) {
compiler.hooks.afterPlugins.tap('emit', function() {
console.log('afterPlugins')
})
}
}
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.less$/,
use: [
path.resolve(__dirname, 'loader', 'style-loader.js'),
path.resolve(__dirname, 'loader', 'less-loader.js')
]
},{
test: /\.js$/,
use: path.resolve(__dirname, 'loader', 'loader1.js')
}
]
},
plugins: [
new P(),
new P1()
]
}
- 新建bin目录,在bin目录中新建webpack.js文件
let path = require('path');
// config配置文件
let config = require(path.resolve('webpack.config.js'))
let Compiler = require('../lib/Compiler.js')
let compiler = new Compiler(config)
compiler.hooks.entryOption.call()
// 标识运行编译
compiler.run()
我们新建lib目录,来实现Compiler.js
let fs = require('fs');
let path = require('path')
let babylon = require('babylon') // Babylon是Babel中使用的JavaScript解析器,把源码转换为ast
let t = require('@babel/types') // 在创建AST的过程中判断各种语法的类型,替换节点
let ejs = require('ejs')
let { SyncHook } = require('tapable')
let traverse = require('@babel/traverse').default // es6模块需要default一下 babel-traverse来进行筛选遍历节点
let generator = require('@babel/generator').default //v生成结果
class Compiler {
constructor(config) {
// entry output
this.config = config
// 保存入口文件路径
this.entryId // './src/index.js'
// 保存所有的模块依赖
this.modules = {}
this.entry = config.entry // 入口路径
// 工作路径
this.root = process.cwd()
// 生命周期
this.hooks = {
entryOption: new SyncHook(),
compile: new SyncHook(),
afterCompile: new SyncHook(),
afterPlugins: new SyncHook(),
run: new SyncHook(),
emit: new SyncHook(),
done: new SyncHook()
}
// 如果传递了plugins参数
let plugins = this.config.plugins
if(Array.isArray(plugins)) {
plugins.forEach( plugin => {
plugin.apply(this)
})
}
this.hooks.afterPlugins.call()
}
getSource(modulePath) {
let content = fs.readFileSync(modulePath, 'utf8')
if(this.config.module.rules){
let rules = this.config.module.rules
// // 拿到每个规则来处理
for(let i = 0; i < rules.length; i++) {
let rule = rules[i]
let {test, use} = rule
let len = use.length - 1
if(test.test(modulePath)) { // 这个模块需要loader来转化
// 获取对应的loader函数
function normalLoader() {
let loader = require(use[len--])
content = loader(content)
// 递归调用loader,实现转化功能
if(len >= 0) {
normalLoader()
}
}
normalLoader()
}
}
}
return content
}
// 解析源码 AST解析语法树 打开https://astexplorer.net/对照语法树查看
parse(source, parentPath) { // source源码, parentPath路径
let ast = babylon.parse(source) // 把源码转换为ast
let dependencies = [] // 存放依赖模块数组
traverse(ast, { // 筛选遍历节点
CallExpression(p) {
let node = p.node // 获取对应节点
if(node.callee.name === 'require') {
node.callee.name = '_webpack_require__'
let moduleName = node.arguments[0].value // 取到的就是模块的引用名字
moduleName = moduleName + (path.extname(moduleName) ? '' : '.js')
moduleName = './' + path.join(parentPath, moduleName)
dependencies.push(moduleName) // 放到依赖数组中
node.arguments = [t.stringLiteral(moduleName)]
}
}
})
let sourceCode = generator(ast).code
return { sourceCode, dependencies }
}
// 构建模块
buildModule(modulePath, isEntry) {
// 拿到模块内容
let source = this.getSource(modulePath)
// 模块id modulePath - this.root
let moduleName = './' + path.relative(this.root, modulePath)
if(isEntry) { // 如果是主入口
this.entryId = moduleName // 保存入口名
}
// 解析需要把source源码进行改造,并返回一个依赖列表
let {sourceCode, dependencies} = this.parse(source, path.dirname(moduleName)) // path.dirname(moduleName) 获取父路径 ./src
// 把相对路径和模块中的内容对应起来
this.modules[moduleName] = sourceCode
dependencies.forEach(dep => { // 副模块加载
this.buildModule(path.join(this.root, dep), false)
})
}
emitFile() { // 发射文件
// 用数据渲染模版文件
// 拿到输出目录
let main = path.join(this.config.output.path, this.config.output.filename)
// 模版路径
let templateStr = this.getSource(path.join(__dirname, 'main.ejs'))
let code = ejs.render(templateStr, {entryId: this.entryId, modules: this.modules})
this.assets = {}
// 资源中路径对应的代码
this.assets[main] = code;
// 写到main文件中
fs.writeFileSync(main, this.assets[main])
}
run() {
this.hooks.run.call()
this.hooks.compile.call()
// 执行, 并且创建模块依赖关系
this.buildModule(path.resolve(this.root, this.entry), true) // true代表是否是主模块
this.hooks.afterCompile.call()
// 发射一个文件 打包后的文件
this.emitFile()
this.hooks.emit.call()
this.hooks.done.call()
}
}
module.exports = Compiler
下面是ejs模版
(function(modules) {
var installedModules = {};
function __webpack_require__(moduleId) {
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
module.l = true;
return module.exports;
}
return __webpack_require__(__webpack_require__.s = "<%-entryId%>");
})({
<%for(let key in modules) {%>
"<%-key%>":
(function(module, exports, __webpack_require__) {
eval(`<%-modules[key]%>`);
}),
<%}%>
});
新建loader文件夹
下面我们来实现一个style-loader
function loader(source) {
let style = `
let style = document.createElement('style')
style.innerHTML = ${JSON.stringify(source)}
document.head.appendChild(style)
`
return style;
}
module.exports = loader
再来实现一个less-loader
let less = require('less')
function loader(source){
let css = ''
less.render(source, function(err, c){
css = c.css
})
css = css.replace(/\n/g,'\\n')
return css
}
module.exports = loader
来新建src目录,测试我们的webpack是否可以运行
// base/b.js
module.exports = 'bbb'
// a.js
let b = require('./base/b.js')
module.exports = 'aaa' + b
// index.js
require('./a.js')
require('./index.less')
// index.less
body{
background: red;
}