webpack整合笔记打包库

528 阅读3分钟

Library

开发一个library,使用者可能的三种引入方式

import library from 'library'
const library = require('library')
require(['library'], function(){
    
})
<script src="library.js"></script>
<script>
    // library.math
</script>

webpack.config.js

{
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'library.js',
        library: 'library',   // 支持html src引入
        libraryTarget: 'umd',  // 支持通用引入方式  
        libraryTarget: 'this'  // this / window,将不支持前三种方式,但是library将挂在this上
    },
    externals: ["lodash"]   // 忽略lodash
}

发布到npm

  • 注册npm
  • 命令行 npm adduser 输入用户名 密码
  • npm publish

A 打包库组件

除了可打包应用,也可打包js库 实现一个大整数加法库的打包

  • 需要打包压缩版和非压缩版本
  • 支持AMD/CJS/ESM模块引入

库的目录结构和打包要求

打包输出的库名称:

  • 未压缩 large-number.js
  • 压缩 large-number.min.js

/dist large-number.js large-number.min.js webpack.config.js packae.json index.js /src index.js

ES module

import * as largeNumber from 'large-number'
//...
largeNumber.add('999', '1')

CJS

const largeNumbers = require('large-number')
//...
largeNumber.add('999', '1')

AMD

require(['large-number'],function(large-number){
    //...
    largeNumber.add('999', '1')
})

直接使用script引入

<html>
    <script src="https://unpkg.com/large-number"/>
    <script>
        //...
        //Global variable
        largeNumber.add('999','1')
        //Property in the window object
        window.largeNumber.add('999','1')
    </script>
</html>

如何将库暴露出去?

library: 指定库的全局变量 libraryTarget: 支持库引入的方式

mkdir large-number
cd large-number
npm init -y
npm i webpack webpack-cli -D

webpack.config.js

module.exports={
    entry:{
        "large-number":"./src/index.js",
        "large-number.min":"./src/index.js"
    },
    output:{
        filename: "[name].js",
        library:"largeNumber",
        libraryExport: "default",
        libraryTarget:"umd"
    }
}

package.json

"scripts": {
    "build":"webpack"
  }

src/index.js

export default function add(a, b){
  let i = a.length -1;
  let j = b.length -1;
  let carry = 0;
  let ret='';
  while(i>=0 || j >= 0){
    let x = 0;
    let y = 0;
    let sum;
    if(i >= 0){
      x = a[i]-"0";
      i--
    }
    if(j >= 0){
      y = b[j]-"0";
      j--
    }
    sum = x + y + carry;

    if(sum >= 10){
      carry = 1;
      sum -= 10;
    }else{
      carry= 0;
    }
    ret = sum + ret;
  }
  if(carry){
    ret = carry + ret;
  }
  return ret;
}

build后,发现dist目录中large-number.js和large-number.min都被压缩了。 下面通过include配置只对.min压缩

npm i terser-webpack-plugin -D
const TerserPlugin = require('terser-webpack-plugin')
module.exports={
    mode:"none",
    //...
    optimization:{
        minimize: true,
        minimizer: [
            new TerserPlugin({
                include:/\.min\.js$/,
            })
        ]
    }
}

设置入口文件

package.json 的 main 字段为index.js package.json

{
    "main":"index.js",
    "scripts":{
        "build":"webpack",
        "prepublish":"webpack"  // npm publish 时打包
    }
}

index.js

if(process.env.NODE_ENV==="production"){
    module.exports = require("./dist/large-number.min.js")
}else{
    module.exports = require("./dist/large-number.js")
}

最后打包发到npm

npm publish

引入

npm i large-bumber -S

B SSR 服务端渲染

优化白屏时间 对SEO友好

渲染:html+css+js+data ==> 渲染后的HTML

服务端: 所有模块等资源都存储在服务端 内网机器捞取数据更快 一个HTML返回所有数据

浏览器和服务器交互流程

客户端渲染 服务端渲染
请求 多个请求(HTML、数据等) 1个请求
加载过程 HTML&数据串行加载 1个请求返回HTML&数据
渲染 前端渲染 服务端渲染
可交互 图片等静态资源加载完成,JS逻辑执行完成可交互

总结:服务端渲染SSR的核心是减少请求

实现思路

服务端

  • 使用 react-dom/server 的 renderToString 方法将React组件渲染成字符串
  • 服务端路由返回对应的模板

客户端

  • 打包出针对服务端的组件

package.json

"scripts": {
    "build:ssr":"webpack --config webpack.ssr.js"
  },

webpack.ssr.js

