#每天都是崭新的一天#再累也要照顾好自己

【知识体系】前端开发人员的知识体系-前端工程,项目构建

开发知识体系,主要是全栈开发知识体系

  • 目的:每一个开发人员都应该形成自己的知识体系,提纲挈领。在设计代码,聊技术,面试,系统结构设计,架构设计等时候,能够游刃有余。
  • 特点:
    • 1、前端领域:Html和css基础,JavaScript,计算机基础,框架和类库,前端工程,项目构建,算法与数据结构等。
    • 2、后端领域:欢迎补充。。。

知识体系概览

前端知识体系

更详细最新的参考github源码地址: full_stack_knowledge_list全栈开发知识体系

欢迎fork,start,有问题提issue,欢迎pull request,后续的更新和维护以github为准。

接上一篇:【知识体系】前端开发人员的知识体系-JavaScript

16、npm的原理

npm成为世界最大的包管理器,原因:用户友好。

1、npm init

用来初始化一个简单的package.json文件。package.json文件用来定义一个package的描述文件。

1、npm init的执行的默认行为

执行npm init --yes,全部使用默认的值。

2、 自定义npm init行为

npm init命令的原理是:调用脚本,输出一个初始化的package.json文件。

获取用户输入使用prompt()方法。

2、依赖包安装

npm的核心功能:依赖管理。执行npm i从package.json中dependencies和devDependencies将依赖包安装到当前目录的node_modules文件夹中。

1、package定义

npm i <package>就可以安装一个包。通常package就是我们需要安装的包名,默认配置下npm会从默认的源(Registry)中查找该包名的对应的包地址,并且下载安装。

<package>还可以是一个指向有效包名的http url/git url/文件夹路径。

package的准确定义,符合以下a)到g)其中一个条件,他就是一个package:
package的准确定义

2、安装本地包/远程git仓库包

共享依赖包,并非非要把包发布到npm源上才能使用。

(1)场景1:本地模块引用

开发中避免不了模块之间调用,开发中,我们把频繁调用的配置模块放在根目录,然后如果有很多层级目录,后来引用

const config = require(''../../../../..config)

这样的路径引用不利于代码重构。这时候我们需要考虑把这个模块分离出来供其他模块共享。
比如config.js可以封装成一个package放到node_modules目录下。

不需要手动拷贝或者创建软连接到node_modules目录,npm 有自己的解决方案:

方案:

1、新增config文件夹,将config.js移入文件夹,名字修改为index.js,创建package.json定义config包

{
    "name": "config",
    "main": "index.js",
    "version": "0.1.0"
}

2、在项目的package.json新增依赖项,然后执行npm i。

{
  "dependencies": {
    "config":"file: ./config"
  }
}

查看 node_modules 目录我们会发现多出来一个名为 config,指向上层 config/ 文件夹的软链接。这是因为 npm 识别 file: 协议的url,得知这个包需要直接从文件系统中获取,会自动创建软链接到 node_modules 中,完成“安装”过程。

(2)场景2:私有git共享package

团队内会有一些代码/公用库需要在团队内不同项目间共享,但可能由于包含了敏感内容。

我们可以简单的将被依赖的包托管到私有的git仓库中,然后将git url保存到dependencies中。npm会直接调用系统的git命令从git仓库拉取包的内容到node_modules中。

npm支持的git url格式:

<protocol>://[<user>[:<password>]@]<hostname>[:<port>][:][/]<path>[#<commit-ish> | #semver:<semver>]

git 路径后可以使用 # 指定特定的 git branch/commit/tag, 也可以 #semver: 指定特定的 semver range.

比如:

git+ssh://git@github.com:npm/npm.git#v1.0.27
git+ssh://git@github.com:npm/npm#semver:^5.0
git+https://isaacs@github.com/npm/npm.git
git://github.com/npm/npm.git#v1.0.27

(3)场景3:开源package问题修复

此时我们可以手动进入 node_modules 目录下修改相应的包内容,也许修改了一行代码就修复了问题。但是这种做法非常不明智!

方案:

fork原作者的git库,在自己的repo修复问题,然后将dependencies中的相应依赖改为自己修复后版本的git url就可以解决问题。

3、npm install如何工作

npm i执行完毕,node_modules中看到所有的依赖包。开发人员无关注node_modules文件夹的结构细节,关注业务代码中引用依赖包。

理解node_modules结构帮助我们更好理解npm如何工作。npm2到npm5变化和改进。

3.1 npm2

