这篇文章将介绍如何利用 webpack 进行单页面应用的开发,算是我在实际开发中的一些心得和体会,在这里给你们作一个分享。webpack 的介绍这里就很少说了,能够直接去官网查看。 关于这个单页面应用你们能够直接去个人github上查看https://github.com/huangshuwei/webpackForSPA,我将结合这个项目去介绍。若是你们以为这篇文章有不妥的地方,还请指出。css
这篇文章的目的是解决咱们在开发中会遇到的问题,不是一篇基础教程,还请谅解。html
我将根据这个目录结构进行讲解java
dist:发布的文件目录,即webpack编译输出的目录node
libs:放置公共的文件,如js、css、img、font等react
mockServer:模拟后端服务,即用webpack开发时模拟调用的后端服务(用nodejs服务模拟)jquery
node_modules:项目依赖的包webpack
src:资源文件,里面包含css、font、html、img、jsgit
package.json:项目配置github
webpack.config.js:webpack的配置文件web
建议先运行一下这个项目,有一个大体的了解,再往下阅读。使用说明:
首先克隆一份到你的本地 $ git clone https://github.com/huangshuwei/webpackForSPA.git 而后 cd 到 ‘webpackForSPA’目录下 $ cd webpackForSPA 接着你能够运行不一样的命令查看结果 发布模式: $ npm run build 开发模式: $ npm run dev 热更新模式 $ npm run dev-hrm 若是使用了热更新模式,而且想要结合后端服务形式运行,那么cd 到‘mockServer’目录下,并执行node 服务: $ cd mockServer $ node server.js
通常开发时和发布时是不一样的,好比开发时文件的访问目录包含‘dist’目录,可是发布上线时,通常会把‘dist’文件夹去掉。
固然还有其余的一些细节不一样。
开发模式:
能看到webpack编译输出的文件
js、css、html文件不须要压缩
能够正确的运行编译输出后的文件
这种模式通常只是用来看webpack编译输出后的文件是否正确
热更新模式:
看不到webpack编译输出的文件
js、css、html文件不须要压缩
更改完文件后无需从新编译并自动刷新浏览器
能够结合后端服务开发,避过浏览器同源策略,如结合java、.net服务等
发布模式:
能看到webpack编译输出的文件
js、css、html文件压缩
文件的层级目录不须要包含‘dist’目录
我区分开发、热更新、发布模式是经过配置‘package.json’文件的运行命令,有些人是经过建立多个不一样的webpack的配置文件来达到想要的效果。
像这个项目就是使用了多个webpack的配置文件。
这是在 package.json 文件中配置的
// package.json 文件 ... "scripts": { "build": "webpack --profile --progress --colors --display-error-details", "dev": "webpack --display-modules --profile --progress --colors --display-error-details", "dev-hrm": "webpack-dev-server --config" }, ...
color 输出结果带彩色,好比:会用红色显示耗时较长的步骤
profile 输出性能数据,能够看到每一步的耗时
progress 输出当前编译的进度,以百分比的形式呈现
display-modules 默认状况下 node_modules 下的模块会被隐藏,加上这个参数能够显示这些被隐藏的模块
display-error-details 输出详细的错误信息
webpack-dev-server 将会开启热更新
更多请参考官网 cli
配置好了package.json文件,咱们就能够这样运行
// 开发模式 npm run dev // 热更新模式 npm run dev-hrm // 发布模式 npm run build
配置完了命令,当咱们运行不一样的命令时,咱们能够经过‘process.env.npm_lifecycle_event’去获取当前运行的命令,根据不一样的命令,咱们能够按照本身的须要作相应的处理。好比开发模式时,容许开启调试,静态资源不要压缩;发布模式时,不容许调试,静态资源要压缩。具体以下:
// webpack.config.js // 获取当前运行的模式 var currentTarget = process.env.npm_lifecycle_event; var debug, // 是不是调试 devServer, // 是不是热更新模式 minimize; // 是否须要压缩 if (currentTarget == "build") { // 发布模式 debug = false, devServer = false, minimize = true; } else if (currentTarget == "dev") { // 开发模式 debug = true, devServer = false, minimize = false; } else if (currentTarget == "dev-hrm") { // 热更新模式 debug = true, devServer = true, minimize = false; }
为了方便咱们频繁使用路径,以下配置
// webpack.config.js var PATHS = { // 发布目录 publicPath: debug ? '/webpackForSPA/dist/' : '/webpackForSPA/', // 公共资源目录 libsPath: path.resolve(process.cwd(), './libs'), // src 资源目录 srcPath: path.resolve(process.cwd(), 'src'), }
webpack的别名的目的就是简化咱们的操做,引用资源时直接使用别名便可(和 seajs 里的别名用法同样)。配置以下:
// webpack.config.js ... resolve:{ alias: { // js jquery: path.join(PATHS.libsPath, "js/jquery/jquery"), underscore: path.join(PATHS.libsPath, "js/underscore/underscore.js"), // css bootstrapcss: path.join(PATHS.libsPath, "css/bootstrap/bootstrap-3.3.5.css"), indexcss: path.join(PATHS.srcPath, "css/index.css"), } } ...
// webpack.config.js ... entry:{ // 入口 js index: './src/js/index.js', // 公共js包含的文件 common: [ path.join(PATHS.libsPath, "js/jquery/jquery.js"), path.join(PATHS.libsPath, "js/underscore/underscore.js") ], } ...
// webpack.config.js ... output:{ // 输出目录 path: path.join(__dirname, 'dist'), // 发布后,资源的引用目录 publicPath: PATHS.publicPath, // 文件名称 filename: 'js/[name].js', // 按需加载模块时输出的文件名称 chunkFilename: 'js/[name].js' } ...
当咱们在js文件中经过require('')引用js时,webpack 默认会将css文件与当前js文件打包一块儿,可是这种方式会阻塞页面的加载,由于css的执行要等待js文件加载进来。因此咱们会把css从js文件中提取出来,放到一个单独的css文件中。这时咱们要使用webpack的插件:extract-text-webpack-plugin,配置以下:
引入插件
// webpack.config.js var ExtractTextPlugin = require("extract-text-webpack-plugin");
配置 loader
// webpack.config.js ... loaders: [ { test: /\.css$/, loader: ExtractTextPlugin.extract("style-loader", "css-loader!postcss-loader") }, ... ] ...
配置 plugins
// webpack.config.js ... plugins:[ new ExtractTextPlugin("css/[name].css", {allChunks: true}), ... ] ...
项目中,咱们一般会有公共的js,好比 jquery、bootstrap、underscore 等,那么这时候咱们须要将这些公共的js单独打包。这时咱们须要用webpack自带的插件:
// webpack.config.js ... plugins:[ // 会把 ‘entry’ 定义的 common 对应的两个js 打包为 ‘common.js’ new webpack.optimize.CommonsChunkPlugin("common", 'js/[name].js', Infinity), ] ...
项目上线后,资源的版本号十分重要。资源没有版本号,即便从新发布,客户端浏览器可能会把老的资源缓存下来,致使没法下载最新的资源。webpack 支持给资源添加版本号,不只仅是js、css,甚至font、img均可以添加版本号。咱们能够经过webpack中的‘chunkhash’来解决。
首先要了解下webpack 中 [hash]、[chunkhash]、[chunkhash:8]的区别。
[hash]:webpack编译会产生一个hash值
[chunkhash]:每一个模块的hash值
[chunkhash:8]:取[chunkhash]的前8位
推荐发布模式使用版本号,其余模式无需使用,热更新模式不支持‘chunkhash’,可是支持‘hash’
资源加版本号,那么咱们的输出的部分都要作改动,而且要区分当前的命令模式,以下:
// webpack.config.js ... output:{ // 输出目录 path: path.join(__dirname, 'dist'), // 发布后,资源的引用目录 publicPath: PATHS.publicPath, // 文件名称 filename: devServer ? 'js/[name].js' : 'js/[name]-[chunkhash:8].js', // 按需加载模块时输出的文件名称 chunkFilename: devServer ? 'js/[name].js' : 'js/[name]-[chunkhash:8].js' } ...
输出公共js的地方也要改动:
// webpack.config.js ... plugins:[ // 会把 ‘entry’ 定义的 common 对应的两个js 打包为 ‘common.js’ new webpack.optimize.CommonsChunkPlugin("common", "" + (devServer ? 'js/[name].js' : "js/[name]-[chunkhash:8].js"), Infinity), ] ...
有个版本号后,咱们考虑如何经过html引用这些含有版本号的js、css、font、img。webpack每次编译后的资源 chunkhash 会随着内容的变化而变化,因此咱们不可能每次都手动的更改html这些资源的引用路径。这时咱们要用到webpack的插件:html-webpack-plugin。这个插件的目的是生成html,也能够根据模板生成html,固然还有其余的功能,具体看插件介绍。下面是的配置:
引入插件
// webpack.config.js var HtmlWebpackPlugin = require('html-webpack-plugin');
配置 plugins,生成须要的html
// webpack.config.js ... plugins:[ new HtmlWebpackPlugin({ filename: 'index.html', template: __dirname + '/src/index.html', inject: 'true' }), new HtmlWebpackPlugin({ filename: 'html/hrm.html', template: __dirname + '/src/html/hrm.html', inject: false, }), new HtmlWebpackPlugin({ filename: 'html/home.html', template: __dirname + '/src/html/home.html', inject: false, }), ] ...
咱们前面说过,webpack 默认只识别 js 文件,因此对于html也要使用对应的loader:
// webpack.config.js ... loaders:[ {test: /\.html$/,loader: "html"}, ] ...
引用图片和字体,须要对应的loader,而且能够设置这些资源大小的临界值,当小于临界值的时候,字体或者图片文件会以base64的形式在html引用,不然则是以资源路径的形式引用。以下:
// webpack.config.js // 图片 loader { test: /\.(png|gif|jpe?g)$/, loader: 'url-loader', query: { /* * limit=10000 : 10kb * 图片大小小于10kb 采用内联的形式,不然输出图片 * */ limit: 10000, name: '/img/[name]-[hash:8].[ext]' } }, // 字体loader { test: /\.(eot|woff|woff2|ttf|svg)$/, loader: 'url-loader', query: { limit: 5000, name: '/font/[name]-[hash:8].[ext]' } },
js、css、html的压缩是少不了的,webpack 自带了压缩插件,若是某些对象名称不想被压缩,能够排除不想要压缩的对象名称。配置以下:
// webpack.config.js ... plugins:[ new webpack.optimize.UglifyJsPlugin({ mangle: { // 排除不想要压缩的对象名称 except: ['$super', '$', 'exports', 'require', 'module', '_'] }, compress: { warnings: false }, output: { comments: false, } }) ] ...
经过webpack编译输出后的项目中,虽然页面已经引用了jquery、underscore,可是仍是没法直接使用‘$’、‘_’对象,咱们能够这样:
var $ = require('jquery'); var _ = require('underscore');
可是这样实在不方便,若是咱们就是要使用‘$’、‘_’对象直接操做,webpack 内置的插件能够帮咱们解决。具体以下:
// webpack.config.js new webpack.ProvidePlugin({ $: "jquery", jQuery: "jquery", "window.jQuery": "jquery", "_": "underscore", }),
在单页面应用中,当咱们加载其余的模板文件时,想要引用这个模板文件对应的js。若是咱们经过这种方式require(),那么webpack会将这个模板文件对应的js也会和当前js打包成一个js。若是项目比较大,那么js文件也将愈来愈大。咱们但愿的是加载模板文件的时候动态的引用这个模板文件对应的js。那么咱们能够经过 require.ensure()的方式。
好比如今有两个导航菜单:
<ul> <li><a href="#home">home</a></li> <li><a href="#hrm">HRM</a></li> </ul>
咱们给这两个菜单绑定点击事件,当点击‘home’时引用对应的‘home.js’;当点击‘HRM’时引用对应的‘hrm.js’,那么大体能够这样:
function loadJs(jsPath) { var currentMod; if (jsPath === './home') { require.ensure([], function (require) { currentMod = require('./home'); }, 'home'); } else if (jsPath === './hrm') { require.ensure([], function (require) { currentMod = require('./hrm'); }, 'hrm'); } }
有时咱们只有在开发过程当中,才想输出log日志。能够用如下webpack内置的插件解决:
// webpack.config.js ... plugins:[ new webpack.DefinePlugin({ // 全局debug标识 __DEV__: debug, }), ] ...
这时代码中就能够这么写了:
if (__DEV__) { console.log('debug 模式'); }
发布前清空发布目录是有必要的,咱们能够经过‘clean-webpack-plugin’插件解决:
引入插件:
// webpack.config.js var CleanWebpackPlugin = require('clean-webpack-plugin');
配置plugins:
// webpack.config.js ... plugins:[ new CleanWebpackPlugin(['dist'], { root: '', // An absolute path for the root of webpack.config.js verbose: true,// Write logs to console. dry: false // Do not delete anything, good for testing. }), ] ...
热更新能够在你代码改变的时候即时编译输出,不用每次都要从都从新编译一遍,而且除了第一次编译比较慢,后面的编译都是增量编译,速度很快。有了这个功能,咱们就不须要,每次都从头编译一次了。配置以下:
// webpack.config.js ... plugins: [ // Enable multi-pass compilation for enhanced performance // in larger projects. Good default. new webpack.HotModuleReplacementPlugin({ multiStep: true }), ], devServer: { // Enable history API fallback so HTML5 History API based // routing works. This is a good default that will come // in handy in more complicated setups. historyApiFallback: true, // Unlike the cli flag, this doesn't set // HotModuleReplacementPlugin! hot: true, inline: true, // Display only errors to reduce the amount of output. stats: 'errors-only', host: "localhost", // Defaults to `localhost` process.env.HOST port: "8080", // Defaults to 8080 process.env.PORT } ...
这时咱们只要打开浏览器,输入:localhost:8080/ 就能看到结果,而且在你修改某些源文件后,浏览器会自动刷新,就能看到webpack 即时编译输出的结果,而不须要从新编译。
咱们在使用webpack开发时不免要结合后端服务开发,好比咱们用webstorm 编译器开发项目,须要调用java的服务,因为有同源策略问题,这时咱们会收到相关报错信息。这时咱们能够经过代理的方式绕过同源策略。
这里我用nodejs 模拟一个后端服务,以下:
// ~/mockServer/server.js var http = require('http'); var content = '▍if you see that,It means you have get the correct data by backend server(mock data by nodejs server)!'; var srv = http.createServer(function (req, res) { res.writeHead(200, {'Content-Type': 'application/text'}); res.end(content); }); srv.listen(8888, function() { console.log('listening on localhost:8888'); });
接下来咱们须要这样配置去调用这个nodejs 的服务。
首先将热更新配置的代码修改成:
// webpack.config.js ... plugins: [ // Enable multi-pass compilation for enhanced performance // in larger projects. Good default. new webpack.HotModuleReplacementPlugin({ multiStep: true }), ], devServer: { // Enable history API fallback so HTML5 History API based // routing works. This is a good default that will come // in handy in more complicated setups. historyApiFallback: true, // Unlike the cli flag, this doesn't set // HotModuleReplacementPlugin! hot: true, inline: true, // Display only errors to reduce the amount of output. stats: 'errors-only', host: "localhost", // Defaults to `localhost` process.env.HOST port: "8080", // Defaults to 8080 process.env.PORT proxy: { '/devApi/*': { target: 'http://localhost:8888/', secure: true, /* * rewrite 的方式扩展性更强,不限制服务的名称 * */ rewrite: function (req) { req.url = req.url.replace(/^\/devApi/, ''); } } } } ...
而后配置一个全局的环境变量,经过DefinePlugin
:
// webpack.config.js ... plugins: [ new webpack.DefinePlugin({ __DEVAPI__: devServer ? "/devApi/" : "''", }), ] ...
最后在调用服务的地方,只须要在调用地址前添加 __DEVAPI__
全局环境变量便可,如:
$.ajax({ url: __DEVAPI__ + 'http://localhost:8888/', data: {}, type: 'get', dataType: 'text', success: function (text) {} })
这样在热更新的模式下,当有__DEVAPI__
的地方就会自动识别为/devApi/
,而这里会经过代理处理帮你重写掉,绕过同源策略。
虽然以上的工做几乎已经知足咱们对webpack的要求了,可是咱们还想懒一点,想在热更新模式下,编译完成后自动打开浏览器。那么咱们能够经过这个插件open-browser-webpack-plugin解决:
引用插件
// webpack.config.js var OpenBrowserPlugin = require('open-browser-webpack-plugin');
配置插件,这个配置要根据项目的具体状况去配置:
// webpack.config.js ... plugins: [ new OpenBrowserPlugin({url: 'http://localhost:8080' + PATHS.publicPath + 'index.html'}) ] ...
以上就是这篇文章的主要内容,但愿经过这篇文章可以给你们带来一些启发。若是有以为哪里不对,或者不合理的地方,欢迎指出。其实webpack还有一个关于版本号的bug,不知道是否是有人解决了,若是有人已经解决了,还请分享。