webpack开发与生产环境配置

前言

做者去年就开始使用webpack, 最先的接触就来自于vue-cli。那个时候工做重点主要也是 vue 的使用,对webpack的配置是知之甚少,期间有问题也是询问大牛 @吕大豹。顺便说一句,对于前端知识体系迷茫的童鞋能够关注豹哥的微信公众号,《大豹杂说》。豹哥对于刚开始小白的本身(虽然如今也白)知无不谈,并且回复超快超认真。这里真的很感谢豹哥。前段时间工做不忙,本身就啃了啃webpack的官方文档,毕竟知识仍是在本身脑壳里踏实。而后根据vue-cli的配置文件丰富了一点新的东西,发布出来你们共享,同时本身也有点疑问,也欢迎各位评论给小子指正。## webpack的学习在前端领域咱们总要面对各类的新框架新工具,那么怎么有效快速的学习掌握一门技能呢?做者的方法是实践是最好的老师,建议新东西了解一些核心的API啊功能啊马上就上手使用,这个过程确定会出现各类问题,在寻求解决问题的途径中逐渐也就加深了理解,带着问题学习总归会事半功倍。拿webpack来说,了解他的一些核心概念,配置文件的入口输出解析loader,plugin等等就能够简单使用了。这里建议一点,学习新知识的时候建议你们最终仍是从官网啊官方文档中学习,英文真的不是事,得试试才知道本身能看懂的。看博客主要都是别人消化以后的东西,再有基础之上再看这些文章固然能起到查漏补缺的功效,可是一开始就看,就很容易受到做者思路局限的影响。javascript

固然这些都是本身的建议啊。因此本篇文章面对的是对webpack有一些简单使用的朋友,你们分享经验而已,若是对webpack还没开始使用的朋友,建议仍是先了解一下webpack的核心知识。官网有中文版,翻译的也很好。css

webpack环境的区别

webpack本质就是一个打包工具,是一种模块化开发的实现,它与gulp与grunt这一类的自动化构建工具不一样,构建工具是优化咱们本身的工做流程,将众多的手工方式改成自动化,好比压缩js、css,编译scss,less。固然webpack的loader与plugin也能够完成这些工做,工具使用看我的公司需求。webpack的主要工做是将咱们我编写的模块化的文件打包编译为浏览器所能辨识的方式。
直白来说,开发环境,就是你的代码在本地服务器上在测试、更改、运行,生产环境你的代码就是已经开始在真实服务器中使用。webpack 能够适用于开发环境主要是运用了node.js 搭建一个本地服务。记得去年我刚开始想须要一个本地服务的时候开始是使用Hbuilder,后来单独用了一个小工具名字好像叫webservice。html

package.json

前面提到了nodejs,node.js是一个javascript运行的平台而不是什么js的框架,它实现的是js不只能够开发客户端浏览器也能够开发服务端。如今的前端项目中都会发现一个package.json前端

{
  "name": "webpack_environment",
  "version": "1.0.0",
  "description": "A webpack environment test",
  "author": "abzerolee",
  "scripts": {
    "dev": "node build/dev-server.js",
    "build": "node build/build.js"
  },
  "dependencies": {
    "nimble": "^0.0.2"
  },
  "devDependencies": {
    "autoprefixer": "^7.1.2",
    "babel-core": "^6.22.1",
    "babel-loader": "^7.1.1",
    "babel-preset-stage-2": "^6.22.0",
    "chalk": "^2.0.1",
    "clean-webpack-plugin": "^0.1.16",
    "connect-history-api-fallback": "^1.3.0",
    "css-loader": "^0.28.0",
    "eventsource-polyfill": "^0.9.6",
    "express": "^4.14.1",
    "extract-text-webpack-plugin": "^3.0.0",
    "file-loader": "^0.11.1",
    "glob": "^7.1.2",
    "html-webpack-plugin": "^2.28.0",
    "http-proxy-middleware": "^0.17.3",
    "less": "^2.7.2",
    "less-loader": "^4.0.5",
    "mockjs": "^1.0.1-beta3",
    "opn": "^5.1.0",
    "ora": "^1.3.0",
    "postcss-loader": "^2.0.6",
    "rimraf": "^2.6.1",
    "style-loader": "^0.18.2",
    "url-loader": "^0.5.8",
    "webpack": "^3.1.0",
    "webpack-dev-middleware": "^1.10.0",
    "webpack-hot-middleware": "^2.18.0",
    "webpack-merge": "^4.1.0"
  }
}

 

