简介
当前市场下,无论angular,vue还是react,都离不开的构建工具的编译与协助,目前打包工具大多基于webpack、gulp
gulp侧重于前端开发的 整个过程 的控制管理(像是流水线)
webpack更侧重于模块打包
我们公司的项目基本上都是基于webpack打包的,了解webpack的构建原理对我们在业务上的理解会更加方便
本次分享会带你了解webpack的整体构建流程,loader和plugin的原理。在往后的业务中,你也能根据业务的需求去实现一个plugin或者loader
什么是webpack
webpack可以看做是模块打包机:它做的事情是,分析你的项目结构,找到JavaScript模块以及其它的一些浏览器不能直接运行的拓展语言(Scss,TypeScript等),并将其打包为合适的格式以供浏览器使用。
webpack官网:webpack.js.org/
webpack核心概念
1.入口(entry)
webpack 创建应用程序所有依赖的关系图。图的起点被称之为入口起点(entry point)。(通常为src/index.js)
2.输出(output)
webpack 的 output 属性描述了如何处理归拢在一起的代码(bundled code)。output选项可以控制webpack如何向硬盘写入编译文件 (通常为dist)
3.loader
webpack把每个文件(.css, .html, .scss, .jpg, etc.)都作为模块处理。然而webpack自身只理解JavaScript。webpack loader 在文件被添加到依赖图中时,将文件源代码转换为模块。
4.插件-plugins
插件目的在于解决 loader无法实现的其他事。loader仅在每个文件的基础上执行转换,而插件(plugins) 常用于(但不限于)在打包模块的 “compilation” 和 “chunk” 生命周期执行操作和自定义功能。
\
分析webpack打包文件
(() => {
"use strict";
var __webpack_modules__ = ({
"./src/index.js":
((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _pageA__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./pageA */ "./src/pageA.js");\n\nfunction add(a, b) {\n return a + b\n}\n\nlet addNum = add(_pageA__WEBPACK_IMPORTED_MODULE_0__.a, 2)\nconsole.log(addNum,'---')\n\n//# sourceURL=webpack://webpackShare/./src/index.js?");
}),
"./src/pageA.js":
((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ "a": () => (/* binding */ a)\n/* harmony export */ });\nconst a = 'a'\n\n\n//# sourceURL=webpack://webpackShare/./src/pageA.js?");
})
});
var __webpack_module_cache__ = {};
function __webpack_require__(moduleId) {
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
var module = __webpack_module_cache__[moduleId] = {
exports: {}
};
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
return module.exports;
}
(() => {
__webpack_require__.d = (exports, definition) => {
for (var key in definition) {
if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
Object.defineProperty(exports, key, {
enumerable: true,
get: definition[key]
});
}
}
};
})();
(() => {
__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
})();
(() => {
__webpack_require__.r = (exports) => {
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, {
value: 'Module'
});
}
Object.defineProperty(exports, '__esModule', {
value: true
});
};
})();
var __webpack_exports__ = __webpack_require__("./src/index.js");
})();
1.modules
webpack_modules 包含所有解析路径和代码
\
2.require函数
webpack_require 函数对不同路径下的代码进行执行
3.入口文件
执行__webpack_require__函数,传入入口文件路径,递归执行依赖的代码
webpack是如何工作的
1.载入配置文件,拿到所有配置项
2.接入tapable事件流管理,初始化插件,在插件监听的不同方法触发回调函数
3.分析入口文件代码,处理loaders并解析生成AST
4.分析AST的特定语法,比如import,require等,寻找依赖,生成ES5代码
5.循环分析依赖,递归所有依赖,组合生成文件依赖图
6.通过执行函数eval执行代码
如何实现一个webpack?
1.读取配置文件,执行打包流程
const path = require('path');
const config = require(path.resolve('webpack.config.js'));
const WebpackCompiler = require('../lib/webpackCompiler.js');
const webpackCompiler = new WebpackCompiler(config);
webpackCompiler.run();
2.初始化事件流,执行插件方法
class WebpackCompiler {
constructor(config) {
this.config = config
this.modules = {}
this.root = process.cwd()
this.entryPath = path.join(this.root, this.config.entry) // 获取入口文件路径
this.hooks = {
entryInit: new tapable.SyncHook(),
beforeCompile: new tapable.SyncHook(),
afterCompile: new tapable.SyncHook(),
afterPlugins: new tapable.SyncHook(),
afterEmit:new tapable.SyncWaterfallHook(['hash'])
}
const plugins = config.plugins
if (Array.isArray(plugins)) {
plugins.forEach(item => {
item.apply(this)
})
}
}
}
3.执行run函数开始构建
run() {
this.hooks.entryInit.call()
this.hooks.beforeCompile.call()
this.makeDependenciesGraph(this.entryPath)
this.hooks.afterCompile.call()
this.outputFile()
this.hooks.afterPlugins.call()
this.hooks.afterEmit.call()
}
4.解析文件代码,循环递归依赖
makeDependenciesGraph(entry) { // 递归查找依赖全部找到所有文件代码和关联关系
let entryModule = this.parse(entry)
let graphArr = [entryModule]
for (let i = 0; i < graphArr.length; i++) {
const item = graphArr[i]
const {
dependencies
} = item
if (dependencies) {
for (let j in dependencies) {
graphArr.push(this.parse(dependencies[j]))
}
}
}
const graph = {}
graphArr.forEach(i => {
graph[i.filename] = {
dependencies: i.dependencies,
code: i.sourceCode
}
})
this.modules = graph // 生成的图谱代码会有一个require函数,需要自己写一个require函数,以及exports对象
}
parse(modulePath) {
const source = this.getSourceByPath(modulePath); //根据路径拿到源码
let ast = parser.parse(source, {
sourceType:'module'
})
let dependencies = {}
traverse(ast, {
ImportDeclaration(p) {
let node = p.node
const dirname = path.dirname(modulePath)
const moduleName = path.join(dirname, node.source.value)
dependencies[node.source.value] = moduleName
}
})
let sourceCode = babel.transformFromAstSync(ast,
null, {
presets: ["@babel/preset-env"]
}).code
return {
filename: modulePath,
sourceCode,
dependencies
}
}
AST Explorer:astexplorer.net/
5.生成代码文件
outputFile() {
let modules = JSON.stringify(this.modules)
let code = `(function(graph){
function require(module) {
function localRequire(relativePath) { //找到真实路径
return require(graph[module].dependencies[relativePath]);
}
var exports = {};
(function(require, exports, code){
//将require和exports传入进去eval里面code里的代码
eval(code)
})(localRequire, exports, graph[module].code);
return exports; // 让其他模块可以使用
};
require('${this.entryPath}') //把入口传入
})(${modules});`
let outPath = path.join(this.config.output.path, this.config.output.filename)
findHasPath(this.config.output.path)
fs.writeFileSync(outPath, code)
}
如何写一个plugin
plugin主要是通过webpack初始化的时候执行plugin里的apply方法,给apply方法传递了webpack实例,通过实例里的tapable的不同钩子触发时机不同回调到不同的plugin的回调函数
class InitPlugin {
apply(compiler) {
compiler.hooks.entryInit.tap('Init',function (res) {
console.log('开始编译')
})
}
}
class JsCopyPlugin {
apply(compiler) {
compiler.hooks.afterEmit.tap('JsCopyPlugin', function (res) {
const randNum = parseInt(Math.random() * 100000000)
const {
path: configPath,
filename
} = compiler.config.output
fs.copyFile(path.join(configPath, filename), path.join(configPath, filename.split('.')[0] +'.' + randNum + '.js'), function (err) {
if (err) console.log(err)
delFileByName(path.join(configPath, filename))
})
return randNum
})
}
}
class CleanDistPlugin {
apply(compiler) {
compiler.hooks.beforeCompile.tap('CleanDistPlugin', function (res) {
delFileFolderByName(path.join(compiler.config.output.path))
})
}
}
class HtmlReloadPlugin {
constructor(options) {
this.options = options
}
apply(compiler) {
let self = this
compiler.hooks.afterEmit.tap('HtmlReloadPlugin', function (res) {
let root = process.cwd()
let content = fs.readFileSync(path.join(root, self.options.src), 'utf-8')
content = content.replace('main.js', `main.${res}.js`)
fs.writeFileSync(path.join(compiler.config.output.path,'./index.html'),content)
})
}
}
如何写一个loader
loader是通过webpack匹配正则后再读取文件源码执行loader函数,将源码传递给loader函数进行处理,loader将处理完后的源码再返回给webpack进行babel的处理
less-loader
const less = require('less')
function loader(source) {
let css = ''
less.render(source,function (err,output) {
css = output.css
})
let style = `
let style =document.createElement('style')
style.innerHTML = \n${JSON.stringify(css)}
document.head.appendChild(style)
`
return style
}
module.exports = loader
style-lodaer
function loader(source) {
let style = `
let style =document.createElement('style')
style.innerHTML = \n${JSON.stringify(source)}
document.head.appendChild(style)
`
return style
}
module.exports = loader