itv_工程化(ts,wepack,babel,vite等)

698 阅读30分钟

微前端

Micro App 是京东出的一款基于 Web Component 原生组件进行渲染的微前端框架,不同于目前流行的开源框架,它从组件化的思维实现微前端,旨在降低上手难度、提升工作效率

micro-app将所有功能都封装到一个类WebComponent组件中,从而实现在基座应用中嵌入一行代码即可渲染一个微前端应用。 同时micro-app还提供了js沙箱样式隔离元素隔离预加载数据通信静态资源补全插件系统一系列完善的功能

image.png

micro优势
  • 完整的沙箱机制和样式隔离
  • 支持多个框架,包括vitenuxtnext
  • qiankun坑太多,网上看了一圈文章,全是踩坑的
  • 完整的通信系统,包括主应用和基座应用互相通信,全局通信等
  • 对现有项目改动很小,不需要改造入口文件和导出特定的生命周期,项目侵入性基本等于没有
micro思路

micro-app之前,业内已经有一些开源的微前端框架,比较流行的有2个:single-spaqiankun single-spa是通过监听 url change 事件,在路由变化时匹配到渲染的子应用并进行渲染,这个思路也是目前实现微前端的主流方式。同时single-spa要求子应用修改渲染逻辑并暴露出三个方法:bootstrapmountunmount,分别对应初始化、渲染和卸载,这也导致子应用需要对入口文件进行修改。因为qiankun是基于single-spa进行封装,所以这些特点也被qiankun继承下来,并且需要对webpack配置进行一些修改

micro-app并没有沿袭single-spa的思路,而是借鉴了WebComponent的思想,通过CustomElement结合自定义的ShadowDom,将微前端封装成一个类WebComponent组件,从而实现微前端的组件化渲染。并且由于自定义ShadowDom的隔离特性,micro-app不需要像single-spaqiankun一样要求子应用修改渲染逻辑并暴露出方法,也不需要修改webpack配置,是目前市面上接入微前端成本最低的方案

核心原理

MicroApp 的核心功能在CustomElement基础上进行构建,CustomElement用于创建自定义标签,并提供了元素的渲染、卸载、属性修改等钩子函数,我们通过钩子函数获知微应用的渲染时机,并将自定义标签作为容器,微应用的所有元素和样式作用域都无法逃离容器边界,从而形成一个封闭的环境。

更多

WebComponents

Web Components 是一套技术,允许创建可重用的自定义元素

Web Components 旨在解决这些问题 — 它由三项主要技术组成,它们可以一起使用来创建封装功能的定制元素,可以在你喜欢的任何地方重用,不必担心代码冲突

  • Custom elements(自定义元素) :一组 JavaScript API,允许您定义 custom elements 及其行为,然后可以在您的用户界面中按照需要使用它们
  • Shadow DOM(影子 DOM) :一组 JavaScript API,用于将封装的“影子”DOM 树附加到元素(与主文档 DOM 分开呈现)并控制其关联的功能。通过这种方式,您可以保持元素的功能私有,这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突
  • HTML templates(HTML 模板):  <template> 和 <slot> 元素使您可以编写不在呈现页面中显示的标记模板。然后它们可以作为自定义元素结构的基础被多次重用

实现 Web Component 的基本方法通常如下所示:

  1. 创建一个类或函数来指定 web 组件的功能,如果使用类
  2. 使用 CustomElementRegistry.define() 方法注册您的新自定义元素,并向其传递要定义的元素名称、指定元素功能的类、以及可选的其所继承自的元素。
  3. 如果需要的话,使用Element.attachShadow() 方法将一个 shadow DOM 附加到自定义元素上。使用通常的 DOM 方法向 shadow DOM 中添加子元素、事件监听器等等。
  4. 如果需要的话,使用 <template> 和<slot> 定义一个 HTML 模板。再次使用常规 DOM 方法克隆模板并将其附加到您的 shadow DOM 中。
  5. 在页面任何您喜欢的位置使用自定义元素,就像使用常规 HTML 元素那样
customElement

customElements.define(name, constructor, options);
name: 自定义元素名称
constructor: 定义元素行为的类
options: 其他参数....

Custom elements 可自定义渲染的 html 元素,并提供了组件的生命周期 connectedCallbackdisconnectCallbackattributeChangedCallback 等提供给开发者聚合逻辑时使用

shadow Dom

Shadow DOM,它可以将一个隐藏的、独立的 DOM 附加到一个元素(custom element)上。shadow root以根节点为起始节点,在这个根节点的下方,可以是任意元素。同时OM 组件标签内的 CSS 和 HTML 会完全的隐式存在于元素内部,在具体页面中,标签内部的HTML结构会存在于 #shdaow-root,而不会在真实的 dom 树中出现。

template

html templates 利用 template 进行元素包裹,包裹的元素不会立即渲染,只有在内容有效的时候,才会解析渲染,具有这个属性后,我们可以在自定义标签中按需添加我们需要的模板,并在自定义标签渲染的时候再去解析我们的模板,这样做可以在 HTML 有频繁更改更新任务,或者重写标记的时候非常有用

可自定义slot插槽

让我们看一个简单的示例:

<template id="my-paragraph">
  <p>My paragraph</p>
</template>

上面的代码不会展示在你的页面中,直到你用 JavaScript 获取它的引用,然后添加到 DOM 中,如下面的代码:

let template = document.getElementById('my-paragraph');
let templateContent = template.content;
document.body.appendChild(templateContent);
案例

在构造函数中,我们会定义元素实例所拥有的全部功能。作为示例,我们首先会将 shadow root 附加到 custom element 上,然后通过一系列 DOM 操作创建 custom element 的内部阴影 DOM(shadow DOM)结构,再将其附加到 shadow root 上。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <my-button>11</my-button>
  
  <template id="mybutton">

    <style>
      button {
        color: white;
        background-color: red;
        padding: 5px;
      }
    </style>

    <button id="btn">点击改变背景颜色</button>
  </template>

  <script>
    class MyButton extends HTMLElement {
      constructor (name) {
          super();
        
          // 创建一个 shadow root
          const shadow = this.attachShadow({mode: 'open'});

          // 把template添加到shadow dom中
          const template = document.getElementById('mybutton');

          const content = template.content.cloneNode(true);
          const btn = content.getElementById('btn');

          btn.addEventListener('click', () => {
            btn.style.backgroundColor = 'blue';
          })
          shadow.appendChild(content);
      }
      // 生命周期
      // connectedCallback:当 custom element首次被插入文档DOM时,被调用。
      // disconnectedCallback:当 custom element从文档DOM中删除时,被调用。
      // adoptedCallback:当 custom element被移动到新的文档时,被调用。
      // attributeChangedCallback: 当 custom element增加、删除、修改自身属性时,被调用。

        connectedCallback() {
          console.log('Custom square element added to page.');
          this.updateStyle();
        }

        disconnectedCallback() {
          console.log('Custom square element removed from page.');
        }
          
        adoptedCallback() {
          console.log('Custom square element moved to new page.');
        }
          
        attributeChangedCallback(name, oldValue, newValue) {
          console.log('Custom square element attributes changed.');
          this.updateStyle();
        }

        updateStyle() {
          console.log('update style');
        }
      }

    // 自定义元素  自定义元素内部创建shadow dom
    window.customElements.define('my-button', MyButton);

    const btn = document.getElementById('btn');
    console.log('out:', btn)
  </script>