这个文件能够用npm 模块管理器生成的,它描述了一个项目的各类信息,注意到script这个属性他对应的dev build就是开发环境与生产环境了,咱们运行命令的话是用 ‘npm run dev’或‘npm run build’其实执行的就是对应的node编译。能够发现这个配置文件告诉咱们开发环境与生产环境的入口文件/build/dev-server.js,/build/build.js。剩下的dependencies / devDependencies则表明两种环境对应的依赖须要。vue

目录结构


  先介绍/node_modules 咱们使用npm install 就是经过package.json中的依赖配置对应安装你须要的一些库,能够发先我在生产环境须要的是nimble。那么这些库存放的地方就是在/node_moudles中。固然你也能够用曾经古老的方法新建一个/lib 而后去官网下载对应js文件,再放入/lib。可是这样对于整个项目的管理并不十分友好,咱们查看项目的依赖库只须要查看package.json就够了 而不是去html页面一个个找<script>标签。
  接下来介绍一系列的.[文件名]这样的配置文件。.[文件名]都是一些你安装的依赖工具的配置文件,好比Babel的.babelrc postcss的.postcssrc ,最后就是一些[文件名].md的文件,md扩展名指的markdown 标记语言编写的文档。好比README.md 介绍的通常是项目的内容简介一些API的使用方法等等。
/build 是项目启动时的一些文件,如 webpack 的配置文件 开发环境服务配置文件 一些简单工具函数/utils.js等等。这里本身也有个问题就是关于dev-client.js的配置,dev-client是模块热加载的一个模块,应该就是当项目在开发环境运行以后命令行中新开的那个窗体的配置。不知道理解的对不对。固然我如今没用这个,项目跑起来也是能够的。java

 

/config 是关于整个项目的环境配置包括开发与生产。咱们在node引入模块的时候能够直接引入目录,node

require('./config');

他默认查找的就是该目录下的index.js文件。固然也能够不叫index.js这个须要一个/config目录下再去写一个package.json指定文件。
/dist与/src /dist目录下是将/src 目录下的源码编译以后生成的文件。通常项目部署就直接能够将/dist目录下的文件放在网站的根目录。/dist就对应生产环境的文件,/src对应开发环境的文件。
/mock 是前台开发的模拟数据接口的文件,里面就是一些后台接口的模拟数据react

var Mock = require('mockjs');

var User = {
  login: {
    code: 0,
    info: null,
    msg: '登陆成功!'
  },
  getVerifyCode: {
    code: 0,
    info: Mock.mock('@string("lower", 4)'),
    msg: '操做成功'
  }
};

module.exports = User;

这里使用了mock.js 来生成模拟数据,用CommonJS规范中module.exports来暴露出数据。对于AMD,CMD,CommonJS这几种模块规范,你们仍是应该有适当的理解,为何要有模块,模块的工做方式有什么。固然这是一种规避跨域问题的模拟测试,项目中也经过http-proxy-middleware的方式解决跨域问题。可是若是后台的进度慢于前台的状况下,这种mock也是一种良好的开发方式。jquery

开发环境 dev-server.js

做者最开始学习webpack的时候,也是从把a.js与b.js引入main.js最后打包生成bundle.js开始的。那个时候对node.js也是只知其一;不知其二,固然如今了解的更多了,并不表明精通。总会好奇一个点就是 刚开始编译的时候是使用webpack

webpack -config webpack.conf.js

后面怎么开始用node编译了。其实这是webpack提供了一个Node.js API,能够直接在Node.js运行时使用。这也就是为何入口文件从webpack.conf.js变成了dev-server.js|build.js的缘由。使用node编译的好处是能够更好的利用一下node的特性 读取文件,模拟API接口等等。

var config = require('../config');
if(!process.env.ENV) {
  process.env.ENV = config.dev.ENV;
}
var utils = require('./utils');
var opn = require('opn');
var path = require('path');
var fs = require('fs');
var express = require('express');
var webpack = require('webpack');
var proxyMiddleware = require('http-proxy-middleware');
var webpackConfig = require('./webpack.dev.conf');

var port = process.env.PORT || config.dev.port;

var autoOpenBrowser = config.dev.autoOpenBrowser;

var proxyTable = config.dev.proxyTable;

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

var apiRouter = express.Router();

var apis = fs.readdirSync(utils.resolve('/mock'));
var apiClass = apis.map(it => it.replace(/\.js$/, ''));

