flura

flura 查看完整档案

深圳编辑南昌大学  |  计算机科学与技术 编辑南昌大学家园工作室  |  前端 编辑 www.flura.cn 编辑
编辑

计算机前端
github: https://github.com/fuchengjx
技术博客: https://www.flura.cn

个人动态

flura 赞了文章 · 8月24日

js字符串转化成json对象,使用JSON.parse()需要注意的地方

相信大部分人都知道或者去百度检索都会得到将js中的字符串转化成json对象常见的3种方法

举例:

var str = '{"name":"小明","age":18}';

将字符串转化json对象:

  1. var json = JSON.parse(str);
  2. var json = eval("(" + str + ")");
  3. var json = (new Function("return " + str))();

通过console.log()将json打印到控制台,不管是使用以上3种哪一种方法,都能在chrome控制台看到成功转化的结果:

图片描述

使用JSON.parse()方法来转化json对象,需要注意的坑点是什么?

1.字符串的数据格式

以上举例 str = '{"name":"小明","age":18}'; 属性name和age都用双引号引住,
有的人可能会习惯写成对象形式的字符串,如:str = '{name:"小明",age:18}';
结果使用JSON.parse()来转化会报错,因为使用JSON.parse需严格遵守JSON规范。

图片描述
图片描述

2.单引号与双引号

我们看到一开始的举例中 var str = '{"name":"小明","age":18}'; 使用单引号来套双引号,如果反过来写呢,如:var str = "{'name':'小明', 'age':18}";(相信也不少人习惯用双引号套单引号)

结果使用JSON.parse()来转化也会报错

图片描述
图片描述

