【发布时间】:2022-10-21 21:49:02
【问题描述】:
问题
在过去的一年半里,我一直在为我的团队使用 Storybook、React 和 Webpack 5 开发一个组件库。最近我们开始研究 Next.JS,并且正在使用它的框架进行一个重大项目。然而,这带来了一些挑战,因为 next.js 呈现服务器端和客户端,这意味着任何使用客户端专有对象/函数等的导入都会导致错误。现在这可以使用动态导入来解决,但如果处理不当,则会产生加载时间或丢失内容。
我们的整个组件库会导致此 SSR 错误。无论您是导入实际使用窗口的按钮还是弹出框,都必须使用动态导入。然后,这会在呈现的页面上创建加载时间和缺少内容。我们甚至不能使用库中的加载组件,因为它需要加载。我们还有一个问题,即使我们在代码中删除了对 window 或 document 的所有引用,我们的一些依赖项也会在某个地方引用它们,我们无法避免它。
我们希望能够对库做的是以多种方式导入它,以隔离对其各个组件的窗口和文档调用,因此我们可以尽可能避免动态加载。
import { Component } from 'Library'import { Component } from 'Library/ComponentCategory'-
import { Component } from 'Library/Component'
三个导入背后的原因很简单:
- 我们希望能够导入整个 库和我们需要的任何组件。除了在 Next.JS 中,这不是问题。在 Next.JS 中,我们永远不会以这种方式导入。
- 我们希望能够导入一个组件类别,因此如果我们使用该类别中的多个组件,我们可以一次导入它们,而不是多次导入。即表单组件。这应该只导入它需要的代码和模块。如果一个类别没有引用客户专属代码,那么它应该可以正常导入。
- 我们希望能够导入一个单独的组件,它只带来它需要的代码和模块,所以如果我们需要动态导入,我们会在单个基础上进行,而不是在整个库范围内进行。
这种导入方式已经实现了,但是不管你走哪条路,还是会触发 Next.JS 'self is not defined' 的错误。这似乎意味着,即使在单个组件导入时,仍会引用库的整个代码库。
尝试的解决方案
窗口文档检查和删除不需要的参考
我们删除了对客户端专有代码的任何不必要的引用,并在我们无法删除的任何语句周围添加了条件语句。
if (typeof window !== 'undefined') {
// Do the thing with window i.e window.location.href = '/href'
}
这没有任何影响,主要是由于 npm 生态系统的性质。在代码、文档、屏幕或窗口的某个地方被调用,我对此无能为力。我们可以在这个条件中包装每个导入,但老实说,这非常粗略,如果不采取其他步骤可能无法解决问题。
库拆分
使用 webpack 5 entry、output 和 splitChunks 魔法也没有解决问题。
第一步是配置输入和输出。所以我将我的条目设置为这样的:
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 仍然对它甚至没有使用的代码大喊大叫。
这次冒险的下一步,也是目前的最后一步,是使用 Webpack 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-modules 生成的字符串。
我接下来要尝试什么
所有结果都将发布在线程上
制作一个虚拟测试库
制作一个包含三个简单组件(红色、蓝色、绿色)的库并尝试将它们拆分出来。一个将包含窗口,并使用 npm pack,我们将继续进行更改,直到 Next.JS 中出现某些内容。我不一定认为这会有所帮助,但可能会提高对一切运作方式的理解。
可能的解决方案
Lerna + 微型图书馆
有趣的是,当我第一次开始在图书馆时,我看到了这个,意识到这是一条我不需要对付的龙并逃跑了。这里的解决方案是将类别分离到它们自己的自包含 npm 包中。然后这些将包含在 lerna 环境中。这也可以在没有 lerna 之类的工具的情况下完成,但我们不想安装组件库的一部分,而是全部安装。我仍然觉得这条路线过于复杂,没有必要,并且从长远来看会导致更多的事情需要维护。它还需要重新考虑结构并重写某些部分,即故事书,部署故事书的 docker 映像
在此处使用汇总或插入捆绑程序名称
同样,这个解决方案有一个有趣的轶事与之相伴。许多 JS 开发人员不了解他们使用的一些基本工具。这并不是说他们是糟糕的开发人员,但是像 create-react-app 这样的 CLI 工具会生成大量所需的项目样板,这意味着开发人员可以专注于其应用程序的功能。我和我的同事就是这种情况,所以我们认为从头开始是有意义的。 Webpack 是我选择的捆绑器(感谢上帝对所有这些 webpack 5 的升级)但也许这是错误的决定,我应该使用汇总?
不要使用 Next.js
这可能是 Next.JS 的问题,而实际上 Next.JS 是问题所在。然而,我认为这是一种不好的看待事物的方式。 Next.JS 是一个非常酷的框架,除了这里描述的问题之外,使用起来非常棒。我们现有的已部署应用程序堆栈是; Webpack,哈巴狗和快递。也许决定使用框架是一个糟糕的举动,我们需要重写当前正在开发的应用程序。我确实记得看到 SSR 错误可能是由反应组件生命周期方法/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!
一个答案表明,问题在于注入 HTML 的 CSS 模块,我需要提取。我更新了我的 webpack 中的 PostCSS 规则,在发现问题之前有 extract: true 和 modules: true。我正在使用MiniCSSExtractPlugin 提取所有带有 webpack 的 css。由于我公司开发的 webapps 上的 Content-Security-Policy 样式规则,通过Style-Loader 之类的工具将样式注入 HTML 会破坏一切。反对在开发环境之外使用诸如样式加载器之类的工具也有很好的论据。
我对 webpack 提取进行了更多研究,并看到人们推荐了可以更好地与 SSR 配合使用的不同工具。我已经看到了有关 MiniTextExtractPlugin 的建议(已弃用,有利于 MiniCSSExtractPlugin)、NullLoader(我相信它解决了与我面临的问题完全不同的问题)、CSSLoader/Locales(我在 css- 中找不到文档) loader docs)和其他一些; ObjectLoader,以及 style-loader、iso-style-loader 等。在我研究这个的过程中,我意识到我已经走入了死胡同。也许 MiniCSSExtractPlugin 在使用 SSR 的应用程序的 webpack 中效果不佳,但引用一个旧视频,“这是一个库”。它早在我们在应用程序中安装和使用它之前就已构建、打包和发布。
Next JS next.config.js next-transpile-modules
我根据这篇文章和其他一些帖子更新了我的应用程序的 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 结论
我目前正在用汇总替换捆绑器,只是为了测试它是否有任何效果。
更新 2 Electric Boogaloo
所以汇总是失败的,没有解决任何问题,但确实暴露了一些问题。
由于问题的性质,我决定从库中动态加载所需的任何内容,并从库中提取加载器,以便我可以将其用于动态加载。
如果我设法以我想要的方式解决了这个问题,我会再做一次更新。但是,我相信这只是将 Next 添加到列表中的另一个问题。
【问题讨论】:
标签: reactjs npm webpack webpack-5 code-splitting