</body>
</html>
WenComponent劣势
  • 未实现跨窗口/框架访问
  • 虽然能实现 CSS 的隔离,但是隔离也会导致和外部 CSS 交互比较难。
更多

为什么选vite

基于esbuild与Rollup,依靠浏览器自身ESM编译功能, 实现极致开发体验的新一代构建工具!

webpack会先打包,然后启动开发服务器,请求服务器时直接给予打包结果。 而vite是直接启动开发服务器,请求哪个模块再对该模块进行实时编译。 由于现代浏览器本身就支持ES Module,会自动向依赖的Module发出请求。vite充分利用这一点,将开发环境下的模块文件,就作为浏览器要执行的文件,而不是像webpack那样进行打包合并。 由于vite在启动的时候不需要打包,也就意味着不需要分析模块的依赖、不需要编译,因此启动速度非常快。当浏览器请求某个模块时,再根据需要对模块内容进行编译。这种按需动态编译的方式,极大的缩减了编译时间,项目越复杂、模块越多,vite的优势越明显。 在HMR方面,当改动了一个模块后,仅需让浏览器重新请求该模块即可,不像webpack那样需要把该模块的相关依赖模块全部编译一次,效率更高。 当需要打包到生产环境时,vite使用传统的rollup进行打包,因此,vite的主要优势在开发阶段<利用esbuild编译、以及预构建>

当前工程化痛点

现在常用的构建工具如Webpack,主要是通过抓取-编译-构建整个应用的代码(也就是常说的打包过程),生成一份编译、优化后能良好兼容各个浏览器的的生产环境代码。在开发环境流程也基本相同,需要先将整个应用构建打包后,再把打包后的代码交给dev server(开发服务器)。

Webpack等构建工具的诞生给前端开发带来了极大的便利,但随着前端业务的复杂化,js代码量呈指数增长,打包构建时间越来越久,dev server(开发服务器)性能遇到瓶颈

  • 缓慢的服务启动:  大型项目中dev server启动时间达到几十秒甚至几分钟。
  • 缓慢的HMR热更新:  即使采用了 HMR 模式,其热更新速度也会随着应用规模的增长而显著下降,已达到性能瓶颈,无多少优化空间。
为什么生产环境用rollup

尽管原生 ESM 现在得到了广泛支持,但由于嵌套导入会导致额外的网络往返,在生产环境中发布未打包的 ESM 仍然效率低下(即使使用 HTTP/2)。为了在生产环境中获得最佳的加载性能,最好还是将代码进行 tree-shaking、懒加载和 chunk 分割(以获得更好的缓存)

esbuild优势
  • 在 Vite 中,HMR 是在原生 ESM 上执行的。当编辑一个文件时,Vite 只需要精确地使已编辑的模块与其最近的 HMR 边界之间的链失活[1](大多数时候只是模块本身),使得无论应用大小如何,HMR 始终能保持快速更新。Vite 同时利用 HTTP 头来加速整个页面的重新加载(再次让浏览器为我们做更多事情):源码模块的请求会根据 304 Not Modified 进行协商缓存,而依赖模块请求则会通过 Cache-Control: max-age=31536000,immutable 进行强缓存,因此一旦被缓存它们将不需要再次请求
  • 使用esbuild预构建第三方依赖库
esbuild在vite中有什么作用
  • 《预构建》开发环境第三方依赖包
  • esbuild用再开发环境,解析依赖,编译不同格式文件成可执行esm的js,rollup用于生产环境,也需要解析编译,但是产物是兼容性更好的原生js
esbuild优点
  • 不适用AST,优化了构建流程
  • esbuild使用go(机器码)编译型语言编写,比起其他使用node.js(解释性)写的构建工具,编译速度快几个数量级
  • js是单线程,esbuild则是单独开启一个进程,使用多线程并行。充分发挥了多核优势
esbuild 缺点
  • 不支持模块热更新
  • 不支持typescript类型检查,构建时候单独使用tsc检测
  • 没有提供操作AST能力,像一些通过操作AST插件(babel-plugin-import)没有很好的迁移方案至esbuild
应用于webpack
  • esbuildPlugin压缩js文件
  • esbuild-loader 翻译js,ts文件
vite 对比 webpack ,优缺点在哪

优点

  1. 更快的冷启动Vite 借助了浏览器对 ESM 规范的支持,采取了与 Webpack 完全不同的 unbundle 机制
  2. 更快的热更新Vite 采用 unbundle 机制,所以 dev server 在监听到文件发生变化以后,只需要通过 ws 连接通知浏览器去重新加载变化的文件,剩下的工作就交给浏览器去做了。

缺点

  1. 开发环境下首屏加载变慢:由于 unbundle 机制,Vite 首屏期间需要额外做其它工作。不过首屏性能差只发生在 dev server 启动以后第一次加载页面时发生。之后再 reload 页面时,首屏性能会好很多。原因是 dev server 会将之前已经完成转换的内容缓存起来
  2. 开发环境下懒加载变慢:跟首屏加载变慢的原因一样。Vite 在懒加载方面的性能也比 Webpack 差。由于 unbundle 机制,动态加载的文件,需要做 resolveloadtransformparse 操作,并且还有大量的 http 请求,导致懒加载性能也受到影响。
  3. webpack支持的更广。由于 Vite 基于ES Module,所以代码中不可以使用CommonJs;webpack更多的关注兼容性, 而Vite 关注浏览器端的开发体验。Vite目前生态还不如 Webpack
参考文章

monorepo

什么是monorepo

答:Monorepo可以理解为一种基于仓库的代码管理策略,它提出将多个代码工程“独立”的放在一个仓库里的管理模式。每个代码工程在逻辑上是可以独立运行开发以及维护管理的。

为什么需要monorepo

Monorepo:只有一个仓库,并且把项目拆分多个独立的代码工程进行管理,而代码工程之间可以通过相应的工具简单的进行代码共享。而传统仓库管理模式则是通过建立多个仓库,每个仓库包含拆分好的代码工程,而仓库间的调用共享则是通过 NPM 或者其他代码引用的方式进行。

如何使用
  • lerna
  • pnpm
更多

