[深入16] webpack

导航

[深入01] 执行上下文
[深入02] 原型链
[深入03] 继承
[深入04] 事件循环
[深入05] 柯里化 偏函数 函数记忆
[深入06] 隐式转换 和 运算符
[深入07] 浏览器缓存机制(http缓存机制)
[深入08] 前端安全
[深入09] 深浅拷贝
[深入10] Debounce Throttle
[深入11] 前端路由
[深入12] 前端模块化
[深入13] 观察者模式 发布订阅模式 双向数据绑定
[深入14] canvas
[深入15] webSocket
[深入16] webpack
[深入17] http 和 https
[深入18] CSS-interview
[深入19] 手写Promise
[深入20] 手写函数

[react] Hooks

[部署01] Nginx
[部署02] Docker 部署vue项目
[部署03] gitlab-CI

[源码-webpack01-前置知识] AST抽象语法树
[源码-webpack02-前置知识] Tapable
[源码-webpack03] 手写webpack - compiler简单编译流程
[源码] Redux React-Redux01
[源码] axios
[源码] vuex
[源码-vue01] data响应式 和 初始化渲染
[源码-vue02] computed 响应式 - 初始化,访问,更新过程

image

前置知识

一些单词

proposal:提议,建议
decorator:修饰
expose:暴露
license:执照,许可
compress:压缩
determine:确定,查明
wildcard:通配符
collapse:塌方,瓦解
quote:引用

extract:抽离,提取
optimization:最佳优化
plug:插头,插座  (pluggable:可插拔)
complete:完成

represents:代表

package.json

name
- 定义包的名称,是发布到npm注册表时,软件包显示的名称
- 必须是小写,允许( 连字符和下划线 ),不允许 ( 空格和其他字符 )
- name: 'webpack-learning'
- 注意:如果要发布到npm上,包的名称必须是唯一的

main !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
- 定义项目的入口点
- 默认是index.js

version
- 软件的当前版本

license
- 代码许可证
- MIT
- ISC

author 和 contributors
- author:一个人
- contributors:多个人
- "author": "...",
- "contributors": [{
    "name": "...",
    "email": "...",
    "url": "..."
}],

keywords
- 搜索索引
- "keywords": ["server", "osiolabs", "express", "compression"]

path.resolve 和 path.join 和 __dirname变量

  • <font color=blue>path.resolve() --------------------------- 生成绝对路径</font>
    • path.resolve([...paths])
    • <font color=red>将( 路径或路径片段 ) 的序列解析为 ( 绝对路径 )</font>
    • 参数:路径或路径片段的序列
    • 返回值:一个绝对路径
    • 注意:
      • 不传参数将返回当前工作目录的绝对路径
      • 0长度的path片段将被忽略
  • <font color=blue>path.join()--------------------------------- 拼接路径</font>
    • path.join([...paths])
    • 注意:
      • 0长度的路径将被忽略
  • <font color=blue> __dirname -------------------------------- 当前模块的目录名,是一个绝对路径</font>

process.env ---------------------------- 返回包含用户环境的对象

  • process.env 返回包含用户环境的对象
  • <font color=red>注意:process.env对象上分配的属性,值会被隐式转换为字符串,当值不是string,number,boolean时可能会报错</font>
  • 注意:可以修改此对象,但这些修改不会反映到 Node.js 进程之外
  • <font color=red>删除属性:delete </font>
    • delete process.env.xxxx
  • 注意:在windows系统上,环境变量不区分大小写
  • <font color=red>process.env.NODE_ENV不是process.env对象的原有属性,是自定义的</font>
    • 原因:在window和mac有差异
    • 所以:一般通过 ( cross-env ) 插件来设置
    • npm install --save-dev cross-env
    • cross-env
(1) 生效
process.env.foo = 'bar';
console.log(process.env.foo);


(2) process.env的属性会隐式转换为字符串
process.env.test = null;
console.log(process.env.test);
// => 'null'
process.env.test = undefined;
console.log(process.env.test);
// => 'undefined'


(3) delete 删除属性
process.env.TEST = 1;
delete process.env.TEST;
console.log(process.env.TEST);
// => undefined


(4) window上 ( 环境变量 ) 不区分大小写
process.env.TEST = 1;
console.log(process.env.test);
// => 1
---
cross-env

{
  "scripts": {
    "build": "cross-env NODE_ENV=production webpack --config build/webpack.config.js"
  }
}

会用到的依赖

- webpack webpack-cli
- html-webpack-plugin
- webpack-dev-server

css相关
- style-loader
- css-loader
- less-loader
- 抽离css
- mini-css-extract-plugin
- 前缀
- postcss-loader
- autoprefixer
- 压缩css
- (注意:用于生产环境才去压缩css,因为影响打包速度)
- optimize-css-assets-webpack-plugin
- uglifyjs-webpack-plugin

js相关
- babel-loader
- @babel/core
- @babel/preset-env
- @babel/plugin-proposal-decorators
- @babel/plugin-proposal-class-properties
- @babel/plugin-transform-runtime
- @babel/runtime
- @babel/polyfill
- eslint ellint-loader babel-eslint
- expose-loader

图片相关
- file-loader
- url-loader
- html-withimg-loader
  • 2021/01/11 更新

源码仓库

代码分割 webpack.DllPlugin 和 optimization.splitChunks 的区别

  • <font color=red>(webpack.DllPlugin,webpack.DllReferencePlugin) 和 optimization.splitChunks 都能实现代码分割</font>
  • 优先使用 optimization.splitChunks.cacheGoup
  • SplitChunksPlugin
SplitChunksPlugin