npm2在安装依赖包,采用的是简单的递归安装方法。每一个包都有自己的依赖包,每一个包的依赖都安装在自己的node_modules中,依赖关系层层递进,构成整个依赖树,这个依赖树与文件系统中的文件结构树一一对应。

最方便的依赖树的方式在根目录下执行npm ls。

优点:

  • 1、层级结构明显,便于傻瓜式管理。

缺点:

  • 1、复杂工程,目录结构可能太深,深层的文件路径过长触发window文件系统中文件路径不能超过260个字符长。
  • 2、部分被多个包依赖的包在很多地方重复安装,造成大量的冗余。

3.2 npm3

npm3的node_modules目录改成更加扁平状层级结构。npm3在安装的时候遍历整个依赖树,计算最合理的文件夹安装方式,所有被重复依赖的包都可以去重安装。

npm来说,同名不同版本的包是两个独立的包。

npm3的依赖树结构不再与文件夹层级一一对应。

3.3 npm5

沿用npm3的扁平化依赖包安装方式。最大的变化时增加package-lock.json文件。

package-lock.json作用:锁定依赖安装结构,发现node_modules目录文件层级结构是与json的结构一一对应。

npm5默认会在执行npm i后生成package-lock.json文件,提交到git/svn代码库。

要升级,不要使用 5.0版本。

注意:在 npm 5.0 中,如果已有 package-lock 文件存在,若手动在 package.json 文件新增一条依赖,再执行 npm install, 新增的依赖并不会被安装到 node_modules 中, package-lock.json 也不会做相应的更新。

4、依赖包版本管理

介绍依赖包升级管理相关知识。

4.1 语义化版本semver

npm依赖管理的一个重要特性采用语义化版本(semver)规范,作为版本管理方案。

语义化版本号必须包含三个数字,格式:major.minor.patch。意思是:主版本号.小版本号.修改版本号。

我们需要在dependencies中使用semver约定的指定所需依赖包的版本号或者范围。

常用的规则如下图:

semver语义化版本

1、任意两条规则,用空格连接起来,表示“与”逻辑,即为两个规则的交集。

如 >=2.3.1 <=2.8.0 可以解读为: >=2.3.1 且 <=2.8.0

  • 可以匹配 2.3.1, 2.4.5, 2.8.0
  • 但不匹配 1.0.0, 2.3.0, 2.8.1, 3.0.0

2、任意两条规则,用||连接起来,表示“或”逻辑,即为两条规则的并集。

如 ^2 >=2.3.1 || ^3 >3.2

  • 可以匹配 2.3.1, 2,8.1, 3.3.1
  • 但不匹配 1.0.0, 2.2.0, 3.1.0, 4.0.0

3、更直观的表示版本号范围的写法

    • 或 x 匹配所有主版本
  • 1 或 1.x 匹配 主版本号为 1 的所有版本
  • 1.2 或 1.2.x 匹配 版本号为 1.2 开头的所有版本

4、在 MAJOR.MINOR.PATCH 后追加 - 后跟点号分隔的标签,作为预发布版本标签
通常被视为不稳定、不建议生产使用的版本。

  • 1.0.0-alpha
  • 1.0.0-beta.1
  • 1.0.0-rc.3

4.2 依赖版本升级

在安装完一个依赖包之后有新的版本发布了,如何使用npm进行版本升级呢?

  • npm i或者npm update,但是不同的npm版本,不同的package.json和package-lock.json文件,安装和升级表现是不同的。

使用npm3的结论:

  • 如果本地 node_modules 已安装,再次执行 install 不会更新包版本, 执行 update 才会更新; 而如果本地 node_modules 为空时,执行 install/update 都会直接安装更新包。
  • npm update 总是会把包更新到符合 package.json 中指定的 semver 的最新版本号——本例中符合 ^1.8.0 的最新版本为 1.15.0
  • 一旦给定 package.json, 无论后面执行 npm install 还是 update, package.json 中的 webpack 版本一直顽固地保持 一开始的 ^1.8.0 岿然不动

使用npm5的结论:

  • 无论何时执行 install, npm 都会优先按照 package-lock 中指定的版本来安装 webpack; 避免了 npm 3 表中情形 b) 的状况;
  • 无论何时完成安装/更新, package-lock 文件总会跟着 node_modules 更新 —— (因此可以视 package-lock 文件为 node_modules 的 JSON 表述)
  • 已安装 node_modules 后若执行 npm update,package.json 中的版本号也会随之更改为 ^1.15.0

4.3 最佳实践

