首页 > 解决方案 > 扩展浏览器原生类时使用 Webpack

问题描述

我正在使用带有 React 和 Typescript 的 Webpack,并且我正在尝试为 WebSocket 创建一个包装器类,这是一个浏览器原生类。

该类在一个文件中webSocketConnection.ts,看起来像这样:

export default class WebSocketConnection extends WebSocket {
    constructor(url: string, protocols?: string | string[]) {
        super(url, protocols);
    }
}

一个单独的文件导入并使用它

import WebSocketConnection from './webSocketConnection';

export function Connect() {
    return new WebSocketConnection("<<someUrl>>");
}

它构建得很好,但是在运行站点时我得到了NodeInvocationException: Prerendering failed because of error: ReferenceError: WebSocket is not defined.

据我了解,这是由于节点找不到WebSocket对象而导致的服务器端错误,即使它在客户端上运行良好。仅在使用new Websocket("<<someUrl>>").

我的期望是,可以通过从捆绑或从服务器中排除特定文件来解决这个问题。

webpack.config.js的是

const path = require('path');
const webpack = require('webpack');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const CheckerPlugin = require('awesome-typescript-loader').CheckerPlugin;
const merge = require('webpack-merge');

module.exports = (env) => {
    const isDevBuild = !(env && env.prod);

    // Configuration in common to both client-side and server-side bundles
    const sharedConfig = () => ({
        stats: { modules: false },
        resolve: {
            extensions: ['.js', '.jsx', '.ts', '.tsx'],
            alias: {
                ["~"]: path.resolve(__dirname, "ClientApp"),
            }
        },
        output: {
            filename: '[name].js',
            publicPath: 'dist/' // Webpack dev middleware, if enabled, handles requests for this URL prefix
        },
        module: {
            rules: [
                { test: /\.tsx?$/, include: /ClientApp/, use: 'awesome-typescript-loader?silent=true' },
                { test: /\.(png|jpg|jpeg|gif|svg)$/, use: 'url-loader?limit=25000' }
            ]
        },
        plugins: [new CheckerPlugin()]
    });

    // Configuration for client-side bundle suitable for running in browsers
    const clientBundleOutputDir = './wwwroot/dist';
    const clientBundleConfig = merge(sharedConfig(), {
        entry: { 'main-client': './ClientApp/boot-client.tsx' },
        module: {
            rules: [
                {
                    test: /\.css$/,
                    oneOf: [
                        {
                            resourceQuery: /raw/,
                            use: ['style-loader', 'css-loader']
                        },
                        {
                            use: ExtractTextPlugin.extract({ use: isDevBuild ? 'css-loader' : 'css-loader?minimize' })
                        }
                    ]
                },
                {
                    test: /\.less$/,
                    use: ExtractTextPlugin.extract(['css-loader', 'less-loader'])
                },
                {
                    test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/,
                    use: [{
                        loader: 'file-loader',
                        options: {
                            name: '[name].[ext]',
                            outputPath: 'fonts/'
                        }
                    }]
                }
            ]
        },
        output: { path: path.join(__dirname, clientBundleOutputDir) },
        plugins: [
            new ExtractTextPlugin('site.css'),
            new webpack.DllReferencePlugin({
                context: __dirname,
                manifest: require('./wwwroot/dist/vendor-manifest.json')
            })
        ].concat(isDevBuild ? [
            // Plugins that apply in development builds only
            new webpack.SourceMapDevToolPlugin({
                filename: '[file].map', // Remove this line if you prefer inline source maps
                moduleFilenameTemplate: path.relative(clientBundleOutputDir, '[resourcePath]') // Point sourcemap entries to the original file locations on disk
            })
        ] : [
                // Plugins that apply in production builds only
                new webpack.optimize.UglifyJsPlugin()
            ])
    });

    // Configuration for server-side (prerendering) bundle suitable for running in Node
    const serverBundleConfig = merge(sharedConfig(), {
        resolve: { mainFields: ['main'] },
        module: {
            rules: [
                { test: /\.css$/, loader: 'ignore-loader' },
                { test: /\.less$/, loader: 'ignore-loader' }
            ]
        },
        entry: { 'main-server': './ClientApp/boot-server.tsx' },
        plugins: [
            new webpack.DllReferencePlugin({
                context: __dirname,
                manifest: require('./ClientApp/dist/vendor-manifest.json'),
                sourceType: 'commonjs2',
                name: './vendor'
            })
        ],
        output: {
            libraryTarget: 'commonjs',
            path: path.join(__dirname, './ClientApp/dist')
        },
        target: 'node',
        devtool: 'inline-source-map'
    });

    return [clientBundleConfig, serverBundleConfig];
};