默认配置:
module.exports = {
  //...
  optimization: { 
  // -------------- 默认配置,该段配置的效果和 optimization: {splitChunks: {}} 效果一样
  // -------------- 即 splitChunks 配一个空对象和下面的代码等价
    splitChunks: {
      chunks: 'async', 
      // -------------- chunks: 'async' 只对异步引入的代码就行代码分割,比如 import().then()语法动态引入的包或者路由组件
      // -------------- chunks: 'all' 对同步引入和异步引入的代码都做代码分割,还需后续配置cacheGroups
      // -------------- chunks: 'initial' 对同步代码进行分割
      minSize: 30000,
      // -------------- minSize: 30 000 表示当包大于( 30KB )时,就进行代码分割
      maxSize: 0,
      // -------------- maxSize表示对打包后的库进行二次拆分,可配可不配
      minChunks: 1,
      // -------------- minChunks 表示当一个模块至少使用多少次时才进行代码分割
      maxAsyncRequests: 5,
      maxInitialRequests: 3,
      automaticNameDelimiter: '~', // 文件命名之间的连接符,比如name和入口文件名之间的连接
      name: true,
      cacheGroups: { // 一个可能包含多个包的组
        vendors: { 
        // ----------------- 表示如果满足chunks设置项,并且在node_modules文件夹下,就打包成 vendors~main.js
        // ----------------- vendors~main.js 中的 main 表示入口文件打包后的文件名是 main.js
        // ----------------- 如果要修改满足chunks的配置引入的包打包的名字用:filename 去配置
        // ----------------- cacheGroups 和 chunks 是配合来用的
          test: /[\\/]node_modules[\\/]/, 
          //---------------- 从node_modules文件夹中引入的,实际上就是第三方组件库,依赖库
          priority: -10,
          // --------------- priority表示当同时满足vendors和default时, piroity越大优先级越高,(优先级)
          filename: 'vendors', // ------------- 打包后满足chunks和以上各个条件的引入的包会被打包到名为 vendors.js 的文件中
        },
        default: { // 不满足vendors但是又满足chunks等以上的一些条件,说明需要做代码分割,但不是vendors,则会走default
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true, 
          // reuseExistingChunk表示一个包如果打包过了,在别的包中又引用了,打包别的包时就不再打包了,直接引用
          filename: 'common'
        }
      }
    }
  }
};

webpack基础

  • webpack是一个 ( <font color=red>模块打包</font> ) 工具,最基本的就是一个js模块打包工具,如果添加了loader则可以打包任何相应的资源
  • 可以 0 配置,但是为了符合项目需求,一般都会自己配置
0配置下

默认配置运行:-------------------------------------- npx webpack xxx.js
打包后生成的 ( 文件夹 ) 名称:---------------------- dist
打包后生成的 ( 文件 ) 名称:------------------------ main.js

(2)
webpack默认配置文件
默认的webpack配置文件名称:------------------------- webpack.config.js || webpackfile.js

(3)
如何用命令行指定webpack的配置文件,并执行这个文件
---------------------------------------------------- npx webpack --config xxxxx

webpack webpack-cli

  • npm install -D webpack-cli webpack
  • 注意是开发时依赖,线上使用打包后的文件

webpack-dev-server

  • npm install -D webpack-dev-server
  • webpack-dev-server可以启一个简单的web服务,并且具有 live-reloading 实时重新加载
  • 配置项:
    • contentBase:启动的服务从哪个目录中加载资源
    • port:服务端口号
    • <font color=red>publicPath:设置公共路径,此路径下的打包文件,可以在浏览器中访问</font>
      • <font color=red>比如部署到CDN上,html的所有引入资源都需要加上CDN的地址作为公共路径,这时需要设置publciPath</font>
    • <font color=red>proxy:设置代理</font> ,使用的是 http-proxy-middleware
    • <font color=red>compress:是否开启 gzip 压缩</font>
    • hot:是否开启热更新 ( 模块热替换 )
    • open:是否在启动服务后打开浏览器
    • host:指定host
    • color:控制台彩色输出,boolean,注意只用于命令行
  • proxy
proxy

var apiProxy = proxy('/api', { target: 'http://www.example.org' });
//                   \____/   \_____________________________/
//                     |                    |
//                   context             options

(1)
proxy: {
    '/api': 'http://www.baidu.com'
}
请求:'/api/user'
会被代理到:'http://www.baidu.com/api/user'

(2)
需求:如何在真实的请求路径中去掉 '/api'
解决:重写路径
proxy: {
    '/api': {
        target: 'http://www.baidu.com',
        pathRewrite: {
            '^/api': ''   //------------------------------------- remove path 即删除 '/api'
        }
    }
}
请求:'/api/user'
会被代理到:'http://www.baidu.com/user'
---
webpack-dev-server

devServer: { // --------------------------------------------- webpack-dev-server配置项
  contentBase: path.join(__dirname, 'dist'), // ------------- 启动服务加载的静态资源
  port: '5000', // ------------------------------------------ 端口号
  compress: true, // ---------------------------------------- 是否开启 gzip 压缩
  open: true, // -------------------------------------------- 是否打开浏览器标签,在服务启动后
  proxy: { // ----------------------------------------------- 代理
    '/api': { // -------------------------------------------- context,需要代理的路径
      target: 'http://yapi.wxb.com.cn', // ------------------ 目标地址
      pathRewrite: { // ------------------------------------- 路径重写
        '^/api': '' // -------------------------------------- remove path 相当于把 '/api' 用 '' 代替
      }
    }
  }
},

<font color=red>proxy 实例</font>

(1)
index.js
const api = new XMLHttpRequest()
api.open('GET', '/api/name', true) // --------------------------------- 请求路径 ( '/api/name' )
api.onload = function() {
  const res = api.response
  console.log(res, 'res')
}
api.send()


(2)
devServer: {
  contentBase: path.join(__dirname, 'dist'),
  port: '5000',
  compress: true,
  open: true,
  proxy: {
    '/api': {
      target: 'http://localhost:7000', // ----------------------- 服务代理到http://localhost:7000
      pathRewrite: {
        '^/api': '' 
        // ------------------------------------------------------ 重写'/api'
        // ------------------------------------------------------ '/api/name' 将代理到 'http://localhost:7000/name'
      }
    }
  }
},


