在 Webpack 中使用 Bootstrap 的首选方式

Preferred way of using Bootstrap in Webpack

大家好,

我一直在为 Webpack 使用 Bootstrap,但我快要崩溃了。我确实浏览了大量的博客文章,他们要么使用 7 个月过时的 'bootstrap-webpack' 插件(令人惊讶的是它不能开箱即用)或者..他们通过导入 Bootstrap 文件包含 'node_modules/*/bootstrap/css/bootstrap.css'.

当然,必须有一种更清洁、更有效的方法来解决这个问题?

这是我当前的 webpack.config.js 文件:

var webpack = require('webpack');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var autoprefixer = require('autoprefixer');
var path = require('path');

module.exports = {
    entry: {
        app: path.resolve(__dirname, 'src/js/main.js')
    },
    module: {
        loaders: [{
            test: /\.js[x]?$/,
            loaders: ['babel-loader?presets[]=es2015&presets[]=react'],
            exclude: /(node_modules|bower_components)/
        }, {
            test: /\.css$/,
            loaders: ['style', 'css']
        }, {
            test: /\.scss$/,
            loaders: ['style', 'css', 'postcss', 'sass']
        }, {
            test: /\.sass$/,
            loader: 'style!css!sass?sourceMap'
        },{
            test: /\.less$/,
            loaders: ['style', 'css', 'less']
        }, {
            test: /\.woff$/,
            loader: "url-loader?limit=10000&mimetype=application/font-woff&name=[path][name].[ext]"
        }, {
            test: /\.woff2$/,
            loader: "url-loader?limit=10000&mimetype=application/font-woff2&name=[path][name].[ext]"
        }, {
            test: /\.(eot|ttf|svg|gif|png)$/,
            loader: "file-loader"
        }]
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '/js/bundle.js',
        sourceMapFilename: '/js/bundle.map',
        publicPath: '/'
    },
    plugins: [
        new ExtractTextPlugin('style.css')
    ],
    postcss: [
        autoprefixer({
            browsers: ['last 2 versions']
        })
    ],
    resolve: {
        extensions: ['', '.js', '.sass'],
        modulesDirectories: ['src', 'node_modules']
    },
    devServer: {
        inline: true,
        contentBase: './dist'
    }
};

我可以去 require('bootstrap')(通过某种方式让 jQuery 发挥作用),但是..我很好奇你们都在想什么和做了什么。

提前致谢:)

我不确定这是否是最好的方法,但是使用 vue.js webapp 对我来说效果很好。你可以看到工作代码 here.

我已将 bootstrap 所需的文件包含在 index.html 中,如下所示:

<head>
  <meta charset="utf-8">
  <title>Hey</title>
  <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">
  <link rel="stylesheet" href="/static/bootstrap.css" type="text/css">

  <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/tether/1.3.7/js/tether.min.js" integrity="sha384-XTs3FgkjiBgo8qjEjBk0tGmf3wPrWtA6coPfQDfFEY8AnYJwjalXCiosYRBIBZX8" crossorigin="anonymous"></script>
  <script  href="/static/bootstrap.js"></script>
</head>

这有效,您可以执行回购。为什么我走这条路是我必须 customise some config in bootstrap 所以我不得不更改变量文件并构建 bootstrap 的代码输出我 bootstrap.jsbootstrap.css 文件,我我在这里使用。


建议 使用 npm 包和 webpack 自定义的替代方法。

首先在你的项目中安装bootstrap:

npm install bootstrap@4.0.0-alpha.5

并确保您可以在组件中使用 sass-loader:

npm install sass-loader node-sass --save-dev

现在转到您的 webpack 配置文件并使用以下内容添加一个 sassLoader 对象:

sassLoader: {
    includePaths: [
        path.resolve(projectRoot, 'node_modules/bootstrap/scss/'),
    ],
},

projectRoot 应该指向您可以导航到 node_packages 的位置,在我的例子中是:path.resolve(__dirname, '../')