apiRouter.route('/:apiClass/:apiName').all(function(req, res) {
  var params = req.params;
  var apiIndex = apiClass.indexOf(params.apiClass)

  var err = {code: 99,info: null, msg: 'no such api'}
  if(apis.length < 1 || apiIndex  === -1)
    return res.json(err);

  var klass = require('../mock/'+ apis[apiIndex]);
  if(klass[params.apiName]){
    res.json(klass[params.apiName]);
  }else{
    res.json(err);
  }
})

app.use('/api', apiRouter);

var devMiddleware = require('webpack-dev-middleware')(compiler, {
  publicPath: webpackConfig.output.publicPath,
  quiet: true
});

var hotMiddleware = require('webpack-hot-middleware')(compiler, {
  log: () => {},
  heartbeat: 2000
})

compiler.plugin('compilation', function (compilation) {
  compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
    hotMiddleware.publish({ action: 'reload' })
    cb()
  })
})

Object.keys(proxyTable).forEach(function (context) {
  var options = proxyTable[context]
  if (typeof options === 'string') {
    options = { target: options }
  }
  app.use(proxyMiddleware(options.filter || context, options))
});

// app.use(require('connect-history-api-fallback')())
app.use(devMiddleware)
app.use(hotMiddleware)

var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)
app.use(staticPath, express.static('./static'));

var uri = 'http://localhost:'+ port;

var _resolve;

var readyPromise = new Promise(resolve => {
  _resolve = resolve
})

console.log('> Starting Server...');
devMiddleware.waitUntilValid(() => {
  console.log('> Listening at ' + uri + '\n')
  // when env is testing, don't need open it
  if (autoOpenBrowser && process.env.ENV !== 'testing') {
    opn(uri)
  }
  _resolve()
})

var server = app.listen(port);

module.exports = {
  ready: readyPromise,
  close: () => {
    server.close()
  }
}

