今天就是2019的最后一天,提早祝你们元旦快乐,css
这几年一路走来略有心得,从了编程,也不能荒废了爱写做的手艺,因此平时有空会写点文章,关于本身的职场、人生经验之谈。前端
今天发表下本身对手写webpack的看法(若有不对,欢迎评论交流)
java
若不是生活所迫,谁会把本身弄的一身才华。[ 手动滑稽 ] node
webpack是一个工具,是一个致力于作前端构建的工具。简单的理解:webpack就是一个模块打包机器,它能够将前端的js代码(无论ES6/ES7)、引用的css资源、图片资源、字体资源等各类资源进行打包整合,最后按照预设规则输出到一个或多个js模块文件中,而且能够作到兼容浏览器运行。webpack
下载、安装、建立配置文件(webpack.config.js)、输入配置项、搞定!web
//webpack.config.js
let path = require('path');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename:'main.js',
path: path.resolve(__dirname, 'dist')
}
}复制代码
这是一个最简单的配置,只包含了模式,入口文件以及出口文件,接下来咱们仅先讨论webpack对js文件的打包编程
首先咱们建立一个空项目webpack-test,该项目下有三个js文件数组
//index.js 入口文件
let result = require('./a.js');
console.log(result);
// a.js 引用不b.js文件
let b = require('./b.js');
module.exports = 'a' + b;
// b.js
module.exports = 'b';复制代码
好了,三个js文件建立好了,咱们指望webpack将这三个文件打包成一个文件,而且能正常打印'ab'。浏览器
命令行执行 npx webpack 能够看到生成main.js文件缓存
// main.js 打包后的文件
(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/a.js":
(function(module, exports, __webpack_require__) {
eval("let b = __webpack_require__(/*! ./b.js */ \"./src/b.js\");\r\n\r\nmodule.exports = 'a' + b;\n\n//# sourceURL=webpack:///./src/a.js?");
}),
"./src/b.js":
(function(module, exports) {
eval("module.exports = 'b';\n\n//# sourceURL=webpack:///./src/b.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?");
})
});复制代码
彻底符合预期,三个 js文件打包成一个,并正常打印出'ab'。
咱们刚才打包出的main.js文件,就是webpack最后生成的文件,那么咱们来分析下man.js,首先将里面内容清空
// main.js
(function(modules) {
var installedModules = {};
function __webpack_require__(moduleId) {
// ...
}
return __webpack_require__(__webpack_require__.s = "./src/index.js");
})();复制代码
整个文件只含一个当即执行函数(IIFE)咱们一般叫它webpackBootstrap ,函数内部最后执行__webpack_require__()函数,这个函数咱们暂且不去理会,咱们先来看传入参数(modules)是什么?
{
"./src/a.js":
(function(module, exports, __webpack_require__) {
eval("let b = __webpack_require__(/*! ./b.js */ \"./src/b.js\");\r\n\r\nmodule.exports = 'a' + b;\n\n//# sourceURL=webpack:///./src/a.js?");
}),
"./src/b.js":
(function(module, exports) {
eval("module.exports = 'b';\n\n//# sourceURL=webpack:///./src/b.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?");
})
}复制代码
很明显,参数是一个对象,key对应各代码块相对路径,value则是代码块自己。 接下来咱们看函数内部都作了什么事?
// main.js
(function(modules) {
// 定义installedModules用来缓存_webpack_require_函数加载过的模块
var installedModules = {};
// 定义模块加载函数 __webpack_require__ 且该函数只接收一个参数moduleId
function __webpack_require__(moduleId) {
// ...
}
// 执行模块加载函数 传入参数为 './src/index.js' 即入口文件相对路径
return __webpack_require__(__webpack_require__.s = "./src/index.js");
})();复制代码
能够看到其实主要作了两件事:
一、定义一个模块加载函数 webpack_require。
二、使用加载函数加载入口模块 “./src/index.js”。
接下来咱们分析__webpack_require__函数内部逻辑
// webpack 模块加载函数 __webpack_require__
function __webpack_require__(moduleId) {
// 重复加载则利用缓存
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 第一次被加载的模块 初始化模块对象 并缓存到installedModules对象里
var module = installedModules[moduleId] = {
i: moduleId, // module 对象i 属性值为传入参数moduleId 即 模块相对路径值
l: false, // l 属性值为false 标识未加载
exports: {} // 模块导出对象
}
//
/**
* module.exports 模块导出对象引用 其实就是改变了模块包裹函数内部的 this 指向
* module 当前模块对象引用
* module.exports 模块导出对象引用
* __webpack_require__ 用于在模块中加载其余模块
*/
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// 模块加载标识为已加载
module.l = true;
// 返回当前模块的导出对象引用
return module.exports;
}复制代码
首先,加载函数使用了闭包变量 installedModules,用来将已加载过的模块保存在内存中。 接着是初始化模块对象,并把它挂载到缓存里。而后是模块的执行过程,加载入口文件时 modules[moduleId] 其实就是 ./src/index.js 对应的模块函数。执行模块函数前传入了跟模块相关的几个实参,让模块能够导出内容,以及加载其余模块的导出。最后标识该模块加载完成,返回模块的导出内容。
根据 webpack_require 的缓存和导出逻辑,咱们得知在整个 IIFE 运行过程当中,加载已缓存的模块时,都会直接返回installedModules[moduleId].exports,换句话说,相同的模块只有在第一次引用的时候才会执行模块自己。
模块都经过modules[moduleId].call(module.exports, module, module.exports, webpack_require);这个函数加载进来 下面咱们就进入到 modules[moduleId]代码块内部。 首先加载的确定是入口文件'./src/index.js'
// "./src/index.js":
(function(module, exports, __webpack_require__) {
let result = __webpack_require__("./src/a.js");
console.log(result);
})复制代码
能够看到当加载index.js的时候 先经过__webpack_require__函数先去加载a.js
// "./src/a.js":
(function(module, exports, __webpack_require__) {
let b = __webpack_require__("./src/b.js");
module.exports = 'a' + b;
})复制代码
当去加载a.js时候其实又先去加载b.js
(function(module, exports) {
module.exports = 'b';
})复制代码
经过代码咱们很直观能够看出模块加载流程,只需肯定入口文件,就能够将全部模块按顺序加载进来,而且经过moduleId参数可确保同一模块只需加载一次
经过以上分析,咱们须要作如下工做:
一、拿到入口文件路径(简单 配置信息里就有) 二、拿到各模块相对路径以及源码(须要本身实现) 三、实现模块加载函数 (参照_webpack_require__)
第一步配置咱们本身的打包命令
"scripts": {
"test-webpack": "node test-webpack.js"
}复制代码
第二步建立执行文件
// test-webpack.js
let path = require('path');
let Compiler = require('./lib/Compiler.js');
// 拿到webpack.config.js
let config = require(path.resolve('webpack.config.js'));
// 编译类
let compiler = new Compiler(config);
// 运行代码
compiler.run();复制代码
这个文件里干了两件事,第一拿到打包配置信息(webpack.config.js),第二执行编译函数(Compiler()),咱们的核心代码其实也就在Compiler.js这个文件里
// Compiler.js
let fs = require('fs');
let path = require('path');
class Compiler{
constructor(config){
// 接收传入config 将config挂载到实例config上
this.config = config;
// 入口文件路径
this.entryPath = config.entry ;
// 工做路径
this.root = process.cwd();
// 解析全部的模块依赖
this.modules = {};
}
// run 方法开始编译
run(){
// 第一步须要建立模块依赖关系
// buildModule 函数接收两个参数,一、文件绝对路径,二、是不是主模块
this.buildModule(path.resolve(this.root,this.entryPath),true);
// 将打包后的文件发射出去
this.emitFile();
}
// 建立模块依赖关系
buildModule(modulePath,isEntry){
}
emitFile(){
}
}
module.exports = Compiler;复制代码
以前咱们说过咱们须要作的就是生成实参, buildModule函数须要帮咱们拿到key值 value值
// 建立模块依赖关系
buildModule(modulePath,isEntry){
// 拿到模块id 相对路径 即key值 根据绝对路径和工做路径就可获取相对路径
let moduleId = './' + path.relative(this.root,modulePath);
// 是不是主入口
if( isEntry ){
this.entryPath = moduleId;
}
// 根据路径拿到模块源码
let sourceCode = fs.readFileSync(modulePath,'utf8');
// 解析源码 改造源码 拿到value值
let { resultCode,dependencies } = this.parse(sourceCode,path.dirname(moduleId));
// 把相对路径和模块中的内容对应起来
this.modules[moduleId] = resultCode;
// 递归执行 加载每个依赖模块
dependencies.forEach(dep=>{
this.buildModule(path.resolve(this.root,dep),false);
})
}复制代码
key 值是肯定的,value值须要咱们解析改造,咱们先来看源码是什么
let result = require('./a.js');
console.log(result);复制代码
再来看目标代码是什么
{"./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?");})
}复制代码
对比源码 和 目标代码
一、将require转为__webpack_require__
二、参数'./a.js' 转为 './src/a.js'
这块须要引入一个概念抽象语法树(AST),解析源码用的,这块不对它作过多介绍。
而后须要引入几个辅助包
一、babylon //其做用是把源码转换为ast
二、@babel/traverse //遍历节点
三、@babel/types //替换遍历节点
四、@babel/generator //生成替换的节点
// 解析源码
parse(source,parentPath){
// 源码解析成ast
let ast = babylon.parse(source);
// 存储每一个模块所需依赖的模块路径
let dependencies = [];
// 遍历解析后的源码
traverse(ast,{
CallExpression(p){
let node = p.node; // 对应每一个节点
if( node.callee.name === 'require' ){
// 若是节点是require 将 require 改形成 __webpack_require__
node.callee.name = '__webpack_require__' ;
// 改造require里面的参数 './a.js' > './src/a.js'
let resultPath = node.arguments[0].value; //拿到模块引用名字
resultPath = resultPath + (path.extname(resultPath)?'':'.js') // 判断是否写后缀名
resultPath = './' + path.join(parentPath,resultPath); //拼接上父路径
dependencies.push(resultPath);
node.arguments = [t.stringLiteral(resultPath)] //源码名字改掉
}
}
});
// 生成转换后的代码
let resultCode = generator(ast).code;
// 输出转化后的源码和依赖
return {resultCode,dependencies }
}复制代码
咱们的核心代码就完成了,打印一下转化后的源码
{ './src\\index.js':
'//index.js 入口文件 \nlet result = __webpack_require__("./src\\\\a.js");\n\nconsole.log(result);',
'./src\\a.js':
'// a.js 引用不b.js文件 \nlet b = __webpack_require__("./src\\\\b.js");\n\nmodule.exports = \'a\' + b;',
'./src\\b.js': '// b.js \nmodule.exports = \'b\';' }复制代码
(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;
}
__webpack_require__.m = modules;
__webpack_require__.c = installedModules;
__webpack_require__.d = function(exports, name, getter) {
if(!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, { enumerable: true, get: getter });
}
};
__webpack_require__.r = function(exports) {
if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
__webpack_require__.t = function(value, mode) {
if(mode & 1) value = __webpack_require__(value);
if(mode & 8) return value;
if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
var ns = Object.create(null);
__webpack_require__.r(ns);
Object.defineProperty(ns, 'default', { enumerable: true, value: value });
if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
return ns;
};
__webpack_require__.n = function(module) {
var getter = module && module.__esModule ?
function getDefault() { return module['default']; } :
function getModuleExports() { return module; };
__webpack_require__.d(getter, 'a', getter);
return getter;
};
__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
__webpack_require__.p = "";
return __webpack_require__(__webpack_require__.s = "<%-entryPath%>");
})({
<%for(let in key modules){%>
"<%-key%>":
(function(module, exports, __webpack_require__) {
eval(`<%-modules[key]%>`),
})
<%}%>
})复制代码
模板文件用ejs去作,将webpackBootstrap源码拿过来稍微改造下,将入口文件路径和参数传入,接下来咱们须要写emitFile函数
emitFile(){
// 输出到那个目录下
let main = path.join(this.config.output.path,this.config.output.filename);
// 将代码都出来
let templateStr = fs.readFileSync(path.join(__dirname,'template.ejs'),'utf8');
// 用ejs渲染 获得目标代码块
let code = ejs.render(templateStr,{entryPath:this.entryPath,modules:this.modules});
// 将路径和代码块对应起来
this.assets = {};
this.assets[main] = code;
// 将文件写入
fs.writeFileSync(main,this.assets[main]);
}复制代码
好了 咱们已经实现了webpack打包,接下来咱们看webpack中的loader与插件
什么是loader? webpack只能处理javaScript的模块,若是须要处理其余类型的文件,就须要使用loader进行转化。说的更直白些就是说webpack打包的时候只能识别.js文件,那么它遇到其余类型的文件就不知道该怎么办了,这个时候须要一个函数将其余类型的文件包起来转换成js代码,而后webpack就能够执行打包,而这个函数就是loader。常见的loder有file-loader、url-loader、style-loader、css-loader、less-loader等。下面咱们手写下less-loader和css-loader。
// less-loader.js
// 引入less模块
let less = require('less');
// loader 就是个函数,拿到源码转换成目标代码返回 less-loader 干的事就是把Less代码转换成css
function loader( code ){
let css = '';
less.render(code,function(err,c){
css = c.css;
})
css = css.replace(/\n/g, '\\n');
return css;
}
module.exports = loader;复制代码
less-loader 干的事就是把Less代码转换成css
// style-loader.js
function loader( code ){
let style = `
document.createElement('style');
style.innerHTML = ${JSON.stringify(code)}
document.head.appendChild(style);
`
return style;
}
module.exports = loader;复制代码
style-loader就更简单了,拿到css源码,而后建立style标签,将源码赋值给style标签,最后将style插入头部
好了,咱们知道loader其实就是函数,目的是为了加载非js文件,那么webpack如何执行这些函数呢?
// webpack.config.js
let path = require('path');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename:'main.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.less/,
use: [
path.resolve(__dirname,'loader','style-loader'),
path.resolve(__dirname,'loader','less-loader'),
]
}
]
}
}复制代码
固然须要在webpack.config.js 文件中配置信息告诉webpack什么状况下用什么loader,上面这段配置信息表面,当加载.less文件时,先执行less-loader函数,再执行 style-loader函数。 而后咱们看在加载函数中拿到这些信息后怎么作?
getSource(modulePath){
// 拿到loader配置信息,是个数组
let rules = this.config.module.rules;
let content = fs.readFileSync(modulePath,'utf8');
// 遍历每一个规则来处理
for ( let i = 0; i < rules.length; i++) {
let rule = rules[i];
let { test, use } = rule;
// 从最后一个规则开始执行
let len = use.length - 1 ;
if( test.test(modulePath) ){ // 这个模块须要用loader转换
// 获取对应的loader函数
function normalLoader(){
let loader = require(use[len--]);
content = loader(content)
// 递归调用
if( len >= 0 ){
normalLoader();
}
}
normalLoader();
}
}
return content;
}复制代码
当咱们加载模块的时候,须要拿到loader配置信息,而后匹配什么文件用什么对应的loader函数,最后将处理后的代码返回。
webpack插件是一个具备apply方法的js对象,apply方法会被webpack的compiler(编译器)对象调用,而且compiler对象可在整个编译生命周期内访问。
实现插件功能咱们的compiler里面必须定义生命周期钩子函数,借助tapable(能够实现发布订阅) 实现咱们的钩子函数。
let { SyncHook } = require('tapable');
class Compiler{
constructor(config){
// 接收传入config 将config挂载到实例config上
this.config = config;
// 入口文件路径
this.entryPath = config.entry ;
// 工做路径
this.root = process.cwd();
// 解析全部的模块依赖
this.modules = {};
// 建立钩子函数
this.hooks = {
entryOption: new SyncHook(),
compile: new SyncHook(),
afterCompile: new SyncHook(),
run: new SyncHook(),
emit: new SyncHook(),
done: new SyncHook(),
}
// 若是配置了plugins 参数 拿到每一个插件 并执行其apply方法。
let plugins = this.config.plugins;
if(Array.isArray(plugins)){
plugins.forEach(plugin=>{
plugin.apply(this);
});
}
}
}复制代码
而后将钩子函数放入对应的生命周期内
// run 方法开始编译
run(){
// 执行开始编译钩子函数
this.hooks.compile.call();
// 第一步须要建立模块依赖关系
// buildModule 函数接收两个参数,一、文件绝对路径,二、是不是主模块
this.buildModule(path.resolve(this.root,this.entryPath),true);
// 执行编译完钩子函数
this.hooks.afterCompile.call();
// 将打包后的文件发射出去
this.emitFile();
// 执行发射完钩子函数
this.hooks.emit.call();
// 最终完成钩子函数
this.hooks.emit.call();
}复制代码
下面咱们就编写一个插件, webpack插件的组成:
一个JavaScript函数或者class(ES6语法)。 在它的原型上定义一个apply方法。 指定挂载的webpack事件钩子。 处理webpack内部实例的特定数据。 功能完成后调用webpack提供的回调。
class TestPlugin{
apply(compiler){
compiler.hooks.compile.tap('compile',function(){
console.log('开始编译阶段,执行须要的插件')
})
}
}复制代码
咱们定义了TestPlugin这么一个插件, 它有一个apply方法,该方法内部监听开始编译钩子函数,最后咱们将插件配置上
plugins: [
new TestPlugin(),
]复制代码
运行
$ yarn run test-webpack
yarn run v1.21.0
$ node test-webpack.js
开始编译阶段,执行须要的插件
Done in 2.82s.复制代码
能够看到咱们的插件已经实现了。
这样咱们一个简易版的webpack就已经实现了。
在2019年的最后一天,送朋友们一句话:“永远年轻,永远热泪盈眶。”
来源于个人我的公众号「码农小刘」。
不为流量,只为能结交更多喜欢互联网、热爱前端的朋友。