(3)
server => index.js
var express = require('express')
var app = express()
app.get('/name', (req, res) => { // ----------------------------- 服务端启动在:'http://localhost:7000/name'
  res.json({
    name: 'woow_wu7'
  })
})
app.listen(7000, () => console.log('the server is running on port 7000'))

html-webpack-plugin

  • npm install html-webpack-plugin -D
  • 配置项:
    • template:模板html文件
    • filename:打包后的文件名
    • <font color=red>hash:在html引用js时是否使用hash戳</font>
    • <font color=red>minify:用于 ( 生产环境 ) 即mode=production中,是一个 ( 布尔值 ) 或者 ( 对象 )</font>
      • removeAttributeQuotes:去除html属性值的双引号
      • collapseWhitespace:折叠空行,即只有一行代码
html-webpack-plugin

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

plugins: [ // ------------------------------------------------- plugins是一个插件数组
  new HtmlWebpackPlugin({
    template: './src/index.html', // -------------------------- 模板html
    filename: 'index.html', // -------------------------------- 打包后的html的文件名
    hash: true, // -------------------------------------------- html引用的js是否带有hash戳
    minify: { // ---------------------------------------------- 压缩优化
      removeAttributeQuotes: true, // ------------------------- 删除html标签属性值的双引号
      collapseWhitespace: true, // ---------------------------- 折叠空行为一行
    }
  })
]

loader

  • <font color=red>loader加载顺序: 从右往左,从下往上</font>
  • loader文档

css相关

( css-loader ) ( style-loader ) ( less-loader )

  • npm install style-loader css-loader less less-loader -D
  • css-loader
    • 主要用来支持 @import 语法的css引入
  • style-loader
    • 主要作用是把css以 ( style标签的 ) 形式嵌入到html的head标签中
    • 注意顺序是:先css-loader处理完css后,再style-loader插入到head标签中
    • 配置项 options
      • <font color=red>insert: 'body' 将style标签插入到body标签中,默认是插入到head标签中</font>
style-loader
css-loader
less-loader 还得安装 less
sass-loader 还得安装 node-sass


module: { // 模块
  rules: [ // 规则
    {
      test: /\.css$/,  // ------------------------ 处理css
      // ----------------------------------------- 正则匹配,注意loader的顺序是从右往左,从下往上
      use: [
        {
          loader: 'style-loader',
          options: {
            // insert: 'body' // ----------------- style标签默认插入到header标签中,这里可以插入到body标签中
          }
        },
        {
          loader: 'css-loader',
        },
      ]
    },{
      test: /\.less$/, // ------------------------ 处理less
      use: [
        {loader: 'style-loader'},
        {loader: 'css-loader'},
        {loader: 'less-loader'}
      ]
    }
  ]
},

mini-css-extract-plugin ------ 抽离css

  • npm install mini-css-extract-plugin -D
  • <table bgcolor=orange><tr><td>默认情况下:style-loader是把css文件转换成 style 标签内嵌在html中,我们希望的是用 link 标签外部引入css,所以用mini-css-extract-plugin来抽离css,通过link标签把css引入html</td></tr></table>
  • extract:抽离的意思
  • mini-css-extract-plugin官网
const MiniCssExtractPlugin =  require('mini-css-extract-plugin') // ---------------- 引入

module: {
  rules: [
    {
      test: /\.css$/,
      use: [
        MiniCssExtractPlugin.loader, // -------------------------------------------- loader
        {
          loader: 'css-loader',
        },
      ]
    },{
      test: /\.less$/,
      use: [
        MiniCssExtractPlugin.loader,
        {loader: 'css-loader'},
        {loader: 'less-loader'}
      ]
    }
  ]
},
plugins: [
  new MiniCssExtractPlugin({ // ---------------------------------------------------- plugin
    filename: 'main.css' // -------------------------------------------------------- 抽离出来的css文件名
    // filename: 'css/main.css' // ----------------- 这样会在打包后生成css文件夹,里面是所有css都在main.css文件中
  })
]

postcss-loader 和 autoprefixer ------ 生成浏览器前缀

  • postcss-loader
    • <table bgcolor=orange><tr><td>用来解决浏览器前缀,兼容性处理,需要单独的配置文件 postcss.config.js</td></tr></table>
    • 解决浏览器前缀:(1) 单独配置postcss.config.js (2) 还需配合插件 autoprefixer
    • <font color=blue>注意loader间的顺序:比如需要处理完less => 加上前缀后 => 识别@import => 再抽离css</font>
    • 即先 less-loader => postcss-loader => css-loader => MiniCssExtractPlugin.loader
  • autoprefixer
    • <font color=red>autoprefixer需要给出浏览器的一些信息,所以要在 package.json 中添加 browserslist 配置 </font>
postcss-loader 和 autoprefixer 解决浏览器前缀问题

(1) loader间的顺序
(2) postcss-loader需要单独配置postcss.config.js
(3) autoprefixer需要给出浏览器的一些信息,需要在 package.json 中配置 browserslist 属性

---
package.json中
"browserslist": [
  "defaults",
  "not ie < 9",
  "last 3 version",
  ">1%",
  "ios 7",
  "last 3 iOS versions"
]

---
postcss.config.js中
const autoprefixer = require('autoprefixer')
module.exports = {
  plugins: [autoprefixer]
}

---
webpack.config.js中
rules: [
  {
    test: /\.css$/,
    use: [
      MiniCssExtractPlugin.loader,
      { loader: 'css-loader'},
      { loader: 'postcss-loader'}, //----------------- postcss-loader,注意顺序
    ]
  },{
    test: /\.less$/,
    use: [
      MiniCssExtractPlugin.loader,
      {loader: 'css-loader'},
      { loader: 'postcss-loader'},
      {loader: 'less-loader'}
    ]
  }
]