3.兼容问题
IE6/7浏览器中不支持使用JSON.parse()方法转成json对象,所以需要引入一个json2.js文件。可以在这个网站(https://github.com/douglascro...)去下载对象文件。

最后总结来说,如果使用JSON.parse()方法来转化成json对象的数据格式的话,需要注意的是被转化的字符串里面的属性要使用引号,并且总体是单引号套双引号的方式,以及IE6/7浏览器是不支持该方法。

当然,如果你使用eval()或者new Function()的方式来转化,那就完全可以忽略上述的这两点需要注意的地方~( ̄▽ ̄~)(~ ̄▽ ̄)~哈哈


附:

eval() 函数可将字符串转换为代码执行,并返回一个或多个值

eval调用时,实例为eval( "( javascript代码 )" )

var str = "function(){alert('a');}“;

str = eval("("+str+")");

str();


eval()的返回值

eval()的返回值遵循以下规则:

1.如果eval()的参数不是字符串,那么eval()将直接返回参数。

2.如果eval()的参数是字符串,那么eval()将这个字符串解析成代码后进行执行,并返回最后一行代码执行的结果。

3.如果字符串无法解析成合法的代码,eval()将抛出SyntaxError错误。

查看原文

赞 8 收藏 10 评论 0

flura 赞了文章 · 8月19日

VSCode技术揭秘(一)

前言

Visual Studio Code(以下简称VSCode)是一个轻量且强大的跨平台开源代码编辑器(IDE),VSCode 采用了 Electron,使用的代码编辑器名为 Monaco、Monaco 也是 Visual Studio Team Service(Visual Studio Online)使用的代码编辑器,在语言上,VSCode 使用了自家的 TypeScript 语言开发。

VSCode提供了强大的插件拓展机制,并提供 插件市场 供开发者发布、下载插件。VSCode提供了丰富的扩展能力模型,例如基础的语法高亮/API提示、引用跳转(转到定义)、文件搜索、主题定制,高级的debug协议等等。但不允许插件直接访问底层UI DOM(即很难定制VSCode外观),因为VSCode开发团队随着优化VSCode而频繁更改UI Dom,所以将UI定制能力限制起来。

但是当你想要开发一款专用IDE时,不想从零开始撸,而是站在巨人的肩膀上做二次开发的话,那么VSCode将是你不二的选择,像 Weex Studio白鹭Egret Wing快应用IDE等IDE,都是基于VSCode扩展增强。

Weex Studio

Egret Wing

快应用

本系列文章将带你了解VSCode源码的整体架构和定制方法,一步一步从源码入手,定制一款专用开发工具。

技术介绍

学习VSCode源码的同学基本上都是做前端工作的,那么node.js和javascript都是基本功了,这里不用过分强调了。但是在阅读VSCode源码之前,还是需要对VSCode使用相关技术框架有所了解。

Electron介绍

众所周知,VSCode是一款桌面编辑器应用,但是前端单纯用js是做不了桌面应用的,所以采用Electron来构建。Electron是基于 Chromium 和 Node.js,使用 JavaScript, HTML 和 CSS 构建跨平台的桌面应用,它兼容 Mac、Windows 和 Linux,可以构建出三个平台的应用程序。


从实现上来看,Electron = Node.js + Chromium + Native API

也就是说Electron拥有Node运行环境,赋予了用户与系统底层进行交互的能力,依靠Chromium提供基于Web技术(HTML、CSS、JS)的界面交互支持,另外还具有一些平台特性,比如桌面通知等。

从API设计上来看,Electron App一般都有1个Main Process和多个Renderer Process:

  • main process:主进程环境下可以访问Node及Native API
  • renderer process:渲染器进程环境下可以访问Browser API和Node API及一部分Native API。

主进程和渲染进程

在Electron应用中,通过执行package.json中的main字段所指向的文件,可以开启electron的主进程(main process)。在主进程中使用BrowserWindow 实例创建web页面,而且一个electron应用有且只能有一个主进程。
主进程一般用于:

  • 多窗体管理(创建/切换)
  • 应用生命周期管理
  • 作为进程通信基站(IPC Server)
  • 工具条菜单栏注册

由于electron使用Chromium来展示web页面,Chromium多进程架构也会被用到。每一张web页面都运行在它自己的进程里,该进程称为渲染进程(renderer process)。渲染进程一般负责界面交互相关的,具体的业务功能。

在web页面里,调用系统底层的API是不被允许的,这是因为在web页面上处理底层GUI资源是非常危险的,很容易导致资源泄漏。如果你想要在web页面上执行GUI操作,相应web页面的渲染进程必须与主进程进行通信,向主进程发起请求去执行那些操作.在electron中,有几种主进程与渲染进程通信的方法,比如用ipcRenderer和ipcMain模块来发送信息,还有RPC风格的远程通信模块。关于Electron进程间通讯,这里不做过多的介绍,可以看Electron官网和网上资料来学习主进程和渲染进程间通讯。

更多的了解可以参考Electron应用架构

编辑器Monaco Editor

微软之前有个项目叫做Monaco Workbench,后来这个项目变成了VSCode,而Monaco Editor(下文简称monaco)就是从这个项目中成长出来的一个web编辑器,他们很大一部分的代码(monaco-editor-core)都是共用的,所以monaco和VSCode在编辑代码,交互以及UI上几乎是一摸一样的,有点不同的是,两者的平台不一样,monaco基于浏览器,而VSCode基于electron,所以功能上VSCode更加健全,并且性能比较强大。

TypeScript

TypeScript是JavaScript类型的超集,它可以编译成纯JavaScript。TypeScript可以在任何浏览器、任何计算机和任何操作系统中运行,并且是开源的。TypeScript具有以下特点:

  • 类型批注和编译时的类型检查
  • 强类型语言
  • 面向对象
  • 类class
  • 接口
  • lambda函数
  • 泛型

VSCode源码的编写主要用TypeScript,所以学习VSCode源码的时候还是先对TypeScript的基本使用有所了解。

以上内容是学习VSCode源码所要了解的基本内容,可以先学习Electron做个简单的桌面应用,然后学习一下TypeScript的基本语法,就可以开始VSCode源码的学习。

VSCode架构

VSCode中包含主进程,渲染进程,同时因为VSCode提供了插件的扩展能力,又出于安全稳定性的考虑,图中又多了一个Extension Host,其实这个Extension Host也是一个独立的进程,用于运行我们的插件代码。并且同渲染进程一样,彼此都是独立互不影响的。Extension Host Process暴露了一些VSCode的API供插件开发者去使用。

VSCode的进程结构

VSCode采用多进程架构,启动后主要由下面几个进程:

  • 后台进程
  • 编辑器窗口 - 由后台进程启动,也是多进程架构

    • HTML编写的UI

      • ActivityBar
      • SideBar
      • Panel
      • Editor
      • StatusBar
    • Nodejs异步IO

      • FileService
      • ConfigurationService
    • 插件宿主进程

      • 插件实例

        • 插件子进程 - 如TS语言服务
      • 插件实例
      • 插件实例
    • Debug进程
    • Search进程

后台进程

后台进程是 VSCode 的入口,主要负责管理编辑器生命周期,进程间通信,自动更新,菜单管理等。

我们启动 VSCode 的时候,后台进程会首先启动,读取各种配置信息和历史记录,然后将这些信息和主窗口 UI 的 HTML 主文件路径整合成一个 URL,启动一个浏览器窗口来显示编辑器的 UI。后台进程会一直关注 UI 进程的状态,当所有 UI 进程被关闭的时候,整个编辑器退出。

此外后台进程还会开启一个本地的 Socket,当有新的 VSCode 进程启动的时候,会尝试连接这个 Socket,并将启动的参数信息传递给它,由已经存在的 VSCode 来执行相关的动作,这样能够保证 VSCode 的唯一性,避免出现多开文件夹带来的问题。

编辑器窗口

编辑器窗口进程负责整个 UI 的展示。也就是我们所见的部分。UI 全部用 HTML 编写没有太多需要介绍的部分。

Nodejs异步IO

项目文件的读取和保存由主进程的 NodeJS API 完成,因为全部是异步操作,即便有比较大的文件,也不会对 UI 造成阻塞。IO 跟 UI 在一个进程,并采用异步操作,在保证 IO 性能的基础上也保证了 UI 的响应速度。

插件进程

每一个 UI 窗口会启动一个 NodeJS 子进程作为插件的宿主进程。所有的插件会共同运行在这个进程中。这样设计最主要的目的就是避免复杂的插件系统阻塞 UI 的响应。但是将插件放在一个单独进程也有很明显的缺点,因为是一个单独的进程,而不是 UI 进程,所以没有办法直接访问 DOM 树,想要实时高效的改变 UI 变得很难,在 VSCode 的扩展体系中几乎没有对 UI 进行扩展的 API。

Debug进程

Debugger 插件跟普通的插件有一点区别,它不运行在插件进程中,而是在每次 debug 的时候由UI单独新开一个进程。

搜索进程

搜索是一个十分耗时的任务,VSCode 也使用的单独的进程来实现这个功能,保证主窗口的效率。将耗时的任务分到多个进程中,有效的保证了主进程的响应速度。

VSCode源码运行

环境安装

  • Git
  • Node.JS,version >= 10.16.0, < 11.0.0
  • Yarn
  • Python,版本2.7以上,不支持3.0及其以上版本(mac电脑自带python不需要下载)

以上环境安装相信大家都轻车熟路了,由于我电脑使用的Mac,所以相关的示例都在Mac系统中运行,windows上的大同小异,具体可以参考官网Wiki文档

源码下载

VSCode的源码每次更新都会优化UI部分,但整体架构是没有差别的,可能网上的关于VSCode的源码教程用的老版本的VSCode,在这里我采用目前最新的版本 - v1.39.2版本来讲解。

源码下载:VSCode Releases

下载后解压用VSCode编辑器打开,在命令行中输入 yarn 命令来安装依赖,中间会很耗时。


中间会安装很多依赖包,如果发现网络不通、下载失败等情况,首先需要检查上述开发环境版本是否正确,必要时需要科学上网。

源码运行

依赖安装完成后,进入到项目中,执行 yarn watch 执行构建工作:


直到你看到 Finished compilation with 0 errors after 108726 ms 输出,说明构建成功了!


这时候不要关闭当前命令行,构建命令没有退出,它会监视vscode源码文件的变化,如果有变化,它会马上执行增量的构建,实时反映源码变化的结果。

新起一个命令行,执行 ./scripts/code.sh ,windows下执行 scriptscode.bat,此时会下载Electron。


下载完成后,即可运行。运行界面如下:

VSCode源码结构

整体文件目录结构如下所示:

├── build         # gulp编译构建脚本
├── extensions    # 内置插件
├── gulpfile.js   # gulp task
├── out           # 编译输出目录
├── resources     # 平台相关静态资源,图标等
├── scripts       # 工具脚本,开发/测试
├── src           # 源码目录
├── test          # 测试套件
└── product.json  # App meta信息

src下文件目录结构,如下图:

├── bootstrap-amd.js    # 子进程实际入口
├── bootstrap-fork.js   #
├── bootstrap-window.js #
├── bootstrap.js        # 子进程环境初始化
├── buildfile.js        # 构建config
├── cli.js              # CLI入口
├── main.js             # 主进程入口
├── paths.js            # AppDataPath与DefaultUserDataPath
├── typings
│   └── xxx.d.ts        # ts类型声明
└── vs
    ├── base            # 定义基础的工具方法和基础的 DOM UI 控件
    │   ├── browser     # 基础UI组件,DOM操作、交互事件、DnD等
    │   ├── common      # diff描述,markdown解析器,worker协议,各种工具函数
    │   ├── node        # Node工具函数
    │   ├── parts       # IPC协议(Electron、Node),quickopen、tree组件
    │   ├── test        # base单测用例
    │   └── worker      # Worker factory 和 main Worker(运行IDE Core:Monaco)
    ├── code            # VSCode Electron 应用的入口,包括 Electron 的主进程脚本入口
    │   ├── electron-browser # 需要 Electron 渲染器处理API的源代码(可以使用 common, browser, node)
    │   ├── electron-main    # 需要Electron主进程API的源代码(可以使用 common, node)
    │   ├── node        # 需要Electron主进程API的源代码(可以使用 common, node)
    │   ├── test
    │   └── code.main.ts
    ├── editor          # Monaco Editor 代码编辑器:其中包含单独打包发布的 Monaco Editor 和只能在 VSCode 的使用的部分
    │   ├── browser     # 代码编辑器核心
    │   ├── common      # 代码编辑器核心
    │   ├── contrib     # vscode 与独立 IDE共享的代码
    │   ├── standalone  # 独立 IDE 独有的代码
    │   ├── test
    │   ├── editor.all.ts
    │   ├── editor.api.ts
    │   ├── editor.main.ts
    │   └── editor.worker.ts
    ├── platform        # 依赖注入的实现和 VSCode 使用的基础服务 Services
    ├── workbench       # VSCode 桌面应用程序工作台的实现
    ├── buildunit.json
    ├── css.build.js    # 用于插件构建的CSS loader
    ├── css.js          # CSS loader
    ├── loader.js       # AMD loader(用于异步加载AMD模块,类似于require.js)
    ├── nls.build.js    # 用于插件构建的 NLS loader
    └── nls.js          # NLS(National Language Support)多语言loader

首先 VSCode 整体由其核心core和内置的扩展Extensions组成,core是实现了基本的代码编辑器和 VSCode 桌面应用程序,即 VSCode workbench,同时提供扩展 API,允许内置的扩展和第三方开发的扩展程序来扩展 VSCode Core 的能力。

其次,由于 VSCode 依赖 Electron,而 Electron 存在着主进程和渲染进程,它们能使用的 API 有所不到,所以 VSCode Core 中每个目录的组织也按照它们能使用的 API 来组织安排。在 Core 下的每个子目录下,按照代码所运行的目标环境分为以下几类:

  • common: 只使用 JavaScript API 的源代码,可能运行在任何环境
  • browser: 需要使用浏览器提供的 API 的源代码,如 DOM 操作等
  • node: 需要使用Node.js提供的 API 的源代码
  • electron-browser: 需要使用 Electron 渲染进程 API 的源代码
  • electron-main: 需要使用 Electron 主进程 API 的源代码

按照上述规则,即src/vs/workbench/browser中的源代码只能使用基本的 JavaScript API 和浏览器提供的 API,而src/vs/workbench/electron-browser中的源代码则可以使用 JavaScript API,浏览器提供的 API、Node.js提供的 API、和 Electron 渲染进程中的 API。

在 VSCode 代码仓库中,出了上述的src/vs的Core之外,还有一大块即 VSCode 内置的扩展,它们源代码位于extensions内。

VSCode 作为代码编辑器,与各种代码编辑的功能如语法高亮、补全提示、验证等都有扩展实现的。所以在 VSCode 的内置扩展内,一大部分都是各种编程语言的支持扩展,如:extensionshtml、extensionsjavascript、extensionscpp等等,大部分语言扩展中都会出现如.tmTheme、.tmLanguage等 TextMate 的语法定义。还有一类内置的扩展是 VSCode 主体扩展,如 VSCode 默认主体extensions/theme-defaults等。

VSCode启动流程

由于VSCode是基于Electron开发的,Electron的启动入口在package.json中,其中的 main 字段所表示的脚本为应用的启动脚本,它将会在主进程中执行。


./out/main.js显然这就是主进程的入口程序,但是main.js是在out文件夹下,很明显是编译输出出来的,然后找到src下tsconfig.json文件中有以下配置:

"outDir": "../out",

所以很明显是将src下代码编译后输出到out文件夹中。所以真实入口在src下main.js中,接下来只需从main.js文件分析即可。

在main.js中,我们可以看到下面一行引入

const app = require('electron').app;

electron.app负责管理Electron 应用程序的生命周期,运行在主进程中,然后找到 ready 监听事件

// Load our code once ready
app.once('ready', function () {
    if (args['trace']) {
        // @ts-ignore
        const contentTracing = require('electron').contentTracing;

        const traceOptions = {
            categoryFilter: args['trace-category-filter'] || '*',
            traceOptions: args['trace-options'] || 'record-until-full,enable-sampling'
        };

        contentTracing.startRecording(traceOptions, () => onReady());
    } else {
        onReady();
    }
});

这个ready监听表示,Electron 会在初始化后并准备,部分 API 在 ready 事件触发后才能使用。创建窗口也需要在ready后创建。最后这个函数中调用 onReady() 函数。

function onReady() {
    perf.mark('main:appReady');

    Promise.all([nodeCachedDataDir.ensureExists(), userDefinedLocale]).then(([cachedDataDir, locale]) => {
        if (locale && !nlsConfiguration) {
            nlsConfiguration = lp.getNLSConfiguration(product.commit, userDataPath, metaDataFile, locale);
        }

        if (!nlsConfiguration) {
            nlsConfiguration = Promise.resolve(undefined);
        }

        // First, we need to test a user defined locale. If it fails we try the app locale.
        // If that fails we fall back to English.
        nlsConfiguration.then(nlsConfig => {

            const startup = nlsConfig => {
                nlsConfig._languagePackSupport = true;
                process.env['VSCODE_NLS_CONFIG'] = JSON.stringify(nlsConfig);
                process.env['VSCODE_NODE_CACHED_DATA_DIR'] = cachedDataDir || '';

                // Load main in AMD
                perf.mark('willLoadMainBundle');
                require('./bootstrap-amd').load('vs/code/electron-main/main', () => {
                    perf.mark('didLoadMainBundle');
                });
            };

            // We received a valid nlsConfig from a user defined locale
            if (nlsConfig) {
                startup(nlsConfig);
            }

            // Try to use the app locale. Please note that the app locale is only
            // valid after we have received the app ready event. This is why the
            // code is here.
            else {
                let appLocale = app.getLocale();
                if (!appLocale) {
                    startup({ locale: 'en', availableLanguages: {} });
                } else {

                    // See above the comment about the loader and case sensitiviness
                    appLocale = appLocale.toLowerCase();

                    lp.getNLSConfiguration(product.commit, userDataPath, metaDataFile, appLocale).then(nlsConfig => {
                        if (!nlsConfig) {
                            nlsConfig = { locale: appLocale, availableLanguages: {} };
                        }

                        startup(nlsConfig);
                    });
                }
            }
        });
    }, console.error);
}

整个函数读取了用户语言设置,然后最终调用了 startup()

const startup = nlsConfig => {
    nlsConfig._languagePackSupport = true;
    process.env['VSCODE_NLS_CONFIG'] = JSON.stringify(nlsConfig);
    process.env['VSCODE_NODE_CACHED_DATA_DIR'] = cachedDataDir || '';

    // Load main in AMD
    perf.mark('willLoadMainBundle');
    require('./bootstrap-amd').load('vs/code/electron-main/main', () => {
        perf.mark('didLoadMainBundle');
    });
};

startup中主要是引入了 boostrap-amd ,这个bootstrap-amd引入了/vs/loader,并创建了一个loader。

const loader = require('./vs/loader');

loader是微软自家的AMD模块加载开源项目:https://github.com/Microsoft/...

然后通过loader加载 vs/code/electron-main/main 模块,这是 VSCode 真正的入口,然后在 vs/code/electron-main/main.ts 中可以看到定义了一个 CodeMain 类,然后初始化这个CodeMain类,并调用了 main 函数。

// src/vs/code/electron-main/main
class CodeMain {
    main(): void {
        ...
        // Launch
        this.startup(args);
    }

    private async startup(args: ParsedArgs): Promise<void> {

        // We need to buffer the spdlog logs until we are sure
        // we are the only instance running, otherwise we'll have concurrent
        // log file access on Windows (https://github.com/Microsoft/vscode/issues/41218)
        const bufferLogService = new BufferLogService();

        const [instantiationService, instanceEnvironment] = this.createServices(args, bufferLogService);
        try {

            // Init services
            await instantiationService.invokeFunction(async accessor => {
                const environmentService = accessor.get(IEnvironmentService);
                const configurationService = accessor.get(IConfigurationService);
                const stateService = accessor.get(IStateService);

                try {
                    await this.initServices(environmentService, configurationService as ConfigurationService, stateService as StateService);
                } catch (error) {

                    // Show a dialog for errors that can be resolved by the user
                    this.handleStartupDataDirError(environmentService, error);

                    throw error;
                }
            });

            // Startup
            await instantiationService.invokeFunction(async accessor => {
                const environmentService = accessor.get(IEnvironmentService);
                const logService = accessor.get(ILogService);
                const lifecycleMainService = accessor.get(ILifecycleMainService);
                const configurationService = accessor.get(IConfigurationService);

                const mainIpcServer = await this.doStartup(logService, environmentService, lifecycleMainService, instantiationService, true);

                bufferLogService.logger = new SpdLogService('main', environmentService.logsPath, bufferLogService.getLevel());
                once(lifecycleMainService.onWillShutdown)(() => (configurationService as ConfigurationService).dispose());

                return instantiationService.createInstance(CodeApplication, mainIpcServer, instanceEnvironment).startup();
            });
        } catch (error) {
            instantiationService.invokeFunction(this.quit, error);
        }
    }

    private createServices(args: ParsedArgs, bufferLogService: BufferLogService): [IInstantiationService, typeof process.env] {
        const services = new ServiceCollection();

        const environmentService = new EnvironmentService(args, process.execPath);
        const instanceEnvironment = this.patchEnvironment(environmentService); // Patch `process.env` with the instance's environment
        services.set(IEnvironmentService, environmentService);

        const logService = new MultiplexLogService([new ConsoleLogMainService(getLogLevel(environmentService)), bufferLogService]);
        process.once('exit', () => logService.dispose());
        services.set(ILogService, logService);

        services.set(IConfigurationService, new ConfigurationService(environmentService.settingsResource));
        services.set(ILifecycleMainService, new SyncDescriptor(LifecycleMainService));
        services.set(IStateService, new SyncDescriptor(StateService));
        services.set(IRequestService, new SyncDescriptor(RequestMainService));
        services.set(IThemeMainService, new SyncDescriptor(ThemeMainService));
        services.set(ISignService, new SyncDescriptor(SignService));

        return [new InstantiationService(services, true), instanceEnvironment];
    }
    ...
}

// Main Startup
const code = new CodeMain();
code.main();

可以看到 main() 函数最终调用了 startup() 函数。

startup() 函数中,先调用了 this.createServices() 函数来创建依赖的Services。
Services(服务) 是 VSCode 中一系列可以被注入的公共模块,这些 Services 分别负责不同的功能,在这里创建了几个基本服务。除了这些基本服务,VSCode 内还包含了大量的服务,如 IModeService、ICodeEditorService、IPanelService 等,通过 VSCode 实现的「依赖注入」模式,可以在需要用到这些服务的地方以 Decorator 的方式做为构造函数参数声明依赖,会被自动注入到类中。关于服务的依赖注入,后面的章节会重点讲解。

private createServices(args: ParsedArgs, bufferLogService: BufferLogService): [IInstantiationService, typeof process.env] {
        const services = new ServiceCollection();

        const environmentService = new EnvironmentService(args, process.execPath);
        const instanceEnvironment = this.patchEnvironment(environmentService); // Patch `process.env` with the instance's environment
        // environmentService 一些基本配置,包括运行目录、用户数据目录、工作区缓存目录等
        services.set(IEnvironmentService, environmentService);

        const logService = new MultiplexLogService([new ConsoleLogMainService(getLogLevel(environmentService)), bufferLogService]);
        process.once('exit', () => logService.dispose());
        // logService 日志服务
        services.set(ILogService, logService);

        // ConfigurationService 配置项
        services.set(IConfigurationService, new ConfigurationService(environmentService.settingsResource));
        // LifecycleService 生命周期相关的一些方法
        services.set(ILifecycleMainService, new SyncDescriptor(LifecycleMainService));
        // StateService 持久化数据
        services.set(IStateService, new SyncDescriptor(StateService));
        // RequestService 请求服务
        services.set(IRequestService, new SyncDescriptor(RequestMainService));
        services.set(IThemeMainService, new SyncDescriptor(ThemeMainService));
        services.set(ISignService, new SyncDescriptor(SignService));

        return [new InstantiationService(services, true), instanceEnvironment];
    }

代码中可以看到 createServices() 最终实例化了一个 InstantiationService 实例并return回去,然后在 startup() 中调用 InstantiationService的createInstance方法并传参数CodeApplication,表示初始化CodeApplication实例,然后调用实例的 startup() 方法。

return instantiationService.createInstance(CodeApplication, mainIpcServer,instanceEnvironment).startup();

接下来我们去看CodeApplication中的startup方法。

//src/vs/code/electron-main/app.ts
export class CodeApplication extends Disposable {
    ...
    async startup(): Promise<void> {
        this.logService.debug('Starting VS Code');
        this.logService.debug(`from: ${this.environmentService.appRoot}`);
        this.logService.debug('args:', this.environmentService.args);

        // Make sure we associate the program with the app user model id
        // This will help Windows to associate the running program with
        // any shortcut that is pinned to the taskbar and prevent showing
        // two icons in the taskbar for the same app.
        const win32AppUserModelId = product.win32AppUserModelId;
        if (isWindows && win32AppUserModelId) {
            app.setAppUserModelId(win32AppUserModelId);
        }

        // Fix native tabs on macOS 10.13
        // macOS enables a compatibility patch for any bundle ID beginning with
        // "com.microsoft.", which breaks native tabs for VS Code when using this
        // identifier (from the official build).
        // Explicitly opt out of the patch here before creating any windows.
        // See: https://github.com/Microsoft/vscode/issues/35361#issuecomment-399794085
        try {
            if (isMacintosh && this.configurationService.getValue<boolean>('window.nativeTabs') === true && !systemPreferences.getUserDefault('NSUseImprovedLayoutPass', 'boolean')) {
                systemPreferences.setUserDefault('NSUseImprovedLayoutPass', 'boolean', true as any);
            }
        } catch (error) {
            this.logService.error(error);
        }

        // Create Electron IPC Server
        const electronIpcServer = new ElectronIPCServer();

        // Resolve unique machine ID
        this.logService.trace('Resolving machine identifier...');
        const { machineId, trueMachineId } = await this.resolveMachineId();
        this.logService.trace(`Resolved machine identifier: ${machineId} (trueMachineId: ${trueMachineId})`);

        // Spawn shared process after the first window has opened and 3s have passed
        const sharedProcess = this.instantiationService.createInstance(SharedProcess, machineId, this.userEnv);
        const sharedProcessClient = sharedProcess.whenReady().then(() => connect(this.environmentService.sharedIPCHandle, 'main'));
        this.lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen).then(() => {
            this._register(new RunOnceScheduler(async () => {
                const userEnv = await getShellEnvironment(this.logService, this.environmentService);

                sharedProcess.spawn(userEnv);
            }, 3000)).schedule();
        });

        // Services
        const appInstantiationService = await this.createServices(machineId, trueMachineId, sharedProcess, sharedProcessClient);

        // Create driver
        if (this.environmentService.driverHandle) {
            const server = await serveDriver(electronIpcServer, this.environmentService.driverHandle!, this.environmentService, appInstantiationService);

            this.logService.info('Driver started at:', this.environmentService.driverHandle);
            this._register(server);
        }

        // Setup Auth Handler
        this._register(new ProxyAuthHandler());

        // Open Windows
        const windows = appInstantiationService.invokeFunction(accessor => this.openFirstWindow(accessor, electronIpcServer, sharedProcessClient));

        // Post Open Windows Tasks
        this.afterWindowOpen();

        // Tracing: Stop tracing after windows are ready if enabled
        if (this.environmentService.args.trace) {
            this.stopTracingEventually(windows);
        }
    }

    ...

    private openFirstWindow(accessor: ServicesAccessor, electronIpcServer: ElectronIPCServer, sharedProcessClient: Promise<Client<string>>): ICodeWindow[] {

        // Register more Main IPC services
        const launchMainService = accessor.get(ILaunchMainService);
        const launchChannel = createChannelReceiver(launchMainService, { disableMarshalling: true });
        this.mainIpcServer.registerChannel('launch', launchChannel);

        // Register more Electron IPC services
        const updateService = accessor.get(IUpdateService);
        const updateChannel = new UpdateChannel(updateService);
        electronIpcServer.registerChannel('update', updateChannel);

        const issueService = accessor.get(IIssueService);
        const issueChannel = createChannelReceiver(issueService);
        electronIpcServer.registerChannel('issue', issueChannel);

        const electronService = accessor.get(IElectronService);
        const electronChannel = createChannelReceiver(electronService);
        electronIpcServer.registerChannel('electron', electronChannel);
        sharedProcessClient.then(client => client.registerChannel('electron', electronChannel));

        const sharedProcessMainService = accessor.get(ISharedProcessMainService);
        const sharedProcessChannel = createChannelReceiver(sharedProcessMainService);
        electronIpcServer.registerChannel('sharedProcess', sharedProcessChannel);

        const workspacesService = accessor.get(IWorkspacesService);
        const workspacesChannel = createChannelReceiver(workspacesService);
        electronIpcServer.registerChannel('workspaces', workspacesChannel);

        const menubarService = accessor.get(IMenubarService);
        const menubarChannel = createChannelReceiver(menubarService);
        electronIpcServer.registerChannel('menubar', menubarChannel);

        const urlService = accessor.get(IURLService);
        const urlChannel = createChannelReceiver(urlService);
        electronIpcServer.registerChannel('url', urlChannel);

        const storageMainService = accessor.get(IStorageMainService);
        const storageChannel = this._register(new GlobalStorageDatabaseChannel(this.logService, storageMainService));
        electronIpcServer.registerChannel('storage', storageChannel);

        const loggerChannel = new LoggerChannel(accessor.get(ILogService));
        electronIpcServer.registerChannel('logger', loggerChannel);
        sharedProcessClient.then(client => client.registerChannel('logger', loggerChannel));

        // ExtensionHost Debug broadcast service
        electronIpcServer.registerChannel(ExtensionHostDebugBroadcastChannel.ChannelName, new ExtensionHostDebugBroadcastChannel());

        // Signal phase: ready (services set)
        this.lifecycleMainService.phase = LifecycleMainPhase.Ready;

        // Propagate to clients
        const windowsMainService = this.windowsMainService = accessor.get(IWindowsMainService);
        this.dialogMainService = accessor.get(IDialogMainService);

        // Create a URL handler to open file URIs in the active window
        const environmentService = accessor.get(IEnvironmentService);
        urlService.registerHandler({
            async handleURL(uri: URI, options?: IOpenURLOptions): Promise<boolean> {

                // Catch file URLs
                if (uri.authority === Schemas.file && !!uri.path) {
                    const cli = assign(Object.create(null), environmentService.args);
                    const urisToOpen = [{ fileUri: URI.file(uri.fsPath) }];

                    windowsMainService.open({ context: OpenContext.API, cli, urisToOpen, gotoLineMode: true });

                    return true;
                }

                return false;
            }
        });

        // Create a URL handler which forwards to the last active window
        const activeWindowManager = new ActiveWindowManager(electronService);
        const activeWindowRouter = new StaticRouter(ctx => activeWindowManager.getActiveClientId().then(id => ctx === id));
        const urlHandlerRouter = new URLHandlerRouter(activeWindowRouter);
        const urlHandlerChannel = electronIpcServer.getChannel('urlHandler', urlHandlerRouter);
        const multiplexURLHandler = new URLHandlerChannelClient(urlHandlerChannel);

        // On Mac, Code can be running without any open windows, so we must create a window to handle urls,
        // if there is none
        if (isMacintosh) {
            urlService.registerHandler({
                async handleURL(uri: URI, options?: IOpenURLOptions): Promise<boolean> {
                    if (windowsMainService.getWindowCount() === 0) {
                        const cli = { ...environmentService.args };
                        const [window] = windowsMainService.open({ context: OpenContext.API, cli, forceEmpty: true, gotoLineMode: true });

                        await window.ready();

                        return urlService.open(uri);
                    }

                    return false;
                }
            });
        }

        // Register the multiple URL handler
        urlService.registerHandler(multiplexURLHandler);

        // Watch Electron URLs and forward them to the UrlService
        const args = this.environmentService.args;
        const urls = args['open-url'] ? args._urls : [];
        const urlListener = new ElectronURLListener(urls || [], urlService, windowsMainService);
        this._register(urlListener);

        // Open our first window
        const macOpenFiles: string[] = (<any>global).macOpenFiles;
        const context = !!process.env['VSCODE_CLI'] ? OpenContext.CLI : OpenContext.DESKTOP;
        const hasCliArgs = args._.length;
        const hasFolderURIs = !!args['folder-uri'];
        const hasFileURIs = !!args['file-uri'];
        const noRecentEntry = args['skip-add-to-recently-opened'] === true;
        const waitMarkerFileURI = args.wait && args.waitMarkerFilePath ? URI.file(args.waitMarkerFilePath) : undefined;

        // new window if "-n" was used without paths
        if (args['new-window'] && !hasCliArgs && !hasFolderURIs && !hasFileURIs) {
            return windowsMainService.open({
                context,
                cli: args,
                forceNewWindow: true,
                forceEmpty: true,
                noRecentEntry,
                waitMarkerFileURI,
                initialStartup: true
            });
        }

        // mac: open-file event received on startup
        if (macOpenFiles && macOpenFiles.length && !hasCliArgs && !hasFolderURIs && !hasFileURIs) {
            return windowsMainService.open({
                context: OpenContext.DOCK,
                cli: args,
                urisToOpen: macOpenFiles.map(file => this.getWindowOpenableFromPathSync(file)),
                noRecentEntry,
                waitMarkerFileURI,
                gotoLineMode: false,
                initialStartup: true
            });
        }

        // default: read paths from cli
        return windowsMainService.open({
            context,
            cli: args,
            forceNewWindow: args['new-window'] || (!hasCliArgs && args['unity-launch']),
            diffMode: args.diff,
            noRecentEntry,
            waitMarkerFileURI,
            gotoLineMode: args.goto,
            initialStartup: true
        });
    }
    ...
}

