如何为next.js拆分webpack + react组件库中的每个组件

bzzcjhmw  于 2022-11-13  发布在  Webpack
关注(0)|答案(1)|浏览(172)

问题所在
在过去的一年半里,我一直在为我的团队使用Storybook、React和Webpack 5开发一个组件库。最近,我们开始关注Next.JS,并使用它的框架进行了一个重要的项目。然而,这也带来了一些挑战,因为Next.js呈现服务器端和客户端。这意味着任何使用客户端独占对象/函数等的导入都会导致错误。现在,这可以通过使用动态导入来解决,但如果处理不正确,则会导致加载时间或丢失内容。
Self Is Not Defined Error
我们的整个组件库导致了这个SSR错误。不管你是导入一个按钮还是一个弹出窗口,你都必须使用动态导入。这会导致加载时间和呈现页面上的内容丢失。我们甚至不能使用库中的加载组件,因为它需要加载。我们还有一个问题,即使我们在代码中去掉了对窗口或文档的所有引用,我们的一些依赖项也会在某个地方引用它们,我们无法避免。
我们希望能够对库做的是以几种方式导入它,以隔离窗口和文档对各自组件的调用,这样我们就可以尽可能避免动态加载。

  • import { Component } from 'Library'
  • import { Component } from 'Library/ComponentCategory'
  • import { Component } from 'Library/Component'

三大进口背后的原因很简单:

  • 我们希望能够导入整个库和任何我们需要的组件。除了在Next.JS中,这不是一个问题。在Next.JS中,我们永远不会这样导入。
  • 我们希望能够导入一个组件类别,因此如果我们使用该类别中的多个组件,我们可以通过一次导入来导入它们,而不是几个。例如,表单组件。这应该只导入它所需要的代码和模块。如果一个类别没有引用客户端独占代码,那么它应该能够正常导入。
  • 我们希望能够导入单独的组件,这只沿着它所需要的代码和模块,所以如果我们需要动态导入,我们在单独的基础上进行,而不是在库范围内。

这种导入方式已经实现了,但是无论你选择哪种方式,它仍然会引发Next.JS 'self is not defined'错误。这似乎意味着,即使是在单个组件导入时,库的整个代码库仍然被引用。

尝试的解决方案

Window文档检查和删除不需要的引用

我们删除了对客户端独占代码的任何不必要的引用,并在无法删除的任何语句周围添加了条件语句。

if (typeof window !== 'undefined') {
   // Do the thing with window i.e window.location.href = '/href'
}

这没有任何效果,主要是由于npm生态系统的性质。在代码、文档、屏幕或窗口中的某个地方被调用了,对此我无能为力。我们可以用这个条件 Package 每个导入,但说实话,这是相当严重的,如果不采取其他步骤,可能无法解决问题。

媒介柜分割

使用webpack 5 entryoutputsplitChunks magic也没有解决问题。
第一步是配置输入和输出,因此我将输入设置为如下形式:

entry: {
    // Entry Points //
    //// Main Entry Point ////
    main: './src/index.ts',
    //// Category Entry Points ////
    Buttons: './src/components/Buttons', // < - This leads to an index.ts
    ...,
    //// Individual Component Entry Points ////
    Button: './src/components/Buttons/Button.tsx',
    OtherComponent: '...',
    ...,
},

和我的输出:

output: {
    path: path.resolve(__dirname, './dist'),
    filename: '[name].js',
    library: pkg.name,
    libraryTarget: 'umd',
    umdNamedDefine: true,
  },

这使得我们现在可以通过分类或单独的组件导入整个库。我们可以看到这一点,因为在库的dist文件夹中,现在有Component.js(.map)文件。不幸的是,这仍然不允许我们悄悄地越过SSR错误。我们可以import Button from Library/dist/Button,但Next.JS仍然尖叫着代码甚至没有使用。
这次冒险的下一步,也是目前的最后一步,是使用Webpacks的splitChunks功能,以及输入/输出的更改。

optimization: {
    runtimeChunk: 'single',
    splitChunks: {
      chunks: 'all',
      maxInitialRequests: Infinity,
      minSize: 0,
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name(module) {
            // get the name. E.g. node_modules/packageName/not/this/part.js
            // or node_modules/packageName
            const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];

            // npm package names are URL-safe, but some servers don't like @ symbols
            return `npm.${packageName.replace('@', '')}`;
          },
        },
      },
    },
  },

