Webpack:
Bundle Your Front-End Resources
Evan Ye@Hackathon Taiwan x Taichung
Who Am I
- 臺中科技大學資工大四
- Web Developer
- 台中前端社群跑龍套講者
- laravel-taiwan/docs 貢獻者
- React
- Laravel
- 內耳失衡成就(1/1)
葉裕安 Evan Ye
@jigsawye
jigsawye.com jigsaw.ye@gmail.com github.com/jigsawye facebook.com/jigsaw.ye
議題
- 講古早
- 以前怎麼搞前端
- 後來怎麼搞前端
- 現在怎麼搞前端
- Webpack Workshop(16關)
- 打完收工
以前怎麼搞前端
<script> <style> 打天下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
/* CSS */
</style>
</head>
<body>
<script src="/js/jquery.min.js">
<script>
// JavaScript Code
</script>
</body>
</html>
進階一點
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="/css/app.css">
</head>
<body>
<script src="/js/jquery.min.js">
<script src="/js/app.js">
</body>
</html>
事情才沒那麼單純
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="/css/normailze.css">
<link rel="stylesheet" href="/css/vendor.css">
<link rel="stylesheet" href="/css/common.css">
<link rel="stylesheet" href="/css/app.css">
<link rel="stylesheet" href="/css/another.css">
<!-- etc -->
</head>
<body>
<script src="/js/jquery.min.js">
<script src="/js/jquery-mobile.min.js">
<script src="/js/bootstrap.min.js">
<script src="/js/first-library.min.js">
<script src="/js/second-library.min.js">
<script src="/js/common.js">
<script src="/js/app.js">
<!-- etc -->
</body>
</html>
Request * N
以前有個案子在 head 放了
30 多支的 JavaScript
(Angular)
缺點
- 雜亂無章難以管理
- Request 次數太多
- 程式碼被看光光
優點
- 適用於新手村
- 簡單快速方便
後來怎麼搞前端
第一部份
處理資源
Grunt
處理你的前端資源
- Compiler
- Uglify
- ...
- Bootstrap 3 就是用 Grunt
module.exports = function(grunt) {
grunt.initConfig({
concat: {
'dist/all.js': ['src/*.js']
},
uglify: {
'dist/all.min.js': ['dist/all.js']
},
jshint: {
files: ['gruntfile.js', 'src/*.js']
},
watch: {
files: ['gruntfile.js', 'src/*.js'],
tasks: ['jshint', 'concat', 'uglify']
}
});
// Load Our Plugins
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-watch');
// Register Default Task
grunt.registerTask('default', ['jshint', 'concat', 'uglify']);
};
像這樣
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script src="/dist/all.min.js">
</body>
</html>
但是它已經埋沒在歷史的洪流中
每18至24個月,前端都會難一倍
你問我那之前學 Grunt 是?
Gulp
我剛學的時候正夯
基本上跟 Grunt 做一樣的事
- 更高效能
- 更易用
- 更易學
var gulp = require('gulp');
var jshint = require('gulp-jshint');
var concat = require('gulp-concat');
var rename = require('gulp-rename');
var uglify = require('gulp-uglify');
// Lint JS
gulp.task('lint', function() {
return gulp.src('src/*.js')
.pipe(jshint())
.pipe(jshint.reporter('default'));
});
// Concat & Minify JS
gulp.task('minify', function(){
return gulp.src('src/*.js')
.pipe(concat('all.js'))
.pipe(gulp.dest('dist'))
.pipe(rename('all.min.js'))
.pipe(uglify())
.pipe(gulp.dest('dist'));
});
// Watch Our Files
gulp.task('watch', function() {
gulp.watch('src/*.js', ['lint', 'minify']);
});
// Default
gulp.task('default', ['lint', 'minify', 'watch']);
像這樣
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script src="/dist/all.min.js">
</body>
</html>
對,做一樣的事
它也已經埋沒在歷史的洪流中
每18至24個月,前端都會難一倍
你問我之前學 Gulp 是?
第二部份
取得 Third Part Library
Bower
管理你的前端資源
- JavaScript(jQuery, etc.)
- CSS(Bootstrap, etc.)
- ...
但是它已經被埋沒在歷史的洪流中
每18至24個月,前端都會難一倍
第三部份
預處理器
JavaScript 提名的有
- CoffeeScript
- LiveScript
- TypeScript
還好他們現在好像都活得好好的
CSS 提名的有
- LESS
- SASS
LESS 好像已經被埋沒了
每18至24個月,前端都會難一倍
現在怎麼搞前端
現世的王道組合
套件管理
預處理器(Babel)
下一代 JavaScript
資源處理
Webpack?
又比 Gulp 好在哪?
輸出只有 js 與 image(甚至沒有)
Hot Module Replacement
潮
Workshop
首要之事
$ npm i -g webpack webpack-dev-server
// npm install --global webpack webpack-dev-server
先安裝 Webpack 就對了
你必須確定已經安裝 node.js
安裝本次 Workshop 的 Dependencies
// package.json
{
"dependencies": {
"babel-core": "~6.0.20",
"babel-loader": "~6.0.1",
"babel-preset-es2015": "~6.0.15",
"babel-preset-react": "~6.0.15",
"bundle-loader": "^0.5.4",
"css-loader": "~0.16.0",
"file-loader": "~0.8.4",
"history": "^1.17.0",
"html-webpack-plugin": "~1.6.2",
"jquery": "~2.1.4",
"jsx-loader": "~0.13.2",
"open-browser-webpack-plugin": "0.0.1",
"react": "~0.14.2",
"react-dom": "~0.14.2",
"react-router": "^1.0.3",
"style-loader": "~0.12.3",
"url-loader": "~0.5.6",
"webpack": "~1.11.0",
"webpack-dev-server": "^1.10.1"
}
}
$ npm i
# npm install
- 建立專案目錄
- 新增 package.json
- 貼上內容
- 安裝
前言
$ browserify main.js > bundle.js
# 會同等於
$ webpack main.js bundle.js
Webpack 是一個如同 Grunt 與 Gulp 的前端建構(Build)系統。
可以像 Browserify 一般將它作為模組打包器
(module bundler)
// webpack.config.js
module.exports = {
entry: './main.js',
output: {
filename: 'bundle.js'
}
};
設定檔:webpack.config.js
透過設定檔執行
$ webpack
一些你應該知道的 command-line 選項
-
webpack – 在 development 環境 build 一次
-
webpack -p – 在 production 環境 build 一次(壓縮)
-
webpack --watch – 監控並自動 build
-
webpack -d – 包含 source maps
-
webpack --colors – 讓它更美觀
你可以撰寫 package.json 檔案的 scripts 區塊
// package.json
{
// ...
"scripts": {
"dev": "webpack-dev-server --devtool eval --progress --colors",
"deploy": "NODE_ENV=production webpack -p"
},
// ...
}
Demo01:進入點檔案(source)
// main.js
document.write('<h1>Hello World</h1>');
進入點檔案會被 Webpack 讀取並 build 成 bundle.js
例如,main.js 是個進入點檔案
Webpack 會遵循 webpack.config.js 來 build bundle.js
index.html
<html>
<body>
<script type="text/javascript" src="bundle.js"></script>
</body>
</html>
// webpack.config.js
module.exports = {
entry: './main.js',
output: {
filename: 'bundle.js'
}
};
執行伺服器,瀏覽 http://127.0.0.1:8080。
$ webpack-dev-server
Demo02:多個進入點檔案(source)
// main1.js
document.write('<h1>Hello World</h1>');
// main2.js
document.write('<h2>Hello Webpack</h2>');
多個進入點檔案是可行的
這在多頁的 app 相當的有用
webpack.config.js
index.html
<html>
<body>
<script src="bundle1.js"></script>
<script src="bundle2.js"></script>
</body>
</html>
module.exports = {
entry: {
bundle1: './main1.js',
bundle2: './main2.js'
},
output: {
filename: '[name].js'
}
};
Demo03:Babel-loader(source)
index.html
main.jsx 是個 JSX 檔案
import React from 'react';
import ReactDOM from 'react-dom';
ReactDOM.render(
<h1>Hello, world!</h1>,
document.querySelector('#wrapper')
);
<html>
<body>
<div id="wrapper"></div>
<script src="bundle.js"></script>
</body>
</html>
webpack.config.js
module.exports = {
entry: './main.jsx',
output: {
filename: 'bundle.js'
},
module: {
loaders:[
{
test: /\.js[x]?$/,
exclude: /node_modules/,
loader: 'babel-loader?presets[]=es2015&presets[]=react'
},
]
}
};
module.loaders 部份被使用於指定 loaders 上方的程式片段使用了 babel-loader 它還同時需要 babel-preset-es2015 與 babel-preset-react 來轉譯 ES6 及 React
你也可以使用其他方式來設定 babel 的 query 選項
module: {
loaders: [
{
test: /\.jsx?$/,
exclude: /node_modules/,
loader: 'babel',
query: {
presets: ['es2015', 'react']
}
}
]
}
Demo04:CSS-loader(source)
Webpack 讓你可以 在 JS 檔案中 require CSS,並使用 CSS-loader 來預處理 CSS 檔案
index.html
<html>
<head>
<script type="text/javascript" src="bundle.js"></script>
</head>
<body>
<h1>Hello World</h1>
</body>
</html>
require('./app.css');
main.js
body {
background-color: blue;
}
app.css
webpack.config.js
module.exports = {
entry: './main.js',
output: {
filename: 'bundle.js'
},
module: {
loaders:[
{ test: /\.css$/, loader: 'style-loader!css-loader' },
]
}
};
這邊需要使用兩種 loaders 來轉譯 CSS 檔案
- CSS-loader 讀取 CSS 檔案
- Style-loader 寫入 Style 標籤至 HTML 頁面
- 不同的 loaders 使用驚嘆號(!)來連接
webpack.config.js
module.exports = {
entry: './main.js',
output: {
filename: 'bundle.js'
},
module: {
loaders:[
{ test: /\.css$/, loaders: ['style-loader', 'css-loader'] },
]
}
};
也可以使用陣列來使用多個 loaders
(注意:key 是 loaders)
在執行伺服器之後 index.html 會擁有 inline style
<head>
<script type="text/javascript" src="bundle.js"></script>
<style type="text/css">
body {
background-color: blue;
}
</style>
</head>
Demo05:Image loader(source)
var img1 = document.createElement("img");
img1.src = require("./small.png");
document.body.appendChild(img1);
var img2 = document.createElement("img");
img2.src = require("./big.png");
document.body.appendChild(img2);
Webpack 也可以在 JS 檔案中 require 圖片
main.js
<html>
<body>
<script type="text/javascript" src="bundle.js"></script>
</body>
</html>
index.html
webpack.config.js
module.exports = {
entry: './main.js',
output: {
filename: 'bundle.js'
},
module: {
loaders:[
{ test: /\.(png|jpg)$/, loader: 'url-loader?limit=8192' }
]
}
};
在執行伺服器之後 small.png 及 big.png 會變成下方的 URLs
<img src="data:image/png;base64,iVBOR...uQmCC">
<img src="4853ca667a2b8b8844eb2693ac1b2578.png">
Demo06:CSS Module(source)
css-loader?modules(查詢參數 modules) 會開啟 CSS Modules 功能 這代表你 module 的 CSS 預設會是 local scoped 的 CSS 你可以在 selector 及/或 rules 使用 :global(...) 切換成關閉(更多資訊)
app.css
index.html
<html>
<body>
<h1 class="h1">Hello World</h1>
<h2 class="h2">Hello Webpack</h2>
<div id="example"></div>
<script src="./bundle.js"></script>
</body>
</html>
.h1 {
color:red;
}
:global(.h2) {
color: blue;
}
main.jsx
var React = require('react');
var ReactDOM = require('react-dom');
var style = require('./app.css');
ReactDOM.render(
<div>
<h1 className={style.h1}>Hello World</h1>
<h2 className="h2">Hello Webpack</h2>
</div>,
document.getElementById('example')
);
webpack.config.js
module.exports = {
entry: './main.jsx',
output: {
filename: 'bundle.js'
},
module: {
loaders:[
{
test: /\.js[x]?$/,
exclude: /node_modules/,
loader: 'babel-loader',
query: {
presets: ['es2015', 'react']
}
},
{
test: /\.css$/,
loader: 'style-loader!css-loader?modules'
}
]
}
};
執行伺服器
$ webpack-dev-server
瀏覽 http://127.0.0.1:8080
-
只有第二個 h1 是紅色,因為它的 CSS 為 local scoped
-
兩個 h2 都是藍色,因為它的 CSS 是 global scoped
Demo07:UglifyJs Plugin(source)
var longVariableName = 'Hello';
longVariableName += ' World';
document.write('<h1>' + longVariableName + '</h1>');
Webpack 擁有 plugin 系統來擴增它的功能 例如,UglifyJs Plugin 會壓縮輸出的(bundle.js) JS 程式碼
main.js
<html>
<body>
<script src="bundle.js"></script>
</boby>
</html>
index.html
webpack.config.js
var webpack = require('webpack');
var uglifyJsPlugin = webpack.optimize.UglifyJsPlugin;
module.exports = {
entry: './main.js',
output: {
filename: 'bundle.js'
},
plugins: [
new uglifyJsPlugin({
compress: {
warnings: false
}
})
]
};
在執行伺服器之後,main.js 會被壓縮成這樣
var o="Hello";o+=" World",document.write("<h1>"+o+"</h1>")
Demo08:HTML Webpack Plugin 及
Open Browser Webpack Plugin(source)
此 demo 為你展示如何載入第三方 plugins
-
html-webpack-plugin 會為你建立 index.html
-
open-browser-webpack-plugin 會在 Webpack 載入後 打開一個新的瀏覽器分頁
webpack.config.js
main.js
document.write('<h1>Hello World</h1>');
var HtmlwebpackPlugin = require('html-webpack-plugin');
var OpenBrowserPlugin = require('open-browser-webpack-plugin');
module.exports = {
entry: './main.js',
output: {
filename: 'bundle.js'
},
plugins: [
new HtmlwebpackPlugin({
title: 'Webpack-demos'
}),
new OpenBrowserPlugin({
url: 'http://localhost:8080'
})
]
};
執行 webpack-dev-server
$ webpack-dev-server
現在你不必手動撰寫 index.html
也不必自己打開瀏覽器
Webpack 會為你做這些事
Demo09: 環境標記(flags)(source)
document.write('<h1>Hello World</h1>');
if (__DEV__) {
document.write(new Date());
}
你可以透過環境標記,讓某些程式碼只在開發環境時啟用
main.js
<html>
<body>
<script src="bundle.js"></script>
</body>
</html>
index.html
webpack.config.js
var webpack = require('webpack');
var devFlagPlugin = new webpack.DefinePlugin({
__DEV__: JSON.stringify(JSON.parse(process.env.DEBUG || 'false'))
});
module.exports = {
entry: './main.js',
output: {
filename: 'bundle.js'
},
plugins: [devFlagPlugin]
};
# Linux & Mac
$ env DEBUG=true webpack-dev-server
# Windows
$ DEBUG=true webpack-dev-server
現在傳遞環境變數至 webpack
Demo10:程式碼分割(source)
對於大型的 web app 來說將所有程式碼放置於同個檔案是很沒效率的,Webpack 讓你可以將它們分割成幾個 chunk 。
特別在某些程式碼區塊只需要在某些情況下 required,這些 chunk 可以按需求載入。
require.ensure 告知 Webpack ./a.js 必須從 bundle.js
分離並 build 至單一的 chunk 檔案
首先,使用 require.ensure 來定義分割點(官方文件)
// main.js
require.ensure(['./a'], function(require) {
var content = require('./a');
document.open();
document.write('<h1>' + content + '</h1>');
document.close();
});
// a.js
module.exports = 'Hello World';
現在 Webpack 會處理依賴、輸出檔案及執行時的相關事務 你不必放置任何冗餘的東西至 index.html 及webpack.config.js
<html>
<body>
<script src="bundle.js"></script>
<body>
</html>
module.exports = {
entry: './main.js',
output: {
filename: 'bundle.js'
}
};
表面上,你不會感覺到任何差異 不過,Webpack 實際上 builds main.js 及 a.js 至不同的 chunks(bundle.js 及1.bundle.js) 並在有需求的時候從 bundle.js 載入 1.bundle.js
執行伺服器
$ webpack-dev-server
Demo11:使用 bundle-loader 進行程式碼分割(source)
另一個程式碼分割的方式是使用 bundle-loader
// main.js
// 現在 a.js 被請求,他會被 bundled 至另一個檔案
var load = require('bundle-loader!./a.js');
// 若要等待 a.js 直到他可用(並取得導出)
// 你需要為進行它非同步等待。
load(function(file) {
document.open();
document.write('<h1>' + file + '</h1>');
document.close();
});
require('bundle-loader!./a.js') 告知 Webpack 從另一個 chunk 載入 a.js
現在 Webpack 會 build main.js 至 bundle.js a.js 至 1.bundle.js
Demo12:通用 chunk(source)
當多個 scripts 擁有通用的 chunks
你可以使用 CommonsChunkPlugin
取出通用的部分至一個分離的檔案
webpack.config.js
// main1.jsx
var React = require('react');
var ReactDOM = require('react-dom');
ReactDOM.render(
<h1>Hello World</h1>,
document.getElementById('a')
);
// main2.jsx
var React = require('react');
var ReactDOM = require('react-dom');
ReactDOM.render(
<h2>Hello Webpack</h2>,
document.getElementById('b')
);
<html>
<body>
<div id="a"></div>
<div id="b"></div>
<script src="init.js"></script>
<script src="bundle1.js"></script>
<script src="bundle2.js"></script>
</body>
</html>
index.html
webpack.config.js
var CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin");
module.exports = {
entry: {
bundle1: './main1.jsx',
bundle2: './main2.jsx'
},
output: {
filename: '[name].js'
},
module: {
loaders:[
{
test: /\.js[x]?$/,
exclude: /node_modules/,
loader: 'babel-loader',
query: {
presets: ['es2015', 'react']
}
},
]
},
plugins: [
new CommonsChunkPlugin('init.js')
]
}
Demo13:第三方 chunk(source)
你也可以使用 CommonsChunkPlugin
從 script 取出第三方函式庫至分離的檔案
var $ = require('jquery');
$('h1').text('Hello World');
main.js
<html>
<body>
<h1></h1>
<script src="vendor.js"></script>
<script src="bundle.js"></script>
</body>
</html>
index.html
webpack.config.js
var webpack = require('webpack');
module.exports = {
entry: {
app: './main.js',
vendor: ['jquery'],
},
output: {
filename: 'bundle.js'
},
plugins: [
new webpack.optimize.CommonsChunkPlugin(
/* chunkName= */'vendor',
/* filename= */'vendor.js'
)
]
};
讓 $ 與 jQuery 在每個 module 都可以使用 而不必撰寫require("jquery") 需要使用 ProvidePlugin(官方文件)
// main.js
$('h1').text('Hello World');
// webpack.config.js
var webpack = require('webpack');
module.exports = {
entry: {
app: './main.js'
},
output: {
filename: 'bundle.js'
},
plugins: [
new webpack.ProvidePlugin({
$: "jquery",
jQuery: "jquery",
"window.jQuery": "jquery"
})
]
};
Demo14:暴露(expose)全域變數 (source)
var data = 'Hello World';
使用一些全域變數,但不想讓他們包含在 Webpack bundle 中 可以啟用 webpack.config.js 中的 externals 區塊 (官方文件)
例如,我們有個 data.js
我們可以暴露 data 作為全域變數
// webpack.config.js
module.exports = {
entry: './main.jsx',
output: {
filename: 'bundle.js'
},
module: {
loaders:[
{
test: /\.js[x]?$/,
exclude: /node_modules/,
loader: 'babel-loader',
query: {
presets: ['es2015', 'react']
}
},
]
},
externals: {
// require('data') 在全域變數資料中是外部且可用的
'data': 'data'
}
};
現在,你在 script 中 require data 作為 module 變數
但是它實際上是全域變數
// main.jsx
var data = require('data');
var React = require('react');
var ReactDOM = require('react-dom');
ReactDOM.render(
<h1>{data}</h1>,
document.body
);
Demo15:Hot Module Replacement(source)
Hot Module Replacement(HMR)在應用程式執行時進行交換、增加,或移除 modules,但頁面不需重新載入 你有兩種方式在 webpack-dev-server 啟用 Hot Module Replacement
app.js
index.html
<html>
<body>
<div id='root'></div>
<script src="/static/bundle.js"></script>
</body>
</html>
import React, { Component } from 'react';
export default class App extends Component {
render() {
return (
<h1>Hello World</h1>
);
}
}
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));
這些選項的意義:
-
--hot: 增加 HotModuleReplacementPlugin並切換 server 至 hot 模式
-
--inline: 嵌入 webpack-dev-server runtime 至 bundle
-
--hot --inline: 還增加了 webpack/hot/dev-server 進入點
(1) 在 command line 指定 --hot 及 --inline
$ webpack-dev-server --hot --inline
-
增加 new webpack.HotModuleReplacementPlugin() 至 plugins 區塊
-
增加 webpack/hot/dev-server 及 webpack-dev-server/client?http://localhost:8080 至 entry 區塊
(2) 修改 webpack.config.js
var webpack = require('webpack');
var path = require('path');
module.exports = {
entry: [
'webpack/hot/dev-server',
'webpack-dev-server/client?http://localhost:8080',
'./index.js'
],
output: {
filename: 'bundle.js',
publicPath: '/static/'
},
plugins: [
new webpack.HotModuleReplacementPlugin()
],
module: {
loaders: [{
test: /\.jsx?$/,
exclude: /node_modules/,
loaders: ['babel-loader'],
query: {
presets: ['es2015', 'react']
},
include: path.join(__dirname, '.')
}]
}
};
webpack.config.js 看起來會像這樣
瀏覽 http://localhost:8080
你應該在瀏覽器中看見「Hello World」
現在執行 dev 伺服器
$ webpack-dev-server
別關閉伺服器。開啟一個新的終端機編輯 App.js 並將「Hello World」修改成「Hello Webpack」 儲存並看看瀏覽器發生什麼事。
Demo16:React router(source)
此 demo 使用 webpack 來 build React-router 的官方範例
讓我們想像一個小型 app
它擁有 dashboard、inbox 及 calendar
+---------------------------------------------------------+
| +---------+ +-------+ +--------+ |
| |Dashboard| | Inbox | |Calendar| Logged in as Jane |
| +---------+ +-------+ +--------+ |
+---------------------------------------------------------+
| |
| Dashboard |
| |
| |
| +---------------------+ +----------------------+ |
| | | | | |
| | + + | +---------> | |
| | | | | | | |
| | | + | | +-------------> | |
| | | | + | | | | |
| | | | | | | | | |
| +-+---+----+-----+----+ +----------------------+ |
| |
+---------------------------------------------------------+
$ webpack-dev-server
Any Question?
Thanks For Listening!
每18至24個月,前端都會難一倍
請銘記在心
哪天這些東西也會被歷史給埋沒...
Webpack: Bundle Your Front-End Resources
By jigsawye
Webpack: Bundle Your Front-End Resources
- 3,133