目標
- 將es6語法,jsx語法,自動轉換成目前主流瀏覽器可以支持的js語法。
- 將less或sass等轉換成css,並自動添加瀏覽器私有前綴。
- 實現代碼熱替換和瀏覽器的自動刷新。
- 對針對開發環境和生產環境生成不同的打包文件。
准備工作
- 安裝nodejs
- 創建一個文件夾(/test)作為本次項目的根目錄,並運行npm init
- npm install webpack -g
- npm install react react-dom --save
- npm install webpack-dev-server babel-core babel-loader babel-preset-es2015 babel-preset-stage-2 babel-preset-react css-loader style-loader stylus-loader extract-text-webpack-plugin --save-dev
- 其它npm包根據需要自行補上
搭建項目骨架
新建一個index.html文件為作本次項目的承載頁面,內容大致如下:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" /> <link rel="stylesheet" type="text/css" href="build/app.css"> <title>react</title> </head> <body> <div id="app"></div> <script type="text/javascript" src="build/app.js"></script> </body> </html>
新建build目錄,用來存放打包之后的樣式和js文件,新建一個app目錄用來存放源代碼,作為一個簡單的示例,做這些就夠了,接下來在app目錄下新建main.jsx和main.styl兩個文件,main.jsx們將被作為整個項目的入口。這個文件的業務邏輯不重要,我們這次不是要真的制作一個吊炸天的項目,只是為了演示如何利用webpack及一些周邊插件實現前端開發的自動化構建。當然,為了寫的點的趣味性,我引入一下react.js來助助興。下面是它的源碼:
'use strict'; import React from "react"; import AboutUs from "./about.jsx"; import ReactDOM from "react-dom"; function bootstrap(){ var initialState = window.list; ReactDOM.render(<AboutUs initialState={initialState} />,document.getElementById('app')); } if(typeof window.addEventListener){ window.addEventListener("DOMContentLoaded",bootstrap); }else{ window.attachEvent('onload',bootstrap); }
react的亮點之一就是它的組件化開發,然而我也不打算在這里免費幫它做宣傳。我在這里創建了一個叫作about.jsx的組件,主要是為了使得本次演示能盡可能的豐滿一點。順便貼一下about.jsx的源碼:
'use strict' import React,{Component} from "react"; class AboutUs extends Component{ constructor(props){ super(props); this.state = { maskActive:false, pageIndex:1 } this.handleClick = this.handleClick.bind(this); } handleClick(){ var pageIndex = this.state.pageIndex+1; this.setState({ pageIndex, maskActive:true }); } memuList(){ let list = this.props.initialState||[]; return list.map((item,i)=>{ return (<li key={'i-'+i} onClick={this.handleClick}>{item.name}</li>) }); } render(){ const {pageIndex,maskActive} = this.state; let maxlength = Math.min(pageIndex * 10,window.innerWidth); let proces = {width:(maxlength) + 'px','textIndent':maxlength+'px'}; return ( <div className="aboutus-content"> <h3> <span className="title">關於我們</span> </h3> <ul> {this.memuList()} </ul> <div className="process"> <div style={proces}>{maxlength}</div> </div> <footer> copyright@2014-2016 湖南長沙互聯網家 </footer> </div> ) } } export default AboutUs;
請忽視里邊的邏輯,我承認寫的確實有點無厘頭。為了讓頁面不至於太倉白,來一個樣式潤下色,所以main.styl就應聲出場了,源碼如下:
html { height: 100%; } body { font-size: 14px; -webkit-user-select:none; } ul { list-style-type:none; display: flex; margin: 0; padding: 0; } li { line-height: 1.2rem; padding: 1rem 2rem; background-color: #884809; border-right: 1px solid wheat; color: white; } footer { display: flex; height: 40px; color: black; line-height: 40px; } .process { height: 40px; width: 100%; line-height: 40px; border: 1px solid gray; } .process div { max-width: 99%; background-color: green; height: 100%; }
嗯,也沒有什么出奇的地方,甚至連sass的語法都沒有,唯一個免強能拿的出手就是這個flex,在將man.styl轉成app.css之后,會自動補上瀏覽器的私有前綴。當然,如果你要在此放一個less/sass的彩蛋,我不反對。為了緊扣主題,下面我的重點工作要開始了
。在test/目錄下新建一個webpack.config.js的文件,我寫的內容是這樣的:
var path = require('path'); var webpack = require('webpack'); var nodeModulesPath = path.resolve(__dirname, 'node_modules'); var ExtractTextPlugin = require("extract-text-webpack-plugin"); var entry = require('./config.js'); module.exports = { entry: entry, resolve: { extentions: ["", "js", "jsx"] }, module: { loaders: [{ test: /\.(es6|jsx)$/, exclude: nodeModulesPath, loader: 'babel-loader', query: { presets: ['react', 'es2015','stage-2'] } }, { test: /\.styl/, exclude: [nodeModulesPath], loader: ExtractTextPlugin.extract('style', 'css!autoprefixer!stylus') }] }, output: { path: path.resolve(__dirname, './build'), publicPath:'/build/', filename: './[name].js', }, plugins: [ new webpack.NoErrorsPlugin(), new ExtractTextPlugin("./[name].css") ] };
為了在生產環境和開發環境復用代碼,我獨立出一個叫作config.js的文件,內容如下:
module.exports ={
app:['./app/main.jsx','./app/main.styl']
}
再接下來是時候修改一下package.json文件了
{ "name": "s-react", "version": "1.0.0", "description": "React is a JavaScript library for building user interfaces.", "main": "index.js", "directories": { "example": "how to use react with webpack" }, "scripts": {"dev": "webpack-dev-server --devtool eval --inline --hot --port 3000","build": "webpack --progress --colors --display-error-details", "test": "echo \"Error: no test specified\" && exit 1" }, "author": "278500368@qq.com", "repository": "https://github.com/bjtqti/study", "license": "MIT", "dependencies": { "react": "^15.3.2", "react-dom": "^15.3.2" }, "devDependencies": { "autoprefixer-loader": "^3.2.0", "babel-core": "^6.17.0", "babel-loader": "^6.2.5", "babel-preset-es2015": "^6.16.0", "babel-preset-react": "^6.16.0", "babel-preset-stage-2": "^6.17.0", "clean-webpack-plugin": "^0.1.13", "css-loader": "^0.25.0", "extract-text-webpack-plugin": "^1.0.1", "style-loader": "^0.13.1", "stylus": "^0.54.5", "stylus-loader": "^2.3.1", "webpack": "^1.13.2", "webpack-dev-server": "^1.16.2" } }
重點關注一scripts里邊的內容,dev的作用是生成一個web開發服務器,通過localhost:3000就可以立即看到頁面效果,關於webpack-dev-server 的使用,網上介紹的很多,我這里着重要強調的就是--hot --inline 的使用,它使得我們以最簡單的方式實現了瀏覽器的自動刷新和代碼的熱替換功能。 當然,還有一種叫作iframe的模式,不過訪問地址要作修改,比如http://localhost:3000/webpack-dev-server/index.html
. 我個人不太喜歡,於是采用了inline的方式。
除了使用CLI的方式之外,還有一種方式,在網上也介紹的很多,不過因為相比CLI方式來說,要繁鎖的多,所以也更容易讓初學者遇到問題,但是它的可配置性更高,針對一些個性化的需求,它可能更容易達成你想要的效果。所以有必要順帶介紹一下這種方式.
首頁在test目錄下新建一個server.js的文件(名字可以隨意)
var config = require("./webpack.config.js"); var webpack = require("webpack"); var webpackDevServer = require('webpack-dev-server'); var compiler = webpack(config); var server = new webpackDevServer(compiler, { hot: true, inline: true, // noInfo: true, publicPath: '/build/', watchOptions: { aggregateTimeout: 300, poll: 1000 }, // historyApiFallback: true }); server.listen(3000, "localhost", function(err, res) { if (err) { console.log(err); } console.log('hmr-server Listening at http://%s:%d','localhost', 3000); });
由於我們不打算用CLI方式,所以--hot -- inline這個參數就移到了這個配置里邊來了,用這種方式比較煩人的地方就是webpack.config.js的entry要做很大的改動,比如
entry: {
app:["webpack-dev-server/client?http://localhost:3000/", "webpack/hot/dev-server",'./app/main.jsx','./asset/main.styl']
}
如果app有多項,那么勢必要寫一個循環來添加,再如果我們改了一下webpack-dev-server的端口,這里邊也要修改,除非把端口號作為一個變量進行拼接。正當你滿懷信心,准備見證奇跡的時候,等來的確是奇怪,瀏覽器的控制台怎么報錯了。原因在於webpack.config.js中的plugs中要加上new webpack.HotModuleReplacementPlugin():
var path = require('path'); var webpack = require('webpack'); var nodeModulesPath = path.resolve(__dirname, 'node_modules'); var ExtractTextPlugin = require("extract-text-webpack-plugin"); var entry = require('./config.js'); entry.app.unshift("webpack-dev-server/client?http://localhost:3000/", "webpack/hot/dev-server"); module.exports = { entry: entry, resolve: { extentions: ["", "js", "jsx"] }, module: { loaders: [{ test: /\.(es6|jsx)$/, exclude: nodeModulesPath, loader: 'babel-loader', query: { presets: ['react', 'es2015','stage-2'] } }, { test: /\.styl/, exclude: [nodeModulesPath], loader: ExtractTextPlugin.extract('style', 'css!autoprefixer!stylus') }] }, output: { path: path.resolve(__dirname, './build'), publicPath:'/build/', filename: './[name].js', }, plugins: [ new webpack.NoErrorsPlugin(), new webpack.HotModuleReplacementPlugin(), new ExtractTextPlugin("./[name].css") ] };
然后我們運行npm run server 實現了和CLI 方式一樣的效果。改一改main.styl,頁面樣式也同步更新,沒有手動刷新造成的白屏現象,更新main.jsx也是同樣的自動更新了,就感覺和ajax的效果一樣。從此改一下代碼按一下F5的時代結束了。
最后就是要打包出生產環境所需要的js和css代碼,這個相對就簡單了許多,只要把webpack.config.js另存為webpack.develop.config.js(名字隨意),然后進去改改就好了:
var path = require('path'); var webpack = require('webpack'); var nodeModulesPath = path.resolve(__dirname, 'node_modules'); var ExtractTextPlugin = require("extract-text-webpack-plugin"); var CleanPlugin = require('clean-webpack-plugin'); var entry = require('./config.js'); module.exports = { entry: entry, resolve:{ extentions:["","js"] }, module: { loaders: [{ test: /\.jsx?$/, exclude: nodeModulesPath, loader: 'babel-loader', query: { presets: ['react','es2015'] } },{ test: /\.styl/, exclude: [nodeModulesPath], loader: ExtractTextPlugin.extract('style', 'css!autoprefixer!stylus') }] }, output: { path: path.resolve(__dirname, './dest'), filename: '[name]-[hash:8].min.js', }, plugins: [ new CleanPlugin('builds'), new ExtractTextPlugin("./[name]-[hash:8].css"), new webpack.DefinePlugin({ 'process.env': {NODE_ENV: JSON.stringify('production')} }), new webpack.optimize.DedupePlugin(), new webpack.optimize.OccurenceOrderPlugin(true), new webpack.optimize.UglifyJsPlugin({ compress: { warnings: false }, output: { comments: false }, sourceMap: false }) ] };
更多的是plugins里邊,多了一些優化的插件,比如合並,壓縮,加上hash值為作版本號,上面的配置中我用[hash:8]截取前8位作為版本號。需要重點提一下就是 extract-text-webpack-plugin 這個插件的使用,它可以使css文件打包成獨立的文件,而不是作為js的一部分混在app.js里邊,它的使用需要注意兩個地方:1是在loader中的寫法loader: ExtractTextPlugin.extract('style', 'css!autoprefixer!stylus')
然后就是在plugins中也要加上,如果是動態name的,要寫成[name]
new ExtractTextPlugin("./[name]-[hash:8].css"), 這個和output中的寫法是對應的。然后我們在package.json的scripts中,增加一個"release": "webpack --config webpack.develop.config.js --display-error-details"
保存,運行npm run release --production就可以看到打包之后的文件了,為了區別開發環境的打包,我這里指定dest目錄下為生產生境下的打包輸出。
最后預覽一下成果:
斷點調試
由於使用了webpack-dev-server開發,代碼是保存在內存中,在瀏覽器的控制面版的source中,只有經過webpack生成之后的js代碼,比如像下面這樣的情況:
/******/ (function(modules) { // webpackBootstrap /******/ var parentHotUpdateCallback = this["webpackHotUpdate"]; /******/ this["webpackHotUpdate"] = /******/ function webpackHotUpdateCallback(chunkId, moreModules) { // eslint-disable-line no-unused-vars /******/ hotAddUpdateChunk(chunkId, moreModules); /******/ if(parentHotUpdateCallback) parentHotUpdateCallback(chunkId, moreModules); /******/ } /******/ /******/ function hotDownloadUpdateChunk(chunkId) { // eslint-disable-line no-unused-vars /******/ var head = document.getElementsByTagName("head")[0]; /******/ var script = document.createElement("script"); /******/ script.type = "text/javascript"; /******/ script.charset = "utf-8"; /******/ script.src = __webpack_require__.p + "" + chunkId + "." + hotCurrentHash + ".hot-update.js"; /******/ head.appendChild(script); /******/ } /******/ /******/ function hotDownloadManifest(callback) { // eslint-disable-line no-unused-vars /******/ if(typeof XMLHttpRequest === "undefined") /******/ return callback(new Error("No browser support")); ......
而手寫的代碼是這樣的
'use strict' import React,{Component} from "react"; class AboutUs extends Component{ constructor(props){ super(props); this.state = { maskActive:false, pageIndex:1 } this.handleClick = this.handleClick.bind(this); } handleClick(){ var pageIndex = this.state.pageIndex+1; this.setState({ pageIndex, maskActive:true }); } memuList(){ let list = this.props.initialState||[]; return list.map((item,i)=>{ return (<li key={'i-'+i} onClick={this.handleClick}>{item.name}</li>) }); } render(){
這時就需要用到devtool這個配置項,有兩種開啟方式,對應於CLI方式,只要在webpack-dev-server 后加上 --devtool source-map 就可以了。對應 webpack.config.js中的方式,則是增加devtool :"source-map" 這一項。兩種試,任選其一即可。
完成這一步之后,重新啟動webpack-dev-server,然后在瀏覽器中,打開控制台,這時,在source選項卡中,會多出一個webpack://的內容,然后找到要斷點執行的代碼,就可以執行斷點調試了。截圖如下:
關於devtool的選項,官網還有其它幾個值,我只用到source-map就滿足需求,其它項未做實踐。
小結
1. 為了方便css文件的統一管理,我把它們統統放在entry中,而網上大都是介紹在js中用require('xxx.css') 的方式,我覺得單獨在entry中引入更加清晰。
2. 為了重點演示自動化構建,關鍵點有兩個地方,1是將代碼進行轉化(es6,jsx,less),打包輸出,2是代碼的熱替換和自動刷新 ,如果有node的部分,還要做node進程的自動重啟。
3. 如果是簡單的需求,使用CLI方式比較簡單省事。
4. 自動化構建看起來很簡單,要系統的掌握並應用到實際開發中,還需要多加實踐。
最后附上本例的所有源碼,方便有需要的同學下載測試,也歡迎提出指導意見