腦闊疼的webpack按需載入

百命發表於2018-11-22

Q1:  什麼是按需載入?

隨著單頁應用發展的越來越龐大,拆分js就是第一要務,拆分後的js,就可以根據我們需求來有選擇性的載入了。

所以第一個問題,就是js怎麼拆?

Q2:js怎麼拆?

1,未拆分前是什麼樣子?

來個demo,先看一下未拆分之前是什麼樣子: a.js:

import b from './b.js';
console.log("this is a.js")
const btn = document.querySelector("#btn");
btn.onclick = ()=>{
  b();
}
複製程式碼

b.js:

export default ()=>{
  console.log("this is b");
}
複製程式碼

html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  <div id="btn">btn</div>
  <script src="./dist/main.js"></script>
</body>
</html>
複製程式碼

webpack.config.js

module.exports = {
  entry:'./a.js',
  output:{
    filename:'[name].js'
  }
}
複製程式碼
  1. a.js引用b.js
  2. webpack打包將b、a都打包到了一起,輸出一個預設的main.js
  3. html引用打包好的main.js 結果如下:

image.png | left | 315x215


2,開搞!

step1:修改webpack.config.js

module.exports = {
  entry:'./a.js',
  output:{
    filename:'[name].js',
    chunkFilename:'[name].js'// 設定按需載入後的chunk名字
  }
}
複製程式碼

這裡就新增了一句,chunkFilename而已,chunkFilename的作用就是用來給拆分後的chunk們起名字的配置項。 ok,執行webpack

image.png | left | 296x138

還是隻打包出了一個main.js,毫無變化... 不用擔心,這是因為還有設定沒搞定。


step2:修改a.js

// import b from './b.js';
console.log("this is a.js")
const btn = document.querySelector("#btn");
btn.onclick = ()=>{
    import('./b').then(function(module){
      const b = module.default;
      b();
    })
}
複製程式碼
  1. 使用es6的import按需語法
  2. 在promise後執行拿到的返回的結果 此時再次執行webpack:

image.png | left | 348x154

輸出檔案變成了兩個,1個main.js、1個1.js 這個1.js很迷...

image.png | left | 668x75

檢視一下原始碼,可以看出來,它其實就是我們的b.js

總結一下 :

  • webpack中output的設定並不決定是否拆分程式碼
  • 拆分程式碼的決定因素在import語法上
  • webpack在掃描到程式碼中有import語法的時候,才決定執行拆分程式碼

step3:怎麼使用?

image.png | left | 441x230

額,成功報錯了...腦闊疼 分析報錯:

  • 按需載入找的檔案是/1.js
  • 但我們打包的結果在dist目錄下,自然不可能在根目錄下找到

step4:配置Public Path基礎路徑

該配置能幫助你為專案中的所有資源指定一個基礎路徑。它被稱為公共路徑(publicPath) 修改webpack.config.js

module.exports = {
  entry:'./a.js',
  output:{
    filename:'[name].js',
    chunkFilename:'[name].js',// 設定按需載入後的chunk名字
    publicPath:'dist/' // 設定基礎路徑
  }
}
複製程式碼

step5:驗證結果

image.png | left | 333x347


  • 點選前
  • 只引用了main.js

image.png | left | 413x703


  • 點選後
  • 載入了1.js
  • 並執行了1.js中的js程式碼
  • 控制檯輸出this is b.js
  • ok,驗證成功

step6:填坑

前面1.js這玩意也不可讀啊,有問題也很難明確,webpack,提供了定義按需chunkname的方式,修改a.js:

// import b from './b.js';
console.log("this is a.js")
const btn = document.querySelector("#btn");
btn.onclick = ()=>{
  import(/* webpackChunkName: "b" */ './b').then(function(module){
    const b = module.default;
    b();
  })
}

複製程式碼

在動態引入的語法前,新增了註釋,註釋就是為chunk命明的方式,結果:

image.png | left | 474x208

輸出了b.js,測試迴歸一次:

image.png | left | 259x432


  • chunk名字對按需載入沒有影響
  • 修改了按需chunk的名字也只是方便檔案可讀性

Q3:按需載入之後還能熱更新嗎?

1,先跑個webpack-dev-server整合起來

先安裝webpack-dev-server,配置npm scripts

{
  "devDependencies": {
    "webpack-dev-server": "^3.1.9"
  },
  "scripts": {
    "start:dev": "webpack-dev-server"
  },
  "dependencies": {
    "webpack": "^4.20.2",
    "webpack-cli": "^3.1.2"
  }
}

複製程式碼

修改webpack.config.js

var path = require('path');
module.exports = {
  entry:'./a.js',
  mode:'development',
  output:{
    filename:'[name].js',
    chunkFilename:'[name].js',// 設定按需載入後的chunk名字
    publicPath:'dist/'
  },
  devServer: {
    contentBase: './',
    compress: true,
    port: 9000
  }
}
複製程式碼
  • 這一次不再通過webpack命令來執行了
  • 而是通過npm run start:dev命令列來執行
  • webpack-dev-server會讀取webpack.config.js中的devServer配置
  • ok,devServer已經整合好了

2,跑起來看看

修改webpack.config.js

var path = require('path');
var webpack = require('webpack');
module.exports = {
  entry:'./a.js',
  mode:'development',
  output:{
    filename:'[name].js',
    chunkFilename:'[name].js',// 設定按需載入後的chunk名字
    publicPath:'dist/'
  },
  devServer: {
    contentBase: './',
    compress: true,
    port: 9000,
    hot: true, // 開啟熱更新
  },
  plugins: [ // 開始熱更新
      new webpack.NamedModulesPlugin(),
      new webpack.HotModuleReplacementPlugin()
  ],
}
複製程式碼

上面一共起作用的就是3句話:

  • devServer中的hot語句
  • plugins中的兩個webpack內建外掛 將這兩個外掛開啟後,還不行,還需要修改入口檔案
// import b from './b.js';
console.log("this is a.js")
const btn = document.querySelector("#btn");
btn.onclick = ()=>{
  import(/* webpackChunkName: "b" */ './b').then(function(module){
    const b = module.default;
    b();
  })
}

if (module.hot) {// 開啟熱替換
     module.hot.accept()
}
複製程式碼

ok,就這麼簡單,熱更新+按需載入就齊活了。


Q4:react-router整合按需載入

react-router文件

業務中,除了點選的時候按需載入,還有大部分場景都是在路由切換的時候進行按需載入

step1:新增babel-loader

修改webpack.config.js

var path = require('path');
var webpack = require('webpack');
module.exports = {
  entry:'./a.js',
  mode:'development',
  output:{
    filename:'[name].js',
    chunkFilename:'[name].js',// 設定按需載入後的chunk名字
    publicPath:'dist/'
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: 'babel-loader',
        }
      }
    ]
  },
  devServer: {
    contentBase: './',
    compress: true,
    port: 9000,
    hot: true,
  },
  plugins: [
      new webpack.NamedModulesPlugin(),
      new webpack.HotModuleReplacementPlugin()
  ],
}
複製程式碼

上面新增的就是新增了一個babel-loader


step2:新增.babelrc

{
  "presets": ["@babel/preset-react","@babel/preset-env"]
}
複製程式碼

step3:書寫jsx

修改a.js

import React,{Component} from 'react';
import ReactDom from 'react-dom';
import B from './b.js';
export default class A extends Component{
  render(){
    return <div>
      this is A
      <B />
    </div>
  }
}
ReactDom.render(<A/>,document.querySelector("#btn"))
if (module.hot) {
     module.hot.accept()
}
複製程式碼

修改b.js

import React,{Component} from 'react';
export default class B extends Component{
  render(){
    return <div>this is B</div>
  }
}
複製程式碼

測試一下:

  • react跑起來了
  • 熱更新依舊有效果

