通用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)
文章看到这里了,如果你觉得对你的打包工作有启发的话,请点个赞吧
