Skip to content

内网迁移实战(Vue 2 / webpack 4 / webpack 5 / Vue 3 + Vite) ​

本文回答一个具体问题:一个已经在联网机器上跑通的宿主项目,怎么一步步搬进没有外网的内网,还能正常 npm install 和 npm run build。

三篇文档分工不同,别混着读:

文档管什么
安装与引用总表内外网各怎么装、怎么写 import、dist/ 怎么拷,一张表速查
各环境引入对照联网环境下,你的框架该写什么配置、贴什么代码
本文从「本地能跑」到「内网能跑」,每个宿主一套按顺序的步骤
内网服务器部署服务器侧:nginx、静态资源、协同、校验、回滚

四者是叠加关系:总表用来查、本文用来走流程,本文只做「让项目在内网编译通过」这一件事,部署上线看第四篇。

0. 三套宿主的差异,先看一眼 ​

宿主走哪个入口联网侧要改的文件内网侧要额外带什么
Vue 2.7 + webpack 4(vue-cli 4 / 老 vue-cli)
0.13 起
一行 alias → dist/lark-doc.common.js只改 vue.config.js 一处(alias);子路径与样式写全 dist/ 路径lark-doc tgz、vue tgz
Vue 2.7 + webpack 4(0.12 及更早)main → dist/lark-doc.common.jsvue.config.js(alias + babel 规则)、业务组件的 import 写全路径lark-doc tgz、vue tgz、2 个 babel 插件 tgz
Vue 2.7 + webpack 5(vue-cli 5 / 原生 webpack5)module → dist/lark-doc.esm.js不用改配置,只改业务代码的 importlark-doc tgz、vue tgz
Vue 3 + Vitelark-doc/element(UMD,原生 DOM + 注入样式,无 Vue)vite.config.js(isCustomElement)、index.html 加一行 <script>只要 dist/ 目录,不用装 npm 包

判断自己是哪一档,只跑一条命令:

bash
node -p "require('webpack/package.json').version" 2>/dev/null || npx vite --version

输出 4.x → 方案 A;5.x → 方案 B;Vite → 方案 C。


1. 所有方案共用的第一步:在联网机上出包 ​

1.1 为什么必须单独出包 ​

内网连不上 registry,npm install lark-doc 会直接失败。npm/tgz 安装方式需要可解析的依赖闭包,内网可使用随依赖打包的 tgz;静态 Element 方式则直接部署完整 dist/,无需安装 npm 包。当前 lark-doc 不要求 Vue runtime;Vue2 宿主自身的框架依赖由宿主管理。

scripts/pack-intranet.sh 就是干这个的,它顺手修掉三类只有老 npm / Windows 才暴露的坑(详见内网服务器部署 · 3.4)。

1.2 三条命令 ​

bash
cd apps/lark-doc
node -v          # 必须是 v14.21.3
npm -v           # >= 8.19.4
npm run check    # 全绿再往下走
bash scripts/pack-intranet.sh --version 0.12.0

dist/ 已是最新时可跳过约 4 分钟的构建:

bash
bash scripts/pack-intranet.sh --version 0.12.0 --skip-build

1.3 产出什么、带走什么 ​

dist-intranet/ 里会有这些文件,按你的宿主挑:

文件方案 A方案 B方案 C说明
lark-doc-<ver>-intranet.tgz✅✅—主包,含依赖闭包
vue-<ver>-intranet.tgz内网没有 vue@2.7 时同左—重打过的 vue(去掉 macOS tar 头)
lark-doc-<ver>-node-modules.zip兜底兜底—npm 彻底装不上时才用
INSTALL-内网.md / sha256.txt✅✅✅说明 + 校验
dist/(从 apps/lark-doc/dist 拷)需要时用 element 时需要✅ 必须整目录,别只挑一个 .js

1.4 搬运前先验一遍 ​

bash
cd dist-intranet && sha256sum -c sha256.txt

拷进内网(U 盘 / scp / 跳板机)后再验一次,传输出错比装错包更常见。


2. 方案 A:Vue 2.7 + webpack 4(vue-cli 4) ​

0.13 起本节只剩一处配置:一行 alias

