美文网首页
webpack实现原理

webpack实现原理

作者: 我叫Aliya但是被占用了 | 来源:发表于2020-01-06 00:00 被阅读0次

Webpack学习笔记
webpack - 项目优化
webpack实现原理
webpack - loader
webpack - plugin
webpack - 项目优化2

创建自己的webpack 全局命令mywebpack

创建一个名为 mywebpack 的项目,目录结构如下:

|- bin
 |- main.js
|- package.json
// package.json
  "bin": {
    "mywebpack": "./bin/main.js"
  },
// main.js
#! /usr/bin/env node
console.log('hahaha')

在目录下执行sudo npm link创建全局npm命令,指向开发目录执行

/usr/local/bin/mywebpack -> /usr/local/lib/node_modules/mywebpack/bin/main.js
/usr/local/lib/node_modules/mywebpack -> /Users/xxxx/xxxx/08.02-mywebpack

实现一个简单的webpack

  • main.js
#! /usr/bin/env node
// 读取配置文件
const path = require('path')
const Compile = require('../src/Compile')

let config_path = path.resolve('webpack.config.js')
let config = require(config_path)
let com = new Compile(config)
com.run()
  • src/Compile.js
const fs = require('fs')
const path = require('path')
const babel = require('@babel/core')
const t = require('@babel/types')
// 所有用到的文件,以文件路径为Key,创建对像
// value为内容,且require被替换为__webpack_require__
// 引入文件路径改为基于根目录路径
// 确定入口文件,执行内容
//
// 创建依赖图谱,把所有依赖做成列表
// 把模板 和 我们解析出来的列表 进行渲染 打包到目标文件中 

class Compile {
    constructor (config) {
        this.config = config
        this.entry = config.entry
        this.root = process.cwd()  // 目标项目物理路径

        this.import_tree = {}   // 引用结构 {路径: 代码}
        // console.log(config)
    }
    run () {
        this.handlerCode(this.entry, true)
        // console.log('runed', this.import_tree)

        this.packagingTemplate()
    }

    // 拼装模板
    packagingTemplate () {
        let ejs = require('ejs')
        let template = fs.readFileSync(path.join(__dirname, './template.ejs'))
        template = ejs.render(template, { entry: this.entry, modules: this.import_tree });
        
        let output_dir = path.join(this.config.output.path, this.config.output.filename)
        fs.writeFileSync(output_dir, template)
    }
 
    handlerCode (filepath, is_main) { 
        // 读取文件
        let in_code = fs.readFileSync(path.join(this.root, filepath)) 

        let relative_path = path.relative(this.root, filepath)
        let parent_path = path.dirname(relative_path)
        let ast = this.handlerAst(in_code, parent_path)
        // console.log('run helf', ast.imports)
        
        ast.imports.forEach(dir => {
            if (!this.import_tree[dir]) this.handlerCode(dir)
        }) 
        // console.log('run', JSON.stringify(this.import_tree))

        this.import_tree[filepath] = ast.out_code
    }

    handlerAst (in_code, parent_path) {
        // 转ast树
        let imports = []
        let out = babel.transform(in_code, {
            plugins: [{ visitor: { CallExpression ({node}) {
                // 找到require代码
                if (node.callee.name == 'require') {
                    // require => __webpack_require__
                    node.callee.name = '__webpack_require__'

                    let import_file = './' + path.join(parent_path, node.arguments[0].value)
                    // 引用文件路径改为基于根目标 
                    node.arguments[0].value = import_file
                    // 递归引用的文件
                    imports.push(import_file)
                }
            } } }]
        })
        return {out_code: out.code, imports: imports}
    }
}

module.exports = Compile
  • src/template.ejs
...
    // Load entry module and return exports
    return __webpack_require__(__webpack_require__.s = "<%= entry %>");
})
/************************************************************************/
({
    <% for (let key in modules) { %>
        "<%- key %>": (function(module, exports, __webpack_require__) {
            eval("<%- modules[key].replace(/\n/g, '\\n').replace(/"/g, '\\"') %>\n\n//# sourceURL=webpack:///<%- key %>?");
        }),
    <% } %>
});

loaders

  • 目标项目 webpack.config.js 中添加
    module: {
        rules: [
            {
                test: /\.txt$/,
                use: [ path.resolve(__dirname, 'loaders', 'txt.js') ] 
            }
        ]
    }
  • 目标项目 loaders/txt.js

必须是个方法,入参为.txt文件内容

module.exports = function (source) {
    console.log(source)
    return source
}

webpack执行后打包成功,并打印出了文件内容。下面开始实现

  • src/Compile.js 在资源加载(读取文件处,单独拿出一个方法)时处理loaders
...
    readSource (filepath) {
        let source = fs.readFileSync(path.join(this.root, filepath), 'utf8')  // 从下向上执行
        this.config.module.rules.reduceRight((perv, item) => {
            let {test: reg, use} = item
            if (reg.test(filepath)) {
                let loader = require(use[0])
                source = loader(source)
            }
        }, '') 
        return source
    }
...

实现less-loader

  • 目标项目 webpack.config.js 中添加
...
            {
                test: /\.less$/,
                // use: [ 'style-loader', 'css-loader', 'less-loader' ]  从左向右执行
                use: [ 
                    path.resolve(__dirname, 'loaders', 'style.js'), 
                    path.resolve(__dirname, 'loaders', 'less.js'),
                ] 
            }
  • 目标项目 loaders/less.js
let less = require('less')
module.exports = function (source) {
    let css = ''
    less.render(source, (err, r) => {   // render是同步的 [黑人问号脸]
        css = r.css
    })
    return css
} 
  • 目标项目 loaders/style.js
module.exports = function (source) {
    // JSON.stringify可以处理引号问题
    let code = `
    let style = document.createElement('style')
    style.innerHTML = ${JSON.stringify(source).replace(/\\n/g,'\\\n')}
    document.head.append(style)
    `
    return code
} 
  • src/Compile.js 在资源加载(读取文件处,单独拿出一个方法)时处理loaders
...
    readSource (filepath) {
        let source = fs.readFileSync(path.join(this.root, filepath), 'utf8')
        this.config.module.rules.reduceRight((perv, item) => {  // 从下向上执行
            let {test: reg, use} = item
            if (reg.test(filepath)) {
                use.reduceRight((perv, loader_name) => {        // 从左向右执行
                    let loader = require(loader_name)
                    source = loader(source)
                }, '')
            }
        }, '')
        return source
    }
...

如果loader是异步的

  • 目标项目 loaders/less.js 不返回数据,使用异步回调
...
    let cb = this.async()   // webpack给的异步回调方法
    setTimeout(() => cb(null, css), 2000)   // 方法要2个入参,err, source
} 
  • src/Compile.js 改动就大发了
    async run () {
        console.log('runing')
        await this.handlerCode(this.entry, true)
        console.log('runed', this.import_tree)
        this.packagingTemplate()
    }

    readSource (filepath) {
        let source = fs.readFileSync(path.join(this.root, filepath), 'utf8')

        return new Promise((resolve) => {
            let rules = this.config.module.rules
            let rule_index = rules.length - 1
            let loader_list = []
            let loader_index = -99
            let isSync = false
            let next = () => { 
                if (rule_index < 0) return resolve(source);
                if (loader_index < 0) {
                    // 读取下一项配置
                    let {test: reg, use} = rules[rule_index--]
                    if (reg.test(filepath)) {
                        loader_list = use
                        loader_index = use.length - 1
                    } else return next();
                }
                isSync = false
                let loader = require(loader_list[loader_index--])
                let new_source = loader.call(this, source)
                if (!isSync) setvalueFn(null, new_source)
            }
            // 赋值并next
            let setvalueFn = (err, new_source) => {
                source = new_source
                next()
            };
            // 异步标识及回调
            this.async = () => {
                isSync = true
                return setvalueFn
            }
            next()

        })
        // return source
    }
 
    async handlerCode (filepath, is_main) { 
        // 读取文件
        console.log('----', filepath)
        let in_code = await this.readSource(filepath)

        let ast = this.handlerAst(in_code, filepath)
        for (let i = 0; i < ast.imports.length; i++) {
            if (!this.import_tree[ast.imports[i]]) await this.handlerCode(ast.imports[i])
        }

        this.import_tree[filepath] = ast.out_code 
    }

plugins

webpack先解析plugins,再解析loader(pulgin会在run之前执行)

  • 目标项目 webpack.config.js 中添加
    plugins: [
        // 
        new A, new B
    ]
}

class A {
    apply () {  // 必须提供apply方法
        console.log('A')
    }
}
class B {
    apply (compiler) {  // compiler上有hooks,为webpack内部的钩子们 => tapable的SyncHook实例
        console.log('B')
    }
}

在任意loader头部添加console,执行后会A B loader ...

打印compiler下的hooks可以看到它上面的钩子们

hooks上的部分钩子
  • tapable的用法
// 非node下的event,发布订阅
let { SyncHook } = require('tapable')
let s = new SyncHook(['a', 'b'])

s.tap('11111', function (a,b,c) {
    console.log('11', a,b,c)
})
s.tap('22222', function (a,b,c) {
    console.log('22', a,b,c)
})

s.call('i', 'am', 'girl')
  • src/Compile.js
let { SyncHook } = require('tapable')
...
    constructor (config) {
        ...
        
        // 使用tapable实现钩子
        this.hooks = {
            beforeRun: new SyncHook(['arg']),
            afterEmit: new SyncHook(['arg'])
        }
        // 处理plugin
        this.config.plugins.forEach(pg => {
            pg.apply(this)
        })
    }
    async run () {
        this.hooks.beforeRun.call('something')
        ...
        
    handlerAst (in_code, filepath) {
        ...
        this.hooks.afterEmit.call('something')
        return {out_code: out.code, imports: imports}
    }
class B {
    apply (compiler) {
        console.log('B')
        compiler.hooks.beforeRun.tap('test', function () {
            console.log('beforeRun')
        })
        compiler.hooks.afterEmit.tap('test', function () {
            console.log('afterEmit')
        })
    }
}

执行后结果A B beforeRun afterEmit loader afterEmit

相关文章

网友评论

      本文标题:webpack实现原理

      本文链接:https://www.haomeiwen.com/subject/vqogoctx.html