2

Webpack Project Configuration

Github Repository

本部分假设你已经对Webpack有了大概的了解,这里我们会针对笔者自己在生产环境下使用的Webpack编译脚本进行的一个总结,在介绍具体的配置方案之前笔者想先概述下该配置文件的设计的目标,或者说是笔者认为一个前端编译环境应该达成的特性,这样以后即使Webpack被淘汰了也可以利用其他的譬如JSPM之类的来完成类似的工作。

  • 单一的配置文件:很多项目里面是把开发环境与生产环境写了两个配置文件,可能笔者比较懒吧,不喜欢这么做,因此笔者的第一个特性就是单一的配置文件,然后通过npm封装不同的编译命令传入环境变量,然后在配置文件中根据不同的环境变量进行动态响应。另外,要保证一个Boilerplate能够在最小修改的情况下应用到其他项目。

  • 多应用入口支持:无论是单页应用还是多页应用,在Webpack中往往会把一个html文件作为一个入口。笔者在进行项目开发时,往往会需要面对多个入口,即多个HTML文件,然后这个HTML文件加载不同的JS或者CSS文件。譬如登录页面与主界面,往往可以视作两个不同的入口。Webpack原生提倡的配置方案是面向过程的,而笔者在这里是面向应用方式的封装配置。

  • 调试时热加载:这个特性毋庸多言,不过热加载因为走得是中间服务器,同时只能支持监听一个项目,因此需要在多应用配置的情况下加上一个参数,即指定当前调试的应用。

  • 自动化的Polyfill:这个是Webpack自带的一个特性吧,不过笔者就加以整合,主要是实现了对于ES6、React、CSS(Flexbox)等等的自动Polyfill。

  • 资源文件的自动管理:这部分主要指从模板自动生成目标HTML文件、自动处理图片/字体等资源文件以及自动提取出CSS文件等。

  • 文件分割与异步加载:可以将多个应用中的公共文件,譬如都引用了React类库的话,可以将这部分文件提取出来,这样前端可以减少一定的数据传输。另外的话还需要支持组件的异步加载,譬如用了React Router,那需要支持组件在需要时再加载。

在发布版本中,可能需要一些特殊的配置或者插件,譬如只有在NODE_ENV环境变量等于production的情况下才会有逻辑配置需要添加在配置文件中,那么在Webpack的配置文件中可以使用如下定义:

var webpack    = require('webpack');
var production = process.env.NODE_ENV === 'production';

var plugins = [
    new webpack.optimize.CommonsChunkPlugin({
        name:      'main', // Move dependencies to our main file
        children:  true, // Look for common dependencies in all children,
        minChunks: 2, // How many times a dependency must come up before being extracted
    }),
];

if (production) {
    plugins = plugins.concat([
       // Production plugins go here
    ]);
}

module.exports = {
    entry:   './src',
    output:  {
        path:       'builds',
        filename:   'bundle.js',
        publicPath: 'builds/',
    },
    plugins: plugins,
    // ...
};

在发布版本中,Webpack的一些配置可以被关闭,譬如:

module.exports = {
    debug:   !production,
    devtool: production ? false : 'eval',

Configuration

package.json

{
  "name": "webpack-boilerplate",
  "version": "1.0.0",
  "description": "Page-Driven Webpack Boilerplate For React-Redux Work Flow",
  "scripts": {
    "start": "node devServer.js",
    "storybook": "start-storybook -p 9001",
    "build:webpack": "NODE_ENV=production webpack -p --config webpack.config.js",
    "build": "npm run clean && npm run build:webpack",
    "build:style-check": "NODE_ENV=production CHECK=true webpack -p --config webpack.config.js",
    "deploy": "npm run build && ./node_modules/.bin/http-server dist",
    "clean": "rimraf dist",
    "lint": "eslint src"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/wxyyxc1992/Webpack-React-Redux-Boilerplate"
  },
  "keywords": [
    "boilerplate",
    "live",
    "hot",
    "reload",
    "react",
    "reactjs",
    "hmr",
    "edit",
    "webpack",
    "babel",
    "react-transform",
    "PostCSS(FlexBox Polyfill)"
  ],
  "author": "Chevalier (http://github.com/wxyyxc1992)",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/wxyyxc1992/Webpack-React-Redux-Boilerplate/issues"
  },
  "homepage": "https://github.com/wxyyxc1992/Webpack-React-Redux-Boilerplate",
  "devDependencies": {
    "@kadira/storybook": "^1.17.1",
    ...
  },
  "dependencies": {
    "boron": "^0.1.2",
    ...
  }
}

webpack.config.js

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

//PostCSS plugins
var autoprefixer = require('autoprefixer');

//webpack plugins
var ProvidePlugin = require('webpack/lib/ProvidePlugin');
var DefinePlugin = require('webpack/lib/DefinePlugin');
var CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var CopyWebpackPlugin = require('copy-webpack-plugin');
var WebpackMd5Hash = require('webpack-md5-hash');
var ExtractTextPlugin = require("extract-text-webpack-plugin");

var NODE_ENV = process.env.NODE_ENV || "develop";//获取命令行变量