这也不起作用,虽然我不能100%确定它是否正确触发,因为我在dist文件夹中没有看到npm.packageName。现在有一堆4531.js(3-4个数字后面跟着js),但是打开这些,包含在webpack生成的代码中,是我写的一些类名,或者是为我的scss模块生成的字符串。

我接下来要尝试的内容

所有结果都将在Thread上发布

生成虚拟测试库

创建一个包含三个简单组件的库(Red,Blue,绿色)并尝试将它们拆分出来。其中一个包含window,使用npm包,我们将不断地进行更改,直到Next.JS中有内容为止。我不认为这一定会有帮助,但可能会提高对一切工作方式的理解。

可能的解决方案

Lerna +微型图书馆

有趣的是,当我第一次开始使用这个库的时候,我看到了这个,意识到这是一个我不需要对付的龙,然后就逃跑了。这里的解决方案是,将这些类别分离到它们自己的自包含的npm包中。这些包将被包含在一个lerna环境中。这也可以在没有像lerna这样的工具的情况下完成。但是我们不想安装部分的组件库,而是要安装全部的组件库。2我还是觉得这个方法太复杂了,不必要的,从长远来看会导致更多的东西需要维护。这也将需要重新思考结构和重写一些章节i。故事书,即部署故事书的Docker映像

使用汇总或在此处插入捆绑包名称

同样,这个解决方案也有一个有趣的轶事。很多JS开发人员不理解他们使用的一些基本工具。这并不是说他们是糟糕的开发人员,但是像create-react-app这样的CLI工具生成了很多所需的项目样板,这意味着开发人员可以专注于他们应用程序的功能。我和我的同事就是这样。所以我们决定从头开始是有意义的。Webpack是我选择的捆绑器(感谢上帝所有那些Webpack 5升级),但也许这是错误的决定,我应该使用Rollup?

不要使用Next.js

有可能这是Next.JS的问题,而实际上Next.JS就是问题所在。我认为这是一个不好的看待问题的方式。Next.JS是一个非常酷的框架,除了这里描述的问题之外,它的使用非常棒。我们现有的部署应用程序堆栈是; Webpack,pug和express。也许决定使用框架是一个糟糕的举动,我们需要重写当前正在开发的应用程序。我确实记得看到SSR错误可能是由React组件生命周期方法/useEffect引起的,所以也许这一直是真实的的罪魁祸首。
额外
该库使用pnpm作为其软件包管理器。

程式库相依性

