本篇将介绍 Webpack 2 的相关配置,以及使用 Webpack 2 进行前端开发的解决方案,或前端开发脚手架
Webpack
Webpack 是一个前端资源加载/打包工具, 只需要相对简单的配置,就可以整合出一套前端开发解决方案
Webpack 一个主要的特性是可以实现前端开发的模块化, 它能分析整个前端工程的项目结构, 找到相关的 JavaScrit 模块以及他的一些样式文件,图片文件等, 并将他们整合成一个或多个文件输出,提供给浏览器执行。
Webpack 通过相应的配置可以实现 JS 代码的降级(ES6 to ES5), 方言的转译(TypeScript to Js, Less to CSS)等
注: 模块化是指: 把前端程序开发像后端开发一样按功能切割成多个小文件(封装成接口), 有利于实现前端的工程化开发,例如 WebApp(SPA)开发
注意: 采用 webpack 打包之后, IE8 的兼容有很严重的问题
功能介绍
本文介绍的 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 访问。
工程目录结构
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、编写入口文件 & 样式文件
//index.js
import './index.less'
let name = 'feifeiyu'
let printName = function() {
document.getElementById('output').innerHTML = name
}
printName()
p {
color: blue;
span {
color: yellow;
}
}
c、在 html 文件中添加外联样式和脚本,
,
对于路径为 ./src/test/index.js 的入口文件, 输出 bundle 文件为 /assets/test/index.bundle.js, 其中 /assets/为 web 的静态文件路径, 本脚手架配置的静态文件路径为 /assets/
如果使能抽取 css, (下一节的 extractCss = true), 打包路径中会输出 /assets/test/index.bundle.css 文件
<head>
<meta charset="utf-8">
<title>TESTtitle>
<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 文件, 配置打包参数(添加打包入口文件, 静态文件路径等)
//是否允许分片打包
//如果允许分片打包,入口文件只能有一个
//只有 WebApp 才会用到分片打包
let codeSplit = true
//打包路径配置
let dirs = {
//入口文件列表
// 如果 codeSplit == true, enteryDir 的长度只能为 1
enteryDir: [
'./src/test/index.js', //类型字符串, 将路径写全
],
//将打包结果输出到目标路径, 为空时,打包后文件输出到 assets 目录
outputDir: '' //支持一个输出路径
}
//是否提取css为单独文件从
let extractCss = false
//跨域调试 host, 一般不用
let hostName = ''
//静态文件路径工程,以 / 开头和结尾
//例如:中,
// /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 进入当前工程根目录
开发环境打包,且使用 webpack-dev-server 服务,
执行 npm start, 采用这种打包模式, 在静态文件路径中不会输出打包结果,
在浏览器输入 localhost:3000, 进入对应的 html 文件打开,即可进行调试
开发环境打包, 不使用 webpack-dev-server 服务
执行 npm run dev, 打包结束后会在静态文件路径输出打包结果
例如: 入口文件为 ‘./src/test/index.js’, 打包输出结果为, ./assets/test/index.bundle.js, 中间路径 web/test 主要用于区分不同的打包工程
生产环境打包,
执行 npm run build, 打包结束后会在静态文件路径输出打包结果, 打包输出文件名中有一段随机数, 用于版本区分。
打包结束后会在 bundle 输出路径中生成一个 manifest.json 文件,里面记录了打包的结果
同时对应 html 文件中的静态文件配置会作出相应的修改, 无需手动修改
源码解析
Webpack.config.js
'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, 剔除无用代码
{
"presets": [
[
"es2015",
{
"modules": false
}
],
"stage-2",
"es2016"
]
}
postcss.config.js
本文件是 wepback postcss-loader 的相关配置, 详情见,postcss-loader 文档
'use strict'
module.exports = {
plugins: [
require('precss'),
require('autoprefixer')({browserslist: ['ie 9', 'last 2 version']})
]
}
bundle.to.html.js
本文件脚本主要是实现 html 文件的外联脚本文件样式文件路径的自动修改
'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(/^\.\/src/, outputDir + '/').replace(/\w+\.(js|ts(x?))$/, 'manifest.json')
} else {
mfPath = config.dirs.enteryDir[0].replace(/^\.\/src/, './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="">/i .test(data)) {
data = data.replace(/<\ script="">/i , '')
} else {
data = data.replace(/<\ body="">/i, '\r\n')
}
}
if(bundle.css) {
if(bundle.css && //i, '')
} else {
data = data.replace(/<\ head="">/i, '\r\n')
}
}
return data
}
//开发环境下更新静态文件路径, 去掉版本号
//一个页面中只能有一个 bundle.js 或只能一个 bundle.css
let modifyHtmlStrDev = (data, bundle) => {
if(/<\ script="">/i .test(data)) {
data = data.replace(/<\ script="">/i , '')
} else {
throw 'path of script bundle.js need to be added to html file'
}
if(config.extractCss) {
if(//i, '')
} else {
if(!config.codeSplit) {
throw '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 = './src/' + 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(/^\.\/src\//, '').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' ? true : false
if(isProduction) {
updateProdHTMLPages()
} else {
setDevHTMLPages()
}
package.json
该文件是 node 依赖管理文件, 同时打包启动命令也在该文件中配置
{
"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"
}
}
完
转载请注明: Vue教程中文网 - 打造国内领先的vue学习网站-vue视频,vue教程,vue学习,vue培训 » webpack项目实战完整教程