美文网首页
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