webpack

webpack执⾏流程

简略流程

image-20220909135015955

图示流程理解分析:

  1. 读取⼊⼝⽂件;

  2. 基于 AST(抽象语法树) 分析⼊⼝⽂件,并产出依赖列表;

  3. AST (Abstract Syntax Tree)抽象语法树 在计算机科学中,或简称语法树(Syntax tree),是源代码语法结构的⼀种抽象表示。它以树状的形式表现编程语⾔的语法结构,树上的每个节点都表示源代码中的⼀种结构。

  4. 使⽤ Babel 将相关模块编译到 ES5;

  5. webpack有⼀个智能解析器(各种babel),⼏乎可以处理任何第三⽅库。⽆论它们的模块形式是CommonJS、AMD还是普通的JS⽂件;甚⾄在加载依赖的时候,允许使⽤动态表require(“、/templates/“+name+”、jade”)。以下这些⼯具底层依赖了不同的解析器⽣成AST,⽐如eslint使⽤了espree、babel使⽤了acorn

  6. 对每个依赖模块产出⼀个唯⼀的 ID,⽅便后续读取模块相关内容;

  7. 将每个依赖以及经过 Babel 编译过后的内容,存储在⼀个对象中进⾏维护;

  8. 遍历上⼀步中的对象,构建出⼀个依赖图(Dependency Graph);

  9. 将各模块内容 bundle 产出

详细流程

img

流程:

  1. 通过命令行和 webpack.config.js 来获取参数
  2. 创建compiler对象,初始化plugins
  3. 开始编译阶段,addEntry添加入口资源
  4. addModule 创建模块
  5. runLoaders 执行 loader
  6. 依赖收集,js 通过acorn解析为 AST,然后查找依赖,并重复 4 步
  7. 构建完依赖树后,进入生成阶段,调用compilation.seal
  8. 经过一系列的optimize优化依赖,生成 chunks,写入文件

打包原理

⼿写webpack原理 https://juejin.cn/post/6854573217336541192

webpack打包原理 https://blog.csdn.net/weixin_41319237/article/details/116194091

主要流程

  1. 需要读到入口文件里面的内容
  2. 分析入口文件,递归的去读取模块所依赖的文件内容,生成AST语法树。
  3. 根据AST语法树,生成浏览器能够运行的代码

具体细节

  1. 获取主模块内容
  2. 分析模块
    • 安装@babel/parser包(生成AST)
  3. 对模块内容进行处理
    • 安装@babel/traverse包(遍历AST收集依赖)
    • 安装@babel/core和@babel/preset-env包 (es6转ES5)
  4. 递归所有模块
  5. 执行require和exports。生成最终代码

基本准备工作

先建一个项目

我们创建了add.js文件和minus.js文件,然后 在index.js中引入,再将index.js文件引入index.html。

代码如下:

add.js

1
2
3
4
export default (a,b)=>{
return a+b;
}
复制代码

minus.js

1
2
3
4
export const minus = (a,b)=>{
return a-b
}
复制代码

index.js

1
2
3
4
5
6
7
8
import add from "./add.js"
import {minus} from "./minus.js";

const sum = add(1,2);
const division = minus(2,1);

console.log(sum);
console.log(division);

index.html

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script src="./src/index.js"></script>
</body>
</html>
复制代码

现在我们打开index.html。你猜会发生什么???显然会报错,因为浏览器还不能识别import等ES6语法

img

获取主模块内容

bundle.js文件

1
2
3
4
5
6
7
// 获取主入口文件
const fs = require('fs')
const getModuleInfo = (file)=>{
const body = fs.readFileSync(file,'utf-8')
console.log(body);
}
getModuleInfo("./src/index.js")

执行一下bundle.js:node bundle.js

img

分析模块babel/parser

分析模块的主要任务是 将获取到的主模块内容 解析成AST语法树,这个需要用到一个依赖包@babel/parser

1
npm install @babel/parser

ok,安装完成我们将@babel/parser引入bundle.js,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 获取主入口文件
const fs = require('fs')
const parser = require('@babel/parser')
const getModuleInfo = (file)=>{
const body = fs.readFileSync(file,'utf-8')
// 新增代码
const ast = parser.parse(body,{
sourceType:'module' //表示我们要解析的是ES模块
});
console.log(ast);
//当前我们解析出来的不单单是index.js文件里的内容,它也包括了文件的其他信息。 而它的内容其实是它的属性program里的body里
console.log(ast.program.body);
}
getModuleInfo("./src/index.js")