现在你可以直接在你的 .vue 文件中使用 bootstrap,当你添加以下内容时,webpack 会为你编译它:

<style lang="scss">
  @import "bootstrap";
</style>

我强烈推荐使用 bootstrap-loader。您添加一个配置文件(.bootstraprc 在您的根文件夹中),您可以在其中排除您不想要的 bootstrap 元素并告诉您的 variables.scssbootstrap.overrides.scss 在哪里。定义您的 SCSS 变量,进行覆盖,添加您的 webpack 条目并继续您的生活。

我使用 webpack 直接从 .less 和 .scss 文件构建 bootstrap。这允许通过覆盖 .less/.scss 中的源来自定义 bootstrap 并且仍然获得 webpack 的所有好处。

您的代码缺少任何 .css/.less/.scss 文件的入口点。您需要为已编译的 css 文件包含一个入口点。对于这个入口点,我用 const 声明了一个数组,然后在数组中包含指向我希望 webpack 编译的源文件的路径。

目前,我将 bootstrap 3 与基本自定义模板和第二个自定义主题一起使用。基本模板使用 bootstrap .less 文件样式,并且它具有在 .less 文件中编写的特定源覆盖。

第二个自定义主题使用 .sass 文件样式,并且与 bootstrap 在 .scss 文件中编写的基础具有类似的覆盖。因此,我需要尝试优化所有这些样式以用于生产(目前大约有 400kb,但这有点重,因为我们选择避免​​ CDN,因为目标是在中国使用)。

下面是一个参考 webpack.config.js,它可以从 .less/.scss/.css 文件构建,还可以做一些其他的事情,比如构建 typescript 模块和使用 Babel 将 es6/typescript 转换为兼容浏览器的 javascript。输出最终出现在我的 /static/dist 文件夹中。

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