在CodeApplication.startup 中首先会启动 SharedProcess 共享进程,同时也创建了一些窗口相关的服务,包括 WindowsManager、WindowsService、MenubarService 等,负责窗口、多窗口管理及菜单等功能。然后调用 openFirstWindow 方法来开启窗口。

在openFirstWindow中,先创建一系列 Electron 的 IPC 频道,用于主进程和渲染进程间通信,其中 window 和 logLevel 频道还会被注册到 sharedProcessClient ,sharedProcessClient 是主进程与共享进程(SharedProcess)进行通信的 client,之后根据 environmentService 提供的相关参数(file_uri、folder_uri)调用了 windowsMainService.open 方法。

windowsMainService是WindowsManager实例化的服务,而WindowsManager是多窗体管理类(src/vs/code/electron-main/windows.ts)。接下来我们看windowsMainService.open 方法,可以看到其调用了doOpen方法。

open(openConfig: IOpenConfiguration): ICodeWindow[] {
        ...

        // Open based on config
        const usedWindows = this.doOpen(openConfig, workspacesToOpen, foldersToOpen, 
        ...
    }

doOpen方法最终调用了openInBrowserWindow方法。

private doOpen(
        openConfig: IOpenConfiguration,
        workspacesToOpen: IWorkspacePathToOpen[],
        foldersToOpen: IFolderPathToOpen[],
        emptyToRestore: IEmptyWindowBackupInfo[],
        emptyToOpen: number,
        fileInputs: IFileInputs | undefined,
        foldersToAdd: IFolderPathToOpen[]
    ) {
        const usedWindows: ICodeWindow[] = [];

        ...

        // Handle empty to open (only if no other window opened)
        if (usedWindows.length === 0 || fileInputs) {
            if (fileInputs && !emptyToOpen) {
                emptyToOpen++;
            }

            const remoteAuthority = fileInputs ? fileInputs.remoteAuthority : (openConfig.cli && openConfig.cli.remote || undefined);

            for (let i = 0; i < emptyToOpen; i++) {
                usedWindows.push(this.openInBrowserWindow({
                    userEnv: openConfig.userEnv,
                    cli: openConfig.cli,
                    initialStartup: openConfig.initialStartup,
                    remoteAuthority,
                    forceNewWindow: openFolderInNewWindow,
                    forceNewTabbedWindow: openConfig.forceNewTabbedWindow,
                    fileInputs
                }));

                // Reset these because we handled them
                fileInputs = undefined;
                openFolderInNewWindow = true; // any other window to open must open in new window then
            }
        }

        return arrays.distinct(usedWindows);
    }

在openInBrowserWindow中,创建一个CodeWindow实例并返回,并且还调用了doOpenInBrowserWindow这个方法,这个方法看下文介绍。

private openInBrowserWindow(options: IOpenBrowserWindowOptions): ICodeWindow {

        ...

        // Create the window
        window = this.instantiationService.createInstance(CodeWindow, {
            state,
            extensionDevelopmentPath: configuration.extensionDevelopmentPath,
            isExtensionTestHost: !!configuration.extensionTestsPath
        });
        ...

        // If the window was already loaded, make sure to unload it
        // first and only load the new configuration if that was
        // not vetoed
        if (window.isReady) {
            this.lifecycleMainService.unload(window, UnloadReason.LOAD).then(veto => {
                if (!veto) {
                    this.doOpenInBrowserWindow(window!, configuration, options);
                }
            });
        } else {
            this.doOpenInBrowserWindow(window, configuration, options);
        }

        return window;
    }

接下来我们找到CodeWindow定义在src/vs/code/electron-main/window.ts中,在CodeWindow的构造函数中调用了createBrowserWindow方法,然后在createBrowserWindow方法中看到实例化了一个BrowserWindow,这是Electron中浏览器窗口的定义。

//src/vs/code/electron-main/window.ts
export class CodeWindow extends Disposable implements ICodeWindow {

    ...

    constructor(
        config: IWindowCreationOptions,
        @ILogService private readonly logService: ILogService,
        @IEnvironmentService private readonly environmentService: IEnvironmentService,
        @IFileService private readonly fileService: IFileService,
        @IConfigurationService private readonly configurationService: IConfigurationService,
        @IThemeMainService private readonly themeMainService: IThemeMainService,
        @IWorkspacesMainService private readonly workspacesMainService: IWorkspacesMainService,
        @IBackupMainService private readonly backupMainService: IBackupMainService,
    ) {
        super();

        this.touchBarGroups = [];
        this._lastFocusTime = -1;
        this._readyState = ReadyState.NONE;
        this.whenReadyCallbacks = [];

        // create browser window
        this.createBrowserWindow(config);

        // respect configured menu bar visibility
        this.onConfigurationUpdated();

        // macOS: touch bar support
        this.createTouchBar();

        // Request handling
        this.handleMarketplaceRequests();

        // Eventing
        this.registerListeners();
    }

    private createBrowserWindow(config: IWindowCreationOptions): void {

        ...
        // Create the browser window.
        this._win = new BrowserWindow(options);
        ...
    }
}

现在窗口有了,那么什么时候加载页面呢?刚刚我们在上文提到,在openInBrowserWindow中,创建一个CodeWindow实例并返回,并且还调用了doOpenInBrowserWindow这个方法,那么我们看一下这个方法的定义。

private doOpenInBrowserWindow(window: ICodeWindow, configuration: IWindowConfiguration, options: IOpenBrowserWindowOptions): void {
        ...
        // Load it
        window.load(configuration);
        ...
    }

这个方法有调用CodeWindow的load方法,然后看一下load方法的定义。会看到调用了this._win.loadURL,这个this._win就是CodeWindow创建的BrowserWindow窗口,这就找到了窗口加载的URL时机。

load(config: IWindowConfiguration, isReload?: boolean, disableExtensions?: boolean): void {
    ...
    // Load URL
    perf.mark('main:loadWindow');
    this._win.loadURL(this.getUrl(configuration));
    ...
}

然后看一下getUrl方法的定义,最终返回的configUrl是调用doGetUrl获取的。

private getUrl(windowConfiguration: IWindowConfiguration): string {

        ...
        let configUrl = this.doGetUrl(config);
        ...

        return configUrl;
    }

然后看一下doGetUrl方法,可以看到返回的Url路径为vs/code/electron-browser/workbench/workbench.html。

private doGetUrl(config: object): string {
    return `${require.toUrl('vs/code/electron-browser/workbench/workbench.html')}?config=${encodeURIComponent(JSON.stringify(config))}`;
}

这是整个 Workbench 的入口,HTML出现了,主进程的使命完成,渲染进程登场。

//src/vs/code/electron-browser/workbench/workbench.html
<!-- Copyright (C) Microsoft Corporation. All rights reserved. -->
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src 'self' https: data: blob: vscode-remote-resource:; media-src 'none'; frame-src 'self' https://*.vscode-webview-test.com; object-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' https:; font-src 'self' https: vscode-remote-resource:;">
    </head>
    <body class="vs-dark" aria-label="">
    </body>

    <!-- Startup via workbench.js -->
    <script data-original="workbench.js"></script>
</html>

workbench.html中加载了workbench.js文件,这个文件负责加载真正的 Workbench 模块并调用其 main 方法初始化主界面。

// src/vs/code/electron-browser/workbench/workbench.js
const bootstrapWindow = require('../../../../bootstrap-window');

// Setup shell environment
process['lazyEnv'] = getLazyEnv();

// Load workbench main JS, CSS and NLS all in parallel. This is an
// optimization to prevent a waterfall of loading to happen, because
// we know for a fact that workbench.desktop.main will depend on
// the related CSS and NLS counterparts.
bootstrapWindow.load([
    'vs/workbench/workbench.desktop.main',
    'vs/nls!vs/workbench/workbench.desktop.main',
    'vs/css!vs/workbench/workbench.desktop.main'
],
    function (workbench, configuration) {
        perf.mark('didLoadWorkbenchMain');

        return process['lazyEnv'].then(function () {
            perf.mark('main/startup');

            // @ts-ignore
            //加载 Workbench 并初始化主界面
            return require('vs/workbench/electron-browser/desktop.main').main(configuration);
        });
    }, {
        removeDeveloperKeybindingsAfterLoad: true,
        canModifyDOM: function (windowConfig) {
            showPartsSplash(windowConfig);
        },
        beforeLoaderConfig: function (windowConfig, loaderConfig) {
            loaderConfig.recordStats = true;
        },
        beforeRequire: function () {
            perf.mark('willLoadWorkbenchMain');
        }
    });

我们可以看到加载了vs/workbench/electron-browser/desktop.main模块,并调用了模块的main方法。main方法中实例化了一个DesktopMain,并调用了DesktopMain的open方法。

class DesktopMain extends Disposable {

    async open(): Promise<void> {
        ...

        // Create Workbench
        const workbench = new Workbench(document.body, services.serviceCollection, services.logService);

        // Listeners
        this.registerListeners(workbench, services.storageService);

        // Startup
        const instantiationService = workbench.startup();
        ...
    }
    ...
}

export function main(configuration: IWindowConfiguration): Promise<void> {
    const renderer = new DesktopMain(configuration);
    return renderer.open();
}

我们看到DesktopMain的open方法中实例化了Workbench类,并调用了Workbench的startup方法。接下来我们看一下这个Workbench类。

export class Workbench extends Layout {

    ...
    startup(): IInstantiationService {
        try {

            // Configure emitter leak warning threshold
            setGlobalLeakWarningThreshold(175);

            // ARIA
            setARIAContainer(document.body);

            // Services
            const instantiationService = this.initServices(this.serviceCollection);

            instantiationService.invokeFunction(async accessor => {
                const lifecycleService = accessor.get(ILifecycleService);
                const storageService = accessor.get(IStorageService);
                const configurationService = accessor.get(IConfigurationService);

                // Layout
                this.initLayout(accessor);

                // Registries
                this.startRegistries(accessor);

                // Context Keys
                this._register(instantiationService.createInstance(WorkbenchContextKeysHandler));

                // Register Listeners
                this.registerListeners(lifecycleService, storageService, configurationService);

                // Render Workbench
                this.renderWorkbench(instantiationService, accessor.get(INotificationService) as NotificationService, storageService, configurationService);

                // Workbench Layout
                this.createWorkbenchLayout(instantiationService);

                // Layout
                this.layout();

                // Restore
                try {
                    await this.restoreWorkbench(accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IViewletService), accessor.get(IPanelService), accessor.get(ILogService), lifecycleService);
                } catch (error) {
                    onUnexpectedError(error);
                }
            });

            return instantiationService;
        } catch (error) {
            onUnexpectedError(error);

            throw error; // rethrow because this is a critical issue we cannot handle properly here
        }
    }
    ...
}