const setMPA = () => {
    //...
    const entryFiles = glob.sync(path.join(__dirname, './src/*/index-server.js'))
    Object.keys(entryFiles)
    .map((index) => {
        const match = entryFile.match(/src\/(.*)\/index-server\.js/)
        //...
        const pageName = match && match[1]
          if(!pageName){
            retrun
          }
          entry[pageName] = entryFile;
          htmlWebpackPlugins.push(
            //...
          )
    }
}
const { entry, htmlWebpackPlugins } = setMPA()
module.exports = {
    output: {
        path: path.join(__dirname, 'dist'),
        filename: '[name]-server.js',
        libraryTarget: 'umd'
      },
}

search/index-server.js

'use strict';
const React = require('react')
const logo = require('./img/FireAnt.png')
require('./search.less')

class Search extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      Text: null
    }
  }
  loadComponent = () => {
    import('./test.js').then((Text) => {
      this.setState({
        Text: Text.default
      })
    })
  }
  render() {
    const {Text} = this.state;
    return <div className="search-text">
      Search Text132123123
      { Text ? <Text /> : 'null' }
      <img src={logo} onClick={this.loadComponent} />
    </div>
  }
}

module.exports = <Search/>;

server/index.js

if(typeof window === 'undefined'){
  global.window = {}  // node没有window
}

const express = require('express')
const {renderToString} = require('react-dom/server')
const SSR = require('../dist/search-server')

const server = (port) =>{
  const app = express()

  app.use(express.static('dist'))

  app.get('/search', (req, res)=>{
    const html = renderMarkup(renderToString(SSR))
    res.status(200).send(html)
  })

  app.listen(port, ()=>{
    console.log('Server is running on port:', port);
    
  })
}
server(process.env.PORT || 3000)
const renderMarkup = (str)=>{
  return `<!DOCTYPE html>
  <html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
  </head>
  <body>
    <div id="root">${str}</div>
  </body>
  </html>`;
}

webpack打包存在的问题

浏览器的全局变量(Node.js中没有document, window)

  • 组件适配:将不兼容的组件根据打包环境进行适配
  • 请求适配:将fetch或者ajax发送请求改成isomorphic-fetch 或 axios

样式问题(Node.js中无法解析css)

  • 方案一:服务端打包通过ignore-loader忽略掉CSS的解析
  • 方案二:将style-loader替换为isomorphic-style-loader (CSS in JS 写法,不推荐)

解决css不显示问题

使用打包出来的浏览器端html为模板 设置占位符,动态插入组件

search/index.html

<div id="root"><!--HTML_PLACEHOLDER--></div>

server/index.js

const fs = require('fs')
const path = require('path')
const template = fs.readFileSync(path.join(__dirname, '../dist/search.html'), 'utf-8')
//...
const renderMarkup = (str)=>{
  return template.replace('<!--HTML_PLACEHOLDER-->',str);
}

首屏数据如何处理?

服务端获取数据 替换占位符

search/index.html

<div id="root"><!--HTML_PLACEHOLDER--></div>
<!--INITIAL_DATA__PLACEHOLDER-->

server/index.js

const data = require('./data.json')
//...
const renderMarkup = (str)=>{
  const dataStr = JSON.stringify(data)
  return template.replace('<!--HTML_PLACEHOLDER-->',str)
    .replace('<!--INITIAL_DATA_PLACEHOLDER-->', `<script>window.__initial_data=${dataStr}</script>`);
}

C 优化构建时命令行的显示日志

当前构建日志展示很多日志,对于业务开发者,其中多数不需要关注

统计信息stats


Preset Alternative Description
"errors-only" none 只在发生错误时输出
"minimal" none 只在发生错误或有新的编译时输出
"none" false 没有输出
"normal" true 标准输出
"verbose" none 全部输出

我们使用'errors-only' webpack.prod.js

module.exports = {
    //...
    stats: 'errors-only'
}

npm run build 后没有提示 webpack.dev.js

module.exports = {
    devServer:{
        contentBase: './dist',
        hot: true,
        stats: 'errors-only'
    }
}

npm run dev后提示 Compiled successfully.

如何优化命令行的构建日志

使用 friendly-errors-webpack-plugin

  • success: 构建成功的日志提示
  • warning: 构建警告的日志提示
  • error:构建报错的日志提示
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin')
module.exports = {
    entry: {
        app: './src/app.js',
        search: './src/search.js'
    },
    output: {
        filename: '[name][chunkhash:8].js',
        path: __dirname + '/dist'
    },
    plugins: [
        new FriendlyErrorsWebpackPlugin()
    ],
    stats: 'errors-only'
}

D 构建异常和中断处理

如何判断构建是否成功?

在CI/CD的pipline 或者发布系统需要知道当前构建状态 每次构建完后输入 echo $? 获取错误码

webpack4 之前标本构建失败不会抛出错误码 NodeJS中的 process.exit规范

  • 0 表成功完成,回调函数中,err 为 null
  • 非 0 表执行失败,回调函数中,err 不为 null, err.code 就是传给 exit 的数字

如何主动捕获并处理构建错误?

compiler 在每次构建结束后会触发done这个hook process.exit主动处理构建报错

plugins: [
    function(){
        this.hooks.done.tap('done', (stats)=>{
            if(
                stats.compliation.errors && 
                stats.compilation.errors.length && 
                process.argv.indexOf('--watch')==-1){
                   console.log('build error');
                   process.exit(1)
                }
        })
    }
]