package.json 中用于 Node 和浏览器的不同主入口点

19

在同构的React应用中,我有一个名为myModule的模块,它在Node和浏览器环境下的行为应该不同。我想在package.json中为myModule配置这个分离点:

package.json

{
  "private": true,
  "name": "myModule",
  "main": "./myModule.server.js",
  "browser": "./myModule.client.js"
}

文件结构

├── myModule
│   ├── myModule.client.js
│   ├── myModule.server.js
│   └── package.json
│ 
├── browser.js
└── server.js

所以当我在Node.js中使用myModule时,我应该只得到myModule.server.js

server.js

import myModule from './myModule';
myModule(); // invoke myModule.server.js

浏览器端应该只使用myModule.client.js构建捆绑包: browser.js
import myModule from './myModule';
myModule(); // invoke myModule.client.js

React Starter Kit使用这种方法,但我无法确定此配置定义在哪里。


动机

  1. package.json是执行此类拆分的良好语义点。
  2. 客户端捆绑包仅包含myModule.client.js

已知解决方案-对我来说不是答案

您可以拥有此类文件结构:

├── myModule
│    ├── myModule.client.js
│    ├── myModule.server.js
│    └── index.js           <-- difference
│ 
├── browser.js
└── server.js

并且在index.js中:

if (process.browser) { // this condition can be different but you get the point
    module.exports = require('./myModule.client');
} else {
    module.exports = require('./myModule.server');
}

这样做的主要问题是客户端捆绑包中包含了大量重 kB 的后端代码。


我的 webpack 配置

我包含了我的 webpack.config.js。奇怪的是,这个配置总是指向 myModule.client.js 用于在浏览器和 node 中。

const webpack = require('webpack');
var path = require('path');
var fs = require('fs');

const DEBUG = !process.argv.includes('--release');
const VERBOSE = !process.argv.includes('--verbose');
const AUTOPREFIXER_BROWSERS = [
    'Android 2.3',
    'Android >= 4',
    'Chrome >= 35',
    'Firefox >= 31',
    'Explorer >= 9',
    'iOS >= 7',
    'Opera >= 12',
    'Safari >= 7.1',
];

let nodeModules = {};
fs.readdirSync('node_modules')
    .filter(function(x) {
        return ['.bin'].indexOf(x) === -1 ;
    })
    .forEach(function(mod) {
        nodeModules[mod] = 'commonjs ' + mod;
    });

let loaders = [
    {
        exclude: /node_modules/,
        loader: 'babel'
    },
    {
        test: [/\.scss$/,/\.css$/],
        loaders: [
            'isomorphic-style-loader',
            `css-loader?${DEBUG ? 'sourceMap&' : 'minimize&'}modules&localIdentName=` +
            `${DEBUG ? '[name]_[local]_[hash:base64:3]' : '[hash:base64:4]'}`,
            'postcss-loader?parser=postcss-scss'
        ]
    },
    {
        test: /\.(png|jpg|jpeg|gif|svg|woff|woff2)$/,
        loader: 'url-loader',
        query: {
            name: DEBUG ? '[name].[ext]' : '[hash].[ext]',
            limit: 10000,
        },
    },
    {
        test: /\.(eot|ttf|wav|mp3)$/,
        loader: 'file-loader',
        query: {
            name: DEBUG ? '[name].[ext]' : '[hash].[ext]',
        },
    },
    {
        test: /\.json$/,
        loader: 'json-loader',
    },
];

const common = {
    module: {
        loaders
    },
    plugins: [
        new webpack.optimize.OccurenceOrderPlugin(),
    ],
    postcss: function plugins(bundler) {
        var plugins = [
            require('postcss-import')({ addDependencyTo: bundler }),
            require('precss')(),
            require('autoprefixer')({ browsers: AUTOPREFIXER_BROWSERS }),
        ];

        return plugins;
    },
    resolve: {
        root: path.resolve(__dirname, 'src'),
        extensions: ['', '.js', '.jsx', '.json']
    }
};


