用webpack搭建多页面项目

8,288 阅读6分钟

最基本的单入口配置

之前使用webpack1.x的可以读读这篇文章《webpack升级指南和特性摘要》

module.exports={
  entry:'./src/index.js'  
  output:{
    path:__dirname+'/build',  //打包路径
    publicPath:publicPath,   //静态资源相对路径
    filename:`[name].js` //打包后的文件名,[name]是指对应的入口文件的名字
  }
}

以上是单入口的基本配置,多页面项目首先需要将入口文件变为多入口,结构类似这样

{
    a:'./src/a.js',
    b:'./src/b.js'
}

那么问题来了,如何将入口文件变成这种形式呢?这里需要用到 nodefs模块(fs模块使用文档),通过 fs.readdirSync 方法获取文件名(fs使用之前需要require该模块的),多层目录需要递归获取,具体操作方法如下:

工具方法可以单独放在 webpack.uitl.js

webpack.util.js

    //getAllFileArr方法
    //递归获取文件列表可能会在多处用到,所以可以封装成方法
    //最后返回的数据格式如下
    /*
    [ [ 'a.js', './src/scripts/', './src/scripts/a.js' ],
        [ 'b.js', './src/scripts/', './src/scripts/b.js' ],
        [ 'c.js', './src/scripts/', './src/scripts/c.js' ] ]
    */
    function getAllFileArr(path){
        let AllFileList=[];
        getAllFile(path)
        function getAllFile(path) {
            var files = [];
            if( fs.existsSync(path) ) {   //是否存在此路径
                files = fs.readdirSync(path); //获取当前目录下一层文件列表
                files.forEach((file,index)=>{ //遍历获取文件
                    var curPath = path + "/" + file;
                    if(fs.statSync(curPath).isDirectory()) { // recurse 查看文件是否是文件夹
                        getAllFile(curPath); //如果是文件夹,继续遍历获取
                    } else {
                        if(file!=='.DS_Store'){
                            //.DS_Store是IDE配置文件不用计入到文件列表
                            AllFileList.push([file,path,curPath]) 
                        }
                    }
                });
            }
        };
        return AllFileList;
    }
    exports.getAllFileArr=getAllFileArr; //对外吐出该方法,供其他文件使用
    //getEntry方法
    //最后返回如下数据结构
    /*
      {
          a: './src/scripts/a.js',
          b: './src/scripts/b.js',
          c: './src/scripts/c.js'
      }
    */
    function getEntyry(path){
        let file_list=getAllFileArr(path);
        let entry={};
          file_list.forEach((item)=>{
              entry[item[0].split('.').slice(0,-1).join('.')]=item[2] //键名去掉文件后缀
          })
        return entry;
    }
    exports.getEntry = getEntry;//对外吐出该方法,供其他文件使用

方法写好后,多入口的基本配置就可以这么写:

webpac.config.js

const utils = require('./webpack.util.js')

module.exports={
  entry:utils.getEntyry('./src/script') //使用getentry方法获取多入口
  output:{
    path:__dirname+'/build',  //打包路径
    publicPath:publicPath,   //静态资源相对路径
    filename:`[name].js` //打包后的文件名,[name]是指对应的入口文件的名字
  }
}

如上配置执行 webpack 命令后,会将入口文件中所有的成员都打包到 build 下,文件名为 entry 对象中的键名。

loader配置

这里列出常用的loader,根据使用的技术框架,可能会有些差别,我的项目用的是 react ,所以 babel-loader 会匹配jsx,如果使用其他框架,则按需配置loader,例如使用vue,则需要新增加一个 vue-loader (具体请自行google

  module:{
    rules:[
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        loader: 'babel-loader',
      },{
        test:/\.(css|scss)$/,
        loader:"style-loader!css-loader!postcss-loader!sass-loader" //webpack2.x是不支持loader简写的,这里稍微注意一下
      },
      {
        test: /\.(png|jpg|gif|svg|eot|ttf|woff)$/,
        loader: 'file-loader',
        options: {
          name: 'source/[name].[ext]?[hash]' //该参数是打包后的文件名
        }
      }
    ]
  },


webpack plugins 配置

  • html单独打包插件

    多数情况下,我们不只是需要打包js文件,而是需要将html页打包,但是html文件是不能作为入口文件的,用fs模块也可以将html拷贝带build,但是文件里的引用关系就麻烦了,这个时候我们就可以借助一个插件 html-webpack-plugin来帮助我们完成该工作,直接上代码,解释写在注释中:

const htmlWebpackPlugin = require('html-webpack-plugin')

