将完成以下功能点:
- webpack打包文件:分析webpack打包之后的文件内容,看webpack是如何加载文件的。
- 实现一个简单的webpack :实现webpack的基本功能,包括语法转换,AST生成等。
- 增加对loader和plugin的处理 :增加loader和piugin的处理,完成整体流程。
webpack打包之后的文件
webpack这里就不过多介绍,还不会的朋友们请移步webpack官方文档 下面我们来看看具体的打包出来的文件
// index.js
let title = require('./title.js')
console.log(title)
// title.js
var title = '我是标题'
module.exports = title
// 打包后的代码
;(function (modules) {
// 模块的缓存
var installedModules = {}
// 自己定义的require方法
function __webpack_require__(moduleId) {
// 检查是否有缓存,有就直接返回
if (installedModules[moduleId]) {
return installedModules[moduleId].exports
}
// 创建一个缓存,并将加载的模块放入
var module = (installedModules[moduleId] = {
i: moduleId, // 模块id
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/index.js': function (module, exports, __webpack_require__) {
const title = __webpack_require__(/*! ./title.js */ './src/title.js')
console.log(title)
},
'./src/title.js': function (module, exports) {
var title = '我是标题' has
module.exports = title
},
})
我们可以看到加载的顺序:我删除了很多要的代码,让代码看起来更容易懂些。
- 自执行函数传入初始化的分析结果。
__webpack_require__
执行传入入口模块。- 执行入口模块的加载方法,然后再次调用
__webpack_require__
加载依赖模块。
读取config 建立依赖关系
#! /user/bin/env node
let path = require('path')
let config = require(path.resolve('webpack.config.js'))
let Compiler = require('../bin/compiler')
let compiler = new Compiler(config)
compiler.run()
// #! /user/bin/env node 指出运行在弄的环境。
5行代码核心就是找到webpack.config.js传到Compiler
类中,然后调用run运行。
Compiler类
class Compiler {
run() {
this.hooks.run.call()
// 根据传入的构建模块依赖关系
const entryPath = path.resolve(this.root, this.entry)
// true 标志是入口
this.buildModule(entryPath, true)
// 发射打包好的文件
this.emitFile()
}
}
module.exports = Compiler
我一步一步的给大家讲解。
run方法
1.触发hooks的run,调用所有监听run的plugin。 2.根据你的工作路径和你的传入的entry,找到你的入口文件。 3.看开始编译你的文件,isEntry标志是否是入口文件。 4.发射已经打包好文件。
buildModule方法
buildModule(modulePath, isEntry) {
let source = this.getSource(modulePath)
// 拿到文件的模块id src/index = user/webpack/src/index - user/webpack/
// moduleName = modulePath - this.root
let moduleName = './' + path.relative(this.root, modulePath)
if(isEntry) this.entryId = moduleName
// 我们要将源码的里面的require -> __webpack_require__ 将应用路径加上./src
// 返回一个依赖列表
let {sourceCode, dependencies} = this.parse(source, path.dirname(moduleName))
this.modules[moduleName] = sourceCode
// 递归调用buildModule 来生成依赖关系
// a.js 中引用b.js
dependencies.forEach(dep => {
this.buildModule(path.join(this.root, dep), false)
})
}
1.拿到入口的源码,getSource根据路径返回源码。 2.moduleName = ./src/index.js => this.entryId (moduleName = ./ + user/webpack/src/index - user/webpack/) 就是相对路径+(文件的绝对路径 减去 文件的执行路径) 3.将源码给到parse解析,返回源码和依赖关系。 4.递归点用buildModule解析模块的依赖模块。
parse方法
/**
* 解析模块
* 路径拼接
* AST解析语法树
*/
parse(source, parentPath) {
let ast = babylon.parse(source)
// 依赖数组
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 += path.extname(moduleName) ? '' : '.js'
moduleName = './' + path.join(parentPath, moduleName)
dependencies.push(moduleName)
node.arguments = [types.stringLiteral(moduleName)]
}
}
})
// 转换后的源码
let sourceCode = generator(ast).code
return {sourceCode, dependencies}
}
1.babylon将我们的源码解析成ast。 2.traverse将我们的源码进行修改,require => _webpack_require_,相对路径也要变成用户执行的文件夹的相对路径。(./src.js => ./src/src.js) 3.dependencies简历依赖关系。 4.types将ast的节点替换掉。 5.generator将ast转换成sourceCode源码。
emitFile方法
// 发射数据,用数据渲染我们的模版
emitFile() {
// 1.拿到输出目录。
const { path, filename} = this.config.output
const { entryId, modules } = this
const main = path.join(path, filename)
const template = this.getSource(path.join(__dirname, 'main.ejs'))
const code = ejs.render(template, {
entryId, modules
})
this.assets = {}
this.assets[main] = code
fs.writeFileSync(main, this.assets[main])
this.hooks.run.done()
}
1.拿到输出目录。 2.拿到ejs的模版(这里说明一下,我们将用户的源码转换的结果,内容格式大致是一定的,就是入口只执行函数,不一样的是内容模块,内容模块我们用this.modules[moduleName]建立了依赖关系) 3.写入文件。 4.执行done的hooks
loader的处理
// 根据路径来获取源码
// 所以这里要根据获取源码的文件类型。用规则来处理源码。
getSource(modulePath) {
const rules = this.config.modules.rules
// 这里默认。rules是【】
rules.forEach(rule => {
// 拿到每个规则和loader
const {test, use} = rule
let len = use.length - 1
if(test.test(modulePath)) {
// 递归调用,用loader处理文件
function normalLoader() {
let loader = require(use[len--])
content = loader(content)
if(len >= 0) {
normalLoader()
}
}
normalLoader()
}
})
const content = fs.readFileSync(modulePath, 'utf8')
return content
}
1.拿到rules循环拿到每个rule 2.判断当前的路径是否匹配rule 3.因为一个路径有多个loader处理所以normalLoader递归调用并处理文件。 4.返回处理以后的文件内容。
plugins的处理
const tapable = require('tapable')
runPlugins() {
const plugins = this.config.plugins
plugins.forEach(plugin => {
// 注意这里并不是改变this指向,而是调用plugin的apply方法,将当前Compiler实力传入
plugin.apply(this)
})
}
// plugins的编写
p.js
class P{
apply(compiler) {
compiler.hooks.run.tap(()=>{
console.log('run')
})
}
}
plugins监听webpack暴露出来的钩子,webpack会在相关的路径上调用,并将当前的compiler返回给你。
基本上我们的webpack差不多就能完成。完整代码