step4:整合react-loadable

react按需載入進化了好幾個方式,目前最新的方式就是使用react-loadable這個元件 官方也推薦使用這個庫來實現,目前這個庫已經1w+star了

修改a.js

import React,{Component} from 'react';
import { BrowserRouter as Router, Route, Switch,Link } from 'react-router-dom';
import ReactDom from 'react-dom';
import Loadable from 'react-loadable';

const Loading = () => <div>Loading...</div>;

const B = Loadable({
  loader: () => import('./b.js'),
  loading: Loading,
})
const C = Loadable({
  loader: () => import('./C.js'),
  loading: Loading,
})
export default class A extends Component{
  render(){
    return <div>
      <Router>
        <div>
          <Route path="/B" component={B}/>
          <Route path="/C" component={C}/>
          <Link to="/B">to B</Link><br/>
          <Link to="/C">to C</Link>
        </div>
      </Router>
    </div>
  }
}
ReactDom.render(<A/>,document.querySelector("#btn"))
if (module.hot) {
     module.hot.accept()
}

複製程式碼
  • loadable中使用的import語法是ECMA未來會支援的動態載入特性
  • loadable很簡單,只需要按照它所規定的語法,包裹一下需要載入的元件就可以

image.png | left | 408x470

點選跳轉toC

image.png | left | 413x511

可以看到載入了1.js,也就是說非同步載入順利完成 但是現在存在問題:在/C路徑下重新整理,會出現無法命中路由的情況


step5:跑個express驗證一下

var express = require('express')
var app = express()
app.use(express.static('dist'))


app.get('*', function (req, res) {
  res.send(`<!DOCTYPE html>
  <html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
  </head>
  <body>
    <div id="btn">btn</div>
    <script src="./main.js"></script>
  </body>
  </html>`)
})
app.listen(5000);
複製程式碼

建立一個簡單的express應用:

  • 驗證通過
  • 同樣會執行按需載入

step6:巢狀路由按需載入

路由一個很常見的功能就是路由巢狀,所以我們的按需載入必須支援巢狀路由才算合理 修改a.js

import React,{Component} from 'react';
import { BrowserRouter as Router, Route, Switch,Link } from 'react-router-dom';
import ReactDom from 'react-dom';
import Loadable from 'react-loadable';

const Loading = (props) => {
  return <div>Loading...</div>
};
const B = Loadable({
  loader: () => import('./b.js'),
  loading: Loading,
})
const C = Loadable({
  loader: () => import('./c.js'),
  loading: Loading,
})
export default class A extends Component{
  render(){
    return <div>
      <Router>
        <div>
          <Route path="/B" component={B}/>
          <Route path="/C" component={C}/>
          <Link to="/B">to B</Link><br/>
          <Link to="/C">to C</Link>
        </div>
      </Router>
    </div>
  }
}
ReactDom.render(<A/>,document.querySelector("#btn"))
if (module.hot) {
     module.hot.accept()
}

複製程式碼

修改c.js

import React,{Component} from 'react';
import { Route,Link} from 'react-router-dom';
import Loadable from 'react-loadable';

const Loading = (props) => {
  return <div>Loadingc...</div>
};

const D = Loadable({
  loader: () => import('./d.js'),
  loading: Loading,
})
export default class C extends Component{
  render(){
    return <div>
      this is C
      <Route path="/C/D" component={D}/>
      <Link to="/C/D">to D</Link>
    </div>
  }
}

複製程式碼
  • 入口檔案引入兩個動態路由B、C
  • c.js中巢狀了路由/C/D
  • 路由/C/D中使用了按需元件D

step7:驗證巢狀路由

入口沒問題

image.png | left | 303x152


點選跳轉動態載入C沒問題

image.png | left | 328x177


點選跳轉D不行了

image.png | left | 747x228


可以看到動態引入資源./d.js的時候,出現了異常,莫名其妙的新增了路徑/C


step8:該死的publicPath

