vue2 组件库开发记录-搭建环境(第二次架构升级)

vue2 组件库开发记录-搭建环境(第二次架构升级)

  • 前言
  • 项目架构变化
  • 初始化 lerna
  • 项目目录结构
  • 子项目目录结构
  • 使用脚本创建组件子项目的模板
  • 编写 rollup+scss 的配置
  • 打包公共代码
  • 打包组件代码
  • 打包全量包
  • 组件文档环境搭建
  • 总结

前言

本文主要是记录我在开发组件库时如何搭建环境(第二次架构升级)。

项目架构变化

本次架构升级主要参考了element-plus,包含以下几方面:

  • 打包构建工具由webpack改成rolluprollup配置要比webpack简单的多了。rollup天然支持tree sharking,并且可以打包出es module格式的文件,这样我们的 js 代码就可以天然支持es moduletree sharking功能
  • 单包架构改成多包架构。使用lerna进行项目管理。每个组件是一个子项目,这样每个组件就可以单独发布和使用,天然按需加载。
  • 样式单独打包。在之前,样式跟组件是一起进行打包的,然后借助mini-css-extract-plugin将样式抽离出来了。但是这种方式并不适合多包架构,所以需要使用gulp对样式进行单独打包
  • 组件和公共代码的引用。在之前,组件跟公共代码是通过webpack提供的路径别名进行引用的,相对于相对路径,可以少写很多的../,并且我们在打包一个组件的时候是不需要吧公共代码和其他组件的代码也打包进去,这样会造成代码的冗余。所以借助路径别名的另一个好处就是可以精准匹配我们引用的公共代码和组件,然后借助externals字段排除掉这些依赖文件。升级到多包架构之后,公共代码和组件都是一个 npm 包,所以我们是通过依赖包的形式引入公共代码和组件的,在使用rollup的时候直接读取package.json中的dependencies字段,通过external字段忽略这些依赖包的打包。相比这下,使用 npm 包引入会比路径别名引入简单多了。

初始化 lerna

  • 安装 lerna
npm i lerna -g
  • 初始化

首先在项目的根目录初始化package.json

npm iniy -y

然后再初始化lerna,我们才用的是固定模式来管理每个包

lerna init
  • 配置 lerna

这个时候你会看见一个lerna.json文件。在文件中写入一下配置:

{
    // 子项目所在的目录
  "packages": [
    "packages/*"
  ],
//   版本号
  "version": "1.3.4",
  "command": {
    //   lerna publish命令相关配置
    "publish": {
        // 忽略md文件的修改。如果某个子项目只修改了md文件,是不会被publish的
      "ignoreChanges": [
        "*.md"
      ],
    //   lerna publish 之后lerna会自动给我们提交代码,message就是提交代码的信息
      "message": "chore(release): publish",
    //   指定发布的地址,私服的话就填写私服的地址
      "registry": "https://registry.npmjs.org"
    }
  }
}

  • 依赖提升

当我们安装依赖的时候,node_modules文件夹会出现在每一个子项目中,我们需要将他们提取到顶层目录中,避免相同依赖包安装多次。所以我们还需要修改一下我们的配置。

lerna.json文件中加入如下配置:

{
    // 使用yarn安装依赖,没有安装yarn的需要安装一下
  "npmClient": "yarn",
//   工作目录
  "useWorkspaces": true,
}

package.json文件中加入如下字段:

{
    // 声明为私包
  "private": true,
//   工作目录
  "workspaces": ["packages/*"]
}
  • lerna 常用命令

经过上面的步骤,我们的 lerna 基本环境已经出来了,下面介绍几个常见的命令

安装所有子项目的依赖包:

lerna bootstrap

给每个项目添加依赖:

lerna add lodash

给指定的子项目添加依赖:

lerna add lodash --scope=@lin-view-ui/button

@lin-view-ui/button指的是子项目中package.json文件中的name字段,并不是指文件夹

显示自上次 publish 之后,有修改过的子项目:

lerna changed

新建一个子项目,但是组件库这里基本用不上,因为生成出来的文件没有符合我的要求,所以需要使用 node 编写一个脚本,代替这条命令:

lerna add button

发布项目:

lerna publish

在发布之前,会让你挑选版本号。发布之后会自动提交你的代码,然后打tag

项目目录结构

