从0-1webpack打包——手写实现webpack(2)

75 阅读1分钟

上一节从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)