//@region 可配置区域

//定义统一的Application,不同的单页面会作为不同的Application
/**
 * @function 开发状态下默认会把JS文本编译为main.bundle.js,然后使用根目录下dev.html作为调试文件.
 * @type {*[]}
 */
var apps = [
    {
        //required
        id: "index",//编号
        title: "Index",//HTML文件标题
        entry: {
            name: "index",//该应用的入口名
            src: "./src/index.js",//该应用对应的入口文件
        },//入口文件
        indexPage: "./src/index.html",//主页文件

        //optional
        dev: false,//判断是否当前正在调试,默认为false
        compiled: true//判斷當前是否加入编译,默认为true
    },
    {
        id: "helloworld",
        title: "HelloWorld",
        entry: {
            name: "helloworld",
            src: "./src/modules/helloworld/container/app.js"
        },
        indexPage: "./src/modules/helloworld/container/helloworld.html",
        dev: false,
        compiled: true
    },
    {
        id: "todolist",
        title: "TodoList",
        compiled: false
    },
    {
        //required
        id: "counter",//编号
        title: "Counter",//HTML文件标题
        entry: {
            name: "counter",//该应用的入口名
            src: "./src/modules/counter/container/app.js",//该应用对应的入口文件
        },//入口文件
        indexPage: "./src/modules/counter/container/counter.html",//主页文件

        //optional
        dev: false,//判断是否当前正在调试,默认为false
        compiled: true//判斷當前是否加入编译,默认为true
    },
    {
        //required
        id: "form",//编号
        title: "Form",//HTML文件标题
        entry: {
            name: "form",//该应用的入口名
            src: "./src/modules/form/form.js"//该应用对应的入口文件
        },//入口文件
        indexPage: "./src/modules/form/form.html",//主页文件

        //optional
        dev: true,//判断是否当前正在调试,默认为false
        compiled: true//判斷當前是否加入编译,默认为true
    }
];

//定义非直接引用依赖
//定义第三方直接用Script引入而不需要打包的类库
//使用方式即为var $ = require("jquery")
const externals = {
    jquery: "jQuery",
    pageResponse: 'pageResponse'
};


/*********************************************************/
/*********************************************************/
/*下面属于静态配置部分,修改请谨慎*/
/*********************************************************/
/*********************************************************/

//开发时的入口考虑到热加载,只用数组形式,即每次只会加载一个文件
var devEntry = [
    'eventsource-polyfill',
    'webpack-hot-middleware/client',
];

//生产环境下考虑到方便编译成不同的文件名,所以使用数组
var proEntry = {
    "vendors": "./src/vendors.js",//存放所有的公共文件
};

//定义HTML文件入口,默认的调试文件为src/index.html
var htmlPages = [];

//遍历定义好的app进行构造
apps.forEach(function (app) {

    //判断是否加入编译
    if (app.compiled === false) {
        //如果还未开发好,就设置为false
        return;
    }

    //添加入入口
    proEntry[app.entry.name] = app.entry.src;

    //构造HTML页面
    htmlPages.push({
        filename: app.id + ".html",
        title: app.title,
        // favicon: path.join(__dirname, 'assets/images/favicon.ico'),
        template: 'underscore-template-loader!' + app.indexPage, //默认使用underscore
        inject: false, // 使用自动插入JS脚本,
        chunks: ["vendors", app.entry.name] //选定需要插入的chunk名
    });

    //判断是否为当前正在调试的
    if (app.dev === true) {
        //如果是当前正在调试的,则加入到devEntry
        devEntry.push(app.entry.src);
    }
});

//@endregion 可配置区域

//基本配置
var config = {
    devtool: 'source-map',
    //所有的出口文件,注意,所有的包括图片等本机被放置到了dist目录下,其他文件放置到static目录下
    output: {
        path: path.join(__dirname, 'dist'),//生成目录
        filename: '[name].bundle.js',//文件名
        sourceMapFilename: '[name].bundle.map'//映射名
        // chunkFilename: '[id].[chunkhash].chunk.js',//块文件索引
    },
    //配置插件
    plugins: [
        // new WebpackMd5Hash(),//计算Hash插件
        new webpack.optimize.OccurenceOrderPlugin(),
        new webpack.DefinePlugin({
            'process.env': {
                //因为使用热加载,所以在开发状态下可能传入的环境变量为空
                'NODE_ENV': process.env.NODE_ENV === undefined ? JSON.stringify('develop') : JSON.stringify(NODE_ENV)
            },
            //判断当前是否处于开发状态
            __DEV__: process.env.NODE_ENV === undefined || process.env.NODE_ENV === "develop" ? JSON.stringify(true) : JSON.stringify(false)
        }),

        //提供者fetch Polyfill插件
        new webpack.ProvidePlugin({
            // 'fetch': 'imports?this=>global!exports?global.fetch!whatwg-fetch'
        }),

        //提取出所有的CSS代码
        new ExtractTextPlugin('[name].css'),

        //自动分割Vendor代码
        new CommonsChunkPlugin({name: 'vendors', filename: 'vendors.bundle.js', minChunks: Infinity}),

        //自动分割Chunk代码
        new CommonsChunkPlugin({
            children: true,
            async: true,
        })
    ],
    module: {
        loaders: [
            {
                test: /\.(js|jsx)$/,
                exclude: /(libs|node_modules)/,
                loader:"babel",
                query: {
                    presets: ["es2015", "react", "stage-2"],
                    plugins: [
                        ["typecheck"],
                        ["transform-flow-strip-types"],
                        ["syntax-flow"],
                        ["transform-class-properties"],
                        ["transform-object-rest-spread"]
                    ]
                }
            },
            {
                test: /\.(eot|woff|woff2|ttf|svg|png|jpe?g|gif)(\?\S*)?$/,
                loader: 'url-loader?limit=8192&name=assets/imgs/[hash].[ext]'
            },// inline base64 URLs for <=8k images, direct URLs for the rest
            {
                test: /\.vue$/,
                loader: 'vue'
            }
        ]
    },
    postcss: [
        autoprefixer({browsers: ['last 10 versions', "> 1%"]})
    ],//使用postcss作为默认的CSS编译器
    resolve: {
        alias: {
            libs: path.resolve(__dirname, 'libs'),
            nm: path.resolve(__dirname, "node_modules"),
            assets: path.resolve(__dirname, "assets"),
        }
    }
};