optimize-css-assets-webpack-plugin 和 uglifyjs-webpack-plugin ------ 压缩css 和 js

  • npm install optimize-css-assets-webpack-plugin uglifyjs-webpack-plugin -D
  • <table bgcolor=orange><tr><td>注意压缩css是用于 ( 生产环境 ) 才去压缩css,因为会影响打包速度</td></tr></table>
  • <table bgcolor=yellow><tr><td>注意:( html-webpack-plugin ) 通 过minify => removeAttribureQuote, collapseWhitespace 来压缩html也是用在 ( 生产环境 )</td></tr></table>
  • optimize-css-assets-webpack-plugin
    • 注意:
      • 问题:使用 optimize-css-assets-webpack-plugin 插件后,单独抽离的css会被压缩,但是打包的js文件又没有被压缩了
      • 解决:使用 uglifyjs-webpack-plugin 插件来解决js没有被压缩的问题
  • uglifyjs-webpack-plugin
optimization: { // -------------------------------- 优化项 (optimization:是最佳优化的意思)
  // ---------------------------------------------- 注意:压缩css和js要在mode=production中才能看到效果,和 html的优化一样
  minimizer: [
    new OptimizeCssAssetsWebpackPlugin(), // ------ optimize-css-assets-webpack-plugin 压缩单独抽离的css
    new UglifyjsWebpackPlugin({ // ---------------- 压缩js
      cache: true,
      parallel: true, // 平行,并行的意思
      sourceMap: true, // 调试映射
    })
  ]
}

js相关

  • babel-loader
  • @babel/core
  • @babel/preset-env
  • @babel/plugin-proposal-decorators // -------------------- 装饰器语法
  • @babel/plugin-proposal-class-properties // -------------- class语法
  • @babel/plugin-transform-runtime
  • @babel/runtime // ---------------------------注意是该依赖是 dependencies 而不是 devDependencies
  • @babel/polyfill
  • npm install babel-loader @babel/core @babel/preset-env -D
  • npm install @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators -D
  • npm install @babel/plugin-transform-runtime -D
  • npm install @babel/runtime -S 注意是-S
  • npm install @babel/polyfill -S 注意是-S
  • babel官网
  • 注意点:
    • babel的配置:
      • <font color=red>(1)可以新建.babelrc文件单独配置</font>
      • <font color=red>(2)可以在webpack的loader配置的babel-loader的options中配置</font>
    • @babel/plugin-proposal-decorators 和 @babel/plugin-proposal-class-properties的顺序
      • @babel/plugin-proposal-decorators --------------在前
      • @babel/plugin-proposal-class-properties---------在后
.babelrc文件
{
  "presets": [
    ["@babel/preset-env", {
      "modules": false
    }]
  ],
  "plugins": [
    ["@babel/plugin-proposal-decorators", {"legacy": true}],
    ["@babel/plugin-proposal-class-properties", {"loose": true}],
    ["@babel/plugin-transform-runtime"]
  ]
}


----
@babel/polyfill 插件是直接引入到入口js文件中
require('@babel/polyfill')

加入eslint校验 (eslint) (eslint-loader) (babel-eslint)

  • npm install eslint eslint-loader babel-eslint -D
  • eslint
  • eslint-loader
  • babel-eslint
  • 注意点:
    • <table bgcolor=orange><tr><td>(1) 需要单独配置 ( .eslintrc.json ) 文件,或者直接在 ( package.json ) 文件的 ( eslintConfig ) 字段中配置rule规则</table></tr></td>
    • <font color=red>(2) 注意eslint-loader和babel-loader的顺序</font>
      • 顺序:先校验再处理
      • 是要先校验eslint-loader,然后在babel-loader转换
      • 在eslint-loader的options选中,可以使用 enforce: 'pre' 来优先执行,即使顺序是eslint-loader在上面或前面
    • <font color=red>(3) 需要安装bebel-eslint插件 Cannot find module 'babel-eslint'</font>

.eslintrc
{
  "parser": "babel-eslint", // 这里必须设置
  "parserOptions": {
    "sourceType": "module",
    "allowImportExportEverywhere": true
  },
  "rules": {
    "indent": "off",
    "no-console": 2 // 不允许console.log()
  },
  "env": {}
}


------------
webpack.config.js
{
  test: /\.js$/,
  use: [ // ----------------------------------------------- use可以是数组 或者 对象
    {
      loader: 'babel-loader',
      options: { // ---------------------------------- 除了在这里配置外,还可以单独设置 .babelrc 文件配置babel
        presets: [ 
          ['@babel/preset-env']
        ],
        plugins: [
          ['@babel/plugin-proposal-decorators', {'legacy': true}],
          ['@babel/plugin-proposal-class-properties', {'loose': true}],
          ['@babel/plugin-transform-runtime']
        ]
      }
    },
    {
      loader: 'eslint-loader', // -------------------------- eslint-loader 
      options: {
        enforce: 'pre'
      }
    }
  ],
  include: path.resolve(__dirname, 'src'),
  exclude: path.resolve(__dirname, 'node_modules'),
},

     // {
      //   test: /\.js$/,
      //   use: { // --------------------------------------- use是对象
      //     loader: 'eslint-loader',
      //     options: {
      //       enforce: 'pre'
      //     },
      //   },
      //   include: path.resolve(__dirname, 'src'),
      //   exclude: path.resolve(__dirname, 'node_modules'),
      // },

loader的类型

  • pre ----------------------- 前置loader
  • post ---------------------- 后置loader
  • normal ------------------- 普通loader
  • inline --------------------- 内联loader,即可以直接写在js代码中的loader

expose-loader 和 wepback.ProvidePlugin()

  • npm install expose-loader -D
  • expose-loader暴露全局的loader
  • expose:暴露
  • expose-loader的三种用法
expose-loader的三种用法

