使用vite打包react 组件
vite 文档
新建项目
npm create vite@latest my-app --template react
npm install
npm run dev
开发组件
- src 下新建components文件夹
- 代码如下
// index.tsx
import ShotVideo from "./video";
import { seekVideo } from "./seekVideo";
export { ShotVideo as VideoShotScreen, seekVideo as VideoSeeksScreen };
export type { ScrollShotUrls, ShotVideoProps } from './video'
// seekVideo.ts
export function seekVideo(
video: HTMLVideoElement,
canvas: HTMLCanvasElement,
time: number,
drawWidth: number,
drawHeight: number,
qutZero?: boolean,
) {
let resolve: (arg0: { time: number; img: string }) => void;
const onSeeked = () => {
video?.removeEventListener('seeked', onSeeked);
const context = canvas.getContext('2d');
context?.drawImage(video, 0, 0, drawWidth, drawHeight);
const img = canvas.toDataURL();
resolve?.({ time, img });
};
return new Promise((r) => {
resolve = r;
video?.addEventListener('seeked', onSeeked);
let t = time;
// 很多视频起始帧是无意义的黑屏,默认取 0.1s 开始
if ((t <= 0 && qutZero) || isNaN(t)) {
t = 0.1;
}
// 末尾的帧向下取整到小数点后 1 位,否则有可能指定 currentTime 无效
if (video?.duration - t < 0.1) {
t = Math.floor(t);
}
video.currentTime = parseInt(`${t * 10}`, 10) / 10;
});
}
// video.tsx
import { useEffect, useRef, useState } from 'react'
import styles from './play.module.less'
import { seekVideo } from './seekVideo';
export interface ScrollShotUrls {
time: number;
img: string;
toobarWidth?: number;
toobarHeight?: number;
shotReady?: boolean;
duration: number;
}
const defaultSlideWidth = 750;
const defaultSlideHeight = 44;
export interface ShotVideoProps {
getUrls?: (urls: ScrollShotUrls[]) => void;
slideWidth?: number;
slideHeight?: number;
file?: File
}
const ShotVideo: React.FC<ShotVideoProps> = ({ slideHeight = defaultSlideHeight, slideWidth = defaultSlideWidth, getUrls, file: uploadFile }) => {
const videoRef = useRef<HTMLVideoElement | null>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const [file, setFile] = useState<File | null>(null);
const [urls, setUrls] = useState<ScrollShotUrls[]>([]);
const [videoSrc, setVideoSrc] = useState('');
const [error, setError] = useState(false);
useEffect(() => {
if (uploadFile) {
setFile(uploadFile);
}
}, [uploadFile])
const handleOnChange = (e: any) => {
const file = e.target.files[0];
setFile(file);
}
useEffect(() => {
if (error) {
console.log('error');
}
}, [error]);
useEffect(() => {
startShot();
}, [file, canvasRef?.current, videoRef?.current, slideWidth, slideWidth])
const startShot = () => {
if (!file || !canvasRef?.current || !videoRef?.current) {
return;
}
const video: HTMLVideoElement = videoRef?.current as HTMLVideoElement;
const url = window.URL.createObjectURL(file);
setVideoSrc(url);
video.src = url;
const canvas: HTMLCanvasElement = canvasRef?.current as HTMLCanvasElement;
video.addEventListener('loadeddata', async () => {
const { duration, videoWidth, videoHeight } = videoRef.current as HTMLVideoElement;
if (!duration || !videoWidth || !videoHeight) {
setError(true);
}
canvas.width = videoWidth;
canvas.height = videoHeight;
const toobarHeight = slideHeight;
const toobarWidth = (toobarHeight * videoWidth) / videoHeight;
const toobarCount = Math.ceil(slideWidth / toobarWidth);
const loopcount = duration / (toobarCount - 1);
const drawWidth = toobarWidth * 2;
const drawHeight = toobarHeight * 2;
video.width = drawWidth;
video.height = drawHeight;
video.style.height = `${drawWidth}px`;
video.style.width = `${drawHeight}px`;
canvas.width = drawWidth;
canvas.height = drawHeight;
const shotUrl: ScrollShotUrls[] = [];
for (let i = 0; i < toobarCount; i++) {
try {
const temp = (await seekVideo(video, canvas, Math.min(loopcount * i, duration), drawWidth, drawHeight, true)) as {
time: number;
img: string;
};
shotUrl.push({ ...temp, toobarWidth, toobarHeight, duration });
} catch (error) {
console.error(error);
}
}
setUrls(shotUrl)
if (getUrls && typeof getUrls === 'function') {
getUrls(shotUrl);
}
video.addEventListener('error', () => {
setError(true);
});
});
}
return (
<>
{
uploadFile ? null :
<div>
<div><input type="file" onChange={handleOnChange} />上传</div>
<canvas ref={canvasRef} />
<video className={styles['playContainer']} ref={videoRef} src={videoSrc} />
{
urls?.map((e, i) => {
return <img src={e?.img} key={i} />
})
}
</div>
}
</>
)
}
export default ShotVideo
打包
- 设置vite.config.ts
// npm install @types/node --save-dev
// npm i @rollup/plugin-typescript
import { readFileSync } from 'fs'
import path from 'path'
import { defineConfig } from 'vite'
import typescript from '@rollup/plugin-typescript'
import react from '@vitejs/plugin-react'
const packageJson = JSON.parse(
readFileSync('./package.json', { encoding: 'utf-8' }),
)
const globals = {
...(packageJson?.dependencies || {}),
}
function resolve(str: string) {
return path.resolve(__dirname, str)
}
export default defineConfig({
plugins: [react(),
typescript({
target: 'es5',
rootDir: resolve('src/components'),
declaration: true,
declarationDir: resolve('dist'),
exclude: resolve('node_modules/**'),
allowSyntheticDefaultImports: true,
}),
],
build: {
// 输出文件夹
outDir: 'dist',
lib: {
// 组件库源码的入口文件
entry: resolve('src/components/index.tsx'),
// 组件库名称
name: 'demo',
// 文件名称, 打包结果举例: suemor.cjs
fileName: 'suemor',
// 打包格式
formats: ['es', 'cjs'],
},
rollupOptions: {
//排除不相关的依赖
external: ['react', 'react-dom', ...Object.keys(globals)],
},
},
})
发布包
设置packages.json
{
"name": "videoscreen",
"private": false,
"version": "0.0.3",
"type": "module",
"main": "./dist/suemor.cjs",
"module": "./dist/suemor.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"require": "./dist/suemor.cjs",
"import": "./dist/suemor.js"
},
"./style": "./dist/style.css"
},
"publishConfig": {
"access": "public"
},
"files": [
"dist"
],
...
}
发布
npm adduser
npm publish
使用
// npm install videoscreen
import {VideoShotScreen} from 'videoscreen';
<VideoShotScreen/>
lint
npm install eslint --save-dev
npx eslint --init
npm install vite-plugin-eslint --save-dev
import eslint from 'vite-plugin-eslint';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react(), eslint()],
})
//.eslintrc.cjs
npm install prettier --save-dev
// .prettierrc.js
module.exports = {
printWidth: 80,
tabWidth: 2,
useTabs: false,
singleQuote: true,
semi:false,
trailingComma: "none",
bracketSpacing: true
}
npm install eslint-config-prettier eslint-plugin-prettier --save-dev
// 设置配置 .eslintrc.cjs https://github.com/prettier/eslint-plugin-prettier#recommended-configuration
// .eslintrc.cjs
/**
* https://github.com/prettier/eslint-plugin-prettier#recommended-configuration
*/
module.exports = {
"env": {
"browser": true,
"es2021": true,
"node": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"plugin:prettier/recommended",
'plugin:react/jsx-runtime'
],
settings: {
react: {
version: 'detect'
}
},
"overrides": [
{
"env": {
"node": true
},
"files": [
".eslintrc.{js,cjs}"
],
"parserOptions": {
"sourceType": "script"
}
}
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": [
"@typescript-eslint",
"react",
"prettier"
],
"rules": {
"prettier/prettier": "error",
"arrow-body-style": "off",
"prefer-arrow-callback": "off",
"react/prop-types": "off"
}
};
package.json 添加
"lint": "eslint --ext .js,.jsx,.ts,.tsx --fix --quiet ./ --report-unused-disable-directives --max-warnings 0",
packages.json
"type": "commonjs",
// packages.json
{
"name": "videoscreen",
"private": false,
"version": "0.0.3",
"type": "commonjs",
"main": "./dist/suemor.cjs",
"module": "./dist/suemor.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"require": "./dist/suemor.cjs",
"import": "./dist/suemor.js"
},
"./style": "./dist/style.css"
},
"publishConfig": {
"access": "public"
},
"files": [
"dist"
],
"scripts": {
"dev": "vite",
"build": "tsc && vite build ",
"lint": "eslint --ext .js,.jsx,.ts,.tsx --fix --quiet ./ --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@rollup/plugin-typescript": "^11.1.2",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/node": "^20.4.9",
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@typescript-eslint/eslint-plugin": "^6.3.0",
"@typescript-eslint/parser": "^6.3.0",
"@vitejs/plugin-react": "^4.0.3",
"eslint": "^8.47.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-react": "^7.33.1",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3",
"less": "^4.2.0",
"prettier": "^3.0.1",
"typescript": "^5.0.2",
"vite": "^4.4.5",
"vite-plugin-eslint": "^1.8.1"
}
}
Husky + lint-staged
npm install husky --save-dev
npm pkg set scripts.prepare="husky install"
npm run prepare
npx husky add .husky/pre-commit "npm run lint"
npm install lint-staged --save-dev
npm install @commitlint/cli @commitlint/config-conventional --save-dev
npx husky add .husky/commit-msg "npx --no-install commitlint -e $HUSKY_GIT_PARAMS"