收集依赖babel/traverse

现在我们需要 遍历AST,将用到的依赖收集起来。什么意思呢?其实就是将用import语句引入的文件路径收集起来。我们将收集起来的路径放到deps里。

前面我们提到过,遍历AST要用到@babel/traverse依赖包

1
npm install @babel/traverse

现在,我们引入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const getModuleInfo = (file)=>{
const body = fs.readFileSync(file,'utf-8')
const ast = parser.parse(body,{
sourceType:'module' //表示我们要解析的是ES模块
});

// 新增代码
const deps = {}
traverse(ast,{
//ImportDeclaration方法代表的是对ast中type类型为ImportDeclaration的节点的处理。
ImportDeclaration({node}){
const dirname = path.dirname(file)
const abspath = './' + path.join(dirname,node.source.value)
deps[node.source.value] = abspath
}
})
console.log(deps);


}
getModuleInfo("./src/index.js")

ES6的AST转化成ES5

1
npm install @babel/core @babel/preset-env

我们现在将依赖引入并使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')
const getModuleInfo = (file)=>{
const body = fs.readFileSync(file,'utf-8')
const ast = parser.parse(body,{
sourceType:'module' //表示我们要解析的是ES模块
});
const deps = {}
traverse(ast,{
ImportDeclaration({node}){
const dirname = path.dirname(file)
const abspath = "./" + path.join(dirname,node.source.value)
deps[node.source.value] = abspath
}
})

新增代码
const {code} = babel.transformFromAst(ast,null,{
presets:["@babel/preset-env"]
})
console.log(code);

}
getModuleInfo("./src/index.js")

递归获取所有依赖

经过上面的过程,现在我们知道getModuleInfo是用来获取一个模块的内容,不过我们还没把获取的内容return出来,因此,更改下getModuleInfo方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const getModuleInfo = (file)=>{
const body = fs.readFileSync(file,'utf-8')
const ast = parser.parse(body,{
sourceType:'module' //表示我们要解析的是ES模块
});
const deps = {}
traverse(ast,{
ImportDeclaration({node}){
const dirname = path.dirname(file)
const abspath = "./" + path.join(dirname,node.source.value)
deps[node.source.value] = abspath
}
})
const {code} = babel.transformFromAst(ast,null,{
presets:["@babel/preset-env"]
})
// 新增代码
const moduleInfo = {file,deps,code}
return moduleInfo
}

我们返回了一个对象 ,这个对象包括主模块的路径(file)主模块的依赖(deps)主模块转化成es5的代码

该方法只能获取一个模块的的信息,但是我们要怎么获取一个模块里面的依赖模块的信息呢?递归。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//递归获取依赖
const parseModules = (file) =>{
const entry = getModuleInfo(file)
const temp = [entry]
for (let i = 0;i<temp.length;i++){
const deps = temp[i].deps
if (deps){
for (const key in deps){
if (deps.hasOwnProperty(key)){
temp.push(getModuleInfo(deps[key]))
}
}
}
}
console.log(temp)
}

讲解下parseModules方法:

  1. 我们首先传入主模块路径
  2. 将获得的模块信息放到temp数组里。
  3. 外面的循坏遍历temp数组,此时的temp数组只有主模块
  4. 循环里面再获得主模块的依赖deps
  5. 遍历deps,通过调用getModuleInfo将获得的依赖模块信息push到temp数组里。

按照目前我们的项目来说执行完,应当是temp 应当是存放了index.js,add.js,minus.js三个模块。 ,执行看看。

img

不过现在的temp数组里的对象格式不利于后面的操作,我们希望是以文件的路径为key,{code,deps}为值的形式存储。因此,我们创建一个新的对象depsGraph。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const parseModules = (file) =>{
const entry = getModuleInfo(file)
const temp = [entry]
for (let i = 0;i<temp.length;i++){
const deps = temp[i].deps
if (deps){
for (const key in deps){
if (deps.hasOwnProperty(key)){
temp.push(getModuleInfo(deps[key]))
}
}
}
}
// 新增代码
const depsGraph = {}
temp.forEach(moduleInfo=>{
depsGraph[moduleInfo.file] = {
deps:moduleInfo.deps,
code:moduleInfo.code
}
})
console.log(depsGraph)
return depsGraph
}