(1)
直接在js中引入:----------------  import $ from 'expose-loader?$!jquery'
上面的操作可以把:$挂载到window上,即可以通过 ( window.$ ) 访问到jquery

(2)
在webpack.config.js中 module => rules 中配置
{
  test: require.resolve('jquery'), 
  // require.resolve()是nodejs中的函数,只要require了jquery就进行匹配
  // require.resolve():使用内部的 require() 机制查询模块的位置,此操作只返回解析后的文件名,不会加载该模块。
  use: [
    {
      loader: 'expose-loader',
      options: 'jquery' // 暴露成window.jquery
    },
    {
      loader: 'expose-loader',
      options: '$'  // 暴露成window.$
    }
  ]
},

(3)
在每个模块中注入$,不需要在每个模块中再引入,可以使用 webpack.ProvidePlugin插件,再plugins中加入
new webpack.ProvidePlugin({ // ----------------- webpack.ProvidePlugin()
    $: 'jquery' // ----------------------------- 在每个模块中都注入$符
}) 
使用:直接在模块中就可以使用 $ , 并不需要在头部引入

图片处理

  • npm install file-loader url-loader html-withimg-loader -D
  • file-loader
    • 会生成一张图片到build的目录下,并将图片的名称返回回来
  • url-loader
    • <font color=red>可以设置大小限制,小于时,url-loader会将图片转成base64,大于时,使用file-loader加载图片</font>
    • 在options: {limit: 200 * 1024}来设置,200k,通过 ( <font color=red>limit</font> ) 来设置
    • <font color=red>options: {outputPath: 'img/'} ----- 将图片打包到img文件夹下</font>
    • <table bgcolor=orange><tr><td>注意:(1)css所有文件单独抽离成一个文件放到一个文件夹中是使用 mini-css-extract-plugin (2) 图片单独抽离到文件夹中是url-loader中配置options的outputPath</td></tr></table>
  • html-withimg-loader
    • <font color=red>注意:使用该插件时可能报错,需要在url-loader的options中设置 esModule: false </font>
  • 图片的引入方式
    • 在js中引入,可以通过new Image().src等
    • 在css中,通过 background-iamge来引入
    • 在html中,通过 img 标签来引入
file-loader
url-loader
html-withimg-loader

webpack.config.js
{
  test: /\.(png|jpg|jpeg|gif)$/,
  use: [
    {
      // loader: 'file-loader',
      loader: 'url-loader',
      options: {
        limit: 200 * 1024, // ----------------- 小于200k,使用base64编码图片,大于使用file-loader加载图片
        esModule: false, // ------------------- 用于html-withimg-plugin生效
        outputPath: 'img/', // ---------------- 输出到 img 文件夹中
        publicPath: 'www.baidu.com/' // ------- 单独配置img的公共路径,而不是在output中全部配置
      }
    }
  ]
},
{
  test: /\.html$/, // ----------------- html-withimg-loader
  use: 'html-withimg-loader'
}

使用:
js中
import one from './images/1.jpg'
const imgx = new Image(100, 100)
imgx.src = one
if (imgx.complete) {
  addChild()
} else {
  imgx.onload = addChild
}
function addChild() {
  document.body.appendChild(imgx)
}
css中
#div-image {
  width: 200px;
  height: 200px;
  background: url('./images/1.jpg');
}
html中
<img src="./images/1.jpg" alt="" width="300" height="300">

静态资源分类

  • 图片
    • url-loader => options => outputPath: 'img/'
  • css
    • mini-css-extract-plugin => filename: 'css/main.css'
  • 公共路径
    • <font color=red>在 output => publicPath 中设置公共路径,当打包后html加载资源时,都会加上公共路径 </font>
    • 比如:不是放在本地,放在CDN上,就要在所有路径上服务器的地址
  • 如果只是想给图片加载公共路径,而其他资源不加的话,就要在url-loader中单独设置,同时output的publicPath不用设置
静态资源分类

图片
{
  test: /\.(png|jpg|jpeg|gif)$/,
  use: [
    {
      // loader: 'file-loader',
      loader: 'url-loader',
      options: {
        limit: 200 * 1024,
        esModule: false, 
        outputPath: 'img/' // ----------------------------- 所有图片将放到打包后的 ( img文件夹中 )
      }
    }
  ]
},


css
plugins: [
  new MiniCssExtractPlugin({
    filename: 'css/main.css' // ---------------------------- 所有css将放入到打包后的 ( css文件夹的main.css中 )
  }),
],


公共路径:所有路径都加
output: {
  filename: 'index.[hash:8].js',
  path: path.resolve(__dirname, 'dist'),
  publicPath: 'www.baidu.com/' // -------------------------- 设置公共路径,打包后html中引入的资源都会加上这个前缀路径
},

公共路径:只是图片加
{
  test: /\.(png|jpg|jpeg|gif)$/,
  use: [
    {
      // loader: 'file-loader',
      loader: 'url-loader',
      options: {
        limit: 200 * 1024,
        esModule: false, 
        outputPath: 'img/',
        publicPath: 'www.baidu.com/' // ---------------------- 只给图片设置publicPath
      }
    }
  ]
},

打包多页应用

  • entry:可以是一个对象
  • output:'[name].js' => []是占位符 => [name]的值就是entry对象中的key
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  mode: 'development',
  entry: {
    home: './src/home.js',
    other: './src/other.js',
  },
  output: {
    filename: '[name].js', // ---------------------------- 占位符,name表示entry对象中的 key
    path: path.resolve(__dirname, 'dist')
  },
  devServer: {
    contentBase: path.resolve(__dirname, 'dist'),
    port: 5000,
    open: true,
    compress: true,
  },
  plugins: [
    new HtmlWebpackPlugin({ // ---------------------------- html-webpack-plugin可以new多个
      template: './src/index.html',
      filename: 'home.html',
      chunks: ['home'] // --------------------------------- 每个chunk对应加载哪些打包后的 js 文件,即 output指定的输出js文件
    }),
    new HtmlWebpackPlugin({
      template: './src/index.html',
      filename: 'other.html',
      chunks: ['other']
    }),
  ]
}

