前端工程化还包括bable

ESbuild

esbuild是新一代的Javascript打包工具。

esbuild以速度快著称,耗时只有webpack的2%到3%。很多工具都内置了esbuild,比如vite和snowpack

esbuild快的原因:

1.使用Go语言编写,可以编译为原生代码

2.解析、打印和源映射完全并行化

3.无须昂贵的数据转换,只需很少的几步完成

目前支持:

commonjs模块、es6模块

使用--bundle与es6模块的静态绑定打包

使用--minify完全压缩(空格、标识符和修饰符)

使用--sourcemap完全支持源映射

支持jsx转向js

使用--define进行编译时标识符替换

使用package.json中的browser字段进行替换

自动监测tsconfig中的baseUrl

安装es-build

npm install esbuild

安装完可以检查一下es build的版本

./node_modules/.bin/esbuild --version

执行打包命令

./node_modules/.bin/esbuild app.jsx --bundle --outfile=out.js

wasm版本

npm install esbuild-wasm

在deno中使用

import * as esbuild from 'https://deno.land/x/esbuild@v0.14.22/mod.js'
const ts = 'let test: boolean = true'
const result = await esbuild.transform(ts, { loader: 'ts' })
console.log('result:', result)
esbuild.stop()

插件

esbuild的插件可以在构建过程中执行,用go或者js编写

let envPlugin = {
  name: 'env',
  setup(build) {
    // Intercept import paths called "env" so esbuild doesn't attempt
    // to map them to a file system location. Tag them with the "env-ns"
    // namespace to reserve them for this plugin.
    build.onResolve({ filter: /^env$/ }, args => ({
      path: args.path,
      namespace: 'env-ns',
    }))

    // Load paths tagged with the "env-ns" namespace and behave as if
    // they point to a JSON file containing the environment variables.
    build.onLoad({ filter: /.*/, namespace: 'env-ns' }, () => ({
      contents: JSON.stringify(process.env),
      loader: 'json',
    }))
    
    let path = require('path')

    // Redirect all paths starting with "images/" to "./public/images/"
    build.onResolve({ filter: /^images\// }, args => {
      return { path: path.join(args.resolveDir, 'public', args.path) }
    })

    // Mark all paths starting with "http://" or "https://" as external
    build.onResolve({ filter: /^https?:\/\// }, args => {
      return { path: args.path, external: true }
    })
    
    // Load ".txt" files and return an array of words
    build.onLoad({ filter: /\.txt$/ }, async (args) => {
      let text = await fs.promises.readFile(args.path, 'utf8')
      return {
        contents: JSON.stringify(text.split(/\s+/)),
        loader: 'json',
      }
    })
    
    build.onStart(() => {
      console.log('build started')
    })
    
    build.onEnd(result => {
      console.log(`build ended with ${result.errors.length} errors`)
    })
  },
}

require('esbuild').build({
  entryPoints: ['app.js'],
  bundle: true,
  outfile: 'out.js',
  plugins: [envPlugin],
}).catch(() => process.exit(1))

snowpack

snowpack现已停止维护,snowpack的团队主要开发astro

Vite

冷启动

在浏览器支持 ES 模块之前,JavaScript 并没有提供的原生机制让开发者以模块化的方式进行开发。这也正是我们对 “打包” 这个概念熟悉的原因:使用工具抓取、处理并将我们的源码模块串联成可以在浏览器中运行的文件。

时过境迁,我们见证了诸如 webpackRollupParcel 等工具的变迁,它们极大地改善了前端开发者的开发体验。

然而,当我们开始构建越来越大型的应用时,需要处理的 JavaScript 代码量也呈指数级增长。包含数千个模块的大型项目相当普遍。我们开始遇到性能瓶颈 —— 使用 JavaScript 开发的工具通常需要很长时间(甚至是几分钟!)才能启动开发服务器,即使使用 HMR,文件修改后的效果也需要几秒钟才能在浏览器中反映出来。如此循环往复,迟钝的反馈会极大地影响开发者的开发效率和幸福感。

当冷启动开发服务器时,基于打包器的方式启动必须优先抓取并构建你的整个应用,然后才能提供服务。

