Webpack學習筆記

傻瓜版Webpack2.x版本示例項目

  • 下載webpack-demo
  • 項目目錄

    ├── app
    │    ├── app.js
    │    ├── app.scss
    │    ├── config.json
    │    ├── demo.js
    │    ├── demo.tmpl.html
    │    ├── greeter.js
    │    ├── icon.png
    │    ├── index.temp.html
    │    ├── jquery-1.11.3.min.js
    │    ├── main.js
    │    ├── main.scss
    ├── package.json
    └── webpack.config.js
  • 運行
    • npm install webpack -g 安裝webpack,運行webpack -h查看是否安裝成功
    • npm install webpack-dev-server -g 安裝webpack-dev-server,可以通過一個socket.io服務實時監聽文件的變化並自動刷新頁面
    • 安裝第三方npm模塊npm install
    • 在根目錄下運行webpack-dev-server命令開啓本地服務
    • 直接打開http://localhost:8080/測試
  • package.json

    {
      "name": "camera",
      "version": "1.0.0",
      "description": "",
      "main": "index.js",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1",
        "build": "webpack --progress --colors"
      },
      "author": "vcxiaohan",
      "license": "ISC",
      "devDependencies": {
        "css-loader": "^0.26.4",
        "extract-text-webpack-plugin": "^2.1.0",
        "file-loader": "^0.10.1",
        "html-webpack-plugin": "^2.28.0",
        "json-loader": "^0.5.4",
        "node-sass": "^4.5.0",
        "sass-loader": "^6.0.3",
        "style-loader": "^0.13.2",
        "url-loader": "^0.5.8",
        "webpack": "^2.2.1"
      }
    }
  • webpack.config.js

    var webpack = require('webpack');
        var HtmlWebpackPlugin = require('html-webpack-plugin');
        var ExtractTextPlugin = require('extract-text-webpack-plugin'); // 必須配合html-webpack-plugin才能生效
    
        module.exports = {
            devtool: 'source-map',
            //配置生成Source Maps,以便報錯時能定位到具體的行列
            entry: { // 入口文件配置項
                main: __dirname + "/app/main.js",
                // js文件標識,傳入HtmlWebpackPlugin的chunks參數位置,以便生成多個頁面時能引用不同的js文件
                demo: __dirname + "/app/demo.js",
                // 
            },
            output: { // 輸出文件配置項
                path: __dirname + "/public",
                // 打包後的文件存放的地方
                filename: "[name].js" // 打包後輸出文件的文件名
            },
            module: { // 在配置文件裏添加JSON loader
                loaders: [{
                    test: /\.json$/,
                    loader: "json-loader" // webpack2.x 版本不能省略loader後綴
                }, {
                    test: /\.scss$/,
                    loader: ExtractTextPlugin.extract({ // extract-text-webpack-plugin2.x 版本寫法
                        fallback: 'style-loader',
                        use: 'css-loader!sass-loader'
                    })
                }, {
                    test: /\.(png|jpg)$/,
                    loader: 'url-loader?limit=10&name=[hash].[ext]' // url-loader需要配合file-loader使用才能生效,否則讀取大圖片的時候會報錯
                }, ]
            },
            plugins: [ // 插件配置項
            new HtmlWebpackPlugin({ // 生成多頁面
                template: __dirname + "/app/index.tmpl.html",
                // 使用模板
                filename: 'index.html',
                // 輸出的html文件名
                chunks: ['main', 'common'],
                // 引用的js文件標識,必須要引入CommonsChunkPlugin獨立出來的common文件
            }), new HtmlWebpackPlugin({ // 生成多頁面
                template: __dirname + "/app/demo.tmpl.html",
                filename: 'demo.html',
                chunks: ['demo'],
            }), new webpack.BannerPlugin("by vcxiaohan"),
            //new webpack.optimize.UglifyJsPlugin(),// 壓縮js
            new ExtractTextPlugin("[name].css"), // 提取css樣式爲單獨的文件
            new webpack.optimize.CommonsChunkPlugin({ // 把main文件標識的公共部分提取出來獨立成common文件
                name: 'common',
                chunks: ['main']
            }), ],
            devServer: { // 本地服務配置項
                contentBase: "./public",
                // 自定義本地服務器基本目錄
                inline: true,
                // 實時刷新
                proxy: { // 代理
                    '/': {
                        target: 'http://v4.faqrobot.net',
                        changeOrigin: true // 解決跨域代理
                    }
                }
            }
        }