- project
  - build  // 打包构建
  - packages
    - button        // button组件
    - alert         // alert组件
    - types         // 类型声明文件
    - utils         // 公共方法
    - locale        // i18n
    - mixins        // 公共mixins
    - test-utils    // 测试相关的方法
    - theme-chalk   // 样式
    - lin-view-ui   // 组件库总入口,全量包
  - script // 脚本命令

子项目目录结构

这里我们的子项目分为 4 中类型,分别是组件,类型声明文件,样式,公共代码。

  • 组件:包含__test__(编写测试用例的文件夹),src(编写代码的地方),types(类型声明文件),index.js(打包的入口文件),package.jsonREADME.md项目说明
  • 类型声明文件:包含src(编写类型声明文件),index.d.ts(入口),package.jsonREADME.md项目说明
  • 公共代码:我们的公共代码分为 3 个子项目,分别是@lin-view-ui/utils(工具类),@lin-view-ui/test-utils(测试相关工具类),@lin-view-ui/mixins(公共 mixins),@lin-view-ui/locale(i18n 相关的东西),每个子项目包含src(编写代码的地方),index.js(打包的入口文件),package.jsonREADME.md项目说明
  • 样式:src(编写样式的地方),package.jsonREADME.md项目说明

特别说明:

  • 因为项目根目录的package.jsonprivate字段为true,所以每个子项目中的package.json文件需要添加如下字段,否则会导致发包不成功
{
  "publishConfig": {
    "access": "public"
  }
}
  • 组件子项目package.json文件必须包含如下字段:
{
  "typings": "types/index.d.ts",
  "peerDependencies": {
    "vue": "^2.6.14"
  },
  "dependencies": {
    "@lin-view-ui/types": "^1.3.3"
  }
}

由于我们的组件是依赖于 vue,所以需要预安装 vue。同时我们还需要类型声明文件来支持 ts。类型声明文件统一写在@lin-view-ui/types子项目中。组件子项目依赖于这个子项目,然后在types/index.d.ts文件中引入对应的组件类型声明,然后在导出。

  • 每个子项目package.json文件包含的如下字段:
{
    // 包名
  "name": "@lin-view-ui/tag",
//   版本号
  "version": "1.3.4",
//   描述
  "description": "tag",
//   npm包入口文件
  "main": "dist/index.js",
//   关键词
  "keywords": [
    "lin-view-ui",
    "tag"
  ],
//   首页地址
  "homepage": "https://github.com/c10342/lin-view-ui/tree/master/packages/tag",
//   仓库地址
  "repository": {
    "type": "git",
    "url": "https://github.com/c10342/lin-view-ui"
  },
//   issues地址
  "bugs": {
    "url": "https://github.com/c10342/lin-view-ui/issues"
  },
//   作者
  "author": "c10342",
//   开源协议
  "license": "MIT",
}

使用脚本创建组件子项目的模板

由于一个组件子项目包含了多个文件模板,测试文件模板,readme 文件模板,index.js 文件模板等等,这些如果是你自己手动去创建的话会很麻烦,所以我们借助 node 编写一个脚本。

  • 使用方式

node ./scripts/componentTemplate.js button

  • 获取创建的组件名
const argv = process.argv;

const componentName = argv[2]; // button
  • 检查是否输入组件名,或者组件名是否已经存在了
const path = require("path");
const packageRoot = path.resolve(__dirname, "../packages");
// 检查有没有输入组件名
if (!componentName) {
  console.log(chalk.blueBright("请输入组件名"));
  return;
}
const compomentPath = path.resolve(packageRoot, componentName);
// 检查输入的组件名是否已经存在了
if (fs.existsSync(compomentPath)) {
  console.log(chalk.blueBright(`${componentName}组件已经存在了`));
  return;
}
  • 创建根目录,src、types 等目录
// 创建组件根目录
fs.mkdirSync(compomentPath);
// 创建src目录
const compomentSrcPath = path.resolve(compomentPath, "src");
fs.mkdirSync(compomentSrcPath);
// 创建__tests__目录
const testsSrcPath = path.resolve(compomentPath, "__tests__");
fs.mkdirSync(testsSrcPath);
// 创建types目录
const typesSrcPath = path.resolve(compomentPath, "types");
fs.mkdirSync(typesSrcPath);
  • 创建模板文件