Vite 通过在一开始将应用中的模块区分为 依赖源码 两类,改进了开发服务器启动时间。

  • 依赖 大多为在开发时不会变动的纯 JavaScript。一些较大的依赖(例如有上百个模块的组件库)处理的代价也很高。依赖也通常会存在多种模块化格式(例如 ESM 或者 CommonJS)。

    Vite 将会使用 esbuild 预构建依赖。Esbuild 使用 Go 编写,并且比以 JavaScript 编写的打包器预构建依赖快 10-100 倍。

    源码 通常包含一些并非直接是 JavaScript 的文件,需要转换(例如 JSX,CSS 或者 Vue/Svelte 组件),时常会被编辑。同时,并不是所有的源码都需要同时被加载(例如基于路由拆分的代码模块)。

  • Vite 以 原生 ESM 方式提供源码。这实际上是让浏览器接管了打包程序的部分工作:Vite 只需要在浏览器请求源码时进行转换并按需提供源码。根据情景动态导入代码,即只在当前屏幕上实际使用时才会被处理。

更新

基于打包器启动时,重建整个包的效率很低。原因显而易见:因为这样更新速度会随着应用体积增长而直线下降。

一些打包器的开发服务器将构建内容存入内存,这样它们只需要在文件更改时使模块图的一部分失活[1],但它也仍需要整个重新构建并重载页面。这样代价很高,并且重新加载页面会消除应用的当前状态,所以打包器支持了动态模块热重载(HMR):允许一个模块 “热替换” 它自己,而不会影响页面其余部分。这大大改进了开发体验 —— 然而,在实践中我们发现,即使采用了 HMR 模式,其热更新速度也会随着应用规模的增长而显著下降。

在 Vite 中,HMR 是在原生 ESM 上执行的。当编辑一个文件时,Vite 只需要精确地使已编辑的模块与其最近的 HMR 边界之间的链失活[1](大多数时候只是模块本身),使得无论应用大小如何,HMR 始终能保持快速更新。

Vite 同时利用 HTTP 头来加速整个页面的重新加载(再次让浏览器为我们做更多事情):源码模块的请求会根据 304 Not Modified 进行协商缓存,而依赖模块请求则会通过 Cache-Control: max-age=31536000,immutable 进行强缓存,因此一旦被缓存它们将不需要再次请求。

打包

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

虽然 esbuild 快得惊人,并且已经是一个在构建库方面比较出色的工具,但一些针对构建 应用 的重要功能仍然还在持续开发中 —— 特别是代码分割和 CSS 处理方面。就目前来说,Rollup 在应用打包方面更加成熟和灵活。尽管如此,当未来这些功能稳定后,我们也不排除使用 esbuild 作为生产构建器的可能。

预构建

vite在构建过程中会有依赖预构建的过程,这个过程有两个目的

1是开发阶段中,Vite 的开发服务器将所有代码视为原生 ES 模块。因此,Vite 必须先将作为 CommonJS 或 UMD 发布的依赖项转换为 ESM。当转换 CommonJS 依赖时,Vite 会执行智能导入分析,这样即使导出是动态分配的(如 React),按名导入也会符合预期效果

2是Vite 将有许多内部模块的 ESM 依赖关系转换为单个模块,以提高后续页面加载性能,这样是出于性能考虑。一些包将它们的 ES 模块构建作为许多单独的文件相互导入。例如,lodash-es 有超过 600 个内置模块!当我们执行 import { debounce } from 'lodash-es' 时,浏览器同时发出 600 多个 HTTP 请求!尽管服务器在处理这些请求时没有问题,但大量的请求会在浏览器端造成网络拥塞,导致页面的加载速度相当慢。

通过预构建 lodash-es 成为一个模块,我们就只需要一个 HTTP 请求了

缓存

预构建文件缓存

Vite 会将预构建的依赖缓存到 node_modules/.vite。它根据几个源来决定是否需要重新运行预构建步骤:

  • 包管理器的 lockfile 内容,例如 package-lock.jsonyarn.lockpnpm-lock.yaml,或者 bun.lockb
  • 补丁文件夹的修改时间
  • 可能在 vite.config.js 相关字段中配置过的
  • NODE_ENV 中的值

只有在上述其中一项发生更改时,才需要重新运行预构建。如果出于某些原因,你想要强制 Vite 重新构建依赖,你可以用 --force 命令行选项启动开发服务器,或者手动删除 node_modules/.vite 目录

浏览器缓存

解析后的依赖请求会以 HTTP 头 max-age=31536000,immutable 强缓存,以提高在开发时的页面重载性能。一旦被缓存,这些请求将永远不会再到达开发服务器。如果安装了不同的版本(这反映在包管理器的 lockfile 中),则附加的版本 query 会自动使它们失效。如果你想通过本地编辑来调试依赖项,你可以:

  1. 通过浏览器调试工具的 Network 选项卡暂时禁用缓存;
  2. 重启 Vite dev server,并添加 --force 命令以重新构建依赖;
  3. 重新载入页面。