module.exports = [
    Object.assign({} , common, { // client
        entry: [
            'babel-polyfill',
            './src/client.js'
        ],
        output: {
            path: __dirname + '/public/',
            filename: 'bundle.js'
        },
        target: 'web',
        node: {
            fs: 'empty',
        },
        devtool: DEBUG ? 'cheap-module-eval-source-map' : false,
        plugins: [
            ...common.plugins,
            new webpack.DefinePlugin({'process.env.BROWSER': true }),
        ],
    }),
    Object.assign({} , common, { // server
        entry: [
            'babel-polyfill',
            './src/server.js'
        ],
        output: {
            path: __dirname + '',
            filename: 'server.js'
        },
        target: 'node',
        plugins: [
            ...common.plugins,
            new webpack.DefinePlugin({'process.env.BROWSER': false }),
        ],
        node: {
            console: false,
            global: false,
            process: false,
            Buffer: false,
            __filename: false,
            __dirname: false,
        },
        externals: nodeModules,

    })
];

似乎react-starter-kit也有不同的webpack配置用于客户端和服务器端。不确定这是否是使魔法发生的原因,但我想你可以看一下:https://github.com/kriasoft/react-starter-kit/blob/master/tools/webpack.config.js#L202 和 https://github.com/kriasoft/react-starter-kit/blob/master/tools/webpack.config.js#L261 - Vasil Dininski
@VasilDininski 我也有两个配置文件(只是看起来有点不同)。在 module.exports 中,我将这两个配置合并成一个数组。react-starter-kit 中也是如此 https://github.com/kriasoft/react-starter-kit/blob/master/tools/webpack.config.js#L309 - Everettss
问题是./server.js没有输出,还是与./public/bundle.js具有相同的内容?至于package.json中的'main' vs 'browser' - 我不认为webpack或浏览器在很大程度上知道或关心package.json,因此您应该将其设置为构建的服务器js。 - Michael Pratt
4个回答

14

行为在这里是标准化的:https://github.com/defunctzombie/package-browser-field-spec

虽然此规范不是官方的,但许多Javascript绑定器都遵循它,包括Webpack、Browserify和React Native打包程序。浏览器字段不仅允许您更改模块入口,还可以替换或忽略模块内的单个文件。它非常强大。

由于Webpack默认为Web捆绑代码,因此如果要将Webpack用于服务器构建,则需要手动禁用浏览器字段。您可以使用target配置选项来完成此操作:https://webpack.js.org/concepts/targets/


6
这个问题已经被问了很长时间了。我只是想澄清之前的答案。
如果您查看React Starter Kit中的tools/webpack.config.js,您将看到它导出了两个略有不同的Webpack配置,例如module.exports = [clientConfig, sererConfig]。服务器端bundle配置将此字段目标设置为node(默认情况下为web)。
似乎这种webpack行为没有记录在案,但是当目标是“node”时,webpack会自动采用“main”条目,当目标是“web”时,webpack会采用“browser”条目。

3
我来晚了,但我认为这份文档是我们正在寻找的:https://webpack.js.org/configuration/resolve/#resolvemainfields因此,当在您的webpack配置中设置target: node时,webpack默认会首先搜索module字段 - 如果不存在,则会使用main字段。同样,target: web的优先顺序是浏览器,然后是模块,最后是主要的。 - Bernard Leech

1
如果你查看React Starter Kit中的tools/webpack.config.js,你会发现它导出了两个略有不同的Webpack配置,例如module.exports = [clientConfig, sererConfig]。服务器端的bundle配置将target字段设置为node(默认情况下为web)。

https://webpack.github.io/docs/configuration.html#target

你所描述的方法非常适用于具有完全相同API但不同实现的模块,例如使用XMLHttpRequest的浏览器特定实现和Node的http模块的服务器实现的HTTP客户端工具:

https://github.com/kriasoft/react-starter-kit/tree/master/src/core/fetch


0
为了在Node模块中为客户端和服务器设置不同的入口点,您可以使用process.browser标志并处理相同的内容。
if (process.browser) {
  // load client entry point
} else {
  // load server entry point
}

这并不是一个很好的解决方案,因为Webpack将包含整个process模块的poly-fill。 - Edwin Pratt

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接