"dependencies": {
    "@fortawesome/fontawesome-pro": "^5.15.4",
    "@fortawesome/fontawesome-svg-core": "^1.2.36",
    "@fortawesome/free-regular-svg-icons": "^5.15.4",
    "@fortawesome/free-solid-svg-icons": "^5.15.4",
    "@fortawesome/pro-regular-svg-icons": "^5.15.4",
    "@fortawesome/react-fontawesome": "^0.1.16",
    "classname": "^0.0.0",
    "classnames": "^2.3.1",
    "crypto-js": "^4.1.1",
    "date-fns": "^2.28.0",
    "formik": "^2.2.9",
    "html-react-parser": "^1.4.5",
    "js-cookie": "^3.0.1",
    "lodash": "^4.17.21",
    "nanoid": "^3.2.0",
    "react-currency-input-field": "^3.6.4",
    "react-datepicker": "^4.6.0",
    "react-day-picker": "^7.4.10",
    "react-modal": "^3.14.4",
    "react-onclickoutside": "^6.12.1",
    "react-router-dom": "^6.2.1",
    "react-select-search": "^3.0.9",
    "react-slider": "^1.3.1",
    "react-tiny-popover": "^7.0.1",
    "react-toastify": "^8.1.0",
    "react-trix": "^0.9.0",
    "trix": "1.3.1",
    "yup": "^0.32.11"
  },
  "devDependencies": {
    "postcss-preset-env": "^7.4.2",
    "@babel/core": "^7.16.12",
    "@babel/preset-env": "^7.16.11",
    "@babel/preset-react": "^7.16.7",
    "@babel/preset-typescript": "^7.16.7",
    "@dr.pogodin/babel-plugin-css-modules-transform": "^1.10.0",
    "@storybook/addon-actions": "^6.4.14",
    "@storybook/addon-docs": "^6.4.14",
    "@storybook/addon-essentials": "^6.4.14",
    "@storybook/addon-jest": "^6.4.14",
    "@storybook/addon-links": "^6.4.14",
    "@storybook/addons": "^6.4.14",
    "@storybook/builder-webpack5": "^6.4.14",
    "@storybook/manager-webpack5": "^6.4.14",
    "@storybook/react": "^6.4.14",
    "@storybook/theming": "^6.4.14",
    "@svgr/webpack": "^6.2.0",
    "@testing-library/react": "^12.1.2",
    "@types/enzyme": "^3.10.11",
    "@types/enzyme-adapter-react-16": "^1.0.6",
    "@types/jest": "^27.4.0",
    "@types/react": "^17.0.38",
    "@types/react-datepicker": "^4.3.4",
    "@types/react-dom": "^17.0.11",
    "@types/react-slider": "^1.3.1",
    "@types/yup": "^0.29.13",
    "@typescript-eslint/eslint-plugin": "^5.10.1",
    "@typescript-eslint/parser": "^5.10.1",
    "@vgrid/sass-inline-svg": "^1.0.1",
    "@wojtekmaj/enzyme-adapter-react-17": "^0.6.6",
    "audit-ci": "^5.1.2",
    "babel-loader": "^8.2.3",
    "babel-plugin-inline-react-svg": "^2.0.1",
    "babel-plugin-react-docgen": "^4.2.1",
    "babel-plugin-react-remove-properties": "^0.3.0",
    "clean-css-cli": "^5.5.0",
    "clean-webpack-plugin": "^4.0.0",
    "copy-webpack-plugin": "^10.2.1",
    "css-loader": "^6.5.1",
    "css-modules-typescript-loader": "^4.0.1",
    "dependency-cruiser": "^11.3.0",
    "enzyme": "^3.11.0",
    "eslint": "^8.7.0",
    "eslint-config-airbnb": "^19.0.4",
    "eslint-config-airbnb-typescript": "^16.1.0",
    "eslint-config-prettier": "^8.3.0",
    "eslint-import-resolver-node": "^0.3.6",
    "eslint-import-resolver-typescript": "^2.5.0",
    "eslint-plugin-css-modules": "^2.11.0",
    "eslint-plugin-import": "^2.25.4",
    "eslint-plugin-jest": "^26.0.0",
    "eslint-plugin-jsx-a11y": "^6.5.1",
    "eslint-plugin-prettier": "^4.0.0",
    "eslint-plugin-react": "^7.28.0",
    "eslint-plugin-react-hooks": "^4.3.0",
    "eslint-plugin-sonarjs": "^0.11.0",
    "eslint-webpack-plugin": "^3.1.1",
    "html-webpack-plugin": "^5.5.0",
    "husky": "^7.0.0",
    "identity-obj-proxy": "^3.0.0",
    "jest": "^27.4.7",
    "jest-environment-enzyme": "^7.1.2",
    "jest-environment-jsdom": "^27.4.6",
    "jest-enzyme": "^7.1.2",
    "jest-fetch-mock": "^3.0.3",
    "jest-sonar-reporter": "^2.0.0",
    "jest-svg-transformer": "^1.0.0",
    "lint-staged": "^12.3.1",
    "mini-css-extract-plugin": "^2.5.3",
    "narn": "^2.1.0",
    "node-notifier": "^10.0.0",
    "np": "^7.6.0",
    "postcss": "^8.4.5",
    "postcss-loader": "^6.2.1",
    "precss": "^4.0.0",
    "prettier": "^2.5.1",
    "prettier-eslint": "^13.0.0",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-is": "^17.0.2",
    "sass": "^1.49.0",
    "sass-loader": "^12.4.0",
    "sass-true": "^6.0.1",
    "sonarqube-scanner": "^2.8.1",
    "storybook-formik": "^2.2.0",
    "style-loader": "^3.3.1",
    "ts-jest": "^27.1.3",
    "ts-loader": "^9.2.6",
    "ts-prune": "^0.10.3",
    "typescript": "^4.5.5",
    "typescript-plugin-css-modules": "^3.4.0",
    "url-loader": "^4.1.1",
    "webpack": "^5.67.0",
    "webpack-cli": "^4.9.2",
    "webpack-dev-server": "^4.7.3",
    "webpack-node-externals": "^3.0.0"
  },
  "peerDependencies": {
    "react": ">=16.14.0",
    "react-dom": ">=16.14.0"
  },

