从零开始搭建webpack4+react+kintone的开发环境

cybozu发表于:2020年09月24日 14:23:41更新于:2021年07月28日 11:49:16

Index

环境要求

  • 这次的配置方案不会涉及到CRA。

  • 请务必安装Node.js的最新版本,可以参照本文安装。

  • 自定义证书部分会用到mkcert,mac下可在控制台中输入 brew install mkcert 来安装,windows下则使用choco install mkcert 。brew或chocolatey的安装方式请自行搜索。

  • 本文默认使用chrome进行调试。

开始吧


建立项目文件夹

让我们新建一个文件夹,命名为react-demo,用vscode打开该文件夹后,选择菜单中的终端->New Terminal 来新建终端窗口。在终端中输入:

npm init -y

上面命令会在你的根目录生成 package.json 文件,该文件定义了这个项目所需要的各种模块,以及项目的配置信息(比如名称、版本、许可证等元数据)。


安装webpack

因为我们使用的是 webpack 4+ 版本,还需要安装 webpack-cli ,在终端中输入:

npm install --save-dev webpack@4 webpack-cli@3

执行完成后,你会发现在 package.json 文件中多了 devDependencies 属性,这是因为命令中使用了 --save-dev的结果,代表了开发依赖。和实际运行无关的开发依赖我们都应该加入devDependencies。


接下来,我们在项目根目录下新建config文件夹,在该文件夹会下保存webpack的配置文件。在config目录下新建一个 webpack.common.config.js 文件,并输入:

const path = require('path');module.exports = {
  entry: {
    app: './src/app.js',
  },
  output: {
    filename: 'js/bundle.js',
    path: path.resolve(__dirname, '../dist')
  }}

entry 属性定义了入口文件,output 定义了编译后的文件名及所在目录。上述代码告诉webpack,把根目录src文件夹下的app.js打包到根目录dist下的js目录中,更名为bundle.js。


为此,我们需要在根目录下建立一个src文件夹,在src下新建一个app.js。然后在 package.json 中配置如下属性:

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "webpack --config ./config/webpack.common.config.js"
  },


让我们在终端执行:

npm run start

你会发现根目录多出了一个文件夹: dist/js ,其中有一个js文件: bundle.js ,那么至此,我们已经成功编译打包了一个js文件,即入口文件: app.js 。


拆分webpack

同一个项目,在不同的运行环境下会有不同的要求。比如在生产环境中,我们需要压缩生成的js文件,而在开发环境中我们并不想压缩js,因为这不太方便我们调试。

在config中添加2个新的js文件,webpack.prod.config.js 和webpack.dev.config.js,这2个文件分别对应生产环境和开发环境。再利用webpack-merge将公用配置分离在 webpack.common.config.js 中。


在终端中输入:

npm install --save-dev webpack-merge

安装结束后,在 webpack.prod.config.js 中输入以下代码:

const { merge } = require('webpack-merge');
const common = require('./webpack.common.config.js');

module.exports = merge(common, {
  mode: 'production',
 });

回到 package.json,修改如下:

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack --config ./config/webpack.prod.config.js"
  },


好了,接下来编译打包看看效果吧,在终端执行:

npm run build

你会看到和之前的start指令一样生成了新的js。


安装React

在终端中输入:

npm install --save react react-dom

安装完成后我们就能写react的JSX了。

我们在src下新建 index.js 并输入以下代码:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './app.js';

function onListShow(ev) {
    ReactDOM.render(
    <React.StrictMode>
        <App />
    </React.StrictMode>, kintone.app.getHeaderSpaceElement());
    return ev;
 }
 
 kintone.events.on("app.record.index.show", onListShow);

在app.js中输入如下代码:

import React from 'react';

function App() {
  return (
    <div className="App">
        <h1>Hello World</h1>
    </div>
  );
}

export default App;

对 webpack.common.config.js 的入口文件进行修改 :

  entry: {
    index: './src/index.js',
  },


使用babel 7

webpack并不支持jsx语法,我们需要利用babel 7做编译转换。

首先安装babel相关的模块,在终端输入:

npm install --save-dev babel-loader @babel/preset-react @babel/preset-env @babel/core

在根目录下新建 babel.config.json 来配置相关的presets,输入下面代码:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "edge": 17,
          "firefox": 60,
          "chrome": 67,
          "safari": 11.1
        },
        "useBuiltIns": "usage"
      }
    ],
    "@babel/preset-react"
  ]}