const utils = require('./webpack.util.js')
module.exports={
  entry:{
        //注意,webpack.optimize.CommonsChunkPlugin打包时候的chunk是跟entry的键名对应的
        app:'./src/app.js',
        lib:['src/lib/fastClick.js','src/lib/vConsole.js'] 
  } 
  output:{
    path:__dirname+'/build',  //打包路径
    publicPath:publicPath,   //静态资源相对路径
    filename:`[name].js` //打包后的文件名,[name]是指对应的入口文件的名字
  }
}


//遍历所有需要打包的html文件,分别配置打包
var html_list=utils.getAllFileArr('./src');
html_list.forEach((item)=>{
  var name = item[2];

  if(/\.html$/.test(item[0])){
    var prex='' //文件前缀,如果想给打包的html放在build下的html文件夹中,则var prex='html/'
    module.exports.plugins.push( //每个文件分别配置插件
      new htmlWebpackPlugin({ 
          favicon: './src/images/favicon.ico', //favicon路径,通过webpack引入同时可以生成hash值
          filename: prex+item[0],
          template: name, //html模板路径
          inject: true, //js插入的位置,true/'head'/'body'/false
          hash: true, //为静态资源生成hash值
          chunks: [item[0].slice(0,-5),'common'],//需要引入的chunk,不配置就会引入所有页面的资源
          minify: { //压缩HTML文件
              removeComments: true, //移除HTML中的注释
              collapseWhitespace: false, //删除空白符与换行符
              ignoreCustomFragments:[
              //     regexp  //不处理 正则匹配到的 内容
              ]
          },
          minify: false //不压缩
      })
    )
  }
})
  • webpack.optimize.CommonsChunkPlugin插件(webpack内置插件文档

    项目中有许多js是多次被引用的,webpack 是会将这些js打包所有 import过它们的js中,这样会导致打包后的js文件都非常庞大,对此 webpack 内置了一个插件 optimize.CommonsChunkPlugin,根据你的配置,会将多次被引用的文件打包到一个公用的js文件中,操作如下:

// 公共代码单独打包
new webpack.optimize.CommonsChunkPlugin({
      name: 'common', //对外吐出的chuank名
      chunks:['app','lib'], //数组,需要打包的文件[a,b,c]对应入口文件中的key
      minChunks:4, //chunks至少引用4次的时候打包
      filename: 'script/[name].js' //打包后的文件名
})

//以及一些其他的常用webpack内置插件

//压缩编译后的代码,加了这个插件后编译速度会很慢,所以一般在生产环境加
new webpack.optimize.UglifyJsPlugin({
  sourceMap: true,
  compress: {
    warnings: false
  }
}),
/*
    UglifyJsPlugin 将不再支持让 Loaders 最小化文件的模式。debug 选项已经被移除。Loaders 不能从 webpack 的配置中读取到他们的配置项。
    loader的最小化文件模式将会在webpack 3或者后续版本中被彻底取消掉.

    为了兼容部分旧式loader,你可以通过 LoaderOptionsPlugin 的配置项来提供这些功能。
*/
new webpack.LoaderOptionsPlugin({
  minimize: true
}),

//代码合并压缩
new webpack.DefinePlugin({
  'process.env': {
    NODE_ENV: '"production"'
  }
})

默认情况下,webpack是将依赖的css以style标签的形式插入到head中,文件依赖多了,也会使打包后的文件过大,extract-text-webpack-plugin 可以将css文件打包成一个公共的css文件,然后以link的方式打包到html的head中:

module:{
    rules:[
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        loader: 'babel-loader',
      },{
        test:/\.(css|scss)$/,
        //注意,使用ExtractTextPlugin时,css相关的loader配置需要修改成如下
        use: ExtractTextPlugin.extract({
          fallback: "style-loader",
          use: "css-loader!postcss-loader!sass-loader"
        })
      },
      {
        test: /\.(png|jpg|gif|svg|eot|ttf|woff)$/,
        loader: 'file-loader',
        options: {
          name: 'source/[name].[ext]?[hash]' //该参数是打包后的文件名
        }
      }
    ]
  },
new ExtractTextPlugin("[name].css?[hash]") //基础配置只需要传入打包名称就行了


devserver

webpack-dev-server 基本上使用webpack构建时,调试阶段必用的,具体参数在 webpack官方文档 解释的比较详细,这里就不多说了,简单的贴一下代码:

devServer: {
    contentBase: "./dist",//本地服务器所加载的页面所在的目录
    //historyApiFallback: true, //非hash模式路由不刷新(适用于单页面开发调试)
    noInfo:true,
    host:'192.168.102.103',
    port:'4001'
},


文件拷贝

