個人Webpack套裝

Webpack Project Configuration

Github Repositoryjavascript

本部分假設你已經對Webpack有了大概的瞭解,這裏咱們會針對筆者本身在生產環境下使用的Webpack編譯腳本進行的一個總結,在介紹具體的配置方案以前筆者想先概述下該配置文件的設計的目標,或者說是筆者認爲一個前端編譯環境應該達成的特性,這樣之後即便Webpack被淘汰了也能夠利用其餘的譬如JSPM之類的來完成相似的工做。css

  • 單一的配置文件:不少項目裏面是把開發環境與生產環境寫了兩個配置文件,可能筆者比較懶吧,不喜歡這麼作,所以筆者的第一個特性就是單一的配置文件,而後經過npm封裝不一樣的編譯命令傳入環境變量,而後在配置文件中根據不一樣的環境變量進行動態響應。另外,要保證一個Boilerplate可以在最小修改的狀況下應用到其餘項目。html

  • 多應用入口支持:不管是單頁應用仍是多頁應用,在Webpack中每每會把一個html文件做爲一個入口。筆者在進行項目開發時,每每會須要面對多個入口,即多個HTML文件,而後這個HTML文件加載不一樣的JS或者CSS文件。譬如登陸頁面與主界面,每每能夠視做兩個不一樣的入口。Webpack原生提倡的配置方案是面向過程的,而筆者在這裏是面向應用方式的封裝配置。前端

  • 調試時熱加載:這個特性毋庸多言,不過熱加載由於走得是中間服務器,同時只能支持監聽一個項目,所以須要在多應用配置的狀況下加上一個參數,即指定當前調試的應用。vue

  • 自動化的Polyfill:這個是Webpack自帶的一個特性吧,不過筆者就加以整合,主要是實現了對於ES六、React、CSS(Flexbox)等等的自動Polyfill。java

  • 資源文件的自動管理:這部分主要指從模板自動生成目標HTML文件、自動處理圖片/字體等資源文件以及自動提取出CSS文件等。node

  • 文件分割與異步加載:能夠將多個應用中的公共文件,譬如都引用了React類庫的話,能夠將這部分文件提取出來,這樣前端能夠減小必定的數據傳輸。另外的話還須要支持組件的異步加載,譬如用了React Router,那須要支持組件在須要時再加載。react

在發佈版本中,可能須要一些特殊的配置或者插件,譬如只有在NODE_ENV環境變量等於production的狀況下才會有邏輯配置須要添加在配置文件中,那麼在Webpack的配置文件中可使用以下定義:jquery

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的一些配置能夠被關閉,譬如: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。這樣子,咱們就能夠把文件的緩存設置得很長,而不用擔憂更新問題。

相關文章
相關標籤/搜索