整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          多頁面項目webpack打包實踐

          多頁面項目webpack打包實踐

          在最近的項目開發中,涉及到了多頁面的 webpack 打包,以下是我項目過程中的一些踩坑總結。

          前言

          項目使用了 vue 作為框架來開發前端頁面,其中需要開發多個前端頁面,包括有登錄、進游戲、充值等等。作為vue最佳的打包工具—— webpack,需要將各個頁面分別打包到不同模板目錄里。

          但默認的 vue 項目框架是單頁面應用的,并不能達到項目開發的目的。這就需要調整 webpack 的配置來實現多頁面的發布處理。

          以下是目錄結構:

          project
          ├───bin
          │   └───vb.js
          ├───build
          │   │   dev.js
          │   │   release.js
          │   │   webpack.config.base.js
          │   │   webpack.config.build.js
          │   └───webpack.config.dev.js
          │   README.md
          │   package.json
          └───src
              ├───components
              │   │   count.vue
              │   │   dialog.vue
              │   │   errortips.vue
              │   └───...
              ├───game
              │   │   game.htm
              │   │   game.js
              │   └───game.vue
              ├───login
              │   │   login.htm
              │   │   login.js
              │   └───login.vue
              ├───pay
              │   │   pay_result.htm
              │   │   pay_result.js
              │   │   pay_result.vue
              │   │   pay.htm
              │   │   pay.js
              │   └───pay.vue
              └───...


          修改配置前的一些知識

          我們知道webpack的核心是一切皆模塊,所以它本質上是一個靜態模塊打包器。當 webpack 處理應用程序時,它會遞歸地構建一個依賴關系圖,其中包含應用程序需要的每個模塊,然后將所有這些模塊打包成一個或多個 bundle。

          官網顯示的這幅圖很形象地描述了這個過程。


          從 webpack v4.0.0 開始,webpack 提供了一系列的配置默認項,讓開發者可以零配置打包,不再強制要求必須進行繁瑣的 webpack 配置,讓開發者可以從繁瑣的配置文件里抽出,專注應用的開發。但是若你需要有特殊的處理,webpack 仍然可以進行高度可配置來滿足你的需求。

          在開始前需要了解四個核心概念:

          • 入口(entry):指示 webpack 應該使用哪個模塊,來作為構建其內部依賴圖的開始。


          • 輸出(output):指示 webpack 在哪里輸出它所創建的 bundles,以及如何命名這些文件


          • loader:讓 webpack 能夠去處理那些非 JavaScript 文件


          • 插件(plugins):可以讓 webpack 執行范圍更廣的任務。


          本篇將會針對這4個核心配置的修改和優化來實現多頁面打包。在 webpack4 的版本,還新增了一個 mode 配置項。mode 有兩個值:development 或者是 production,用戶可以啟用相應模式下的 webpack 內置的優化。不同 mode 的區別與默認配置可以參考:https://segmentfault.com/a/1190000013712229


          一、入口配置

          在單頁面應用里,一般在根目錄下面會有一個 index.html 文件。它是頁面的 html 模板文件。但是在多頁面應用里,則會有多個應用模板文件,為了方便管理,可以將不同類的入口文件、邏輯處理和模板文件分別存放在相應的獨立目錄。若用到了組件,則單獨將組件存放在一個目錄。

          project
          └───src
              ├───components
              │   │   count.vue
              │   │   dialog.vue
              │   │   errortips.vue
              │   └───...
              ├───game
              │   │   game.htm
              │   │   game.js
              │   └───game.vue
              ├───login
              │   │   login.htm
              │   │   login.js
              │   └───login.vue
              ├───pay
              │   │   pay_result.htm
              │   │   pay_result.js
              │   │   pay_result.vue
              │   │   pay.htm
              │   │   pay.js
              │   └───pay.vue
              └───...


          webpack 的入口配置中是支持多入口的,給 entry 傳入對象即可,如下所示:

          const config={
            entry: {
              game: './src/game/game.js',
              login: './src/login/login.js',
              pay: './src/pay/pay.js',
              pay_result: './src/pay/pay_result.js'
            }
          };

          但這樣的配置對于未知頁面數量的項目并不友好,若每新增頁面都要重新配置和重啟程序,顯然是不合理的。而我們可以創建一個getEntry()的方法來遍歷文件夾來獲取入口。

          const fs=require('fs');
          const glob=require("glob"); 
          function getEntry() {
              const entry={};
              //讀取src目錄所有page入口
              glob.sync('./src/*/*.js') //獲取符合正則的文件數組
                  .forEach(function (filePath) {
                      var name=filePath.match(/\/src\/(.+)\/*.js/);
                      name=name[1];
                      //須有配套的模板文件才認為是入口
                      if (!fs.existsSync('./src/' + name + '.htm')) {
                          return;
                      }
                      entry[name]=filePath;
                  });
              return entry;
          };
          module.exports={
            // 多入口
            entry: getEntry(),
          }


          二、輸出配置

          輸出配置僅需指定一個

          const config={
              output: {
                  path: path.join(__projectDir, __setting.distJs),
                  publicPath: __setting.domainJs, //自定義變量,用來定義公共靜態資源路徑
                  filename: '[name][hash].js'
              },
          };
          • path:目標輸出目錄的絕對路徑
          • publicPath:文件中靜態資源的引用路徑

          https://www.webpackjs.com/configuration/output/#output-publicpath

          • filename:用于輸出文件的文件名

          https://www.webpackjs.com/configuration/output/#output-filename

          在配置中有以下幾點需要注意:

          • publicPath

          publicPath 是指定在瀏覽器中所引用的「此輸出目錄對應的公開 URL」。

          簡單的例子:

          publicPath: "https://cdn.example.com/assets/"

          輸出到html則變成

          <script src="https://cdn.example.com/assets/bundle.js"></script>

          這個屬性是整個項目共用的靜態資源路徑,若某個模塊需要使用其他的靜態資源路徑。webpack 提供了__webpack_public_path__來動態設置 publicPath,只需在入口文件的最頂部定義即可。

          __webpack_public_path__=myRuntimePublicPath; // 一定要寫在最頂部
          • hash

          filename的[hash]是以項目為維度的 hash 值,若輸出了多個文件,則文件名都會共用一個 hash 值。

          filename的[chunkhash]是以chunk為維度生成的 hash 值,不同入口生成不同的 chunkhash 值。

          filename的[contenthash]根據資源內容生成的 hash 值。

          通常使用 hash 或 chunkhash,contenthash 通常用于某些特殊場景(官方文檔在使用 ExtractTextWebpackPlugin 插件時有使用)。

          https://www.webpackjs.com/plugins/extract-text-webpack-plugin/


          三、loader配置

          由于 webpack 只能理解 JavaScript 和 JSON 文件。而配置 loader 就是讓 webpack 能夠去處理其他類型的文件,并將它們轉換為有效模塊。

          loader 可以使開發者在 import 或"加載"模塊時預處理文件。例如,將內聯圖像轉換為 data URL,或者允許開發者直接在 JavaScript 模塊中 import CSS文件

          1、js 模塊

          加載js模塊,我們通常是為了引入babel,讓其能將ES6的語法轉成ES5,讓項目能在低版本的瀏覽器上運行。

          js文件需要使用babel的話,引入babel-loader

          const config={
              module: {
                  rules: [{
                      test: /\.js$/,
                      include: [path.resolve(__projectDir, 'src')], //通過include精確指定只處理哪些目錄下的文件
                      exclude: /node_modules/, //設置哪些目錄里的文件不進行處理
                      loader: "babel-loader"
                  }]
              }
          }

          但僅僅配置了babel-loader還不夠,還需要配置 babel 的環境,需要引入 polyfill。

          引入 polyfill 的方式有很多種,根據 vue 官方文檔在瀏覽器兼容性的處理,默認使用的是@vue/babel-preset-app ,它通過@babel/preset-env和browserslist配置來決定項目需要的 polyfill。

          https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/babel-preset-app


          browserslist

          項目根目錄創建.browserslist文件

          > 1%
          last 2 versions

          當然,你也可以在package.json文件里添加的browserslist字段來配置。

          這個配置的目的是為了指定了項目的目標瀏覽器的范圍,配置的值會被 @babel/preset-env 用來確定需要轉譯的 JavaScript 特性。

          詳細的說明可以查閱 https://github.com/browserslist/browserslist,了解如何指定瀏覽器范圍。

          Polyfill

          項目根目錄創建.babelrc文件

          {
            "presets": [
              ["@babel/preset-env",
                {
                  "modules": false, // 對ES6的模塊文件不做轉化,以便使用tree shaking、sideEffects等
                  "useBuiltIns": "entry", // browserslist環境不支持的所有墊片都導入
                  "corejs": {
                    "version": 3, // 使用core-js@3
                    "proposals": true
                  }
                }
              ]
            ]
          }

          這里特別說下的是配置里的useBuiltIns,可設置的值分別是"usage" | "entry" | false,3個值分別代表:

          • usage 會根據配置的瀏覽器兼容,以及你代碼中用到的 API 來進行 polyfill,實現了按需添加。
          • entry 根據配置的瀏覽器兼容,引入瀏覽器不兼容的 polyfill。這時會自動根據 browserslist 替換成瀏覽器不兼容的所有 polyfill。
          • false 此時不對 polyfill 做操作。如果引入@babel/polyfill,則無視配置的瀏覽器兼容,引入所有的 polyfill。

          項目使用的是"useBuiltIns": "entry",所以需要指定corejs的版本,這里使用的版本是core-js@3,所以我們在 webpack 的入口配置里加上"core-js/stable"和 "regenerator-runtime/runtime"。

          function getEntry() {
              const entry={};
              //讀取src目錄所有page入口
              glob.sync('./src/*/*.js') //獲取符合正則的文件數組
                  .forEach(function (filePath) {
                      var name=filePath.match(/\/src\/(.+)\/*.js/);
                      name=name[1];
                      //須有配套的模板文件才認為是入口
                      if (!fs.existsSync('./src/' + name + '.htm')) {
                          return;
                      }
                      entry[name]=["core-js/stable", "regenerator-runtime/runtime", path.join(__projectDir, filePath)];
                  });
              return entry;
          };


          2、css 模塊

          我們通常使用style-loader和css-loader。css-loader用來處理 js 文件中引入的 css 模塊(處理@import和url()),style-loader是將css-loader打包好的css代碼以<style>標簽的形式插入到 html 文件中。而 webpack 對于 loader 的調用是從右往左的,所以通常是這樣配置:

          {
              test: /\.css$/,
              use: [ 'style-loader', 'css-loader' ]
          }

          我們在項目中還經常會使用 sass 或者 scss。sass 是一種 CSS 的預編譯語言。因此 webpack 要將其處理會使用更多 loader。

          {
              test: /\.(sc|sa)ss$/,
              use: [{
                  loader: 'vue-style-loader'
              }, {
                  loader: 'css-loader',
                  options: {
                      sourceMap: true,
                  }
              }, {
                  loader: 'postcss-loader',
                  options: {
                      sourceMap: true
                  }
              }, {
                  loader: 'sass-loader',
                  options: {
                      sourceMap: true
                  }
              }, {
                  loader: 'sass-resources-loader', //組件里面使用全局scss
                  options: {
                      sourceMap: true,
                      resources: [
                          path.resolve('./src/public/css/common.scss')
                      ]
                  }
              }]
          }

          在使用sass-loader的時候若某個 scss 文件(比如a.scss)@import 了其他 scss 文件(比如b.scss),如果b.scss里的url()的路徑是相對路徑,在sass-loader處理過后給css-loader處理時就會報錯,找不到url()里指定的資源。

          這是因為sass-loader處理時,會將 scss 文件里 @import 路徑的文件一并合并進來,結合上面的例子就是b.scss會被sass-loader合并進a.scss。

          如何解決呢?可以有兩個解決方法:

          • 將資源路徑改為變量來統一管理
          • 通過 alias 設置路徑別名,從而便捷使用絕對路徑。注意在scss文件中使用 alias 里定義的路徑別名時,需要帶上~前綴,否則打包時仍會被識別為普通路徑。

          在項目中由于還用到了postcss-loader,我們還須要在根目錄創建postcss-loader的配置文件postcss.config.js

          //自動添加css瀏覽器前綴
          module.exports={
              plugins: [
                  require('autoprefixer')
              ]
          }


          3、圖片等靜態資源

          對于圖片資源的打包,經常會使用file-loader來完成,配置也很簡單:

          {
            test: /\.(gif|png|jpe?g)$/,
            loader: 'file-loader',
          }

          打包后,會將圖片移動到了 dist 目錄下,并將該圖片改名為[hash].[ext]格式的圖片。開發者也可以根據需要,修改輸出的文件名。

          但在項目開發過程中,我們會創建很多張圖片,這就使得頁面在加載是時候會發送很多http請求,當頁面圖片過多,會影響的頁面的性能。所以,這里推薦使用url-loader。

          {
              test: /\.(png|jpg|jepg|svg|gif)$/,
              use: [{
                  loader: 'url-loader',
                  options: {
                      limit: 10240, //這里的單位是b
                      name: 'image/[name][hash].[ext]' //打包后輸出路徑
                  }
              }]
          }

          使用url-loader我們可以通過設置limit的值,將文件大小小于某個值的圖片打包成base64的形式存放在打包后的 js 中,若超過了這個設定值,默認會使用file-loader(所以雖然代碼沒有配置 file-loader,但還是需要使用安裝file-loader),并且會將配置的選項傳遞給file-loader。


          4、import AMD 模塊

          有時我們需要在項目里使用一些 AMD 模塊或者完全不支持模塊化的庫。例如移動端經常使用的 zepto。如果我們直接使用 import zepto 的方式引入是會報錯的:Uncaught TypeError: Cannot read property 'createElement' of undefined

          要使用也很簡單,使用script-loader和exports-loader即可:

          {
              test: require.resolve('zepto'),
              use: ['exports-loader?window.Zepto','script-loader']
          }
          • script-loader 用 eval 的方法將 zepto 在引入的時候執行了一遍,此時 zepto 庫已存在于 window.Zepto
          • exports-loader 將傳入的 window.Zepto 以 module.exports=window.Zepto 的形式向外暴露接口,使這個模塊符合 CommonJS 規范,支持 import 這樣我們就可以直接import $ from 'zepto'了,其他 AMD 模塊或者其他不支持模塊化的庫也類似。



          四、plugins

          webpack 可以使用插件(plugins)來讓開發者能夠在打包的過程中實現更多功能,插件會在整個構建過程中生效,并執行相關的任務。這里會介紹幾個比較實用的插件:

          1、mini-css-extract-plugin

          在使用style-loader處理后,css 文件會作為模塊打包進 js 文件里。若我們想將 js 文件和 css 文件分離。就可以使用mini-css-extract-plugin:

          module: {
              rules: [{
                  test: /\.css$/,
                  use: [{
                      loader: MiniCssExtractPlugin.loader
                  },
                      'css-loader'
                  ]
              }]
          },
          plugins: [
              new MiniCssExtractPlugin({
                  filename: 'css/[hash].css'
              })
          ]


          2、copy-webpack-plugin

          有時候我們會有一些沒經過打包的文件需要復制到我們的生產目錄里,copy-webpack-plugin就可以實現這個功能。

          plugins: [
              new CopyWebpackPlugin([
                  {
                      from: { glob: './src/public/*.htm', dot: true },
                      to: path.join(__setting.distTpl, 'public','[name].htm')
                  }
              ], { copyUnmodified: true })
          ]


          3、html-webpack-plugin

          我們前面介紹入口配置的時候會看到只配置了 js 文件,只是因為 webpack 現在入口只支持 js 文件,所以打包輸出的也是 js 文件,那如果我們需要將 js 文件引入到 html 里,就需要使用到html-webpack-plugin插件。

          html-webpack-plugin在使用的時候,是必須一個入口對應一個配置的,所以我們前面使用了多頁面的配置,也需要進行相應的修改,修改后的getEntry方法:

          const htmlPluginArray=[];
          function getEntry() {
              const entry={};
              //讀取src目錄所有page入口
              glob.sync('./src/' + __setting.moduleId + '/*.js')
                  .forEach(function (filePath) {
                      var name=filePath.match(/\/src\/(.+)\/*.js/);
                      name=name[1];
                      if (!fs.existsSync(path.join(__projectDir, './src/' + name + '.htm'))) {
                          return;
                      }
                      entry[name]=["core-js/stable", "regenerator-runtime/runtime", path.join(__projectDir, filePath)];
          +           htmlPluginArray.push(new HtmlWebpackPlugin({
          +               filename: `${__setting.distTpl}/${name}.htm`,
          +               template: './src/' + name + '.htm',
          +               inject: 'body',
          +               minify: {
          +                   removeComments: true,
          +                   collapseWhitespace: true
          +               },
          +               chunks: [name],
          +               inlineSource: '.(js|css)'
          +           }))
                  });
              return entry;
          };
          
          
          // 配置plugin,由于plugins通常使用數組類型來配置,
          // 所以可以使用concat方法將配置好的html的數組添加進去。
          plugins: [
              new MiniCssExtractPlugin({
                  filename: 'css/[hash].css'
              })
          ].concat(htmlPluginArray),


          里面的一些配置是要注意一下的:

          • filename

          filename 是配置需要將 html 改成什么名字并輸出到哪里的配置。這里配置的的路徑是以 output 里配置的path為相對路徑的,我們上面 output 配置的是

          path: path.join(__projectDir, __setting.distJs)

          那最終的html輸出路徑就是

          path.join(__projectDir, __setting.distJs, 
          `${__setting.distTpl}/${name}.htm`)
          • minify

          是將html里的代碼進行壓縮。如果 minify 選項設置為 true 或者配置對象 ( true 是 webpack 模式為 production 時的默認值),生成的 HTML 將使用 HTML-minifier壓縮代碼,更多具體的配置可以看這里minification。

          • 其他

          template 生成 filename 文件的模版。重點:與 filename 的路徑不同, 當匹配模版路徑的時候將會從項目的根路徑開始。

          inject 制定 webpack 打包的 js css 靜態資源插入到 html 的位置。

          chunks 指定模板允許添加哪個入口文件。若不配置這個會將所有的入口都添加進來。


          4、html-webpack-inline-source-plugin

          若我們想將打包好的 js 代碼 inline 進 html 的話,就要使用到html-webpack-inline-source-plugin

          可以看到上面html-webpack-plugin的配置里有inlineSource: '.(js|css)'

          這就是告訴html-webpack-inline-source-plugin需要將打包好的代碼 inline 進 html 里,插件需要添加到html-webpack-plugin的配置后

          plugins: [
              new MiniCssExtractPlugin({
                  filename: 'css/[hash].css'
              })
          ].concat(htmlPluginArray).concat([
              new HtmlWebpackInlineSourcePlugin()
          ])

          但是html-webpack-inline-source-plugin也僅能將打包后輸出的 js 文件引入 html,若你想將 html 碼其他使用 script 標簽加載的 js 文件或者 style 標簽加載的 css 文件也 inline 進 html 里的話,html-webpack-inline-source-plugin并不能實現。從html-webpack-plugin里的 Issues 來看,html-webpack-plugin的作者也無意做這樣的事情,但也給出了建議,可以借助html-webpack-plugin插件的 hooks html-webpack-plugin-before-html-processing達到我們需要的效果。


          5、自定義插件

          上面說到要將外部的靜態文件也 inline 進 html,我們可以編寫自定義插件,借助html-webpack-plugin插件的 hooks html-webpack-plugin-before-html-processing,再結合inline-source組件來實現我們的功能。

          const {
              inlineSource
          }=require('inline-source');//加載inline-source組件
          //定義方法
          function scriptInlineHtml(options) {
              // Configure your plugin with options...
              this.options=options || {};
          }
          
          
          scriptInlineHtml.prototype.apply=function (compiler) {
              let that=this;
              (compiler.hooks ? //判斷webpack版本,4.0以上和4.0以下的處理不一樣
                  compiler.hooks.compilation.tap.bind(compiler.hooks.compilation, 'script-inline-html') :
                  compiler.plugin.bind(compiler, 'compilation'))(function (compilation) {
                      (compilation.hooks ?
                          compilation.hooks.htmlWebpackPluginBeforeHtmlProcessing.tapAsync.bind(compilation.hooks.htmlWebpackPluginBeforeHtmlProcessing, 'script-inline-html') :
                          compilation.plugin.bind(compilation, 'html-webpack-plugin-before-html-processing'))(async function (htmlPluginData, callback) {
          
          
                              //獲取的html內容處理后重新賦值;
                              try {
                                  htmlPluginData.html=await inlineSource(htmlPluginData.html, that.options);
                                  // Do something with html
                              } catch (err) {
                                  // Handle error
                              }
                              //繼續執行下個插件
                              callback(null, htmlPluginData);
                          });
                  });
          };
          //webpack插件添加
          plugins: [
              new MiniCssExtractPlugin({
                  filename: 'css/[hash].css'
              })
          ].concat(htmlPluginArray).concat([
              new scriptInlineHtml(),
              new HtmlWebpackInlineSourcePlugin()
          ])

          使用

          <script src="/src/public/js/px2rem.js" inline></script>

          這里結合 inline 靜態資源,簡單介紹了自定義插件的使用,在html-webpack-plugin構建 html 過程中,還提供其他一系列的事件。

          Async:

          • html-webpack-plugin-before-html-generation
          • html-webpack-plugin-before-html-processing
          • html-webpack-plugin-alter-asset-tags
          • html-webpack-plugin-after-html-processing
          • html-webpack-plugin-after-emit

          Sync:

          • html-webpack-plugin-alter-chunks

          這些事件可以讓我們在構建 html 的不同階段里,通過一些處理來達到我們的目的。例如:可以結合smarty.js將使用了 smarty 的模板,引入一些模擬數據后解析成正常的html代碼;讀取 HTML 文件進行翻譯文本的替換,實現頁面的多語言化。打包不同皮膚的html文件等等。



          五、其他配置

          1、resolve

          resolve 配置規定了 webpack 如何尋找各個依賴模塊。

          前面有講到使用 alias 設置路徑別名。在資源引用時,如果資源引用路徑太深,又比較常用,我們可以定義路徑別名,例如:

          resolve: {
              alias: {
                  '@': path.resolve(__projectDir, 'src')
              }
          }

          我們就可以直接在代碼中這樣引用了:

          let backimg=require("@/public/image/common/ico-back.png").default;


          2、webpack dev server

          webpack-dev-server是開發時的必備利器,它可以在本地起一個簡單的 web 服務器,當文件發生變化時,能夠實時重新加載。webpack-dev-server的配置也很簡單:

          devServer: {
              contentBase: __projectDir, //頁面的基礎目錄
              publicPath:'/',
              port: 8080,
              host: '127.0.0.1',
              open: true, //是否運行后自動打開瀏覽器
              hot: true
          }

          啟動 webpack-dev-server 后,在目標文件夾中是看不到編譯后的文件的,實時編譯后的文件都保存到了內存當中。

          1) HMR

          hot設置為 true 是啟用 webpack 的 模塊熱替換( HMR )功能,但這里注意必須要添加插件webpack.HotModuleReplacementPlugin 才能完全啟用 HMR

          2) publicPath

          publicPath 路徑下的打包文件可以在瀏覽器中訪問,webpack-dev-server 打包的內容是放在內存中的,并沒有實際創建文件,這些打包后的資源對外的的根目錄就是 publicPath。

          默認 devServer.publicPath 是 '/',所以你的包( bundle )可以通過 http://127.0.0.1:8080/bundle.js 訪問。注意:當這里的 publicPath 和 output 的 publicPath 同時設置時,這里的優先級更高。


          總結

          webpack 的配置能介紹的點其實還有很多,例如開發環境和生產環境進行配置分離;利用瀏覽器的緩存將公共的模塊抽離分開打包;還有很多常用 plugins 插件等等。

          這篇文章是以我在開發某個多頁面應用項目為例,總結了一些我在webpack配置上的理解。希望能對瀏覽這篇文章的小伙伴有幫助。



          作者:HZH

          來源-微信公眾號:三七互娛技術團隊

          出處:https://mp.weixin.qq.com/s/JzZDqe-f_NRMmdxDLXC7tQ

          問大家一個問題,曾經的你是否也遇到過,一個項目中有好幾個頁面長得基本相同,但又差那么一點,想用 vue extends 繼承它又不能按需繼承html模板部分,恰好 B 頁面需要用的 A 頁面 80% 的模板,剩下的 20% 由 B 頁面自定義,舉個栗子:

          我們假設這是兩個頁面,B頁面比A頁面多了個p標簽,剩余的東西都一樣,難道僅僅是因為這一個 p標簽就要重新寫一份模板嗎?相信大部分伙伴解決方式是把公共部分抽成一個組件來用,這是一個好的做法。沒錯,但是來了,老板讓你在 標題1、標題2下面分別插入一段內容,這會兒你是不是頭大了?難道只能重寫一份了嗎?當然不是,來開始我們的填坑之路~(當你的業務能用插槽或者組件抽離的方式固然更好,以下內容僅針對當你項目達到一定體量,vue老三套難以處理的情況下采用)

          準備工作

          準備以下工具包:

          • node-html-parser: 將html生成dom樹 官網
          npm install --save node-html-parser
          


          思路

          1. 子頁面提供繼承的父頁面的路徑,如下:
          <template extend="./xxx.vue">
          </template>
          


          1. 子頁面需要通過一個自定義標簽(假設是 extend)的方式,來決定如何拓展父頁面,如下就應該是一個替換的操作,它最少應該具備拓展類型 type 與目標節點 target 屬性。
          <template extend="./xxx.vue">
            <div>
              <extend type="replace" target="#div_1">
                <a>通過replace替換掉父頁面下id為div_1的元素 </a>
              </extend>
            </div>
          </template>
          


          最終它生成的應該是除了 id 為 div_1元素被<a>通過replace替換掉父頁面下id為div_1的元素 </a>替換掉之外,剩下的全部和xxx.vue一樣的頁面。

          梳理需求點

          子頁面繼承父頁面既可以完全繼承,也可以通過某種方式以父頁面為基板,對其進行增、刪、改。方便理解,我們先定義一個自定義標簽 extend,子頁面通過該標簽對其繼承的頁面操刀動手術,為了實現一個比較完善的繼承拓展,extend 標簽需要具備以下屬性:

          Extend Attributes

          參數

          說明

          類型

          可選值

          type

          指定擴展類型

          string

          insert(插入)、replace(替換)、remove(移除)、append(向子集追加)

          position

          指定插入的位置(僅在 type 取值 insert 時生效)

          string

          before(目標前)、after(目標后)

          指定插入的位置(僅在 type 取值 append 時生效,用于指定插入成為第幾個子節點)

          number

          -

          target

          指定擴展的目標

          string


          實現需求

          新建一個vue2的項目,項目結構如下:

          我們的繼承拓展通過自定義loader在編譯的時候實現,進入到src/loader/index.js

          const extend=require('./extend');
          module.exports=function (source) {
               // 當前模塊目錄
               const resourcePath=this.resourcePath;
               // 合并
               const result=new extend(source, resourcePath).mergePage();
               // console.log('result :>> ', result);
               // 返回合并后的內容
               this.callback(null, result);
          };
          


          實現繼承拓展主要邏輯代碼:src/loader/extend.js

          const parser=require('node-html-parser');
          const fs=require('fs');
          const pathFile=require('path');
          /**
           * 通過node-html-parser解析頁面文件重組模板
           * @param {String} source 頁面內容
           * @param {String} resourcePath 頁面目錄
           * @returns {String} 重組后的文件內容
           */
          class Extend {
              constructor(source, resourcePath) {
                  this.source=source;
                  this.resourcePath=resourcePath;
              }
              // 合并頁面
              mergePage() {
                  // 通過node-html-parser解析模板文件
                  const pageAst=parser.parse(this.source).removeWhitespace();
                  // 獲取template標簽extend屬性值
                  const extendPath=pageAst.querySelector('template').getAttribute('extend');
                  if (!extendPath) {
                      return pageAst.toString();
                  }
                  // extendPath文件內容
                  const extendContent=fs.readFileSync(pathFile.resolve(pathFile.dirname(this.resourcePath), extendPath), 'utf-8');
                  // extendContent文件解析
                  const extendAst=parser.parse(extendContent).removeWhitespace();
                  // 獲取頁面文件標簽為extend的元素
                  const extendElements=pageAst.querySelectorAll('extend');
          
                  extendElements.forEach((el)=> {
                      // 獲取對應屬性值
                      const type=el.getAttribute('type');
                      const target=el.getAttribute('target');
                      const position=parseInt(el.getAttribute('position'));
          
                      // 匹配模板符合target的元素
                      let templateElements=extendAst.querySelectorAll(target);
          
                      // type屬性為insert
                      if (type==='insert') {
                          templateElements.forEach((tel)=> {
                              // 通過position屬性判斷插入位置 默認為after
                              if (position==='before') {
                                  el.childNodes.forEach((child)=> {
                                      tel.insertAdjacentHTML('beforebegin', child.toString());
                                  });
                              } else {
                                  el.childNodes.forEach((child)=> {
                                      tel.insertAdjacentHTML('afterend', child.toString());
                                  });
                              }
                          });
                      }
                      // type屬性為append
                      if (type==='append') {
                          templateElements.forEach((tel)=> {
                             const elNodes=el.childNodes;
                             let tlNodes=tel.childNodes;
                             const len=tlNodes.filter((node)=> node.nodeType===1 || node.nodeType===3).length;
                              // 未傳position屬性或不為數字、大于len、小于0時默認插入到最后
                              if(isNaN(position) || position > len || position <=0){
                                  elNodes.forEach((child)=> {
                                      tel.insertAdjacentHTML('beforeend', child.toString());
                                  });
                              }else {
                                  tlNodes=[...tlNodes.slice(0, position-1), ...elNodes, ...tlNodes.slice(position-1)]
                                  tel.set_content(tlNodes);
                              }
                          });
                      }
                      // type屬性為replace
                      if (type==='replace') {
                          templateElements.forEach((tel)=> {
                              tel.replaceWith(...el.childNodes);
                          });
                      }
                      // type屬性為remove
                      if (type==='remove') {
                          templateElements.forEach((tel)=> {
                              tel.remove();
                          });
                      }
                  });
                  // 重組文件內容
                  const template=extendAst.querySelector('template').toString();
                  const script=pageAst.querySelector('script').toString();
                  const style=extendAst.querySelector('style').toString() + pageAst.querySelector('style').toString() 
                  return`${template}${script}${style}`
              }
          
          }
          module.exports=Extend;
          


          好的,自定義loader已經編寫完成,在vue.config.js里面配置好我們的loader

          const { defineConfig }=require('@vue/cli-service')
          module.exports=defineConfig({
            configureWebpack: {
              module: {
                rules: [
                  {
                    test: /\.vue$/,
                    use: [
                      {
                        loader: require.resolve('./src/loader'),
                      },
                    ],
                  },
                ],
              },
            },
          })
          
          


          接下來我們嘗試編寫A頁面和B頁面:

          A.vue:

          <template>
            <div class="template">
                <div id="div_1" class="div">父頁面的div_1</div>
                <div id="div_2" class="div">父頁面的div_2</div>
                <div id="div_3" class="div">父頁面的div_3</div>
                <div id="div_4" class="div">父頁面的div_4</div>
                <div id="div_5" class="div">父頁面的div_5</div>
                <div id="div_6" class="div">父頁面的div_6</div>
                <div id="div_7" class="div">父頁面的div_7</div>
                <div id="div_8" class="div">父頁面的div_8</div>
            </div>
          </template>
          <script>
          export default {
            name: 'COM_A',
            props: {
              msg: String
            }
          }
          </script>
          <style scoped>
          .div {
            color: #42b983;
            font-size: 1.5em;
            margin: 0.5em;
            padding: 0.5em;
            border: 2px solid #42b983; 
            border-radius:  0.2em;
          }
          </style>
          


          B.vue:

          <template extend="./A.vue">
            <div>
              <extend type="insert" target="#div_1" position="after">
                <div id="div_child" class="div">子頁面的div_5</div>
              </extend>
              <extend type="append" target="#div_3" position="2">
                <a> 子頁面通過append插入的超鏈接 </a>
              </extend>
            </div>
          </template>
          <script>
          import A from './A.vue'
          export default {
            name: 'COM_B',
            extends: A,//繼承業務邏輯代碼
            props: {
              msg: String
            }
          }
          </script>
          <style scoped>
          #div_child {
            color: #d68924;
            font-size: 1.5em;
            margin: 0.5em;
            padding: 0.5em;
            border: 2px solid #d68924;
          }
          a {
            color: blue;
            font-size: 0.7em;
          }
          </style>
          


          我們在App.vue下引入B.vue

          <template>
            <div id="app">
              <B/>
            </div>
          </template>
          <script>
          import B from './components/B.vue'
          export default {
            name: 'App',
            components: {
              B
            }
          }
          </script>
          <style>
          #app {
            font-family: Avenir, Helvetica, Arial, sans-serif;
            -webkit-font-smoothing: antialiased;
            -moz-osx-font-smoothing: grayscale;
            text-align: center;
            color: #2c3e50;
            margin-top: 60px;
          }
          </style>
          


          當我們執行編譯的時候,實際上B.vue的編譯結果如下:

          <template>
            <div class="template">
              <div id="div_1" class="div">父頁面的div_1</div>
              <div id="div_child" class="div">子頁面的div_5</div>
              <div id="div_2" class="div">父頁面的div_2</div>
              <div id="div_3" class="div">
                父頁面的div_3
                <a> 子頁面通過append插入的超鏈接 </a>
              </div>
              <div id="div_4" class="div">父頁面的div_4</div>
              <div id="div_5" class="div">父頁面的div_5</div>
              <div id="div_6" class="div">父頁面的div_6</div>
              <div id="div_7" class="div">父頁面的div_7</div>
              <div id="div_8" class="div">父頁面的div_8</div>
            </div>
          </template>
          <script>
          import A from './A.vue'
          export default {
            name: 'COM_B',
            extends: A,//繼承業務邏輯代碼
            props: {
              msg: String
            }
          }
          </script>
          <style scoped>
          .div {
            color: #42b983;
            font-size: 1.5em;
            margin: 0.5em;
            padding: 0.5em;
            border: 2px solid #42b983;
            border-radius: 0.2em;
          }
          </style>
          <style scoped>
          #div_child {
            color: #d68924;
            font-size: 1.5em;
            margin: 0.5em;
            padding: 0.5em;
            border: 2px solid #d68924;
          }
          
          a {
            color: blue;
            font-size: 0.7em;
          }
          </style>
          


          注意我們在B.vue使用了extends繼承了組件A,這里是為了能復用業務邏輯代碼,最后我們運行代碼,頁面輸出為:

          結語

          在真實的項目當中,我們遇到大量重復的頁面但是又有小區別的頁面,是可以通過這種方式減少我們的代碼量,當然也許有更好的辦法,也希望大伙能提出寶貴的建議。

          最后引用一下 @XivLaw 老哥的評論:有很多人說通過cv就能解決,但是當你的業務有成千上萬個頁面是趨同,并且具有相同的基本功能,當界面需要統一調整或者需要進行ui統一管控的時候,cv就成了你的累贅了。 也有朋友說通過組件化和插槽解決,組件化是一個不錯的方案,但是當成千上萬個趨同的界面存在時,插槽并一定能覆蓋所有的業務定制化。 使不使用這種方式,主要看你的業務。

          直白一點說就是:我現在有一千個頁面幾乎一樣,有的頁面是頭部多一點東西,有的是底部,有的是某個按鈕旁邊多一個按鈕,有的是輸入框之間多個輸入框,ui或者界面或者同時需要添加固定功能,需要調整的時候,這一千個頁面要怎么調?


          作者:小小小小_柏
          鏈接:https://juejin.cn/post/7347973138787467274


          端工程化是什么?一提到工程化我們的第一反應往往就是 webpack。webpack 確實是前端工程化中重要的工具,但二者并不能劃等號。

          其實顧名思義,前端,工程化,就是把前端做成一項工程。這其中的區別類似于,寫一個簡單的展示頁面=小孩子搭積木,一個大型的項目=開發商蓋房子。搭積木只需要簡單的手工操作,而蓋房子則需要的工程規劃、設計、資源管理等一系列流程。

          前端工程化貫穿從編碼、發布到運維的整個前端研發生命周期,一切以提高效率、降低成本、質量保證為目的的手段都屬于工程化。它借鑒了軟件工程相關的方法和思想,通過使用工具、流程、最佳實踐和規范來提高前端開發效率、質量和可維護性;旨在解決前端開發中出現的各種挑戰,包括項目復雜性、跨瀏覽器兼容性、性能優化、代碼可維護性以及團隊協作等問題。

          前端開發模式進化史

          首先讓我們回顧一下前端開發模式的演化歷史,前端工程化正是為了應對這些演化中出現的挑戰和需求而發展起來的:

          1. 前后端混合:服務端渲染,javascript僅實現交互
          2. 前后端分離:借助 ajax 實現前后端分離、單頁應用(SPA)等新模式
          3. 模塊化開發:npm 管理模塊、Webpack 編譯打包資源
          4. 模塊化 + MVVM:基于 React 或 Vue 等框架進行組件化開發,不再手動操作 html 元素

          前端工程化解決了什么問題

          那么前端工程化究竟解決了什么問題呢:

          1. 全局作用域問題:前端工程化可以幫助解決全局作用域污染問題。模塊化開發工具如Webpack和ES6模塊化幫助開發者將代碼分解為模塊,避免全局變量沖突,并提高代碼的可維護性。
          2. 編碼規范:通過代碼規范工具(如ESLint、TSLint)和自動化代碼格式化工具(如Prettier),前端工程化可以確保代碼風格一致,減少錯誤和提高可讀性。
          3. 資源合并和壓縮:前端工程化工具可以自動合并和壓縮CSS、JavaScript和圖片等前端資源,以減小文件大小,提高頁面加載速度,并減少帶寬占用。
          4. 高版本JS預發降級:前端工程化工具可以使用特性檢測和polyfill庫,以確保新版本JavaScript特性在舊版本瀏覽器中仍然可用。這有助于實現跨瀏覽器兼容性。
          5. 模塊管理:前端工程化工具和模塊化開發使前端項目更易于管理,避免依賴混亂,促進代碼的重用和維護。
          6. 自動化測試:前端工程化通過測試工具和自動化測試流程,幫助檢測和預防潛在的問題,確保代碼的可靠性。
          7. 持續集成和持續交付(CI/CD) :前端工程化支持CI/CD流程,以確保代碼在每次更改后都經過構建、測試和部署。這有助于快速交付功能,減少錯誤。
          8. 性能優化:前端工程化工具支持性能優化策略,如延遲加載、資源緩存和減少HTTP請求次數,以提供更好的用戶體驗。
          9. 團隊協作:前端工程化規范化項目結構、版本控制、文檔和工作流程,促進多人協作,減少溝通和協調成本。

          舉幾個企業中的例子,比如前端團隊從幾個人增加到了幾百人,如果不統一代碼規范,閱讀和接手他人代碼的心智負擔較大;項目代碼從幾百幾千行增加到幾萬幾十萬行之后,如果不做模塊化處理,單個文件過大,閱讀和維護困難,可復用性差;項目數量從幾十個發展到成千上萬個,如果沒有前端研發的腳手架,每個項目都要重復搭建,不同開發人員構建的項目難以統一管理。

          前端模塊化

          前端模塊化是前端工程化的一個重要組成部分,前者關注代碼的組織和結構,而后者關注整個前端開發過程的自動化和最佳實踐。前端工程化借助前端模塊化來提高代碼的組織和可維護性,從而解決前端開發中的一系列問題。

          前端模塊化是什么?

          前端模塊化指的是將前端代碼分解成獨立的可復用的模塊,以便更好地組織、維護和擴展代碼。模塊可以包括JavaScript、CSS、HTML等各種前端資源。前端模塊化的目標是將復雜的前端應用程序分解為小塊,每個塊都有特定的功能,可以獨立開發和測試。

          前端模塊是一種規范,而不是具體的實現。比方說 Node.js 實現了 CommonJS 規范,ES6 模塊提供了 ESM 規范。這些規范有兩個共性:

          • 將復雜程序根據規范拆分成若干模塊,一個模塊包括輸入和輸出
          • 模塊的內部實現是私有的,對外暴露接口與其他模塊通信

          前端模塊化發展史

          ① 全局函數模式

          將不同的功能封裝到單獨的全局函數中,通過函數引用功能。比方說一個加法模塊:

          function sum(a, b) {
              return a + b;
          }
          


          這就是一個全局函數,可以在任何地方調用。

          缺點: 如果出現相同的函數名,容易引起沖突。

          ② 命名空間模式

          在 window 下新建一個對象屬性來存放模塊(命名只要不沖突即可,這樣只需要確保這個屬性名唯一,就能解決全局函數模式的缺點,如 __Module),再將模塊中的變量和功能作為 __Module 的屬性。

          var __Module={
              sum: function(a, b) {
                  return a + b;
              }
          }
          


          缺點: 外部能夠修改模塊內部的數據,喪失了封裝性。(window.__Module.屬性名 可以直接修改模塊)

          ③ 立即執行函數模式

          通過IIFE(立即調用函數表達式),利用閉包來創建私有變量。

          為了解決 namespace 模式中的缺點,我們可以將創建 __Module 的過程放在一個IIFE中:

          (function () {
              var x=1;
          
              function getX() {
                  return x;
              }
          
              function setX(val) {
                  x=val;
              }
          
              function sum(a, b) {
                  return a + b;
              }
          
              window.__Module={
                  x,
                  setX,
                  getX,
                  sum,
              };
          })();
          


          這樣 window.__Module 下的 x 只是一個拷貝,真正的 x 存放在IIFE的私有作用域中,而不是全局作用域。這樣,外部代碼就無法直接訪問或修改真正的 x 變量。

          我們還可以稍加改動,實現一個增強的IIFE模式,使它能夠支持自定義傳入依賴:

          // 模塊A
          (function (dependencyA, dependencyB) {
              // 在這里可以使用 dependencyA 和 dependencyB
              function doSomething() {
                  dependencyA.doThis();
                  dependencyB.doThat();
              }
          
              // 向全局暴露公共接口
              window.ModuleA={
                  doSomething: doSomething
              };
          })(window.DependencyA, window.DependencyB);
          
          // 模塊B
          (function () {
              function doThis() {
                  // 實現某些功能
              }
          
              // 向全局暴露公共接口
              window.DependencyA={
                  doThis: doThis
              };
          })();
          
          // 模塊C
          (function () {
              function doThat() {
                  // 實現某些功能
              }
          
              // 向全局暴露公共接口
              window.DependencyB={
                  doThat: doThat 
              };
          })();
          


          至此,已經是我們自己通過純 JavaScript 來實現模塊化方案的最終方案了。但是,它沒有特定的語法支持,代碼閱讀困難,也沒有完善的依賴管理、模塊加載機制。

          所以,為了應對大型項目中復雜的模塊化需求,我們需要更加現代的模塊系統。

          ? CommonJS

          CommonJS 是 Node.js 中默認的模塊化規范:

          • 文件級別的模塊作用域(每個文件就是一個作用域):每個 CommonJS 模塊都有自己的作用域。
          • 使用 require 函數來導入,通過 module.exports 導出。
          • CommonJS 模塊是同步加載的,這意味著模塊在導入時會阻塞執行,直到模塊完全加載并可用,并且模塊加載的順序會按照其在代碼中出現的順序。
          • 模塊可以多次加載,首次加載的時候會運行模塊并對輸出結果進行緩存,再次加載時會直接使用緩存中的結果。

          Node 中 CommonJS 的原理可以分成三部分來看:

          主模塊加載

          應用程序的入口點,包含 require 調用加載其他模塊。

          模塊加載

          1. 解析模塊標識符: 它會解析模塊的標識符,通常是一個文件路徑,以確定要加載的模塊。
          2. 檢查模塊緩存: CommonJS實現會檢查模塊緩存來查看是否已經加載了該模塊。如果已經加載,它會直接返回緩存中的模塊對象。
          3. 創建模塊對象: 如果模塊尚未加載,CommonJS實現會創建一個新的模塊對象,通常是一個包含了 exports 和 module 屬性的對象。
          4. 執行模塊代碼: 接下來,CommonJS實現會將模塊的代碼包裝在一個IIFE中,并向該IIFE傳遞 exports、module 和 require,以確保模塊的作用域是隔離的。模塊的代碼會在這個作用域內執行,可以在模塊內定義變量和函數,并通過 exports 和 module.exports 暴露模塊的接口。
          (function (exports, module, require) {
            // 模塊內部的代碼
            // 可以在這里定義模塊內的變量和函數
            // 通過exports和module.exports來暴露模塊的接口
          })(exports, module, require);
          


          1. 緩存模塊: 模塊加載完成后,CommonJS實現會將這個模塊對象緩存起來,使用模塊標識符作為鍵,以便后續 require 調用可以直接返回緩存的模塊對象。

          模塊緩存

          已加載的模塊以模塊標識符為鍵,模塊對象為值存儲在模塊緩存中。 這個緩存允許模塊在后續的 require 調用中被快速訪問,而不需要重新加載。

          AMD

          Node 模塊通常都位于本地,加載速度快,不用擔心同步加載帶來的阻塞問題。但是在瀏覽器運行過程中,同步加載會阻塞頁面的渲染。

          require.js 中實現了AMD(Asynchronous Module Definition) ,旨在解決瀏覽器環境中的異步模塊加載和依賴管理問題。它的主要特點是允許在瀏覽器中異步加載模塊,以提高性能和模塊化的管理。

          CMD

          sea.js中實現了CMD(Common Module Definition),它整合了 CommonJS 和 AMD 的優點。

          實際上,AMD和CMD等模塊規范有一個最大的問題:它們沒有得到官方的JavaScript語言規范的支持,這也是他們最終過時了的原因。在現代前端開發中,ES6模塊已經成為了主要的模塊化解決方案。

          ? ESM

          前面所說的幾種模塊化規范都必須在運行時才能確定依賴和輸入輸出,而 ESModule 的理念是在編譯時就確定模塊依賴的輸入輸出。

          ? CommonJS 和 ESModule 規范對比:

          • CommonJS 模塊輸出的是值的拷貝,ESM 模塊輸出的是值的引用
          • CommonJS 模塊是運行時加載,ESM 模塊是編譯時輸出接口。
          • CommonJS 是單個對象導出,多次導出會覆蓋之前的結果;ESM 可以導出多個。
          • CommonJS 模塊是同步加載,ESM 支持異步加載。
          • CommonJS 的 this 是當前模塊,ESM 的 this 是 undefined。

          現在大多數瀏覽器中默認的模塊化規范都是 ESM 了,作為一種規范它已經比較成熟了,但是我們在瀏覽器模塊化問題上仍有一些問題未能解決:

          • 瀏覽器沒有模塊管理能力,模塊分散在各個項目中無法復用。
          • 性能加載慢,大型項目中無法直接使用。

          為了解決這兩個問題,前端工程化又引入了兩個新的工具:

          引入 npm 負責管理模塊,引入打包工具比如 webpack 進行打包聚合提高性能。

          npm 簡介

          npm 的全稱是 Node Package Manager,它是一個用于 Node.js 包的默認包管理器。npm 的主要目標是提供一個集中的、共享的解決方案,用于開發者之間共享和復用代碼。在 npm 出現之前,開發者想要在另一個項目中復用某個模塊,只能通過復制和粘貼文件的方式。有了 npm 之后,開發者可以把所有模塊都上傳到倉庫(registry):在模塊內創建 package.json 文件來標注模塊的基本信息,然后通過 npm publish 命令發布模塊;使用時通過 npm install 命令安裝指定模塊到 node_modules 目錄。

          webpack 簡介

          雖然 npm 能解決模塊的管理問題,但它無法解決加載性能問題。為了解決這個問題,webpack 誕生了。webpack 的整個工作流程可以簡單地理解為先合并、再分割:

          • 合并:為了解決項目中依賴文件過多,而導致 HTTP 請求過多的問題,webpack 會把所有資源文件視為模塊,分析模塊之間的依賴關系,并把所有的依賴打包成一個或多個 bundle.js 文件。
          • 分割:合并打包會帶來一個問題——單文件過大,導致加載時間過長。為了解決這個問題,Webpack 引入了代碼分割的概念。代碼分割允許你將一個大的 bundle 文件分割成多個小文件,這些小文件在需要時才會被加載。這提高了應用程序的加載性能,因為瀏覽器只需要下載當前頁面所需的代碼塊,而不是整個應用的所有代碼。Webpack 提供了不同的代碼分割策略,如按路由、按組件或按異步加載。

          在 webpack 打包過程中,首先會通過 entry(入口)找到需要打包的文件,然后通過 module(模塊)來處理各種類型的文件。在處理文件時,會用到各種 loader(加載器),比如 babel-loader 用來處理 JS 文件,css-loader 和 style-loader 用來處理 CSS 文件。最后通過 output(輸出)把處理過的文件輸出到指定的目錄。


          作者:FrontEnd_Reese
          鏈接:https://juejin.cn/post/7291186181157535800


          主站蜘蛛池模板: 国产成人精品久久一区二区三区av| 蜜臀AV一区二区| 成人午夜视频精品一区| 久久99精品波多结衣一区| 亚洲视频免费一区| 国产一区二区精品尤物| 国产精品区一区二区三| 无码少妇一区二区三区浪潮AV| 91精品乱码一区二区三区| 一本AV高清一区二区三区| 国产日韩精品一区二区在线观看| 国产丝袜视频一区二区三区 | 国产伦一区二区三区高清| 国产一区二区三区韩国女主播| 亚洲无线码在线一区观看| 国产伦精品一区二区三区免费迷| 亚洲一区免费观看| 国模吧一区二区三区| 国产精品免费大片一区二区| 性色AV一区二区三区无码| 无码欧精品亚洲日韩一区| 国产高清一区二区三区四区| 午夜性色一区二区三区不卡视频 | 日韩精品中文字幕无码一区| 三上悠亚亚洲一区高清| 国产a久久精品一区二区三区| 国精产品一区一区三区MBA下载| 精品欧洲av无码一区二区三区| 成人一区二区免费视频| 久久亚洲中文字幕精品一区| 国产成人高清精品一区二区三区| 国产成人欧美一区二区三区 | 国产精品一区二区四区| 亚洲色精品VR一区区三区 | 99久久精品午夜一区二区| 色欲精品国产一区二区三区AV| 国产成人无码一区二区在线播放 | 精品日韩一区二区三区视频| 色噜噜狠狠一区二区三区| 久久精品午夜一区二区福利| 蜜桃视频一区二区三区在线观看|