pnpm 跟 npm 区别

pnpm pnpm就是一个包管理工具,原生支持Monorepo,比npm和yarn更快一些

image.png

image.png

image.png

我们知道了pnpm在全局通过Store来存储所有的node_modules依赖,并且在.pnpm目录中存储项目的hard links,通过hard link来链接真实的文件资源,项目中则通过symbolic link链接到.pnpm/node_modules目录中,依赖放置在同一级别避免了循环的软链

硬链接

硬连接指通过索引节点来进行连接。在Linux的文件系统中,保存在磁盘分区中的文件不管是什么类型都给它分配一个编号,称为索引节点号(Inode Index)。在Linux中,多个文件名指向同一索引节点是存在的。一般这种连接就是硬连接。硬连接的作用是允许一个文件拥有多个有效路径名,这样用户就可以建立硬连接到重要文件,以防止“误删”的功能。其原因如上所述,因为对应该目录的索引节点有一个以上的连接。只删除一个连接并不影响索引节点本身和其它的连接,只有当最后一个连接被删除后,文件的数据块及目录的连接才会被释放。也就是说,文件真正删除的条件是与之相关的所有硬连接文件均被删除。

软链接

称之为符号链接(Symbolic Link),也叫软链接。软链接文件有类似于Windows的快捷方式。它实际上是一个特殊的文件。软链接保存了其代表的文件的绝对路径,访问时替换自身路径

首先介绍下 link,也就是软硬连接,这是操作系统提供的机制,硬连接就是同一个文件的不同引用,而软链接是新建一个文件,文件内容指向另一个文件的真实路径。在pnpm中软连接处存储的硬链接的地址

概念

如果不复制文件,只在全局仓库保存一份 npm 包的内容,其余的地方都 link 过去呢?

这样不会有复制多次的磁盘空间浪费,而且也不会有路径过长的问题。因为路径过长的限制本质上是不能有太深的目录层级,现在都是各个位置的目录的 link,并不是同一个目录,所以也不会有长度限制

所有的依赖都在这里铺平了,都是从全局 store 硬连接过来的,然后包和包之间的依赖关系是通过软链接组织的。

image.png

优势
  • 安装速度快
  • 占用磁盘空间少
  • 支持monorepo
  • 解决幽灵依赖问题、依赖不确定性问题、依赖项安装重复
注意

npm1/2采用嵌套的方式,yarn采用平铺式管理,但是有可能一个包还会嵌套node_nodules,因为一个包是可能有多个版本的,提升只能提升一个,所以后面再遇到相同包的不同版本,依然还是用嵌套的方式。

更多

webpack 基本配置

点击展开code

const os = require('os')
const { resolve } = require('path')
const webpack = require('webpack')
const HappyPack = require('happypack')
const WebpackBar = require('webpackbar')
const CopyPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin')
const CircularDependencyPlugin = require('circular-dependency-plugin')

const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length })
const source = resolve(__dirname, '..', 'src')

module.exports = {
  context: source,
  entry: {
    app: './index.tsx',
  },
  watchOptions: {
    // 不监听的 node_modules 目录下的文件
    ignored: /node_modules/,
  },
  stats: {
    assets: true,
    builtAt: true,
    colors: true,
    chunks: false,
    children: false,
    env: true,
    entrypoints: false,
    errors: true,
    errorDetails: true,
    hash: true,
    modules: false,
    moduleTrace: true,
    performance: true,
    publicPath: true,
    timings: true,
    version: true,
    warnings: true,
  },
  resolve: {
    unsafeCache: true,
    mainFiles: ['index'],
    mainFields: ['main'],
    extensions: ['.ts', '.tsx', '.js'],
    modules: [source, 'node_modules'],
    alias: {
      '@': source,
      'react-dom': '@hot-loader/react-dom',
      moment: 'dayjs',
      '@components': resolve(__dirname, '../src/components'),
      '@ant-design/icons/lib/dist$': resolve(__dirname, '../src/assets/icons.ts'),
    },
  },
  module: {
    rules: [
      {
        test: /\.((ts|js)(x?))$/,
        exclude: /node_modules/,
        include: [source],
        use: 'happypack/loader?id=babel',
      },
      {
        test: /\.(jpe?g|png|gif|svg)$/,
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 8 * 1024,
              outputPath: 'images',
              name: '[path][name].[ext]',
            },
          },
        ],
      },
    ],
  },
  plugins: [
    new WebpackBar(),
    new CopyPlugin([
      {
        from: resolve(__dirname, '../static'),
        ignore: ['dll/*'],
        to: resolve(__dirname, '../dist'),
      },
    ]),
    new HtmlWebpackPlugin({
      title: '新际通管理平台',
      filename: 'index.html',
      template: resolve(__dirname, '../public/index.html'),
      hash: true,
      minify: {
        removeRedundantAttributes: true,
        collapseWhitespace: true,
        removeAttributeQuotes: true,
        removeComments: true,
        collapseBooleanAttributes: true,
      },
      // favicon: resolve(__dirname, '../public/favicon.ico'),
    }),
    new HappyPack({
      id: 'babel',
      threadPool: happyThreadPool,
      loaders: [
        'cache-loader',
        {
          loader: 'babel-loader',
          query: {
            cacheDirectory: './node_modules/webpack_cache/',
          },
        },
        'eslint-loader',
      ],
    }),
    new webpack.IgnorePlugin(/^\.\/locale$/, /dayjs$/),
    new HardSourceWebpackPlugin(),
    new CircularDependencyPlugin({
      exclude: /node_modules/,
      include: /src/,
      failOnError: true,
      allowAsyncCycles: false,
      cwd: process.cwd(),
    }),
  ],
}

Webpack5 新特性有哪些

  • 模块联邦

    • JavaScript 应用得以从另一个 JavaScript 应用中动态地加载代。
  • 尝试用持久性缓存来提高构建性能

    • 内置 FileSystem Cache 能力加速二次构建。 cache: memory | 'filesystem' 。
  • 尝试用更好的 Tree Shaking 和代码生成来改善包大小

    • webpack5,可以进行根据作用域之间的关系进行优化。比如: a.js 中到处了两个方法 a 和 b,在 index.js 中引入了 a.js 到处的 a 方法,没有引用 b 方法。那么 webpack4 打包出来的结果包含了 index.js 和 a.js 的内容,包含了没有用到的 b 方法。但是 webpack5 的 treeshaking,会进行作用域分析,打包结果只有 index 和 a 文件中的 a 方法,没有用到的 b 方法是不会被打包进来的。
    • webpack4 的 treeshaking 是关注 import 了某个库的什么模块,那么我就打包什么;webpack5 更精细化,直接分析到哪些变量有效地用到了,那么我就打包哪些变量。
  • 更友好的 chunk Cache 支持性,chunkid 不变

    • Webpack5 之前,文件打包后的chunk名称是通过 ID 顺序排列的,一旦后续有一个文件进行了改动<创建或者删除>,那么必将造成后面的文件打包出来的文件名产生变化,即使文件内容没有产生改变。因此会造成资源的缓存失效。
    • Webpack5 有着更友好的长期缓存能力支持,其通过 hash 生成算法,为打包后的 modules 和 chunks 计算出一个短的数字ID ,这样即使中间删除了某一个文件,也不会造成大量的文件缓存失效,未影响到chunk从缓存中读取,加快打包速度。

