webpack项目实战完整教程

webpack admin 暂无评论

0064cTs2jw1ezz4j2z7u9j30dt07iq44.jpg

本篇将介绍 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 进入当前工程根目录

  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

'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项目实战完整教程

喜欢 ()or分享