上一节从0-1webpack打包——手写实现webpack(2)实现了webpack的打包功能,本节将实现webpack的添加loader功能。
1 基本介绍
webpack在一般情况下只能处理js文件,但是我们的前端项目里面往往有多种多样的文件,比如.css文件 .vue文件等,这些文件的处理都需要引入loader。在webpack官方文档中,需要在module里的rules里配置
module.exports = {
...
module: {
rules: [
{
test: /.(js|jsx)$/,
use: 'babel-loader',
},
],
},
...
};
2 编写一个处理JSON文件的loader
在根目录下创建jsonLoader.js文件,写入
export function jsonLoader(source){
return `export default ${JSON.stringify(source)}`
};
source是代表JSON文件的内容。原理就是将JSON格式的数据转为string,后期处理的时候就可以把这个数据当做js里面的一个对象进行处理。
3 处理文件
参照webpack的官方文档,我们在index.js里面定义一个对象webpackConfig
const webpackConfig = {
module:{
rules:[
{
test: /\.json$/,
use: jsonLoader,
},
],
}
}
接着就是处理JSON文件,处理应放在读取代码之后,将JSON文件内容放入jsonLoader函数
import {jsonLoader} from "../webpackSource/jsonLoader.js"
...
let source = fs.readFileSync(filePath,{
encoding:"utf-8"
});
const loaders = webpackConfig.module.rules
loaders.forEach(({test,use})=>{
if(test.test(filePath)){
source = use(source)
}
})
逻辑就是遍历那些正则表达式,对于匹配的文件,使用相应的loader进行处理,到这里其实功能已经实现了。
4 改进
阅读官方文档可以发现,有些loader的配置是一个数组,那么就增加一个数组的判断。
loaders.forEach(({test,use})=>{
// regexObj.test(str)方法
// 从后往前的调用loader
if(test.test(filePath)){
if(Array.isArray(use)){
use.reverse().forEach((fn)=>{
source = fn(source)
})
}
else{
source = use(source)
}
}
})
注意数组里面的loader的执行顺序是从后往前,后面的loader的处理结果交给前面的loader。
此外,还有一些loader要调用webpack的一些api,那么就可以写成
const loaderContext = {
addDeps(dep){
console.log("dep",dep)
}
}
loaders.forEach(({test,use})=>{
// regexObj.test(str)方法
// 从后往前的调用loader
if(test.test(filePath)){
if(Array.isArray(use)){
use.reverse().forEach((fn)=>{
source = fn.call(loaderContext,source)
})
}
else{
source = use.call(loaderContext,source)
}
}
})
这样loader对应的this就会变成loaderContext。
5 代码
// 入口文件
import fs from "fs"
import parser from "@babel/parser"
import traverse from "@babel/traverse"
import path from "path"
import ejs from "ejs"
import {transformFromAst} from "babel-core"
import {jsonLoader} from "../webpackSource/jsonLoader.js"
let id = 0
// 获取文件内容
// 获取依赖关系
const webpackConfig = {
module:{
rules:[
{
test: /\.json$/,
use: jsonLoader,
},
],
}
}
function createAsset(filePath){
const deps = []
let source = fs.readFileSync(filePath,{
encoding:"utf-8"
});
const loaders = webpackConfig.module.rules
const loaderContext = {
addDeps(dep){
console.log("dep",dep)
}
}
loaders.forEach(({test,use})=>{
// regexObj.test(str)方法
// 从后往前的调用loader
if(test.test(filePath)){
if(Array.isArray(use)){
use.reverse().forEach((fn)=>{
source = fn.call(loaderContext,source)
})
}
else{
source = use.call(loaderContext,source)
}
}
})
// 得到ast树
const ast = parser.parse(source,{
sourceType:"module"
})
// console.log(ast)
// traverse去遍历ast
traverse.default(ast,{
// 针对import类的节点
ImportDeclaration({node}){
// 收集到了
// 添加到依赖关系里面
deps.push(node.source.value)
}
})
const {code} = transformFromAst(ast,null,{
presets:["env"]
})
// console.log("code",code,"code")
return {filePath,code,deps,mapping:{},id:id++,};
}
function createGraph(){
const mainAsset = createAsset("./example/main.js")
const queue = [mainAsset]
for(const asset of queue){
asset.deps.forEach(ralativePath=>{
// console.log("depsssss",path.resolve("./example",ralativePath),ralativePath)
const child = createAsset(path.resolve("./example",ralativePath))
asset.mapping[ralativePath] = child.id
queue.push(child)
})
}
return queue
}
function build(graph){
const template = fs.readFileSync('./bundle.ejs',{
encoding:"utf-8"
})
const data = graph.map((asset)=>{
const {id,code,mapping} = asset
return {
id,
code,
mapping,
}
})
const code1 = ejs.render(template,{data})
fs.writeFileSync("./dist/bundle.js",code1)
}
const graph = createGraph()
build(graph)