问题所在
在过去的一年半里,我一直在为我的团队使用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 entry
、output
和splitChunks
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: true
和modules: 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电动布加罗
因此,汇总是一个失败,没有解决任何问题,但确实带来了一些问题。
由于问题的性质,我决定只从库中动态加载所需的任何内容,并从库中提取加载程序,以便我可以将其用于动态加载。
如果我设法解决这个问题的方式,我打算,我会作出另一个更新。但我相信,这只是另一个问题与下一个添加到列表。
1条答案
按热度按时间1mrurvl11#
几个月前我在创建react库时遇到过同样的问题。我试着从webpack改为rollup,但这不是问题的直接原因。所以我检查了捆绑的代码,发现问题是两个捆绑器处理css的方式(我的组件使用了css模块)。它们使用“window”和“document”来动态地将css追加到文档中。所以我搜索了很多,并设法配置bundlers来提取css,以正确的方式进行服务器端渲染。下面是我的rollup.config.js,如果你需要它: