CobraJ

CobraJ 查看完整档案

北京编辑北京邮电大学网络教育学院  |  计算机科学 编辑某通信服务公司  |  前端工程师 编辑 none.none 编辑
编辑

take it easy , relax

个人动态

CobraJ 收藏了文章 · 1月27日

理解 JavaScript 的 async/await

2020-06-04 更新

JavaScript 中的 async/await 是 AsyncFunction 特性 中的关键字。目前为止,除了 IE 之外,常用浏览器和 Node (v7.6+) 都已经支持该特性。具体支持情况可以在 这里 查看。


我第一次看到 async/await 这组关键字并不是在 JavaScript 语言里,而是在 C# 5.0 的语法中。C# 的 async/await 需要在 .NET Framework 4.5 以上的版本中使用,因此我还很悲伤了一阵——为了要兼容 XP 系统,我们开发的软件不能使用高于 4.0 版本的 .NET Framework。

我之前在《闲谈异步调用“扁平”化》 中就谈到了这个问题。无论是在 C# 还是 JavaScript 中,async/await 都是非常棒的特性,它们也都是非常甜的语法糖。C# 的 async/await 实现离不开 Task 或 Task\<Result\> 类,而 JavaScript 的 async/await 实现,也离不开 Promise

现在抛开 C# 和 .NET Framework,专心研究下 JavaScript 的 async/await。

1. async 和 await 在干什么

任意一个名称都是有意义的,先从字面意思来理解。async 是“异步”的简写,而 await 可以认为是 async wait 的简写。所以应该很好理解 async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成。

另外还有一个很有意思的语法规定,await 只能出现在 async 函数中。然后细心的朋友会产生一个疑问,如果 await 只能出现在 async 函数中,那这个 async 函数应该怎么调用?

如果需要通过 await 来调用一个 async 函数,那这个调用的外面必须得再包一个 async 函数,然后……进入死循环,永无出头之日……

如果 async 函数不需要 await 来调用,那 async 到底起个啥作用?

1.1. async 起什么作用

这个问题的关键在于,async 函数是怎么处理它的返回值的!

我们当然希望它能直接通过 return 语句返回我们想要的值,但是如果真是这样,似乎就没 await 什么事了。所以,写段代码来试试,看它到底会返回什么:

async function testAsync() {
    return "hello async";
}

const result = testAsync();
console.log(result);

看到输出就恍然大悟了——输出的是一个 Promise 对象。

c:\var\test> node --harmony_async_await .
Promise { 'hello async' }

所以,async 函数返回的是一个 Promise 对象。从文档中也可以得到这个信息。async 函数(包含函数语句、函数表达式、Lambda表达式)会返回一个 Promise 对象,如果在函数中 return 一个直接量,async 会把这个直接量通过 Promise.resolve() 封装成 Promise 对象。

补充知识点 [2020-06-04]

Promise.resolve(x) 可以看作是 new Promise(resolve => resolve(x)) 的简写,可以用于快速封装字面量对象或其他对象,将其封装成 Promise 实例。

async 函数返回的是一个 Promise 对象,所以在最外层不能用 await 获取其返回值的情况下,我们当然应该用原来的方式:then() 链来处理这个 Promise 对象,就像这样

testAsync().then(v => {
    console.log(v);    // 输出 hello async
});

现在回过头来想下,如果 async 函数没有返回值,又该如何?很容易想到,它会返回 Promise.resolve(undefined)

联想一下 Promise 的特点——无等待,所以在没有 await 的情况下执行 async 函数,它会立即执行,返回一个 Promise 对象,并且,绝不会阻塞后面的语句。这和普通返回 Promise 对象的函数并无二致。

那么下一个关键点就在于 await 关键字了。

1.2. await 到底在等啥

一般来说,都认为 await 是在等待一个 async 函数完成。不过按语法说明,await 等待的是一个表达式,这个表达式的计算结果是 Promise 对象或者其它值(换句话说,就是没有特殊限定)。

因为 async 函数返回一个 Promise 对象,所以 await 可以用于等待一个 async 函数的返回值——这也可以说是 await 在等 async 函数,但要清楚,它等的实际是一个返回值。注意到 await 不仅仅用于等 Promise 对象,它可以等任意表达式的结果,所以,await 后面实际是可以接普通函数调用或者直接量的。所以下面这个示例完全可以正确运行

function getSomething() {
    return "something";
}

async function testAsync() {
    return Promise.resolve("hello async");
}

async function test() {
    const v1 = await getSomething();
    const v2 = await testAsync();
    console.log(v1, v2);
}

test();

1.3. await 等到了要等的,然后呢

await 等到了它要等的东西,一个 Promise 对象,或者其它值,然后呢?我不得不先说,await 是个运算符,用于组成表达式,await 表达式的运算结果取决于它等的东西。

如果它等到的不是一个 Promise 对象,那 await 表达式的运算结果就是它等到的东西。

如果它等到的是一个 Promise 对象,await 就忙起来了,它会阻塞后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。

看到上面的阻塞一词,心慌了吧……放心,这就是 await 必须用在 async 函数中的原因。async 函数调用不会造成阻塞,它内部所有的阻塞都被封装在一个 Promise 对象中异步执行。

2. async/await 帮我们干了啥

2.1. 作个简单的比较

上面已经说明了 async 会将其后的函数(函数表达式或 Lambda)的返回值封装成一个 Promise 对象,而 await 会等待这个 Promise 完成,并将其 resolve 的结果返回出来。

现在举例,用 setTimeout 模拟耗时的异步操作,先来看看不用 async/await 会怎么写

function takeLongTime() {
    return new Promise(resolve => {
        setTimeout(() => resolve("long_time_value"), 1000);
    });
}

takeLongTime().then(v => {
    console.log("got", v);
});

如果改用 async/await 呢,会是这样

function takeLongTime() {
    return new Promise(resolve => {
        setTimeout(() => resolve("long_time_value"), 1000);
    });
}

async function test() {
    const v = await takeLongTime();
    console.log(v);
}

test();

眼尖的同学已经发现 takeLongTime() 没有申明为 async。实际上,takeLongTime() 本身就是返回的 Promise 对象,加不加 async 结果都一样,如果没明白,请回过头再去看看上面的“async 起什么作用”。

又一个疑问产生了,这两段代码,两种方式对异步调用的处理(实际就是对 Promise 对象的处理)差别并不明显,甚至使用 async/await 还需要多写一些代码,那它的优势到底在哪?

2.2. async/await 的优势在于处理 then 链

单一的 Promise 链并不能发现 async/await 的优势,但是,如果需要处理由多个 Promise 组成的 then 链的时候,优势就能体现出来了(很有意思,Promise 通过 then 链来解决多层回调的问题,现在又用 async/await 来进一步优化它)。

假设一个业务,分多个步骤完成,每个步骤都是异步的,而且依赖于上一个步骤的结果。我们仍然用 setTimeout 来模拟异步操作:

/**
 * 传入参数 n,表示这个函数执行的时间(毫秒)
 * 执行的结果是 n + 200,这个值将用于下一步骤
 */
function takeLongTime(n) {
    return new Promise(resolve => {
        setTimeout(() => resolve(n + 200), n);
    });
}

function step1(n) {
    console.log(`step1 with ${n}`);
    return takeLongTime(n);
}

function step2(n) {
    console.log(`step2 with ${n}`);
    return takeLongTime(n);
}

function step3(n) {
    console.log(`step3 with ${n}`);
    return takeLongTime(n);
}

现在用 Promise 方式来实现这三个步骤的处理

function doIt() {
    console.time("doIt");
    const time1 = 300;
    step1(time1)
        .then(time2 => step2(time2))
        .then(time3 => step3(time3))
        .then(result => {
            console.log(`result is ${result}`);
            console.timeEnd("doIt");
        });
}

doIt();

// c:\var\test>node --harmony_async_await .
// step1 with 300
// step2 with 500
// step3 with 700
// result is 900
// doIt: 1507.251ms

输出结果 resultstep3() 的参数 700 + 200 = 900doIt() 顺序执行了三个步骤,一共用了 300 + 500 + 700 = 1500 毫秒,和 console.time()/console.timeEnd() 计算的结果一致。

如果用 async/await 来实现呢,会是这样

async function doIt() {
    console.time("doIt");
    const time1 = 300;
    const time2 = await step1(time1);
    const time3 = await step2(time2);
    const result = await step3(time3);
    console.log(`result is ${result}`);
    console.timeEnd("doIt");
}

doIt();

结果和之前的 Promise 实现是一样的,但是这个代码看起来是不是清晰得多,几乎跟同步代码一样

2.3. 还有更酷的

现在把业务要求改一下,仍然是三个步骤,但每一个步骤都需要之前每个步骤的结果。

function step1(n) {
    console.log(`step1 with ${n}`);
    return takeLongTime(n);
}

function step2(m, n) {
    console.log(`step2 with ${m} and ${n}`);
    return takeLongTime(m + n);
}

function step3(k, m, n) {
    console.log(`step3 with ${k}, ${m} and ${n}`);
    return takeLongTime(k + m + n);
}

这回先用 async/await 来写:

async function doIt() {
    console.time("doIt");
    const time1 = 300;
    const time2 = await step1(time1);
    const time3 = await step2(time1, time2);
    const result = await step3(time1, time2, time3);
    console.log(`result is ${result}`);
    console.timeEnd("doIt");
}

doIt();

// c:\var\test>node --harmony_async_await .
// step1 with 300
// step2 with 800 = 300 + 500
// step3 with 1800 = 300 + 500 + 1000
// result is 2000
// doIt: 2907.387ms

除了觉得执行时间变长了之外,似乎和之前的示例没啥区别啊!别急,认真想想如果把它写成 Promise 方式实现会是什么样子?

function doIt() {
    console.time("doIt");
    const time1 = 300;
    step1(time1)
        .then(time2 => {
            return step2(time1, time2)
                .then(time3 => [time1, time2, time3]);
        })
        .then(times => {
            const [time1, time2, time3] = times;
            return step3(time1, time2, time3);
        })
        .then(result => {
            console.log(`result is ${result}`);
            console.timeEnd("doIt");
        });
}

doIt();

有没有感觉有点复杂的样子?那一堆参数处理,就是 Promise 方案的死穴—— 参数传递太麻烦了,看着就晕!

3. 洗洗睡吧

就目前来说,已经理解 async/await 了吧?但其实还有一些事情没提及——Promise 有可能 reject 啊,怎么处理呢?如果需要并行处理3个步骤,再等待所有结果,又该怎么处理呢?

阮一峰老师已经说过了,我就懒得说了。

4. 推荐相关文章

5. 来跟边城(作者)学 更新@2020-11-14

TypeScript从入门到实践 【2020 版】

TypeScript从入门到实践 【2020 版】

6. 关于转载 补充@2020-03-05

常有读者问是否可以转载。

笔者表示欢迎各位转载,但转载时一定注明作者和出处,谢谢!


公众号-边城客栈
请关注公众号 边城客栈

看完了先别走,点个赞啊 ⇓,赞赏 ⇘ 也行!

查看原文

CobraJ 收藏了文章 · 1月27日

让 Flutter 在鸿蒙系统上跑起来

前言

鸿蒙系统 (HarmonyOS)是华为推出的一款面向未来、面向全场景的分布式操作系统。在传统单设备系统能力的基础上,鸿蒙提出了基于同一套系统能力、适配多种终端形态的分布式理念。自 2020 年 9 月 HarmonyOS 2.0 发布以来,华为加快了鸿蒙系统大规模落地的步伐,预计 2021 年底,鸿蒙系统会覆盖包括手机、平板、智能穿戴、智慧屏、车机在内数亿台终端设备。对移动应用而言,新的系统理念、新的交互形式,也意味着新的机遇。如果能够利用好鸿蒙的开发生态及其特性能力,可以让应用覆盖更多的交互场景和设备类型,从而带来新的增长点。

与面临的机遇相比,适配鸿蒙系统带来的挑战同样巨大。当前手机端,尽管鸿蒙系统仍然支持安卓 APK 安装及运行,但长期来看,华为势必会抛弃 AOSP,逐步发展出自己的生态,这意味着现有安卓应用在鸿蒙设备上将会逐渐变成“二等公民”。然而,如果在 iOS 及 Android 之外再重新开发和维护一套鸿蒙应用,在如今业界越来越注重开发迭代效率的环境下,所带来的开发成本也是难以估量的。因此,通过打造一套合适的跨端框架,以相对低的成本移植应用到鸿蒙平台,并利用好该系统的特性能力,就成为了一个非常重要的选项。

在现有的众多跨端框架当中,Flutter 以其自渲染能力带来的多端高度一致性,在新系统的适配上有着突出的优势。虽然Flutter 官方并没有适配鸿蒙的计划,但经过一段时间的探索和实践,美团外卖 MTFlutter 团队成功实现了 Flutter 对于鸿蒙系统的原生支持。

这里也要提前说明一下,因为鸿蒙系统目前还处于Beta版本,所以这套适配方案还没有在实际业务中上线,属于技术层面比较前期的探索。接下来本文会通过原理和部分实现细节的介绍,分享我们在移植和开发过程中的一些经验。希望能对大家有所启发或者帮助。

背景知识和基础概念介绍

在适配开始之前,我们要明确好先做哪些事情。先来回顾一下 Flutter 的三层结构:

在 Flutter 的架构设计中,最上层为框架层,使用 Dart 语言开发,面向 Flutter 业务的开发者;中间层为引擎层,使用 C/C++ 开发,实现了 Flutter 的渲染管线和 Dart 运行时等基础能力;最下层为嵌入层,负责与平台相关的能力实现。显然我们要做的是将嵌入层移植到鸿蒙上,确切地说,我们要通过鸿蒙原生提供的平台能力,重新实现一遍 Flutter 嵌入层

对于 Flutter 嵌入层的适配,Flutter 官方有一份不算详细的指南,实际操作起来成本很高。由于鸿蒙的业务开发语言仍然可用 Java,在很多基础概念上与 Android 也有相似之处(如下表所示),我们可以从 Android 的实现入手,完成对鸿蒙的移植。

Flutter 在鸿蒙上的适配

如前文所述,要完成 Flutter 在新系统上的移植,我们需要完整实现 Flutter 嵌入层要求的所有子模块,而从能力支持角度,渲染交互以及其他必要的原生平台能力是保证 Flutter 应用能够运行起来的最基本的要素,需要优先支持。接下来会依次进行介绍。

1. 渲染流程打通

我们再来回顾一下 Flutter 的图像渲染流程。如图所示,设备发起垂直同步(VSync)信号之后,先经过 UI 线程的渲染管线(Animate/Build/Layout/Paint),再经过 Raster 线程的组合和栅格化,最终通过 OpenGL 或 Vulkan 将图像上屏。这个流程的大部分工作都由框架层和引擎层完成,对于鸿蒙的适配,我们主要关注的是与设备自身能力相关的问题,即:

(1) 如何监听设备的 VSync 信号并通知 Flutter 引擎?
(2) OpenGL/Vulkan 用于上屏的窗口对象从何而来?

VSync 信号的监听及传递

在 Flutter 引擎的 Android 实现中,设备的 VSync 信号通过 Choreographer 触发,它产生及消费流程如下图所示:

Flutter VSync

Flutter 框架注册 VSync 回调之后,通过 C++ 侧的 VsyncWaiter 类等待 VSync 信号,后者通过 JNI 等一系列调用,最终 Java 侧的 VsyncWaiter 类调用 Android SDK 的 Choreographer.postFrameCallback) 方法,再通过 JNI 一层层传回 Flutter 引擎消费掉此回调。Java 侧的 VsyncWaiter 核心代码如下:

@Override
public void asyncWaitForVsync(long cookie) {
  Choreographer.getInstance()
      .postFrameCallback(
        new Choreographer.FrameCallback() {
          @Override
          public void doFrame(long frameTimeNanos) {
            float fps = windowManager.getDefaultDisplay().getRefreshRate();
            long refreshPeriodNanos = (long) (1000000000.0 / fps);
            FlutterJNI.nativeOnVsync(
              frameTimeNanos, frameTimeNanos + refreshPeriodNanos, cookie);
          }
        });
}

在整个流程中,除了来自 Android SDK 的 Choreographer 以外,大多数逻辑几乎都由 C++ 和 Java 的基础 SDK 实现,可以直接在鸿蒙上复用,问题是鸿蒙目前的 API 文档中尚没有开放类似 Choreographer 的能力。所以现阶段我们可以借用鸿蒙提供的类似 iOS Grand Central Dispatch 的线程 API,模拟出 VSync 的信号触发与回调:

@Override
public void asyncWaitForVsync(long cookie) {
  // 模拟每秒 60 帧的屏幕刷新间隔:向主线程发送一个异步任务, 16ms 后调用
  applicationContext.getUITaskDispatcher().delayDispatch(() -> {
    float fps = 60; // 设备刷新帧率,HarmonyOS 未暴露获取帧率 API,先写死 60 帧
    long refreshPeriodNanos = (long) (1000000000.0 / fps);
    long frameTimeNanos = System.nanoTime();
    FlutterJNI.nativeOnVsync(frameTimeNanos, frameTimeNanos + refreshPeriodNanos, cookie);
  }, 16);
};

渲染窗口的构建及传递

在这一部分,我们需要在鸿蒙系统上构建平台容器,为 Flutter 引擎的图形渲染提供用于上屏的窗口对象。同样,我们参考 Flutter for Android 的实现,看一下 Android 系统是怎么做的:

Flutter 在 Android 上支持 Vulkan 和 OpenGL 两种渲染引擎,篇幅原因我们只关注 OpenGL。抛开复杂的注册及调用细节,本质上整个流程主要做了三件事:

  1. 创建了一个视图对象,提供可用于直接绘制的 Surface,将它通过 JNI 传递给原生侧;
  2. 在原生侧获取 Surface 关联的本地窗口对象,并交给 Flutter 的平台容器;
  3. 将本地窗口对象转换为 OpenGL ES 可识别的绘图表面(EGLSurface),用于 Flutter 引擎的渲染上屏。

接下来我们用鸿蒙提供的平台能力实现这三点。

a. 可用于直接绘制的视图对象

鸿蒙系统的 UI 框架提供了很多常用视图组件(Component),比如按钮、文字、图片、列表等,但我们需要抛开这些上层组件,获得直接绘制的能力。借助官方 媒体播放器开发指导 文档,可以发现鸿蒙提供了 SurfaceProvider 类,它管理的 Surface 对象可以用于视频解码后的展示。而 Flutter 渲染与视频上屏从原理上是类似的,因此我们可以借用 SurfaceProvider 实现 Surface 的管理和创建:

// 创建一个用于管理 Surface 的容器组件
SurfaceProvider surfaceProvider = new SurfaceProvider(context);
// 注册视图创建回调
surfaceProvider.getSurfaceOps().get().addCallback(surfaceCallback);

// ... 在 surfaceCallback 中
@Override
public void surfaceCreated(SurfaceOps surfaceOps) {
  Surface surface = surfaceOps.getSurface();
  // ...将 surface 通过 JNI 交给 Native 侧
  FlutterJNI.onSurfaceCreated(surface);
}

b. 与 Surface 关联的本地窗口对象

鸿蒙目前开放的 Native API 并不多,在官方文档中我们可以比较容易地找到 Native_layer API。根据文档的说明,Native API 中的 NativeLayer 对象刚好对应了 Java 侧的 Surface 类,借助 GetNativeLayer 方法,我们实现了两者之间的转化:

// platform_view_android_jni_impl.cc
static void SurfaceCreated(JNIEnv* env, jobject jcaller, jlong shell_holder, jobject jsurface) {
  fml::jni::ScopedJavaLocalFrame scoped_local_reference_frame(env);
  // 通过鸿蒙 Native API 获取本地窗口对象 NativeLayer
  auto window = fml::MakeRefCounted<AndroidNativeWindow>(
      GetNativeLayer(env, jsurface));
  ANDROID_SHELL_HOLDER->GetPlatformView()->NotifyCreated(std::move(window));
}

c. 与本地窗口对象关联的 EGLSurface

在 Android 的 AOSP 实现中,EGLSurface 可通过 EGL 库的 eglCreateWindowSurface 方法从本地窗口对象 ANativeWindow 创建而来。对于鸿蒙而言,虽然我们没有从公开文档找到类似的说明,但是 鸿蒙标准库 默认支持了 OpenGL ES,而且鸿蒙 SDK 中也附带了 EGL 相关的库及头文件,我们有理由相信在鸿蒙系统上,EGLSurface 也可以通过此方法从前一步生成的 NativeLayer 转化而来,在之后的验证中我们也确认了这一点:

// window->handle() 即为之前得到的 NativeLayer
EGLSurface surface = eglCreateWindowSurface(
      display, config_, reinterpret_cast<EGLNativeWindowType>(window->handle()),
      attribs);
//...交给 Flutter 渲染管线

2. 交互能力实现

交互能力是支撑 Flutter 应用能够正常运行的另一个基本要求。在 Flutter 中,交互包含了各种触摸事件、鼠标事件、键盘录入事件的传递及消费。以触摸事件为例,Flutter 事件传递的整个流程如下图所示:

Flutter 事件分发

iOS/Android 的原生容器通过触摸事件的回调 API 接收到事件之后,会将其打包传递至引擎层,后者将事件传发给 Flutter 框架层,并完成事件的消费、分发和逻辑处理。同样,整个流程的大部分工作已经由 Flutter 统一,我们要做的仅仅是在原生容器上监听用户的输入,并封装成指定格式交给引擎层而已。

在鸿蒙系统上,我们可以借助平台提供的 多模输入 API,实现多种类型事件的监听:

flutterComponent.setTouchEventListener(touchEventListener); // 触摸及鼠标事件
flutterComponent.setKeyEventListener(keyEventListener); // 键盘录入事件
flutterComponent.setSpeechEventListener(speechEventListener); // 语音录入事件

对于事件的封装处理,可以复用 Android 已有逻辑,只需要关注鸿蒙与 Android 在事件处理上的对应关系即可,比如触摸事件的部分对应关系:

3. 其他必要的平台能力

为了保证 Flutter 应用能够正常运行,除了最基本的渲染和交互外,我们的嵌入层还要提供资源管理、事件循环、生命周期同步等平台能力。对于这些能力 Flutter 大多都在嵌入层的公共部分有抽象类声明,只需要使用鸿蒙 API 重新实现一遍即可。

比如资源管理,引擎提供了 AssetResolver 声明,我们可以使用鸿蒙 Rawfile API 来实现:

class HAPAssetMapping : public fml::Mapping {
 public:
  HAPAssetMapping(RawFile* asset) : asset_(asset) {}
  ~HAPAssetMapping() override { CloseRawFile(asset_); }

  size_t GetSize() const override { return GetRawFileSize(asset_); }

  const uint8_t* GetMapping() const override {
    return reinterpret_cast<const uint8_t*>(GetRawFileBuffer(asset_));
  }

 private:
  RawFile* const asset_;

  FML_DISALLOW_COPY_AND_ASSIGN(HAPAssetMapping);
};

对于事件循环,引擎提供了 MessageLoopImpl 抽象类,我们可以使用鸿蒙 Native_EventHandler API 实现:

// runner_ 为鸿蒙 EventRunnerNativeImplement 的实例
void MessageLoopHarmony::Run() {
  FML_DCHECK(runner_ == GetEventRunnerNativeObjForThread());
  int result = ::EventRunnerRun(runner_);
  FML_DCHECK(result == 0);
}

void MessageLoopHarmony::Terminate() {
  int result = ::EventRunnerStop(runner_);
  FML_DCHECK(result == 0);
}

对于生命周期的同步,鸿蒙的 Page Ability 提供了完整的生命周期回调(如下图所示),我们只需要在对应的时机将状态上报给引擎即可。

Page Ability Lifecycle

当以上这些能力都准备好之后,我们就可以成功把 Flutter 应用跑起来了。以下是通过 DevEco Studio 运行官方 flutter gallery 应用的截图,截图中 Flutter 引擎已经使用鸿蒙系统的平台能力进行了重写:

DevEco Running Flutte

借由鸿蒙的多设备支持能力,此应用甚至可在 TV、车机、手表、平板等设备上运行:

Flutter Multiple Devices

总结和展望

通过上述的构建和适配工作,我们以极小的开发成本实现了 Flutter 在鸿蒙系统上的移植,基于 Flutter 开发的上层业务几乎不做任何修改就可以在鸿蒙系统上原生运行,为迎接鸿蒙系统后续的大规模推广也提前做好了技术储备。

当然,故事到这里并没有结束。在最基本的运行和交互能力之上,我们更需要关注 Flutter 与鸿蒙自身生态的结合:如何优雅地适配鸿蒙的分布式技术?如何用 Flutter 实现设备之间的快速连接、资源共享?现有的众多 Flutter 插件如何应用到鸿蒙系统上?未来 MTFlutter 团队将在这些方面做更深入的探索,因为解决好这些问题,才是真正能让应用覆盖用户生活的全场景的关键。

参考文献

作者简介

杨超,2016 年加入美团外卖技术团队,目前主要负责 MTFlutter 相关的基础建设工作。

| 想阅读更多技术文章,请关注美团技术团队(meituantech)官方微信公众号。

| 在公众号菜单栏回复【2019年货】、【2018年货】、【2017年货】、【算法】等关键词,可查看美团技术团队历年技术文章合集。

查看原文

CobraJ 回答了问题 · 1月27日

vue.js 传入模板到子组件{{}}无法解析

你这当然不会解释了,首先你吧模版当作了字符串用v-html写入了dom,这都意味着数据都挂载完了,里面的变量都不再做解析了,另外,你这么传模版,参数不传,这个组件是有作用域的,即便你不这么写,也应该是显示不出来,你直接{{content}}试一下,另外把 totalSeconds 也放在组件上当属性传递过去试试

关注 3 回答 2

CobraJ 回答了问题 · 1月13日

vue hash模式禁止浏览器回退

