webpack4-js模块实现

前言:
ES6模块化是ECMA提出的JavaScript模块化规范,他在语言的层面上实现了模块化。浏览器厂商和node都宣布要原生支持该规范。他将逐渐取代上一篇所提到的CommonJSAMD规范,成为浏览器和服务器通用的模块解决方案。

采用ES6模块化导入和导出时的代码如下:

// 导入
import { name } from './person.js'
// 导出
export const name = 'zl';

ES6模块虽然是终极模块化方案,但他的缺点在于目前无法直接运行在大部分JavaScript运行环境下,必须通过工具转化成标准的ES5后才能够正常运行。webpack就是这样一种自动化构建工具,能够把源代码转化成发布到线上的可执行JavaScriptCSSHTML代码,所以学习他的重要性不言而喻。

1. webpack

webpack能够做的事情有很多:

  • 代码转化(各种插件/loader):ES6编译成ES5LESS编译成CSS等;
  • 文件优化:压缩JavaScriptCSSHTML代码,压缩合并图片等;
  • 代码分割:提取多个页面的公共代码、提取首屏不需要执行的部分代码让其异步加载;
  • 代码合并:在采用模块化的项目当中会有多个模块和文件,需要构建功能把模块分类合并成一个文件;
  • 自动刷新(devserver):监听本地源代码的变化,自动重新构建、刷新浏览器;
  • 自动发布:更新完代码后,自动构建出线上发布代码并传输给发布系统。

webpack中一切文件皆为模块,通过loader转化文件,通过plugin注入钩子,最后输出由多个模块组合成的文件,webpack专注于构建模块化项目。

2. 安装

  • 全局安装 npm install webpack -g
    但是这种安装不推荐,不同的人安装的版本不同时,打包会受到影响,打包不成功。

  • 本地安装
    npm init -y(这里的-y表示直接略过所有问答,全部采用默认答案)
    npm install webpack webpack-cli -D (开发依赖,只在开发环境下使用)

安装好之后的我们的文件目录下会多一个node_modules文件夹,其中的bin文件夹中有本次安装的webpack
webpack4-js模块实现_第1张图片

src文件夹下我们创建a.js以及入口文件index.js

// a.js
module.exports = "happy coding!"
// index.js
/* 入口文件 */
let str = require("./a.js");
console.log(str)

3. 打包并实现

下面我们执行一下打包操作:npx webpack,可以看到我们的文件目录中多了一个dist文件夹:
在这里插入图片描述
这里的main.js就是默认的打包出口。这里要强调一下在webpack3中需在webpack.config.js中配置是开发模式还是生产模式,默认在生产模式下打包出来的内容只有按一行显示,不会给压缩、换行。
在这里插入图片描述
webpack4中,可以通过设置模式设置当前是开发模式还是生产模式:
webpack4-js模块实现_第2张图片
执行命令:npx webpack --mode development
再来看生成的打包文件main.js中的内容:
webpack4-js模块实现_第3张图片
而且打包出来的文件可以在浏览器使用,在src下新建index.html,引入main.js文件:




  practise
  
  





运行一下,看下浏览器打印出的结果:
webpack4-js模块实现_第4张图片
下面来看看打包出来的文件中的内容,在我们不引入其他模块了,在入口文件index.js中打印一行字符串,然后打包得到main.js文件,我们将main.js中核心的代码留下,其他的都删掉:

// main.js
(function(modules) {
	function __webpack_require__(moduleId) {  // moduleId代表文件名
		var module =  {
			exports: {}
		};
		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
		return module.exports;
	}
	return __webpack_require__("./src/index.js");
})
({
  "./src/index.js":
  (function(module, exports) {
    eval("console.log(\"hello, coder!\");\n\n//# sourceURL=webpack:///./src/index.js?");
  })
});

整体来看核心代码就是一个自执行函数,传入的参数是一个对象,属性是"./src/index.js",值即为后面所跟的函数,这个对象会传到函数参数modules中。在自执行函数中定义了函数__webpack_require__,然后返回一个执行该函数的结果,传入的是一个文件名 "./src/index.js",所以moduleId就代表文件名。然后新建了一个对象module ,通过modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);取传入的modules的属性值,即:

(function(module, exports) {
    eval("console.log(\"hello, coder!\");\n\n//# sourceURL=webpack:///./src/index.js?");
 })

并执行,最后返回执行结果:return module.exports;。可以看出,这就是我们在模块化规范简单实现中实现的req,见https://blog.csdn.net/zl13015214442/article/details/96109681。

4. 实现webpack中js的模块