常用配置

本地开发配置

import { defineConfig } from 'vite'
import path from 'path';

export default defineConfig({
  resolve: {
    alias: [
      { find: "src", replacement: path.resolve(__dirname,'src') },
    ]
  },
  server: {
  	// 端口
    port: 8001,
    // 是否开启https服务
    https: true,
    // 代理
    proxy: {
      '/project/delete': {
        target: 'https://www.your-request-url.com',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/project\/delete/, '')
      },
      '/project/update': {
        target: 'https://www.your-request-url.com',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/project\/update/, '')
      },
    }
  },
  css: {
    preprocessorOptions: {
      less: {
        additionalData: `@import "${path.resolve(__dirname, 'src/theme.module.less')}";`,
        javascriptEnabled: true,
      }
    },
  },
})

支持react

@vitejs/plugin-react插件

在vite.config.ts中配置

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
}

按需导入antd样式

vite-plugin-imp

npm i vite-plugin-imp -D

在vite.config.ts中配置

import { defineConfig } from 'vite'
import vitePluginImp from 'vite-plugin-imp'
export default defineConfig({
  plugins: [
  	vitePluginImp({
      libList: [
        {
          libName: 'lodash',
          libDirectory: '',
          camel2DashComponentName: false
        },
        {
          libName: 'antd',
          style(name) {
            // use less
            return `antd/es/${name}/style/index.js`
          }
        },
      ]
    })
  ]
})

react快速刷新

@vitejs/plugin-react-refresh

import { defineConfig } from 'vite'
import reactRefresh from '@vitejs/plugin-react-refresh'
import path from 'path';

export default defineConfig({
  plugins: [reactRefresh()],
})

vue3单文件支持

// vite.config.js
import vue from '@vitejs/plugin-vue'

export default {
  plugins: [vue()],
}

Vue3支持jsx开发

Vue 用户应使用官方提供的 @vitejs/plugin-vue-jsx 插件,它提供了 Vue 3 特性的支持,包括 HMR,全局组件解析,指令和插槽

// vite.config.js
import vueJsx from '@vitejs/plugin-vue-jsx'

export default {
  plugins: [
    vueJsx({
      // options are passed on to @vue/babel-plugin-jsx
    }),
  ],
}

打包

打包时需要确认自己的项目类型。

如果是一个完成的应用,需要将html打包进项目的,可以使用默认配置。

如果自己的项目是SDK,或者是一个react组件。那么就需要使用另外一种库模式来打包。

如果是node环境的npm包,比如脚手架之类需要执行某些node命令行的配置,都会有所不同

常规配置

import { defineConfig } from 'vite'
import reactRefresh from '@vitejs/plugin-react-refresh'
import path from 'path';

export default defineConfig({
  plugins: [reactRefresh()],
  build: {
    rollupOptions: {
      output:{
		  entryFileNames: `[name].${timestamp}.js`,
	      chunkFileNames: `[name].${timestamp}.js`,
	      // css文件名
	      assetFileNames: `[name].${timestamp}.[ext]`
	      // 比如你想构建出来的css为dist/index.css,那么你可以这样
	      //  assetFileNames: `index.[ext]`
	  }
	}
  },
})

库模式

import { defineConfig } from 'vite'
import path from 'path';

export default defineConfig({
  build: {
    lib: {
      // 入口文件
      entry: path.resolve(__dirname, 'src/index.tsx'),
      // umd、iife的格式vite要求必须要有name作为导出的全局变量
      name: "SpecialEffect",
      // 导出格式,默认为["iife","umd"]
      formats: ['iife'],
      // js打包名称,当然这部分官方文档更加详细
      fileName: () => "index.js"
    },
	
	// umd格式下,支持将不需要打包的第三方库,排除在外,并指定全部环境
	// 提供的全局变量代替,比如以下的例子,不将react打包,由全局React
	// 变量提供react库
	rollupOptions: {
      external: ['react'],
      output: {
        globals: {
          react: 'React'
        }
      }
    }
  },
})

wp2vite

安装

npm install -g wp2vite

命令行使用

cd your_workspace/your_project
## 执行wp2vite的命令行
wp2vite 
or 
wp2vite init

然后启动项目就行

// 安装依赖
npm install

// 启动项目
npm run dev // 如果原先你的项目有dev script,请执行下面的命令
or
npm run vite-start