楼上正解,前提是能改上个网页的项目,如果不能就
window.addEventListener(`"popstate",` `function(e) {`

alert(`"我监听到了浏览器的返回按钮事件啦");//根据自己的需求实现自己的功能`

},false`);`
搞一下试试吧

关注 3 回答 3

CobraJ 回答了问题 · 2020-12-31

antdPro的项目,怎么写路由拦截器?

我看了一下 antdPro 用的是umi路由插件。

然后我看api文档有个 wrappers 不知道是不是你想要的路由守卫,一般react都是靠生命周期, 只是看你再什么地方拦截了,vue可能是开放了路由的hashchange事件,还有前进后退的事件,封装了守卫,不过还可以在引入路由的根文件位置在对应的生命周期做拦截。

希望能帮到你

关注 3 回答 2

CobraJ 收藏了文章 · 2020-12-25

掌握这些typescript性能提升一大截

typescript性能(译)

有些简单的Typescript配置,可以让你获得更快的编译和编辑体验,这些方法越早掌握越好。下面列举了除了最佳实践以外,还有一些用于调查缓慢的编译/编辑体验的常用技术,以及一些作为最后手段来帮助TypeScript团队调查问题的常用方法。

编写易编译代码

优先使用接口而不是交叉类型

很多时候,简单对象类型的类型别名与接口的作用非常相似

interface Foo { prop: string }

type Bar = { prop: string }; 

然而,只要你需要定义两个及以上的类型,你就可以选用接口来扩展这些类型,或者在类型别名中对它们相交,这时差异就变得明显了。

由于接口定义的是单一平面对象类型,可以检测属性是否冲突,解决这些冲突是非常必要的。另一方面,交叉类型只是递归的合并属性,有些情况下会产生never。接口则表现的一贯很好,而交叉类型定义的类型别名不能显示在其他的交叉类型上。接口之间的类型关系也会被缓存,而不是整个交叉类型。最后值得注意的区别是,如果是交叉类型,会在检查“有效” /“展平”类型之前检查所有属性。

因此,建议在创建交叉类型时使用带有接口/扩展的扩展类型

- type Foo = Bar & Baz & {

-     someProp: string;

- }

+ interface Foo extends Bar, Baz {

+     someProp: string;

+ } 

使用类型注释

添加类型注释,尤其是返回类型,可以节省编译器的大量工作。这是因为命名类型比匿名类型更简洁(编译器更喜欢),这减少了大量的读写声明文件的时间。虽然类型推导是非常方便的,没有必要到处这么做。但是,如果您知道了代码的慢速部分,可能会很有用

- import { otherFunc } from "other";

+ import { otherFunc, otherType } from "other";

- export function func() {

+ export function func(): otherType {

      return otherFunc();

  } 

优先使用基础类型而不是联合类型

联合类型非常好用--它可以让你表达一种类型的可能值范围

interface WeekdaySchedule {

    day: "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday";

    wake: Time;

    startWork: Time;

    endWork: Time;

    sleep: Time;

}

interface WeekendSchedule {

    day: "Saturday" | "Sunday";

    wake: Time;

    familyMeal: Time;

    sleep: Time;

}

declare function printSchedule(schedule: WeekdaySchedule | WeekendSchedule

); 

但是他们也带来了一定开销。每次将参数传递给 printSchedule 时,需要比较联合类型里的每个元素。对于一个由两个元素组成的联合类型来说,这是微不足道的。但是,如果你的联合类型有很多元素,这将引起编译速度的问题。例如,从联合类型中淘汰多余的部分,元素需要成对的去比较,工作量是呈二次递增的。当大量联合类型交叉一起时发生这种检查,会在每个联合类型上相交导致大量的类型,需要减少这种情况发生。避免这种情况的一种方法是使用子类型,而不是联合类型。

interface Schedule {

    day: "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday" | "Saturday" | "Sunday";

    wake: Time;

    sleep: Time;

}

interface WeekdaySchedule extends Schedule {

    day: "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday";

    startWork: Time;

    endWork: Time;

}

interface WeekendSchedule extends Schedule {

    day: "Saturday" | "Sunday";

    familyMeal: Time;

}

declare function printSchedule(schedule: Schedule); 

一个更现实的例子是,定义每种内置DOM元素的类型时。这种情况下,更优雅的方式是创建一个包含所有元素的 HtmlElement 基础类型,其中包括 DivElement 、 ImgElement 等。使用继承而不是创建一个无穷多的联合类型 DivElement | /.../ | ImgElement | /.../ 。

使用项目引用

使用TypeScript构建内容较多的代码时,将代码库组织成几个独立的项目会很有用。每个项目都有自己的 tsconfig.json ,可能它会对其他项目有依赖性。这有益于避免在一次编译中导入太多文件,也使某些代码库布局策略更容易地放在一起。

有一些非常基本的方法将一个代码库分解成多个项目。举个例子,一个程序代码,一部分用作客户端,一部分用作服务端,另一部分被其它两个共享

测试也可以分解到自己的项目中

一个常见的问题是 "一个项目应该有多大?"。这很像问 "一个函数应该有多大?"或 "一个类应该有多大?",在很大程度上,这归结于经验。人们熟悉的一种分割JS/TS代码的方法是使用文件夹。作为一种启发式的方法,如果它们关联性足够大,可以放在同一个文件夹中,那么它们就属于同一个项目。除此之外,要避免出现极大或极小规模的项目。如果一个项目比其他所有项目加起来都要大,那就是一个警告信号。同样,最好避免有几十个单文件项目,因为也会增加开销。

你可以在这里阅读更多关于项目参考资料

配置tsconfig.json或jsconfig.json

TypeScript和JavaScript用户可以用tsconfig.json文件任意配置编译方式。JavaScript用户也可以使用jsconfig.json文件配置自己的编辑体验。

指定文件

你应该始终确保你的配置文件没有包含太多文件

在 tsconfig.json 中,有两种方式可以指定项目中的文件

  • files列表
  • include、exclude列表

两者的主要区别是,files期望得到一个源文件的文件路径列表,而include/exclude使用通配符模式对文件进行匹配

虽然指定文件可以让TypeScript直接快速地加载文件,但如果你的项目中有很多文件,而不只是几个顶层的入口,那就会很麻烦。此外,很容易忘记添加新文件到tsconfig.json中,这意味着你可能最终会得到奇怪的编辑器行为,这些新文件被错误地分析,这些都很棘手。

include/exclude有助于避免指定这些文件,但代价是:必须通过include包含的目录来发现文件。当运行大量的文件夹时,这可能会减慢编译速度。此外,有时编译会包含很多不必要的.d.ts文件和测试文件,这会增加编译时间和内存开销。最后,虽然exclude有一些合理的默认值,但某些配置比如mono-repos,意味着像node_modules这样的 "重 "文件夹仍然可以最终被包含。

对于最佳做法,我们建议如下:

  • 在您的项目中只指定输入文件夹(即您想将其源代码包含在编译/分析中的文件夹)
  • 不要把其他项目的源文件混在同一个文件夹里
  • 如果把测试和其他源文件放在同一个文件夹里,请给它们取一个不同的名字,这样就可以很容易地把它们排除在外
  • 避免在源目录中出现大的构建工件和依赖文件夹,如node_modules

注意:如果没有排除列表,默认情况下node_modules是被排除的;一旦添加了node_modules,就必须明确地将node_modules添加到列表中。

下面是一个合理的tsconfig.json,用来演示这个操作

{

    "compilerOptions": {

    // ...

    },

    "include": ["src"],

    "exclude": ["**/node_modules", "**/.*/"],

} 

控制包含的@types

默认情况下,TypeScript会自动包含每一个在node_modules文件夹中找到的@types包,不管你是否导入它。这是为了在使用Node.js、Jasmine、Mocha、Chai等工具/包时,使某些东西 "能够工作",因为这些工具/包没有被导入--它们只是被加载到全局环境中

有时这种逻辑在编译和编辑场景下都会拖慢程序的构建时间,甚至会造成多个全局包的声明冲突的问题,造成类似于如下问题

Duplicate identifier 'IteratorResult'.

Duplicate identifier 'it'.

Duplicate identifier 'define'.

Duplicate identifier 'require'. 

在不需要全局包的情况下,修复方法很简单,只要在 tsconfig.json/jsconfig.json 中为 "type "选项指定一个空字段即可。

// src/tsconfig.json

{

    "compilerOptions": {

        // ...

        // Don't automatically include anything.

        // Only include `@types` packages that we need to import.

        "types" : []

    },

    "files": ["foo.ts"]

} 

如果您仍然需要一些全局包,请将它们添加到类型字段中

// tests/tsconfig.json

{

   "compilerOptions": {

       // ...

       // Only include `@types/node` and `@types/mocha`.

       "types" : ["node", "mocha"]

   },

   "files": ["foo.test.ts"]

} 

增量项目输出

--incremental标志允许TypeScript将上次编译的状态保存到一个 .tsbuildinfo 文件中。这个文件用来计算上次运行后可能被重新检查/重新输出的最小文件集,就像TypeScript的--watch模式一样。

当对项目引用使用复合标志时,默认情况下会启用增量编译,但这样也能带来同样的速度提升。

跳过 .d.ts 检查

默认情况下,TypeScript会对一个项目中的所有.d.ts文件进行全面检查,以发现问题或不一致的地方;然而,这检查通常是不必要的。大多数时候,.d.ts文件都是已知如何工作的--类型之间相互扩展的方式已经被验证过一次,重要的声明还是会被检查。

TypeScript提供了一个选项,使用skipDefaultLibCheck标志来跳过.d.ts文件的类型检查(例如lib.d.ts)

另外,你也可以启用 skipLibCheck 标志来跳过编译中的所有 .d.ts 文件

这两个选项通常会隐藏.d.ts文件中的错误配置和冲突,所以只建议在快速构建场景中使用它们。

使用更快的差异检查

狗的列表是动物的列表吗?也就是说,List<Dog>是否可以分配给List<Animals>?寻找答案的直接方法是逐个成员进行类型结构比较。不幸的是,这可能带来昂贵的性能开销。然而,如果我们对List<T>有足够的了解,我们可以将这个可分配性检查简化为确定Dog,是否可以分配给Animal(即不考虑List<T>的每个成员)。特别是,当我们需要知道类型参数T的差别。编译器只有在启用strictFunctionTypes标志的情况下,才能充分利用这种潜在的加速优势(否则,它就会使用较慢的,但更宽松的结构检查)。因此,我们建议使用 --strictFunctionTypes 来构建(默认在 --strict 下启用)

配置其他构建工具

TypeScript编译经常与其他构建工具一起执行--特别是在编写可能涉及捆绑程序的Web应用程序时。虽然我们只能对一些构建工具提出建议,但理想情况下,这些技术可以被普及。

确保除了阅读本节外,你还阅读了关于你所选择的构建工具的性能--例如:

  • ts-loader的Faster Builds部分
  • awesome-typescript-loader的性能问题部分

并行类型检查

类型检查通常需要从其他文件中获取信息,与转换/输出代码等其他步骤相比,类型检查可能相对昂贵。因为类型检查可能会花费更多的时间,它可能会影响到内部的开发循环--换句话说,你可能会经历更长的编辑/编译/运行周期,这可能会令你头疼。

出于这个原因,一些构建工具可以在一个单独的进程中运行类型检查,而不会阻塞输出。虽然这意味着在TypeScript构建而发生错误报告之前已经有无效的代码运行,通常会先在编辑器中看到错误,而不会被长时间地阻止运行工作代码

一个实际的例子是Webpack的fork-ts-checker-webpack-plugin插件,或者awesome-typescript-loader有时也会这样做。

隔离文件输出

默认情况下,TypeScript输出需要的语义信息可能不是本地文件。这是为了理解如何输出像 const enums 和 namespaces 这样的功能。但是需要检查其他文件来生成某个文件,这会使输出速度变慢。

对需要非本地信息的功能需求是比较少见的--常规枚举可以用来代替const枚举,模块可以用来代替命名空间。鉴于此,TypeScript提供了isolatedModules标志,以便在由非本地信息驱动的功能上报错。启用 isolatedModules 意味着你的代码库对于使用 TypeScript APIs(如 transpileModule)或替代编译器(如 Babel)的工具是安全的。

举个例子,下面的代码在运行时无法正常使用独立的文件转换,因为const enum值被期望内联;幸运的是, isolatedModules会在早期告诉我们这一点

// ./src/fileA.ts

export declare const enum E {

    A = 0,

    B = 1,

}

// ./src/fileB.ts

import { E } from "./fileA";

console.log(E.A);//          ~

// error: Cannot access ambient const enums when the '--isolatedModules' flag is provided. 
记住:isolatedModules不会自动让代码生成速度更快--它只是告诉你,你即将使用一个可能不被支持的功能。你要的是独立模块在不同的构建工具和API中的输出

可以通过使用以下工具来影响独立文件的输出

  • ts-loader提供了一个transpileOnly标志,通过使用transpileModule来执行独立文件输出
  • awesome-typescript-loader提供了一个transpileOnly标志,通过使用transpileModule来执行独立文件输出
  • TypeScript可以直接使用transpileModule API
  • awesome-typescript-loader提供了useBabel标志
  • babel-loader以单独的方式编译文件(但不提供类型检查)
  • gulp-typescript 启用 isolatedModules 时,可以实现独立文件输出
  • rollup-plugin-typescript只执行独立文件编译
  • ts-jest可以使用( isolatedModules标志设为true )isolatedModules为true
  • ts-node 可以检测 tsconfig.json 的 "ts-node "字段中的 "transpileOnly "选项,也有一个 --transpile-only 标志。

调查问题

有一定的方法可以得到可能出问题的提示

禁用编辑器插件

编辑器的体验受到插件的影响。尝试禁用插件(尤其是JavaScript/TypeScript相关的插件),看看是否能解决性能和响应速度方面的问题。

某些编辑器也有自己的性能故障排除指南,所以可以考虑阅读一下。例如,Visual Studio Code也有自己的性能问题介绍。

诊断扩展

你可以用--extendedDiagnostics来运行TypeScript,以获得编译器花费时间的打印日志。

Files:                         6

Lines:                     24906

Nodes:                    112200

Identifiers:               41097

Symbols:                   27972

Types:                      8298

Memory used:              77984K

Assignability cache size:  33123

Identity cache size:           2

Subtype cache size:            0

I/O Read time:             0.01s

Parse time:                0.44s

Program time:              0.45s

Bind time:                 0.21s

Check time:                1.07s

transformTime time:        0.01s

commentTime time:          0.00s

I/O Write time:            0.00s

printTime time:            0.01s

Emit time:                 0.01s

Total time:                1.75s 
请注意,总时间不是前面所有时间的总和,因为有一些重叠,有些工作是没有衡量工具的。

对于大多数用户来说,最相关的信息是:

FieldMeaning
Filesthe number of files that the program is including (use --listFiles to see what they are).
I/O Read timetime spent reading from the file system - this includes traversing include'd folders.
Parse timetime spent scanning and parsing the program
Program timecombined time spent performing reading from the file system, scanning and parsing the program, and other calculation of the program graph. These steps are intermingled and combined here because files need to be resolved and loaded once they're included via imports and exports.
Bind timeTime spent building up various semantic information that is local to a single file.
Check timeTime spent type-checking the program.
transformTime timeTime spent rewriting TypeScript ASTs (trees that represent source files) into forms that work in older runtimes.
commentTimeTime spent calculating comments in output files.
I/O Write timeTime spent writing/updating files on disk.
printTimeTime spent calculating the string representation of an output file and emitting it to disk.

考虑到这些投入,你可能会想问一些问题:

  • 文件数/代码行数是否与您项目中的文件数大致一致?如果不符合,请尝试运行--listFiles
  • 程序时间或I/O读取时间是否相当高?请确保你的include/exclude配置正确
  • 其他时间看起来不对劲吗?你可能想提出一个问题。你可以做以下事情来帮助诊断

    • 如果打印时间较高,则使用emitDeclarationOnly运行
    • 阅读关于报告编译器性能问题的说明

显示配置

当运行 tsc 时,并不能明显地看到编译的内容设置,特别是考虑到 tsconfig.jsons 可以扩展其他配置文件。showConfig 可以解释 tsc 将为一个调用计算着什么。

tsc --showConfig

# or to select a specific config file...

tsc --showConfig -p tsconfig.json 

追踪分辨率

运行 traceResolution 可以有助于解释,一个文件为什么被包含在编译中。输出有点繁琐,所以你可能想把输出重定向到一个文件。

tsc --traceResolution > resolution.txt 

如果你发现了一个不应该存在的文件,你可能需要修改你的tsconfig.json中的include/exclude列表,或者,你可能需要调整其他设置,比如type、typeRoots或paths

独立运行tsc

很多时候,用户在使用第三方构建工具(如Gulp、Rollup、Webpack等)时都会遇到性能缓慢的问题。运行tsc --extendedDiagnostics,可以发现TypeScript和工具之间的差异,用以说明外部配置的错误或效率低下。

一些需要注意的问题:

  • tsc和集成了TypeScript的构建工具在构建时间上有很大的区别吗?
  • 如果构建工具提供诊断,那么TypeScript的分辨率和构建工具的分辨率是否有区别?
  • 构建工具是否有自己的配置,可能的原因是什么?
  • 构建工具是否有可能是TypeScript集成的配置原因?(例如ts-loader的选项?)

升级依赖性

有时TypeScript的类型检查会受到计算密集的.d.ts文件的影响。这很罕见也很可能会发生。升级到一个较新的TypeScript版本(可以更有效率)或一个较新版本的@types包(可能已经恢复了一个回归)通常可以解决这个问题。

常见的问题

一旦你已经排除了故障,你可能想探索一些常见问题的修复方法。如果以下解决方案不起作用,可能值得提出问题。

include和exclude配置不当

如上所述,include/exclude选项可以在以下几个方面被滥用

ProblemCauseFix
node_modules was accidentally included from deeper folderexclude was not set"exclude": ["/node_modules", "/.*/"]
node_modules was accidentally included from deeper folder"exclude": ["node_modules"]"exclude": ["/node_modules", "/.*/"]
Hidden dot files (e.g. .git) were accidentally included"exclude": ["**/node_modules"]"exclude": ["/node_modules", "/.*/"]
Unexpected files are being included.include was not set"include": ["src"]

提出问题

如果你的项目已经进行了正确的优化配置,你可能需要提出一个问题。

最好的性能问题报告包含容易获得的和最小的问题复制品。换句话说,一个容易通过git克隆的代码库,只包含几个文件。它们不需要与构建工具的外部集成--它们可以通过调用tsc或调用TypeScript API的独立代码。不优先考虑那些需要复杂调用和设置的代码库。

我们理解这一点却不容易实现--特别是,很难在代码库中隔离问题的源头,而且共享知识产权可能也是一个问题。在某些情况下,如果我们认为问题影响较大,团队将愿意发送一份保密协议(NDA)。

无论是否可以复制,在提交问题时,按照这些方法,将有助于为您提供性能修复。

报告编译器性能问题

有时,你会在构建时间以及编辑场景中发现性能问题。在这种情况下,最好关注于TypeScript编译器。

首先,应该使用TypeScript的next版本,以确保你不会碰到那些已解决的问题。

npm install --save-dev typescript@next

# or

yarn add typescript@next --dev 

一个编译器的问题可能包括

  • 安装的TypeScript版本(例如:npx tsc -v 或 yarn tsc -v)
  • TypeScript运行的Node版本(例如:node -v)
  • 使用extendedDiagnostics运行的输出(tsc --extendedDiagnostics -p tsconfig.json)
  • 理想的情况是,一个项目能够展示所遇到的问题
  • 剖析编译器的输出日志(isolate---.log 和.cpuprofile 文件)

剖析编译器

通过使用--trace-ic标志与--generateCpuProfile标志,来让TypeScript运行Node.js v10+,这对团队提供诊断结果来说是很重要的:

node --trace-ic ./node_modules/typescript/lib/tsc.js --generateCpuProfile profile.cpuprofile -p tsconfig.json 

这里的 ./node_modules/typescript/lib/tsc.js 可以用来替换你的TypeScript编译器的安装版本,而tsconfig.json可以是任何TypeScript配置文件。 profile.cpuprofile是你选择的输出文件。

这将产生两个文件:

  • --trace-ic 将输出到 isolate---*.log 的文件中(例如 isolate-00000176DB2DF130-17676-v8.log)
  • --generateCpuProfile将以您选择的名称输出到一个文件中。在上面的例子中,它将是一个名为 profile.cpuprofile 的文件
警告:这些文件可能包含你的工作空间的信息,包括文件路径和源代码。这两个文件都可以作为纯文本阅读,您可以在将它们提交为 GitHub 问题之前修改它们。(例如,清除可能暴露内部专用信息的文件路径)。

但是,如果你对在GitHub上公开发布这些有任何顾虑,请告诉我们,可以私下分享细节。

报告编辑绩效问题

编辑性能经常受到很多东西的影响,TypeScript团队唯一能控制的是JavaScript/TypeScript语言服务的性能,以及该语言服务和某些编辑器(即Visual Studio、Visual Studio Code、Visual Studio for Mac和Sublime Text)之间的集成。确保所有第三方插件在编辑器中被关闭,以确定是否有TypeScript本身的问题。

编辑性能问题稍有涉及,但同样的想法也适用于:可被克隆的最小重现代码库是理想的,虽然在某些情况下,团队将能够签署NDA来调查和隔离问题。

包括tsc--extendedDiagnostics的输出是很好的上下文,但取一个TSServer日志是最有用的。

收集TSServer日志

在Visual Studio代码中收集TSServer日志
  1. 打开你的命令调色板,然后选择

    1. 进入 "首选项 "打开您的全局设置。打开用户设置
    2. 入偏好设置,打开本地项目。打开工作区设置
  2. 设置选项 "typecript.tsserver.log":"verbose"
  3. 重启VS Code,重现问题
  4. 在VS Code中,运行TypeScript。打开TS服务器日志命令
  5. 这将打开tsserver.log文件

⚠警告:TSServer日志可能会包含你的工作空间的信息,包括文件路径和源代码。如果你对在GitHub上公开发布有任何顾虑,请告诉我们,你可以私下分享细节。

查看原文

CobraJ 收藏了问题 · 2020-12-25

.ts文件识别不了.vue 文件

main.ts

import Vue from "vue";
import Component from "vue-class-component";
import App from "./App.vue";

报这个错误

Cannot find module './App.vue' or its corresponding type declarations.

tsconfig.json

{ // 编译选项
  "compilerOptions": {
    // 编译输出目标 ES 版本
    "target": "esnext",
    // 采用的模块系统
    "module": "esnext",
    // 以严格模式解析
    "strict": false,
    "jsx": "preserve",
    // 从 tslib 导入外部帮助库: 比如__extends,__rest等
    "importHelpers": true,
    // 如何处理模块
    "moduleResolution": "node",
    // 启用装饰器
    "experimentalDecorators": true,
    "esModuleInterop": true,
    // 允许从没有设置默认导出的模块中默认导入
    "allowSyntheticDefaultImports": true,
    // 定义一个变量就必须给它一个初始值
    "strictPropertyInitialization" : false,
    // 允许编译javascript文件
    "allowJs": true,
    // 是否包含可以用于 debug 的 sourceMap
    "sourceMap": true,
    // 忽略 this 的类型检查, Raise error on this expressions with an implied any type.
    "noImplicitThis": false,
    // 解析非相对模块名的基准目录 
    "baseUrl": ".",
    // 给错误和消息设置样式,使用颜色和上下文。
    "pretty": true,
    // 设置引入的定义文件
    "types": ["webpack-env", "mocha", "chai"],
    // 指定特殊模块的路径
    "paths": {
      "@/*": ["src/*"]
    },
    // 编译过程中需要引入的库文件的列表
    "lib": ["esnext", "dom", "dom.iterable", "scripthost"],
    "typeRoots": [
      "./types",
      "./node_modules/vue/types",
      "./src"
    ],
  },
  // ts 管理的文件
  "include": [
    "src/**/*.ts",
    "src/**/*.tsx",
    "src/**/*.vue",
    "tests/**/*.ts",
    "tests/**/*.tsx"
  ],
  // ts 排除的文件
  "exclude": ["node_modules"]
}

shims-vue.d.ts

import Vue from "vue";
import VueRouter, { Route } from "vue-router";

declare module '*.vue' {
  export default Vue
}


declare module "vue/types/vue" {
  interface Vue {
    $router: VueRouter; // 这表示this下有这个东西
    $route: Route;
    $http: any;
    $Message: any;
    $Modal: any;
  }
}

第一次用ts写vue,不知道为什么报这个错误,

.vue文件里能识别.vue文件,.ts文件里就识别不了,但是页面能正常打开

package.json

{
  "name": "scgx",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "start": "vue-cli-service serve",
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint",
    "test:unit": "vue-cli-service test:unit"
  },
  "dependencies": {
    "axios": "^0.18.0",
    "element-ui": "^2.13.2",
    "vue": "^2.6.6",
    "vue-class-component": "^6.0.0",
    "vue-property-decorator": "^7.0.0",
    "vue-router": "^3.0.1",
    "vuex": "^3.0.1"
  },
  "devDependencies": {
    "@types/chai": "^4.1.0",
    "@types/mocha": "^5.2.4",
    "@vue/cli-plugin-babel": "^3.5.0",
    "@vue/cli-plugin-eslint": "^3.5.0",
    "@vue/cli-plugin-typescript": "^3.5.0",
    "@vue/cli-plugin-unit-mocha": "^3.5.0",
    "@vue/cli-service": "^3.5.0",
    "@vue/eslint-config-prettier": "^4.0.1",
    "@vue/eslint-config-typescript": "^4.0.0",
    "@vue/test-utils": "1.0.0-beta.29",
    "babel-eslint": "^10.0.1",
    "babel-plugin-component": "^1.1.1",
    "chai": "^4.1.2",
    "eslint": "^5.8.0",
    "eslint-plugin-vue": "^5.0.0",
    "less": "^3.0.4",
    "less-loader": "^4.1.0",
    "typescript": "^3.2.1",
    "vue-template-compiler": "^2.5.21",
    "vuex-class": "^0.3.2"
  }
}

vue.config.js

const path = require("path");
const sourceMap = process.env.NODE_ENV === "development";

module.exports = {
  // 基本路径
  publicPath: "./",
  // 输出文件目录
  outputDir: "dist",
  // eslint-loader 是否在保存的时候检查
  lintOnSave: false,
  // webpack配置
  // see https://github.com/vuejs/vue-cli/blob/dev/docs/webpack.md
  chainWebpack: () => {},
  configureWebpack: config => {
    if (process.env.NODE_ENV === "production") {
      // 为生产环境修改配置...
      config.mode = "production";
    } else {
      // 为开发环境修改配置...
      config.mode = "development";
    }

    Object.assign(config, {
      // 开发生产共同配置
      resolve: {
        extensions: [".js", ".vue", ".json", ".ts", ".tsx"],
        alias: {
          vue$: "vue/dist/vue.js",
          "@": path.resolve(__dirname, "./src"),
          "@c": path.resolve(__dirname, "./src/components"),
          utils: path.resolve(__dirname, "./src/utils"),
          views: path.resolve(__dirname, "./src/views"),
          assets: path.resolve(__dirname, "./src/assets"),
          com: path.resolve(__dirname, "./src/components")
        }
      }
    });
  },
  // 生产环境是否生成 sourceMap 文件
  productionSourceMap: sourceMap,
  // css相关配置
  css: {
    // 是否使用css分离插件 ExtractTextPlugin
    extract: true,
    // 开启 CSS source maps?
    sourceMap: false,
    // css预设器配置项
    loaderOptions: {},
    // 启用 CSS modules for all css / pre-processor files.
    modules: false
  },
  // use thread-loader for babel & TS in production build
  // enabled by default if the machine has more than 1 cores
  parallel: require("os").cpus().length > 1,
  // PWA 插件相关配置
  // see https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-pwa
  pwa: {},
  // webpack-dev-server 相关配置
  devServer: {
    open: process.platform === "darwin",
    host: "localhost",
    port: 3001, //8080,
    https: false,
    hotOnly: false,
    proxy: {
      // 设置代理
      // proxy all requests starting with /api to jsonplaceholder
      "/api": {
        target: "http://localhost:8989/",
        changeOrigin: true,
        ws: true,
        pathRewrite: {
          "^/api": ""
        }
      }
    },
    before: app => {}
  },
  // 第三方插件配置
  pluginOptions: {
    // ...
  }
};

CobraJ 收藏了文章 · 2020-12-25

关于ts-loader 中的 appendTsSuffixTo浅见

引言

项目使用的 Vue + TypeScript + webpack,其中TypeScript 使用的是ts-loader
由于使用了vue单文件组件,所以ts-loader配置了appendTsSuffixTo: [/\.vue$/]
但是发现在使用thread-loadercache-loader加速构建时,会报Could not find file: '*.vue.ts'的错误。但是项目中并没有*.vue.ts的文件。

关于appendTsSuffixTo

官方文档给出的解释:appendTsxSuffixTo

A list of regular expressions to be matched against filename. If filename matches one of the regular expressions, a .ts or .tsx suffix will be appended to that filename.
This is useful for *.vue file format for now. (Probably will benefit from the new single file format in the future.)。

大致意思是,会给对应文件添加个.ts.tsx后缀。这也就是报错的找不到vue.ts的由来。
让我们来梳理下ts编译vue 单文件组件的过程:

vue 单文件组件中假如使用了lang="ts"ts-loader需要配置appendTsSuffixTo: [/\.vue$/],用来给.vue文件添加个.ts后缀用于编译。

但是这个.ts文件并不实际存在,所以在使用cache-loader时,会报找不到这个文件的错误。

解决方案

由于报的是找不到文件错误,那我们就把 TypeScript代码.vue中移出来。
使用一个单独的ts文件,然后vue在引用这个ts文件

xxx.vue文件:


<template>
<div>
</div>
</template>

<script lang="ts" data-original="./xxx.ts"></script>

<style>
</style>

xxx.ts文件:

export default {
}

参考

  1. threader-loader例子
  2. Vue single file , after add lang="ts",Module build failed: Error: Could not find file: '*.vue'.
  3. ts-loader
  4. awesome-typescript-loader
查看原文

CobraJ 收藏了文章 · 2020-11-03

Docker 与 Kubernetes在前端开发的应用

Docker是开发人员和系统管理员使用容器开发、部署和运行应用程序的平台,使用Linux容器来部署应用程序称为集装箱化,使用Docker可以轻松部署应用程序。Docker 和传统部署方式最大的不同在于,它将不会限制我们使用任何工具,任何语言,任何版本的 runtime,Docker 将我们的应用看成一个只提供网络服务的盒子(也即容器),Kubernetes 则是对这些盒子进行更多自动化的操作,自动创建,自动重启,自动扩容,自动调度,这个过程称之为容器编排。

现在,容器编排技术给 Web 应用带来了巨大的灵活性,让我们轻松创建需要的程序对外提供服务。和传统的 IaaS 相比,不需要去关心云主机申请,云主机配置等信息,也不需考虑云主机故障导致的服务不可用,由 Kubernetes 的副本控制器帮我们完成云主机故障发生后容器迁移。下面就让我们来看一下Docker和Kubernetes在前端应用开发的一些应用。

Docker安装

和前端工具一样,使用Docker容器之前,需要先安装对应的工具。
Linux Debian/Ubuntu, 安装 社区版DockerCE
Windows 一键安装
macOS 一键安装

需要说明的是,Windows7 系统,需要使用 VirtualBox 安装 Linux 作为 Docker 的宿主机,Windows10 Pro 会使用 Hyper-V 安装 Linux 作为 Docker 的宿主机。

我们可以使用阿里云的镜像进行下载,然后再进行安装。

http://mirrors.aliyun.com/docker-toolbox/mac/docker-for-mac/

安装完成后,启动Docker即可。

配置镜像加速

在国内访问默认的官方镜像比较慢,我们可以使用阿里云的镜像加速,注册阿里账号并申请容器服务之后,然后点击容器镜像服务的镜像加速地址查看地址,如下图所示。
在这里插入图片描述
然后在Docker的Preferences中配置中添加加速地址,选择Resources的Proxies填入阿里云的镜像加速地址,如下所示。
在这里插入图片描述

注册Docker ID

w为了方便使用和管理镜像,我们可以点击Docker官方地址来注册Docker ID。注册完成之后,就可以在Mac版Docker桌面工具中进行登录,并查看自己已有的镜像,如下图所示。
在这里插入图片描述

基本使用

Docker命令

打开终端,然后输入命令docker即可查看Docker支持的命令,如下所示。

Usage:    docker [OPTIONS] COMMAND
 
A self-sufficient runtime for containers
 
Options:
      --config string      Location of client config files (default "/Users/crane/.docker")
  -c, --context string     Name of the context to use to connect to the daemon (overrides DOCKER_HOST env var and default context set with "docker context use")
  -D, --debug              Enable debug mode
  -H, --host list          Daemon socket(s) to connect to
  -l, --log-level string   Set the logging level ("debug"|"info"|"warn"|"error"|"fatal") (default "info")
      --tls                Use TLS; implied by --tlsverify
      --tlscacert string   Trust certs signed only by this CA (default "/Users/crane/.docker/ca.pem")
      --tlscert string     Path to TLS certificate file (default "/Users/crane/.docker/cert.pem")
      --tlskey string      Path to TLS key file (default "/Users/crane/.docker/key.pem")
      --tlsverify          Use TLS and verify the remote
  -v, --version            Print version information and quit
 
Management Commands:
  app*        Docker Application (Docker Inc., v0.8.0)
  builder     Manage builds
  buildx*     Build with BuildKit (Docker Inc., v0.3.1-tp-docker)
  checkpoint  Manage checkpoints
  config      Manage Docker configs
  container   Manage containers
  context     Manage contexts
  image       Manage images
  manifest    Manage Docker image manifests and manifest lists
  network     Manage networks
  node        Manage Swarm nodes
  plugin      Manage plugins
  secret      Manage Docker secrets
  service     Manage services
  stack       Manage Docker stacks
  swarm       Manage Swarm
  system      Manage Docker
  trust       Manage trust on Docker images
  volume      Manage volumes
 
Commands:
  attach      Attach local standard input, output, and error streams to a running container
  build       Build an image from a Dockerfile
  commit      Create a new image from a container's changes
  cp          Copy files/folders between a container and the local filesystem
  create      Create a new container
  deploy      Deploy a new stack or update an existing stack
  diff        Inspect changes to files or directories on a container's filesystem
  events      Get real time events from the server
  exec        Run a command in a running container
  export      Export a container's filesystem as a tar archive
  history     Show the history of an image
  images      List images
  import      Import the contents from a tarball to create a filesystem image
  info        Display system-wide information
  inspect     Return low-level information on Docker objects
  kill        Kill one or more running containers
  load        Load an image from a tar archive or STDIN
  login       Log in to a Docker registry
  logout      Log out from a Docker registry
  logs        Fetch the logs of a container
  pause       Pause all processes within one or more containers
  port        List port mappings or a specific mapping for the container
  ps          List containers
  pull        Pull an image or a repository from a registry
  push        Push an image or a repository to a registry
  rename      Rename a container
  restart     Restart one or more containers
  rm          Remove one or more containers
  rmi         Remove one or more images
  run         Run a command in a new container
  save        Save one or more images to a tar archive (streamed to STDOUT by default)
  search      Search the Docker Hub for images
  start       Start one or more stopped containers
  stats       Display a live stream of container(s) resource usage statistics
  stop        Stop one or more running containers
  tag         Create a tag TARGET_IMAGE that refers to SOURCE_IMAGE
  top         Display the running processes of a container
  unpause     Unpause all processes within one or more containers
  update      Update configuration of one or more containers
  version     Show the Docker version information
  wait        Block until one or more containers stop, then print their exit codes
Run 'docker COMMAND --help' for more information on a command.

关于这些命令的含义,可以看Docker 常用命令

比如,我们要搜索nginx镜像,那么只需要使用下面的命令即可。

docker search nginx

搜索结果中标记【OFFICIAL】的为官方镜像,其他为用户自定义镜像,可根据实际需要选择。
在这里插入图片描述
如果要获取镜像,可以使用docker pull命令,如下所示,获取nginx。


# 拉取指定版本xxx镜像
docker pull nginx:xxx
# 拉取最新版本镜像 
docker pull nginx    //等价于docker pull nginx:latest

镜像拉取成功后,然后使用下面的命令启动nginx容器,如下所示。

docker run -d -p 8080:80 --name docker-nginx nginx

上面的命令会将容器内部的80端口映射到了本机的8080端口,所以启动成功后可以使用http://localhost:8080/访问docker容器内部nginx80端口映射的地址,如下图所示。
在这里插入图片描述
一些常见的启动参数:

  • -p 本机端口:容器端口 映射本地端口到容器
  • -P 将容器端口映射为本机随机端口
  • -v 本地路径或卷名:容器路径 将本地路径或者数据卷挂载到容器的指定位置
  • -it 作为交互式命令启动
  • -d 将容器放在后台运行 --rm 容器退出后清除资源

查看及停止容器

查看容器的基本命令如下所示。

# 查看运行中的容器
docker ps
 
# 查看所有容器(包括正在运行和已经停止运行的)
docker ps -a

停止容器命令使用的是kill命令,如下所示。

# 通过id直接关闭容器
# docker kill a0fbf4519279
# 通过容器名称直接关闭容器
docker kill docker-nginx
 
# 通过id直接容器 默认等待十秒 超时强制关闭
# docker stop a0fbf4519279
# 通过容器名称关闭容器 默认等待十秒 超时强制关闭  等价于 docker stop -t=10 docker-nginx
docker stop docker-nginx

重新启动容器的命令如下。

# 启动容器可通过容器id或者容器名称
# 通过容器名称启动容器,如果已启动则忽略
docker start docker-nginx
 
# 通过容器名称重新启动容器,如果未启动则直接启动,如果已启动则关闭再启动
# docker restart docker-nginx

Docker是如何工作的

Docker 使用的是 C/S 结构,即客户端/服务器体系结构。明白了 Docker 客户端与 Docker 服务器进行交互时, Docker 服务端负责构建、运行和分发 Docker 镜像。 并且Docker 客户端和服务端可以运行在一台机器上,可以通过 RESTful 、 stock 或网络接口与远程 Docker 服务端进行通信。

而Docker 的底层核心原理是利用了 Linux 内核的 namespace 以及 cgroup 特性,其中 namespace 进行资源隔离,cgroup 进行资源配额, 其中 Linux 内核中一共有 6 种 namespace,分别对应如下。

在这里插入图片描述
在系统调用中有三个与namespace有关的函数,分别是clone、unshare和setns。

clone:如果想让子进程拥有独立的网络地址,TCP/IP 协议栈,那么就可以使用clone,如下所示。参考:https://man7.org/linux/man-pages/man2/clone.2.html

clone(cb, *stack , CLONE_NEWNET, 0)

unshare:将当前进程转移到新的 namespace 中, 例如使用 fork 或 vfork 创建的进程将默认共享父级资源,使用 unshare 将子进程从父级取消共享。参考:https://man7.org/linux/man-pages/man2/setns.2.html

setns:给指定的PID指定 namespace, 通常用于共享 namespace。参考:https://man7.org/linux/man-pages/man2/setns.2.html

Linux 的内核层支持了在系统调用中隔离 namespace, 通过给一个进程分配单独的 namespace 从而让其在各个资源维度进行隔离,每个进程都能获取到自己的主机名,IPC、 PID、IP和根文件系统、用户组等信息,就像在一个独占系统中,不过虽然资源进行了隔离,但是内核还是共享同一个,这也是比传统虚拟机轻量的原因之一。

另外只有资源进行隔离还不够,要想保证真正的故障隔离,互不影响, 还需要对针对 CPU, 内存,GPU 等进行限制,因为如果一个程序出现死循环或者内存泄露也会导致别的程序无法运行。

Docker 网络

一个容器要想提供服务,就需要将自身的网络暴露出去。Docker 是与宿主机上的环境是隔离的,要想暴露服务就需要显示告诉 Docker 哪些端口允许外部访问,在运行 docker run -p 80:80 nginx 时这里就是将容器内部的 80 端口暴露到宿主机的 80 端口上,具体的端口转发下面会具体分析一下。容器的网络部分是容器中最重要的部分,也是构建大型集群的基石,在我们部署 Docker 的应用时,需要要对网络有个基本的了解。

目前,Docker 提供了四种网络模式,分别为 Host、Container、 None和Bridge ,我们可以使用 --net 来进行指定网络模式。

Host 模式

Host 模式不会单独为容器创建 network namespace, 容器内部直接使用宿主机网卡,此时容器内获取 ip 为宿主机 ip,端口绑定直接绑在宿主机网卡上,优点是网络传输时不用经过 NAT 转换,效率更高速度更快。但是docker host上已经使用的端口就不能再用了,网络的隔离性不好。

可以使用如下的命令开启Host模式。

docker run --net host nginx

Host模式的示意图如下所示。
在这里插入图片描述

Container 模式

和指定的 container 共享 network namespace, 共享网络配置,ip 地址和端口,其中无法共享网络模式为 Host 的容器。新创建的容器不会创建自己的网卡,配置自己的 IP,而是和一个指定的容器共享 IP、端口范围等。同样,两个容器除了网络方面,其他的如文件系统、进程列表等还是隔离的,两个容器的进程可以通过 lo 网卡设备通信。

开启Host模式的命令如下。

docker run --net container:xxx_containerid nginx

None 模式

使用none模式,Docker容器拥有自己的Network Namespace,但是,并不为Docker容器进行任何网络配置。也就是说,这个Docker容器没有网卡、IP、路由等信息。需要我们自己为Docker容器添加网卡、配置IP等。指定为 None 模式的容器内将不会分配网卡设备,仅有内部 lo 网络。
在这里插入图片描述
启动None 模式的命令如下所示。

docker run --net none busybox ifconfig

Bridge 模式

该模式为默认模式,容器启动时会被分配一个单独的 network namespace,同时 Docker 在安装/初始化时会在宿主机上创建一个名为 docker0 的网桥,该网桥也作为容器的默认网关,容器网络会在该网关网段内进行 ip 的分配。

Bridge 模式的启动命令如下所示。

docekr run --net bridge busybox ifconfig

Bridge模式的示意图下图所示。
在这里插入图片描述
并且,当我们执行 docker run -p 3000:80 nginx 命令时,Docker 会在宿主机上创建下面一条 iptable 转发规则。
在这里插入图片描述

在上图中,当外部请求主机网卡 3000 端口时将它进行目的地址转换(DNAT), 目的地址修改为 172.18.0.2,端口修改为 80,修改好目的地址后流量会从本机默认网卡经过 docker0 转发到对应的容器,这样当外部请求宿主机的 3000 端口,内部会将流量转发给内部容器服务,从而实现服务的暴露,流程示意图如下所示。
在这里插入图片描述

同样 Docker 内部访问外部接口也会进行源地址转换(SNAT), 容器内部请求 google.com, 服务器上收到的则是主机网卡的 ip。

Bridge 模式由于多了一层 NAT 转换所以效率会比 Host 模式差一些,但是能够很好的隔离外部网络环境,让容器独享 ip 且具有完整的端口空间。

上面四种网络模式是 Docker 自带的几种工作方式,但是部署 Kubernetes 需要所有的容器都工作在一个局域网中,所以在部署集群时需要多主机网络插件的支持。

Flannel

多主机网络解决方案有 CNCF 推出的 CNI 规范以及 Docker 自带的 CNM 方案,但是目前大家用的最多的还是 CNI 规范,其中一种实现就是 Flannel。

Flannel 使用了报文嵌套技术来解决多主机网络互通问题,将原始报文进行封包,指定包ip为目的主机地址,等包到达主机后再进行拆包传送到对应的容器。Flannel是 CoreOS 团队针对 Kubernetes 设计的一个覆盖网络(Overlay Network)工具,其目的在于帮助每一个使用 Kuberentes 的 CoreOS 主机拥有一个完整的子网。下图显示 flannel 使用效率更高的 UDP 协议来在主机间传输报文。

在这里插入图片描述
对上图,我们简单的说明下:

  1. 数据从源容器中发出后,经由所在主机的docker0虚拟网卡转发到flannel0虚拟网卡,这是个P2P的虚拟网卡,flanneld服务监听在网卡的另外一端。
  2. Flannel通过Etcd服务维护了一张节点间的路由表,该张表里保存了各个节点主机的子网网段信息。
  3. 源主机的flanneld服务将原本的数据内容UDP封装后根据自己的路由表投递给目的节点的flanneld服务,数据到达以后被解包,然后直接进入目的节点的flannel0虚拟网卡,然后被转发到目的主机的docker0虚拟网卡,最后就像本机容器通信一样的由docker0路由到达目标容器。

目前主流跨主机通信目前常用的有三种,分别是Overlay、hostgw和BGP技术。

  • overlay: 即上面的报文嵌套。

hostgw: 通过修改主机路由表实现转发,不需要拆包和封包,效率更高,但同样限制比较多,只适合在相同局域网中的主机使用。
BGP:使用软件实现的 BGP(边界网关协议)以此向网络中的路由器广播路由规则。和 hostgw 一样不需要拆包,但是实现成本较高。

Kubernetes

在小规模场景下,使用 Docker 可以一键部署应用确实很方便,但是当出现需要在几百台主机上进行多副本部署,需要管理这么多主机的运行状态以及服务的故障时需要在其他主机重启服务,想象一下就知道手动的方式不是一种可取的方案,这时候就需要利用 Kubernetes 这种更高维度的编排工具来管理了。

Kubernetes 简称 K8S,是Google 2014年创建管理的大规模容器管理技术。 简单说 K8S 就是抽象了硬件资源,将 N 台物理机或云主机抽象成一个资源池,容器的调度交给 K8S 进行管理,CPU 不够用就调度到一台足够使用的机器上,内存不满足要求就会寻找一台有足够内存的机器在上面创建对应的容器,服务因为某些原因挂了, K8S 还会帮我们自动迁移重启。K8S是一个开源的平台,实现了容器集群的自动化部署、自动扩缩容、维护等功能。

Kubernetes 具有如下特点:

  • 可移植: 支持公有云,私有云,混合云,多重云(multi-cloud)。
  • 可扩展: 模块化, 插件化, 可挂载, 可组合。
  • 自动化: 自动部署,自动重启,自动复制,自动伸缩/扩展。

Kubernetes架构

K8s集群由两节点组成:Master和Node。在Master上运行etcd,Api Server,Controller Manager和Scheduler四个组件。后三个组件构成了K8s的总控中心,负责对集群中所有资源进行管控和调度。在每个Node上运行kubect、proxy和docker daemon三个组件,负责对节点上的Pod的生命周期进行管理,以及实现服务代理的功能。另外所有节点上都可以运行kubectl命令行工具。

API Server作为集群的核心,负责集群各功能模块之间的通信。集群内的功能模块通过Api Server将信息存入到etcd,其他模块通过Api Server读取这些信息,从而实现模块之间的信息交互。Node节点上的Kubelet每隔一个时间周期,通过Api Server报告自身状态,Api Server接收到这些信息后,将节点信息保存到etcd中。Controller Manager中 的Node controller通过Api server定期读取这些节点状态信息,并做响应处理。Scheduler监听到某个Pod创建的信息后,检索所有符合该pod要求的节点列表,并将pod绑定到节点列表中最符合要求的节点上。如果scheduler监听到某个Pod被删除,则调用api server删除该Pod资源对象。kubelet监听pod信息,如果监听到pod对象被删除,则删除本节点上的相应的pod实例,如果监听到修改Pod信息,则会相应地修改本节点的Pod实例。

Kubernetes主要由以下几个核心组件组成:

  • etcd保存了整个集群的状态;
  • apiserver提供了资源操作的唯一入口,并提供认证、授权、访问控制、API注册和发现等机制;
  • controller manager负责维护集群的状态,比如故障检测、自动扩展、滚动更新等;
  • scheduler负责资源的调度,按照预定的调度策略将Pod调度到相应的机器上;
  • kubelet负责本Node节点上的Pod的创建、修改、监控、删除等生命周期管理,同时Kubelet定时“上报”本Node的状态信息到Api Server里;
  • Container runtime负责镜像管理以及Pod和容器的真正运行(CRI);

Kubernetes安装

在Mac中安装了Docker之后,会自动安装了Kubernetes,正常情况下,我们只需要在Docker的Preferrences->Kubernetes中勾选Enable Kubernetes,然后点击Apply按钮即可。
在这里插入图片描述
但是由于【墙】的问题,如果您是直接在Docker中启用Kubernetes,Kubernetes的状态会一直都是kubernetes is starting...,原因是有一些Kubernetes依赖的镜像不能正常的下载。
在这里插入图片描述
Github上有个开源项目可以帮我们手动拉取镜像,执行下面命令拉去改项目代码到本地。

https://github.com/xiangzhihong/k8s-docker-desktop-for-mac
cd k8s-docker-desktop-for-mac
sh load_images.sh

然后,执行下面的命令验证是否安装成功。

kubectl cluster-info 

K8S 与传统 IaaS 系统的不同

IaaS 就是 Infrastructure as a service, 所谓基础设施即服务,开发者想要上线一个新应用需要申请主机,ip, 域名等一系列资源,然后登录主机自行搭建所需环境,部署应用上线,这样不仅不利于大规模操作,而且还增加了出错的可能,运维或开发这常常自己写脚本自动化完成,遇到一些差异再手动修改脚本,非常痛苦。

K8S 则是将基础设施可编程化,由原来的人工申请改为一个清单文件自动创建,开发者只需要提交一份文件,K8S 将会自动为你分配创建所需的资源。对这些设施的 CRUD 都可以通过程序的方式自动化操作。

为了了解 K8S 的基础知识,下面来部署一个 Node SSR 应用。首先,初始化一个应用模版。

npm install create-next-app
npx create-next-app next-app
cd next-app

创建好工程后给添加一个 Dockerfile 用来构建服务的镜像Dockerfile。

FROM node:8.16.1-slim as build

COPY ./ /app

WORKDIR /app
RUN npm install
RUN npm run build
RUN rm -rf .git


FROM node:8.16.1-slim

COPY --from=build /app /

EXPOSE 3000
WORKDIR /app

CMD ["npm", "start"]

Dockerfile 做了两部分优化:

  • 使用精简版的 node 基础镜像, 大大减少镜像体积
  • 使用分步构建的方式, 能够减少镜像层数以及移除临时文件从而减少了镜像体积。

然后使用下面的命令构建镜像。

docker build  . --tag next-app

之后我们就可以向 Kubernetes 提出我们应用的要求了。为了保证高可用,服务至少创建两个副本,我们还需要一个应用的域名当这个域名请求到我们集群上时自动转发到我们的服务上。那么我们对应的配置文件Deployment.yaml就可以这么写。

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: app-ingress
spec:
  rules:
  - host: next-app-server
    http:
      paths:
      - backend:
          serviceName: app-service
          servicePort: 80

---
kind: Service
apiVersion: v1
metadata:
  name: app-service
spec:
  selector:
    app: web
  ports:
  - port: 80
    targetPort: 3000

---

apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-deployment
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
      - image: next-app
        name: next-app
          imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 3000

上面清单主要的功能是:

  • 首先需要一个 Deployment 控制器,镜像为 next-app, 服务端口为 3000,给我创建两个副本。
  • 还需要创建一个 Service, 这个 Service 指向由副本控制器创建的几个 next-app。
  • 申请一个 Ingress 入口, 域名为 next-app-server, 其指向刚刚的 Service。

然后,执行下面的命令提交申请给 K8S。

kubectl apply -f ./Deployment.yaml

接着就可以看到已经部署的 pod。

sh-4.4$ kubectl get pod
NAME                              READY     STATUS    RESTARTS   AGE
app-deployment-594c48dbdb-4f4cg   1/1       Running   0          1m
app-deployment-594c48dbdb-snj54   1/1       Running   0          1m

然后浏览器打开 Ingress 里配置的域名即可访问对应的应用(前提是这个域名能够打到你的 K8S 集群节点上),如下图所示。
在这里插入图片描述

应用发布系统

K8S 仅仅负责容器的编排,实际上如果部署应用还需要外部 Pipeline 的支持,代码的构建,静态检查,镜像的打包由 Pipeline 完成。
在这里插入图片描述
目前,国内用的比较多的发布系统常常由下面几个服务组成:GitLab/GitHub、Jenkins,、Sonar,、Harbor等。

查看原文

CobraJ 收藏了文章 · 2020-11-03

简要分析webpack打包后代码

开门见山

1.打包单一模块

webpack.config.js

module.exports = {
    entry:"./chunk1.js",
    output: {
        path: __dirname + '/dist',
        filename: '[name].js'
    },
};

chunk1.js

var chunk1=1;
exports.chunk1=chunk1;

打包后,main.js(webpack生成的一些注释已经去掉)

 (function(modules) { // webpackBootstrap
     // The module cache
     var installedModules = {};
     // 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] = {
             exports: {},
             id: moduleId,
             loaded: false
         };
         // Execute the module function
         modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
         // Flag the module as loaded
         module.loaded = true;
         // Return the exports of the module
         return module.exports;
     }


     // expose the modules object (__webpack_modules__)
     __webpack_require__.m = modules;
     // expose the module cache
     __webpack_require__.c = installedModules;
     // __webpack_public_path__
     __webpack_require__.p = "";
     // Load entry module and return exports
     return __webpack_require__(0);
 })([function(module, exports) {
    var chunk1=1;
    exports.chunk1=chunk1;
}]);

