babel解析过程(ES6——ast-ES5-graph(依赖图)-build.js)
- 首先加载fs然后读取入口文件,获取入口的文件内容;
import fs = require('fs');
function createAsset(filename){
const content = fs.readFileSync(入口文件,'utf-8')
}
//content打印为入口文件的代码
createAsset('./src/index.js')
- 使用babel/parser(解析器),解析成ast
import fs = require('fs');
+ import parser = require('@/babel/paser')
function createAsset(filename){
const content = fs.readFileSync(入口文件,'utf-8')
//一参为要解析的内容
+ const ast = parser.parse(content,{
+ sourceType: 'module'//模块化解析
+ })
}
//content打印为入口文件的代码
createAsset('./src/index.js')
+使用babel/traverse(遍历ast),进行对ast树进行遍历import节点获取导入的文件内容,例如:
import fs = require('fs');
import parser = require('@/babel/paser')
+ import traverse = require('@/babel/traverse').default
function createAsset(filename){
const content = fs.readFileSync(入口文件,'utf-8')
//一参为要解析的内容
const ast = parser.parse(content,{
sourceType: 'module'//模块化解析
})
+ const dependencied = [];//因为不只有一个import
+ //遍历ast,获取import节点,获取对应内容,即文件路径,例如import './info.js',会获取到'./info.js'这个内容
+ traverse(ast,{
+ ImportDeclaration: ({node}) => {
+ //向数组存储import的内容
+ dependencied.push(node.source.value);
+ }
+ })
}
//content打印为入口文件的代码
createAsset('./src/index.js')
- 使用babel/core进行把es6变成es5语法
import fs = require('fs');
import parser = require('@/babel/paser')
import traverse = require('@/babel/traverse').default
+ import babel = require('@/babel/core')
function createAsset(filename){
const content = fs.readFileSync(入口文件,'utf-8')
//一参为要解析的内容
const ast = parser.parse(content,{
sourceType: 'module'//模块化解析
})
const dependencied = [];//因为不只有一个import
//遍历ast,获取import节点,获取对应内容,即文件路径,例如import './info.js',会获取到'./info.js'这个内容
traverse(ast,{
ImportDeclaration: ({node}) => {
//向数组存储import的内容
dependencied.push(node.source.value);
}
})
+ //ast--ES5 code
+ const {code} = babel.transformFromAstSync(ast,null,{
+ presets: ['@/babel/preset-env']
+ })
}
//content打印为入口文件的代码
createAsset('./src/index.js')
- 最后返回对象:
import fs = require('fs');
import parser = require('@/babel/paser')
import traverse = require('@/babel/traverse').default
import babel = require('@/babel/core')
+ let ID = 0;
function createAsset(filename){
const content = fs.readFileSync(入口文件,'utf-8')
//一参为要解析的内容
const ast = parser.parse(content,{
sourceType: 'module'//模块化解析
})
const dependencied = [];//因为不只有一个import
//遍历ast,获取import节点,获取对应内容,即文件路径,例如import './info.js',会获取到'./info.js'这个内容
traverse(ast,{
ImportDeclaration: ({node}) => {
//向数组存储import的内容
dependencied.push(node.source.value);
}
})
//ast--ES5 code
const {code} = babel.transformFromAstSync(ast,null,{
presets: ['@/babel/preset-env']
})
+ let id = ID;
+ return {
+ id,
+ filename,//'./src/index.js'
+ dependencied,'['./info.js']'
+ code//es5代码
+ }
}
//content打印为入口文件的代码
createAsset('./src/index.js')
- 构建文件依赖图:,根据获取到的入口文件依赖,然后获取逐层引入的依赖,例如入口文件引入的依赖为info.js,而info.js引入了consts.js,现在把它们都扁平化的存储在一个数组上。
+ const path = require('path')
...
funtion createGrap(entry){
const mainAsset = createAsset(entry)//获取到一个文件的对象而已{id: 1,filename:'./index.js',decepenice: ['./info.js'],code: 当前index.js文件转换后的ES5code}
const queue = [mainAsset]
//使用递归也可以
for(const asset of queue){
const dirname = path.dirname(asset.filename)
asset.mapping = {};//添加映射,后面可以直接根据id找到该模块依赖的模块
asset.dependencies.forEach(realtivePath => {
const absolutePath = path.join(dirname,relativePath)
const child = createAsset(absolutePath)
asset.mapping[realtivePath] = child.id;//该默认的依赖映射
queue.push(child)
})
}
return queue
}
- createAsset('./src/index.js')
+ const graph = createGrap('./src/index.js')
最后的queue形式为如图所示:
- 实现CMD API整合模块化代码(graph(依赖图)->bundle.js)
...
function bundle(graph){
//转换为['function(){ es5 code }',{'依赖路径': 该依赖id}]形式
const modules = ''
graph.forEach(mo=>{
modules+ = `
${mod.id}: [
//因为code中的es5代码包含了require,module,exports这些所以要作为参数传入,后面就实现这些参数即可
function (require,module,exports){
$(mod.code}
},
${JSON.stringify(mod.mapping)}
]
`
})
//构造立即调用函数表达式,因为打包出去的就是字符串
//module其实就是改造好的graph,更加直观的可以知道依赖关系
const result = `(function(modules){
//实现require
fuction require(id){
const [fn,mapping] = modules[id];
//把该模块的es5代码使用require导入的相对路径转为替换id,然而可以作为fn的第一个参数require传入
function loaclRequire(relativePath){
return require(mapping[relativePath]);
}
//二三参数
const module = {
exports: {}
}
fn(loaclRequire,module,module.exports)//实现fn的三个方法require,module,exports
}
require(0);//执行到入口函数,拿到key为0的值,即入口文件转换后的数组([fun,mapping])
})({${modules}})`
}
const graph = createGrap('./src/index.js')
+ const result = bundle(graph)
最后验证把result打印出来,在console运行,如果可以进行对应的代码输出说明成功。
module大概为如下图所示作为参数传入:
webpack打包过程
- 打包后之后的文件
-
为一个自执行函数
-
参数通过对象的形式传递
- key为文件路径 value为一个使用eval函数包裹的es5code的函数
- 如果存在多个相互依赖的文件,参数则为多个key/value对象
-
怎么把所有文件打包之后形成一个文件,通过_webpack_require_
-
原理通过递归方式不停的调用自己
-
webpack 构建流程
Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程 :
- 初始化参数:从配置文件和 Shell 语句(即命令行)中读取与合并参数,得出最终的参数。
- 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译。而run方法就是使用@babel/parser解析成AST语法树。
- 确定入口:根据配置中的 entry 找出所有的入口文件。
- 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理。
- 完成模块编译:在经过第 4 步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系。
- 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会。
- 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。
简易实现webpack
我们打包后看到的bundle.js内容大概为如图所示:
// 定义一个立即执行函数,传入生成的依赖关系图
;(function(graph) {
// 重写require函数
function require(moduleId) {
// 找到对应moduleId的依赖对象,调用require函数,eval执行,拿到exports对象
function localRequire(relativePath) {
return require(graph[moduleId].dependecies[relativePath]) // {__esModule: true, say: ƒ say(name)}
}
// 定义exports对象
var exports = {}
;(function(require, exports, code) {
// commonjs语法使用module.exports暴露实现,我们传入的exports对象会捕获依赖对象(hello.js)暴露的实现(exports.say = say)并写入
eval(code)
})(localRequire, exports, graph[moduleId].code)
// 暴露exports对象,即暴露依赖对象对应的实现
return exports
}
// 从入口文件开始执行
require('./src/index.js')
})({
'./src/index.js': {
dependecies: { './hello.js': './src/hello.js' },
code: '"use strict";\n\nvar _hello = require("./hello.js");\n\ndocument.write((0, _hello.say)("webpack"));'
},
'./src/hello.js': {
dependecies: {},
code:
'"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\nexports.say = say;\n\nfunction say(name) {\n return "hello ".concat(name);\n}'
}
})
下面代码为从compiler.run()=>ast树的解析=>ast树遍历获取收集所需依赖=>转换为es5代码=>遍历解析所有依赖项,用一维数组形式表现依赖关系图=>重写require函数,输出bundle方法
const fs = require('fs')
const path = require('path')
const options = require('./webpack.config')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const { transformFromAst } = require('@babel/core')
const Parser = {
getAst: path => {
// 读取入口文件
const content = fs.readFileSync(path, 'utf-8')
// 将文件内容转为AST抽象语法树
return parser.parse(content, {
sourceType: 'module'
})
},
getDependecies: (ast, filename) => {
const dependecies = {}
// 遍历所有的 import 模块,存入dependecies
traverse(ast, {
// 类型为 ImportDeclaration 的 AST 节点 (即为import 语句)
ImportDeclaration({ node }) {
const dirname = path.dirname(filename)
// 保存依赖模块路径,之后生成依赖关系图需要用到
const filepath = './' + path.join(dirname, node.source.value)
dependecies[node.source.value] = filepath
}
})
return dependecies
},
getCode: ast => {
// AST转换为code
const { code } = transformFromAst(ast, null, {
presets: ['@babel/preset-env']
})
return code
}
}
class Compiler {
constructor(options) {
// webpack 配置
const { entry, output } = options
// 入口
this.entry = entry
// 出口
this.output = output
// 模块
this.modules = []
}
// 构建启动
run() {
// 解析入口文件
const info = this.build(this.entry)
this.modules.push(info)
this.modules.forEach(({ dependecies }) => {
// 判断有依赖对象,递归解析所有依赖项
if (dependecies) {
for (const dependency in dependecies) {
this.modules.push(this.build(dependecies[dependency]))
}
}
})
// 生成依赖关系图
const dependencyGraph = this.modules.reduce(
(graph, item) => ({
...graph,
// 使用文件路径作为每个模块的唯一标识符,保存对应模块的依赖对象和文件内容
[item.filename]: {
dependecies: item.dependecies,
code: item.code
}
}),
{}
)
this.generate(dependencyGraph)
}
build(filename) {
const { getAst, getDependecies, getCode } = Parser
const ast = getAst(filename)
const dependecies = getDependecies(ast, filename)
const code = getCode(ast)
return {
// 文件路径,可以作为每个模块的唯一标识符
filename,
// 依赖对象,保存着依赖模块路径
dependecies,
// 文件内容
code
}
}
// 重写 require函数 (浏览器不能识别commonjs语法),输出bundle
generate(code) {
// 输出文件路径
const filePath = path.join(this.output.path, this.output.filename)
// 懵逼了吗? 没事,下一节我们捋一捋
const bundle = `(function(graph){
function require(module){
function localRequire(relativePath){
return require(graph[module].dependecies[relativePath])
}
var exports = {};
(function(require,exports,code){
eval(code)
})(localRequire,exports,graph[module].code);
return exports;
}
require('${this.entry}')
})(${JSON.stringify(code)})`
// 把文件内容写入到文件系统
fs.writeFileSync(filePath, bundle, 'utf-8')
}
}
new Compiler(options).run()
webpack优化
-
webpack生成和开发环境使用插件差异
-
webpack开发和生产时候常用插件
- 开发
- decServe
- 生产
- HtmlWebpackPlugin:根据模板html生成index.html,也可以配置html压缩
- mini-css-extract-plugin: 将css提取出来单独文件,原因为css要变成js,js文件最后在创建style标签进行内容填充,如果js文件就会过大加载就会变慢,会出现闪屏现象
- postcss-loder、postcss-perset-env插件:进行兼容性处理,例如自动添加样式前缀
- optimizeCssAssetsWebpackPlugin:css压缩
- 开发
-
自带优化
- 函数名字简化
- tree-sharking: 在生产环境下,依赖关系的解析时,会把无用的代码不进行打包,例如
import {a} from 'aa.js',只会引入aa.js中的a代码,aa.js除了a的代码都不会被打包在dist的输出文件中。 - scope-hositing: 作用域提升,如果代码为变量定义的也不会打包的代码中,例如
var a = 1;var b=2; console.log(a+b)==>console.log(3). - source-map: 打包后文件和源文件的映射
-
手动配置的优化
- 速度
- 多线程打包(happypack):下载loader,一般配合babel-loader使用。但是体积小的文件打包时间会比较慢
- 体积
- code split(代码分割):按需加载文件
- 多入口拆分:node_module每个为一个文件,一个入口即为一个chunk同时也会是一个bundle
module.exports = { //单入口 //entry: './scr/index.js', //多入口 entry: { //多入口:有一个入口,最终就输出一个bundle index: './src/js/index.js', test: './src/js/test.js' }, output:{ //多入口为生成多个输出文件,因此命名时候主要 //使用contenthash原因为打包输出的时候,只有文件内容更改过才会生成一个新的hash值,内部不变生成的hash就一直不变 filename: 'js/[name].[contenthash:10].js', path: resolve(__dirname,'build') } }- optiomization拆分
- js使用import实现按需加载(vue路由也是使用这种方式实现)
- PWA:
- IgnorePlugin:moment时间插件该插件为很多语言包,我们可以使用IgnorePlugin把不需要的语言包删掉
- externals:把一些第三方库不打包到压缩文件去,才用cdn形式在html上引入,例如jquery。然后配置:
//不打包 externals: { 'jquery': '$' } //不解析 modules: { noParse: /jquery/, }- dll: 如果按照所有优化配置,发现打包体积还是过大时,可以使用动态链接库(提取到一个单独文件打包,最后放在cdn上引入使用)
- code split(代码分割):按需加载文件
- 其他需要引入的插件
- autoPrefixer: 加载前缀
- babel-loader: 解析es6语法
- 速度