再修改 webpack.common.config.js :

module.exports = {
  //.................,
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        use: 'babel-loader',
        exclude: /node_modules/,
      }
    ]
  }}

上述代码告诉webpack,所有以js/jsx为后缀的文件都需要通过babel-loader进行转换,但不需要去处理node_modules下的文件。


接下来让我们运行

npm run build

dist目录下生成了bundle.js,把它上传到kintone上的某个app中看看效果吧。


优化生产环境


换个不确定的文件名

kintone并不能很直观的显示当前自定义的js的版本,最简单的方法就是给予不同的命名。利用 [hash] 或[chunkhash] 即可。

修改webpack.prod.config.js的exports代码

module.exports = merge(common, {
  mode: 'production',
  output: {
    filename: 'js/[name].[chunkhash:8].bundle.js',
  },

output的filename直接覆盖了webpack.common.config.js下的配置,让我们再运行一下

npm run build

看看输出的文件名把。


清理dist文件夹

生成了新的js后,我们发现,旧的bundle.js没有被移除。利用clean-webpack-plugin来帮我们做预处理。
在终端输入:

npm install --save-dev clean-webpack-plugin

修改 webpck.prod.config.js :

const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = merge(common, {
//..........,
  plugins: [
    new CleanWebpackPlugin()
  ],
});

再度运行

npm run build

看看效果把。


分离公用代码

查看生成的js文件,我们发现有100k以上的size,这是因为我们编译时把react部分也编译进去了。

随着业务的进行,这个文件会越来越大,而react部分其实不怎么会变动,因此我们需要把它分离出来,以后只更新自己的业务js即可。

修改 webpack.prod.config.js ,增加一个入口:

module.exports = merge(common, {
  //..........,
  entry: {
    framework: ['react','react-dom'],
  },
  //..........,
});

再次build,我们会发现,react已经被打包成framework,但index没有缩小,这是由于我们尚未抽离公共部分所致。

添加代码至 webpack.prod.config.js :

module.exports = merge(common, {
  //............,
  optimization: {
    splitChunks: {
      chunks: 'all',
      minSize: 30000,
      maxSize: 0,
      minChunks: 1,
      cacheGroups: {
        framework: {
          test: "framework",
          name: "framework",
          enforce: true
        },
        vendors: {
          priority: -10,
          test: /node_modules/,
          name: "vendor",
          enforce: true,
        },
      }
    }
  },
});


cacheGroups对象,定义了需要被抽离的模块。

name是抽离后生成的名字,framework和入口文件模块名称相同,这样抽离出来的新生成的framework模块会覆盖被抽离的framework模块,虽然他们都叫framework。

vendors这个缓存组,它的test设置为 /node_modules/ 表示只筛选从node_modules文件夹下引入的模块,所以所有第三方模块才会被拆分出来。

再度运行

npm run build

看看效果把。


加载css

在src目录下新建app.css,并输入以下代码:

.App {
    height: 200px;
    display: flex;
    justify-content: center;
    align-items: center;
    background-color: lightblue;
  }
  
  h1 {
    font-size: 16px;
    color: white;
  }

打开app.js,添加以下代码:

import './app.css';

webpack只能编译js文件,css文件是无法被识别并编译的,我们需要loader加载器来进行预处理。
在终端输入以下指令:

npm install --save-dev css-loader mini-css-extract-plugin

css-loader这里就不做过多介绍了,mini-css-extract-plugin则是为了把css提取到单独的文件中。


在 webpack.prod.config.js 中添加下面代码:

const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = merge(common, {
  //............,
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'css/[name].[hash].css',
      chunkFilename: 'css/[id].[hash].css',
    }),
    new CleanWebpackPlugin()
  ],
   module: {
    rules: [
      {
        test: /\.css$/,
        use: [ 
          MiniCssExtractPlugin.loader,
          'css-loader' 
        ]
      }
    ]
  },
  //............,
});

遇到后缀为.css的文件,webpack先用css-loader加载器去解析这个文件,遇到“@import”等语句就将相应样式文件引入(所以如果没有css-loader,就没法解析这类语句),最后计算完的css,再通过 MiniCssExtractPlugin 输出到相应的css文件中。

再度运行

npm run build

然后把所有文件上传到kintone的app中,看看效果把。


压缩代码