更新於2018-8-10

webpack3.12.0版本示例項目

  • package.json
{
  "name": "vue",
  "version": "1.0.0",
  "description": "A Vue.js project",
  "author": "LAPTOP-ASHTD2FO\\Think <[email protected]>",
  "private": true,
  "scripts": {
    "dll": "webpack --config webpack.dll.conf.js",
    "dev": "webpack-dev-server --config webpack.conf.js",
    "start": "npm run dev",
    "unit": "jest --config test/unit/jest.conf.js --coverage",
    "e2e": "node test/e2e/runner.js",
    "test": "npm run unit && npm run e2e",
    "lint": "eslint --ext .js,.vue src test/unit test/e2e/specs",
    "build": "webpack --config webpack.conf.js"
  },
  "dependencies": {
    "jquery": "^3.3.1",
    "vue": "^2.5.2",
    "vue-router": "^3.0.1"
  },
  "devDependencies": {
    "add-asset-html-webpack-plugin": "^2.1.3",
    "autoprefixer": "^7.1.2",
    "babel-core": "^6.22.1",
    "babel-eslint": "^8.2.1",
    "babel-helper-vue-jsx-merge-props": "^2.0.3",
    "babel-jest": "^21.0.2",
    "babel-loader": "^7.1.1",
    "babel-plugin-dynamic-import-node": "^1.2.0",
    "babel-plugin-syntax-jsx": "^6.18.0",
    "babel-plugin-transform-es2015-modules-commonjs": "^6.26.0",
    "babel-plugin-transform-runtime": "^6.22.0",
    "babel-plugin-transform-vue-jsx": "^3.5.0",
    "babel-preset-env": "^1.3.2",
    "babel-preset-stage-2": "^6.22.0",
    "babel-register": "^6.22.0",
    "chalk": "^2.0.1",
    "chromedriver": "^2.27.2",
    "clean-webpack-plugin": "^0.1.19",
    "copy-webpack-plugin": "^4.0.1",
    "cross-spawn": "^5.0.1",
    "css-hot-loader": "^1.4.1",
    "css-loader": "^0.28.0",
    "eslint": "^4.15.0",
    "eslint-config-standard": "^10.2.1",
    "eslint-friendly-formatter": "^3.0.0",
    "eslint-loader": "^1.7.1",
    "eslint-plugin-import": "^2.7.0",
    "eslint-plugin-node": "^5.2.0",
    "eslint-plugin-promise": "^3.4.0",
    "eslint-plugin-standard": "^3.0.1",
    "eslint-plugin-vue": "^4.0.0",
    "extract-text-webpack-plugin": "^3.0.0",
    "file-loader": "^1.1.4",
    "friendly-errors-webpack-plugin": "^1.6.1",
    "happypack": "^5.0.0",
    "html-webpack-plugin": "^2.30.1",
    "jest": "^22.0.4",
    "jest-serializer-vue": "^0.3.0",
    "nightwatch": "^0.9.12",
    "node-notifier": "^5.1.2",
    "node-sass": "^4.9.3",
    "optimize-css-assets-webpack-plugin": "^3.2.0",
    "ora": "^1.2.0",
    "portfinder": "^1.0.13",
    "postcss-import": "^11.0.0",
    "postcss-loader": "^2.0.8",
    "postcss-url": "^7.2.1",
    "purify-css": "^1.2.5",
    "purifycss-webpack": "^0.7.0",
    "rimraf": "^2.6.0",
    "sass-loader": "^7.1.0",
    "selenium-server": "^3.0.1",
    "semver": "^5.3.0",
    "shelljs": "^0.7.6",
    "style-loader": "^0.22.0",
    "uglifyjs-webpack-plugin": "^1.1.1",
    "url-loader": "^0.5.8",
    "vue-jest": "^1.0.2",
    "vue-loader": "^13.3.0",
    "vue-style-loader": "^3.0.1",
    "vue-template-compiler": "^2.5.2",
    "webpack": "^3.12.0",
    "webpack-bundle-analyzer": "^2.9.0",
    "webpack-dev-server": "^2.9.1",
    "webpack-merge": "^4.1.0"
  },
  "engines": {
    "node": ">= 6.0.0",
    "npm": ">= 3.0.0"
  },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not ie <= 8"
  ]
}
  • webpack.dll.conf.js
const path = require('path')
const webpack = require('webpack')
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
const pkg = require('./package.json')
const library = '[name]_lib'