有些时候,我们需要将某些文件直接拷贝到build目录下,如某些xml配置文件,通过 fs.createReadStreamfs.createWriteStream 进行文件的拷贝和移动(详细说明请看 fs模块使用文档):

module.exports.plugins.push(function(){
    //打包完毕后将devconfig.xml文件移动到build目录下
    return this.plugin('done', function(stats) {
          // 创建读取流
          var readable = fs.createReadStream( './devconfig.xml');
          // 创建写入流
          var writable = fs.createWriteStream( './build/config.xml' );

          // 通过管道来传输流
          readable.pipe( writable );
    });
});


项目发布

在开发的阶段,我们往往不需要让文件打包到最优状态,因为需要保证打包速度,但是在发布的时候需要打包到最优状态,这就需要我们对开发和生产两种模式做不同的处理,我是采用 cross-env这个包获取NODE_ENV的值来判断当前是什么环境:

if (process.env.NODE_ENV === 'production') {
  //生产模式下进行打包优化
}

如何来改变NODE_ENV的值呢? cross-env 可以帮助我们通过命令来修改, 执行以下命令,就能将 process.env.NODE_ENV的值变为'development'

$ cross-env NODE_ENV=development

暂时整理的就这么多,后期有用到新的会继续跟进,有错误的地方还忘指出,谢谢!! 最后贴出完整的配置:


完整配置

  • webpack.util.js

let fs =require('fs')

//获取入口文件对象
function getEntry(file_list){
  var entry={};
  file_list.forEach((item)=>{
      entry[item[0].split('.').slice(0,-1).join('.')]=item[2]
  })
  return entry;
  /*entry 看起来就是这样
      {
          a: './src/scripts/a.js',
          b: './src/scripts/b.js',
          index: './src/scripts/index.js'
      }
  */
}
exports.getEntry = getEntry;


//递归遍历所有文件
function getAllFileArr(path){
    var AllFileList=[];
    getAllFile(path)
    function getAllFile(path) {
        var files = [];
        if( fs.existsSync(path) ) {   //是否存在此路径
            files = fs.readdirSync(path);
            files.forEach(function(file,index){
                var curPath = path + "/" + file;
                if(fs.statSync(curPath).isDirectory()) { // recurse 查看文件是否是文件夹
                    getAllFile(curPath);
                } else {
                    if(file!=='.DS_Store'){
                        AllFileList.push([file,path,curPath])
                    }
                }
            });
        }
    };
    /*
        最后AllFileList 看起来就是这样
        [ [ 'a.js', './src/scripts/', './src/scripts/a.js' ],
          [ 'b.js', './src/scripts/', './src/scripts/b.js' ],
          [ 'index.js', './src/scripts/', './src/scripts/index.js' ] ]
     */
    return AllFileList;
}
exports.getAllFileArr=getAllFileArr;


//删除文件夹 ,递归删除
function deleteFolderRecursive(path) {
    var files = [];
    if( fs.existsSync(path) ) {
        files = fs.readdirSync(path);
        files.forEach(function(file,index){
            var curPath = path + "/" + file;
            if(fs.statSync(curPath).isDirectory()) { // recurse 查看文件是否是文件夹
                deleteFolderRecursive(curPath);
            } else { // delete file
                fs.unlinkSync(curPath);
            }
        });
        fs.rmdirSync(path);
    }
};

exports.deleteFolderRecursive=deleteFolderRecursive;


  • webpack.config.js

const path =require('path');
const fs =require('fs')
const webpack = require('webpack');
const htmlWebpackPlugin = require('html-webpack-plugin')
let ExtractTextPlugin = require('extract-text-webpack-plugin')

const utils = require('./webpack.util.js')

//打包之前删除build文件夹
utils.deleteFolderRecursive('./build')

let publicPath='./'
    ,updateTime=new Date().getTime()