我们可以看到Workbench继承Layout布局类,在 workbench.startup 方法中构建主界面布局、创建全局事件监听以及实例化一些依赖的服务,全部完成后会还原之前打开的编辑器,整个 Workbench 加载完成。

所以前文中的大量代码只是为这里最终创建主界面做铺垫,Workbench 模块主要代码都在 vs/workbench 目录下,主要负责界面元素的创建和具体业务功能的实现。

至此,从启动到加载到html,再到构建主界面布局,整个流程很清晰。

应用信息定制

在VSCode源码根目录下有一个product.json文件,此文件用于配置应用的信息。

{
    "nameShort": "Code - OSS",
    "nameLong": "Code - OSS",
    "applicationName": "code-oss",
    "dataFolderName": ".vscode-oss",
    "win32MutexName": "vscodeoss",
    "licenseName": "MIT",
    "licenseUrl": "https://github.com/Microsoft/vscode/blob/master/LICENSE.txt",
    "win32DirName": "Microsoft Code OSS",
    "win32NameVersion": "Microsoft Code OSS",
    "win32RegValueName": "CodeOSS",
    "win32AppId": "{{E34003BB-9E10-4501-8C11-BE3FAA83F23F}",
    "win32x64AppId": "{{D77B7E06-80BA-4137-BCF4-654B95CCEBC5}",
    "win32UserAppId": "{{C6065F05-9603-4FC4-8101-B9781A25D88E}",
    "win32x64UserAppId": "{{C6065F05-9603-4FC4-8101-B9781A25D88E}",
    "win32AppUserModelId": "Microsoft.CodeOSS",
    "win32ShellNameShort": "C&ode - OSS",
    "darwinBundleIdentifier": "com.visualstudio.code.oss",
    "linuxIconName": "com.visualstudio.code.oss",
    "licenseFileName": "LICENSE.txt",
    "reportIssueUrl": "https://github.com/Microsoft/vscode/issues/new",
    "urlProtocol": "code-oss",
    "extensionAllowedProposedApi": [
        "ms-vscode.references-view"
    ]
}

可以修改product.json的信息来更新定制VSCode的名称等信息。如果你在执行了./scripts/code.sh后修改了product.json的信息,比如修改了nameLong的配置,这时候重新运行./scripts/code.sh会报错。

错误信息是 ./scripts/code.sh: line 53: /Users/jiangshuaijie/Desktop/vscode-1.39.2/.build/electron/test.app/Contents/MacOS/Electron: No such file or directory ,可以看出是在code.sh中报错了,看一下code.sh中内容。

...
function code() {
    cd "$ROOT"
    if [[ "$OSTYPE" == "darwin"* ]]; then
        NAME=`node -p "require('./product.json').nameLong"`
        CODE="./.build/electron/$NAME.app/Contents/MacOS/Electron"
    else
        NAME=`node -p "require('./product.json').applicationName"`
        CODE=".build/electron/$NAME"
    fi
    ...
    # Launch Code
    exec "$CODE" . "$@"
}
...

最终根据product.json中的nameLong来运行根目录下.build/electron/下生成的app,这时候的应用是之前生成过的,所以会报错。我们只需删除掉根目录下.build文件夹,重新执行./scripts/code.sh即可。

应用图标定制

在VSCode源码根目录下resources文件夹主要用于存放VSCode平台的静态资源,例如应用图标等。

其中darwin、linux、win32对应三个不同的平台,可以在不同平台文件夹下替换图片资源。

更多精彩请关注 https://codeteenager.github.i... ,后面会更新更多内容。
查看原文

赞 3 收藏 2 评论 1

flura 回答了问题 · 2019-11-28

解决vue有没有好用的三级日期联动控件

关注 4 回答 3

flura 关注了问题 · 2019-11-28

父子组件双向绑定的问题

请问父子组件的值怎么双向绑定,子组件代码我简略了

父组件

<plants-select v-model="form.plantCode" />

子组件

<el-select clearable v-model="factoryId">
      <el-option v-for="item in factoryRole" :key="item.code" :label="item.name" :value="item.code"></el-option>
</el-select>
export default {
  data() {
    return {
      factoryId: '',
      factoryList: [{},{},{}]
    }
  },
  props: {
    value: {
      type: String,
      required: false
    }
  },
  methods: {
    handleChange(val) {
     
    }
  }
}