常用loader

vite-svg-loader

安装

npm install vite-svg-loader --save-dev

使用

import svgLoader from 'vite-svg-loader'

export default defineConfig({
  plugins: [vue(), svgLoader({
    svgoConfig: {
      multipass: true
    }
  })
  ]
})

模块联邦

MF 提供的是一种加载方式,并不是 webpack 独有的,所以社区中已经提供了一个的 Vite 模块联邦方案: vite-plugin-federation,这个方案基于 Vite(Rollup) 也实现了完整的模块联邦能力。

首先需要安装 @originjs/vite-plugin-federation

app1 当前应用: vite.config.js配置

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import federation from "@originjs/vite-plugin-federation";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    react(),
    federation({
      name: "app1",
      remotes: {
        app2: "http://localhost:3002/assets/remoteEntry.js",
      },
      shared: ["react"],
    }),
  ],
});

app2远程应用

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import federation from "@originjs/vite-plugin-federation";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    react(),
    federation({
      name: "app2",
      filename: "remoteEntry.js",
      library: { type: "module" },
      exposes: {
        "./App": "./src/App.jsx",
      },
      shared: ["react"],
    }),
  ],
});

vite-plugin-federation 还可以和 webpack 配合使用

remotes: {
    app2: {
        external: 'http://localhost:5011/remoteEntry.js',
        format: 'var',
        from: 'webpack'
    }
}

使用时有两点注意:

React 项目中不能使用异构组件(例如 vite 使用 webpack 的组件或者反之),因为现在还无法保证 vite/rollupwebpack 在打包 commonjs 框架时转换出 export 一致的 chunk,这是使用 shared 的先决条件

vite 使用 webpack 组件相对容易,但是 webpack 使用 vite 组件时 vite-plugin-federation 组件最好是 esm 格式,因为其他格式暂时缺少测试用例完成测试

插件开发

vite支持所有的rollup的插件api, rollup的api包括

在启动时调用的钩子

options,buildStart

在每个传入模块请求时被调用的钩子:

resolveId,load,transform

在服务器被关闭时调用

buildEnd、closeBundle

Vite 插件也可以提供钩子来服务于特定的 Vite 目标。这些钩子会被 Rollup 忽略

config

configResolved

在解析 Vite 配置后调用。使用这个钩子读取和存储最终解析的配置。当插件需要根据运行的命令做一些不同的事情时,它也很有用

const examplePlugin = () => {
  let config

  return {
    name: 'read-config',

    configResolved(resolvedConfig) {
      // 存储最终解析的配置
      config = resolvedConfig
    },

    // 在其他钩子中使用存储的配置
    transform(code, id) {
      if (config.command === 'serve') {
        // dev: 由开发服务器调用的插件
      } else {
        // build: 由 Rollup 调用的插件
      }
    },
  }
}

configureServer

类型:(server: ViteDevServer) => (() => void) | void | Promise<(() => void) | void>

是用于配置开发服务器的钩子

configureServer 钩子将在内部中间件被安装前调用,所以自定义的中间件将会默认会比内部中间件早运行。如果你想注入一个在内部中间件 之后 运行的中间件,你可以从 configureServer 返回一个函数,将会在内部中间件安装后被调用

const myPlugin = () => ({
  name: 'configure-server',
  configureServer(server) {
    // 返回一个在内部中间件安装后
    // 被调用的后置钩子
    return () => {
      server.middlewares.use((req, res, next) => {
        // 自定义请求处理...
      })
    }
  },
})

configurePreviewServer

configureServer 相同但是作为预览服务器。它提供了一个 connect 服务器实例及其底层的 http server。与 configureServer 类似,configurePreviewServer 这个钩子也是在其他中间件安装前被调用的。如果你想要在其他中间件 之后 安装一个插件,你可以从 configurePreviewServer 返回一个函数,它将会在内部中间件被安装之后再调用

const myPlugin = () => ({
  name: 'configure-preview-server',
  configurePreviewServer(server) {
    // 返回一个钩子,会在其他中间件安装完成后调用
    return () => {
      server.middlewares.use((req, res, next) => {
        // 自定义处理请求 ...
      })
    }
  },
})

transformIndexHtml

转换 index.html 的专用钩子。钩子接收当前的 HTML 字符串和转换上下文。上下文在开发期间暴露ViteDevServer实例,在构建期间暴露 Rollup 输出的包