webpack 4 不读 exports,且 0.13 起包里不再有 browser 字段,所以它会落到 module → esm.js(保留现代语法,不承诺 webpack4 可解析)。在 vue.config.js 里把主入口显式指到 CJS 档即可; CJS 档已随 UMD / element 一起降到 ES2018,所以:

  • 不用 装那两个 babel 插件(@babel/plugin-proposal-optional-chaining / -nullish-coalescing-operator),也就不用为它们打 tgz
  • 不用 设 window.__LARK_DOC_PUBLIC_PATH__ —— CJS 以相对 require 加载兄弟 chunk,不用 script URL 基址
  • 只剩两件事:alias 一行,以及子路径 / 样式写全(lark-doc/dist/lark-doc-element.umd.min.js、lark-doc/dist/lark-doc.css)
js
// vue.config.js —— 0.13+ 联网侧唯一的改动
module.exports = {
  configureWebpack: {
    resolve: { alias: { 'lark-doc$': 'lark-doc/dist/lark-doc.common.js' } },
  },
}
js
// src/main.js —— 0.13+ 的引入写法;样式与子路径都要写全 dist 路径
import { LarkDoc } from 'lark-doc'
import 'lark-doc/dist/lark-doc.css'

下面 2.1~2.7 是 0.12 及更早版本的完整流程,版本锁在旧版时照抄;已升到 0.13 可以只读 2.6(vendor 打包)与 2.7(验收),跳过配置与插件部分。

关键顺序:先在联网机上改好、跑通,再把改好的项目搬进去——在内网排编译错,成本是联网时的十倍。

2.1 联网机 · 第 1 步:确认宿主版本 ​

bash
node -p "require('webpack/package.json').version"    # 期望 4.x
node -p "require('vue/package.json').version"        # 期望 2.7.x

vue-cli 4 内部固定 webpack 4.x;vue-cli 5 是 5.x,看到 5.x 就别抄本节配置,直接去方案 B。

2.2 联网机 · 第 2 步:改 vue.config.js(两处,缺一不可) ​

js
// vue.config.js(vue-cli 4 + Vue 2.7)
module.exports = {
  productionSourceMap: false,
  configureWebpack: {
    resolve: {
      alias: {
        // ① webpack4 的 mainFields 里 module 优先,会命中 esm 产物,
        //    而 esm 产物含 import.meta(KaTeX 字体地址),acorn 6.4.2 解析不了。
        //    显式把入口指到 main 的 CJS 单文件。
        'lark-doc$': 'lark-doc/dist/lark-doc.common.js',
      },
    },
  },
  chainWebpack(config) {
    // ② CJS 产物里还有 ?. / ??,webpack4 默认 babel 不管 node_modules,
    //    要对这一个文件单独降级。configFile / babelrc 必须关掉,
    //    否则会被项目根 babel.config.js 二次转译,破坏产物的模块语义。
    config.module.rules.unshift({
      test: /lark-doc[\\/]dist[\\/]lark-doc(\.common|\.umd\.min)\.js$/,
      use: {
        loader: 'babel-loader',
        options: {
          configFile: false,
          babelrc: false,
          sourceType: 'script',
          compact: false,
          plugins: [
            '@babel/plugin-proposal-optional-chaining',
            '@babel/plugin-proposal-nullish-coalescing-operator',
          ],
        },
      },
    })
  },
}

这两条规则必须排在默认 js 规则之前(rules.unshift 已保证);写成 rules.push 等于没写。

2.3 联网机 · 第 3 步:业务代码里三处写全路径 ​

webpack 4 不读 package.json 的 exports 字段,所有子路径都要落到真实文件:

js
// ① 样式:不能写 'lark-doc/style.css'
import 'lark-doc/dist/lark-doc.css'
// ② 主入口:由 2.2 的 alias 兜住,这里照常写包名
import { LarkDoc } from 'lark-doc'
// ③ 如果用 Web Component:也是全路径
import 'lark-doc/dist/lark-doc-element.umd.min.js'

2.4 联网机 · 第 4 步:本地先 build 一次 ​

bash
npm run build

产物里能 grep 到 lark-doc 的内容,说明入口确实走的是 CJS:

bash
grep -l "lark-doc" dist/js/*.js | head

这一步不通过,别往下走。

2.5 联网机 · 第 5 步:把 babel 插件也打成 tgz(最容易漏的一步) ​

2.2 里那两个 @babel/plugin-proposal-* 是 devDependencies,内网装不上就编译不过。在联网机上先落成 tgz:

bash
mkdir -p vendor && cd vendor
npm pack @babel/plugin-proposal-optional-chaining@7
npm pack @babel/plugin-proposal-nullish-coalescing-operator@7

2.6 内网 · 第 6 步:铺 vendor/ 并安装 ​

拷进去的目录长这样:

你的项目/
└── vendor/
    ├── lark-doc-0.12.0-intranet.tgz
    ├── vue-2.7.14-intranet.tgz
    ├── babel-plugin-proposal-optional-chaining-7.x.x.tgz
    └── babel-plugin-proposal-nullish-coalescing-operator-7.x.x.tgz

package.json 改成文件引用(比 npm i ./xxx.tgz 更好,npm ci 也能复现):

jsonc
{
  "dependencies": {
    "vue": "file:./vendor/vue-2.7.14-intranet.tgz",
    "lark-doc": "file:./vendor/lark-doc-0.12.0-intranet.tgz"
  },
  "devDependencies": {
    "@babel/plugin-proposal-optional-chaining": "file:./vendor/babel-plugin-proposal-optional-chaining-7.x.x.tgz",
    "@babel/plugin-proposal-nullish-coalescing-operator": "file:./vendor/babel-plugin-proposal-nullish-coalescing-operator-7.x.x.tgz"
  }
}

然后:

bash
npm install --no-audit --no-fund

两个不要

  • 不要加 --legacy-peer-deps:npm 6 不认识这个参数,直接 unknown option 退出。
  • 不要装 @tiptap/vue-2:tgz 里已带,再装一份变双实例,症状是 Vue packages version mismatch 或白屏。

2.7 内网 · 第 7 步:冒烟三条 ​

bash
node -p "require('lark-doc/package.json').version"          # 0.12.0
node -e "const m=require('lark-doc');console.log(typeof m.LarkDoc, Object.keys(m).length)"
# 期望:function 27
node -p "require('@babel/plugin-proposal-optional-chaining/package.json').version"

2.8 内网 · 第 8 步:build 并验收 ​

bash
npm run build

打开产物页面,控制台应该满足:无 Unexpected token、无 Cannot read properties of undefined、编辑区能输入、工具栏图标正常。缓存与字体的验收清单见内网服务器部署 · 8.4。

2.9 方案 A 报错对照 ​

报错根因改哪里
Module parse failed: Unexpected tokenalias 没生效,命中了 esm 产物(import.meta)2.2 的 ① 是否真的写进 configureWebpack.resolve.alias
Module parse failed 但报在 ?. / ?? 处babel 规则没匹配上或排在后面2.2 的 ② 必须 unshift;正则要能匹配 node_modules/lark-doc/dist/...
Cannot read properties of undefined (reading 'call'),白屏0.11.2 之前的 lib 内层 runtime 标识符冲突升级包,宿主侧无法绕过
Failed to resolve entry for 'lark-doc/style.css'webpack4 不读 exports改成 lark-doc/dist/lark-doc.css
Can't resolve '@babel/plugin-proposal-*'插件没带进内网回到 2.5 补 tgz

3. 方案 B:Vue 2.7 + webpack 5(vue-cli 5 / 原生 webpack5) ​

3.1 联网机 · 第 1 步:什么配置都不用改 ​

webpack 5 认 exports 字段,import { LarkDoc } from 'lark-doc' 直接命中 dist/lark-doc.esm.js,懒区块由宿主自己切分。不要把方案 A 的 alias 和 babel 规则抄过来——多一层编译,还容易把 esm 语义弄坏。

对照一下「不需要」的清单:

js
// vue.config.js(vue-cli 5)—— 与 lark-doc 相关的部分全是"不需要"
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  // 不需要 alias(webpack5 默认 mainFields 含 module)
  // 不需要 transpileDependencies(esm 产物已按 browserslist 编好)
  // 不需要改 rules(webpack5 自己能吃 import.meta)
  // 不需要引 CSS(esm 产物随 JS 注入 <style>)
})

业务代码只需要一行:

js
import { LarkDoc } from 'lark-doc'

3.2 联网机 · 第 2 步(可选):用了 Web Component 才需要拷资源 ​

如果宿主里还有 <lark-doc> 那类页面(import 'lark-doc/element'),element 产物的 publicPath: 'auto' 会按宿主的 chunk 目录(vue-cli 默认 js/)推导资源地址,所以字体和按需区块必须落在 js/ 下:

js
// vue.config.js 追加
const path = require('path')
const larkDocSource = path.dirname(require.resolve('lark-doc/package.json'))

module.exports = {
  chainWebpack(config) {
    config.plugins.delete('prefetch')      // 别让 KaTeX / DOCX 首屏被预取
    config.plugin('copy-lark-doc-vendor').use(require('copy-webpack-plugin'), [{
      patterns: [
        { from: path.join(larkDocSource, 'dist/fonts'), to: 'js/fonts' },
        { from: path.join(larkDocSource, 'dist/lark-doc-element.umd.min*.js'), to: 'js' },
      ],
    }])
    config.module.rule('js').exclude.add(path.join(larkDocSource, 'src/assets'))
  },
}

只用主入口(LarkDoc 类 / LarkDocEditor 组件)的话,这一节整段跳过。

一句话判断

浏览器 404 的路径前缀是什么,就往那个目录拷。报 js/fonts/KaTeX_Main-Regular.woff2 404,就补 dist/fonts → js/fonts;报 js/lark-doc-element.umd.min.math.js 404,就补懒区块。

3.3 内网 · 第 3 步:装包 ​

与 2.6 完全一样,只是不需要那两个 babel 插件 tgz:

jsonc
{
  "dependencies": {
    "vue": "file:./vendor/vue-2.7.14-intranet.tgz",
    "lark-doc": "file:./vendor/lark-doc-0.12.0-intranet.tgz"
  }
}
bash
npm install --no-audit --no-fund
npm run build

3.4 方案 A 与 B 的差别,只记三行 ​

webpack 4(0.13+)webpack 4(0.12-)webpack 5
入口一行 alias → main(CJS 主文件 + require 兄弟 chunk)main(CJS,8.6 MB)+ babel 降级module(ESM,首屏 gzip ≈ 446 KB)
宿主配置1 处(alias);子路径与样式写全 dist/alias + babel 规则,两处无
额外依赖无2 个 babel 插件无

4. 方案 C:Vue 3 + Vite ​

4.1 Vue3 可以直接使用默认入口 ​

当前默认、Element 和 legacy 产物均无 Vue 运行时依赖,也没有 vue 或 @tiptap/vue-2 peer。Vue3 / React / 原生宿主都可正常 npm i lark-doc,无需 --force 或 --legacy-peer-deps,随后在客户端挂载 new LarkDoc()。

下面保留的是另一条可选路线:静态托管 Element UMD,使用 <lark-doc> 标签。它使用同一份 DOM renderer,样式随 JS 注入,并非内联 Vue2 的避让方案。

4.2 推荐路线:不装包,把 dist/ 当静态资源引 ​

这条路线没有 peer 冲突、不需要内网 npm、也不经过打包器二次处理,是内网最稳的一条。

第 1 步(联网机):按第 1 节出包,然后拷 dist/。

第 2 步(内网):把整个 dist/ 目录放进 Vite 的 public/:

你的 vue3 项目/
├── public/
│   └── vendor/lark-doc/
│       ├── lark-doc-element.umd.min.js
│       ├── lark-doc-element.umd.min.math.js
│       ├── lark-doc-element.umd.min.office.js
│       ├── lark-doc-element.umd.min.highlight.js
│       ├── lark-doc-element.umd.min.mermaid.js
│       └── fonts/            ← 28 个 KaTeX 字体
├── index.html
└── vite.config.js

别只拷那一个 .js

math / office / highlight / mermaid 是运行时的动态加载目标,fonts/ 是公式渲染的字体。少一个就是「编辑器能开、点公式 404」。整目录一起放,同位放置——这也是 publicPath: 'auto' 能自己算对地址的前提。

第 3 步(内网):index.html 加一行(放在 <div id="app"> 之前的 head 里):

html
<!-- 根路径部署 -->
<script src="/vendor/lark-doc/lark-doc-element.umd.min.js"></script>

<!-- 子路径部署(vite.config.js 里 base: '/wiki/')时改用这个 -->
<script src="%BASE_URL%vendor/lark-doc/lark-doc-element.umd.min.js"></script>

样式不用另外引,产物自己注入 <style>。

第 4 步(内网):vite.config.js 告诉 Vue 编译器 <lark-doc> 是自定义元素:

js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  base: '/',            // 子路径部署改成 '/wiki/',并同步第 3 步的写法
  plugins: [
    vue({
      template: {
        compilerOptions: {
          // 不配 → 控制台刷 Failed to resolve component: lark-doc
          isCustomElement: (tag) => tag === 'lark-doc',
        },
      },
    }),
  ],
})

第 5 步(内网):业务组件里直接用标签,对象型能力走 options:

vue
<template>
  <lark-doc ref="el" title="会议纪要" toolbar-keys="history,headings,marks,insert" />
</template>

<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'

const el = ref(null)
onMounted(() => {
  el.value.options = {
    uploader: { upload: async (file) => (await upload(file)).url },
    ai: { enabled: false },
  }
  el.value.addEventListener('lark-doc-change', (e) => console.log(e.detail.payload.json))
})
onBeforeUnmount(() => el.value && el.value.destroy())
</script>

第 6 步(内网):构建并检查资源是否跟着走。

bash
npm run build
ls dist/vendor/lark-doc/         # public/ 下的东西原样复制到产物根
ls dist/vendor/lark-doc/fonts | wc -l    # 期望 28

4.3 备选路线:一定想写 import 语句 ​

Vue 3 项目里装 tgz 要显式跳过 peer 校验(内网 npm 8+ 才有这个参数;npm 6 只会 warn,不用加):

bash
npm i ./vendor/lark-doc-0.12.0-intranet.tgz --legacy-peer-deps --no-audit --no-fund
js
import 'lark-doc/element'   // 自动注册 <lark-doc>

这条路的代价:element 产物被宿主打包器再打一遍,publicPath: 'auto' 推出来的是宿主的 chunk 目录(Vite 默认 assets/),字体和按需区块要手动拷到 assets/ 下,否则 404。排查口诀与 3.2 相同。除非团队强制统一走 npm 依赖,否则用 4.2。

4.4 方案 C 三个常见卡点 ​

现象原因处理
标签渲染成空、控制台 Failed to resolve component: lark-doc没配 isCustomElement4.2 第 4 步
编辑器能开,点公式 / 图表 / 导入 Word 报 404dist/ 没整目录拷,或没放在同一层4.2 第 2 步
子路径部署后所有资源 404base 与 script src 写法不匹配4.2 第 3、4 步一起改

5. 内网构建的五个通病(三套宿主都适用) ​

#症状原因处理
1The engine "node" is incompatible宿主 engines 锁 node 14.21.3内网构建机装同版本;或确认宿主已放开 engines
2装完能用,跑一次 npm ci 全没了npm ci 会先删 node_modules/走方案 2 直铺的,禁止再跑 npm ci;要复现就用 file: 引用
3npm install 卡在 postinstall 联网下载node-sass / canvas / puppeteer 类包要在装的时候下二进制内网镜像源或 --ignore-scripts,并把二进制预先放好
4JavaScript heap out of memoryterser 并行压缩在低配构建机上爆内存NODE_OPTIONS=--max-old-space-size=4096,或关掉并行压缩
5页面白屏,Network 里 chunk 全 404宿主 publicPath / nginx 子路径不一致先对齐 publicPath 与 location,再看资源是否整目录就位

6. 三套宿主的改动清单(git diff 应该只有这些) ​

宿主联网侧改的文件内网侧新增的文件
Vue 2.7 + webpack 4(0.13+)业务组件 import(要公式/Word 再加一行 __LARK_DOC_PUBLIC_PATH__)vendor/*.tgz、package.json 的 file:
Vue 2.7 + webpack 4(0.12-)vue.config.js、业务组件 importvendor/*.tgz、package.json 的 file:、2 个 babel 插件 tgz
Vue 2.7 + webpack 5业务组件 import(用了 element 再加 vue.config.js)vendor/*.tgz、package.json 的 file:
Vue 3 + Vitevite.config.js、index.htmlpublic/vendor/lark-doc/(整目录)

超出这张表的改动,通常是踩了「抄错另一档配置」的坑——回到第 0 节重新对一遍。

7. 下一步 ​

编译通过只说明包能在内网用。上线还差三件事:静态资源托管(dist/ 整目录同位)、nginx 路由与 MIME、验收与回滚,全部在内网服务器部署;断网后的运行时行为(图片外链、AI 请求)见离线与资源本地化。