Webpack 2 打包解决方案

本篇将介绍 Webpack 2 的相关配置,以及使用 Webpack 2 进行前端开发的解决方案,或前端开发脚手架

Webpack

Webpack 是一个前端资源加载/打包工具, 只需要相对简单的配置,就可以整合出一套前端开发解决方案
Webpack 一个主要的特性是可以实现前端开发的模块化, 它能分析整个前端工程的项目结构, 找到相关的 JavaScrit 模块以及他的一些样式文件,图片文件等, 并将他们整合成一个或多个文件输出,提供给浏览器执行。
Webpack 通过相应的配置可以实现 JS 代码的降级(ES6 to ES5), 方言的转译(TypeScript to Js, Less to CSS)等
注: 模块化是指: 把前端程序开发像后端开发一样按功能切割成多个小文件(封装成接口), 有利于实现前端的工程化开发,例如 WebApp(SPA)开发
注意: 采用 webpack 打包之后, IE8 的兼容有很严重的问题
Webpack 2.x 介绍&文档
Webpack 2.x 入门博客

功能介绍

本文介绍的 Webpack 脚手架实现如下功能:

  • js 文件整合打包, 输出 bundle.js(webpack 输出结果文件称为 bundle 文件), babel转码(ES6 to SE5)
  • css/less 文件整合打包, 可以打入 bundle.js 文件中, 也可以提取为 bundle.css 文件
  • 图片打包, 如果图片大小小于 0.5kb, 直接以 Base64 格式加入 bundle.js, 如果大于 0.5kb 直接输出到 bundle 文件所在目录的 image/ 路径下
  • 测试环境打包: 在 chrome 下可 debug。
  • 生产环境打包: 代码压缩丑化, 自动添加 bundle 文件的版本, 自动更新 html 中对应的 bundle 文件路径
  • 提供 webpack-dev-server 服务, 通过 localhost:3000 访问。

工程目录结构

1
2
3
4
5
6
7
8
9
10
11
package-tool  #工程目录
├──src #所有开发文件所在目录
| └──test # 例:test 项目开发目录
├──assets # 静态文件路径, 即打包结果文件存放路径
├──build # 该路径下存放打包工具相关脚本
| ├──bundle.to.html.js # js 脚本, 修改 html 文件中引入 bundle 文件的路径
| ├──webpack.config.js # Webpack 打包配置文件
| └──postcss.config.js # CSS 样式处理配置文件
├──.babelrc # babel 转码配置文件
├──package.config.js # Webpack 打包参数配置文件
└──package.json # node 工程的依赖管理,脚本命令配置文件

使用脚手架

1、安装脚手架

安装 nodeJs
下载: https://github.com/FeifeiyuM/package-tool/tree/webpack2
命令行进入脚手架根目录, 执行 npm install

2、新建工程

例如:
a、在 ./src/路径下新建文件夹 test
b、在 ./src/test 路径下 新建 index.js 作为打包的入口文件, 新建html文件 index.html, 新建样式文件 index.less
注意: 入口文件名,与 html 文件名务必保持一致, 例如: 如果入口文件名为 main.js, 与其对应的 html 文件名为 main.html
c、编写入口文件 & 样式文件

1
2
3
4
5
6
7
//index.js
import './index.less'
let name = 'feifeiyu'
let printName = function() {
document.getElementById('output').innerHTML = name
}
printName()
1
2
3
4
5
6
p {
color: blue;
span {
color: yellow;
}
}

c、在 html 文件中添加外联样式和脚本,
<script src=”/assets/test/index.bundle.js”></script>,
<link rel=”stylesheet” href=”/assets/test2/index.bundle.css”>
对于路径为 ./src/test/index.js 的入口文件, 输出 bundle 文件为 /assets/test/index.bundle.js, 其中 /assets/为 web 的静态文件路径, 本脚手架配置的静态文件路径为 /assets/
如果使能抽取 css, (下一节的 extractCss = true), 打包路径中会输出 /assets/test/index.bundle.css 文件

1
2
3
4
5
6
7
8
9
<head>
<meta charset="utf-8">
<title>TEST</title>
<link rel="stylesheet" href="/assets/test2/index.bundle.css">
</head>
<body>
<p>输出:<span id="output"></span></p>
<script src="/assets/test2/index.bundle.js"></script>
</body>