webpack5新特性

webpack 热更新原理

webpack 如何自定义loader

带有副作用的内容转换器,函数, 从右到左执行,实质上是一个compose函数 将源文件经过转换输出新的结果,支持链式操作,其本质上就是一个函数


module.exports = function(source, sourceMap?, data?) {}

/** 同步loader */
module.exports = function (context) {
  const options = LeviLoaderUtils.getOptions(this);

  console.log('loader配置项:', options);

  const result = context.concat(`console.log("${options.message || '没有配置项'}");`);

  return result;
};

/** 异步loader */
// module.exports = function (context) {
//   let count = 1;
//   const options = LeviLoaderUtils.getOptions(this);
//   const callback = this.async();

//   console.log(options);
//   const timer = setInterval(() => {
//     count = count + 1;
//     console.log(count);
//   }, 1000);

//   setTimeout(() => {
//     callback(null, context);
//     clearInterval(timer);
//   }, 4000);
// };

    // webpack callnack完整签名如下
    this.callback(
        // 异常信息,Loader 正常运行时传递 null 值即可
        err: Error | null,
        // 转译结果
        content: string | Buffer,
        // 源码的 sourcemap 信息
        sourceMap?: SourceMap,
        // 任意需要在 Loader 间传递的值
        // 经常用来传递 ast 对象,避免重复解析
        data?: any
    );

参考文章

webpack 如何自定义plugin

plugin本质上就是拥有apply方法的对象,在webpack初始化会执行apply方法
plugin 可以监听webpack各个生命周期广播出来的事件,在合适的实际,通过webpack释放的API来改变输出结果

class Levi_Plugin {
  apply(compiler) {
    //不推荐使用,plugin函数被废弃了
    // compiler.plugin("compile", (compilation) => {
    //   console.log("compile");
    // });
    //注册完成的钩子
    compiler.hooks.done.tap("MyPlugin", (compilation) => {
      console.log("compilation done");
    });
  }
}

// 注册异步狗子
class Levi_Plugin {
  apply(compiler) {
    compiler.hooks.run.tapAsync("MyPlugin", (compilation, callback) => {
      setTimeout(()=>{
        console.log("compilation run");
        callback()
      }, 1000)
    });
    compiler.hooks.emit.tapPromise("MyPlugin", (compilation) => {
      return new Promise((resolve, reject) => {
        setTimeout(()=>{
          console.log("compilation emit");
          resolve();
        }, 1000)
      });
    });
  }
}

module.exports = Levi_Plugin;
/* eslint-disable */

function _interopDefault(ex) {
  return ex && typeof ex === 'object' && 'default' in ex ? ex['default'] : ex
}

var fs = _interopDefault(require('fs'))
var path = _interopDefault(require('path'))
var OSS = _interopDefault(require('ali-oss'))
var chalk = _interopDefault(require('chalk'))

/**
 * 插件的基本构成:
 * 1. 一个具名 JavaScript 函数
 * 2. 在它的原型上定义 apply 方法
 * 3. 指定一个触及到 webpack 本身的事件钩子
 * 4. 操作 webpack compilation 特定数据
 * 5. 在实现功能后调用 webpack 提供的 calllback
 * ! https://webpack.docschina.org/contribute/writing-a-plugin/
 */

class OSSPlugin {
  /**
   * Creates an instance of OSS.
   * @param {object} options
   * @memberof OSS
   * @property {string} region
   * @property {string} accessKeyId
   * @property {string} accessKeySecret
   * @property {string} bucket
   * @property {string} BUILDFOLDER
   * @property {string} PROJECT_NAME
   * @property {string} RESOURCE_PREFIX
   * @property {string} BUILD_ENV
   * @property {string} TIME_STAMP
   */
  constructor(options) {
    this.apply = compiler => {
      /** 编译资源 -> 磁盘 */
      compiler.hooks.afterEmit.tapAsync('OSS', this.afterEmit)
    }

    this.afterEmit = async (compilation, done) => {
      this.handleTraversing(this.options.BUILDFOLDER)
      done()
    }

    this.handleTraversing = async buildFolder => {
      const files = fs.readdirSync(`${buildFolder}`)
      for (let file of files) {
        let target = path.join(buildFolder, file)
        fs.statSync(target).isFile()
          ? await this.handleUpload(target)
          : await this.handleTraversing(target)
      }
    }

    this.handleUpload = async file => {
      try {
        const { PROJECT_NAME, BUILD_ENV, TIME_STAMP } = this.options
        const start = Date.now()
        let targetPath = this.platform.includes('win') ? file.split('\\').join('/') : file
        targetPath = targetPath.replace(/^dist[\/|\\](.*)/, '$1')
        await this.client.put(
          `levis/${PROJECT_NAME}/${BUILD_ENV}/${TIME_STAMP}/${targetPath}`,
          `${file}`,
        )
        const useTime = (new Date() - start) / 1000
        this.handleTips({
          targetPath,
          useTime,
        })
      } catch (e) {
        process.exit(-1)
      }
    }

    this.handleTips = ({ targetPath, useTime }) => {
      console.log(
        `${chalk.green(`✔ `)} ${chalk.cyan(`${targetPath}`)} ${chalk.grey(`上传用时: ${useTime}`)}`,
      )
    }

    this.options = options
    this.platform = process.platform
    this.client = new OSS(options)
  }
}

module.exports = OSSPlugin

  • compiler对象包含了 Webpack 环境所有的的配置信息。这个对象在启动 webpack 时被一次性建立,并配置好所有可操作的设置,包括 options,loader 和 plugin。当在 webpack 环境中应用一个插件时,插件将收到此 compiler 对象的引用。可以使用它来访问 webpack 的主环境
  • compilation对象包含了当前的模块资源、编译生成资源、变化的文件等。
参考文档

webpack中loader和plugin区别

  • loader,它是一个转换器,将A文件进行编译成B文件,比如:将A.less转换为A.css,单纯的文件转换过程。
  • 在webpack运行的生命周期中会广播出许多事件,plugin可以监听这些事件,在合适的时机通过webpack提供的API改变输出结果

Loader 本质就是一个函数,在该函数中对接收到的内容进行转换,返回转换后的结果。 因为 Webpack 只认识 JavaScript,所以 Loader 就成了翻译官,对其他类型的资源进行转译的预处理工作

