前端自动化构建学习笔记
目标
- 将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. 自动化构建看起来很简单,要系统的掌握并应用到实际开发中,还需要多加实践。
最后附上本例的所有源码,方便有需要的同学下载测试,也欢迎提出指导意见