这个钩子可以是异步的,并且可以返回以下其中之一:

  • 经过转换的 HTML 字符串
  • 注入到现有 HTML 中的标签描述符对象数组({ tag, attrs, children })。每个标签也可以指定它应该被注入到哪里(默认是在 <head> 之前)
  • 一个包含 { html, tags } 的对象
const htmlPlugin = () => {
  return {
    name: 'html-transform',
    transformIndexHtml(html) {
      return html.replace(
        /<title>(.*?)<\/title>/,
        `<title>Title replaced!</title>`,
      )
    },
  }
}

插件的顺序

一个 Vite 插件可以额外指定一个 enforce 属性(类似于 webpack 加载器)来调整它的应用顺序。enforce 的值可以是prepost。解析后的插件将按照以下顺序排列:

  • Alias
  • 带有 enforce: 'pre' 的用户插件
  • Vite 核心插件
  • 没有 enforce 值的用户插件
  • Vite 构建用的插件
  • 带有 enforce: 'post' 的用户插件
  • Vite 后置构建插件(最小化,manifest,报告)

apply 属性指明它们仅在 'build''serve' 模式时调用

function myPlugin() {
  return {
    name: 'build-only',
    apply: 'build' // 或 'serve'
    // apply(config, { command }) {
  	// 非 SSR 情况下的 build
  	// return command === 'build' && !config.build.ssr
  	// }
  }
}

底层包

magic-string

安装

npm i magic-string

使用

import MagicString from 'magic-string';
import fs from 'fs'

const s = new MagicString('problems = 99');

s.update(0, 8, 'answer');
s.toString(); // 'answer = 99'

s.update(11, 13, '42'); // character indices always refer to the original string
s.toString(); // 'answer = 42'

s.prepend('var ').append(';'); // most methods are chainable
s.toString(); // 'var answer = 42;'

const map = s.generateMap({
  source: 'source.js',
  file: 'converted.js.map',
  includeContent: true
}); // generates a v3 sourcemap

fs.writeFileSync('converted.js', s.toString());
fs.writeFileSync('converted.js.map', map.toString());

错误

Uncaught Error: Target container is not a DOM element

根元素未找到.原因是: 默认生成的 index.html 中

<div id="root"></div>

https://segmentfault.com/a/1190000039795956

Babel

Babel主要做的就是这几点:

  • 语法转换:将新的语法转换成兼容性更好的旧语法。
  • 特性垫片:通过Polyfill 实现目标环境中缺少的特性,将环境的特性差异垫平,说白了还是做兼容。

比如下面这段代码

// Babel Input: ES2015 arrow function
[1, 2, 3].map((n) => n + 1);

// Babel Output: ES5 equivalent
[1, 2, 3].map(function(n) {
  return n + 1;
});

babel的使用场景主要分为两种:命令行和构建工具。

babel为命令行提供了@babel/cli工具,支持在命令行直接执行babel命令。

npm install --save-dev @babel/core @babel/cli

安装好在项目中执行babel命令就可以进行转换了。

babel src --out-dir lib # 将src下的文件处理后输出到lib

在构建工具中使用是我们使用babel更常用的方式。

以webpack为例,babel提供了一个loader:babel-loader,使用这个loader我们就能很轻松实现代码转换。

module: {
  rules: [
    {
      test: /\.js$/,
      loader: 'babel-loader',
      options: {
          // 配置项
      }
    }
  ]
}

bable的运行方式:

Babel的运行过程分为三个阶段:解析 --- 转换 --- 生成

  1. 解析:解析代码,并生成AST抽象语法树
  2. 转换:将上一步的AST转换成目标环境支持的AST
  3. 生成:根据转换后的AST生成目标代码

babel配置

babel7支持多种方式来进行配置:

除了json文件之外,babel还支持其他格式的配置文件,比如 .js.cjs(Commonjs)和.mjs(ESModule),它们相对于json格式更加灵活,可以在配置中进行编程处理,但是会使配置无法静态分析,失去可缓存性。

// babel.config.js
module.exports = {
  presets: [],
  plugins: []
};