Plugin 就是插件,基于事件流框架 Tapable,插件可以扩展 Webpack 的功能,在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果

Loader 在 module.rules 中配置,作为模块的解析规则,类型为数组。每一项都是一个 Object,内部包含了 test(类型文件)、loader、options (参数)等属性。

Plugin 在 plugins 中单独配置,类型为数组,每一项是一个 Plugin 的实例,参数都通过构造函数传入。

  • compiler 对象代表了完整的 webpack 环境配置。这个对象在启动 webpack 时被一次性建立,并配置好所有可操作的设置,包括 options,loader 和 plugin。当在 webpack 环境中应用一个插件时,插件将收到此 compiler 对象的引用。可以使用它来访问 webpack 的主环境
  • compilation 对象代表了一次资源版本构建。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation,从而生成一组新的编译资源。一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。compilation 对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用

webpack构建流程

  • 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数
  • 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译
  • 确定入口:根据配置中的 entry 找出所有的入口文件
  • 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
  • 完成模块编译:在经过第4步使用 Loader翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系
  • 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把分包规则把每个Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
  • 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。

简单说

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

常见的loader有哪些

  • ts-loader
  • url-loader
  • file-loader
  • less-loader
  • css-loader
  • style-loader
  • babel-loader
  • eslint-loader
  • cache-loader
  • thread-loader

常见的Plugins有哪些

  • happypack 多进程构建 被thread-loader代替
  • clean-webpack-plugin
  • html-webpack-plugin 简化html创建
  • mini-css-extract-plugin 提取css文件
  • hardsource-webpack-plugin 启用缓存 被cache-loader代替
  • define-plugin 定义环境变量 (Webpack4 之后指定 mode 会自动配置)
  • webpack-bundle-analyzer 可视化 Webpack 输出文件的体积 (业务组件、依赖第三方模块)

webpack module/bundle/chunk关系

  • module 在webpack中,一个文件对应一个module
  • chunk: webpack根据文件引用顺序,生成chunk文件
  • bundle: 最终输出的文件,与chunk可能是多对一关系

module就是没有编译的源文件,webpack根据文件引用关系,生成chunk文件,webpack处理好chunk后,生成bundle文件

    index.bundle.js/ index.bundle.css 在index.js被引用,分别被打包进chunk 0 中,最后chunk 0 生成了2个bundle

如何提升webpack构建速度<Webpack的性能优化>

  • 启用多进程构建(happypack/thead-loader)
  • 压缩代码
    • 使用mini-css-extract-plugin 提取公共css
    • terser-webpack-plugin 开启js多进程压缩
  • 缩小打包作用域
    • exclude/include (确定 loader 规则范围)
    • resolve.modules 指定解析第三方包的目录位置 (减少不必要的查找)
    • resolve.extensions 尽可能减少后缀尝试的可能性
    • 合理使用alias,简化导入
  • 利用缓存提升二次加载速度 hard-source-webpack-plugin
  • 提取页面公共资源
    • 使用 SplitChunksPlugin 进行(公共脚本、基础包、页面公共文件)分离(Webpack4内置) ,替代了 CommonsChunkPlugin 插件
  • 提取公共js资源 => splitChunks
    module.exports = {
        optimization: {
            splitChunks: {
                cacheGroups: {
                    utils: {
                        chunks: 'initial',
                        minSize: 0,
                        minChunks: 2
                    }
                }
            }
        }
    };
    

webpack如何文件监听

  • 启动命令行添加 --watch
  • webpack 配置文件中 添加 watch: true

webpack是如何支持esmodule

cjs-> esm export default属性怎么处理的

esModuleInterop 到底做了什么?

我们知道,对于直接 import 默认导入的话, esmodule 相当于导入 default 属性,事实上 commonjs 并没有导出 default ,但 webpack 帮我们进行了兼容__webpack_require__.n

var _add__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__( "./src/cjs.js");
var _add__WEBPACK_IMPORTED_MODULE_0___default = __webpack_require__.n(_add__WEBPACK_IMPORTED_MODULE_0__);

__webpack_require__.n = (module) => {
  var getter = module && module.__esModule ? // 判断是否为 esmodule 模块
      () => (module['default']) :
  () => (module); // 直接返回整个模块
  __webpack_require__.d(getter, { a: getter }); // 这句没懂
  return getter;
};

esmodule下引用cjs/esm,webpack 的编译机制比较特别

// 代码经过简化
// before
import cjs from "./cjs";
console.log(cjs);
// after
var cjs = __webpack_require__("./src/cjs.js");
var cjsdefault = __webpack_require__.n(cjs); 
console.log(cjsdefault);

// before
import esm from "./esm";
console.log(esm);
// after
var esm = __webpack_require__("./src/esm.js");
console.log(esm["default"]); // 直接返回

// before
import {a, b} from "./esm";
console.log(esm);
// after
var {a,b} = __webpack_require__("./src/esm.js");
console.log(a, b);
  • 不管是 esmodule 还是 commonjs 模块,最终都转换成了 module = {exports: {}} 形式的模块,所以它们之间的混用成为了可能
  • import commonjs 模块的话,import 拿到的就是整个 module.exports 对象,正常使用即可。如果我们直接改写 import 默认导入 对象,webpack 会认为等同于 export default ,进行兼容处理
  • require esmodule 模块的话,如果之前 esmodule 模块中有 export default ,那么使用的时候需要显示的调用 xxx.default ,对于其他的 export 正常使用即可

前端模块化由来

nodejs如何支持es module

esmodule、commonjs区别

解决了什么问题

  • 解决变量污染问题,每个文件都是独立的作用域,所以不存在变量污染
  • 解决文件依赖问题,一个文件里可以清楚的看到依赖了那些其它文件
commonjs

    // 导出 utils.js
    module.export = {
        name: 'levis',
        age: 20
    }
    
    export.name = 'levis'
    
    
    // 导入
    const { name } = require('utils.js')

支持动态导入

    let lists = ["./index.js", "./config.js"]
    lists.forEach((url) => require(url)) // 动态导入

    if (lists.length) {
        require(lists[0]) // 动态导入
    }

commonjs导入的值是拷贝,改变可以导入变量的值


    // utils.js
    let num = 0;
    module.export = {
        name,
        add(){
            num++;
        }
    }
    
    // app.js
    const {num, add} = require('utils.js');
    console.log(num) // 0 
    add() 
    console.log(num) // 0 num = 10
esmodule
   export name = 'levis';
   expor defaull  { name: 'levis', age: 20 }
   
   import {name} from './xxx.js';
   import * as Utils from '../js'

