1. 程式人生 > 實用技巧 >java的23種設計模式之開閉原則

java的23種設計模式之開閉原則

前言

此係列作為筆者之前發過的前端高頻面試整理的補充 會比較偏向中高前端面試問題 當然大家都是從新手一路走過來的 感興趣的朋友們都可以看哈

吐槽:提到 webpack 真是一把傷心淚 還記得 weboack1.0 版本剛出來的時候 我按照文件擼了一遍配置項 和官網一樣的配置就是各種報錯 看都看不懂 那時候加上剛做前端不久 真是 webpack 從入門到放棄 尤其是現在面試前端高階職位時 你會發現面試官特別喜歡問 webpack 相關的東西 我見過的幾個大廠面試 自我介紹完了就開始擼 webpack 了 哈哈(也許是我簡歷有寫一些架構經驗

改觀:目前 webpack 已經到了 4.0 版本 官方文件和很多東西都進行了優化 現在使用更簡單 加上各種腳手架基本幫我們配置好了 webpack 但是這並不代表我們不需要知道 webpack 原理 因為站在前端架構的角度去分析 前端自動化打包這一塊是必須要精通的 同時作為中高階前端 對於前端工具也是必須要牢牢掌握的

目錄[隱藏]

1 webpack 是什麼

webpack 是自動化打包解決方案,也可以理解為是一個模組打包機。它幫助我們分析專案結構,找到 JavaScript 模組以及其它的一些瀏覽器不能直接執行的拓展語言(Scss,TypeScript 等),並將其打包為合適的格式以供瀏覽器使用。

如果沒有 webpack 我們手動去處理上訴的事情該多麻煩

2 webpack 常見有哪些配置

  1. Entry:入口,Webpack 執行構建的第一步將從 Entry 開始,可抽象成輸入。
  2. Output:輸出結果,在 Webpack 經過一系列處理並得出最終想要的程式碼後輸出結果。
  3. mode:提供 mode 配置選項,告知 webpack 使用相應模式的內建優化
  4. Module:模組,在 Webpack 裡一切皆模組,一個模組對應著一個檔案。
  5. Chunk:程式碼塊,一個 Chunk 由多個模組組合而成,用於程式碼合併與分割。
  6. Loader:模組轉換器,用於把模組原內容按照需求轉換成新內容。
  7. Plugin:擴充套件外掛,在 Webpack 構建流程中的特定時機注入擴充套件邏輯來改變構建結果或做你想要的事情。

3 webpack 工作流程

  1. 引數解析:從配置檔案和 Shell 語句中讀取與合併引數,得出最終的引數
  2. 找到入口檔案:從 Entry 裡配置的 Module 開始遞迴解析 Entry 依賴的所有 Module
  3. 呼叫 Loader 編譯檔案:每找到一個 Module, 就會根據配置的 Loader 去找出對應的轉換規則
  4. 遍歷 AST,收集依賴:對 Module 進行轉換後,再解析出當前 Module 依賴的 Module
  5. 生成 Chunk:這些模組會以 Entry 為單位進行分組,一個 Entry 和其所有依賴的 Module 被分到一個組也就是一個 Chunk
  6. 輸出檔案:最後 Webpack 會把所有 Chunk 轉換成檔案輸出

想看具體的 webpack 編譯原理原始碼的同學傳送門

4 常見 Loader 配置以及工作流程

// webpack.config.js
module.exports = {
  module: {
    rules: [
     {
        test: /\.vue$/,
        loader: 'vue-loader'
      },
      { test: /\.js$/, use: 'babel-loader' },
      {
        test: /\.css$/,
        use: [
          { loader: 'style-loader' },
          { loader: 'css-loader' },
          { loader: 'postcss-loader' },
        ]
      }
    ]
  }
};

Loader 工作流程

  1. webpack.config.js 裡配置了一個 模組 的 Loader;
  2. 遇到 相應模組 檔案時,觸發了 該模組的 loader;
  3. loader 接受了一個表示該 模組 檔案內容的 source;
  4. loader 使用 webapck 提供的一系列 api 對 source 進行轉換,得到一個 result;
  5. 將 result 返回或者傳遞給下一個 Loader,直到處理完畢。

看看 less-loader 的例子(具體的 loader 實現可以看官網例子

let less = require('less');
module.exports = function (source) {
    const callback = this.async();
    //this.async() 返回一個回撥函式,用於非同步執行
    less.render(source, (err, result) => {
    //使用less處理對應的less檔案的source
        callback(err, result.css);
    });
}

5 常見 plugin 配置以及簡易原理

專案常用外掛介紹

  1. extract-text-webpack-plugin

webpack 預設會將 css 當做一個模組打包到一個 chunk 中,extract-text-webpack-plugin 的作用就是將 css 提取成獨立的 css 檔案

const ExtractTextPlugin = require('extract-text-webpack-plugin');
new ExtractTextPlugin({
    filename: 'css/[name].css',
})
{
    test: /\.css$/,
    use: ExtractTextPlugin.extract({
        use: ['css-loader','postcss-loader','less-loader'],
        fallback: 'vue-style-loader',  #使用vue時要用這個配置
    })
},
  1. html-webpack-plugin
    這個外掛很重要,作用一是建立 HTML 頁面檔案到你的輸出目錄,作用二是將 webpack 打包後的 chunk 自動引入到這個 HTML 中
const HtmlPlugin = require('html-webpack-plugin')
new HtmlPlugin({
    filename: 'index.html',
    template: 'pages/index.html'
}
  1. DefinePlugin
    定義全域性常量
new webpack.DefinePlugin({
    'process.env': {
        NODE_ENV: JSON.stringify(process.env.NODE_ENV)
    },
    PRODUCTION: JSON.stringify(PRODUCTION),
    APP_CONFIG: JSON.stringify(appConfig[process.env.NODE_ENV]),
}),
  1. UglifyJsPlugin
    js 壓縮
new webpack.optimize.UglifyJsPlugin()

注意: webpack4 已經移除了該外掛,用 optimization.minimize 替代

  1. CommonsChunkPlugin
    CommonsChunkPlugin 主要是用來提取第三方庫(如 jQuery)和公共模組(公共 js,css 都可以),常用於多頁面應用程式,生成公共 chunk,避免重複引用。
{
    entry: {
        vendor: 'index.js'
    },
    plugins: [
        new webpack.optimize.CommonsChunkPlugin({
            name: ['vendor','runtime'],
            filename: '[name].js'
        }),
    ]
}

注意: webpack4 已經移除了該外掛,用 optimization.SplitChunks 替代

簡易原理

外掛就像是一個插入到生產線中的一個功能,在特定的時機對生產線上的資源做處理。webpack 通過 Tapable 來組織這條複雜的生產線。 webpack 在編譯過程式碼程中,會觸發一系列 Tapable 鉤子事件,外掛所做的,就是找到相應的鉤子,往上面掛上自己的任務,也就是註冊事件,這樣,當 webpack 構建的時候,外掛註冊的事件就會隨著鉤子的觸發而執行了。

webpack 外掛由以下組成:

  • 一個 JavaScript 命名函式。
  • 在外掛函式的 prototype 上定義一個 apply 方法。
  • 指定一個繫結到 webpack 自身的事件鉤子。
  • 處理 webpack 內部例項的特定資料。
  • 功能完成後呼叫 webpack 提供的回撥

自定義外掛例子(更多知識請看官網建立外掛

// 一個 JavaScript 命名函式。
function MyExampleWebpackPlugin() {
};
// 在外掛函式的 prototype 上定義一個 `apply` 方法。
MyExampleWebpackPlugin.prototype.apply = function(compiler) {
  // 指定一個掛載到 webpack 自身的事件鉤子。
  compiler.plugin('webpacksEventHook', function(compilation /* 處理 webpack 內部例項的特定資料。*/, callback) {
    console.log("This is an example plugin!!!");
    // 功能完成後呼叫 webpack 提供的回撥。
    callback();
  });
};

6 webpack 打包速度太慢怎麼辦

  1. 縮小編譯範圍,減少不必要的編譯工作,即 modules、mainFields、noParse、includes、exclude、alias 全部用起來。
const resolve = dir => path.join(__dirname, '..', dir);
resolve: {
    modules: [ // 指定以下目錄尋找第三方模組,避免webpack往父級目錄遞迴搜尋
        resolve('src'),
        resolve('node_modules'),
        resolve(config.common.layoutPath)
    ],
    mainFields: ['main'], // 只採用main欄位作為入口檔案描述欄位,減少搜尋步驟
    alias: {
        vue$: "vue/dist/vue.common",
        "@": resolve("src") // 快取src目錄為@符號,避免重複定址
    }
},
module: {
    noParse: /jquery|lodash/, // 忽略未採用模組化的檔案,因此jquery或lodash將不會被下面的loaders解析
    // noParse: function(content) {
    //     return /jquery|lodash/.test(content)
    // },
    rules: [
        {
            test: /\.js$/,
            include: [ // 表示只解析以下目錄,減少loader處理範圍
                resolve("src"),
                resolve(config.common.layoutPath)
            ],
            exclude: file => /test/.test(file), // 排除test目錄檔案
            loader: "happypack/loader?id=happy-babel" // 後面會介紹
        },
    ]
}
  1. webpack-parallel-uglify-plugin 外掛(優化 js 壓縮過程)

webpack-parallel-uglify-plugin 能夠把任務分解給多個子程序去併發的執行,子程序處理完後再把結果傳送給主程序,從而實現併發編譯,進而大幅提升 js 壓縮速度

const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');
// ...
optimization: {
    minimizer: [
        new ParallelUglifyPlugin({ // 多程序壓縮
            cacheDir: '.cache/',
            uglifyJS: {
                output: {
                    comments: false,
                    beautify: false
                },
                compress: {
                    warnings: false,
                    drop_console: true,
                    collapse_vars: true,
                    reduce_vars: true
                }
            }
        }),
    ]
}
  1. HappyPack
    在 webpack 執行在 node 中打包的時候是單執行緒去一件一件事情的做,HappyPack 可以開啟多個子程序去併發執行,子程序處理完後把結果交給主程序
const HappyPack = require('happypack');
module.exports = {
	entry: './src/index.js',
	output: {
		path: path.join(__dirname, './dist'),
		filename: 'main.js',
    },
    module: {
        rules: [
            {
                test: /\.jsx?$/,
                exclude: /node_modules/,
                use: 'happypack/loader?id=babel',
            },
        ]
    },
    plugins: [
        new HappyPack({
            id: 'babel',  //id值,與loader配置項對應
            threads: 4,  //配置多少個子程序
            loaders: ['babel-loader']  //用什麼loader處理
        }),
    ]
}
  1. DLL 動態連結

第三方庫不是經常更新,打包的時候希望分開打包,來提升打包速度。打包 dll 需要新建一個 webpack 配置檔案(webpack.dll.config.js),在打包 dll 的時候,webpack 做一個索引,寫在 manifest 檔案中。然後打包專案檔案時只需要讀取 manifest 檔案。

const webpack = require("webpack");
const path = require('path');
const CleanWebpackPlugin = require("clean-webpack-plugin");
const dllPath = path.resolve(__dirname, "../src/assets/dll"); // dll檔案存放的目錄
module.exports = {
    entry: {
        // 把 vue 相關模組的放到一個單獨的動態連結庫
        vue: ["babel-polyfill", "fastclick", "vue", "vue-router", "vuex", "axios", "element-ui"]
    },
    output: {
        filename: "[name]-[hash].dll.js", // 生成vue.dll.js
        path: dllPath,
        library: "_dll_[name]"
    },
    plugins: [
        new CleanWebpackPlugin(["*.js"], { // 清除之前的dll檔案
            root: dllPath,
        }),
        new webpack.DllPlugin({
            name: "_dll_[name]",
            // manifest.json 描述動態連結庫包含了哪些內容
            path: path.join(__dirname, "./", "[name].dll.manifest.json")
        }),
    ],
};

接著, 需要在 package.json 中新增 dll 命令。

"scripts": {
    "dll": "webpack --mode production --config build/webpack.dll.config.js"
}

執行 npm run dll 後,會生成 ./src/assets/dll/vue.dll-[hash].js 公共 js 和 ./build/vue.dll.manifest.json 資源說明檔案,至此 dll 準備工作完成,接下來在 webpack 中引用即可。

externals: {
    'vue': 'Vue',
    'vue-router': 'VueRouter',
    'vuex': 'vuex',
    'elemenct-ui': 'ELEMENT',
    'axios': 'axios',
    'fastclick': 'FastClick'
},
plugins: [
    ...(config.common.needDll ? [
        new webpack.DllReferencePlugin({
            manifest: require("./vue.dll.manifest.json")
        })
    ] : [])
]

7 webpack 如何優化前端效能

  1. 第三方庫按需載入、路由懶載入
//第三方ui庫element,vant等庫都提供來按需載入的方式,避免全部引入,加大專案體積
import { Button, Select } from 'element-ui';
//路由懶載入
const showImage = () => import('@/components/common/showImage');
  1. 程式碼分割
  • 提取第三方庫vendor
module.exports = {
    entry: {
        main: './src/index.js',
        vendor: ['react', 'react-dom'],
    },
}
  • 依賴庫分離splitChunks
optimization: {
  splitChunks: {
     chunks: "async", // 必須三選一: "initial" | "all"(推薦) | "async" (預設就是async)
     minSize: 30000, // 最小尺寸,30000
     minChunks: 1, // 最小 chunk ,預設1
     maxAsyncRequests: 5, // 最大非同步請求數, 預設5
     maxInitialRequests : 3, // 最大初始化請求書,預設3
     automaticNameDelimiter: '~',// 打包分隔符
     name: function(){}, // 打包後的名稱,此選項可接收 function
     cacheGroups:{ // 這裡開始設定快取的 chunks
         priority: 0, // 快取組優先順序
         vendor: { // key 為entry中定義的 入口名稱
             chunks: "initial", // 必須三選一: "initial" | "all" | "async"(預設就是async)
             test: /react|lodash/, // 正則規則驗證,如果符合就提取 chunk
             name: "vendor", // 要快取的 分隔出來的 chunk 名稱
             minSize: 30000,
             minChunks: 1,
             enforce: true,
             maxAsyncRequests: 5, // 最大非同步請求數, 預設1
             maxInitialRequests : 3, // 最大初始化請求書,預設1
             reuseExistingChunk: true // 可設定是否重用該chunk
         }
     }
  }
 },
  1. 刪除冗餘程式碼

Tree-Shaking

https://juejin.im/post/5eb502de6fb9a0437c393979