明确webpack实现的性能
实质上,webpack 是一个古代 JavaScript 应用程序的动态模块打包器(module bundler)。 当 webpack 解决应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中蕴含应用程序须要的每个模块,而后将所有这些模块打包成一个或多个 bundle。
咱们的代码次要实现以下四步
graph TD A(1.找到入口文件) --> B(2.解析入口文件并提取出依赖) B --> C{3.递归发明出依赖图} C --> D(4.把所有文件打包成一个文件)
开始开发
1.目录下新建三个js源文件
- entry.js
import message from './message.js'console.log(message)
- message.js
import name from "./name.js"export default `${name} is a girl`
- name.js
export const name = 'Yolanda'
梳理下依赖关系,咱们的入口文件是entry.js,entry依赖message,message依赖name。
2.新建一个mywebpack.js文件,首先读取一下entry入口文件中的内容。
读取文件须要用到node的一个根底api-fs(文件系统),fs.readFileSync能够同步拿到js文件中的代码内容。
- mywebpack.js
const fs = require('fs')function creatAsset (filename){ const content = fs.readFileSync(filename,'utf-8') console.log(content)}creatAsset('./source/entry.js')
在当前目录下运行一下命令, 看一下输入node mywebpack.js
输入内容为entry.js中的代码:
import message from './message.js';console.log(message);
3. 剖析ast,思考如何解析出entry.js文件中的依赖
能够应用ast工具 https://astexplorer.net/
看一下entry.js文件的ast是什么?
3.1 能够看到最下级是一个File, File中蕴含换一个program, 就是咱们的程序
3.2 在program的body属性里, 就是咱们各种语法的形容
3.3 能够看到第一个就是 ImportDeclaration, 也就是引入的申明.
3.4 ImportDeclaration里有一个source属性, 它的value就是引入的文件地址 './message.js'
4.生成entry.js的ast
首先装置一个Babylon(基于Babel的js解析工具)npm i babylon
- mywebpack.js
const fs = require('fs');const babylon = require('babylon');function createAsset(filename) { const content = fs.readFileSync(filename, 'utf-8'); const ast = babylon.parse(content, { sourceType: 'module' }); console.log(ast);}createAsset('./source/entry.js');
能够看到输入了一个Object, 这就是咱们entry.js的AST.
5.基于这个ast,找到entry.js的ImportDeclaration中的source.value
首先,须要遍历出ImportDeclaration节点,那就须要一个工具:babel-traverse(能够像遍历对象一样遍历ast中的节点)npm i babel-traverse
而后利用它遍历并获取到对应节点,提供一个函数来操作此节点。
- mywebpack.js
const fs = require('fs');const babylon = require('babylon');const traverse = require('babel-traverse').default;function createAsset(filename) { const content = fs.readFileSync(filename, 'utf-8'); const ast = babylon.parse(content, { sourceType: 'module' }); traverse(ast, { ImportDeclaration: ({ node }) => { console.log(node) } })}createAsset('./source/entry.js');
6.获取entry.js的依赖
可能会呈现多个依赖,这里须要建一个数组存储。
- mywebpack.js
const fs = require('fs');const babylon = require('babylon');const traverse = require('babel-traverse').default;function createAsset(filename) { const content = fs.readFileSync(filename, 'utf-8'); const ast = babylon.parse(content, { sourceType: 'module' }); const dependencies = []; traverse(ast, { ImportDeclaration: ({ node }) => { dependencies.push(node.source.value); } }) console.log(dependencies);}createAsset('./source/entry.js');
能够自行打印一下dependencies数组看看.这里输入的是一个包含ImportDeclaration.source.value的数组。
7.优化creatAsset函数,减少id用于辨别文件
- mywebpack.js
const fs = require('fs');const babylon = require('babylon');const traverse = require('babel-traverse').default;let ID = 0;function createAsset(filename) { const content = fs.readFileSync(filename, 'utf-8'); const ast = babylon.parse(content, { sourceType: 'module' }); const dependencies = []; traverse(ast, { ImportDeclaration: ({ node }) => { dependencies.push(node.source.value); } }) const id = ID++; return { id, filename, dependencies }}const mainAsset = createAsset('./source/entry.js');console.log(mainAsset);
运行一下看看, 是不是返回了正确的后果.
8.当初咱们有了单个文件的依赖了,接下来尝试建设模块间的依赖关系
新建一个createGraph函数,在这个函数里援用createAsset。
同时entry这个参数是动静的,所以createGraph接管entry这个参数。
- mywebpack.js - createGraph
function createGraph(entry) { const mainAsset = createAsset(entry);}const graph = createGraph('./source/entry.js');console.log(graph);
申明一个数组:allAsset用于存储全副的asset
- mywebpack.js - createGraph
function createGraph(entry) { const mainAsset = createAsset(entry);}const graph = createGraph('./source/entry.js');console.log(graph);
9.把相对路径转换为绝对路径
咱们在dependencies中存储的都是相对路径,然而咱们须要绝对路径能力拿到模块的Asset,这个时候要想方法拿到每个模块的绝对路径。
这里用到了node的另一个根底API:path,其中用到了它的两个办法:
1.path.dirname(获取以后文件的路径名)
2.path.join(拼接门路)
- mywebpack.js - createGraph
function createGraph(entry) { const mainAsset = createAsset(entry); const allAsset = [mainAsset]; for (let asset of allAsset) { const dirname = path.dirname(asset.filename); asset.dependencies.forEach(relativePath => { const absoultePath = path.join(dirname, relativePath); const childAsset = createAsset(absoultePath); }); }}
10.咱们还须要一个map,用于记录dependencies和childAsset之间的关系
map能够存储模块间的依赖关系,不便后续的查找。
- mywebpack.js - createGraph
function createGraph(entry) { const mainAsset = createAsset(entry); const allAsset = [mainAsset]; for (let asset of allAsset) { const dirname = path.dirname(asset.filename); asset.mapping = {}; asset.dependencies.forEach(relativePath => { const absoultePath = path.join(dirname, relativePath); const childAsset = createAsset(absoultePath); asset.mapping[relativePath] = childAsset.id; }); }}
11.遍历所有文件,造成最终的依赖图
外围的一步,就一行代码,实现所有模块的遍历。
- mywebpack.js - createGraph
function createGraph(entry) { const mainAsset = createAsset(entry); const allAsset = [mainAsset]; for (let asset of allAsset) { const dirname = path.dirname(asset.filename); asset.mapping = {}; asset.dependencies.forEach(relativePath => { const absoultePath = path.join(dirname, relativePath); const childAsset = createAsset(absoultePath); asset.mapping[relativePath] = childAsset.id; //⬇️要害一步,向数组中推动子依赖 allAsset.push(childAsset); }); } return allAsset;}
获取到了文件依赖图之后,咱们须要把所有文件打包成一个文件而后输入。
12.新增一个办法bundle
最初一个函数了!胜利在望~
bundle实现的是通过graph来发明一个立刻执行函数,立刻执行函数的参数有全副模块的代码体。
- mywebpack.js - bundle
function bundle(graph) { //自执行函数的参数 let modules = ''; //遍历所有模块,拼接modules字符串:每个id对应一个value,value里要包含该module的可执行代码 graph.forEach(module => { modules += `${module.id}:[ ],`; }) //返回的后果 const result = ` (function() { })({${modules}}) `;}
到当初咱们会发现,graph里只记录了每个模块对应的依赖关系,并没有包含可执行的代码体。
所以下一步,咱们须要获取每个模块的代码体。
批改createAsset函数,获取到代码体且编译。
首先咱们会用到babel-core,不便各个插件剖析语法进行相应的解决。
装置他!npm i babel-core
还会用到 babel-preset-env,它能够依据开发者的配置,按需加载插件,能够作为预设来编译代码。
装置他!npm i babel-preset-env
- mywebpack.js - createAsset
function createAsset(filename) { const content = fs.readFileSync(filename, 'utf-8'); const ast = babylon.parse(content, { sourceType: 'module' }); const dependencies = []; traverse(ast, { ImportDeclaration: ({ node }) => { dependencies.push(node.source.value); } }) const id = ID++; const { code } = babel.transformFromAst(ast, null, { // 第三个参数, 通知babel以什么形式编译咱们的代码. 这里咱们就用官网提供的preset-env, 编译es2015+的js代码. // 当然还有其余的各种预设, 能够编译ts, react等等代码. presets: ['env'] }) return { id, filename, dependencies, code }}
13.把获取到的code放到result中
依据commonJs的标准,每个模块的代码函数须要接管三个参数:require,module,exports。
- module变量代表以后的模块,它是一个变量,其中exports属性就是对外的模块。加载某个模块,其实就是加载该模块的module.exports属性。
mywebpack.js - bundle
function bundle(graph) { let modules = ''; graph.forEach(module => { modules += `${module.id}:[ function(require, module, exports) { ${module.code} }, ],`; }) const result = ` (function() { })({${modules}}) `; return result;}
接下来开始实现require函数,首先须要把mapping传到对应modules的value外面。
function bundle(graph) { let modules = ''; graph.forEach(module => { modules += `${module.id}:[ function(require, module, exports) { ${module.code} }, ${JSON.stringify(module.mapping)}, ],`; }) const result = ` (function() { })({${modules}}) `; return result;}
requier要接管一个参数,来示意引入了哪些代码,这个时候就能够用mapping的映射关系,应用id找到引入的代码。
function bundle(graph) { let modules = ''; graph.forEach(module => { modules += `${module.id}:[ function(require, module, exports) { ${module.code} }, ${JSON.stringify(module.mapping)}, ],`; }) // 取出的fn是下面定义的modules里的function,mapping是${JSON.stringify(module.mapping)} const result = ` (function(modules) { function require(id) { const [fn, mapping] = modules[id]; function localRequire(relativePath) { return require(mapping[relativePath]); } const module = { exports: {}}; fn(localRequire, module, module.exports); return module.exports; } require(0); })({${modules}}) `; return result;}
当当当!到这里就根本实现了咱们的简易版webpack,这个时候能够node运行下mywebpack.js,把输入的后果代码复制到浏览器里看看能不能失常运行。
浏览器打印出:Yolanda is a girl 就意味着你的webpack曾经失常运行起来了!给本人鼓个掌吧~
最初能够优化一下流程,每次都复制到浏览器看输入后果有点吃力,能够配置下package.json用命令行操作。npm init
- package.json
"scripts": { "build": "rm -rf dist.js && node mywebpack.js > dist.js"},
最初运行
npm run build
.
功败垂成!
贴上mywebpack.js残缺代码:
// 步骤:// 1.找到一个入口文件;// 2.解析这个入口文件,提起入口文件的依赖;// 3.解析入口文件中依赖的依赖,递归的发明一个依赖图,能够形容文件间的依赖关系;// 4.将所有的文件打包成一个文件。//node外面的fs模块能够读取文件const fs = require('fs')//node的根底api 能够帮忙拿到绝对路径const path = require('path')//babel(语法转换)的解析工具const babylon = require('babylon')//babel-traverse :能够像遍历对象一样遍历ast中的节点const tranverse = require('babel-traverse').default;//babel-core:把js文件解析为astconst babel = require('babel-core')//记录IDlet ID = 0;//拿到该file的id,filename 以及 依赖的数组function createAsset(filename){ //同步的读取文件 const content =fs.readFileSync(filename,'utf-8') //转化为ast找到相应节点 const ast = babylon.parse(content,{ sourceType:'module' }) //定义一个数组用于贮存依赖 const dependencies = [] //定义一个id 每调用一次creatAsset函数id++ let id = ID++ //遍历拿到ImportDeclaration中的source中的value tranverse(ast,{ ImportDeclaration:({node})=>{ //拿到node中的source的value push到贮存依赖的数组里 dependencies.push(node.source.value) } }) const{ code } = babel.transformFromAst(ast , null,{ presets:['env'] }) //输入一个对象,其中包含id,filename以及它所依赖的dependencies return { id, filename, dependencies, code }}//创立依赖图 外围!!function createGraph(entry){ const mainAsset = createAsset(entry) const allAsset = [mainAsset] for( let asset of allAsset){ //拿到以后的dirname const dirname= path.dirname(asset.filename) //给asset新增一个mapping对象,不便后续查找 asset.mapping = {} //将dependencies中的相对路径转化为绝对路径 asset.dependencies.forEach(relativePath=>{ const absolutePath = path.join(dirname,relativePath) //拿到绝对路径下的子元素的asset内容 const childAsset = createAsset(absolutePath) //存储对应关系 asset.mapping[relativePath]=childAsset.id //递归的外围一行代码!! allAsset.push(childAsset) }) } return allAsset}//创立整体后果代码块(须要立刻执行)接管module作为参数function bundle (graph){ let modules = '' graph.forEach(module=>{ modules+=` ${module.id}:[ function(require,module,exports){ ${module.code} }, ${JSON.stringify(module.mapping)} ], ` }) //输入的后果为字符串 const result = `( function(modules){ function require(id){ const [fn,mapping] = modules[id]; function localRequire(relativePath){ return require(mapping[relativePath]) } const module = { exports:{}} fn(localRequire,module,module.exports) return module.exports } require(0) } )({${modules}})` return result}//传入相对路径const graph = createGraph('./source/entry.js')const res = bundle(graph)console.log(res)