esmodule导入的值是值的引用,与原module值存在映射关系。不能进行修改,也就是说值是只读状态

    // index.js
    export let num = 0;
    export function add() {
        ++ num
    }

    import { num, add } from "./index.js"
    console.log(num) // 0
    add()
    console.log(num) // 1
    num = 10 // 抛出错误

esmodule是静态,import只能声明在文件最顶部,不能动态导入,原因是esmodule语句运行于代码编译时

区别
  • 两者的模块导入导出语法不同:commonjs是module.exports,exports导出,require导入;ES6则是export导出,import导入
  • CommonJs导出值是拷贝,可以修改导出的值, esmodule导出是引用值之前都存在映射关系,并且值都是可读的,不能修改
  • CommonJs同步加载模块,会阻塞后续代码的运行,代码发生在运行时, esmodule是编译时加载,代码发生在编译时,同时esmodule在编译时候就能确定模块之间依赖关系,以及导入导出的值。正是基于这个关系,使得tree-shaking成为可能
    // CommonJS模块
     let { stat, exists, readFile } = require('fs');
    
     // 等同于
     let _fs = require('fs');
     let stat = _fs.stat;
     let exists = _fs.exists;
     let readfile = _fs.readfile;
    
     上面代码的实质是整体加载fs模块(即加载fs的所有方法),生成一个对象(_fs),
     然后再从这个对象上面读取 3 个方法。这种加载称为“运行时加载”,
     因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”
     
     
     import { stat, exists, readFile } from 'fs';
     上面代码的实质是从fs模块加载 3 个方法,其他方法不加载。
     这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,
     效率要比 CommonJS 模块的加载方式高;
    
    
参考文章

packjson中main module browser区别以及优先级

package.json 中 你还不清楚的 browser,module,main 字段优先级

  • main : 定义了 npm 包的入口文件,browser 环境和 node 环境均可使用
  • module : 定义 npm 包的 ESM 规范的入口文件,browser 环境和 node 环境均可使用
  • ·browser : 定义 npm 包在 browser 环境下的入口文件
webpack + web + ESM/commonjs
    import test from 'test';
    const axios = require('axios')

实际上的加载优先级是 browser = browser+mjs > module > browser+cjs > main
也就是说 webpack 会根据这个顺序去寻找字段指定的文件,直到找到为止

webpack + node + ESM/commonjs
    import test from 'test';
    const axios = require('axios')

我们清楚,使用 webpack 构建项目的时候,有一个 target 选项,默认为 web,即用 web 应用构建。

优先级是: module > main

node + ESM/commonjs

通过node直接执行文件,加载main入口文件

当我们需要进行一些 同构项目,或者其他 node 项目的构建的时候,我们需要将 webpack.config.js 的 target 选项设置为 node 进行构建

总结
  • 如果 npm 包导出的是 ESM 规范的包,使用 module
  • 如果 npm 包只在 web 端使用,并且严禁在 server 端使用,使用 browser。
  • 如果 npm 包只在 server 端使用,使用 main
  • 如果 npm 包在 web 端和 server 端都允许使用,使用 browser 和 main

typescript 模块解析

typescript模块解析

泛型理解,写一个泛型例子

// 获取函数参数类型
const fn = (name: number, age: string, area: object) => `${name}${age}`
type GetFnType<T extends Function> =  T extends (...args: [ ...infer First ,infer R]) => any ? R : never;
type GetFnTypeResult =  GetFnType<typeof fn>;

// 函数增加参数
export type AppendArgument<T extends (...args: unknown[]) => unknown, NewArg> = 
  T extends (...args: infer R) => infer U 
  ? (...args: [...R, NewArg]) => U
  : never
  

// 动态修改interface 中key的必填或者非readonly等
type LeviPartial<T extends Object> = {
    readonly [Key in keyof T as `${Uppercase<Key & string>}_`]: T[Key];
}
interface PartialObj  {
    name: string;
    age?: number;
    area: boolean
}
type LeviPartialResult = LeviPartial<PartialObj>

// 取元素第一个类型
type GetFirstType<T extends any[]> = T extends [...infer R, unknown] ? R : never;
type GetFirstTypeResult=  GetFirstType<[1,2,34,45]>

// StartWith
type StartWith<T extends string, Prefix extends string> = T extends `${Prefix}${string}` ? true : false;
type EndWith<T extends string, U extends string> = T extends `${string}${U}` ? true : false;
type StartWithResult = StartWith<'abcsd', 'a'>;
type EndWithResult = EndWith<'abcsd', 'd'>;

type Push<T extends unknown[], Target extends unknown> =  [...T, Target];
type PushResult = Push<[1,2,3],4>

// 替换Str 代替replace
type ReplaceStr<Str extends string, To extends string, Target extends string> = Str extends `${infer Prefix}${To}${infer Rest}` ? `${Prefix}${Target}${ReplaceStr<Rest, To, Target>}` : Str;
type ReplaceStrResult = ReplaceStr<'nsm', 'n', 'z'>

// 首字母大写
type TransformCapitalizeStr<T extends string> = 
  T extends `${infer First}${infer Rest}`
  ? `${Uppercase<First>}${Rest}`
  : T
type TransformCapitalizeStrResult = TransformCapitalizeStr<'levi'>

//  驼峰
type HandleString<T> = T extends `${infer First}_${infer Target}${infer Rest}` ? `${First}${Uppercase<Target>}${HandleString<Rest>}` : T;
type HandleStringResult = HandleString<'_abc_defg_wzy'>

// 获取Promise类型
export type GetPromiseTYpe<T> = T extends Promise<infer R> ? R : never

// 删除指定字符串
type Equal<T, U> = T extends U ? U extends T ? true : false : false
type DropChar<T extends string, U extends string> = T extends `${infer First}${infer Rest}` ? `${Equal<First, U> extends true ? '' : First}${DropChar<Rest, U>}` : T;
type DropCharResult = DropChar<'abcdb', 'b'> 

// Zip
type Zip<T extends unknown[], U extends unknown[]> = T extends [infer TFirst, ...infer TRest] ? U extends [infer UFirst, ...infer URest] ? [[TFirst, UFirst],  ...Zip<TRest, URest>] : [] : [];
type ZipResult = Zip<[1, 2], ['levi', 'nsm']>

// FilterByValueType
type FilterByValueType <T extends Object, ValueType extends unknown> = {
    [Key in keyof T as T[Key] extends ValueType ? Key : never] : T[Key]
}
type FilterByValueTypeResult = FilterByValueType<{name: string, age: number, area: string[]}, number | string>

// 递归获取Promise 返回值类型
type GetPromiseDeepType<T> = T extends Promise<infer R> ? GetPromiseDeepType<R> : T
type GetPromiseDeepTypeResult = GetPromiseDeepType<Promise<Promise<Promise<Record<string, any>>>>>

