前言:
ES6
模块化是ECMA
提出的JavaScript
模块化规范,他在语言的层面上实现了模块化。浏览器厂商和node都宣布要原生支持该规范。他将逐渐取代上一篇所提到的CommonJS
和AMD
规范,成为浏览器和服务器通用的模块解决方案。
采用ES6模块化导入和导出时的代码如下:
// 导入
import { name } from './person.js'
// 导出
export const name = 'zl';
ES6
模块虽然是终极模块化方案,但他的缺点在于目前无法直接运行在大部分JavaScript
运行环境下,必须通过工具转化成标准的ES5
后才能够正常运行。webpack
就是这样一种自动化构建工具,能够把源代码转化成发布到线上的可执行JavaScript
、CSS
、HTML
代码,所以学习他的重要性不言而喻。
1. webpack
webpack
能够做的事情有很多:
- 代码转化(各种插件/
loader
):ES6
编译成ES5
,LESS
编译成CSS
等; - 文件优化:压缩
JavaScript
、CSS
、HTML
代码,压缩合并图片等; - 代码分割:提取多个页面的公共代码、提取首屏不需要执行的部分代码让其异步加载;
- 代码合并:在采用模块化的项目当中会有多个模块和文件,需要构建功能把模块分类合并成一个文件;
- 自动刷新(
devserver
):监听本地源代码的变化,自动重新构建、刷新浏览器; - 自动发布:更新完代码后,自动构建出线上发布代码并传输给发布系统。
在webpack
中一切文件皆为模块,通过loader
转化文件,通过plugin
注入钩子,最后输出由多个模块组合成的文件,webpack
专注于构建模块化项目。
2. 安装
-
全局安装
npm install webpack -g
但是这种安装不推荐,不同的人安装的版本不同时,打包会受到影响,打包不成功。 -
本地安装
npm init -y
(这里的-y
表示直接略过所有问答,全部采用默认答案)
npm install webpack webpack-cli -D
(开发依赖,只在开发环境下使用)
安装好之后的我们的文件目录下会多一个node_modules
文件夹,其中的bin
文件夹中有本次安装的webpack
:
在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
中,可以通过设置模式设置当前是开发模式还是生产模式:
执行命令:npx webpack --mode development
再来看生成的打包文件main.js
中的内容:
而且打包出来的文件可以在浏览器使用,在src
下新建index.html
,引入main.js
文件:
<!DOCTYPE html>
<html lang="en">
<head>
<title>practise</title>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<meta name="viewport" id="WebViewport" content="initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
</head>
<body>
<script src="main.js"></script>
</body>
</html>
运行一下,看下浏览器打印出的结果:
下面来看看打包出来的文件中的内容,在我们不引入其他模块了,在入口文件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.youkuaiyun.com/zl13015214442/article/details/96109681。
4. 实现webpack中js的模块
我们自己实现一个类似这样的webpack
,在根目录下新建文件夹zlpack->bin->zlpack.js
我们的目标是在命令行输入zlpack
回车也能像webpack
那样自动打包,我们知道在npm
全局安装的都可以在命令行中使用,首先得有package.json
,在zlpack
路径下npm init -y
:
这样在该目录下就有了package.json
文件,其中bin
这个字段表示执行zlpack
命令的时候执行的是bin
目录下zlpack
这个文件:
下面关键的一步是我们需要把我们构造的zlpack
这个文件夹引入到npm
全局命令下,执行npm link
:
此时我们在zlpack.js
中写一段js
代码:console.log('hello zlpack');
,然后在命令行执行zlpack
,会提示:
是因为我们没有说明当前文件是js
文件,所以npm
分辨不出,而js
文件应该用node
运行,所以应该在zlpack.js
添加一行:
#! /usr/bin/env node // 就是解决了不同的用户node路径不同的问题,可以让系统动态的去查找node来执行你的脚本文件。
console.log('hello zlpack');
然后重新链接一下,再执行zlpack
:
可见已经成功执行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
:
然后看一下ejs
的使用:
let ejs = require('ejs');
let name = '100';
console.log(ejs.render('<a><%-name%></a>', {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
文件内容:
可以看出生成了和webpack
打包生成的类似文件,下面我们在浏览器执行(index.html
中引入main.js
),看能否有结果:
至此,我们就实现了一个单文件的打包过程。
4. 有依赖关系的多个js文件的打包实现
比如我们src
下的入口文件index.js
中require
了同级的a.js
文件,建立了依赖关系:
// index.js
let result = require('./a.js');
console.log(result);
// a.js
module.exports = "happy coding!"
同样,我们首先看一下源码是怎么打包的,执行npx webpack
:
来看生成的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<modules.length; i++) {
let module = modules[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
打包流程就简单的实现了,暂时还没有涉及到更深入的plugin
和loader
的编译打包过程。