通用npm包(组件库)打包构建
一、背景
现在的前端开发,绕不开使用一些第三方的NPM包。同时,很多公司不论大小都有了自己的NPM仓库,然后发布一些只有自己使用的私有NPM包。我们在发包的时候,包的代码怎么构建才能体积最小,同时做到新能最好就成了一个非常重要的问题。
在一些发包实现中,无论是UI包还是纯粹的TypeScript Utils包,你当然都可以不打包而直接将代码放到npm包的dist目录中,然后以来业务工程进行构建。但是这显然是有问题的,因为这些NPM包都是比较稳定的代码,不像业务代码那样需要进行频繁的修改和调试了,直接暴露源码给业务使用,会使这些包在不同的项目中多次重复构建,无疑会拉长业务开发时热更新和生产构建时间。
二、如何解决
就像背景中提到的,一般我们的NPM包可以分为两类,分别是:
- 纯TypeScript(javascript)类型,只包含逻辑代码
- UI类,包含VUE相关代码的组件,根据vue的2和3的版本不同,又可以细分分别适用于vue2和vue3的两个小类
在分为以上以上两类的同时,还有一种情况需要考虑下,就是有些C端的产品,可能会进行服务端渲染优化。因此我们需要考虑到这些包可能会在Node环境中运行,甚至一些utils包就是直接运行在Node环境中的。所以这个时候我们就需要一份已与CommonJS规范的导出。另外,我们有些工具包还要考虑是否开源出去的情况,如果考虑开源的话可能还需要一份基于UMD规范的导出。
所以,根据以上情况,我们需要多份打包脚本配置。
使用TSC打包Typescript为ESModule、CommonJS及UMD格式
package.json
{ // 下列内容只包含了打包构建用到的配置,其他字段用到的请自行补充 "main": "dist/main/index.js", "typings": "dist/types/index.d.ts", "module": "dist/module/index.js", "unpkg": "dist/umd/index.js", "exports": { ".": { "types": "./dist/main/index.d.ts", "import": "./dist/module/index.js", "require": "./dist/main/index.js", "browser": "./dist/umd/index.js" } }, "scripts": { "prepublish": "pnpm build", "build": "run-p build:*", "build:main": "tsc -p ./pack/tsconfig.json", "build:module": "tsc -p ./pack/tsconfig.module.json", "build:umd": "tsc -p ./pack/tsconfig.umd.json", "watch:module": "tsc -p ./pack/tsconfig.module.json -w", "watch:main": "tsc -p ./pack/tsconfig.json -w" }, "files": [ "dist", "src" ], "devDependencies": { "@types/node":"^18.13.0", "typescript": "^4.9.5", "npm-run-all": "^4.1.5" } }
1.基本配置&生成基于CommonJS规范的导出
pack/tsconfig.json
{ "compilerOptions": { "target": "ES6", // 编译目标版本 "outDir": "../dist/main", // 输出目录 "rootDir": "../src", // 根目录 "moduleResolution": "nodenext", // 如何处理模块 "module": "commonjs", // 生成的模块系统代码 "declaration": true, // 生成相应的 .d.ts文件 "declarationDir":"../dist/types", // 生成相应的 .d.ts文件的存放目录 "inlineSourceMap": true, // 生成单个的sourcemaps文件 "esModuleInterop": true, // https://www.typescriptlang.org/tsconfig#esModuleInterop "resolveJsonModule": true, // 允许导入json文件 "strict": false, // 不使用严格模式 "noUnusedLocals": true, // 若有未使用的局部变量则抛错 "noUnusedParameters": true, // 若有未使用的参数则抛错 "noImplicitReturns": true, // 不是函数的所有返回路径都有返回值时报错 "noFallthroughCasesInSwitch": true, // 报告switch语句的fallthrough错误。(即,不允许switch的case语句贯穿) "traceResolution": false, // 不生成模块解析日志信息 "listEmittedFiles": false, // 不打印出编译后生成文件的名字。 "listFiles": false, // 编译过程中不打印文件名。 "pretty": true, // 给错误和消息设置样式,使用颜色和上下文 "lib": ["dom"], // 编译过程中需要引入的库文件的列表 "types": ["node"], // 要包含的类型声明文件名列表 "typeRoots": ["node_modules/@types", "src/types"], // 要包含的类型声明文件路径列表 }, "include": ["../src/**/*.ts"], "exclude": ["**/node_modules/**"], "compileOnSave": true }
2.生成基于ESModule的导出文件
pack/tsconfig.module.json
{ "extends": "./tsconfig", "compilerOptions": { "target": "ES2015", "outDir": "../dist/module", "module": "ES2015", "moduleResolution": "nodenext", "resolveJsonModule": false, // 允许导入json文件 }, }
3.生成基于UMD的导出文件
pack/tsconfig.umd.json
{ "extends": "./tsconfig", "compilerOptions": { "target": "es5", "outDir": "../dist/umd", "module": "UMD", "moduleResolution": "nodenext", "resolveJsonModule": false, // 允许导入json文件 "lib": ["es5", "dom"], // 编译过程中需要引入的库文件的列表 } }
基于Webpack打包通用UI库(Base Vue2)
工程目录结构
package.json
{ "name": "private pkg", "version": "0.0.1-beta.0", "main": "dist/main/index.js", "typings": "dist/types/index.d.ts", "module": "dist/module/index.js", "unpkg": "dist/umd/index.js", "exports": { ".": { "types": "./dist/types/index.d.ts", "import": "./dist/module/index.js", "require": "./dist/main/index.js", "browser": "./dist/umd/index.js" } }, "scripts": { "build": "run-p build:*", "build:esm": "webpack --config pack/webpack.esmodule.conf.js", "build:common": "webpack --config pack/webpack.common.conf.js", "build:types": "vue-tsc --skipLibCheck --declaration --emitDeclarationOnly --declarationDir dist/types" }, "files": [ "dist" ], "devDependencies": { "filter": "^0.1.1", "unplugin-vue-components": "^0.24.0", "vue-template-compiler": "2.7.14", "vue-loader": "^15.10.0", "vue":"^2.7.0", "vue-tsc": "^1.2.0", "npm-run-all": "^4.1.5", "webpack-node-externals": "^3.0.0", "progress-bar-webpack-plugin": "^2.1.0", "esbuild-loader": "^4.0.0", "webpack-merge": "^5.0.0", "css-loader": "^6.8.1", "postcss-loader": "^7.3.3", "sass-loader": "^13.3.2", "style-loader": "^3.3.3", } }
tsconfig.json
{ "compilerOptions": { "target": "ES6", "module": "ES6", "moduleResolution": "nodenext", "strict": false, "sourceMap": true, "importHelpers": true, "experimentalDecorators": true, "noImplicitAny": false, "noImplicitThis": false, "esModuleInterop": true, "jsx": "preserve", "jsxFactory": "VueTsxSupport", "allowJs": true, "declaration": true, "declarationDir": "./dist/types", "skipLibCheck": true, "types": [ "webpack-env", "vue" ] }, "include": [ "shims-vue.d.ts", "./index.ts" ], "exclude": [ "node_modules/**/*" ], "vueCompilerOptions": { "target": 3 } }
webpack.base.conf.js
const ProgressBarPlugin = require('progress-bar-webpack-plugin'); const {VueLoaderPlugin} = require('vue-loader'); const webpackConf = { mode: 'production', entry: { app: ['./index.ts'] }, module: { rules: [ { test: /\.vue$/, loader: 'vue-loader', options: { compilerOptions: { preserveWhitespace: false } } }, { test: /\.(j|t)sx?$/, exclude: /(node_modules)/, use: { loader: 'esbuild-loader', options: { loader: 'tsx', target: 'esnext' // Syntax to compile to (see options below for possible values) } } } ] }, optimization: { minimize: true }, resolve: { extensions: ['.js', '.vue', '.json', '.ts', '.tsx', '.scss'], modules: ['node_modules'], alias: { '@': './src' } }, stats: 'normal', plugins: [ new VueLoaderPlugin(), new ProgressBarPlugin({ complete: 'O', incomplete: '=' }) ] }; module.exports = webpackConf;
webpack.common.conf.js
const baseWebpackConf = require('./webpack.base.conf.js'); const {merge} = require('webpack-merge'); const nodeExternals = require('webpack-node-externals'); const path = require('path'); const webpackConf = { output: { path: path.resolve(process.cwd(), './dist/main'), filename: `index.js`, chunkFilename: '[id].js', library: { type: 'commonjs2' } }, module: { rules: [ { test: /\.(sa|sc|c)ss$/i, use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader'] } ] }, externals: [nodeExternals()] }; module.exports = merge(baseWebpackConf, webpackConf);
webpack.esmodule.conf.js
const baseWebpackConf = require('./webpack.base.conf.js'); const {merge} = require('webpack-merge'); const path = require('path'); const webpackConf = { output: { path: path.resolve(process.cwd(), './dist/module'), filename: `index.js`, chunkFilename: '[id].js', library: { type: 'module' } }, experiments: { outputModule: true }, module: { rules: [ { test: /\.(sa|sc|c)ss$/i, use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader'] } ] }, externals: [ { vue: 'vue', 'vue-property-decorator': 'vue-property-decorator', //这里很重要,冗余的其他库都需要在这里配置external掉 } ] }; module.exports = merge(baseWebpackConf, webpackConf);
webpack.umd.conf.js
const baseWebpackConf = require('./webpack.base.conf.js'); const {merge} = require('webpack-merge'); const webpackConf = { output: { path: path.resolve(process.cwd(), './dist/module'), filename: `${pkgName}.umd.js`, chunkFilename: '[id].js', library: { name: `${pkgName}`, type: 'umd', export: 'default' } }, externals: [ { vue: 'vue' } ] }; module.exports = merge(baseWebpackConf, webpackConf);
基于Webpack打包通用UI库(Base Vue3)
工程目录结构同上
package.json
{ "name": "", "version": "0.0.1", "main": "dist/main/index.js", "typings": "dist/types/index.d.ts", "module": "dist/module/index.js", "unpkg": "dist/umd/index.js", "exports": { ".": { "types": "./dist/types/index.d.ts", "import": "./dist/module/index.js", "require": "./dist/main/index.js", "browser": "./dist/umd/index.js" } }, "scripts": { "build": "run-p build:*", "build:esm": "webpack --config pack/webpack.esmodule.conf.js", "build:common": "webpack --config pack/webpack.common.conf.js", "build:types": "vue-tsc --skipLibCheck --declaration --emitDeclarationOnly --declarationDir dist/types" }, "files": [ "dist" ], "devDependencies": { "@vue/compiler-sfc": "^3.2.47", "filter": "^0.1.1", "unplugin-vue-components": "^0.24.0", "vue": "^3.2.47", "vue-loader": "^17.0.0", "vue-tsc": "^1.2.0", "npm-run-all": "^4.1.5", "webpack-node-externals": "^3.0.0", "progress-bar-webpack-plugin": "^2.1.0", "esbuild-loader": "^4.0.0", "webpack-merge": "^5.0.0", "css-loader": "^6.8.1", "postcss-loader": "^7.3.3", "sass-loader": "^13.3.2", "style-loader": "^3.3.3", } }
tsconfig.json
{ "compilerOptions": { "target": "ES6", "module": "ES6", "moduleResolution": "nodenext", "strict": false, "sourceMap": true, "importHelpers": true, "experimentalDecorators": true, "noImplicitAny": false, "noImplicitThis": false, "esModuleInterop": true, "jsx": "preserve", "jsxFactory": "VueTsxSupport", "allowJs": true, "declaration": true, "declarationDir": "./dist/types", "skipLibCheck": true, // "baseUrl": ".", // "outDir": "./dist/types", "types": [ "webpack-env", "vue" ] }, "include": [ "shims-vue.d.ts", "./index.ts" ], "exclude": [ "node_modules/**/*" ], "vueCompilerOptions": { "target": 3 } }
webpack.base.conf.js
const ProgressBarPlugin = require('progress-bar-webpack-plugin'); const {VueLoaderPlugin} = require('vue-loader'); const webpackConf = { mode: 'production', entry: { app: ['./index.ts'] }, module: { rules: [ { test: /\.vue$/, loader: 'vue-loader', options: { compilerOptions: { preserveWhitespace: false } } }, { test: /\.(j|t)sx?$/, exclude: /(node_modules)/, use: { loader: 'esbuild-loader', options: { loader: 'tsx', target: 'esnext' // Syntax to compile to (see options below for possible values) } } } ] }, optimization: { minimize: false }, resolve: { extensions: ['.js', '.vue', '.json', '.ts', '.tsx', '.scss'], modules: ['node_modules'], alias: { '@': './src' } }, stats: 'normal', plugins: [ new VueLoaderPlugin(), new ProgressBarPlugin({ complete: 'O', incomplete: '=' }) ] }; module.exports = webpackConf;
webpack.common.conf.js
const baseWebpackConf = require('./webpack.base.conf.js'); const {merge} = require('webpack-merge'); const nodeExternals = require('webpack-node-externals'); const path = require('path'); const webpackConf = { output: { path: path.resolve(process.cwd(), './dist/main'), filename: `index.js`, chunkFilename: '[id].js', library: { type: 'commonjs2' } }, module: { rules: [ { test: /\.(sa|sc|c)ss$/i, use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader'] } ] }, externals: [nodeExternals()] }; module.exports = merge(baseWebpackConf, webpackConf);
webpack.module.conf.js
const baseWebpackConf = require('./webpack.base.conf.js'); const {merge} = require('webpack-merge'); const path = require('path'); const webpackConf = { output: { path: path.resolve(process.cwd(), './dist/module'), filename: `index.js`, chunkFilename: '[id].js', library: { type: 'module' } }, experiments: { outputModule: true }, module: { rules: [ { test: /\.(sa|sc|c)ss$/i, use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader'] } ] }, externals: [ { vue: 'vue' // 这里很重要,冗余的其他库都需要在这里配置external掉 } ] }; module.exports = merge(baseWebpackConf, webpackConf);
webpack.umd.conf.js
const baseWebpackConf = require('./webpack.base.conf.js'); const {merge} = require('webpack-merge'); const webpackConf = { output:{ path: path.resolve(process.cwd(), './dist/module'), filename: `${pkgName}.umd.js`, chunkFilename: '[id].js', library:{ name:`${pkgName}`, type:'umd', export: 'default', } }, externals:[{ vue: 'vue' }] } module.exports = merge(baseWebpackConf,webpackConf)
文章看到这里了,如果你觉得对你的打包工作有启发的话,请点个赞吧
#webpack##架构开发##npm##组件库#