source-map 源码映射

  • devtool: 'source-map' // 显示行数,产生map文件
  • devtool: 'eval-source-map' // 显示行数,不产生map文件

watch 实时打包

watch: true,
watchOptions: {
  aggregateTimeout: 300, // 防抖
  poll: 1000, // 每秒询问1000次
  ignored: /node_modules/
},

aggregate: 总计,合计的意思
poll:是轮询的意思

clean-webpack-plugin ---------- 删除打包后的文件夹,默认是删除 output.paht 指定的文件夹

const {CleanWebpackPlugin} = require('clean-webpack-plugin')

plugins: [
  new CleanWebpackPlugin() //---------- 默认是删除 output.path 指定的文件夹
],

copy-webpack-plugin ---------- 复制文件

  • npm install copy-webpack-plugin -D
const CopyWebpackPlugin = require('copy-webpack-plugin')

plugins: [
  new CopyWebpackPlugin([{
    from: './src/copywebpackplugin', // -------- 将该文件夹中的内容拷贝到dist文件夹中
    to: './' // -------------------------------- 默认是输出到 output.path 指定的文件夹中
  }])
],

BannerPlugin ---------------- webpack自带的plugin,用于在js文件开头注释一些说明内容

new webpack.BannerPlugin({ banner: ' by woow_wu7'})


对比:webpack自带插件还有 webpack.ProvidePlugin ------- 将jquery暴露到每个模块,为$符号
// 在每个模块中注入$,不需要在每个模块中再引入,可以使用 webpack.ProvidePlugin插件,再plugins中加入
new webpack.ProvidePlugin({
    $: 'jquery'
}),

webpack-dev-server => before(app) => 实现mock数据

devServer: {
  contentBase: path.join(__dirname, 'dist'),
  port: '5000',
  compress: true,
  open: true,
  // proxy: {
  //   '/api': {
  //     target: 'http://localhost:7000',
  //     pathRewrite: {
  //       '^/api': ''
  //     }
  //   }
  // },
  before(app) { // ------------------------------ before钩子函数,app即express()
    app.get('/api/user', (req, res) => {
      res.json({
        user: 'woow_wu7'
      })
    })
  }
},


const api = new XMLHttpRequest()
api.open('GET', '/api/user', true) 
// -------------------------------------- 在webpack-dev-sesrver的bofore(app)中已经提供了服务路由并返回数据
// -------------------------------------- 所以这里可以直接访问
api.onload = function() {
  const res = api.response
  console.log(res, 'res')
}
api.send()

resolve 解析 -------------------- alias 和 extensions

  • <font color=red>resolve.alias :创建 import 或者 export 的别名</font>
  • <font color=blue>resolve.extensions : 自动解析确定的扩展</font>
    • 注意:<font color=blue>( resolve.extenstions ) 是一个 ( 数组 ),成员是 ( .后缀的字符串 )</font>
    • 注意:import SomeFile from "./somefile.ext",要想正确的解析,一个包含“*”的字符串必须包含在数组中。
    • 应用:在引入文件时,省略后缀 ( 优先级是从左往右 )
resolve.alias
resolve.extensions

设置:
resolve: {
  alias: {
    Imageff: path.resolve(__dirname, 'src/images'), // 用 Imageff 代替 images文件夹的绝对路径
  },
  extensions: ['.js', '.css', '.less', '*'] 
  // ------------------------------------------------- import时省略后缀时,先找.js文件,再找.css文件
  // ------------------------------------------------- 注意:'*' 表示所有类型的文件
},

使用:
import imagex from 'Imageff/1.jpg'

webpack.DefinePlugin ---------- 定义环境变量,即创建一个在编译时可以全局配置的常量

(1)
new webpack.DefinePlugin({ // ----------------------- new webpack.DefinePlugin()
  DEV: JSON.stringify('DEV'), // -------------------- ( JSON.stringify('DEV') === "'DEV'" )
  BOOLEAN1: true,
  BOOLEAN2: JSON.stringify(true) // ----------------- 这里BOOLEAN2和上面BOOLEAN1等价
}),
// -------------------------------------------------- 所以数据可以用JSON.stringify()来操作,传入什么类型就返回什么类型


(2) webpack的插件常有的有:
new webpack.DefinePlugin({ // ----------------------- webpack.definePlugin
  DEV: JSON.stringify('DEV'),
  BOOLEAN1: true,
  BOOLEAN2: JSON.stringify(true)
}),
new webpack.ProvidePlugin({ // ---------------------- webpack.ProvidePlugin
  $: 'jquery'
}),
new webpack.BannerPlugin({ // ----------------------- webpacck.BannerPlugin
  banner: 'by woow_wu7'
})

webapck-merge ------------ 区分不同环境

const merge = require('webpack-merge'); // --------------- webpack-merge
const base = require('./webpack.base.js')

module.exports = merge(base, {
  mode: 'developmen',
  devServer: {
    contentBase: './dist',
    port: 8000,
    open: true,
    compress: true
  }
})

noParse

  • <font color=red>module.noParse 如果包没有其他的依赖项,则可以通过noParse使webpack不去解析该包的依赖关系,提高构建速度</font>
  • 所以该包中:不能含有import,require,define等任何的导入机制
  • module.noParse
module: {
    noParse: /jquery|lodash/, // ------ 不去解析jquery或lodash的依赖关系,因为它们俩都没有依赖其他库,从而提高构建速度
    rules: []
}    