关注 4 回答 3

flura 回答了问题 · 2019-11-28

父子组件双向绑定的问题

.sync 修饰符实现了对一个 prop 进行“双向绑定”,当一个子组件改变了一个带 .sync 的 prop 的值时,这个变化也会同步到父组件中所绑定的值。

关注 4 回答 3

flura 发布了文章 · 2019-11-23

前端gitlab-ci实现自动化部署


本文是我配置一个Gitlab CI实现一个前端项目自动打包部署的踩坑体会。

背景

为什么要去配置这么一个自动化部署CI,这个需求是什么?

我所接手的这一个项目是比较老比较大的项目(vue),它依赖了一些很麻烦的包,这些包很难在window下环境友好运行,所以导致这个项目无法打包部署。这给我们整个团队带来了很大的不便,部署只能用linux很麻烦的手动部署。(学生党不可能人人有钱买Mac吧),所以我想配置一个gitlab-ci实现项目的自动部署,以提升效率。

介绍

Gitlab

一个基于git实现的在线代码仓库软件,你可以用Gitlab自己搭建一个类似于Github一样的系统,一般用于在企业、学校等内部网络搭建Git私服。

什么是持续集成/持续部署(CI/CD)?

阮一峰有篇文章很好的介绍了持续集成是什么?

持续集成指的是,频繁地(一天多次)将代码集成到主干。

持续集成的目的,就是让产品可以快速迭代,同时还能保持高质量。它的核心措施是,代码集成到主干之前,必须通过自动化测试。只要有一个测试用例失败,就不能集成。

通俗易懂的来说就是把 代码测试、打包、发布等工作交给一些工具来自动完成、这样可以提高很多效率,减少失误,开发人员只要关心把代码提交到git就行了。

Gitlab的CI

从 GitLab 8.0 开始,GitLab CI 就已经集成在 GitLab 中,我们只要在项目中添加一个 .gitlab-ci.yml 文件,然后添加一个 Runner,即可进行持续集成。 而且随着 GitLab 的升级,GitLab CI 变得越来越强大。

.gitlab-ci.yml

在git项目的根目录下的一个文件,记录了一系列的阶段和执行规则。GitLab-CI在push后会解析它,根据里面的内容调用runner来运行。

简单来说就是,你利用Git版本管理Push了本地代码到你的gitlab.com上,然后Gitlab,就通知你的服务器(runner服务器),也就是Gitlab-runner来运行构建任务。然后跑测试用例,测试用例通过了就生成Build出相应的环境的代码,自动部署上不同的环境服务器上面去。

GitLab-Runner

这个是脚本执行的承载者,.gitlab-ci.yml的script部分的运行就是由runner来负责的。GitLab-CI浏览过项目里的.gitlab-ci.yml文件之后,根据里面的规则,分配到各个Runner来运行相应的脚本script。这些脚本有的是测试项目用的,有的是部署用的。

1569760156912

快速开始

简而言之,CI所需要的步骤可以归结为:

  1. 添加.gitlab-ci.yml到项目的根目录
  2. 配置一个Runner

从此刻开始,在每一次push到Git仓库的过程中,Runner会自动开启pipline,pipline将显示在项目的Pipline页面中。


本指南要求:

  • 使用版本8.0+ 的GitLab实例或者是使用GitLab.com
  • 一个想使用GitLab CI的项目

配置.gitlab-ci.yml

  1. 在项目的根目录创建一个名为.gitlab-ci.yml的文件。注意:.gitlab-ci.yml是一个###&_10_###&文件,所以必须要格外注意锁紧。使用空格,而不是tabs。
   image: node  # 选用docker镜像
   
   stages: # Stages 表示构建阶段,这里有两个阶段 install, deploy
   - install
   - deploy
   
   install-staging:dep: # Jobs 表示构建工作,表示某个 Stage 里面执行的工作。
     stage: install
     only: # 定义了只有在被merge到了master分支上 才会执行部署脚本。
     - master
     script:
     - echo "=====start install======"
     - npm install --registry=https://registry.npm.taobao.org  #安装依赖
     - echo "=====end install======"
     artifacts:  # 将这个job生成的依赖传递给下一个job。需要设置dependencies
       expire_in: 60 mins   # artifacets 的过期时间,因为这些数据都是直接保存在 Gitlab 机器上的,过于久远的资源就可以删除掉了
       paths:  # 需要被传递给下一个job的目录。
       - node_modules/
       
   deploy-staging:dep:
     stage: deploy
     only:
     - master
     script:
     - echo "=====start build======"
     - npm run build  # 将项目打包
     - echo "=====end build======"
     - echo "=====start deploy======"
     - npm run deploy # 此处执行部署脚本,将打包好的静态文件上传到阿里云的oss上,为了保护项目安全,使抽象成deploy步骤。
     - echo "=====end deploy!!!!!!======"