img

导入导出

我们现在的目的就是要生成一个bundle.js文件,也就是打包后的一个文件。其实思路很简单,就是把index.js的内容和它的依赖模块整合起来。然后把代码写到一个新建的js文件。

1
2
3
4
5
6
7
8
9
10
11
// index.js
"use strict"
var _add = _interopRequireDefault(require("./add.js"));
var _minus = require("./minus.js");
function _interopRequireDefault(obj)
{
return obj && obj.__esModule ? obj : { "default": obj };
}
var sum = (0, _add["default"])(1, 2);
var division = (0, _minus.minus)(2, 1);
console.log(sum); console.log(division);
1
2
3
4
5
6
// add.js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true});
exports["default"] = void 0;
var _default = function _default(a, b) { return a + b;};
exports["default"] = _default;

但是我们现在是不能执行index.js这段代码的,因为浏览器不会识别执行require和exports。

不能识别是为什么?因为没有定义这require函数,和exports对象。那我们可以自己定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const bundle = (file) => {
//返回一个整合完整的字符串代码
const depsGraph = JSON.stringify(parseModules(file));
console.log(depsGraph);
/**
* 把保存下来的depsGraph,传入一个立即执行函数。
将主模块路径传入require函数执行
执行reuire函数的时候,又立即执行一个立即执行函数,这里是把code的值传进去了
执行eval(code)。也就是执行主模块的code这段代码
*/
return `(function (graph) {
function require(file) {
//转化成绝对路径
function absRequire(relPath) {
return require(graph[file].deps[relPath])
}
//执行add.js的code时候,会遇到exports这个还没定义的问题.因此我们可以自己定义一个exports对象。
var exports = {};
(function (require, exports, code) {
console.log(1, exports);
//code代码执行过程中会执行到require函数。
//这时会调用这个require,也就是我们传入的absRequire
eval(code);
})(absRequire, exports, graph[file].code)
return exports;
}
require('${file}')
})(${depsGraph})`;
};
const content = bundle("./src/index.js");
  • 把保存下来的depsGraph,传入一个立即执行函数。

    • 将主模块路径传入require函数执行

      • reuire函数中立即执行函数

        • require:absRequire,因为code代码中require路径不是绝对路径,需要转化成绝对路径,因此写一个函数absRequire来转化

        • exports:exports

          • 增添了一个空对象 exports,执行add.js代码的时候,会往这个空对象上增加一些属性

            1
            2
            3
            4
            5
            6
            7
            8
            9
            10
            11
            // add.js
            "use strict";
            Object.defineProperty(exports, "__esModule", { value: true});
            exports["default"] = void 0;
            var _default = function _default(a, b) { return a + b;};
            exports["default"] = _default;
            //执行完这段代码后
            exports = {
            __esModule:{ value: true},
            defaultfunction _default(a, b) { return a + b;}
            }
          • 然后我们把exports对象return出去。

            1
            var _add = _interopRequireDefault(require("./add.js"));

            return出去的值,被_interopRequireDefault接收,_interopRequireDefault再返回default这个属性给_add,因此_add = function _default(a, b) { return a + b;}

        • code:graph[file].code

          • 执行eval(code),也就是执行模块的code这段代码
            • 执行eval(code)过程会执行到require函数,这时会调用这个require,也就是我们传入的absRequire,而执行absRequire就执行了return require(graph[file].deps[relPath])这段代码,也就是执行了外面这个require。而执行require(”./src/add.js”)之后,又会执行eval,也就是执行add.js文件的代码。

单应用框架

软件架构模式

https://www.pianshen.com/article/3716256399/

MVC,MVP和MVVM都是常见的软件架构设计模式(Architectural Pattern),它通过分离关注点来改进代码的组织方式。它们目标都是解耦,解耦好处一个是关注点分离,提升代码可维护和可读性,并且提升代码复用性。

MVC

MVC全名是Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范,用一种业务逻辑、数据、界面显示分离的软件设计规范。

  • Model(模型) :数据层。将js的ajax当做Model,也就是数据层,通过ajax从服务器获取数据。

  • View(视图) :视图层。

  • Controller(控制器):交互层。用户对View的操作交给了Controller处理,在Controller中响应View的事件调用Model的接口对数据进行操作,一旦Model发生变化便通知相关视图进行更新