这其实就是一个立即执行函数,简化一下就是:

(function(module){})([function(){},function(){}]);

OK,看一下自运行的匿名函数里面干了什么:

function(modules) { // webpackBootstrap
     // modules就是一个数组,元素就是一个个函数体,就是我们声明的模块
     var installedModules = {};
     // The require function
     function __webpack_require__(moduleId) {
         ...
     }
     // expose the modules object (__webpack_modules__)
     __webpack_require__.m = modules;
     // expose the module cache
     __webpack_require__.c = installedModules;
     // __webpack_public_path__
     __webpack_require__.p = "";
     // Load entry module and return exports
     return __webpack_require__(0);
 }

整个函数里就声明了一个变量installedModules 和函数__webpack_require__,并在函数上添加了一个m,c,p属性,m属性保存的是传入的模块数组,c属性保存的是installedModules变量,P是一个空字符串。最后执行__webpack_require__函数,参数为零,并将其执行结果返回。下面看一下__webpack_require__干了什么:

function __webpack_require__(moduleId) {
        //moduleId就是调用是传入的0
         // installedModules[0]是undefined,继续往下
         if(installedModules[moduleId])
             return installedModules[moduleId].exports;
         // module就是{exports: {},id: 0,loaded: false}
         var module = installedModules[moduleId] = {
             exports: {},
             id: moduleId,
             loaded: false
         };
         // 下面接着分析这个
         modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
         // 表明模块已经载入
         module.loaded = true;
         // 返回module.exports(注意modules[moduleId].call的时候module.exports会被修改)
         return module.exports;
     }

