深入浅出Webpack.pdf

入门

module

文件级别,是指输出前

chunk

输出后的 module 的合集

hash

工程级别,在 outpput 的 filename中使用hash

chunkhash

文件级别的,一般在 output 的 chunkFilename 中使用它

contenthash

在css分离的时候使用 contenthash

模块化

CommonJS

  • 代码可用于Node.js 环境,例如做同构应用

  • 通过npm 发布的很多第三方模块都采用该规范

  • 缺点:无法直接运行在浏览器环境下,必须通过工具转换成标准的ES5

AMD

  • 可在不转换代码的情况下直接在浏览器中运行

  • 可异步加载依赖

  • 可并行加载多个依赖

  • 代码可运行在浏览器环境和 Node.js 环境下

  • 缺点:JS 运行环境没有原生支持 AMD,需要先导入实现了 AMD 的库

ES6

  • ECMA 提出的 JS 模块化规范

  • 浏览器和服务器通用的终极模块解决方案

  • 缺点:目前无法直接运行在JS运行环境,需通过工具转换成ES5

样式模块化

将样式片段放入通用文件,在另一个文件通过 @import 引入使用

  1. // util.scss
  2. @mixin center {
  3. // 水平竖直居中
  4. position: absolute;
  5. left: 50%;
  6. top: 50%;
  7. transform: translate(-50%, -50%);
  8. }
  1. // main.scss
  2. // 导入和使用 util.scss 中定义的样式片段
  3. @import "util";
  4. #box {
  5. @include center;
  6. }

构建

  • 代码转换:将TS编译成JS,将SCSS编译成CSS等
  • 文件优化:压缩混淆静态资源
  • 代码分割:提取多个页面的公共代码,提取首屏不需要执行部分的代码让其异步加载
  • 模块合并:在采用模块化的项目里会有多个模块和文件,需要通过构建功能将模块分类合并成一个文件
  • 自动刷新:监听本地源码的变化,自动重新构建、刷新浏览器
  • 代码校验:在代码被提交到仓库前需要校验代码是否符合规范,以及单元测试是否通过
  • 自动发布:更新代码后,自动构建出线上发布代码并传输给发布系统

npm script

  • 内置,无需安装其他依赖

  • 缺点:功能太简单了,不方便管理多个任务之间的依赖

Grunt

  • 进化版的 npm script

  • 灵活:只负责执行我们定义的任务

  • 大量的可复用插件封装好了常见的构建任务

  • 缺点:集成度不高,要写很多配置后才可以使用,无法做到开箱即用

  • 配置文件:Gruntfile.js

  • 执行命令:Grunt dev

Gulp

  • 基于流的自动化构建工具

  • Grunt的进化版,除了可以管理和执行任务,还支持监听、读写文件、流式处理

  • 提供系列插件处理流,流可以在插件之间传递

  • 缺点:集成度不高,要写很多配置后才可以使用,无法做到开箱即用

  • gulp.task: 注册一个任务

  • gulp.run: 执行任务

  • gulp.watch: 监听文件的变化

  • gulp.src: 读取文件

  • gulp.dest: 写文件

  • pipe(): 管道

webpack

  • 一切文件皆模块,通过 Loader 转换文件,通过 Plugin 注入钩子

  • 专注于处理模块化的项目,能做到开箱即用

  • 可通过 Plugin 扩展,完整好用又不失灵活

  • 使用场景不局限于Web开发

  • 社区庞大活跃,经常引入紧跟时代发展的新特性,能为大多数场景找到已有的开源扩展

  • 良好的开发体验

  • 缺点:只能用于采用模块化开发的项目

Rollup

  • 只专注于ES6 的模块打包工具

  • 能针对ES6进行 Tree Shaking,以去除那些已被定义但没被使用的代码并进行 Scope Hoisting,以减小输出文件的大小和提升运行性能

  • 生态链还不完善,体验不如 Webpack

  • 功能不如Webpack完善,但配置和使用更简单

  • 不支持 Code Spliting,但是打包出来的代码中没有 Webpack 那段模块的加载、执行和缓存的代码,即打包出来的代码更小更快

Webpack

使用 Loader(模块转换器)

  • Loader 可以看作是具有文件转换功能的翻译员

  • use 属性的值需要是一个由 Loader 名称组成的数组, Loader 的执行顺序是由后到前的

  • 每个 Loader 都可以通过 URL querystring 的方式传入参数。例如 css-loader?minimize 中的 minimize 告诉 css-loader 要开启 css 压缩

使用 Plugin(扩展插件)

plugins 属性是一个数组,里面的每一项都是插件的一个实例,在实例化一个组件时可以通过构造函数传入这个组件支持的配置属性

使用 DevServer

DevServer 会启动一个 HTTP 服务器用于服务网页请求,同时会帮助启动 webpack, 并接收 webpack 发出的文件变更的信号,通过 WebSocket 协议自动刷新网页做到实时预览。

DevServer 会将 Webpack 构建出的文件保存在内存中,在要访问输出的文件时,必须通过 HTTP 服务访问。

DevServer 会忽略 webpack.config.js 中配置的 output.path 属性