img

1.View传送指令到Controller。

2.Controller完成业务逻辑后改变Model状态。

3.Model将新的数据发送至View,用户得到反馈。

缺点

1.m层和v层直接打交道,导致这两层耦合度高

2.因为所有逻辑都写在c层,导致c层特别臃肿

控制div是否显示:

1
2
3
4
5
6
7
8
9
10
11
12
<body>
<div id="box">我显示的</div>
<button id="btn">点击</button>

<script>
btn.onclick = function() {
if(box.style.display === 'none') box.style.display = 'block';
else box.style.display = 'none';
}
</script>
</body>
复制代码

用数据驱动模型模型来写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<body>
<div id="box">我显示的</div>
<button id="btn">点击</button>

<script>
let is_shown = true;
function render(el, is_shown) {
if(is_shown) el.style.display = 'block';
else el.style.display = 'none';
}
btn.onclick = function() {
is_shown = !is_shown;
render(box, is_shown);
}
</script>
</body>
复制代码

看起来好像多了几行代码,但是对于第二种代码来说,简单抽象封装了 render 函数,我们只需要修改 is_shownbool 值,而无需在意 render 函数内部的执行,就可以实现通过数据修改来驱动视图的更新。

MVVM

mvc中Controller演变成mvvm中的viewModel。 mvvm主要解决了mvc中大量DOM操作使页面首次渲染性能降低,加载速度变慢的问题 。

Model-View-ViewModel即模型-视图-视图模型。

  • 模型:数据层。后端传递的数据。

  • 视图:视图层。

  • 视图模型:mvvm模式的核心,它是连接view和model的桥梁。

总结在MVVM的框架下视图和模型是不能直接通信的。它们通过ViewModel来通信,ViewModel通常要实现一个observer观察者,当数据发生变化,ViewModel能够监听到数据的这种变化,然后通知到对应的视图做自动更新,而当用户操作视图,ViewModel也能监听到视图的变化,然后通知数据做改动,这实际上就实现了数据的双向绑定

区别

  • dom操作方式

    • MVC来讲,MVC操作的是真实dom,对于数据的更新需要找到对应抽象类来直接操作真实dom
    • 对于MVVM来讲,它操作的是虚拟dom、在数据的更新后,该框架重新生成一个虚拟dom树,与旧虚拟dom树进行比对,然后替换修改的地方,所以可以将渲染视图抽象成一个函数类
  • 视图更新

    • MVVM完全不需要考虑视图更新对dom树的操作,框架会自动响应绑定对视图的更新
  • 性能

    • 页面首次渲染,MVVM框架可能会比MVC框架快一些,因为MVVM只会进行一次对真实dom的操作,而MVC可能会进行多次真实dom的操作
    • 在首屏渲染完毕后,用户开始对页面进行直接操作时,MVVM的性能肯定会输MVC的
      • 对于MVC构建的页面来说,用户修改数据,该框架会根据绑定的dom元素直接进行修改
      • 而对于MVVM构建的页面来说,用户修改数据,该框架会重新生成虚拟dom树与原树进行比对,再修改
      • 虽然可以进行diff(新旧虚拟dom树比对算法)优化,但是一个是直接操作,一个需要最少O(n)算法比对在进行真实dom操作
  • 框架

    • 常见的MVC框架有:Angular.js

框架对比

https://juejin.cn/post/6844903974437388295

https://zhuanlan.zhihu.com/p/100228073

渐进式

我们可以通过添加组件系统(components)、客户端路由(vue-router)、大规模状态管理(vuex)来构建一个完整的框架,这都是可选的

开发团队

  • React是由FaceBook前端官方团队进行维护和更新的;因此,React的维护开发团队,技术实力比较雄厚;
  • Vue:第一版,主要是有作者 尤雨溪 专门进行维护的,当 Vue更新到 2.x 版本后,也有了一个小团队进行相关的维护和开发;

编写语法

vue

vue推荐的做法是webpack+vue-loader的单文件组件格式,vue保留了html、css、js分离的写法,使得现有的前端开发者在开发的时候能保持原有的习惯,更接近常用的web开发方式,模板就是普通的html,数据绑定使用mustache风格,样式直接使用css。其中