我常用的node是8.11.x,npm是5.6.0。

  • 使用npm >= 5.1 版本,保持package-lock.json文件默认开启配置。
  • 初始化,npm i <package>安装依赖包,默认保存^X.Y.Z,项目提交package.json和package-lock.json。
  • 不要手动修改package-lock.json
  • 升级依赖包:
    • 升级小版本,执行npm update升级到新的小版本。
    • 升级大版本,执行npm install <package-name>@<version> 升级到新的大版本。
    • 手动修改package.json中的版本号,然后npm i。
    • 本地验证升级新版本后没有问题,提交新的package.json和package-lock.json文件。
  • 降级依赖包:
    • 正确:npm i <package-name>@<version>验证没有问题后,提交package.json和package-lock.json文件。
    • 错误:修改package.json中的版本号,执行npm i不会生效。因为package-lock.json锁定了版本。
  • 删除依赖包:
    • A计划:npm uninstall <package>。提交package.json和package-lock.json。
    • B计划:在package.json中删除对应的包,然后执行npm i,提交package.json和package-lock.json。

5、npm的sctipts

5.1 基本使用

npm scripts是npm的一个重要的特性。在package.json中scripts字段定义一个脚本。

比如:

{
    "scripts": {
        "echo": "echo HELLO WORLD"
    }
}

我们可以通过npm run echo 命令执行这段脚本,就像shell中执行echo HELLO WOLRD,终端是可以看到输出的。

总结如下:

  • npm run 命令执行时,会把./node_modules/.bin目录添加到执行环境的PATH变量中。全局的没有安装的包,在node_modules中安装了,通过npm run 可以调用该命令。
  • 执行npm 脚本时要传入参数,需要在命令后加 -- 表明,比如 npm run test -- --grep="pattern" 可以将--grep="pattern"参数传给test命令。
  • npm 还提供了pre和post两种钩子的机制,可以定义某个脚本前后的执行脚本。
  • 运行时变量:npm run 的脚本执行环境内,可以通过环境变量的方式获取更多的运行相关的信息。可以通过process.env对象访问获得:
    • npm_lifecycle_event:正在运行的脚本名称
    • npm_package_<key>:获取当前package.json中某一个字段的匹配值:如包名npm_package_name
    • npm_package_<key>_<sub-key>:package中的嵌套字段。

5.2 node_modules/.bin目录

保存了依赖目录中所安装的可供调用的命令行包。本质是一个可执行文件到指定文件源的映射。

例如 webpack 就属于一个命令行包。如果我们在安装 webpack 时添加 --global 参数,就可以在终端直接输入 webpack 进行调用。

上一节所说,npm run 命令在执行时会把 ./node_modules/.bin 加入到 PATH 中,使我们可直接调用所有提供了命令行调用接口的依赖包。所以这里就引出了一个最佳实践:

  • 将项目依赖的命令行工具安装到项目依赖文件夹中,然后通过 npm scripts 调用;而非全局安装

于是 npm 从5.2 开始自带了一个新的工具 npx.

5.3 npx

npx 的使用很简单,就是执行 npx <command> 即可,这里的 <command> 默认就是 ./node_modules 目录中安装的可执行脚本名。例如上面本地安装好的 webpack 包,我们可以直接使用 npx webpack 执行即可。

5.4 用法

  • 1、传入参数
    "scripts": {
    "serve": "vue-cli-service serve",
    "serve1": "vue-cli-service --serve1",
    "serve2": "vue-cli-service -serve2",
    "serve3": "vue-cli-service serve --mode=dev --mobile -config build/example.js"
    }
    
    除了第一个可执行的命令,以空格分割的任何字符串都是参数,并且都能通过process.argv属性访问。

比如执行npm run serve3命令,process.argv的具体内容为:

[ '/usr/local/Cellar/node/7.7.1_1/bin/node',
  '/Users/mac/Vue-projects/hao-cli/node_modules/.bin/vue-cli-service',
  'serve',
  '--mode=dev',
  '--mobile',
  '-config',
  'build/example.js'
]
  • 2、多命令运行
    在启动时可能需要同时执行多个任务,多个任务的执行顺序决定了项目的表现。

(1)串行执行

串行执行,要求前一个任务执行成功之后才能执行下一个任务。使用 && 服务来连接。

npm run scipt1 && npm run script2

串行执行命令,只要一个命令执行失败,整个脚本会中止的。

(2)并行执行

并行执行,就是多个命令同时平行执行,使用 & 符号来连接。