babel虽然有多种方式来写配置文件,但是配置的内容都是类似的,主要包含pluginspresets两部分,不过这里需要注意几点:

  • 配置格式:plugins和presets配置项均为数组,如果每项不需要配置的话,直接放入数组(值为项的字符串名称);如果要配置的话,则需要把格式改成数组,该数组的第一个元素是该项的字符串名字,第二个元素是该项的配置对象。这里填写的plugins和presets,babel会自动检查是否已被安装在node_modules下面,当然,也可以使用相对路径来使用本地文件。

    "plugins": [
      [
        "component",
        {
          "libraryName": "element-ui",
          "styleLibraryName": "theme-chalk"
        }
      ],
      "@babel/plugin-transform-runtime"
    ]
  • 执行顺序:

    plugins会从前到后执行,presets会从后向前执行,且plugins会比presets先执行

    presets逆序执行的目的是为了保证向后兼容,我们在配置的时候按照规范的时间顺序列出即可,如['es2015', 'stage-1'],这样就会先使用stage-1,否则就会出现错误。

  • 短名称:在配置plugin和preset名字的时候,可以省略名字中的babel-plugin-preset-字样,仅使用插件的名字就可以了。

    {
    "plugins": [
      "myPlugin", // 完整名字:"babel-plugin-myPlugin"
      "@org/myPlugin" // 完整名字:"@org/babel-plugin-myPlugin"
    ],
    "presets":[
      "myPreset", // 完整名字:"babel-preset-myPreset"
      "@babel/myPreset" // 完整名字:@babel/preset-myPreset
    ]
    }

plugin

babel的转换完全依靠插件,babel拥有的能力完全取决于给它配置了哪些插件。

babel的插件分为两种:

  • 语法插件:赋予babel解析特定语法的能力,可以理解一个为babel服务的翻译官,仅做翻译解析工作,工作在AST转换阶段。
  • 转译插件:转译插件即把特定的语法转换成指定标准的语法。

    比如将箭头函数 (x) => x 转换成 function (x) {return x},只需要配置箭头函数转译插件即可。

插件的使用大概分为两步:

  1. 将插件添加到配置文件的plugins属性中,可根据插件文档做相应配置。
  2. 安装插件到项目中,使用npm或者yarn均可

每个插件的功能是单一的,所以一般转换需求要使用的插件数量比较多,这时候可以考虑presets

工具包

@babel/template可以用以字符串形式的代码构建AST树节点,快速优雅开发插件

npm install --save-dev @babel/template

使用

// 引入babel-template
const template = require('babel-template');

// 定义try/catch语句模板
let tryTemplate = `
try {
} catch (e) {
console.log(CatchError:e)
}`;

// 创建模板
const temp = template(tryTemplate);

// 给模版增加key,添加console.log打印信息
let tempArgumentObj = {
   // 通过types.stringLiteral创建字符串字面量
   CatchError: types.stringLiteral('Error')
};

// 通过temp创建try语句的AST节点
let tryNode = temp(tempArgumentObj);

编写插件

和webpack一样,bable可以使用自定义插件实现功能

module.exports = function(babel) {
  let t = babel.type
  return {
    visitor: {
      Identifier(path, state) {
        const name = path.node.name;
        // reverse the name: JavaScript -> tpircSavaJ
        path.node.name = name
          .split("")
          .reverse()
          .join("");
      },
    },
  };
}

实现一个try/catch替换async函数功能

module.exports = function (babel) {
   let t = babel.type
   return { 
     visitor: {
       AwaitExpression(path) {
       
        // 判断父路径中是否已存在try语句,若存在直接返回
        if (path.findParent((p) => p.isTryStatement())) {
          return false;
        }
       
         let node = path.node;
         const asyncPath = path.findParent((p) => p.node.async && (p.isFunctionDeclaration() || p.isArrowFunctionExpression() || p.isFunctionExpression() || p.isObjectMethod()));
         
         let tryNode = temp(tempArgumentObj);
         
         // 获取父节点的函数体body
         let info = asyncPath.node.body;

         // 将函数体放到try语句的body中
         tryNode.block.body.push(...info.body);

         // 将父节点的body替换成新创建的try语句
         info.body = [tryNode];
       }
     }
   }
 }

https://juejin.cn/post/7155434131831128094#heading-17

preset

在实际的开发中,我们如果用ES6来进行开发的话,那需要转换的语法是非常多的,难道要一个一个去配置吗?还要不要配陪妹子了?

这个时候,就该presets登场了。presets可以理解为一组插件的集合,就像你去德克士直接点一个全家桶,而不必一个一个给服务员说要鸡腿、鸡翅、薯条...(如果想和服务员小姐姐多待一会,也不是不可以)

  • 配置文件:通过配置文件的targets属性指定目标环境

    {
    "presets": [
      ["env", {
        "targets": {
          "browsers": ["last 2 versions", "safari >= 7"],
          "node": "12.10"
        }
      }]
    ]
    }

    browserslistrc:如果是浏览器或者Electron,官方推荐使用.browserslistrc

    文件来指定目标环境(可以和autoprefixer、stylelint等其他工具共享配置)。

    不过如果在配置文件中设置了 targetsignoreBrowserslistConfig.browserslistrc中配置的内容将不会生效。

