最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 手写简单的webpack

    正文概述 掘金(cv开发工程师)   2020-12-08   375

    实现webpack打包功能

    简单分析webpack打包的结果

    先建立三个文件 index.js

    let news = require('./news.js')
    console.log(news.content)
    

    message.js

    module.exports = {
        content: `今天要下雨啦`
    }
    

    news.js

    let message = require('./message.js')
    module.exports = {
      content: `今天有个大新闻,爆炸新闻!!!!内容是${message.content}`
    }
    

    然后我们利用webpack进行打包,来分析这个打包后的结果

    对这个打包后的结果进行简化分析。抽离出来的核心代码如下

     (function(modules) { 
    	var installedModules = {};
    	function __webpack_require__(moduleId) {·       
    	if(installedModules[moduleId]) {
            return installedModules[moduleId].exports;		
        }
        var module = installedModules[moduleId] = {
            i: moduleId,
    		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__) {eval("let news = __webpack_require__(/*! ./news.js */ \"./src/news.js\")\nconsole.log(news.content)\n\n//# sourceURL=webpack:///./src/index.js?");}),
        "./src/message.js":(function(module, exports) {eval("\nmodule.exports = {\n    content: `今天要下雨啦`\n}\n\n//# sourceURL=webpack:///./src/message.js?")}),
        "./src/news.js":(function(module, exports, __webpack_require__) {eval("let message = __webpack_require__(/*! ./message.js */ \"./src/message.js\")\nmodule.exports = {\n    content: `今天有个大新闻,爆炸新闻!!!!内容是${message.content}`\n}\n\n//# sourceURL=webpack:///./src/news.js?");})
    })  
    
    //  对这段代码进行分析
    // 首先是一个自调用函数  modules: 是一个对象  obj
    //  执行了这个自调用函数,自调用函数。内部返回了一个__webpack_require__()  传参为entry  里面的值 
    // 执行__webpack_require__() 传入entry的值
    //  开始执行函数  
    // 函数内部  module = {
    //    i: modulesId,
    //    l: false
    //    export: {}
    // }
    // 继续执行,调用了最外层传入的参数,改变this指向为空{}, 最后就是一个__webpack_require__的递归调用
    

    所以其实我们要实现一个webpack.主要任务有两个

    1. 将所有的require替换成__webpack__require,
    2. 将模块中的所有依赖进行读取,拼接成一个对象,传入自调用函数里面去。

    搭建项目基础骨架

    1. 创建一个bin目录,在bin目录下创建yj-pack文件.这个文件的主要目的就是读取webpack.config.js配置目录(暂不支持webpack那种自定义配置名称),将读取的配置传入compiler模块,让compiler模块进行相应的处理。此模块不做其他操作
    #! /usr/bin/env node 
    const path = require('path') 
    //  1. 读取需要打包项目的配置文件
    let config = require(path.resolve('webpack.config.js'))
    // let config  = require(process.cwd(),'webpack.config.js')
    const Compiler = require('../lib/compiler')
    let a = new Compiler(config)
    a.start()
    
    1. 创建我们的compiler模块
    class Compiler {
        constructor(config) {
           this.config = config   // 将配置初始化
           this.entry = config.entry   // 入口的相对路径
           this.root = process.cwd()   // 执行命令的当前目录
        }
        start() {
          // 进行相应的文件操作
        }
    }
    module.exports = Compiler
    
    

    读取入口文件

    1. 首先我们思考一下,我们需要做什么?

    我们主要目的是读取入口文件,分析各个模块之间的依赖关系,将require替换成__webpack_require__

    class Compiler {
        constructor(config) {
           this.config = config   // 将配置初始化
           this.entry = config.entry   // 入口的相对路径
           this.root = process.cwd()   // 执行命令的当前目录
           this.analyseObj = {}   // 这个就是我们最后需要的文件对象
        }
    	// 工具函数--- 用来拼接路径
        getOriginPath(path1,path2) {
            return path.resolve(path1,path2)
        }
        // 工具函数--- 读取文件
        readFile(modulePath) {
    		 return fs.readFileSync(modulePath,'utf-8')
    	}
        // 入口函数
        start() {
          // 进行相应的文件操作
          // 拿到入口文件的路径,进行分析
          let originPath = this.getOriginPath(this.root,this.entry)
          this.depAnalyse(originPath)
          }
         //核心函数
        depAnalyse(modulePath){
        // 这样content,就是我们就可以读取webpack.config.js里面的入口文件
        let content =  this.readFile(modulePath)
       }
    }
    

    利用AST语法树替换require

    这一步读取文件之后,将require替换成__webpack_require__ .主要是利用来babel插件

    const fs = require('fs')
    const path = require('path')
    const traverse = require('@babel/traverse').default;
    const parser = require('@babel/parser');
    const generate = require('@babel/generator').default
    class Compiler {
        constructor(config) {
           this.config = config   // 将配置初始化
           this.entry = config.entry   // 入口的相对路径
           this.root = process.cwd()   // 执行命令的当前目录
           this.analyseObj = {}   // 这个就是我们最后需要的文件对象
        }
        // 工具函数--- 用来拼接路径
        getOriginPath(path1,path2) {
          return path.resolve(path1,path2)
        }
        // 工具函数--- 读取文件
        readFile(modulePath) {
          return fs.readFileSync(modulePath,'utf-8')
        }
        // 入口函数
        start() {
          // 进行相应的文件操作
          // 拿到入口文件的路径,进行分析
          let originPath = this.getOriginPath(this.root,this.entry)
          this.depAnalyse(originPath)
        }
        // 核心函数
        depAnalyse(modulePath){
          //这样content,就是我们就可以读取webpack.config.js里面的入口文件
          let content =  this.readFile(modulePath)
          // 将代码转化为ast语法树    
          const ast = parser.parse(content) 
          // traverse是将AST里面的内容进行替换
          traverse(ast, {
              CallExpression(p) {
                if(p.node.callee.name === 'require') {
                    p.node.callee.name = '__webpack_require__'
                }
              }
          })
          // 最后将ast语法树转化为代码
          let sourceCode =  generate(ast).code
        }
    }
    这样我们就完成了第一步,读取了当前入口文件,然后将内容的require进行了替换
    

    递归实现模块依赖分析

    其实上述步骤还有一定的问题。如果index.js里面有多个模块依赖怎么办?类似 index.js

    let a = require('./news.js)
    let b = require('./news1.js)
    

    因为我们需要将每个模块的依赖放在一个数组中进行保存。然后在对每个模块进行递归遍历。 那么我们继续改进depAnalyse函数

    depAnalyse(modulePath){
      // 这样content,就是我们就可以读取webpack.config.js里面的入口文件
      let content =  this.readFile(modulePath)
      // 将代码转化为ast语法树
      const ast = parser.parse(content) 
      // 用于存取当前模块的所有依赖。便于后面遍历
      let dependencies = []
      // traverse是将AST里面的内容进行替换
       traverse(ast, {
           CallExpression(p) {
             if(p.node.callee.name === 'require') {
                 p.node.callee.name = '__webpack_require__'
    		     // 这里是对路径进行处理,因为在window下面文件路径\.而lunix下面是/ 。所以我们进行统一处理一下
                 let oldValue = p.node.arguments[0].value
                 p.node.arguments[0].value = './'+ path.join('src',oldValue).replace(/\\+/g,'/')
    		     // 将当前模块依赖的文件路径推送到数组里面,遍历我们后续进行递归遍历
                 dependencies.push(p.node.arguments[0].value)
              }
            }
        })
        // 最后将ast语法树转化为代码
        let sourceCode =  generate(ast).code
        // 把当前的依赖,和文件内容推到对象里面去。
        let relavitePath = './'+ path.relative(this.root,modulePath).replace(/\\+/g,'/')
        this.analyseObj[relavitePath] = sourceCode
        // 每个模块可能还有其他的依赖,所以我们需要递归遍历一下。
        dependencies.forEach(dep=>{
          //递归一下
          this.depAnalyse(this.getOriginPath(this.root,dep))
        })
    }
    

    这样你打印this.analyseObj就发现已经得到了我们想要的对象。接下来我们思考怎么生成webpack模版。

    生成webpack模版文件

    1. 首先我们找到最开始简化后webpack的打包文件。我们利用ejs进行相应的改造,建立一个template文件夹,建立output.ejs模版。模版如下
     (function(modules) { 
    	var installedModules = {};
    	function __webpack_require__(moduleId) {
    	if(installedModules[moduleId]) {
            return installedModules[moduleId].exports;		
        }
        var module = installedModules[moduleId] = {
            i: moduleId,
    		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 = "<%-entry%>");
     })
     ({
         <% for (let k in modules) { %>	
    		"<%-k%>": 
    		(function(module, exports, __webpack_require__) {eval(`<%-modules[k]%>`)
    	 }),
    	 <%}%>
    })
    

    让webpack结合ejs,输出文件

    1. 这一步主要是当我们分析完成文件之后。将模版和我们分析的this.analyseObj进行结合
    start() {
      let originPath = this.getOriginPath(this.root,this.entry)
      this.depAnalyse(originPath)
      // 编译完成
      this.emitFile()
    }
    emitFile() {
      let template= this.readFile(path.join(__dirname,'../template/output.ejs'))
      let result =  ejs.render(template,{
        entry: this.entry,
        modules: this.analyseObj
      })
      let outputPath = path.join(this.config.output.path,this.config.output.filename)
      fs.writeFileSync(outputPath,result)
      // console.log(result)
    }
    

    这样我们就将文件输出到指定目录了。然后利用node执行,或者放在浏览器执行。就可以读取了。这样我们就实现了简单的webpack打包功能

    实现webpack的loader功能

    loader其实本质上就是一个函数

    在webpack中怎么开发一个自己的loader

    首先在webpack.config.js中定义自己的loader

    const path = require('path')
    module.exports = {
        entry: './src/index.js',
        output: {
            path: path.join(__dirname, 'dist'),
            filename: 'bundle.js'
        },
        mode: 'development',
        module:{
            rules: [
                // {
                //   test: /\.js$/,
                //   use: ['./src/loaders/loader1.js','./src/loaders/loader2.js','./src/loaders/loader3.js' ]     
                // }
                // {
                //     test: /\.js$/,
                //     use: './src/loaders/loader2.js'
                // },
                {
                test: /\.js$/,
                use: {
                  loader: './src/loaders/loader1.js',
                  options: {
                      name: '今天111111111'
                  }
                }
               }
            ]
        }
    }
    

    然后在自己新建自己的loader

    module.exports = function(source){
        console.log(this.query)
        return source.replace(/今天/g,this.query.name)
    }
    

    这样我们最基础的loader就已经实现了。这个loader的功能就是将js的所有 ‘今天’替换成‘'今天111111111’,现在我们知道怎么写一个简单的loader,那么接下来我们看一下怎么让我们自己写的webpck支持loader功能

    怎么让我们写的webpack支持loader功能

    1. 由上面的loader进行简单的分析。我们主要是读取webpack.config.js的modules。然后匹配相应的文件,最后对文件进行相应的处理.

    我们思考一下loader在什么时候执行呢?,应该在我们读取文件之后就应该执行,所以我们继续对dpAnalyse进行改造 注意: loader的执行顺序是从下到上,从右到左

    depAnalyse(modulePath){
      // 这样content,就是我们就可以读取webpack.config.js里面的入口文件
      let content =  this.readFile(modulePath)
      // loader的处理主要是首先读取webpack.config.js里面的配置文件module.rules。 首先判断js里面的配置文件module.rules       存在与否,然后在对里面的loader进行倒叙遍历
      // 开始处理loader
        for(var i = this.rules.length-1;i>=0;i--){
            // this.rules[i]  
            let {test,use} = this.rules[i]
            //  匹配是否符合规则
            if(test.test(modulePath)){
                if(Array.isArray(use)){
                // 这里要判断数字,对象,字符
                //   这儿可以封装一下,这里面没有封装
                for(var j=use.length-1;j>=0;j--){
                    let loader =  require(path.join(this.root,use[j])) 
                    content = loader(content)
                  }
                }else if(typeof use === 'string') {
                    let loader =  require(path.join(this.root,use)) 
                    content = loader(content)
                }else if(use instanceof Object){
                    // console.log(use.options)
                    // console.log("现在use是第一项")
                    let loader =  require(path.join(this.root,use.loader)) 
                    content = loader.call({query:use.options},content)
                }
               
            } 
        }
      // 将代码转化为ast语法树
      const ast = parser.parse(content) 
      // 用于存取当前模块的所有依赖。便于后面遍历
      let dependencies = []
      // traverse是将AST里面的内容进行替换
       traverse(ast, {
           CallExpression(p) {
             if(p.node.callee.name === 'require') {
                 p.node.callee.name = '__webpack_require__'
    		     // 这里是对路径进行处理,因为在window下面文件路径\.而lunix下面是/ 。所以我们进行统一处理一下
                 let oldValue = p.node.arguments[0].value
                 p.node.arguments[0].value = './'+ path.join('src',oldValue).replace(/\\+/g,'/')
    		     // 将当前模块依赖的文件路径推送到数组里面,遍历我们后续进行递归遍历
                 dependencies.push(p.node.arguments[0].value)
              }
            }
        })
        // 最后将ast语法树转化为代码
        let sourceCode =  generate(ast).code
        // 把当前的依赖,和文件内容推到对象里面去。
        let relavitePath = './'+ path.relative(this.root,modulePath).replace(/\\+/g,'/')
        this.analyseObj[relavitePath] = sourceCode
        // 每个模块可能还有其他的依赖,所以我们需要递归遍历一下。
        dependencies.forEach(dep=>{
          //递归一下
          this.depAnalyse(this.getOriginPath(this.root,dep))
        })
    }
    

    这样我们就简单实现了loader的功能

    实现webpack的plugins功能

    怎么开发一个自定义的plugins

    plugins 其实就是一个自定义的类。但webpack中规定了我们这个类里面必须要实现apply方法。 其实原理就是webpack中内部实现了自己的一套生命周期,然后你只需要在你们apply方法里面去调用webpack里面提供的生命周期就行

    1. 接下来我们就实现自己最简单的helloworld的plugins
    class HelloWorldPlugin{
        apply(Compiler){
           // 在文件打包结束后执行
            Compiler.hooks.done.tap('HelloWorldPlugin',(compilation)=> {
                console.log("整个webpack打包结束")
            })
            // 在webpack输出文件的时候执行
            Compiler.hooks.emit.tap('HelloWorldPlugin',(compilation)=> {
                console.log("文件开始发射")
            })
            // console.log('hello world')
        }
    }
    module.exports = HelloWorldPlugin
    

    最后我们只需要在webpack.config.js引入相应的plugins就可以了。

    利用tapable实现生命周期

    上面可以看到webpack实现了自己的生命周期。那么他是怎么实现的呢?核心其实就是一个发布订阅者模式,webpack其实主要是利用了一个核心库tapable 那么我们自己来构建一个简单的类,来实现相应的生命周期。主要分为三步

    1. 注册自定义的事件
    2. 在适当的时机去绑定事件
    3. 在适当的时机去调用事件
    // study 前端
    const {SyncHook}  = require('tapable')
    class Frontend{
        constructor() {
            this.hooks = {
               beforeStudy: new SyncHook(),
               afterHtml: new SyncHook(),
               afterCss: new SyncHook(),
               afterJs: new SyncHook(),
               afterReact: new SyncHook() 
            }
        }
        study() {
            console.log('开始准备学习')
            this.hooks.beforeStudy.call()
            console.log('开始准备学习html')
            this.hooks.afterHtml.call()
            console.log('开始准备学习css')
            this.hooks.afterCss.call()
            console.log('开始准备学习js')
            this.hooks.afterJs.call()
            console.log('开始准备学习react')
            this.hooks.afterReact.call()
        }
    }
    let f = new Frontend()
    f.hooks.afterHtml.tap('afterHtml',()=>{
        console.log("学完html后我想造淘宝")
    })
    
    f.study()
    

    这样我们就实现了自己的生命周期函数

    让我们手写的webpack支持plugins功能

    在compiler函数一初始化的时候就定义自己的webpack的生命周期,并且在start中间进行相应的调用,这样我们就实现了自己的生命周期

    class Compiler {
        constructor(config) {
           this.config = config 
           this.entry = config.entry
           this.root = process.cwd()
           this.analyseObj = {}
           this.rules = config.module.rules
           this.hooks = {
            //    生命周期的定义
            compile: new SyncHook(),
            afterCompile: new SyncHook(),
            emit: new SyncHook(),
            afterEmit: new SyncHook(),
            done: new SyncHook()
           }
           // plugins数组中所有插件对象,调用apply方法,相当于注册事件
           if(Array.isArray(this.config.plugins)){
            this.config.plugins.forEach(plugin => {
                plugin.apply(this)
            })
           }
        }
        start() {
            // 开始编译了
            this.hooks.compile.call()
            //  开始打包
            //  依赖分析
            let originPath = this.getOriginPath(this.root,this.entry)
            this.depAnalyse(originPath)
            // 编译完成了
            this.hooks.afterCompile.call()
            // 开始发射文件了
            this.hooks.emit.call()
            this.emitFile()
            this.hooks.afterEmit.call()
            this.hooks.done.call()
        }
      }
    

    为自己的webpack写一个htmlplugin插件

    那么接下来,我们为自己的webpack写一个htmlplugin插件 实现自己的htmlplugin类。并且在相应的webpack周期内执行相应的函数 这里使用了cheerio库,主要是可以对html字符串进行dom操作

    const fs = require('fs')
    const cheerio = require('cheerio')
    
    class HTMLPlugin{
        constructor(options){
            this.options = options
        }
        apply(Compiler){
            //  注册afterEmit 事件
            Compiler.hooks.afterEmit.tap('HTMLPlugin',(compilation)=> {
                // console.log("整个webpack打包结束")
               let result = fs.readFileSync(this.opions.template,'utf-8')
                // console.log(this.options)
                // console.log(result)
                let $ = cheerio.load(result)
                Object.keys(compilation.assets).forEach(item=>{
                    $(`<script src="${item}"></script>`).appendTo('body')
                })
                // 生成html。输出
                // $.html()
                fs.writeFileSync('./dist/' + this.options.filename,$.html())
            })
            // console.log('hello world')
        }
    }
    module.exports = HTMLPlugin
    

    源码地址

    最后我们就大功告成。详细代码(github):
    github.com/yujun96/yj-…
    github.com/yujun96/web…


    起源地下载网 » 手写简单的webpack

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元