下面以创建一个package.json文件为例,其他文件模板创建是同样的道理。思路:读取模板->借助handlebars填写模板数据->写入最终模板字符串到文件目录中

function writePakcageTpl() {
  const parmas = {
    name: componentName,
  };
  const tplStr = fs.readFileSync(
    path.resolve(__dirname, "./template/package.tpl"),
    "utf-8"
  );
  const result = handlebars.compile(tplStr)(parmas);
  fs.writeFileSync(path.resolve(compomentPath, "package.json"), result);
}
writePakcageTpl();

编写 rollup+scss 的配置

  • 读取项目依赖

读取每个子项目中的package.json文件的dependenciespeerDependenciesdevDependencies,这些都是要在打包的时候排除的掉的依赖。

const root = path.resolve(__dirname, "../packages");
function getExternalsDep(name, dev = false) {
  // 获取子项目根目录
  const dir = path.resolve(root, name);
  // 加载package.json
  const pck = require(path.resolve(dir, "./package.json"));
  const dependencies = pck.dependencies || {};
  const peerDependencies = pck.peerDependencies || {};
  const externals = [];
  // 获取key值
  Object.keys(dependencies).forEach((key) => externals.push(key));
  Object.keys(peerDependencies).forEach((key) => externals.push(key));
  if (dev) {
    const devDependencies = pck.devDependencies || {};
    Object.keys(devDependencies).forEach((key) => externals.push(key));
  }
  return [...new Set(externals), "flv.js/dist/flv.js"];
}
getExternalsDep("button");
  • 创建输入配置

安装依赖

npm i @rollup/plugin-node-resolve [email protected] @rollup/plugin-commonjs rollup del rollup-plugin-terser @rollup/plugin-image [email protected] vue-template-compiler -D

特别注意:rollup-plugin-vue 最新版已经出到6.0.0了,但是6.0.0这个版本不支持vue2的打包,所以我们要回退到5.x的版本。rollup-plugin-babel最新版出到了5.x了,但是我发现 vue 组件内部的箭头函数没有被转换,不知道为什么,回退到4.x的版本箭头函数就可以被转换了

const { nodeResolve } = require("@rollup/plugin-node-resolve");
const babel = require("rollup-plugin-babel");
const commonjs = require("@rollup/plugin-commonjs");
const vue = require("rollup-plugin-vue");
const { terser } = require("rollup-plugin-terser");
const image = require("@rollup/plugin-image");
function createInputConfig(options = {}) {
  const config = {
    // 入口文件
    input: options.input,
    // 不需要进行打包的文件或者依赖
    external: options.external,
    plugins: [
      // 识别node_modules目录
      nodeResolve(),
      // 处理vue文件
      vue({}),
      babel({
        // 防止打包node_modules下的文件
        exclude: "node_modules/**",
        // 使plugin-transform-runtime生效
        runtimeHelpers: true,
      }),
      // 将 CommonJS 模块转换为 ES6 的 Rollup 插件,rollup默认只支持 ES6+的模块方式
      commonjs(),
      //   将图片转成base64
      image(),
    ],
  };
  //  压缩文件
  if (options.minify) {
    config.plugins.push(terser());
  }
  //   其他插件
  if (options.plugins) {
    config.plugins.unshift(...options.plugins);
  }
  return config;
}

注意:plugins需要注意一下插件的顺序,特别是@rollup/plugin-commonjs这个插件的顺序。

由于我们的配置中使用babel所以,我们需要在项目的根目录中新建一个babel.config.js文件,配置一下 babel 的一些东西

module.exports = {
  presets: [
    [
      // 配置支持es的一些新特性
      "@babel/preset-env",
      {
        //   按需引入需要用到的一些新特性语法
        useBuiltIns: "usage",
        // 使用core-js3的版本
        corejs: 3,
        targets: {
          // 打包出来的文件最低需要支持到ie10
          ie: "10",
        },
      },
    ],
  ],
  plugins: [
    //   避免全局变量污染
    "@babel/plugin-transform-runtime",
    // 处理vue的jsx语法
    "@vue/babel-plugin-transform-vue-jsx",
  ],
};
  • 创建输出配置
/**
 * @param {*} distPath 打包的输出路径
 * @param {*} options 其他可选配置参数
 */