npm run script1 & npm run script2
  • 3、env 环境变量
    在执行npm run脚本时,npm会设置一些特殊的env环境变量。其中package.json中的所有字段,都会被设置为以npm_package_ 开头的环境变量。
  • 4、指令钩子
    在执行npm scripts命令(无论是自定义还是内置)时,都经历了pre和post两个钩子,在这两个钩子中可以定义某个命令执行前后的命令。
    比如在执行npm run serve命令时,会依次执行npm run preserve、npm run serve、npm run postserve,所以可以在这两个钩子中自定义一些动作:
"scripts": {
  "preserve": "xxxxx",
  "serve": "cross-env NODE_ENV=production webpack",
  "postserve": "xxxxxx"
}
  • 5、常用脚本示例
    `
    // 删除目录
    "clean": "rimraf dist/*",

// 本地搭建一个http服务
"server": "http-server -p 9090 dist/",

// 打开浏览器
"open:dev": "opener http://localhost:9090",

// 实时刷新
"livereload": "live-reload --port 9091 dist/",

// 构建 HTML 文件
"build:html": "jade index.jade > dist/index.html",

// 只要 CSS 文件有变动,就重新执行构建
"watch:css": "watch 'npm run build:css' assets/styles/",

// 只要 HTML 文件有变动,就重新执行构建
"watch:html": "watch 'npm run build:html' assets/html",

// 部署到 Amazon S3
"deploy:prod": "s3-cli sync ./dist/ s3://example-com/prod-site/",

// 构建 favicon
"build:favicon": "node scripts/favicon.js",



### 6 npm配置
#### 6.1 npm config
+ 通过npm config ls -l 可查看npm 的所有配置,包括默认配置。
+ 通过npm config set <key> <value>,常见配置:
    + proxy,https-proxy:指定npm使用的代理
    + registry:指定npm下载安装包时的源,默认是https://registry.npmjs.org。可以指定私有的registry源。
    + package-lock.json:指定是否默认生成package-lock.json,建议保持默认true。
    + save :true/false指定是否在npm i之后保存包为dependencies,npm5开始默认为true。
+ 通过npm config delete <key> 删除指定的配置项。    


#### 6.2 npmrc文件
可以通过删除npm config命令修改配置,还可以通过npmrc文件直接修改配置。
+ npmrc文件优先级由高到低,包括:
    + 工程内配置文件:项目根目录下的.npmrc文件
    + 用户级配置文件:用户配置里
    + 全局配置文件
    + npm内置配置文件
我们可以在自己的团队中在根目录下创建一个.npmrc文件来共享需要在团队中共享的npm运行相关配置。

比如:我们在公司内网下需要代理才能访问默认源:https://registry.npmjs.org源;或者访问内网的registry,就可以在工作项目下新增.npmrc文件并提交代码库。

示例配置:

proxy = http://proxy.example.com/
https-proxy = http://proxy.example.com/
registry = http://registry.example.com/

这种在工程内配置文件的优先级最高,作用域在这个项目下,可以很好的隔离公司项目和学习研究的项目两种不同环境。

**将这个功能与 ~/.npm-init.js 配置相结合,可以将特定配置的 .npmrc 跟 .gitignore, README 之类文件一起做到 npm init 脚手架中,进一步减少手动配置。**

#### 6.3 node版本约束
一个团队中共享了相同的代码,但是每个人开发机器不一致,使用的node版本也不一致,服务端可能与开发环境不一致。

+ 这就带来了不一致的因素----方案:声明式约束+脚本限制。

+ 声明:通过package.json的engines属性声明应用运行所需的版本要求。例如我呢项目中使用了async,await特性,得知[node查阅兼容表格](https://node.green/)得知最低支持版本是7.6.0.因此指定engines配置为:
```json
{
  "engines": {"node": ">=7.6.0"}
}
  • 强约束(可选):需要添加强约束,需要自己写脚本钩子,读取并解析engines字段的semver range并与运行环境做比对校验并适当提醒。

总结

  • npm init初始化新项目
  • 统一项目配置:需要团队共享npm config配置项,固化到.npmrc文件中
  • 统一运行环境:统一package.json,统一package-lock.json文件。
  • 合理使用多样化的源安装依赖包
  • 使用npm版本:>= 5.2版本
  • 使用npm scripts和npx管理相应脚本
  • 安全漏洞检查:npm audit fix修复安全漏洞的依赖包(本质:自动更新到兼容的安全版本)

17、babel

