prefacio
En el entorno front-end actual, la ingeniería front-end se deriva gradualmente de la anterior html
, css
y de lo simple a lo complejo, volviéndose cada vez más complicado. El más complicado nos pertenece . Los ingenieros han aparecido para especializarse en la configuración .js
webpack
webpack
webpack
Hay decenas de miles de herramientas de empaque de ingeniería de front-end, quién es la tuya NO.One
.
Este artículo implementa una herramienta de empaquetado simple javaScript
, que no implica no javaScript
empaquetado, como: css
, html
, 静态文件
etc.
ambiente
Nuestro ordenador necesita estar equipado con node
un entorno.
Piezas necesarias Herramientas
fs
fs
Los módulos se usan para manipular archivos. Este módulo solo node
se puede usar en el entorno, no en el navegador.
camino
path
Un módulo es un módulo para trabajar con archivos y rutas de archivos.
@babel/parser
@babel/parser
El módulo se utiliza para recibir código fuente, realizar análisis léxico, análisis de sintaxis y generar 抽象语法树(Abstract Syntax Tree)
, denominado ast
.
@babel/atravesar
@babel/traverse
Los módulos para actualizaciones transversales que usamos @babel/parser
generaron AST
. Operar en nodos específicos.
@babel/núcleo
@babel/core
transform
El código utilizado para compilar nuestro módulo en el módulo se puede convertir a la primera versión del código para que sea más compatible. En este artículo transformFromAstSync
, el efecto es el mismo.
@babel/preset-env
@babel/preset-env
es规范
Un módulo es un módulo de herramienta preestablecido para un entorno inteligente, que nos permite escribir código utilizando la última versión , sin tener que administrar todos los tediosos detalles de qué transformaciones de sintaxis se requieren para el entorno de destino.
escribir un paquete
Combinaremos los módulos de herramientas anteriores para escribir una js
herramienta de empaquetado propia. Si necesitamos empaquetar elementos que no son de js
contenido, necesitamos otras herramientas de módulo.
Lo que este artículo logra solo se puede empaquetar js
, hagámoslo juntos.
Hay un pequeño detalle para recordar a mis amigos
El contenido empaquetado en mac
el sistema es el mismo que imprime el terminal, pero la descarga está oculta.终端
window
mac
Construcción del entorno
Primero necesitamos crear una nueva carpeta, luego ejecutar npm init
/ pnpm init
para generar package.json
archivos. Luego instale los módulos anteriores.
//npm
npm i @babel/core @babel/parser @babel/preset-env @babel/traverse
//pnpm
pnpm i @babel/core @babel/parser @babel/preset-env @babel/traverse
main.js
archivo nuevo
Creamos un nuevo main.js
archivo para escribir nuestro código, por supuesto, puede usar otros nombres de archivo.
nuevo src
directorio
Aquí necesitamos crear un nuevo src
directorio para contener el código que escribimos.
En src
el directorio, creamos dos nuevos js
archivos, de la siguiente manera:
// foo.js
const foo = () => {
console.log('我是foo');
}
export {
foo
}
Vamos a crear un nuevo index.js
archivo, importarlo foo.js
y index.js
ejecutar foo
el método en el método. Luego realizamos index
defensas.
// index.js
import {
foo } from "./foo.js"
const index = () => {
foo()
console.log('我是index');
for(let item of [1, 2]){
console.log(item);
}
}
index()
escribirmain.js
Ahora es el main.js
momento de que empecemos a escribir.
Introduce el módulo de la herramienta que solo necesitábamos, aquí necesitamos usar require
la forma de referencia,
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
// 这里需要添加.default来引入@babel/traverse模块,因为require不支持default的导出,故添加此内容
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')
leer el contenido del archivo
Agregamos readFile
un método para leer el contenido del archivo que escribimos js
, aquí usamos el método fs
en el módulo readFileSync
y establecemos el formato del contenido en utf-8
.
Luego pasamos index.js
la ruta al archivo y ejecutamos el método.
const readFile = (fileName) => {
const content = fs.readFileSync(fileName, 'utf-8')
console.log(content);
}
readFile('./src/index.js')
Ejecutamos en la terminal node main.js
.
Vemos que la terminal imprime index.js
el contenido de nuestro archivo. El contenido es exactamente el mismo que el nuestro index.js
, la diferencia es que la cadena impresa es una cadena con un \n
carácter de nueva línea agregado dentro.
import {
foo } from "./foo.js"
const index = () => {
foo()
console.log('我是index');
for(let item of [1, 2]){
console.log(item);
}
}
index()
Generar ast
un árbol de sintaxis a partir del contenido del archivo obtenido
Arriba tenemos el código que escribimos, ahora necesitamos @babel/parser
generar el nuestro a través de la herramienta ast
.
Lo agregamos readFile
en el método @babel/parser
y lo configuramos sourceType
en module
. Y aún ejecutar en la terminal node main.js
.
const ast = parser.parse(content, {
sourceType: 'module'
})
console.log(ast);
El resultado impreso es el siguiente, que es un node
nodo de formato, y el contenido de nuestro código está en program
-> body
.
Node {
type: 'File',
start: 0,
end: 165,
loc: SourceLocation {
start: Position {
line: 1, column: 0, index: 0 },
end: Position {
line: 12, column: 7, index: 165 },
filename: undefined,
identifierName: undefined
},
errors: [],
program: Node {
type: 'Program',
start: 0,
end: 165,
loc: SourceLocation {
start: [Position],
end: [Position],
filename: undefined,
identifierName: undefined
},
sourceType: 'module',
interpreter: null,
body: [ [Node], [Node], [Node] ],
directives: []
},
comments: []
}
@babel/traverse
Actualizar nuestro recorrido usandoast
Aquí usamos @babel/traverse
la herramienta para iterar sobre lo que acabamos de generar ast
.
En este entorno, necesitamos crear un nuevo dependencies
objeto llamado , que se usa para instalar ast
las dependencias que hemos procesado.
Simplemente pasaremos yast
agregaremos un parámetro formal para recibir la ruta de cada archivo para una de las funciones.option
ImportDeclaration
const dependencies = {
}
traverse(ast, {
ImportDeclaration: ({
node }) => {
}
})
Manejamos path
rutas a nuestros archivos a través de módulos.
const dirName = path.dirname(fileName)
Necesitamos hacer un procesamiento adicional con nuestros nombres de archivo y rutas. Y regex para reemplazar las barras invertidas.
const dir = './' + path.join(dirName, node.source.value).replace('\\', '/')
En el código anterior node.source.value
están todos los nombres de archivo y las rutas que obtuvimos ast
según.
Almacenamos la ruta del archivo que obtuvimos dependencies
en el objeto.
dependencies[node.source.value] = dir
Finalmente ejecutamos en la terminal node main.js
e imprimimos nuestro dependencies
objeto. El contenido impreso es consistente con la ruta del archivo que necesitamos compilar.
{
'./foo.js': './src/foo.js' }
@babel/core
Transcodificar nuestro código usando
Aquí necesitamos usar la solución @babel/core
en la herramienta transform
para traducir nuestro código, para que nuestro código también pueda ejecutarse normalmente en la primera versión del navegador.
Aquí usamos lo último api
transformFromAstSync
para transpilar nuestro código.
transformFromAstSync
Qué hace: vuelve a compilar la transcodificación que acabamos de modificar ast
en nuestro código.
Solo necesitamos su código transformado, no necesitamos nada más, así que desestructuramos su resultado para obtener solo su código.
const {
code } = babel.transformFromAstSync(ast, null, {
})
Necesitamos usarlo aquí @babel/preset-env
para degradar nuestro código, es decir, se escribe la nueva versión de la especificación que usamos y debemos transferirla de nuevo a la versión anterior de la especificación. Si no lo manejamos aquí, nuestro código no lo manejará y la salida será como es.
Entonces necesitamos agregarle una presets
propiedad y ponerla en nuestra @babel/preset-env
herramienta. Aquí modules
configuramos la propiedad false
para permitirle generar esm
el código formateado.
Otras extensiones de propiedad: commonjs
, amd
, umd
, systemjs
,auto
const {
code } = babel.transformFromAstSync(ast, null, {
presets: [
[
"@babel/preset-env",
{
modules: false
}
]
]
})
Lo ejecutamos en la terminal node main.js
, y el contenido impreso es el siguiente:
import {
foo } from "./foo.js";
var index = function index() {
foo();
console.log('我是index');
for (var _i = 0, _arr = [1, 2]; _i < _arr.length; _i++) {
var item = _arr[_i];
console.log(item);
}
};
index();
El código completo del método readFile:
const readFile = (fileName) => {
const content = fs.readFileSync(fileName, 'utf-8')
const ast = parser.parse(content, {
sourceType: 'module'
})
const dependencies = {
}
traverse(ast, {
ImportDeclaration: ({
node }) => {
const dirName = path.dirname(fileName)
const dir = './' + path.join(dirName, node.source.value).replace('\\', '/')
dependencies[node.source.value] = dir
}
})
const {
code } = babel.transformFromAstSync(ast, null, {
presets: [
[
"@babel/preset-env",
{
modules: false
}
]
]
})
return {
fileName,
dependencies,
code
}
}
Ha degradado con éxito nuestro código y devolvemos nuestro , 文件名/文件路径
, 依赖关系(dependencies)
, para nuestro uso posterior.代码(code)
return
Escribir un generador de dependencia
Necesitamos crear un nuevo createDependciesGraph
método llamado , que recopilará las dependencias de nuestros archivos. Agregue un parámetro formal para recibir el nombre de archivo que le pasamos.
const createDependciesGraph = entry => {
}
Cree una graphList
matriz nombrada para contener el valor de retorno de nuestro readFile
método return
.
const graphList = [readFile(entry)]
Necesitamos procesamiento recursivo aquí graphList
para evitar múltiples dependencias en él.
for(let i = 0;i < graphList.length; i++){
}
Necesitamos organizar cada elemento en el bucle, por lo que declaramos que uno item
se mantiene.
const item = graphList[i]
También necesitamos almacenar temporalmente las dependencias de cada elemento aquí.
const {
dependencies } = item
Aquí necesitamos agregar un juicio.Si hay una relación de dependencia, continuamos ciclando su capa de dependencia nuevamente y la insertamos, graphList
para anidar recursivamente el ciclo y leer el contenido del archivo hasta que finalice el ciclo.
if(dependencies){
for(let j in dependencies){
graphList.push( readFile( dependencies[j] ) )
}
}
Código completo para esta parte:
const createDependciesGraph = entry => {
const graphList = [readFile(entry)]
for(let i = 0;i < graphList.length; i++){
const item = graphList[i]
const {
dependencies } = item
if(dependencies){
for(let j in dependencies){
graphList.push(
readFile(dependencies[j])
)
}
}
}
console.log(graphList);
}
Imprimimos lo procesado graphList
, terminal input node main.js
, el resultado es el siguiente:
[
{
fileName: './src/index.js',
dependencies: {
'./foo.js': './src/foo.js' },
code: import {
foo } from "./foo.js";
var index = function index() {
foo();
console.log('我是index');
for (var _i = 0, _arr = [1, 2]; _i < _arr.length; _i++) {
var item = _arr[_i];
console.log(item);
}
};
index();
},
{
fileName: './src/foo.js',
dependencies: {
},
code: var foo = function foo() {
console.log('我是foo');};export {
foo };
}
]
Peinado de capa de relaciones
Hemos visto que nuestra capa de relación se ha impreso por completo en este momento, y debemos resolverlo ahora.
Creamos un nuevo objeto para contener nuestra capa de relación peinada.
const graph = {
}
Aquí recorremos graphList
la matriz y graph
escribimos nuestra capa de dependencia detallada en ella.
for(let item of graphList){
const {
dependencies, code} = item
graph[item.fileName] = {
dependencies,
code
}
}
Imprimamos el contenido ordenado en este momento, todavía escribiendo en la terminal node main.js
:
{
'./src/index.js': {
dependencies: {
'./foo.js': './src/foo.js' },
code: import {
foo } from "./foo.js";
var index = function index() {
foo();\n' +
console.log('我是index');
for (var _i = 0, _arr = [1, 2]; _i < _arr.length; _i++) {
var item = _arr[_i];
console.log(item);
}
};
index();
},
'./src/foo.js': {
dependencies: {
},
code: var foo = function foo() {
console.log('我是foo');};export {
foo };
}
}
Necesitamos devolver la capa de relación ordenada return
para nuestro uso posterior.
return graph
El código completo del método createDependenciesGraph:
const createDependciesGraph = entry => {
const graphList = [readFile(entry)]
for(let i = 0;i < graphList.length; i++){
const item = graphList[i]
const {
dependencies } = item
if(dependencies){
for(let j in dependencies){
graphList.push(
readFile(dependencies[j])
)
}
}
}
const graph = {
}
for(let item of graphList){
const {
dependencies, code} = item
graph[item.fileName] = {
dependencies,
code
}
}
return graph
}
Primero creemos un método de administración de archivos
En este paso, primero creamos un método de administración de carpetas, que se usa para borrar el directorio y volver a crearlo cada vez que empacamos.
Declaramos un rmdir
método llamado, que administra nuestra carpeta de directorio de empaque
const rmdir = async () => {
}
Démosle return
una new Promise
instancia interna, en cuanto a la razón, la usaremos más adelante y la entenderemos, lo cual es conveniente para que la usemos más adelante.
const rmdir = async () => {
return new Promise(async (resolve, reject) => {
})
}
Declaramos uno err
para obtener errores cuando operamos carpetas y archivos,
let err = null
Leemos el estado de nuestra carpeta de embalaje actual, la vaciamos y la borramos si existe. recursive
Indica si se debe eliminar la carpeta, true
que es eliminar.
const isDir = fs.existsSync('dist')
if(isDir){
fs.rmdir('dist', {
recursive: true}, error => {
if(error){
err = error
}
})
}
Aquí hacemos un juicio equivocado, y cuando err
es cierto lanzamos un error y return
salimos.
if(err){
reject(err)
return
}
Aquí lo usamos setTimeout
para retrasar la notificación de éxito para evitar eliminar carpetas y crear carpetas al mismo tiempo, lo que resulta en una creación fallida.
setTimeout(()=>{
resolve()
}, 1000)
rmdir código completo:
const rmdir = async () => {
return new Promise(async (resolve, reject) => {
let err = null
const isDir = fs.existsSync('dist')
if(isDir){
fs.rmdir('dist', {
recursive: true}, error => {
if(error){
err = error
}
})
}
if(err){
reject(err)
return
}
setTimeout(()=>{
resolve()
}, 1000)
})
}
método generador de código
Aquí estoy usando esbuild
un modo de salida empaquetado, es decir, los archivos empaquetados se generan sincrónicamente de acuerdo con las reglas del directorio cuando se creó el proyecto.
Aquí creamos un generateCode
método llamado , hacemos nuestra llamada de entrada de generación de código y escribimos el archivo generado.
const generateCode = entry => {
}
Llame al método dentro de él createDependciesGraph
y entry(打包的入口文件)
páselo. Y declarar codeInfo
aceptar.
const codeInfo = createDependciesGraph(entry)
Podemos imprimirlo primero para ver codeInfo
cómo se ve.
{
'./src/index.js': {
dependencies: {
'./foo.js': './src/foo.js' },
code: import {
foo } from "./foo.js";
var index = function index() {
foo();
console.log('我是index');
for (var _i = 0, _arr = [1, 2]; _i < _arr.length; _i++) {
var item = _arr[_i];
console.log(item);
}
};
index();
},
'./src/foo.js': {
dependencies: {
},
code: var foo = function foo() {
console.log('我是foo');};export {
foo };
}
}
Ahora creamos carpetas y escribimos archivos basados en dependencias.
En este momento, hemos alineado rmdir
el método justo ahora, llamamos rmdir
al método y .then
escribimos nuestro proceso de creación de archivos en él. Esta es la razón por la que rmdir
se devolvió uno cuando Promise
se creó hace un momento. Después de eliminar y vaciar el directorio del paquete, se crean la carpeta y los archivos del paquete, de modo que evitamos el problema de crear y eliminar carpetas y archivos al mismo tiempo.
rmdir().then(()=>{
})
Ahora vamos a crear la carpeta del directorio de empaquetado.
fs.mkdir('dist', () => {
})
Hacemos un bucle de nuestras dependencias en la devolución de llamada para crear la carpeta empaquetada, porque codeInfo
es un objeto, no podemos usarlo for..of...
y usamos el es6
nuevo en for..in..
.
for(let key in codeInfo){
}
Aquí creamos una carpeta con el mismo nombre y escribimos el código especificado en un archivo con el mismo nombre. Aquí obtenemos split
el nombre del archivo actual por el método que usamos y tomamos el último elemento, porque el último elemento es nuestro nombre de archivo.
let value = key.split('/')
value = value[value.length - 1]
Creamos un precio de venta basado en el nombre del archivo obtenido anteriormente y escribimos el código correspondiente en el archivo.
let value = key.split('/')
value = value[value.length - 1]
fs.writeFile(`./dist/${
value}`, codeInfo[key]['code'], [], () => {
})
código completo del método generateCode
const generateCode = entry => {
const codeInfo = createDependciesGraph(entry)
console.log(codeInfo);
rmdir().then(()=>{
fs.mkdir('dist', () => {
for(let key in codeInfo){
let value = key.split('/')
value = value[value.length - 1]
fs.writeFile(`./dist/${
value}`, codeInfo[key]['code'], [], () => {
})
}
})
})
}
Necesitamos main.js
llamar al método de nuestro generateCode
generador de código en . Necesitamos pasar el archivo de entrada del archivo del paquete mientras llamamos.
generateCode('./src/index.js')
Terminamos de escribir, ahora vamos a ejecutarlo, ingresarlo en la terminal node main.js
y permitirlo.
Encontraremos que nuestro directorio de proyectos ha generado un directorio que contiene dist
nuestros archivos.src
js
Veamos foo.js
si index.js
el archivo contiene src
el contenido del directorio. ,
foo.js
var foo = function foo() {
console.log('我是foo');
};
export {
foo };
índice.js
import {
foo } from "./foo.js";
var index = function index() {
foo();
console.log('我是index');
for (var _i = 0, _arr = [1, 2]; _i < _arr.length; _i++) {
var item = _arr[_i];
console.log(item);
}
};
index();
Verifique que los archivos empaquetados permitan
Creamos uno nuevo index.html
, e importamos los archivos dist
en el directorio index.js
.
<script src="./dist/index.js" type="module"></script>
El efecto es el siguiente:
Nuestros archivos empaquetados se pueden ejecutar normalmente.
código completo
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')
const readFile = (fileName) => {
const content = fs.readFileSync(fileName, 'utf-8')
const ast = parser.parse(content, {
sourceType: 'module'
})
const dependencies = {
}
traverse(ast, {
ImportDeclaration: ({
node }) => {
const dirName = path.dirname(fileName)
const dir = './' + path.join(dirName, node.source.value).replace('\\', '/')
dependencies[node.source.value] = dir
}
})
const {
code } = babel.transformFromAstSync(ast, null, {
presets: [
[
"@babel/preset-env",
{
modules: false
}
]
]
})
return {
fileName,
dependencies,
code
}
}
const createDependciesGraph = entry => {
const graphList = [readFile(entry)]
for(let i = 0;i < graphList.length; i++){
const item = graphList[i]
const {
dependencies } = item
if(dependencies){
for(let j in dependencies){
graphList.push(
readFile(dependencies[j])
)
}
}
}
const graph = {
}
for(let item of graphList){
const {
dependencies, code} = item
graph[item.fileName] = {
dependencies,
code
}
}
return graph
}
const generateCode = entry => {
const codeInfo = createDependciesGraph(entry)
rmdir().then(()=>{
fs.mkdir('dist', () => {
for(let key in codeInfo){
let value = key.split('/')
value = value[value.length - 1]
fs.writeFile(`./dist/${
value}`, codeInfo[key]['code'], [], () => {
})
}
})
})
}
const rmdir = async () => {
return new Promise(async (resolve, reject) => {
let err = null
const isDir = fs.existsSync('dist')
if(isDir){
fs.rmdir('dist', {
recursive: true}, error => {
if(error){
err = error
}
})
}
if(err){
reject(err)
return
}
setTimeout(()=>{
resolve()
}, 1000)
})
}
generateCode('./src/index.js')
Resumir
En este punto, nuestra JavaScript打包器
implementación simple ha terminado. La implementación de este empaquetador simple solo se usa para comprender y comprender el principio del empaquetador principal.
Nuestro empaquetador actual todavía tiene algunos defectos:
- Los archivos de directorio anidados no se pueden empaquetar.
- solo puede empaquetar
js
archivos