上面的代码用过vue-cli的朋友应该很熟悉。对于vue-cli的介绍你们能够本身去官网查看。这里推荐一个对配置文件逐句注释的[文章](https://github.com/DDFE/DDFE-blog/issues/10),细微之处仍是有差别的,可是大致不离。

咱们尽可能用直白的语言来分析一下这个文件,
1. 程序开始运行,引入环境的配置文件/config 这里前文提到为何能够省略index.js。而后判断process.env表示的用户环境变量 ENV 为什么种环境,官网翻译进程对象process是一个全局的,它提供有关当前Node.js进程的信息和控制。这个环境变量咱们能够在命令行中启动程序时输入,当node没法判断环境时咱们手动的设置为开发环境的变量,在/config/index.js config.dev.ENV <=> 'dev'。而后引入咱们须要的库和文件,好比工具函数库utils 自启动浏览器opn(服务启动后自动打开浏览器) 文件系统fs nodejs框架express(用来启动本地服务器,部署静态服务,模拟路由接口)。
2. 引入库以后即是定义咱们的整个项目服务app,经过webpack的nodeAPI编译开发环境的配置文件,定义webpack提供的服务的中间件webpack-dev-middleware,将编译内容写入内存中,启用热加载的中间件,html模板template更新则强制刷新页面,以及配置跨域代理请求的中间件。中间件的概念其实就是工做流的思想,记得有一个例子很直白
可乐的生成:水 -> 净化 -> 调配 -> 装瓶 -> 质检 -> 饮用可乐,水到可乐,每个中间过程都认为是一个中间件
3. 经过express.Router()来定义接口,全部本地请求的/api开头的url都解析以后的/api/:apiClass/:apiName,apiName对应/mock文件下的js文件名,apiName对应js文件暴露出的对象的属性也就是数据。。
4. 这里由于配置了mock的缘由我就去除了connect-history-api-fallback,它的做用由于找不到接口的话指定一个页面重定向,若是接口API找不到它就会默认定向到index.html。接下来是拼接/static文件路径,个人静态资源都是放在assets目录下就就删除了该文件夹。(对这点我也存有疑问就是vue-cli的这个/static文件夹究竟是指哪些静态资源?)。以后是服务启动,监听端口打开浏览器。

到这里,咱们就能够经过对src的源码进行修改开发了。

生产环境 build.js

process.env.ENV = 'prod';

var ora = require('ora');
var path = require('path')
var chalk = require('chalk')
var webpack = require('webpack')
var config = require('../config')
var webpackConfig = require('./webpack.prod.conf')

var spinner = ora('building for production...');
spinner.start()

webpack(webpackConfig, function (err, stats) {
  spinner.stop()
  if (err) throw err
  process.stdout.write(stats.toString({
    colors: true,
    modules: false,
    children: false,
    chunks: true,
    chunkModules: false
  }) + '\n\n')

  console.log(chalk.cyan('  Build complete.\n'))
  console.log(chalk.yellow(
    '  Tip: built files are meant to be served over an HTTP server.\n' +
    '  Opening index.html over file:// won\'t work.\n'
  ))
})

编译打包功能就不须要配置服务了,固然打包的时候须要一下提示,进度,就须要ora chalk这些模块了。打包这里和vue-cli不太同样得是我没有使用rmrf 而是用了一个插件CleanWebpackPlugin来清空/dist目录下的文件。固然也能够只清空某个文件而不是整个目录。

配置文件

/config/index.js && /build/utils.js

 

1. /config/index.js主要暴露了两个对象一个属性

var path = require('path');

module.exports = {
  // 项目根目录
  _root_: path.resolve(__dirname, '../'),
  // 生产环境设置
  build: {
    ENV: 'prod',
    index: path.resolve(__dirname, '../dist/index.html'), // 编译完成首页
    assestsRoot: path.resolve(__dirname, '../dist'), // 静态根目录
    assetsSubDirectory: 'static',
    assetsPublicPath: '',
    prodSourceMap: false,
    productionGzip: false,
    productionGzipExtensions: ['js', 'css']
  },
  // 开发环境配置
  dev: {
    ENV: 'dev',
    port: '3000',
    autoOpenBrowser: false,
    assetsSubDirectory: 'static',
    assetsPublicPath: '/',
    cssSourceMap: false,
    proxyTable: {
      // '/api': {
      //   target: 'http://localhost:3100',
      //   changeOrigin: true
      // }
    }
  }
}

这里注意的一个点就是build.assetsPublicPath <=> 编译发布的根目录,可配置为资源服务器域名或 CDN 域名,那么不少朋友vue编译完本地File://打不开就是由于这里配置的是'/'指的是服务器的根目录,部署到服务器上是没有问题的,若是你要本地打开,设为空字符串便可。

第二个须要注意的就是dev.proxyTable的接口属性,如个人配置其实就是跨域请求'http://localhost:3100/api'注意接口名的对应。


2. utils是在编写配置文件时你须要的一些函数,好比vue-cli中关于样式的loader都是在这里配置的

var path = require('path');
var config = require('../config');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var glob = require('glob');

exports.assetsPath = function(_path) {
  var assetsSubDirectory = process.env.ENV === 'prod' 
    ? config.build.assetsSubDirectory 
    : config.build.assetsSubDirectory;
  return path.posix.join(assetsSubDirectory, _path)
}

exports.resolve = function(dir) {
  return path.join(__dirname, '..', dir);
}

exports.cssLoaders = function(options) {
  var cssLoader = {
    loader: 'css-loader',
    options: {
      minmize: process.env.ENV === 'prod',
      sourceMap: options.sourceMap
    }
  }
  
  function generLoaders(loader, loaderOptions) {
    var loaders = [cssLoader, ];
    if(loader) {
      loaders.push({
        loader: loader +'-loader',
        options: Object.assign({}, loaderOptions, {
          sourceMap: options.sourceMap
        })
      })
    }

    if(options.extract) {
      return ExtractTextPlugin.extract({
        use: loaders,
        fallback: 'style-loader',
      })
    }else {
      return ['style-loader'].concat(loaders)
    }
  }

  return {
    css: generLoaders(),
    postcss: generLoaders(),
    less: generLoaders('less'),
    sass: generLoaders('sass', {indentedSyntax: true}),
    scss: generLoaders('sass')
  }
}

exports.styleLoader = function(option) {
  var output = [];
  var loaders = exports.cssLoaders(option);
  for(var extension in loaders){
    output.push({
      test: new RegExp('\\.'+ extension +'$'),
      use: loaders[extension]
    }) 
  }
  return output
}

exports.getEntries = function(_path) {
  var entries = {};
  glob.sync(_path).forEach(function(entry) {
    var basename = path.basename(entry, path.extname(entry));
    var pathname = entry.split('/').splice(-3).splice(0, 1) +'/'+ basename;
    entries[basename] = entry;
  });

  return entries;
}

1. assetsPath(_path)是返回静态资源_path的全路径,

2. resolve(dir)是返回dir的绝对路径,为何会单独写resolve主要是webpack的配置文件不在项目根目录而是在/build下。
3. getEntries(_path) 是经过glob(路径模式匹配模块)匹配多页面入口文件的函数,最终返回一个入口对象,在这里网上不少其余得例子都是

{
'module/index': ...
'module/user': ...
}

这致使开发环境下须要在url去添加http://localhost:3000/module/index.html才能查看文件,生产环境编译以后的文件也是在/dist/module/index.html 这里直接将basename 做为属性名则会解决。

4. styleLoader() 返回一个webpack配置文件中moudle.rules对应的数组,内部调用cssLoader(来生成对应的sass、less加载编译) 这里不太明白的朋友建议能够在vscode下断点调试一下,看他每次生成对象对应的一些配置。

webpack.*.conf.js

webpack的配置文件各类各样,这是由于他高度自定义决定的,你能够配置任何你想要的loader plugin来完成你的工做。像vue-cli即是定义了一个基础的base配置,以后区分开发与生产须要的不一样插件,都是代码复用。base.conf中应该注意的是多入口与单入口的配置

...
var entries = utils.getEntries('./src/modules/**/*.js');

module.exports = {
  // entry: {
  //   app: utils.resolve('/src/main.js'),
  // },
  entry: entries,
  output: {
    path: config.build.assestsRoot,
    filename: '[name].js',
    publicPath: process.env.ENV === 'prod' ? config.build.assetsPublicPath : config.dev.assetsPublicPath,
  },
...

 dev.conf的配置

module.exports = merge(baseWebpackConfig, {
  module: {
    rules: utils.styleLoader({
      sourceMap: config.dev.cssSourceMap
    })
  },
  plugins: [
    new webpack.DefinePlugin({
      'process.env': config.dev.ENV,
      dev_port: '"http://localhost:3000/api"'
    }),
    new webpack.HotModuleReplacementPlugin(),
    // spa 则应用以下配置
    // new HtmlWebpackPlugin({
    //   title: 'Single-Page'+pathname,
    //   filename: 'index.html',
    //   template: utils.resolve('/src/index.html'),
    //   inject: true
    // })
  ]
})
// 多页面应用配置 根据modules 动态生成html
var pages = utils.getEntries('./src/modules/**/*.html');

for(var pathname in pages){
  var conf = {
    filename: pathname +'.html',
    template: pages[pathname],
    chunks: [pathname],
    inject: true
  }
  module.exports.plugins.push(new HtmlWebpackPlugin(conf))
}

该配置只使用了三个插件 DefinePlugin这个插件能够用来定义全局变量,在编译时将你的引用的dev_port 转换为 "http://locahost:3000/api" 要注意的是他转化的是值,好比 dev_port <=> 'b' 那么你在编写代码时 引用了dev_port实际上他是将变量名替换为b而不是'b'字符串,能够看以下报错,因此要使用字符串时须要外层包裹单引号。

// dev.conf
...
 new webpack.DefinePlugin({
      'process.env': config.dev.ENV,
      dev_port: 'b'
    }),
...
// /src/modules/index.js
...
console.log(dev_port);
...

 

HotModuleReplacementPlugint插件在页面进行变动的时候只会重绘对应的页面模块,不会重绘整个html文件。

HtmlWebpackPlugin有几个页面则对应生成几个配置。

prod.conf

与dev.conf相似的有,
DefinePlugin 可是这个时候要把dev_port切换后台接口所在服务器的域名。这样不用每次编译前再去修改 固然叫host可能更准确(忽略个人瞎起名字)。HtmlWebpackPlugin就是一些生成html文件是否压缩是否去除属性引用的配置。

不一样之处有配置了CommonsChunkPlugin提取公共模块,(要注意minChunks最少引用次数的配置),ExtractTextPlugin提取CSS文件 而不是style标签插入html。

结语

啃文档啃了一个星期多,边啃边练一个星期,构思写做三天,起码如今对weback的配置再恐惧了,文章有点过长能看到这的朋友首先谢谢你的阅读,源码在[github](https://github.com/abzerolee/webpack_env) 这个环境也是当时用来打包一个之前用jquery的项目的因此没有配框架vue react之类的。过段时间啃完了create-react-app 的实现应该还会出一期关于 webpack 原理的学习笔记。还但愿继续关注。文中若有一些问题也但愿你们及时指正。

相关文章
相关标签/搜索