presets中的其他配置:

  • modules: 用于指定转换后的模块规范,可设置为"amd" | "umd" | "systemjs" | "commonjs" | "cjs" | "auto" | false,默认为"auto"
  • useBuiltIns:此选项配置如何@babel/preset-env处理polyfill,在后面介绍polyfill我们会详细介绍。
  • corejs:用于指定corejs的版本,仅当使用了useBuiltIns: usage或者

    useBuiltIns: entry此配置才有效,使用时一定要配置正确的版本,否则会报错。

    当前默认的版本是2.0,不过建议使用3.0版本,因为2.0已经不会再添加新的特性了,如果使用了一些较新的语法,将会无法转换。

@babel/preset-env

这个是我们最常用的,也是官方现在推荐的preset,它是一个智能预设,我们无需再关心特定环境下所需的转换插件(只需要指定目标环境),就可以使用最新的语法。除了能够减少使用难度,还能够使转换出来的代码体积更小。

preset-env会根据配置的目标环境中缺少的功能,选择对应的插件进行必要的转换和polyfill,而目标环境支持的特性就不会转换,而不是无脑的一把梭哈。

polyfill

前面我们的配置其实是不完美的,转换出来的代码直接在浏览器运行是可能会出问题的。这是因为babel默认只转换了语法,而对于一些新的API是并没有处理的,如Generator、Set、Maps、Proxy、Promise 等全局对象及其方法都不会转码,如果我们在代码中使用到了就会报错。

此时,我们就需要做Polyfill,也就是所谓的'垫片',作用就是把浏览器的差异垫平,人话就是通过模拟为这些浏览器补全缺失的全局对象及API。

常用插件

@babel/plugin-transform-runtime 是一个可重复使用Babel注入的帮助程序的插件,避免重复注入,以节省代码大小。通常还要搭配@babel/runtime一起使用。

安装

npm install --save-dev @babel/plugin-transform-runtime 
npm install --save @babel/runtime # 提供缺失的特性,须要在生产环境中安装

配置

// babel.config.json
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "usage",
        "corejs": 3
      }
    ]
  ],
  "plugins": [["@babel/plugin-transform-runtime"]]
}

Babel-plugin-import

使用 antd 时, 需要加载组件的样式 antd/dist/antd.css, 但是我们大部分时候不需要使用到antd所有的组件, 更不需要载入所有组件的样式。

所以antd提供了按需引入

import Button from 'antd/lib/button';
import 'antd/lib/button/style';

babel-plugin-import能够帮助我们在引入组件的时候自动加载相关样式。,从而不必全局引入antd、lodash等

安装

npm install babel-plugin-import --save-dev

在babelrc中配置

'plugins': [
	['import',{'libraryName': 'antd', 'style': true,'libraryDirectory': 'lib'},'antd']
	['import',{'libraryName': 'antd-mobile', 'libraryDirectory': 'lib'},'antd-mobile']
]

Surcause

sucrase支持的类型

jsx => 转换jsx/tsx

typescript => 将 ts -> js

Flow => check types

imports => cjs

react-hot-loader => 执行react-hot-loader项目中的等效转换

jest => 提升期望的jest方法调用上面的导入方式与babel-plugin-jest-hoist相同

安装

yarn add --dev sucrase  # Or npm install --save-dev sucrase
node -r sucrase/register main.ts

使用

yarn add --dev sucrase ts-node typescript
./node_modules/.bin/ts-node --transpiler sucrase/ts-node-plugin main.ts

在ts-node中使用

ts-node --transpiler sucrase/ts-node-plugin

使用

import {transform} from "sucrase";
const compiledCode = transform(code, {transforms: ["typescript", "imports"]}).code;

bunldephobia

检查引入的node包打包之后所占的体积

https://bundlephobia.com/

https://github.com/pastelsky/bundlephobia

SWC

swc是一个类似babel的JavaScript/TypeScript编译器,但是由于swc使用Rust实现,所以具备比babel出色的性能。

webpack与bable的性能瓶颈都在于JS语言,出现了go实现的esbuild与Rus实现的swc等工具,esbuild想要开辟一个构建工具性能的新时代,创建一个易用的现代打包器,swc的目标则是是替代babel,在官方文档的Comparison with babel这个单元,我们可以看到swc团队将相当一部分bable的插件重写了以增强swc的能力。