谢谢你的阅读,任何建议都会很棒。

更新1

首先,这里是我忘记包括的webpack配置,减去所有入口点。

const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const nodeExternals = require('webpack-node-externals');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const inliner = require('@vgrid/sass-inline-svg');
const ESLintPlugin = require('eslint-webpack-plugin');
const pkg = require('./package.json');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  // Note: Please add comments to new entry point category additions
  entry: {
    // Entry Points //
    //// Main Entry Point ////
    main: './src/index.ts',
    //// Category Entry Points ////
    Buttons: './src/components/Buttons/index.ts',
    ...
  },
  // context: path.resolve(__dirname),
  resolve: {
    modules: [__dirname, 'node_modules'],
    extensions: ['.ts', '.tsx', '.js', '.jsx', '.json', '.scss', '.css'],
  },
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: '[name].js',
    library: pkg.name,
    libraryTarget: 'umd',
    umdNamedDefine: true,
  },
  optimization: {
    runtimeChunk: 'single',
    splitChunks: {
      chunks: 'all',
      maxInitialRequests: Infinity,
      minSize: 0,
      minChunks: 1,
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name(module) {
            // get the name. E.g. node_modules/packageName/not/this/part.js
            // or node_modules/packageName
            const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];

            // npm package names are URL-safe, but some servers don't like @ symbols
            return `npm.${packageName.replace('@', '')}`;
          },
        },
      },
    },
  },
  devtool: 'source-map',
  module: {
    rules: [
      // ! This rule generates the ability to use S/CSS Modules but kills global css
      {
        test: /\.(scss|css)$/,
        use: [
          MiniCssExtractPlugin.loader,
          { loader: 'css-modules-typescript-loader' },
          {
            loader: 'css-loader', //2
            options: {
              modules: {
                localIdentName: '[local]_[hash:base64:5]',
              },
              importLoaders: 1,
            },
          },
          {
            loader: 'postcss-loader',
            options: {
              postcssOptions: {
                extract: true,
                modules: true,
                use: ['sass'],
              },
            },
          },
          'sass-loader',
        ],
        include: /\.module\.css$/,
      },
      // ! This allows for global css alongside the module rule.  Also generates the d.ts files for s/css modules (Haven't figured out why).
      {
        test: /\.(scss|css)$/,
        use: [
          MiniCssExtractPlugin.loader,
          { loader: 'css-modules-typescript-loader' },
          'css-loader',
          {
            loader: 'postcss-loader',
            options: {
              postcssOptions: {
                extract: true,
                use: ['sass'],
              },
            },
          },
          'sass-loader',
        ],
        exclude: /\.module\.css$/,
      },
      {
        test: /\.(ts|tsx)$/,
        loader: 'ts-loader',
        exclude: /node_modules/,
      },
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
        },
      },
      // {
      //   test: /\.(js|jsx|ts|tsx)$/,
      //   exclude: /node_modules/,
      //   use: {
      //     loader: 'eslint-webpack-plugin',
      //   },
      // },
      {
        test: /\.(png|jpg|jpeg|woff|woff2|eot|ttf)$/,
        type: 'asset/resource',
      },
      {
        test: /\.svg$/,
        use: ['@svgr/webpack', 'url-loader'],
      },
    ],
  },

  plugins: [
    new CleanWebpackPlugin(),
    new CopyWebpackPlugin({
      patterns: [{ from: './src/scss/**/*.scss', to: './scss/' }],
    }),
    new MiniCssExtractPlugin(),
    new ESLintPlugin(),
  ],
  externals: [nodeExternals()],
};

解压缩CSS!!!