module.exports={
  entry:{
    ...utils.getEntry(utils.getAllFileArr('./src/script')),
    react:'react',
    jquery:'jquery'
  },
  output:{
    path:__dirname+'/build',
    publicPath:publicPath,
    filename:`script/[name].js`
  },
  module:{
    rules:[
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        loader: 'babel-loader',
      },{
        test:/\.(css|scss)$/,
        // loader:"style-loader!css-loader!postcss-loader!sass-loader"
        use: ExtractTextPlugin.extract({
          fallback: "style-loader",
          use: "css-loader!postcss-loader!sass-loader"
        })
      },
      {
        test: /\.(png|jpg|gif|svg|eot|ttf|woff)$/,
        loader: 'file-loader',
        options: {
          name: 'source/[name].[ext]?[hash]'
        }
      }
    ]
  },
  resolve:{
    extensions:['.scss', '.js','.jsx'],
    alias: {
      'bassCss':__dirname+'/src/css',
      'image':__dirname+'/src/image',
      'components':__dirname+'/src/script/components'
    }
  },
  devServer: {
    // contentBase: "./dist",//本地服务器所加载的页面所在的目录
    // historyApiFallback: true, //不跳转
    noInfo:true,
    host:'192.168.102.103',
    port:'4001'
  },
  plugins:[
    new webpack.optimize.UglifyJsPlugin({
      sourceMap: true,
      compress: {
        warnings: false
      }
    }),
    new webpack.LoaderOptionsPlugin({
      minimize: true
    }),
    // 公共代码单独打包
    new webpack.optimize.CommonsChunkPlugin({
      name: 'common', //对外吐出的chuank名
      chunks:Object.keys(utils.getEntry(utils.getAllFileArr('./src/script'))), //数组,需要打包的文件[a,b,c]对应入口文件中的key
      minChunks:4, //chunks至少引用4次的时候打包
      filename: 'script/[name].js' //打包后的文件名
    })
  ]
}

module.exports.plugins.push(new ExtractTextPlugin("[name].css?[hash]"))

//复制 config.xml 到 build目录下
module.exports.plugins.push(function(){

    return this.plugin('done', function(stats) {
          // 创建读取流
          var readable = fs.createReadStream( './devconfig.xml');
          // 创建写入流
          var writable = fs.createWriteStream( './build/config.xml' );

          // 通过管道来传输流
          readable.pipe( writable );
    });
});


//将html文件打包
var html_list=utils.getAllFileArr('./src');
html_list.forEach((item)=>{
  var name = item[2];

  if(/\.html$/.test(item[0])){
    var prex=''//item[1].indexOf('html')>-1?'html/':''
    module.exports.plugins.push(
      new htmlWebpackPlugin({ //根据模板插入css/js等生成最终HTML
          // favicon: './src/images/favicon.ico', //favicon路径,通过webpack引入同时可以生成hash值
          filename: prex+item[0],
          template: name, //html模板路径
          inject: true, //js插入的位置,true/'head'/'body'/false
          hash: true, //为静态资源生成hash值
          chunks: [item[0].slice(0,-5),'common'],//需要引入的chunk,不配置就会引入所有页面的资源
          minify: { //压缩HTML文件
              removeComments: true, //移除HTML中的注释
              collapseWhitespace: false, //删除空白符与换行符
              // ignoreCustomFragments:[
              //     /\{\{[\s\S]*?\}\}/g  //不处理 {{}} 里面的 内容
              // ]
          },
          minify: false //不压缩
      })
    )
  }
})




//生产模式打包的时候进行代码压缩合并优化
if (process.env.NODE_ENV === 'production') {
  module.exports.devtool = '#eval-source-map'
  module.exports.output.publicPath='./'

  //发布时给文件名加上时间
  module.exports.plugins[module.exports.plugins.length-1]=new ExtractTextPlugin(`css/${updateTime}_[name].css?[hash]`);
  module.exports.output.filename=`script/${updateTime}_[name].js`;

  module.exports.plugins = (module.exports.plugins || []).concat([
    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: '"production"'
      }
    })
  ])
}


  • package.json

{
  "name": "yit01",
  "version": "1.0.0",
  "description": "",
  "main": "webpack.config.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "cross-env NODE_ENV=development webpack --progress --hide-modules --watch",
    "build": "cross-env NODE_ENV=production webpack --progress --hide-modules"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "babel-core": "^6.26.0",
    "babel-loader": "^7.1.2",
    "babel-preset-es2015": "^6.24.1",
    "babel-preset-react": "^6.24.1",
    "babel-preset-stage-2": "^6.24.1",
    "cross-env": "^5.1.1",
    "css-loader": "^0.28.7",
    "extract-text-webpack-plugin": "^3.0.2",
    "file-loader": "^1.1.5",
    "html-webpack-plugin": "^2.30.1",
    "node-sass": "^4.6.0",
    "postcss-loader": "^2.0.8",
    "sass-loader": "^6.0.6",
    "scss-loader": "0.0.1",
    "style-loader": "^0.19.0",
    "webpack": "^3.8.1",
    "webpack-dev-server": "^2.9.4"
  },
  "dependencies": {
    "jquery": "^3.2.1",
    "js-md5": "^0.7.2",
    "react": "^16.0.0",
    "react-dom": "^16.0.0",
    "react-fastclick": "^3.0.2",
    "react-lazyload": "^2.3.0",
    "react-redux": "^5.0.6",
    "react-router": "^4.2.0",
    "react-router-dom": "^4.2.2",
    "redux": "^3.7.2"
  }
}