打开我们生成的所有文件,我们发现js是被压缩的,但css没有压缩,这是因为webpack没有配置默认的css压缩方案。

使用optimize-css-assets-webpack-plugin可以完成压缩,但很不幸,单独使用会覆盖js的压缩,所以必须配合TerserPlugin一起使用。

在终端输入:

npm install --save-dev optimize-css-assets-webpack-plugin terser-webpack-plugin

在 webpack.prod.config.js 中添加下面代码:

const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');

module.exports = merge(common, {
  //............,
   optimization: {
    minimizer: [
      new TerserPlugin(),
      new OptimizeCssAssetsPlugin({
        assetNameRegExp:/\.css$/g,
        cssProcessor:require("cssnano"),
        cssProcessorPluginOptions:{
          preset:['default', { discardComments: { removeAll:true } }]
        },
        canPrint:true
      })
    ],
      //............,
   }
  //............,
});

css的压缩处理器默认使用cssnano,压缩时会移除注释。


再度运行

npm run build

看看效果把。


配置开发环境

和生产环境不同,开发环境下,我不希望会打包出这么多的js和css,我希望1个js文件就行。
压缩会让我调试困难,而且我希望能有热更新的功能,不要每次都去run build。
这些要求可以通过配置我们的webpack.prod.config.js文件来实现。


安装devServer开发环境

webpack给我们提供了devServer开发环境,在终端输入以下代码:

npm install webpack-dev-server --save-dev

将下面代码添加到 webpack.dev.config.js 中:

const path = require('path');
const { merge } = require('webpack-merge');
const common = require('./webpack.common.config.js');
const webpack = require('webpack');

module.exports = merge(common, {
  mode: 'development',
  devServer: {
    contentBase: path.resolve(__dirname, '../dist'),
    open: true,
    port: 9000,
    compress: true,
    https: true,
    disableHostCheck: true,
    hot: true
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin()
  ]
});

通过上面的代码,我们配置了一个webserver服务器,利用HotModuleReplacementPlugin来实现热更新。


截止发稿,最新的webpack-dev-server版本为3.11.0,该版本存在bug,需要我们添加
disableHostCheck: true 来回避。


修改我们的package.json,加入启动代码:

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack --config ./config/webpack.prod.config.js",
    "start": "webpack-dev-server --inline --config ./config/webpack.dev.config.js"
  },

至此,build命令对应生产环境,start命令对应开发环境。
先别急着启动,还有什么被遗忘了?


加载css

之前说过,webpack不能处理css。我们需要在 webpack.dev.config.js 中加载css处理模块。

生产环境中我们把css独立分离了出去,在开发环境中再这么做的话,意味着我们要在kintone里面填写2个url,js一个,css一个,有点麻烦。
利用 style-loader 可以解决上述问题。


在终端中输入:

npm install style-loader --save-dev

在 webpack.dev.config.js 中加入下面代码:

module.exports = merge(common, {
  //...
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [ 
          'style-loader', 
          'css-loader' 
        ]
      }
    ]
  },
  //...
});

css-loader计算生成css,style-loader则会把生成的css放入style标签下。
loader是有顺序的,webpack肯定是先将所有css模块依赖解析完得到计算结果再创建style标签。因此应该把style-loader放在css-loader的前面。


在终端运行

npm run start

顺利启动服务后,把 https://localhost:9000/js/bundle.js 填到kintone app里面看看效果把。


生成ssl证书

dev-server启动后,那个大大的非安全证书认证是不是挺让人不爽的?为此我们需要利用mkcert来解决证书问题。

在终端中依次输入

mkcert -install
# 根据要求输入Password
mkcert localhost 127.0.0.1 ::1

之后你会发现在你的项目根目录下生成了localhost+2-key.pem和localhost+2.pem。

在 webpack.dev.config.js 中加入下面代码:

const fs = require('fs');

module.exports = merge(common, {
  //...
  devServer: {
    //...
    key: fs.readFileSync(path.resolve(__dirname, '../localhost+2-key.pem')),
    cert: fs.readFileSync(path.resolve(__dirname,'../localhost+2.pem')),
    //...
  }
  //...
});

在终端再次运行

npm run start

你会发现烦人的非安全警告消失了。


其他

限于篇幅,@babel/plugin-transform-runtime、Less、Sass、图片加载、Eslint、TypeScript支持等功能不会在本篇中列出,感兴趣的朋友可以先自己研究。