赞
踩
根据storybook 官网文档,需要在已有的项目中运行
npx storybook@latest init
也就是事先需要通过vite创建一个项目。
npm create vite@latest
之后再运行storybook 的命令,storybook会自动分析使用的构建工具,安装好需要的依赖,并自动新增文件。
storybook 增加了/src/stories目录,用于存放组件。
默认情况下,/src/stories 中放着 .vue, .stories, .css 文件,由于我希望把组件源代码单独放在一个文件夹下
因此在项目根目录 增加 /packages 目录,下面存放以组件名称命名的文件夹。以每个文件夹为单位表示为一个组件。
/src/stories 中.stories 文件引用/packages 目录下的.vue 用于生成storybook演示文档。
对于vite 的普通打包模式下,入口必须为index.html,因此需要使用vite 支持的lib模式打包。
lib模式下,对于图片等静态资源,处理为base64内联到代码中。且不支持配置。
base64的问题在于增大js体积,且不利于相同图片的复用。这块待研究。
直接使用vite build 命令,会使用项目根目录下的vite.config.js 对指定的入口进行构建。构建产物默认输出到dist目录下。
lib模式下,构建产物有
对于一个vue组件来说,这种格式正式我想要的。
而/package 目录下面会有好多组件,因此需要借助vite 的JavsScript API 进行循环
根据官网描述,使用下面js代码, 可以通过js启动vite 打包。
- import { build } from 'vite';
- build({
- //... vite config
- })
这里的 build() 方法传入的config对象,会和项目下的 vite.config.js 合并。
所以将一些公共的配置可以写在 vite.config.js下
- import { defineConfig } from 'vite';
- import vue from '@vitejs/plugin-vue';
- import vueJsx from '@vitejs/plugin-vue-jsx';
- import dts from 'vite-plugin-dts';
-
- // https://vitejs.dev/config/
- export default defineConfig({
- build: {
- rollupOptions: {
- external: ['vue'], // 排除三方包
- },
- },
- plugins: [
- vue(),
- vueJsx(),
- dts({
- outputDir: './lib',
- entryRoot: './packages',
- }),
- ],
- });
- /**
- * 构建一个组件
- * @param {string} compName 组件名称,对应packages目录下的文件夹名称
- */
- async function buildAComponent(compName) {
- const entry = path.join(packagesDir, compName, 'index.ts');
- const out = path.join(outDir, compName);
- await build({
- build: {
- outDir: out,
- lib: {
- entry: [entry],
- formats: ['es'],
- },
- },
- });
- }
这个是构建一个组件的方法,因此,我只要通过node 的 fs 模块读取并分析/packages 路径下的文件夹,遍历出来构建即可。
- import { readdirSync, writeFileSync } from 'fs';
- import path, { dirname } from 'path';
- import { fileURLToPath } from 'url';
- import { build } from 'vite';
-
- const __dirname = dirname(fileURLToPath(import.meta.url));
- const packagesDir = path.join(__dirname, './packages');
- const outDir = path.join(__dirname, './lib');
- async function main() {
- const packages = readdirSync(packagesDir);
- let indexFileContent = '';
- const exportNames = [];
- const promise = [];
- for (const folderName of packages) {
- // const stats = lstatSync(path.join(packagesDir, compName));
- // if (stats.isDirectory()) {
- // // 如果是目录,则构建
- // }
- const firstLetter = folderName[0];
- const isFirstLetterUpperCase = /[A-Z]/.test(firstLetter);
- if (isFirstLetterUpperCase && folderName.indexOf('.ts') === -1) {
- promise.push(buildAComponent(folderName));
- indexFileContent += `import { ${folderName} } from './${folderName}/index.js';\n`;
- exportNames.push(folderName);
- }
- }
- await Promise.all(promise);
- indexFileContent += `export { ${exportNames.join(', ')} };`;
- writeFileSync(path.join(outDir, 'index.js'), indexFileContent);
- console.log('created: lib/index.js');
-
- }
- main();
-
- /**
- * 构建一个组件
- * @param {string} compName 组件名称,对应packages目录下的文件夹名称
- */
- async function buildAComponent(compName) {
- const entry = path.join(packagesDir, compName, 'index.ts');
- const out = path.join(outDir, compName);
- await build({
- build: {
- outDir: out,
- lib: {
- entry: [entry],
- formats: ['es'],
- },
- },
- });
- }
main 函数中判断文件夹名称前面是大写字母开头的才当作组件来处理。
这一个目录入口为index.ts 构建完成后,输出到lib
至此,主要构建完成了
由于一个npm库需要有一个默认的入口js,因此需要在/lib 目录下面增加一个index.js 用于导入lib 下面所有组件,并导出。这部分代码在上面 build.js 中已经体现。
export { Button } from './Button/index.ts';
- {
- "main": "lib/index.js",
- "module": "lib/index.js",
- "files": [
- "lib",
- "packages"
- ],
- //...
- }
使用vite-plugin-dts 插件
- import dts from 'vite-plugin-dts';
-
- // ...
-
- plugins:[
- // vue(),
- // vueJsx(),
- dts({
- outputDir: './lib',
- entryRoot: './packages',
- }),
- ]
构建dts速度有点慢 。
这样配置,会在lib/Button/ 下面增加d.ts 文件了。
与库的默认入口文件一样,需要指定默认declare 文件,package.json types 指定为 /packages/main.ts。(这里暂时指定.ts 文件,可能在一些构建工具中不识别,因为那些工具只识别d.ts 文件)
一个组件默认的构建结果为index.js, style.css
鉴于使用的情况下引入 import { Button } from 'xxx'; 之外还要 import 'xxx/lib/Button/style.css';
使用babel-import-plugin 可以解决按需引入样式问题。
由于我的目标是不使用babel-import-pluing 因此,完成这个效果,只需要在/lib/Button/index.js 文件的最上方引入/lib/Button/style.css即可。
因为不想用node
由于vite基于rollup,根据rollup 官网文档,我们可以在插件的 generateBundle 钩子中在生成产物前操作文件内容。
- /**
- * 在产物js上导入css
- */
- function () {
- return {
- name: 'auto-import-style',
- generateBundle(options, bundle) {
- bundle['index.js'].code = 'import "./style.css";\n' + bundle['index.js'].code;
- },
- };
- },
npm上也有类似的插件 vite-plugin-libcss
不能将vue依赖打包进代码中,否则会导致组件无法使用。
打包组件的产物js中,最上方应该为 import {} from 'vue'; 这样的代码。
通过配置vite.config.js 下的rollupOptions.external即可:
- build: {
- rollupOptions: {
- external: ['vue'],
- // output: {
- // globals: {
- // vue: 'Vue', // umd需要
- // },
- // },
- },
- },
这里注释掉rollupOptions.output.globals 原因是默认构建lib会输入umd 和 es 模块的文件。我构建时会指定 build.lib.formats: ['es'] 就不需要了。
由于现在我看使用vite-plugin-dts 生成d.ts 文件使用的时间过长。先制定一下增量构建的方案。
鉴于在正常开发过程中,始终用master分支作为正式分支。
因此在开发分支中,可以借助git 来对比当前开发分支与master分支的区别
使用node 的child_process 执行git命令
git diff origin/master --name-only
可检查当前分支,对比master,哪些文件有变化。
比如我更改了Button.vue文件,此git控制台输出:
packages/Button/Button.vue
得到控制台输出后,可以确定package/Button 组件有变动,只需要构建package/Button/index 即可。
以此仅构建变更过的组件。
在本组件库中,存在组件间相互引用的情况。
这种情况导致下面的问题
Button组件修改,增量构建不会重新构建依赖的组件A.vue,B.vue
Button.vue 的代码会加入到其他组件的产物中,导致重复代码,体积增大。
组件间的依赖不构建。A.vue 中import 的 Button.vue 转换成成../lib/Button/index.js,也就是Button的构建产物。
有rollupOptions.external 可以配置 '../Button' 不打包。
如果用别名的方式是否可行。类似一般vue项目中 '@' 表示'src' 一样,将内部依赖的组件都用别名如 '@pkg/Button' 引入。
虽然可行,但是增加心智负担,且一旦有一个没加路径别名,则功亏一篑。
rollupOptions.external 支持传入函数。是否可以在函数内判断导入的组件属于package/ 一级目录下的组件。待续
是否存在钩子使扫描到的依赖不处理。并且支持替换导入语句。
通过rollupOptions.external,该字段支持传入回调函数。
- rollupOptions: {
- external(source,importer,isResolved){
- if(!isResolved){
- return ['vue'].includes(source);
- }else if(importer){
- return /[\\/]packages[\\/][A-Z]\w+[\\/]index.[jt]s/;
- }
- }
- }
isResolved 表示导入source 是否被转化为绝对路径。
这样通过正则表达式可以确定一些import语句不需要转换。
由于使用external讲import '' from './xxx.ts',语句不处理,会将语句原封不动输出到产物中。而产物中不能有 .ts 后缀的引入。因此需要将ts 后缀转换为js后缀。
同自动导入css一样。我们在rollupPlugin 的generateBundle 钩子中,在产物文件生成前,读取编译后的代码字符串,正则匹配出 import 语句后面的 .ts 并转换为 .js 即可。
- /**
- * 在产物js上导入css
- */
- function () {
- return {
- name: 'auto-import-style',
- generateBundle(options, bundle) {
- bundle['index.js'].code = 'import "./style.css";\n' + bundle['index.js'].code;
- // new
- bundle['index.js'].code = bundle['index'].code.replace(\.\.[\\/]\w+[\\/]index.ts/g, str => str.replace('.ts','.js'));
- },
- };
- },
比如 package/Button/A 引入了 package/Button/index
此时会报错:
"xxx" is imported as an external by "xxx", but is already an existing non-external module id
说明引入的文件已经配置external了。此时应该调整rollupOptions.external 的计算规则,保证同组件间引入文件不被external
/packages 目录下如果组件之间相互引用,则必须要引入其组件的 index.ts 文件。
否则在rolluptOption.external 中判断不是引的index 则直接抛错误,中断构建。
因为构建完的产物只有一个index.js,引入其他的会报错找不到。
- const packagesIndexReg = /[\\/]packages[\\/][A-Z]\w+[\\/]index.[jt]s/;
- const packagesReg = /[\\/]packages[\\/][A-Z]\w+[\\/]/
- rollupOptions: {
- external(source,importer,isResolved){
- if(!isResolved){
- return ['vue'].includes(source);
- }else if(importer){
- const sourceName = source.match(packagesReg);
- const isPackageIndex = packagesIndexReg.test(source);
- if(sourceName){
- const importerName = importer.match(packagesReg)
- const isSamePackage = sourceName[0] === importerName[0];
- const isSourceImporterIndex = packagesIndexReg.test(source);
- if(!isSourceImporterIndex && !isSamePackage){
- throw new Error('报错组件间引用请引入index.ts');// 直接中断构建
- }
- // 防止组件自引用index导致的问题
- return !isSamePackage;
- }
- return isPackageIndex;
- }
- }
- }
通过正则来确定是否组件间引用,和组件内引用。
使用 vue-i18n@9.6.5 做国际化,由于我不想在组件库内部打包进vue-i18n 因此这里需要定义一下方按。
使用项目的vue-i18n。
组件库提供方法在项目中通过app.use进行安装,并将项目的i18n传进来。
中文作为key基本不用翻译。因为$t("中文") 在获取不到翻译时,会输出$t 的入参字符串("中文")。
- import * as cn from './cn/index.js'
- import * as en from './en/index.js'
- const langs= {en,cn}
- /**
- * @param {import('vue').App} app
- * @param {object} options
- * @param {import('vue-i18n').I18n} options.i18n 项目重传入的i18n
- */
- export default function (app, options) {
- options.i18n.global.mergeLocaleMessage('en',en);
- options.i18n.global.mergeLocaleMessage('cn',cn);
- }
- import locale from 'my-component/locale';
- import { createI18n } from 'vue-i18n';
- import { createApp } from 'vue';
-
- const app = createApp();
- const i18n = createI18n();
- app.use(i18n).use(locale, {i18n});
通过项目中的use 将项目的i18n实例传入组件库。
通过i18n.global.mergeLocaleMessage 方法,将组件库内的翻译合并到项目的i18n 中。
这里可能回污染项目中i18n 的命名空间。比如项目中 $(‘a’) 的翻译是’b', 但是组件库中的翻译是'c',这样回导致覆盖项目中的国际化。因此需要做隔离。
这里使用最简单的隔离,就是组件库中的翻译统一带一个固定的前缀,比如"myComp"。
正式弄的话前缀需要设置奇怪点。比如 “__comp__” 以确保不与项目命名空间冲突。
- import * as cn from './cn/index.js'
- import * as en from './en/index.js'
-
- /**
- * @param {import('vue').App} app
- * @param {object} options
- * @param {import('vue-i18n').I18n} options.i18n 项目重传入的i18n
- */
- export default function (app, options) {
- options.i18n.global.mergeLocaleMessage('cn', { myComp: { cn } });
- options.i18n.global.mergeLocaleMessage('en', { myComp: { en } });
- }
此时组件库中使用$t 需要加上前缀。$t('myComp.xxx');
将所有$t 加上前缀会导致中文输出会带上前缀。页面上会展示 “myComp.中文”
因此需要将前缀去除。
i18n 提供了找不到翻译时的回调。i18n.global.setMIssingHandler
- //locale
- export default function(app, options){
- //...
- if (options.global.setMissingHandler){
- options.global.setMissingHandler((lang, key) => {
- return key.replace('myComp', '');
- });
- } else if(options.i18n.global.hasOwnProperty('missing')){
- // i18n 替换api兼容,作者在某些版本中变成了这个
- options.i18n.global.missing = (lang, key) => {
- return key.replace('myComp', '');
- }
- }
- }
此时就可以去除前缀了。
实际使用中,项目中并不使用vue-i18n 来翻译。因此app.use 中不会传递i18n。
这种情况需要自行处理。
重写app.config.globalProperties.$t
- /**
- * @param {import('vue').App} app
- */
- function fallbackToCN(app) {
- const cnMessage = cn.myComp;
- /** 保留旧t */
- const t = app.config.globalProperties.$t || (v => v)
-
- app.config.globalProperties.$t = str => {
- const keyChain = str.split('.');
- if (keyChain[0] !== 'myComp') return t(str);
- let value = cnMessage;
- for (let k of keyChain.slice(1)) {
- const next = value[k];
- if (!next) return k;
- value = next;
- }
- return value;
- }
- }
这样简单支持了拿不到翻译时,去取$t 入参的最后一个key;
考虑优秀的组件不应当在代码中写死翻译,且翻译使用量较少。因此强制只使用 $t 一个函数也无可厚非。
。。。待续。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。