javascript - 扩展浏览器原生类时使用 Webpack

标签 javascript reactjs typescript webpack

我正在将 Webpack 与 React 和 Typescript 结合使用,我正在尝试为浏览器 native 类 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];
};

更新 2 下午 36 点 编译后的结果如下:

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

最佳答案

下午 6:42 更新:经过进一步测试,原始答案确实构建正确但未正确运行。尽管显式地将原型(prototype)设置为 WebSocket,它仍然在 super() 期间调用 WebSocketMock。

第二种方法确实有效,只是发现你根本无法在 Chrome 中扩展 WebSocket,因为你总是会得到错误 Failed to construct 'WebSocket': Please use the 'new' operator, this DOM 对象构造函数不能作为函数调用。

万一其他人需要扩展可以扩展的浏览器原生类,这就是成功完成的方式:

///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>>");
}

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

OP 在这里,有效的解决方案意味着创建一个模拟类 WebSocketMock,它具有与 WebSocket 所有相同的属性,但没有实现,并且具有 WebSocketConnection 扩展 WebSocketMock。之后,我会将 WebSocketConnection 的原型(prototype)更新为 WebSocket(如果存在)。此 if 语句在浏览器中为真,但在节点中为假。

TypeScript 解决方案:

/* 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);

关于javascript - 扩展浏览器原生类时使用 Webpack,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/51448557/

相关文章:

javascript - 如何以两种方式将 css 转换隐藏在另一个元素后面?

javascript - Typescript 模块加载错误

javascript - jQuery 自定义选项卡无法正常工作

reactjs - 在 TypeScript 中定义 React 组件的 contextTypes 的正确方法是什么?

typescript - 仅对定义文件允许隐式任何

javascript - 如何在使用ajax提交的codeigniter中使用form_validation验证表单数据

Javascript : Scroll to top to the div when enter key is pressed

javascript - 如何使用 recompose hocs 测试 React 组件

angularjs - Angular 2 V/S Angular js V/S React js v/s typescript

javascript - 无法读取未定义的属性 "toFixed"