我们自己实现一个类似这样的webpack,在根目录下新建文件夹zlpack->bin->zlpack.js
在这里插入图片描述
我们的目标是在命令行输入zlpack回车也能像webpack那样自动打包,我们知道在npm全局安装的都可以在命令行中使用,首先得有package.json,在zlpack路径下npm init -y
webpack4-js模块实现_第5张图片
这样在该目录下就有了package.json文件,其中bin这个字段表示执行zlpack命令的时候执行的是bin目录下zlpack这个文件:
webpack4-js模块实现_第6张图片
下面关键的一步是我们需要把我们构造的zlpack这个文件夹引入到npm全局命令下,执行npm link
webpack4-js模块实现_第7张图片
此时我们在zlpack.js中写一段js代码:console.log('hello zlpack');,然后在命令行执行zlpack,会提示:
webpack4-js模块实现_第8张图片
是因为我们没有说明当前文件是js文件,所以npm分辨不出,而js文件应该用node运行,所以应该在zlpack.js添加一行:

#! /usr/bin/env node  // 就是解决了不同的用户node路径不同的问题,可以让系统动态的去查找node来执行你的脚本文件。
console.log('hello zlpack');

然后重新链接一下,再执行zlpack
webpack4-js模块实现_第9张图片
可见已经成功执行zlpack.js文件。那么此时我们就可以在任意目录下使用zlpack这样一个命令了。
下面我们就要实现webpack打包的过程了,首先最后会生成我们在之前的main.js中分析的那个自执行函数,我们当做模板粘到zlpack.js中,并定义入口和出口文件,要实现将内容打包到出口文件中:

#! /usr/bin/env node
// 这个文件就要描述如何打包
// 入口文件
let entry = '../src/index.js';  
// 出口文件
let output = '../dist/main.js';
let fs = require('fs');
let script = fs.readFileSync(entry, 'utf8');
let templpate = `
(function(modules) {
	function __webpack_require__(moduleId) {
		var module = {
			exports: {}
		};
		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
		return module.exports;
	}
	return __webpack_require__("./src/index.js");
})
({
  "./src/index.js":
  (function(module, exports) {
    eval("console.log(\"hello, coder!\");\n\n//# sourceURL=webpack:///./src/index.js?");
  })
});`

接下来要把template中的内容替换掉,分别替换成我们定义的出口文件和入口文件,用ejs实现比较方便,首先安装ejs
webpack4-js模块实现_第10张图片
然后看一下ejs的使用:

let ejs = require('ejs');
let name = '100';
console.log(ejs.render('<%-name%>', {name}));

执行这段代码(也可以直接zlpack),可以看到已经将name这个变量塞到a标签中,并渲染出来。
下面了解了ejs的使用后就可以进行替换了:
在替换eval()中的内容时,我们考虑到eval执行一个字符串是不能保留原本的换行的,所以用EES6中的模板字符串来替换,模板字符串中的换行和空格是被保留的。而我们在template外面已经使用了模板字符串,eval中再使用的时候要反斜杠进行转义,下面为替换结果:

#! /usr/bin/env node
// 这个文件就要描述如何打包
// 入口文件
let entry = '../src/index.js';  
// 出口文件
let output = '../dist/main.js';
let fs = require('fs');
let script = fs.readFileSync(entry, 'utf8');
let ejs = require('ejs');
let templpate = `
(function(modules) {
	function __webpack_require__(moduleId) {
		var module = {
			exports: {}
		};
		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
		return module.exports;
	}
	return __webpack_require__("<%-entry%>");
})
({
  "<%-entry%>":
  (function(module, exports) {
    eval(\`<%-script%>\`);
  })
});`
// 使用ejs来渲染template
let result = ejs.render(templpate, {
    entry,
    script
});
// result为替换后的结果 最终要写入到output当中
fs.writeFileSync(output, result);
console.log('编译成功');

执行一下zlpack,运行结果:
在这里插入图片描述
说明已经打包成功了,内容已经写入了main.js中,下面我们来看生成的main.js文件内容:
webpack4-js模块实现_第11张图片
可以看出生成了和webpack打包生成的类似文件,下面我们在浏览器执行(index.html中引入main.js),看能否有结果:
在这里插入图片描述
至此,我们就实现了一个单文件的打包过程。

4. 有依赖关系的多个js文件的打包实现

比如我们src下的入口文件index.jsrequire了同级的a.js文件,建立了依赖关系:

// index.js
let result = require('./a.js');
console.log(result);
// a.js
module.exports = "happy coding!"

同样,我们首先看一下源码是怎么打包的,执行npx webpack
webpack4-js模块实现_第12张图片
来看生成的main.js文件,还是将核心的代码留下,其他删掉:

(function(modules) { 
	function __webpack_require__(moduleId) {
		var module = {
			exports: {}
		};
		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
		return module.exports;
	}
	return __webpack_require__("./src/index.js");
})
  ({
    "./src/a.js": (function(module, exports) {
      eval("module.exports = \"happy coding!\"\r\n\n\n//# sourceURL=webpack:///./src/a.js?");
    }),
    "./src/index.js": (function(module, exports, __webpack_require__) {
      eval("let result = __webpack_require__(/*! ./a.js */ \"./src/a.js\");\r\nconsole.log(result);\n\n//# sourceURL=webpack:///./src/index.js?");
    })
  });

可见有依赖的过程和单个文件的大包过程分析起来也大同小异,在立即执行函数中最后返回执行结果:

return __webpack_require__("./src/index.js");

首先执行index.js,由传入立即执行函数的对象

({
    "./src/a.js": (function(module, exports) {
      eval("module.exports = \"happy coding!\"\r\n\n\n//# sourceURL=webpack:///./src/a.js?");
    }),
    "./src/index.js": (function(module, exports, __webpack_require__) {
      eval("let result = __webpack_require__(/*! ./a.js */ \"./src/a.js\");\r\nconsole.log(result);\n\n//# sourceURL=webpack:///./src/index.js?");
    })
  })

可以看出,执行index.js对应的函数中的eval时,又执行了一遍

__webpack_require__(/*! ./a.js */ \"./src/a.js\")

在这个过程中就执行了index.js所依赖的模块a.js,最后返回执行后的结果。
下面我们来实现一下带有依赖模块的打包文件,将我们上一个单文件的zlpack.js存为zlpack1.js,然后再新建zlpack.js文件:

#! /usr/bin/env node
// 这个文件就要描述如何打包
let entry = '../src/index.js';  
let output = '../dist/main.js';
let fs = require('fs');
let path = require('path');
let ejs = require('ejs');
let script = fs.readFileSync(entry, 'utf8');
// 存放我们匹配到的路径以及该路径对应的内容 来处理依赖关系
let modules = [];
// 在这里我们要判断读取的index.js文件内容script中是否有require('./a.js')
// 然后将require('./a.js')替换成require('../src/a.js')
script = script.replace(/require\(['"](.+?)['"]\)/g, function() {
    // 将路径拼接成../src/a.js
    let name = path.join('../src', arguments[1]);  
    // 读取../src/a.js文件中的内容
    let content = fs.readFileSync(name, 'utf8');
    modules.push({
        name,
        content
    });
    return `__webpack_require__('${name}')`;
});
let template = `
(function(modules) { 
	function __webpack_require__(moduleId) {
		var module = {
			exports: {}
		};
		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
		return module.exports;
	}
	return __webpack_require__("<%-entry%>");
})
  ({
   "<%-entry%>": (function(module, exports, __webpack_require__) {
     eval(\`<%-script%>\`);
    })
    <%for(let i=0; i,
        "<%-module.name%>": (function(module, exports, __webpack_require__) {
            eval(\`<%-module.content%>\`);
        })
    <%}%>
  });`
// 使用ejs来渲染template
let result = ejs.render(template, {
    entry,
    script,
    modules
});
// result为替换后的结果 最终要写入到output当中
fs.writeFileSync(output, result);
console.log('编译成功'); 

主要区别在于要在一开始判断入口文件index.js中的内容是否有require关键字引入其他的依赖文件,然后将其路径进行替换,并且得到依赖文件中的内容用于之后在template中用ejs进行替换渲染。最后执行zlpack
在这里插入图片描述
来看一下生成的main.js文件打包内容:

(function(modules) { 
	function __webpack_require__(moduleId) {
		var module = {
			exports: {}
		};
		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
		return module.exports;
	}
	return __webpack_require__("../src/index.js");
})
  ({
   "../src/index.js": (function(module, exports, __webpack_require__) {
     eval(`let result = __webpack_require__('..\src\a.js');
console.log(result);`);
    }), 
    "..\src\a.js": (function(module, exports, __webpack_require__) {
      eval(`module.exports = "happy coding!"`);
    })
  });

我们试一下看是否能在浏览器中执行(index.html):
在这里插入图片描述
可见入口文件index.js所依赖的a.js文件已被执行,并打印出内容。
至此,我们关于有依赖的webpack打包流程就简单的实现了,暂时还没有涉及到更深入的pluginloader的编译打包过程。

你可能感兴趣的:(webpack)