TypeScript – Work with JavaScript Library (using esbuild)
前言
JavaScript 早期是没有 Modular 和 Type (类型) 的. 随着这几年的普及, 几乎有维护的 Library 都有 Modular 和 Type 了.
但万一遇到没有 Modular 或者 Type 的 LIbrary 时, 要如何 import 和类型安全的调用它们呢?
这篇就是要讲讲这些麻烦事儿. 以前做 Angular 开发的时候也写过一篇相关的文章, 但那篇是基于 Angular 的, 这篇则底层一点, 只用了 TypeScript 和 esbuild
esbuild 介绍
它是一个 JavaScript / TypeScript 打包工具. 它的特点就是 TMD 的快
Webpack 需要 40秒 打包的内容, 它只需要 0.33秒, 为什么它这么快呢? 可以看这篇 下一个时代的打包工具 esbuild, 主要是利用了 ES Module 和 Go 语言.
但同时它也牺牲了很多东西, 可以看这篇 Esbuild 为什么那么快, 比如: 不支持 ts 类型检查, 无法 transpile 到 es5, 没有 AST 树做扩展等等.
vite, snowpack, Webpack esbuild-loader 都是基于它.
esbuild get started 看这篇就可以了, 超级简单的
The Limitation with TypeScript
参考: Docs – Features that need a type system are not supported
先讲一下 esbuild 对 TypeScript 的一些局限.
1. 不支持 tsconfig.json 中的 emitDecoratorMetadata, 这个是配合反射 (reflect-metadata) 用的. 复杂项目会是好帮手
2. 不支持 declaration, output 无法输出 .d.ts, 做 library 的话不适合用 esbuild
所以如果你遇到以上情况, 那就不能用 esbuild 了 (esbuild-loader 也是不能用)
搭环境
npm i typescript -g npm i esbuild -g mkdir work-with-js-library cd work-with-js-library yarn init tsc --init yarn add typescript --dev yarn add esbuild --dev
for Yarn 3 的话, 请改成 yarn init -2
index.html
<!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> <script src="./bundle.js" defer></script> </head> <body> <h1>Hello World</h1> </body> </html>
package.json
{ "scripts": { "esbuild": "esbuild index.ts --bundle --outfile=bundle.js --watch" } }
p.s 如果有 setup esbuild.js config 的话, 就不使用上面这个 scripe 了, 往下看
for Yarn 3 的话, 不能直接使用 esbuild command
先安装 esbuild plugin
更新: 20-02-2023 从 v0.15.0 以后 native 就直接支持 PnP 了. 不需要在安装 plugin (安装反而会有 bug, 比如 TS + PnP + esbuild + RxJS 会 error)
yarn add @yarnpkg/esbuild-plugin-pnp --dev
再搞一个 esbuild.js config file (虽然 native 支持 PnP 后, 我们不必在安装 plugin 同时也就没有必要弄 config 了, 但 config 在真实开发还是需要的, 比如 file path 等等还是得 config 配置.)
const esbuild = require("esbuild"); const { pnpPlugin } = require("@yarnpkg/esbuild-plugin-pnp"); esbuild .context({ entryPoints: ["./index.ts"], minify: process.env.NODE_ENV === "production", bundle: true, outfile: "./bundle.js", sourcemap: true, plugins: [pnpPlugin()], }) .then((ctx) => { ctx.watch(); });
运行 command 要加 yarn 在前面.
yarn node esbuild.js
Import JavaScript library that have modular and @type
这个是最常见的一种情况, 像 jquery, lodash. 它们都有 modular, 虽然没有直接提供 Type Declarations, 但是在 npm 大家庭 里面能找到.
安装模块和 @types
yarn add jquery yarn add @types/jquery --dev yarn add lodash yarn add @types/lodash --dev
tsconfig 什么都不需要调, 因为 typesRoot 默认就会去 node_modules/@types 里面找类型.
index.ts
import $ from 'jquery'; import _ from 'lodash'; $('h1').css('color', 'red'); _.forEach([1, 2, 3], (index) => $('body').append($(`<h1>${index}</h1>`)));
直接 import 就能调用了. jquery 和 lodash 都是 export default.
效果
npm run esbuild > Open with Live Server
Import no modular and no type JavaScript library
这种 Library 通常已经没用人维护了. 它的使用方式往往是叫你在 html 放 <script>
然后通过 window['library'].anyFunction() 或者全局变量 library.anyFunction() 去调用.
global-js-library.js
window['globalJsLibrary'] = { myVar: 'globalJsLibrary myVar', myFunction: () => { console.log('globalJsLibrary myFunction called'); }, }; const globalJsLibrary = window['globalJsLibrary']; Object.defineProperty(String.prototype, 'firstLetterUppercase', { value() { return this.charAt(0).toUpperCase() + this.substring(1); }, });
没有 modular 也没有类型, 直接通过 window 和全局变量曝露接口.
首先做一个 Type Declarations
global-js-library.d.ts
interface Window { globalJsLibrary: { myVar: string; myFunction: () => void; }; } interface String { firstLetterUppercase(): string; } declare var globalJsLibrary: Window['globalJsLibrary'];
这个文件要放在哪里呢?
by right, 应该放到 ./types 里面, 然后在 tsconfig.json 设置
{ "compilerOptions": { "typeRoots": ["./node_modules/@types", "./src/types"] } }
typeRoots 的默认是 node_modules/@types, override 了记得要补回去.
顺便说一下 types 是指在 typeRoots 里面再选出某一些 type 而已. 比如
"types": ["jquery", "global-js-library"]
但是我发现, 即使不设置 typeRoots, 而且不管 .d.ts 放到哪里, 最终都是可以跑的. 懒得去研究了.
我就放在 .js 隔壁
index.ts
import 'global-js-library';
console.log(globalJsLibrary.myVar);
console.log(window.globalJsLibrary.myVar);
console.log('hello world'.firstLetterUppercase());
有了类型, 就可以调用到了.
注意它的 import 路径是和 jquery, lodash 一样的, 放 module name.
我这个 .js 文件没有放到 node_modules 里面, 但大部分情况这类 library 还是会有 npm 下载的 (除非真的太旧了).
没有在 node_modules 就不可以直接通过 module name import, 因为它不知道 source 在哪里.
这时需要去 tsconfig.json 加上 paths
{ "compilerOptions": { "paths": { "global-js-library": ["./global-js-library.js"] } } }
接着 npm run esbuild > Run with Live Server 就看见效果了.
Import no type JavaScript library
上面的例子比较极端, 连 modular 都没有. 但大部分 library 都是有的, 顶多没有类型而已.
这个例子教你如果写各种类型 export 和如何 import 它们.
no-type-library.js
es2020 版本
export const myVar = 'myVar'; export function myFunction() { console.log('myFunction called'); } export default class { constructor() { this.name = 'Derrick'; } }
UMD 版本
(function (factory) { if (typeof module === "object" && typeof module.exports === "object") { var v = factory(require, exports); if (v !== undefined) module.exports = v; } else if (typeof define === "function" && define.amd) { define(["require", "exports"], factory); } })(function (require, exports) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.myFunction = exports.myVar = void 0; exports.myVar = 'myVar'; function myFunction() { console.log('myFunction called'); } exports.myFunction = myFunction; class default_1 { constructor() { this.name = 'Derrick'; } } exports.default = default_1; });
注: esbuild 只支持 UMD 和 ES Module, 2 种 modular 规范而已. 其它的比如: AMD, System 都是不支持的哦. Github Issue – Support AMD as input format
no-type-library.d.ts
declare module 'no-type-library' { export const myVar = 'myVar'; export function myFunction(): void; export default class { name: string; } }
index.ts
import myClass, { myFunction, myVar } from 'no-type-library'; myFunction(); console.log('myVar', myVar); console.log('myClass', new myClass());
tsconfig.json
{ "compilerOptions": { "paths": { "no-type-library": ["./no-type-library.js"] } } }
如果 .js 在 node_modules 里就不需要 paths
npm run esbuild > Run with Live Server 就看见效果了.
Multiple Entry Points
在做 Web Worker 单元测试时, 需要使用 esbuild 的多路径.
参考:
每一个 entryPoints 都会输出一个 file 在 outdir. 上面这个例子就会创建出 /bundle/index.js 和 /bundle/worker.js
如果 index.ts 内有 import 其它 ts file 那么会被 bundle 起来. worker.ts 内有 import ts 也是会打包. 注: 它没有 shared 的概念, 两条打包路径是独立的
但如果是用 importScripts('xxx.js') 则不会 bundle 哦, 另外, importScripts 需要 declare
declare function importScripts(...urls: string[]): void;
修改 index.html 路径
引入 worker.js