接着看一下modules[moduleId].call(module.exports, module, module.exports, __webpack_require__),其实就是

modules[moduleId].call({}, module, module.exports, __webpack_require__)

对call不了解当然也可以认为是这样(但是并不是等价,call能确保当模块中使用this的时候,this是指向module.exports的):

function  a(module, exports) {
    var chunk1=1;
    exports.chunk1=chunk1;
}
a(module, exports,__webpack_require__);

传入的module就是{exports: {},id: 0,loaded: false},exports就是{},__webpack_require__就是声明的__webpack_require__函数(传入这个函数有什么用呢,第二节将会介绍);
运行后module.exports就是{chunk1:1}。所以当我们使用chunk1这个模块的时候(比如var chunk1=require("chunk1"),得到的就是一个对象{chunk1:1})。如果模块里没有exports.chunk1=chunk1或者module.exports=chunk1得到的就是一个空对象{}

2.使用模块

上面我们已经分析了webpack是怎么打包一个模块的(入口文件就是一个模块),现在我们来看一下使用一个模块,然后使用模块的文件作为入口文件
webpack.config.js

module.exports = {
    entry:"./main.js",
    output: {
        path: __dirname + '/dist',
        filename: '[name].js'
    }
};

main.js

var chunk1=require("./chunk1");
console.log(chunk1);