babel对于大多数前端开发人员来说,不陌生,但是背后的原理海慧寺黑盒。

我们需要了解babel背后的原理在我们开发中广泛应用。

17.1 babel简单应用

[1,2,3].map(n => n+1);

经过babel转译之后,代码变成这样

[1,2,3].map(function(n){
  return n + 1;
})

那我们应该知道了babel定位:babel将ES6新引进的语法转换为浏览器可以运行的ES5语法。

17.2 babel背后

babel过程:解析----转换---生成。

babel背后过程

我们看到一个叫AST(抽象语法树)的东西。

主要三个过程:

  • 解析:将代码(字符串)转换为AST(抽象语法树)。
  • 转换:访问AST的节点进行变化操作生成新的AST。
  • 生成:以新的AST为基础生成代码

17.3 过程1:代码解析(parse)

代码解析(parse)将一段代码解析成一个数据结构。其中主要关键步骤:

  • 词法分析:代码(字符串)分割成token流。即语法单元组成的数组。
  • 语法分析:分析token流(生成的数组)生成AST。

1.1词法分析

词法分析,首先明白JS中哪些属于语法单元?

  • 数字:js中科学计数法以及普通数组都是语法单元。
  • 括号:(和)只要出现,不管意义都算是语法单元。
  • 标识符:连续字符,常见变量,常量,关键字等等
  • 运算符:+,-,*,/等。
  • 注释和中括号。

我们来看一下简单的词法分析器(Tokenizer)

// 词法分析器,接收字符串返回token数组
export const tokenizer = (code) => {

    // 储存 token 的数组
    const tokens  = [];

    // 指针
    let current = 0;

    while (current < code.length) {
        // 获取指针指向的字符
        const char = code[current];

        // 我们先处理单字符的语法单元 类似于`;` `(` `)`等等这种
        if (char === '(' || char === ')') {
            tokens.push({
                type: 'parens',
                value: char,
            });

            current ++;

            continue;
        }

        // 我们接着处理标识符,标识符一般为以字母、_、$开头的连续字符
        if (/[a-zA-Z\$\_]/.test(char)) {
            let value = '';
            value += char;
            current ++;

            // 如果是连续字那么将其拼接在一起,随后指针后移
            while (/[a-zA-Z0-9\$\_]/.test(code[current]) && current < code.length) {
                value += code[current];
                current ++;
            }

            tokens.push({
                type: 'identifier',
                value,
            });

            continue;
        }


        // 处理空白字符
        if (/\s/.test(char)) {
            let value = '';
            value += char;
            current ++;

            //道理同上
            while (/\s]/.test(code[current]) && current < code.length) {
                value += code[current];
                current ++;
            }

            tokens.push({
                type: 'whitespace',
                value,
            });

            continue;
        }


        // 处理逗号分隔符
        if (/,/.test(char)) {

            tokens.push({
                type: ',',
                value: ',',
            });

            current ++;
            continue;
        }

        // 处理运算符
        if (/=|\+|>/.test(char)) {
            let value = '';
            value += char;
            current ++;

            while (/=|\+|>/.test(code[current])) {
                value += code[current];
                current ++;
            }

            // 当 = 后面有 > 时为箭头函数而非运算符
            if (value === '=>') {
                tokens.push({
                    type: 'ArrowFunctionExpression',
                    value,
                });
                continue;
            }

            tokens.push({
                type: 'operator',
                value,
            });

            continue;
        }

        // 如果碰到我们词法分析器以外的字符,则报错
        throw new TypeError('I dont know what this character is: ' + char);
    }

    return tokens;
};

上述的这个词法分析器:主要是针对例子的箭头函数。

1.2语法分析

语法分析之所以复杂,是因为要分析各种语法的可能性,需要开发者根据token流(上一节我们生成的 token 数组)提供的信息来分析出代码之间的逻辑关系,只有经过词法分析 token 流才能成为有结构的抽象语法树.

做语法分析最好依照标准,大多数 JavaScript Parser 都遵循estree规范

1、语句(Statements): 语句是 JavaScript 中非常常见的语法,我们常见的循环、if 判断、异常处理语句、with 语句等等都属于语句。

2、表达式(Expressions): 表达式是一组代码的集合,它返回一个值,表达式是另一个十分常见的语法,函数表达式就是一种典型的表达式,如果你不理解什么是表达式, MDN上有很详细的解释.

3、声明(Declarations): 声明分为变量声明和函数声明,表达式(Expressions)中的函数表达式的例子用声明的写法就是下面这样.