這裡疑惑了好一會,還查了很多內容,最後痛定思痛察覺到應該還是publicPath設定有問題,重新檢查了設定,修改webpack.config.js

var path = require('path');
var webpack = require('webpack');
module.exports = {
  entry:'./a.js',
  mode:'development',
  output:{
    path:path.resolve(__dirname, 'dist'),
    filename:'[name].js',
    chunkFilename:'[name].js',// 設定按需載入後的chunk名字
    publicPath:'/dist/'
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: 'babel-loader',
        }
      }
    ]
  },
  devServer: {
    contentBase: './',
    compress: true,
    port: 9000,
    hot: true,
  },
  plugins: [
      new webpack.NamedModulesPlugin(),
      new webpack.HotModuleReplacementPlugin()
  ],
}
複製程式碼

這裡唯一的改動,就是publicPath由原來的dist/,變成/dist/,只要把前面的路徑補上,就不會去找相對的地址了。


Q5:到真實專案裡怎麼搞?

前面看似解決了問題,但在真實場景下,我們的要求肯定會更高! 首先就是要封裝一個便捷使用的按需載入元件。

step1:封裝LazyLoad元件

理想很美好,現實很骨幹

const LazyLoad = (path)=>{
  return Loadable({
    loader: () => import(path),
    loading: Loading,
  })
}

const B = LazyLoad('./b.js')
複製程式碼

然後就收穫了報錯

image.png | left | 747x55

這是因為webpack編譯的時候import預發==不支援動態路徑==


step2:可怕的import,瞭解一下

import不支援動態路徑,是因為webpack需要先掃一遍js檔案,找出裡面按需載入的部分,進行按需打包,但不會關心內部的js執行上下文,也就是說,在webpack掃描的時候,js中的變數並不會計算出結果,所以import不支援動態路徑。


step3:封裝非import部分

既然import不能搞,那隻能封裝非import的部分了

const LazyLoad = loader => Loadable({
  loader,
  loading:Loading,
})
複製程式碼

把loader這部分當作引數分離出去,下面就是具體的使用

const B = LazyLoad(()=>import('./b.js'));
const C = LazyLoad(()=>import('./c.js'));
複製程式碼

下面是全部程式碼

import React,{Component} from 'react';
import { BrowserRouter as Router, Route, Switch,Link } from 'react-router-dom';
import ReactDom from 'react-dom';
import Loadable from 'react-loadable';

const Loading = (props) => {
  return <div>Loading...</div>
};

const LazyLoad = loader => Loadable({
  loader,
  loading:Loading,
})
const B = LazyLoad(()=>import('./b.js'));
const C = LazyLoad(()=>import('./c.js'));

export default class A extends Component{
  render(){
    return <div>
      <Router>
        <div>
          <Route path="/B" component={B}/>
          <Route path="/C" component={C}/>
          <Link to="/B">to B</Link><br/>
          <Link to="/C">to C</Link>
        </div>
      </Router>
    </div>
  }
}
ReactDom.render(<A/>,document.querySelector("#btn"))
if (module.hot) {
     module.hot.accept()
}

複製程式碼

上面的封裝方式並不是十分完美,webpack文件上說支援: ==import(./dynamic/\${path})的方式== 只要不全是變數貌似也是支援的,這就要看具體的業務形態了,如果按需的部分都在某個目錄下,這種操作或許更舒適一些。

按目前的方式的話,看似比較繁瑣,不過可以通過配置webpack的alias別名來進行路徑支援。


Q6:按需載入+router config

react router除了元件方式以外,還可以通過config的方式來進行配置,config的方式便於統一維護controller層。

step1:封裝LazyLoad

建立LazyLoad.js檔案

import React from 'react';
import Loadable from 'react-loadable';
const Loading = (props) => {
  return <div>Loading...</div>
};

export default loader => Loadable({
  loader,
  loading:Loading,
})
複製程式碼

首先把Lazyload元件單獨封裝出去


step2:配置routes

建立routes.js