module.exports = {
  entry: {
    vendors: Object.keys(pkg.dependencies),
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].dll.js',
    chunkFilename: '[name].chunk.js',
    // DllPlugin插件需要該配置參數,用來生成manifest的映射名稱
    library
  },
  plugins: [
    // 該插件將第三方靜態資源單獨打包處理,避免我們在各種環境中重複打包永遠不會變化的文件,優化效果非常明顯(需要配合webpack內置插件DllReferencePlugin一起使用)
    new webpack.DllPlugin({
      // 打包後的文件路徑
      path: path.resolve(__dirname, 'dist/[name]-manifest.json'),
      name: library
    }),
    // 壓縮js文件,不要使用webpack內置的壓縮插件,因爲其版本低,緩存和多線程壓縮配置項都不生效,這裏使用獨立的壓縮插件還是有效的,同時該插件在設置babel的module爲false時默認開啓js tree shaking功能
    new UglifyJsPlugin({
      // 開啓緩存
      cache: true,
      // 開啓多線程並行處理
      parallel: true,
    }),
  ]
}
  • webpack.conf.js
const path = require('path')
const webpack = require('webpack')
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin')
const CleanWebpackPlugin = require('clean-webpack-plugin')
const glob = require('glob')
const PurifyCSSPlugin = require('purifycss-webpack')
const os = require('os')
const HappyPack = require('happypack')
const threadPool = HappyPack.ThreadPool({ size: os.cpus().length })

// 樣式loader配置文件,因爲有多處用到,故抽離成方法以便調用,一個ExtractTextPlugin實例是allInOne模式的,即所有的css合併成一個css文件,如果想抽離成多個文件,需要生成多個ExtractTextPlugin實例分別調用,如果想再優化的話,可以配合DllPlugin插件(這裏我偷個懶,沒有分開配置css和scss,請看我對css文件的正則,導致css和scss都會走這個方法,其實css是不需要sass-loader的)
function generateLoaders() {
  return ExtractTextPlugin.extract({
    fallback: 'style-loader',
    use: ['css-loader', 'sass-loader'],
  })
}