const parser = tokens => {
    // 声明一个全时指针,它会一直存在
    let current = -1;

    // 声明一个暂存栈,用于存放临时指针
    const tem = [];

    // 指针指向的当前token
    let token = tokens[current];

    const parseDeclarations = () => {

        // 暂存当前指针
        setTem();

        // 指针后移
        next();

        // 如果字符为'const'可见是一个声明
        if (token.type === 'identifier' && token.value === 'const') {
            const declarations = {
                type: 'VariableDeclaration',
                kind: token.value
            };

            next();

            // const 后面要跟变量的,如果不是则报错
            if (token.type !== 'identifier') {
                throw new Error('Expected Variable after const');
            }

            // 我们获取到了变量名称
            declarations.identifierName = token.value;

            next();

            // 如果跟着 '=' 那么后面应该是个表达式或者常量之类的,额外判断的代码就忽略了,直接解析函数表达式
            if (token.type === 'operator' && token.value === '=') {
                declarations.init = parseFunctionExpression();
            }

            return declarations;
        }
    };

    const parseFunctionExpression = () => {
        next();

        let init;
        // 如果 '=' 后面跟着括号或者字符那基本判断是一个表达式
        if (
            (token.type === 'parens' && token.value === '(') ||
            token.type === 'identifier'
        ) {
            setTem();
            next();
            while (token.type === 'identifier' || token.type === ',') {
                next();
            }

            // 如果括号后跟着箭头,那么判断是箭头函数表达式
            if (token.type === 'parens' && token.value === ')') {
                next();
                if (token.type === 'ArrowFunctionExpression') {
                    init = {
                        type: 'ArrowFunctionExpression',
                        params: [],
                        body: {}
                    };

                    backTem();

                    // 解析箭头函数的参数
                    init.params = parseParams();

                    // 解析箭头函数的函数主体
                    init.body = parseExpression();
                } else {
                    backTem();
                }
            }
        }

        return init;
    };

    const parseParams = () => {
        const params = [];
        if (token.type === 'parens' && token.value === '(') {
            next();
            while (token.type !== 'parens' && token.value !== ')') {
                if (token.type === 'identifier') {
                    params.push({
                        type: token.type,
                        identifierName: token.value
                    });
                }
                next();
            }
        }

        return params;
    };

    const parseExpression = () => {
        next();
        let body;
        while (token.type === 'ArrowFunctionExpression') {
            next();
        }

        // 如果以(开头或者变量开头说明不是 BlockStatement,我们以二元表达式来解析
        if (token.type === 'identifier') {
            body = {
                type: 'BinaryExpression',
                left: {
                    type: 'identifier',
                    identifierName: token.value
                },
                operator: '',
                right: {
                    type: '',
                    identifierName: ''
                }
            };
            next();

            if (token.type === 'operator') {
                body.operator = token.value;
            }

            next();

            if (token.type === 'identifier') {
                body.right = {
                    type: 'identifier',
                    identifierName: token.value
                };
            }
        }

        return body;
    };

    // 指针后移的函数
    const next = () => {
        do {
            ++current;
            token = tokens[current]
                ? tokens[current]
                : { type: 'eof', value: '' };
        } while (token.type === 'whitespace');
    };

    // 指针暂存的函数
    const setTem = () => {
        tem.push(current);
    };

    // 指针回退的函数
    const backTem = () => {
        current = tem.pop();
        token = tokens[current];
    };

    const ast = {
        type: 'Program',
        body: []
    };

    while (current < tokens.length) {
        const statement = parseDeclarations();
        if (!statement) {
            break;
        }
        ast.body.push(statement);
    }
    return ast;
};

17.4 过程2:代码转换

  • 代码解析和代码生成是babel。
  • 代码转换是babel插件

比如taro就是用babel完成小程序语法转换。

代码转换的关键是根据当前的抽象语法树,以我们定义的规则生成新的抽象语法树。转换的过程就是新的抽象语法树生成过程。

代码转换的具体过程:

  • 遍历抽象语法树(实现遍历器traverser)
  • 代码转换(实现转换器transformer)

17.5 过程3:代码转换生成代码(实现生成器generator)

生成代码这一步实际上是根据我们转换后的抽象语法树来生成新的代码,我们会实现一个函数, 他接受一个对象( ast),通过递归生成最终的代码

17.6 核心原理

Babel 的核心代码是 babel-core 这个 package,Babel 开放了接口,让我们可以自定义 Visitor,在AST转换时被调用。所以 Babel 的仓库中还包括了很多插件,真正实现语法转换的其实是这些插件,而不是 babel-core 本身。