webpack.IgnorePlugin

  • resourceRegExp:匹配(test)资源请求路径的正则表达式。
  • contextRegExp:(可选)匹配(test)资源上下文(目录)的正则表达式。
  • webpack自带的插件
    • webpack.IgnorePlugin--------- 忽略引入
    • webpack.DefinePlugin--------- 定义全局常量
    • webpack.ProvidePlugin-------- 暴露包的名字改名
    • webpack.BannerPlugin--------- 打包的js的最前面注入一写信息字符串
----
webpack.config.js => plugins
new webpack.IgnorePlugin({ 
  // ------------------------------- 表示从 ( moment ) 中引入 ( ./local ) 文件时,将不去引入./local文件夹中的文件
  // ------------------------------- 但是需要使用中文包,所以忽略后,再 ( 手动引入 )
  resourceRegExp: /^\.\/locale$/,
  contextRegExp: /moment$/
});

-----
index.js
import moment from 'moment'
import 'moment/locale/zh-cn'; // ------------------------------------ 手动引入包
moment.locale('zh-cn')
const d = moment("20111031", "YYYYMMDD").fromNow();
console.log(d, 'd')

webpack.DllPlugin 和 webpack.DllReferencePlugin -------- 动态链接库,单独打包一些库

  • webpack.DllPlugin:=> name,path
  • webpack.DllReferencePlugin:=> manifest
  • manifest:清单
------
webpack.config.react.js // ------------------------------------- 专门用来打包react和react-dom
(1)
const path = require('path')
const webpack = require('webpack')
module.exports = {
  mode: 'development',
  entry: {
    react: ['react', 'react-dom']
  },
  output: {
    filename: '_dll_[name].js', // ---------------------------- 打包后是 ( _dll_react.js )
    path: path.resolve(__dirname, 'dist'),
    library: '_dll_[name]', // 打包后,把打包的文件赋值给_dll_[name]变量,全局变量的名称
    libraryTarget: 'var', // 变量的类型 
  },
  plugins: [
    new webpack.DllPlugin({ // -------------------------------- webpack.DllPlugin 用于生成任务清单,即manifest.json文件
      name: '_dll_[name]',  
      // name 表示暴露到全局的库的名称
      // 注意: name 和 output.librry 要一致
      // 注意:name代表的就是打包后生成在dist目录下的 manifest.json 中的变量值
      path: path.resolve(__dirname, 'dist', '[name].manifest.json')
    })
  ]
}
(2)
打包:使用命令打包:npx webpack --config webpack.config.react.js
生成:
(1) _dll_react.js // ----------------------------------------- 文件的内容会赋值给一个变量 ( _dll_react )
(2) manifest.json文件



------
(3)
webpack.config.js
plugins: [
  new webpack.DllReferencePlugin({ //----------------------------- 引用清单
    manifest: path.resolve(__dirname, 'dist', 'react.manifest.json') //- 引用上面打包生成的 manifest.json 文件
  })
],


-----
(4)
index.js
import react from 'react'
import {render} from 'react-dom'
render(<h1>jsx</h1>, document.getElementById('root'))

(5)


效果:打包main.js从( 1.4M => 500KB )

happypack ------------------------- 多线程打包

module.rules:
{
  test: /\.js$/,
  use: 'happypack/loader?id=js',
  },
}

plugins:
new HappyPack({
    id: 'js',
    use: [{
        loader: 'babel-loader'
    }]
})

代码分割 optimization.splitChunks

- 抽离公共组件和第三方组件,可以缓存,则不需要重复加载
- optimization => splitChunks => cacheGroups => vendors和commons
- priority: 是优先的意思
- optimization: {
    minimizer: {}, // 压缩css和js的配置项
    splitChunks: {
      cacheGroups: {
        commons: {
          name: 'commons',
          chunks: 'initial',
          minChunks: 1,
          priority: 10,
          minSize: 0,
        },
        vendors: { // vendor是小贩的意思
          test: /node_modules/, // 范围是node_modules中的第三方依赖,注意zhe
          name: 'vendors', // 抽离出来的包的名字
          chunks: 'initial', // 初始化加载的时候就抽离公共代码
          minChunks: 1, // 被引用的次数
          priority: 11, // priority: 是优先级的意思,数字越大表示优先级越高
          minSize: 0,
        }
      }
    }
  }

@babel/plugin-syntax-dynamic-import --------------------- 实现懒加载

  • npm install @babel/plugin-syntax-dynamic-import 语法动态导入
  • syntax:语法
  • dynamic:动态
