记录使用typescript配合webpack打包一个javascript library的配置过程.
目标是构建一个可以同时支持 CommonJs , esm , amd 这个几个js模块系统的javascript库, 然后还有一个单独打包出一个css的样式文件的需求.
为此以构建一个名为 loaf 的javascript库为例; 首先新建项目文件目录 loaf , 并进入此文件目录执行 npm init 命令, 然后按照控制台的提示输入对应的信息, 完成后就会在loaf目录下得到一个 package.json 文件
然后使用 npm i 命令安装所需的依赖
npm i -D webpack webpack-cli typescript babel-loader @babel/core @babel/preset-env @babel/preset-typescript ts-node @types/node @types/webpack mini-css-extract-plugin css-minimizer-webpack-plugin less less-loader terser-webpack-plugin
依赖说明
webpack webpack-cli: webpack打包工具和webpack命令行接口 typescript: 用于支持typescript语言 babel-loader @babel/core @babel/preset-env @babel/preset-typescript: babel相关的东西, 主要是需要babel-loader将编写的typescript代码转译成es5或es6已获得更好的浏览器兼容性 ts-node @types/node @types/webpack: 安装这几个包是为了能用typescript编写webpack配置文件(webpack.config.ts) mini-css-extract-plugin less less-loader: 编译提取less文件到单独的css文件的相关依赖 css-minimizer-webpack-plugin terser-webpack-plugin: 用于最小化js和css文件尺寸的webpack插件
入口文件
通常使用 index.ts 作为入口, 并将其放到 src 目录下, 由于有输出样式文件的需求, 所以还要新建 styles/index.less
mkdir src && touch src/index.ts mkdir src/styles && touch src/styles/index.less
tsconfig配置
新建 tsconfig.json 文件
touch tsconfig.json
填入以下配置(部分选项配有注释):
{ "compilerOptions": { "outDir": "dist/lib", "sourceMap": false, "noImplicitAny": true, "module": "commonjs", // 开启这个选项, 可以让你直接通过`import`的方式来引用commonjs模块 // 这样你的代码库中就可以统一的使用import导入依赖了, 而不需要另外再使用require导入commonjs模块 "esModuleInterop": true, // 是否允许合成默认导入 // 开启后, 依赖的模块如果没有导出默认的模块 // 那么typescript会帮你给该模块自动合成一个默认导出让你可以通过默认导入的方式引用该模块 "allowSyntheticDefaultImports": true, // 是否生成`.d.ts`的类型声明文件 "declaration": true, // 输出的目标js版本, 这里用es6, 然后配和babel进行转译才以获得良好的浏览器兼容 "target": "es6", "allowJs": true, "moduleResolution": "node", "lib": ["es2015", "dom"], "declarationMap": true, // 启用严格的null检查 "strictNullChecks": true, // 启用严格的属性初始化检查 // 启用后类属性必须显示标注为可空或赋一个非空的初始值 "strictPropertyInitialization": true }, "exclude": ["node_modules"], "include": ["src/**/*"] }
webpack配置文件
创建 webpack.config.ts
touch webpack.config.ts
webpack.config.ts
import path from "path"; import { Configuration, Entry } from "webpack"; import MiniCssExtractPlugin from 'mini-css-extract-plugin'; import CssMinimizer from 'css-minimizer-webpack-plugin'; import TerserPlugin from 'terser-webpack-plugin' const isProd = process.env.NODE_ENV === 'production'; /** * 这里用到了webpack的[Multiple file types per entry](https://webpack.js.org/guides/entry-advanced/)特性 * 注意`.less`入口文件必须放在`.ts`文件前 */ const entryFiles: string[] = ['./src/styles/index.less', './src/index.ts']; const entry: Entry = { index: entryFiles, 'index.min': entryFiles, }; const config: Configuration = { entry, optimization: { minimize: true, minimizer: [ new TerserPlugin({ test: /.min.js$/ }), new CssMinimizer({ test: /.min.css$/, }), ], }, module: { rules: [ { test: /.ts$/, loader: 'babel-loader', exclude: /node_modules/, options: { presets: ['@babel/env', '@babel/typescript'], }, }, { test: /.less$/, use: [ isProd ? MiniCssExtractPlugin.loader : 'style-loader', { loader: 'css-loader', }, 'postcss-loader', 'less-loader', ], }, ], }, output: { path: path.resolve(__dirname, 'dist/umd'), library: { type: 'umd', name: { amd: 'loaf', commonjs: 'loaf', root: 'loaf', }, }, }, resolve: { extensions: ['.ts', '.less'], }, devtool: 'source-map', plugins: [ new MiniCssExtractPlugin({ filename: '[name].css', }), ], }; export default config;
webpack入口文件配置
... const isProd = process.env.NODE_ENV === 'production'; /** * 这里用到了webpack的[Multiple file types per entry](https://webpack.js.org/guides/entry-advanced/)特性 * 注意`.less`入口文件必须放在`.ts`文件前 */ const entryFiles: string[] = ['./src/styles/index.less', './src/index.ts']; const entry: Entry = { index: entryFiles, 'index.min': entryFiles, }; const config: Configuration = { entry, ... } ...
在上面的 webpack.config.json 中,我们配置了两个入口分别是 index 和 index.min ,不难看出,多出的一个 index.min 入口是为了经过压缩后js和css文件,在生产环境使用一般都会使用 .min.js 结尾的文件以减少网络传输时的尺寸; 实现这个还需要结合 optimization 相关配置, 如下:
optimization: { minimize: true, minimizer: [ new TerserPlugin({ test: /.min.js$/ }), new CssMinimizer({ test: /.min.css$/, }), ], },
另外, index 和 index.min 的值都是相同的 entryFiles 对象,这个对象是一个字符串数组,里面放的就是我们的入口文件相对路径,这里一定要注意把 ./src/styles/index.less 置于 ./src/index.ts 之前。
webpack为typescript和less文件配置各自的loader
配置完入口后, 就需要为typescript和less代码配置各自的loader
module: { rules: [ { test: /.ts$/, loader: 'babel-loader', exclude: /node_modules/, options: { presets: ['@babel/env', '@babel/typescript'], }, }, { test: /.less$/, use: [ isProd ? MiniCssExtractPlugin.loader : 'style-loader', { loader: 'css-loader', }, 'postcss-loader', 'less-loader', ], }, ], },mini-css-extract-plugin less less-loader : 编译提取less文件到单独的css文件的相关依赖
上面的配置为.ts结尾的文件配置了 babel-loader ; 为 .less 结尾的文件配置一串loader, 使用了 use , use中的loader的执行顺序是从后往前的, 上面less的配置就是告诉webpack遇到less文件时, 一次用 less-loader -> postcss-loader -> css-loader -> 生产环境用 MiniCssExtractPlugin.loader() 否则用 style-loader ;
MiniCssExtractPlugin.loader 使用前要先在 plugins 进行初始化
... const config = { ... plugins: [ new MiniCssExtractPlugin({ filename: '[name].css', }), ], ... } ...
webpack的output配置
... const config = { ... output: { path: path.resolve(__dirname, 'dist/umd'), library: { type: 'umd', name: { amd: 'loaf', commonjs: 'loaf', root: 'loaf', }, }, }, ... } ...
这里配置webpack以umd的方式输出到相对目录 dist/umd 目录中, umd 是 Universal Module Definition (通用模块定义)的缩写, umd格式输出library允许用户通过 commonjs , AMD , <script src="..."> 的方式对library进行引用 config.library.name 可以为不同的模块系统配置不同的导出模块名供客户端来进行引用; 由于这里的导出模块名都是 loaf , 所以也可以直接 config.library.name 设置成 loaf .
运行webpack进行打包
现在回到最开始通过 npm init 生成的 package.json 文件, 在修改其内容如下
{ "name": "loaf", "version": "1.0.0", "description": "A demo shows how to create & build a javascript library with webpack & typescript", "main": "index.js", "scripts": { "build:umd": "webpack -c webpack.config.ts --node-env production --env NODE_ENV=production", "test": "npm run test" }, "keywords": [ "demo" ], "author": "laggage", "license": "MIT", "devDependencies": { "@babel/core": "^7.18.6", "@babel/preset-env": "^7.18.6", "@babel/preset-typescript": "^7.18.6", "@types/node": "^18.0.0", "@types/webpack": "^5.28.0", "babel-loader": "^8.2.5", "css-loader": "^6.7.1", "css-minimizer-webpack-plugin": "^4.0.0", "less": "^4.1.3", "less-loader": "^11.0.0", "mini-css-extract-plugin": "^2.6.1", "postcss-loader": "^7.0.0", "style-loader": "^3.3.1", "terser-webpack-plugin": "^5.3.3", "ts-node": "^10.8.2", "typescript": "^4.7.4", "webpack": "^5.73.0", "webpack-cli": "^4.10.0" } }
新增了一个脚本命令 "build:umd": "webpack -c webpack.config.ts --node-env production --env NODE_ENV=production" , 然后命令行到项目目录下执行 npm run build:umd , 不出意外应该就构建成功了, 此时生成的dist目录结构如下
dist └── umd ├── index.css ├── index.js ├── index.js.map ├── index.min.css ├── index.min.js └── index.min.js.map 1 directory, 6 files
测试验证
新建 demo.html 进行测试
mkdir demo && touch demo/demo.html
demo/demo.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> </head> <body> <script src="dist/umd/index.js"></script> <script type="text/javascript"> console.log(loaf, '\n', loaf.Foo) </script> </body> </html>
用浏览器打开 demo.html , 然后F12打开控制台, 可以看到如下输出则说明初步达成了目标:
Module {__esModule: true, Symbol(Symbol.toStringTag): 'Module'} demo.html:13 ? Foo() { var _bar = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : new Bar(); src_classCallCheck(this, Foo); this._bar = _bar; }
输出esm模块
完成上面的步骤后, 我们已经到了一个umd模块的输出, 相关文件都在 dist/umd 目录下; 其中包含可供 CommonJs ESM AMD 模块系统和 script标签 使用的umd格式的javascript文件和一个单独的css样式文件.
已经输出了umd格式的js了, 为什么还要输出esm模块? ----TreeShaking
Tree shaking is a term commonly used in the JavaScript context for dead-code elimination. It relies on the static structure of ES2015 module syntax, i.e. import and export. The name and concept have been popularized by the ES2015 module bundler rollup.
此库的使用者也使用了类似webpack之类的支持 Tree Shaking
的模块打包工具,需要让使用者的打包工具能对这个js库 loaf 进行死代码优化 Tree Shaking
从webpack文档中看出, tree-shaking依赖于ES2015( ES2015 module syntax , ES2015=ES6)的模块系统, tree-shaking可以对打包体积有不错优化, 所以为了支持使用者进行 tree-shaking , 输出esm模块(esm模块就是指 ES2015 module syntax)是很有必要的.
用tsc输出esm和类型声明文件
tsc -p tsconfig.json --declarationDir ./dist/typings -m es6 --outDir dist/lib-esm
上面的命令使用typescript编译器命令行接口 tsc 输出了ES6模块格式的javascript文件到 dist/lib-esm 目录下
将这个目录加入到 package.json 的 scripts 配置中:
package.json
{ "name": "loaf", "version": "1.0.0", "description": "A demo shows how to create & build a javascript library with webpack & typescript", "main": "index.js", "scripts": { "build:umd": "webpack -c webpack.config.ts --node-env production --env NODE_ENV=production", "build:lib-esm": "tsc -p tsconfig.json --declarationDir ./dist/typings -m es6 --outDir dist/lib-esm", "test": "npm run test" }, "keywords": [ "demo" ], "author": "laggage", "license": "MIT", "devDependencies": { "@babel/core": "^7.18.6", "@babel/preset-env": "^7.18.6", "@babel/preset-typescript": "^7.18.6", "@types/node": "^18.0.0", "@types/webpack": "^5.28.0", "babel-loader": "^8.2.5", "css-loader": "^6.7.1", "css-minimizer-webpack-plugin": "^4.0.0", "less": "^4.1.3", "less-loader": "^11.0.0", "mini-css-extract-plugin": "^2.6.1", "postcss-loader": "^7.0.0", "style-loader": "^3.3.1", "terser-webpack-plugin": "^5.3.3", "ts-node": "^10.8.2", "typescript": "^4.7.4", "webpack": "^5.73.0", "webpack-cli": "^4.10.0" } }
然后运行: npm run build:lib-esm , 此时dist目录结构如下:
dist ├── lib-esm │ ├── bar.js │ └── index.js ├── typings │ ├── bar.d.ts │ ├── bar.d.ts.map │ ├── index.d.ts │ └── index.d.ts.map └── umd ├── index.css ├── index.js ├── index.js.map ├── index.min.css ├── index.min.js └── index.min.js.map 3 directories, 12 files
多出了两个子目录分别为 lib-esm 和 typings , 分别放着es6模块格式的javascript输出文件和typescript类型声明文件.
完善package.json文件
到目前为止, package.json 的scripts配置中, 已经有了 build:umd 和 build:lib-esm 用于构建umd格式的输出和esm格式的输出, 现在我们再向添加一个 build 用来组合 build:umd 和 build:lib-esm 并进行最终的构建, 再次之前先安装一个依赖 shx , 用于跨平台执行一些shell脚本: npm i -D shx ;
更新 package.json 文件:
package.json
{ "name": "loaf", "version": "1.0.0", "description": "A demo shows how to create & build a javascript library with webpack & typescript", "main": "index.js", "scripts": { "build": "shx rm -rf dist/** && npm run build:umd && npm run build:lib-esm", "build:umd": "webpack -c webpack.config.ts --node-env production --env NODE_ENV=production", "build:lib-esm": "tsc -p tsconfig.json --declarationDir ./dist/typings -m es6 --outDir dist/lib-esm", "test": "npm run test" }, "keywords": [ "demo" ], "author": "laggage", "license": "MIT", "devDependencies": { "@babel/core": "^7.18.6", "@babel/preset-env": "^7.18.6", "@babel/preset-typescript": "^7.18.6", "@types/node": "^18.0.0", "@types/webpack": "^5.28.0", "babel-loader": "^8.2.5", "css-loader": "^6.7.1", "css-minimizer-webpack-plugin": "^4.0.0", "less": "^4.1.3", "less-loader": "^11.0.0", "mini-css-extract-plugin": "^2.6.1", "postcss-loader": "^7.0.0", "shx": "^0.3.4", "style-loader": "^3.3.1", "terser-webpack-plugin": "^5.3.3", "ts-node": "^10.8.2", "typescript": "^4.7.4", "webpack": "^5.73.0", "webpack-cli": "^4.10.0" } }
package.json 文件生成typescript声明文件所在的路径(可以参考typescript官网:Including declarations in your npm package):
package.json
{ "name": "loaf", "version": "1.0.0", "description": "A demo shows how to create & build a javascript library with webpack & typescript", "main": "index.js", "typings": "./typings", "scripts": { "build": "shx rm -rf dist/** && npm run build:umd && npm run build:lib-esm", "build:umd": "webpack -c webpack.config.ts --node-env production --env NODE_ENV=production", "build:lib-esm": "tsc -p tsconfig.json --declarationDir ./dist/typings -m es6 --outDir dist/lib-esm", "test": "npm run test" }, "keywords": [ "demo" ], "author": "laggage", "license": "MIT", "devDependencies": { "@babel/core": "^7.18.6", "@babel/preset-env": "^7.18.6", "@babel/preset-typescript": "^7.18.6", "@types/node": "^18.0.0", "@types/webpack": "^5.28.0", "babel-loader": "^8.2.5", "css-loader": "^6.7.1", "css-minimizer-webpack-plugin": "^4.0.0", "less": "^4.1.3", "less-loader": "^11.0.0", "mini-css-extract-plugin": "^2.6.1", "postcss-loader": "^7.0.0", "shx": "^0.3.4", "style-loader": "^3.3.1", "terser-webpack-plugin": "^5.3.3", "ts-node": "^10.8.2", "typescript": "^4.7.4", "webpack": "^5.73.0", "webpack-cli": "^4.10.0" } }
package.json中添加 exports 配置声明模块导出路径
package.json 中的 exports 字段用于告诉使用者引用此库时从哪里寻找对应的模块文件. 比如使用者可能通过esm模块引用此库:
import { Foo } from 'loaf'; const foo = new Foo();
此时如果我们的package.json中没有指定 exports 字段, 那么模块系统会去寻找 node_modules/index.js , 结果肯定是找不到的, 因为我们真正的esm格式的输出文件应该是在 node_modules/loaf/lib-esm 中的
于是我们可以这样来配置 exports :
package.json
{ "name": "loaf", "version": "1.0.0", "description": "A demo shows how to create & build a javascript library with webpack & typescript", "main": "index.js", "typings": "./typings", "exports": { "./*": "./lib-esm/*", "./umd/*": "./umd" }, "scripts": { "build": "shx rm -rf dist/** && npm run build:umd && npm run build:lib-esm", "build:umd": "webpack -c webpack.config.ts --node-env production --env NODE_ENV=production", "build:lib-esm": "tsc -p tsconfig.json --declarationDir ./dist/typings -m es6 --outDir dist/lib-esm", "test": "npm run test" }, "keywords": [ "demo" ], "author": "laggage", "license": "MIT", "devDependencies": { "@babel/core": "^7.18.6", "@babel/preset-env": "^7.18.6", "@babel/preset-typescript": "^7.18.6", "@types/node": "^18.0.0", "@types/webpack": "^5.28.0", "babel-loader": "^8.2.5", "css-loader": "^6.7.1", "css-minimizer-webpack-plugin": "^4.0.0", "less": "^4.1.3", "less-loader": "^11.0.0", "mini-css-extract-plugin": "^2.6.1", "postcss-loader": "^7.0.0", "shx": "^0.3.4", "style-loader": "^3.3.1", "terser-webpack-plugin": "^5.3.3", "ts-node": "^10.8.2", "typescript": "^4.7.4", "webpack": "^5.73.0", "webpack-cli": "^4.10.0" } }
用api-extractor提取出干净的 .d.ts
在上面的用tsc输出esm和类型声明文件这一段中, 我们通过tsc命令输出了typescript了类型声明文件到 dist/types 目录下, 这个目录下有两个 .d.ts 文件, 分别是 bar.d.ts 和 foo.d.ts , 通常是希望这些声明文件都在一个文件 index.d.ts 中的, 如果他们分散开了, 以本库为例, 如果我要使用本库中的 Bar 类, 那么我可能需要这样来导入:
import { Bar } from 'loaf/typings/bar';
我不觉得的这种导入方式是好的做法, 理想的导入方式应该像下面这样:
import { Bar } from 'loaf';
所以接下来, 还要引入微软提供的 api-extractor
配置使用API extractor
安装依赖:
npm install -D @microsoft/api-extractor
再全局安装下:
npm install -g @microsoft/api-extractor
生成 api-extractor.json
api-extractor init
稍微修改下 api-extractor.json
<
api-extractor.json
{ ... "scripts": { "build": "shx rm -rf dist/** && npm run build:umd && npm run build:lib-esm && npm run build:extract-api", "build:umd": "webpack -c webpack.config.ts --node-env production --env NODE_ENV=production", "build:lib-esm": "tsc -p tsconfig.json --declarationDir ./dist/typings-temp -m es6 --outDir dist/lib-esm", "build:extract-api": "api-extractor run && shx rm -rf dist/typings-temp", "build:extract-api-local": "shx mkdir -p ./etc && npm run build:lib-esm && api-extractor run -l", "test": "npm run test" }, ... }
更新 package.json
/** * * @internal */ export class Bar { bar() {} }
注意, 这里处理新增了一个 build:extract-api 到scripts配置中, 还修改了 build:lib-esm 的配置, 将其输出的typescript类型声明文件放到了, typings-temp目录中, 最后这个目录是要删除; 还要注意, 每次提交代码到版本管理工具前, 要先运行 npm run build:extract-api-local , 这个命令会生成 ./etc/<libraryName>.api.md 文件, 这个文件是api-extractor生成的api文档, 应该要放到版本管理工具中去的, 以便可以看到每次提交代码时API的变化.
用@internal标注只希望在内部使用的class
比如, 我希望 Bar 类不能被此库的使用者使用, 我可以加上下面这段注释
/** * * @internal */ export class Bar { bar() {} }
然后来看看生成的 index.d.ts 文件:
/** * * @internal */ declare class Bar { bar(): void; } export declare class Foo { private _bar; constructor(_bar?: Bar); foo(): void; loaf(): void; } export { }
可以看出 index.d.ts 文件中虽然declare了 Bar , 但是并未导出 Bar
这个特性是由api-extractor提供的, 更多api-extractor的内容移步官方文档
小结
至此, 我们就可以构建一个可以通过诸如 AMD CommonJs esm 等js模块系统或是使用 script标签 的方式引用的js库了, 主要用到了 webpack typescript api-extractor 这些工具. 完整的示例代码可以访问 github-laggage/loaf 查看.
到此这篇关于typescript+webpack构建一个js库的文章就介绍到这了,更多相关webpack构建js库内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!
查看更多关于使用typescript+webpack构建一个js库的示例详解的详细内容...