// 反转数组
type ReverseArr<T extends unknown[]> = T extends [...infer Rest, infer Last] ? [Last, ...ReverseArr<Rest>] : [];
type ReverseArrResult = ReverseArr<[1,2,3,4,5]>

// includes 
type IsEqual<A, B> = (A extends B ? true : false) & (B extends A ? true : false);
type Includes<T, Target> = T extends [infer First, ...infer Rest] ?  IsEqual<First, Target> extends true ? true : Includes<Rest, Target> : false;
type IncludesResult = Includes<['a', 'b', 'c', 'd'], 'c'>

// 删除指定元素
type RemoveItem<Rest extends unknown[], Target extends unknown, Result extends unknown[] = []> = Rest extends [infer First, ...infer R] ? 
    IsEqual<Target, First> extends  true ? RemoveItem<R, Target, Result> : RemoveItem<R, Target, [...Result, First]> : Result;
type RemoveItemResult = RemoveItem<[1,2,3,4], 12>;

// 字符串处理为联合类型
type StringToUnion<T extends string> =  T extends `${infer First}${infer Rest}` ? First | StringToUnion<Rest> : never;
type StringToUnionResult = StringToUnion<'eee'>;

// 反转字符串
type ReverseStr<Str extends string, Result extends string = ''> = Str extends `${infer First}${infer Rest}` ?  ReverseStr<Rest, `${First}${Result}`> : Result;
type ReverseStrResult = ReverseStr<'abcd'>;

// 构造指定数组
type BuildArray<Len extends number,  Arr extends unknown[] = [],  Ele = unknown> = Arr['length'] extends Len ? Arr :  BuildArray<Len, [...Arr, Ele]>;
type BuildArrayResult = BuildArray<7>;

// 数组长度计算
type ArrayAdd<T extends number, U extends number> =  [...BuildArray<T>, ...BuildArray<U>]['length'];
type ArraySubtract<T extends number, U extends number> = BuildArray<T> extends [...BuildArray<U>, ...infer R] ? R['length'] : never;
type ArrayAddResult = ArrayAdd<7, 2>;
type ArraySubtractResult = ArraySubtract<7, 2>;

//  字符串长度计数
type StrLen <Str extends string, Result extends unknown[] = []> = Str extends `${infer First}${infer Rest}` ? StrLen<Rest, [...Result, First]> : Result['length']
type StrLenResult = StrLen<'abcde'>

// 自定义exclude
type Exclude<T extends any, U extends any> = T extends U ? never : T;
type ExcludeResult = Exclude<'a' | 'b' | 'c', 'a'>

// 自定义Omit
type LeviOmit<T extends unknown, U extends unknown> = Pick<T, Exclude<keyof T, U>>;
type LeviOmitResult = LeviOmit<{name: string, age: number, area: string, isAhs: boolean}, 'name' | 'isAhs'>;

const enum 与普通enum 区别

  • const枚举会在编译结束被删除,普通枚举最终被编译为一个对象
  • const 枚举不支持动态计算
        const enum Demo1 {
          A = d * 1,  // 报错
          B = d * 2,  // 报错
        }
    
        enum Demo2 {
          A = d * 1,
          B = d * 2,
        }
    

interface Type区别

相同点

  • 都可以描述Object或者Function
    interface Person {
        name: string;
        age: number;
    }
    
    inteface Fn {
        (): Pormise<boolean>
    }
    
    
   type Person = {
       name: string;
       age: number;
   }
   
   type Fn = () => Promise<boolean>

  • 都可以被继承
  • 类可以实现implments inteface/type中的属性或方法

不同点

  • type 可以定义类型,声明联合类型、元祖, interface做不到
        type Str = string;
        type Action = 'view' | 'delete'
        type Area = ['shanghai', 'beijing']
    
    
  • interface会声明合并,type不行
        interface Person {name: string}
        interface Persin {age: number}
        
        const user: Person = {
            name: 'levi',
            age: 20
        }
        
        type Person { namestring };
    
        // Error: 标识符“Person”重复。ts(2300)
        type Person { agenumber }
    

unknown 与 any区别

使用unknown 可以保证类型安全,使用 any 则彻底放弃了类型检查 , 在很多情况下, 我们可以使用 unknown 来替代 any , 既灵活, 又可以继续保证类型安全. unknown 情况下,使用以下2种方式收窄类型

  • 使用类型断言缩小未知范围
  • 使用类型守卫方式
类型守卫

ts会在条件语句种收窄变量的类型,有以下几种方式

  • typeof
  • instanceof
  • in判断: for(let key in obj)
  • 字面量相等攀登: === 、 !== 、 !

module namespace区别

module

TypeScript 与ECMAScript 2015 一样,任何包含顶级 import 或者 export 的文件都被当成一个模块 相反地,如果一个文件不带有顶级的import或者export声明,那么它的内容被视为全局可见的

模块会在它自己的作用域,而不是在全局作用域里执行。这意味着,在一个模块中声明的变量、函数、类等,对于模块之外的代码都是不可见的,除非你显示的导出这些值

namesapce

命名空间一个最明确的目的就是解决重名问题,是全局的,命名空间定义了标识符的可见范围,一个标识符可在多个名字空间中定义,它在不同命名空间中互不影响

TypeScript 中命名空间使用 namespace 来定义,语法格式如下:

    // Validation.ts
    namespace SomeNameSpaceName { 
       export interface ISomeInterfaceName {  name: string    }   
       export class SomeClassName {      }   
    }
    
    // main.ts
    /// <reference path="Validation.ts" />
    export const name: SomeNameSpaceName.ISomeInterfaceName = {name: 'levis'}
区别
  • 命名空间最终编译后一个普通的带有名字的JavaScript对象
  • 像命名空间一样,模块可以包含代码和声明。不同的是模块可以声明它的依赖
  • 同一个命名空间可以拆分到多个文件
   // Validation.ts
    namespace Validation {
        export interface StringValidator {
            isAcceptable(s: string): boolean;
        }
    }
    
   // LettersOnlyValidator.ts
   /// <reference path="Validation.ts" />
    namespace Validation {
        const lettersRegexp = /^[A-Za-z]+$/;
        export class LettersOnlyValidator implements StringValidator {
            isAcceptable(s: string) {
                return lettersRegexp.test(s);
            }
        }
    }
    
   // ZipCodeValidator.ts
   /// <reference path="Validation.ts" />
    namespace Validation {
        const numberRegexp = /^[0-9]+$/;
        export class ZipCodeValidator implements StringValidator {
            isAcceptable(s: string) {
                return s.length === 5 && numberRegexp.test(s);
            }
        }
    }
    
    // App.ts 使用
    /// <reference path="Validation.ts" />
    /// <reference path="LettersOnlyValidator.ts" />
    /// <reference path="ZipCodeValidator.ts" />
    
    // Some samples to try
    let strings = ["Hello", "98052", "101"];
    // Validators to use
    let validators: { [s: string]: Validation.StringValidator } = {};
    validators["ZIP code"] = new Validation.ZipCodeValidator();
    validators["Letters only"] = new Validation.LettersOnlyValidator();