import LazyLoad from './LazyLoad';
export default [
  {
    path: "/B",
    component: LazyLoad(()=>import('./b.js'))
  },
  {
    path: "/C",
    component: LazyLoad(()=>import('./c.js')),
    routes: [
      {
        path: "/C/D",
        component: LazyLoad(()=>import('./d.js'))
      },
      {
        path: "/C/E",
        component: LazyLoad(()=>import('./e.js'))
      }
    ]
  }
];
複製程式碼

配置routes檔案,用來動態引入路由


step3:封裝工具方法RouteWithSubRoutes

建立utils.js

import React from 'react';
import {Route} from 'react-router-dom';
export const RouteWithSubRoutes = route => (
  <Route
    path={route.path}
    render={props => (
      // pass the sub-routes down to keep nesting
      <route.component {...props} routes={route.routes} />
    )}
  />
);
複製程式碼

==這一步特別重要、特別重要、特別重要==

這個工具方法的作用就是將元件渲染出來


step4:修改第一層路由入口

import React,{Component} from 'react';
import { BrowserRouter as Router, Route, Switch,Link } from 'react-router-dom';
import ReactDom from 'react-dom';

import {RouteWithSubRoutes} from './utils';
import routes from './routes';

export default class A extends Component{
  render(){
    return <div>
      <Router>
        <div>
          <Link to="/B">to B</Link><br/>
          <Link to="/C">to C</Link>
          {routes.map((route, i) => <RouteWithSubRoutes key={i} {...route} />)}
        </div>
      </Router>
    </div>
  }
}
ReactDom.render(<A/>,document.querySelector("#btn"))
if (module.hot) {
     module.hot.accept()
}
複製程式碼
  1. 引入RouteWithSubRoutes工具方法
  2. 引入routes路由配置檔案
  3. 在包裹的檔案中進行routes遍歷渲染

==注意:這裡只處理了第一層路由== ==注意:這裡只處理了第一層路由== ==注意:這裡只處理了第一層路由==


step5:修改二級路由入口

路由配置化之後,巢狀子路由要以函式式來書寫

import React,{Component} from 'react';
import {RouteWithSubRoutes} from './utils';
import { Link} from 'react-router-dom';

export default ({ routes }) => (
  <div>
    this is C
    <Link to="/C/D">to D</Link>
    <Link to="/C/E">to E</Link>
    {routes.map((route, i) => <RouteWithSubRoutes key={i} {...route} />)}
  </div>
);
複製程式碼
  1. 引入RouteWithSubRoutes工具方法
  2. 暴露的函式接受一個引數routes
  3. routes即config中內層的配置,也就是二級路由配置
  4. 二級路由配置,繼續通過RouteWithSubRoutes進行渲染

==注意:config巢狀路由,需要逐層,一層一層的通過RouteWithSubRoutes來渲染。== ==新人很容易忽視這一點!== ==新人很容易忽視這一點!== ==新人很容易忽視這一點!==


Q7:router隨心用?

前面使用config的方式配置了路由,但其實這裡也可以混用,就是config方式+元件的方式混合使用。 修改二級路由入口:

import React from 'react';
import { Link,Route} from 'react-router-dom';
//import {RouteWithSubRoutes} from './utils';
import LazyLoad from './LazyLoad';

const D = LazyLoad(() => import('./d.js'))
const E = LazyLoad(() => import('./e.js'))

export default ({ routes }) => (
  <div>
    this is C
    <Route path="/C/D" component={D}/>
    <Route path="/C/E" component={E}/>
    <Link to="/C/D">to D</Link>
    <Link to="/C/E">to E</Link>
    {/* {routes.map((route, i) => <RouteWithSubRoutes key={i} {...route} />)} */}
  </div>
);
複製程式碼

其實,這裡的話,就是隨便搞了

路由的話,還是統一維護為好,當然也可以根據業務來自主選擇需要的方式!。

腦闊疼的webpack按需載入告一段落了。

相關文章