(1) 在webpack.config.js中配置,或者在.babelrc中配置
module: {
    rules: [
        {
            test: /\.js$/,
            use: [
                {
                    loader: 'babel-loader',
                    options: {
                        presets: {
                            ['@babel/preset-env'],
                            ['@babel/preset-react'],
                        },
                        plugins: {
                            ['@babel/plugin-proposal-decorators', {'legacy': true}],
                            ['@babel/plugin-proposal-class-properties', {'loose': true],
                            ['@babel/plugin-transform-runtime'],
                            ['@babel/plugin-syntax-dynamic-import']
                        }
                    }
                },
                {
                    loader: 'eslint-loader',
                    options: {
                      enforce: 'pre'
                    }
                }
            ]
        }
    ]
}


(2) index.js
const button = document.createElement('button')
button.innerHTML = 'button'
button.addEventListener('click', () => {
  console.log('button clicked')
  import('./dynamic.js').then(res => console.log(res.default, 'res')) // ----------- 语法动态导入,使用jsonp实现
})
document.body.appendChild(button)



(3)
- import只能用在顶部报错:解决需要安装 babel-eslint 插件
- 安装,babel-eslint插件,并且在 .eslintrc.json中做如下配置
- {
    "parser": "babel-eslint",
    "parserOptions": {
        "sourceType": "module",
        "allowImportExportEverywhere": true, // ---------------------- 在所有地方都能import和export
    },
    "rules": {
        "indent": "off"
    },
    "env": {}
}
- https://stackoverflow.com/questions/39158552/ignore-eslint-error-import-and-export-may-only-appear-at-the-top-level

webpack.HotModuleReplacementPlugin 和 webpack.NameModulesPlugin --------- 热更新

- 热更新
- new webpack.HotModuleReplacementPlugin() // 热更新
- new webpack.NameModulesPlugin() // 打印热更新模块的路径
- 1. 首先在 devServer 配置中增加 hot: true,表示开启热更新
- 2. 在plugins数组中 new webpack.HotModuleReplacementPlugin() 和 new webpack.NameModulesPlugin()
- 3. 在入口js文件中:

(1)
devServer: {
    contentBase: path.join(__dirname, 'dist'),
    port: '5000',
    compress: true,
    open: true,
    hot: true, //-------------------------------------------------------- hot: true 开启热更新模式
    // proxy: {
    //   '/api': {
    //     target: 'http://localhost:7000',
    //     pathRewrite: {
    //       '^/api': ''
    //     }
    //   }
    // },
    before(app) {
      app.get('/api/user', (req, res) => {
        res.json({
          user: 'woow_wu7'
        })
      })
    }
  },
  
(2)
 plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html',
      filename: 'index.html',
      hash: true,
      minify: {
        removeAttributeQuotes: true,
        collapseWhitespace: true,
      }
    }),
    new MiniCssExtractPlugin({
      filename: 'css/main.css'
    }),
    // new CleanWebpackPlugin(),
    new CopyWebpackPlugin([{
      from: './src/copywebpackplugin',
      to: './'
    }]),
    new webpack.DefinePlugin({
      DEV: JSON.stringify('DEV'),
      BOOLEAN1: true,
      BOOLEAN2: JSON.stringify(true)
    }),
    new webpack.IgnorePlugin({
      resourceRegExp: /^\.\/local$/,
      contextRegExp: /moment$/,
    }),
    new webpack.ProvidePlugin({
      $: 'jquery'
    }),
    new webpack.BannerPlugin({
      banner: 'by woow_wu7'
    }),
    new webpack.NamedModulesPlugin(), // --------------------- webpack.NameModulesPlugin 打印更新的模块路径
    new webpack.HotModuleReplacementPlugin() // -------------- webpack.HotModuleReplacePlugin 热更新插件
    // new webpack.DllReferencePlugin({
    //   manifest: path.resolve(__dirname, 'dist', 'manifest.json')
    // })
  ],


(3) index.js
import hotNow from './hot';
console.log(hotNow)
if (module.hot) { // ----------------------------------------- 如果hot.module存在,开启了热更新
  module.hot.accept('./hot.js', function(){ // --------------- 监听 './hot.js'的改变,如果改变,执行回调
    const res = require('./hot.js') // ----------------------- 重新加载,并打印最新值
    console.log(res, '热更新后新的返回值')
  })
}

编写一个loader

  • loader就是一个 ( <font color=red>函数</font> ),第一个参数表示 ( <font color=red>该loader匹配的文件的源代码</font> )
  • 不能写成箭头函数,因为需要通过 this 获取更多 api
  • this.query
    • 如何获取loader中的配置参数:( <font color=red>options对象</font> ) this.query指向的就是options对象
    • 如果 loader 中没有 options,而是以 query 字符串作为参数调用时,this.query 就是一个以 ? 开头的字符
      • 注意:this.query已经废弃,使用 <font color=red>loader-utils</font> 中的 <font color=red>getOptions</font> 来获取 options 对象
  • loader-utils
    • npm install loader-utils -D
    • 通过loader-utils中的 getOptions 获取 loader的options配置对象
  • this.callback
    • 参数
      • 第一个参数:err // Error 或者 null
      • 第二个参数:content // string或者buffer,即处理过后的源代码
      • 第三个参数:sourceMap // 可选,必须是一个可以被这个模块解析的 source map
      • 第四个参数:meta //可选,即元数据
      • // https://www.webpackjs.com/api/loaders/#this-callback
  • this.async
    • <font color=red>处理loader中的异步操作</font>
    • <font color=red>this.async()方法返回 this.callback</font>
  • <font size=6 >resolveLoader</font>
    • webpack配置项
    • resolveLoader的配置项
      • 代码:modules: ['node_modules', './src/loaders']
      • 表示:在寻找loader的时候,先去node_modules文件夹中共寻找,没找到再去'./src/loaders'文件夹中找
  • loader API

最简单的replace-loader

replace-loader

---
目录:src/loaders/replace-loader.js
module.exports = function(source) { // --------------------- loader就是一个函数,参数是源码
  return source.replace('hello', 'hi!')
}

目录:webpack.config.js
module: {
  rules: [{
      test: /\.js$/,
      use: [path.resolve(__dirname, './src/loaders/replace-loader.js')]
  }]
}

升级版

目录:webpack.config.js
(1)
module.exports = {
    resolveLoader: { // ---------------------------- resolveLoader
      modules: ['node_modules', './src/loaders/'] // 表示在寻找loader时,先去node_modules中找,再去loaders文件夹中找
    },
]
(2)
{
  loader: 'replace-loader', // -------------------- 加载replace-loader,即文件名
  options: {
    name: 'hi!!!!!!!!@!!!!!!' // ---------------------- options的name属性
  }
}


目录:src/loaders/replace-loader
const loaderUtils = require('loader-utils') // ---------------- loader-utils
module.exports = function(source) {
  console.log(this.query) // options配置对象

  const options = loaderUtils.getOptions(this) // ------------- loaderUtils.getOptions(this) 获取 options
  const callback = this.async() // ---------------------------- 处理异步loader, this.async()返回this.callback
  setTimeout(function() {
    const result = source.replace('hello', options.name)
    callback(null, result)
  }, 1000)
}

资料

我的简书:https://www.jianshu.com/p/1fc4640f4538
webpack优化:https://juejin.im/post/6844903782581534727#heading-7
process.env https://www.jianshu.com/p/c8f9c61c2f20

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容