從 Webpack 1 到 Webpack 3!

Cooookie
StreetVoice Lab
Published in
10 min readSep 20, 2017

因為 海總理 日日逼迫嗚嗚,而且 StreetVoice 原本的 js files 太巨大,再加上官方已經停止維護 Webpack 1:

webpack v1 is deprecated. We encourage all developers to upgrade to webpack 2.

所以就來升級啦!

至於可能有人想問為什麼直接從 v1 升到 v3?

完全是因為動作太慢哈哈哈!要升級時 Webpack 版本號已經來到 3.5 了,那乾脆就跳過 v2。

這次升級後可以明顯感受到的差異是:

  • build 的速度快了 33 %!
  • webpack config 的寫法比之前好理解一些
  • 終於有乖乖做 dynamic import

以下會將這次升級的過程分成幾個部分來說明!

升級相關的調整

升級相關套件:

$ npm install —save-dev webpack@^3.5.6 webpack-bundle-tracker webpack-dev-middleware babel babel-core babel-loader babel-polyfill babel-preset-es2015 babel-preset-react uglifyjs-webpack-plugin lodash-webpack-plugin$ npm install —save react@^15.6.0 react-dom@^15.6.0 prop-types react-addons-update react-transition-group

修改 webpack.config.js:

  • resolve.root, resolve.modulesDirectories 的內容統一放到 resolve.modules
  • resolve.extensions 陣列中第一個不用再放空字串
  • module.loaders.loaders 改為 module.rules.use
  • 開發時使用的 NoErrorsPlugin 更名為 NoEmitOnErrorsPlugin
  • Production 使用 ModuleConcatenationPlugin 減少 closure 數量加快 JS 執行速度
resolve: {
modules: [path.resolve('./static/jsx'), 'node_modules'],
extensions: ['.js', '.jsx'],
},
module: {
rules: [
{
...,
use: ['babel-loader'],
},
]
},
plugins: [
new webpack.optimize.ModuleConcatenationPlugin(),
new webpack.NoEmitOnErrorsPlugin(),
]

.babelrc

{
"presets": [
["es2015", { "loose" : true, "modules": false }],
"react",
],
"plugins": [
"react-hot-loader/babel", // webpack hmr
"transform-object-rest-spread", // webpack 3 import()
"transform-class-properties", // ES6 class
"syntax-dynamic-import", // ES6 decorator
"transform-decorators-legacy", // ES6 spread properties
],
}

既然是升級,那 react 也要一起!

基本上升級 react 的過程中沒遇到什麼問題,唯一一個是 this.refs.xxx 正式被棄用,不再支持字串的用法,使用 callback 的方式更有彈性。

// before
<MyComponent ref="foo" />
// usage
this.refs.foo
// after
<MyComponent ref={(el) => { this.foo = el; }} />
// usage
this.foo

寫了個 HOC 處理 dynamic import

在這次升級之前其實都沒有好好實施 dynamic import(打屁股),所以就來使用 Webpack 的 import() 吧!

順便提一下 Webpack 3 的新功能 — Magic comment: 可以使用 import( /* webpackChunkName: "chunk-name" */ "module") 來替 chunk 命名。

// LazyLoading.jsx
import React from 'react';
const LazyLoading = (getComponent, moduleName = 'default') => class LazyLoadingWrapper extends React.Component {
constructor(props) {
super(props);
this.state = {
Component: null,
};
}
componentWillMount() {
getComponent()
.then((esModule) => {
// store the component in state
this.setState({ Component: esModule[moduleName] });
})
.catch((err) => {
console.log('error')
console.log(err)
});
}
render() {
const { Component } = this.state;
// react cannot render null component
if (!Component) return <div style={{ display: 'none' }}></div>;
return <Component {...this.props} />;
}
};
export default LazyLoading;// webpack.config.js
output: {
...,
chunkFilename: 'chunk-[name]-[chunkhash].js',
}
// Usage
import LazyLoading from 'LazyLoading.jsx';
LazyLoading(() => import(/* webpackChunkName: "MyLazyLoagingComponent" */ 'path-to-component'));

發文附圖!

上圖以 StreetVoice 加入歌單的功能為例,使用 dynamic import 之後,就會動態載入相應的 chunk ,減少網站第一次載入的檔案大小,進而降低網站載入的時間。

使用 dynamic import 後在 production 拿不到 chunk files

因為當初的 webpack config 並沒有特別指定 publicPath,導致在動態載入時跑去 / 拿了,所以請大家 千萬 不要忘了在 webpack.config.js 加上 publicPath

// webpack.config.js
output: {
publicPath: 'your-CDN',
}

從 webpack-dev-server 到 webpack-hot-middleware

同時也升級了 react-hot-loader ,跟著官方的 Upgrade Guide ,升完看起來不錯?但實際測試後發現了幾個問題:

  1. component 外面多了一層 <AppContainer> 導致原本 component 的 public method 無法順利被呼叫
  2. 在 build production 時 react-hot-loader 相關的 code 也被 bundle 進去導致檔案變大
  3. 有些 component hmr 會失效

一氣之下改用 express + webpack-dev-middleware + webpack-hot-middleware 試試看,結果意外解決了問題?

想不通為什麼換了一套之後 hmr 就沒有問題了,如果有人知道可以留言告訴我嗎?

麻煩勒 (o´・ε・`o)

// server.js
const express = require('express');
const webpack = require('webpack');
const config = require('path-to-config');
const createWebpackMiddleware = require('webpack-dev-middleware');
const createWebpackHotMiddleware = require('webpack-hot-middleware');
const path = require('path');
const compiler = webpack(config);
const app = express();
const webpackDevMiddleware = createWebpackMiddleware(compiler, {
publicPath: config.output.publicPath,
hot: true,
headers: {'Access-Control-Allow-Origin': '*'},
inline: true,
historyApiFallback: true,
watchOptions: {
poll: 500
}
});
app.use(webpackDevMiddleware);
app.use(createWebpackHotMiddleware(compiler));
app.listen(3000, () => console.log('Listening at http://localhost:3000'));
// webpack.config.js
entry: {
'app': [
'babel-polyfill',
'react-hot-loader/patch',
'webpack/hot/only-dev-server',
'webpack-hot-middleware/client?path=http://localhost:3000/__webpack_hmr',
'./src/app',
]
}
// App.js
function render() {
ReactDOM.render(
<App />,
document.getElementById('root')
);
}
render();if (module.hot) {
module.hot.accept('path-to-App', () => {
render();process.env.NODE_ENV
}
}

而檔案變大則是因為 babel plugin 在 build 時 process.env.NOD_ENV 還是 undefined,在 build 時加上 NODE_ENV=production 就解決了。

// package.json
"scripts": {
"build-production": "NODE_ENV=production webpack --config webpack.config.js --progress",
}

這樣就完成升級啦!希望可以幫助升級苦手的人:)

工商時間

歡迎喜歡獨立音樂的工程師一起來 StreetVoice 玩,當然只要愛寫扣都歡迎啦哈哈哈,我們缺 Python 工程師!也缺 PM ,什麼都缺,歡迎洽詢!

--

--