深入浅出Webpack - 3 - webpack实战

Webpack实战

框架结合

  1. Vue
  • 接入webpack:

    • 修改webpack.config.js:
    module: {
        rules: [
            {
                test: /\.vue$/,
                use: ['vue-loader']
            }
        ]
    }
    
    • 安装依赖
    • vue-loader:解析和转换.vue文件,提取出其中的逻辑代码script、样式代码style及HTML模板template,再分别将它们交给对应的Loader去处理。
    • css-loader:加载由vue-loader提取出的CSS代码。
    • vue-template-compiler:将vue-loader提取出的HTML模板编译成对应的可执行的JavaScript代码。预先编译好HTML模板相对于在浏览器中编译HTML模板,性能更好。
    npm i -S vue
    npm i -D vue-loader css-loader vue-template-compiler
    
  • 接入ts

    • 修改tsconfig.json:
    {
        "compilerOptions": {
            "target": "es5",
            "strict": true,
            "module": "es2015",
            "moduleResolution": "node",
        }
    }
    
    • vue文件,script标签中的lang="ts"用于指明代码的语法是TypeScript

    • 由于 ts 不认识以.vue 结尾的文件,所以为了让其支持 import App from'./App.vue'导入语句,还需要以下文件vue-shims.d.ts定义.vue文件的类型,告诉ts编译器.vue文件其实是一个Vue

    declare module "*.vue" {
        import Vue from "vue";
        export default Vue;
    }
    
    • 修改webpack.config.ts
    const path = require('path');
    module.exports = {
        resolve: {
            extensions: ['.ts', '.js', '.vue', '.json', '.css'], 
        },
        module: {
            rules: [
                {
                    test: /\.vue$/,
                    use: ['vue-loader']
                },
                {
                    test: /\.ts$/,
                    use: ['ts-loader'],
                    exclude: /node_modules/,
                    options: {
                        appendTsSuffixTo: [/\.vue$/] // 处理.vue文件中的TypeScript代码
                    }
                }
            ]
        }
    }
    
    • npm i -D ts-loader typescript
  1. React
  • 接入webpack

    • (1)在使用 Babel 的项目中接入 React 框架很简单,只需要加入 React 所依赖的 Presets babel-preset-react
    • (2)安装依赖
      npm i -D react react-dom
      npm i -D babel-preset-react
      
    • (3)修改.babelrc,"presets": ['react']
    • (4)修改main.js
      import * as React from 'react'
      import {Component} from 'react'
      import { render } from 'react-dom'
      
      class Button extends Component {
      render(){
      return <h1>hello</h1>
      }
      }
      render(<Button />, window.document.getElementById('app'))
      
  • 接入ts:

    • 使用了JSX语法的文件后缀必须是tsx;
    • 由于React不是采用TS编写的,所以需要安装react和react-dom对应的TypeScript接口描述模块@types/react和@types/react-dom才能通过编译。
    • npm i react react-dom @types/react @types/react-dom
    • 修改tsconfig.json
      {
          "compilerOptions": {
              "jsx": "react"
          }
      }
      
    • main.js修改为main.tsx
      // webpack.config.ts
          resolve: {
              extensions: ['.ts', '.tsx', '.js', '.json', '.css'], // 自动解析确定的扩展
          },
          module: {
              rules: [
                  {
                      test: /\.tsx?$/,
                      use: ['awesome-typescript-loader']
                  },
              ]
          }
      

同构应用

  • 同构应用,从一份项目源码中构建出两份JS代码,一份用于在浏览器端运行,一份用于在 Node.js环境中运行并渲染出 HTML。

  • 同构应用的运行原理的核心在于虚拟 DOM,通过JS对象描述原本的 DOM 结构。在需要更新 DOM 时不直接操作 DOM树,而是在更新JS对象后再映射成DOM操作。

  • react-dom在渲染虚拟DOM树时有两种方式

    • 通过render()函数去操作浏览器DOM树来展示出结果。
    • 通过renderToString()计算表示虚拟DOM的HTML形式的字符串。
  1. 由于要从一份源码中构建出两份不同的代码,所以需要有两份 Webpack 配置文件分别与之对应。构建用于浏览器环境的配置和前面讲的没有差别,本节侧重于讲解如何构建用于服务端渲染的代码。用于构建浏览器环境代码的 webpack.config.js 配置文件保持不变,新建一个专门用于构建服务端渲染代码的配置文件webpack_server.config.js

    // 侧重于优化输出质量的文件
    // webpack.config.js 
    const path = require('path');
    const DefinePlugin = require('webpack/lib/DefinePlugin');
    const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin');
    const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
    const ExtractTextPlugin = require('extract-text-webpack-plugin');
    const { AutoWebPlugin } = require('web-webpack-plugin');
    const HappyPack = require('happypack');
    const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');
    
    // 自动寻找pages目录下的所有目录,将每个目录看作一个单页应用
    const autoWebPlugin = new AutoWebPlugin('./src/pages', {
        // HTML模块文件所在的文件路径
        template: './template.html',
        // 提取所有页面的公共代码
        commonsChunk: {
            // 提取公共代码的Chunk名称
            name: 'common'
        },
        // 指定存放css文件的CDN目录URL
        stylePublicPath: '//css.cdn.com/id/',
    })
    
    module.exports = {
        // AutoWebPlugin会为寻找到的所有单页应用生成对应的入口配置,通过autoWebPlugin.entry方法可以获取生成入口的配置
        entry: autoWebPlugin.entry({
            // 这里可以加入额外需要的Chunk入口
            base: './src/base.js',
        }),
        output: {
            // 为输出的文件名称加上Hash值
            filename: '[name]_[chunkhash:8].js',
            path: path.resolve(__dirname, './dist'),
            // 指定存放js文件的CDN目录URL
            publicPath: '//js.cdn.com/id/'
        },
        resolve: {
            // 使用绝对路径指明第三方模块存放的位置,以减少搜索步骤
            // __dirname表示当前工作目录,即项目根目录
            modules: [path.resolve(__dirname, 'node_modules')],
            // 针对npm中的第三方模块,优先采用jsnext:main中指向的ES6模块化语法的文件,使用Tree Shaking
            // 只采用main字段作为入口文件描述字段,以减少搜索引擎
            mainFields: ['jsnext:main', 'main'],
        },
        module: {
            rules: [
                {
                    // 如果项目中只有js文件,就不要写成jsx,以提升正则表达式的性能
                    test: /\.js$/,
                    // 使用HappyPack加速构建
                    use: 'happypack/loader?id=babel',
                    // 只对项目根目录下src目录下的文件,采用babel-loader
                    include: path.resolve(__dirname, 'src'), 
                    // // 排除node_modules目录下的文件
                    // exclude: /node_modules/
                },
                {
                    test: /\.js$/,
                    use: ['happypack/loader?id=ui-component'],
                    include: path.resolve(__dirname, 'src'),
                },
                {
                    // 使用HappyPack处理css文件
                    test: /\.css$/,
                    // 提取chunk值的css代码到单独的文件中
                    use: ExtractTextPlugin.extract({
                        use: ['happypack/loader?id=css'],
                        // 指定存放css中导入资源的CDN目录URL
                        publicPath: '//img.cdn.com/id/'
                    })
                }
            ]
        },
        plugins: [
            autoWebPlugin,
            // 开启Scope Hoisting
            new ModuleConcatenationPlugin(),
            // 使用HappyPack来加速babel-loader的构建
            new HappyPack({
                // 用唯一标识符id来代表当前的HappyPack用来处理一类特定的文件   
                id: 'babel',
                // babel-loader支持缓存转换出的结果,通过cacheDirectory选项开启
                loaders: ['babel-loader?cacheDirectory=true'],
            }),
            new HappyPack({
                // UI组件加载拆分
                id: 'ui-component',
                // 使用babel-loader处理UI组件
                loaders: [
                    {
                        loader: 'ui-component-loader',
                        options: {
                            lib: 'antd', // 指定UI组件库
                            style: 'style/index.css', // 指定UI组件库的样式文件
                            camel2: '-'
                        }
                    }
                ],
            }),
            new HappyPack({
                // css文件加载拆分
                id: 'css',
                // 使用css-loader和style-loader处理css文件
                // minimize压缩css代码
                loaders: ['css-loader?minimize'],
            }),
            new ExtractTextPlugin({
                // 为输出的css文件名称加上Hash值
                // 通过contenthash来确保每次css代码变更时,输出的css文件名称都会变更
                // 这样可以确保浏览器每次都能加载最新的css文件
                filename: '[name]_[contenthash:8].css',
            }),
            new CommonsChunkPlugin({
                // 从common和base两个现成的chunk中提取公共的部分
                chunks: ['common', 'base'], 
                // 将公共的部分放到base中
                name: 'base', 
            }),
            new DefinePlugin({
                // 定义NODE_ENV为production,表示当前是生产环境,去除在react代码开发时才需要的部分
                'process.env': {
                    NODE_ENV: JSON.stringify('production'),
                }
                // 'process.env.NODE_ENV': JSON.stringify('production'),
            }),
            new ParallelUglifyPlugin({
                // 压缩js代码
                uglifyJS: {
                    output: {
                        comments: false, // 去除注释
                        beautify: false, // 不美化输出
                    },
                    compress: {
                        warnings: false, // 去除警告
                        drop_debugger: true, // 去除debugger语句
                        drop_console: true, // 去除console语句
                        collapse_vars: true, // 内嵌已定义但只用到一次的变量
                        reduce_vars: true, // 提取出现多次但没有定义成变量去引用的静态值
                    }
                }
            })
        ],
    
    }
    
    // webpack_server.config.js
    const path = require('path');
    const nodeExternals = require('webpack-node-externals');
    module.exports = {
        // js执行入口文件
        entry: './src/server.js',
        // 为了不将Node.js内置的模块打包进输出文件中
        target: 'node',
        // 不将node_modules目录下的模块打包进输出文件中
        externals: [nodeExternals()],
        output: {
            // 为了以commonjs2规范导出渲染函数,以被采用Node.js编写的HTTP服务调用
            libraryTarget: 'commonjs2',
            filename: 'bundle_server.js',
            // 将输出文件都放在dist目录下
            path: path.resolve(__dirname, './dist'),
        },
        module: {
            rules: [
                {
                    test: /\.js$/,
                    use: ['babel-loader'],
                    exclude: path.resolve(__dirname, 'node_modules'),
                },
                {
                    // css代码不能被打包到用于服务端的代码中,忽略css文件
                    test: /\.css$/,
                    use: ['ignore-loader'],
                }
            ]
        },
        devtool: 'source-map',
    }
    
  2. 将页面的根组件放到一个单独的文件AppComponent.js 中,该文件只能包含根组件的代码,不能包含渲染入口的代码,而且需要导出根组件以供渲染入口调用

    // AppComponent.js:
    
    import React, { component } from 'react';
    import './main.css'
    
    export class AppComponent extends Component {
        render() {
            return (
                <div className="app">
                    Hello World, React!
                </div>
            )
        }
    }
    
  3. 分别为不同环境的渲染入口写两份不同的文件,即用于浏览器端渲染 DOM 的main_browser.js文件和用于服务端渲染HTML字符串的main_server.js文件。

    // main_browser.js:
    
    import React from 'react';
    import { render } from 'react-dom';
    import { AppComponent } from './AppComponent';
    
    // 将根组件渲染到DOM树上
    render(<AppComponent />, document.getElementById('root'));
    
    
    // main_server.js:
    
    import React from 'react'
    import { renderToString } from 'react-dom/server'
    import { AppComponent } from './AppComponent'
    
    // 导出渲染函数,以供采用Node.js编写的HTTP服务调用
    export function render() { 
        // 将根组件渲染为HTML字符串
        return renderToString(<AppComponent />)
    }
    
  4. 为了能将渲染的完整 HTML 文件通过 HTTP 服务返回给请求端,还需要用 Node.js 编写一个HTTP服务器。

    http_server.js:
    
    const express = require('express');
    const { render } = require('./dist/bundle_server');
    
    const app = express();
    
    app.get('/', (req, res) => { 
        res.send(`
            <html>
                <head>
                    <meta charset="UTF-8">
                    <title>React SSR</title>
                    <link rel="stylesheet" href="/main.css">
                </head>
                <body>
                    <div id="root">${render()}</div>
                    <!-- 导入webpack输出的用于浏览器端渲染的js文件 -->
                    <script src="/bundle_browser.js"></script>
                </body>
            </html>
            `)
    })
    
    // 其他请求路径返回对应的本地文件
    app.use(express.static('.'));
    
    app.listen(3000, () => { 
        console.log('HTTP server is running on http://localhost:3000');
    })
    
  5. 安装依赖

    npm i -D css-loader style-loader ignore-loader webpack-node-externals
    npm i -S express
    
  6. 执行构建,编译出目标文件

    • 执行命令 webpack--config webpack_server.config.js,构建出用于服务端渲染的./dist/bundle_server.js文件。
    • 执行命令 webpack,构建出用于浏览器环境运行的./dist/bundle_browser.js文件,默认的配置文件为webpack.config.js。
    • 构建执行完成后,执行 node./http_server.js 启动 HTTP 服务器,再用浏览器去访问http://localhost:3000,就能看到Hello,Webpack了。为了验证服务端渲染的结果,需要打开浏览器的开发工具中的网络抓包一栏,再重新刷新浏览器,就能抓到请求HTML的包,可以看到服务器返回的是渲染出内容后的HTML,而不是HTML模板,这说明同构应用的改造完成了。



参考&感谢各路大神

  • [深入浅出Webpack-吴浩麟]
posted @ 2024-08-23 18:40  安静的嘶吼  阅读(5)  评论(0)    收藏  举报