对比 babel,swc 有至少 10 倍以上的性能优势

同时swc也提供了一个构建打包工具spack,这个工具并不是一个全面的构建工具,但是我们可以通过这个工具简单的体验一下swc的能力

我们在简单的工具库编写时,可以使用swc提供的spack命令来快速打包出commonjs, es6, amd, umd

安装/使用

安装

npm i -D @swc/core @swc/cli
## yarn add --dev @swc/core @swc/cli

在项目的根目录创建一个 .swcrc 文件,配置信息如下

[
  {
    "jsc": { // 编译规则 
      "target": "es5", // 输出js的规范 
      "parser": { 
        // 除了 ecmascript,还支持 typescript 
        "syntax": "ecmascript", 
        // 是否解析jsx,对应插件 @babel/plugin-transform-react-jsx 
        "jsx": false, 
        // 是否支持装饰器,对应插件 @babel/plugin-syntax-decorators 
        "decorators": false, 
        // 是否支持动态导入,对应插件 @babel/plugin-syntax-dynamic-import 
        "dynamicImport": false, 
        // …… 
        // babel 的大部分插件都能在这里找到对应配置 
      }, 
      "minify": {}, // 压缩相关配置,需要先开启压缩 
    },
    "env": {
      "targets": {
        "chrome": "58",
        "ie": "11"
      }
    }
    "minify": true // 是否开启压缩 
  }
]

配置spack

const { config } = require("@swc/core/spack");

module.exports = config({
  //入口文件
  entry: __dirname + "/src/index.ts",
  //输出
  output: {
    path: __dirname + "/dist",
  },
  // swc编译配置
  options:{
    jsc: {
        //解析配置
        parser: {
          syntax: "typescript", //输入文件格式
          tsx: false,    // 是否支持tsx
          dynamicImport: false, //是否支持动态导入
          decorators: false, //是否支持装饰器
        },
        transform: null,
        target: "es5", //转译目标
        loose: false, 
        externalHelpers: false,
        keepClassNames: false
      },
      // 输出文件配置
      module: {
          type: "commonjs",  
          strict: false,
          strictMode: true,
          lazy: false,
          noInterop: false,
          ignoreDynamic: false
        }
  }
});

在package.json中增加

"scripts": {
  "build": "spack"
}

与webpack一起使用

SWC 也提供了 Webpack Loder,可以和 Webpack 一起使用

安装 @swc/coreswc-loader

cnpm install --save-dev @swc/core swc-loader

安装完成后在 Webpack 的配置文件中添加 SWC 的 Loder 配置

const path = require('path');

module.exports = {
  //  入口文件
  entry: './src/main.js',
  output: {
    //  打包完成后输出的目录
    path: path.resolve(__dirname, 'dist'),
    //  打包完成后的 JS 文件名
    filename: 'index.js',
  },
  module: {
    rules: [
      //  SWC 的 Loder 配置
      {
        test: /\.m?js$/,
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: 'swc-loader',
        },
      },
    ],
  },
};

SWC 的配置还是和上面一样的使用 .swcrc 的配置

运行打包后 swc-loder 就会调用 swc-core 转码

SWC 和 Webpack 一起使用也还是不能转换 ES6 中新增的 Promise 之类的功能,所以还需要使用 polyfill 或 core-js

因为 core-js 可以按需引入,而 polyfill 需要配合 Babel 才能实现按需引入,所以这里就使用 core-js

cnpm install --save-dev core-js

安装完成后在 main.js 中引入需要的库,比如我用到了 Promise ,我就需要引入 core-js 的 Promise 转换库

import 'core-js/features/promise';

core-js 的转换模块就在 core-js/features 目录中

打包完成后就可以在 IE 浏览器中使用 Promise 之类的功能了

Rome

Rome是 Babel 作者做的基于 Nodejs 的前端基建全家桶,包含但不限于 Babel, ESLint, webpack, Prettier, Jest。

rome 是个全家桶 API,所以你只需要 yarn add rome 就完成了所有环境准备工作。

  • rome bundle 打包项目。
  • rome compile 编译单个文件。
  • rome develop 调试项目。
  • rome parse 解析文件抽象语法树。
  • rome analyzeDependencies 分析依赖。

Rome 还将文件格式化与 Lint 合并为了 rome check 命令,并提供了友好 UI 终端提示

如果你觉得我的文章对你有帮助的话,希望可以推荐和交流一下。欢迎關注和 Star 本博客或者关注我的 Github