一个答案是,问题是CSS模块被注入到HTML中,我需要提取。我更新了我的webpack中的PostCSS规则,在认识到问题之前有extract: truemodules: true。我正在使用MiniCSSExtractPlugin提取webpack中的所有CSS。由于我的公司开发的webapps上的内容安全策略样式规则,通过像Style-Loader这样的工具将样式注入到HTML中会破坏一切。也有很好的理由反对在开发环境之外使用像style-loader这样的工具。
我对webpack提取做了更多的研究,看到人们推荐了不同的工具,这些工具可以更好地处理SSR。我看到了关于MiniTextExtractPlugin(它被MiniCSSExtractPlugin取代了)、NullLoader(我相信它解决了一个与我所面临的问题完全不同的问题)、CSSLoader/Locales(我在css-loader文档中找不到它的文档)和其他一些工具的推荐; ObjectLoader,以及样式加载器,iso-style-loader等。在我的研究过程中,我意识到我是在一个死胡同。也许MiniCSSCextractPlugin在一个使用SSR的应用程序的webpack中工作得很差,但引用一个老视频,“这是一个库”。它的建立,打包和发布早在我们在我们的应用程序中安装和使用它之前。

Next.js next.config.js下一个传输模块

我根据这篇文章和其他一些文章更新了我的应用程序的Next.JS配置。https://github.com/vercel/next.js/issues/10975#issuecomment-605528116
现在这是我的next.js配置

const withTM = require('next-transpile-modules')(['@company/package']); // pass the modules you would like to see transpiled

module.exports = withTM({
  webpack: (config, { isServer }) => {
    // Fixes npm packages that depend on `fs` module
    if (!isServer) {
      config.resolve.fallback = {
        fs: false,
      };
    }
    return config;
  },
});

这也没有解决问题。

停止将SCSS与库捆绑在一起

库使用CopyWebpackPlugin将所有scss复制到构建中的一个目录中。这允许我们公开mixin、变量、公共全局类名等。在调试webpack的尝试中,我关闭了它。这没有任何效果,但我还是会记录下来。

更新1结论

我目前正在用rollup替换bundler,只是为了测试它是否有任何效果。

更新2电动布加罗

因此,汇总是一个失败,没有解决任何问题,但确实带来了一些问题。
由于问题的性质,我决定只从库中动态加载所需的任何内容,并从库中提取加载程序,以便我可以将其用于动态加载。
如果我设法解决这个问题的方式,我打算,我会作出另一个更新。但我相信,这只是另一个问题与下一个添加到列表。

1mrurvl1

1mrurvl11#

几个月前我在创建react库时遇到过同样的问题。我试着从webpack改为rollup,但这不是问题的直接原因。所以我检查了捆绑的代码,发现问题是两个捆绑器处理css的方式(我的组件使用了css模块)。它们使用“window”和“document”来动态地将css追加到文档中。所以我搜索了很多,并设法配置bundlers来提取css,以正确的方式进行服务器端渲染。下面是我的rollup.config.js,如果你需要它:

import commonjs from "@rollup/plugin-commonjs";
import resolve from "@rollup/plugin-node-resolve";
import typescript from "@rollup/plugin-typescript";
import dts from "rollup-plugin-dts";
import peerDepsExternal from "rollup-plugin-peer-deps-external";
import postcss from "rollup-plugin-postcss";
import { terser } from "rollup-plugin-terser";

const packageJson = require("./package.json");

export default [
  {
    input: "src/index.ts",
    output: [
      {
        file: packageJson.main,
        format: "cjs",
        sourcemap: true,
      },
      {
        file: packageJson.module,
        format: "esm",
        sourcemap: true,
      },
    ],
    plugins: [
      resolve(),
      commonjs(),
      peerDepsExternal(),
      typescript({
        tsconfig: "./tsconfig.json",
        include: [
          "src/**/*"
        ],
        exclude: [
          "node_modules",
          "**/*.test.ts",
          "**/*.test.tsx",
          "**/*.stories.tsx",
        ],
      }),

      postcss({
        extract: true,
        modules: true,
        use: ["sass"],
      }),
      terser(),
    ],
  },
  {
    input: "dist/esm/types/index.d.ts",
    output: [{ file: "dist/index.d.ts", format: "esm" }],
    plugins: [dts()],
  },
];

相关问题