3、打包参数配置

进入工程根目录下的 package.config.js 文件, 配置打包参数(添加打包入口文件, 静态文件路径等)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//是否允许分片打包
//如果允许分片打包,入口文件只能有一个
//只有 WebApp 才会用到分片打包
let codeSplit = true

//打包路径配置
let dirs = {
//入口文件列表
// 如果 codeSplit == true, enteryDir 的长度只能为 1
enteryDir: [
'./src/test/index.js', //类型字符串, 将路径写全
],
//将打包结果输出到目标路径, 为空时,打包后文件输出到 assets 目录
outputDir: '' //支持一个输出路径
}

//是否提取css为单独文件从
let extractCss = false

//跨域调试 host, 一般不用
let hostName = ''

//静态文件路径工程,以 / 开头和结尾
//例如: <script src="/assets/test/index.bundle.js"></script> 中,
// /assets/ 就是整个打包工程的静态文件路径
let hostPre = '/assets/'
//如果使能分片 codeSplit == true, 此时 hostPre = 静态文件路径 + 入口文件去掉./src 和文件名
//例如: 入口文件为 './src/vuestart/index.js', 采用分片打包
// let hostPre = '/assets/vuestart/'

let config = {
dirs: dirs,
extractCss: extractCss,
codeSplit: codeSplit,
hostName: hostName,
hostPre: hostPre
}

module.exports = config

2、打包命令

命令行 console 进入当前工程根目录
1、开发环境打包,且使用 webpack-dev-server 服务,

  • 执行 npm start, 采用这种打包模式, 在静态文件路径中不会输出打包结果,
  • 在浏览器输入 localhost:3000, 进入对应的 html 文件打开,即可进行调试

2、开发环境打包, 不使用 webpack-dev-server 服务

  • 执行 npm run dev, 打包结束后会在静态文件路径输出打包结果
  • 例如: 入口文件为 ‘./src/test/index.js’, 打包输出结果为, ./assets/test/index.bundle.js, 中间路径 web/test 主要用于区分不同的打包工程

3、生产环境打包,

  • 执行 npm run build, 打包结束后会在静态文件路径输出打包结果, 打包输出文件名中有一段随机数, 用于版本区分。
  • 打包结束后会在 bundle 输出路径中生成一个 manifest.json 文件,里面记录了打包的结果
  • 同时对应 html 文件中的静态文件配置会作出相应的修改, 无需手动修改

源码解析

Webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
'use strict'
const fs = require('fs')
const path = require('path')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const ManifestPlugin = require('webpack-manifest-plugin')
const CleanWebpackPlugin = require('clean-webpack-plugin')
const config = require('../package.config')
//判断开发环境还是生产环境
let isProduction = process.env.NODE_ENV === 'production' ? true : false
console.log('isProduction', isProduction)
let getEnteryFiles = function() {
let enteryObj = {}
if(config.dirs.enteryDir.length === 0 || config.dirs.enteryDir === undefined) {
console.log('[webpack]', 'entry path need to configurated ')
return
} else {
if(config.codeSplit && config.dirs.enteryDir.length > 1) {
console.log('here')
throw ('[error]: only one entry file need when use code splitting!!')
}
// 路径分析
if(config.codeSplit) { //如果是分片打包, 为了分片文件的输出路径
let enteryName = /\w+\.(js|ts)$/.exec(config.dirs.enteryDir[0])[0].replace(/\.(js|ts(x?))$/, '')
console.log('[wepbapck] code split file', enteryName)
enteryObj[enteryName] = config.dirs.enteryDir[0]
} else { //正常打包
config.dirs.enteryDir.map((item, index) => {
fs.stat(item, function(err, state) {
if(err) {
throw ('path' + item + 'is not a correct path!')
}
})
//打包文件
let enteryName = item.replace(/\.\/src\//, '').replace(/\.(js|ts(x?))$/, '')
console.log('[webpack] file item: ', enteryName)
enteryObj[enteryName] = item
})
}
}
return enteryObj
}
let enteryFiles = getEnteryFiles()
let outputDir = () => {
if(config.codeSplit) {
//如果是分片打包,要让分片文件输出到与主文件一致的目录
//需要通过 output.path 来控制分片文件输出路径
let outPath = config.dirs.enteryDir[0].replace(/\.\/src\//, '').replace(/\/\w+\.(js|ts(x?))$/, '')
return path.resolve(__dirname, '..', 'assets', outPath)
} else {
if(config.dirs.outputDir) {
return path.resolve(__dirname, '..', config.dirs.outputDir)
} else {
return path.resolve(__dirname, '..', 'assets')
}
}
}
let staticPath = () => {
if(config.codeSplit) {
//如果是分片打包,要让分片文件输出到与主文件一致的目录
//需要通过 output.publicPath 来控制分片静态文件路径
let outPath = config.dirs.enteryDir[0].replace(/\.\/src\//, '').replace(/\w+\.(js|ts(x?))$/, '')
return path.join('/assets', outPath)
} else {
return '/assets'
}
}
//css extract
let getPlugins = function() {
let plugins = []
//抽取 css 文件插件
plugins.push(new ExtractTextPlugin({
filename: isProduction ? '[name].[chunkhash].bundle.css' : '[name].bundle.css', //抽取 CSS 文件名称
disable: !config.extractCss, //是否使能 CSS 抽取
allChunks: true
}))
if(isProduction) {
let staticFilePath = []
if(config.codeSplit) {
//分片打包模式下, 打包结果输出 manifest.json
let outPath = config.dirs.enteryDir[0].replace(/\.\/src\//, '').replace(/\w+\.(js|ts(x?))$/, '')
plugins.push(new ManifestPlugin({filename: 'manifest.json', publicPath: config.hostPre + outPath}))
//配置需要清空的打包路径
staticFilePath.push('assets/' + outPath)
} else {
//正常打包模式下, 打包结果输出 manifest.json
plugins.push(new ManifestPlugin({filename: 'manifest.json', publicPath: config.hostPre}))
//配置需要清空的打包路径
for(let key in enteryFiles) {
staticFilePath.push('assets/' + key.replace(/\w+$/, ''))
}
}
//清空的打包路径
plugins.push(
new CleanWebpackPlugin(staticFilePath)
)
}
return plugins
}
let moduleConfig = {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
exclude: /node_modules/
},
{
test: /\.ts(x?)$/,
use: [
{
loader: 'babel-loader'
},
{
loader: 'ts-loader'
}
]
},
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/
},
{
test: /\.css$/,
exclude: /node_modules/,
use: ExtractTextPlugin.extract([
{
loader: 'css-loader',
options: {
importLoaders: 1
}
},
{
loader: 'postcss-loader'
}
])
},
{
test: /\.less$/,
exclude: /node_modules/,
use: ExtractTextPlugin.extract([
{
loader: 'css-loader',
options: {
importLoaders: 1
}
},
{
loader: 'less-loader'
},
{
loader: 'postcss-loader'
}
])
},
{
test: /\.(jpe?g|png|gif|svg)$/i,
loader: 'url-loader',
options: {
limit: 500, //图片大小超过0.5kb, 不压缩入 bundle
name: 'images/[name].[ext]' //图片输出路径
}
}
]
}
module.exports = {
devtool: isProduction ? '' : 'cheap-eval-source-map',
entry: enteryFiles,
output: {
filename: isProduction ? '[name].[chunkhash].bundle.js' : '[name].bundle.js',
path: outputDir(),
publicPath: staticPath()
},
module: moduleConfig,
plugins: getPlugins(),
performance: { //开发环境下不显示包过大警告
hints: false
},
devServer: {
host: '0.0.0.0',
port: 3000,
open: false,
publicPath: staticPath()
}
}

Babel 配置文件 .babelrc

与webpack1.x 的 配置略有改变, 支持 tree-shaking, 剔除无用代码

1
2
3
4
5
6
7
8
9
10
11
12
{
"presets": [
[
"es2015",
{
"modules": false
}
],
"stage-2",
"es2016"
]
}

postcss.config.js

本文件是 wepback postcss-loader 的相关配置, 详情见,postcss-loader 文档

1
2
3
4
5
6
7
'use strict'
module.exports = {
plugins: [
require('precss'),
require('autoprefixer')({browserslist: ['ie 9', 'last 2 version']})
]
}

bundle.to.html.js

本文件脚本主要是实现 html 文件的外联脚本文件样式文件路径的自动修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
'use strict'
const fs = require('fs')
const config = require('../package.config')
//加载 打包结果文件 manifest.json 内容
let getManifest = () => {
let mfPath = ''
if(config.codeSplit) {
//采用 codeSplit 模式的 manifest.json 路径
if(config.dirs.outputDir) {
let outputDir = config.dirs.outputDir.replace(/\/$/, '')
mfPath = config.dirs.enteryDir[0].replace(/^\./, outputDir).replace(/\w+\.(js|ts(x?))$/, 'manifest.json')
} else {
mfPath = config.dirs.enteryDir[0].replace(/^\./, './assets').replace(/\w+\.(js|ts(x?))$/, 'manifest.json')
}

} else {
//采用正常模式的 manifest.json 路径
if(config.dirs.outputDir) {
let outputDir = config.dirs.outputDir.replace(/\/$/, '')
mfPath = outputDir + '/manifest.json'
} else {
mfPath = './assets/manifest.json'
}
}
//读取文件,并返回
return new Promise((resolve, reject) => {
fs.readFile(mfPath, (err, data) => {
if(err) {
reject(err)
} else {
resolve(JSON.parse(data))
}
})
})
}
//生产环境下更新静态文件路径
//一个页面中只能有一个 bundle.js 或只能一个 bundle.css
let modifyHtmlStrProd = (data, bundle) => {
if(bundle.js) {
if(/<script.*\.bundle\.js\??.*"><\/script>/i.test(data)) {
data = data.replace(/<script.*\.bundle\.js\??.*"><\/script>/i, '<script src="' + bundle.js + '"></script>')
} else {
data = data.replace(/<\/body>/i, '<script src="' + bundle.js + '"></script>\r\n</body>')
}
}
if(bundle.css) {
if(bundle.css && /<link rel="stylesheet".*\.bundle\.css\??.*"\/?>/i.test(data)) {
data = data.replace(/<link rel="stylesheet".*\.bundle\.css"\/?>/i, '<link rel="stylesheet" href="' + bundle.css + '">')
} else {
data = data.replace(/<\/head>/i, '<link rel="stylesheet" href="' + bundle.css + '">\r\n</head>')
}
}
return data
}
//开发环境下更新静态文件路径, 去掉版本号
//一个页面中只能有一个 bundle.js 或只能一个 bundle.css
let modifyHtmlStrDev = (data, bundle) => {
if(/<script.*\.bundle\.js\??.*"><\/script>/i.test(data)) {
data = data.replace(/<script.*\.bundle\.js\??.*"><\/script>/i, '<script src="' + bundle.js + '"></script>')
} else {
throw 'path of script bundle.js need to be added to html file'
}
if(config.extractCss) {
if(/<link rel="stylesheet".*\.bundle\.css\??.*"\/?>/i.test(data)) {
data = data.replace(/<link rel="stylesheet".*\.bundle\.css\??.*"\/?>/i, '<link rel="stylesheet" href="' + bundle.css + '">')
} else {
if(!config.codeSplit) {
console.log('path of stylesheet bundle.css need to be added to html file')
}
}
}
return data
}
//检查文件函数
let checkFile = (filePath) => {
try {
if(fs.statSync(filePath).isFile()) { //是否存在文件
return filePath
} else { //如果不是文件,抛出错误
throw '[error]: target html file not exist'
}
} catch(err) { //如果无法读取
//换成 index.html 文件
filePath = filePath.replace(/\w+\.html/, 'index.html')
if(fs.statSync(filePath).isFile()) {
return filePath //返回更新后的路径
} else {
throw '[error]: target html file not exist'
}
}
}
let updateProdHTMLPages = () => {
getManifest().then(data => {
let bundleMap = {}
for(let key in data) {
//提取有效的输出 bundle 文件
if(key.indexOf('bundle') > -1) {
continue
}
//提取有效的 css bundle
if(/\.css/.test(key)) {
let bundleKey = key.replace('.css', '')
if(bundleMap[bundleKey]) {
bundleMap[bundleKey].css = data[key]
} else {
bundleMap[bundleKey] = {
css: data[key]
}
}
} else if(/\.(js|ts(x?))$/.test(key)) {
//提取有效的 js bundle
let bundleKey = key.replace(/\.(js|ts(x?))$/, '')
if(bundleMap[bundleKey]) {
bundleMap[bundleKey].js = data[key]
} else {
bundleMap[bundleKey] = {
js: data[key]
}
}
}
}
for(let key in bundleMap) {
let filePath = ''
//根据入口文件寻找 对应的 html 文件, 要保证 入口文件名 和 html 文件名一致,
if(config.codeSplit) {
filePath = config.dirs.enteryDir[0].replace(/\.(js|ts(x?))$/, '.html')
} else {
filePath = './' + key + '.html'
}
//检查文件是否存在
filePath = checkFile(filePath)

fs.readFile(filePath, (err, data) => {
if(err) {
console.log('[error]: failed to read ', filePath, err)
return
}
//修改 html 内容
data = modifyHtmlStrProd(data.toString(), bundleMap[key])
//重新写入 html 修改后的内容
fs.writeFile(filePath, data, (err, data) => {
if(err) {
console.log('[error]: failed to update ' + filePath)
return
}
console.log('[success]: ' + filePath + ' updated successfully')
})
})
}
}).catch((err) => {
//do nothing
console.log('[error]: manifest.json read error', err)
})
}
let setDevHTMLPages = () => {
let enterys = config.dirs.enteryDir
for(let i = 0; i < enterys.length; i++) {
//根据入口文件确定输出 bundle 文件, 此时 bundle 文件名中没有版本号
let staticName = enterys[i].replace(/^\.\//, '').replace(/\.(js|ts(x?))$/, '')
let bundleMap = {
js: config.hostPre + staticName + '.bundle.js',
css: config.hostPre + staticName + '.bundle.css'
}
//根据入口文件,确定对应的 html 文件
let filePath = enterys[i].replace(/\.(js|ts(x?))$/, '.html')
//检查文件是否存在
filePath = checkFile(filePath)
fs.readFile(filePath, (err, data) => {
if(err) {
console.log('[error]: failed to read ' + filePath)
return
}
//更新静态文件路径
data = modifyHtmlStrDev(data.toString(), bundleMap)
//重新写入数据
fs.writeFile(filePath, data, (err, data) => {
if(err) {
console.log('[error]: failed to update ' + filePath)
return
}
console.log('[success]: ' + filePath + ' updated successfully')
})
})
}
}
//开发环境判断
let isProduction = process.env.NODE_ENV === 'production'
if(isProduction) {
updateProdHTMLPages()
} else {
setDevHTMLPages()
}

package.json

该文件是 node 依赖管理文件, 同时打包启动命令也在该文件中配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
{
"name": "package-tool",
"version": "1.0.0",
"description": "for package compile base on webpack",
"main": "server.js",
"scripts": {
"start": "export NODE_ENV='development' && node build/bundle.to.html.js && webpack-dev-server --config build/webpack.config.js --watch ",
"dev": "export NODE_ENV='development' && node build/bundle.to.html.js && webpack --config build/webpack.config.js --progress --watch",
"ver": "node build/bundle.to.html.js",
"build": "export NODE_ENV='production' && webpack --config build/webpack.config.js -p && node build/bundle.to.html.js"
},
"keywords": [
"package",
"compile",
"webpack"
],
"author": "feifeiyu",
"license": "MIT",
"devDependencies": {
"autoprefixer": "^6.6.1",
"babel": "^6.5.2",
"babel-core": "^6.21.0",
"babel-loader": "^6.2.10",
"babel-preset-es2015": "^6.18.0",
"babel-preset-es2016": "^6.16.0",
"babel-preset-stage-2": "^6.18.0",
"clean-webpack-plugin": "^0.1.15",
"css-loader": "^0.26.1",
"extract-text-webpack-plugin": "^2.0.0-beta.4",
"file-loader": "^0.9.0",
"less": "^2.7.2",
"less-loader": "^2.2.3",
"postcss-loader": "^1.2.1",
"precss": "^1.4.0",
"style-loader": "^0.13.1",
"ts-loader": "^1.3.3",
"typescript": "^2.1.5",
"url-loader": "^0.5.7",
"vue-loader": "^10.0.2",
"vue-style-loader": "^1.0.0",
"vue-template-compiler": "^2.1.8",
"webpack": "^2.2.0-rc.8",
"webpack-dev-server": "^2.2.0-rc.0",
"webpack-manifest-plugin": "^1.1.0"
},
"dependencies": {
"vue": "^2.1.8",
"vue-router": "^2.1.1",
"vuex": "^2.1.1",
"vuex-router-sync": "^4.1.0"
}
}

END