实时预览

  • webpack 默认关闭监听模式

  • 可通过 webpack —watch 来开启

  • DevServer 默认开启监听模式

    • DevServer 会在构建出的 JS 代码中注入一个代理客户端用于控制网页

    • 网页和 DevServer 之间通过 Webpack 协议通信,以方便 DevServer 主动向客户端发送命令

    • DevServer 在收到 Webpack 的文件变化通知时,通过注入的客户端控制网页刷新

  • 只有 entry 入口的文件能够实时预览

模块热替换

  • 默认关闭

  • 启动 DevServer 时添加 —hot参数

  • 在文件修改时只替换已更新模块,而不需要重新加载整个网页

SourceMap

  • 在启动webpack时添加 —devtool source-map

webpack 构建打包流程

  • Module:一切皆模块,一个文件即一个模块。webpack 会从配置的 Entry 开始递归找出所有依赖的模块

  • Chunk:代模块,一个 Chunk 由多个模块组合而成,用于代码合并与分割

webpack 会从 Entry 配置的 Module 开始,递归解析 Entry 依赖的所有 Module。每找到一个 Module,就会根据配置的 Loader 去找出对应的转换规则,对 Module 进行转换后,再解析出当前 Module 依赖的 Module。这些模块会以 Entry 为单位进行分组,一个 Entry 及其所有依赖的 Module 被分到一个组也就是一个 Chunk。最后,webpack会将所有 Chunk 转换成文件输出。在整个流程中,webpack 会在恰当的时机执行 Plugin 里定义的逻辑。