18、webpack

可以参考这个:http://www.chengxinsong.cn/post/57

19、nginx

19.1 正向代理

  • 正向代理是客户端和源服务器之间的服务器。
  • 影藏客户端:由代理服务器去访问源服务器。
  • 每一次请求都是到代理服务器,代理转发真实请求到源服务器并获取结果返回给客户端。

作用

  • 1、翻墙:绕过无法访问的节点。
  • 2、缓存:数据缓存在failing服务器上,客户端请求内容在缓存中,就不会访问源服务器。
  • 3、权限控制:防火墙授权代理服务器权限,客户端可以使用正向代理通过防火墙。
  • 4、影藏访问者:通过配置,源服务器无法获取真实的客户端信息。

19.2 反向代理

  • 对于客户端,反向代理服务器是源服务器
  • 影藏真实的服务器:代理服务器代替源服务器去接受并返回客户端请求。

作用

  • 1、影藏真实服务器:防止服务器被恶意攻击。
  • 2、缓存作用:数据婚存在代理服务器上,客户端请求的内容在职缓存中不去访问源服务器。
  • 3、均衡负载,nginx的均衡负载

19.3 正向代理和反向代理区别

正向代理:代理的是客户端。反向代理:代理的是服务端。

正向代理与反向代理区别

正向代理与反向代理区别

19.3 nginx在前端用途

1、快速实现简单的访问限制

经常希望网站让某些特定的用户群体(比如公司内网)访问,或者不让某一个IP访问。

location / {
    deny 192.168.1.100;
    allow 192.168.1.10/200;
    allow 10.110.50.16;
    deny all;
}

其实deny和allow是ngx_http_access_module模块(已内置)中的语法。采用的是从上到下匹配方式,匹配到就跳出不再继续匹配。

上述配置的意思就是,首先禁止192.168.1.100访问,然后允许192.168.1.10-200 ip段内的访问(排除192.168.1.100),同时允许10.110.50.16这个单独ip的访问,剩下未匹配到的全部禁止访问。

实际生产中,经常和ngx_http_geo_module模块(可以更好地管理ip地址表,已内置)配合使用。

2、解决跨域

使用Nginx可以纯前端解决请求跨域问题。

同时约定一个url规则来表明代理请求的身份,然后Nginx通过匹配该规则,将请求代理回原来的域。

 #请求跨域,这里约定代理请求url path是以/apis/开头
 location ^~/apis/ {
    # 这里重写了请求,将正则匹配中的第一个()中$1的path,拼接到真正的请求后面,并用break停止后续匹配
    rewrite ^/apis/(.*)$ /$1 break;
    proxy_pass http://www.chengxinsong.cn/;
 }  

20、CDN

CDN是内容分发网络。原理:对域名设置CNAME,将CNAME指向CDN服务商,在DNS解析的时候全局均衡负载DNS解析,根据地理位置信息解析对应的IP地址,用户就近访问。

解决的问题:

  • 解决网络堵塞的状况
  • 用户就近取所需内容
  • 提高网站访问的速度

21、git

21.1 git背后原理

git中存在4中对象来帮助我们进行版本管理,分别:

  • blob对象:存放文件内容
  • tree对象:存放目录和文件信息
  • commit对象:存放每个commit的信息
  • tag对象:存放跟标签有关信息

原理:git只关心文件内容,git不管文件存放位置和文件名字,只要两个文件内容一模一样的,那背后只有一个blob对象,这个特性使得git
使用快照功能而不用担心存放内容过大,每次新的commit只要内容不变,我们只需要指向之前的blob对象,而不需要重新建立一个新的blob对象。

.git文件夹下面有objects文件夹,存放着我们刚才说的4个对象等文件,文件名其实就是警告sha1算法获取的一串id,而内容就是我们对象内容经过压缩算法后的结果。

21.2 git文件状态

git文件状态

  • 工作区:进行创建文件,修改文件,删除文件等操作。
  • 暂存区:通过git add就将工作区变动文件送进暂存区,不变的文件保留快照。
  • 对象库(本地库):通过git commit将暂存区的文件送进本地库。
    注意点:(1)commit的时候必须加描述message,描述不能为空。

设置暂存区的原因:

  • 实现部分提交到本地库
  • 工作区职能是文件内容操作,如果还需要维护一些待提交文件,违反了单一职责原则。

21.3 commit