function createEsOutput(distPath, options = {}) {
  return {
    file: distPath,
    format: "es",
    ...options,
  };
}
// 打包出umd格式的文件
function createUmdOutput(distPath, options = {}) {
  return {
    file: distPath,
    format: "umd",
    ...options,
  };
}
  • 打包构建 js 和 vue 文件

安装依赖

npm i rollup -D
const rollup = require("rollup");
/**
 * @param {*} inputOptions 输入配置 createInputConfig创建
 * @param {*} outputOptions 输出配置 createEsOutput或者createUmdOutput创建
 */
async function rollupBuild(inputOptions, outputOptions) {
  const bundle = await rollup.rollup(inputOptions);
  await bundle.write(outputOptions);
}
  • 打包 scss

安装依赖

npm i gulp gulp-sass gulp-clean-css gulp-rename gulp-autoprefixer gulp-concat node-sass sass -D

打包 css 包含 2 部分,一是将scss编译为css,二是拷贝字体图标文件,因为 gulp 是不会去处理字体图标文件,所以需要拷贝一份到输出目录

const { src, dest } = require("gulp");
// 编译scss
const sass = require("gulp-sass");
// 压缩样式文件
const cssmin = require("gulp-clean-css");
// 重命名文件名
const rename = require("gulp-rename");
// 样式添加前缀
const autoprefixer = require("gulp-autoprefixer");
// 将多个文件拼接成一个文件
const concat = require("gulp-concat");
const path = require("path");
// 样式存放的目录
const root = path.resolve(__dirname, "../packages/theme-chalk");
const resolve = (pathSrc) => path.resolve(root, pathSrc);
// 编译scss。srcPath:入口,可以是数组,最终会合并成一个文件;distPath:输出路径
const buildScss = (
  srcPath,
  distPath,
  options = {
    basename: "style",
  }
) => {
  return (
    src(srcPath)
      //   将多个样式文件拼接成一个
      .pipe(concat("style.scss"))
      // 编译scss
      .pipe(sass().on("error", sass.logError))
      // 给样式添加前缀
      .pipe(autoprefixer({ cascade: false }))
      // 压缩样式文件
      .pipe(cssmin())
      .pipe(
        // 修改文件名
        rename((srcPath) => {
          srcPath.basename = options.basename;
          // 后缀名
          srcPath.extname = ".css";
        })
      )
      // 输出到指定目录
      .pipe(dest(distPath))
  );
};
// 将字体图标文件拷贝到输出目录
function copyfont(distPath) {
  return src(resolve("./src/fonts") + "/**")
    .pipe(cssmin())
    .pipe(dest(distPath));
}
  • 其他

删除指定目录

const del = require("del");
const clean = (cleanPath) => {
  return del(cleanPath, {
    force: true,
  });
};

非组件子项目的子项目(白名单)

const whiteList = [
  "locale",
  "mixins",
  "theme-chalk",
  "utils",
  "lin-view-ui",
  "test-utils",
  "types",
];

打包公共代码

我们的公共代码主要有 4 个子项目,其中有三个需要进行打包的,分别是utilslocalemixins。下面以讲解打包utils为例,其他 2 个子项目跟他是一样的

// 子项目根路径
const root = path.resolve(__dirname, "../packages/utils");
const resolve = (pathSrc) => {
  return path.resolve(root, pathSrc);
};
// 创建输入配置
function createConfig(filename) {
  let input = filename;
  // 如果文件名不是index.js,路径需要加一个src
  if (filename !== "index.js") {
    input = `./src/${filename}`;
  }
  return createInputConfig({
    input: resolve(input),
    external: getExternalsDep("utils"),
  });
}
// 打包单个文件的函数
const buildOne = async (filename) => {
  const inputOptions = createConfig(filename);
  const outputOptions = createEsOutput(resolve(`./dist/${filename}`));
  await rollupBuild(inputOptions, outputOptions);
  console.log(filename, "done");
};
// 获取所有需要进行打包的文件
const fileList = fs.readdirSync(resolve("./src"));
fileList.push("index.js");
// 打包整个utils目录的文件
const build = async () => {
  // 先删除旧的输出目录
  await clean(resolve("./dist"));
  fileList.forEach((filename) => buildOne(filename));
};
build();

打包组件代码

