寫過 React ,用的是 create-react-app ,寫過 Vue ,用的是 vue-cli , 第一次想了解一下 Webpack 。javascript
個人環境 Mac OS, node: v8.11.1, npm: 5.6.0, Webpack: 3.12.0css
我就不說亂七八糟的術語了,就是把不少的 JS 文件打包到一個文件(固然也可能不止一個)的工具,方便咱們寫模塊化的 JS 代碼。而經過一些 plugin 和 loader 可能提供一些其餘有用的功能以及處理其餘格式的文件。html
先建立一個文件夾,在終端運行命令 npm init 來建立一個 package.json 文件,這個文件用來描述項目信息,隨便填或者一直回車就能夠。前端
package.jsonvue
{ "name": "webpack-study-note-1", "version": "1.0.0", "description": "webpack學習筆記", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "repository": "https://github.com/G-lory/front-end-practice/tree/master/webpack/study-notes", "author": "G-lory", "license": "ISC" }
先建一個 src 文件夾用來 js 源文件。建立兩個 js 文件。java
// index.js const foo = require('./others.js'); let app = document.getElementById('app'); for (let i = 0; i < 10; i++) { let p = document.createElement('p'); p.innerText = foo(i); app.appendChild(p); } // others.js function foo(idx) { return `the ${idx + 1}th row`; } module.exports = foo;
並在根目錄建立 index.htmlnode
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width,initial-scale=1.0"> <title>webpack study notes</title> </head> <body> <div id="app"></div> </body> </html>
目錄結構是這樣的:react
若是想在 index.html 引用全部的js文件,就要經過<srcipt>標籤將js文件所有導入,並且還要注意順序。如今經過 module.exports 和 require 在JS中引用,而後把這些文件打包成一個文件,那麼 index.html 直接引用最終的那個 js 文件就能夠了。webpack
首先安裝 Webpack 。這裏使用 Webpack3 版本3到4有不少變化,若是你用的4,基本就不用看下去了。git
安裝命令: npm install webpack@3 --save-dev
其中 install 可簡寫爲 i , --save-dev 可簡寫爲 -D,表示僅在開發環境依賴,會在package.json的 devDependencies 字段記錄 。相對的是 --save 表示運行時依賴,簡寫爲 -S, 會在package.json的 dependencies 字段記錄。@3 表示指定安裝版本。
項目下會生成一個 node_modules 文件夾。裏面是安裝的依賴包。不用去管這個文件夾。
而後在根目錄下建立 webpack.config.js 文件。這是webpack默認的配置文件名。這個文件其實就是一個普通的 js 腳本文件,能夠經過require引用一些模塊,最後導出配置對象。
// webpack.config.js var path = require('path'); // node 內置模塊 module.exports = { entry: './src/index.js', // 入口文件 至關於 entry: { main: './src/index.js' } output: { // 出口 path: path.resolve(__dirname, 'dist'), filename: 'bundle.js' } }
一個最簡單的配置文件,指定了輸入輸出。輸入和輸出均可以指定多個,這裏暫時用不到。
在 package.json 中添加打包命令
"scripts": { "build": "webpack" },
而後在命令行執行 npm run build 就能夠進行打包了,會生成文件 /dist/bundle.js 。
打開文件能夠看到,前面是 webpack 生成的一些代碼,後面就是 index.js 和 others.js 中的代碼。
而後在 index.html 中引用文件
<script src="dist/bundle.js"></script>
在瀏覽器打開 index.html 文件 正常運行。
完整代碼見: https://github.com/G-lory/front-end-practice/tree/master/webpack/study-notes/step1
個人我的感受,loader就是處理文件的,先使用loader將文件轉換成想要的樣子,好比Webpack默認不能處理的圖片要先使用file-loader處理,es6先使用babel-loder處理成es5防止瀏覽器不兼容等等。
而 plugin 能夠作一些其餘的神奇並且頗有用的事情(我在說什麼……
以前的代碼使用的ES6,如今就嘗試下把它轉換成ES5,須要使用 babel-loader。
安裝:
npm install babel-loader@7 babel-core babel-preset-env -D
注意這裏爲了和webapck3兼容,須要指定 babel-loader 版本。
而後修改 webpack 配置文件。這裏 babel 的配置含義可見 https://segmentfault.com/a/1190000008159877
var path = require('path'); module.exports = { entry: './src/index.js', // 入口 output: { // 出口 path: path.resolve(__dirname, 'dist'), filename: 'bundle.js' }, module: { // 配置loader rules: [ { test: /\.jsx?/, // 正則表達式 匹配文件名 exclude: /node_modules/, // exclude 表示排除的路徑 也能夠添加 include 字段設置匹配路徑 use: { loader: 'babel-loader', // 對符合上面約束條件的文件 使用的 loader options: { presets: ['env'] } } } ] } }
babel 默認是不進行轉換的,須要設置插件,這裏經過 presets 設置插件指定代碼的轉換規則。
再次執行 npm run build 能夠看到 bundle.js 中 let 都變成了 var。說明 babel 生效了。
接下來在再試一下 Promise。
修改 JS 代碼
//index.js const foo = require('./others.js'); let app = document.getElementById('app'); for (let i = 0; i < 10; i++) { let p = document.createElement('p'); foo(i).then(content => { p.innerText = content; app.appendChild(p); }) } // others.js function foo(idx) { return new Promise(function (resolve, reject) { resolve(`the ${idx + 1}th row`); }) } module.exports = foo;
打包後發現 Promise 相關帶並無進行處理。原來上面的配置只能轉換ES的新語法,對於新的API(Promise、Set、Map 等新增對象,Object.assign、Object.entries等靜態方法。)卻沒有做用。
有兩種方式解決這個問題,babel-polyfill 或 babel-runtime,前者默認所有加載,後者是按需加載。這麼說好像有錯....能夠閱讀 https://juejin.im/post/5a96859a6fb9a063523e2591
安裝:
npm install babel-plugin-transform-runtime -D
而後修改 babel 配置
{ test: /\.js$/, // 正則表達式 匹配文件名 exclude: /node_modules/, // exclude 表示排除的路徑 也能夠添加 include 字段設置匹配路徑 use: { loader: 'babel-loader', // 對符合上面約束條件的文件 使用的 loader options: { presets: ['env'], plugins: ['transform-runtime'] } } }
如今再打包試一下會發現 bundle.js 文件的體積大了一些 那是多了 Promise 的 polyfill,打開 bundle.js 能看到相關代碼。
上面是 loader 的使用,再試一下 plugin 的使用。
html-webpack-plugin 能夠生成一個 html 文件,把生成的 js 文件自動注入其中。
安裝
npm install html-webpack-plugin -D
在配置文件中添加 plugins 字段
var path = require('path'); var HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { // ... plugins: [ new HtmlWebpackPlugin({ filename: 'index.html', // 生成文件名 template: 'index.html' // 模板 }) ] }
如今能夠把 /index.js 中引入 js 的語句刪除了,而後從新打包。
能夠看到dist文件夾生成了一個 index.html 文件,該文件中引入了 js。
完整代碼見: https://github.com/G-lory/front-end-practice/tree/master/webpack/study-notes/step2
react 使用的是 jsx 語法,須要用 babel 將 jsx 轉換成 js。
首先安裝 React
npm install react react-dom --save
而後安裝 babel 轉換 react 文件的插件
npm i babel-preset-react -D
在 src 文件夾的文件改成下面幾個文件:
index.jsx
// index.jsx import React from 'react'; import { render } from 'react-dom'; import Input from './input'; import List from './list'; class App extends React.Component { constructor(props) { super(props); this.state = { list: [] }; } addItem(item) { this.setState({ list: this.state.list.concat(item) }) } removeItem(idx) { this.setState({ list: this.state.list.filter((it, id) => id !== idx) }) } render() { return( <div className='todoList'> <Input handleSubmit={this.addItem.bind(this)} /> <List list={this.state.list} handleRemove={this.removeItem.bind(this)} /> </div> ) } } render(<App />, document.getElementById('app'));
input.jsx
// input.jsx import React, { Component } from 'react'; class Input extends Component { constructor(props) { super(props); this.state = { content: '' }; } submit() { if (this.state.content === '') return ; // 提交數據並清空 this.props.handleSubmit(this.state.content); this.setState({ content: '' }) } handleChange(e) { this.setState({ content: e.target.value }) } render() { return ( <div className='input'> <p> <textarea value={this.state.content} onChange={this.handleChange.bind(this)} > </textarea> </p> <p className='btn'> <button onClick={this.submit.bind(this)}>提交</button> </p> </div> ) } } export default Input;
list.jsx
// list.jsx import React, { Component } from 'react'; class List extends Component { render() { return ( <div> { this.props.list.map((item, idx) => <div className='listItem' key={idx}> <span>{item}</span> <button onClick={() => this.props.handleRemove(idx)}>刪除</button> </div> ) } </div> ) } } export default List;
而後修改 webpack.config.js
var path = require('path'); var HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { entry: './src/index.jsx', // 入口 output: { // 出口 path: path.resolve(__dirname, 'dist'), filename: 'bundle.js' }, module: { // 配置loader rules: [ { test: /\.jsx?/, // 正則表達式 匹配文件名 exclude: /node_modules/, // exclude 表示排除的路徑 也能夠添加 include 字段設置匹配路徑 use: { loader: 'babel-loader', // 對符合上面約束條件的文件 使用的 loader options: { presets: ['env', 'react'], plugins: ['transform-runtime'] } } } ] }, resolve: { // 代碼模塊路徑解析的配置 extensions: ['.js', '.jsx'] // 進行模塊路徑解析時,webpack 會嘗試補全後綴名來進行查找 }, plugins: [ new HtmlWebpackPlugin({ filename: 'index.html', // 生成文件名 template: 'index.html' // 模板 }) ] }
此次添加了字段 resolve.extensions 注意到 index.jsx 引用文件時沒有添加文件後綴,由於經過 resolve.extensions 的配置 Webpack 會嘗試補全指定後綴來查找。嘗試補全的順序是數組中元素的順序。
打包後打開 dist/index.html 文件,能夠看到一個雖然很醜可是能正常運行的頁面。
打開 dist/bundle.js 能夠發現文件的長度達到了 2w+ 行。那是由於咱們把 react 也打包進來了。
react 是咱們直接引入的代碼,裏面的內容不多會更改,而咱們本身寫頁面會常常變化,因此爲了充分利用頁面緩存,但願把 node_modules 中的代碼單獨打包成一個 js 文件。
修改 webpack.config.js
var path = require('path'); var HtmlWebpackPlugin = require('html-webpack-plugin'); var webpack = require('webpack'); module.exports = { ... plugins: [ new HtmlWebpackPlugin({ filename: 'index.html', // 生成文件名 template: 'index.html' // 模板 }), new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', // 使用 vendor 入口做爲公共部分 filename: "vendor.js", minChunks: (module, count) => { return module.context && module.context.includes("node_modules"); } }) ] }
如今再次打包會出現 dist 下面會出現三個文件 而 bundle.js 中只有幾百行代碼了。
咱們也能夠給文件名添加 hash 防止瀏覽器緩存。
var path = require('path'); var HtmlWebpackPlugin = require('html-webpack-plugin'); var webpack = require('webpack'); module.exports = { ... output: { // 出口 path: path.resolve(__dirname, 'dist'), filename: 'js/[name].[chunkhash].js', }, ... plugins: [ new HtmlWebpackPlugin({ filename: 'index.html', // 生成文件名 template: 'index.html' // 模板 }), new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', // 使用 vendor 入口做爲公共部分 filename: "js/[name].[chunkhash].js", minChunks: (module, count) => { return module.context && module.context.includes("node_modules"); } }) ] }
完整代碼見: https://github.com/G-lory/front-end-practice/tree/master/webpack/study-notes/step3
由於打包會花費很長時間 尤爲是文件多的時候。咱們開發時須要獲取及時反饋,而不能每次打包後觀察錯對。
使用 webpack-dev-server 能夠很簡單的啓動一個本地靜態服務。
安裝
npm i webpack-dev-server@2 -D
爲了配合 Webpack3 須要指定版本。
而後在 package.json 添加腳本命令 start
"scripts": { "start": "webpack-dev-server", "build": "webpack" },
而後運行 npm run start 會默認在 http://localhost:8080/ 啓動一個服務器 打開以後和以前打包的頁面是同樣的 嘗試修改文件 會發現頁面會實時變化。
能夠在配置文件添加 devServer 字段配置 webpack-dev-server 的選項,好比下面配置打開地址和端口號
devServer: { host: 'localhost', port: 8888, open: true // 自動打開瀏覽器 }
完整代碼見: https://github.com/G-lory/front-end-practice/tree/master/webpack/study-notes/step4
上面完成的頁面很醜,由於尚未加入樣式。沒用過less,可是sass-node那個包實在是很麻煩,仍是選擇了less,畢竟只是個demo。
不論是less仍是css,Webpack都不認識,須要加入loader來處理。
安裝
npm i less less-loader css-loader@0 style-loader -D
而後按照官網的提示,在配置文件的 rules 添加代碼
module: { // 配置loader rules: [ { test: /\.jsx?/, // 正則表達式 匹配文件名 exclude: /node_modules/, // exclude 表示排除的路徑 也能夠添加 include 字段設置匹配路徑 use: { loader: 'babel-loader', // 對符合上面約束條件的文件 使用的 loader options: { presets: ['env', 'react'], plugins: ['transform-runtime'] } } }, { test: /\.less$/, include: [ path.resolve(__dirname, 'src') ], use: [{ loader: 'style-loader' // creates style nodes from JS strings }, { loader: 'css-loader' // translates CSS into CommonJS }, { loader: 'less-loader' // compiles Less to CSS }] } ] },
而後添加樣式文件
/* index.less */ * { margin: 0; padding: 0; } .todoList { padding: 10px 50px; } /* input.less */ .input { margin-bottom: 10px; textarea { width: calc(100% - 10px); height: 50px; color: #9c9c9c; border-radius: 5px; resize: none; outline: none; padding: 5px; margin-bottom: 5px; } .btn { text-align: right; button { border: none; outline: none; background-color: transparent; } } } /* list.less */ .listItem { height: 40px; line-height: 40px; border: 1px solid #d6d6d6; display: flex; margin-bottom: 10px; padding: 10px; span { flex-grow: 1; color: #9c9c9c; } button { border: none; outline: none; background-color: transparent; } }
而後再每一個文件分別引用就能夠。
import './index.less'; /* index.jsx */ import './input.less'; /* input.jsx */ import './list.less'; /* list.jsx */
查看頁面,樣式已經生效,可是這樣的問題是,全部的樣式都是全局樣式,容易發生命名衝突的狀況,css模塊化能夠解決這個問題。
css-loader 有一個 modules 可配置項,表示是否模塊化,配置改成:
{ test: /\.less$/, include: [ path.resolve(__dirname, 'src') ], use: [{ loader: 'style-loader' // creates style nodes from JS strings }, { loader: 'css-loader', // translates CSS into CommonJS options: { modules: true, localIdentName: '[name]__[local]___[hash:base64:5]' // 生成的css類名 -> [文件名]__[類名]___[哈希] } }, { loader: 'less-loader' // compiles Less to CSS }] }
如今可使用模塊化樣式了。
如今引用類是須要醬紫
import styles from './index.less'; .... render() { return( <div className={styles.todoList}> <Input handleSubmit={this.addItem.bind(this)} /> <List list={this.state.list} handleRemove={this.removeItem.bind(this)} /> </div> ) }
而全局樣式須要醬紫:
:global(*) { margin: 0; padding: 0; }
以前處於緩存的考慮,把 node_modules 單獨打包,如今出於一樣的考慮,須要把 css 也單獨打包。
以前的 loader 是把 css 轉成了 js 代碼,而把 css 單獨打包成一個文件,須要使用 ExtractTextPlugin。
安裝:
npm install extract-text-webpack-plugin -D
修改配置文件
var path = require('path'); var HtmlWebpackPlugin = require('html-webpack-plugin'); var webpack = require('webpack'); var ExtractTextPlugin = require("extract-text-webpack-plugin"); module.exports = { // ... module: { // 配置loader rules: [ // ... { test: /\.less$/, include: [ path.resolve(__dirname, 'src') ], use: ExtractTextPlugin.extract({ fallback: 'style-loader', use: [{ loader: 'css-loader', // translates CSS into CommonJS options: { modules: true, localIdentName: '[name]__[local]___[hash:base64:5]' // 生成的css類名 -> [文件名]__[類名]___[哈希] } }, { loader: 'less-loader' // compiles Less to CSS }] }) }, ] }, // ... plugins: [ new HtmlWebpackPlugin({ filename: 'index.html', // 生成文件名 template: 'index.html' // 模板 }), new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', // 使用 vendor 入口做爲公共部分 filename: "js/[name].[chunkhash].js", minChunks: (module, count) => { return module.context && module.context.includes("node_modules"); } }), new ExtractTextPlugin('css/[name].[contenthash].css') ] }
再次打包,如今CSS文件也單獨分離出來了。
完整代碼見: https://github.com/G-lory/front-end-practice/tree/master/webpack/study-notes/step5
每一個正經的前端應該都知道雪碧圖是什麼吧,反正我不知道……我還覺得是瀑布流什麼的神奇效果……
經過 Webpack 的插件,能夠自動把引入的圖片生成雪碧圖。也能夠用 url-loader 來處理圖片,這裏沒有選擇使用。
首先 Webpack 不識別圖片類型的文件 要引入 file-loader ,同時引入 webpack-spritesmith 用來生成雪碧圖。
安裝
npm install file-loader webpack-spritesmith -D
我找了兩個圖片
delete.png 和 submit.png 放到 /images 文件夾下面
處理圖片要添加 loader
{ test: /\.(png|jpg|gif)$/, use: [ { loader: 'file-loader', options: {} } ] }
生成雪碧圖添加 plugins
var SpritesmithPlugin = require('webpack-spritesmith'); new SpritesmithPlugin({ src: { cwd: path.resolve(__dirname, 'images'), // 多個圖片所在的目錄 glob: '*.png' // 匹配圖片的路徑 }, target: { // 生成最終圖片的路徑 image: path.resolve(__dirname, 'src/spritesmith-generated/sprite.png'), // 生成所需 less 代碼 css: path.resolve(__dirname, 'src/spritesmith-generated/sprite.less'), }, apiOptions: { cssImageRef: "~sprite.png" } })
如今打包的時候會在 /src/spritesmith-generated 生成雪碧圖和所需的 less 代碼
生成的雪碧圖
嘗試使用,修改 input.less 和 list.less
input.less
/* input.less */ @import './spritesmith-generated/sprite.less'; .input { /* ignore.. */ .btn { text-align: right; button { .sprite(@submit); border: none; outline: none; background-color: transparent; } } }
list.less
@import './spritesmith-generated/sprite.less'; .listItem { /* ignore.. */ button { .sprite(@delete); border: none; outline: none; background-color: transparent; } }
而後啓動項目會發現報錯了……
雖然也沒看明白什麼意思吧……反正就是在 less-loader 添加配置項 javascriptEnabled: true
而後打包發現路徑不對查了下發現須要給 ExtractTextPlugin 配置 publicPath
最後該規則改成
{ test: /\.less$/, include: [ path.resolve(__dirname, 'src'), ], use: ExtractTextPlugin.extract({ fallback: 'style-loader', use: [{ loader: 'css-loader', // translates CSS into CommonJS options: { modules: true, localIdentName: '[name]__[local]___[hash:base64:5]' // 生成的css類名 -> [文件名]__[類名]___[哈希] } }, { loader: 'less-loader', // compiles Less to CSS options: { javascriptEnabled: true } }], publicPath: "../" }) },
可是仍是有報錯
這個我是真的不知道怎麼解決,只是發現去掉 css module 就沒有這個問題了,因而我刪掉了 css module 部分……(配合標題 我TM是真的菜
而後就能夠正常打包了。一個 To Do List 就勉強作好了……
完整代碼見: https://github.com/G-lory/front-end-practice/tree/master/webpack/study-notes/step6
我仍是老老實實的用 create-react-app 和 vue-cli 吧……
每個用到的 loader 和 plugin 的 GitHub 都會參考到 就不寫了。