打包后

(function (modules) { // webpackBootstrap
    // The module cache
    var installedModules = {};
    // 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] = {
            exports: {},
            id: moduleId,
            loaded: false
        };
        // Execute the module function
        modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
        // Flag the module as loaded
        module.loaded = true;
        // Return the exports of the module
        return module.exports;
    }
    // expose the modules object (__webpack_modules__)
    __webpack_require__.m = modules;
    // expose the module cache
    __webpack_require__.c = installedModules;
    // __webpack_public_path__
    __webpack_require__.p = "";
    // Load entry module and return exports
    return __webpack_require__(0);
})([function (module, exports, __webpack_require__) {
    var chunk1=__webpack_require__(1);
    console.log(chunk1);
}, function (module, exports) {
    var chunk1 = 1;
    exports.chunk1 = chunk1;
}]);

不一样的地方就是自执行函数的参数由

[function(module, exports) { var chunk1=1; exports.chunk1=chunk1;}]

变为

[function (module, exports, __webpack_require__) {
    var chunk1=__webpack_require__(1);
    console.log(chunk1);
}, function (module, exports) {
    var chunk1 = 1;
    exports.chunk1 = chunk1;
}]

其实就是多了一个main模块,不过这个模块没有导出项,而且这个模块依赖于chunk1模块。所以当运行__webpack_require__(0)的时候,main模块缓存到installedModules[0]上,modules[0].call(也就是调用main模块)的时候,chunk1被缓存到installedModules[1]上,并且导出对象{chunk1:1}给模块main使用

