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 原理的學習筆記。還但願繼續關注。文中若有一些問題也但願你們及時指正。

相關文章
相關標籤/搜索