// plugins
const ForkTsCheckerNotifierWebpackPlugin = require('fork-ts-checker-notifier-webpack-plugin');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const ManifestPlugin = require('webpack-manifest-plugin');
const MomentLocalesPlugin = require('moment-locales-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const TerserJSPlugin = require('terser-webpack-plugin');
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');


// take debug mode from the environment
const debug = (process.env.NODE_ENV !== 'production');
// Development asset host (webpack dev server)
const publicHost = debug ? 'http://localhost:9001' : '';
const rootAssetPath = path.join(__dirname, 'src');


const manifestOptions = {
    publicPath: `${publicHost}/static/dist/`,
  };

const babelLoader = {
  loader: 'babel-loader',
  options: {
    cacheDirectory: true,
    presets: [
        '@babel/preset-env'
    ]
  }
};

const app_css = [
    // specific order generally matters
    path.join(__dirname, 'src', 'bootstrap-template1', 'assets', 'css', 'fonts', 'Roboto', 'css', 'fonts.css'),
    path.join(__dirname, 'node_modules', 'font-awesome', 'css', 'font-awesome.css'),
    // This is bootstrap 3.3.7 base styling writtin in .less
    path.join(__dirname, 'src', 'bootstrap-template1', 'assets',  'less', '_main_full', 'bootstrap.less'),
    // bootstrap theme in .scss -> src\bp\folder\theme\src\scss\styles.scss
    path.join(__dirname, 'src', 'bp', 'folder', 'theme', 'src', 'scss', 'styles.scss'),
    // back to .less -> 'src/bootstrap-template1/assets/less/_main_full/core.less',
    path.join(__dirname, 'src', 'bootstrap-template1', 'assets', 'less', '_main_full', 'core.less'),
    // 'src/bootstrap-template1/assets/less/_main_full/components.less',
    path.join(__dirname, 'src', 'bootstrap-template1', 'assets', 'less', '_main_full', 'components.less'),
    //'src/bootstrap-template1/assets/less/_main_full/colors.less',
    path.join(__dirname, 'src', 'bootstrap-template1', 'assets', 'less', '_main_full', 'colors.less'),
    // <!-- syntax highlighting in .css --> src/bp/folder/static/css/pygments.css
    path.join(__dirname, 'src', 'bp', 'folder', 'static', 'css', 'pygments.css'),
    // <!-- lato/ptsans font we want to serve locally --> src/fonts/googlefonts.css'
    path.join(__dirname, 'src', 'fonts', 'googlefonts.css'),
    // a .css style -> 'src/bp/appbase/styles/md_table_generator.css'
    path.join(__dirname, 'src', 'bp', 'appbase', 'styles', 'md_table_generator.css'),
    // another .css style -> hopscotch 'src/libs/hopscotch/dist/css/hopscotch.min.css'
    path.join(__dirname, 'src', 'libs', 'hopscotch', 'dist', 'css', 'hopscotch.min.css'),
    //LAST final custom snippet styles to ensure they take priority 'src/css/style.css',
    path.join(__dirname, 'src', 'css', 'style.css')
];

const vendor_js = [
    //'core-js',
    'whatwg-fetch',
];

const app_js = [
    // a typescript file! :)
    path.join(__dirname, 'src', 'typescript', 'libs', 'md-table', 'src', 'extension.ts'),
    // base bootstrap 3.3.7 javascript
    path.join(__dirname, 'node_modules', 'bootstrap', 'dist', 'js', 'bootstrap.js'),
    path.join(__dirname, 'src', 'main', 'app.js'),
    // src/bootstrap-template1/assets/js/plugins/forms/styling/uniform.min.js'
    path.join(__dirname, 'node_modules', '@imanov', 'jquery.uniform', 'src', 'js', 'jquery.uniform.js'),
    // src/bootstrap-template1/assets/js/plugins/ui/moment/moment.min.js'
];

function recursiveIssuer(m) {
  if (m.issuer) {
    return recursiveIssuer(m.issuer);
  } else if (m.name) {
    return m.name;
  } else {
    return false;
  }
}

module.exports = {
    context: process.cwd(), // to automatically find tsconfig.json
    // context: __dirname,
    entry: {
        app_css,
        vendor_js,
        app_js,
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        publicPath: `${publicHost}/static/dist/`,
        chunkFilename: '[id].[hash:7].js',
        filename: '[name].[hash:7].js'
    },
    resolve: {
        extensions: [".webpack.js", ".web.js",".tsx", ".ts", ".js", ".css"],
        alias: {
            jquery$: path.resolve(__dirname, 'node_modules', 'jquery', 'dist', 'jquery.js'),
        }
    },
    target: "web",
    devtool: 'source-map',
    devServer: {
        // this devserver proxies all requests to my python development server except 
        // webpack compiled files in the `/static/dist` folder 
        clientLogLevel: 'warning',
        contentBase: path.join(__dirname, './src'),
        publicPath: 'dist',
        open: true,
        historyApiFallback: true,
        stats: 'errors-only',
        headers: {'Access-Control-Allow-Origin': '*'},
        watchContentBase: true,
        port: 9001,
        proxy: {
            '!(/dist/**/**.*)': {
                target: 'http://127.0.0.1:8000',
            },
        },
    },
    mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',
    optimization: {
        minimizer: [new TerserJSPlugin({}), new OptimizeCssAssetsPlugin({})],
        splitChunks: {
          cacheGroups: {
            appStyles: {
              name: 'app',
              // https://webpack.js.org/plugins/mini-css-extract-plugin/#extracting-css-based-on-entry
              test: (m, c, entry = 'app') =>
                m.constructor.name === 'CssModule' && recursiveIssuer(m) === entry,
              chunks: 'all',
              enforce: true,
            },
          },
        },
    },
    plugins: [
        new webpack.ProvidePlugin({
            $: 'jquery',
            jQuery: 'jquery',
            'window.jQuery': 'jquery',
            'window.$': 'jquery'
        }),
        // Strip all locales from moment.js except "zh-cn"
        // ("en" is built into Moment and can’t be removed)
        new MomentLocalesPlugin({
            localesToKeep: ['zh-cn'],
        }),
        new ForkTsCheckerWebpackPlugin({
            tslint: true, useTypescriptIncrementalApi: true
        }),
        new ForkTsCheckerNotifierWebpackPlugin({ title: 'TypeScript', excludeWarnings: false }),
        new MiniCssExtractPlugin({
            filename: '[name].[hash:7].css',
            chunkFilename: '[id].[hash:7].css',
            moduleFilename: ({ name }) => `${name.replace('/js/', '/css/')}.[hash:7].css`
        }),
         new OptimizeCssAssetsPlugin({
            assetNameRegExp: /\.optimize\.css$/g,
            cssProcessor: require('cssnano'),
            cssProcessorPluginOptions: {
            preset: ['default', { discardComments: { removeAll: true } }],
            },
            canPrint: true
        }),
        new ManifestPlugin({
            ...manifestOptions
        }),
    ].concat(debug ? [] : [
        // production webpack plugins go here
        new webpack.DefinePlugin({
            'process.env': {
                NODE_ENV: JSON.stringify('production'),
            }
        }),
        new ForkTsCheckerWebpackPlugin({
            async: false,
            useTypescriptIncrementalApi: true,
            memoryLimit: 2048
        }),
    ]),
    module: {
        rules: [
                {
                // jinja/nunjucks templates
                test: /\.jinja2$/,
                loader: 'jinja-loader',
                query: {
                    root:'../templates'
                }
            },
            {
                test: /\.ts(x?)$/,
                exclude: /node_modules/,
                use: [
                    babelLoader,
                    {
                        loader: 'ts-loader',
                        options:
                            {   // disable type checker - we will use it in
                                // fork-ts-checker-webpack-plugin
                                transpileOnly: true
                            }
                    }
                ]
            },
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: [
                    babelLoader
                ]
            },
            {
                test: /\.(html|jinja2)$/,
                loader: 'raw-loader'
            },
            {
                test: /\.(sc|sa|c)ss$/,
                use: [
                    {
                        loader: MiniCssExtractPlugin.loader,
                        options: {
                          hmr: debug,
                            // only use if hmr is not working correctly
                          // reloadAll: true,
                        },
                    },
                    {
                        loader: "css-loader",
                    },
                    {
                        loader: "sass-loader"
                    },
                ]
            },
            {
                test: /\.less$/,
                use: [
                    {
                        loader: MiniCssExtractPlugin.loader,
                        options: {
                          hmr: debug,
                            // only use if hmr is not working correctly
                          // reloadAll: true,
                        },
                    },
                    {
                        loader: 'css-loader', // translates CSS into CommonJS
                    },
                    {
                        loader: 'less-loader', // compiles Less to CSS
                    },
                ],
            },
            {
                test: /\.(ttf|eot|svg|gif|ico)$/,
                use: [
                    {
                        loader: 'file-loader',
                        options: {
                            name: '[path][name].[hash:7].[ext]',
                            context: rootAssetPath
                        },
                    },
                ],
            },
            {
                test: /\.(jpe?g|png)$/i,
                loader: 'responsive-loader',
                options: {
                    name: '[path][name].[hash:7].[ext]',
                    adapter: require('responsive-loader/sharp'),
                    context: rootAssetPath
                }
            },
            {
                test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
                 loader:'url-loader',
                 options:{
                    limit: 10000,
                    mimetype: 'application/font-woff',
                    // name: ('fonts/[path][name].[hash:7].[ext]'),
                    name: ('fonts/[name].[hash:7].[ext]'),
                 }
            },
            {
                test: require.resolve("jquery"),
                use:[
                    { loader: "expose-loader", options:"$" },
                    { loader: "expose-loader", options:"jQuery" },
                    { loader: "expose-loader", options:"jquery" }
                ]
            }
        ]
    },
};