3.重复使用模块

webpack.config.js

module.exports = {
    entry:"./main.js",
    output: {
        path: __dirname + '/dist',
        filename: '[name].js'
    }
};

main.js

var chunk1=require("./chunk1");
var chunk2=require(".chunlk2");
console.log(chunk1);
console.log(chunk2);

chunk1.js

var chunk2=require("./chunk2");
var chunk1=1;
exports.chunk1=chunk1;

chunk2.js

var chunk2=1;
exports.chunk2=chunk2;

打包后

(function (modules) { // webpackBootstrap
    // The module cache
    var installedModules = {};
    // 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] = {
            exports: {},
            id: moduleId,
            loaded: false
        };
        // Execute the module function
        modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
        // Flag the module as loaded
        module.loaded = true;
        // Return the exports of the module
        return module.exports;
    }
    // expose the modules object (__webpack_modules__)
    __webpack_require__.m = modules;
    // expose the module cache
    __webpack_require__.c = installedModules;
    // __webpack_public_path__
    __webpack_require__.p = "";
    // Load entry module and return exports
    return __webpack_require__(0);
})([function (module, exports, __webpack_require__) {

    var chunk1 = __webpack_require__(1);
    var chunk2 = __webpack_require__(2);
    console.log(chunk1);
    console.log(chunk2);
}, function (module, exports, __webpack_require__) {

    __webpack_require__(2);
    var chunk1 = 1;
    exports.chunk1 = chunk1;
}, function (module, exports) {

    var chunk2 = 1;
    exports.chunk2 = chunk2;
}]);

不难发现,当需要重复使用模块的时候,缓存变量installedModules 就起作用了

4.多个打包入口

不管是单一模块还是重复模块,和以上两种一样

5.入口参数为数组

webpack.config.js

module.exports = {
    entry:['./main.js','./main1.js'],
    output: {
        path: __dirname + '/dist',
        filename: '[name].js'
    }
};

打包后

[
/* 0 */
/***/ function(module, exports, __webpack_require__) {
    __webpack_require__(1);
    module.exports = __webpack_require__(3);
/***/ },
/* 1 */
/***/ function(module, exports, __webpack_require__) {
    var chunk1=__webpack_require__(2);
    console.log(chunk1);
/***/ },
/* 2 */
/***/ function(module, exports) {
    var chunk1=1;
    exports.chunk1=chunk1;
/***/ },
/* 3 */
/***/ function(module, exports, __webpack_require__) {
    var chunk1=__webpack_require__(2);
/***/ }
/******/ ]

这里只截取自执行匿名函数的参数,因为其他代码与之前一样。可以看到1就是main默模块,2就是chunk1模块,3就是mian1模块,0的作用就是运行模块mian,mian1,然后将main1模块导出(main1中没有导出项,所以到导出{}),总结一下:入口参数是字符串不管是多入口还是单入口,最后都会将入口模块的导出项导出,没有导出项就导出{},而入口参数是数组,就会将最后一个模块导出(webpackg官网有说明)

6.使用CommonsChunkPlugin插件

webpack.config.js

var CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin");
module.exports = {
    entry: {
        main: './main.js',
        main1: './main1.js',
    },
    output: {
        path: __dirname + '/dist',
        filename: '[name].js'
    },
    plugins: [
        new CommonsChunkPlugin({
        name: "common"
        })
    ]
};

main mian1中都require了chunk1,所以chunk1会被打包到common。
打包后,common.js

(function (modules) { // webpackBootstrap
    // install a JSONP callback for chunk loading
    var parentJsonpFunction = window["webpackJsonp"];
    window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules) {
        // add "moreModules" to the modules object,
        // then flag all "chunkIds" as loaded and fire callback
        var moduleId, chunkId, i = 0, callbacks = [];
        for (; i < chunkIds.length; i++) {
            chunkId = chunkIds[i];
            if (installedChunks[chunkId])
                callbacks.push.apply(callbacks, installedChunks[chunkId]);
            installedChunks[chunkId] = 0;
        }
        for (moduleId in moreModules) {
            modules[moduleId] = moreModules[moduleId];
        }
        if (parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules);
        while (callbacks.length)
            callbacks.shift().call(null, __webpack_require__);
        if (moreModules[0]) {
            installedModules[0] = 0;
            return __webpack_require__(0);
        }
    };
    // The module cache
    var installedModules = {};
    // object to store loaded and loading chunks
    // "0" means "already loaded"
    // Array means "loading", array contains callbacks
    var installedChunks = {
        2: 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] = {
            exports: {},
            id: moduleId,
            loaded: false
        };
        // Execute the module function
        modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
        // Flag the module as loaded
        module.loaded = true;
        // Return the exports of the module
        return module.exports;
    }
    // This file contains only the entry chunk.
    // The chunk loading function for additional chunks
    __webpack_require__.e = function requireEnsure(chunkId, callback) {
        // "0" is the signal for "already loaded"
        if (installedChunks[chunkId] === 0)
            return callback.call(null, __webpack_require__);
        // an array means "currently loading".
        if (installedChunks[chunkId] !== undefined) {
            installedChunks[chunkId].push(callback);
        } else {
            // start chunk loading
            installedChunks[chunkId] = [callback];
            var head = document.getElementsByTagName('head')[0];
            var script = document.createElement('script');
            script.type = 'text/javascript';
            script.charset = 'utf-8';
            script.async = true;
            script.src = __webpack_require__.p + "" + chunkId + "." + ({ "0": "main", "1": "main1" }[chunkId] || chunkId) + ".js";
            head.appendChild(script);
        }
    };
    // expose the modules object (__webpack_modules__)
    __webpack_require__.m = modules;
    // expose the module cache
    __webpack_require__.c = installedModules;
    // __webpack_public_path__
    __webpack_require__.p = "";
})([, function (module, exports) {

    var chunk1 = 1;
    exports.chunk1 = chunk1;

}]);

main.js

webpackJsonp([0],[function(module, exports, __webpack_require__) {

    var chunk1=__webpack_require__(1);
    console.log(chunk1);
 }]);

main1.js

webpackJsonp([1],[function(module, exports, __webpack_require__) {
    var chunk1=__webpack_require__(1);
    console.log(chunk1);
}]);

与之前相比,多了webpackJsonp函数,立即执行的匿名函数没有立即调用__webpack_require__(0)。看一下webpackJsonp:

var parentJsonpFunction = window["webpackJsonp"];
    window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules) {
        //moreModules为独立chunk代码,chunkIds标记独立chunk唯一性避免按需加载时重复加载
        //以main.js中代码为例,chunkIds为[0],moreModules为
        //[function(module, exports, __webpack_require__) {
        //    var chunk1=__webpack_require__(1);
        //    console.log(chunk1);
        //}]
        var moduleId, chunkId, i = 0, callbacks = [];
        for (; i < chunkIds.length; i++) {
            chunkId = chunkIds[i];//chunkId=0
            if (installedChunks[chunkId])
                callbacks.push.apply(callbacks,installedChunks[chunkId]);//0 push入callbacks(使用requireEnsure不再是0)
            //赋值为0表明chunk已经loaded
            installedChunks[chunkId] = 0;
        }
        for (moduleId in moreModules) {
            //modules[0]会被覆盖
            modules[moduleId] = moreModules[moduleId];
        }
        //按当前情况parentJsonpFunction一直未undefined
        if (parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules);
        //按当前情况callbacks=[]
        while (callbacks.length)
            callbacks.shift().call(null, __webpack_require__);
        if (moreModules[0]) {
            installedModules[0] = 0;
            return __webpack_require__(0);
        }
    };
    // 缓存模块,通过闭包引用(window["webpackJsonp"]可以访问到)
    var installedModules = {};
    //2为公共chunck唯一ID,0表示已经loaded
    var installedChunks = {
        2: 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] = {
            exports: {},
            id: moduleId,
            loaded: false
        };
        // Execute the module function
        modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
        // Flag the module as loaded
        module.loaded = true;
        // Return the exports of the module
        return module.exports;
    }
    //按需加载
    __webpack_require__.e = function requireEnsure(chunkId, callback) {
        // "0" is the signal for "already loaded"
        if (installedChunks[chunkId] === 0)
            return callback.call(null, __webpack_require__);
        // an array means "currently loading".
        if (installedChunks[chunkId] !== undefined) {
            installedChunks[chunkId].push(callback);
        } else {
            // start chunk loading
            installedChunks[chunkId] = [callback];
            var head = document.getElementsByTagName('head')[0];
            var script = document.createElement('script');
            script.type = 'text/javascript';
            script.charset = 'utf-8';
            script.async = true;
            script.src = __webpack_require__.p + "" + chunkId + "." + ({ "0": "main", "1": "main1" }[chunkId] || chunkId) + ".js";
            head.appendChild(script);
        }
    };