由于我们的组件都是放在packages目录下面的,所以我们需要读取packages目录下面的文件夹,然后过滤带那些非组件的文件夹。入口文件都是index.js。组件打包完成之后,还需要吧组件对应的样式文件也打包一下

const root = path.resolve(__dirname, "../packages");
const resolve = (pathSrc) => path.resolve(root, pathSrc);
function createConfig(filename) {
  return createInputConfig({
    input: resolve(`./${filename}/index.js`),
    external: getExternalsDep(filename),
  });
}
const buildComponent = async (comp) => {
  const inputConfig = createConfig(comp);
  const outputConfig = createEsOutput(resolve(`./${comp}/dist/index.js`));
  await clean(resolve(`./${comp}/dist`));
  await rollupBuild(inputConfig, outputConfig);
  // 打包样式,这里连同base基础样式也要一起打包进去
  await buildScss(
    [
      resolve(`./theme-chalk/src/${comp}.scss`),
      resolve(`./theme-chalk/src/base.scss`),
    ],
    resolve(`./${comp}/dist`)
  );
  // 拷贝字体图标
  await copyfont(resolve(`./${comp}/dist/fonts`));
  console.log(comp, "done");
};
// 读取组件目录。并过滤非组件的目录
const compList = fs
  .readdirSync(root)
  .filter((fileName) => !whiteList.includes(fileName));
const build = () => {
  compList.forEach((comp) => buildComponent(comp));
};
build();

打包全量包

打包出来的全量包需要支持按需加载功能,否则会增大开发者的 bundle 文件体积。这里我们借助babel-plugin-component来实现我们的按需加载功能。我们打包出来的目录结构需要符合babel-plugin-component的要求才能实现按需加载,目录格式如下:

- lib
  - theme-chalk // 样式存放目录
    - fonts     // 字体图标文件
    - button.css // button组件样式
    - alert.css // alert组件样式
    - base.css // 通用样式必须要有,但是也可以通过配置设置成非必须
    - index.css // 总样式文件
  - button.js // button组件
  - alert.js // alert组件
  - utils.js // 通用方法-公共代码
  - locale.js // i18n-公共代码
  - mixins // 公共逻辑-公共代码
  - index.umd.js // umd格式的全量包
  - index.js  // npm包的模块入口,esmodule格式
  • 打包 umd 格式的全量包

首先我们要解决一些依赖包的引用问题。比如我引用了@lin-view-ui/button这个组件,实际上我们可以通过lerna进行软连接,链接到node_modules中,这样就不用处理引用的问题了。但是有一个坏处,就是button组件必须是已经经过打包的,并且生成了打包目录。因为 npm 包的查找规则是先查找package.json中的main字段所指向的文件,而我们的main字段则是指向已经经过打包的文件。为了解决上述问题,我们需要借助@rollup/plugin-alias这个插件来实现路径的改写,也就是路径别名。最终的路径别名映射到真实的路径是这样的:@lin-view-ui/button->packages/button,rollup 遇见packages/button就会自动查找packages/button下面的 index.js 文件。全量样式的话我们不需要在这里打包,因为后面还有一个打包出 esmodule 格式文件的步骤,我们在这一步中打包全量样式。

const root = path.resolve(__dirname, "../packages/lin-view-ui");
const packagesRoot = path.resolve(__dirname, "../packages");
const resolveInput = (pathSrc) => path.resolve(root, pathSrc);
const buildumdIndex = async () => {
  const inputConfig = createInputConfig({
    input: resolveInput("./index.js"),
    // 全量包只需要跳过vue依赖的打包即可
    external: ["vue"],
    // 压缩文件
    minify: true,
    plugins: [
      alias({
        // 将@lin-view-ui映射到packages目录
        entries: [{ find: /^@lin-view-ui/, replacement: packagesRoot }],
      }),
    ],
  });
  const outputConfig = createUmdOutput(resolveOutput("./lib/index.umd.js"), {
    // umd格式的文件需要包含一个name,作为全局变量
    name: "LinViewUi",
    globals: {
      // 上面打包的时候跳过了vue,所以我们需要声明一下外部的vue变量是什么
      vue: "Vue",
    },
  });
  await rollupBuild(inputConfig, outputConfig);
  console.log("umd build success");
};
  • 打包 esmodule 格式的全量包