使用git log查看之前的一些commit信息
commit
我们发现commit后边的数字字母串,就是commit对象的sha1值。因为git采用的分布式的版本控制系统,使用税数字会出现不同人提交的
出现id都是1,2,3等重复的情况,造成冲突。

commit下面还有作者和邮箱,可以咋子三个地方配置:

  • 1、/etc/gitconfig,通过git config --system设置
  • 2、~/.gitconfig通过git config --global设置
  • 3、针对特定项目,在.git/config文件中,通过git config --local设置
    3只会在特定项目生效,2只会在当前主机用户的文件系统中生效,1是全局生效。设置的地方优先级:3 > 2 > 1。

21.4 指令

1、查看git log

  • git log -3 查看最近3条commit
  • git log --author='saucxs' 查找一位作者叫saucxs的相关commit
  • git log --grep='saucxs' 查找commit信息包含saucxs的相关commit

2、查看当前的git配置,包括用户名,邮箱等

  • git config --list

3、删除掉.gitignore里面过滤的文件

  • git clean -fX

4、git add .和git add *区别

  • git add . 会被本地所有untrack文件加入暂存区,并且根据.gitignore做过滤
  • git add * 会忽略.gitgnore把任何文件都加入

5、分支相关

  • git branch 查看分支列表
  • git branch -a 查看所有分支,包括远程分支
  • git status 查看当前在哪个分支
  • git branch 分支名 创建新的分支
  • git checkout 分支名 切换分支
  • git branch -d 分支名 删除分支
  • git merge 分支名 将该分支合并到当前分支
  • git branch -m 原分支名 新分支名

6、工作区相关

  • git stash 将工作区保存
  • git stash list 列出所有的保存

7、回退

  • git reset --hard HEAD^ 回退到上一次的提交
  • git reset --hard HEAD^^ 回退到上上一次的提交
  • git reset --hard HEAD~n 回退到当前分钟至上的前n次提交
  • git reset --hard commit-id 信息的前几位 回退到指定commit
  • git reflog 查看我们执行了回退,但是我们又想回到该版本之后的commit id,reflog记录着Head指针的改变历史

8、裁减掉当前远程分支

  • git remote prune origin 把远程被删的分支删掉

9、git checkout commit-id与git reset --hard commit-id区别

两个都是可以回到对应的commit点,但是checkout的是处于游离状态,任何修改不提交的话会有警告,所以我们一般是创建新的分支。

10、仓库

  • git remote rename 仓库名 仓库名2 把远程仓库名从origin重命名为origin2
  • git remote rm 仓库名 删除远程仓库名

11、git pull会多一个merge commmit的情况,为什么?

git pull,会先执行git fetch,接着执行git merge,因为合并产生额外的commit,这时候就可以使用git pull --rebase来解决问题。

12、git rebase

直接使用根基分支的修改:当我们执行rebase时,当遇到冲突时,我们除了可以手动的去解决冲突外,还可以通过执行git rebase --skip直接的使用根基分支的修改。

与git merge区别:git rebase会将两条分支变成同一条commit链,而merge依然可以看到分叉的commit信息,原理:rebase修改了git的提交历史,把之前分支上的commit一个个接到另外一条分支上。

执行git reset commit-id --hard就可以回到rebase前的状态了。
另外在每次做rebase前,git都会为我们创建一个ORIG_HEAD记录rebase前的状态让我们不需要reflog就可以快速切回去,所以就可以直接执行git reset ORIG_HEAD --hard

13、开发中有个线上bug,功能写了一半,不能提交,怎么处置?

使用git stash将工作区的内容存储起来,然后切换新分支完成bug修复,在切换到未完成的分支,执行git stash pop将未完成的工作区还原到工作区。

git

欢迎关注

介绍

感谢你的阅读,本文由 sau交流学习社区 版权所有。
如若转载,请注明出处:sau交流学习社区-power by saucxs(程新松)(/page/842.html)
交流咨询
    官方QQ群
    群号663940201,欢迎加入!
    sau交流学习社区交流群

微信群
欢迎加入微信群
微信公众号
欢迎关注微信公众号

图文推荐

微信群
saucxs聊天机器人
saucxs
hi ,欢迎来到sau交流学习社区,欢迎与我聊天,问我问题哦!
您正在使用的浏览器是,不在支持范围内!
为了您的正常使用与展示,推荐使用Chrome浏览器68以上版本
支持浏览器:
火狐浏览器最新版
Safari浏览器最新版
Edge浏览器最新版
IE浏览器10,11