//进行脚本组装
config.externals = externals;

//自动创建HTML代码
htmlPages.forEach(function (p) {
    config.plugins.push(new HtmlWebpackPlugin(p));
});

//为开发状态下添加插件
if (process.env.NODE_ENV === undefined || process.env.NODE_ENV === "develop") {

    //配置SourceMap
    config.devtool = 'cheap-module-eval-source-map';

    //设置入口为调试入口
    config.entry = devEntry;

    //設置公共目錄名
    config.output.publicPath = '/dist/'//公共目录名

    //调试状态下的CSS
    config.module.loaders.push({
        test: /\.(scss|sass|css)$/,
        loader: 'style-loader!css-loader!postcss-loader!sass'
    });


    //添加插件
    config.plugins.push(new webpack.HotModuleReplacementPlugin());
    config.plugins.push(new webpack.NoErrorsPlugin());

} else {

    //如果是生产环境下
    config.entry = proEntry;

    //如果是生成环境下,将文件名加上hash
    config.output.filename = '[name].bundle.js.[hash:8]';

    //設置公共目錄名
    config.output.publicPath = '/'//公共目录名

    //发布状态下添加Loader
    config.module.loaders.push({
        test: /\.(scss|sass|css)$/,
        loader: ExtractTextPlugin.extract('style-loader', 'css-loader!postcss-loader!sass')
    });

    //添加代码压缩插件
    config.plugins.push(
        new webpack.optimize.UglifyJsPlugin({
            compressor: {
                warnings: false
            }
        }));

    //添加MD5计算插件

    //判断是否需要进行检查
    if (process.env.CHECK === "true") {
        config.module.loaders[0].loaders.push("eslint-loader");
    }
}

module.exports = config;

devServer.js

var path = require('path');
var express = require('express');
var webpack = require('webpack');
//默认是开发时配置
var config = require('./webpack.config');

var app = express();
var compiler = webpack(config);

app.use(require('webpack-dev-middleware')(compiler, {
  noInfo: true,
  publicPath: config.output.publicPath
}));

app.use(require('webpack-hot-middleware')(compiler));

app.get('*', function(req, res) {
  res.sendFile(path.join(__dirname + "/src/", "dev.html"));
});

//监听本地端口
app.listen(3000, 'localhost', function(err) {
  if (err) {
    console.log(err);
    return;
  }

  console.log('Listening at http://localhost:3000');
});

Deployment

开始这个小节之前,可以先看下大神的一篇文章:大公司里怎样开发和部署前端代码

对于静态文件,第一次获取之后,文件内容没改变的话,浏览器直接读取缓存文件即可。那如果缓存设置过长,文件要更新怎么办呢?嗯,以文件内容的 MD5 作为文件名就是一个不错的解决方案。来看下用 webpack 应该怎样实现


output: {

    path: xxx,

    publicPath: yyy,

    filename: '[name]-[chunkhash:6].js'

}

打包后的文件名加入了 hash 值


const bundler = webpack(config)



bundler.run((err, stats) => {

  let assets = stats.toJson().assets

  let name



  for (let i = 0; i < assets.length; i++) {

    if (assets[i].name.startsWith('main')) {

      name = assets[i].name

      break

    }

  }



  fs.stat(config.buildTemplatePath, (err, stats) => {

    if (err) {

      fs.mkdirSync(config.buildTemplatePath)

    }



    writeTemplate(name)

  })

})

手动调用 webpack 的 API,获取打包后的文件名,通过 writeTemplate 更新 html 代码。完整代码猛戳 gitst。这样子,我们就可以把文件的缓存设置得很长,而不用担心更新问题。


王下邀月熊_Chevalier
22.5k 声望8.5k 粉丝

爱代码 爱生活 希望成为全栈整合师