其实也不能叫做全量包吧,因为从上面的目录中可以看见已经又一个button.js的文件了,所以当我们遇见button组件就可以改成引用./button.js,这样就可以不用把button组件打包进来了。所以我们这里也要修改一下依赖包的引用问题。比如改成这个样子:@lin-view-ui/button->./button.js@lin-view-ui/mixins->./mixins.js。同时我们还要在这一步打包出一个全量样式文件,还有一个 base.css 样式文件。

const formatImportPath = (id) => {
  // 修改依赖的引入方式,`@lin-view-ui/button`->`./button.js`
  if (id.match(/^@lin-view-ui/)) {
    const depName = id.split("/")[1];
    return `./${depName}.js`;
  }
};
const buildesIndex = async () => {
  const inputConfig = createInputConfig({
    input: resolveInput("./index.js"),
    external: getExternalsDep("lin-view-ui", true),
    plugins: [
      alias({
        entries: [{ find: /^@lin-view-ui/, replacement: packagesRoot }],
      }),
    ],
  });
  const outputConfig = createEsOutput(resolveOutput("./lib/index.js"), {
    // 这里,关键
    paths: formatImportPath,
  });
  await rollupBuild(inputConfig, outputConfig);
  // 打包全量样式
  await buildScss(
    resolveInput(`../theme-chalk/src/index.scss`),
    resolveOutput(`./lib/theme-chalk`),
    {
      // 样式文件叫index.css
      basename: "index",
    }
  );
  // 打包base样式文件
  await buildScss(
    resolveInput(`../theme-chalk/src/base.scss`),
    resolveOutput(`./lib/theme-chalk`),
    {
      basename: "base",
    }
  );
  // 拷贝字体图标文件
  await copyfont(resolveOutput(`./lib/theme-chalk/fonts`));
  console.log("es build success");
};
  • 打包组件

同样也需要把依赖的引用路径进行转化一下,@lin-view-ui/button->./button.js

const resolvePackage = (pathSrc) => path.resolve(packagesRoot, pathSrc);
const buildesComponent = async (comp) => {
  const outputConfig = createEsOutput(resolveOutput(`./lib/${comp}.js`), {
    paths: formatImportPath,
  });
  const inputConfig = createCompConfig(comp);
  await rollupBuild(inputConfig, outputConfig);
  // 打包组件样式
  await buildScss(
    resolvePackage(`./theme-chalk/src/${comp}.scss`),
    resolveOutput(`./lib/theme-chalk`),
    {
      basename: comp,
    }
  );
  console.log(comp, "done");
};
const compList = fs
  .readdirSync(packagesRoot)
  .filter((fileName) => !whiteList.includes(fileName));
compList.forEach((comp) => buildesComponent(comp));
  • 打包公共代码

公共代码包含utilsmixinslocale,同样也需要把依赖的引用路径进行转化一下。以utils为例

const buildesUtils = async () => {
  const inputConfig = createInputConfig({
    input: resolvePackage("./utils/index"),
    external: getExternalsDep("utils"),
  });
  const outputConfig = createEsOutput(resolveOutput(`./lib/utils.js`), {
    paths: formatImportPath,
  });
  await rollupBuild(inputConfig, outputConfig);
  console.log("utils", "done");
};
buildesUtils();
  • 总结

其实当我们的配置编写完毕之后,也就是上面的创建输入配置创建输出配置完成,打包工作就是指定入口文件和输出路径。从上面的打包组件,公共代码等步骤可以看出,其实就是在确定输入输出路径。而且配置起来也很简单,对比webpack来说一个字就是爽。对于样式来说,其实也是确定输入输出路径,只是你要确定一下编译样式的时机(每个组件打包完成编译一下对应组件的样式,全量包编译完成的时候也需要编译全量样式)。

组件文档环境搭建

文档的打包也是需要路径别名来处理@lin-view-ui/button这种引入依赖,转化关系是这样子的@lin-view-ui/button->packages/button。但是webpackage比较有趣的是它的查找方式。packages/button是一个文件夹,webpack首先会先查找文件夹下面是否有package.json文件,没有就直接获取文件夹下面名为的index.js的文件,有则读取package.json文件的main字段,根据main字段查找入口,上面已经说到了,main字段是指向我们打包出来的目录dist/index.js,但是我们想要让webpack直接查找到我们的button/index.js文件,所以我们需要修改一下webpack的查找方式,我们需要借助resolve.mainFields字段,让webpack先根据我们规定的doc字段查找文件,没有在根据main字段查找文件。然后我们需要在每个子项目中的package.json文件中添加doc字段。关键代码如下:

webpack.docs.js

module.exports = {
  resolve: {
    // 先查找package.json文件的doc字段,在查找main字段
    mainFields: ["doc", "main"],
  },
};

packages/button/package.json

{
  "doc": "index.js"
}

这样我们就可以在开发环境的时候直接修改代码,然后组件文档就能及时更新看到效果了,不用等待子项目 build 一次,再去刷新页面查看效果。

组件文档配置如下:

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const VueLoaderPlugin = require("vue-loader/lib/plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const output = path.resolve(__dirname, "../docs-dist");
let entry = path.resolve(__dirname, "../docs/main.js");
const isDev = process.env.NODE_ENV === "development";
const target = process.env.target;
if (target) {
  entry = path.resolve(__dirname, `../${target}/main.js`);
}
const devConfig = {
  mode: isDev ? "development" : "production",
  performance: {
    hints: false,
  },
  stats: {
    modules: false,
    children: false,
    chunks: false,
    chunkModules: false,
  },
  resolve: {
    extensions: [".js", ".jsx", "md", ".vue", ".json"],
    // 设置路径别名
    alias: {
      "@lin-view-ui": path.join(__dirname, "../packages"),
    },
    mainFields: ["doc", "main"],
  },
  entry,
  output: {
    path: output,
    filename: isDev ? "js/[name].js" : "js/[name].[hash].js",
  },
  devtool: isDev ? "cheap-module-eval-source-map" : false,
  devServer: {
    open: true,
    overlay: true, // 错误直接显示在浏览器中
    contentBase: output,
    hot: true,
    historyApiFallback: true,
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        use: ["babel-loader"],
        exclude: /node_modules/,
      },
      {
        test: /\.vue$/,
        loader: "vue-loader",
      },
      {
        test: /\.md$/,
        use: [
          {
            loader: "vue-loader",
          },
          {
            loader: path.resolve(__dirname, "./md-loader/index.js"),
          },
        ],
      },
      {
        test: /\.css$/,
        use: [
          isDev ? "style-loader" : MiniCssExtractPlugin.loader,
          {
            loader: "css-loader",
            options: {
              importLoaders: 1,
            },
          },
          "postcss-loader",
        ],
      },
      {
        test: /\.scss$/,
        use: [
          isDev ? "style-loader" : MiniCssExtractPlugin.loader,
          {
            loader: "css-loader",
            options: {
              importLoaders: 2,
            },
          },
          "postcss-loader",
          "sass-loader",
        ],
      },
      {
        test: /\.(png|jpg|jpeg|gif|eot|ttf|svg|woff|woff2)$/,
        use: {
          loader: "url-loader",
          options: {
            name: "[name].[hash].[ext]",
            outputPath: "images/",
            limit: 10240,
            esModule: false,
          },
        },
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, "../docs/public/index.html"),
      filename: "index.html",
      favicon: path.resolve(__dirname, "../docs/public/favicon.ico"),
    }),
    new VueLoaderPlugin(),
  ],
};
if (!isDev) {
  devConfig.plugins.push(new CleanWebpackPlugin());
  devConfig.plugins.push(
    new MiniCssExtractPlugin({
      filename: "css/[name].[contenthash].css",
      chunkFilename: "css/[name].[contenthash].chunk.css",
    })
  );
}
module.exports = devConfig;

总结

本次架构升级最大的收获就是对rolluplerna有了更深的了解。以及对多包架构模式也有了一定的了解。升级过程中,遇见了不少坑坑洼洼,还好最终都能解决掉。其中有不少的问题都是依赖包的版本问题引起的,比如rollup-plugin-babel最新版本的包不能把vue组件的箭头函数转化为普通函数等等。最后,如果有兴趣一起交流学习的同学,欢迎私信我或者下方留言。同时,如果这篇文章能够帮助你,希望你能够给我点个赞。

github
文档地址

你可能感兴趣的:(笔记,文章,vue,组件库,rollup)