配置

  • Entry:模块的入口
  • Output:最终代码输出
  • Module: 配置处理模块的规则
  • Resolve: 配置寻找模块的规则
  • Plugin:配置扩展插件
  • DevServer:配置DevServer
  • 其他:其他零散的配置项

    整体配置结构:整体地描述各配置项的结构

    ```javascript const path = require(‘path’);

// 定义了一些文件夹的路径 const ROOT_PATH = path.resolve(__dirname); const APP_PATH = path.resolve(ROOT_PATH, ‘src’); const APP_PATH2 = path.resolve(ROOT_PATH, ‘src/index2.js’); const BUILD_PATH = path.resolve(ROOT_PATH, ‘dist’);

module.exports = { // 项目的文件夹 可以直接用文件夹名称 默认会找index.js 也可以确定是哪个文件名字 // entry 表示入口,webpack 执行构建的第一步将从 Entry 开始,可抽象成输入 entry: APP_PATH, // 只有1个入口,入口只有1个文件 entry: [APP_PATH, APP_PATH2], // 只有1个入口,入口有2个文件 entry: {a: APP_PATH, b: APP_PATH2}, // 有2个入口

// 输出的文件名 合并以后的js会命名为bundle.js output: { path: BUILD_PATH, // 输出文件的存放目录,必须是 string 类型的绝对路径

  1. // 输出文件的名称
  2. filename: 'build.js', // 完整的名称
  3. filename: '[name].js', // 在配置了多个 entry 时,通过名称模版为不同的 entry生成不同的文件名称
  4. filename: '[chunkhash].js', // 根据文件内容的 Hash 值生成文件的名称,用于浏览器长时间缓存文件
  5. // 发布到线上的所有资源的 URL 前缀,为 string 类型
  6. publishPath: '/assets/', // 放到指定目录下
  7. publishPath: '', // 放到根目录下
  8. publishPath: 'https://cdn.example.com/', // 放到 CDN 上
  9. // 导出库的名称,为 string 类型
  10. // 不填它时,默认的输出格式是匿名的立即执行函数
  11. library: 'MyLibrary',
  12. // 导出库的类型,为枚举类型,默认是 var
  13. // 可以是 umd | umd2 | commonjs2 | commonjs | amd | this | var | assign | window | global | jsonp
  14. libraryTarget: 'umd',
  15. // 是否包含有一的文件路径信息到生成的代码里,为 boolean 类型
  16. pathinfo: true,
  17. // 附加 Chunk 的文件名称
  18. chunkFilename: '[id].js',
  19. chunkFilename: '[chunkhash].js',
  20. // JSONP 异步加载资源时的回调函数名称,需要和服务器搭配使用
  21. jsonpFunction: 'myWebpackJsonp',
  22. // 生成的 Source Map 的名称
  23. sourceMapFilename: '[file].map',
  24. // 浏览器开发者工具里显示的源码模块名称
  25. devtoolModuleFilenameTemplate: 'webpack:///[resource-path]',
  26. // 异步加载跨域的资源时使用的方式
  27. crossOriginLoading: 'use-credentials',
  28. crossOriginLoading: 'anonymous',
  29. crossOriginLoading: false,

},

// devserver 配置 devServer: { proxy: { // DevServer 相关的配置 ‘api’: ‘https://localhost:3000‘ }, contentBase: path.jion(__dirname, ‘public’), // 配置 DevServer HTTP 服务器的文件根目录 compress: true, // 是否开启 Gzip 压缩

  1. historyApiFallback: true, // 是否开启 H5 History API 网页
  2. historyApiFallback: {
  3. rewrites: [
  4. // /user 开发的都返回 user.html
  5. { from: /^\user/, to: '/user.html' },
  6. { from: /^\game/, to: '/game.html' },
  7. // 其他的都返回 index.html
  8. { from: /./, to: '/index.html' },
  9. ]

}, hot: true, // 是否开启模块热替换功能 https: false, // 是否开启 HTTPS 模式 profile: true, // 是否捕捉 webpack 构建的性能信息,用于分析是什么原因导致构建性能不佳 cache: false, // 是否启用缓存来提升构建速度 watch: true, // 是否开启监听者模式 watchOptions: { // 不监听的文件或文件夹,支持正则匹配,默认为空 ignored: /node_modules/, // 监听到变化发生后,等300ms再执行动作,截流,防止文件更新太快导致重新编译频率太快。默认为 300ms aggregateTimeout: 300, // 不停地询问系统指定的文件有没有发生变化,默认1000次/s poll: 1000 }, inline: true, progress: true, },

// 配置模块相关 module: { rules: [ // 配置 Loader

  1. {
  2. test: /\.(jsx|js)$/, // 正则命中要使用 Loader 的文件
  3. use: 'babel-loader?cacheDirectory',
  4. exclude: path.resolve(__dirname, 'node_modules') // 忽略这里面的文件
  5. },
  6. {
  7. test: /\.(css|less)$/,
  8. use: [{ // 使用哪些 Loader,有先后次序,从后向前执行
  9. loader: 'style-loader',
  10. options: { // 向 loader 传入一些参数
  11. singleton: true, // 是否只使用一个style标签
  12. transform: './src/css/transform.js', // 可以执行一个js,在loader执行的时候执行,也就是浏览器环境,能拿到浏览器的相关信息
  13. }
  14. }, {
  15. loader: 'css-loader'
  16. }, {
  17. loader: 'less-loader'
  18. }],
  19. exclude: path.resolve(__dirname, 'node_modules')
  20. },
  21. {
  22. test: /\.(png|svg|jpg|gif|webp|ico)$/,
  23. use: [
  24. 'file-loader'
  25. ],
  26. exclude: path.resolve(__dirname, 'node_modules')
  27. },
  28. {
  29. test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
  30. use: [{
  31. loader: 'url-loader',
  32. query: {
  33. limit: 10000
  34. }
  35. }],
  36. include: path.resolve(__dirname, 'assets/fonts') // 只会命中这里的文件
  37. },
  38. {
  39. test: /\.(jsx|js)$/,
  40. use: 'eslint-loader',
  41. exclude: path.resolve(__dirname, 'node_modules')
  42. }
  43. ],
  44. noParse: [ // 不用解析和处理的模块
  45. /special-library\.js/ // 用正则匹配
  46. ]

},

// 配置插件 plugins: [],

// 配置寻找模块的规则 resolve: { modules: [ // 寻找模块的根目录,为 array 类型,默认以 node_modules 为根目录 ‘node_modules’, APP_PATH ] // 自动补全的拓展名 extensions: [‘.js’, ‘.jsx’], // 路径别名,用于映射模块 alias: { ‘assets’: path.resolve(dirname, ‘assets’), ‘templates’: path.resolve(dirname, ‘src/templates’), ‘components’: path.resolve(dirname, ‘components’), ‘only$’: path.resolve(dirname, ‘only’), // 使用结尾符号 $ }, symlinks: true, // 是否跟随文件的软链接去搜寻模块的路径 descriptionFiles: [‘package.json’], // 模块的描述文件 mainFields: [‘main’], // 模块的描述文件里描述入口的文件的字段名 enforceExtension: false, // 是否强制导入语句写明文件后缀 },

// 输出文件的性能检查配置 performance: [ hints: ‘warning’, // 有性能问题时输出警告 hints: ‘error’, // 有性能问题时输出错误 hints: false, // 关闭性能检查

  1. maxAssetSize: 200000, // 最大文件的大小(单位为bytes)
  2. maxEntrypointSize: 400000, // 最大入口文件的大小
  3. assetFilter: function(assetFilename) { // 过滤要检查的文件
  4. return assetFilename.endsWith('.css') || assetFilename.endsWith('.js')
  5. }

],

devtool: ‘source-map’, // 配置 source-map 类型 context: __dirname, // webpack 使用的根目录, string 类型必须是绝对路径

// 配置输出代码的运行环境 target: ‘web’, // 浏览器,默认 target: ‘webworker’, // WebWorker target: ‘node’, // Node.js,使用’require’语句加载 chunk 代码 target: ‘async-node’, // Node.js,异步加载 chunk 代码 target: ‘node-webkit’, // nw.js target: ‘electron-main’, // electron,主线程 target: ‘electron-renderer’, // electron,渲染线程

externals: { // 使用来自 JS 运行环境提供的全局变量 jquery: ‘jQuery’ },

stats: { // 控制台输出日志控制 assets: true, colors: true, errors: true, errorDetails: true, hash: true,

};

  1. style-loader 中用到的 transform.js
  2. ```javascript
  3. module.exports = function(css) {
  4. if(window.innerWidth > 400) {
  5. // css += 'html{background: aqua;}'
  6. css = css.replace('red', 'aqua')
  7. }else {
  8. css = css.replace('aqua', 'red')
  9. }
  10. console.log(css);
  11. return css;
  12. }

多种配置类型

  1. const path = require('path');
  2. module.exports = [ // 导出一个数组,数组中包含的每份配置都会执行一遍构建
  3. // 采用 Object 描述的一份配置
  4. {
  5. // ...
  6. },
  7. // 采用函数描述的一份配置
  8. function (env = {}, argv) {
  9. const minimizer = [];
  10. const isProd = env['prod'];
  11. // 在生产环境中才压缩
  12. if (isProd) {
  13. minimizer.push(
  14. new UglifyJSPlugin({
  15. cache: true,
  16. parallel: true,
  17. uglifyOptions: {
  18. compress: true,
  19. ecma: 6,
  20. mangle: true,
  21. },
  22. sourceMap: false,
  23. })
  24. )
  25. }
  26. return {
  27. // ...
  28. optimization: {
  29. minimizer: minimizer
  30. }
  31. // 在生产环境中不输出 Source Map
  32. devtool: isProd ? undefined : '#eval-source-map',
  33. // ...
  34. };
  35. },
  36. // 采用异步函数描述的一份配置
  37. function () {
  38. return new Promise((resolve, reject) => {
  39. setTimeout(() => {
  40. resolve({
  41. // ...
  42. })
  43. }, 5000)
  44. })
  45. }
  46. ]

当运行 webpack 时,会向函数传入两个参数:

  • env: 当前运行时的 webpack 专属环境变量,env 是一个 Obj, 读取时直接访问 Obj 的属性,将它设置为需要在启动 webpack 时带上的参数,例如启动命令是 webpack —env.prod —env.bao=foo, 则 env 的值是 {“prod”: true, “bao”: “foo”}

  • argv: 代表在启动 webpack 时通过命令行传入的所有参数,例如 —config、—env、—devtool, 可以通过 webpack -h 列出所有 webpack 支持的命令行参数

实战

Babel

babelrc

.babelrc

  1. {
  2. "presets": ["es2015", "stage-0", "react"],
  3. "plugins": [
  4. "transform-runtime",
  5. ["import", { "libraryName": "antd", "libraryDirectory": "es", "style": "css" }]
  6. ]
  7. }

presets

要转换的源码使用了哪些新的语法特性,“预设”其实是一组“插件”的集合

  • 已被写入ES标准里的特性:”es2015”、”es2016”、”es2017”、”env”(包含前3个)
  • 社区特性:”stage-0”、”stage-1”、”stage-2”、”stage-3”、”stage-4”
  • 特殊场景语法特性:”react”

plugins

插件可以控制如何转换代码

  • “transform-runtime”:babel-plugin-transform-runtime 和 babel-runtime 配套使用,减少冗余代码

接入 webpack

  1. module.exports = {
  2. // ...
  3. // 输出 source-map 以方便直接调试 ES6 代码
  4. devtool: '#source-map',
  5. module: {
  6. rules: [
  7. // exclude 排除,不需要编译的目录
  8. {
  9. test: /\.(jsx|js)$/,
  10. use: 'babel-loader',
  11. exclude: path.resolve(__dirname, 'node_modules')
  12. },
  13. ]
  14. }
  15. }

TS

tsconfig.json

  1. {
  2. "compilerOptions": {
  3. "importHelpers": true
  4. }
  5. }

开启 TS 的 importHelpers 选项可以让 ES6 转 ES5 语法时注入的辅助函数不会重复出现在多个文件中,避免代码冗余。

接入 webpack

  1. npm i -D typescript awesome-typescript-loader
  1. module.exports = {
  2. // ...
  3. // 输出 source-map 以方便直接调试 TS 代码
  4. devtool: '#source-map',
  5. resolve: { extensions: ['.ts', '.js'] },
  6. module: {
  7. rules: [
  8. {
  9. test: /\.ts$/,
  10. use: 'awesome-typescript-loader',
  11. exclude: path.resolve(__dirname, 'node_modules')
  12. },
  13. ]
  14. }
  15. }

SCSS

  1. npm i -D node-sass sass-loader css-loader style-loader
  1. module.exports = {
  2. // ...
  3. // 输出 source-map 以方便直接调试 TS 代码
  4. devtool: '#source-map',
  5. resolve: { extensions: ['.ts', '.js'] },
  6. module: {
  7. rules: [
  8. {
  9. test: /\.(scss|less)$/,
  10. use: [{
  11. loader: 'style-loader',
  12. options: {
  13. singleton: true
  14. }
  15. }, {
  16. loader: 'css-loader'
  17. }, {
  18. loader: 'sass-loader'
  19. }],
  20. exclude: path.resolve(__dirname, 'node_modules')
  21. },
  22. ]
  23. }
  24. }

PostCSS

  • 向CSS 自动加前缀,使得转换成目前的浏览器识别的CSS,如—webkit
  • postcss-cssnext 插件可以让我们使用下一代 CSS 语法写代码(现代浏览器都已经支持下一代 CSS语法了,所以可以不装插件了)
  1. npm i -D style-loader css-loader postcss-loader less-loader postcss-cssnext

postcss.config.js

  1. module.exports = {
  2. plugins: [
  3. require('postcss-cssnext')
  4. ]
  5. }

接入 webpack

  1. module.exports = {
  2. // ...
  3. module: {
  4. rules: [
  5. {
  6. test: /\.less$/,
  7. use: [{
  8. loader: 'style-loader',
  9. options: {
  10. singleton: true
  11. }
  12. }, {
  13. loader: 'css-loader'
  14. }, {
  15. loader: 'postcss-loader'
  16. }, 'less-loader'],
  17. exclude: path.resolve(__dirname, 'node_modules')
  18. },
  19. ]
  20. }
  21. }

React

React 与 Babel

  1. npm i -D react react-dom Babel-preset-react

.babelrc

  1. {
  2. "presets": ["es2015", "stage-0", "react"],
  3. "plugins": [
  4. "transform-runtime",
  5. ["import", { "libraryName": "antd", "libraryDirectory": "es", "style": "css" }]
  6. ]
  7. }

React 与 TS

  1. npm i -D react react-dom @types/react @types/react-dom typescript awesome-typescript-loader

tsconfig.json

  1. {
  2. "compilerOptions": {
  3. "jsx": "react" // 开启 JSX,支持 React
  4. }
  5. }
  1. module.exports = {
  2. // ...
  3. // 输出 source-map 以方便直接调试 TS 代码
  4. devtool: '#source-map',
  5. resolve: { extensions: ['.ts', '.tsx', '.js'] },
  6. module: {
  7. rules: [
  8. {
  9. test: /\.tsx?$/, // 同时匹配ts、tsx 后缀
  10. use: 'awesome-typescript-loader',
  11. exclude: path.resolve(__dirname, 'node_modules')
  12. },
  13. ]
  14. }
  15. }

Vue

  1. npm i -S vue
  2. npm i -D vue-loader css-loader vue-template-compiler

vue 与 TS

tsconfig.json

  1. {
  2. "compilerOptions": {
  3. "target": "es5",// vue 支持的浏览器支持保持一致
  4. "strict": true, // 可以对this上的数据属性进行更严格的推断
  5. "module": "es2015", // 使 tree shaking 生效
  6. "moduleResolution": "node",
  7. }
  8. }
  1. npm i -D ts-loader typescript
  1. module.exports = {
  2. // ...
  3. // 输出 source-map 以方便直接调试 TS 代码
  4. devtool: '#source-map',
  5. resolve: { extensions: ['.ts', '.vue', '.js', '.json'] },
  6. module: {
  7. rules: [
  8. {
  9. test: /\.ts$/,
  10. use: ['ts-loader'],
  11. options: {
  12. // 让 tsc 将 vue 文件当成一个 TS 模块去处理,以解决 module not found 的问题,tsc 本身不会处理.vue 结尾的文件
  13. appendTsSuffixTo: [/\.vue$/],
  14. }
  15. },
  16. ]
  17. }
  18. }

Angular2

tsconfig.json

  1. {
  2. "compilerOptions": {
  3. "target": "es5",
  4. "module": "commonjs",
  5. "sourceMap": "true",
  6. "experimentalDecorators": true, // 开启对注解的支持
  7. "lib": ["es2015", "dom"], // ng2 依赖新的 JS API DOM 操作
  8. "exclude": "/node_modules/*",
  9. }
  10. }

单页面注入 HTML

  • 采用ES6 及 React
  • 为页面加入 Google Analytics
  • 为页面加入 Disqus 用户评论,这部分代码需要异步加载以提升首屏加载速度
  • 压缩和分离 JS 和 CSS 代码,提升加载速度

web-webpack-plugin

web-webpack-plugin

  1. const path = require('path');
  2. const { WebPlugin } = require('web-webpack-plugin');
  3. module.exports = {
  4. // ...
  5. plugins: [
  6. new WebPlugin({
  7. template: './template.html', // html 模版文件所在的文件路径
  8. filename: 'index.html' // 输出的 html 的文件名称
  9. })
  10. ]
  11. }
  1. <!doctype html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="utf-8">
  5. <title>Dbox UI</title>
  6. <link rel="icon" href="favicon.ico" type="image/x-icon" />
  7. <link rel="shortcut icon" href="favicon.ico" type="image/x-icon" />
  8. <!-- 注入 chunk app 中的 CSS 代码 -->
  9. <link rel="stylesheet" href="app?_inline" />
  10. <!-- 注入 google_analytics 中的 JS 代码 -->
  11. <script src="./google_analytics.js?_inline"></script>
  12. <!-- 异步加载 Disqus 评论 -->
  13. <script src="https://dive-into-webpack.disqus.com/embed.js"></script>
  14. </head>
  15. <body>
  16. <div id="root"></div>
  17. <!-- built files will be auto injected -->
  18. <!-- 导入 Chunk app 中的 JS 代码 -->
  19. <script src="app"></script>
  20. <!--Disqus 评论容器 -->
  21. <div id="disqus_thread"></div>
  22. </body>
  23. </html>

app?_inline:

  • app: 表示资源内容来自哪里

  • querystring: 表示这些资源的注入方式。?_inline&_dist

    • _inline : 表示这些代码需要被内嵌到这个标签所在的位置

    • _dist : 只有在生产环境下才引入该资源

    • _dev : 只有在开发环境下才引入该资源

    • _ie : 只有在 IE 浏览器下才需要引入该资源,通过<[if IE] > resource <![endif]>注释实现

管理多个单页应用

  • 项目由多个单页应用组成,如主页index.html, 用户登陆页 login.html
  • 多个应用之间会有公共的代码部分,将公共部分抽离成独立的文件以防止重复加载。例如多个页面都使用了一套 CSS 样式 ,都采用了 React 框架
  • 随着业务的发展会不断加入新的单页应用,但是在加入新应用时不能改动构建相关的代码

解决方案:
web-webpack-plugin

目录结构

  1. ├── pages
  2. ├── index
  3. ├── index.css // 该页面单独需要的 CSS 样式
  4. ├── index.js // 该页面的入口文件
  5. ├── login
  6. ├── index.css
  7. ├── index.js
  8. ├── common.css // 所有页面都需要的公共 CSS 样式
  9. ├── google_analytics
  10. ├── template.html
  11. ├── webpack.config.js

修改 webpack 配置

  1. const path = require('path');
  2. const { WebPlugin } = require('web-webpack-plugin');
  3. const AutoWebPlugin = new WebPlugin({
  4. template: './template.html', // html 模版文件所在的文件路径
  5. postEntrys: ['./common.css'], // 所有页面的依赖的通用文件
  6. commonsChunk: { name: 'common' }, // 提取出公共代码 chunk 的名称
  7. filename: 'index.html', // 输出的 html 的文件名称
  8. })
  9. module.exports = {
  10. // ...
  11. // 需要添加入口
  12. entry: {
  13. AutoWebPlugin.entry({
  14. // 这里可以加入额外需要的 Chunk 入口
  15. })
  16. },
  17. plugins: [
  18. AutoWebPlugin,/
  19. ]
  20. }
  1. <!doctype html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="utf-8">
  5. <title>Dbox UI</title>
  6. <link rel="icon" href="favicon.ico" type="image/x-icon" />
  7. <link rel="shortcut icon" href="favicon.ico" type="image/x-icon" />
  8. <!-- 在这注入该页面所依赖但没有手动导入 CSS -->
  9. <!-- STYLE -->
  10. <!-- 注入 google_analytics 中的 JS 代码 -->
  11. <script src="./google_analytics.js?_inline"></script>
  12. <!-- 异步加载 Disqus 评论 -->
  13. <script src="https://dive-into-webpack.disqus.com/embed.js"></script>
  14. </head>
  15. <body>
  16. <div id="root"></div>
  17. <!-- built files will be auto injected -->
  18. <!-- 在这注入该页面所依赖但没有手动导入的JS -->
  19. <!-- SCRIPT -->
  20. <!--Disqus 评论容器 -->
  21. <div id="disqus_thread"></div>
  22. </body>
  23. </html>
  • : 将 CSS 类型的文件注入 所在的位置,如果 不存在,就注入 HTML HEAD 标签的最后

  • :将 JS 类型的文件注入 所在的位置,如果 不存在,就注入 HTML BODY 标签的最后

同构应用

VDOM

  • 通过 DOM Diff 算法能找出两个不同 obj 的最小差异,得出最小的 DOM 操作

  • VDOM 在渲染时不仅可以通过操作 DOM 树表示结果,也可以有其他表示方式,例如将VDOM 渲染成字符串(服务器端渲染) 或者渲染成手机 APP 原生的 UI 组件( RN )

react-dom

react-dom 通过两种方式渲染 VDOM

  • 通过 render() 函数去操作浏览器 DOM 树来展示出结果

  • 通过 renderToString() 函数计算表示 VDOM 的 HTML 形式的字符串

同构

一份源码构建出两份 JS 代码

  • 用于在浏览器端运行

  • 用于在 Node.js 中运行并渲染出 HTML

    • 不能包含浏览器环境提供的 API,例如 document 进行 DOM 操作,Node 不支持这些 API

    • 不能包含 CSS 代码,因为服务端渲染的目的是渲染出 HTML 的内容,渲染出 Css 代码会增加额外的计算量,影响服务器的渲染性能

    • 不能像浏览器环境的输出代码那样将 node_modules 里的第三方模块和 Node.js 原生模块打包进去,而是需要通过 CommonJS 规范引入这些模块

    • 需要通过 CommonJS 规范导出一个渲染函数,用于在 HTTP 服务器中执行这个渲染函数,渲染出 HTML的内容后返回

webpack_server.config.js

  1. const path = require('path');
  2. const { nodeExternals } = require('webpack-node-externals');
  3. // 定义了一些文件夹的路径
  4. const ROOT_PATH = path.resolve(__dirname);
  5. const APP_PATH = path.resolve(ROOT_PATH, './main_server.js');
  6. const BUILD_PATH = path.resolve(ROOT_PATH, './dist');
  7. module.exports = {
  8. // JS 执行入口文件
  9. entry: APP_PATH,
  10. target: 'node', // Node.js,使用'require'语句加载 chunk 代码,为了不将 Node.js 内置的模块打包进输出文件中, 因为运行环境就是 Node
  11. externals: [ // 使用来自 JS 运行环境提供的全局变量
  12. nodeExternals() // 为了不将 node_modules 目录下的第三方模块打包进输出文件中,因为 Node 会默认去 node_modules 寻找和使用第三方模块
  13. ],
  14. output: {
  15. path: BUILD_PATH,
  16. filename: 'bundle.server.js',
  17. libraryTarget: 'commonjs2', // 采用 Node 的 HTTP 服务
  18. },
  19. devtool: 'source-map', // 配置 source-map 类型
  20. // css 处理
  21. module: {
  22. rules: [
  23. // exclude 排除,不需要编译的目录
  24. {
  25. test: /\.(jsx|js)$/,
  26. use: 'babel-loader',
  27. exclude: path.resolve(__dirname, 'node_modules')
  28. },
  29. {
  30. test: /\.(css|less)$/,
  31. use: [{ loader: 'ignore-loader' }], // 忽略依赖的 CSS 文件,CSS 会影响服务端的渲染性能,也是做服务端渲染的不重要的部分
  32. },
  33. {
  34. test: /\.(png|svg|jpg|gif|webp|ico)$/,
  35. use: [
  36. 'file-loader'
  37. ],
  38. exclude: path.resolve(__dirname, 'node_modules')
  39. },
  40. {
  41. test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
  42. use: [{
  43. loader: 'url-loader',
  44. query: {
  45. limit: 10000
  46. }
  47. }],
  48. include: path.resolve(__dirname, 'assets/fonts')
  49. },
  50. {
  51. test: /\.(jsx|js)$/,
  52. use: 'eslint-loader',
  53. exclude: path.resolve(__dirname, 'node_modules')
  54. }
  55. ]
  56. }
  57. };

根组件 AppComponent.js

  1. import React, { Component } from "react";
  2. import './main.css';
  3. class AppComponent extends Component {
  4. render() {
  5. return <h1>Hello, Webpack</h1>;
  6. }
  7. }
  8. export default AppComponent;

main_browser.js

  1. import React, { Component } from "react";
  2. import { render } from "react-dom";
  3. import AppComponent from "./AppComponent";
  4. render(<AppComponent />, window.document.getElementById('app'));

main_server.js

  1. import React, { Component } from "react";
  2. import { renderToString } from "react-dom";
  3. import AppComponent from "./AppComponent";
  4. export function render() {
  5. // 将根组件渲染成 HTML 字符串
  6. return renderToString(<AppComponent />)
  7. }

http_server.js

  1. const express = require('express')
  2. const { render } = require('./dist/bundle.server')
  3. // 返回 HTTP 服务器自带配置系统
  4. const app = express()
  5. // 配置路由
  6. // 默认路由
  7. app.get('/', function (req, res) {
  8. res.send(`
  9. <!DOCTYPE html>
  10. <html lang="en">
  11. <head>
  12. <meta charset="UTF-8">
  13. <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
  14. <title>Demo</title>
  15. </head>
  16. <body>
  17. <div id="app">${render()}</div>
  18. <script src="./dist/bundle.server.js"></script>
  19. </body>
  20. </html>
  21. `)
  22. })
  23. // 其他请求路径返回对应的本地文件
  24. app.use(express.static('.'))
  25. app.listen(3000, function () {
  26. console.log('\033[96m + \033[39m app listening on * : 3000');
  27. })
  1. npm i -D css-loader style-loader ignore-loader webpack-node-externals
  1. npm i -S express
  1. webpack --config webpack_server.config.js # 构建出 ./dist/bundle.server.js
  1. webpack # 构建出 ./dist/bundle.browser.js

Electron

Node.js + Chromium

主进程main.js

webpack 配置

添加一行即可:

  1. target: 'electron-renderer', // electron,渲染线程

构建 NPM 模块

webpack 只适合于构建完整不可分割的 NPM 模块,像antd 这种由大量独立组件组成的库将不再适合

构建离线应用

  • 在没有网络的情况下也能打开网页

  • 由于部分被缓存的资源直接从本地加载,所以对用户来说可以加快网页的加载速度,对网站运营者来说可以减少服务器的压力及传输流量费用

  • 核心是”离线缓存技术“:Service Workers 通过拦截网络请求实现离线缓存,也是构建 PWA 应用的关键技术之一

Service Workers

检查代码

  • 代码风格

  • 潜在问题:分析代码在运行过程中可能出现的潜在 Bug

检查工具

最佳实践

  • 由于执行检查步骤的计算量大,所以整合到 webpack 中会导致构建变慢

  • 在整合到 webpack 后,输出的错误信息是通过行号来定位错误的,没有编辑器集成显示错误直观

  • 使用集成了代码检查功能的编辑器,让编辑器实时、直观地显示错误

  • 将代码检查步骤放到代码提交时,也就是说在代码提交前调用检查工具去检查代码,只有在检查都通过时才提交代码,这样就保证提交到仓库的代码都通过了检查

图片(PDF|SWF)加载

file-loader

将 JS 和 CSS 中导入图片的语句替换成正确的地址,同时将文件输出到对应的位置

url-loader

  • 将文件的内容经过 base64 编码后的字符串注入 JS 或 CSS 中

  • 将网页需要用的小图片注入代码中, 以减少加载次数

  • 图片体积太大会导致因JS|CSS文件过大而带来的网页加载缓慢的问题

图片优化

加载SVG

  • svg 是文本格式的文件

  • 比位图清晰,在任意缩放的情况下都不会破坏图形的清晰度,能方便地解决高分辨率屏幕下图像显示不清楚的问题

  • 在图形线条比较简单的情况下,SVG 大小要小于位图

  • 图形相同的 SVG 比对应的高清图有更好的渲染性能

  • SVG 采用和 HTML 一致的 XML 语法描述。灵活性很高

使用

  • 直接当成图片使用引入到HTML 、CSS 中

  • raw-loader: 将 svg 文本文件的内容读取出来,注入 JS 或 CSS 中

  • svg-inline-loader:增加了 SVG 的压缩功能

SourceMap

设置devtool

  • dev: cheap-module-eval-source-map, 速度最快。因为 dev 环境下不会做代码压缩,即使没有列信息也不会影响断点调试

  • prod:hidden-source-map,最详细但不暴露出去。由于 prod 环境代码压缩后只有一行,所以需要列信息


分解记忆**

  • eval: 不会生成.map文件,因此不能用于生产环境
  • module:会对引入的库做映射
  • cheap:会提示到第几行报错, 但不会有列信息。

加载现有的SourceMap

webpack 默认不会加载第三方模块附加的 SourceMap 文件,需要使用 source-map-loader , 同时采用 include 去命中自己关心的文件,以提升构建速度。在使用第三方模块出现问题时方便调试。

优化

缩小文件搜索范围

Loader 配置

  • test

  • use

  • include

  • exclude

  • cacheDirectory: babel 支持缓存转换出的结果

  1. module.exports = {
  2. //...
  3. rules: [
  4. {
  5. test: /\.(jsx|js)$/,
  6. use: 'babel-loader?cacheDirectory',
  7. exclude: path.resolve(__dirname, 'node_modules')
  8. },
  9. ]
  10. }

resolve.modules 配置

配置Webpack去哪些目录下找第三方模块,默认是[‘node_modules’], 通过配置绝对路径减少搜索步骤

  1. module.exports = {
  2. resolve: {
  3. modules: [path.resolve(__dirname, 'node_modules')]
  4. }
  5. }

resolve.mainFields 配置

用于配置第三方模块使用哪个入口文件;可用于不同的运行环境下使用不同的代码,例如在浏览器中通过原生的fetch 或者 XMLHttpRequest 实现,在 Node.js 中通过 http 模块实现

resolve.mainFields 的默认值和当前的 target 配置有关系

  • 当 target 为 web 或者 webworker 时,值是[“browser”, “module”, “main”]

  • 当 target 为其他情况时,值是 [“module”, “main”]

由于大多数第三方模块都采用 main 字段去描述入口文件的位置,所以可以通过配置 mainFields 字段为 main 值,减少搜索步骤,但是注意只要有一个模块出错,都可能会造成构建出的代码无法正常运行

  1. module.exports = {
  2. resolve: {
  3. mainFields: ['main'],
  4. }
  5. }

resolve.alias 配置

  • 通过配置alias 使得不同环境引用不同的代码,如react-native-web 的使用

  • 由于webpack 默认会从第三方模块中的 package.json 中的指定入口文件递归解析和处理依赖文件,一般情况下该入口文件会定义成包含检查和警告的未被压缩的代码,但是直接使用单独、完整的min.js文件可以跳过耗时的递归解析操作

  1. module.exports = {
  2. resolve: {
  3. alias: {
  4. 'react': path.resolve(__dirname, './node_modules/react/dist/react.min.js')
  5. },
  6. }
  7. }

resolve.extensions 配置

  • 后缀尝试列表要尽可能小,不要将项目中不可能存在的情况写到后缀尝试列表中

  • 频率出现最高的文件后缀要优先放在最前面,以做到尽快退出寻找过程

  • 在源码中写导入语句时,要尽可能带上后缀,从而可以避免寻找过程

  1. module.exports = {
  2. resolve: {
  3. extensions: ['js'],
  4. }
  5. }

module.noParse 配置

让 webpack 忽略对部分没采用模块化的文件的递归处理,如jQuery| react.min.js
注意被忽略掉的文件里不应该包含 import | require | define 等模块化的语句,不然会导致在构建出的代码包含无法在浏览器环境下执行的模块化语句

  1. const path = require('path)
  2. module.exports = {
  3. module: {
  4. noParse: [/react\.min\.js$/],
  5. }
  6. }

资源