module.exports = {
  // source map配置項,由於打包後的bundle是一個文件,出現js錯誤後,不能定位到具體的模塊文件,開啓此項,幫我們解決此問題,但是不同的source map模式處理速度不同,建議不同環境,選擇不同的模式
  // devtool: 'cheap-module-eval-source-map',
  entry: {
    app: './app.js',
    // app2: './app2.js',
    // app3: './app3.js',
    // vendors: ['vue', 'jquery'],
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name]-[hash:7].bundle.js',
    chunkFilename: '[name].chunk.js'
  },
  resolve: {
    extensions: ['.js', '.vue', '.json'],
    alias: {
      // vue別名,我們在使用npm安裝vue的時候,會安裝vue的各種版本,適用於不同的環境,由於在webpack2.x版本中我們都是使用ES6 Module規範,如import、export等關鍵字,所以我們需要配置能遵循ES6 Module規範的那個vue版本
      'vue$': 'vue/dist/vue.esm.js',
      // 文件夾前綴別名,在引用某個模塊時,我們需要寫出絕對路徑,很麻煩,有了這個別名,我們可以把絕對路徑的前綴使用@來代替
      '@': path.resolve(__dirname, './'),
    }
  },
  module: {
    rules: [
      {
        test: /\.vue$/,
        use: {
          loader: 'vue-loader',
          options: {
            loaders: {
              /* // 這裏可以省略,因爲vue文件中的js會自動根據.babelrc文件的配置去處理
              js: {
                loader: 'babel-loader',
                // options: '.babelrc文件內容拷貝至此亦可',
              }, */
              css: generateLoaders(),
              scss: generateLoaders(),
            }
          }
        },
      }, {
        test: /\.js$/,
        /* use: {
          // 將相應的id任務的loader交給happypack去處理,由於happypack可以開啓多個線程並行處理,可以優化速度,個人測試,效果並不理想,慎用,網上也看到很多人說使用happypack沒有效果,個人測試用例:對element-ui、jquery、vue、vue-router共4個模塊大約900kb,使用babel編譯,正常編譯和使用happypack編譯時間上並無什麼差別(happypack顯示開啓了4個線程,偶爾時間反而還會更多)
          loader: 'happypack/loader?id=babel',
        }, */
        use: {
          loader: 'babel-loader',
          /* // 使用babel-loader插件時,如果我們想要編譯es6爲低版本瀏覽器能兼容的es5的話,我們需要爲該loader配置一些參數,而且每個需要用到babel-loader插件的地方,都要在相應的地方寫配置參數,比如我們要編譯.vue文件中的<script>塊的es6語法時,需要用到vue-loader,在vue-loader配置參數中我們依然要寫一遍babel-loader的配置參數,顯得很繁瑣,因此我們把要寫的配置參數統統提取出來放到.babelrc文件中,所有要用到babel的地方,在編譯時都會自動去查詢有沒有這個文件存在,而且慶幸的是這個文件支持JSON5格式,即我們在這個文件中可以隨心所欲的寫單引號、加單行、多行註釋、所有的鍵都可以不加雙引號,就像寫一個普通的js對象一樣,而不是寫一個格式要求嚴格的JSON文件
          options: '.babelrc文件內容拷貝至此亦可', */
        },
        exclude: '/node_modules'
      }, {// 開啓模塊熱更新後,style-loader會自動幫我們處理css模塊並實現模塊局部更新,但是由於我們又使用了ExtractTextPlugin插件抽離css樣式爲單獨的文件,所以這時css熱更新會失效(該插件缺少處理熱更新的api),當然我們可以不用ExtractTextPlugin插件,這樣一來我們引入的樣式文件都是以style標籤的形式插入html中,很不友好,爲此網上有人專門寫了一個CSS Hot Loader插件,使我們在使用ExtractTextPlugin插件的同時又能實現css模塊熱更新功能(總結:開發模式只需要熱更新,提高編譯速度,推薦使用寫法1,生產模式只需要提取css文件,推薦使用寫法2)
        test: /\.s?css$/,
        /* // css loader寫法1(此寫法需主動把plugins配置插件處的ExtractTextPlugin的代碼註釋)
        // 最簡單的處理css的loader寫法,hot爲true時,css熱更新生效,但是不能抽離css爲單獨的樣式文件
        use: [
          { loader: 'style-loader' },
          { loader: 'css-loader' },
        ], */
        // css loader寫法2
        // hot爲true時,css熱更新失效,但是能抽離css爲單獨的樣式文件
        use: generateLoaders(),
        /* // css loader寫法3(個人測試結果,該css-hot-loader的css熱更新效果並不理想,慎用)
        // hot爲true時,css熱更新生效,同時能抽離css爲單獨的樣式文件
        use: ['css-hot-loader'].concat(generateLoaders()), */
      }, {
        test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
        loader: 'url-loader',
        options: {
          // 當圖片文件的大小比limit大時,會把文件交給file-loader去處理,反之,則自己處理並返回該文件的base64字符串,我個人認爲base64的利大於弊,在這裏我設爲-1,表示任何時候都不使用base64(base64優點:減少http請求、字符串可以使用gzip;缺點:一般圖片轉爲base64後,總字符串的體積會變大,不可讀,並且顯得冗餘,還會增加CSSOM解析時間,我比較傾向的是使用cssSprites的方案來合併小圖標)
          limit: -1,
          name: './dist/img/[name].[hash:7].[ext]',
        }
      },
    ]
  },
  plugins: [
    /* // 打包結果可視化分析,該插件會自動打開一個窗口,展示打包後的結果,我們可以根據最終打包的模塊依賴做分析、優化工作
    new BundleAnalyzerPlugin(), */
    // 該插件將第三方靜態資源單獨打包處理,避免我們在各種環境中重複打包永遠不會變化的文件,優化效果非常明顯(需要配合webpack內置插件DllPlugin一起使用)
    new webpack.DllReferencePlugin({
      manifest: require('./dist/vendors-manifest.json')
    }),
    // 生成頁面插件,可以自定義模板、頁面title,寫多個即表示生成多頁面
    new HtmlWebpackPlugin({
      template: 'test.html',
    }),
    // 生成script標籤引用指定路徑,以實現自動添加靜態資源到頁面中,由於我們使用了DllReferencePlugin,所以我們需要手動把打包後的資源路徑添加到頁面中,而該插件幫我們自動完成
    new AddAssetHtmlPlugin({
      filepath: path.resolve(__dirname, 'dist/*.dll.js'),
      includeSourcemap: false,
      // 文件名帶有隨機hash值,防止緩存,不能設置hash值長度,真的難受
      hash: true,
    }),
    // 提取css樣式爲單獨的文件
    new ExtractTextPlugin({
      // 多入口時,必需使用name或contenthash值來確定打包出的css文件名,這樣提取出來的多個css文件就會以不同的名字命名,避免相互覆蓋
      filename: '[name]-[contenthash:7].css',
      allChunks: true,
    }),
    /* // 清理無用css,即css tree shaking,比如dom樹中壓根沒有這個節點,但是我們卻寫了這個節點的樣式,這個插件會幫我們除掉沒有用的css樣式,個人測試,效果並不理想,慎用
    new PurifyCSSPlugin({
      // 個人測試,只使用bootstrap的分頁樣式(其他的模塊也有問題),進行shaking後,css文件確實小了很多,但是展示的分頁樣式跟用shaking之前差了很多
      paths: glob.sync(path.resolve(__dirname, './test.html')),
    }), */
    // 提取公用代碼,webpack的一個優化點,比如某個模塊我們在不同的入口分別引入了1次,那麼最終打包的多個bundle中都會有該模塊的代碼,增大了代碼體積,使用此插件我們可以從多個bundle中提取公用的模塊,減少代碼體積,但是注意提取過程需要時間,所以建議開發環境下開啓
    new webpack.optimize.CommonsChunkPlugin({
      // 提取出來的公用代碼的名稱
      name: 'common',
      // 配置幾個入口都引入該模塊時纔去提取,該值比較重要,決定了最終提取的公用代碼的體積和無用代碼量(無用代碼量:如果你設爲1,那麼只要某個模塊被某個入口引入了1次,該模塊就會被提取,但是其他的入口可能並不需要這個模塊,那麼就會成爲無用代碼)
      minChunks: 2,
    }),
    // 注入全局變量,相當於使用別名來代替每次手動引入某個模塊,如以下,我們在文件中就不需要再手動書寫'import $ from \'jquery\''了
    new webpack.ProvidePlugin({
      $: 'jquery',
    }),
    /* // 該插件讓webpack能同時使用多個線程去處理loader,個人測試,效果並不理想,慎用
    new HappyPack({
      // 處理的任務id
      id: 'babel',
      loaders: [{
        loader: 'babel-loader',
        options: {
          presets: [
            ['babel-preset-env', {
              targets: {
                browsers: ['last 2 versions']
              }
            }]
          ]
        }
      }],
      // 使用的線程數
      threadPool
    }), */
    // 在打包前對指定文件夾清理,由於我們加了hash值來命名文件,每次文件改動後,會重新生成新的文件,所以需要定時清理文件夾,開發環境不需要使用
    new CleanWebpackPlugin(['dist'], {
      // 不寫,也不會報錯
      root: __dirname,
      // 清理時,排除某些文件,比如我們的第三方依賴庫,從來沒有改變過,所以不需要清理
      exclude: ['vendors.dll.js', 'vendors-manifest.json'],
    }),
    // hot爲true時,必須使用該插件熱更新才能生效
    new webpack.HotModuleReplacementPlugin(),
    // 熱更新時,輸出本次熱更新的模塊相對路徑
    new webpack.NamedModulesPlugin(),
  ],
  devServer: {// 當使用webpack-dev-server模塊來啓動項目時,該配置項生效,會開啓一個node服務器
    // 開啓熱更新
    hot: true,
    // 路由錯誤處理,當我們訪問一個不存在的路由時,express會報404,該插件在出現此情況時,重定向到某個自己寫的頁面,更友好的提醒開發者
    historyApiFallback: {
      rewrites: [
        {
          from: /./,
          to: '/404.html'
        }
      ]
    }
  }
}

細節整理

  • extract-text-webpack-plugin需要配合html-webpack-plugin使用才能生效
  • url-loader需要配合file-loader使用才能生效,否則讀取大圖片的時候會報錯(最新版的url-loader已經內置了file-loader)
  • CommonsChunkPlugin提取出來的文件,必須要在HtmlWebpackPlugin裏面引用,否則報錯webpackJsonp is not defined?
  • Babel其實是幾個模塊化的包,其核心功能位於稱爲babel-core的npm包中,不過webpack把它們整合在一起使用,但是對於每一個你需要的功能或拓展,你都需要安裝單獨的包(用得最多的是解析Es6的babel-preset-es2015包和解析JSX的babel-preset-react包)。

參考文檔

發佈了221 篇原創文章 · 獲贊 155 · 訪問量 71萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章