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',
{ "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", ... } }
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;
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'); });
开始这个小节以前,能够先看下大神的一篇文章:大公司里怎样开发和部署前端代码。
对于静态文件,第一次获取以后,文件内容没改变的话,浏览器直接读取缓存文件便可。那若是缓存设置过长,文件要更新怎么办呢?嗯,以文件内容的 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。这样子,咱们就能够把文件的缓存设置得很长,而不用担忧更新问题。