这是我的.gitlab-ci.yml脚本。(# 为.gitlab-ci.yml脚本注释)

image

 image: node  # 选用docker镜像
我项目的 CI 任务是选的在 Docker 上运行,所以每次执行 CI 任务的时候,都会新启动一个 Docker 容器。因     为是前端项目,所以需要node环境。所以选用的是node镜像。也可以选择自己的docker镜像。


stages

stages: # Stages 表示构建阶段,这里有两个阶段 install, deploy
- install
- deploy

Stages 表示构建阶段,说白了就是上面提到的流程。 我们可以在一次 Pipeline 中定义多个 Stages,每个Stage可以完成不同的任务。 Stages有下面的特点:

  • 所有 Stages 会按照顺序运行,即当一个 Stage 完成后,下一个 Stage 才会开始
  • 只有当所有 Stages 完成后,该构建任务 (Pipeline) 才会成功

    • 如果任何一个 Stage 失败,那么后面的 Stages 不会执行,该构建任务 (Pipeline) 失败

only

only:
- master

只有maser分支才会触发这个脚本,因为我们采用的git-flow工作流,开发人员可能把自己未完善的分支(没有经过上级code review)提交到线上仓库,那么只要有push就会触发部署到线上环境,这样的后果是不堪设想的,所以必须加一个only,只有经过了code review的代码 被merge进入了maser分支才会实现部署到线上环境。

Jobs

install-staging:dep: # Jobs 表示构建工作,表示某个 Stage 里面执行的工作。
  stage: install

Jobs 表示构建工作,表示某个 Stage 里面执行的工作。 我们可以在 Stages 里面定义多个 Jobs,这些 Jobs 会有以下特点:

  • 相同 Stage 中的 Jobs 会并行执行
  • 相同 Stage 中的 Jobs 都执行成功时,该 Stage 才会成功

    • 如果任何一个 Job 失败,那么该 Stage 失败,即该构建任务 (Pipeline) 失败

script

      script:
       - echo "=====start install======"
       - npm install --registry=https://registry.npm.taobao.org
       - echo "=====end install======"

script是一段由Runner执行的shell脚本

artifact

    artifacts:  # 将这个job生成的依赖传递给下一个job。需要设置dependencies
      expire_in: 60 mins   # artifacets 的过期时间,因为这些数据都是直接保存在 Gitlab 机器上的,过于久远的资源就可以删除掉了
      paths:
      - node_modules/

artifacts 被用于在job作业成功后将制定列表里的文件或文件夹附加到job上,传递给下一个job,如果要在两个job之间传递artifacts,你必须设置dependencies

脚本总结

总结: 这个脚本的作用是 将merge进入master分支的代码打包并部署到阿里云的oss上。这里最值得注意的就是artifact,因为定义了两个job,其实每个job都是用的新的镜像,所以这样就会导致install阶段与deploy阶段没有任何关系,但是实际上deploy阶段是依赖install阶段安装的node_module的。所以必须将install阶段安装的 node_modules传递给下一个job(deploy),这就需要用到artifact或者cache了(这里我用的是artifact)。ps:我还其实还把这两个job整合成一个了,但是不知道为什么明明两个分开执行就只要10min,而合在一个job就要超过1h,最后导致超时Pipeline失败。

更多详细配置可以看这篇博客gitlab-ci配置详解

或者参考官方文档 官方配置文档

  1. 推送.gitlab-ci.yml到GitLab

    一旦创建了.gitlab-ci.yml,你应该及时添加到Git仓库并推送到GitLab。

    现在到Pipelines页面查看,将会看到该Pipline处于等待状态。

    1569815810780

配置Runner

在GitLab中,Runners将会运行你在.gitlab-ci.yml中定义的jobs。Runner可以是虚拟机,VPS,裸机,docker容器,甚至一堆容器。GitLab和Runners通过API通信,所以唯一的要求就是运行Runners的机器可以联网。

一个Runner可以服务GitLab中的某个特定的项目或者是多个项目。如果它服务所有的项目,则被称为共享的Runner。

Runners文档中查阅更多关于不同Runners的信息。

你可以通过Settings->CI/CD查找是否有Runners分配到你的项目中。创建一个Runner是简单且直接的。官方支持的Runner是用GO语言写的,它的文档在这里https://docs.gitlab.com/runner/

为了有一个功能性的Runner,你需要遵循以下步骤:

安装

  1. 添加Gitlab的官方源:

    # For Debian/Ubuntu
    curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-ci-multi-runner/script.deb.sh | sudo bash
    
    # For CentOS
    curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-ci-multi-runner/script.rpm.sh | sudo bash
  2. 安装

    # For Debian/Ubuntu
    sudo apt-get install gitlab-ci-multi-runner
    
    # For CentOS
    sudo yum install gitlab-ci-multi-runner
  3. 注册Runner
    Runner需要注册到Gitlab才可以被项目所使用,一个gitlab-ci-multi-runner服务可以注册多个Runner。

    $ sudo gitlab-ci-multi-runner register
    
    Please enter the gitlab-ci coordinator URL (e.g. https://gitlab.com )
    https://mygitlab.com/ci
    Please enter the gitlab-ci token for this runner
    xxx-xxx-xxx
    Please enter the gitlab-ci description for this runner
    my-runner
    INFO[0034] fcf5c619 Registering runner... succeeded
    Please enter the executor: shell, docker, docker-ssh, ssh?
    Please enter the Docker image (eg. ruby:2.1):
    node
    INFO[0037] Runner registered successfully. Feel free to start it, but if it's
    running already the config should be automatically reloaded!

ps: 这里面的gitlab-ci coordinator URL 与token

   Please enter the gitlab-ci coordinator URL (e.g. https://gitlab.com )
   https://mygitlab.com/ci
   Please enter the gitlab-ci token for this runner
   xxx-xxx-xxx

是在gitlab配置对应的runner里可以查看的。

1569816937998

1569817315527

配置

按照上面的连接设置你自己的Runner,我这边因为这是专用对于一个项目的runner,所以我配置的是specific runner。

一旦Runner安装好,你可以从项目的Settings->CI/CD找到Runner页面。

1569816937998

1569817028804

sf上的markdown编辑器显示的格式不太对,而.gitlab-ci.yml脚本很注重格式,所以要负责脚本可以参考我的原文
原文链接

参考

查看原文

赞 1 收藏 1 评论 0

flura 发布了文章 · 2019-11-19

vue-bus使用遇到的坑(兄弟组件无法传值)

vue中eventbus被多次触发,在this.$on监听事件时,内部的this发生改变导致,无法在vue实例中添加数据。

项目场景

一开始的需求是这样的,为了实现两个组件(A.vue ,B.vue)之间的数据传递。 页面A,点击页面上的某个A上的某一个按钮之后,页面会跳转到页面B。这个时候需要将页面A上的数据携带过去给页面B。

业务代码

// 点击之后,emit自定义事件(increment) 就会跳转到/B页面。 接下来就是在B页面中on 接受这个事件(increment)。获得这个数据。
<template>
  <div>
    I am AChild
    <button @click="increment">emit</button>
  </div>
</template>

<script>
export default {
  name: 'Achild',
  methods: {
    increment() {
      console.log('A触发了 $emit')
      this.bus.$emit('increment', '我是increment')
      this.$router.push('/B')
    }
  },
</script>
<template>
  <div>
    I am BChild
    <p>{{info}}</p>
  </div>
</template>

<script>
export default {
  name: 'Bchild',
  data() {
    return {
      info: 'default info'
    }
  },
  created() {
    let _this = this
    console.log('Bchild this', this)
    this.bus.$on('increment', function(data) {
      console.log('这是Bchild收到的值: ', data)
      console.log('Bchild in _this: ', _this)
      _this.info = data
    })
  },
</script>

按照理论,我觉得只要在页面A触发了increment事件,页面B就会理所当然的接受了数据。然而,结果却不如人意。

从这里可以发现 页面B根本就没有接收到这个事件。

然后再从页面B回退到 页面A, 再重复一遍emit increment事件。会神奇的发现B竟然收到了 A传递过来的数据。

你会发现,第一次触发事件increment的时候,B并没有收到。 第二次触发的时候,就输出了一个。第三次触发的时候,就又输出了两个、、、依次增加。而且你还会发现打印出的on的回调函数打印出的this指向,并不是指向当前vue实例(B.vue)。而且明明是顺序执行,却偏偏是异步执行。on的回调函数先于 console.log('Bchild this', this)执行。

  • 问题1:为什么第一次触发事件的时候,页面B on没有监听到事件。
  • 问题2:为什么后面再一次依次去触发的时候会出现,每一次都会发现好像之前的on事件分发都没有被撤销一样,导致每一次的事件触发执行越来越多。
  • 问题3:为什么是on里的回调函数先执行? 输出的指向且并不指向当前vue实例?

解决

这些问题的出现还要从vue的生命周期讲起。可以参考我的另一篇关于vue生命周期的博客

v4

从这里我们可以清楚的看到,当我们还在页面A触发emit事件时,页面B还没有生成,也就是说页面B中created中所监听的来自于A中的事件还没有被触发。这个时候你A中emit事件的时候,B还没有监听到。

再仔细看看,当我们从A页面跳转到B页面中的时候发生了什么?首先是B组件created 然后beforeMounted接着A组件才被销毁,A组件才仔细beforeDestory,以及destoryed。然后B组件再执行mounted。 所以我们可以把A页面组件中的emit事件放到beforeDestory里,因为这个时候,B组件的created钩子已经执行,也就可以监听到从A传过来的事件了。而且从周期来看,B的$on监听,也不能放在mounted钩子里,不然也会出现监听不到的情况。

<template>
  <div>
    I am AChild
    <button @click="increment">emit</button>
  </div>
</template>

<script>
export default {
  name: 'Achild',
  methods: {
    increment() {
      console.log('A触发了 $emit')
      this.$router.push('/B')
    }
  },
  beforeDestroy () {
    this.bus.$emit('increment', '我是increment')
  }
 }
</script>

修改过后效果图:

v5

我们可以看到修改后,B明显可以收到A传递过来的数据。但是多次点击,事件的触发还是会依次增加,控制台打印的输出每次都有增加。而且每次在$on里的回调函数会打印出以前监听到的vue实例,和本次监听的实例。

总结

查找各方面资料,才知道$on事件是不会自动销毁的。需要我们手动来销毁。

这是因为Bus是全局的,并不随着页面的切换而重新执行生命周期,所以$on能存储到以前的实例,这样看起来才比较奇怪。如果没有A组件没有将emit放在beforeDestory钩子里,通过全局的事件总线bus(没有受生命周期约束),而B里的 $on里没有监听到最新的emit,只会收到以前的事件,所以$on的this会指向上次B.vue的vue实例。导致现在的B.vue就算看起来拿到了数据,也无法挂载到现在的B实例上。

所以在B组件添加

beforeDestroy () {
   this.bus.$off('increment')
}

建议

使用bus时一定要注意,组件的生命周期。对于这种会被销毁的vue实例。一定要把emit放在beforeDestory里面。并且要记得将$on销毁。

如果是要保存这种状态最好使用vuex,进行数据传递。这样这些传递的值,就不会受组件的销毁新建的影响,可以保存下来。

原博客
参考博客

查看原文

赞 5 收藏 1 评论 3

flura 赞了文章 · 2019-10-31

浅析HTTP/2的多路复用

HTTP/2有三大特性:头部压缩、Server Push、多路复用。前两个特性意思比较明确,也好理解,唯有多路复用不太好理解,尤其是和HTTP1.1进行对比的时候,这个问题我想了很长时间,也对比了很长时间,现在把思考的结果分享出来,希望对大家有帮忙。

先来说说Keep-Alive

在没有Keep-Alive前,我们与服务器请求数据的流程是这样:

clipboard.png

  • 浏览器请求//static.mtime.cn/a.js-->解析域名-->HTTP连接-->服务器处理文件-->返回数据-->浏览器解析、渲染文件
  • 浏览器请求//static.mtime.cn/b.js-->解析域名-->HTTP连接-->服务器处理文件-->返回数据-->浏览器解析、渲染文件
  • ...
  • ...
  • ...
  • 这样循环下去,直至全部文件下载完成。

这个流程最大的问题就是:每次请求都会建立一次HTTP连接,也就是我们常说的3次握手4次挥手,这个过程在一次请求过程中占用了相当长的时间,而且逻辑上是非必需的,因为不间断的请求数据,第一次建立连接是正常的,以后就占用这个通道,下载其他文件,这样效率多高啊!你猜对了,这就是Keep-Alive

Keep-Alive解决的问题

Keep-Alive解决的核心问题:一定时间内,同一域名多次请求数据,只建立一次HTTP请求,其他请求可复用每一次建立的连接通道,以达到提高请求效率的问题。这里面所说的一定时间是可以配置的,不管你用的是Apache还是nginx

HTTP1.1还是存在效率问题

如上面所说,在HTTP1.1中是默认开启了Keep-Alive,他解决了多次连接的问题,但是依然有两个效率上的问题:

  • 第一个:串行的文件传输。当请求a文件时,b文件只能等待,等待a连接到服务器、服务器处理文件、服务器返回文件,这三个步骤。我们假设这三步用时都是1秒,那么a文件用时为3秒,b文件传输完成用时为6秒,依此类推。(注:此项计算有一个前提条件,就是浏览器和服务器是单通道传输)
  • 第二个:连接数过多。我们假设Apache设置了最大并发数为300,因为浏览器限制,浏览器发起的最大请求数为6,也就是服务器能承载的最高并发为50,当第51个人访问时,就需要等待前面某个请求处理完成。

HTTP/2的多路复用

HTTP/2的多路复用就是为了解决上述的两个性能问题,我们来看一下,他是如何解决的。

  • 解决第一个:在HTTP1.1的协议中,我们传输的requestresponse都是基本于文本的,这样就会引发一个问题:所有的数据必须按顺序传输,比如需要传输:hello world,只能从hd一个一个的传输,不能并行传输,因为接收端并不知道这些字符的顺序,所以并行传输在HTTP1.1是不能实现的。

clipboard.png

HTTP/2引入二进制数据帧的概念,其中帧对数据进行顺序标识,如下图所示,这样浏览器收到数据之后,就可以按照序列对数据进行合并,而不会出现合并后数据错乱的情况。同样是因为有了序列,服务器就可以并行的传输数据,这就是所做的事情。

clipboard.png

  • 解决第二个问题:HTTP/2对同一域名下所有请求都是基于,也就是说同一域名不管访问多少文件,也只建立一路连接。同样Apache的最大连接数为300,因为有了这个新特性,最大的并发就可以提升到300,比原来提升了6倍!

以前我们做的性能优化不适用于HTTP/2

  • JS文件的合并。我们现在优化的一个主要方向就是尽量的减少HTTP的请求数, 对我们工程中的代码,研发时分模块开发,上线时我们会把所有的代码进行压缩合并,合并成一个文件,这样不管多少模块,都请求一个文件,减少了HTTP的请求数。但是这样做有一个非常严重的问题:文件的缓存。当我们有100个模块时,有一个模块改了东西,按照之前的方式,整个文件浏览器都需要重新下载,不能被缓存。现在我们有了HTTP/2了,模块就可以单独的压缩上线,而不影响其他没有修改的模块。
  • 多域名提高浏览器的下载速度。之前我们有一个优化就是把css文件和js文件放到2个域名下面,这样浏览器就可以对这两个类型的文件进行同时下载,避免了浏览器6个通道的限制,这样做的缺点也是明显的,1.DNS的解析时间会变长。2.增加了服务器的压力。有了HTTP/2之后,根据上面讲的原理,我们就不用这么搞了,成本会更低。
查看原文

赞 59 收藏 41 评论 8

flura 赞了文章 · 2019-10-27

超详细的webpack原理解读

webpack原理解读

本文抄自《深入浅出webpack》,建议想学习原理的手打一遍,操作一遍,给别人讲一遍,然后就会了

在阅读前希望您已有webpack相关的实践经验,不然读了也读不懂

本文阅读需要几分钟,理解需要自己动手操作蛮长时间

0 配置文件

首先简单看一下webpack配置文件(webpack.config.js):

var path = require('path');
var node_modules = path.resolve(__dirname, 'node_modules');
var pathToReact = path.resolve(node_modules, 'react/dist/react.min.js');

module.exports = {
  // 入口文件,是模块构建的起点,同时每一个入口文件对应最后生成的一个 chunk。
  entry: {
    bundle: [
      'webpack/hot/dev-server',
      'webpack-dev-server/client?http://localhost:8080',
      path.resolve(__dirname, 'app/app.js')
    ]
  },
  // 文件路径指向(可加快打包过程)。
  resolve: {
    alias: {
      'react': pathToReact
    }
  },
  // 生成文件,是模块构建的终点,包括输出文件与输出路径。
  output: {
    path: path.resolve(__dirname, 'build'),
    filename: '[name].js'
  },
  // 这里配置了处理各模块的 loader ,包括 css 预处理 loader ,es6 编译 loader,图片处理 loader。
  module: {
    loaders: [
      {
        test: /\.js$/,
        loader: 'babel',
        query: {
          presets: ['es2015', 'react']
        }
      }
    ],
    noParse: [pathToReact]
  },
  // webpack 各插件对象,在 webpack 的事件流中执行对应的方法。
  plugins: [
    new webpack.HotModuleReplacementPlugin()
  ]
};

1. 工作原理概述

1.1 基本概念

在了解webpack原理之前,需要掌握以下几个核心概念

  • Entry: 入口,webpack构建第一步从entry开始
  • module:模块,在webpack中一个模块对应一个文件。webpack会从entry开始,递归找出所有依赖的模块
  • Chunk:代码块,一个chunk由多个模块组合而成,用于代码合并与分割
  • Loader: 模块转换器,用于将模块的原内容按照需求转换成新内容
  • Plugin:拓展插件,在webpack构建流程中的特定时机会广播对应的事件,插件可以监听这些事件的发生,在特定的时机做对应的事情

1.2 流程概述

webpack从启动到结束依次执行以下操作:

graph TD
初始化参数 --> 开始编译 
开始编译 -->确定入口 
确定入口 --> 编译模块
编译模块 --> 完成编译模块
完成编译模块 --> 输出资源
输出资源 --> 输出完成

各个阶段执行的操作如下:

  1. 初始化参数:从配置文件(默认webpack.config.js)和shell语句中读取与合并参数,得出最终的参数
  2. 开始编译(compile):用上一步得到的参数初始化Comiler对象,加载所有配置的插件,通过执行对象的run方法开始执行编译
  3. 确定入口:根据配置中的entry找出所有的入口文件
  4. 编译模块:从入口文件出发,调用所有配置的Loader对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过处理
  5. 完成编译模块:经过第四步之后,得到了每个模块被翻译之后的最终内容以及他们之间的依赖关系
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的chunk,再将每个chunk转换成一个单独的文件加入输出列表中,这是可以修改输出内容的最后机会
  7. 输出完成:在确定好输出内容后,根据配置(webpack.config.js && shell)确定输出的路径和文件名,将文件的内容写入文件系统中(fs)

在以上过程中,webpack会在特定的时间点广播特定的事件,插件监听事件并执行相应的逻辑,并且插件可以调用webpack提供的api改变webpack的运行结果

1.3 流程细节

webpack构建流程可分为以下三大阶段。

  1. 初始化:启动构建,读取与合并配置参数,加载plugin,实例化Compiler
  2. 编译:从Entry出发,针对每个Module串行调用对应的Loader去翻译文件中的内容,再找到该Module依赖的Module,递归的进行编译处理
  3. 输出:将编译后的Module组合成Chunk,将Chunk转换成文件,输出到文件系统中

如果只执行一次,流程如上,但在开启监听模式下,流程如下图

graph TD

  初始化-->编译;
  编译-->输出;
  输出-->文本发生变化
  文本发生变化-->编译

1.3.1初始化阶段

在初始化阶段会发生的事件如下

事件描述
初始化参数从配置文件和shell语句中读取与合并参数,得出最终的参数,这个过程还会执行配置文件中的插件实例化语句 new Plugin()
实例化Compiler实例化Compiler,传入上一步得到的参数,Compiler负责文件监听和启动编译。在Compiler实例中包含了完整的webpack配置,全局只有一个Compiler实例。
加载插件依次调用插件的apply方法,让插件可以监听后续的所有事件节点。同时向插件中传入compiler实例的引用,以方便插件通过compiler调用webpack的api
environment开始应用Node.js风格的文件系统到compiler对象,以方便后续的文件寻找和读取
Entry-option读取配置的Entrys,为每个Entry实例化一个对应的EntryPlugin,为后面该Entry的递归解析工作做准备
After-plugins调用完所有内置的和配置的插件的apply方法
After-resolvers根据配置初始化resolver,resolver负责在文件系统中寻找指定路径的文件

#### 1.3.2 编译阶段 (事件名全为小写)

事件解释
run启动一次编译
Watch-run在监听模式下启动编译,文件发生变化会重新编译
compile告诉插件一次新的编译将要启动,同时会给插件带上compiler对象
compilation当webpack以开发模式运行时,每当检测到文件的变化,便有一次新的compilation被创建。一个Compilation对象包含了当前的模块资源、编译生成资源、变化的文件等。compilation对象也提供了很多事件回调给插件进行拓展
make一个新的compilation对象创建完毕,即将从entry开始读取文件,根据文件类型和编译的loader对文件进行==编译==,编译完后再找出该文件依赖的文件,递归地编译和解析
after-compile一次compilation执行完成
invalid当遇到错误会触发改事件,该事件不会导致webpack退出

在编译阶段最重要的事件是compilation,因为在compilation阶段调用了Loader,完成了每个模块的==转换==操作。在compilation阶段又会发生很多小事件,如下表

事件解释
build-module使用相应的Loader去转换一个模块
Normal-module-loader在使用loader转换完一个模块后,使用acorn解析转换后的内容,输出对应的抽象语法树(AST),以方便webpack对代码进行分析
program从配置的入口模块开始,分析其AST,当遇到require等导入其他模块的语句时,便将其加入依赖的模块列表中,同时对于新找出来的模块递归分析,最终弄清楚所有模块的依赖关系
seal所有模块及依赖的模块都通过Loader转换完成,根据依赖关系生成Chunk

2.3 输出阶段

输出阶段会发生的事件及解释:

事件解释
should-emit所有需要输出的文件已经生成,询问插件有哪些文件需要输出,有哪些不需要输出
emit确定好要输出哪些文件后,执行文件输出,==可以在这里获取和修改输出的内容==
after-mit文件输出完毕
done成功完成一次完整的编译和输出流程
failed如果在编译和输出中出现错误,导致webpack退出,就会直接跳转到本步骤,插件可以在本事件中获取具体的错误原因

在输出阶段已经得到了各个模块经过转化后的结果和其依赖关系,并且将相应的模块组合在一起形成一个个chunk.在输出阶段根据chunk的类型,使用对应的模板生成最终要输出的文件内容. |

//以下代码用来包含webpack运行过程中的每个阶段
//file:webpack.config.js

const path = require('path');
//插件监听事件并执行相应的逻辑
class TestPlugin {
  constructor() {
    console.log('@plugin constructor');
  }

  apply(compiler) {
    console.log('@plugin apply');

    compiler.plugin('environment', (options) => {
      console.log('@environment');
    });

    compiler.plugin('after-environment', (options) => {
      console.log('@after-environment');
    });

    compiler.plugin('entry-option', (options) => {
      console.log('@entry-option');
    });

    compiler.plugin('after-plugins', (options) => {
      console.log('@after-plugins');
    });

    compiler.plugin('after-resolvers', (options) => {
      console.log('@after-resolvers');
    });

    compiler.plugin('before-run', (options, callback) => {
      console.log('@before-run');
      callback();
    });

    compiler.plugin('run', (options, callback) => {
      console.log('@run');
      callback();
    });

    compiler.plugin('watch-run', (options, callback) => {
      console.log('@watch-run');
      callback();
    });

    compiler.plugin('normal-module-factory', (options) => {
      console.log('@normal-module-factory');
    });

    compiler.plugin('context-module-factory', (options) => {
      console.log('@context-module-factory');
    });

    compiler.plugin('before-compile', (options, callback) => {
      console.log('@before-compile');
      callback();
    });

    compiler.plugin('compile', (options) => {
      console.log('@compile');
    });

    compiler.plugin('this-compilation', (options) => {
      console.log('@this-compilation');
    });

    compiler.plugin('compilation', (options) => {
      console.log('@compilation');
    });

    compiler.plugin('make', (options, callback) => {
      console.log('@make');
      callback();
    });

    compiler.plugin('compilation', (compilation) => {

      compilation.plugin('build-module', (options) => {
        console.log('@build-module');
      });

      compilation.plugin('normal-module-loader', (options) => {
        console.log('@normal-module-loader');
      });

      compilation.plugin('program', (options, callback) => {
        console.log('@program');
        callback();
      });

      compilation.plugin('seal', (options) => {
        console.log('@seal');
      });
    });

    compiler.plugin('after-compile', (options, callback) => {
      console.log('@after-compile');
      callback();
    });

    compiler.plugin('should-emit', (options) => {
      console.log('@should-emit');
    });

    compiler.plugin('emit', (options, callback) => {
      console.log('@emit');
      callback();
    });

    compiler.plugin('after-emit', (options, callback) => {
      console.log('@after-emit');
      callback();
    });

    compiler.plugin('done', (options) => {
      console.log('@done');
    });

    compiler.plugin('failed', (options, callback) => {
      console.log('@failed');
      callback();
    });

    compiler.plugin('invalid', (options) => {
      console.log('@invalid');
    });

  }
}
#在目录下执行
webpack
#输出以下内容
@plugin constructor
@plugin apply
@environment
@after-environment
@entry-option
@after-plugins
@after-resolvers
@before-run
@run
@normal-module-factory
@context-module-factory
@before-compile
@compile
@this-compilation
@compilation
@make
@build-module
@normal-module-loader
@build-module
@normal-module-loader
@seal
@after-compile
@should-emit
@emit
@after-emit
@done
Hash: 19ef3b418517e78b5286
Version: webpack 3.11.0
Time: 95ms
    Asset     Size  Chunks             Chunk Names
bundle.js  3.03 kB       0  [emitted]  main
   [0] ./main.js 44 bytes {0} [built]
   [1] ./show.js 114 bytes {0} [built]

2 输出文件分析

2.1 举个栗子

下面通过 Webpack 构建一个采用 CommonJS 模块化编写的项目,该项目有个网页会通过 JavaScript 在网页中显示 Hello,Webpack

运行构建前,先把要完成该功能的最基础的 JavaScript 文件和 HTML 建立好,需要如下文件:

页面入口文件 index.html

<html>
<head>
  <meta charset="UTF-8">
</head>
<body>
<div id="app"></div>
<!--导入 Webpack 输出的 JavaScript 文件-->
<script data-original="./dist/bundle.js"></script>
</body>
</html>

JS 工具函数文件 show.js

// 操作 DOM 元素,把 content 显示到网页上
function show(content) {
  window.document.getElementById('app').innerText = 'Hello,' + content;
}

// 通过 CommonJS 规范导出 show 函数
module.exports = show;

JS 执行入口文件 main.js

// 通过 CommonJS 规范导入 show 函数
const show = require('./show.js');
// 执行 show 函数
show('Webpack');

Webpack 在执行构建时默认会从项目根目录下的 webpack.config.js 文件读取配置,所以你还需要新建它,其内容如下:

const path = require('path');

module.exports = {
  // JavaScript 执行入口文件
  entry: './main.js',
  output: {
    // 把所有依赖的模块合并输出到一个 bundle.js 文件
    filename: 'bundle.js',
    // 输出文件都放到 dist 目录下
    path: path.resolve(__dirname, './dist'),
  }
};

由于 Webpack 构建运行在 Node.js 环境下,所以该文件最后需要通过 CommonJS 规范导出一个描述如何构建的 Object 对象。

|-- index.html
|-- main.js
|-- show.js
|-- webpack.config.js

一切文件就绪,在项目根目录下执行 webpack 命令运行 Webpack 构建,你会发现目录下多出一个 dist目录,里面有个 bundle.js 文件, bundle.js 文件是一个可执行的 JavaScript 文件,它包含页面所依赖的两个模块 main.jsshow.js 及内置的 webpackBootstrap 启动函数。 这时你用浏览器打开 index.html 网页将会看到 Hello,Webpack

2.2 bundle.js文件做了什么

看之前记住:一个模块就是一个文件,

首先看下bundle.js长什么样子:

image-20190113213327207

注意:序号1处是个自执行函数,序号2作为自执行函数的参数传入

具体代码如下:(建议把以下代码放入编辑器中查看,最好让index.html执行下,弄清楚执行的顺序)

(function(modules) { // webpackBootstrap
  // 1. 缓存模块
  var installedModules = {};
  // 2. 定义可以在浏览器使用的require函数
  function __webpack_require__(moduleId) {

    // 2.1检查模块是否在缓存里,在的话直接返回
    if(installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    // 2.2 模块不在缓存里,新建一个对象module=installModules[moduleId] {i:moduleId,l:模块是否加载,exports:模块返回值}
    var module = installedModules[moduleId] = {
      i: moduleId,//第一次执行为0
      l: false,
      exports: {}
    };//第一次执行module:{i:0,l:false,exports:{}}
    // 2.3 执行传入的参数中对应id的模块 第一次执行数组中传入的第一个参数
          //modules[0].call({},{i:0,l:false,exports:{}},{},__webpack_require__函数)
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
    // 2.4 将这个模块标记为已加载
    module.l = true;
    // 2.5 返回这个模块的导出值
    return module.exports;
  }
  // 3. webpack暴露属性 m c d n o p
  __webpack_require__.m = modules;
  __webpack_require__.c = installedModules;
  __webpack_require__.d = function(exports, name, getter) {
    if(!__webpack_require__.o(exports, name)) {
      Object.defineProperty(exports, name, {
        configurable: false,
        enumerable: true,
        get: getter
      });
    }
  };
  __webpack_require__.n = function(module) {
    var getter = module && module.__esModule ?
      function getDefault() { return module['default']; } :
      function getModuleExports() { return module; };
    __webpack_require__.d(getter, 'a', getter);
    return getter;
  };
  __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
  __webpack_require__.p = "";
  // 4. 执行reruire函数引入第一个模块(main.js对应的模块)
  return __webpack_require__(__webpack_require__.s = 0);
})
([ // 0. 传入参数,参数是个数组

  /* 第0个参数 main.js对应的文件*/
  (function(module, exports, __webpack_require__) {

    // 通过 CommonJS 规范导入 show 函数
    const show = __webpack_require__(1);//__webpack_require__(1)返回show
    // 执行 show 函数
    show('Webpack');

  }),
  /* 第1个参数 show.js对应的文件 */
  (function(module, exports) {

    // 操作 DOM 元素,把 content 显示到网页上
    function show(content) {
      window.document.getElementById('app').innerText = 'Hello,' + content;
    }
    // 通过 CommonJS 规范导出 show 函数
    module.exports = show;

  })
]);

以上看上去复杂的代码其实是一个自执行函数(文件作为自执行函数的参数),可以简写如下:

(function(modules){
    //模拟require语句
    function __webpack_require__(){}
    //执行存放所有模块数组中的第0个模块(main.js)
    __webpack_require_[0]
})([/*存放所有模块的数组*/])

bundles.js能直接在浏览器中运行的原因是,在输出的文件中通过__webpack_require__函数,定义了一个可以在浏览器中执行的加载函数(加载文件使用ajax实现),来模拟Node.js中的require语句。

原来一个个独立的模块文件被合并到了一个单独的 bundle.js 的原因在于浏览器不能像 Node.js 那样快速地去本地加载一个个模块文件,而必须通过网络请求去加载还未得到的文件。 如果模块数量很多,加载时间会很长,因此把所有模块都存放在了数组中,执行一次网络加载。

修改main.js,改成import引入模块

import show from './show';
show('Webpack');

在目录下执行webpack,会发现:

  1. 生成的代码会有所不同,但是主要的区别是自执行函数的参数不同,也就是2.2代码的第二部分不同
([//自执行函数和上面相同,参数不同
/* 0 */
(function(module, __webpack_exports__, __webpack_require__) {

"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__show__ = __webpack_require__(1);

Object(__WEBPACK_IMPORTED_MODULE_0__show__["a" /* default */])('Webpack');


}),
/* 1 */
(function(module, __webpack_exports__, __webpack_require__) {

"use strict";
/* harmony export (immutable) */ __webpack_exports__["a"] = show;
function show(content) {
  window.document.getElementById('app').innerText = 'Hello,' + content;
}


})
]);

参数不同的原因是es6的import和export模块被webpack编译处理过了,其实作用是一样的,接下来看一下在main.js中异步加载模块时,bundle.js是怎样的

2.3异步加载时,bundle.js代码分析

main.js修改如下

import('./show').then(show=>{
    show('Webpack')
})

构建成功后会生成两个文件

  1. bundle.js 执行入口文件
  2. 0.bundle.js 异步加载文件

其中0.bundle.js文件的内容如下:

webpackJsonp(/*在其他文件中存放的模块的ID*/[0],[//本文件所包含的模块
/* 0 */,
/* 1 show.js对应的模块 */
(function(module, __webpack_exports__, __webpack_require__) {

  "use strict";
  Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
  /* harmony export (immutable) */ 
  __webpack_exports__["default"] = show;

  function show(content) {
    window.document.getElementById('app').innerText = 'Hello,' + content;
  }

})
]);

bundle.js文件的内容如下:

注意:bundle.js比上面的bundle.js的区别在于:

  1. 多了一个__webpack_require__.e,用于加载被分割出去的需要异步加载的chunk对应的文件
  2. 多了一个webpackJsonp函数,用于从异步加载的文件中安装模块
(function(modules) { // webpackBootstrap
    // install a JSONP callback for chunk loading
  var parentJsonpFunction = window["webpackJsonp"];
  // webpackJsonp用于从异步加载的文件中安装模块
  // 将webpackJsonp挂载到全局是为了方便在其他文件中调用
  /**
   * @param chunkIds 异步加载的模块中需要安装的模块对应的id
   * @param moreModules 异步加载的模块中需要安装模块列表
   * @param executeModules 异步加载的模块安装成功后需要执行的模块对应的index
   */
    window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
        // add "moreModules" to the modules object,
        // then flag all "chunkIds" as loaded and fire callback
        var moduleId, chunkId, i = 0, resolves = [], result;
        for(;i < chunkIds.length; i++) {
            chunkId = chunkIds[i];
            if(installedChunks[chunkId]) {
                resolves.push(installedChunks[chunkId][0]);
            }
            installedChunks[chunkId] = 0;
        }
        for(moduleId in moreModules) {
            if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
                modules[moduleId] = moreModules[moduleId];
            }
        }
        if(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules, executeModules);
        while(resolves.length) {
            resolves.shift()();
        }
    };
    // The module cache
    var installedModules = {};
    // objects to store loaded and loading chunks
    var installedChunks = {
        1: 0
    };
    // The require function
    function __webpack_require__(moduleId) {
        // Check if module is in cache
        if(installedModules[moduleId]) {
            return installedModules[moduleId].exports;
        }
        // Create a new module (and put it into the cache)
        var module = installedModules[moduleId] = {
            i: moduleId,
            l: false,
            exports: {}
        };
        // Execute the module function
        modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
        // Flag the module as loaded
        module.l = true;
        // Return the exports of the module
        return module.exports;
    }
    // This file contains only the entry chunk.
  // The chunk loading function for additional chunks
  /**
   * 用于加载被分割出去的需要异步加载的chunk对应的文件
   * @param chunkId 需要异步加载的chunk对应的id
   * @returns {Promise}
   */
    __webpack_require__.e = function requireEnsure(chunkId) {
      var installedChunkData = installedChunks[chunkId];
      if(installedChunkData === 0) {
        return new Promise(function(resolve) { resolve(); });
      }
      // a Promise means "currently loading".
      if(installedChunkData) {
        return installedChunkData[2];
      }
      // setup Promise in chunk cache
      var promise = new Promise(function(resolve, reject) {
        installedChunkData = installedChunks[chunkId] = [resolve, reject];
      });
      installedChunkData[2] = promise;
      // start chunk loading
      var head = document.getElementsByTagName('head')[0];
      var script = document.createElement('script');
      script.type = "text/javascript";
      script.charset = 'utf-8';
      script.async = true;
      script.timeout = 120000;
      if (__webpack_require__.nc) {
        script.setAttribute("nonce", __webpack_require__.nc);
      }
      script.src = __webpack_require__.p + "" + chunkId + ".bundle.js";
      var timeout = setTimeout(onScriptComplete, 120000);
      script.onerror = script.onload = onScriptComplete;
      function onScriptComplete() {
        // avoid mem leaks in IE.
        script.onerror = script.onload = null;
        clearTimeout(timeout);
        var chunk = installedChunks[chunkId];
        if(chunk !== 0) {
          if(chunk) {
            chunk[1](new Error('Loading chunk ' + chunkId + ' failed.'));
          }
          installedChunks[chunkId] = undefined;
        }
      };
      head.appendChild(script);
      return promise;
    };
    // expose the modules object (__webpack_modules__)
    __webpack_require__.m = modules;
    // expose the module cache
    __webpack_require__.c = installedModules;
    // define getter function for harmony exports
    __webpack_require__.d = function(exports, name, getter) {
        if(!__webpack_require__.o(exports, name)) {
            Object.defineProperty(exports, name, {
                configurable: false,
                enumerable: true,
                get: getter
            });
        }
    };
    // getDefaultExport function for compatibility with non-harmony modules
    __webpack_require__.n = function(module) {
        var getter = module && module.__esModule ?
            function getDefault() { return module['default']; } :
            function getModuleExports() { return module; };
        __webpack_require__.d(getter, 'a', getter);
        return getter;
    };
    // Object.prototype.hasOwnProperty.call
    __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
    // __webpack_public_path__
    __webpack_require__.p = "";
    // on error function for async loading
    __webpack_require__.oe = function(err) { console.error(err); throw err; };
    // Load entry module and return exports
    return __webpack_require__(__webpack_require__.s = 0);
})
/************************************************************************/
([//存放没有经过异步加载的,随着执行入口文件加载的模块
/* 0 */
/***/ (function(module, exports, __webpack_require__) {

__webpack_require__.e/* import() */(0).then(__webpack_require__.bind(null, 1)).then(show=>{
    show('Webpack')
})


/***/ })
]);
查看原文

赞 70 收藏 49 评论 3

flura 赞了文章 · 2019-10-25

HTTP强缓存和协商缓存

浏览器缓存

浏览器缓存是浏览器在本地磁盘对用户最近请求过的文档进行存储,当访问者再次访问同一页面时,浏览器就可以直接从本地磁盘加载文档。

所以根据上面的特点,浏览器缓存有下面的优点:

  1. 减少冗余的数据传输

  2. 减少服务器负担

  3. 加快客户端加载网页的速度

浏览器缓存是Web性能优化的重要方式。那么浏览器缓存的过程究竟是怎么样的呢?

在浏览器第一次发起请求时,本地无缓存,向web服务器发送请求,服务器起端响应请求,浏览器端缓存。过程如下:
浏览器第一次请求

在第一次请求时,服务器会将页面最后修改时间通过Last-Modified标识由服务器发送给客户端,客户端记录修改时间;服务器还会生成一个Etag,并发送给客户端。

浏览器后续再次进行请求时:
浏览器后续再次请求

浏览器缓存主要分为强强缓存(也称本地缓存)和协商缓存(也称弱缓存)。根据上图,浏览器在第一次请求发生后,再次发送请求时:

  • 浏览器请求某一资源时,会先获取该资源缓存的header信息,然后根据header中的Cache-ControlExpires来判断是否过期。若没过期则直接从缓存中获取资源信息,包括缓存的header的信息,所以此次请求不会与服务器进行通信。这里判断是否过期,则是强缓存相关。后面会讲Cache-ControlExpires相关。

  • 如果显示已过期,浏览器会向服务器端发送请求,这个请求会携带第一次请求返回的有关缓存的header字段信息,比如客户端会通过If-None-Match头将先前服务器端发送过来的Etag发送给服务器,服务会对比这个客户端发过来的Etag是否与服务器的相同,若相同,就将If-None-Match的值设为false,返回状态304,客户端继续使用本地缓存,不解析服务器端发回来的数据,若不相同就将If-None-Match的值设为true,返回状态为200,客户端重新机械服务器端返回的数据;客户端还会通过If-Modified-Since头将先前服务器端发过来的最后修改时间戳发送给服务器,服务器端通过这个时间戳判断客户端的页面是否是最新的,如果不是最新的,则返回最新的内容,如果是最新的,则返回304,客户端继续使用本地缓存。

强缓存

强缓存是利用http头中的ExpiresCache-Control两个字段来控制的,用来表示资源的缓存时间。强缓存中,普通刷新会忽略它,但不会清除它,需要强制刷新。浏览器强制刷新,请求会带上Cache-Control:no-cachePragma:no-cache

Expires

Expires是http1.0的规范,它的值是一个绝对时间的GMT格式的时间字符串。如我现在这个网页的Expires值是:expires:Fri, 14 Apr 2017 10:47:02 GMT。这个时间代表这这个资源的失效时间,只要发送请求时间是在Expires之前,那么本地缓存始终有效,则在缓存中读取数据。所以这种方式有一个明显的缺点,由于失效的时间是一个绝对时间,所以当服务器与客户端时间偏差较大时,就会导致缓存混乱。如果同时出现Cache-Control:max-ageExpires,那么max-age优先级更高。如我主页的response headers部分如下:

cache-control:max-age=691200
expires:Fri, 14 Apr 2017 10:47:02 GMT

那么表示资源可以被缓存的最长时间为691200秒,会优先考虑max-age

Cache-Control

Cache-Control是在http1.1中出现的,主要是利用该字段的max-age值来进行判断,它是一个相对时间,例如Cache-Control:max-age=3600,代表着资源的有效期是3600秒。cache-control除了该字段外,还有下面几个比较常用的设置值:

  • no-cache:不使用本地缓存。需要使用缓存协商,先与服务器确认返回的响应是否被更改,如果之前的响应中存在ETag,那么请求的时候会与服务端验证,如果资源未被更改,则可以避免重新下载。

  • no-store:直接禁止游览器缓存数据,每次用户请求该资源,都会向服务器发送一个请求,每次都会下载完整的资源。

  • public:可以被所有的用户缓存,包括终端用户和CDN等中间代理服务器。

  • private:只能被终端用户的浏览器缓存,不允许CDN等中继缓存服务器对其缓存。
    Cache-Control与Expires可以在服务端配置同时启用,同时启用的时候Cache-Control优先级高。

协商缓存

协商缓存就是由服务器来确定缓存资源是否可用,所以客户端与服务器端要通过某种标识来进行通信,从而让服务器判断请求资源是否可以缓存访问。

普通刷新会启用弱缓存,忽略强缓存。只有在地址栏或收藏夹输入网址、通过链接引用资源等情况下,浏览器才会启用强缓存,这也是为什么有时候我们更新一张图片、一个js文件,页面内容依然是旧的,但是直接浏览器访问那个图片或文件,看到的内容却是新的。

这个主要涉及到两组header字段:EtagIf-None-MatchLast-ModifiedIf-Modified-Since。上面以及说得很清楚这两组怎么使用啦~复习一下:

EtagIf-None-Match

Etag/If-None-Match返回的是一个校验码。ETag可以保证每一个资源是唯一的,资源变化都会导致ETag变化。服务器根据浏览器上送的If-None-Match值来判断是否命中缓存。

与Last-Modified不一样的是,当服务器返回304 Not Modified的响应时,由于ETag重新生成过,response header中还会把这个ETag返回,即使这个ETag跟之前的没有变化。

Last-Modify/If-Modify-Since

浏览器第一次请求一个资源的时候,服务器返回的header中会加上Last-Modify,Last-modify是一个时间标识该资源的最后修改时间,例如Last-Modify: Thu,31 Dec 2037 23:59:59 GMT。

当浏览器再次请求该资源时,request的请求头中会包含If-Modify-Since,该值为缓存之前返回的Last-Modify。服务器收到If-Modify-Since后,根据资源的最后修改时间判断是否命中缓存。

如果命中缓存,则返回304,并且不会返回资源内容,并且不会返回Last-Modify。

为什么要有Etag

你可能会觉得使用Last-Modified已经足以让浏览器知道本地的缓存副本是否足够新,为什么还需要Etag呢?HTTP1.1中Etag的出现主要是为了解决几个Last-Modified比较难解决的问题:

  • 一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新GET;

  • 某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次),If-Modified-Since能检查到的粒度是s级的,这种修改无法判断(或者说UNIX记录MTIME只能精确到秒);

  • 某些服务器不能精确的得到文件的最后修改时间。

Last-Modified与ETag是可以一起使用的,服务器会优先验证ETag,一致的情况下,才会继续比对Last-Modified,最后才决定是否返回304。

另外觉得一篇很好的文章,从缓存策略来学习HTTP缓存:HTTP基于缓存策略三要素分解法

查看原文

赞 67 收藏 58 评论 3

认证与成就

  • 获得 20 次点赞
  • 获得 4 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 4 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

  • 垃圾分类小助手

    为了更好的垃圾分类,编写了一个智能回答小助手,数据来源于上海市垃圾查询爬取 与 福州第一技师学院的生活垃圾分类知识汇总

注册于 2019-07-23
个人主页被 240 人浏览