控制反转、依赖注入原理

背景

前端应用在不断壮大的过程中,内部模块间的依赖可能也会随之越来越复杂,模块间的 低复用性 导致应用 难以维护,不过我们可以借助计算机领域的一些优秀的编程理念来一定程度上解决这些问题,接下来要讲述的 IoC 就是其中之一。

控制反转/依赖注入的目的并非为软件系统带来更多功能,而是为了提升对象重用的频率、扩展了灵活性

控制反转

控制反转是一种思想,依赖注入则是这一思想的具体实现方式

  1. 高层次的模块不应该依赖于低层次的模块,它们都应该依赖于抽象
  2. 抽象不应该依赖于具体实现,具体实现应该依赖于抽象
  3. 面向接口编程 而不要面向实现编程
依赖注入

依赖注入是实现程序解耦的一种方式, 将创建对象的任务转移给其他对象处理,并直接使用依赖项的过程,被称为“依赖项注入

案例1
    // app.js
    class App {
        static modules = []
        constructor(options) {
            this.options = options;
            this.init();
        }
        init() {
            window.addEventListener('DOMContentLoaded', () => {
                this.initModules();
                this.options.onReady(this);
            });
        }
        static use(module) {
            Array.isArray(module) ? module.map(item => App.use(item)) : App.modules.push(module);
        }
        initModules() {
            App.modules.map(module => module.init && typeof module.init == 'function' && module.init(this));
        }
    }
    
    
    // modules/Router.js
    import Router from 'path/to/Router';
    export default {
        init(app) {
            app.router = new Router(app.options.router);
            app.router.to('home');
        }
    };
    // modules/Track.js
    import Track from 'path/to/Track';
    export default {
        init(app) {
            app.track = new Track(app.options.track);
            app.track.tracking();
        }
    };

    // index.js
    import App from 'path/to/App';
    import Router from './modules/Router';
    import Track from './modules/Track';

    App.use([Router, Track]);

    new App({
        router: {
            mode: 'history',
        },
        track: {
            // ...
        },
        onReady(app) {
            // app.options ...
        },
    });

案例2 typescript装饰器

import { Controller, Get } from '@nestjs/common';

@Controller('cats')
export class CatsController {
  @Get()
  findAll(): string {
    return 'This action returns all cats';
  }
}
为什么需要依赖注入
  • 便于单元测试
  • 统一管理依赖对象
参考文章

装饰器分类、原理

装饰器分类
  • 类装饰器
  • 类方法装饰器
  • 类属性装饰器
  • 参数装饰器

简单的一个log装饰器

    class Math {
      @log
      add(a, b) {
        return a + b;
      }
    }

    function log(target, name, descriptor) {
      var oldValue = descriptor.value;

      descriptor.value = function() {
        console.log(`Calling "${name}" with`, arguments);
        return oldValue.apply(null, arguments);
      };

      return descriptor;
    }

    const math = new Math();

    // passed parameters should get logged now
    math.add(2, 4);

装饰器工厂

    function color(value: string) { // 这是一个装饰器工厂
        return function (target) { //  这是装饰器
            // do something with "target" and "value"...
        }
    }
    
    
    @color('red')
    class Levi {
        
    }
为什么装饰器不能用于函数?

装饰器对类的行为的改变,是代码编译时发生的,而不是在运行时。这意味着,修饰器能在编译阶段运行代码

装饰器只能用于类和类的方法,类的属性,不能用于函数,因为存在函数提升

var counter = 0;

var add = function () {
  counter++;
};

@add
function foo() {}
复制代码

上面的代码,意图是执行后counter等于 1,但是实际上结果是counter等于 0。因为函数提升,使得实际执行的代码是下面这样。

@add
function foo() {
}

var counter;
var add;

counter = 0;

add = function () {
  counter++;
};
复制代码

总之,由于存在函数提升,使得装饰器不能用于函数。类是不会提升的,所以就没有这方面的问题。

如果要修饰函数,可以采用高阶函数思想

    function method() {
        console.log('todo')
    }
    
    function wrapperd(fn) {
        return function() {
               console.log('自定义something')
               const result = fn.apply(...arguments)
               console.log('自定义something')
               return result;
        }
    }
    
    const levi = wrapperd(method)
更多

描述babel的转换过程

  • 解析<parse>:将代码转换成 AST
  • 转换<transform>:访问 AST 的节点进行变换操作生产新的 AST
  • 生成<generate>:以新的 AST 为基础生成代码

babel的预设和插件的区别是什么

  • 预设是将一系列插件(plugin)集合起来,暴露单个出口出来
  • plugin是将而是es2015+语法转发为es5语法

babel-runtime和babel-polyfill作用及区别

Babel默认只转换新的JavaScript语法,而不转换新的API。 例如,Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise 等全局对象,以及一些定义在全局对象上的方法(比如 Object.assign)都不会转译。 如果想使用这些新的对象和方法,则需要为当前环境提供一个polyfill

  • babel-polyfill会”加载整个polyfill库”,可通过usage以及browserList配置按需加载,针对编译的代码中新的API进行处理,并且在代码中插入一些帮助函数
    • 引入babel-polyfill会有一定副作用,比如:
      • 修改现有的全局对象:比如修改了Array、String的原型链等
      • 举个例子,我在项目中定义了跟规范不一致的Array.from()函数(别管我为什么不一样,就是这么任 性),同时引入了一个库(依赖babel-polyfill),此时,这个库可能覆盖了自定义的 Array.from()函数,导致出错
  • babel-runtime用以提供编译模块的工具函数, 启用插件babel-plugin-transform-runtime

babel 配置还存在两个问题

  • 从上转译结果来看,includes 这个api直接是 require 了一下,并不是另一种更符合直觉的方式:
  • babel 转译 syntax 时,有时候会使用一些辅助的函数帮忙实现功能

babel-plugin-transform-runtime 插件做了如下事情

  • api 从之前的直接修改原型改为了从一个统一的模块<来自@babel/runtime>中引入,避免了对全局变量及其原型的污染,解决了第一个问题
  • helpers 从之前的原文件定义改为了从一个统一的模块中引入,使得打包的结果中每个 helper 只会存在一个,解决了第二个问题

babel-plugin-transform-runtime用于构建过程的代码转换,babel-runtime是实际导入项目代码的功能模块

@babel/plugin-transform-runtime 的作用是将 helper 和 polyfill 都改为从一个统一的地方引入,并且引入的对象和全局变量是完全隔离的

babel-runtime使用与性能优化