好像看不出什么。。。,修改一下
webpack.config.js

var CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin");
module.exports = {
    entry: {
        main: './main.js',
        main1: './main1.js',
        chunk1:["./chunk1"]
    },
    output: {
        path: __dirname + '/dist2',
        filename: '[name].js'
    },
    plugins: [
        new CommonsChunkPlugin({
        name: ["chunk1"],
        filename:"common.js",
        minChunks:3,
        })
    ]
};

main,main1都分别require chunk1,chunk2,然后将chunk1打包到公共模块(minChunks:3,chunk2不会被打包到公共模块)。自运行匿名函数最后多了

   return __webpack_require__(0);

则installedModules[0]为已经loaded,看common.js,installedModules[1]也会loaded。
main.js

webpackJsonp([1], [function (module, exports, __webpack_require__) {

    var chunk1 = __webpack_require__(1);
    var chunk2 = __webpack_require__(2);
    exports.a = 1;
    console.log(chunk1);
}, , function (module, exports) {
    var chunk2 = 1;
    exports.chunk2 = chunk2;

}
]);

main1.js

webpackJsonp([2], [function (module, exports, __webpack_require__) {

    var chunk1 = __webpack_require__(1);
    var chunk2 = __webpack_require__(2);
    exports.a = 1;
    console.log(chunk1);
}, , function (module, exports) {
    var chunk2 = 1;
    exports.chunk2 = chunk2;
}
]);

common.js modules:

[function (module, exports, __webpack_require__) {

    module.exports = __webpack_require__(1);
}, function (module, exports) {

    var chunk1 = 1;
    exports.chunk1 = chunk1;
}]

以main.js的代码为例,调用webpackJsonp,传入的参数chunkIds为[1],moreModules为

[function (module, exports, __webpack_require__) {

    var chunk1 = __webpack_require__(1);
    var chunk2 = __webpack_require__(2);
    exports.a = 1;
    console.log(chunk1);
}, , function (module, exports) {
    var chunk2 = 1;
    exports.chunk2 = chunk2;

}]
var moduleId, chunkId, i = 0, callbacks = [];
        for (; i < chunkIds.length; i++) {
            chunkId = chunkIds[i];//1
            //false,赋值为0后还是false
            if (installedChunks[chunkId])
                callbacks.push.apply(callbacks, installedChunks[chunkId]);
            installedChunks[chunkId] = 0;
        }
        //三个模块
        for (moduleId in moreModules) {
            //moduleId:0,1,2  moreModules[1]为空模块,自执行函数的参数(公共模块)会被覆盖,但是参数中的相应模块已经loaded并且缓存
            modules[moduleId] = moreModules[moduleId];
        }
        if (parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules);
        while (callbacks.length)
            callbacks.shift().call(null, __webpack_require__);
        if (moreModules[0]) {
            //installedModules[0]会重新load,但是load的是moreModules[0],因为modules[0]已经被覆盖,moreModules[0]依赖于
            //modules[1]、modules[2],modules[1]已经loaded
            installedModules[0] = 0;
            return __webpack_require__(0);
        }

再看下面的情况:
common.js 自执行函数参数(公共模块)(没有return __webpack_require__(0))

[,function(module, exports, __webpack_require__) {

    var chunk1=1;
    var chunk2=__webpack_require__(2);
    exports.chunk1=chunk1;
},function(module, exports) {

    var chunk2=1;
    exports.chunk2=chunk2;
}]

main.js

webpackJsonp([0],[
/* 0 */
/***/ function(module, exports, __webpack_require__) {

    var chunk1=__webpack_require__(1);
    var chunk2=__webpack_require__(2);
    exports.a=1;
    console.log(chunk1);
    //main
/***/ }
]);

以main调用分析

         var moduleId, chunkId, i = 0, callbacks = [];
         for(;i < chunkIds.length; i++) {
             chunkId = chunkIds[i];//0
             if(installedChunks[chunkId])
                 callbacks.push.apply(callbacks, installedChunks[chunkId]);
             installedChunks[chunkId] = 0;//表明唯一索引为0的chunk已经loaded
         }
         for(moduleId in moreModules) {
            //moreModules只有一个元素,所以modules[1]、modules[2]不会被覆盖
             modules[moduleId] = moreModules[moduleId];
         }
         if(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules);
         while(callbacks.length)
             callbacks.shift().call(null, __webpack_require__);
         if(moreModules[0]) {
             installedModules[0] = 0;
            //moreModules[0]即modules[0]依赖modules[1]、即modules[2](没有被覆盖很关键)
             return __webpack_require__(0);
         }

还有这种打包情况:
common.js不包含公共模块,即自执行函数参数为[]。
main.js

webpackJsonp([0,1],[
function(module, exports, __webpack_require__) {

    var chunk1=__webpack_require__(1);
    var chunk2=__webpack_require__(2);
    exports.a=1;
    console.log(chunk1);
},function(module, exports) {
    var chunk1=1;
    exports.chunk1=chunk1;
},function(module, exports) {
    var chunk2=1;
    exports.chunk2=chunk2;
}]);

以main调用分析

     var moduleId, chunkId, i = 0, callbacks = [];
         for(;i < chunkIds.length; i++) {
             chunkId = chunkIds[i];//0,1
             if(installedChunks[chunkId])
                 callbacks.push.apply(callbacks, installedChunks[chunkId]);
             installedChunks[chunkId] = 0;
         }
         for(moduleId in moreModules) {
            //moreModules全部转移到modules
             modules[moduleId] = moreModules[moduleId];
         }
         if(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules);
         while(callbacks.length)
             callbacks.shift().call(null, __webpack_require__);
         if(moreModules[0]) {
            //modules[0]是chunk文件运行代码
             installedModules[0] = 0;
             return __webpack_require__(0);
         }

7.按需加载

webpack.config.json

module.exports = {
  entry: './main.js',
  output: {
    filename: 'bundle.js'
  }
};

main.js

require.ensure(['./a'], function(require) {
  var content = require('./a');
  document.open();
  document.write('<h1>' + content + '</h1>');
  document.close();
});

a.js

module.exports = 'Hello World';

打包后

bundle.js

/******/ (function(modules) { // webpackBootstrap
/******/     // install a JSONP callback for chunk loading
/******/     var parentJsonpFunction = window["webpackJsonp"];
/******/     window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules) {
/******/         // add "moreModules" to the modules object,
/******/         // then flag all "chunkIds" as loaded and fire callback
/******/         var moduleId, chunkId, i = 0, callbacks = [];
/******/         for(;i < chunkIds.length; i++) {
/******/             chunkId = chunkIds[i];
/******/             if(installedChunks[chunkId])
/******/                 callbacks.push.apply(callbacks, installedChunks[chunkId]);
/******/             installedChunks[chunkId] = 0;
/******/         }
/******/         for(moduleId in moreModules) {
/******/             modules[moduleId] = moreModules[moduleId];
/******/         }
/******/         if(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules);
/******/         while(callbacks.length)
/******/             callbacks.shift().call(null, __webpack_require__);

/******/     };

/******/     // The module cache
/******/     var installedModules = {};

/******/     // object to store loaded and loading chunks
/******/     // "0" means "already loaded"
/******/     // Array means "loading", array contains callbacks
/******/     var installedChunks = {
/******/         0: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] = {
/******/             exports: {},
/******/             id: moduleId,
/******/             loaded: false
/******/         };

/******/         // Execute the module function
/******/         modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

/******/         // Flag the module as loaded
/******/         module.loaded = true;

/******/         // Return the exports of the module
/******/         return module.exports;
/******/     }

/******/     // This file contains only the entry chunk.
/******/     // The chunk loading function for additional chunks
/******/     __webpack_require__.e = function requireEnsure(chunkId, callback) {
/******/         // "0" is the signal for "already loaded"
/******/         if(installedChunks[chunkId] === 0)
/******/             return callback.call(null, __webpack_require__);

/******/         // an array means "currently loading".
/******/         if(installedChunks[chunkId] !== undefined) {
/******/             installedChunks[chunkId].push(callback);
/******/         } else {
/******/             // start chunk loading
/******/             installedChunks[chunkId] = [callback];
/******/             var head = document.getElementsByTagName('head')[0];
/******/             var script = document.createElement('script');
/******/             script.type = 'text/javascript';
/******/             script.charset = 'utf-8';
/******/             script.async = true;

/******/             script.src = __webpack_require__.p + "" + chunkId + ".bundle.js";
/******/             head.appendChild(script);
/******/         }
/******/     };

/******/     // expose the modules object (__webpack_modules__)
/******/     __webpack_require__.m = modules;

/******/     // expose the module cache
/******/     __webpack_require__.c = installedModules;

/******/     // __webpack_public_path__
/******/     __webpack_require__.p = "";

/******/     // Load entry module and return exports
/******/     return __webpack_require__(0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ function(module, exports, __webpack_require__) {

    __webpack_require__.e/* nsure */(1, function(require) {
      var content = __webpack_require__(1);
      document.open();
      document.write('<h1>' + content + '</h1>');
      document.close();
    });


/***/ }
/******/ ]);

1.bundle.js

webpackJsonp([1],[
/* 0 */,
/* 1 */
/***/ function(module, exports) {

    module.exports = 'Hello World';


/***/ }
]);

和使用CommonsChunkPlugin打包的差异在于

/******/     // This file contains only the entry chunk.
/******/     // The chunk loading function for additional chunks
/******/     __webpack_require__.e = function requireEnsure(chunkId, callback) {
/******/         // "0" is the signal for "already loaded"
/******/         if(installedChunks[chunkId] === 0)
/******/             return callback.call(null, __webpack_require__);

/******/         // an array means "currently loading".
/******/         if(installedChunks[chunkId] !== undefined) {
/******/             installedChunks[chunkId].push(callback);
/******/         } else {
/******/             // start chunk loading
/******/             installedChunks[chunkId] = [callback];
/******/             var head = document.getElementsByTagName('head')[0];
/******/             var script = document.createElement('script');
/******/             script.type = 'text/javascript';
/******/             script.charset = 'utf-8';
/******/             script.async = true;

/******/             script.src = __webpack_require__.p + "" + chunkId + ".bundle.js";
/******/             head.appendChild(script);
/******/         }
/******/     };

模块main的id为0,模块a的id为1。return __webpack_require__(0),则加载main模块,
modules[0].call(module.exports, module, module.exports, __webpack_require__)则调用函数

function(module, exports, __webpack_require__) {

    __webpack_require__.e/* nsure */(1, function(require) {
      var content = __webpack_require__(1);
      document.open();
      document.write('<h1>' + content + '</h1>');
      document.close();
    }
/******/     // This file contains only the entry chunk.
/******/     // The chunk loading function for additional chunks
/******/     __webpack_require__.e = function requireEnsure(chunkId, callback) {
                //installedChunks[1]为undefined
/******/         // "0" is the signal for "already loaded"
/******/         if(installedChunks[chunkId] === 0)
/******/             return callback.call(null, __webpack_require__);

/******/         // an array means "currently loading".
/******/         if(installedChunks[chunkId] !== undefined) {
/******/             installedChunks[chunkId].push(callback);
/******/         } else {
/******/             // start chunk loading
/******/             installedChunks[chunkId] = [callback];//installedChunks[1]为数组,表明currently loading
/******/             var head = document.getElementsByTagName('head')[0];
/******/             var script = document.createElement('script');
/******/             script.type = 'text/javascript';
/******/             script.charset = 'utf-8';
/******/             script.async = true;

/******/             script.src = __webpack_require__.p + "" + chunkId + ".bundle.js";
/******/             head.appendChild(script);
                    //加载完后直接调用
                    /******/webpackJsonp([1],[
                    /******//* 0 */,
                    /******//* 1 */
                    /******//***/ function(module, exports) {
                    /******/
                    /******/    module.exports = 'Hello World';
                    /******/
                    /******/
                    /******//***/ }
                    /******/]);
                    /******/         }
                    /******/     };
                    //installedChunks[1]在webpackJsonp得到调用

installedChunks[1]为数组,元素为main模块的执行代码

/******/     window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules) {
                //moreModules为模块a的代码
/******/         // add "moreModules" to the modules object,
/******/         // then flag all "chunkIds" as loaded and fire callback
/******/         var moduleId, chunkId, i = 0, callbacks = [];
/******/         for(;i < chunkIds.length; i++) {
/******/             chunkId = chunkIds[i];
/******/             if(installedChunks[chunkId])//installedChunks[0]==0,installedChunks[1]为数组
/******/                 callbacks.push.apply(callbacks, installedChunks[chunkId]);//callbacks为模块main执行代码,不为数组
/******/             installedChunks[chunkId] = 0;//installedChunks[1]不为数组,表明已经加载
/******/         }
/******/         for(moduleId in moreModules) {
/******/             modules[moduleId] = moreModules[moduleId];
/******/         }
/******/         if(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules);
/******/         while(callbacks.length)
/******/             callbacks.shift().call(null, __webpack_require__);

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

认证与成就

  • 获得 27 次点赞
  • 获得 17 枚徽章 获得 1 枚金徽章, 获得 3 枚银徽章, 获得 13 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-08-18
个人主页被 885 人浏览