UPDATE 2 36pm 转译 后的结果是这样的:

var WebSocketConnection = (function (_super) {
    __extends(WebSocketConnection, _super);
    function WebSocketConnection(url, protocols) {
        return _super.call(this, url, protocols) || this;
    }
    return WebSocketConnection;
}(WebSocket));

标签: javascriptreactjstypescriptwebpack

解决方案


下午 6:42 更新:经过进一步测试,原始答案确实构建正确,但运行不正确。尽管将原型显式设置为 WebSocket,但在super().

第二种方法确实有效,只是发现您根本无法在 Chrome 中扩展 WebSocket,因为您总是会收到错误消息Failed to construct 'WebSocket': Please use the 'new' operator, this DOM object constructor cannot be called as a function.

如果其他人需要扩展一个可以扩展的浏览器原生类,那么它是如何成功完成的:

///Inside of file webSocketConnection.ts
export interface WebSocketConnection extends WebSocket {
    //Custom properties here
}

let classVar: any;

if (typeof(WebSocket) !== 'undefined') {
    classVar= class WebSocketConnection extends WebSocket {
        constructor(url: string, protocols?: string | string[]) {
            super(url, protocols);
        }
    }
}

export default function(url: string, protocols?: string | string[]): WebSocketConnection {
    return new classVar(url, protocols) as WebSocketConnection;
}

--

///Inside of a second file
import createWebSocket, { WebSocketConnection } from './webSocketConnection';

function DoSomething() {
    //Note no "new" keyword used, because this function isn't actually a constructor
    let socket: WebSocketConnection = createWebSocket("<<someUrl>>");
}

为了完成,非 TypeScript 解决方案看起来像这样:

///Inside of file webSocketConnection.js
let classVar;

if (typeof(WebSocket) !== 'undefined') {
    classVar = class WebSocketConnection extends WebSocket {
        constructor(url, protocols) {
            super(url, protocols);
        }
    }
}

export default function(url, protocols) {
    return new classVar(url, protocols);
}

--

///Inside of a second file
import createWebSocket from './webSocketConnection';

function DoSomething() {
    //Note no "new" keyword used, because this function isn't actually a constructor
    let socket = createWebSocket("<<someUrl>>");
}

原始答案 - 没有用,但留在这里,因为它可能会为某人提供洞察力

在这里,有效的解决方案意味着创建一个模拟类,该类WebSocketMock具有与 的所有相同属性WebSocket,但未实现,并且具有WebSocketConnection扩展WebSocketMock。之后,如果存在的话,我会更新WebSocketConnectionto be的原型。WebSocketif句话在浏览器中是正确的,但在节点中是错误的。

打字稿解决方案:

/* Mock class = WebSocketMock; new empty class that looks similar to original class
 * Original class = WebSocket; browser-only class we want to extend
 * New class = WebSocketConnection; class that extends original class
 */

/* Creating a blank interface, with the same name as the mock class,
 * that extends the original interface we're trying to mock
 * allows the mock class to have all the properties of the original class
 * without having to actually implement blank versions of them
 */
interface WebSocketMock extends WebSocket {
}

/* The mock class must have the same constructor as the original class
 * so that the new class can use super() with the right signature
 */
class WebSocketMock {
    constructor(url: string, protocols?: string | string[]) {
    }
}

// New class extends the mock class
export default class WebSocketConnection extends WebSocketMock {
    constructor(url: string, protocols?: string | string[]) {
        super(url, protocols);
    }

    //Other properties and code will be added here
}

/* Updates the prototype of the new class to use the original class
 * when the original class exists. Of course, if you try to use the new
 * class in an environment (read: browser) that doesn't have the original
 * class, everything would break, as it's just an empty "shim"
 */
if (typeof (WebSocket) !== 'undefined')
    Object.setPrototypeOf(WebSocketConnection, WebSocket);

如果没有 typescript,它可能看起来像这样(我没有使用 Webpack 进行测试的无 TypeScript 环境)

class WebSocketMock {
    constructor(url, protocols) {
    }
}

export default class WebSocketConnection extends WebSocketMock {
    constructor(url, protocols) {
        super(url, protocols);
    }

    //Other properties and code will be added here
}

if (typeof (WebSocket) !== 'undefined')
    Object.setPrototypeOf(Object.getPrototypeOf(WebSocketConnection), WebSocket);

推荐阅读