深入浅出Webpack - 3 - webpack实战
Webpack实战
框架结合
- 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
- 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形式的字符串。
-
由于要从一份源码中构建出两份不同的代码,所以需要有两份 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', }
-
将页面的根组件放到一个单独的文件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> ) } }
-
分别为不同环境的渲染入口写两份不同的文件,即用于浏览器端渲染 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 />) }
-
为了能将渲染的完整 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'); })
-
安装依赖
npm i -D css-loader style-loader ignore-loader webpack-node-externals npm i -S express
-
执行构建,编译出目标文件
- 执行命令
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-吴浩麟]